Part V · Toward real engineering A wallet is not an address, and it is not a private key.
Three threads planted earlier come due here:
Chapter 2: “one key can make unlimited banks”
Chapter 6: a UTXO must be spent whole, so change addresses keep appearing
Chapter 12: reusing an address exposes your balance
Put together, a normally used wallet accumulates hundreds or thousands of addresses, each with its own private key.
Which raises a very practical question:
Must the user back up a thousand private keys?
15.1 This chapter is a toy, and says so¶
Everything here uses hash-based derivation so you can see the structure.
Real Bitcoin uses BIP39 (mnemonics), BIP32 (hierarchical derivation) and BIP44/84 (path conventions), which involve PBKDF2, HMAC-SHA512, chain codes, hardened derivation, and more.
Never use the toy version with real money. It is a map, not an implementation.
import hashlib
def H(s): return hashlib.sha256(s.encode()).hexdigest()
WORDS = "apple river moon school orange winter cloud piano".split()
mnemonic = " ".join(WORDS)
def mnemonic_to_seed(words, passphrase=""):
'''Toy: mnemonic + optional passphrase -> seed. Real BIP39 uses PBKDF2.'''
return H("mnemonic|" + words + "|" + passphrase)
seed = mnemonic_to_seed(mnemonic)
print("mnemonic:", mnemonic)
print("seed:", seed[:24], "...")
mnemonic: apple river moon school orange winter cloud piano
seed: fffbb3ee9d30ff05bfb6c9ea ...
Start with what a mnemonic is not:
A mnemonic is not an address.
A mnemonic is not a private key.
It is the entrance to an entire key tree.Those eight English words go through a function and become a seed — a 64-character
hex string. Real BIP39 uses 12 or 24 words drawn from a fixed list of 2048.
(Why words instead of raw hex? Because humans copy words correctly. In 64 hex characters,
b and 6, 0 and O get miscopied constantly; “apple river moon” does not. This is pure
human-factors engineering, not cryptography.)
15.2 HD = hierarchical + deterministic¶
Hierarchical: nested like folders.
Deterministic: the same seed grows exactly the same keys, every time.The shape:
seed
|
master
|
m/0 m/1
| |
m/0/0 m/1/0
m/0/1 m/1/1def master_key(seed):
return H("master|" + seed)
def derive(parent_key, index):
'''Toy child derivation. Real BIP32 also has chain codes and hardening.'''
return H("child|" + parent_key + "|" + str(index))
def derive_path(seed, path):
'''A path like m/0/1/2.'''
key = master_key(seed)
if path == "m":
return key
for part in path.split("/")[1:]:
key = derive(key, int(part))
return key
for path in ["m", "m/0", "m/0/0", "m/0/1", "m/1/0"]:
print(path, "->", derive_path(seed, path)[:18], "...")
m -> e3a8c97baf3823cf91 ...
m/0 -> 827769c859bd8a36c9 ...
m/0/0 -> f64d20feee002ad2ed ...
m/0/1 -> a6eeca7890d9f773b4 ...
m/1/0 -> d1f45fe362855fa9f3 ...
derive_path is a for loop that hashes once per level of the path.
m = H("master|" + seed)
m/0 = H("child|" + m + "|0")
m/0/1 = H("child|" + m/0 + "|1")A “key tree” is not a mysterious structure. It is “hash repeatedly, carrying an index each time.”
And the whole meaning of deterministic is: hashing is a pure function (chapter 1). Same seed, same path, same key — today, next year, on any machine.
That is why backing up a set of words restores an entire wallet. You are not backing up keys. You are backing up the recipe that grows them.
15.3 From child key to address¶
def priv_to_pub(priv):
return H("pub|" + priv)[:32]
def pub_to_address(pub):
return "toy1" + H("addr|" + pub)[:24]
def address_at(seed, path):
priv = derive_path(seed, path)
pub = priv_to_pub(priv)
return pub_to_address(pub)
for i in range(5):
path = f"m/0/{i}"
print(path, "->", address_at(seed, path))
m/0/0 -> toy1cbaff0d44b1a5128976996c0
m/0/1 -> toy1d6169b9a4f3f8ddd4c2eaf81
m/0/2 -> toy1b919bc758f1023bdac9fa27c
m/0/3 -> toy1a76250a920c418a55cdd2fca
m/0/4 -> toy1b88c25397d593378fd856d83
The pipeline:
child private key -> public key -> public key hash -> addressNote priv_to_pub is fake here (one hash). In reality it is chapter 13’s
scalar_mul(priv, G) — elliptic curve hopping. And pub_to_address is really chapter 14’s
HASH160, plus Bech32 encoding and a checksum.
Apart from those two substitutions, the structure is identical.
Run it and you get five addresses that look completely unrelated. An outsider cannot tell they belong to the same person — yet all five recover from the same mnemonic.
Why your wallet keeps handing you fresh addresses:
m/0/0was used, so next time you getm/0/1, thenm/0/2. Each is new; all grow on the same tree.
That is property ② from section 2.0’s table — unlinkability. One key, a hundred mutually anonymous banks.
15.4 That strange-looking path¶
In a real wallet you will see paths like this:
m / 84' / 0' / 0' / 0 / 0Don’t panic. Read it as a folder path.
path_meaning = [
("m", "root"),
("84'", "address type: native SegWit"),
("0'", "coin: Bitcoin mainnet"),
("0'", "account 0"),
("0", "external receiving address"),
("0", "address index 0"),
]
for part, meaning in path_meaning:
print(f"{part:<4} -> {meaning}")
m -> root
84' -> address type: native SegWit
0' -> coin: Bitcoin mainnet
0' -> account 0
0 -> external receiving address
0 -> address index 0
Three practical facts:
The 0 and 1 at level five are a convention:
0is for addresses you hand out,1is for change. Those change outputs from chapter 6 live on them/84'/0'/0'/1/*branch.The
84'determines what your addresses look like:44'gives old-style1...,84'givesbc1q...,86'gives Taproot’sbc1p.... Same mnemonic, different branch, completely different set of addresses — which is the real reason behind “I imported my seed phrase and the balance is zero.” The money is not lost. You are looking at the wrong branch of the tree.
The apostrophe means hardened derivation. It solves a genuine security problem: BIP32 lets you derive all child public keys from an extended public key (convenient — a server can generate receiving addresses without any private key). But if one child private key leaks alongside that extended public key, the parent private key can be recovered. Hardened derivation cuts that path — at the cost of no longer deriving child public keys from public keys alone.
Another convenience-versus-safety trade, and again not a mathematical question.
15.5 Backup and loss¶
seed_again = mnemonic_to_seed(mnemonic)
print("same seed?", seed_again == seed)
print("same address at m/0/3?", address_at(seed_again, "m/0/3") == address_at(seed, "m/0/3"))
wrong_seed = mnemonic_to_seed("apple river moon school orange winter cloud wrong")
print("change one word -- same address?", address_at(wrong_seed, "m/0/3") == address_at(seed, "m/0/3"))
same seed? True
same address at m/0/3? True
change one word -- same address? False
The first two print True, the last prints False.
Change one word and the entire tree is different — avalanche again (chapter 1), except here it protects you from a mistyped word quietly landing in someone else’s wallet.
The mnemonic is the root of the whole wallet. Lose it and the wallet may be gone forever. Let someone see it and they can grow the same tree and take everything.
That deserves saying more harshly: in Bitcoin, “knowing the mnemonic” and “owning the money” are the same thing. No support line, no recovery, no appeal. That is the cost of decentralization, and it is also its definition.
YOUR TURN¶
# YOUR TURN:
# 1) Print addresses m/1/0 through m/1/4. How do they relate to the m/0/x ones?
# 2) Pass passphrase="secret" to mnemonic_to_seed. Do all the addresses change?
# (This is the real "25th word" / hidden wallet feature.)
# 3) Why do wallets keep issuing new addresses instead of reusing one?
Question 2 points at a real feature: a BIP39 passphrase derives an entirely different tree. Same mnemonic plus a different passphrase equals a different wallet. Some people use it as a duress wallet — the tree you hand over under coercion holds only a small balance.
15.6 The ledger¶
| What you wrote | Field |
|---|---|
mnemonic_to_seed() — words to seed | Key management |
derive_path() — repeated hashing grows a tree | Key management |
The path convention m/84'/0'/0'/0/0 | Standardization / interoperability |
| Words rather than hex for backup | Human factors |
| Hardened derivation | Security engineering |
Zero cryptography — hashing here is a derivation function, not a security primitive.
The sentence worth keeping:
A wallet is not an address and not a private key. A wallet is a key tree that can be regrown from a mnemonic.
Next¶
Hardware signing.
This chapter turned your private key into a tree. The next one asks a more dangerous question:
Which machine should the root of that tree live on?
If it lives on a networked computer, that computer can be infected, remotely controlled, and have its disk read from end to end.
The next chapter’s answer is a small room with no network cable in it.