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:

Shared Taproot Address#

  • Address:tb1pjfdm902y2adr08qnn4tahxjvp6x5selgmvzx63yfqk2hdey02yvqjcr29q

  • Feature: Five different spending methods using the same address

Script Tree Design#

                 Merkle Root
                /            \
        Branch0              Branch1
        /      \             /      \
   Script0   Script1    Script2   Script3
  (Hashlock) (Multisig)  (CSV)    (Sig)

Four script path details:

  1. 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]

  2. 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]

  3. 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

  4. Script 3 (simple signature): Bob can spend immediately with signature

    • Simplest script path

    • Witness data: [bob_sig, script, control_block]

  5. 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:

  1. Efficiency:

    • OP_CHECKSIGADD: Verify one by one, stop immediately on failure

    • OP_CHECKMULTISIG: Must check all possible signature combinations

  2. Simplified stack operations:

    • OP_CHECKSIGADD: Clear counter mechanism

    • OP_CHECKMULTISIG: Complex stack ops and off-by-one issues

  3. 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#

  1. Control block structure: Internal pubkey 50be5fc4..., sibling 1 fe78d852... (Script0), sibling 2 da551975... (Branch1).

  2. Merkle levels: Script1 → Branch0(Script0, Script1) → Root(Branch0, Branch1).

  3. Lexicographic order: TapBranch calculation must follow lexicographic sort.

  4. 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#

  1. 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.

  2. OP_CHECKSIGADD multisig: Tapscript’s counter-style multisig replaces traditional OP_CHECKMULTISIG—clearer stack ops, higher efficiency, native x-only pubkey support.

  3. CSV timelock sequence handling: TxInput’s sequence must match script’s OP_CHECKSEQUENCEVERIFY—an easily overlooked implementation detail.

  4. 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.