p2p_unrequested_blocks.py
1 #!/usr/bin/env python3 2 # Copyright (c) 2015-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 """Test processing of unrequested blocks. 6 7 Setup: two nodes, node0 + node1, not connected to each other. Node1 will have 8 nMinimumChainWork set to 0x10, so it won't process low-work unrequested blocks. 9 10 We have one P2PInterface connection to node0 called test_node, and one to node1 11 called min_work_node. 12 13 The test: 14 1. Generate one block on each node, to leave IBD. 15 16 2. Mine a new block on each tip, and deliver to each node from node's peer. 17 The tip should advance for node0, but node1 should skip processing due to 18 nMinimumChainWork. 19 20 Node1 is unused in tests 3-7: 21 22 3. Mine a block that forks from the genesis block, and deliver to test_node. 23 Node0 should not process this block (just accept the header), because it 24 is unrequested and doesn't have more or equal work to the tip. 25 26 4a,b. Send another two blocks that build on the forking block. 27 Node0 should process the second block but be stuck on the shorter chain, 28 because it's missing an intermediate block. 29 30 4c.Send 288 more blocks on the longer chain (the number of blocks ahead 31 we currently store). 32 Node0 should process all but the last block (too far ahead in height). 33 34 5. Send a duplicate of the block in #3 to Node0. 35 Node0 should not process the block because it is unrequested, and stay on 36 the shorter chain. 37 38 6. Send Node0 an inv for the height 3 block produced in #4 above. 39 Node0 should figure out that Node0 has the missing height 2 block and send a 40 getdata. 41 42 7. Send Node0 the missing block again. 43 Node0 should process and the tip should advance. 44 45 8. Create a fork which is invalid at a height longer than the current chain 46 (ie to which the node will try to reorg) but which has headers built on top 47 of the invalid block. Check that we get disconnected if we send more headers 48 on the chain the node now knows to be invalid. 49 50 9. Test Node1 is able to sync when connected to node0 (which should have sufficient 51 work on its chain). 52 """ 53 54 import time 55 56 from test_framework.blocktools import create_block, create_coinbase, create_tx_with_script 57 from test_framework.messages import CBlockHeader, CInv, MSG_BLOCK, msg_block, msg_headers, msg_inv 58 from test_framework.p2p import p2p_lock, P2PInterface 59 from test_framework.test_framework import BitcoinTestFramework 60 from test_framework.util import ( 61 assert_equal, 62 assert_raises_rpc_error, 63 ) 64 65 66 class AcceptBlockTest(BitcoinTestFramework): 67 def set_test_params(self): 68 self.setup_clean_chain = True 69 self.num_nodes = 2 70 self.extra_args = [[], ["-minimumchainwork=0x10"]] 71 72 def setup_network(self): 73 self.setup_nodes() 74 75 def check_hash_in_chaintips(self, node, blockhash): 76 tips = node.getchaintips() 77 for x in tips: 78 if x["hash"] == blockhash: 79 return True 80 return False 81 82 def run_test(self): 83 test_node = self.nodes[0].add_p2p_connection(P2PInterface()) 84 min_work_node = self.nodes[1].add_p2p_connection(P2PInterface()) 85 86 # 1. Have nodes mine a block (leave IBD) 87 [self.generate(n, 1, sync_fun=self.no_op) for n in self.nodes] 88 tips = [int("0x" + n.getbestblockhash(), 0) for n in self.nodes] 89 90 # 2. Send one block that builds on each tip. 91 # This should be accepted by node0 92 blocks_h2 = [] # the height 2 blocks on each node's chain 93 block_time = int(time.time()) + 1 94 for i in range(2): 95 blocks_h2.append(create_block(tips[i], create_coinbase(2), block_time)) 96 blocks_h2[i].solve() 97 block_time += 1 98 test_node.send_and_ping(msg_block(blocks_h2[0])) 99 100 with self.nodes[1].assert_debug_log(expected_msgs=[f"AcceptBlockHeader: not adding new block header {blocks_h2[1].hash}, missing anti-dos proof-of-work validation"]): 101 min_work_node.send_and_ping(msg_block(blocks_h2[1])) 102 103 assert_equal(self.nodes[0].getblockcount(), 2) 104 assert_equal(self.nodes[1].getblockcount(), 1) 105 106 # Ensure that the header of the second block was also not accepted by node1 107 assert_equal(self.check_hash_in_chaintips(self.nodes[1], blocks_h2[1].hash), False) 108 self.log.info("First height 2 block accepted by node0; correctly rejected by node1") 109 110 # 3. Send another block that builds on genesis. 111 block_h1f = create_block(int("0x" + self.nodes[0].getblockhash(0), 0), create_coinbase(1), block_time) 112 block_time += 1 113 block_h1f.solve() 114 test_node.send_and_ping(msg_block(block_h1f)) 115 116 tip_entry_found = False 117 for x in self.nodes[0].getchaintips(): 118 if x['hash'] == block_h1f.hash: 119 assert_equal(x['status'], "headers-only") 120 tip_entry_found = True 121 assert tip_entry_found 122 assert_raises_rpc_error(-1, "Block not found on disk", self.nodes[0].getblock, block_h1f.hash) 123 124 # 4. Send another two block that build on the fork. 125 block_h2f = create_block(block_h1f.sha256, create_coinbase(2), block_time) 126 block_time += 1 127 block_h2f.solve() 128 test_node.send_and_ping(msg_block(block_h2f)) 129 130 # Since the earlier block was not processed by node, the new block 131 # can't be fully validated. 132 tip_entry_found = False 133 for x in self.nodes[0].getchaintips(): 134 if x['hash'] == block_h2f.hash: 135 assert_equal(x['status'], "headers-only") 136 tip_entry_found = True 137 assert tip_entry_found 138 139 # But this block should be accepted by node since it has equal work. 140 self.nodes[0].getblock(block_h2f.hash) 141 self.log.info("Second height 2 block accepted, but not reorg'ed to") 142 143 # 4b. Now send another block that builds on the forking chain. 144 block_h3 = create_block(block_h2f.sha256, create_coinbase(3), block_h2f.nTime+1) 145 block_h3.solve() 146 test_node.send_and_ping(msg_block(block_h3)) 147 148 # Since the earlier block was not processed by node, the new block 149 # can't be fully validated. 150 tip_entry_found = False 151 for x in self.nodes[0].getchaintips(): 152 if x['hash'] == block_h3.hash: 153 assert_equal(x['status'], "headers-only") 154 tip_entry_found = True 155 assert tip_entry_found 156 self.nodes[0].getblock(block_h3.hash) 157 158 # But this block should be accepted by node since it has more work. 159 self.nodes[0].getblock(block_h3.hash) 160 self.log.info("Unrequested more-work block accepted") 161 162 # 4c. Now mine 288 more blocks and deliver; all should be processed but 163 # the last (height-too-high) on node (as long as it is not missing any headers) 164 tip = block_h3 165 all_blocks = [] 166 for i in range(288): 167 next_block = create_block(tip.sha256, create_coinbase(i + 4), tip.nTime+1) 168 next_block.solve() 169 all_blocks.append(next_block) 170 tip = next_block 171 172 # Now send the block at height 5 and check that it wasn't accepted (missing header) 173 test_node.send_and_ping(msg_block(all_blocks[1])) 174 assert_raises_rpc_error(-5, "Block not found", self.nodes[0].getblock, all_blocks[1].hash) 175 assert_raises_rpc_error(-5, "Block not found", self.nodes[0].getblockheader, all_blocks[1].hash) 176 177 # The block at height 5 should be accepted if we provide the missing header, though 178 headers_message = msg_headers() 179 headers_message.headers.append(CBlockHeader(all_blocks[0])) 180 test_node.send_message(headers_message) 181 test_node.send_and_ping(msg_block(all_blocks[1])) 182 self.nodes[0].getblock(all_blocks[1].hash) 183 184 # Now send the blocks in all_blocks 185 for i in range(288): 186 test_node.send_message(msg_block(all_blocks[i])) 187 test_node.sync_with_ping() 188 189 # Blocks 1-287 should be accepted, block 288 should be ignored because it's too far ahead 190 for x in all_blocks[:-1]: 191 self.nodes[0].getblock(x.hash) 192 assert_raises_rpc_error(-1, "Block not found on disk", self.nodes[0].getblock, all_blocks[-1].hash) 193 194 # 5. Test handling of unrequested block on the node that didn't process 195 # Should still not be processed (even though it has a child that has more 196 # work). 197 198 # The node should have requested the blocks at some point, so 199 # disconnect/reconnect first 200 201 self.nodes[0].disconnect_p2ps() 202 self.nodes[1].disconnect_p2ps() 203 204 test_node = self.nodes[0].add_p2p_connection(P2PInterface()) 205 206 test_node.send_and_ping(msg_block(block_h1f)) 207 assert_equal(self.nodes[0].getblockcount(), 2) 208 self.log.info("Unrequested block that would complete more-work chain was ignored") 209 210 # 6. Try to get node to request the missing block. 211 # Poke the node with an inv for block at height 3 and see if that 212 # triggers a getdata on block 2 (it should if block 2 is missing). 213 with p2p_lock: 214 # Clear state so we can check the getdata request 215 test_node.last_message.pop("getdata", None) 216 test_node.send_message(msg_inv([CInv(MSG_BLOCK, block_h3.sha256)])) 217 218 test_node.sync_with_ping() 219 with p2p_lock: 220 getdata = test_node.last_message["getdata"] 221 222 # Check that the getdata includes the right block 223 assert_equal(getdata.inv[0].hash, block_h1f.sha256) 224 self.log.info("Inv at tip triggered getdata for unprocessed block") 225 226 # 7. Send the missing block for the third time (now it is requested) 227 test_node.send_and_ping(msg_block(block_h1f)) 228 assert_equal(self.nodes[0].getblockcount(), 290) 229 self.nodes[0].getblock(all_blocks[286].hash) 230 assert_equal(self.nodes[0].getbestblockhash(), all_blocks[286].hash) 231 assert_raises_rpc_error(-1, "Block not found on disk", self.nodes[0].getblock, all_blocks[287].hash) 232 self.log.info("Successfully reorged to longer chain") 233 234 # 8. Create a chain which is invalid at a height longer than the 235 # current chain, but which has more blocks on top of that 236 block_289f = create_block(all_blocks[284].sha256, create_coinbase(289), all_blocks[284].nTime+1) 237 block_289f.solve() 238 block_290f = create_block(block_289f.sha256, create_coinbase(290), block_289f.nTime+1) 239 block_290f.solve() 240 # block_291 spends a coinbase below maturity! 241 tx_to_add = create_tx_with_script(block_290f.vtx[0], 0, script_sig=b"42", amount=1) 242 block_291 = create_block(block_290f.sha256, create_coinbase(291), block_290f.nTime+1, txlist=[tx_to_add]) 243 block_291.solve() 244 block_292 = create_block(block_291.sha256, create_coinbase(292), block_291.nTime+1) 245 block_292.solve() 246 247 # Now send all the headers on the chain and enough blocks to trigger reorg 248 headers_message = msg_headers() 249 headers_message.headers.append(CBlockHeader(block_289f)) 250 headers_message.headers.append(CBlockHeader(block_290f)) 251 headers_message.headers.append(CBlockHeader(block_291)) 252 headers_message.headers.append(CBlockHeader(block_292)) 253 test_node.send_and_ping(headers_message) 254 255 tip_entry_found = False 256 for x in self.nodes[0].getchaintips(): 257 if x['hash'] == block_292.hash: 258 assert_equal(x['status'], "headers-only") 259 tip_entry_found = True 260 assert tip_entry_found 261 assert_raises_rpc_error(-1, "Block not found on disk", self.nodes[0].getblock, block_292.hash) 262 263 test_node.send_message(msg_block(block_289f)) 264 test_node.send_and_ping(msg_block(block_290f)) 265 266 self.nodes[0].getblock(block_289f.hash) 267 self.nodes[0].getblock(block_290f.hash) 268 269 test_node.send_message(msg_block(block_291)) 270 271 # At this point we've sent an obviously-bogus block, wait for full processing 272 # and assume disconnection 273 test_node.wait_for_disconnect() 274 275 self.nodes[0].disconnect_p2ps() 276 test_node = self.nodes[0].add_p2p_connection(P2PInterface()) 277 278 # We should have failed reorg and switched back to 290 (but have block 291) 279 assert_equal(self.nodes[0].getblockcount(), 290) 280 assert_equal(self.nodes[0].getbestblockhash(), all_blocks[286].hash) 281 assert_equal(self.nodes[0].getblock(block_291.hash)["confirmations"], -1) 282 283 # Now send a new header on the invalid chain, indicating we're forked off, and expect to get disconnected 284 block_293 = create_block(block_292.sha256, create_coinbase(293), block_292.nTime+1) 285 block_293.solve() 286 headers_message = msg_headers() 287 headers_message.headers.append(CBlockHeader(block_293)) 288 test_node.send_message(headers_message) 289 test_node.wait_for_disconnect() 290 291 # 9. Connect node1 to node0 and ensure it is able to sync 292 self.connect_nodes(0, 1) 293 self.sync_blocks([self.nodes[0], self.nodes[1]]) 294 self.log.info("Successfully synced nodes 1 and 0") 295 296 if __name__ == '__main__': 297 AcceptBlockTest().main()