3.2 Segwit和Taproot

1. P2WPKH:数据结构的革新

到2017年,比特币面临两个重要问题:交易延展性(交易ID可被修改)和区块空间有限。这个我们晚点再讲。隔离见证(SegWit)升级通过改变交易数据结构解决了这些问题,引入了P2WPKH(Pay to Witness Public Key Hash)。

锁定脚本

OP_0 <公钥哈希>

这个简化的脚本是P2WPKH的标志。它不再包含完整的验证逻辑,而是告诉节点:"这是一个版本0的隔离见证输出,验证数据在witness字段中。"

解锁

在SegWit中,解锁脚本被移到一个全新的交易部分 - 见证(witness)数据结构中:

<签名> <公钥>

大家可以发现,Segwit把之前sigscript的部分放到了一个独立的空间中了。

验证过程

验证逻辑与P2PKH相同,但执行环境不同:

  1. 验证节点识别这是SegWit输出

  2. 从见证数据获取签名和公钥

  3. 验证公钥哈希与脚本中的哈希匹配

  4. 验证签名有效性

P2WPKH地址以"bc1q"开头,如:bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4

P2WPKH的意义

P2WPKH代表了比特币的结构性创新:

  1. 数据隔离:将签名数据(见证数据)从交易主体移出

  2. 解决延展性:签名不再影响交易ID的计算

  3. 区块容量扩展:见证数据在计算区块大小时享有折扣

  4. 向后兼容:旧节点仍然可以验证交易(忽略见证数据)

这是比特币在保持核心原则不变的情况下,通过数据结构改变实现了重大升级。

P2TR:综合创新的巅峰

锁定脚本

OP_1 <32字节输出密钥>

这个简单脚本隐藏了极大的复杂性 - 输出密钥可能是一个简单公钥,也可能包含了一整棵脚本默克尔树的根哈希。

花费方式

P2TR提供了两种截然不同的花费路径:

1. 密钥路径花费(简单情况):

<Schnorr签名>

为何这么简单,某种签名+某种密钥就能开锁吗?这看起来很简单,但有一个重要细节:输出密钥本身是"调整后"的。它不是普通公钥,而是内部密钥(internal key)与脚本树根(merkle root)的组合。具体来说,输出密钥 = 内部密钥 + SHA256(内部密钥 || 脚本树根) * G(其中G是椭圆曲线的生成点)。看不懂直接忽略。只需要理解为,解锁的第一种选择,用签名可以解锁。

2. 脚本路径花费(复杂条件):

<控制块> <脚本> <脚本输入...>

脚本路径的主要创新在于证明提供的脚本确实是承诺树的一部分。随后还需要像传统脚本一样执行该脚本并验证提供的输入数据(如签名)。

控制块包含完整的内部密钥和默克尔证明,脚本是要执行的具体脚本(如多签脚本),脚本输入是满足脚本条件的数据(如签名)。

验证过程

  1. 检查是否提供了签名(密钥路径)或控制块(脚本路径)

  2. 对于密钥路径:直接验证Schnorr签名

  3. 对于脚本路径:验证控制块,执行脚本

P2TR地址以"bc1p"开头,如:bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqzk5jj0

P2TR的综合创新

P2TR融合了前几代脚本的所有优点,同时添加了新的改进:

  1. 从P2PKH借鉴:基本的密钥验证概念

  2. 从P2SH借鉴:将复杂脚本编码为哈希(更高级的MAST结构)

  3. 从SegWit借鉴:隔离见证数据结构和效率

  4. 全新创新

    • Schnorr签名(更小,支持密钥/签名聚合)

    • MAST(默克尔抽象语法树,只公开使用的脚本路径)

    • 密钥和脚本路径的统一外观(隐私增强)

说说交易延展性

交易延展性是指在不改变交易基本内容(发送方、接收方、金额)的情况下,可以修改交易的ID。为什么会发生?

在比特币早期设计中,交易ID(txid)是通过对整个交易数据(包括签名)进行哈希计算得出的。而问题在于,签名本身可以有多种有效形式,就像你的签名每次都有细微差别一样。

想象你发送了1个比特币给朋友:

  1. 你创建并签名了一笔交易,交易ID是 abc123

  2. 你在区块浏览器上用 abc123 追踪这笔交易

  3. 矿工(或任何人)在不改变交易有效性的情况下,稍微修改了你的签名格式

  4. 交易被确认,但现在交易ID变成了 xyz789

  5. 你无法在区块浏览器上找到 abc123,可能误以为交易失败

  6. 你可能会再次发送1个比特币,结果朋友收到了2个比特币

实际危害

交易延展性导致的主要问题:

  1. 交易追踪困难:如果你发送一笔交易后想查询其状态,却发现交易ID变了,你会误以为交易失败

  2. 二层网络风险:比如闪电网络这样的二层解决方案需要可靠地引用之前的交易,延展性问题会破坏这种依赖关系

  3. 双重支付攻击:攻击者可以修改自己发送的交易的ID,然后假装交易从未发生,尝试再次花费同一笔资金

一个真实案例

Mt. Gox(曾经最大的比特币交易所)部分归因于交易延展性问题导致破产。他们的系统在看不到修改后的交易ID时,误以为交易失败并重复发送比特币。

SegWit如何解决这个问题

隔离见证(SegWit)通过一个简单而优雅的方式解决了这个问题:

  1. 将签名数据("见证"数据)从用于计算交易ID的部分移除

  2. 交易ID现在只基于交易的"不可变"部分计算

  3. 即使有人修改了签名,交易ID也保持不变

这就像给信件一个基于内容而非信封的唯一标识符,无论信封如何变化,内容标识符保持不变。

效果

解决交易延展性带来了几个重要好处:

  1. 使闪电网络等二层解决方案成为可能

  2. 提高了交易确认的可靠性

  3. 简化了钱包开发和交易跟踪

  4. 允许更复杂的智能合约设计

交易延展性问题的解决是比特币发展历程中的一个重要里程碑,为更多创新功能铺平了道路。

传统比特币交易ID(txid)是通过对整个交易数据(包括签名)进行哈希计算得出的

使用Hal Finney的交易为例:

传统交易ID的计算过程

交易ID ea44e97271691990157559d0bdd9959e02790c34db6c006d779e82fa5aee708e (Hal Finney的后续交易) 是通过对整个交易数据进行双SHA256哈希计算得出的。

1. 传统交易的数据结构包括:

  • 版本号 (4字节)

  • 输入计数 (varint)

  • 输入列表:

    • 前一个交易ID (32字节)

    • 输出索引 (4字节)

    • 脚本长度 (varint)

    • 脚本 (包含签名数据) - 这是关键部分!

    • 序列号 (4字节)

  • 输出计数 (varint)

  • 输出列表:

    • 金额 (8字节)

    • 脚本长度 (varint)

    • 脚本

  • 锁定时间 (4字节)

2. 计算过程:

txid = SHA256(SHA256(完整交易数据))

然后对结果进行字节序反转。

3. 在Finney的交易中:

这笔交易的解锁脚本(ScriptSig)包含:

30440220576497b7e6f9b553c0aba0d8929432550e092db9c130aae37b84b545e7f4a36c022066cb982ed80608372c139d7bb9af335423d5280350fe3e06bd510e695480914f01

这是一个DER编码的ECDSA签名,它是交易ID计算的一部分。

延展性问题

在传统交易中,如果签名数据被修改(即使保持有效性),交易ID就会改变。以下是可能导致签名变化的几种方式:

1. DER编码的多样性

ECDSA签名由(r,s)值组成,但s值有两种等效表示形式。如果将:

30440220576497b7e6f9b553c0aba0d8929432550e092db9c130aae37b84b545e7f4a36c022066cb982ed80608372c139d7bb9af335423d5280350fe3e06bd510e695480914f01

修改为使用等效的s值,签名仍然有效,但整个交易ID会改变。

2. 空字节填充

在DER编码中,可以添加额外的空字节,签名仍然有效,但会导致交易ID变化。

3. 签名哈希类型修改

签名末尾的哈希类型字节(01)在某些情况下可以修改,保持签名有效性但改变交易ID。

举例说明

假设有人修改了Finney交易的签名,将最后的哈希类型标志从01改为81(同样有效),新的签名可能是:

30440220576497b7e6f9b553c0aba0d8929432550e092db9c130aae37b84b545e7f4a36c022066cb982ed80608372c139d7bb9af335423d5280350fe3e06bd510e695480914f81

这个变化会导致:

  1. 新签名仍然是有效的

  2. 交易的功能完全相同

  3. 但计算出的交易ID完全不同

这就是为什么传统交易容易受到交易延展性攻击的原因。在双方相互依赖交易ID的场景(如闪电网络)中,这种可延展性会导致严重问题。

而SegWit通过将签名数据从交易ID计算中移除,彻底解决了这个问题,使交易ID在签名修改的情况下保持不变。

Segwit将签名数据("见证"数据)从用于计算交易ID的部分移,交易ID现在只基于交易的"不可变"部分计算。

我们举个真实例子:交易ID 9f67ae2890f33dcd6c44b2026c9f924ec31d29f1365d1d5b9e7fd8746a3b6523。

对于SegWit交易,计算txid时会忽略所有见证数据。具体步骤如下:

  1. 收集交易的非见证部分数据

    • 版本号 (4字节): 通常是 01000000

    • 输入计数 (varint): 此交易为 01 (1个输入)

    • 输入详情 (不包含见证数据):

      • 前一个交易ID (32字节,字节序反转)

      • 前一个输出索引 (4字节)

      • scriptSig长度 (varint): 由于是SegWit,这里为 00 (空)

      • scriptSig: 空

      • 序列号 (4字节): fdffffff (启用了RBF)

    • 输出计数 (varint): 02 (2个输出)

    • 输出详情:

      • 输出1金额 (8字节): 138.75340531 BTC的satoshi值

      • 输出1脚本长度 (varint)

      • 输出1脚本: 00143df5e6917c0fa9ce0d059065156d7c68fbc1f52a

      • 输出2金额 (8字节): 0.32100000 BTC的satoshi值

      • 输出2脚本长度 (varint)

      • 输出2脚本: a914b3dded4c789ee4f10b8a0a726bb2b54e13a4fadf87

    • 锁定时间 (4字节): 通常是 00000000

  2. 将这些数据连接成一个字节序列

  3. 对这个字节序列进行双SHA256哈希

    txid = SHA256(SHA256(非见证数据))
  4. 对结果进行字节序反转(比特币使用小端序表示哈希值)

关键是,这个计算过程完全排除了见证数据(此交易中的签名3044...和公钥0263...)。这样,即使见证数据被修改(保持有效性),交易ID也保持不变。

说说Schnorr签名

Schnorr签名确实是比特币采用Taproot之前就应该实现的技术,这种延迟主要是出于专利和兼容性考虑。Schnorr签名和传统的ECDSA签名,最重要的是签名聚合能力。

多签场景比较

传统多签(类比一种物理反应):

假设Alice、Bob和Carol共同控制一个3-of-3多签钱包:

传统方式(ECDSA):
- Alice的签名:71字节
- Bob的签名:71字节
- Carol的签名:71字节
- 三个公钥:每个33字节
- 多签脚本结构:额外约22字节
总计:约280字节

可见每个签名和公钥都必须单独列出,就像物理混合物的物理反应,成分彼此独立。传统多签让数据量变得很大。

Schnorr多签(化学反应):

Schnorr方式:
- 聚合签名:64字节
- 聚合公钥:32字节
总计:96字节

三个参与者可以"离线"聚合他们的签名和公钥,产生一个单一的签名和公钥,看起来与单签交易完全相同!这就像化学反应,生成了全新的产物,外部无法分辨原始成分。

真实世界示例

在现实中,这种差异尤为明显:

Bitfinex在2020年移动了约5,000个比特币,使用了一个复杂的多签操作。这笔交易占用了区块的12%空间!如果使用Schnorr签名,同样的安全级别可能只需要原来的三分之一或更少的空间。

Bitcoin Core团队签名:每次发布都需要多位开发者签名验证。使用Schnorr,他们可以生成一个单一签名,节省空间并提高隐私。

Schnorr签名的数学魔力

Schnorr签名的"化学反应"效果来自于其线性特性:

Copy对于公钥P1、P2和对应的签名S1、S2:
聚合公钥 = P1 + P2
聚合签名 = S1 + S2

这种简单的加法关系使它具有出色的聚合特性,而ECDSA没有这种特性。

比特币在Taproot升级上使用了Schnorr签名,是一个重要的理论应用。Schnorr在各方面的性能上都完胜ECDSA。

Last updated