l0rinc
commented at 11:10 AM on October 11, 2026:
contributor
Problem: Snapshot loading doesn't validate the announced coin count against the unique UTXOs loaded.
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.
Snapshot loading also accepts transaction groups with no coins.
They leave the UTXO hash unchanged and don't advance the coin count, so parsing them bypasses the interrupt check and can delay shutdown.
Fix: Compare the announced count with the unique coin count already produced by the existing UTXO statistics scan, and reject zero-count transaction groups while parsing the snapshot.
When both the content hash and coin count are wrong, loading reports the hash error first.
DrahtBot added the label Validation on Oct 11, 2026
DrahtBot
commented at 11:11 AM on October 11, 2026:
contributor
<!--e57a25ab6845829454e8d69fc972939a-->
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.
If your review is incorrectly listed, please copy-paste <code><!--meta-tag:bot-skip--></code> into the comment that the bot should ignore.
<!--174a7506f384e20aa4161008e828411d-->
Conflicts
Reviewers, this pull request conflicts with the following ones:
#36633 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/36633.svg"></sub> (validation: bound snapshot cache growth by bytes, not coin count by fametrano)
#36603 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/36603.svg"></sub> (Improve loadtxoutset performance by ajtowns)
#36598 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/36598.svg"></sub> ([Demo] validation: remove AssumeUTXO by l0rinc)
#35307 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/35307.svg"></sub> (blockstorage: keep snapshot base in normal blockfile range by shuv-amp)
If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.
<!--5faf32d7da4f0f540f40219e4f7537a3-->
l0rinc force-pushed on Oct 11, 2026
DrahtBot added the label CI failed on Oct 11, 2026
l0rinc renamed this: validation: reject duplicate UTXO snapshot records validation: reject duplicate coins and empty snapshot groups on Oct 11, 2026
l0rinc force-pushed on Oct 11, 2026
DrahtBot removed the label CI failed on Oct 11, 2026
l0rinc renamed this: validation: reject duplicate coins and empty snapshot groups assumeUTXO: reject duplicate coins and empty snapshot groups on Oct 14, 2026
cyb3ralbert
commented at 7:04 AM on October 15, 2026:
contributor
Could the new cases use the existing node (nodes[1]) instead of adding a fifth node? At bf7ea08, feature_assumeutxo.py passes with snapshot_node = self.nodes[1], both with and without num_nodes = 4. The fifth node conflicts with #35307, which also sets num_nodes = 5 and uses nodes[4]. With both PRs merged (head d07900cf8), src/ merges cleanly, but feature_assumeutxo.py fails on the duplicate-records case with "Can't activate a snapshot-based chainstate more than once".
Tested bf7ea08 on regtest with a 299-coin snapshot. The new cases fail on the merge base (a8b69bd) and pass on the PR head. A file with duplicated records and a doubled count is accepted on the base (coins_loaded: 598) but rejected on the head with "Bad snapshot coins count: expected 598, got 299". A single empty group after the header is accepted on the base and rejected on the head.
With 30M empty groups (990 MB) before a valid body, the base accepts the file in 2.13 s and exits 1.00 s after stop is issued 1 s into the load. The head rejects it in 0.04 s. After either rejection, chainstate_snapshot is gone, getchainstates shows one chainstate, and a valid snapshot then loads and survives a restart.
The count check also catches a single repeated outpoint, both within a group and in a repeated group. In either case, the head rejects it with "Bad snapshot coins count: expected 300, got 299" and leaves one chainstate.
wallet_assumeutxo.py and validation_chainstatemanager_tests pass. Both utxo_snapshot and utxo_snapshot_invalid fuzz targets ran for 10 minutes each on the head using libFuzzer without sanitizers, starting from an empty corpus, with no crashes (127k and 1.45M runs, respectively).
Not tested: mainnet-size snapshots.
l0rinc renamed this: assumeUTXO: reject duplicate coins and empty snapshot groups AssumeUTXO: reject duplicate coins and empty snapshot groups on Oct 15, 2026
test: characterize duplicate snapshot records
A repeated snapshot payload preserves the unique UTXO set and content hash while doubling the announced record count.
Record that current loading accepts this mismatch before enforcing the metadata contract.
Also preserve the content-hash error when the announced count differs from the unique UTXO count.
8e2b70b77b
validation: check imported unique UTXO count
Repeated records can produce the committed set while overstating the announced coin count.
Check the unique count from the existing statistics scan after the content hash, preserving hash-error precedence.
f1a903e47b
test: characterize empty snapshot groups
An empty transaction group adds no coin and leaves the committed UTXO hash unchanged.
Record its current acceptance before requiring every group to contain a coin.
773024812c
l0rinc force-pushed on Oct 15, 2026
validation: reject empty snapshot groups
Reject transaction groups with no coins instead of accepting bytes that contribute nothing to the checked UTXO set.
Snapshots generated by dumptxoutset already contain only nonempty groups.
e8e8cbcaa2
l0rinc force-pushed on Oct 15, 2026
fametrano
commented at 11:37 AM on October 25, 2026:
none
tACKe8e8cbcaa25ba75ad04cbacea2ca3d4254bf23d0
A repeated outpoint need not repeat the same coin. I appended a second record for an existing outpoint to the regtest snapshot, with a 500-byte script, and raised the count by one. On the base (cce32b1687) the file loads with coins_loaded 300, and gettxout returns the original coin: the coins cache keeps the first record for an outpoint, so the content hash still matches. The head rejects the file with "Bad snapshot coins count: expected 300, got 299". With the extra record placed before the original, both reject it on the hash.
The loaded UTXO set is still the committed one. What the base leaves unchecked is the reported count and the extra bytes in the file. AssumeutxoData holds no coin count, so this holds for a file matching any hardcoded entry, mainnet included.
I built bitcoind at the head and at the base, and ran the head's feature_assumeutxo.py and a small loadtxoutset script on both. The test fails on the base at the duplicate case and passes on the head. I can share the script.
This is a metadata mirror of the GitHub repository
bitcoin/bitcoin.
This site is not affiliated with GitHub.
Content is generated from a GitHub metadata backup.
generated: 2026-10-27 17:51 UTC
This site is hosted by @0xB10C More mirrored repositories can be found on mirror.b10c.me