# BRC-188: User Management Protocol (UMP) Ty Everett (ty@projectbabbage.com) ## Abstract The User Management Protocol (UMP) represents a recoverable wallet account with an on-chain, spendable account descriptor. The descriptor contains encrypted copies of the account's root keys, hashes used for discovery, encrypted recovery factors, and, optionally, encrypted profiles and password derivation metadata. Any two of a presentation key, a password, and a recovery key can recover the account. Changing those factors normally spends the current descriptor and creates its successor while preserving the account's underlying keys. This document specifies the deployed CWI-style UMP formats, including mandatory compatibility with legacy PBKDF2 accounts, the version 3 KDF extension, account and profile operations, and the `tm_users` topic and `ls_users` lookup service. It explains how a Wallet Authentication Backend (WAB) can supply one factor without becoming an indispensable custodian, and how a user can recover without that backend. It also specifies first-accepted hash ownership and the spend continuity required to replace an account descriptor. Section 3.4 supplies the BRC-184 ProtoMap registration request for UMP's two wallet-administrative derivation protocols, including their exact identifiers, proposed metadata, permission boundaries, and maintenance arrangements. ## 1. Status, scope, and terminology The uppercase terms MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY have the meanings in RFC 2119 and RFC 8174. They describe conformance to this proposal, not a claim that every historical implementation already enforces every validation requirement here. UMP is an optional account-management system beneath the [BRC-100](./0100.md) wallet interface. A BRC-100 wallet need not use UMP. UMP does not replace that interface, specify application permissions, standardize every WAB authentication method, or provide a complete wallet transaction-storage backup. The account descriptor is called a **token** because it is a tracked UTXO, not because it represents money or a transferable username. This document distinguishes three things: * **Account format and operations:** the interoperable key scheme and on-chain descriptor, including historical formats that readers must understand. * **Overlay admission and discovery:** the stateful `tm_users` and `ls_users` behavior, including its ordering and replication limits. * **Implementation compatibility:** the local token/snapshot encodings and particular deployment differences documented in Sections 12 and 14. These local encodings are not an alternative on-chain envelope. An **outpoint** is a transaction ID and a zero-based output index. In UMP JSON and local token records its canonical text is `txid.index`: 64 lowercase hexadecimal characters, a literal full stop, and a decimal integer from 0 through 4294967295 with no redundant leading zero. The transaction ID uses conventional display byte order. An outpoint is not a version number. A **rendition** is one descriptor output in an account's history. A **successor** is created by a transaction spending its predecessor. A **lineage** is the resulting spend-connected history. Two outputs with an equal discovery hash are not, by that equality alone, in the same lineage. ## 2. Background and motivation Ty Everett's *Computing with Integrity* (2020), especially its key definitions and initial bootstrap process, described a password, presentation key, backup key, primary key, and privileged access key, with pairwise XOR and authenticated encryption supporting recovery. The paper's “backup key” corresponds to the recovery key here. It is a conceptual source, not the wire specification for today's UMP: its publication envelope and some key arrangements predate the spendable descriptors, derivation protocols, and KDF parameters specified below. [Original paper](https://github.com/open-cash-standards/papers/blob/master/computing-with-integrity/paper.pdf). A person ordinarily wants to open the same account on another device, change a password without changing their identity, and recover when a device, password, or service becomes unavailable. Keeping only a password with a vendor makes the vendor indispensable; keeping only one irreplaceable secret makes its loss catastrophic. UMP distributes recovery across three factors while keeping the account's cryptographic roots stable. A WAB typically associates an externally verified authentication method, such as a phone number and a one-time code, with a randomly generated presentation key. After verification it returns that key to the wallet over a protected connection. The wallet hashes it to discover the UMP token, obtains the password locally, and recovers the account. The telephone number, code, WAB database ID, and WAB URL do not appear in the UMP descriptor and are not inputs to its password KDF. The recovery key is independently retained by the user. If the password is forgotten, the presentation key and recovery key suffice. If the WAB disappears, refuses service, changes ownership, or otherwise becomes unsuitable, the password and recovery key suffice. This second path requires neither the old WAB's permission nor knowledge of its current authentication policies. A replacement client can implement it directly from this specification. “Two of three” describes access to encrypted root-key material. UMP is neither a Bitcoin `OP_CHECKMULTISIG` construction nor Shamir secret sharing. Its on-chain token has a single derived signing key. The distinction matters: password prompts and privileged operations are wallet policy, whereas spending the token is enforced by its locking script. ## 3. Keys and cryptographic primitives ### 3.1 Values and notation All keys and salts in the canonical account format are 32-byte byte strings. Preserve leading zero bytes. `||` means byte concatenation; `X XOR Y` means position-by-position XOR of equal-length byte strings. No hashing, string encoding, or integer serialization is implicit in XOR. | Symbol | Meaning | Source and retention | | --- | --- | --- | | `S` | Public password salt | Fresh cryptographically random 32 bytes at creation and each password change | | `W` | Password key | Derived from the exact password bytes and `S` using Section 4 | | `P` | Presentation key | Random factor supplied or recovered through a WAB or another user-selected mechanism | | `R` | Recovery key | Random factor generated by the client and saved independently by the user | | `A` | Root primary key | Random root used to build the ordinary/default wallet | | `V` | Root privileged key | Independent random root used for privileged operations and factor wrapping | | `H_P` | Presentation discovery hash | `SHA256(P)` | | `H_R` | Recovery discovery hash | `SHA256(R)` | `P` and `R` are secret bytes, not their public keys. Hash the raw 32 bytes once with SHA-256; do not hash their hexadecimal or Base64 representations, a phone number, or a secp256k1 public key. The salt and both hashes are public. Interpret `A` and `V` as unsigned, big-endian secp256k1 private scalars when performing wallet operations. New roots and derived profile roots MUST be nonzero and less than the curve order `n`. Generate new roots or pads if necessary. Never remove leading zero bytes before XOR or serialize a canonical root in fewer than 32 bytes. A presentation key used as a private key by an ancillary funding protocol must also satisfy that protocol's scalar requirements. ### 3.2 Authenticated encryption envelope Define `E(K, M)` as AES-256-GCM encryption with key `K`, no additional authenticated data, a fresh random **32-byte IV**, and a 16-byte authentication tag. Its serialized result is: ```text IV[32] || ciphertext[len(M)] || authenticationTag[16] ``` `D(K, C)` parses that exact layout, authenticates, and returns the plaintext, or fails. This is the existing SDK symmetric encryption layout; replacing the IV with the common 12-byte GCM IV changes the format. Do not add a salt header, padding, version prefix, or a separate tag-length byte. GCM supports the specified IV length directly; do not truncate or hash it before passing it to GCM. An encrypted canonical 32-byte key occupies 80 bytes. Every encryption operation MUST use a fresh IV, including operations encrypting different fields with the same key. Authentication failure MUST NOT return partial plaintext or trigger creation of a replacement account. Fixed IVs in Section 13 are test fixtures only. ### 3.3 Root-derived keys UMP uses the [BRC-42](../key-derivation/0042.md) and [BRC-43](../key-derivation/0043.md) derivation scheme, with the following exact, lowercase protocol names and string key IDs: | Purpose | Root | Protocol ID | Key ID | Counterparty | | --- | --- | --- | --- | --- | | Token locking key and field signature | `A` | `[2, "admin user management token"]` | `"1"` | `"self"` | | Symmetric wrapping of `P`, `W`, and `R` | `V` | `[2, "admin key wrapping"]` | `"1"` | `"self"` | These are wallet administrative protocols; applications do not gain access merely by requesting the names. The root/default profile, not the currently selected non-default profile, is used for token operations. For completeness, the self-derivation needed here can be implemented as follows. Let `G` be the secp256k1 generator, `r` a root scalar, `SEC(Q)` the 33-byte compressed encoding of a point, and `BE32(x)` its unsigned 32-byte big-endian encoding. For an invoice string `I`, compute: ```text shared = SEC((r * r mod n) * G) tweak = integer_big_endian(HMAC-SHA256(key=shared, message=UTF8(I))) d = (r + tweak) mod n ``` The UMP token private scalar is `d` with root `A` and invoice `2-admin user management token-1`; its locking public key is `SEC(d * G)`. Reject an invalid derived scalar rather than using an invalid point. For factor wrapping, use root `V` and invoice `2-admin key wrapping-1` to derive `d_V`. The wrapping key is: ```text K_wrap = BE32(x-coordinate((d_V * d_V mod n) * G)) ``` This is self ECDH between the BRC-42 child keys. It is **not** `V`, `SHA256(V)`, the compressed shared point, or SHA-256 of its x-coordinate. `E(K_wrap, factor)` is the result of a BRC-100 `encrypt` operation under the privileged root with the wrapping parameters above. ### 3.4 BRC-184 protocol registration This proposal also serves as a registration request under [BRC-184](./0184.md) and the [registry submission guide](../REGISTRY_SUBMISSIONS.md). It requests **two ProtoMap descriptions for existing protocols**, with the following exact identities and proposed display metadata: | Protocol ID | Proposed name | Proposed description | | --- | --- | --- | | `[2, "admin user management token"]` | UMP Account Token | Derives the wallet-administrative key that signs UMP account descriptors and authorizes spending their outputs. It controls account updates, not general application spending or proof of a person's identity. | | `[2, "admin key wrapping"]` | UMP Recovery Factor Wrapping | Encrypts and decrypts UMP presentation, password-derived, and recovery keys under the root privileged key. Decryption exposes recovery factors; this protocol is reserved for wallet administration. | Security level `2` is part of each registry identity. The protocol strings are the exact lowercase strings in Section 3.3, including spaces; neither `UMP` nor `188` is a substitute identifier. Both protocols use key ID `"1"` and counterparty `"self"` for the operations specified here. These derivation parameters describe UMP use; they are not extra ProtoMap identity fields, and `"self"` is not an account-specific public key to publish in a registry. **Capabilities and access boundaries.** The account-token protocol uses root `A` for public-key derivation, descriptor field signing and verification, and the input signature that spends the previous token. The wrapping protocol uses root `V` for authenticated encryption and decryption of `P`, `W`, and `R`; it wraps the derived password key, not the plaintext password. The exact cryptography and root/default-profile requirements remain those of Sections 3.2 and 3.3. Both identifiers are administrative under [BRC-44](../key-derivation/0044.md). A wallet MUST preserve that administrative restriction independently of registry metadata: a listing does not grant an external application access, and a friendly description is not permission to export roots, decrypt recovery factors, or sign account updates. These are descriptions of existing internal capabilities, not application permission schemes or installable permission modules. The common submission information for both entries is: | Item | Registration information | | --- | --- | | Registry and requested publisher | ProtoMap; Metanet Trust Services. Other publishers may independently describe the same identifiers. | | Requested networks | Mainnet and testnet, as separate network-scoped publications under the publisher's identified public key for each network. | | Requested action | Create descriptions for the two exact tuples. If that publisher already has a current record for either tuple on a requested network, identify its public key, outpoint, and current content in the review thread and handle the corresponding change as an update or correction. | | Proposal and review record | [BRCs PR #278](https://github.com/bsv-blockchain/BRCs/pull/278). This is a proposed specification of implemented protocols; the request does not assert that a registry record has been published. | | Documentation | This BRC, especially Sections 3–7, 14, and 15. The repository location is `wallet/0188.md`; a publication should use an absolute public URL to the reviewed revision, retaining the specification commit in the review record. | | Icon | [UMP account descriptor icon](./media/0188-ump.svg), shared by both entries. This newly contributed, static SVG is dedicated to the public domain under [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/). It carries no vendor endorsement and may be copied or mirrored. Use an absolute retrievable URL to the reviewed asset revision when publishing. | | Implementation evidence | The pinned Wallet Toolbox `CWIStyleWalletManager` in Section 14 uses the token tuple in `buildAndSend` and token validation, and the wrapping tuple in factor encryption and `getFactor`. The pinned Metanet Client Desktop and Metanet Explorer integrations provide the application context. This is evidence of implemented use, not a claim that every deployed version conforms to this proposal. | | Maintenance contact | Ty Everett, [@ty-everett](https://github.com/ty-everett), through the linked BRC PR or a linked registry correction/dispute request. Preserve the public review history and source revisions. | **Compatibility and maintenance.** The eleven-field legacy layout, the legacy layout with profiles, and the v3 layouts all retain these two exact tuples. PBKDF2 and Argon2id select password derivation within UMP; they do not create new ProtoMap identities. A metadata correction MUST NOT silently repurpose either established tuple for incompatible semantics. Maintainers should notify publishers of changed meaning, documentation, icons, contacts, deprecation, or migration requirements, preserving the legacy account compatibility described in Sections 4 and 14. Neither tuple is deprecated by this proposal. `tm_users` and `ls_users` are the overlay topic and lookup-service names specified in Sections 9 and 10, not BRC-43 protocol tuples. This request creates no ProtoMap entry for those names, no wildcard entry for profiles or user-specific identifiers, and no BasketMap or CertMap registration. The KDF algorithm names, discovery hashes, factor values, roots, passwords, WAB authentication data, and user-specific counterparties are not registration payloads. **Editorial handling.** The existing BRC PR is the initial inclusion and maintenance thread; request Metanet Trust Services review there by tagging @ty-everett rather than creating a duplicate request. Ty Everett is both this proposal's author and the named registry contact, so the request is not independent review. Seek an uninvolved reviewer where practicable and record any limitation. The publisher should record its decision, reviewed specification and icon revisions, network, publication public key, and resulting record outpoints in that thread. Do not mark the request published merely because the BRC is merged; confirm the resulting records through lookup after publication. No publisher key or existing record outpoint is asserted by this request. Under BRC-184, these records are optional, publisher-attributed descriptions. Their presence, absence, withdrawal, or availability MUST NOT determine whether an otherwise supported UMP account can be discovered, recovered, or updated. Registry publication confers neither identifier exclusivity nor authority over `tm_users` admission, `ls_users` results, WAB selection, account ownership, or the user's wallet permissions. Consumers retain network and publisher attribution and the exact tuples when interpreting metadata. ## 4. Password derivation and legacy compatibility ### 4.1 Password bytes Encode the entered Unicode password as UTF-8. Do not trim, case-fold, normalize Unicode, prepend an identity, or append a terminator. Canonically equivalent Unicode strings can produce different keys. UIs SHOULD preserve the user's input exactly and avoid silently rewriting it. The public salt is used as raw bytes, not as the text of its encoding. ### 4.2 Legacy accounts: mandatory PBKDF2 support An account with no on-chain KDF metadata uses: ```text W = PBKDF2-HMAC-SHA512(UTF8(password), S, iterations=7777, dkLen=32) ``` Every UMP recovery implementation MUST support this derivation. It is not PBKDF2-HMAC-SHA256, 7,777 bytes of output, or a truncation of a differently parameterized PBKDF2 call. The rule applies both to the eleven-field legacy format and to legacy tokens with encrypted profiles. The absence of KDF metadata is a positive selection of this historical derivation. Readers MUST NOT apply the current new-account default to those tokens. A failed PBKDF2 decryption is not permission to silently try a new KDF and rewrite the account. ### 4.3 Version 3 metadata Version 3 adds three fields after the core fields and any profile field: | Field | Byte representation | | --- | --- | | `umpVersion` | One byte `03` | | `kdfAlgorithm` | UTF-8 `argon2id` or `pbkdf2-sha512`, exactly | | `kdfParams` | UTF-8 JSON object containing the parameters below | For `argon2id`, all four members are required. The deployed new-account default is: ```json {"iterations":7,"memoryKiB":131072,"parallelism":1,"hashLength":32} ``` Use Argon2id version 1.3 (`0x13`), `t=iterations`, `m=memoryKiB` in **KiB**, `p=parallelism`, and the given output length. There is no Argon2 secret input or associated data. `131072` denotes 128 MiB. UMP's interoperable factor length is 32 bytes, so producers MUST set `hashLength` to 32. Readers MUST NOT truncate, extend, or otherwise reinterpret an Argon2id result of a different length to make it fit the XOR operations. For `pbkdf2-sha512`, `iterations` is required and sets the PBKDF2 iteration count. The derived key is always **32 bytes**. Existing writers can also include `memoryKiB`, `parallelism`, and `hashLength`, because they serialize a common KDF configuration object. `memoryKiB` and `parallelism` do not affect PBKDF2. For compatibility, the PBKDF2 implementation's fixed 32-byte result takes precedence over any `hashLength` member; new producers SHOULD use 32 or omit that member. For example: ```json {"iterations":7777} ``` JSON member order and insignificant whitespace do not change KDF meaning, but they do change the signed field bytes and therefore the script and transaction ID. Writers SHOULD emit the compact order `iterations`, `memoryKiB`, `parallelism`, `hashLength`, omitting unused members. Readers MUST authenticate the original bytes and MUST NOT reserialize JSON before checking the field signature. Recognized numeric parameters must be JSON numbers representing positive integers, not strings or booleans. A parser SHOULD reject duplicate parameter names and MUST NOT interpret unrecognized members as executable instructions or alternate algorithms. The current wallet resource envelope bounds Argon2id iterations at 20, memory at 262144 KiB, and parallelism at 16; PBKDF2 iterations at 10000000; and a supplied `hashLength` at 64. It limits the KDF JSON field to 1024 bytes. Argon2 also requires its own valid parameter relationships, including sufficient memory for the selected parallelism. These bounds do not make every accepted configuration a usable UMP factor: Argon2id still needs a 32-byte result. Readers MUST validate and bound work before running a KDF. A device unable to process a supported account MUST report that limitation and allow recovery on a suitable device, not substitute weaker parameters or classify the account as absent. Unknown algorithms, unsupported version markers, and malformed metadata MUST produce an unsupported/malformed-account result. They MUST NOT fall back to legacy derivation. Implementations claiming complete current account compatibility MUST support both algorithms and the default Argon2id parameters. ### 4.4 Changing the derivation An existing `W` is not enough to change algorithms. Migration requires the plaintext password or a newly chosen password, a fresh salt, and a newly derived `W`. The new encrypted fields and the new metadata MUST be published together in one successor token. A factor or profile update that does not change the password MUST preserve `S`, `W`, and the existing KDF interpretation. In particular, it MUST NOT label a legacy PBKDF2-derived `W` as Argon2id. A password change may either retain legacy PBKDF2 encoding or publish a correctly marked v3 successor. Deriving with Argon2id while omitting the metadata is not a valid migration. ## 5. Account descriptor fields and locking script ### 5.1 Core fields Field indexes start at zero. Every format starts with these eleven fields in this order: | Index | Name | Exact content | | --- | --- | --- | | 0 | `passwordSalt` | `S` | | 1 | `passwordPresentationPrimary` | `E(W XOR P, A)` | | 2 | `passwordRecoveryPrimary` | `E(W XOR R, A)` | | 3 | `presentationRecoveryPrimary` | `E(P XOR R, A)` | | 4 | `passwordPrimaryPrivileged` | `E(W XOR A, V)` | | 5 | `presentationRecoveryPrivileged` | `E(P XOR R, V)` | | 6 | `presentationHash` | `SHA256(P)` | | 7 | `recoveryHash` | `SHA256(R)` | | 8 | `presentationKeyEncrypted` | `E(K_wrap, P)` | | 9 | `passwordKeyEncrypted` | `E(K_wrap, W)` | | 10 | `recoveryKeyEncrypted` | `E(K_wrap, R)` | Fields 0, 6, and 7 are 32 bytes. The other core fields are 80 bytes for canonical 32-byte plaintexts. A compatibility reader MAY accept a historically serialized privileged scalar of 1 through 32 bytes in decrypted fields 4 or 5, interpreting it as unsigned big-endian and left-padding it to 32 bytes for subsequent key operations. This accommodates integer serializers that dropped leading zeros; it does not authorize variable-length presentation, recovery, or password factors, and new writers MUST emit fixed-width roots. There is no `UMP` magic string, plaintext account name, root public key, rendition counter, WAB identifier, or timestamp in the core fields. The locking public key is outside this array. Adding such a field at the beginning would shift the entire format and is not compatible. ### 5.2 Four interoperable layouts The optional profile field is specified in Section 8. The final field signature is **not** a protocol data field in this table. | Layout | Protocol data fields | Count before signature | Count including signature | | --- | --- | --- | --- | | Legacy without profiles | `F0` through `F10` | 11 | 12 | | Legacy with profiles | `F0` through `F10`, `profilesEncrypted` | 12 | 13 | | v3 without profiles | `F0` through `F10`, `03`, algorithm, parameters | 14 | 15 | | v3 with profiles | `F0` through `F10`, `profilesEncrypted`, `03`, algorithm, parameters | 15 | 16 | The legacy on-chain formats have no explicit version byte. Calling the profile extension “v2” is useful shorthand and matches a local serialization version; it does not mean a byte `02` exists in its locking script. For v3 the marker is at protocol field 11 without profiles and field 12 with profiles. Readers MUST recognize the layout by the field count and marker position after validating/removing the field signature. The marker is the binary byte `03`, not UTF-8 `"3"` (`33`) or a four-byte integer. Profiles are either omitted or contain a nonempty authenticated encryption envelope. Producers MUST NOT insert an empty placeholder to move the version marker. Unknown trailing extensions require a separately specified format; readers MUST NOT guess their meanings. ### 5.3 PushDrop envelope and field signature UMP uses the lock-before-data variant of the [BRC-48](../scripts/0048.md) PushDrop family. Its complete locking script is: ```text PUSH33(lockingPublicKey) OP_CHECKSIG PUSH(F0) ... PUSH(F_last) PUSH(fieldSignature) OP_2DROP ... OP_2DROP [OP_DROP] ``` There is one `OP_2DROP` for every pair of pushed data/signature fields and a final `OP_DROP` when their total is odd. The `OP_CHECKSIG` result remains on the stack. Do not substitute the lock-after-data example in BRC-48: current UMP readers expect the public-key lock first. The `fieldSignature` is a secp256k1 ECDSA signature by the token locking key over: ```text SHA256(F0 || F1 || ... || F_last) ``` It is DER-encoded, with no transaction sighash byte. No field lengths, Script push opcodes, public key, satoshi value, or transaction data enter this preimage. Producers SHOULD use low-S signatures. The signature field is excluded from its own preimage. Consumers MUST verify it under the public key from this same script, and MUST NOT remove a field merely because it resembles DER. It authenticates the descriptor under its token key; it does not independently prove ownership of a phone number, knowledge of the recovery factors, first-registration priority, or an account's history. Use minimal Script pushes: direct pushes through 75 bytes, `OP_PUSHDATA1` for 76 through 255 bytes, `OP_PUSHDATA2` with a little-endian 16-bit length for 256 through 65535 bytes, and `OP_PUSHDATA4` with a little-endian 32-bit length above that. Single-byte values 1 through 16 use `OP_1` through `OP_16`; thus the v3 marker is encoded as `OP_3` (`53`). Empty data uses `OP_0`, and the Script-number negative-one byte `81` uses `OP_1NEGATE`. Those last cases are not valid replacements for required nonempty UMP fields. Readers MUST recognize the small-integer opcode as its logical field value, including `[3]` for `OP_3`. Conforming descriptor consumers MUST check the entire script, its compressed public key, allowed field count, minimal pushes, exact drop sequence, absence of extra opcodes, and valid field signature. They MUST bound individual and aggregate payload sizes before processing expensive content. The current wallet ceiling is 16777216 bytes for both a field and the aggregate decoded fields, including the signature; operational transaction size limits may be lower. ### 5.4 Spending and output value New UMP writers MUST create a one-satoshi descriptor output. Historical topic admission does not enforce the exact amount, and account discovery readers SHOULD NOT discard otherwise usable historical account data solely because an older output carries a different positive amount. Spending always uses the actual source amount. The unlocking script is a single minimally pushed transaction signature under the same token private key. The standard wallet update uses `SIGHASH_ALL | SIGHASH_FORKID` (`41`), without `ANYONECANPAY`, binding all inputs and outputs. Unlike the field signature, this signature has a sighash byte after its DER encoding. Calculate its preimage using the source outpoint, full source locking script, source amount, and the finalized transaction according to Bitcoin's fork-ID signature rules. The account scheme does not turn the script into a covenant. A holder of the root primary key can derive the token signing key. Requiring privileged access before an ordinary client rewraps recovery factors is a client policy, not an extra consensus signature requirement. ## 6. Creation, authentication, and recovery ### 6.1 Creating an account 1. Obtain or establish `P`, and look up `H_P` as specified in Sections 10 and 11. Resolve existing-account evidence before offering account creation. A failed or ambiguous lookup is not an empty account. 2. Generate `R` and require the user to save it independently. Generate fresh `S`, `A`, and `V`, and derive `W` from the selected password and KDF. New accounts SHOULD use the v3 Argon2id default. 3. Compute every core field and the field signature. Initially there need not be a profile field. 4. Fund a transaction containing the one-satoshi token and necessary fees, using the default wallet identity rooted at `A`. A WAB faucet is an optional funding mechanism, not part of the account format. 5. Submit the transaction and required transaction evidence to `tm_users` using BRC-22/SHIP. Retain the exact transaction, resulting outpoint, and recoverable submission state. Verify that the returned transaction ID is the one actually submitted. 6. Commit local account state only when publication has a known successful result. If a WAB has a pending registration, finalize that association after publishing the token. On a timeout, reconcile the transaction and registration before retrying; do not generate a second account as a blind retry. The account password and recovery key MUST NOT be sent to the WAB as part of this flow. A recovery-key saver MUST not report success before the user or their selected backup mechanism has actually accepted the key. ### 6.2 The three factor pairs | Factors available | Discovery | Root primary recovery | Root privileged recovery | | --- | --- | --- | --- | | `P` and password | `H_P` | `A = D(W XOR P, F1)` | `V = D(W XOR A, F4)` | | `R` and password | `H_R` | `A = D(W XOR R, F2)` | `V = D(W XOR A, F4)` | | `P` and `R` | `H_P` or `H_R`, with matching hashes | `A = D(P XOR R, F3)` | `V = D(P XOR R, F5)` | For the password paths, obtain `S` and KDF metadata from the authenticated token before deriving `W`. For each supplied non-password factor, verify its discovery hash against the token. Authenticate every envelope used, validate the resulting root scalars, and reconstruct the token public key from `A`; it MUST agree with the descriptor's locking key before using that descriptor for account updates. Never accept a plausible-looking decrypted integer without GCM authentication. Ordinary login may keep `A` in a protected session while deferring reconstruction of `V` until a privileged operation requires a password prompt. Recovery can reconstruct `V` immediately and use it temporarily to replace a missing factor. Once `V` is available, compute `K_wrap` and decrypt fields 8, 9, and 10 to recover `P`, `W`, and `R`. This recovers the password **key**, not the original password text. Possession of `A` plus the password also recovers `V` from field 4. Consequently, a saved session containing `A` is security-sensitive even if it does not contain `V`. ### 6.3 Vendor-independent recovery A client implementing recovery without a WAB performs the following operation: 1. Decode the user's saved `R`, compute `H_R`, and query independently selected `ls_users` providers. It MAY also use a saved authenticated descriptor and outpoint as a starting point for recovering its spend history. 2. Validate the returned descriptor, transaction evidence, and lineage. Resolve stale renditions and ambiguity under Section 11. Do not require an old WAB pin when unambiguous independent evidence suffices. 3. Ask for the password locally, derive `W` using that account's exact KDF, and recover `A` and `V` by the second row of the table. 4. Recover the wrapped factors and profiles. Rebuild the same default and profile identities and reconnect to compatible wallet storage using those identities. 5. If desired, establish a presentation key with a new backend and publish a successor token replacing `P`, consuming the old descriptor. Preserve the roots and profile pads. The previous WAB does not sign or approve this transition. A conforming recovery tool MUST expose the `R` plus password path without first requiring successful authentication to the old WAB. A particular wallet UI may expose fewer recovery paths; that is a product limitation, not a cryptographic requirement of UMP. Recovery still needs the descriptor and transaction evidence, suitable storage or a backup for the wallet's other state, and funds for an update transaction. UMP does not make a vanished storage service's private records reappear. Save recovery material, known outpoints, and appropriate wallet backups independently of a single backend or overlay host. ### 6.4 Recovery-key text UMP itself uses raw `R`. Metanet clients commonly export it as standard, padded RFC 4648 Base64: a 32-byte key encodes to 44 characters ending in `=`. The surrounding text file's title and date are presentation, not protocol fields. A receiving client MUST decode the selected key text to exactly 32 bytes. It MUST NOT hash or use the Base64 characters as the factor. This format is not a mnemonic, WIF, or a Shamir share. ## 7. Updating an account and handling interruption ### 7.1 Spend the predecessor Every normal account change MUST consume the exact current UMP outpoint and publish one successor for that account in the **same transaction**. Funding inputs and change outputs may also appear. The token need not be output zero; identify its actual index by the finalized output script and value, and reject an ambiguous match. Before signing, obtain the predecessor's transaction evidence, check its outpoint and authenticated hashes, and verify that its locking key is the root/default wallet's expected token key. Bind the requested input, source amount, successor script, and final transaction before accepting a wallet action result. If the predecessor cannot be obtained, refuse the update. Do not silently turn it into an unrelated initial mint. Retain the old local state and all new recovery material until the successor's publication outcome is known. A lost acknowledgment may follow a successful transaction; retrying the identical transaction or discovering its successor is appropriate, whereas generating another independent token is not. Concurrent devices may attempt to spend the same predecessor. Reconcile to the accepted spend and its lineage, then rebuild any still-desired change from the new current token. There is no last-write-wins timestamp field. ### 7.2 Factor changes | Operation | Required changes | Preserved values | | --- | --- | --- | | Password change/reset | Fresh `S`; new `W`; matching KDF metadata; re-encrypt affected root and factor envelopes | `A`, `V`, `P`, `R`, profile IDs and pads | | Recovery-key replacement | Fresh saved `R`; new `H_R`; re-encrypt envelopes involving or wrapping `R` | `A`, `V`, `P`, `S`, `W`, KDF interpretation, profiles | | Presentation-key replacement | New `P`; new `H_P`; re-encrypt envelopes involving or wrapping `P` | `A`, `V`, `R`, `S`, `W`, KDF interpretation, profiles | | Profile-list change | New encrypted profile list, or omission when empty | Roots, factors, salt, and KDF interpretation | An implementation MAY re-encrypt all envelopes on any update, as the reference wallet does, with fresh IVs. It MUST use one internally consistent set of factors throughout the successor. Preserving `A` and `V` preserves default identity, derived protocol keys, and profile identities. When changing a WAB association, coordinate the off-chain and on-chain steps so an interrupted operation remains recoverable. The deployed phone-change flow stages the new association and key, spends the UMP token to publish the rotation, then finalizes the WAB change. The WAB retains current and pending keys during that interval. An idempotent reconciliation path is necessary because a database commit and a Bitcoin transaction are not one atomic operation. A new backend may use different authentication APIs while producing the same UMP transition. ### 7.3 Deletion and revocation limits Spending a token without an admitted successor removes that live descriptor from lookup when the overlay observes the spend. Historical bytes remain on-chain. Deleting a WAB row, deleting a local wallet, relinquishing local output tracking, and spending a UMP descriptor are different operations; none should be represented as all the others. Factor rotation does not erase old encrypted envelopes. Anyone who has already recovered `A` or `V`, or who later obtains enough factors to decrypt an earlier rendition, may still recover the unchanged roots. UMP factor rotation is therefore not root-key revocation. Remediation of compromised roots requires migrating the affected assets, identities, permissions, and data under an appropriate separate procedure. A client MUST NOT promise that a password change alone revokes knowledge of historical roots. ## 8. Profiles Profiles provide multiple wallet identities beneath one recoverable UMP account. The default profile has the 16-byte all-zero ID, uses `A` and `V` directly, and is not included as an ordinary entry in the encrypted list. For each additional profile, generate a distinct random nonzero 16-byte ID and independent 32-byte `primaryPad` and `privilegedPad`. Its roots are: ```text A_profile = A XOR primaryPad V_profile = V XOR privilegedPad ``` Interpret them as fixed-width secp256k1 scalars and validate them as in Section 3.1. The profile ID and name do not themselves derive either key. Losing a pad is losing the corresponding derivation information. The plaintext is a UTF-8 JSON array of objects: ```json [ { "name": "work", "id": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], "primaryPad": [192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223], "privilegedPad": [224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255], "createdAt": 1700000000 } ] ``` The byte arrays are arrays of JSON integers from 0 through 255, not hex or Base64 strings. `createdAt` is seconds since the Unix epoch; writers use a nonnegative integer. Names are nonempty strings of at most 250 UTF-16 code units, matching the current JavaScript client bound. New lists MUST have unique IDs and SHOULD have case-insensitively unique names, reserving `default`. Readers MUST bound the list; the deployed maximum is 1000 entries. Invalid or unauthenticated profile data MUST produce an explicit error rather than silently discard profiles and save an empty list. The on-chain profile field is: ```text profilesEncrypted = E(A, UTF8(JSON(profileArray))) ``` It is encrypted directly under the **root primary key**, not `V`, `K_wrap`, or a profile root. Historical source comments have described this differently; the primary-key encryption is the implemented format. The root wallet can therefore discover its profiles during ordinary login. Property order and JSON whitespace may vary; authenticate and decrypt the actual bytes. If no additional profiles exist, omit the field. Adding or deleting a profile publishes a successor UMP token. Switching the active profile is a local choice and requires no transaction. UMP publication always uses the default profile's token key. Removing a profile entry does not spend its assets or make old on-chain encrypted profile lists disappear; clients SHOULD ensure that users have handled the profile's assets and backups before removing its discovery metadata. ## 9. The `tm_users` overlay ### 9.1 A fairly strict overlay UMP is a **fairly strict overlay**, using [BRC-183](../overlays/0183.md) as the framework for explaining that qualification: most participating nodes should carry most account records most of the time, and discovery should ordinarily identify the same account lineage across providers. It is not intended as a collection of unrelated vendor-curated account catalogues. “Fairly strict” is a description of UMP's operational model, not a third formal category introduced into BRC-183. The qualification is deliberate. This is not a guarantee that every node has identical state or that local first-admission races have a globally deterministic winner. BRC-183's fully Strict Overlay definition is stronger: it expects common state at a common chain checkpoint under prescribed rules. The deployed UMP profile described here has local first-accepted reservations, incomplete historical coverage, and possible legacy ambiguity. Calling it fairly strict does not add a consensus or block-order tie-break that its implementations do not have. Hosts SHOULD replicate and reconcile account records and their relevant spend histories, and clients SHOULD use independent providers. A temporary difference in coverage is not an excuse to discard existing ownership checks or admit an unrelated replacement. GASP and BRC-64 history may help distribute evidence; merely using those transports does not settle independent initial-claim races. ### 9.2 Admission input and validation The topic name is exactly `tm_users`. Submission follows [BRC-22](../overlays/0022.md), using BEEF transaction evidence and the overlay engine's normal Bitcoin validation. Only inputs already recognized as relevant UMP coins are eligible predecessors in `previousCoins`; input indexes are not output indexes. A claimed predecessor in JSON or an arbitrary funding input is not a token spend. The baseline HTTP submission is: ```http POST /submit HTTP/1.1 Content-Type: application/octet-stream X-Topics: ["tm_users"] ``` The body is the serialized BEEF containing the submitted transaction and its required evidence, not a JSON serialization of the eleven fields or bare transaction hex. UMP defines no off-chain field payload. The per-topic JSON acknowledgment identifies actual output indexes and retained input indexes. For example, an update consuming relevant input 0 and admitting descriptor output 0 can return: ```json {"tm_users":{"outputsToAdmit":[0],"coinsToRetain":[0]}} ``` Initial creation has no UMP predecessor to retain. A successful HTTP response with an empty `outputsToAdmit` array is not acknowledgment that the requested descriptor joined the topic. Submission acknowledgment and Bitcoin confirmation remain separate observations. A conforming host MUST identify UMP descriptor candidates, validate their publicly checkable format, and apply the ownership rules below before final admission. It cannot decrypt the envelopes and therefore cannot certify that the publisher chose mutually consistent plaintext factors. It MUST NOT claim that admission proves a password or recovery key will work. The Topic Manager at the pinned source revision uses a minimum shape check weaker than a wallet's authenticated descriptor parser: it decodes PushDrop, checks at least eleven fields and 32-byte hashes, and, when a single-byte v3 marker is found at field 11 or 12, checks marker `03`, one of the two algorithm names, and JSON with positive `iterations`. That acceptance alone is not sufficient for descriptor conformance. Hosts claiming the complete validation profile in this proposal MUST bring publicly checkable checks into alignment with Section 5, while preserving recovery access to existing account data. Consumers MUST perform their own descriptor checks regardless of a host's software version. Outputs are considered in ascending transaction output index. For each candidate, form its exact outpoint and read `H_P` and `H_R`. The following rules govern identity reservations even when an older host's shape validation is more permissive. ### 9.3 Separate hash namespaces and first-accepted ownership Maintain an ownership map with two distinct namespaces: ```text (presentation, H_P) -> current owner outpoint (recovery, H_R) -> current owner outpoint ``` An equal byte string in different namespaces is not a collision. Both claims must succeed for a candidate output to be admitted. Checking only presentation hashes, or only recovery hashes, is insufficient. For each hash claim: 1. If no live owner or unexpired provisional reservation exists, the first successful reservation acquires the hash provisionally. 2. Reprocessing the same outpoint is idempotent and does not create a competitor. 3. If a different admitted owner exists, a candidate may take over that claim **only if the transaction creating it actually consumes that exact owner outpoint as a recognized UMP input**. 4. Otherwise reject the conflicting candidate. Matching a hash, using the same locking public key, mentioning an ancestor, or spending an unrelated token is not sufficient. These checks apply independently to both hashes. If the hashes have different current owners, the transaction must consume both owners to satisfy both claims; consuming only one is insufficient. A normal single-account update consumes its one predecessor and either retains its two hashes or replaces one with an unclaimed hash. Two sibling successor outputs cannot both own the same hash; reservation processing within one transaction must serialize that conflict. “First” means first successfully reserved and admitted under that host's serialized ownership state, including shared state across replicas of that host. It does **not** mean the first lookup result, lowest transaction ID, largest fee, latest timestamp, lowest block height, or first message received by an arbitrary peer. Implementations MUST serialize competing claims using durable shared state or an equivalent atomic mechanism; a check followed by an independent insert is not a concurrency-safe ownership decision. A new host must synchronize relevant ownership and history before claiming equivalent coverage. The deployed protocol does not define a universal reconciliation winner when disconnected hosts have already admitted different, unrelated initial claims. Such a conflict requires explicit evidence-based reconciliation and remains ambiguity to a client until resolved. A host MUST NOT silently replace a live owner merely because a different peer supplied another same-hash output later. ### 9.4 Reservation lifecycle Reservation is not final admission, and final overlay admission is not mining confirmation. The implementation's reservation `confirm` operation means the output has been admitted/indexed, including when its Bitcoin transaction is still unconfirmed. * An initial reservation is provisional until admission succeeds. A failed candidate must release its provisional claims. * A transfer stages a pending successor while retaining the admitted predecessor as owner. A different pending claimant must not displace that successor while its reservation is active. * If acquiring the second hash fails, undo only the claims or renewals made for this candidate, restoring their previous state. Do not erase another candidate's ownership. * Successful admission/indexing commits the successor's reservations. Spending removes the predecessor from live lookup and transfers retained hashes to the admitted successor. Hashes not retained by a successor are released. * An aborted or expired pending transfer must leave the prior admitted owner intact. Admission validation performed as a dry run must not acquire or change reservations. The deployed provisional timeout defaults to 900 seconds and is configurable. Expiration releases abandoned **provisional** work; it is not a lease on a successfully admitted account and MUST NOT expire an admitted owner merely through inactivity. Multi-process hosts must coordinate expiration, retries, commit, and rollback against the same shared ownership state. The deployed store releases an owner's claims when its output is spent or evicted. Thus reservations protect the current live owner; they are not permanent reservations of every hash ever present in the account's history. Once a claim is genuinely released and has no pending successor, it can be claimed again. A reappearance of a released hash is not proof of the old account's identity. Administrative eviction is consequently a consequential state change, not a harmless way to clear a cache. Hosts MUST keep live lookup and ownership state coherent across accepted spends, rejected submissions, restart, and reorganization handling. If evidence is rolled back or a previously selected transaction ceases to be valid, repair/replay the affected records and reservations rather than retain stale ownership. The existing first-admission model does not specify a separate universal reorganization ordering algorithm; operators must not advertise stronger convergence than they can establish. ### 9.5 History and legacy ambiguity An accepted transaction returns its admitted output indexes and retains the relevant input coins as history under [BRC-64](../overlays/0064.md). The reference Topic Manager returns `coinsToRetain: previousCoins` when it admits outputs. A transaction with no admitted UMP outputs returns empty admission/retention instructions; engine spend notifications and history policy remain distinct from the lookup's live-record set. Retained history is not a second live account. The lookup projection removes a spent or evicted output, while history retained by the engine can establish how a surviving rendition descends from an earlier one. Older hosts could contain multiple unrelated records for the same hash. The deployed reservation migration seeds each hash from its first already-indexed occurrence, using existing insertion order, and does not silently delete other historical candidates. This is a local database migration rule, not evidence that that occurrence was first on the blockchain. Existing ambiguity remains visible to consumers and must be handled as described below. A migration MUST NOT declare every same-hash record to be part of one lineage. ## 10. The `ls_users` lookup service The service name is exactly `ls_users`. Use a [BRC-24](../overlays/0024.md) lookup question with a plain JSON query object. The supported selectors are: Send the question as the JSON body of `POST /lookup` with `Content-Type: application/json` on the selected overlay host. ```json {"service":"ls_users","query":{"presentationHash":"<64 hex characters>"}} ``` ```json {"service":"ls_users","query":{"recoveryHash":"<64 hex characters>"}} ``` ```json {"service":"ls_users","query":{"outpoint":"<64 hex characters>.0"}} ``` Angle-bracket strings above denote syntax, not literal requests. Hash selectors are exactly 32 bytes expressed in hexadecimal. The service accepts either hex case and normalizes to lowercase. Outpoint syntax is defined in Section 1. Queries contain at least one selector, no unrecognized members, and only string selector values. Invalid lengths, malformed hexadecimal, object-valued selectors, and out-of-range output indexes are errors, not empty matches. Clients SHOULD send exactly one selector. For compatibility with the deployed service, when several valid selectors are present the precedence is `presentationHash`, then `recoveryHash`, then `outpoint`; they are **not** combined with AND or OR. Every supplied selector is validated before this precedence is applied, so a malformed lower-priority selector still makes the query invalid. The service indexes fields 6 and 7 of admitted outputs. A lookup returns references to live records, which the overlay engine resolves to the normal BRC-24 `output-list` answer, with `beef` and `outputIndex` for each entry. BEEF contains the transaction and whatever supporting evidence/history the host provides; its outer JSON encoding follows BRC-24. The topic's internal `{txid, outputIndex}` references are not a replacement client-facing response format. There is no UMP-specific response that contains plaintext keys or decrypted account data. In the JSON transport, `beef` is an array of integer bytes from 0 through 255 and `outputIndex` is a zero-based integer into that entry's subject transaction. The outer object has `"type":"output-list"` and an `outputs` array. UMP assigns no meaning to optional BRC-24 `context` bytes. A clean empty answer has this exact shape: ```json {"type":"output-list","outputs":[]} ``` BRC-24's compact binary response can carry the same logical results when supported by the transport. Neither encoding changes the UMP selector semantics or permits a client to skip transaction and descriptor validation. The current lookup limits a response to the 100 most recently indexed matching records, newest insertion first. This exists to keep legacy ambiguity bounded and avoid indefinitely hiding a recent lineage tip behind old records. That ordering is not account priority, and the bound is not proof that all possible records were returned. There is no pagination selector in this profile. Clients encountering a saturated or incomplete response MUST account for possible missing history/candidates rather than claim completeness. Use BRC-88 SHIP/SLAP discovery, explicit user-selected providers, or other supported overlay discovery to locate the topic and service. No hostname is an intrinsic part of UMP. Provider diversity improves availability but does not transform several agreeing replies into proof of a global registry's completeness. ## 11. Selecting the account from lookup evidence ### 11.1 Validate before choosing A lookup result is evidence to validate, not an account chosen by array position. A consumer MUST: 1. Parse and validate its transaction/BEEF evidence using the wallet's applicable Bitcoin verification and chain-tracking rules. An output's field signature alone is not an SPV proof. 2. Derive the output's actual transaction ID, check that its index exists, and parse the authenticated UMP descriptor. 3. Check that the requested presentation or recovery hash actually matches the corresponding token field. For an outpoint query, check the exact transaction ID and output index. 4. Deduplicate identical outpoints while retaining all useful evidence. Two hosts may provide different history depths for the same output; a shallow first copy must not mask deeper evidence from another copy. 5. Resolve spend continuity before using a candidate's secrets or updating it. ### 11.2 Spend-based selection and legacy fallback If matching candidates include predecessor and successor renditions, collect outpoints spent by each candidate's transaction and its available ancestors. A candidate is superseded if another candidate's history consumes it. Intermediate renditions need not appear as separate lookup results if their transactions are present in the evidence. Choose the sole unsuperseded candidate only when the evidence supports that conclusion; absent history is not proof of independence or current unspentness. The deployed wallet also has a legacy disambiguation heuristic: among several surviving candidates it prefers the sole candidate whose creating transaction demonstrably spends an authenticated UMP output with the same presentation hash **or** the same recovery hash. This favors a continuing account over an independent re-registration. This heuristic is weaker than demonstrating descent from an already trusted outpoint; it MUST NOT be described as a universal first-registration proof or used to override conflicting, stronger continuity evidence. If no unique choice is established, report ambiguity. Some WABs return an optional `umpTokenOutpoint` pin. Current clients use it only when ordinary lineage selection remains ambiguous and only if the pin equals one of the already validated matching candidates. A pin MUST NOT inject an unseen output, waive descriptor/evidence validation, override an unambiguous selected lineage, or become mandatory for independent recovery. It is an explicit extra trust input useful for legacy support, not an on-chain authority. ### 11.3 Empty, failed, and inconsistent lookups Keep these outcomes distinct: a validated account; a clean empty response under the chosen provider policy; malformed or unsupported candidates; unavailable/incomplete discovery; and unresolved ambiguity. Malformed and failed peers must not override positive validated account evidence. The current interactor waits for its selected hosts, resolves matching candidates, and can return “not found” when no validated match remains and at least one host returned a clean empty response. Other failed or malformed replies do not necessarily veto that result. This is an availability-oriented discovery policy, **not a cryptographic nonexistence proof**. A new implementation SHOULD make its absence policy explicit and use remembered outpoints, local snapshots, or WAB existing-account evidence to prevent accidental re-registration. If a backend or a local record says the account exists but the overlay cannot establish its token, the client MUST offer retry or recovery instead of silently creating new roots. A WAB returning a different presentation key does not itself prove that the user's old account ceased to exist. A recoverable error must remain distinguishable from successful onboarding. ## 12. Local serialization compatibility This section documents the Wallet Toolbox formats used to persist UMP state. They are useful to independent recovery/import tools but MUST NOT be confused with the Script field arrays. An implementation that does not import these local formats may still implement the on-chain protocol fully. ### 12.1 Binary token record Define `VEC(bytes)` as Bitcoin CompactSize length followed by those bytes. CompactSize uses one byte below 253, `fd` plus a little-endian uint16 through 65535, `fe` plus a little-endian uint32 through 4294967295, and `ff` plus a little-endian uint64 thereafter. Writers use the shortest representation; readers MUST bound lengths before allocation and reject truncation. All three local record versions start with a one-byte version and eleven `VEC` values in exactly the core-field order of Section 5.1: ```text local v1 = 01 || VEC(F0) ... VEC(F10) || VEC(UTF8(currentOutpoint)) local v2 = 02 || VEC(F0) ... VEC(F10) || profileFlag[1] || [VEC(profilesEncrypted)] || VEC(UTF8(currentOutpoint)) local v3 = 03 || VEC(F0) ... VEC(F10) || profileFlag[1] || [VEC(profilesEncrypted)] || kdfFlag[1] || [umpVersion[1] || VEC(UTF8(kdfAlgorithm)) || VEC(UTF8(kdfParams))] || VEC(UTF8(currentOutpoint)) ``` Each optional group is present if and only if its preceding flag is `01`; `00` means absent. New writers MUST use only these two flag values. Current writers use local v2 without KDF metadata, including accounts without profiles, and local v3 with `kdfFlag=01` and `umpVersion=03` when metadata is present. A compatibility reader may encounter local v3 with `kdfFlag=00`, which means no metadata and therefore legacy PBKDF2; the normal writer does not produce it. The record has no field signature, locking public key, transaction, or BEEF. It carries the outpoint separately because an on-chain output cannot include its own eventual transaction ID. The local version byte does not become an extra Script field. A local record must be reconciled against the authenticated on-chain descriptor before being treated as current. Some historical local readers tolerated colon-separated or otherwise noncanonical outpoint strings; emit the canonical form and do not pass those historical spellings directly into `ls_users`. ### 12.2 Session snapshot The manager also supports these enclosing formats: ```text snapshot v1 = 01 || snapshotKey[32] || E(snapshotKey, payload) snapshot v2 = 02 || snapshotKey[32] || activeProfileId[16] || E(snapshotKey, payload) payload = A[32] || VEC(serializedLocalTokenRecord) ``` Snapshot v1 implies the default profile; v2 includes the selected profile ID. The payload encryption uses the Section 3.2 envelope. A fresh random snapshot key is generated for each saved snapshot. The current outer snapshot version is 2 even when it contains a local v3 token. Bounds in current clients limit the full snapshot to 16777216 bytes. The decryption key is included **inside the same snapshot**. The envelope is not password protection of an exported snapshot and does not conceal `A` from someone holding the snapshot bytes. Protect the whole snapshot using the platform's storage protections or an independently specified backup encryption format. Neither the outer version nor the active profile ID is GCM additional authenticated data in this legacy format. Importers MUST validate lengths, profile selection, and restored account state; they MUST NOT publish snapshots as if they were public UMP tokens. ## 13. Test vectors and conformance scenarios All keys, salts, IVs, signatures, outpoints, and transactions in this section are artificial test material. They are public and MUST NOT be used to protect an actual account. Hexadecimal strings represent raw bytes. Unless stated otherwise, a digest is a single SHA-256 over the bytes, in digest order, not a Bitcoin transaction ID. ### 13.1 Factors, KDFs, and derivation The base password and salt below exercise legacy PBKDF2, explicitly parameterized PBKDF2, and the v3 Argon2id default. The Unicode example checks UTF-8 handling without normalization. The explicit PBKDF2 vector changes only its iteration count to 20000. ```json { "inputs": { "password": "UMP test password", "passwordUTF8": "554d5020746573742070617373776f7264", "salt": "a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf", "presentationKey": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", "recoveryKey": "202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", "recoveryKeyBase64": "ICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj8=", "rootPrimaryKey": "404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f", "rootPrivilegedKey": "606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f" }, "kdf": { "legacyPBKDF2": "26bdd08c841a0d82e52002ac6e1bacadc631535b45efa41c8e61a51ba1ed898b", "explicitPBKDF2Iterations": 20000, "explicitPBKDF2": "e4d65939ad9542a2a76fa057e0b2ee4f60901e6d098797569caabf1bdf3d9046", "argon2id": "e9b3af6baf992a0cfca8608ed1e4ccbab8b73cb39b367b88842a4358aded517e", "unicodePassword": "café🔑", "unicodeUTF8": "636166c3a9f09f9491", "unicodeLegacyPBKDF2": "93d8b2fad4e55838a5bcc6e832da747128f7a6f3ec908e6e757b5e462f4dc726" }, "derivation": { "lockingPrivateKey": "d782d5d8f28e4c0a81e5199d2d3c1e3295804dbabb1e457d34d0f39510cf73fd", "lockingPublicKey": "03df6ac569c2b43e7080b4313e8eb7e2aa9fded22cd1bfc737ba58834a233f6d1e", "wrappingKey": "57237aa4cc9eabd12dedf1003d007cef25ae652144cc90524025dbc10dafd682", "profilePrimaryKey": "8080808080808080808080808080808080808080808080808080808080808080", "profilePrivilegedKey": "8080808080808080808080808080808080808080808080808080808080808080" } } ``` `lockingPrivateKey`, `lockingPublicKey`, and `wrappingKey` must be obtained from the Section 3.3 formulas. The two profile roots happen to be equal in this artificial fixture because of the selected pads; normal generation uses independent random pads and roots. ### 13.2 Core encryption fields For an encrypted core field with index `i`, use a 32-byte IV all of whose bytes equal `0x20 + i`. Thus field 1 uses thirty-two `21` bytes, field 8 uses thirty-two `28` bytes, and so forth. The following array is the exact eleven legacy core fields, in index order: ```json [ "a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf", "21212121212121212121212121212121212121212121212121212121212121214afaeda03ef301636464d7d5c7509ec3cd2529b23b601730fa36c2c2907a161f8ad37e2e2e513b7f6113b11c16245174", "222222222222222222222222222222222222222222222222222222222222222240e7193fe34a4ba03a83c53ef23857a7f56e6b7937619ade7d0c6da7ccde16c39c6e14130c280290310657e0e17603ce", "2323232323232323232323232323232323232323232323232323232323232323d8baa13b67236864bdc4b7d480a7e1b6a0081cae15d5e3fb37a0e6afdf2d234db2698e4c3333dcb4781f09d47583b431", "2424242424242424242424242424242424242424242424242424242424242424c806599299843f0ef400336bf624c6620a41291ecd6dedafbaca62373c84290276939bc4ba5c185b93b31e010d70b1c4", "2525252525252525252525252525252525252525252525252525252525252525746400da26b109ebed5cd792186ce1d68911fa9065ab44af8808afa6f499ba09b51dcd8ddce568b47df8fe525b3c5fcf", "630dcd2966c4336691125448bbb25b4ff412a49c732db2c8abc1b8581bd710dd", "72dbb7336c76780023f83da4c355f2eeea85733b13d3477697917790c1229084", "28282828282828282828282828282828282828282828282828282828282828286b2ece523fcca61e3044c168dbdc22960ca285b0d034fe9ecaed0088a41e0b2ab3b5cfaeb178ae8a938335a940d20219", "292929292929292929292929292929292929292929292929292929292929292989007c80f70e6d7a368406db15ec1f6031a5ab5e66947f88974b7060663dff05017a2b0b79e90bdb25c9bffa084766d1", "2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a0c8c8f0442ea7796f632aa85217b3383461510de04d0971a3e840b1811c3e0ecc397b7c631fa6c78b752fb894e370abd" ] ``` For the v3 default-Argon2id fixture, preserve every legacy field except fields 1, 2, 4, and 9. Replace those four fields with the following values, using the Argon2id-derived password key and the same test IV convention: ```json { "1": "21212121212121212121212121212121212121212121212121212121212121213aa2118b4059597616798308f01b802043daaec95d45b1548d9dcd6b0904cf828c89edf7049abdfaf723700f010ee7da", "2": "2222222222222222222222222222222222222222222222222222222222222222c9227649168bb4c0523aaaf4d3424f96851bf0f0bc1c9ed47bc14ba29c4d98a9943d669a05ece30f293670c49d9335a5", "4": "242424242424242424242424242424242424242424242424242424242424242463d275073ae2149bf1826156597657b095789b854472391985104a949ed5054475a36316f4e0b105144c8ee99b4005ff", "9": "2929292929292929292929292929292929292929292929292929292929292929460e0367dc8d4af42f0c64f9aa137f774f23c4b6b84da01c9d0096236a3d27f08dfceaf92e532610be26bb1937a2cd93" } ``` For each fixture, every authentication path in Section 6.2 MUST recover the specified `A` and `V`. Unwrapping fields 8, 9, and 10 MUST recover `P`, that fixture's `W`, and `R`. In particular, recovering `W` must not be mistaken for recovering the password text. ### 13.3 Profile encryption Encrypt the exact compact UTF-8 plaintext string below directly under `A`, with an IV of thirty-two `2b` bytes. The expected envelope is: ```json { "plaintext": "[{\"name\":\"work\",\"id\":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16],\"primaryPad\":[192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223],\"privilegedPad\":[224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255],\"createdAt\":1700000000}]", "ciphertext": "2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b76b339fe21577315aff41a307a05bb07b78f3cc4680c9e70e853ae1f2fcd921bd5eb90193169976987b1a06afddf2d9794db9f7acda18eb585233fdf87cee51a651dd39dc1464f6b8c2cfdc0413c877ca0ae3a17a1af3224da42e6a1ef16abc205bf5960fac9d040c615656a7f9a984411eaa65d1d51e40f3890f831dcd80281eef7b2ab07d3670d75fd02d06efe5688c1e461501661ffbfc5971a9873f5ea43b663680e36bcc832b00306feb395ef3ea52923511c520abd4bc882b9148f51911cf4e429955b001cab0f874a3cad7a7ca187023284c4b659c74047e9278ecd2650bdb14fe1c0c697d978bc43ae39a87df7153d594f4b5015ba4eb739b7223fc711fcec05f84ef83e653f038746c9a7d99f5af7dd840743aef840cab7ee58eb068127977161d8ae7ca7837a140d52715b5c47867c970082b3650acd8e9f4a6919f18742b2f01aabb9fe8338f1d59ca6e177c4834e74f3880d5a7c444dca304d1fb022fd0ab0249249430efbb603352d0818bb990f86fbd015dae385fda578336e5bcf48628ac5b3" } ``` ### 13.4 Four complete script fixtures Construct these arrays of protocol fields: * `legacy`: the eleven legacy fields above. * `legacyProfiles`: the legacy fields followed by the profile ciphertext. * `v3`: the Argon2id core fields followed by byte `03`, UTF-8 `argon2id`, and the compact parameter JSON below. * `v3Profiles`: the Argon2id core fields, profile ciphertext, and the same three metadata fields. The metadata bytes are the UTF-8 encoding of this exact line, with no trailing newline: ```json {"iterations":7,"memoryKiB":131072,"parallelism":1,"hashLength":32} ``` For each array, append the supplied DER signature and construct the full Section 5.3 script using the specified locking public key. These signatures verify over one SHA-256 of the concatenated protocol fields. The script byte length and hash include the public-key lock, all pushes, the signature, and the drops. In particular, the marker uses byte `53` as a Script opcode and each 80-byte envelope uses `4c50` as its push prefix. ```json { "legacy": { "protocolFieldCount": 11, "signature": "3044022075a1b4714fde1247c288e754c6238104da5a058688f0e183d7756a1ce7b21cd7022046889977214ea25070df53dff3e3ba36dcccb7e2cc9240bff771c63e80337dd8", "lockingScriptBytes": 867, "lockingScriptSHA256": "c910ffe2e844e1b80f67f42477aed17ca5691840f2fa1e4c693bee75a416ff55" }, "legacyProfiles": { "protocolFieldCount": 12, "signature": "304402204ed6733a4ca24354dff7cb1b4731f95deb76f36c4563c59acc95a2cac9a1191b02206a518acb049df32a157115871d658d3d80fba3baa227d6335ddf82ffd078b8e8", "lockingScriptBytes": 1294, "lockingScriptSHA256": "f815e08411607e7ba174d0d1d23e9b63888d2a8b7c107fae17fc97c699a15a5b" }, "v3": { "protocolFieldCount": 14, "signature": "3044022041fbc6e35c81c9f83f15fe830630c20e86c86ef74c44270e33d157af59b4dad102203329b148ba0534d2c1898c3841aa387db7ad67491b40dd9d9b010ae86ae46990", "lockingScriptBytes": 947, "lockingScriptSHA256": "96d8493aa3955d96d166376cb3df00b1db23e294dcc70dfd1310c0bb711a5be0" }, "v3Profiles": { "protocolFieldCount": 15, "signature": "30440220088f074c2309c101910fca2651ad2ed29bdd770eac588dbdf620445abee043fe0220212da5a7d937789a4fc34b40df3ef059a1f8ab1643e10744add1198b23f45c43", "lockingScriptBytes": 1373, "lockingScriptSHA256": "37891ef831b68f81560cc6a60f5a2c26d7b9e02e1efcb54348716170e57f94c7" } } ``` These are byte-exact serialization vectors using the supplied signatures. Another valid ECDSA nonce can yield another valid signature and therefore a different script hash; conformance does not require every implementation to reproduce the signing nonce. A verifier must accept the supplied signatures. ### 13.5 A token-spend transition The following is a deterministic transaction serialization and signing fixture. It is not a funded transaction or proof of mainnet/testnet admission; its funding reference is deliberately synthetic. Construct the source transaction with version 1, one input, one output, and locktime 0. The input's displayed source transaction ID is 64 `1` characters, its index is 0, its unlocking script is the single byte `00`, and its sequence is `ffffffff`. The output has one satoshi and the `legacy` locking script from Section 13.4. Construct the update transaction with version 1, one input, one output, and locktime 0. Its input spends output 0 of that source transaction, has sequence `ffffffff`, and uses the exact unlocking script below. Its output has one satoshi and the `v3Profiles` locking script. Use Bitcoin transaction serialization, little-endian integer fields, CompactSize lengths, and reversed transaction-ID bytes in the input outpoint. Verify the update's `41` transaction signature using the source's complete script and one-satoshi amount. The expected identifiers are: ```json { "sourceTXID": "1c19754519e326506e3dc89195aae6899d01d2d7cc779483342bdc86e53a5b81", "sourceOutputIndex": 0, "sourceSatoshis": 1, "updateTXID": "57d59fef5e30bfd201b0603d7114beb5e5db06276084873330105560142146ca", "updateUnlockingScript": "483045022100b932fff36cb4e31988f8af0d495fcdc50976d0f3d74da5748d1636436340f04a022038353325ff224db984c42a1c447a604c7aa5b5c5834469d5835bd00e0b9742a341", "currentOutpoint": "57d59fef5e30bfd201b0603d7114beb5e5db06276084873330105560142146ca.0" } ``` The update is the successor because it spends the source outpoint. It has no rendition counter. Looking up both outputs with that history available must select the update. This vector models an authenticated legacy-to-Argon2id migration with explicit metadata and preserved roots. ### 13.6 Local records and snapshots Use the `currentOutpoint` from Section 13.5 for all three local-record fixtures. For local v1 use the eleven legacy fields and no flags; for local v2 use the legacy fields, `profileFlag=01`, and the profile ciphertext; for local v3 use the Argon2id fields, `profileFlag=01`, the profile ciphertext, `kdfFlag=01`, marker `03`, and the exact Argon2id metadata bytes from Section 13.4. Serialize according to Section 12.1. The expected byte lengths and hashes are: ```json { "v1": { "bytes": 815, "sha256": "838dbcbb7b662f476455682a5e8a1cfc1a5efd97e37461381438ffeaa5db5ac8" }, "v2": { "bytes": 1242, "sha256": "45712ee2f8abda14f5f6162a0bca80811a97efee4effd180bcafb31e1e8e0c28" }, "v3": { "bytes": 1321, "sha256": "feabe05ade12a642a51c0b1ecd56786348c200d65cc7bb34891e39f0b6f3cb3d" } } ``` For the enclosing snapshots, use the given snapshot key and IV, with the version-specific token record named below. Both payloads start with `A`; snapshot v1 implies the default profile and snapshot v2 explicitly selects the fixture's additional profile. The following hashes cover the entire outer snapshot, including the clear snapshot key and, for v2, the active profile ID: ```json { "snapshotKey": "5555555555555555555555555555555555555555555555555555555555555555", "iv": "6666666666666666666666666666666666666666666666666666666666666666", "activeProfileId": "0102030405060708090a0b0c0d0e0f10", "v1": { "tokenRecord": "v1", "bytes": 931, "sha256": "22f06ea6483790c9e41f33d5a7a2ac3d21d3a496a68b5b57a3b144447d305a04" }, "v2": { "tokenRecord": "v3", "bytes": 1453, "sha256": "14aff452c151285a896a9fd385981dcc88e13287b602d06a745aa7853e0d75fe" } } ``` ### 13.7 Parsing and cryptographic rejection cases Apply each mutation independently to an otherwise valid fixture. These cases are requirements for descriptor consumers; they do not assert that every historical topic manager already made the same checks. | Case | Expected result | | --- | --- | | Each of the four Section 13.4 layouts | Parse successfully; distinguish the field signature, optional profile, and KDF metadata | | Legacy fields with no metadata | Derive with PBKDF2-HMAC-SHA512, 7777 iterations, 32 bytes | | v3 algorithm `pbkdf2-sha512`, parameters `{"iterations":20000}` | Derive the explicit PBKDF2 result in Section 13.1 | | Same PBKDF2 metadata with `hashLength:64` added | Still derive 32 bytes, as implemented; never derive a 64-byte XOR factor | | Change one ciphertext byte without resigning | Reject field authentication | | A correctly signed token whose used ciphertext fails GCM authentication | Reject that recovery attempt; do not create a new account | | Missing signature or a DER-shaped last field that does not verify | Reject; do not strip it by appearance | | Put UTF-8 `3` in the v3 marker field | Reject unsupported layout/version | | Unknown KDF algorithm, missing required Argon2id member, fractional/string iteration value, or KDF JSON over the supported bound | Reject before performing the KDF | | Argon2id `hashLength:64` | Reject as an unsupported UMP factor length; do not truncate | | Put the version after an empty profile placeholder | Reject this producer layout; do not treat it as a real encrypted profile | | Move the public-key lock to the end, insert an extra opcode, or use a nonminimal data push | Reject noncanonical UMP script | | Use a 31-byte discovery hash or hash the Base64 text of `R` | Reject format or fail to match the requested account, respectively | | Wrong password, `P`, or `R` | Hash mismatch and/or authenticated-decryption failure, never partial plaintext | | Corrupt/truncate a snapshot or reference a missing active profile | Import error; do not save a replacement empty-profile state | | Read a valid snapshot without an additional outer protection mechanism | Recover `A`; this demonstrates why the snapshot is secret-bearing | ### 13.8 Overlay state transitions These are state-machine vectors, independent of Bitcoin byte fixtures. Each transaction and claimed source output is assumed to have passed normal transaction validation. `a`, `b`, and `c` are distinct outpoints; `p0`, `p1`, `r0`, and `r1` are distinct 32-byte hash values. Except where stated, each row starts with admitted owner `a` holding `(presentation,p0)` and `(recovery,r0)`, and no other reservation. “Consumes” means inclusion in the transaction's actual recognized UMP input set. | Candidate/event | Consumes | Expected result | | --- | --- | --- | | Initial `a(p0,r0)` in an empty store | None | Reserve both, then admit/index and commit both | | Replay `a(p0,r0)` | None | Idempotent; no second record | | `b(p0,r0)` | None | Reject both-hash collision | | `b(p0,r1)` | None | Reject presentation collision | | `b(p1,r0)` | None | Reject recovery collision and roll back the provisional `p1` claim | | `b(p0,r0)` | `a` | Stage and then commit successor ownership to `b`; `a` leaves live lookup | | `b(p1,r0)` | `a` | Admit presentation rotation; transfer `r0`, acquire `p1`, release `p0` on spend | | `b(p0,r1)` | `a` | Admit recovery rotation; transfer `p0`, acquire `r1`, release `r0` on spend | | `b(p0,r0)` with evidence merely mentioning an ancestor of `a` | No current owner | Reject: a shared ancestor reference does not consume the current owner | | `b(p0,r0)` and `c(p0,r1)` in one transaction | `a` | The later conflicting output cannot also acquire `p0` | | Concurrent unrelated initial claims for the same hash in an empty shared store | None | At most one active provisional winner; at most one admitted owner | | Pending successor `b` fails or expires | `a` had been the proposed input | Restore/retain admitted owner `a`; do not expire it | | Dry-run validation of `b` | `a` | No reservation or ownership mutation | | Spend `a` with no successor retaining `p0` | `a` | Remove live `a`; release `p0`, not a permanent historical lock | | After actual release, new `b(p0,r1)` | None | May acquire free hashes; this does not prove continuity with `a` | For a two-owner vector, start with `a(p0,r0)` and `b(p1,r1)`. Candidate `c(p0,r1)` consuming only `a` fails because `r1` belongs to `b`; consuming both satisfies the per-hash ownership checks. For a namespace vector, a new presentation hash equal to the **bytes** of another account's recovery hash does not conflict merely through that cross-namespace equality. ### 13.9 Lookup and selection scenarios | Evidence or request | Expected result | | --- | --- | | The Section 13.1 presentation key | Query `presentationHash` equal to legacy field 6, not the presentation public key | | The Section 13.1 recovery key | Query `recoveryHash` equal to legacy field 7 | | Two valid selectors in one query | Apply presentation/recovery/outpoint precedence, not conjunction | | Valid presentation selector plus malformed recovery selector | Query error, despite the presentation selector's precedence | | Same outpoint returned by several hosts with different history depth | One candidate; retain the deeper usable evidence | | Source and update from Section 13.5, with their connecting history | Select the update as the sole unsuperseded candidate | | A three-rendition chain with the middle rendition absent from lookup but present in BEEF ancestry | Select the surviving tip using the available spend evidence | | Two unrelated validated same-hash initial tokens, neither with trusted continuity | Ambiguous; do not select the first response | | One surviving proven same-identity continuation and one unrelated initial token | The deployed legacy heuristic prefers the continuation; do not call it a global first-claim proof | | Two surviving proven continuations | Remain ambiguous unless stronger evidence resolves the conflict | | A WAB pin naming an output absent from the validated matching candidate set | Ignore the pin; do not fetch-and-trust it as an override | | A WAB pin naming a validated candidate, but normal lineage selection is already decisive | Normal lineage selection wins | | One validated matching account plus failed, empty, or malformed peers | Preserve the positive account evidence | | Only failures and no clean empty reply | Indeterminate/unavailable, not new-account success | | No validated match and one clean empty reply | Current interactor can return not-found; existing-account evidence still prevents blind onboarding | | A response at its 100-record limit | Treat completeness as unproven; do not use insertion order as ownership priority | ## 14. Implementations and compatibility notes This document was checked against the following pinned source snapshots. These links are evidence of implemented behavior, not normative dependencies needed to interpret the format: * [Wallet Toolbox `CWIStyleWalletManager`](https://github.com/bsv-blockchain/ts-stack/blob/daca1565f383184bc3bfe0718b34a13e3583abd5/packages/wallet/wallet-toolbox/src/CWIStyleWalletManager.ts): core factors, field order, legacy/v3 KDF handling, profiles, publication, lookup selection, and local serialization. * [Wallet Toolbox `WalletAuthenticationManager`](https://github.com/bsv-blockchain/ts-stack/blob/daca1565f383184bc3bfe0718b34a13e3583abd5/packages/wallet/wallet-toolbox/src/WalletAuthenticationManager.ts) and [WAB client](https://github.com/bsv-blockchain/ts-stack/blob/daca1565f383184bc3bfe0718b34a13e3583abd5/packages/wallet/wallet-toolbox/src/wab-client/WABClient.ts): presentation-key delivery, account continuity, registration, optional pins, and staged phone changes. * [UMP Topic Manager](https://github.com/bsv-blockchain/ts-stack/blob/daca1565f383184bc3bfe0718b34a13e3583abd5/packages/overlays/topics/src/ump/UMPTopicManager.ts), [identity reservation store](https://github.com/bsv-blockchain/ts-stack/blob/daca1565f383184bc3bfe0718b34a13e3583abd5/packages/overlays/topics/src/ump/UMPIdentityStore.ts), and [lookup service](https://github.com/bsv-blockchain/ts-stack/blob/daca1565f383184bc3bfe0718b34a13e3583abd5/packages/overlays/topics/src/ump/UMPLookupService.ts): shared reservations, first-accepted admission, spend/eviction lifecycle, legacy migration, query precedence, and bounded result ordering. * [SDK symmetric encryption](https://github.com/bsv-blockchain/ts-stack/blob/daca1565f383184bc3bfe0718b34a13e3583abd5/packages/sdk/src/primitives/SymmetricKey.ts), [key derivation](https://github.com/bsv-blockchain/ts-stack/blob/daca1565f383184bc3bfe0718b34a13e3583abd5/packages/sdk/src/wallet/KeyDeriver.ts), [PushDrop](https://github.com/bsv-blockchain/ts-stack/blob/daca1565f383184bc3bfe0718b34a13e3583abd5/packages/sdk/src/script/templates/PushDrop.ts), and [canonical envelope validation](https://github.com/bsv-blockchain/ts-stack/blob/daca1565f383184bc3bfe0718b34a13e3583abd5/packages/sdk/src/script/templates/PushDropValidation.ts). * [Metanet Client Desktop](https://github.com/p2ppsr/metanet-client-desktop/tree/1743ecb6fe9099f413f53cb9632a9e756d18ce8a), including its [wallet construction](https://github.com/p2ppsr/metanet-client-desktop/blob/1743ecb6fe9099f413f53cb9632a9e756d18ce8a/src/WalletContext.tsx), [recovery-key export](https://github.com/p2ppsr/metanet-client-desktop/blob/1743ecb6fe9099f413f53cb9632a9e756d18ce8a/src/components/RecoveryKeyHandler.tsx), and [recovery without the presentation factor](https://github.com/p2ppsr/metanet-client-desktop/blob/1743ecb6fe9099f413f53cb9632a9e756d18ce8a/src/pages/Recovery/LostPhone.tsx). * [Metanet Explorer](https://github.com/p2ppsr/metanet-explorer-mobile/tree/a33b37cc30dcaa71dd1c59811fd26e8252511f2f), including its [wallet construction](https://github.com/p2ppsr/metanet-explorer-mobile/blob/a33b37cc30dcaa71dd1c59811fd26e8252511f2f/context/WalletContext.tsx), [recovery UI](https://github.com/p2ppsr/metanet-explorer-mobile/blob/a33b37cc30dcaa71dd1c59811fd26e8252511f2f/app/auth/recovery.tsx), and [recovery-key export](https://github.com/p2ppsr/metanet-explorer-mobile/blob/a33b37cc30dcaa71dd1c59811fd26e8252511f2f/components/RecoveryKeySaver.tsx). The standalone [older UMP services repository](https://github.com/bsv-blockchain/ump-services/tree/be60909707e8b1d91b664b49fe60bc19455ce5b8) is historical evidence for field compatibility; its simpler shape-based Topic Manager is not evidence that first-accepted reservations are universally deployed. An operator must inspect the actual software serving its topic. Likewise, a particular UI's recovery menu is not the complete set of cryptographic recovery possibilities. Several implementation distinctions must be preserved when applying this specification: 1. **Descriptor validation and historical overlay validation differ.** The contemporary wallet verifies the canonical envelope and field signature, while historical topic managers accept a broader set of shapes. Publicly indexable data is not automatically an authenticated, usable account. This proposal's validation requirements do not certify those older hosts. 2. **Password migration must carry its KDF.** In the pinned manager, the legacy password-change path selects its configured new-account KDF, while the common factor updater preserves metadata from the old token. For a metadata-free token and the default Argon2id configuration, those two decisions do not form a valid migration. Implementers MUST follow Section 4.4 rather than copy that mismatch. Non-password updates correctly need to preserve the legacy interpretation; a password migration needs to pass the newly selected metadata with the newly derived factor. This is an implementation compatibility issue, not another implicit wire version to guess during login. 3. **Validate actual factor lengths.** The general KDF configuration validator accepts some output lengths that do not fit 32-byte XOR factors. PBKDF2's implemented fixed output length and Argon2id's required UMP factor length are specified separately in Section 4.3. Scalar-to-byte conversions must also preserve or restore leading zeros as described in Section 5.1. 4. **Replication and conflict convergence are different.** The production shared reservation store defines per-host first-accepted ownership. A process-local test store is not a substitute for coordination across serving replicas, and neither is a global ordering mechanism between disconnected hosts. 5. **Absence and recovery UI are policy boundaries.** A clean empty reply is not a nonexistence proof, and a WAB-based screen is not a reason to withhold the independent recovery algorithm. Implementations should expose their actual guarantees plainly. The vectors were generated with `@bsv/sdk` 2.8.8, Node's AES-GCM/PBKDF2, and `hash-wasm` 4.12.0, then checked independently using Python's `hashlib`/`hmac`, `cryptography`, and the Argon2 reference binding. Independent checks covered KDF outputs, self-derived keys, every key envelope, profile encryption, all four field signatures and scripts, the update transaction signature and IDs, and the local record/snapshot bytes. The six factor-pair recovery cases and four authenticated field layouts were also checked against `@bsv/wallet-toolbox-client` 2.14.2. These are offline conformance fixtures, not evidence of live account publication or universal deployment conformance. ## 15. Security and operational considerations A WAB is trusted to protect and correctly return one factor and to describe its authentication state honestly. Possession of `P` alone does not directly supply a root, but it permits offline password guesses against the published password/presentation envelope. Possession of `R` similarly permits guessing against its password envelope. Password strength and the account's actual KDF matter; “two of three” must not be advertised as making a weak password harmless. Legacy PBKDF2 support is an accessibility requirement, not a recommendation to keep creating weakly stretched accounts. The recovery key should be stored independently of the presentation backend and ordinary session data. A backend holding both `P` and `R` can recover without the password. A root privileged key can recover the wrapped factors, and a root primary key can authorize token spends. Their different roles do not eliminate the need to protect both roots and the client that uses them. Public discovery hashes and token histories reveal linkage across renditions that retain a hash or share a spend lineage. Profiles are encrypted, but their existence, ciphertext sizes, and account updates may still be observable. Sending discovery hashes to providers discloses those queries to the providers. Hashes of random factors are not permission to publish the factors, local snapshots, passwords, or WAB authentication payloads in diagnostics. First-accepted reservations protect a live claim against later unrelated same-hash admission. They do not authenticate civil identity, settle namespace disputes, prove network-wide first publication, or make a compromised backend honest. Clients must validate continuity and preserve independent recovery evidence. Pins are support decisions with a trust cost, not cryptographic proof. Backups and overlays serve different purposes. An overlay can provide the public encrypted descriptor and its history, but cannot replace private wallet storage, encrypted application files, or a recovery key that was never saved. Operators should retain enough transaction/history evidence to recover after failures and should not evict ownership records casually. Users should be able to change client, WAB, overlay providers, and storage arrangements without being forced to generate unrelated account roots. ## References * [Ty Everett, *Computing with Integrity* (2020)](https://github.com/open-cash-standards/papers/blob/889f38bcbce8773718b248b1704a91e0a19567dd/computing-with-integrity/paper.pdf): conceptual background, particularly the key definitions and bootstrap process. * [BRC-42: BSV Key Derivation Scheme](../key-derivation/0042.md), [BRC-43: Security Levels, Protocol IDs, Key IDs and Counterparties](../key-derivation/0043.md), and [BRC-44: Admin-reserved and Prohibited Key Derivation Protocols](../key-derivation/0044.md). * [BRC-48: Pay to Push Drop](../scripts/0048.md) and [BRC-100: Wallet-to-Application Interface](./0100.md). * [BRC-22: Overlay Network Data Synchronization](../overlays/0022.md), [BRC-24: Overlay Network Lookup Services](../overlays/0024.md), and [BRC-64: Overlay Network Transaction History Tracking](../overlays/0064.md). * [BRC-62: BEEF](../transactions/0062.md), [BRC-95: Atomic BEEF](../transactions/0095.md), and [BRC-76: Graph Aware Sync Protocol](../transactions/0076.md). * [BRC-87: Topic and Lookup Naming](../overlays/0087.md), [BRC-88: Overlay Services Synchronization Architecture](../overlays/0088.md), and [BRC-183: A Framework for Strict and Federated Overlays](../overlays/0183.md). * [BRC-184: Optional Metadata Registries and Their Stewardship](./0184.md) and [registry submission and maintenance guide](../REGISTRY_SUBMISSIONS.md). * [RFC 8018](https://www.rfc-editor.org/rfc/rfc8018): PBKDF2; [RFC 9106](https://www.rfc-editor.org/rfc/rfc9106): Argon2; [NIST SP 800-38D](https://doi.org/10.6028/NIST.SP.800-38D): GCM; [RFC 4648](https://www.rfc-editor.org/rfc/rfc4648): Base64. * [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119) and [RFC 8174](https://www.rfc-editor.org/rfc/rfc8174): requirement terminology.