CryptoNetHub

Building a 4-Leaf Taproot Tree in Python: The First Complete Implementation on Bitcoin Testnet

HODLing is the beginning. But Bitcoin was meant to be programmed. “Not Just HODLing: Real Bitcoin Script Engineering” starts here. Enter fullscreen modeExit fullscreen modeMost Bitcoin Taproot tutorials stop. . .

· 7 min read

Building a 4-Leaf Taproot Tree in Python: The First Complete Implementation on Bitcoin Testnet

HODLing is the beginning. But Bitcoin was meant to be programmed. “Not Just HODLing: Real Bitcoin Script Engineering” starts here. Enter fullscreen modeExit fullscreen modeMost 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:tb1pjfdm902y2adr08qnn4tahxjvp6x5selgmvzx63yfqk2hdey02yvqjcr29q5 Confirmed Spending Transactions (we’ll show you the TXIDs)Every script path works flawlesslyWhat makes this significant:Complete validation: Not just theory — every spending path confirmed on testnetProduction-ready code: Real Python implementation using bitcoinutilsComplex conditional logic: Hash locks, multisig, timelocks, and fallbacks in one treeMaximum privacy: Key path spends look identical to regular transactionsThis implementation proves that sophisticated Bitcoin smart contracts can operate with the same privacy and efficiency as simple payments. Why 4-Leaf Trees MatterMost 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 pathsLightning Channels: Multiple cooperative close scenarios with different participant setsAtomic Swaps: Hash time locked contracts with various fallback conditionsInheritance Planning: Time-based access with multiple beneficiary optionsTechnical Advantages:Selective revelation: Only the executed script is exposed, others remain hiddenFee efficiency: Smaller than equivalent traditional multi-condition scriptsFlexible logic: Multiple execution paths in a single commitmentThe 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 DesignWe’ll build this balanced Merkle tree structure:Merkle Root / \ Branch0 Branch1 / \ / \ Script0 Script1 Script2 Script3 Hashlock Multi CSV SigEnter fullscreen modeExit fullscreen modeEach script serves a different purpose in a realistic multi-party protocol:Script 0 (SHA256 Hashlock): Anyone with the preimage “helloworld” can spendScript 1 (2-of-2 Multisig): Requires cooperation between Alice and BobScript 2 (CSV Timelock): Bob can spend after waiting 2 blocksScript 3 (Simple Signature): Bob can spend immediately with his signaturePlus theKey Path: Alice can spend with maximum privacy using her tweaked private key. Implementation Setupfrombitcoinutils. setupimportsetupfrombitcoinutils. keysimportPrivateKeyfrombitcoinutils. scriptimportScriptfrombitcoinutils. transactionsimportTransaction,TxInput,TxOutput,TxWitnessInputfrombitcoinutils. utilsimportControlBlockimporthashlib# Use testnet for safe experimentationsetup('testnet')# Generate keys for our participantsalice_priv=PrivateKey. from_wif("")bob_priv=PrivateKey. from_wif("")alice_pub=alice_priv. get_public_key()bob_pub=bob_priv. get_public_key()Enter fullscreen modeExit fullscreen modeBuilding the Four ScriptsScript 0: SHA256 HashlockThis script implements a simple hash-time-lock pattern common in atomic swaps:# Anyone who knows the preimage can spendpreimage="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()}")Enter fullscreen modeExit fullscreen modeHow 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 efficientOP_CHECKSIGADDinstead of legacyOP_CHECKMULTISIG:# Requires both Alice and Bob's signaturesscript2=Script(["OP_0",# Initialize counteralice_pub. to_x_only_hex(),# Alice's x-only pubkey"OP_CHECKSIGADD",# Verify Alice sig, increment counterbob_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])Enter fullscreen modeExit fullscreen modeKey innovation:OP_CHECKSIGADDis more efficient thanOP_CHECKMULTISIGand handles x-only public keys natively. Script 2: CSV TimelockThis implements a relative timelock — Bob can spend after a certain number of blocks:frombitcoinutils. utilsimportSequence,TYPE_RELATIVE_TIMELOCK# Bob can spend after waiting 2 blocksrelative_blocks=2seq=Sequence(TYPE_RELATIVE_TIMELOCK,relative_blocks)script3=Script([seq. for_script(),# Push sequence value"OP_CHECKSEQUENCEVERIFY",# Verify relative timelock"OP_DROP",# Clean up stackbob_pub. to_x_only_hex(),# Bob's pubkey"OP_CHECKSIG"# Verify Bob's signature])Enter fullscreen modeExit fullscreen modeImportant: The transaction input must set the sequence number to enable the timelock. Script 3: Simple SignatureThe simplest script — just Bob’s signature:# Bob can spend immediatelyscript4=Script([bob_pub. to_x_only_hex(),"OP_CHECKSIG"])Enter fullscreen modeExit fullscreen modeCreating the Taproot AddressNow 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 keytaproot_address=alice_pub. get_taproot_address(tree)print(f"Taproot Address:{taproot_address. to_string()}")# This is where we'll send our test fundscommit_address="tb1pjfdm902y2adr08qnn4tahxjvp6x5selgmvzx63yfqk2hdey02yvqjcr29q"Enter fullscreen modeExit fullscreen modeThis address is acryptographic commitmentto all four scripts plus Alice’s key path. From the outside, it’s indistinguishable from any other Taproot address. Spending Path 1: SHA256 HashlockFirst, let’s spend using the hashlock by revealing the preimage:# Build the spending transactioncommit_txid="245563c5aa4c6d32fc34eed2f182b5ed76892d13370f067dc56f34616b66c468"# From your funding transactioninput_amount=1200# satoshisoutput_amount=666txin=TxInput(commit_txid,0)# spending output 0txout=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 hashlockscript1. to_hex(),# The script being executedcb. to_hex()# Proof that this script was committed])tx. witnesses. append(witness)print(f"Hashlock Transaction:{tx. serialize()}")Enter fullscreen modeExit fullscreen modeTestnet Result: txid:1ba4835fca1c94e7eb0016ce37c6de2545d07d84a97436f8db999f33a6fd6845Spending Path 2: 2-of-2 MultisigFor 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 orderwitness=TxWitnessInput([sig_bob,# Consumed second by the scriptsig_alice,# Consumed first by the scriptscript2. to_hex(),cb. to_hex()])tx. witnesses. append(witness)Enter fullscreen modeExit fullscreen modeTestnet Result:1951a3be0f05df377b1789223f6da66ed39c781aaf39ace0bf98c3beb7e604a1Spending Path 3: CSV TimelockThe timelock spend requires setting a custom sequence number:# Create transaction with custom sequence for timelockseq_for_input=seq. for_input_sequence()txin=TxInput(commit_txid,0,sequence=seq_for_input)# Key difference!# Rest is similar to other script path spendscb=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()])Enter fullscreen modeExit fullscreen modeTestnet Result: txid:98361ab2c19aa0063f7572cfd0f66cb890b403d2dd12029426613b40d17f41eeSpending Path 4: Simple SignatureThe 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()])Enter fullscreen modeExit fullscreen modeTestnet Result: txid:1af46d4c71e121783c3c7195f4b45025a1f38b73fc8898d2546fc33b4c6c71b9Spending 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 keysig_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 pathtapleaf_scripts=tree# Needed for tweak calculation)# Witness contains only the signature-maximum efficiency!witness=TxWitnessInput([sig_alice])tx. witnesses. append(witness)Enter fullscreen modeExit fullscreen modeTestnet Result: txid:1e518aa540bc770df549ec9836d89783ca19fc79b84e7407a882cbe9e95600daCommon Pitfalls and SolutionsWitness Stack OrderingThe multisig witness order is critical:# ❌ Wrong: Alice sig firstwitness=[sig_alice,sig_bob,script,control_block]Enter fullscreen modeExit fullscreen mode# ✅ Correct: Bob sig first (consumed second)witness=[sig_bob,sig_alice,script,control_block]Enter fullscreen modeExit fullscreen modeSequence Numbers for CSVCSV scripts require specific transaction sequence values:# ❌ Wrong: Default sequencetxin=TxInput(txid,vout)Enter fullscreen modeExit fullscreen mode# ✅ Correct: CSV-compatible sequencetxin=TxInput(txid,vout,sequence=seq. for_input_sequence())Enter fullscreen modeExit fullscreen modeScript Path vs Key Path SigningThe signing process is different for each path:# Key path: script_path=False, provide tree for tweaksig=priv. sign_taproot_input(. . . ,script_path=False,tapleaf_scripts=tree)Enter fullscreen modeExit fullscreen mode# Script path: script_path=True, provide specific scriptsig=priv. sign_taproot_input(. . . ,script_path=True,tapleaf_script=script)Enter fullscreen modeExit fullscreen modeWhat This ProvesThis 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 ResourcesPart 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 inclusionthrough Merkle proofsWhy witness stack ordering mattersfor script executionThe mathematical guaranteesbehind Taproot’s privacy propertiesSecurity considerationsfor production deploymentsOptimization strategiesfor even larger script treesWe’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 CodeAll code and on-chain data analysis are available at mygithubrepoFeel 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)*. *ByAaron RecompileonJuly 2, 2025. Canonical linkExported fromMediumon July 3, 2026.

Source: dev.to

More Bitcoin news