backport: assumeutxo M2 — EvoDB multi-chainstate isolation and subsystem gating#51
backport: assumeutxo M2 — EvoDB multi-chainstate isolation and subsystem gating#51PastaPastaPasta wants to merge 25 commits into
Conversation
CBatchedSigShares::sigShares is a std::vector whose upper bound is the compile-time constant MAX_MSGS_TOTAL_BATCHED_SIGS, so express the cap inside SERIALIZE_METHODS with LIMITED_VECTOR. Wire counts above the cap now throw before any element is decoded or memory allocated, and the write side stays interoperable with the unbounded reader. At the QBSIGSHARES callsite the inner cap alone is not enough: many individually-valid batches can still exceed the total sig-share cap. Bound the outer batch count via ReadCompactSize with range_check=false (so this callsite's own limit, not the generic MAX_SIZE cap, decides) and check the running sig-share total as each batch is decoded so we stop before an attacker forces us through the full cross product of the per-vector limits. Any failure is logged, bans the peer, and rethrows. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthroughThis PR introduces identity-aware EvoDB transactions for normal and snapshot chainstates, threads explicit chain context through quorum and special-transaction processing, blocks DKG/signing during unvalidated snapshots, adds bounded signature decoding, improves block-data error handling, and expands unit and integration coverage. ChangesDual-chainstate validation and snapshot support
Conflict comment state parsing
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant Chainstate
participant EvoDB
participant QuorumBlockProcessor
participant QuorumManager
participant SigSharesManager
Chainstate->>EvoDB: BeginTransaction(EvoDbIdentity)
Chainstate->>QuorumBlockProcessor: ProcessBlock(chainstate)
QuorumBlockProcessor->>QuorumManager: GetQuorum(..., chain)
QuorumManager-->>QuorumBlockProcessor: chain-aware quorum
Chainstate->>SigSharesManager: CreateSigShare()
SigSharesManager-->>Chainstate: blocked or produced signature share
Chainstate->>EvoDB: CommitRootTransaction(EvoDbIdentity)
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
01795d3 to
dc94574
Compare
552e404 to
2a37be9
Compare
35d0d60 to
183f168
Compare
77a5910 to
56cd37a
Compare
The hidden state block was rendered first, so raw notification previews of the managed conflict comment started with an opaque base64 blob instead of the advisory text. Render it after the advisory instead. Reading it back can no longer take the first marker in the body, since advisory content (a PR title, say) may contain marker-like text ahead of the real block. Anchor on the last marker instead, unless the body starts with one, which is how comments in the old marker-first layout are still read.
The QSIGSHARE, QSIGSESANN and QSIGSHARESINV/QGETSIGSHARES handlers each decoded a std::vector unbounded before comparing the resulting count against a callsite-specific cap. That lets a malicious peer force us to allocate and decode arbitrarily large batches before we reject them. Replace the post-decode size checks with the shared UnserializeVectorWithMaxSize primitive, which reads the CompactSize prefix with the default range-checked ReadCompactSize (rejecting any count above the generic MAX_SIZE cap before this tighter callsite max_size is even applied), compares the resulting size_t directly against max_size with no separate uint64_t staging, and returns false without consuming any element on rejection. Convert the false return into an ios_base::failure so the surrounding catch uniformly logs, bans the peer, and rethrows for any decode failure — matching the existing QSIGSHARESINV/QGETSIGSHARES behavior on a malformed stream. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…oder The self-review of the batched sig-share intake bounds flagged one gap: the running-total abort in the QBSIGSHARES handler — where many individually-valid batches together exceed MAX_MSGS_TOTAL_BATCHED_SIGS — had no direct unit coverage. The check lived inline in NetSigning::ProcessMessage, so the only way to reach it was to drive a full P2P message through the handler. Extract the QBSIGSHARES decode loop into a free function, llmq::UnserializeBatchedSigShares(CDataStream&), that production calls inside the existing try/catch. Wire format, peer banning, and logging semantics are unchanged — the callsite still logs, bans, and rethrows on any ios_base::failure. The extracted decoder is the exact code that runs in production, so the new tests exercise the real path rather than a mirror. Add three targeted cases in llmq_utils_tests: - oversized outer batch count (built from empty batches so only the outer-count guard, not an end-of-stream artifact, can reject it), - oversized running aggregate across batches each within the per-batch cap, - an aggregate exactly at the cap, which must decode intact. Both count guards were mutation-verified: neutralizing either guard makes its corresponding test fail and no other. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
56cd37a to
f4b01da
Compare
f4b01da to
6a803ac
Compare
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/test/validation_chainstatemanager_tests.cpp (1)
463-500: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy liftReopen EvoDB to simulate an actual node restart.
SimulateNodeRestartreplaces only the chainstate manager; the sameCEvoDBobject and its transaction contexts survive. Consequently, the persistence test at Lines 767-793 can pass using retained state rather than data reopened from disk. Close and recreatem_node.evodbwith.wipe=falseafter tearing down its consumers.🤖 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/test/validation_chainstatemanager_tests.cpp` around lines 463 - 500, Update SimulateNodeRestart to close and recreate m_node.evodb with wipe=false after tearing down all consumers that depend on it and before constructing the replacement ChainstateManager. Ensure the recreated CEvoDB uses the existing database contents while clearing the old transaction contexts, so persistence tests exercise reopened disk state.
🤖 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/smldiff.cpp`:
- Around line 185-189: Remove the BLOCK_HAVE_DATA requirement from the
baseBlockIndex ancestry/list-anchor validation in src/evo/smldiff.cpp (lines
185-189), while retaining data checks for the blockIndex that is actually read.
In src/llmq/snapshot.cpp, update lines 43, 89-90, and 123 so metadata-only
CBlockIndex and snapshot lookup paths do not require raw block data; keep such
checks only at actual disk-read sites.
In `@src/test/util/setup_common.cpp`:
- Line 247: Reset or otherwise destroy m_node.evodb in
BasicTestingSetup::~BasicTestingSetup before calling
fs::remove_all(m_path_root), ensuring the disk-backed EvoDB is closed while
preserving the existing in-memory behavior.
In `@src/test/validation_chainstatemanager_tests.cpp`:
- Line 818: Update the restart call in the validation-chainstate test to pass
flush_chainstates=false after erasing the snapshot marker, so the erased marker
remains absent when startup failure is checked. Ensure SimulateNodeRestart
supports this no-flush path with a real EvoDB reopen, while preserving the
existing restart behavior elsewhere.
In `@src/validation.cpp`:
- Around line 3564-3567: Extend the active-chainstate guards in
src/validation.cpp at lines 3564-3567 and 3669-3672 to also enclose the adjacent
uiInterface.NotifyBlockTip calls at lines 3570-3573 and 3674-3677. Keep both
SynchronousUpdatedBlockTip and UpdatedBlockTip notifications restricted to
ActiveChainstate, preventing all related UI tip notifications from background
chainstates.
In `@test/lint/lint-circular-dependencies.py`:
- Line 27: Remove the allowlisted cycle entry involving blockfilter and
interfaces/chain.h from the circular-dependency test. Refactor the dependency
path through validation, kernel/chain, and interfaces/chain.h so shared chain
metadata comes from a lower-level header or an interface rather than the
concrete interface-layer dependency, preserving the intended isolation boundary.
---
Outside diff comments:
In `@src/test/validation_chainstatemanager_tests.cpp`:
- Around line 463-500: Update SimulateNodeRestart to close and recreate
m_node.evodb with wipe=false after tearing down all consumers that depend on it
and before constructing the replacement ChainstateManager. Ensure the recreated
CEvoDB uses the existing database contents while clearing the old transaction
contexts, so persistence tests exercise reopened disk state.
🪄 Autofix (Beta)
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: defaults
Review profile: CHILL
Plan: Pro
Run ID: 9edf0b40-f213-4658-b878-17634348f43a
📒 Files selected for processing (51)
src/Makefile.amsrc/Makefile.test.includesrc/active/context.cppsrc/active/context.hsrc/active/dkgsessionhandler.cppsrc/dbwrapper.hsrc/evo/assetlocktx.cppsrc/evo/assetlocktx.hsrc/evo/chainhelper.cppsrc/evo/chainhelper.hsrc/evo/creditpool.cppsrc/evo/deterministicmns.cppsrc/evo/deterministicmns.hsrc/evo/evodb.cppsrc/evo/evodb.hsrc/evo/mnhftx.cppsrc/evo/mnhftx.hsrc/evo/smldiff.cppsrc/evo/smldiff.hsrc/evo/specialtxman.cppsrc/evo/specialtxman.hsrc/interfaces/chain.hsrc/kernel/chain.cppsrc/kernel/chain.hsrc/llmq/blockprocessor.cppsrc/llmq/blockprocessor.hsrc/llmq/context.cppsrc/llmq/quorumsman.cppsrc/llmq/quorumsman.hsrc/llmq/signing_shares.cppsrc/llmq/signing_shares.hsrc/llmq/snapshot.cppsrc/net_processing.cppsrc/node/chainstate.cppsrc/node/interfaces.cppsrc/node/miner.cppsrc/rpc/blockchain.cppsrc/rpc/masternode.cppsrc/rpc/quorums.cppsrc/test/evo_db_tests.cppsrc/test/evo_deterministicmns_tests.cppsrc/test/util/setup_common.cppsrc/test/util/setup_common.hsrc/test/validation_chainstatemanager_tests.cppsrc/validation.cppsrc/validation.hsrc/versionbits.hsrc/wallet/test/fuzz/notifications.cppsrc/wallet/wallet.cppsrc/wallet/wallet.htest/lint/lint-circular-dependencies.py
| "kernel/mempool_persist -> validation -> kernel/mempool_persist", | ||
| # Dash | ||
| "banman -> common/bloom -> evo/assetlocktx -> llmq/quorumsman -> llmq/blockprocessor -> net -> banman", | ||
| "blockfilter -> evo/specialtx_filter -> evo/providertx -> validation -> kernel/chain -> interfaces/chain.h -> blockfilter", |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift
Break the new interface-layer cycle instead of allowlisting it.
The cycle routes implementation dependencies back through interfaces/chain.h, weakening the isolation boundary. Move the shared chain metadata contract to a lower-level header or replace the concrete dependency, then remove this exception.
As per coding guidelines, “Use interfaces in src/interfaces/ for codebase isolation and inter-process communication.” <coding_guidelines>
🤖 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 `@test/lint/lint-circular-dependencies.py` at line 27, Remove the allowlisted
cycle entry involving blockfilter and interfaces/chain.h from the
circular-dependency test. Refactor the dependency path through validation,
kernel/chain, and interfaces/chain.h so shared chain metadata comes from a
lower-level header or an interface rather than the concrete interface-layer
dependency, preserving the intended isolation boundary.
Source: Coding guidelines
6a803ac to
014afb4
Compare
8ff75e0 llmq: cover QBSIGSHARES aggregate sig-share bound with a testable decoder (PastaClaw) e51e304 llmq: bound LLMQ signing vector intake (PastaClaw) c0af156 llmq: bound batched sig-share intake (PastaClaw) Pull request description: Depends on dashpay#7439. Please review only the two command-specific commits here. ## Issue being fixed or feature implemented LLMQ signing P2P messages previously deserialized peer-controlled vectors before enforcing the existing per-message count limits. This hardens the signing message intake path so oversized counts are rejected before vector materialization. This intentionally does not include the QFCOMMITMENT dynamic-bitset fix. ## What was done? - Use the shared runtime-bounded vector reader for signing message batches in `NetSigning::ProcessMessage`. - Bound `CBatchedSigShares::sigShares` declaratively with `LIMITED_VECTOR`. - Retain explicit running-total accounting for QBSIGSHARES, where many individually valid batches could exceed the aggregate cap. - Ban peers on malformed or oversized signing vectors. - Add unit coverage for the exact boundary and oversized batched sig-share vectors. The reusable bounded-vector serialization primitives are introduced separately in dashpay#7439. ## How Has This Been Tested? - `src/test/test_dash --run_test=llmq_utils_tests` - `src/test/test_dash --run_test=serialize_tests` - `test/lint/lint-whitespace.py` - `test/lint/lint-circular-dependencies.py` ## Breaking Changes None. ## Checklist: - [x] I have performed a self-review of my own code - [x] I have commented my code, particularly in hard-to-understand areas - [x] I have added or updated relevant unit/integration/functional/e2e tests - [ ] I have made corresponding changes to the documentation - [ ] I have assigned this pull request to a milestone _(for repository code-owners and collaborators only) ACKs for top commit: UdjinM6: utACK 8ff75e0 Tree-SHA512: 873fb78ef55749449c0548828dc8709bf0709637732a000bd009d8bba9ed0270e1582eb0020f852c3a8e8bb6d448d1f7b8c0402e271ac42155081a9d13d25bc8
014afb4 to
06b5c6f
Compare
Workaround implementation stays on place to reduce further conflicts
… tests of deterministicmns
… tests of block-reward-reallocation
g_txindex has own tests and no need in general environment for regression tests
754ffa6 ci: keep conflict handler tests out of lint (PastaClaw) e86bacf ci: move conflict comment state after advisory (PastaClaw) Pull request description: ## Issue being fixed or feature implemented Conflict advisory comments begin with a hidden HTML block containing base64-encoded state. GitHub hides this block in the web UI, but Slack's raw comment preview shows it before the useful conflict information. ## What was done? - Move the hidden state block to the end of the comment so notification previews begin with the human-readable advisory. - Continue parsing existing comments that use the legacy marker-first layout. - Run the focused conflict-handler tests from the PR-head lint job. ## How Has This Been Tested? - `python3 .github/workflows/test_handle_potential_conflicts.py` (15 tests) - `python3 -m py_compile .github/workflows/handle_potential_conflicts.py .github/workflows/test_handle_potential_conflicts.py` - `test/lint/lint-python.py` - `test/lint/lint-whitespace.py` - YAML parse of `.github/workflows/lint.yml` - `git diff --check upstream/develop...HEAD` ## Breaking Changes None. ## Checklist: - [x] I have performed a self-review of my own code - [x] I have commented my code, particularly in hard-to-understand areas - [x] I have added or updated relevant unit/integration/functional/e2e tests - [ ] I have made corresponding changes to the documentation - [ ] I have assigned this pull request to a milestone ACKs for top commit: PastaPastaPasta: utACK 754ffa6 knst: utACK 754ffa6 Tree-SHA512: ce9b8b609fe6507caba70ddca0f52911eea9038b70b2a800f69517538261aa3cebf67e08fc11a121f46d82db183ea96b489756b13129e5d0e479767d53e391de
…round BIP30/BIP34 36a5509 test: remove unused scan-pre-bip34-coinbases.py once fix is merged (Konstantin Akimov) 69dd635 refactor: remove Bitcoin's specific hashes for BIP30/BIP34 (Konstantin Akimov) e207edc test: script to scan pre-bip34 coinbases to find potential issues for futher blocks (Konstantin Akimov) e6d6f07 feat: remove bip30 field from coinstat index (Konstantin Akimov) Pull request description: ## Issue being fixed or feature implemented Bitcoin Core has workarounds for BIP30/BIP34 historical blocks; this workaround is irrelevant for Dash Core. ## What was done? Removed Bitcoin Core exceptions from validation for Dash Core. Removed bip30 counter from RPC output `gettxoutsetinfo` which is always 0 for dash core. Though, we can't fully remove checks from validation.cpp same as it is done in PIVX (PIVX-Project#1775) because we had 951 blocks at mainnet and 76 blocks on testnet before BIP34 has been activated and this check is still used for validation of old blocks. ## How Has This Been Tested? Run `scan-pre-bip34-coinbases.py` to find a blocks that violates bip34 safety measures before bip34 is activated for both main and testnet. $ ./scan-pre-bip34-coinbases.py --network=test --datadir=/DASH/.dashcore Scanning test heights 1..75 (BIP34Height=76) @ http://127.0.0.1:19998/ Scanned 75 blocks (heights 1..75, BIP34Height=76). 0 candidate future-collision targets (indicated height > BIP34Height). 1 scriptSigs not minimally encoding any integer. These are safe ONLY IF the decoder above is exhaustive -- please eyeball: h= 16 scriptSig=0110062f503253482f042c51615308f800000402000000102f7374726174756d2d7365727665722f RESULT: No pre-BIP34 coinbase has a leading minimal CScriptNum > BIP34Height. Assuming the decoder is correct (verify against the unparseable list), BIP30 enforcement is unreachable on this chain. $ ./scan-pre-bip34-coinbases.py --network=main --datadir=/DASH/.dashcore Scanning main heights 1..950 (BIP34Height=951) @ http://127.0.0.1:9998/ Scanned 950 blocks (heights 1..950, BIP34Height=951). 0 candidate future-collision targets (indicated height > BIP34Height). 0 scriptSigs not minimally encoding any integer. These are safe ONLY IF the decoder above is exhaustive -- please eyeball: RESULT: No pre-BIP34 coinbase has a leading minimal CScriptNum > BIP34Height. Assuming the decoder is correct (verify against the unparseable list), BIP30 enforcement is unreachable on this chain. Extra double check is call of `gettxoutsetinfo none` to be sure that `bip30` is 0 for main and testnets: $ gettxoutsetinfo none { "height": 2476080, "bestblock": "0000000000000010ee3a3763bed9f1b5ec25e08d0594749a968636b714d0924b", "txouts": 4514705, "bogosize": 339509165, "total_amount": 12710996.18871387, "total_unspendable_amount": -740940.11336565, "block_info": { "prevout_spent": 6.93831412, "coinbase": 1.27251741, "new_outputs_ex_coinbase": 6.93814597, "unspendable": 0.49787579, "unspendables": { "genesis_block": 0.00000000, "bip30": 0.00000000, "scripts": 0.49787579, "unclaimed_rewards": 0.00000000 } } } ## Breaking Changes Removed rpc field `bip30` from RPC `gettxoutsetinfo` ## Checklist: - [x] 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 - [x] I have assigned this pull request to a milestone Top commit has no ACKs. Tree-SHA512: 966a8e314195e9d9581b886313cb38aac35ed9f3eb3ff61eb132352c1504b3de522a1d389cf008df5b261f7e280cd1b410d2b27e1661d1e96c5679cabe5cfbb7
… optional step 198dc72 feat: remove g_txindex and govman from TestChainSetup as optional step (Konstantin Akimov) 901d908 refactor: remove GetTransaction and g_txindex usages from regressions tests of block-reward-reallocation (Konstantin Akimov) c9ddd4b refactor: remove GetTransaction and g_txindex usages from regressions tests of deterministicmns (Konstantin Akimov) Pull request description: ## Issue being fixed or feature implemented g_txindex has own tests and there's no needs in tx-index in the common environment for regression tests This PR is a blocker for dashpay#7462 - particularly bitcoin#25494 ## What was done? Removed usages of g_txindex by calling `GetTransaction` from block_reward_reallocation_tests.cpp, evo_deterministicmns_tests.cpp Simplified and removed duplicated code from `evo_deterministicmns_tests.cpp` ## How Has This Been Tested? Run unit & functional tests. ## Breaking Changes n/a ## Checklist: - [x] 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 - [x] I have assigned this pull request to a milestone _(for repository code-owners and collaborators only)_ Top commit has no ACKs. Tree-SHA512: a50e2a3ab85eef3a343e854f425174d6d4d0be840beb204893852f14b5cef7ad7e77c7024a16270ba7f6a4cb49a159b9e47c0320600efb583b52287b8312388b
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.
06b5c6f to
1a95697
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (2)
src/test/validation_chainstatemanager_tests.cpp (1)
785-807: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winRe-verify: does
SimulateNodeRestart()'s default flush restore the marker this test just erased?This test erases and fully commits the SNAPSHOT EvoDB marker (789-795), then calls
this->SimulateNodeRestart();with the defaultflush_chainstates=true, which flushes every chainstate — including the snapshot chainstate whose marker was just erased — beforeDetectSnapshotChainstateis checked. A sibling test (chainstatemanager_evodb_snapshot_only_flush_restart, 748-759) deliberately usesSimulateNodeRestart(/*flush_chainstates=*/false)to avoid exactly this kind of marker rewrite. This is the same scenario a past review flagged as defeating the missing-marker test via an unwanted flush; the resolution is marked "Addressed" but the visible code here still uses the defaulttrue, so it's worth confirmingChainstate::ForceFlushStateToDisk()doesn't unconditionally re-derive/re-write the EvoDB best-block marker (if it does, this test would pass for the wrong reason, or could become flaky).#!/bin/bash # Inspect ForceFlushStateToDisk to see whether it unconditionally writes the EvoDB best-block marker. rg -n -B3 -A25 'void Chainstate::ForceFlushStateToDisk' src/validation.cpp rg -n -B3 -A5 'WriteBestBlock' src/validation.cpp🤖 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/test/validation_chainstatemanager_tests.cpp` around lines 785 - 807, Use SimulateNodeRestart(/*flush_chainstates=*/false) in chainstatemanager_snapshot_init_missing_evodb_marker so the erased and committed SNAPSHOT EvoDB marker remains absent when DetectSnapshotChainstate runs; keep the rest of the test unchanged.src/validation.cpp (1)
3564-3573: 🎯 Functional Correctness | 🟠 MajorUI tip notification still not gated to the active chainstate.
GetMainSignals().SynchronousUpdatedBlockTip/UpdatedBlockTipare now correctly restricted to the active chainstate (Lines 3564-3567), but the adjacentuiInterface.NotifyBlockTip(...)a few lines below (3571-3573) is gated only bypindex_was_in_chain, not bythis == &m_chainman.ActiveChainstate(). The same gap exists inMarkConflictingBlockat Lines 3669-3677. Every other refactored call site in this file (ConnectBlock,DisconnectBlock,ActivateBestChain) gates the ValidationInterface signals and the UI notification together in a single active-chainstate check — these two sites are the outliers. A background/snapshot-validation chainstate invalidating/marking-conflicting a block on its own chain would still surface a UI tip update for a chain that isn't the active one.This was flagged in a prior review round and marked addressed, but the current code still shows the unfixed pattern.
🐛 Suggested fix (apply analogously at both sites)
InvalidChainFound(to_mark_failed); if (this == &m_chainman.ActiveChainstate()) { GetMainSignals().SynchronousUpdatedBlockTip(m_chain.Tip(), nullptr, IsInitialBlockDownload()); GetMainSignals().UpdatedBlockTip(m_chain.Tip(), nullptr, IsInitialBlockDownload()); + + // Only notify about a new block tip if the active chain was modified. + if (pindex_was_in_chain) { + uiInterface.NotifyBlockTip(GetSynchronizationState(IsInitialBlockDownload()), to_mark_failed->pprev); + } } } - - // Only notify about a new block tip if the active chain was modified. - if (pindex_was_in_chain) { - uiInterface.NotifyBlockTip(GetSynchronizationState(IsInitialBlockDownload()), to_mark_failed->pprev); - } return true;🤖 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 3564 - 3573, Gate the uiInterface.NotifyBlockTip call in both the shown block-tip handling path and MarkConflictingBlock with the same this == &m_chainman.ActiveChainstate() condition used for the corresponding ValidationInterface signals, while preserving the existing pindex_was_in_chain check.
🧹 Nitpick comments (6)
src/evo/evodb.cpp (1)
96-123: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winNon-NORMAL best-block key hardcodes
uint8_t{1}regardless of which identity it is.
ReadBestBlock/WriteBestBlockroute every identity other thanNORMALto the same literal key(EVODB_BEST_BLOCK, uint8_t{1}). Since every identity'sroot_transactionis layered over the same underlyingCDBWrapper(db), this only avoids collisions today because exactly one non-NORMALidentity (SNAPSHOT) exists. If a third identity is introduced (the PR notes marker work continues into M3), two non-NORMALidentities would silently overwrite each other's best-block marker on the shared disk namespace.Deriving the suffix from the identity itself removes the landmine cheaply:
♻️ Suggested fix
- return transaction.Read(std::make_pair(EVODB_BEST_BLOCK, uint8_t{1}), hash); + return transaction.Read(std::make_pair(EVODB_BEST_BLOCK, static_cast<uint8_t>(identity)), hash);and symmetrically in
WriteBestBlock.🤖 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 96 - 123, Update ReadBestBlock and WriteBestBlock so the non-NORMAL best-block key derives its suffix from the provided EvoDbIdentity instead of hardcoding uint8_t{1}. Preserve the existing EVODB_BEST_BLOCK key for NORMAL and ensure distinct non-NORMAL identities use distinct keys in the shared database namespace.src/validation.h (1)
542-542: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider marking the method as
const.If
EvoDbInconsistencyMessagedoes not modify the state of the object, it is best practice to mark it asconst.♻️ Proposed refactor
- std::string EvoDbInconsistencyMessage(); + std::string EvoDbInconsistencyMessage() const;🤖 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.h` at line 542, Update EvoDbInconsistencyMessage to be const-qualified if its implementation does not modify object state, and ensure the corresponding definition matches the declaration.src/test/block_reward_reallocation_tests.cpp (2)
54-54: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueFix the typo in the parameter name.
The parameter name is misspelled as
utoxs. Consider renaming it toutxos(and updating its usages within the methods) for clarity and consistency.
src/test/block_reward_reallocation_tests.cpp#L54-L54: renameutoxstoutxos.src/test/block_reward_reallocation_tests.cpp#L84-L84: renameutoxstoutxos.🤖 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/test/block_reward_reallocation_tests.cpp` at line 54, Rename the SelectUTXOs parameter from utoxs to utxos and update all references within the function. Apply the same spelling correction at src/test/block_reward_reallocation_tests.cpp lines 54-54 and 84-84.
86-86: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueInitialize the
changevariable.Although
SelectUTXOswrites a value to this reference internally, explicitly initializing the variable at declaration improves safety and readability.♻️ Proposed refactor
- CAmount change; + CAmount change = 0;🤖 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/test/block_reward_reallocation_tests.cpp` at line 86, Initialize the CAmount variable change at its declaration before passing it to SelectUTXOs, while preserving the existing reference usage and value assignment flow.src/llmq/net_signing.cpp (1)
67-130: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExtract the repeated log/ban/rethrow pattern.
The try/catch block (log rejection,
BanNode,throw;) is duplicated across QSIGSHARE, QSIGSESANN, QSIGSHARESINV/QGETSIGSHARES, and QBSIGSHARES. A small templated helper would remove the duplication and keep future limit/message-type changes in one place.♻️ Suggested helper
template <typename T> static bool TryUnserializeCapped(CDataStream& vRecv, std::vector<T>& msgs, size_t max_size, std::string_view msg_type, NodeId peer_id, PeerManager& peer_manager, std::function<void(NodeId)> ban_node) { try { if (!UnserializeVectorWithMaxSize(vRecv, msgs, max_size)) { throw std::ios_base::failure(strprintf("%s vector size too large", msg_type)); } return true; } catch (const std::ios_base::failure& e) { LogPrint(BCLog::LLMQ_SIGS, "NetSigning::%s -- rejected %s from peer=%d: %s\n", __func__, msg_type, peer_id, e.what()); ban_node(peer_id); throw; } }🤖 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/net_signing.cpp` around lines 67 - 130, Extract the duplicated deserialization error handling from NetSigning’s message branches into a reusable templated helper near the relevant implementation, covering capped vector deserialization, rejection logging, peer banning, and exception rethrowing. Update the QSIGSHARE, QSIGSESANN, QSIGSHARESINV/QGETSIGSHARES, and QBSIGSHARES paths to use the helper while preserving each branch’s existing size limit and deserialization routine.src/test/evo_deterministicmns_tests.cpp (1)
149-173: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
CreateProRegTxExternalCollateralfunds a redundant collateral-sized output.Since the collateral is supplied externally via
collateralOutpoint, this helper doesn't need to fund anotherdmn_types::Regular.collat_amountoutput — the other update-tx helpers in this file fund with a nominal1 * COINfor exactly this reason. Creating a second collateral-value output here is at best confusing and at worst a latent trap for any future caller that runsGetCollateralOutpointon this function's output expecting it to identify a real collateral.♻️ Suggested funding-amount fix
- const auto spent = FundTransaction(chainman, tx, utxos, scriptPayout, dmn_types::Regular.collat_amount); + const auto spent = FundTransaction(chainman, tx, utxos, scriptPayout, 1 * COIN);🤖 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/test/evo_deterministicmns_tests.cpp` around lines 149 - 173, Update CreateProRegTxExternalCollateral so FundTransaction uses the nominal 1 * COIN amount instead of dmn_types::Regular.collat_amount, while preserving the externally supplied collateralOutpoint as the actual collateral reference.
🤖 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/active/context.cpp`:
- Around line 98-109: Move the nodeman->UpdatedBlockTip call in
CActiveMasternodeManager::UpdatedBlockTip below the
IsSnapshotActiveAndUnvalidated guard, so it is skipped while snapshot validation
is active. Preserve the existing m_snapshot_duty_blocked logging and state
transitions, and invoke UpdatedBlockTip only after validation has completed.
In `@src/test/evo_deterministicmns_tests.cpp`:
- Around line 1454-1471: Update
deterministic_mn_list_diff_serialization_is_canonical to test a serialization
behavior that specifically requires canonical key ordering, rather than only
comparing different insertion orders of unordered_map entries. Add a scenario
whose serialized output differs when key sorting is removed, and assert the
canonical result against an explicitly constructed expected ordering or byte
sequence.
---
Duplicate comments:
In `@src/test/validation_chainstatemanager_tests.cpp`:
- Around line 785-807: Use SimulateNodeRestart(/*flush_chainstates=*/false) in
chainstatemanager_snapshot_init_missing_evodb_marker so the erased and committed
SNAPSHOT EvoDB marker remains absent when DetectSnapshotChainstate runs; keep
the rest of the test unchanged.
In `@src/validation.cpp`:
- Around line 3564-3573: Gate the uiInterface.NotifyBlockTip call in both the
shown block-tip handling path and MarkConflictingBlock with the same this ==
&m_chainman.ActiveChainstate() condition used for the corresponding
ValidationInterface signals, while preserving the existing pindex_was_in_chain
check.
---
Nitpick comments:
In `@src/evo/evodb.cpp`:
- Around line 96-123: Update ReadBestBlock and WriteBestBlock so the non-NORMAL
best-block key derives its suffix from the provided EvoDbIdentity instead of
hardcoding uint8_t{1}. Preserve the existing EVODB_BEST_BLOCK key for NORMAL and
ensure distinct non-NORMAL identities use distinct keys in the shared database
namespace.
In `@src/llmq/net_signing.cpp`:
- Around line 67-130: Extract the duplicated deserialization error handling from
NetSigning’s message branches into a reusable templated helper near the relevant
implementation, covering capped vector deserialization, rejection logging, peer
banning, and exception rethrowing. Update the QSIGSHARE, QSIGSESANN,
QSIGSHARESINV/QGETSIGSHARES, and QBSIGSHARES paths to use the helper while
preserving each branch’s existing size limit and deserialization routine.
In `@src/test/block_reward_reallocation_tests.cpp`:
- Line 54: Rename the SelectUTXOs parameter from utoxs to utxos and update all
references within the function. Apply the same spelling correction at
src/test/block_reward_reallocation_tests.cpp lines 54-54 and 84-84.
- Line 86: Initialize the CAmount variable change at its declaration before
passing it to SelectUTXOs, while preserving the existing reference usage and
value assignment flow.
In `@src/test/evo_deterministicmns_tests.cpp`:
- Around line 149-173: Update CreateProRegTxExternalCollateral so
FundTransaction uses the nominal 1 * COIN amount instead of
dmn_types::Regular.collat_amount, while preserving the externally supplied
collateralOutpoint as the actual collateral reference.
In `@src/validation.h`:
- Line 542: Update EvoDbInconsistencyMessage to be const-qualified if its
implementation does not modify object state, and ensure the corresponding
definition matches the declaration.
🪄 Autofix (Beta)
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: defaults
Review profile: CHILL
Plan: Pro
Run ID: 286d0ead-f9b0-42f1-8d60-585ac42c9b99
📒 Files selected for processing (52)
.github/workflows/handle_potential_conflicts.py.github/workflows/test_handle_potential_conflicts.pysrc/Makefile.test.includesrc/active/context.cppsrc/active/context.hsrc/active/dkgsessionhandler.cppsrc/dbwrapper.hsrc/evo/assetlocktx.cppsrc/evo/assetlocktx.hsrc/evo/chainhelper.cppsrc/evo/chainhelper.hsrc/evo/creditpool.cppsrc/evo/deterministicmns.cppsrc/evo/deterministicmns.hsrc/evo/evodb.cppsrc/evo/evodb.hsrc/evo/mnhftx.cppsrc/evo/mnhftx.hsrc/evo/smldiff.cppsrc/evo/smldiff.hsrc/evo/specialtxman.cppsrc/evo/specialtxman.hsrc/index/coinstatsindex.cppsrc/index/coinstatsindex.hsrc/llmq/blockprocessor.cppsrc/llmq/blockprocessor.hsrc/llmq/context.cppsrc/llmq/net_signing.cppsrc/llmq/net_signing.hsrc/llmq/quorumsman.cppsrc/llmq/quorumsman.hsrc/llmq/signing_shares.cppsrc/llmq/signing_shares.hsrc/llmq/snapshot.cppsrc/net_processing.cppsrc/node/chainstate.cppsrc/node/miner.cppsrc/rpc/blockchain.cppsrc/rpc/masternode.cppsrc/rpc/quorums.cppsrc/test/block_reward_reallocation_tests.cppsrc/test/evo_db_tests.cppsrc/test/evo_deterministicmns_tests.cppsrc/test/governance_inv_tests.cppsrc/test/llmq_utils_tests.cppsrc/test/util/setup_common.cppsrc/test/util/setup_common.hsrc/test/validation_chainstatemanager_tests.cppsrc/validation.cppsrc/validation.hsrc/versionbits.htest/functional/feature_coinstatsindex.py
💤 Files with no reviewable changes (2)
- src/index/coinstatsindex.cpp
- test/functional/feature_coinstatsindex.py
🚧 Files skipped from review as they are similar to previous changes (27)
- src/llmq/context.cpp
- src/evo/chainhelper.cpp
- src/evo/deterministicmns.h
- src/evo/assetlocktx.h
- src/versionbits.h
- src/node/miner.cpp
- src/evo/creditpool.cpp
- src/rpc/quorums.cpp
- src/active/dkgsessionhandler.cpp
- src/node/chainstate.cpp
- src/rpc/masternode.cpp
- src/active/context.h
- src/evo/smldiff.cpp
- src/evo/mnhftx.h
- src/evo/smldiff.h
- src/llmq/signing_shares.cpp
- src/net_processing.cpp
- src/evo/specialtxman.h
- src/evo/assetlocktx.cpp
- src/llmq/quorumsman.h
- src/evo/mnhftx.cpp
- src/Makefile.test.include
- src/test/util/setup_common.h
- src/llmq/quorumsman.cpp
- src/llmq/blockprocessor.h
- src/evo/specialtxman.cpp
- src/evo/evodb.h
| nodeman->UpdatedBlockTip(pindexNew, pindexFork, fInitialDownload); | ||
|
|
||
| if (m_chainman.IsSnapshotActiveAndUnvalidated()) { | ||
| if (!m_snapshot_duty_blocked.exchange(true)) { | ||
| LogPrintf("Masternode DKG participation and quorum signing are disabled until snapshot background validation completes\n"); | ||
| } | ||
| return; | ||
| } | ||
| if (m_snapshot_duty_blocked.exchange(false)) { | ||
| LogPrintf("Snapshot background validation completed; masternode DKG participation and quorum signing are enabled\n"); | ||
| } | ||
|
|
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
ast-grep outline src/active/masternode.cpp --items all --match 'UpdatedBlockTip' 2>/dev/null || \
fd -i masternode src/active --type fRepository: PastaPastaPasta/dash
Length of output: 320
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== src/active/masternode.cpp outline =="
ast-grep outline src/active/masternode.cpp --view expanded || true
echo
echo "== UpdatedBlockTip implementation =="
sed -n '150,240p' src/active/masternode.cpp | cat -n
echo
echo "== UpdatedBlockTip call sites =="
rg -n "UpdatedBlockTip\(" src | cat -nRepository: PastaPastaPasta/dash
Length of output: 18878
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '175,260p' src/active/masternode.cpp | cat -nRepository: PastaPastaPasta/dash
Length of output: 4476
🏁 Script executed:
#!/bin/bash
set -euo pipefail
grep -n "void CActiveMasternodeManager::UpdatedBlockTip" -A80 -B20 src/active/masternode.cppRepository: PastaPastaPasta/dash
Length of output: 4919
🌐 Web query:
"src/active/masternode.cpp CActiveMasternodeManager::UpdatedBlockTip Dash"
💡 Result:
In the Dash Core codebase, the function CActiveMasternodeManager::UpdatedBlockTip is a method responsible for handling notifications when the blockchain tip is updated [1]. It is part of the CActiveMasternodeManager class, which manages the operational state of a node acting as a masternode [1]. This method is inherited from CValidationInterface and serves as a listener that reacts to changes in the blockchain chainstate [1]. When a new block tip is processed, UpdatedBlockTip is triggered to verify and maintain the masternode's status, ensuring that the local node's information remains synchronized with the deterministic masternode list [2]. Key responsibilities of this method include: 1. Verification of Masternode Status: It checks if the masternode is still valid according to the deterministic masternode (DIP3) rules [2]. 2. State Transition Management: If the masternode information (such as the ProTx hash, operator key, or IP address) changes or if the masternode is removed from the list, the method updates the local active masternode state accordingly [2]. 3. Synchronization: It helps the local node react to events like operator key changes or network address updates, triggering a reset or update of the local masternode configuration when necessary [2]. While historically located in src/masternode/activemasternode.cpp, the CActiveMasternodeManager and related logic have been subject to architectural refactors, such as moving components into the src/active/ directory to support improved context management within the Dash Core node [3][4].
Citations:
- 1: https://dash-docs.github.io/en/doxygen/html/class_c_active_masternode_manager.html
- 2: https://dash-docs.github.io/en/doxygen/html/activemasternode_8cpp_source.html
- 3: refactor: extract
CActiveMasternodeManagerfromLLMQContext(3/n, DKG session isolation,ActiveContextconsolidation) dashpay/dash#7065 - 4: e9f8294
🌐 Web query:
"\"CActiveMasternodeManager::UpdatedBlockTip\" dash github"
💡 Result:
CActiveMasternodeManager::UpdatedBlockTip is a virtual method within the Dash Core codebase, defined in the CActiveMasternodeManager class [1][2]. It is a component of the node's validation interface, specifically implemented to handle notifications when the block chain tip is updated [1][2]. Key characteristics and functionality include: Implementation and Role: The method is an implementation of the CValidationInterface::UpdatedBlockTip interface [1]. It is called on a background thread whenever a new block tip is processed, allowing the active masternode manager to respond to changes in the blockchain [1]. Core Logic: When triggered, the method performs several checks to manage the state of the active masternode [3]: - It verifies if DIP3 (Deterministic Masternode List) enforcement is active for the new block height [3]. - It compares the masternode list before and after the block update to detect changes such as the masternode being removed from the list, changes to the operator key, or changes to the masternode's IP address [3]. - Depending on these checks, it updates the internal state of the active masternode (e.g., setting the state to MASTERNODE_REMOVED, MASTERNODE_OPERATOR_KEY_CHANGED, or MASTERNODE_PROTX_IP_CHANGED) and resets relevant identity information if necessary [3]. Technical Context: The method is defined in src/masternode/activemasternode.cpp and declared in src/masternode/activemasternode.h [3][2]. It is critical for ensuring that an active masternode remains synchronized and compliant with the network's deterministic masternode list as the chain progresses [3]. It is often part of broader architectural components, such as those involved in Distributed Key Generation (DKG) session management and masternode synchronization [4][5].
Citations:
- 1: https://dash-docs.github.io/en/doxygen/html/class_c_active_masternode_manager.html
- 2: https://dash-docs.github.io/en/doxygen/html/activemasternode_8h_source.html
- 3: https://dash-docs.github.io/en/doxygen/html/activemasternode_8cpp_source.html
- 4: refactor: extract
CActiveMasternodeManagerfromLLMQContext(3/n, DKG session isolation,ActiveContextconsolidation) dashpay/dash#7065 - 5: https://dash-docs.github.io/en/doxygen/html/masternode-sync_8cpp_source.html
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== m_state and GetStateString uses =="
rg -n "\bm_state\b|GetStateString\(" src/active src/evo src/llmq src/governance src/masternode src/test | cat -n
echo
echo "== InitInternal / Init in src/active/masternode.cpp =="
sed -n '112,175p' src/active/masternode.cpp | cat -nRepository: PastaPastaPasta/dash
Length of output: 11450
Gate nodeman->UpdatedBlockTip under the snapshot check
CActiveMasternodeManager::UpdatedBlockTip reads the DML for pindexNew/pindexNew->pprev and mutates m_state/m_protx_hash, so it depends on chain data from the current tip. Skip it until snapshot background validation completes, just like the other masternode duties.
🤖 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/active/context.cpp` around lines 98 - 109, Move the
nodeman->UpdatedBlockTip call in CActiveMasternodeManager::UpdatedBlockTip below
the IsSnapshotActiveAndUnvalidated guard, so it is skipped while snapshot
validation is active. Preserve the existing m_snapshot_duty_blocked logging and
state transitions, and invoke UpdatedBlockTip only after validation has
completed.
| BOOST_AUTO_TEST_CASE(deterministic_mn_list_diff_serialization_is_canonical) | ||
| { | ||
| CDeterministicMNListDiff forward; | ||
| forward.updatedMNs.emplace(1, CDeterministicMNStateDiff{}); | ||
| forward.updatedMNs.emplace(2, CDeterministicMNStateDiff{}); | ||
|
|
||
| CDeterministicMNListDiff reverse; | ||
| reverse.updatedMNs.emplace(2, CDeterministicMNStateDiff{}); | ||
| reverse.updatedMNs.emplace(1, CDeterministicMNStateDiff{}); | ||
|
|
||
| CDataStream forward_stream{SER_DISK, CLIENT_VERSION}; | ||
| CDataStream reverse_stream{SER_DISK, CLIENT_VERSION}; | ||
| forward_stream << forward; | ||
| reverse_stream << reverse; | ||
|
|
||
| BOOST_CHECK_EQUAL_COLLECTIONS(forward_stream.begin(), forward_stream.end(), | ||
| reverse_stream.begin(), reverse_stream.end()); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Inspect the declared type of updatedMNs and its Serialize/Unserialize logic.
rg -n 'updatedMNs' src/evo/deterministicmns.hRepository: PastaPastaPasta/dash
Length of output: 1008
Use a test that actually depends on canonicalization
updatedMNs is an std::unordered_map, and serialization already sorts the keys before writing them, so forward vs. reverse insertion produces identical bytes even without this change. Add a case that would fail if that ordering were removed.
🤖 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/test/evo_deterministicmns_tests.cpp` around lines 1454 - 1471, Update
deterministic_mn_list_diff_serialization_is_canonical to test a serialization
behavior that specifically requires canonical key ordering, rather than only
comparing different insertion orders of unordered_map entries. Add a scenario
whose serialized output differs when key sorting is removed, and assert the
canonical result against an explicitly constructed expected ordering or byte
sequence.
Issue being fixed or feature implemented
M1 (#50) landed snapshot persistence but kept Dash's single-EvoDB
assumption. To support two live chainstates (background/normal +
snapshot) each with independent masternode/quorum/credit-pool state, EvoDB
needs stable per-chainstate identities and transaction overlays, and every
side-effect subsystem (MN list notifications, LLMQ signing, RPC, UI) needs
to bind validation to the calling chainstate. This is milestone 2/7 (design
docs D2/D3). The execution
plan and per-unit ledger (M2 section) are tracked outside the repo in a
private gist, not checked in: https://gist.github.com/PastaPastaPasta/aa52b1f89fb74a0566ba3b5e15afab6a
PR 2/7 in the stacked series — base is
developafter M1 merged.What was done?
CEvoDBwith per-chainstatebest-block markers and flush transactions (Option A from the design doc:
union converges, no merge needed at completion). First implementation was
rejected on round 1 adversarial review (cross-context pending-read
visibility broke crash atomicity, unstable role-keyed markers, undo-erase
cross-chainstate deletion, tombstones defeated by fallback reads); reworked
to own-overlay+disk-only reads, storage-identity-keyed markers/contexts, an
undo-erase guard, and a downgrade flag. Two items were explicitly deferred
to M3: marker promotion to the legacy
b_b4key at completion, and holisticbase-state comparison (both left as TODOs in
evodb.h).by the calling chainstate and snapshot-validation state.
Two review rounds found and fixed a critical seeded-commitment accounting
bug and, more seriously, that MNHF/asset-unlock quorum resolution used
active-chain semantics during background validation — fixed by threading
the validating chainstate through
CQuorumManager::HasQuorum/GetQuorum/ScanQuorums.Masternodes refuse DKG/signing on an unvalidated snapshot chainstate.
CEvoDB::WriteDerivedbyte-comparesre-serialized values, so every block-derived payload type must serialize
canonically. Two latent violations (
CDeterministicMNListDiff::updatedMNsas an unordered_map, MNHF
Signalsas an unordered_map) causedBLOCK_CONSENSUSfailures onreconsiderblockafter restart — fixed bysorting on serialize / switching to
std::map. As a result,feature_mnehf.pyandfeature_asset_locks.pyare now permanent membersof the milestone functional gate from M2 onward.
How Has This Been Tested?
make check: exit 0, 0 failures.feature_mnehf,feature_asset_locks,feature_dip3_deterministicmns×2,feature_llmq_signing×2,feature_llmq_rotation,rpc_quorum— all passed.verification → final adjudication); milestone gate PASS on 2026-07-11.
Breaking Changes
None externally. Internally,
ActivateExistingSnapshotbecomes fallible(returns/propagates failure instead of the upstream reference-return shape) —
noted for future backports that touch this path.
Checklist:
Summary by CodeRabbit
New Features
quorumParticipationto masternode status responses.Bug Fixes
bip30category withgenesis_block.Tests