第5章:Taproot:Bitcoin 脚本系统的演进#
参考:
examples/ch05_simple_taproot.py
最后更新: 2026-06-16
Taproot 让复杂的花费条件在链上看起来和普通支付一模一样:在资金被花费之前,一棵繁复的脚本树和一笔单签支付无从区分。
让这件事成立的有两块机器,本章会一块一块地把它们引入:
Schnorr 签名(BIP340)——一种用来替换 ECDSA 的新签名方案。
Key tweaking(BIP341)——一种把额外信息附加到公钥上的方式,而公钥在链上看起来不变。
后面会出现的几个词——tweak、commitment、Merkle root、script 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 的有效签名
由此引出三种构造:
公钥聚合:多人公钥合并为一个
签名唯一性:多方协作产出一个签名
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 支出)
这套关系保证:
任何人都能从 P 和承诺算出 P’(给定 internal key P 和可选的 Merkle 根 M)
只有密钥持有者能从 d 和 tweak 算出 d’
关系 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 的关键点:
两条花费路径:tweaked 后的公钥提供两种花费方法:
Key path:直接用 tweaked 私钥签名(协作)
Script path:揭示 internal public key,证明脚本执行(回退)
密码学绑定:tweak 值把 output key 绑定到特定的脚本承诺
可验证:任何人都能验证 tweaked 后的公钥确实承诺到特定条件
不可区分: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 支付无法区分
关键观察:
Taproot 地址生成:
get_taproot_address()内部应用 BIP341 的 tweakSchnorr 签名:
sign_taproot_input()产出 64 字节 BIP340 签名最小见证:见证栈只需签名(SIGHASH_DEFAULT 时 64 字节)
相同形态:与任何 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 结构、在创建输出时承诺、并仅在实际使用某条路径时才揭示。