Two related developer write-ups describe building Taproot script trees using Python and the bitcoinutils library, then spending from the resulting Taproot outputs on Bitcoin testnet. One post focuses on a complete 4-leaf Taproot tree that combines multiple script-path conditions with a key-path spend: a SHA256 hashlock (with a revealed preimage), a 2-of-2 multisig leaf using OP_CHECKSIGADD, a CSV relative timelock leaf using OP_CHECKSEQUENCEVERIFY, and a simple signature leaf, plus an additional key-path option for Alice. The author reports five confirmed testnet spending transactions corresponding to each spend path, providing TXIDs and demonstrating script validation via control blocks. A second post covers a simpler dual-leaf Taproot setup with two script-path leaves—hashlock and pay-to-pubkey—paired with a key-path for the internal key holder. It includes real testnet transactions and an explanation of how Control Blocks prove script inclusion and how the TapTweak computation derives the final output key. Both posts emphasize witness construction requirements (including element counts and, for multisig/CHECKSIGADD, stack ordering) and debugging common errors such as script mismatch or witness-program mismatch, alongside code-level details for tapleaf hashing and Control Block verification.
Developer posts Python Taproot implementations with validated script-path spending on Bitcoin testnet
Two related developer write-ups describe building Taproot script trees using Python and the bitcoinutils library, then spending from the resulting Taproot outputs on Bitcoin testnet. One post focuses...
- Both posts use Python and the bitcoinutils tooling to construct Taproot script trees and generate Taproot addresses.
- Each script-path spend provides a Control Block that proves the spent leaf is committed in the Taproot output via the Merkle root and tweak derivation.
- One implementation uses a 4-leaf Taproot tree (hashlock, 2-of-2 multisig via OP_CHECKSIGADD, CSV timelock, and single signature) and reports confirmed testnet spending transactions for the different paths.
- The other implementation uses a simpler 2-leaf script tree (SHA256 hashlock and pay-to-pubkey) and provides testnet transactions demonstrating script-path spending.
- Both accounts describe key-path spending as a separate authorization method where the witness contains only the signature, with Taproot key tweaking derived from the internal key and committed script tree.
HODLing is the beginning. But Bitcoin was meant to be programmed. “Not Just HODLing: Real Bitcoin Script Engineering” starts here. Most Bitcoin Taproot tutorials stop at 1–2 leaves. This is different. What you’re about to see is the first publicly available demonstration of a complete 4-leaf Taproot implementation with every spending path validated on Bitcoin testnet. While most examples show simple key-path-only usage or basic 2-script trees, this implementation demonstrates Taproot’s true potential: a single address hiding five different ways to spend funds. Live Testnet Proof: Commit Address: tb1pjfdm902y2adr08qnn4tahxjvp6x5selgmvzx63yfqk2hdey02yvqjcr29q 5 Confirmed Spending Transactions (we’ll show you the TXIDs) Every script path works flawlessly What makes this significant: Complete validation: Not just theory — every spending path confirmed on testnet Production-ready code: Real Python implementation using bitcoinutils Complex conditional logic: Hash locks, multisig, timelocks, and fallbacks in one tree Maximum privacy: Key path spends look identical to regular transactions This implementation proves that sophisticated Bitcoin smart contracts can operate with the same privacy and efficiency as simple payments. Why 4-Leaf Trees Matter Most production Taproot usage follows the path of least resistance: key-path-only transactions that look like regular single-signature spends. While this maximizes privacy, it leaves Taproot’s smart contract potential largely untapped. A 4-leaf tree demonstrates several capabilities that simple implementations miss: Real-World Applications: Wallet Recovery: Progressive access control with timelock + multisig + emergency paths Lightning Channels: Multiple cooperative close scenarios with different participant sets Atomic Swaps: Hash time locked contracts with various fallback conditions Inheritance Planning: Time-based access with multiple beneficiary options Technical Advantages: Selective revelation: Only the executed script is exposed, others remain hidden Fee efficiency: Smaller than equivalent traditional multi-condition scripts Flexible logic: Multiple execution paths in a single commitment The beauty is that from the outside, our complex 4-leaf tree looks identical to any other Taproot address — until you need to use one of the hidden spending conditions. Our Script Tree Design We’ll build this balanced Merkle tree structure: Merkle Root / \ Branch0 Branch1 / \ / \ Script0 Script1 Script2 Script3 Hashlock Multi CSV Sig Each script serves a different purpose in a realistic multi-party protocol: Script 0 (SHA256 Hashlock): Anyone with the preimage “helloworld” can spend Script 1 (2-of-2 Multisig): Requires cooperation between Alice and Bob Script 2 (CSV Timelock): Bob can spend after waiting 2 blocks Script 3 (Simple Signature): Bob can spend immediately with his signature Plus the Key Path: Alice can spend with maximum privacy using her tweaked private key. Implementation Setup from bitcoinutils.setup import setup from bitcoinutils.keys import PrivateKey from bitcoinutils.script import Script from bitcoinutils.transactions import Transaction, TxInput, TxOutput, TxWitnessInput from bitcoinutils.utils import ControlBlock import hashlib # Use testnet for safe experimentation setup('testnet') # Generate keys for our participants alice_priv = PrivateKey.from_wif("") bob_priv = PrivateKey.from_wif("") alice_pub = alice_priv.get_public_key() bob_pub = bob_priv.get_public_key() Building the Four Scripts Script 0: SHA256 Hashlock This script implements a simple hash-time-lock pattern common in atomic swaps: # Anyone who knows the preimage can spend preimage = "helloworld" hash1 = hashlib.sha256(preimage.encode('utf-8')).hexdigest() script1 = Script([ 'OP_SHA256', hash1, 'OP_EQUALVERIFY', 'OP_TRUE' ]) print(f"Script 0 (Hashlock): {script1.to_hex()}") How it works: The spender provides the preimage in the witness. The script hashes it with SHA256, compares against the committed hash, and succeeds if they match. Script 1: 2-of-2 Multisig (Tapscript) This uses Tapscript’s efficient OP_CHECKSIGADD instead of legacy OP_CHECKMULTISIG: # Requires both Alice and Bob's signatures script2 = Script([ "OP_0", # Initialize counter alice_pub.to_x_only_hex(), # Alice's x-only pubkey "OP_CHECKSIGADD", # Verify Alice sig, increment counter bob_pub.to_x_only_hex(), # Bob's x-only pubkey "OP_CHECKSIGADD", # Verify Bob sig, increment counter "OP_2", # Required signature count "OP_EQUAL" # Check counter == required count ]) Key innovation: OP_CHECKSIGADD is more efficient than OP_CHECKMULTISIG and handles x-only public keys natively. Script 2: CSV Timelock This implements a relative timelock — Bob can spend after a certain number of blocks: from bitcoinutils.utils import Sequence, TYPE_RELATIVE_TIMELOCK # Bob can spend after waiting 2 blocks relative_blocks = 2 seq = Sequence(TYPE_RELATIVE_TIMELOCK, relative_blocks) script3 = Script([ seq.for_script(), # Push sequence value "OP_CHECKSEQUENCEVERIFY", # Verify relative timelock "OP_DROP", # Clean up stack bob_pub.to_x_only_hex(), # Bob's pubkey "OP_CHECKSIG" # Verify Bob's signature ]) Important: The transaction input must set the sequence number to enable the timelock. Script 3: Simple Signature The simplest script — just Bob’s signature: # Bob can spend immediately script4 = Script([ bob_pub.to_x_only_hex(), "OP_CHECKSIG" ]) Creating the Taproot Address Now we combine all scripts into a Merkle tree and generate the address: # Build the script tree: [[left_branch], [right_branch]] tree = [[script1, script2], [script3, script4]] # Generate Taproot address using Alice's internal key taproot_address = alice_pub.get_taproot_address(tree) print(f"Taproot Address: {taproot_address.to_string()}") # This is where we'll send our test funds commit_address = "tb1pjfdm902y2adr08qnn4tahxjvp6x5selgmvzx63yfqk2hdey02yvqjcr29q" This address is a cryptographic commitment to all four scripts plus Alice’s key path. From the outside, it’s indistinguishable from any other Taproot address. Spending Path 1: SHA256 Hashlock First, let’s spend using the hashlock by revealing the preimage: # Build the spending transaction commit_txid = "245563c5aa4c6d32fc34eed2f182b5ed76892d13370f067dc56f34616b66c468" # From your funding transaction input_amount = 1200 # satoshis output_amount = 666 txin = TxInput(commit_txid, 0) # spending output 0 txout = TxOutput(output_amount, alice_pub.get_taproot_address().to_script_pub_key()) tx = Transaction([txin], [txout], has_segwit=True) # Create control block for script1 (index 0 in our tree) cb = ControlBlock(alice_pub, tree, 0, is_odd=taproot_address.is_odd()) # Build witness: [preimage, script, control_block] preimage_hex = preimage.encode('utf-8').hex() witness = TxWitnessInput([ preimage_hex, # The secret that unlocks the hashlock script1.to_hex(), # The script being executed cb.to_hex() # Proof that this script was committed ]) tx.witnesses.append(witness) print(f"Hashlock Transaction: {tx.serialize()}") Testnet Result: txid:1ba4835fca1c94e7eb0016ce37c6de2545d07d84a97436f8db999f33a6fd6845 Spending Path 2: 2-of-2 Multisig For the multisig spend, we need signatures from both Alice and Bob: # Create control block for script2 (index 1 in our tree) cb = ControlBlock(alice_pub, tree, 1, is_odd=taproot_address.is_odd()) # Sign the transaction with both keys (script path signing) sig_alice = alice_priv.sign_taproot_input( tx, 0, [taproot_address.to_script_pub_key()], [input_amount], script_path=True, tapleaf_script=script2, tweak=False ) sig_bob = bob_priv.sign_taproot_input( tx, 0, [taproot_address.to_script_pub_key()], [input_amount], script_path=True, tapleaf_script=script2, tweak=False ) # Build witness: [bob_sig, alice_sig, script, control_block] # Note: Bob's signature goes first due to stack consumption order witness = TxWitnessInput([ sig_bob, # Consumed second by the script sig_alice, # Consumed first by the script script2.to_hex(), cb.to_hex() ]) tx.witnesses.append(witness) Testnet Result: 1951a3be0f05df377b1789223f6da66ed39c781aaf39ace0bf98c3beb7e604a1 Spending Path 3: CSV Timelock The timelock spend requires setting a custom sequence number: # Create transaction with custom sequence for timelock seq_for_input = seq.for_input_sequence() txin = TxInput(commit_txid, 0, sequence=seq_for_input) # Key difference! # Rest is similar to other script path spends cb = ControlBlock(alice_pub, tree, 2, is_odd=taproot_address.is_odd()) sig_bob = bob_priv.sign_taproot_input( tx, 0, [taproot_address.to_script_pub_key()], [input_amount], script_path=True, tapleaf_script=script3, tweak=False ) witness = TxWitnessInput([ sig_bob, script3.to_hex(), cb.to_hex() ]) Testnet Result: txid:98361ab2c19aa0063f7572cfd0f66cb890b403d2dd12029426613b40d17f41ee Spending Path 4: Simple Signature The simplest script path spend: cb = ControlBlock(alice_pub, tree, 3, is_odd=taproot_address.is_odd()) sig_bob = bob_priv.sign_taproot_input( tx, 0, [taproot_address.to_script_pub_key()], [input_amount], script_path=True, tapleaf_script=script4, tweak=False ) witness = TxWitnessInput([ sig_bob, script4.to_hex(), cb.to_hex() ]) Testnet Result: txid:1af46d4c71e121783c3c7195f4b45025a1f38b73fc8898d2546fc33b4c6c71b9 Spending Path 5: Key Path (Maximum Privacy) The most efficient and private option — looks like a regular single-sig transaction: # No control block needed for key path! # Alice signs with her tweaked private key sig_alice = alice_priv.sign_taproot_input( tx, 0, [taproot_address.to_script_pub_key()], [input_amount], script_path=False, # This is key path, not script path tapleaf_scripts=tree # Needed for tweak calculation ) # Witness contains only the signature-maximum efficiency! witness = TxWitnessInput([sig_alice]) tx.witnesses.append(witness) Testnet Result: txid:1e518aa540bc770df549ec9836d89783ca19fc79b84e7407a882cbe9e95600da Common Pitfalls and Solutions Witness Stack Ordering The multisig witness order is critical: # ❌ Wrong: Alice sig first witness = [sig_alice, sig_bob, script, control_block] # ✅ Correct: Bob sig first (consumed second) witness = [sig_bob, sig_alice, script, control_block] Sequence Numbers for CSV CSV scripts require specific transaction sequence values: # ❌ Wrong: Default sequence txin = TxInput(txid, vout) # ✅ Correct: CSV-compatible sequence txin = TxInput(txid, vout, sequence=seq.for_input_sequence()) Script Path vs Key Path Signing The signing process is different for each path: # 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) What This Proves This implementation demonstrates several critical points about Taproot’s real-world capabilities: Protocol Maturity: Complex script trees work flawlessly on Bitcoin’s network, proving the BIP 341 implementation is production-ready. Privacy Preservation: Each execution path reveals only the necessary conditions while keeping alternatives completely hidden. Efficiency: Even sophisticated 4-leaf trees maintain reasonable transaction sizes and fees. Flexibility: The same UTXO can accommodate radically different spending conditions — cooperation, disputes, timeouts, or emergencies. Developer Readiness: The tooling ecosystem successfully supports advanced Taproot patterns, not just simple key-path-only usage. Next Steps and Resources Part 2 Preview: In the next article, we’ll dive deep into the cryptographic mechanics that make this work. You’ll learn: How Control Blocks prove script inclusion through Merkle proofs Why witness stack ordering matters for script execution The mathematical guarantees behind Taproot’s privacy properties Security considerations for production deployments Optimization strategies for even larger script trees We’ll dissect the actual control blocks from our testnet transactions, analyze the TaggedHash construction step-by-step, and explore the trade-offs between tree depth, privacy, and efficiency. Part 3 Vision: An interactive visual simulator where you can experiment with different tree structures, generate control blocks, and see exactly how script path proofs work — all in your browser. The future of Bitcoin smart contracts isn’t just about what’s theoretically possible — it’s about what works reliably in practice. This implementation moves us one step closer to that future. Source Code All code and on-chain data analysis are available at my github repo Feel free to Star and Fork! This article is part of the “Not Just HODLing: Real Bitcoin Script Engineering” series. Previous articles covered [*CSV timelocks with P2SH](https://medium.com/@aaron.recompile/how-i-built-a-time-locked-bitcoin-script-with-csv-and-p2sh-c48c0389709d) and [*basic Taproot implementation](https://medium.com/@aaron.recompile/a-guide-to-creating-taproot-scripts-with-python-bitcoinutils-e088633bc2a7)*.* By Aaron Recompile on July 2, 2025. Canonical link Exported from Medium on July 3, 2026.
2 hours agoHODLing is the beginning. But Bitcoin was meant to be programmed. "Not Just HODLing: Real Bitcoin Script Engineering" starts here. Building Taproot addresses with dual Script Path leaves and understanding both Key Path and Script Path spending through real testnet transactions Target Audience: Developers already comfortable with Bitcoin key/address basics and want to go deeper into Taproot script path spending. This is not a beginner’s guide — it assumes familiarity with SegWit and Python Bitcoin tooling. Article Structure Preview Motivation and Case Overview Building the Dual-Leaf Script Path (Code Walkthrough) Three-Path Spending Structure Deep Dive Animated Stack Execution Walkthrough Control Block Cryptographic Analysis The Tweak: From Internal Key to Taproot Address Debugging and On-Chain Data Verification Summary and Takeaways 1. Motivation and Case Overview The Scenario: Alice wants to create a flexible payment arrangement with Bob. She needs an address where: She can spend the funds herself anytime (Key Path) Bob can claim the funds if he knows a secret preimage (HashLock) Bob can also claim the funds using his signature as an alternative (Pay-to-PubKey) This is useful for conditional payments, atomic swaps, or escrow-like arrangements where multiple parties need different ways to access the same funds. The goal of this guide: Build a Taproot address with three spending paths: Key Path (Alice) + Script Path with two leaves (HashLock + P2PK) Actually send and spend coins using both Script Path leaves, broadcasting real transactions on testnet Through this dual-leaf Script Path case, help you fully understand Taproot’s script trees, Control Blocks, and the cryptographic proofs that make Script Path spending secure Be able to trace every byte of witness data and verify Control Block construction in a block explorer Our Testnet Achievement: Escrow Address: tb1p93c4wxsr87p88jau7vru83zpk6xl0shf5ynmutd9x0gxwau3tngq9a4w3z HashLock Script Transaction: b61857a05852482c9d5ffbb8159fc2ba1efa3dd16fe4595f121fc35878a2e430 P2PK Script Transaction: 185024daff64cea4c82f129aa9a8e97b4622899961452d1d144604e65a70cfe0 2. Building the Dual-Path Taproot (Code Walkthrough) Let’s walk through the key components needed to create our dual-path Taproot address and spending transactions. Setting Up Keys and Scripts def create_dual_path_taproot(): setup('testnet') # Alice's key (internal key for Key Path) alice_private = PrivateKey('') alice_public = alice_private.get_public_key() # Bob's key (for Script Path 2) bob_private = PrivateKey('') bob_public = bob_private.get_public_key() # Create HashLock script (Script Path 1) preimage = "helloworld" preimage_hash = hashlib.sha256(preimage.encode()).hexdigest() hashlock_script = Script([ 'OP_SHA256', preimage_hash, 'OP_EQUALVERIFY', 'OP_TRUE' # Anyone with correct preimage can spend, not locked to specific key # WARNING: Should be used with off-chain protocols to prevent front-running ]) # Create Pay-to-PubKey script (Script Path 2) p2pk_script = Script([ bob_public.to_x_only_hex(), 'OP_CHECKSIG' ]) # Build script tree and generate Taproot address all_scripts = [hashlock_script, p2pk_script] taproot_address = alice_public.get_taproot_address(all_scripts) return taproot_address, all_scripts, alice_private, bob_private, preimage HashLock Script Spending def spend_hashlock_script(utxo_txid, utxo_index, input_amount, taproot_address, all_scripts, alice_public, preimage): # Build transaction txin = TxInput(utxo_txid, utxo_index) txout = TxOutput(to_satoshis(input_amount - 0.0002), alice_public.get_taproot_address().to_script_pub_key()) tx = Transaction([txin], [txout], has_segwit=True) # Create Control Block for hashlock_script (index 0) control_block = ControlBlock(alice_public, all_scripts, 0, is_odd=taproot_address.is_odd()) # Build witness: [preimage, script, control_block] hashlock_script = all_scripts[0] preimage_hex = preimage.encode().hex() tx.witnesses.append(TxWitnessInput([ preimage_hex, hashlock_script.to_hex(), control_block.to_hex() ])) return tx Pay-to-PubKey Script Spending def spend_p2pk_script(utxo_txid, utxo_index, input_amount, taproot_address, all_scripts, alice_public, bob_private): # Build transaction txin = TxInput(utxo_txid, utxo_index) txout = TxOutput(to_satoshis(input_amount - 0.0002), bob_private.get_public_key().get_taproot_address().to_script_pub_key()) tx = Transaction([txin], [txout], has_segwit=True) # Sign with Bob's key for Script Path bob_signature = bob_private.sign_taproot_input( tx, 0, [taproot_address.to_script_pub_key()], [to_satoshis(input_amount)], script_path=True, tapleaf_script=all_scripts[1], tweak=False ) # Create Control Block for p2pk_script (index 1) control_block = ControlBlock(alice_public, all_scripts, 1, is_odd=taproot_address.is_odd()) # Build witness: [signature, script, control_block] tx.witnesses.append(TxWitnessInput([ bob_signature, all_scripts[1].to_hex(), control_block.to_hex() ])) return tx 3. Two-Script Tree Structure Deep Dive Our Taproot address demonstrates three total spending paths: TAPROOT ADDRESS / \ KEY PATH SCRIPT PATH (Alice) / \ / \ HASHLOCK P2PK (index 0) (index 1) The Complete Structure: 1 Key Path: Alice can spend directly using her tweaked private key 2 Script Path Leaves: Two distinct scripts form a binary merkle tree Our Script Path contains a simple binary tree with two script leaves: MERKLE ROOT / \ / \ HASHLOCK P2PK (index 0) (index 1) Key Insight: Unlike traditional representations, Taproot trees only contain Script Paths. The Key Path (Alice’s direct spending) is not a tree node — it’s computed separately using the internal key + merkle root tweak. Script Details HashLock Script (Anyone can spend with preimage): hashlock_script = Script([ 'OP_SHA256', # Hash the input '936a185caaa266bb9cbe981e9e05cb78cd732b0b3280eb944412bb6f8f8f07af', # Expected hash 'OP_EQUALVERIFY', # Verify hashes match 'OP_TRUE' # Always succeed if hash matches ]) Pay-to-PubKey Script (Only Bob can spend with signature): p2pk_script = Script([ '84b5951609b76619a1ce7f48977b4312ebe226987166ef044bfb374ceef63af5', # Bob's x-only pubkey 'OP_CHECKSIG' # Verify signature against pubkey ]) 4. Animated Stack Execution Walkthrough HashLock Script Execution Real Transaction: b61857a05852482c9d5ffbb8159fc2ba1efa3dd16fe4595f121fc35878a2e430 Step 1: Initial State ┌─────────────────────────────────────────────────────────────────┐ │ Witness Data: │ │ [0] 68656c6c6f776f726c64 ("helloworld") │ │ [1] a820936a185caaa266bb...8851 (script) │ │ [2] c050be5fc44ec580c3...f9df (control block) │ └─────────────────────────────────────────────────────────────────┘ Step 2: Preimage pushed to stack ┌─────────────────────────────────────────────────────────────────┐ │ "helloworld" │ ← Stack Top └─────────────────────────────────────────────────────────────────┘ Stack (Top → Bottom): ["helloworld"] Script: [OP_SHA256, expected_hash, OP_EQUALVERIFY, OP_TRUE] Step 3: OP_SHA256 executes ┌─────────────────────────────────────────────────────────────────┐ │ 936a185caaa266bb9cbe981e9e05cb78cd732b0b3280eb944412bb6f8f8f07af │ ← Stack Top (computed hash) └─────────────────────────────────────────────────────────────────┘ Stack (Top → Bottom): [computed_hash] Script: [expected_hash, OP_EQUALVERIFY, OP_TRUE] Step 4: Expected hash pushed from script ┌─────────────────────────────────────────────────────────────────┐ │ 936a185caaa266bb9cbe981e9e05cb78cd732b0b3280eb944412bb6f8f8f07af │ ← Stack Top (expected hash) │ 936a185caaa266bb9cbe981e9e05cb78cd732b0b3280eb944412bb6f8f8f07af │ ← Stack Bottom (computed hash) └─────────────────────────────────────────────────────────────────┘ Stack (Top → Bottom): [expected_hash, computed_hash] Script: [OP_EQUALVERIFY, OP_TRUE] Step 5: OP_EQUALVERIFY executes (Pops both values, compares them, pushes result) ┌─────────────────────────────────────────────────────────────────┐ │ 1 (true) │ ← Stack Top (comparison result) └─────────────────────────────────────────────────────────────────┘ Stack (Top → Bottom): [true] Script: [OP_TRUE] Step 6: OP_TRUE executes ┌─────────────────────────────────────────────────────────────────┐ │ 1 (true) │ ← Stack Top (OP_TRUE result) │ 1 (true) │ ← Stack Bottom (previous result) └─────────────────────────────────────────────────────────────────┘ Stack (Top → Bottom): [true, true] Result: SUCCESS ✅ Pay-to-PubKey Script Execution Real Transaction: 185024daff64cea4c82f129aa9a8e97b4622899961452d1d144604e65a70cfe0 Step 1: Initial State ┌─────────────────────────────────────────────────────────────────┐ │ Witness Data: │ │ [0] 26a0eadca0bba3d1bb...f1c5c20 (bob_signature) │ │ [1] 84b5951609b76619a1...63af5acc (script) │ │ [2] c050be5fc44ec580c38...f659e (control block) │ └─────────────────────────────────────────────────────────────────┘ Step 2: Signature pushed to stack ┌─────────────────────────────────────────────────────────────────┐ │ 26a0eadca0bba3d1bb6f82b8e1f76e2d84038c97a92fa95cc0b9f6a6a59ba... │ ← Stack Top └─────────────────────────────────────────────────────────────────┘ Stack (Top → Bottom): [bob_signature] Script: [bob_pubkey, OP_CHECKSIG] Step 3: Bob's pubkey pushed from script ┌─────────────────────────────────────────────────────────────────┐ │ 84b5951609b76619a1ce7f48977b4312ebe226987166ef044bfb374ceef63af5 │ ← Stack Top (pubkey) │ 26a0eadca0bba3d1bb6f82b8e1f76e2d84038c97a92fa95cc0b9f6a6a59ba... │ ← Stack Bottom (signature) └─────────────────────────────────────────────────────────────────┘ Stack (Top → Bottom): [bob_pubkey, bob_signature] Script: [OP_CHECKSIG] Step 4: OP_CHECKSIG validates signature (Pops pubkey and signature, validates, pushes result) ┌─────────────────────────────────────────────────────────────────┐ │ 1 (true) │ ← Stack Top (validation result) └─────────────────────────────────────────────────────────────────┘ Stack (Top → Bottom): [true] Result: SUCCESS ✅ 5. Control Block Cryptographic Analysis Every Script Path spending requires a Control Block that cryptographically proves the script belongs to this Taproot address. Control Block Construction Deep Dive Every Script Path spending requires a Control Block that cryptographically proves the script belongs to this Taproot address. Let’s examine how the ControlBlock constructor works: # Create Control Block for hashlock_script (index 0) control_block = ControlBlock( alice_public, # Internal public key all_scripts, # Complete script array [hashlock_script, p2pk_script] 0, # Script index (hashlock_script is at index 0) is_odd=taproot_address.is_odd() # Affects the first byte prefix ) Key Details: leaf_index: Directly corresponds to script position in the array. The library uses this to determine which script we’re proving and which becomes the sibling hash. is_odd: Controls the parity bit in the Control Block’s first byte. If the final Taproot output key’s y-coordinate is odd, this becomes c1 instead of c0. Script order matters: The array order [hashlock_script, p2pk_script] determines the merkle tree construction and sibling relationships. Control Block Structure Breakdown HashLock Control Block: c050be5fc44ec580c387bf45df275aaa8b27e2d7716af31f10eeed357d126bb4d32faaa677cb6ad6a74bf7025e4cd03d2a82c7fb8e3c277916d7751078105cf9df P2PK Control Block: c050be5fc44ec580c387bf45df275aaa8b27e2d7716af31f10eeed357d126bb4d3fe78d8523ce9603014b28739a51ef826f791aa17511e617af6dc96a8f10f659e Three-Part Analysis # Part 1: Leaf Version (1 byte) leaf_version = "c0" # TAPSCRIPT_VERSION = 0xc0 # Part 2: Internal Public Key (32 bytes) internal_key = "50be5fc44ec580c387bf45df275aaa8b27e2d7716af31f10eeed357d126bb4d3" # Part 3: Sibling Hash (32 bytes) # For HashLock: P2PK Script's tapleaf hash hashlock_sibling = "2faaa677cb6ad6a74bf7025e4cd03d2a82c7fb8e3c277916d7751078105cf9df" # For P2PK: HashLock Script's tapleaf hash p2pk_sibling = "fe78d8523ce9603014b28739a51ef826f791aa17511e617af6dc96a8f10f659e" Verifying Sibling Hashes import hashlib def compute_tapleaf_hash(script): """Compute BIP-341 tapleaf hash""" script_bytes = bytes.fromhex(script.to_hex()) script_length = len(script_bytes) # Build leaf data: version + varint + script leaf_data = bytes([0xc0]) + bytes([script_length]) + script_bytes # Tagged hash: SHA256(tag || tag || data) tag_hash = hashlib.sha256(b"TapLeaf").digest() return hashlib.sha256(tag_hash + tag_hash + leaf_data).digest().hex() # Compute tapleaf hashes for verification hashlock_tapleaf = compute_tapleaf_hash(hashlock_script) p2pk_tapleaf = compute_tapleaf_hash(p2pk_script) print(f"HashLock tapleaf: {hashlock_tapleaf}") print(f"P2PK tapleaf: {p2pk_tapleaf}") # Verify against Control Block sibling hashes print(f"HashLock CB sibling matches P2PK tapleaf: ✅") print(f"P2PK CB sibling matches HashLock tapleaf: ✅") 6. The Tweak: From Internal Key to Taproot Address The cryptographic transformation that creates our Taproot address from Alice’s internal key: Tweak Formula taproot_output_key = internal_key + (tweak * G) Where: internal_key = Alice's public key tweak = tagged_hash("TapTweak", internal_key || merkle_root) G = Bitcoin's elliptic curve generator point Computing Our Specific Tweak import hashlib def tagged_hash(tag, msg): """Helper function for BIP-340 tagged hashes""" tag_hash = hashlib.sha256(tag.encode()).digest() return hashlib.sha256(tag_hash + tag_hash + msg).digest() def compute_tweak(): # Internal key (Alice's pubkey x-only) internal_key_hex = "50be5fc44ec580c387bf45df275aaa8b27e2d7716af31f10eeed357d126bb4d3" # Our computed tapleaf hashes hashlock_tapleaf = "fe78d8523ce9603014b28739a51ef826f791aa17511e617af6dc96a8f10f659e" p2pk_tapleaf = "2faaa677cb6ad6a74bf7025e4cd03d2a82c7fb8e3c277916d7751078105cf9df" # Merkle root = SHA256(smaller_hash || larger_hash) # Note: "||" means concatenation, not Python logical OR if hashlock_tapleaf < p2pk_tapleaf: left, right = hashlock_tapleaf, p2pk_tapleaf else: left, right = p2pk_tapleaf, hashlock_tapleaf merkle_root = hashlib.sha256( bytes.fromhex(left) + bytes.fromhex(right) # Concatenation of byte arrays ).digest() # Compute tweak = tagged_hash("TapTweak", internal_key || merkle_root) internal_key_bytes = bytes.fromhex(internal_key_hex) tweak = tagged_hash("TapTweak", internal_key_bytes + merkle_root) print(f"Internal key: {internal_key_hex}") print(f"Merkle root: {merkle_root.hex()}") print(f"Tweak: {tweak.hex()}") return tweak Key Path vs Script Path: Key Path spending requires the tweak to be applied to Alice’s private key, while Script Path spending uses tweak=False because the script itself provides the spending authorization. Concatenation notation: The || in cryptographic formulas means byte concatenation, implemented as + operator on Python byte arrays. This tweak, when added to Alice’s internal key via elliptic curve point addition, produces the final Taproot output key that becomes our address. 7. Debugging and On-Chain Data Verification Control Block Verification Process The Control Block’s essential purpose: prove that a script belongs to the Taproot address through cryptographic verification. def verify_control_block(control_block_hex, script_hex, taproot_address): """Verify Control Block proves script membership""" # Parse control block components leaf_version = control_block_hex[:2] internal_key = control_block_hex[2:66] sibling_hash = control_block_hex[66:130] # Step 1: Compute script's tapleaf hash script_tapleaf = compute_tapleaf_hash_from_hex(script_hex) # Step 2: Reconstruct merkle root using sibling if script_tapleaf < sibling_hash: merkle_root = sha256(script_tapleaf + sibling_hash) else: merkle_root = sha256(sibling_hash + script_tapleaf) # Step 3: Compute tweak from internal key + merkle root tweak = tagged_hash("TapTweak", internal_key + merkle_root) # Step 4: Derive taproot output key output_key = internal_key_point + (tweak * G) # Step 5: Check if derived address matches original derived_address = key_to_taproot_address(output_key) return derived_address == taproot_address What Control Block Verification Proves When verification succeeds, it mathematically guarantees: Script Authenticity: This exact script was committed when the address was created Merkle Inclusion: The script is a legitimate leaf in the original script tree Address Derivation: The internal key + this script tree = this exact Taproot address No Forgery Possible: Cannot create valid Control Blocks for uncommitted scripts Key Path: The Stealth Option Alice can also spend using Key Path — the most private and efficient method: def alice_key_path_spend(): # Sign with tweaked private key alice_tweaked_sig = alice_private.sign_taproot_input( tx, 0, [scriptPubKey], [amount], script_path=False, # Key Path signing tapleaf_scripts=all_scripts # Full script tree for tweak calculation ) # Witness contains only the signature witness = [alice_tweaked_sig] # Single element! return witness Key Path Advantages: Perfect Privacy: No one knows other spending conditions exist Maximum Efficiency: Smallest possible witness data (~64 bytes) Indistinguishable: Looks identical to any other single-signature transaction 8. What Could Go Wrong: Debug Tips Common Control Block Issues Invalid Control Block Error: SCRIPT_ERR_TAPROOT_SCRIPT_MISMATCH Cause: Control Block doesn’t prove script membership Debug: Verify leaf_index matches script position in array Check: Ensure script hasn’t been modified after address creation Witness Program Mismatch: SCRIPT_ERR_WITNESS_PROGRAM_MISMATCH Cause: Witness data has wrong number of elements or incorrect sizes Debug: HashLock requires 3 elements: [preimage, script, control_block] Check: P2PK requires 3 elements: [signature, script, control_block] Tapleaf Hash Calculation Errors Wrong Script Encoding: # ❌ Wrong: Using raw script string script_bytes = "OP_SHA256 936a18... OP_EQUALVERIFY OP_TRUE".encode() # ✅ Correct: Using Script.to_hex() script_bytes = bytes.fromhex(script.to_hex()) Varint Encoding Issues: # The BIP-341 tapleaf hash includes script length as varint # For scripts < 253 bytes, varint is just the length byte # Our scripts are typically 35-36 bytes, so varint = [35] or [36] leaf_data = bytes([0xc0]) + bytes([len(script_bytes)]) + script_bytes Address Derivation Debugging Parity Bit Mismatch: # If derived address doesn't match, check the parity bit if taproot_output_key.y % 2 == 1: control_block_prefix = 0xc1 # Odd y-coordinate else: control_block_prefix = 0xc0 # Even y-coordinate 9. Summary and Takeaways Through this real case and code, you have mastered: Taproot dual-path script construction using HashLock and P2PK patterns Control Block three-part structure and verification with deep understanding of leaf_index and parity bits BIP-341 tapleaf hash computation including proper varint encoding Tweak calculation from internal key to Taproot address with tagged hash implementation Animated stack-based script execution with precise stack ordering Debugging and on-chain data analysis including common error patterns You now have a testnet-proven example to audit and replay. This forms the foundation for advanced applications like MuSig2, Taproot Assets, or recursive covenant design. This is the essence of Real Bitcoin Script Engineering with Python Bitcoinutils. Source Code All complete code examples and on-chain transaction analysis are available at my github repository. Feel free to Star and Fork! References BIP-341: Taproot: SegWit version 1 spending rules python-bitcoin-utils: Official implementation library By Aaron Recompile on June 29, 2025. Canonical link Exported from Medium on July 3, 2026.
2 hours agoSamit Dravid’s batting draws comparisons to Rahul Dravid in Maharaja Trophy
Samit Dravid’s performance in the Maharaja Trophy is drawing widespread comparisons to his father, Rahul Dravid. Multipl...
VAR overturns late Croatia equaliser, extending Ronaldo’s World Cup stay
In the latest moment of Cristiano Ronaldo’s ongoing World Cup run, a major VAR decision denies Croatia a late equalising...
Youth Minister Clarifies NYSC Uniform: Adire Mentioned, Not Approved Replacement
Nigeria’s Minister of Youth Development, Ayodele Olawande, clarifies that Adire fabric is not yet approved as a replacem...