/ test / functional / rpc_getblockfrompeer.py
rpc_getblockfrompeer.py
  1  #!/usr/bin/env python3
  2  # Copyright (c) 2020-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 the getblockfrompeer RPC."""
  6  
  7  from test_framework.authproxy import JSONRPCException
  8  from test_framework.messages import (
  9      CBlock,
 10      from_hex,
 11      msg_headers,
 12      NODE_WITNESS,
 13  )
 14  from test_framework.p2p import (
 15      P2P_SERVICES,
 16      P2PInterface,
 17  )
 18  from test_framework.test_framework import BitcoinTestFramework
 19  from test_framework.util import (
 20      assert_equal,
 21      assert_raises_rpc_error,
 22  )
 23  
 24  
 25  class GetBlockFromPeerTest(BitcoinTestFramework):
 26      def set_test_params(self):
 27          self.num_nodes = 3
 28          self.extra_args = [
 29              [],
 30              [],
 31              ["-fastprune", "-prune=1"]
 32          ]
 33  
 34      def setup_network(self):
 35          self.setup_nodes()
 36  
 37      def check_for_block(self, node, hash):
 38          try:
 39              self.nodes[node].getblock(hash)
 40              return True
 41          except JSONRPCException:
 42              return False
 43  
 44      def run_test(self):
 45          self.log.info("Mine 4 blocks on Node 0")
 46          self.generate(self.nodes[0], 4, sync_fun=self.no_op)
 47          assert_equal(self.nodes[0].getblockcount(), 204)
 48  
 49          self.log.info("Mine competing 3 blocks on Node 1")
 50          self.generate(self.nodes[1], 3, sync_fun=self.no_op)
 51          assert_equal(self.nodes[1].getblockcount(), 203)
 52          short_tip = self.nodes[1].getbestblockhash()
 53  
 54          self.log.info("Connect nodes to sync headers")
 55          self.connect_nodes(0, 1)
 56          self.sync_blocks(self.nodes[0:2])
 57  
 58          self.log.info("Node 0 should only have the header for node 1's block 3")
 59          x = next(filter(lambda x: x['hash'] == short_tip, self.nodes[0].getchaintips()))
 60          assert_equal(x['status'], "headers-only")
 61          assert_raises_rpc_error(-1, "Block not available (not fully downloaded)", self.nodes[0].getblock, short_tip)
 62  
 63          self.log.info("Fetch block from node 1")
 64          peers = self.nodes[0].getpeerinfo()
 65          assert_equal(len(peers), 1)
 66          peer_0_peer_1_id = peers[0]["id"]
 67  
 68          self.log.info("Arguments must be valid")
 69          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)
 70  
 71          # cli handles wrong types differently
 72          if not self.options.usecli:
 73              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)
 74              assert_raises_rpc_error(-3, "JSON value of type string is not of expected type number", self.nodes[0].getblockfrompeer, short_tip, "0")
 75  
 76          self.log.info("We must already have the header")
 77          assert_raises_rpc_error(-1, "Block header missing", self.nodes[0].getblockfrompeer, "00" * 32, 0)
 78  
 79          self.log.info("Non-existent peer generates error")
 80          for peer_id in [-1, peer_0_peer_1_id + 1]:
 81              assert_raises_rpc_error(-1, "Peer does not exist", self.nodes[0].getblockfrompeer, short_tip, peer_id)
 82  
 83          self.log.info("Fetching from pre-segwit peer generates error")
 84          self.nodes[0].add_p2p_connection(P2PInterface(), services=P2P_SERVICES & ~NODE_WITNESS)
 85          peers = self.nodes[0].getpeerinfo()
 86          assert_equal(len(peers), 2)
 87          presegwit_peer_id = peers[1]["id"]
 88          assert_raises_rpc_error(-1, "Pre-SegWit peer", self.nodes[0].getblockfrompeer, short_tip, presegwit_peer_id)
 89  
 90          self.log.info("Successful fetch")
 91          result = self.nodes[0].getblockfrompeer(short_tip, peer_0_peer_1_id)
 92          self.wait_until(lambda: self.check_for_block(node=0, hash=short_tip), timeout=1)
 93          assert_equal(result, {})
 94  
 95          self.log.info("Don't fetch blocks we already have")
 96          assert_raises_rpc_error(-1, "Block already downloaded", self.nodes[0].getblockfrompeer, short_tip, peer_0_peer_1_id)
 97  
 98          self.log.info("Don't fetch blocks while the node has not synced past it yet")
 99          # For this test we need node 1 in prune mode and as a side effect this also disconnects
100          # the nodes which is also necessary for the rest of the test.
101          self.restart_node(1, ["-prune=550"])
102  
103          # Generate a block on the disconnected node that the pruning node is not connected to
104          blockhash = self.generate(self.nodes[0], 1, sync_fun=self.no_op)[0]
105          block_hex = self.nodes[0].getblock(blockhash=blockhash, verbosity=0)
106          block = from_hex(CBlock(), block_hex)
107  
108          # Connect a P2PInterface to the pruning node and have it submit only the header of the
109          # block that the pruning node has not seen
110          node1_interface = self.nodes[1].add_p2p_connection(P2PInterface())
111          node1_interface.send_and_ping(msg_headers([block]))
112  
113          # Get the peer id of the P2PInterface from the pruning node
114          node1_peers = self.nodes[1].getpeerinfo()
115          assert_equal(len(node1_peers), 1)
116          node1_interface_id = node1_peers[0]["id"]
117  
118          # Trying to fetch this block from the P2PInterface should not be possible
119          error_msg = "In prune mode, only blocks that the node has already synced previously can be fetched from a peer"
120          assert_raises_rpc_error(-1, error_msg, self.nodes[1].getblockfrompeer, blockhash, node1_interface_id)
121  
122          self.log.info("Connect pruned node")
123          self.connect_nodes(0, 2)
124          pruned_node = self.nodes[2]
125          self.sync_blocks([self.nodes[0], pruned_node])
126  
127          # We need to generate more blocks to be able to prune
128          self.generate(self.nodes[0], 400, sync_fun=self.no_op)
129          self.sync_blocks([self.nodes[0], pruned_node])
130          pruneheight = pruned_node.pruneblockchain(300)
131          assert_equal(pruneheight, 248)
132          # Ensure the block is actually pruned
133          pruned_block = self.nodes[0].getblockhash(2)
134          assert_raises_rpc_error(-1, "Block not available (pruned data)", pruned_node.getblock, pruned_block)
135  
136          self.log.info("Fetch pruned block")
137          peers = pruned_node.getpeerinfo()
138          assert_equal(len(peers), 1)
139          pruned_node_peer_0_id = peers[0]["id"]
140          result = pruned_node.getblockfrompeer(pruned_block, pruned_node_peer_0_id)
141          self.wait_until(lambda: self.check_for_block(node=2, hash=pruned_block), timeout=1)
142          assert_equal(result, {})
143  
144          self.log.info("Fetched block persists after next pruning event")
145          self.generate(self.nodes[0], 250, sync_fun=self.no_op)
146          self.sync_blocks([self.nodes[0], pruned_node])
147          pruneheight += 251
148          assert_equal(pruned_node.pruneblockchain(700), pruneheight)
149          assert_equal(pruned_node.getblock(pruned_block)["hash"], "196ee3a1a6db2353965081c48ef8e6b031cb2115d084bec6fec937e91a2c6277")
150  
151          self.log.info("Fetched block can be pruned again when prune height exceeds the height of the tip at the time when the block was fetched")
152          self.generate(self.nodes[0], 250, sync_fun=self.no_op)
153          self.sync_blocks([self.nodes[0], pruned_node])
154          pruneheight += 250
155          assert_equal(pruned_node.pruneblockchain(1000), pruneheight)
156          assert_raises_rpc_error(-1, "Block not available (pruned data)", pruned_node.getblock, pruned_block)
157  
158  
159  if __name__ == '__main__':
160      GetBlockFromPeerTest(__file__).main()