diff --git a/src/Makefile.am b/src/Makefile.am index 9f7c04a8ac29..4676656960d4 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -237,6 +237,7 @@ BITCOIN_CORE_H = \ evo/simplifiedmns.h \ evo/smldiff.h \ evo/snapshot.h \ + evo/snapshot_types.h \ evo/specialtx.h \ evo/specialtx_filter.h \ evo/specialtxman.h \ @@ -558,6 +559,7 @@ libbitcoin_node_a_SOURCES = \ evo/simplifiedmns.cpp \ evo/smldiff.cpp \ evo/snapshot.cpp \ + evo/snapshot_chain.cpp \ evo/specialtx.cpp \ evo/specialtx_filter.cpp \ evo/specialtxman.cpp \ @@ -1293,6 +1295,7 @@ libdashkernel_la_SOURCES = \ evo/providertx_util.cpp \ evo/simplifiedmns.cpp \ evo/smldiff.cpp \ + evo/snapshot.cpp \ evo/specialtx.cpp \ evo/specialtx_filter.cpp \ evo/specialtxman.cpp \ diff --git a/src/evo/chainhelper.cpp b/src/evo/chainhelper.cpp index 8e92000851e1..dc2d5504d6a5 100644 --- a/src/evo/chainhelper.cpp +++ b/src/evo/chainhelper.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -27,6 +28,8 @@ CChainstateHelper::CChainstateHelper(CEvoDB& evodb, CDeterministicMNManager& dmn isman{isman}, mn_sync{mn_sync}, m_dmnman{dmnman}, + m_qblockman{qblockman}, + m_qsnapman{qsnapman}, credit_pool_manager{std::make_unique(evodb, chainman)}, m_chainlocks{chainlocks}, ehf_manager{std::make_unique(evodb, consensus_params)}, @@ -66,7 +69,12 @@ int32_t CChainstateHelper::GetBestChainLockHeight() const { return m_chainlocks. uint256 CChainstateHelper::GetDeterministicMNListHash(const CBlockIndex* pindex) const { - return SerializeHash(m_dmnman.GetListForBlock(Assert(pindex))); + const CBlockIndex* index{Assert(pindex)}; + CDeterministicMNList list{m_dmnman.GetListForBlock(index)}; + if (list.GetBlockHash().IsNull()) { + list = CDeterministicMNList{index->GetBlockHash(), index->nHeight, 0}; + } + return evo::CanonicalMNListHash(list); } /** Passthrough functions to CCreditPoolManager */ diff --git a/src/evo/chainhelper.h b/src/evo/chainhelper.h index a501dd82ceaf..e6f2be69abf0 100644 --- a/src/evo/chainhelper.h +++ b/src/evo/chainhelper.h @@ -46,6 +46,8 @@ class CChainstateHelper llmq::CInstantSendManager& isman; const CMasternodeSync& mn_sync; CDeterministicMNManager& m_dmnman; + llmq::CQuorumBlockProcessor& m_qblockman; + llmq::CQuorumSnapshotManager& m_qsnapman; public: const std::unique_ptr credit_pool_manager; @@ -75,6 +77,9 @@ class CChainstateHelper /** Return a canonical hash of the deterministic MN list derived at a block. */ uint256 GetDeterministicMNListHash(const CBlockIndex* pindex) const; + CDeterministicMNManager& DeterministicMNManager() { return m_dmnman; } + llmq::CQuorumBlockProcessor& QuorumBlockProcessor() { return m_qblockman; } + llmq::CQuorumSnapshotManager& QuorumSnapshotManager() { return m_qsnapman; } /** Passthrough functions to CCreditPoolManager */ CCreditPool GetCreditPool(const CBlockIndex* const pindex); diff --git a/src/evo/creditpool.cpp b/src/evo/creditpool.cpp index 239b951a27d0..4e16d1823d67 100644 --- a/src/evo/creditpool.cpp +++ b/src/evo/creditpool.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include @@ -125,12 +126,12 @@ std::optional CCreditPoolManager::GetFromCache(const CBlockIndex& b return pool; } } - if (block_index.nHeight % DISK_SNAPSHOT_PERIOD == 0) { - if (evoDb.Read(std::make_pair(DB_CREDITPOOL_SNAPSHOT, block_hash), pool)) { - LOCK(cache_mutex); - creditPoolCache.insert(block_hash, pool); - return pool; - } + // Snapshot activation may deliberately seed a full state at a height that + // is not one of the normal periodic checkpoints. + if (evoDb.Read(std::make_pair(DB_CREDITPOOL_SNAPSHOT, block_hash), pool)) { + LOCK(cache_mutex); + creditPoolCache.insert(block_hash, pool); + return pool; } return std::nullopt; } @@ -155,6 +156,12 @@ void CCreditPoolManager::AddToCache(const uint256& block_hash, int height, const } } +bool CCreditPoolManager::SeedSnapshot(const CBlockIndex* block, const CCreditPool& pool) +{ + if (!Assume(block != nullptr)) return false; + return evoDb.WriteDerived(std::make_pair(DB_CREDITPOOL_SNAPSHOT, block->GetBlockHash()), pool); +} + CCreditPool CCreditPoolManager::ConstructCreditPool(const gsl::not_null block_index, CCreditPool prev) { std::optional opt_block_data = GetCreditDataFromBlock(block_index, m_chainman.GetConsensus()); diff --git a/src/evo/creditpool.h b/src/evo/creditpool.h index 891449f24f1f..dc839e63f181 100644 --- a/src/evo/creditpool.h +++ b/src/evo/creditpool.h @@ -142,6 +142,8 @@ class CCreditPoolManager * it can happen if there limits of withdrawal (unlock) exceed */ CCreditPool GetCreditPool(const CBlockIndex* block) EXCLUSIVE_LOCKS_REQUIRED(!cache_mutex); + /** Seed a full pool snapshot in the current EvoDB transaction. */ + bool SeedSnapshot(const CBlockIndex* block, const CCreditPool& pool) EXCLUSIVE_LOCKS_REQUIRED(!cache_mutex); private: std::optional GetFromCache(const CBlockIndex& block_index) EXCLUSIVE_LOCKS_REQUIRED(!cache_mutex); diff --git a/src/evo/deterministicmns.cpp b/src/evo/deterministicmns.cpp index d1f1eca0c85b..07bc4a1383bd 100644 --- a/src/evo/deterministicmns.cpp +++ b/src/evo/deterministicmns.cpp @@ -686,6 +686,18 @@ CDeterministicMNManager::CDeterministicMNManager(CEvoDB& evoDb, CMasternodeMetaM CDeterministicMNManager::~CDeterministicMNManager() = default; +bool CDeterministicMNManager::SeedListForBlock(const CDeterministicMNList& list) +{ + return m_evoDb.WriteDerived(std::make_pair(DB_LIST_SNAPSHOT, list.GetBlockHash()), list); +} + +void CDeterministicMNManager::InvalidateListCacheForBlock(const uint256& block_hash) +{ + LOCK(cs); + mnListsCache.erase(block_hash); + mnListDiffsCache.erase(block_hash); +} + bool CDeterministicMNManager::ProcessBlock(const CBlock& block, gsl::not_null pindex, BlockValidationState& state, const CDeterministicMNList& newList, MNListUpdates& updatesRet) @@ -851,6 +863,7 @@ CDeterministicMNList CDeterministicMNManager::GetListForBlockInternal(gsl::not_n mnListsCache.emplace(pindex->GetBlockHash(), snapshot); break; } + if (m_list_snapshot_miss_hook) m_list_snapshot_miss_hook(pindex); // no snapshot found yet, check diffs auto itDiffs = mnListDiffsCache.find(pindex->GetBlockHash()); diff --git a/src/evo/deterministicmns.h b/src/evo/deterministicmns.h index 2941f1cc71f3..aa401dde0bc5 100644 --- a/src/evo/deterministicmns.h +++ b/src/evo/deterministicmns.h @@ -21,6 +21,7 @@ #include #include +#include #include #include #include @@ -778,6 +779,7 @@ class CDeterministicMNManager Uint256HashMap mnListsCache GUARDED_BY(cs); Uint256HashMap mnListDiffsCache GUARDED_BY(cs); + std::function m_list_snapshot_miss_hook GUARDED_BY(cs); const CBlockIndex* tipIndex GUARDED_BY(cs) {nullptr}; const CBlockIndex* m_initial_snapshot_index GUARDED_BY(cs) {nullptr}; @@ -801,6 +803,21 @@ class CDeterministicMNManager }; CDeterministicMNList GetListAtChainTip() EXCLUSIVE_LOCKS_REQUIRED(!cs); + /** Seed a canonical full-list snapshot in the current EvoDB transaction. */ + bool SeedListForBlock(const CDeterministicMNList& list) EXCLUSIVE_LOCKS_REQUIRED(!cs); + + /** Invalidate cached list data so the next lookup reloads it from EvoDB. */ + void InvalidateListCacheForBlock(const uint256& block_hash) EXCLUSIVE_LOCKS_REQUIRED(!cs); + + /** Test-only guard invoked after a full-list cache/EvoDB miss, before + * ordinary diff-chain reconstruction can access earlier NORMAL state. */ + void SetListSnapshotMissHookForTesting(std::function hook) + EXCLUSIVE_LOCKS_REQUIRED(!cs) + { + LOCK(cs); + m_list_snapshot_miss_hook = std::move(hook); + } + void SetListForBlockForTesting(const CDeterministicMNList& list) EXCLUSIVE_LOCKS_REQUIRED(!cs) { LOCK(cs); diff --git a/src/evo/evodb.cpp b/src/evo/evodb.cpp index 88787fe1e8ea..1fa16d1d212e 100644 --- a/src/evo/evodb.cpp +++ b/src/evo/evodb.cpp @@ -42,6 +42,12 @@ CEvoDB::CEvoDB(const util::DbWrapperParams& db_params) : CEvoDB::~CEvoDB() = default; +bool CEvoDB::HasActiveTransaction() +{ + LOCK(cs); + return active_transaction.has_value(); +} + CEvoDB::TransactionContext& CEvoDB::GetContext(EvoDbIdentity identity) { auto it = transaction_contexts.find(identity); @@ -143,6 +149,19 @@ bool CEvoDB::HasDualChainstateMarker() return db->Exists(EVODB_DUAL_CHAINSTATE); } +// Reader is the raw DB or a transaction; Eraser a batch or the same transaction. +template +static void EraseHistoricalMNListMarkers(Reader& reader, Eraser& eraser) +{ + std::vector required; + if (reader.Read(EVODB_REQUIRED_WORK_MNLISTS, required)) { + for (const auto& block_hash : required) { + eraser.Erase(std::make_pair(EVODB_BACKGROUND_WORK_MNLIST_HASH, block_hash)); + } + } + eraser.Erase(EVODB_REQUIRED_WORK_MNLISTS); +} + void CEvoDB::EraseSnapshotMarkers() { LOCK(cs); @@ -150,6 +169,8 @@ void CEvoDB::EraseSnapshotMarkers() transaction.Erase(std::make_pair(EVODB_BEST_BLOCK, uint8_t{1})); transaction.Erase(EVODB_SNAPSHOT_MNLIST_HASH); transaction.Erase(EVODB_BACKGROUND_MNLIST_HASH); + EraseHistoricalMNListMarkers(transaction, transaction); + transaction.Erase(EVODB_SNAPSHOT_EVO_SECTION); transaction.Erase(EVODB_DUAL_CHAINSTATE); } @@ -182,6 +203,26 @@ bool CEvoDB::ReadBackgroundMNListHash(uint256& block_hash, uint256& mn_list_hash return true; } +void CEvoDB::WriteRequiredWorkMNListHashes(const std::vector& block_hashes) +{ + Write(EVODB_REQUIRED_WORK_MNLISTS, block_hashes); +} + +bool CEvoDB::ReadRequiredWorkMNListHashes(std::vector& block_hashes) +{ + return Read(EVODB_REQUIRED_WORK_MNLISTS, block_hashes); +} + +void CEvoDB::WriteBackgroundWorkMNListHash(const uint256& block_hash, const uint256& mn_list_hash) +{ + Write(std::make_pair(EVODB_BACKGROUND_WORK_MNLIST_HASH, block_hash), mn_list_hash); +} + +bool CEvoDB::ReadBackgroundWorkMNListHash(const uint256& block_hash, uint256& mn_list_hash) +{ + return Read(std::make_pair(EVODB_BACKGROUND_WORK_MNLIST_HASH, block_hash), mn_list_hash); +} + bool CEvoDB::PromoteSnapshotMarkers(const uint256& expected_snapshot_tip) { LOCK(cs); @@ -198,7 +239,9 @@ bool CEvoDB::PromoteSnapshotMarkers(const uint256& expected_snapshot_tip) uint256 normal_tip; const bool already_promoted = db->Read(EVODB_BEST_BLOCK, normal_tip) && normal_tip == expected_snapshot_tip && !db->Exists(EVODB_DUAL_CHAINSTATE) && !db->Exists(EVODB_SNAPSHOT_MNLIST_HASH) && - !db->Exists(EVODB_BACKGROUND_MNLIST_HASH); + !db->Exists(EVODB_BACKGROUND_MNLIST_HASH) && + !db->Exists(EVODB_REQUIRED_WORK_MNLISTS) && + !db->Exists(EVODB_SNAPSHOT_EVO_SECTION); if (already_promoted) m_default_identity = EvoDbIdentity::NORMAL; return already_promoted; } @@ -209,6 +252,8 @@ bool CEvoDB::PromoteSnapshotMarkers(const uint256& expected_snapshot_tip) batch.Erase(snapshot_key); batch.Erase(EVODB_SNAPSHOT_MNLIST_HASH); batch.Erase(EVODB_BACKGROUND_MNLIST_HASH); + EraseHistoricalMNListMarkers(*db, batch); + batch.Erase(EVODB_SNAPSHOT_EVO_SECTION); batch.Erase(EVODB_DUAL_CHAINSTATE); if (!db->WriteBatch(batch, /*fSync=*/true)) return false; // The dual-chainstate run is over: the promoted state is the NORMAL @@ -231,6 +276,8 @@ bool CEvoDB::DiscardSnapshotMarkers() batch.Erase(std::make_pair(EVODB_BEST_BLOCK, uint8_t{1})); batch.Erase(EVODB_SNAPSHOT_MNLIST_HASH); batch.Erase(EVODB_BACKGROUND_MNLIST_HASH); + EraseHistoricalMNListMarkers(*db, batch); + batch.Erase(EVODB_SNAPSHOT_EVO_SECTION); batch.Erase(EVODB_DUAL_CHAINSTATE); if (!db->WriteBatch(batch, /*fSync=*/true)) return false; // The snapshot chainstate is gone; transaction-less access must resolve diff --git a/src/evo/evodb.h b/src/evo/evodb.h index 40d37b48229d..7aff76c9f986 100644 --- a/src/evo/evodb.h +++ b/src/evo/evodb.h @@ -31,6 +31,9 @@ static const std::string EVODB_BEST_BLOCK = "b_b4"; static const std::string EVODB_DUAL_CHAINSTATE = "b_dcs"; static const std::string EVODB_SNAPSHOT_MNLIST_HASH = "b_dcs_mn"; static const std::string EVODB_BACKGROUND_MNLIST_HASH = "b_dcs_bg_mn"; +static const std::string EVODB_REQUIRED_WORK_MNLISTS = "b_dcs_req_mn"; +static const std::string EVODB_BACKGROUND_WORK_MNLIST_HASH = "b_dcs_bg_work_mn"; +static const std::string EVODB_SNAPSHOT_EVO_SECTION = "b_dcs_evo"; enum class EvoDbIdentity { NORMAL, @@ -224,6 +227,7 @@ class CEvoDB bool CommitRootTransaction(EvoDbIdentity identity = EvoDbIdentity::NORMAL, bool sync = false) EXCLUSIVE_LOCKS_REQUIRED(!cs); bool IsEmpty() { return db->IsEmpty(); } + bool HasActiveTransaction() EXCLUSIVE_LOCKS_REQUIRED(!cs); //! Set the identity used by reads/writes outside any transaction. Must //! track the active chainstate: snapshot activation sets SNAPSHOT; @@ -250,6 +254,10 @@ class CEvoDB bool ReadSnapshotBaseMNListHash(uint256& hash) EXCLUSIVE_LOCKS_REQUIRED(!cs); void WriteBackgroundMNListHash(const uint256& block_hash, const uint256& mn_list_hash) EXCLUSIVE_LOCKS_REQUIRED(!cs); bool ReadBackgroundMNListHash(uint256& block_hash, uint256& mn_list_hash) EXCLUSIVE_LOCKS_REQUIRED(!cs); + void WriteRequiredWorkMNListHashes(const std::vector& block_hashes) EXCLUSIVE_LOCKS_REQUIRED(!cs); + bool ReadRequiredWorkMNListHashes(std::vector& block_hashes) EXCLUSIVE_LOCKS_REQUIRED(!cs); + void WriteBackgroundWorkMNListHash(const uint256& block_hash, const uint256& mn_list_hash) EXCLUSIVE_LOCKS_REQUIRED(!cs); + bool ReadBackgroundWorkMNListHash(const uint256& block_hash, uint256& mn_list_hash) EXCLUSIVE_LOCKS_REQUIRED(!cs); /** * Atomically promote the surviving snapshot marker to the legacy NORMAL key diff --git a/src/evo/mnhftx.cpp b/src/evo/mnhftx.cpp index 7014a6924d0e..98351446298a 100644 --- a/src/evo/mnhftx.cpp +++ b/src/evo/mnhftx.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include @@ -382,6 +383,12 @@ void CMNHFManager::AddToCache(const Signals& signals, const CBlockIndex* const p } } +bool CMNHFManager::SeedSignals(const CBlockIndex* pindex, const Signals& signals) +{ + if (!Assume(pindex != nullptr)) return false; + return m_evoDb.WriteDerived(std::make_pair(DB_SIGNALS_v2, pindex->GetBlockHash()), signals); +} + void CMNHFManager::AddSignal(const CBlockIndex* const pindex, int bit) { auto signals = GetForBlock(pindex->pprev); diff --git a/src/evo/mnhftx.h b/src/evo/mnhftx.h index ead66e3fa876..03f62dac4399 100644 --- a/src/evo/mnhftx.h +++ b/src/evo/mnhftx.h @@ -136,6 +136,8 @@ class CMNHFManager : public AbstractEHFManager void AddSignal(const CBlockIndex* const pindex, int bit) EXCLUSIVE_LOCKS_REQUIRED(!cs_cache); bool ForceSignalDBUpdate(const CBlockIndex* tip) EXCLUSIVE_LOCKS_REQUIRED(::cs_main, !cs_cache); + /** Seed the signals at a block in the current EvoDB transaction. */ + bool SeedSignals(const CBlockIndex* pindex, const Signals& signals) EXCLUSIVE_LOCKS_REQUIRED(!cs_cache); private: void AddToCache(const Signals& signals, const CBlockIndex* const pindex) EXCLUSIVE_LOCKS_REQUIRED(!cs_cache); diff --git a/src/evo/snapshot.h b/src/evo/snapshot.h index b0944838f90a..3d4ad4d19ef7 100644 --- a/src/evo/snapshot.h +++ b/src/evo/snapshot.h @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -30,7 +31,17 @@ #include #include +class CBlockIndex; +class CChainParams; +class ChainstateManager; class CCbTx; +class CCreditPoolManager; +class CMNHFManager; + +namespace llmq { +class CQuorumBlockProcessor; +class CQuorumSnapshotManager; +} // namespace llmq namespace evo { @@ -586,7 +597,7 @@ QuorumSnapshotEntry ReadRotationSnapshot(Stream& s, const Consensus::LLMQParams& s >> entry.cycle_base_block_hash >> entry.work_block_hash >> entry.snapshot.mnSkipListMode; // BuildQuorumSnapshot sizes this bitset to the complete work-block MN list, // not to the quorum size. The exact historical-list size is chain-aware and - // is checked by the chain-aware validation layered on later in the series. + // is checked by ValidateEvoSnapshotAgainstChain. const size_t bit_count{ReadBoundedCompactSize(s, EVO_SNAPSHOT_MAX_MNS, "rotation bitset")}; ReadFixedBitSet(s, entry.snapshot.activeQuorumMembers, bit_count); const size_t skip_count{ReadBoundedCompactSize(s, EVO_SNAPSHOT_MAX_SKIPLIST_ENTRIES, "rotation skip list")}; @@ -722,6 +733,12 @@ void EvoSnapshot::Unserialize(Stream& s) /** Single SHA256 of the canonical SER_DISK/CLIENT_VERSION encoding. */ uint256 GetEvoSnapshotHash(const EvoSnapshot& snapshot); +bool BuildEvoSnapshot(const CChainParams& chainparams, const ChainstateManager& chainman, + CDeterministicMNManager& dmnman, + const llmq::CQuorumBlockProcessor& qblockman, llmq::CQuorumSnapshotManager& qsnapman, + CCreditPoolManager& cpoolman, CMNHFManager& mnhfman, const CBlockIndex* base_index, + EvoSnapshot& snapshot, std::string& error) EXCLUSIVE_LOCKS_REQUIRED(::cs_main); + struct QuorumReconstructionHeight { Consensus::LLMQType llmq_type; bool rotation; @@ -737,6 +754,11 @@ std::vector EvoSnapshotReconstructionHeights( bool ReconstructHistoricalMNLists(const EvoSnapshot& snapshot, std::map& lists, std::string& error, size_t max_records = EVO_SNAPSHOT_MAX_RECONSTRUCTION_RECORDS); +/** Validate all snapshot invariants requiring the block index or deployments. */ +bool ValidateEvoSnapshotAgainstChain(const EvoSnapshot& snapshot, const ChainstateManager& chainman, + const CBlockIndex* base_index, std::string& error) + EXCLUSIVE_LOCKS_REQUIRED(::cs_main); + /** Pure CbTx checks over already-built snapshot content. */ bool VerifyEvoSnapshotCbTx(const EvoSnapshot& snapshot, const CCbTx& cbtx, std::string& error); diff --git a/src/evo/snapshot_chain.cpp b/src/evo/snapshot_chain.cpp new file mode 100644 index 000000000000..999ec8ff45db --- /dev/null +++ b/src/evo/snapshot_chain.cpp @@ -0,0 +1,404 @@ +// 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. + +// Chain-aware companions to evo/snapshot.cpp, deliberately header-less: these +// implementations are declared in evo/snapshot.h but need validation.h and +// llmq internals, so they live in their own compilation unit to keep the +// snapshot codec free of an evo/snapshot -> validation -> evo/snapshot cycle. + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace evo { +namespace { + +bool ValidateCommitmentAgainstChain(const MinedQuorumCommitment& entry, const ChainstateManager& chainman, + const CBlockIndex* base_index, const Consensus::LLMQParams& params, + bool rotation_enabled) EXCLUSIVE_LOCKS_REQUIRED(::cs_main) +{ + const CBlockIndex* quorum_index{chainman.m_blockman.LookupBlockIndex(entry.quorum_base_block_hash)}; + const CBlockIndex* mined_index{chainman.m_blockman.LookupBlockIndex(entry.mined_block_hash)}; + if (quorum_index == nullptr || mined_index == nullptr || + base_index->GetAncestor(quorum_index->nHeight) != quorum_index || + base_index->GetAncestor(mined_index->nHeight) != mined_index) return false; + const int cycle_height{quorum_index->nHeight - quorum_index->nHeight % params.dkgInterval}; + if (rotation_enabled) { + if (entry.commitment.quorumIndex != quorum_index->nHeight % params.dkgInterval || + entry.commitment.quorumIndex < 0 || + entry.commitment.quorumIndex >= params.signingActiveQuorumCount) return false; + } else if (quorum_index->nHeight != cycle_height || entry.commitment.quorumIndex != 0) { + return false; + } + const int mined_cycle{mined_index->nHeight - mined_index->nHeight % params.dkgInterval}; + if (mined_cycle != cycle_height || mined_index->nHeight % params.dkgInterval < params.dkgMiningWindowStart || + mined_index->nHeight % params.dkgInterval > params.dkgMiningWindowEnd) return false; + const uint16_t expected_version{llmq::CFinalCommitment::GetVersion( + rotation_enabled, DeploymentActiveAfter(quorum_index, chainman.GetConsensus(), Consensus::DEPLOYMENT_V19))}; + return entry.commitment.nVersion == expected_version && entry.commitment.VerifySizes(params); +} + +MinedQuorumCommitment ReadCommitment(const llmq::CQuorumBlockProcessor& qblockman, Consensus::LLMQType type, + const CBlockIndex* quorum_index, const CBlockIndex* work_index, + std::string& error) +{ + auto [commitment, mined_hash] = qblockman.GetMinedCommitment(type, quorum_index->GetBlockHash()); + if (mined_hash.IsNull()) error = "mined quorum commitment not found for " + quorum_index->GetBlockHash().ToString(); + return {quorum_index->GetBlockHash(), work_index->GetBlockHash(), std::move(commitment), mined_hash}; +} + + +} // namespace + +bool BuildEvoSnapshot(const CChainParams& chainparams, const ChainstateManager& chainman, + CDeterministicMNManager& dmnman, + const llmq::CQuorumBlockProcessor& qblockman, llmq::CQuorumSnapshotManager& qsnapman, + CCreditPoolManager& cpoolman, CMNHFManager& mnhfman, const CBlockIndex* base_index, + EvoSnapshot& snapshot, std::string& error) +{ + AssertLockHeld(::cs_main); + error.clear(); + if (base_index == nullptr) { + error = "evo snapshot base block is null"; + return false; + } + + EvoSnapshot result; + result.base_block_hash = base_index->GetBlockHash(); + if (!DeploymentActiveAt(*base_index, chainparams.GetConsensus(), Consensus::DEPLOYMENT_DIP0003)) { + result.mn_list = CDeterministicMNList{base_index->GetBlockHash(), base_index->nHeight, 0}; + result.Validate(); + snapshot = std::move(result); + return true; + } + result.mn_list = dmnman.GetListForBlock(base_index); + std::map> historical; + std::map, uint256> modifiers; + + const auto register_work_block = [&](const Consensus::LLMQParams& params, + bool rotation_enabled, + const CBlockIndex* quorum_index) -> const CBlockIndex* { + const CBlockIndex* modifier_base{rotation_enabled + ? quorum_index->GetAncestor(quorum_index->nHeight - quorum_index->nHeight % params.dkgInterval) + : quorum_index}; + if (modifier_base == nullptr) return nullptr; + const CBlockIndex* work_index{ + (rotation_enabled || + DeploymentActiveAfter(modifier_base, chainparams.GetConsensus(), Consensus::DEPLOYMENT_V20)) + ? modifier_base->GetAncestor(modifier_base->nHeight - llmq::WORK_DIFF_DEPTH) + : modifier_base}; + if (work_index == nullptr) return nullptr; + historical.try_emplace(work_index->GetBlockHash(), work_index, dmnman.GetListForBlock(work_index)); + modifiers.emplace(std::make_pair(params.type, work_index->GetBlockHash()), + llmq::utils::GetQuorumHashModifier(params, chainparams.GetConsensus(), modifier_base)); + return work_index; + }; + + for (const auto& params : chainparams.GetConsensus().llmqs) { + if (!chainman.IsQuorumTypeEnabled(params.type, base_index)) continue; + QuorumSnapshotData data; + data.llmq_type = params.type; + data.rotation_enabled = llmq::IsQuorumRotationEnabled(params, base_index); + + const size_t active_count{static_cast(params.signingActiveQuorumCount)}; + const size_t total_count{SnapshotCommitmentCount(params, data.rotation_enabled)}; + std::vector indexes; + if (data.rotation_enabled) { + indexes = qblockman.GetLastMinedCommitmentsPerQuorumIndexUntilBlock(params.type, base_index, 0); + } else { + indexes = qblockman.GetMinedCommitmentsUntilBlock(params.type, base_index, total_count); + } + // A young chain (or a freshly activated type) legitimately has fewer + // mined commitments than the parameter-derived horizon. Emit what + // exists: the CbTx quorum merkle root pins the active set at + // completion, so a shortfall cannot be used to hide commitments. + const size_t emit_active{std::min(indexes.size(), active_count)}; + for (size_t i{0}; i < emit_active; ++i) { + const CBlockIndex* work_index{register_work_block(params, data.rotation_enabled, indexes[i])}; + if (work_index == nullptr) { + error = "missing active quorum work block"; + return false; + } + auto entry{ReadCommitment(qblockman, params.type, indexes[i], work_index, error)}; + if (!error.empty()) return false; + data.active_commitments.emplace_back(std::move(entry)); + } + + if (data.rotation_enabled) { + indexes = qblockman.GetLastMinedCommitmentsPerQuorumIndexUntilBlock(params.type, base_index, 1); + } else { + indexes.erase(indexes.begin(), indexes.begin() + emit_active); + } + const size_t safety_count{std::min(indexes.size(), total_count - active_count)}; + for (size_t i{0}; i < safety_count; ++i) { + const CBlockIndex* work_index{register_work_block(params, data.rotation_enabled, indexes[i])}; + if (work_index == nullptr) { + error = "missing safety quorum work block"; + return false; + } + auto entry{ReadCommitment(qblockman, params.type, indexes[i], work_index, error)}; + if (!error.empty()) return false; + data.safety_commitments.emplace_back(std::move(entry)); + } + + if (data.rotation_enabled) { + std::vector one_type{params}; + for (const auto& required : EvoSnapshotReconstructionHeights(base_index->nHeight, one_type)) { + const int cycle_height{required.quorum_height}; + const int work_height{required.work_height}; + const CBlockIndex* cycle_index{cycle_height >= 0 ? base_index->GetAncestor(cycle_height) : nullptr}; + const CBlockIndex* work_index{cycle_index && work_height >= 0 + ? cycle_index->GetAncestor(work_height) + : nullptr}; + // Horizons preceding the chain, and cycles that predate the + // type's first rotation DKG, have no snapshot to carry. A gap + // on a mature chain surfaces at completion, where quorum + // reconstruction from the carried state must match the chain. + if (cycle_index == nullptr || work_index == nullptr) continue; + auto stored{qsnapman.GetSnapshotForBlock(params.type, cycle_index)}; + if (!stored) continue; + data.rotation_snapshots.push_back( + {cycle_index->GetBlockHash(), work_index->GetBlockHash(), *stored}); + historical.try_emplace(work_index->GetBlockHash(), work_index, dmnman.GetListForBlock(work_index)); + modifiers.emplace(std::make_pair(params.type, work_index->GetBlockHash()), + llmq::utils::GetQuorumHashModifier(params, chainparams.GetConsensus(), cycle_index)); + } + } + data.active_commitments = CanonicallySortedCopy(std::move(data.active_commitments)); + data.safety_commitments = CanonicallySortedCopy(std::move(data.safety_commitments)); + data.rotation_snapshots = CanonicallySortedCopy(std::move(data.rotation_snapshots)); + result.quorums.emplace_back(std::move(data)); + } + + std::vector> ordered_history; + ordered_history.reserve(historical.size()); + for (auto& [_, indexed_list] : historical) ordered_history.emplace_back(std::move(indexed_list)); + std::sort(ordered_history.begin(), ordered_history.end(), [](const auto& a, const auto& b) { + return std::make_tuple(a.first->nHeight, a.first->GetBlockHash()) > + std::make_tuple(b.first->nHeight, b.first->GetBlockHash()); + }); + CDeterministicMNList previous_list{result.mn_list}; + uint256 previous_hash{result.base_block_hash}; + for (const auto& [index, list] : ordered_history) { + if (index->GetBlockHash() == result.base_block_hash) continue; + result.historical_mn_list_diffs.push_back({previous_hash, index->GetBlockHash(), index->nHeight, + list.GetTotalRegisteredCount(), CanonicalMNListHash(list), + previous_list.BuildDiff(list)}); + previous_hash = index->GetBlockHash(); + previous_list = list; + } + for (const auto& [key, modifier] : modifiers) { + result.quorum_modifiers.push_back({key.first, key.second, modifier}); + } + result.credit_pool = cpoolman.GetCreditPool(base_index); + result.mnhf_signals = mnhfman.GetSignalsStage(base_index); + result.quorums = CanonicallySortedCopy(std::move(result.quorums)); + result.historical_mn_list_diffs = CanonicallySortedCopy(std::move(result.historical_mn_list_diffs)); + result.quorum_modifiers = CanonicallySortedCopy(std::move(result.quorum_modifiers)); + try { + result.Validate(); + } catch (const std::exception& e) { + error = e.what(); + return false; + } + snapshot = std::move(result); + return true; +} + +bool ValidateEvoSnapshotAgainstChain(const EvoSnapshot& snapshot, const ChainstateManager& chainman, + const CBlockIndex* base_index, std::string& error) +{ + AssertLockHeld(::cs_main); + error.clear(); + const auto fail = [&](const std::string& message) { + error = message; + return false; + }; + if (base_index == nullptr || snapshot.base_block_hash != base_index->GetBlockHash() || + snapshot.mn_list.GetBlockHash() != base_index->GetBlockHash() || + snapshot.mn_list.GetHeightForSnapshotCodec() != base_index->nHeight) { + return fail("evo snapshot base block/height mismatch"); + } + try { + snapshot.Validate(/*require_canonical_order=*/true); + } catch (const std::exception& e) { + return fail(e.what()); + } + + const auto& consensus{chainman.GetConsensus()}; + if (!DeploymentActiveAt(*base_index, consensus, Consensus::DEPLOYMENT_DIP0003)) { + if (!snapshot.quorums.empty() || !snapshot.historical_mn_list_diffs.empty() || + !snapshot.quorum_modifiers.empty() || snapshot.mn_list.GetCounts().total() != 0 || + snapshot.mn_list.GetTotalRegisteredCount() != 0 || snapshot.credit_pool.locked != 0 || + snapshot.credit_pool.currentLimit != 0 || snapshot.credit_pool.latelyUnlocked != 0 || + !snapshot.credit_pool.indexes.IsEmpty() || !snapshot.mnhf_signals.empty()) { + return fail("nonempty pre-DIP3 evo snapshot"); + } + return true; + } + + std::map historical_lists; + if (!ReconstructHistoricalMNLists(snapshot, historical_lists, error)) return false; + for (const auto& entry : snapshot.historical_mn_list_diffs) { + const CBlockIndex* index{chainman.m_blockman.LookupBlockIndex(entry.block_hash)}; + if (index == nullptr || base_index->GetAncestor(index->nHeight) != index || + entry.height != index->nHeight) { + return fail("invalid historical evo MN list chain data"); + } + } + + std::map actual; + for (const auto& data : snapshot.quorums) actual.emplace(data.llmq_type, &data); + size_t enabled_count{0}; + std::set required_work_hashes; + for (const auto& params : consensus.llmqs) { + if (!chainman.IsQuorumTypeEnabled(params.type, base_index)) continue; + ++enabled_count; + const auto it{actual.find(params.type)}; + if (it == actual.end()) return fail("missing enabled evo quorum type"); + const auto& data{*it->second}; + const bool rotation_enabled{llmq::IsQuorumRotationEnabled(params, base_index)}; + const size_t active_count{static_cast(params.signingActiveQuorumCount)}; + const size_t total_count{SnapshotCommitmentCount(params, rotation_enabled)}; + if (data.rotation_enabled != rotation_enabled || data.active_commitments.size() > active_count || + data.safety_commitments.size() > total_count - active_count) { + return fail("evo quorum params/count mismatch"); + } + + std::set active_indexes; + const auto validate_work_block = [&](const MinedQuorumCommitment& entry) EXCLUSIVE_LOCKS_REQUIRED(::cs_main) { + const CBlockIndex* quorum_index{chainman.m_blockman.LookupBlockIndex(entry.quorum_base_block_hash)}; + if (quorum_index == nullptr) return false; + const CBlockIndex* modifier_base{rotation_enabled + ? quorum_index->GetAncestor(quorum_index->nHeight - quorum_index->nHeight % params.dkgInterval) + : quorum_index}; + if (modifier_base == nullptr) return false; + const CBlockIndex* expected_work{ + (rotation_enabled || DeploymentActiveAfter(modifier_base, consensus, Consensus::DEPLOYMENT_V20)) + ? modifier_base->GetAncestor(modifier_base->nHeight - llmq::WORK_DIFF_DEPTH) + : modifier_base}; + return expected_work != nullptr && entry.work_block_hash == expected_work->GetBlockHash(); + }; + for (const auto& entry : data.active_commitments) { + if (!ValidateCommitmentAgainstChain(entry, chainman, base_index, params, rotation_enabled) || + !validate_work_block(entry) || + (rotation_enabled && !active_indexes.insert(entry.commitment.quorumIndex).second)) { + return fail("invalid active evo quorum commitment chain data"); + } + required_work_hashes.insert(entry.work_block_hash); + } + for (const auto& entry : data.safety_commitments) { + if (!ValidateCommitmentAgainstChain(entry, chainman, base_index, params, rotation_enabled) || + !validate_work_block(entry)) { + return fail("invalid safety evo quorum commitment chain data"); + } + required_work_hashes.insert(entry.work_block_hash); + } + std::map rotations; + for (const auto& entry : data.rotation_snapshots) rotations.emplace(entry.cycle_base_block_hash, &entry); + const auto heights{EvoSnapshotReconstructionHeights(base_index->nHeight, {params})}; + if (rotations.size() > (rotation_enabled ? heights.size() : size_t{0})) { + return fail("evo rotation snapshot count mismatch"); + } + if (rotation_enabled) { + // Every carried rotation snapshot must sit at a derived horizon + // cycle with the matching work ancestor. Horizons the chain or the + // type's rotation history cannot provide are legitimately absent; + // completion-time quorum reconstruction establishes sufficiency. + size_t matched{0}; + for (const auto& required : heights) { + const int cycle_height{required.quorum_height}; + const int work_height{required.work_height}; + const CBlockIndex* cycle{cycle_height >= 0 ? base_index->GetAncestor(cycle_height) : nullptr}; + const CBlockIndex* work{work_height >= 0 ? base_index->GetAncestor(work_height) : nullptr}; + if (cycle == nullptr || work == nullptr) continue; + const auto rotation{rotations.find(cycle->GetBlockHash())}; + if (rotation == rotations.end()) continue; + if (rotation->second->work_block_hash != work->GetBlockHash()) { + return fail("evo rotation cycle/work ancestor mismatch"); + } + const auto historical{historical_lists.find(work->GetBlockHash())}; + if (historical == historical_lists.end() || + rotation->second->snapshot.activeQuorumMembers.size() != + historical->second.GetCounts().total()) { + return fail("evo rotation bitset/work-block MN count mismatch"); + } + required_work_hashes.insert(work->GetBlockHash()); + ++matched; + } + if (matched != rotations.size()) return fail("unknown evo rotation cycle"); + } + } + if (actual.size() != enabled_count) return fail("unexpected disabled evo quorum type"); + + std::set historical_hashes; + for (const auto& [hash, list] : historical_lists) historical_hashes.insert(hash); + historical_hashes.erase(base_index->GetBlockHash()); + required_work_hashes.erase(base_index->GetBlockHash()); + if (historical_hashes != required_work_hashes) return fail("missing or extra historical evo MN list"); + + std::map, uint256> seeded_modifiers; + for (const auto& entry : snapshot.quorum_modifiers) { + seeded_modifiers.emplace(std::make_pair(entry.llmq_type, entry.work_block_hash), entry.modifier); + } + for (const auto& data : snapshot.quorums) { + const auto params{chainman.GetParams().GetLLMQ(data.llmq_type)}; + if (!params) return fail("unknown chain LLMQ parameters for modifier"); + const auto check_modifier = [&](const uint256& quorum_hash, const uint256& work_hash) EXCLUSIVE_LOCKS_REQUIRED(::cs_main) { + const auto seeded{seeded_modifiers.find(std::make_pair(data.llmq_type, work_hash))}; + const CBlockIndex* quorum_index{chainman.m_blockman.LookupBlockIndex(quorum_hash)}; + const CBlockIndex* work_index{chainman.m_blockman.LookupBlockIndex(work_hash)}; + if (seeded == seeded_modifiers.end() || quorum_index == nullptr || work_index == nullptr) return false; + if ((work_index->nStatus & BLOCK_HAVE_DATA) != 0 && + seeded->second != llmq::utils::GetQuorumHashModifier(*params, consensus, quorum_index)) return false; + return true; + }; + for (const auto* commitments : {&data.active_commitments, &data.safety_commitments}) { + for (const auto& entry : *commitments) { + const CBlockIndex* quorum_index{chainman.m_blockman.LookupBlockIndex(entry.quorum_base_block_hash)}; + const CBlockIndex* modifier_base{data.rotation_enabled && quorum_index != nullptr + ? quorum_index->GetAncestor(quorum_index->nHeight - quorum_index->nHeight % params->dkgInterval) + : quorum_index}; + if (modifier_base == nullptr || + !check_modifier(modifier_base->GetBlockHash(), entry.work_block_hash)) { + return fail("evo seeded quorum modifier mismatch"); + } + } + } + for (const auto& entry : data.rotation_snapshots) { + if (!check_modifier(entry.cycle_base_block_hash, entry.work_block_hash)) { + return fail("evo seeded rotation modifier mismatch"); + } + } + } + + if (!MoneyRange(snapshot.credit_pool.locked) || !MoneyRange(snapshot.credit_pool.currentLimit) || + !MoneyRange(snapshot.credit_pool.latelyUnlocked)) return fail("invalid evo credit pool monetary value"); + for (const auto& [bit, height] : snapshot.mnhf_signals) { + if (bit >= VERSIONBITS_NUM_BITS || height < 0 || height > base_index->nHeight) { + return fail("invalid evo MNHF signal bit/height"); + } + } + return true; +} + +} // namespace evo diff --git a/src/evo/snapshot_types.h b/src/evo/snapshot_types.h new file mode 100644 index 000000000000..a6b4389ef1e5 --- /dev/null +++ b/src/evo/snapshot_types.h @@ -0,0 +1,20 @@ +// Copyright (c) 2026 The Dash Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_EVO_SNAPSHOT_TYPES_H +#define BITCOIN_EVO_SNAPSHOT_TYPES_H + +#include + +namespace evo { + +class SnapshotStateMismatchError : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; + +} // namespace evo + +#endif // BITCOIN_EVO_SNAPSHOT_TYPES_H diff --git a/src/evo/specialtxman.cpp b/src/evo/specialtxman.cpp index ac5daf3b3a26..04a3a1754971 100644 --- a/src/evo/specialtxman.cpp +++ b/src/evo/specialtxman.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -908,7 +909,10 @@ bool CSpecialTxProcessor::ProcessSpecialTxsInBlock(Chainstate& chainstate, const LogPrint(BCLog::BENCHMARK, " - m_qblockman.ProcessBlock: %.2fms [%.2fs]\n", 0.001 * (nTime5 - nTime4), nTimeQuorum * 0.000001); - CDeterministicMNList mn_list; + // Even before DIP3, bind the canonical empty list to the block so the + // independently derived completion hash has the same identity as an + // empty evo snapshot section. + CDeterministicMNList mn_list{pindex->GetBlockHash(), pindex->nHeight, 0}; if (DeploymentActiveAt(*pindex, m_consensus_params, Consensus::DEPLOYMENT_DIP0003)) { if (!BuildNewListFromBlock(block, pindex->pprev, is_v24_active, view, true, state, mn_list)) { // pass the state returned by the function above diff --git a/src/llmq/blockprocessor.cpp b/src/llmq/blockprocessor.cpp index 6ef091bfd927..6b0bdf621807 100644 --- a/src/llmq/blockprocessor.cpp +++ b/src/llmq/blockprocessor.cpp @@ -669,6 +669,35 @@ std::pair CQuorumBlockProcessor::GetMinedCommitment(C return ret; } +bool CQuorumBlockProcessor::SeedMinedCommitment(Consensus::LLMQType llmqType, const uint256& quorum_hash, + const CFinalCommitment& commitment, + const uint256& mined_block_hash) +{ + AssertLockHeld(::cs_main); + const auto llmq_params = Params().GetLLMQ(llmqType); + const CBlockIndex* mined_index = m_chainman.m_blockman.LookupBlockIndex(mined_block_hash); + const CBlockIndex* quorum_base_index = m_chainman.m_blockman.LookupBlockIndex(quorum_hash); + if (!llmq_params || mined_index == nullptr || quorum_base_index == nullptr) return false; + if (!m_evoDb.WriteDerived( + std::make_pair(DB_MINED_COMMITMENT, std::make_pair(llmqType, quorum_hash)), + std::make_pair(commitment, mined_block_hash))) { + return false; + } + + // Replay ProcessCommitment's iteration index exactly. These entries drive + // the first post-snapshot CbTx quorum-merkle-root calculation. + if (IsQuorumRotationEnabled(*llmq_params, quorum_base_index)) { + m_evoDb.Write(BuildInversedHeightKeyIndexed(llmqType, mined_index->nHeight, + int(commitment.quorumIndex)), + quorum_base_index->nHeight); + } else { + m_evoDb.Write(BuildInversedHeightKey(llmqType, mined_index->nHeight), quorum_base_index->nHeight); + } + DropQcHashesCache(); + WITH_LOCK(minableCommitmentsCs, mapMinedCommitmentBlockCache.erase(llmqType, quorum_hash)); + return true; +} + // The returned quorums are in reversed order, so the most recent one is at index 0 std::vector CQuorumBlockProcessor::GetMinedCommitmentsUntilBlock(Consensus::LLMQType llmqType, gsl::not_null pindex, size_t maxCount) const { diff --git a/src/llmq/blockprocessor.h b/src/llmq/blockprocessor.h index 841a1c2b3ded..ebd229d76f32 100644 --- a/src/llmq/blockprocessor.h +++ b/src/llmq/blockprocessor.h @@ -126,6 +126,10 @@ class CQuorumBlockProcessor 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; + /** Seed a mined commitment in the current EvoDB transaction. */ + bool SeedMinedCommitment(Consensus::LLMQType llmqType, const uint256& quorum_hash, + const CFinalCommitment& commitment, const uint256& mined_block_hash) + EXCLUSIVE_LOCKS_REQUIRED(::cs_main, !minableCommitmentsCs, !m_qc_hashes_cache_mutex); /** * Serialized hashes of the commitments mined for the quorums active as of pindexPrev. diff --git a/src/llmq/snapshot.cpp b/src/llmq/snapshot.cpp index bde0d32fb783..e39ad808e32b 100644 --- a/src/llmq/snapshot.cpp +++ b/src/llmq/snapshot.cpp @@ -12,6 +12,8 @@ #include #include #include +#include +#include #include #include @@ -321,10 +323,50 @@ void CQuorumSnapshotManager::StoreSnapshotForBlock(const Consensus::LLMQType llm { auto snapshotHash = ::SerializeHash(std::make_pair(llmqType, pindex->GetBlockHash())); - // LOCK(::cs_main); - AssertLockNotHeld(m_evoDb.cs); - LOCK2(snapshotCacheCs, m_evoDb.cs); - m_evoDb.GetRawDB().Write(std::make_pair(DB_QUORUM_SNAPSHOT, snapshotHash), snapshot); + if (!m_evoDb.WriteDerived(std::make_pair(DB_QUORUM_SNAPSHOT, snapshotHash), snapshot)) { + // A mismatch is local EvoDB corruption, not a statement about the + // block. Abort here like the credit pool does: quorum members are also + // computed outside the block-connect catches (DKG, RPC), and those + // catches must not translate this into a consensus rejection. + const std::string msg = strprintf("CQuorumSnapshotManager::%s -- EvoDB quorum snapshot mismatch for block %s", + __func__, pindex->GetBlockHash().ToString()); + AbortNode(msg); + throw EvoDbInconsistencyError(msg); + } + LOCK(snapshotCacheCs); quorumSnapshotCache.insert(snapshotHash, snapshot); } + +bool CQuorumSnapshotManager::SeedSnapshotForBlock(const Consensus::LLMQType llmqType, const CBlockIndex* pindex, + const CQuorumSnapshot& snapshot) +{ + if (!Assume(pindex != nullptr)) return false; + const auto snapshot_hash = ::SerializeHash(std::make_pair(llmqType, pindex->GetBlockHash())); + return m_evoDb.WriteDerived(std::make_pair(DB_QUORUM_SNAPSHOT, snapshot_hash), snapshot); +} + +bool CQuorumSnapshotManager::SeedQuorumModifier(Consensus::LLMQType llmq_type, + const uint256& work_block_hash, + const uint256& modifier) +{ + return m_evoDb.WriteDerived(std::make_tuple(std::string_view{"llmq_M3"}, llmq_type, work_block_hash), modifier); +} + +std::optional CQuorumSnapshotManager::GetSeededQuorumModifier( + Consensus::LLMQType llmq_type, const uint256& work_block_hash) const +{ + uint256 modifier; + if (!m_evoDb.Read(std::make_tuple(std::string_view{"llmq_M3"}, llmq_type, work_block_hash), modifier)) { + return std::nullopt; + } + return modifier; +} + +void CQuorumSnapshotManager::InvalidateSnapshotCacheForBlock(Consensus::LLMQType llmq_type, + const uint256& block_hash) +{ + const auto snapshot_hash{::SerializeHash(std::make_pair(llmq_type, block_hash))}; + LOCK(snapshotCacheCs); + quorumSnapshotCache.erase(snapshot_hash); +} } // namespace llmq diff --git a/src/llmq/snapshot.h b/src/llmq/snapshot.h index 43370849c461..108fc0cdffe3 100644 --- a/src/llmq/snapshot.h +++ b/src/llmq/snapshot.h @@ -248,6 +248,15 @@ class CQuorumSnapshotManager std::optional GetSnapshotForBlock(Consensus::LLMQType llmqType, const CBlockIndex* pindex); void StoreSnapshotForBlock(Consensus::LLMQType llmqType, const CBlockIndex* pindex, const CQuorumSnapshot& snapshot); + /** Seed EvoDB without publishing state to the shared NORMAL-chainstate cache. */ + bool SeedSnapshotForBlock(Consensus::LLMQType llmqType, const CBlockIndex* pindex, + const CQuorumSnapshot& snapshot); + /** Seed/read the exact v20 score modifier keyed by type and work block. */ + bool SeedQuorumModifier(Consensus::LLMQType llmq_type, const uint256& work_block_hash, + const uint256& modifier); + std::optional GetSeededQuorumModifier(Consensus::LLMQType llmq_type, + const uint256& work_block_hash) const; + void InvalidateSnapshotCacheForBlock(Consensus::LLMQType llmq_type, const uint256& block_hash); }; } // namespace llmq diff --git a/src/llmq/utils.cpp b/src/llmq/utils.cpp index 6dda88f527cc..dd1cacaca644 100644 --- a/src/llmq/utils.cpp +++ b/src/llmq/utils.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -23,6 +24,7 @@ #include #include #include +#include /** * Forward declarations @@ -96,8 +98,8 @@ uint256 GetHashModifierFromWorkBlock(const Consensus::LLMQParams& llmqParams, co return ::SerializeHash(std::make_pair(llmqParams.type, pWorkBlockIndex->GetBlockHash())); } -uint256 GetHashModifier(const Consensus::LLMQParams& llmqParams, const Consensus::Params& consensus_params, - gsl::not_null pCycleQuorumBaseBlockIndex) +uint256 CalculateHashModifier(const Consensus::LLMQParams& llmqParams, const Consensus::Params& consensus_params, + gsl::not_null pCycleQuorumBaseBlockIndex) { ASSERT_IF_DEBUG(pCycleQuorumBaseBlockIndex->nHeight % llmqParams.dkgInterval == 0); const CBlockIndex* pWorkBlockIndex = pCycleQuorumBaseBlockIndex->GetAncestor(pCycleQuorumBaseBlockIndex->nHeight - llmq::WORK_DIFF_DEPTH); @@ -114,6 +116,22 @@ uint256 GetHashModifier(const Consensus::LLMQParams& llmqParams, const Consensus return ::SerializeHash(std::make_pair(llmqParams.type, pCycleQuorumBaseBlockIndex->GetBlockHash())); } +uint256 GetHashModifier(const Consensus::LLMQParams& llmq_params, const Consensus::Params& consensus_params, + gsl::not_null cycle_index, + const llmq::CQuorumSnapshotManager* snapshot_manager) +{ + const CBlockIndex* work_index{cycle_index->GetAncestor(cycle_index->nHeight - llmq::WORK_DIFF_DEPTH)}; + if (snapshot_manager != nullptr && work_index != nullptr) { + if (const auto seeded{snapshot_manager->GetSeededQuorumModifier(llmq_params.type, work_index->GetBlockHash())}) { + if (WITH_LOCK(::cs_main, return (work_index->nStatus & BLOCK_HAVE_DATA) == 0;)) return *seeded; + const uint256 recomputed{CalculateHashModifier(llmq_params, consensus_params, cycle_index)}; + if (recomputed != *seeded) throw evo::SnapshotStateMismatchError("seeded quorum score modifier mismatch"); + return recomputed; + } + } + return CalculateHashModifier(llmq_params, consensus_params, cycle_index); +} + std::vector CalculateScoresForQuorum(QuorumMembers&& dmns, const uint256& modifier, const bool onlyEvoNodes) { std::vector scores; @@ -187,6 +205,7 @@ QuorumMembers CalculateQuorum(List&& mn_list, const uint256& modifier, size_t ma std::vector GetQuorumQuarterMembersBySnapshot(const Consensus::LLMQParams& llmqParams, CDeterministicMNManager& dmnman, + const llmq::CQuorumSnapshotManager& qsnapman, const Consensus::Params& consensus_params, const CBlockIndex* pCycleQuorumBaseBlockIndex, const llmq::CQuorumSnapshot& snapshot, int nHeight) @@ -201,7 +220,7 @@ std::vector GetQuorumQuarterMembersBySnapshot(const Consensus::LL const CBlockIndex* pWorkBlockIndex = pCycleQuorumBaseBlockIndex->GetAncestor( pCycleQuorumBaseBlockIndex->nHeight - llmq::WORK_DIFF_DEPTH); auto mn_list = dmnman.GetListForBlock(pWorkBlockIndex); - const auto modifier = GetHashModifier(llmqParams, consensus_params, pCycleQuorumBaseBlockIndex); + const auto modifier = GetHashModifier(llmqParams, consensus_params, pCycleQuorumBaseBlockIndex, &qsnapman); auto sortedAllMns = CalculateQuorum(mn_list, modifier); std::vector usedMNs; @@ -289,7 +308,8 @@ std::vector GetQuorumQuarterMembersBySnapshot(const Consensus::LL } QuorumMembers ComputeQuorumMembers(Consensus::LLMQType llmqType, const CChainParams& chainparams, - const CDeterministicMNList& mn_list, const CBlockIndex* pQuorumBaseBlockIndex) + const CDeterministicMNList& mn_list, const CBlockIndex* pQuorumBaseBlockIndex, + const llmq::CQuorumSnapshotManager* qsnapman) { bool EvoOnly = (chainparams.GetConsensus().llmqTypePlatform == llmqType) && DeploymentActiveAfter(pQuorumBaseBlockIndex, chainparams.GetConsensus(), Consensus::DEPLOYMENT_V19); @@ -300,7 +320,8 @@ QuorumMembers ComputeQuorumMembers(Consensus::LLMQType llmqType, const CChainPar return {}; } - const auto modifier = GetHashModifier(llmq_params_opt.value(), chainparams.GetConsensus(), pQuorumBaseBlockIndex); + const auto modifier = GetHashModifier(llmq_params_opt.value(), chainparams.GetConsensus(), pQuorumBaseBlockIndex, + qsnapman); return CalculateQuorum(mn_list, modifier, llmq_params_opt->size, EvoOnly); } @@ -316,7 +337,7 @@ void BuildQuorumSnapshot(const Consensus::LLMQParams& llmqParams, const Consensu const auto allMnsTotal = allMns.GetCounts().total(); quorumSnapshot.activeQuorumMembers.resize(allMnsTotal); - const auto modifier = GetHashModifier(llmqParams, consensus_params, pCycleQuorumBaseBlockIndex); + const auto modifier = GetHashModifier(llmqParams, consensus_params, pCycleQuorumBaseBlockIndex, nullptr); auto sortedAllMns = CalculateQuorum(allMns, modifier); LogPrint(BCLog::LLMQ, "BuildQuorumSnapshot h[%d] numMns[%d]\n", pCycleQuorumBaseBlockIndex->nHeight, @@ -503,6 +524,7 @@ std::vector ComputeQuorumMembersByQuarterRotation(const Consensus break; } prev_cycles[idx]->m_members = GetQuorumQuarterMembersBySnapshot(llmqParams, util_params.m_dmnman, + util_params.m_qsnapman, util_params.m_chainman.GetConsensus(), prev_cycles[idx]->m_cycle_index, prev_cycles[idx]->m_snap, @@ -546,6 +568,13 @@ std::vector ComputeQuorumMembersByQuarterRotation(const Consensus namespace llmq { namespace utils { +uint256 GetQuorumHashModifier(const Consensus::LLMQParams& llmq_params, + const Consensus::Params& consensus_params, + gsl::not_null cycle_quorum_base_index) +{ + return CalculateHashModifier(llmq_params, consensus_params, cycle_quorum_base_index); +} + BlsCheck::BlsCheck() = default; BlsCheck::BlsCheck(CBLSSignature sig, std::vector pubkeys, uint256 msg_hash, std::string id_string) : @@ -631,7 +660,8 @@ std::optional> ComputeQuorumMembersFromWorkBlo return quorumMembers[quorumIndex]; } -QuorumMembers GetAllQuorumMembers(Consensus::LLMQType llmqType, const UtilParameters& util_params, bool reset_cache) +static QuorumMembers GetAllQuorumMembersInternal(Consensus::LLMQType llmqType, const UtilParameters& util_params, + bool reset_cache) { static RecursiveMutex cs_members; static PerLlmqTypeCache mapQuorumMembers GUARDED_BY(cs_members); @@ -701,7 +731,7 @@ QuorumMembers GetAllQuorumMembers(Consensus::LLMQType llmqType, const UtilParame const CBlockIndex* pWorkBlockIndex = pCycleQuorumBaseBlockIndex->GetAncestor(cycleQuorumBaseHeight - WORK_DIFF_DEPTH); const auto modifier = GetHashModifier(llmq_params, util_params.m_chainman.GetConsensus(), - pCycleQuorumBaseBlockIndex); + pCycleQuorumBaseBlockIndex, &util_params.m_qsnapman); auto q = ComputeQuorumMembersByQuarterRotation(llmq_params, util_params.replace_index(pCycleQuorumBaseBlockIndex), pWorkBlockIndex, cycleQuorumBaseHeight, modifier, /*predicting=*/false); @@ -721,7 +751,7 @@ QuorumMembers GetAllQuorumMembers(Consensus::LLMQType llmqType, const UtilParame : util_params.m_base_index.get(); CDeterministicMNList mn_list = util_params.m_dmnman.GetListForBlock(pWorkBlockIndex); quorumMembers = ComputeQuorumMembers(llmqType, util_params.m_chainman.GetParams(), mn_list, - util_params.m_base_index); + util_params.m_base_index, &util_params.m_qsnapman); } LOCK(cs_members); @@ -729,6 +759,15 @@ QuorumMembers GetAllQuorumMembers(Consensus::LLMQType llmqType, const UtilParame return quorumMembers; } +QuorumMembers GetAllQuorumMembers(Consensus::LLMQType llmqType, const UtilParameters& util_params, bool reset_cache) +{ + // A SnapshotStateMismatchError from a seeded-modifier disagreement + // propagates to the caller. Production code does not seed modifiers yet; + // the load-time integration later in the series routes this into the + // controlled invalid-snapshot path. + return GetAllQuorumMembersInternal(llmqType, util_params, reset_cache); +} + uint256 DeterministicOutboundConnection(const uint256& proTxHash1, const uint256& proTxHash2) { // We need to deterministically select who is going to initiate the connection. The naive way would be to simply diff --git a/src/llmq/utils.h b/src/llmq/utils.h index ef9b11987bb4..441cd5aa7ebf 100644 --- a/src/llmq/utils.h +++ b/src/llmq/utils.h @@ -43,6 +43,11 @@ struct UtilParameters { }; namespace utils { +/** Normal consensus modifier calculation; snapshot overrides are internal to reconstruction. */ +uint256 GetQuorumHashModifier(const Consensus::LLMQParams& llmq_params, + const Consensus::Params& consensus_params, + gsl::not_null cycle_quorum_base_index); + struct BlsCheck { CBLSSignature m_sig; std::vector m_pubkeys; diff --git a/src/test/evo_db_tests.cpp b/src/test/evo_db_tests.cpp index cf5de0d76edb..bff9d3450fd0 100644 --- a/src/test/evo_db_tests.cpp +++ b/src/test/evo_db_tests.cpp @@ -240,10 +240,18 @@ BOOST_AUTO_TEST_CASE(snapshot_markers_can_be_discarded) auto tx = db.BeginTransaction(EvoDbIdentity::SNAPSHOT); db.WriteSnapshotBaseMNListHash(BlockHash(4)); db.WriteBackgroundMNListHash(BlockHash(40), BlockHash(4)); + db.Write(EVODB_SNAPSHOT_EVO_SECTION, BlockHash(44)); db.WriteDualChainstateMarker(); tx->Commit(); } BOOST_REQUIRE(db.CommitRootTransaction(EvoDbIdentity::SNAPSHOT)); + { + auto tx = db.BeginTransaction(EvoDbIdentity::NORMAL); + db.WriteRequiredWorkMNListHashes({BlockHash(30), BlockHash(35)}); + db.WriteBackgroundWorkMNListHash(BlockHash(30), BlockHash(3)); + tx->Commit(); + } + BOOST_REQUIRE(db.CommitRootTransaction(EvoDbIdentity::NORMAL)); BOOST_REQUIRE(db.HasDualChainstateMarker()); // Abandoning snapshot activation after the markers were committed must @@ -264,6 +272,10 @@ BOOST_AUTO_TEST_CASE(snapshot_markers_can_be_discarded) BOOST_CHECK(!reopened.ReadSnapshotBaseMNListHash(hash)); BOOST_CHECK(!reopened.ReadBackgroundMNListHash(hash, hash2)); BOOST_CHECK(!reopened.HasDualChainstateMarker()); + std::vector required; + BOOST_CHECK(!reopened.ReadRequiredWorkMNListHashes(required)); + BOOST_CHECK(!reopened.ReadBackgroundWorkMNListHash(BlockHash(30), hash)); + BOOST_CHECK(!reopened.Exists(EVODB_SNAPSHOT_EVO_SECTION)); } BOOST_AUTO_TEST_CASE(snapshot_marker_promotion_and_discard) diff --git a/src/test/evo_snapshot_tests.cpp b/src/test/evo_snapshot_tests.cpp index 2d3f97647c16..f9167ac2a726 100644 --- a/src/test/evo_snapshot_tests.cpp +++ b/src/test/evo_snapshot_tests.cpp @@ -223,8 +223,39 @@ void CheckInvalid(evo::EvoSnapshot snapshot) { BOOST_CHECK_THROW(snapshot.Valida // bucket. static_assert(std::is_same_v); +//! Restores consensus params mutated through const_cast when the test case +//! leaves scope, including through a failed BOOST_REQUIRE, so mutated state +//! cannot leak into cases running later in the same process. +class [[nodiscard]] ConsensusParamsRestorer +{ + Consensus::Params& m_params; + const Consensus::Params m_saved; + +public: + explicit ConsensusParamsRestorer(const Consensus::Params& params) : + m_params{const_cast(params)}, m_saved{params} + { + } + ~ConsensusParamsRestorer() { m_params = m_saved; } + Consensus::Params& Get() { return m_params; } +}; + } // namespace +//! Chain fixture whose activation heights are already in force while the chain +//! is mined, so every historical coinbase is the CbTx that v20-era code paths +//! (e.g. the quorum hash modifier's chainlock probe) are entitled to assume. +//! Forcing the heights down through const_cast after mining instead would leave +//! pre-DIP3 coinbases on a chain claiming v20 was always active, which trips +//! GetTxPayload's payload-type assertion in debug builds. +struct SnapshotActivationChainSetup : public TestChainSetup { + SnapshotActivationChainSetup() : + TestChainSetup{102, CBaseChainParams::REGTEST, + {"-dip3params=2:2", "-testactivationheight=v20@2", "-testactivationheight=mn_rr@2"}} + { + } +}; + BOOST_AUTO_TEST_SUITE(evo_snapshot_tests) BOOST_FIXTURE_TEST_CASE(populated_roundtrip_and_representation_independence, BasicTestingSetup) @@ -272,6 +303,610 @@ BOOST_FIXTURE_TEST_CASE(populated_roundtrip_and_representation_independence, Bas } } +BOOST_FIXTURE_TEST_CASE(snapshot_identity_seeding_is_retrievable, TestChain100Setup) +{ + const CBlockIndex* base{WITH_LOCK(::cs_main, return m_node.chainman->ActiveTip())}; + BOOST_REQUIRE(base != nullptr); + const auto list{MNList(base->GetBlockHash(), base->nHeight, false)}; + const CBlockIndex* historical_index{base->GetAncestor(50)}; + const auto historical_list{MNList(historical_index->GetBlockHash(), historical_index->nHeight, true)}; + const auto indexed_commitment = [&](Consensus::LLMQType type, int quorum_height, int mined_height, + bool rotated, int16_t quorum_index = 0) { + auto entry{Commitment(type, 1, 2, rotated, quorum_index)}; + entry.quorum_base_block_hash = base->GetAncestor(quorum_height)->GetBlockHash(); + entry.commitment.quorumHash = entry.quorum_base_block_hash; + entry.mined_block_hash = base->GetAncestor(mined_height)->GetBlockHash(); + return entry; + }; + const std::vector nonrotated{ + indexed_commitment(Consensus::LLMQType::LLMQ_TEST, 48, 58, false), + indexed_commitment(Consensus::LLMQType::LLMQ_TEST, 72, 82, false), + }; + const std::vector rotated{ + indexed_commitment(Consensus::LLMQType::LLMQ_TEST_DIP0024, 72, 84, true, 0), + indexed_commitment(Consensus::LLMQType::LLMQ_TEST_DIP0024, 73, 85, true, 1), + }; + CCreditPool pool; + pool.locked = 123; + pool.currentLimit = 45; + pool.latelyUnlocked = 6; + AbstractEHFManager::Signals signals{{2, base->nHeight}}; + llmq::CQuorumSnapshot quorum_snapshot{{true, false, true}, SnapshotSkipMode::MODE_NO_SKIPPING, {}}; + + ConsensusParamsRestorer params_restorer{Params().GetConsensus()}; + const int old_dip3_height{params_restorer.Get().DIP0003Height}; + params_restorer.Get().DIP0003Height = 1; + BOOST_CHECK_EQUAL(m_node.dmnman->GetListForBlock(base).GetCounts().total(), 0U); + BOOST_CHECK_EQUAL(m_node.dmnman->GetListForBlock(historical_index).GetCounts().total(), 0U); + + { + auto tx{m_node.evodb->BeginTransaction(EvoDbIdentity::SNAPSHOT)}; + BOOST_REQUIRE(m_node.dmnman->SeedListForBlock(list)); + BOOST_REQUIRE(m_node.dmnman->SeedListForBlock(historical_list)); + { + LOCK(::cs_main); + for (const auto& entry : nonrotated) { + BOOST_REQUIRE(m_node.llmq_ctx->quorum_block_processor->SeedMinedCommitment( + entry.commitment.llmqType, entry.quorum_base_block_hash, + entry.commitment, entry.mined_block_hash)); + } + for (const auto& entry : rotated) { + BOOST_REQUIRE(m_node.llmq_ctx->quorum_block_processor->SeedMinedCommitment( + entry.commitment.llmqType, entry.quorum_base_block_hash, + entry.commitment, entry.mined_block_hash)); + } + } + BOOST_REQUIRE(m_node.llmq_ctx->qsnapman->SeedSnapshotForBlock( + Consensus::LLMQType::LLMQ_TEST, base, quorum_snapshot)); + BOOST_REQUIRE(m_node.chain_helper->credit_pool_manager->SeedSnapshot(base, pool)); + BOOST_REQUIRE(m_node.chain_helper->ehf_manager->SeedSignals(base, signals)); + tx->Commit(); + } + BOOST_REQUIRE(m_node.evodb->CommitRootTransaction(EvoDbIdentity::SNAPSHOT, /*sync=*/true)); + { + LOCK(::cs_main); + m_node.dmnman->InvalidateListCacheForBlock(base->GetBlockHash()); + m_node.dmnman->InvalidateListCacheForBlock(historical_index->GetBlockHash()); + } + + CDeterministicMNList stored_list; + CDeterministicMNList stored_historical_list; + { + auto tx{m_node.evodb->BeginTransaction(EvoDbIdentity::SNAPSHOT)}; + stored_list = m_node.dmnman->GetListForBlock(base); + stored_historical_list = m_node.dmnman->GetListForBlock(historical_index); + } + m_node.dmnman->InvalidateListCacheForBlock(base->GetBlockHash()); + m_node.dmnman->InvalidateListCacheForBlock(historical_index->GetBlockHash()); + const auto subsequent_list{m_node.dmnman->GetListForBlock(base)}; + const auto subsequent_historical_list{m_node.dmnman->GetListForBlock(historical_index)}; + params_restorer.Get().DIP0003Height = old_dip3_height; + BOOST_CHECK(evo::CanonicalMNListHash(stored_list) == evo::CanonicalMNListHash(list)); + BOOST_CHECK(evo::CanonicalMNListHash(stored_historical_list) == evo::CanonicalMNListHash(historical_list)); + BOOST_CHECK(evo::CanonicalMNListHash(subsequent_list) == evo::CanonicalMNListHash(list)); + BOOST_CHECK(evo::CanonicalMNListHash(subsequent_historical_list) == evo::CanonicalMNListHash(historical_list)); + const auto [stored_commitment, stored_mined_hash]{ + m_node.llmq_ctx->quorum_block_processor->GetMinedCommitment( + nonrotated.back().commitment.llmqType, nonrotated.back().quorum_base_block_hash)}; + BOOST_CHECK_EQUAL(stored_mined_hash, nonrotated.back().mined_block_hash); + BOOST_CHECK_EQUAL(SerializeHash(stored_commitment), SerializeHash(nonrotated.back().commitment)); + { + LOCK(::cs_main); + const auto plain{m_node.llmq_ctx->quorum_block_processor->GetMinedCommitmentsUntilBlock( + Consensus::LLMQType::LLMQ_TEST, base, 2)}; + BOOST_REQUIRE_EQUAL(plain.size(), 2U); + BOOST_CHECK_EQUAL(plain[0]->nHeight, 72); + BOOST_CHECK_EQUAL(plain[1]->nHeight, 48); + const auto indexed{m_node.llmq_ctx->quorum_block_processor->GetLastMinedCommitmentsPerQuorumIndexUntilBlock( + Consensus::LLMQType::LLMQ_TEST_DIP0024, base, 0)}; + BOOST_REQUIRE_EQUAL(indexed.size(), 2U); + BOOST_CHECK_EQUAL(indexed[0]->nHeight, 72); + BOOST_CHECK_EQUAL(indexed[1]->nHeight, 73); + + CBlock first_post_base_block; + uint256 quorum_root; + BlockValidationState state; + BOOST_CHECK_MESSAGE(CalcCbTxMerkleRootQuorums(first_post_base_block, base, + *m_node.llmq_ctx->quorum_block_processor, quorum_root, state), + state.ToString()); + } + const auto stored_snapshot{m_node.llmq_ctx->qsnapman->GetSnapshotForBlock( + Consensus::LLMQType::LLMQ_TEST, base)}; + BOOST_REQUIRE(stored_snapshot.has_value()); + BOOST_CHECK(stored_snapshot->activeQuorumMembers == quorum_snapshot.activeQuorumMembers); + + CCreditPool stored_pool; + AbstractEHFManager::Signals stored_signals; + BOOST_REQUIRE(m_node.evodb->Read(std::make_pair(std::string{"cpm_S"}, base->GetBlockHash()), stored_pool)); + BOOST_REQUIRE(m_node.evodb->Read(std::make_pair(std::string{"mnhf_s2"}, base->GetBlockHash()), stored_signals)); + BOOST_CHECK_EQUAL(stored_pool.locked, pool.locked); + BOOST_CHECK(stored_signals == signals); +} + +BOOST_FIXTURE_TEST_CASE(snapshot_seed_rollback_does_not_publish_caches, TestChain100Setup) +{ + const CBlockIndex* base{WITH_LOCK(::cs_main, return m_node.chainman->ActiveTip())}; + BOOST_REQUIRE(base != nullptr); + const auto seeded_list{MNList(base->GetBlockHash(), base->nHeight, false)}; + CCreditPool seeded_pool; + seeded_pool.locked = 123; + AbstractEHFManager::Signals seeded_signals{{2, base->nHeight}}; + const llmq::CQuorumSnapshot seeded_quorum{{true, false, true}, SnapshotSkipMode::MODE_NO_SKIPPING, {}}; + + { + auto tx{m_node.evodb->BeginTransaction(EvoDbIdentity::SNAPSHOT)}; + BOOST_REQUIRE(m_node.dmnman->SeedListForBlock(seeded_list)); + BOOST_REQUIRE(m_node.chain_helper->credit_pool_manager->SeedSnapshot(base, seeded_pool)); + BOOST_REQUIRE(m_node.chain_helper->ehf_manager->SeedSignals(base, seeded_signals)); + BOOST_REQUIRE(m_node.llmq_ctx->qsnapman->SeedSnapshotForBlock( + Consensus::LLMQType::LLMQ_TEST, base, seeded_quorum)); + BOOST_CHECK(!WITH_LOCK(::cs_main, return m_node.llmq_ctx->quorum_block_processor->SeedMinedCommitment( + Consensus::LLMQType::LLMQ_TEST, H(200), + Commitment(Consensus::LLMQType::LLMQ_TEST, 1, 2, false).commitment, H(201)))); + // Destruction without Commit() rolls the complete scoped transaction back. + } + + CDeterministicMNList db_list; + CCreditPool db_pool; + AbstractEHFManager::Signals db_signals; + const auto quorum_hash{SerializeHash(std::make_pair(Consensus::LLMQType::LLMQ_TEST, base->GetBlockHash()))}; + llmq::CQuorumSnapshot db_quorum; + BOOST_CHECK(!m_node.evodb->Read(std::make_pair(std::string{"dmn_S3"}, base->GetBlockHash()), db_list)); + BOOST_CHECK(!m_node.evodb->Read(std::make_pair(std::string{"cpm_S"}, base->GetBlockHash()), db_pool)); + BOOST_CHECK(!m_node.evodb->Read(std::make_pair(std::string{"mnhf_s2"}, base->GetBlockHash()), db_signals)); + BOOST_CHECK(!m_node.evodb->Read(std::make_pair(std::string_view{"llmq_S"}, quorum_hash), db_quorum)); + + { + ConsensusParamsRestorer params_restorer{Params().GetConsensus()}; + params_restorer.Get().DIP0003Height = 1; + params_restorer.Get().V20Height = 1; + BOOST_CHECK_EQUAL(m_node.dmnman->GetListForBlock(base).GetCounts().total(), 0U); + BOOST_CHECK_EQUAL(m_node.chain_helper->credit_pool_manager->GetCreditPool(base).locked, 0); + BOOST_CHECK(m_node.chain_helper->ehf_manager->GetSignalsStage(base).empty()); + } + BOOST_CHECK(!m_node.llmq_ctx->qsnapman->GetSnapshotForBlock( + Consensus::LLMQType::LLMQ_TEST, base).has_value()); +} + +BOOST_FIXTURE_TEST_CASE(quorum_members_reconstruct_from_seeded_state_only, SnapshotActivationChainSetup) +{ + const CBlockIndex* tip{WITH_LOCK(::cs_main, return m_node.chainman->ActiveTip())}; + BOOST_REQUIRE(tip != nullptr); + ConsensusParamsRestorer global_restorer{Params().GetConsensus()}; + ConsensusParamsRestorer chain_restorer{m_node.chainman->GetConsensus()}; + auto& global_consensus{global_restorer.Get()}; + auto& consensus{chain_restorer.Get()}; + auto plain{evo::SnapshotLLMQParams(Consensus::LLMQType::LLMQ_TEST)}; + auto rotated{evo::SnapshotLLMQParams(Consensus::LLMQType::LLMQ_TEST_DIP0024)}; + plain.dkgInterval = 12; + plain.dkgMiningWindowStart = 1; + plain.dkgMiningWindowEnd = 3; + rotated.dkgInterval = 12; + consensus.llmqs = {plain, rotated}; + global_consensus.llmqs = consensus.llmqs; + + const CBlockIndex* quorum{tip->GetAncestor(96)}; + BOOST_REQUIRE(quorum != nullptr); + std::map lists; + const auto make_list = [&](const CBlockIndex* work) { + CDeterministicMNList list{work->GetBlockHash(), work->nHeight, 100}; + for (uint8_t i{0}; i < 12; ++i) { + list.AddMN(MN(20 + i, 20 + i, MnType::Regular, ProTxVersion::LegacyBLS, 20 + i), false); + } + return list; + }; + const CBlockIndex* plain_work{quorum->GetAncestor(88)}; + lists.emplace(plain_work, make_list(plain_work)); + std::vector rotated_cycles; + for (const int height : {96, 84, 72, 60}) { + const CBlockIndex* cycle{tip->GetAncestor(height)}; + const CBlockIndex* work{tip->GetAncestor(height - llmq::WORK_DIFF_DEPTH)}; + rotated_cycles.emplace_back(cycle); + lists.try_emplace(work, make_list(work)); + } + for (const auto& [work, list] : lists) m_node.dmnman->SetListForBlockForTesting(list); + BOOST_REQUIRE(m_node.chainman->IsQuorumTypeEnabled(plain.type, quorum->pprev)); + BOOST_REQUIRE(m_node.chainman->IsQuorumTypeEnabled(rotated.type, quorum->pprev)); + BOOST_REQUIRE_EQUAL(m_node.dmnman->GetListForBlock(plain_work).GetCounts().enabled(), 12U); + const llmq::CQuorumSnapshot empty_snapshot{std::vector(12, false), + SnapshotSkipMode::MODE_NO_SKIPPING, {}}; + for (size_t i{1}; i < rotated_cycles.size(); ++i) { + m_node.llmq_ctx->qsnapman->StoreSnapshotForBlock(rotated.type, rotated_cycles[i], empty_snapshot); + } + + // Derive the oracle through a separate manager, cache, and EvoDB. The + // manager under test is seeded only after these expected sets exist. + CEvoDB expected_db{util::DbWrapperParams{.path = m_args.GetDataDirBase() / "evo_snapshot_oracle", + .memory = true, .wipe = true}}; + CMasternodeMetaMan expected_meta; + CDeterministicMNManager expected_dmnman{expected_db, expected_meta}; + llmq::CQuorumSnapshotManager expected_qsnapman{expected_db}; + { + auto tx{expected_db.BeginTransaction(EvoDbIdentity::NORMAL)}; + for (const auto& [_, list] : lists) BOOST_REQUIRE(expected_dmnman.SeedListForBlock(list)); + for (size_t i{1}; i < rotated_cycles.size(); ++i) { + expected_qsnapman.StoreSnapshotForBlock(rotated.type, rotated_cycles[i], empty_snapshot); + } + tx->Commit(); + } + const auto plain_expected{llmq::utils::GetAllQuorumMembers( + plain.type, {expected_dmnman, expected_qsnapman, *m_node.chainman, quorum}, true)}; + const auto rotated_expected{llmq::utils::GetAllQuorumMembers( + rotated.type, {expected_dmnman, expected_qsnapman, *m_node.chainman, quorum}, true)}; + BOOST_REQUIRE(!plain_expected.empty()); + BOOST_REQUIRE(!rotated_expected.empty()); + + CBLSSecretKey quorum_key; + quorum_key.MakeNewKey(); + llmq::CFinalCommitment seeded_commitment{plain, quorum->GetBlockHash()}; + seeded_commitment.nVersion = llmq::CFinalCommitment::BASIC_BLS_NON_INDEXED_QUORUM_VERSION; + seeded_commitment.quorumPublicKey = quorum_key.GetPublicKey(); + seeded_commitment.quorumVvecHash = H(201); + const CBlockIndex* mined_index{tip->GetAncestor(98)}; + + { + auto tx{m_node.evodb->BeginTransaction(EvoDbIdentity::SNAPSHOT)}; + for (const auto& [work, list] : lists) BOOST_REQUIRE(m_node.dmnman->SeedListForBlock(list)); + BOOST_REQUIRE(m_node.llmq_ctx->qsnapman->SeedQuorumModifier( + plain.type, plain_work->GetBlockHash(), + llmq::utils::GetQuorumHashModifier(plain, consensus, quorum))); + for (const auto* cycle : rotated_cycles) { + const CBlockIndex* work{cycle->GetAncestor(cycle->nHeight - llmq::WORK_DIFF_DEPTH)}; + BOOST_REQUIRE(m_node.llmq_ctx->qsnapman->SeedQuorumModifier( + rotated.type, work->GetBlockHash(), + llmq::utils::GetQuorumHashModifier(rotated, consensus, cycle))); + } + for (size_t i{1}; i < rotated_cycles.size(); ++i) { + BOOST_REQUIRE(m_node.llmq_ctx->qsnapman->SeedSnapshotForBlock( + rotated.type, rotated_cycles[i], empty_snapshot)); + } + BOOST_REQUIRE(WITH_LOCK(::cs_main, return m_node.llmq_ctx->quorum_block_processor->SeedMinedCommitment( + plain.type, quorum->GetBlockHash(), seeded_commitment, mined_index->GetBlockHash());)); + tx->Commit(); + } + + std::map saved_status; + { + LOCK(::cs_main); + for (const auto& [work, _] : lists) { + auto* mutable_work{const_cast(work)}; + saved_status.emplace(mutable_work, mutable_work->nStatus); + mutable_work->nStatus &= ~BLOCK_HAVE_DATA; + m_node.dmnman->InvalidateListCacheForBlock(work->GetBlockHash()); + } + for (size_t i{1}; i < rotated_cycles.size(); ++i) { + m_node.llmq_ctx->qsnapman->InvalidateSnapshotCacheForBlock(rotated.type, + rotated_cycles[i]->GetBlockHash()); + } + } + std::vector plain_seeded; + std::vector rotated_seeded; + std::vector scanned; + llmq::VerifyRecSigStatus recovered_sig_status{llmq::VerifyRecSigStatus::NoQuorum}; + { + auto tx{m_node.evodb->BeginTransaction(EvoDbIdentity::SNAPSHOT)}; + plain_seeded = llmq::utils::GetAllQuorumMembers( + plain.type, {*m_node.dmnman, *m_node.llmq_ctx->qsnapman, *m_node.chainman, quorum}, true); + rotated_seeded = llmq::utils::GetAllQuorumMembers( + rotated.type, {*m_node.dmnman, *m_node.llmq_ctx->qsnapman, *m_node.chainman, quorum}, true); + scanned = m_node.llmq_ctx->qman->ScanQuorums(plain.type, tip, 1); + const uint256 id{H(202)}; + const uint256 msg_hash{H(203)}; + const llmq::SignHash sign_hash{plain.type, quorum->GetBlockHash(), id, msg_hash}; + recovered_sig_status = llmq::VerifyRecoveredSig( + plain.type, *m_node.llmq_ctx->qman, tip, id, msg_hash, + quorum_key.Sign(sign_hash.Get(), /*specificLegacyScheme=*/false)); + } + const auto hashes = [](const auto& members) { + std::vector result; + for (const auto& member : members) result.emplace_back(member->proTxHash); + return result; + }; + BOOST_CHECK(hashes(plain_seeded) == hashes(plain_expected)); + BOOST_CHECK(hashes(rotated_seeded) == hashes(rotated_expected)); + BOOST_REQUIRE_EQUAL(scanned.size(), 1U); + BOOST_CHECK(hashes(scanned[0]->members) == hashes(plain_expected)); + BOOST_CHECK(recovered_sig_status == llmq::VerifyRecSigStatus::Valid); + + // Prove reconstruction fails closed instead of falling through to the + // ordinary diff chain when one required seeded full list is absent. + size_t forbidden_fallbacks{0}; + m_node.dmnman->SetListSnapshotMissHookForTesting([&](const CBlockIndex* index) { + ++forbidden_fallbacks; + throw std::logic_error(strprintf("forbidden NORMAL MN-list fallback at height %d", index->nHeight)); + }); + { + auto tx{m_node.evodb->BeginTransaction(EvoDbIdentity::SNAPSHOT)}; + m_node.evodb->Erase(std::make_pair(std::string{"dmn_S3"}, plain_work->GetBlockHash())); + m_node.dmnman->InvalidateListCacheForBlock(plain_work->GetBlockHash()); + BOOST_CHECK_THROW(llmq::utils::GetAllQuorumMembers( + plain.type, {*m_node.dmnman, *m_node.llmq_ctx->qsnapman, *m_node.chainman, quorum}, true), + std::logic_error); + } + m_node.dmnman->SetListSnapshotMissHookForTesting({}); + BOOST_CHECK_EQUAL(forbidden_fallbacks, 1U); + + { + LOCK(::cs_main); + for (const auto& [work, status] : saved_status) work->nStatus = status; + } + { + auto tx{m_node.evodb->BeginTransaction(EvoDbIdentity::SNAPSHOT)}; + const auto modifier_key{std::make_tuple(std::string_view{"llmq_M3"}, plain.type, + plain_work->GetBlockHash())}; + m_node.evodb->Erase(modifier_key); + m_node.evodb->Write(modifier_key, H(254)); + BOOST_CHECK_THROW(llmq::utils::GetAllQuorumMembers( + plain.type, {*m_node.dmnman, *m_node.llmq_ctx->qsnapman, *m_node.chainman, quorum}, true), + evo::SnapshotStateMismatchError); + } +} + +BOOST_FIXTURE_TEST_CASE(chain_validation_pre_dip3_matrix, TestChain100Setup) +{ + const CBlockIndex* base{WITH_LOCK(::cs_main, return m_node.chainman->ActiveTip())}; + BOOST_REQUIRE(base != nullptr); + evo::EvoSnapshot snapshot; + snapshot.base_block_hash = base->GetBlockHash(); + snapshot.mn_list = CDeterministicMNList{base->GetBlockHash(), base->nHeight, 0}; + std::string error; + BOOST_CHECK(WITH_LOCK(::cs_main, + return evo::ValidateEvoSnapshotAgainstChain(snapshot, *m_node.chainman, base, error))); + + auto wrong_base{snapshot}; + wrong_base.base_block_hash = H(99); + BOOST_CHECK(!WITH_LOCK(::cs_main, + return evo::ValidateEvoSnapshotAgainstChain(wrong_base, *m_node.chainman, base, error))); + + auto nonempty{snapshot}; + nonempty.credit_pool.locked = 1; + BOOST_CHECK(!WITH_LOCK(::cs_main, + return evo::ValidateEvoSnapshotAgainstChain(nonempty, *m_node.chainman, base, error))); + + // Well-formed for the context-free codec, but nothing can be registered before DIP3. + auto populated{snapshot}; + populated.mn_list = CDeterministicMNList{base->GetBlockHash(), base->nHeight, 1}; + populated.mn_list.AddMN(MN(0, 1, MnType::Regular, ProTxVersion::LegacyBLS, 1), /*fBumpTotalCount=*/false); + BOOST_CHECK( + !WITH_LOCK(::cs_main, return evo::ValidateEvoSnapshotAgainstChain(populated, *m_node.chainman, base, error))); + BOOST_CHECK_EQUAL(error, "nonempty pre-DIP3 evo snapshot"); + auto counted{snapshot}; + counted.mn_list = CDeterministicMNList{base->GetBlockHash(), base->nHeight, 1}; + BOOST_CHECK(!WITH_LOCK(::cs_main, return evo::ValidateEvoSnapshotAgainstChain(counted, *m_node.chainman, base, error))); + BOOST_CHECK_EQUAL(error, "nonempty pre-DIP3 evo snapshot"); + + ConsensusParamsRestorer params_restorer{m_node.chainman->GetConsensus()}; + auto& mutable_consensus{params_restorer.Get()}; + mutable_consensus.DIP0003Height = 1; + mutable_consensus.V19Height = 1; + mutable_consensus.llmqs = {evo::SnapshotLLMQParams(Consensus::LLMQType::LLMQ_TEST)}; + + evo::EvoSnapshot active{snapshot}; + evo::QuorumSnapshotData quorum_data; + quorum_data.llmq_type = Consensus::LLMQType::LLMQ_TEST; + const auto& params{mutable_consensus.llmqs.front()}; + const auto make_commitment = [&](int quorum_height, int mined_height) { + evo::MinedQuorumCommitment entry; + const CBlockIndex* quorum{base->GetAncestor(quorum_height)}; + entry.quorum_base_block_hash = quorum->GetBlockHash(); + entry.work_block_hash = entry.quorum_base_block_hash; + entry.mined_block_hash = base->GetAncestor(mined_height)->GetBlockHash(); + entry.commitment.nVersion = llmq::CFinalCommitment::BASIC_BLS_NON_INDEXED_QUORUM_VERSION; + entry.commitment.llmqType = params.type; + entry.commitment.quorumHash = entry.quorum_base_block_hash; + entry.commitment.signers.resize(params.size); + entry.commitment.validMembers.resize(params.size); + return entry; + }; + quorum_data.active_commitments = {make_commitment(72, 82), make_commitment(48, 58)}; + quorum_data.safety_commitments = {make_commitment(24, 34)}; + std::sort(quorum_data.active_commitments.begin(), quorum_data.active_commitments.end(), + [](const auto& a, const auto& b) { + return std::tie(a.quorum_base_block_hash, a.mined_block_hash) < + std::tie(b.quorum_base_block_hash, b.mined_block_hash); + }); + active.quorums = {quorum_data}; + CDeterministicMNList previous{active.mn_list}; + uint256 previous_hash{active.base_block_hash}; + for (const int height : {72, 48, 24}) { + const CBlockIndex* work{base->GetAncestor(height)}; + CDeterministicMNList list{work->GetBlockHash(), height, 0}; + active.historical_mn_list_diffs.push_back({previous_hash, work->GetBlockHash(), height, 0, + evo::CanonicalMNListHash(list), previous.BuildDiff(list)}); + active.quorum_modifiers.push_back({params.type, work->GetBlockHash(), + llmq::utils::GetQuorumHashModifier(params, mutable_consensus, work)}); + previous_hash = work->GetBlockHash(); + previous = std::move(list); + } + std::sort(active.quorum_modifiers.begin(), active.quorum_modifiers.end(), [](const auto& a, const auto& b) { + return std::tie(a.llmq_type, a.work_block_hash) < std::tie(b.llmq_type, b.work_block_hash); + }); + const bool active_valid{WITH_LOCK(::cs_main, + return evo::ValidateEvoSnapshotAgainstChain(active, *m_node.chainman, base, error))}; + BOOST_CHECK_MESSAGE(active_valid, error); + + auto wrong_counts{active}; + wrong_counts.quorums[0].safety_commitments.clear(); + BOOST_CHECK(!WITH_LOCK(::cs_main, + return evo::ValidateEvoSnapshotAgainstChain(wrong_counts, *m_node.chainman, base, error))); + + auto non_ancestor{active}; + non_ancestor.quorums[0].active_commitments[0].quorum_base_block_hash = H(99); + non_ancestor.quorums[0].active_commitments[0].commitment.quorumHash = H(99); + BOOST_CHECK(!WITH_LOCK(::cs_main, + return evo::ValidateEvoSnapshotAgainstChain(non_ancestor, *m_node.chainman, base, error))); + + // A young chain carries fewer commitments than the parameter horizon. A + // coherent snapshot with a single active commitment, its historical diff, + // and its modifier must pass both validation layers: parameter counts are + // maxima, and completeness is established by the completion-time CbTx + // quorum merkle root, not by per-type count equality. + evo::EvoSnapshot partial{snapshot}; + evo::QuorumSnapshotData partial_data; + partial_data.llmq_type = Consensus::LLMQType::LLMQ_TEST; + partial_data.active_commitments = {make_commitment(72, 82)}; + partial.quorums = {partial_data}; + { + const CBlockIndex* work{base->GetAncestor(72)}; + CDeterministicMNList list{work->GetBlockHash(), 72, 0}; + partial.historical_mn_list_diffs.push_back({partial.base_block_hash, work->GetBlockHash(), 72, 0, + evo::CanonicalMNListHash(list), partial.mn_list.BuildDiff(list)}); + partial.quorum_modifiers.push_back({params.type, work->GetBlockHash(), + llmq::utils::GetQuorumHashModifier(params, mutable_consensus, work)}); + } + BOOST_CHECK_NO_THROW(partial.Validate()); + const bool partial_valid{WITH_LOCK(::cs_main, + return evo::ValidateEvoSnapshotAgainstChain(partial, *m_node.chainman, base, error))}; + BOOST_CHECK_MESSAGE(partial_valid, error); +} + +BOOST_FIXTURE_TEST_CASE(builder_emits_available_history_on_young_chains, SnapshotActivationChainSetup) +{ + // No masternodes exist and no DKGs have run on this fixture chain, so every + // enabled LLMQ type has zero mined commitments and zero rotation cycles. + // dumptxoutset-grade building must succeed on such a chain and emit the + // history that exists rather than failing the parameter-derived horizon. + const CBlockIndex* base{WITH_LOCK(::cs_main, return m_node.chainman->ActiveTip())}; + BOOST_REQUIRE(base != nullptr); + evo::EvoSnapshot snapshot; + std::string error; + const bool built{WITH_LOCK(::cs_main, + return evo::BuildEvoSnapshot(Params(), *m_node.chainman, *m_node.dmnman, + *m_node.llmq_ctx->quorum_block_processor, *m_node.llmq_ctx->qsnapman, + *m_node.chain_helper->credit_pool_manager, *m_node.chain_helper->ehf_manager, + base, snapshot, error))}; + BOOST_REQUIRE_MESSAGE(built, error); + for (const auto& data : snapshot.quorums) { + BOOST_CHECK(data.active_commitments.empty()); + BOOST_CHECK(data.safety_commitments.empty()); + BOOST_CHECK(data.rotation_snapshots.empty()); + } + BOOST_CHECK_NO_THROW(snapshot.Validate()); + const bool valid{WITH_LOCK(::cs_main, + return evo::ValidateEvoSnapshotAgainstChain(snapshot, *m_node.chainman, base, error))}; + BOOST_CHECK_MESSAGE(valid, error); +} + +BOOST_FIXTURE_TEST_CASE(rotation_bitset_matches_historical_work_list, TestChain100Setup) +{ + const CBlockIndex* base{WITH_LOCK(::cs_main, return m_node.chainman->ActiveTip())}; + BOOST_REQUIRE(base != nullptr); + ConsensusParamsRestorer params_restorer{m_node.chainman->GetConsensus()}; + auto& consensus{params_restorer.Get()}; + auto params{evo::SnapshotLLMQParams(Consensus::LLMQType::LLMQ_TEST_DIP0024)}; + params.dkgInterval = 12; + params.dkgMiningWindowStart = 2; + params.dkgMiningWindowEnd = 6; + consensus.llmqs = {params}; + consensus.DIP0003Height = 1; + consensus.V19Height = 1; + + evo::EvoSnapshot snapshot; + snapshot.base_block_hash = base->GetBlockHash(); + snapshot.mn_list = CDeterministicMNList{base->GetBlockHash(), base->nHeight, 100}; + evo::QuorumSnapshotData data; + data.llmq_type = params.type; + data.rotation_enabled = true; + const auto commitment = [&](int quorum_height, int mined_height, int16_t quorum_index) { + evo::MinedQuorumCommitment entry; + const CBlockIndex* quorum{base->GetAncestor(quorum_height)}; + const CBlockIndex* cycle{quorum->GetAncestor(quorum->nHeight - quorum->nHeight % params.dkgInterval)}; + entry.quorum_base_block_hash = quorum->GetBlockHash(); + entry.work_block_hash = cycle->GetAncestor(cycle->nHeight - llmq::WORK_DIFF_DEPTH)->GetBlockHash(); + entry.mined_block_hash = base->GetAncestor(mined_height)->GetBlockHash(); + entry.commitment.nVersion = llmq::CFinalCommitment::BASIC_BLS_INDEXED_QUORUM_VERSION; + entry.commitment.llmqType = params.type; + entry.commitment.quorumHash = entry.quorum_base_block_hash; + entry.commitment.quorumIndex = quorum_index; + entry.commitment.signers.resize(params.size); + entry.commitment.validMembers.resize(params.size); + return entry; + }; + data.active_commitments = {commitment(84, 86, 0), commitment(85, 87, 1)}; + data.safety_commitments = {commitment(72, 74, 0), commitment(73, 75, 1)}; + + std::map required_work; + for (const auto& required : evo::EvoSnapshotReconstructionHeights(base->nHeight, {params})) { + const int cycle_height{required.quorum_height}; + const int work_height{required.work_height}; + const CBlockIndex* cycle{base->GetAncestor(cycle_height)}; + const CBlockIndex* work{base->GetAncestor(work_height)}; + BOOST_REQUIRE(cycle != nullptr); + BOOST_REQUIRE(work != nullptr); + const size_t population{static_cast(params.size + 3)}; + data.rotation_snapshots.push_back({cycle->GetBlockHash(), work->GetBlockHash(), + llmq::CQuorumSnapshot{std::vector(population, true), SnapshotSkipMode::MODE_NO_SKIPPING, {}}}); + required_work.emplace(work->GetBlockHash(), work); + } + for (const auto* commitments : {&data.active_commitments, &data.safety_commitments}) { + for (const auto& entry : *commitments) { + const CBlockIndex* work{WITH_LOCK(::cs_main, + return m_node.chainman->m_blockman.LookupBlockIndex(entry.work_block_hash);)}; + BOOST_REQUIRE(work != nullptr); + required_work.emplace(entry.work_block_hash, work); + } + } + const auto commitment_less = [](const auto& a, const auto& b) { + return std::tie(a.quorum_base_block_hash, a.mined_block_hash) < + std::tie(b.quorum_base_block_hash, b.mined_block_hash); + }; + std::sort(data.active_commitments.begin(), data.active_commitments.end(), commitment_less); + std::sort(data.safety_commitments.begin(), data.safety_commitments.end(), commitment_less); + std::sort(data.rotation_snapshots.begin(), data.rotation_snapshots.end(), [](const auto& a, const auto& b) { + return std::tie(a.cycle_base_block_hash, a.work_block_hash) < + std::tie(b.cycle_base_block_hash, b.work_block_hash); + }); + snapshot.quorums = {std::move(data)}; + + std::vector ordered_work; + for (const auto& [_, work] : required_work) ordered_work.emplace_back(work); + std::sort(ordered_work.begin(), ordered_work.end(), [](const auto* a, const auto* b) { return a->nHeight > b->nHeight; }); + CDeterministicMNList previous{snapshot.mn_list}; + uint256 previous_hash{snapshot.base_block_hash}; + for (const auto* work : ordered_work) { + CDeterministicMNList work_list{work->GetBlockHash(), work->nHeight, 100}; + for (uint8_t i{0}; i < params.size + 3; ++i) { + work_list.AddMN(MN(20 + i, 20 + i, MnType::Regular, ProTxVersion::LegacyBLS, 20 + i), false); + } + snapshot.historical_mn_list_diffs.push_back( + {previous_hash, work->GetBlockHash(), work->nHeight, work_list.GetTotalRegisteredCount(), + evo::CanonicalMNListHash(work_list), previous.BuildDiff(work_list)}); + previous_hash = work->GetBlockHash(); + previous = std::move(work_list); + } + std::map modifier_cycles; + for (const auto* commitments : {&snapshot.quorums[0].active_commitments, &snapshot.quorums[0].safety_commitments}) { + for (const auto& entry : *commitments) { + const CBlockIndex* quorum_index{WITH_LOCK(::cs_main, + return m_node.chainman->m_blockman.LookupBlockIndex(entry.quorum_base_block_hash);)}; + const CBlockIndex* cycle{quorum_index->GetAncestor( + quorum_index->nHeight - quorum_index->nHeight % params.dkgInterval)}; + modifier_cycles.emplace(entry.work_block_hash, cycle); + } + } + for (const auto& entry : snapshot.quorums[0].rotation_snapshots) { + modifier_cycles.emplace(entry.work_block_hash, WITH_LOCK(::cs_main, + return m_node.chainman->m_blockman.LookupBlockIndex(entry.cycle_base_block_hash);)); + } + for (const auto& [work_hash, cycle] : modifier_cycles) { + snapshot.quorum_modifiers.push_back({params.type, work_hash, + llmq::utils::GetQuorumHashModifier(params, consensus, cycle)}); + } + + std::string error; + const bool valid{WITH_LOCK(::cs_main, + return evo::ValidateEvoSnapshotAgainstChain(snapshot, *m_node.chainman, base, error))}; + BOOST_CHECK_MESSAGE(valid, error); + auto short_bitset{snapshot}; + short_bitset.quorums[0].rotation_snapshots[0].snapshot.activeQuorumMembers.pop_back(); + BOOST_CHECK(!WITH_LOCK(::cs_main, + return evo::ValidateEvoSnapshotAgainstChain(short_bitset, *m_node.chainman, base, error))); + auto bad_modifier{snapshot}; + bad_modifier.quorum_modifiers[0].modifier.begin()[0] ^= 1; + BOOST_CHECK(!WITH_LOCK(::cs_main, + return evo::ValidateEvoSnapshotAgainstChain(bad_modifier, *m_node.chainman, base, error))); +} + BOOST_AUTO_TEST_CASE(reconstruction_horizon_height_enumeration) { const auto rotated{evo::SnapshotLLMQParams(Consensus::LLMQType::LLMQ_TEST_DIP0024)}; diff --git a/src/test/util/setup_common.cpp b/src/test/util/setup_common.cpp index fe1fe185d637..4a7b7cd3c4a3 100644 --- a/src/test/util/setup_common.cpp +++ b/src/test/util/setup_common.cpp @@ -494,6 +494,8 @@ TestChainSetup::TestChainSetup( { 98, uint256S("0x150e127929d578d8129b77a6cb7e2e343a1379aa3feaaa9cce59e0a645756a81") }, /*TestChain100Setup=*/ { 100, uint256S("0x6ffb83129c19ebdf1ae3771be6a67fe34b35f4c956326b9ba152fac1649f65ae") }, + /*SnapshotActivationChainSetup=*/ + { 102, uint256S("0x37876f3493ac152f9a0bdf0049d85969fe3ba82745733a81c8e1afeefa16ab3b") }, /*TestChainV19BeforeActivationSetup=*/ { 103, uint256S("0x13adad9565d0ca558f5675c50e3828f4354d26b64de044ebc88686056f30faab") }, /*TestChainDIP3BeforeActivationSetup=*/