Skip to content
BIP32 HD Wallets
step 1/5

Reading — step 1 of 5

Read

~3 min readCryptography Foundations

BIP32 HD Wallets

Early Bitcoin wallets generated a fresh random key per address and kept a keypool file. Lose the file — or restore a backup that predates a key — and coins are gone forever. Hierarchical Deterministic wallets (BIP32, 2012) fixed this with one idea: derive an unbounded tree of keys from a single seed, deterministically. Back up 16–64 bytes once (in practice a 12–24-word BIP39 mnemonic); recover every key you will ever create.

Master key: one HMAC

I = HMAC-SHA512(key = b"Bitcoin seed", msg = seed_bytes)
master_private_key = I[:32]
master_chain_code  = I[32:]

Note the roles: the ASCII string "Bitcoin seed" is the HMAC key — a fixed protocol constant — and your seed is the message. The 64-byte output splits into two 32-byte halves: the private key, and the chain code — extra entropy carried alongside the key so that knowing a private key alone is not enough to derive its children. Key + chain code together are called an extended key.

Child derivation (this course's educational variant)

h         = HMAC-SHA512(key = chain, msg = chain || index_be4 || key_be32)
child_key = (key + int(h[:32])) mod N        # N = secp256k1 group order
child_chain = h[32:]

Applied once per path segment: m/0/1 means derive child 0 of the master, then child 1 of that. The mod N matters — a child key is used as an ECDSA scalar, so it must stay inside the group order (and additive derivation is what real BIP32 uses too, for a deep reason: it makes public-key derivation possible without private keys, since (k + h)·G = k·G + h·G).

Be honest about where we simplify. Real BIP32's non-hardened message is serP(parent_pubkey) || ser32(index) — the public key, and the chain code appears only as the HMAC key, never in the message. Ours feeds chain || index || private_key for implementation clarity (no elliptic-curve code needed). The master step, however, is exactly the spec: test 0's expected output is the genuine BIP32 Test Vector 1 master key — your code reproduces the real standard for m.

Hardened derivation, and why real wallets care

Real BIP32 splits indexes: below 2^31 is normal (children derivable from the extended public key — great for watch-only servers generating receive addresses), at or above 2^31 is hardened (requires the private key). The reason is a sharp edge every wallet engineer memorizes: with non-hardened derivation, an attacker holding the extended public key (xpub) plus any one child private key can algebraically recover the parent private key — and with it, every sibling. Hardened derivation at the account level (BIP44's m/44'/0'/0') firewalls that. Our variant is non-hardened only.

Your exercise

Input <seed_hex>|<path>; print KEY:<64 hex> and CHAIN:<64 hex>. Path m is just the master — no derivation steps. The graded mistake, and it's nearly universal on first attempt: HMAC-ing the seed as ASCII text instead of decoded bytes. bytes.fromhex("ff") is one byte, b"ff" is two — the hidden test ff|m/255 exists precisely to catch it, and even test 0 (000102...0f|m) fails loudly, printing something other than the canonical KEY:e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35. Runners-up: swapping the halves (key is the first 32 bytes), packing the index little-endian (it's struct.pack(">I", idx) — big-endian), and skipping mod N on the addition.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…