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) 2018-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"""Backwards compatibility functional test7Test various backwards compatibility scenarios. Requires previous releases binaries,8see test/README.md.10Due to RPC changes introduced in various versions the below tests11won't work for older versions without some patches or workarounds.13Use only the latest patch version of each release, unless a test specifically14needs an older patch version.15"""17import json18import os19import shutil21from test_framework.blocktools import COINBASE_MATURITY22from test_framework.extendedkey import ExtendedPrivateKey23from test_framework.test_framework import BitcoinTestFramework24from test_framework.descriptors import descsum_create25from test_framework.messages import ser_string27from test_framework.util import (28assert_equal,29assert_greater_than,30assert_not_equal,31assert_raises_rpc_error,32)34LAST_KEYPOOL_INDEX = 9 # Index of the last derived address with the keypool size of 1036class BackwardsCompatibilityTest(BitcoinTestFramework):37def set_test_params(self):38self.setup_clean_chain = True39self.num_nodes = 1140# Add new version after each release:41self.extra_args = [42["-addresstype=bech32", "[email protected]"], # Pre-release: use to mine blocks. noban for immediate tx relay43["-nowallet", "-addresstype=bech32", "[email protected]"], # Pre-release: use to receive coins, swap wallets, etc44["-nowallet", "-addresstype=bech32", "[email protected]"], # v31.145["-nowallet", "-addresstype=bech32", "[email protected]"], # v31.046["-nowallet", "-addresstype=bech32", "[email protected]"], # v30.247["-nowallet", "-addresstype=bech32", "[email protected]"], # v25.048["-nowallet", "-addresstype=bech32", "[email protected]"], # v24.0.149["-nowallet", "-addresstype=bech32", "[email protected]"], # v23.050["-nowallet", "-addresstype=bech32", "[email protected]", f"-keypool={LAST_KEYPOOL_INDEX + 1}"], # v22.051["-nowallet", "-addresstype=bech32", "[email protected]"], # v0.21.052["-nowallet", "-addresstype=bech32", "[email protected]"], # v0.20.153]54self.wallet_names = [self.default_wallet_name]56def skip_test_if_missing_module(self):57self.skip_if_no_wallet()58self.skip_if_no_previous_releases()60def setup_nodes(self):61self.add_nodes(self.num_nodes, extra_args=self.extra_args, versions=[62None,63None,64310100,65310000,66300200,67250000,68240001,69230000,70220000,71210000,72200100,73])75self.start_nodes()76self.import_deterministic_coinbase_privkeys()78def split_version(self, node):79major = node.version // 1000080minor = (node.version % 10000) // 10081patch = (node.version % 100)82return (major, minor, patch)84def major_version_equals(self, node, major):85node_major, _, _ = self.split_version(node)86return node_major == major88def major_version_less_than(self, node, major):89node_major, _, _ = self.split_version(node)90return node_major < major92def major_version_at_least(self, node, major):93node_major, _, _ = self.split_version(node)94return node_major >= major96def test_v22_inactivehdchain_path(self):97self.log.info("Testing inactive hd chain bad derivation path cleanup")98# 0.21.x and 22.x would both produce bad derivation paths when topping up an inactive hd chain99# Make sure that this is being automatically cleaned up by migration100node_master = self.nodes[1]101node_v22 = self.nodes[self.num_nodes - 3]102wallet_name = "bad_deriv_path"103node_v22.createwallet(wallet_name=wallet_name, descriptors=False)104bad_deriv_wallet = node_v22.get_wallet_rpc(wallet_name)106# Make a dump of the wallet to get an unused address107dump_path = node_v22.wallets_path / f"{wallet_name}.dump"108bad_deriv_wallet.dumpwallet(dump_path)109addr = None110seed = None111with open(dump_path) as f:112for line in f:113if f"hdkeypath=m/0'/0'/{LAST_KEYPOOL_INDEX}'" in line:114addr = line.split(" ")[4].split("=")[1]115elif " hdseed=1 " in line:116seed = line.split(" ")[0]117assert addr is not None118assert seed is not None119# Rotate seed and unload120bad_deriv_wallet.sethdseed()121bad_deriv_wallet.unloadwallet()122# Receive at addr to trigger inactive chain topup on next load123self.nodes[0].sendtoaddress(addr, 1)124self.generate(self.nodes[0], 1, sync_fun=self.no_op)125self.sync_all(nodes=[self.nodes[0], node_master, node_v22])126node_v22.loadwallet(wallet_name)128# Dump again to find bad hd keypath129bad_deriv_path = f"m/0'/0'/{LAST_KEYPOOL_INDEX}'/0'/0'/{LAST_KEYPOOL_INDEX + 1}'"130good_deriv_path = f"m/0h/0h/{LAST_KEYPOOL_INDEX + 1}h"131os.unlink(dump_path)132bad_deriv_wallet.dumpwallet(dump_path)133bad_path_addr = None134with open(dump_path) as f:135for line in f:136if f"hdkeypath={bad_deriv_path}" in line:137bad_path_addr = line.split(" ")[4].split("=")[1]138assert bad_path_addr is not None139assert_equal(bad_deriv_wallet.getaddressinfo(bad_path_addr)["hdkeypath"], bad_deriv_path)141# Verify that this bad derivation path addr is actually at m/0'/0'/10' by making a new wallet with the same seed but larger keypool142node_v22.createwallet(wallet_name="path_verify", descriptors=False, blank=True)143verify_wallet = node_v22.get_wallet_rpc("path_verify")144verify_wallet.sethdseed(True, seed)145# Bad addr is after keypool, so need to generate it by refilling146verify_wallet.keypoolrefill(LAST_KEYPOOL_INDEX + 2)147assert_equal(verify_wallet.getaddressinfo(bad_path_addr)["hdkeypath"], good_deriv_path.replace("h", "'"))149# Migrate with master150# Since all keymeta records are now deleted after migration, the derivation path151# should now be correct as it is derived on-the-fly from the inactive hd chain's descriptor152backup_path = node_v22.wallets_path / f"{wallet_name}.bak"153bad_deriv_wallet.backupwallet(backup_path)154wallet_dir_master = os.path.join(node_master.wallets_path, wallet_name)155os.makedirs(wallet_dir_master, exist_ok=True)156shutil.copy(backup_path, os.path.join(wallet_dir_master, "wallet.dat"))157node_master.migratewallet(wallet_name)158bad_deriv_wallet_master = node_master.get_wallet_rpc(wallet_name)159assert_equal(bad_deriv_wallet_master.getaddressinfo(bad_path_addr)["hdkeypath"], good_deriv_path)160bad_deriv_wallet_master.unloadwallet()162def check_keymeta(conn):163# Retrieve all records that have the "keymeta" prefix. The remaining key data varies for each record.164keymeta_rec = conn.execute(f"SELECT value FROM main where key >= x'{ser_string(b'keymeta').hex()}' AND key < x'{ser_string(b'keymetb').hex()}'").fetchone()165assert_equal(keymeta_rec, None)167wallet_db = node_master.wallets_path / wallet_name / "wallet.dat"168self.inspect_sqlite_db(wallet_db, check_keymeta)170def test_ignore_legacy_during_startup(self, legacy_nodes, node_master):171self.log.info("Test that legacy wallets are ignored during startup on v29+")173legacy_node = legacy_nodes[0]174wallet_name = f"legacy_up_{legacy_node.version}"175legacy_node.loadwallet(wallet_name)176legacy_wallet = legacy_node.get_wallet_rpc(wallet_name)178# Move legacy wallet to latest node179wallet_path = node_master.wallets_path / wallet_name180wallet_path.mkdir()181legacy_wallet.backupwallet(wallet_path / "wallet.dat")182legacy_wallet.unloadwallet()184# Write wallet so it is automatically loaded during init185settings_path = node_master.chain_path / "settings.json"186with settings_path.open("w") as fp:187json.dump({"wallet": [wallet_name]}, fp)189# Restart latest node and verify that the legacy wallet load is skipped without exiting early during init.190self.restart_node(node_master.index, extra_args=[])191# Ensure we receive the warning message and clear the stderr pipe.192node_master.stderr.seek(0)193warning_msg = node_master.stderr.read().decode('utf-8').strip()194assert "The wallet appears to be a Legacy wallet, please use the wallet migration tool (migratewallet RPC or the GUI option)" in warning_msg195node_master.stderr.truncate(0), node_master.stderr.seek(0) # reset buffer197# Verify the node is still running (no shutdown occurred during startup)198node_master.getblockcount()199# Reset settings for any subsequent test200os.remove(settings_path)202def test_downgrade_preserves_witness_variants(self, node_master, node_miner, node_old):203# Ensure the wallet keeps every witness variant of a tx during a downgrade. An old204# release updating the tx record must not drop them.205self.log.info("Test that witness variants survive a downgrade that rewrites the tx record")207# Earlier tests may have restarted node_master or dropped its peers208self.connect_nodes(node_miner.index, node_master.index)209self.connect_nodes(node_master.index, node_old.index)210self.sync_blocks([node_miner, node_master, node_old])212wallet_name = "altwit_downgrade"213node_master.createwallet(wallet_name)214wallet = node_master.get_wallet_rpc(wallet_name)216xprvs = [ExtendedPrivateKey.generate() for _ in range(0, 2)]217xpubs = [xprv.pubkey() for xprv in xprvs]219# Taproot descriptor with a public-only internal key: the wallet can only220# spend via the script path, so the key path is signed separately below.221script_path_desc = descsum_create(f"tr({xpubs[0].to_string()}/*,pk({xprvs[1].to_string()}/*))")222assert_equal(wallet.importdescriptors([{"desc": script_path_desc, "active": True, "timestamp": "now"}])[0]["success"], True)224# Fund tr output owned by the wallet225node_miner.sendtoaddress(wallet.getnewaddress(address_type="bech32m"), 1)226self.sync_mempools([node_miner, node_master])227self.generate(node_miner, 1, sync_fun=self.no_op)228self.sync_blocks([node_miner, node_master, node_old])229wallet.syncwithvalidationinterfacequeue()231# Two witness variants of the same spend: same txid, different wtxid232psbt = wallet.walletcreatefundedpsbt(outputs=[{node_miner.getnewaddress(): 0.5}])["psbt"]233script_path_tx = wallet.finalizepsbt(wallet.walletprocesspsbt(psbt=psbt, finalize=False)["psbt"])["hex"]234script_path_wtxid = node_master.decoderawtransaction(script_path_tx)["hash"]236key_path_desc = descsum_create(f"tr({xprvs[0].to_string()}/*,pk({xpubs[1].to_string()}/*))")237key_path_psbt = node_master.descriptorprocesspsbt(psbt=psbt, descriptors=[{"desc": key_path_desc}], finalize=False)["psbt"]238key_path_tx = wallet.finalizepsbt(key_path_psbt)["hex"]239key_path_wtxid = node_master.decoderawtransaction(key_path_tx)["hash"]240assert_not_equal(script_path_wtxid, key_path_wtxid)242# Store variant A through the mempool; store variant B by mining it. The243# confirmed (key path) variant becomes canonical, the script path an alternate.244txid = node_master.sendrawtransaction(script_path_tx)245wallet.syncwithvalidationinterfacequeue()246block_hash = self.generateblock(node_master, node_miner.getnewaddress(), [key_path_tx], sync_fun=self.no_op)["hash"]247self.sync_blocks([node_miner, node_master, node_old])248wallet.syncwithvalidationinterfacequeue()249assert_equal(wallet.gettransaction(txid)["alternate_wtxids"], [script_path_wtxid])250wallet.unloadwallet()
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.