diff --git a/test/functional/abc-invalid-chains.py b/test/functional/abc-invalid-chains.py new file mode 100755 --- /dev/null +++ b/test/functional/abc-invalid-chains.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 +# Copyright (c) 2019 The Bitcoin developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-ilncense.php. + +import time + +from test_framework.test_framework import BitcoinTestFramework +from test_framework.comptool import RejectResult, TestInstance, TestManager +from test_framework.mininode import network_thread_start +from test_framework.util import assert_equal +from test_framework.blocktools import ( + create_block, + create_coinbase, +) + + +class InvalidChainsTest(BitcoinTestFramework): + def set_test_params(self): + self.num_nodes = 1 + self.setup_clean_chain = True + self.tip = None + self.blocks = {} + self.block_heights = {} + self.extra_args = [["-whitelist=127.0.0.1"]] + + def next_block(self, number): + if self.tip == None: + base_block_hash = self.genesis_hash + block_time = int(time.time()) + 1 + else: + base_block_hash = self.tip.sha256 + block_time = self.tip.nTime + 1 + + height = self.block_heights[base_block_hash] + 1 + coinbase = create_coinbase(height) + coinbase.rehash() + block = create_block(base_block_hash, coinbase, block_time) + + block.solve() + self.tip = block + self.block_heights[block.sha256] = height + assert number not in self.blocks + self.blocks[number] = block + return block + + def run_test(self): + self.test = TestManager(self, self.options.tmpdir) + self.test.add_all_connections(self.nodes) + network_thread_start() + self.test.run() + + def get_tests(self): + node = self.nodes[0] + self.genesis_hash = int(node.getbestblockhash(), 16) + self.block_heights[self.genesis_hash] = 0 + + # returns a test case that asserts that the current tip was accepted + def accepted(expectedTipHash=None): + if expectedTipHash is None: + return TestInstance([[self.tip, True]]) + else: + return TestInstance([[self.tip, True, expectedTipHash]]) + + # returns a test case that asserts that the current tip was rejected + def rejected(reject=None): + if reject is None: + return TestInstance([[self.tip, False]]) + else: + return TestInstance([[self.tip, reject]]) + + # move the tip back to a previous block + def tip(number): + self.tip = self.blocks[number] + + # shorthand for functions + block = self.next_block + + # Reference for blocks mined in this test: + # + # 11 21 -- 221 - 222 + # / / / + # 0 - 1 - 2 - 22 - 23 - 24 - 25 + # \ + # -- 12 - 13 - 14 + + # Generate some valid blocks + block(0) + yield accepted() + block(1) + yield accepted() + block(2) + yield accepted() + + # Explicitly invalidate blocks 1 and 2 + # See below for why we do this + node.invalidateblock(self.blocks[1].hash) + assert_equal(self.blocks[0].hash, node.getbestblockhash()) + node.invalidateblock(self.blocks[2].hash) + assert_equal(self.blocks[0].hash, node.getbestblockhash()) + + # Mining on top of blocks 1 or 2 is rejected + tip(1) + block(11) + yield rejected(RejectResult(16, b'bad-prevblk')) + + tip(2) + block(21) + yield rejected(RejectResult(16, b'bad-prevblk')) + + # Reconsider block 2 to remove invalid status from *both* 1 and 2 + # The goal is to test that block 1 is not retaining any internal state + # that prevents us from accepting blocks building on top of block 1 + node.reconsiderblock(self.blocks[2].hash) + assert_equal(self.blocks[2].hash, node.getbestblockhash()) + + # Mining on the block 1 chain should be accepted + # (needs to mine two blocks because less-work chains are not processed) + test = TestInstance(sync_every_block=False) + tip(1) + block(12) + test.blocks_and_transactions.append([self.tip, None]) + block(13) + test.blocks_and_transactions.append([self.tip, None]) + yield test + + # Mining on the block 2 chain should still be accepted + # (needs to mine two blocks because less-work chains are not processed) + test = TestInstance(sync_every_block=False) + tip(2) + block(22) + test.blocks_and_transactions.append([self.tip, None]) + # Mine block 221 for later + block(221) + test.blocks_and_transactions.append([self.tip, None]) + yield test + + # Mine more blocks from block 22 to be longest chain + test = TestInstance(sync_every_block=False) + tip(22) + block(23) + test.blocks_and_transactions.append([self.tip, None]) + block(24) + test.blocks_and_transactions.append([self.tip, None]) + yield test + + # Sanity checks + assert_equal(self.blocks[24].hash, node.getbestblockhash()) + assert any(self.blocks[221].hash == chaintip["hash"] + for chaintip in node.getchaintips()) + + # Invalidating the block 2 chain should reject new blocks on that chain + node.invalidateblock(self.blocks[2].hash) + assert_equal(self.blocks[13].hash, node.getbestblockhash()) + + # Mining on the block 2 chain should be rejected + tip(24) + block(25) + yield rejected(RejectResult(16, b'bad-prevblk')) + + # Continued mining on the block 1 chain is still ok + tip(13) + block(14) + yield accepted() + + # Mining on a once-valid chain forking from block 2's longest chain, + # which is now invalid, should also be rejected. + tip(221) + block(222) + yield rejected(RejectResult(16, b'bad-prevblk')) + + +if __name__ == '__main__': + InvalidChainsTest().main()