p2p_handshake.py raw

   1  #!/usr/bin/env python3
   2  # Copyright (c) 2024 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 P2P behaviour during the handshake phase (VERSION, VERACK messages).
   7  """
   8  import itertools
   9  import random
  10  import time
  11  
  12  from test_framework.test_framework import LimenkaTestFramework
  13  from test_framework.messages import (
  14      NODE_REDUCED_DATA,
  15      NODE_NETWORK,
  16      NODE_NETWORK_LIMITED,
  17      NODE_NONE,
  18      NODE_P2P_V2,
  19      NODE_WITNESS,
  20      msg_version,
  21  )
  22  from test_framework.p2p import (
  23      P2PInterface,
  24      P2P_SERVICES,
  25      P2P_SUBVERSION,
  26      P2P_VERSION,
  27  )
  28  from test_framework.util import (
  29      assert_equal,
  30      p2p_port,
  31  )
  32  
  33  
  34  # Desirable service flags for outbound non-pruned and pruned peers. Note that
  35  # the desirable service flags for pruned peers are dynamic and only apply if
  36  #  1. the peer's service flag NODE_NETWORK_LIMITED is set *and*
  37  #  2. the local chain is close to the tip (<24h)
  38  
  39  # Base service flags (without BIP-110)
  40  BASE_SERVICE_FLAGS_FULL = NODE_NETWORK | NODE_WITNESS
  41  BASE_SERVICE_FLAGS_PRUNED = NODE_NETWORK_LIMITED | NODE_WITNESS
  42  
  43  # Full service flags (with BIP-110)
  44  FULL_SERVICE_FLAGS_FULL = NODE_NETWORK | NODE_WITNESS | NODE_REDUCED_DATA
  45  FULL_SERVICE_FLAGS_PRUNED = NODE_NETWORK_LIMITED | NODE_WITNESS | NODE_REDUCED_DATA
  46  
  47  
  48  class P2PHandshakeTest(LimenkaTestFramework):
  49      def set_test_params(self):
  50          self.num_nodes = 1
  51          self.extra_args = [
  52              ["-maxstaleoutbound=2",],
  53          ]
  54  
  55      def add_outbound_connection(self, node, connection_type, services, wait_for_disconnect):
  56          peer = node.add_outbound_p2p_connection(
  57              P2PInterface(), p2p_idx=0, wait_for_disconnect=wait_for_disconnect,
  58              connection_type=connection_type, services=services,
  59              supports_v2_p2p=self.options.v2transport, advertise_v2_p2p=self.options.v2transport)
  60          if not wait_for_disconnect:
  61              # check that connection is alive past the version handshake and disconnect manually
  62              peer.sync_with_ping()
  63              peer.peer_disconnect()
  64              peer.wait_for_disconnect()
  65          self.wait_until(lambda: len(node.getpeerinfo()) == 0)
  66  
  67      def test_desirable_service_flags(self, node, service_flag_tests, desirable_service_flags, expect_disconnect):
  68          """Check that connecting to a peer either fails or succeeds depending on its offered
  69             service flags in the VERSION message. The test is exercised for all relevant
  70             outbound connection types where the desirable service flags check is done."""
  71          CONNECTION_TYPES = ["outbound-full-relay", "block-relay-only", "addr-fetch"]
  72          for conn_type, services in itertools.product(CONNECTION_TYPES, service_flag_tests):
  73              if self.options.v2transport:
  74                  services |= NODE_P2P_V2
  75              expected_result = "disconnect" if expect_disconnect else "connect"
  76              self.log.info(f'    - services 0x{services:08x}, type "{conn_type}" [{expected_result}]')
  77              if expect_disconnect:
  78                  assert (services & desirable_service_flags) != desirable_service_flags
  79                  expected_debug_log = f'does not offer the expected services ' \
  80                          f'({services:08x} offered, {desirable_service_flags:08x} expected)'
  81                  with node.assert_debug_log([expected_debug_log]):
  82                      self.add_outbound_connection(node, conn_type, services, wait_for_disconnect=True)
  83              else:
  84                  assert (services & desirable_service_flags) == desirable_service_flags
  85                  self.add_outbound_connection(node, conn_type, services, wait_for_disconnect=False)
  86  
  87      def test_startingheight(self, node):
  88          for fake_startheight in [-2**31, -1, 0, 1000000, 2**31-1] + [random.randint(-2**31, 2**31) for _ in range(5)]:
  89              peer = node.add_p2p_connection(P2PInterface(), send_version=False, wait_for_verack=False)
  90              version = msg_version()
  91              version.nVersion = P2P_VERSION
  92              version.strSubVer = P2P_SUBVERSION
  93              version.nServices = P2P_SERVICES
  94              version.nStartingHeight = fake_startheight
  95              peer.send_message(version)
  96              peer.wait_for_verack()
  97              peer_info = node.getpeerinfo()[-1]
  98              assert_equal(peer_info['startingheight'], fake_startheight)
  99              peer.peer_disconnect()
 100  
 101      def generate_at_mocktime(self, time):
 102          self.nodes[0].setmocktime(time)
 103          self.generate(self.nodes[0], 1)
 104          self.nodes[0].setmocktime(0)
 105  
 106      def run_test(self):
 107          node = self.nodes[0]
 108  
 109          self.log.info("Check that peers lacking base service flags are disconnected")
 110          # These should always be disconnected regardless of BIP-110 counter
 111          self.test_desirable_service_flags(node, [NODE_NONE, NODE_NETWORK, NODE_WITNESS],
 112                                            BASE_SERVICE_FLAGS_FULL, expect_disconnect=True)
 113  
 114          self.log.info("Check that first 2 non-BIP110 peers connect, 3rd is rejected")
 115          # Connect first 2 non-BIP110 peers and keep them connected
 116          non_bip110_services = NODE_NETWORK | NODE_WITNESS
 117          if self.options.v2transport:
 118              non_bip110_services |= NODE_P2P_V2
 119          peer1 = node.add_outbound_p2p_connection(
 120              P2PInterface(), p2p_idx=0, wait_for_disconnect=False,
 121              connection_type="outbound-full-relay", services=non_bip110_services,
 122              supports_v2_p2p=self.options.v2transport, advertise_v2_p2p=self.options.v2transport)
 123          peer1.sync_with_ping()
 124          peer2 = node.add_outbound_p2p_connection(
 125              P2PInterface(), p2p_idx=1, wait_for_disconnect=False,
 126              connection_type="outbound-full-relay", services=non_bip110_services,
 127              supports_v2_p2p=self.options.v2transport, advertise_v2_p2p=self.options.v2transport)
 128          peer2.sync_with_ping()
 129          assert len(node.getpeerinfo()) == 2
 130          # Third non-BIP110 peer should be rejected
 131          with node.assert_debug_log(["peer lacks NODE_REDUCED_DATA and already have 2 non-BIP110 outbound peers"]):
 132              node.add_outbound_p2p_connection(
 133                  P2PInterface(), p2p_idx=2, wait_for_disconnect=True,
 134                  connection_type="outbound-full-relay", services=non_bip110_services,
 135                  supports_v2_p2p=self.options.v2transport, advertise_v2_p2p=self.options.v2transport)
 136          # Clean up - disconnect the 2 non-BIP110 peers
 137          peer1.peer_disconnect()
 138          peer2.peer_disconnect()
 139          peer1.wait_for_disconnect()
 140          peer2.wait_for_disconnect()
 141          self.wait_until(lambda: len(node.getpeerinfo()) == 0)
 142  
 143          self.log.info("Check that BIP110 peers always connect")
 144          self.test_desirable_service_flags(node, [NODE_NETWORK | NODE_WITNESS | NODE_REDUCED_DATA],
 145                                            BASE_SERVICE_FLAGS_FULL, expect_disconnect=False)
 146  
 147          self.log.info("Check that limited peers are only desired if the local chain is close to the tip (<24h)")
 148          self.generate_at_mocktime(int(time.time()) - 25 * 3600)  # tip outside the 24h window, should fail
 149          self.test_desirable_service_flags(node, [NODE_NETWORK_LIMITED | NODE_WITNESS | NODE_REDUCED_DATA],
 150                                            BASE_SERVICE_FLAGS_FULL, expect_disconnect=True)
 151          self.generate_at_mocktime(int(time.time()) - 23 * 3600)  # tip inside the 24h window, should succeed
 152          self.test_desirable_service_flags(node, [NODE_NETWORK_LIMITED | NODE_WITNESS | NODE_REDUCED_DATA],
 153                                            BASE_SERVICE_FLAGS_PRUNED, expect_disconnect=False)
 154  
 155          self.log.info("Check that feeler connections get disconnected immediately")
 156          with node.assert_debug_log(["feeler connection completed"]):
 157              self.add_outbound_connection(node, "feeler", NODE_NONE, wait_for_disconnect=True)
 158  
 159          self.log.info("Check that connecting to ourself leads to immediate disconnect")
 160          with node.assert_debug_log(["connected to self", "disconnecting"]):
 161              node_listen_addr = f"127.0.0.1:{p2p_port(0)}"
 162              node.addconnection(node_listen_addr, "outbound-full-relay", self.options.v2transport)
 163              self.wait_until(lambda: len(node.getpeerinfo()) == 0)
 164  
 165          self.log.info("Check that peer's announced starting height is remembered")
 166          self.test_startingheight(node)
 167  
 168  
 169  if __name__ == '__main__':
 170      P2PHandshakeTest(__file__).main()
 171