HDWallet 原理分析
种子是怎么一步步生成地址的?为何种子能管理那么多地址?为何能在不生成私钥的情况下直接派生出很多公钥?本文为您揭晓。
概述
分层确定性钱包,可以从一个种子派生出一系列密钥对用于生成地址,便于钱包的备份与管理
助记词、种子、公钥、地址之间的关系:
助记词与种子公钥与地址之间只能单向推导
涉及到的 BIP 协议:
本篇文章,我将对上述协议分别展开讨论与分析。
BIP39
此处查看 BIP39 文档
文档概要:
- 定义了助记词的生成规则
- 定义了助记词到种子的转换规则
- 定义了助记词 wordlist,目前包含7种语言,每种 2048个单词
- 助记词到种子的推导是单向的
助记词的生成:
- 产生一个随机数作为熵 entropy,长度为 128-256 bits,必须为 32 bits 的整数倍。
- 然后在 entropy 尾部追加校验 checksum,checksum 是取 entropy 的 sha256 哈希值的前 n 位,位数跟 entropy 的长度有关,具体如下:
entropy | checksum | entropy+checksum | mnemonic |
---|---|---|---|
128 | 4 | 132 | 12 |
160 | 5 | 165 | 15 |
192 | 6 | 198 | 18 |
224 | 7 | 231 | 21 |
256 | 8 | 264 | 24 |
- 然后 将 entropy+checksum 进行分组,每组 11 bits,每组的取值范围是 0 ~ 2047,刚好映射 wordlist 里的单词。
- 将映射的单词以空格隔开拼接为字符串,即为助记词。
助记词到种子的推导:
通过 PBKDF2 函数生成大小为 64 byte 的种子。
PBKDF2(Password-Based Key Derivation Function 2)是一个基于口令的密钥推导方法,用于增强弱秘钥的安全性。本质上就是基于 hash 函数通过加盐和迭代因子让处理速度变慢,减少爆破风险。具体可参考 wiki
该函数定义如下:
DK = PBKDF2(PRF, Password, Salt, c, dkLen)
其中 :
- PRF 为伪随机函数相当于一个 hash 函数
- Password 是口令,由用户负责安全
- Salt 是盐,用于增加破解难度
- c 是迭代次数,越大越安全
- dkLen 是产生的密钥长度
在 bip39 中,用于产生种子的上述参数分别为:
- HMAC-SHA512 单向的 hash 算法
- 助记词字符串
- “mnemonic"+passphrase(口令是可选的)
- 2048
- 512(bits)
由函数 PBKDF2 可知,助记词到种子的推导是单向的不可逆的。
代码参考:https://github.com/tpkeeper/addrtool
func TestGenMnemonic(t *testing.T) {
//生成熵
entropyBytes, _ := bip39.NewEntropy(128)
t.Log("entropyBytes:", entropyBytes)
//生成助记词
mnemonic, _ := bip39.NewMnemonic(entropyBytes)
t.Log("mnemonic:", mnemonic)
}
func TestMnemonicToSeed(t *testing.T) {
mnemonic := "chef fiction deputy stage pudding pink skirt often decade drift music loop"
//助记词生成种子 password 为空
seed := bip39.NewSeed(mnemonic, "")
t.Log("seed:", hex.EncodeToString(seed))
}
//output:
//entropyBytes: [158 45 139 248 16 245 71 178 223 231 241 118 0 211 244 134]
//mnemonic: owner hobby wrap capable federal sunny legend wreck invite alley wood aspect
//seed: 04ef53d66b17fdfb6538c5d183f0b0569fc1c79d07f044f7670c3038aff411e5abcbe8c457b584d0c1e3504ab94fb311f9097a793c20dfc746a87087ed5dc119
BIP32
查看文档 BIP32
概要:
- 定义了由种子推导树状扩展密钥对的算法与规则
基本概念:
- 扩展秘钥有两种,扩展私钥和扩展公钥,扩展私钥可以扩展子私钥,扩展公钥可以扩展子公钥
- 扩展私钥定义为:(k , c),其中 k 为私钥,c 为 链码 chaincode
- 扩展公钥定义为:(K , c),其中 K 为公钥,c 为 链码 chaincode
- 子秘钥扩展方法定义为:CKD(extended key , index),其中参数为扩展秘钥和索引。
需要注意:
- 扩展为非强化子秘钥时 index 范围为: 0~2^{31}-1,扩展为强化子秘钥时 index 范围为: 2^{31} ~ (2^{32}-1)
- 只有扩展私钥才能扩展强化子扩展秘钥
扩展的具体过程:
- 首先计算主扩展秘钥,即树根对应的扩展秘钥。
计算 HMAC-SHA512("Bitcoin seed" , seed) 得到 512 bits,其中参数 seed 是在 BIP32 中生成的种子。然后将结果分为 L 和 R,各占 32 字节,分别作为主扩展秘钥的私钥和链码,得到主扩展秘钥。 - 然后通过 CKD(extended key , index) 方法向下层层扩展子密钥。
CKD()方法扩展子秘钥有如下场景:
- 父扩展私钥 -> 强化子扩展私钥
- 父扩展私钥 -> 非强化子扩展私钥
- 父扩展公钥 -> 非强化子扩展公钥
- 父扩展公钥 -> 强化子扩展公钥(不允许)
有上图可知,场景 3,可以在不生成私钥的情况下,通过公钥扩展子公钥。这些公钥对应的私钥正好需要通过场景 2 来额外生成。具体的原理用到了椭圆曲线加密算法 ECC 的运算特性。途中的 ||
是字节拼接操作,+
和 x
都是 ECC 里的运算。在 ECC 中有以下定义:
key x G = pubKey
(key1 + key2) x G = pubKey1 + pubkey2
现在我们来证明 childPrivKey 就是 childPubKey 的私钥:
已知:
上图中场景 2 和场景 3,推导出的 il 是同一个值
il + parentPrivKey = childPrivKey
il x G + parentPubKey = childPubKey
我们可以得出:
il x G + parentPrivKey x G = childPrivKey x G
parentPrivKe