P2TR MAST - Merklized Alternative Script Trees

Overview

MAST (Merklized Alternative Script Trees) is a technique that allows multiple spending conditions to be committed to a single output while only revealing the condition that is actually used at spending time. In Taproot (P2TR), MAST is implemented through a Merkle tree of scripts that is tweaked into the output public key.

The key insight is that complex Bitcoin contracts often have multiple spending paths, but typically only one path is ever executed. With MAST, the unused paths remain completely hidden, providing both privacy (observers cannot see what other conditions existed) and efficiency (only the executed script and its merkle proof are published on-chain).

Key Concept

A P2TR output commits to both an internal public key (for key-path spending) and an optional script tree (for script-path spending). The script tree uses Merkle tree construction so that only the executed branch needs to be revealed.

P2TR MAST - Merklized Alternative Script Trees Multiple spending conditions, reveal only what you use Script Tree Construction TapLeaf = tagged_hash("TapLeaf", leaf_version || compact_size(script) || script) Script A (Leaf) e.g. 2-of-3 multisig 0xc0 || script_a Script B (Leaf) e.g. timelock + key 0xc0 || script_b Script C (Leaf) e.g. hashlock 0xc0 || script_c Script D (Leaf) e.g. recovery key 0xc0 || script_d TapBranch = tagged_hash("TapBranch", sorted(left_hash || right_hash)) Branch AB hash(A || B) sorted Branch CD hash(C || D) sorted Merkle Root (m) 32 bytes - commits to all scripts Lexicographic Sorting Child hashes are sorted before concatenation to ensure unique tree structure regardless of insertion order. This prevents different orderings from producing different roots for the same set of scripts. Output Key Derivation Internal Key (P) 32 bytes x-only pubkey + Merkle Root (m) from script tree TapTweak t = H(P || m) Q = P + t·G Output Key Q (on-chain) The output key Q commits to both the internal key P AND the entire script tree via the merkle root m Script Path Spending (Revealing Script B) Script B (revealed) execute this script REVEALED Hash of Script A sibling hash in merkle proof Hash of Branch CD sibling branch hash in merkle proof Scripts C, D NEVER revealed privacy preserved Control Block (witness element) leaf_ver 1 byte (0xc0) Internal Key (P) 32 bytes Hash(Script A) 32 bytes Hash(Branch CD) 32 bytes ... more hashes 32 bytes each Witness Stack Control Block Script B Script Inputs (sig, ...) top

Script Tree Construction

The script tree is a binary Merkle tree where each leaf contains a spending script. The tree is constructed bottom-up, starting from the leaf scripts and combining them into branch nodes until a single root hash is produced.

Leaf Nodes (TapLeaf)

Each script is converted into a leaf hash using the TapLeaf tagged hash. The leaf hash commits to both the leaf version and the script content:

TapLeaf = tagged_hash("TapLeaf", leaf_version || compact_size(script) || script)

The leaf_version is currently 0xc0 for Tapscript (the default). This version byte allows for future script upgrades while maintaining backward compatibility. The compact size prefix indicates the length of the script that follows.

Branch Nodes (TapBranch)

Internal branch nodes are computed by hashing the concatenation of their two child hashes. Critically, the child hashes are lexicographically sorted before concatenation:

TapBranch = tagged_hash("TapBranch", sorted(left_hash || right_hash))
Why Lexicographic Sorting?

Sorting ensures that the same set of scripts always produces the same merkle root, regardless of the order in which they were inserted into the tree. This is essential for deterministic address generation and prevents subtle bugs where different implementations might produce different addresses for the same scripts.

Tagged Hashing

Taproot uses "tagged hashes" throughout its construction. A tagged hash prefixes the data with a tag-specific value to create domain separation:

tagged_hash(tag, data) = SHA256(SHA256(tag) || SHA256(tag) || data)

The tag is hashed twice and prepended to the data. This technique prevents cross-protocol attacks where a valid hash in one context could be reinterpreted in another context. Each operation in Taproot uses a unique tag:

Tag Usage
TapLeaf Hashing individual scripts into leaf nodes
TapBranch Combining two child hashes into a branch node
TapTweak Computing the tweak from internal key and merkle root
TapSighash Computing the signature hash for Taproot spends

Output Key Derivation

The output key Q that appears on-chain is derived from two components:

  • The internal public key P.
  • The merkle root m of the script tree.

This derivation uses elliptic curve point addition:

Step 1: Compute the Tweak

t = tagged_hash("TapTweak", P || m)

The tweak t is a 32-byte scalar derived from the concatenation of the internal key's x-coordinate and the merkle root.

Step 2: Apply the Tweak

Q = P + t·G

The output key is computed by adding the internal key point P to the tweak scalar multiplied by the generator point G. This is standard elliptic curve point addition on secp256k1.

Key Property

The output key Q commits to both the internal key and the entire script tree. Anyone who knows P and m can verify that Q was correctly derived, but without this knowledge, Q looks like any other public key.

Key-Path Only Outputs

If no script tree is needed (key-path only), the merkle root is omitted and the tweak is computed as:

t = tagged_hash("TapTweak", P)

This still applies a tweak to prevent certain attacks, but commits only to the internal key.

Spending Paths

A P2TR output can be spent in two ways:

  • The key path (using the internal key).
  • A script path (using one of the scripts in the tree).

Key Path Spend

The spender provides a Schnorr signature for the output key Q.

  • Most efficient (single 64-byte signature).
  • Indistinguishable from script path spends on-chain.
  • Requires knowledge of the internal key's private key.
  • Script tree remains completely hidden.

Witness Stack:

top Signature

Script Path Spend

The spender reveals and executes one script from the tree.

  • Requires merkle proof to the executed script.
  • Only the used script is revealed.
  • Other scripts remain hidden.
  • Proof size is O(log n) for n scripts.

Witness Stack:

top Control Block Script Script Inputs

Control Block Structure

When spending via script path, the witness includes a "control block" that provides the merkle proof. The control block contains:

Field Size Description
leaf_version + parity 1 byte Leaf version (0xc0 or 0xc1) with output key parity in the lowest bit
internal_key 32 bytes The x-only internal public key P
merkle_path 32 × d bytes Sibling hashes from leaf to root (d = tree depth)

The control block size depends on the tree depth. For a balanced tree with n leaves, the merkle path contains log₂(n) hashes, each 32 bytes.

Control Block Layout Generic: leaf_version 1 byte (P) internal_key 32 bytes sibling_0 32 bytes sibling_1 32 bytes ... merkle_path (32 × depth bytes) Example (Script B, 4-script tree): 0xc0 1 byte P (internal key) 32 bytes Hash(Script A) 32 bytes Hash(Branch CD) 32 bytes Merkle Path (64 bytes)

Verification Process

When a node receives a script path spend, it must verify that the revealed script is committed to by the output key. The key insight is that the control block contains only the sibling hashes needed for the merkle proof, not the hash of the script being spent.

Why the Spent Script Hash is Not in the Control Block

When spending Script B, the script itself is revealed separately in the witness as raw bytes. The verifier computes the hash of this revealed script using TapLeaf. The control block only needs to provide the sibling hashes along the merkle path, because everything else can be derived:

  • Hash(Script A): Script B's sibling at the leaf level (from control block).
  • Hash(Branch CD): Branch AB's sibling at the branch level (from control block).

This is standard merkle proof logic: you reveal the data you're proving (Script B), and provide only the sibling hashes. The verifier reconstructs the path from leaf to root, then verifies it matches the committed output key.

Verification Path (Script B Example)

Verification Path for Script B Script B (revealed) from witness TapLeaf(Script B) verifier computes + Hash(Script A) from control block Hash(Branch AB) verifier computes + Hash(Branch CD) from control block Merkle Root (m) verifier computes + Internal Key (P) from control block Output Key (Q') Q' = P + H(P||m)·G Must match on-chain output key Q If match → script was committed at creation From control block Verifier computes From witness

Step-by-Step Verification

  1. Extract components: Parse the witness to get script inputs, the raw script, and the control block.
  2. Compute leaf hash: Hash the revealed script using TapLeaf(leaf_version, script). This produces Hash(Script B).
  3. Reconstruct Branch AB: Combine Hash(Script B) with Hash(Script A) from the control block using TapBranch (sorting lexicographically).
  4. Reconstruct Merkle Root: Combine Hash(Branch AB) with Hash(Branch CD) from the control block using TapBranch.
  5. Compute expected output key: Using the internal key P from the control block and the computed merkle root m, derive Q' = P + H(P||m)·G.
  6. Verify match: Check that Q' equals the actual on-chain output key Q (including y-coordinate parity from the control block's first byte).
  7. Execute script: If verification passes, execute the revealed script with the provided inputs.
Security Guarantee

If verification succeeds, the script was committed to at output creation time. No script can be "injected" after the fact because the merkle root is cryptographically bound to the output key.

Practical Examples

Example 1: 2-of-2 Multisig with Timeout Recovery

A common pattern for shared custody where two parties (Alice and Bob) normally spend together, but Alice can recover funds alone after a timeout period. This is useful for escrow, joint accounts, or any situation where cooperative spending is preferred but a fallback is needed.

2-of-2 with Timeout Recovery Internal Key (P) MuSig2(Alice, Bob) aggregated key Merkle Root script tree (fallback only) Script A: Cooperative OP_CHECKSIGADD 2-of-2 (backup if MuSig fails) Script B: Timeout CSV 52560 + Alice key (~1 year timelock) Spending Scenarios Normal: Key path (MuSig2 sig) — most efficient Recovery: Script B after 1 year — Alice alone

How It Works

The internal key is a MuSig2 aggregated public key combining Alice and Bob's keys. When both parties cooperate, they produce a single Schnorr signature for the key path spend. This is the most efficient option (64-byte witness) and reveals nothing about the timeout fallback.

If Bob becomes unresponsive or uncooperative, Alice waits for the timelock (e.g., 52560 blocks ≈ 1 year) and then spends via Script B. The witness includes her signature, the script, and a control block with Hash(Script A) as the sibling proof. Script A's existence is revealed only as a hash, not its contents.

Why include Script A?

Script A provides a non-timelocked 2-of-2 multisig as backup in case MuSig2 signing fails (e.g., nonce generation issues). In practice, key path is always preferred, so Script A is rarely used. Placing the timeout script (more likely fallback) at the same tree level keeps proof sizes equal.

Example 2: Inheritance with Multiple Heirs

A sophisticated inheritance setup where different beneficiaries can claim funds after progressively longer timeouts. The owner retains full control via key path, and heirs only gain access if the owner becomes incapacitated or passes away.

Inheritance with Multiple Heirs Internal Key (P) Owner's key Merkle Root Branch AB Branch CD Script A Spouse key CSV ~1 year Script B Child key CSV ~2 years Script C Lawyer key CSV ~3 years Script D Charity key CSV ~5 years Now 1 year 2 years 3 years 5 years Key path spend (owner) reveals nothing about inheritance structure Heirs don't know they're beneficiaries until a script path is used

Spending Priority and Privacy

The owner normally spends via key path, which is indistinguishable from any other Taproot transaction. The entire inheritance structure remains hidden. Heirs don't even know they're beneficiaries, providing protection against social engineering or coercion.

If the owner becomes incapacitated, heirs can claim in priority order based on timelock expiration:

  • Spouse (1 year): First to gain access. If they claim, only Script A is revealed. The existence of Scripts B, C, D remains hidden (only their branch hashes appear in the control block).
  • Child (2 years): Can claim if spouse doesn't act within 2 years. Reveals Script B, hides C and D.
  • Lawyer (3 years): Estate executor fallback if family doesn't claim.
  • Charity (5 years): Ultimate fallback preventing permanent loss of funds.

What Each Heir Sees

When the spouse claims via Script A, the on-chain witness reveals:

  • Script A contents (spouse's key + 1-year timelock).
  • Hash(Script B): proves Script B exists but not its contents.
  • Hash(Branch CD): proves more scripts exist but reveals nothing about them.

The child, lawyer, and charity learn that two other spending conditions existed (from Hash(Script B) and Hash(Branch CD)), but cannot determine who or what they were. This preserves family privacy even after a claim.

Tree Optimization Note

In this example, the spouse script (most likely to be used) is placed at tree depth 2, same as all other scripts. For larger trees, you could place higher-priority heirs closer to the root to reduce their proof size. However, this would reveal information about priority ordering. The balanced tree approach treats all heirs equally in terms of on-chain footprint.

Refreshing the Setup

The owner can periodically move funds to a new P2TR output with updated timelocks, effectively resetting the countdown. This proves continued control and prevents heirs from counting down to access.