第4章:构建 SegWit 交易#
参考:
code/chapter04/
最后更新: 2026-06-16
隔离见证(Segregated Witness,SegWit)把签名数据从交易的其余部分里分离出来。本章从头到尾构建一笔真实的 SegWit 交易——构造、签名、序列化、追踪执行——用 testnet 数据,展示这种分离如何修掉可锻性(malleability),并搭起 Taproot 后面要依赖的见证模型。
# 本章环境:bitcoinutils(一次性加载,后续代码块复用)
from bitcoinutils.setup import setup
from bitcoinutils.keys import PrivateKey, P2pkhAddress, P2wpkhAddress
from bitcoinutils.transactions import Transaction, TxInput, TxOutput, TxWitnessInput
from bitcoinutils.utils import to_satoshis
from bitcoinutils.script import Script
setup('testnet')
'testnet'
4.1 交易可锻性:SegWit 解决的问题#
传统交易结构 vs SegWit#
传统交易把一切哈希在一起得到 TXID。SegWit 把见证移出这个哈希:
Legacy Transaction Structure:
┌─────────────────────────────────────────┐
│ Version │ Inputs │ Outputs │ Locktime │
│ │ ┌─────┐│ │ │
│ │ │ScSig││ │ │ } All included in TXID
│ │ │ ││ │ │
│ │ └─────┘│ │ │
└─────────────────────────────────────────┘
↓
TXID = SHA256(SHA256(entire_transaction))
SegWit Transaction Structure:
┌─────────────────────────────────────────┐
│ Version │ Inputs │ Outputs │ Locktime │ } Base Transaction
│ │ ┌─────┐│ │ │
│ │ │Empty││ │ │
│ │ │ScSig││ │ │
│ │ └─────┘│ │ │
└─────────────────────────────────────────┘
} TXID = SHA256(SHA256(base_only))
┌─────────────────────────────────────────┐
│ Witness Data (Separated) │ } Committed separately
│ ┌─────────────────────────────────┐ │
│ │ Signature │ Public Key │ │ (For P2WPKH)
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘
正是这一处改动——见证数据移出 TXID——消除了可锻性。下面说明旧结构当初为什么会可锻。
可锻性问题#
在 SegWit 之前,第三方可以在不破坏签名的前提下给它重新编码,而这样做会改变交易的 TXID。ECDSA 签名用 DER(Distinguished Encoding Rules)序列化,同一个签名有不止一种合法的 DER 编码。例如:
原始签名:
304402201234567890abcdef...(71 字节)可锻版本:
3045022100001234567890abcdef...(72 字节,补了零)
两种编码验证出来是同一个签名,但字节不同。由于传统比特币把整个 scriptSig——签名也在里面——折进 TXID,这两种编码就让同一笔经济交易有了两个不同的 TXID。
这会破坏任何把后续交易钉在前一笔 TXID 上的协议——闪电网络尤甚:
Lightning Channel Setup:
Funding TX (TXID_A) → Commitment TX → Timeout TX
↓ ↓
References References
TXID_A TXID_B
If TXID_A changes due to malleability:
→ Commitment TX becomes invalid
→ Timeout TX becomes invalid
→ Entire channel unusable
像这样的预签名交易链,假定 funding 的 TXID 是固定的。一旦它被锻改,建在它之上的每一笔交易都成了孤儿。
Legacy 与 SegWit 代码对比#
这个改动直接体现在你怎么签名上。legacy P2PKH 把签名放进 scriptSig,参与 TXID;SegWit P2WPKH 把 scriptSig 留空,签名进见证:
# Legacy: sig 在 scriptSig,参与 TXID
sig = sk.sign_input(tx, 0, previous_locking_script)
txin.script_sig = Script([sig, pk])
# SegWit: sig 在 witness,不参与 TXID
script_code = public_key.get_address().to_script_pub_key()
sig = private_key.sign_segwit_input(tx, 0, script_code, amount)
txin.script_sig = Script([])
tx.witnesses.append(TxWitnessInput([sig, public_key.to_hex()]))
两个细节要紧:你调 sign_segwit_input(不是 sign_input),而且 script_code 取自这把密钥的 legacy P2PKH 形式、输入金额现在也是被签的一部分。两者都是 BIP143 的要求,等 §4.4 追踪执行时就能看到它们各自为什么出现。
# 示例 1: Legacy vs SegWit 签名对比
# 参考: code/chapter04/01_legacy_vs_segwit_comparison.py
sk = PrivateKey('cPeon9fBsW2BxwJTALj3hGzh9vm8C52Uqsce7MzXGS1iFJkPF4AT')
# Legacy: sig 在 scriptSig
prev = Script(["OP_DUP","OP_HASH160", sk.get_public_key().get_address().to_hash160(), "OP_EQUALVERIFY","OP_CHECKSIG"])
tx_legacy = Transaction([TxInput('5e4a294028ea8cb0e156dac36f4444e2c445c7b393e87301b12818b06cee49e0', 0)],
[TxOutput(to_satoshis(0.00000866), P2pkhAddress('myYHJtG3cyoRseuTwvViGHgP2efAvZkYa4').to_script_pub_key())])
sig = sk.sign_input(tx_legacy, 0, prev)
tx_legacy.inputs[0].script_sig = Script([sig, sk.get_public_key().to_hex()])
# SegWit: sig 在 witness
pk = sk.get_public_key()
script_code = pk.get_address().to_script_pub_key()
txin = TxInput('1454438e6f417d710333fbab118058e2972127bdd790134ab74937fa9dddbc48', 0)
txout = TxOutput(to_satoshis(0.00000666), P2wpkhAddress('tb1qckeg66a6jx3xjw5mrpmte5ujjv3cjrajtvm9r4').to_script_pub_key())
tx_sw = Transaction([txin], [txout], has_segwit=True)
sig_sw = sk.sign_segwit_input(tx_sw, 0, script_code, to_satoshis(0.00001))
txin.script_sig = Script([])
tx_sw.witnesses.append(TxWitnessInput([sig_sw, pk.to_hex()]))
print("Legacy scriptSig 含签名;SegWit witness 含签名,scriptSig 为空")
Legacy scriptSig 含签名;SegWit witness 含签名,scriptSig 为空
4.2 构建一笔完整的 SegWit 交易#
我们一步步构建一笔真实的 SegWit 交易,盯着每个阶段字节的变化。
交易准备#
# 示例 2: SegWit 交易设置
# 参考: code/chapter04/02_create_segwit_transaction.py
sk = PrivateKey('cPeon9fBsW2BxwJTALj3hGzh9vm8C52Uqsce7MzXGS1iFJkPF4AT')
pk = sk.get_public_key()
script_code = pk.get_address().to_script_pub_key()
to_addr = P2wpkhAddress('tb1qckeg66a6jx3xjw5mrpmte5ujjv3cjrajtvm9r4')
utxo_txid = '1454438e6f417d710333fbab118058e2972127bdd790134ab74937fa9dddbc48'
txin = TxInput(utxo_txid, 0)
txout = TxOutput(to_satoshis(0.00000666), to_addr.to_script_pub_key())
tx = Transaction([txin], [txout], has_segwit=True)
print(f"From: {to_addr.to_string()}\nTo: {to_addr.to_string()}")
输出:
From: tb1qckeg66a6jx3xjw5mrpmte5ujjv3cjrajtvm9r4
To: tb1qckeg66a6jx3xjw5mrpmte5ujjv3cjrajtvm9r4
这里 from 和 to 是同一个地址——这是一笔自转账,好让例子只用一把密钥。
注: 这是一笔成功广播过的真实 testnet 交易,TXID 是 271cf6285479885a5ffa4817412bfcf55e7d2cf43ab1ede06c4332b46084e3e6,可以在 testnet 浏览器上查看。
4.3 SegWit 交易构造与分析#
我们一步步构建交易,盯着每个阶段字节的变化。
阶段 1:创建未签名交易#
# 示例 3: 创建 SegWit 交易(阶段1和阶段2)
# 参考: code/chapter04/02_create_segwit_transaction.py
sk = PrivateKey('cPeon9fBsW2BxwJTALj3hGzh9vm8C52Uqsce7MzXGS1iFJkPF4AT')
pk = sk.get_public_key()
script_code = pk.get_address().to_script_pub_key()
to_addr = P2wpkhAddress('tb1qckeg66a6jx3xjw5mrpmte5ujjv3cjrajtvm9r4')
utxo_txid = '1454438e6f417d710333fbab118058e2972127bdd790134ab74937fa9dddbc48'
utxo_amount = 1000
txin = TxInput(utxo_txid, 0)
txout = TxOutput(to_satoshis(0.00000666), to_addr.to_script_pub_key())
tx = Transaction([txin], [txout], has_segwit=True)
unsigned_tx = tx.serialize()
sig = sk.sign_segwit_input(tx, 0, script_code, to_satoshis(utxo_amount / 100000000))
txin.script_sig = Script([])
tx.witnesses.append(TxWitnessInput([sig, pk.to_hex()]))
signed_tx = tx.serialize()
print(f"未签名: {len(unsigned_tx)//2} bytes; 签名后: {len(signed_tx)//2} bytes")
print("TXID: 271cf6285479885a5ffa4817412bfcf55e7d2cf43ab1ede06c4332b46084e3e6")
未签名: 84 bytes; 签名后: 191 bytes
TXID: 271cf6285479885a5ffa4817412bfcf55e7d2cf43ab1ede06c4332b46084e3e6
未签名交易输出:
0200000000010148bcdd9dfa3749b74a1390d7bd272197e2588011abfb3303717d416f8e4354140000000000fdffffff019a02000000000000160014c5b28d6bba91a2693a9b1876bcd3929323890fb200000000
解析组件:
Version: 02000000
Marker: 00 (SegWit indicator)
Flag: 01 (SegWit version)
Input Count: 01
TXID: 1454438e6f417d710333fbab118058e2972127bdd790134ab74937fa9dddbc48
VOUT: 00000000
ScriptSig: 00 (empty, 0 bytes)
Sequence: fffffffd (RBF enabled - Replace-By-Fee)
Output Count: 01
Value: 9a02000000000000 (666 sats)
Script Len: 16 (22 bytes)
ScriptPubKey: 0014c5b28d6bba91a2693a9b1876bcd3929323890fb2
Locktime: 00000000
哪怕还没签名,交易也已经带着 SegWit marker/flag(00 01)和一个空的 scriptSig。还没有见证——那是下一阶段的事。
阶段 2:加上 SegWit 签名#
阶段 2 输出:
ScriptSig: ''
Witness: [3044022015098d26...e33c0301 (sig), 02898711e6bf...c8519 (pk)]
Signed TX: 191 bytes
scriptSig 仍然是空的;一切授权这笔花费的东西,现在都在末尾追加的见证段里。
已验证的交易: 此交易已成功广播到 testnet。TXID: 271cf6285479885a5ffa4817412bfcf55e7d2cf43ab1ede06c4332b46084e3e6
交易结构:签名前后#
签名前(阶段 1):
Standard Bitcoin Transaction Format (with SegWit marker/flag)
├── Version: 02000000
├── Marker: 00 (SegWit indicator)
├── Flag: 01 (SegWit version)
├── Input Count: 01
├── Input Data: 48bcdd9d...00fdffffff (ScriptSig empty)
├── Output Count: 01
├── Output Data: 9a020000...3890fb2
└── Locktime: 00000000
Total: 84 bytes (base transaction)
签名后(阶段 2):
SegWit Transaction Format
├── Version: 02000000
├── Marker: 00 (SegWit indicator)
├── Flag: 01 (SegWit version)
├── Input Count: 01
├── Input Data: 48bcdd9d...00fdffffff (ScriptSig still empty)
├── Output Count: 01
├── Output Data: 9a020000...3890fb2
├── Witness Data: 0247304402...c8519 (NEW - authorization data)
└── Locktime: 00000000
Total: 191 bytes (added witness section: 82 bytes)
注: Sequence 0xfffffffd 开启了 RBF(Replace-By-Fee),所以这笔交易之后能被一笔更高费率的版本替换——这也是浏览器给它打 “RBF” 标的原因。
注: marker/flag(00 01)只出现在序列化形式里,用来标明 SegWit。它们不参与 txid(它们参与 wtxid)。
原始交易逐字段拆解#
带标签组件的完整签名交易:
[VERSION] 02000000
[MARKER] 00 (SegWit indicator)
[FLAG] 01 (SegWit version)
[INPUT_COUNT] 01
[TXID] 1454438e6f417d710333fbab118058e2972127bdd790134ab74937fa9dddbc48
[VOUT] 00000000
[SCRIPTSIG_LEN] 00 (Empty - authorization moved to witness)
[SEQUENCE] fffffffd
[OUTPUT_COUNT] 01
[VALUE] 9a02000000000000 (666 satoshis)
[SCRIPT_LEN] 16 (22 bytes)
[SCRIPTPUBKEY] 0014c5b28d6bba91a2693a9b1876bcd3929323890fb2
[WITNESS_ITEMS] 02 (2 items: signature + public key)
[SIG_LEN] 47 (71 bytes)
[SIGNATURE] 3044022015098d26...e33c0301
[PK_LEN] 21 (33 bytes)
[PUBLIC_KEY] 02898711e6bf...c8519
[LOCKTIME] 00000000
# 可运行:解析 SegWit 交易关键字段(标准库)
import struct
tx_hex = "0200000000010148bcdd9dfa3749b74a1390d7bd272197e2588011abfb3303717d416f8e4354140000000000fdffffff019a02000000000000160014c5b28d6bba91a2693a9b1876bcd3929323890fb202473044022015098d26918b46ab36b0d1b50ee502b33d5c5b5257c76bd6d00ccb31452c25ae0220256e82d4df10981f25f91e5273be39fced8fe164434616c94fa48f3549e33c03012102898711e6bf63f5cbe1b38c05e89d6c391c59e9f8f695da44bf3d20ca674c851900000000"
b = bytes.fromhex(tx_hex)
ver = struct.unpack("<I", b[0:4])[0]
marker, flag = b[4], b[5]
nin = b[6]
print(f"Version: {ver}, Marker: {marker:02x}, Flag: {flag:02x}, Inputs: {nin}")
print(f"SegWit: {marker == 0 and flag == 1}")
Version: 2, Marker: 00, Flag: 01, Inputs: 1
SegWit: True
4.4 P2WPKH 栈执行#
现在把这笔花费送进脚本解释器追踪一遍,用真实交易的数据。
ScriptPubKey: 0014c5b28d6b...890fb2(OP_0 + 20B hash)
Witness: [3044022015098d26...e33c0301 (sig), 02898711e6bf...c8519 (pk)]
一段 P2WPKH 锁定脚本就是 OP_0 <20 字节哈希>。Bitcoin Core 看到这个模式——见证版本 0、一个 20 字节的程序——并不会跑普通脚本。它识别出 P2WPKH,然后对见证项跑等价的 P2PKH 检查:
等价脚本:OP_DUP OP_HASH160 <hash> OP_EQUALVERIFY OP_CHECKSIG
正是这种识别让 SegWit 保持向后兼容。一个 legacy 节点看到 OP_0 <20 字节>,把它读成 anyone-can-spend,于是不理解也照样转发;一个 SegWit 节点识别出这个模式,执行见证规则。同样的机制,扩展到版本 1、32 字节程序(OP_1 <32 字节>),正是从第 5 章起 Taproot 输出被识别的方式。
栈执行简要#
│ (empty) │ → 见证压栈 → │ 02898711e6bf...c8519 (pk) │
│ 304402201509...33c0301 (sig) │
└───────────────────────────────────┘
→ OP_DUP → OP_HASH160 → 哈希匹配 → OP_CHECKSIG → │ 1 (TRUE) │
这笔花费通过了:见证版本 0、公钥哈希等于那个 20 字节程序、签名验证通过——而且因为 TXID 排除了见证,这笔交易不再可锻。
SegWit 给一笔交易两个标识符:txid(base 交易的哈希,不含见证)和 wtxid(含见证)。矿工通过一个见证承诺——wtxid 的 Merkle 根,放在 coinbase 里——提交整个区块的见证数据。
4.5 从 SegWit 到 Taproot#
SegWit 立下的三样东西,是 Taproot 直接接着用的。
见证版本框架#
SegWit 按版本和程序长度定义输出,给后来的版本留了位子:
Version 0: P2WPKH (OP_0 <20-bytes>) and P2WSH (OP_0 <32-bytes>)
Version 1: P2TR (OP_1 <32-bytes>) - Taproot
Taproot 就是见证版本 1 加一个 32 字节程序——本章追踪的这个框架里的下一个槽位。
抗可锻性#
稳定的 TXID 才让预签名交易链可以安全地搭建——闪电通道和其他二层协议都依赖它。
基于权重的费用#
SegWit 给见证字节的计费低于 base 字节:
Transaction Weight = (Base Size × 4) + Witness Size
Virtual Size = Weight ÷ 4
base 字节每个算 4 个权重单位;见证字节每个算 1 个。所以把授权数据移进见证会降低它的权重。你实际省多少,取决于交易里有多少是授权数据——这取决于结构,不是一个固定百分比。
对一个 2-of-3 多签来说差别很大,因为授权本身就大。在 legacy 形式里它待在 scriptSig 里,按满权重计:
scriptSig: OP_0 <sig1> <sig2> <redeemScript>
Total: ~300 bytes in scriptSig (counted at full weight)
在 SegWit P2WSH 里,同样的数据移到见证,每个字节只算四分之一:
scriptSig: <empty> (0 bytes)
witness: OP_0 <sig1> <sig2> <witnessScript>
Total: ~300 bytes in witness (charged at 1 wu/byte)
一笔交易里授权数据占比越大,见证折扣帮得越多——这就是为什么复杂脚本受益最大。Taproot 把这一点又推进了一步:靠密钥聚合,一笔多方花费可以只在链上放一个 64 字节签名,付的费接近单签交易。那是第 5 章起的事。
章节总结#
我们把一笔 SegWit 交易从构造一路做到执行,看到了延续进 Taproot 的四样东西:
见证结构 —— 把签名从交易主体里分出来,正是脚本树和密钥聚合后面所依赖的那个切分。
抗可锻性 —— 把见证移出 TXID,稳住了交易 ID,这是预签名二层协议所要求的。
按模式执行 —— 解释器识别
OP_0 <20 字节>并跑等价的 P2PKH 检查;Taproot 的OP_1 <32 字节>是同一个想法往上一个版本。基于权重的费用 —— 给见证字节更低的计费,奖励把授权数据移出 base 交易。
P2TR 直接建在这四样之上,再加 Schnorr 签名、密钥聚合、Merkle 脚本树。
下一章从 Schnorr 签名讲起——它的线性性让密钥聚合和 key tweak 成为可能,是 Taproot 的第一块。