p2p_unrequested_blocks.py raw

   1  #!/usr/bin/env python3
   2  # Copyright (c) 2015-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  """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_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], height=2, ntime=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_hex}, 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_hex), 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), height=1, ntime=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_hex:
 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 available (not fully downloaded)", self.nodes[0].getblock, block_h1f.hash_hex)
 123  
 124          # 4. Send another two block that build on the fork.
 125          block_h2f = create_block(block_h1f.hash_int, height=2, ntime=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_hex:
 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_hex)
 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.hash_int, height=3, ntime=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_hex:
 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_hex)
 157  
 158          # But this block should be accepted by node since it has more work.
 159          self.nodes[0].getblock(block_h3.hash_hex)
 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.hash_int, height=i + 4, ntime=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_without_ping(msg_block(all_blocks[1]))
 174          test_node.wait_for_disconnect()
 175          assert_raises_rpc_error(-5, "Block not found", self.nodes[0].getblock, all_blocks[1].hash_hex)
 176          assert_raises_rpc_error(-5, "Block not found", self.nodes[0].getblockheader, all_blocks[1].hash_hex)
 177          test_node = self.nodes[0].add_p2p_connection(P2PInterface())
 178  
 179          # The block at height 5 should be accepted if we provide the missing header, though
 180          headers_message = msg_headers()
 181          headers_message.headers.append(CBlockHeader(all_blocks[0]))
 182          test_node.send_without_ping(headers_message)
 183          test_node.send_and_ping(msg_block(all_blocks[1]))
 184          self.nodes[0].getblock(all_blocks[1].hash_hex)
 185  
 186          # Now send the blocks in all_blocks
 187          for i in range(288):
 188              test_node.send_without_ping(msg_block(all_blocks[i]))
 189          test_node.sync_with_ping()
 190  
 191          # Blocks 1-287 should be accepted, block 288 should be ignored because it's too far ahead
 192          for x in all_blocks[:-1]:
 193              self.nodes[0].getblock(x.hash_hex)
 194          assert_raises_rpc_error(-1, "Block not available (not fully downloaded)", self.nodes[0].getblock, all_blocks[-1].hash_hex)
 195  
 196          # 5. Test handling of unrequested block on the node that didn't process
 197          # Should still not be processed (even though it has a child that has more
 198          # work).
 199  
 200          # The node should have requested the blocks at some point, so
 201          # disconnect/reconnect first
 202  
 203          self.nodes[0].disconnect_p2ps()
 204          self.nodes[1].disconnect_p2ps()
 205  
 206          test_node = self.nodes[0].add_p2p_connection(P2PInterface())
 207  
 208          test_node.send_and_ping(msg_block(block_h1f))
 209          assert_equal(self.nodes[0].getblockcount(), 2)
 210          self.log.info("Unrequested block that would complete more-work chain was ignored")
 211  
 212          # 6. Try to get node to request the missing block.
 213          # Poke the node with an inv for block at height 3 and see if that
 214          # triggers a getdata on block 2 (it should if block 2 is missing).
 215          with p2p_lock:
 216              # Clear state so we can check the getdata request
 217              test_node.last_message.pop("getdata", None)
 218              test_node.send_without_ping(msg_inv([CInv(MSG_BLOCK, block_h3.hash_int)]))
 219  
 220          test_node.sync_with_ping()
 221          with p2p_lock:
 222              getdata = test_node.last_message["getdata"]
 223  
 224          # Check that the getdata includes the right block
 225          assert_equal(getdata.inv[0].hash, block_h1f.hash_int)
 226          self.log.info("Inv at tip triggered getdata for unprocessed block")
 227  
 228          # 7. Send the missing block for the third time (now it is requested)
 229          test_node.send_and_ping(msg_block(block_h1f))
 230          assert_equal(self.nodes[0].getblockcount(), 290)
 231          self.nodes[0].getblock(all_blocks[286].hash_hex)
 232          assert_equal(self.nodes[0].getbestblockhash(), all_blocks[286].hash_hex)
 233          assert_raises_rpc_error(-1, "Block not available (not fully downloaded)", self.nodes[0].getblock, all_blocks[287].hash_hex)
 234          self.log.info("Successfully reorged to longer chain")
 235  
 236          # 8. Create a chain which is invalid at a height longer than the
 237          # current chain, but which has more blocks on top of that
 238          block_289f = create_block(all_blocks[284].hash_int, height=289, ntime=all_blocks[284].nTime+1)
 239          block_289f.solve()
 240          block_290f = create_block(block_289f.hash_int, height=290, ntime=block_289f.nTime+1)
 241          block_290f.solve()
 242          # block_291 spends a coinbase below maturity!
 243          tx_to_add = create_tx_with_script(block_290f.vtx[0], 0, script_sig=b"42", amount=1)
 244          block_291 = create_block(block_290f.hash_int, height=291, ntime=block_290f.nTime+1, txlist=[tx_to_add])
 245          block_291.solve()
 246          block_292 = create_block(block_291.hash_int, height=292, ntime=block_291.nTime+1)
 247          block_292.solve()
 248  
 249          # Now send all the headers on the chain and enough blocks to trigger reorg
 250          headers_message = msg_headers()
 251          headers_message.headers.append(CBlockHeader(block_289f))
 252          headers_message.headers.append(CBlockHeader(block_290f))
 253          headers_message.headers.append(CBlockHeader(block_291))
 254          headers_message.headers.append(CBlockHeader(block_292))
 255          test_node.send_and_ping(headers_message)
 256  
 257          tip_entry_found = False
 258          for x in self.nodes[0].getchaintips():
 259              if x['hash'] == block_292.hash_hex:
 260                  assert_equal(x['status'], "headers-only")
 261                  tip_entry_found = True
 262          assert tip_entry_found
 263          assert_raises_rpc_error(-1, "Block not available (not fully downloaded)", self.nodes[0].getblock, block_292.hash_hex)
 264  
 265          test_node.send_without_ping(msg_block(block_289f))
 266          test_node.send_and_ping(msg_block(block_290f))
 267  
 268          self.nodes[0].getblock(block_289f.hash_hex)
 269          self.nodes[0].getblock(block_290f.hash_hex)
 270  
 271          test_node.send_without_ping(msg_block(block_291))
 272  
 273          # At this point we've sent an obviously-bogus block, wait for full processing
 274          # and assume disconnection
 275          test_node.wait_for_disconnect()
 276  
 277          self.nodes[0].disconnect_p2ps()
 278          test_node = self.nodes[0].add_p2p_connection(P2PInterface())
 279  
 280          # We should have failed reorg and switched back to 290 (but have block 291)
 281          assert_equal(self.nodes[0].getblockcount(), 290)
 282          assert_equal(self.nodes[0].getbestblockhash(), all_blocks[286].hash_hex)
 283          assert_equal(self.nodes[0].getblock(block_291.hash_hex)["confirmations"], -1)
 284  
 285          # Now send a new header on the invalid chain, indicating we're forked off, and expect to get disconnected
 286          block_293 = create_block(block_292.hash_int, height=293, ntime=block_292.nTime+1)
 287          block_293.solve()
 288          headers_message = msg_headers()
 289          headers_message.headers.append(CBlockHeader(block_293))
 290          test_node.send_without_ping(headers_message)
 291          test_node.wait_for_disconnect()
 292  
 293          # 9. Connect node1 to node0 and ensure it is able to sync
 294          self.connect_nodes(0, 1)
 295          self.sync_blocks([self.nodes[0], self.nodes[1]])
 296          self.log.info("Successfully synced nodes 1 and 0")
 297  
 298  if __name__ == '__main__':
 299      AcceptBlockTest(__file__).main()
 300