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 bitcoin#15006, but it's reasonable to expose this feature by RPC as well.
  • Loading branch information
ryanofsky committed Jul 13, 2020
1 parent 1b8f9f4 commit 65bcf8e
Show file tree
Hide file tree
Showing 10 changed files with 158 additions and 8 deletions.
21 changes: 21 additions & 0 deletions src/interfaces/chain.cpp
Expand Up @@ -372,6 +372,27 @@ class ChainImpl : public Chain
RPCRunLater(name, std::move(fn), seconds);
}
int rpcSerializationFlags() override { return RPCSerializationFlags(); }
util::SettingsValue getRwSetting(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 updateRwSetting(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 <functional>
#include <memory>
Expand Down Expand Up @@ -269,6 +270,12 @@ class Chain
//! Current RPC serialization flags.
virtual int rpcSerializationFlags() = 0;

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

//! Write a setting to <datadir>/settings.json.
virtual bool updateRwSetting(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
3 changes: 3 additions & 0 deletions src/rpc/client.cpp
Expand Up @@ -172,6 +172,9 @@ static const CRPCConvertParam vRPCConvertParams[] =
{ "createwallet", 2, "blank"},
{ "createwallet", 4, "avoid_reuse"},
{ "createwallet", 5, "descriptors"},
{ "createwallet", 6, "load_on_startup"},
{ "loadwallet", 1, "load_on_startup"},
{ "unloadwallet", 1, "load_on_startup"},
{ "getnodeaddresses", 0, "count"},
{ "stop", 0, "wait" },
};
Expand Down
11 changes: 10 additions & 1 deletion src/wallet/init.cpp
Expand Up @@ -9,6 +9,7 @@
#include <node/context.h>
#include <node/ui_interface.h>
#include <outputtype.h>
#include <univalue.h>
#include <util/check.h>
#include <util/moneystr.h>
#include <util/system.h>
Expand Down Expand Up @@ -118,6 +119,14 @@ void WalletInit::Construct(NodeContext& node) const
LogPrintf("Wallet disabled!\n");
return;
}
args.SoftSetArg("-wallet", "");
// If there's no wallet setting, initialize it with a default wallet in list
// of wallets to load.
if (!args.IsArgSet("wallet")) {
args.LockSettings([&](util::Settings& settings) {
util::SettingsValue wallets(util::SettingsValue::VARR);
wallets.push_back(""); // Default wallet name is ""
settings.rw_settings["wallet"] = wallets;
});
}
node.chain_clients.emplace_back(interfaces::MakeWalletClient(*node.chain, args, args.GetArgs("-wallet")));
}
25 changes: 25 additions & 0 deletions src/wallet/load.cpp
Expand Up @@ -13,6 +13,8 @@
#include <wallet/wallet.h>
#include <wallet/walletdb.h>

#include <univalue.h>

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

bool AddWalletSetting(interfaces::Chain& chain, const std::string& wallet_name)
{
util::SettingsValue setting_value = chain.getRwSetting("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.updateRwSetting("wallet", setting_value);
}

bool RemoveWalletSetting(interfaces::Chain& chain, const std::string& wallet_name)
{
util::SettingsValue setting_value = chain.getRwSetting("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.updateRwSetting("wallet", new_value);
}
6 changes: 6 additions & 0 deletions src/wallet/load.h
Expand Up @@ -34,4 +34,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
38 changes: 33 additions & 5 deletions src/wallet/rpcwallet.cpp
Expand Up @@ -30,6 +30,7 @@
#include <wallet/coincontrol.h>
#include <wallet/context.h>
#include <wallet/feebumper.h>
#include <wallet/load.h>
#include <wallet/rpcwallet.h>
#include <wallet/wallet.h>
#include <wallet/walletdb.h>
Expand Down Expand Up @@ -229,6 +230,18 @@ static void SetFeeEstimateMode(const CWallet* pwallet, CCoinControl& cc, const U
}
}

static void UpdateWalletSetting(interfaces::Chain& chain,
const std::string& wallet_name,
const UniValue& load_on_startup,
std::vector<bilingual_str>& warnings)
{
if (load_on_startup.isTrue() && !AddWalletSetting(chain, wallet_name)) {
warnings.emplace_back(Untranslated("Wallet list setting could not be written, so wallet may be not loaded on node restart."));
} else if (load_on_startup.isFalse() && !RemoveWalletSetting(chain, wallet_name)) {
warnings.emplace_back(Untranslated("Wallet list setting could not be written, so wallet may be reloaded on node restart."));
}
}

static UniValue getnewaddress(const JSONRPCRequest& request)
{
RPCHelpMan{"getnewaddress",
Expand Down Expand Up @@ -2483,6 +2496,7 @@ static UniValue loadwallet(const JSONRPCRequest& request)
"\napplied to the new wallet (eg -zapwallettxes, rescan, etc).\n",
{
{"filename", RPCArg::Type::STR, RPCArg::Optional::NO, "The wallet directory or .dat file."},
{"load_on_startup", RPCArg::Type::BOOL, /* default */ "null", "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, "", "",
Expand Down Expand Up @@ -2515,6 +2529,8 @@ static UniValue loadwallet(const JSONRPCRequest& request)
std::shared_ptr<CWallet> const wallet = LoadWallet(*context.chain, location, error, warnings);
if (!wallet) throw JSONRPCError(RPC_WALLET_ERROR, error.original);

UpdateWalletSetting(*context.chain, location.GetName(), request.params[1], warnings);

UniValue obj(UniValue::VOBJ);
obj.pushKV("name", wallet->GetName());
obj.pushKV("warning", Join(warnings, Untranslated("\n")).original);
Expand Down Expand Up @@ -2599,6 +2615,7 @@ static UniValue createwallet(const JSONRPCRequest& request)
{"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."},
{"descriptors", RPCArg::Type::BOOL, /* default */ "false", "Create a native descriptor wallet. The wallet will use descriptors internally to handle address creation"},
{"load_on_startup", RPCArg::Type::BOOL, /* default */ "null", "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, "", "",
Expand Down Expand Up @@ -2654,6 +2671,8 @@ static UniValue createwallet(const JSONRPCRequest& request)
// no default case, so the compiler can warn about missing cases
}

UpdateWalletSetting(*context.chain, request.params[0].get_str(), request.params[6], warnings);

UniValue obj(UniValue::VOBJ);
obj.pushKV("name", wallet->GetName());
obj.pushKV("warning", Join(warnings, Untranslated("\n")).original);
Expand All @@ -2668,8 +2687,11 @@ static UniValue unloadwallet(const JSONRPCRequest& request)
"Specifying the wallet name on a wallet endpoint is invalid.",
{
{"wallet_name", RPCArg::Type::STR, /* default */ "the wallet name from the RPC request", "The name of the wallet to unload."},
{"load_on_startup", RPCArg::Type::BOOL, /* default */ "null", "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::NONE, "", ""},
RPCResult{RPCResult::Type::OBJ, "", "", {
{RPCResult::Type::STR, "warning", "Warning message if wallet was not unloaded cleanly."},
}},
RPCExamples{
HelpExampleCli("unloadwallet", "wallet_name")
+ HelpExampleRpc("unloadwallet", "wallet_name")
Expand Down Expand Up @@ -2697,9 +2719,15 @@ static UniValue unloadwallet(const JSONRPCRequest& request)
throw JSONRPCError(RPC_MISC_ERROR, "Requested wallet already unloaded");
}

interfaces::Chain& chain = wallet->chain();
std::vector<bilingual_str> warnings;

UnloadWallet(std::move(wallet));
UpdateWalletSetting(chain, wallet_name, request.params[1], warnings);

return NullUniValue;
UniValue result(UniValue::VOBJ);
result.pushKV("warning", Join(warnings, Untranslated("\n")).original);
return result;
}

static UniValue listunspent(const JSONRPCRequest& request)
Expand Down Expand Up @@ -4137,7 +4165,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", "descriptors"} },
{ "wallet", "createwallet", &createwallet, {"wallet_name", "disable_private_keys", "blank", "passphrase", "avoid_reuse", "descriptors", "load_on_startup"} },
{ "wallet", "dumpprivkey", &dumpprivkey, {"address"} },
{ "wallet", "dumpwallet", &dumpwallet, {"filename"} },
{ "wallet", "encryptwallet", &encryptwallet, {"passphrase"} },
Expand Down Expand Up @@ -4170,7 +4198,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 All @@ -4182,7 +4210,7 @@ static const CRPCCommand commands[] =
{ "wallet", "setwalletflag", &setwalletflag, {"flag","value"} },
{ "wallet", "signmessage", &signmessage, {"address","message"} },
{ "wallet", "signrawtransactionwithwallet", &signrawtransactionwithwallet, {"hexstring","prevtxs","sighashtype"} },
{ "wallet", "unloadwallet", &unloadwallet, {"wallet_name"} },
{ "wallet", "unloadwallet", &unloadwallet, {"wallet_name", "load_on_startup"} },
{ "wallet", "upgradewallet", &upgradewallet, {"version"} },
{ "wallet", "walletcreatefundedpsbt", &walletcreatefundedpsbt, {"inputs","outputs","locktime","options","bip32derivs"} },
{ "wallet", "walletlock", &walletlock, {} },
Expand Down
4 changes: 2 additions & 2 deletions test/functional/test_framework/test_node.py
Expand Up @@ -649,10 +649,10 @@ def __init__(self, rpc, cli=False, descriptors=False):
def __getattr__(self, name):
return getattr(self.rpc, name)

def createwallet(self, wallet_name, disable_private_keys=None, blank=None, passphrase='', avoid_reuse=None, descriptors=None):
def createwallet(self, wallet_name, disable_private_keys=None, blank=None, passphrase='', avoid_reuse=None, descriptors=None, load_on_startup=None):
if descriptors is None:
descriptors = self.descriptors
return self.__getattr__('createwallet')(wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors)
return self.__getattr__('createwallet')(wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors, load_on_startup)

def importprivkey(self, privkey, label=None, rescan=None):
wallet_info = self.getwalletinfo()
Expand Down
1 change: 1 addition & 0 deletions test/functional/test_runner.py
Expand Up @@ -242,6 +242,7 @@
'p2p_node_network_limited.py',
'p2p_permissions.py',
'feature_blocksdir.py',
'wallet_startup.py',
'feature_config_args.py',
'feature_settings.py',
'rpc_getdescriptorinfo.py',
Expand Down
50 changes: 50 additions & 0 deletions test/functional/wallet_startup.py
@@ -0,0 +1,50 @@
#!/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 setup_nodes(self):
self.add_nodes(self.num_nodes)
self.start_nodes()

def run_test(self):
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', load_on_startup=False)
self.nodes[0].unloadwallet(wallet_name='w4', load_on_startup=False)
self.nodes[0].loadwallet(filename='w4', load_on_startup=True)
assert_equal(set(self.nodes[0].listwallets()), set(('', 'w1', 'w2', 'w3', 'w4')))
self.stop_nodes()
self.start_node(0)
assert_equal(set(self.nodes[0].listwallets()), set(('', 'w2', 'w4')))
self.nodes[0].unloadwallet(wallet_name='', load_on_startup=False)
self.nodes[0].unloadwallet(wallet_name='w4', load_on_startup=False)
self.nodes[0].loadwallet(filename='w3', load_on_startup=True)
self.nodes[0].loadwallet(filename='', load_on_startup=False)
self.stop_nodes()
self.start_node(0)
assert_equal(set(self.nodes[0].listwallets()), set(('w2', 'w3')))

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

0 comments on commit 65bcf8e

Please sign in to comment.