From 59b87a27efea819e433c727756bf5fac57b33dd6 Mon Sep 17 00:00:00 2001 From: John Newbery <john@johnnewbery.com> Date: Mon, 7 May 2018 17:08:03 -0400 Subject: [PATCH 1/7] [wallet] Fix potential memory leak in CreateWalletFromFile Fix proposed by ryanofsky in https://github.com/bitcoin/bitcoin/pull/12647#discussion_r174875670 --- src/wallet/wallet.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 5be9f4a29..f9f567009 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -4012,7 +4012,10 @@ CWallet* CWallet::CreateWalletFromFile(const std::string& name, const fs::path& int64_t nStart = GetTimeMillis(); bool fFirstRun = true; - CWallet *walletInstance = new CWallet(name, WalletDatabase::Create(path)); + // Make a temporary wallet unique pointer so memory doesn't get leaked if + // wallet creation fails. + auto temp_wallet = MakeUnique<CWallet>(name, WalletDatabase::Create(path)); + CWallet* walletInstance = temp_wallet.get(); DBErrors nLoadWalletRet = walletInstance->LoadWallet(fFirstRun); if (nLoadWalletRet != DBErrors::LOAD_OK) { @@ -4224,7 +4227,6 @@ CWallet* CWallet::CreateWalletFromFile(const std::string& name, const fs::path& } walletInstance->m_last_block_processed = chainActive.Tip(); - RegisterValidationInterface(walletInstance); if (chainActive.Tip() && chainActive.Tip() != pindexRescan) { @@ -4290,6 +4292,10 @@ CWallet* CWallet::CreateWalletFromFile(const std::string& name, const fs::path& } } } + + // Register with the validation interface. It's ok to do this after rescan since we're still holding cs_main. + RegisterValidationInterface(temp_wallet.release()); + walletInstance->SetBroadcastTransactions(gArgs.GetBoolArg("-walletbroadcast", DEFAULT_WALLETBROADCAST)); { From 470316c3bf5ca343d5d66b94839169a4572eceb7 Mon Sep 17 00:00:00 2001 From: John Newbery <john@johnnewbery.com> Date: Thu, 19 Apr 2018 17:42:40 -0400 Subject: [PATCH 2/7] [wallet] setup wallet background flushing in WalletInit directly WalletInit::Start calls postInitProcess() for each wallet. Previously each call to postInitProcess() would attempt to schedule wallet background flushing. Just start wallet background flushing once from WalletInit::Start(). --- src/wallet/init.cpp | 6 +++++- src/wallet/wallet.cpp | 10 +--------- src/wallet/wallet.h | 4 +--- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/src/wallet/init.cpp b/src/wallet/init.cpp index 6c5522e4b..e9710012b 100644 --- a/src/wallet/init.cpp +++ b/src/wallet/init.cpp @@ -6,6 +6,7 @@ #include <chainparams.h> #include <init.h> #include <net.h> +#include <scheduler.h> #include <util.h> #include <utilmoneystr.h> #include <validation.h> @@ -264,8 +265,11 @@ bool WalletInit::Open() const void WalletInit::Start(CScheduler& scheduler) const { for (CWallet* pwallet : GetWallets()) { - pwallet->postInitProcess(scheduler); + pwallet->postInitProcess(); } + + // Run a thread to flush wallet periodically + scheduler.scheduleEvery(MaybeCompactWalletDB, 500); } void WalletInit::Flush() const diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index f9f567009..4d3e3813a 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -23,7 +23,6 @@ #include <primitives/block.h> #include <primitives/transaction.h> #include <script/script.h> -#include <scheduler.h> #include <timedata.h> #include <txmempool.h> #include <utilmoneystr.h> @@ -4308,18 +4307,11 @@ CWallet* CWallet::CreateWalletFromFile(const std::string& name, const fs::path& return walletInstance; } -std::atomic<bool> CWallet::fFlushScheduled(false); - -void CWallet::postInitProcess(CScheduler& scheduler) +void CWallet::postInitProcess() { // Add wallet transactions that aren't already in a block to mempool // Do this here as mempool requires genesis block to be loaded ReacceptWalletTransactions(); - - // Run a thread to flush wallet periodically - if (!CWallet::fFlushScheduled.exchange(true)) { - scheduler.scheduleEvery(MaybeCompactWalletDB, 500); - } } bool CWallet::BackupWallet(const std::string& strDest) diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index 1e23b44ea..ebf14d438 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -68,7 +68,6 @@ class CCoinControl; class COutput; class CReserveKey; class CScript; -class CScheduler; class CTxMemPool; class CBlockPolicyEstimator; class CWalletTx; @@ -675,7 +674,6 @@ class WalletRescanReserver; //forward declarations for ScanForWalletTransactions class CWallet final : public CCryptoKeyStore, public CValidationInterface { private: - static std::atomic<bool> fFlushScheduled; std::atomic<bool> fAbortRescan{false}; std::atomic<bool> fScanningWallet{false}; // controlled by WalletRescanReserver std::mutex mutexScanning; @@ -1127,7 +1125,7 @@ public: * Wallet post-init setup * Gives the wallet a chance to register repetitive tasks and complete post-init tasks */ - void postInitProcess(CScheduler& scheduler); + void postInitProcess(); bool BackupWallet(const std::string& strDest); From e0e90db07b4e798dd1625bd23c2e9bd96fc6ff49 Mon Sep 17 00:00:00 2001 From: John Newbery <john@johnnewbery.com> Date: Wed, 18 Apr 2018 13:11:28 -0400 Subject: [PATCH 3/7] [wallet] Add CWallet::Verify function This allows a single wallet to be verified. Prior to this commit, all wallets were verified together by the WalletInit::Verify() function at start-up. Individual wallet verification will be done when loading wallets dynamically at runtime. --- src/wallet/init.cpp | 53 +++++++++------------------------------- src/wallet/wallet.cpp | 56 +++++++++++++++++++++++++++++++++++++++++++ src/wallet/wallet.h | 3 +++ 3 files changed, 70 insertions(+), 42 deletions(-) diff --git a/src/wallet/init.cpp b/src/wallet/init.cpp index e9710012b..01c927f03 100644 --- a/src/wallet/init.cpp +++ b/src/wallet/init.cpp @@ -190,55 +190,24 @@ bool WalletInit::Verify() const uiInterface.InitMessage(_("Verifying wallet(s)...")); + std::vector<std::string> wallet_files = gArgs.GetArgs("-wallet"); + + // Parameter interaction code should have thrown an error if -salvagewallet + // was enabled with more than wallet file, so the wallet_files size check + // here should have no effect. + bool salvage_wallet = gArgs.GetBoolArg("-salvagewallet", false) && wallet_files.size() <= 1; + // Keep track of each wallet absolute path to detect duplicates. std::set<fs::path> wallet_paths; - for (const std::string& walletFile : gArgs.GetArgs("-wallet")) { - // Do some checking on wallet path. It should be either a: - // - // 1. Path where a directory can be created. - // 2. Path to an existing directory. - // 3. Path to a symlink to a directory. - // 4. For backwards compatibility, the name of a data file in -walletdir. - fs::path wallet_path = fs::absolute(walletFile, GetWalletDir()); - fs::file_type path_type = fs::symlink_status(wallet_path).type(); - if (!(path_type == fs::file_not_found || path_type == fs::directory_file || - (path_type == fs::symlink_file && fs::is_directory(wallet_path)) || - (path_type == fs::regular_file && fs::path(walletFile).filename() == walletFile))) { - return InitError(strprintf( - _("Invalid -wallet path '%s'. -wallet path should point to a directory where wallet.dat and " - "database/log.?????????? files can be stored, a location where such a directory could be created, " - "or (for backwards compatibility) the name of an existing data file in -walletdir (%s)"), - walletFile, GetWalletDir())); - } + for (const auto wallet_file : wallet_files) { + fs::path wallet_path = fs::absolute(wallet_file, GetWalletDir()); if (!wallet_paths.insert(wallet_path).second) { - return InitError(strprintf(_("Error loading wallet %s. Duplicate -wallet filename specified."), walletFile)); + return InitError(strprintf(_("Error loading wallet %s. Duplicate -wallet filename specified."), wallet_file)); } - std::string strError; - if (!WalletBatch::VerifyEnvironment(wallet_path, strError)) { - return InitError(strError); - } - - if (gArgs.GetBoolArg("-salvagewallet", false)) { - // Recover readable keypairs: - CWallet dummyWallet("dummy", WalletDatabase::CreateDummy()); - std::string backup_filename; - if (!WalletBatch::Recover(wallet_path, (void *)&dummyWallet, WalletBatch::RecoverKeysOnlyFilter, backup_filename)) { - return false; - } - } - - std::string strWarning; - bool dbV = WalletBatch::VerifyDatabaseFile(wallet_path, strWarning, strError); - if (!strWarning.empty()) { - InitWarning(strWarning); - } - if (!dbV) { - InitError(strError); - return false; - } + if (!CWallet::Verify(wallet_file, salvage_wallet)) return false; } return true; diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 4d3e3813a..d4bbb7f06 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -27,6 +27,7 @@ #include <txmempool.h> #include <utilmoneystr.h> #include <wallet/fees.h> +#include <wallet/walletutil.h> #include <algorithm> #include <assert.h> @@ -3989,6 +3990,61 @@ void CWallet::MarkPreSplitKeys() } } +bool CWallet::Verify(std::string wallet_file, bool salvage_wallet) +{ + // Do some checking on wallet path. It should be either a: + // + // 1. Path where a directory can be created. + // 2. Path to an existing directory. + // 3. Path to a symlink to a directory. + // 4. For backwards compatibility, the name of a data file in -walletdir. + LOCK(cs_wallets); + fs::path wallet_path = fs::absolute(wallet_file, GetWalletDir()); + fs::file_type path_type = fs::symlink_status(wallet_path).type(); + if (!(path_type == fs::file_not_found || path_type == fs::directory_file || + (path_type == fs::symlink_file && fs::is_directory(wallet_path)) || + (path_type == fs::regular_file && fs::path(wallet_file).filename() == wallet_file))) { + return InitError(strprintf( + _("Invalid -wallet path '%s'. -wallet path should point to a directory where wallet.dat and " + "database/log.?????????? files can be stored, a location where such a directory could be created, " + "or (for backwards compatibility) the name of an existing data file in -walletdir (%s)"), + wallet_file, GetWalletDir())); + } + + // Make sure that the wallet path doesn't clash with an existing wallet path + for (auto wallet : GetWallets()) { + if (fs::absolute(wallet->GetName(), GetWalletDir()) == wallet_path) { + return InitError(strprintf(_("Error loading wallet %s. Duplicate -wallet filename specified."), wallet_file)); + } + } + + std::string strError; + if (!WalletBatch::VerifyEnvironment(wallet_path, strError)) { + return InitError(strError); + } + + if (salvage_wallet) { + // Recover readable keypairs: + CWallet dummyWallet("dummy", WalletDatabase::CreateDummy()); + std::string backup_filename; + if (!WalletBatch::Recover(wallet_path, (void *)&dummyWallet, WalletBatch::RecoverKeysOnlyFilter, backup_filename)) { + return false; + } + } + + std::string strWarning; + bool dbV = WalletBatch::VerifyDatabaseFile(wallet_path, strWarning, strError); + if (!strWarning.empty()) { + InitWarning(strWarning); + } + if (!dbV) { + InitError(strError); + return false; + } + + return true; +} + CWallet* CWallet::CreateWalletFromFile(const std::string& name, const fs::path& path) { const std::string& walletFile = name; diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index ebf14d438..200248bcf 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -1118,6 +1118,9 @@ public: /** Mark a transaction as replaced by another transaction (e.g., BIP 125). */ bool MarkReplaced(const uint256& originalHash, const uint256& newHash); + //! Verify wallet naming and perform salvage on the wallet if required + static bool Verify(std::string wallet_file, bool salvage_wallet); + /* Initializes the wallet, returns a new CWallet instance or a null pointer in case of an error */ static CWallet* CreateWalletFromFile(const std::string& name, const fs::path& path); From 876eb64680968c8fe2a28d1ecfd88a08d8967ead Mon Sep 17 00:00:00 2001 From: John Newbery <john@johnnewbery.com> Date: Wed, 18 Apr 2018 14:17:09 -0400 Subject: [PATCH 4/7] [wallet] Pass error message back from CWallet::Verify() Pass an error message back from CWallet::Verify(), and call InitError/InitWarning from WalletInit::Verify(). This means that we can call CWallet::Verify() independently from WalletInit and not have InitErrors printed to stdout. It also means that the error can be reported to the user if dynamic wallet load fails. --- src/wallet/init.cpp | 7 ++++++- src/wallet/wallet.cpp | 31 +++++++++++-------------------- src/wallet/wallet.h | 2 +- 3 files changed, 18 insertions(+), 22 deletions(-) diff --git a/src/wallet/init.cpp b/src/wallet/init.cpp index 01c927f03..5cfa86451 100644 --- a/src/wallet/init.cpp +++ b/src/wallet/init.cpp @@ -207,7 +207,12 @@ bool WalletInit::Verify() const return InitError(strprintf(_("Error loading wallet %s. Duplicate -wallet filename specified."), wallet_file)); } - if (!CWallet::Verify(wallet_file, salvage_wallet)) return false; + std::string error_string; + std::string warning_string; + bool verify_success = CWallet::Verify(wallet_file, salvage_wallet, error_string, warning_string); + if (!error_string.empty()) InitError(error_string); + if (!warning_string.empty()) InitWarning(warning_string); + if (!verify_success) return false; } return true; diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index d4bbb7f06..74f36e9ab 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -3990,7 +3990,7 @@ void CWallet::MarkPreSplitKeys() } } -bool CWallet::Verify(std::string wallet_file, bool salvage_wallet) +bool CWallet::Verify(std::string wallet_file, bool salvage_wallet, std::string& error_string, std::string& warning_string) { // Do some checking on wallet path. It should be either a: // @@ -4004,23 +4004,24 @@ bool CWallet::Verify(std::string wallet_file, bool salvage_wallet) if (!(path_type == fs::file_not_found || path_type == fs::directory_file || (path_type == fs::symlink_file && fs::is_directory(wallet_path)) || (path_type == fs::regular_file && fs::path(wallet_file).filename() == wallet_file))) { - return InitError(strprintf( - _("Invalid -wallet path '%s'. -wallet path should point to a directory where wallet.dat and " + error_string = strprintf( + "Invalid -wallet path '%s'. -wallet path should point to a directory where wallet.dat and " "database/log.?????????? files can be stored, a location where such a directory could be created, " - "or (for backwards compatibility) the name of an existing data file in -walletdir (%s)"), - wallet_file, GetWalletDir())); + "or (for backwards compatibility) the name of an existing data file in -walletdir (%s)", + wallet_file, GetWalletDir()); + return false; } // Make sure that the wallet path doesn't clash with an existing wallet path for (auto wallet : GetWallets()) { if (fs::absolute(wallet->GetName(), GetWalletDir()) == wallet_path) { - return InitError(strprintf(_("Error loading wallet %s. Duplicate -wallet filename specified."), wallet_file)); + error_string = strprintf("Error loading wallet %s. Duplicate -wallet filename specified.", wallet_file); + return false; } } - std::string strError; - if (!WalletBatch::VerifyEnvironment(wallet_path, strError)) { - return InitError(strError); + if (!WalletBatch::VerifyEnvironment(wallet_path, error_string)) { + return false; } if (salvage_wallet) { @@ -4032,17 +4033,7 @@ bool CWallet::Verify(std::string wallet_file, bool salvage_wallet) } } - std::string strWarning; - bool dbV = WalletBatch::VerifyDatabaseFile(wallet_path, strWarning, strError); - if (!strWarning.empty()) { - InitWarning(strWarning); - } - if (!dbV) { - InitError(strError); - return false; - } - - return true; + return WalletBatch::VerifyDatabaseFile(wallet_path, warning_string, error_string); } CWallet* CWallet::CreateWalletFromFile(const std::string& name, const fs::path& path) diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index 200248bcf..fecc2178e 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -1119,7 +1119,7 @@ public: bool MarkReplaced(const uint256& originalHash, const uint256& newHash); //! Verify wallet naming and perform salvage on the wallet if required - static bool Verify(std::string wallet_file, bool salvage_wallet); + static bool Verify(std::string wallet_file, bool salvage_wallet, std::string& error_string, std::string& warning_string); /* Initializes the wallet, returns a new CWallet instance or a null pointer in case of an error */ static CWallet* CreateWalletFromFile(const std::string& name, const fs::path& path); From 5d152601e940bd5f4043253b216a645679aff75d Mon Sep 17 00:00:00 2001 From: John Newbery <john@johnnewbery.com> Date: Wed, 18 Apr 2018 16:01:39 -0400 Subject: [PATCH 5/7] [wallet] [rpc] Add loadwallet RPC The new `loadwallet` RPC method allows an existing wallet to be loaded dynamically at runtime. `unloadwallet` and `createwallet` are not implemented. Notably, `loadwallet` can only be used to load existing wallets, not to create a new wallet. --- src/wallet/rpcwallet.cpp | 48 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index a7b2b05d9..8cd395283 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -2994,6 +2994,53 @@ static UniValue listwallets(const JSONRPCRequest& request) return obj; } +UniValue loadwallet(const JSONRPCRequest& request) +{ + if (request.fHelp || request.params.size() != 1) + throw std::runtime_error( + "loadwallet \"filename\"\n" + "\nLoads a wallet from a wallet file or directory." + "\nNote that all wallet command-line options used when starting bitcoind will be" + "\napplied to the new wallet (eg -zapwallettxes, upgradewallet, rescan, etc).\n" + "\nArguments:\n" + "1. \"filename\" (string, required) The wallet directory or .dat file.\n" + "\nResult:\n" + "{\n" + " \"name\" : <wallet_name>, (string) The wallet name if loaded successfully.\n" + " \"warning\" : <warning>, (string) Warning message if wallet was not loaded cleanly.\n" + "}\n" + "\nExamples:\n" + + HelpExampleCli("loadwallet", "\"test.dat\"") + + HelpExampleRpc("loadwallet", "\"test.dat\"") + ); + std::string wallet_file = request.params[0].get_str(); + std::string error; + + fs::path wallet_path = fs::absolute(wallet_file, GetWalletDir()); + if (fs::symlink_status(wallet_path).type() == fs::file_not_found) { + throw JSONRPCError(RPC_WALLET_NOT_FOUND, "Wallet " + wallet_file + " not found."); + } + + std::string warning; + if (!CWallet::Verify(wallet_file, false, error, warning)) { + throw JSONRPCError(RPC_WALLET_ERROR, "Wallet file verification failed: " + error); + } + + CWallet * const wallet = CWallet::CreateWalletFromFile(wallet_file, fs::absolute(wallet_file, GetWalletDir())); + if (!wallet) { + throw JSONRPCError(RPC_WALLET_ERROR, "Wallet loading failed."); + } + AddWallet(wallet); + + wallet->postInitProcess(); + + UniValue obj(UniValue::VOBJ); + obj.pushKV("name", wallet->GetName()); + obj.pushKV("warning", warning); + + return obj; +} + static UniValue resendwallettransactions(const JSONRPCRequest& request) { CWallet * const pwallet = GetWalletForJSONRPCRequest(request); @@ -4197,6 +4244,7 @@ static const CRPCCommand commands[] = { "wallet", "listtransactions", &listtransactions, {"account|dummy","count","skip","include_watchonly"} }, { "wallet", "listunspent", &listunspent, {"minconf","maxconf","addresses","include_unsafe","query_options"} }, { "wallet", "listwallets", &listwallets, {} }, + { "wallet", "loadwallet", &loadwallet, {"filename"} }, { "wallet", "lockunspent", &lockunspent, {"unlock","transactions"} }, { "wallet", "sendfrom", &sendfrom, {"fromaccount","toaddress","amount","minconf","comment","comment_to"} }, { "wallet", "sendmany", &sendmany, {"fromaccount|dummy","amounts","minconf","comment","subtractfeefrom","replaceable","conf_target","estimate_mode"} }, From a46aeb690141f8457cf7d19f5b4b84f97ce410c8 Mon Sep 17 00:00:00 2001 From: John Newbery <john@johnnewbery.com> Date: Wed, 18 Apr 2018 16:32:16 -0400 Subject: [PATCH 6/7] [wallet] [tests] Test loadwallet Add testcases to wallet_multiwallet.py to test the new `loadwallet` RPC method. --- test/functional/wallet_multiwallet.py | 41 +++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/test/functional/wallet_multiwallet.py b/test/functional/wallet_multiwallet.py index e0571ea8f..567177352 100755 --- a/test/functional/wallet_multiwallet.py +++ b/test/functional/wallet_multiwallet.py @@ -170,5 +170,46 @@ class MultiWalletTest(BitcoinTestFramework): assert_equal(w1.getwalletinfo()['paytxfee'], 0) assert_equal(w2.getwalletinfo()['paytxfee'], 4.0) + self.log.info("Test dynamic wallet loading") + + self.restart_node(0, ['-nowallet']) + assert_equal(node.listwallets(), []) + assert_raises_rpc_error(-32601, "Method not found", node.getwalletinfo) + + self.log.info("Load first wallet") + loadwallet_name = node.loadwallet(wallet_names[0]) + assert_equal(loadwallet_name['name'], wallet_names[0]) + assert_equal(node.listwallets(), wallet_names[0:1]) + node.getwalletinfo() + w1 = node.get_wallet_rpc(wallet_names[0]) + w1.getwalletinfo() + + self.log.info("Load second wallet") + loadwallet_name = node.loadwallet(wallet_names[1]) + assert_equal(loadwallet_name['name'], wallet_names[1]) + assert_equal(node.listwallets(), wallet_names[0:2]) + assert_raises_rpc_error(-19, "Wallet file not specified", node.getwalletinfo) + w2 = node.get_wallet_rpc(wallet_names[1]) + w2.getwalletinfo() + + self.log.info("Load remaining wallets") + for wallet_name in wallet_names[2:]: + loadwallet_name = self.nodes[0].loadwallet(wallet_name) + assert_equal(loadwallet_name['name'], wallet_name) + + assert_equal(set(self.nodes[0].listwallets()), set(wallet_names)) + + # Fail to load if wallet doesn't exist + assert_raises_rpc_error(-18, 'Wallet wallets not found.', self.nodes[0].loadwallet, 'wallets') + + # Fail to load duplicate wallets + assert_raises_rpc_error(-4, 'Wallet file verification failed: Error loading wallet w1. Duplicate -wallet filename specified.', self.nodes[0].loadwallet, wallet_names[0]) + + # Fail to load if one wallet is a copy of another + assert_raises_rpc_error(-1, "BerkeleyBatch: Can't open database w8_copy (duplicates fileid", self.nodes[0].loadwallet, 'w8_copy') + + # Fail to load if wallet file is a symlink + assert_raises_rpc_error(-4, "Wallet file verification failed: Invalid -wallet path 'w8_symlink'", self.nodes[0].loadwallet, 'w8_symlink') + if __name__ == '__main__': MultiWalletTest().main() From cd53981b3d0d4697ed46c7bedbf10f464aca4ccc Mon Sep 17 00:00:00 2001 From: John Newbery <john@johnnewbery.com> Date: Wed, 18 Apr 2018 16:51:53 -0400 Subject: [PATCH 7/7] [docs] Add release notes for `loadwallet` RPC. --- doc/release-notes-pr10740.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 doc/release-notes-pr10740.md diff --git a/doc/release-notes-pr10740.md b/doc/release-notes-pr10740.md new file mode 100644 index 000000000..a57cbc793 --- /dev/null +++ b/doc/release-notes-pr10740.md @@ -0,0 +1,8 @@ +Dynamic loading of wallets +-------------------------- + +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. + +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. + +This feature is currently only available through the RPC interface. Wallets loaded in this way will not display in the bitcoin-qt GUI.