From b52f2568e6946525e62e87e621c8acc5fc110843 Mon Sep 17 00:00:00 2001 From: Ryan Ofsky Date: Thu, 13 Jan 2022 07:55:18 -0500 Subject: [PATCH 01/10] partial merge bitcoin/bitcoin#25494: add BlockInfo notifications This backports the first commit of bitcoin/bitcoin#25494, which introduces kernel/chain.{h,cpp}, interfaces::BlockInfo, and kernel::MakeBlockInfo. The remaining index refactoring commits are intentionally excluded. Upstream commit: a0b5b4ae5a24536d333cbce2ea584f2d935c651f --- src/Makefile.am | 2 ++ src/interfaces/chain.h | 18 +++++++++++++++-- src/kernel/chain.cpp | 26 +++++++++++++++++++++++++ src/kernel/chain.h | 19 ++++++++++++++++++ src/node/interfaces.cpp | 5 +++-- src/wallet/test/fuzz/notifications.cpp | 18 +++++++++++++---- src/wallet/wallet.cpp | 25 ++++++++++++------------ src/wallet/wallet.h | 4 ++-- test/lint/lint-circular-dependencies.py | 1 + 9 files changed, 96 insertions(+), 22 deletions(-) create mode 100644 src/kernel/chain.cpp create mode 100644 src/kernel/chain.h diff --git a/src/Makefile.am b/src/Makefile.am index 3395d9e602fb..dd4d128a1a5e 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -279,6 +279,7 @@ BITCOIN_CORE_H = \ interfaces/node.h \ interfaces/wallet.h \ kernel/blockmanager_opts.h \ + kernel/chain.h \ kernel/chainstatemanager_opts.h \ kernel/checks.h \ kernel/coinstats.h \ @@ -562,6 +563,7 @@ libbitcoin_node_a_SOURCES = \ instantsend/lock.cpp \ instantsend/net_instantsend.cpp \ instantsend/signing.cpp \ + kernel/chain.cpp \ kernel/checks.cpp \ kernel/coinstats.cpp \ kernel/context.cpp \ diff --git a/src/interfaces/chain.h b/src/interfaces/chain.h index c4bb7b7df0f2..a804d84c149c 100644 --- a/src/interfaces/chain.h +++ b/src/interfaces/chain.h @@ -20,6 +20,7 @@ class ArgsManager; class CBlock; class CConnman; +class CBlockUndo; class CFeeRate; class CRPCCommand; class CScheduler; @@ -79,6 +80,19 @@ class FoundBlock mutable bool found = false; }; +//! Block data sent with blockConnected, blockDisconnected notifications. +struct BlockInfo { + const uint256& hash; + const uint256* prev_hash = nullptr; + int height = -1; + int file_number = -1; + unsigned data_pos = 0; + const CBlock* data = nullptr; + const CBlockUndo* undo_data = nullptr; + + BlockInfo(const uint256& hash LIFETIMEBOUND) : hash(hash) {} +}; + //! Interface giving clients (wallet processes, maybe other analysis tools in //! the future) ability to access to the chain state, receive notifications, //! estimate fees, and submit transactions. @@ -272,8 +286,8 @@ class Chain virtual ~Notifications() {} virtual void transactionAddedToMempool(const CTransactionRef& tx, int64_t nAcceptTime) {} virtual void transactionRemovedFromMempool(const CTransactionRef& tx, MemPoolRemovalReason reason) {} - virtual void blockConnected(const CBlock& block, int height) {} - virtual void blockDisconnected(const CBlock& block, int height) {} + virtual void blockConnected(const BlockInfo& block) {} + virtual void blockDisconnected(const BlockInfo& block) {} virtual void updatedBlockTip() {} virtual void chainStateFlushed(const CBlockLocator& locator) {} virtual void notifyChainLock(const CBlockIndex* pindexChainLock, const std::shared_ptr& clsig) {} diff --git a/src/kernel/chain.cpp b/src/kernel/chain.cpp new file mode 100644 index 000000000000..82e77125d7f3 --- /dev/null +++ b/src/kernel/chain.cpp @@ -0,0 +1,26 @@ +// Copyright (c) 2022 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include +#include +#include +#include + +class CBlock; + +namespace kernel { +interfaces::BlockInfo MakeBlockInfo(const CBlockIndex* index, const CBlock* data) +{ + interfaces::BlockInfo info{index ? *index->phashBlock : uint256::ZERO}; + if (index) { + info.prev_hash = index->pprev ? index->pprev->phashBlock : nullptr; + info.height = index->nHeight; + LOCK(::cs_main); + info.file_number = index->nFile; + info.data_pos = index->nDataPos; + } + info.data = data; + return info; +} +} // namespace kernel diff --git a/src/kernel/chain.h b/src/kernel/chain.h new file mode 100644 index 000000000000..f0750f82663f --- /dev/null +++ b/src/kernel/chain.h @@ -0,0 +1,19 @@ +// Copyright (c) 2022 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_KERNEL_CHAIN_H +#define BITCOIN_KERNEL_CHAIN_H + +class CBlock; +class CBlockIndex; +namespace interfaces { +struct BlockInfo; +} // namespace interfaces + +namespace kernel { +//! Return data from block index. +interfaces::BlockInfo MakeBlockInfo(const CBlockIndex* block_index, const CBlock* data = nullptr); +} // namespace kernel + +#endif // BITCOIN_KERNEL_CHAIN_H diff --git a/src/node/interfaces.cpp b/src/node/interfaces.cpp index a78fd6d8dad1..09381ff5581c 100644 --- a/src/node/interfaces.cpp +++ b/src/node/interfaces.cpp @@ -39,6 +39,7 @@ #include #include #include +#include #include #include #include @@ -1105,11 +1106,11 @@ class NotificationsProxy : public CValidationInterface } void BlockConnected(const std::shared_ptr& block, const CBlockIndex* index) override { - m_notifications->blockConnected(*block, index->nHeight); + m_notifications->blockConnected(kernel::MakeBlockInfo(index, block.get())); } void BlockDisconnected(const std::shared_ptr& block, const CBlockIndex* index) override { - m_notifications->blockDisconnected(*block, index->nHeight); + m_notifications->blockDisconnected(kernel::MakeBlockInfo(index, block.get())); } void UpdatedBlockTip(const CBlockIndex* index, const CBlockIndex* fork_index, bool is_ibd) override { diff --git a/src/wallet/test/fuzz/notifications.cpp b/src/wallet/test/fuzz/notifications.cpp index 62c951c7a371..1f20ba463cba 100644 --- a/src/wallet/test/fuzz/notifications.cpp +++ b/src/wallet/test/fuzz/notifications.cpp @@ -135,8 +135,13 @@ FUZZ_TARGET(wallet_notifications, .init = initialize_setup) block.vtx.emplace_back(MakeTransactionRef(tx)); } // Mine block - a.wallet->blockConnected(block, chain.size()); - b.wallet->blockConnected(block, chain.size()); + const uint256& hash = block.GetHash(); + interfaces::BlockInfo info{hash}; + info.prev_hash = &block.hashPrevBlock; + info.height = chain.size(); + info.data = █ + a.wallet->blockConnected(info); + b.wallet->blockConnected(info); // Store the coins for the next block Coins coins_new; for (const auto& tx : block.vtx) { @@ -152,8 +157,13 @@ FUZZ_TARGET(wallet_notifications, .init = initialize_setup) auto& [coins, block]{chain.back()}; if (block.vtx.empty()) return; // Can only disconnect if the block was submitted first // Disconnect block - a.wallet->blockDisconnected(block, chain.size() - 1); - b.wallet->blockDisconnected(block, chain.size() - 1); + const uint256& hash = block.GetHash(); + interfaces::BlockInfo info{hash}; + info.prev_hash = &block.hashPrevBlock; + info.height = chain.size() - 1; + info.data = █ + a.wallet->blockDisconnected(info); + b.wallet->blockDisconnected(info); chain.pop_back(); }); auto& [coins, first_block]{chain.front()}; diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 94dd93781d2c..20e1402711b2 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -1472,17 +1472,17 @@ void CWallet::transactionRemovedFromMempool(const CTransactionRef& tx, MemPoolRe } } -void CWallet::blockConnected(const CBlock& block, int height) +void CWallet::blockConnected(const interfaces::BlockInfo& block) { - const uint256& block_hash = block.GetHash(); + assert(block.data); LOCK(cs_wallet); - m_last_block_processed_height = height; - m_last_block_processed = block_hash; + m_last_block_processed_height = block.height; + m_last_block_processed = block.hash; WalletBatch batch(GetDatabase()); - for (size_t index = 0; index < block.vtx.size(); index++) { - SyncTransaction(block.vtx[index], TxStateConfirmed{block_hash, height, static_cast(index)}, batch); - transactionRemovedFromMempool(block.vtx[index], MemPoolRemovalReason::BLOCK); + for (size_t index = 0; index < block.data->vtx.size(); index++) { + SyncTransaction(block.data->vtx[index], TxStateConfirmed{block.hash, block.height, static_cast(index)}, batch); + transactionRemovedFromMempool(block.data->vtx[index], MemPoolRemovalReason::BLOCK); } // reset cache to make sure no longer immature coins are included @@ -1490,21 +1490,22 @@ void CWallet::blockConnected(const CBlock& block, int height) fAnonymizableTallyCachedNonDenom = false; } -void CWallet::blockDisconnected(const CBlock& block, int height) +void CWallet::blockDisconnected(const interfaces::BlockInfo& block) { + assert(block.data); LOCK(cs_wallet); // At block disconnection, this will change an abandoned transaction to // be unconfirmed, whether or not the transaction is added back to the mempool. // User may have to call abandontransaction again. It may be addressed in the // future with a stickier abandoned state or even removing abandontransaction call. - m_last_block_processed_height = height - 1; - m_last_block_processed = block.hashPrevBlock; + m_last_block_processed_height = block.height - 1; + m_last_block_processed = *Assert(block.prev_hash); - int disconnect_height = height; + int disconnect_height = block.height; WalletBatch batch(GetDatabase()); - for (const CTransactionRef& ptx : block.vtx) { + for (const CTransactionRef& ptx : Assert(block.data)->vtx) { SyncTransaction(ptx, TxStateInactive{}, batch); for (const CTxIn& tx_in : ptx->vin) { diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index 6665af8ac35c..f38876aa9c72 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -683,8 +683,8 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati CWalletTx* AddToWallet(CTransactionRef tx, const TxState& state, const UpdateWalletTxFn& update_wtx=nullptr, bool fFlushOnClose=true, bool rescanning_old_block = false); bool LoadToWallet(const uint256& hash, const UpdateWalletTxFn& fill_wtx) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); void transactionAddedToMempool(const CTransactionRef& tx, int64_t nAcceptTime) override; - void blockConnected(const CBlock& block, int height) override; - void blockDisconnected(const CBlock& block, int height) override; + void blockConnected(const interfaces::BlockInfo& block) override; + void blockDisconnected(const interfaces::BlockInfo& block) override; void updatedBlockTip() override; int64_t RescanFromTime(int64_t startTime, const WalletRescanReserver& reserver, bool update); diff --git a/test/lint/lint-circular-dependencies.py b/test/lint/lint-circular-dependencies.py index ab9c1d4f311d..5de91c4b08d4 100755 --- a/test/lint/lint-circular-dependencies.py +++ b/test/lint/lint-circular-dependencies.py @@ -24,6 +24,7 @@ "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", "coinjoin/client -> coinjoin/util -> wallet/wallet -> psbt -> node/transaction -> net_processing -> coinjoin/walletman -> coinjoin/client", "common/bloom -> evo/assetlocktx -> llmq/commitment -> evo/deterministicmns -> evo/simplifiedmns -> merkleblock -> common/bloom", "common/bloom -> evo/assetlocktx -> llmq/quorumsman -> llmq/blockprocessor -> net -> common/bloom", From 4e452bf1f61c501882f62f5f61472cae3122c8b8 Mon Sep 17 00:00:00 2001 From: James O'Beirne Date: Thu, 10 Nov 2022 12:03:39 -0500 Subject: [PATCH 02/10] partial merge bitcoin/bitcoin#27596: validation: add ChainstateRole This is an early partial pick of bitcoin/bitcoin#27596; the remainder will be backported later. Upstream commit: c6af23c5179cc383f8e6c275373af8d11e6a989f --- src/Makefile.am | 1 + src/kernel/chain.cpp | 11 +++++++++++ src/kernel/chain.h | 20 ++++++++++++++++++++ src/validation.cpp | 10 ++++++++++ src/validation.h | 7 +++++++ 5 files changed, 49 insertions(+) diff --git a/src/Makefile.am b/src/Makefile.am index dd4d128a1a5e..6472835899f1 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -1287,6 +1287,7 @@ libdashkernel_la_SOURCES = \ init/common.cpp \ instantsend/db.cpp \ instantsend/instantsend.cpp \ + kernel/chain.cpp \ kernel/checks.cpp \ kernel/coinstats.cpp \ kernel/context.cpp \ diff --git a/src/kernel/chain.cpp b/src/kernel/chain.cpp index 82e77125d7f3..b8037f51799e 100644 --- a/src/kernel/chain.cpp +++ b/src/kernel/chain.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include @@ -24,3 +25,13 @@ interfaces::BlockInfo MakeBlockInfo(const CBlockIndex* index, const CBlock* data return info; } } // namespace kernel + +std::ostream& operator<<(std::ostream& os, const ChainstateRole& role) { + switch(role) { + case ChainstateRole::NORMAL: os << "normal"; break; + case ChainstateRole::ASSUMEDVALID: os << "assumedvalid"; break; + case ChainstateRole::BACKGROUND: os << "background"; break; + default: os.setstate(std::ios_base::failbit); + } + return os; +} diff --git a/src/kernel/chain.h b/src/kernel/chain.h index f0750f82663f..feba24a557e6 100644 --- a/src/kernel/chain.h +++ b/src/kernel/chain.h @@ -5,6 +5,8 @@ #ifndef BITCOIN_KERNEL_CHAIN_H #define BITCOIN_KERNEL_CHAIN_H +#include + class CBlock; class CBlockIndex; namespace interfaces { @@ -14,6 +16,24 @@ struct BlockInfo; namespace kernel { //! Return data from block index. interfaces::BlockInfo MakeBlockInfo(const CBlockIndex* block_index, const CBlock* data = nullptr); + } // namespace kernel +//! This enum describes the various roles a specific Chainstate instance can take. +//! Other parts of the system sometimes need to vary in behavior depending on the +//! existence of a background validation chainstate, e.g. when building indexes. +enum class ChainstateRole { + // Single chainstate in use, "normal" IBD mode. + NORMAL, + + // Doing IBD-style validation in the background. Implies use of an assumed-valid + // chainstate. + BACKGROUND, + + // Active assumed-valid chainstate. Implies use of a background IBD chainstate. + ASSUMEDVALID, +}; + +std::ostream& operator<<(std::ostream& os, const ChainstateRole& role); + #endif // BITCOIN_KERNEL_CHAIN_H diff --git a/src/validation.cpp b/src/validation.cpp index 8fbf243f64fa..d1d9e1d6f660 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -5858,3 +5858,13 @@ Chainstate& ChainstateManager::ActivateExistingSnapshot(CTxMemPool* mempool, uin m_active_chainstate = m_snapshot_chainstate.get(); return *m_snapshot_chainstate; } + +ChainstateRole Chainstate::GetRole() const +{ + if (m_chainman.GetAll().size() <= 1) { + return ChainstateRole::NORMAL; + } + return (this != &m_chainman.ActiveChainstate()) ? + ChainstateRole::BACKGROUND : + ChainstateRole::ASSUMEDVALID; +} diff --git a/src/validation.h b/src/validation.h index 78ebdf366c04..112d37ee12dc 100644 --- a/src/validation.h +++ b/src/validation.h @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -535,6 +536,12 @@ class Chainstate const std::unique_ptr& chain_helper, std::optional from_snapshot_blockhash = std::nullopt); + //! Return the current role of the chainstate. See `ChainstateManager` + //! documentation for a description of the different types of chainstates. + //! + //! @sa ChainstateRole + ChainstateRole GetRole() const EXCLUSIVE_LOCKS_REQUIRED(::cs_main); + /** * Initialize the CoinsViews UTXO set database management data structures. The in-memory * cache is initialized separately. From ee01a0fa0670e5ee91a2ac49449ceb4187628940 Mon Sep 17 00:00:00 2001 From: Pasta Date: Fri, 10 Jul 2026 23:28:48 -0500 Subject: [PATCH 03/10] evo: make CEvoDB consistency state per-chainstate --- src/dbwrapper.h | 16 ++++++ src/evo/evodb.cpp | 106 +++++++++++++++++++++++++++--------- src/evo/evodb.h | 135 +++++++++++++++++++++++++++++++++++++++------- 3 files changed, 213 insertions(+), 44 deletions(-) diff --git a/src/dbwrapper.h b/src/dbwrapper.h index 817c8172defd..5d1a47d0bec9 100644 --- a/src/dbwrapper.h +++ b/src/dbwrapper.h @@ -669,6 +669,22 @@ class CDBTransaction { return parent.Read(ssKey, value); } + /** Read a value only if it is present in this transaction's write set. */ + template + bool ReadPending(const K& key, V& value) { + const CDataStream ssKey = KeyToDataStream(key); + auto it = writes.find(ssKey); + if (it == writes.end()) { + return false; + } + auto* impl = dynamic_cast*>(it->second.get()); + if (!impl) { + throw std::runtime_error("ReadPending called with V != previously written type"); + } + value = impl->value; + return true; + } + template bool Exists(const K& key) { return Exists(KeyToDataStream(key)); diff --git a/src/evo/evodb.cpp b/src/evo/evodb.cpp index c387d5e313c6..cab109daf53c 100644 --- a/src/evo/evodb.cpp +++ b/src/evo/evodb.cpp @@ -6,8 +6,9 @@ #include -CEvoDBScopedCommitter::CEvoDBScopedCommitter(CEvoDB &_evoDB) : - evoDB(_evoDB) +CEvoDBScopedCommitter::CEvoDBScopedCommitter(CEvoDB& _evoDB, EvoDbIdentity identity) : + evoDB{_evoDB}, + identity{identity} { } @@ -21,60 +22,117 @@ void CEvoDBScopedCommitter::Commit() { assert(!didCommitOrRollback); didCommitOrRollback = true; - evoDB.CommitCurTransaction(); + evoDB.CommitCurTransaction(identity); } void CEvoDBScopedCommitter::Rollback() { assert(!didCommitOrRollback); didCommitOrRollback = true; - evoDB.RollbackCurTransaction(); + evoDB.RollbackCurTransaction(identity); } CEvoDB::CEvoDB(const util::DbWrapperParams& db_params) : - db{util::MakeDbWrapper({db_params.path / "evodb", db_params.memory, db_params.wipe, /*cache_size=*/64 << 20})}, - rootBatch{*db}, - rootDBTransaction{*db, rootBatch}, - curDBTransaction{rootDBTransaction, rootDBTransaction} + db{util::MakeDbWrapper({db_params.path / "evodb", db_params.memory, db_params.wipe, /*cache_size=*/64 << 20})} { + transaction_contexts.emplace(EvoDbIdentity::NORMAL, std::make_unique(*db)); } CEvoDB::~CEvoDB() = default; -void CEvoDB::CommitCurTransaction() +CEvoDB::TransactionContext& CEvoDB::GetContext(EvoDbIdentity identity) +{ + auto [it, inserted] = transaction_contexts.try_emplace(identity); + if (inserted) it->second = std::make_unique(*db); + return *it->second; +} + +const CEvoDB::TransactionContext& CEvoDB::GetContext(EvoDbIdentity identity) const +{ + return *transaction_contexts.at(identity); +} + +EvoDbIdentity CEvoDB::GetCurrentIdentity() const { return active_transaction.value_or(active_chainstate_identity); } + +void CEvoDB::SetActiveChainstateIdentity(EvoDbIdentity identity) +{ + LOCK(cs); + assert(!active_transaction.has_value()); + active_chainstate_identity = identity; +} + +std::unique_ptr CEvoDB::BeginTransaction(EvoDbIdentity identity) +{ + LOCK(cs); + assert(!active_transaction.has_value()); + active_transaction = identity; + GetContext(identity); + return std::make_unique(*this, identity); +} + +void CEvoDB::CommitCurTransaction(EvoDbIdentity identity) { LOCK(cs); - curDBTransaction.Commit(); + assert(active_transaction == identity); + GetContext(identity).cur_transaction.Commit(); + active_transaction.reset(); } -void CEvoDB::RollbackCurTransaction() +void CEvoDB::RollbackCurTransaction(EvoDbIdentity identity) { LOCK(cs); - curDBTransaction.Clear(); + assert(active_transaction == identity); + GetContext(identity).cur_transaction.Clear(); + active_transaction.reset(); } -bool CEvoDB::CommitRootTransaction() +bool CEvoDB::CommitRootTransaction(EvoDbIdentity identity) { LOCK(cs); - assert(curDBTransaction.IsClean()); - rootDBTransaction.Commit(); - bool ret = db->WriteBatch(rootBatch); - rootBatch.Clear(); + auto& context = GetContext(identity); + assert(context.cur_transaction.IsClean()); + context.root_transaction.Commit(); + bool ret = db->WriteBatch(context.root_batch); + context.root_batch.Clear(); return ret; } -bool CEvoDB::VerifyBestBlock(const uint256& hash) +bool CEvoDB::ReadBestBlock(EvoDbIdentity identity, uint256& hash) +{ + LOCK(cs); + auto& transaction = GetContext(identity).cur_transaction; + if (identity == EvoDbIdentity::NORMAL) { + return transaction.Read(EVODB_BEST_BLOCK, hash); + } + return transaction.Read(std::make_pair(EVODB_BEST_BLOCK, uint8_t{1}), hash); +} + +bool CEvoDB::VerifyBestBlock(EvoDbIdentity identity, const uint256& hash) { // Make sure evodb is consistent. // If we already have best block hash saved, the previous block should match it. - uint256 hashBestBlock; - if (!Read(EVODB_BEST_BLOCK, hashBestBlock)) { - return false; + uint256 hash_best_block; + return ReadBestBlock(identity, hash_best_block) && hash_best_block == hash; +} + +void CEvoDB::WriteBestBlock(EvoDbIdentity identity, const uint256& hash) +{ + LOCK(cs); + auto& transaction = GetContext(identity).cur_transaction; + if (identity == EvoDbIdentity::NORMAL) { + transaction.Write(EVODB_BEST_BLOCK, hash); + } else { + transaction.Write(std::make_pair(EVODB_BEST_BLOCK, uint8_t{1}), hash); } - return hashBestBlock == hash; } -void CEvoDB::WriteBestBlock(const uint256& hash) +void CEvoDB::WriteDualChainstateMarker() { - Write(EVODB_BEST_BLOCK, hash); + Write(EVODB_DUAL_CHAINSTATE, uint8_t{1}); +} + +bool CEvoDB::HasDualChainstateMarker() +{ + LOCK(cs); + return db->Exists(EVODB_DUAL_CHAINSTATE); } diff --git a/src/evo/evodb.h b/src/evo/evodb.h index 38c6662633df..8d55cae91027 100644 --- a/src/evo/evodb.h +++ b/src/evo/evodb.h @@ -8,6 +8,10 @@ #include #include +#include +#include +#include + class uint256; namespace util { struct DbWrapperParams; @@ -18,6 +22,18 @@ struct DbWrapperParams; // "b_b3" was used after masternode type introduction in evoDB // "b_b4" was used after storing protx version for each masternode in evoDB static const std::string EVODB_BEST_BLOCK = "b_b4"; +// Released Dash software has no snapshot-chainstate detection, ignores the +// chainstate_snapshot directory, and loads the chainstate directory together +// with this legacy marker. That pair is the background chainstate's own coins +// and marker, so downgrading mid-snapshot safely reverts to background IBD. +static const std::string EVODB_DUAL_CHAINSTATE = "b_dcs"; + +// TODO(assumeutxo): snapshot completion must promote the SNAPSHOT marker to +// the legacy key when chainstate_snapshot is renamed over chainstate. +enum class EvoDbIdentity { + NORMAL, + SNAPSHOT, +}; class CEvoDB; @@ -25,10 +41,11 @@ class CEvoDBScopedCommitter { private: CEvoDB& evoDB; + const EvoDbIdentity identity; bool didCommitOrRollback{false}; public: - explicit CEvoDBScopedCommitter(CEvoDB& _evoDB); + CEvoDBScopedCommitter(CEvoDB& _evoDB, EvoDbIdentity identity); ~CEvoDBScopedCommitter(); void Commit(); @@ -45,9 +62,26 @@ class CEvoDB using RootTransaction = CDBTransaction; using CurTransaction = CDBTransaction; - CDBBatch rootBatch; - RootTransaction rootDBTransaction; - CurTransaction curDBTransaction; + struct TransactionContext { + CDBBatch root_batch; + RootTransaction root_transaction; + CurTransaction cur_transaction; + + explicit TransactionContext(CDBWrapper& db) : + root_batch{db}, + root_transaction{db, root_batch}, + cur_transaction{root_transaction, root_transaction} + { + } + }; + + std::map> transaction_contexts; + std::optional active_transaction; + EvoDbIdentity active_chainstate_identity{EvoDbIdentity::NORMAL}; + + TransactionContext& GetContext(EvoDbIdentity identity); + const TransactionContext& GetContext(EvoDbIdentity identity) const; + EvoDbIdentity GetCurrentIdentity() const; public: CEvoDB() = delete; @@ -56,44 +90,95 @@ class CEvoDB explicit CEvoDB(const util::DbWrapperParams& db_params); ~CEvoDB(); - std::unique_ptr BeginTransaction() EXCLUSIVE_LOCKS_REQUIRED(!cs) - { - LOCK(cs); - return std::make_unique(*this); - } + /** Select the overlay used by reads outside a scoped transaction. */ + void SetActiveChainstateIdentity(EvoDbIdentity identity) EXCLUSIVE_LOCKS_REQUIRED(!cs); + std::unique_ptr BeginTransaction(EvoDbIdentity identity = EvoDbIdentity::NORMAL) EXCLUSIVE_LOCKS_REQUIRED(!cs); CurTransaction& GetCurTransaction() EXCLUSIVE_LOCKS_REQUIRED(cs) { AssertLockHeld(cs); // lock must be held from outside as long as the DB transaction is used - return curDBTransaction; + return GetContext(GetCurrentIdentity()).cur_transaction; } template bool Read(const K& key, V& value) EXCLUSIVE_LOCKS_REQUIRED(!cs) { LOCK(cs); - return curDBTransaction.Read(key, value); + return GetContext(GetCurrentIdentity()).cur_transaction.Read(key, value); } template void Write(const K& key, const V& value) EXCLUSIVE_LOCKS_REQUIRED(!cs) { LOCK(cs); - curDBTransaction.Write(key, value); + GetContext(GetCurrentIdentity()).cur_transaction.Write(key, value); + } + + /** + * Write immutable block-derived data, accepting an identical existing value. + * TODO(assumeutxo): WriteDerived spot-checks are not the holistic base-state + * comparison required at snapshot completion. + */ + template + bool WriteDerived(const K& key, const V& value) EXCLUSIVE_LOCKS_REQUIRED(!cs) + { + LOCK(cs); + const EvoDbIdentity identity = GetCurrentIdentity(); + auto& transaction = GetContext(identity).cur_transaction; + V existing; + bool write{true}; + if (transaction.Read(key, existing)) { + CDataStream existing_stream{SER_DISK, CLIENT_VERSION}; + CDataStream value_stream{SER_DISK, CLIENT_VERSION}; + existing_stream << existing; + value_stream << value; + const bool matches = existing_stream.size() == value_stream.size() && + std::equal(existing_stream.begin(), existing_stream.end(), value_stream.begin()); + if (!matches) { + LogPrintf("ERROR: CEvoDB::WriteDerived: block-derived payload mismatch in EvoDB\n"); + return false; + } + write = false; + } + + // Cross-identity writes of the same key are disjoint by construction + // (background validates blocks <= snapshot base; the snapshot chainstate + // validates blocks > base; seeded data is flushed at activation). This check is + // verification-only insurance for that invariant and must never suppress the + // caller's own write. + for (const auto& [other_identity, context] : transaction_contexts) { + if (other_identity == identity || !context) continue; + V pending; + if (!context->root_transaction.ReadPending(key, pending)) continue; + + CDataStream pending_stream{SER_DISK, CLIENT_VERSION}; + CDataStream value_stream{SER_DISK, CLIENT_VERSION}; + pending_stream << pending; + value_stream << value; + const bool matches = pending_stream.size() == value_stream.size() && + std::equal(pending_stream.begin(), pending_stream.end(), value_stream.begin()); + if (!matches) { + LogPrintf("ERROR: CEvoDB::WriteDerived: cross-identity block-derived payload mismatch in EvoDB\n"); + return false; + } + } + + if (write) transaction.Write(key, value); + return true; } template bool Exists(const K& key) EXCLUSIVE_LOCKS_REQUIRED(!cs) { LOCK(cs); - return curDBTransaction.Exists(key); + return GetContext(GetCurrentIdentity()).cur_transaction.Exists(key); } template void Erase(const K& key) EXCLUSIVE_LOCKS_REQUIRED(!cs) { LOCK(cs); - curDBTransaction.Erase(key); + GetContext(GetCurrentIdentity()).cur_transaction.Erase(key); } CDBWrapper& GetRawDB() @@ -103,21 +188,31 @@ class CEvoDB [[nodiscard]] size_t GetMemoryUsage() const { - return rootDBTransaction.GetMemoryUsage(); + size_t result{0}; + for (const auto& [_, context] : transaction_contexts) { + result += context->root_transaction.GetMemoryUsage(); + } + return result; } - bool CommitRootTransaction() EXCLUSIVE_LOCKS_REQUIRED(!cs); + bool CommitRootTransaction(EvoDbIdentity identity = EvoDbIdentity::NORMAL) EXCLUSIVE_LOCKS_REQUIRED(!cs); bool IsEmpty() { return db->IsEmpty(); } - bool VerifyBestBlock(const uint256& hash) EXCLUSIVE_LOCKS_REQUIRED(!cs); - void WriteBestBlock(const uint256& hash) EXCLUSIVE_LOCKS_REQUIRED(!cs); + bool ReadBestBlock(EvoDbIdentity identity, uint256& hash) EXCLUSIVE_LOCKS_REQUIRED(!cs); + bool VerifyBestBlock(EvoDbIdentity identity, const uint256& hash) EXCLUSIVE_LOCKS_REQUIRED(!cs); + void WriteBestBlock(EvoDbIdentity identity, const uint256& hash) EXCLUSIVE_LOCKS_REQUIRED(!cs); + void WriteDualChainstateMarker() EXCLUSIVE_LOCKS_REQUIRED(!cs); + bool HasDualChainstateMarker() EXCLUSIVE_LOCKS_REQUIRED(!cs); + + bool VerifyBestBlock(const uint256& hash) EXCLUSIVE_LOCKS_REQUIRED(!cs) { return VerifyBestBlock(EvoDbIdentity::NORMAL, hash); } + void WriteBestBlock(const uint256& hash) EXCLUSIVE_LOCKS_REQUIRED(!cs) { WriteBestBlock(EvoDbIdentity::NORMAL, hash); } private: // only CEvoDBScopedCommitter is allowed to invoke these friend class CEvoDBScopedCommitter; - void CommitCurTransaction() EXCLUSIVE_LOCKS_REQUIRED(!cs); - void RollbackCurTransaction() EXCLUSIVE_LOCKS_REQUIRED(!cs); + void CommitCurTransaction(EvoDbIdentity identity) EXCLUSIVE_LOCKS_REQUIRED(!cs); + void RollbackCurTransaction(EvoDbIdentity identity) EXCLUSIVE_LOCKS_REQUIRED(!cs); }; #endif // BITCOIN_EVO_EVODB_H From 2e9c8f8d09ac8e436feada2c3bd01a8a049a3330 Mon Sep 17 00:00:00 2001 From: Pasta Date: Fri, 10 Jul 2026 23:28:49 -0500 Subject: [PATCH 04/10] backport: Dash adaptations for bitcoin#27596 (per-chainstate EvoDB validation) --- src/evo/creditpool.cpp | 10 +- src/evo/deterministicmns.cpp | 27 ++- src/evo/mnhftx.cpp | 9 +- src/llmq/blockprocessor.cpp | 63 +++++-- src/llmq/blockprocessor.h | 6 + src/node/chainstate.cpp | 36 ++-- src/test/util/setup_common.cpp | 30 ++-- src/test/util/setup_common.h | 18 +- .../validation_chainstatemanager_tests.cpp | 160 +++++++++++++++++- src/validation.cpp | 141 ++++++++++++--- src/validation.h | 21 ++- 11 files changed, 431 insertions(+), 90 deletions(-) diff --git a/src/evo/creditpool.cpp b/src/evo/creditpool.cpp index 980891fd7c95..97ea2595661e 100644 --- a/src/evo/creditpool.cpp +++ b/src/evo/creditpool.cpp @@ -136,13 +136,17 @@ std::optional CCreditPoolManager::GetFromCache(const CBlockIndex& b void CCreditPoolManager::AddToCache(const uint256& block_hash, int height, const CCreditPool &pool) { + if (height % DISK_SNAPSHOT_PERIOD == 0) { + if (!evoDb.WriteDerived(std::make_pair(DB_CREDITPOOL_SNAPSHOT, block_hash), pool)) { + LogPrintf("ERROR: CCreditPoolManager::%s -- EvoDB credit pool mismatch for block %s\n", + __func__, block_hash.ToString()); + throw std::runtime_error("EvoDB credit pool payload mismatch"); + } + } { LOCK(cache_mutex); creditPoolCache.insert(block_hash, pool); } - if (height % DISK_SNAPSHOT_PERIOD == 0) { - evoDb.Write(std::make_pair(DB_CREDITPOOL_SNAPSHOT, block_hash), pool); - } } CCreditPool CCreditPoolManager::ConstructCreditPool(const gsl::not_null block_index, CCreditPool prev) diff --git a/src/evo/deterministicmns.cpp b/src/evo/deterministicmns.cpp index df22b9198362..a64ae27a4ad7 100644 --- a/src/evo/deterministicmns.cpp +++ b/src/evo/deterministicmns.cpp @@ -645,7 +645,24 @@ bool CDeterministicMNManager::ProcessBlock(const CBlock& block, gsl::not_nullpprev); diff = oldList.BuildDiff(newList); - // apply platform unban for platform revive too + if (!m_evoDb.WriteDerived(std::make_pair(DB_LIST_DIFF, newList.GetBlockHash()), diff)) { + LogPrintf("ERROR: CDeterministicMNManager::%s -- EvoDB list diff mismatch for block %s\n", + __func__, newList.GetBlockHash().ToString()); + return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "failed-dmn-block"); + } + if ((nHeight % DISK_SNAPSHOT_PERIOD) == 0 || pindex->pprev == m_initial_snapshot_index) { + if (!m_evoDb.WriteDerived(std::make_pair(DB_LIST_SNAPSHOT, newList.GetBlockHash()), newList)) { + LogPrintf("ERROR: CDeterministicMNManager::%s -- EvoDB list snapshot mismatch for block %s\n", + __func__, newList.GetBlockHash().ToString()); + return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "failed-dmn-block"); + } + mnListsCache.emplace(newList.GetBlockHash(), newList); + LogPrintf("CDeterministicMNManager::%s -- Wrote snapshot. nHeight=%d, mapCurMNs.allMNsCount=%d\n", + __func__, nHeight, newList.GetCounts().total()); + } + + // apply platform unban for platform revive too, after all persistent + // payload checks have succeeded for (int i = 1; i < (int)block.vtx.size(); i++) { const CTransaction& tx = *block.vtx[i]; if (!tx.IsSpecialTxVersion() || tx.nType != TRANSACTION_PROVIDER_UPDATE_SERVICE) { @@ -661,14 +678,6 @@ bool CDeterministicMNManager::ProcessBlock(const CBlock& block, gsl::not_nullpprev == m_initial_snapshot_index) { - m_evoDb.Write(std::make_pair(DB_LIST_SNAPSHOT, newList.GetBlockHash()), newList); - mnListsCache.emplace(newList.GetBlockHash(), newList); - LogPrintf("CDeterministicMNManager::%s -- Wrote snapshot. nHeight=%d, mapCurMNs.allMNsCount=%d\n", - __func__, nHeight, newList.GetCounts().total()); - } - diff.nHeight = pindex->nHeight; mnListDiffsCache.emplace(pindex->GetBlockHash(), diff); mnListsCache.emplace(newList.GetBlockHash(), newList); diff --git a/src/evo/mnhftx.cpp b/src/evo/mnhftx.cpp index a7fe8436ad3b..29cf8fb6c455 100644 --- a/src/evo/mnhftx.cpp +++ b/src/evo/mnhftx.cpp @@ -336,13 +336,16 @@ void CMNHFManager::AddToCache(const Signals& signals, const CBlockIndex* const p { assert(pindex != nullptr); const uint256& blockHash = pindex->GetBlockHash(); + if (DeploymentActiveAt(*pindex, m_chainman.GetConsensus(), Consensus::DEPLOYMENT_V20) && + !m_evoDb.WriteDerived(std::make_pair(DB_SIGNALS_v2, blockHash), signals)) { + LogPrintf("ERROR: CMNHFManager::%s -- EvoDB MNHF state mismatch for block %s\n", + __func__, blockHash.ToString()); + throw std::runtime_error("EvoDB MNHF payload mismatch"); + } { LOCK(cs_cache); mnhfCache.insert(blockHash, signals); } - if (!DeploymentActiveAt(*pindex, m_chainman.GetConsensus(), Consensus::DEPLOYMENT_V20)) return; - - m_evoDb.Write(std::make_pair(DB_SIGNALS_v2, blockHash), signals); } void CMNHFManager::AddSignal(const CBlockIndex* const pindex, int bit) diff --git a/src/llmq/blockprocessor.cpp b/src/llmq/blockprocessor.cpp index a4793f0df3d8..885c3f8ddd9a 100644 --- a/src/llmq/blockprocessor.cpp +++ b/src/llmq/blockprocessor.cpp @@ -22,9 +22,11 @@ #include #include #include +#include #include #include +#include #include static void PreComputeQuorumMembers(CDeterministicMNManager& dmnman, llmq::CQuorumSnapshotManager& qsnapman, @@ -45,6 +47,35 @@ static const std::string DB_MINED_COMMITMENT_BY_INVERSED_HEIGHT_Q_INDEXED = "q_m static const std::string DB_BEST_BLOCK_UPGRADE = "q_bbu2"; +bool EraseMinedCommitmentIfUnreferenced(CEvoDB& evo_db, const Chainstate& chainstate, + gsl::not_null pindex, + Consensus::LLMQType llmq_type, const uint256& quorum_hash) +{ + AssertLockHeld(::cs_main); + const auto chainstates = chainstate.m_chainman.GetAll(); + const bool block_used_by_other_chainstate = std::any_of( + chainstates.begin(), chainstates.end(), + [&](const Chainstate* other) { + return other != &chainstate && other->m_chain.Contains(pindex); + }); + if (block_used_by_other_chainstate) { + return false; + } + evo_db.Erase(std::make_pair(DB_MINED_COMMITMENT, std::make_pair(llmq_type, quorum_hash))); + return true; +} + +template +static bool SerializedEqual(const T& lhs, const T& rhs) +{ + CDataStream lhs_stream{SER_DISK, CLIENT_VERSION}; + CDataStream rhs_stream{SER_DISK, CLIENT_VERSION}; + lhs_stream << lhs; + rhs_stream << rhs; + return lhs_stream.size() == rhs_stream.size() && + std::equal(lhs_stream.begin(), lhs_stream.end(), rhs_stream.begin()); +} + CQuorumBlockProcessor::CQuorumBlockProcessor(Chainstate& chainstate, CDeterministicMNManager& dmnman, CEvoDB& evoDb, CQuorumSnapshotManager& qsnapman, int8_t bls_threads) : m_chainstate{chainstate}, @@ -319,8 +350,11 @@ bool CQuorumBlockProcessor::ProcessCommitment(int nHeight, const uint256& blockH return true; } - if (HasMinedCommitment(llmq_params.type, quorumHash)) { - // should not happen as it's already handled in ProcessBlock + const auto stored_commitment = GetMinedCommitment(llmq_params.type, quorumHash); + if (!stored_commitment.first.IsNull() && + !SerializedEqual(stored_commitment, std::make_pair(qc, blockHash))) { + // Preserve the existing duplicate-commitment result while allowing an + // exact block re-derivation to proceed through all validation below. return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "bad-qc-dup"); } @@ -359,7 +393,11 @@ bool CQuorumBlockProcessor::ProcessCommitment(int nHeight, const uint256& blockH // Store commitment in DB auto cacheKey = std::make_pair(llmq_params.type, quorumHash); - m_evoDb.Write(std::make_pair(DB_MINED_COMMITMENT, cacheKey), std::make_pair(qc, blockHash)); + if (!m_evoDb.WriteDerived(std::make_pair(DB_MINED_COMMITMENT, cacheKey), std::make_pair(qc, blockHash))) { + LogPrintf("ERROR: CQuorumBlockProcessor::%s -- EvoDB quorum commitment mismatch for quorum %s\n", + __func__, quorumHash.ToString()); + return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "bad-qc-dup"); + } if (rotation_enabled) { m_evoDb.Write(BuildInversedHeightKeyIndexed(llmq_params.type, nHeight, int(qc.quorumIndex)), pQuorumBaseBlockIndex->nHeight); @@ -397,15 +435,18 @@ bool CQuorumBlockProcessor::UndoBlock(const CBlock& block, gsl::not_nullnHeight, int(qc.quorumIndex))); + if (!EraseMinedCommitmentIfUnreferenced(m_evoDb, m_chainstate, pindex, qc.llmqType, qc.quorumHash)) { + LogPrint(BCLog::LLMQ, "%s -- retaining commitment for block %s used by another chainstate\n", + __func__, pindex->GetBlockHash().ToString()); } else { - m_evoDb.Erase(BuildInversedHeightKey(qc.llmqType, pindex->nHeight)); + const auto& llmq_params_opt = Params().GetLLMQ(qc.llmqType); + assert(llmq_params_opt.has_value()); + + if (IsQuorumRotationEnabled(llmq_params_opt.value(), pindex)) { + m_evoDb.Erase(BuildInversedHeightKeyIndexed(qc.llmqType, pindex->nHeight, int(qc.quorumIndex))); + } else { + m_evoDb.Erase(BuildInversedHeightKey(qc.llmqType, pindex->nHeight)); + } } WITH_LOCK(minableCommitmentsCs, mapHasMinedCommitmentCache[qc.llmqType].erase(qc.quorumHash)); diff --git a/src/llmq/blockprocessor.h b/src/llmq/blockprocessor.h index 4cf2598b3f44..fafaf36a7328 100644 --- a/src/llmq/blockprocessor.h +++ b/src/llmq/blockprocessor.h @@ -38,6 +38,12 @@ namespace llmq class CFinalCommitment; class CQuorumSnapshotManager; +/** Erase a mined commitment unless another chainstate still contains its block. */ +bool EraseMinedCommitmentIfUnreferenced(CEvoDB& evo_db, const Chainstate& chainstate, + gsl::not_null pindex, + Consensus::LLMQType llmq_type, const uint256& quorum_hash) + EXCLUSIVE_LOCKS_REQUIRED(::cs_main); + class CQuorumBlockProcessor { private: diff --git a/src/node/chainstate.cpp b/src/node/chainstate.cpp index 79b7e353367d..0fd028841bff 100644 --- a/src/node/chainstate.cpp +++ b/src/node/chainstate.cpp @@ -5,12 +5,13 @@ #include #include -#include #include +#include #include #include #include #include +#include #include #include #include @@ -60,8 +61,16 @@ ChainstateLoadResult LoadChainstate(ChainstateManager& chainman, // Load the fully validated chainstate. chainman.InitializeChainstate(options.mempool, *evodb, chain_helper); - // Load a chain created from a UTXO snapshot, if any exist. - chainman.DetectSnapshotChainstate(options.mempool); + // Load a chain created from a UTXO snapshot, if any exist. Reindexing wipes + // EvoDB before this point, so record the persisted snapshot independently + // and do not try to activate it without its marker. + const bool has_snapshot{node::FindSnapshotChainstateDir().has_value()}; + if (has_snapshot && !(options.reindex || options.reindex_chainstate)) { + bilingual_str snapshot_error; + if (!chainman.DetectSnapshotChainstate(options.mempool, snapshot_error)) { + return {ChainstateLoadStatus::FAILURE, snapshot_error}; + } + } auto& pblocktree{chainman.m_blockman.m_block_tree_db}; // new CBlockTreeDB tries to delete the existing file, which @@ -69,11 +78,6 @@ ChainstateLoadResult LoadChainstate(ChainstateManager& chainman, pblocktree.reset(); pblocktree.reset(new CBlockTreeDB(cache_sizes.block_tree_db, options.block_tree_db_in_memory, options.reindex)); - DashChainstateSetup(chainman, mn_metaman, sporkman, chainlocks, mn_sync, chain_helper, - dmnman, *evodb, llmq_ctx, options.mempool, data_dir, options.dash_dbs_in_memory, - /*llmq_dbs_wipe=*/options.reindex || options.reindex_chainstate, options.bls_threads, options.worker_count, - options.max_recsigs_age); - if (options.reindex) { pblocktree->WriteReindexing(true); //If we're reindexing in prune mode, wipe away unusable block files and all undo data files @@ -88,11 +92,23 @@ ChainstateLoadResult LoadChainstate(ChainstateManager& chainman, // block file from disk. // Note that it also sets fReindex global based on the disk flag! // From here on, fReindex and options.reindex values may be different! - if (!chainman.LoadBlockIndex()) { + if (!chainman.LoadBlockIndex(/*reset_assumed_valid=*/has_snapshot && options.reindex_chainstate)) { if (options.check_interrupt && options.check_interrupt()) return {ChainstateLoadStatus::INTERRUPTED, {}}; return {ChainstateLoadStatus::FAILURE, _("Error loading block database")}; } + if (has_snapshot && (options.reindex || options.reindex_chainstate)) { + LogPrintf("[snapshot] deleting snapshot chainstate due to reindexing\n"); + if (!chainman.DeleteSnapshotChainstate()) { + return {ChainstateLoadStatus::FAILURE, _("Couldn't remove snapshot chainstate")}; + } + } + + DashChainstateSetup(chainman, mn_metaman, sporkman, chainlocks, mn_sync, chain_helper, dmnman, *evodb, llmq_ctx, + options.mempool, data_dir, options.dash_dbs_in_memory, + /*llmq_dbs_wipe=*/options.reindex || options.reindex_chainstate, options.bls_threads, + options.worker_count, options.max_recsigs_age); + if (!chainman.BlockIndex().empty() && !chainman.m_blockman.LookupBlockIndex(chainman.GetConsensus().hashGenesisBlock)) { return {ChainstateLoadStatus::FAILURE_INCOMPATIBLE_DB, _("Incorrect or no genesis block found. Wrong datadir for network?")}; @@ -159,7 +175,7 @@ ChainstateLoadResult LoadChainstate(ChainstateManager& chainman, // TODO: CEvoDB instance should probably be a part of Chainstate // (for multiple chainstates to actually work in parallel) // and not a global - if (&chainman.ActiveChainstate() == chainstate && !evodb->CommitRootTransaction()) { + if (&chainman.ActiveChainstate() == chainstate && !evodb->CommitRootTransaction(chainstate->EvoDbIdentity())) { return {ChainstateLoadStatus::FAILURE, _("Failed to commit Evo database")}; } diff --git a/src/test/util/setup_common.cpp b/src/test/util/setup_common.cpp index 957c494dfcc1..556f348d0839 100644 --- a/src/test/util/setup_common.cpp +++ b/src/test/util/setup_common.cpp @@ -168,9 +168,10 @@ struct NetworkSetup }; static NetworkSetup g_networksetup_instance; -BasicTestingSetup::BasicTestingSetup(const std::string& chainName, const std::vector& extra_args) +BasicTestingSetup::BasicTestingSetup(const std::string& chainName, const std::vector& extra_args, bool dash_dbs_in_memory) : m_path_root{fs::temp_directory_path() / "test_common_" PACKAGE_NAME / g_insecure_rand_ctx_temp_path.rand256().ToString()}, - m_args{} + m_args{}, + m_dash_dbs_in_memory{dash_dbs_in_memory} { m_node.args = &gArgs; std::vector arguments = Cat( @@ -243,7 +244,7 @@ BasicTestingSetup::BasicTestingSetup(const std::string& chainName, const std::ve m_node.netfulfilledman = std::make_unique(); m_node.sporkman = std::make_unique(); m_node.chainlocks = std::make_unique(*m_node.sporkman); - m_node.evodb = std::make_unique(util::DbWrapperParams{.path = m_node.args->GetDataDirNet(), .memory = true, .wipe = true}); + m_node.evodb = std::make_unique(util::DbWrapperParams{.path = m_node.args->GetDataDirNet(), .memory = m_dash_dbs_in_memory, .wipe = true}); static bool noui_connected = false; if (!noui_connected) { @@ -258,10 +259,10 @@ BasicTestingSetup::~BasicTestingSetup() { SetMockTime(0s); // Reset mocktime for following tests LogInstance().DisconnectTestLogger(); + m_node.evodb.reset(); fs::remove_all(m_path_root); gArgs.ClearArgs(); - m_node.evodb.reset(); m_node.sporkman.reset(); m_node.netfulfilledman.reset(); m_node.mn_metaman.reset(); @@ -273,8 +274,8 @@ BasicTestingSetup::~BasicTestingSetup() m_node.args = nullptr; } -ChainTestingSetup::ChainTestingSetup(const std::string& chainName, const std::vector& extra_args) - : BasicTestingSetup(chainName, extra_args) +ChainTestingSetup::ChainTestingSetup(const std::string& chainName, const std::vector& extra_args, bool dash_dbs_in_memory) + : BasicTestingSetup(chainName, extra_args, dash_dbs_in_memory) { const CChainParams& chainparams = Params(); @@ -326,7 +327,7 @@ void ChainTestingSetup::LoadVerifyActivateChainstate() options.mempool = Assert(m_node.mempool.get()); options.block_tree_db_in_memory = m_block_tree_db_in_memory; options.coins_db_in_memory = m_coins_db_in_memory; - options.dash_dbs_in_memory = true; + options.dash_dbs_in_memory = m_dash_dbs_in_memory; options.reindex = node::fReindex; options.reindex_chainstate = m_args.GetBoolArg("-reindex-chainstate", false); options.prune = node::fPruneMode; @@ -370,8 +371,9 @@ TestingSetup::TestingSetup( const std::string& chainName, const std::vector& extra_args, const bool coins_db_in_memory, - const bool block_tree_db_in_memory) - : ChainTestingSetup(chainName, extra_args) + const bool block_tree_db_in_memory, + const bool dash_dbs_in_memory) + : ChainTestingSetup(chainName, extra_args, dash_dbs_in_memory) { m_coins_db_in_memory = coins_db_in_memory; m_block_tree_db_in_memory = block_tree_db_in_memory; @@ -445,8 +447,9 @@ TestChain100Setup::TestChain100Setup( const std::string& chain_name, const std::vector& extra_args, const bool coins_db_in_memory, - const bool block_tree_db_in_memory) - : TestChainSetup{100, chain_name, extra_args, coins_db_in_memory, block_tree_db_in_memory} + const bool block_tree_db_in_memory, + const bool dash_dbs_in_memory) + : TestChainSetup{100, chain_name, extra_args, coins_db_in_memory, block_tree_db_in_memory, dash_dbs_in_memory} { } @@ -455,8 +458,9 @@ TestChainSetup::TestChainSetup( const std::string& chain_name, const std::vector& extra_args, const bool coins_db_in_memory, - const bool block_tree_db_in_memory) - : TestingSetup{chain_name, extra_args, coins_db_in_memory, block_tree_db_in_memory} + const bool block_tree_db_in_memory, + const bool dash_dbs_in_memory) + : TestingSetup{chain_name, extra_args, coins_db_in_memory, block_tree_db_in_memory, dash_dbs_in_memory} { SetMockTime(1598887952); constexpr std::array vchKey = { diff --git a/src/test/util/setup_common.h b/src/test/util/setup_common.h index 8001b84248df..252ec92d5c35 100644 --- a/src/test/util/setup_common.h +++ b/src/test/util/setup_common.h @@ -96,11 +96,14 @@ void DashChainstateSetupClose(node::NodeContext& node); struct BasicTestingSetup { node::NodeContext m_node; // keep as first member to be destructed last - explicit BasicTestingSetup(const std::string& chainName = CBaseChainParams::MAIN, const std::vector& extra_args = {}); + explicit BasicTestingSetup(const std::string& chainName = CBaseChainParams::MAIN, + const std::vector& extra_args = {}, + bool dash_dbs_in_memory = true); ~BasicTestingSetup(); const fs::path m_path_root; ArgsManager m_args; + const bool m_dash_dbs_in_memory; }; @@ -113,7 +116,9 @@ struct ChainTestingSetup : public BasicTestingSetup { bool m_coins_db_in_memory{true}; bool m_block_tree_db_in_memory{true}; - explicit ChainTestingSetup(const std::string& chainName = CBaseChainParams::MAIN, const std::vector& extra_args = {}); + explicit ChainTestingSetup(const std::string& chainName = CBaseChainParams::MAIN, + const std::vector& extra_args = {}, + bool dash_dbs_in_memory = true); ~ChainTestingSetup(); // Supplies a chainstate, if one is needed @@ -127,7 +132,8 @@ struct TestingSetup : public ChainTestingSetup { const std::string& chainName = CBaseChainParams::MAIN, const std::vector& extra_args = {}, const bool coins_db_in_memory = true, - const bool block_tree_db_in_memory = true); + const bool block_tree_db_in_memory = true, + const bool dash_dbs_in_memory = true); ~TestingSetup(); }; @@ -147,7 +153,8 @@ struct TestChainSetup : public TestingSetup const std::string& chain_name = CBaseChainParams::REGTEST, const std::vector& extra_args = {}, const bool coins_db_in_memory = true, - const bool block_tree_db_in_memory = true); + const bool block_tree_db_in_memory = true, + const bool dash_dbs_in_memory = true); ~TestChainSetup(); /** @@ -225,7 +232,8 @@ struct TestChain100Setup : public TestChainSetup { const std::string& chain_name = CBaseChainParams::REGTEST, const std::vector& extra_args = {}, const bool coins_db_in_memory = true, - const bool block_tree_db_in_memory = true); + const bool block_tree_db_in_memory = true, + const bool dash_dbs_in_memory = true); }; /** diff --git a/src/test/validation_chainstatemanager_tests.cpp b/src/test/validation_chainstatemanager_tests.cpp index 19298329f03d..b986abd35a5f 100644 --- a/src/test/validation_chainstatemanager_tests.cpp +++ b/src/test/validation_chainstatemanager_tests.cpp @@ -33,6 +33,18 @@ using node::SnapshotMetadata; +namespace { + +void SeedSnapshotMarker(CEvoDB& evodb, const uint256& hash) +{ + auto tx = evodb.BeginTransaction(EvoDbIdentity::SNAPSHOT); + evodb.WriteBestBlock(EvoDbIdentity::SNAPSHOT, hash); + tx->Commit(); + BOOST_REQUIRE(evodb.CommitRootTransaction(EvoDbIdentity::SNAPSHOT)); +} + +} // namespace + BOOST_FIXTURE_TEST_SUITE(validation_chainstatemanager_tests, ChainTestingSetup) //! Basic tests for ChainstateManager. @@ -81,8 +93,11 @@ BOOST_AUTO_TEST_CASE(chainstatemanager) // Create a snapshot-based chainstate. // const uint256 snapshot_blockhash = GetRandHash(); - Chainstate& c2 = WITH_LOCK(::cs_main, return manager.ActivateExistingSnapshot( + SeedSnapshotMarker(evodb, snapshot_blockhash); + Chainstate* c2_ptr = WITH_LOCK(::cs_main, return manager.ActivateExistingSnapshot( &mempool, snapshot_blockhash)); + BOOST_REQUIRE(c2_ptr); + Chainstate& c2 = *c2_ptr; chainstates.push_back(&c2); DashChainstateSetup(manager, m_node, /*llmq_dbs_in_memory=*/true, /*llmq_dbs_wipe=*/false); @@ -158,7 +173,11 @@ BOOST_AUTO_TEST_CASE(chainstatemanager_rebalance_caches) // Create a snapshot-based chainstate. // - Chainstate& c2 = WITH_LOCK(cs_main, return manager.ActivateExistingSnapshot(&mempool, GetRandHash())); + const uint256 snapshot_blockhash = GetRandHash(); + SeedSnapshotMarker(evodb, snapshot_blockhash); + Chainstate* c2_ptr = WITH_LOCK(cs_main, return manager.ActivateExistingSnapshot(&mempool, snapshot_blockhash)); + BOOST_REQUIRE(c2_ptr); + Chainstate& c2 = *c2_ptr; chainstates.push_back(&c2); c2.InitCoinsDB( /*cache_size_bytes=*/1 << 23, /*in_memory=*/true, /*should_wipe=*/false); @@ -191,6 +210,7 @@ struct SnapshotTestSetup : TestChain100Setup { {}, /*coins_db_in_memory=*/false, /*block_tree_db_in_memory=*/false, + /*dash_dbs_in_memory=*/false, } { } @@ -288,6 +308,15 @@ struct SnapshotTestSetup : TestChain100Setup { Chainstate& snapshot_chainstate = chainman.ActiveChainstate(); + // To be checked against later when we try loading a subsequent snapshot. + uint256 loaded_snapshot_blockhash{*chainman.SnapshotBlockhash()}; + + BOOST_CHECK(m_node.evodb->VerifyBestBlock( + EvoDbIdentity::SNAPSHOT, loaded_snapshot_blockhash)); + BOOST_CHECK(m_node.evodb->VerifyBestBlock( + EvoDbIdentity::NORMAL, loaded_snapshot_blockhash)); + BOOST_CHECK(m_node.evodb->HasDualChainstateMarker()); + { LOCK(::cs_main); @@ -307,9 +336,6 @@ struct SnapshotTestSetup : TestChain100Setup { BOOST_CHECK_EQUAL(tip->nChainTx, au_data.nChainTx); - // To be checked against later when we try loading a subsequent snapshot. - uint256 loaded_snapshot_blockhash{*chainman.SnapshotBlockhash()}; - // Make some assertions about the both chainstates. These checks ensure the // legacy chainstate hasn't changed and that the newly created chainstate // reflects the expected content. @@ -344,6 +370,10 @@ struct SnapshotTestSetup : TestChain100Setup { constexpr size_t new_coins{100}; mineBlocks(new_coins); // Defined in TestChain100Setup. + const uint256 snapshot_tip = WITH_LOCK(chainman.GetMutex(), return chainman.ActiveTip()->GetBlockHash()); + BOOST_CHECK(m_node.evodb->VerifyBestBlock(EvoDbIdentity::SNAPSHOT, snapshot_tip)); + BOOST_CHECK(m_node.evodb->VerifyBestBlock(EvoDbIdentity::NORMAL, loaded_snapshot_blockhash)); + { LOCK(::cs_main); size_t coins_in_active{0}; @@ -420,6 +450,65 @@ struct SnapshotTestSetup : TestChain100Setup { } }; +struct SnapshotReindexTestSetup : ChainTestingSetup { + const bool m_old_reindex{node::fReindex}; + + SnapshotReindexTestSetup() : + ChainTestingSetup{CBaseChainParams::REGTEST, {}, /*dash_dbs_in_memory=*/false} + { + m_coins_db_in_memory = false; + m_block_tree_db_in_memory = false; + } + + ~SnapshotReindexTestSetup() { node::fReindex = m_old_reindex; } + + void CheckSnapshotReindexLoad(bool reindex, bool reindex_chainstate) + { + ChainstateManager& chainman = *Assert(m_node.chainman); + const uint256 base_blockhash = GetRandHash(); + { + LOCK(::cs_main); + chainman.InitializeChainstate(m_node.mempool.get(), *m_node.evodb, m_node.chain_helper); + SeedSnapshotMarker(*m_node.evodb, base_blockhash); + Chainstate* snapshot_chainstate = chainman.ActivateExistingSnapshot(m_node.mempool.get(), base_blockhash); + BOOST_REQUIRE(snapshot_chainstate); + snapshot_chainstate->InitCoinsDB( + /*cache_size_bytes=*/1 << 20, /*in_memory=*/false, /*should_wipe=*/true); + BOOST_REQUIRE(node::WriteSnapshotBaseBlockhash(*snapshot_chainstate)); + chainman.ResetChainstates(); + } + BOOST_REQUIRE(node::FindSnapshotChainstateDir()); + + node::ChainstateLoadOptions options; + options.mempool = Assert(m_node.mempool.get()); + options.block_tree_db_in_memory = m_block_tree_db_in_memory; + options.coins_db_in_memory = m_coins_db_in_memory; + options.dash_dbs_in_memory = m_dash_dbs_in_memory; + options.reindex = reindex; + options.reindex_chainstate = reindex_chainstate; + options.bls_threads = llmq::DEFAULT_BLSCHECK_THREADS; + options.worker_count = llmq::DEFAULT_WORKER_COUNT; + options.max_recsigs_age = llmq::DEFAULT_MAX_RECOVERED_SIGS_AGE; + + const auto [status, error] = node::LoadChainstate(chainman, *Assert(m_node.mn_metaman.get()), + *Assert(m_node.sporkman.get()), *Assert(m_node.chainlocks.get()), + *Assert(m_node.mn_sync.get()), m_node.chain_helper, + m_node.dmnman, m_node.evodb, m_node.llmq_ctx, + Assert(m_node.args)->GetDataDirNet(), m_cache_sizes, options); + BOOST_REQUIRE_MESSAGE(status == node::ChainstateLoadStatus::SUCCESS, error.original); + BOOST_CHECK(!node::FindSnapshotChainstateDir()); + BOOST_CHECK(!chainman.IsSnapshotActive()); + BOOST_CHECK_EQUAL(chainman.GetAll().size(), 1); + BOOST_CHECK(!chainman.ActiveChainstate().m_from_snapshot_blockhash); + + uint256 snapshot_marker; + BOOST_CHECK(!m_node.evodb->ReadBestBlock(EvoDbIdentity::SNAPSHOT, snapshot_marker)); + + DashChainstateSetupClose(m_node); + WITH_LOCK(::cs_main, chainman.ResetChainstates()); + } +}; + //! Test basic snapshot activation. BOOST_FIXTURE_TEST_CASE(chainstatemanager_activate_snapshot, SnapshotTestSetup) { @@ -487,8 +576,12 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager_loadblockindex, TestChain100Setup) BOOST_CHECK_EQUAL(expected_assumed_valid, num_assumed_valid); - Chainstate& cs2 = WITH_LOCK(::cs_main, - return chainman.ActivateExistingSnapshot(&mempool, GetRandHash())); + const uint256 snapshot_blockhash = GetRandHash(); + SeedSnapshotMarker(*m_node.evodb, snapshot_blockhash); + Chainstate* cs2_ptr = WITH_LOCK(::cs_main, + return chainman.ActivateExistingSnapshot(&mempool, snapshot_blockhash)); + BOOST_REQUIRE(cs2_ptr); + Chainstate& cs2 = *cs2_ptr; reload_all_block_indexes(); @@ -502,6 +595,22 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager_loadblockindex, TestChain100Setup) BOOST_CHECK_EQUAL(cs2.setBlockIndexCandidates.count(validated_tip), 1); BOOST_CHECK_EQUAL(cs2.setBlockIndexCandidates.count(assumed_tip), 1); BOOST_CHECK_EQUAL(cs2.setBlockIndexCandidates.size(), num_indexes); + + // -reindex-chainstate removes the snapshot before rebuilding coins. Its + // block-index load must therefore clear snapshot-only metadata before + // enforcing the single-chainstate invariants. + { + LOCK(::cs_main); + BOOST_REQUIRE(chainman.DeleteSnapshotChainstate()); + cs1.UnloadBlockIndex(); + BOOST_REQUIRE(chainman.LoadBlockIndex(/*reset_assumed_valid=*/true)); + BOOST_CHECK_EQUAL(chainman.GetAll().size(), 1); + for (int i = assumed_valid_start_idx; i < last_assumed_valid_idx; ++i) { + CBlockIndex* index = cs1.m_chain[i]; + BOOST_CHECK(!index->IsAssumedValid()); + BOOST_CHECK_EQUAL(index->nTx, 0); + } + } } //! Ensure that snapshot chainstates initialize properly when found on disk. @@ -565,4 +674,41 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager_snapshot_init, SnapshotTestSetup) } } +BOOST_FIXTURE_TEST_CASE(chainstatemanager_snapshot_init_missing_evodb_marker, SnapshotTestSetup) +{ + this->SetupSnapshot(); + + { + auto tx = m_node.evodb->BeginTransaction(EvoDbIdentity::SNAPSHOT); + m_node.evodb->Erase(std::make_pair(EVODB_BEST_BLOCK, uint8_t{1})); + tx->Commit(); + } + BOOST_REQUIRE(m_node.evodb->CommitRootTransaction(EvoDbIdentity::SNAPSHOT)); + + ChainstateManager& restarted = this->SimulateNodeRestart(); + WITH_LOCK(::cs_main, restarted.InitializeChainstate( + m_node.mempool.get(), *m_node.evodb, m_node.chain_helper)); + + bilingual_str error; + BOOST_CHECK(!WITH_LOCK(::cs_main, return restarted.DetectSnapshotChainstate(m_node.mempool.get(), error))); + BOOST_CHECK(error.original.find("Snapshot chainstate EvoDB marker") != std::string::npos); + + WITH_LOCK(::cs_main, restarted.ResetChainstates()); + fs::remove_all(gArgs.GetDataDirNet() / "chainstate_snapshot"); + this->LoadVerifyActivateChainstate(); + g_txindex = std::make_unique(1 << 20, /*memory=*/true); + BOOST_REQUIRE(g_txindex->Start(restarted.ActiveChainstate())); + IndexWaitSynced(*g_txindex); +} + +BOOST_FIXTURE_TEST_CASE(chainstatemanager_snapshot_init_reindex_missing_evodb_marker, SnapshotReindexTestSetup) +{ + CheckSnapshotReindexLoad(/*reindex=*/true, /*reindex_chainstate=*/false); +} + +BOOST_FIXTURE_TEST_CASE(chainstatemanager_snapshot_init_reindex_chainstate_missing_evodb_marker, SnapshotReindexTestSetup) +{ + CheckSnapshotReindexLoad(/*reindex=*/false, /*reindex_chainstate=*/true); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/src/validation.cpp b/src/validation.cpp index d1d9e1d6f660..9227c10d2612 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -1619,6 +1619,19 @@ Chainstate::Chainstate(CTxMemPool* mempool, m_chainman(chainman), m_from_snapshot_blockhash(from_snapshot_blockhash) {} +::EvoDbIdentity Chainstate::EvoDbIdentity() const +{ + return m_from_snapshot_blockhash ? ::EvoDbIdentity::SNAPSHOT : ::EvoDbIdentity::NORMAL; +} + +std::string Chainstate::EvoDbInconsistencyMessage() +{ + if (m_chainman.GetAll().size() == 1 && m_evoDb.HasDualChainstateMarker()) { + return "Found EvoDB inconsistency after a previous dual-chainstate run; you must reindex to continue"; + } + return "Found EvoDB inconsistency, you must reindex to continue"; +} + void Chainstate::InitCoinsDB( size_t cache_size_bytes, bool in_memory, @@ -1959,9 +1972,9 @@ DisconnectResult Chainstate::DisconnectBlock(const CBlock& block, const CBlockIn assert(m_chain_helper); bool fDIP0003Active = DeploymentActiveAt(*pindex, m_params.GetConsensus(), Consensus::DEPLOYMENT_DIP0003); - if (fDIP0003Active && !m_evoDb.VerifyBestBlock(pindex->GetBlockHash())) { + if (fDIP0003Active && !m_evoDb.VerifyBestBlock(EvoDbIdentity(), pindex->GetBlockHash())) { // Nodes that upgraded after DIP3 activation will have to reindex to ensure evodb consistency - AbortNode("Found EvoDB inconsistency, you must reindex to continue"); + AbortNode(EvoDbInconsistencyMessage()); return DISCONNECT_FAILED; } @@ -2037,7 +2050,7 @@ DisconnectResult Chainstate::DisconnectBlock(const CBlock& block, const CBlockIn // move best block pointer to prevout block view.SetBestBlock(pindex->pprev->GetBlockHash()); - m_evoDb.WriteBestBlock(pindex->pprev->GetBlockHash()); + m_evoDb.WriteBestBlock(EvoDbIdentity(), pindex->pprev->GetBlockHash()); if (mnlist_updates_opt.has_value()) { auto& mnlu = mnlist_updates_opt.value(); @@ -2192,9 +2205,9 @@ bool Chainstate::ConnectBlock(const CBlock& block, BlockValidationState& state, if (pindex->pprev) { bool fDIP0003Active = DeploymentActiveAt(*pindex, m_params.GetConsensus(), Consensus::DEPLOYMENT_DIP0003); - if (fDIP0003Active && !m_evoDb.VerifyBestBlock(pindex->pprev->GetBlockHash())) { + if (fDIP0003Active && !m_evoDb.VerifyBestBlock(EvoDbIdentity(), pindex->pprev->GetBlockHash())) { // Nodes that upgraded after DIP3 activation will have to reindex to ensure evodb consistency - return AbortNode(state, "Found EvoDB inconsistency, you must reindex to continue"); + return AbortNode(state, EvoDbInconsistencyMessage()); } } nBlocksTotal++; @@ -2502,7 +2515,7 @@ bool Chainstate::ConnectBlock(const CBlock& block, BlockValidationState& state, // add this block to the view's block chain view.SetBestBlock(pindex->GetBlockHash()); - m_evoDb.WriteBestBlock(pindex->GetBlockHash()); + m_evoDb.WriteBestBlock(EvoDbIdentity(), pindex->GetBlockHash()); // Block is committed: keep the scheme it switched to (fJustCheck dry runs returned above). bls_scheme_guard.Commit(); @@ -2687,7 +2700,7 @@ bool Chainstate::FlushStateToDisk( } { LOG_TIME_SECONDS("write evodb cache to disk"); - if (!m_evoDb.CommitRootTransaction()) { + if (!m_evoDb.CommitRootTransaction(EvoDbIdentity())) { return AbortNode(state, "Failed to commit EvoDB"); } } @@ -2848,7 +2861,7 @@ bool Chainstate::DisconnectTip(BlockValidationState& state, DisconnectedBlockTra // Apply the block atomically to the chain state. int64_t nStart = GetTimeMicros(); { - auto dbTx = m_evoDb.BeginTransaction(); + auto dbTx = m_evoDb.BeginTransaction(EvoDbIdentity()); CCoinsViewCache view(&CoinsTip()); assert(view.GetBestBlock() == pindexDelete->GetBlockHash()); @@ -2995,7 +3008,7 @@ bool Chainstate::ConnectTip(BlockValidationState& state, CBlockIndex* pindexNew, // nBlocksTotal may be zero until the ConnectBlock() call below. LogPrint(BCLog::BENCHMARK, " - Load block from disk: %.2fms\n", (nTime2 - nTime1) * MILLI); { - auto dbTx = m_evoDb.BeginTransaction(); + auto dbTx = m_evoDb.BeginTransaction(EvoDbIdentity()); CCoinsViewCache view(&CoinsTip()); bool rv = ConnectBlock(blockConnecting, state, pindexNew, view); @@ -4382,7 +4395,7 @@ bool TestBlockValidity(BlockValidationState& state, indexDummy.phashBlock = &block_hash; // begin tx and let it rollback - auto dbTx = evoDb.BeginTransaction(); + auto dbTx = evoDb.BeginTransaction(chainstate.EvoDbIdentity()); // NOTE: CheckBlockHeader is called by CheckBlock if (!ContextualCheckBlockHeader(block, state, chainstate.m_blockman, chainstate.m_chainman, pindexPrev, adjusted_time_callback())) @@ -4470,7 +4483,7 @@ bool CVerifyDB::VerifyDB( ScopedBLSLegacyScheme bls_scheme_guard; // begin tx and let it rollback - auto dbTx = evoDb.BeginTransaction(); + auto dbTx = evoDb.BeginTransaction(chainstate.EvoDbIdentity()); // Verify blocks in the best chain if (nCheckDepth <= 0 || nCheckDepth > chainstate.m_chain.Height()) { @@ -4651,12 +4664,12 @@ bool Chainstate::ReplayBlocks() pindexFork = LastCommonAncestor(pindexOld, pindexNew); assert(pindexFork != nullptr); const bool fDIP0003Active = DeploymentActiveAt(*pindexOld, m_params.GetConsensus(), Consensus::DEPLOYMENT_DIP0003); - if (fDIP0003Active && !m_evoDb.VerifyBestBlock(pindexOld->GetBlockHash())) { - return error("ReplayBlocks(DASH): Found EvoDB inconsistency"); + if (fDIP0003Active && !m_evoDb.VerifyBestBlock(EvoDbIdentity(), pindexOld->GetBlockHash())) { + return error("ReplayBlocks(DASH): %s", EvoDbInconsistencyMessage()); } } - auto dbTx = m_evoDb.BeginTransaction(); + auto dbTx = m_evoDb.BeginTransaction(EvoDbIdentity()); // Rollback along the old branch. while (pindexOld != pindexFork) { @@ -4689,7 +4702,7 @@ bool Chainstate::ReplayBlocks() } cache.SetBestBlock(pindexNew->GetBlockHash()); - m_evoDb.WriteBestBlock(pindexNew->GetBlockHash()); + m_evoDb.WriteBestBlock(EvoDbIdentity(), pindexNew->GetBlockHash()); bool flushed = cache.Flush(); assert(flushed); dbTx->Commit(); @@ -4704,7 +4717,7 @@ void Chainstate::UnloadBlockIndex() setBlockIndexCandidates.clear(); } -bool ChainstateManager::LoadBlockIndex() +bool ChainstateManager::LoadBlockIndex(bool reset_assumed_valid) { AssertLockHeld(cs_main); // Load block index from databases @@ -4719,6 +4732,42 @@ bool ChainstateManager::LoadBlockIndex() std::sort(vSortedByHeight.begin(), vSortedByHeight.end(), CBlockIndexHeightOnlyComparator()); + if (reset_assumed_valid) { + // The snapshot chainstate is being removed for -reindex-chainstate, + // so metadata faked solely for snapshot use must not survive. + for (CBlockIndex* index : vSortedByHeight) { + if (!index->IsAssumedValid()) continue; + + index->nStatus &= ~BLOCK_ASSUMED_VALID; + if (!index->IsValid(BLOCK_VALID_TRANSACTIONS)) { + index->nTx = 0; + } + m_blockman.m_dirty_blockindex.insert(index); + } + + // Recompute chain transaction counts after removing fake nTx values, + // including descendants and forks whose linkage depended on them. + m_blockman.m_blocks_unlinked.clear(); + for (CBlockIndex* index : vSortedByHeight) { + const unsigned int old_chain_tx{index->nChainTx}; + if (index->nTx > 0 && (!index->pprev || index->pprev->nChainTx > 0)) { + index->nChainTx = (index->pprev ? index->pprev->nChainTx : 0) + index->nTx; + } else { + index->nChainTx = 0; + if (index->nTx > 0 && index->pprev) { + m_blockman.m_blocks_unlinked.emplace(index->pprev, index); + } + } + if (index->nChainTx != old_chain_tx) { + m_blockman.m_dirty_blockindex.insert(index); + } + } + + // Persist the cleanup before deleting the snapshot directory so a + // failed write leaves startup able to retry it. + if (!m_blockman.WriteBlockIndexDB()) return false; + } + // Find start of assumed-valid region. int first_assumed_valid_height = std::numeric_limits::max(); @@ -5341,6 +5390,7 @@ Chainstate& ChainstateManager::InitializeChainstate(CTxMemPool* mempool, m_ibd_chainstate = std::make_unique( mempool, m_blockman, *this, evoDb, chain_helper); m_active_chainstate = m_ibd_chainstate.get(); + evoDb.SetActiveChainstateIdentity(EvoDbIdentity::NORMAL); return *m_active_chainstate; } @@ -5356,7 +5406,7 @@ const AssumeutxoData* ExpectedAssumeutxo( return nullptr; } -static bool DeleteCoinsDBFromDisk(const fs::path db_path, bool is_snapshot) +[[nodiscard]] static bool DeleteCoinsDBFromDisk(const fs::path db_path, bool is_snapshot) EXCLUSIVE_LOCKS_REQUIRED(::cs_main) { AssertLockHeld(::cs_main); @@ -5496,6 +5546,7 @@ bool ChainstateManager::ActivateSnapshot( assert(chaintip_loaded); m_active_chainstate = m_snapshot_chainstate.get(); + m_active_chainstate->m_evoDb.SetActiveChainstateIdentity(EvoDbIdentity::SNAPSHOT); LogPrintf("[snapshot] successfully activated snapshot %s\n", base_blockhash.ToString()); LogPrintf("[snapshot] (%.2f MB)\n", @@ -5703,6 +5754,17 @@ bool ChainstateManager::PopulateAndValidateSnapshot( index->nChainTx = au_data.nChainTx; snapshot_chainstate.setBlockIndexCandidates.insert(snapshot_start_block); + { + auto db_tx = snapshot_chainstate.m_evoDb.BeginTransaction(EvoDbIdentity::SNAPSHOT); + snapshot_chainstate.m_evoDb.WriteBestBlock(EvoDbIdentity::SNAPSHOT, base_blockhash); + snapshot_chainstate.m_evoDb.WriteDualChainstateMarker(); + db_tx->Commit(); + } + if (!snapshot_chainstate.m_evoDb.CommitRootTransaction(EvoDbIdentity::SNAPSHOT)) { + LogPrintf("[snapshot] failed to commit snapshot EvoDB marker\n"); + return false; + } + LogPrintf("[snapshot] validated snapshot (%.2f MB)\n", coins_cache.DynamicMemoryUsage() / (1000 * 1000)); return true; @@ -5809,6 +5871,26 @@ void ChainstateManager::ResetChainstates() m_active_chainstate = nullptr; } +bool ChainstateManager::DeleteSnapshotChainstate() +{ + AssertLockHeld(::cs_main); + assert(m_ibd_chainstate); + + // Reindex startup deliberately skips snapshot activation because its EvoDB + // marker has already been wiped, so use the discovered on-disk directory. + if (const auto snapshot_datadir = node::FindSnapshotChainstateDir()) { + if (!DeleteCoinsDBFromDisk(*snapshot_datadir, /*is_snapshot=*/true)) { + LogPrintf("Deletion of %s failed. Please remove it manually to continue reindexing.\n", + fs::PathToString(*snapshot_datadir)); + return false; + } + } + m_active_chainstate = m_ibd_chainstate.get(); + m_active_chainstate->m_evoDb.SetActiveChainstateIdentity(EvoDbIdentity::NORMAL); + m_snapshot_chainstate.reset(); + return true; +} + ChainstateManager::~ChainstateManager() { LOCK(::cs_main); @@ -5828,35 +5910,46 @@ bool IsBIP30Unspendable(const CBlockIndex& block_index) (block_index.nHeight==91812 && block_index.GetBlockHash() == uint256S("0x00000000000af0aed4792b1acee3d966af36cf5def14935db8de83d6f9306f2f")); } -bool ChainstateManager::DetectSnapshotChainstate(CTxMemPool* mempool) +bool ChainstateManager::DetectSnapshotChainstate(CTxMemPool* mempool, bilingual_str& error) { assert(!m_snapshot_chainstate); std::optional path = node::FindSnapshotChainstateDir(); if (!path) { - return false; + return true; } std::optional base_blockhash = node::ReadSnapshotBaseBlockhash(*path); if (!base_blockhash) { - return false; + return true; } LogPrintf("[snapshot] detected active snapshot chainstate (%s) - loading\n", fs::PathToString(*path)); - this->ActivateExistingSnapshot(mempool, *base_blockhash); + if (!this->ActivateExistingSnapshot(mempool, *base_blockhash)) { + error = _("Snapshot chainstate EvoDB marker is missing. Reindex is required."); + return false; + } return true; } -Chainstate& ChainstateManager::ActivateExistingSnapshot(CTxMemPool* mempool, uint256 base_blockhash) +Chainstate* ChainstateManager::ActivateExistingSnapshot(CTxMemPool* mempool, uint256 base_blockhash) { assert(!m_snapshot_chainstate); + CEvoDB& evo_db = this->ActiveChainstate().m_evoDb; + uint256 snapshot_evo_tip; + if (!evo_db.ReadBestBlock(EvoDbIdentity::SNAPSHOT, snapshot_evo_tip)) { + LogPrintf("[snapshot] snapshot EvoDB marker is missing for base block %s\n", + base_blockhash.ToString()); + return nullptr; + } m_snapshot_chainstate = std::make_unique( mempool, m_blockman, *this, - this->ActiveChainstate().m_evoDb, + evo_db, this->ActiveChainstate().m_chain_helper, base_blockhash); LogPrintf("[snapshot] switching active chainstate to %s\n", m_snapshot_chainstate->ToString()); m_active_chainstate = m_snapshot_chainstate.get(); - return *m_snapshot_chainstate; + evo_db.SetActiveChainstateIdentity(EvoDbIdentity::SNAPSHOT); + return m_snapshot_chainstate.get(); } ChainstateRole Chainstate::GetRole() const diff --git a/src/validation.h b/src/validation.h index 112d37ee12dc..35aa56f78f68 100644 --- a/src/validation.h +++ b/src/validation.h @@ -57,6 +57,7 @@ class CTxMemPool; class TxValidationState; class CChainstateHelper; class ChainstateManager; +enum class EvoDbIdentity; struct PrecomputedTransactionData; struct ChainTxData; struct DisconnectedBlockTransactions; @@ -542,6 +543,11 @@ class Chainstate //! @sa ChainstateRole ChainstateRole GetRole() const EXCLUSIVE_LOCKS_REQUIRED(::cs_main); + //! Return the stable EvoDB identity corresponding to this chainstate's coins DB. + ::EvoDbIdentity EvoDbIdentity() const; + + std::string EvoDbInconsistencyMessage(); + /** * Initialize the CoinsViews UTXO set database management data structures. The in-memory * cache is initialized separately. @@ -1095,8 +1101,10 @@ class ChainstateManager [[nodiscard]] MempoolAcceptResult ProcessTransaction(const CTransactionRef& tx, bool test_accept=false, bool bypass_limits=false) EXCLUSIVE_LOCKS_REQUIRED(cs_main); - //! Load the block tree and coins database from disk, initializing state if we're running with -reindex - bool LoadBlockIndex() EXCLUSIVE_LOCKS_REQUIRED(cs_main); + //! Load the block tree and coins database from disk, initializing state if we're running with -reindex. + //! If reset_assumed_valid is true, remove snapshot-only block index metadata before + //! populating the remaining chainstate's candidate set. + bool LoadBlockIndex(bool reset_assumed_valid = false) EXCLUSIVE_LOCKS_REQUIRED(cs_main); //! Check to see if caches are out of balance and if so, call //! ResizeCoinsCaches() as needed. @@ -1108,14 +1116,17 @@ class ChainstateManager //! When starting up, search the datadir for a chainstate based on a UTXO //! snapshot that is in the process of being validated. - bool DetectSnapshotChainstate(CTxMemPool* mempool) EXCLUSIVE_LOCKS_REQUIRED(::cs_main); + bool DetectSnapshotChainstate(CTxMemPool* mempool, bilingual_str& error) EXCLUSIVE_LOCKS_REQUIRED(::cs_main); void ResetChainstates() EXCLUSIVE_LOCKS_REQUIRED(::cs_main); + //! Remove the snapshot-based chainstate and all on-disk artifacts. + //! Used when reindex{-chainstate} is called during snapshot use. + [[nodiscard]] bool DeleteSnapshotChainstate() EXCLUSIVE_LOCKS_REQUIRED(::cs_main); + //! Switch the active chainstate to one based on a UTXO snapshot that was loaded //! previously. - Chainstate& ActivateExistingSnapshot(CTxMemPool* mempool, uint256 base_blockhash) - EXCLUSIVE_LOCKS_REQUIRED(::cs_main); + Chainstate* ActivateExistingSnapshot(CTxMemPool* mempool, uint256 base_blockhash) EXCLUSIVE_LOCKS_REQUIRED(::cs_main); ~ChainstateManager(); }; From 7952a50d9ff52e50b7776fd52ff1f2b50bbcbb55 Mon Sep 17 00:00:00 2001 From: Pasta Date: Fri, 10 Jul 2026 23:28:50 -0500 Subject: [PATCH 05/10] test: add dual-chainstate EvoDB consistency coverage --- src/Makefile.test.include | 1 + src/test/evo_db_tests.cpp | 197 ++++++++++++++++++ .../validation_chainstatemanager_tests.cpp | 98 ++++++++- 3 files changed, 291 insertions(+), 5 deletions(-) create mode 100644 src/test/evo_db_tests.cpp diff --git a/src/Makefile.test.include b/src/Makefile.test.include index e98111bed99c..3c25d47d8597 100644 --- a/src/Makefile.test.include +++ b/src/Makefile.test.include @@ -110,6 +110,7 @@ BITCOIN_TESTS =\ test/dynamic_activation_thresholds_tests.cpp \ test/evo_assetlocks_tests.cpp \ test/evo_cbtx_tests.cpp \ + test/evo_db_tests.cpp \ test/evo_deterministicmns_tests.cpp \ test/evo_islock_tests.cpp \ test/evo_mnhf_tests.cpp \ diff --git a/src/test/evo_db_tests.cpp b/src/test/evo_db_tests.cpp new file mode 100644 index 000000000000..1082e219a624 --- /dev/null +++ b/src/test/evo_db_tests.cpp @@ -0,0 +1,197 @@ +// 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. + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +namespace { + +using Payload = std::vector; + +uint256 BlockHash(uint32_t height) +{ + return uint256S(strprintf("%064x", height)); +} + +auto PayloadKey(uint32_t height) +{ + return std::make_pair(std::string{"test_evo_payload"}, BlockHash(height)); +} + +Payload PayloadFor(uint32_t height) +{ + return {static_cast(height), static_cast(height >> 8)}; +} + +void WritePayload(CEvoDB& db, EvoDbIdentity identity, uint32_t height) +{ + auto tx = db.BeginTransaction(identity); + db.Write(PayloadKey(height), PayloadFor(height)); + tx->Commit(); +} + +void WriteMarker(CEvoDB& db, EvoDbIdentity identity, const uint256& hash) +{ + auto tx = db.BeginTransaction(identity); + db.WriteBestBlock(identity, hash); + tx->Commit(); +} + +} // namespace + +BOOST_FIXTURE_TEST_SUITE(evo_db_tests, BasicTestingSetup) + +BOOST_AUTO_TEST_CASE(own_overlay_tombstone) +{ + CEvoDB db{util::DbWrapperParams{.path = m_args.GetDataDirBase() / "evodb_tombstone", .memory = true, .wipe = true}}; + const auto key = PayloadKey(1); + + WritePayload(db, EvoDbIdentity::NORMAL, 1); + BOOST_REQUIRE(db.CommitRootTransaction(EvoDbIdentity::NORMAL)); + + { + auto tx = db.BeginTransaction(EvoDbIdentity::NORMAL); + db.Erase(key); + tx->Commit(); + } + { + auto tx = db.BeginTransaction(EvoDbIdentity::NORMAL); + Payload value; + BOOST_CHECK(!db.Read(key, value)); + } + { + auto tx = db.BeginTransaction(EvoDbIdentity::SNAPSHOT); + Payload value; + BOOST_REQUIRE(db.Read(key, value)); + BOOST_CHECK(value == PayloadFor(1)); + } +} + +BOOST_AUTO_TEST_CASE(active_chainstate_read_identity) +{ + CEvoDB db{util::DbWrapperParams{.path = m_args.GetDataDirBase() / "evodb_read_identity", .memory = true, .wipe = true}}; + const auto key = PayloadKey(2); + + WritePayload(db, EvoDbIdentity::SNAPSHOT, 2); + + Payload value; + BOOST_CHECK(!db.Read(key, value)); + + db.SetActiveChainstateIdentity(EvoDbIdentity::SNAPSHOT); + BOOST_REQUIRE(db.Read(key, value)); + BOOST_CHECK(value == PayloadFor(2)); + + { + auto tx = db.BeginTransaction(EvoDbIdentity::NORMAL); + BOOST_CHECK(!db.Read(key, value)); + } + + BOOST_REQUIRE(db.Read(key, value)); + BOOST_CHECK(value == PayloadFor(2)); +} + +BOOST_AUTO_TEST_CASE(write_derived_verifies_other_unflushed_overlay) +{ + const fs::path path = m_args.GetDataDirBase() / "evodb_derived_overlay"; + const auto key = PayloadKey(2); + const auto payload = PayloadFor(2); + Payload mismatch = payload; + mismatch.push_back(0xff); + + { + CEvoDB db{util::DbWrapperParams{.path = path, .memory = false, .wipe = true}}; + { + auto tx = db.BeginTransaction(EvoDbIdentity::NORMAL); + BOOST_REQUIRE(db.WriteDerived(key, payload)); + tx->Commit(); + } + { + auto tx = db.BeginTransaction(EvoDbIdentity::SNAPSHOT); + BOOST_CHECK(!db.WriteDerived(key, mismatch)); + BOOST_REQUIRE(db.WriteDerived(key, payload)); + tx->Commit(); + } + BOOST_REQUIRE(db.CommitRootTransaction(EvoDbIdentity::SNAPSHOT)); + // Destroying db drops NORMAL's unflushed context. The reopened value + // therefore proves SNAPSHOT's identical overlap did not suppress its write. + } + + CEvoDB reloaded{util::DbWrapperParams{.path = path, .memory = false, .wipe = false}}; + Payload value; + BOOST_REQUIRE(reloaded.Read(key, value)); + BOOST_CHECK(value == payload); +} + +BOOST_AUTO_TEST_CASE(write_derived_rejects_disk_mismatch) +{ + CEvoDB db{util::DbWrapperParams{.path = m_args.GetDataDirBase() / "evodb_derived_mismatch", .memory = true, .wipe = true}}; + const auto key = PayloadKey(3); + + WritePayload(db, EvoDbIdentity::NORMAL, 3); + BOOST_REQUIRE(db.CommitRootTransaction(EvoDbIdentity::NORMAL)); + + auto tx = db.BeginTransaction(EvoDbIdentity::SNAPSHOT); + Payload mismatch = PayloadFor(3); + mismatch.push_back(0xff); + BOOST_CHECK(!db.WriteDerived(key, mismatch)); +} + +BOOST_AUTO_TEST_CASE(marker_flush_independence) +{ + const fs::path path = m_args.GetDataDirBase() / "evodb_markers"; + { + CEvoDB db{util::DbWrapperParams{.path = path, .memory = false, .wipe = true}}; + WriteMarker(db, EvoDbIdentity::NORMAL, BlockHash(10)); + WriteMarker(db, EvoDbIdentity::SNAPSHOT, BlockHash(100)); + BOOST_REQUIRE(db.CommitRootTransaction(EvoDbIdentity::NORMAL)); + BOOST_REQUIRE(db.CommitRootTransaction(EvoDbIdentity::SNAPSHOT)); + + WriteMarker(db, EvoDbIdentity::NORMAL, BlockHash(11)); + WriteMarker(db, EvoDbIdentity::SNAPSHOT, BlockHash(101)); + BOOST_REQUIRE(db.CommitRootTransaction(EvoDbIdentity::NORMAL)); + } + { + CEvoDB db{util::DbWrapperParams{.path = path, .memory = false, .wipe = false}}; + BOOST_CHECK(db.VerifyBestBlock(EvoDbIdentity::NORMAL, BlockHash(11))); + BOOST_CHECK(db.VerifyBestBlock(EvoDbIdentity::SNAPSHOT, BlockHash(100))); + + WriteMarker(db, EvoDbIdentity::NORMAL, BlockHash(12)); + WriteMarker(db, EvoDbIdentity::SNAPSHOT, BlockHash(102)); + BOOST_REQUIRE(db.CommitRootTransaction(EvoDbIdentity::SNAPSHOT)); + } + { + CEvoDB db{util::DbWrapperParams{.path = path, .memory = false, .wipe = false}}; + BOOST_CHECK(db.VerifyBestBlock(EvoDbIdentity::NORMAL, BlockHash(11))); + BOOST_CHECK(db.VerifyBestBlock(EvoDbIdentity::SNAPSHOT, BlockHash(102))); + } +} + +BOOST_AUTO_TEST_CASE(normal_marker_preserves_legacy_key_bytes) +{ + CEvoDB db{util::DbWrapperParams{.path = m_args.GetDataDirBase() / "evodb_legacy_key", .memory = true, .wipe = true}}; + WriteMarker(db, EvoDbIdentity::NORMAL, BlockHash(20)); + BOOST_REQUIRE(db.CommitRootTransaction(EvoDbIdentity::NORMAL)); + + CDataStream expected{SER_DISK, CLIENT_VERSION}; + expected << EVODB_BEST_BLOCK; + std::unique_ptr it{db.GetRawDB().NewIterator()}; + it->SeekToFirst(); + BOOST_REQUIRE(it->Valid()); + const CDataStream actual = it->GetKey(); + BOOST_CHECK_EQUAL_COLLECTIONS(actual.begin(), actual.end(), expected.begin(), expected.end()); + it->Next(); + BOOST_CHECK(!it->Valid()); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/validation_chainstatemanager_tests.cpp b/src/test/validation_chainstatemanager_tests.cpp index b986abd35a5f..8870213da6d1 100644 --- a/src/test/validation_chainstatemanager_tests.cpp +++ b/src/test/validation_chainstatemanager_tests.cpp @@ -410,11 +410,11 @@ struct SnapshotTestSetup : TestChain100Setup { return std::make_tuple(&validation_chainstate, &snapshot_chainstate); } - // Simulate a restart of the node by flushing all state to disk, clearing the - // existing ChainstateManager, and unloading the block index. + // Simulate a restart of the node by optionally flushing all state to disk, + // clearing the existing ChainstateManager, and unloading the block index. // // @returns a reference to the "restarted" ChainstateManager - ChainstateManager& SimulateNodeRestart() + ChainstateManager& SimulateNodeRestart(bool flush_chainstates = true) { ChainstateManager& chainman = *Assert(m_node.chainman); @@ -428,8 +428,10 @@ struct SnapshotTestSetup : TestChain100Setup { g_txindex.reset(); { LOCK(::cs_main); - for (Chainstate* cs : chainman.GetAll()) { - cs->ForceFlushStateToDisk(); + if (flush_chainstates) { + for (Chainstate* cs : chainman.GetAll()) { + cs->ForceFlushStateToDisk(); + } } // Tear down Dash managers connected to the mempool and old chainstate // before LoadVerifyActivateChainstate() recreates them below. @@ -674,6 +676,92 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager_snapshot_init, SnapshotTestSetup) } } +BOOST_FIXTURE_TEST_CASE(chainstatemanager_evodb_reorg_erase_guard, SnapshotTestSetup) +{ + auto [background_chainstate, snapshot_chainstate] = this->SetupSnapshot(); + const auto llmq_type = Consensus::LLMQType::LLMQ_TEST; + const uint256 shared_quorum_hash = GetRandHash(); + const uint256 snapshot_only_quorum_hash = GetRandHash(); + const auto shared_key = std::make_pair(std::string{"q_mc"}, std::make_pair(llmq_type, shared_quorum_hash)); + const auto snapshot_only_key = std::make_pair(std::string{"q_mc"}, std::make_pair(llmq_type, snapshot_only_quorum_hash)); + const CBlockIndex* shared_block; + const CBlockIndex* snapshot_only_block; + { + LOCK(::cs_main); + shared_block = snapshot_chainstate->m_chain[background_chainstate->m_chain.Height()]; + snapshot_only_block = snapshot_chainstate->m_chain[background_chainstate->m_chain.Height() + 1]; + BOOST_REQUIRE(background_chainstate->m_chain.Contains(shared_block)); + BOOST_REQUIRE(!background_chainstate->m_chain.Contains(snapshot_only_block)); + } + + // Constructing a mined quorum commitment through snapshot activation is + // impractical here, so exercise the production erase guard with its real + // second Chainstate and synthetic commitment keys written through EvoDB. + { + auto tx = m_node.evodb->BeginTransaction(EvoDbIdentity::SNAPSHOT); + m_node.evodb->Write(shared_key, shared_block->GetBlockHash()); + m_node.evodb->Write(snapshot_only_key, snapshot_only_block->GetBlockHash()); + tx->Commit(); + } + BOOST_REQUIRE(m_node.evodb->CommitRootTransaction(EvoDbIdentity::SNAPSHOT)); + + { + auto tx = m_node.evodb->BeginTransaction(EvoDbIdentity::SNAPSHOT); + BOOST_CHECK(!WITH_LOCK(::cs_main, return llmq::EraseMinedCommitmentIfUnreferenced( + *m_node.evodb, *snapshot_chainstate, shared_block, llmq_type, shared_quorum_hash))); + BOOST_CHECK(WITH_LOCK(::cs_main, return llmq::EraseMinedCommitmentIfUnreferenced( + *m_node.evodb, *snapshot_chainstate, snapshot_only_block, llmq_type, snapshot_only_quorum_hash))); + tx->Commit(); + } + + { + auto tx = m_node.evodb->BeginTransaction(EvoDbIdentity::SNAPSHOT); + uint256 value; + BOOST_CHECK(m_node.evodb->Read(shared_key, value)); + BOOST_CHECK(!m_node.evodb->Read(snapshot_only_key, value)); + } +} + +BOOST_FIXTURE_TEST_CASE(chainstatemanager_evodb_snapshot_only_flush_restart, SnapshotTestSetup) +{ + this->SetupSnapshot(); + ChainstateManager& chainman = *Assert(m_node.chainman); + uint256 normal_marker; + BOOST_REQUIRE(m_node.evodb->ReadBestBlock(EvoDbIdentity::NORMAL, normal_marker)); + + mineBlocks(1); + const uint256 snapshot_marker = WITH_LOCK(chainman.GetMutex(), return chainman.ActiveTip()->GetBlockHash()); + WITH_LOCK(::cs_main, chainman.ActiveChainstate().ForceFlushStateToDisk()); + + ChainstateManager& restarted = this->SimulateNodeRestart(/*flush_chainstates=*/false); + this->LoadVerifyActivateChainstate(); + g_txindex = std::make_unique(1 << 20, /*memory=*/true); + BOOST_REQUIRE(g_txindex->Start(restarted.ActiveChainstate())); + IndexWaitSynced(*g_txindex); + + BOOST_CHECK(m_node.evodb->VerifyBestBlock(EvoDbIdentity::SNAPSHOT, snapshot_marker)); + BOOST_CHECK(m_node.evodb->VerifyBestBlock(EvoDbIdentity::NORMAL, normal_marker)); + { + LOCK(::cs_main); + BOOST_REQUIRE_EQUAL(restarted.GetAll().size(), 2); + for (Chainstate* chainstate : restarted.GetAll()) { + BOOST_CHECK(m_node.evodb->VerifyBestBlock(chainstate->EvoDbIdentity(), chainstate->CoinsTip().GetBestBlock())); + } + } +} + +BOOST_FIXTURE_TEST_CASE(chainstatemanager_evodb_legacy_pair_after_snapshot, SnapshotTestSetup) +{ + auto [background_chainstate, snapshot_chainstate] = this->SetupSnapshot(); + WITH_LOCK(::cs_main, background_chainstate->ForceFlushStateToDisk()); + WITH_LOCK(::cs_main, snapshot_chainstate->ForceFlushStateToDisk()); + + uint256 legacy_marker; + BOOST_REQUIRE(m_node.evodb->GetRawDB().Read(EVODB_BEST_BLOCK, legacy_marker)); + const uint256 background_coins_tip = WITH_LOCK(::cs_main, return background_chainstate->CoinsTip().GetBestBlock()); + BOOST_CHECK_EQUAL(legacy_marker, background_coins_tip); +} + BOOST_FIXTURE_TEST_CASE(chainstatemanager_snapshot_init_missing_evodb_marker, SnapshotTestSetup) { this->SetupSnapshot(); From 073d8842b88d585e99de7185a4263c195fe550bc Mon Sep 17 00:00:00 2001 From: Pasta Date: Sat, 11 Jul 2026 00:25:08 -0500 Subject: [PATCH 06/10] dash: bind block validation to the calling chainstate 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. --- src/evo/assetlocktx.cpp | 70 +++++++++-- src/evo/assetlocktx.h | 11 ++ src/evo/mnhftx.cpp | 23 +++- src/evo/mnhftx.h | 4 + src/evo/specialtxman.cpp | 22 ++-- src/evo/specialtxman.h | 5 +- src/llmq/blockprocessor.cpp | 110 ++++++++++-------- src/llmq/blockprocessor.h | 23 ++-- src/llmq/context.cpp | 2 +- src/llmq/quorumsman.cpp | 69 ++++++++++- src/llmq/quorumsman.h | 16 +++ .../validation_chainstatemanager_tests.cpp | 85 ++++++++++++++ src/validation.cpp | 6 +- 13 files changed, 362 insertions(+), 84 deletions(-) diff --git a/src/evo/assetlocktx.cpp b/src/evo/assetlocktx.cpp index cba0bdc55534..966845f77ce9 100644 --- a/src/evo/assetlocktx.cpp +++ b/src/evo/assetlocktx.cpp @@ -96,7 +96,10 @@ std::string CAssetLockPayload::ToString() const const std::string ASSETUNLOCK_REQUESTID_PREFIX = "plwdtx"; -bool CAssetUnlockPayload::VerifySig(const llmq::CQuorumManager& qman, const uint256& msgHash, gsl::not_null pindexTip, TxValidationState& state) const +template +static bool VerifyAssetUnlockSig(const CAssetUnlockPayload& payload, ScanQuorums&& scan_quorums, + GetQuorum&& get_quorum, const uint256& msgHash, + gsl::not_null pindexTip, TxValidationState& state) { // That quourm hash must be active at `requestHeight`, // and at the quorumHash must be active in either the current or previous quorum cycle @@ -110,36 +113,60 @@ bool CAssetUnlockPayload::VerifySig(const llmq::CQuorumManager& qman, const uint // We check all active quorums + 1 the latest inactive const int quorums_to_scan = llmq_params_opt->signingActiveQuorumCount + 1; - const auto quorums = qman.ScanQuorums(llmqType, pindexTip, quorums_to_scan); + const auto quorums = scan_quorums(llmqType, pindexTip, quorums_to_scan); - if (bool isActive = std::any_of(quorums.begin(), quorums.end(), [&](const auto &q) { return q->qc->quorumHash == quorumHash; }); !isActive) { + if (bool isActive = std::any_of(quorums.begin(), quorums.end(), [&](const auto &q) { return q->qc->quorumHash == payload.getQuorumHash(); }); !isActive) { return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-assetunlock-too-old-quorum"); } - if (static_cast(pindexTip->nHeight) < requestedHeight || pindexTip->nHeight >= getHeightToExpiry()) { + if (static_cast(pindexTip->nHeight) < payload.getRequestedHeight() || pindexTip->nHeight >= payload.getHeightToExpiry()) { LogPrint(BCLog::CREDITPOOL, "Asset unlock tx %d with requested height %d could not be accepted on height: %d\n", - index, requestedHeight, pindexTip->nHeight); + payload.getIndex(), payload.getRequestedHeight(), pindexTip->nHeight); return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-assetunlock-too-late"); } - const auto quorum = qman.GetQuorum(llmqType, quorumHash); + const auto quorum = get_quorum(llmqType, payload.getQuorumHash()); // quorum must be valid at this point. Let's check and throw error just in case if (!quorum) { - LogPrintf("%s: ERROR! No quorum for credit pool found for hash=%s\n", __func__, quorumHash.ToString()); + LogPrintf("%s: ERROR! No quorum for credit pool found for hash=%s\n", __func__, payload.getQuorumHash().ToString()); return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-assetunlock-quorum-internal-error"); } - const uint256 requestId = ::SerializeHash(std::make_pair(ASSETUNLOCK_REQUESTID_PREFIX, index)); + const uint256 requestId = ::SerializeHash(std::make_pair(ASSETUNLOCK_REQUESTID_PREFIX, payload.getIndex())); if (const llmq::SignHash signHash(llmqType, quorum->qc->quorumHash, requestId, msgHash); - quorumSig.VerifyInsecure(quorum->qc->quorumPublicKey, signHash.Get())) { + payload.getQuorumSig().VerifyInsecure(quorum->qc->quorumPublicKey, signHash.Get())) { return true; } return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-assetunlock-not-verified"); } -bool CheckAssetUnlockTx(const BlockManager& blockman, const llmq::CQuorumManager& qman, const CTransaction& tx, gsl::not_null pindexPrev, const std::optional& indexes, TxValidationState& state) +bool CAssetUnlockPayload::VerifySig(const llmq::CQuorumManager& qman, const uint256& msgHash, + gsl::not_null pindexTip, TxValidationState& state) const +{ + return VerifyAssetUnlockSig(*this, [&](Consensus::LLMQType llmq_type, const CBlockIndex* pindex, size_t count) { + return qman.ScanQuorums(llmq_type, pindex, count); + }, [&](Consensus::LLMQType llmq_type, const uint256& quorum_hash) { + return qman.GetQuorum(llmq_type, quorum_hash); + }, msgHash, pindexTip, state); +} + +bool CAssetUnlockPayload::VerifySig(const llmq::CQuorumManager& qman, const CChain& chain, const uint256& msgHash, + gsl::not_null pindexTip, TxValidationState& state) const +{ + AssertLockHeld(::cs_main); + return VerifyAssetUnlockSig(*this, [&](Consensus::LLMQType llmq_type, const CBlockIndex* pindex, size_t count) NO_THREAD_SAFETY_ANALYSIS { + return qman.ScanQuorums(llmq_type, pindex, count, chain); + }, [&](Consensus::LLMQType llmq_type, const uint256& quorum_hash) NO_THREAD_SAFETY_ANALYSIS { + return qman.GetQuorum(llmq_type, quorum_hash, chain); + }, msgHash, pindexTip, state); +} + +template +static bool CheckAssetUnlockTxImpl(const BlockManager& blockman, VerifySig&& verify_sig, const CTransaction& tx, + gsl::not_null pindexPrev, + const std::optional& indexes, TxValidationState& state) { // Some checks depends from blockchain status also, such as `known indexes` and `withdrawal limits` // They are omitted here and done by CCreditPool @@ -180,7 +207,28 @@ bool CheckAssetUnlockTx(const BlockManager& blockman, const llmq::CQuorumManager uint256 msgHash = tx_copy.GetHash(); - return assetUnlockTx.VerifySig(qman, msgHash, pindexPrev, state); + return verify_sig(assetUnlockTx, msgHash, pindexPrev, state); +} + +bool CheckAssetUnlockTx(const BlockManager& blockman, const llmq::CQuorumManager& qman, const CTransaction& tx, + gsl::not_null pindexPrev, const std::optional& indexes, + TxValidationState& state) +{ + return CheckAssetUnlockTxImpl(blockman, [&](const CAssetUnlockPayload& payload, const uint256& msg_hash, + const CBlockIndex* pindex, TxValidationState& tx_state) { + return payload.VerifySig(qman, msg_hash, pindex, tx_state); + }, tx, pindexPrev, indexes, state); +} + +bool CheckAssetUnlockTx(const BlockManager& blockman, const llmq::CQuorumManager& qman, const CChain& chain, + const CTransaction& tx, gsl::not_null pindexPrev, + const std::optional& indexes, TxValidationState& state) +{ + AssertLockHeld(::cs_main); + return CheckAssetUnlockTxImpl(blockman, [&](const CAssetUnlockPayload& payload, const uint256& msg_hash, + const CBlockIndex* pindex, TxValidationState& tx_state) NO_THREAD_SAFETY_ANALYSIS { + return payload.VerifySig(qman, chain, msg_hash, pindex, tx_state); + }, tx, pindexPrev, indexes, state); } bool GetAssetUnlockFee(const CTransaction& tx, CAmount& txfee, TxValidationState& state) diff --git a/src/evo/assetlocktx.h b/src/evo/assetlocktx.h index 6a00f605f2c0..634174e1b0f6 100644 --- a/src/evo/assetlocktx.h +++ b/src/evo/assetlocktx.h @@ -10,13 +10,17 @@ #include #include #include +#include +#include #include #include class CBlockIndex; +class CChain; class CRangesSet; class TxValidationState; +extern RecursiveMutex cs_main; // NOLINT(readability-redundant-declaration) struct RPCResult; namespace llmq { class CQuorumManager; @@ -114,6 +118,9 @@ class CAssetUnlockPayload [[nodiscard]] UniValue ToJson() const; bool VerifySig(const llmq::CQuorumManager& qman, const uint256& msgHash, gsl::not_null pindexTip, TxValidationState& state) const; + bool VerifySig(const llmq::CQuorumManager& qman, const CChain& chain, const uint256& msgHash, + gsl::not_null pindexTip, TxValidationState& state) const + EXCLUSIVE_LOCKS_REQUIRED(::cs_main); // getters uint8_t getVersion() const @@ -156,6 +163,10 @@ class CAssetUnlockPayload bool CheckAssetLockTx(const CTransaction& tx, TxValidationState& state); bool CheckAssetUnlockTx(const node::BlockManager& blockman, const llmq::CQuorumManager& qman, const CTransaction& tx, gsl::not_null pindexPrev, const std::optional& indexes, TxValidationState& state); +bool CheckAssetUnlockTx(const node::BlockManager& blockman, const llmq::CQuorumManager& qman, const CChain& chain, + const CTransaction& tx, gsl::not_null pindexPrev, + const std::optional& indexes, TxValidationState& state) + EXCLUSIVE_LOCKS_REQUIRED(::cs_main); bool GetAssetUnlockFee(const CTransaction& tx, CAmount& txfee, TxValidationState& state); #endif // BITCOIN_EVO_ASSETLOCKTX_H diff --git a/src/evo/mnhftx.cpp b/src/evo/mnhftx.cpp index 29cf8fb6c455..22a16706222f 100644 --- a/src/evo/mnhftx.cpp +++ b/src/evo/mnhftx.cpp @@ -103,7 +103,9 @@ bool MNHFTxPayload::IsTriviallyValid(TxValidationState& state) const return true; } -bool CheckMNHFTx(const ChainstateManager& chainman, const llmq::CQuorumManager& qman, const CTransaction& tx, const CBlockIndex* pindexPrev, TxValidationState& state) +template +static bool CheckMNHFTxImpl(const ChainstateManager& chainman, GetQuorum&& get_quorum, + const CTransaction& tx, const CBlockIndex* pindexPrev, TxValidationState& state) { if (!tx.IsSpecialTxVersion() || tx.nType != TRANSACTION_MNHF_SIGNAL) { return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-mnhf-type"); @@ -141,7 +143,7 @@ bool CheckMNHFTx(const ChainstateManager& chainman, const llmq::CQuorumManager& const uint256 msgHash = tx_copy.GetHash(); const Consensus::LLMQType llmqType = Params().GetConsensus().llmqTypeMnhf; - const auto quorum = qman.GetQuorum(llmqType, mnhfTx.signal.quorumHash); + const auto quorum = get_quorum(llmqType, mnhfTx.signal.quorumHash); if (!quorum) { return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-mnhf-missing-quorum"); } @@ -154,6 +156,23 @@ bool CheckMNHFTx(const ChainstateManager& chainman, const llmq::CQuorumManager& return true; } +bool CheckMNHFTx(const ChainstateManager& chainman, const llmq::CQuorumManager& qman, + const CTransaction& tx, const CBlockIndex* pindexPrev, TxValidationState& state) +{ + return CheckMNHFTxImpl(chainman, [&](Consensus::LLMQType llmq_type, const uint256& quorum_hash) { + return qman.GetQuorum(llmq_type, quorum_hash); + }, tx, pindexPrev, state); +} + +bool CheckMNHFTx(const ChainstateManager& chainman, const llmq::CQuorumManager& qman, const CChain& chain, + const CTransaction& tx, const CBlockIndex* pindexPrev, TxValidationState& state) +{ + AssertLockHeld(::cs_main); + return CheckMNHFTxImpl(chainman, [&](Consensus::LLMQType llmq_type, const uint256& quorum_hash) NO_THREAD_SAFETY_ANALYSIS { + return qman.GetQuorum(llmq_type, quorum_hash, chain); + }, tx, pindexPrev, state); +} + std::optional extractEHFSignal(const CTransaction& tx) { if (!tx.IsSpecialTxVersion() || tx.nType != TRANSACTION_MNHF_SIGNAL) { diff --git a/src/evo/mnhftx.h b/src/evo/mnhftx.h index 16cd13b0b90c..94d1bc1c2476 100644 --- a/src/evo/mnhftx.h +++ b/src/evo/mnhftx.h @@ -21,6 +21,7 @@ class BlockValidationState; class CBlock; class CBlockIndex; +class CChain; class CEvoDB; class CTransaction; class ChainstateManager; @@ -156,5 +157,8 @@ class CMNHFManager : public AbstractEHFManager std::optional extractEHFSignal(const CTransaction& tx); bool CheckMNHFTx(const ChainstateManager& chainman, const llmq::CQuorumManager& qman, const CTransaction& tx, const CBlockIndex* pindexPrev, TxValidationState& state); +bool CheckMNHFTx(const ChainstateManager& chainman, const llmq::CQuorumManager& qman, const CChain& chain, + const CTransaction& tx, const CBlockIndex* pindexPrev, TxValidationState& state) + EXCLUSIVE_LOCKS_REQUIRED(::cs_main); #endif // BITCOIN_EVO_MNHFTX_H diff --git a/src/evo/specialtxman.cpp b/src/evo/specialtxman.cpp index 72ba23ca3206..82b546726fbd 100644 --- a/src/evo/specialtxman.cpp +++ b/src/evo/specialtxman.cpp @@ -173,6 +173,7 @@ bool CheckCbTxBestChainlock(const CCbTx& cbTx, const CBlockIndex* pindex, const static bool CheckSpecialTxInner(CDeterministicMNManager& dmnman, llmq::CQuorumSnapshotManager& qsnapman, const ChainstateManager& chainman, const llmq::CQuorumManager& qman, + const CChain* chain, const CTransaction& tx, const CBlockIndex* pindexPrev, const CCoinsViewCache& view, const std::optional& indexes, bool check_sigs, TxValidationState& state) EXCLUSIVE_LOCKS_REQUIRED(::cs_main) @@ -209,11 +210,13 @@ static bool CheckSpecialTxInner(CDeterministicMNManager& dmnman, llmq::CQuorumSn case TRANSACTION_QUORUM_COMMITMENT: return llmq::CheckLLMQCommitment({dmnman, qsnapman, chainman, pindexPrev}, tx, state); case TRANSACTION_MNHF_SIGNAL: - return CheckMNHFTx(chainman, qman, tx, pindexPrev, state); + return chain ? CheckMNHFTx(chainman, qman, *chain, tx, pindexPrev, state) : + CheckMNHFTx(chainman, qman, tx, pindexPrev, state); case TRANSACTION_ASSET_LOCK: return CheckAssetLockTx(tx, state); case TRANSACTION_ASSET_UNLOCK: - return CheckAssetUnlockTx(chainman.m_blockman, qman, tx, pindexPrev, indexes, state); + return chain ? CheckAssetUnlockTx(chainman.m_blockman, qman, *chain, tx, pindexPrev, indexes, state) : + CheckAssetUnlockTx(chainman.m_blockman, qman, tx, pindexPrev, indexes, state); } } catch (const std::exception& e) { LogPrintf("%s -- failed: %s\n", __func__, e.what()); @@ -226,7 +229,7 @@ static bool CheckSpecialTxInner(CDeterministicMNManager& dmnman, llmq::CQuorumSn bool CSpecialTxProcessor::CheckSpecialTx(const CTransaction& tx, const CBlockIndex* pindexPrev, const CCoinsViewCache& view, bool check_sigs, TxValidationState& state) { AssertLockHeld(::cs_main); - return CheckSpecialTxInner(m_dmnman, m_qsnapman, m_chainman, m_qman, tx, pindexPrev, view, std::nullopt, check_sigs, + return CheckSpecialTxInner(m_dmnman, m_qsnapman, m_chainman, m_qman, nullptr, tx, pindexPrev, view, std::nullopt, check_sigs, state); } @@ -616,7 +619,7 @@ bool CSpecialTxProcessor::RebuildListFromBlock(const CBlock& block, gsl::not_nul return true; } -bool CSpecialTxProcessor::ProcessSpecialTxsInBlock(const CBlock& block, const CBlockIndex* pindex, const CCoinsViewCache& view, bool fJustCheck, +bool CSpecialTxProcessor::ProcessSpecialTxsInBlock(Chainstate& chainstate, const CBlock& block, const CBlockIndex* pindex, const CCoinsViewCache& view, bool fJustCheck, bool fCheckCbTxMerkleRoots, BlockValidationState& state, std::optional& updatesRet) { AssertLockHeld(::cs_main); @@ -680,7 +683,8 @@ bool CSpecialTxProcessor::ProcessSpecialTxsInBlock(const CBlock& block, const CB TxValidationState tx_state; // At this moment CheckSpecialTx() may fail by 2 possible ways: // consensus failures and "TX_BAD_SPECIAL" - if (!CheckSpecialTxInner(m_dmnman, m_qsnapman, m_chainman, m_qman, *ptr_tx, pindex->pprev, view, indexes, + if (!CheckSpecialTxInner(m_dmnman, m_qsnapman, m_chainman, m_qman, &chainstate.m_chain, + *ptr_tx, pindex->pprev, view, indexes, fCheckCbTxMerkleRoots, tx_state)) { assert(tx_state.GetResult() == TxValidationResult::TX_CONSENSUS || tx_state.GetResult() == TxValidationResult::TX_BAD_SPECIAL); return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, tx_state.GetRejectReason(), @@ -704,7 +708,7 @@ bool CSpecialTxProcessor::ProcessSpecialTxsInBlock(const CBlock& block, const CB LogPrint(BCLog::BENCHMARK, " - CheckCreditPoolDiffForBlock: %.2fms [%.2fs]\n", 0.001 * (nTime4 - nTime3), nTimeCreditPool * 0.000001); - if (!m_qblockman.ProcessBlock(block, pindex, state, fJustCheck, fCheckCbTxMerkleRoots)) { + if (!m_qblockman.ProcessBlock(chainstate, block, pindex, state, fJustCheck, fCheckCbTxMerkleRoots)) { // pass the state returned by the function above return false; } @@ -765,7 +769,7 @@ bool CSpecialTxProcessor::ProcessSpecialTxsInBlock(const CBlock& block, const CB LogPrint(BCLog::BENCHMARK, " - CalcCbTxMerkleRootQuorums: %.2fms [%.2fs]\n", 0.001 * (nTime6_2 - nTime6_1), nTimeMerkleQuorums * 0.000001); - if (!CheckCbTxBestChainlock(*opt_cbTx, pindex, m_consensus_params, m_chainman.ActiveChain(), m_qman, + if (!CheckCbTxBestChainlock(*opt_cbTx, pindex, m_consensus_params, chainstate.m_chain, m_qman, m_chainlocks, state)) { // pass the state returned by the function above return false; @@ -803,7 +807,7 @@ bool CSpecialTxProcessor::ProcessSpecialTxsInBlock(const CBlock& block, const CB return true; } -bool CSpecialTxProcessor::UndoSpecialTxsInBlock(const CBlock& block, const CBlockIndex* pindex, std::optional& updatesRet) +bool CSpecialTxProcessor::UndoSpecialTxsInBlock(Chainstate& chainstate, const CBlock& block, const CBlockIndex* pindex, std::optional& updatesRet) { AssertLockHeld(::cs_main); @@ -825,7 +829,7 @@ bool CSpecialTxProcessor::UndoSpecialTxsInBlock(const CBlock& block, const CBloc return false; } - if (!m_qblockman.UndoBlock(block, pindex)) { + if (!m_qblockman.UndoBlock(chainstate, block, pindex)) { return false; } } catch (const std::exception& e) { diff --git a/src/evo/specialtxman.h b/src/evo/specialtxman.h index 013fbfa5f1b7..aa78b74913a0 100644 --- a/src/evo/specialtxman.h +++ b/src/evo/specialtxman.h @@ -22,6 +22,7 @@ class CDeterministicMNList; class CDeterministicMNManager; class CTransaction; class ChainstateManager; +class Chainstate; class CMNHFManager; class TxValidationState; struct MNListUpdates; @@ -70,10 +71,10 @@ class CSpecialTxProcessor bool CheckSpecialTx(const CTransaction& tx, const CBlockIndex* pindexPrev, const CCoinsViewCache& view, bool check_sigs, TxValidationState& state) EXCLUSIVE_LOCKS_REQUIRED(::cs_main); - bool ProcessSpecialTxsInBlock(const CBlock& block, const CBlockIndex* pindex, const CCoinsViewCache& view, bool fJustCheck, + bool ProcessSpecialTxsInBlock(Chainstate& chainstate, const CBlock& block, const CBlockIndex* pindex, const CCoinsViewCache& view, bool fJustCheck, bool fCheckCbTxMerkleRoots, BlockValidationState& state, std::optional& updatesRet) EXCLUSIVE_LOCKS_REQUIRED(::cs_main); - bool UndoSpecialTxsInBlock(const CBlock& block, const CBlockIndex* pindex, std::optional& updatesRet) + bool UndoSpecialTxsInBlock(Chainstate& chainstate, const CBlock& block, const CBlockIndex* pindex, std::optional& updatesRet) EXCLUSIVE_LOCKS_REQUIRED(::cs_main); diff --git a/src/llmq/blockprocessor.cpp b/src/llmq/blockprocessor.cpp index 885c3f8ddd9a..3e8cbc39de78 100644 --- a/src/llmq/blockprocessor.cpp +++ b/src/llmq/blockprocessor.cpp @@ -76,14 +76,14 @@ static bool SerializedEqual(const T& lhs, const T& rhs) std::equal(lhs_stream.begin(), lhs_stream.end(), rhs_stream.begin()); } -CQuorumBlockProcessor::CQuorumBlockProcessor(Chainstate& chainstate, CDeterministicMNManager& dmnman, CEvoDB& evoDb, +CQuorumBlockProcessor::CQuorumBlockProcessor(ChainstateManager& chainman, CDeterministicMNManager& dmnman, CEvoDB& evoDb, CQuorumSnapshotManager& qsnapman, int8_t bls_threads) : - m_chainstate{chainstate}, + m_chainman{chainman}, m_dmnman{dmnman}, m_evoDb{evoDb}, m_qsnapman{qsnapman} { - utils::InitQuorumsCache(mapHasMinedCommitmentCache, m_chainstate.m_chainman.GetConsensus()); + utils::InitQuorumsCache(mapMinedCommitmentBlockCache, m_chainman.GetConsensus()); LogPrintf("BLS verification uses %d additional threads\n", bls_threads); m_bls_queue.StartWorkerThreads(bls_threads); } @@ -125,7 +125,8 @@ MessageProcessingResult CQuorumBlockProcessor::ProcessMessage(const CNode& peer, const CBlockIndex* pQuorumBaseBlockIndex; { LOCK(::cs_main); - pQuorumBaseBlockIndex = m_chainstate.m_blockman.LookupBlockIndex(qc.quorumHash); + const auto& active_chainstate = m_chainman.ActiveChainstate(); + pQuorumBaseBlockIndex = active_chainstate.m_blockman.LookupBlockIndex(qc.quorumHash); if (pQuorumBaseBlockIndex == nullptr) { LogPrint(BCLog::LLMQ, "CQuorumBlockProcessor::%s -- unknown block %s in commitment, peer=%d\n", __func__, qc.quorumHash.ToString(), peer.GetId()); @@ -133,7 +134,7 @@ MessageProcessingResult CQuorumBlockProcessor::ProcessMessage(const CNode& peer, // fully synced return ret; } - if (m_chainstate.m_chain.Tip()->GetAncestor(pQuorumBaseBlockIndex->nHeight) != pQuorumBaseBlockIndex) { + if (active_chainstate.m_chain.Tip()->GetAncestor(pQuorumBaseBlockIndex->nHeight) != pQuorumBaseBlockIndex) { LogPrint(BCLog::LLMQ, "CQuorumBlockProcessor::%s -- block %s not in active chain, peer=%d\n", __func__, qc.quorumHash.ToString(), peer.GetId()); // same, can't punish @@ -146,7 +147,7 @@ MessageProcessingResult CQuorumBlockProcessor::ProcessMessage(const CNode& peer, ret.m_error = MisbehavingError{100}; return ret; } - if (pQuorumBaseBlockIndex->nHeight < (m_chainstate.m_chain.Height() - llmq_params_opt->dkgInterval)) { + if (pQuorumBaseBlockIndex->nHeight < (active_chainstate.m_chain.Height() - llmq_params_opt->dkgInterval)) { LogPrint(BCLog::LLMQ, "CQuorumBlockProcessor::%s -- block %s is too old, peer=%d\n", __func__, qc.quorumHash.ToString(), peer.GetId()); if (peer.GetCommonVersion() >= QFCOMMIT_STALE_REPROP_BAN_VERSION) { @@ -176,7 +177,7 @@ MessageProcessingResult CQuorumBlockProcessor::ProcessMessage(const CNode& peer, } } - if (!qc.Verify({m_dmnman, m_qsnapman, m_chainstate.m_chainman, pQuorumBaseBlockIndex}, /*checkSigs=*/true)) { + if (!qc.Verify({m_dmnman, m_qsnapman, m_chainman, pQuorumBaseBlockIndex}, /*checkSigs=*/true)) { LogPrint(BCLog::LLMQ, "CQuorumBlockProcessor::%s -- commitment for quorum %s:%d is not valid quorumIndex[%d] nversion[%d], peer=%d\n", __func__, qc.quorumHash.ToString(), std23::to_underlying(qc.llmqType), qc.quorumIndex, qc.nVersion, peer.GetId()); @@ -193,18 +194,18 @@ MessageProcessingResult CQuorumBlockProcessor::ProcessMessage(const CNode& peer, return ret; } -bool CQuorumBlockProcessor::ProcessBlock(const CBlock& block, gsl::not_null pindex, BlockValidationState& state, bool fJustCheck, bool fBLSChecks) +bool CQuorumBlockProcessor::ProcessBlock(Chainstate& chainstate, const CBlock& block, gsl::not_null pindex, BlockValidationState& state, bool fJustCheck, bool fBLSChecks) { AssertLockHeld(::cs_main); const auto blockHash = pindex->GetBlockHash(); - if (!DeploymentActiveAt(*pindex, m_chainstate.m_chainman.GetConsensus(), Consensus::DEPLOYMENT_DIP0003)) { + if (!DeploymentActiveAt(*pindex, m_chainman.GetConsensus(), Consensus::DEPLOYMENT_DIP0003)) { m_evoDb.Write(DB_BEST_BLOCK_UPGRADE, blockHash); return true; } - PreComputeQuorumMembers(m_dmnman, m_qsnapman, m_chainstate.m_chainman, pindex, /*reset_cache=*/false); + PreComputeQuorumMembers(m_dmnman, m_qsnapman, m_chainman, pindex, /*reset_cache=*/false); std::multimap qcs; if (!GetCommitmentsFromBlock(block, pindex, qcs, state)) { @@ -215,13 +216,13 @@ bool CQuorumBlockProcessor::ProcessBlock(const CBlock& block, gsl::not_nullpprev)) { + for (const Consensus::LLMQParams& params : GetEnabledQuorumParams(m_chainman, pindex->pprev)) { // skip these checks when replaying blocks after the crash - if (m_chainstate.m_chain.Tip() == nullptr) { + if (chainstate.m_chain.Tip() == nullptr) { break; } - const size_t numCommitmentsRequired = GetNumCommitmentsRequired(params, pindex->nHeight); + const size_t numCommitmentsRequired = GetNumCommitmentsRequired(params, chainstate.m_chain, pindex->nHeight); const auto numCommitmentsInNewBlock = qcs.count(params.type); if (numCommitmentsRequired < numCommitmentsInNewBlock) { @@ -240,13 +241,13 @@ bool CQuorumBlockProcessor::ProcessBlock(const CBlock& block, gsl::not_null queue_control(&m_bls_queue); for (const auto& [_, qc] : qcs) { if (qc.IsNull()) continue; - const auto* pQuorumBaseBlockIndex = m_chainstate.m_blockman.LookupBlockIndex(qc.quorumHash); + const auto* pQuorumBaseBlockIndex = chainstate.m_blockman.LookupBlockIndex(qc.quorumHash); if (pQuorumBaseBlockIndex == nullptr) { LogPrint(BCLog::LLMQ, "[ProcessBlock] h[%d] unexpectedly failed due to no known pindex for hash[%s]\n", pindex->nHeight, qc.quorumHash.ToString()); return false; } - qc.VerifySignatureAsync({m_dmnman, m_qsnapman, m_chainstate.m_chainman, pQuorumBaseBlockIndex}, &queue_control); + qc.VerifySignatureAsync({m_dmnman, m_qsnapman, m_chainman, pQuorumBaseBlockIndex}, &queue_control); } if (!queue_control.Wait()) { @@ -255,7 +256,7 @@ bool CQuorumBlockProcessor::ProcessBlock(const CBlock& block, gsl::not_nullnHeight, blockHash, qc, state, fJustCheck)) { + if (!ProcessCommitment(chainstate, pindex->nHeight, blockHash, qc, state, fJustCheck)) { LogPrintf("[ProcessBlock] failed h[%d] llmqType[%d] version[%d] quorumIndex[%d] quorumHash[%s]\n", pindex->nHeight, std23::to_underlying(qc.llmqType), qc.nVersion, qc.quorumIndex, qc.quorumHash.ToString()); return false; } @@ -295,7 +296,7 @@ static bool IsMiningPhase(const Consensus::LLMQParams& llmqParams, const CChain& return nHeight >= quorumCycleMiningStartHeight && nHeight <= quorumCycleMiningEndHeight; } -bool CQuorumBlockProcessor::ProcessCommitment(int nHeight, const uint256& blockHash, const CFinalCommitment& qc, +bool CQuorumBlockProcessor::ProcessCommitment(Chainstate& chainstate, int nHeight, const uint256& blockHash, const CFinalCommitment& qc, BlockValidationState& state, bool fJustCheck) { AssertLockHeld(::cs_main); @@ -307,7 +308,7 @@ bool CQuorumBlockProcessor::ProcessCommitment(int nHeight, const uint256& blockH } const auto& llmq_params = llmq_params_opt.value(); - uint256 quorumHash = GetQuorumBlockHash(llmq_params, m_chainstate.m_chain, nHeight, qc.quorumIndex); + uint256 quorumHash = GetQuorumBlockHash(llmq_params, chainstate.m_chain, nHeight, qc.quorumIndex); LogPrint(BCLog::LLMQ, /* Continued */ "%s -- processing commitment for block height=%d, type=%d, quorumIndex=%d, quorumHash=%s, signers=%s, " @@ -317,7 +318,7 @@ bool CQuorumBlockProcessor::ProcessCommitment(int nHeight, const uint256& blockH qc.CountValidMembers(), qc.quorumPublicKey.ToString(), fJustCheck); // skip `bad-qc-block` checks below when replaying blocks after the crash - if (m_chainstate.m_chain.Tip() == nullptr) { + if (chainstate.m_chain.Tip() == nullptr) { quorumHash = qc.quorumHash; } @@ -358,12 +359,12 @@ bool CQuorumBlockProcessor::ProcessCommitment(int nHeight, const uint256& blockH return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "bad-qc-dup"); } - if (!IsMiningPhase(llmq_params, m_chainstate.m_chain, nHeight)) { + if (!IsMiningPhase(llmq_params, chainstate.m_chain, nHeight)) { // should not happen as it's already handled in ProcessBlock return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "bad-qc-height"); } - const auto* pQuorumBaseBlockIndex = m_chainstate.m_blockman.LookupBlockIndex(qc.quorumHash); + const auto* pQuorumBaseBlockIndex = chainstate.m_blockman.LookupBlockIndex(qc.quorumHash); if (pQuorumBaseBlockIndex == nullptr) { LogPrint(BCLog::LLMQ, "%s -- unexpectedly failed due to no known pindex for hash[%s]\n", __func__, qc.quorumHash.ToString()); @@ -371,7 +372,7 @@ bool CQuorumBlockProcessor::ProcessCommitment(int nHeight, const uint256& blockH } // we don't validate signatures here; they already validated on previous step - if (!qc.Verify({m_dmnman, m_qsnapman, m_chainstate.m_chainman, pQuorumBaseBlockIndex}, /*checksigs=*/false)) { + if (!qc.Verify({m_dmnman, m_qsnapman, m_chainman, pQuorumBaseBlockIndex}, /*checksigs=*/false)) { LogPrint(BCLog::LLMQ, /* Continued */ "%s -- height=%d, type=%d, quorumIndex=%d, quorumHash=%s, signers=%s, validMembers=%d, " "quorumPublicKey=%s qc verify failed.\n", @@ -407,7 +408,7 @@ bool CQuorumBlockProcessor::ProcessCommitment(int nHeight, const uint256& blockH { LOCK(minableCommitmentsCs); - mapHasMinedCommitmentCache[qc.llmqType].erase(qc.quorumHash); + mapMinedCommitmentBlockCache[qc.llmqType].erase(qc.quorumHash); minableCommitmentsByQuorum.erase(cacheKey); minableCommitments.erase(::SerializeHash(qc)); } @@ -418,11 +419,11 @@ bool CQuorumBlockProcessor::ProcessCommitment(int nHeight, const uint256& blockH return true; } -bool CQuorumBlockProcessor::UndoBlock(const CBlock& block, gsl::not_null pindex) +bool CQuorumBlockProcessor::UndoBlock(Chainstate& chainstate, const CBlock& block, gsl::not_null pindex) { AssertLockHeld(::cs_main); - PreComputeQuorumMembers(m_dmnman, m_qsnapman, m_chainstate.m_chainman, pindex, /*reset_cache=*/true); + PreComputeQuorumMembers(m_dmnman, m_qsnapman, m_chainman, pindex, /*reset_cache=*/true); std::multimap qcs; if (BlockValidationState dummy; !GetCommitmentsFromBlock(block, pindex, qcs, dummy)) { @@ -435,7 +436,7 @@ bool CQuorumBlockProcessor::UndoBlock(const CBlock& block, gsl::not_nullGetBlockHash().ToString()); } else { @@ -449,7 +450,7 @@ bool CQuorumBlockProcessor::UndoBlock(const CBlock& block, gsl::not_nullGetAncestor(nHeight); + assert(nHeight <= chain.Height() + 1); + const auto* const pindex = chain.Height() < nHeight ? chain.Tip() : chain.Tip()->GetAncestor(nHeight); bool rotation_enabled = IsQuorumRotationEnabled(llmqParams, pindex); size_t quorums_num = rotation_enabled ? llmqParams.signingActiveQuorumCount : 1; size_t ret{0}; for (const auto quorumIndex : util::irange(quorums_num)) { - uint256 quorumHash = GetQuorumBlockHash(llmqParams, m_chainstate.m_chain, nHeight, quorumIndex); - if (!quorumHash.IsNull() && !HasMinedCommitment(llmqParams.type, quorumHash)) ++ret; + uint256 quorumHash = GetQuorumBlockHash(llmqParams, chain, nHeight, quorumIndex); + if (!quorumHash.IsNull() && !HasMinedCommitment(llmqParams.type, quorumHash, chain)) ++ret; } return ret; @@ -542,17 +543,33 @@ uint256 CQuorumBlockProcessor::GetQuorumBlockHash(const Consensus::LLMQParams& l bool CQuorumBlockProcessor::HasMinedCommitment(Consensus::LLMQType llmqType, const uint256& quorumHash) const { - bool fExists; - if (LOCK(minableCommitmentsCs); mapHasMinedCommitmentCache[llmqType].get(quorumHash, fExists)) { - return fExists; - } + LOCK(::cs_main); + return HasMinedCommitment(llmqType, quorumHash, m_chainman.ActiveChain()); +} - fExists = m_evoDb.Exists(std::make_pair(DB_MINED_COMMITMENT, std::make_pair(llmqType, quorumHash))); +bool CQuorumBlockProcessor::HasMinedCommitment(Consensus::LLMQType llmqType, const uint256& quorumHash, + const CChain& chain) const +{ + AssertLockHeld(::cs_main); - LOCK(minableCommitmentsCs); - mapHasMinedCommitmentCache[llmqType].insert(quorumHash, fExists); + uint256 mined_block_hash; + bool cached; + { + LOCK(minableCommitmentsCs); + cached = mapMinedCommitmentBlockCache[llmqType].get(quorumHash, mined_block_hash); + } + if (!cached) { + mined_block_hash = GetMinedCommitment(llmqType, quorumHash).second; + // Do not negatively cache. Snapshot activation seeds EvoDB directly, + // outside ProcessCommitment's normal cache-invalidation path. + if (!mined_block_hash.IsNull()) { + LOCK(minableCommitmentsCs); + mapMinedCommitmentBlockCache[llmqType].insert(quorumHash, mined_block_hash); + } + } - return fExists; + const CBlockIndex* mined_block = m_chainman.m_blockman.LookupBlockIndex(mined_block_hash); + return mined_block != nullptr && chain.Contains(mined_block); } std::pair CQuorumBlockProcessor::GetMinedCommitment(Consensus::LLMQType llmqType, @@ -767,25 +784,26 @@ std::optional> CQuorumBlockProcessor::GetMineableC AssertLockHeld(::cs_main); std::vector ret; + const auto& active_chain = m_chainman.ActiveChain(); - if (GetNumCommitmentsRequired(llmqParams, nHeight) == 0) { + if (GetNumCommitmentsRequired(llmqParams, active_chain, nHeight) == 0) { // no commitment required return std::nullopt; } // Note: This function can be called for new blocks - assert(nHeight <= m_chainstate.m_chain.Height() + 1); - const auto *const pindex = m_chainstate.m_chain.Height() < nHeight ? m_chainstate.m_chain.Tip() : m_chainstate.m_chain.Tip()->GetAncestor(nHeight); + assert(nHeight <= active_chain.Height() + 1); + const auto* const pindex = active_chain.Height() < nHeight ? active_chain.Tip() : active_chain.Tip()->GetAncestor(nHeight); bool rotation_enabled = IsQuorumRotationEnabled(llmqParams, pindex); - bool basic_bls_enabled{DeploymentActiveAfter(pindex, m_chainstate.m_chainman.GetConsensus(), Consensus::DEPLOYMENT_V19)}; + bool basic_bls_enabled{DeploymentActiveAfter(pindex, m_chainman.GetConsensus(), Consensus::DEPLOYMENT_V19)}; size_t quorums_num = rotation_enabled ? llmqParams.signingActiveQuorumCount : 1; std::stringstream ss; for (const auto quorumIndex : util::irange(quorums_num)) { CFinalCommitment cf; - uint256 quorumHash = GetQuorumBlockHash(llmqParams, m_chainstate.m_chain, nHeight, quorumIndex); + uint256 quorumHash = GetQuorumBlockHash(llmqParams, active_chain, nHeight, quorumIndex); if (quorumHash.IsNull()) { break; } diff --git a/src/llmq/blockprocessor.h b/src/llmq/blockprocessor.h index fafaf36a7328..ad07ba0e7788 100644 --- a/src/llmq/blockprocessor.h +++ b/src/llmq/blockprocessor.h @@ -26,6 +26,7 @@ class CBlockIndex; class CBLSSignature; class CChain; class Chainstate; +class ChainstateManager; class CDataStream; class CDeterministicMNManager; class CEvoDB; @@ -47,7 +48,7 @@ bool EraseMinedCommitmentIfUnreferenced(CEvoDB& evo_db, const Chainstate& chains class CQuorumBlockProcessor { private: - Chainstate& m_chainstate; + ChainstateManager& m_chainman; CDeterministicMNManager& m_dmnman; CEvoDB& m_evoDb; CQuorumSnapshotManager& m_qsnapman; @@ -58,22 +59,24 @@ class CQuorumBlockProcessor std::map, uint256> minableCommitmentsByQuorum GUARDED_BY(minableCommitmentsCs); std::map minableCommitments GUARDED_BY(minableCommitmentsCs); - mutable std::map> mapHasMinedCommitmentCache GUARDED_BY(minableCommitmentsCs); + // Cache the block in which a commitment was mined. Membership in a + // particular chain is checked on every call so reorgs need no cache flush. + mutable std::map> mapMinedCommitmentBlockCache GUARDED_BY(minableCommitmentsCs); public: CQuorumBlockProcessor() = delete; CQuorumBlockProcessor(const CQuorumBlockProcessor&) = delete; CQuorumBlockProcessor& operator=(const CQuorumBlockProcessor&) = delete; - explicit CQuorumBlockProcessor(Chainstate& chainstate, CDeterministicMNManager& dmnman, CEvoDB& evoDb, + explicit CQuorumBlockProcessor(ChainstateManager& chainman, CDeterministicMNManager& dmnman, CEvoDB& evoDb, CQuorumSnapshotManager& qsnapman, int8_t bls_threads); ~CQuorumBlockProcessor(); [[nodiscard]] MessageProcessingResult ProcessMessage(const CNode& peer, std::string_view msg_type, CDataStream& vRecv) EXCLUSIVE_LOCKS_REQUIRED(!minableCommitmentsCs); - bool ProcessBlock(const CBlock& block, gsl::not_null pindex, BlockValidationState& state, + bool ProcessBlock(Chainstate& chainstate, const CBlock& block, gsl::not_null pindex, BlockValidationState& state, bool fJustCheck, bool fBLSChecks) EXCLUSIVE_LOCKS_REQUIRED(::cs_main, !minableCommitmentsCs); - bool UndoBlock(const CBlock& block, gsl::not_null pindex) + bool UndoBlock(Chainstate& chainstate, const CBlock& block, gsl::not_null pindex) EXCLUSIVE_LOCKS_REQUIRED(::cs_main, !minableCommitmentsCs); //! it returns hash of commitment if it should be relay, otherwise nullopt @@ -88,6 +91,8 @@ class CQuorumBlockProcessor EXCLUSIVE_LOCKS_REQUIRED(::cs_main, !minableCommitmentsCs); bool HasMinedCommitment(Consensus::LLMQType llmqType, const uint256& quorumHash) const EXCLUSIVE_LOCKS_REQUIRED(!minableCommitmentsCs); + bool HasMinedCommitment(Consensus::LLMQType llmqType, const uint256& quorumHash, const CChain& chain) const + EXCLUSIVE_LOCKS_REQUIRED(::cs_main, !minableCommitmentsCs); std::pair GetMinedCommitment(Consensus::LLMQType llmqType, const uint256& quorumHash) const; std::vector GetMinedCommitmentsUntilBlock(Consensus::LLMQType llmqType, gsl::not_null pindex, size_t maxCount) const; @@ -100,10 +105,14 @@ class CQuorumBlockProcessor std::optional GetLastMinedCommitmentsByQuorumIndexUntilBlock(Consensus::LLMQType llmqType, const CBlockIndex* pindex, int quorumIndex, size_t cycle) const; private: static bool GetCommitmentsFromBlock(const CBlock& block, gsl::not_null pindex, std::multimap& ret, BlockValidationState& state) EXCLUSIVE_LOCKS_REQUIRED(::cs_main); - bool ProcessCommitment(int nHeight, const uint256& blockHash, const CFinalCommitment& qc, BlockValidationState& state, + bool ProcessCommitment(Chainstate& chainstate, int nHeight, const uint256& blockHash, const CFinalCommitment& qc, BlockValidationState& state, bool fJustCheck) EXCLUSIVE_LOCKS_REQUIRED(::cs_main, !minableCommitmentsCs); - size_t GetNumCommitmentsRequired(const Consensus::LLMQParams& llmqParams, int nHeight) const +public: + // Public for multi-chainstate accounting tests and callers which validate + // against a chainstate other than the active one. + size_t GetNumCommitmentsRequired(const Consensus::LLMQParams& llmqParams, const CChain& chain, int nHeight) const EXCLUSIVE_LOCKS_REQUIRED(::cs_main, !minableCommitmentsCs); +private: static uint256 GetQuorumBlockHash(const Consensus::LLMQParams& llmqParams, const CChain& active_chain, int nHeight, int quorumIndex) EXCLUSIVE_LOCKS_REQUIRED(::cs_main); }; } // namespace llmq diff --git a/src/llmq/context.cpp b/src/llmq/context.cpp index 3d28a364fa5b..49c74bb43bb3 100644 --- a/src/llmq/context.cpp +++ b/src/llmq/context.cpp @@ -17,7 +17,7 @@ LLMQContext::LLMQContext(CDeterministicMNManager& dmnman, CEvoDB& evo_db, CSpork int16_t worker_count, int64_t max_recsigs_age) : bls_worker{std::make_shared()}, qsnapman{std::make_unique(evo_db)}, - quorum_block_processor{std::make_unique(chainman.ActiveChainstate(), dmnman, evo_db, + quorum_block_processor{std::make_unique(chainman, dmnman, evo_db, *qsnapman, bls_threads)}, qman{std::make_unique(*bls_worker, dmnman, evo_db, *quorum_block_processor, *qsnapman, chainman, db_params)}, diff --git a/src/llmq/quorumsman.cpp b/src/llmq/quorumsman.cpp index 7b64829e71cc..52f910043352 100644 --- a/src/llmq/quorumsman.cpp +++ b/src/llmq/quorumsman.cpp @@ -146,6 +146,13 @@ bool CQuorumManager::HasQuorum(Consensus::LLMQType llmqType, const CQuorumBlockP return quorum_block_processor.HasMinedCommitment(llmqType, quorumHash); } +bool CQuorumManager::HasQuorum(Consensus::LLMQType llmqType, const CQuorumBlockProcessor& quorum_block_processor, + const uint256& quorumHash, const CChain& chain) +{ + AssertLockHeld(::cs_main); + return quorum_block_processor.HasMinedCommitment(llmqType, quorumHash, chain); +} + std::vector CQuorumManager::ScanQuorums(Consensus::LLMQType llmqType, size_t nCountRequested) const { const CBlockIndex* pindex = WITH_LOCK(::cs_main, return m_chainman.ActiveTip()); @@ -155,6 +162,21 @@ std::vector CQuorumManager::ScanQuorums(Consensus::LLMQType llmqTyp std::vector CQuorumManager::ScanQuorums(Consensus::LLMQType llmqType, gsl::not_null pindexStart, size_t nCountRequested) const +{ + return ScanQuorums(llmqType, pindexStart, nCountRequested, nullptr); +} + +std::vector CQuorumManager::ScanQuorums(Consensus::LLMQType llmqType, + gsl::not_null pindexStart, + size_t nCountRequested, const CChain& chain) const +{ + AssertLockHeld(::cs_main); + return ScanQuorums(llmqType, pindexStart, nCountRequested, &chain); +} + +std::vector CQuorumManager::ScanQuorums(Consensus::LLMQType llmqType, + gsl::not_null pindexStart, + size_t nCountRequested, const CChain* chain) const { if (nCountRequested == 0 || !m_chainman.IsQuorumTypeEnabled(llmqType, pindexStart)) { return {}; @@ -185,7 +207,7 @@ std::vector CQuorumManager::ScanQuorums(Consensus::LLMQType llmqTyp size_t nScanCommitments{nCountRequested}; std::vector vecResultQuorums; - { + if (chain == nullptr) { LOCK(m_cs_maps); if (scanQuorumsCache.empty()) { for (const auto& llmq : Params().GetConsensus().llmqs) { @@ -220,6 +242,8 @@ std::vector CQuorumManager::ScanQuorums(Consensus::LLMQType llmqTyp // If there is nothing in cache request at least keepOldConnections because this gets cached then later nScanCommitments = std::max(nCountRequested, static_cast(llmq_params_opt->keepOldConnections)); } + } else { + nScanCommitments = std::max(nCountRequested, static_cast(llmq_params_opt->keepOldConnections)); } // Get the block indexes of the mined commitments to build the required quorums from @@ -237,7 +261,14 @@ std::vector CQuorumManager::ScanQuorums(Consensus::LLMQType llmqTyp // We assume that every quorum asked for is available to us on hand, if this // fails then we can assume that something has gone wrong and we should stop // trying to process any further and return a blank. - auto quorum = GetQuorum(llmqType, pQuorumBaseBlockIndex, populate_cache); + CQuorumCPtr quorum; + if (chain) { + quorum = [&]() NO_THREAD_SAFETY_ANALYSIS { + return GetQuorum(llmqType, pQuorumBaseBlockIndex, *chain, populate_cache); + }(); + } else { + quorum = GetQuorum(llmqType, pQuorumBaseBlockIndex, populate_cache); + } if (!quorum) { LogPrintf("%s: ERROR! Unexpected missing quorum with llmqType=%d, blockHash=%s, populate_cache=%s\n", __func__, std23::to_underlying(llmqType), pQuorumBaseBlockIndex->GetBlockHash().ToString(), @@ -248,7 +279,7 @@ std::vector CQuorumManager::ScanQuorums(Consensus::LLMQType llmqTyp } const size_t nCountResult{vecResultQuorums.size()}; - if (nCountResult > 0) { + if (nCountResult > 0 && chain == nullptr) { LOCK(m_cs_maps); // Don't cache more than keepOldConnections elements // because signing by old quorums requires the exact quorum hash @@ -344,6 +375,19 @@ CQuorumCPtr CQuorumManager::GetQuorum(Consensus::LLMQType llmqType, const uint25 return GetQuorum(llmqType, pQuorumBaseBlockIndex); } +CQuorumCPtr CQuorumManager::GetQuorum(Consensus::LLMQType llmqType, const uint256& quorumHash, + const CChain& chain) const +{ + AssertLockHeld(::cs_main); + + const auto* pQuorumBaseBlockIndex = m_chainman.m_blockman.LookupBlockIndex(quorumHash); + if (!pQuorumBaseBlockIndex) { + LogPrint(BCLog::LLMQ, "CQuorumManager::%s -- block %s not found\n", __func__, quorumHash.ToString()); + return nullptr; + } + return GetQuorum(llmqType, pQuorumBaseBlockIndex, chain); +} + CQuorumCPtr CQuorumManager::GetQuorum(Consensus::LLMQType llmqType, gsl::not_null pQuorumBaseBlockIndex, bool populate_cache) const { auto quorumHash = pQuorumBaseBlockIndex->GetBlockHash(); @@ -362,6 +406,25 @@ CQuorumCPtr CQuorumManager::GetQuorum(Consensus::LLMQType llmqType, gsl::not_nul return BuildQuorumFromCommitment(llmqType, pQuorumBaseBlockIndex, populate_cache); } +CQuorumCPtr CQuorumManager::GetQuorum(Consensus::LLMQType llmqType, + gsl::not_null pQuorumBaseBlockIndex, + const CChain& chain, bool populate_cache) const +{ + AssertLockHeld(::cs_main); + + const auto quorumHash = pQuorumBaseBlockIndex->GetBlockHash(); + if (!HasQuorum(llmqType, quorumBlockProcessor, quorumHash, chain)) { + return nullptr; + } + + CQuorumPtr pQuorum; + if (LOCK(m_cs_maps); mapQuorumsCache[llmqType].get(quorumHash, pQuorum)) { + return pQuorum; + } + + return BuildQuorumFromCommitment(llmqType, pQuorumBaseBlockIndex, populate_cache); +} + bool CQuorumManager::RegisterDataRequest(const CQuorumDataRequestKey& key, const CQuorumDataRequest& request, bool add_expiry_bias) const { diff --git a/src/llmq/quorumsman.h b/src/llmq/quorumsman.h index 1a35b1cf0ab0..b2fad2533ee9 100644 --- a/src/llmq/quorumsman.h +++ b/src/llmq/quorumsman.h @@ -116,10 +116,15 @@ class CQuorumManager final std::vector>& vec_enc) const; static bool HasQuorum(Consensus::LLMQType llmqType, const CQuorumBlockProcessor& quorum_block_processor, const uint256& quorumHash); + static bool HasQuorum(Consensus::LLMQType llmqType, const CQuorumBlockProcessor& quorum_block_processor, + const uint256& quorumHash, const CChain& chain) + EXCLUSIVE_LOCKS_REQUIRED(::cs_main); // all these methods will lock cs_main for a short period of time CQuorumCPtr GetQuorum(Consensus::LLMQType llmqType, const uint256& quorumHash) const EXCLUSIVE_LOCKS_REQUIRED(!cs_db, !m_cs_maps, !m_cache_cs); + CQuorumCPtr GetQuorum(Consensus::LLMQType llmqType, const uint256& quorumHash, const CChain& chain) const + EXCLUSIVE_LOCKS_REQUIRED(::cs_main, !cs_db, !m_cs_maps, !m_cache_cs); std::vector ScanQuorums(Consensus::LLMQType llmqType, size_t nCountRequested) const EXCLUSIVE_LOCKS_REQUIRED(!cs_db, !m_cs_maps, !m_cache_cs); @@ -127,6 +132,10 @@ class CQuorumManager final std::vector ScanQuorums(Consensus::LLMQType llmqType, gsl::not_null pindexStart, size_t nCountRequested) const EXCLUSIVE_LOCKS_REQUIRED(!cs_db, !m_cs_maps, !m_cache_cs); + std::vector ScanQuorums(Consensus::LLMQType llmqType, + gsl::not_null pindexStart, + size_t nCountRequested, const CChain& chain) const + EXCLUSIVE_LOCKS_REQUIRED(::cs_main, !cs_db, !m_cs_maps, !m_cache_cs); bool IsMasternode() const; bool IsWatching() const; @@ -156,6 +165,10 @@ class CQuorumManager final private: // all private methods here are cs_main-free + std::vector ScanQuorums(Consensus::LLMQType llmqType, + gsl::not_null pindexStart, + size_t nCountRequested, const CChain* chain) const + EXCLUSIVE_LOCKS_REQUIRED(!cs_db, !m_cs_maps, !m_cache_cs); bool BuildQuorumContributions(const CFinalCommitmentPtr& fqc, const std::shared_ptr& quorum) const; CQuorumPtr BuildQuorumFromCommitment(Consensus::LLMQType llmqType, @@ -166,6 +179,9 @@ class CQuorumManager final CQuorumCPtr GetQuorum(Consensus::LLMQType llmqType, gsl::not_null pindex, bool populate_cache = true) const EXCLUSIVE_LOCKS_REQUIRED(!cs_db, !m_cs_maps, !m_cache_cs); + CQuorumCPtr GetQuorum(Consensus::LLMQType llmqType, gsl::not_null pindex, + const CChain& chain, bool populate_cache = true) const + EXCLUSIVE_LOCKS_REQUIRED(::cs_main, !cs_db, !m_cs_maps, !m_cache_cs); void CacheWarmingThreadMain() const EXCLUSIVE_LOCKS_REQUIRED(!m_cache_cs); void MigrateOldQuorumDB(CEvoDB& evoDb) const EXCLUSIVE_LOCKS_REQUIRED(!cs_db); diff --git a/src/test/validation_chainstatemanager_tests.cpp b/src/test/validation_chainstatemanager_tests.cpp index 8870213da6d1..eb919fcc4082 100644 --- a/src/test/validation_chainstatemanager_tests.cpp +++ b/src/test/validation_chainstatemanager_tests.cpp @@ -3,6 +3,7 @@ // file COPYING or http://www.opensource.org/licenses/mit-license.php. // #include +#include #include #include #include @@ -23,7 +24,11 @@ #include #include #include +#include +#include +#include #include +#include #include @@ -722,6 +727,86 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager_evodb_reorg_erase_guard, SnapshotTestS } } +BOOST_FIXTURE_TEST_CASE(chainstatemanager_mined_commitment_is_chain_aware, SnapshotTestSetup) +{ + auto [background_chainstate, snapshot_chainstate] = this->SetupSnapshot(); + const auto llmq_type = Consensus::LLMQType::LLMQ_TEST; + const auto llmq_params = Params().GetLLMQ(llmq_type).value(); + const int target_height = background_chainstate->m_chain.Height() + 1; + BOOST_REQUIRE_GE(target_height % llmq_params.dkgInterval, llmq_params.dkgMiningWindowStart); + BOOST_REQUIRE_LE(target_height % llmq_params.dkgInterval, llmq_params.dkgMiningWindowEnd); + + const CBlockIndex* quorum_base; + const CBlockIndex* snapshot_mined_block; + { + LOCK(::cs_main); + quorum_base = background_chainstate->m_chain[target_height - (target_height % llmq_params.dkgInterval)]; + snapshot_mined_block = snapshot_chainstate->m_chain[target_height]; + BOOST_REQUIRE(quorum_base); + BOOST_REQUIRE(snapshot_mined_block); + BOOST_REQUIRE(background_chainstate->m_chain.Contains(quorum_base)); + BOOST_REQUIRE(!background_chainstate->m_chain.Contains(snapshot_mined_block)); + BOOST_REQUIRE(snapshot_chainstate->m_chain.Contains(snapshot_mined_block)); + } + + const uint256 quorum_hash = quorum_base->GetBlockHash(); + const auto key = std::make_pair(std::string{"q_mc"}, std::make_pair(llmq_type, quorum_hash)); + const auto inverse_height_key = std::make_tuple(std::string{"q_mcih"}, llmq_type, + htobe32_internal(~uint32_t{0} - target_height)); + const llmq::CFinalCommitment seeded_commitment{llmq_params, quorum_hash}; + { + auto tx = m_node.evodb->BeginTransaction(EvoDbIdentity::SNAPSHOT); + m_node.evodb->Write(key, std::make_pair(seeded_commitment, snapshot_mined_block->GetBlockHash())); + m_node.evodb->Write(inverse_height_key, quorum_base->nHeight); + tx->Commit(); + } + + std::pair raw_commitment; + BOOST_CHECK(!m_node.evodb->GetRawDB().Read(key, raw_commitment)); + int raw_quorum_height; + BOOST_CHECK(!m_node.evodb->GetRawDB().Read(inverse_height_key, raw_quorum_height)); + + auto& qblockman = *Assert(m_node.llmq_ctx)->quorum_block_processor; + auto& qman = *Assert(m_node.llmq_ctx)->qman; + { + LOCK(::cs_main); + const auto mined_commitments = qblockman.GetMinedCommitmentsUntilBlock(llmq_type, snapshot_mined_block, + /*maxCount=*/1); + BOOST_REQUIRE_EQUAL(mined_commitments.size(), 1); + BOOST_CHECK_EQUAL(mined_commitments.front(), quorum_base); + + // Exercise quorum resolution itself, including the cache-order hazard: + // the first background lookup must not negatively cache the unflushed + // snapshot commitment, and a later positive cache entry must not make + // that commitment resolve for the background chain. + BOOST_CHECK(qman.GetQuorum(llmq_type, quorum_hash, background_chainstate->m_chain) == nullptr); + BOOST_CHECK(qman.GetQuorum(llmq_type, quorum_hash, snapshot_chainstate->m_chain) != nullptr); + BOOST_CHECK(qman.GetQuorum(llmq_type, quorum_hash, background_chainstate->m_chain) == nullptr); + + BOOST_CHECK(qblockman.HasMinedCommitment(llmq_type, quorum_hash)); + BOOST_CHECK(qblockman.HasMinedCommitment(llmq_type, quorum_hash, snapshot_chainstate->m_chain)); + BOOST_CHECK(!qblockman.HasMinedCommitment(llmq_type, quorum_hash, background_chainstate->m_chain)); + + BOOST_CHECK(llmq::CQuorumManager::HasQuorum(llmq_type, qblockman, quorum_hash)); + BOOST_CHECK(llmq::CQuorumManager::HasQuorum(llmq_type, qblockman, quorum_hash, + snapshot_chainstate->m_chain)); + BOOST_CHECK(!llmq::CQuorumManager::HasQuorum(llmq_type, qblockman, quorum_hash, + background_chainstate->m_chain)); + + // ConnectBlock accepts exactly this required count. The record from + // snapshot chain A therefore cannot suppress the commitment expected + // in chain B's block at the same height. + BOOST_CHECK_EQUAL(qblockman.GetNumCommitmentsRequired(llmq_params, snapshot_chainstate->m_chain, target_height), 0); + BOOST_CHECK_EQUAL(qblockman.GetNumCommitmentsRequired(llmq_params, background_chainstate->m_chain, target_height), 1); + + CBlock block; + BOOST_REQUIRE(node::ReadBlockFromDisk(block, snapshot_mined_block, Params().GetConsensus())); + BlockValidationState state; + BOOST_CHECK(qblockman.ProcessBlock(*background_chainstate, block, snapshot_mined_block, state, + /*fJustCheck=*/true, /*fBLSChecks=*/false)); + } +} + BOOST_FIXTURE_TEST_CASE(chainstatemanager_evodb_snapshot_only_flush_restart, SnapshotTestSetup) { this->SetupSnapshot(); diff --git a/src/validation.cpp b/src/validation.cpp index 9227c10d2612..8c4c43c54990 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -1994,7 +1994,7 @@ DisconnectResult Chainstate::DisconnectBlock(const CBlock& block, const CBlockIn } std::optional 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)) { error("DisconnectBlock(): UndoSpecialTxsInBlock failed"); return DISCONNECT_FAILED; } @@ -2330,7 +2330,7 @@ bool Chainstate::ConnectBlock(const CBlock& block, BlockValidationState& state, // MUST process special txes before updating UTXO to ensure consistency between mempool and block processing std::optional mnlist_updates_opt{std::nullopt}; - if (!m_chain_helper->special_tx->ProcessSpecialTxsInBlock(block, pindex, view, fJustCheck, fScriptChecks, state, mnlist_updates_opt)) { + if (!m_chain_helper->special_tx->ProcessSpecialTxsInBlock(*this, block, pindex, view, fJustCheck, fScriptChecks, state, mnlist_updates_opt)) { return error("ConnectBlock(DASH): ProcessSpecialTxsInBlock for block %s failed with %s", pindex->GetBlockHash().ToString(), state.ToString()); } @@ -4613,7 +4613,7 @@ bool Chainstate::RollforwardBlock(const CBlockIndex* pindex, CCoinsViewCache& in // MUST process special txes before updating UTXO to ensure consistency between mempool and block processing BlockValidationState state; std::optional mnlist_updates_opt{std::nullopt}; - if (!m_chain_helper->special_tx->ProcessSpecialTxsInBlock(block, pindex, inputs, false /*fJustCheck*/, false /*fScriptChecks*/, state, mnlist_updates_opt)) { + if (!m_chain_helper->special_tx->ProcessSpecialTxsInBlock(*this, block, pindex, inputs, false /*fJustCheck*/, false /*fScriptChecks*/, state, mnlist_updates_opt)) { return error("RollforwardBlock(DASH): ProcessSpecialTxsInBlock for block %s failed with %s", pindex->GetBlockHash().ToString(), state.ToString()); } From 18ed8a5d0077e05d9c800061c33cfcf480ace940 Mon Sep 17 00:00:00 2001 From: Pasta Date: Sat, 11 Jul 2026 00:26:24 -0500 Subject: [PATCH 07/10] validation: suppress background chainstate notifications 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. --- .../validation_chainstatemanager_tests.cpp | 44 ++++++++++++++++++- src/validation.cpp | 39 +++++++++++----- src/validation.h | 3 ++ 3 files changed, 73 insertions(+), 13 deletions(-) diff --git a/src/test/validation_chainstatemanager_tests.cpp b/src/test/validation_chainstatemanager_tests.cpp index eb919fcc4082..292da34bbeb7 100644 --- a/src/test/validation_chainstatemanager_tests.cpp +++ b/src/test/validation_chainstatemanager_tests.cpp @@ -28,18 +28,35 @@ #include #include #include +#include #include +#include #include #include +#include #include using node::SnapshotMetadata; namespace { +class TipEventCounter final : public CValidationInterface +{ +public: + int block_connected{0}; + int updated_tip{0}; + int mn_list_changed{0}; + int chainstate_flushed{0}; + + void BlockConnected(const std::shared_ptr&, const CBlockIndex*) override { ++block_connected; } + void UpdatedBlockTip(const CBlockIndex*, const CBlockIndex*, bool) override { ++updated_tip; } + void NotifyMasternodeListChanged(bool, const CDeterministicMNList&, const CDeterministicMNListDiff&) override { ++mn_list_changed; } + void ChainStateFlushed(const CBlockLocator&) override { ++chainstate_flushed; } +}; + void SeedSnapshotMarker(CEvoDB& evodb, const uint256& hash) { auto tx = evodb.BeginTransaction(EvoDbIdentity::SNAPSHOT); @@ -73,6 +90,7 @@ BOOST_AUTO_TEST_CASE(chainstatemanager) WITH_LOCK(::cs_main, c1.InitCoinsCache(1 << 23)); DashChainstateSetup(manager, m_node, /*llmq_dbs_in_memory=*/true, /*llmq_dbs_wipe=*/false); + BOOST_REQUIRE(c1.LoadGenesisBlock()); BOOST_CHECK(!manager.IsSnapshotActive()); BOOST_CHECK(WITH_LOCK(::cs_main, return !manager.IsSnapshotValidated())); @@ -112,8 +130,10 @@ BOOST_AUTO_TEST_CASE(chainstatemanager) c2.InitCoinsDB( /*cache_size_bytes=*/1 << 23, /*in_memory=*/true, /*should_wipe=*/false); WITH_LOCK(::cs_main, c2.InitCoinsCache(1 << 23)); - // Unlike c1, which doesn't have any blocks. Gets us different tip, height. + // Give the snapshot chainstate its own genesis candidate and tip. c2.LoadGenesisBlock(); + WITH_LOCK(::cs_main, c2.setBlockIndexCandidates.insert( + manager.m_blockman.LookupBlockIndex(Params().GenesisBlock().GetHash()))); BlockValidationState dummy_state; BOOST_CHECK(c2.ActivateBestChain(dummy_state, nullptr)); @@ -137,6 +157,28 @@ BOOST_AUTO_TEST_CASE(chainstatemanager) // CCoinsViewCache instances. BOOST_CHECK(exp_tip != exp_tip2); + // Connect genesis through the now-background chainstate. This exercises + // Dash special-transaction and quorum processing with a non-active caller, + // and background validation must not emit active-tip notifications. + SyncWithValidationInterfaceQueue(); + TipEventCounter event_counter; + RegisterValidationInterface(&event_counter); + int ui_mn_list_changed{0}; + auto ui_connection = uiInterface.NotifyMasternodeListChanged_connect( + [&](const CDeterministicMNList&, const CBlockIndex*) { ++ui_mn_list_changed; }); + BlockValidationState background_state; + BOOST_CHECK(c1.ActivateBestChain(background_state, nullptr)); + WITH_LOCK(::cs_main, c1.ForceFlushStateToDisk()); + SyncWithValidationInterfaceQueue(); + ui_connection.disconnect(); + UnregisterValidationInterface(&event_counter); + BOOST_CHECK_EQUAL(c1.m_chain.Tip(), WITH_LOCK(::cs_main, return manager.ActiveChain().Genesis())); + BOOST_CHECK_EQUAL(event_counter.block_connected, 0); + BOOST_CHECK_EQUAL(event_counter.updated_tip, 0); + BOOST_CHECK_EQUAL(event_counter.mn_list_changed, 0); + BOOST_CHECK_EQUAL(event_counter.chainstate_flushed, 0); + BOOST_CHECK_EQUAL(ui_mn_list_changed, 0); + // Let scheduler events finish running to avoid accessing memory that is going to be unloaded SyncWithValidationInterfaceQueue(); diff --git a/src/validation.cpp b/src/validation.cpp index 8c4c43c54990..40f690267eae 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -2052,7 +2052,7 @@ DisconnectResult Chainstate::DisconnectBlock(const CBlock& block, const CBlockIn view.SetBestBlock(pindex->pprev->GetBlockHash()); m_evoDb.WriteBestBlock(EvoDbIdentity(), pindex->pprev->GetBlockHash()); - if (mnlist_updates_opt.has_value()) { + if (this == &m_chainman.ActiveChainstate() && mnlist_updates_opt.has_value()) { auto& mnlu = mnlist_updates_opt.value(); GetMainSignals().NotifyMasternodeListChanged(true, mnlu.old_list, mnlu.diff); uiInterface.NotifyMasternodeListChanged(mnlu.new_list, pindex->pprev); @@ -2520,7 +2520,7 @@ bool Chainstate::ConnectBlock(const CBlock& block, BlockValidationState& state, // Block is committed: keep the scheme it switched to (fJustCheck dry runs returned above). bls_scheme_guard.Commit(); - if (mnlist_updates_opt.has_value()) { + if (this == &m_chainman.ActiveChainstate() && mnlist_updates_opt.has_value()) { const auto& mnlu = mnlist_updates_opt.value(); GetMainSignals().NotifyMasternodeListChanged(false, mnlu.old_list, mnlu.diff); uiInterface.NotifyMasternodeListChanged(mnlu.new_list, pindex); @@ -2714,8 +2714,9 @@ bool Chainstate::FlushStateToDisk( (bool)fFlushForPrune); } } - if (full_flush_completed) { + if (full_flush_completed && this == &m_chainman.ActiveChainstate()) { // Update best block in wallet (so we can detect restored wallets). + // TODO(assumeutxo): upstream tags this notification with ChainstateRole instead of suppressing; adopt when backporting index/wallet assumeutxo support. GetMainSignals().ChainStateFlushed(m_chain.GetLocator()); } } catch (const std::runtime_error& e) { @@ -2908,7 +2909,9 @@ bool Chainstate::DisconnectTip(BlockValidationState& state, DisconnectedBlockTra UpdateTip(pindexDelete->pprev); // Let wallets know transactions went from 1-confirmed to // 0-confirmed or conflicted: - GetMainSignals().BlockDisconnected(pblock, pindexDelete); + if (this == &m_chainman.ActiveChainstate()) { + GetMainSignals().BlockDisconnected(pblock, pindexDelete); + } int64_t nTime2 = GetTimeMicros(); @@ -3335,9 +3338,11 @@ bool Chainstate::ActivateBestChain(BlockValidationState& state, std::shared_ptr< } pindexNewTip = m_chain.Tip(); - for (const PerBlockConnectTrace& trace : connectTrace.GetBlocksConnected()) { - assert(trace.pblock && trace.pindex); - GetMainSignals().BlockConnected(trace.pblock, trace.pindex); + if (this == &m_chainman.ActiveChainstate()) { + for (const PerBlockConnectTrace& trace : connectTrace.GetBlocksConnected()) { + assert(trace.pblock && trace.pindex); + GetMainSignals().BlockConnected(trace.pblock, trace.pindex); + } } } while (!m_chain.Tip() || (starting_tip && CBlockIndexWorkComparator()(m_chain.Tip(), starting_tip))); if (!blocks_connected) return true; @@ -3347,7 +3352,7 @@ bool Chainstate::ActivateBestChain(BlockValidationState& state, std::shared_ptr< // Notify external listeners about the new tip. // Enqueue while holding cs_main to ensure that UpdatedBlockTip is called in the order in which blocks are connected - if (pindexFork != pindexNewTip) { + if (this == &m_chainman.ActiveChainstate() && pindexFork != pindexNewTip) { // Notify ValidationInterface subscribers GetMainSignals().SynchronousUpdatedBlockTip(pindexNewTip, pindexFork, fInitialDownload); GetMainSignals().UpdatedBlockTip(pindexNewTip, pindexFork, fInitialDownload); @@ -3556,8 +3561,10 @@ bool Chainstate::InvalidateBlock(BlockValidationState& state, CBlockIndex* pinde } InvalidChainFound(to_mark_failed); - GetMainSignals().SynchronousUpdatedBlockTip(m_chain.Tip(), nullptr, IsInitialBlockDownload()); - GetMainSignals().UpdatedBlockTip(m_chain.Tip(), nullptr, IsInitialBlockDownload()); + 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. @@ -3659,8 +3666,10 @@ bool Chainstate::MarkConflictingBlock(BlockValidationState& state, CBlockIndex * } ConflictingChainFound(pindex); - GetMainSignals().SynchronousUpdatedBlockTip(m_chain.Tip(), nullptr, IsInitialBlockDownload()); - GetMainSignals().UpdatedBlockTip(m_chain.Tip(), nullptr, IsInitialBlockDownload()); + 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) { @@ -5783,6 +5792,12 @@ bool ChainstateManager::IsSnapshotActive() const return m_snapshot_chainstate && m_active_chainstate == m_snapshot_chainstate.get(); } +bool ChainstateManager::IsSnapshotActiveAndUnvalidated() const +{ + LOCK(::cs_main); + return m_snapshot_chainstate && m_active_chainstate == m_snapshot_chainstate.get() && !m_snapshot_validated; +} + bool ChainstateManager::IsQuorumTypeEnabled(const Consensus::LLMQType llmqType, gsl::not_null pindexPrev, std::optional optDIP0024IsActive, diff --git a/src/validation.h b/src/validation.h index 35aa56f78f68..19e3aed244ce 100644 --- a/src/validation.h +++ b/src/validation.h @@ -1058,6 +1058,9 @@ class ChainstateManager //! Is there a snapshot in use and has it been fully validated? bool IsSnapshotValidated() const EXCLUSIVE_LOCKS_REQUIRED(::cs_main) { return m_snapshot_validated; } + //! Whether active-state-dependent masternode duties must remain disabled. + bool IsSnapshotActiveAndUnvalidated() const; + /** * Process an incoming block. This only returns after the best known valid * block is made active. Note that it does not, however, guarantee that the From bde6cada2072c5a60c06ddc151239f545dde5281 Mon Sep 17 00:00:00 2001 From: Pasta Date: Sat, 11 Jul 2026 00:26:32 -0500 Subject: [PATCH 08/10] dash: guard serving unavailable snapshot history 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. --- src/evo/smldiff.cpp | 28 +++++++++++++++++-- src/evo/smldiff.h | 6 +++- src/llmq/snapshot.cpp | 15 ++++++++++ src/net_processing.cpp | 16 +++++++++-- .../validation_chainstatemanager_tests.cpp | 27 ++++++++++++++++++ 5 files changed, 86 insertions(+), 6 deletions(-) diff --git a/src/evo/smldiff.cpp b/src/evo/smldiff.cpp index 3c0991ea33c4..30d953054a8d 100644 --- a/src/evo/smldiff.cpp +++ b/src/evo/smldiff.cpp @@ -67,7 +67,8 @@ bool CSimplifiedMNListDiff::BuildQuorumsDiff(const CBlockIndex* baseBlockIndex, return true; } -bool CSimplifiedMNListDiff::BuildQuorumChainlockInfo(const llmq::CQuorumManager& qman, const CBlockIndex* blockIndex) +bool CSimplifiedMNListDiff::BuildQuorumChainlockInfo(const llmq::CQuorumManager& qman, const CBlockIndex* blockIndex, + std::string& errorRet) { // Group quorums (indexes corresponding to entries of newQuorums) per CBlockIndex containing the expected CL // signature in CbTx. We want to avoid to load CbTx now, as more than one quorum will target the same block: hence @@ -89,6 +90,12 @@ bool CSimplifiedMNListDiff::BuildQuorumChainlockInfo(const llmq::CQuorumManager& // first DKG) - 8 In case of non-rotation, quorums rely on the CL sig expected in the block of the DKG - 8 const CBlockIndex* pWorkBaseBlockIndex = blockIndex->GetAncestor(quorum->m_quorum_base_block_index->nHeight - quorum->qc->quorumIndex - 8); + if (!(pWorkBaseBlockIndex->nStatus & BLOCK_HAVE_DATA)) { + errorRet = strprintf("block data for quorum work block %s is not available (pruned or below an unvalidated " + "snapshot base)", + pWorkBaseBlockIndex->GetBlockHash().ToString()); + return false; + } workBaseBlockIndexMap.insert(std::make_pair(pWorkBaseBlockIndex, idx)); } @@ -182,6 +189,16 @@ bool BuildSimplifiedMNListDiff(CDeterministicMNManager& dmnman, const Chainstate errorRet = strprintf("base block %s is higher then block %s", baseBlockHash.ToString(), blockHash.ToString()); return false; } + if (!(baseBlockIndex->nStatus & BLOCK_HAVE_DATA)) { + errorRet = strprintf("block data for base block %s is not available (pruned or below an unvalidated snapshot base)", + baseBlockIndex->GetBlockHash().ToString()); + return false; + } + if (!(blockIndex->nStatus & BLOCK_HAVE_DATA)) { + errorRet = strprintf("block data for block %s is not available (pruned or below an unvalidated snapshot base)", + blockIndex->GetBlockHash().ToString()); + return false; + } auto baseDmnList = dmnman.GetListForBlock(baseBlockIndex); auto dmnList = dmnman.GetListForBlock(blockIndex); @@ -198,8 +215,8 @@ bool BuildSimplifiedMNListDiff(CDeterministicMNManager& dmnman, const Chainstate } if (DeploymentActiveAfter(blockIndex, chainman.GetConsensus(), Consensus::DEPLOYMENT_V20)) { - if (!mnListDiffRet.BuildQuorumChainlockInfo(qman, blockIndex)) { - errorRet = strprintf("failed to build quorum chainlock info"); + if (!mnListDiffRet.BuildQuorumChainlockInfo(qman, blockIndex, errorRet)) { + if (errorRet.empty()) errorRet = strprintf("failed to build quorum chainlock info"); return false; } } @@ -223,3 +240,8 @@ bool BuildSimplifiedMNListDiff(CDeterministicMNManager& dmnman, const Chainstate return true; } + +bool IsBlockDataUnavailableError(const std::string& error) +{ + return error.find("is not available (pruned or below an unvalidated snapshot base)") != std::string::npos; +} diff --git a/src/evo/smldiff.h b/src/evo/smldiff.h index 4dfc692511fc..c7a79340fcb8 100644 --- a/src/evo/smldiff.h +++ b/src/evo/smldiff.h @@ -84,7 +84,8 @@ class CSimplifiedMNListDiff bool BuildQuorumsDiff(const CBlockIndex* baseBlockIndex, const CBlockIndex* blockIndex, const llmq::CQuorumBlockProcessor& quorum_block_processor); - bool BuildQuorumChainlockInfo(const llmq::CQuorumManager& qman, const CBlockIndex* blockIndex); + bool BuildQuorumChainlockInfo(const llmq::CQuorumManager& qman, const CBlockIndex* blockIndex, std::string& errorRet) + EXCLUSIVE_LOCKS_REQUIRED(::cs_main); [[nodiscard]] static RPCResult GetJsonHelp(const std::string& key, bool optional); [[nodiscard]] UniValue ToJson(bool extended = false) const; @@ -95,4 +96,7 @@ bool BuildSimplifiedMNListDiff(CDeterministicMNManager& dmnman, const Chainstate const uint256& baseBlockHash, const uint256& blockHash, CSimplifiedMNListDiff& mnListDiffRet, std::string& errorRet, bool extended = false) EXCLUSIVE_LOCKS_REQUIRED(::cs_main); +/** Whether a serving failure is caused by this node not retaining the requested block data. */ +bool IsBlockDataUnavailableError(const std::string& error); + #endif // BITCOIN_EVO_SMLDIFF_H diff --git a/src/llmq/snapshot.cpp b/src/llmq/snapshot.cpp index 389b624c01fd..ee5d09a3b26a 100644 --- a/src/llmq/snapshot.cpp +++ b/src/llmq/snapshot.cpp @@ -19,10 +19,20 @@ namespace { constexpr std::string_view DB_QUORUM_SNAPSHOT{"llmq_S"}; +bool CheckBlockDataAvailable(gsl::not_null pindex, std::string& error) + EXCLUSIVE_LOCKS_REQUIRED(::cs_main) +{ + if (pindex->nStatus & BLOCK_HAVE_DATA) return true; + error = strprintf("block data for block %s is not available (pruned or below an unvalidated snapshot base)", + pindex->GetBlockHash().ToString()); + return false; +} + //! Constructs a llmq::CycleData and populate it with metadata std::optional ConstructCycle(llmq::CQuorumSnapshotManager& qsnapman, const Consensus::LLMQType& llmq_type, bool skip_snap, int32_t height, gsl::not_null index_tip, std::string& error) + EXCLUSIVE_LOCKS_REQUIRED(::cs_main) { llmq::CycleData ret; ret.m_cycle_index = index_tip->GetAncestor(height); @@ -30,11 +40,13 @@ std::optional ConstructCycle(llmq::CQuorumSnapshotManager& qsna error = "Cannot find block"; return std::nullopt; } + if (!CheckBlockDataAvailable(ret.m_cycle_index, error)) return std::nullopt; ret.m_work_index = ret.m_cycle_index->GetAncestor(ret.m_cycle_index->nHeight - llmq::WORK_DIFF_DEPTH); if (!ret.m_work_index) { error = "Cannot find work block"; return std::nullopt; } + if (!CheckBlockDataAvailable(ret.m_work_index, error)) return std::nullopt; if (!skip_snap) { if (auto opt_snap = qsnapman.GetSnapshotForBlock(llmq_type, ret.m_cycle_index); opt_snap.has_value()) { ret.m_snap = opt_snap.value(); @@ -74,6 +86,7 @@ bool BuildQuorumRotationInfo(CDeterministicMNManager& dmnman, CQuorumSnapshotMan errorRet = strprintf("block %s is not in the active chain", blockHash.ToString()); return false; } + if (!CheckBlockDataAvailable(blockIndex, errorRet)) return false; baseBlockIndexes.push_back(blockIndex); } // Sort in all cases: the legacy path (served to peers < EFFICIENT_QRINFO_VERSION) @@ -93,6 +106,7 @@ bool BuildQuorumRotationInfo(CDeterministicMNManager& dmnman, CQuorumSnapshotMan errorRet = strprintf("tip block not found"); return false; } + if (!CheckBlockDataAvailable(tipBlockIndex, errorRet)) return false; if (use_legacy_construction) { // Build MN list Diff always with highest baseblock if (!BuildSimplifiedMNListDiff(dmnman, chainman, qblockman, qman, baseBlockIndexes.back()->GetBlockHash(), @@ -106,6 +120,7 @@ bool BuildQuorumRotationInfo(CDeterministicMNManager& dmnman, CQuorumSnapshotMan errorRet = strprintf("block not found"); return false; } + if (!CheckBlockDataAvailable(blockIndex, errorRet)) return false; // Quorum rotation is enabled only for InstantSend atm. Consensus::LLMQType llmqType = Params().GetConsensus().llmqTypeDIP0024InstantSend; diff --git a/src/net_processing.cpp b/src/net_processing.cpp index b8d059ae1abd..2aa086d7f80d 100644 --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -5509,7 +5509,15 @@ void PeerManagerImpl::ProcessMessage( m_connman.PushMessage(&pfrom, msgMaker.Make(NetMsgType::MNLISTDIFF, mnListDiff)); } else { strError = strprintf("getmnlistdiff failed for baseBlockHash=%s, blockHash=%s. error=%s", cmd.baseBlockHash.ToString(), cmd.blockHash.ToString(), strError); - Misbehaving(*peer, 1, strError); + if (IsBlockDataUnavailableError(strError)) { + // The peer made a plausible request which this pruned or + // snapshot-backed node cannot serve. Like pruned getdata, + // silently drop it without attributing our missing history to + // the peer. + LogPrint(BCLog::NET, "%s\n", strError); + } else { + Misbehaving(*peer, 1, strError); + } } return; } @@ -5549,7 +5557,11 @@ void PeerManagerImpl::ProcessMessage( m_connman.PushMessage(&pfrom, msgMaker.Make(NetMsgType::QUORUMROTATIONINFO, quorumRotationInfoRet)); } else { strError = strprintf("getquorumrotationinfo failed for size(baseBlockHashes)=%d, blockRequestHash=%s. error=%s", cmd.baseBlockHashes.size(), cmd.blockRequestHash.ToString(), strError); - Misbehaving(*peer, 1, strError); + if (IsBlockDataUnavailableError(strError)) { + LogPrint(BCLog::NET, "%s\n", strError); + } else { + Misbehaving(*peer, 1, strError); + } } return; } diff --git a/src/test/validation_chainstatemanager_tests.cpp b/src/test/validation_chainstatemanager_tests.cpp index 292da34bbeb7..cc73ed9f8ad5 100644 --- a/src/test/validation_chainstatemanager_tests.cpp +++ b/src/test/validation_chainstatemanager_tests.cpp @@ -22,6 +22,7 @@ #include #include +#include #include #include #include @@ -825,6 +826,32 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager_mined_commitment_is_chain_aware, Snaps BOOST_CHECK(qman.GetQuorum(llmq_type, quorum_hash, snapshot_chainstate->m_chain) != nullptr); BOOST_CHECK(qman.GetQuorum(llmq_type, quorum_hash, background_chainstate->m_chain) == nullptr); + // The requested endpoints are available, but the quorum's ChainLock + // work block is below the unvalidated snapshot base and unavailable. + // Do not turn that read failure into a null ChainLock signature. + const CBlockIndex* request_base = snapshot_mined_block->pprev; + CBlockIndex* quorum_work_block = snapshot_chainstate->m_chain[quorum_base->nHeight - seeded_commitment.quorumIndex - 8]; + BOOST_REQUIRE(request_base->nStatus & BLOCK_HAVE_DATA); + BOOST_REQUIRE(snapshot_mined_block->nStatus & BLOCK_HAVE_DATA); + BOOST_REQUIRE_LT(quorum_work_block->nHeight, request_base->nHeight); + const uint32_t old_work_status = quorum_work_block->nStatus; + auto& consensus = const_cast(m_node.chainman->GetConsensus()); + const int old_v20_height = consensus.V20Height; + quorum_work_block->nStatus &= ~BLOCK_HAVE_DATA; + consensus.V20Height = 1; + + CSimplifiedMNListDiff response; + std::string error; + BOOST_CHECK(!BuildSimplifiedMNListDiff(*m_node.dmnman, *m_node.chainman, qblockman, qman, + request_base->GetBlockHash(), snapshot_mined_block->GetBlockHash(), + response, error)); + + consensus.V20Height = old_v20_height; + quorum_work_block->nStatus = old_work_status; + BOOST_CHECK(IsBlockDataUnavailableError(error)); + BOOST_CHECK(error.find(quorum_work_block->GetBlockHash().ToString()) != std::string::npos); + BOOST_CHECK(response.quorumsCLSigs.empty()); + BOOST_CHECK(qblockman.HasMinedCommitment(llmq_type, quorum_hash)); BOOST_CHECK(qblockman.HasMinedCommitment(llmq_type, quorum_hash, snapshot_chainstate->m_chain)); BOOST_CHECK(!qblockman.HasMinedCommitment(llmq_type, quorum_hash, background_chainstate->m_chain)); From b6b89c5496fe4cbfb6312606d31fee50efa9a718 Mon Sep 17 00:00:00 2001 From: Pasta Date: Sat, 11 Jul 2026 00:26:43 -0500 Subject: [PATCH 09/10] dash: refuse masternode duty on unvalidated snapshots 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. --- src/active/context.cpp | 13 +++++++++++ src/active/context.h | 3 +++ src/active/dkgsessionhandler.cpp | 23 +++++++++++++++++++ src/llmq/signing_shares.cpp | 17 ++++++++++++++ src/llmq/signing_shares.h | 1 + src/rpc/masternode.cpp | 7 +++++- src/rpc/quorums.cpp | 4 ++++ .../validation_chainstatemanager_tests.cpp | 4 ++++ 8 files changed, 71 insertions(+), 1 deletion(-) diff --git a/src/active/context.cpp b/src/active/context.cpp index 7dca348d11a7..0798746f5d39 100644 --- a/src/active/context.cpp +++ b/src/active/context.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -35,6 +36,7 @@ ActiveContext::ActiveContext(CBLSWorker& bls_worker, ChainstateManager& chainman const CBLSSecretKey& operator_sk, const util::DbWrapperParams& db_params, bool quorums_watch) : llmq::QuorumRole{qman}, m_bls_worker{bls_worker}, + m_chainman{chainman}, m_quorums_watch{quorums_watch}, nodeman{std::make_unique(connman, dmnman, operator_sk)}, dkgdbgman{std::make_unique(dmnman, qsnapman, chainman)}, @@ -94,6 +96,17 @@ void ActiveContext::UpdatedBlockTip(const CBlockIndex* pindexNew, const CBlockIn return; 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"); + } + ehf_sighandler->UpdatedBlockTip(pindexNew); gov_signer->UpdatedBlockTip(pindexNew); qdkgsman->UpdatedBlockTip(pindexNew, fInitialDownload); diff --git a/src/active/context.h b/src/active/context.h index 75d092920c4a..ecff35a188cf 100644 --- a/src/active/context.h +++ b/src/active/context.h @@ -12,6 +12,7 @@ #include #include +#include #include class CActiveMasternodeManager; @@ -49,7 +50,9 @@ struct DbWrapperParams; struct ActiveContext final : public llmq::QuorumRole, public CValidationInterface { private: CBLSWorker& m_bls_worker; + ChainstateManager& m_chainman; const bool m_quorums_watch{false}; + std::atomic_bool m_snapshot_duty_blocked{false}; public: ActiveContext() = delete; diff --git a/src/active/dkgsessionhandler.cpp b/src/active/dkgsessionhandler.cpp index 8ea565e8f53f..56cfedf0d42f 100644 --- a/src/active/dkgsessionhandler.cpp +++ b/src/active/dkgsessionhandler.cpp @@ -14,6 +14,7 @@ #include #include #include +#include namespace llmq { ActiveDKGSessionHandler::ActiveDKGSessionHandler( @@ -41,6 +42,8 @@ ActiveDKGSessionHandler::~ActiveDKGSessionHandler() = default; void ActiveDKGSessionHandler::UpdatedBlockTip(const CBlockIndex* pindexNew) { + if (m_chainman.IsSnapshotActiveAndUnvalidated()) return; + //AssertLockNotHeld(cs_main); //Indexed quorums (greater than 0) are enabled with Quorum Rotation if (quorumIndex > 0 && !IsQuorumRotationEnabled(params, pindexNew)) { @@ -76,6 +79,10 @@ std::pair ActiveDKGSessionHandler::GetPhaseAndQuorumHash() bool ActiveDKGSessionHandler::InitNewQuorum(gsl::not_null pQuorumBaseBlockIndex) { + if (m_chainman.IsSnapshotActiveAndUnvalidated()) { + LogPrint(BCLog::LLMQ_DKG, "%s -- refusing DKG participation while snapshot background validation is incomplete\n", __func__); + return false; + } if (!DeploymentDIP0003Enforced(pQuorumBaseBlockIndex->nHeight, Params().GetConsensus())) { return false; } @@ -100,6 +107,10 @@ void ActiveDKGSessionHandler::WaitForNextPhase(std::optional curPha LogPrint(BCLog::LLMQ_DKG, "ActiveDKGSessionHandler::%s -- %s qi[%d] - starting, curPhase=%d, nextPhase=%d\n", __func__, params.name, quorumIndex, curPhase.has_value() ? std23::to_underlying(*curPhase) : -1, std23::to_underlying(nextPhase)); while (true) { + if (m_chainman.IsSnapshotActiveAndUnvalidated()) { + LogPrint(BCLog::LLMQ_DKG, "ActiveDKGSessionHandler::%s -- %s qi[%d] - aborting because snapshot background validation is incomplete\n", __func__, params.name, quorumIndex); + throw AbortPhaseException(); + } if (stopRequested) { LogPrint(BCLog::LLMQ_DKG, "ActiveDKGSessionHandler::%s -- %s qi[%d] - aborting due to stop/shutdown requested\n", __func__, params.name, quorumIndex); throw AbortPhaseException(); @@ -139,6 +150,10 @@ void ActiveDKGSessionHandler::WaitForNewQuorum(const uint256& oldQuorumHash) con LogPrint(BCLog::LLMQ_DKG, "ActiveDKGSessionHandler::%s -- %s qi[%d]- starting\n", __func__, params.name, quorumIndex); while (true) { + if (m_chainman.IsSnapshotActiveAndUnvalidated()) { + LogPrint(BCLog::LLMQ_DKG, "ActiveDKGSessionHandler::%s -- %s qi[%d] - aborting because snapshot background validation is incomplete\n", __func__, params.name, quorumIndex); + throw AbortPhaseException(); + } if (stopRequested) { LogPrint(BCLog::LLMQ_DKG, "ActiveDKGSessionHandler::%s -- %s qi[%d] - aborting due to stop/shutdown requested\n", __func__, params.name, quorumIndex); throw AbortPhaseException(); @@ -186,6 +201,10 @@ void ActiveDKGSessionHandler::SleepBeforePhase(QuorumPhase curPhase, const uint2 LogPrint(BCLog::LLMQ_DKG, "ActiveDKGSessionHandler::%s -- %s qi[%d] - starting sleep for %d ms, curPhase=%d\n", __func__, params.name, quorumIndex, sleepTime, std23::to_underlying(curPhase)); while (SteadyClock::now() < endTime) { + if (m_chainman.IsSnapshotActiveAndUnvalidated()) { + LogPrint(BCLog::LLMQ_DKG, "ActiveDKGSessionHandler::%s -- %s qi[%d] - aborting because snapshot background validation is incomplete\n", __func__, params.name, quorumIndex); + throw AbortPhaseException(); + } if (stopRequested) { LogPrint(BCLog::LLMQ_DKG, "ActiveDKGSessionHandler::%s -- %s qi[%d] - aborting due to stop/shutdown requested\n", __func__, params.name, quorumIndex); throw AbortPhaseException(); @@ -220,6 +239,10 @@ void ActiveDKGSessionHandler::HandlePhase(QuorumPhase curPhase, QuorumPhase next LogPrint(BCLog::LLMQ_DKG, "ActiveDKGSessionHandler::%s -- %s qi[%d] - starting, curPhase=%d, nextPhase=%d\n", __func__, params.name, quorumIndex, std23::to_underlying(curPhase), std23::to_underlying(nextPhase)); SleepBeforePhase(curPhase, expectedQuorumHash, randomSleepFactor, runWhileWaiting); + if (m_chainman.IsSnapshotActiveAndUnvalidated()) { + LogPrint(BCLog::LLMQ_DKG, "%s -- refusing DKG participation while snapshot background validation is incomplete\n", __func__); + throw AbortPhaseException(); + } startPhaseFunc(); WaitForNextPhase(curPhase, nextPhase, expectedQuorumHash, runWhileWaiting); diff --git a/src/llmq/signing_shares.cpp b/src/llmq/signing_shares.cpp index bec02769c7ab..c08f276229fe 100644 --- a/src/llmq/signing_shares.cpp +++ b/src/llmq/signing_shares.cpp @@ -767,6 +767,11 @@ bool CSigSharesManager::AsyncSignIfMember(Consensus::LLMQType llmqType, CSigning { AssertLockNotHeld(cs_pendingSigns); + if (!IsQuorumSigningAllowed(m_chainman)) { + LogPrint(BCLog::LLMQ, "%s -- refusing quorum signature while snapshot background validation is incomplete\n", __func__); + return false; + } + if (m_mn_activeman.GetProTxHash().IsNull()) return false; auto quorum = [&]() { @@ -1511,6 +1516,11 @@ void CSigSharesManager::AsyncSign(CQuorumCPtr quorum, const uint256& id, const u pendingSigns.emplace_back(std::move(quorum), id, msgHash); } +bool CSigSharesManager::IsQuorumSigningAllowed(const ChainstateManager& chainman) +{ + return !chainman.IsSnapshotActiveAndUnvalidated(); +} + std::optional CSigSharesManager::CreateSigShareForSingleMember(const CQuorum& quorum, const uint256& id, const uint256& msgHash) const { cxxtimer::Timer t(true); @@ -1550,6 +1560,13 @@ std::optional CSigSharesManager::CreateSigShareForSingleMember(const std::optional CSigSharesManager::CreateSigShare(const CQuorum& quorum, const uint256& id, const uint256& msgHash) const { + // This is the signature-production boundary. Keep the gate here so direct + // callers (including `quorum sign ... submit=false`) cannot bypass it. + if (!IsQuorumSigningAllowed(m_chainman)) { + LogPrint(BCLog::LLMQ, "%s -- refusing quorum signature while snapshot background validation is incomplete\n", __func__); + return std::nullopt; + } + auto activeMasterNodeProTxHash = m_mn_activeman.GetProTxHash(); if (!quorum.IsValidMember(activeMasterNodeProTxHash)) { diff --git a/src/llmq/signing_shares.h b/src/llmq/signing_shares.h index 3fdfe8e80033..c0ea2130c90f 100644 --- a/src/llmq/signing_shares.h +++ b/src/llmq/signing_shares.h @@ -498,6 +498,7 @@ class CSigSharesManager : public llmq::CRecoveredSigsListener void AsyncSign(CQuorumCPtr quorum, const uint256& id, const uint256& msgHash) EXCLUSIVE_LOCKS_REQUIRED(!cs_pendingSigns, !cs); + static bool IsQuorumSigningAllowed(const ChainstateManager& chainman); std::optional CreateSigShare(const CQuorum& quorum, const uint256& id, const uint256& msgHash) const EXCLUSIVE_LOCKS_REQUIRED(!cs); void ForceReAnnouncement(const CQuorum& quorum, Consensus::LLMQType llmqType, const uint256& id, diff --git a/src/rpc/masternode.cpp b/src/rpc/masternode.cpp index 2eb8940767b1..f07711db6f3a 100644 --- a/src/rpc/masternode.cpp +++ b/src/rpc/masternode.cpp @@ -190,6 +190,7 @@ static RPCHelpMan masternode_status() CDeterministicMNState::GetJsonHelp(/*key=*/"dmnState", /*optional=*/true), {RPCResult::Type::STR, "state", "Masternode state (human-readable string)"}, {RPCResult::Type::STR, "status", "Masternode status (human-readable string, based on current state)"}, + {RPCResult::Type::BOOL, "quorumParticipation", "Whether DKG participation and quorum signing are enabled"}, } }, RPCExamples{""}, @@ -215,7 +216,11 @@ static RPCHelpMan masternode_status() mnObj.pushKV("dmnState", dmn->pdmnState->ToJson(dmn->nType)); } mnObj.pushKV("state", mn_activeman.GetStateString()); - mnObj.pushKV("status", mn_activeman.GetStatus()); + const bool quorum_participation = !EnsureChainman(node).IsSnapshotActiveAndUnvalidated(); + mnObj.pushKV("status", quorum_participation ? mn_activeman.GetStatus() : + strprintf("%s; DKG participation and quorum signing disabled until snapshot background validation completes", + mn_activeman.GetStatus())); + mnObj.pushKV("quorumParticipation", quorum_participation); return mnObj; }, diff --git a/src/rpc/quorums.cpp b/src/rpc/quorums.cpp index 3b09ad18ed33..11cb8b8c3411 100644 --- a/src/rpc/quorums.cpp +++ b/src/rpc/quorums.cpp @@ -529,6 +529,10 @@ static UniValue quorum_sign_helper(const JSONRPCRequest& request, Consensus::LLM if (!request.params[3].isNull()) { fSubmit = ParseBoolV(request.params[3], "submit"); } + if (!llmq::CSigSharesManager::IsQuorumSigningAllowed(chainman)) { + throw JSONRPCError(RPC_MISC_ERROR, + "Quorum signing is disabled until snapshot background validation completes"); + } if (fSubmit) { return CHECK_NONFATAL(node.active_ctx)->shareman->AsyncSignIfMember(llmqType, *llmq_ctx.sigman, id, msgHash, quorumHash); } else { diff --git a/src/test/validation_chainstatemanager_tests.cpp b/src/test/validation_chainstatemanager_tests.cpp index cc73ed9f8ad5..0d451f4c55bf 100644 --- a/src/test/validation_chainstatemanager_tests.cpp +++ b/src/test/validation_chainstatemanager_tests.cpp @@ -94,6 +94,8 @@ BOOST_AUTO_TEST_CASE(chainstatemanager) BOOST_REQUIRE(c1.LoadGenesisBlock()); BOOST_CHECK(!manager.IsSnapshotActive()); + BOOST_CHECK(!manager.IsSnapshotActiveAndUnvalidated()); + BOOST_CHECK(llmq::CSigSharesManager::IsQuorumSigningAllowed(manager)); BOOST_CHECK(WITH_LOCK(::cs_main, return !manager.IsSnapshotValidated())); auto all = manager.GetAll(); BOOST_CHECK_EQUAL_COLLECTIONS(all.begin(), all.end(), chainstates.begin(), chainstates.end()); @@ -140,6 +142,8 @@ BOOST_AUTO_TEST_CASE(chainstatemanager) BOOST_CHECK(manager.IsSnapshotActive()); BOOST_CHECK(WITH_LOCK(::cs_main, return !manager.IsSnapshotValidated())); + BOOST_CHECK(manager.IsSnapshotActiveAndUnvalidated()); + BOOST_CHECK(!llmq::CSigSharesManager::IsQuorumSigningAllowed(manager)); BOOST_CHECK_EQUAL(&c2, &manager.ActiveChainstate()); BOOST_CHECK(&c1 != &manager.ActiveChainstate()); auto all2 = manager.GetAll(); From 47dac03a035bb386c067b9144951672e57e8b9ff Mon Sep 17 00:00:00 2001 From: Pasta Date: Sat, 11 Jul 2026 01:23:04 -0500 Subject: [PATCH 10/10] fix: canonicalize serialization of block-derived EvoDB payloads --- src/evo/chainhelper.cpp | 2 +- src/evo/chainhelper.h | 4 ++-- src/evo/deterministicmns.h | 12 ++++++++++-- src/evo/evodb.h | 4 ++++ src/node/miner.cpp | 2 +- src/rpc/blockchain.cpp | 2 +- src/test/evo_deterministicmns_tests.cpp | 19 +++++++++++++++++++ src/versionbits.h | 2 +- 8 files changed, 39 insertions(+), 8 deletions(-) diff --git a/src/evo/chainhelper.cpp b/src/evo/chainhelper.cpp index 06a610e92194..acffa3cd2d4a 100644 --- a/src/evo/chainhelper.cpp +++ b/src/evo/chainhelper.cpp @@ -85,7 +85,7 @@ bool CChainstateHelper::RemoveConflictingISLockByTx(const CTransaction& tx) return true; } -std::unordered_map CChainstateHelper::GetSignalsStage(const CBlockIndex* const pindexPrev) +std::map CChainstateHelper::GetSignalsStage(const CBlockIndex* const pindexPrev) { return ehf_manager->GetSignalsStage(pindexPrev); } diff --git a/src/evo/chainhelper.h b/src/evo/chainhelper.h index f3d0cbc2c34c..f68c48bd26bf 100644 --- a/src/evo/chainhelper.h +++ b/src/evo/chainhelper.h @@ -6,9 +6,9 @@ #define BITCOIN_EVO_CHAINHELPER_H #include +#include #include #include -#include class CBlockIndex; class CCreditPoolManager; @@ -77,7 +77,7 @@ class CChainstateHelper bool IsInstantSendWaitingForTx(const uint256& hash) const; bool RemoveConflictingISLockByTx(const CTransaction& tx); - std::unordered_map GetSignalsStage(const CBlockIndex* const pindexPrev); + std::map GetSignalsStage(const CBlockIndex* const pindexPrev); }; #endif // BITCOIN_EVO_CHAINHELPER_H diff --git a/src/evo/deterministicmns.h b/src/evo/deterministicmns.h index 3a684ffa78df..f79df8990adf 100644 --- a/src/evo/deterministicmns.h +++ b/src/evo/deterministicmns.h @@ -21,11 +21,13 @@ #include #include +#include #include #include #include #include #include +#include class CBlock; class CBlockIndex; @@ -593,9 +595,15 @@ class CDeterministicMNListDiff s << addedMNs; WriteCompactSize(s, updatedMNs.size()); - for (const auto& [internalId, pdmnState] : updatedMNs) { + std::vector updatedMNsInternalIds; + updatedMNsInternalIds.reserve(updatedMNs.size()); + for (const auto& entry : updatedMNs) { + updatedMNsInternalIds.emplace_back(entry.first); + } + std::sort(updatedMNsInternalIds.begin(), updatedMNsInternalIds.end()); + for (const auto& internalId : updatedMNsInternalIds) { WriteVarInt(s, internalId); - s << pdmnState; + s << updatedMNs.at(internalId); } WriteCompactSize(s, removedMns.size()); diff --git a/src/evo/evodb.h b/src/evo/evodb.h index 8d55cae91027..40567a326acf 100644 --- a/src/evo/evodb.h +++ b/src/evo/evodb.h @@ -116,6 +116,10 @@ class CEvoDB /** * Write immutable block-derived data, accepting an identical existing value. + * V must have canonical serialization: its bytes must be a pure function of + * its logical content. Audited call-site types are CDeterministicMNListDiff, + * CDeterministicMNList, AbstractEHFManager::Signals, the mined-commitment + * pair, and CCreditPool. * TODO(assumeutxo): WriteDerived spot-checks are not the holistic base-state * comparison required at snapshot completion. */ diff --git a/src/node/miner.cpp b/src/node/miner.cpp index 656e21d35fd8..9a9327a7a6f3 100644 --- a/src/node/miner.cpp +++ b/src/node/miner.cpp @@ -473,7 +473,7 @@ void BlockAssembler::addPackageTxs(const CTxMemPool& mempool, int& nPackagesSele } // This map with signals is used only to find duplicates - std::unordered_map signals = m_chain_helper.ehf_manager->GetSignalsStage(pindexPrev); + auto signals = m_chain_helper.ehf_manager->GetSignalsStage(pindexPrev); // mapModifiedTx will store sorted packages after they are modified // because some of their txs are already in the block diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index c46b1358c81b..6253b53aef7e 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -1457,7 +1457,7 @@ static void SoftForkDescPushBack(const CBlockIndex* active_chain_tip, UniValue& softforks.pushKV(DeploymentName(dep), rv); } -static void SoftForkDescPushBack(const CBlockIndex* active_chain_tip, const std::unordered_map& signals, UniValue& softforks, const ChainstateManager& chainman, Consensus::DeploymentPos id) +static void SoftForkDescPushBack(const CBlockIndex* active_chain_tip, const AbstractEHFManager::Signals& signals, UniValue& softforks, const ChainstateManager& chainman, Consensus::DeploymentPos id) { // For BIP9 deployments. diff --git a/src/test/evo_deterministicmns_tests.cpp b/src/test/evo_deterministicmns_tests.cpp index 4241faa7fa8f..e7c620ab7d6d 100644 --- a/src/test/evo_deterministicmns_tests.cpp +++ b/src/test/evo_deterministicmns_tests.cpp @@ -1506,6 +1506,25 @@ static void SmlCache(TestChainSetup& setup) BOOST_AUTO_TEST_SUITE(evo_dip3_activation_tests) +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()); +} + struct TestChainDIP3BeforeActivationSetup : public TestChainSetup { TestChainDIP3BeforeActivationSetup() : TestChainSetup(430) diff --git a/src/versionbits.h b/src/versionbits.h index 036fba26b423..ef26590d2ee8 100644 --- a/src/versionbits.h +++ b/src/versionbits.h @@ -105,7 +105,7 @@ class VersionBitsCache class AbstractEHFManager { public: - using Signals = std::unordered_map; + using Signals = std::map; public: AbstractEHFManager() = default;