Chapter 4: Building SegWit Transactions#
Segregated Witness (SegWit) fundamentally restructured Bitcoin transactions by separating signature data from transaction data. This chapter demonstrates SegWit’s core innovations through complete transaction implementation—from creation to broadcast—tracing the witness execution model that makes Taproot possible using real testnet data.
# Chapter environment: bitcoinutils (load once, reuse in subsequent code cells)
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')
4.1 Transaction Malleability: What SegWit Solved#
Legacy vs SegWit Transaction Structure#
Legacy Bitcoin transactions bundle all data together for TXID calculation; SegWit separates witness data:
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)
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘
Malleability Problem Demonstration#
Before SegWit, attackers could modify signature encoding without affecting validity, but changing the TXID. ECDSA uses DER format which allows multiple valid encodings of the same signature. For example:
Original signature:
304402201234567890abcdef...(71 bytes)Malleable version:
3045022100001234567890abcdef...(72 bytes, with zero-padding)
Both signatures are cryptographically identical and pass ECDSA verification, but have different byte representations. Since Legacy includes the entire scriptSig in TXID calculation, these different encodings produce different transaction IDs for the same economic transaction.
This malleability broke protocols relying on specific TXIDs, especially Lightning Network:
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
Legacy vs SegWit Key Code#
# Legacy: sig in scriptSig, participates in TXID
sig = sk.sign_input(tx, 0, previous_locking_script)
txin.script_sig = Script([sig, pk])
# SegWit: sig in witness, does not participate in 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()]))
# Example 1: Legacy vs SegWit signature comparison
# Reference: code/chapter04/01_legacy_vs_segwit_comparison.py
sk = PrivateKey('cPeon9fBsW2BxwJTALj3hGzh9vm8C52Uqsce7MzXGS1iFJkPF4AT')
# Legacy: sig in 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 in 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 contains signature; SegWit witness contains signature, scriptSig empty")
4.2 Creating a Complete SegWit Transaction#
Let’s build a real SegWit transaction step by step to understand how SegWit solves malleability.
Transaction Setup#
# Example 2: SegWit transaction setup
# Reference: 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()}")
Output:
From: tb1qckeg66a6jx3xjw5mrpmte5ujjv3cjrajtvm9r4
To: tb1qckeg66a6jx3xjw5mrpmte5ujjv3cjrajtvm9r4
Note: This example uses a real transaction successfully broadcast to testnet. TXID: 271cf6285479885a5ffa4817412bfcf55e7d2cf43ab1ede06c4332b46084e3e6, viewable on testnet explorers.
4.3 SegWit Transaction Construction and Analysis#
Step-by-Step Transaction Building#
Let’s build the transaction step by step, observing data structure changes at each stage:
Stage 1: Create Unsigned Transaction#
# Example 3: Create SegWit transaction (Stages 1 and 2)
# Reference: 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"Unsigned: {len(unsigned_tx)//2} bytes; Signed: {len(signed_tx)//2} bytes")
print("TXID: 271cf6285479885a5ffa4817412bfcf55e7d2cf43ab1ede06c4332b46084e3e6")
Unsigned transaction output:
0200000000010148bcdd9dfa3749b74a1390d7bd272197e2588011abfb3303717d416f8e4354140000000000fdffffff019a02000000000000160014c5b28d6bba91a2693a9b1876bcd3929323890fb200000000
Parsed components:
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
Key observations:
Standard Bitcoin transaction structure
ScriptSig empty (
00)—normal for SegWitNo witness data yet
Stage 2: Add SegWit Signature#
Stage 2 output:
ScriptSig: ''
Witness: [3044022015098d26...e33c0301 (sig), 02898711e6bf...c8519 (pk)]
Signed TX: 191 bytes
Verified transaction: Successfully broadcast to testnet. TXID: 271cf6285479885a5ffa4817412bfcf55e7d2cf43ab1ede06c4332b46084e3e6
Key changes:
ScriptSig remains empty
Witness data appears
Transaction grows (witness section added)
Transaction Structure Comparison#
Before signing (Stage 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)
After signing (Stage 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)
Note: Sequence 0xfffffffd enables RBF (Replace-By-Fee), allowing replacement with higher fees. This is why explorers show ‘RBF’ in transaction features.
Note: Marker/flag (00 01) appears only in serialized form to indicate SegWit; not included in txid (they are in wtxid).
Raw Transaction Parsed Components#
Fully signed transaction with labeled components:
[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
# Runnable: Parse SegWit transaction key fields (stdlib)
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}")
4.4 P2WPKH Stack Execution Analysis#
ScriptPubKey: 0014c5b28d6b...890fb2 (OP_0 + 20B hash)
Witness: [3044022015098d26...e33c0301 (sig), 02898711e6bf...c8519 (pk)]
Equivalent script: OP_DUP OP_HASH160 <hash> OP_EQUALVERIFY OP_CHECKSIG
Stack Execution (Brief)#
│ (empty) │ → witness push → │ 02898711e6bf...c8519 (pk) │
│ 304402201509...33c0301 (sig) │
└───────────────────────────────────┘
→ OP_DUP → OP_HASH160 → hash match → OP_CHECKSIG → │ 1 (TRUE) │
txid excludes witness; wtxid includes witness. Miners commit via witness commitment in coinbase.
4.5 SegWit to Taproot Evolution#
SegWit established the architectural foundation for Taproot through three key innovations:
Witness Version Framework#
Version 0: P2WPKH (OP_0 <20-bytes>) and P2WSH (OP_0 <32-bytes>)
Version 1: P2TR (OP_1 <32-bytes>) - Taproot
Malleability Resistance#
Stable transaction IDs enable:
Lightning Network channel
Complex pre-signed transaction chains
Reliable Layer 2 protocols
Economic Incentives#
SegWit introduced weight-based fee calculation:
Transaction Weight = (Base Size × 4) + Witness Size
Virtual Size = Weight ÷ 4
Intuition: Witness bytes charged at 1 weight unit/byte, base bytes at 4 wu/byte. Savings depend on how much auth data moves to witness (structure-dependent, not fixed 25%/75%).
Space efficiency through separation:
In Legacy transactions, 2-of-3 multisig requires large scriptSig:
scriptSig: <empty> <sig1> <sig2> <redeemScript>
Total: ~300 bytes in scriptSig (counted at full weight)
In SegWit P2WSH, the same authorization moves to witness:
scriptSig: <empty> (0 bytes)
witness: <empty> <sig1> <sig2> <witnessScript>
Total: ~300 bytes in witness (75% discount)
This architectural change makes complex scripts economically viable. SegWit multisig pays ~25% less than Legacy equivalent; simple single-sig saves less.
Taproot amplifies: SegWit’s economic framework laid groundwork for Taproot’s greater efficiency. Taproot can make complex multi-party arrangements as cheap as single-sig on-chain via key aggregation, fully leveraging SegWit’s witness discount structure.
Chapter Summary#
This chapter demonstrated SegWit’s core innovations through complete transaction implementation:
Witness structure: Separating signature data from transaction logic created foundation for Taproot’s script trees and key-aggregation single-sig on-chain.
Malleability resistance: Stable transaction IDs enable Taproot-optimized Layer 2 ecosystems with more efficient authorization schemes.
Stack execution model: SegWit interpreter’s pattern recognition for witness programs provides template for Taproot’s OP_1 execution model.
Economic framework: Weight unit discounts create incentives for advanced script design; Taproot maximizes these via signature aggregation and script tree efficiency.
Understanding SegWit’s witness architecture and execution model is essential for mastering Taproot, as P2TR builds directly on these foundations while adding Schnorr signatures, key aggregation, and Merkle script trees.
In the next chapter, we’ll explore how Schnorr signatures provide the mathematical primitives enabling Taproot’s key aggregation and signature efficiency, creating Bitcoin’s most advanced authorization scheme on SegWit’s witness architecture.