net_peer_eviction_tests.cpp raw

   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 = NodeSeconds{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 = NodeSeconds{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 = NodeSeconds{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 = NodeSeconds{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 = NodeSeconds{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 = NodeSeconds{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 = NodeSeconds{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 = NodeSeconds{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 = NodeSeconds{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 = NodeSeconds{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 = NodeSeconds{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 = NodeSeconds{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 = NodeSeconds{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 = NodeSeconds{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 = NodeSeconds{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 = NodeSeconds{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 = NodeSeconds{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 = NodeSeconds{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 = NodeSeconds{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 = NodeSeconds{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 = NodeSeconds{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 = NodeSeconds{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 = NodeSeconds{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 = NodeSeconds{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 = NodeSeconds{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 = NodeSeconds{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 = NodeSeconds{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 = NodeSeconds{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()
 686