/ src / test / net_peer_eviction_tests.cpp
net_peer_eviction_tests.cpp
  1  // Copyright (c) 2021-present The Bitcoin Core developers
  2  // Distributed under the MIT software license, see the accompanying
  3  // file COPYING or http://www.opensource.org/licenses/mit-license.php.
  4  
  5  #include <netaddress.h>
  6  #include <net.h>
  7  #include <test/util/net.h>
  8  #include <test/util/setup_common.h>
  9  
 10  #include <boost/test/unit_test.hpp>
 11  
 12  #include <algorithm>
 13  #include <functional>
 14  #include <optional>
 15  #include <unordered_set>
 16  #include <vector>
 17  
 18  BOOST_FIXTURE_TEST_SUITE(net_peer_eviction_tests, BasicTestingSetup)
 19  
 20  // Create `num_peers` random nodes, apply setup function `candidate_setup_fn`,
 21  // call ProtectEvictionCandidatesByRatio() to apply protection logic, and then
 22  // return true if all of `protected_peer_ids` and none of `unprotected_peer_ids`
 23  // are protected from eviction, i.e. removed from the eviction candidates.
 24  bool IsProtected(int num_peers,
 25                   std::function<void(NodeEvictionCandidate&)> candidate_setup_fn,
 26                   const std::unordered_set<NodeId>& protected_peer_ids,
 27                   const std::unordered_set<NodeId>& unprotected_peer_ids,
 28                   FastRandomContext& random_context)
 29  {
 30      std::vector<NodeEvictionCandidate> candidates{GetRandomNodeEvictionCandidates(num_peers, random_context)};
 31      for (NodeEvictionCandidate& candidate : candidates) {
 32          candidate_setup_fn(candidate);
 33      }
 34      std::shuffle(candidates.begin(), candidates.end(), random_context);
 35  
 36      const size_t size{candidates.size()};
 37      const size_t expected{size - size / 2}; // Expect half the candidates will be protected.
 38      ProtectEvictionCandidatesByRatio(candidates);
 39      BOOST_CHECK_EQUAL(candidates.size(), expected);
 40  
 41      size_t unprotected_count{0};
 42      for (const NodeEvictionCandidate& candidate : candidates) {
 43          if (protected_peer_ids.contains(candidate.id)) {
 44              // this peer should have been removed from the eviction candidates
 45              BOOST_TEST_MESSAGE(strprintf("expected candidate to be protected: %d", candidate.id));
 46              return false;
 47          }
 48          if (unprotected_peer_ids.contains(candidate.id)) {
 49              // this peer remains in the eviction candidates, as expected
 50              ++unprotected_count;
 51          }
 52      }
 53  
 54      const bool is_protected{unprotected_count == unprotected_peer_ids.size()};
 55      if (!is_protected) {
 56          BOOST_TEST_MESSAGE(strprintf("unprotected: expected %d, actual %d",
 57                                       unprotected_peer_ids.size(), unprotected_count));
 58      }
 59      return is_protected;
 60  }
 61  
 62  BOOST_AUTO_TEST_CASE(peer_protection_test)
 63  {
 64      FastRandomContext random_context{true};
 65      int num_peers{12};
 66  
 67      // Expect half of the peers with greatest uptime (the lowest m_connected)
 68      // to be protected from eviction.
 69      BOOST_CHECK(IsProtected(
 70          num_peers, [](NodeEvictionCandidate& c) {
 71              c.m_connected = std::chrono::seconds{c.id};
 72              c.m_is_local = false;
 73              c.m_network = NET_IPV4;
 74          },
 75          /*protected_peer_ids=*/{0, 1, 2, 3, 4, 5},
 76          /*unprotected_peer_ids=*/{6, 7, 8, 9, 10, 11},
 77          random_context));
 78  
 79      // Verify in the opposite direction.
 80      BOOST_CHECK(IsProtected(
 81          num_peers, [num_peers](NodeEvictionCandidate& c) {
 82              c.m_connected = std::chrono::seconds{num_peers - c.id};
 83              c.m_is_local = false;
 84              c.m_network = NET_IPV6;
 85          },
 86          /*protected_peer_ids=*/{6, 7, 8, 9, 10, 11},
 87          /*unprotected_peer_ids=*/{0, 1, 2, 3, 4, 5},
 88          random_context));
 89  
 90      // Test protection of onion, localhost, and I2P peers...
 91  
 92      // Expect 1/4 onion peers to be protected from eviction,
 93      // if no localhost, I2P, or CJDNS peers.
 94      BOOST_CHECK(IsProtected(
 95          num_peers, [](NodeEvictionCandidate& c) {
 96              c.m_is_local = false;
 97              c.m_network = (c.id == 3 || c.id == 8 || c.id == 9) ? NET_ONION : NET_IPV4;
 98          },
 99          /*protected_peer_ids=*/{3, 8, 9},
100          /*unprotected_peer_ids=*/{},
101          random_context));
102  
103      // Expect 1/4 onion peers and 1/4 of the other peers to be protected,
104      // sorted by longest uptime (lowest m_connected), if no localhost, I2P or CJDNS peers.
105      BOOST_CHECK(IsProtected(
106          num_peers, [](NodeEvictionCandidate& c) {
107              c.m_connected = std::chrono::seconds{c.id};
108              c.m_is_local = false;
109              c.m_network = (c.id == 3 || c.id > 7) ? NET_ONION : NET_IPV6;
110          },
111          /*protected_peer_ids=*/{0, 1, 2, 3, 8, 9},
112          /*unprotected_peer_ids=*/{4, 5, 6, 7, 10, 11},
113          random_context));
114  
115      // Expect 1/4 localhost peers to be protected from eviction,
116      // if no onion, I2P, or CJDNS peers.
117      BOOST_CHECK(IsProtected(
118          num_peers, [](NodeEvictionCandidate& c) {
119              c.m_is_local = (c.id == 1 || c.id == 9 || c.id == 11);
120              c.m_network = NET_IPV4;
121          },
122          /*protected_peer_ids=*/{1, 9, 11},
123          /*unprotected_peer_ids=*/{},
124          random_context));
125  
126      // Expect 1/4 localhost peers and 1/4 of the other peers to be protected,
127      // sorted by longest uptime (lowest m_connected), if no onion, I2P, or CJDNS peers.
128      BOOST_CHECK(IsProtected(
129          num_peers, [](NodeEvictionCandidate& c) {
130              c.m_connected = std::chrono::seconds{c.id};
131              c.m_is_local = (c.id > 6);
132              c.m_network = NET_IPV6;
133          },
134          /*protected_peer_ids=*/{0, 1, 2, 7, 8, 9},
135          /*unprotected_peer_ids=*/{3, 4, 5, 6, 10, 11},
136          random_context));
137  
138      // Expect 1/4 I2P peers to be protected from eviction,
139      // if no onion, localhost, or CJDNS peers.
140      BOOST_CHECK(IsProtected(
141          num_peers, [](NodeEvictionCandidate& c) {
142              c.m_is_local = false;
143              c.m_network = (c.id == 2 || c.id == 7 || c.id == 10) ? NET_I2P : NET_IPV4;
144          },
145          /*protected_peer_ids=*/{2, 7, 10},
146          /*unprotected_peer_ids=*/{},
147          random_context));
148  
149      // Expect 1/4 I2P peers and 1/4 of the other peers to be protected, sorted
150      // by longest uptime (lowest m_connected), if no onion, localhost, or CJDNS peers.
151      BOOST_CHECK(IsProtected(
152          num_peers, [](NodeEvictionCandidate& c) {
153              c.m_connected = std::chrono::seconds{c.id};
154              c.m_is_local = false;
155              c.m_network = (c.id == 4 || c.id > 8) ? NET_I2P : NET_IPV6;
156          },
157          /*protected_peer_ids=*/{0, 1, 2, 4, 9, 10},
158          /*unprotected_peer_ids=*/{3, 5, 6, 7, 8, 11},
159          random_context));
160  
161      // Expect 1/4 CJDNS peers to be protected from eviction,
162      // if no onion, localhost, or I2P peers.
163      BOOST_CHECK(IsProtected(
164          num_peers, [](NodeEvictionCandidate& c) {
165              c.m_is_local = false;
166              c.m_network = (c.id == 2 || c.id == 7 || c.id == 10) ? NET_CJDNS : NET_IPV4;
167          },
168          /*protected_peer_ids=*/{2, 7, 10},
169          /*unprotected_peer_ids=*/{},
170          random_context));
171  
172      // Expect 1/4 CJDNS peers and 1/4 of the other peers to be protected, sorted
173      // by longest uptime (lowest m_connected), if no onion, localhost, or I2P peers.
174      BOOST_CHECK(IsProtected(
175          num_peers, [](NodeEvictionCandidate& c) {
176              c.m_connected = std::chrono::seconds{c.id};
177              c.m_is_local = false;
178              c.m_network = (c.id == 4 || c.id > 8) ? NET_CJDNS : NET_IPV6;
179          },
180          /*protected_peer_ids=*/{0, 1, 2, 4, 9, 10},
181          /*unprotected_peer_ids=*/{3, 5, 6, 7, 8, 11},
182          random_context));
183  
184      // Tests with 2 networks...
185  
186      // Combined test: expect having 1 localhost and 1 onion peer out of 4 to
187      // protect 1 localhost, 0 onion and 1 other peer, sorted by longest uptime;
188      // stable sort breaks tie with array order of localhost first.
189      BOOST_CHECK(IsProtected(
190          4, [](NodeEvictionCandidate& c) {
191              c.m_connected = std::chrono::seconds{c.id};
192              c.m_is_local = (c.id == 4);
193              c.m_network = (c.id == 3) ? NET_ONION : NET_IPV4;
194          },
195          /*protected_peer_ids=*/{0, 4},
196          /*unprotected_peer_ids=*/{1, 2},
197          random_context));
198  
199      // Combined test: expect having 1 localhost and 1 onion peer out of 7 to
200      // protect 1 localhost, 0 onion, and 2 other peers (3 total), sorted by
201      // uptime; stable sort breaks tie with array order of localhost first.
202      BOOST_CHECK(IsProtected(
203          7, [](NodeEvictionCandidate& c) {
204              c.m_connected = std::chrono::seconds{c.id};
205              c.m_is_local = (c.id == 6);
206              c.m_network = (c.id == 5) ? NET_ONION : NET_IPV4;
207          },
208          /*protected_peer_ids=*/{0, 1, 6},
209          /*unprotected_peer_ids=*/{2, 3, 4, 5},
210          random_context));
211  
212      // Combined test: expect having 1 localhost and 1 onion peer out of 8 to
213      // protect protect 1 localhost, 1 onion and 2 other peers (4 total), sorted
214      // by uptime; stable sort breaks tie with array order of localhost first.
215      BOOST_CHECK(IsProtected(
216          8, [](NodeEvictionCandidate& c) {
217              c.m_connected = std::chrono::seconds{c.id};
218              c.m_is_local = (c.id == 6);
219              c.m_network = (c.id == 5) ? NET_ONION : NET_IPV4;
220          },
221          /*protected_peer_ids=*/{0, 1, 5, 6},
222          /*unprotected_peer_ids=*/{2, 3, 4, 7},
223          random_context));
224  
225      // Combined test: expect having 3 localhost and 3 onion peers out of 12 to
226      // protect 2 localhost and 1 onion, plus 3 other peers, sorted by longest
227      // uptime; stable sort breaks ties with the array order of localhost first.
228      BOOST_CHECK(IsProtected(
229          num_peers, [](NodeEvictionCandidate& c) {
230              c.m_connected = std::chrono::seconds{c.id};
231              c.m_is_local = (c.id == 6 || c.id == 9 || c.id == 11);
232              c.m_network = (c.id == 7 || c.id == 8 || c.id == 10) ? NET_ONION : NET_IPV6;
233          },
234          /*protected_peer_ids=*/{0, 1, 2, 6, 7, 9},
235          /*unprotected_peer_ids=*/{3, 4, 5, 8, 10, 11},
236          random_context));
237  
238      // Combined test: expect having 4 localhost and 1 onion peer out of 12 to
239      // protect 2 localhost and 1 onion, plus 3 other peers, sorted by longest uptime.
240      BOOST_CHECK(IsProtected(
241          num_peers, [](NodeEvictionCandidate& c) {
242              c.m_connected = std::chrono::seconds{c.id};
243              c.m_is_local = (c.id > 4 && c.id < 9);
244              c.m_network = (c.id == 10) ? NET_ONION : NET_IPV4;
245          },
246          /*protected_peer_ids=*/{0, 1, 2, 5, 6, 10},
247          /*unprotected_peer_ids=*/{3, 4, 7, 8, 9, 11},
248          random_context));
249  
250      // Combined test: expect having 4 localhost and 2 onion peers out of 16 to
251      // protect 2 localhost and 2 onions, plus 4 other peers, sorted by longest uptime.
252      BOOST_CHECK(IsProtected(
253          16, [](NodeEvictionCandidate& c) {
254              c.m_connected = std::chrono::seconds{c.id};
255              c.m_is_local = (c.id == 6 || c.id == 9 || c.id == 11 || c.id == 12);
256              c.m_network = (c.id == 8 || c.id == 10) ? NET_ONION : NET_IPV6;
257          },
258          /*protected_peer_ids=*/{0, 1, 2, 3, 6, 8, 9, 10},
259          /*unprotected_peer_ids=*/{4, 5, 7, 11, 12, 13, 14, 15},
260          random_context));
261  
262      // Combined test: expect having 5 localhost and 1 onion peer out of 16 to
263      // protect 3 localhost (recovering the unused onion slot), 1 onion, and 4
264      // others, sorted by longest uptime.
265      BOOST_CHECK(IsProtected(
266          16, [](NodeEvictionCandidate& c) {
267              c.m_connected = std::chrono::seconds{c.id};
268              c.m_is_local = (c.id > 10);
269              c.m_network = (c.id == 10) ? NET_ONION : NET_IPV4;
270          },
271          /*protected_peer_ids=*/{0, 1, 2, 3, 10, 11, 12, 13},
272          /*unprotected_peer_ids=*/{4, 5, 6, 7, 8, 9, 14, 15},
273          random_context));
274  
275      // Combined test: expect having 1 localhost and 4 onion peers out of 16 to
276      // protect 1 localhost and 3 onions (recovering the unused localhost slot),
277      // plus 4 others, sorted by longest uptime.
278      BOOST_CHECK(IsProtected(
279          16, [](NodeEvictionCandidate& c) {
280              c.m_connected = std::chrono::seconds{c.id};
281              c.m_is_local = (c.id == 15);
282              c.m_network = (c.id > 6 && c.id < 11) ? NET_ONION : NET_IPV6;
283          },
284          /*protected_peer_ids=*/{0, 1, 2, 3, 7, 8, 9, 15},
285          /*unprotected_peer_ids=*/{5, 6, 10, 11, 12, 13, 14},
286          random_context));
287  
288      // Combined test: expect having 2 onion and 4 I2P out of 12 peers to protect
289      // 2 onion (prioritized for having fewer candidates) and 1 I2P, plus 3
290      // others, sorted by longest uptime.
291      BOOST_CHECK(IsProtected(
292          num_peers, [](NodeEvictionCandidate& c) {
293              c.m_connected = std::chrono::seconds{c.id};
294              c.m_is_local = false;
295              if (c.id == 8 || c.id == 10) {
296                  c.m_network = NET_ONION;
297              } else if (c.id == 6 || c.id == 9 || c.id == 11 || c.id == 12) {
298                  c.m_network = NET_I2P;
299              } else {
300                  c.m_network = NET_IPV4;
301              }
302          },
303          /*protected_peer_ids=*/{0, 1, 2, 6, 8, 10},
304          /*unprotected_peer_ids=*/{3, 4, 5, 7, 9, 11},
305          random_context));
306  
307      // Tests with 3 networks...
308  
309      // Combined test: expect having 1 localhost, 1 I2P and 1 onion peer out of 4
310      // to protect 1 I2P, 0 localhost, 0 onion and 1 other peer (2 total), sorted
311      // by longest uptime; stable sort breaks tie with array order of I2P first.
312      BOOST_CHECK(IsProtected(
313          4, [](NodeEvictionCandidate& c) {
314              c.m_connected = std::chrono::seconds{c.id};
315              c.m_is_local = (c.id == 2);
316              if (c.id == 3) {
317                  c.m_network = NET_I2P;
318              } else if (c.id == 1) {
319                  c.m_network = NET_ONION;
320              } else {
321                  c.m_network = NET_IPV6;
322              }
323          },
324          /*protected_peer_ids=*/{0, 3},
325          /*unprotected_peer_ids=*/{1, 2},
326          random_context));
327  
328      // Combined test: expect having 1 localhost, 1 I2P and 1 onion peer out of 7
329      // to protect 1 I2P, 0 localhost, 0 onion and 2 other peers (3 total) sorted
330      // by longest uptime; stable sort breaks tie with array order of I2P first.
331      BOOST_CHECK(IsProtected(
332          7, [](NodeEvictionCandidate& c) {
333              c.m_connected = std::chrono::seconds{c.id};
334              c.m_is_local = (c.id == 4);
335              if (c.id == 6) {
336                  c.m_network = NET_I2P;
337              } else if (c.id == 5) {
338                  c.m_network = NET_ONION;
339              } else {
340                  c.m_network = NET_IPV6;
341              }
342          },
343          /*protected_peer_ids=*/{0, 1, 6},
344          /*unprotected_peer_ids=*/{2, 3, 4, 5},
345          random_context));
346  
347      // Combined test: expect having 1 localhost, 1 I2P and 1 onion peer out of 8
348      // to protect 1 I2P, 1 localhost, 0 onion and 2 other peers (4 total) sorted
349      // by uptime; stable sort breaks tie with array order of I2P then localhost.
350      BOOST_CHECK(IsProtected(
351          8, [](NodeEvictionCandidate& c) {
352              c.m_connected = std::chrono::seconds{c.id};
353              c.m_is_local = (c.id == 6);
354              if (c.id == 5) {
355                  c.m_network = NET_I2P;
356              } else if (c.id == 4) {
357                  c.m_network = NET_ONION;
358              } else {
359                  c.m_network = NET_IPV6;
360              }
361          },
362          /*protected_peer_ids=*/{0, 1, 5, 6},
363          /*unprotected_peer_ids=*/{2, 3, 4, 7},
364          random_context));
365  
366      // Combined test: expect having 4 localhost, 2 I2P, and 2 onion peers out of
367      // 16 to protect 1 localhost, 2 I2P, and 1 onion (4/16 total), plus 4 others
368      // for 8 total, sorted by longest uptime.
369      BOOST_CHECK(IsProtected(
370          16, [](NodeEvictionCandidate& c) {
371              c.m_connected = std::chrono::seconds{c.id};
372              c.m_is_local = (c.id == 6 || c.id > 11);
373              if (c.id == 7 || c.id == 11) {
374                  c.m_network = NET_I2P;
375              } else if (c.id == 9 || c.id == 10) {
376                  c.m_network = NET_ONION;
377              } else {
378                  c.m_network = NET_IPV4;
379              }
380          },
381          /*protected_peer_ids=*/{0, 1, 2, 3, 6, 7, 9, 11},
382          /*unprotected_peer_ids=*/{4, 5, 8, 10, 12, 13, 14, 15},
383          random_context));
384  
385      // Combined test: expect having 1 localhost, 8 I2P and 1 onion peer out of
386      // 24 to protect 1, 4, and 1 (6 total), plus 6 others for 12/24 total,
387      // sorted by longest uptime.
388      BOOST_CHECK(IsProtected(
389          24, [](NodeEvictionCandidate& c) {
390              c.m_connected = std::chrono::seconds{c.id};
391              c.m_is_local = (c.id == 12);
392              if (c.id > 14 && c.id < 23) { // 4 protected instead of usual 2
393                  c.m_network = NET_I2P;
394              } else if (c.id == 23) {
395                  c.m_network = NET_ONION;
396              } else {
397                  c.m_network = NET_IPV6;
398              }
399          },
400          /*protected_peer_ids=*/{0, 1, 2, 3, 4, 5, 12, 15, 16, 17, 18, 23},
401          /*unprotected_peer_ids=*/{6, 7, 8, 9, 10, 11, 13, 14, 19, 20, 21, 22},
402          random_context));
403  
404      // Combined test: expect having 1 localhost, 3 I2P and 6 onion peers out of
405      // 24 to protect 1, 3, and 2 (6 total, I2P has fewer candidates and so gets the
406      // unused localhost slot), plus 6 others for 12/24 total, sorted by longest uptime.
407      BOOST_CHECK(IsProtected(
408          24, [](NodeEvictionCandidate& c) {
409              c.m_connected = std::chrono::seconds{c.id};
410              c.m_is_local = (c.id == 15);
411              if (c.id == 12 || c.id == 14 || c.id == 17) {
412                  c.m_network = NET_I2P;
413              } else if (c.id > 17) { // 4 protected instead of usual 2
414                  c.m_network = NET_ONION;
415              } else {
416                  c.m_network = NET_IPV4;
417              }
418          },
419          /*protected_peer_ids=*/{0, 1, 2, 3, 4, 5, 12, 14, 15, 17, 18, 19},
420          /*unprotected_peer_ids=*/{6, 7, 8, 9, 10, 11, 13, 16, 20, 21, 22, 23},
421          random_context));
422  
423      // Combined test: expect having 1 localhost, 7 I2P and 4 onion peers out of
424      // 24 to protect 1 localhost, 2 I2P, and 3 onions (6 total), plus 6 others
425      // for 12/24 total, sorted by longest uptime.
426      BOOST_CHECK(IsProtected(
427          24, [](NodeEvictionCandidate& c) {
428              c.m_connected = std::chrono::seconds{c.id};
429              c.m_is_local = (c.id == 13);
430              if (c.id > 16) {
431                  c.m_network = NET_I2P;
432              } else if (c.id == 12 || c.id == 14 || c.id == 15 || c.id == 16) {
433                  c.m_network = NET_ONION;
434              } else {
435                  c.m_network = NET_IPV6;
436              }
437          },
438          /*protected_peer_ids=*/{0, 1, 2, 3, 4, 5, 12, 13, 14, 15, 17, 18},
439          /*unprotected_peer_ids=*/{6, 7, 8, 9, 10, 11, 16, 19, 20, 21, 22, 23},
440          random_context));
441  
442      // Combined test: expect having 8 localhost, 4 CJDNS, and 3 onion peers out
443      // of 24 to protect 2 of each (6 total), plus 6 others for 12/24 total,
444      // sorted by longest uptime.
445      BOOST_CHECK(IsProtected(
446          24, [](NodeEvictionCandidate& c) {
447              c.m_connected = std::chrono::seconds{c.id};
448              c.m_is_local = (c.id > 15);
449              if (c.id > 10 && c.id < 15) {
450                  c.m_network = NET_CJDNS;
451              } else if (c.id > 6 && c.id < 10) {
452                  c.m_network = NET_ONION;
453              } else {
454                  c.m_network = NET_IPV4;
455              }
456          },
457          /*protected_peer_ids=*/{0, 1, 2, 3, 4, 5, 7, 8, 11, 12, 16, 17},
458          /*unprotected_peer_ids=*/{6, 9, 10, 13, 14, 15, 18, 19, 20, 21, 22, 23},
459          random_context));
460  
461      // Tests with 4 networks...
462  
463      // Combined test: expect having 1 CJDNS, 1 I2P, 1 localhost and 1 onion peer
464      // out of 5 to protect 1 CJDNS, 0 I2P, 0 localhost, 0 onion and 1 other peer
465      // (2 total), sorted by longest uptime; stable sort breaks tie with array
466      // order of CJDNS first.
467      BOOST_CHECK(IsProtected(
468          5, [](NodeEvictionCandidate& c) {
469              c.m_connected = std::chrono::seconds{c.id};
470              c.m_is_local = (c.id == 3);
471              if (c.id == 4) {
472                  c.m_network = NET_CJDNS;
473              } else if (c.id == 1) {
474                  c.m_network = NET_I2P;
475              } else if (c.id == 2) {
476                  c.m_network = NET_ONION;
477              } else {
478                  c.m_network = NET_IPV6;
479              }
480          },
481          /*protected_peer_ids=*/{0, 4},
482          /*unprotected_peer_ids=*/{1, 2, 3},
483          random_context));
484  
485      // Combined test: expect having 1 CJDNS, 1 I2P, 1 localhost and 1 onion peer
486      // out of 7 to protect 1 CJDNS, 0, I2P, 0 localhost, 0 onion and 2 other
487      // peers (3 total) sorted by longest uptime; stable sort breaks tie with
488      // array order of CJDNS first.
489      BOOST_CHECK(IsProtected(
490          7, [](NodeEvictionCandidate& c) {
491              c.m_connected = std::chrono::seconds{c.id};
492              c.m_is_local = (c.id == 4);
493              if (c.id == 6) {
494                  c.m_network = NET_CJDNS;
495              } else if (c.id == 5) {
496                  c.m_network = NET_I2P;
497              } else if (c.id == 3) {
498                  c.m_network = NET_ONION;
499              } else {
500                  c.m_network = NET_IPV4;
501              }
502          },
503          /*protected_peer_ids=*/{0, 1, 6},
504          /*unprotected_peer_ids=*/{2, 3, 4, 5},
505          random_context));
506  
507      // Combined test: expect having 1 CJDNS, 1 I2P, 1 localhost and 1 onion peer
508      // out of 8 to protect 1 CJDNS, 1 I2P, 0 localhost, 0 onion and 2 other
509      // peers (4 total) sorted by longest uptime; stable sort breaks tie with
510      // array order of CJDNS first.
511      BOOST_CHECK(IsProtected(
512          8, [](NodeEvictionCandidate& c) {
513              c.m_connected = std::chrono::seconds{c.id};
514              c.m_is_local = (c.id == 3);
515              if (c.id == 5) {
516                  c.m_network = NET_CJDNS;
517              } else if (c.id == 6) {
518                  c.m_network = NET_I2P;
519              } else if (c.id == 3) {
520                  c.m_network = NET_ONION;
521              } else {
522                  c.m_network = NET_IPV6;
523              }
524          },
525          /*protected_peer_ids=*/{0, 1, 5, 6},
526          /*unprotected_peer_ids=*/{2, 3, 4, 7},
527          random_context));
528  
529      // Combined test: expect having 2 CJDNS, 2 I2P, 4 localhost, and 2 onion
530      // peers out of 16 to protect 1 CJDNS, 1 I2P, 1 localhost, 1 onion (4/16
531      // total), plus 4 others for 8 total, sorted by longest uptime.
532      BOOST_CHECK(IsProtected(
533          16, [](NodeEvictionCandidate& c) {
534              c.m_connected = std::chrono::seconds{c.id};
535              c.m_is_local = (c.id > 5);
536              if (c.id == 11 || c.id == 15) {
537                  c.m_network = NET_CJDNS;
538              } else if (c.id == 10 || c.id == 14) {
539                  c.m_network = NET_I2P;
540              } else if (c.id == 8 || c.id == 9) {
541                  c.m_network = NET_ONION;
542              } else {
543                  c.m_network = NET_IPV4;
544              }
545          },
546          /*protected_peer_ids=*/{0, 1, 2, 3, 6, 8, 10, 11},
547          /*unprotected_peer_ids=*/{4, 5, 7, 9, 12, 13, 14, 15},
548          random_context));
549  
550      // Combined test: expect having 6 CJDNS, 1 I2P, 1 localhost, and 4 onion
551      // peers out of 24 to protect 2 CJDNS, 1 I2P, 1 localhost, and 2 onions (6
552      // total), plus 6 others for 12/24 total, sorted by longest uptime.
553      BOOST_CHECK(IsProtected(
554          24, [](NodeEvictionCandidate& c) {
555              c.m_connected = std::chrono::seconds{c.id};
556              c.m_is_local = (c.id == 13);
557              if (c.id > 17) {
558                  c.m_network = NET_CJDNS;
559              } else if (c.id == 17) {
560                  c.m_network = NET_I2P;
561              } else if (c.id == 12 || c.id == 14 || c.id == 15 || c.id == 16) {
562                  c.m_network = NET_ONION;
563              } else {
564                  c.m_network = NET_IPV6;
565              }
566          },
567          /*protected_peer_ids=*/{0, 1, 2, 3, 4, 5, 12, 13, 14, 17, 18, 19},
568          /*unprotected_peer_ids=*/{6, 7, 8, 9, 10, 11, 15, 16, 20, 21, 22, 23},
569          random_context));
570  }
571  
572  // Returns true if any of the node ids in node_ids are selected for eviction.
573  bool IsEvicted(std::vector<NodeEvictionCandidate> candidates, const std::unordered_set<NodeId>& node_ids, FastRandomContext& random_context)
574  {
575      std::shuffle(candidates.begin(), candidates.end(), random_context);
576      const std::optional<NodeId> evicted_node_id = SelectNodeToEvict(std::move(candidates));
577      if (!evicted_node_id) {
578          return false;
579      }
580      return node_ids.contains(*evicted_node_id);
581  }
582  
583  // Create number_of_nodes random nodes, apply setup function candidate_setup_fn,
584  // apply eviction logic and then return true if any of the node ids in node_ids
585  // are selected for eviction.
586  bool IsEvicted(const int number_of_nodes, std::function<void(NodeEvictionCandidate&)> candidate_setup_fn, const std::unordered_set<NodeId>& node_ids, FastRandomContext& random_context)
587  {
588      std::vector<NodeEvictionCandidate> candidates = GetRandomNodeEvictionCandidates(number_of_nodes, random_context);
589      for (NodeEvictionCandidate& candidate : candidates) {
590          candidate_setup_fn(candidate);
591      }
592      return IsEvicted(candidates, node_ids, random_context);
593  }
594  
595  BOOST_AUTO_TEST_CASE(peer_eviction_test)
596  {
597      FastRandomContext random_context{true};
598  
599      for (int number_of_nodes = 0; number_of_nodes < 200; ++number_of_nodes) {
600          // Four nodes with the highest keyed netgroup values should be
601          // protected from eviction.
602          BOOST_CHECK(!IsEvicted(
603                          number_of_nodes, [number_of_nodes](NodeEvictionCandidate& candidate) {
604                              candidate.nKeyedNetGroup = number_of_nodes - candidate.id;
605                          },
606                          {0, 1, 2, 3}, random_context));
607  
608          // Eight nodes with the lowest minimum ping time should be protected
609          // from eviction.
610          BOOST_CHECK(!IsEvicted(
611                          number_of_nodes, [](NodeEvictionCandidate& candidate) {
612                              candidate.m_min_ping_time = std::chrono::microseconds{candidate.id};
613                          },
614                          {0, 1, 2, 3, 4, 5, 6, 7}, random_context));
615  
616          // Four nodes that most recently sent us novel transactions accepted
617          // into our mempool should be protected from eviction.
618          BOOST_CHECK(!IsEvicted(
619                          number_of_nodes, [number_of_nodes](NodeEvictionCandidate& candidate) {
620                              candidate.m_last_tx_time = std::chrono::seconds{number_of_nodes - candidate.id};
621                          },
622                          {0, 1, 2, 3}, random_context));
623  
624          // Up to eight non-tx-relay peers that most recently sent us novel
625          // blocks should be protected from eviction.
626          BOOST_CHECK(!IsEvicted(
627                          number_of_nodes, [number_of_nodes](NodeEvictionCandidate& candidate) {
628                              candidate.m_last_block_time = std::chrono::seconds{number_of_nodes - candidate.id};
629                              if (candidate.id <= 7) {
630                                  candidate.m_relay_txs = false;
631                                  candidate.fRelevantServices = true;
632                              }
633                          },
634                          {0, 1, 2, 3, 4, 5, 6, 7}, random_context));
635  
636          // Four peers that most recently sent us novel blocks should be
637          // protected from eviction.
638          BOOST_CHECK(!IsEvicted(
639                          number_of_nodes, [number_of_nodes](NodeEvictionCandidate& candidate) {
640                              candidate.m_last_block_time = std::chrono::seconds{number_of_nodes - candidate.id};
641                          },
642                          {0, 1, 2, 3}, random_context));
643  
644          // Combination of the previous two tests.
645          BOOST_CHECK(!IsEvicted(
646                          number_of_nodes, [number_of_nodes](NodeEvictionCandidate& candidate) {
647                              candidate.m_last_block_time = std::chrono::seconds{number_of_nodes - candidate.id};
648                              if (candidate.id <= 7) {
649                                  candidate.m_relay_txs = false;
650                                  candidate.fRelevantServices = true;
651                              }
652                          },
653                          {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}, random_context));
654  
655          // Combination of all tests above.
656          BOOST_CHECK(!IsEvicted(
657                          number_of_nodes, [number_of_nodes](NodeEvictionCandidate& candidate) {
658                              candidate.nKeyedNetGroup = number_of_nodes - candidate.id;           // 4 protected
659                              candidate.m_min_ping_time = std::chrono::microseconds{candidate.id}; // 8 protected
660                              candidate.m_last_tx_time = std::chrono::seconds{number_of_nodes - candidate.id};    // 4 protected
661                              candidate.m_last_block_time = std::chrono::seconds{number_of_nodes - candidate.id}; // 4 protected
662                          },
663                          {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19}, random_context));
664  
665          // An eviction is expected given >= 29 random eviction candidates. The eviction logic protects at most
666          // four peers by net group, eight by lowest ping time, four by last time of novel tx, up to eight non-tx-relay
667          // peers by last novel block time, and four more peers by last novel block time.
668          if (number_of_nodes >= 29) {
669              BOOST_CHECK(SelectNodeToEvict(GetRandomNodeEvictionCandidates(number_of_nodes, random_context)));
670          }
671  
672          // No eviction is expected given <= 20 random eviction candidates. The eviction logic protects at least
673          // four peers by net group, eight by lowest ping time, four by last time of novel tx and four peers by last
674          // novel block time.
675          if (number_of_nodes <= 20) {
676              BOOST_CHECK(!SelectNodeToEvict(GetRandomNodeEvictionCandidates(number_of_nodes, random_context)));
677          }
678  
679          // Cases left to test:
680          // * "If any remaining peers are preferred for eviction consider only them. [...]"
681          // * "Identify the network group with the most connections and youngest member. [...]"
682      }
683  }
684  
685  BOOST_AUTO_TEST_SUITE_END()