DEV Community

aaron.recompile
aaron.recompile

Posted on • Originally published at Medium

RootScope: A Tool for Reconstructing Taproot Script Paths — Step by Step

When you spend from a Taproot script path, the hash chain is deterministic. Reconstructing it by hand is a trap.



Taproot is elegant on paper. Spend from a script path, and the witness contains everything needed to prove your script was committed: the script itself, the control block, and the Merkle path up to the root. The math is clean.

The problem is inspection.

Given a real transaction, try to manually verify that the reconstructed Merkle root matches the output key. You’ll need to:

  1. Parse the control block byte by byte

  2. Compute TaggedHash("TapLeaf", version || compact_size(len) || script)

  3. Walk the sibling hashes in the control block up to the root

  4. Compute TaggedHash("TapTweak", internal_pubkey || root)

  5. Do the elliptic curve point addition on secp256k1

  6. Bech32m-encode the result

  7. Check that it matches the output address

Miss one step — wrong hash tag, wrong byte order, parity bit ignored — and you get a silent mismatch. No error message. Just a wrong address.

I built RootScope to do this deterministically and show every step.


What RootScope Does

Given a script and control block, RootScope computes the complete reconstruction chain:

TapLeaf hash
  → TapBranch step 1
  → TapBranch step 2
  → ...
  → Merkle root
  → TapTweak
  → tweaked output key
  → bech32m address
  → ✓ matches expected address (optional check)
Enter fullscreen mode Exit fullscreen mode

Each intermediate value is shown. Nothing is hidden.

There’s also a fetch-witness helper: give it a txid + vin, and it resolves the witness from a block explorer and prefills the inputs. No manual hex-copying from a mempool explorer.

Repo: github.com/aaron-recompile/rootscope


The Hash Chain, Concretely

BIP 341 defines three tagged hashes used in Taproot construction:

TapLeaf:

TapLeaf_hash = TaggedHash("TapLeaf", leafVersion || compact_size(len) || script)
Enter fullscreen mode Exit fullscreen mode

TapBranch (each step up the Merkle path):

TapBranch_hash = TaggedHash("TapBranch", left || right)
Enter fullscreen mode Exit fullscreen mode

where left and right are the lexicographically-sorted pair — the control block provides sibling hashes in order, so sorting happens at construction time.

TapTweak:

tweak = TaggedHash("TapTweak", internal_pubkey || merkle_root)
taproot_pubkey = lift_x(internal_pubkey) + tweak × G
Enter fullscreen mode Exit fullscreen mode

The final output key is the x-coordinate of that point. Parity check against the control block’s low bit completes the verification.

RootScope implements all of this in pure Python — hashlib only, no external crypto libraries. crypto.py re-implements tagged_hash, secp256k1 lift_x, point_add, point_mul, and bech32m_encode/decode from scratch. Every function is independently testable.


Validation

Before touching mainnet data, I ran the full BIP 341 test vectors.

wallet-test-vectors.json contains 6 test cases with 12 distinct script-path spending paths — including unbalanced trees (case 3) and mixed-depth paths (cases 5 and 6). All 12 pass:

$ make bip341-vectors
Running BIP341 test vectors...
 [PASS] Case 1 - path 0/0
 [PASS] Case 1 - path 0/1
 [PASS] Case 2 - path 0/0
 ...
 [PASS] Case 6 - path 0/1/0


12 / 12 PASS
Enter fullscreen mode Exit fullscreen mode

Additional vectors from Mastering Taproot cover single-leaf, dual-leaf (balanced), and the first documented 4-leaf Taproot tree — including the full unbalanced variant from Chapter 8.

Control block guard checks:

  • (len - 33) % 32 == 0 — control block length must be valid

  • depth ≤ 128 — as specified in BIP 341

  • output-key parity check against the version & 0x01 bit

Quick repro:

git clone https://github.com/aaron-recompile/rootscope
cd rootscope
python3 -m venv .venv && ./.venv/bin/python -m pip install -r backend/requirements.txt

make bip341-vectors
./.venv/bin/python -m backend.cli tx \
  f1de8d3b3894a7cc0efffa7332bf7236ad089195b9d642121f4844f13e01f1e0 \
  --vin 0 --network mainnet
Enter fullscreen mode Exit fullscreen mode

The Deeper the Tree, the More the Control Block Reveals

Most mainnet script-path spends use single-leaf trees — 33-byte control blocks with no Merkle path at all. That’s the on-chain reality. But it’s not what Taproot was designed for. The interesting structures are in the Mastering Taproot book vectors and the BIP 341 test cases.

Depth progression: single-leaf to 4-leaf

The book’s three chapters correspond to three tree shapes. Control block length grows linearly with depth:

Chapter 06 — single leaf (depth=0)
control block = 1 + 32 = 33 bytes

internal_key
       │
     leaf_A  ← script here, no sibling hashes in control block
Chapter 07 - dual-leaf balanced (depth=1)
control block = 1 + 32 + 32 = 65 bytes


  internal_key
       │
    Branch
   ┌───┴───┐
 leaf_A  leaf_B  ← reveal A, control block carries hash of B
Chapter 08 - 4-leaf balanced (depth=2)
control block = 1 + 32 + 32 + 32 = 97 bytes


  internal_key
       │
      Root
   ┌───┴────┐
Branch0   Branch1
┌──┴──┐  ┌──┴──┐
S0   S1  S2   S3  ← reveal S1: control block carries [S0_hash, Branch1_hash]
Enter fullscreen mode Exit fullscreen mode

Each additional depth level adds 32 bytes. Verification complexity is O(depth).

Unbalanced trees: two different proofs, same output address

This is the most counterintuitive part of Taproot’s design: different leaves in the same tree can have different control block depths, but all resolve to the same output address.

Unbalanced 3-leaf tree:

      Root
    ┌──┴──┐
  Branch  leaf_C  ← depth=1, control block carries Branch_hash
  ┌─┴─┐
leaf_A leaf_B     ← depth=2, control block carries [leaf_B_hash, leaf_C_hash]
Enter fullscreen mode Exit fullscreen mode

Revealing leaf_A (depth=2):

control_block = version | parity
              + internal_pubkey       (32 bytes)
              + leaf_B_hash           (32 bytes, sibling)
              + leaf_C_hash           (32 bytes, sibling one level up)
total = 97 bytes
Enter fullscreen mode Exit fullscreen mode

Revealing leaf_C (depth=1):

control_block = version | parity
              + internal_pubkey       (32 bytes)
              + Branch_AB_hash        (32 bytes, entire left subtree)
total = 65 bytes
Enter fullscreen mode Exit fullscreen mode

Two paths, identical Merkle root, identical output address. RootScope reconstructs both correctly and produces consistent results.

BIP 341 Case 3: non-standard leaf version

Test case 3 in wallet-test-vectors.json includes a path with leaf version 0xfa — not the standard Tapscript 0xc0:

control_block = "faee4fe085983462a184015d1f782d6a5f8b9c2b..."
#                ^^
#                0xfa = non-standard leaf version, low bit = parity
Enter fullscreen mode Exit fullscreen mode

The script for this path is the raw bytes 06424950333431 — ASCII "BIP341". From the perspective of address verification, the leaf version only affects the TapLeaf hash computation. TapBranch and TapTweak are unaffected. RootScope handles this correctly.


Open Questions

  1. Depth distribution at scale. Does depth > 0 appear more often in protocol-specific spending — Lightning, RGB, covenant constructions? Are there real multi-leaf trees in the wild beyond ordinals infrastructure?

  2. Template labeling. The current pipeline identifies templates by script structure. Automatic classification is possible but risks false positives. Is conservative, opt-in labeling better for a tool like this?

  3. Dataset integration. Are there public datasets of script-path spends — academic datasets, mempool.space exports, or other tooling outputs — that would plug directly into the batch pipeline?


Repo: github.com/aaron-recompile/rootscope


Related: [*Building a 4-Leaf Taproot Tree in Python](https://medium.com/@aaron.recompile/building-a-4-leaf-taproot-tree-in-python-the-first-complete-implementation-on-bitcoin-testnet-3a9b2c8e7f1d) · [*Taproot Control Block Deep Analysis](https://medium.com/@aaron.recompile/taproot-control-block-deep-analysis-stack-execution-visualization-5ff10f98032c)

By Aaron Recompile on March 9, 2026.

Canonical link

Exported from Medium on July 3, 2026.

Top comments (0)