转载请注明出处:www.huamo.online
字节杭州 求贤若渴:
分层确定性钱包(HD Wallets
)
在使用钱包,以及看以太坊代码时,都会碰到m/44'/60'/0'/0
这样奇怪的字符串,以及派生路径(DerivationPath
)这样的术语,决定搞个明白,随后发现这些奇怪的东西都与一个名词有关:分层确定性钱包
分层确定性钱包
的概念最初来源于bitcoin
,出自BIP-0032
。其英文叫做Hierarchical Deterministic Wallets
,简称为HD Wallets
。一个分层确定性钱包可以部分或全部共享给不同的系统,每一部分都拥有或不拥有花费数字货币的能力。
动机
比特币早期的钱包是非确定性的,使用随机生成的私钥。中本聪提出了每个交易使用一个地址的概念,为了避免每次交易完后都需要备份钱包(为了备份私钥),早期的钱包会默认预先生成100
个私钥放在私钥池里以供使用,但这只是延缓了备份的间隔,需要备份的私钥数量并没有减少,依然不方便管理。另外,早期的钱包并没有打算在多个系统上同时共享使用。他们支持通过钱包的加密功能隐藏私钥而且不共享密码,但这样“阉割过”的钱包也失去了生成公钥的能力。
而确定性钱包不需要频繁的备份。椭圆曲线数学允许在不暴露私钥的情况下计算出公钥内容。这就可以支持一些场景,比如一个网上商店可以让它的网络服务器,在不接触相应私钥(花费收到的资金时需要用到私钥)的情况下,为每笔订单或每个客户生成新的地址(公钥哈希)。
一般来说,确定性钱包通常只包含一条由秘钥对构成的链。只有一条链就意味着共享一个钱包时,要么就全共享,要么就什么都不共享。然而,在有些场景下,人们可能只需要其中一些秘钥来共享和回复。在上一段提到的网店例子中,网络服务器并不需要访问卖家钱包中的所有公钥,并且它只需要访问那些接受顾客付款的地址,而不用去访问例如卖家要花钱时生成的地址。分层确定性钱包基于多个秘钥对链来支持选择性共享,这些秘钥对链由同一个根生成。
至此,可以看到,钱包分为非确定性和确定性。所谓非确定性即钱包随机生成多个私钥,而确定性钱包是由一个根(种子)生成多个秘钥对。而分层确定性钱包可以通过一个根生成多个秘钥对的链(即构成了树),来支持选择性共享。
从下文的学习中再来读这一段,发现确定性钱包的主要应用场景就是:在不需要暴露私钥的情况下,只靠父公钥就能生成大量地址,而这些地址都能靠主私钥获取绝对控制权。
规范:密钥生成
约定
在本文其余部分中,我们假定是基于比特币中使用的公钥密码学,即ECC中用到的由secp256k1
(http://www.secg.org/sec2-v2.pdf)定义的域和曲线参数。以下变量分别为:
整数取模曲线的阶(这个阶记为n)
曲线上点的坐标
字节序列。
用两个坐标对的加法($+$)来表示椭圆曲线群的操作。连接($||$)操作是将一个字节序列加到另一个的尾部。
作为标准转换函数,我们假设:
$point(p)$:返回对
secp256k1
基点和整数$p$的EC点乘运算(重复EC群操作实现)得到的坐标对。$ser_{32}(i)$:将一个32位无符号整型
i
序列化为一个4字节的序列,大端优先。$ser_{256}(p)$:将整数
p
序列化为一个32字节序列,大端优先。$$ser_P(P)$$:将点$$P = (x, y)$$序列化为一个字节序列,使用的方法为
SEC1
压缩格式:$$(0x02 \ or \ 0x03) || ser_{256}(x)$$,其中头部字节取决于省略的$y$坐标的奇偶性。$parse_{256}(p)$:将一个32字节序列转化为一个256位整数,大端优先。
扩展的密钥
接下来,我们将会定义一个函数,用来从一个父密钥生成一系列的子密钥。为了防止结果仅依赖于父密钥本身,我们首先会用一个额外的256位调节数(entropy
)同时扩展私钥和公钥。这个扩展用到的调节数,称为链码(chain code
),对于相应的私钥和公钥是相同的,并且由32个字节构成。
我们将扩展后的私钥记为(k, c)
,其中k
是原本普通的私钥,c
是链码。将扩展后的公钥记为(K, c)
,其中K = point(k)
,并且c
同样是链码。
每一个扩展后的密钥都有$2^{31}$个普通的子密钥,以及$2^{31}$个强化的子密钥。每一个子密钥都有一个索引序号。其中普通子密钥使用索引$0 \sim 2^{31}-1$,强化的子密钥使用索引$2^{31} \sim 2^{32} - 1$。为了方便强化子密钥的索引记法,数字$i_H$表示的就是$i + 2^{31}$。
子密钥生成函数(Child key derivation
[CKD
])
给定一个父扩展密钥和一个索引i
,可以计算出对应的子扩展密钥。实现这个的算法取决于子密钥是否是强化密钥(或者说,是否$i \geqslant 2^{31}$),以及我们讨论的是私钥还是公钥
父私钥 $\longrightarrow$ 子私钥
函数$$CKDpriv((k_{par}, c_{par}), i) \rightarrow (k_i, c_i)$$基于父扩展私钥计算出一个子扩展私钥,以下是算法流程:
检查是否$i \geqslant 2^{31}$(即子密钥是否是一个强化密钥)。
如果是强化子私钥:令
$$I = HMAC-SHA512(Key = c_{par}, \ Data = 0x00 || ser_{256}(k_{par}) || ser_{32}(i))$$
(注意:
0x00
填充了私钥将其扩展为33字节)。如果是普通子私钥:令
$$I = HMAC-SHA512(Key = c_{par}, \ Data = ser_{P}(point(k_{par})) || ser_{32}(i))$$
将$I$分割为2个32字节序列,分别记为$I_L$和$I_R$
返回的子私钥$$k_i$$为$$k_i = parse_{256}(I_L) + k_{par}(mod \ n)$$。
n为曲线中的一个常数,表示基点的阶
返回的子链码$c_i$为$c_i = I_R$。
在$parse_{256}(I_L) \geqslant n $或者$k_i = 0$的情况下,得到的密钥无效,此时应该为$i$处理下一个值。(这个概率低于$\frac{1}{2^{127}}$)
上面提到的
HMAC-SHA512
函数定义于RFC 4231
父公钥 $\longrightarrow$ 子公钥
函数$$CKD_{pub}((K_{par}, c_{par}), i) \rightarrow (K_{i}, c_{i})$$基于父扩展公钥计算出一个子扩展公钥。该函数只定义于非强化的子公钥。以下是算法流程:
检查是否$i \geqslant 2^{31}$(即子密钥是否是一个强化密钥)。
如果是强化子公钥:返回失败
如果是普通子公钥:令
$$I = HMAC-SHA512(Key = c_{par}, Data = ser_{P}(K_{par}) || ser_{32}(i))$$
将$I$分割为2个32字节序列,分别记为$I_L$和$I_R$。
返回的子公钥$$K_i$$为$$K_i = point(parse_{256}(I_L)) + K_{par}$$。
返回的链码为$c_i = I_R$
在$parse_{256}(I_L) \geqslant n$或者$K_i$是无穷远点的情况下,返回的密钥无效,此时应该为$i$处理下一个值。
父私钥 $\longrightarrow$ 子公钥
函数$N((k, c)) \rightarrow (K, c)$基于一个扩展私钥,计算出对应的扩展公钥(这个私钥是“阉割过”的版本,因为它无法对交易进行签名)。
返回的公钥$K$为$K = point(k)$
返回的链码$c$就是传入的链码$c$。
为了计算一个父私钥的子公钥,可以用以下2种方式:
$$N(CKD_{priv}((k_{par}, c_{par}), i))$$该方式总是可行的。
$$CKD_{pub}(N(k_{par}, c_{par}), i)$$,该方式只适用于生成非强化的子公钥。
两者等价意味着非强化的密钥很有实用意义:可以使用一个给定的父公钥生成一系列子公钥,而无需知道任何私钥。同时在强化密钥方面两者又是有区别的,通常不使用非强化密钥的原因是考虑到安全性,下面有更多这方面的信息。
父公钥 $\longrightarrow$ 子私钥
这是不可能实现的!
密钥树
下一步就是级联多个CKD
结构来构造出一颗树。首先以一个根开始,这个根就是主扩展私钥m
。通过对多个i
值计算$$CKD_{priv}(m, i)$$,可以得到一系列的1级派生节点。这些节点的每一个也是一个扩展私钥,$$CKD_{priv}$$函数也同样适用于它们。
为了记法简单,我们将$$CKD_{priv}(CKD_{priv}(CKD_{priv}(m, 3_H),2),5)$$记为$$m/3_H/2/5$$。这种方式同样适用于公钥衍生,例如$$CKD_{pub}(CKD_{pub}(CKD_{pub}(M,3),2),5)$$记为$$M/3/2/5$$。这将会具有以下特性:
$$N(m/a/b/c) = N(m/a/b)/c = N(m/a)/b/c = N(m)/a/b/c = M/a/b/c$$(从数学上证明了,用父私钥生成子公钥,可以用父公钥生成子公钥代替,这样就可以不暴露父私钥)
$$N(m/a_H/b/c) = N(m/a_H/b)/c = N(m/a_H)/b/c$$。然而,再往后,$$N(m/a_H)$$不能等价于$$N(m)/a_H$$,因为这是不可能实现的:$$CKD_{pub}(N(m), i)$$只适用于非强化的子公钥。
密钥标识
扩展密钥可以用ECDSA
的公钥K
序列化之后的Hash160
(先对序列化结果进行SHA256
,然后再进行RIPEMD160
)结果进行标识,忽略链码。这样完全对应于传统的比特币地址数据。但不建议用base58
格式来表达该数据,因为这样它就可能会被解释为一个地址(但其实它只是个扩展密钥)。
这种标识符的前32位被称为密钥指纹。
序列化格式
扩展的私钥和公钥按照如下格式进行序列化:
4字节:version字节(在比特币主网上,扩展公钥为
0x0488B21E
, 扩展私钥为0x0488ADE4
; 在比特币测试网络上,扩展公钥为0x043587CF
, 扩展私钥为0x04358394
)1字节:depth字节,
0x00
表示主节点,0x01
表示一级衍生密钥, 以此类推下去。4字节:父密钥的指纹(从上节可知为父扩展密钥的前32位取出传到这里),如果是主密钥,则该字段为
0x00000000
。4字节:孩子编号。即$$ser_{32}(i)$$,如果是主密钥,则该字段为
0x00000000
。32字节:链码
33字节:待扩展的公钥是$$ser_p{(K)}$$,而若是待扩展的私钥,则是$$0x00 || ser_{256}{(k)}$$
这78个字节的结构可以像其它比特币数据一样进行Base58
编码:先加上32位的校验和(通过2次SHA256校验和得到),然后转化为Base58
表述。结果就是编码后的字符串达到112个字符。由于version字节的不同,这种Base58
表达的字符串,如果是主网,则以xprv
或者xpub
开头, 如果是测试网络,则以tprv
或者tpub
开头。
密钥指纹只是用来快速检测父亲和孩子节点的,软件必须愿意去处理冲突。在内部实现中,可以使用完整的160位标识符。
当导入一个序列化的扩展公钥时,实现必须验证公钥中的X坐标是否是曲线上的点,如果不是,则认为扩展公钥无效。
主私钥生成
主私钥不是直接生成的,而是通过一个种子得到的,主要是出于安全方面的考虑。
从一个
(P)RNG
【(伪)随机数产生器】生成一个选定长度的种子序列S
(128 ~ 512位, 推荐256位)计算
I = HMAC-SHA512(Key = "Bitcoin seed", Data = S)
将I拆分为2个32字节序列:$$I_{L}, I_{R}$$
将$parse_{256}{(I_L)}$作为主私钥, $I_R$作为主链码。
如果$I_L = 0 or I_L \geqslant n$,则认为主私钥无效
BIP-43
在BIP-32的算法基础上,BIP-43引入了Purpose
域。
背景
由于BIP-32
规范给予实现者太多的自由,很多实现都可以声称是BIP-32
兼容的,但它们实际上可以采用不同的逻辑架构,最终导致这些钱包无法互操作,这有悖于BIP-32
设计的初衷。
Purpose
提议BIP-32
密钥树的第一层为purpose
段,它定义了未来在该节点下方的结构的用处。
1 | m / purpose' / * |
撇号'
表示使用了BIP-32
的强化派生。
BIP-44
在BIP-43
提议的purpose
模式基础上,BIP-44
做了大幅扩展,提出了相当全面的层次结构。可以处理多种货币,多个账户,一个账户上的内部外部链,以及每个链上的数百万个地址。
路径层级
在BIP-32路径格式下,定义5个层级:
1 | m / purpose' / coin_type' / account' / change / address_index |
撇号'
表示使用了BIP-32
的强化派生。
每一层级都有一个特殊含义:
Purpose
:依照BIP-43
的建议,遵循BIP-44
提案的子树结构的purpose
是44
或者0x8000002C
。该层使用强化派生。Coin type
:理论上来说一个主节点(种子)可以生成无限数量的独立加密货币,但是多种类型的加密货币共用相同的空间并不太好。该层级为每一种货币创建一个独立的子树,避免复用地址,也提高了隐私性。开发者可以为自己的货币申请一个未使用的常量数字代表这个货币。BIP-044已申请货币类型列表,从列表中可以看到以太币的代号是60
或者0x8000003c
。该层也使用强化派生。Account
:这一层将密钥空间区分为独立的用户ID,以保证钱包不会混淆不同账户的货币。用户可以像传统银行账户的方式来组织自己的资产:用于捐赠,用于储蓄,用于日常开销等。这个字段从数字0
开始顺序递增,该数字用于作为BIP-32
中的子密钥索引。当前一个账户没有任何交易历史的时候(意思是该账户的地址从未使用过),软件应该阻止创建一个新账户。还有一点就是,当导入外部来源的种子时,软件需要能够发现所有已经被使用的账户。下面有一个”账户发现”的算法描述。这一层也使用强化派生。Change
:即找零,常量0表示外部链,常量1表示内部链(即常说的找零地址)。外部链用于在钱包外可见的地址(例如收款)。内部链用于在外部不可见的地址,用于返回交易找零。这一层使用公共派生。Index
:地址由数字0开始顺序递增。用作BIP-32
中的子密钥索引。该层使用公共派生。
账户发现
当从外部源导入主种子时,软件需要开始按照如下方式开始搜寻账户:
生成第一个账户节点(
account = 0
)生成这个账户的外部链节点
依照下述的间隔极限搜寻出外部链的地址。
如果在外部链上没有找到交易,停止搜寻,算法结束
如果找到一些交易,将
account
字段加1,再继续第一步。
如前所述,当前一个账户没有交易历史时,软件需要阻止创建新账户,所以该搜寻算法是凑效的。
算法是根据交易历史处理的,而不是账户余额,所以你可以有一个账户货币数量为0,算法依然会继续向后搜寻
地址间隔极限
当前间隔极限设为20
,如果软件连续找到20个未使用的地址,它就认为在这个点上的后续地址都未被使用并停止搜寻地址链。只需要搜寻外部链,因为内部链只用来接收相关外部链转来的货币。
当用户尝试在外部链上超出间隔极限去创建一个新地址时,钱包软件需要给出警告。这样整个创建-发现
账户地址的算法就形成了一个闭环。
参考资料
https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki
https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki
https://github.com/bitcoin/bips/blob/master/bip-0043.mediawiki
https://github.com/satoshilabs/slips/blob/master/slip-0044.md
强化子密钥存在的必要性:https://bitcoin.stackexchange.com/questions/56916/derivation-of-parent-private-key-from-non-hardened-child
https://bitcoinmagazine.com/articles/deterministic-wallets-advantages-flaw-1385450276/
转载请注明出处:www.huamo.online