Chapter 6: Building Real Taproot Contracts#
Why Script Path Changes Everything#
What if you could build a contract on Bitcoin that looks like a simple payment—until it reveals a puzzle that unlocks funds?
Imagine: you want to create a digital bounty where anyone who solves the puzzle gets rewarded, but if no one solves it, you want to reclaim the funds. Or you’re selling digital goods where buyers automatically get unlock keys after payment, but you retain refund control.
Traditional Bitcoin scripts either fully expose all conditions (compromising privacy) or require complex multisig setups (inefficient). Worse, even simple single-sig payments can be easily identified by transaction patterns on the blockchain.
Taproot’s Script Path makes this elegant: Complex conditional contracts look identical to ordinary payments when unspent, revealing only the necessary execution path when needed.
Building on the Taproot theoretical foundation from previous chapters, we now dive into Script Path spending patterns. This chapter demonstrates dual-path authorization in Taproot’s script tree through practical conditional payment scenarios: secret-based conditional payments and direct key holder control.
Let’s start with a concrete business scenario to understand Taproot Script Path’s practical value: Alice wants to create a conditional payment contract with these features:
Conditional path: Anyone who knows the secret “helloworld” can claim funds
Alternative path: Alice, as fund owner, can reclaim funds anytime with her private key
Privacy requirement: When unspent, external observers cannot distinguish simple payment from complex contract
This dual-path design is very common in practice:
Application Scenario |
Description |
|---|---|
Digital goods sales |
Buyer gets unlock key after payment; seller retains refund authority |
Bounty tasks |
Puzzle solver claims reward; unused bounty can be reclaimed by publisher |
Conditional escrow |
Auto-releases under specific conditions; otherwise owner can reclaim |
Educational incentives |
Students claim reward on correct answer; teacher retains management control |
In our scenario, Alice’s Taproot address contains two spending paths:
Key Path (collaborative path):
Alice signs directly with her tweaked private key
64-byte Schnorr signature, maximum efficiency
Complete privacy, no script information leakage
Script Path:
Uses hash lock script:
OP_SHA256 <hash> OP_EQUALVERIFY OP_TRUEAnyone who knows preimage “helloworld” can spend
Requires revealing script content, but doesn’t expose Key Path
Before diving into code, we need to understand Taproot’s core development pattern: Commit-Reveal. This pattern will be the foundational model for all our subsequent Taproot applications.
Commit-Reveal Pattern Overview#
Commit phase:
Build script tree containing multiple spending conditions
Commit script tree to Taproot address
Generate ‘intermediate address’ or ‘custody address’ to lock funds
External parties cannot know specific spending conditions
Reveal phase:
Choose one spending condition to unlock
Key Path: Spend directly with key (maximum privacy)
Script Path: Reveal and execute specific script branch
Only expose actually used conditions; others stay private forever
The power: In Commit phase, all contracts of different complexity look identical; in Reveal phase, only the actually used branch needs to be exposed.
Let’s learn the complete Taproot single-leaf script implementation through a simple hash lock case. Based on Alice’s conditional payment scenario, we’ll implement:
Hash lock script: Verify hash of secret “helloworld”
Single-leaf structure: Simplest script tree, one script branch only
Dual-path spending: Key Path (Alice’s direct control) + Script Path (conditional spending)
Tagged Hash (BIP340): Add tag prefixes for different purposes to prevent hash collisions.
Phase 1: Commit Phase#
Key technical analysis:
Script:a8=OP_SHA256,20=PUSH32,936a185c...07af=SHA256(“helloworld”),88=EQUALVERIFY,51=TRUE。
Commit key code:
hash_hex = hashlib.sha256(b"helloworld").hexdigest()
tr_script = Script(['OP_SHA256', hash_hex, 'OP_EQUALVERIFY', 'OP_TRUE'])
tree = [[tr_script]]
taproot_address = alice_pub.get_taproot_address(tree)
Address: tb1p53ncq9ytax924ps66z6al3wfhy6a29w8h6xfu27xem06t98zkmvsakd43h
Key Path#
Witness: 64-byte Schnorr signature. Need tapleaf_scripts for tweak calculation; script_path=False.
Script Path Spending#
Witness order: [preimage, script, control_block]. Single-leaf control block 33 bytes (version+parity + internal_pubkey, no Merkle path).
cb = ControlBlock(alice_pub, tree, 0, is_odd=taproot_address.is_odd())
tx.witnesses.append(TxWitnessInput(["helloworld".encode().hex(), tr_script.to_hex(), cb.to_hex()]))
Transaction 68f7c8f0… Witness Stack#
[0] 68656c6c6f776f726c64 (preimage)
[1] a820936a185c...8851 (script)
[2] c150be5fc4...bb4d3 (control_block)
# Single-leaf Taproot contract implementation (btcaaron)
# Reference: examples/ch06_single_leaf_contract.py
from btcaaron import Key, TapTree
alice = Key.from_wif("cRxebG1hY6vVgS9CSLNaEbEJaXkpZvc6nFeqqGT7v6gcW7MbzKNT")
# Commit phase: Build single-leaf script tree (hashlock + Key Path)
program = (TapTree(internal_key=alice)
.hashlock("helloworld", label="hash")
).build()
print("=== SINGLE-LEAF TAPROOT CONTRACT ===")
print(f"Address: {program.address}")
print(f"Leaves: {program.leaves}")
print(program.visualize())
print(f"Expected address: tb1p53ncq9ytax924ps66z6al3wfhy6a29w8h6xfu27xem06t98zkmvsakd43h")
# Key Path spending (Alice reclaims directly)
tx_key = (program.keypath()
.from_utxo("4fd83128fb2df7cd25d96fdb6ed9bea26de755f212e37c3aa017641d3d2d2c6d", 0, sats=3900)
.to("tb1p060z97qusuxe7w6h8z0l9kam5kn76jur22ecel75wjlmnkpxtnls6vdgne", 3700)
.sign(alice)
.build())
print(f"\nKey Path TXID: {tx_key.txid}")
# Script Path spending (anyone who knows preimage can spend)
tx_hash = (program.spend("hash")
.from_utxo("9e193d8c5b4ff4ad7cb13d196c2ecc210d9b0ec144bb919ac4314c1240629886", 0, sats=5000)
.to("tb1p060z97qusuxe7w6h8z0l9kam5kn76jur22ecel75wjlmnkpxtnls6vdgne", 4000)
.unlock(preimage="helloworld")
.build())
print(f"Script Path TXID: {tx_hash.txid}")
# Runnable: Parse single-leaf control block (33 bytes, tx 68f7c8f0... Script Path)
cb_hex = "c150be5fc44ec580c387bf45df275aaa8b27e2d7716af31f10eeed357d126bb4d3"
cb = bytes.fromhex(cb_hex)
print(f"Control block length: {len(cb)} bytes")
print(f"Internal pubkey: {cb[1:33].hex()[:16]}...")
Common Errors and Debugging#
Witness order: ✅ [preimage, script, control_block]. ❌ Don’t reverse.
Checkpoints: Script consistency, control block is_odd, preimage UTF-8→hex encoding ("helloworld" → 68656c6c6f776f726c64).
Next, let’s observe the hash lock script’s complete stack execution:
Execution script:OP_SHA256 OP_PUSHBYTES_32 936a185c...07af OP_EQUALVERIFY OP_PUSHNUM_1
0. Initial stack: Load script input#
│ 68656c6c6f776f726c64 │
│ (preimage_hex: "helloworld") │
└──────────────────────────────────────────────────┘
1. OP_SHA256: Compute SHA256 hash of stack top#
OP_SHA256 pops preimage from stack top, computes SHA256, pushes result back:
│ 936a185c...07af (computed_hash) │
└─────────────────────────────────┘
(Computation: SHA256(“helloworld”) = 936a185c…07af)
2. PUSH 32 bytes: Push expected hash#
Script pushes preset expected hash to stack top:
│ 936a185c...07af (expected_hash) │
│ 936a185c...07af (computed_hash) │
└─────────────────────────────────┘
(Stack now has two identical hashes)
3. OP_EQUALVERIFY: Verify hashes equal#
OP_EQUALVERIFY pops two elements, compares; if equal continues, else script fails:
│ (empty_stack) │
└───────────────┘
(Verification success: hashes equal, both elements consumed)
4. OP_TRUE: Push success flag#
Finally, OP_TRUE (OP_PUSHNUM_1) pushes 1 to stack, marking script success:
│ 01 (true_value) │
└─────────────────┘
(Script success: stack top is non-zero)
Through actual code and on-chain analysis, we can clearly see differences between the two spending methods:
Aspect |
Key Path |
Script Path |
|---|---|---|
Witness Data |
1 element (64-byte signature) |
3 elements (input+script+control block) |
Transaction Size |
~153 bytes |
~234 bytes |
Privacy Level |
Complete privacy, zero information leakage |
Partial privacy, only exposes used script branch |
Verification Complexity |
Single Schnorr signature verification |
Control block verification + script execution |
Fee Cost |
Lowest cost |
Medium cost (~50% additional overhead) |
This selective reveal design lets Taproot support various complex scenarios: digital goods, bounty tasks, conditional escrow, multi-party contracts—while maintaining maximum privacy when unused.
Unlike P2SH where all conditions are revealed when spent, Taproot Script Path ensures only the executed branch is seen. This fundamental shift redefines Bitcoin’s contract privacy model.
Traditional script limitations:
All spending conditions visible on-chain
Complex contracts easily identified by observers
Even unused branches compromise privacy
Taproot’s privacy innovation:
Unused conditions stay hidden forever
Complex contracts indistinguishable from simple payments
Only reveal executed logic, maintain maximum privacy
This privacy-first design makes Taproot the foundation for Bitcoin’s next-generation smart contracts, where complexity doesn’t compromise confidentiality.
Through Alice’s hash lock contract, we deeply understand Taproot’s revolutionary approach to Bitcoin smart contracts:
The Power of Commit-Reveal Pattern#
Commit phase: Commit complex conditional logic to a normal Taproot address, generating an intermediate address to lock funds
Reveal phase: Choose Key Path or Script Path spending as needed, exposing only necessary information
Technical Implementation Mastery#
Single-leaf script tree: TapLeaf hash directly as Merkle root, no extra Merkle computation
Control block verification: Cryptographically prove script legitimacy via address recovery
Stack execution: Hash lock achieves conditional spending via hash match verification
Development Best Practices#
Tagged Hash understanding: Master hash tags for different purposes, ensure security
Witness data order: Strictly follow [input_params, script, control_block] order
Systematic debugging: Use our debug flow to troubleshoot common issues
Code consistency: Use helper functions to align commit-reveal phases
The Bigger Picture#
This chapter establishes more than hash lock implementation—it shows Taproot’s fundamental privacy revolution. By allowing complex contracts to masquerade as simple payments before execution, Taproot changes Bitcoin’s capabilities without sacrificing user privacy.
Next: In the next chapter, we’ll explore dual-leaf script trees, learning to organize multiple spending conditions in one Taproot address, introduce real Merkle tree computation, and experience Taproot script tree architecture’s full power for more complex scenarios.