From 990d950a7331e9e2d65f3be2d59d850a0733a2dd Mon Sep 17 00:00:00 2001 From: Pasta Date: Fri, 10 Jul 2026 23:28:48 -0500 Subject: [PATCH 01/17] evo: make CEvoDB consistency state per-chainstate --- src/dbwrapper.h | 16 ++++++ src/evo/evodb.cpp | 106 +++++++++++++++++++++++++++-------- src/evo/evodb.h | 137 ++++++++++++++++++++++++++++++++++++++-------- 3 files changed, 213 insertions(+), 46 deletions(-) diff --git a/src/dbwrapper.h b/src/dbwrapper.h index 4060d8138d99..95ef9fb9dca5 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..60f1768b3d78 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 = transaction_contexts.find(identity); + if (it == transaction_contexts.end()) { + // Construct the context before inserting so a throwing constructor + // cannot leave a null entry behind for later dereference. + it = transaction_contexts.emplace(identity, std::make_unique(*db)).first; + } + 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(EvoDbIdentity::NORMAL); +} + +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_DUAL_CHAINSTATE, uint8_t{1}); +} + +bool CEvoDB::HasDualChainstateMarker() { - Write(EVODB_BEST_BLOCK, hash); + LOCK(cs); + return db->Exists(EVODB_DUAL_CHAINSTATE); } diff --git a/src/evo/evodb.h b/src/evo/evodb.h index 38c6662633df..60e7a463866b 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(); @@ -38,16 +55,32 @@ class CEvoDBScopedCommitter class CEvoDB { public: - Mutex cs; + mutable Mutex cs; private: std::unique_ptr db; 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 GUARDED_BY(cs); + std::optional active_transaction GUARDED_BY(cs); + + TransactionContext& GetContext(EvoDbIdentity identity) EXCLUSIVE_LOCKS_REQUIRED(cs); + const TransactionContext& GetContext(EvoDbIdentity identity) const EXCLUSIVE_LOCKS_REQUIRED(cs); + EvoDbIdentity GetCurrentIdentity() const EXCLUSIVE_LOCKS_REQUIRED(cs); public: CEvoDB() = delete; @@ -56,44 +89,93 @@ 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); - } + 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() @@ -101,23 +183,34 @@ class CEvoDB return *db; } - [[nodiscard]] size_t GetMemoryUsage() const + [[nodiscard]] size_t GetMemoryUsage() const EXCLUSIVE_LOCKS_REQUIRED(!cs) { - return rootDBTransaction.GetMemoryUsage(); + LOCK(cs); + 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 3775ae42d5d30d5760e47cf3d9bd7ac176abc3ae Mon Sep 17 00:00:00 2001 From: Pasta Date: Fri, 10 Jul 2026 23:28:49 -0500 Subject: [PATCH 02/17] evo: wire per-chainstate transactions into 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 | 7 ++ src/node/chainstate.cpp | 12 ++- src/test/util/setup_common.cpp | 31 ++++---- src/test/util/setup_common.h | 18 +++-- .../validation_chainstatemanager_tests.cpp | 75 ++++++++++++++++-- src/validation.cpp | 78 +++++++++++++------ src/validation.h | 10 ++- 11 files changed, 263 insertions(+), 77 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 20db7a1f08ec..1c224090a9dc 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 b044f7c13dad..6028d4ae6455 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(const ChainstateManager& chainman, CDeterministicMNManager& dmnman, CEvoDB& evoDb, CQuorumSnapshotManager& qsnapman, int8_t bls_threads) : m_chainman{chainman}, @@ -332,8 +363,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"); } @@ -372,7 +406,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); @@ -477,15 +515,18 @@ bool CQuorumBlockProcessor::UndoBlock(const CBlock& block, gsl::not_nullnHeight, int(qc.quorumIndex))); + if (!EraseMinedCommitmentIfUnreferenced(m_evoDb, m_chainman.ActiveChainstate(), 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)); + } } // Only once this commitment's state change is complete; see ProcessCommitment. diff --git a/src/llmq/blockprocessor.h b/src/llmq/blockprocessor.h index 2932c2596c17..e8da65c9ab99 100644 --- a/src/llmq/blockprocessor.h +++ b/src/llmq/blockprocessor.h @@ -26,6 +26,7 @@ class CBlock; class CBlockIndex; class CBLSSignature; class CChain; +class Chainstate; class ChainstateManager; class CDataStream; class CDeterministicMNManager; @@ -44,6 +45,12 @@ using QcHashMap = std::map>; //! As above, but keyed by quorumIndex, for rotation-enabled types. using QcIndexedHashMap = std::map>; +/** 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 c86f6173afad..83c6127a80b7 100644 --- a/src/node/chainstate.cpp +++ b/src/node/chainstate.cpp @@ -66,7 +66,10 @@ ChainstateLoadResult LoadChainstate(ChainstateManager& chainman, const CacheSize chainman.InitializeChainstate(options.mempool, *evodb, chain_helper); // Load a chain created from a UTXO snapshot, if any exist. - chainman.DetectSnapshotChainstate(options.mempool); + 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 @@ -176,7 +179,12 @@ ChainstateLoadResult LoadChainstate(ChainstateManager& chainman, const CacheSize // 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()) { + // Commit every chainstate's own identity: ReplayBlocks processes + // special transactions, and its coins repair is flushed to disk + // immediately, so leaving a non-active identity's EvoDB writes in the + // in-memory overlay would let a crash strand the coins DB ahead of + // that identity's best-block marker. + if (!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 0a64da394f70..bb18cddd78ad 100644 --- a/src/test/util/setup_common.cpp +++ b/src/test/util/setup_common.cpp @@ -143,9 +143,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( @@ -218,7 +219,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) { @@ -233,10 +234,11 @@ BasicTestingSetup::~BasicTestingSetup() { SetMockTime(0s); // Reset mocktime for following tests LogInstance().DisconnectTestLogger(); + // Close disk-backed EvoDB before deleting its data directory. + 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(); @@ -248,8 +250,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(); @@ -306,7 +308,7 @@ void ChainTestingSetup::LoadVerifyActivateChainstate() options.data_dir = Assert(m_node.args)->GetDataDirNet(); 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; @@ -334,8 +336,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; @@ -407,8 +410,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} { } @@ -417,8 +421,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 132a5a14b868..92ad4f602b29 100644 --- a/src/test/util/setup_common.h +++ b/src/test/util/setup_common.h @@ -91,11 +91,14 @@ std::unique_ptr MakePeerManager(CConnman& connman, 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; }; @@ -108,7 +111,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 @@ -122,7 +127,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(); }; @@ -142,7 +148,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(); /** @@ -230,7 +237,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 5b576b3bc58e..cc5a9d8e4b52 100644 --- a/src/test/validation_chainstatemanager_tests.cpp +++ b/src/test/validation_chainstatemanager_tests.cpp @@ -34,6 +34,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) static void DashChainstateSetup(ChainstateManager& chainman, @@ -109,8 +121,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); @@ -185,7 +200,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); @@ -218,6 +237,7 @@ struct SnapshotTestSetup : TestChain100Setup { {}, /*coins_db_in_memory=*/false, /*block_tree_db_in_memory=*/false, + /*dash_dbs_in_memory=*/false, } { } @@ -315,6 +335,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); @@ -334,9 +363,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. @@ -371,6 +397,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}; @@ -503,8 +533,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(); @@ -575,4 +609,31 @@ 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_AUTO_TEST_SUITE_END() diff --git a/src/validation.cpp b/src/validation.cpp index d6d48d6b9e1f..a9f61d96f981 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -1620,6 +1620,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, @@ -1960,9 +1973,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; } @@ -2038,7 +2051,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); @@ -4383,7 +4396,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)) @@ -4471,7 +4484,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()) { @@ -4652,12 +4665,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) { @@ -4690,7 +4703,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(); @@ -5704,6 +5717,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; @@ -5845,33 +5869,43 @@ bool IsBIP30Unspendable(const CBlockIndex& block_index) DeploymentActiveAfter(pindexPrev, chainman, Consensus::DEPLOYMENT_V24)); } -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; + return m_snapshot_chainstate.get(); } diff --git a/src/validation.h b/src/validation.h index bf8c321fac78..a00cd9d59e7d 100644 --- a/src/validation.h +++ b/src/validation.h @@ -56,6 +56,7 @@ class CTxMemPool; class TxValidationState; class CChainstateHelper; class ChainstateManager; +enum class EvoDbIdentity; struct PrecomputedTransactionData; struct ChainTxData; struct DisconnectedBlockTransactions; @@ -534,6 +535,11 @@ class Chainstate const std::unique_ptr& chain_helper, std::optional from_snapshot_blockhash = std::nullopt); + //! 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. @@ -1102,13 +1108,13 @@ 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); //! Switch the active chainstate to one based on a UTXO snapshot that was loaded //! previously. - Chainstate& ActivateExistingSnapshot(CTxMemPool* mempool, uint256 base_blockhash) + Chainstate* ActivateExistingSnapshot(CTxMemPool* mempool, uint256 base_blockhash) EXCLUSIVE_LOCKS_REQUIRED(::cs_main); ~ChainstateManager(); From 8cd55e668017a493dc0c4c7d29f0a6b0ac36c9d9 Mon Sep 17 00:00:00 2001 From: Pasta Date: Fri, 10 Jul 2026 23:28:50 -0500 Subject: [PATCH 03/17] test: add dual-chainstate EvoDB consistency coverage --- src/Makefile.test.include | 1 + src/test/evo_db_tests.cpp | 174 ++++++++++++++++++ .../validation_chainstatemanager_tests.cpp | 98 +++++++++- 3 files changed, 268 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 5e9d47f8c4c7..b16f02f9aae3 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..42caa4a2890b --- /dev/null +++ b/src/test/evo_db_tests.cpp @@ -0,0 +1,174 @@ +// 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(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 cc5a9d8e4b52..1c5e641b5dc8 100644 --- a/src/test/validation_chainstatemanager_tests.cpp +++ b/src/test/validation_chainstatemanager_tests.cpp @@ -437,19 +437,21 @@ 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); BOOST_TEST_MESSAGE("Simulating node restart"); { LOCK(::cs_main); - for (Chainstate* cs : chainman.GetAll()) { - cs->ForceFlushStateToDisk(); + if (flush_chainstates) { + for (Chainstate* cs : chainman.GetAll()) { + cs->ForceFlushStateToDisk(); + } } DashChainstateSetupClose(m_node); chainman.ResetChainstates(); @@ -609,6 +611,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 2fc3c51c2792e99f646e25dce056154ca85fd393 Mon Sep 17 00:00:00 2001 From: Pasta Date: Sat, 11 Jul 2026 00:25:08 -0500 Subject: [PATCH 04/17] 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 | 107 ++++++++++-------- src/llmq/blockprocessor.h | 22 ++-- src/llmq/quorumsman.cpp | 69 ++++++++++- src/llmq/quorumsman.h | 16 +++ src/test/evo_cbtx_tests.cpp | 2 +- .../validation_chainstatemanager_tests.cpp | 70 ++++++++++++ src/validation.cpp | 10 +- 13 files changed, 343 insertions(+), 88 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 24fdd1082ea4..13b3b3d07d24 100644 --- a/src/evo/specialtxman.cpp +++ b/src/evo/specialtxman.cpp @@ -179,6 +179,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) @@ -215,11 +216,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()); @@ -232,7 +235,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); } @@ -629,7 +632,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); @@ -693,7 +696,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(), @@ -717,7 +721,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; } @@ -778,7 +782,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; @@ -816,7 +820,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); @@ -838,7 +842,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 b4e0d7168538..860e027a93de 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 6028d4ae6455..bcb51051379f 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(const ChainstateManager& chainman, CDeterministicMNManager& dmnman, - CEvoDB& evoDb, CQuorumSnapshotManager& qsnapman, int8_t bls_threads) : +CQuorumBlockProcessor::CQuorumBlockProcessor(ChainstateManager& chainman, CDeterministicMNManager& dmnman, CEvoDB& evoDb, + CQuorumSnapshotManager& qsnapman, int8_t bls_threads) : m_chainman{chainman}, m_dmnman{dmnman}, m_evoDb{evoDb}, m_qsnapman{qsnapman} { - utils::InitQuorumsCache(mapHasMinedCommitmentCache, 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); } @@ -138,7 +138,8 @@ MessageProcessingResult CQuorumBlockProcessor::ProcessMessage(const CNode& peer, const CBlockIndex* pQuorumBaseBlockIndex; { LOCK(::cs_main); - pQuorumBaseBlockIndex = m_chainman.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()); @@ -146,7 +147,7 @@ MessageProcessingResult CQuorumBlockProcessor::ProcessMessage(const CNode& peer, // fully synced return ret; } - if (m_chainman.ActiveChain().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 @@ -159,7 +160,7 @@ MessageProcessingResult CQuorumBlockProcessor::ProcessMessage(const CNode& peer, ret.m_error = MisbehavingError{100}; return ret; } - if (pQuorumBaseBlockIndex->nHeight < (m_chainman.ActiveChain().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) { @@ -206,7 +207,7 @@ 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); @@ -230,11 +231,11 @@ bool CQuorumBlockProcessor::ProcessBlock(const CBlock& block, gsl::not_nullpprev)) { // skip these checks when replaying blocks after the crash - if (m_chainman.ActiveChain().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) { @@ -253,7 +254,7 @@ 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_chainman.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()); @@ -268,7 +269,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; } @@ -308,7 +309,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); @@ -320,7 +321,7 @@ bool CQuorumBlockProcessor::ProcessCommitment(int nHeight, const uint256& blockH } const auto& llmq_params = llmq_params_opt.value(); - uint256 quorumHash = GetQuorumBlockHash(llmq_params, m_chainman.ActiveChain(), 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, " @@ -330,7 +331,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_chainman.ActiveChain().Tip() == nullptr) { + if (chainstate.m_chain.Tip() == nullptr) { quorumHash = qc.quorumHash; } @@ -371,12 +372,12 @@ bool CQuorumBlockProcessor::ProcessCommitment(int nHeight, const uint256& blockH return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "bad-qc-dup"); } - if (!IsMiningPhase(llmq_params, m_chainman.ActiveChain(), 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_chainman.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()); @@ -425,7 +426,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)); } @@ -498,7 +499,7 @@ std::optional> CQuorumBlockProcessor::Get return std::make_pair(m_qc_hashes_cached, m_qc_indexed_hashes_cached); } -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); @@ -515,7 +516,7 @@ bool CQuorumBlockProcessor::UndoBlock(const CBlock& block, gsl::not_nullGetBlockHash().ToString()); } else { @@ -532,7 +533,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_chainman.ActiveChain(), 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; @@ -626,34 +626,45 @@ uint256 CQuorumBlockProcessor::GetQuorumBlockHash(const Consensus::LLMQParams& l bool CQuorumBlockProcessor::HasMinedCommitment(Consensus::LLMQType llmqType, const uint256& quorumHash) const { - bool fExists; + LOCK(::cs_main); + return HasMinedCommitment(llmqType, quorumHash, m_chainman.ActiveChain()); +} + +bool CQuorumBlockProcessor::HasMinedCommitment(Consensus::LLMQType llmqType, const uint256& quorumHash, + const CChain& chain) const +{ + AssertLockHeld(::cs_main); + + uint256 mined_block_hash; + bool cached; { // Defence-in-depth: this map is only pre-seeded by InitQuorumsCache() with the LLMQ types // from the chain's consensus params. operator[] with any other type would insert a // default-constructed, zero-capacity cache and abort in its constructor, so treat an // unregistered type as "no mined commitment" rather than indexing the map. LOCK(minableCommitmentsCs); - auto it = mapHasMinedCommitmentCache.find(llmqType); - if (it == mapHasMinedCommitmentCache.end()) { + auto it = mapMinedCommitmentBlockCache.find(llmqType); + if (it == mapMinedCommitmentBlockCache.end()) { return false; } - if (it->second.get(quorumHash, fExists)) { - return fExists; - } - } - - fExists = m_evoDb.Exists(std::make_pair(DB_MINED_COMMITMENT, std::make_pair(llmqType, quorumHash))); - - { - LOCK(minableCommitmentsCs); - // The key set is fixed at construction, so this can only miss if the type was unregistered, - // which the check above already returned on. - if (auto it = mapHasMinedCommitmentCache.find(llmqType); it != mapHasMinedCommitmentCache.end()) { - it->second.insert(quorumHash, fExists); + cached = it->second.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); + // The key set is fixed at construction, so this can only miss if the type was + // unregistered, which the check above already returned on. + if (auto it = mapMinedCommitmentBlockCache.find(llmqType); it != mapMinedCommitmentBlockCache.end()) { + it->second.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, @@ -868,16 +879,16 @@ 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 - const CChain& active_chain{m_chainman.ActiveChain()}; assert(nHeight <= active_chain.Height() + 1); - const auto *const pindex = active_chain.Height() < nHeight ? active_chain.Tip() : active_chain.Tip()->GetAncestor(nHeight); + 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_chainman.GetConsensus(), Consensus::DEPLOYMENT_V19)}; @@ -887,7 +898,7 @@ std::optional> CQuorumBlockProcessor::GetMineableC for (const auto quorumIndex : util::irange(quorums_num)) { CFinalCommitment cf; - uint256 quorumHash = GetQuorumBlockHash(llmqParams, m_chainman.ActiveChain(), 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 e8da65c9ab99..5b1430cec0ba 100644 --- a/src/llmq/blockprocessor.h +++ b/src/llmq/blockprocessor.h @@ -54,7 +54,7 @@ bool EraseMinedCommitmentIfUnreferenced(CEvoDB& evo_db, const Chainstate& chains class CQuorumBlockProcessor { private: - const ChainstateManager& m_chainman; + ChainstateManager& m_chainman; CDeterministicMNManager& m_dmnman; CEvoDB& m_evoDb; CQuorumSnapshotManager& m_qsnapman; @@ -65,7 +65,9 @@ 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); // Memoizes GetQcHashes(). The whole-result cache is keyed on the set of active // quorum base blocks, the LRU on those base-block hashes; neither key identifies @@ -83,7 +85,7 @@ class CQuorumBlockProcessor CQuorumBlockProcessor() = delete; CQuorumBlockProcessor(const CQuorumBlockProcessor&) = delete; CQuorumBlockProcessor& operator=(const CQuorumBlockProcessor&) = delete; - explicit CQuorumBlockProcessor(const ChainstateManager& chainman, CDeterministicMNManager& dmnman, CEvoDB& evoDb, + explicit CQuorumBlockProcessor(ChainstateManager& chainman, CDeterministicMNManager& dmnman, CEvoDB& evoDb, CQuorumSnapshotManager& qsnapman, int8_t bls_threads); ~CQuorumBlockProcessor(); @@ -104,9 +106,9 @@ class CQuorumBlockProcessor const ConsumeRequestFn& consume_request) 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, !m_qc_hashes_cache_mutex); - 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, !m_qc_hashes_cache_mutex); //! it returns hash of commitment if it should be relay, otherwise nullopt @@ -121,6 +123,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; /** @@ -145,10 +149,14 @@ class CQuorumBlockProcessor void DropQcHashesCache() EXCLUSIVE_LOCKS_REQUIRED(!m_qc_hashes_cache_mutex); 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, !m_qc_hashes_cache_mutex); - 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/quorumsman.cpp b/src/llmq/quorumsman.cpp index d340631f6636..d58b73ef55e1 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(); @@ -370,6 +414,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/evo_cbtx_tests.cpp b/src/test/evo_cbtx_tests.cpp index 2f37ccf18f08..2e8231812d87 100644 --- a/src/test/evo_cbtx_tests.cpp +++ b/src/test/evo_cbtx_tests.cpp @@ -209,7 +209,7 @@ BOOST_FIXTURE_TEST_CASE(qc_hash_cache_invalidated_by_undoblock, Dip3ActiveSetup) { LOCK(cs_main); auto dbTx = evoDb.BeginTransaction(); - BOOST_REQUIRE(qblockman.UndoBlock(block_with_qc, &pindex_mined)); + BOOST_REQUIRE(qblockman.UndoBlock(m_node.chainman->ActiveChainstate(), block_with_qc, &pindex_mined)); // Install the replacement while the disconnect transaction is still open. WriteMinedCommitment(evoDb, qc_b, mined_hash_b, mined_height); dbTx->Commit(); diff --git a/src/test/validation_chainstatemanager_tests.cpp b/src/test/validation_chainstatemanager_tests.cpp index 1c5e641b5dc8..46556c2bcbb0 100644 --- a/src/test/validation_chainstatemanager_tests.cpp +++ b/src/test/validation_chainstatemanager_tests.cpp @@ -24,7 +24,10 @@ #include #include +#include +#include #include +#include #include @@ -657,6 +660,73 @@ 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 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())); + tx->Commit(); + } + BOOST_REQUIRE(m_node.evodb->CommitRootTransaction(EvoDbIdentity::SNAPSHOT)); + + auto& qblockman = *Assert(m_node.llmq_ctx)->quorum_block_processor; + auto& qman = *Assert(m_node.llmq_ctx)->qman; + { + LOCK(::cs_main); + 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)); + + // Exercise quorum resolution itself, including the cache-order hazard: + // a snapshot-only commitment must not resolve for the background + // chain, even after the snapshot lookup has populated the quorum cache. + 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); + + // 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 a9f61d96f981..f6d475fcbd0d 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -1995,7 +1995,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()); } @@ -2469,7 +2469,7 @@ bool Chainstate::ConnectBlock(const CBlock& block, BlockValidationState& state, : is_v24_active ? SuperBlockCheckType::DisallowDuplicates : SuperBlockCheckType::AllowDuplicates; - if (!m_chain_helper->mn_payments->IsBlockValueValid(m_chainman.ActiveChain(), block, pindex->pprev, blockSubsidy + feeReward, strError, check_superblock)) { + if (!m_chain_helper->mn_payments->IsBlockValueValid(m_chain, block, pindex->pprev, blockSubsidy + feeReward, strError, check_superblock)) { // NOTE: Do not punish, the node might be missing governance data LogPrintf("ERROR: ConnectBlock(DASH): %s\n", strError); return state.Invalid(BlockValidationResult::BLOCK_RESULT_UNSET, "bad-cb-amount"); @@ -2479,7 +2479,7 @@ bool Chainstate::ConnectBlock(const CBlock& block, BlockValidationState& state, LogPrint(BCLog::BENCHMARK, " - IsBlockValueValid: %.2fms [%.2fs (%.2fms/blk)]\n", MILLI * (nTime5_3 - nTime5_2), nTimeValueValid * MICRO, nTimeValueValid * MILLI / nBlocksTotal); const MnRewardEra mn_reward_era{GetMnRewardEraAfter(pindex->pprev, m_chainman)}; - if (!m_chain_helper->mn_payments->IsBlockPayeeValid(m_chainman.ActiveChain(), *block.vtx[0], pindex->pprev, blockSubsidy, feeReward, mn_reward_era, is_v24_active, check_superblock)) { + if (!m_chain_helper->mn_payments->IsBlockPayeeValid(m_chain, *block.vtx[0], pindex->pprev, blockSubsidy, feeReward, mn_reward_era, is_v24_active, check_superblock)) { // NOTE: Do not punish, the node might be missing governance data LogPrintf("ERROR: ConnectBlock(DASH): couldn't find masternode or superblock payments\n"); return state.Invalid(BlockValidationResult::BLOCK_RESULT_UNSET, "bad-cb-payee"); @@ -4614,7 +4614,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 f5c93ce0659ab91667bd6f34b9f012322f187fa6 Mon Sep 17 00:00:00 2001 From: Pasta Date: Sat, 11 Jul 2026 00:26:24 -0500 Subject: [PATCH 05/17] 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 46556c2bcbb0..75cc2cb75653 100644 --- a/src/test/validation_chainstatemanager_tests.cpp +++ b/src/test/validation_chainstatemanager_tests.cpp @@ -27,18 +27,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); @@ -102,6 +119,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())); @@ -138,8 +156,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)); @@ -163,6 +183,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 f6d475fcbd0d..eecc89f2dd1b 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -2053,7 +2053,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(); @@ -3336,9 +3339,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; @@ -3348,7 +3353,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); @@ -3557,8 +3562,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. @@ -3660,8 +3667,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) { @@ -5746,6 +5755,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, const CBlockIndex* pindexPrev, std::optional optDIP0024IsActive, diff --git a/src/validation.h b/src/validation.h index a00cd9d59e7d..c6872ae9c168 100644 --- a/src/validation.h +++ b/src/validation.h @@ -1047,6 +1047,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 d3aea545b3882c858dfd7ad6f131f37b56e1e242 Mon Sep 17 00:00:00 2001 From: Pasta Date: Sat, 11 Jul 2026 00:26:32 -0500 Subject: [PATCH 06/17] 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 | 15 +++++++++++++++ src/evo/smldiff.h | 3 +++ src/llmq/snapshot.cpp | 29 +++++++++++++++++++++++++++-- src/net_processing.cpp | 16 ++++++++++++++-- 4 files changed, 59 insertions(+), 4 deletions(-) diff --git a/src/evo/smldiff.cpp b/src/evo/smldiff.cpp index 3c0991ea33c4..a8578cf601d9 100644 --- a/src/evo/smldiff.cpp +++ b/src/evo/smldiff.cpp @@ -182,6 +182,16 @@ bool BuildSimplifiedMNListDiff(CDeterministicMNManager& dmnman, const Chainstate errorRet = strprintf("base block %s is higher then block %s", baseBlockHash.ToString(), blockHash.ToString()); return false; } + // No availability check for baseBlockIndex: the base is only used for + // EvoDB-backed masternode/quorum state, which pruned nodes retain, and + // GetListForBlock below fails with the same sentinel when a snapshot + // node has not validated the base yet. Only the target block is read + // from disk (for cbTx and its merkle tree). + 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); @@ -223,3 +233,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..fe74a542b4b9 100644 --- a/src/evo/smldiff.h +++ b/src/evo/smldiff.h @@ -95,4 +95,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 49d15a7c2609..cd0a4e6bf124 100644 --- a/src/llmq/snapshot.cpp +++ b/src/llmq/snapshot.cpp @@ -19,12 +19,26 @@ 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, + const Consensus::LLMQType& llmq_type, bool skip_snap, + bool need_block_data, int32_t height, gsl::not_null index_tip, std::string& error) + EXCLUSIVE_LOCKS_REQUIRED(::cs_main) { llmq::CycleData ret; + // The cycle block itself only keys EvoDB-backed snapshot metadata, so it + // needs no block data. The work block becomes the target of an inner + // mnlistdiff (read from disk) only on the legacy construction path. ret.m_cycle_index = index_tip->GetAncestor(height); if (!ret.m_cycle_index) { error = "Cannot find block"; @@ -35,6 +49,7 @@ std::optional ConstructCycle(llmq::CQuorumSnapshotManager& qsna error = "Cannot find work block"; return std::nullopt; } + if (need_block_data && !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 +89,10 @@ bool BuildQuorumRotationInfo(CDeterministicMNManager& dmnman, CQuorumSnapshotMan errorRet = strprintf("block %s is not in the active chain", blockHash.ToString()); return false; } + // No availability check: these are only diff bases, which need + // EvoDB-backed state rather than block data; the inner + // BuildSimplifiedMNListDiff calls surface the sentinel error if + // a snapshot node cannot construct a base list yet. baseBlockIndexes.push_back(blockIndex); } // Sort in all cases: the legacy path (served to peers < EFFICIENT_QRINFO_VERSION) @@ -93,6 +112,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 +126,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; @@ -117,6 +138,7 @@ bool BuildQuorumRotationInfo(CDeterministicMNManager& dmnman, CQuorumSnapshotMan const int cycleLength = llmq_params_opt->dkgInterval; auto cycle_base_opt = ConstructCycle(qsnapman, llmqType, /*skip_snap=*/true, + /*need_block_data=*/use_legacy_construction, /*height=*/blockIndex->nHeight - (blockIndex->nHeight % cycleLength), blockIndex, errorRet); if (!cycle_base_opt.has_value()) { @@ -137,6 +159,7 @@ bool BuildQuorumRotationInfo(CDeterministicMNManager& dmnman, CQuorumSnapshotMan auto target_cycles{response.GetCycles()}; for (size_t idx{0}; idx < target_cycles.size(); idx++) { auto cycle_opt = ConstructCycle(qsnapman, llmqType, /*skip_snap=*/false, + /*need_block_data=*/use_legacy_construction, /*height=*/cycle_base_opt->m_cycle_index->nHeight - (cycleLength * (idx + 1)), tipBlockIndex, errorRet); if (!cycle_opt.has_value()) { @@ -172,7 +195,9 @@ bool BuildQuorumRotationInfo(CDeterministicMNManager& dmnman, CQuorumSnapshotMan } for (const auto& h : snapshotHeightsNeeded) { - auto cycle_opt = ConstructCycle(qsnapman, llmqType, /*skip_snap=*/false, /*height=*/h, tipBlockIndex, errorRet); + auto cycle_opt = ConstructCycle(qsnapman, llmqType, /*skip_snap=*/false, + /*need_block_data=*/use_legacy_construction, /*height=*/h, tipBlockIndex, + errorRet); if (!cycle_opt.has_value()) { return false; } diff --git a/src/net_processing.cpp b/src/net_processing.cpp index 97554d4ecfde..1bad4bcc5a5f 100644 --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -5466,7 +5466,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; } @@ -5506,7 +5514,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; } From 80103536f4058eaf5762e3d688ad1ec1f1a01cec Mon Sep 17 00:00:00 2001 From: Pasta Date: Sat, 11 Jul 2026 00:26:43 -0500 Subject: [PATCH 07/17] 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 c77e38fb147d..a92c782c3b14 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 c09470239ff5..278293f9e285 100644 --- a/src/llmq/signing_shares.cpp +++ b/src/llmq/signing_shares.cpp @@ -767,6 +767,11 @@ bool CSigSharesManager::AsyncSignIfMember(Consensus::LLMQType llmqType, const ui { 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 40d2dc620186..5b00e7ae0e5b 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 9417fee601b8..7a7380dc1b75 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 4ec10d621dee..633c4e642988 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, id, msgHash, quorumHash); } else { diff --git a/src/test/validation_chainstatemanager_tests.cpp b/src/test/validation_chainstatemanager_tests.cpp index 75cc2cb75653..9e02eb63081c 100644 --- a/src/test/validation_chainstatemanager_tests.cpp +++ b/src/test/validation_chainstatemanager_tests.cpp @@ -122,6 +122,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()); @@ -165,6 +167,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 341972df9198bac197024e3e534e4ada6ffe4d68 Mon Sep 17 00:00:00 2001 From: Pasta Date: Sat, 11 Jul 2026 01:23:04 -0500 Subject: [PATCH 08/17] 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 60e7a463866b..8e70749b13f4 100644 --- a/src/evo/evodb.h +++ b/src/evo/evodb.h @@ -113,6 +113,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 bd61a3504a01..32fe543a94de 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 76bcc47dfbb1..b93e31c9f8a0 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -1455,7 +1455,7 @@ static void SoftForkDescPushBack(const CBlockIndex* blockindex, UniValue& softfo softforks.pushKV(DeploymentName(dep), rv); } -static void SoftForkDescPushBack(const CBlockIndex* blockindex, const std::unordered_map& signals, UniValue& softforks, const ChainstateManager& chainman, Consensus::DeploymentPos id) +static void SoftForkDescPushBack(const CBlockIndex* blockindex, 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 3f264bd7aa0b..b716dc868b19 100644 --- a/src/test/evo_deterministicmns_tests.cpp +++ b/src/test/evo_deterministicmns_tests.cpp @@ -1583,6 +1583,25 @@ BOOST_AUTO_TEST_SUITE(evo_dip3_activation_tests) // the sixth registration. constexpr int DIP3_ACTIVATION_HEIGHT{109}; +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(DIP3_ACTIVATION_HEIGHT - 2, CBaseChainParams::REGTEST, {"-dip3params=109:500"}, diff --git a/src/versionbits.h b/src/versionbits.h index 46f5bb48d8ca..aa72fda9c91d 100644 --- a/src/versionbits.h +++ b/src/versionbits.h @@ -110,7 +110,7 @@ class VersionBitsCache class AbstractEHFManager { public: - using Signals = std::unordered_map; + using Signals = std::map; public: AbstractEHFManager() = default; From 2cb2a024d7f60b5009081888c14067c7a0339383 Mon Sep 17 00:00:00 2001 From: Pasta Date: Tue, 14 Jul 2026 11:18:42 -0500 Subject: [PATCH 09/17] test: adapt AssumeUTXO fixtures after txindex removal --- src/test/util/setup_common.cpp | 6 ++++++ src/test/validation_chainstatemanager_tests.cpp | 7 ------- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/test/util/setup_common.cpp b/src/test/util/setup_common.cpp index bb18cddd78ad..52b35807c060 100644 --- a/src/test/util/setup_common.cpp +++ b/src/test/util/setup_common.cpp @@ -69,6 +69,7 @@ #include #include #include +#include #include #include #include @@ -398,6 +399,11 @@ TestingSetup::~TestingSetup() m_node.connman->Stop(); } + // govman holds a reference to chain_helper->superblocks, so it must be + // reset before chain_helper is destroyed (matches PrepareShutdown ordering + // in init.cpp). Keep this defensive for fixtures that construct govman. + m_node.govman.reset(); + if (m_node.mempool) { m_node.mempool->DisconnectManagers(); } diff --git a/src/test/validation_chainstatemanager_tests.cpp b/src/test/validation_chainstatemanager_tests.cpp index 9e02eb63081c..64d32b4c4fb0 100644 --- a/src/test/validation_chainstatemanager_tests.cpp +++ b/src/test/validation_chainstatemanager_tests.cpp @@ -14,7 +14,6 @@ #include #include #include -#include #include #include #include @@ -786,9 +785,6 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager_evodb_snapshot_only_flush_restart, Sna 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)); @@ -835,9 +831,6 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager_snapshot_init_missing_evodb_marker, Sn 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_AUTO_TEST_SUITE_END() From ec7ec1a15b20e59c0978e82ed25909145c5ac1bc Mon Sep 17 00:00:00 2001 From: pasta Date: Sat, 1 Aug 2026 16:10:43 -0500 Subject: [PATCH 10/17] evo: abort node on WriteDerived mismatch instead of consensus rejection A WriteDerived failure means independently derived block data disagrees with the copy already recorded in EvoDB. That is local state corruption (or a cross-chainstate divergence bug), never evidence about the block being processed. Previously the mismatch surfaced as BLOCK_CONSENSUS: the block was persistently marked BLOCK_FAILED_VALID (surviving restart and forking the node off the network) and the relaying peer was handed a 100-point misbehavior score via BlockChecked, which background validation also triggers. Instead, follow the existing EvoDbInconsistencyMessage convention: request node shutdown via AbortNode and fail validation with M_ERROR, which neither marks the block invalid nor punishes peers. The credit-pool and MNHF sites abort at the throw site because miner and RPC callers never pass through a validation-state catch; a typed EvoDbInconsistencyError lets the four block-path catch blocks that would otherwise swallow it into BLOCK_CONSENSUS reclassify it as M_ERROR. Co-Authored-By: Claude Fable 5 --- src/evo/creditpool.cpp | 17 ++++++++++++++--- src/evo/deterministicmns.cpp | 18 ++++++++++++------ src/evo/evodb.h | 11 +++++++++++ src/evo/mnhftx.cpp | 17 ++++++++++++++--- src/evo/specialtxman.cpp | 9 +++++++++ src/llmq/blockprocessor.cpp | 9 +++++---- 6 files changed, 65 insertions(+), 16 deletions(-) diff --git a/src/evo/creditpool.cpp b/src/evo/creditpool.cpp index 97ea2595661e..5d9a1f60d3cf 100644 --- a/src/evo/creditpool.cpp +++ b/src/evo/creditpool.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include @@ -138,9 +139,14 @@ void CCreditPoolManager::AddToCache(const uint256& block_hash, int height, const { 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"); + // A mismatch is local EvoDB corruption, not a statement about the + // block. Abort here: some callers (miner, RPC) never pass through a + // validation-state catch, and the block-connect catches must not + // translate this into a consensus rejection. + const std::string msg = strprintf("CCreditPoolManager::%s -- EvoDB credit pool mismatch for block %s", + __func__, block_hash.ToString()); + AbortNode(msg); + throw EvoDbInconsistencyError(msg); } } { @@ -335,6 +341,11 @@ std::optional GetCreditPoolDiffForBlock(CCreditPoolManager& cpo } } return creditPoolDiff; + } catch (const EvoDbInconsistencyError& e) { + // Local EvoDB corruption (the node is already aborting): fail with + // M_ERROR so the block is not marked invalid. + state.Error(e.what()); + return std::nullopt; } catch (const std::exception& e) { LogPrintf("%s -- failed: %s\n", __func__, e.what()); state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "failed-getcreditpooldiff"); diff --git a/src/evo/deterministicmns.cpp b/src/evo/deterministicmns.cpp index 1c224090a9dc..b9cd014ac691 100644 --- a/src/evo/deterministicmns.cpp +++ b/src/evo/deterministicmns.cpp @@ -17,6 +17,7 @@ #include #include #include