feature_chain_tiebreaks.py raw

   1  #!/usr/bin/env python3
   2  # Copyright (c) The Limenka 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 that the correct active block is chosen in complex reorgs."""
   6  
   7  from test_framework.blocktools import create_block
   8  from test_framework.messages import CBlockHeader
   9  from test_framework.p2p import P2PDataStore
  10  from test_framework.test_framework import LimenkaTestFramework
  11  from test_framework.util import assert_equal
  12  
  13  class ChainTiebreaksTest(LimenkaTestFramework):
  14      def set_test_params(self):
  15          self.num_nodes = 2
  16          self.setup_clean_chain = True
  17  
  18      @staticmethod
  19      def send_headers(node, blocks):
  20          """Submit headers for blocks to node."""
  21          for block in blocks:
  22              # Use RPC rather than P2P, to prevent the message from being interpreted as a block
  23              # announcement.
  24              node.submitheader(hexdata=CBlockHeader(block).serialize().hex())
  25  
  26      def test_chain_split_in_memory(self):
  27          node = self.nodes[0]
  28          # Add P2P connection to limenkad
  29          peer = node.add_p2p_connection(P2PDataStore())
  30  
  31          self.log.info('Precomputing blocks')
  32          #
  33          #          /- B3 -- B7
  34          #        B1      \- B8
  35          #       /  \
  36          #      /    \ B4 -- B9
  37          #   B0           \- B10
  38          #      \
  39          #       \  /- B5
  40          #        B2
  41          #          \- B6
  42          #
  43          blocks = []
  44  
  45          # Construct B0, building off genesis.
  46          start_height = node.getblockcount()
  47          blocks.append(create_block(
  48              hashprev=int(node.getbestblockhash(), 16),
  49              tmpl={"height": start_height + 1}
  50          ))
  51          blocks[-1].solve()
  52  
  53          # Construct B1-B10.
  54          for i in range(1, 11):
  55              blocks.append(create_block(
  56                  hashprev=int(blocks[(i - 1) >> 1].hash, 16),
  57                  tmpl={
  58                      "height": start_height + (i + 1).bit_length(),
  59                      # Make sure each block has a different hash.
  60                      "curtime": blocks[-1].nTime + 1,
  61                  }
  62              ))
  63              blocks[-1].solve()
  64  
  65          self.log.info('Make sure B0 is accepted normally')
  66          peer.send_blocks_and_test([blocks[0]], node, success=True)
  67          # B0 must be active chain now.
  68          assert_equal(node.getbestblockhash(), blocks[0].hash)
  69  
  70          self.log.info('Send B1 and B2 headers, and then blocks in opposite order')
  71          self.send_headers(node, blocks[1:3])
  72          peer.send_blocks_and_test([blocks[2]], node, success=True)
  73          peer.send_blocks_and_test([blocks[1]], node, success=False)
  74          # B2 must be active chain now, as full data for B2 was received first.
  75          assert_equal(node.getbestblockhash(), blocks[2].hash)
  76  
  77          self.log.info('Send all further headers in order')
  78          self.send_headers(node, blocks[3:])
  79          # B2 is still the active chain, headers don't change this.
  80          assert_equal(node.getbestblockhash(), blocks[2].hash)
  81  
  82          self.log.info('Send blocks B7-B10')
  83          peer.send_blocks_and_test([blocks[7]], node, success=False)
  84          peer.send_blocks_and_test([blocks[8]], node, success=False)
  85          peer.send_blocks_and_test([blocks[9]], node, success=False)
  86          peer.send_blocks_and_test([blocks[10]], node, success=False)
  87          # B2 is still the active chain, as B7-B10 have missing parents.
  88          assert_equal(node.getbestblockhash(), blocks[2].hash)
  89  
  90          self.log.info('Send parents B3-B4 of B8-B10 in reverse order')
  91          peer.send_blocks_and_test([blocks[4]], node, success=False, force_send=True)
  92          peer.send_blocks_and_test([blocks[3]], node, success=False, force_send=True)
  93          # B9 is now active. Despite B7 being received earlier, the missing parent.
  94          assert_equal(node.getbestblockhash(), blocks[9].hash)
  95  
  96          self.log.info('Invalidate B9-B10')
  97          node.invalidateblock(blocks[9].hash)
  98          node.invalidateblock(blocks[10].hash)
  99          # B7 is now active.
 100          assert_equal(node.getbestblockhash(), blocks[7].hash)
 101  
 102          # Invalidate blocks to start fresh on the next test
 103          node.invalidateblock(blocks[0].hash)
 104  
 105      def test_chain_split_from_disk(self):
 106          node = self.nodes[0]
 107          peer = node.add_p2p_connection(P2PDataStore())
 108  
 109          self.log.info('Precomputing blocks')
 110          #
 111          #      A1
 112          #     /
 113          #   G
 114          #     \
 115          #      A2
 116          #
 117          blocks = []
 118  
 119          # Construct two blocks building from genesis.
 120          start_height = node.getblockcount()
 121          genesis_block = node.getblock(node.getblockhash(start_height))
 122          prev_time = genesis_block["time"]
 123  
 124          for i in range(0, 2):
 125              blocks.append(create_block(
 126                  hashprev=int(genesis_block["hash"], 16),
 127                  tmpl={"height": start_height + 1,
 128                  # Make sure each block has a different hash.
 129                  "curtime": prev_time + i + 1,
 130                  }
 131              ))
 132              blocks[-1].solve()
 133  
 134          # Send blocks and test the last one is not connected
 135          self.log.info('Send A1 and A2. Make sure that only the former connects')
 136          peer.send_blocks_and_test([blocks[0]], node, success=True)
 137          peer.send_blocks_and_test([blocks[1]], node, success=False)
 138  
 139          self.log.info('Restart the node and check that the best tip before restarting matched the ones afterwards')
 140          # Restart and check enough times for this to eventually fail if the logic is broken
 141          for _ in range(10):
 142              self.restart_node(0)
 143              assert_equal(blocks[0].hash, node.getbestblockhash())
 144  
 145      def run_test(self):
 146          self.test_chain_split_in_memory()
 147          self.test_chain_split_from_disk()
 148  
 149  
 150  if __name__ == '__main__':
 151      ChainTiebreaksTest(__file__).main()
 152