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