# 《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 章开始,内容逐渐进入正题。 ### 公私钥 首先是公私钥。 私钥的本质是一个随机数,当然是在非常大范围内随机地选择出来一个。这个范围是 $2^{256}$。 程序具体的实现过程通常是用一个可信的随机源产生一个比 256 位更大一些的字符串,然后放入到安全的 SHA256 哈希函数中,产生的这个 256 位的数据可以被认为是一个随机数。 之后还介绍了具体的一些椭圆曲线算法。 ### 压缩公钥 最早的公钥是 65 字节的,但因为公钥是在 $y^2 \; mod \; p = (x^3 + 7) mod \; p$ 曲线上的点 $(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 字节 | ### 私钥的不同形式(包括压缩私钥) 和压缩公钥类似,也有压缩私钥的概念。 但私钥本身是一个 $2^{256}$ 的随机数,所以没法再压缩了。意思只是,从压缩私钥只应导出压缩公钥。 而且压缩私钥反而长度会比普通私钥(WIF,Wallet Import Format)多 1 个字节,用来表示压缩公钥的 `y` 值正负。 私钥的不同格式: | 格式 | 固定前缀 | 格式 | | -------------- | -------- | ----------------------------- | | 16 进制 | 无固定 | 64 位长度的 16 进制的数字 | | WIF | 5 | Base58 | | WIF-compressed | K 或 L | 增加 0x01 这么 1 个字节的后缀 | ## 5. Wallet Recovery 尽管理论上有了私钥就可以推导出公钥了,但为了使用上的方便和隐私,钱包需要使用分层确定钱包等方式来维护多个公私钥对。因此,钱包采用什么方式来恢复就成了一个很重要的技术。 ### 早期方式 为了能有多个地址,早期的钱包需要把所有使用到的私钥保存下来。这么做需要保存的量比较大。 ### 确定性密钥生成及 BIP32 确定性生成更多密钥,在主密钥上加上一个数字,生成一个新的密钥。这项技术被称为 _key tweak_: $$\begin{aligned} K&=k\times G \\ K + (123 \times G) &= (k+123)\times G \end{aligned}$$ 而具体怎么加这个数字,可以采用树形的结构,逐层来添加,并且层级没有限制。因此,可以无限生成(但恢复时也就无法全部遍历,因此备份路径也就很重要了,后文会提到)。 ### 种子、助记词与口令短语 有比较流行的几种对随机种子生成助记词(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 的多签,不如使用其他更安全的多签方案(参考曾汨老师在亿聪哲史播客里的[介绍](https://open.spotify.com/episode/0TC7Ou2B3cOWYrWucUeQeK)) ### 非密钥数据备份 除了密钥数据外,钱包可能还要备份非密钥数据,例如交易备注。这样方便在不同钱包间迁移数据,以及用于会计软件等。 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' | 2. 显式路径 好处是可以具体说明花费条件。 目前使用显式路径的,基本都采用了“输出脚本描述符”的方式,描述了脚本和配合使用的密钥(或密钥路径)。目前的标准包括:BIP380、BIP381、BIP382、BIP383、BIP384、BIP385、BIP386 和 BIP389。 ### 脑钱包 记住助记词不等于脑钱包。 脑钱包一般指的是人为挑选的一些有意义的词语,因此随机性比较差,相对不安全。 ### BIP39 BIP39 的具体过程如下: ![image](https://hackmd.io/_uploads/rykYDNlvT.png) 1. 产生随机熵 2. 通过随机熵生成助记词(恢复码) 3. 通过助记词生成根种子 4. 通过 HMAC-SHA512 的哈希函数生成 512 位的结果: 1. 左 256 位作为主密钥(并生成长度多一个字节的主公钥) 2. 右 256 位,作为主 chain code,在后续派生子密钥中,作为确定性的随机熵 ### 扩展密钥 扩展密钥:密钥 + chain code “扩展”的意思也可以被理解为是未来可以扩展更多密钥。 扩展子密钥的过程可以递归的进行: 输入: - 父公钥 - 父 chain code - 索引号 输出: - 子密钥(左 256 位) - 子chain code (右 256 位) ![image](https://hackmd.io/_uploads/rJSBt4xPa.png) ### 强化派生 使用私钥,而不是公钥来派生子密钥 ![image](https://hackmd.io/_uploads/Bk9rkGdDp.png) 具体的过程除了看书,还有不少资料可以参考,例如 BTC Study 上的 [BIP32 拓展密钥图解](https://www.btcstudy.org/2023/10/09/bip-32-extended-keys-diagram/) ### 索引规则和路径 因为选不同的索引数字,会对应到不同的子密钥,而这一过程又是可以不断重复下去的,所以需要一套规则来识别出来。 #### 索引数字的规则 - 普通派生: 从 $0$ 到 $2^{31} – 1$ - 强化派生: 从 $2^{31}$ 到 $2^{32} – 1$,用 `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 < $2^{32} -1$:允许 RBF - 另外,0 < sequence < $2^{31} -1$:有一个时间锁 ### 输出 包括: - 金额 - 输出脚本 ### UTXO 和 UTXO 集 有输出后,这笔交易未花费前就是一个 UTXO,需要保存 如果有了粉尘攻击或铭文之后, UTXO 集会很大。所以还有一种解决办法是 Utreexo,一般节点只保存一个 UTXO 集的承诺。但仍然需要某些节点保存全部 UTXO 数据。 另外,脚本中有一个特例是 OP_RETURN,不可花费(所以一般也都是 0 金额的),所以也就不用占用 UTXO 集。一般用来携带数据。 ### 交易延展性 Malleability,也被翻译成 “熔融性”,在密码学上是指给定一个签名,对方/敌手可以修改这个签名(虽然不知道私钥),使其成为不同但仍然有效的一个签名。 但比特币这里的延展性也会涉及到包括: - 循环依赖: - $Tx_0$ 是 Alice 和 Bob 双方注资的一笔交易,但他们会先签另一个后续交易 $Tx_1$ 来确保可以退回各自的钱 - 早期版本的交易格式是全部的字段,其中也有包含了签名的输入字段,都用来产生 $Tx_0id$ - Alice 和 Bob 没办法构造 $Tx_1$,因为需要使用到 $Tx_0id$, - 但如果 $Tx_0id$ 双方都签署了,其中一方在签退款交易 $Tx_1$ 之前就可以直接广播上链了,也就其实没有起到作用 - 第三方延展性问题: - 一个不牵扯到的第三方修改了签名 - 书里的例子是使用一些替代性方法但可以实现相同的脚本效果(例如:`OP_2`、`OP_PUSH1 0x002` 等等),从而与原交易产生冲突 - 第二方延展性问题: - BIP62 尽可能降低了第三方延展性问题,但还有第二方(对手方)延展性问题 - 对方可以重新产生一个签名(选择不同的随机数即可。类似在不同拷贝的合同上签名,书写会略有不同),产生一个冲突同时 $Txid$ 不一样的交易 在这篇 2014 年的文章 [Bitcoin Transaction Malleability and MtGox](https://link.springer.com/chapter/10.1007/978-3-319-11212-1_18) 里也提到过延展性的问题。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 个单位的虚拟字节等于一个实际的字节) 虽然书里没有过多展开,但这也和隔离见证联系比较紧密,因为隔离见证区域里的数据在计算上是有折扣。更多可以参考:[见证数据的折扣:为什么有些字节比别的字节 “更轻”](https://www.btcstudy.org/2023/12/23/the-witness-discount-why-some-bytes-are-cheaper-than-others/)。 ## 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](https://proofofexistence.com/) 电子公证服务,使用固定的前缀 `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_CHECKMULTISIG` 和 `OP_CHECKMULTISIGVERIFY`,因为和 schnorr 签名批量验证的方式没法很好的结合;对应的,增加了 `OP_CHECKSIGADD`,成功验证一个签名就加一,用来数有多少个签名通过校验了 - 所有在 tapscript 里的签名都是用 schnorr 签名(BIP340) - OP_SUCCESSx opcode,重新定义了一些之前的 opcode,并认为是执行结果成功(这样以后还可以通过软分叉方式将其定义为在某些条件下失败。但如果反过来则很难,需要硬分叉) ## 8. Digital Signatures ### 格式与流程 ECDSA:使用 DER 格式 schnorr:使用一种更简单的序列化格式 虽然算法不一样,但比特币里都遵循相同的签名流程: $$Sig = F_{sig}(F_{hash}(m),x)$$ 其中: - $x$ 私钥 - $m$ 被签名的消息 - $F_{hash}$ 哈希函数 - $F_{sig}$ 签名函数 - $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](https://hackmd.io/_uploads/SyeJx-iPp.png) #### 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$,但参与聚合签名的其实是 $zG-yG$ - 这样,聚合签名就成了 $yG+zG-yG$,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 个子交易或者触发体积限制(参考:[什么是 “锚点输出”?](https://www.btcstudy.org/2022/08/08/anchor-outputs/) ### CPFP carve out 和锚点输出 为解决上面的问题,开发者提出了一个例外规则:CPFP carve out,允许增加第 25 个交易(体积不超过 1,000 vbyte) 目前大多数的闪电网络实现都通过锚点输出的交易模板来支持 carve out。 锚点输出在书里没有展开。还是可以参考上面的文章:[什么是 “锚点输出”?](https://www.btcstudy.org/2022/08/08/anchor-outputs/)。锚点输出是一笔承诺交易的两个输出(各属于通道的其中一方),这样任何一方都可以安全地为承诺交易追加手续费,而不必担心交易被对方钉死。 另外还有一些解决方案在讨论中,例如临时锚点(ephemeral anchors)等。 ### 其他问题 #### 手续费狙击(fee sniping) 是一种偏理论的攻击。矿工试图重写历史区块,从而打包未来更多的高费率交易,让自己受益。 #### 替代循环攻击 除了书里介绍的问题外,还有例如今年(2023)10 月份披露出来的对闪电网络的 “替代循环攻击”。具体可以参考 [mononautical 的解释](https://twitter.com/mononautical/status/1715736832950825224)和[我之前的记录](https://twitter.com/hu_zhiwei/status/1716342376342684036)。 ## 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% 的区块标记了 `2`,`1` 版本的区块则不再被接受。所有区块都必须符合新的共识规则 后来还有两次升级使用了 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 安全这章感觉写的其实一般。不如直接参考曾汨老师的[比特币多签指南](https://www.btcstudy.org/2022/12/09/a-guide-for-bitcoin-multi-sig-wallets-by-mi-zeng/)等安全指导文章。 ## 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。也可以参考我之前写的[闪电网络的学习记录](https://mirror.xyz/huzhiwei.eth/J2Kv1ATWo0_d3ZidG-8BDrIJcYX7DFew_ruQ2p03sgU)。 ## Appendix A. The Bitcoin Whitepaper by Satoshi Nakamoto 附录:白皮书 ## Appendix B. Errata to the Bitcoin Whitepaper 很有意思的是,紧急着白皮书就是对白皮书的 “勘误”。 虽然不能完全算是勘误,因为不全是错误,大部分都是因为技术迭代、有了新的研究发现或术语定义发生了变化等,白皮书里的内容已经不符合现在的实际情况了。这一点我和阿剑老师在 [Hash Out 42 Podcast](https://open.spotify.com/show/2gry8EwViUfEgV9xhskKIs) 上也会提到。 因此,学习比特币真的不能把白皮书看成那么重要的材料,尽管仍然是一份很好的、很 high-level 的入门概述材料。 ## Appendix C. Bitcoin Improvement Proposals BIP1 里定义了三种 BIP 提案: - 标准型 BIP:会影响到大部分或所有节点实现的。例如对网络的修改等 - 信息型 BIP:描述一个比特币设计问题、概要指导或提供一些信息 - 过程型 BIP:描述一项比特币过程或提议对一个过程进行修改。类似标准型 BIP,但是对协议以外的内容进行描述