From d56ba94feb3f1b014e35ad00ab85edb9d5feba1d Mon Sep 17 00:00:00 2001 From: pasta Date: Thu, 13 Aug 2026 14:11:23 -0500 Subject: [PATCH 1/3] feat(evo): expose complete masternode operator-key history --- src/Makefile.am | 1 + src/interfaces/masternode_operator.h | 31 ++++ src/interfaces/node.h | 7 + src/node/interfaces.cpp | 220 +++++++++++++++++++++++- src/test/evo_deterministicmns_tests.cpp | 181 ++++++++++++++++++- test/util/data/non-backported.txt | 1 + 6 files changed, 437 insertions(+), 4 deletions(-) create mode 100644 src/interfaces/masternode_operator.h diff --git a/src/Makefile.am b/src/Makefile.am index fb1ffde2529a..65f2c7424946 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -280,6 +280,7 @@ BITCOIN_CORE_H = \ interfaces/handler.h \ interfaces/init.h \ interfaces/ipc.h \ + interfaces/masternode_operator.h \ interfaces/node.h \ interfaces/wallet.h \ kernel/blockmanager_opts.h \ diff --git a/src/interfaces/masternode_operator.h b/src/interfaces/masternode_operator.h new file mode 100644 index 000000000000..e5b0e5897521 --- /dev/null +++ b/src/interfaces/masternode_operator.h @@ -0,0 +1,31 @@ +// Copyright (c) 2026 The Dash Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_INTERFACES_MASTERNODE_OPERATOR_H +#define BITCOIN_INTERFACES_MASTERNODE_OPERATOR_H + +#include + +#include +#include + +namespace interfaces { + +/** Whether the node could provide a complete active-chain operator-key history. */ +enum class MasternodeOperatorKeyHistoryStatus : uint8_t { + SUCCESS, + HISTORY_UNAVAILABLE, +}; + +/** Operator public keys ever assigned by ProRegTx or ProUpRegTx on the active chain. */ +struct MasternodeOperatorKeyHistory { + MasternodeOperatorKeyHistoryStatus status{MasternodeOperatorKeyHistoryStatus::HISTORY_UNAVAILABLE}; + std::vector> public_keys; + uint256 tip_hash; + int tip_height{-1}; +}; + +} // namespace interfaces + +#endif // BITCOIN_INTERFACES_MASTERNODE_OPERATOR_H diff --git a/src/interfaces/node.h b/src/interfaces/node.h index ba85809416be..e1d708028cdc 100644 --- a/src/interfaces/node.h +++ b/src/interfaces/node.h @@ -6,6 +6,7 @@ #define BITCOIN_INTERFACES_NODE_H #include // For CAmount +#include #include // For NodeId #include // For banmap_t #include // For Network @@ -129,6 +130,12 @@ class EVO public: virtual ~EVO() {} virtual std::pair getListAtChainTip() = 0; + /** + * Return every operator key assigned on the active chain, or HISTORY_UNAVAILABLE without + * partial results. This may perform a long-running block-data scan and must not run while + * holding cs_main. + */ + virtual MasternodeOperatorKeyHistory getMasternodeOperatorKeyHistory() = 0; virtual void setContext(node::NodeContext* context) {} }; diff --git a/src/node/interfaces.cpp b/src/node/interfaces.cpp index f7f91f8a368d..ef086a2b03e3 100644 --- a/src/node/interfaces.cpp +++ b/src/node/interfaces.cpp @@ -11,10 +11,13 @@ #include #include #include +#include #include #include #include #include +#include +#include #include #include #include @@ -23,16 +26,18 @@ #include #include #include +#include #include #include #include #include -#include +#include #include #include #include #include #include +#include #include #include #include @@ -40,7 +45,6 @@ #include #include #include -#include #include #include #include @@ -55,12 +59,14 @@ #include #include #include +#include #include #include #include #include #include #include +#include #include #include #include @@ -80,9 +86,12 @@ #include #include +#include #include #include #include +#include +#include #include #include @@ -94,6 +103,8 @@ using interfaces::GOV; using interfaces::Handler; using interfaces::LLMQ; using interfaces::MakeHandler; +using interfaces::MasternodeOperatorKeyHistory; +using interfaces::MasternodeOperatorKeyHistoryStatus; using interfaces::MnEntry; using interfaces::MnEntryCPtr; using interfaces::MnList; @@ -208,9 +219,85 @@ class MnListImpl : public MnList class EVOImpl : public EVO { private: + using OperatorKeySet = std::set>; + + struct BlockLocation { + const CBlockIndex* index; + FlatFilePos position; + }; + ChainstateManager& chainman() { return *Assert(m_context->chainman); } NodeContext& context() { return *Assert(m_context); } + static bool AddOperatorKey(const CBLSLazyPublicKey& lazy_public_key, OperatorKeySet& keys) + { + const CBLSPublicKey& public_key{lazy_public_key.Get()}; + if (!public_key.IsValid()) return false; + + std::vector canonical{public_key.ToByteVector(/*specificLegacyScheme=*/false)}; + CBLSPublicKey decoded; + decoded.SetBytes(canonical, /*specificLegacyScheme=*/false); + if (!decoded.IsValid() || decoded != public_key) return false; + + keys.emplace(std::move(canonical)); + return true; + } + + static bool ScanOperatorKeys(std::vector& blocks, OperatorKeySet& keys, size_t& transaction_count) + { + AssertLockNotHeld(::cs_main); + std::sort(blocks.begin(), blocks.end(), [](const BlockLocation& lhs, const BlockLocation& rhs) { + return std::tie(lhs.position.nFile, lhs.position.nPos) < std::tie(rhs.position.nFile, rhs.position.nPos); + }); + + size_t processed_blocks{0}; + try { + for (size_t first{0}; first < blocks.size();) { + const int file_number{blocks[first].position.nFile}; + size_t last{first + 1}; + while (last < blocks.size() && blocks[last].position.nFile == file_number) + ++last; + + CAutoFile file{OpenBlockFile(FlatFilePos{file_number, 0}, /*fReadOnly=*/true), SER_DISK, CLIENT_VERSION}; + if (file.IsNull()) return false; + + for (size_t i{first}; i < last; ++i) { + if (ShutdownRequested() || std::fseek(file.Get(), blocks[i].position.nPos, SEEK_SET) != 0) { + return false; + } + + CBlock block; + file >> block; + if (block.GetHash() != blocks[i].index->GetBlockHash()) return false; + + bool mutated{false}; + if (BlockMerkleRoot(block, &mutated) != block.hashMerkleRoot || mutated) return false; + + transaction_count += block.vtx.size(); + for (const CTransactionRef& tx : block.vtx) { + if (tx->nType == TRANSACTION_PROVIDER_REGISTER) { + const auto payload{GetTxPayload(*tx)}; + if (!payload || !AddOperatorKey(payload->pubKeyOperator, keys)) return false; + } else if (tx->nType == TRANSACTION_PROVIDER_UPDATE_REGISTRAR) { + const auto payload{GetTxPayload(*tx)}; + if (!payload || !AddOperatorKey(payload->pubKeyOperator, keys)) return false; + } + } + if (++processed_blocks % 100000 == 0) { + LogPrint(BCLog::BENCHMARK, "Masternode operator-key history scan progress: %u/%u blocks\n", + static_cast(processed_blocks), static_cast(blocks.size())); + } + } + first = last; + } + } catch (const std::exception&) { + return false; + } + return true; + } + + static MasternodeOperatorKeyHistory UnavailableHistory() { return {}; } + public: std::pair getListAtChainTip() override { @@ -224,13 +311,140 @@ class EVOImpl : public EVO } return {nullptr, nullptr}; } - void setContext(NodeContext* context) override + + MasternodeOperatorKeyHistory getMasternodeOperatorKeyHistory() override + EXCLUSIVE_LOCKS_REQUIRED(!m_operator_key_history_mutex) + { + AssertLockNotHeld(::cs_main); + LOCK(m_operator_key_history_mutex); + if (ShutdownRequested()) return UnavailableHistory(); + + const int activation_height{chainman().GetConsensus().DIP0003Height}; + const CBlockIndex* working_tip{m_operator_key_history_tip}; + OperatorKeySet working_keys{m_operator_key_history}; + + while (!ShutdownRequested()) { + std::vector blocks; + const CBlockIndex* captured_tip{nullptr}; + int start_height{activation_height}; + bool history_available{true}; + { + LOCK(::cs_main); + if (node::fReindex || node::fImporting || chainman().IsSnapshotActive() || + chainman().ActiveChainstate().IsInitialBlockDownload()) { + return UnavailableHistory(); + } + + const CChain& active_chain{chainman().ActiveChain()}; + captured_tip = active_chain.Tip(); + if (!captured_tip || captured_tip != chainman().m_best_header || + captured_tip->GetBlockTime() < GetTime() - nMaxTipAge) { + return UnavailableHistory(); + } + + if (working_tip && !active_chain.Contains(working_tip)) { + working_tip = nullptr; + working_keys.clear(); + } + if (working_tip) start_height = std::max(activation_height, working_tip->nHeight + 1); + + if (start_height <= captured_tip->nHeight) { + blocks.reserve(captured_tip->nHeight - start_height + 1); + for (int height{start_height}; height <= captured_tip->nHeight; ++height) { + const CBlockIndex* index{active_chain[height]}; + if (!index || !(index->nStatus & BLOCK_HAVE_DATA)) { + history_available = false; + break; + } + const FlatFilePos position{index->GetBlockPos()}; + if (position.IsNull()) { + history_available = false; + break; + } + blocks.push_back({index, position}); + } + } + } + if (!history_available) return UnavailableHistory(); + + const int64_t scan_start{GetTimeMicros()}; + size_t transaction_count{0}; + if (!ScanOperatorKeys(blocks, working_keys, transaction_count)) { + LogPrint(BCLog::BENCHMARK, /* Continued */ + "Masternode operator-key history scan failed after %.2fms (%u blocks, %u transactions)\n", + (GetTimeMicros() - scan_start) * 0.001, static_cast(blocks.size()), + static_cast(transaction_count)); + return UnavailableHistory(); + } + LogPrint(BCLog::BENCHMARK, /* Continued */ + "Masternode operator-key history scan completed in %.2fms (%u blocks, %u transactions, %u keys)\n", + (GetTimeMicros() - scan_start) * 0.001, static_cast(blocks.size()), + static_cast(transaction_count), static_cast(working_keys.size())); + + enum class TipState { + EXACT, + EXTENSION, + FORK, + UNAVAILABLE + }; + TipState tip_state{TipState::UNAVAILABLE}; + { + LOCK(::cs_main); + if (!ShutdownRequested() && !node::fReindex && !node::fImporting && !chainman().IsSnapshotActive() && + !chainman().ActiveChainstate().IsInitialBlockDownload()) { + const CBlockIndex* current_tip{chainman().ActiveChain().Tip()}; + if (current_tip != chainman().m_best_header || !current_tip || + current_tip->GetBlockTime() < GetTime() - nMaxTipAge) { + return UnavailableHistory(); + } + if (current_tip == captured_tip) { + tip_state = TipState::EXACT; + } else if (current_tip && current_tip->nHeight > captured_tip->nHeight && + current_tip->GetAncestor(captured_tip->nHeight) == captured_tip) { + tip_state = TipState::EXTENSION; + } else if (current_tip) { + tip_state = TipState::FORK; + } + } + } + + if (tip_state == TipState::EXACT) { + m_operator_key_history_tip = captured_tip; + m_operator_key_history = working_keys; + std::vector> public_keys{working_keys.begin(), working_keys.end()}; + return { + MasternodeOperatorKeyHistoryStatus::SUCCESS, + std::move(public_keys), + captured_tip->GetBlockHash(), + captured_tip->nHeight, + }; + } + if (tip_state == TipState::UNAVAILABLE) return UnavailableHistory(); + + if (tip_state == TipState::EXTENSION) { + working_tip = captured_tip; + } else { + working_tip = nullptr; + working_keys.clear(); + } + } + return UnavailableHistory(); + } + + void setContext(NodeContext* context) override EXCLUSIVE_LOCKS_REQUIRED(!m_operator_key_history_mutex) { + AssertLockNotHeld(::cs_main); + LOCK(m_operator_key_history_mutex); + m_operator_key_history_tip = nullptr; + m_operator_key_history.clear(); m_context = context; } private: NodeContext* m_context{nullptr}; + Mutex m_operator_key_history_mutex; + const CBlockIndex* m_operator_key_history_tip GUARDED_BY(m_operator_key_history_mutex){nullptr}; + OperatorKeySet m_operator_key_history GUARDED_BY(m_operator_key_history_mutex); }; class GOVImpl : public GOV diff --git a/src/test/evo_deterministicmns_tests.cpp b/src/test/evo_deterministicmns_tests.cpp index 8ca40a2d2909..65a971b52986 100644 --- a/src/test/evo_deterministicmns_tests.cpp +++ b/src/test/evo_deterministicmns_tests.cpp @@ -16,10 +16,12 @@ #include #include #include +#include #include -#include #include #include +#include +#include #include #include #include @@ -35,6 +37,8 @@ #include +#include +#include #include #include #include @@ -1678,6 +1682,181 @@ struct TestMNChainSetup : public TestChainSetup { const CScript coinbase_pk; }; +BOOST_AUTO_TEST_CASE(operator_key_history_is_complete_and_fail_closed) +{ + const std::chrono::seconds previous_mock_time{GetMockTime()}; + struct MockTimeGuard { + std::chrono::seconds previous; + ~MockTimeGuard() { SetMockTime(previous); } + } mock_time_guard{previous_mock_time}; + + TestMNChainSetup setup(DIP3_ACTIVATION_HEIGHT - 2, {"-dip3params=109:500"}); + setup.ProcessBlock(); // The next block may contain DIP3 transactions. + + CKey owner_key; + CBLSSecretKey registered_key; + auto registration{CreateProRegTx(setup.chainman, setup.utxos, 19999, GenerateRandomAddress(), setup.coinbaseKey, + owner_key, registered_key)}; + const uint256 pro_tx_hash{registration.GetHash()}; + setup.ProcessBlock({registration}); + const CBlockIndex* registration_block{setup.Tip()}; + + auto node{interfaces::MakeNode(setup.m_node)}; + auto canonical = [](const CBLSSecretKey& key) { + return key.GetPublicKey().ToByteVector(/*specificLegacyScheme=*/false); + }; + auto contains = [](const interfaces::MasternodeOperatorKeyHistory& history, const std::vector& key) { + return std::ranges::find(history.public_keys, key) != history.public_keys.end(); + }; + auto require_history = [&](const std::vector& expected, + const std::vector& excluded = {}) { + const auto history{node->evo().getMasternodeOperatorKeyHistory()}; + BOOST_REQUIRE(history.status == interfaces::MasternodeOperatorKeyHistoryStatus::SUCCESS); + BOOST_CHECK(history.tip_hash == setup.Tip()->GetBlockHash()); + BOOST_CHECK_EQUAL(history.tip_height, setup.Tip()->nHeight); + BOOST_CHECK_EQUAL(history.public_keys.size(), expected.size()); + for (const CBLSSecretKey* key : expected) + BOOST_CHECK(contains(history, canonical(*key))); + for (const CBLSSecretKey* key : excluded) + BOOST_CHECK(!contains(history, canonical(*key))); + }; + + // A legacy wire key is exposed in canonical basic serialization. + require_history({®istered_key}); + uint32_t registration_status{0}; + { + LOCK(::cs_main); + registration_status = registration_block->nStatus; + const_cast(registration_block)->nStatus &= ~BLOCK_HAVE_DATA; + } + require_history({®istered_key}); // Exact-tip cache hit does not reread historical blocks. + { + LOCK(::cs_main); + const_cast(registration_block)->nStatus = registration_status; + } + + CBlock pending_block{setup.CreateBlock({}, setup.coinbase_pk, setup.chainman.ActiveChainstate())}; + BlockValidationState header_state; + BOOST_REQUIRE(setup.chainman.ProcessNewBlockHeaders({pending_block.GetBlockHeader()}, header_state)); + { + const auto unavailable{node->evo().getMasternodeOperatorKeyHistory()}; + BOOST_CHECK(unavailable.status == interfaces::MasternodeOperatorKeyHistoryStatus::HISTORY_UNAVAILABLE); + BOOST_CHECK(unavailable.public_keys.empty()); + } + BOOST_REQUIRE(setup.chainman.ProcessNewBlock(std::make_shared(pending_block), + /*force_processing=*/true, /*new_block=*/nullptr)); + setup.dmnman.UpdatedBlockTip(setup.Tip()); + require_history({®istered_key}); + + struct FlagGuard { + std::atomic_bool& flag; + const bool previous; + explicit FlagGuard(std::atomic_bool& flag_in) : + flag{flag_in}, + previous{flag.exchange(true)} + { + } + ~FlagGuard() { flag = previous; } + }; + auto check_unavailable = [&] { + const auto unavailable{node->evo().getMasternodeOperatorKeyHistory()}; + BOOST_CHECK(unavailable.status == interfaces::MasternodeOperatorKeyHistoryStatus::HISTORY_UNAVAILABLE); + BOOST_CHECK(unavailable.public_keys.empty()); + }; + { + FlagGuard guard{node::fImporting}; + check_unavailable(); + } + { + FlagGuard guard{node::fReindex}; + check_unavailable(); + } + require_history({®istered_key}); + + SetMockTime(GetTime() + nMaxTipAge + 1); + check_unavailable(); + SetMockTime(setup.Tip()->GetBlockTime() + 1); + require_history({®istered_key}); + + { + LOCK(::cs_main); + const_cast(registration_block)->nStatus &= ~BLOCK_HAVE_DATA; + } + node->setContext(&setup.m_node); + check_unavailable(); // Rebinding even the same context invalidates cached block-index identity. + { + LOCK(::cs_main); + const_cast(registration_block)->nStatus = registration_status; + } + require_history({®istered_key}); + + CBLSSecretKey rotated_key_1, rotated_key_2; + rotated_key_1.MakeNewKey(); + rotated_key_2.MakeNewKey(); + auto rotate_1{CreateProUpRegTx(setup.chainman, setup.utxos, pro_tx_hash, owner_key, rotated_key_1.GetPublicKey(), + owner_key.GetPubKey().GetID(), GenerateRandomAddress(), setup.coinbaseKey, + ProTxVersion::LegacyBLS)}; + auto rotate_2{CreateProUpRegTx(setup.chainman, setup.utxos, pro_tx_hash, owner_key, rotated_key_2.GetPublicKey(), + owner_key.GetPubKey().GetID(), GenerateRandomAddress(), setup.coinbaseKey, + ProTxVersion::LegacyBLS)}; + setup.ProcessBlock({rotate_1, rotate_2}); + + // Both intra-block keys must survive even though the end-of-block deterministic-MN diff only + // describes the final state. + require_history({®istered_key, &rotated_key_1, &rotated_key_2}); + + CBLSSecretKey revoked_key; + revoked_key.MakeNewKey(); + auto rotate_before_revoke{CreateProUpRegTx(setup.chainman, setup.utxos, pro_tx_hash, owner_key, + revoked_key.GetPublicKey(), owner_key.GetPubKey().GetID(), + GenerateRandomAddress(), setup.coinbaseKey, ProTxVersion::LegacyBLS)}; + // Per-transaction validation uses pindexPrev, so the revocation is signed by the key current at + // the start of the block. The registrar update immediately before it must still enter history. + auto revoke{CreateProUpRevTx(setup.chainman, setup.utxos, pro_tx_hash, rotated_key_2, setup.coinbaseKey)}; + setup.ProcessBlock({rotate_before_revoke, revoke}); + require_history({®istered_key, &rotated_key_1, &rotated_key_2, &revoked_key}); + + const SimpleUTXOMap fork_utxos{setup.utxos}; + CBLSSecretKey abandoned_fork_key; + abandoned_fork_key.MakeNewKey(); + auto abandoned_rotation{CreateProUpRegTx(setup.chainman, setup.utxos, pro_tx_hash, owner_key, + abandoned_fork_key.GetPublicKey(), owner_key.GetPubKey().GetID(), + GenerateRandomAddress(), setup.coinbaseKey, ProTxVersion::LegacyBLS)}; + setup.ProcessBlock({abandoned_rotation}); + const CBlockIndex* abandoned_block{setup.Tip()}; + setup.ProcessBlock(); + + uint32_t saved_status{0}; + { + LOCK(::cs_main); + saved_status = abandoned_block->nStatus; + const_cast(abandoned_block)->nStatus &= ~BLOCK_HAVE_DATA; + } + const auto unavailable{node->evo().getMasternodeOperatorKeyHistory()}; + BOOST_CHECK(unavailable.status == interfaces::MasternodeOperatorKeyHistoryStatus::HISTORY_UNAVAILABLE); + BOOST_CHECK(unavailable.public_keys.empty()); + { + LOCK(::cs_main); + const_cast(abandoned_block)->nStatus = saved_status; + } + require_history({®istered_key, &rotated_key_1, &rotated_key_2, &revoked_key, &abandoned_fork_key}); + + BlockValidationState invalidate_state; + BOOST_REQUIRE( + setup.chainman.ActiveChainstate().InvalidateBlock(invalidate_state, const_cast(abandoned_block))); + setup.dmnman.UpdatedBlockTip(setup.Tip()); + setup.utxos = fork_utxos; + + CBLSSecretKey replacement_fork_key; + replacement_fork_key.MakeNewKey(); + auto replacement_rotation{CreateProUpRegTx(setup.chainman, setup.utxos, pro_tx_hash, owner_key, + replacement_fork_key.GetPublicKey(), owner_key.GetPubKey().GetID(), + GenerateRandomAddress(), setup.coinbaseKey, ProTxVersion::LegacyBLS)}; + setup.ProcessBlock({replacement_rotation}); + require_history({®istered_key, &rotated_key_1, &rotated_key_2, &revoked_key, &replacement_fork_key}, + {&abandoned_fork_key}); +} + struct TestChainV24SignalBeforeV19Setup : public TestMNChainSetup { TestChainV24SignalBeforeV19Setup() : TestMNChainSetup(494, diff --git a/test/util/data/non-backported.txt b/test/util/data/non-backported.txt index f759bb01a1e1..8edad27fc43c 100644 --- a/test/util/data/non-backported.txt +++ b/test/util/data/non-backported.txt @@ -25,6 +25,7 @@ src/index/spent*.cpp src/index/spent*.h src/index/timestamp*.cpp src/index/timestamp*.h +src/interfaces/masternode_operator.h src/instantsend/*.cpp src/instantsend/*.h src/llmq/*.cpp From d33d642d9eeadef2a3d65f708d8cb82a9122246b Mon Sep 17 00:00:00 2001 From: pasta Date: Thu, 13 Aug 2026 14:19:03 -0500 Subject: [PATCH 2/3] test(evo): cover unavailable history during IBD --- src/test/evo_deterministicmns_tests.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/test/evo_deterministicmns_tests.cpp b/src/test/evo_deterministicmns_tests.cpp index 65a971b52986..9c13b6934cca 100644 --- a/src/test/evo_deterministicmns_tests.cpp +++ b/src/test/evo_deterministicmns_tests.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include @@ -1771,6 +1772,13 @@ BOOST_AUTO_TEST_CASE(operator_key_history_is_complete_and_fail_closed) FlagGuard guard{node::fReindex}; check_unavailable(); } + + TestChainState& chainstate = *static_cast(&setup.chainman.ActiveChainstate()); + SetMockTime(GetTime() + nMaxTipAge + 1); + chainstate.ResetIbd(); + check_unavailable(); + chainstate.JumpOutOfIbd(); + SetMockTime(setup.Tip()->GetBlockTime() + 1); require_history({®istered_key}); SetMockTime(GetTime() + nMaxTipAge + 1); From 1035fb61726bb563d979d788501ad9461b29d345 Mon Sep 17 00:00:00 2001 From: pasta Date: Thu, 13 Aug 2026 15:06:22 -0500 Subject: [PATCH 3/3] fix: permit operator history after snapshot validation --- src/node/interfaces.cpp | 5 +++-- src/test/validation_chainstatemanager_tests.cpp | 6 ++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/node/interfaces.cpp b/src/node/interfaces.cpp index ef086a2b03e3..f009cda874c8 100644 --- a/src/node/interfaces.cpp +++ b/src/node/interfaces.cpp @@ -330,7 +330,7 @@ class EVOImpl : public EVO bool history_available{true}; { LOCK(::cs_main); - if (node::fReindex || node::fImporting || chainman().IsSnapshotActive() || + if (node::fReindex || node::fImporting || chainman().IsSnapshotActiveAndUnvalidated() || chainman().ActiveChainstate().IsInitialBlockDownload()) { return UnavailableHistory(); } @@ -390,7 +390,8 @@ class EVOImpl : public EVO TipState tip_state{TipState::UNAVAILABLE}; { LOCK(::cs_main); - if (!ShutdownRequested() && !node::fReindex && !node::fImporting && !chainman().IsSnapshotActive() && + if (!ShutdownRequested() && !node::fReindex && !node::fImporting && + !chainman().IsSnapshotActiveAndUnvalidated() && !chainman().ActiveChainstate().IsInitialBlockDownload()) { const CBlockIndex* current_tip{chainman().ActiveChain().Tip()}; if (current_tip != chainman().m_best_header || !current_tip || diff --git a/src/test/validation_chainstatemanager_tests.cpp b/src/test/validation_chainstatemanager_tests.cpp index 901a3f094f65..3b6b8ee4b44f 100644 --- a/src/test/validation_chainstatemanager_tests.cpp +++ b/src/test/validation_chainstatemanager_tests.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -939,6 +940,9 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager_snapshot_completion, SnapshotTestSetup BOOST_CHECK_EQUAL(snapshot_chainstate_dir, gArgs.GetDataDirNet() / "chainstate_snapshot"); BOOST_CHECK(chainman.IsSnapshotActive()); + auto node{interfaces::MakeNode(m_node)}; + BOOST_CHECK(node->evo().getMasternodeOperatorKeyHistory().status == + interfaces::MasternodeOperatorKeyHistoryStatus::HISTORY_UNAVAILABLE); const uint256 snapshot_tip_hash = WITH_LOCK(chainman.GetMutex(), return chainman.ActiveTip()->GetBlockHash()); @@ -948,6 +952,8 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager_snapshot_completion, SnapshotTestSetup WITH_LOCK(::cs_main, BOOST_CHECK(chainman.IsSnapshotValidated())); BOOST_CHECK(chainman.IsSnapshotActive()); + BOOST_CHECK(node->evo().getMasternodeOperatorKeyHistory().status == + interfaces::MasternodeOperatorKeyHistoryStatus::SUCCESS); // Cache should have been rebalanced and reallocated to the "only" remaining // chainstate.