{"content":{"title":"HDWallet 原理分析","body":"## 概述\r\n\r\n分层确定性钱包，可以从一个种子派生出一系列密钥对用于生成地址，便于钱包的备份与管理\r\n\r\n**助记词、种子、公钥、地址之间的关系：**\r\n\r\n*助记词与种子公钥与地址之间只能单向推导*\r\n\r\n![hdwallet.png](https://img.learnblockchain.cn/attachments/2020/03/MhoL36eA5e724523c4c8a.png)\r\n\r\n**涉及到的 BIP 协议：**\r\n\r\n1. [BIP39](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki) 定义助记词的生成规则和助记词到种子的推导规则\r\n\r\n2. [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) hd 钱包核心提案，定义分层概念和算法 \r\n\r\n3. [BIP44](https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki) 定义 5 层路径规则\r\n\r\n4. [BIP45](https://github.com/bitcoin/bips/blob/master/bip-0045.mediawiki) 定义多签地址生成规则\r\n\r\n本篇文章，我将对上述协议分别展开讨论与分析。\r\n\r\n## BIP39\r\n\r\n此处查看 [BIP39 文档](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki)\r\n\r\n文档概要：\r\n* 定义了助记词的生成规则\r\n* 定义了助记词到种子的转换规则\r\n* 定义了助记词 wordlist，目前包含7种语言，每种 2048个单词\r\n* 助记词到种子的推导是单向的\r\n\r\n**助记词的生成：**\r\n\r\n1. 产生一个随机数作为熵 entropy，长度为 128-256 bits，必须为 32 bits 的整数倍。\r\n2. 然后在 entropy 尾部追加校验 checksum，checksum 是取 entropy 的 sha256 哈希值的前 n 位，位数跟 entropy 的长度有关，具体如下：\r\n\r\n|  entropy  | checksum | entropy+checksum |  mnemonic  |\r\n|--- | --- | --- | --- |\r\n|  128  |  4 |   132  |  12  |\r\n|  160  |  5 |   165  |  15  |\r\n|  192  |  6 |   198  |  18  |\r\n|  224  |  7 |   231  |  21  |\r\n|  256  |  8 |   264  |  24  |\r\n\r\n3. 然后 将 entropy+checksum 进行分组，每组 11 bits，每组的取值范围是  0 ~ 2047，刚好映射 wordlist 里的单词。\r\n4. 将映射的单词以空格隔开拼接为字符串，即为助记词。\r\n\r\n**助记词到种子的推导：**\r\n\r\n通过 PBKDF2 函数生成大小为 64 byte 的种子。\r\n\r\nPBKDF2（Password-Based Key Derivation Function 2）是一个基于口令的密钥推导方法，用于增强弱秘钥的安全性。本质上就是基于 hash 函数通过加盐和迭代因子让处理速度变慢，减少爆破风险。具体可参考 [wiki](https://en.wikipedia.org/wiki/PBKDF2)\r\n\r\n该函数定义如下：\r\n\r\nDK = PBKDF2(PRF, Password, Salt, c, dkLen)\r\n\r\n其中 ：\r\n\r\n* PRF 为伪随机函数相当于一个 hash 函数\r\n* Password 是口令，由用户负责安全\r\n* Salt 是盐，用于增加破解难度\r\n* c 是迭代次数，越大越安全\r\n* dkLen 是产生的密钥长度\r\n\r\n在 bip39 中，用于产生种子的上述参数分别为：\r\n\r\n* HMAC-SHA512 单向的 hash 算法\r\n* 助记词字符串\r\n* “mnemonic\"+passphrase（口令是可选的）\r\n* 2048\r\n* 512（bits）\r\n\r\n由函数 PBKDF2 可知，**助记词到种子的推导是单向的不可逆的。**\r\n\r\n\r\n**代码参考：**[https://github.com/tpkeeper/addrtool](\r\nhttps://github.com/tpkeeper/addrtool)\r\n\r\n```go\r\nfunc TestGenMnemonic(t *testing.T) {\r\n\t//生成熵\r\n\tentropyBytes,_:=bip39.NewEntropy(128)\r\n\tt.Log(\"entropyBytes：\",entropyBytes)\r\n\r\n\t//生成助记词\r\n\tmnemonic,_:=bip39.NewMnemonic(entropyBytes)\r\n\tt.Log(\"mnemonic：\",mnemonic)\r\n}\r\nfunc TestMnemonicToSeed(t *testing.T) {\r\n\tmnemonic :=\"chef fiction deputy stage pudding pink skirt often decade drift music loop\"\r\n\t//助记词生成种子 password 为空\r\n\tseed:=bip39.NewSeed(mnemonic,\"\")\r\n\tt.Log(\"seed：\",hex.EncodeToString(seed))\r\n}\r\n\r\n//output:\r\n//entropyBytes： [158 45 139 248 16 245 71 178 223 231 241 118 0 211 244 134]\r\n//mnemonic： owner hobby wrap capable federal sunny legend wreck invite alley wood aspect\r\n//seed： 04ef53d66b17fdfb6538c5d183f0b0569fc1c79d07f044f7670c3038aff411e5abcbe8c457b584d0c1e3504ab94fb311f9097a793c20dfc746a87087ed5dc119\r\n```\r\n\r\n## BIP32\r\n\r\n查看文档 [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki)\r\n\r\n概要：\r\n* 定义了由种子推导树状扩展密钥对的算法与规则\r\n\r\n基本概念：\r\n\r\n* 扩展秘钥有两种，扩展私钥和扩展公钥，扩展私钥可以扩展子私钥，扩展公钥可以扩展子公钥\r\n* 扩展私钥定义为：(k , c)，其中 k 为私钥，c 为 链码 chaincode\r\n* 扩展公钥定义为：(K , c)，其中 K 为公钥，c 为 链码 chaincode\r\n* 子秘钥扩展方法定义为：CKD(extended key , index），其中参数为扩展秘钥和索引。\r\n\r\n需要注意：\r\n\r\n* 扩展为非强化子秘钥时 index 范围为: 0~2^{31}-1，扩展为强化子秘钥时 index 范围为: 2^{31} ~ (2^{32}-1)\r\n* 只有扩展私钥才能扩展强化子扩展秘钥\r\n\r\n\r\n扩展的具体过程：\r\n\r\n1. 首先计算主扩展秘钥，即树根对应的扩展秘钥。\r\n  计算 HMAC-SHA512(\"Bitcoin seed\" , seed) 得到 512 bits，其中参数 seed 是在 BIP32 中生成的种子。然后将结果分为 L 和 R，各占 32 字节，分别作为主扩展秘钥的私钥和链码，得到主扩展秘钥。\r\n2. 然后通过 CKD(extended key , index) 方法向下层层扩展子密钥。\r\n\r\nCKD()方法扩展子秘钥有如下场景：\r\n\r\n1. 父扩展私钥 -> 强化子扩展私钥\r\n2. 父扩展私钥 -> 非强化子扩展私钥\r\n3. 父扩展公钥 -> 非强化子扩展公钥\r\n4. 父扩展公钥 -> 强化子扩展公钥(不允许) \r\n\r\n![ckd.png](https://img.learnblockchain.cn/attachments/2020/03/Y1pfUww95e724522cab6a.png)\r\n\r\n有上图可知，场景 3，可以在不生成私钥的情况下，通过公钥扩展子公钥。这些公钥对应的私钥正好需要通过场景 2 来额外生成。具体的原理用到了椭圆曲线加密算法 ECC 的运算特性。途中的 `||` 是字节拼接操作，`+` 和 `x` 都是 ECC 里的运算。在 ECC 中有以下定义：\r\n\r\nkey x G = pubKey\r\n\r\n(key1 + key2) x G = pubKey1 + pubkey2\r\n\r\n现在我们来证明 childPrivKey 就是 childPubKey 的私钥：\r\n\r\n已知：\r\n\r\n上图中场景 2 和场景 3，推导出的 il 是同一个值\r\n\r\nil + parentPrivKey = childPrivKey\r\n\r\nil x G + parentPubKey = childPubKey\r\n\r\n我们可以得出：\r\n\r\nil x G + parentPrivKey x G = childPrivKey x G\r\n\r\nparentPrivKey x G = parentPubKey\r\n\r\n进而得出：\r\n\r\nil x G + parentPubkey = childPrivKey x G = childPubKey\r\n\r\n所以 childPrivKey 就是 childPubkey 对应的私钥.\r\n\r\n由以上过程分析，我们不难发现，**ckd 方法的核心思想，就是父私钥加上一个随机数字得到子私钥，而这个随机数字的产生是需要一规则的，这样才能做到子地址可管理。**\r\n\r\n**代码参考:** [https://github.com/tpkeeper/addrtool/](https://github.com/tpkeeper/addrtool)\r\n\r\n```golang\r\nfunc TestSeedToPubkey(t *testing.T) {\r\n\tseed := \"04ef53d66b17fdfb6538c5d183f0b0569fc1c79d07f044f7670c3038aff411e5abcbe8c457b584d0c1e3504ab94fb311f9097a793c20dfc746a87087ed5dc119\"\r\n\thexByte, _ := hex.DecodeString(seed)\r\n\t//m\r\n\tmasterExtKey, _ := bip32.NewMasterKey(hexByte)\r\n\t//m/purpose'\r\n\tpurposeExtKey,_:=masterExtKey.NewChildKey(bip32.FirstHardenedChild+44)\r\n\t//m/purpose'/cointype'\r\n\tcoinTypeExtKey,_:=purposeExtKey.NewChildKey(bip32.FirstHardenedChild+0)\r\n\t//m/purpose'/cointype'/account'\r\n\taccountExtKey,_:=coinTypeExtKey.NewChildKey(bip32.FirstHardenedChild+0)\r\n\t//m/purpose'/cointype'/account'/change\r\n\tchangeExtKey,_:=accountExtKey.NewChildKey(0)\r\n\t//m/purpose'/cointype'/account'/change/addrIndex\r\n\taddrIndex0ExtKey,_:=changeExtKey.NewChildKey(0)\r\n\t//pubkey\r\n\tt.Log(hex.EncodeToString(addrIndex0ExtKey.PublicKey().Key))\r\n}\r\n```\r\n\r\n\r\n## BIP44\r\n\r\n查看文档：[BIP44](https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki)\r\n\r\n概要：\r\n* 定义了5层路径规则，可兼容多账号多币种\r\n\r\n\r\n**bip44 协议的 5 层路径规则：**\r\n\r\n*路径：m/purpse’/coin_type’/account’/change/address_index（符号 ‘ 表示强化子秘钥，需要 index >= 2^{31}*\r\n\r\n* m：主扩展密钥\r\n* purpose： bip44/bip45 \r\n* coin_type： 币种 \r\n* account： 钱包账户 \r\n* change： 0 对外 / 1 找零 \r\n* address_index： 地址索引\r\n\r\n每一层对应的关系如下：\r\n![bip44.png](https://img.learnblockchain.cn/attachments/2020/03/9Pr9u9m95e7245226be04.png)\r\n\r\n**公钥的推导：**\r\n\r\n通过场景 1 和 2 扩展的子扩展密钥 (k,c)：\r\n\r\npubKey = k x G\r\n\r\n通过场景 3 扩展的子扩展密钥 (K,c):\r\n\r\npubKey = K\r\n\r\n## 地址的最终生成\r\n\r\n简单的理解，地址就是 公钥或者脚本 的哈希值的 base58 格式。\r\n\r\n**常用的地址的格式：**\r\n\r\n**P2PKH** (Pay To PubKey Hash) 格式的地址\r\n\r\n![p2pkh_addr.png](https://img.learnblockchain.cn/attachments/2020/03/ppTfsi2J5e7245240c5d7.png)\r\n\r\n**P2SH** (Pay To Script Hash) 格式的地址\r\n\r\n![p2sh_addr.png](https://img.learnblockchain.cn/attachments/2020/03/v5bwCKi45e724524772db.png)\r\n\r\n\r\n前缀占用一个字节，表示地址类型。\r\n\r\nhash160(pubkey) 占用 20 字节。\r\n\r\n校验位占用 4 个字节，是对 **前缀 + hash160(pubkey)** 进行两次 sha256 取前四个字节。\r\n\r\n使用 base58 便于更友好的显示，增加的校验还可以防止用户输入错误，bip32 中也是这种格式来显示扩展密钥。\r\n\r\n**代码参考：**[https://github.com/tpkeeper/addrtool](https://github.com/tpkeeper/addrtool)\r\n\r\n```golang\r\nfunc PubkeyToAddress(key []byte,netId byte)(string){\r\n\thash160Bytes:=btcutil.Hash160(key)\r\n\treturn base58.CheckEncode(hash160Bytes,netId)\r\n}\r\n```\r\n\r\nbase58前缀目录一览：\r\n\r\n*其中 xpub xprv 就是 BIP32 中的扩展公/私密钥的 base58 导出格式*\r\n\r\n![base58pre.png](https://img.learnblockchain.cn/attachments/2020/03/clU2yw7z5e724521ca8aa.png)"},"author":{"user":"https://learnblockchain.cn/people/474","address":null},"history":null,"timestamp":1685877472,"version":1}