深入理解比特币(二) 钱包和地址

钱包是比特币世界里最重要的概念之一. 这里要说的钱包并不是指具体的某种软件或硬件产品, 而是其更本质的定义: 私钥即钱包. 比特币之所以被被称作加密货币是因为它的实现是建立在椭圆曲线密码学的基础之上的. 在比特币的世界里, 资产以一种被称作 UTXO (unspent transaction outputs) 的数据形式存在. 比特币 UTXO 对网络里的所有用户都是可见的, 谁能够提供一个凭证(在密码学中这样的凭证一般被称为"见证" Witness)使 UTXO 中预先设定花费条件成立, 谁就拥有了这笔资产的所有权. 而私钥正是能提供这个凭证的关键所在. 掌握了某个私钥也就意味拥有了与该私钥对应的所有资产.

关于椭圆曲线密码学的知识可以参考我翻译的 椭圆曲线密码学: 入门 系列文章, 文章的原作者是来自爱尔兰亚马逊的软件工程师, 这几篇文章也是我读过的关于介绍椭圆曲线密码学最好的入门文章, 有兴趣的朋友可以前往阅读, 相信也会让你受益匪浅.

私钥,公钥和地址

在椭圆曲线密码学中, 私钥 e 是一个随机数, 而对应的公钥是私钥和椭圆曲线上的特定生成点 G 相乘得到的另一个点 P. 即:
$$
P = eG
$$

由于受到椭圆曲线上的离散对数难题的限制(这也是椭圆曲线密码学的根基), 我们可以通过私钥 e 算出公钥 P, 却难以通过公钥反推出私钥. 此外, 通过私钥产生的签名可以用公钥进行验证. 比特币正是利用上述特性, 在交易时, 通过私钥对 UTXO 生成签名并提供公钥供整个比特币网络进行验证.

SEC 公钥

我们说比特币的公钥实际上是椭圆曲线上的一个点, 为了便于公钥在网络中传输我们需要对公钥进行序列化, 这就是 SEC(Standards for Efficient Cryptography). SEC 格式有两种形式: 未压缩的和压缩的. 未压缩的 SEC 公钥可以表示为: 0x04 + 以大端序表示的32字节x坐标 + 以大端序表示的32字节y坐标. 相比而言, 压缩的 SEC 公钥要复杂一些.

我们知道, 比特币所使用的椭圆曲线可以表示为 $y^2 = x^3 + ax + b$ 的形式. 可见, 对于任何一个 x 都存在 y 和 -y 两个纵坐标与之对应. 不过需要注意的是因为该曲线定义在有限域 p 上 (关于有限域的知识可以阅读 椭圆曲线密码学: 有限域和离散对数进行学习), 所以 -y 实际上的坐标是 p - y. 因为比特币曲线(secp256k1)的域 p 是一个奇数(更准确地说是一个可以表示为 4n + 3 形式的质数, 即高斯质数). 所以 y 和 p - y 一定有一个为奇数, 一个为偶数. 所以通过确定的 x 坐标和 y 坐标的奇偶性就能唯一确定椭圆曲线上的一个点. 压缩的 SEC 公钥正是通过这一属性实现的. 即, 压缩的 SEC 公钥可以表示为: 0x02(y为偶数)或0x03(y为奇数) + 以大端序表示的32字节x坐标.

关于通过 y 的奇偶性求出 y 坐标的详细推导过程, 可以参考 Programming Bitcoin Chapter.4 P77 - P78 的内容.

DER 签名

比特币采用了 DER 格式对生成的数字签名进行序列化, 椭圆曲线上的数字签名可以通过 r 和 s 两个值来表示(关于椭圆曲线密码学签名相关的知识, 可以阅读 椭圆曲线密码学: ECDH 和 ECDSA). 具体规则如下图所示:

der

其中 0x30 为前缀, 0x45 表示签名的长度, 0x02 为标志位, 之后是以大端序表示的 r, 0x02 标志位, 最后是以大端序表示的 s.

地址格式

在进行比特币交易时, 收款的一方需要将钱包提供给付款方. 出于易读性及安全性方面的考量, 中本聪没有直接将公钥作为钱包地址(早期版本也曾直接使用公钥作为地址). 钱包地址可以通过如下方式从公钥转化而来:

  1. 通过 Hash160 函数计算压缩或未压缩的 SEC 公钥的哈希值
  2. 根据是否为测试网络添加 0x6f0x00 前缀
  3. 计算以上值的 Base58 编码, 所得的结果即为比特币钱包的地址

pubkey-to-address

关于 Hash160 与 Base58 算法的细节, 可以自行 Google.

WIF 私钥

比特币的私钥本质上是一个可能很大的随机数, 私钥本身就对应着比特币网络中的一个钱包. 为了更方面地管理资产, 我们可以将私钥导入钱包软件. 为了达成这一目的, WIF(Wallet Import Format) 私钥格式诞生了. WIF 格式的具体规则如下:

  1. 根据是否为测试网络添加 0xef0x80 前缀
  2. 将私钥表示为 32 位大端序字节流
  3. 根据地址公钥是否压缩添加 0x01 后缀
  4. 将以上三者拼接起来
  5. 计算以上结果的 hash256 哈希值, 并返回前 4 个字节
  6. 将 4 和 5 的拼接起来计算其 Base58 编码

上述步骤所得结果即为 WIF 格式的私钥.

HD钱包

如前文所说, 比特币的钱包本质上只是一个随机数, 也就是说只要我们随机生成一个整数, 就能得到一个比特币钱包, 这就是所谓的 非确定性(随机)钱包. 生成这样的钱包十分简单, 但它的确定也十分明显, 因为钱包是随机生成的, 我们难以对其产生记忆. 一旦记录钱包的软件或硬件损坏, 很可能导致资产的丢失.

为了解决这一问题, 具有层级结构的HD(Hierarchical Deterministic)钱包应运而生(BIP-32).

hd-wallet

HD钱包需要一个种子作为生成钱包的材料, 通过一系列英文单词生成一个种子已是一种标准方案, 在比特币优化提案 BIP-39 中对相关的细节进行了定义. 这些用于生成钱包种子的英文单词也叫助记词, 根据 BIP-39 的标准, 助记词的生成过程如下图所示:

bip-39

简单地说, 随机数(在这里也被叫做熵)与一定长度的校验和拼接在一起, 并按位分割为若干段, 每一段与单词表中的一个词相对应.

除了指定助记词之外, 在 BIP-39 中还定义了一个允许用户自己填写的字符串作为生成钱包种子的盐(salt). 通常, 也可以将该字符串理解为用户为钱包设置的密码. 不过需要注意的是, 此处是没有严格意义上密码错误的概念的. 如果用户提供了不正确的密码, 则会对应到一个新的钱包种子. 不过在大多数钱包产品中, 还是会在软件层面对密码进行校验以提供更友好的用户体验.

助记词和用户密码一起通过一个单项的哈希函数最终生成HD钱包的种子.

种子输入到 HMAC-SHA512 算法中就可以得到一个可用来创造主私钥(master private key(m))和主链码(a master chain code)的哈希. 链码的作用我们将在后面讲到.

generate-wallet

有了主密钥和主链码, 我们现在可以开始正式创建钱包了. 在HD钱包中, 我们使用CKD(Child Key Derivation)函数去从母密钥衍生出子密钥. 具体步骤如下图所示:

从上图可以看出, 母公钥和母链码及序号通过 HMAC-SHA512 算法输出结果的左边部分与母私钥相加所得到的结果便是子私钥. 其中序号是一个 32 字节的整数, 也就是说, 每一个母秘钥都可以通过这种方式生成 $2^32$ 次方个子秘钥(实际上是 $2^31$ 个, 原因稍后会讲到), 这足以满足绝大多数业务的需要, 然而子秘钥依然可以通过此种方式继续生成它的子密钥!

另外, 还有一个值得关注的点, 由母私钥推导子公钥的方程式 $P_{child} = (e_{parent} + L_{256}) * G$ 我们可以发现:

$$ \begin{array}{rl} P_{child} & = (e_{parent} + L_{256}) * G \\ & = e_{parent} * G + L_{256} * G \\ & = P_{parent} + L_{256} * G \end{array} $$

实际上我们可以通过母公钥和母链码直接推导出子公钥, 而无需知道子私钥. 因为通过秘钥加链码可以推导出子密钥, 所以这两者一起也被称作扩展密钥(Extended Key). 扩展密钥编码用的 Base58Check使用特殊的版本号,这导致在Base58编码字符中,出现前缀"xprv"(扩展私钥)和"xpub"(扩展公钥). 通过扩展公钥生成子公钥的方式的优点很明显, 我们可以将其部署到安全级别不太高的网络环境中生成用于收款的地址, 而在需要使用该地址进行付款时, 在更安全的离线环境中通过母私钥推导出对应的子私钥完成对该笔交易的签名.

然而使用扩展公钥也存在着潜在隐患, 那就是如果任何一个子私钥意外泄漏, 当攻击者持有扩展公钥时, 可以利用这两者推导出母私钥, 进而计算出所有的子私钥. 具体步骤如下:

  1. 计算出子私钥对应的子公钥
  2. 通过扩展公钥遍历序号生成子公钥, 直到计算出的公钥与上一步得到的公钥相等, 由此我们可得到该子私钥对应的序号
  3. 已知扩展公钥和子私钥的序号, 我们可以计算出该子私钥对应的 $L_{256}$
  4. 子私钥 - $L_{256}$ 即为母私钥

为了规避该问题带来的风险, 硬化的子密钥衍生方法出现了.

hardened-child-key-derivation

由上图可以发现, HMAC-SHA512 函数的输入参数从母公钥变成了母私钥, 攻击者也就无法推导出$L_{256}$了. 显然, 换用硬化的子密钥衍生方法后, 也无法实现从母公钥到子公钥的推导了. 所以我们通常对主密钥所衍生的第一层级的子密钥使用强化衍生, 而在更高层的钱包中使用扩展公钥的方案, 从而在安全性与便利性之间找到平衡点.

为了区分密钥是从正常衍生函数中衍生出来还是从强化衍生函数中产出,这个索引号被分为两个范围. 索引号在0和$2^{31}–1$(0x0 to 0x7FFFFFFF)之间的是只被用在常规衍生. 索引号在$2^{31}$和$2^{32}– 1$(0x80000000 to 0xFFFFFFFF)之间的只被用在强化衍生. 因此, 索引号小于$2^{31}$就意味着子密钥是常规的, 而大于或者等于$2^{31}$的子密钥就是强化型的.

HD钱包中的密钥是用"路径"命名的, 且每个级别之间用斜杠(/)字符来表示. 由主私钥衍生出的私钥起始以"m"打头. 由主公钥衍生的公钥起始以"M"打头. 因此, 母密钥生成的第一个子私钥是m/0。第一个公钥是M/0. 第一个子密钥的子密钥就是m/0/1. 以此类推.

密钥的"祖先"是从右向左读, 直到你达到了衍生出的它的主密钥. 举个例子, 标识符m/x/y/z描述的是子密钥m/x/y的第z个子密钥. 而子密钥m/x/y又是m/x的第y个子密钥. m/x又是m的第x个子密钥.

path

HD钱包树状结构提供了极大的灵活性, 每一个母扩展密钥有40亿个子密钥, 而每个子密钥又会有40亿个子密钥并且以此类推. 只要你愿意, 这个树结构可以无限类推到无穷代. 但是, 又由于有了这个灵活性, 对无限的树状结构进行导航就变得异常困难. 尤其是对于在不同的HD钱包之间进行转移交易, 因为内部组织到内部分支以及亚分支的可能性是无穷的. 为解决这个问题 BIP-43BIP-44 出现了, 这两个提案分别对比特币钱包进行了多功能与多账户的定义, 有兴趣的同学可以前往阅读.

Show Comments