Skip to content

Commit 63893d5

Browse files
committed
Merge bitcoin/bitcoin#26595: wallet: be able to specify a wallet name and passphrase to migratewallet
9486509 wallet, rpc: Update migratewallet help text for encrypted wallets (Andrew Chow) aaf02b5 tests: Tests for migrating wallets by name, and providing passphrase (Andrew Chow) 7fd125b wallet: Be able to unlock the wallet for migration (Andrew Chow) 6bdbc5f rpc: Allow users to specify wallet name for migratewallet (Andrew Chow) dbfa345 wallet: Allow MigrateLegacyToDescriptor to take a wallet name (Andrew Chow) Pull request description: `migratewallet` currently operates on wallets that are already loaded, however this is not necessarily required, and in the future, not possible once the legacy wallet is removed. So we need to also be able to give the wallet name to migrate. Additionally, the passphrase is required when migrating a wallet. Since a wallet may not be loaded when we migrate, and as we currently unload wallets when migrating, we need the passphrase to be given to `migratewallet` in order to migrate encrypted wallets. Fixes #27048 ACKs for top commit: john-moffett: reACK 9486509 pinheadmz: ACK 9486509 furszy: ACK 9486509 Tree-SHA512: 35e2ba69a148e129a41e20d7fb99c4cab7947b1b7e7c362f4fd06ff8ac6e79e476e07207e063ba5b80e1a33e2343f4b4f1d72d7930ce80c34571c130d2f5cff4
2 parents 8b4dc94 + 9486509 commit 63893d5

File tree

4 files changed

+127
-33
lines changed

4 files changed

+127
-33
lines changed

src/wallet/rpc/wallet.cpp

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -720,9 +720,12 @@ static RPCHelpMan migratewallet()
720720
"A new wallet backup will need to be made.\n"
721721
"\nThe migration process will create a backup of the wallet before migrating. This backup\n"
722722
"file will be named <wallet name>-<timestamp>.legacy.bak and can be found in the directory\n"
723-
"for this wallet. In the event of an incorrect migration, the backup can be restored using restorewallet." +
724-
HELP_REQUIRING_PASSPHRASE,
725-
{},
723+
"for this wallet. In the event of an incorrect migration, the backup can be restored using restorewallet."
724+
"\nEncrypted wallets must have the passphrase provided as an argument to this call.",
725+
{
726+
{"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."},
727+
{"passphrase", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "The wallet passphrase"},
728+
},
726729
RPCResult{
727730
RPCResult::Type::OBJ, "", "",
728731
{
@@ -738,16 +741,26 @@ static RPCHelpMan migratewallet()
738741
},
739742
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
740743
{
741-
std::shared_ptr<CWallet> wallet = GetWalletForJSONRPCRequest(request);
742-
if (!wallet) return NullUniValue;
744+
std::string wallet_name;
745+
if (GetWalletNameFromJSONRPCRequest(request, wallet_name)) {
746+
if (!(request.params[0].isNull() || request.params[0].get_str() == wallet_name)) {
747+
throw JSONRPCError(RPC_INVALID_PARAMETER, "RPC endpoint wallet and wallet_name parameter specify different wallets");
748+
}
749+
} else {
750+
if (request.params[0].isNull()) {
751+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Either RPC endpoint wallet or wallet_name parameter must be provided");
752+
}
753+
wallet_name = request.params[0].get_str();
754+
}
743755

744-
if (wallet->IsCrypted()) {
745-
throw JSONRPCError(RPC_WALLET_WRONG_ENC_STATE, "Error: migratewallet on encrypted wallets is currently unsupported.");
756+
SecureString wallet_pass;
757+
wallet_pass.reserve(100);
758+
if (!request.params[1].isNull()) {
759+
wallet_pass = std::string_view{request.params[1].get_str()};
746760
}
747761

748762
WalletContext& context = EnsureWalletContext(request.context);
749-
750-
util::Result<MigrationResult> res = MigrateLegacyToDescriptor(std::move(wallet), context);
763+
util::Result<MigrationResult> res = MigrateLegacyToDescriptor(wallet_name, wallet_pass, context);
751764
if (!res) {
752765
throw JSONRPCError(RPC_WALLET_ERROR, util::ErrorString(res).original);
753766
}

src/wallet/wallet.cpp

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3884,7 +3884,7 @@ std::optional<MigrationData> CWallet::GetDescriptorsForLegacy(bilingual_str& err
38843884

38853885
std::optional<MigrationData> res = legacy_spkm->MigrateToDescriptor();
38863886
if (res == std::nullopt) {
3887-
error = _("Error: Unable to produce descriptors for this legacy wallet. Make sure the wallet is unlocked first");
3887+
error = _("Error: Unable to produce descriptors for this legacy wallet. Make sure to provide the wallet's passphrase if it is encrypted.");
38883888
return std::nullopt;
38893889
}
38903890
return res;
@@ -4174,32 +4174,19 @@ bool DoMigration(CWallet& wallet, WalletContext& context, bilingual_str& error,
41744174
return true;
41754175
}
41764176

4177-
util::Result<MigrationResult> MigrateLegacyToDescriptor(std::shared_ptr<CWallet>&& wallet, WalletContext& context)
4177+
util::Result<MigrationResult> MigrateLegacyToDescriptor(const std::string& wallet_name, const SecureString& passphrase, WalletContext& context)
41784178
{
4179-
// Before anything else, check if there is something to migrate.
4180-
if (!wallet->GetLegacyScriptPubKeyMan()) {
4181-
return util::Error{_("Error: This wallet is already a descriptor wallet")};
4182-
}
4183-
41844179
MigrationResult res;
41854180
bilingual_str error;
41864181
std::vector<bilingual_str> warnings;
41874182

4188-
// Make a backup of the DB
4189-
std::string wallet_name = wallet->GetName();
4190-
fs::path this_wallet_dir = fs::absolute(fs::PathFromString(wallet->GetDatabase().Filename())).parent_path();
4191-
fs::path backup_filename = fs::PathFromString(strprintf("%s-%d.legacy.bak", wallet_name, GetTime()));
4192-
fs::path backup_path = this_wallet_dir / backup_filename;
4193-
if (!wallet->BackupWallet(fs::PathToString(backup_path))) {
4194-
return util::Error{_("Error: Unable to make a backup of your wallet")};
4195-
}
4196-
res.backup_path = backup_path;
4197-
4198-
// Unload the wallet so that nothing else tries to use it while we're changing it
4199-
if (!RemoveWallet(context, wallet, /*load_on_start=*/std::nullopt, warnings)) {
4200-
return util::Error{_("Unable to unload the wallet before migrating")};
4183+
// If the wallet is still loaded, unload it so that nothing else tries to use it while we're changing it
4184+
if (auto wallet = GetWallet(context, wallet_name)) {
4185+
if (!RemoveWallet(context, wallet, /*load_on_start=*/std::nullopt, warnings)) {
4186+
return util::Error{_("Unable to unload the wallet before migrating")};
4187+
}
4188+
UnloadWallet(std::move(wallet));
42014189
}
4202-
UnloadWallet(std::move(wallet));
42034190

42044191
// Load the wallet but only in the context of this function.
42054192
// No signals should be connected nor should anything else be aware of this wallet
@@ -4213,15 +4200,43 @@ util::Result<MigrationResult> MigrateLegacyToDescriptor(std::shared_ptr<CWallet>
42134200
return util::Error{Untranslated("Wallet file verification failed.") + Untranslated(" ") + error};
42144201
}
42154202

4203+
// Make the local wallet
42164204
std::shared_ptr<CWallet> local_wallet = CWallet::Create(empty_context, wallet_name, std::move(database), options.create_flags, error, warnings);
42174205
if (!local_wallet) {
42184206
return util::Error{Untranslated("Wallet loading failed.") + Untranslated(" ") + error};
42194207
}
42204208

4209+
// Before anything else, check if there is something to migrate.
4210+
if (!local_wallet->GetLegacyScriptPubKeyMan()) {
4211+
return util::Error{_("Error: This wallet is already a descriptor wallet")};
4212+
}
4213+
4214+
// Make a backup of the DB
4215+
fs::path this_wallet_dir = fs::absolute(fs::PathFromString(local_wallet->GetDatabase().Filename())).parent_path();
4216+
fs::path backup_filename = fs::PathFromString(strprintf("%s-%d.legacy.bak", wallet_name, GetTime()));
4217+
fs::path backup_path = this_wallet_dir / backup_filename;
4218+
if (!local_wallet->BackupWallet(fs::PathToString(backup_path))) {
4219+
return util::Error{_("Error: Unable to make a backup of your wallet")};
4220+
}
4221+
res.backup_path = backup_path;
4222+
42214223
bool success = false;
42224224
{
42234225
LOCK(local_wallet->cs_wallet);
42244226

4227+
// Unlock the wallet if needed
4228+
if (local_wallet->IsLocked() && !local_wallet->Unlock(passphrase)) {
4229+
if (passphrase.find('\0') == std::string::npos) {
4230+
return util::Error{Untranslated("Error: Wallet decryption failed, the wallet passphrase was not provided or was incorrect.")};
4231+
} else {
4232+
return util::Error{Untranslated("Error: Wallet decryption failed, the wallet passphrase entered was incorrect. "
4233+
"The passphrase contains a null character (ie - a zero byte). "
4234+
"If this passphrase was set with a version of this software prior to 25.0, "
4235+
"please try again with only the characters up to — but not including — "
4236+
"the first null character.")};
4237+
}
4238+
}
4239+
42254240
// First change to using SQLite
42264241
if (!local_wallet->MigrateToSQLite(error)) return util::Error{error};
42274242

src/wallet/wallet.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1016,7 +1016,7 @@ struct MigrationResult {
10161016
};
10171017

10181018
//! Do all steps to migrate a legacy wallet to a descriptor wallet
1019-
util::Result<MigrationResult> MigrateLegacyToDescriptor(std::shared_ptr<CWallet>&& wallet, WalletContext& context);
1019+
util::Result<MigrationResult> MigrateLegacyToDescriptor(const std::string& wallet_name, const SecureString& passphrase, WalletContext& context);
10201020
} // namespace wallet
10211021

10221022
#endif // BITCOIN_WALLET_WALLET_H

test/functional/wallet_migration.py

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -400,11 +400,75 @@ def test_pk_coinbases(self):
400400
def test_encrypted(self):
401401
self.log.info("Test migration of an encrypted wallet")
402402
wallet = self.create_legacy_wallet("encrypted")
403+
default = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
403404

404405
wallet.encryptwallet("pass")
406+
addr = wallet.getnewaddress()
407+
txid = default.sendtoaddress(addr, 1)
408+
self.generate(self.nodes[0], 1)
409+
bals = wallet.getbalances()
410+
411+
assert_raises_rpc_error(-4, "Error: Wallet decryption failed, the wallet passphrase was not provided or was incorrect", wallet.migratewallet)
412+
assert_raises_rpc_error(-4, "Error: Wallet decryption failed, the wallet passphrase was not provided or was incorrect", wallet.migratewallet, None, "badpass")
413+
assert_raises_rpc_error(-4, "The passphrase contains a null character", wallet.migratewallet, None, "pass\0with\0null")
414+
415+
wallet.migratewallet(passphrase="pass")
416+
417+
info = wallet.getwalletinfo()
418+
assert_equal(info["descriptors"], True)
419+
assert_equal(info["format"], "sqlite")
420+
assert_equal(info["unlocked_until"], 0)
421+
wallet.gettransaction(txid)
422+
423+
assert_equal(bals, wallet.getbalances())
424+
425+
def test_unloaded(self):
426+
self.log.info("Test migration of a wallet that isn't loaded")
427+
wallet = self.create_legacy_wallet("notloaded")
428+
default = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
429+
430+
addr = wallet.getnewaddress()
431+
txid = default.sendtoaddress(addr, 1)
432+
self.generate(self.nodes[0], 1)
433+
bals = wallet.getbalances()
434+
435+
wallet.unloadwallet()
436+
437+
assert_raises_rpc_error(-8, "RPC endpoint wallet and wallet_name parameter specify different wallets", wallet.migratewallet, "someotherwallet")
438+
assert_raises_rpc_error(-8, "Either RPC endpoint wallet or wallet_name parameter must be provided", self.nodes[0].migratewallet)
439+
self.nodes[0].migratewallet("notloaded")
405440

406-
assert_raises_rpc_error(-15, "Error: migratewallet on encrypted wallets is currently unsupported.", wallet.migratewallet)
407-
# TODO: Fix migratewallet so that we can actually migrate encrypted wallets
441+
info = wallet.getwalletinfo()
442+
assert_equal(info["descriptors"], True)
443+
assert_equal(info["format"], "sqlite")
444+
wallet.gettransaction(txid)
445+
446+
assert_equal(bals, wallet.getbalances())
447+
448+
def test_unloaded_by_path(self):
449+
self.log.info("Test migration of a wallet that isn't loaded, specified by path")
450+
wallet = self.create_legacy_wallet("notloaded2")
451+
default = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
452+
453+
addr = wallet.getnewaddress()
454+
txid = default.sendtoaddress(addr, 1)
455+
self.generate(self.nodes[0], 1)
456+
bals = wallet.getbalances()
457+
458+
wallet.unloadwallet()
459+
460+
wallet_file_path = os.path.join(self.nodes[0].datadir, "regtest", "wallets", "notloaded2")
461+
self.nodes[0].migratewallet(wallet_file_path)
462+
463+
# Because we gave the name by full path, the loaded wallet's name is that path too.
464+
wallet = self.nodes[0].get_wallet_rpc(wallet_file_path)
465+
466+
info = wallet.getwalletinfo()
467+
assert_equal(info["descriptors"], True)
468+
assert_equal(info["format"], "sqlite")
469+
wallet.gettransaction(txid)
470+
471+
assert_equal(bals, wallet.getbalances())
408472

409473
def run_test(self):
410474
self.generate(self.nodes[0], 101)
@@ -416,6 +480,8 @@ def run_test(self):
416480
self.test_no_privkeys()
417481
self.test_pk_coinbases()
418482
self.test_encrypted()
483+
self.test_unloaded()
484+
self.test_unloaded_by_path()
419485

420486
if __name__ == '__main__':
421487
WalletMigrationTest().main()

0 commit comments

Comments
 (0)