Inside Bitcoin
Opening the selected file…
Inside Bitcoin
Opening the selected file…
The code behind the network, with room to understand it.
Release published 2026-10-28 · Snapshot fetched 2026-11-01. Latest stable release at fetch time; development branches and Git history are excluded. Only the selected code window and visible folders load here.
1#!/usr/bin/env python32# Copyright (c) 2017-present The Bitcoin Core developers3# Distributed under the MIT software license, see the accompanying4# file COPYING or http://www.opensource.org/licenses/mit-license.php.5"""Test recovery from a crash during chainstate writing.7- 4 nodes8* node0, node1, and node2 will have different dbcrash ratios, and different9dbcache sizes10* node3 will be a regular node, with no crashing.11* The nodes will not connect to each other.13- use default test framework starting chain. initialize starting_tip_height to14tip height.16- Main loop:17* generate lots of transactions on node3, enough to fill up a block.18* uniformly randomly pick a tip height from starting_tip_height to19tip_height; with probability 1/(height_difference+4), invalidate this block.20* mine enough blocks to overtake tip_height at start of loop.21* for each node in [node0,node1,node2]:22- for each mined block:23* submit block to node24* if node crashed on/after submitting:25- restart until recovery succeeds26- check that utxo matches node3 using gettxoutsetinfo"""28import random29import time31from test_framework.blocktools import COINBASE_MATURITY32from test_framework.messages import (33COIN,34)35from test_framework.test_framework import BitcoinTestFramework36from test_framework.util import (37assert_not_equal,38assert_equal,39)40from test_framework.wallet import (41MiniWallet,42getnewdestination,43)46class ChainstateWriteCrashTest(BitcoinTestFramework):47def set_test_params(self):48self.num_nodes = 449self.rpc_timeout = 48051# Set -maxmempool=0 to turn off mempool memory sharing with dbcache52self.base_args = [53"-maxmempool=0",54"-dbbatchsize=200000",55]57# Set different crash ratios and cache sizes. Note that not all of58# -dbcache goes to the in-memory coins cache.59self.node0_args = ["-dbcrashratio=8", "-dbcache=4"] + self.base_args60self.node1_args = ["-dbcrashratio=16", "-dbcache=8"] + self.base_args61self.node2_args = ["-dbcrashratio=24", "-dbcache=16"] + self.base_args63# Node3 is a normal node with default args, except will mine full blocks64# and txs with "dust" outputs65self.node3_args = ["-blockmaxweight=4000000", "-dustrelayfee=0"]66self.extra_args = [self.node0_args, self.node1_args, self.node2_args, self.node3_args]68def setup_network(self):69self.add_nodes(self.num_nodes, extra_args=self.extra_args)70self.start_nodes()71# Leave them unconnected, we'll use submitblock directly in this test73def restart_node(self, node_index, *, expected_tip):74"""Start up a given node id, wait for the tip to reach the given block hash, and calculate the utxo hash.76Exceptions during startup or subsequent RPC calls should indicate a node crash (due to -dbcrashratio), in which case we try again. Give up77after a timeout. Returns the utxo hash of the given node."""79time_start = time.time()80while time.time() - time_start < 120 * self.options.timeout_factor:81try:82# Any of these RPC calls could throw due to node crash83self.start_node(node_index)84self.nodes[node_index].waitforblock(expected_tip)85utxo_hash = self.nodes[node_index].gettxoutsetinfo()['hash_serialized_3']86return utxo_hash87except Exception:88# An exception here should mean the node is about to crash.89# If bitcoind exits, then try again. wait_for_node_exit()90# enforces that bitcoind crashed.91self.wait_for_node_exit(node_index, timeout=10)92self.crashed_on_restart += 194# If we got here, bitcoind isn't coming back up on restart. Could be a95# bug in bitcoind, or we've gotten unlucky with our dbcrash ratio --96# perhaps we generated a test case that blew up our cache?97# If this happens, the test should try to restart without -dbcrashratio98# and make sure that recovery happens.99raise AssertionError(f"Unable to successfully restart node {node_index} in allotted time")101def submit_block_catch_error(self, node_index, block):102"""Try submitting a block to the given node.104Catch any exceptions that indicate the node has crashed.105The caller will check that a crash happened.106Returns true if the block was submitted successfully; false otherwise."""108try:109self.nodes[node_index].submitblock(block)110return True111except Exception as e:112self.log.debug(f"node {node_index} submitblock raised exception: {e}")113return False115def sync_node3blocks(self, block_hashes):116"""Use submitblock to sync node3's chain with the other nodes118If submitblock fails, restart the node and get the new utxo hash.119If any nodes crash while updating, we'll compare utxo hashes to120ensure recovery was successful."""122node3_utxo_hash = self.nodes[3].gettxoutsetinfo()['hash_serialized_3']124# Retrieve all the blocks from node3125blocks = []126for block_hash in block_hashes:127blocks.append([block_hash, self.nodes[3].getblock(block_hash, 0)])129# Deliver each block to each other node130for i in range(3):131nodei_utxo_hash = None132self.log.debug(f"Syncing blocks to node {i}")133for (block_hash, block) in blocks:134# Get the block from node3, and submit to node_i135self.log.debug(f"submitting block {block_hash}")136if not self.submit_block_catch_error(i, block):137# TODO: more carefully check that the crash is due to -dbcrashratio138# (change the exit code perhaps, and check that here?)139# wait_for_node_exit() enforces that bitcoind crashed.140self.wait_for_node_exit(i, timeout=30)141self.log.debug(f"Restarting node {i} after block hash {block_hash}")142nodei_utxo_hash = self.restart_node(i, expected_tip=block_hash)143assert nodei_utxo_hash is not None144self.restart_counts[i] += 1145else:146# Clear it out after successful submitblock calls -- the cached147# utxo hash will no longer be correct148nodei_utxo_hash = None150# Check that the utxo hash matches node3's utxo set151# NOTE: we only check the utxo set if we had to restart the node152# after the last block submitted:153# - checking the utxo hash causes a cache flush, which we don't154# want to do every time; so155# - we only update the utxo cache after a node restart, since flushing156# the cache is a no-op at that point157if nodei_utxo_hash is not None:158self.log.debug(f"Checking txoutsetinfo matches for node {i}")159assert_equal(nodei_utxo_hash, node3_utxo_hash)161def verify_utxo_hash(self):162"""Verify that the utxo hash of each node matches node3.164Restart any nodes that crash while querying."""165node3_utxo_hash = self.nodes[3].gettxoutsetinfo()['hash_serialized_3']166self.log.info("Verifying utxo hash matches for all nodes")168for i in range(3):169try:170nodei_utxo_hash = self.nodes[i].gettxoutsetinfo()['hash_serialized_3']171except Exception:172# probably a crash on db flushing173self.wait_for_node_exit(i, timeout=10)174nodei_utxo_hash = self.restart_node(i, expected_tip=self.nodes[3].getbestblockhash())175assert_equal(nodei_utxo_hash, node3_utxo_hash)177def generate_small_transactions(self, node, count, utxo_list):178FEE = 1000179num_transactions = 0180random.shuffle(utxo_list)181while len(utxo_list) >= 2 and num_transactions < count:182utxos_to_spend = [utxo_list.pop() for _ in range(2)]183input_amount = int(sum([utxo['value'] for utxo in utxos_to_spend]) * COIN)184if input_amount < FEE:185# Sanity check -- if we chose inputs that are too small, skip186continue188tx = self.wallet.create_self_transfer_multi(189utxos_to_spend=utxos_to_spend,190num_outputs=3,191fee_per_output=FEE // 3,192)193node.sendrawtransaction(hexstring=tx["hex"], maxfeerate=0)194num_transactions += 1196def run_test(self):197self.wallet = MiniWallet(self.nodes[3])198initial_height = self.nodes[3].getblockcount()199self.generate(self.nodes[3], COINBASE_MATURITY, sync_fun=self.no_op)201# Track test coverage statistics202self.restart_counts = [0, 0, 0] # Track the restarts for nodes 0-2203self.crashed_on_restart = 0 # Track count of crashes during recovery205# Start by creating a lot of utxos on node3206utxo_list = []207for _ in range(5):208utxo_list.extend(self.wallet.send_self_transfer_multi(from_node=self.nodes[3], num_outputs=1000)['new_utxos'])209self.generate(self.nodes[3], 1, sync_fun=self.no_op)210assert_equal(len(self.nodes[3].getrawmempool()), 0)211self.log.info(f"Prepped {len(utxo_list)} utxo entries")213# Sync these blocks with the other nodes214block_hashes_to_sync = []215for height in range(initial_height + 1, self.nodes[3].getblockcount() + 1):216block_hashes_to_sync.append(self.nodes[3].getblockhash(height))218self.log.debug(f"Syncing {len(block_hashes_to_sync)} blocks with other nodes")219# Syncing the blocks could cause nodes to crash, so the test begins here.220self.sync_node3blocks(block_hashes_to_sync)222starting_tip_height = self.nodes[3].getblockcount()224# Main test loop:225# each time through the loop, generate a bunch of transactions,226# and then either mine a single new block on the tip, or some-sized reorg.227for i in range(40):228self.log.info(f"Iteration {i}, generating 2500 transactions {self.restart_counts}")229# Generate a bunch of small-ish transactions230self.generate_small_transactions(self.nodes[3], 2500, utxo_list)231# Pick a random block between current tip, and starting tip232current_height = self.nodes[3].getblockcount()233random_height = random.randint(starting_tip_height, current_height)234self.log.debug(f"At height {current_height}, considering height {random_height}")235if random_height > starting_tip_height:236# Randomly reorg from this point with some probability (1/4 for237# tip, 1/5 for tip-1, ...)238if random.random() < 1.0 / (current_height + 4 - random_height):239self.log.debug(f"Invalidating block at height {random_height}")240self.nodes[3].invalidateblock(self.nodes[3].getblockhash(random_height))242# Now generate new blocks until we pass the old tip height243self.log.debug("Mining longer tip")244block_hashes = []245while current_height + 1 > self.nodes[3].getblockcount():246block_hashes.extend(self.generatetoaddress(247self.nodes[3],248nblocks=min(10, current_height + 1 - self.nodes[3].getblockcount()),249# new address to avoid mining a block that has just been invalidated250address=getnewdestination()[2],
Independent explanations by MyBitcoinExplorer. Not affiliated with or endorsed by Bitcoin Core. Guides are educational and do not replace the source or its tests.
Bitcoin Core source © its contributors, distributed under the MIT license. Original notices and bundled third-party licenses are retained in the source tree.