比特币钱包介绍

2022/03/18 北京

比特币钱包涉及钱包程序和钱包文件。钱包程序创建公钥来接受比特币(satoshis)付款,并使用对应的私钥来花掉比特币。钱包文件保存私钥和其他与钱包程序相关的交易信息(可选)。

钱包程序 Wallet Programs

允许接受和支付比特币是钱包软件的唯一功能,但是一个特定的钱包程序不需要同时做这两件事,两个钱包程序可以一起工作,一个程序分发公钥来接收比特币,一个程序进行交易签名来支付这些比特币。

钱包程序也需要和peer-to-peer网络进行交互,以从区块链中获得信息并广播出新的交易。当然,分发公钥和交易签名程序并不需要和peer-to-peer网络本身进行交互。

因此钱包系统(wallet system)就有三个必须的,但是缺可以独立的部分:一个公钥分发程序,一个签名程序,一个联网程序。

NOTE: 这里说的是公钥分发的通常情形。在一些情况下,P2PKH和P2SH的散列值将被分发来代替公钥的分发,实际的公钥只有在他们控制的output被支付时才分发。
上面和下面说的输出outputs,通常就是指 未使用的交易输出 unspent transaction outputs 缩写是UTXO,就是比特币。

完整功能的钱包 Full-Service Wallets

最简单的钱包是一个执行三个功能的程序:

  • 生成私钥,并派生对应的公钥,并在需要时分发这些公钥;
  • 监控支付给这个公钥的outputs,在支付outputs时,创建交易和进行交易签名;
  • 广播已经完成签名的交易。

img

现在几乎所有流行的BTC钱包都是Full-Service Wallets

Full-Service Wallets的优点是容易使用,单独的一个程序可以完成用户支付和接收比特币的全部工作。

Full-Service Wallets的缺点是,他们把私钥保存在可以连接到Internet的设备上,因为联网所以这样的设备中的私钥被攻击会很容易。

签名钱包 Signing-Only Wallets

私钥可以保存在一个在更安全环境中的单独的钱包程序中来提高安全性,这些签名钱包和可以与peer-to-peer网络交互的联网钱包配合使用。

签名钱包通常由确定性密钥(deterministic key)创建,用来创建可以生成子公私钥的父公私钥。

img

当第一次运行时,签名钱包创建一个父私钥,并将对应的公钥传输给联网钱包。

联网钱包使用父公钥派生出子公钥,帮助分发他们(可选的),监控支付给这些公钥的outputs,创建没有签名的支付交易,并把没有签名的支付交易传输给签名钱包。

通常用户有机会使用签名钱包查看未签名交易的详情(尤其是outputs的详情)。

在用户查看步骤(可选的)之后,签名钱包使用父私钥派生相应的子私钥并进行交易签名,将签名的交易传回给联网钱包。

联网钱包把签名的交易广播到peer-to-peer网络上。

离线钱包 Offline Wallets

几个full-service wallets也可以当作两个独立的钱包使用:一个程序实例当作签名钱包(通常称为“离线钱包”),另一个程序实例当作联网钱包(通常称作在线钱包或者监控钱包)。

脱机钱包在不联网的设备上运行,可以减少供给量。如果这种情况,通常由用户来掌握所有数据的传输和使用可移动设备比如USB驱动器。用户的工作流是这样的:

  • 1.(离线)关闭设备上所有网络连接,并安装钱包软件。以脱机模式启动软件,创建父私钥和父公钥,并赋值父公钥到可移动介质上。

  • 2.(在线)在另一台设备上安装钱包软件。这台设备联网,从可移动介质上导入父公钥。下面的过程就像使用full-service wallet一样,分发公钥来接收支付。当准备消费比特币时,填写output详情并把钱包生成的未签名的交易保存到可移动介质上。

  • 3.(离线)在脱机实例中打开未签名的交易,审查交易的详情,确保支付金额和地址正确。这个可以阻止恶意软件(malware)欺骗用户签署交易,从而支付给攻击者。审查后,签署交易并保存到可移动介质。

  • 4.(在线)在在线实例中打开已签名的交易,以便广播到peer-to-peer网络。

离线钱包的主要优点在于同完整功能的钱包相比,大大的提告了安全性。只要脱机钱包没有被破坏(或者有缺陷),用户在签名之前会检查所有支付的交易,即使在线钱包被破坏,用户的比特币也是安全的。

离线钱包的主要缺点是麻烦,为了最大的安全性,要求用户必须离线操作。任何时候要支付比特币,都必须启动离线设备,用户必须从在线设备物理拷贝数据到离线设备并再从离线设备拷贝数据回在线设备。

硬件钱包 Hardware Wallets

硬件钱包是专门用于签名的钱包设备,一般是智能卡等安全芯片开发的设备。他们可以安全与其他联网设备通信,用户也不需要手动传输数据了。硬件钱包的工作流程是这样的:

  • (硬件)生成父私钥和公钥,将硬件钱包连接到一个联网设备上,这样联网设备就可以获得父公钥;
  • (联网)像使用完整功能钱包一样,分发公钥来接收支付,当准备支付比特币时,填写交易详情,连接硬件钱包,然后点击消费,联网钱包会将交易详情发送给硬件钱包;
  • (硬件)查看硬件钱包屏幕上的交易详情,一些硬件钱包可能会提示输入PIN,硬件钱包对交易进行签名,并将交易签名返回给联网钱包。

分发钱包 Distributing-Only Wallets

运行再很难保证安全的环境中(比如web服务器)的钱包程序,只能设计成分发公钥而不能有其他功能。这种简单的钱包有两种常见的设计方法:

img

  • 把大量的公钥或者地址保存到数据库中,然后根据请求分发一条数据库内的条目比如一个公钥或者地址。为了避免重复使用密钥,web服务器应该追踪使用过的密钥,并且永远不要用尽数据库中的公钥。

  • 使用父公钥创建子公钥。为了避免重复使用密钥,必须使用一种方法确保一个公钥不会被分发两次。

这两种方法都不会增加大量的开销。

钱包文件 Wallet Files

比特币钱包的核心是一组私钥。这些集合被数字化的保存在一个文件中,甚至可以保存在一张纸上。

私钥格式 Private Key Formats

私钥是用来解锁对应公钥地址的比特币的。在比特币中,标准格式的私钥是一个256bit的数字,值在下列范围内:

0x01 ~ 0xFFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFE BAAE DCE6 AF48 A03B BFD2 5E8C D036 4140

这个范围是由比特币使用的secp256k1 ECDSA加密标准管理。

钱包导入格式 Wallet Import Format(WIF)

为了使得私钥复制不容易出错,可以使用钱包导入格式WIF。WIF使用base58Check对私钥进行编码,大大的降低了复制出错的机会,就像标准比特币地址一样。

  • 1.使用一个私钥
  • 2.在前面添加一个0x80作为mainnet地址,或者添加一个0xEF作为testnet地址;
  • 3.如果他应该和压缩公钥一起使用,在后面追加一个0x01.如果与未压缩的公钥一起使用,则不会追加任何数据。
  • 4.对扩展后的密钥进行SHA-256哈希;
  • 5.对SHA-256的结果进行SHA-256哈希;
  • 6.取第二个哈希结果的前4字节作为校验和;
  • 7.把从第5布获得的校验和添加到第2步扩展密钥的末尾;
  • 8.使用Base58Check编码把第7步的数据转换未Base58字符串。

迷你私钥格式 Mini Private Key Format

迷你私钥格式是一种将私钥编码到30个字符以内的方法,可以将密钥嵌入到较小的物理空间中,比如物理比特币Token,或者QR Code中。

  • 1.迷你密钥的第一个字符是‘S’;
  • 2.为了确定私钥格式良好,再私钥上添加一个问号;
  • 3.计算SHA256哈希,如果产生的第一个字节是‘00’,他是格式良好的。密钥的限制规则是一种输入检查typo-checking方法,用户使用使用随机数生成密钥,直到生成格式良好的密钥;
  • 4.为了生成完整私钥,用户只需要获取原始迷你私钥的单个SHA256哈希值。这个过程是单向的,很难从生成密钥计算出迷你私钥格式;
  • 5.在很多实现中不允许字符‘1’出现在迷你私钥中,因为它与‘l’在视觉上相似;

上面写的很别扭,我回头再整理一下,重新写一下。

## 公钥格式 Public Key Formats

比特币的ECDSA公钥表示特定椭圆曲线上的一个点,比特币使用的是secp256k1。再传统的未压缩形式中,公钥包含identification byte,一个32 bytes的X坐标,一个32 bytes的Y坐标。

下面一个超级简单的图示意比特币使用的椭圆曲线上的一个点,$y^2= x^3+7$

img

沿着曲线只有两个点共享任何X坐标,所以只需要一个bit来表示Y坐标的正负即可,也就是把Y坐标压缩成1bit,这样就在不改变任何内容的情况下,实现了接近50%的公钥压缩

使用这个压缩公钥不会丢失数据,只需要使用少量运算就可以重建Y坐标并使用未压缩的公钥。在secp256官方文档中描述了未压缩和压缩的公钥,广泛使用的密码算法库一般都支持这两种格式公钥。

因为容易使用,他们减少了区块链的空间,压缩的公钥是比特币内核Bitcoin Core的默认值,也是所有比特币软件的推荐默认值。

Bitcoin Core在0.6之前的版本,使用了未压缩密钥。这个造成一些复杂性。未压缩的密钥和压缩的密钥哈希形式不同。所以同一个密钥适用两个不同的P2PKH地址。这也就意味着在签名脚本中密钥必须以正确的格式提交,以便与前面输出的公钥脚本中的哈希值匹配。

因为这个原因,Bitcoin Core使用了几个不同的identifier byte来帮助程序识别密钥应该如何使用:

  • 与压缩公钥一起使用的私钥在Base58编码之前附加了0x01;
  • 未压缩的公钥以0x04开头;压缩的公钥0x03或者0x02开头,这个前缀也在secp256k1 官方文档中使用。

分层确定性密钥创建 Hierarchical Deterministic Key Creation

早期的比特币客户端中钱包都是随机生成的私钥集合,这些密钥都需要用户自己保存副本,如果一旦密钥丢失,那么对应的钱包就不能访问,钱包控制的比特币也就无法使用了。比特币有一个避免地址重复使用的原则,每个比特币地址只能一次交易,这样用户就会有大量的私钥要进行保存和备份,所以早期方案不是一个好的办法。

分层确定性密钥和传输协议大大简化了钱包备份,消除了使用同一个钱包的多个程序之间重复通信的需要,允许创建可以独立操作的子账户,赋予每个父账户监控或者控制其子账户的能力,即使子账户受到破坏,每个账户分为完全访问和限制访问两部分,这样不受信任的用户就或者程序就可以接收或者监控支付,但是不能对外支付。

HD协议利用了ECDSA公钥创建函数-point(),它去一个大数(私钥),并将其转换为曲线上的的点(公钥):

point(private_key) == public_key

因为point()的工作方式,它让通过组合一个已经存在的公钥(父公钥)和另一个公钥(通过一个integer(i)值)来创建子公钥成为可能。p是一个所有比特币软件都使用的全局常量

point( (parent_private_key + i) % p ) == parent_public_key + point(i)

这就意味着两个或者多个独立的程序,如果对一个整数序列达成一致,就可以从单一的父密钥对创建一系列唯一的子密钥对,而不需要进行额外的通信。此外,为收款分发新的公钥,程序可以在不接触私钥的情况实现,从而允许公钥分发程序在一个不安全的环境(web服务器)上运行。

子公钥也可以通过重复子密钥分散操作来创建他们自己的子公钥(孙公钥):

point( (child_private_key + i) % p ) == child_public_key + point(i)

是否创建子公钥或者更后代的公钥,对于所有交易来说,使用一个可预测的整数序列不会比使用单一公钥好,因为任何人知道一个子公钥可以找到所有其他的通过这个父公钥分散的子公钥。相反的,使用一个随机种子来确定生成的整数序列,这样没有种子的人无法看到子公钥之间的关系。

HD协议使用一个单一的根种子和无关联的确定代际(unlinkable deterministically-generated)的整数来创建子代,孙子代和其他更后代的密钥。每个子密钥也通过它的父代获得一个代际(deterministically-generated)种子,称为链代码(chain code),所以一个chain code受到破坏,不会破坏整个序列的层次结构。

img

如上图所示,HD密钥分散需要四个输入:

  • 父私钥和父公钥,常规的未压缩256 bits的ECDSA密钥;
  • 父chain code是256 bits看起来随机的数据;
  • 索引index是程序指定的32-bit整数。

在上图所示的标准形式中,父chain code、父公钥和索引index被输入到单向哈希HMAC-SHA512中,生成确定代际但是看起来随机(determistically-generated-but-seemingly-random)的512 bits数据。哈希输出总共512 bits,右边的256 bits(低256 bits)数据被用来作为新的子chain code。哈希输出的左边256 bits被当作一个整数和父私钥或者父公钥组合(父私钥和哈希输出的高256bits椭圆曲线上做加法模G运算),来创建子私钥或者子公钥:

child_private_key == (parent_private_key + lefthand_hash_output) % G
child_public_key == point( (parent_private_key + lefthand_hash_output) % G )
child_public_key == point(child_private_key) == parent_public_key + point(lefthand_hash_output)

父chain code、父公钥、索引index计算HMAC-SHA512的过程如下:父公钥(256bits)和子密钥的index(32bits)进行拼接,公钥在高位索引在地位,合并后的字节序是大端,对合并后的数据进行HMAC-SHA512运算,父chain code作为哈希密钥。

可以看出来,父私钥和对应代际的chain code可以算出子私钥,然后用point()和子私钥可以算出子公钥,还可以用子公钥和point(父chain code)算出子公钥,这样也可以在不需要私钥的情况下,只知道某一代际的公钥和对应代际的chain code就可以算出下一代的公钥。

指定不同的代际索引index,可以使用相同的父密钥分散出不同的无关联的子密钥。子密钥使用子chain code重复密钥分散过程可以生成无关联的孙密钥。

因为创建子密钥需要密钥和chain code两者,密钥和chain code合在一起被称作扩展密钥。一个扩展私钥和对应的扩展公钥具有相同的chain code。 主私钥(最顶层)和主chain code是由随机数生成,如下所示:

img

根种子(root seed)是由123 bits、256 bits或者512 bits的随机数生成的。这个根种子,最少128 bits是需要用户备份的唯一数据,将来用于通过特定的钱包和设置来分散所有的密钥。

根种子通过哈希来创建512 bits看起来随机的数据,通过这些数据来创建主私钥和主chain code(合在一起称作主扩展密钥)。主公钥通过主私钥使用point()计算得出,主公钥和主chain code合在一起称作主扩展公钥。主扩展密钥和其他扩展密钥在功能上等效,只是因为它位于最上层的位置,所以才显得不同。

根种子的哈希后512 bits的输出,左边256 bits作为主私钥,右边256bits作为主chain code

强化密钥 Hardened Keys

强化扩展密钥(Hardened extended keys)修复了普通扩展密钥的一个潜在问题。如果攻击者获得了一个普通扩展密钥的父chain code和父公钥,他就可以暴力获得所有的通过这个chain code派生出来的chain code。如果攻击者还获得了一个子私钥、孙子私钥或者更下一代的私钥,他就可以使用chain code生成这个私钥后代所有的私钥了,如下面孙子和重孙子一代所示:

img

更糟糕的是,攻击者可以逆向(reverse)普通的子私钥分散公式,只要从子私钥中减去父chain code就可以恢复父私钥,如上图子代和父代所示。这意味着一个攻击者,只要获得了一个扩展公钥和及其后代的任何私钥,就可以恢复出改公钥的私钥及其分散出的所有密钥。

因为扩展公钥中有对应层级的chain code,所以可以得到这个公钥后代任意代际的公钥,所以只要获得了这个公钥后代的私钥,就可以算到这个私钥上一代的chain code,然后通过私钥-chain code计算出上一代私钥,最后推算出这个公钥和后代的所有密钥。

img

上面的强化公示将索引index、父chain code和父私钥组合在一起用来创建产生子chain code和子私钥的数据。这个公示让在不知道父私钥的情况下不能创建子公钥。换句话说,父扩展公钥不能创建强化子公钥。

强化密钥生成过程,在父私钥前面补一个0x00字节,父私钥和索引index拼接,私钥在高位,索引在低位,字节序为大端。对拼接后的数据进行HMAC-SHA512运算,哈希密钥是父chain code,父私钥和哈希的高256 bits做椭圆曲线上的加法模运算生成子私钥,低256 bits是子chain code。如果ECC模运算结果出现0,那么索引值递增,然后再次计算密钥。子公钥可以通过子私钥计算出来。

因此,强化扩展私钥没有普通的扩展私钥有用,然而强化扩展私钥会创建一个防火墙,使得多层密钥分散泄露不会发生。因为强化子扩展公钥无法仅仅靠自己生成孙chain code,父扩展公钥的泄露不能和孙私钥的泄露组合创建重孙扩展私钥。

HD协议使用不同的索引index来指示是生成普通还是强化密钥。索引Index从0x00~0x7FFFFFFF将生成普通密钥;索引Index从0x80000000~0xFFFFFFFF将生成强化密钥。为了便于描述,许多开发者使用’(prime symbol)来表示强化密钥,所以第一个普通密钥(0x00)是0,第一个强化密钥(0x80000000)是0’。

(比特币的开发者通常使用ASCII的撇号,而不是使用unicode的prime symbol。)

这个压缩描述进一步结合斜杠和m或者M前缀,指示层次(hierarchy)和密钥类型。m表示私钥,M表示公钥。例如,m/0’/0/122’表示主私钥的第1代(index=0)强化子密钥的第1代(index=0)普通子密钥的第123代(index=122)强化子私钥(通过索引index)。

img

遵守BIP32 HD协议的钱包只创建主私钥(m)的强化子密钥来防止子密钥泄露而导致主密钥泄露。因为主密钥不存在普通子密钥,所以主公钥也不会在HD钱包里使用。所有其他密钥可以有普通子密钥,所以可以使用对应的普通扩展公钥。

HD协议还描述了扩展公钥和扩展私钥的序列化格式。详细情况可以参看BIP32协议。

保存根种子 Storing Root Seeds

HD协议里的根种子(root seeds)是128、256或者512 bits的随机数,这些种子需要备份保存。为了方便,可以使用非数字化备份的方法,比如记忆、手抄等。BIP39定义了一个方法,通过助记符来创建512 bits的根种子。

生成单词数与使用的熵值相关:

Entropy Bits Words
128 12
160 15
192 18
224 21
256 24

密码短语(passphrase)可以是任意长度,它可以简单得追加到助记符pseudo-sentence,mnemonic和password将使用2048次HMAC-SHA512运算,产生一个看起来随机的512 bits种子。

松散密钥钱包 Loose-Key Wallets

松散密钥钱包似乎中文也有叫零型非确定钱包,也被称作Just a Bunch Of Keys(JBOK),是一种Bitcoin Core客户端早期的钱包形式,已经被弃用。Bitcoin Core客户端钱包通过伪随机数发生器自动创建100个公私钥对供以后使用。

这些没有使用的私钥存储在一个虚拟的密钥池(key pool)中,之前生成的密钥被使用后,就会生成新的密钥放到池中,保证池中有100个未使用的密钥。

Post Directory