#36500 AssumeUTXO: reject duplicate coins and empty snapshot groups

ReviewabilityReviewsAgreementSize
#36500 l0rinc AssumeUTXO: reject duplicate coins and empty snapshot groupsWaiting on author1Strong11 + 25 tests
  • Reject malformed AssumeUTXO snapshots with duplicate coin records or empty groups
  • Prevent inflated coin counts in loadtxoutset and delays during node shutdown
  • Ready to review
  • Substantive support with no open objections
  • Verified rejection of duplicate records and empty groups without fuzz crashes (cyb3ralbert)
  • Verified duplicate records are rejected by coin count on head versus base (fametrano)
  • Nonblocking suggestion to reuse an existing test node remains open (cyb3ralbert)
  • 11 lines added or modified outside tests
  • 25 lines added or modified in tests
  • 10 lines removed in total
  • 3 files, 4 commits
Validation#30 of 53
P3 · bug fix
  • P3 because it adds sanity checks to AssumeUTXO snapshot loading rather than fixing consensus flaws
  • Snapshot content hashes are already verified before this change
  • Prevents misleading coin counts and node shutdown delays caused by malformed snapshots
  • Ranking pass: Moved up: grouped with the other assumeutxo input-validation fixes it overlaps with
Reviewability: ReadyStalePaused
Agreement: StrongPositivePositive w/ caveatsNeutralMildDisputedBlockedCrickets
Reviews: current code-review ACKs, then (+stale ACKs) and -NACKs; greener = more ACKs.
Size: lines added or modified outside tests, then in tests; greener = smaller.

Goal

Validates the announced coin count in AssumeUTXO snapshot metadata against the unique UTXO count produced during import, and rejects transaction groups specifying zero coins during deserialization. Content hash checks take precedence over coin count errors.

Problem: AssumeUTXO snapshot loading verifies content hash but does not check the metadata's announced coin count against the unique UTXOs imported into the chainstate. A malformed snapshot with duplicate records collapses outpoints into single entries, matching the serialized UTXO hash while reporting an inflated count in `loadtxoutset`. Furthermore, empty transaction groups (zero coins) pass deserialization without advancing the coin counter, which bypasses the loop interrupt check and can delay node shutdown.

Evidence of importance:

  • PR description: 'Repeated outpoints collapse into one coin, so a snapshot can repeat every record, announce the doubled count, and still match the committed UTXO hash. loadtxoutset then reports the inflated count as coins_loaded.'
  • Commit e8e8cbcaa2: 'Snapshots generated by dumptxoutset already contain only nonempty groups.'

Risk of a mistake: medium (validation). A flaw in snapshot validation could falsely reject valid AssumeUTXO snapshots or prevent users from loading them via RPC.

Changed since the previous assessment: The implementation and tests are now separated into characterization test commits and implementation commits, with no substantive changes to the validation logic.

Reviewability

Paused · Waiting on author

Open objection from cyb3ralbert with no author reply for 7 days; review would repeat what has been asked.

Waiting on (from the thread read):

  • author: response to cyb3ralbert's suggestion to reuse nodes[1] 2026-10-15

Agreement

Strong

substantive support, no open objection (cyb3ralbert, fametrano)

Thread notes: Added fametrano's tested ACK; cyb3ralbert's suggestion on test node reuse remains open.

Objections, suggestions, and questions:

ReviewerTypeHarm, request, or questionStatusMust beAuthor repliedQuote
cyb3ralbert (contributor)suggestion, maintenanceUse the existing node (nodes[1]) instead of adding a fifth node to avoid test conflicts with #35307openansweredno2026-10-15: 'Could the new cases use the existing `node` (`nodes[1]`) instead of adding a fifth node?'

Support:

  • cyb3ralbert (contributor) (2026-10-15) [tested]: verified on regtest that duplicate records and empty groups are properly rejected and fuzz targets ran with no crashes
  • fametrano (contributor) (Tested ACK, 2026-10-25) [both: feature_assumeutxo.py]: verified on regtest and functional tests that duplicate records are rejected by coin count on head while passing on base

Participants:

  • cyb3ralbert (contributor, support, 1 statements 2026-10-15 to 2026-10-15): tested on regtest and fuzz targets; suggested reusing nodes[1] in the functional test
  • fametrano (contributor, support, 1 statements 2026-10-25 to 2026-10-25): tested ACK; verified rejection of duplicate coin records on head vs base

Files

34 lines under test/bench/ci.

  • test/functional/feature_assumeutxo.py +25/-9
  • src/validation.cpp +10/-0
  • src/node/utxo_snapshot.h +1/-1

Card

The short text the ranking pass compares across PRs.

Validates AssumeUTXO snapshot metadata coin counts against unique coins imported and disallows empty transaction groups during snapshot deserialization. Previously, snapshots with duplicated records could match the committed UTXO hash while reporting an inflated coin count, and zero-coin transaction groups could bypass loop interrupt checks to delay node shutdown. Legitimate snapshots generated by dumptxoutset already only produce non-empty groups, preserving compatibility.

Data

dossier JSON · extract JSON · model ledger, generated 2026-10-26T01:22, confidence high, input hash b8c5f6b2f1a9e625
thread read through 2026-10-25 (2 statements), head e8e8cbcaa2
labels: Validation

Categories

Validation

P3 · bug fix (ranking pass: Moved up: grouped with the other assumeutxo input-validation fixes it overlaps with)

Tightens AssumeUTXO snapshot validation by ensuring imported unique UTXOs match announced counts and preventing empty groups from delaying node shutdown. Because snapshot content hashes are already verified prior to this change, this is a modest sanity-check fix rather than a critical consensus or chainstate corruption issue.

Membership: Has the Validation label, edits src/validation.cpp and src/node/utxo_snapshot.h, and modifies AssumeUTXO snapshot validation logic.

Factors: security/stability 1, bug 1, performance 0, user value 0, leverage 0