Skip to content

bip360: fix leafHashes order and a control byte in the pqc test vectors - #2220

Merged
jonatack merged 1 commit into
bitcoin:masterfrom
jeanpablojp:bip360-fix-pqc-vectors
Aug 1, 2026
Merged

bip360: fix leafHashes order and a control byte in the pqc test vectors#2220
jonatack merged 1 commit into
bitcoin:masterfrom
jeanpablojp:bip360-fix-pqc-vectors

Conversation

@jeanpablojp

@jeanpablojp jeanpablojp commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Two fixes in bip-0360/ref-impl/common/tests/data/p2mr_pqc_construction.json, found while implementing BIP 360 against the published vectors.

First, the three-leaf vectors (p2mr_three_leaf_complex and p2mr_three_leaf_alternative) list intermediary.leafHashes with entries 0 and 2 swapped relative to the depth-first order of the script tree. The merkle root, scriptPubKey, address and control blocks in those same vectors all follow depth-first order, as do the equivalent trees in p2mr_construction.json. This looks like a leftover from the ordering problem that #2202 fixed for the control blocks.

Check against the spec formulas:

import json, hashlib

def tagged(tag, data):
    t = hashlib.sha256(tag.encode()).digest()
    return hashlib.sha256(t + t + data).digest()

d = json.load(open("p2mr_pqc_construction.json"))
tv = [t for t in d["test_vectors"] if t["id"] == "p2mr_three_leaf_complex"][0]
leaves = []
def walk(n):
    if isinstance(n, list):
        for x in n: walk(x)
    else: leaves.append(n)
walk(tv["given"]["scriptTree"])
for i, leaf in enumerate(leaves):
    s = bytes.fromhex(leaf["script"])
    h = tagged("TapLeaf", bytes([leaf["leafVersion"]]) + bytes([len(s)]) + s).hex()
    print(i, h == tv["intermediary"]["leafHashes"][i])

Before this change that prints "0 False / 1 True / 2 False", i.e. entries 0 and 2 hold each other's values. Same for p2mr_three_leaf_alternative.

Second, in p2mr_different_version_leaves the second leaf has "leafVersion": 250 (0xfa), but scriptPathControlBlocks[1] started with c1. The control byte carries the leaf version in its upper 7 bits with the low bit set, so it should be fb. The vector's leafHashes[1] and merkle root already correspond to the leaf hashed under 0xfa and the path bytes are correct, so only the first byte was wrong. As published, a spender using that control block would recompute the leaf hash under 0xc0 and fail the merkle check.

After both fixes every success vector in the file passes a full recomputation from the spec formulas: leaf hashes, merkle root, scriptPubKey, bech32m address, control blocks, and walking each control block path back to the root.

In p2mr_pqc_construction.json, the three-leaf vectors
(p2mr_three_leaf_complex, p2mr_three_leaf_alternative) listed
intermediary.leafHashes with entries 0 and 2 swapped relative to the
depth-first order of the script tree, while the merkle root,
scriptPubKey, address and control blocks all follow depth-first
order (as do the same trees in p2mr_construction.json).

In p2mr_different_version_leaves, scriptPathControlBlocks[1] started
with byte 0xc1 although the leaf it commits to has leafVersion 0xfa;
the control byte carries the leaf version in its upper 7 bits with
the low bit set, so it must be 0xfb. The vector's leafHashes[1] and
merkle root already correspond to the 0xfa leaf.
jeanpablojp added a commit to jeanpablojp/bitcoin that referenced this pull request Jul 27, 2026
Check the official BIP 360 construction vectors, vendored from
bitcoin/bips at commit 0fdf6ffdbb394a73c80978ae647322ceda8b9337, in
the style of the existing BIP 341 vector tests.

For each vector the harness rebuilds the script tree with the
consensus hashing functions and compares leaf hashes, Merkle root,
scriptPubKey, bech32m address and control blocks against the
published values, then walks each control block back through
ComputeP2MRMerkleRoot() to tie the vectors to the code that validates
blocks.

All nine vectors in p2mr_construction.json pass. Two vectors in
p2mr_pqc_construction.json carry bugs (swapped leaf hashes in the
three-leaf trees, and a control byte that does not match its leaf
version); the fix is bitcoin/bips#2220. Until it lands, the harness
asserts those divergences still reproduce, so the exceptions cannot
outlive the pinned vectors.
jeanpablojp added a commit to jeanpablojp/bitcoin-p2mr that referenced this pull request Jul 27, 2026
All nine vectors in p2mr_construction.json pass; the two bugs found
in the pqc file went upstream as bitcoin/bips#2220, and the doc-side
problems as #2221. Keep the issue drafts out of the repo now that
they are PRs.
@jonatack jonatack added Pending acceptance This BIP modification requires sign-off by the champion of the BIP being modified Bug fix labels Jul 28, 2026

@jonatack jonatack left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixes LGTM (thanks!) pinging @cryptoquick @EthanHeilman for feedback or sign-off.

@cryptoquick cryptoquick left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good changes. I'll make sure they're also reflected in our bindings vectors with this PR:

jbride/libbitcoinpqc-bindings#2

@jonatack jonatack removed the Pending acceptance This BIP modification requires sign-off by the champion of the BIP being modified label Aug 1, 2026
@jonatack
jonatack merged commit b31410c into bitcoin:master Aug 1, 2026
4 checks passed
jeanpablojp added a commit to jeanpablojp/bitcoin that referenced this pull request Aug 1, 2026
The pqc vector fix (bitcoin/bips#2220) was merged upstream as commit
b31410ca587c. Replace the vendored p2mr_pqc_construction.json with the
fixed file, remove the KnownVectorBug exceptions that asserted the
divergence at the old pin, and update the pin references. All 16
vectors now pass as ordinary cases.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants