HD钱包

HD Wallet(Hierarchical Deterministic Wallet,分层确定性钱包)是基于[BIP32](bips/bip-0032.mediawiki at master · bitcoin/bips (github.com))实现的用于管理私钥的工具。

非确定性钱包和确定性钱包

非确定性钱包可以参考比特币客户端使用随机生成密钥的方式。

比特币客户端会预先生成100个随机私钥缓存在一个密钥池,从最开始就生成足够多的私钥并且每个密钥只使用一次。这种钱包难以管理、 备份以及导入密钥。非确定性钱包的每一个密钥都需要进行备份,如果钱包不可访问时,没有备份的密钥就会失去其资金的控制权

HD-wallet

确定性钱包中通过一个主密钥派生出子密钥,这个主密钥称为种子(seed)。

HD钱包是确定性钱包的一种衍生,HD钱包遵循BIP32标准,它通过seed导出树状结构的密钥,使得父密钥可以衍生一系列子密钥,每个子密钥又可以衍生出一系列孙密钥,以此类推,无限衍生。

wallet

与HD钱包相关的BIP

BIP32、BIP39和BIP44是Bitcoin Improvement Proposals(比特币改进提案)中定义的三种标准,用于增强比特币和其他加密货币钱包的功能和安全性。

BIP32描述了比特币分层确定性钱包

BIP39描述了助记词的实现

BIP43和BIP44,这两个协议规定了钱包的树结构,即HD 钱包标准路径(主要分析BIP44)

BIP32-HD原理

BIP32由以下几部分来进行深入:

  • 密钥序列化格式:密钥的数据结构
  • 密钥派生:规定子密钥是如何从父密钥中派生
  • 密钥树:父密钥派生子密钥所形成的树状结构

下面通过理论与代码(golang)相结合的方式去理解BIP32。

BIP32实现代码:go-bip32/bip32.go at master · tyler-smith/go-bip32 (github.com)

序列化格式:密钥结构

BIP32规定密钥的序列化格式包括6个类型:

  • 版本(version):4 byte,对密钥版本的预定。主网的公钥版本-0x0488B21E,私钥版本-0x0488ADE4(测试网 公钥版本-0x043587CF,私钥版本-0x04358394)
  • 深度(depth):1 byte,密钥当前的层级。如:主密钥层级为0(0x00),派生的一级子密钥层级为1 (0x01)
  • 父密钥指纹(FingerPrint):4 byte,父密钥hash值的前四位。主密钥的父指纹为0x00000000
  • 子编号(child number):4 byte,密钥的索引
  • 链码(chain code):32 byte,通过HMAC-SHA512计算的得到右32字节
  • 公钥或私钥 (Key): 33 byte ,通过HMAC-SHA512计算的得到左32字节
1
2
3
4
5
6
7
8
9
type Key struct {
Key []byte // 33 bytes
Version []byte // 4 bytes
ChildNumber []byte // 4 bytes
FingerPrint []byte // 4 bytes
ChainCode []byte // 32 bytes
Depth byte // 1 bytes
IsPrivate bool // unserialized
}

密钥派生:生成子密钥过程

密钥派生有很多种方式推导密钥,比如:父私钥→子私钥、父公钥→子公钥、父私钥→子公钥以及一种不可行的方式,父公钥→子私钥。下面从父私钥推导子公钥的过程进行描述

扩展密钥

用于计算子密钥的一部分

将256位的私钥或公钥扩展为512位的位串,将左256表示为k, 右256位表示为c,得到扩展密钥 (k,c)。其中,k是密钥序列化中的key,c是链码

强化密钥

得到密钥的种类,有根据给的索引大小区分普通密钥和强化密钥

为什么需要强化密钥(强化派生):当黑客拿到你的未硬化的扩展子私钥和扩展父公钥(链码)可以反向推导出父私钥或者所有的姊妹钱包,从而盗取你账户的所有资产

细节深入:HD Wallets: Why Hardened Derivation Matters? | by Blaine Malone | Medium

每个扩展密钥有 2 31个普通子密钥, 2 31个强化子密钥(hardened child keys)。 这些子密钥都有一个索引。 普通子密钥使用索引0到 2 31-1。 强化子密钥使用索引 2 31 到 2 31-1。 为了简化强化子密钥索引的符号,数字i H表示i + 2 31

父私钥 → 子私钥

  • 子密钥派生函数(Child key derivation):CKDprev( (k,c) , i ),其中i是钱包索引,这个索引与构建密钥树有关
    • 输入扩展密钥与索引 i
    • 将CKD中参数传入到HMAC-SHA512计算
    • 判断(k,c)是否是强化密钥,比较i的值:i > 2 31
      • 如果i > = 2 31, data = 0x00 || k || i (注意:0x00将私钥补齐到33字节长)
      • i < 2 31,data = k || i
  • 得到 l = CKDprev( (k,c) , i ),l 是一个长度为 512 的位串。将l 分为两部分lL , lR
  • 得到子密钥:Ki = (lL + k) mod n
    • 条件: lL < n ,Ki != 0 , len(Ki ) != 32 (否则得到密钥是无效的)
    • 这里的n是指secp256k1标准定义的参数(Integers modulo the order of the curve, 简称:n)
  • 得到子密钥链码:Ci = lR

代码分析

创建主密钥

主密钥通过种子得到,使用hmac函数计算出位串(Key =“比特币种子”,Data = seed),取lL 作为子密钥,lR作为链码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
func NewMasterKey(seed []byte) (*Key, error) {
// Generate key and chaincode
hmac := hmac.New(sha512.New, []byte("Bitcoin seed"))
_, err := hmac.Write(seed)

intermediary := hmac.Sum(nil)

// Split it into our key and chain code
keyBytes := intermediary[:32]
chainCode := intermediary[32:]

// Create the key struct
key := &Key{
Version: PrivateWalletVersion,
ChainCode: chainCode,
Key: keyBytes,
Depth: 0x0,
ChildNumber: []byte{0x00, 0x00, 0x00, 0x00},
FingerPrint: []byte{0x00, 0x00, 0x00, 0x00},
IsPrivate: true,
}

return key, nil
}

父私钥推导子密钥

用seed生成主密钥后,调用该方法去生成一个子密钥

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// NewChildKey derives a child key from a given parent as outlined by bip32
func (key *Key) NewChildKey(childIdx uint32) (*Key, error) {
// Fail early if trying to create hardned child from public key
if !key.IsPrivate && childIdx >= FirstHardenedChild {
return nil, ErrHardnedChildPublicKey
}

intermediary, err := key.getIntermediary(childIdx)
if err != nil {
return nil, err
}

// Create child Key with data common to all both scenarios
childKey := &Key{
ChildNumber: uint32Bytes(childIdx),
ChainCode: intermediary[32:],
Depth: key.Depth + 1,
IsPrivate: key.IsPrivate,
}

// Bip32 CKDpriv
if key.IsPrivate {
childKey.Version = PrivateWalletVersion
fingerprint, err := hash160(publicKeyForPrivateKey(key.Key))
if err != nil {
return nil, err
}
childKey.FingerPrint = fingerprint[:4]
childKey.Key = addPrivateKeys(intermediary[:32], key.Key)

// Validate key
err = validatePrivateKey(childKey.Key)
if err != nil {
return nil, err
}
// Bip32 CKDpub
} else {
// ...
}
return childKey, nil
}

通过扩展密钥计算lL , lR的位串

使用HMAC-SHA512算法计算,先要对索引进行比较,密钥是否是强化密钥

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func (key *Key) getIntermediary(childIdx uint32) ([]byte, error) {
// Get intermediary to create key and chaincode from
// Hardened children are based on the private key
// NonHardened children are based on the public key
childIndexBytes := uint32Bytes(childIdx)

var data []byte
// FirstHardenedChild = uint32(0x80000000)
if childIdx >= FirstHardenedChild {
data = append([]byte{0x0}, key.Key...)
} else {
if key.IsPrivate {
data = publicKeyForPrivateKey(key.Key)
} else {
data = key.Key
}
}
data = append(data, childIndexBytes...)

hmac := hmac.New(sha512.New, key.ChainCode)
_, err := hmac.Write(data)
if err != nil {
return nil, err
}
return hmac.Sum(nil), nil
}

计算Ki

使用该公式计算 Ki = (lL + k) mod n

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func addPrivateKeys(key1 []byte, key2 []byte) []byte {
var key1Int big.Int
var key2Int big.Int
key1Int.SetBytes(key1)
key2Int.SetBytes(key2)

key1Int.Add(&key1Int, &key2Int)
key1Int.Mod(&key1Int, curve.Params().N)

b := key1Int.Bytes()
if len(b) < 32 {
extra := make([]byte, 32-len(b))
b = append(extra, b...)
}
return b
}

钱包结构:HD钱包的路径

前面我们通过CKD函数计算出密钥,计算密钥需要两个参数一个是扩展密钥,一个是索引。通过这个索引我们能够沿着一条路径构建出子密钥,比如:构造一个路径为 “m/0/0/1” 的密钥来控制钱包

  • 构造过程,从seed得到主密钥m,通过m去构造路径:
  • CKD( CKD( CKD( m, 0), 0 ), 1) = CKD(CKD(m,0), 0)/1 = CKD(m, 0)/0/1 = m/0/0/1

derivation

Source: https://github.com/bitcoin/bips/blob/master/bip-0032/derivation.png

BIP39-助记词标准

BIP39描述了记助词的实现,主要分为两个部分:生成记助词和将记助词转化为二进制种子

为什么需要记助词:BIP32通过输入一个seed就能输出主密钥和一堆子密钥,我们只需记住seed就能控制钱包,但是对于人类来说记忆一串毫无关联的数字是很困难的。所以BIP39提出通过一组容易记住的单词(或者说一个句子)来用于生成seed。

生成记助词

记助词生成过程:

  1. 生成一个初始熵(entropy),熵的长度在128~256位且必须为32位的倍数。其中ENT(entropy length)= 熵的长度
  2. 将初始熵进行SHA256计算,取前 ENT/32 位作为校验和(checksum)。其中 CS(checksum length)= ENT/32
  3. 将熵和校验和拼接(校验和附加在熵后),这个串会被分为11位一组,每一组会对应单词表的索引(0-2047),一共有 MS (mnemonic sentence) = ( ENT + CS ) / 11 组单词

单词表实现:python-mnemonic/src/mnemonic/wordlist/english.txt at master · trezor/python-mnemonic (github.com)

ENT CS ENT + CS MS
128 4 132 12
160 5 165 15
192 6 198 18
224 7 231 21
256 8 264 24

bip39-part1

Source: https://github.com/ethereumbook/ethereumbook

记助词生成种子

记助词生成seed时需要两个参数:记助词(mnemonic)和盐(salt,也能叫密码,passphrase)。salt能够保护钱包,比如:黑客必须同时获得你的记助词和密码才能拿到生成主密钥的seed

生成seed的过程

  1. 向PBKDF2函数输入参数:记助词、盐
  2. PBKDF2函数计算得到一个512位的seed

bip39-part2

Source: https://github.com/ethereumbook/ethereumbook

BIP44-HD钱包路径标准

BIP32中描述了HD钱包的结构,其每一层大约有40亿的子密钥和40亿的强化密钥而每一层又能继续衍生下去,这导致钱包里账户的路径近乎是无限的。如果没有一个明确的标准去约束密钥派生的路径,那么更换钱包时就可能出现兼容性问题

强化派生路径表示例子: m/1’/0’ , 其路径上有 ““作为强化派生的标记

路径级别

BIP44规定的路径有五个级别

m/purpose’/coin_type’/accout’/change/address_index

  • purpose:协议BIP44,一般设置为常量44’ (0x8000002C)
  • coin_type:币种类型,比特币为0’ (0x80000000)
  • accout:账户类型,为用户划分不同身份。从0’开始递增
  • change:钱包地址对外部是否可见,0用于外部链,1用于内部链
  • index:地址索引,地址从索引0开始按顺序递增编号

引用

  1. https://github.com/the-web3/blockchain-wallet/tree/master/basicWallet
  2. [Web3专题(三) 2种钱包之分层确定性钱包(HD Wallet,BIP32,BIP39,BIP44) | 登链社区 | 区块链技术社区 (learnblockchain.cn)](https://learnblockchain.cn/article/7098#实现一个以太坊钱包(符合 BIP-44 标准的路径))
  3. BIP: https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki
  4. https://github.com/ethereumbook/ethereumbook

吐槽:为什么国内都不喜欢把引用的图源和链接注释出来…