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
  1. 第一部分是签名:

    • 3044 - DER格式的签名标记

    • 0220212d4a6f059e1f9586b99fc61575eb2f66bc273c902b04b19efcb1344a535cf5 - r值

    • 02202ec7d216c3519e516af0cb01edf9712c602b0eb3811238fd9d16a93b4a543a01 - s值

    • 01 - 哈希类型 (SIGHASH_ALL)

  2. 第二部分是公钥:

    • 02898711e6bf63f5cbe1b38c05e89d6c391c59e9f8f695da44bf3d20ca674c8519

    • 这是一个压缩格式的公钥 (33字节,以02开头表示y坐标是偶数)

公钥与公钥哈希的关系

这里有个重要的对应关系:

  • 公钥: 02898711e6bf63f5cbe1b38c05e89d6c391c59e9f8f695da44bf3d20ca674c8519

  • 公钥哈希: c5b28d6bba91a2693a9b1876bcd3929323890fb2

公钥哈希是通过以下步骤计算的:

  1. 对公钥进行SHA-256哈希

  2. 对结果再进行RIPEMD-160哈希

  3. 结果就是c5b28d6bba91a2693a9b1876bcd3929323890fb2,这个值用于:

    • Legacy地址的scriptPubKey: 76a914c5b28d6bba91a2693a9b1876bcd3929323890fb288ac

    • SegWit地址的scriptPubKey: 0014c5b28d6bba91a2693a9b1876bcd3929323890fb2

考考你,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编码格式,这使得交易更紧凑高效。

对应关系详解

  1. 从SegWit到Taproot的演变

    • 前序交易来自SegWit地址,输出到了第一个Taproot地址

    • SegWit使用了传统的ECDSA签名:30440220212d4a6f059e1f9...和公钥:02898711e6bf63f5...

    • Taproot则使用了单一的Schnorr签名:7d25fbc9b98ee0eb...

  2. Taproot的内部密钥

    • Taproot地址中的32字节值(912591f39338714fce2e5b14dd7e0604e9448c24c1e369d7d1bf8f53b5f697a3)不是公钥哈希,而是直接的"输出密钥"

    • 这个输出密钥是内部密钥(可能与调整因子结合)的修改版本

  3. 验证过程

    • 比特币节点验证Taproot交易时:

      1. 提取输出密钥 (912591f39338714f...)

      2. 使用Schnorr签名验证算法验证签名 (7d25fbc9b98ee0eb...)

      3. 验证成功后允许花费

总结一下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