Skip to content

Commit 4cfe17c

Browse files
committed
Merge #10740: [wallet] loadwallet RPC - load wallet at runtime
cd53981 [docs] Add release notes for `loadwallet` RPC. (John Newbery) a46aeb6 [wallet] [tests] Test loadwallet (John Newbery) 5d15260 [wallet] [rpc] Add loadwallet RPC (John Newbery) 876eb64 [wallet] Pass error message back from CWallet::Verify() (John Newbery) e0e90db [wallet] Add CWallet::Verify function (John Newbery) 470316c [wallet] setup wallet background flushing in WalletInit directly (John Newbery) 59b87a2 [wallet] Fix potential memory leak in CreateWalletFromFile (John Newbery) Pull request description: Adds a `loadwallet` RPCs. This allows wallets to be loaded dynamically during runtime without having to stop-start the node with new `-wallet` params. Includes functional tests and release notes. Limitations: - currently this functionality is only available through the RPC interface. - wallets loaded in this way will not be displayed in the GUI. Tree-SHA512: f80dfe32b77f5c97ea3732ac538de7d6ed7e7cd0413c2ec91096bb652ad9bccf05d847ddbe81e7cd3cd44eb8030a51a5f00083871228b1b9b0b8398994f6f9f1
2 parents 11e7bdf + cd53981 commit 4cfe17c

File tree

6 files changed

+178
-57
lines changed

6 files changed

+178
-57
lines changed

doc/release-notes-pr10740.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Dynamic loading of wallets
2+
--------------------------
3+
4+
Previously, wallets could only be loaded at startup, by specifying `-wallet` parameters on the command line or in the bitcoin.conf file. It is now possible to load wallets dynamically at runtime by calling the `loadwallet` RPC.
5+
6+
The wallet can be specified as file/directory basename (which must be located in the `walletdir` directory), or as an absolute path to a file/directory.
7+
8+
This feature is currently only available through the RPC interface. Wallets loaded in this way will not display in the bitcoin-qt GUI.

src/wallet/init.cpp

Lines changed: 21 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#include <chainparams.h>
77
#include <init.h>
88
#include <net.h>
9+
#include <scheduler.h>
910
#include <util.h>
1011
#include <utilmoneystr.h>
1112
#include <validation.h>
@@ -189,55 +190,29 @@ bool WalletInit::Verify() const
189190

190191
uiInterface.InitMessage(_("Verifying wallet(s)..."));
191192

193+
std::vector<std::string> wallet_files = gArgs.GetArgs("-wallet");
194+
195+
// Parameter interaction code should have thrown an error if -salvagewallet
196+
// was enabled with more than wallet file, so the wallet_files size check
197+
// here should have no effect.
198+
bool salvage_wallet = gArgs.GetBoolArg("-salvagewallet", false) && wallet_files.size() <= 1;
199+
192200
// Keep track of each wallet absolute path to detect duplicates.
193201
std::set<fs::path> wallet_paths;
194202

195-
for (const std::string& walletFile : gArgs.GetArgs("-wallet")) {
196-
// Do some checking on wallet path. It should be either a:
197-
//
198-
// 1. Path where a directory can be created.
199-
// 2. Path to an existing directory.
200-
// 3. Path to a symlink to a directory.
201-
// 4. For backwards compatibility, the name of a data file in -walletdir.
202-
fs::path wallet_path = fs::absolute(walletFile, GetWalletDir());
203-
fs::file_type path_type = fs::symlink_status(wallet_path).type();
204-
if (!(path_type == fs::file_not_found || path_type == fs::directory_file ||
205-
(path_type == fs::symlink_file && fs::is_directory(wallet_path)) ||
206-
(path_type == fs::regular_file && fs::path(walletFile).filename() == walletFile))) {
207-
return InitError(strprintf(
208-
_("Invalid -wallet path '%s'. -wallet path should point to a directory where wallet.dat and "
209-
"database/log.?????????? files can be stored, a location where such a directory could be created, "
210-
"or (for backwards compatibility) the name of an existing data file in -walletdir (%s)"),
211-
walletFile, GetWalletDir()));
212-
}
203+
for (const auto wallet_file : wallet_files) {
204+
fs::path wallet_path = fs::absolute(wallet_file, GetWalletDir());
213205

214206
if (!wallet_paths.insert(wallet_path).second) {
215-
return InitError(strprintf(_("Error loading wallet %s. Duplicate -wallet filename specified."), walletFile));
216-
}
217-
218-
std::string strError;
219-
if (!WalletBatch::VerifyEnvironment(wallet_path, strError)) {
220-
return InitError(strError);
207+
return InitError(strprintf(_("Error loading wallet %s. Duplicate -wallet filename specified."), wallet_file));
221208
}
222209

223-
if (gArgs.GetBoolArg("-salvagewallet", false)) {
224-
// Recover readable keypairs:
225-
CWallet dummyWallet("dummy", WalletDatabase::CreateDummy());
226-
std::string backup_filename;
227-
if (!WalletBatch::Recover(wallet_path, (void *)&dummyWallet, WalletBatch::RecoverKeysOnlyFilter, backup_filename)) {
228-
return false;
229-
}
230-
}
231-
232-
std::string strWarning;
233-
bool dbV = WalletBatch::VerifyDatabaseFile(wallet_path, strWarning, strError);
234-
if (!strWarning.empty()) {
235-
InitWarning(strWarning);
236-
}
237-
if (!dbV) {
238-
InitError(strError);
239-
return false;
240-
}
210+
std::string error_string;
211+
std::string warning_string;
212+
bool verify_success = CWallet::Verify(wallet_file, salvage_wallet, error_string, warning_string);
213+
if (!error_string.empty()) InitError(error_string);
214+
if (!warning_string.empty()) InitWarning(warning_string);
215+
if (!verify_success) return false;
241216
}
242217

243218
return true;
@@ -264,8 +239,11 @@ bool WalletInit::Open() const
264239
void WalletInit::Start(CScheduler& scheduler) const
265240
{
266241
for (CWallet* pwallet : GetWallets()) {
267-
pwallet->postInitProcess(scheduler);
242+
pwallet->postInitProcess();
268243
}
244+
245+
// Run a thread to flush wallet periodically
246+
scheduler.scheduleEvery(MaybeCompactWalletDB, 500);
269247
}
270248

271249
void WalletInit::Flush() const

src/wallet/rpcwallet.cpp

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2994,6 +2994,53 @@ static UniValue listwallets(const JSONRPCRequest& request)
29942994
return obj;
29952995
}
29962996

2997+
UniValue loadwallet(const JSONRPCRequest& request)
2998+
{
2999+
if (request.fHelp || request.params.size() != 1)
3000+
throw std::runtime_error(
3001+
"loadwallet \"filename\"\n"
3002+
"\nLoads a wallet from a wallet file or directory."
3003+
"\nNote that all wallet command-line options used when starting bitcoind will be"
3004+
"\napplied to the new wallet (eg -zapwallettxes, upgradewallet, rescan, etc).\n"
3005+
"\nArguments:\n"
3006+
"1. \"filename\" (string, required) The wallet directory or .dat file.\n"
3007+
"\nResult:\n"
3008+
"{\n"
3009+
" \"name\" : <wallet_name>, (string) The wallet name if loaded successfully.\n"
3010+
" \"warning\" : <warning>, (string) Warning message if wallet was not loaded cleanly.\n"
3011+
"}\n"
3012+
"\nExamples:\n"
3013+
+ HelpExampleCli("loadwallet", "\"test.dat\"")
3014+
+ HelpExampleRpc("loadwallet", "\"test.dat\"")
3015+
);
3016+
std::string wallet_file = request.params[0].get_str();
3017+
std::string error;
3018+
3019+
fs::path wallet_path = fs::absolute(wallet_file, GetWalletDir());
3020+
if (fs::symlink_status(wallet_path).type() == fs::file_not_found) {
3021+
throw JSONRPCError(RPC_WALLET_NOT_FOUND, "Wallet " + wallet_file + " not found.");
3022+
}
3023+
3024+
std::string warning;
3025+
if (!CWallet::Verify(wallet_file, false, error, warning)) {
3026+
throw JSONRPCError(RPC_WALLET_ERROR, "Wallet file verification failed: " + error);
3027+
}
3028+
3029+
CWallet * const wallet = CWallet::CreateWalletFromFile(wallet_file, fs::absolute(wallet_file, GetWalletDir()));
3030+
if (!wallet) {
3031+
throw JSONRPCError(RPC_WALLET_ERROR, "Wallet loading failed.");
3032+
}
3033+
AddWallet(wallet);
3034+
3035+
wallet->postInitProcess();
3036+
3037+
UniValue obj(UniValue::VOBJ);
3038+
obj.pushKV("name", wallet->GetName());
3039+
obj.pushKV("warning", warning);
3040+
3041+
return obj;
3042+
}
3043+
29973044
static UniValue resendwallettransactions(const JSONRPCRequest& request)
29983045
{
29993046
CWallet * const pwallet = GetWalletForJSONRPCRequest(request);
@@ -4197,6 +4244,7 @@ static const CRPCCommand commands[] =
41974244
{ "wallet", "listtransactions", &listtransactions, {"account|dummy","count","skip","include_watchonly"} },
41984245
{ "wallet", "listunspent", &listunspent, {"minconf","maxconf","addresses","include_unsafe","query_options"} },
41994246
{ "wallet", "listwallets", &listwallets, {} },
4247+
{ "wallet", "loadwallet", &loadwallet, {"filename"} },
42004248
{ "wallet", "lockunspent", &lockunspent, {"unlock","transactions"} },
42014249
{ "wallet", "sendfrom", &sendfrom, {"fromaccount","toaddress","amount","minconf","comment","comment_to"} },
42024250
{ "wallet", "sendmany", &sendmany, {"fromaccount|dummy","amounts","minconf","comment","subtractfeefrom","replaceable","conf_target","estimate_mode"} },

src/wallet/wallet.cpp

Lines changed: 56 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@
2323
#include <primitives/block.h>
2424
#include <primitives/transaction.h>
2525
#include <script/script.h>
26-
#include <scheduler.h>
2726
#include <timedata.h>
2827
#include <txmempool.h>
2928
#include <utilmoneystr.h>
3029
#include <wallet/fees.h>
30+
#include <wallet/walletutil.h>
3131

3232
#include <algorithm>
3333
#include <assert.h>
@@ -3990,6 +3990,52 @@ void CWallet::MarkPreSplitKeys()
39903990
}
39913991
}
39923992

3993+
bool CWallet::Verify(std::string wallet_file, bool salvage_wallet, std::string& error_string, std::string& warning_string)
3994+
{
3995+
// Do some checking on wallet path. It should be either a:
3996+
//
3997+
// 1. Path where a directory can be created.
3998+
// 2. Path to an existing directory.
3999+
// 3. Path to a symlink to a directory.
4000+
// 4. For backwards compatibility, the name of a data file in -walletdir.
4001+
LOCK(cs_wallets);
4002+
fs::path wallet_path = fs::absolute(wallet_file, GetWalletDir());
4003+
fs::file_type path_type = fs::symlink_status(wallet_path).type();
4004+
if (!(path_type == fs::file_not_found || path_type == fs::directory_file ||
4005+
(path_type == fs::symlink_file && fs::is_directory(wallet_path)) ||
4006+
(path_type == fs::regular_file && fs::path(wallet_file).filename() == wallet_file))) {
4007+
error_string = strprintf(
4008+
"Invalid -wallet path '%s'. -wallet path should point to a directory where wallet.dat and "
4009+
"database/log.?????????? files can be stored, a location where such a directory could be created, "
4010+
"or (for backwards compatibility) the name of an existing data file in -walletdir (%s)",
4011+
wallet_file, GetWalletDir());
4012+
return false;
4013+
}
4014+
4015+
// Make sure that the wallet path doesn't clash with an existing wallet path
4016+
for (auto wallet : GetWallets()) {
4017+
if (fs::absolute(wallet->GetName(), GetWalletDir()) == wallet_path) {
4018+
error_string = strprintf("Error loading wallet %s. Duplicate -wallet filename specified.", wallet_file);
4019+
return false;
4020+
}
4021+
}
4022+
4023+
if (!WalletBatch::VerifyEnvironment(wallet_path, error_string)) {
4024+
return false;
4025+
}
4026+
4027+
if (salvage_wallet) {
4028+
// Recover readable keypairs:
4029+
CWallet dummyWallet("dummy", WalletDatabase::CreateDummy());
4030+
std::string backup_filename;
4031+
if (!WalletBatch::Recover(wallet_path, (void *)&dummyWallet, WalletBatch::RecoverKeysOnlyFilter, backup_filename)) {
4032+
return false;
4033+
}
4034+
}
4035+
4036+
return WalletBatch::VerifyDatabaseFile(wallet_path, warning_string, error_string);
4037+
}
4038+
39934039
CWallet* CWallet::CreateWalletFromFile(const std::string& name, const fs::path& path)
39944040
{
39954041
const std::string& walletFile = name;
@@ -4012,7 +4058,10 @@ CWallet* CWallet::CreateWalletFromFile(const std::string& name, const fs::path&
40124058

40134059
int64_t nStart = GetTimeMillis();
40144060
bool fFirstRun = true;
4015-
CWallet *walletInstance = new CWallet(name, WalletDatabase::Create(path));
4061+
// Make a temporary wallet unique pointer so memory doesn't get leaked if
4062+
// wallet creation fails.
4063+
auto temp_wallet = MakeUnique<CWallet>(name, WalletDatabase::Create(path));
4064+
CWallet* walletInstance = temp_wallet.get();
40164065
DBErrors nLoadWalletRet = walletInstance->LoadWallet(fFirstRun);
40174066
if (nLoadWalletRet != DBErrors::LOAD_OK)
40184067
{
@@ -4224,7 +4273,6 @@ CWallet* CWallet::CreateWalletFromFile(const std::string& name, const fs::path&
42244273
}
42254274

42264275
walletInstance->m_last_block_processed = chainActive.Tip();
4227-
RegisterValidationInterface(walletInstance);
42284276

42294277
if (chainActive.Tip() && chainActive.Tip() != pindexRescan)
42304278
{
@@ -4290,6 +4338,10 @@ CWallet* CWallet::CreateWalletFromFile(const std::string& name, const fs::path&
42904338
}
42914339
}
42924340
}
4341+
4342+
// Register with the validation interface. It's ok to do this after rescan since we're still holding cs_main.
4343+
RegisterValidationInterface(temp_wallet.release());
4344+
42934345
walletInstance->SetBroadcastTransactions(gArgs.GetBoolArg("-walletbroadcast", DEFAULT_WALLETBROADCAST));
42944346

42954347
{
@@ -4302,18 +4354,11 @@ CWallet* CWallet::CreateWalletFromFile(const std::string& name, const fs::path&
43024354
return walletInstance;
43034355
}
43044356

4305-
std::atomic<bool> CWallet::fFlushScheduled(false);
4306-
4307-
void CWallet::postInitProcess(CScheduler& scheduler)
4357+
void CWallet::postInitProcess()
43084358
{
43094359
// Add wallet transactions that aren't already in a block to mempool
43104360
// Do this here as mempool requires genesis block to be loaded
43114361
ReacceptWalletTransactions();
4312-
4313-
// Run a thread to flush wallet periodically
4314-
if (!CWallet::fFlushScheduled.exchange(true)) {
4315-
scheduler.scheduleEvery(MaybeCompactWalletDB, 500);
4316-
}
43174362
}
43184363

43194364
bool CWallet::BackupWallet(const std::string& strDest)

src/wallet/wallet.h

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@ class CCoinControl;
6868
class COutput;
6969
class CReserveKey;
7070
class CScript;
71-
class CScheduler;
7271
class CTxMemPool;
7372
class CBlockPolicyEstimator;
7473
class CWalletTx;
@@ -675,7 +674,6 @@ class WalletRescanReserver; //forward declarations for ScanForWalletTransactions
675674
class CWallet final : public CCryptoKeyStore, public CValidationInterface
676675
{
677676
private:
678-
static std::atomic<bool> fFlushScheduled;
679677
std::atomic<bool> fAbortRescan{false};
680678
std::atomic<bool> fScanningWallet{false}; // controlled by WalletRescanReserver
681679
std::mutex mutexScanning;
@@ -1120,14 +1118,17 @@ class CWallet final : public CCryptoKeyStore, public CValidationInterface
11201118
/** Mark a transaction as replaced by another transaction (e.g., BIP 125). */
11211119
bool MarkReplaced(const uint256& originalHash, const uint256& newHash);
11221120

1121+
//! Verify wallet naming and perform salvage on the wallet if required
1122+
static bool Verify(std::string wallet_file, bool salvage_wallet, std::string& error_string, std::string& warning_string);
1123+
11231124
/* Initializes the wallet, returns a new CWallet instance or a null pointer in case of an error */
11241125
static CWallet* CreateWalletFromFile(const std::string& name, const fs::path& path);
11251126

11261127
/**
11271128
* Wallet post-init setup
11281129
* Gives the wallet a chance to register repetitive tasks and complete post-init tasks
11291130
*/
1130-
void postInitProcess(CScheduler& scheduler);
1131+
void postInitProcess();
11311132

11321133
bool BackupWallet(const std::string& strDest);
11331134

test/functional/wallet_multiwallet.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,5 +170,46 @@ def run_test(self):
170170
assert_equal(w1.getwalletinfo()['paytxfee'], 0)
171171
assert_equal(w2.getwalletinfo()['paytxfee'], 4.0)
172172

173+
self.log.info("Test dynamic wallet loading")
174+
175+
self.restart_node(0, ['-nowallet'])
176+
assert_equal(node.listwallets(), [])
177+
assert_raises_rpc_error(-32601, "Method not found", node.getwalletinfo)
178+
179+
self.log.info("Load first wallet")
180+
loadwallet_name = node.loadwallet(wallet_names[0])
181+
assert_equal(loadwallet_name['name'], wallet_names[0])
182+
assert_equal(node.listwallets(), wallet_names[0:1])
183+
node.getwalletinfo()
184+
w1 = node.get_wallet_rpc(wallet_names[0])
185+
w1.getwalletinfo()
186+
187+
self.log.info("Load second wallet")
188+
loadwallet_name = node.loadwallet(wallet_names[1])
189+
assert_equal(loadwallet_name['name'], wallet_names[1])
190+
assert_equal(node.listwallets(), wallet_names[0:2])
191+
assert_raises_rpc_error(-19, "Wallet file not specified", node.getwalletinfo)
192+
w2 = node.get_wallet_rpc(wallet_names[1])
193+
w2.getwalletinfo()
194+
195+
self.log.info("Load remaining wallets")
196+
for wallet_name in wallet_names[2:]:
197+
loadwallet_name = self.nodes[0].loadwallet(wallet_name)
198+
assert_equal(loadwallet_name['name'], wallet_name)
199+
200+
assert_equal(set(self.nodes[0].listwallets()), set(wallet_names))
201+
202+
# Fail to load if wallet doesn't exist
203+
assert_raises_rpc_error(-18, 'Wallet wallets not found.', self.nodes[0].loadwallet, 'wallets')
204+
205+
# Fail to load duplicate wallets
206+
assert_raises_rpc_error(-4, 'Wallet file verification failed: Error loading wallet w1. Duplicate -wallet filename specified.', self.nodes[0].loadwallet, wallet_names[0])
207+
208+
# Fail to load if one wallet is a copy of another
209+
assert_raises_rpc_error(-1, "BerkeleyBatch: Can't open database w8_copy (duplicates fileid", self.nodes[0].loadwallet, 'w8_copy')
210+
211+
# Fail to load if wallet file is a symlink
212+
assert_raises_rpc_error(-4, "Wallet file verification failed: Invalid -wallet path 'w8_symlink'", self.nodes[0].loadwallet, 'w8_symlink')
213+
173214
if __name__ == '__main__':
174215
MultiWalletTest().main()

0 commit comments

Comments
 (0)