Bitcoin多签技术演进:从P2SH到Taproot Script Path的范式转变
Bitcoin多签技术演进:从P2SH到Taproot Script Path的范式转变
摘要
Bitcoin多签技术从传统P2SH演进到Taproot Script Path,表面上看似简单的升级,实际上代表了脚本执行机制的根本性变革。本文通过分析真实链上交易数据,详细对比了OP_CHECKMULTISIG
与OP_CHECKSIGADD
两种操作码的执行机制,揭示了为什么相同的公钥对在不同方案中需要不同的签名顺序。研究表明,这种差异源于栈操作机制的本质不同:从匹配算法到计数器系统的技术演进。
引言
2021年Taproot的激活为Bitcoin带来了新的脚本执行环境。其中,OP_CHECKSIGADD
操作码的引入不仅仅是对传统OP_CHECKMULTISIG
的简单替代,而是代表了Script设计哲学的根本转变。通过分析两笔使用相同公钥对但采用不同多签方案的真实交易,我们可以清楚地看到这种技术演进的具体体现。
技术背景
传统P2SH多签(OP_CHECKMULTISIG)
传统的P2SH多签使用OP_CHECKMULTISIG
操作码,其脚本结构如下:
OP_2 <pubkey1> <pubkey2> OP_2 OP_CHECKMULTISIG
对应的scriptSig结构:
OP_0 <signature1> <signature2> <redeemScript>
Taproot Script Path多签(OP_CHECKSIGADD)
Taproot Script Path使用OP_CHECKSIGADD
操作码,脚本结构:
OP_0 <pubkey1> OP_CHECKSIGADD <pubkey2> OP_CHECKSIGADD OP_2 OP_EQUAL
对应的witness结构:
<signature2> <signature1> <script> <controlBlock>
真实案例分析
链上数据对比
我们分析了两笔测试网交易,它们使用相同的公钥对但采用不同的多签方案:
案例1:Taproot Script Path多签
交易ID: 03f4b6384d9a4262bb7df2d72138c5905fa6c48b7c264b75238dcd93e7679f3c
公钥对:
- Alice: 50be5fc44ec580c387bf45df275aaa8b27e2d7716af31f10eeed357d126bb4d3
- Bob: 84b5951609b76619a1ce7f48977b4312ebe226987166ef044bfb374ceef63af5
Witness结构:
[0] Bob签名: 8fd1ac2f9a8fe6ae7ac06e14e1412b0ea40f0b50d95bc95590d29daef5e8e55073d215e802947070f4a0ca07cc74bd41c1c37db7d3fe5851faf6734e047ea045
[1] Alice签名: 44313c70aa3c5450d239e7a591e7ba13b3d73f85c6c90563cb594627bdfd5bfdfeb700f413e00091edd3f6fcc113847a1b16677a2997466a255dd9ac0d6cd2bd
[2] 脚本: 002050be5fc44ec580c387bf45df275aaa8b27e2d7716af31f10eeed357d126bb4d3ba2084b5951609b76619a1ce7f48977b4312ebe226987166ef044bfb374ceef63af5ba5287
[3] 控制块: c050be5fc44ec580c387bf45df275aaa8b27e2d7716af31f10eeed357d126bb4d3
案例2:传统P2SH多签
交易ID: ab5ba1cc5b2b16b4c60e6740c590da90b53e9527732b54f6d39f87427d86b3c5
使用相同的公钥对
ScriptSig结构:
[0] OP_0: 00
[1] Alice签名: 304402202fb043d3c243799310d84a3058f99a51fb315056d929262937ffd1a3f8f4679b0220083c5972f13c3d549c2a1decaa12545b5c60fc7c479125f5a68d93272961855701
[2] Bob签名: 304402206d92a65686a47377400e82ed1e8f41654a6d9d467d2a3090c0213db3df9517d502201ba37517e72e19d0e96a14ff65735cd26c6dbd4ce21bf883099fdce84a1af44a01
[3] 赎回脚本: 52210250be5fc44ec580c387bf45df275aaa8b27e2d7716af31f10eeed357d126bb4d3210284b5951609b76619a1ce7f48977b4312ebe226987166ef044bfb374ceef63af552ae
关键观察
相同公钥,不同签名顺序
Taproot: Bob签名在witness[0],Alice签名在witness[1]
P2SH: Alice签名在scriptSig[1],Bob签名在scriptSig[2]
签名格式差异
Taproot: 64字节Schnorr签名
P2SH: ~71字节DER格式ECDSA签名
脚本结构对比
Taproot:
OP_0 <alice_pubkey> OP_CHECKSIGADD <bob_pubkey> OP_CHECKSIGADD OP_2 OP_EQUAL
P2SH:
OP_2 <alice_pubkey> <bob_pubkey> OP_2 OP_CHECKMULTISIG
栈执行机制深度分析
Taproot Script Path执行流程
阶段1:初始witness栈状态
栈状态 (顶→底):
- Bob签名 (witness[0])
- Alice签名 (witness[1])
- 脚本内容 (witness[2])
- 控制块 (witness[3])
阶段2:OP_0执行
操作: 推入计数器初始值
栈状态:
- 0 (计数器)
- Bob签名
- Alice签名
- ...
阶段3:推入Alice公钥 + 第一个OP_CHECKSIGADD
操作: OP_PUSHBYTES_32 <alice_pubkey> OP_CHECKSIGADD
弹出: Alice公钥(栈顶) + 0(计数器) + Alice签名
验证: Alice签名 ↔ Alice公钥 ✓
推入: 1 (计数器递增)
阶段4:推入Bob公钥 + 第二个OP_CHECKSIGADD
操作: OP_PUSHBYTES_32 <bob_pubkey> OP_CHECKSIGADD
弹出: Bob公钥(栈顶) + 1(计数器) + Bob签名
验证: Bob签名 ↔ Bob公钥 ✓
推入: 2 (计数器递增)
阶段5:最终验证
操作: OP_2 OP_EQUAL
栈状态: 2 (成功签名数) | 2 (期望值)
比较: 2 == 2 → TRUE
结果: 脚本执行成功 ✓
传统P2SH执行流程
阶段1:P2SH哈希验证
操作: OP_HASH160 <script_hash> OP_EQUAL
验证: SHA256(RIPEMD160(赎回脚本)) 与输出脚本哈希匹配 ✓
阶段2:赎回脚本展开
赎回脚本内容: OP_2 <alice_pubkey> <bob_pubkey> OP_2 OP_CHECKMULTISIG
栈重新排列: 按OP_CHECKMULTISIG要求的格式组织
阶段3:OP_CHECKMULTISIG匹配算法
匹配过程:
1. 取Alice签名 → 尝试Alice公钥 → 验证成功 ✓
2. 取Bob签名 → 尝试Bob公钥 → 验证成功 ✓
3. 消耗额外的OP_0 (修复off-by-one bug)
4. 检查: 2个成功签名 >= 2个要求签名 ✓
结果: 推入TRUE,脚本执行成功
技术机制对比分析
OP_CHECKSIGADD vs OP_CHECKMULTISIG 详细比较
算法模式
计数器累加系统
签名匹配算法
栈操作方式
每次弹出3个元素
批量处理多个元素
时间复杂度
O(n) - 每签名验证一次
O(m×n) - 可能多次尝试匹配
签名顺序要求
由栈LIFO特性决定
必须严格按公钥顺序
批量验证支持
✓ 支持(Schnorr)
✗ 不支持(ECDSA)
历史漏洞
✓ 无off-by-one问题
✗ 需要额外OP_0修复
公钥数量限制
999个(栈大小限制)
20个(硬编码限制)
签名类型
Schnorr (64字节)
ECDSA (~71字节)
为什么签名顺序不同?
这是两种不同算法架构导致的必然结果:
1. Taproot的栈消费机制:
脚本执行顺序: OP_0 <alice_pubkey> OP_CHECKSIGADD <bob_pubkey> OP_CHECKSIGADD OP_2 OP_EQUAL
栈操作分析:
- 第一个OP_CHECKSIGADD: 需要Alice签名在栈顶位置
- 第二个OP_CHECKSIGADD: 需要Bob签名在更深的栈位置
- 由于栈的LIFO特性: Bob签名必须先入栈,Alice签名后入栈
因此witness结构必须是: [Bob签名, Alice签名, 脚本, 控制块]
2. P2SH的匹配逻辑:
脚本结构: OP_2 <alice_pubkey> <bob_pubkey> OP_2 OP_CHECKMULTISIG
匹配要求:
- OP_CHECKMULTISIG要求签名必须按公钥在脚本中出现的顺序提供
- Alice公钥在脚本中排第一 → Alice签名必须排第一
- Bob公钥在脚本中排第二 → Bob签名必须排第二
因此scriptSig结构是: [OP_0, Alice签名, Bob签名, 赎回脚本]
性能与安全性改进
计算效率大幅提升
传统OP_CHECKMULTISIG的性能瓶颈:
最坏情况时间复杂度: O(m×n),其中m为签名数,n为公钥数
签名匹配重试机制导致额外的计算开销
不支持批量验证,每个签名独立处理
硬编码的20个公钥限制
OP_CHECKSIGADD的性能优势:
固定时间复杂度: O(n),每个签名只验证一次
支持Schnorr签名的批量验证特性,可并行处理多个签名
理论上支持999个公钥(仅受栈大小限制)
更紧凑的签名格式(64字节 vs ~71字节)
安全性全面增强
1. 消除历史安全漏洞:
彻底修复OP_CHECKMULTISIG的off-by-one bug
强制执行MINIMALIF规则,减少交易可塑性攻击面
消除签名重排序攻击的可能性
2. 密码学技术升级:
从ECDSA升级到更安全的Schnorr签名算法
支持密钥聚合和阈值签名等高级密码学方案
更强的抗量子计算攻击能力(相对而言)
3. 资源管理优化:
引入基于witness权重的sigop预算系统
移除全局sigop限制带来的复杂交互
为矿工提供更灵活的区块构造策略
开发实践指南
关键编程要点
1. 树结构的强制要求
# ❌ 错误:直接传递脚本对象
taproot_addr = alice_pub.get_taproot_address(multisig_script)
# ✅ 正确:必须使用树结构包装,即使是单叶子
tree = [[multisig_script]] # 注意双层列表结构
taproot_addr = alice_pub.get_taproot_address(tree)
2. 控制块的正确构造
control_block = ControlBlock(
internal_pubkey=alice_public, # 内部公钥
scripts=tree, # 完整的Merkle树结构
leaf_index=0, # 目标脚本在树中的索引
is_odd=taproot_address.is_odd() # 地址的奇偶性标记
)
3. Taproot签名参数的关键差异
# Taproot脚本路径签名
signature = private_key.sign_taproot_input(
tx, input_index,
[taproot_address.to_script_pub_key()],
[input_amount_in_satoshis],
script_path=True, # 必须指定为脚本路径花费
tapleaf_script=multisig_script, # 注意: 单数形式,不是tapleaf_scripts
tweak=False # 通常不使用tweaking
)
4. Witness元素顺序的正确构造
# Taproot Script Path的witness结构必须精确匹配栈消费顺序
witness_data = [
bob_signature, # 第1个元素: Bob签名(最先被消费)
alice_signature, # 第2个元素: Alice签名(第二个被消费)
multisig_script.to_hex(), # 第3个元素: 脚本内容
control_block.to_hex() # 第4个元素: 控制块
]
常见错误与解决方案
1. witness顺序错误
# ❌ 常见错误:按直觉排序
witness_data = [alice_sig, bob_sig, script, control_block]
# ✅ 正确做法:按栈消费顺序排序
witness_data = [bob_sig, alice_sig, script, control_block]
2. 树结构格式错误
# ❌ 错误:忘记双层列表包装
tree = [multisig_script]
# ✅ 正确:即使单叶子也要双层包装
tree = [[multisig_script]]
3. 签名函数参数混淆
# ❌ 错误:使用复数形式的参数名
sign_taproot_input(..., tapleaf_scripts=script, ...)
# ✅ 正确:使用单数形式
sign_taproot_input(..., tapleaf_script=script, ...)
技术演进的深层意义
从OP_CHECKMULTISIG到OP_CHECKSIGADD的转变体现了Bitcoin协议设计思想的重大转型:
1. 设计哲学:从复杂到简洁
传统方式:复杂的签名匹配算法,需要处理各种边界情况和异常路径
现代方式:简洁的计数器机制,逻辑清晰直观,易于理解和验证
2. 系统架构:从限制到灵活
传统限制:硬编码的20个公钥限制,固定的执行模式,难以扩展
现代设计:可配置的参数限制,支持更复杂的脚本组合和高级用例
3. 技术集成:从孤立到协同
孤立设计:每个功能独立实现,难以与其他Bitcoin特性有效协同
协同设计:与批量验证、Schnorr签名、Merkle树等现代特性深度集成
4. 升级策略:从向后兼容到面向未来
传统策略:受历史技术债务束缚,难以进行根本性改进
未来导向:通过OP_SUCCESS机制为未来的软分叉升级预留扩展空间
5. 性能优化:从功能实现到效率优先
功能导向:优先保证功能正确性,性能考虑为次要因素
效率优先:在保证安全的前提下,将性能和可扩展性提升到首要位置
技术影响与展望
对Bitcoin生态的积极影响
1. 开发者体验改善:
更直观的脚本编写模式
减少因历史包袱导致的开发陷阱
更好的工具链和调试支持
2. 网络性能提升:
交易验证速度显著提升
更高的网络吞吐量潜力
减少节点的计算资源消耗
3. 安全性增强:
减少攻击面和漏洞风险
更强的密码学安全保障
更好的抗审查能力
对未来发展的启示
这种技术演进模式为Bitcoin未来的发展提供了重要启示:
渐进式创新:通过软分叉逐步引入新技术,避免激进变革的风险
向后兼容性:在引入新功能的同时保持对现有系统的支持
模块化设计:将复杂功能分解为独立的、可组合的模块
长期规划:为未来的技术升级预留足够的设计空间
结论
Taproot Script Path多签技术相比传统P2SH多签的"签名顺序差异",实际上揭示了两种根本不同的技术架构和设计理念。这种变化远不止是表面的API调整,而是从算法设计、系统架构到安全模型的全方位重新思考。
OP_CHECKSIGADD的引入代表了Bitcoin脚本系统向更高效、更安全、更可扩展方向的重要演进。它不仅解决了传统OP_CHECKMULTISIG方案的诸多技术债务,更为Bitcoin未来的协议升级建立了坚实的技术基础。
对于Bitcoin开发者而言,深入理解这些底层技术差异不仅有助于正确实现Taproot功能,更重要的是能够把握Bitcoin技术发展的内在逻辑和演进趋势。这种理解将成为构建下一代Bitcoin应用和服务的重要基础。
随着Bitcoin生态系统的持续发展,我们可以预期这种技术演进模式——通过软分叉引入新的操作码来替代过时的实现——将继续推动整个网络向更高的技术标准和性能水平演进。这不仅体现了Bitcoin协议的强大适应性和进化能力,也为加密货币技术的长期发展树立了典范。
参考文献
BIP 342: Validation of Taproot Scripts. Pieter Wuille, Jonas Nick, Anthony Towns. 2020.
BIP 341: Taproot: SegWit version 1 spending rules. Pieter Wuille, Jonas Nick, Anthony Towns. 2020.
BIP 340: Schnorr Signatures for secp256k1. Pieter Wuille, Jonas Nick, Tim Ruffing. 2020.
Bitcoin Core Implementation: src/script/interpreter.cpp. Bitcoin Core Contributors.
Aaron Recompile: "How I Built a Time-Locked Bitcoin Script with CSV and P2SH." Medium, 2024.
测试网数据来源: mempool.space Bitcoin Testnet Explorer.
Bitcoin Stack Exchange: 相关技术讨论和问答社区资源.
Bitcoin Optech Newsletter: Taproot技术解析和最佳实践指南.
声明: 本文基于Bitcoin Core 27.0实现和真实测试网交易数据进行分析。所有代码示例和技术细节均经过实际验证。文章内容仅供技术研究和教育用途,不构成投资建议。
作者信息: 基于实际的Bitcoin Taproot多签开发实践经验,结合链上数据分析和Bitcoin Core源码研究完成。
版本信息: Version 1.0 - 2025年6月
Last updated