diff --git a/doc/files.md b/doc/files.md index 928977143b..c22dda98a2 100644 --- a/doc/files.md +++ b/doc/files.md @@ -1,31 +1,34 @@ * banlist.dat: stores the IPs/Subnets of banned nodes * bitcoin.conf: contains configuration settings for bitcoind or bitcoin-qt * bitcoind.pid: stores the process id of bitcoind while running * blocks/blk000??.dat: block data (custom, 128 MiB per file); since 0.8.0 * blocks/rev000??.dat; block undo data (custom); since 0.8.0 (format changed since pre-0.8) * blocks/index/*; block index (LevelDB); since 0.8.0 * chainstate/*; block chain state database (LevelDB); since 0.8.0 -* database/*: BDB database environment; only used for wallet since 0.8.0 -* db.log: wallet database log file +* database/*: BDB database environment; only used for wallet since 0.8.0; moved to wallets/ directory on new installs since 0.18.7 +* db.log: wallet database log file; moved to wallets/ directory on new installs since 0.18.7 * debug.log: contains debug information and general logging generated by bitcoind or bitcoin-qt * fee_estimates.dat: stores statistics used to estimate minimum transaction fees and priorities required for confirmation; since 0.10.0 * mempool.dat: dump of the mempool's transactions; since 0.14.0. * peers.dat: peer IP address database (custom format); since 0.7.0 -* wallet.dat: personal wallet (BDB) with keys and transactions +* wallet.dat: personal wallet (BDB) with keys and transactions; moved to wallets/ directory on new installs since 0.18.7 +* wallets/database/*: BDB database environment; used for wallets since 0.18.7 +* wallets/db.log: wallet database log file; since 0.18.7 +* wallets/wallet.dat: personal wallet (BDB) with keys and transactions; since 0.18.7 * .cookie: session RPC authentication cookie (written at start when cookie authentication is used, deleted on shutdown): since 0.12.0 * onion_private_key: cached Tor hidden service private key for `-listenonion`: since 0.12.0 Only used in pre-0.8.0 --------------------- * blktree/*; block chain index (LevelDB); since pre-0.8, replaced by blocks/index/* in 0.8.0 * coins/*; unspent transaction output database (LevelDB); since pre-0.8, replaced by chainstate/* in 0.8.0 Only used before 0.8.0 --------------------- * blkindex.dat: block chain index database (BDB); replaced by {chainstate/*,blocks/index/*,blocks/rev000??.dat} in 0.8.0 * blk000?.dat: block data (custom, 2 GiB per file); replaced by blocks/blk000??.dat in 0.8.0 Only used before 0.7.0 --------------------- * addr.dat: peer IP address database (BDB); replaced by peers.dat in 0.7.0 diff --git a/src/wallet/init.cpp b/src/wallet/init.cpp index 2ea54615c4..0ca7bf5a97 100644 --- a/src/wallet/init.cpp +++ b/src/wallet/init.cpp @@ -1,424 +1,429 @@ // Copyright (c) 2009-2010 Satoshi Nakamoto // Copyright (c) 2009-2017 The Bitcoin Core developers // Copyright (c) 2018 The Bitcoin developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include "init.h" #include "config.h" #include "net.h" #include "util.h" #include "utilmoneystr.h" #include "validation.h" #include "wallet/rpcwallet.h" #include "wallet/wallet.h" #include "wallet/walletutil.h" #include "walletinitinterface.h" class WalletInit : public WalletInitInterface { public: //! Return the wallets help message. std::string GetHelpString(bool showDebug) override; //! Wallets parameter interaction bool ParameterInteraction() override; //! Register wallet RPCs. void RegisterRPC(CRPCTable &tableRPC) override; //! Responsible for reading and validating the -wallet arguments and //! verifying the wallet database. // This function will perform salvage on the wallet if requested, as long // as only one wallet is being loaded (WalletParameterInteraction forbids // -salvagewallet, -zapwallettxes or -upgradewallet with multiwallet). bool Verify(const CChainParams &chainParams) override; //! Load wallet databases. bool Open(const CChainParams &chainParams) override; //! Complete startup of wallets. void Start(CScheduler &scheduler) override; //! Flush all wallets in preparation for shutdown. void Flush() override; //! Stop all wallets. Wallets will be flushed first. void Stop() override; //! Close all wallets. void Close() override; }; static WalletInit g_wallet_init; WalletInitInterface *const g_wallet_init_interface = &g_wallet_init; std::string WalletInit::GetHelpString(bool showDebug) { std::string strUsage = HelpMessageGroup(_("Wallet options:")); strUsage += HelpMessageOpt( "-disablewallet", _("Do not load the wallet and disable wallet RPC calls")); strUsage += HelpMessageOpt( "-keypool=", strprintf(_("Set key pool size to (default: %u)"), DEFAULT_KEYPOOL_SIZE)); strUsage += HelpMessageOpt( "-fallbackfee=", strprintf(_("A fee rate (in %s/kB) that will be used when fee " "estimation has insufficient data (default: %s)"), CURRENCY_UNIT, FormatMoney(DEFAULT_FALLBACK_FEE))); strUsage += HelpMessageOpt( "-paytxfee=", strprintf( _("Fee (in %s/kB) to add to transactions you send (default: %s)"), CURRENCY_UNIT, FormatMoney(payTxFee.GetFeePerK()))); strUsage += HelpMessageOpt( "-rescan", _("Rescan the block chain for missing wallet transactions on startup")); strUsage += HelpMessageOpt( "-salvagewallet", _("Attempt to recover private keys from a corrupt wallet on startup")); strUsage += HelpMessageOpt("-spendzeroconfchange", strprintf(_("Spend unconfirmed change when sending " "transactions (default: %d)"), DEFAULT_SPEND_ZEROCONF_CHANGE)); strUsage += HelpMessageOpt("-txconfirmtarget=", strprintf(_("If paytxfee is not set, include enough fee " "so transactions begin confirmation on " "average within n blocks (default: %u)"), DEFAULT_TX_CONFIRM_TARGET)); strUsage += HelpMessageOpt( "-usehd", _("Use hierarchical deterministic key generation (HD) after BIP32. " "Only has effect during wallet creation/first start") + " " + strprintf(_("(default: %d)"), DEFAULT_USE_HD_WALLET)); strUsage += HelpMessageOpt("-upgradewallet", _("Upgrade wallet to latest format on startup")); strUsage += HelpMessageOpt("-wallet=", _("Specify wallet file (within data directory)") + " " + strprintf(_("(default: %s)"), DEFAULT_WALLET_DAT)); strUsage += HelpMessageOpt( "-walletbroadcast", _("Make the wallet broadcast transactions") + " " + strprintf(_("(default: %d)"), DEFAULT_WALLETBROADCAST)); strUsage += HelpMessageOpt( "-walletdir=", _("Specify directory to hold wallets (default: /wallets if it " "exists, otherwise )")); strUsage += HelpMessageOpt("-walletnotify=", _("Execute command when a wallet transaction " "changes (%s in cmd is replaced by TxID)")); strUsage += HelpMessageOpt( "-zapwallettxes=", _("Delete all wallet transactions and only recover those parts of the " "blockchain through -rescan on startup") + " " + _("(1 = keep tx meta data e.g. account owner and payment " "request information, 2 = drop tx meta data)")); if (showDebug) { strUsage += HelpMessageGroup(_("Wallet debugging/testing options:")); strUsage += HelpMessageOpt( "-dblogsize=", strprintf("Flush wallet database activity from memory to disk log " "every megabytes (default: %u)", DEFAULT_WALLET_DBLOGSIZE)); strUsage += HelpMessageOpt( "-flushwallet", strprintf("Run a thread to flush wallet periodically (default: %d)", DEFAULT_FLUSHWALLET)); strUsage += HelpMessageOpt( "-privdb", strprintf("Sets the DB_PRIVATE flag in the wallet db " "environment (default: %d)", DEFAULT_WALLET_PRIVDB)); strUsage += HelpMessageOpt( "-walletrejectlongchains", strprintf(_("Wallet will not create transactions that violate " "mempool chain limits (default: %d)"), DEFAULT_WALLET_REJECT_LONG_CHAINS)); } return strUsage; } bool WalletInit::ParameterInteraction() { CFeeRate minRelayTxFee = GetConfig().GetMinFeePerKB(); gArgs.SoftSetArg("-wallet", DEFAULT_WALLET_DAT); const bool is_multiwallet = gArgs.GetArgs("-wallet").size() > 1; if (gArgs.GetBoolArg("-disablewallet", DEFAULT_DISABLE_WALLET)) { return true; } if (gArgs.GetBoolArg("-blocksonly", DEFAULT_BLOCKSONLY) && gArgs.SoftSetBoolArg("-walletbroadcast", false)) { LogPrintf("%s: parameter interaction: -blocksonly=1 -> setting " "-walletbroadcast=0\n", __func__); } if (gArgs.GetBoolArg("-salvagewallet", false) && gArgs.SoftSetBoolArg("-rescan", true)) { if (is_multiwallet) { return InitError( strprintf("%s is only allowed with a single wallet file", "-salvagewallet")); } // Rewrite just private keys: rescan to find transactions LogPrintf("%s: parameter interaction: -salvagewallet=1 -> setting " "-rescan=1\n", __func__); } int zapwallettxes = gArgs.GetArg("-zapwallettxes", 0); // -zapwallettxes implies dropping the mempool on startup if (zapwallettxes != 0 && gArgs.SoftSetBoolArg("-persistmempool", false)) { LogPrintf("%s: parameter interaction: -zapwallettxes=%s -> setting " "-persistmempool=0\n", __func__, zapwallettxes); } // -zapwallettxes implies a rescan if (zapwallettxes != 0) { if (is_multiwallet) { return InitError( strprintf("%s is only allowed with a single wallet file", "-zapwallettxes")); } if (gArgs.SoftSetBoolArg("-rescan", true)) { LogPrintf("%s: parameter interaction: -zapwallettxes=%s -> setting " "-rescan=1\n", __func__, zapwallettxes); } LogPrintf("%s: parameter interaction: -zapwallettxes= -> setting " "-rescan=1\n", __func__); } if (is_multiwallet) { if (gArgs.GetBoolArg("-upgradewallet", false)) { return InitError( strprintf("%s is only allowed with a single wallet file", "-upgradewallet")); } } if (gArgs.GetBoolArg("-sysperms", false)) { return InitError("-sysperms is not allowed in combination with enabled " "wallet functionality"); } if (gArgs.GetArg("-prune", 0) && gArgs.GetBoolArg("-rescan", false)) { return InitError( _("Rescans are not possible in pruned mode. You will need to use " "-reindex which will download the whole blockchain again.")); } if (minRelayTxFee.GetFeePerK() > HIGH_TX_FEE_PER_KB) { InitWarning( AmountHighWarn("-minrelaytxfee") + " " + _("The wallet will avoid paying less than the minimum relay fee.")); } if (gArgs.IsArgSet("-fallbackfee")) { Amount nFeePerK = Amount::zero(); if (!ParseMoney(gArgs.GetArg("-fallbackfee", ""), nFeePerK)) { return InitError( strprintf(_("Invalid amount for -fallbackfee=: '%s'"), gArgs.GetArg("-fallbackfee", ""))); } if (nFeePerK > HIGH_TX_FEE_PER_KB) { InitWarning(AmountHighWarn("-fallbackfee") + " " + _("This is the transaction fee you may pay when fee " "estimates are not available.")); } CWallet::fallbackFee = CFeeRate(nFeePerK); } if (gArgs.IsArgSet("-paytxfee")) { Amount nFeePerK = Amount::zero(); if (!ParseMoney(gArgs.GetArg("-paytxfee", ""), nFeePerK)) { return InitError( AmountErrMsg("paytxfee", gArgs.GetArg("-paytxfee", ""))); } if (nFeePerK > HIGH_TX_FEE_PER_KB) { InitWarning(AmountHighWarn("-paytxfee") + " " + _("This is the transaction fee you will pay if you " "send a transaction.")); } payTxFee = CFeeRate(nFeePerK, 1000); if (payTxFee < minRelayTxFee) { return InitError(strprintf( _("Invalid amount for -paytxfee=: '%s' (must " "be at least %s)"), gArgs.GetArg("-paytxfee", ""), minRelayTxFee.ToString())); } } if (gArgs.IsArgSet("-maxtxfee")) { Amount nMaxFee = Amount::zero(); if (!ParseMoney(gArgs.GetArg("-maxtxfee", ""), nMaxFee)) { return InitError( AmountErrMsg("maxtxfee", gArgs.GetArg("-maxtxfee", ""))); } if (nMaxFee > HIGH_MAX_TX_FEE) { InitWarning(_("-maxtxfee is set very high! Fees this large could " "be paid on a single transaction.")); } maxTxFee = nMaxFee; if (CFeeRate(maxTxFee, 1000) < minRelayTxFee) { return InitError(strprintf( _("Invalid amount for -maxtxfee=: '%s' (must " "be at least the minrelay fee of %s to prevent " "stuck transactions)"), gArgs.GetArg("-maxtxfee", ""), minRelayTxFee.ToString())); } } nTxConfirmTarget = gArgs.GetArg("-txconfirmtarget", DEFAULT_TX_CONFIRM_TARGET); bSpendZeroConfChange = gArgs.GetBoolArg("-spendzeroconfchange", DEFAULT_SPEND_ZEROCONF_CHANGE); return true; } void WalletInit::RegisterRPC(CRPCTable &t) { if (gArgs.GetBoolArg("-disablewallet", false)) { return; } RegisterWalletRPCCommands(t); } bool WalletInit::Verify(const CChainParams &chainParams) { if (gArgs.GetBoolArg("-disablewallet", DEFAULT_DISABLE_WALLET)) { return true; } if (gArgs.IsArgSet("-walletdir") && !fs::is_directory(GetWalletDir())) { - return InitError(strprintf( - _("Error: Specified wallet directory \"%s\" does not exist."), - gArgs.GetArg("-walletdir", "").c_str())); + if (fs::exists(fs::system_complete(gArgs.GetArg("-walletdir", "")))) { + return InitError( + strprintf(_("Specified -walletdir \"%s\" is not a directory"), + gArgs.GetArg("-walletdir", "").c_str())); + } + return InitError( + strprintf(_("Specified -walletdir \"%s\" does not exist"), + gArgs.GetArg("-walletdir", "").c_str())); } LogPrintf("Using wallet directory %s\n", GetWalletDir().string()); uiInterface.InitMessage(_("Verifying wallet(s)...")); // Keep track of each wallet absolute path to detect duplicates. std::set wallet_paths; for (const std::string &walletFile : gArgs.GetArgs("-wallet")) { if (fs::path(walletFile).filename() != walletFile) { return InitError( strprintf(_("Error loading wallet %s. -wallet parameter must " "only specify a filename (not a path)."), walletFile)); } if (SanitizeString(walletFile, SAFE_CHARS_FILENAME) != walletFile) { return InitError(strprintf(_("Error loading wallet %s. Invalid " "characters in -wallet filename."), walletFile)); } fs::path wallet_path = fs::absolute(walletFile, GetWalletDir()); if (fs::exists(wallet_path) && (!fs::is_regular_file(wallet_path) || fs::is_symlink(wallet_path))) { return InitError(strprintf(_("Error loading wallet %s. -wallet " "filename must be a regular file."), walletFile)); } if (!wallet_paths.insert(wallet_path).second) { return InitError(strprintf(_("Error loading wallet %s. Duplicate " "-wallet filename specified."), walletFile)); } std::string strError; if (!CWalletDB::VerifyEnvironment(walletFile, GetWalletDir().string(), strError)) { return InitError(strError); } if (gArgs.GetBoolArg("-salvagewallet", false)) { // Recover readable keypairs: CWallet dummyWallet(chainParams); std::string backup_filename; if (!CWalletDB::Recover(walletFile, (void *)&dummyWallet, CWalletDB::RecoverKeysOnlyFilter, backup_filename)) { return false; } } std::string strWarning; bool dbV = CWalletDB::VerifyDatabaseFile( walletFile, GetWalletDir().string(), strWarning, strError); if (!strWarning.empty()) { InitWarning(strWarning); } if (!dbV) { InitError(strError); return false; } } return true; } bool WalletInit::Open(const CChainParams &chainParams) { if (gArgs.GetBoolArg("-disablewallet", DEFAULT_DISABLE_WALLET)) { LogPrintf("Wallet disabled!\n"); return true; } for (const std::string &walletFile : gArgs.GetArgs("-wallet")) { CWallet *const pwallet = CWallet::CreateWalletFromFile(chainParams, walletFile); if (!pwallet) { return false; } vpwallets.push_back(pwallet); } return true; } void WalletInit::Start(CScheduler &scheduler) { for (CWalletRef pwallet : vpwallets) { pwallet->postInitProcess(scheduler); } } void WalletInit::Flush() { for (CWalletRef pwallet : vpwallets) { pwallet->Flush(false); } } void WalletInit::Stop() { for (CWalletRef pwallet : vpwallets) { pwallet->Flush(true); } } void WalletInit::Close() { for (CWalletRef pwallet : vpwallets) { delete pwallet; } vpwallets.clear(); } diff --git a/src/wallet/walletutil.cpp b/src/wallet/walletutil.cpp index e474e2423c..839ae26d31 100644 --- a/src/wallet/walletutil.cpp +++ b/src/wallet/walletutil.cpp @@ -1,27 +1,27 @@ // Copyright (c) 2017 The Bitcoin Core developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. -#include "wallet/walletutil.h" +#include fs::path GetWalletDir() { fs::path path; if (gArgs.IsArgSet("-walletdir")) { path = fs::system_complete(gArgs.GetArg("-walletdir", "")); if (!fs::is_directory(path)) { // If the path specified doesn't exist, we return the deliberately // invalid empty string. path = ""; } } else { path = GetDataDir(); // If a wallets directory exists, use that, otherwise default to // GetDataDir if (fs::is_directory(path / "wallets")) { path /= "wallets"; } } return path; } diff --git a/src/wallet/walletutil.h b/src/wallet/walletutil.h index a94f286a44..0e6d128cc5 100644 --- a/src/wallet/walletutil.h +++ b/src/wallet/walletutil.h @@ -1,13 +1,13 @@ // Copyright (c) 2017 The Bitcoin Core developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #ifndef BITCOIN_WALLET_UTIL_H #define BITCOIN_WALLET_UTIL_H -#include "util.h" +#include //! Get the path of the wallet directory. fs::path GetWalletDir(); #endif // BITCOIN_WALLET_UTIL_H diff --git a/test/functional/wallet_multiwallet.py b/test/functional/wallet_multiwallet.py index 28378d1af6..c37b4b8b43 100755 --- a/test/functional/wallet_multiwallet.py +++ b/test/functional/wallet_multiwallet.py @@ -1,132 +1,137 @@ #!/usr/bin/env python3 # Copyright (c) 2017 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 multiwallet. Verify that a bitcoind node can load multiple wallet files """ import os import shutil from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal, assert_raises_rpc_error class MultiWalletTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 1 self.extra_args = [ ['-wallet=w1', '-wallet=w2', '-wallet=w3', '-wallet=w']] def run_test(self): assert_equal(set(self.nodes[0].listwallets()), {"w1", "w2", "w3", "w"}) self.stop_node(0) # should not initialize if there are duplicate wallets self.assert_start_raises_init_error( 0, ['-wallet=w1', '-wallet=w1'], 'Error loading wallet w1. Duplicate -wallet filename specified.') # should not initialize if wallet file is a directory wallet_dir = os.path.join( self.options.tmpdir, 'node0', 'regtest', 'wallets') os.mkdir(os.path.join(wallet_dir, 'w11')) self.assert_start_raises_init_error( 0, ['-wallet=w11'], 'Error loading wallet w11. -wallet filename must be a regular file.') # should not initialize if one wallet is a copy of another shutil.copyfile(os.path.join(wallet_dir, 'w2'), os.path.join(wallet_dir, 'w22')) self.assert_start_raises_init_error( 0, ['-wallet=w2', '-wallet=w22'], 'duplicates fileid') # should not initialize if wallet file is a symlink os.symlink(os.path.join(wallet_dir, 'w1'), os.path.join(wallet_dir, 'w12')) self.assert_start_raises_init_error( 0, ['-wallet=w12'], 'Error loading wallet w12. -wallet filename must be a regular file.') # should not initialize if the specified walletdir does not exist self.assert_start_raises_init_error( - 0, ['-walletdir=bad'], 'Error: Specified wallet directory "bad" does not exist.') + 0, ['-walletdir=bad'], 'Error: Specified -walletdir "bad" does not exist') + # should not initialize if the specified walletdir is not a directory + not_a_dir = os.path.join(wallet_dir, 'notadir') + open(not_a_dir, 'a').close() + self.assert_start_raises_init_error( + 0, ['-walletdir='+not_a_dir], 'Error: Specified -walletdir "' + not_a_dir + '" is not a directory') # if wallets/ doesn't exist, datadir should be the default wallet dir wallet_dir2 = os.path.join( self.options.tmpdir, 'node0', 'regtest', 'walletdir') os.rename(wallet_dir, wallet_dir2) self.start_node(0, ['-wallet=w4', '-wallet=w5']) assert_equal(set(self.nodes[0].listwallets()), {"w4", "w5"}) w5 = self.nodes[0].get_wallet_rpc("w5") w5.generate(1) self.stop_node(0) # now if wallets/ exists again, but the rootdir is specified as the walletdir, w4 and w5 should still be loaded os.rename(wallet_dir2, wallet_dir) self.start_node(0, ['-wallet=w4', '-wallet=w5', '-walletdir=' + os.path.join(self.options.tmpdir, 'node0', 'regtest')]) assert_equal(set(self.nodes[0].listwallets()), {"w4", "w5"}) w5 = self.nodes[0].get_wallet_rpc("w5") w5_info = w5.getwalletinfo() assert_equal(w5_info['immature_balance'], 50) self.stop_node(0) self.start_node(0, self.extra_args[0]) w1 = self.nodes[0].get_wallet_rpc("w1") w2 = self.nodes[0].get_wallet_rpc("w2") w3 = self.nodes[0].get_wallet_rpc("w3") w4 = self.nodes[0].get_wallet_rpc("w") wallet_bad = self.nodes[0].get_wallet_rpc("bad") w1.generate(1) # accessing invalid wallet fails assert_raises_rpc_error(-18, "Requested wallet does not exist or is not loaded", wallet_bad.getwalletinfo) # accessing wallet RPC without using wallet endpoint fails assert_raises_rpc_error(-19, "Wallet file not specified (must request wallet RPC through /wallet/ uri-path).", self.nodes[0].getwalletinfo) # check w1 wallet balance w1_info = w1.getwalletinfo() assert_equal(w1_info['immature_balance'], 50) w1_name = w1_info['walletname'] assert_equal(w1_name, "w1") # check w2 wallet balance w2_info = w2.getwalletinfo() assert_equal(w2_info['immature_balance'], 0) w2_name = w2_info['walletname'] assert_equal(w2_name, "w2") w3_name = w3.getwalletinfo()['walletname'] assert_equal(w3_name, "w3") w4_name = w4.getwalletinfo()['walletname'] assert_equal(w4_name, "w") w1.generate(101) assert_equal(w1.getbalance(), 100) assert_equal(w2.getbalance(), 0) assert_equal(w3.getbalance(), 0) assert_equal(w4.getbalance(), 0) w1.sendtoaddress(w2.getnewaddress(), 1) w1.sendtoaddress(w3.getnewaddress(), 2) w1.sendtoaddress(w4.getnewaddress(), 3) w1.generate(1) assert_equal(w2.getbalance(), 1) assert_equal(w3.getbalance(), 2) assert_equal(w4.getbalance(), 3) batch = w1.batch([w1.getblockchaininfo.get_request(), w1.getwalletinfo.get_request()]) assert_equal(batch[0]["result"]["chain"], "regtest") assert_equal(batch[1]["result"]["walletname"], "w1") if __name__ == '__main__': MultiWalletTest().main()