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 Migrating a wallet from legacy to descriptor."""6from contextlib import suppress7from pathlib import Path8import json9import os.path10import random11import shutil12import struct13import time14from decimal import Decimal16from test_framework.address import (17key_to_p2pkh,18key_to_p2wpkh,19script_to_p2sh,20script_to_p2wsh,21)22from test_framework.descriptors import descsum_create23from test_framework.key import ECPubKey24from test_framework.test_framework import BitcoinTestFramework25from test_framework.messages import COIN, CTransaction, CTxOut, ser_string26from test_framework.script import hash16027from test_framework.script_util import key_to_p2pkh_script, key_to_p2pk_script, script_to_p2sh_script, script_to_p2wsh_script28from test_framework.util import (29assert_equal,30assert_greater_than,31assert_raises_rpc_error,32find_vout_for_address,33sha256sum_file,34)35from test_framework.wallet_util import (36get_generate_key,37generate_keypair,38)40BTREE_MAGIC = 0x05316243class WalletMigrationTest(BitcoinTestFramework):44def set_test_params(self):45self.setup_clean_chain = True46self.num_nodes = 247self.supports_cli = False48self.extra_args = [[], ["-deprecatedrpc=create_bdb"]]50def skip_test_if_missing_module(self):51self.skip_if_no_wallet()52self.skip_if_no_previous_releases()54def setup_nodes(self):55self.add_nodes(56self.num_nodes,57extra_args=self.extra_args,58versions=[59None,60280200,61],62)63self.start_nodes()64self.init_wallet(node=0)66def assert_is_sqlite(self, wallet_name, wallet_loaded = True):67wallet_file_path = self.master_node.wallets_path / wallet_name / self.wallet_data_filename68with open(wallet_file_path, 'rb') as f:69file_magic = f.read(16)70assert_equal(file_magic, b'SQLite format 3\x00')71# if the wallet is not loaded we can't ask the node about it72if wallet_loaded:73assert_equal(self.master_node.get_wallet_rpc(wallet_name).getwalletinfo()["format"], "sqlite")75def assert_is_bdb(self, wallet_name):76with open(self.master_node.wallets_path / wallet_name / self.wallet_data_filename, "rb") as f:77data = f.read(16)78_, _, magic = struct.unpack("QII", data)79assert_equal(magic, BTREE_MAGIC)81def create_legacy_wallet(self, wallet_name, **kwargs):82self.old_node.createwallet(wallet_name=wallet_name, descriptors=False, **kwargs)83wallet = self.old_node.get_wallet_rpc(wallet_name)84info = wallet.getwalletinfo()85assert_equal(info["descriptors"], False)86assert_equal(info["format"], "bdb")87return wallet89def assert_addr_info_equal(self, addr_info, addr_info_old):90assert_equal(addr_info["address"], addr_info_old["address"])91assert_equal(addr_info["scriptPubKey"], addr_info_old["scriptPubKey"])92assert_equal(addr_info["ismine"], addr_info_old["ismine"])93assert_equal(addr_info["hdkeypath"], addr_info_old["hdkeypath"].replace("'","h"))94assert_equal(addr_info["solvable"], addr_info_old["solvable"])95assert_equal(addr_info["ischange"], addr_info_old["ischange"])96assert_equal(addr_info["hdmasterfingerprint"], addr_info_old["hdmasterfingerprint"])98def assert_list_txs_equal(self, received_list_txs, expected_list_txs):99for d in received_list_txs:100if "parent_descs" in d:101del d["parent_descs"]102for d in expected_list_txs:103if "parent_descs" in d:104del d["parent_descs"]105assert_equal(received_list_txs, expected_list_txs)107def check_address(self, wallet, addr, is_mine, is_change, label):108addr_info = wallet.getaddressinfo(addr)109assert_equal(addr_info['ismine'], is_mine)110assert_equal(addr_info['ischange'], is_change)111if label is not None:112assert_equal(addr_info['labels'], [label]),113else:114assert_equal(addr_info['labels'], []),116def migrate_and_get_rpc(self, wallet_name, **kwargs):117# Since we may rescan on loading of a wallet, make sure that the best block118# is written before beginning migration119# Reload to force write that record120self.old_node.unloadwallet(wallet_name)121self.old_node.loadwallet(wallet_name)122assert_equal(self.old_node.get_wallet_rpc(wallet_name).getwalletinfo()["descriptors"], False)123# Now unload so we can copy it to the master node for the migration test124self.old_node.unloadwallet(wallet_name)125if wallet_name == "":126shutil.copyfile(self.old_node.wallets_path / "wallet.dat", self.master_node.wallets_path / "wallet.dat")127else:128src = os.path.abspath(self.old_node.wallets_path / wallet_name)129dst = os.path.abspath(self.master_node.wallets_path / wallet_name)130if src != dst :131shutil.copytree(self.old_node.wallets_path / wallet_name, self.master_node.wallets_path / wallet_name, dirs_exist_ok=True)132# Check that the wallet shows up in listwalletdir with a warning about migration133wallets = self.master_node.listwalletdir()134for w in wallets["wallets"]:135if w["name"] == wallet_name:136assert_equal(w["warnings"], ["This wallet is a legacy wallet and will need to be migrated with migratewallet before it can be loaded"])138# migratewallet uses current time in naming the backup file, set a mock time139# to check that this works correctly.140mocked_time = int(time.time())141self.master_node.setmocktime(mocked_time)142# Migrate, checking that rescan does not occur143with self.master_node.assert_debug_log(expected_msgs=[], unexpected_msgs=["Rescanning"]):144migrate_info = self.master_node.migratewallet(wallet_name=wallet_name, **kwargs)145self.master_node.setmocktime(0)146# Update wallet name in case the initial wallet was completely migrated to a watch-only wallet147# (in which case the wallet name would be suffixed by the 'watchonly' term)148migrated_wallet_name = migrate_info['wallet_name']149wallet = self.master_node.get_wallet_rpc(migrated_wallet_name)150wallet_info = wallet.getwalletinfo()151assert_equal(wallet_info["descriptors"], True)152self.assert_is_sqlite(migrated_wallet_name)153# Always verify the backup path exist after migration154assert os.path.exists(migrate_info['backup_path'])155if wallet_name == "":156backup_prefix = "default_wallet"157else:158backup_prefix = os.path.basename(os.path.realpath(self.old_node.wallets_path / wallet_name))160backup_filename = f"{backup_prefix}_{mocked_time}.legacy.bak"161expected_backup_path = self.master_node.wallets_path / backup_filename162assert_equal(str(expected_backup_path), migrate_info['backup_path'])163assert {"name": backup_filename} not in self.master_node.listwalletdir()["wallets"]165# Open the wallet with sqlite and verify that the wallet has the last hardened cache flag166# set and the last hardened cache entries167def check_last_hardened(conn):168flags_rec = conn.execute(f"SELECT value FROM main WHERE key = x'{ser_string(b'flags').hex()}'").fetchone()169flags = int.from_bytes(flags_rec[0], byteorder="little")171# All wallets should have the upgrade flag set172assert_equal(bool(flags & (1 << 2)), True)174# Fetch all records with the walletdescriptorlhcache prefix175# if the wallet has private keys and is not blank176if wallet_info["private_keys_enabled"] and not wallet_info["blank"]:177lh_cache_recs = conn.execute(f"SELECT value FROM main where key >= x'{ser_string(b'walletdescriptorlhcache').hex()}' AND key < x'{ser_string(b'walletdescriptorlhcachf').hex()}'").fetchall()178assert_greater_than(len(lh_cache_recs), 0)180inspect_path = os.path.join(self.options.tmpdir, os.path.basename(f"{migrated_wallet_name}_inspect.dat"))181wallet.backupwallet(inspect_path)182self.inspect_sqlite_db(inspect_path, check_last_hardened)184return migrate_info, wallet186def test_basic(self):187# Update listtransctions' output from old nodes to be compatible188def listtransactions_compatibility(list):189deprecated_keys = {"bip125-replaceable"}190for obj in list:191for key in deprecated_keys:192obj.pop(key)193obj["alternate_wtxids"] = []194return list196default = self.master_node.get_wallet_rpc(self.default_wallet_name)198self.log.info("Test migration of a basic keys only wallet without balance")199basic0 = self.create_legacy_wallet("basic0")201addr = basic0.getnewaddress()202change = basic0.getrawchangeaddress()204old_addr_info = basic0.getaddressinfo(addr)205old_change_addr_info = basic0.getaddressinfo(change)206assert_equal(old_addr_info["ismine"], True)207assert_equal(old_addr_info["hdkeypath"], "m/0'/0'/0'")208assert_equal(old_change_addr_info["ismine"], True)209assert_equal(old_change_addr_info["hdkeypath"], "m/0'/1'/0'")211# Note: migration could take a while.212_, basic0 = self.migrate_and_get_rpc("basic0")214# The wallet should create the following descriptors:215# * BIP32 descriptors in the form of "0h/0h/*" and "0h/1h/*" (2 descriptors)216# * BIP44 descriptors in the form of "44h/1h/0h/0/*" and "44h/1h/0h/1/*" (2 descriptors)217# * BIP49 descriptors, P2SH(P2WPKH), in the form of "86h/1h/0h/0/*" and "86h/1h/0h/1/*" (2 descriptors)218# * BIP84 descriptors, P2WPKH, in the form of "84h/1h/0h/1/*" and "84h/1h/0h/1/*" (2 descriptors)219# * BIP86 descriptors, P2TR, in the form of "86h/1h/0h/0/*" and "86h/1h/0h/1/*" (2 descriptors)220# * A combo(PK) descriptor for the wallet master key.221# So, should have a total of 11 descriptors on it.222assert_equal(len(basic0.listdescriptors()["descriptors"]), 11)224# Compare addresses info225addr_info = basic0.getaddressinfo(addr)226change_addr_info = basic0.getaddressinfo(change)227self.assert_addr_info_equal(addr_info, old_addr_info)228self.assert_addr_info_equal(change_addr_info, old_change_addr_info)230addr_info = basic0.getaddressinfo(basic0.getnewaddress("", "bech32"))231assert_equal(addr_info["hdkeypath"], "m/84h/1h/0h/0/0")233self.log.info("Test migration of a basic keys only wallet with a balance")234basic1 = self.create_legacy_wallet("basic1")236for _ in range(0, 10):237default.sendtoaddress(basic1.getnewaddress(), 1)239self.generate(self.master_node, 1)241for _ in range(0, 5):242basic1.sendtoaddress(default.getnewaddress(), 0.5)244self.generate(self.master_node, 1)245bal = basic1.getbalance()246txs = basic1.listtransactions()247addr_gps = basic1.listaddressgroupings()249basic1_migrate, basic1 = self.migrate_and_get_rpc("basic1")250assert_equal(basic1.getbalance(), bal)
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.