Chapter 8: Four-Leaf Taproot Script Tree#
Introduction: Leap from Theory to Practice#
In previous chapters we mastered Taproot fundamentals and dual-leaf implementation. However, real enterprise applications need more complex logic—four-leaf script trees represent the mainstream complexity of Taproot technology in practice.
Why Are Four-Leaf Script Trees So Important?#
Most Taproot applications stay at simple key path spending, maximizing privacy but leaving most smart contract potential undeveloped. Four-leaf trees demonstrate key capabilities missing from simple implementations:
Real-world application scenarios:
Wallet recovery: Progressive access control with timelock + multisig + emergency path
Lightning Network channels: Multiple cooperative close scenarios with different participant sets
Atomic swaps: Hash timelock contracts with various fallback conditions
Inheritance planning: Time-based access with multiple beneficiary options
Technical advantages:
Selective disclosure: Only expose executed script; others stay hidden
Fee efficiency: Smaller than equivalent traditional multi-condition scripts
Flexible logic: Multiple execution paths within single commitment
Real Case Study: Full Verification on Testnet#
Let’s analyze the actual structure of a four-leaf script tree implemented and verified on testnet:
Script Tree Design#
Merkle Root
/ \
Branch0 Branch1
/ \ / \
Script0 Script1 Script2 Script3
(Hashlock) (Multisig) (CSV) (Sig)
Four script path details:
Script 0 (SHA256 hash lock): Anyone who knows preimage “helloworld” can spend
Implements hash lock pattern in atomic swaps
Witness data: [preimage, script, control_block]
Script 1 (2-of-2 multisig): Requires Alice and Bob cooperation
Uses Tapscript’s efficient OP_CHECKSIGADD instead of traditional OP_CHECKMULTISIG
Witness data: [bob_sig, alice_sig, script, control_block]
Script 2 (CSV timelock): Bob can spend after 2 blocks
Implements relative timelock
Witness data: [bob_sig, script, control_block]
Key: Transaction input must set custom sequence value
Script 3 (simple signature): Bob can spend immediately with signature
Simplest script path
Witness data: [bob_sig, script, control_block]
Key Path: Alice uses tweaked private key for maximum privacy spending
Looks like ordinary single-sig transaction
Witness data: [alice_sig]
Deep Technical Implementation Analysis#
Building four-leaf tree with bitcoinutils: Initialize keys, define four scripts, then specify tree structure with nested list to generate address.
Python Implementation Framework#
alice_priv = PrivateKey("cRxebG1hY6vVgS9CSLNaEbEJaXkpZvc6nFeqqGT7v6gcW7MbzKNT")
bob_priv = PrivateKey.from_wif("cSNdLFDf3wjx1rswNL2jKykbVkC6o56o5nYZi4FUkWKjFn2Q5DSG")
alice_pub = alice_priv.get_public_key()
bob_pub = bob_priv.get_public_key()
Building the four scripts#
# Script 0: Hashlock (requires import hashlib)
hash0 = hashlib.sha256(b"helloworld").hexdigest()
script0 = Script(['OP_SHA256', hash0, 'OP_EQUALVERIFY', 'OP_TRUE'])
# Script 1: 2-of-2 Multisig
script1 = Script(["OP_0", alice_pub.to_x_only_hex(), "OP_CHECKSIGADD",
bob_pub.to_x_only_hex(), "OP_CHECKSIGADD", "OP_2", "OP_EQUAL"])
# Script 2: CSV timelock
seq = Sequence(TYPE_RELATIVE_TIMELOCK, 2)
script2 = Script([seq.for_script(), "OP_CHECKSEQUENCEVERIFY", "OP_DROP",
bob_pub.to_x_only_hex(), "OP_CHECKSIG"])
# Script 3: Simple signature
script3 = Script([bob_pub.to_x_only_hex(), "OP_CHECKSIG"])
Creating Taproot address#
Four-leaf tree specified as [[s0,s1],[s2,s3]] for Branch0 and Branch1:
tree = [[script0, script1], [script2, script3]]
taproot_address = alice_pub.get_taproot_address(tree)
# Four-leaf Taproot script tree (btcaaron)
# Reference: examples/ch08_four_leaf_tree.py
from btcaaron import Key, TapTree
alice = Key.from_wif("cRxebG1hY6vVgS9CSLNaEbEJaXkpZvc6nFeqqGT7v6gcW7MbzKNT")
bob = Key.from_wif("cSNdLFDf3wjx1rswNL2jKykbVkC6o56o5nYZi4FUkWKjFn2Q5DSG")
# Four-leaf: hashlock | 2of2 multisig | CSV timelock | bob checksig
program = (TapTree(internal_key=alice)
.hashlock("helloworld", label="hash")
.multisig(2, [alice, bob], label="2of2")
.timelock(blocks=2, then=bob, label="csv")
.checksig(bob, label="bob")
).build()
print("=== FOUR-LEAF TAPROOT TREE ===")
print(f"Address: {program.address}")
print(f"Leaves: {program.leaves}")
print(program.visualize())
# Five spending path examples
# 1. Hashlock
tx_h = program.spend("hash").from_utxo("245563c5aa4c6d32fc34eed2f182b5ed76892d13370f067dc56f34616b66c468", 0, sats=1200).to("tb1p060z97qusuxe7w6h8z0l9kam5kn76jur22ecel75wjlmnkpxtnls6vdgne", 666).unlock(preimage="helloworld").build()
print(f"Hashlock TXID: {tx_h.txid}")
# 2. 2of2
tx_m = program.spend("2of2").from_utxo("1ed5a3e97a6d3bc0493acc2aac15011cd99000b52e932724766c3d277d76daac", 0, sats=1400).to("tb1p060z97qusuxe7w6h8z0l9kam5kn76jur22ecel75wjlmnkpxtnls6vdgne", 668).sign(alice, bob).build()
print(f"2of2 Multisig TXID: {tx_m.txid}")
# 3. Key Path (Alice)
tx_k = program.keypath().from_utxo("42a9796a91cf971093b35685db9cb1a164fb5402aa7e2541ea7693acc1923059", 0, sats=2000).to("tb1p060z97qusuxe7w6h8z0l9kam5kn76jur22ecel75wjlmnkpxtnls6vdgne", 888).sign(alice).build()
print(f"Key Path TXID: {tx_k.txid}")
Core Implementation of Script Path Spending#
bitcoinutils key logic for five spending paths (see examples/ch08_* or btcaaron cell above for runnable code).
1. Hash lock#
Witness: [preimage, script, control_block]. TXID: 1ba4835f...
cb = ControlBlock(alice_pub, tree, 0, is_odd=taproot_address.is_odd())
tx.witnesses.append(TxWitnessInput(["helloworld".encode().hex(), script0.to_hex(), cb.to_hex()]))
2. Multisig (2-of-2)#
Witness order: Bob signature first (stack bottom, consumed second), Alice second.script_path=True。TXID: 1951a3be...
cb = ControlBlock(alice_pub, tree, 1, is_odd=taproot_address.is_odd())
# When signing: script_path=True, tapleaf_script=script1
tx.witnesses.append(TxWitnessInput([sig_bob, sig_alice, script1.to_hex(), cb.to_hex()]))
3. CSV timelock#
Key: TxInput(..., sequence=seq.for_input_sequence()). Witness: [sig_bob, script, cb]. TXID: 98361ab2...
txin = TxInput(commit_txid, vout, sequence=seq.for_input_sequence())
cb = ControlBlock(alice_pub, tree, 2, is_odd=taproot_address.is_odd())
tx.witnesses.append(TxWitnessInput([sig_bob, script2.to_hex(), cb.to_hex()]))
4. Simple signature#
Witness: [sig_bob, script, cb]. TXID: 1af46d4c...
cb = ControlBlock(alice_pub, tree, 3, is_odd=taproot_address.is_odd())
tx.witnesses.append(TxWitnessInput([sig_bob, script3.to_hex(), cb.to_hex()]))
5. Key Path (maximum privacy)#
script_path=False, provide only tapleaf_scripts=tree for tweak. Witness is just [sig_alice].TXID: 1e518aa5...
sig_alice = alice_priv.sign_taproot_input(..., script_path=False, tapleaf_scripts=tree)
tx.witnesses.append(TxWitnessInput([sig_alice]))
Multisig Stack Execution: OP_CHECKSIGADD Innovation#
In previous chapters we familiarized with single-sig stack execution. Four-leaf trees introduce 2-of-2 multisig scripts. We use Tapscript’s efficient OP_CHECKSIGADD. Let’s analyze its stack execution in detail.
Multisig script structure#
script1 = Script(["OP_0", alice_pub.to_x_only_hex(), "OP_CHECKSIGADD",
bob_pub.to_x_only_hex(), "OP_CHECKSIGADD", "OP_2", "OP_EQUAL"])
Witness order#
Key: Bob signature first (stack bottom, consumed second), Alice second (stack top, consumed first).
tx.witnesses.append(TxWitnessInput([sig_bob, sig_alice, script1.to_hex(), cb.to_hex()]))
Stack execution: How OP_CHECKSIGADD works#
Execution script:OP_0 [Alice_PubKey] OP_CHECKSIGADD [Bob_PubKey] OP_CHECKSIGADD OP_2 OP_EQUAL
Initial state: Witness data on stack#
Stack State (bottom to top):
│ sig_alice │ ← Stack top, consumed first
│ sig_bob │ ← Consumed second by OP_CHECKSIGADD
└─────────────--┘
1. OP_0: Initialize signature counter#
Stack State:
│ 0 │ ← Counter initial value
│ sig_alice │
│ sig_bob │
└─────────────┘
2. [Alice_PubKey]: Push Alice’s public key#
Stack State:
│ alice_pubkey│ ← Alice's 32-byte x-only public key
│ 0 │ ← Counter
│ sig_alice │
│ sig_bob │
└─────────────┘
3. OP_CHECKSIGADD: Verify Alice signature and increment counter#
Execution Process:
- Pop alice_pubkey
- Pop sig_alice (note: pop from lower layer)
- Verify signature: schnorr_verify(sig_alice, alice_pubkey, sighash)
- Pop counter 0
- Verification successful: push (0+1=1)
Stack State:
│ 1 │ ← Counter incremented to 1 ✅
│ sig_bob │
└─────────────┘
4. [Bob_PubKey]: Push Bob’s public key#
Stack State:
│ bob_pubkey │ ← Bob's 32-byte x-only public key
│ 1 │ ← Current counter value
│ sig_bob │
└─────────────┘
5. OP_CHECKSIGADD: Verify Bob signature again and increment counter#
Execution Process:
- Pop bob_pubkey
- Pop sig_bob
- Verify signature: schnorr_verify(sig_bob, bob_pubkey, sighash)
- Pop counter 1
- Verification successful: push (1+1=2)
Stack State:
│ 2 │ ← Counter incremented to 2 ✅
└─────────────┘
6. OP_2: Push required signature count#
Stack State:
│ 2 │ ← Required signature count
│ 2 │ ← Actual verified signature count
└─────────────┘
7. OP_EQUAL: Check if two values are equal#
Execution Process:
- Pop both 2s
- Compare: 2 == 2 is true
- Push 1 (indicating script execution success)
Final Stack State:
│ 1 │ ← Script execution success flag ✅
└─────────────┘
OP_CHECKSIGADD vs Traditional OP_CHECKMULTISIG#
Technical advantage comparison:
Efficiency:
OP_CHECKSIGADD: Verify one by one, stop immediately on failure
OP_CHECKMULTISIG: Must check all possible signature combinations
Simplified stack operations:
OP_CHECKSIGADD: Clear counter mechanism
OP_CHECKMULTISIG: Complex stack ops and off-by-one issues
Native x-only pubkey support:
OP_CHECKSIGADD: Direct 32-byte x-only pubkey support
OP_CHECKMULTISIG: Requires 33-byte compressed pubkey
Witness stack order#
OP_CHECKSIGADD consumes stack top (alice_sig) first, then bottom (bob_sig), so witness must be [sig_bob, sig_alice, ...]. ❌ [sig_alice, sig_bob, ...] fails verification.
Four-Leaf Control Block Extension#
Four-leaf control block is 97 bytes, containing two-level Merkle proof. Example transaction 1951a3be0f05df377b1789223f6da66ed39c781aaf39ace0bf98c3beb7e604a1 (Script 1 multisig):
Witness stack: Bob sig → Alice sig → multisig script → 97-byte control block.
Control block byte layout:
Byte 0: version (0xc0) + parity
Bytes 1–32: internal pubkey (Alice x-only)
Bytes 33–64: sibling 1 (Script 0’s TapLeaf hash)
Bytes 65–96: sibling 2 (Branch 1’s TapBranch hash)
Merkle hierarchy: Script1_TapLeaf + sibling1 → Branch0; Branch0 + sibling2 → Root; TapTweak(internal ‖ root) → output pubkey.
paths#
paths = {
0: "[Script1_TapLeaf, Branch1_TapBranch]", # Hashlock
1: "[Script0_TapLeaf, Branch1_TapBranch]", # Multisig
2: "[Script3_TapLeaf, Branch0_TapBranch]", # CSV
3: "[Script2_TapLeaf, Branch0_TapBranch]" # Simple Sig
}
Byte parsing#
cb_bytes = bytes.fromhex(cb_hex)
internal_pubkey = cb_bytes[1:33].hex()
sibling_1 = cb_bytes[33:65].hex()
sibling_2 = cb_bytes[65:97].hex()
Key technical insights#
Control block structure: Internal pubkey
50be5fc4..., sibling 1fe78d852...(Script0), sibling 2da551975...(Branch1).Merkle levels: Script1 → Branch0(Script0, Script1) → Root(Branch0, Branch1).
Lexicographic order: TapBranch calculation must follow lexicographic sort.
Verification chain: Control block proves script belongs to original Taproot commitment.
# Runnable: Parse 97-byte control block of tx 1951a3be... (stdlib only)
cb_hex = "c050be5fc44ec580c387bf45df275aaa8b27e2d7716af31f10eeed357d126bb4d3fe78d8523ce9603014b28739a51ef826f791aa17511e617af6dc96a8f10f659eda55197526f26fa309563b7a3551ca945c046e5b7ada957e59160d4d27f299e3"
cb = bytes.fromhex(cb_hex)
print(f"Control block length: {len(cb)} bytes")
print(f"Internal pubkey: {cb[1:33].hex()[:16]}...")
print(f"Sibling 1: {cb[33:65].hex()[:16]}...")
print(f"Sibling 2: {cb[65:97].hex()[:16]}...")
Common Programming Pitfalls and Solutions#
1. Witness stack order#
Multisig witness order is critical:
# ❌ Wrong: Alice signature first
witness = [sig_alice, sig_bob, script, control_block]
# ✅ Correct: Bob signature first (consumed second)
witness = [sig_bob, sig_alice, script, control_block]
2. CSV script sequence value#
CSV script needs specific transaction sequence value:
# ❌ Wrong: Default sequence
txin = TxInput(txid, vout)
# ✅ Correct: CSV-compatible sequence
txin = TxInput(txid, vout, sequence=seq.for_input_sequence())
3. Script Path vs Key Path signing#
The two paths have different signing processes:
# Key path: script_path=False, provide tree for tweak
sig = priv.sign_taproot_input(..., script_path=False, tapleaf_scripts=tree)
# Script path: script_path=True, provide specific script
sig = priv.sign_taproot_input(..., script_path=True, tapleaf_script=script)
Conclusion: Chapter Technical Summary#
This chapter extends previous fundamentals to near-production complexity through complete four-leaf Taproot implementation.
Chapter Key Takeaways#
Four-leaf two-level proof structure: Control block expands from 65 (dual-leaf) to 97 bytes, with two sibling hashes forming complete two-level Merkle proof chain.
OP_CHECKSIGADD multisig: Tapscript’s counter-style multisig replaces traditional OP_CHECKMULTISIG—clearer stack ops, higher efficiency, native x-only pubkey support.
CSV timelock sequence handling:
TxInput’ssequencemust match script’sOP_CHECKSEQUENCEVERIFY—an easily overlooked implementation detail.Five spending paths share one address: Five real testnet transactions verify four-leaf correctness; Key Path spending is indistinguishable from ordinary single-sig on-chain.
Limitations#
This chapter’s four-leaf tree uses balanced structure. In practice, place high-probability scripts in shallow tree layers to reduce Merkle proof size and fees.
Hash lock script still ends with
OP_TRUE(see Ch6 security note); production should bind signature verification.Elliptic curve operations in verification code are library-internal; underlying implementation not shown.
Next Steps#
With four-leaf trees mastered, we have the foundation for understanding more complex Bitcoin protocols. Subsequent chapters will explore these techniques in Lightning Network, Ordinals, and other real protocols.