diff --git a/src/llmq/quorums.cpp b/src/llmq/quorums.cpp index 3c467aca2530..856b2ee4a33a 100644 --- a/src/llmq/quorums.cpp +++ b/src/llmq/quorums.cpp @@ -25,7 +25,9 @@ #include #include #include +#include +#include #include namespace llmq @@ -207,6 +209,82 @@ bool CQuorum::ReadContributions(const CDBWrapper& db) return true; } +size_t CQuorumManager::ComputeAncestorCacheMaxSize() +{ + size_t max_size = 0; + for (const auto& llmq : Params().GetConsensus().llmqs) { + size_t size = llmq.max_cycles(llmq.keepOldConnections) * (llmq.dkgMiningWindowEnd - llmq.dkgMiningWindowStart) + 128; + max_size = std::max(max_size, size); + } + // Clamp to reasonable bounds + return std::clamp(max_size, size_t(512), size_t(4096)); +} + +CQuorumManager::ActiveChainView CQuorumManager::GetActiveChainView() const +{ + LOCK(cs_active_chain_view); + return m_active_chain_view; +} + +const CBlockIndex* CQuorumManager::FindAncestorFast(const ActiveChainView& view, int target_height) const +{ + if (target_height < 0 || target_height > view.height || !view.tip) { + return nullptr; + } + LOCK(cs_ancestor_cache); + if (m_lru_tip_hash != view.hash) { + // Distinguish extension vs reorg (or skipped updates). + // If the previous tip hash is an ancestor of the current tip at the recorded height, + // this is an extension (possibly by multiple blocks) and the cache remains valid. + // Otherwise, clear the cache to avoid cross-fork pollution. + bool is_extension = false; + if (m_lru_tip_height >= 0) { + const CBlockIndex* old_tip_as_ancestor = view.tip->GetAncestor(m_lru_tip_height); + if (old_tip_as_ancestor && old_tip_as_ancestor->GetBlockHash() == m_lru_tip_hash) { + is_extension = true; + } + } + if (!is_extension) { + m_ancestor_lru.clear(); + } + m_lru_tip_hash = view.hash; + m_lru_tip_height = view.height; + } + const CBlockIndex* out; + if (m_ancestor_lru.get(target_height, out)) { + return out; + } + out = view.tip->GetAncestor(target_height); + if (out) { + m_ancestor_lru.insert(target_height, out); + } + return out; +} + +void CQuorumManager::ChainViewSubscriber::UpdatedBlockTip(const CBlockIndex* pindexNew, const CBlockIndex* pindexFork, bool fInitialDownload) +{ + if (!pindexNew) return; + + // Update snapshot + { + LOCK(m_qman.cs_active_chain_view); + m_qman.m_active_chain_view.tip = pindexNew; + m_qman.m_active_chain_view.height = pindexNew->nHeight; + m_qman.m_active_chain_view.hash = pindexNew->GetBlockHash(); + } + + // Handle ancestor cache: clear on reorg, keep on extension + { + LOCK(m_qman.cs_ancestor_cache); + if (pindexFork != pindexNew->pprev) { + // Reorg detected: clear cache + m_qman.m_ancestor_lru.clear(); + } + m_qman.m_lru_tip_hash = pindexNew->GetBlockHash(); + m_qman.m_lru_tip_height = pindexNew->nHeight; + } +} + CQuorumManager::CQuorumManager(CBLSWorker& _blsWorker, CChainState& chainstate, CDeterministicMNManager& dmnman, CDKGSessionManager& _dkgManager, CEvoDB& _evoDb, CQuorumBlockProcessor& _quorumBlockProcessor, CQuorumSnapshotManager& qsnapman, @@ -222,16 +300,36 @@ CQuorumManager::CQuorumManager(CBLSWorker& _blsWorker, CChainState& chainstate, m_qsnapman{qsnapman}, m_mn_activeman{mn_activeman}, m_mn_sync{mn_sync}, - m_sporkman{sporkman} + m_sporkman{sporkman}, + m_ancestor_lru{ComputeAncestorCacheMaxSize()} { utils::InitQuorumsCache(mapQuorumsCache, false); quorumThreadInterrupt.reset(); MigrateOldQuorumDB(_evoDb); + + // Initialize snapshot from current tip (under cs_main for bootstrap) + { + LOCK(::cs_main); + const CBlockIndex* tip = m_chainstate.m_chain.Tip(); + if (tip) { + LOCK(cs_active_chain_view); + m_active_chain_view.tip = tip; + m_active_chain_view.height = tip->nHeight; + m_active_chain_view.hash = tip->GetBlockHash(); + } + } + + // Register ValidationInterface subscriber + m_chain_view_subscriber = std::make_unique(*this); + RegisterValidationInterface(m_chain_view_subscriber.get()); } CQuorumManager::~CQuorumManager() { Stop(); + if (m_chain_view_subscriber) { + UnregisterValidationInterface(m_chain_view_subscriber.get()); + } } void CQuorumManager::Start() @@ -523,7 +621,17 @@ bool CQuorumManager::RequestQuorumData(CNode* pfrom, CConnman& connman, const CQ std::vector CQuorumManager::ScanQuorums(Consensus::LLMQType llmqType, size_t nCountRequested) const { - const CBlockIndex* pindex = WITH_LOCK(::cs_main, return m_chainstate.m_chain.Tip()); + auto view = GetActiveChainView(); + const CBlockIndex* pindex = view.tip; + if (pindex == nullptr) { + pindex = WITH_LOCK(::cs_main, return m_chainstate.m_chain.Tip()); + } else { + // If the cached view lags behind the active chain, prefer the up-to-date tip for correctness. + const int active_height = WITH_LOCK(::cs_main, return m_chainstate.m_chain.Height()); + if (view.height < active_height) { + pindex = WITH_LOCK(::cs_main, return m_chainstate.m_chain.Tip()); + } + } return ScanQuorums(llmqType, pindex, nCountRequested); } @@ -547,10 +655,17 @@ std::vector CQuorumManager::ScanQuorums(Consensus::LLMQType llmqTyp // too early for this cycle, use the previous one // bail out if it's below genesis block if (quorumCycleMiningEndHeight < llmq_params_opt->dkgInterval) return {}; - pindexStore = pindexStart->GetAncestor(quorumCycleMiningEndHeight - llmq_params_opt->dkgInterval); + // IMPORTANT: compute ancestors strictly relative to pindexStart's chain context + // to avoid cross-fork/cross-tip contamination from global cached views. + const CBlockIndex* ancestor = pindexStart->GetAncestor(quorumCycleMiningEndHeight - llmq_params_opt->dkgInterval); + if (ancestor == nullptr) return {}; + pindexStore = gsl::not_null{ancestor}; } else if (pindexStart->nHeight > quorumCycleMiningEndHeight) { // we are past the mining phase of this cycle, use it - pindexStore = pindexStart->GetAncestor(quorumCycleMiningEndHeight); + // See note above: stay on pindexStart's chain + const CBlockIndex* ancestor = pindexStart->GetAncestor(quorumCycleMiningEndHeight); + if (ancestor == nullptr) return {}; + pindexStore = gsl::not_null{ancestor}; } // everything else is inside the mining phase of this cycle, no pindexStore adjustment needed @@ -1210,17 +1325,17 @@ CQuorumCPtr SelectQuorumForSigning(const Consensus::LLMQParams& llmq_params, con size_t poolSize = llmq_params.signingActiveQuorumCount; CBlockIndex* pindexStart; - { - LOCK(::cs_main); - if (signHeight == -1) { - signHeight = active_chain.Height(); - } - int startBlockHeight = signHeight - signOffset; - if (startBlockHeight > active_chain.Height() || startBlockHeight < 0) { - return {}; - } - pindexStart = active_chain[startBlockHeight]; + // Resolve strictly relative to the authoritative active chain to avoid any + // cross-tip inconsistencies during rotations or rapid tip changes. + LOCK(::cs_main); + if (signHeight == -1) { + signHeight = active_chain.Height(); + } + int startBlockHeight = signHeight - signOffset; + if (startBlockHeight > active_chain.Height() || startBlockHeight < 0) { + return {}; } + pindexStart = active_chain[startBlockHeight]; if (IsQuorumRotationEnabled(llmq_params, pindexStart)) { auto quorums = qman.ScanQuorums(llmq_params.type, pindexStart, poolSize); diff --git a/src/llmq/quorums.h b/src/llmq/quorums.h index f1dc5df1849f..78b1d8ffa2c8 100644 --- a/src/llmq/quorums.h +++ b/src/llmq/quorums.h @@ -17,10 +17,12 @@ #include #include #include +#include #include #include +#include #include #include @@ -258,6 +260,33 @@ class CQuorumManager // it maps `quorum_hash` to `pindex` mutable Uint256LruHashMap quorumBaseBlockIndexCache; + // Active chain snapshot to avoid cs_main contention + struct ActiveChainView { + const CBlockIndex* tip{nullptr}; + int height{-1}; + uint256 hash; + }; + mutable Mutex cs_active_chain_view; + ActiveChainView m_active_chain_view GUARDED_BY(cs_active_chain_view); + + // Ancestor lookup cache (height -> CBlockIndex*) for current tip + mutable Mutex cs_ancestor_cache; + mutable unordered_lru_cache> m_ancestor_lru GUARDED_BY(cs_ancestor_cache); + mutable uint256 m_lru_tip_hash GUARDED_BY(cs_ancestor_cache); + mutable int m_lru_tip_height GUARDED_BY(cs_ancestor_cache){-1}; + static size_t ComputeAncestorCacheMaxSize(); + + // ValidationInterface subscriber for tip updates + class ChainViewSubscriber : public CValidationInterface { + public: + explicit ChainViewSubscriber(CQuorumManager& qman) : m_qman(qman) {} + virtual ~ChainViewSubscriber() = default; + void UpdatedBlockTip(const CBlockIndex* pindexNew, const CBlockIndex* pindexFork, bool fInitialDownload) override; + private: + CQuorumManager& m_qman; + }; + std::unique_ptr m_chain_view_subscriber; + mutable ctpl::thread_pool workerPool; mutable CThreadInterrupt quorumThreadInterrupt; @@ -276,10 +305,10 @@ class CQuorumManager void Stop(); void TriggerQuorumDataRecoveryThreads(CConnman& connman, const CBlockIndex* pIndex) const - EXCLUSIVE_LOCKS_REQUIRED(!cs_db, !cs_scan_quorums, !cs_map_quorums); + EXCLUSIVE_LOCKS_REQUIRED(!cs_db, !cs_scan_quorums, !cs_map_quorums, !cs_active_chain_view, !cs_ancestor_cache); void UpdatedBlockTip(const CBlockIndex* pindexNew, CConnman& connman, bool fInitialDownload) const - EXCLUSIVE_LOCKS_REQUIRED(!cs_db, !cs_scan_quorums, !cs_map_quorums); + EXCLUSIVE_LOCKS_REQUIRED(!cs_db, !cs_scan_quorums, !cs_map_quorums, !cs_active_chain_view, !cs_ancestor_cache); [[nodiscard]] MessageProcessingResult ProcessMessage(CNode& pfrom, CConnman& connman, std::string_view msg_type, CDataStream& vRecv) @@ -294,17 +323,21 @@ class CQuorumManager CQuorumCPtr GetQuorum(Consensus::LLMQType llmqType, const uint256& quorumHash) const EXCLUSIVE_LOCKS_REQUIRED(!cs_db, !cs_map_quorums); std::vector ScanQuorums(Consensus::LLMQType llmqType, size_t nCountRequested) const - EXCLUSIVE_LOCKS_REQUIRED(!cs_db, !cs_map_quorums, !cs_scan_quorums); + EXCLUSIVE_LOCKS_REQUIRED(!cs_db, !cs_map_quorums, !cs_scan_quorums, !cs_active_chain_view, !cs_ancestor_cache); // this one is cs_main-free std::vector ScanQuorums(Consensus::LLMQType llmqType, const CBlockIndex* pindexStart, size_t nCountRequested) const - EXCLUSIVE_LOCKS_REQUIRED(!cs_db, !cs_map_quorums, !cs_scan_quorums); + EXCLUSIVE_LOCKS_REQUIRED(!cs_db, !cs_map_quorums, !cs_scan_quorums, !cs_active_chain_view, !cs_ancestor_cache); + + // Active chain snapshot accessors (public for use by SelectQuorumForSigning) + ActiveChainView GetActiveChainView() const EXCLUSIVE_LOCKS_REQUIRED(!cs_active_chain_view); + const CBlockIndex* FindAncestorFast(const ActiveChainView& view, int target_height) const EXCLUSIVE_LOCKS_REQUIRED(!cs_ancestor_cache); private: // all private methods here are cs_main-free void CheckQuorumConnections(CConnman& connman, const Consensus::LLMQParams& llmqParams, const CBlockIndex* pindexNew) const - EXCLUSIVE_LOCKS_REQUIRED(!cs_db, !cs_scan_quorums, !cs_map_quorums); + EXCLUSIVE_LOCKS_REQUIRED(!cs_db, !cs_scan_quorums, !cs_map_quorums, !cs_active_chain_view, !cs_ancestor_cache); CQuorumPtr BuildQuorumFromCommitment(Consensus::LLMQType llmqType, gsl::not_null pQuorumBaseBlockIndex,