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) 2023-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 for assumeutxo wallet related behavior.6See feature_assumeutxo.py for background.7"""8from test_framework.address import address_to_scriptpubkey9from test_framework.descriptors import descsum_create10from test_framework.test_framework import BitcoinTestFramework11from test_framework.messages import COIN12from test_framework.util import (13assert_equal,14assert_greater_than,15assert_raises_rpc_error,16dumb_sync_blocks,17ensure_for,18)19from test_framework.wallet import MiniWallet20from test_framework.wallet_util import get_generate_key22START_HEIGHT = 19923SNAPSHOT_BASE_HEIGHT = 29924FINAL_HEIGHT = 39927class AssumeutxoTest(BitcoinTestFramework):28def skip_test_if_missing_module(self):29self.skip_if_no_wallet()31def set_test_params(self):32"""Use the pregenerated, deterministic chain up to height 199."""33self.num_nodes = 434self.rpc_timeout = 12035self.extra_args = [36[],37[],38[],39["-fastprune", "-prune=1"],40]42def setup_network(self):43"""Start with the nodes disconnected so that one can generate a snapshot44including blocks the other hasn't yet seen."""45self.add_nodes(self.num_nodes, self.extra_args)46self.start_nodes()48def import_descriptor(self, node, wallet_name, key, timestamp):49import_request = [{"desc": descsum_create("pkh(" + key.pubkey + ")"),50"timestamp": timestamp,51"label": "Descriptor import test"}]52wrpc = node.get_wallet_rpc(wallet_name)53return wrpc.importdescriptors(import_request)55def validate_snapshot_import(self, node, loaded, base_hash):56assert_equal(loaded['coins_loaded'], SNAPSHOT_BASE_HEIGHT)57assert_equal(loaded['base_height'], SNAPSHOT_BASE_HEIGHT)59normal, snapshot = node.getchainstates()["chainstates"]60assert_equal(normal['blocks'], START_HEIGHT)61assert 'snapshot_blockhash' not in normal62assert_equal(normal['validated'], True)63assert_equal(snapshot['blocks'], SNAPSHOT_BASE_HEIGHT)64assert_equal(snapshot['snapshot_blockhash'], base_hash)65assert_equal(snapshot['validated'], False)67assert_equal(node.getblockchaininfo()["blocks"], SNAPSHOT_BASE_HEIGHT)69def complete_background_validation(self, node):70self.connect_nodes(0, node.index)72# Ensuring snapshot chain syncs to tip73self.wait_until(lambda: node.getchainstates()['chainstates'][-1]['blocks'] == FINAL_HEIGHT)74self.sync_blocks(nodes=(self.nodes[0], node))76# Ensuring background validation completes77self.wait_until(lambda: len(node.getchainstates()['chainstates']) == 1)79def test_backup_during_background_sync_pruned_node(self, n3, dump_output, expected_error_message):80self.log.info("Backup from the snapshot height can be loaded during background sync (pruned node)")81loaded = n3.loadtxoutset(dump_output['path'])82assert_greater_than(n3.pruneblockchain(START_HEIGHT), 0)83self.validate_snapshot_import(n3, loaded, dump_output['base_hash'])84n3.restorewallet("w", "backup_w.dat")85# Balance of w wallet is still 0 because n3 has not synced yet86assert_equal(n3.getbalance(), 0)88n3.unloadwallet("w")89self.log.info("Backup from before the snapshot height can't be loaded during background sync (pruned node)")90assert_raises_rpc_error(-4, expected_error_message, n3.restorewallet, "w2", "backup_w2.dat")92def test_restore_wallet_pruneheight(self, n3):93self.log.info("Ensuring wallet can't be restored from a backup that was created before the pruneheight (pruned node)")94self.complete_background_validation(n3)95# After background sync, pruneheight is reset to 0, so mine 200 blocks96# and prune the chain again97self.generate(n3, nblocks=200, sync_fun=self.no_op)98assert_equal(n3.pruneblockchain(FINAL_HEIGHT), 298) # 298 is the height of the last block pruned (pruneheight 299)99error_message = "Wallet loading failed. Prune: last wallet synchronisation goes beyond pruned data. You need to -reindex (download the whole blockchain again in case of a pruned node)"100# This backup (backup_w2.dat) was created at height 199, so it can't be restored in a node with a pruneheight of 299101assert_raises_rpc_error(-4, error_message, n3.restorewallet, "w2_pruneheight", "backup_w2.dat")103self.log.info("Ensuring wallet can be restored from a backup that was created at the pruneheight (pruned node)")104# This backup (backup_w.dat) was created at height 299, so it can be restored in a node with a pruneheight of 299105n3.restorewallet("w_alt", "backup_w.dat")106# Check balance of w_alt wallet107w_alt = n3.get_wallet_rpc("w_alt")108assert_equal(w_alt.getbalance(), 34)110def run_test(self):111"""112Bring up four (disconnected) nodes:113- n0: mine some blocks and create a UTXO snapshot114- n1: load the snapshot and test loading a wallet backup and descriptors during and after background sync115- n2: load the snapshot and check the wallet balance during background sync116- n3: load the snapshot, prune the chain, and test loading a wallet backup during and after background sync117"""118n0 = self.nodes[0]119n1 = self.nodes[1]120n2 = self.nodes[2]121n3 = self.nodes[3]123self.mini_wallet = MiniWallet(n0)125# Mock time for a deterministic chain126for n in self.nodes:127n.setmocktime(n.getblockheader(n.getbestblockhash())['time'])129# Create a wallet that we will create a backup for later (at snapshot height)130n0.createwallet('w')131w = n0.get_wallet_rpc("w")132w_address = w.getnewaddress()134# Create another wallet and backup now (before snapshot height)135n0.createwallet('w2')136w2 = n0.get_wallet_rpc("w2")137w2_address = w2.getnewaddress()138w2.backupwallet("backup_w2.dat")140# Generate a series of blocks that `n0` will have in the snapshot,141# but that n1 doesn't yet see. In order for the snapshot to activate,142# though, we have to ferry over the new headers to n1 so that it143# isn't waiting forever to see the header of the snapshot's base block144# while disconnected from n0.145for i in range(100):146if i % 3 == 0:147self.mini_wallet.send_self_transfer(from_node=n0)148self.generate(n0, nblocks=1, sync_fun=self.no_op)149newblock = n0.getblock(n0.getbestblockhash(), 0)151# make n1 aware of the new header, but don't give it the block.152n1.submitheader(newblock)153n2.submitheader(newblock)154n3.submitheader(newblock)155# Ensure everyone is seeing the same headers.156for n in self.nodes:157assert_equal(n.getblockchaininfo()[158"headers"], SNAPSHOT_BASE_HEIGHT)160# This backup is created at the snapshot height, so it's161# not part of the background sync anymore162w.backupwallet("backup_w.dat")164self.log.info("-- Testing assumeutxo")166assert_equal(n0.getblockcount(), SNAPSHOT_BASE_HEIGHT)167assert_equal(n1.getblockcount(), START_HEIGHT)169self.log.info(170f"Creating a UTXO snapshot at height {SNAPSHOT_BASE_HEIGHT}")171dump_output = n0.dumptxoutset('utxos.dat', "latest")173assert_equal(174dump_output['txoutset_hash'],175"106b2c56233e378a824cf0d5ff2be42ed32c72f1605c9be288d00942908a40ac")176assert_equal(dump_output["nchaintx"], 334)177assert_equal(n0.getblockchaininfo()["blocks"], SNAPSHOT_BASE_HEIGHT)179# Mine more blocks on top of the snapshot that n1 hasn't yet seen. This180# will allow us to test n1's sync-to-tip on top of a snapshot.181w_skp = address_to_scriptpubkey(w_address)182w2_skp = address_to_scriptpubkey(w2_address)183for i in range(100):184if i % 3 == 0:185self.mini_wallet.send_to(from_node=n0, scriptPubKey=w_skp, amount=1 * COIN)186self.mini_wallet.send_to(from_node=n0, scriptPubKey=w2_skp, amount=10 * COIN)187self.generate(n0, nblocks=1, sync_fun=self.no_op)189assert_equal(n0.getblockcount(), FINAL_HEIGHT)190assert_equal(n1.getblockcount(), START_HEIGHT)191assert_equal(n2.getblockcount(), START_HEIGHT)193assert_equal(n0.getblockchaininfo()["blocks"], FINAL_HEIGHT)195self.log.info(196f"Loading snapshot into second node from {dump_output['path']}")197loaded = n1.loadtxoutset(dump_output['path'])198self.validate_snapshot_import(n1, loaded, dump_output['base_hash'])200self.log.info("Backup from the snapshot height can be loaded during background sync")201n1.restorewallet("w", "backup_w.dat")202# Balance of w wallet is still 0 because n1 has not synced yet203assert_equal(n1.getbalance(), 0)205self.log.info("Backup from before the snapshot height can't be loaded during background sync")206# Error message for wallets that need blocks before the snapshot height.207def loading_error(height):208return f"Wallet loading failed. Error loading wallet. Wallet requires blocks to be downloaded, and software does not currently support loading wallets while blocks are being downloaded out of order when using assumeutxo snapshots. Wallet should be able to load successfully after node sync reaches height {height}"209# The target height is SNAPSHOT_BASE_HEIGHT because that's when background sync completes.210assert_raises_rpc_error(-4, loading_error(SNAPSHOT_BASE_HEIGHT), n1.restorewallet, "w2", "backup_w2.dat")212self.test_backup_during_background_sync_pruned_node(n3, dump_output, loading_error(SNAPSHOT_BASE_HEIGHT))214self.log.info("Test loading descriptors during background sync")215wallet_name = "w1"216n1.createwallet(wallet_name, disable_private_keys=True)217key = get_generate_key()218block_info = n1.getblockchaininfo()219time = block_info["time"]220def expected_rescan_error(timestamp):221return f"Rescan failed for descriptor with timestamp {timestamp}. There was an error reading a block from time {time}, which is after or within 7200 seconds of key creation, and could contain transactions pertaining to the desc. As a result, transactions and coins using this desc may not appear in the wallet. This error is likely caused by an in-progress assumeutxo background sync. Check logs or getchainstates RPC for assumeutxo background sync progress and try again later."222timestamp = 0223result = self.import_descriptor(n1, wallet_name, key, timestamp)224assert_equal(result[0]['error']['code'], -1)225assert_equal(result[0]['error']['message'], expected_rescan_error(timestamp))226now_timestamp = block_info["mediantime"]227result = self.import_descriptor(n1, wallet_name, get_generate_key(), "now")228assert_equal(result[0]['error']['code'], -1)229assert_equal(result[0]['error']['message'], expected_rescan_error(now_timestamp))231self.log.info("Test that rescanning blocks from before the snapshot fails when blocks are not available from the background sync yet")232w1 = n1.get_wallet_rpc(wallet_name)233assert_raises_rpc_error(-1, "Failed to rescan unavailable blocks likely due to an in-progress assumeutxo background sync. Check logs or getchainstates RPC for assumeutxo background sync progress and try again later.", w1.rescanblockchain, 100)235PAUSE_HEIGHT = FINAL_HEIGHT - 40237self.log.info(f"Unload wallets and sync node up to height {PAUSE_HEIGHT}")238n1.unloadwallet("w")239n1.unloadwallet(wallet_name)240dumb_sync_blocks(src=n0, dst=n1, height=PAUSE_HEIGHT)242self.log.info("Verify node state during background sync")243# Verify there are still two chainstates (background validation not complete)244chainstates = n1.getchainstates()['chainstates']245assert_equal(len(chainstates), 2)246# The background chainstate should still be at START_HEIGHT247assert_equal(chainstates[0]['blocks'], START_HEIGHT)248assert_equal(chainstates[1]["blocks"], PAUSE_HEIGHT)250# After restart, wallets that existed before cannot be loaded because
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.