Skip to content

Commit

Permalink
Add loadwallet and createwallet RPC load_on_startup options
Browse files Browse the repository at this point in the history
This maintains a persistent list of wallets stored in settings that will
automatically be loaded on startup. Being able to load a wallet automatically
on startup will be more useful in the GUI when the option to create wallets is
added in #15006, but it's reasonable to expose this feature by RPC as well.
  • Loading branch information
ryanofsky committed Dec 2, 2019
1 parent f763f26 commit 5ccdb04
Show file tree
Hide file tree
Showing 8 changed files with 133 additions and 2 deletions.
21 changes: 21 additions & 0 deletions src/interfaces/chain.cpp
Expand Up @@ -365,6 +365,27 @@ class ChainImpl : public Chain
RPCRunLater(name, std::move(fn), seconds);
}
int rpcSerializationFlags() override { return RPCSerializationFlags(); }
util::SettingsValue getSetting(const std::string& name) override
{
util::SettingsValue result;
gArgs.LockSettings([&](const util::Settings& settings) {
if (const util::SettingsValue* value = util::FindKey(settings.rw_settings, name)) {
result = *value;
}
});
return result;
}
bool updateSetting(const std::string& name, const util::SettingsValue& value) override
{
gArgs.LockSettings([&](util::Settings& settings) {
if (value.isNull()) {
settings.rw_settings.erase(name);
} else {
settings.rw_settings[name] = value;
}
});
return gArgs.WriteSettingsFile();
}
void requestMempoolTransactions(Notifications& notifications) override
{
LOCK2(::cs_main, ::mempool.cs);
Expand Down
7 changes: 7 additions & 0 deletions src/interfaces/chain.h
Expand Up @@ -7,6 +7,7 @@

#include <optional.h> // For Optional and nullopt
#include <primitives/transaction.h> // For CTransactionRef
#include <util/settings.h> // For util::SettingsValue

#include <memory>
#include <stddef.h>
Expand Down Expand Up @@ -248,6 +249,12 @@ class Chain
//! Current RPC serialization flags.
virtual int rpcSerializationFlags() = 0;

// Return <datadir>/settings.json setting value.
virtual util::SettingsValue getSetting(const std::string& name) = 0;

//! Write a setting to <datadir>/settings.json.
virtual bool updateSetting(const std::string& name, const util::SettingsValue& value) = 0;

//! Synchronously send TransactionAddedToMempool notifications about all
//! current mempool transactions to the specified handler and return after
//! the last one is sent. These notifications aren't coordinated with async
Expand Down
2 changes: 2 additions & 0 deletions src/rpc/client.cpp
Expand Up @@ -169,6 +169,8 @@ static const CRPCConvertParam vRPCConvertParams[] =
{ "createwallet", 1, "disable_private_keys"},
{ "createwallet", 2, "blank"},
{ "createwallet", 4, "avoid_reuse"},
{ "createwallet", 5, "load_on_startup"},
{ "loadwallet", 1, "load_on_startup"},
{ "getnodeaddresses", 0, "count"},
{ "stop", 0, "wait" },
};
Expand Down
25 changes: 25 additions & 0 deletions src/wallet/load.cpp
Expand Up @@ -12,6 +12,8 @@
#include <util/translation.h>
#include <wallet/wallet.h>

#include <univalue.h>

bool VerifyWallets(interfaces::Chain& chain, const std::vector<std::string>& wallet_files)
{
if (gArgs.IsArgSet("-walletdir")) {
Expand Down Expand Up @@ -116,3 +118,26 @@ void UnloadWallets()
UnloadWallet(std::move(wallet));
}
}

bool AddWalletSetting(interfaces::Chain& chain, const std::string& wallet_name)
{
util::SettingsValue setting_value = chain.getSetting("wallet");
if (!setting_value.isArray()) setting_value.setArray();
for (const util::SettingsValue& value : setting_value.getValues()) {
if (value.isStr() && value.get_str() == wallet_name) return true;
}
setting_value.push_back(wallet_name);
return chain.updateSetting("wallet", setting_value);
}

bool RemoveWalletSetting(interfaces::Chain& chain, const std::string& wallet_name)
{
util::SettingsValue setting_value = chain.getSetting("wallet");
if (!setting_value.isArray()) return true;
util::SettingsValue new_value(util::SettingsValue::VARR);
for (const util::SettingsValue& value : setting_value.getValues()) {
if (!value.isStr() || value.get_str() != wallet_name) new_value.push_back(value);
}
if (new_value.size() == setting_value.size()) return true;
return chain.updateSetting("wallet", new_value);
}
6 changes: 6 additions & 0 deletions src/wallet/load.h
Expand Up @@ -35,4 +35,10 @@ void StopWallets();
//! Close all wallets.
void UnloadWallets();

//! Add wallet name to persistent configuration so it will be loaded on startup.
bool AddWalletSetting(interfaces::Chain& chain, const std::string& wallet_name);

//! Remove wallet name from persistent configuration so it will not be loaded on startup.
bool RemoveWalletSetting(interfaces::Chain& chain, const std::string& wallet_name);

#endif // BITCOIN_WALLET_LOAD_H
32 changes: 30 additions & 2 deletions src/wallet/rpcwallet.cpp
Expand Up @@ -26,6 +26,7 @@
#include <util/validation.h>
#include <wallet/coincontrol.h>
#include <wallet/feebumper.h>
#include <wallet/load.h>
#include <wallet/psbtwallet.h>
#include <wallet/rpcwallet.h>
#include <wallet/wallet.h>
Expand Down Expand Up @@ -2571,6 +2572,7 @@ static UniValue loadwallet(const JSONRPCRequest& request)
"\napplied to the new wallet (eg -zapwallettxes, upgradewallet, rescan, etc).\n",
{
{"filename", RPCArg::Type::STR, RPCArg::Optional::NO, "The wallet directory or .dat file."},
{"load_on_startup", RPCArg::Type::BOOL, /* default */ "false", "Save wallet name to persistent settings and load on startup."},
},
RPCResult{
"{\n"
Expand All @@ -2596,11 +2598,21 @@ static UniValue loadwallet(const JSONRPCRequest& request)
}
}

bool load_on_startup = false;
if (!request.params[1].isNull() && request.params[1].isBool()) {
load_on_startup = request.params[1].get_bool();
}

std::string error;
std::vector<std::string> warning;
std::shared_ptr<CWallet> const wallet = LoadWallet(*g_rpc_chain, location, error, warning);
if (!wallet) throw JSONRPCError(RPC_WALLET_ERROR, error);

if (load_on_startup && !AddWalletSetting(wallet->chain(), location.GetName())) {
throw JSONRPCError(RPC_MISC_ERROR, "Wallet loaded, but load-on-startup list could not be written, so wallet "
"may be not loaded on node restart.");
}

UniValue obj(UniValue::VOBJ);
obj.pushKV("name", wallet->GetName());
obj.pushKV("warning", Join(warning, "\n"));
Expand Down Expand Up @@ -2686,6 +2698,7 @@ static UniValue createwallet(const JSONRPCRequest& request)
{"blank", RPCArg::Type::BOOL, /* default */ "false", "Create a blank wallet. A blank wallet has no keys or HD seed. One can be set using sethdseed."},
{"passphrase", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "Encrypt the wallet with this passphrase."},
{"avoid_reuse", RPCArg::Type::BOOL, /* default */ "false", "Keep track of coin reuse, and treat dirty and clean coins differently with privacy considerations in mind."},
{"load_on_startup", RPCArg::Type::BOOL, /* default */ "false", "Save wallet name to persistent settings and load on startup."},
},
RPCResult{
"{\n"
Expand Down Expand Up @@ -2722,6 +2735,11 @@ static UniValue createwallet(const JSONRPCRequest& request)
flags |= WALLET_FLAG_AVOID_REUSE;
}

bool load_on_startup = false;
if (!request.params[5].isNull() && request.params[5].isBool()) {
load_on_startup = request.params[5].get_bool();
}

std::string error;
std::shared_ptr<CWallet> wallet;
WalletCreationStatus status = CreateWallet(*g_rpc_chain, passphrase, flags, request.params[0].get_str(), error, warnings, wallet);
Expand All @@ -2735,6 +2753,11 @@ static UniValue createwallet(const JSONRPCRequest& request)
// no default case, so the compiler can warn about missing cases
}

if (load_on_startup && !AddWalletSetting(wallet->chain(), wallet->GetName())) {
throw JSONRPCError(RPC_MISC_ERROR, "Wallet loaded, but load-on-startup list could not be written, so wallet "
"may be not loaded on node restart.");
}

UniValue obj(UniValue::VOBJ);
obj.pushKV("name", wallet->GetName());
obj.pushKV("warning", Join(warnings, "\n"));
Expand Down Expand Up @@ -2778,7 +2801,12 @@ static UniValue unloadwallet(const JSONRPCRequest& request)
throw JSONRPCError(RPC_MISC_ERROR, "Requested wallet already unloaded");
}

interfaces::Chain& chain = wallet->chain();
UnloadWallet(std::move(wallet));
if (!RemoveWalletSetting(chain, wallet_name)) {
throw JSONRPCError(RPC_MISC_ERROR, "Wallet unloaded, but load-on-startup list could not be written, so wallet "
"may be reloaded on node restart.");
}

return NullUniValue;
}
Expand Down Expand Up @@ -4240,7 +4268,7 @@ static const CRPCCommand commands[] =
{ "wallet", "addmultisigaddress", &addmultisigaddress, {"nrequired","keys","label","address_type"} },
{ "wallet", "backupwallet", &backupwallet, {"destination"} },
{ "wallet", "bumpfee", &bumpfee, {"txid", "options"} },
{ "wallet", "createwallet", &createwallet, {"wallet_name", "disable_private_keys", "blank", "passphrase", "avoid_reuse"} },
{ "wallet", "createwallet", &createwallet, {"wallet_name", "disable_private_keys", "blank", "passphrase", "avoid_reuse", "load_on_startup"} },
{ "wallet", "dumpprivkey", &dumpprivkey, {"address"} },
{ "wallet", "dumpwallet", &dumpwallet, {"filename"} },
{ "wallet", "encryptwallet", &encryptwallet, {"passphrase"} },
Expand Down Expand Up @@ -4272,7 +4300,7 @@ static const CRPCCommand commands[] =
{ "wallet", "listunspent", &listunspent, {"minconf","maxconf","addresses","include_unsafe","query_options"} },
{ "wallet", "listwalletdir", &listwalletdir, {} },
{ "wallet", "listwallets", &listwallets, {} },
{ "wallet", "loadwallet", &loadwallet, {"filename"} },
{ "wallet", "loadwallet", &loadwallet, {"filename", "load_on_startup"} },
{ "wallet", "lockunspent", &lockunspent, {"unlock","transactions"} },
{ "wallet", "removeprunedfunds", &removeprunedfunds, {"txid"} },
{ "wallet", "rescanblockchain", &rescanblockchain, {"start_height", "stop_height"} },
Expand Down
1 change: 1 addition & 0 deletions test/functional/test_runner.py
Expand Up @@ -210,6 +210,7 @@
'p2p_node_network_limited.py',
'p2p_permissions.py',
'feature_blocksdir.py',
'wallet_startup.py',
'feature_config_args.py',
'rpc_help.py',
'feature_help.py',
Expand Down
41 changes: 41 additions & 0 deletions test/functional/wallet_startup.py
@@ -0,0 +1,41 @@
#!/usr/bin/env python3
# Copyright (c) 2017-2019 The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""Test wallet load on startup.
Verify that a bitcoind node can maintain list of wallets loading on startup
"""
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import (
assert_equal,
)


class WalletStartupTest(BitcoinTestFramework):
def set_test_params(self):
self.setup_clean_chain = True
self.num_nodes = 1
self.supports_cli = True

def skip_test_if_missing_module(self):
self.skip_if_no_wallet()

def run_test(self):
node = self.nodes[0]

self.nodes[0].createwallet(wallet_name='w0', load_on_startup=True)
self.nodes[0].createwallet(wallet_name='w1', load_on_startup=False)
self.nodes[0].createwallet(wallet_name='w2', load_on_startup=True)
self.nodes[0].createwallet(wallet_name='w3', load_on_startup=False)
self.nodes[0].createwallet(wallet_name='w4', load_on_startup=False)
self.nodes[0].unloadwallet(wallet_name='w0')
self.nodes[0].unloadwallet(wallet_name='w4')
self.nodes[0].loadwallet(filename='w4', load_on_startup=True)
assert_equal(set(node.listwallets()), set(('', 'w1', 'w2', 'w3', 'w4')))
self.stop_nodes()
self.start_node(0, [])
assert_equal(set(node.listwallets()), set(('w2', 'w4')))

if __name__ == '__main__':
WalletStartupTest().main()

0 comments on commit 5ccdb04

Please sign in to comment.