比特币开发指南——钱包

96
通若
2017.11.22 13:48* 字数 6337

比特币钱包指的是钱包程序或钱包文件。钱包程序创建公钥以接收聪,并使用私钥花费这些聪。钱包文件存储私钥和其他与交易相关的信息(可选)。

钱包程序和钱包文件在下面章节分别讨论,而且这篇文章一直试图使得我们讨论的钱包程序和钱包文件更加清晰。

钱包程序

允许聪的接收和花费只是钱包软件基本的功能——但是一个特殊的钱包并不需要同时做这两件事。两个钱包程序可以一起工作,一个程序为了接收聪而分发公钥,另一个为了花费这些聪而签署交易。

钱包程序也需要和P2P网络交互以便从区块链中获取信息并广播新的交易。然而,分发公钥或签署交易的程序不需要和P2P网络本身进行交互。

这要求钱包系统要有三个必要但相互独立的部分:公钥分发程序,签名程序,网络程序。下面的章节中,我们将描述这些部分的常见组合。

注:我们一般只谈论公钥分发。许多情况下,P2PKH或P2SH hash取代公钥被分发,实际上公钥只有在他们控制花费输出时才会使用。

全服务钱包

最简单的钱包程序实现了以下功能:生成私钥、得到相应的公钥、根据需要帮助分发公钥,监控花费到这些公钥上的输出,创建并签署花费这些输出的交易,并广播这些签名的交易。

image.png

截止本篇文章为止,几乎所有流行的钱包都可被用作全服务钱包。

全服务钱包的优点是便于使用。一个简单地程序实现了用户接受和花费聪需要做的所有事情。

全服务钱包的主要缺点是在一台联网的设备上存储私钥。这种设备的妥协是很常见的事(大概意思是全服务钱包功能虽然完善,但是需要联网设备存储私钥,算是一种折中吧),而且网络连接使得把私钥从折中设备传送给攻击者变得很容易。

为了防止私钥被偷,许多钱包程序给用户提供了加密包含了私钥的钱包文件的选项。从而在私钥不被使用时得以保护,但是不能防止意图捕获加密秘钥或从内存读取解密秘钥的攻击。

仅签署钱包

为了增强安全,私钥可被一个运行在更加安全环境下的钱包程序生成和存储。这些仅签署钱包和一个与P2P网络交互的网络钱包协同工作。

仅签署钱包程序典型地使用确定性秘钥创建(后续章节将会陈述)创建父私钥和父公钥,父私钥和父公钥又能创建子私钥和子公钥。

image.png

初次运行时,仅签署钱包创建一个父私钥并把相应的父公钥传送给联网钱包。

联网钱包使用父公钥获得子公钥,选择性地帮助分发子公钥,监控花费到这些公钥上的输出,创建花费这些输出的未签名的交易,把未签名的交易传送给仅签署钱包。

在可选的审查步骤之后,仅签署钱包使用父私钥得到相应的子私钥并给交易签名,再把已签名的交易返回给联网钱包。网络钱包签署交易并广播到P2P网络中。

下面章节描述了仅签署钱包两个最常见的变体:线下钱包和硬件钱包

线下钱包

几个全服务钱包程序也像两个独立钱包一样操作:一个程序和仅签署钱包一样操作,另一个程序向网络钱包(常被叫做线上钱包或仅查看钱包)一样操作。

线下钱包如此形象因为它运行在一台没有联网的设备上,极大地减少攻击数量。如果这样的话,需要用户使用可移动媒体如USB设备处理所有的数据传输。用户工作流如下:

  1. (线下)禁用设备的所有网络连接并安装钱包软件,以线下模式启动钱包软件创建父私钥和父公钥。把父公钥复制到可移动媒体(如U盘)中。
  2. (线上)在另一台设备上安装钱包软件,这台设备需要连接网络并从可移动媒体中导入父公钥。像使用全服务钱包一样,分发公钥接收付款。当准备花费聪时,完善输出信息并把被钱包生成的未签名交易保存到可移动媒体中。
  3. (线下):在线下程序中打开未签名脚,审查输出详情确保花费正确数量的聪到正确的地址上。避免线上钱包中的恶意软件诱骗用户签署支付给攻击者的交易。审查之后,签署交易并保存到可移动媒体中。
  4. (线上)在联网程序中打开已签名的交易以便广播到P2P网络中。

线下钱包的主要优点在于较之于全服务钱包极大地提升了安全性。只要线下钱包不妥协(或缺陷)而且用户在签名之前审查所有的开支交易,虽然线上钱包妥协,但是用户的聪依然安全。

线下钱包的主要缺点就是麻烦。为了最大限度的安全,要求用户提供仅用于线下工作的设备。不管什么时候要花费积蓄,线下设备必须运行,而且用户必须物理地复制数据到线上和线下。

硬件钱包

硬件钱包是用于运行仅签署钱包的设备。设备的贡献使得它们消除了在一般用途的操作系统中存在的许多漏洞,允许它们和其他设备直接安全的通信,而不用用户手动传输数据。用户工作流如下:

  1. (硬件)创建父私钥和父公钥。把硬件钱包连接到一个联网的设备上以便获得父公钥
  2. (联网)像全服务钱包所做的一样,分发公钥接收付款。当花费聪时,完善交易细节,连接到硬件钱包,点击花费。网络钱包将自动将交易细节发送给硬件钱包。
  3. (硬件)在硬件钱包屏幕上审查交易详情。有些硬件钱包会有大小写或PIN码提示。硬件钱包签名交易并上传给网络钱包。
  4. (联网)联网钱包从硬件钱包收到签名交易并广播到P2P网络中。

硬件钱包的主要优势是较之于全服务钱包极大地提升了安全性,并比线下钱包减少了很多不必要的麻烦。

硬件钱包的主要缺点就是它们的麻烦性。虽然麻烦比线下钱包少,用户仍需要购买硬件钱包设备,并且不论什么时候他们需要使用仅签署钱包制定交易,硬件钱包都要处于运行状态。

一个额外的(希望是暂时的)缺点就是,截止本文为止,很少流行的钱包程序支持硬件钱包——虽然几乎所有的流行钱包程序都声称至少支持一种硬件钱包模型。

仅分发钱包

运行在很难确保安全环境下的钱包软件,如web服务器,可被设计成分发公钥(包括P2PKH或P2SH地址),有两种最常见的方式设计这些极简钱包。


image.png
  • 预先填充一些公钥或地址到数据库中,然后分发数据库中的一个条目的公钥脚本或地址。为了避免秘钥复用,webserver应当追踪用过的秘钥并保证公钥不被用完。正如下一个方法所建议的,通过使用父公钥可以使这件事更加容易
  • 使用一个父公钥创建子公钥。为了避免秘钥复用,必须使用方法确保同样的秘钥不会被分发两次。可以为每一个被分发的秘钥分配一个数据库条目或秘钥索引增长的指针。

两种方法都不会增加大量开销,尤其是如果数据库被用作使用一个独立的公钥关联收入支付,以便支付跟踪。

钱包文件

比特币的核心就是私钥集合。这些集合数字化地存储在文件里,甚至可以物理地存储在纸片上。

私钥格式

私钥用于从特定地址解锁聪。在比特币中,标准格式的私钥是一个256位的数字,在这两个值之间:
0x01和0xFFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFE BAAE DCE6 AF48 A03B BFD2 5E8C D036 4140之间,几乎占了2^256 -1的所有值。这个范围是由比特币使用的secp256k1椭圆曲线加密算法控制的。

钱包导入格式(WIF)

为了减少私钥复制的错误,钱包导入格式(WIF)或许会被利用到。WIF使用base58校验编码私钥,极大地降低了复制错误几率,和比特币地址非常相像。

  1. 取私钥
  2. 私钥前添加0x80字节成为主网地址或添加0xef成为测试网络地址
  3. 如果和压缩后公钥一起使用,需要在后面加上0x01;如果和非压缩公钥一起使用,则无需添加
  4. 对被扩展的秘钥实行SHA-256 hash
  5. 对SHA-256结果实行SHA-256 hash
  6. 获取第二次SHA-256 hash的前四个字节,这是校验和
  7. 把4字节的校验和添加到被扩展秘钥尾部第五到第二个位置中间
  8. 使用Base58校验编码把字节字符串结果转换成base58编码函数

该过程是可逆的,使用Base58解码函数,并移除添加的字节

微型私钥格式

微型私钥格式是以低于30个字符编码私钥的方法,使得私钥能被嵌入到小的物理空间中,如物理比特币令牌,以及更抗损伤的二维码。

  1. 微型秘钥的第一个字符是S
  2. 为了确定微型秘钥是否很好的格式化,需要在私钥上添加问题标志
  3. 计算SHA256 hash。如果生成的第一个字节是00,说明格式较好。该秘钥限制如同错误校验机制。用户使用随机数暴力执行进程直到格式较好的微型私钥出现。

为了获得完整的私钥,用户只需要原始微型秘钥的一个单独的SHA256 hash。这个过程是单向的,很难从获得的秘钥中计算出微型私钥格式。

许多实现不允许字符1出现在微型私钥中,因为1和l直观看起来差不多。

公钥格式

比特币椭圆曲线算法公钥代表着secp256k1算法定义的一个特定的椭圆曲线中的一点。在传统未经压缩的格式中,公钥包含一个标识字节,一个32位的X坐标,一个32位的Y坐标。下面这个极度简化了的图表展示了被比特币使用的椭圆曲线中的一个点,y^2 = x^3 + 7,在连续数的域上。

image.png

(Secp256k1)

通过降低Y坐标,不用修改任何基础就能将公钥大小减少50%。这是可能的,因为在曲线中只有两个点能共享一个X坐标,所以32位的Y坐标可被一个单独的位替代,表示出现在图表的top还是bottom端。

通过创建这些压缩公钥,没有数据会丢失——只需要一点CPU重构Y坐标和访问未压缩的公钥。未压缩和压缩的公钥都被官方的secp256k1文档描述,而且默认被广泛使用的OpenSSL库支持。

因为它们易于使用,而且减少了区块链用于存储公钥的空间的一半大小,所以比特币核心默认压缩公钥而且推荐所有的比特币软件默认压缩。

然而,比特币核心0.6版本之前使用未压缩秘钥。所以制造了一些难题,正如未压缩秘钥的hash形式不同于压缩秘钥的hash形式,所以同一个秘钥使用两种不同的P2PKH地址。这也意味着在签名脚本中秘钥必须以正确格式提交,以便匹配之前输出的公钥脚本中的hash。

为此,比特币核心使用几种不同的标识字节帮助程序标识秘钥应被如何使用:

  1. 要和压缩公钥一起使用的私钥在被Base-58编码之前要添加上0x01(具体可参考上面私钥编码章节)
  2. 未压缩公钥以0x04开始;压缩公钥以0x03或0x02开始,具体取决于它们比曲线中间点更大还是更小。这些前缀字节在官方的secp256k1文档中都被使用。

层次确定秘钥创建

层次确定秘钥创建和传输协议(HD协议)极大地简化了钱包备份,消除了使用同一钱包的多个程序之间重复通信的需求,允许可以独立操作的子账户的创建,赋予父账户监控或控制子账户的能力,把每个账户划分成全接入和限制接入两种,以便不诚实的用户或程序被允许接受或监控付款,但是无法花费它们。

HD协议利用了椭圆曲线公钥创建函数point(),该函数取一个大的数字(私钥)并转换成图形点(公钥):

point(private_key) == public_key

因为point()工作的方式,有可能通过组合一个已存在的父公钥和另一个由任意数字i创建的公钥生成一个子公钥。如果把i加入到原始父私钥中,这个子公钥将和被point()函数创建的公钥相同,而且这个和的剩余被所有比特币软件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协议使用一个单独的根种子创建子层级,孙子以及其他更低级的具有不确定整数的秘钥。每个子秘钥也能从父秘钥中获得确定性生成的种子,叫做链码,所以链码的泄漏并不一定泄漏整个层级的整数序列,允许继续使用主链码,例如,基于web的公钥分发程序被攻击。


image.png

如上表所示,HD秘钥派生需要四个输入:

  • 父私钥和父公钥是常规的未经压缩的256位的椭圆曲线算法秘钥
  • 父链码是256位伪随机数据
  • 索引号是程序指定的一个32位的整数

上表展示的正常形式中,父链码、父公钥和索引号被单向加密hash(HMAC-SHA512)生成512位的确定性生成但是伪随机的数据。hash输出右侧的256位伪随机数据用作新的子链码。hash输出左侧的256位数据被用作和父公钥或父私钥结合的数字,分别创建一个子私钥或子公钥。

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)

指定不同的索引数将从相同的父秘钥中创建不同的非关联的子秘钥。对子秘钥使用子链码重复该流程将创建非关联的孙秘钥。

因为创建子秘钥要求一个秘钥和链码,秘钥和链码合在一起叫做扩展秘钥。扩展私钥和相应的扩展公钥有相同的链码。主私钥(顶层父私钥)和主链码聪随机数据中派生,如下图所示:


image.png

根种子从128、256或512位的随机数据中创建。为了得到每一个被特定钱包程序使用特定设置创建的秘钥,128位的根种子是用户唯一需要备份的数据。

注:截止本文为止,HD钱包程序并不期望成为全兼容的,所以用户必须只使用与某个特定根种子具有相同HD祥哥设置的相同的的HD钱包程序。

根种子被hash以创建512位伪随机数据,从这些数据中创建了主私钥和主链码(合一叫做主扩展私钥)。主公钥从主私钥使用poin()创建,主公钥和主链码结合就是扩展公钥。主扩展秘钥功能上等同于其他扩展秘钥;层级顶部的位置唯一使得它们特殊。

硬化秘钥

硬化扩展秘钥修改了正常扩展秘钥的一个潜在问题。如果攻击者获得正常的父链码和父公钥,他可以暴力攻击所有从之派生的链码。如果攻击者也获得一个子、孙或更低级的私钥,他可以使用链码生成所有从私钥派生的扩展私钥,如下图所示:

image.png

如上图所示,或许更糟糕的是,攻击者可以逆转正常的私钥派生公式并从子私钥中减去父链码恢复父公钥。这意味着获取扩展公钥和任何从之派生的私钥的攻击者可以恢复公钥的私钥和由之生成的所有秘钥。

因此,扩展公钥的链码部分应该比公钥更加安全,而且用户不应在不可信任的环境中暴露非扩展私钥。

权衡左右,这个问题可以被修复,通过使用硬化秘钥派生共识替代正常秘钥派生公式。

上面章节描述的正常的秘钥派生公式,把索引号、父链码以及父公钥组合到一起创建子链码和和整数值,该整数值和父私钥组合创建子私钥。


image.png

上图所示的硬化公式把索引号、父链码和父私钥组合一起创建数据,该数据用作生成子链码和子私钥。公式使得不知道父私钥不可能创建子公钥。换句话说,父扩展公钥不能创建硬化子公钥。

因此,硬化扩展私钥比正常的扩展私钥用处更少——然而,硬化扩展私钥创建了一道防火墙阻止多层级秘钥派生泄漏。因为硬化子扩展公钥自己不能生成孙链码,父扩展公钥的折中无法和孙私钥的折中结合到一起,从而创建孙扩展私钥。

HD协议使用不同的索引号表明是否应该生成一个正常的或硬化的秘钥。从0x00到0x7fffffff(0到2^31 -1)的索引号将生成一个正常的秘钥;从0x800000000到0xffffffff的索引号将生成一个硬化秘钥。为了使得描述更加容易,许多开发者使用首要标志表明硬化秘钥,所以第一个正常的秘钥(0x00)是0而第一个硬化秘钥(0x80000000)是0'。

比特币开发者典型地使用ASCII撇号而非统一码首要标志,我们后续将会遵循该惯例。

紧凑的描述更近一步组合斜线和m或M前缀表明层级和秘钥类型,m表示私钥,M表示公钥。例如,m/0'/0/122'引用主私钥的第一个硬化孩子的第一个正常的孩子的第123个硬化私有的孩子。下面的层级展示了首要符号和硬化秘钥防火墙。

image.png

遵循BIP32 HD协议的钱包仅创建主私钥(m)的硬化孩子防止已泄漏的子秘钥泄漏主秘钥。因为主秘钥没有正常的孩子,HD 钱包不使用主公钥。其他所有的秘钥都可以有正常的孩子,所以相应的扩展公钥或许会被代替使用。

HD协议也把扩展公钥和扩展私钥描述成一个序列化的格式。详情请看开发者索引钱包章节或全HD协议说明的BIP32。

存储根种子

HD协议中的根种子是128、256,或512位的需精确备份随机数据。为了便于使用非数字化的备份方法,例如记忆和手抄,BIP39定义了从常见的自然语言词语中的伪句子(记忆)创建一个512位的根种子。该根种子自身是从128到256位的熵创建而且可以选择性地被密码保护。

生成的单词数量与使用的熵量有关:

image.png

密码可以是任意长度。它只是附加在助记符伪句子上,而且助记符和密码都被使用HMAC-SHA512 hash 2048次,从而生成一个512位伪随机数。因为hash函数的任意输入都创建一个512位伪随机种子,没有证据证明用户输入了争取的密码,可能允许用户在受胁迫的情况下保护种子。

详细请看BIP39.

松秘钥钱包

松秘钥钱包也叫做只是一堆秘钥(JBOK),是源于比特币核心客户端钱包的一个过时了的钱包形式。比特币核心客户端通过一个伪随机数生成器自动生成100个秘钥对,以便后续使用。

没有用到的秘钥存储早虚拟的秘钥池,当前一个秘钥被使用时就立刻生成一个新的秘钥,确保秘钥池维持100个未使用的秘钥。(如果钱包被加密,新的秘钥只能在钱包解锁是生成)

这给备份秘钥造成了相当大的困扰,要手动备份保存新生成的秘钥。如果新的秘钥对被生成、使用然后在备份钱丢失,存储的聪有可能永久丢失。许多古旧风格的移动钱包遵循相似的格式,当仅当用户命令时才生成一个新的私钥。

这种钱包类型正在积极地逐步淘汰,并且由于备份的麻烦而被禁止使用。

随想录
Web note ad 1