diff --git a/test/functional/feature_chain_tiebreaks.py b/test/functional/feature_chain_tiebreaks.py new file mode 100755 index 00000000000..cb1f9ebec9c --- /dev/null +++ b/test/functional/feature_chain_tiebreaks.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +# Copyright (c) The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test that the correct active block is chosen in complex reorgs.""" + +from test_framework.blocktools import create_block +from test_framework.messages import CBlockHeader +from test_framework.p2p import P2PDataStore +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import assert_equal + +class ChainTiebreaksTest(BitcoinTestFramework): + def set_test_params(self): + self.num_nodes = 2 + self.setup_clean_chain = True + + @staticmethod + def send_headers(node, blocks): + """Submit headers for blocks to node.""" + for block in blocks: + # Use RPC rather than P2P, to prevent the message from being interpreted as a block + # announcement. + node.submitheader(hexdata=CBlockHeader(block).serialize().hex()) + + def run_test(self): + node = self.nodes[0] + # Add P2P connection to bitcoind + peer = node.add_p2p_connection(P2PDataStore()) + + self.log.info('Precomputing blocks') + # + # /- B3 -- B7 + # B1 \- B8 + # / \ + # / \ B4 -- B9 + # B0 \- B10 + # \ + # \ /- B5 + # B2 + # \- B6 + # + blocks = [] + + # Construct B0, building off genesis. + start_height = node.getblockcount() + blocks.append(create_block( + hashprev=int(node.getbestblockhash(), 16), + tmpl={"height": start_height + 1} + )) + blocks[-1].solve() + + # Construct B1-B10. + for i in range(1, 11): + blocks.append(create_block( + hashprev=blocks[(i - 1) >> 1].hash_int, + tmpl={ + "height": start_height + (i + 1).bit_length(), + # Make sure each block has a different hash. + "curtime": blocks[-1].nTime + 1, + } + )) + blocks[-1].solve() + + self.log.info('Make sure B0 is accepted normally') + peer.send_blocks_and_test([blocks[0]], node, success=True) + # B0 must be active chain now. + assert_equal(node.getbestblockhash(), blocks[0].hash_hex) + + self.log.info('Send B1 and B2 headers, and then blocks in opposite order') + self.send_headers(node, blocks[1:3]) + peer.send_blocks_and_test([blocks[2]], node, success=True) + peer.send_blocks_and_test([blocks[1]], node, success=False) + # B2 must be active chain now, as full data for B2 was received first. + assert_equal(node.getbestblockhash(), blocks[2].hash_hex) + + self.log.info('Send all further headers in order') + self.send_headers(node, blocks[3:]) + # B2 is still the active chain, headers don't change this. + assert_equal(node.getbestblockhash(), blocks[2].hash_hex) + + self.log.info('Send blocks B7-B10') + peer.send_blocks_and_test([blocks[7]], node, success=False) + peer.send_blocks_and_test([blocks[8]], node, success=False) + peer.send_blocks_and_test([blocks[9]], node, success=False) + peer.send_blocks_and_test([blocks[10]], node, success=False) + # B2 is still the active chain, as B7-B10 have missing parents. + assert_equal(node.getbestblockhash(), blocks[2].hash_hex) + + self.log.info('Send parents B3-B4 of B8-B10 in reverse order') + peer.send_blocks_and_test([blocks[4]], node, success=False, force_send=True) + peer.send_blocks_and_test([blocks[3]], node, success=False, force_send=True) + # B9 is now active. Despite B7 being received earlier, the missing parent. + assert_equal(node.getbestblockhash(), blocks[9].hash_hex) + + self.log.info('Invalidate B9-B10') + node.invalidateblock(blocks[9].hash_hex) + node.invalidateblock(blocks[10].hash_hex) + # B7 is now active. + assert_equal(node.getbestblockhash(), blocks[7].hash_hex) + +if __name__ == '__main__': + ChainTiebreaksTest(__file__).main() diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 44c2885bdf8..3cf3e243600 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -320,6 +320,7 @@ BASE_SCRIPTS = [ 'feature_includeconf.py', 'feature_addrman.py', 'feature_asmap.py', + 'feature_chain_tiebreaks.py', 'feature_fastprune.py', 'feature_framework_miniwallet.py', 'mempool_unbroadcast.py',