Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 129 additions & 14 deletions src/llmq/quorums.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@
#include <util/time.h>
#include <util/underlying.h>
#include <validation.h>
#include <validationinterface.h>

#include <algorithm>
#include <cxxtimer.hpp>

namespace llmq
Expand Down Expand Up @@ -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;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix clang-format issue.

The CI pipeline reports a clang-format difference at this line. Please run clang-format to fix the formatting.

🧰 Tools
🪛 GitHub Actions: Clang Diff Format Check

[error] 261-261: Clang-format differences found in quorums.cpp. Apply clang-format to fix formatting differences.

🤖 Prompt for AI Agents
In src/llmq/quorums.cpp around line 261, the file's formatting at the return
statement does not match clang-format expectations; run clang-format (using the
repo's configured style) on this file or adjust whitespace so the line reads
exactly as formatted by clang-format (e.g., ensure correct indentation and
spacing around the return statement) and commit the resulting change.

}

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,
Expand All @@ -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<ChainViewSubscriber>(*this);
RegisterValidationInterface(m_chain_view_subscriber.get());
}

CQuorumManager::~CQuorumManager()
{
Stop();
if (m_chain_view_subscriber) {
UnregisterValidationInterface(m_chain_view_subscriber.get());
}
}

void CQuorumManager::Start()
Expand Down Expand Up @@ -523,7 +621,17 @@ bool CQuorumManager::RequestQuorumData(CNode* pfrom, CConnman& connman, const CQ

std::vector<CQuorumCPtr> 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);
}

Expand All @@ -547,10 +655,17 @@ std::vector<CQuorumCPtr> 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<const CBlockIndex*>{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<const CBlockIndex*>{ancestor};
}
// everything else is inside the mining phase of this cycle, no pindexStore adjustment needed

Expand Down Expand Up @@ -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);
Expand Down
43 changes: 38 additions & 5 deletions src/llmq/quorums.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@
#include <saltedhasher.h>
#include <util/threadinterrupt.h>
#include <util/time.h>
#include <validationinterface.h>

#include <gsl/pointers.h>

#include <atomic>
#include <functional>
#include <map>
#include <utility>

Expand Down Expand Up @@ -258,6 +260,33 @@ class CQuorumManager
// it maps `quorum_hash` to `pindex`
mutable Uint256LruHashMap<const CBlockIndex*, 128 /*max_size*/> 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<int, const CBlockIndex*, std::hash<int>> 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<ChainViewSubscriber> m_chain_view_subscriber;

mutable ctpl::thread_pool workerPool;
mutable CThreadInterrupt quorumThreadInterrupt;

Expand All @@ -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)
Expand All @@ -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<CQuorumCPtr> 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<CQuorumCPtr> 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<const CBlockIndex*> pQuorumBaseBlockIndex,
Expand Down
Loading