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