Skip to content

backport: assumeutxo M2 — EvoDB multi-chainstate isolation and subsystem gating - #7456

Open
PastaPastaPasta wants to merge 17 commits into
dashpay:developfrom
PastaPastaPasta:assumeutxo/m2-evodb-roles
Open

backport: assumeutxo M2 — EvoDB multi-chainstate isolation and subsystem gating#7456
PastaPastaPasta wants to merge 17 commits into
dashpay:developfrom
PastaPastaPasta:assumeutxo/m2-evodb-roles

Conversation

@PastaPastaPasta

@PastaPastaPasta PastaPastaPasta commented Jul 13, 2026

Copy link
Copy Markdown
Member

Issue being fixed or feature implemented

M1 (#7451) added AssumeUTXO snapshot persistence, but Dash stores deterministic masternode, quorum, MNHF, and credit-pool state in a shared EvoDB. Running the snapshot and background chainstates concurrently therefore requires independent EvoDB transaction state and markers, chain-aware Dash validation, and protection against emitting or signing from the wrong chainstate.

This is milestone 2 of the AssumeUTXO series. It supplies the Dash-specific multi-chainstate foundation required by the later background-completion and loadtxoutset milestones.

What was done?

  • Added stable NORMAL and SNAPSHOT EvoDB identities, each with an independent transaction overlay, root batch, and best-block marker while retaining one physical EvoDB.
  • Added WriteDerived for immutable block-derived records. Independently derived values must serialize identically, including values pending in the other chainstate's overlay.
  • Wired block connect, disconnect, replay, verification, flushing, and snapshot activation to the calling chainstate's EvoDB identity. Snapshot activation now refuses a missing EvoDB marker instead of attaching snapshot coins to inconsistent Dash state.
  • Protected shared mined-quorum commitments from cross-chainstate erasure.
  • Passed the validating chainstate through Dash special-transaction and quorum processing. MNHF, asset-unlock, and quorum lookups now evaluate membership relative to the caller's chain instead of implicitly borrowing the active chain.
  • Suppressed active-tip, wallet, UI, and deterministic-masternode notifications from background validation. BlockChecked remains ungated because it reports validation results rather than active-tip changes.
  • Avoided penalizing peers when this snapshot-backed or pruned node cannot serve otherwise plausible masternode-list or quorum-rotation history.
  • Disabled DKG participation and quorum signing while an active snapshot remains unvalidated, including a final guard at the signature-share production boundary and clear RPC/status reporting.
  • Canonicalized serialization of block-derived masternode-list diffs and MNHF signals so logically identical values cannot produce false EvoDB mismatches.

Review follow-ups (appended commits):

  • A WriteDerived mismatch is local EvoDB corruption, never evidence about the block. It now aborts the node with M_ERROR (matching the existing EvoDbInconsistencyMessage convention) instead of marking the block BLOCK_CONSENSUS-invalid and penalizing the relaying peer. A typed EvoDbInconsistencyError preserves that classification through the catch blocks on the miner, RPC, and MNHF-recomputation paths.
  • EvoDB access outside a BeginTransaction scope previously always bound to the NORMAL identity, so transaction-less consumers (RPC, mempool, miner, P2P serving) could not see snapshot-chain records pending in the SNAPSHOT overlay. CEvoDB now tracks a default identity that snapshot activation sets to SNAPSHOT and ResetChainstates resets; the background-completion milestone must reset it to NORMAL at marker promotion (TODO noted in code).
  • GetListForBlockInternal no longer fabricates an empty "initial snapshot" masternode list when a diff for a DIP3-active block is missing; it throws instead. The message deliberately carries the IsBlockDataUnavailableError sentinel and is deliberately a plain runtime_error rather than EvoDbInconsistencyError: at that layer a missing diff can be benign (pending in the other chainstate's unflushed overlay, e.g. while serving historical mnlistdiff), so it is reported as unavailable history without penalizing the requesting peer, and only definite mismatches abort the node. Scheduler-thread consumers (CActiveMasternodeManager::UpdatedBlockTip, governance trigger creation) catch it and skip the update, since an uncaught exception there would terminate the node.

Second review round (appended commits):

  • bls::bls_legacy_scheme is process-wide, but the correct value belongs to the block being validated. ConnectBlock only saved and committed the flag; it never established it, and ProcessSpecialTxsInBlock switches legacy to basic only when crossing V19 forward. With an active post-V19 snapshot, the background chainstate therefore validated pre-V19 blocks under the basic scheme, and a background disconnect across V19 could commit legacy onto the active chain's consumers. ConnectBlock now enters under the scheme the block's parent left behind, and ConnectTip/DisconnectTip commit it only from the active chainstate.
  • The scheduler-thread guards caught every std::exception, so the plain runtime_errors that CDeterministicMNList::ApplyDiff raises for missing removals or updates, duplicate masternodes, and duplicate unique properties were logged as benign unavailable history while the active masternode stayed READY. The unavailable-history condition now has its own BlockDataUnavailableError type and is the only thing those guards (and BuildSimplifiedMNListDiff) catch; everything else propagates exactly as before the guards existed.
  • CEvoDB::GetCurrentIdentity() resolved to a process-wide active_transaction, so an open background NORMAL transaction redirected every concurrent transaction-less read away from the active snapshot's overlay. An open transaction is one validation execution context, not a process mode, so it now resolves only for the thread that began it.
  • Snapshot state could outlive the chainstate it describes in two ways, both fixed: PopulateAndValidateSnapshot commits the SNAPSHOT best-block and dual-chainstate markers as its last step, so abandoning activation afterwards (e.g. WriteSnapshotBaseBlockhash on an unwritable datadir) left a single-chainstate node carrying both; and -reindex/-reindex-chainstate wiped the shared EvoDB, erasing the SNAPSHOT marker while chainstate_snapshot stayed on disk, so startup failed on the erased marker with advice the user had already followed. Markers are now rolled back on abandoned activation, and the persisted snapshot chainstate's on-disk artifacts are discarded alongside the EvoDB wipe -- the Dash-shaped equivalent of the reindex-time cleanup in assumeutxo (2) bitcoin/bitcoin#27596/assumeutxo: Fix -reindex before snapshot was validated bitcoin/bitcoin#29726, which runs before any chainstate has coins views and so needs no mempool transfer or leveldb-lock dance.

The batch contains the original eight focused commits, a rebase fixture adaptation after the txindex removal, and eight review follow-up commits. The partial Bitcoin Core BlockInfo and ChainstateRole prerequisites were intentionally moved to the later loadtxoutset milestone where their APIs are first consumed.

How Has This Been Tested?

  • Added focused EvoDB unit coverage for overlay isolation, tombstones, cross-overlay derived-value checks, independent marker flushing, disk mismatch rejection, and preservation of the legacy NORMAL marker key.
  • Added dual-chainstate integration coverage for restart consistency, snapshot-only flushing, shared quorum erasure, chain-aware commitment/quorum lookup, cache-order isolation, and suppression of background notifications.
  • Added canonical serialization coverage for deterministic masternode-list diffs.
  • Added transaction_less_access_uses_default_identity covering default-identity resolution of transaction-less reads and writes. Known gap: the DIP3-active missing-diff throw has no end-to-end unit test because regtest activates DIP3 at height 432, above the unit-test chain heights; it is covered by the serving-path catch and review.
  • Before the stack-only relocation of the two unused Bitcoin prerequisite commits, make check passed and the following functional subset passed: feature_mnehf.py, feature_asset_locks.py, feature_dip3_deterministicmns.py (both modes), feature_llmq_signing.py (both modes), feature_llmq_rotation.py, and rpc_quorum.py.
  • After the review follow-up commits: full rebuild plus evo_db_tests, evo_deterministicmns_tests, evo_mnhf_tests, evo_assetlocks_tests, and validation_chainstatemanager_tests pass (20 cases), and lint-circular-dependencies is clean.
  • After the second review round (rebased onto develop at e829261): full rebuild clean and the complete test_dash suite passes (815 cases). Each new regression test was negative-controlled -- chainstate_connectblock_bls_scheme fails on both halves of the BLS fix independently, open_transaction_does_not_capture_other_threads fails without the thread scoping, and chainstatemanager_snapshot_discarded_on_reindex reproduces the unrecoverable reindex before the fix.
  • Every rewritten commit passes git diff-tree --check.

Breaking Changes

No released-interface changes. Internally, ActivateExistingSnapshot becomes fallible so startup can reject a snapshot chainstate whose EvoDB marker is missing. Two RPC-visible additions while a snapshot is active and unvalidated: masternode status gains a quorumParticipation field and appends a "DKG participation and quorum signing disabled" clause to status, and quorum sign fails with an explanatory error. AbstractEHFManager::Signals changed from std::unordered_map to std::map (canonical serialization); the on-disk encoding is unchanged, but iteration order of EHF signals in getblockchaininfo output is now sorted.

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have made corresponding changes to the documentation (the AssumeUTXO documentation update is part of a later milestone)
  • I have assigned this pull request to a milestone

@PastaPastaPasta

Copy link
Copy Markdown
Member Author

See https://gist.github.com/PastaPastaPasta/c5108775f7b126f88856cde20d7a8556 for ai generated commit by commit explanation.

@PastaPastaPasta
PastaPastaPasta force-pushed the assumeutxo/m2-evodb-roles branch 2 times, most recently from 06b5c6f to 1a95697 Compare July 14, 2026 17:01
@github-actions

Copy link
Copy Markdown

This pull request has conflicts, please rebase.

@github-actions

Copy link
Copy Markdown

This pull request has conflicts, please rebase.

@PastaPastaPasta
PastaPastaPasta force-pushed the assumeutxo/m2-evodb-roles branch from 26c8718 to b6edcf8 Compare August 1, 2026 18:17
@PastaPastaPasta
PastaPastaPasta force-pushed the assumeutxo/m2-evodb-roles branch from 11a31da to 5f3a876 Compare August 1, 2026 22:40
@knst

knst commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

that's not my final review feedback, I am not determined yet how exactly improve or change this PR or keep it as it is

  1. New sanitizers requires chainstate to be replaced to chainmanager; I repeat these changes independently, see fix: multiple asan's finding #7513
    These changes are unrelated to content of 7456 [make evodb multi-chainstate] and I'd like to get them merged independently.

  2. conceptually 7456 changes should work and it seems fine for me, concept ACK for me for idea as working.
    Though, I think changes here are not full.

There are several components that are logically part of chainstate, but initialized separately and behave differently.
They are:

  • credit_pool_manager (part of CChainstateHelper)
  • ehf_manager (part of CChainstateHelper)
  • mn_payments (part of CChainstateHelper)
  • dmnman. It's initialized inside chainstate, has in dependencies only evodb but many piece of validation of blocks are depends on dmnman
  • quorumsman. This class seems as a bit over-complex assuming that multiple chains could exist due to keeping caches inside, see: mapQuorumsCache, scanQuorumsCache, quorumBaseBlockIndexCache. Instead having 2 implementation for chain == nullptr and chain != nullptr maybe better to pull out these members to new object that is part of chainstate?

@PastaPastaPasta what is your thoughts on credit_pool_manager, ehf_manager, mn_payments? I think they should be part of ChainState.

And one more things: you have no plans to make evodb a part of assume-utxo, right?
Though, I think credit_pool_manager and ehf_manager probably should be.

=================

EvoDbIdentity - can I get clarification about this one? it means that one physical file with database will keep several logical copies of evo-db, right?
why not move evodb to chainstate in this case?
Alternativly, would not it be better to keep evodb as a just separate file?

-evodb = std::make_unique<CEvoDB>(db_params);
+m_evodb{std::make_unique<CEvoDB>(db_params, snapshot_base ? "evodb" + std::string{node::SNAPSHOT_CHAINSTATE_SUFFIX} : "evodb")},

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown

This pull request has conflicts, please rebase.

@PastaPastaPasta
PastaPastaPasta force-pushed the assumeutxo/m2-evodb-roles branch from 5f3a876 to d47d9b8 Compare August 2, 2026 18:42
@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown

This pull request has conflicts, please rebase.

@PastaPastaPasta
PastaPastaPasta force-pushed the assumeutxo/m2-evodb-roles branch from d47d9b8 to 7fabb1d Compare August 2, 2026 21:04
@PastaPastaPasta
PastaPastaPasta force-pushed the assumeutxo/m2-evodb-roles branch from 7fabb1d to bdd5626 Compare August 2, 2026 21:15
@PastaPastaPasta

Copy link
Copy Markdown
Member Author

@knst Fable's raw response:

The load-bearing fact his review is missing
Every piece of Dash evo state he lists is a pure function of blocks, keyed by block hash: creditPoolCache is Uint256LruHashMap<CCreditPool> looked up by pindex, mnhfCache likewise, dmnman's MN lists are block-keyed diffs, and quorum composition at base block B depends only on B's ancestry. Both chainstates share one global block index, so this state is chainstate-agnostic by construction — the MN list at height h is identical whether the background chain or the snapshot chain derived it.

The only state that is genuinely per-chainstate is tip tracking: which block each chainstate has flushed up to (best-block markers) and the in-flight transaction/undo stacks for the block currently connecting. That's exactly — and only — what EvoDbIdentity splits. Everything else stays shared, and the overlap becomes a feature: WriteDerived verifies that independently derived payloads agree byte-for-byte, aborting the node on divergence.

That's the frame for answering everything below.

Per-question answers
"EvoDbIdentity — one physical file keeping several logical copies of evo-db, right?"
No — this is the key clarification. One physical DB, one shared copy of all block-derived data (MN diffs, commitments, credit pool, EHF signals). Per-identity there are only: a transaction context (pending batches) and a best-block marker. The committed keyspace is not duplicated; cross-identity writes of the same key are disjoint by construction (background validates ≤ base, snapshot validates > base, seed is flushed at activation), and WriteDerived enforces identity when they do overlap. It's closer to "one ledger, two bookmarks" than "two ledgers."

"Why not move evodb into Chainstate / keep it as a separate suffixed file?"
His suffixed-file suggestion mirrors upstream's chainstate_snapshot dir and is a defensible alternative — but it was considered and has concrete costs:

Completion atomicity. Upstream completion renames chainstate_snapshot → chainstate. A second DB means a second rename that cannot be atomic with the first, which squares the crash-recovery matrix M3's RecoverSnapshotCleanup already has to handle for the chainstate rename alone. The single-DB design completes via PromoteSnapshotMarkers — one atomic batch write, no rename.
Lost cross-validation. With a shared keyspace, background validation incrementally re-derives and cross-checks the snapshot's seeded evo state via WriteDerived, catching a bad snapshot early and localizing the divergence. Separate files reduce this to a single holistic compare at completion.
Duplication. Two files store identical derived data for the overlapping range.
Downgrade safety was designed around the single file: released software ignores chainstate_snapshot and reads the legacy b_b4 marker, so the chainstate dir + legacy marker remain a consistent background pair and downgrade mid-snapshot degrades to background IBD (documented at the top of evodb.h).
"Should credit_pool_manager / ehf_manager / mn_payments (and dmnman) be part of Chainstate?"
I'd argue no, as members — and the series is deliberate about the alternative: managers stay singletons and the calling Chainstate& is threaded through validation ("bind block validation to the calling chainstate"). Reasons: (a) their state is block-keyed, so per-chainstate instances would duplicate identical state and forfeit the cross-check; (b) Chainstate objects are created and destroyed across snapshot activation/completion/ResetChainstates — embedding managers there recreates precisely the lifetime hazards #7513 just fixed, now for every consumer holding a manager reference (mempool, miner, RPC, net); (c) mempool/P2P/RPC need MN-list and quorum answers independent of which chainstate is active, so they'd route through ActiveChainstate()'s embedded members anyway — same shape, more indirection; (d) it forks upstream's Chainstate layout, growing the perpetual backport diff. On dmnman specifically: it's initialized in LoadChainstate/CompleteChainstateInitialization (not "inside chainstate") because reindex has to wipe and recreate it in lockstep with pblocktree — it's a per-load singleton, and [#7471](https://github.com/dashpay/dash/pull/7471) already moved its wiring into ChainstateLoadOptions.

"quorumsman is over-complex; pull the caches into a per-chainstate object?"
I think this reads the caches backwards. mapQuorumsCache/scanQuorumsCache/quorumBaseBlockIndexCache are all keyed by quorum-base block hash or pindex — that's what makes them already correct under multiple chainstates, and shared caching dedupes work where the two chains overlap (everything ≤ base). Per-chainstate cache objects would duplicate memory, recompute on the background chain, and add cache-lifecycle churn when the snapshot chainstate is deleted at completion. The chain == nullptr / != nullptr dual paths in ScanQuorums are a fair complexity complaint, but that's an independent refactor opportunity, not something the assumeutxo work requires or worsens.

"You have no plans to make evodb part of assume-utxo, right? Though credit_pool_manager and ehf_manager probably should be."
This one he'll be glad to hear: the plan is the opposite, and it's already built — he's reviewing M2 in isolation. M4 (assumeutxo/m4-evo-snapshot, fork PR PastaPastaPasta#53) defines evo snapshot format v3: the dump embeds the MN list at the base, quorum commitments and rotation cycles, the credit pool, MNHF/EHF signals, and asset-unlock ranges, with an evo_hash commitment in dumptxoutset output; loadtxoutset seeds evodb from it and background validation cross-checks the seed as it catches up. So credit pool and EHF are exactly where he thinks they should be — in the snapshot, not left behind.

This is based on the context of the full WIP assumeutxo implementation. I think it answers your questions well.

@PastaPastaPasta
PastaPastaPasta marked this pull request as ready for review August 3, 2026 00:04
@thepastaclaw

thepastaclaw commented Aug 3, 2026

Copy link
Copy Markdown

🔍 Review in progress — actively reviewing now (commit 066fca8)

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

Potential PR merge conflicts

This is advisory only. It does not block CI, but it marks PRs that will likely need a rebase depending on merge order.

If this PR merges first

These open PRs will likely need a rebase:

If these PRs merge first

This PR will likely need a rebase:

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Preliminary review — Codex only

The chain-aware EvoDB changes are generally well structured, but startup has a confirmed recovery regression: both reindex modes erase the snapshot EvoDB marker before requiring it, so a node with a persisted snapshot chainstate cannot reindex. The commit stack also contains six uncompilable bisect points due to stale txindex fixture calls; two additional history rewrites would keep corrective changes atomic with the behavior they fix.

Validated blockers were found in the Codex precheck. Sonnet is deferred until a fresh Codex revalidation clears the blocker gate.

Review provenance

  • Codex reviewers: gpt-5.6-sol — general (completed), gpt-5.6-sol — dash-core-commit-history (completed), gpt-5.6-sol — backport-reviewer (completed)
  • Verifier: gpt-5.6-sol — verifier
  • Sonnet: not run (deferred by blocker gate)

🔴 2 blocking | 🟡 2 suggestion(s)

3 additional finding(s) omitted (not in diff).

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `src/node/chainstate.cpp`:
- [BLOCKING] src/node/chainstate.cpp:57-71: Reindex cannot recover a node with a persisted snapshot chainstate
  `LoadChainstate()` constructs `CEvoDB` with `wipe = true` for both `-reindex` and `-reindex-chainstate`, which removes the SNAPSHOT best-block marker. The `chainstate_snapshot` directory and its base-blockhash file remain on disk, so `DetectSnapshotChainstate()` finds the directory and `ActivateExistingSnapshot()` immediately rejects it because the marker was just erased. Startup exits before the snapshot coins database can be wiped, making the error's instruction to reindex ineffective on every retry. Explicit reindexing must remove the persisted snapshot chainstate artifacts, or otherwise avoid enforcing the erased marker while coherently discarding that chainstate, and a regression test should cover reindex with `chainstate_snapshot` present.

In `<commit:ae5e18f>`:
- [BLOCKING] <commit:ae5e18f>:1: Fold the delayed txindex cleanup into the test commit
  Commit `f77de21` adds `TxIndex(1 << 20, true)` and `Start(restarted.ActiveChainstate())`, but at that commit `TxIndex` requires a `std::unique_ptr<interfaces::Chain>` as its first argument and `BaseIndex::Start()` takes no arguments. The calls therefore do not compile, leaving all six commits from `f77de21` through `92b2b98` unusable bisect points until `ae5e18f` removes them. Rewrite `f77de21` without the stale txindex restart code. The `govman` teardown-order change in `ae5e18f` is independent and should be folded into the fixture-lifetime commit that requires it or retained as a separately described teardown fix.

In `<commit:d596742>`:
- [SUGGESTION] <commit:d596742>:1: Squash the WriteDerived error-classification corrections
  Commit `f75031e` adds the `WriteDerived` call sites but initially reports mismatches as consensus failures or throws generic exceptions that enclosing catches convert to consensus failures. Commit `d596742` changes these newly introduced paths to node-abort/M_ERROR handling, and `d0cda00` then fixes the remaining `GetForBlock` rethrow that lost this classification. Squash both corrective commits into `f75031e` so the feature is introduced with the final error semantics and no intermediate commit can persistently mark a valid block failed or penalize its relaying peer.

In `<commit:8d7b52a>`:
- [SUGGESTION] <commit:8d7b52a>:1: Keep the new masternode-list throw and its callers atomic
  Commit `4a3c1fd` makes `GetListForBlockInternal()` throw when a DIP3-active list diff is unavailable in a dual-chainstate run, while `CActiveMasternodeManager::UpdatedBlockTip()` and `GovernanceSigner::CreateGovernanceTrigger()` still call it without catches in that commit. These callbacks execute through the scheduler-backed validation interface, so the intermediate commit can terminate the node. Move the throw and the two catches from `8d7b52a` into one atomic commit, or squash `8d7b52a` into `4a3c1fd` and update the subject to cover both the new failure mode and its caller handling.

Comment thread src/node/chainstate.cpp
@PastaPastaPasta
PastaPastaPasta force-pushed the assumeutxo/m2-evodb-roles branch from 8d7b52a to 993d53b Compare August 3, 2026 18:42

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 993d53b8f8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/validation.cpp

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Preliminary review — Codex only

All four carried-forward prior findings remain valid: snapshot reindex recovery is broken, six intermediate revisions do not compile, and two corrective changes remain separated from the commits that introduce their behavior. The latest delta is tree-equivalent to the prior head and adds no new logical change; cumulative reinspection additionally confirms the missing bitcoin#27596/bitcoin#29726 cleanup prerequisite, incorrect process-wide BLS state during background validation, and overly broad scheduler exception handling. Five canonical blockers require changes before merge.

Validated blockers were found in the Codex precheck. Sonnet is deferred until a fresh Codex revalidation clears the blocker gate.

Review provenance

  • Codex reviewers: gpt-5.6-sol — general (completed), gpt-5.6-sol — dash-core-commit-history (completed), gpt-5.6-sol — backport-reviewer (completed)
  • Verifier: gpt-5.6-sol — verifier
  • Sonnet: not run (deferred by blocker gate)

🔴 5 blocking | 🟡 2 suggestion(s)

3 additional finding(s) omitted (not in diff).

1 carried-forward finding(s) already raised on this PR; not re-posting as new inline comments.

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `src/node/chainstate.cpp`:
- [BLOCKING] src/node/chainstate.cpp:57-71: Missing prerequisite: bitcoin#27596 snapshot reindex cleanup
  Upstream bitcoin#27596 commit `c711ca186f8` added snapshot-chainstate deletion during both reindex modes, and bitcoin#29726 commit `e57f951805b` corrected that operation to locate the on-disk snapshot directory without initialized coins views and transfer the mempool before destroying the snapshot chainstate. Neither operation exists in this PR's base or head. Because this stack now wipes the shared EvoDB before requiring the snapshot-specific marker, omitting that prerequisite makes persisted-snapshot reindexing unrecoverable. Adapt the corrected cleanup here, including the necessary Dash manager/EvoDB teardown and rebinding, and test both reindex modes.

In `<commit:0d36cebe991>`:
- [BLOCKING] <commit:0d36cebe991>:1: Fold the delayed txindex cleanup into the test commit
  Commit `19da2c28add` adds `std::make_unique<TxIndex>(1 << 20, true)` and `Start(restarted.ActiveChainstate())`. At that exact revision, `TxIndex` requires a `std::unique_ptr<interfaces::Chain>` as its first constructor argument and `BaseIndex::Start()` takes no arguments, so both calls fail to compile. The six revisions from `19da2c28add` through `d36983cfba1` remain unusable bisect points until `0d36cebe991` removes the stale code. Rewrite `19da2c28add` without those calls, and fold the independent `govman` teardown-order repair into the fixture-lifetime change that requires it or retain it as a separately described commit.

In `src/validation.cpp`:
- [BLOCKING] src/validation.cpp:2518-2521: Background validation still uses the active chainstate's global BLS scheme
  `bls::bls_legacy_scheme` is process-wide, but `ConnectBlock()` only saves the current global value and commits any successful transition; it never initializes the scheme from the chainstate being validated. With an active post-V19 snapshot, the global flag is basic, so background validation of pre-V19 blocks starts under the basic scheme and never switches to legacy because `ProcessSpecialTxsInBlock()` only changes the flag while crossing V19 forward. Historical BLS-serialized quorum and special-transaction data can therefore be decoded or validated under the wrong scheme. A background disconnect across V19 can conversely commit the legacy scheme and leave active-chain consumers using it. Establish the scheme from the calling chainstate before each background connect or disconnect and restore the active chainstate's scheme afterward, with dual-chainstate coverage spanning V19.

In `src/active/masternode.cpp`:
- [BLOCKING] src/active/masternode.cpp:183-192: Only swallow the intended unavailable-history exception
  The new scheduler protection catches every `std::exception`, although the recoverable condition is specifically the error matched by `IsBlockDataUnavailableError`. `GetListForBlock()` can also throw for inconsistent list diffs: `ApplyDiff()` reports missing removals or updates, duplicate masternodes, and duplicate unique properties with ordinary `std::runtime_error`. Those failures are now logged as benign unavailable history, leaving the active masternode in its previous READY state; `GovernanceSigner::CreateGovernanceTrigger()` has the same broad catch. Return only for the unavailable-history sentinel, and propagate or abort for all other exceptions so local EvoDB/list corruption is not hidden.

In `<commit:03cb36c524b>`:
- [SUGGESTION] <commit:03cb36c524b>:1: Squash the WriteDerived error-classification corrections
  Commit `8c1ec1e9aef` introduces `WriteDerived` call sites that classify deterministic EvoDB mismatches as consensus failures or throw generic exceptions that enclosing catches translate into consensus failures. Commits `03cb36c524b` and `7a12dba1298` later repair those paths to abort the node with `M_ERROR` and preserve that classification through `CMNHFManager::GetForBlock()`. Canonical serialization required for reliable byte comparison also does not arrive until `d36983cfba1`, six commits after first use. Introduce `WriteDerived` with its final corruption semantics and canonical payload serialization so intermediate revisions cannot reject valid blocks, penalize peers, or compare logically identical values using noncanonical bytes.

In `<commit:993d53b8f87>`:
- [SUGGESTION] <commit:993d53b8f87>:1: Keep the new masternode-list throw and its callers atomic
  Commit `04c615834a0` makes `GetListForBlockInternal()` throw when a DIP3-active list diff is unavailable, but `CActiveMasternodeManager::UpdatedBlockTip()` and `GovernanceSigner::CreateGovernanceTrigger()` still call it without catches at that revision. These callbacks execute through the scheduler-backed validation interface, so the intermediate commit can terminate the node. Move the missing-diff behavior and both caller adaptations into one atomic commit, or squash `993d53b8f87` into `04c615834a0` and update the subject accordingly.

Comment thread src/node/chainstate.cpp
Comment thread src/validation.cpp
Comment thread src/active/masternode.cpp
PastaPastaPasta and others added 17 commits August 4, 2026 15:47
Pass the validating chainstate through special transaction and quorum commitment processing instead of borrowing the active chainstate.

Interpret mined-commitment records and quorum resolution relative to the caller's chain. The cached values remain reusable, but chain membership is reevaluated across reorgs and chainstates while public non-validation callers retain active-chain semantics. This prevents snapshot-seeded records from suppressing commitments or satisfying MNHF and asset-unlock quorum lookups during background validation.

Add dual-chainstate coverage for a commitment seeded at a block not yet contained by the background chain, including HasQuorum and GetQuorum cache-order checks.
Emit block, tip, deterministic masternode-list, UI, and flush notifications only for the active chainstate. In particular, suppressing background ChainStateFlushed prevents a background locator from regressing wallet best-block state.

Keep BlockChecked ungated because its subscribers are mining/block-submit and peer validation/relay accounting; it does not reach CMNAuth. Document all 21 B3 call-site dispositions and extend the dual-chainstate test with validation-interface and UI counters.
Check local block-data availability before building masternode-list diffs and quorum rotation info. Treat failures caused by pruning or an unvalidated snapshot base like pruned getdata: log and silently drop the plausible request without increasing the peer's misbehavior score.

Malformed and implausible requests retain the pre-existing penalties.
Disable DKG participation and quorum signing until snapshot background validation completes. Enforce the refusal at CreateSigShare, the actual share-production boundary, so direct RPC, async, and queued signing paths cannot bypass it.

The quorum sign RPC now returns a clear JSON-RPC error for both submit modes, and masternode status exposes the disabled participation state. Add unit coverage for the shared production-gate predicate across snapshot activation.
A WriteDerived failure means independently derived block data disagrees with the copy already recorded in EvoDB. That is local state corruption (or a cross-chainstate divergence bug), never evidence about the block being processed. Previously the mismatch surfaced as BLOCK_CONSENSUS: the block was persistently marked BLOCK_FAILED_VALID (surviving restart and forking the node off the network) and the relaying peer was handed a 100-point misbehavior score via BlockChecked, which background validation also triggers.

Instead, follow the existing EvoDbInconsistencyMessage convention: request node shutdown via AbortNode and fail validation with M_ERROR, which neither marks the block invalid nor punishes peers. The credit-pool and MNHF sites abort at the throw site because miner and RPC callers never pass through a validation-state catch; a typed EvoDbInconsistencyError lets the four block-path catch blocks that would otherwise swallow it into BLOCK_CONSENSUS reclassify it as M_ERROR.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ethrow

CMNHFManager::GetForBlock re-throws internal ProcessBlock failures as a plain runtime_error, which would let a downstream generic catch misreport an EvoDB mismatch (M_ERROR) as a consensus failure. Re-throw typed when the validation state carries M_ERROR.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reads and writes outside a BeginTransaction scope previously always bound to the NORMAL identity. While a snapshot chainstate is active, transaction-less consumers (RPC, mempool, miner, P2P serving) could not see snapshot-chain records still pending in the SNAPSHOT root overlay, and transaction-less writes dirtied the wrong overlay. CEvoDB now tracks a default identity which snapshot activation (ActivateSnapshot, ActivateExistingSnapshot) sets to SNAPSHOT and ResetChainstates resets; snapshot completion must reset it to NORMAL when marker promotion lands.

Also make the GetListForBlockInternal fallback loud: a missing list diff for a DIP3-active block is pending-elsewhere or corrupt data, never the pre-DIP3 genesis of the masternode list, so throw instead of silently caching an empty list and clobbering m_initial_snapshot_index. The thrown message carries the IsBlockDataUnavailableError sentinel and BuildSimplifiedMNListDiff converts it into a serve failure, so peers requesting such history are not penalized.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
GetListForBlock can now throw when list data is unavailable (missing diff for a DIP3-active block). CActiveMasternodeManager::UpdatedBlockTip and GovernanceSigner's trigger creation run on the scheduler thread, where an uncaught exception terminates the node; catch it there and skip the update instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The scheduler-thread guards added for snapshot nodes caught every std::exception, but the only recoverable condition is a masternode list diff that is not on this node yet. CDeterministicMNList::ApplyDiff raises a plain std::runtime_error for missing removals or updates, duplicate masternodes, and duplicate unique properties -- local EvoDB corruption, which was then logged as benign unavailable history while the active masternode stayed READY.

Give the unavailable-history condition its own BlockDataUnavailableError type and catch only that. Everything else propagates exactly as it did before these guards existed. BuildSimplifiedMNListDiff is narrowed the same way, so local corruption no longer turns into a peer-facing error string.
bls::bls_legacy_scheme is process-wide, but the correct value is a property of the block being processed. ConnectBlock only saved the current value and committed a successful transition; it never established the scheme for the chainstate it was validating. ProcessSpecialTxsInBlock switches legacy->basic when crossing V19 forward and never the other way, so with an active post-V19 snapshot the background chainstate validated pre-V19 blocks under the basic scheme, decoding historical BLS-serialized quorum and special-transaction data with the wrong one. A background disconnect across V19 could conversely commit legacy and leave active-chain consumers there.

Enter ConnectBlock under the scheme the block's parent left behind, and commit it out of ConnectTip/DisconnectTip only from the active chainstate.
GetCurrentIdentity() resolved to the process-wide active_transaction, so while the background chainstate held a NORMAL transaction open every concurrent transaction-less read was redirected to NORMAL instead of the active snapshot's default identity. CQuorumManager::GetQuorum releases cs_main before BuildQuorumFromCommitment calls GetMinedCommitment: if background validation started in between, snapshot-only records still pending in the SNAPSHOT overlay dropped out of that lookup and the active node intermittently reported a missing quorum.

An open transaction is the identity of one validation execution context, not of the process, so resolve it only for the thread that began it.
…away

Two ways a persisted snapshot chainstate could outlive itself:

PopulateAndValidateSnapshot commits the SNAPSHOT best-block and dual-chainstate markers as its last step. If activation was then abandoned -- WriteSnapshotBaseBlockhash on an unwritable datadir, say -- only the coins directory was removed. The node went back to a single chainstate carrying a permanent dual-chainstate marker, which turns supported legacy missing-diff bootstrapping into unavailable history, and a stale SNAPSHOT marker that a future snapshot directory could satisfy ActivateExistingSnapshot with. Erase both when activation is rolled back.

LoadChainstate constructs CEvoDB with wipe=true for -reindex and -reindex-chainstate, erasing the SNAPSHOT marker while chainstate_snapshot and its base-blockhash file stay on disk. DetectSnapshotChainstate then found the directory and ActivateExistingSnapshot rejected it on the marker just erased, so startup failed with advice ('reindex is required') the user had already followed -- on every retry. Discard the persisted snapshot chainstate's on-disk artifacts alongside the EvoDB wipe. This is the Dash-shaped equivalent of the reindex-time cleanup in bitcoin#27596/bitcoin#29726; it runs before any chainstate has coins views, so no mempool transfer or leveldb-lock dance is needed.
@PastaPastaPasta
PastaPastaPasta force-pushed the assumeutxo/m2-evodb-roles branch from 993d53b to 066fca8 Compare August 4, 2026 21:32
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 066fca8b4f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/active/context.cpp

nodeman->UpdatedBlockTip(pindexNew, pindexFork, fInitialDownload);

if (m_chainman.IsSnapshotActiveAndUnvalidated()) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Move snapshot gate before masternode tip update

This guard runs after nodeman->UpdatedBlockTip(), so an active masternode that is not yet READY during an unvalidated snapshot can enter CActiveMasternodeManager::UpdatedBlockTip()'s Init(pindexNew) path before the gate is checked; InitInternal() calls GetListForBlock(pindexNew) without the new BlockDataUnavailableError catch, and that uncaught exception is on the scheduler validation callback path. In the same missing-history snapshot scenario this gate is meant to suppress, the node can terminate before DKG/signing is disabled, so check IsSnapshotActiveAndUnvalidated() before calling nodeman or catch the unavailable-list exception in the init path as well.

AGENTS.md reference: AGENTS.md:L168-L170

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
src/llmq/snapshot.cpp (1)

22-29: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add coverage for unavailability without peer penalization.

GETQUORUMROTATIONINFO already treats is not available (pruned or below an unvalidated snapshot base) as local unavailability, but p2p_quorum_data.py should include a pruned/unvalidated-snapshot request to cover this peer-sparing path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/llmq/snapshot.cpp` around lines 22 - 29, Extend the GETQUORUMROTATIONINFO
coverage in p2p_quorum_data.py with a request for block data that is unavailable
because it is pruned or below an unvalidated snapshot base. Assert that this
local-unavailability response does not penalize the peer, preserving the
existing handling for the error text produced by CheckBlockDataAvailable.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/evo/evodb.cpp`:
- Around line 67-74: Update CEvoDB::BeginTransaction so GetContext(identity) and
CEvoDBScopedCommitter construction complete before assigning active_transaction
and active_transaction_thread; only publish transaction state after both
fallible operations succeed, while preserving the existing lock and returned
committer behavior.

In `@src/validation.cpp`:
- Around line 3594-3597: Require this == &m_chainman.ActiveChainstate() in the
UI tip notification conditions for both invalidation handling at
src/validation.cpp lines 3594-3597 and conflicting-chain handling at lines
3699-3702, alongside the existing pindex_was_in_chain checks; leave the
main-signal notifications unchanged.
- Line 2011: Initialize the required pre-disconnect BLS scheme before
UndoSpecialTxsInBlock in src/validation.cpp:2011, while preserving the scheme
transition committed by DisconnectTip; also initialize the pre-connect scheme
before ProcessSpecialTxsInBlock during replay in src/validation.cpp:4655 and
restore the active-chainstate scheme afterward. Add coverage for interrupted
post-V19 snapshot replay and CVerifyDB validation.

---

Nitpick comments:
In `@src/llmq/snapshot.cpp`:
- Around line 22-29: Extend the GETQUORUMROTATIONINFO coverage in
p2p_quorum_data.py with a request for block data that is unavailable because it
is pruned or below an unvalidated snapshot base. Assert that this
local-unavailability response does not penalize the peer, preserving the
existing handling for the error text produced by CheckBlockDataAvailable.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 37a9ab73-7488-4e6f-9838-ceae31e6e69a

📥 Commits

Reviewing files that changed from the base of the PR and between e829261 and 066fca8.

📒 Files selected for processing (45)
  • src/Makefile.test.include
  • src/active/context.cpp
  • src/active/context.h
  • src/active/dkgsessionhandler.cpp
  • src/active/masternode.cpp
  • src/dbwrapper.h
  • src/evo/assetlocktx.cpp
  • src/evo/assetlocktx.h
  • src/evo/chainhelper.cpp
  • src/evo/chainhelper.h
  • src/evo/creditpool.cpp
  • src/evo/deterministicmns.cpp
  • src/evo/deterministicmns.h
  • src/evo/evodb.cpp
  • src/evo/evodb.h
  • src/evo/mnhftx.cpp
  • src/evo/mnhftx.h
  • src/evo/smldiff.cpp
  • src/evo/smldiff.h
  • src/evo/specialtxman.cpp
  • src/evo/specialtxman.h
  • src/governance/signing.cpp
  • src/llmq/blockprocessor.cpp
  • src/llmq/blockprocessor.h
  • src/llmq/quorumsman.cpp
  • src/llmq/quorumsman.h
  • src/llmq/signing_shares.cpp
  • src/llmq/signing_shares.h
  • src/llmq/snapshot.cpp
  • src/net_processing.cpp
  • src/node/chainstate.cpp
  • src/node/miner.cpp
  • src/rpc/blockchain.cpp
  • src/rpc/masternode.cpp
  • src/rpc/quorums.cpp
  • src/test/evo_cbtx_tests.cpp
  • src/test/evo_db_tests.cpp
  • src/test/evo_deterministicmns_tests.cpp
  • src/test/util/setup_common.cpp
  • src/test/util/setup_common.h
  • src/test/validation_chainstate_tests.cpp
  • src/test/validation_chainstatemanager_tests.cpp
  • src/validation.cpp
  • src/validation.h
  • src/versionbits.h
🚧 Files skipped from review as they are similar to previous changes (36)
  • src/versionbits.h
  • src/Makefile.test.include
  • src/llmq/signing_shares.h
  • src/test/evo_deterministicmns_tests.cpp
  • src/net_processing.cpp
  • src/rpc/masternode.cpp
  • src/rpc/quorums.cpp
  • src/active/context.cpp
  • src/node/miner.cpp
  • src/llmq/quorumsman.h
  • src/dbwrapper.h
  • src/governance/signing.cpp
  • src/llmq/signing_shares.cpp
  • src/rpc/blockchain.cpp
  • src/evo/smldiff.cpp
  • src/evo/chainhelper.cpp
  • src/test/evo_cbtx_tests.cpp
  • src/evo/chainhelper.h
  • src/evo/creditpool.cpp
  • src/evo/assetlocktx.h
  • src/evo/mnhftx.cpp
  • src/evo/specialtxman.h
  • src/active/dkgsessionhandler.cpp
  • src/llmq/blockprocessor.h
  • src/active/masternode.cpp
  • src/evo/deterministicmns.cpp
  • src/evo/smldiff.h
  • src/test/util/setup_common.cpp
  • src/evo/specialtxman.cpp
  • src/active/context.h
  • src/llmq/quorumsman.cpp
  • src/evo/assetlocktx.cpp
  • src/test/validation_chainstatemanager_tests.cpp
  • src/llmq/blockprocessor.cpp
  • src/evo/evodb.h
  • src/evo/mnhftx.h

Comment thread src/evo/evodb.cpp
Comment on lines +67 to +74
std::unique_ptr<CEvoDBScopedCommitter> CEvoDB::BeginTransaction(EvoDbIdentity identity)
{
LOCK(cs);
assert(!active_transaction.has_value());
active_transaction = identity;
active_transaction_thread = std::this_thread::get_id();
GetContext(identity);
return std::make_unique<CEvoDBScopedCommitter>(*this, identity);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate evodb files =="
fd -a 'evodb\.(cpp|h)$|EvoDb' . || true

echo "== evodb outline =="
ast-grep outline src/evo/evodb.cpp --view expanded || true

echo "== relevant evodb.cpp sections =="
wc -l src/evo/evodb.cpp src/evo/evodb.h
sed -n '1,180p' src/evo/evodb.cpp
echo "== evodb.h =="
sed -n '1,240p' src/evo/evodb.h

echo "== calls to BeginTransaction and active_transaction =="
rg -n "BeginTransaction|active_transaction|active_transaction_thread|CEvoDBScopedCommitter|GetContext|Start|Commit|Rollback" src test contrib -S || true

Repository: dashpay/dash

Length of output: 50369


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== evodb.cpp relevant lines =="
wc -l src/evo/evodb.cpp src/evo/evodb.h
sed -n '1,130p' src/evo/evodb.cpp
echo
sed -n '1,220p' src/evo/evodb.h

echo
echo "== focused BeginTransaction/assert usages =="
rg -n "active_transaction|active_transaction_thread|BeginTransaction|GetContext\\(|Commit|Rollback|scoped" src/evo/evodb.* src/test/evo_db_tests.cpp src -g '*.cpp' -g '*.h' -S

echo
echo "== evodb tests relevant sections =="
sed -n '1,120p' src/test/evo_db_tests.cpp
sed -n '120,280p' src/test/evo_db_tests.cpp

Repository: dashpay/dash

Length of output: 50369


Set transaction state after fallible construction.

GetContext(identity) and std::make_unique<CEvoDBScopedCommitter> can throw after active_transaction is set. The failed call leaves state without a committer to clear it, so the next BeginTransaction asserts and an in-party transaction may resolve with the stale identity if the caller reuses its thread. Build the context and committer before assigning active_transaction and active_transaction_thread.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/evo/evodb.cpp` around lines 67 - 74, Update CEvoDB::BeginTransaction so
GetContext(identity) and CEvoDBScopedCommitter construction complete before
assigning active_transaction and active_transaction_thread; only publish
transaction state after both fallible operations succeed, while preserving the
existing lock and returned committer behavior.

Comment thread src/validation.cpp

std::optional<MNListUpdates> mnlist_updates_opt{std::nullopt};
if (!m_chain_helper->special_tx->UndoSpecialTxsInBlock(block, pindex, mnlist_updates_opt)) {
if (!m_chain_helper->special_tx->UndoSpecialTxsInBlock(*this, block, pindex, mnlist_updates_opt)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Initialize the BLS scheme in direct replay and verification paths.

ReplayBlocks() and CVerifyDB call DisconnectBlock() directly. ReplayBlocks() also calls RollforwardBlock() directly. These paths bypass the guards in DisconnectTip() and ConnectBlock(). A post-V19 operation can then process BLS data with the scheme left by another chainstate.

  • src/validation.cpp#L2011-L2011: establish the required pre-disconnect scheme for direct DisconnectBlock() callers while preserving the transition that DisconnectTip() commits.
  • src/validation.cpp#L4655-L4655: establish the required pre-connect scheme before ProcessSpecialTxsInBlock() during replay, then restore the active-chainstate scheme after replay.

Add coverage for interrupted post-V19 snapshot replay and CVerifyDB validation.

📍 Affects 1 file
  • src/validation.cpp#L2011-L2011 (this comment)
  • src/validation.cpp#L4655-L4655
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/validation.cpp` at line 2011, Initialize the required pre-disconnect BLS
scheme before UndoSpecialTxsInBlock in src/validation.cpp:2011, while preserving
the scheme transition committed by DisconnectTip; also initialize the
pre-connect scheme before ProcessSpecialTxsInBlock during replay in
src/validation.cpp:4655 and restore the active-chainstate scheme afterward. Add
coverage for interrupted post-V19 snapshot replay and CVerifyDB validation.

Comment thread src/validation.cpp
Comment on lines +3594 to +3597
if (this == &m_chainman.ActiveChainstate()) {
GetMainSignals().SynchronousUpdatedBlockTip(m_chain.Tip(), nullptr, IsInitialBlockDownload());
GetMainSignals().UpdatedBlockTip(m_chain.Tip(), nullptr, IsInitialBlockDownload());
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Suppress UI tip notifications for background chainstate changes.

GetMainSignals() now checks the active chainstate. The nearby uiInterface.NotifyBlockTip() calls only check pindex_was_in_chain. A background invalidation or chainlock conflict can therefore report its tip as a UI tip update.

  • src/validation.cpp#L3594-L3597: require the active-chainstate check in the invalidation UI notification condition.
  • src/validation.cpp#L3699-L3702: require the active-chainstate check in the conflicting-chain UI notification condition.
📍 Affects 1 file
  • src/validation.cpp#L3594-L3597 (this comment)
  • src/validation.cpp#L3699-L3702
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/validation.cpp` around lines 3594 - 3597, Require this ==
&m_chainman.ActiveChainstate() in the UI tip notification conditions for both
invalidation handling at src/validation.cpp lines 3594-3597 and
conflicting-chain handling at lines 3699-3702, alongside the existing
pindex_was_in_chain checks; leave the main-signal notifications unchanged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants