Skip to content

bip360: bound merkle depth at 128 and replace validation asserts in ref-impl - #2273

Open
let-the-dreamers-rise wants to merge 1 commit into
bitcoin:masterfrom
let-the-dreamers-rise:bip360-refimpl-depth-bound-and-raises
Open

bip360: bound merkle depth at 128 and replace validation asserts in ref-impl#2273
let-the-dreamers-rise wants to merge 1 commit into
bitcoin:masterfrom
let-the-dreamers-rise:bip360-refimpl-depth-bound-and-raises

Conversation

@let-the-dreamers-rise

Copy link
Copy Markdown

Two conformance/robustness fixes to bip-0360/ref-impl/python/p2mr.py, found by differential testing against an independent implementation (byte-for-byte agreement with the reference on all 16 official construction vectors and on 2000 seeded random script trees before divergence analysis). Reported to the BIP authors by email first; opening it here at @EthanHeilman's request so it is trackable — issues are disabled on this repo, so a PR is the closest equivalent.

1. No depth bound — the ref-impl can emit consensus-invalid control blocks

BIP 360 Script Validation: the control block "must have length 1 + 32 * m, for a value of m that is an integer between 0 and 128, inclusive." compute_merkle_root / compute_control_block applied no bound, so a 129-deep tree produced a normal-looking root and a 4129-byte control block (m = 129) — an output consensus must reject as unspendable.

import p2mr
leaf = lambda s: {"leafVersion": 0xC0, "script": s}
tree = leaf("51")
for _ in range(129):
    tree = [tree, leaf("52")]
print(len(p2mr.compute_control_block(0, tree)))   # 4129 before this PR

Now refused with ValueError; m = 128 is still accepted.

2. Validation asserts are stripped under python -O

assert len(tree) == 2 vanishes under -O, so a malformed 3-child branch silently returned the root of its first two leaves — the third script leaf dropped from the commitment with no error:

$ python3 -O
>>> import p2mr
>>> l = lambda s: {"leafVersion": 0xC0, "script": s}
>>> p2mr.compute_merkle_root([l("51"), l("52"), l("53")]).hex()
# identical to the 2-leaf root before this PR, no error

Validation in the construction path now raises explicitly, which survives -O. assert is left in place for genuine internal invariants.

Tests

Adds negative_structure_tests() — ternary branch refused, m = 128 accepted, m = 129 refused for both the root and the control block — run from BIP360_tests(). The existing 9/9 vector suite passes with and without -O.

Two related things I'd rather ask than patch: tapleaf_hash refuses empty scripts (deliberate, or defensive?), and construction silently masks odd leaf versions 0xc1 -> 0xc0 per v = c[0] & 0xfe (would you rather refuse odd versions than rewrite them?). Happy to follow up either way.

Full graded writeup with method and repro instructions: https://github.com/let-the-dreamers-rise/p2mr-assurance-lab/blob/main/FINDINGS.md

The control block length rule in Script Validation caps m at 128, but
compute_merkle_root and compute_control_block applied no depth bound, so a
129-deep tree produced a 4129-byte control block for an output consensus
must reject as unspendable.

Structural validation also used assert, which python -O strips: a malformed
3-child branch silently returned the root of its first two leaves rather
than raising, dropping a script leaf from the commitment.

Adds negative_structure_tests covering both, run from BIP360_tests.
@let-the-dreamers-rise
let-the-dreamers-rise force-pushed the bip360-refimpl-depth-bound-and-raises branch from 6ef7c33 to 3c8190e Compare September 2, 2026 23:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant