diff --git a/src/Makefile.am b/src/Makefile.am index 8ecd3918045..7e957d67d46 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -164,6 +164,7 @@ BITCOIN_CORE_H = \ wallet/rpcwallet.h \ wallet/wallet.h \ wallet/walletdb.h \ + wallet/walletinit.h \ warnings.h \ zmq/zmqabstractnotifier.h \ zmq/zmqconfig.h\ @@ -242,6 +243,7 @@ libbitcoin_wallet_a_SOURCES = \ wallet/rpcwallet.cpp \ wallet/wallet.cpp \ wallet/walletdb.cpp \ + wallet/walletinit.cpp \ $(BITCOIN_CORE_H) # crypto primitives library diff --git a/src/init.cpp b/src/init.cpp index 672ef77e80f..7896d5fad43 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -43,7 +43,7 @@ #include "utilmoneystr.h" #include "validationinterface.h" #ifdef ENABLE_WALLET -#include "wallet/wallet.h" +#include "wallet/walletinit.h" #endif #include "warnings.h" #include @@ -187,11 +187,6 @@ void Shutdown() StopREST(); StopRPC(); StopHTTPServer(); -#ifdef ENABLE_WALLET - for (CWalletRef pwallet : vpwallets) { - pwallet->Flush(false); - } -#endif MapPort(false); UnregisterValidationInterface(peerLogic.get()); peerLogic.reset(); @@ -230,9 +225,7 @@ void Shutdown() pblocktree = NULL; } #ifdef ENABLE_WALLET - for (CWalletRef pwallet : vpwallets) { - pwallet->Flush(true); - } + ShutdownWallets(); #endif #if ENABLE_ZMQ @@ -252,10 +245,7 @@ void Shutdown() #endif UnregisterAllValidationInterfaces(); #ifdef ENABLE_WALLET - for (CWalletRef pwallet : vpwallets) { - delete pwallet; - } - vpwallets.clear(); + DeleteWallets(); #endif globalVerifyHandle.reset(); ECC_Stop(); @@ -404,7 +394,7 @@ std::string HelpMessage(HelpMessageMode mode) strUsage += HelpMessageOpt("-maxuploadtarget=", strprintf(_("Tries to keep outbound traffic under the given target (in MiB per 24h), 0 = no limit (default: %d)"), DEFAULT_MAX_UPLOAD_TARGET)); #ifdef ENABLE_WALLET - strUsage += CWallet::GetWalletHelpString(showDebug); + strUsage += GetWalletHelpString(showDebug); #endif #if ENABLE_ZMQ @@ -1052,8 +1042,7 @@ bool AppInitParameterInteraction() nBytesPerSigOp = GetArg("-bytespersigop", nBytesPerSigOp); #ifdef ENABLE_WALLET - if (!CWallet::ParameterInteraction()) - return false; + if (!WalletParameterInteraction()) return false; #endif fIsBareMultisigStd = GetBoolArg("-permitbaremultisig", DEFAULT_PERMIT_BAREMULTISIG); @@ -1219,8 +1208,7 @@ bool AppInitMain(boost::thread_group& threadGroup, CScheduler& scheduler) // ********************************************************* Step 5: verify wallet database integrity #ifdef ENABLE_WALLET - if (!CWallet::Verify()) - return false; + if (!VerifyWallets()) return false; #endif // ********************************************************* Step 6: network initialization // Note that we absolutely cannot open any actual connections @@ -1507,8 +1495,7 @@ bool AppInitMain(boost::thread_group& threadGroup, CScheduler& scheduler) // ********************************************************* Step 8: load wallet #ifdef ENABLE_WALLET - if (!CWallet::InitLoadWallet()) - return false; + if (!InitLoadWallets()) return false; #else LogPrintf("No wallet support compiled in!\n"); #endif @@ -1638,9 +1625,7 @@ bool AppInitMain(boost::thread_group& threadGroup, CScheduler& scheduler) uiInterface.InitMessage(_("Done loading")); #ifdef ENABLE_WALLET - for (CWalletRef pwallet : vpwallets) { - pwallet->postInitProcess(scheduler); - } + WalletCompleteStartup(scheduler); #endif return !fRequestShutdown; diff --git a/src/wallet/db.cpp b/src/wallet/db.cpp index c8928a3b666..6cdfeb010ed 100644 --- a/src/wallet/db.cpp +++ b/src/wallet/db.cpp @@ -227,40 +227,12 @@ bool CDB::Recover(const std::string& filename, void *callbackDataIn, bool (*reco bool CDB::VerifyEnvironment(const std::string& walletFile, const fs::path& dataDir, std::string& errorStr) { - LogPrintf("Using BerkeleyDB version %s\n", DbEnv::version(0, 0, 0)); - LogPrintf("Using wallet %s\n", walletFile); - - // Wallet file must be a plain filename without a directory - if (walletFile != fs::basename(walletFile) + fs::extension(walletFile)) - { - errorStr = strprintf(_("Wallet %s resides outside data directory %s"), walletFile, dataDir.string()); - return false; - } - - if (!bitdb.Open(dataDir)) - { - // try moving the database env out of the way - fs::path pathDatabase = dataDir / "database"; - fs::path pathDatabaseBak = dataDir / strprintf("database.%d.bak", GetTime()); - try { - fs::rename(pathDatabase, pathDatabaseBak); - LogPrintf("Moved old %s to %s. Retrying.\n", pathDatabase.string(), pathDatabaseBak.string()); - } catch (const fs::filesystem_error&) { - // failure is ok (well, not really, but it's not worse than what we started with) - } - - // try again - if (!bitdb.Open(dataDir)) { - // if it still fails, it probably means we can't even create the database env - errorStr = strprintf(_("Error initializing wallet database environment %s!"), GetDataDir()); - return false; - } - } return true; } bool CDB::VerifyDatabaseFile(const std::string& walletFile, const fs::path& dataDir, std::string& warningStr, std::string& errorStr, CDBEnv::recoverFunc_type recoverFunc) { + LogPrintf("Using wallet %s\n", walletFile); if (fs::exists(dataDir / walletFile)) { std::string backup_filename; @@ -558,22 +530,32 @@ bool CDB::Rewrite(CWalletDBWrapper& dbw, const char* pszSkip) return false; } +void CDBEnv::Shutdown() +{ + char** listp; + if (mapFileUseCount.empty()) { + dbenv->log_archive(&listp, DB_ARCH_REMOVE); + Close(); + if (!fMockDb) + fs::remove_all(fs::path(strPath) / "database"); + } +} -void CDBEnv::Flush(bool fShutdown) +void CDBEnv::Flush(std::string strFile) { int64_t nStart = GetTimeMillis(); // Flush log data to the actual data file on all files that are not in use - LogPrint(BCLog::DB, "CDBEnv::Flush: Flush(%s)%s\n", fShutdown ? "true" : "false", fDbEnvInit ? "" : " database not started"); + LogPrint(BCLog::DB, "CDBEnv::Flush: Flush%s\n", fDbEnvInit ? "" : " database not started"); if (!fDbEnvInit) return; { LOCK(cs_db); std::map::iterator mi = mapFileUseCount.begin(); while (mi != mapFileUseCount.end()) { - std::string strFile = (*mi).first; + std::string foundStrFile = (*mi).first; int nRefCount = (*mi).second; - LogPrint(BCLog::DB, "CDBEnv::Flush: Flushing %s (refcount = %d)...\n", strFile, nRefCount); - if (nRefCount == 0) { + if (foundStrFile == strFile && nRefCount == 0) { + LogPrint(BCLog::DB, "CDBEnv::Flush: Flushing %s (refcount = %d)...\n", strFile, nRefCount); // Move log data to the dat file CloseDb(strFile); LogPrint(BCLog::DB, "CDBEnv::Flush: %s checkpoint\n", strFile); @@ -586,16 +568,7 @@ void CDBEnv::Flush(bool fShutdown) } else mi++; } - LogPrint(BCLog::DB, "CDBEnv::Flush: Flush(%s)%s took %15dms\n", fShutdown ? "true" : "false", fDbEnvInit ? "" : " database not started", GetTimeMillis() - nStart); - if (fShutdown) { - char** listp; - if (mapFileUseCount.empty()) { - dbenv->log_archive(&listp, DB_ARCH_REMOVE); - Close(); - if (!fMockDb) - fs::remove_all(fs::path(strPath) / "database"); - } - } + LogPrint(BCLog::DB, "CDBEnv::Flush: Flush took %15dms\n", GetTimeMillis() - nStart); } } @@ -684,9 +657,9 @@ bool CWalletDBWrapper::Backup(const std::string& strDest) return false; } -void CWalletDBWrapper::Flush(bool shutdown) +void CWalletDBWrapper::Flush() { if (!IsDummy()) { - env->Flush(shutdown); + env->Flush(strFile); } } diff --git a/src/wallet/db.h b/src/wallet/db.h index 7cccc65660d..4c2268a12c8 100644 --- a/src/wallet/db.h +++ b/src/wallet/db.h @@ -70,7 +70,8 @@ class CDBEnv bool Open(const fs::path& path); void Close(); - void Flush(bool fShutdown); + void Flush(std::string strFile); + void Shutdown(); void CheckpointLSN(const std::string& strFile); void CloseDb(const std::string& strFile); @@ -119,7 +120,7 @@ class CWalletDBWrapper /** Make sure all changes are flushed to disk. */ - void Flush(bool shutdown); + void Flush(); void IncrementUpdateCounter(); diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index e0c7ab9f0f9..477c66fb464 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -2390,6 +2390,7 @@ UniValue getwalletinfo(const JSONRPCRequest& request) "Returns an object containing various wallet state info.\n" "\nResult:\n" "{\n" + " \"walletname\": xxxxx, (string) the wallet name\n" " \"walletversion\": xxxxx, (numeric) the wallet version\n" " \"balance\": xxxxxxx, (numeric) the total confirmed balance of the wallet in " + CURRENCY_UNIT + "\n" " \"unconfirmed_balance\": xxx, (numeric) the total unconfirmed balance of the wallet in " + CURRENCY_UNIT + "\n" @@ -2412,6 +2413,7 @@ UniValue getwalletinfo(const JSONRPCRequest& request) UniValue obj(UniValue::VOBJ); size_t kpExternalSize = pwallet->KeypoolCountExternalKeys(); + obj.push_back(Pair("walletname", pwallet->GetName())); obj.push_back(Pair("walletversion", pwallet->GetVersion())); obj.push_back(Pair("balance", ValueFromAmount(pwallet->GetBalance()))); obj.push_back(Pair("unconfirmed_balance", ValueFromAmount(pwallet->GetUnconfirmedBalance()))); @@ -2432,6 +2434,133 @@ UniValue getwalletinfo(const JSONRPCRequest& request) return obj; } +UniValue listwallets(const JSONRPCRequest& request) +{ + if (request.fHelp || request.params.size() != 0) + throw std::runtime_error( + "listwallets\n" + "Returns a list of currently loaded wallets.\n" + "For full information on the wallet, use \"getwalletinfo\"\n" + "\nResult:\n" + "[\n" + " \"walletname\": xxxxx, (string) the wallet name\n" + " ...\n" + "]\n" + "\nExamples:\n" + + HelpExampleCli("listwallets", "") + + HelpExampleRpc("listwallets", "") + ); + + LOCK2(cs_main, cs_wallets); + + UniValue obj(UniValue::VARR); + + for (CWalletRef pwallet : vpwallets) { + + if (!EnsureWalletIsAvailable(pwallet, request.fHelp)) { + return NullUniValue; + } + + LOCK(pwallet->cs_wallet); + + obj.push_back(pwallet->GetName()); + } + + return obj; +} + +UniValue loadwallet(const JSONRPCRequest& request) +{ + if (request.fHelp || request.params.size() != 1) + throw std::runtime_error( + "loadwallet \"filename\"\n" + "\nLoads a wallet.dat file.\n" + "\nArguments:\n" + "1. \"filename\" (string, required) The wallet.dat file\n" + "\nResult:\n" + " (string) The wallet name if loaded successfully.\n" + "\nExamples:\n" + + HelpExampleCli("loadwallet", "\"test.dat\"") + + HelpExampleRpc("loadwallet", "\"test.dat\"") + ); + + LOCK2(cs_main, cs_wallets); + + std::string walletFile = request.params[0].get_str(); + + if (boost::filesystem::path(walletFile).filename() != walletFile) { + throw JSONRPCError(RPC_WALLET_ERROR, "Wallet file parameter must only specify a filename (not a path)"); + } else if (SanitizeString(walletFile, SAFE_CHARS_FILENAME) != walletFile) { + throw JSONRPCError(RPC_WALLET_ERROR, "Invalid characters in wallet file parameter."); + } + + if (!VerifyWallet(walletFile, false)) { + throw JSONRPCError(RPC_WALLET_ERROR, "Wallet file verification failed."); + } + + CWallet * const pwallet = CWallet::CreateWalletFromFile(walletFile); + if (!pwallet) { + throw JSONRPCError(RPC_WALLET_ERROR, "Wallet creation failed."); + } + vpwallets.push_back(pwallet); + + pwallet->ReacceptWalletTransactions(); + + UniValue obj(UniValue::VSTR); + obj = walletFile; + + return obj; + +} + +UniValue unloadwallet(const JSONRPCRequest& request) +{ + if (request.fHelp || request.params.size() != 1) + throw std::runtime_error( + "unloadwallet \"walletname\"\n" + "\nUnloads a wallet.\n" + "\nArguments:\n" + "1. \"walletname\" (string, required) The wallet name\n" + "\nExamples:\n" + "\nResult:\n" + "true|false (boolean) Whether the wallet was unloaded successfully.\n" + + HelpExampleCli("unloadwallet", "\"test.dat\"") + + HelpExampleRpc("unloadwallet", "\"test.dat\"") + ); + + LOCK2(cs_main, cs_wallets); + + std::string walletName = request.params[0].get_str(); + + bool unloaded = false; + + for (std::vector::iterator wallet_iter = vpwallets.begin(); wallet_iter != vpwallets.end(); wallet_iter++) { + + if (!EnsureWalletIsAvailable(*wallet_iter, request.fHelp)) { + throw JSONRPCError(RPC_WALLET_ERROR, "Error accessing wallets"); + } + + if ((*wallet_iter)->GetName() == walletName) { + + (*wallet_iter)->Flush(); + UnregisterValidationInterface(*wallet_iter); + delete *wallet_iter; + vpwallets.erase(wallet_iter); + + unloaded = true; + break; + } + } + + if (!unloaded) { + throw JSONRPCError(RPC_WALLET_ERROR, strprintf("Failed to find wallet %s", walletName)); + } + + UniValue obj(UniValue::VBOOL); + obj = unloaded; + return obj; +} + UniValue resendwallettransactions(const JSONRPCRequest& request) { CWallet * const pwallet = GetWalletForJSONRPCRequest(request); @@ -3019,6 +3148,8 @@ static const CRPCCommand commands[] = { "wallet", "listsinceblock", &listsinceblock, false, {"blockhash","target_confirmations","include_watchonly"} }, { "wallet", "listtransactions", &listtransactions, false, {"account","count","skip","include_watchonly"} }, { "wallet", "listunspent", &listunspent, false, {"minconf","maxconf","addresses","include_unsafe","query_options"} }, + { "wallet", "listwallets", &listwallets, true, {} }, + { "wallet", "loadwallet", &loadwallet, true, {"filename"} }, { "wallet", "lockunspent", &lockunspent, true, {"unlock","transactions"} }, { "wallet", "move", &movecmd, false, {"fromaccount","toaccount","amount","minconf","comment"} }, { "wallet", "sendfrom", &sendfrom, false, {"fromaccount","toaddress","amount","minconf","comment","comment_to"} }, @@ -3027,6 +3158,7 @@ static const CRPCCommand commands[] = { "wallet", "setaccount", &setaccount, true, {"address","account"} }, { "wallet", "settxfee", &settxfee, true, {"amount"} }, { "wallet", "signmessage", &signmessage, true, {"address","message"} }, + { "wallet", "unloadwallet", &unloadwallet, true, {"walletname"} }, { "wallet", "walletlock", &walletlock, true, {} }, { "wallet", "walletpassphrasechange", &walletpassphrasechange, true, {"oldpassphrase","newpassphrase"} }, { "wallet", "walletpassphrase", &walletpassphrase, true, {"passphrase","timeout"} }, diff --git a/src/wallet/test/wallet_test_fixture.cpp b/src/wallet/test/wallet_test_fixture.cpp index 922fcc8e898..26ed8a99c00 100644 --- a/src/wallet/test/wallet_test_fixture.cpp +++ b/src/wallet/test/wallet_test_fixture.cpp @@ -30,6 +30,7 @@ WalletTestingSetup::~WalletTestingSetup() delete pwalletMain; pwalletMain = NULL; - bitdb.Flush(true); + bitdb.Flush("wallet_test.dat"); + bitdb.Shutdown(); bitdb.Reset(); } diff --git a/src/wallet/test/wallet_tests.cpp b/src/wallet/test/wallet_tests.cpp index 96a1b14b60e..156403d60ed 100644 --- a/src/wallet/test/wallet_tests.cpp +++ b/src/wallet/test/wallet_tests.cpp @@ -606,7 +606,8 @@ class ListCoinsTestingSetup : public TestChain100Setup ~ListCoinsTestingSetup() { wallet.reset(); - ::bitdb.Flush(true); + ::bitdb.Flush("wallet_test.dat"); + ::bitdb.Shutdown(); ::bitdb.Reset(); } diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 0d1a86dd244..4129654c408 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -35,6 +35,8 @@ #include #include +/** Protects the vpwallets vector */ +CCriticalSection cs_wallets; std::vector vpwallets; /** Transaction fee set by the user */ CFeeRate payTxFee(DEFAULT_TRANSACTION_FEE); @@ -433,51 +435,9 @@ bool CWallet::HasWalletSpend(const uint256& txid) const return (iter != mapTxSpends.end() && iter->first.hash == txid); } -void CWallet::Flush(bool shutdown) +void CWallet::Flush() { - dbw->Flush(shutdown); -} - -bool CWallet::Verify() -{ - if (GetBoolArg("-disablewallet", DEFAULT_DISABLE_WALLET)) - return true; - - uiInterface.InitMessage(_("Verifying wallet(s)...")); - - for (const std::string& walletFile : gArgs.GetArgs("-wallet")) { - if (boost::filesystem::path(walletFile).filename() != walletFile) { - return InitError(_("-wallet parameter must only specify a filename (not a path)")); - } else if (SanitizeString(walletFile, SAFE_CHARS_FILENAME) != walletFile) { - return InitError(_("Invalid characters in -wallet filename")); - } - - std::string strError; - if (!CWalletDB::VerifyEnvironment(walletFile, GetDataDir().string(), strError)) { - return InitError(strError); - } - - if (GetBoolArg("-salvagewallet", false)) { - // Recover readable keypairs: - CWallet dummyWallet; - std::string backup_filename; - if (!CWalletDB::Recover(walletFile, (void *)&dummyWallet, CWalletDB::RecoverKeysOnlyFilter, backup_filename)) { - return false; - } - } - - std::string strWarning; - bool dbV = CWalletDB::VerifyDatabaseFile(walletFile, GetDataDir().string(), strWarning, strError); - if (!strWarning.empty()) { - InitWarning(strWarning); - } - if (!dbV) { - InitError(strError); - return false; - } - } - - return true; + dbw->Flush(); } void CWallet::SyncMetaData(std::pair range) @@ -3731,41 +3691,28 @@ std::vector CWallet::GetDestValues(const std::string& prefix) const return values; } -std::string CWallet::GetWalletHelpString(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("-mintxfee=", strprintf(_("Fees (in %s/kB) smaller than this are considered zero fee for transaction creation (default: %s)"), - CURRENCY_UNIT, FormatMoney(DEFAULT_TRANSACTION_MINFEE))); - 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: %u)"), 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: %u)"), DEFAULT_USE_HD_WALLET)); - strUsage += HelpMessageOpt("-walletrbf", strprintf(_("Send transactions with full-RBF opt-in enabled (default: %u)"), DEFAULT_WALLET_RBF)); - 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: %u)"), DEFAULT_WALLETBROADCAST)); - 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: %u)", DEFAULT_FLUSHWALLET)); - strUsage += HelpMessageOpt("-privdb", strprintf("Sets the DB_PRIVATE flag in the wallet db environment (default: %u)", DEFAULT_WALLET_PRIVDB)); - strUsage += HelpMessageOpt("-walletrejectlongchains", strprintf(_("Wallet will not create transactions that violate mempool chain limits (default: %u)"), DEFAULT_WALLET_REJECT_LONG_CHAINS)); +bool VerifyWallet(std::string walletFile, bool salvage_wallet) +{ + if (salvage_wallet) { + // Recover readable keypairs: + CWallet dummyWallet; + std::string backup_filename; + if (!CWalletDB::Recover(walletFile, (void *)&dummyWallet, CWalletDB::RecoverKeysOnlyFilter, backup_filename)) { + return false; + } } - return strUsage; + std::string strError; + std::string strWarning; + bool dbV = CWalletDB::VerifyDatabaseFile(walletFile, GetDataDir().string(), strWarning, strError); + if (!strWarning.empty()) { + InitWarning(strWarning); + } + if (!dbV) { + InitError(strError); + return false; + } + return true; } CWallet* CWallet::CreateWalletFromFile(const std::string walletFile) @@ -3958,24 +3905,6 @@ CWallet* CWallet::CreateWalletFromFile(const std::string walletFile) return walletInstance; } -bool CWallet::InitLoadWallet() -{ - if (GetBoolArg("-disablewallet", DEFAULT_DISABLE_WALLET)) { - LogPrintf("Wallet disabled!\n"); - return true; - } - - for (const std::string& walletFile : gArgs.GetArgs("-wallet")) { - CWallet * const pwallet = CreateWalletFromFile(walletFile); - if (!pwallet) { - return false; - } - vpwallets.push_back(pwallet); - } - - return true; -} - std::atomic CWallet::fFlushScheduled(false); void CWallet::postInitProcess(CScheduler& scheduler) @@ -3990,106 +3919,6 @@ void CWallet::postInitProcess(CScheduler& scheduler) } } -bool CWallet::ParameterInteraction() -{ - SoftSetArg("-wallet", DEFAULT_WALLET_DAT); - const bool is_multiwallet = gArgs.GetArgs("-wallet").size() > 1; - - if (GetBoolArg("-disablewallet", DEFAULT_DISABLE_WALLET)) - return true; - - if (GetBoolArg("-blocksonly", DEFAULT_BLOCKSONLY) && SoftSetBoolArg("-walletbroadcast", false)) { - LogPrintf("%s: parameter interaction: -blocksonly=1 -> setting -walletbroadcast=0\n", __func__); - } - - if (GetBoolArg("-salvagewallet", false) && 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__); - } - - // -zapwallettx implies a rescan - if (GetBoolArg("-zapwallettxes", false) && SoftSetBoolArg("-rescan", true)) { - if (is_multiwallet) { - return InitError(strprintf("%s is only allowed with a single wallet file", "-zapwallettxes")); - } - LogPrintf("%s: parameter interaction: -zapwallettxes= -> setting -rescan=1\n", __func__); - } - - if (is_multiwallet) { - if (GetBoolArg("-upgradewallet", false)) { - return InitError(strprintf("%s is only allowed with a single wallet file", "-upgradewallet")); - } - } - - if (GetBoolArg("-sysperms", false)) - return InitError("-sysperms is not allowed in combination with enabled wallet functionality"); - if (GetArg("-prune", 0) && 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 (IsArgSet("-mintxfee")) - { - CAmount n = 0; - if (!ParseMoney(GetArg("-mintxfee", ""), n) || 0 == n) - return InitError(AmountErrMsg("mintxfee", GetArg("-mintxfee", ""))); - if (n > HIGH_TX_FEE_PER_KB) - InitWarning(AmountHighWarn("-mintxfee") + " " + - _("This is the minimum transaction fee you pay on every transaction.")); - CWallet::minTxFee = CFeeRate(n); - } - if (IsArgSet("-fallbackfee")) - { - CAmount nFeePerK = 0; - if (!ParseMoney(GetArg("-fallbackfee", ""), nFeePerK)) - return InitError(strprintf(_("Invalid amount for -fallbackfee=: '%s'"), 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 (IsArgSet("-paytxfee")) - { - CAmount nFeePerK = 0; - if (!ParseMoney(GetArg("-paytxfee", ""), nFeePerK)) - return InitError(AmountErrMsg("paytxfee", 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)"), - GetArg("-paytxfee", ""), ::minRelayTxFee.ToString())); - } - } - if (IsArgSet("-maxtxfee")) - { - CAmount nMaxFee = 0; - if (!ParseMoney(GetArg("-maxtxfee", ""), nMaxFee)) - return InitError(AmountErrMsg("maxtxfee", 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)"), - GetArg("-maxtxfee", ""), ::minRelayTxFee.ToString())); - } - } - nTxConfirmTarget = GetArg("-txconfirmtarget", DEFAULT_TX_CONFIRM_TARGET); - bSpendZeroConfChange = GetBoolArg("-spendzeroconfchange", DEFAULT_SPEND_ZEROCONF_CHANGE); - fWalletRbf = GetBoolArg("-walletrbf", DEFAULT_WALLET_RBF); - - return true; -} - bool CWallet::BackupWallet(const std::string& strDest) { return dbw->Backup(strDest); diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index 4f558adc772..4b36805eb85 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -30,6 +30,7 @@ #include typedef CWallet* CWalletRef; +extern CCriticalSection cs_wallets; extern std::vector vpwallets; /** @@ -650,7 +651,6 @@ class CAccountingEntry class CWallet : public CCryptoKeyStore, public CValidationInterface { private: - static std::atomic fFlushScheduled; std::atomic fAbortRescan; std::atomic fScanningWallet; @@ -796,6 +796,7 @@ class CWallet : public CCryptoKeyStore, public CValidationInterface fScanningWallet = false; } + static std::atomic fFlushScheduled; std::map mapWallet; std::list laccentries; @@ -1051,11 +1052,8 @@ class CWallet : public CCryptoKeyStore, public CValidationInterface bool HasWalletSpend(const uint256& txid) const; //! Flush wallet (bitdb flush) - void Flush(bool shutdown=false); + void Flush(); - //! Verify the wallet database and perform salvage if required - static bool Verify(); - /** * Address book entry changed. * @note called with lock cs_wallet held. @@ -1092,12 +1090,8 @@ class CWallet : public CCryptoKeyStore, public CValidationInterface /** Mark a transaction as replaced by another transaction (e.g., BIP 125). */ bool MarkReplaced(const uint256& originalHash, const uint256& newHash); - /* Returns the wallets help message */ - static std::string GetWalletHelpString(bool showDebug); - /* Initializes the wallet, returns a new CWallet instance or a null pointer in case of an error */ static CWallet* CreateWalletFromFile(const std::string walletFile); - static bool InitLoadWallet(); /** * Wallet post-init setup @@ -1105,9 +1099,6 @@ class CWallet : public CCryptoKeyStore, public CValidationInterface */ void postInitProcess(CScheduler& scheduler); - /* Wallets parameter interaction */ - static bool ParameterInteraction(); - bool BackupWallet(const std::string& strDest); /* Set the HD chain model (chain child index counters) */ @@ -1127,6 +1118,9 @@ class CWallet : public CCryptoKeyStore, public CValidationInterface bool SetHDMasterKey(const CPubKey& key); }; +//! Verify the database for a single wallet and perform salvage if required +bool VerifyWallet(std::string walletFile, bool salvage_wallet); + /** A key allocated from the key pool. */ class CReserveKey : public CReserveScript { diff --git a/src/wallet/walletdb.cpp b/src/wallet/walletdb.cpp index 8321719b560..bc540636794 100644 --- a/src/wallet/walletdb.cpp +++ b/src/wallet/walletdb.cpp @@ -748,6 +748,8 @@ DBErrors CWalletDB::ZapWalletTx(std::vector& vWtx) void MaybeCompactWalletDB() { + LOCK(cs_wallets); + static std::atomic fOneThread; if (fOneThread.exchange(true)) { return; diff --git a/src/wallet/walletinit.cpp b/src/wallet/walletinit.cpp new file mode 100644 index 00000000000..e3d2b3f6428 --- /dev/null +++ b/src/wallet/walletinit.cpp @@ -0,0 +1,244 @@ +#include "fs.h" +#include "net.h" +#include "scheduler.h" +#include "util.h" +#include "utilmoneystr.h" +#include "validation.h" +#include "wallet/walletinit.h" + +std::string GetWalletHelpString(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("-mintxfee=", strprintf(_("Fees (in %s/kB) smaller than this are considered zero fee for transaction creation (default: %s)"), + CURRENCY_UNIT, FormatMoney(DEFAULT_TRANSACTION_MINFEE))); + 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: %u)"), 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: %u)"), DEFAULT_USE_HD_WALLET)); + strUsage += HelpMessageOpt("-walletrbf", strprintf(_("Send transactions with full-RBF opt-in enabled (default: %u)"), DEFAULT_WALLET_RBF)); + 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: %u)"), DEFAULT_WALLETBROADCAST)); + 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: %u)", DEFAULT_FLUSHWALLET)); + strUsage += HelpMessageOpt("-privdb", strprintf("Sets the DB_PRIVATE flag in the wallet db environment (default: %u)", DEFAULT_WALLET_PRIVDB)); + strUsage += HelpMessageOpt("-walletrejectlongchains", strprintf(_("Wallet will not create transactions that violate mempool chain limits (default: %u)"), DEFAULT_WALLET_REJECT_LONG_CHAINS)); + } + + return strUsage; +} + +bool WalletParameterInteraction() +{ + SoftSetArg("-wallet", DEFAULT_WALLET_DAT); + const bool is_multiwallet = gArgs.GetArgs("-wallet").size() > 1; + + if (GetBoolArg("-disablewallet", DEFAULT_DISABLE_WALLET)) + return true; + + if (GetBoolArg("-blocksonly", DEFAULT_BLOCKSONLY) && SoftSetBoolArg("-walletbroadcast", false)) { + LogPrintf("%s: parameter interaction: -blocksonly=1 -> setting -walletbroadcast=0\n", __func__); + } + + if (GetBoolArg("-salvagewallet", false) && 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__); + } + + // -zapwallettx implies a rescan + if (GetBoolArg("-zapwallettxes", false) && SoftSetBoolArg("-rescan", true)) { + if (is_multiwallet) { + return InitError(strprintf("%s is only allowed with a single wallet file", "-zapwallettxes")); + } + LogPrintf("%s: parameter interaction: -zapwallettxes= -> setting -rescan=1\n", __func__); + } + + if (is_multiwallet) { + if (GetBoolArg("-upgradewallet", false)) { + return InitError(strprintf("%s is only allowed with a single wallet file", "-upgradewallet")); + } + } + + if (GetBoolArg("-sysperms", false)) + return InitError("-sysperms is not allowed in combination with enabled wallet functionality"); + if (GetArg("-prune", 0) && 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 (IsArgSet("-mintxfee")) + { + CAmount n = 0; + if (!ParseMoney(GetArg("-mintxfee", ""), n) || 0 == n) + return InitError(AmountErrMsg("mintxfee", GetArg("-mintxfee", ""))); + if (n > HIGH_TX_FEE_PER_KB) + InitWarning(AmountHighWarn("-mintxfee") + " " + + _("This is the minimum transaction fee you pay on every transaction.")); + CWallet::minTxFee = CFeeRate(n); + } + if (IsArgSet("-fallbackfee")) + { + CAmount nFeePerK = 0; + if (!ParseMoney(GetArg("-fallbackfee", ""), nFeePerK)) + return InitError(strprintf(_("Invalid amount for -fallbackfee=: '%s'"), 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 (IsArgSet("-paytxfee")) + { + CAmount nFeePerK = 0; + if (!ParseMoney(GetArg("-paytxfee", ""), nFeePerK)) + return InitError(AmountErrMsg("paytxfee", 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)"), + GetArg("-paytxfee", ""), ::minRelayTxFee.ToString())); + } + } + if (IsArgSet("-maxtxfee")) + { + CAmount nMaxFee = 0; + if (!ParseMoney(GetArg("-maxtxfee", ""), nMaxFee)) + return InitError(AmountErrMsg("maxtxfee", 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)"), + GetArg("-maxtxfee", ""), ::minRelayTxFee.ToString())); + } + } + nTxConfirmTarget = GetArg("-txconfirmtarget", DEFAULT_TX_CONFIRM_TARGET); + bSpendZeroConfChange = GetBoolArg("-spendzeroconfchange", DEFAULT_SPEND_ZEROCONF_CHANGE); + fWalletRbf = GetBoolArg("-walletrbf", DEFAULT_WALLET_RBF); + + return true; +} + +bool VerifyWallets() +{ + if (GetBoolArg("-disablewallet", DEFAULT_DISABLE_WALLET)) + return true; + + uiInterface.InitMessage(_("Verifying wallet(s)...")); + + LogPrintf("Using BerkeleyDB version %s\n", DbEnv::version(0, 0, 0)); + + fs::path dataDir = GetDataDir().string(); + + if (!bitdb.Open(dataDir)) + { + // try moving the database env out of the way + fs::path pathDatabase = dataDir / "database"; + fs::path pathDatabaseBak = dataDir / strprintf("database.%d.bak", GetTime()); + try { + fs::rename(pathDatabase, pathDatabaseBak); + LogPrintf("Moved old %s to %s. Retrying.\n", pathDatabase.string(), pathDatabaseBak.string()); + } catch (const fs::filesystem_error&) { + // failure is ok (well, not really, but it's not worse than what we started with) + } + + // try again + if (!bitdb.Open(dataDir)) { + // if it still fails, it probably means we can't even create the database env + return InitError(strprintf(_("Error initializing wallet database environment %s!"), GetDataDir())); + } + } + + for (const std::string& walletFile : gArgs.GetArgs("-wallet")) { + if (boost::filesystem::path(walletFile).filename() != walletFile) { + return InitError(_("-wallet parameter must only specify a filename (not a path)")); + } else if (SanitizeString(walletFile, SAFE_CHARS_FILENAME) != walletFile) { + return InitError(_("Invalid characters in -wallet filename")); + } + + // -salvagewallet is only permitted when loading a single wallet + // (belt-and-suspenders - WalletParameterInteraction() should prevent -salvagewallet with multiwallet) + bool salvage_wallet = GetBoolArg("-salvagewallet", false) && (gArgs.GetArgs("-wallet").size()) == 1; + if (!VerifyWallet(walletFile, salvage_wallet)) return false; + } + return true; +} + +bool InitLoadWallets() +{ + if (GetBoolArg("-disablewallet", DEFAULT_DISABLE_WALLET)) { + LogPrintf("Wallet disabled!\n"); + return true; + } + + for (const std::string& walletFile : gArgs.GetArgs("-wallet")) { + CWallet * const pwallet = CWallet::CreateWalletFromFile(walletFile); + if (!pwallet) { + return false; + } + vpwallets.push_back(pwallet); + } + + return true; +} + +void WalletCompleteStartup(CScheduler& scheduler) +{ + if (GetBoolArg("-disablewallet", DEFAULT_DISABLE_WALLET)) return; + + for (CWalletRef pwallet : vpwallets) { + // Add wallet transactions that aren't already in a block to mempool + // Do this here as mempool requires genesis block to be loaded + pwallet->ReacceptWalletTransactions(); + } + + // Run a thread to flush wallet periodically + if (!CWallet::fFlushScheduled.exchange(true)) { + scheduler.scheduleEvery(MaybeCompactWalletDB, 500); + } +} + +void ShutdownWallets() +{ + if (GetBoolArg("-disablewallet", DEFAULT_DISABLE_WALLET)) return; + + LOCK(bitdb.cs_db); + for (CWalletRef pwallet : vpwallets) { + pwallet->Flush(); + } + bitdb.Shutdown(); +} + +void DeleteWallets() +{ + if (GetBoolArg("-disablewallet", DEFAULT_DISABLE_WALLET)) return; + + for (CWalletRef pwallet : vpwallets) { + delete pwallet; + } + vpwallets.clear(); +} diff --git a/src/wallet/walletinit.h b/src/wallet/walletinit.h new file mode 100644 index 00000000000..9cb3978ab35 --- /dev/null +++ b/src/wallet/walletinit.h @@ -0,0 +1,24 @@ +#include "wallet/wallet.h" + +#include + +//!Returns the wallets help message +std::string GetWalletHelpString(bool showDebug); + +//!Wallet parameter interaction +bool WalletParameterInteraction(); + +//! Read and validate the -wallet arguments and verify the wallet database. +bool VerifyWallets(); + +//! Load walllets from file on startup +bool InitLoadWallets(); + +//! Post-init setup for wallets +void WalletCompleteStartup(CScheduler& scheduler); + +//! Flush wallets +void ShutdownWallets(); + +//! Delete wallets +void DeleteWallets(); diff --git a/test/functional/multiwallet.py b/test/functional/multiwallet.py new file mode 100755 index 00000000000..d26489d7889 --- /dev/null +++ b/test/functional/multiwallet.py @@ -0,0 +1,38 @@ +#!/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 +""" +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import assert_equal + +class Multiwallet(BitcoinTestFramework): + def __init__(self): + super().__init__() + self.num_nodes = 1 + self.extra_args = [["-wallet=wallet0.dat"]] + + def run_test(self): + wallet0_name = self.nodes[0].getwalletinfo()["walletname"] + + # stop-start the node with a new wallet + self.stop_nodes() + self.nodes[0] = self.start_node(0, self.options.tmpdir, ["-wallet=wallet1.dat"]) + wallet1_name = self.nodes[0].getwalletinfo()["walletname"] + + # Load wallet0 + self.nodes[0].loadwallet("wallet0.dat") + + # Assert that node0 has both wallets loaded correctly + assert_equal(self.nodes[0].listwallets(), [wallet1_name, wallet0_name]) + + # Unload wallet1 + self.nodes[0].unloadwallet("wallet1.dat") + + assert_equal(self.nodes[0].listwallets(), [wallet0_name]) + +if __name__ == '__main__': + Multiwallet().main() diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 54f625514bd..6a7bc160295 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -88,6 +88,7 @@ 'mempool_spendcoinbase.py', 'mempool_reorg.py', 'mempool_persist.py', + 'multiwallet.py', 'httpbasics.py', 'multi_rpc.py', 'proxy_test.py',