Skip to content

Commit 8a897de

Browse files
thepastaclawclaude
andcommitted
fix: invalidate quorum commitment cache on disconnect
CachedGetQcHashesQcIndexedHashes keyed both process-lifetime layers only by quorum base blocks/hashes, not by the mined CFinalCommitment. Different valid branches can therefore produce different commitment hashes for the same base list. evoDb rolled back on disconnect, but the caches did not, which can make CalcCbTxMerkleRootQuorums reject a valid replacement branch with bad-cbtx-quorummerkleroot. Invalidate both the outer whole-result cache and the per-quorum LRU from CQuorumBlockProcessor::UndoBlock when a non-null commitment is undone, and add unit coverage for the branch-change and UndoBlock invalidation seams. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 6493af2 commit 8a897de

4 files changed

Lines changed: 275 additions & 22 deletions

File tree

src/evo/cbtx.cpp

Lines changed: 45 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,35 @@ bool CheckCbTx(const CCbTx& cbTx, const CBlockIndex* pindexPrev, TxValidationSta
4949
using QcHashMap = std::map<Consensus::LLMQType, std::vector<uint256>>;
5050
using QcIndexedHashMap = std::map<Consensus::LLMQType, std::map<int16_t, uint256>>;
5151

52+
// Process-lifetime caches for CalcCbTxMerkleRootQuorums.
53+
//
54+
// The outer whole-result cache is keyed only by the set of active quorum *base*
55+
// blocks. The inner LRU is keyed only by those base-block hashes. Neither key
56+
// includes the serialized CFinalCommitment that was actually mined for that
57+
// base on the active chain. Different valid branches can therefore mine
58+
// different commitments for the same base list, so these caches must be dropped
59+
// whenever mined commitment state is undone (see InvalidateCachedQcHashes).
60+
namespace {
61+
Mutex g_qc_hashes_cache_mutex;
62+
std::map<Consensus::LLMQType, std::vector<const CBlockIndex*>> g_quorums_cached GUARDED_BY(g_qc_hashes_cache_mutex);
63+
std::map<Consensus::LLMQType, Uint256LruHashMap<std::pair<uint256, int>>> g_qc_hashes_lru GUARDED_BY(g_qc_hashes_cache_mutex);
64+
QcHashMap g_qcHashes_cached GUARDED_BY(g_qc_hashes_cache_mutex);
65+
QcIndexedHashMap g_qcIndexedHashes_cached GUARDED_BY(g_qc_hashes_cache_mutex);
66+
} // anonymous namespace
67+
68+
void InvalidateCachedQcHashes()
69+
{
70+
LOCK(g_qc_hashes_cache_mutex);
71+
g_quorums_cached.clear();
72+
g_qcHashes_cached.clear();
73+
g_qcIndexedHashes_cached.clear();
74+
// Clear per-type LRU contents but keep the map entries so InitQuorumsCache is
75+
// not required on every post-invalidation miss.
76+
for (auto& [_, cache] : g_qc_hashes_lru) {
77+
cache.clear();
78+
}
79+
}
80+
5281
/**
5382
* Handles the calculation or caching of qcHashes and qcIndexedHashes
5483
* @param pindexPrev The const CBlockIndex* (ie a block) of a block. Both the Quorum list and quorum rotation activation status will be retrieved based on this block.
@@ -58,45 +87,40 @@ auto CachedGetQcHashesQcIndexedHashes(const CBlockIndex* pindexPrev, const llmq:
5887
std::optional<std::pair<QcHashMap /*qcHashes*/, QcIndexedHashMap /*qcIndexedHashes*/>> {
5988
auto quorums = quorum_block_processor.GetMinedAndActiveCommitmentsUntilBlock(pindexPrev);
6089

61-
static Mutex cs_cache;
62-
static std::map<Consensus::LLMQType, std::vector<const CBlockIndex*>> quorums_cached GUARDED_BY(cs_cache);
63-
static std::map<Consensus::LLMQType, Uint256LruHashMap<std::pair<uint256, int>>> qc_hashes_cached GUARDED_BY(cs_cache);
64-
static QcHashMap qcHashes_cached GUARDED_BY(cs_cache);
65-
static QcIndexedHashMap qcIndexedHashes_cached GUARDED_BY(cs_cache);
66-
67-
LOCK(cs_cache);
68-
if (quorums == quorums_cached) {
69-
return std::make_pair(qcHashes_cached, qcIndexedHashes_cached);
90+
LOCK(g_qc_hashes_cache_mutex);
91+
if (quorums == g_quorums_cached) {
92+
return std::make_pair(g_qcHashes_cached, g_qcIndexedHashes_cached);
7093
}
7194

72-
// Quorums set is different, reset cached values
73-
quorums_cached.clear();
74-
qcHashes_cached.clear();
75-
qcIndexedHashes_cached.clear();
76-
if (qc_hashes_cached.empty()) {
77-
llmq::utils::InitQuorumsCache(qc_hashes_cached, Params().GetConsensus());
95+
// Quorums set changed: rebuild whole-result caches. Keep the per-base LRU;
96+
// branch-dependent staleness is handled by InvalidateCachedQcHashes().
97+
g_quorums_cached.clear();
98+
g_qcHashes_cached.clear();
99+
g_qcIndexedHashes_cached.clear();
100+
if (g_qc_hashes_lru.empty()) {
101+
llmq::utils::InitQuorumsCache(g_qc_hashes_lru, Params().GetConsensus());
78102
}
79103

80104
for (const auto& [llmqType, vecBlockIndexes] : quorums) {
81105
const auto& llmq_params_opt = Params().GetLLMQ(llmqType);
82106
assert(llmq_params_opt.has_value());
83107
bool rotation_enabled = llmq::IsQuorumRotationEnabled(llmq_params_opt.value(), pindexPrev);
84-
auto& vec_hashes = qcHashes_cached[llmqType];
108+
auto& vec_hashes = g_qcHashes_cached[llmqType];
85109
vec_hashes.reserve(vecBlockIndexes.size());
86-
auto& map_indexed_hashes = qcIndexedHashes_cached[llmqType];
110+
auto& map_indexed_hashes = g_qcIndexedHashes_cached[llmqType];
87111
for (const auto& blockIndex : vecBlockIndexes) {
88112
uint256 block_hash{blockIndex->GetBlockHash()};
89113

90114
std::pair<uint256, int> qc_hash;
91-
if (!qc_hashes_cached[llmqType].get(block_hash, qc_hash)) {
115+
if (!g_qc_hashes_lru[llmqType].get(block_hash, qc_hash)) {
92116
auto [pqc, dummy_hash] = quorum_block_processor.GetMinedCommitment(llmqType, block_hash);
93117
if (dummy_hash == uint256::ZERO) {
94118
// this should never happen
95119
return std::nullopt;
96120
}
97121
qc_hash.first = ::SerializeHash(pqc);
98122
qc_hash.second = rotation_enabled ? pqc.quorumIndex : 0;
99-
qc_hashes_cached[llmqType].insert(block_hash, qc_hash);
123+
g_qc_hashes_lru[llmqType].insert(block_hash, qc_hash);
100124
}
101125
if (rotation_enabled) {
102126
map_indexed_hashes[qc_hash.second] = qc_hash.first;
@@ -105,8 +129,8 @@ auto CachedGetQcHashesQcIndexedHashes(const CBlockIndex* pindexPrev, const llmq:
105129
}
106130
}
107131
}
108-
std::swap(quorums_cached, quorums);
109-
return std::make_pair(qcHashes_cached, qcIndexedHashes_cached);
132+
std::swap(g_quorums_cached, quorums);
133+
return std::make_pair(g_qcHashes_cached, g_qcIndexedHashes_cached);
110134
}
111135

112136
auto CalcHashCountFromQCHashes(const QcHashMap& qcHashes)

src/evo/cbtx.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,15 @@ bool CalcCbTxMerkleRootQuorums(const CBlock& block, const CBlockIndex* pindexPre
7474
const llmq::CQuorumBlockProcessor& quorum_block_processor, uint256& merkleRootRet,
7575
BlockValidationState& state);
7676

77+
/**
78+
* Drop process-lifetime CbTx quorum-commitment hash caches.
79+
*
80+
* Required when mined commitment data for a quorum base block can change without the
81+
* active base-block list changing (e.g. disconnect/reorg of the block that mined a
82+
* different valid CFinalCommitment for the same quorumHash).
83+
*/
84+
void InvalidateCachedQcHashes();
85+
7786
std::optional<std::pair<CBLSSignature, uint32_t>> GetNonNullCoinbaseChainlock(const CBlockIndex* pindex);
7887

7988
#endif // BITCOIN_EVO_CBTX_H

src/llmq/blockprocessor.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
#include <llmq/blockprocessor.h>
66

7+
#include <evo/cbtx.h>
78
#include <evo/evodb.h>
89
#include <evo/specialtx.h>
910
#include <llmq/commitment.h>
@@ -391,12 +392,14 @@ bool CQuorumBlockProcessor::UndoBlock(const CBlock& block, gsl::not_null<const C
391392
return false;
392393
}
393394

395+
bool undone_commitment{false};
394396
for (auto& [_, qc2] : qcs) {
395397
auto& qc = qc2; // cannot capture structured binding into lambda
396398
if (qc.IsNull()) {
397399
continue;
398400
}
399401

402+
undone_commitment = true;
400403
m_evoDb.Erase(std::make_pair(DB_MINED_COMMITMENT, std::make_pair(qc.llmqType, qc.quorumHash)));
401404

402405
const auto& llmq_params_opt = Params().GetLLMQ(qc.llmqType);
@@ -414,6 +417,12 @@ bool CQuorumBlockProcessor::UndoBlock(const CBlock& block, gsl::not_null<const C
414417
AddMineableCommitment(qc);
415418
}
416419

420+
// Drop both CbTx qc-hash cache layers: mined commitments are branch-dependent
421+
// even when the active quorum base-block list is unchanged.
422+
if (undone_commitment) {
423+
InvalidateCachedQcHashes();
424+
}
425+
417426
m_evoDb.Write(DB_BEST_BLOCK_UPGRADE, pindex->pprev->GetBlockHash());
418427

419428
return true;

0 commit comments

Comments
 (0)