wallet: migration, fix watch-only and solvables wallets names
What changed, and why it matters
This commit fixes a naming bug during Bitcoin Core wallet migration. When a user migrates the default wallet (which has no name), the newly created watch-only and solvables wallets were being given names like '_watchonly' and '_solvables' instead of 'default_wallet_watchonly' and 'default_wallet_solvables'. The fix applies the same 'default_wallet' prefix already used for backup files. This is primarily a correctness and usability fix, not a security vulnerability, though the resulting name collision could theoretically cause migration failures or confusion.
No urgent security action required. Treat as a normal bug fix. Users relying on wallet migration should ensure they are on a version containing this commit to avoid unexpected wallet names or migration failures when migrating the default wallet.
Security signals we found
Wallet migration naming collision could cause migration failure or overwrite adjacent wallet directories
No cryptographic, consensus, or network-layer changes
No explicit security impact described by vendor
Evidence from the diff
The change introduces a helper function MigrationPrefixName() that returns the wallet name if present or ‘default_wallet’ otherwise. It uses this helper when naming the watch-only and solvables wallets produced by descriptor migration, and also for the backup file prefix. Previously, calling wallet.GetName() on an unnamed default wallet returned an empty string, producing wallet names like ‘_watchonly’ and ‘_solvables’. The patch also updates functional tests to verify the new naming behavior and to clean up the generated wallets.
Changed components
src/wallet/wallet.cpptest/functional/wallet_migration.pyBitcoin Core wallet migration (legacy to descriptor)Inspect captured patch +44 / −5
diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp
index 987c2155..4db94020 100644
--- a/src/wallet/wallet.cpp
+++ b/src/wallet/wallet.cpp
@@ -4096,6 +4096,15 @@ bool CWallet::CanGrindR() const
return !IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS);
}
+// Returns wallet prefix for migration.
+// Used to name the backup file and newly created wallets.
+// E.g. a watch-only wallet is named "<prefix>_watchonly".
+static std::string MigrationPrefixName(CWallet& wallet)
+{
+ const std::string& name{wallet.GetName()};
+ return name.empty() ? "default_wallet" : name;
+}
+
bool DoMigration(CWallet& wallet, WalletContext& context, bilingual_str& error, MigrationResult& res) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet)
{
AssertLockHeld(wallet.cs_wallet);
@@ -4127,7 +4136,7 @@ bool DoMigration(CWallet& wallet, WalletContext& context, bilingual_str& error,
DatabaseStatus status;
std::vector<bilingual_str> warnings;
- std::string wallet_name = wallet.GetName() + "_watchonly";
+ std::string wallet_name = MigrationPrefixName(wallet) + "_watchonly";
std::unique_ptr<WalletDatabase> database = MakeWalletDatabase(wallet_name, options, status, error);
if (!database) {
error = strprintf(_("Wallet file creation failed: %s"), error);
@@ -4166,7 +4175,7 @@ bool DoMigration(CWallet& wallet, WalletContext& context, bilingual_str& error,
DatabaseStatus status;
std::vector<bilingual_str> warnings;
- std::string wallet_name = wallet.GetName() + "_solvables";
+ std::string wallet_name = MigrationPrefixName(wallet) + "_solvables";
std::unique_ptr<WalletDatabase> database = MakeWalletDatabase(wallet_name, options, status, error);
if (!database) {
error = strprintf(_("Wallet file creation failed: %s"), error);
@@ -4281,7 +4290,7 @@ util::Result<MigrationResult> MigrateLegacyToDescriptor(std::shared_ptr<CWallet>
// cases, but in the case where the wallet name is a path to a data file,
// the name of the data file is used, and in the case where the wallet name
// is blank, "default_wallet" is used.
- const std::string backup_prefix = wallet_name.empty() ? "default_wallet" : [&] {
+ const std::string backup_prefix = wallet_name.empty() ? MigrationPrefixName(*local_wallet) : [&] {
// fs::weakly_canonical resolves relative specifiers and remove trailing slashes.
const auto legacy_wallet_path = fs::weakly_canonical(GetWalletDir() / fs::PathFromString(wallet_name));
return fs::PathToString(legacy_wallet_path.filename());
diff --git a/test/functional/wallet_migration.py b/test/functional/wallet_migration.py
index 27caab8f..f802c186 100755
--- a/test/functional/wallet_migration.py
+++ b/test/functional/wallet_migration.py
@@ -3,6 +3,7 @@
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""Test Migrating a wallet from legacy to descriptor."""
+from pathlib import Path
import os.path
import random
import shutil
@@ -659,6 +660,14 @@ class WalletMigrationTest(BitcoinTestFramework):
assert_equal(bals, wallet.getbalances())
+ def clear_default_wallet(self, backup_file):
+ # Test cleanup: Clear unnamed default wallet for subsequent tests
+ (self.old_node.wallets_path / "wallet.dat").unlink()
+ (self.master_node.wallets_path / "wallet.dat").unlink(missing_ok=True)
+ shutil.rmtree(self.master_node.wallets_path / "default_wallet_watchonly", ignore_errors=True)
+ shutil.rmtree(self.master_node.wallets_path / "default_wallet_solvables", ignore_errors=True)
+ backup_file.unlink()
+
def test_default_wallet(self):
self.log.info("Test migration of the wallet named as the empty string")
wallet = self.create_legacy_wallet("")
@@ -676,6 +685,26 @@ class WalletMigrationTest(BitcoinTestFramework):
assert os.path.basename(res["backup_path"]).startswith("default_wallet")
wallet.unloadwallet()
+ self.clear_default_wallet(backup_file=Path(res["backup_path"]))
+
+ def test_default_wallet_watch_only(self):
+ self.log.info("Test unnamed (default) watch-only wallet migration")
+ master_wallet = self.master_node.get_wallet_rpc(self.default_wallet_name)
+ wallet = self.create_legacy_wallet("", blank=True)
+ wallet.importaddress(master_wallet.getnewaddress(address_type="legacy"))
+
+ res, wallet = self.migrate_and_get_rpc("")
+
+ info = wallet.getwalletinfo()
+ assert_equal(info["descriptors"], True)
+ assert_equal(info["format"], "sqlite")
+ assert_equal(info["private_keys_enabled"], False)
+ assert_equal(info["walletname"], "default_wallet_watchonly")
+ # Check the default wallet is not available anymore
+ assert not (self.master_node.wallets_path / "wallet.dat").exists()
+
+ wallet.unloadwallet()
+ self.clear_default_wallet(backup_file=Path(res["backup_path"]))
def test_default_wallet_failure(self):
self.log.info("Test failure during unnamed (default) wallet migration")
@@ -685,7 +714,7 @@ class WalletMigrationTest(BitcoinTestFramework):
# Create wallet directory with the watch-only name and a wallet file.
# Because the wallet dir exists, this will cause migration to fail.
- watch_only_dir = self.master_node.wallets_path / "_watchonly"
+ watch_only_dir = self.master_node.wallets_path / "default_wallet_watchonly"
os.mkdir(watch_only_dir)
shutil.copyfile(self.old_node.wallets_path / "wallet.dat", watch_only_dir / "wallet.dat")
@@ -705,7 +734,7 @@ class WalletMigrationTest(BitcoinTestFramework):
self.assert_is_bdb("")
# Test cleanup: clear default wallet for next test
- os.remove(self.old_node.wallets_path / "wallet.dat")
+ self.clear_default_wallet(backup_path)
def test_direct_file(self):
self.log.info("Test migration of a wallet that is not in a wallet directory")
@@ -1594,6 +1623,7 @@ class WalletMigrationTest(BitcoinTestFramework):
self.test_wallet_with_path("path/that/ends/in/..")
self.test_default_wallet_failure()
self.test_default_wallet()
+ self.test_default_wallet_watch_only()
self.test_direct_file()
self.test_addressbook()
self.test_migrate_raw_p2sh()
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.