backport: assumeutxo M3 — background validation completion and snapshot promotion - #7553
Merged
PastaPastaPasta merged 14 commits intoAug 12, 2026
Conversation
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Issue being fixed or feature implemented
M1 (#7451) added AssumeUTXO snapshot persistence and M2 (#7456) gave the snapshot and background chainstates independent EvoDB identities, markers, and chain-aware Dash validation. What was still missing is the end of the lifecycle: nothing ever completed background validation, so a snapshot-backed node stayed in the dual-chainstate state (with DKG participation and quorum signing disabled) forever.
This is milestone 3 of the AssumeUTXO series: background validation completion. When the background chainstate reaches the snapshot base block, the node now verifies the background-derived state against the snapshot, disables the background chainstate, and on the next restart promotes the snapshot chainstate (coins directory and EvoDB markers) to the normal single-chainstate layout.
What was done?
Upstream backports (kept 1:1 where practical, Dash adaptations in separate commits):
ChainstateManager::MaybeCompleteSnapshotValidation()(UTXO-set hash comparison againstm_assumeutxo_datawhen the background tip reaches the base block) andValidatedSnapshotCleanup()(restart-time promotion ofchainstate_snapshotoverchainstate), withSnapshotCompletionResultreporting and thechainstate_snapshot_INVALIDquarantine path.Chainstate::m_disabledreplaces ad-hoc usability checks,CompleteChainstateInitialization()split out ofLoadChainstate()so chainstates can be reinitialized after cleanup,LoadExternalBlockFilemoved toChainstateManager, per-blockfile undo tracking without the active-chain reference, andBLOCK_ASSUMED_VALIDdocumentation/semantics updates.Dash-specific completion path:
CEvoDB::PromoteSnapshotMarkers()atomically (single synced batch) moves the SNAPSHOT best-block marker to the legacy NORMAL key and removes all dual-chainstate metadata;DiscardSnapshotMarkers()does the same for a rejected snapshot while preserving NORMAL state. Both reset the transaction-less default identity to NORMAL, closing theTODO(assumeutxo)markers left in M2.EVODB_SNAPSHOT_MNLIST_HASH); the background chainstate independently records the list hash it derives when it connects the base block (EVODB_BACKGROUND_MNLIST_HASH). Completion compares them (in addition to the upstream UTXO-set hash) and fails withSnapshotCompletionResult::EVO_STATE_MISMATCHon divergence. This is the first installment of the holistic base-state comparison M2 deferred; extending it to the CbTxmerkleRootMNList/merkleRootQuorumsand credit-pool commitments is called out as a TODO for theloadtxoutsetmilestone, where the snapshot payload gains Dash state. Until then this comparison is a corruption tripwire, not an independent check: in the only case both markers exist (activation with the background tip already at the base) they are written from a single derivation, and a cold-start activation captures neither and skips the comparison. Divergence therefore only signals on-disk damage to the marker pair; the independent comparison arrives when the snapshot payload carries the base MN list.ValidatedSnapshotCleanup()performs two directory renames plus a marker promotion, each individually durable.RecoverSnapshotCleanup()(run at startup before chainstate detection) classifies every interruption point — first rename done, both renames done with markers pending, promotion durable but deletion pending, invalid-snapshot rename done with marker discard pending — and either rolls back, finishes the promotion, or fails with a precise error instead of the generic reindex advice.EraseSnapshotMarkers()(the abandoned-activation rollback from M2) now also erases the new MN-list-hash markers; snapshot activation moves the mempool to the snapshot chainstate and restart activation clears it from the background chainstate (the assumeutxo (2) bitcoin/bitcoin#27596 shape), so background block connects can no longer callremoveForBlock/removeExpiredAssetUnlockagainst mempool state built on the snapshot tip; the invalid-snapshot revert hands the mempool back.Review follow-ups from the M2 merge applied here:
BLOCK_DATA_UNAVAILABLE_SUFFIX) shared by every producer and the matcher.CQuorumBlockProcessor::ProcessCommitment.Review follow-ups from the #7553 review round:
ChainstateManager::LoadBlockIndex()as a normal startup failure (recoverable via the standard reindex advice), instead of aborting in candidate admission;GetSnapshotBaseBlock()regains upstream's cachedSnapshotBase()delegation (bitcoin d4a11ab) that the initial adaptation had dropped. Note the related deliberate deviation:MaybeCompleteSnapshotValidation()converts one upstream hard assert into aSKIPPEDreturn for synthetic in-memory unit fixtures, discriminated byCoinsDB().StoragePath()being empty.RenameDurably()/RemoveAllDurably()helpers (fs::rename/fs::remove_all+DirectoryCommit), so the crash-recovery invariant is enforced by the helper rather than by remembering a follow-up call at six sites.RecoverSnapshotCleanup(must land in the same end state as a completed discard), and background-MN-hash coverage in the marker rollback test.With completion wired, the M2 duty gate resolves end-to-end:
IsSnapshotActiveAndUnvalidated()becomes false at completion, so DKG participation and quorum signing re-enable without a restart, and themasternode statusclause clears.How Has This Been Tested?
ab65592f85d); every conflict was resolved against M2's final review round (thread-scoped EvoDB transactions,EraseSnapshotMarkers, reindex-time snapshot discard, fallibleDetectSnapshotChainstate, BLS scheme establishment). The merged M2 testchainstate_connectblock_bls_schemeis adapted in the Rework validation logic for assumeutxo bitcoin/bitcoin#27746 commit forAcceptBlockmoving toChainstateManager.--enable-debug), then the completetest_dashsuite passes ("No errors detected"), including targeted reruns ofevo_db_tests,validation_chainstatemanager_tests,validation_chainstate_tests,evo_deterministicmns_tests,evo_mnhf_tests,evo_assetlocks_tests,evo_cbtx_tests,blockmanager_tests,coinstatsindex_tests, andvalidation_block_tests.snapshot_marker_promotion_and_discard(promotion/discard idempotency across restarts), the extended abandoned-activation marker rollback test,chainstatemanager_snapshot_completionand_hash_mismatch(upstream-shaped), anEVO_STATE_MISMATCHcompletion case, four crash-recovery tests that each reproduce a distinctValidatedSnapshotCleanupinterruption point on disk and drive it throughLoadVerifyActivateChainstate(), and mempool-ownership assertions at both activation paths.ConnectTip→MaybeCompleteSnapshotValidationEvoDB transaction lifecycle (the scoped committer closes before completion runs, so the single-open-transaction invariant holds), BLS-scheme guard nesting across connect/disconnect, all four mempool handoff transitions, and the recovery state machine. Its two "correct but implicit" findings are addressed in the final commit (at-rest raw reads for the lifecycle markers; a comment documenting the deliberate promote/discard overlap inRecoverSnapshotCleanup).lint-circular-dependencies,lint-python, andgit diff --checkare clean.Breaking Changes
None released. The dual-chainstate on-disk state introduced in M2 (unreleased) gains two lifecycle marker keys (
b_dcs_mn,b_dcs_bg_mn); nodes that never load a snapshot never write any of them.ChainstateLoadStatus::FAILURE_FATALis a new internal failure class treated likeFAILURE_INCOMPATIBLE_DBat init.Checklist:
doc/design/assumeutxo.mdupdated for the implemented lifecycle; the user-facing AssumeUTXO documentation lands withloadtxoutset)