p2p_eviction.py raw

   1  #!/usr/bin/env python3
   2  # Copyright (c) 2019-2021 The Limenka developers
   3  # Distributed under the MIT software license, see the accompanying
   4  # file COPYING or http://www.opensource.org/licenses/mit-license.php.
   5  
   6  """ Test node eviction logic
   7  
   8  When the number of peers has reached the limit of maximum connections,
   9  the next connecting inbound peer will trigger the eviction mechanism.
  10  We cannot currently test the parts of the eviction logic that are based on
  11  address/netgroup since in the current framework, all peers are connecting from
  12  the same local address. See Issue #14210 for more info.
  13  Therefore, this test is limited to the remaining protection criteria.
  14  """
  15  import time
  16  
  17  from test_framework.blocktools import (
  18      create_block,
  19      create_coinbase,
  20  )
  21  from test_framework.messages import (
  22      msg_pong,
  23      msg_tx,
  24  )
  25  from test_framework.p2p import (
  26      P2PDataStore,
  27      P2PInterface,
  28  )
  29  from test_framework.test_framework import LimenkaTestFramework
  30  from test_framework.util import assert_equal
  31  from test_framework.wallet import MiniWallet
  32  
  33  
  34  class SlowP2PDataStore(P2PDataStore):
  35      def on_ping(self, message):
  36          time.sleep(0.1)
  37          self.send_message(msg_pong(message.nonce))
  38  
  39  
  40  class SlowP2PInterface(P2PInterface):
  41      def on_ping(self, message):
  42          time.sleep(0.1)
  43          self.send_message(msg_pong(message.nonce))
  44  
  45  
  46  class P2PEvict(LimenkaTestFramework):
  47      def set_test_params(self):
  48          self.num_nodes = 1
  49          # The choice of maxconnections=32 results in a maximum of 21 inbound connections
  50          # (32 - 10 outbound - 1 feeler). 20 inbound peers are protected from eviction:
  51          # 4 by netgroup, 4 that sent us blocks, 4 that sent us transactions and 8 via lowest ping time
  52          self.extra_args = [['-maxconnections=32']]
  53  
  54      def run_test(self):
  55          protected_peers = set()  # peers that we expect to be protected from eviction
  56          current_peer = -1
  57          node = self.nodes[0]
  58          self.wallet = MiniWallet(node)
  59  
  60          self.log.info("Create 4 peers and protect them from eviction by sending us a block")
  61          for _ in range(4):
  62              block_peer = node.add_p2p_connection(SlowP2PDataStore())
  63              current_peer += 1
  64              block_peer.sync_with_ping()
  65              best_block = node.getbestblockhash()
  66              tip = int(best_block, 16)
  67              best_block_time = node.getblock(best_block)['time']
  68              block = create_block(tip, create_coinbase(node.getblockcount() + 1), best_block_time + 1)
  69              block.solve()
  70              block_peer.send_blocks_and_test([block], node, success=True)
  71              protected_peers.add(current_peer)
  72  
  73          self.log.info("Create 5 slow-pinging peers, making them eviction candidates")
  74          for _ in range(5):
  75              node.add_p2p_connection(SlowP2PInterface())
  76              current_peer += 1
  77  
  78          self.log.info("Create 4 peers and protect them from eviction by sending us a tx")
  79          for i in range(4):
  80              txpeer = node.add_p2p_connection(SlowP2PInterface())
  81              current_peer += 1
  82              txpeer.sync_with_ping()
  83  
  84              tx = self.wallet.create_self_transfer()['tx']
  85              txpeer.send_message(msg_tx(tx))
  86              protected_peers.add(current_peer)
  87  
  88          self.log.info("Create 8 peers and protect them from eviction by having faster pings")
  89          for _ in range(8):
  90              fastpeer = node.add_p2p_connection(P2PInterface())
  91              current_peer += 1
  92              self.wait_until(lambda: "ping" in fastpeer.last_message, timeout=10)
  93  
  94          # Make sure by asking the node what the actual min pings are
  95          peerinfo = node.getpeerinfo()
  96          pings = {}
  97          for i in range(len(peerinfo)):
  98              pings[i] = peerinfo[i]['minping'] if 'minping' in peerinfo[i] else 1000000
  99          sorted_pings = sorted(pings.items(), key=lambda x: x[1])
 100  
 101          # Usually the 8 fast peers are protected. In rare case of unreliable pings,
 102          # one of the slower peers might have a faster min ping though.
 103          for i in range(8):
 104              protected_peers.add(sorted_pings[i][0])
 105  
 106          self.log.info("Create peer that triggers the eviction mechanism")
 107          node.add_p2p_connection(SlowP2PInterface())
 108  
 109          # One of the non-protected peers must be evicted. We can't be sure which one because
 110          # 4 peers are protected via netgroup, which is identical for all peers,
 111          # and the eviction mechanism doesn't preserve the order of identical elements.
 112          evicted_peers = []
 113          for i in range(len(node.p2ps)):
 114              if not node.p2ps[i].is_connected:
 115                  evicted_peers.append(i)
 116  
 117          self.log.info("Test that one peer was evicted")
 118          self.log.debug("{} evicted peer: {}".format(len(evicted_peers), set(evicted_peers)))
 119          assert_equal(len(evicted_peers), 1)
 120  
 121          self.log.info("Test that no peer expected to be protected was evicted")
 122          self.log.debug("{} protected peers: {}".format(len(protected_peers), protected_peers))
 123          assert evicted_peers[0] not in protected_peers
 124  
 125          self.log.info("Test that whitebind inbounds get extra eviction power")
 126          # Allow 10 inbound connections, set whitebind and forceinbound
 127          self.restart_node(0, extra_args=['-maxconnections=21', '-whitebind=127.0.0.1:30201', '-whitebind=forceinbound@127.0.0.1:30202'])
 128          self.log.debug("Fill connections with unprivileged peers")
 129          for i in range(10):
 130              node.add_p2p_connection(P2PInterface())
 131  
 132          # Create a peer that expects to be rejected
 133          # FIXME: "multiprocess, i686, DEBUG" CI task has a reliable timeout issue for v2transport
 134          class RejectedPeer(P2PInterface):
 135              def connection_lost(self, exc):
 136                  return
 137  
 138          allowed_peers = []
 139  
 140          self.log.debug("Generic inbound gets rejected when full")
 141          with node.assert_debug_log(["failed to find an eviction candidate - connection dropped (full)"]):
 142              node.add_p2p_connection(RejectedPeer(), wait_for_verack=False, supports_v2_p2p=False)
 143  
 144          self.log.debug("Default whitebind inbound gets rejected, even when full")
 145          with node.assert_debug_log(["failed to find an eviction candidate - connection dropped (full)"]):
 146              node.add_p2p_connection(RejectedPeer(), wait_for_verack=False, supports_v2_p2p=False, dstport=30201)
 147  
 148          self.log.debug("ForceInbound whitebind inbound gets connected, even when full")
 149          allowed_peers.append(node.add_p2p_connection(P2PInterface(), dstport=30202))
 150  
 151          peerinfo = node.getpeerinfo()
 152          assert_equal(len(peerinfo), 10)
 153          for peer in peerinfo:
 154              if "30202" in peer["addrbind"]:
 155                  assert peer["forced_inbound"]
 156              else:
 157                  assert not peer["forced_inbound"]
 158  
 159          self.log.debug("Generic inbound gets rejected when whitebind peer is filling inbound slot")
 160          with node.assert_debug_log(["failed to find an eviction candidate - connection dropped (full)"]):
 161              node.add_p2p_connection(RejectedPeer(), supports_v2_p2p=False, wait_for_verack=False)
 162  
 163  if __name__ == '__main__':
 164      P2PEvict(__file__).main()
 165