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