钱包与助记词:BIP-39/32/44(师生对话实录)
Web3 区块链系列 · 阶段 0 · 地基与密码学 · 第 4/57 篇
上一篇:《公私钥与数字签名:从私钥到地址》 · 下一篇:《手写 100 行迷你区块链》
学习大纲:《Web3 区块链学习总纲》
写在前面
第 3 篇结尾我留了个心眼:私钥是 64 个十六进制字符的随机数,可我装 MetaMask 的时候,它让我抄写的却是 12 个英文单词——这两样东西凭什么等价?而且这 12 个词好像不止管一个地址,还能「换链」用?
继续对话老办法:AI 当老师我当学生,每课一个概念,有问题就打断。本篇把「钱包」这个词的外壳拆掉——你会发现链上根本没有钱包这个东西,有的只是一棵从 12 个词长出来的密钥树。
课程路线图:
① 钱包的本质:密钥管理器 → ② BIP-39:12 个词怎么编码随机数 → ③ 助记词 → 种子 → ④ BIP-32:一棵密钥树 → ⑤ BIP-44:路径规则 → ⑥ keystore 与冷热钱包
环境:WSL2 Ubuntu-22.04 + Python 3.10(pip3 install mnemonic bip_utils)。本文助记词每次运行随机生成、当次即弃,读者自己的助记词永远不要在任何机器上「演示」。规范出处:BIP-39、BIP-32、BIP-44。
第 1 课:钱包不是装钱的容器
🧑🏫 老师:
先纠正一个几乎所有新人(包括三天前的你)都有的默认想象:钱包 ≈ 装币的袋子。链上不存在「钱包」这个对象。
翻一下账本就明白了:以太坊的世界状态是一张表——地址 → {nonce, balance, code, storage}(2.1 篇细讲)。你的「资产」就是你地址下的一行余额数字,它躺在账本里,不躺在任何「钱包」里。私钥丢了,那行数字还在账上,只是你再也没有合法的签名能力去动它。
那 MetaMask 到底是什么?拆开看只有两样:
「钱包」的真实成分
├── 一把(或一棵)私钥 ← 秘密,必须守住
├── 签名工具 + 与链交互的界面 ← 代码,谁都可以有所以规范的说法:钱包 = 密钥管理器 + 签名工具。浏览器插件、手机 App、硬件设备……都是这套东西的不同外壳。理解了这一点,一堆问题自动有答案:为什么换个钱包 App 导入助记词,资产「还在」?(资产本来就在链上,换的只是签名工具。)为什么助记词能给所有钱包用?(它管理的是钥匙,不是资产。)——第 1 篇那句 "Not your keys, not your coins" 的完整版其实是:钥匙即钱包,账本即资产。
一句话收口:链上没有钱包对象,只有「地址 → 余额」的账本行;钱包 = 密钥管理器 + 签名工具,资产从不搬家。
第 2 课:BIP-39——12 个英文单词怎么装下 128 位随机数
🧑🎓 学生: 那 12 个单词和 64 个 hex 字符的私钥,到底谁是谁?
🧑🏫 老师:
先亲手生成一份,再拆原理:
from mnemonic import Mnemonic
m = Mnemonic("english")
mnemonic_words = m.generate(strength=128) # 128 位熵 = 12 词
print("助记词:", mnemonic_words)
print("词数:", len(mnemonic_words.split()))助记词: orchard remove rigid mean lottery deny grit excess wise mercy grid install
词数: 12原理是纯编码学。BIP-39 规定了一张 2048 个词的公开词表(每个词有固定编号 0~2047,恰好 11 位二进制):
128 位随机熵
│ + 4 位校验和(熵的 SHA-256 前几位)
▼
132 位 ──每 11 位切一段──► 12 个编号 ──查表──► 12 个英文单词所以「12 个词」不是什么新密码学,只是把 128 位随机数翻译成人能抄、能念、能跨语言校验的形式。为什么值得这么做?——64 个 hex 字符抄错一位没人发现;12 个单词抄错了,orchard 写成 orchid 这种错误当场校验得出来:
words = mnemonic_words.split()
tampered = words.copy(); tampered[3] = "zoo" # 换成词表里合法的词
print("原助记词校验:", m.check(mnemonic_words))
print("换词后校验: ", m.check(" ".join(tampered)))原助记词校验: True
换词后校验: False换成 zoo——一个完全合法的词表词——校验立刻失败。因为校验和是对整串熵算的:改动任何一位,132 位的「熵+校验」就对不上了。这就是那 4 位校验和买来的能力:抄写错误当场暴露,而不是等到三年后恢复钱包时才发现「当时抄错了一个词」。
(24 词 = 256 位熵 + 8 位校验,机制相同;词表有中英文等多语言版本,但跨语言不能混用,恢复时用什么语言生成就得用什么语言。)
一句话收口:BIP-39 = 128 位熵 + 4 位校验,11 位一组查 2048 词表编成 12 个词;「人话」只是外壳,随机数还是那个随机数,抄错当场翻脸。
插问 1:12 个词只有 128 位熵,比 256 位私钥「弱」一半,够吗?
🧑🎓 学生: 上一篇说私钥是 256 位、穷举要 10⁷⁷ 次。助记词只有 128 位熵——是不是钱包比裸私钥弱了「一半」?
🧑🏫 老师:
数字上确实小了天文数字倍(2¹²⁸ ≈ 3.4 × 10³⁸),但「一半」和「够不够」是两回事——密码学强度不是线性指标。
思考一下攻击者的成本:2¹²⁸ 次尝试,哪怕全世界所有计算机从宇宙大啸炸跑到今天也跑不完(可观测宇宙的年龄 ≈ 4.3 × 10¹⁷ 秒,每秒试 10¹⁸ 个也要 3.4 亿亿年)。128 位熵在工程上就是「不可穷举」,比特币生态选它是权衡的结果:
- 助记词要人抄写,24 词比 12 词多一倍的抄错面;
- 私钥层面的安全短板几乎从不在「被穷举」——钓鱼、恶意软件、助记词拍照上传网盘,这些才是真实死因。熵从 128 提到 256,挡不住任何一种真实攻击;
- 需要更高安全边际时选 24 词(256 位)就是了,MetaMask 创建时给的就是 12 词档。
顺带把「暴力破解助记词」的题也做了:12 个词的排列组合 2048¹² ≈ 2¹³²,其中通过校验和的有效组合只有 2¹²⁸——所以猜词的难度和猜 128 位随机数完全一样,校验和一点不泄露强度。
一句话收口:128 位熵在工程上等同不可穷举;助记词的真实风险是「被偷」不是「被猜」,加长词表防的是抄写错误。
第 3 课:助记词 → 种子——2048 轮拉伸,还有个「第 25 词」
🧑🏫 老师:
12 个词还不是钥匙的终点,下一步是变成 512 位的种子(seed):
from bip_utils import Bip39SeedGenerator
seed = Bip39SeedGenerator(mnemonic_words).Generate()
print("种子(512bit):", seed.hex()[:32], "…")种子(512bit): c7daab286e36752542ab8cae38ede8e2 …这一步用的是 PBKDF2-HMAC-SHA512,参数刻意设得很重:2048 轮迭代、盐是常量 "mnemonic"——目的是「拉伸」:让每次错误猜测都付 2048 轮哈希的成本,把离线爆破的速率压到地板。
重的地方在 passphrase(口令),俗称「第 25 词」:PBKDF2 的盐实际是 "mnemonic" + passphrase。同一份助记词,配不同口令,长出完全不同的种子、完全不同的钱包树。两个用法:
- 防胁迫的钱包分层:空钱包配空口令,真资产配只有你知道的口令——被逼交助记词时交出「诱饵树」;
- 副作用也要懂:口令忘了 = 和助记词丢了同罪(没有「找回口令」),而且不同钱包 App 对 passphrase 的支持深浅不一,别在恢复实验里给自己埋雷。
一句话收口:助记词经 PBKDF2(2048 轮)长成 512 位种子;passphrase 是平行的宇宙开关——同一助记词、不同口令、两棵互不相识的树。
第 4 课:BIP-32——从种子长出一棵密钥树
🧑🎓 学生: 种子有了,怎么就「无数把钥匙」了?上一篇明明说一个私钥推一个地址。
🧑🏫 老师:
上一篇的流水线没错,BIP-32 做的事是在它前面加了一层结构:把种子当作树根,用 HMAC-SHA512 一层层派生子密钥:
种子 ──HMAC("Bitcoin seed")──► 主私钥 + 主链码
│
┌────────────────────┼────────────────────┐
▼ ▼ ▼
子密钥 m/0 子密钥 m/1 … 子密钥 m/2³¹-1
│ │
▼ ▼
孙密钥 m/0/0 孙密钥 m/1/0 …(可继续往下)每个节点都是一对「私钥 + 链码」;父节点用链码和编号派生子节点,不需要「解开」任何一层。整棵树的性质极好:
- 确定性:同一棵种子永远长出同一棵树(下一课实测);
- 无限性:树可以无限深,密钥取之不尽——「每个 DApp 用一个新地址」不再是管理噩梦;
- 可分工:可以只把「子公钥」交给收款方(只读监督收入,不能花)——观察钱包的原理。
路径里的 '(如 m/44'/60')念「硬化(hardened)」,含义是:这一层用私钥参与派生(而不是用公钥),换来「兄弟节点之间、上下层之间互相推不出对方」的隔离。不必深究数学,记住工程效果:暴露某个子公钥,不会连累整棵树。
一句话收口:BIP-32 把种子变成一棵可无限派生、完全确定、层间隔离的密钥树;一个助记词 = 一整座钥匙工厂。
第 5 课:BIP-44——路径规则,钥匙工厂的「门牌号」
🧑🏫 老师:
树有了,但不同钱包软件得约定「以太坊地址从树的哪里摘」,否则 MetaMask 摘的和你自己脚本摘的对不上号。这个约定就是 BIP-44 的五段式路径:
m / 44' / 60' / 0' / 0 / 0
│ │ │ │ │ └─ 地址序号:0, 1, 2, …(每点一次「新地址」+1)
│ │ │ │ └──── 外部链/内部链:以太坊恒为 0(比特币用 1 做找零)
│ │ │ └─────── 账户:0 号账户、1 号账户…(MetaMask 左上角能切)
│ │ └──────────── 币种(coin type):60 = 以太坊,0 = 比特币
│ └────────────────── 目的:44 = 「这是 BIP-44 钱包」的固定标记
└────────────────────── 从主密钥开始实测——从我们这 12 个词派生 5 个地址:
from bip_utils import Bip44, Bip44Coins, Bip44Changes
for i in range(5):
addr = (Bip44.FromSeed(seed, Bip44Coins.ETHEREUM)
.Purpose().Coin().Account(0).Change(Bip44Changes.CHAIN_EXT)
.AddressIndex(i).PublicKey().ToAddress())
print(f" m/44'/60'/0'/0/{i} → {addr}")m/44'/60'/0'/0/0 → 0xDBC97fCf532DD15b804a815c659d0d546569cEc2
m/44'/60'/0'/0/1 → 0x8AB91b89A34153f633e74b4eb4a67baDECcfA1FD
m/44'/60'/0'/0/2 → 0x38b03159abC0046614d2dD9e3889C004349ebFe2
m/44'/60'/0'/0/3 → 0x534724852019c6314cC63AbF0b19c23390E9e1bc
m/44'/60'/0'/0/4 → 0x76d563Ba6c9A07da928A8b8BAc7e9bce8Da013c5占位大纲的验收点直接过了:序号递增,地址各不相同;而这些地址全部由同一份助记词掌控——MetaMask 里点「创建新账户」干的就是把序号 +1。再验确定性:
重新派生 m/44'/60'/0'/0/0: 0xDBC97fCf532DD15b804a815c659d0d546569cEc2 ← 与上面第 0 个一致(补充一个对照点:MetaMask 还支持「导入私钥」加账户——那种账户不在树上(路径是孤立的一把钥匙),所以导出助记词恢复钱包时不含它们。「助记词恢复不了导入的私钥账户」是客服高频问题,答案就在树外。)
一句话收口:BIP-44 = 全行业的钥匙门牌号;钱包点「新地址」就是序号 +1,恢复钱包就是按同一套门牌重摘一遍。
插问 2:同一助记词为什么能在所有链上用?
🧑🎓 学生: 我把同一份助记词导入比特币钱包和以太坊钱包,各自能管各自的资产——一棵树怎么结出两种果子?
🧑🏫 老师:
答案就藏在路径第二段 coin type。实测同一份助记词:
btc = (Bip44.FromSeed(seed, Bip44Coins.BITCOIN)
.Purpose().Coin().Account(0).Change(Bip44Changes.CHAIN_EXT)
.AddressIndex(0).PublicKey().ToAddress())
eth = (Bip44.FromSeed(seed, Bip44Coins.ETHEREUM)
.Purpose().Coin().Account(0).Change(Bip44Changes.CHAIN_EXT)
.AddressIndex(0).PublicKey().ToAddress())
print("m/44'/0'/0'/0/0 (比特币, coin=0) →", btc)
print("m/44'/60'/0'/0/0 (以太坊, coin=60) →", eth)m/44'/0'/0'/0/0 (比特币, coin=0) → 1B7phQtfWTT3opFmRA6DTNdm1W77ibc6nQ
m/44'/60'/0'/0/0 (以太坊, coin=60) → 0xDBC97fCf532DD15b804a815c659d0d546569cEc2同一棵树、同一条流水线,coin type 从 0 换到 60,摘下来的就是比特币的私钥和以太坊的私钥(后面各自走第 3 篇的推导:一个出 1 开头地址,一个出 0x 地址)。
所以「通用助记词」的真相:不是各条链都认这 12 个词,而是各链钱包都按 BIP-39/32/44 同一张图纸摘钥匙——图纸是比特币生态定的(BIP 系列本就是 Bitcoin Improvement Proposal),以太坊等后来者照抄了工厂设计,只在 coin type 段分了岔。注册表里 60 以外还有几百个币种编号,含义同构。
风险也顺理成章:这 12 个词丢了,你在所有链上的资产一起丢;泄露也一起泄露。通用性的代价是把鸡蛋彻底放进了同一只篮子。
一句话收口:跨链通用 = 各链钱包都按 BIP 同一张图纸摘钥匙,coin type 段分岔;一份助记词是一篮子全部鸡蛋。
第 6 课:keystore 文件与冷热钱包——钥匙怎么存
🧑🎓 学生: 私钥、助记词都是明文秘密,总不能天天在电脑里裸放吧?存法上有什么讲究?
🧑🏫 老师:
(先补齐占位清单最后一块:keystore。)老牌钱包(geth、MyCrypto 时代)的标准存法是 Web3 Secret Storage 格式,俗称 keystore JSON:
明文私钥 ──用「你的密码」经 scrypt/kdf 拉伸出密钥──► AES 加密 ──► keystore.json特点:文件本身可以相对放心地放磁盘/网盘,但密码不在这文件里——解锁时要人工输密码解密。注意它只是「又一层口令」,和 passphrase 一样:密码忘 = 私钥亡。
存法的完整版图按「钥匙接触不接触联网设备」排:
| 形态 | 私钥住在哪 | 信任假设 | 适合 |
|---|---|---|---|
| 热钱包(浏览器/手机插件) | 联网设备的内存/磁盘 | 设备无恶意软件 | 小额、日常交互 |
| 冷钱包(离线签名) | 永不联网的机器/纸 | 那台机器+打印过程安全 | 中长期持有 |
| 硬件钱包(Ledger/Trezor) | 专用安全芯片,私钥物理上出不来 | 设备固件 + 厂商 | 大额长期(仍然「not your keys」的解药之一) |
| 多签(Safe 类) | 一组钥匙,m-of-n 才能动 | 分散的多把钥匙持有者 | 团队金库、DAO(4.5 篇实操) |
| keystore 文件 | 密码加密的 JSON | 密码强度 + 文件保管 | 兼容/备份场景 |
排布的底层逻辑只有一条:签名发生在哪里,信任就必须押在哪里。硬件钱包的本事是「把签名关进芯片」——交易进去、签名出来,私钥全程不出笼;多签更进一步,「单点被攻破」都凑不齐 m 把钥匙。第 1 篇说的「自托管的对价是自己负责」,到本课就是一张选型表。
一句话收口:存钥匙 = 决定「签名发生在哪、信任押在哪」;从热钱包到多签,是信任从单台设备逐步拆散的过程。
插问 3:助记词泄露 vs 助记词丢失,各是什么下场?
🧑🎓 学生: 纸面推演一下这两个场景——好让我真正记住风险管理该做什么。
🧑🏫 老师:
推演两边,动作清单不同:
场景 A:助记词泄露(拍照上网盘、输入钓鱼网站、给「客服」看过):
时间线:泄露 → 攻击者导入自己的钱包 → 扫描树上所有有余额的地址 → 签名转走
你的窗口:从泄露到被扫到之间的几分钟~几小时
正确动作:立刻建新钱包(新熵!)→ 资产自查自转 → 旧树宣告死刑(所有链!)
常见错误:只转以太坊不转比特币(插问 2:一棵树多链全死);
或删掉 MetaMask 觉得就安全了(攻击者根本不用你的设备)场景 B:助记词丢失(纸条烧了、口令忘了、备份盘坏了):
时间线:丢失 → 你再也派生不出那棵树 → 地址里的余额成为永久观光数字
你的窗口:无。BIP-39/32 是确定性的:没有那 12 个词,2^128 的树海里无人能替你找回
正确动作:(事前)多地备份:两三份异地、防火防水、互不告知的存放点
常见错误:备份在「同一场火灾里」;或给备份拍照——数字副本的泄露面回到场景 A两个场景对照出备份的本质:防丢失要副本,防泄露要隔离——这两者天然打架,工程答案永远是折中(比如 2-of-3 的多地分片、或 Steel 板材做物理 durable)。没有完美方案,只有和你资产量级匹配的方案。
一句话收口:泄露 = 与攻击者赛跑全链搬家;丢失 = 永久观光没有救援;备份的艺术是在「副本」和「隔离」之间选折中。
小结
钱包的完整图景:
- 钱包 ≠ 装钱的容器:链上只有「地址 → 余额」的账本行;钱包 = 密钥管理器 + 签名工具。
- BIP-39:128 位熵 + 4 位校验,11 位一组查 2048 词表 → 12 个词;换词校验当场翻脸。
- 熵的量级:128 位工程上等同不可穷举;真实风险是被偷不是被猜。
- 助记词 → 种子:PBKDF2 2048 轮;passphrase(第 25 词)开出平行宇宙。
- BIP-32:种子长成无限、确定、层间隔离的密钥树。
- BIP-44:
m/44'/60'/0'/0/i是全行业门牌号;新地址 = 序号 +1(实测 5 地址互不相同、重派一致)。 - 跨链通用:各链钱包按同一图纸摘钥匙,coin type 分岔——一份助记词一篮子全部鸡蛋。
- 存法谱系:热/冷/硬件/多签/keystore,签名发生在哪、信任押在哪。
- 泄露与丢失:一个赛跑搬家、一个永无救援;备份在副本与隔离间折中。
验收清单(做完再进下一篇):
思考题:硬件钱包厂商跑路/固件后门,你的资产还安全吗?多签为什么是「更彻底」的答案?(提示:签名需要几把钥匙、每把押在哪。)
下一篇:《手写 100 行迷你区块链》——零件(哈希、签名)全部到手,组装一台能挖矿、能防篡改的机器。
本篇实验脚本(可照抄)
# wallet.py —— 助记词生成、校验、派生
from mnemonic import Mnemonic
from bip_utils import Bip39SeedGenerator, Bip44, Bip44Coins, Bip44Changes
m = Mnemonic("english")
words = m.generate(strength=128) # ① 12 词
print("助记词:", words, "| 校验:", m.check(words))
seed = Bip39SeedGenerator(words).Generate() # ③ 种子(可传 passphrase)
for i in range(5): # ⑤ BIP-44 派生
addr = (Bip44.FromSeed(seed, Bip44Coins.ETHEREUM)
.Purpose().Coin().Account(0).Change(Bip44Changes.CHAIN_EXT)
.AddressIndex(i).PublicKey().ToAddress())
print(f"m/44'/60'/0'/0/{i} → {addr}")
btc = (Bip44.FromSeed(seed, Bip44Coins.BITCOIN) # ⑦ 同树跨链
.Purpose().Coin().Account(0).Change(Bip44Changes.CHAIN_EXT)
.AddressIndex(0).PublicKey().ToAddress())
print("m/44'/0'/0'/0/0 (BTC) →", btc)参考资料
- BIP-39(助记词)、BIP-32(分层确定性钱包)、BIP-44(多币种派生路径)
- Web3 Secret Storage(keystore 格式)
- SLIP-44 注册表(coin type 编号)
- 本机:WSL2 Ubuntu-22.04 + Python 3.10(mnemonic + bip_utils)