Skip to content

Commit 35cae56

Browse files
committed
Merge bitcoin/bitcoin#31423: wallet: migration, avoid creating spendable wallet from a watch-only legacy wallet
b789907 wallet: migration, avoid creating spendable wallet from a watch-only legacy wallet (furszy) e86d71b wallet: refactor, dedup wallet re-loading code (furszy) 1de423e wallet: introduce method to return all db created files (furszy) d04f6a9 refactor: remove sqlite dir path back-and-forth conversion (furszy) Pull request description: Currently, the migration process creates a brand-new descriptor wallet with no connection to the user's legacy wallet when the legacy wallet lacks key material and contains only watch-only scripts. This behavior is not aligned with user expectations. If the legacy wallet contains only watch-only scripts, the migration process should only generate a watch-only wallet instead. TODO List: * Explain that `migratewallet` renames the watch-only after migration, and also that the wallet will not have keys enabled. ACKs for top commit: achow101: ACK b789907 pablomartin4btc: tACK b789907 rkrux: LGTM ACK b789907 Tree-SHA512: 1d583ac4b206fb477e9727daf4b5ad9c3e18b12d40e1ab4a61e8565da44c3d0327c892b51cf47b4894405d122e414cefb6b6366c357e02a74a7ca96e06762d83
2 parents a92e8b1 + b789907 commit 35cae56

File tree

8 files changed

+86
-24
lines changed

8 files changed

+86
-24
lines changed

src/qt/walletcontroller.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -468,7 +468,7 @@ void MigrateWalletActivity::migrate(const std::string& name)
468468
auto res{node().walletLoader().migrateWallet(name, passphrase)};
469469

470470
if (res) {
471-
m_success_message = tr("The wallet '%1' was migrated successfully.").arg(GUIUtil::HtmlEscape(GUIUtil::WalletDisplayName(res->wallet->getWalletName())));
471+
m_success_message = tr("The wallet '%1' was migrated successfully.").arg(GUIUtil::HtmlEscape(GUIUtil::WalletDisplayName(name)));
472472
if (res->watchonly_wallet_name) {
473473
m_success_message += QChar(' ') + tr("Watchonly scripts have been migrated to a new wallet named '%1'.").arg(GUIUtil::HtmlEscape(GUIUtil::WalletDisplayName(res->watchonly_wallet_name.value())));
474474
}

src/wallet/db.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,9 @@ class WalletDatabase
155155
/** Return path to main database file for logs and error messages. */
156156
virtual std::string Filename() = 0;
157157

158+
/** Return paths to all database created files */
159+
virtual std::vector<fs::path> Files() = 0;
160+
158161
virtual std::string Format() = 0;
159162

160163
/** Make a DatabaseBatch connected to this database */

src/wallet/migrate.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ class BerkeleyRODatabase : public WalletDatabase
5050

5151
/** Return path to main database file for logs and error messages. */
5252
std::string Filename() override { return fs::PathToString(m_filepath); }
53+
std::vector<fs::path> Files() override { return {m_filepath}; }
5354

5455
std::string Format() override { return "bdb_ro"; }
5556

src/wallet/sqlite.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,12 +112,12 @@ Mutex SQLiteDatabase::g_sqlite_mutex;
112112
int SQLiteDatabase::g_sqlite_count = 0;
113113

114114
SQLiteDatabase::SQLiteDatabase(const fs::path& dir_path, const fs::path& file_path, const DatabaseOptions& options, bool mock)
115-
: WalletDatabase(), m_mock(mock), m_dir_path(fs::PathToString(dir_path)), m_file_path(fs::PathToString(file_path)), m_write_semaphore(1), m_use_unsafe_sync(options.use_unsafe_sync)
115+
: WalletDatabase(), m_mock(mock), m_dir_path(dir_path), m_file_path(fs::PathToString(file_path)), m_write_semaphore(1), m_use_unsafe_sync(options.use_unsafe_sync)
116116
{
117117
{
118118
LOCK(g_sqlite_mutex);
119119
LogPrintf("Using SQLite Version %s\n", SQLiteDatabaseVersion());
120-
LogPrintf("Using wallet %s\n", m_dir_path);
120+
LogPrintf("Using wallet %s\n", fs::PathToString(m_dir_path));
121121

122122
if (++g_sqlite_count == 1) {
123123
// Setup logging
@@ -253,7 +253,7 @@ void SQLiteDatabase::Open()
253253

254254
if (m_db == nullptr) {
255255
if (!m_mock) {
256-
TryCreateDirectories(fs::PathFromString(m_dir_path));
256+
TryCreateDirectories(m_dir_path);
257257
}
258258
int ret = sqlite3_open_v2(m_file_path.c_str(), &m_db, flags, nullptr);
259259
if (ret != SQLITE_OK) {

src/wallet/sqlite.h

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ class SQLiteDatabase : public WalletDatabase
104104
private:
105105
const bool m_mock{false};
106106

107-
const std::string m_dir_path;
107+
const fs::path m_dir_path;
108108

109109
const std::string m_file_path;
110110

@@ -147,6 +147,14 @@ class SQLiteDatabase : public WalletDatabase
147147
bool Backup(const std::string& dest) const override;
148148

149149
std::string Filename() override { return m_file_path; }
150+
/** Return paths to all database created files */
151+
std::vector<fs::path> Files() override
152+
{
153+
std::vector<fs::path> files;
154+
files.emplace_back(m_dir_path / fs::PathFromString(m_file_path));
155+
files.emplace_back(m_dir_path / fs::PathFromString(m_file_path + "-journal"));
156+
return files;
157+
}
150158
std::string Format() override { return "sqlite"; }
151159

152160
/** Make a SQLiteBatch connected to this database */

src/wallet/test/util.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ class MockableDatabase : public WalletDatabase
109109
void Close() override {}
110110

111111
std::string Filename() override { return "mockable"; }
112+
std::vector<fs::path> Files() override { return {}; }
112113
std::string Format() override { return "mock"; }
113114
std::unique_ptr<DatabaseBatch> MakeBatch() override { return std::make_unique<MockableBatch>(m_records, m_pass); }
114115
};

src/wallet/wallet.cpp

Lines changed: 47 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3846,6 +3846,9 @@ util::Result<void> CWallet::ApplyMigrationData(WalletBatch& local_wallet_batch,
38463846
return util::Error{Untranslated(STR_INTERNAL_BUG("Error: Legacy wallet data missing"))};
38473847
}
38483848

3849+
// Note: when the legacy wallet has no spendable scripts, it must be empty at the end of the process.
3850+
bool has_spendable_material = !data.desc_spkms.empty() || data.master_key.key.IsValid();
3851+
38493852
// Get all invalid or non-watched scripts that will not be migrated
38503853
std::set<CTxDestination> not_migrated_dests;
38513854
for (const auto& script : legacy_spkm->GetNotMineScriptPubKeys()) {
@@ -3877,9 +3880,9 @@ util::Result<void> CWallet::ApplyMigrationData(WalletBatch& local_wallet_batch,
38773880
m_external_spk_managers.clear();
38783881
m_internal_spk_managers.clear();
38793882

3880-
// Setup new descriptors
3883+
// Setup new descriptors (only if we are migrating any key material)
38813884
SetWalletFlagWithDB(local_wallet_batch, WALLET_FLAG_DESCRIPTORS);
3882-
if (!IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) {
3885+
if (has_spendable_material && !IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) {
38833886
// Use the existing master key if we have it
38843887
if (data.master_key.key.IsValid()) {
38853888
SetupDescriptorScriptPubKeyMans(local_wallet_batch, data.master_key);
@@ -4033,6 +4036,14 @@ util::Result<void> CWallet::ApplyMigrationData(WalletBatch& local_wallet_batch,
40334036
}
40344037
}
40354038

4039+
// If there was no key material in the main wallet, there should be no records on it anymore.
4040+
// This wallet will be discarded at the end of the process. Only wallets that contain the
4041+
// migrated records will be presented to the user.
4042+
if (!has_spendable_material) {
4043+
if (!m_address_book.empty()) return util::Error{_("Error: Not all address book records were migrated")};
4044+
if (!mapWallet.empty()) return util::Error{_("Error: Not all transaction records were migrated")};
4045+
}
4046+
40364047
return {}; // all good
40374048
}
40384049

@@ -4270,6 +4281,14 @@ util::Result<MigrationResult> MigrateLegacyToDescriptor(std::shared_ptr<CWallet>
42704281
}
42714282
}
42724283

4284+
// Indicates whether the current wallet is empty after migration.
4285+
// Notes:
4286+
// When non-empty: the local wallet becomes the main spendable wallet.
4287+
// When empty: The local wallet is excluded from the result, as the
4288+
// user does not expect an empty spendable wallet after
4289+
// migrating only watch-only scripts.
4290+
bool empty_local_wallet = false;
4291+
42734292
{
42744293
LOCK(local_wallet->cs_wallet);
42754294
// First change to using SQLite
@@ -4278,6 +4297,8 @@ util::Result<MigrationResult> MigrateLegacyToDescriptor(std::shared_ptr<CWallet>
42784297
// Do the migration of keys and scripts for non-empty wallets, and cleanup if it fails
42794298
if (HasLegacyRecords(*local_wallet)) {
42804299
success = DoMigration(*local_wallet, context, error, res);
4300+
// No scripts mean empty wallet after migration
4301+
empty_local_wallet = local_wallet->GetAllScriptPubKeyMans().empty();
42814302
} else {
42824303
// Make sure that descriptors flag is actually set
42834304
local_wallet->SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
@@ -4291,21 +4312,31 @@ util::Result<MigrationResult> MigrateLegacyToDescriptor(std::shared_ptr<CWallet>
42914312
// fails to reload.
42924313
std::set<fs::path> wallet_dirs;
42934314
if (success) {
4315+
Assume(!res.wallet); // We will set it here.
4316+
// Check if the local wallet is empty after migration
4317+
if (empty_local_wallet) {
4318+
// This wallet has no records. We can safely remove it.
4319+
std::vector<fs::path> paths_to_remove = local_wallet->GetDatabase().Files();
4320+
local_wallet.reset();
4321+
for (const auto& path_to_remove : paths_to_remove) fs::remove_all(path_to_remove);
4322+
}
4323+
42944324
// Migration successful, unload all wallets locally, then reload them.
4295-
// Reload the main wallet
4296-
wallet_dirs.insert(fs::PathFromString(local_wallet->GetDatabase().Filename()).parent_path());
4297-
success = reload_wallet(local_wallet);
4298-
res.wallet = local_wallet;
4299-
res.wallet_name = wallet_name;
4300-
if (success && res.watchonly_wallet) {
4301-
// Reload watchonly
4302-
wallet_dirs.insert(fs::PathFromString(res.watchonly_wallet->GetDatabase().Filename()).parent_path());
4303-
success = reload_wallet(res.watchonly_wallet);
4304-
}
4305-
if (success && res.solvables_wallet) {
4306-
// Reload solvables
4307-
wallet_dirs.insert(fs::PathFromString(res.solvables_wallet->GetDatabase().Filename()).parent_path());
4308-
success = reload_wallet(res.solvables_wallet);
4325+
// Note: We use a pointer to the shared_ptr to avoid increasing its reference count,
4326+
// as 'reload_wallet' expects to be the sole owner (use_count == 1).
4327+
for (std::shared_ptr<CWallet>* wallet_ptr : {&local_wallet, &res.watchonly_wallet, &res.solvables_wallet}) {
4328+
if (success && *wallet_ptr) {
4329+
std::shared_ptr<CWallet>& wallet = *wallet_ptr;
4330+
// Save db path and reload wallet
4331+
wallet_dirs.insert(fs::PathFromString(wallet->GetDatabase().Filename()).parent_path());
4332+
success = reload_wallet(wallet);
4333+
4334+
// When no wallet is set, set the main wallet.
4335+
if (!res.wallet) {
4336+
res.wallet_name = wallet->GetName();
4337+
res.wallet = std::move(wallet);
4338+
}
4339+
}
43094340
}
43104341
}
43114342
if (!success) {

test/functional/wallet_migration.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# Distributed under the MIT software license, see the accompanying
44
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
55
"""Test Migrating a wallet from legacy to descriptor."""
6-
6+
import os.path
77
import random
88
import shutil
99
import struct
@@ -121,9 +121,14 @@ def migrate_and_get_rpc(self, wallet_name, **kwargs):
121121
# Migrate, checking that rescan does not occur
122122
with self.master_node.assert_debug_log(expected_msgs=[], unexpected_msgs=["Rescanning"]):
123123
migrate_info = self.master_node.migratewallet(wallet_name=wallet_name, **kwargs)
124+
# Update wallet name in case the initial wallet was completely migrated to a watch-only wallet
125+
# (in which case the wallet name would be suffixed by the 'watchonly' term)
126+
wallet_name = migrate_info['wallet_name']
124127
wallet = self.master_node.get_wallet_rpc(wallet_name)
125128
assert_equal(wallet.getwalletinfo()["descriptors"], True)
126129
self.assert_is_sqlite(wallet_name)
130+
# Always verify the backup path exist after migration
131+
assert os.path.exists(migrate_info['backup_path'])
127132
return migrate_info, wallet
128133

129134
def test_basic(self):
@@ -1024,8 +1029,15 @@ def test_migrate_simple_watch_only(self):
10241029
wallet.importaddress(address=p2pk_script.hex())
10251030
# Migrate wallet in the latest node
10261031
res, _ = self.migrate_and_get_rpc("bare_p2pk")
1027-
wo_wallet = self.master_node.get_wallet_rpc(res['watchonly_name'])
1032+
wo_wallet = self.master_node.get_wallet_rpc(res['wallet_name'])
10281033
assert_equal(wo_wallet.listdescriptors()['descriptors'][0]['desc'], descsum_create(f'pk({pubkey.hex()})'))
1034+
assert_equal(wo_wallet.getwalletinfo()["private_keys_enabled"], False)
1035+
1036+
# Ensure that migrating a wallet with watch-only scripts does not create a spendable wallet.
1037+
assert_equal('bare_p2pk_watchonly', res['wallet_name'])
1038+
assert "bare_p2pk" not in self.master_node.listwallets()
1039+
assert "bare_p2pk" not in [w["name"] for w in self.master_node.listwalletdir()["wallets"]]
1040+
10291041
wo_wallet.unloadwallet()
10301042

10311043
def test_manual_keys_import(self):
@@ -1055,7 +1067,9 @@ def test_manual_keys_import(self):
10551067
res, _ = self.migrate_and_get_rpc("import_pubkeys")
10561068

10571069
# Same as before, there should be descriptors in the watch-only wallet for the imported pubkey
1058-
wo_wallet = self.nodes[0].get_wallet_rpc(res['watchonly_name'])
1070+
wo_wallet = self.nodes[0].get_wallet_rpc(res['wallet_name'])
1071+
# Assert this is a watch-only wallet
1072+
assert_equal(wo_wallet.getwalletinfo()["private_keys_enabled"], False)
10591073
# As we imported the pubkey only, there will be no key origin in the following descriptors
10601074
pk_desc = descsum_create(f'pk({pubkey_hex})')
10611075
pkh_desc = descsum_create(f'pkh({pubkey_hex})')
@@ -1066,6 +1080,10 @@ def test_manual_keys_import(self):
10661080
# Verify all expected descriptors were migrated
10671081
migrated_desc = [item['desc'] for item in wo_wallet.listdescriptors()['descriptors']]
10681082
assert_equal(expected_descs, migrated_desc)
1083+
# Ensure that migrating a wallet with watch-only scripts does not create a spendable wallet.
1084+
assert_equal('import_pubkeys_watchonly', res['wallet_name'])
1085+
assert "import_pubkeys" not in self.master_node.listwallets()
1086+
assert "import_pubkeys" not in [w["name"] for w in self.master_node.listwalletdir()["wallets"]]
10691087
wo_wallet.unloadwallet()
10701088

10711089
def test_p2wsh(self):

0 commit comments

Comments
 (0)