validation: check the most recent AssumeUTXO commitment during IBD #36543

pull l0rinc wants to merge 7 commits into bitcoin:master from l0rinc:l0rinc/assumeutxo-latest-snapshot changing 21 files +224 −221
  1. l0rinc commented at 1:40 PM on October 15, 2026: contributor

    Problem: AssumeUTXO verifies imported UTXOs against a hash we bake into the binary. Every node reconstructs the same UTXO set during ordinary IBD, but does not check its hash against the existing AssumeUTXO commitment.

    Fix: During ordinary block validation, verify the AssumeUTXO hash when block connection reaches the corresponding block. Log mismatches and raise a GUI/RPC warning while allowing IBD to continue. Flush pending changes to disk before hashing the UTXO set and comparing the result with the commitment.

    Keep only the latest AssumeUTXO commitment per network to limit the extra hashing work during IBD, and update the release process to replace it. Users who need older snapshots for research or other purposes can use a compatible older release or restore the corresponding commitment as m_assumeutxo_data and recompile.

  2. DrahtBot added the label Validation on Oct 15, 2026
  3. DrahtBot commented at 1:40 PM on October 15, 2026: contributor

    <!--e57a25ab6845829454e8d69fc972939a-->

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    <!--006a51241073e994b41acfe9ec718e94-->

    External sites

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline and AI policy for information on the review process.

    Type Reviewers
    Concept ACK Sjors, optout21, fametrano

    If your review is incorrectly listed, please copy-paste <code>&lt;!--meta-tag:bot-skip--&gt;</code> into the comment that the bot should ignore.

    <!--174a7506f384e20aa4161008e828411d-->

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #36778 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/36778.svg"></sub> (assumeutxo: replace loadtxoutset RPC with a startup arg by l0rinc)
    • #36696 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/36696.svg"></sub> (CTransaction: Add constructor taking inputs and outputs by purpleKarrot)
    • #36326 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/36326.svg"></sub> (kernel: Use typed errors for fatal and flush error notifications by arejula27)
    • #36114 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/36114.svg"></sub> (wallet: harden external signer psbt processing, revamp mock by Sjors)
    • #35642 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/35642.svg"></sub> (headersync: do parameter search at runtime by sipa)
    • #35557 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/35557.svg"></sub> (kernel, validation: Add btck_chainstate_manager_set_clock_time by ryanofsky)

    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-->

  4. l0rinc force-pushed on Oct 15, 2026
  5. DrahtBot added the label CI failed on Oct 15, 2026
  6. DrahtBot commented at 2:01 PM on October 15, 2026: contributor

    <!--85328a0da195eb286784d51f73fa0af9-->

    🚧 At least one of the CI tasks failed. <sub>Task iwyu: https://github.com/bitcoin/bitcoin/actions/runs/38744710504/job/116455202409</sub> <sub>LLM reason (✨ experimental): CI failed because the IWYU check modified src/kernel/chainparams.cpp by removing an unused cassert include, leaving an uncommitted diff.</sub>

    <details><summary>Hints</summary>

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

    </details>

  7. DrahtBot removed the label CI failed on Oct 15, 2026
  8. Sjors commented at 3:24 AM on October 17, 2026: member

    Concept ACK, see #32377, with two caveats:

    stopping the node on a mismatch

    I think this risks introducing a new effective checkpoint. If the snapshot were malicious, normal users couldn't sync and a subset may decide to restart in AssumeUTXO mode.

    It seems better to emit a warning, severe enough that the GUI shows it, but not aborting. It's a clear sign there's something wrong with the software. Even though it doesn't affect them, they should seek advice, since it's a giant red flag.

    (in such a malicious scenario, developers would presumably change the warning to an error, exactly to achieve this wrong behavior)

    This was already discussed in #32377 (comment), but the implementation doesn't match the conclusion there.

    Keep only the latest AssumeUTXO commitment per network to limit the extra hashing work during IBD

    Maybe limit the scope of this PR by only checking the latest hash, but not (yet) deleting the other hashes.


    You mentioned offline that the performance impact of this extra check is minimal. It would be good to provide some numbers in the PR description.

    I'm not up to speed on how often and when we flush the chainstate to disk during IBD nowadays. How does this interact with that?

  9. optout21 commented at 5:16 AM on October 17, 2026: contributor

    Concept ACK

    As there is a hash provided that requires trust, the more checks the better. It makes sense to use all nodes performing IBD to also perform this check, as it's relatively cheap to do.

    It seems better to emit a warning, severe enough that the GUI shows it, but not aborting.

    Two suggestions:

    • Introduce an option to skip the new AssumeUTXO hash check (may be a hidden option) -- useful in itself.
    • In the unlikely case a hash mismatch is found, abort, and advice the user to shout out and restart with the ignore AssumeUTXO hash check option. That way the effect is hard to ignore by the user, but easy to bypass.

    Maybe limit the scope of this PR by only checking the latest hash, but not (yet) deleting the other hashes.

    For clarity it's worth mentioning that the latest hash a user is checking may not be the same as the latest hash of the latest release, nor the one used by most nodes in the network. (For the sake of example, assuming v35 has been released, and a majority of the network is on v34, when a user is running v33, the checked hash will not match the hash used by many other nodes.) Note that it would be desirable to check the hash of newer versions, but of course it's not possible to hardcode hashes of future releases in the source code. So not checking the non-latest hashes is fine, but it's useful to keep them for visibility.

  10. ajtowns commented at 4:21 AM on October 24, 2026: contributor

    What's the performance impact when hitting such a block?

    Doesn't this make the result of a release including bad snaphot data worse for everyone -- Alice who does IBD has her node crash despite having good data that she fully verified, and Bob who uses the bad snapshot still potentially fails to an incorrect chain unless Alice has already done her IBD run and Bob happens to hear Alice complain about the failure?

    Having predictable snapshot points (every 14112 blocks per bip452) would allow every node to calculate the appropriate hash (in the background on a leveldb snapshot without cs_main held, hopefully) and remember it, prior to a new release being considered, making verification much easier, and consistent across implementations. Including the block's muhash in the assumeutxo set would also probably work here, since muhash is already calculated and stored in coinstatsindex.

  11. fametrano commented at 11:38 AM on October 25, 2026: contributor

    Concept ACK

    One issue should be fixed before merge: a node that fails the new check aborts on an assertion, and a plain restart then syncs past the commitment without checking it again. I built 97b2138e39 with one digit of the regtest commitment's hash at height 299 changed, and let it sync the feature_assumeutxo.py chain to 310 from an unmodified node. In nine runs out of nine it logged the mismatch and the fatal error, then died with SIGABRT on Assertion failed: (hashPrevBlock == view.GetBestBlock()) in ConnectBlock. Restarted with no new option, it starts at 299, syncs to 310 and never hashes the UTXO set again. -reindex-chainstate stops it once more, and a plain restart then syncs past 299 again. The same change on cce32b1687 syncs to 310. I can share the harness.

    In ConnectTip, block 299 reaches the coins cache and the disk before the check, and the failure returns before m_chain.SetTip. The coins are at 299 while the chain stays at 298. Connecting 299 again, when the next block arrives during shutdown, trips the assert. The next start takes its tip from the coins. Moving the check after UpdateTip is not enough: on regtest the node then aborts in CheckBlockIndex instead. The unit test processes no block after the mismatch, so it reaches neither assert.

    On warning versus stopping (Sjors, ajtowns): as it stands, the stop lasts only until the next restart.

  12. test: move AssumeUTXO commitments into test fixtures
    Move the height-110 and height-200 regtest commitments into their unit and fuzz fixtures.
    Keep the immutable fixture parameters alive across chainstate-manager restarts.
    Leave only the height-299 functional-test commitment in the default regtest parameters.
    Persist block-index test edits before reloading to prepare for the IBD check's forced flush.
    Check that an unrelated chain passes the fixture's commitment height without starting a UTXO scan.
    Check that interrupted snapshot validation leaves the snapshot unvalidated and can be retried.
    1378f4e654
  13. chainparams: use the latest AssumeUTXO commitment
    Match AssumeUTXO imports, background validation, and chainstate reloads against the latest commitment.
    Keep the height and block hash mismatch checks, and report the supported height for an unknown snapshot block hash.
    Older AssumeUTXO commitments are no longer recognized.
    All configured commitment lists are nonempty and ordered by increasing height, so the last entry supplies the same height as the previous max_element lookup for rollback exports.
    Use the same accessor for the fixture checks and remove the optional lookup APIs and height list.
    a8e4000c19
  14. refactor: extract AssumeUTXO hash verification
    Extract UTXO hashing, interruption handling, commitment comparison, and result logging from AssumeUTXO background validation.
    Keep snapshot invalidation at the caller so ordinary block validation can reuse the check.
    Use log wording that also fits ordinary block validation.
    7566d689ce
  15. refactor: store one AssumeUTXO commitment
    All consumers now use only the latest commitment.
    Retain only that commitment to limit the extra hashing work during IBD.
    Remove the unused older production commitments and replace the vector with one optional `AssumeutxoData`.
    Keep it disengaged until the network constructor assigns the commitment.
    3221dd7fa9
  16. validation: verify the provided AssumeUTXO hash
    Verify the provided AssumeUTXO hash when block connection reaches the corresponding block.
    Skip other chains at the same height.
    This checks the developer-provided commitment against the UTXO set that every node reconstructs during IBD (see #32377).
    Reuse the AssumeUTXO background verifier and its reporting.
    Flush pending UTXO changes to disk before hashing the set.
    Stop on a flush failure before updating the tip or mempool.
    Report failures through the GUI and RPC warning fields so a bad compiled commitment does not stop ordinary IBD.
    Preserve normal shutdown interruption handling.
    AssumeUTXO import verification and background validation remain mandatory.
    f98aca5b8f
  17. validation: propagate snapshot verification errors
    `MaybeValidateSnapshot()` already invalidates a failed snapshot and requests shutdown, but `ConnectTip()` reports success to its caller.
    Return a runtime error for snapshot-validation failures without marking the connected block invalid.
    Keep interrupted hashing distinct so normal shutdown does not become a validation failure.
    cc29a67338
  18. doc: describe latest AssumeUTXO commitment checks
    Document the IBD commitment check and its GUI/RPC warning on a mismatch.
    Explain the compatibility change and how to use older snapshots for research.
    Update the release instructions to replace the commitment instead of appending it.
    396f6ddc2a
  19. l0rinc force-pushed on Nov 2, 2026
  20. l0rinc commented at 11:46 AM on November 2, 2026: contributor

    Thanks for the reviews!

    Maybe limit the scope of this PR by only checking the latest hash, but not (yet) deleting the other hashes.

    If we keep multiple hashes, we should test all of them, which would introduce a measurable delay during IBD after a while. If we only check the latest, that would give false confidence for previous ones. I'd rather keep just the latest commitment and remove the vector and lookup APIs. I don't see a clear use case for keeping that complexity, and if one comes up later, it's straightforward to add support back cleanly.

    It seems better to emit a warning, severe enough that the GUI shows it, but not aborting

    Agreed, adjusted.

    You mentioned offline that the performance impact of this extra check is minimal. It would be good to provide some numbers in the PR description.

    My measurements indicate no speed difference whatsoever in total -reindex-chainstate time before and after this change. Let me know if your measurements differ. For historical context, my earlier gettxoutsetinfo measurements took 137-144 seconds, and fjahr measured 56.5 seconds on an M4 at height 887,381.

    Introduce an option to skip the new AssumeUTXO hash check (may be a hidden option) -- useful in itself.

    Since mismatches no longer stop IBD, I'd leave out the extra option to keep this change simple.

    I'm not up to speed on how often and when we flush the chainstate to disk during IBD nowadays. How does this interact with that?

    I plotted the flushes and their causes here. This PR adds a forced sync at the AssumeUTXO height, writing pending changes while retaining the cache before hashing the UTXO set. The next periodic write is then scheduled about an hour later.

    Doesn't this make the result of a release including bad snaphot data worse for everyone

    I have changed it to raise a GUI/RPC warning so ordinary IBD continues.

    Having predictable snapshot points [...] prior to a new release being considered

    Including the block's muhash in the assumeutxo set would also probably work here

    I'm not sure I have all the context that you're referring to. Using MuHash could avoid the scan for nodes already running coinstatsindex, but would require adding another hash to the chain parameters. Given how fast this is currently, I'd keep this PR focused on checking the existing commitment during ordinary IBD.

    a node that fails the new check aborts on an assertion, and a plain restart then syncs past the commitment without checking it again.

    Thanks for the detailed reproducer. Switching the IBD mismatch to a warning also removes the early return that left the coins and tip inconsistent. The updated test connects the following block and checks that the node stays running with the warning set.

    The check still isn't rerun on startup beyond that height, and the GUI/RPC warning isn't persisted across restarts. If reviewers find this surprising, I can adjust.

  21. fametrano commented at 4:53 PM on November 2, 2026: contributor

    Yes, the lost warning surprises me and I would keep it across restarts. The abort I reported is gone at 396f6ddc.

    I repeated my setup with one digit of the regtest commitment's hash changed, syncing feature_assumeutxo.py's chain from an unmodified node (harness and output). In nine runs out of nine the node hashes when it connects block 299, sets the warning in getblockchaininfo and getnetworkinfo, runs -alertnotify once and syncs on to 310; an unmodified build raises nothing. After a plain restart the warning is gone from both RPCs and another block does not bring it back, so only the debug.log line remains. -reindex-chainstate raises it again until the next restart, and invalidateblock and reconsiderblock on 299 raise it again too. I did not build the GUI or measure mainnet.

    The other two kernel::Warnings are recomputed from the tip, so a restart does not lose them for good. This one cannot be, because the height has passed. In #32377 the check's value is that one honest participant exposes a mismatch, which needs the operator to see it, and whoever restarts after IBD before looking, without -alertnotify, sees it only in the log. Ralph has no report on this head; its last one, on the fatal path, asks to persist the pending check, while I only ask to keep a warning already raised, not to hash again at startup. A flag in the block index database, read at startup like prunedblockfiles, looks enough, though I have not built it: named after the commitment, so a new release does not inherit it, and cleared by a later matching check. Not blocking, your call.


github-metadata-mirror

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-11-12 22:51 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me