p2p_blockfilters.py
1 #!/usr/bin/env python3 2 # Copyright (c) 2019-2022 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_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(BitcoinTestFramework): 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().main()