种子是怎么一步步生成地址的?为何种子能管理那么多地址?为何能在不生成私钥的情况下直接派生出很多公钥?本文为您揭晓。
分层确定性钱包,可以从一个种子派生出一系列密钥对用于生成地址,便于钱包的备份与管理
助记词、种子、公钥、地址之间的关系:
助记词与种子公钥与地址之间只能单向推导
涉及到的 BIP 协议:
本篇文章,我将对上述协议分别展开讨论与分析。
此处查看 BIP39 文档
文档概要:
助记词的生成:
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 |
助记词到种子的推导:
通过 PBKDF2 函数生成大小为 64 byte 的种子。
PBKDF2(Password-Based Key Derivation Function 2)是一个基于口令的密钥推导方法,用于增强弱秘钥的安全性。本质上就是基于 hash 函数通过加盐和迭代因子让处理速度变慢,减少爆破风险。具体可参考 wiki
该函数定义如下:
DK = PBKDF2(PRF, Password, Salt, c, dkLen)
其中 :
在 bip39 中,用于产生种子的上述参数分别为:
由函数 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
概要:
基本概念:
需要注意:
扩展的具体过程:
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
parentPrivKey x G = parentPubKey
进而得出:
il x G + parentPubkey = childPrivKey x G = childPubKey
所以 childPrivKey 就是 childPubkey 对应的私钥.
由以上过程分析,我们不难发现,ckd 方法的核心思想,就是父私钥加上一个随机数字得到子私钥,而这个随机数字的产生是需要一规则的,这样才能做到子地址可管理。
代码参考: https://github.com/tpkeeper/addrtool/
func TestSeedToPubkey(t *testing.T) {
seed := "04ef53d66b17fdfb6538c5d183f0b0569fc1c79d07f044f7670c3038aff411e5abcbe8c457b584d0c1e3504ab94fb311f9097a793c20dfc746a87087ed5dc119"
hexByte, _ := hex.DecodeString(seed)
//m
masterExtKey, _ := bip32.NewMasterKey(hexByte)
//m/purpose'
purposeExtKey,_:=masterExtKey.NewChildKey(bip32.FirstHardenedChild+44)
//m/purpose'/cointype'
coinTypeExtKey,_:=purposeExtKey.NewChildKey(bip32.FirstHardenedChild+0)
//m/purpose'/cointype'/account'
accountExtKey,_:=coinTypeExtKey.NewChildKey(bip32.FirstHardenedChild+0)
//m/purpose'/cointype'/account'/change
changeExtKey,_:=accountExtKey.NewChildKey(0)
//m/purpose'/cointype'/account'/change/addrIndex
addrIndex0ExtKey,_:=changeExtKey.NewChildKey(0)
//pubkey
t.Log(hex.EncodeToString(addrIndex0ExtKey.PublicKey().Key))
}
查看文档:BIP44
概要:
bip44 协议的 5 层路径规则:
路径:m/purpse’/coin_type’/account’/change/address_index(符号 ‘ 表示强化子秘钥,需要 index >= 2^{31}
每一层对应的关系如下:
公钥的推导:
通过场景 1 和 2 扩展的子扩展密钥 (k,c):
pubKey = k x G
通过场景 3 扩展的子扩展密钥 (K,c):
pubKey = K
简单的理解,地址就是 公钥或者脚本 的哈希值的 base58 格式。
常用的地址的格式:
P2PKH (Pay To PubKey Hash) 格式的地址
P2SH (Pay To Script Hash) 格式的地址
前缀占用一个字节,表示地址类型。
hash160(pubkey) 占用 20 字节。
校验位占用 4 个字节,是对 前缀 + hash160(pubkey) 进行两次 sha256 取前四个字节。
使用 base58 便于更友好的显示,增加的校验还可以防止用户输入错误,bip32 中也是这种格式来显示扩展密钥。
代码参考:https://github.com/tpkeeper/addrtool
func PubkeyToAddress(key []byte,netId byte)(string){
hash160Bytes:=btcutil.Hash160(key)
return base58.CheckEncode(hash160Bytes,netId)
}
base58前缀目录一览:
其中 xpub xprv 就是 BIP32 中的扩展公/私密钥的 base58 导出格式
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!