Add unit testing of node eviction logic.
Closes #19966.
Add unit testing of node eviction logic.
Closes #19966.
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.
Reviewers, this pull request conflicts with the following ones:
If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.
966@@ -1016,7 +967,51 @@ bool CConnman::AttemptToEvictConnection()
967 vEvictionCandidates = std::move(mapNetGroupNodes[naMostConnections]);
968
969 // Disconnect from the network group with the most connections
970- NodeId evicted = vEvictionCandidates.front().id;
971+ return vEvictionCandidates.front();
972+}
973+
974+/** Try to find a connection to evict when the node is full.
Concept ACK. Thanks for adding tests.
I sometimes wonder if it would make sense to move “semi-internal” things that are only exposed externally for unit testing, like in this case “NodeEvictionCandidate” and “SelectNodeToEvict”, to a separate set of headers. But not here anyhow.
815+namespace {
816+// The number of eviction candidates at (or above) which we are guaranteed that
817+// the current eviction logic will find a node to evict no matter what eviction
818+// candidates it is given. This constant may need to be adjusted if the eviction
819+// logic changes.
820+const int GUARANTEED_EVICTION_AT_N_CANDIDATES_OR_ABOVE = 29;
Would it be valuable to break this out into the various reasons the peers are protected from eviction? For example:
In src/net.h
0const int PEERS_PROTECTED_BY_NET_GROUP = 4
In src/net.cpp
0EraseLastKElements(vEvictionCandidates, CompareNetGroupKeyed, PEERS_PROTECTED_BY_NET_GROUP);
In src/test/net_tests.cpp:
0GUARANTEED_EVICTION_AT_N_CANDIDATES_OR_ABOVE = PEERS_PROTECTED_BY_NET_GROUP + ... + 1
0constexpr int GUARANTEED_EVICTION_AT_N_CANDIDATES_OR_ABOVE{29};
821+
822+// The number of eviction candidates at (or below) which we are guaranteed that
823+// the current eviction logic won't find a node to evict no matter what eviction
824+// candidates it is given. This constant may need to be adjusted if the eviction
825+// logic changes.
826+const int GUARANTEED_NON_EVICTION_AT_N_CANDIDATES_OR_BELOW = 20;
Yes, but I’m afraid I cannot help you with that at the moment: so far it is a purely an empirical/experimental result :D
Counterexamples or proofs welcome! :)
1003+ NodeEvictionCandidate candidate = {node->GetId(), node->nTimeConnected, node->nMinPingUsecTime,
1004+ node->nLastBlockTime, node->nLastTXTime,
1005+ HasAllDesirableServiceFlags(node->nServices),
1006+ peer_relay_txes, peer_filter_not_null, node->nKeyedNetGroup,
1007+ node->m_prefer_evict, node->addr.IsLocal()};
1008+ vEvictionCandidates.push_back(candidate);
emplace_back
here?
I’m afraid that won’t be possible until C++20ยน :) See P0960: “Allow initializing aggregates from a parenthesized list of values” .
ยน Assuming you mean without adding a new ctor to NodeEvictionCandidate
.
2c62fb6
. Thank you, @practicalswift for making the code more testable. Also for exposure to idiomatic modern C++. I will checkout the branch and run soon.
903- * for each of several distinct characteristics which are difficult
904- * to forge. In order to partition a node the attacker must be
905- * simultaneously better at all of them than honest peers.
906- */
907-bool CConnman::AttemptToEvictConnection()
908+NODISCARD Optional<NodeEvictionCandidate> SelectNodeToEvict(std::vector<NodeEvictionCandidate>&& vEvictionCandidates)
NODISCARD
should be replaced with [[nodiscard]]
NodeEvictionCandidate
optional here, when only the NodeId is used? I think it’d be better to return an Optional<NodeId>
1005+ HasAllDesirableServiceFlags(node->nServices),
1006+ peer_relay_txes, peer_filter_not_null, node->nKeyedNetGroup,
1007+ node->m_prefer_evict, node->addr.IsLocal()};
1008+ vEvictionCandidates.push_back(candidate);
1009+ }
1010+ node_to_evict = SelectNodeToEvict(std::move(vEvictionCandidates));
cs_vNodes
held? Previously, we’d release the lock before running through this logic.
std::move
here? Why not just pass this as an lvalue reference?
Good point. It can be moved to the outer scope. Thanks!
Context: #19972 (review)
I understand why you’d want to pass by ref, but there’s no benefit to passing by rvalue. This code suggests to me that SelectNodeToEvict()
is going to keep ownership of vEvictionCandidates
and you’re using move semantics to avoid a copy, but that’s not actually happening here.
There’s no harm in using &&
and move
here, but it’s unnecessary and possibly confusing.
772@@ -771,4 +773,129 @@ BOOST_AUTO_TEST_CASE(PoissonNextSend)
773 g_mock_deterministic_tests = false;
774 }
775
776+std::vector<NodeEvictionCandidate> GetRandomNodeEvictionCandidates(const int n_candidates, FastRandomContext& random_context)
777+{
778+ std::vector<NodeEvictionCandidate> candidates;
779+ for (int i = 0; i < n_candidates; ++i) {
780+ candidates.push_back({/* id */ static_cast<NodeId>(random_context.randrange(100)),
id
to i
directly making it unique. Instead of relying on the caller doing this :)
798+ Shuffle(shuffled_candidates.begin(), shuffled_candidates.end(), random_context);
799+ const Optional<NodeEvictionCandidate> evicted_node = SelectNodeToEvict(std::move(shuffled_candidates));
800+ if (!evicted_node) {
801+ return false;
802+ }
803+ return std::find(node_ids.begin(), node_ids.end(), evicted_node->id) != node_ids.end();
GetRandomNodeEvictionCandidates
as suggested.
851+
852+ // Four nodes with the highest keyed netgroup values should be
853+ // protected from eviction.
854+ BOOST_CHECK(!IsEvicted(
855+ number_of_nodes, [number_of_nodes](NodeEvictionCandidate& candidate, const size_t idx) {
856+ candidate.id = idx;
NodeEvictionCandidate
s with sequential node id numbers, you wouldn’t need to do this here.
8@@ -9,6 +9,7 @@
9
10 #include <net.h>
11
12+#include <attributes.h>
NODISCARD
790+ /* m_is_local */ random_context.randbool()});
791+ }
792+ return candidates;
793+}
794+
795+bool IsEvicted(const std::vector<NodeEvictionCandidate>& candidates, const std::vector<NodeId> node_ids, FastRandomContext& random_context)
IsEvicted()
could use a very short comment to explain the interface (here: returns true if any of the node ids in node_ids
are selected for eviction).
I understand why you’d want to pass by ref, but there’s no benefit to passing by rvalue. This code suggests to me that SelectNodeToEvict() is going to keep ownership of vEvictionCandidates and you’re using move semantics to avoid a copy, but that’s not actually happening here.
I’ve now reverted to the original version (alternative 1 below), since your suggestion and sipa’s suggestion are mutually exclusive :)
I’ll let others chime in regarding the choice between the three alternatives:
0Alternative 1. Optional<NodeId> SelectNodeToEvict(std::vector<NodeEvictionCandidate>)
1Alternative 2. Optional<NodeId> SelectNodeToEvict(std::vector<NodeEvictionCandidate>&)
2Alternative 3. Optional<NodeId> SelectNodeToEvict(std::vector<NodeEvictionCandidate>&&)
Trying to summarize the pros/cons:
SelectNodeToEvict
modifies its input.Perhaps the members of the review club could chime in. Some questions for the review club: Do you agree with the pros/cons of each alternative? Which alternative do you find easier to review? Which alternative do you prefer? Is it possible to quantify the real-world impact of the extraneous copy in a theoretical worst-case scenario?
since your suggestion and sipa’s suggestion are mutually exclusive :)
I don’t think they are mutually exclusive. There are two questions here:
I don’t think there’s any disagreement about (1). We should pass by reference to avoid a copy.
For (2), I think it’s slightly odd to pass by rvalue reference when it’s not necessary to do so, but that’s more of a stylistic thing. I see now that you’re calling the function with a prvalue in the tests, which means you can’t pass by lvalue reference. My preference would therefore be to revert to what you had before.
795+// Returns true if any of the node ids in node_ids are selected for eviction.
796+bool IsEvicted(const std::vector<NodeEvictionCandidate>& candidates, const std::vector<NodeId> node_ids, FastRandomContext& random_context)
797+{
798+ std::vector<NodeEvictionCandidate> shuffled_candidates = candidates;
799+ Shuffle(shuffled_candidates.begin(), shuffled_candidates.end(), random_context);
800+ const Optional<NodeId> evicted_node_id = SelectNodeToEvict(shuffled_candidates);
Any reason not to shuffle in place?
0bool IsEvicted(std::vector<NodeEvictionCandidate>& candidates, const std::vector<NodeId> node_ids, FastRandomContext& random_context)
1{
2 Shuffle(candidates.begin(), candidates.end(), random_context);
3 const Optional<NodeId> evicted_node_id = SelectNodeToEvict(candidates);
Updated accordingly.
Personally I prefer the std::vector<NodeEvictionCandidate>&&
alternative in this case since it makes it clear from reading the code at the call site that vEvictionCandidates
is not intended to be used after the SelectNodeToEvict
call.
Hopefully any use-after-moved-from would be quite obvious.
Example of future patches where the signature makes a difference from a gotcha perspective:
Given signature Optional<NodeId> SelectNodeToEvict(std::vector<NodeEvictionCandidate>&)
0 Optional<NodeId> node_id_to_evict = SelectNodeToEvict(vEvictionCandidates);
1 if (!node_id_to_evict) {
2 return false;
3 }
4+ if (!vEvictionCandidates.empty()) { // Likely BAD, but not immediately obvious why
5+ โฆ do something with the now perhaps surprisingly modified vEvictionCandidates โฆ
Given signature Optional<NodeId> SelectNodeToEvict(std::vector<NodeEvictionCandidate>&&)
0 Optional<NodeId> node_id_to_evict = SelectNodeToEvict(std::move(vEvictionCandidates));
1 if (!node_id_to_evict) {
2 return false;
3 }
4+ if (!vEvictionCandidates.empty()) { // BAD and hopefully obviously so thanks to std::move above :)
5+ โฆ do something with vEvictionCandidates which is now in some valid but otherwise indeterminate state โฆ
Personally I prefer the std::vector&& alternative in this case since it makes it clear from reading the code at the call site that vEvictionCandidates is not intended to be used after the SelectNodeToEvict call.
Fine by me!
818+namespace {
819+// The number of eviction candidates at (or above) which we are guaranteed that
820+// the current eviction logic will find a node to evict no matter what eviction
821+// candidates it is given. This constant may need to be adjusted if the eviction
822+// logic changes.
823+constexpr int GUARANTEED_EVICTION_AT_N_CANDIDATES_OR_ABOVE{29};
Please add some justification for these numbers, even if it’s a description of the simulation/test you ran to get them. I don’t understand how this 29 is guaranteed and the 20 is guaranteed below.
This reply makes it sound like it’s not guaranteed?
847+ BOOST_CHECK(!SelectNodeToEvict(GetRandomNodeEvictionCandidates(number_of_nodes, random_context)));
848+ }
849+ if (number_of_nodes >= GUARANTEED_EVICTION_AT_N_CANDIDATES_OR_ABOVE) {
850+ BOOST_CHECK(SelectNodeToEvict(GetRandomNodeEvictionCandidates(number_of_nodes, random_context)));
851+ } else {
852+ continue;
I think you’re not running the following checks if the number of nodes is between the min and the max because of this continue
. Is that the intention?
Edit: ignore me! I’m looking at the wrong for loop.
Edit again: Nope, I think I was right the first time…
@narula Oh, good catch! The continue
should be dropped. I don’t remember what the apparently incorrect reasoning behind it was TBH :)
Thanks for catching it!
848+ }
849+ if (number_of_nodes >= GUARANTEED_EVICTION_AT_N_CANDIDATES_OR_ABOVE) {
850+ BOOST_CHECK(SelectNodeToEvict(GetRandomNodeEvictionCandidates(number_of_nodes, random_context)));
851+ } else {
852+ continue;
853+ }
I’m a HUGE supporter of the early return pattern (AKA bouncer pattern) that you’re using here. However, maybe this alternative is a little easier to parse.
(Maybe not in this annoyingly narrow GitHub window though…)
0 if (number_of_nodes < GUARANTEED_EVICTION_AT_N_CANDIDATES_OR_ABOVE) {
1 // Verify correctness of GUARANTEED_NON_EVICTION_AT_N_CANDIDATES_OR_BELOW
2 if (number_of_nodes <= GUARANTEED_NON_EVICTION_AT_N_CANDIDATES_OR_BELOW) {
3 // At this number of nodes, we're guaranteed to never evict any of them
4 BOOST_CHECK(!SelectNodeToEvict(GetRandomNodeEvictionCandidates(number_of_nodes, random_context)));
5 }
6 // There is no need to run the sub-tests for fewer than the number of nodes guaranteed for eviction
7 // ^ FWIW I don't agree with this, see below
8 continue;
9 }
10 // number_of_nodes >= GUARANTEED_EVICTION_AT_N_CANDIDATES_OR_ABOVE
11 // Verify correctness of GUARANTEED_EVICTION_AT_N_CANDIDATES_OR_ABOVE
12 BOOST_CHECK(SelectNodeToEvict(GetRandomNodeEvictionCandidates(number_of_nodes, random_context)));
Honestly though, I think it’s just the similarity between the names of GUARANTEED_EVICTION_AT_N_CANDIDATES_OR_ABOVE
and GUARANTEED_NON_EVICTION_AT_N_CANDIDATES_OR_BELOW
that is making this so difficult. Maybe (maybe…) EVICTION_THRESHOLD
and NON_EVICTION_THRESHOLD
and put the fact that it’s inclusive on both ends in the comments? Or follow the half-open convention of “lower bounds are inclusive, upper bounds are exclusive”. Popular in Python, not sure about C++.
All of this said, why have the early return at all? All of the tests below check for non-eviction. Can’t we run them regardless of the number of nodes?
(Feel free to reword my code comments)
Thanks for reviewing!
Pushed an updated version:
Now skipping the GUARANTEED_EVICTION_AT_N_CANDIDATES_OR_ABOVE
and GUARANTEED_NON_EVICTION_AT_N_CANDIDATES_OR_BELOW
checking which isn’t particularly important compared to the other tests.
Also fixed an incorrect comment.
Should hopefully be ready for final review :)
848+ number_of_nodes, [number_of_nodes](NodeEvictionCandidate& candidate) {
849+ candidate.nLastTXTime = number_of_nodes - candidate.id;
850+ },
851+ {0, 1, 2, 3}, random_context));
852+
853+ // Four non-tx-relay peers that most recently sent us novel blocks
nLastBlockTime
based protections: that’s why more than four are protected in practice in this specific test case. Now testing the two different nLastBlockTime
protections a.) independently and b.) jointly.
807+// apply eviction logic and then return true if any of the node ids in node_ids
808+// are selected for eviction.
809+bool IsEvicted(const int number_of_nodes, std::function<void(NodeEvictionCandidate&)> candidate_setup_fn, const std::vector<NodeId> node_ids, FastRandomContext& random_context)
810+{
811+ std::vector<NodeEvictionCandidate> candidates = GetRandomNodeEvictionCandidates(number_of_nodes, random_context);
812+ for (size_t i = 0; i < candidates.size(); ++i) {
798+ Shuffle(candidates.begin(), candidates.end(), random_context);
799+ const Optional<NodeId> evicted_node_id = SelectNodeToEvict(std::move(candidates));
800+ if (!evicted_node_id) {
801+ return false;
802+ }
803+ return std::find(node_ids.begin(), node_ids.end(), *evicted_node_id) != node_ids.end();
how is this different from a less verbose
0 return node_ids.find(*evicted_node_id) != node_ids.end();
std::vector
doesn’t have a find
function. Perhaps you were thinking of std::map
or std::set
? :)
785+ /* fRelevantServices */ random_context.randbool(),
786+ /* fRelayTxes */ random_context.randbool(),
787+ /* fBloomFilter */ random_context.randbool(),
788+ /* nKeyedNetGroup */ random_context.randrange(100),
789+ /* prefer_evict */ random_context.randbool(),
790+ /* m_is_local */ random_context.randbool()});
ACK.
Nice test.
Updated.
Addressed feedback from @MarcoFalke and @jnewbery.
Also added a combined test which tests the protections based on nKeyedNetGroup
(4 peers protected), nMinPingUsecTime
(8 peers protected), nLastTXTime
(4 peers protected) and nLastBlockTime
(8 peers protected) jointly.
Kindly re-review thoroughly :)
828+
829+ for (int i = 0; i < NODE_EVICTION_TEST_ROUNDS; ++i) {
830+ for (int number_of_nodes = 0; number_of_nodes < NODE_EVICTION_TEST_UP_TO_N_NODES; ++number_of_nodes) {
831+ // Four nodes with the highest keyed netgroup values should be
832+ // protected from eviction.
833+ BOOST_CHECK(!IsEvicted(
In the previous version of the test, we tested:
In the new version, we are checking that the protected peers are not evicted in any situation (3). The last test case covers for (1). However, the test will not alert if an eviction does not happen when it should (>= 29 peers). Am I missing something?
Personally, I found the GUARANTEED_EVICTION_THRESHOLD and GUARANTEED_NON_EVICTION_THRESHOLD valuable to test and also as documentation.
From PR Review club:
Q: Why is eviction guaranteed if we have at least 29 eviction candidates?
The code in net.cpp (https://github.com/bitcoin/bitcoin/blob/fabecce/src/net.cpp#L954-L968) protects at most 28 peers from eviction (4 by net group, 8 by lowest ping time, 4 by last time of novel tx, up to 8 non-tx-relay peers by last novel block time, and 4 more peers by last novel block time). So any additional peers are guaranteed to be candidates for eviction.
Q: Why is non-eviction guaranteed if we have no more than 20 eviction candidates? Is 20 the highest number of nodes that guarantees non-eviction?
The protection at net.cpp::961 for up to 8 non-tx-relay peers may, or may not apply to the randomly generated eviction candidates since !n.fRelayTxes && n.fRelevantServices
will evaluate to randbool
. So we cannot count them in the guaranteed non-eviction?
So, guaranteed non-eviction is only on 4(CompareNetGroupKeyed) + 8(ReverseCompareNodeMinPingTime) + 4(CompareNodeTXTime) + 4(CompareNodeBlockTime) = 20.
Even if we remove the <=20 assertion, we are already checking non-eviction <=20 (it’s implicit in the tests). So the tests will need need to updated when changes are made anyway. I am not sure I understand why that’s undesirable.
I think it is useful to test that eviction does indeed happen (not just that protections are provided) when it should. After all, SelectNodeToEvict
should select a node to evict :) Without the >=29 assertion, does this test guarantee that?
All that said, @jnewbery is infinitely more qualified than I am. I’m happy with whatever you all decide and will learn from it.
1c9b235
cr ACK 6767d63d02cc8a670f3d409db1c78a77fdcb5363 ๐ค
Signature:
0-----BEGIN PGP SIGNED MESSAGE-----
1Hash: SHA512
2
3cr ACK 6767d63d02cc8a670f3d409db1c78a77fdcb5363 ๐ค
4-----BEGIN PGP SIGNATURE-----
5
6iQGzBAEBCgAdFiEE+rVPoUahrI9sLGYTzit1aX5ppUgFAlwqrYAACgkQzit1aX5p
7pUgJLwwAljffgO2Xw1Iez563pRA0ztSBIkuzEBPGf9nYifcbQJsfPMCdLzuu3MPQ
8ZgfWPQNmtEuSTNq0R4Hl+XaPmBcoS5LDOTfxJWsQtb+kE0uXVfN1nRu4+d91/Sor
9+NT9Ach7YRVvJPOZZc0euxyka0pxLwX3+LdXa1k26gL2teT5knaROnCfSxenrFrO
10YIzbBfkn2lm/TMu4FhC2hW+OZon4zfHHCPzJYkQR2FByixLDqIIEOJiOY9ycLJQm
11ys9xGzn0lZz0/hw4ApEe2b+lLREn/6YMjPNefo0hWcADAYDbjsvEYBsFm0Z6m5Mw
12BXA4lhUPWes5QboZ1LCxrEdxGG10uj8LwIBwESp/KVpZ53w0f9yU+mBQ68bruAHh
135hFAJmriES6q8Ml/q2b3IUq8tXwOBM+2poGR8MtTYV6Obk/3eus2bA33EVsfNzm3
14ZXzo/F1yQvXlD1kSDU3Aez1yodeLK3GQNoTKaPIGeIwOM3sPhDhJ6REMYskLM4tt
15PxqHvOZz
16=Gf1H
17-----END PGP SIGNATURE-----
Timestamp of file with hash 23398d20d71a2405d911d0dc99e4fc62b592150f49fa6c17afd6d588c418522c -
cr ACK 1c9b235c330fe7b9b71f687ef14cb10fe588172b ๐ฒ
Signature:
0-----BEGIN PGP SIGNED MESSAGE-----
1Hash: SHA512
2
3cr ACK 1c9b235c330fe7b9b71f687ef14cb10fe588172b ๐ฒ
4-----BEGIN PGP SIGNATURE-----
5
6iQGzBAEBCgAdFiEE+rVPoUahrI9sLGYTzit1aX5ppUgFAlwqrYAACgkQzit1aX5p
7pUjQWgv/TnEWYjD/Ws2y2WL23agZIEyXZwlxbLCJTWkydKzBnOrPdP1QOBl3DZ95
81ICN5MfBErZtO825TG8NU2VfCesJRIJKV4996nw7GqpN+KfBlkYHNipYfoF9xdHB
9J0MT/97/wsrsWGhYEdwx3ftk0CHMNNDMN++N4BSx3h314jGnuNzV4DiUoz2ea9X3
101IAsitmrqa37xIlLIvdLSwRVDcWQyJ7oQ+NUo2KsSztXJ6XEpZcUAqhfRwuS9Ku4
11R3DIJHLXKk8PUEXlnmDWVtfnhuhGW4g3V6ZJdUId7OfUr85H/rGSxSeejqSqxLt/
128hGiRsuftelXPfd7SOAjRW5h6x6NFCJBgcVVOYKzjvjyknyH3it3IrPi9WFwLLWS
1398fNiUtiE2SsXOUFXxbbyalCZlQols/pBklibFoPNPhfHCT7ZPvazINGBJwkwxVS
14GL1IIAKftgsUJhmiSDy/LHfHwXC2vOL2ipdPLJuR7CPLPj9iBZ2picP4h3zRpSo2
15lubdxdSX
16=z6dl
17-----END PGP SIGNATURE-----
Timestamp of file with hash 3a47dabfa2f12ebc4fbd136dcd7771912bfcd7ed7ce86ba72f473c364fba70a0 -
code review ACK 1c9b235c33
Either are fine to merge.
902- * for each of several distinct characteristics which are difficult
903- * to forge. In order to partition a node the attacker must be
904- * simultaneously better at all of them than honest peers.
905- */
906-bool CConnman::AttemptToEvictConnection()
907+[[nodiscard]] Optional<NodeId> SelectNodeToEvict(std::vector<NodeEvictionCandidate>&& vEvictionCandidates)
fbba7d8aada5b1d7a63ad4133 add #include <optional.h>
header
perhaps call this SelectConnectionToEvict()
for symmetry with AttemptToEvictConnection()
, or SelectPeerToEvict()
as a peer is a node that is not ours
Now including optional.h
.
Regarding the naming: I see your point but given NodeEvictionCandidate
inputs I think SelectNodeToEvict
works.
1004+ peer_relay_txes, peer_filter_not_null, node->nKeyedNetGroup,
1005+ node->m_prefer_evict, node->addr.IsLocal()};
1006+ vEvictionCandidates.push_back(candidate);
1007+ }
1008+ }
1009+ Optional<NodeId> node_id_to_evict = SelectNodeToEvict(std::move(vEvictionCandidates));
793+ }
794+ return candidates;
795+}
796+
797+// Returns true if any of the node ids in node_ids are selected for eviction.
798+bool IsEvicted(std::vector<NodeEvictionCandidate> candidates, const std::vector<NodeId> node_ids, FastRandomContext& random_context)
6767d63d02cc8a670f3d409db1 ref to const?
0bool IsEvicted(std::vector<NodeEvictionCandidate> candidates, const std::vector<NodeId>& node_ids, FastRandomContext& random_context)
806+}
807+
808+// Create number_of_nodes random nodes, apply setup function candidate_setup_fn,
809+// apply eviction logic and then return true if any of the node ids in node_ids
810+// are selected for eviction.
811+bool IsEvicted(const int number_of_nodes, std::function<void(NodeEvictionCandidate&)> candidate_setup_fn, const std::vector<NodeId> node_ids, FastRandomContext& random_context)
6767d63d02cc8a670f3d409 ref to const here as well
0bool IsEvicted(const int number_of_nodes, std::function<void(NodeEvictionCandidate&)> candidate_setup_fn, const std::vector<NodeId>& node_ids, FastRandomContext& random_context)
ACK fee88237e03c21bf81f21098e6b89ecfa5327cee
Thanks for adding this nicely done coverage! Looking forward to more.
ACK fee88237e03c21bf81f21098e6b89ecfa5327cee ๐ผ
Signature:
0-----BEGIN PGP SIGNED MESSAGE-----
1Hash: SHA512
2
3ACK fee88237e03c21bf81f21098e6b89ecfa5327cee ๐ผ
4-----BEGIN PGP SIGNATURE-----
5
6iQGzBAEBCgAdFiEE+rVPoUahrI9sLGYTzit1aX5ppUgFAlwqrYAACgkQzit1aX5p
7pUiu/Av/RLQ2/lM0mJ7uTlvDebTgk57QGAdIs4rv+t9X7wUizqNhEjFHU2KC0HS0
8hU6QjHZelMuy0IvIRrcT33LCZkczPQbEQ4ZMAV5VZhuRNvAisi1PHxueX5U+oT83
9UKS899fiy7VN3RD7g7E/sMDNuGAo1hSV7MgYQMOlCG8dSJ3bbuF2d1jNEYqJQ7oa
10MkmdtB0SwvCOtZyvcsVDI8yC8UJ5r2ybgE+bYfMK9BzMUyWQriWmOnoVQBrnwb5j
11Zr7g/4pnq04078EuaBVP981P+Pc4pj6Eo63iIinabl74aLU3hsHa0bnUCkpihBSg
12stooipOqKXQsOS/jI1XyElhclE3EolEE6+0QIXFpx3H7AlkovPdF/LeeezH0dVDB
13hSl7VrdKrTxP7kjSBKBMOiPbzBdVSqo6ktaqhBI7YXlDepR7V54/PlDs7QEcM91r
143iEvD/wbMFJQZHzHEbmCINPXBwdOh9MleFgm0dgTMor7djOpaxgbYq/2Y5Bx4/4K
15s1ZuLUUS
16=f3JL
17-----END PGP SIGNATURE-----
Timestamp of file with hash fb125172e7abd1e8e484ae4f527e5e86e05737d1b1e006310dca8dbb38203afc -