p2p_blockfilters.py raw
1 #!/usr/bin/env python3
2 # Copyright (c) 2019-2022 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 """Tests NODE_COMPACT_FILTERS (BIP 157/158).
6
7 Tests that a node configured with -blockfilterindex and -peerblockfilters signals
8 NODE_COMPACT_FILTERS and can serve cfilters, cfheaders and cfcheckpts.
9 """
10
11 from test_framework.messages import (
12 FILTER_TYPE_BASIC,
13 NODE_COMPACT_FILTERS,
14 hash256,
15 msg_getcfcheckpt,
16 msg_getcfheaders,
17 msg_getcfilters,
18 ser_uint256,
19 uint256_from_str,
20 )
21 from test_framework.p2p import P2PInterface
22 from test_framework.test_framework import LimenkaTestFramework
23 from test_framework.util import (
24 assert_equal,
25 )
26
27 class FiltersClient(P2PInterface):
28 def __init__(self):
29 super().__init__()
30 # Store the cfilters received.
31 self.cfilters = []
32
33 def pop_cfilters(self):
34 cfilters = self.cfilters
35 self.cfilters = []
36 return cfilters
37
38 def on_cfilter(self, message):
39 """Store cfilters received in a list."""
40 self.cfilters.append(message)
41
42
43 class CompactFiltersTest(LimenkaTestFramework):
44 def set_test_params(self):
45 self.setup_clean_chain = True
46 self.rpc_timeout = 480
47 self.num_nodes = 2
48 self.extra_args = [
49 ["-blockfilterindex", "-peerblockfilters"],
50 ["-blockfilterindex"],
51 ]
52
53 def run_test(self):
54 # Node 0 supports COMPACT_FILTERS, node 1 does not.
55 peer_0 = self.nodes[0].add_p2p_connection(FiltersClient())
56 peer_1 = self.nodes[1].add_p2p_connection(FiltersClient())
57
58 # Nodes 0 & 1 share the same first 999 blocks in the chain.
59 self.generate(self.nodes[0], 999)
60
61 # Stale blocks by disconnecting nodes 0 & 1, mining, then reconnecting
62 self.disconnect_nodes(0, 1)
63
64 stale_block_hash = self.generate(self.nodes[0], 1, sync_fun=self.no_op)[0]
65 self.nodes[0].syncwithvalidationinterfacequeue()
66 assert_equal(self.nodes[0].getblockcount(), 1000)
67
68 self.generate(self.nodes[1], 1001, sync_fun=self.no_op)
69 assert_equal(self.nodes[1].getblockcount(), 2000)
70
71 # Check that nodes have signalled NODE_COMPACT_FILTERS correctly.
72 assert peer_0.nServices & NODE_COMPACT_FILTERS != 0
73 assert peer_1.nServices & NODE_COMPACT_FILTERS == 0
74
75 # Check that the localservices is as expected.
76 assert int(self.nodes[0].getnetworkinfo()['localservices'], 16) & NODE_COMPACT_FILTERS != 0
77 assert int(self.nodes[1].getnetworkinfo()['localservices'], 16) & NODE_COMPACT_FILTERS == 0
78
79 self.log.info("get cfcheckpt on chain to be re-orged out.")
80 request = msg_getcfcheckpt(
81 filter_type=FILTER_TYPE_BASIC,
82 stop_hash=int(stale_block_hash, 16),
83 )
84 peer_0.send_and_ping(message=request)
85 response = peer_0.last_message['cfcheckpt']
86 assert_equal(response.filter_type, request.filter_type)
87 assert_equal(response.stop_hash, request.stop_hash)
88 assert_equal(len(response.headers), 1)
89
90 self.log.info("Reorg node 0 to a new chain.")
91 self.connect_nodes(0, 1)
92 self.sync_blocks(timeout=600)
93 self.nodes[0].syncwithvalidationinterfacequeue()
94
95 main_block_hash = self.nodes[0].getblockhash(1000)
96 assert main_block_hash != stale_block_hash, "node 0 chain did not reorganize"
97
98 self.log.info("Check that peers can fetch cfcheckpt on active chain.")
99 tip_hash = self.nodes[0].getbestblockhash()
100 request = msg_getcfcheckpt(
101 filter_type=FILTER_TYPE_BASIC,
102 stop_hash=int(tip_hash, 16),
103 )
104 peer_0.send_and_ping(request)
105 response = peer_0.last_message['cfcheckpt']
106 assert_equal(response.filter_type, request.filter_type)
107 assert_equal(response.stop_hash, request.stop_hash)
108
109 main_cfcheckpt = self.nodes[0].getblockfilter(main_block_hash, 'basic')['header']
110 tip_cfcheckpt = self.nodes[0].getblockfilter(tip_hash, 'basic')['header']
111 assert_equal(
112 response.headers,
113 [int(header, 16) for header in (main_cfcheckpt, tip_cfcheckpt)],
114 )
115
116 self.log.info("Check that peers can fetch cfcheckpt on stale chain.")
117 request = msg_getcfcheckpt(
118 filter_type=FILTER_TYPE_BASIC,
119 stop_hash=int(stale_block_hash, 16),
120 )
121 peer_0.send_and_ping(request)
122 response = peer_0.last_message['cfcheckpt']
123
124 stale_cfcheckpt = self.nodes[0].getblockfilter(stale_block_hash, 'basic')['header']
125 assert_equal(
126 response.headers,
127 [int(header, 16) for header in (stale_cfcheckpt, )],
128 )
129
130 self.log.info("Check that peers can fetch cfheaders on active chain.")
131 request = msg_getcfheaders(
132 filter_type=FILTER_TYPE_BASIC,
133 start_height=1,
134 stop_hash=int(main_block_hash, 16),
135 )
136 peer_0.send_and_ping(request)
137 response = peer_0.last_message['cfheaders']
138 main_cfhashes = response.hashes
139 assert_equal(len(main_cfhashes), 1000)
140 assert_equal(
141 compute_last_header(response.prev_header, response.hashes),
142 int(main_cfcheckpt, 16),
143 )
144
145 self.log.info("Check that peers can fetch cfheaders on stale chain.")
146 request = msg_getcfheaders(
147 filter_type=FILTER_TYPE_BASIC,
148 start_height=1,
149 stop_hash=int(stale_block_hash, 16),
150 )
151 peer_0.send_and_ping(request)
152 response = peer_0.last_message['cfheaders']
153 stale_cfhashes = response.hashes
154 assert_equal(len(stale_cfhashes), 1000)
155 assert_equal(
156 compute_last_header(response.prev_header, response.hashes),
157 int(stale_cfcheckpt, 16),
158 )
159
160 self.log.info("Check that peers can fetch cfilters.")
161 stop_hash = self.nodes[0].getblockhash(10)
162 request = msg_getcfilters(
163 filter_type=FILTER_TYPE_BASIC,
164 start_height=1,
165 stop_hash=int(stop_hash, 16),
166 )
167 peer_0.send_and_ping(request)
168 response = peer_0.pop_cfilters()
169 assert_equal(len(response), 10)
170
171 self.log.info("Check that cfilter responses are correct.")
172 for cfilter, cfhash, height in zip(response, main_cfhashes, range(1, 11)):
173 block_hash = self.nodes[0].getblockhash(height)
174 assert_equal(cfilter.filter_type, FILTER_TYPE_BASIC)
175 assert_equal(cfilter.block_hash, int(block_hash, 16))
176 computed_cfhash = uint256_from_str(hash256(cfilter.filter_data))
177 assert_equal(computed_cfhash, cfhash)
178
179 self.log.info("Check that peers can fetch cfilters for stale blocks.")
180 request = msg_getcfilters(
181 filter_type=FILTER_TYPE_BASIC,
182 start_height=1000,
183 stop_hash=int(stale_block_hash, 16),
184 )
185 peer_0.send_and_ping(request)
186 response = peer_0.pop_cfilters()
187 assert_equal(len(response), 1)
188
189 cfilter = response[0]
190 assert_equal(cfilter.filter_type, FILTER_TYPE_BASIC)
191 assert_equal(cfilter.block_hash, int(stale_block_hash, 16))
192 computed_cfhash = uint256_from_str(hash256(cfilter.filter_data))
193 assert_equal(computed_cfhash, stale_cfhashes[999])
194
195 self.log.info("Requests to node 1 without NODE_COMPACT_FILTERS results in disconnection.")
196 requests = [
197 msg_getcfcheckpt(
198 filter_type=FILTER_TYPE_BASIC,
199 stop_hash=int(main_block_hash, 16),
200 ),
201 msg_getcfheaders(
202 filter_type=FILTER_TYPE_BASIC,
203 start_height=1000,
204 stop_hash=int(main_block_hash, 16),
205 ),
206 msg_getcfilters(
207 filter_type=FILTER_TYPE_BASIC,
208 start_height=1000,
209 stop_hash=int(main_block_hash, 16),
210 ),
211 ]
212 for request in requests:
213 peer_1 = self.nodes[1].add_p2p_connection(P2PInterface())
214 with self.nodes[1].assert_debug_log(expected_msgs=["requested unsupported block filter type"]):
215 peer_1.send_message(request)
216 peer_1.wait_for_disconnect()
217
218 self.log.info("Check that invalid requests result in disconnection.")
219 requests = [
220 # Requesting too many filters results in disconnection.
221 (
222 msg_getcfilters(
223 filter_type=FILTER_TYPE_BASIC,
224 start_height=0,
225 stop_hash=int(main_block_hash, 16),
226 ), "requested too many cfilters/cfheaders"
227 ),
228 # Requesting too many filter headers results in disconnection.
229 (
230 msg_getcfheaders(
231 filter_type=FILTER_TYPE_BASIC,
232 start_height=0,
233 stop_hash=int(tip_hash, 16),
234 ), "requested too many cfilters/cfheaders"
235 ),
236 # Requesting unknown filter type results in disconnection.
237 (
238 msg_getcfcheckpt(
239 filter_type=255,
240 stop_hash=int(main_block_hash, 16),
241 ), "requested unsupported block filter type"
242 ),
243 # Requesting unknown hash results in disconnection.
244 (
245 msg_getcfcheckpt(
246 filter_type=FILTER_TYPE_BASIC,
247 stop_hash=123456789,
248 ), "requested invalid block hash"
249 ),
250 (
251 # Request with (start block height > stop block height) results in disconnection.
252 msg_getcfheaders(
253 filter_type=FILTER_TYPE_BASIC,
254 start_height=1000,
255 stop_hash=int(self.nodes[0].getblockhash(999), 16),
256 ), "sent invalid getcfilters/getcfheaders with start height 1000 and stop height 999"
257 ),
258 ]
259 for request, expected_log_msg in requests:
260 peer_0 = self.nodes[0].add_p2p_connection(P2PInterface())
261 with self.nodes[0].assert_debug_log(expected_msgs=[expected_log_msg]):
262 peer_0.send_message(request)
263 peer_0.wait_for_disconnect()
264
265 self.log.info("Test -peerblockfilters without -blockfilterindex raises an error")
266 self.stop_node(0)
267 self.nodes[0].extra_args = ["-peerblockfilters"]
268 msg = "Error: Cannot set -peerblockfilters without -blockfilterindex."
269 self.nodes[0].assert_start_raises_init_error(expected_msg=msg)
270
271 self.log.info("Test unknown value to -blockfilterindex raises an error")
272 self.nodes[0].extra_args = ["-blockfilterindex=abc"]
273 msg = "Error: Unknown -blockfilterindex value abc."
274 self.nodes[0].assert_start_raises_init_error(expected_msg=msg)
275
276 def compute_last_header(prev_header, hashes):
277 """Compute the last filter header from a starting header and a sequence of filter hashes."""
278 header = ser_uint256(prev_header)
279 for filter_hash in hashes:
280 header = hash256(ser_uint256(filter_hash) + header)
281 return uint256_from_str(header)
282
283
284 if __name__ == '__main__':
285 CompactFiltersTest(__file__).main()
286