5.2 双叶子节点的Taproot编程
场景设计
Alice想安全地发送一些测试网比特币到中间地址,这个地址可以用三种方式解锁:
Key Path(密钥路径): Alice随时用她的私钥取回资金,无需任何条件。
Script Path(脚本路径)1 : 任何知道秘密前像(preimage)"helloworld"的人(比如Carol)可以通过脚本条件领取资金。
Script Path(脚本路径)2:Bob 使用他的私钥签名也可以解锁
提交交易(Commit)
提交交易(Commit): Alice将资金发送到这个中间地址。这次中间地址产生,所需要的是Alice的内部公钥和更复杂的双叶子脚本Tweak所产生的。这具体在我们的代码中是这样的:首先哈希锁脚本是
hash_script = Script([
'OP_SHA256',
preimage_hash,
'OP_EQUALVERIFY',
'OP_TRUE'
])
Bob的签名脚本是
bob_script = Script([
bob_public.to_x_only_hex(),
'OP_CHECKSIG'
])
两者合成默克尔数:
all_leafs = [hash_script, bob_script]
再产生的Taproot地址
taproot_address = alice_public.get_taproot_address(all_leafs)
我们知道,这里用了库函数的get_taproot_address方法,背后其实就是Q = P + H(P || T) × G
我们的代码如下:
"""
Bob + HTLC 双叶子脚本测试 - Commit 脚本
创建一个包含 hash script (helloworld) 和 Bob script 的双脚本 Taproot 地址
这样我们可以测试 Bob Script Path 是否正确工作
=== 脚本树结构 ===
简单的双叶子树:
ROOT
/ \
/ \
HASH BOB
(hello (P2PK)
world)
三种花费方式:
1. Script Path 1:任何人提供 preimage "helloworld" 来花费
2. Script Path 2:Bob 使用私钥签名来花费
3. Key Path:Alice 用私钥直接花费
"""
from bitcoinutils.setup import setup
from bitcoinutils.utils import to_satoshis
from bitcoinutils.script import Script
from bitcoinutils.transactions import Transaction, TxInput, TxOutput
from bitcoinutils.keys import PrivateKey
import hashlib
def main():
# 设置测试网
setup('testnet')
# Alice 的密钥(内部密钥,Key Path)
alice_private = PrivateKey('cRxebG1hY6vVgS9CSLNaEbEJaXkpZvc6nFeqqGT7v6gcW7MbzKNT')
alice_public = alice_private.get_public_key()
# Bob 的密钥(Script Path 2)
bob_private = PrivateKey('cSNdLFDf3wjx1rswNL2jKykbVkC6o56o5nYZi4FUkWKjFn2Q5DSG')
bob_public = bob_private.get_public_key()
# 创建 preimage 的哈希(Script Path 1)
preimage = "helloworld"
preimage_bytes = preimage.encode('utf-8')
preimage_hash = hashlib.sha256(preimage_bytes).hexdigest()
# Script Path 1: 哈希锁脚本 - 验证 preimage
hash_script = Script([
'OP_SHA256',
preimage_hash,
'OP_EQUALVERIFY',
'OP_TRUE'
])
# Script Path 2: Bob 的签名脚本 - P2PK
bob_script = Script([
bob_public.to_x_only_hex(),
'OP_CHECKSIG'
])
print("=== Bob + Hash 双脚本 Taproot 地址信息 ===")
print(f"Alice 私钥 (Key Path): {alice_private.to_wif()}")
print(f"Alice 公钥 (Internal Key): {alice_public.to_hex()}")
print(f"Bob 私钥 (Script Path 2): {bob_private.to_wif()}")
print(f"Bob 公钥 (Script Path 2): {bob_public.to_hex()}")
print(f"Bob 公钥 (x-only): {bob_public.to_x_only_hex()}")
print(f"Preimage (Script Path 1): {preimage}")
print(f"Preimage Hash: {preimage_hash}")
# 按照验证的双脚本模式创建脚本树:平铺结构
all_leafs = [hash_script, bob_script]
# 创建 Taproot 地址
taproot_address = alice_public.get_taproot_address(all_leafs)
print(f"\nTaproot 地址: {taproot_address.to_string()}")
print(f"\n=== 脚本路径信息 ===")
print(f"Hash Script (索引 0): {hash_script}")
print(f"Bob Script (索引 1): {bob_script}")
print(f"\n=== 使用说明 ===")
print("1. 向这个 Taproot 地址发送比特币")
print("2. 可以通过以下三种方式花费:")
print(" - Key Path: Alice 使用她的私钥直接签名")
print(" - Script Path 1: 任何人提供正确的 preimage 'helloworld'")
print(" - Script Path 2: Bob 使用他的私钥签名")
print(f"\n=== 脚本树结构 ===")
print("简单的双叶子树:")
print(" ROOT")
print(" / \\")
print(" / \\")
print("HASH BOB")
print("(hello (P2PK)")
print(" world) ")
# 验证 Control Block 构造
from bitcoinutils.utils import ControlBlock
print(f"\n=== Control Block 预览 ===")
# Hash Script Control Block (索引 0)
hash_cb = ControlBlock(alice_public, all_leafs, 0, is_odd=taproot_address.is_odd())
print(f"Hash Script Control Block: {hash_cb.to_hex()}")
# Bob Script Control Block (索引 1)
bob_cb = ControlBlock(alice_public, all_leafs, 1, is_odd=taproot_address.is_odd())
print(f"Bob Script Control Block: {bob_cb.to_hex()}")
print(f"\n✅ 准备就绪!向地址 {taproot_address.to_string()} 发送测试币后,")
print("就可以测试 Bob Script Path 是否正确工作了!")
if __name__ == "__main__":
main()
代码运行后产生了一个中间地址
tb1p93c4wxsr87p88jau7vru83zpk6xl0shf5ynmutd9x0gxwau3tngq9a4w3z
整体代码的运行结果如下:
=== Bob + Hash 双脚本 Taproot 地址信息 ===
Alice 私钥 (Key Path): cRxebG1hY6vVgS9CSLNaEbEJaXkpZvc6nFeqqGT7v6gcW7MbzKNT
Alice 公钥 (Internal Key): 0250be5fc44ec580c387bf45df275aaa8b27e2d7716af31f10eeed357d126bb4d3
Bob 私钥 (Script Path 2): cSNdLFDf3wjx1rswNL2jKykbVkC6o56o5nYZi4FUkWKjFn2Q5DSG
Bob 公钥 (Script Path 2): 0284b5951609b76619a1ce7f48977b4312ebe226987166ef044bfb374ceef63af5
Bob 公钥 (x-only): 84b5951609b76619a1ce7f48977b4312ebe226987166ef044bfb374ceef63af5
Preimage (Script Path 1): helloworld
Preimage Hash: 936a185caaa266bb9cbe981e9e05cb78cd732b0b3280eb944412bb6f8f8f07af
Taproot 地址: tb1p93c4wxsr87p88jau7vru83zpk6xl0shf5ynmutd9x0gxwau3tngq9a4w3z
=== 脚本路径信息 ===
Hash Script (索引 0): ['OP_SHA256', '936a185caaa266bb9cbe981e9e05cb78cd732b0b3280eb944412bb6f8f8f07af', 'OP_EQUALVERIFY', 'OP_TRUE']
Bob Script (索引 1): ['84b5951609b76619a1ce7f48977b4312ebe226987166ef044bfb374ceef63af5', 'OP_CHECKSIG']
=== 使用说明 ===
1. 向这个 Taproot 地址发送比特币
2. 可以通过以下三种方式花费:
- Key Path: Alice 使用她的私钥直接签名
- Script Path 1: 任何人提供正确的 preimage 'helloworld'
- Script Path 2: Bob 使用他的私钥签名
=== 脚本树结构 ===
简单的双叶子树:
ROOT
/ \
/ \
HASH BOB
(hello (P2PK)
world)
=== Control Block 预览 ===
Hash Script Control Block: c050be5fc44ec580c387bf45df275aaa8b27e2d7716af31f10eeed357d126bb4d32faaa677cb6ad6a74bf7025e4cd03d2a82c7fb8e3c277916d7751078105cf9df
Bob Script Control Block: c050be5fc44ec580c387bf45df275aaa8b27e2d7716af31f10eeed357d126bb4d3fe78d8523ce9603014b28739a51ef826f791aa17511e617af6dc96a8f10f659e
✅ 准备就绪!向地址 tb1p93c4wxsr87p88jau7vru83zpk6xl0shf5ynmutd9x0gxwau3tngq9a4w3z 发送测试币后,
就可以测试 Bob Script Path 是否正确工作了!
三种花费路径揭示(Reveal Transaction)
1. Script Path 1- 用preimage解开哈希锁
关键代码片段,control block 的写法:
control_block = ControlBlock(
alice_public, # internal_pub
all_leafs, # all_leafs
0, # script_index (hash_script 是第 0 个)
is_odd=taproot_address.is_odd()
)
见证数据的构成:
tx.witnesses.append(TxWitnessInput([
preimage_hex, # preimage 的十六进制
hash_script.to_hex(), # 脚本的十六进制
control_block.to_hex() # 控制块的十六进制
]))
注意Script Path 花费 - 不需要签名,只需要提供 preimage。
完整的代码如下:
from bitcoinutils.setup import setup
from bitcoinutils.utils import to_satoshis, ControlBlock
from bitcoinutils.script import Script
from bitcoinutils.transactions import Transaction, TxInput, TxOutput, TxWitnessInput
from bitcoinutils.keys import PrivateKey
import hashlib
def main():
setup('testnet')
# Alice 的密钥(内部密钥)
alice_private = PrivateKey('cRxebG1hY6vVgS9CSLNaEbEJaXkpZvc6nFeqqGT7v6gcW7MbzKNT')
alice_public = alice_private.get_public_key()
# Bob 的密钥
bob_private = PrivateKey('cSNdLFDf3wjx1rswNL2jKykbVkC6o56o5nYZi4FUkWKjFn2Q5DSG')
bob_public = bob_private.get_public_key()
# Preimage 和哈希
preimage = "helloworld"
preimage_hex = preimage.encode('utf-8').hex()
preimage_hash = hashlib.sha256(preimage.encode('utf-8')).hexdigest()
# 重建脚本(必须与 commit 时完全相同)
hash_script = Script(['OP_SHA256', preimage_hash, 'OP_EQUALVERIFY', 'OP_TRUE'])
bob_script = Script([bob_public.to_x_only_hex(), 'OP_CHECKSIG'])
# 重建脚本树
all_leafs = [hash_script, bob_script]
taproot_address = alice_public.get_taproot_address(all_leafs)
print(f"=== Hash Script Path 解锁 ===")
print(f"Taproot 地址: {taproot_address.to_string()}")
print(f"Preimage: {preimage}")
print(f"Preimage Hex: {preimage_hex}")
print(f"使用脚本: {hash_script} (索引 0)")
# 输入信息(需要替换为实际的 UTXO)
commit_txid = "f02c055369812944390ca6a232190ec0db83e4b1b623c452a269408bf8282d66" # 替换为实际的交易ID
input_amount = 0.00001234 # 5000 satoshis,替换为实际金额
output_amount = 0.00001034 # 4500 satoshis,扣除手续费
# 构建交易
txin = TxInput(commit_txid, 0)
# 输出到 Alice 的简单 Taproot 地址
txout = TxOutput(to_satoshis(output_amount), alice_public.get_taproot_address().to_script_pub_key())
tx = Transaction([txin], [txout], has_segwit=True)
# 创建 Control Block
# hash_script 是索引 0
control_block = ControlBlock(
alice_public, # internal_pub
all_leafs, # all_leafs
0, # script_index (hash_script 是第 0 个)
is_odd=taproot_address.is_odd()
)
print(f"Control Block: {control_block.to_hex()}")
# Script Path 花费 - 不需要签名,只需要提供 preimage
# 见证数据格式:[preimage, script, control_block]
tx.witnesses.append(TxWitnessInput([
preimage_hex, # preimage 的十六进制
hash_script.to_hex(), # 脚本的十六进制
control_block.to_hex() # 控制块的十六进制
]))
# 输出信息
print(f"\n=== 交易信息 ===")
print(f"Input Amount: {input_amount} tBTC ({to_satoshis(input_amount)} satoshis)")
print(f"Output Amount: {output_amount} tBTC ({to_satoshis(output_amount)} satoshis)")
print(f"Fee: {input_amount - output_amount} tBTC ({to_satoshis(input_amount - output_amount)} satoshis)")
print(f"TxId: {tx.get_txid()}")
print(f"Raw Tx: {tx.serialize()}")
print(f"\n=== 验证 ===")
# 验证 Control Block 是否与预期一致
# 基于之前的成功经验,hash script (索引 0) 的 Control Block 应该是:
expected_hash_cb = "c050be5fc44ec580c387bf45df275aaa8b27e2d7716af31f10eeed357d126bb4d312273573f45d51a1c48a75e76fe1d955f9f00acb1fe288510ab242f0851a7bf5"
our_cb = control_block.to_hex()
print(f"我们的 Control Block: {our_cb}")
print(f"预期 Control Block: {expected_hash_cb}")
print(f"Control Block 匹配: {'✅' if our_cb == expected_hash_cb else '❌'}")
if our_cb != expected_hash_cb:
print("⚠️ Control Block 不匹配,可能需要调整参数")
else:
print("✅ Control Block 正确,交易应该能成功!")
print(f"\n📝 使用说明:")
print("1. 替换 commit_txid 和 input_amount 为实际值")
print("2. 任何知道 preimage 'helloworld' 的人都可以执行此花费")
print("3. 不需要任何私钥,只需要知道 preimage")
if __name__ == "__main__":
main()
运行的结果请见
https://mempool.space/testnet/tx/b61857a05852482c9d5ffbb8159fc2ba1efa3dd16fe4595f121fc35878a2e430
从mempool浏览器的链上数据上显示的上一节课程内容中我们熟悉的HTLC解锁的结构。
2. Script Path 2- 用Bob签名解开哈希锁
关键代码片段,control block 的写法:
# 构造 Control Block(基于双 hashlock 成功经验,Bob Script 索引 = 1)
control_block = ControlBlock(
alice_public, # internal_pub
all_leafs, # all_leafs
1, # script_index (bob_script 是索引 1)
is_odd=taproot_address.is_odd()
)
签名部分的写法:
from bitcoinutils.setup import setup
from bitcoinutils.utils import to_satoshis, ControlBlock
from bitcoinutils.script import Script
from bitcoinutils.transactions import Transaction, TxInput, TxOutput, TxWitnessInput
from bitcoinutils.keys import PrivateKey
import hashlib
def main():
setup('testnet')
# Alice 和 Bob 的密钥
alice_private = PrivateKey('cRxebG1hY6vVgS9CSLNaEbEJaXkpZvc6nFeqqGT7v6gcW7MbzKNT')
alice_public = alice_private.get_public_key()
bob_private = PrivateKey('cSNdLFDf3wjx1rswNL2jKykbVkC6o56o5nYZi4FUkWKjFn2Q5DSG')
bob_public = bob_private.get_public_key()
# 构造 hash_script 和 bob_script
preimage = "helloworld" # 实际使用时请替换
preimage_hash = hashlib.sha256(preimage.encode('utf-8')).hexdigest()
hash_script = Script(['OP_SHA256', preimage_hash, 'OP_EQUALVERIFY', 'OP_TRUE'])
bob_script = Script([bob_public.to_x_only_hex(), 'OP_CHECKSIG'])
# 构造 Taproot 地址(包含两个叶子)
all_leafs = [hash_script, bob_script]
taproot_address = alice_public.get_taproot_address(all_leafs)
print(f"Taproot 地址: {taproot_address.to_string()}")
print(f"Bob 公钥 (x-only): {bob_public.to_x_only_hex()}")
print(f"Bob Script: {bob_script}")
# UTXO 信息(请替换为实际 UTXO)
commit_txid = "8caddfad76a5b3a8595a522e24305dc20580ca868ef733493e308ada084a050c" # UTXO 的 txid
vout = 1 # UTXO 的索引
input_amount = 0.00001111 # 输入金额(BTC)
output_amount = 0.00000900 # 输出金额(BTC),已扣除手续费
# 构造交易
txin = TxInput(commit_txid, vout)
txout = TxOutput(to_satoshis(output_amount), bob_public.get_taproot_address().to_script_pub_key())
tx = Transaction([txin], [txout], has_segwit=True)
# 构造 Control Block(bob_script 索引为 1)
control_block = ControlBlock(
alice_public,
all_leafs,
1, # bob_script 在 all_leafs 中的索引
is_odd=taproot_address.is_odd()
)
print(f"Control Block: {control_block.to_hex()}")
# 用 Bob 的私钥进行 Script Path 签名(标准写法)
sig = bob_private.sign_taproot_input(
tx, 0,
[taproot_address.to_script_pub_key()],
[to_satoshis(input_amount)],
script_path=True,
tapleaf_script=bob_script,
tweak=False
)
print(f"签名: {sig}")
# 构造见证数据
tx.witnesses.append(TxWitnessInput([
sig,
bob_script.to_hex(),
control_block.to_hex()
]))
print(f"TxId: {tx.get_txid()}")
print(f"Raw Tx: {tx.serialize()}")
print("\n请将原始交易广播到 testnet 网络。")
if __name__ == "__main__":
main()
结果运行如下
https://mempool.space/testnet/tx/185024daff64cea4c82f129aa9a8e97b4622899961452d1d144604e65a70cfe0
解释一下链上各部分的含义:
① 签名
26a0eadca0bba3d1bb6f82b8e1f76e2d84038c97a92fa95cc0b9f6a6a59bac5f9977d7cb33dbd188b1b84e6d5a9447231353590578f358b2f18a66731f9f1c5c
这是 Bob 用私钥 对该输入(包括 bob_script 作为 tapleaf)做的 Schnorr 签名。
该签名证明了 Bob 拥有 bob_script 中公钥的私钥,且同意花费这笔 UTXO。
② 脚本(bob_script)
2084b5951609b76619a1ce7f48977b4312ebe226987166ef044bfb374ceef63af5ac
20 开头,后面 32 字节是 Bob 的 x-only 公钥(84b595...63af5),ac 是 OP_CHECKSIG。
反汇编为:
OP_PUSHBYTES_32 84b5951609b76619a1ce7f48977b4312ebe226987166ef044bfb374ceef63af5
OP_CHECKSIG
这就是 Taproot 地址的第二个叶子脚本(bob_script),只有 Bob 能签名花费。
③ 控制块(Control Block)
c050be5fc44ec580c387bf45df275aaa8b27e2d7716af31f10eeed357d126bb4d3fe78d8523ce9603014b28739a51ef826f791aa17511e617af6dc96a8f10f659e
Control Block 证明 bob_script 是该 Taproot 地址的一个叶子。
结构为:
1 字节前缀(c0...,包含奇偶性和内部公钥信息)
32 字节内部公钥(Alice 的 x-only 公钥)
32 字节 Merkle sibling(另一个叶子的哈希,即 hash_script 的哈希)
验证者用 Control Block + bob_script 可以重建 Taproot Merkle root,进而验证该脚本属于该地址。
3. Key Path - 用Alice签名解锁
这个跟上一节的类似,完整代码如下
from bitcoinutils.setup import setup
from bitcoinutils.utils import to_satoshis
from bitcoinutils.script import Script
from bitcoinutils.transactions import Transaction, TxInput, TxOutput, TxWitnessInput
from bitcoinutils.keys import PrivateKey
import hashlib
def main():
setup('testnet')
# Alice 的密钥(内部密钥)
alice_private = PrivateKey('cRxebG1hY6vVgS9CSLNaEbEJaXkpZvc6nFeqqGT7v6gcW7MbzKNT')
alice_public = alice_private.get_public_key()
# Bob 的密钥
bob_private = PrivateKey('cSNdLFDf3wjx1rswNL2jKykbVkC6o56o5nYZi4FUkWKjFn2Q5DSG')
bob_public = bob_private.get_public_key()
# 重建脚本(Key Path 花费需要完整的脚本树信息来计算 tweak)
preimage = "helloworld"
preimage_hash = hashlib.sha256(preimage.encode('utf-8')).hexdigest()
hash_script = Script(['OP_SHA256', preimage_hash, 'OP_EQUALVERIFY', 'OP_TRUE'])
bob_script = Script([bob_public.to_x_only_hex(), 'OP_CHECKSIG'])
# 重建脚本树
all_leafs = [hash_script, bob_script]
taproot_address = alice_public.get_taproot_address(all_leafs)
print(f"=== Alice Key Path 解锁 ===")
print(f"Taproot 地址: {taproot_address.to_string()}")
print(f"Alice 私钥: {alice_private.to_wif()}")
print(f"Alice 公钥: {alice_public.to_hex()}")
print(f"花费方式: Key Path (最私密)")
# 输入信息(需要替换为实际的 UTXO)
commit_txid = "9fafbb99a88e75e2c023bd89d2c7ad7f55be7c615d99737700ed97636e7d069b" # 替换为实际的交易ID
input_amount = 0.00001266 # 5000 satoshis,替换为实际金额
output_amount = 0.00001066 # 4500 satoshis,扣除手续费
# 构建交易
txin = TxInput(commit_txid, 0)
# 输出到 Alice 的简单 Taproot 地址
txout = TxOutput(to_satoshis(output_amount), alice_public.get_taproot_address().to_script_pub_key())
tx = Transaction([txin], [txout], has_segwit=True)
print(f"\n=== 交易构建 ===")
print(f"Input: {commit_txid}:0")
print(f"Output: {alice_public.get_taproot_address().to_string()}")
# Alice 使用 Key Path 签名
# Key Path 需要完整的脚本树信息来计算正确的 tweak
sig = alice_private.sign_taproot_input(
tx,
0,
[taproot_address.to_script_pub_key()], # 输入的 scriptPubKey
[to_satoshis(input_amount)], # 输入金额
script_path=False, # Key Path 花费
tapleaf_scripts=all_leafs # 完整的脚本树(用于计算 tweak)
)
print(f"Alice 签名: {sig}")
# Key Path 花费的见证数据只包含签名
tx.witnesses.append(TxWitnessInput([sig]))
# 输出信息
print(f"\n=== 交易信息 ===")
print(f"Input Amount: {input_amount} tBTC ({to_satoshis(input_amount)} satoshis)")
print(f"Output Amount: {output_amount} tBTC ({to_satoshis(output_amount)} satoshis)")
print(f"Fee: {input_amount - output_amount} tBTC ({to_satoshis(input_amount - output_amount)} satoshis)")
print(f"TxId: {tx.get_txid()}")
print(f"Raw Tx: {tx.serialize()}")
print(f"\n=== Key Path 特性 ===")
print("✅ 只需要 Alice 的私钥")
print("✅ 见证数据只有一个签名,最小化")
print("✅ 外界无法知道还有其他花费路径(完美隐私)")
print("✅ 与普通的单签名交易无法区分")
print("✅ 手续费最低,因为见证数据最少")
print(f"\n=== 见证数据分析 ===")
print("Key Path 见证数据结构:")
print(" [alice_signature] <- 只有一个元素")
print("")
print("对比 Script Path 见证数据结构:")
print(" [signature/preimage, script, control_block] <- 三个元素")
print("")
print("这就是 Key Path 的优势:简洁、私密、高效!")
print(f"\n📝 使用说明:")
print("1. 替换 commit_txid 和 input_amount 为实际值")
print("2. 只有 Alice 可以执行此花费")
print("3. 这是最推荐的花费方式(如果 Alice 同意)")
if __name__ == "__main__":
main()
运行结果见浏览器,一个典型的签名+公钥 解锁的链上结构:
https://mempool.space/testnet/tx/b11f27fdbe2323179260093f387a1ab5d5c1ea4b5524e2facd89813fe1daca8d
至此,我们掌握了更复杂的双叶子节点形成的taproot地址的编程。
Last updated