diff --git a/src/wallet/rpc/wallet.cpp b/src/wallet/rpc/wallet.cpp index 23a88cd51b1..3162cce9873 100644 --- a/src/wallet/rpc/wallet.cpp +++ b/src/wallet/rpc/wallet.cpp @@ -720,9 +720,12 @@ static RPCHelpMan migratewallet() "A new wallet backup will need to be made.\n" "\nThe migration process will create a backup of the wallet before migrating. This backup\n" "file will be named -.legacy.bak and can be found in the directory\n" - "for this wallet. In the event of an incorrect migration, the backup can be restored using restorewallet." + - HELP_REQUIRING_PASSPHRASE, - {}, + "for this wallet. In the event of an incorrect migration, the backup can be restored using restorewallet." + "\nEncrypted wallets must have the passphrase provided as an argument to this call.", + { + {"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."}, + {"passphrase", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "The wallet passphrase"}, + }, RPCResult{ RPCResult::Type::OBJ, "", "", { @@ -738,16 +741,26 @@ static RPCHelpMan migratewallet() }, [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue { - std::shared_ptr wallet = GetWalletForJSONRPCRequest(request); - if (!wallet) return NullUniValue; + std::string wallet_name; + if (GetWalletNameFromJSONRPCRequest(request, wallet_name)) { + if (!(request.params[0].isNull() || request.params[0].get_str() == wallet_name)) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "RPC endpoint wallet and wallet_name parameter specify different wallets"); + } + } else { + if (request.params[0].isNull()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Either RPC endpoint wallet or wallet_name parameter must be provided"); + } + wallet_name = request.params[0].get_str(); + } - if (wallet->IsCrypted()) { - throw JSONRPCError(RPC_WALLET_WRONG_ENC_STATE, "Error: migratewallet on encrypted wallets is currently unsupported."); + SecureString wallet_pass; + wallet_pass.reserve(100); + if (!request.params[1].isNull()) { + wallet_pass = std::string_view{request.params[1].get_str()}; } WalletContext& context = EnsureWalletContext(request.context); - - util::Result res = MigrateLegacyToDescriptor(std::move(wallet), context); + util::Result res = MigrateLegacyToDescriptor(wallet_name, wallet_pass, context); if (!res) { throw JSONRPCError(RPC_WALLET_ERROR, util::ErrorString(res).original); } diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index f2c0fdcb3dc..daddd6446d2 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -3884,7 +3884,7 @@ std::optional CWallet::GetDescriptorsForLegacy(bilingual_str& err std::optional res = legacy_spkm->MigrateToDescriptor(); if (res == std::nullopt) { - error = _("Error: Unable to produce descriptors for this legacy wallet. Make sure the wallet is unlocked first"); + error = _("Error: Unable to produce descriptors for this legacy wallet. Make sure to provide the wallet's passphrase if it is encrypted."); return std::nullopt; } return res; @@ -4174,32 +4174,19 @@ bool DoMigration(CWallet& wallet, WalletContext& context, bilingual_str& error, return true; } -util::Result MigrateLegacyToDescriptor(std::shared_ptr&& wallet, WalletContext& context) +util::Result MigrateLegacyToDescriptor(const std::string& wallet_name, const SecureString& passphrase, WalletContext& context) { - // Before anything else, check if there is something to migrate. - if (!wallet->GetLegacyScriptPubKeyMan()) { - return util::Error{_("Error: This wallet is already a descriptor wallet")}; - } - MigrationResult res; bilingual_str error; std::vector warnings; - // Make a backup of the DB - std::string wallet_name = wallet->GetName(); - fs::path this_wallet_dir = fs::absolute(fs::PathFromString(wallet->GetDatabase().Filename())).parent_path(); - fs::path backup_filename = fs::PathFromString(strprintf("%s-%d.legacy.bak", wallet_name, GetTime())); - fs::path backup_path = this_wallet_dir / backup_filename; - if (!wallet->BackupWallet(fs::PathToString(backup_path))) { - return util::Error{_("Error: Unable to make a backup of your wallet")}; + // If the wallet is still loaded, unload it so that nothing else tries to use it while we're changing it + if (auto wallet = GetWallet(context, wallet_name)) { + if (!RemoveWallet(context, wallet, /*load_on_start=*/std::nullopt, warnings)) { + return util::Error{_("Unable to unload the wallet before migrating")}; + } + UnloadWallet(std::move(wallet)); } - res.backup_path = backup_path; - - // Unload the wallet so that nothing else tries to use it while we're changing it - if (!RemoveWallet(context, wallet, /*load_on_start=*/std::nullopt, warnings)) { - return util::Error{_("Unable to unload the wallet before migrating")}; - } - UnloadWallet(std::move(wallet)); // Load the wallet but only in the context of this function. // No signals should be connected nor should anything else be aware of this wallet @@ -4213,15 +4200,43 @@ util::Result MigrateLegacyToDescriptor(std::shared_ptr return util::Error{Untranslated("Wallet file verification failed.") + Untranslated(" ") + error}; } + // Make the local wallet std::shared_ptr local_wallet = CWallet::Create(empty_context, wallet_name, std::move(database), options.create_flags, error, warnings); if (!local_wallet) { return util::Error{Untranslated("Wallet loading failed.") + Untranslated(" ") + error}; } + // Before anything else, check if there is something to migrate. + if (!local_wallet->GetLegacyScriptPubKeyMan()) { + return util::Error{_("Error: This wallet is already a descriptor wallet")}; + } + + // Make a backup of the DB + fs::path this_wallet_dir = fs::absolute(fs::PathFromString(local_wallet->GetDatabase().Filename())).parent_path(); + fs::path backup_filename = fs::PathFromString(strprintf("%s-%d.legacy.bak", wallet_name, GetTime())); + fs::path backup_path = this_wallet_dir / backup_filename; + if (!local_wallet->BackupWallet(fs::PathToString(backup_path))) { + return util::Error{_("Error: Unable to make a backup of your wallet")}; + } + res.backup_path = backup_path; + bool success = false; { LOCK(local_wallet->cs_wallet); + // Unlock the wallet if needed + if (local_wallet->IsLocked() && !local_wallet->Unlock(passphrase)) { + if (passphrase.find('\0') == std::string::npos) { + return util::Error{Untranslated("Error: Wallet decryption failed, the wallet passphrase was not provided or was incorrect.")}; + } else { + return util::Error{Untranslated("Error: Wallet decryption failed, the wallet passphrase entered was incorrect. " + "The passphrase contains a null character (ie - a zero byte). " + "If this passphrase was set with a version of this software prior to 25.0, " + "please try again with only the characters up to — but not including — " + "the first null character.")}; + } + } + // First change to using SQLite if (!local_wallet->MigrateToSQLite(error)) return util::Error{error}; diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index 5dd694e1148..32cb3e3f59b 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -1016,7 +1016,7 @@ struct MigrationResult { }; //! Do all steps to migrate a legacy wallet to a descriptor wallet -util::Result MigrateLegacyToDescriptor(std::shared_ptr&& wallet, WalletContext& context); +util::Result MigrateLegacyToDescriptor(const std::string& wallet_name, const SecureString& passphrase, WalletContext& context); } // namespace wallet #endif // BITCOIN_WALLET_WALLET_H diff --git a/test/functional/wallet_migration.py b/test/functional/wallet_migration.py index 72c5fe7b840..7c2959bb895 100755 --- a/test/functional/wallet_migration.py +++ b/test/functional/wallet_migration.py @@ -400,11 +400,75 @@ class WalletMigrationTest(BitcoinTestFramework): def test_encrypted(self): self.log.info("Test migration of an encrypted wallet") wallet = self.create_legacy_wallet("encrypted") + default = self.nodes[0].get_wallet_rpc(self.default_wallet_name) wallet.encryptwallet("pass") + addr = wallet.getnewaddress() + txid = default.sendtoaddress(addr, 1) + self.generate(self.nodes[0], 1) + bals = wallet.getbalances() - assert_raises_rpc_error(-15, "Error: migratewallet on encrypted wallets is currently unsupported.", wallet.migratewallet) - # TODO: Fix migratewallet so that we can actually migrate encrypted wallets + assert_raises_rpc_error(-4, "Error: Wallet decryption failed, the wallet passphrase was not provided or was incorrect", wallet.migratewallet) + assert_raises_rpc_error(-4, "Error: Wallet decryption failed, the wallet passphrase was not provided or was incorrect", wallet.migratewallet, None, "badpass") + assert_raises_rpc_error(-4, "The passphrase contains a null character", wallet.migratewallet, None, "pass\0with\0null") + + wallet.migratewallet(passphrase="pass") + + info = wallet.getwalletinfo() + assert_equal(info["descriptors"], True) + assert_equal(info["format"], "sqlite") + assert_equal(info["unlocked_until"], 0) + wallet.gettransaction(txid) + + assert_equal(bals, wallet.getbalances()) + + def test_unloaded(self): + self.log.info("Test migration of a wallet that isn't loaded") + wallet = self.create_legacy_wallet("notloaded") + default = self.nodes[0].get_wallet_rpc(self.default_wallet_name) + + addr = wallet.getnewaddress() + txid = default.sendtoaddress(addr, 1) + self.generate(self.nodes[0], 1) + bals = wallet.getbalances() + + wallet.unloadwallet() + + assert_raises_rpc_error(-8, "RPC endpoint wallet and wallet_name parameter specify different wallets", wallet.migratewallet, "someotherwallet") + assert_raises_rpc_error(-8, "Either RPC endpoint wallet or wallet_name parameter must be provided", self.nodes[0].migratewallet) + self.nodes[0].migratewallet("notloaded") + + info = wallet.getwalletinfo() + assert_equal(info["descriptors"], True) + assert_equal(info["format"], "sqlite") + wallet.gettransaction(txid) + + assert_equal(bals, wallet.getbalances()) + + def test_unloaded_by_path(self): + self.log.info("Test migration of a wallet that isn't loaded, specified by path") + wallet = self.create_legacy_wallet("notloaded2") + default = self.nodes[0].get_wallet_rpc(self.default_wallet_name) + + addr = wallet.getnewaddress() + txid = default.sendtoaddress(addr, 1) + self.generate(self.nodes[0], 1) + bals = wallet.getbalances() + + wallet.unloadwallet() + + wallet_file_path = os.path.join(self.nodes[0].datadir, "regtest", "wallets", "notloaded2") + self.nodes[0].migratewallet(wallet_file_path) + + # Because we gave the name by full path, the loaded wallet's name is that path too. + wallet = self.nodes[0].get_wallet_rpc(wallet_file_path) + + info = wallet.getwalletinfo() + assert_equal(info["descriptors"], True) + assert_equal(info["format"], "sqlite") + wallet.gettransaction(txid) + + assert_equal(bals, wallet.getbalances()) def run_test(self): self.generate(self.nodes[0], 101) @@ -416,6 +480,8 @@ class WalletMigrationTest(BitcoinTestFramework): self.test_no_privkeys() self.test_pk_coinbases() self.test_encrypted() + self.test_unloaded() + self.test_unloaded_by_path() if __name__ == '__main__': WalletMigrationTest().main()