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