Skip to content

fix(btc-verification): bind inclusion proof to the block's Merkle tree - #175

Merged
MdTeach merged 2 commits into
mainfrom
fix/inclusion-proof-depth-binding
Jul 5, 2026
Merged

fix(btc-verification): bind inclusion proof to the block's Merkle tree#175
MdTeach merged 2 commits into
mainfrom
fix/inclusion-proof-depth-binding

Conversation

@prajwolrg

Copy link
Copy Markdown
Collaborator

Description

TxidInclusionProof::verify reconstructed the Merkle root by folding the supplied siblings into the leaf txid, but never checked those siblings against the tree the root actually commits to. It therefore proved a weaker statement than intended — "some fold reaches the root" rather than "the leaf sits at index i of an N-leaf tree". Two consequences, both covered by the added test:

  • a zero-length proof passes off any single hash (e.g. an internal Merkle node) as a whole-block root; and
  • an out-of-range position verifies against the real root, since only the low siblings.len() bits of the position feed left/right ordering.

The fix requires the caller to pass the block's transaction count and rejects any proof whose position is not a valid leaf index (< tx_count) or whose sibling count differs from the tree depth ceil(log2(tx_count)). The depth check is Bitcoin Core's standard mitigation against the 64-byte node/transaction ambiguity, where an internal node presented as a leaf yields a proof shorter than the true tree depth.

Notes to Reviewers

This is soundness hardening of the proof verifier, not a patch for an actively exploitable bug. The sole production caller (check_block_integrity) verifies the coinbase against a PoW-committed header.merkle_root and gates on is_coinbase(), and the guest always has the full block. A working forgery would therefore need either a full 256-bit preimage on the committed root, or a 64-byte string that is simultaneously a real block's internal Merkle node and a structurally valid coinbase — neither is feasible. The change makes verify sound on its own rather than relying on those external invariants, which matters because verify/compute_root are pub and reusable elsewhere.

Separately: the coinbase inclusion proof barely optimizes anything, since the segwit path already walks all of txdata to compute the witness root. Recomputing the txid Merkle root directly (as the non-segwit path does) would bind the coinbase fully and make the proof — and this whole bug class — unnecessary. Worth a follow-up ticket; out of scope here.

Type of Change

  • Security fix

Checklist

  • I have performed a self-review of my code.
  • I have commented my code where necessary.
  • My changes do not introduce new warnings.
  • I have added tests that prove my changes are effective or that my feature works.
  • New and existing tests pass with my changes.

prajwolrg added 2 commits July 2, 2026 12:22
TxidInclusionProof::verify reconstructs the Merkle root from the supplied
siblings without binding the proof to the tree structure. Nothing checks the
number of siblings against the expected tree depth, nor that the position is a
valid leaf index. This lets an attacker forge inclusion:

  - a zero-length proof passes off any single hash (e.g. an internal Merkle
    node, which under Bitcoin's 64-byte tx ambiguity can also be a valid txid)
    as a whole-block root; and
  - an out-of-range position verifies against the real Merkle root, since only
    the low siblings.len() bits of the position feed left/right ordering.

Add a test that exercises both to lock in the current (vulnerable) behavior
before the fix.
TxidInclusionProof::verify folded the supplied siblings into a root without
checking them against the tree the root commits to, so it proved a weaker
statement than intended ("some fold reaches the root") rather than "the leaf
sits at index i of an N-leaf tree". A zero-length proof passed off any single
hash as a whole-block root, and out-of-range positions verified because only
the low siblings.len() bits feed left/right ordering.

Require the caller to supply the block's transaction count and reject any proof
whose position is not a valid leaf index or whose sibling count differs from the
tree depth ceil(log2(tx_count)). The depth check is Bitcoin Core's standard
mitigation against the 64-byte node/transaction ambiguity, where an internal
node presented as a leaf yields a proof shorter than the true depth.

Not known to be exploitable in the current wiring: the sole caller checks the
coinbase against a PoW-committed root and gates on is_coinbase(), so a forgery
would need a full preimage on the root or a 64-byte internal node that is also a
structurally valid coinbase. This closes the gap so the verifier is sound on its
own rather than relying on those external invariants.
@prajwolrg
prajwolrg force-pushed the fix/inclusion-proof-depth-binding branch from a3d42dc to 6571f31 Compare July 2, 2026 06:46
@codecov

codecov Bot commented Jul 2, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

Files with missing lines Coverage Δ
crates/btc-verification/src/body_verification.rs 95.79% <100.00%> (+0.14%) ⬆️
crates/btc-verification/src/inclusion_proof.rs 97.32% <100.00%> (+1.48%) ⬆️

... and 3 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown

Commit: 0ad621f
SP1 Execution Results

program cycles gas
asm-stf 136,419,395 134,133,504
moho 5,223,535 5,525,318

@prajwolrg
prajwolrg requested a review from MdTeach July 2, 2026 09:14
@prajwolrg
prajwolrg marked this pull request as ready for review July 2, 2026 09:52
@prajwolrg

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Nice work!

Reviewed commit: 6571f3129f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@MdTeach
MdTeach added this pull request to the merge queue Jul 5, 2026
Merged via the queue into main with commit 1737c95 Jul 5, 2026
23 checks passed
@MdTeach
MdTeach deleted the fix/inclusion-proof-depth-binding branch July 5, 2026 22:32
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.

2 participants