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 5fe8100ff3
  meshcollider:
    utACK 5fe8100ff36fed6d50c2a25b028f57b25af3504c

Tree-SHA512: 9639df4d8ad32f255f5b868320dc69878bd9aceb3b471b49dfad500b67681e2d354292b5410982fbf18e25a44ed0c06fd4a0dd010e82807c2e00ff32e84047a1
This commit is contained in:
Samuel Dobson 2021-08-15 16:46:51 +12:00 committed by pasta
parent c99fe37702
commit 783a6bd842
No known key found for this signature in database
GPG Key ID: 52527BEDABE87984
3 changed files with 125 additions and 42 deletions

View File

@ -215,6 +215,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
{ "createwallet", 4, "avoid_reuse"}, { "createwallet", 4, "avoid_reuse"},
{ "createwallet", 5, "descriptors"}, { "createwallet", 5, "descriptors"},
{ "createwallet", 6, "load_on_startup"}, { "createwallet", 6, "load_on_startup"},
{ "restorewallet", 2, "load_on_startup"},
{ "loadwallet", 1, "load_on_startup"}, { "loadwallet", 1, "load_on_startup"},
{ "unloadwallet", 1, "load_on_startup"}, { "unloadwallet", 1, "load_on_startup"},
{ "upgradetohd", 3, "rescan"}, { "upgradetohd", 3, "rescan"},

View File

@ -2742,6 +2742,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, *context.m_coinjoin_loader, 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 upgradetohd() static RPCHelpMan upgradetohd()
{ {
return RPCHelpMan{"upgradetohd", return RPCHelpMan{"upgradetohd",
@ -2863,30 +2894,7 @@ static RPCHelpMan loadwallet()
WalletContext& context = EnsureWalletContext(request.context); WalletContext& context = EnsureWalletContext(request.context);
const std::string name(request.params[0].get_str()); const std::string name(request.params[0].get_str());
DatabaseOptions options; auto [wallet, warnings] = LoadWalletHelper(context, request.params[1], name);
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, *context.m_coinjoin_loader, 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);
}
UniValue obj(UniValue::VOBJ); UniValue obj(UniValue::VOBJ);
obj.pushKV("name", wallet->GetName()); obj.pushKV("name", wallet->GetName());
@ -3054,6 +3062,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() static RPCHelpMan unloadwallet()
{ {
return RPCHelpMan{"unloadwallet", return RPCHelpMan{"unloadwallet",
@ -4638,6 +4708,7 @@ static const CRPCCommand commands[] =
{ "wallet", "addmultisigaddress", &addmultisigaddress, {"nrequired","keys","label"} }, { "wallet", "addmultisigaddress", &addmultisigaddress, {"nrequired","keys","label"} },
{ "wallet", "backupwallet", &backupwallet, {"destination"} }, { "wallet", "backupwallet", &backupwallet, {"destination"} },
{ "wallet", "createwallet", &createwallet, {"wallet_name", "disable_private_keys", "blank", "passphrase", "avoid_reuse", "descriptors", "load_on_startup"} }, { "wallet", "createwallet", &createwallet, {"wallet_name", "disable_private_keys", "blank", "passphrase", "avoid_reuse", "descriptors", "load_on_startup"} },
{ "wallet", "restorewallet", &restorewallet, {"wallet_name","backup_file", "load_on_startup"} },
{ "wallet", "dumphdinfo", &dumphdinfo, {} }, { "wallet", "dumphdinfo", &dumphdinfo, {} },
{ "wallet", "dumpprivkey", &dumpprivkey, {"address"} }, { "wallet", "dumpprivkey", &dumpprivkey, {"address"} },
{ "wallet", "dumpwallet", &dumpwallet, {"filename"} }, { "wallet", "dumpwallet", &dumpwallet, {"filename"} },

View File

@ -107,6 +107,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[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)) 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): def init_three(self):
self.init_wallet(0) self.init_wallet(0)
self.init_wallet(1) self.init_wallet(1)
@ -165,28 +177,27 @@ class WalletBackupTest(BitcoinTestFramework):
## ##
# Test restoring spender wallets from backups # Test restoring spender wallets from backups
## ##
self.log.info("Restoring using wallet.dat") self.log.info("Restoring wallets on node 3 using backup files")
self.stop_three()
self.erase_three()
# Start node2 with no chain self.restore_nonexistent_wallet()
shutil.rmtree(os.path.join(self.nodes[2].datadir, self.chain, 'blocks'))
shutil.rmtree(os.path.join(self.nodes[2].datadir, self.chain, 'chainstate'))
shutil.rmtree(os.path.join(self.nodes[2].datadir, self.chain, 'evodb'))
shutil.rmtree(os.path.join(self.nodes[2].datadir, self.chain, 'llmq'))
# Restore wallets from backup backup_file_0 = os.path.join(self.nodes[0].datadir, 'wallet.bak')
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)) backup_file_1 = os.path.join(self.nodes[1].datadir, 'wallet.bak')
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)) backup_file_2 = os.path.join(self.nodes[2].datadir, 'wallet.bak')
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))
self.log.info("Re-starting nodes") self.nodes[3].restorewallet("res0", backup_file_0)
self.start_three() self.nodes[3].restorewallet("res1", backup_file_1)
self.sync_blocks() self.nodes[3].restorewallet("res2", backup_file_2)
assert_equal(self.nodes[0].getbalance(), balance0) res0_rpc = self.nodes[3].get_wallet_rpc("res0")
assert_equal(self.nodes[1].getbalance(), balance1) res1_rpc = self.nodes[3].get_wallet_rpc("res1")
assert_equal(self.nodes[2].getbalance(), balance2) 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)
self.restore_wallet_existent_name()
if not self.options.descriptors: if not self.options.descriptors:
self.log.info("Restoring using dumped wallet") self.log.info("Restoring using dumped wallet")