From d6ea4a8514e9aa132d6205a4ddd8365257874dcd Mon Sep 17 00:00:00 2001 From: Navid Rahimi Date: Sun, 23 Aug 2026 23:39:15 +0000 Subject: [PATCH] LLMQ: Bound seen ChainLock cache --- src/llmq/quorums_chainlocks.cpp | 14 ++-- src/llmq/quorums_chainlocks.h | 8 ++- src/test/CMakeLists.txt | 1 + src/test/llmq_chainlock_cache_tests.cpp | 86 +++++++++++++++++++++++++ 4 files changed, 103 insertions(+), 6 deletions(-) create mode 100644 src/test/llmq_chainlock_cache_tests.cpp diff --git a/src/llmq/quorums_chainlocks.cpp b/src/llmq/quorums_chainlocks.cpp index 3694966879..89c8140f38 100644 --- a/src/llmq/quorums_chainlocks.cpp +++ b/src/llmq/quorums_chainlocks.cpp @@ -29,7 +29,8 @@ std::string CChainLockSig::ToString() const } CChainLocksHandler::CChainLocksHandler(CScheduler* _scheduler) : - scheduler(_scheduler) + scheduler(_scheduler), + seenChainLocks(MAX_SEEN_CHAINLOCKS) { } @@ -61,7 +62,8 @@ void CChainLocksHandler::Stop() bool CChainLocksHandler::AlreadyHave(const CInv& inv) { LOCK(cs); - return seenChainLocks.count(inv.hash) != 0; + return seenChainLocks.count(inv.hash) != 0 || + (bestChainLock.nHeight != -1 && inv.hash == bestChainLockHash); } bool CChainLocksHandler::GetChainLockByHash(const uint256& hash, llmq::CChainLockSig& ret) @@ -101,9 +103,10 @@ void CChainLocksHandler::ProcessNewChainLock(NodeId from, const llmq::CChainLock { LOCK(cs); - if (!seenChainLocks.emplace(hash, GetTimeMillis()).second) { + if (seenChainLocks.count(hash) != 0) { return; } + seenChainLocks.insert(std::make_pair(hash, GetTimeMillis())); if (bestChainLock.nHeight != -1 && clsig.nHeight <= bestChainLock.nHeight) { // no need to process/relay older CLSIGs @@ -687,7 +690,9 @@ void CChainLocksHandler::Cleanup() for (auto it = seenChainLocks.begin(); it != seenChainLocks.end(); ) { if (GetTimeMillis() - it->second >= CLEANUP_SEEN_TIMEOUT) { - it = seenChainLocks.erase(it); + const uint256 hash = it->first; + ++it; + seenChainLocks.erase(hash); } else { ++it; } @@ -735,4 +740,3 @@ bool IsChainlocksEnabled() if (!llmq::chainLocksHandler) return false; return llmq::chainLocksHandler->IsChainlocksEnabled(); } - diff --git a/src/llmq/quorums_chainlocks.h b/src/llmq/quorums_chainlocks.h index c2cfbd8619..78171146bb 100644 --- a/src/llmq/quorums_chainlocks.h +++ b/src/llmq/quorums_chainlocks.h @@ -8,6 +8,7 @@ #include "llmq/quorums.h" #include "llmq/quorums_signing.h" +#include "limitedmap.h" #include "net.h" #include "chainparams.h" @@ -20,6 +21,8 @@ class CScheduler; namespace llmq { +struct CChainLocksHandlerTestAccess; + class CChainLockSig { public: @@ -43,8 +46,11 @@ class CChainLockSig class CChainLocksHandler : public CRecoveredSigsListener { + friend struct CChainLocksHandlerTestAccess; + static const int64_t CLEANUP_INTERVAL = 1000 * 30; static const int64_t CLEANUP_SEEN_TIMEOUT = 24 * 60 * 60 * 1000; + static constexpr size_t MAX_SEEN_CHAINLOCKS{1024}; // how long to wait for ixlocks until we consider a block with non-ixlocked TXs to be safe to sign static const int64_t WAIT_FOR_ISLOCK_TIMEOUT = 10 * 60; @@ -72,7 +78,7 @@ class CChainLocksHandler : public CRecoveredSigsListener BlockTxs blockTxs; std::unordered_map txFirstSeenTime; - std::map seenChainLocks; + limitedmap seenChainLocks; int64_t lastCleanupTime{0}; diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt index 6408280c81..33ff58ea74 100644 --- a/src/test/CMakeLists.txt +++ b/src/test/CMakeLists.txt @@ -48,6 +48,7 @@ add_executable(test_firo ${CMAKE_CURRENT_SOURCE_DIR}/key_tests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/dbwrapper_tests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/limitedmap_tests.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/llmq_chainlock_cache_tests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/logging_tests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/llmq_signing_shares_tests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/main_tests.cpp diff --git a/src/test/llmq_chainlock_cache_tests.cpp b/src/test/llmq_chainlock_cache_tests.cpp new file mode 100644 index 0000000000..3780c737ec --- /dev/null +++ b/src/test/llmq_chainlock_cache_tests.cpp @@ -0,0 +1,86 @@ +// Copyright (c) 2026 The Firo developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include "llmq/quorums_chainlocks.h" +#include "test/test_bitcoin.h" + +#include + +#include + +namespace llmq +{ + +struct CChainLocksHandlerTestAccess +{ + static size_t SeenCount(CChainLocksHandler& handler) + { + LOCK(handler.cs); + return handler.seenChainLocks.size(); + } + + static bool SetSeenTime(CChainLocksHandler& handler, const uint256& hash, int64_t time) + { + LOCK(handler.cs); + auto it = handler.seenChainLocks.find(hash); + if (it == handler.seenChainLocks.end()) { + return false; + } + handler.seenChainLocks.update(it, time); + return true; + } + + static void SetBest(CChainLocksHandler& handler, const CChainLockSig& clsig, const uint256& hash) + { + LOCK(handler.cs); + handler.bestChainLock = clsig; + handler.bestChainLockHash = hash; + } +}; + +} // namespace llmq + +BOOST_FIXTURE_TEST_SUITE(llmq_chainlock_cache_tests, TestingSetup) + +BOOST_AUTO_TEST_CASE(stale_chainlock_cache_is_bounded) +{ + constexpr size_t MAX_SEEN_CHAINLOCKS{1024}; + + llmq::CChainLockSig bestChainLock; + bestChainLock.nHeight = 100; + bestChainLock.blockHash = uint256S("1"); + const uint256 bestHash = ::SerializeHash(bestChainLock); + llmq::CChainLocksHandlerTestAccess::SetBest(*llmq::chainLocksHandler, bestChainLock, bestHash); + + BOOST_CHECK(llmq::chainLocksHandler->AlreadyHave(CInv(MSG_CLSIG, bestHash))); + + uint256 firstStaleHash; + std::vector recentStaleHashes; + for (size_t i = 0; i <= MAX_SEEN_CHAINLOCKS; ++i) { + llmq::CChainLockSig staleChainLock; + staleChainLock.nHeight = bestChainLock.nHeight; + staleChainLock.blockHash = uint256S(strprintf("%064x", static_cast(i + 2))); + const uint256 hash = ::SerializeHash(staleChainLock); + if (i == 0) { + firstStaleHash = hash; + } + if (i >= MAX_SEEN_CHAINLOCKS - 1) { + recentStaleHashes.emplace_back(hash); + } + + llmq::chainLocksHandler->ProcessNewChainLock(-1, staleChainLock, hash); + BOOST_REQUIRE(llmq::CChainLocksHandlerTestAccess::SetSeenTime( + *llmq::chainLocksHandler, hash, static_cast(i))); + BOOST_CHECK_LE(llmq::CChainLocksHandlerTestAccess::SeenCount(*llmq::chainLocksHandler), MAX_SEEN_CHAINLOCKS); + } + + BOOST_CHECK_EQUAL(llmq::CChainLocksHandlerTestAccess::SeenCount(*llmq::chainLocksHandler), MAX_SEEN_CHAINLOCKS); + BOOST_CHECK(!llmq::chainLocksHandler->AlreadyHave(CInv(MSG_CLSIG, firstStaleHash))); + for (const uint256& hash : recentStaleHashes) { + BOOST_CHECK(llmq::chainLocksHandler->AlreadyHave(CInv(MSG_CLSIG, hash))); + } + BOOST_CHECK(llmq::chainLocksHandler->AlreadyHave(CInv(MSG_CLSIG, bestHash))); +} + +BOOST_AUTO_TEST_SUITE_END()