第5章:Taproot:Bitcoin 脚本系统的演进#

参考: examples/ch05_simple_taproot.py
最后更新: 2026-06-16


Taproot 让复杂的花费条件在链上看起来和普通支付一模一样:在资金被花费之前,一棵繁复的脚本树和一笔单签支付无从区分。

让这件事成立的有两块机器,本章会一块一块地把它们引入:

  1. Schnorr 签名(BIP340)——一种用来替换 ECDSA 的新签名方案。

  2. Key tweaking(BIP341)——一种把额外信息附加到公钥上的方式,而公钥在链上看起来不变。

后面会出现的几个词——tweakcommitmentMerkle rootscript path——是在为第 6–8 章做铺垫。如果第一次读到觉得某个词重,跳过它就好。 本章只默认你已经掌握第 1–4 章给的东西:密钥、签名、脚本、见证。

5.1 Taproot 承诺:统一外观#

Taproot 让复杂的花费条件在链上和普通支付看起来一致。无论一笔输出代表:

  • 简单的单签支付

  • 复杂的多方合约

  • Lightning Network 通道

  • 多授权级别的企业资金库

它们在花费之前在区块链上呈现相同的形态。这种一致由两块机器实现:Schnorr 签名和 key tweaking。

5.2 Schnorr 签名#

为什么要换成 Schnorr?ECDSA 的局限#

比特币 2009 年上线时就用 ECDSA,一路用到现在。ECDSA 用来单独签名和验签是够用的——但 Taproot 的设计需要的不止这些。它需要签名不会在传播过程中被改头换面、需要多方协作时签名能干净地合并、需要签名在简单代数运算下表现一致。ECDSA 从来不是为这些场景设计的。

ECDSA 中那些挡了 Taproot 路的性质:

  • 可篡改:第三方可以在不破坏有效性的前提下改写签名编码——同一个签名,链上会出现两种形态。

  • 无法聚合:两方各自的两个签名只能保持为两个,没法合并成一个。

  • 无线性:把两个 ECDSA 签名相加,并不能得到对应公钥之和的合法签名。没有干净的代数可用。

  • 体积可变:通常 71–72 字节,取决于编码。

BIP340 在同一条 secp256k1 曲线上规定了 Schnorr 签名,针对 Taproot 需要的性质做了设计:

  • 不可篡改:确定性 nonce、x-only 公钥(x-only public key)、严格编码——消除了 ECDSA 那种第三方可篡改向量。

  • 可聚合:多个公钥可合并为一个;多方协作签名可在链上以一条 64 字节签名落地。

  • 线性:这是 Taproot 真正需要的性质。下一节展开。

  • 固定 64 字节:体积更小、更统一。

Schnorr 线性#

让 Taproot 成立的代数性质:

 Alice 对消息 M 持有签名 A
 Bob  对消息 M 持有签名 B
 A + B  (Alice + Bob) 合并公钥下对 M 的有效签名

由此引出三种构造:

  1. 公钥聚合:多人公钥合并为一个

  2. 签名唯一性:多方协作产出一个签名

  3. Key tweaking:公钥可被确定性地”调谐”以提交承诺

注:上面所说的”签名唯一性”指通过 MuSig2(钱包层协议)在链上产出一个 BIP340 签名,并不是 consensus 层跨输入的签名聚合。

ECDSA vs Schnorr 直观对比#

ECDSA Multisig (3-of-3):
┌─────────────────────────────────────┐
│           Transaction               │
├─────────────────────────────────────┤
│ Alice Signature:   [71 bytes]       │
│ Bob Signature:     [72 bytes]       │
│ Charlie Signature: [70 bytes]       │
├─────────────────────────────────────┤
│ Total Size: ~213 bytes              │
│ Verifications: 3 separate           │
│ Privacy: reveals 3 participants     │
│ Appearance: multi                   │
└─────────────────────────────────────┘

Schnorr Aggregated (3-of-3):
┌─────────────────────────────────────┐
│           Transaction               │
├─────────────────────────────────────┤
│ Aggregated Signature: [64 bytes]    │
├─────────────────────────────────────┤
│ Total Size: 64 bytes                │
│ Verifications: 1 single check       │
│ Privacy: hides participant count    │
│ Appearance: single                  │
└─────────────────────────────────────┘

5.3 Key Tweaking:通往 Taproot 的桥梁#

Taproot 通过 key tweaking(在 BIP340/341/342 中也称作 tweakable commitment)利用 Schnorr 的线性。

直观写法:

t = H("TapTweak" || internal_pubkey || merkle_root)

形式化(BIP341):

t  = int(HashTapTweak(xonly_internal_key || merkle_root_or_empty)) mod n

P' = P + t * G
d' = d + t

Even-Y 要求(BIP340):
Taproot 使用 x-only 公钥——但 secp256k1 上的实际点仍有两个可能的 y 值(even / odd)。
BIP340 的规则:最终的 tweaked output key 必须对应 even-y 点
若结果落到 odd-y,实现会翻转私钥 d' = n d',使 P' = d'*G 落到 even 那一支。

(这点在后面会用到:在 script-path 支出中,这个奇偶性会被编码到 control block 的最低位。如果不在这里把这点记住,后面 script-path 验证会通不过。)

Tweak 流程#

Internal Key (P) ─────────► + tweak ─────────► Output Key (P')
                              ▲                      │
                              │                      │
                       Merkle Root ◄────────────────┘
                    script_path_commitment

变量说明:

  • P = Internal Key(用户控制的原始公钥)

  • M = Merkle 根(对所有可能花费条件的承诺)

  • t = Tweak Value(由 P 与 M 确定性计算)

  • P' = Output Key(最终上链的 Taproot 地址公钥)

  • d' = Tweaked Private Key(用于 key-path 支出)

这套关系保证:

  1. 任何人都能从 P 和承诺算出 P’(给定 internal key P 和可选的 Merkle 根 M)

  2. 只有密钥持有者能从 d 和 tweak 算出 d’

  3. 关系 d’ × G = P’ 成立(签名验证有效)

关键代码#

# Key-path-only: tree 为空,t = HashTapTweak(internal_pubkey || b''), P' = P + t*G
address = pubkey.get_taproot_address([])
# 示例 1: Key-path-only Taproot 地址(btcaaron)
# 参考: examples/ch05_simple_taproot.py

from btcaaron import Key, TapTree

sender = Key.from_wif("cPeon9fBsW2BxwJTALj3hGzh9vm8C52Uqsce7MzXGS1iFJkPF4AT")
program = TapTree(internal_key=sender).build()

print("=== KEY-PATH-ONLY TAPROOT 地址 ===")
print(f"内部密钥 (x-only): {sender.xonly}")
print(f"Taproot 地址:     {program.address}")
print(f"叶子脚本: {program.leaves}(空)")
print("btcaaron 内部完成: t=HashTapTweak(x-only||merkle_root), P'=P+t×G")
=== KEY-PATH-ONLY TAPROOT 地址 ===
内部密钥 (x-only): 898711e6bf63f5cbe1b38c05e89d6c391c59e9f8f695da44bf3d20ca674c8519
Taproot 地址:     tb1pjyjeruun8pc5ln3wtv2d6lsxqn55frpyc83kn473h7848d0kj73sxy3ku8
叶子脚本: [](空)
btcaaron 内部完成: t=HashTapTweak(x-only||merkle_root), P'=P+t×G

Key tweaking 的关键点:

  1. 两条花费路径:tweaked 后的公钥提供两种花费方法:

    • Key path:直接用 tweaked 私钥签名(协作)

    • Script path:揭示 internal public key,证明脚本执行(回退)

  2. 密码学绑定:tweak 值把 output key 绑定到特定的脚本承诺

  3. 可验证:任何人都能验证 tweaked 后的公钥确实承诺到特定条件

  4. 不可区分:tweaked 后的公钥在数学上与任何其他 Schnorr 公钥没有区别

5.4 为什么这能实现统一外观#

Schnorr 签名和 key tweaking 结合,让简单支付和复杂合约在链上呈现相同形态:

Simple Payment:
├── Internal Key: Just a regular private key
├── Script Commitment: Empty (no conditions)
├── Tweaked Key: Internal key + H(key || empty)
└── Spending: 64-byte Schnorr signature

Complex Contract:
├── Internal Key: Same regular private key
├── Script Commitment: Merkle root of 100 conditions
├── Tweaked Key: Internal key + H(key || merkle_root)
└── Spending: 64-byte Schnorr signature (if cooperative)

外部观察者看到的:两笔交易都是 64 字节签名,无法区分内部复杂度。

5.5 简单 Taproot 交易:整合所有内容#

下面通过一笔最基础的 Taproot-to-Taproot 交易看它在实践中怎么跑:

# 示例 2: 简单 Taproot 交易(btcaaron)
# 参考: examples/ch05_simple_taproot.py

from btcaaron import Key, TapTree

sender = Key.from_wif("cPeon9fBsW2BxwJTALj3hGzh9vm8C52Uqsce7MzXGS1iFJkPF4AT")
program = TapTree(internal_key=sender).build()

tx = (program.keypath()
    .from_utxo("b0f49d2f30f80678c6053af09f0611420aacf20105598330cb3f0ccb8ac7d7f0", 0, sats=29200)
    .to("tb1p53ncq9ytax924ps66z6al3wfhy6a29w8h6xfu27xem06t98zkmvsakd43h", 29000)
    .sign(sender)
    .build())

print("=== TAPROOT 交易 ===")
print(f"从:   {program.address}")
print(f"到:   tb1p53ncq9ytax924ps66z6al3wfhy6a29w8h6xfu27xem06t98zkmvsakd43h")
print(f"金额: 29,000 sats (手续费: 200 sats)")
print(f"TXID: {tx.txid}")
print()
print("见证: 64 字节 Schnorr 签名,与任何 Taproot 支付无法区分")
=== TAPROOT 交易 ===
从:   tb1pjyjeruun8pc5ln3wtv2d6lsxqn55frpyc83kn473h7848d0kj73sxy3ku8
到:   tb1p53ncq9ytax924ps66z6al3wfhy6a29w8h6xfu27xem06t98zkmvsakd43h
金额: 29,000 sats (手续费: 200 sats)
TXID: 2a1312702ca1bb5f69d4099dd9f1da74c9601e885512e19a99ef018544c47c24

见证: 64 字节 Schnorr 签名,与任何 Taproot 支付无法区分

关键观察:

  1. Taproot 地址生成get_taproot_address() 内部应用 BIP341 的 tweak

  2. Schnorr 签名sign_taproot_input() 产出 64 字节 BIP340 签名

  3. 最小见证:见证栈只需签名(SIGHASH_DEFAULT 时 64 字节)

  4. 相同形态:与任何 Taproot 交易不可区分

5.6 链上示例:Testnet Taproot 转账#

来检视一笔真实的 Taproot 交易。

TXID: a3b4d0382efd189619d4f5bd598b6421e709649b87532d53aecdc76457a42cb6

Input:  ScriptPubKey OP_1 912591f3...5f697a3, Witness [7d25fbc9...da99f3]
Output: tb1p53ncq9...

签名恰好 64 字节(r 32B + s 32B),无可变编码;见证里只有签名一项,不含公钥(与 SegWit 不同)。

5.7 key-path 栈执行#

跟踪上面那笔交易在脚本栈上的执行过程:

│ (empty)              │  →  OP_1  →  │ 912591f3...5f697a3 (output_key) │
└───────────┘                         └───────────────────────────────────┘
     →  见证压栈  →  │ 7d25fbc9...da99f3 (schnorr_signature) │
                    │ 912591f3...5f697a3 (output_key)        │
                    └───────────────────────────────────────┘
     →  Schnorr 验证  →  │ 1 (TRUE) │

模式 OP_1 <32 字节> 选择 Taproot 解释器。witness 仅含签名 → 走 key path;witness 包含脚本与 control block → 走 script path(详见第六章)。Schnorr 验证执行 BIP340:解析 (r, s),计算挑战 e,计算 R = s·G e·P,若 r 等于 R 的 x 坐标则接受。

5.8 输出形态:Legacy → SegWit → Taproot#

Legacy P2PKH:
├── ScriptPubKey: OP_DUP OP_HASH160 <20-byte-hash> OP_EQUALVERIFY OP_CHECKSIG
├── ScriptSig: <signature> <public_key>
└── Size: ~225 bytes
   Information Revealed: Single signature spending

SegWit P2WPKH:
├── ScriptPubKey: OP_0 <20-byte-hash>
├── Witness: [signature, public_key]
└── Size: ~165 bytes
   Information Revealed: Single signature spending

Taproot P2TR (Simple):
├── ScriptPubKey: OP_1 <32-byte-output-key>
├── Witness: [schnorr_signature]
└── Size: ~135 bytes
   Information Revealed: Nothing about internal complexity

Taproot P2TR (Complex Contract):
├── ScriptPubKey: OP_1 <32-byte-output-key>
├── Witness: [schnorr_signature]
└── Size: ~135 bytes
   Information Revealed: Nothing about internal complexity

简单 Taproot 行与复杂 Taproot 行在输出层面字节级一致。花费之前无法区分。

# 可运行:解析 64 字节 Schnorr 签名为 r/s(标准库)
sig_hex = "7d25fbc9b98ee0eb09ed38c2afc19127465b33d6120f4db8d4fd46e532e30450d7d2a1f1dd7f03e8488c434d10f4051741921d695a44fb774897020f41da99f3"
sig = bytes.fromhex(sig_hex)
r, s = sig[:32], sig[32:]
print(f"r ({len(r)}B): {r.hex()[:16]}...{r.hex()[-8:]}")
print(f"s ({len(s)}B): {s.hex()[:16]}...{s.hex()[-8:]}")

5.9 编程差异:SegWit vs Taproot#

# SegWit P2WPKH: address = pk.get_segwit_address(); witness = [sig, pubkey]
# Taproot P2TR:  address = pubkey.get_taproot_address([]); witness = [sig]

两处 API 改动是承重的:

  • 签名换成 sign_taproot_input()(Schnorr,BIP340),不再用 sign_segwit_input()(ECDSA)。

  • 见证只含签名——公钥已经放在 scriptPubKey 里作为 output key。

5.10 协作 vs script path:成本不对称#

协作的 key-path 支出产出 64 字节见证,无论 output key 背后有多少方。Script-path 支出需要揭示 leaf 脚本与 control block(33 字节 internal pubkey + 叶深度 × 32 字节 Merkle proof),见证体积随树深与脚本长度而增长。

Cooperative Spending (Key Path):
├── Parties: Alice, Bob, Charlie (all agree)
├── Witness: [64-byte signature]
├── Size: ~135 bytes
└── Privacy: looks like single-sig

Non-Cooperative Spending (Script Path):
├── Parties: Alice, Bob, Charlie (dispute)
├── Witness: [script_data, revealed_script, control_block]
├── Size: ~200-500 bytes
└── Privacy: reveals one condition

Fee 上的差异让”协作”在可用时总是更便宜的那条路。

章节总结#

Taproot 用 BIP340 Schnorr(64 字节定长)替换了 ECDSA,并对 output key 应用 BIP341 的 tweak P' = P + t·G。这条 tweak 把 output key 绑定到一个空提交(仅 key-path)或一棵脚本树的 Merkle 根。

链上两种情况呈现相同的形态——OP_1 <32 字节>——key-path 支出的见证也都是 64 字节,与内部复杂度无关。因此无法从外部区分:

  • 简单的单签支付

  • 复杂的多方合约

  • Lightning Network 操作

  • 企业资金库交易

性质上:

  • 交易更小(64 字节签名)

  • 验证更快(单次签名检查)

  • 未使用的条件保持私有

下一章会展示如何把任意花费条件组织进脚本树的 Merkle 结构、在创建输出时承诺、并仅在实际使用某条路径时才揭示。