From 3db85e24529a98bfb01859479f86afbbf6ed7c0a Mon Sep 17 00:00:00 2001 From: pasta Date: Tue, 11 Nov 2025 12:02:53 -0600 Subject: [PATCH 1/5] feat: enhance CQuorumManager with active chain view and ancestor caching - Introduced ActiveChainView structure to minimize cs_main contention. - Added ComputeAncestorCacheMaxSize method to determine cache size limits. - Implemented FindAncestorFast for efficient ancestor lookups. - Integrated ChainViewSubscriber for block tip updates and cache management. - Updated relevant methods to utilize the new caching mechanism for improved performance. --- src/llmq/quorums.cpp | 129 +++++++++++++++++++++++++++++++++++++++++-- src/llmq/quorums.h | 43 +++++++++++++-- 2 files changed, 162 insertions(+), 10 deletions(-) diff --git a/src/llmq/quorums.cpp b/src/llmq/quorums.cpp index 3c467aca2530..b1f5f72bf3fd 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,8 @@ 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 ? view.tip : WITH_LOCK(::cs_main, return m_chainstate.m_chain.Tip()); return ScanQuorums(llmqType, pindex, nCountRequested); } @@ -547,10 +646,16 @@ 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); + auto view = GetActiveChainView(); + const CBlockIndex* ancestor = view.tip ? FindAncestorFast(view, quorumCycleMiningEndHeight - llmq_params_opt->dkgInterval) : pindexStart->GetAncestor(quorumCycleMiningEndHeight - llmq_params_opt->dkgInterval); + if (!ancestor) return {}; + pindexStore = ancestor; } else if (pindexStart->nHeight > quorumCycleMiningEndHeight) { // we are past the mining phase of this cycle, use it - pindexStore = pindexStart->GetAncestor(quorumCycleMiningEndHeight); + auto view = GetActiveChainView(); + const CBlockIndex* ancestor = view.tip ? FindAncestorFast(view, quorumCycleMiningEndHeight) : pindexStart->GetAncestor(quorumCycleMiningEndHeight); + if (!ancestor) return {}; + pindexStore = ancestor; } // everything else is inside the mining phase of this cycle, no pindexStore adjustment needed @@ -1210,7 +1315,21 @@ CQuorumCPtr SelectQuorumForSigning(const Consensus::LLMQParams& llmq_params, con size_t poolSize = llmq_params.signingActiveQuorumCount; CBlockIndex* pindexStart; - { + auto view = qman.GetActiveChainView(); + if (view.tip) { + if (signHeight == -1) { + signHeight = view.height; + } + int startBlockHeight = signHeight - signOffset; + if (startBlockHeight > view.height || startBlockHeight < 0) { + return {}; + } + const CBlockIndex* ancestor = qman.FindAncestorFast(view, startBlockHeight); + if (!ancestor) { + return {}; + } + pindexStart = const_cast(ancestor); + } else { LOCK(::cs_main); if (signHeight == -1) { signHeight = active_chain.Height(); 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, From bd8c92e0abc7e5dd99f780fe633829f2a95fb3cd Mon Sep 17 00:00:00 2001 From: pasta Date: Tue, 11 Nov 2025 21:11:12 -0600 Subject: [PATCH 2/5] fix: correct ancestor computation in quorum scanning and selection Fix regression introduced in 3db85e24529 that caused incorrect quorum information to be returned during chain reorgs and block invalidations. Changes: - ScanQuorums(pindexStart): Compute ancestors strictly from pindexStart's chain context instead of mixing with global ActiveChainView to avoid cross-fork contamination - SelectQuorumForSigning: Add proper fallback to cs_main path when cached view cannot satisfy requested height or cache misses occur This ensures correctness during invalidate/reconsider scenarios while maintaining cs_main-free performance in busy paths when the cached view is sufficient. --- src/llmq/quorums.cpp | 52 ++++++++++++++++++++++++++++++++------------ 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/src/llmq/quorums.cpp b/src/llmq/quorums.cpp index b1f5f72bf3fd..8a783141a966 100644 --- a/src/llmq/quorums.cpp +++ b/src/llmq/quorums.cpp @@ -646,16 +646,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 {}; - auto view = GetActiveChainView(); - const CBlockIndex* ancestor = view.tip ? FindAncestorFast(view, quorumCycleMiningEndHeight - llmq_params_opt->dkgInterval) : pindexStart->GetAncestor(quorumCycleMiningEndHeight - llmq_params_opt->dkgInterval); - if (!ancestor) return {}; - pindexStore = ancestor; + // 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 - auto view = GetActiveChainView(); - const CBlockIndex* ancestor = view.tip ? FindAncestorFast(view, quorumCycleMiningEndHeight) : pindexStart->GetAncestor(quorumCycleMiningEndHeight); - if (!ancestor) return {}; - pindexStore = ancestor; + // 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 @@ -1317,18 +1318,41 @@ CQuorumCPtr SelectQuorumForSigning(const Consensus::LLMQParams& llmq_params, con CBlockIndex* pindexStart; auto view = qman.GetActiveChainView(); if (view.tip) { + // Prefer cs_main-free path via active chain view when it can satisfy the requested height. if (signHeight == -1) { signHeight = view.height; } int startBlockHeight = signHeight - signOffset; - if (startBlockHeight > view.height || startBlockHeight < 0) { - return {}; - } - const CBlockIndex* ancestor = qman.FindAncestorFast(view, startBlockHeight); - if (!ancestor) { + if (startBlockHeight >= 0) { + if (startBlockHeight <= view.height) { + const CBlockIndex* ancestor = qman.FindAncestorFast(view, startBlockHeight); + if (ancestor != nullptr) { + pindexStart = const_cast(ancestor); + } else { + // Fallback to cs_main if cache missed (e.g. during reorg/initialization) + LOCK(::cs_main); + if (signHeight == -1) { + signHeight = active_chain.Height(); + } + if (startBlockHeight > active_chain.Height() || startBlockHeight < 0) { + return {}; + } + pindexStart = active_chain[startBlockHeight]; + } + } else { + // View is behind requested height; fallback to cs_main + LOCK(::cs_main); + if (signHeight == -1) { + signHeight = active_chain.Height(); + } + if (startBlockHeight > active_chain.Height() || startBlockHeight < 0) { + return {}; + } + pindexStart = active_chain[startBlockHeight]; + } + } else { return {}; } - pindexStart = const_cast(ancestor); } else { LOCK(::cs_main); if (signHeight == -1) { From a8fb74f70b01667d0310f791dfdf0e8b86400640 Mon Sep 17 00:00:00 2001 From: pasta Date: Wed, 12 Nov 2025 09:44:43 -0600 Subject: [PATCH 3/5] fix: prevent stale cached tip in ScanQuorums(size_t) by preferring active chain tip when view lags This addresses rotation test flakiness by avoiding returning quorum sets based on a stale ActiveChainView during rapid tip changes. Default path remains cs_main-free; we only fall back to cs_main when the cached view height is behind the active chain. --- src/llmq/quorums.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/llmq/quorums.cpp b/src/llmq/quorums.cpp index 8a783141a966..a380575f1122 100644 --- a/src/llmq/quorums.cpp +++ b/src/llmq/quorums.cpp @@ -622,7 +622,16 @@ bool CQuorumManager::RequestQuorumData(CNode* pfrom, CConnman& connman, const CQ std::vector CQuorumManager::ScanQuorums(Consensus::LLMQType llmqType, size_t nCountRequested) const { auto view = GetActiveChainView(); - const CBlockIndex* pindex = view.tip ? view.tip : WITH_LOCK(::cs_main, return m_chainstate.m_chain.Tip()); + 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); } From 970ba10dfd84c2a55204b67a091cc5045d7db355 Mon Sep 17 00:00:00 2001 From: pasta Date: Wed, 12 Nov 2025 10:22:19 -0600 Subject: [PATCH 4/5] fix: resolve quorum selection against active chain when signHeight is provided Ensure verification paths (e.g. ChainLocks, rotation) derive pindexStart from the authoritative active chain when a specific height is given. Keep cs_main-free fast path only for callers without an explicit height. This avoids mismatches during rapid tip changes and prevents missed ChainLocks in feature_llmq_rotation.py. --- src/llmq/quorums.cpp | 54 ++++++++++++++++++++++---------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/src/llmq/quorums.cpp b/src/llmq/quorums.cpp index a380575f1122..2252e854b50b 100644 --- a/src/llmq/quorums.cpp +++ b/src/llmq/quorums.cpp @@ -1325,53 +1325,53 @@ CQuorumCPtr SelectQuorumForSigning(const Consensus::LLMQParams& llmq_params, con size_t poolSize = llmq_params.signingActiveQuorumCount; CBlockIndex* pindexStart; - auto view = qman.GetActiveChainView(); - if (view.tip) { - // Prefer cs_main-free path via active chain view when it can satisfy the requested height. - if (signHeight == -1) { - signHeight = view.height; - } + // If caller provided an explicit signHeight, always resolve relative to the active chain + // to guarantee correctness during rapid tip changes and rotations. + if (signHeight != -1) { + LOCK(::cs_main); int startBlockHeight = signHeight - signOffset; - if (startBlockHeight >= 0) { + if (startBlockHeight > active_chain.Height() || startBlockHeight < 0) { + return {}; + } + pindexStart = active_chain[startBlockHeight]; + } else { + // No explicit height: prefer cs_main-free path via active chain view + auto view = qman.GetActiveChainView(); + if (view.tip) { + signHeight = view.height; + int startBlockHeight = signHeight - signOffset; + if (startBlockHeight < 0) { + return {}; + } if (startBlockHeight <= view.height) { const CBlockIndex* ancestor = qman.FindAncestorFast(view, startBlockHeight); - if (ancestor != nullptr) { - pindexStart = const_cast(ancestor); - } else { + if (!ancestor) { // Fallback to cs_main if cache missed (e.g. during reorg/initialization) LOCK(::cs_main); - if (signHeight == -1) { - signHeight = active_chain.Height(); - } - if (startBlockHeight > active_chain.Height() || startBlockHeight < 0) { + if (startBlockHeight > active_chain.Height()) { return {}; } pindexStart = active_chain[startBlockHeight]; + } else { + pindexStart = const_cast(ancestor); } } else { // View is behind requested height; fallback to cs_main LOCK(::cs_main); - if (signHeight == -1) { - signHeight = active_chain.Height(); - } - if (startBlockHeight > active_chain.Height() || startBlockHeight < 0) { + if (startBlockHeight > active_chain.Height()) { return {}; } pindexStart = active_chain[startBlockHeight]; } } else { - return {}; - } - } else { 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]; } - int startBlockHeight = signHeight - signOffset; - if (startBlockHeight > active_chain.Height() || startBlockHeight < 0) { - return {}; - } - pindexStart = active_chain[startBlockHeight]; } if (IsQuorumRotationEnabled(llmq_params, pindexStart)) { From 56f40c3ae74f5d899032a8f1ab02d5692f9f0800 Mon Sep 17 00:00:00 2001 From: pasta Date: Wed, 12 Nov 2025 10:29:38 -0600 Subject: [PATCH 5/5] fix: make SelectQuorumForSigning resolve strictly via active chain under cs_main Always derive pindexStart from the authoritative active chain (with SIGN_HEIGHT_OFFSET) for both signing and verification. This removes cached-view variability that led to missed ChainLocks in rotation tests, while keeping other paths cs_main-free. --- src/llmq/quorums.cpp | 57 ++++++++------------------------------------ 1 file changed, 10 insertions(+), 47 deletions(-) diff --git a/src/llmq/quorums.cpp b/src/llmq/quorums.cpp index 2252e854b50b..856b2ee4a33a 100644 --- a/src/llmq/quorums.cpp +++ b/src/llmq/quorums.cpp @@ -1325,54 +1325,17 @@ CQuorumCPtr SelectQuorumForSigning(const Consensus::LLMQParams& llmq_params, con size_t poolSize = llmq_params.signingActiveQuorumCount; CBlockIndex* pindexStart; - // If caller provided an explicit signHeight, always resolve relative to the active chain - // to guarantee correctness during rapid tip changes and rotations. - if (signHeight != -1) { - LOCK(::cs_main); - int startBlockHeight = signHeight - signOffset; - if (startBlockHeight > active_chain.Height() || startBlockHeight < 0) { - return {}; - } - pindexStart = active_chain[startBlockHeight]; - } else { - // No explicit height: prefer cs_main-free path via active chain view - auto view = qman.GetActiveChainView(); - if (view.tip) { - signHeight = view.height; - int startBlockHeight = signHeight - signOffset; - if (startBlockHeight < 0) { - return {}; - } - if (startBlockHeight <= view.height) { - const CBlockIndex* ancestor = qman.FindAncestorFast(view, startBlockHeight); - if (!ancestor) { - // Fallback to cs_main if cache missed (e.g. during reorg/initialization) - LOCK(::cs_main); - if (startBlockHeight > active_chain.Height()) { - return {}; - } - pindexStart = active_chain[startBlockHeight]; - } else { - pindexStart = const_cast(ancestor); - } - } else { - // View is behind requested height; fallback to cs_main - LOCK(::cs_main); - if (startBlockHeight > active_chain.Height()) { - return {}; - } - pindexStart = active_chain[startBlockHeight]; - } - } else { - LOCK(::cs_main); - 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);