Skip to content

Commit 0a77441

Browse files
committed
Merge bitcoin/bitcoin#31451: wallet: migration, avoid loading legacy wallet after failure when BDB isn't compiled
589ed1a wallet: migration, avoid loading wallet after failure when it wasn't loaded before (furszy) Pull request description: Fixes #31447. During migration failure, only load wallet back into memory when the wallet was loaded prior to migration. This fixes the case where BDB is not supported, which implies that no legacy wallet can be loaded into memory due to the lack of db writing functionality. Link to error description bitcoin/bitcoin#31447 (comment). This PR also improves migration backup related comments to better document the current workflow. ACKs for top commit: achow101: ACK 589ed1a rkrux: ACK 589ed1a pablomartin4btc: tACK 589ed1a Tree-SHA512: c7a489d2b253c574ee0287b691ebe29fe8d026f659f68a3f6108eca8b4e1e420c67ca7803c6bd70c1e1440791833fabca3afbcf8fe8524c6c9fc08de95b618d0
2 parents 56725f8 + 589ed1a commit 0a77441

File tree

3 files changed

+40
-13
lines changed

3 files changed

+40
-13
lines changed

src/wallet/wallet.cpp

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -490,7 +490,9 @@ std::shared_ptr<CWallet> CreateWallet(WalletContext& context, const std::string&
490490
return wallet;
491491
}
492492

493-
std::shared_ptr<CWallet> RestoreWallet(WalletContext& context, const fs::path& backup_file, const std::string& wallet_name, std::optional<bool> load_on_start, DatabaseStatus& status, bilingual_str& error, std::vector<bilingual_str>& warnings)
493+
// Re-creates wallet from the backup file by renaming and moving it into the wallet's directory.
494+
// If 'load_after_restore=true', the wallet object will be fully initialized and appended to the context.
495+
std::shared_ptr<CWallet> RestoreWallet(WalletContext& context, const fs::path& backup_file, const std::string& wallet_name, std::optional<bool> load_on_start, DatabaseStatus& status, bilingual_str& error, std::vector<bilingual_str>& warnings, bool load_after_restore)
494496
{
495497
DatabaseOptions options;
496498
ReadDatabaseArgs(*context.args, options);
@@ -515,13 +517,17 @@ std::shared_ptr<CWallet> RestoreWallet(WalletContext& context, const fs::path& b
515517

516518
fs::copy_file(backup_file, wallet_file, fs::copy_options::none);
517519

518-
wallet = LoadWallet(context, wallet_name, load_on_start, options, status, error, warnings);
520+
if (load_after_restore) {
521+
wallet = LoadWallet(context, wallet_name, load_on_start, options, status, error, warnings);
522+
}
519523
} catch (const std::exception& e) {
520524
assert(!wallet);
521525
if (!error.empty()) error += Untranslated("\n");
522526
error += Untranslated(strprintf("Unexpected exception: %s", e.what()));
523527
}
524-
if (!wallet) {
528+
529+
// Remove created wallet path only when loading fails
530+
if (load_after_restore && !wallet) {
525531
fs::remove_all(wallet_path);
526532
}
527533

@@ -4527,7 +4533,7 @@ util::Result<MigrationResult> MigrateLegacyToDescriptor(const std::string& walle
45274533
}
45284534
if (!success) {
45294535
// Migration failed, cleanup
4530-
// Copy the backup to the actual wallet dir
4536+
// Before deleting the wallet's directory, copy the backup file to the top-level wallets dir
45314537
fs::path temp_backup_location = fsbridge::AbsPathJoin(GetWalletDir(), backup_filename);
45324538
fs::copy_file(backup_path, temp_backup_location, fs::copy_options::none);
45334539

@@ -4564,17 +4570,24 @@ util::Result<MigrationResult> MigrateLegacyToDescriptor(const std::string& walle
45644570
}
45654571

45664572
// Restore the backup
4567-
DatabaseStatus status;
4568-
std::vector<bilingual_str> warnings;
4569-
if (!RestoreWallet(context, temp_backup_location, wallet_name, /*load_on_start=*/std::nullopt, status, error, warnings)) {
4570-
error += _("\nUnable to restore backup of wallet.");
4573+
// Convert the backup file to the wallet db file by renaming it and moving it into the wallet's directory.
4574+
// Reload it into memory if the wallet was previously loaded.
4575+
bilingual_str restore_error;
4576+
const auto& ptr_wallet = RestoreWallet(context, temp_backup_location, wallet_name, /*load_on_start=*/std::nullopt, status, restore_error, warnings, /*load_after_restore=*/was_loaded);
4577+
if (!restore_error.empty()) {
4578+
error += restore_error + _("\nUnable to restore backup of wallet.");
45714579
return util::Error{error};
45724580
}
45734581

4574-
// Move the backup to the wallet dir
4582+
// The wallet directory has been restored, but just in case, copy the previously created backup to the wallet dir
45754583
fs::copy_file(temp_backup_location, backup_path, fs::copy_options::none);
45764584
fs::remove(temp_backup_location);
45774585

4586+
// Verify that there is no dangling wallet: when the wallet wasn't loaded before, expect null.
4587+
// This check is performed after restoration to avoid an early error before saving the backup.
4588+
bool wallet_reloaded = ptr_wallet != nullptr;
4589+
assert(was_loaded == wallet_reloaded);
4590+
45784591
return util::Error{error};
45794592
}
45804593
return res;

src/wallet/wallet.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ std::shared_ptr<CWallet> GetDefaultWallet(WalletContext& context, size_t& count)
9595
std::shared_ptr<CWallet> GetWallet(WalletContext& context, const std::string& name);
9696
std::shared_ptr<CWallet> LoadWallet(WalletContext& context, const std::string& name, std::optional<bool> load_on_start, const DatabaseOptions& options, DatabaseStatus& status, bilingual_str& error, std::vector<bilingual_str>& warnings);
9797
std::shared_ptr<CWallet> CreateWallet(WalletContext& context, const std::string& name, std::optional<bool> load_on_start, DatabaseOptions& options, DatabaseStatus& status, bilingual_str& error, std::vector<bilingual_str>& warnings);
98-
std::shared_ptr<CWallet> RestoreWallet(WalletContext& context, const fs::path& backup_file, const std::string& wallet_name, std::optional<bool> load_on_start, DatabaseStatus& status, bilingual_str& error, std::vector<bilingual_str>& warnings);
98+
std::shared_ptr<CWallet> RestoreWallet(WalletContext& context, const fs::path& backup_file, const std::string& wallet_name, std::optional<bool> load_on_start, DatabaseStatus& status, bilingual_str& error, std::vector<bilingual_str>& warnings, bool load_after_restore = true);
9999
std::unique_ptr<interfaces::Handler> HandleLoadWallet(WalletContext& context, LoadWalletFn load_wallet);
100100
void NotifyWalletLoaded(WalletContext& context, const std::shared_ptr<CWallet>& wallet);
101101
std::unique_ptr<WalletDatabase> MakeWalletDatabase(const std::string& name, const DatabaseOptions& options, DatabaseStatus& status, bilingual_str& error);

test/functional/wallet_migration.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -896,9 +896,7 @@ def test_failed_migration_cleanup(self):
896896
shutil.copytree(self.old_node.wallets_path / "failed", self.master_node.wallets_path / "failed")
897897
assert_raises_rpc_error(-4, "Failed to create database", self.master_node.migratewallet, "failed")
898898

899-
assert "failed" in self.master_node.listwallets()
900-
assert "failed_watchonly" not in self.master_node.listwallets()
901-
assert "failed_solvables" not in self.master_node.listwallets()
899+
assert all(wallet not in self.master_node.listwallets() for wallet in ["failed", "failed_watchonly", "failed_solvables"])
902900

903901
assert not (self.master_node.wallets_path / "failed_watchonly").exists()
904902
# Since the file in failed_solvables is one that we put there, migration shouldn't touch it
@@ -912,6 +910,22 @@ def test_failed_migration_cleanup(self):
912910
_, _, magic = struct.unpack("QII", data)
913911
assert_equal(magic, BTREE_MAGIC)
914912

913+
####################################################
914+
# Perform the same test with a loaded legacy wallet.
915+
# The wallet should remain loaded after the failure.
916+
#
917+
# This applies only when BDB is enabled, as the user
918+
# cannot interact with the legacy wallet database
919+
# without BDB support.
920+
if self.is_bdb_compiled() is not None:
921+
# Advance time to generate a different backup name
922+
self.master_node.setmocktime(self.master_node.getblockheader(self.master_node.getbestblockhash())['time'] + 100)
923+
assert "failed" not in self.master_node.listwallets()
924+
self.master_node.loadwallet("failed")
925+
assert_raises_rpc_error(-4, "Failed to create database", self.master_node.migratewallet, "failed")
926+
wallets = self.master_node.listwallets()
927+
assert "failed" in wallets and all(wallet not in wallets for wallet in ["failed_watchonly", "failed_solvables"])
928+
915929
def test_blank(self):
916930
self.log.info("Test that a blank wallet is migrated")
917931
wallet = self.create_legacy_wallet("blank", blank=True)

0 commit comments

Comments
 (0)