From 9ac20b8cf9913718eb2fb7c3c8d37abea915fdff Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Wed, 29 Jul 2026 01:34:27 +0700 Subject: [PATCH 1/7] fix: suppress intentional unsigned overflow in test/lcg.h test/lcg.h:28 state = state * 6364136223846793005 + 1442695040888963407 an MMIX linear congruential generator; and it's meant to be unsigned overflowed --- test/sanitizer_suppressions/ubsan | 1 + 1 file changed, 1 insertion(+) diff --git a/test/sanitizer_suppressions/ubsan b/test/sanitizer_suppressions/ubsan index 8b7f01a0b167..b486e22bea12 100644 --- a/test/sanitizer_suppressions/ubsan +++ b/test/sanitizer_suppressions/ubsan @@ -45,6 +45,7 @@ unsigned-integer-overflow:coins.cpp unsigned-integer-overflow:compressor.cpp unsigned-integer-overflow:crypto/ unsigned-integer-overflow:hash.cpp +unsigned-integer-overflow:lcg.h unsigned-integer-overflow:policy/fees.cpp unsigned-integer-overflow:prevector.h unsigned-integer-overflow:EvalScript From a3a259e6a6f96d40888e0df34120c536f2f5aa97 Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Wed, 29 Jul 2026 01:48:03 +0700 Subject: [PATCH 2/7] fix: make the block-height narrowing in address index keys explicit m_block_height is int32_t but the on-disk field is a big-endian uint32, so Unserialize narrowed implicitly and -fsanitize=integer reported every read whose top bit was set: implicit conversion from type 'uint32_t' of value 3400325678 to type 'int32_t' changed the value to -894641618 --- src/index/addressindex.cpp | 2 +- src/index/addressindex_types.h | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/index/addressindex.cpp b/src/index/addressindex.cpp index 2a96dde8fac0..b28a5a57c021 100644 --- a/src/index/addressindex.cpp +++ b/src/index/addressindex.cpp @@ -352,7 +352,7 @@ bool AddressIndex::CustomRewind(const interfaces::BlockKey& current_tip, const i // Remove spending activity from history addressIndex.push_back( - std::make_pair(CAddressIndexKey(address_type, address_bytes, pindex->nHeight, i + 1, txhash, j, true), + std::make_pair(CAddressIndexKey(address_type, address_bytes, pindex->nHeight, i, txhash, j, true), prevout.nValue * -1)); // Restore to unspent index diff --git a/src/index/addressindex_types.h b/src/index/addressindex_types.h index 450fb611207b..3d45c96ab14b 100644 --- a/src/index/addressindex_types.h +++ b/src/index/addressindex_types.h @@ -147,7 +147,7 @@ struct CAddressIndexKey { { m_address_type = static_cast(ser_readdata8(s)); m_address_bytes.Unserialize(s); - m_block_height = ser_readdata32be(s); + m_block_height = static_cast(ser_readdata32be(s)); m_block_tx_pos = ser_readdata32be(s); m_tx_hash.Unserialize(s); m_tx_index = ser_readdata32(s); @@ -228,7 +228,7 @@ struct CAddressIndexIteratorHeightKey { { m_address_type = static_cast(ser_readdata8(s)); m_address_bytes.Unserialize(s); - m_block_height = ser_readdata32be(s); + m_block_height = static_cast(ser_readdata32be(s)); } }; From 533db584466e94a776bb6bfe33f518e2b3d813f1 Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Wed, 29 Jul 2026 01:57:26 +0700 Subject: [PATCH 3/7] fix: avoid decrementing past zero to prevent false sanitizer reports The reverse-iteration idiom `for (size_t idx = n; idx-- > 0;)` decrements on the final check too, so idx wraps to SIZE_MAX every time the loop ends, not just when prev_cycles is empty. -fsanitize=integer reports it on each exit: llmq/utils.cpp:519:54: runtime error: unsigned integer overflow: 0 - 1 cannot be represented in type 'size_t' index/addressindex.cpp:309 undoing a block's transactions in reverse llmq/snapshot.cpp:194 building the mnlistdiff chain in reverse src/qt/proposalmodel.cpp:408 uses the same shape but counts with int, where 0 - 1 is representable, so it is left alone. --- src/index/addressindex.cpp | 8 ++++---- src/llmq/snapshot.cpp | 4 ++-- src/llmq/utils.cpp | 5 +++-- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/index/addressindex.cpp b/src/index/addressindex.cpp index b28a5a57c021..fbd862769580 100644 --- a/src/index/addressindex.cpp +++ b/src/index/addressindex.cpp @@ -306,9 +306,9 @@ bool AddressIndex::CustomRewind(const interfaces::BlockKey& current_tip, const i pindex->GetBlockHash().ToString(), block.vtx.size() - 1, blockundo.vtxundo.size()); } - for (size_t i = blockundo.vtxundo.size(); i-- > 0;) { - const CTransactionRef& tx = block.vtx[i + 1]; - const CTxUndo& txundo = blockundo.vtxundo[i]; + for (size_t i = blockundo.vtxundo.size(); i > 0; --i) { + const CTransactionRef& tx = block.vtx[i]; + const CTxUndo& txundo = blockundo.vtxundo[i-1]; const uint256 txhash = tx->GetHash(); // Undo outputs (remove from unspent index and transaction history) @@ -324,7 +324,7 @@ bool AddressIndex::CustomRewind(const interfaces::BlockKey& current_tip, const i // Remove receiving activity from history addressIndex.push_back(std::make_pair(CAddressIndexKey(address_type, address_bytes, pindex->nHeight, - i + 1, txhash, k, false), + i, txhash, k, false), out.nValue)); // Remove from unspent index (mark for deletion) diff --git a/src/llmq/snapshot.cpp b/src/llmq/snapshot.cpp index 389b624c01fd..49d15a7c2609 100644 --- a/src/llmq/snapshot.cpp +++ b/src/llmq/snapshot.cpp @@ -191,8 +191,8 @@ bool BuildQuorumRotationInfo(CDeterministicMNManager& dmnman, CQuorumSnapshotMan } if (!use_legacy_construction) { - for (size_t idx = target_cycles.size(); idx-- > 0;) { - auto* cycle{target_cycles[idx]}; + for (size_t n = target_cycles.size(); n > 0; --n) { + auto* cycle{target_cycles[n - 1]}; if (!BuildSimplifiedMNListDiff(dmnman, chainman, qblockman, qman, GetLastBaseBlockHash(baseBlockIndexes, cycle->m_work_index, use_legacy_construction), diff --git a/src/llmq/utils.cpp b/src/llmq/utils.cpp index 5668f4678fb5..106af38c2c68 100644 --- a/src/llmq/utils.cpp +++ b/src/llmq/utils.cpp @@ -516,8 +516,9 @@ std::vector ComputeQuorumMembersByQuarterRotation(const Consensus if (LogAcceptDebug(BCLog::LLMQ)) { for (const size_t i : util::irange(nQuorums)) { std::stringstream ss; - for (size_t idx = prev_cycles.size(); idx-- > 0;) { - ss << strprintf(" %dCmns[%s]", idx, ToString(prev_cycles[idx]->m_members[i])); + for (size_t idx = prev_cycles.size(); idx > 0; --idx) { + const size_t cycle = idx - 1; + ss << strprintf(" %dCmns[%s]", cycle, ToString(prev_cycles[cycle]->m_members[i])); } ss << strprintf(" new[%s]", ToString(newQuarterMembers[i])); LogPrint(BCLog::LLMQ, "QuarterComposition h[%d] i[%d]:%s\n", cycleBaseHeight, i, ss.str()); From 064d07acaa4c7e3c6793c68bcd974e6cfd411f11 Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Wed, 29 Jul 2026 02:14:44 +0700 Subject: [PATCH 4/7] fix: hold ChainstateManager, not a Chainstate reference CQuorumBlockProcessor stored `Chainstate& m_chainstate`, bound once when LLMQContext is built in node::DashChainstateSetup(). A reference cannot be rebound, but the object it names does not live as long as LLMQContext: ChainstateManager::ResetChainstates() destroys the chainstate and InitializeChainstate() allocates a new one. Everything the block processor does afterwards reads freed memory. AddressSanitizer catches it in validation_chainstate_tests/chainstate_update_tip: ERROR: AddressSanitizer: heap-use-after-free READ of size 8 at offset 136 inside a 288-byte region llmq::CQuorumBlockProcessor::ProcessBlock llmq/blockprocessor.cpp:171 CSpecialTxProcessor::ProcessSpecialTxsInBlock evo/specialtxman.cpp:707 Chainstate::ConnectBlock validation.cpp:2320 freed by ChainstateManager::ResetChainstates validation.cpp:5807 Offset 136 is Chainstate::m_chainman, which is what line 171 dereferences. Not reachable on a running node today: ResetChainstates() and ActivateSnapshot() have no callers outside tests, and Dash has no loadtxoutset RPC, so no snapshot is ever activated. The reference is still wrong on its own terms though, because ActivateSnapshot() repoints m_active_chainstate without freeing the old chainstate, which would leave the block processor validating commitments against the background chain. Better to fix it now than to wait for assumeutxo to be wired up. Holding the manager and asking for the active chainstate per call removes the lifetime question entirely: ChainstateManager is owned by NodeContext and outlives LLMQContext. It also matches the immediate caller, CSpecialTxProcessor, which already keeps `const ChainstateManager&`, and shortens the nine sites that spelled out m_chainstate.m_chainman. --- src/llmq/blockprocessor.cpp | 58 +++++++++++++++++++------------------ src/llmq/blockprocessor.h | 6 ++-- src/llmq/context.cpp | 4 +-- 3 files changed, 35 insertions(+), 33 deletions(-) diff --git a/src/llmq/blockprocessor.cpp b/src/llmq/blockprocessor.cpp index c901bfbcc3ab..ef2b5d3e7694 100644 --- a/src/llmq/blockprocessor.cpp +++ b/src/llmq/blockprocessor.cpp @@ -45,14 +45,14 @@ static const std::string DB_MINED_COMMITMENT_BY_INVERSED_HEIGHT_Q_INDEXED = "q_m static const std::string DB_BEST_BLOCK_UPGRADE = "q_bbu2"; -CQuorumBlockProcessor::CQuorumBlockProcessor(Chainstate& chainstate, CDeterministicMNManager& dmnman, CEvoDB& evoDb, - CQuorumSnapshotManager& qsnapman, int8_t bls_threads) : - m_chainstate{chainstate}, +CQuorumBlockProcessor::CQuorumBlockProcessor(const ChainstateManager& chainman, CDeterministicMNManager& dmnman, + CEvoDB& evoDb, CQuorumSnapshotManager& qsnapman, int8_t bls_threads) : + m_chainman{chainman}, m_dmnman{dmnman}, m_evoDb{evoDb}, m_qsnapman{qsnapman} { - utils::InitQuorumsCache(mapHasMinedCommitmentCache, m_chainstate.m_chainman.GetConsensus()); + utils::InitQuorumsCache(mapHasMinedCommitmentCache, m_chainman.GetConsensus()); LogPrintf("BLS verification uses %d additional threads\n", bls_threads); m_bls_queue.StartWorkerThreads(bls_threads); } @@ -94,7 +94,7 @@ MessageProcessingResult CQuorumBlockProcessor::ProcessMessage(const CNode& peer, const CBlockIndex* pQuorumBaseBlockIndex; { LOCK(::cs_main); - pQuorumBaseBlockIndex = m_chainstate.m_blockman.LookupBlockIndex(qc.quorumHash); + pQuorumBaseBlockIndex = m_chainman.m_blockman.LookupBlockIndex(qc.quorumHash); if (pQuorumBaseBlockIndex == nullptr) { LogPrint(BCLog::LLMQ, "CQuorumBlockProcessor::%s -- unknown block %s in commitment, peer=%d\n", __func__, qc.quorumHash.ToString(), peer.GetId()); @@ -102,7 +102,7 @@ MessageProcessingResult CQuorumBlockProcessor::ProcessMessage(const CNode& peer, // fully synced return ret; } - if (m_chainstate.m_chain.Tip()->GetAncestor(pQuorumBaseBlockIndex->nHeight) != pQuorumBaseBlockIndex) { + if (m_chainman.ActiveChain().Tip()->GetAncestor(pQuorumBaseBlockIndex->nHeight) != pQuorumBaseBlockIndex) { LogPrint(BCLog::LLMQ, "CQuorumBlockProcessor::%s -- block %s not in active chain, peer=%d\n", __func__, qc.quorumHash.ToString(), peer.GetId()); // same, can't punish @@ -115,7 +115,7 @@ MessageProcessingResult CQuorumBlockProcessor::ProcessMessage(const CNode& peer, ret.m_error = MisbehavingError{100}; return ret; } - if (pQuorumBaseBlockIndex->nHeight < (m_chainstate.m_chain.Height() - llmq_params_opt->dkgInterval)) { + if (pQuorumBaseBlockIndex->nHeight < (m_chainman.ActiveChain().Height() - llmq_params_opt->dkgInterval)) { LogPrint(BCLog::LLMQ, "CQuorumBlockProcessor::%s -- block %s is too old, peer=%d\n", __func__, qc.quorumHash.ToString(), peer.GetId()); if (peer.GetCommonVersion() >= QFCOMMIT_STALE_REPROP_BAN_VERSION) { @@ -145,7 +145,7 @@ MessageProcessingResult CQuorumBlockProcessor::ProcessMessage(const CNode& peer, } } - if (!qc.Verify({m_dmnman, m_qsnapman, m_chainstate.m_chainman, pQuorumBaseBlockIndex}, /*checkSigs=*/true)) { + if (!qc.Verify({m_dmnman, m_qsnapman, m_chainman, pQuorumBaseBlockIndex}, /*checkSigs=*/true)) { LogPrint(BCLog::LLMQ, "CQuorumBlockProcessor::%s -- commitment for quorum %s:%d is not valid quorumIndex[%d] nversion[%d], peer=%d\n", __func__, qc.quorumHash.ToString(), std23::to_underlying(qc.llmqType), qc.quorumIndex, qc.nVersion, peer.GetId()); @@ -168,12 +168,12 @@ bool CQuorumBlockProcessor::ProcessBlock(const CBlock& block, gsl::not_nullGetBlockHash(); - if (!DeploymentActiveAt(*pindex, m_chainstate.m_chainman.GetConsensus(), Consensus::DEPLOYMENT_DIP0003)) { + if (!DeploymentActiveAt(*pindex, m_chainman.GetConsensus(), Consensus::DEPLOYMENT_DIP0003)) { m_evoDb.Write(DB_BEST_BLOCK_UPGRADE, blockHash); return true; } - PreComputeQuorumMembers(m_dmnman, m_qsnapman, m_chainstate.m_chainman, pindex, /*reset_cache=*/false); + PreComputeQuorumMembers(m_dmnman, m_qsnapman, m_chainman, pindex, /*reset_cache=*/false); std::multimap qcs; if (!GetCommitmentsFromBlock(block, pindex, qcs, state)) { @@ -184,9 +184,9 @@ bool CQuorumBlockProcessor::ProcessBlock(const CBlock& block, gsl::not_nullpprev)) { + for (const Consensus::LLMQParams& params : GetEnabledQuorumParams(m_chainman, pindex->pprev)) { // skip these checks when replaying blocks after the crash - if (m_chainstate.m_chain.Tip() == nullptr) { + if (m_chainman.ActiveChain().Tip() == nullptr) { break; } @@ -209,13 +209,13 @@ bool CQuorumBlockProcessor::ProcessBlock(const CBlock& block, gsl::not_null queue_control(&m_bls_queue); for (const auto& [_, qc] : qcs) { if (qc.IsNull()) continue; - const auto* pQuorumBaseBlockIndex = m_chainstate.m_blockman.LookupBlockIndex(qc.quorumHash); + const auto* pQuorumBaseBlockIndex = m_chainman.m_blockman.LookupBlockIndex(qc.quorumHash); if (pQuorumBaseBlockIndex == nullptr) { LogPrint(BCLog::LLMQ, "[ProcessBlock] h[%d] unexpectedly failed due to no known pindex for hash[%s]\n", pindex->nHeight, qc.quorumHash.ToString()); return false; } - qc.VerifySignatureAsync({m_dmnman, m_qsnapman, m_chainstate.m_chainman, pQuorumBaseBlockIndex}, &queue_control); + qc.VerifySignatureAsync({m_dmnman, m_qsnapman, m_chainman, pQuorumBaseBlockIndex}, &queue_control); } if (!queue_control.Wait()) { @@ -276,7 +276,7 @@ bool CQuorumBlockProcessor::ProcessCommitment(int nHeight, const uint256& blockH } const auto& llmq_params = llmq_params_opt.value(); - uint256 quorumHash = GetQuorumBlockHash(llmq_params, m_chainstate.m_chain, nHeight, qc.quorumIndex); + uint256 quorumHash = GetQuorumBlockHash(llmq_params, m_chainman.ActiveChain(), nHeight, qc.quorumIndex); LogPrint(BCLog::LLMQ, /* Continued */ "%s -- processing commitment for block height=%d, type=%d, quorumIndex=%d, quorumHash=%s, signers=%s, " @@ -286,7 +286,7 @@ bool CQuorumBlockProcessor::ProcessCommitment(int nHeight, const uint256& blockH qc.CountValidMembers(), qc.quorumPublicKey.ToString(), fJustCheck); // skip `bad-qc-block` checks below when replaying blocks after the crash - if (m_chainstate.m_chain.Tip() == nullptr) { + if (m_chainman.ActiveChain().Tip() == nullptr) { quorumHash = qc.quorumHash; } @@ -324,12 +324,12 @@ bool CQuorumBlockProcessor::ProcessCommitment(int nHeight, const uint256& blockH return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "bad-qc-dup"); } - if (!IsMiningPhase(llmq_params, m_chainstate.m_chain, nHeight)) { + if (!IsMiningPhase(llmq_params, m_chainman.ActiveChain(), nHeight)) { // should not happen as it's already handled in ProcessBlock return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "bad-qc-height"); } - const auto* pQuorumBaseBlockIndex = m_chainstate.m_blockman.LookupBlockIndex(qc.quorumHash); + const auto* pQuorumBaseBlockIndex = m_chainman.m_blockman.LookupBlockIndex(qc.quorumHash); if (pQuorumBaseBlockIndex == nullptr) { LogPrint(BCLog::LLMQ, "%s -- unexpectedly failed due to no known pindex for hash[%s]\n", __func__, qc.quorumHash.ToString()); @@ -337,7 +337,7 @@ bool CQuorumBlockProcessor::ProcessCommitment(int nHeight, const uint256& blockH } // we don't validate signatures here; they already validated on previous step - if (!qc.Verify({m_dmnman, m_qsnapman, m_chainstate.m_chainman, pQuorumBaseBlockIndex}, /*checksigs=*/false)) { + if (!qc.Verify({m_dmnman, m_qsnapman, m_chainman, pQuorumBaseBlockIndex}, /*checksigs=*/false)) { LogPrint(BCLog::LLMQ, /* Continued */ "%s -- height=%d, type=%d, quorumIndex=%d, quorumHash=%s, signers=%s, validMembers=%d, " "quorumPublicKey=%s qc verify failed.\n", @@ -451,7 +451,7 @@ bool CQuorumBlockProcessor::UndoBlock(const CBlock& block, gsl::not_null qcs; if (BlockValidationState dummy; !GetCommitmentsFromBlock(block, pindex, qcs, dummy)) { @@ -535,18 +535,19 @@ size_t CQuorumBlockProcessor::GetNumCommitmentsRequired(const Consensus::LLMQPar { AssertLockHeld(::cs_main); - if (!IsMiningPhase(llmqParams, m_chainstate.m_chain, nHeight)) return 0; + if (!IsMiningPhase(llmqParams, m_chainman.ActiveChain(), nHeight)) return 0; // Note: This function can be called for new blocks - assert(nHeight <= m_chainstate.m_chain.Height() + 1); - const auto *const pindex = m_chainstate.m_chain.Height() < nHeight ? m_chainstate.m_chain.Tip() : m_chainstate.m_chain.Tip()->GetAncestor(nHeight); + const CChain& active_chain{m_chainman.ActiveChain()}; + assert(nHeight <= active_chain.Height() + 1); + const auto *const pindex = active_chain.Height() < nHeight ? active_chain.Tip() : active_chain.Tip()->GetAncestor(nHeight); bool rotation_enabled = IsQuorumRotationEnabled(llmqParams, pindex); size_t quorums_num = rotation_enabled ? llmqParams.signingActiveQuorumCount : 1; size_t ret{0}; for (const auto quorumIndex : util::irange(quorums_num)) { - uint256 quorumHash = GetQuorumBlockHash(llmqParams, m_chainstate.m_chain, nHeight, quorumIndex); + uint256 quorumHash = GetQuorumBlockHash(llmqParams, m_chainman.ActiveChain(), nHeight, quorumIndex); if (!quorumHash.IsNull() && !HasMinedCommitment(llmqParams.type, quorumHash)) ++ret; } @@ -803,18 +804,19 @@ std::optional> CQuorumBlockProcessor::GetMineableC } // Note: This function can be called for new blocks - assert(nHeight <= m_chainstate.m_chain.Height() + 1); - const auto *const pindex = m_chainstate.m_chain.Height() < nHeight ? m_chainstate.m_chain.Tip() : m_chainstate.m_chain.Tip()->GetAncestor(nHeight); + const CChain& active_chain{m_chainman.ActiveChain()}; + assert(nHeight <= active_chain.Height() + 1); + const auto *const pindex = active_chain.Height() < nHeight ? active_chain.Tip() : active_chain.Tip()->GetAncestor(nHeight); bool rotation_enabled = IsQuorumRotationEnabled(llmqParams, pindex); - bool basic_bls_enabled{DeploymentActiveAfter(pindex, m_chainstate.m_chainman.GetConsensus(), Consensus::DEPLOYMENT_V19)}; + bool basic_bls_enabled{DeploymentActiveAfter(pindex, m_chainman.GetConsensus(), Consensus::DEPLOYMENT_V19)}; size_t quorums_num = rotation_enabled ? llmqParams.signingActiveQuorumCount : 1; std::stringstream ss; for (const auto quorumIndex : util::irange(quorums_num)) { CFinalCommitment cf; - uint256 quorumHash = GetQuorumBlockHash(llmqParams, m_chainstate.m_chain, nHeight, quorumIndex); + uint256 quorumHash = GetQuorumBlockHash(llmqParams, m_chainman.ActiveChain(), nHeight, quorumIndex); if (quorumHash.IsNull()) { break; } diff --git a/src/llmq/blockprocessor.h b/src/llmq/blockprocessor.h index 6aa82bad75ce..63bf7ac70c62 100644 --- a/src/llmq/blockprocessor.h +++ b/src/llmq/blockprocessor.h @@ -25,7 +25,7 @@ class CBlock; class CBlockIndex; class CBLSSignature; class CChain; -class Chainstate; +class ChainstateManager; class CDataStream; class CDeterministicMNManager; class CEvoDB; @@ -46,7 +46,7 @@ using QcIndexedHashMap = std::map()}, qsnapman{std::make_unique(evo_db)}, - quorum_block_processor{std::make_unique(chainman.ActiveChainstate(), dmnman, evo_db, - *qsnapman, bls_threads)}, + quorum_block_processor{ + std::make_unique(chainman, dmnman, evo_db, *qsnapman, bls_threads)}, qman{std::make_unique(*bls_worker, dmnman, evo_db, *quorum_block_processor, *qsnapman, chainman, db_params)}, sigman{std::make_unique(*qman, db_params, max_recsigs_age)}, From fb7fa62f6181df0c2208222c81bd471ed30e17d5 Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Wed, 29 Jul 2026 02:36:12 +0700 Subject: [PATCH 5/7] fix: hold ChainstateManager in chainlock, instantsend, not a Chainstate reference Same defect as CQuorumBlockProcessor, in the three remaining long-lived objects that captured a Chainstate reference at construction: chainlock::ChainLockSigner active/context.cpp:45 instantsend::InstantSendSigner active/context.cpp:47 NetInstantSend init.cpp:2179 All three were handed chainman.ActiveChainstate() once and stored the result as a reference. ChainstateManager::ResetChainstates() destroys that object, so any use afterwards touches freed memory, and ActivateSnapshot() repoints the active chainstate without freeing the old one, which would leave these three working against the background chain. None is reachable from a test that resets chainstates, so unlike CQuorumBlockProcessor there is no sanitizer failure to point at. They are the same bug regardless, and leaving three of four instances in place would just invite the next one. NetInstantSend needs a mutable chainstate for InvalidateBlock, ResetBlockFailureFlags and ActivateBestChain. That still works through a const manager, because ChainstateManager::ActiveChainstate() is a const method returning a non-const reference. The LookupBlockIndex at net_instantsend.cpp:604 goes through ActiveChainstate().m_blockman rather than the manager's own m_blockman, because the const manager would select the const overload and the result has to stay writable, as the comment above it already noted. SelectQuorumForSigning keeps using ActiveChainstate().m_chain instead of ActiveChain(); the latter is annotated EXCLUSIVE_LOCKS_REQUIRED(cs_main) and that call site does not hold it, so the direct member access preserves the existing locking exactly. --- src/active/context.cpp | 4 ++-- src/chainlock/signing.cpp | 10 +++++----- src/chainlock/signing.h | 5 +++-- src/init.cpp | 2 +- src/instantsend/net_instantsend.cpp | 18 +++++++++--------- src/instantsend/net_instantsend.h | 8 ++++---- src/instantsend/signing.cpp | 8 ++++---- src/instantsend/signing.h | 5 +++-- 8 files changed, 31 insertions(+), 29 deletions(-) diff --git a/src/active/context.cpp b/src/active/context.cpp index 7dca348d11a7..c77e38fb147d 100644 --- a/src/active/context.cpp +++ b/src/active/context.cpp @@ -42,9 +42,9 @@ ActiveContext::ActiveContext(CBLSWorker& bls_worker, ChainstateManager& chainman shareman{std::make_unique(connman, chainman, sigman, *nodeman, qman, sporkman)}, gov_signer{std::make_unique(connman, dmnman, govman, superblocks, *nodeman, chainman, mn_sync)}, ehf_sighandler{std::make_unique(chainman, sigman, *shareman, qman)}, - cl_signer{std::make_unique(chainman.ActiveChainstate(), chainlocks, clhandler, isman, + cl_signer{std::make_unique(chainman, chainlocks, clhandler, isman, qman, sigman, *shareman, mn_sync)}, - is_signer{std::make_unique(chainman.ActiveChainstate(), chainlocks, isman, sigman, + is_signer{std::make_unique(chainman, chainlocks, isman, sigman, *shareman, qman, sporkman, mempool, mn_sync)} { } diff --git a/src/chainlock/signing.cpp b/src/chainlock/signing.cpp index 0eb5b58660c8..b9e2f04c5fb2 100644 --- a/src/chainlock/signing.cpp +++ b/src/chainlock/signing.cpp @@ -20,11 +20,11 @@ using node::ReadBlockFromDisk; namespace chainlock { -ChainLockSigner::ChainLockSigner(Chainstate& chainstate, const chainlock::Chainlocks& chainlocks, +ChainLockSigner::ChainLockSigner(const ChainstateManager& chainman, const chainlock::Chainlocks& chainlocks, ChainlockHandler& clhandler, const llmq::CInstantSendManager& isman, const llmq::CQuorumManager& qman, llmq::CSigningManager& sigman, llmq::CSigSharesManager& shareman, const CMasternodeSync& mn_sync) : - m_chainstate{chainstate}, + m_chainman{chainman}, m_chainlocks{chainlocks}, m_clhandler{clhandler}, m_isman{isman}, @@ -88,7 +88,7 @@ void ChainLockSigner::TrySignChainTip() return; } - const CBlockIndex* pindex = WITH_LOCK(::cs_main, return m_chainstate.m_chain.Tip()); + const CBlockIndex* pindex = WITH_LOCK(::cs_main, return m_chainman.ActiveChain().Tip()); if (!pindex || !pindex->pprev) { return; @@ -232,7 +232,7 @@ ChainLockSigner::BlockTxs::mapped_type ChainLockSigner::GetBlockTxs(const uint25 uint32_t blockTime; { LOCK(::cs_main); - const auto* pindex = m_chainstate.m_blockman.LookupBlockIndex(blockHash); + const auto* pindex = m_chainman.m_blockman.LookupBlockIndex(blockHash); if (!pindex) { return nullptr; } @@ -305,7 +305,7 @@ void ChainLockSigner::Cleanup() std::vector> removed; LOCK2(::cs_main, cs_signer); for (auto it = blockTxs.begin(); it != blockTxs.end();) { - const auto* pindex = m_chainstate.m_blockman.LookupBlockIndex(it->first); + const auto* pindex = m_chainman.m_blockman.LookupBlockIndex(it->first); if (!pindex) { it = blockTxs.erase(it); } else if (m_chainlocks.HasChainLock(pindex->nHeight, pindex->GetBlockHash())) { diff --git a/src/chainlock/signing.h b/src/chainlock/signing.h index 753d534f1237..8249ceb9a79a 100644 --- a/src/chainlock/signing.h +++ b/src/chainlock/signing.h @@ -12,6 +12,7 @@ class CScheduler; class CMasternodeSync; +class ChainstateManager; namespace llmq { class CInstantSendManager; class CRecoveredSig; @@ -25,7 +26,7 @@ class ChainlockHandler; class ChainLockSigner final : public llmq::CRecoveredSigsListener, public CValidationInterface { private: - Chainstate& m_chainstate; + const ChainstateManager& m_chainman; const chainlock::Chainlocks& m_chainlocks; ChainlockHandler& m_clhandler; const llmq::CInstantSendManager& m_isman; @@ -58,7 +59,7 @@ class ChainLockSigner final : public llmq::CRecoveredSigsListener, public CValid ChainLockSigner() = delete; ChainLockSigner(const ChainLockSigner&) = delete; ChainLockSigner& operator=(const ChainLockSigner&) = delete; - explicit ChainLockSigner(Chainstate& chainstate, const chainlock::Chainlocks& chainlocks, + explicit ChainLockSigner(const ChainstateManager& chainman, const chainlock::Chainlocks& chainlocks, ChainlockHandler& clhandler, const llmq::CInstantSendManager& isman, const llmq::CQuorumManager& qman, llmq::CSigningManager& sigman, llmq::CSigSharesManager& shareman, const CMasternodeSync& mn_sync); diff --git a/src/init.cpp b/src/init.cpp index 34b43d4f8096..56835ad468e7 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -2176,7 +2176,7 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) // ********************************************************* Step 7d: Setup other Dash services - node.peerman->AddExtraHandler(std::make_unique(node.peerman.get(), *node.llmq_ctx->isman, node.active_ctx ? node.active_ctx->is_signer.get() : nullptr, *node.llmq_ctx->sigman, *node.llmq_ctx->qman, *node.chainlocks, chainman.ActiveChainstate(), *node.mempool, *node.mn_sync)); + node.peerman->AddExtraHandler(std::make_unique(node.peerman.get(), *node.llmq_ctx->isman, node.active_ctx ? node.active_ctx->is_signer.get() : nullptr, *node.llmq_ctx->sigman, *node.llmq_ctx->qman, *node.chainlocks, chainman, *node.mempool, *node.mn_sync)); node.peerman->AddExtraHandler(std::make_unique(node.peerman.get(), *node.llmq_ctx->sigman, node.active_ctx ? node.active_ctx->shareman.get() : nullptr, *node.sporkman)); { diff --git a/src/instantsend/net_instantsend.cpp b/src/instantsend/net_instantsend.cpp index 08b67aea937f..5941d81c6fcc 100644 --- a/src/instantsend/net_instantsend.cpp +++ b/src/instantsend/net_instantsend.cpp @@ -73,12 +73,12 @@ bool NetInstantSend::ValidateIncomingISLock(const instantsend::InstantSendLock& std::optional NetInstantSend::ResolveCycleHeight(const uint256& cycle_hash) { - auto cycle_height = GetBlockHeight(m_is_manager, m_chainstate, cycle_hash); + auto cycle_height = GetBlockHeight(m_is_manager, m_chainman.ActiveChainstate(), cycle_hash); if (cycle_height) { return cycle_height; } - const auto block_index = WITH_LOCK(::cs_main, return m_chainstate.m_blockman.LookupBlockIndex(cycle_hash)); + const auto block_index = WITH_LOCK(::cs_main, return m_chainman.m_blockman.LookupBlockIndex(cycle_hash)); if (block_index == nullptr) { return std::nullopt; } @@ -131,7 +131,7 @@ std::unique_ptr NetInstantSend::BuildVeri continue; } - auto cycleHeightOpt = GetBlockHeight(m_is_manager, m_chainstate, islock->cycleHash); + auto cycleHeightOpt = GetBlockHeight(m_is_manager, m_chainman.ActiveChainstate(), islock->cycleHash); if (!cycleHeightOpt) { data->batchVerifier.badSources.emplace(nodeId); continue; @@ -145,7 +145,7 @@ std::unique_ptr NetInstantSend::BuildVeri nSignHeight = cycleHeight + dkgInterval - 1; } // For RegTest non-rotating quorum cycleHash has directly quorum hash - auto quorum = llmq_params.useRotation ? llmq::SelectQuorumForSigning(llmq_params, m_chainstate.m_chain, m_qman, + auto quorum = llmq_params.useRotation ? llmq::SelectQuorumForSigning(llmq_params, m_chainman.ActiveChainstate().m_chain, m_qman, id, nSignHeight, signOffset) : m_qman.GetQuorum(llmq_params.type, islock->cycleHash); @@ -375,7 +375,7 @@ void NetInstantSend::ProcessInstantSendLock(NodeId from, const uint256& hash, co auto tx = GetTransaction(nullptr, &m_mempool, islock->txid, Params().GetConsensus(), hashBlock); const bool found_transaction{tx != nullptr}; // we ignore failure here as we must be able to propagate the lock even if we don't have the TX locally - const auto minedHeight = GetBlockHeight(m_is_manager, m_chainstate, hashBlock); + const auto minedHeight = GetBlockHeight(m_is_manager, m_chainman.ActiveChainstate(), hashBlock); if (found_transaction) { // Let's see if the TX that was locked by this islock is already mined in a ChainLocked block. If yes, // we can simply ignore the islock, as the ChainLock implies locking of all TXs in that chain @@ -601,8 +601,8 @@ void NetInstantSend::ResolveBlockConflicts(const uint256& islockHash, const inst BlockValidationState state; // need non-const pointer - auto pindex2 = WITH_LOCK(::cs_main, return m_chainstate.m_blockman.LookupBlockIndex(pindex->GetBlockHash())); - if (!m_chainstate.InvalidateBlock(state, pindex2)) { + auto pindex2 = WITH_LOCK(::cs_main, return m_chainman.ActiveChainstate().m_blockman.LookupBlockIndex(pindex->GetBlockHash())); + if (!m_chainman.ActiveChainstate().InvalidateBlock(state, pindex2)) { LogPrintf("NetInstantSend::%s -- InvalidateBlock failed: %s\n", __func__, state.ToString()); // This should not have happened and we are in a state were it's not safe to continue anymore assert(false); @@ -612,13 +612,13 @@ void NetInstantSend::ResolveBlockConflicts(const uint256& islockHash, const inst } else { LogPrintf("NetInstantSend::%s -- resetting block %s\n", __func__, pindex2->GetBlockHash().ToString()); LOCK(::cs_main); - m_chainstate.ResetBlockFailureFlags(pindex2); + m_chainman.ActiveChainstate().ResetBlockFailureFlags(pindex2); } } if (activateBestChain) { BlockValidationState state; - if (!m_chainstate.ActivateBestChain(state)) { + if (!m_chainman.ActiveChainstate().ActivateBestChain(state)) { LogPrintf("NetInstantSend::%s -- ActivateBestChain failed: %s\n", __func__, state.ToString()); // This should not have happened and we are in a state were it's not safe to continue anymore assert(false); diff --git a/src/instantsend/net_instantsend.h b/src/instantsend/net_instantsend.h index a6c29d7a6769..9981b7ccd4f3 100644 --- a/src/instantsend/net_instantsend.h +++ b/src/instantsend/net_instantsend.h @@ -14,7 +14,7 @@ #include #include -class Chainstate; +class ChainstateManager; namespace Consensus { struct LLMQParams; @@ -44,7 +44,7 @@ class NetInstantSend final : public NetHandler, public CValidationInterface public: NetInstantSend(PeerManagerInternal* peer_manager, llmq::CInstantSendManager& is_manager, instantsend::InstantSendSigner* signer, llmq::CSigningManager& sigman, llmq::CQuorumManager& qman, - const chainlock::Chainlocks& chainlocks, Chainstate& chainstate, CTxMemPool& mempool, + const chainlock::Chainlocks& chainlocks, const ChainstateManager& chainman, CTxMemPool& mempool, const CMasternodeSync& mn_sync) : NetHandler(peer_manager), m_is_manager{is_manager}, @@ -52,7 +52,7 @@ class NetInstantSend final : public NetHandler, public CValidationInterface m_sigman{sigman}, m_qman(qman), m_chainlocks{chainlocks}, - m_chainstate{chainstate}, + m_chainman{chainman}, m_mempool{mempool}, m_mn_sync{mn_sync} { @@ -113,7 +113,7 @@ class NetInstantSend final : public NetHandler, public CValidationInterface llmq::CSigningManager& m_sigman; llmq::CQuorumManager& m_qman; const chainlock::Chainlocks& m_chainlocks; - Chainstate& m_chainstate; + const ChainstateManager& m_chainman; CTxMemPool& m_mempool; const CMasternodeSync& m_mn_sync; diff --git a/src/instantsend/signing.cpp b/src/instantsend/signing.cpp index 267debe29bd3..2dbcf9e58017 100644 --- a/src/instantsend/signing.cpp +++ b/src/instantsend/signing.cpp @@ -30,11 +30,11 @@ using node::fReindex; using node::GetTransaction; namespace instantsend { -InstantSendSigner::InstantSendSigner(Chainstate& chainstate, const chainlock::Chainlocks& chainlocks, +InstantSendSigner::InstantSendSigner(const ChainstateManager& chainman, const chainlock::Chainlocks& chainlocks, llmq::CInstantSendManager& isman, llmq::CSigningManager& sigman, llmq::CSigSharesManager& shareman, llmq::CQuorumManager& qman, CSporkManager& sporkman, CTxMemPool& mempool, const CMasternodeSync& mn_sync) : - m_chainstate{chainstate}, + m_chainman{chainman}, m_chainlocks{chainlocks}, m_isman{isman}, m_sigman{sigman}, @@ -211,7 +211,7 @@ bool InstantSendSigner::CheckCanLock(const COutPoint& outpoint, bool printDebug, if (auto ret = m_isman.GetCachedHeight(hashBlock)) { blockHeight = *ret; } else { - const CBlockIndex* pindex = WITH_LOCK(::cs_main, return m_chainstate.m_blockman.LookupBlockIndex(hashBlock)); + const CBlockIndex* pindex = WITH_LOCK(::cs_main, return m_chainman.m_blockman.LookupBlockIndex(hashBlock)); if (pindex == nullptr) { if (printDebug) { LogPrint(BCLog::INSTANTSEND, "%s -- txid=%s: failed to determine mined height for parent TX %s\n", @@ -390,7 +390,7 @@ void InstantSendSigner::TrySignInstantSendLock(const CTransaction& tx) const auto& llmq_params_opt = Params().GetLLMQ(llmqType); assert(llmq_params_opt); - const auto quorum = llmq::SelectQuorumForSigning(llmq_params_opt.value(), m_chainstate.m_chain, m_qman, id); + const auto quorum = llmq::SelectQuorumForSigning(llmq_params_opt.value(), m_chainman.ActiveChain(), m_qman, id); if (!quorum) { LogPrint(BCLog::INSTANTSEND, "%s -- failed to select quorum. islock id=%s, txid=%s\n", __func__, id.ToString(), diff --git a/src/instantsend/signing.h b/src/instantsend/signing.h index fe06d2aa7c45..9b413ed8f57b 100644 --- a/src/instantsend/signing.h +++ b/src/instantsend/signing.h @@ -12,6 +12,7 @@ #include class CMasternodeSync; +class ChainstateManager; class CSporkManager; class CTxMemPool; @@ -33,7 +34,7 @@ namespace instantsend { class InstantSendSigner final : public llmq::CRecoveredSigsListener { private: - Chainstate& m_chainstate; + const ChainstateManager& m_chainman; const chainlock::Chainlocks& m_chainlocks; llmq::CInstantSendManager& m_isman; llmq::CSigningManager& m_sigman; @@ -66,7 +67,7 @@ class InstantSendSigner final : public llmq::CRecoveredSigsListener InstantSendSigner() = delete; InstantSendSigner(const InstantSendSigner&) = delete; InstantSendSigner& operator=(const InstantSendSigner&) = delete; - explicit InstantSendSigner(Chainstate& chainstate, const chainlock::Chainlocks& chainlocks, + explicit InstantSendSigner(const ChainstateManager& chainman, const chainlock::Chainlocks& chainlocks, llmq::CInstantSendManager& isman, llmq::CSigningManager& sigman, llmq::CSigSharesManager& shareman, llmq::CQuorumManager& qman, CSporkManager& sporkman, CTxMemPool& mempool, const CMasternodeSync& mn_sync); From 0b1b0a8e54e17c89e8487e0a602c869b9270b44c Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Wed, 29 Jul 2026 02:17:30 +0700 Subject: [PATCH 6/7] fix: stop capturing wrapper parameters by reference in RPC handlers Four RPC handlers were built by a helper that takes a bool, and captured that bool with [&]. The lambda is stored in the returned RPCHelpMan and outlives the helper, so by the time a request arrives the parameter's stack slot is long gone: ERROR: AddressSanitizer: stack-use-after-return READ of size 1 ... thread T20 (d-httpworker.2) protx_register_fund_wrapper(bool)::$_0::operator() rpc/evo.cpp:528 RPCHelpMan::HandleRequest rpc/util.cpp:530 CRPCTable::execute rpc/server.cpp:516 HTTPReq_JSONRPC httprpc.cpp:242 Affected: rpc/evo.cpp:526 protx register_fund / register_fund_legacy rpc/evo.cpp:577 protx register / register_legacy rpc/evo.cpp:629 protx register_prepare / register_prepare_legacy rpc/governance.cpp:689 gobject list / gobject diff The read decides whether the deprecated legacy BLS scheme is being requested, and in gobject's case whether to return a diff, so a stale value silently picks the wrong behaviour rather than failing loudly. Capturing by value is enough; none of the four bodies uses anything else from the enclosing scope. protx_update_registrar_wrapper takes the same kind of parameter but derives the flag from self.m_name inside the lambda, so it was never affected. masternodelist_helper and rpc/node.cpp's echo() use their parameter only while building the RPCHelpMan, before the lambda exists. This is what made 33 functional tests fail under ASan: every masternode, LLMQ and governance test registers a masternode, so all of them hit protx register_fund and died in the same place. --- src/rpc/evo.cpp | 6 +++--- src/rpc/governance.cpp | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/rpc/evo.cpp b/src/rpc/evo.cpp index e15bc74a18ea..520545e6c243 100644 --- a/src/rpc/evo.cpp +++ b/src/rpc/evo.cpp @@ -529,7 +529,7 @@ static RPCHelpMan protx_register_fund_wrapper(const bool legacy) RPCExamples{ HelpExampleCli("protx", rpc_example) }, - [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue + [legacy](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue { if (legacy && !IsDeprecatedRPCEnabled("legacy_mn")) { throw std::runtime_error("DEPRECATED: Pass config option -deprecatedrpc=legacy_mn to enable this RPC"); @@ -580,7 +580,7 @@ static RPCHelpMan protx_register_wrapper(bool legacy) RPCExamples{ HelpExampleCli("protx", rpc_example), }, - [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue + [legacy](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue { if (legacy && !IsDeprecatedRPCEnabled("legacy_mn")) { throw std::runtime_error("DEPRECATED: Pass config option -deprecatedrpc=legacy_mn to enable this RPC"); @@ -632,7 +632,7 @@ static RPCHelpMan protx_register_prepare_wrapper(const bool legacy) RPCExamples{ HelpExampleCli("protx", rpc_example) }, - [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue + [legacy](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue { if (legacy && !IsDeprecatedRPCEnabled("legacy_mn")) { throw std::runtime_error("DEPRECATED: Pass config option -deprecatedrpc=legacy_mn to enable this RPC"); diff --git a/src/rpc/governance.cpp b/src/rpc/governance.cpp index a4a97f6596cb..aaa45d061f8f 100644 --- a/src/rpc/governance.cpp +++ b/src/rpc/governance.cpp @@ -686,7 +686,7 @@ static RPCHelpMan gobject_list_helper(const bool make_a_diff) }, }, RPCExamples{""}, - [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue + [make_a_diff](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue { std::string strCachedSignal = "valid"; if (!request.params[0].isNull()) { From 11108da3501783f15c73bded7b424d48d04516c9 Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Sun, 2 Aug 2026 22:16:08 +0700 Subject: [PATCH 7/7] fix: take cs_main when obtaining the active chain Holding ChainstateManager rather than a Chainstate reference meant reaching the chain through ChainstateManager::ActiveChain(), which is annotated EXCLUSIVE_LOCKS_REQUIRED(GetMutex()) with GetMutex() returning ::cs_main. Neither call site holds it, and the fuzz job builds with -Wthread-safety -Werror, so this is a hard error there: instantsend/signing.cpp:393:90: error: calling function 'ActiveChain' requires holding mutex 'cs_main' exclusively [-Werror,-Wthread-safety-analysis] Use the idiom the other two SelectQuorumForSigning callers already use, in llmq/ehf_signals.cpp and llmq/signing_shares.cpp: take cs_main just long enough to obtain the reference, then call with it held no longer. In BuildVerificationBatch the reference is hoisted out of the loop over pending locks so the lock is taken once rather than per entry. net_instantsend.cpp reached the chain as ActiveChainstate().m_chain, which compiles because the member carries no annotation. That is the same unlocked access, only hidden from the analyser, so it is converted too. This deliberately does not change what is serialised. The traversal inside SelectQuorumForSigning still runs without cs_main, exactly as it did before the refactor, when both sites read m_chainstate.m_chain directly. Holding cs_main across the whole call would be a real behaviour change: it takes cs_main on the InstantSend signing path, which is where lock order inversions live, and belongs in its own change with a tsan run behind it rather than in a refactor. --- src/instantsend/net_instantsend.cpp | 4 +++- src/instantsend/signing.cpp | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/instantsend/net_instantsend.cpp b/src/instantsend/net_instantsend.cpp index 5941d81c6fcc..07e86fd780f4 100644 --- a/src/instantsend/net_instantsend.cpp +++ b/src/instantsend/net_instantsend.cpp @@ -108,6 +108,8 @@ std::unique_ptr NetInstantSend::BuildVeri { auto data = std::make_unique(); + const CChain& active_chain = *WITH_LOCK(::cs_main, return &m_chainman.ActiveChain()); + for (const auto& pending : pend) { const auto& hash = pending.islock_hash; auto nodeId = pending.node_id; @@ -145,7 +147,7 @@ std::unique_ptr NetInstantSend::BuildVeri nSignHeight = cycleHeight + dkgInterval - 1; } // For RegTest non-rotating quorum cycleHash has directly quorum hash - auto quorum = llmq_params.useRotation ? llmq::SelectQuorumForSigning(llmq_params, m_chainman.ActiveChainstate().m_chain, m_qman, + auto quorum = llmq_params.useRotation ? llmq::SelectQuorumForSigning(llmq_params, active_chain, m_qman, id, nSignHeight, signOffset) : m_qman.GetQuorum(llmq_params.type, islock->cycleHash); diff --git a/src/instantsend/signing.cpp b/src/instantsend/signing.cpp index 2dbcf9e58017..5ff589adb757 100644 --- a/src/instantsend/signing.cpp +++ b/src/instantsend/signing.cpp @@ -390,7 +390,8 @@ void InstantSendSigner::TrySignInstantSendLock(const CTransaction& tx) const auto& llmq_params_opt = Params().GetLLMQ(llmqType); assert(llmq_params_opt); - const auto quorum = llmq::SelectQuorumForSigning(llmq_params_opt.value(), m_chainman.ActiveChain(), m_qman, id); + const CChain& active_chain = *WITH_LOCK(::cs_main, return &m_chainman.ActiveChain()); + const auto quorum = llmq::SelectQuorumForSigning(llmq_params_opt.value(), active_chain, m_qman, id); if (!quorum) { LogPrint(BCLog::INSTANTSEND, "%s -- failed to select quorum. islock id=%s, txid=%s\n", __func__, id.ToString(),