wallet: make loading the wallet after migrating optional
What changed, and why it matters
This Bitcoin Core change lets users migrate old-style wallets to the newer descriptor format without automatically loading the migrated wallet afterward. The main goal is to support pruned nodes, which previously could not migrate because loading the wallet required full block history. The patch also removes the old wallet from the startup list when migration is done without loading. There is no obvious security bug here; it is a feature/robustness improvement.
No security action required. Treat as a normal feature/robustness patch. Reviewers may want to confirm that skipping `LoadWallet` does not leave stale state or partially migrated wallets in unexpected conditions, and that the `RemoveWalletSetting` empty-name path is only reachable through intended RPC/GUI flows.
Security signals we found
No direct vulnerability signal in diff
Change is feature-oriented: optional wallet loading after migration
Defensive setting update: old wallet removed from startup list when not loaded
No memory safety, cryptographic, or authorization changes visible
No explicit vendor security framing in commit message
Evidence from the diff
The commit adds an optional load_wallet parameter to MigrateLegacyToDescriptor and DoMigration. When load_wallet=false, the migration still creates the new descriptor wallet files and updates settings, but it skips the post-migration LoadWallet call and explicitly sets the old wallet’s load_on_startup to false. A related tweak in RemoveWalletSetting allows an empty wallet name to suppress the backwards-compatible default wallet autoload by writing an empty array when the setting is null. The change is defensive and broadens migration support for pruned nodes.
Changed components
src/wallet/wallet.cppsrc/wallet/wallet.hWallet migration (legacy to descriptor)Wallet startup/load-on-startup settingsInspect captured patch +36 / −22
diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp
index c84effe7..e64b27a6 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<MigrationResult> MigrateLegacyToDescriptor(const std::string& wallet_name, const SecureString& passphrase, WalletContext& context)
+util::Result<MigrationResult> MigrateLegacyToDescriptor(const std::string& wallet_name, const SecureString& passphrase, WalletContext& context, bool load_wallet)
{
std::vector<bilingual_str> warnings;
bilingual_str error;
@@ -4336,10 +4343,10 @@ util::Result<MigrationResult> 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<MigrationResult> MigrateLegacyToDescriptor(std::shared_ptr<CWallet> local_wallet, const SecureString& passphrase, WalletContext& context)
+util::Result<MigrationResult> MigrateLegacyToDescriptor(std::shared_ptr<CWallet> local_wallet, const SecureString& passphrase, WalletContext& context, bool load_wallet)
{
MigrationResult res;
bilingual_str error;
@@ -4405,7 +4412,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 {
@@ -4447,30 +4454,37 @@ util::Result<MigrationResult> MigrateLegacyToDescriptor(std::shared_ptr<CWallet>
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<CWallet>* wallet_ptr : {&local_wallet, &res.watchonly_wallet, &res.solvables_wallet}) {
if (success && *wallet_ptr) {
std::shared_ptr<CWallet>& 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 b964846f..dfd52c1d 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<MigrationResult> MigrateLegacyToDescriptor(const std::string& wallet_name, const SecureString& passphrase, WalletContext& context);
+[[nodiscard]] util::Result<MigrationResult> 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<MigrationResult> MigrateLegacyToDescriptor(std::shared_ptr<CWallet> local_wallet, const SecureString& passphrase, WalletContext& context);
+[[nodiscard]] util::Result<MigrationResult> MigrateLegacyToDescriptor(std::shared_ptr<CWallet> local_wallet, const SecureString& passphrase, WalletContext& context, bool load_wallet = true);
//! Determine the path that the wallet is stored in
util::Result<fs::path> GetWalletPath(const std::string& name);
Why this scored 23/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.