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) 2026-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 wallet migration from ancient versions (v0.14.3) to descriptor wallets.7Previous releases are required by this test, see test/README.md.8"""9from decimal import Decimal10from pathlib import Path11import shutil13from test_framework.blocktools import COINBASE_MATURITY14from test_framework.test_framework import BitcoinTestFramework15from test_framework.util import (16assert_equal,17assert_greater_than,18assert_is_hash_string,19assert_raises_rpc_error,20dumb_sync_blocks,21)23# Wallet version constants from ancient Bitcoin Core (pre-0.15)24FEATURE_COMPRPUBKEY = 60000 # Non-HD wallet version25FEATURE_HD = 130000 # Single-chain HD wallet version (pre-v0.15)26WALLET_PASSPHRASE = "test_passphrase"29def get_vout_addresses(decoded_tx):30for vout in decoded_tx["vout"]:31script_pub_key = vout["scriptPubKey"]32if "address" in script_pub_key:33yield script_pub_key["address"]34else:35yield from script_pub_key.get("addresses", [])38def normalize_listtransactions(txs):39"""Return cross-version stable fields from listtransactions output."""40return sorted(41(tx["txid"], tx["amount"], tx["category"], tx["confirmations"])42for tx in txs43)46class WalletAncientMigrationTest(BitcoinTestFramework):47def set_test_params(self):48self.setup_clean_chain = True49self.num_nodes = 851def skip_test_if_missing_module(self):52self.skip_if_no_wallet()53self.skip_if_no_previous_releases()55def setup_network(self):56# Use dedicated old/new node pairs for each migration scenario to avoid57# resetting node datadirs between sub-tests.58self.add_nodes(self.num_nodes, versions=[140300, None] * 4)60def run_migration_test(61self,62wallet_type,63old_node_idx,64new_node_idx,65extra_args_old,66expected_version,67expect_hd,68passphrase=None,69):70"""Test migration of a v0.14.3 wallet to descriptor wallet."""71old_node = self.nodes[old_node_idx]72new_node = self.nodes[new_node_idx]74self.log.info(f"Testing {wallet_type} wallet migration")75self.start_node(old_node_idx, extra_args=extra_args_old)77# Create addresses and verify wallet version78old_addresses = [old_node.getnewaddress() for _ in range(4)]79unfunded_address = old_node.getnewaddress()80old_wallet_info = old_node.getwalletinfo()81assert_equal(old_wallet_info['walletversion'], expected_version)82# v0.14.3 predates HD split keypool (v0.15+).83assert_equal('keypoolsize_hd_internal' in old_wallet_info, False)84assert_equal('hdmasterkeyid' in old_wallet_info, expect_hd)86# Generate blocks and create transaction history87self.generatetoaddress(old_node, COINBASE_MATURITY + 1, old_addresses[0], sync_fun=self.no_op)88send_txs = []89for i, amount in enumerate([0.1, 0.2, 0.3], start=1):90send_txs.append((old_node.sendtoaddress(old_addresses[i], amount), old_addresses[i]))91self.generatetoaddress(old_node, 1, old_addresses[0], sync_fun=self.no_op)93old_balance = old_node.getbalance()94old_txs = old_node.listtransactions("*", 1000)95old_wallet_state = old_node.getwalletinfo()96assert_equal(old_balance, Decimal("99.99986440"))97assert_equal(len(old_txs), 108)99# Collect all addresses including change; listtransactions omits change100# addresses for self-sends, so decode the sent transactions explicitly.101change_addresses = set()102for txid, destination in send_txs:103decoded_tx = old_node.decoderawtransaction(old_node.gettransaction(txid)["hex"])104for address in get_vout_addresses(decoded_tx):105if address != destination:106change_addresses.add(address)108all_addresses = {tx['address'] for tx in old_txs if 'address' in tx} | change_addresses109assert_equal(unfunded_address in all_addresses, False)110assert_greater_than(len(change_addresses), 0)111assert_greater_than(len(all_addresses), 0)113# Sync blocks to modern node via RPC (avoids filesystem copy and reindex)114self.start_node(new_node_idx)115dumb_sync_blocks(src=old_node, dst=new_node)117if passphrase is None:118self.stop_node(old_node_idx)119else:120# v0.14.3 shuts down after encrypting the wallet.121old_node.encryptwallet(passphrase)122old_node.wait_until_stopped()124# Copy and migrate wallet125self.log.info("Migrating wallet to descriptor format")126old_wallet_path = old_node.chain_path / "wallet.dat"127migrated_wallet_dir = new_node.wallets_path / "migrated_wallet"128migrated_wallet_dir.mkdir(parents=True)129shutil.copy2(old_wallet_path, migrated_wallet_dir / "wallet.dat")131with new_node.assert_debug_log(expected_msgs=[], unexpected_msgs=["Rescanning"]):132migration_result = new_node.migratewallet("migrated_wallet", passphrase=passphrase)133assert_equal(migration_result['wallet_name'], 'migrated_wallet')134assert_equal('watchonly_name' in migration_result, False)135assert_equal('solvables_name' in migration_result, False)136assert Path(migration_result['backup_path']).is_file()137new_wallet = new_node.get_wallet_rpc(migration_result['wallet_name'])139# Verify migration results140self.log.info("Verifying migration")141new_wallet_info = new_wallet.getwalletinfo()142assert_equal(new_wallet_info['format'], 'sqlite')143assert_equal(new_wallet_info['descriptors'], True)145# v0.14.3 HD wallets use a single external chain. Encrypting an HD146# wallet rotates its seed, leaving the original chain inactive.147single_chain_hd_descriptors = [148descriptor for descriptor in new_wallet.listdescriptors()['descriptors']149if '/0h/0h/*h)' in descriptor['desc']150]151expected_single_chain_hd_descriptors = 0152if expect_hd:153expected_single_chain_hd_descriptors = 1154if passphrase is not None:155expected_single_chain_hd_descriptors += 1156assert_equal(len(single_chain_hd_descriptors), expected_single_chain_hd_descriptors)158assert_equal(new_wallet_info['txcount'], 105)159if passphrase is not None:160assert_equal(new_wallet_info['unlocked_until'], 0)161assert_equal(new_wallet.getbalance(), old_balance)162new_balances = new_wallet.getbalances()['mine']163assert_equal(new_balances['trusted'], old_wallet_state['balance'])164assert_equal(new_balances['untrusted_pending'], old_wallet_state['unconfirmed_balance'])165assert_equal(new_balances['immature'], old_wallet_state['immature_balance'])166assert_equal(new_wallet_info['txcount'], old_wallet_state['txcount'])167new_txs = new_wallet.listtransactions("*", 1000)168assert_equal(normalize_listtransactions(new_txs), normalize_listtransactions(old_txs))170# Verify all addresses are still owned171for addr in all_addresses:172assert_equal(new_wallet.getaddressinfo(addr)['ismine'], True)173assert_equal(new_wallet.getaddressinfo(unfunded_address)['ismine'], True)175# Test post-migration functionality176new_addr = new_wallet.getnewaddress()177if passphrase is not None:178assert_raises_rpc_error(-13, "Please enter the wallet passphrase", new_wallet.sendtoaddress, new_addr, 0.1)179new_wallet.walletpassphrase(passphrase, 60)180txid = new_wallet.sendtoaddress(new_addr, 0.1)181assert_is_hash_string(txid)183self.stop_node(new_node_idx)184self.log.info(f"{wallet_type} wallet migration successful")186def run_test(self):187self.log.info("Testing wallet migration from v0.14.3")189self.run_migration_test(190wallet_type="non-HD",191old_node_idx=0,192new_node_idx=1,193extra_args_old=["-usehd=0", "-keypool=10"],194expected_version=FEATURE_COMPRPUBKEY,195expect_hd=False,196)198self.run_migration_test(199wallet_type="encrypted non-HD",200old_node_idx=2,201new_node_idx=3,202extra_args_old=["-usehd=0", "-keypool=10"],203expected_version=FEATURE_COMPRPUBKEY,204expect_hd=False,205passphrase=WALLET_PASSPHRASE,206)208self.run_migration_test(209wallet_type="HD (VERSION_HD_BASE)",210old_node_idx=4,211new_node_idx=5,212extra_args_old=["-keypool=10"],213expected_version=FEATURE_HD,214expect_hd=True,215)217self.run_migration_test(218wallet_type="encrypted HD (VERSION_HD_BASE)",219old_node_idx=6,220new_node_idx=7,221extra_args_old=["-keypool=10"],222expected_version=FEATURE_HD,223expect_hd=True,224passphrase=WALLET_PASSPHRASE,225)228if __name__ == '__main__':229WalletAncientMigrationTest(__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.