Example control block for a leaf at depth 2:
8. Script Path Spending in Taproot
Script path spending is one of the two ways to spend a P2TR (Taproot) output. It involves revealing one of the scripts committed to in the Taproot output's Merkle tree and providing witness data to satisfy that script.
The Full Script Path Spending Flow
Step 1: Sender creates a P2TR output
The sender constructs a script tree (MAST):
Root
/ \
Leaf A Branch
/ \
Leaf B Leaf C
Each leaf contains a Tapscript:
- Leaf A: 2-of-2 multisig between Alice and Bob
- Leaf B: Alice alone after 6 months (timelock)
- Leaf C: 3-of-5 federation recovery
The sender tweaks an internal key with the Merkle root:
tweaked_pubkey = internal_key + tagged_hash("TapTweak", internal_key + merkle_root) * G
Output: OP_1 <tweaked_pubkey_x_coordinate>
Step 2: Spender chooses a script path
To spend via Leaf B (Alice's timelock path):
1. Assemble the Tapscript for Leaf B
2. Build the control block proving Leaf B is in the tree
3. Provide the script inputs (Alice's signature)
Step 3: Construct the witness stack
Witness stack (script path):
[
<alice_schnorr_signature>, ← script inputs (satisfying Leaf B)
<leaf_B_tapscript>, ← the script being executed
<control_block> ← proof of inclusion in the tree
]
If annex present:
[
<alice_schnorr_signature>,
<leaf_B_tapscript>,
<control_block>,
<annex>
]
The Control Block in Detail
The control block is the cryptographic proof that the revealed script is actually part of the committed Taproot tree. It contains the internal public key and the Merkle path from the leaf to the root.
def build_control_block(internal_pubkey, leaf_version, merkle_path, tweaked_pubkey_parity):
"""
internal_pubkey: 32-byte x-only pubkey
leaf_version: e.g., 0xc0
merkle_path: list of 32-byte branch hashes
tweaked_pubkey_parity: 0 or 1 (parity of tweaked key y-coordinate)
"""
version_and_parity = (leaf_version | tweaked_pubkey_parity).to_bytes(1, 'big')
path_bytes = b''.join(merkle_path)
return version_and_parity + internal_pubkey + path_bytes
# Example control block for a leaf at depth 2:
# 1 byte (version+parity) + 32 bytes (internal key) + 2×32 bytes (path) = 97 bytes
Validation of Script Path Spending
When a node sees a Taproot input spending via the script path, it validates in this order:
def validate_taproot_script_path(witness, scriptPubKey):
# 1. Extract components
if has_annex(witness):
annex = witness[-1]
control_block = witness[-2]
tapscript = witness[-3]
script_inputs = witness[:-3]
else:
control_block = witness[-1]
tapscript = witness[-2]
script_inputs = witness[:-2]
# 2. Parse control block
leaf_version = control_block[0] & 0xfe
parity = control_block[0] & 0x01
internal_key = control_block[1:33]
merkle_path = [control_block[33+i*32 : 65+i*32] for i in range((len(control_block)-33)//32)]
# 3. Compute leaf hash
leaf_hash = tagged_hash("TapLeaf",
bytes([leaf_version]) + compact_size(len(tapscript)) + tapscript)
# 4. Compute Merkle root by walking up the path
current = leaf_hash
for branch_hash in merkle_path:
if current <= branch_hash:
current = tagged_hash("TapBranch", current + branch_hash)
else:
current = tagged_hash("TapBranch", branch_hash + current)
merkle_root = current
# 5. Verify the tweaked pubkey matches
tweak = tagged_hash("TapTweak", internal_key + merkle_root)
expected_tweaked_key = point_add(point(internal_key), point_mul(G, tweak))
output_key = scriptPubKey[2:] # The 32-byte x-only key from OP_1 <key>
if x_only(expected_tweaked_key) != output_key:
return False # Wrong key
if has_even_y(expected_tweaked_key) != (parity == 0):
return False # Wrong parity
# 6. Execute the tapscript
return execute_tapscript(tapscript, script_inputs)
Technical Insight
This topic covers essential mechanics for Chapter 11. Understanding these details is key to mastering advanced Bitcoin script constructions like Taproot and specialized covenants.
TeachMeBitcoin is an ad-free, open-source educational repository curated by a passionate team of Bitcoin researchers and educators for public benefit. If you found our articles helpful, please consider supporting our hosting and ongoing content updates with a clean donation: