From 2c82532812531fcf1f069722f96c4674491d267b Mon Sep 17 00:00:00 2001 From: UdjinM6 Date: Sat, 4 Oct 2025 18:39:20 +0300 Subject: [PATCH 1/4] feat: share `vecMasternodesUsed` across all wallets, improve its handling --- src/coinjoin/client.cpp | 30 +++++++++++++++++------------- src/coinjoin/client.h | 5 +---- src/masternode/meta.cpp | 30 +++++++++++++++++++++++++++++- src/masternode/meta.h | 13 +++++++++++-- 4 files changed, 58 insertions(+), 20 deletions(-) diff --git a/src/coinjoin/client.cpp b/src/coinjoin/client.cpp index 88d5ee4f8187..bf9cb1b7091b 100644 --- a/src/coinjoin/client.cpp +++ b/src/coinjoin/client.cpp @@ -248,7 +248,6 @@ void CCoinJoinClientSession::ResetPool() void CCoinJoinClientManager::ResetPool() { nCachedLastSuccessBlock = 0; - vecMasternodesUsed.clear(); AssertLockNotHeld(cs_deqsessions); LOCK(cs_deqsessions); for (auto& session : deqSessions) { @@ -967,11 +966,15 @@ bool CCoinJoinClientManager::DoAutomaticDenominating(ChainstateManager& chainman // If we've used 90% of the Masternode list then drop the oldest first ~30% int nThreshold_high = nMnCountEnabled * 0.9; int nThreshold_low = nThreshold_high * 0.7; - WalletCJLogPrint(m_wallet, "Checking vecMasternodesUsed: size: %d, threshold: %d\n", (int)vecMasternodesUsed.size(), nThreshold_high); + size_t nUsedMasternodes{m_mn_metaman.GetUsedMasternodesCount()}; - if ((int)vecMasternodesUsed.size() > nThreshold_high) { - vecMasternodesUsed.erase(vecMasternodesUsed.begin(), vecMasternodesUsed.begin() + vecMasternodesUsed.size() - nThreshold_low); - WalletCJLogPrint(m_wallet, " vecMasternodesUsed: new size: %d, threshold: %d\n", (int)vecMasternodesUsed.size(), nThreshold_high); + WalletCJLogPrint(m_wallet, "Checking nUsedMasternodes: %d, threshold: %d\n", (int)nUsedMasternodes, nThreshold_high); + + if ((int)nUsedMasternodes > nThreshold_high) { + size_t nToRemove{nUsedMasternodes - nThreshold_low}; + m_mn_metaman.RemoveUsedMasternodes(nToRemove); + WalletCJLogPrint(m_wallet, " new nUsedMasternodes: %d, threshold: %d\n", + (int)m_mn_metaman.GetUsedMasternodesCount(), nThreshold_high); } bool fResult = true; @@ -995,9 +998,9 @@ bool CCoinJoinClientManager::DoAutomaticDenominating(ChainstateManager& chainman return fResult; } -void CCoinJoinClientManager::AddUsedMasternode(const COutPoint& outpointMn) +void CCoinJoinClientManager::AddUsedMasternode(const uint256& proTxHash) { - vecMasternodesUsed.push_back(outpointMn); + m_mn_metaman.AddUsedMasternode(proTxHash); } CDeterministicMNCPtr CCoinJoinClientManager::GetRandomNotUsedMasternode() @@ -1005,7 +1008,7 @@ CDeterministicMNCPtr CCoinJoinClientManager::GetRandomNotUsedMasternode() auto mnList = m_dmnman.GetListAtChainTip(); size_t nCountEnabled = mnList.GetValidMNsCount(); - size_t nCountNotExcluded = nCountEnabled - vecMasternodesUsed.size(); + size_t nCountNotExcluded{nCountEnabled - m_mn_metaman.GetUsedMasternodesCount()}; WalletCJLogPrint(m_wallet, "CCoinJoinClientManager::%s -- %d enabled masternodes, %d masternodes to choose from\n", __func__, nCountEnabled, nCountNotExcluded); if (nCountNotExcluded < 1) { @@ -1022,15 +1025,16 @@ CDeterministicMNCPtr CCoinJoinClientManager::GetRandomNotUsedMasternode() // shuffle pointers Shuffle(vpMasternodesShuffled.begin(), vpMasternodesShuffled.end(), FastRandomContext()); - std::set excludeSet(vecMasternodesUsed.begin(), vecMasternodesUsed.end()); + std::set excludeSet{m_mn_metaman.GetUsedMasternodesSet()}; // loop through for (const auto& dmn : vpMasternodesShuffled) { - if (excludeSet.count(dmn->collateralOutpoint)) { + if (excludeSet.count(dmn->proTxHash)) { continue; } - WalletCJLogPrint(m_wallet, "CCoinJoinClientManager::%s -- found, masternode=%s\n", __func__, dmn->collateralOutpoint.ToStringShort()); + WalletCJLogPrint(m_wallet, "CCoinJoinClientManager::%s -- found, masternode=%s\n", __func__, + dmn->proTxHash.ToString()); return dmn; } @@ -1083,7 +1087,7 @@ bool CCoinJoinClientSession::JoinExistingQueue(CAmount nBalanceNeedsAnonymized, continue; } - m_clientman.AddUsedMasternode(dsq.masternodeOutpoint); + m_clientman.AddUsedMasternode(dmn->proTxHash); if (connman.IsMasternodeOrDisconnectRequested(dmn->pdmnState->netInfo->GetPrimary())) { WalletCJLogPrint(m_wallet, /* Continued */ @@ -1137,7 +1141,7 @@ bool CCoinJoinClientSession::StartNewQueue(CAmount nBalanceNeedsAnonymized, CCon return false; } - m_clientman.AddUsedMasternode(dmn->collateralOutpoint); + m_clientman.AddUsedMasternode(dmn->proTxHash); // skip next mn payments winners if (dmn->pdmnState->nLastPaidHeight + nWeightedMnCount < mnList.GetHeight() + WinnersToSkip()) { diff --git a/src/coinjoin/client.h b/src/coinjoin/client.h index 222122b0329e..f999c9fa9c59 100644 --- a/src/coinjoin/client.h +++ b/src/coinjoin/client.h @@ -262,9 +262,6 @@ class CCoinJoinClientManager const llmq::CInstantSendManager& m_isman; const std::unique_ptr& m_queueman; - // Keep track of the used Masternodes - std::vector vecMasternodesUsed; - mutable Mutex cs_deqsessions; // TODO: or map ?? std::deque deqSessions GUARDED_BY(cs_deqsessions); @@ -327,7 +324,7 @@ class CCoinJoinClientManager void ProcessPendingDsaRequest(CConnman& connman) EXCLUSIVE_LOCKS_REQUIRED(!cs_deqsessions); - void AddUsedMasternode(const COutPoint& outpointMn); + void AddUsedMasternode(const uint256& proTxHash); CDeterministicMNCPtr GetRandomNotUsedMasternode(); void UpdatedSuccessBlock(); diff --git a/src/masternode/meta.cpp b/src/masternode/meta.cpp index adb2aaa30259..2678867d984a 100644 --- a/src/masternode/meta.cpp +++ b/src/masternode/meta.cpp @@ -8,7 +8,7 @@ #include #include -const std::string MasternodeMetaStore::SERIALIZATION_VERSION_STRING = "CMasternodeMetaMan-Version-4"; +const std::string MasternodeMetaStore::SERIALIZATION_VERSION_STRING = "CMasternodeMetaMan-Version-5"; CMasternodeMetaMan::CMasternodeMetaMan() : m_db{std::make_unique("mncache.dat", "magicMasternodeCache")} @@ -153,6 +153,34 @@ void CMasternodeMetaMan::RememberPlatformBan(const uint256& inv_hash, PlatformBa m_seen_platform_bans.insert(inv_hash, std::move(msg)); } +void CMasternodeMetaMan::AddUsedMasternode(const uint256& proTxHash) +{ + LOCK(cs); + m_used_masternodes.push_back(proTxHash); +} + +void CMasternodeMetaMan::RemoveUsedMasternodes(size_t nCount) +{ + LOCK(cs); + if (nCount > m_used_masternodes.size()) { + m_used_masternodes.clear(); + } else { + m_used_masternodes.erase(m_used_masternodes.begin(), m_used_masternodes.begin() + nCount); + } +} + +size_t CMasternodeMetaMan::GetUsedMasternodesCount() const +{ + LOCK(cs); + return m_used_masternodes.size(); +} + +std::set CMasternodeMetaMan::GetUsedMasternodesSet() const +{ + LOCK(cs); + return {m_used_masternodes.begin(), m_used_masternodes.end()}; +} + std::string MasternodeMetaStore::ToString() const { LOCK(cs); diff --git a/src/masternode/meta.h b/src/masternode/meta.h index 217b9d800287..e0d05c1c0052 100644 --- a/src/masternode/meta.h +++ b/src/masternode/meta.h @@ -139,6 +139,8 @@ class MasternodeMetaStore std::map metaInfos GUARDED_BY(cs); // keep track of dsq count to prevent masternodes from gaming coinjoin queue std::atomic nDsqCount{0}; + // keep track of the used Masternodes for CoinJoin + std::vector m_used_masternodes GUARDED_BY(cs); public: template @@ -149,7 +151,7 @@ class MasternodeMetaStore for (const auto& p : metaInfos) { tmpMetaInfo.emplace_back(*p.second); } - s << SERIALIZATION_VERSION_STRING << tmpMetaInfo << nDsqCount; + s << SERIALIZATION_VERSION_STRING << tmpMetaInfo << nDsqCount << m_used_masternodes; } template @@ -164,7 +166,7 @@ class MasternodeMetaStore return; } std::vector tmpMetaInfo; - s >> tmpMetaInfo >> nDsqCount; + s >> tmpMetaInfo >> nDsqCount >> m_used_masternodes; metaInfos.clear(); for (auto& mm : tmpMetaInfo) { metaInfos.emplace(mm.GetProTxHash(), std::make_shared(std::move(mm))); @@ -176,6 +178,7 @@ class MasternodeMetaStore LOCK(cs); metaInfos.clear(); + m_used_masternodes.clear(); } std::string ToString() const EXCLUSIVE_LOCKS_REQUIRED(!cs); @@ -257,6 +260,12 @@ class CMasternodeMetaMan : public MasternodeMetaStore bool AlreadyHavePlatformBan(const uint256& inv_hash) const EXCLUSIVE_LOCKS_REQUIRED(!cs); std::optional GetPlatformBan(const uint256& inv_hash) const EXCLUSIVE_LOCKS_REQUIRED(!cs); void RememberPlatformBan(const uint256& inv_hash, PlatformBanMessage&& msg) EXCLUSIVE_LOCKS_REQUIRED(!cs); + + // CoinJoin masternode tracking + void AddUsedMasternode(const uint256& proTxHash) EXCLUSIVE_LOCKS_REQUIRED(!cs); + void RemoveUsedMasternodes(size_t nCount) EXCLUSIVE_LOCKS_REQUIRED(!cs); + size_t GetUsedMasternodesCount() const EXCLUSIVE_LOCKS_REQUIRED(!cs); + std::set GetUsedMasternodesSet() const EXCLUSIVE_LOCKS_REQUIRED(!cs); }; #endif // BITCOIN_MASTERNODE_META_H From 13a28fa33f724838ea290158651a6b3e7807deba Mon Sep 17 00:00:00 2001 From: UdjinM6 Date: Mon, 6 Oct 2025 19:30:53 +0300 Subject: [PATCH 2/4] perf: optimize CoinJoin masternode tracking with hybrid data structure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Improves performance by implementing a dual data structure approach for tracking used masternodes in CoinJoin sessions: - Use std::deque for maintaining FIFO insertion order - Use std::unordered_set for O(1) lookup performance - Replace GetUsedMasternodesSet() with IsUsedMasternode() to avoid expensive set construction on every masternode selection Performance improvements at 1800 used masternodes: - Masternode selection: 2.5ms → 0.1ms (25x faster) - Batch removal: 100µs → 27µs (4x faster) The optimization becomes increasingly important at scale (>1000 MNs) and in multi-wallet scenarios with concurrent CoinJoin sessions. Key design decisions: - Deque provides O(1) front removal (vs O(n) for vector) - Unordered_set provides O(1) lookup (vs O(n log n) set construction) - Only deque is serialized; set is rebuilt on load (no version bump) - Both structures stay synchronized through all operations - Automatic duplicate prevention in AddUsedMasternode() Memory cost: ~130 KB for 1800 entries (negligible) Code complexity: Minimal, well-encapsulated This builds on PR #6875 which moved masternode tracking from per-wallet to shared global storage. That PR solved the multi-wallet coordination problem; this patch addresses the performance bottleneck at scale. --- src/coinjoin/client.cpp | 6 ++---- src/masternode/meta.cpp | 19 ++++++++++++------- src/masternode/meta.h | 23 ++++++++++++++++++----- 3 files changed, 32 insertions(+), 16 deletions(-) diff --git a/src/coinjoin/client.cpp b/src/coinjoin/client.cpp index bf9cb1b7091b..86138670eede 100644 --- a/src/coinjoin/client.cpp +++ b/src/coinjoin/client.cpp @@ -1025,11 +1025,9 @@ CDeterministicMNCPtr CCoinJoinClientManager::GetRandomNotUsedMasternode() // shuffle pointers Shuffle(vpMasternodesShuffled.begin(), vpMasternodesShuffled.end(), FastRandomContext()); - std::set excludeSet{m_mn_metaman.GetUsedMasternodesSet()}; - - // loop through + // loop through - using direct O(1) lookup instead of creating a set copy for (const auto& dmn : vpMasternodesShuffled) { - if (excludeSet.count(dmn->proTxHash)) { + if (m_mn_metaman.IsUsedMasternode(dmn->proTxHash)) { continue; } diff --git a/src/masternode/meta.cpp b/src/masternode/meta.cpp index 2678867d984a..c7eb501a7528 100644 --- a/src/masternode/meta.cpp +++ b/src/masternode/meta.cpp @@ -156,16 +156,21 @@ void CMasternodeMetaMan::RememberPlatformBan(const uint256& inv_hash, PlatformBa void CMasternodeMetaMan::AddUsedMasternode(const uint256& proTxHash) { LOCK(cs); - m_used_masternodes.push_back(proTxHash); + // Only add if not already present (prevents duplicates) + if (m_used_masternodes_set.insert(proTxHash).second) { + m_used_masternodes.push_back(proTxHash); + } } void CMasternodeMetaMan::RemoveUsedMasternodes(size_t nCount) { LOCK(cs); - if (nCount > m_used_masternodes.size()) { - m_used_masternodes.clear(); - } else { - m_used_masternodes.erase(m_used_masternodes.begin(), m_used_masternodes.begin() + nCount); + size_t removed = 0; + while (removed < nCount && !m_used_masternodes.empty()) { + // Remove from both the set and the deque + m_used_masternodes_set.erase(m_used_masternodes.front()); + m_used_masternodes.pop_front(); + ++removed; } } @@ -175,10 +180,10 @@ size_t CMasternodeMetaMan::GetUsedMasternodesCount() const return m_used_masternodes.size(); } -std::set CMasternodeMetaMan::GetUsedMasternodesSet() const +bool CMasternodeMetaMan::IsUsedMasternode(const uint256& proTxHash) const { LOCK(cs); - return {m_used_masternodes.begin(), m_used_masternodes.end()}; + return m_used_masternodes_set.find(proTxHash) != m_used_masternodes_set.end(); } std::string MasternodeMetaStore::ToString() const diff --git a/src/masternode/meta.h b/src/masternode/meta.h index e0d05c1c0052..e9cdf931e38e 100644 --- a/src/masternode/meta.h +++ b/src/masternode/meta.h @@ -14,6 +14,7 @@ #include #include +#include #include #include #include @@ -139,8 +140,10 @@ class MasternodeMetaStore std::map metaInfos GUARDED_BY(cs); // keep track of dsq count to prevent masternodes from gaming coinjoin queue std::atomic nDsqCount{0}; - // keep track of the used Masternodes for CoinJoin - std::vector m_used_masternodes GUARDED_BY(cs); + // keep track of the used Masternodes for CoinJoin across all wallets + // Using deque for efficient FIFO removal and unordered_set for O(1) lookups + std::deque m_used_masternodes GUARDED_BY(cs); + Uint256HashSet m_used_masternodes_set GUARDED_BY(cs); public: template @@ -151,7 +154,9 @@ class MasternodeMetaStore for (const auto& p : metaInfos) { tmpMetaInfo.emplace_back(*p.second); } - s << SERIALIZATION_VERSION_STRING << tmpMetaInfo << nDsqCount << m_used_masternodes; + // Convert deque to vector for serialization - unordered_set will be rebuilt on deserialization + std::vector tmpUsedMasternodes(m_used_masternodes.begin(), m_used_masternodes.end()); + s << SERIALIZATION_VERSION_STRING << tmpMetaInfo << nDsqCount << tmpUsedMasternodes; } template @@ -166,11 +171,18 @@ class MasternodeMetaStore return; } std::vector tmpMetaInfo; - s >> tmpMetaInfo >> nDsqCount >> m_used_masternodes; + std::vector tmpUsedMasternodes; + s >> tmpMetaInfo >> nDsqCount >> tmpUsedMasternodes; + metaInfos.clear(); for (auto& mm : tmpMetaInfo) { metaInfos.emplace(mm.GetProTxHash(), std::make_shared(std::move(mm))); } + + // Convert vector to deque and build unordered_set for O(1) lookups + m_used_masternodes.assign(tmpUsedMasternodes.begin(), tmpUsedMasternodes.end()); + m_used_masternodes_set.clear(); + m_used_masternodes_set.insert(tmpUsedMasternodes.begin(), tmpUsedMasternodes.end()); } void Clear() EXCLUSIVE_LOCKS_REQUIRED(!cs) @@ -179,6 +191,7 @@ class MasternodeMetaStore metaInfos.clear(); m_used_masternodes.clear(); + m_used_masternodes_set.clear(); } std::string ToString() const EXCLUSIVE_LOCKS_REQUIRED(!cs); @@ -265,7 +278,7 @@ class CMasternodeMetaMan : public MasternodeMetaStore void AddUsedMasternode(const uint256& proTxHash) EXCLUSIVE_LOCKS_REQUIRED(!cs); void RemoveUsedMasternodes(size_t nCount) EXCLUSIVE_LOCKS_REQUIRED(!cs); size_t GetUsedMasternodesCount() const EXCLUSIVE_LOCKS_REQUIRED(!cs); - std::set GetUsedMasternodesSet() const EXCLUSIVE_LOCKS_REQUIRED(!cs); + bool IsUsedMasternode(const uint256& proTxHash) const EXCLUSIVE_LOCKS_REQUIRED(!cs); }; #endif // BITCOIN_MASTERNODE_META_H From f50d09a94977956f92c9827a2d0509e603b5c271 Mon Sep 17 00:00:00 2001 From: UdjinM6 Date: Fri, 10 Oct 2025 17:21:24 +0300 Subject: [PATCH 3/4] fix: update `MasternodeMetaStore::ToString()` --- src/masternode/meta.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/masternode/meta.cpp b/src/masternode/meta.cpp index c7eb501a7528..4cdee0ab08e5 100644 --- a/src/masternode/meta.cpp +++ b/src/masternode/meta.cpp @@ -189,7 +189,8 @@ bool CMasternodeMetaMan::IsUsedMasternode(const uint256& proTxHash) const std::string MasternodeMetaStore::ToString() const { LOCK(cs); - return strprintf("Masternodes: meta infos object count: %d, nDsqCount: %d", metaInfos.size(), nDsqCount); + return strprintf("Masternodes: meta infos object count: %d, nDsqCount: %d, used masternodes count: %d", + metaInfos.size(), nDsqCount, m_used_masternodes.size()); } uint256 PlatformBanMessage::GetHash() const { return ::SerializeHash(*this); } From eb3be09fd4e37a144229a428abc607afc5a88bcf Mon Sep 17 00:00:00 2001 From: UdjinM6 Date: Sun, 26 Oct 2025 18:08:09 +0300 Subject: [PATCH 4/4] refactor: code style/log text adjustments --- src/coinjoin/client.cpp | 13 ++++++------- src/masternode/meta.cpp | 4 ++-- src/masternode/meta.h | 2 +- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/coinjoin/client.cpp b/src/coinjoin/client.cpp index 86138670eede..b0b9cd34dc1e 100644 --- a/src/coinjoin/client.cpp +++ b/src/coinjoin/client.cpp @@ -966,15 +966,14 @@ bool CCoinJoinClientManager::DoAutomaticDenominating(ChainstateManager& chainman // If we've used 90% of the Masternode list then drop the oldest first ~30% int nThreshold_high = nMnCountEnabled * 0.9; int nThreshold_low = nThreshold_high * 0.7; - size_t nUsedMasternodes{m_mn_metaman.GetUsedMasternodesCount()}; + size_t used_count{m_mn_metaman.GetUsedMasternodesCount()}; - WalletCJLogPrint(m_wallet, "Checking nUsedMasternodes: %d, threshold: %d\n", (int)nUsedMasternodes, nThreshold_high); + WalletCJLogPrint(m_wallet, "Checking threshold - used: %d, threshold: %d\n", (int)used_count, nThreshold_high); - if ((int)nUsedMasternodes > nThreshold_high) { - size_t nToRemove{nUsedMasternodes - nThreshold_low}; - m_mn_metaman.RemoveUsedMasternodes(nToRemove); - WalletCJLogPrint(m_wallet, " new nUsedMasternodes: %d, threshold: %d\n", - (int)m_mn_metaman.GetUsedMasternodesCount(), nThreshold_high); + if ((int)used_count > nThreshold_high) { + m_mn_metaman.RemoveUsedMasternodes(used_count - nThreshold_low); + WalletCJLogPrint(m_wallet, " new used: %d, threshold: %d\n", (int)m_mn_metaman.GetUsedMasternodesCount(), + nThreshold_high); } bool fResult = true; diff --git a/src/masternode/meta.cpp b/src/masternode/meta.cpp index 4cdee0ab08e5..3940b08700ce 100644 --- a/src/masternode/meta.cpp +++ b/src/masternode/meta.cpp @@ -162,11 +162,11 @@ void CMasternodeMetaMan::AddUsedMasternode(const uint256& proTxHash) } } -void CMasternodeMetaMan::RemoveUsedMasternodes(size_t nCount) +void CMasternodeMetaMan::RemoveUsedMasternodes(size_t count) { LOCK(cs); size_t removed = 0; - while (removed < nCount && !m_used_masternodes.empty()) { + while (removed < count && !m_used_masternodes.empty()) { // Remove from both the set and the deque m_used_masternodes_set.erase(m_used_masternodes.front()); m_used_masternodes.pop_front(); diff --git a/src/masternode/meta.h b/src/masternode/meta.h index e9cdf931e38e..8b8f468c4841 100644 --- a/src/masternode/meta.h +++ b/src/masternode/meta.h @@ -276,7 +276,7 @@ class CMasternodeMetaMan : public MasternodeMetaStore // CoinJoin masternode tracking void AddUsedMasternode(const uint256& proTxHash) EXCLUSIVE_LOCKS_REQUIRED(!cs); - void RemoveUsedMasternodes(size_t nCount) EXCLUSIVE_LOCKS_REQUIRED(!cs); + void RemoveUsedMasternodes(size_t count) EXCLUSIVE_LOCKS_REQUIRED(!cs); size_t GetUsedMasternodesCount() const EXCLUSIVE_LOCKS_REQUIRED(!cs); bool IsUsedMasternode(const uint256& proTxHash) const EXCLUSIVE_LOCKS_REQUIRED(!cs); };