/ test / functional / p2p_eviction.py
p2p_eviction.py
  1  #!/usr/bin/env python3
  2  # Copyright (c) 2019-present The Bitcoin Core 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  )
 20  from test_framework.messages import (
 21      msg_pong,
 22      msg_tx,
 23  )
 24  from test_framework.p2p import (
 25      P2PDataStore,
 26      P2PInterface,
 27  )
 28  from test_framework.test_framework import BitcoinTestFramework
 29  from test_framework.util import assert_equal
 30  from test_framework.wallet import MiniWallet
 31  
 32  
 33  class SlowP2PDataStore(P2PDataStore):
 34      def on_ping(self, message):
 35          time.sleep(0.1)
 36          self.send_without_ping(msg_pong(message.nonce))
 37  
 38  
 39  class SlowP2PInterface(P2PInterface):
 40      def on_ping(self, message):
 41          time.sleep(0.1)
 42          self.send_without_ping(msg_pong(message.nonce))
 43  
 44  
 45  class P2PEvict(BitcoinTestFramework):
 46      def set_test_params(self):
 47          self.num_nodes = 1
 48          # The choice of maxconnections=32 results in a maximum of 21 inbound connections
 49          # (32 - 10 outbound - 1 feeler). 20 inbound peers are protected from eviction:
 50          # 4 by netgroup, 4 that sent us blocks, 4 that sent us transactions and 8 via lowest ping time
 51          self.extra_args = [['-maxconnections=32']]
 52  
 53      def run_test(self):
 54          protected_peers = set()  # peers that we expect to be protected from eviction
 55          current_peer = -1
 56          node = self.nodes[0]
 57          self.wallet = MiniWallet(node)
 58  
 59          self.log.info("Create 4 peers and protect them from eviction by sending us a block")
 60          for _ in range(4):
 61              block_peer = node.add_p2p_connection(SlowP2PDataStore())
 62              current_peer += 1
 63              block_peer.sync_with_ping()
 64              best_block = node.getbestblockhash()
 65              tip = int(best_block, 16)
 66              best_block_time = node.getblock(best_block)['time']
 67              block = create_block(tip, height=node.getblockcount() + 1, ntime=best_block_time + 1)
 68              block.solve()
 69              block_peer.send_blocks_and_test([block], node, success=True)
 70              protected_peers.add(current_peer)
 71  
 72          self.log.info("Create 5 slow-pinging peers, making them eviction candidates")
 73          for _ in range(5):
 74              node.add_p2p_connection(SlowP2PInterface())
 75              current_peer += 1
 76  
 77          self.log.info("Create 4 peers and protect them from eviction by sending us a tx")
 78          for i in range(4):
 79              txpeer = node.add_p2p_connection(SlowP2PInterface())
 80              current_peer += 1
 81              txpeer.sync_with_ping()
 82  
 83              tx = self.wallet.create_self_transfer()['tx']
 84              txpeer.send_without_ping(msg_tx(tx))
 85              protected_peers.add(current_peer)
 86  
 87          self.log.info("Create 8 peers and protect them from eviction by having faster pings")
 88          for _ in range(8):
 89              fastpeer = node.add_p2p_connection(P2PInterface())
 90              current_peer += 1
 91              self.wait_until(lambda: "ping" in fastpeer.last_message, timeout=10)
 92  
 93          # Make sure by asking the node what the actual min pings are
 94          peerinfo = node.getpeerinfo()
 95          pings = {}
 96          for i in range(len(peerinfo)):
 97              pings[i] = peerinfo[i]['minping'] if 'minping' in peerinfo[i] else 1000000
 98          sorted_pings = sorted(pings.items(), key=lambda x: x[1])
 99  
100          # Usually the 8 fast peers are protected. In rare case of unreliable pings,
101          # one of the slower peers might have a faster min ping though.
102          for i in range(8):
103              protected_peers.add(sorted_pings[i][0])
104  
105          self.log.info("Create peer that triggers the eviction mechanism")
106          node.add_p2p_connection(SlowP2PInterface())
107  
108          # One of the non-protected peers must be evicted. We can't be sure which one because
109          # 4 peers are protected via netgroup, which is identical for all peers,
110          # and the eviction mechanism doesn't preserve the order of identical elements.
111          evicted_peers = []
112          for i in range(len(node.p2ps)):
113              if not node.p2ps[i].is_connected:
114                  evicted_peers.append(i)
115  
116          self.log.info("Test that one peer was evicted")
117          self.log.debug("{} evicted peer: {}".format(len(evicted_peers), set(evicted_peers)))
118          assert_equal(len(evicted_peers), 1)
119  
120          self.log.info("Test that no peer expected to be protected was evicted")
121          self.log.debug("{} protected peers: {}".format(len(protected_peers), protected_peers))
122          assert evicted_peers[0] not in protected_peers
123  
124  
125  if __name__ == '__main__':
126      P2PEvict(__file__).main()