Skip to content

Commit 502d22c

Browse files
committed
Merge bitcoin/bitcoin#22541: Add a new RPC command: restorewallet
5fe8100 Change the wallet_backup.py test to use the restorewallet RPC command instead of restoring wallets manually. (lsilva01) ae23fab 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 5fe8100 prayank23: tACK bitcoin/bitcoin@5fe8100 meshcollider: utACK 5fe8100 Tree-SHA512: 9639df4d8ad32f255f5b868320dc69878bd9aceb3b471b49dfad500b67681e2d354292b5410982fbf18e25a44ed0c06fd4a0dd010e82807c2e00ff32e84047a1
2 parents be499aa + 5fe8100 commit 502d22c

File tree

3 files changed

+125
-40
lines changed

3 files changed

+125
-40
lines changed

src/rpc/client.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
187187
{ "createwallet", 5, "descriptors"},
188188
{ "createwallet", 6, "load_on_startup"},
189189
{ "createwallet", 7, "external_signer"},
190+
{ "restorewallet", 2, "load_on_startup"},
190191
{ "loadwallet", 1, "load_on_startup"},
191192
{ "unloadwallet", 1, "load_on_startup"},
192193
{ "getnodeaddresses", 0, "count"},

src/wallet/rpcwallet.cpp

Lines changed: 95 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2572,6 +2572,37 @@ static RPCHelpMan listwallets()
25722572
};
25732573
}
25742574

2575+
static std::tuple<std::shared_ptr<CWallet>, std::vector<bilingual_str>> LoadWalletHelper(WalletContext& context, UniValue load_on_start_param, const std::string wallet_name)
2576+
{
2577+
DatabaseOptions options;
2578+
DatabaseStatus status;
2579+
options.require_existing = true;
2580+
bilingual_str error;
2581+
std::vector<bilingual_str> warnings;
2582+
std::optional<bool> load_on_start = load_on_start_param.isNull() ? std::nullopt : std::optional<bool>(load_on_start_param.get_bool());
2583+
std::shared_ptr<CWallet> const wallet = LoadWallet(*context.chain, wallet_name, load_on_start, options, status, error, warnings);
2584+
2585+
if (!wallet) {
2586+
// Map bad format to not found, since bad format is returned when the
2587+
// wallet directory exists, but doesn't contain a data file.
2588+
RPCErrorCode code = RPC_WALLET_ERROR;
2589+
switch (status) {
2590+
case DatabaseStatus::FAILED_NOT_FOUND:
2591+
case DatabaseStatus::FAILED_BAD_FORMAT:
2592+
code = RPC_WALLET_NOT_FOUND;
2593+
break;
2594+
case DatabaseStatus::FAILED_ALREADY_LOADED:
2595+
code = RPC_WALLET_ALREADY_LOADED;
2596+
break;
2597+
default: // RPC_WALLET_ERROR is returned for all other cases.
2598+
break;
2599+
}
2600+
throw JSONRPCError(code, error.original);
2601+
}
2602+
2603+
return { wallet, warnings };
2604+
}
2605+
25752606
static RPCHelpMan loadwallet()
25762607
{
25772608
return RPCHelpMan{"loadwallet",
@@ -2598,30 +2629,7 @@ static RPCHelpMan loadwallet()
25982629
WalletContext& context = EnsureWalletContext(request.context);
25992630
const std::string name(request.params[0].get_str());
26002631

2601-
DatabaseOptions options;
2602-
DatabaseStatus status;
2603-
options.require_existing = true;
2604-
bilingual_str error;
2605-
std::vector<bilingual_str> warnings;
2606-
std::optional<bool> load_on_start = request.params[1].isNull() ? std::nullopt : std::optional<bool>(request.params[1].get_bool());
2607-
std::shared_ptr<CWallet> const wallet = LoadWallet(*context.chain, name, load_on_start, options, status, error, warnings);
2608-
if (!wallet) {
2609-
// Map bad format to not found, since bad format is returned when the
2610-
// wallet directory exists, but doesn't contain a data file.
2611-
RPCErrorCode code = RPC_WALLET_ERROR;
2612-
switch (status) {
2613-
case DatabaseStatus::FAILED_NOT_FOUND:
2614-
case DatabaseStatus::FAILED_BAD_FORMAT:
2615-
code = RPC_WALLET_NOT_FOUND;
2616-
break;
2617-
case DatabaseStatus::FAILED_ALREADY_LOADED:
2618-
code = RPC_WALLET_ALREADY_LOADED;
2619-
break;
2620-
default: // RPC_WALLET_ERROR is returned for all other cases.
2621-
break;
2622-
}
2623-
throw JSONRPCError(code, error.original);
2624-
}
2632+
auto [wallet, warnings] = LoadWalletHelper(context, request.params[1], name);
26252633

26262634
UniValue obj(UniValue::VOBJ);
26272635
obj.pushKV("name", wallet->GetName());
@@ -2795,6 +2803,68 @@ static RPCHelpMan createwallet()
27952803
};
27962804
}
27972805

2806+
static RPCHelpMan restorewallet()
2807+
{
2808+
return RPCHelpMan{
2809+
"restorewallet",
2810+
"\nRestore and loads a wallet from backup.\n",
2811+
{
2812+
{"wallet_name", RPCArg::Type::STR, RPCArg::Optional::NO, "The name that will be applied to the restored wallet"},
2813+
{"backup_file", RPCArg::Type::STR, RPCArg::Optional::NO, "The backup file that will be used to restore the wallet."},
2814+
{"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."},
2815+
},
2816+
RPCResult{
2817+
RPCResult::Type::OBJ, "", "",
2818+
{
2819+
{RPCResult::Type::STR, "name", "The wallet name if restored successfully."},
2820+
{RPCResult::Type::STR, "warning", "Warning message if wallet was not loaded cleanly."},
2821+
}
2822+
},
2823+
RPCExamples{
2824+
HelpExampleCli("restorewallet", "\"testwallet\" \"home\\backups\\backup-file.bak\"")
2825+
+ HelpExampleRpc("restorewallet", "\"testwallet\" \"home\\backups\\backup-file.bak\"")
2826+
+ HelpExampleCliNamed("restorewallet", {{"wallet_name", "testwallet"}, {"backup_file", "home\\backups\\backup-file.bak\""}, {"load_on_startup", true}})
2827+
+ HelpExampleRpcNamed("restorewallet", {{"wallet_name", "testwallet"}, {"backup_file", "home\\backups\\backup-file.bak\""}, {"load_on_startup", true}})
2828+
},
2829+
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
2830+
{
2831+
2832+
WalletContext& context = EnsureWalletContext(request.context);
2833+
2834+
std::string backup_file = request.params[1].get_str();
2835+
2836+
if (!fs::exists(backup_file)) {
2837+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Backup file does not exist");
2838+
}
2839+
2840+
std::string wallet_name = request.params[0].get_str();
2841+
2842+
const fs::path wallet_path = fsbridge::AbsPathJoin(GetWalletDir(), wallet_name);
2843+
2844+
if (fs::exists(wallet_path)) {
2845+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Wallet name already exists.");
2846+
}
2847+
2848+
if (!TryCreateDirectories(wallet_path)) {
2849+
throw JSONRPCError(RPC_WALLET_ERROR, strprintf("Failed to create database path '%s'. Database already exists.", wallet_path.string()));
2850+
}
2851+
2852+
auto wallet_file = wallet_path / "wallet.dat";
2853+
2854+
fs::copy_file(backup_file, wallet_file, fs::copy_option::fail_if_exists);
2855+
2856+
auto [wallet, warnings] = LoadWalletHelper(context, request.params[2], wallet_name);
2857+
2858+
UniValue obj(UniValue::VOBJ);
2859+
obj.pushKV("name", wallet->GetName());
2860+
obj.pushKV("warning", Join(warnings, Untranslated("\n")).original);
2861+
2862+
return obj;
2863+
2864+
},
2865+
};
2866+
}
2867+
27982868
static RPCHelpMan unloadwallet()
27992869
{
28002870
return RPCHelpMan{"unloadwallet",
@@ -4639,6 +4709,7 @@ static const CRPCCommand commands[] =
46394709
{ "wallet", &bumpfee, },
46404710
{ "wallet", &psbtbumpfee, },
46414711
{ "wallet", &createwallet, },
4712+
{ "wallet", &restorewallet, },
46424713
{ "wallet", &dumpprivkey, },
46434714
{ "wallet", &dumpwallet, },
46444715
{ "wallet", &encryptwallet, },

test/functional/wallet_backup.py

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,18 @@ def erase_three(self):
111111
os.remove(os.path.join(self.nodes[1].datadir, self.chain, 'wallets', self.default_wallet_name, self.wallet_data_filename))
112112
os.remove(os.path.join(self.nodes[2].datadir, self.chain, 'wallets', self.default_wallet_name, self.wallet_data_filename))
113113

114+
def restore_nonexistent_wallet(self):
115+
node = self.nodes[3]
116+
nonexistent_wallet_file = os.path.join(self.nodes[0].datadir, 'nonexistent_wallet.bak')
117+
wallet_name = "res0"
118+
assert_raises_rpc_error(-8, "Backup file does not exist", node.restorewallet, wallet_name, nonexistent_wallet_file)
119+
120+
def restore_wallet_existent_name(self):
121+
node = self.nodes[3]
122+
wallet_file = os.path.join(self.nodes[0].datadir, 'wallet.bak')
123+
wallet_name = "res0"
124+
assert_raises_rpc_error(-8, "Wallet name already exists.", node.restorewallet, wallet_name, wallet_file)
125+
114126
def init_three(self):
115127
self.init_wallet(0)
116128
self.init_wallet(1)
@@ -169,26 +181,27 @@ def run_test(self):
169181
##
170182
# Test restoring spender wallets from backups
171183
##
172-
self.log.info("Restoring using wallet.dat")
173-
self.stop_three()
174-
self.erase_three()
184+
self.log.info("Restoring wallets on node 3 using backup files")
175185

176-
# Start node2 with no chain
177-
shutil.rmtree(os.path.join(self.nodes[2].datadir, self.chain, 'blocks'))
178-
shutil.rmtree(os.path.join(self.nodes[2].datadir, self.chain, 'chainstate'))
186+
self.restore_nonexistent_wallet()
179187

180-
# Restore wallets from backup
181-
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))
182-
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))
183-
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))
188+
backup_file_0 = os.path.join(self.nodes[0].datadir, 'wallet.bak')
189+
backup_file_1 = os.path.join(self.nodes[1].datadir, 'wallet.bak')
190+
backup_file_2 = os.path.join(self.nodes[2].datadir, 'wallet.bak')
184191

185-
self.log.info("Re-starting nodes")
186-
self.start_three()
187-
self.sync_blocks()
192+
self.nodes[3].restorewallet("res0", backup_file_0)
193+
self.nodes[3].restorewallet("res1", backup_file_1)
194+
self.nodes[3].restorewallet("res2", backup_file_2)
195+
196+
res0_rpc = self.nodes[3].get_wallet_rpc("res0")
197+
res1_rpc = self.nodes[3].get_wallet_rpc("res1")
198+
res2_rpc = self.nodes[3].get_wallet_rpc("res2")
199+
200+
assert_equal(res0_rpc.getbalance(), balance0)
201+
assert_equal(res1_rpc.getbalance(), balance1)
202+
assert_equal(res2_rpc.getbalance(), balance2)
188203

189-
assert_equal(self.nodes[0].getbalance(), balance0)
190-
assert_equal(self.nodes[1].getbalance(), balance1)
191-
assert_equal(self.nodes[2].getbalance(), balance2)
204+
self.restore_wallet_existent_name()
192205

193206
if not self.options.descriptors:
194207
self.log.info("Restoring using dumped wallet")

0 commit comments

Comments
 (0)