aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSamuel Dobson <dobsonsa68@gmail.com>2021-08-15 16:46:51 +1200
committerSamuel Dobson <dobsonsa68@gmail.com>2021-08-15 16:58:05 +1200
commit502d22ceed1f90ed41336260f8eb428d3acaf514 (patch)
tree962792ed52293c77b5c8da903401abe5bd658a45
parentbe499aa07ff10957debc9b10a93d0781acec2e43 (diff)
parent5fe8100ff36fed6d50c2a25b028f57b25af3504c (diff)
downloadbitcoin-502d22ceed1f90ed41336260f8eb428d3acaf514.tar.xz
Merge bitcoin/bitcoin#22541: Add a new RPC command: restorewallet
5fe8100ff36fed6d50c2a25b028f57b25af3504c Change the wallet_backup.py test to use the restorewallet RPC command instead of restoring wallets manually. (lsilva01) ae23faba6fc5cabc896f1175456d1018576f912d Add a new RPC command: restorewallet (lsilva01) Pull request description: As far as I know, there is no command to restore the wallet from a backup file. The only way to do this is to replace the `wallet.dat` of a newly created wallet with the backup file, which is hardly an intuitive way. This PR implements the `restorewallet` RPC command which restores the wallet from the backup file. To test: First create a backup file: `$ bitcoin-cli -rpcwallet="wallet-01" backupwallet /home/Backups/wallet-01.bak` Then restore it in another wallet: `$ bitcoin-cli restorewallet "restored-wallet-01" /home/Backups/wallet-01.bak` ACKs for top commit: achow101: re-ACK 5fe8100ff36fed6d50c2a25b028f57b25af3504c prayank23: tACK https://github.com/bitcoin/bitcoin/commit/5fe8100ff36fed6d50c2a25b028f57b25af3504c meshcollider: utACK 5fe8100ff36fed6d50c2a25b028f57b25af3504c Tree-SHA512: 9639df4d8ad32f255f5b868320dc69878bd9aceb3b471b49dfad500b67681e2d354292b5410982fbf18e25a44ed0c06fd4a0dd010e82807c2e00ff32e84047a1
-rw-r--r--src/rpc/client.cpp1
-rw-r--r--src/wallet/rpcwallet.cpp119
-rwxr-xr-xtest/functional/wallet_backup.py45
3 files changed, 125 insertions, 40 deletions
diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp
index 9b5d181c4e..4357ab2bb3 100644
--- a/src/rpc/client.cpp
+++ b/src/rpc/client.cpp
@@ -187,6 +187,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
{ "createwallet", 5, "descriptors"},
{ "createwallet", 6, "load_on_startup"},
{ "createwallet", 7, "external_signer"},
+ { "restorewallet", 2, "load_on_startup"},
{ "loadwallet", 1, "load_on_startup"},
{ "unloadwallet", 1, "load_on_startup"},
{ "getnodeaddresses", 0, "count"},
diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp
index 2e2300f887..2a5b547858 100644
--- a/src/wallet/rpcwallet.cpp
+++ b/src/wallet/rpcwallet.cpp
@@ -2572,6 +2572,37 @@ static RPCHelpMan listwallets()
};
}
+static std::tuple<std::shared_ptr<CWallet>, std::vector<bilingual_str>> LoadWalletHelper(WalletContext& context, UniValue load_on_start_param, const std::string wallet_name)
+{
+ DatabaseOptions options;
+ DatabaseStatus status;
+ options.require_existing = true;
+ bilingual_str error;
+ std::vector<bilingual_str> warnings;
+ std::optional<bool> load_on_start = load_on_start_param.isNull() ? std::nullopt : std::optional<bool>(load_on_start_param.get_bool());
+ std::shared_ptr<CWallet> const wallet = LoadWallet(*context.chain, wallet_name, load_on_start, options, status, error, warnings);
+
+ if (!wallet) {
+ // Map bad format to not found, since bad format is returned when the
+ // wallet directory exists, but doesn't contain a data file.
+ RPCErrorCode code = RPC_WALLET_ERROR;
+ switch (status) {
+ case DatabaseStatus::FAILED_NOT_FOUND:
+ case DatabaseStatus::FAILED_BAD_FORMAT:
+ code = RPC_WALLET_NOT_FOUND;
+ break;
+ case DatabaseStatus::FAILED_ALREADY_LOADED:
+ code = RPC_WALLET_ALREADY_LOADED;
+ break;
+ default: // RPC_WALLET_ERROR is returned for all other cases.
+ break;
+ }
+ throw JSONRPCError(code, error.original);
+ }
+
+ return { wallet, warnings };
+}
+
static RPCHelpMan loadwallet()
{
return RPCHelpMan{"loadwallet",
@@ -2598,30 +2629,7 @@ static RPCHelpMan loadwallet()
WalletContext& context = EnsureWalletContext(request.context);
const std::string name(request.params[0].get_str());
- DatabaseOptions options;
- DatabaseStatus status;
- options.require_existing = true;
- bilingual_str error;
- std::vector<bilingual_str> warnings;
- std::optional<bool> load_on_start = request.params[1].isNull() ? std::nullopt : std::optional<bool>(request.params[1].get_bool());
- std::shared_ptr<CWallet> const wallet = LoadWallet(*context.chain, name, load_on_start, options, status, error, warnings);
- if (!wallet) {
- // Map bad format to not found, since bad format is returned when the
- // wallet directory exists, but doesn't contain a data file.
- RPCErrorCode code = RPC_WALLET_ERROR;
- switch (status) {
- case DatabaseStatus::FAILED_NOT_FOUND:
- case DatabaseStatus::FAILED_BAD_FORMAT:
- code = RPC_WALLET_NOT_FOUND;
- break;
- case DatabaseStatus::FAILED_ALREADY_LOADED:
- code = RPC_WALLET_ALREADY_LOADED;
- break;
- default: // RPC_WALLET_ERROR is returned for all other cases.
- break;
- }
- throw JSONRPCError(code, error.original);
- }
+ auto [wallet, warnings] = LoadWalletHelper(context, request.params[1], name);
UniValue obj(UniValue::VOBJ);
obj.pushKV("name", wallet->GetName());
@@ -2795,6 +2803,68 @@ static RPCHelpMan createwallet()
};
}
+static RPCHelpMan restorewallet()
+{
+ return RPCHelpMan{
+ "restorewallet",
+ "\nRestore and loads a wallet from backup.\n",
+ {
+ {"wallet_name", RPCArg::Type::STR, RPCArg::Optional::NO, "The name that will be applied to the restored wallet"},
+ {"backup_file", RPCArg::Type::STR, RPCArg::Optional::NO, "The backup file that will be used to restore the wallet."},
+ {"load_on_startup", RPCArg::Type::BOOL, RPCArg::Optional::OMITTED_NAMED_ARG, "Save wallet name to persistent settings and load on startup. True to add wallet to startup list, false to remove, null to leave unchanged."},
+ },
+ RPCResult{
+ RPCResult::Type::OBJ, "", "",
+ {
+ {RPCResult::Type::STR, "name", "The wallet name if restored successfully."},
+ {RPCResult::Type::STR, "warning", "Warning message if wallet was not loaded cleanly."},
+ }
+ },
+ RPCExamples{
+ HelpExampleCli("restorewallet", "\"testwallet\" \"home\\backups\\backup-file.bak\"")
+ + HelpExampleRpc("restorewallet", "\"testwallet\" \"home\\backups\\backup-file.bak\"")
+ + HelpExampleCliNamed("restorewallet", {{"wallet_name", "testwallet"}, {"backup_file", "home\\backups\\backup-file.bak\""}, {"load_on_startup", true}})
+ + HelpExampleRpcNamed("restorewallet", {{"wallet_name", "testwallet"}, {"backup_file", "home\\backups\\backup-file.bak\""}, {"load_on_startup", true}})
+ },
+ [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
+{
+
+ WalletContext& context = EnsureWalletContext(request.context);
+
+ std::string backup_file = request.params[1].get_str();
+
+ if (!fs::exists(backup_file)) {
+ throw JSONRPCError(RPC_INVALID_PARAMETER, "Backup file does not exist");
+ }
+
+ std::string wallet_name = request.params[0].get_str();
+
+ const fs::path wallet_path = fsbridge::AbsPathJoin(GetWalletDir(), wallet_name);
+
+ if (fs::exists(wallet_path)) {
+ throw JSONRPCError(RPC_INVALID_PARAMETER, "Wallet name already exists.");
+ }
+
+ if (!TryCreateDirectories(wallet_path)) {
+ throw JSONRPCError(RPC_WALLET_ERROR, strprintf("Failed to create database path '%s'. Database already exists.", wallet_path.string()));
+ }
+
+ auto wallet_file = wallet_path / "wallet.dat";
+
+ fs::copy_file(backup_file, wallet_file, fs::copy_option::fail_if_exists);
+
+ auto [wallet, warnings] = LoadWalletHelper(context, request.params[2], wallet_name);
+
+ UniValue obj(UniValue::VOBJ);
+ obj.pushKV("name", wallet->GetName());
+ obj.pushKV("warning", Join(warnings, Untranslated("\n")).original);
+
+ return obj;
+
+},
+ };
+}
+
static RPCHelpMan unloadwallet()
{
return RPCHelpMan{"unloadwallet",
@@ -4639,6 +4709,7 @@ static const CRPCCommand commands[] =
{ "wallet", &bumpfee, },
{ "wallet", &psbtbumpfee, },
{ "wallet", &createwallet, },
+ { "wallet", &restorewallet, },
{ "wallet", &dumpprivkey, },
{ "wallet", &dumpwallet, },
{ "wallet", &encryptwallet, },
diff --git a/test/functional/wallet_backup.py b/test/functional/wallet_backup.py
index 05a0ef0ea1..c7a983556d 100755
--- a/test/functional/wallet_backup.py
+++ b/test/functional/wallet_backup.py
@@ -111,6 +111,18 @@ class WalletBackupTest(BitcoinTestFramework):
os.remove(os.path.join(self.nodes[1].datadir, self.chain, 'wallets', self.default_wallet_name, self.wallet_data_filename))
os.remove(os.path.join(self.nodes[2].datadir, self.chain, 'wallets', self.default_wallet_name, self.wallet_data_filename))
+ def restore_nonexistent_wallet(self):
+ node = self.nodes[3]
+ nonexistent_wallet_file = os.path.join(self.nodes[0].datadir, 'nonexistent_wallet.bak')
+ wallet_name = "res0"
+ assert_raises_rpc_error(-8, "Backup file does not exist", node.restorewallet, wallet_name, nonexistent_wallet_file)
+
+ def restore_wallet_existent_name(self):
+ node = self.nodes[3]
+ wallet_file = os.path.join(self.nodes[0].datadir, 'wallet.bak')
+ wallet_name = "res0"
+ assert_raises_rpc_error(-8, "Wallet name already exists.", node.restorewallet, wallet_name, wallet_file)
+
def init_three(self):
self.init_wallet(0)
self.init_wallet(1)
@@ -169,26 +181,27 @@ class WalletBackupTest(BitcoinTestFramework):
##
# Test restoring spender wallets from backups
##
- self.log.info("Restoring using wallet.dat")
- self.stop_three()
- self.erase_three()
+ self.log.info("Restoring wallets on node 3 using backup files")
- # Start node2 with no chain
- shutil.rmtree(os.path.join(self.nodes[2].datadir, self.chain, 'blocks'))
- shutil.rmtree(os.path.join(self.nodes[2].datadir, self.chain, 'chainstate'))
+ self.restore_nonexistent_wallet()
- # Restore wallets from backup
- shutil.copyfile(os.path.join(self.nodes[0].datadir, 'wallet.bak'), os.path.join(self.nodes[0].datadir, self.chain, 'wallets', self.default_wallet_name, self.wallet_data_filename))
- shutil.copyfile(os.path.join(self.nodes[1].datadir, 'wallet.bak'), os.path.join(self.nodes[1].datadir, self.chain, 'wallets', self.default_wallet_name, self.wallet_data_filename))
- shutil.copyfile(os.path.join(self.nodes[2].datadir, 'wallet.bak'), os.path.join(self.nodes[2].datadir, self.chain, 'wallets', self.default_wallet_name, self.wallet_data_filename))
+ backup_file_0 = os.path.join(self.nodes[0].datadir, 'wallet.bak')
+ backup_file_1 = os.path.join(self.nodes[1].datadir, 'wallet.bak')
+ backup_file_2 = os.path.join(self.nodes[2].datadir, 'wallet.bak')
- self.log.info("Re-starting nodes")
- self.start_three()
- self.sync_blocks()
+ self.nodes[3].restorewallet("res0", backup_file_0)
+ self.nodes[3].restorewallet("res1", backup_file_1)
+ self.nodes[3].restorewallet("res2", backup_file_2)
+
+ res0_rpc = self.nodes[3].get_wallet_rpc("res0")
+ res1_rpc = self.nodes[3].get_wallet_rpc("res1")
+ res2_rpc = self.nodes[3].get_wallet_rpc("res2")
+
+ assert_equal(res0_rpc.getbalance(), balance0)
+ assert_equal(res1_rpc.getbalance(), balance1)
+ assert_equal(res2_rpc.getbalance(), balance2)
- assert_equal(self.nodes[0].getbalance(), balance0)
- assert_equal(self.nodes[1].getbalance(), balance1)
- assert_equal(self.nodes[2].getbalance(), balance2)
+ self.restore_wallet_existent_name()
if not self.options.descriptors:
self.log.info("Restoring using dumped wallet")