From 97d08d62baf00dc4b045c4b9e2fd82ebd27b3d45 Mon Sep 17 00:00:00 2001 From: Pol Espinasa Date: Wed, 20 May 2026 23:44:00 +0200 Subject: [PATCH 1/5] refactor: store wallet names to MigrationResult Store wallet names into MigrationResult struct when migrating a wallet. Also refactor the RPC and the wallet interface to rely on them instead of pointers to shared_ptr objects. This allows in a future commit migrate wallet without loading them. --- src/wallet/interfaces.cpp | 4 ++-- src/wallet/rpc/wallet.cpp | 8 ++++---- src/wallet/wallet.cpp | 11 +++++++++-- src/wallet/wallet.h | 2 ++ 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/wallet/interfaces.cpp b/src/wallet/interfaces.cpp index 4eee155ce21..0237c795e1b 100644 --- a/src/wallet/interfaces.cpp +++ b/src/wallet/interfaces.cpp @@ -605,8 +605,8 @@ public: if (!res) return util::Error{util::ErrorString(res)}; WalletMigrationResult out{ .wallet = MakeWallet(m_context, res->wallet), - .watchonly_wallet_name = res->watchonly_wallet ? std::make_optional(res->watchonly_wallet->GetName()) : std::nullopt, - .solvables_wallet_name = res->solvables_wallet ? std::make_optional(res->solvables_wallet->GetName()) : std::nullopt, + .watchonly_wallet_name = res->watchonly_wallet_name, + .solvables_wallet_name = res->solvables_wallet_name, .backup_path = res->backup_path, }; return out; diff --git a/src/wallet/rpc/wallet.cpp b/src/wallet/rpc/wallet.cpp index b6c9179d19d..df40e51e347 100644 --- a/src/wallet/rpc/wallet.cpp +++ b/src/wallet/rpc/wallet.cpp @@ -625,11 +625,11 @@ static RPCMethod migratewallet() UniValue r{UniValue::VOBJ}; r.pushKV("wallet_name", res->wallet_name); - if (res->watchonly_wallet) { - r.pushKV("watchonly_name", res->watchonly_wallet->GetName()); + if (res->watchonly_wallet_name.has_value()) { + r.pushKV("watchonly_name", res->watchonly_wallet_name.value()); } - if (res->solvables_wallet) { - r.pushKV("solvables_name", res->solvables_wallet->GetName()); + if (res->solvables_wallet_name.has_value()) { + r.pushKV("solvables_name", res->solvables_wallet_name.value()); } r.pushKV("backup_path", res->backup_path.utf8string()); diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index fcc1969d0a4..c84effe7237 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -4449,6 +4449,7 @@ util::Result MigrateLegacyToDescriptor(std::shared_ptr LogInfo("Loading new wallets after migration...\n"); // Migration successful, load all the migrated wallets. + bool main_wallet_set{false}; for (std::shared_ptr* wallet_ptr : {&local_wallet, &res.watchonly_wallet, &res.solvables_wallet}) { if (success && *wallet_ptr) { std::shared_ptr& wallet = *wallet_ptr; @@ -4467,9 +4468,15 @@ util::Result MigrateLegacyToDescriptor(std::shared_ptr // Set the first successfully loaded wallet as the main one. // The loop order is intentional and must always start with the local wallet. - if (!res.wallet) { - res.wallet_name = wallet->GetName(); + if (!main_wallet_set) { + res.wallet_name = wallet_name; res.wallet = std::move(wallet); + main_wallet_set = true; + } + if (wallet_ptr == &res.watchonly_wallet) { + res.watchonly_wallet_name = wallet_name; + } else if (wallet_ptr == &res.solvables_wallet) { + res.solvables_wallet_name = wallet_name; } } } diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index 32ef9c8e1a3..b964846f351 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -1134,6 +1134,8 @@ bool RemoveWalletSetting(interfaces::Chain& chain, const std::string& wallet_nam struct MigrationResult { std::string wallet_name; + std::optional watchonly_wallet_name; + std::optional solvables_wallet_name; std::shared_ptr wallet; std::shared_ptr watchonly_wallet; std::shared_ptr solvables_wallet; From 4acd063ba6fbe32bf28babac052cbbfd1d965c56 Mon Sep 17 00:00:00 2001 From: Pol Espinasa Date: Wed, 20 May 2026 21:31:56 +0200 Subject: [PATCH 2/5] wallet: make loading the wallet after migrating optional Loading the wallet after migrating is not a necessary step. By making it optional pruned nodes can also migrate legacy wallets. Also remove the migrated wallet from the list of wallets to load on startup. --- src/wallet/wallet.cpp | 54 +++++++++++++++++++++++++++---------------- src/wallet/wallet.h | 4 ++-- 2 files changed, 36 insertions(+), 22 deletions(-) diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index c84effe7237..e64b27a633e 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -109,7 +109,14 @@ bool AddWalletSetting(interfaces::Chain& chain, const std::string& wallet_name) bool RemoveWalletSetting(interfaces::Chain& chain, const std::string& wallet_name) { const auto update_function = [&wallet_name](common::SettingsValue& setting_value) { - if (!setting_value.isArray()) return interfaces::SettingsAction::SKIP_WRITE; + if (!setting_value.isArray()) { + if (wallet_name.empty() && setting_value.isNull()) { + // Empty setting suppresses backwards-compatible default wallet autoload. + setting_value.setArray(); + return interfaces::SettingsAction::WRITE; + } + return interfaces::SettingsAction::SKIP_WRITE; + } common::SettingsValue new_value(common::SettingsValue::VARR); for (const auto& value : setting_value.getValues()) { if (!value.isStr() || value.get_str() != wallet_name) new_value.push_back(value); @@ -4177,7 +4184,7 @@ static std::string MigrationPrefixName(CWallet& wallet) return name.empty() ? "default_wallet" : name; } -bool DoMigration(CWallet& wallet, WalletContext& context, bilingual_str& error, MigrationResult& res) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet) +bool DoMigration(CWallet& wallet, WalletContext& context, bilingual_str& error, MigrationResult& res, const bool load_on_startup = true) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet) { AssertLockHeld(wallet.cs_wallet); @@ -4240,7 +4247,7 @@ bool DoMigration(CWallet& wallet, WalletContext& context, bilingual_str& error, } // Add the wallet to settings - UpdateWalletSetting(*context.chain, wallet_name, /*load_on_startup=*/true, warnings); + UpdateWalletSetting(*context.chain, wallet_name, load_on_startup, warnings); } if (data->solvable_descs.size() > 0) { wallet.WalletLogPrintf("Making a new watchonly wallet containing the unwatched solvable scripts\n"); @@ -4279,7 +4286,7 @@ bool DoMigration(CWallet& wallet, WalletContext& context, bilingual_str& error, } // Add the wallet to settings - UpdateWalletSetting(*context.chain, wallet_name, /*load_on_startup=*/true, warnings); + UpdateWalletSetting(*context.chain, wallet_name, load_on_startup, warnings); } } @@ -4294,7 +4301,7 @@ bool DoMigration(CWallet& wallet, WalletContext& context, bilingual_str& error, }); } -util::Result MigrateLegacyToDescriptor(const std::string& wallet_name, const SecureString& passphrase, WalletContext& context) +util::Result MigrateLegacyToDescriptor(const std::string& wallet_name, const SecureString& passphrase, WalletContext& context, bool load_wallet) { std::vector warnings; bilingual_str error; @@ -4336,10 +4343,10 @@ util::Result MigrateLegacyToDescriptor(const std::string& walle return util::Error{Untranslated("Wallet loading failed.") + Untranslated(" ") + error}; } - return MigrateLegacyToDescriptor(std::move(local_wallet), passphrase, context); + return MigrateLegacyToDescriptor(std::move(local_wallet), passphrase, context, load_wallet); } -util::Result MigrateLegacyToDescriptor(std::shared_ptr local_wallet, const SecureString& passphrase, WalletContext& context) +util::Result MigrateLegacyToDescriptor(std::shared_ptr local_wallet, const SecureString& passphrase, WalletContext& context, bool load_wallet) { MigrationResult res; bilingual_str error; @@ -4405,7 +4412,7 @@ util::Result MigrateLegacyToDescriptor(std::shared_ptr // Do the migration of keys and scripts for non-empty wallets, and cleanup if it fails if (HasLegacyRecords(*local_wallet)) { - success = DoMigration(*local_wallet, context, error, res); + success = DoMigration(*local_wallet, context, error, res, load_wallet); // No scripts mean empty wallet after migration empty_local_wallet = local_wallet->GetAllScriptPubKeyMans().empty(); } else { @@ -4447,30 +4454,37 @@ util::Result MigrateLegacyToDescriptor(std::shared_ptr for (const auto& path_to_remove : paths_to_remove) fs::remove(path_to_remove); } - LogInfo("Loading new wallets after migration...\n"); - // Migration successful, load all the migrated wallets. + if (load_wallet) { + LogInfo("Loading new wallets after migration...\n"); + /** We only override the load_on_startup setting in case the user explicitly said + * that he does not want to load the wallet, otherwise keep the old wallet configuration */ + } else { + UpdateWalletSetting(*context.chain, wallet_name, /*load_on_startup=*/false, warnings); + } + // Migration successful, if load_wallet is set load all the migrated wallets. bool main_wallet_set{false}; for (std::shared_ptr* wallet_ptr : {&local_wallet, &res.watchonly_wallet, &res.solvables_wallet}) { if (success && *wallet_ptr) { std::shared_ptr& wallet = *wallet_ptr; - // Track db path and load wallet + // Track db path track_for_cleanup(*wallet); assert(wallet.use_count() == 1); std::string wallet_name = wallet->GetName(); wallet.reset(); - wallet = LoadWallet(context, wallet_name, /*load_on_start=*/std::nullopt, options, status, error, warnings); - if (!wallet) { - LogError("Failed to load wallet '%s' after migration. Rolling back migration to preserve consistency. " - "Error cause: %s\n", wallet_name, error.original); - success = false; - break; + if (load_wallet) { + wallet = LoadWallet(context, wallet_name, /*load_on_start=*/std::nullopt, options, status, error, warnings); + if (!wallet) { + LogError("Failed to load wallet '%s' after migration. Rolling back migration to preserve consistency. " + "Error cause: %s\n", wallet_name, error.original); + success = false; + break; + } } - - // Set the first successfully loaded wallet as the main one. + // Set the first wallet as the main one. // The loop order is intentional and must always start with the local wallet. if (!main_wallet_set) { res.wallet_name = wallet_name; - res.wallet = std::move(wallet); + if (load_wallet) res.wallet = std::move(wallet); main_wallet_set = true; } if (wallet_ptr == &res.watchonly_wallet) { diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index b964846f351..dfd52c1dbfe 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -1143,9 +1143,9 @@ struct MigrationResult { }; //! Do all steps to migrate a legacy wallet to a descriptor wallet -[[nodiscard]] util::Result MigrateLegacyToDescriptor(const std::string& wallet_name, const SecureString& passphrase, WalletContext& context); +[[nodiscard]] util::Result MigrateLegacyToDescriptor(const std::string& wallet_name, const SecureString& passphrase, WalletContext& context, bool load_wallet = true); //! Requirement: The wallet provided to this function must be isolated, with no attachment to the node's context. -[[nodiscard]] util::Result MigrateLegacyToDescriptor(std::shared_ptr local_wallet, const SecureString& passphrase, WalletContext& context); +[[nodiscard]] util::Result MigrateLegacyToDescriptor(std::shared_ptr local_wallet, const SecureString& passphrase, WalletContext& context, bool load_wallet = true); //! Determine the path that the wallet is stored in util::Result GetWalletPath(const std::string& name); From b98dd63da7b9b14a3655f00021cc5a35826ccc15 Mon Sep 17 00:00:00 2001 From: Pol Espinasa Date: Wed, 20 May 2026 21:34:13 +0200 Subject: [PATCH 3/5] rpc: Add load_wallet argument to migratewallet RPC --- src/rpc/client.cpp | 1 + src/wallet/rpc/wallet.cpp | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index a28543fb3bb..4741df8a947 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -388,6 +388,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "loadtxoutset", 0, "path", ParamFormat::STRING }, { "migratewallet", 0, "wallet_name", ParamFormat::STRING }, { "migratewallet", 1, "passphrase", ParamFormat::STRING }, + { "migratewallet", 2, "load_wallet"}, { "setlabel", 1, "label", ParamFormat::STRING }, { "signmessage", 1, "message", ParamFormat::STRING }, { "signmessagewithprivkey", 1, "message", ParamFormat::STRING }, diff --git a/src/wallet/rpc/wallet.cpp b/src/wallet/rpc/wallet.cpp index df40e51e347..1dd668c03e4 100644 --- a/src/wallet/rpc/wallet.cpp +++ b/src/wallet/rpc/wallet.cpp @@ -593,6 +593,7 @@ static RPCMethod migratewallet() { {"wallet_name", RPCArg::Type::STR, RPCArg::DefaultHint{"the wallet name from the RPC endpoint"}, "The name of the wallet to migrate. If provided both here and in the RPC endpoint, the two must be identical."}, {"passphrase", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "The wallet passphrase"}, + {"load_wallet", RPCArg::Type::BOOL, RPCArg::Default{true}, "Load the wallet after migration."}, }, RPCResult{ RPCResult::Type::OBJ, "", "", @@ -617,8 +618,10 @@ static RPCMethod migratewallet() wallet_pass = std::string_view{request.params[1].get_str()}; } + const bool loadwallet = self.Arg("load_wallet"); + WalletContext& context = EnsureWalletContext(request.context); - util::Result res = MigrateLegacyToDescriptor(wallet_name, wallet_pass, context); + util::Result res = MigrateLegacyToDescriptor(wallet_name, wallet_pass, context, loadwallet); if (!res) { throw JSONRPCError(RPC_WALLET_ERROR, util::ErrorString(res).original); } From 517d37ce3ebfe264f44f7b563ca29dfdb0891534 Mon Sep 17 00:00:00 2001 From: Pol Espinasa Date: Mon, 11 May 2026 22:12:52 +0200 Subject: [PATCH 4/5] test: tests wallet migration with load_wallet disabled Co-authored-by: w0xlt --- test/functional/wallet_migration.py | 147 +++++++++++++++++++++++++++- 1 file changed, 144 insertions(+), 3 deletions(-) diff --git a/test/functional/wallet_migration.py b/test/functional/wallet_migration.py index e28cfd578c5..d2c155938af 100755 --- a/test/functional/wallet_migration.py +++ b/test/functional/wallet_migration.py @@ -5,6 +5,7 @@ """Test Migrating a wallet from legacy to descriptor.""" from contextlib import suppress from pathlib import Path +import json import os.path import random import shutil @@ -62,12 +63,14 @@ class WalletMigrationTest(BitcoinTestFramework): self.start_nodes() self.init_wallet(node=0) - def assert_is_sqlite(self, wallet_name): + def assert_is_sqlite(self, wallet_name, wallet_loaded = True): wallet_file_path = self.master_node.wallets_path / wallet_name / self.wallet_data_filename with open(wallet_file_path, 'rb') as f: file_magic = f.read(16) assert_equal(file_magic, b'SQLite format 3\x00') - assert_equal(self.master_node.get_wallet_rpc(wallet_name).getwalletinfo()["format"], "sqlite") + # if the wallet is not loaded we can't ask the node about it + if wallet_loaded: + assert_equal(self.master_node.get_wallet_rpc(wallet_name).getwalletinfo()["format"], "sqlite") def assert_is_bdb(self, wallet_name): with open(self.master_node.wallets_path / wallet_name / self.wallet_data_filename, "rb") as f: @@ -1493,6 +1496,134 @@ class WalletMigrationTest(BitcoinTestFramework): assert_equal(watchonly.getaddressinfo(tr_addr)["ismine"], True) assert_equal(watchonly.getaddressinfo(tr_script_addr)["ismine"], True) + def test_no_load_after_migration(self): + self.log.info("Test migration with load_wallet disabled") + default = self.master_node.get_wallet_rpc(self.default_wallet_name) + + wallet_name = "no_load_after_migration" + wallet = self.create_legacy_wallet(wallet_name) + addr = wallet.getnewaddress() + txid = default.sendtoaddress(addr, 1) + self.generate(self.master_node, 1) + bals = wallet.getbalances() + + # Copy wallet from old node to master node + self.old_node.unloadwallet(wallet_name) + shutil.copytree( + self.old_node.wallets_path / wallet_name, + self.master_node.wallets_path / wallet_name, + dirs_exist_ok=True, + ) + + migrate_info = self.master_node.migratewallet(wallet_name=wallet_name, load_wallet=False) + + # The wallet should NOT be loaded after migration + assert wallet_name not in self.master_node.listwallets() + + # The returned wallet_name should be populated correctly + assert_equal(migrate_info["wallet_name"], wallet_name) + + # The backup path should be reported and exist on disk + assert os.path.exists(migrate_info["backup_path"]) + + # The migrated wallet files should be on disk in SQLite format + self.assert_is_sqlite(wallet_name, wallet_loaded = False) + + # Load the wallet and verify its state is correct after migration + self.master_node.loadwallet(wallet_name) + loaded_wallet = self.master_node.get_wallet_rpc(wallet_name) + info = loaded_wallet.getwalletinfo() + assert_equal(info["descriptors"], True) + assert_equal(info["format"], "sqlite") + loaded_wallet.gettransaction(txid) + assert_equal(loaded_wallet.getbalance(), bals["mine"]["trusted"]) + loaded_wallet.unloadwallet() + + def test_no_load_unnamed_wallet_does_not_autoload(self): + self.log.info("Test no-load migration of unnamed wallet suppresses default autoload") + self.master_node = self.nodes[0] + self.old_node = self.nodes[1] + + wallet = self.create_legacy_wallet("", load_on_startup=False) + wallet.unloadwallet() + + self.stop_node(0) + + def remove_wallet_setting(node): + settings_path = node.chain_path / "settings.json" + if not settings_path.exists(): + return + with settings_path.open(encoding="utf8") as settings_file: + settings = json.load(settings_file) + settings.pop("wallet", None) + with settings_path.open("w", encoding="utf8") as settings_file: + json.dump(settings, settings_file, indent=4) + settings_file.write("\n") + + remove_wallet_setting(self.master_node) + (self.master_node.wallets_path / "wallet.dat").unlink(missing_ok=True) + shutil.copyfile(self.old_node.wallets_path / "wallet.dat", self.master_node.wallets_path / "wallet.dat") + self.start_node(0) + + assert_equal(self.master_node.listwallets(), []) + + migrate_info = self.master_node.migratewallet(wallet_name="", load_wallet=False) + assert_equal(migrate_info["wallet_name"], "") + backup_path = Path(migrate_info["backup_path"]) + assert "" not in self.master_node.listwallets() + + with (self.master_node.chain_path / "settings.json").open(encoding="utf8") as settings_file: + wallet_setting_after_migration = json.load(settings_file).get("wallet") + + self.restart_node(0) + wallets_after_restart = self.master_node.listwallets() + + assert "" not in wallets_after_restart + + self.clear_default_wallet(backup_path) + self.master_node.loadwallet(self.default_wallet_name, load_on_startup=True) + + assert_equal(wallet_setting_after_migration, []) + + def test_no_load_reports_auxiliary_wallet_names(self): + self.log.info("Test no-load migration reports auxiliary wallet names") + wallet_name = "no_load_auxiliary_names" + wallet = self.create_legacy_wallet(wallet_name) + + wallet.importaddress(address=self.master_node.get_wallet_rpc(self.default_wallet_name).getnewaddress(), rescan=False) + _, pubkey = generate_keypair(compressed=True, wif=True) + wallet.addmultisigaddress(nrequired=1, keys=[pubkey.hex()]) + + # Simulate that the wallet was created by the master node with an old version + # so it should already know the wallet prior migration. Set load_on_startup true, + # to simulate that the wallet was automatically being loaded on each restart. + self.master_node.createwallet(wallet_name=wallet_name, load_on_startup=True) + self.master_node.unloadwallet(wallet_name) + self.cleanup_folder(self.master_node.wallets_path / wallet_name) + with (self.master_node.chain_path / "settings.json").open(encoding="utf8") as settings_file: + assert wallet_name in json.load(settings_file).get("wallet", []) + + self.old_node.unloadwallet(wallet_name) + shutil.copytree(self.old_node.wallets_path / wallet_name, self.master_node.wallets_path / wallet_name) + + migrate_info = self.master_node.migratewallet(wallet_name=wallet_name, load_wallet=False) + + assert_equal(migrate_info["wallet_name"], wallet_name) + assert_equal(migrate_info["watchonly_name"], f"{wallet_name}_watchonly") + assert_equal(migrate_info["solvables_name"], f"{wallet_name}_solvables") + assert wallet_name not in self.master_node.listwallets() + assert f"{wallet_name}_watchonly" not in self.master_node.listwallets() + assert f"{wallet_name}_solvables" not in self.master_node.listwallets() + + # Migrate wallet with load_wallet=False should have overwritten the wallet settings + # and the load_on_startup setting must removed. + with (self.master_node.chain_path / "settings.json").open(encoding="utf8") as settings_file: + startup_wallets = json.load(settings_file).get("wallet", []) + assert wallet_name not in startup_wallets + assert f"{wallet_name}_watchonly" not in startup_wallets + assert f"{wallet_name}_solvables" not in startup_wallets + + def test_solvable_no_privs(self): self.log.info("Test migrating a multisig that we do not have any private keys for") wallet = self.create_legacy_wallet("multisig_noprivs") @@ -1544,7 +1675,7 @@ class WalletMigrationTest(BitcoinTestFramework): self.connect_nodes(1, 0) def unsynced_wallet_on_pruned_node_fails(self): - self.log.info("Test migration of an unsynced wallet on a pruned node fails gracefully") + self.log.info("Test migration of an unsynced wallet on a pruned node fails gracefully if loadwallet is set") wallet = self.create_legacy_wallet("", load_on_startup=False) last_wallet_synced_block = wallet.getwalletinfo()['lastprocessedblock']['height'] wallet.unloadwallet() @@ -1571,6 +1702,13 @@ class WalletMigrationTest(BitcoinTestFramework): backup_path = self.master_node.wallets_path / f"default_wallet_{mocked_time}.legacy.bak" assert backup_path.exists() + self.log.info("Test migration of an unsynced wallet on a pruned node does not fails if loadwallet is not set") + self.master_node.migratewallet("", load_wallet=False) + # The wallet should NOT be loaded after migration + assert "" not in self.master_node.listwallets() + # Load the wallet should fail + assert_raises_rpc_error(-4, "last wallet synchronisation goes beyond pruned data. You need to -reindex (download the whole blockchain again in case of a pruned node)", self.master_node.loadwallet, filename="") + self.clear_default_wallet(backup_path) @@ -1617,6 +1755,9 @@ class WalletMigrationTest(BitcoinTestFramework): self.test_disallowed_p2wsh() self.test_miniscript() self.test_taproot() + self.test_no_load_after_migration() + self.test_no_load_unnamed_wallet_does_not_autoload() + self.test_no_load_reports_auxiliary_wallet_names() self.test_solvable_no_privs() self.test_loading_failure_after_migration() From 0cdd817a82a26be53efa6038dae85fceb0cf2248 Mon Sep 17 00:00:00 2001 From: Pol Espinasa Date: Tue, 12 May 2026 16:20:28 +0200 Subject: [PATCH 5/5] add release note --- doc/release-notes-35266 | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 doc/release-notes-35266 diff --git a/doc/release-notes-35266 b/doc/release-notes-35266 new file mode 100644 index 00000000000..2c54c88e7f7 --- /dev/null +++ b/doc/release-notes-35266 @@ -0,0 +1,7 @@ +RPC +--- + +The `migratewallet` RPC now gives the option to not load the descriptor wallet after migrating it from a legacy wallet. +This will now allow pruned nodes to migrate a wallet out of sync below the pruning height. +Note that to use the new wallet it must be loaded to a full node anyway. +