diff --git a/src/bitcoin-chainstate.cpp b/src/bitcoin-chainstate.cpp index 91c5639e508..71d0f3e95cc 100644 --- a/src/bitcoin-chainstate.cpp +++ b/src/bitcoin-chainstate.cpp @@ -144,16 +144,18 @@ public: int main(int argc, char* argv[]) { // SETUP: Argument parsing and handling - if (argc != 2) { + const bool has_regtest_flag{argc == 3 && std::string(argv[1]) == "-regtest"}; + if (argc < 2 || argc > 3 || (argc == 3 && !has_regtest_flag)) { std::cerr - << "Usage: " << argv[0] << " DATADIR" << std::endl + << "Usage: " << argv[0] << " [-regtest] DATADIR" << std::endl << "Display DATADIR information, and process hex-encoded blocks on standard input." << std::endl + << "Uses mainnet parameters by default, regtest with -regtest flag" << std::endl << std::endl << "IMPORTANT: THIS EXECUTABLE IS EXPERIMENTAL, FOR TESTING ONLY, AND EXPECTED TO" << std::endl << " BREAK IN FUTURE VERSIONS. DO NOT USE ON YOUR ACTUAL DATADIR." << std::endl; return 1; } - std::filesystem::path abs_datadir{std::filesystem::absolute(argv[1])}; + std::filesystem::path abs_datadir{std::filesystem::absolute(argv[argc-1])}; std::filesystem::create_directories(abs_datadir); btck_LoggingOptions logging_options = { @@ -169,7 +171,7 @@ int main(int argc, char* argv[]) Logger logger{std::make_unique()}; ContextOptions options{}; - ChainParams params{ChainType::MAINNET}; + ChainParams params{has_regtest_flag ? ChainType::REGTEST : ChainType::MAINNET}; options.SetChainParams(params); options.SetNotifications(std::make_shared()); diff --git a/src/kernel/chainparams.cpp b/src/kernel/chainparams.cpp index 50891a8d045..300df95fb8e 100644 --- a/src/kernel/chainparams.cpp +++ b/src/kernel/chainparams.cpp @@ -638,7 +638,7 @@ public: .blockhash = consteval_ctor(uint256{"385901ccbd69dff6bbd00065d01fb8a9e464dede7cfe0372443884f9b1dcf6b9"}), }, { - // For use by test/functional/feature_assumeutxo.py + // For use by test/functional/feature_assumeutxo.py and test/functional/tool_bitcoin_chainstate.py .height = 299, .hash_serialized = AssumeutxoHash{uint256{"d2b051ff5e8eef46520350776f4100dd710a63447a8e01d917e92e79751a63e2"}}, .m_chain_tx_count = 334, diff --git a/src/validation.cpp b/src/validation.cpp index b12703e4641..e919552ad23 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -6226,7 +6226,7 @@ Chainstate& ChainstateManager::AddChainstate(std::unique_ptr chainst // Transfer possession of the mempool to the chainstate. // Mempool is empty at this point because we're still in IBD. - assert(prev_chainstate.m_mempool->size() == 0); + assert(!prev_chainstate.m_mempool || prev_chainstate.m_mempool->size() == 0); assert(!curr_chainstate.m_mempool); std::swap(curr_chainstate.m_mempool, prev_chainstate.m_mempool); return curr_chainstate; diff --git a/test/functional/tool_bitcoin_chainstate.py b/test/functional/tool_bitcoin_chainstate.py index 7795b20df70..8f33da357be 100755 --- a/test/functional/tool_bitcoin_chainstate.py +++ b/test/functional/tool_bitcoin_chainstate.py @@ -2,25 +2,58 @@ # Copyright (c) 2022-present 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 bitcoin-chainstate tool functionality + +Test basic block processing via bitcoin-chainstate tool, including detecting +duplicates and malformed input. + +Test that bitcoin-chainstate can load a datadir initialized with an assumeutxo +snapshot and extend the snapshot chain with new blocks. +""" import subprocess from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import assert_equal +from test_framework.wallet import MiniWallet + +START_HEIGHT = 199 +# Hardcoded in regtest chainparams +SNAPSHOT_BASE_BLOCK_HEIGHT = 299 +SNAPSHOT_BASE_BLOCK_HASH = "7cc695046fec709f8c9394b6f928f81e81fd3ac20977bb68760fa1faa7916ea2" + class BitcoinChainstateTest(BitcoinTestFramework): def skip_test_if_missing_module(self): self.skip_if_no_bitcoin_chainstate() def set_test_params(self): - self.setup_clean_chain = True - self.chain = "" - self.num_nodes = 1 - # Set prune to avoid disk space warning. - self.extra_args = [["-prune=550"]] + """Use the pregenerated, deterministic chain up to height 199.""" + self.num_nodes = 2 - def add_block(self, datadir, input, expected_stderr): + def setup_network(self): + """Start with the nodes disconnected so that one can generate a snapshot + including blocks the other hasn't yet seen.""" + self.add_nodes(2) + self.start_nodes() + + def generate_snapshot_chain(self): + self.log.info(f"Generate deterministic chain up to block {SNAPSHOT_BASE_BLOCK_HEIGHT} for node0 while node1 disconnected") + n0 = self.nodes[0] + assert_equal(n0.getblockcount(), START_HEIGHT) + n0.setmocktime(n0.getblockheader(n0.getbestblockhash())['time']) + mini_wallet = MiniWallet(n0) + for i in range(SNAPSHOT_BASE_BLOCK_HEIGHT - n0.getblockchaininfo()["blocks"]): + if i % 3 == 0: + mini_wallet.send_self_transfer(from_node=n0) + self.generate(n0, nblocks=1, sync_fun=self.no_op) + assert_equal(n0.getblockcount(), SNAPSHOT_BASE_BLOCK_HEIGHT) + assert_equal(n0.getbestblockhash(), SNAPSHOT_BASE_BLOCK_HASH) + return n0.dumptxoutset('utxos.dat', "latest") + + def add_block(self, datadir, input, expected_stderr=None, expected_stdout=None): proc = subprocess.Popen( - self.get_binaries().chainstate_argv() + [datadir], + self.get_binaries().chainstate_argv() + ["-regtest", datadir], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, @@ -30,20 +63,44 @@ class BitcoinChainstateTest(BitcoinTestFramework): self.log.debug("STDOUT: {0}".format(stdout.strip("\n"))) self.log.info("STDERR: {0}".format(stderr.strip("\n"))) - if expected_stderr not in stderr: - raise AssertionError(f"Expected stderr output {expected_stderr} does not partially match stderr:\n{stderr}") + if expected_stderr is not None and expected_stderr not in stderr: + raise AssertionError(f"Expected stderr output '{expected_stderr}' does not partially match stderr:\n{stderr}") + if expected_stdout is not None and expected_stdout not in stdout: + raise AssertionError(f"Expected stdout output '{expected_stdout}' does not partially match stdout:\n{stdout}") + + def basic_test(self): + n0 = self.nodes[0] + n1 = self.nodes[1] + datadir = n1.chain_path + n1.stop_node() + block = n0.getblock(n0.getblockhash(START_HEIGHT+1), 0) + self.log.info(f"Test bitcoin-chainstate {self.get_binaries().chainstate_argv()} with datadir: {datadir}") + self.add_block(datadir, block, expected_stderr="Block has not yet been rejected") + self.add_block(datadir, block, expected_stderr="duplicate") + self.add_block(datadir, "00", expected_stderr="Block decode failed") + self.add_block(datadir, "", expected_stderr="Empty line found") + + def assumeutxo_test(self, dump_output_path): + n0 = self.nodes[0] + n1 = self.nodes[1] + self.start_node(1) + self.log.info("Submit headers for new blocks to node1, then load the snapshot so it activates") + for height in range(START_HEIGHT+2, SNAPSHOT_BASE_BLOCK_HEIGHT+1): + block = n0.getblock(n0.getblockhash(height), 0) + n1.submitheader(block) + assert_equal(n1.getblockcount(), START_HEIGHT+1) + loaded = n1.loadtxoutset(dump_output_path) + assert_equal(loaded['base_height'], SNAPSHOT_BASE_BLOCK_HEIGHT) + datadir = n1.chain_path + n1.stop_node() + self.log.info(f"Test bitcoin-chainstate {self.get_binaries().chainstate_argv()} with an assumeutxo datadir: {datadir}") + new_tip_hash = self.generate(n0, nblocks=1, sync_fun=self.no_op)[0] + self.add_block(datadir, n0.getblock(new_tip_hash, 0), expected_stdout="Block tip changed") def run_test(self): - node = self.nodes[0] - datadir = node.cli.datadir - node.stop_node() - - self.log.info(f"Testing bitcoin-chainstate {self.get_binaries().chainstate_argv()} with datadir: {datadir}") - block_one = "010000006fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000982051fd1e4ba744bbbe680e1fee14677ba1a3c3540bf7b1cdb606e857233e0e61bc6649ffff001d01e362990101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d0104ffffffff0100f2052a0100000043410496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52da7589379515d4e0a604f8141781e62294721166bf621e73a82cbf2342c858eeac00000000" - self.add_block(datadir, block_one, "Block has not yet been rejected") - self.add_block(datadir, block_one, "duplicate") - self.add_block(datadir, "00", "Block decode failed") - self.add_block(datadir, "", "Empty line found") + dump_output = self.generate_snapshot_chain() + self.basic_test() + self.assumeutxo_test(dump_output['path']) if __name__ == "__main__": BitcoinChainstateTest(__file__).main()