Derivation and wallet identity — v1¶
This document defines how a PiWalletSV wallet derives its keys and addresses from a BIP39 mnemonic, and how the resulting wallet is identified on the wire.
1. Seed¶
A wallet is created from a BIP39 mnemonic. Both 12-word and 24-word mnemonics are supported. The seed is computed per BIP39:
seed = PBKDF2-HMAC-SHA512(
password = NFKD(mnemonic),
salt = "mnemonic" || NFKD(passphrase), # passphrase is "" for v1
iterations = 2048,
dkLen = 64,
)
passphrase is the empty string in v1. We may expose an optional
user-supplied passphrase in v2.
The resulting 64-byte seed is the input to BIP32 master derivation.
2. Master and account keys¶
BIP32 master derivation is unchanged from the spec:
I = HMAC-SHA512(key = "Bitcoin seed", data = seed)
master_xprv = I[0..32] # 32-byte private key
master_chain = I[32..64] # 32-byte chain code
The signer keeps the master xprv encrypted at rest (see the security chapter for details). The account-level xprv is derived once and is the key that's actually exposed through the public API.
The account-level path is fixed at:
That is:
44'— BIP44 purpose (hardened).236'— BSV's SLIP-44 coin type (hardened).0'— account index, hardened. v1 ships with a single account per wallet; multi-account is deferred to v2.
A wallet's exported xpub_export.path field MUST be the literal
string "m/44'/236'/0'". Any other value SHOULD cause a companion to
reject the envelope.
3. Address derivation¶
Receive and change addresses are derived non-hardened from the account xpub:
receive_i = account_xpub / 0 / i # external chain, i ∈ [0, 2^31)
change_i = account_xpub / 1 / i # internal chain, i ∈ [0, 2^31)
The on-the-wire derivation arrays in unsigned_proposal are
[branch, index] two-tuples; branch is 0 (receive) or 1 (change)
and index is a non-hardened 31-bit unsigned integer.
The signer MUST reject:
branchnot in{0, 1};index < 0;index >= 2^31(=0x80000000, which would be a hardened index the signer cannot derive from an xpub anyway).
3.1 Address format¶
PiWalletSV v1 supports P2PKH addresses only on BSV mainnet.
pubkey = secp256k1 point at <xpub>/<branch>/<index>
hash160 = RIPEMD160(SHA256(serialized_compressed_pubkey))
payload = 0x00 || hash160 # 0x00 = BSV mainnet P2PKH version
checksum = SHA256(SHA256(payload))[0..4]
address = base58(payload || checksum)
The compressed public key is 33 bytes (0x02|0x03 || X). Other
encodings (uncompressed 0x04 || X || Y, hybrid, etc.) MUST NOT be
used.
The corresponding P2PKH locking script for an address is the standard Bitcoin script:
In hex this is 76a914 <hash160> 88ac (25 bytes total).
unsigned_proposal.outputs[*].script MUST contain this hex form,
lowercase. Companions building proposals SHOULD construct the script
from the recipient's base58 address with the inverse decode; they
SHOULD NOT inline an address string into the envelope.
Future protocol revisions may add P2PK, multisig, or token scripts; v1 deliberately constrains the script set so the signer's parse can be small and total.
4. Wallet fingerprint¶
The companion identifies a paired wallet (and routes proposals back to it) by a 4-byte self fingerprint of the account xpub:
This is the BIP32 "fingerprint" of the account-level xpub itself.
Note that this is not the parent_fingerprint field that BIP32
serializes inside an extended key, which references the parent's
public key. We use the self-fingerprint because two unrelated wallets
will always have different self-fingerprints, whereas they could
share a parent-fingerprint if they derive from the same intermediate
node.
A companion MUST:
- Compute the expected self-fingerprint locally when it receives an
xpub_exportand compare it to the envelope'sfpfield. If they don't match, the companion MUST refuse to save the wallet. - Include the same 4-byte value as
walletFpin everyunsigned_proposalit sends back, so the signer can route the proposal to the right vault entry when more than one wallet is stored. - Surface the fingerprint to the user in hex (8 lowercase hex chars, no separator) — this is how PiWalletSV's bonnet UI and companion UI both display it.
A signer MUST:
- Refuse to sign any proposal whose
walletFpdoesn't match a stored wallet's self-fingerprint. - Refuse to emit a
signed_txwhosewalletFpvalue differs from the proposal it just signed.
5. Gap-limit conventions¶
Address discovery on the companion side follows BIP44's gap-limit rule. PiWalletSV defaults to 20 unused addresses in a row before abandoning a branch. Implementations MAY use a larger gap, but SHOULD NOT go lower; older wallets paired against the same xpub may have created sparse usage patterns.
The "next receive index" pointer the companion stores per wallet
(nextReceiveIndex in the reference implementation) is purely a
companion-side convenience: the signer does not read or care about
it. A companion is free to invent its own bookkeeping.
6. Sample / canonical wallet¶
The canonical test vectors in tests/fixtures/addresses_canonical.json
use the BIP39 mnemonic:
with an empty passphrase. The fixture records the account xpub, its 4-byte fingerprint, and the first few receive/change addresses. Any v1-conformant implementation MUST reproduce them exactly.
See conformance.md for the file's exact format
and how to use it as a regression check.