p2p_bip434_feature.py raw
1 #!/usr/bin/env python3
2 # Copyright (c) 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 BIP434 feature negotiation."""
7
8 from test_framework.messages import (
9 msg_version,
10 ser_compact_size,
11 )
12 from test_framework.p2p import (
13 P2PInterface,
14 P2P_SERVICES,
15 P2P_SUBVERSION,
16 P2P_VERSION_RELAY,
17 )
18 from test_framework.test_framework import BitcoinTestFramework
19 from test_framework.util import assert_equal
20
21 # Pre-BIP434 protocol version
22 PRE_FEATURE_VERSION = 70016
23 # Protocol version which enables BIP434 FEATURE negotiation
24 FEATURE_VERSION = 70017
25 # BIP434 wire-format limits
26 MIN_FEATUREID_LENGTH = 4
27 MAX_FEATUREID_LENGTH = 80
28 MAX_FEATUREDATA_LENGTH = 512
29
30
31 class RawFeature:
32 """A FEATURE message with a hand-crafted payload."""
33 msgtype = b"feature"
34
35 def __init__(self, payload):
36 self.payload = payload
37
38 def serialize(self):
39 return self.payload
40
41 def __repr__(self):
42 return f"RawFeature(payload_len={len(self.payload)})"
43
44
45 def feature_wire(feature_id, feature_data, *, trailing=b""):
46 """Build a FEATURE payload plus optional trailing bytes."""
47 if isinstance(feature_id, str):
48 feature_id = feature_id.encode()
49 return (ser_compact_size(len(feature_id)) + feature_id
50 + ser_compact_size(len(feature_data)) + feature_data
51 + trailing)
52
53
54 def _version_msg(nversion):
55 v = msg_version()
56 v.nVersion = nversion
57 v.strSubVer = P2P_SUBVERSION
58 v.nServices = P2P_SERVICES
59 v.relay = P2P_VERSION_RELAY
60 return v
61
62
63 class FeaturePeer(P2PInterface):
64 """P2PInterface that counts FEATURE messages received."""
65
66 def __init__(self):
67 super().__init__()
68 self.got_feature_count = 0
69 self.last_feature = None
70
71 def on_feature(self, message):
72 self.got_feature_count += 1
73 self.last_feature = message
74
75
76 class FeaturePeerNoVerack(FeaturePeer):
77 """Peer that records but does not auto-reply to the node's VERSION, so the
78 node stays in the post-VERSION / pre-VERACK window where FEATURE messages
79 are valid to send."""
80
81 def on_version(self, message):
82 self.nServices = message.nServices
83 self.relay = message.relay
84
85
86 class P2PBIP434FeatureTest(BitcoinTestFramework):
87 def set_test_params(self):
88 self.num_nodes = 1
89 # peertimeout=999 prevents the node from kicking the peer for being idle
90 self.extra_args = [["-debug=net", "-peertimeout=999"]]
91
92 def run_test(self):
93 self.test_advertised_version()
94 self.test_no_feature_to_pre_70017_peer()
95 self.test_features_announced_to_modern_peer()
96 self.test_feature_after_verack_disconnects()
97 self.test_feature_before_version_ignored()
98 self.test_feature_id_length_boundaries()
99 self.test_feature_data_length_boundaries()
100 self.test_trailing_bytes_disconnect()
101 self.test_truncated_feature_id_disconnect()
102 self.test_truncated_feature_data_disconnect()
103 self.test_non_ascii_feature_id_accepted()
104 self.test_many_features_in_handshake()
105 self.test_recv_feature_from_pre_70017_peer()
106
107 def _silent_peer(self, *, nversion=FEATURE_VERSION):
108 peer = self.nodes[0].add_p2p_connection(
109 FeaturePeerNoVerack(),
110 send_version=False, wait_for_verack=False,
111 )
112 peer.send_without_ping(_version_msg(nversion))
113 peer.wait_for_verack()
114 return peer
115
116 def _expect_accept(self, payload, *, log_substring="unknown feature advertised",
117 nversion=FEATURE_VERSION):
118 peer = self._silent_peer(nversion=nversion)
119 with self.nodes[0].assert_debug_log([log_substring], timeout=2):
120 peer.send_without_ping(RawFeature(payload))
121 assert peer.is_connected, "peer disconnected after a well-formed FEATURE"
122 self.nodes[0].disconnect_p2ps()
123
124 def _expect_disconnect(self, payload, *, log_substring="invalid feature payload",
125 nversion=FEATURE_VERSION):
126 peer = self._silent_peer(nversion=nversion)
127 with self.nodes[0].assert_debug_log([log_substring], timeout=2):
128 peer.send_without_ping(RawFeature(payload))
129 peer.wait_for_disconnect()
130
131 def test_advertised_version(self):
132 self.log.info("Test that node advertises FEATURE to peer with protocol version 70017")
133 peer = self.nodes[0].add_p2p_connection(FeaturePeer())
134 assert_equal(peer.last_message["version"].nVersion, FEATURE_VERSION)
135 self.nodes[0].disconnect_p2ps()
136
137 def test_no_feature_to_pre_70017_peer(self):
138 self.log.info("Test that node doesn't send FEATURE to a peer with protocol version <70017")
139 peer = self.nodes[0].add_p2p_connection(
140 FeaturePeer(), send_version=False, wait_for_verack=False,
141 )
142 peer.send_without_ping(_version_msg(PRE_FEATURE_VERSION))
143 peer.wait_for_verack()
144 # The node's full handshake has now been delivered to us; if any
145 # FEATURE would have been sent it would be in last_message by now.
146 assert_equal(peer.got_feature_count, 0)
147 self.nodes[0].disconnect_p2ps()
148
149 def test_features_announced_to_modern_peer(self):
150 self.log.info("Test that node announces correct number of features to a 70017 peer")
151 peer = self.nodes[0].add_p2p_connection(FeaturePeer())
152 assert_equal(peer.got_feature_count, 0)
153 self.nodes[0].disconnect_p2ps()
154
155 def test_feature_after_verack_disconnects(self):
156 self.log.info("Test that FEATURE after VERACK triggers disconnect")
157 peer = self.nodes[0].add_p2p_connection(FeaturePeer())
158 peer.sync_with_ping() # ensure node has set fSuccessfullyConnected
159 with self.nodes[0].assert_debug_log(["feature received after verack"], timeout=2):
160 peer.send_without_ping(RawFeature(feature_wire(b"abcd", b"")))
161 peer.wait_for_disconnect()
162
163 def test_feature_before_version_ignored(self):
164 self.log.info("Test that FEATURE before any VERSION is silently ignored")
165 peer = self.nodes[0].add_p2p_connection(
166 FeaturePeer(), send_version=False, wait_for_verack=False,
167 )
168 with self.nodes[0].assert_debug_log(
169 ["non-version message before version handshake"], timeout=2,
170 ):
171 peer.send_without_ping(RawFeature(feature_wire(b"abcd", b"")))
172 assert peer.is_connected
173 self.nodes[0].disconnect_p2ps()
174
175 def test_feature_id_length_boundaries(self):
176 self.log.info("Test feature_id length boundaries")
177 for length, accept in [(0, False),
178 (3, False),
179 (MIN_FEATUREID_LENGTH, True),
180 (MAX_FEATUREID_LENGTH, True),
181 (MAX_FEATUREID_LENGTH + 1, False)]:
182 payload = feature_wire(b"a" * length, b"")
183 if accept:
184 self._expect_accept(payload)
185 else:
186 self._expect_disconnect(payload)
187
188 def test_feature_data_length_boundaries(self):
189 self.log.info("Test feature_data length boundaries")
190 for length, accept in [(0, True),
191 (MAX_FEATUREDATA_LENGTH, True),
192 (MAX_FEATUREDATA_LENGTH + 1, False)]:
193 payload = feature_wire(b"abcd", b"\x00" * length)
194 if accept:
195 self._expect_accept(payload)
196 else:
197 self._expect_disconnect(payload)
198
199 def test_trailing_bytes_disconnect(self):
200 self.log.info("Test that trailing bytes after data triggers disconnect")
201 self._expect_disconnect(
202 feature_wire(b"abcd", b"", trailing=b"\x00"),
203 )
204
205 def test_truncated_feature_id_disconnect(self):
206 self.log.info("Test that truncated feature_id triggers disconnect")
207 payload = ser_compact_size(10) + b"abcde"
208 self._expect_disconnect(payload)
209
210 def test_truncated_feature_data_disconnect(self):
211 self.log.info("Test that truncated feature_data triggers disconnect")
212 payload = (ser_compact_size(MIN_FEATUREID_LENGTH) + b"abcd"
213 + ser_compact_size(10) + b"xx")
214 self._expect_disconnect(payload)
215
216 def test_non_ascii_feature_id_accepted(self):
217 self.log.info("Test that feature_id with non-ASCII bytes is still accepted")
218 # BIP says SHOULD, not MUST, on this
219 self._expect_accept(feature_wire(b"\x00\xff\x01\x7f", b""))
220
221 def test_many_features_in_handshake(self):
222 self.log.info("Test multiple FEATURE advertisements")
223 peer = self._silent_peer()
224 with self.nodes[0].assert_debug_log(["unknown feature advertised"], timeout=2):
225 for i in range(16):
226 peer.send_without_ping(
227 RawFeature(feature_wire(f"feat{i:04d}".encode(), b""))
228 )
229 assert peer.is_connected
230 self.nodes[0].disconnect_p2ps()
231
232 def test_recv_feature_from_pre_70017_peer(self):
233 self.log.info("Test that FEATURE from <70017 peer triggers disconnect")
234 self._expect_disconnect(
235 feature_wire(b"abcd", b""),
236 log_substring="feature received with incompatible version",
237 nversion=PRE_FEATURE_VERSION,
238 )
239
240
241 if __name__ == "__main__":
242 P2PBIP434FeatureTest(__file__).main()
243