Chapter 7: Taproot Dual-Leaf Script Tree#


From Single to Dual Leaf: The True Power of Taproot Script Trees#

In the previous chapter we mastered single-leaf Taproot Script Path via Alice’s hash lock contract. However, Taproot’s true power lies in its multi-branch script tree architecture—elegantly organizing multiple spending conditions within one address for complex conditional logic.

Imagine: Alice wants a digital escrow contract supporting secret-based auto-unlock (hash lock) and direct private key control for Bob. In traditional Bitcoin this requires complex multisig or multiple addresses. Taproot’s dual-leaf script tree elegantly integrates both into one address:

  • Script Path 1: Hash lock script, anyone who knows “helloworld” can spend

  • Script Path 2: Bob script, only Bob’s private key holder can spend

  • Key Path: Alice as internal key holder can spend directly (maximum privacy)

The elegance: external observers cannot distinguish simple payment from complex three-path conditional contract. Only when spent is the used path selectively revealed.

Unlike single-leaf trees where TapLeaf hash is Merkle root, dual-leaf script trees require building a real Merkle tree:

        Merkle Root
       /           \
  TapLeaf A    TapLeaf B
(Hash Script) (Bob Script)

Key implementation points:

  1. TapLeaf hash computation: Each script computes its TapLeaf hash separately

  2. TapBranch hash: Compute after lexicographically sorting two TapLeaf hashes

  3. Control block construction: Each script needs sibling hash as Merkle proof

Let’s understand how this works through actual on-chain transaction data.

We’ll analyze the complete dual-leaf implementation based on two real testnet transactions:

Transaction 1: Hash script path spending#

  • Transaction ID:b61857a05852482c9d5ffbb8159fc2ba1efa3dd16fe4595f121fc35878a2e430

  • Taproot address:tb1p93c4wxsr87p88jau7vru83zpk6xl0shf5ynmutd9x0gxwau3tngq9a4w3z

  • Spending method: Script Path (using preimage “helloworld”)

Transaction 2: Bob script path spending#

  • Transaction ID:185024daff64cea4c82f129aa9a8e97b4622899961452d1d144604e65a70cfe0

  • Taproot address:tb1p93c4wxsr87p88jau7vru83zpk6xl0shf5ynmutd9x0gxwau3tngq9a4w3z

  • Spending method: Script Path (Bob’s private key signature)

Note: both transactions use the same Taproot address, proving they come from the same dual-leaf script tree!

Commit Phase: Dual-Leaf Script Tree Construction#

Flat structure [hash_script, bob_script]: hash_script index 0, bob_script index 1. (See btcaaron cell above for runnable code.)

Script Construction#

# Requires: import hashlib, and bitcoinutils Script, PrivateKey, get_taproot_address
preimage_hash = hashlib.sha256(b"helloworld").hexdigest()
hash_script = Script(['OP_SHA256', preimage_hash, 'OP_EQUALVERIFY', 'OP_TRUE'])
bob_script = Script([bob_pub.to_x_only_hex(), 'OP_CHECKSIG'])

Address Generation#

all_leafs = [hash_script, bob_script]
taproot_address = alice_pub.get_taproot_address(all_leafs)
# Dual-leaf Taproot script tree (btcaaron)
# Reference: examples/ch07_dual_leaf_tree.py

from btcaaron import Key, TapTree

alice = Key.from_wif("cRxebG1hY6vVgS9CSLNaEbEJaXkpZvc6nFeqqGT7v6gcW7MbzKNT")
bob   = Key.from_wif("cSNdLFDf3wjx1rswNL2jKykbVkC6o56o5nYZi4FUkWKjFn2Q5DSG")

# Dual-leaf tree: [hashlock] | [bob checksig]
program = (TapTree(internal_key=alice)
    .hashlock("helloworld", label="hash")
    .checksig(bob, label="bob")
).build()

print("=== DUAL-LEAF TAPROOT TREE ===")
print(f"Address: {program.address}")
print(f"Leaves: {program.leaves}")
print(program.visualize())

# Hash lock path spending
tx_hash = (program.spend("hash")
    .from_utxo("f02c055369812944390ca6a232190ec0db83e4b1b623c452a269408bf8282d66", 0, sats=1234)
    .to("tb1p060z97qusuxe7w6h8z0l9kam5kn76jur22ecel75wjlmnkpxtnls6vdgne", 1034)
    .unlock(preimage="helloworld")
    .build())
print(f"\nHashlock TXID: {tx_hash.txid}")

# Bob signature path spending
tx_bob = (program.spend("bob")
    .from_utxo("8caddfad76a5b3a8595a522e24305dc20580ca868ef733493e308ada084a050c", 1, sats=1111)
    .to("tb1pshzcvake3a3d76jmue3jz4hyh35yvk0gjj752pd53ys9txy5c3aswe5cn7", 900)
    .sign(bob)
    .build())
print(f"Bob signature TXID: {tx_bob.txid}")

Reveal Phase: Two Script Path Spendings#

1. Hash script path (index 0)#

Witness: [preimage_hex, script, control_block]. TXID: b61857a0...

cb = ControlBlock(alice_pub, all_leafs, 0, is_odd=taproot_address.is_odd())
tx.witnesses.append(TxWitnessInput(["helloworld".encode().hex(), hash_script.to_hex(), cb.to_hex()]))

2. Bob script path (index 1)#

Witness: [sig, script, control_block]. Key: script_path=True, tapleaf_script=bob_script (singular), tweak=False.TXID: 185024da...

cb = ControlBlock(alice_pub, all_leafs, 1, is_odd=taproot_address.is_odd())
sig = bob_priv.sign_taproot_input(..., script_path=True, tapleaf_script=bob_script, tweak=False)
tx.witnesses.append(TxWitnessInput([sig, bob_script.to_hex(), cb.to_hex()]))

Compare: Hash path uses preimage verification; Bob path uses Schnorr signature; control blocks each contain sibling TapLeaf hash.

In dual-leaf trees, each script’s control block contains its sibling hash as Merkle proof. Let’s analyze actual on-chain data:

Hash script path control block#

Data extracted from transaction b61857a0…:

Control Block: c050be5fc44ec580c387bf45df275aaa8b27e2d7716af31f10eeed357d126bb4d32faaa677cb6ad6a74bf7025e4cd03d2a82c7fb8e3c277916d7751078105cf9df
Structure breakdown:
├─ c0: Leaf version (0xc0)
├─ 50be5fc44ec580c387bf45df275aaa8b27e2d7716af31f10eeed357d126bb4d3: Alice internal pubkey
└─ 2faaa677cb6ad6a74bf7025e4cd03d2a82c7fb8e3c277916d7751078105cf9df: Bob Script's TapLeaf hash

Bob script path control block#

Data extracted from transaction 185024da…:

Control Block: c050be5fc44ec580c387bf45df275aaa8b27e2d7716af31f10eeed357d126bb4d3fe78d8523ce9603014b28739a51ef826f791aa17511e617af6dc96a8f10f659e
Structure breakdown:
├─ c0: Leaf version (0xc0)
├─ 50be5fc44ec580c387bf45df275aaa8b27e2d7716af31f10eeed357d126bb4d3: Alice internal pubkey (same!)
└─ fe78d8523ce9603014b28739a51ef826f791aa17511e617af6dc96a8f10f659e: Hash Script's TapLeaf hash

Important: Both control blocks share same internal pubkey; Merkle path is sibling TapLeaf hashes.

Dual-leaf control block structure (65 bytes)#

Byte 0: version+parity; 1–32: internal pubkey; 33–64: sibling TapLeaf hash.

cb = bytes.fromhex(control_block_hex)
internal_pubkey = cb[1:33].hex()
sibling = cb[33:65].hex()
# Runnable: Parse dual-leaf control block (65 bytes, tx b61857a0... hash path)
cb_hex = "c050be5fc44ec580c387bf45df275aaa8b27e2d7716af31f10eeed357d126bb4d32faaa677cb6ad6a74bf7025e4cd03d2a82c7fb8e3c277916d7751078105cf9df"
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 (Bob TapLeaf): {cb[33:65].hex()[:16]}...")

Now let’s analyze the hash script path’s full execution. Based on transaction b61857a0... data:

Witness data structure#

Witness Stack:
[0] 68656c6c6f776f726c64 (preimage_hex)
[1] a820936a185c...8851 (script_hex)
[2] c050be5fc4...cf9df (control_block)

Script bytecode parsing#

Hash script: a820936a185caaa266bb9cbe981e9e05cb78cd732b0b3280eb944412bb6f8f8f07af8851

Bytecode breakdown:
a8 = OP_SHA256
20 = OP_PUSHBYTES_32
936a185caaa266bb9cbe981e9e05cb78cd732b0b3280eb944412bb6f8f8f07af = SHA256("helloworld")
88 = OP_EQUALVERIFY
51 = OP_PUSHNUM_1 (OP_TRUE)

Stack execution—hash script path#

Execution script:OP_SHA256 OP_PUSHBYTES_32 936a185caaa266bb9cbe981e9e05cb78cd732b0b3280eb944412bb6f8f8f07af OP_EQUALVERIFY OP_PUSHNUM_1

0. Initial state: Load script input#

│ 68656c6c6f776f726c64 (preimage_hex) │
└──────────────────────────────────────┘

(Preimage “helloworld” hex representation already on stack)

1. OP_SHA256: Compute SHA256 hash of stack top#

│ 936a185c...07af (computed_hash) │
└─────────────────────────────────┘

(SHA256(“helloworld”) = 936a185c…07af)

2. OP_PUSHBYTES_32: Push expected hash#

│ 936a185c...07af (expected_hash) │
│ 936a185c...07af (computed_hash) │
└─────────────────────────────────┘

(Stack top now has two identical hash values)

3. OP_EQUALVERIFY: Verify hashes equal#

│ (empty_stack) │
└───────────────┘

(Verification succeeds: expected_hash == computed_hash, both elements removed)

4. OP_PUSHNUM_1: Push success flag#

│ 01 (true_value) │
└─────────────────┘

(Script success: stack top is non-zero)

Next, let’s analyze Bob script path execution. Based on transaction 185024da... data:

Witness data structure#

Witness Stack:
[0] 26a0eadc...f9f1c5c (bob_signature)
[1] 2084b59516...63af5ac (script_hex)
[2] c050be5fc4...0f659e (control_block)

Script bytecode parsing#

Bob script: 2084b5951609b76619a1ce7f48977b4312ebe226987166ef044bfb374ceef63af5ac

Bytecode breakdown:
20 = OP_PUSHBYTES_32
84b5951609b76619a1ce7f48977b4312ebe226987166ef044bfb374ceef63af5 = Bob's x-only pubkey
ac = OP_CHECKSIG

Stack Execution: Bob Script Path#

Execution script:OP_PUSHBYTES_32 84b5951609b76619a1ce7f48977b4312ebe226987166ef044bfb374ceef63af5 OP_CHECKSIG

0. Initial state: Load script input#

│ 26a0eadc...f9f1c5c (bob_signature) │
└────────────────────────────────────┘

(Bob’s 64-byte Schnorr signature already on stack)

1. OP_PUSHBYTES_32: Push Bob’s x-only public key#

│ 84b59516...eef63af5 (bob_pubkey)   │
│ 26a0eadc...f9f1c5c (bob_signature) │
└────────────────────────────────────┘

(Bob’s 32-byte x-only pubkey pushed to stack top)

2. OP_CHECKSIG: Verify Schnorr signature#

│ 01 (signature_valid) │
└──────────────────────┘

(Signature valid: Bob’s key corresponds to this pubkey, signature valid for transaction data)

Verification details:

  1. Pop pubkey from stack:84b5951609b76619a1ce7f48977b4312ebe226987166ef044bfb374ceef63af5

  2. Pop signature from stack:26a0eadca0bba3d1bb6f82b8e1f76e2d84038c97a92fa95cc0b9f6a6a59bac5f...

  3. Verify signature validity using BIP340 Schnorr verification

  4. Verification success, push 1 for TRUE

By comparing single-leaf and dual-leaf implementations, we clearly see Merkle tree computation differences:

Single-leaf script tree#

Merkle Root = TapLeaf Hash
            = Tagged_Hash("TapLeaf", 0xc0 + len(script) + script)

Characteristics:

  • Simple and direct, TapLeaf hash as Merkle root

  • Control block has only internal pubkey, no Merkle path

  • For simple single-condition verification

Dual-leaf script tree#

Merkle Root = TapBranch Hash
            = Tagged_Hash("TapBranch", sorted(TapLeaf_A, TapLeaf_B))
TapLeaf_A = Tagged_Hash("TapLeaf", 0xc0 + len(script_A) + script_A)
TapLeaf_B = Tagged_Hash("TapLeaf", 0xc0 + len(script_B) + script_B)

Characteristics:

  • Real Merkle tree structure, needs TapBranch computation

  • Lexicographic sort ensures deterministic result

  • Control block contains sibling hash as Merkle proof

  • Supports complex multi-condition verification

Control block size comparison#

Script Tree Type

Control Block Size

Structure

Single-leaf

33 bytes

[version+parity] + [internal_pubkey]

Dual-leaf

65 bytes

[version+parity] + [internal_pubkey] + [sibling_hash]

Four-leaf

97 bytes

[version+parity] + [internal_pubkey] + [sibling_hash] + [parent_sibling_hash]

As script tree depth increases, control block grows linearly but remains much more efficient than traditional multisig scripts.

Programming Best Practices#

1. Commit phase#

leafs = [hash_script, bob_script]  # Index order is fixed
taproot_address = alice_key.get_taproot_address(leafs)

2. Script Path spending template#

control_block = ControlBlock(internal_key, leafs, script_index, is_odd=taproot_addr.is_odd())
witness = TxWitnessInput([*input_data, leafs[script_index].to_hex(), control_block.to_hex()])

3. Common errors#

Script index mismatch: ❌ ControlBlock(..., 1, ...) with leafs[0] fails. ✅ Indices must match.

Debug sibling: cb = bytes.fromhex(cb_hex); actual = cb[33:65], compare with expected TapLeaf hash.

Through actual on-chain data we can quantitatively analyze different spending methods’ performance and privacy:

Spending Method

Transaction Size

Witness Data

Computational Complexity

Privacy Level

Relative Fee Cost

Key Path

~110 bytes

64-byte signature

1 signature verification

Complete privacy

Baseline (1.0x)

Hash Script

~180 bytes

preimage+script+cb

Hash calculation+Merkle verification

Exposes Hash Lock

Medium (1.6x)

Bob Script

~185 bytes

signature+script+cb

Signature verification+Merkle verification

Exposes P2PK structure

Medium (1.7x)

Key insights:

  1. Key Path is always optimal: Regardless of tree complexity, Key Path has highest efficiency and privacy

  2. Script Path cost is manageable: Compared to traditional complex scripts, Taproot’s overhead is acceptable

  3. Value of selective reveal: Only the actually used path is exposed; unused paths stay private forever

Through dual-leaf implementation we’ve mastered Taproot multi-path spending: real Merkle tree building, control blocks with sibling proofs, and coordination of different scripts in one address. More importantly, we understand Taproot’s core philosophy—selective reveal, expose only used paths, perfect balance between complex functionality and high privacy.

In the next chapter we’ll explore multi-layer nested script trees and advanced Taproot patterns, learning to build enterprise-grade applications supporting more spending conditions, and combining timelocks, multisig, and other advanced features for more complex and practical smart contract systems.

Dual-leaf script trees are a major milestone in Taproot application development—they show how to achieve real functional complexity while maintaining simplicity. This is Bitcoin Taproot technology’s essence: Simple on the outside, powerful within.