Skip to content

Fix BIP-360 P2MR consensus, descriptor, policy, and test gaps#1

Open
starius wants to merge 17 commits into
jbride:p2mrfrom
starius:p2mr-fixes
Open

Fix BIP-360 P2MR consensus, descriptor, policy, and test gaps#1
starius wants to merge 17 commits into
jbride:p2mrfrom
starius:p2mr-fixes

Conversation

@starius

@starius starius commented Jun 30, 2026

Copy link
Copy Markdown

This PR fixes several BIP-360 P2MR implementation issues found while comparing the branch against the BIP text and the existing Taproot/Tapscript behavior.

P2MR was not enforced during block validation

SCRIPT_VERIFY_P2MR was wired into policy flags, but block script flags omitted it. A native witness-v2 32-byte output could still be spent inside a block through the unknown-witness-program success path.

Relevant broken code: block script flags omit P2MR and the interpreter returns success when P2MR is not enabled.

Fix: add P2MR to block script verification flags and add a functional block-rejection test proving an invalid native P2MR spend is rejected by consensus validation.

tmr(...) descriptors emitted witness version 3

tmr(...) generated OP_3 <32-byte-root>, while BIP-360 and the interpreter use native witness v2 (bc1z...).

Relevant broken code: descriptor output script construction.

Fix: emit OP_2 <32-byte-root> and add descriptor coverage that checks the script, solver type, and encoded address prefix.

Depth-zero P2MR executed the committed leaf script

BIP-360 says m = 0 succeeds immediately after the committed root matches. The implementation instead executed the committed tapscript leaf, so a valid depth-zero P2MR spend could fail if the committed script evaluated false.

Relevant broken code and test expectation: depth-zero execution path and test vector expecting failure.

Fix: return success after commitment and parity checks for zero-depth control blocks, and update script/functional tests to cover the intended anyone-can-spend behavior.

P2MR allowed over-deep control blocks

BIP-360 caps the Merkle path at 128 nodes. The implementation derived the maximum from Taproot's 33-byte control block and then extended it again, allowing up to 257 P2MR nodes instead of 1 + 32 * 128.

Relevant broken code: control block constants and P2MR control-size check.

Fix: cap P2MR control blocks at 1 + 32 * 128 and add a regression test for a 129-node control path.

P2MR parity was only enforced for tapscript leaves

BIP-360 requires the low bit of c[0] to be 1 for every leaf version, including future leaf versions. The implementation only enforced that rule for tapscript leaves.

Relevant broken code: parity check limited to tapscript.

Fix: reject low-bit-zero P2MR control bytes before leaf-version dispatch and add future-leaf parity coverage.

OP_SUCCESS127 was redefined globally for tapscript

The branch treated OP_SUCCESS127 as an SLH-DSA verifier for all tapscript spends, including ordinary P2TR spends, and skipped upstream OP_SUCCESS127 functional coverage. That changed BIP-342 behavior outside the P2MR proposal.

Relevant broken code and skipped coverage: global OP_SUCCESS127 verifier path and functional test skip.

Fix: remove the global OP_SUCCESS127 verifier path and restore the normal OP_SUCCESSx functional coverage.

The added P2QRH functional test did not validate P2MR

The test funded a witness-v3 output that decoded as witness_unknown, swallowed mempool/broadcast failures, and still reported success.

Relevant broken test: broad exception handling around failed spend paths.

Fix: remove that obsolete test and replace it with native witness-v2 P2MR functional coverage that must pass testmempoolaccept, broadcast, and mining.

P2MR address and script tests were weakened

Key I/O skipped normal checks for 0x52 scripts, the script fuzzer skipped WitnessV2P2MR, and script asset tests looked for OP_3 while describing witness version 2.

Relevant broken tests: key I/O skip, script fuzzer skip, and asset test OP_3 detection.

Fix: restore native v2/32 P2MR address round trips, fuzzer coverage, and OP_2 script detection, with an explicit P2MR key I/O regression test.

P2MR standardness policy used an oversized stack item limit

P2MR tapscript leaves follow BIP-342 execution rules, but policy still carried an 8000-byte P2MR-specific stack item limit and stale SLH-DSA comments.

Relevant broken policy: oversized P2MR stack item limit and stale SLH-DSA policy comments.

Fix: align P2MR standard stack item policy with the existing 80-byte tapscript limit, remove the stale comments, restrict the policy branch to native P2MR spends, and add a direct unit test for the 80/81-byte boundary.

Verification

build/bin/test_bitcoin --run_test=script_tests,script_standard_tests,key_io_tests,descriptor_tests,script_assets_tests,transaction_tests --catch_system_errors=no --log_level=test_suite

build/test/functional/test_runner.py --jobs=1 --combinedlogslen=0 feature_p2mr.py feature_taproot.py

🤖 Assisted by Codex GPT-5

starius added 17 commits June 30, 2026 02:33
P2MR validation was wired into standard and mandatory policy flags, but
block validation still built its script flag set from P2SH, witness, and
taproot only. A native v2 32-byte witness program could therefore be
spent arbitrarily inside a block because it continued down the
unknown-witness-program success path.

Include SCRIPT_VERIFY_P2MR in the block script flags so block connection
evaluates the P2MR control block, Merkle root, and tapscript rules
instead of treating these outputs as anyone-can-spend witness upgrades.
A native v2 32-byte output must not remain spendable through the generic
unknown-witness-program path once P2MR validation is active. This
regression test mines such an output, matures it, and submits a block
containing a one-element witness spend that has no P2MR script path.

With block script flags enforcing P2MR, submitblock fails with
block-script-verify-flag-failed. If the consensus flag wiring is
reverted, the same block connects because the spend is treated as an
unknown witness program.
BIP-360 assigns Pay-to-Merkle-Root to native witness version 2 with a
32-byte Merkle root program. The tmr() descriptor builder was emitting
OP_3, so descriptors produced v3/bech32m outputs that decoded as
witness_unknown and never exercised the P2MR validation path.

Emit OP_2 for tmr() outputs so descriptor-derived scripts match the
interpreter, address encoder, and solver's WITNESS_V2_P2MR type.
tmr() descriptors must materialize native witness v2 P2MR outputs.
Without coverage, the descriptor code could emit OP_3 and still parse
successfully while producing addresses that decode as witness_unknown.

Add a descriptor regression that expands tmr(pk(...)), asserts the
script starts with OP_2 OP_PUSHBYTES_32, checks Solver returns
WITNESS_V2_P2MR, and confirms the encoded address uses the bc1z
witness-v2 prefix.
A P2MR control block with no Merkle path commits directly to the witness
program. After the root check succeeds, BIP-360 specifies that this case
terminates successfully instead of executing the leaf script.

The interpreter was executing c1/tapscript leaves even when the control
block length was one byte, so a committed depth-zero OP_FALSE-style
script could fail. Return success after the commitment and parity checks
when the P2MR path depth is zero.
Depth-zero P2MR spends are anyone-can-spend after the committed root is
verified; the leaf script is data used for the commitment, not executed
script.

Update the existing unequal OP_EQUAL vector to expect success under P2MR
activation. Reverting the interpreter fix makes this vector fail with
EVAL_FALSE, which is the old incorrect execution path.
BIP-360 uses a one-byte P2MR control header and permits at most 128
Merkle branch nodes. The implementation derived the maximum from the
Taproot control size and then added another 128 nodes, allowing 257 P2MR
path elements.

Set P2MR_CONTROL_MAX_SIZE to 1 + 32 * 128 so over-deep P2MR script-path
spends fail at the control-size check before commitment validation.
P2MR has a maximum Merkle path depth of 128 branch nodes. A control
block with 129 nodes should fail because of its size, not after trying
to verify a commitment.

Add a script test that builds a 129-node P2MR control block and asserts
P2MR_WRONG_CONTROL_SIZE. Reverting the size cap changes the symptom to
WITNESS_PROGRAM_MISMATCH, proving the over-deep path was accepted past
the size check.
BIP-360 reserves the low bit of the P2MR control byte and requires it to
be set for every leaf version. The interpreter only enforced that rule
for tapscript leaves, so a future leaf version with low bit zero could
satisfy the commitment and be accepted as an upgradable leaf.

Reject any P2MR control byte with parity bit zero before leaf-version
dispatch, and make the error text describe the generic control-byte rule
rather than only the 0xc1 tapscript case.
The P2MR parity-bit rule is not limited to tapscript leaf version 0xc0.
Future leaf versions must also use control bytes with the low bit set.

Add a depth-zero future-leaf spend whose root matches when interpreted
as leaf version 0xc2 but whose control byte has parity bit zero.
Reverting the parity fix makes the spend return OK instead of
P2MR_WRONG_PARITY_BIT.
OP_SUCCESSx opcodes in tapscript must short-circuit to success unless
policy discouragement flags are being applied. This branch had redefined
OP_SUCCESS127/OP_SUBSTR as an SLH-DSA verifier for all tapscript spends,
including ordinary P2TR, so scripts that BIP-342 treats as unconditional
success could fail.

Remove the OP_SUCCESS127 verifier path, its oversized stack exception,
and the PQC/debug plumbing from the script interpreter. OP_SUBSTR now
follows the same OP_SUCCESS pre-scan as the other reserved success
opcodes.
OP_SUCCESS127 is reserved by BIP-342 like the other OP_SUCCESSx opcodes.
The functional taproot test had skipped opcode 127 because the branch
redefined it for SLH-DSA verification, so the regression was hidden.

Re-enable the existing OP_SUCCESSx cases for OP_SUBSTR. If the
interpreter again special-cases opcode 127, these spends fail instead of
following the normal OP_SUCCESS success path.
The P2QRH functional test did not validate P2MR behavior. It funded a
witness-v3 output that decoded as witness_unknown, then caught and
logged the testmempoolaccept and sendrawtransaction failures before
reporting success.

Remove the obsolete runner entry and test file so the suite no longer
carries coverage that can pass without exercising native witness-v2 P2MR
validation.
Functional P2MR coverage should prove that native witness-v2 outputs
reach the P2MR interpreter path, not the unknown-witness path. A
depth-zero P2MR leaf with a matching TapLeaf root can be spent without a
signature after the commitment check succeeds.

Extend feature_p2mr.py to fund a native v2 P2MR output, assert
testmempoolaccept allows the matching depth-zero spend, broadcast it,
and mine it. Reverting the descriptor/version or depth-zero validation
fixes makes this spend fail with a symptomatic policy or script error.
Several tests had temporary skips or version checks that avoided native
witness-v2 P2MR data. key_io_valid_parse skipped all 0x52 scripts, the
script fuzzer skipped WitnessV2P2MR destination round-trips, and
script_assets_tests looked for OP_3 while describing witness version 2.

Restore the normal key/address assertions for v2 scripts, let the fuzzer
round-trip WitnessV2P2MR destinations, and detect P2MR scriptPubKeys
with OP_2 OP_PUSHBYTES_32.
The generic key_io_valid fixtures include short witness-v2
unknown-program examples, but they did not pin the native v2/32 P2MR
destination path directly.

Add a unit test that constructs a WitnessV2P2MR root, encodes it as a
mainnet bech32m address, decodes it back to WitnessV2P2MR, and verifies
the generated scriptPubKey starts with OP_2 OP_PUSHBYTES_32.
P2MR tapscript leaves share the BIP 342 execution rules, so their
standard witness stack item limit should match the existing tapscript
policy limit.

The branch still carried an 8000-byte P2MR-specific limit and comments
about a removed SLH-DSA OP_SUCCESS127 experiment. That made standard
P2MR policy looser than taproot for no BIP-360 reason, and the
witness-standardness branch also described the output as witness version
3 even though P2MR is native witness v2.

Set the P2MR stack item limit back to 80 bytes, remove the stale SLH-DSA
wording, and apply this policy branch only to native v2/32 P2MR spends.
Add a transaction unit test that accepts an 80-byte initial stack item
and rejects an 81-byte item, so reverting the limit produces a direct
policy regression.
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