rpc, wallet: add an option to not load the wallet after migrating #35266

pull polespinasa wants to merge 5 commits into bitcoin:master from polespinasa:2026-05-11-addoptiontonorescanwhenwalletmigrate changing 7 files +209 −34
  1. polespinasa commented at 8:47 PM on May 11, 2026: member

    This PR is motivated by this Stack Exchange question.

    Long story short, someone who has a node pruned before his legacy wallet birthday, is unable to migrate the wallet as it is not possible to load it.

    Loading is not necessary for migration, and migrating without wanting to use the wallet in that node is a valid use-case.

    This PR adds a new RPC argument to migratewallet that allow the user disabling the wallet loading. Second commits adds tests for it.

    Follow-up: Add an option to the GUI to not load the wallet after migrating.

  2. DrahtBot commented at 8:47 PM on May 11, 2026: contributor

    <!--e57a25ab6845829454e8d69fc972939a-->

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    <!--006a51241073e994b41acfe9ec718e94-->

    Code Coverage & Benchmarks

    For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/35266.

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    ACK achow101, w0xlt, pablomartin4btc

    If your review is incorrectly listed, please copy-paste <code>&lt;!--meta-tag:bot-skip--&gt;</code> into the comment that the bot should ignore.

    <!--174a7506f384e20aa4161008e828411d-->

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #34909 (wallet, refactor: modularise wallet by extracting out legacy wallet migration by rkrux)

    If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

  3. w0xlt commented at 9:00 PM on May 11, 2026: contributor

    Concept ACK.

  4. polespinasa force-pushed on May 11, 2026
  5. DrahtBot added the label CI failed on May 11, 2026
  6. polespinasa force-pushed on May 11, 2026
  7. in src/wallet/rpc/wallet.cpp:596 in 525b30dcb2
     592 | @@ -593,6 +593,7 @@ static RPCMethod migratewallet()
     593 |          {
     594 |              {"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."},
     595 |              {"passphrase", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "The wallet passphrase"},
     596 | +            {"load_wallet", RPCArg::Type::BOOL, RPCArg::Optional::OMITTED, "Load the wallet after migration. If not specified the wallet will be loaded."}
    


    achow101 commented at 9:34 PM on May 11, 2026:

    In 6d3916f03206cb123da342da1200259e554bae46 "rpc, wallet: add option to not load the wallet after migration"

                {"load_wallet", RPCArg::Type::BOOL, RPCArg::Default{true}, "Load the wallet after migration."}
    

    polespinasa commented at 10:21 PM on May 11, 2026:

    done

  8. in src/wallet/rpc/wallet.cpp:624 in 525b30dcb2
     617 | @@ -617,8 +618,13 @@ static RPCMethod migratewallet()
     618 |                  wallet_pass = std::string_view{request.params[1].get_str()};
     619 |              }
     620 |  
     621 | +            bool loadwallet{true};
     622 | +            if (!request.params[2].isNull()) {
     623 | +                loadwallet = request.params[2].get_bool();
     624 | +            }
    


    achow101 commented at 9:37 PM on May 11, 2026:

    In 6d3916f03206cb123da342da1200259e554bae46 "rpc, wallet: add option to not load the wallet after migration"

                bool loadwallet = self.Arg<bool>("load_wallet");
    

    polespinasa commented at 10:21 PM on May 11, 2026:

    done

  9. in src/wallet/wallet.cpp:4461 in 525b30dcb2 outdated
    4465 | -                    LogError("Failed to load wallet '%s' after migration. Rolling back migration to preserve consistency. "
    4466 | -                             "Error cause: %s\n", wallet_name, error.original);
    4467 | -                    success = false;
    4468 | -                    break;
    4469 | +                if (load_wallet) {
    4470 | +                    wallet = LoadWallet(context, wallet_name, /*load_on_start=*/std::nullopt, options, status, error, warnings);
    


    achow101 commented at 9:52 PM on May 11, 2026:

    In 6d3916f03206cb123da342da1200259e554bae46 "rpc, wallet: add option to not load the wallet after migration"

    For watchonly_wallet and solvables_wallet, without loading those wallets, we lose the output to the user that those wallets were created. MigrationResult should be updated to include the watchonly and solvables wallets names so we can return that info to the user when those wallets are not being loaded.


    polespinasa commented at 2:12 PM on May 12, 2026:

    We only lose that output when not loading the wallet? I mean is it something this PR broke?


    achow101 commented at 5:51 PM on May 12, 2026:

    When those wallets are not loaded, we no longer inform the user that those wallets have been created in the RPC result. But we should still do that, so MigrationResult should have fields for those wallet names.


    pablomartin4btc commented at 3:21 AM on June 24, 2026:

    is this still an issue?


    polespinasa commented at 7:01 AM on June 25, 2026:

    no, it was fixed in 97d08d62baf00dc4b045c4b9e2fd82ebd27b3d45

  10. DrahtBot removed the label CI failed on May 11, 2026
  11. polespinasa force-pushed on May 11, 2026
  12. polespinasa commented at 10:38 PM on May 11, 2026: member

    note to self - need release note

  13. in src/wallet/rpc/wallet.cpp:596 in d77b45e9c9
     592 | @@ -593,6 +593,7 @@ static RPCMethod migratewallet()
     593 |          {
     594 |              {"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."},
     595 |              {"passphrase", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "The wallet passphrase"},
     596 | +            {"load_wallet", RPCArg::Type::BOOL, RPCArg::Default{true}, "Load the wallet after migration. If not specified the wallet will be loaded."}
    


    stickies-v commented at 1:11 PM on May 12, 2026:

    nit: the Default value is already encoded in the help string, no need to manually repeat it with "if not specified..."

    bitcoin-cli help migratewallet
    ...
    3. load_wallet    (boolean, optional, default=true) Load the wallet after migration. If not specified the wallet will be loaded.`
    

    polespinasa commented at 2:14 PM on May 12, 2026:

    wops, true! forgot to remove that part of the sentence when added the Default value :)

    fixed!

  14. polespinasa force-pushed on May 12, 2026
  15. w0xlt commented at 9:30 PM on May 12, 2026: contributor

    The current code fails when migration creates watchonly and/or solvables wallets.

    This happens in the post-migration loop inside MigrateLegacyToDescriptor():

    for (std::shared_ptr<CWallet>* wallet_ptr : {&local_wallet, &res.watchonly_wallet, &res.solvables_wallet}) { ... }
    

    Inside the loop, the wallet pointer is reset before the optional reload:

    wallet.reset();
    

    When load_wallet=false, the wallet is not reloaded, so res.wallet remains null. The code then uses !res.wallet to decide whether the primary wallet name still needs to be set:

    if (!res.wallet) {
        res.wallet_name = wallet_name;
    }
    

    Because res.wallet remains null throughout the loop, this condition is true for the primary wallet, the watchonly wallet, and the solvables wallet. As a result, res.wallet_name is overwritten and ends up containing the last auxiliary wallet name, e.g. <wallet>_solvables, instead of the primary wallet name.

    The same no-load path also clears res.watchonly_wallet and res.solvables_wallet, so the RPC response cannot derive watchonly_name or solvables_name from those pointers.

    Test:

    diff --git a/test/functional/wallet_migration.py b/test/functional/wallet_migration.py
    index 48ae1fb051..78730aa3fe 100755
    --- a/test/functional/wallet_migration.py
    +++ b/test/functional/wallet_migration.py
    @@ -1537,6 +1537,27 @@ class WalletMigrationTest(BitcoinTestFramework):
             assert_equal(loaded_wallet.getbalance(), bals["mine"]["trusted"])
             loaded_wallet.unloadwallet()
     
    +    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()])
    +
    +        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()
    +
         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")
    @@ -1669,6 +1690,7 @@ class WalletMigrationTest(BitcoinTestFramework):
             self.test_miniscript()
             self.test_taproot()
             self.test_no_load_after_migration()
    +        self.test_no_load_reports_auxiliary_wallet_names()
             self.test_solvable_no_privs()
             self.test_loading_failure_after_migration()
    
  16. polespinasa force-pushed on May 20, 2026
  17. polespinasa commented at 7:53 PM on May 20, 2026: member

    @w0xlt @achow101 I think the issue you were pointing is solved.

    Inside the loop, the wallet pointer is reset before the optional reload:

    I am actually not sure if we need the wallet.reset() as we are just overriding it after that. @w0xlt I picked your test and added you as a co-author.

  18. in src/wallet/wallet.cpp:4460 in 0c6df0a168
    4465 | -                    LogError("Failed to load wallet '%s' after migration. Rolling back migration to preserve consistency. "
    4466 | -                             "Error cause: %s\n", wallet_name, error.original);
    4467 | -                    success = false;
    4468 | -                    break;
    4469 | +                if (load_wallet) {
    4470 | +                    wallet.reset();
    


    achow101 commented at 8:12 PM on May 20, 2026:

    In 0c6df0a1684f0f48ed92b9dcc8416c1a3d174d98 "wallet: make loading the wallet after migrating optional"

    This reset() should be outside of the if to ensure that the wallet is unloaded from the local context. Otherwise the solvables and watchonly wallets are left loaded in a context without a chain.


    polespinasa commented at 10:00 PM on May 20, 2026:

    done

  19. polespinasa force-pushed on May 20, 2026
  20. polespinasa force-pushed on May 20, 2026
  21. DrahtBot added the label CI failed on May 20, 2026
  22. 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<CWallet> objects.
    
    This allows in a future commit migrate wallet without loading them.
    97d08d62ba
  23. polespinasa force-pushed on May 20, 2026
  24. w0xlt commented at 11:55 PM on May 20, 2026: contributor

    Currently when load_wallet=false, the migration code still wrote the new wallet name(s) into settings.json's wallet array, so the wallets would silently be reloaded on the next node restart — defeating the whole point of load_wallet=false.

    Suggestion:

    <details> <summary>diff</summary>

    diff --git a/src/wallet/test/wallet_tests.cpp b/src/wallet/test/wallet_tests.cpp
    index 5cd47f6722..bd0367f3a4 100644
    --- a/src/wallet/test/wallet_tests.cpp
    +++ b/src/wallet/test/wallet_tests.cpp
    @@ -244,6 +244,13 @@ BOOST_FIXTURE_TEST_CASE(write_wallet_settings_concurrently, TestingSetup)
             Assert(RemoveWalletSetting(*chain, strprintf("wallet_%d", i)));
         },
                                 /*num_expected_wallets=*/0);
    +
    +    Assert(chain->deleteRwSettings("wallet"));
    +    BOOST_REQUIRE(chain->getRwSetting("wallet").isNull());
    +    Assert(RemoveWalletSetting(*chain, ""));
    +    auto wallets = chain->getRwSetting("wallet");
    +    BOOST_CHECK(wallets.isArray());
    +    BOOST_CHECK_EQUAL(wallets.getValues().size(), 0);
     }
     
     static int64_t AddTx(ChainstateManager& chainman, CWallet& wallet, uint32_t lockTime, int64_t mockTime, int64_t blockTime)
    diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp
    index 792aafaaa7..342e179c88 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, bool load_wallet) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet)
     {
         AssertLockHeld(wallet.cs_wallet);
     
    @@ -4239,8 +4246,10 @@ bool DoMigration(CWallet& wallet, WalletContext& context, bilingual_str& error,
                     }
                 }
     
    -            // Add the wallet to settings
    -            UpdateWalletSetting(*context.chain, wallet_name, /*load_on_startup=*/true, warnings);
    +            if (load_wallet) {
    +                // Add the wallet to settings
    +                UpdateWalletSetting(*context.chain, wallet_name, /*load_on_startup=*/true, warnings);
    +            }
             }
             if (data->solvable_descs.size() > 0) {
                 wallet.WalletLogPrintf("Making a new watchonly wallet containing the unwatched solvable scripts\n");
    @@ -4278,8 +4287,10 @@ bool DoMigration(CWallet& wallet, WalletContext& context, bilingual_str& error,
                     }
                 }
     
    -            // Add the wallet to settings
    -            UpdateWalletSetting(*context.chain, wallet_name, /*load_on_startup=*/true, warnings);
    +            if (load_wallet) {
    +                // Add the wallet to settings
    +                UpdateWalletSetting(*context.chain, wallet_name, /*load_on_startup=*/true, warnings);
    +            }
             }
         }
     
    @@ -4405,7 +4416,7 @@ util::Result<MigrationResult> MigrateLegacyToDescriptor(std::shared_ptr<CWallet>
     
             // 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 {
    @@ -4439,6 +4450,9 @@ util::Result<MigrationResult> MigrateLegacyToDescriptor(std::shared_ptr<CWallet>
     
         if (success) {
             Assume(!res.wallet); // We will set it here.
    +        if (!load_wallet) {
    +            UpdateWalletSetting(*context.chain, wallet_name, /*load_on_startup=*/false, warnings);
    +        }
             // Check if the local wallet is empty after migration
             if (empty_local_wallet) {
                 // This wallet has no records. We can safely remove it.
    @@ -4455,13 +4469,16 @@ util::Result<MigrationResult> MigrateLegacyToDescriptor(std::shared_ptr<CWallet>
                     // Track db path
                     track_for_cleanup(*wallet);
                     assert(wallet.use_count() == 1);
    -                std::string wallet_name = wallet->GetName();
    +                const std::string migrated_wallet_name = wallet->GetName();
                     wallet.reset();
    +                if (!load_wallet) {
    +                    UpdateWalletSetting(*context.chain, migrated_wallet_name, /*load_on_startup=*/false, warnings);
    +                }
                     if (load_wallet) {
    -                    wallet = LoadWallet(context, wallet_name, /*load_on_start=*/std::nullopt, options, status, error, warnings);
    +                    wallet = LoadWallet(context, migrated_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);
    +                                 "Error cause: %s\n", migrated_wallet_name, error.original);
                             success = false;
                             break;
                         }
    @@ -4469,13 +4486,13 @@ util::Result<MigrationResult> MigrateLegacyToDescriptor(std::shared_ptr<CWallet>
                     // Set the first wallet as the main one.
                     // The loop order is intentional and must always start with the local wallet.
                     if (res.wallet_name.empty()) {
    -                    res.wallet_name = wallet_name;
    +                    res.wallet_name = migrated_wallet_name;
                         if (load_wallet) res.wallet = std::move(wallet);
                     }
                     if (wallet_ptr == &res.watchonly_wallet) {
    -                    res.watchonly_wallet_name = wallet_name;
    +                    res.watchonly_wallet_name = migrated_wallet_name;
                     } else if (wallet_ptr == &res.solvables_wallet) {
    -                    res.solvables_wallet_name = wallet_name;
    +                    res.solvables_wallet_name = migrated_wallet_name;
                     }
                 }
             }
    diff --git a/test/functional/wallet_migration.py b/test/functional/wallet_migration.py
    index 36eba41282..44bd7744bb 100755
    --- a/test/functional/wallet_migration.py
    +++ b/test/functional/wallet_migration.py
    @@ -4,13 +4,14 @@
     # file COPYING or http://www.opensource.org/licenses/mit-license.php.
     """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
     import struct
     import time
     from decimal import Decimal
    +from pathlib import Path
     
     from test_framework.address import (
         key_to_p2pkh,
    @@ -1547,6 +1548,12 @@ class WalletMigrationTest(BitcoinTestFramework):
             _, pubkey = generate_keypair(compressed=True, wif=True)
             wallet.addmultisigaddress(nrequired=1, keys=[pubkey.hex()])
     
    +        self.master_node.createwallet(wallet_name=wallet_name, load_on_startup=True)
    +        self.master_node.unloadwallet(wallet_name)
    +        shutil.rmtree(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)
     
    @@ -1559,6 +1566,12 @@ class WalletMigrationTest(BitcoinTestFramework):
             assert f"{wallet_name}_watchonly" not in self.master_node.listwallets()
             assert f"{wallet_name}_solvables" not in self.master_node.listwallets()
     
    +        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")
    

    </details>

  25. DrahtBot removed the label CI failed on May 21, 2026
  26. polespinasa force-pushed on May 21, 2026
  27. polespinasa commented at 9:06 AM on May 21, 2026: member

    Currently when load_wallet=false, the migration code still wrote the new wallet name(s) into settings.json's wallet array, so the wallets would silently be reloaded on the next node restart — defeating the whole point of load_wallet=false.

    Good catch!! I was testing it with only wallet_name, but unless you have a previous wallet created with that name it does not add it to settings.json.

    I (think) I fixed it with a more simpler approach, your diff had some changes that I think are not necessary. Why the changes in RemoveWalletSetting?

  28. w0xlt commented at 7:29 PM on May 21, 2026: contributor

    The RemoveWalletSetting change is needed for the unnamed wallet case.

    For old unnamed wallets, if settings.json has no wallet key, startup may still treat the existing top-level wallet.dat specially and load it for backwards compatibility. So “no wallet setting” does not mean “load no wallets”.

    Bitcoin Core no longer auto-creates a default wallet, but it still has backwards-compatible auto-load behavior for an existing top-level unnamed wallet (src/wallet/load.cpp):

      // For backwards compatibility if an unnamed top level wallet exists...
      if (!args.IsArgSet("wallet")) {
          ...
          if (MakeWalletDatabase("", ...)) {
              wallets.push_back("");
              chain.overwriteRwSetting("wallet", wallets, SKIP_WRITE);
          }
      }
    

    When migratewallet("", load_wallet=false) runs, we need to persist the user’s intent that the unnamed wallet should not be loaded on the next startup. But current RemoveWalletSetting("", ...) sees the missing/null wallet setting and returns SKIP_WRITE, so nothing is written. After restart, Core can again see no wallet key and autoload the unnamed wallet.

    The patch suggested above makes RemoveWalletSetting("", ...) write an explicit empty array:

    "wallet": []
    

    That means “there are intentionally zero startup wallets”, which suppresses the backwards-compatible unnamed-wallet autoload. For named wallets, the old no-op behavior when the setting is missing is still fine.

    The test below shows this behavior. It will fail with current code and succeed in the suggested above patch:

    <details> <summary>diff</summary>

    diff --git a/test/functional/wallet_migration.py b/test/functional/wallet_migration.py
    index cd4411682d..f563efd172 100755
    --- a/test/functional/wallet_migration.py
    +++ b/test/functional/wallet_migration.py
    @@ -183,6 +183,17 @@ class WalletMigrationTest(BitcoinTestFramework):
     
             return migrate_info, wallet
     
    +    def remove_wallet_setting(self, 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")
    +
         def test_basic(self):
             default = self.master_node.get_wallet_rpc(self.default_wallet_name)
     
    @@ -1540,6 +1551,42 @@ class WalletMigrationTest(BitcoinTestFramework):
             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)
    +        self.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()
    +
    +        if "" in wallets_after_restart:
    +            self.master_node.unloadwallet("")
    +        self.clear_default_wallet(backup_path)
    +        self.master_node.loadwallet(self.default_wallet_name, load_on_startup=True)
    +
    +        assert_equal(wallet_setting_after_migration, [])
    +        assert "" not in wallets_after_restart
    +
    +
         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"
    @@ -1711,6 +1758,7 @@ class WalletMigrationTest(BitcoinTestFramework):
             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()
    

    </details>

  29. polespinasa commented at 10:33 AM on May 22, 2026: member

    Oh I see, I didn't catch that odd case. Thanks @w0xlt. Taking your patch :)

  30. polespinasa force-pushed on May 22, 2026
  31. w0xlt commented at 11:07 PM on May 22, 2026: contributor

    ACK 565037e30f2b005d22f3757111c9aba2902a8786

  32. polespinasa requested review from achow101 on Jun 4, 2026
  33. in doc/release-notes-35266:4 in 565037e30f
       0 | @@ -0,0 +1,7 @@
       1 | +RPC
       2 | +---
       3 | +
       4 | +The `migratewallet` RPC now gives the option to not load the wallet after migrating it.
    


    pablomartin4btc commented at 6:07 PM on June 13, 2026:

    nit (I know that legacy can't longer be loaded... or if we don't want to use "descriptor" for some reason you can use "new" as at the last sentence)

    The `migratewallet` RPC now gives the option to not load the descriptor wallet after having been migrated.
    
  34. in doc/release-notes-35266:5 in 565037e30f
       0 | @@ -0,0 +1,7 @@
       1 | +RPC
       2 | +---
       3 | +
       4 | +The `migratewallet` RPC now gives the option to not load the wallet after migrating it.
       5 | +This will allow pruned nodes that cannot migrate an old wallet to be able to migrate it.
    


    pablomartin4btc commented at 6:12 PM on June 13, 2026:

    nit...

    This will now allow pruned nodes to be able to migrate an out of sync wallet.
    
  35. pablomartin4btc commented at 6:13 PM on June 13, 2026: member

    Concept ACK

    Long story short, someone who has a node pruned before his legacy wallet birthday, is unable to migrate the wallet as it is not possible to load it.

    Is that because currently in master at the end of the migration, the once migrated descriptor wallet can't be loaded (due to missing blocks in the pruned node)? (I'd clarify that's the descriptor wallet that can't be loaded). Even now with your PR, the loading is optional, by default is true, so the user will get the error first, the process will fail and the user will have to run it again (and somehow finding out that they need to pass this new arg set to false)? I don't know if possible but perhaps the migration should finish with success in the case that the problem is only loading the migrated wallet due to its sync and the user should be informed of that? Unless there are other issues that could be hidden with this approach, but if we allow them to set the load migrated wallet to false, it's the same case.

    Loading is not necessary for migration...

    So the legacy wallet (BERKELEY_RO db) it's getting loaded (CWallet::LoadExisting(empty_context..) but because it has an empty context doesn't need the rescan?

  36. polespinasa commented at 7:34 PM on June 19, 2026: member

    Even now with your PR, the loading is optional, by default is true, so the user will get the error first, the process will fail and the user will have to run it again (and somehow finding out that they need to pass this new arg set to false)?

    Yes, which would be similar as setting it to false by default and users trying to load it manually and failing.

    I don't know if possible but perhaps the migration should finish with success in the case that the problem is only loading the migrated wallet due to its sync and the user should be informed of that?

    I don't think this is a good idea, ideally the wallet should be able to be loaded to be sure the migration is correct and there is no corruption or any other error. This PR only adds an option to not load it bc in a specific case where a user is unable to load it because the node is pruned and has no other option.

    Making it check if the reason for not loading it is due to being pruned adds some complexity that I don't see worth adding. And IIUC errors are passed as strings so it's something I don't find "pretty" to use as conditional.

    Also at the end this is a "one"-time use RPC call, it's not a big deal to fail the migration and start it again.

    So the legacy wallet (BERKELEY_RO db) it's getting loaded (CWallet::LoadExisting(empty_context..) but because it has an empty context doesn't need the rescan?

    Correct :)

  37. polespinasa force-pushed on Jun 22, 2026
  38. polespinasa commented at 11:33 AM on June 22, 2026: member

    Force pushed to fix some nits @pablomartin4btc taken with a few tweaks.

  39. achow101 added the label Wallet on Jun 22, 2026
  40. in test/functional/wallet_migration.py:1582 in de7a10285a
    1577 | +
    1578 | +        self.restart_node(0)
    1579 | +        wallets_after_restart = self.master_node.listwallets()
    1580 | +
    1581 | +        if "" in wallets_after_restart:
    1582 | +            self.master_node.unloadwallet("")
    


    achow101 commented at 8:10 PM on June 22, 2026:

    In de7a10285ac327901b90cd19ef7942ef6200a3cf "test: tests wallet migration with load_wallet disabled"

    This is unnecessary, if the test is successful, the wallet will not be loaded. The assert of that can be moved up so that the test fails before you try to delete things.

  41. in src/wallet/wallet.cpp:114 in 75c50d786c
     108 | @@ -109,7 +109,14 @@ bool AddWalletSetting(interfaces::Chain& chain, const std::string& wallet_name)
     109 |  bool RemoveWalletSetting(interfaces::Chain& chain, const std::string& wallet_name)
     110 |  {
     111 |      const auto update_function = [&wallet_name](common::SettingsValue& setting_value) {
     112 | -        if (!setting_value.isArray()) return interfaces::SettingsAction::SKIP_WRITE;
     113 | +        if (!setting_value.isArray()) {
     114 | +            if (wallet_name.empty() && setting_value.isNull()) {
     115 | +                // Empty setting sppresses backwards-compatible default wallet autoload.
    


    achow101 commented at 8:11 PM on June 22, 2026:

    In 75c50d786c7abf16317ba6c2285b9a26848dda5e "wallet: make loading the wallet after migrating optional"

    nit: s/sppresses/suppresses

  42. 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.
    4acd063ba6
  43. rpc: Add load_wallet argument to migratewallet RPC b98dd63da7
  44. test: tests wallet migration with load_wallet disabled
    Co-authored-by: w0xlt <woltx@protonmail.com>
    517d37ce3e
  45. add release note 0cdd817a82
  46. polespinasa force-pushed on Jun 22, 2026
  47. polespinasa commented at 9:08 PM on June 22, 2026: member

    Force pushed to address some nits and a small test correction

  48. achow101 commented at 12:01 AM on June 23, 2026: member

    ACK 0cdd817a82a26be53efa6038dae85fceb0cf2248

  49. DrahtBot requested review from w0xlt on Jun 23, 2026
  50. DrahtBot requested review from pablomartin4btc on Jun 23, 2026
  51. w0xlt commented at 6:18 AM on June 23, 2026: contributor

    ACK 0cdd817a82a26be53efa6038dae85fceb0cf2248

  52. in src/wallet/rpc/wallet.cpp:596 in 0cdd817a82
     592 | @@ -595,6 +593,7 @@ static RPCMethod migratewallet()
     593 |          {
     594 |              {"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."},
     595 |              {"passphrase", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "The wallet passphrase"},
     596 | +            {"load_wallet", RPCArg::Type::BOOL, RPCArg::Default{true}, "Load the wallet after migration."},
    


    pablomartin4btc commented at 3:23 AM on June 24, 2026:

    nit: perhaps some hint can be added here... (or similar idea)

                {"load_wallet", RPCArg::Type::BOOL, RPCArg::Default{true}, "Load the wallet after migration. Set to false if the wallet is out of sync on a pruned node."},
    
  53. pablomartin4btc commented at 3:31 AM on June 24, 2026: member
  54. sedited merged this on Jun 24, 2026
  55. sedited closed this on Jun 24, 2026

  56. polespinasa deleted the branch on Jun 25, 2026
  57. in doc/release-notes-35266:7 in 0cdd817a82
       0 | @@ -0,0 +1,7 @@
       1 | +RPC
       2 | +---
       3 | +
       4 | +The `migratewallet` RPC now gives the option to not load the descriptor wallet after migrating it from a legacy wallet.
       5 | +This will now allow pruned nodes to migrate a wallet out of sync below the pruning height.
       6 | +Note that to use the new wallet it must be loaded to a full node anyway.
       7 | +
    


    sedited commented at 12:35 PM on June 26, 2026:

    Missed that the file suffix is missing here. Do we care about that?


    polespinasa commented at 12:37 PM on June 26, 2026:

    Oh I missed that, my bad. Should I open small PR just adding it?


    sedited commented at 12:41 PM on June 26, 2026:

    I'm just not sure if any tooling depends on the .md suffix. Otherwise it seems pretty harmless if the only impact is that github isn't rendering it like the others until we consolidate them for v32.


    polespinasa commented at 12:44 PM on June 26, 2026:

    I'm just not sure if any tooling depends on the .md suffix.

    Probably @achow101 knows it


    achow101 commented at 7:18 PM on June 26, 2026:

    It shouldn't matter, the consolidation is done by hand.


github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-07-02 06:51 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me