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) 2019-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.6"""Test tx status in case of reorgs while wallet being shutdown.8Wallet txn status rely on block connection/disconnection for its9accuracy. In case of reorgs happening while wallet being shutdown10block updates are not going to be received. At wallet loading, we11check against chain if confirmed txn are still in chain and change12their status if block in which they have been included has been13disconnected.14"""16from decimal import Decimal17import shutil19from test_framework.test_framework import BitcoinTestFramework20from test_framework.util import (21assert_equal,22assert_greater_than,23assert_not_equal,24assert_raises_rpc_error25)27class ReorgsRestoreTest(BitcoinTestFramework):28def set_test_params(self):29self.num_nodes = 331def skip_test_if_missing_module(self):32self.skip_if_no_wallet()34def test_coinbase_automatic_abandon_during_startup(self):35##########################################################################################################36# Verify the wallet marks coinbase transactions, and their descendants, as abandoned during startup when #37# the block is no longer part of the best chain. #38##########################################################################################################39self.log.info("Test automatic coinbase abandonment during startup")40# Test setup: Sync nodes for the coming test, ensuring both are at the same block, then disconnect them to41# generate two competing chains. After disconnection, verify no other peer connection exists.42self.connect_nodes(1, 0)43self.sync_blocks(self.nodes[:2])44self.disconnect_nodes(1, 0)45for node in self.nodes[:2]:46assert_equal(len(node.getpeerinfo()), 0)48# Create a new block in node0, coinbase going to wallet049self.nodes[0].createwallet(wallet_name="w0", load_on_startup=True)50wallet0 = self.nodes[0].get_wallet_rpc("w0")51self.generatetoaddress(self.nodes[0], 1, wallet0.getnewaddress(), sync_fun=self.no_op)52node0_coinbase_tx_hash = wallet0.getblock(wallet0.getbestblockhash(), verbose=1)['tx'][0]54# Mine 100 blocks on top to mature the coinbase and create a descendant55self.generate(self.nodes[0], 101, sync_fun=self.no_op)56# Make descendant, send-to-self57descendant_tx_id = wallet0.sendtoaddress(wallet0.getnewaddress(), 1)59# Verify balance60wallet0.syncwithvalidationinterfacequeue()61assert(wallet0.getbalances()['mine']['trusted'] > 0)63# Now create a fork in node1. This will be used to replace node0's chain later.64self.nodes[1].createwallet(wallet_name="w1", load_on_startup=True)65wallet1 = self.nodes[1].get_wallet_rpc("w1")66self.generatetoaddress(self.nodes[1], 1, wallet1.getnewaddress(), sync_fun=self.no_op)67wallet1.syncwithvalidationinterfacequeue()69# Verify both nodes are on a different chain70block0_best_hash, block1_best_hash = wallet0.getbestblockhash(), wallet1.getbestblockhash()71assert(block0_best_hash != block1_best_hash)73# Stop both nodes and replace node0 chain entirely for the node1 chain74self.stop_nodes()75for path in ["chainstate", "blocks"]:76self.cleanup_folder(self.nodes[0].chain_path / path)77shutil.copytree(self.nodes[1].chain_path / path, self.nodes[0].chain_path / path)79# Start node0 and verify that now it has node1 chain and no info about its previous best block80self.start_node(0)81wallet0 = self.nodes[0].get_wallet_rpc("w0")82assert_equal(wallet0.getbestblockhash(), block1_best_hash)83assert_raises_rpc_error(-5, "Block not found", wallet0.getblock, block0_best_hash)85# Verify the coinbase tx was marked as abandoned and balance correctly computed86tx_info = wallet0.gettransaction(node0_coinbase_tx_hash)['details'][0]87assert_equal(tx_info['abandoned'], True)88assert_equal(tx_info['category'], 'orphan')89assert(wallet0.getbalances()['mine']['trusted'] == 0)90# Verify the coinbase descendant was also marked as abandoned91assert_equal(wallet0.gettransaction(descendant_tx_id)['details'][0]['abandoned'], True)93def test_reorg_handling_during_unclean_shutdown(self):94self.log.info("Test that wallet transactions are un-abandoned in case of temporarily invalidated blocks and wallet doesn't crash due to a duplicate block disconnection event after an unclean shutdown")95node = self.nodes[0]96# Receive coinbase reward on a new wallet97node.createwallet(wallet_name="reorg_crash", load_on_startup=True)98wallet = node.get_wallet_rpc("reorg_crash")99self.generatetoaddress(node, 1, wallet.getnewaddress(), sync_fun=self.no_op)101# Restart to ensure node and wallet are flushed102self.restart_node(0)103wallet = node.get_wallet_rpc("reorg_crash")104assert_greater_than(wallet.getbalances()["mine"]["immature"], 0)106# Disconnect tip and sync wallet state107tip = wallet.getbestblockhash()108tip_height = wallet.getblockstats(hash_or_height=tip)["height"]109wallet.invalidateblock(tip)110wallet.syncwithvalidationinterfacequeue()112# Tip was disconnected, ensure coinbase has been abandoned113assert_equal(wallet.getbalances()["mine"]["immature"], 0)114coinbase_tx_id = wallet.getblock(tip, verbose=1)["tx"][0]115assert_equal(wallet.gettransaction(coinbase_tx_id)['details'][0]['abandoned'], True)117# Abort process abruptly to mimic an unclean shutdown (no chain state flush to disk)118node.kill_process()120# Restart the node and confirm that it has not persisted the last chain state changes to disk121# that leads to a rescan by the wallet122with self.nodes[0].assert_debug_log(expected_msgs=[f"Rescanning last 1 blocks (from block {tip_height - 1})...\n"]):123self.start_node(0)124assert_equal(node.getbestblockhash(), tip)126# After disconnecting the block, the wallet should record the new best block.127# Upon reload after the crash, since the chainstate was not flushed, the tip contains the previously abandoned128# coinbase. This was rescanned and now un-abandoned.129wallet = node.get_wallet_rpc("reorg_crash")130assert_equal(wallet.gettransaction(coinbase_tx_id)['details'][0]['abandoned'], False)131assert_greater_than(wallet.getbalances()["mine"]["immature"], 0)133# Previously, a bug caused the node to crash if two block disconnection events occurred consecutively.134# Ensure this is no longer the case by simulating a new reorg.135node.invalidateblock(tip)136assert(node.getbestblockhash() != tip)137# Ensure wallet state is consistent now138assert_equal(wallet.gettransaction(coinbase_tx_id)['details'][0]['abandoned'], True)139assert_equal(wallet.getbalances()["mine"]["immature"], 0)141# And finally, verify the state if the block ends up being into the best chain again142node.reconsiderblock(tip)143assert_equal(wallet.gettransaction(coinbase_tx_id)['details'][0]['abandoned'], False)144assert_greater_than(wallet.getbalances()["mine"]["immature"], 0)146def run_test(self):147# Send a tx from which to conflict outputs later148txid_conflict_from = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), Decimal("10"))149self.generate(self.nodes[0], 1)151# Disconnect node1 from others to reorg its chain later152self.disconnect_nodes(0, 1)153self.disconnect_nodes(1, 2)154self.connect_nodes(0, 2)156# Send a tx to be unconfirmed later157txid = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), Decimal("10"))158tx = self.nodes[0].gettransaction(txid)159self.generate(self.nodes[0], 4, sync_fun=self.no_op)160self.sync_blocks([self.nodes[0], self.nodes[2]])161tx_before_reorg = self.nodes[0].gettransaction(txid)162assert_equal(tx_before_reorg["confirmations"], 4)164# Disconnect node0 from node2 to broadcast a conflict on their respective chains165self.disconnect_nodes(0, 2)166nA = next(tx_out["vout"] for tx_out in self.nodes[0].gettransaction(txid_conflict_from)["details"] if tx_out["amount"] == Decimal("10"))167inputs = []168inputs.append({"txid": txid_conflict_from, "vout": nA})169outputs_1 = {}170outputs_2 = {}172# Create a conflicted tx broadcast on node0 chain and conflicting tx broadcast on node1 chain. Both spend from txid_conflict_from173outputs_1[self.nodes[0].getnewaddress()] = Decimal("9.99998")174outputs_2[self.nodes[0].getnewaddress()] = Decimal("9.99998")175conflicted = self.nodes[0].signrawtransactionwithwallet(self.nodes[0].createrawtransaction(inputs, outputs_1))176conflicting = self.nodes[0].signrawtransactionwithwallet(self.nodes[0].createrawtransaction(inputs, outputs_2))178conflicted_txid = self.nodes[0].sendrawtransaction(conflicted["hex"])179self.generate(self.nodes[0], 1, sync_fun=self.no_op)180conflicting_txid = self.nodes[2].sendrawtransaction(conflicting["hex"])181self.generate(self.nodes[2], 9, sync_fun=self.no_op)183# Reconnect node0 and node2 and check that conflicted_txid is effectively conflicted184self.connect_nodes(0, 2)185self.sync_blocks([self.nodes[0], self.nodes[2]])186conflicted = self.nodes[0].gettransaction(conflicted_txid)187conflicting = self.nodes[0].gettransaction(conflicting_txid)188assert_equal(conflicted["confirmations"], -9)189assert_equal(conflicted["walletconflicts"][0], conflicting["txid"])191# Node0 wallet is shutdown192self.restart_node(0)194# The block chain re-orgs and the tx is included in a different block195self.generate(self.nodes[1], 9, sync_fun=self.no_op)196self.nodes[1].sendrawtransaction(tx["hex"])197self.generate(self.nodes[1], 1, sync_fun=self.no_op)198self.nodes[1].sendrawtransaction(conflicted["hex"])199self.generate(self.nodes[1], 1, sync_fun=self.no_op)201# Node0 wallet file is loaded on longest sync'ed node1202self.stop_node(1)203self.nodes[0].backupwallet(self.nodes[0].datadir_path / 'wallet.bak')204shutil.copyfile(self.nodes[0].datadir_path / 'wallet.bak', self.nodes[1].chain_path / self.default_wallet_name / self.wallet_data_filename)205self.start_node(1)206tx_after_reorg = self.nodes[1].gettransaction(txid)207# Check that normal confirmed tx is confirmed again but with different blockhash208assert_equal(tx_after_reorg["confirmations"], 2)209assert_not_equal(tx_before_reorg["blockhash"], tx_after_reorg["blockhash"])210conflicted_after_reorg = self.nodes[1].gettransaction(conflicted_txid)211# Check that conflicted tx is confirmed again with blockhash different than previously conflicting tx212assert_equal(conflicted_after_reorg["confirmations"], 1)213assert_not_equal(conflicting["blockhash"], conflicted_after_reorg["blockhash"])215# Verify we mark coinbase txs, and their descendants, as abandoned during startup216self.test_coinbase_automatic_abandon_during_startup()218# Verify reorg behavior during an unclean shutdown219self.test_reorg_handling_during_unclean_shutdown()222if __name__ == '__main__':223ReorgsRestoreTest(__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.