rpc_getblockfrompeer.py raw

   1  #!/usr/bin/env python3
   2  # Copyright (c) 2020-2022 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 the getblockfrompeer RPC."""
   6  
   7  from test_framework.authproxy import JSONRPCException
   8  from test_framework.messages import (
   9      CBlock,
  10      from_hex,
  11      msg_block,
  12      msg_headers,
  13      NODE_WITNESS,
  14  )
  15  from test_framework.p2p import (
  16      P2P_SERVICES,
  17      P2PInterface,
  18  )
  19  from test_framework.test_framework import LimenkaTestFramework
  20  from test_framework.util import (
  21      assert_equal,
  22      assert_raises_rpc_error,
  23  )
  24  
  25  
  26  class GetBlockFromPeerTest(LimenkaTestFramework):
  27      def set_test_params(self):
  28          self.num_nodes = 3
  29          self.extra_args = [
  30              [],
  31              [],
  32              ["-fastprune", "-prune=1"]
  33          ]
  34  
  35      def setup_network(self):
  36          self.setup_nodes()
  37  
  38      def check_for_block(self, node, hash):
  39          try:
  40              self.nodes[node].getblock(hash)
  41              return True
  42          except JSONRPCException:
  43              return False
  44  
  45      def run_test(self):
  46          self.log.info("Mine 4 blocks on Node 0")
  47          self.generate(self.nodes[0], 4, sync_fun=self.no_op)
  48          assert_equal(self.nodes[0].getblockcount(), 204)
  49  
  50          self.log.info("Mine competing 3 blocks on Node 1")
  51          self.generate(self.nodes[1], 3, sync_fun=self.no_op)
  52          assert_equal(self.nodes[1].getblockcount(), 203)
  53          short_tip = self.nodes[1].getbestblockhash()
  54  
  55          self.log.info("Connect nodes to sync headers")
  56          self.connect_nodes(0, 1)
  57          self.sync_blocks(self.nodes[0:2])
  58  
  59          self.log.info("Node 0 should only have the header for node 1's block 3")
  60          x = next(filter(lambda x: x['hash'] == short_tip, self.nodes[0].getchaintips()))
  61          assert_equal(x['status'], "headers-only")
  62          assert_raises_rpc_error(-1, "Block not available (not fully downloaded)", self.nodes[0].getblock, short_tip)
  63  
  64          self.log.info("Fetch block from node 1")
  65          peers = self.nodes[0].getpeerinfo()
  66          assert_equal(len(peers), 1)
  67          peer_0_peer_1_id = peers[0]["id"]
  68  
  69          self.log.info("Arguments must be valid")
  70          assert_raises_rpc_error(-8, "hash must be of length 64 (not 4, for '1234')", self.nodes[0].getblockfrompeer, "1234", peer_0_peer_1_id)
  71          assert_raises_rpc_error(-3, "JSON value of type number is not of expected type string", self.nodes[0].getblockfrompeer, 1234, peer_0_peer_1_id)
  72          assert_raises_rpc_error(-3, "JSON value of type string is not of expected type number", self.nodes[0].getblockfrompeer, short_tip, "0")
  73  
  74          self.log.info("We can request blocks for which we do not have the header")
  75          self.nodes[0].getblockfrompeer("11" * 32, 0)
  76  
  77          self.log.info("Non-existent peer generates error")
  78          for peer_id in [-1, peer_0_peer_1_id + 1]:
  79              assert_raises_rpc_error(-1, "Peer does not exist", self.nodes[0].getblockfrompeer, short_tip, peer_id)
  80  
  81          self.log.info("Fetching from pre-segwit peer generates error")
  82          self.nodes[0].add_p2p_connection(P2PInterface(), services=P2P_SERVICES & ~NODE_WITNESS)
  83          peers = self.nodes[0].getpeerinfo()
  84          assert_equal(len(peers), 2)
  85          presegwit_peer_id = peers[1]["id"]
  86          assert_raises_rpc_error(-1, "Pre-SegWit peer", self.nodes[0].getblockfrompeer, short_tip, presegwit_peer_id)
  87  
  88          self.log.info("Fetching from same peer twice generates error")
  89          self.nodes[0].add_p2p_connection(P2PInterface())
  90          peers = self.nodes[0].getpeerinfo()
  91          assert_equal(len(peers), 3)
  92          slow_peer_id = peers[2]["id"]
  93          assert_equal(self.nodes[0].getblockfrompeer(short_tip, slow_peer_id), {})
  94          assert_raises_rpc_error(-1, "Already requested from this peer", self.nodes[0].getblockfrompeer, short_tip, slow_peer_id)
  95  
  96          self.log.info("Successful fetch")
  97          result = self.nodes[0].getblockfrompeer(short_tip, peer_0_peer_1_id)
  98          self.wait_until(lambda: self.check_for_block(node=0, hash=short_tip), timeout=1)
  99          assert_equal(result, {})
 100  
 101          self.log.info("Don't fetch blocks we already have")
 102          assert_raises_rpc_error(-1, "Block already downloaded", self.nodes[0].getblockfrompeer, short_tip, peer_0_peer_1_id)
 103  
 104          self.log.info("Non-existent peer generates error, even if we already have the block")
 105          assert_raises_rpc_error(-1, "Block already downloaded", self.nodes[0].getblockfrompeer, short_tip, peer_0_peer_1_id + 1)
 106  
 107          self.log.info("Do fetch blocks even if the node has not seen the header yet")
 108          # For this test we need node 1 in prune mode and as a side effect this also disconnects
 109          # the nodes which is also necessary for the rest of the test.
 110          self.restart_node(1, ["-prune=550"])
 111  
 112          # Generate a block on the disconnected node that the pruning node is not connected to
 113          blockhash = self.generate(self.nodes[0], 1, sync_fun=self.no_op)[0]
 114          block_hex = self.nodes[0].getblock(blockhash=blockhash, verbosity=0)
 115          block = from_hex(CBlock(), block_hex)
 116  
 117          # Connect a P2PInterface to the pruning node
 118          p2p_i = P2PInterface()
 119          node1_interface = self.nodes[1].add_p2p_connection(p2p_i)
 120  
 121          node1_peers = self.nodes[1].getpeerinfo()
 122          assert_equal(len(node1_peers), 1)
 123          node1_interface_id = node1_peers[0]["id"]
 124          assert_equal(self.nodes[1].getblockfrompeer(blockhash, node1_interface_id), {})
 125          block.calc_sha256()
 126          p2p_i.wait_for_getdata([block.sha256])
 127          p2p_i.send_and_ping(msg_block(block))
 128          assert_equal(block_hex, self.nodes[1].getblock(blockhash, 0))
 129          self.nodes[1].disconnectnode(nodeid=node1_interface_id)
 130  
 131          self.log.info("Do fetch blocks even if the node has not synced past it yet")
 132  
 133          # Generate a block on the disconnected node that the pruning node is not connected to
 134          blockhash = self.generate(self.nodes[0], 1, sync_fun=self.no_op)[0]
 135          block_hex = self.nodes[0].getblock(blockhash=blockhash, verbosity=0)
 136          block = from_hex(CBlock(), block_hex)
 137  
 138          # Connect a P2PInterface to the pruning node and have it submit only the header of the
 139          # block that the pruning node has not seen
 140          node1_interface = self.nodes[1].add_p2p_connection(P2PInterface())
 141          node1_interface.send_and_ping(msg_headers([block]))
 142  
 143          # Get the peer id of the P2PInterface from the pruning node
 144          node1_interface = self.nodes[1].add_p2p_connection(P2PInterface())
 145          node1_peers = self.nodes[1].getpeerinfo()
 146          assert_equal(len(node1_peers), 2)
 147          node1_interface_id = node1_peers[1]["id"]
 148  
 149          # Trying to fetch this block from the P2PInterface should be possible
 150          assert_equal(self.nodes[1].getblockfrompeer(blockhash, node1_interface_id), {})
 151  
 152          self.log.info("Connect pruned node")
 153          self.connect_nodes(0, 2)
 154          pruned_node = self.nodes[2]
 155          self.sync_blocks([self.nodes[0], pruned_node])
 156  
 157          # We need to generate more blocks to be able to prune
 158          self.generate(self.nodes[0], 400, sync_fun=self.no_op)
 159          self.sync_blocks([self.nodes[0], pruned_node])
 160  
 161          # The goal now will be to mimic the automatic pruning process and verify what happens when we fetch an historic
 162          # block at any point of time.
 163          #
 164          # Starting with three blocks files. The pruning process will prune them one by one. And, at the second pruning
 165          # event, the test will fetch the past block. Which will be stored at the latest block file. Which can only be
 166          # pruned when the latest block file is full (in this case, the third one), and a new one is created.
 167  
 168          # First prune event, prune first block file
 169          highest_pruned_block_num = pruned_node.getblockfileinfo(0)["highest_block"]
 170          pruneheight = pruned_node.pruneblockchain(highest_pruned_block_num + 1)
 171          assert_equal(pruneheight, highest_pruned_block_num)
 172          # Ensure the block is actually pruned
 173          fetch_block_num = 2
 174          pruned_block = self.nodes[0].getblockhash(fetch_block_num)
 175          assert_raises_rpc_error(-1, "Block not available (pruned data)", pruned_node.getblock, pruned_block)
 176  
 177          self.log.info("Fetch pruned block")
 178          peers = pruned_node.getpeerinfo()
 179          assert_equal(len(peers), 1)
 180          pruned_node_peer_0_id = peers[0]["id"]
 181          result = pruned_node.getblockfrompeer(pruned_block, pruned_node_peer_0_id)
 182          self.wait_until(lambda: self.check_for_block(node=2, hash=pruned_block), timeout=1)
 183          assert_equal(result, {})
 184  
 185          # Validate that the re-fetched block was stored at the last, current, block file
 186          assert_equal(fetch_block_num, pruned_node.getblockfileinfo(2)["lowest_block"])
 187  
 188          self.log.info("Fetched block persists after next pruning event")
 189          self.generate(self.nodes[0], 250, sync_fun=self.no_op)
 190          self.sync_blocks([self.nodes[0], pruned_node])
 191  
 192          # Second prune event, prune second block file
 193          highest_pruned_block_num = pruned_node.getblockfileinfo(1)["highest_block"]
 194          pruneheight = pruned_node.pruneblockchain(highest_pruned_block_num + 1)
 195          assert_equal(pruneheight, highest_pruned_block_num)
 196          # As the re-fetched block is in the third file, and we just pruned the second one, 'getblock' must work.
 197          assert_equal(pruned_node.getblock(pruned_block)["hash"], "36c56c5b5ebbaf90d76b0d1a074dcb32d42abab75b7ec6fa0ffd9b4fbce8f0f7")
 198  
 199          self.log.info("Re-fetched block can be pruned again when a new block file is created")
 200          self.generate(self.nodes[0], 250, sync_fun=self.no_op)
 201          self.sync_blocks([self.nodes[0], pruned_node])
 202  
 203          # Third prune event, prune third block file
 204          highest_pruned_block_num = pruned_node.getblockfileinfo(2)["highest_block"]
 205          pruneheight = pruned_node.pruneblockchain(highest_pruned_block_num + 1)
 206          assert_equal(pruneheight, highest_pruned_block_num)
 207          # and check that the re-fetched block file is now pruned
 208          assert_raises_rpc_error(-1, "Block not available (pruned data)", pruned_node.getblock, pruned_block)
 209  
 210  
 211  if __name__ == '__main__':
 212      GetBlockFromPeerTest(__file__).main()
 213