p2p_blockfilters.py
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 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 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()