Try   HackMD

《Mastering Bitcoin, 3rd Edition》读书笔记

1. Introduction

没有太多值得细读的内容(如果了解比特币的基本知识的话)。

2. How Bitcoin Works

和第 1 章类似。只有下条:

Bitcoin peers are commonly called “full verification nodes,” or full nodes for short.

从这里可以看到“全节点”的定义。

可能很多人(包括我自己以前的)理解是,全节点的“全”指的是完整保存了数据;但实际上,在比特币的语境中,全节点指的是“可完全验证的节点”。

3. Bitcoin Core: The Reference Implementation

介绍了比特币协议和比特币实现的关系(例如 Bitcoin Core)。主要介绍的是 Bitcoin Core 的操作。部署节点的时候还是推荐看文档,本章的描述可以搭配着看。

除了 Bitcoin Core 之外,也列举了其他客户端实现,例如 btcd、rust-bitcoin 等等。

4. Keys and Addresses

第 4 章开始,内容逐渐进入正题。

公私钥

首先是公私钥。

私钥的本质是一个随机数,当然是在非常大范围内随机地选择出来一个。这个范围是 2256

程序具体的实现过程通常是用一个可信的随机源产生一个比 256 位更大一些的字符串,然后放入到安全的 SHA256 哈希函数中,产生的这个 256 位的数据可以被认为是一个随机数。

之后还介绍了具体的一些椭圆曲线算法。

压缩公钥

最早的公钥是 65 字节的,但因为公钥是在 y2modp=(x3+7)modp 曲线上的点 (x,y),因此只需要保存 x 的值,通过私钥可以计算出来 y 的值,这样可以降低将近 50% 存储空间(另外需要保存 y 的正负)。

地址

值得注意的是,一个早期很有意思的想法和实现:早期版本的比特币软件支持将 IP 地址作为收款地址。但后来因为需要保持在线、隐私和 NAT 映射的问题等,这个功能被移除掉了(这次笔记里第 1 次出现设计对于隐私的考量)。

同样在白皮书里出现的 P2PK 的地址也和付款给 IP 一样,其实没有大规模的使用过。后来使用的更多的是 P2PKH。

P2PKH 对公钥的承诺是把公钥先 SHA256、然后 RIPEMD-160。这么做的原因,中本聪没有说明过。

公钥 -> 地址

P2PKH 和 P2SH 是仅有的 2 个使用 base58check 编码的地址(脚本模板),目前主要都改用为 bech32 地址。

Bech32

bech 的意思是 BCH (震惊!),但其实这个 BCH 是在 1959、1960 年发现循环纠错编码的三位科学家名字(Bose–Chaudhuri–Hocquenghem)的简称。32 代表使用了 32 种字符。也因为这个纠错的能力,比 base58check 更可靠。

另有一个有趣的优化例子,Bech32 倾向于全部使用小写字母,但如果改成全部大写的话,编码成二维码所需要的空间会更小。

Bech32m

bech32 bech32m
Witness Version bc1q bc1p
Witness Program 20 或 32 字节 32 字节(但未来可能有其他长度)
checksum 6 字节 6 字节

私钥的不同形式(包括压缩私钥)

和压缩公钥类似,也有压缩私钥的概念。

但私钥本身是一个 2256 的随机数,所以没法再压缩了。意思只是,从压缩私钥只应导出压缩公钥。

而且压缩私钥反而长度会比普通私钥(WIF,Wallet Import Format)多 1 个字节,用来表示压缩公钥的 y 值正负。

私钥的不同格式:

格式 固定前缀 格式
16 进制 无固定 64 位长度的 16 进制的数字
WIF 5 Base58
WIF-compressed K 或 L 增加 0x01 这么 1 个字节的后缀

5. Wallet Recovery

尽管理论上有了私钥就可以推导出公钥了,但为了使用上的方便和隐私,钱包需要使用分层确定钱包等方式来维护多个公私钥对。因此,钱包采用什么方式来恢复就成了一个很重要的技术。

早期方式

为了能有多个地址,早期的钱包需要把所有使用到的私钥保存下来。这么做需要保存的量比较大。

确定性密钥生成及 BIP32

确定性生成更多密钥,在主密钥上加上一个数字,生成一个新的密钥。这项技术被称为 key tweak

K=k×GK+(123×G)=(k+123)×G

而具体怎么加这个数字,可以采用树形的结构,逐层来添加,并且层级没有限制。因此,可以无限生成(但恢复时也就无法全部遍历,因此备份路径也就很重要了,后文会提到)。

种子、助记词与口令短语

有比较流行的几种对随机种子生成助记词(recovery code)的方案:

  • BIP39:12-24 个单词(同时也可以有本地化语言,例如中文)
  • Electrum v2
  • Aezeed: LND 在使用的,对 BIP39 进行了改进
  • Muun
  • SLIP39:BIP39 同一批作者的后续方案,对一个种子可以有多份助记词(例如生成 5 份助记词,但只需要其中 3 份就可以恢复出种子)
  • 还有一些新方案:例如 Codex32,可支持最多 31 份助记词

口令短语

口令短语(passphrase)可以与助记词(recovery code)同时使用,这在 BIP39、Electrum v2、Aezeed、SLIP39中都支持。

每个口令短语会导致生成一个种子,进而形成不同的 BIP32 的树。

所以其实也没有“错误口令”,因为每个口令都会指向一个(一系列)钱包。

检测助记词泄漏!

可以在不设置口令生成的 BIP32 地址中放入一些少量的资金,在设置口令的地址中放入剩余的大量资金。

如果有一天发现不设置口令地址中的钱被转移走了,则说明这份助记词已经被泄漏,需要尽快转移有口令地址中的资金。

不过需要谨慎使用口令短语!

因为这个实践其实未必安全!因为从上面可以看到,助记词和口令短语必须配合使用才能恢复。所以从实际使用的效果来看,这个就类似 2-2 的多签,不如使用其他更安全的多签方案(参考曾汨老师在亿聪哲史播客里的介绍

非密钥数据备份

除了密钥数据外,钱包可能还要备份非密钥数据,例如交易备注。这样方便在不同钱包间迁移数据,以及用于会计软件等。

BIP329 就是用来统一设置这个格式的。

另外还有对于 LN 数据的备份等。

派生路径的备份

备份派生路径有 2 种思路,一种是都使用标准的派生路径;另一种是显示的和助记词一起备份(包括路径、花费条件等)

  1. 标准派生路径:
标准 脚本 BIP32 路径
BIP44 P2PKH m/44'/0'/0'
BIP49 Nested P2WPKH m/49'/1'/0'
BIP84 P2WPKH m/84'/0'/0'
BIP86 P2TR 单密钥 m/86'/0'/0'
  1. 显式路径
    好处是可以具体说明花费条件。
    目前使用显式路径的,基本都采用了“输出脚本描述符”的方式,描述了脚本和配合使用的密钥(或密钥路径)。目前的标准包括:BIP380、BIP381、BIP382、BIP383、BIP384、BIP385、BIP386 和 BIP389。

脑钱包

记住助记词不等于脑钱包。

脑钱包一般指的是人为挑选的一些有意义的词语,因此随机性比较差,相对不安全。

BIP39

BIP39 的具体过程如下:
image

  1. 产生随机熵
  2. 通过随机熵生成助记词(恢复码)
  3. 通过助记词生成根种子
  4. 通过 HMAC-SHA512 的哈希函数生成 512 位的结果:
    1. 左 256 位作为主密钥(并生成长度多一个字节的主公钥)
    2. 右 256 位,作为主 chain code,在后续派生子密钥中,作为确定性的随机熵

扩展密钥

扩展密钥:密钥 + chain code
“扩展”的意思也可以被理解为是未来可以扩展更多密钥。

扩展子密钥的过程可以递归的进行:
输入:

  • 父公钥
  • 父 chain code
  • 索引号

输出:

  • 子密钥(左 256 位)
  • 子chain code (右 256 位)

image

强化派生

使用私钥,而不是公钥来派生子密钥

image

具体的过程除了看书,还有不少资料可以参考,例如 BTC Study 上的 BIP32 拓展密钥图解

索引规则和路径

因为选不同的索引数字,会对应到不同的子密钥,而这一过程又是可以不断重复下去的,所以需要一套规则来识别出来。

索引数字的规则

  • 普通派生: 从 02311
  • 强化派生: 从 2312321,用 i' 的形式来表示

另外,主密钥中也跟随了密码学上的惯例:

  • m 表示从主私钥派生出来的私钥
  • M 表示从主公钥派生出来的公钥

因为子密钥可以无限、任意的生成下去,所以需要一套标准来统一设置路径:

  • BIP43:规定第一级表示“目的”(purpose)
  • BIP44:设置了 5 级的路径:

m / purpose' / coin_type' / account' / change / address_index

其中:

  • purpose':44'
  • coin_type':0' 主网、 1' 测试网
  • account':用户可以开始设置“账户”,强化地址
  • change:找零地址
  • address_index:每次使用的“账户地址”

6. Transactions

主要讨论了交易的序列化和结构、以及交易延展性问题。

版本

  • v2:涉及到一次共识规则的升级 - BIP68(对一个 opcode OP_CHECKSEQUENCEVERIFY 的含义做了修改,改为 RBF 和相对时间锁,见下文中的输入部份的序列号字段。)
  • v3:目前正在讨论。不涉及到共识规则的修改,只涉及到全节点转发策略

输入

序列号(sequence)字段:

  • 设计为交易的版本号。原本是预留给实现类似支付通道:大的序号覆盖小的序号的交易
  • 原本设想可以实现类似的闪电网络的效果(以最大的序号为最新交易状态,可以发布上链)
  • 但这样做会有问题:无法确保大的序号肯定会先于小序号得到确认(矿工激励、网络传输)以及也无法知道目前的序号是全局最大的

序列号:

  • sequence < 2321:允许 RBF
  • 另外,0 < sequence < 2311:有一个时间锁

输出

包括:

  • 金额
  • 输出脚本

UTXO 和 UTXO 集

有输出后,这笔交易未花费前就是一个 UTXO,需要保存

如果有了粉尘攻击或铭文之后, UTXO 集会很大。所以还有一种解决办法是 Utreexo,一般节点只保存一个 UTXO 集的承诺。但仍然需要某些节点保存全部 UTXO 数据。

另外,脚本中有一个特例是 OP_RETURN,不可花费(所以一般也都是 0 金额的),所以也就不用占用 UTXO 集。一般用来携带数据。

交易延展性

Malleability,也被翻译成 “熔融性”,在密码学上是指给定一个签名,对方/敌手可以修改这个签名(虽然不知道私钥),使其成为不同但仍然有效的一个签名。

但比特币这里的延展性也会涉及到包括:

  • 循环依赖:
    • Tx0 是 Alice 和 Bob 双方注资的一笔交易,但他们会先签另一个后续交易 Tx1 来确保可以退回各自的钱
    • 早期版本的交易格式是全部的字段,其中也有包含了签名的输入字段,都用来产生 Tx0id
    • Alice 和 Bob 没办法构造 Tx1,因为需要使用到 Tx0id
    • 但如果 Tx0id 双方都签署了,其中一方在签退款交易 Tx1 之前就可以直接广播上链了,也就其实没有起到作用
  • 第三方延展性问题:
    • 一个不牵扯到的第三方修改了签名
    • 书里的例子是使用一些替代性方法但可以实现相同的脚本效果(例如:OP_2OP_PUSH1 0x002 等等),从而与原交易产生冲突
  • 第二方延展性问题:
    • BIP62 尽可能降低了第三方延展性问题,但还有第二方(对手方)延展性问题
    • 对方可以重新产生一个签名(选择不同的随机数即可。类似在不同拷贝的合同上签名,书写会略有不同),产生一个冲突同时 Txid 不一样的交易

在这篇 2014 年的文章 Bitcoin Transaction Malleability and MtGox 里也提到过延展性的问题。MtGox 声称攻击者就是用延展性攻击的办法耗尽了交易所资金。

但延展性也不完全是坏事,有些交易就是希望被其他人来扩展和修改,例如用 SIGHASH 来让其他人补完交易内容(代付手续、众筹)等。所以书里也强调,如果说是延展性攻击,可以用 “unwanted malleability”。

隔离见证

早在 2011 年,开始者就知道怎么解决上面提到的循环依赖、第二方、第三方延展性问题。思路也很直接:既然是因为会产生不同的 Txid,那就不要把输入脚本包含了在产生 Txid 的计算里。

输入脚本里的数据,一个更抽象的名字是 “见证”(witness):了解了某项知识才能解锁资金。所以上面这种办法也叫做是 “隔离见证”。

虽然思路很简单,但如何实现升级却需要仔细考虑:

  • 硬分叉:
    • 直接改共识规则(例如不计算输入脚本)
    • 简单粗暴(开发) + 简单粗暴(部署)
  • 软分叉:
    • 2015 年提出,采用向后兼容升级的方式
    • 基于 anyone-can-spend 输入脚本(问题:老节点上是否会有问题?任何都可花费?)
    • 空脚本输入,这样就不会影响到 txid 的计算
    • 区别在于:老节点允许(allow)空输入脚本、新节点要求(require)空输入脚本

这样锁定-解锁关系就有了以下的演变:

  • 白皮书:公钥 vs 签名
  • 最早实现:输出脚本 vs 输入脚本
  • 隔离见证:见证程序 vs 见证结构

Coinbase 交易

一类特殊的交易,就是 “挖矿” 的收益。

  • 输入,null txid(全 0)和最大的输出序号(0xffffffff)
  • 输出,金额是除了挖矿产出外,还包括所有交易的手续费(每笔交易的输入输出差)

另有一个特殊之处在于 “成熟度要求”(maturity rule):100 个区块确认后才可花费。

交易重量和 Vbyte

交易的手续费计费单位为:交易重量,或者是虚拟字节(Vbyte。4 个单位的虚拟字节等于一个实际的字节)

虽然书里没有过多展开,但这也和隔离见证联系比较紧密,因为隔离见证区域里的数据在计算上是有折扣。更多可以参考:见证数据的折扣:为什么有些字节比别的字节 “更轻”

7. Authorization and Authentication

授权与鉴权。这部分延续上一章的交易内容,重点介绍了脚本

无状态的验证

  • 一个值得注意的特点:脚本语言是无状态的。
  • 也就是脚本所有所需要的信息,都包含在了脚本自身以及执行它的交易里,而不需要保存其他先前状态或为以后保存状态。这点应该是另外一个和以太坊等计算模型的区别。

脚本构造

  • 最早是简单的连接把输入和输出链接在一起,然后执行
    • 但这样会有问题:2010 年的 “1 OP_RETURN” 的 bug
  • 目前是脚本分别执行,堆栈在两个执行中进行转移
    • 首先执行输入脚本
    • 如果没有问题,并且操作都执行完成后,将堆栈复制,然后执行输出脚本

脚本多签(以及其中的一个 “bug”)

现在就可以用脚本里的 OP_CHECKMULTISIG 来实现多签了,比如 2-3 的多签(当然还有更灵活、成本更低的无脚本方式)。

这里有一个有意思的事情是,Bitcoin 最早的开发者(就是指中本聪?),在实现 OP_CHECKMULTISIG 的时候,还需要堆栈里额外 pop 一个没有实际意义的哑元。可以是任何一种内容,但通常使用 OP_0

所以实际的全部脚本会类似于:

OP_0 <Sig B> <Sig C> 2 <Pubkey A> <Pubkey B> <Pubkey C> 3 OP_CHECKMULTISIG

书里也不清楚这种设计到底是一个 bug 还是一个(为未来升级预留的)feature,因此书里称其为 “oddity”。但因为已经是共识规则了,所以未来的 OP_CHECKMULTISIG 都必须沿用。这可能也算是软分叉升级的一种技术债?

P2SH

BIP13 提出的,将一个脚本哈希作为地址。

这其实也可以算作另一种账户抽象或智能合约账户的实现。

OP_RETURN 及应用

这里又提了一次 OP_RETURN。除了不可花费、不用包含在 UTXO 集中之外,还提到了一些应用.
例如:Proof of Existence 电子公证服务,使用固定的前缀 DOCPROOF

时间锁

包括绝对时间锁、相对时间锁

实现层面包括:交易层面、脚本 opcde 层面

  • 交易层面:sequence 字段
  • 脚本层面:OP_CLTV(绝对时间锁)、OP_CSV(相对时间锁)

P2WPKH、P2WSH

segwit v0 版本的见证程序可以包括:P2WPKH、P2WSH

区别是:

  • P2WPKH 是 20 字节
  • P2WPSH 是 32 字节

另外有一点,P2WPKH 必须从压缩公钥的哈希来构造出来。这应该也体现了对节约链上空间的极度追求?(因为花费的见证结构里要提供公钥)

nested P2WPKH、nested P2WSH

P2WPKH、P2WSH 同样都可以嵌入在一个 P2SH 地址中,成为 nested P2WPKH 和 nested P2WSH。

这样做的好处是,兼容原有 UTXO 输出(例如发送方没升级),同时享受到低手续费(例如接收方已经升级)。

书里的这个例子里就是:

  • 接收方 Bob 已经隔离见证升级了,所以用了一个对外看起来就是 P2SH,但实际是 nested 的地址,发送给没升级的 Alice
  • 这样 Bob 可以把见证结构都放在见证区域,以降低手续费。但同时 Alice 的钱包也不需要隔离见证的知识,只需要将输出脚本用 P2SH 形式写好就行

MAST

MAST 可以包含更多的程序内容或条件,而实现这种方式,只需要对各种花费条件做一个承诺,所以 MAST 仍然用到了 Merkle 树。这样做的好处:

  • 节约空间。只需要 Merkle 树根
  • 隐私。不需要揭露其他的分支,只需要真正花费的这条路径上的叶子(脚本)和路径(哈希)

除了增加复杂度之外,BIP114、BIP116 提议的这种方式没其他缺点。但后面有了更好的实现——Taproot。

另外,“MAST” 其实有两个含义:

  • merklized abstract syntax tree
    • 和编译原理里的类似,把一个程序的数据和操作形成一个树
    • 需要至少揭示 1 个 32 字节的程序摘要,所以空间利用率不太高
  • merklized alternative script tree
    • 目前所说的大部分都是指这个!
    • 是一组脚本,而非一个

P2C

Pay to Contract,也是 BIP32 里提到的派生密钥的一种使用方法。作用:

  • 实现为某个事项而付款(用地址来表明,而不是公开可知道的 “memo”)
  • 隐私性(双方在没有争议的时候可以不揭露),因为是一个派生出来的新地址

书中的例子:因为某个事项,Bob 付款给 Alice

  • Bob 将这个事项变成哈希,进而转换成一个数字
  • Tweak:Bob 把这个数字加到 Alice 的公钥上,形成了一个新的公钥
  • 对应这个公钥的私钥,只有 Alice 可用他的私钥来生成

这样:

  • 当有争议时,Bob 可以揭示他写的信息(描述),第三方都可以比对哈希,证明 Bob 确实是为该款项付款的
  • 如果没有争议,Alice 和 Bob 都可以不揭示这个描述,这样就可以其他付款没有区别

不过目前更多使用的 P2C 是在 Taproot 里的稍微不太一样的形式。

无脚本的多签(门限签名)

  • 生成
    • 参与多签的各方独立生成部分私钥
    • 各方通过这个部分私钥,独立生成部分公钥(以上步骤和普通公私钥生成类似)
    • 然后各方共享这个公钥,生成一个 “无脚本多签” 的公钥
  • 花费(签名)
    • 各方独立生成一个部分签名
    • 然后再整合成一个签名

比脚本方式:

  • 隐私性更好,因为没在脚本里
  • 体积固定
  • 但需要更多轮的交互
  • 没有抗抵赖性(例如 2-3 多签中的一个人 Carol 声称自己没参与签名、是 Alice 和 Bob 签的,但他也没法证明)

Taproot

和 P2C 略有区别,tweak 用的哈希是对一个程序代码的承诺。承诺的方式是对 一个 MAST 的树根

  • keypath 花费:
    • 只用到单签,或无脚本多签
    • 最终要上链的包括:
      • witness program:公钥
      • witness structure:签名
  • keyscript 花费:
    • 使用到一个脚本树
    • 最终要上链的包括:
      • witness program:公钥
      • witness structure:版本号、潜在要用到的密钥(称为:taproot internal key)、执行脚本(称为: leaf script)、从树根到叶子路径上的每个树分叉处的哈希、其他脚本需要的数据(例如签名、哈希原像等)

Tapscript

Taproot 使用的和比特币脚本里的略微有些差别,因此被称为是 tapscript。主要区别包括:

  • 去掉了 OP_CHECKMULTISIGOP_CHECKMULTISIGVERIFY,因为和 schnorr 签名批量验证的方式没法很好的结合;对应的,增加了 OP_CHECKSIGADD,成功验证一个签名就加一,用来数有多少个签名通过校验了
  • 所有在 tapscript 里的签名都是用 schnorr 签名(BIP340)
  • OP_SUCCESSx opcode,重新定义了一些之前的 opcode,并认为是执行结果成功(这样以后还可以通过软分叉方式将其定义为在某些条件下失败。但如果反过来则很难,需要硬分叉)

8. Digital Signatures

格式与流程

ECDSA:使用 DER 格式
schnorr:使用一种更简单的序列化格式

虽然算法不一样,但比特币里都遵循相同的签名流程:
Sig=Fsig(Fhash(m),x)
其中:

  • x 私钥
  • m 被签名的消息
  • Fhash 哈希函数
  • Fsig 签名函数
  • Sig 签名结果

SIGHASH

签名可以只签交易内容的一部份

基本类型

SIGHASH flag 描述
ALL 0x01 签名适用于所有的输入和输出
NONE 0x02 签名适用于所有的输入,不适用于任何输出
SINGLE 0x03 签名适用于所有的输入,只适用于和这个输入对应的(即相同 index)的输出

修饰的类型

基本类型可以被 SIGHASH_ANYONECANPAY 所修饰,产生 3 种新类型

SIGHASH flag Value Description
ALL|ANYONECANPAY 0x81 签名适用于一个输入和所有的输出
NONE|ANYONECANPAY 0x82 签名适用于一个输入,不适用于输出
SINGLE|ANYONECANPAY 0x83 签名适用于一个输入和对应的(即相同 index)的输出

第二版里其中有一个图,显示的更直观。不确定为什么在第三版里删除了。
image

SIGHASH 的用途

  • ALL|ANYONECANPAY:众筹类的交易。输出已经确定,但输入可以被多个人所添加知道满足输出的金额
  • NONE:空白支票,任何人都可以在输出部分写入自己的地址来获得这笔资金
  • NONE|ANYONECANPAY:粉尘回收器。铭文其实就可以基于这个 SIGHASH 来回收掉

BIP118

还有一些提案在准备扩充 SIGHASH,例如 BIP118 想增加的:

  • SIGHASH_ANYPREVOUT
    • 不为输入字段的 outpoints 签名,而是允许之前任何一个符合特定 witness program 的输出
    • 例如,Alice 收到两笔输出,有同样的金额和同样的 witness program
    • Alice 用 SIGHASH_ANYPREVOUT 给其中一笔的签名,也可以被复制并用在另外一笔花费到相同地址的交易上
  • SIGHASH_ANYPREVOUTANYSCRIPT
    • 不限定 outpoings、金额和 witness program,也就是允许之前任何一笔输出
    • 例如,Alice 收到两笔输出,有着不一样的金额和不一样的 witness program
    • Alice 用 SIGHASH_ANYPREVOUTANYSCRIPT 给其中一笔的签名,也可以被复制并用在另外一笔花费到相同地址的交易上

Schnorr 签名

Schnorr 签名的特点:

  • 可证明的安全性
  • 线性可加
  • 批量验证

与 Schnorr 签名对应的还有一个概念是 Schnorr 身份协议。本书里提供了一个简化后的例子。

Schnorr 其实和 ZKP 也有关系。

Schnorr 身份识别协议也算一种交互式的 ZKP:与挑战者交互(承诺、挑战、证明)后,证明者可以证明出来他知道一些见证。

Schnorr 签名是用 Fiat-Shamir 转换把 Schnorr 身份识别协议变成非交互式的。办法是:

  • 用随机的哈希函数输出模拟挑战者的验证
  • 而进一步的,可以哈希的原像可以使用待签名消息(与公开 nonce kG 连接在一起,c=hash(kG||m)

这样就有了 1)可以证明知道某个见证 2)与这个证据相关的、对消息的承诺。

比特币上的 Schnorr 签名

比特币使用的是 BIP340——secp256k1 的 Schnorr 签名

因为有非强化的密钥派生(用公钥来派生),所以可以在公钥签过名的消息上,也增加用来派生的消息,然后产生一个可以通过验证的新签名。所以 BIP340 需要也对公钥进行承诺,也就是 c=hash(kG||xG||m)

基于 Schnorr 的多签

因为线性可加,多签产生非常简单。聚合公钥 xG=yG+zG

但还可能会有一些问题,例如密钥抵消攻击(key cancellation attack)。

  • Alice 生成了 yG 给 Bob
  • Bob 这次也生成 zG,但参与聚合签名的其实是 zGyG
  • 这样,聚合签名就成了 yG+zGyG,Alice 的密钥被抵消(cancel out)。也就是 Bob 自己掌握的 zG

有很多办法解决,例如最简单的方案是各方在共享公钥前,需要先对公钥进行承诺(但这样也在非强化的密钥派生情况下会失效)。除此之外,还有对 nonce 的攻击等等。

所以,为了安全的多签,需要建立一些协议。一些常用的协议包括:

  • Musig:需要 3 轮通信,但优势在于比较简单
  • Musig2:只需要 2 轮通信。对绝大多数应用来说,Musig2 是目前的最优选择
  • Musig-DN:DN 的意思是 “确定性 nonce”。实施起来比较复杂

签名中的随机数

签名用到的 k 值有讲究:

  • 只能使用一次:用相同的 k 给不同的消息签名,会泄露私钥!因此,k 有时也被称为 nonce
  • 必须保密:k 有时也被称为临时私钥
  • 必须随机:如果 nonce 的分布不均匀、可预测,仍然有攻击方法

所以可以看到,以太坊的 nonce 和密码学上的 nonce 其实不算同一个概念(待确认)。

为了避免重复:

  • 对于 ECDSA,业界普遍使用 RFC6979。方案是基于消息和私钥生成一个 nonce,对于这样相同的消息就只会确定性的选用同样的 nonce。
  • 对于 Schnorr,有 BIP340,也能实现这种确定性的 nonce

一个验签的问题

因为验签需要做大量哈希运算,哈希运算数量和签名数量成平方关系,在以前的版本里,攻击者可以构造出一个有大量签名的交易,来 DoS 比特币网络。

隔离见证在 BIP143 中使用了一种改进后的承诺哈希算法,哈希运算数量和签名数量成线性关系

9. Transaction Fees

介绍了手续费的来源(输出 - 输入),手续费的单位等。

另外也介绍了 RBF、CPFP 两种追加手续费的方案,以及包中继等。

某种程度上也可以认为是一种递进关系?

  • RBF,比较 mempool 里的单笔交易替换手续费大小
  • CPFP,比较关联的交易总手续费大小
  • 包中继,比较中继过来的一 “包”(package)的交易总手续费

在闪电网络中,CPFP 可能会更有用。因为在不合作的关闭通道时,没办法新签名一笔交易来 RBF 了,只能使用 CPFP;但如果父交易都没办法进入 mempool,则只能依靠包中继了。

下面,重点在于这些手续费引发的一些问题以及如何解决的。

交易钉死攻击

Transaction Pinning,攻击者有时候可以阻止受害者或让他很难追加手续费。

  • RBF:
    • Alice 给 Bob(攻击者)发了一笔小体积(100 vbyte)的交易
    • 但 Bob 用这个输出生成了一笔大体积的交易(例如 100,000 vbyte)
    • 这时候,如果 Alice 想替换她最初的交易,就得付出更多的费用(原来的 1000 倍)
  • CPFP:
    • 交易的关联太多。替换其中一笔,可能会消耗节点过多的资源
    • 攻击者给原交易附加 24 个子交易或者触发体积限制(参考:什么是 “锚点输出”?

CPFP carve out 和锚点输出

为解决上面的问题,开发者提出了一个例外规则:CPFP carve out,允许增加第 25 个交易(体积不超过 1,000 vbyte)

目前大多数的闪电网络实现都通过锚点输出的交易模板来支持 carve out。

锚点输出在书里没有展开。还是可以参考上面的文章:什么是 “锚点输出”?。锚点输出是一笔承诺交易的两个输出(各属于通道的其中一方),这样任何一方都可以安全地为承诺交易追加手续费,而不必担心交易被对方钉死。

另外还有一些解决方案在讨论中,例如临时锚点(ephemeral anchors)等。

其他问题

手续费狙击(fee sniping)

是一种偏理论的攻击。矿工试图重写历史区块,从而打包未来更多的高费率交易,让自己受益。

替代循环攻击

除了书里介绍的问题外,还有例如今年(2023)10 月份披露出来的对闪电网络的 “替代循环攻击”。具体可以参考 mononautical 的解释我之前的记录

10. The Bitcoin Network

网络这章主要将比特币的网络架构、数据包传输、节点间的请求等。

首先提了一个很有意思的观察(注意下面这句的大小写):目前的互联网架构(internet architecture) 是更层级化的,但互联网协议(Internet Protocol) 仍然是扁平化的。

下面讲解了致密区块(compact block)以及 FIBRE。

轻客户端

有一个注意的:轻客户端可以验证存在一笔交易,但无法确定是否有双花,因为它没有全部的交易信息

Bloom 过滤器

为了降低轻客户端与全节点之间的通讯,引入了 Bloom 过滤器。

轻客户端送到 bloom 过滤器里的包括:交易 ID、每个交易输出的数据部分、每个交易的输入、签名。

但问题在于缺乏隐私。全节点可以根据每次请求的 bloom 过滤器分析出来地址之间的关联。Bitcoin Core 已经将 bloom 过滤器仅限于节点的白名单地址中。

致密区块过滤器

虽然名字都有 “致密”(compact),但是和致密区块没关系。

由服务器(节点)产生过滤器,而不是客户端(钱包、轻客户端)发送请求过去,因此更加隐私。另外:

  • GCS(Golomb-Rice 编码集):
    • 数据集在 bloom 过滤器基础上,改用了 GCS
    • 思想是将数据列表排序,并计算他们的差值(因为差值一般容易重复,这样方便压缩数据)
  • 从多个节点下载区块过滤器
    • 避免一个节点欺骗
    • 从多个节点获取相同的过滤器

11. Blockchain

区块头和区块高度

2 种区块标识符:区块头哈希、区块高度

区块哈希:

  • 32 字节的区块哈希,更准确的名称是 “区块头哈希”,因为只用到了区块头。
  • 另外,区块哈希并没有实际包含在区块数据结构中。而是由节点从网络获取区块后,自行计算出来

区块高度:

  • 区块高度一开始并没有包含在区块数据中。由节点自行识别。
  • BIP34 后,开始将区块高度包含在 coinbase 交易中。

然后本章讲解了 merkle 树。

测试网

  • Testnet
    • 最老的测试网
    • 和主网几乎一样
    • 目前是 testnet3
    • 但网络可能不稳定(因为矿工激励等)
  • Signet
    • PoA 的测试网
    • 建议所有应用软件的开发在 signet 上(挖矿软件在 testnet3 上)
    • 有一个默认的 signet
    • 但也允许自己跑一个定制化的 signet(基于 BIP325)
  • Regtest
    • 本地测试网
    • “reg” 的意思是 “regression”,回归测试

12. Mining and Consensus

2023 年的 Coinbase 交易一般有 2 个输出:

  • 0 金额的输出:用 OP_RETURN 承诺到所有隔离见证交易的见证数据
  • 矿工自己收取的奖励

时间:MTP

MTP:Median Time Past

有两个重要且有区别的时间概念:实际时间(wall time)、共识时间(consensus time)。

由于分布式系统很难同步时间,另外矿工有可能因为经济利益而对时间撒谎(把时间往未来调,这样来操控降低挖矿难度)。因此有两个共识规则:

  • 节点不能接收未来 2 小时以外的区块(问题:如果 2 个小时内没挖出来块怎么办?)
  • 节点不能接收时间早于过去 11 个区块时间的中位数(即 MTP)的区块

共识时间(MTP)通常比实际时间晚 1 个小时(11 个区块的中位数)。

时间锁

BIP68 相对时间锁激活的时候,也对锁相关的 “时间” 计算有了改变。

矿工会尽可能使用最晚的时间(因为可以打包更多时间锁的交易)。为了进一步确保安全,MTP 目前(BIP113 之后)是共识时间,用来计算各种时间锁。因为中位数更难被操控。

区块时间间隔

书里有一点值得注意:更短的区块间隔,并不意味着更早的结算。

因为这只意味着接收人愿意承担更弱的安全保证。

所以目前大多数人仍然倾向于比特币的 10 分钟区块间隔。

矿池

介绍了一些矿池的技巧:

  • 矿池一般会设置更低难度,一般比主网低 1000 倍。这样方便统计各参与者的实际算力情况(能贡献多少 share)。
  • 每个矿池参与者会被分配一个略微不一样的 coinbase 交易模板。这样他们就可以在不同的区块头上计算,避免重复

矿池管理协议

Stratum:

  • v1
    • 矿池服务器构造一个候选区块:打包交易、添加 coinbase 交易、计算 merkle 根、指向前一个区块哈希
    • 然后把这个备选区块送给参与者作为模板
  • v2
    • 允许矿池参与者自行选择哪些交易打包到他们的区块里(基于他们自己的全节点)

share chain:

  • P2Pool 把矿池服务器去中心化了,实现了一个类似区块链的并行系统
  • share chain 是一个难度比比特币低的区块链
  • 当一个 share chain 上的区块也达到比特币的难度时,就会打包到比特币主链上,并按照 share 给参与者分配

算力攻击

这里有一个值得注意的:双花攻击只能用在攻击者自己的交易商,因为只有攻击者自己才能产生合法的签名。

共识规则的升级

和很多传统系统甚至大多数区块链网络不一样的地方。

硬分叉一般包括 4 个阶段:软件分叉、网络分叉、挖矿算力分叉、链分叉

软分叉其实不算一个分叉,因为是对共识规则的向前兼容(?)修改,运行未升级的客户端继续在新的规则下运行。有很多办法可以实现,如利用 NOP 操作符(已有两次软分叉升级利用了 NOP)。

但软分叉也不是百利而无一害的,会引入技术债,而且实际上是无法回退的(因为算是引入了新的约束条件。如果回退,则使用新规则下的交易资金会有损失)。

另外,就算是软分叉升级,方式也是多种多样的。历史上采用了多种形式。

激活方式:BIP34

BIP34 本身是定义了区块头比较包含区块高度(见之前笔记里的 “区块头和区块高度”)。它采用了一个两阶段的激活机制,基于 1,000 个区块的滑动窗口期。如果准备好了,矿工将他们的区块版本从 1 改为 2

  • 75%。如果滑动窗口期内的 75% 的区块标记了 2,那么 BIP34 规定的 “区块头比较包含区块高度” 的规则必须开始在标 2 的区块中包含;但 1 版本的区块仍然被接受
  • 95%。如果滑动窗口期内的 95% 的区块标记了 21 版本的区块则不再被接受。所有区块都必须符合新的共识规则

后来还有两次升级使用了 BIP34 的方式:

  • BIP66:区块版本是 3
  • BIP65:区块版本是 4

但这几次软分叉升级后, BIP34 采用的这种方式就被替代掉了。因为有些缺点:

  • 同时只能有一个软分叉升级进行
  • 因为版本号是递增的,所以没办法直观的看到提案被拒绝了、然后再提一个新的

激活方式:BIP9

  • 将整数位变为 bit 位。这样还能使用剩下的 32 - 3 = 29 bit 作为同时进行的 29 个提案
  • 设置了最长的时间。如果超时,那么就认为是不通过了
  • 过期后,升级的 bit 位可以重新利用

流程是:

  • 窗口期设置为固定的 2,016 个区块,即一个难度调整周期内
  • 如果发出升级信号的区块超过了 95%,则在下一个难度调整周期后激活

但后来也发现了一些问题:

  • 在 segwit 中,一些矿工迟迟不表态,因为可能会影响他们秘密采用的挖矿方式
  • 后来大家发现,矿工可能会阻碍一些提案,只要长时间不表态就行

激活方式:BIP8

和 BIP9 基本相同,只增加了一个 MUST_SIGNAL 阶段,矿工必须表态。

一些人认为就是因为要表态会使用 BIP8,taproot 升级才能最终成功激活。但这也无法证明或证伪,以及看出来 BIP8 到底在这次升级中贡献了多少。

快速试错

快速试错(speedy trial):“fail fast or succeed eventually”。

最后为了激活 taproot,介于 BIP9 和 BIP8 的一种方式被提议使用,一种修改版的 BIP9:

  • 给矿工一个非常短的时间来表态
  • 如果表态不成功,则会考虑用其他方式激活
  • 如果表态成功,则在 6 个月之后的特定区块才会激活

不过未来的升级中,还不太确定会使用哪种方式,例如是否还会再用快速试错。

考虑到最近的 BIP119 等提议,估计届时又会有很多的关于激活方式的讨论。

但本章最后有一句话很有深意:

The one constant characteristic of consensus software development is that change is difficult and consensus forces compromise.

13. Bitcoin Security

安全这章感觉写的其实一般。不如直接参考曾汨老师的比特币多签指南等安全指导文章。

14. Second-Layer Applications

染色币

这部分也包括了 RGB、Taproot Assets 等,概念还是包括一次性封条、P2C、客户端验证

Taproot Assets

和 RGB 的区别:

  • 使用的 P2C 的形式,和 taproot 里的 MAST 很类似
  • Taproot Assets 设计为与闪电网络兼容(毕竟都是 Lightning Labs)。资产通过闪电网络转发,有原生转发(native forwarding,每一跳都需要知道染色币的含义)、转义转发(translated forwarding,发送者的邻居和接收者的邻居能转义)
    • RGB 和 Taproot Assets 从技术上对两种都支持
    • 但 Taproots Assets 主要在做转义的转发
    • RGB 想两种都支持

闪电网络

介绍了很多,不过又是一个大的 rabbit hole。也可以参考我之前写的闪电网络的学习记录

Appendix A. The Bitcoin Whitepaper by Satoshi Nakamoto

附录:白皮书

Appendix B. Errata to the Bitcoin Whitepaper

很有意思的是,紧急着白皮书就是对白皮书的 “勘误”。

虽然不能完全算是勘误,因为不全是错误,大部分都是因为技术迭代、有了新的研究发现或术语定义发生了变化等,白皮书里的内容已经不符合现在的实际情况了。这一点我和阿剑老师在 Hash Out 42 Podcast 上也会提到。

因此,学习比特币真的不能把白皮书看成那么重要的材料,尽管仍然是一份很好的、很 high-level 的入门概述材料。

Appendix C. Bitcoin Improvement Proposals

BIP1 里定义了三种 BIP 提案:

  • 标准型 BIP:会影响到大部分或所有节点实现的。例如对网络的修改等
  • 信息型 BIP:描述一个比特币设计问题、概要指导或提供一些信息
  • 过程型 BIP:描述一项比特币过程或提议对一个过程进行修改。类似标准型 BIP,但是对协议以外的内容进行描述