4.2 从P2WPKH到P2TR,Segwit to Taproot
4.2 从P2WPKH到P2TR
这一节主要讲如何解锁segwit地址
from bitcoinutils.setup import setup
from bitcoinutils.utils import to_satoshis
from bitcoinutils.transactions import Transaction, TxInput, TxOutput, TxWitnessInput
from bitcoinutils.keys import P2wpkhAddress, PrivateKey, P2trAddress
from bitcoinutils.script import Script
def main():
# 设置测试网
setup('testnet')
# 发送方私钥和公钥
from_private_key = PrivateKey('cPeon9fBsW2BxwJTALj3hGzh9vm8C52Uqsce7MzXGS1iFJkPF4AT')
from_pub = from_private_key.get_public_key()
# 赎回脚本
redeem_script = from_pub.get_segwit_address().to_script_pub_key()
# 创建交易输入
txin = TxInput(
'b774683366afe7d28fe7997c1235fd0d321b9212424041f9c90a6ebe28b3834d',
0
)
# 输入金额
amount = 0.000006
# 创建交易输出
to_address = P2trAddress('tb1pjyjeruun8pc5ln3wtv2d6lsxqn55frpyc83kn473h7848d0kj73sxy3ku8')
txout = TxOutput(to_satoshis(0.000004), to_address.to_script_pub_key())
# 创建交易(启用 segwit)
tx = Transaction([txin], [txout], has_segwit=True)
# 获取脚本代码
script_code = from_pub.get_address().to_script_pub_key()
# 计算签名
sig = from_private_key.sign_segwit_input(tx, 0, script_code, to_satoshis(amount))
# 设置赎回脚本
txin.script_sig = Script([])
# 添加见证数据
tx.witnesses.append(TxWitnessInput([sig, from_pub.to_hex()]))
# 获取签名后的交易
signed_tx = tx.serialize()
print("\n已签名的交易:")
print(signed_tx)
print("\n交易信息:")
print(f"从地址: {from_pub.get_segwit_address().to_string()}")
print(f"到地址: {to_address.to_string()}")
print(f"发送金额: 0.000004 BTC")
print(f"手续费: 0.000002 BTC")
print("\n您可以在这里广播交易:")
print("https://mempool.space/testnet/tx/push")
if __name__ == "__main__":
main()
代码片段解析
1-2、通过PrivateKey函数获得私钥 , 通过私钥的get_public_key()方法获得公钥, 跟之前的一样
3、获得发送地址的scriptpubkey
script_code = from_pub.get_address().to_script_pub_key()
4、接下来构建交易输入和输出,并组装交易,需要启动segwit
tx = Transaction([txin], [txout], has_segwit=True)
5、计算签名,注意用的是sign_segwit_input方法(之前legacy用的是私钥的sign_input方法),也用上了发送发的scriptpubkey
sig = from_private_key.sign_segwit_input(tx, 0, script_code, to_satoshis(total_input))
6、把 scriptsig变为空(这是segwit的概念精髓)
txin.script_sig = Script([])
7、添加见证数据
tx.witnesses.append(TxWitnessInput([sig, from_pub.to_hex()]))
8、交易序列化并发送交易
signed_tx = tx.serialize()
总结一下Segwit编程的关键区别:
# 1. 创建SegWit交易
tx = Transaction([txin], [txout], has_segwit=True) # 或 as_segwit=True
# 2. 签名需要额外的金额参数
sig = private_key.sign_segwit_input(
tx,
0, # 输入索引
script_pubkey, # 锁定脚本
to_satoshis(amount) # 前序UTXO的金额 (这是SegWit新增的)
)
# 3. 使用专门的segwit签名方法
# legacy: sign_input()
# segwit: sign_segwit_input()
# 4. script_sig置空
txin.script_sig = Script([]) # 空的script_sig
# 5. witness数据单独添加
tx.set_witness(0, [sig, pubkey]) # 或
tx.witnesses.append([sig, pubkey])
我们来看看浏览器里面各数据字节的解析b0f49d2f30f80678c6053af09f0611420aacf20105598330cb3f0ccb8ac7d7f0.
见证数据包含两个部分:
30440220212d4a6f059e1f9586b99fc61575eb2f66bc273c902b04b19efcb1344a535cf502202ec7d216c3519e516af0cb01edf9712c602b0eb3811238fd9d16a93b4a543a0101
02898711e6bf63f5cbe1b38c05e89d6c391c59e9f8f695da44bf3d20ca674c8519
第一部分是签名:
3044
- DER格式的签名标记0220212d4a6f059e1f9586b99fc61575eb2f66bc273c902b04b19efcb1344a535cf5
- r值02202ec7d216c3519e516af0cb01edf9712c602b0eb3811238fd9d16a93b4a543a01
- s值01
- 哈希类型 (SIGHASH_ALL)
第二部分是公钥:
02898711e6bf63f5cbe1b38c05e89d6c391c59e9f8f695da44bf3d20ca674c8519
这是一个压缩格式的公钥 (33字节,以02开头表示y坐标是偶数)
公钥与公钥哈希的关系
这里有个重要的对应关系:
公钥:
02898711e6bf63f5cbe1b38c05e89d6c391c59e9f8f695da44bf3d20ca674c8519
公钥哈希:
c5b28d6bba91a2693a9b1876bcd3929323890fb2
公钥哈希是通过以下步骤计算的:
对公钥进行SHA-256哈希
对结果再进行RIPEMD-160哈希
结果就是
c5b28d6bba91a2693a9b1876bcd3929323890fb2
,这个值用于:Legacy地址的scriptPubKey:
76a914
c5b28d6bba91a2693a9b1876bcd3929323890fb2
88ac
SegWit地址的scriptPubKey:
0014
c5b28d6bba91a2693a9b1876bcd3929323890fb2
考考你,76a914、88ac、0014又指的是什么

P2TR到P2TR
from bitcoinutils.setup import setup
from bitcoinutils.utils import to_satoshis
from bitcoinutils.transactions import Transaction, TxInput, TxOutput, TxWitnessInput
from bitcoinutils.keys import PrivateKey, P2trAddress
def main():
# 设置测试网
setup('testnet')
# 发送方信息(使用正确的私钥)
from_private_key = PrivateKey('cPeon9fBsW2BxwJTALj3hGzh9vm8C52Uqsce7MzXGS1iFJkPF4AT')
from_pub = from_private_key.get_public_key()
from_address = from_pub.get_taproot_address()
# 接收方地址
to_address = P2trAddress('tb1p53ncq9ytax924ps66z6al3wfhy6a29w8h6xfu27xem06t98zkmvsakd43h')
# 创建交易输入
txin = TxInput(
'b0f49d2f30f80678c6053af09f0611420aacf20105598330cb3f0ccb8ac7d7f0',
0
)
# 输入金额(用于签名)
input_amount = 0.00029200
amounts = [to_satoshis(input_amount)]
# 输入脚本(用于签名)
input_script = from_address.to_script_pub_key()
scripts = [input_script]
# 创建交易输出
amount_to_send = 0.00029000
txout = TxOutput(
to_satoshis(amount_to_send),
to_address.to_script_pub_key()
)
# 创建交易(启用 segwit)
tx = Transaction([txin], [txout], has_segwit=True)
print("\n未签名的交易:")
print(tx.serialize())
print("\nTxId:", tx.get_txid())
# 签名交易
sig = from_private_key.sign_taproot_input(
tx,
0,
scripts,
amounts
)
# 添加见证数据
tx.witnesses.append(TxWitnessInput([sig]))
# 获取签名后的交易
signed_tx = tx.serialize()
print("\n已签名的交易:")
print(signed_tx)
print("\n交易信息:")
print(f"从地址 (P2TR): {from_address.to_string()}")
print(f"到地址 (P2TR): {to_address.to_string()}")
print(f"发送金额: {amount_to_send} BTC")
print(f"手续费: {input_amount - amount_to_send} BTC")
print(f"交易大小: {tx.get_size()} bytes")
print(f"虚拟大小: {tx.get_vsize()} vbytes")
print("\n您可以在这里广播交易:")
print("https://mempool.space/testnet/tx/push")
if __name__ == "__main__":
main()
结果是这样的:
https://mempool.space/testnet/tx/a3b4d0382efd189619d4f5bd598b6421e709649b87532d53aecdc76457a42cb6
让我分析这笔Taproot到Taproot的交易结构和对应关系。Taproot是比特币最新的脚本类型,采用了Schnorr签名技术。
Taproot交易解析
这里我们看到从一个Taproot地址 (tb1pjyjeruun8pc5ln3wtv2d6lsxqn55frpyc83kn473h7848d0kj73sxy3ku8) 发送到另一个Taproot地址 (tb1p53ncq9ytax924ps66z6al3wfhy6a29w8h6xfu27xem06t98zkmvsakd43h)。
输入分析
输入部分:
地址:tb1pjyjeruun8pc5ln3wtv2d6lsxqn55frpyc83kn473h7848d0kj73sxy3ku8
前序输出脚本:
OP_PUSHNUM_1 OP_PUSHBYTES_32 912591f39338714fce2e5b14dd7e0604e9448c24c1e369d7d1bf8f53b5f697a3
前序输出类型:V1_P2TR
见证数据分析
这是Taproot的关键部分:
7d25fbc9b98ee0eb09ed38c2afc19127465b33d6120f4db8d4fd46e532e30450d7d2a1f1dd7f03e8488c434d10f4051741921d695a44fb774897020f41da99f3
与SegWit交易不同,Taproot的见证数据只有一个组件:单个Schnorr签名。这是Taproot的"密钥路径花费"方式。
Schnorr签名具有64字节的固定长度,没有DER编码格式,这使得交易更紧凑高效。
对应关系详解
从SegWit到Taproot的演变:
前序交易来自SegWit地址,输出到了第一个Taproot地址
SegWit使用了传统的ECDSA签名:
30440220212d4a6f059e1f9...
和公钥:02898711e6bf63f5...
Taproot则使用了单一的Schnorr签名:
7d25fbc9b98ee0eb...
Taproot的内部密钥:
Taproot地址中的32字节值(
912591f39338714fce2e5b14dd7e0604e9448c24c1e369d7d1bf8f53b5f697a3
)不是公钥哈希,而是直接的"输出密钥"这个输出密钥是内部密钥(可能与调整因子结合)的修改版本
验证过程:
比特币节点验证Taproot交易时:
提取输出密钥 (
912591f39338714f...
)使用Schnorr签名验证算法验证签名 (
7d25fbc9b98ee0eb...
)验证成功后允许花费
总结一下Taproot交易相比SegWit的主要编程区别:
# 1. 地址生成方式不同
# SegWit: P2wpkhAddress
from_address = P2wpkhAddress(...)
# Taproot: 需要先获取公钥,再获取Taproot地址
from_pub = from_private_key.get_public_key()
from_address = from_pub.get_taproot_address()
# 2. 签名方法改变
# SegWit: sign_segwit_input()
sig = private_key.sign_segwit_input(tx, 0, script_code, amount)
# Taproot: sign_taproot_input()
sig = private_key.sign_taproot_input(
tx,
0, # 输入索引
scripts, # 所有输入的脚本列表
amounts # 所有输入的金额列表
)
# 3. witness数据结构更简单
# SegWit: [signature, public_key]
tx.set_witness(0, [sig, pubkey])
# Taproot: 只需要签名
tx.witnesses.append(TxWitnessInput([sig]))
# 4. 交易创建时同样需要segwit标志
tx = Transaction([txin], [txout], has_segwit=True)
# 5. script_sig同样为空
txin.script_sig = Script([]) # 这点与SegWit相同
地址格式更简洁(以 'tb1p' 开头)
签名数据更精简(不需要公钥)
整体结构更优化

Last updated