test: Add bitcoin-chainstate test for assumeutxo functionality
What changed, and why it matters
This commit adds a new automated test for the bitcoin-chainstate tool. It checks that the tool can load a data directory initialized with an assumeutxo snapshot and continue processing new blocks. There is no change to production code that handles user funds, network messages, or consensus rules, and no security fix is present.
No security action required. This is a normal test-only commit and can be reviewed through standard code-review processes.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The commit is purely test infrastructure: it updates a comment in src/kernel/chainparams.cpp to note an additional test consumer, and expands test/functional/tool_bitcoin_chainstate.py to cover the bitcoin-chainstate tool with an assumeutxo-initialized datadir. No C++ runtime logic is modified. The test uses two regtest nodes, generates a deterministic chain, dumps a UTXO snapshot, loads it on the second node, stops the node, and then feeds a new block to the standalone chainstate tool expecting ‘Block tip changed’.
Changed components
test/functional/tool_bitcoin_chainstate.pysrc/kernel/chainparams.cpp (comment only)Inspect captured patch +77 / −20
diff --git a/src/kernel/chainparams.cpp b/src/kernel/chainparams.cpp
index 50891a8d..300df95f 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/test/functional/tool_bitcoin_chainstate.py b/test/functional/tool_bitcoin_chainstate.py
index 7795b20d..8f33da35 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()
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.