Skip to content

fix(dash)!: bound partial merkle trees by Dash's block size - #1307

Merged
xdustinface merged 3 commits into
devfrom
shumkov/pmt-dash-block-bound
Nov 4, 2026
Merged

xdustinface merged 3 commits into
devfrom
shumkov/pmt-dash-block-bound

Conversation

@shumkov

@shumkov shumkov commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

Follow-up to #1200. This gap was listed in the handoff comment on #1205 (#1205 (comment)): "extract_matches uses Bitcoin's 16,666-transaction bound where a Dash block holds about 33,333".

Problem

PartialMerkleTree::extract_matches refused a tree of more than MAX_BLOCK_WEIGHT / MIN_TRANSACTION_WEIGHT = 4,000,000 / 240 = 16,666 transactions. That is Bitcoin's BIP141 bound. A Dash block holds up to 2 MB over the 60 bytes of the smallest transaction. Dash Core's CPartialMerkleTree::ExtractMatches refuses only nTransactions > MaxBlockSize() / 60 = 33,333 (src/merkleblock.cpp, src/consensus/consensus.h).

Since #1200, every masternode list diff proves its coinbase with this tree. For a valid Dash block of 16,667–33,333 transactions, MnListDiff::coinbase_merkle_root fails with InvalidCoinbaseProof. That is one of the two refusals FeedError::blames_peer holds against the peer. So the honest peer is banned, then the next peer asked for that list, and masternode sync stops. Any miner can build such a block on purpose, with about 1 MB of 60-byte transactions.

#400 (SegWit/Taproot removal) left MAX_BLOCK_WEIGHT and MIN_TRANSACTION_WEIGHT in blockdata::constants. This was their only use.

Fix

extract_matches bounds the count by a private MAX_BLOCK_TRANSACTIONS = MAX_BLOCK_SIZE / 60, Dash Core's bound. blockdata::constants::MAX_BLOCK_SIZE is new: it is Dash Core's MAX_DIP0001_BLOCK_SIZE, 2,000,000 bytes. It replaces the BIP141 MAX_BLOCK_WEIGHT and MIN_TRANSACTION_WEIGHT, which nothing in the workspace used any more.

The other checks are unchanged:

  • no transactions;
  • more hashes than transactions;
  • fewer bits than hashes;
  • every bit and hash used up;
  • identical left/right refused.

The traversal stays bounded by the hashes and flag bits the proof carries. The depth is log2 of the count, so the higher bound adds no work. A security review found no new forgery, malleability, depth-confusion or DoS surface.

Rejected: a dependency on base-sdk's dash-primitives for MAX_DIP0001_BLOCK_SIZE, which adds a crate for one constant.

Red → green

a_partial_merkle_tree_spans_as_many_transactions_as_a_dash_block_holds (dash/src/merkle_tree/block.rs) was committed before the fix:

Before the fix (5b20827) After
Result assertion failed: tree(33_333).extract_matches(&mut vec![], &mut vec![]).is_ok() passes; 33,334 transactions are still refused with TooManyTransactions

Breaking

blockdata::constants::MAX_BLOCK_WEIGHT and MIN_TRANSACTION_WEIGHT are removed. Use MAX_BLOCK_SIZE, Dash's limit in bytes. dev already carries breaking dashcore changes since v0.45.0.

Checks (local)

At 1ca9924 (head), every check below passed in full: dashd_masternode 11/11 and dashd_sync 38/38. The 8 tests dev already ignores (3 #[ignore], 5 ignore doctests) were skipped.

At f5b94f7, before the constants change:

  • cargo fmt --all --check, and typos 1.40.0 on the changed files.
  • cargo +1.89 check --workspace --all-features.
  • cargo clippy --workspace --all-targets --all-features -- -D warnings.
  • RUSTDOCFLAGS="-D warnings" cargo doc --workspace --all-features --no-deps.
  • cargo test --workspace --all-features --no-fail-fast with the dashd regtest env: everything passed, including dashd_masternode 11/11, except one test in dashd_sync.
  • dashd_sync ran while another session's dashd suite and builds held the machine at a load average of about 25.
    • First full run: 37/38. test_coinbase_matures_when_a_later_block_touches_the_wallet failed ("the newest coinbase is immature").
    • Second full run: 35/38. That test, test_coinbase_matures_without_later_wallet_activity and test_runtime_add_with_tip_advance_during_rescan failed.
    • The two coinbase tests pass alone, 3 runs of 3.
    • The full suite passes 38/38 with --test-threads=4.
    • These regtest blocks hold a handful of transactions, far below the old 16,666 bound, so this change cannot reach them.
  • Skipped: the 8 tests dev already ignores (3 #[ignore], 5 ignore doctests).

Notes

  • Out of scope (pre-existing): PartialMerkleTree::from_parts expands the flag bytes into a Vec<bool> before any bound check.

🤖 Generated with Claude Code

PR Hygiene · 1ca9924

  • Bots — coderabbitai ✓
  • Self-review — posted; again after any push
  • Build green
  • Approvals
    • files with no dedicated owner (dash/src/blockdata/constants.rs, dash/src/merkle_tree/block.rs) — QuantumExplorer or ZocoLini or xdustinface
    • xdustinface requested changes — waiting for them to re-review or dismiss

When every merge requirement is met, the PR Hygiene check passes. Reviewer limits do not block merging; other required GitHub checks and protections still apply.

Summary by CodeRabbit

  • Behavior Changes
    • Once DIP-0001 is active, the maximum serialized block size is 2 MB.
    • Transaction inclusion proofs now use a transaction-count limit based on that block-size cap. Proofs for up to 33,333 transactions are accepted; proofs for 33,334 or more are rejected.

shumkov and others added 2 commits November 3, 2026 19:17
A Dash block holds up to 33,333 transactions, its 2 MB size limit over the
60 bytes of the smallest transaction. Dash Core's
`CPartialMerkleTree::ExtractMatches` allows that many
(`nTransactions > MaxBlockSize() / 60`). `extract_matches` still uses
Bitcoin's BIP141 bound, `MAX_BLOCK_WEIGHT / MIN_TRANSACTION_WEIGHT` =
16,666, so it refuses the proof of any larger Dash block. That includes
the coinbase proof of a masternode list diff at that block, which
`coinbase_merkle_root` reports as `InvalidCoinbaseProof`, and that
refusal bans the peer.

This test fails on dev: 33,333 transactions are refused with
`TooManyTransactions`. The next commit fixes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`PartialMerkleTree::extract_matches` refused a tree of more than
`MAX_BLOCK_WEIGHT / MIN_TRANSACTION_WEIGHT` = 16,666 transactions. That
is Bitcoin's BIP141 bound. A Dash block holds up to 2 MB over the 60
bytes of the smallest transaction, so up to 33,333 transactions, and
Dash Core's `CPartialMerkleTree::ExtractMatches` allows exactly that
(`MaxBlockSize() / 60`, src/merkleblock.cpp).

So a valid Dash block above 16,666 transactions could not be proven. A
masternode list diff at that block failed its coinbase proof with
`InvalidCoinbaseProof`. That refusal bans the peer, so every honest peer
asked for that list was banned in turn, and masternode sync stopped. Any
miner can build such a block.

Raising the bound adds no work. The traversal is bounded by the hashes
and flag bits the proof carries, and the depth of the tree is log2 of
the count.

The BIP141 constants stay public in `blockdata::constants`. Nothing in
the crate uses them any more.

Test would have caught this in CI:
`a_partial_merkle_tree_spans_as_many_transactions_as_a_dash_block_holds`.
✖ before the fix (33,333 transactions refused with
`TooManyTransactions`), ✔ after.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review.

Walkthrough

The change adds a 2,000,000-byte serialized block-size limit and uses it to set the maximum transaction count accepted by partial Merkle proof extraction. Tests check the boundary at 33,333 and 33,334 transactions.

Changes: Partial Merkle proof limit

Layer / File(s) Summary
Block-size-based proof validation
dash/src/blockdata/constants.rs, dash/src/merkle_tree/block.rs
Adds MAX_BLOCK_SIZE and derives a private transaction-count limit from it. extract_matches rejects counts above that limit. Tests check acceptance at 33,333 transactions and rejection at 33,334.

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix

Possibly related PRs

Suggested reviewers: zocolini, quantumexplorer

Merge Risk: Minimal · up to 1ca99

The change permits larger valid proofs; no actionable merge-blocking risk remains identified.

Pre-merge checks: Passed 5
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely describes the main change: limiting partial merkle trees using Dash's block-size bound.
Docstring Coverage Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentat…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 1 functions across 2 files.

Finishing Touches
Generate docstrings
  • Commit to this branch
  • Create a new PR
Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

CodeRabbit Verify

  • Run CodeRabbit Verify to test the changes in this pull request.
Recent review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between 2900e46 and 1ca9924.

Files selected for processing: 2
  • dash/src/blockdata/constants.rs
  • dash/src/merkle_tree/block.rs

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Waiting for bot review — coderabbitai not yet. Wait for the missing reviews, or a writer can post /skip-bots to proceed without them; blocking findings still need addressing.
Full checklist in the description.

@github-actions github-actions Bot added the waiting-bots Waiting for the review bots to report on this head label Nov 3, 2026
@codecov

codecov Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 82.16%. Comparing base (2900e46) to head (1ca9924).
⚠️ Report is 11 commits behind head on dev.

Additional details and impacted files
@@            Coverage Diff             @@
##              dev    #1307      +/-   ##
==========================================
+ Coverage   82.13%   82.16%   +0.03%     
==========================================
  Files         290      290              
  Lines       76201    76323     +122     
==========================================
+ Hits        62588    62713     +125     
+ Misses      13613    13610       -3     
Flag Coverage Δ
core 82.03% <100.00%> (+0.11%) ⬆️
ffi 60.37% <ø> (ø)
rpc 52.10% <ø> (ø)
spv 93.91% <ø> (+0.01%) ⬆️
wallet 83.07% <ø> (ø)
Files with missing lines Coverage Δ
dash/src/blockdata/constants.rs 100.00% <ø> (ø)
dash/src/merkle_tree/block.rs 88.59% <100.00%> (+0.81%) ⬆️

... and 4 files with indirect coverage changes

@xdustinface xdustinface left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Code

  1. Put Dash's block size in blockdata::constants and drop MAX_BLOCK_WEIGHT and MIN_TRANSACTION_WEIGHT.
    After this PR nothing in the workspace uses those two constants. They still describe Bitcoin's BIP141 limit, the one that caused this bug, while Dash's real limit sits as a bare 2_000_000 in merkle_tree/block.rs. Removing them costs nothing extra, because dev already carries unreleased breaking dashcore changes since v0.45.0 (#400, #1288). Replace both with pub const MAX_BLOCK_SIZE: u32 = 2_000_000; (Core's MAX_DIP0001_BLOCK_SIZE) in constants.rs and derive MAX_BLOCK_TRANSACTIONS from it. The whole workspace still compiles with that change.

@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

Bots are done — your move: address xdustinface requested changes, then post /self-reviewed.
Full checklist in the description.

@github-actions github-actions Bot added waiting-self-review Waiting for the author to post /self-reviewed bot-review-skipped A required review bot did not report; it was skipped by the window or by a person. and removed waiting-bots Waiting for the review bots to report on this head labels Nov 3, 2026
…s block size

`MAX_BLOCK_WEIGHT` and `MIN_TRANSACTION_WEIGHT` described Bitcoin's
BIP141 limit, the bound that refused large Dash blocks' partial merkle
trees. Nothing in the workspace uses them any more, while Dash's block
size sat as a bare `2_000_000` in `merkle_tree/block.rs`.

`blockdata::constants::MAX_BLOCK_SIZE` is now Dash Core's
`MAX_DIP0001_BLOCK_SIZE`, 2,000,000 bytes, and the partial merkle tree
bound is derived from it. The bound stays at 33,333 transactions.

BREAKING CHANGE: `blockdata::constants::MAX_BLOCK_WEIGHT` and
`MIN_TRANSACTION_WEIGHT` are removed. Use `MAX_BLOCK_SIZE`, Dash's limit
in bytes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@shumkov shumkov changed the title fix(dash): bound partial merkle trees by Dash's block size fix(dash)!: bound partial merkle trees by Dash's block size Nov 3, 2026
@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Waiting for bot review — coderabbitai not yet. Wait for the missing reviews, or a writer can post /skip-bots to proceed without them; blocking findings still need addressing.
Full checklist in the description.

@github-actions github-actions Bot added waiting-bots Waiting for the review bots to report on this head and removed waiting-self-review Waiting for the author to post /self-reviewed bot-review-skipped A required review bot did not report; it was skipped by the window or by a person. labels Nov 3, 2026
@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

Bots are done — your move: address xdustinface requested changes, then post /self-reviewed.
Full checklist in the description.

@github-actions github-actions Bot added waiting-self-review Waiting for the author to post /self-reviewed and removed waiting-bots Waiting for the review bots to report on this head labels Nov 3, 2026
@shumkov

shumkov commented Nov 4, 2026

Copy link
Copy Markdown
Collaborator Author

/self-reviewed

@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

Ready for review — files with no dedicated owner: QuantumExplorer or ZocoLini or xdustinface · re-review or resolve: xdustinface.
Full checklist in the description.

@github-actions github-actions Bot added ready-for-human Bots have reported, the author has self-reviewed, and the build is green: this needs a human. and removed waiting-self-review Waiting for the author to post /self-reviewed labels Nov 4, 2026
@xdustinface
xdustinface merged commit 04f8afd into dev Nov 4, 2026
44 of 46 checks passed
@xdustinface
xdustinface deleted the shumkov/pmt-dash-block-bound branch November 4, 2026 14:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-human Bots have reported, the author has self-reviewed, and the build is green: this needs a human.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants