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) 2020-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 indices in conjunction with prune."""6import concurrent.futures7import os8from test_framework.test_framework import BitcoinTestFramework9from test_framework.test_node import TestNode10from test_framework.util import (11assert_equal,12assert_greater_than,13assert_raises_rpc_error,14JSONRPCException,15)17from typing import List, Any19def send_batch_request(node: TestNode, method: str, params: List[Any]) -> List[Any]:20"""Send batch request and parse all results"""21data = [getattr(node, method).get_request(*p) for p in params]22response = node.batch(data)23result = []24for item in response:25assert "error" not in item, item["error"]26result.append(item["result"])28return result31class FeatureIndexPruneTest(BitcoinTestFramework):32def set_test_params(self):33self.num_nodes = 434self.extra_args = [35["-fastprune", "-prune=1", "-blockfilterindex=1"],36["-fastprune", "-prune=1", "-coinstatsindex=1"],37["-fastprune", "-prune=1", "-blockfilterindex=1", "-coinstatsindex=1"],38[],39]41def setup_network(self):42self.setup_nodes() # No P2P connection, so that linear_sync works44def linear_sync(self, node_from, *, height_from=None):45# Linear sync over RPC, because P2P sync may not be linear46to_height = node_from.getblockcount()47if height_from is None:48height_from = min([n.getblockcount() for n in self.nodes]) + 149with concurrent.futures.ThreadPoolExecutor(max_workers=self.num_nodes) as rpc_threads:50for i in range(height_from, to_height + 1):51b = node_from.getblock(blockhash=node_from.getblockhash(i), verbosity=0)52list(rpc_threads.map(lambda n: n.submitblock(b), self.nodes))54def generate(self, node, num_blocks, sync_fun=None):55return super().generate(node, num_blocks, sync_fun=sync_fun or (lambda: self.linear_sync(node)))57def sync_index(self, height):58expected_filter = {59'basic block filter index': {'synced': True, 'best_block_height': height},60}61self.wait_until(lambda: self.nodes[0].getindexinfo() == expected_filter)63expected_stats = {64'coinstatsindex': {'synced': True, 'best_block_height': height}65}66self.wait_until(lambda: self.nodes[1].getindexinfo() == expected_stats, timeout=150)68expected = {**expected_filter, **expected_stats}69self.wait_until(lambda: self.nodes[2].getindexinfo() == expected)71def restart_without_indices(self):72for i in range(3):73self.restart_node(i, extra_args=["-fastprune", "-prune=1"])75def check_for_block(self, node, hash):76try:77self.nodes[node].getblock(hash)78return True79except JSONRPCException:80return False82def run_test(self):83filter_nodes = [self.nodes[0], self.nodes[2]]84stats_nodes = [self.nodes[1], self.nodes[2]]86self.log.info("check if we can access blockfilters and coinstats when pruning is enabled but no blocks are actually pruned")87self.sync_index(height=200)88tip = self.nodes[0].getbestblockhash()89for node in filter_nodes:90assert_greater_than(len(node.getblockfilter(tip)['filter']), 0)91for node in stats_nodes:92assert node.gettxoutsetinfo(hash_type="muhash", hash_or_height=tip)['muhash']94self.generate(self.nodes[0], 500)95self.sync_index(height=700)97self.log.info("prune some blocks")98for node in self.nodes[:2]:99with node.assert_debug_log(['limited pruning to height 689']):100pruneheight_new = node.pruneblockchain(400)101# the prune heights used here and below are magic numbers that are determined by the102# thresholds at which block files wrap, so they depend on disk serialization and default block file size.103assert_equal(pruneheight_new, 249)105self.log.info("check if we can access the tips blockfilter and coinstats when we have pruned some blocks")106tip = self.nodes[0].getbestblockhash()107for node in filter_nodes:108assert_greater_than(len(node.getblockfilter(tip)['filter']), 0)109for node in stats_nodes:110assert node.gettxoutsetinfo(hash_type="muhash", hash_or_height=tip)['muhash']112self.log.info("check if we can access the blockfilter and coinstats of a pruned block")113height_hash = self.nodes[0].getblockhash(2)114for node in filter_nodes:115assert_greater_than(len(node.getblockfilter(height_hash)['filter']), 0)116for node in stats_nodes:117assert node.gettxoutsetinfo(hash_type="muhash", hash_or_height=height_hash)['muhash']119# mine and sync index up to a height that will later be the pruneheight120self.generate(self.nodes[0], 54)121self.sync_index(height=754)123self.restart_without_indices()125self.log.info("make sure trying to access the indices throws errors")126for node in filter_nodes:127msg = "Index is not enabled for filtertype basic"128assert_raises_rpc_error(-1, msg, node.getblockfilter, height_hash)129for node in stats_nodes:130msg = "Querying specific block heights requires coinstatsindex"131assert_raises_rpc_error(-8, msg, node.gettxoutsetinfo, "muhash", height_hash)133self.generate(self.nodes[0], 746)135self.log.info("prune exactly up to the indices best blocks while the indices are disabled")136for i in range(3):137pruneheight_2 = self.nodes[i].pruneblockchain(1000)138assert_equal(pruneheight_2, 753)139# Restart the nodes again with the indices activated140self.restart_node(i, extra_args=self.extra_args[i])142self.log.info("make sure that we can continue with the partially synced indices after having pruned up to the index height")143self.sync_index(height=1500)145self.log.info("prune further than the indices best blocks while the indices are disabled")146self.restart_without_indices()147self.generate(self.nodes[0], 1000)149for i in range(3):150pruneheight_3 = self.nodes[i].pruneblockchain(2000)151assert_greater_than(pruneheight_3, pruneheight_2)152self.stop_node(i)154self.log.info("make sure we get an init error when starting the nodes again with the indices")155filter_msg = "Error: basic block filter index best block of the index goes beyond pruned data (including undo data). Please disable the index or reindex (which will download the whole blockchain again)"156stats_msg = "Error: coinstatsindex best block of the index goes beyond pruned data (including undo data). Please disable the index or reindex (which will download the whole blockchain again)"157end_msg = f"{os.linesep}Error: A fatal internal error occurred, see debug.log for details: Failed to start indexes, shutting down…"158for i, msg in enumerate([filter_msg, stats_msg, filter_msg]):159self.nodes[i].assert_start_raises_init_error(extra_args=self.extra_args[i], expected_msg=msg+end_msg)161self.log.info("fetching the missing blocks with getblockfrompeer doesn't work for block filter index and coinstatsindex")162# Only checking the first two nodes since this test takes a long time163# and the third node is kind of redundant in this context164for i, msg in enumerate([filter_msg, stats_msg]):165self.restart_node(i, extra_args=["-prune=1", "-fastprune"])166node = self.nodes[i]167prune_height = node.getblockchaininfo()["pruneheight"]168self.connect_nodes(i, 3)169peers = node.getpeerinfo()170assert_equal(len(peers), 1)171peer_id = peers[0]["id"]173# 1500 is the height to where the indices were able to sync previously174hashes = send_batch_request(node, "getblockhash", [[a] for a in range(1500, prune_height)])175send_batch_request(node, "getblockfrompeer", [[bh, peer_id] for bh in hashes])176# Ensure all necessary blocks have been fetched before proceeding177for bh in hashes:178self.wait_until(lambda: self.check_for_block(i, bh), timeout=10)180# Upon restart we expect the same errors as previously although all181# necessary blocks have been fetched. Both indices need the undo182# data of the blocks to be available as well and getblockfrompeer183# can not provide that.184self.stop_node(i)185node.assert_start_raises_init_error(extra_args=self.extra_args[i], expected_msg=msg+end_msg)187self.log.info("make sure the nodes start again with the indices and an additional -reindex arg")188for i in range(3):189restart_args = self.extra_args[i] + ["-reindex"]190self.restart_node(i, extra_args=restart_args)192self.linear_sync(self.nodes[3])193self.sync_index(height=2500)195for node in self.nodes[:2]:196with node.assert_debug_log(['limited pruning to height 2489']):197pruneheight_new = node.pruneblockchain(2500)198assert_equal(pruneheight_new, 2013)200self.log.info("ensure that prune locks don't prevent indices from failing in a reorg scenario")201with self.nodes[0].assert_debug_log(['basic block filter index prune lock moved back to 2480']):202self.nodes[3].invalidateblock(self.nodes[0].getblockhash(2480))203self.generate(self.nodes[3], 30, sync_fun=lambda: self.linear_sync(self.nodes[3], height_from=2480))206if __name__ == '__main__':207FeatureIndexPruneTest(__file__).main()
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.