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:
TapLeaf hash computation: Each script computes its TapLeaf hash separately
TapBranch hash: Compute after lexicographically sorting two TapLeaf hashes
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:
b61857a05852482c9d5ffbb8159fc2ba1efa3dd16fe4595f121fc35878a2e430Taproot address:
tb1p93c4wxsr87p88jau7vru83zpk6xl0shf5ynmutd9x0gxwau3tngq9a4w3zSpending method: Script Path (using preimage “helloworld”)
Transaction 2: Bob script path spending#
Transaction ID:
185024daff64cea4c82f129aa9a8e97b4622899961452d1d144604e65a70cfe0Taproot address:
tb1p93c4wxsr87p88jau7vru83zpk6xl0shf5ynmutd9x0gxwau3tngq9a4w3zSpending 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:
Pop pubkey from stack:
84b5951609b76619a1ce7f48977b4312ebe226987166ef044bfb374ceef63af5Pop signature from stack:
26a0eadca0bba3d1bb6f82b8e1f76e2d84038c97a92fa95cc0b9f6a6a59bac5f...Verify signature validity using BIP340 Schnorr verification
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:
Key Path is always optimal: Regardless of tree complexity, Key Path has highest efficiency and privacy
Script Path cost is manageable: Compared to traditional complex scripts, Taproot’s overhead is acceptable
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.