-
Notifications
You must be signed in to change notification settings - Fork 1.2k
fix(net): amortize ChainLock seen-cache pruning #7482
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -31,6 +31,11 @@ class unordered_limitedmap | |
| size_type nPruneAfterSize; | ||
|
|
||
| public: | ||
| //! nMaxSizeIn is the number of elements retained after a prune. nPruneAfterSizeIn is the size | ||
| //! the map may grow to before the next insertion prunes it; it defaults to nMaxSizeIn, which | ||
| //! means prune() -- and therefore a sort of every element -- runs on *every* insertion past | ||
| //! nMaxSizeIn. Callers whose keys are attacker-supplied should pass a larger value (e.g. | ||
| //! 2 * nMaxSizeIn) so that sorting is amortised over a batch of evictions instead. | ||
| explicit unordered_limitedmap(size_type nMaxSizeIn, size_type nPruneAfterSizeIn = 0) | ||
| { | ||
| assert(nMaxSizeIn > 0); | ||
|
|
@@ -68,7 +73,11 @@ class unordered_limitedmap | |
| return; | ||
| itTarget->second = v; | ||
| } | ||
| //! Number of elements retained after a prune. | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. technically, not it's not "max size" but "cut-off size". |
||
| size_type max_size() const { return nMaxSize; } | ||
| //! Size the map is allowed to grow to before a prune is triggered. Always >= max_size(); | ||
| //! when larger, the map temporarily holds more than max_size() elements between prunes. | ||
| size_type prune_after_size() const { return nPruneAfterSize; } | ||
| size_type max_size(size_type nMaxSizeIn, size_type nPruneAfterSizeIn = 0) | ||
| { | ||
| assert(nMaxSizeIn > 0); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -196,18 +196,35 @@ BOOST_FIXTURE_TEST_CASE(seen_chainlock_cache_is_bounded, TestingSetup) | |
| { | ||
| m_node.clhandler->CheckActiveState(); | ||
|
|
||
| const size_t max_size = m_node.clhandler->SeenChainLockCacheMaxSizeForTesting(); | ||
| BOOST_REQUIRE_GT(max_size, 0U); | ||
|
|
||
| for (size_t i = 0; i < max_size + 1; ++i) { | ||
| const size_t retained_size = m_node.clhandler->SeenChainLockCacheRetainedSizeForTesting(); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this test doesn't test properly functionality, because it retrieve if by mistake setup ridicolous or completely wrong these values, the test will succeed because it retrieve these numbers from clhandler. I'd propose to just push bunch of chainlocks to clhandler and everytime just be sure that it is not more than 2000 + retrieve couple olds to be sure that they are still on place. This test is too strict about implementation but not really testing that values (prune_after_size, retained_size) are sane at all.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Open to followup PRs |
||
| const size_t prune_after_size = m_node.clhandler->SeenChainLockCachePruneAfterSizeForTesting(); | ||
| BOOST_REQUIRE_GT(retained_size, 0U); | ||
| // The cache prunes with hysteresis: it is allowed to grow past the retained size and is only | ||
| // pruned back down once it exceeds the (larger) prune-after size. Pruning sorts every entry, | ||
| // so pruning on each insertion past the retained size would be a peer-triggered CPU | ||
| // amplification path. | ||
| BOOST_REQUIRE_GT(prune_after_size, retained_size); | ||
|
|
||
| const auto process = [&](size_t i) { | ||
| auto clsig = CreateChainLock(static_cast<int32_t>(i), GetTestBlockHash(static_cast<uint32_t>(2000 + i))); | ||
| [[maybe_unused]] const auto result = | ||
| m_node.clhandler->ProcessNewChainLock(/*from=*/-1, clsig, *m_node.llmq_ctx->qman, ::SerializeHash(clsig)); | ||
| BOOST_CHECK_LE(m_node.clhandler->SeenChainLockCacheSizeForTesting(), max_size); | ||
| if (i == 0) { | ||
| BOOST_CHECK_GT(m_node.clhandler->SeenChainLockCacheSizeForTesting(), 0U); | ||
| } | ||
| }; | ||
|
|
||
| // Growing up to the prune-after size must not evict anything, so no prune (and no sort) has | ||
| // run yet -- in particular there is no strict cap at the retained size. | ||
| for (size_t i = 0; i < prune_after_size; ++i) { | ||
| process(i); | ||
| BOOST_CHECK_EQUAL(m_node.clhandler->SeenChainLockCacheSizeForTesting(), i + 1U); | ||
| } | ||
| BOOST_CHECK_GT(m_node.clhandler->SeenChainLockCacheSizeForTesting(), retained_size); | ||
|
|
||
| // Crossing the prune-after size prunes back down to the retained size in a single batch. | ||
| process(prune_after_size); | ||
| BOOST_CHECK_EQUAL(m_node.clhandler->SeenChainLockCacheSizeForTesting(), retained_size); | ||
|
|
||
| // Repeated prune cycles are covered generically by limitedmap_prune_after_size_test; this | ||
| // case only pins down how ChainlockHandler wires the cache up. | ||
| } | ||
|
|
||
| BOOST_FIXTURE_TEST_CASE(best_chainlock_is_already_have_after_seen_cache_eviction, TestingSetup) | ||
|
|
@@ -219,15 +236,18 @@ BOOST_FIXTURE_TEST_CASE(best_chainlock_is_already_have_after_seen_cache_eviction | |
| BOOST_REQUIRE(m_node.chainlocks->UpdateBestChainlock(best_hash, best_clsig, /*pindex=*/nullptr)); | ||
| BOOST_CHECK(m_node.clhandler->AlreadyHave(CInv{MSG_CLSIG, best_hash})); | ||
|
|
||
| const size_t max_size = m_node.clhandler->SeenChainLockCacheMaxSizeForTesting(); | ||
| BOOST_REQUIRE_GT(max_size, 0U); | ||
| const size_t prune_after_size = m_node.clhandler->SeenChainLockCachePruneAfterSizeForTesting(); | ||
| BOOST_REQUIRE_GT(prune_after_size, 0U); | ||
|
|
||
| for (size_t i = 0; i < max_size + 1; ++i) { | ||
| // Insert enough unique CLSIGs to force at least one prune of the seen cache. | ||
| for (size_t i = 0; i < prune_after_size + 1; ++i) { | ||
| auto clsig = CreateChainLock(static_cast<int32_t>(101 + i), GetTestBlockHash(static_cast<uint32_t>(3000 + i))); | ||
| [[maybe_unused]] const auto result = | ||
| m_node.clhandler->ProcessNewChainLock(/*from=*/-1, clsig, *m_node.llmq_ctx->qman, ::SerializeHash(clsig)); | ||
| BOOST_CHECK_LE(m_node.clhandler->SeenChainLockCacheSizeForTesting(), max_size); | ||
| BOOST_CHECK_LE(m_node.clhandler->SeenChainLockCacheSizeForTesting(), prune_after_size); | ||
| } | ||
| BOOST_CHECK_EQUAL(m_node.clhandler->SeenChainLockCacheSizeForTesting(), | ||
| m_node.clhandler->SeenChainLockCacheRetainedSizeForTesting()); | ||
|
|
||
| BOOST_CHECK(m_node.clhandler->AlreadyHave(CInv{MSG_CLSIG, best_hash})); | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO it's too excessive comment about details of implementation and too easy to become out-dated when code is changed.