Creating a Taproot output with a 2-of-2 multisig script:
From TeachMeBitcoin, the free encyclopedia
Reading time: 4 min
7. Fully Annotated P2TR Script Path Execution
Script path spending reveals one of the scripts hidden in the Taproot MAST tree. It is used when the key path is unavailable or when a specific script condition must be demonstrated on-chain.
Example: 2-of-2 Multisig in Script Path
# Creating a Taproot output with a 2-of-2 multisig script:
# The Tapscript leaf:
tapscript = (
bytes([0x20]) + pubkey_alice + # Push Alice's 32-byte x-only pubkey
bytes([0xac]) + # OP_CHECKSIG
bytes([0x20]) + pubkey_bob + # Push Bob's 32-byte x-only pubkey
bytes([0xba]) + # OP_CHECKSIGADD
bytes([0x52]) + # OP_2
bytes([0x87]) # OP_EQUAL
)
# Compute leaf hash:
leaf_hash = tagged_hash("TapLeaf", bytes([0xc0]) + compact_size(len(tapscript)) + tapscript)
# Since there's only one leaf, merkle_root = leaf_hash
merkle_root = leaf_hash
# Create P2TR output:
scriptPubKey = create_p2tr_output(internal_key, merkle_root)
Script Path Witness Structure
For 2-of-2 multisig script path spend:
Witness stack (no annex):
[
<alice_schnorr_sig>, ← satisfies OP_CHECKSIG for Alice's key
<bob_schnorr_sig>, ← satisfies OP_CHECKSIGADD for Bob's key
<tapscript>, ← the script being executed (105 bytes for 2-of-2)
<control_block> ← 33 bytes (no merkle path since only 1 leaf)
]
Control block for single-leaf tree (33 bytes):
Byte 0: 0xc0 | parity_bit (0xc0 = leaf version, | parity of tweaked key y)
Bytes 1-32: internal_key (the 32-byte x-only internal public key)
(no merkle path — leaf IS the root)
Step-by-Step Script Path Validation
def validate_taproot_script_path_example(witness, scriptPubKey):
"""
Detailed validation walkthrough for 2-of-2 Tapscript.
"""
# === Step 1: Parse witness ===
alice_sig = witness[0] # 64 bytes
bob_sig = witness[1] # 64 bytes
tapscript = witness[2] # The script
control_block = witness[3] # 33 bytes (single leaf)
# === Step 2: Parse control block ===
cb_version_parity = control_block[0]
leaf_version = cb_version_parity & 0xfe # 0xc0
parity = cb_version_parity & 0x01 # 0 or 1
internal_key = control_block[1:33] # 32 bytes
merkle_path = [] # Empty — single leaf
# === Step 3: Compute leaf hash ===
leaf_hash = tagged_hash(
"TapLeaf",
bytes([leaf_version]) + compact_size(len(tapscript)) + tapscript
)
# === Step 4: Walk merkle path ===
# No path to walk — single leaf
merkle_root = leaf_hash
# === Step 5: Compute tweaked key and verify ===
tweak = tagged_hash("TapTweak", internal_key + merkle_root)
tweak_int = int.from_bytes(tweak, 'big')
internal_point = lift_x(int.from_bytes(internal_key, 'big'))
tweaked_point = point_add(internal_point, point_mul(G, tweak_int))
output_key = scriptPubKey[2:] # 32-byte key from OP_1 <key>
assert x_only(tweaked_point) == output_key, "Key mismatch!"
assert (tweaked_point.y % 2 == 0) == (parity == 0), "Parity mismatch!"
# === Step 6: Execute the Tapscript ===
# Initial stack from witness (excluding script and control block):
stack = [alice_sig, bob_sig]
# Tapscript: <alice_pk> OP_CHECKSIG <bob_pk> OP_CHECKSIGADD OP_2 OP_EQUAL
# Instruction 1: Push alice_pk (32 bytes)
stack.append(pubkey_alice)
# Stack: [alice_sig, bob_sig, alice_pk]
# Instruction 2: OP_CHECKSIG
sig = stack.pop() # alice_sig (bottom of top pair)
pk = stack.pop() # alice_pk
# Wait — Tapscript CHECKSIG pops pubkey first, then sig
# Stack before CHECKSIG: [alice_sig, bob_sig, alice_pk]
pk = stack.pop() # alice_pk
sig = stack.pop() # bob_sig ← Hmm,
# Correct execution for: <alice_sig> <bob_sig> <alice_pk> OP_CHECKSIG <bob_pk> OP_CHECKSIGADD OP_2 OP_EQUAL
# Witness items become initial stack bottom-to-top
# Correct trace:
# Initial: [alice_sig, bob_sig] (alice_sig at bottom, bob_sig at top)
# Push alice_pk: [alice_sig, bob_sig, alice_pk]
# OP_CHECKSIG: pops alice_pk and bob_sig
# verifies bob_sig against alice_pk → FAIL
# The script must match witness ordering! Correct script for this witness:
# <bob_pk> OP_CHECKSIG <alice_pk> OP_CHECKSIGADD OP_2 OP_EQUAL
# Push 0 first: 0 <alice_sig> <bob_sig>
# Push bob_pk, CHECKSIG: bob_sig vs bob_pk → 1
# Push alice_pk, CHECKSIGADD: alice_sig + 1 → 2
# OP_2 OP_EQUAL: 2 == 2 → TRUE
print("Script execution successful!")
Technical Insight
This topic covers essential mechanics for Chapter 12. Understanding these details is key to mastering advanced Bitcoin script constructions like Taproot and specialized covenants.
☕ Help support TeachMeBitcoin
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:
Ethereum:
0x578417C51783663D8A6A811B3544E1f779D39A85
Bitcoin:
bc1q77k9e95rn669kpzyjr8ke9w95zhk7pa5s63qzz
Solana:
4ycT2ayqeMucixj3wS8Ay8Tq9NRDYRPKYbj3UGESyQ4J