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