diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/Makefile.am | 2 | ||||
-rw-r--r-- | src/Makefile.test.include | 1 | ||||
-rw-r--r-- | src/bitcoind.cpp | 27 | ||||
-rw-r--r-- | src/common/init.cpp | 74 | ||||
-rw-r--r-- | src/common/init.h | 39 | ||||
-rw-r--r-- | src/index/base.cpp | 2 | ||||
-rw-r--r-- | src/node/chainstate.cpp | 122 | ||||
-rw-r--r-- | src/node/interface_ui.cpp | 13 | ||||
-rw-r--r-- | src/node/interface_ui.h | 2 | ||||
-rw-r--r-- | src/node/minisketchwrapper.cpp | 8 | ||||
-rw-r--r-- | src/qt/bitcoin.cpp | 122 | ||||
-rw-r--r-- | src/random.cpp | 14 | ||||
-rw-r--r-- | src/rpc/rawtransaction.cpp | 4 | ||||
-rw-r--r-- | src/shutdown.cpp | 2 | ||||
-rw-r--r-- | src/test/translation_tests.cpp | 21 | ||||
-rw-r--r-- | src/test/validation_chainstatemanager_tests.cpp | 171 | ||||
-rw-r--r-- | src/util/system.cpp | 40 | ||||
-rw-r--r-- | src/util/system.h | 13 | ||||
-rw-r--r-- | src/util/time.h | 2 | ||||
-rw-r--r-- | src/util/translation.h | 15 | ||||
-rw-r--r-- | src/validation.cpp | 364 | ||||
-rw-r--r-- | src/validation.h | 123 | ||||
-rw-r--r-- | src/wallet/rpc/backup.cpp | 4 | ||||
-rw-r--r-- | src/wallet/wallet.cpp | 4 |
24 files changed, 951 insertions, 238 deletions
diff --git a/src/Makefile.am b/src/Makefile.am index 72e7db5334..7dc5594cf2 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -134,6 +134,7 @@ BITCOIN_CORE_H = \ clientversion.h \ coins.h \ common/bloom.h \ + common/init.h \ common/run_command.h \ common/url.h \ compat/assumptions.h \ @@ -640,6 +641,7 @@ libbitcoin_common_a_SOURCES = \ chainparams.cpp \ coins.cpp \ common/bloom.cpp \ + common/init.cpp \ common/interfaces.cpp \ common/run_command.cpp \ compressor.cpp \ diff --git a/src/Makefile.test.include b/src/Makefile.test.include index 83b721d8bf..a39b0abd9d 100644 --- a/src/Makefile.test.include +++ b/src/Makefile.test.include @@ -147,6 +147,7 @@ BITCOIN_TESTS =\ test/timedata_tests.cpp \ test/torcontrol_tests.cpp \ test/transaction_tests.cpp \ + test/translation_tests.cpp \ test/txindex_tests.cpp \ test/txpackage_tests.cpp \ test/txreconciliation_tests.cpp \ diff --git a/src/bitcoind.cpp b/src/bitcoind.cpp index 6851f86297..b69913dddb 100644 --- a/src/bitcoind.cpp +++ b/src/bitcoind.cpp @@ -9,6 +9,7 @@ #include <chainparams.h> #include <clientversion.h> +#include <common/init.h> #include <common/url.h> #include <compat/compat.h> #include <init.h> @@ -120,7 +121,7 @@ static bool AppInit(NodeContext& node, int argc, char* argv[]) SetupServerArgs(args); std::string error; if (!args.ParseParameters(argc, argv, error)) { - return InitError(Untranslated(strprintf("Error parsing command line arguments: %s\n", error))); + return InitError(Untranslated(strprintf("Error parsing command line arguments: %s", error))); } // Process help and version before taking care about datadir @@ -150,31 +151,17 @@ static bool AppInit(NodeContext& node, int argc, char* argv[]) std::any context{&node}; try { - if (!CheckDataDirOption(args)) { - return InitError(Untranslated(strprintf("Specified data directory \"%s\" does not exist.\n", args.GetArg("-datadir", "")))); - } - if (!args.ReadConfigFiles(error, true)) { - return InitError(Untranslated(strprintf("Error reading configuration file: %s\n", error))); - } - // Check for chain settings (Params() calls are only valid after this clause) - try { - SelectParams(args.GetChainName()); - } catch (const std::exception& e) { - return InitError(Untranslated(strprintf("%s\n", e.what()))); + if (auto error = common::InitConfig(args)) { + return InitError(error->message, error->details); } // Error out when loose non-argument tokens are encountered on command line for (int i = 1; i < argc; i++) { if (!IsSwitchChar(argv[i][0])) { - return InitError(Untranslated(strprintf("Command line contains unexpected token '%s', see bitcoind -h for a list of options.\n", argv[i]))); + return InitError(Untranslated(strprintf("Command line contains unexpected token '%s', see bitcoind -h for a list of options.", argv[i]))); } } - if (!args.InitSettings(error)) { - InitError(Untranslated(error)); - return false; - } - // -server defaults to true for bitcoind but not for the GUI so do this here args.SoftSetBoolArg("-server", true); // Set this early so that parameter interactions go to console @@ -210,7 +197,7 @@ static bool AppInit(NodeContext& node, int argc, char* argv[]) } break; case -1: // Error happened. - return InitError(Untranslated(strprintf("fork_daemon() failed: %s\n", SysErrorString(errno)))); + return InitError(Untranslated(strprintf("fork_daemon() failed: %s", SysErrorString(errno)))); default: { // Parent: wait and exit. int token = daemon_ep.TokenRead(); if (token) { // Success @@ -222,7 +209,7 @@ static bool AppInit(NodeContext& node, int argc, char* argv[]) } } #else - return InitError(Untranslated("-daemon is not supported on this operating system\n")); + return InitError(Untranslated("-daemon is not supported on this operating system")); #endif // HAVE_DECL_FORK } // Lock data directory after daemonization diff --git a/src/common/init.cpp b/src/common/init.cpp new file mode 100644 index 0000000000..159eb7e2ef --- /dev/null +++ b/src/common/init.cpp @@ -0,0 +1,74 @@ +// Copyright (c) 2023 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 <common/init.h> +#include <chainparams.h> +#include <fs.h> +#include <tinyformat.h> +#include <util/system.h> +#include <util/translation.h> + +#include <algorithm> +#include <exception> +#include <optional> + +namespace common { +std::optional<ConfigError> InitConfig(ArgsManager& args, SettingsAbortFn settings_abort_fn) +{ + try { + if (!CheckDataDirOption(args)) { + return ConfigError{ConfigStatus::FAILED, strprintf(_("Specified data directory \"%s\" does not exist."), args.GetArg("-datadir", ""))}; + } + std::string error; + if (!args.ReadConfigFiles(error, true)) { + return ConfigError{ConfigStatus::FAILED, strprintf(_("Error reading configuration file: %s"), error)}; + } + + // Check for chain settings (Params() calls are only valid after this clause) + SelectParams(args.GetChainName()); + + // Create datadir if it does not exist. + const auto base_path{args.GetDataDirBase()}; + if (!fs::exists(base_path)) { + // When creating a *new* datadir, also create a "wallets" subdirectory, + // whether or not the wallet is enabled now, so if the wallet is enabled + // in the future, it will use the "wallets" subdirectory for creating + // and listing wallets, rather than the top-level directory where + // wallets could be mixed up with other files. For backwards + // compatibility, wallet code will use the "wallets" subdirectory only + // if it already exists, but never create it itself. There is discussion + // in https://github.com/bitcoin/bitcoin/issues/16220 about ways to + // change wallet code so it would no longer be necessary to create + // "wallets" subdirectories here. + fs::create_directories(base_path / "wallets"); + } + const auto net_path{args.GetDataDirNet()}; + if (!fs::exists(net_path)) { + fs::create_directories(net_path / "wallets"); + } + + // Create settings.json if -nosettings was not specified. + if (args.GetSettingsPath()) { + std::vector<std::string> details; + if (!args.ReadSettingsFile(&details)) { + const bilingual_str& message = _("Settings file could not be read"); + if (!settings_abort_fn) { + return ConfigError{ConfigStatus::FAILED, message, details}; + } else if (settings_abort_fn(message, details)) { + return ConfigError{ConfigStatus::ABORTED, message, details}; + } else { + details.clear(); // User chose to ignore the error and proceed. + } + } + if (!args.WriteSettingsFile(&details)) { + const bilingual_str& message = _("Settings file could not be written"); + return ConfigError{ConfigStatus::FAILED_WRITE, message, details}; + } + } + } catch (const std::exception& e) { + return ConfigError{ConfigStatus::FAILED, Untranslated(e.what())}; + } + return {}; +} +} // namespace common diff --git a/src/common/init.h b/src/common/init.h new file mode 100644 index 0000000000..380ac3ac7e --- /dev/null +++ b/src/common/init.h @@ -0,0 +1,39 @@ +// Copyright (c) 2023 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_COMMON_INIT_H +#define BITCOIN_COMMON_INIT_H + +#include <util/translation.h> + +#include <functional> +#include <optional> +#include <string> +#include <vector> + +class ArgsManager; + +namespace common { +enum class ConfigStatus { + FAILED, //!< Failed generically. + FAILED_WRITE, //!< Failed to write settings.json + ABORTED, //!< Aborted by user +}; + +struct ConfigError { + ConfigStatus status; + bilingual_str message{}; + std::vector<std::string> details{}; +}; + +//! Callback function to let the user decide whether to abort loading if +//! settings.json file exists and can't be parsed, or to ignore the error and +//! overwrite the file. +using SettingsAbortFn = std::function<bool(const bilingual_str& message, const std::vector<std::string>& details)>; + +/* Read config files, and create datadir and settings.json if they don't exist. */ +std::optional<ConfigError> InitConfig(ArgsManager& args, SettingsAbortFn settings_abort_fn = nullptr); +} // namespace common + +#endif // BITCOIN_COMMON_INIT_H diff --git a/src/index/base.cpp b/src/index/base.cpp index 6f2ce2efe4..7c570d4534 100644 --- a/src/index/base.cpp +++ b/src/index/base.cpp @@ -35,7 +35,7 @@ static void FatalError(const char* fmt, const Args&... args) std::string strMessage = tfm::format(fmt, args...); SetMiscWarning(Untranslated(strMessage)); LogPrintf("*** %s\n", strMessage); - AbortError(_("A fatal internal error occurred, see debug.log for details")); + InitError(_("A fatal internal error occurred, see debug.log for details")); StartShutdown(); } diff --git a/src/node/chainstate.cpp b/src/node/chainstate.cpp index 626010d26f..125d6de5a5 100644 --- a/src/node/chainstate.cpp +++ b/src/node/chainstate.cpp @@ -28,38 +28,13 @@ #include <vector> namespace node { -ChainstateLoadResult LoadChainstate(ChainstateManager& chainman, const CacheSizes& cache_sizes, - const ChainstateLoadOptions& options) +// Complete initialization of chainstates after the initial call has been made +// to ChainstateManager::InitializeChainstate(). +static ChainstateLoadResult CompleteChainstateInitialization( + ChainstateManager& chainman, + const CacheSizes& cache_sizes, + const ChainstateLoadOptions& options) EXCLUSIVE_LOCKS_REQUIRED(::cs_main) { - auto is_coinsview_empty = [&](Chainstate* chainstate) EXCLUSIVE_LOCKS_REQUIRED(::cs_main) { - return options.reindex || options.reindex_chainstate || chainstate->CoinsTip().GetBestBlock().IsNull(); - }; - - if (!chainman.AssumedValidBlock().IsNull()) { - LogPrintf("Assuming ancestors of block %s have valid signatures.\n", chainman.AssumedValidBlock().GetHex()); - } else { - LogPrintf("Validating signatures for all blocks.\n"); - } - LogPrintf("Setting nMinimumChainWork=%s\n", chainman.MinimumChainWork().GetHex()); - if (chainman.MinimumChainWork() < UintToArith256(chainman.GetConsensus().nMinimumChainWork)) { - LogPrintf("Warning: nMinimumChainWork set below default value of %s\n", chainman.GetConsensus().nMinimumChainWork.GetHex()); - } - if (chainman.m_blockman.GetPruneTarget() == std::numeric_limits<uint64_t>::max()) { - LogPrintf("Block pruning enabled. Use RPC call pruneblockchain(height) to manually prune block and undo files.\n"); - } else if (chainman.m_blockman.GetPruneTarget()) { - LogPrintf("Prune configured to target %u MiB on disk for block and undo files.\n", chainman.m_blockman.GetPruneTarget() / 1024 / 1024); - } - - LOCK(cs_main); - chainman.m_total_coinstip_cache = cache_sizes.coins; - chainman.m_total_coinsdb_cache = cache_sizes.coins_db; - - // Load the fully validated chainstate. - chainman.InitializeChainstate(options.mempool); - - // Load a chain created from a UTXO snapshot, if any exist. - chainman.DetectSnapshotChainstate(options.mempool); - auto& pblocktree{chainman.m_blockman.m_block_tree_db}; // new CBlockTreeDB tries to delete the existing file, which // fails if it's still open from the previous loop. Close it first: @@ -111,6 +86,13 @@ ChainstateLoadResult LoadChainstate(ChainstateManager& chainman, const CacheSize return {ChainstateLoadStatus::FAILURE, _("Error initializing block database")}; } + auto is_coinsview_empty = [&](Chainstate* chainstate) EXCLUSIVE_LOCKS_REQUIRED(::cs_main) { + return options.reindex || options.reindex_chainstate || chainstate->CoinsTip().GetBestBlock().IsNull(); + }; + + assert(chainman.m_total_coinstip_cache > 0); + assert(chainman.m_total_coinsdb_cache > 0); + // Conservative value which is arbitrarily chosen, as it will ultimately be changed // by a call to `chainman.MaybeRebalanceCaches()`. We just need to make sure // that the sum of the two caches (40%) does not exceed the allowable amount @@ -175,6 +157,84 @@ ChainstateLoadResult LoadChainstate(ChainstateManager& chainman, const CacheSize return {ChainstateLoadStatus::SUCCESS, {}}; } +ChainstateLoadResult LoadChainstate(ChainstateManager& chainman, const CacheSizes& cache_sizes, + const ChainstateLoadOptions& options) +{ + if (!chainman.AssumedValidBlock().IsNull()) { + LogPrintf("Assuming ancestors of block %s have valid signatures.\n", chainman.AssumedValidBlock().GetHex()); + } else { + LogPrintf("Validating signatures for all blocks.\n"); + } + LogPrintf("Setting nMinimumChainWork=%s\n", chainman.MinimumChainWork().GetHex()); + if (chainman.MinimumChainWork() < UintToArith256(chainman.GetConsensus().nMinimumChainWork)) { + LogPrintf("Warning: nMinimumChainWork set below default value of %s\n", chainman.GetConsensus().nMinimumChainWork.GetHex()); + } + if (chainman.m_blockman.GetPruneTarget() == std::numeric_limits<uint64_t>::max()) { + LogPrintf("Block pruning enabled. Use RPC call pruneblockchain(height) to manually prune block and undo files.\n"); + } else if (chainman.m_blockman.GetPruneTarget()) { + LogPrintf("Prune configured to target %u MiB on disk for block and undo files.\n", chainman.m_blockman.GetPruneTarget() / 1024 / 1024); + } + + LOCK(cs_main); + + chainman.m_total_coinstip_cache = cache_sizes.coins; + chainman.m_total_coinsdb_cache = cache_sizes.coins_db; + + // Load the fully validated chainstate. + chainman.InitializeChainstate(options.mempool); + + // Load a chain created from a UTXO snapshot, if any exist. + chainman.DetectSnapshotChainstate(options.mempool); + + auto [init_status, init_error] = CompleteChainstateInitialization(chainman, cache_sizes, options); + if (init_status != ChainstateLoadStatus::SUCCESS) { + return {init_status, init_error}; + } + + // If a snapshot chainstate was fully validated by a background chainstate during + // the last run, detect it here and clean up the now-unneeded background + // chainstate. + // + // Why is this cleanup done here (on subsequent restart) and not just when the + // snapshot is actually validated? Because this entails unusual + // filesystem operations to move leveldb data directories around, and that seems + // too risky to do in the middle of normal runtime. + auto snapshot_completion = chainman.MaybeCompleteSnapshotValidation(); + + if (snapshot_completion == SnapshotCompletionResult::SKIPPED) { + // do nothing; expected case + } else if (snapshot_completion == SnapshotCompletionResult::SUCCESS) { + LogPrintf("[snapshot] cleaning up unneeded background chainstate, then reinitializing\n"); + if (!chainman.ValidatedSnapshotCleanup()) { + AbortNode("Background chainstate cleanup failed unexpectedly."); + } + + // Because ValidatedSnapshotCleanup() has torn down chainstates with + // ChainstateManager::ResetChainstates(), reinitialize them here without + // duplicating the blockindex work above. + assert(chainman.GetAll().empty()); + assert(!chainman.IsSnapshotActive()); + assert(!chainman.IsSnapshotValidated()); + + chainman.InitializeChainstate(options.mempool); + + // A reload of the block index is required to recompute setBlockIndexCandidates + // for the fully validated chainstate. + chainman.ActiveChainstate().UnloadBlockIndex(); + + auto [init_status, init_error] = CompleteChainstateInitialization(chainman, cache_sizes, options); + if (init_status != ChainstateLoadStatus::SUCCESS) { + return {init_status, init_error}; + } + } else { + return {ChainstateLoadStatus::FAILURE, _( + "UTXO snapshot failed to validate. " + "Restart to resume normal initial block download, or try loading a different snapshot.")}; + } + + return {ChainstateLoadStatus::SUCCESS, {}}; +} + ChainstateLoadResult VerifyLoadedChainstate(ChainstateManager& chainman, const ChainstateLoadOptions& options) { auto is_coinsview_empty = [&](Chainstate* chainstate) EXCLUSIVE_LOCKS_REQUIRED(::cs_main) { diff --git a/src/node/interface_ui.cpp b/src/node/interface_ui.cpp index 08d1e03541..9dd1e7d9cf 100644 --- a/src/node/interface_ui.cpp +++ b/src/node/interface_ui.cpp @@ -4,6 +4,7 @@ #include <node/interface_ui.h> +#include <util/string.h> #include <util/translation.h> #include <boost/signals2/optional_last_value.hpp> @@ -62,6 +63,18 @@ bool InitError(const bilingual_str& str) return false; } +bool InitError(const bilingual_str& str, const std::vector<std::string>& details) +{ + // For now just flatten the list of error details into a string to pass to + // the base InitError overload. In the future, if more init code provides + // error details, the details could be passed separately from the main + // message for rich display in the GUI. But currently the only init + // functions which provide error details are ones that run during early init + // before the GUI uiInterface is registered, so there's no point passing + // main messages and details separately to uiInterface yet. + return InitError(details.empty() ? str : strprintf(Untranslated("%s:\n%s"), str, MakeUnorderedList(details))); +} + void InitWarning(const bilingual_str& str) { uiInterface.ThreadSafeMessageBox(str, "", CClientUIInterface::MSG_WARNING); diff --git a/src/node/interface_ui.h b/src/node/interface_ui.h index 9f6503b4a1..22c241cb78 100644 --- a/src/node/interface_ui.h +++ b/src/node/interface_ui.h @@ -116,7 +116,7 @@ void InitWarning(const bilingual_str& str); /** Show error message **/ bool InitError(const bilingual_str& str); -constexpr auto AbortError = InitError; +bool InitError(const bilingual_str& str, const std::vector<std::string>& details); extern CClientUIInterface uiInterface; diff --git a/src/node/minisketchwrapper.cpp b/src/node/minisketchwrapper.cpp index 67e823cb68..96707f7a0a 100644 --- a/src/node/minisketchwrapper.cpp +++ b/src/node/minisketchwrapper.cpp @@ -23,17 +23,17 @@ static constexpr uint32_t BITS = 32; uint32_t FindBestImplementation() { - std::optional<std::pair<int64_t, uint32_t>> best; + std::optional<std::pair<SteadyClock::duration, uint32_t>> best; uint32_t max_impl = Minisketch::MaxImplementation(); for (uint32_t impl = 0; impl <= max_impl; ++impl) { - std::vector<int64_t> benches; + std::vector<SteadyClock::duration> benches; uint64_t offset = 0; /* Run a little benchmark with capacity 32, adding 184 entries, and decoding 11 of them once. */ for (int b = 0; b < 11; ++b) { if (!Minisketch::ImplementationSupported(BITS, impl)) break; Minisketch sketch(BITS, impl, 32); - auto start = GetTimeMicros(); + auto start = SteadyClock::now(); for (uint64_t e = 0; e < 100; ++e) { sketch.Add(e*1337 + b*13337 + offset); } @@ -41,7 +41,7 @@ uint32_t FindBestImplementation() sketch.Add(e*1337 + b*13337 + offset); } offset += (*sketch.Decode(32))[0]; - auto stop = GetTimeMicros(); + auto stop = SteadyClock::now(); benches.push_back(stop - start); } /* Remember which implementation has the best median benchmark time. */ diff --git a/src/qt/bitcoin.cpp b/src/qt/bitcoin.cpp index 99faa51ea0..5244b72689 100644 --- a/src/qt/bitcoin.cpp +++ b/src/qt/bitcoin.cpp @@ -9,6 +9,7 @@ #include <qt/bitcoin.h> #include <chainparams.h> +#include <common/init.h> #include <init.h> #include <interfaces/handler.h> #include <interfaces/init.h> @@ -165,54 +166,36 @@ static void initTranslations(QTranslator &qtTranslatorBase, QTranslator &qtTrans } } -static bool InitSettings() +static bool ErrorSettingsRead(const bilingual_str& error, const std::vector<std::string>& details) { - gArgs.EnsureDataDir(); - if (!gArgs.GetSettingsPath()) { - return true; // Do nothing if settings file disabled. - } - - std::vector<std::string> errors; - if (!gArgs.ReadSettingsFile(&errors)) { - std::string error = QT_TRANSLATE_NOOP("bitcoin-core", "Settings file could not be read"); - std::string error_translated = QCoreApplication::translate("bitcoin-core", error.c_str()).toStdString(); - InitError(Untranslated(strprintf("%s:\n%s\n", error, MakeUnorderedList(errors)))); - - QMessageBox messagebox(QMessageBox::Critical, PACKAGE_NAME, QString::fromStdString(strprintf("%s.", error_translated)), QMessageBox::Reset | QMessageBox::Abort); - /*: Explanatory text shown on startup when the settings file cannot be read. - Prompts user to make a choice between resetting or aborting. */ - messagebox.setInformativeText(QObject::tr("Do you want to reset settings to default values, or to abort without making changes?")); - messagebox.setDetailedText(QString::fromStdString(MakeUnorderedList(errors))); - messagebox.setTextFormat(Qt::PlainText); - messagebox.setDefaultButton(QMessageBox::Reset); - switch (messagebox.exec()) { - case QMessageBox::Reset: - break; - case QMessageBox::Abort: - return false; - default: - assert(false); - } - } - - errors.clear(); - if (!gArgs.WriteSettingsFile(&errors)) { - std::string error = QT_TRANSLATE_NOOP("bitcoin-core", "Settings file could not be written"); - std::string error_translated = QCoreApplication::translate("bitcoin-core", error.c_str()).toStdString(); - InitError(Untranslated(strprintf("%s:\n%s\n", error, MakeUnorderedList(errors)))); - - QMessageBox messagebox(QMessageBox::Critical, PACKAGE_NAME, QString::fromStdString(strprintf("%s.", error_translated)), QMessageBox::Ok); - /*: Explanatory text shown on startup when the settings file could not be written. - Prompts user to check that we have the ability to write to the file. - Explains that the user has the option of running without a settings file.*/ - messagebox.setInformativeText(QObject::tr("A fatal error occurred. Check that settings file is writable, or try running with -nosettings.")); - messagebox.setDetailedText(QString::fromStdString(MakeUnorderedList(errors))); - messagebox.setTextFormat(Qt::PlainText); - messagebox.setDefaultButton(QMessageBox::Ok); - messagebox.exec(); + QMessageBox messagebox(QMessageBox::Critical, PACKAGE_NAME, QString::fromStdString(strprintf("%s.", error.translated)), QMessageBox::Reset | QMessageBox::Abort); + /*: Explanatory text shown on startup when the settings file cannot be read. + Prompts user to make a choice between resetting or aborting. */ + messagebox.setInformativeText(QObject::tr("Do you want to reset settings to default values, or to abort without making changes?")); + messagebox.setDetailedText(QString::fromStdString(MakeUnorderedList(details))); + messagebox.setTextFormat(Qt::PlainText); + messagebox.setDefaultButton(QMessageBox::Reset); + switch (messagebox.exec()) { + case QMessageBox::Reset: return false; + case QMessageBox::Abort: + return true; + default: + assert(false); } - return true; +} + +static void ErrorSettingsWrite(const bilingual_str& error, const std::vector<std::string>& details) +{ + QMessageBox messagebox(QMessageBox::Critical, PACKAGE_NAME, QString::fromStdString(strprintf("%s.", error.translated)), QMessageBox::Ok); + /*: Explanatory text shown on startup when the settings file could not be written. + Prompts user to check that we have the ability to write to the file. + Explains that the user has the option of running without a settings file.*/ + messagebox.setInformativeText(QObject::tr("A fatal error occurred. Check that settings file is writable, or try running with -nosettings.")); + messagebox.setDetailedText(QString::fromStdString(MakeUnorderedList(details))); + messagebox.setTextFormat(Qt::PlainText); + messagebox.setDefaultButton(QMessageBox::Ok); + messagebox.exec(); } /* qDebug() message handler --> debug.log */ @@ -546,7 +529,7 @@ int GuiMain(int argc, char* argv[]) SetupUIArgs(gArgs); std::string error; if (!gArgs.ParseParameters(argc, argv, error)) { - InitError(strprintf(Untranslated("Error parsing command line arguments: %s\n"), error)); + InitError(strprintf(Untranslated("Error parsing command line arguments: %s"), error)); // Create a message box, because the gui has neither been created nor has subscribed to core signals QMessageBox::critical(nullptr, PACKAGE_NAME, // message cannot be translated because translations have not been initialized @@ -587,34 +570,23 @@ int GuiMain(int argc, char* argv[]) // Gracefully exit if the user cancels if (!Intro::showIfNeeded(did_show_intro, prune_MiB)) return EXIT_SUCCESS; - /// 6a. Determine availability of data directory - if (!CheckDataDirOption(gArgs)) { - InitError(strprintf(Untranslated("Specified data directory \"%s\" does not exist.\n"), gArgs.GetArg("-datadir", ""))); - QMessageBox::critical(nullptr, PACKAGE_NAME, - QObject::tr("Error: Specified data directory \"%1\" does not exist.").arg(QString::fromStdString(gArgs.GetArg("-datadir", "")))); - return EXIT_FAILURE; - } - try { - /// 6b. Parse bitcoin.conf - /// - Do not call gArgs.GetDataDirNet() before this step finishes - if (!gArgs.ReadConfigFiles(error, true)) { - InitError(strprintf(Untranslated("Error reading configuration file: %s\n"), error)); - QMessageBox::critical(nullptr, PACKAGE_NAME, - QObject::tr("Error: Cannot parse configuration file: %1.").arg(QString::fromStdString(error))); - return EXIT_FAILURE; + /// 6-7. Parse bitcoin.conf, determine network, switch to network specific + /// options, and create datadir and settings.json. + // - Do not call gArgs.GetDataDirNet() before this step finishes + // - Do not call Params() before this step + // - QSettings() will use the new application name after this, resulting in network-specific settings + // - Needs to be done before createOptionsModel + if (auto error = common::InitConfig(gArgs, ErrorSettingsRead)) { + InitError(error->message, error->details); + if (error->status == common::ConfigStatus::FAILED_WRITE) { + // Show a custom error message to provide more information in the + // case of a datadir write error. + ErrorSettingsWrite(error->message, error->details); + } else if (error->status != common::ConfigStatus::ABORTED) { + // Show a generic message in other cases, and no additional error + // message in the case of a read error if the user decided to abort. + QMessageBox::critical(nullptr, PACKAGE_NAME, QObject::tr("Error: %1").arg(QString::fromStdString(error->message.translated))); } - - /// 7. Determine network (and switch to network specific options) - // - Do not call Params() before this step - // - Do this after parsing the configuration file, as the network can be switched there - // - QSettings() will use the new application name after this, resulting in network-specific settings - // - Needs to be done before createOptionsModel - - // Check for chain settings (Params() calls are only valid after this clause) - SelectParams(gArgs.GetChainName()); - } catch(std::exception &e) { - InitError(Untranslated(strprintf("%s\n", e.what()))); - QMessageBox::critical(nullptr, PACKAGE_NAME, QObject::tr("Error: %1").arg(e.what())); return EXIT_FAILURE; } #ifdef ENABLE_WALLET @@ -622,10 +594,6 @@ int GuiMain(int argc, char* argv[]) PaymentServer::ipcParseCommandLine(argc, argv); #endif - if (!InitSettings()) { - return EXIT_FAILURE; - } - QScopedPointer<const NetworkStyle> networkStyle(NetworkStyle::instantiate(Params().NetworkIDString())); assert(!networkStyle.isNull()); // Allow for separate UI settings for testnets diff --git a/src/random.cpp b/src/random.cpp index 432592589a..f4c51574cc 100644 --- a/src/random.cpp +++ b/src/random.cpp @@ -221,14 +221,14 @@ static void SeedHardwareSlow(CSHA512& hasher) noexcept { } /** Use repeated SHA512 to strengthen the randomness in seed32, and feed into hasher. */ -static void Strengthen(const unsigned char (&seed)[32], int microseconds, CSHA512& hasher) noexcept +static void Strengthen(const unsigned char (&seed)[32], SteadyClock::duration dur, CSHA512& hasher) noexcept { CSHA512 inner_hasher; inner_hasher.Write(seed, sizeof(seed)); // Hash loop unsigned char buffer[64]; - int64_t stop = GetTimeMicros() + microseconds; + const auto stop{SteadyClock::now() + dur}; do { for (int i = 0; i < 1000; ++i) { inner_hasher.Finalize(buffer); @@ -238,7 +238,7 @@ static void Strengthen(const unsigned char (&seed)[32], int microseconds, CSHA51 // Benchmark operation and feed it into outer hasher. int64_t perf = GetPerformanceCounter(); hasher.Write((const unsigned char*)&perf, sizeof(perf)); - } while (GetTimeMicros() < stop); + } while (SteadyClock::now() < stop); // Produce output from inner state and feed it to outer hasher. inner_hasher.Finalize(buffer); @@ -492,13 +492,13 @@ static void SeedSlow(CSHA512& hasher, RNGState& rng) noexcept } /** Extract entropy from rng, strengthen it, and feed it into hasher. */ -static void SeedStrengthen(CSHA512& hasher, RNGState& rng, int microseconds) noexcept +static void SeedStrengthen(CSHA512& hasher, RNGState& rng, SteadyClock::duration dur) noexcept { // Generate 32 bytes of entropy from the RNG, and a copy of the entropy already in hasher. unsigned char strengthen_seed[32]; rng.MixExtract(strengthen_seed, sizeof(strengthen_seed), CSHA512(hasher), false); // Strengthen the seed, and feed it into hasher. - Strengthen(strengthen_seed, microseconds, hasher); + Strengthen(strengthen_seed, dur, hasher); } static void SeedPeriodic(CSHA512& hasher, RNGState& rng) noexcept @@ -518,7 +518,7 @@ static void SeedPeriodic(CSHA512& hasher, RNGState& rng) noexcept LogPrint(BCLog::RAND, "Feeding %i bytes of dynamic environment data into RNG\n", hasher.Size() - old_size); // Strengthen for 10 ms - SeedStrengthen(hasher, rng, 10000); + SeedStrengthen(hasher, rng, 10ms); } static void SeedStartup(CSHA512& hasher, RNGState& rng) noexcept @@ -538,7 +538,7 @@ static void SeedStartup(CSHA512& hasher, RNGState& rng) noexcept LogPrint(BCLog::RAND, "Feeding %i bytes of environment data into RNG\n", hasher.Size() - old_size); // Strengthen for 100 ms - SeedStrengthen(hasher, rng, 100000); + SeedStrengthen(hasher, rng, 100ms); } enum class RNGLevel { diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp index 5ed8aee9ea..21d49fda9d 100644 --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -216,10 +216,10 @@ static RPCHelpMan getrawtransaction() {RPCResult::Type::NUM, "fee", /*optional=*/true, "transaction fee in " + CURRENCY_UNIT + ", omitted if block undo data is not available"}, {RPCResult::Type::ARR, "vin", "", { - {RPCResult::Type::OBJ, "", "utxo being spent, omitted if block undo data is not available", + {RPCResult::Type::OBJ, "", "utxo being spent", { {RPCResult::Type::ELISION, "", "Same output as verbosity = 1"}, - {RPCResult::Type::OBJ, "prevout", /*optional=*/true, "Only if undo information is available)", + {RPCResult::Type::OBJ, "prevout", /*optional=*/true, "The previous output, omitted if block undo data is not available", { {RPCResult::Type::BOOL, "generated", "Coinbase or not"}, {RPCResult::Type::NUM, "height", "The height of the prevout"}, diff --git a/src/shutdown.cpp b/src/shutdown.cpp index 57d6d2325d..2fffc0663c 100644 --- a/src/shutdown.cpp +++ b/src/shutdown.cpp @@ -27,7 +27,7 @@ bool AbortNode(const std::string& strMessage, bilingual_str user_message) if (user_message.empty()) { user_message = _("A fatal internal error occurred, see debug.log for details"); } - AbortError(user_message); + InitError(user_message); StartShutdown(); return false; } diff --git a/src/test/translation_tests.cpp b/src/test/translation_tests.cpp new file mode 100644 index 0000000000..bda5dfd099 --- /dev/null +++ b/src/test/translation_tests.cpp @@ -0,0 +1,21 @@ +// Copyright (c) 2023 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 <tinyformat.h> +#include <util/translation.h> + +#include <boost/test/unit_test.hpp> + +BOOST_AUTO_TEST_SUITE(translation_tests) + +BOOST_AUTO_TEST_CASE(translation_namedparams) +{ + bilingual_str arg{"original", "translated"}; + bilingual_str format{"original [%s]", "translated [%s]"}; + bilingual_str result{strprintf(format, arg)}; + BOOST_CHECK_EQUAL(result.original, "original [original]"); + BOOST_CHECK_EQUAL(result.translated, "translated [translated]"); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/validation_chainstatemanager_tests.cpp b/src/test/validation_chainstatemanager_tests.cpp index 78301c7c14..6fc9d0fa51 100644 --- a/src/test/validation_chainstatemanager_tests.cpp +++ b/src/test/validation_chainstatemanager_tests.cpp @@ -474,9 +474,10 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager_loadblockindex, TestChain100Setup) //! Ensure that snapshot chainstates initialize properly when found on disk. BOOST_FIXTURE_TEST_CASE(chainstatemanager_snapshot_init, SnapshotTestSetup) { - this->SetupSnapshot(); - ChainstateManager& chainman = *Assert(m_node.chainman); + Chainstate& bg_chainstate = chainman.ActiveChainstate(); + + this->SetupSnapshot(); fs::path snapshot_chainstate_dir = *node::FindSnapshotChainstateDir(); BOOST_CHECK(fs::exists(snapshot_chainstate_dir)); @@ -489,6 +490,20 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager_snapshot_init, SnapshotTestSetup) auto all_chainstates = chainman.GetAll(); BOOST_CHECK_EQUAL(all_chainstates.size(), 2); + // "Rewind" the background chainstate so that its tip is not at the + // base block of the snapshot - this is so after simulating a node restart, + // it will initialize instead of attempting to complete validation. + // + // Note that this is not a realistic use of DisconnectTip(). + DisconnectedBlockTransactions unused_pool; + BlockValidationState unused_state; + { + LOCK2(::cs_main, bg_chainstate.MempoolMutex()); + BOOST_CHECK(bg_chainstate.DisconnectTip(unused_state, &unused_pool)); + unused_pool.clear(); // to avoid queuedTx assertion errors on teardown + } + BOOST_CHECK_EQUAL(bg_chainstate.m_chain.Height(), 109); + // Test that simulating a shutdown (resetting ChainstateManager) and then performing // chainstate reinitializing successfully cleans up the background-validation // chainstate data, and we end up with a single chainstate that is at tip. @@ -520,10 +535,160 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager_snapshot_init, SnapshotTestSetup) // chainstate. for (Chainstate* cs : chainman_restarted.GetAll()) { if (cs != &chainman_restarted.ActiveChainstate()) { - BOOST_CHECK_EQUAL(cs->m_chain.Height(), 110); + BOOST_CHECK_EQUAL(cs->m_chain.Height(), 109); } } } } +BOOST_FIXTURE_TEST_CASE(chainstatemanager_snapshot_completion, SnapshotTestSetup) +{ + this->SetupSnapshot(); + + ChainstateManager& chainman = *Assert(m_node.chainman); + Chainstate& active_cs = chainman.ActiveChainstate(); + auto tip_cache_before_complete = active_cs.m_coinstip_cache_size_bytes; + auto db_cache_before_complete = active_cs.m_coinsdb_cache_size_bytes; + + SnapshotCompletionResult res; + auto mock_shutdown = [](bilingual_str msg) {}; + + fs::path snapshot_chainstate_dir = *node::FindSnapshotChainstateDir(); + BOOST_CHECK(fs::exists(snapshot_chainstate_dir)); + BOOST_CHECK_EQUAL(snapshot_chainstate_dir, gArgs.GetDataDirNet() / "chainstate_snapshot"); + + BOOST_CHECK(chainman.IsSnapshotActive()); + const uint256 snapshot_tip_hash = WITH_LOCK(chainman.GetMutex(), + return chainman.ActiveTip()->GetBlockHash()); + + res = WITH_LOCK(::cs_main, + return chainman.MaybeCompleteSnapshotValidation(mock_shutdown)); + BOOST_CHECK_EQUAL(res, SnapshotCompletionResult::SUCCESS); + + WITH_LOCK(::cs_main, BOOST_CHECK(chainman.IsSnapshotValidated())); + BOOST_CHECK(chainman.IsSnapshotActive()); + + // Cache should have been rebalanced and reallocated to the "only" remaining + // chainstate. + BOOST_CHECK(active_cs.m_coinstip_cache_size_bytes > tip_cache_before_complete); + BOOST_CHECK(active_cs.m_coinsdb_cache_size_bytes > db_cache_before_complete); + + auto all_chainstates = chainman.GetAll(); + BOOST_CHECK_EQUAL(all_chainstates.size(), 1); + BOOST_CHECK_EQUAL(all_chainstates[0], &active_cs); + + // Trying completion again should return false. + res = WITH_LOCK(::cs_main, + return chainman.MaybeCompleteSnapshotValidation(mock_shutdown)); + BOOST_CHECK_EQUAL(res, SnapshotCompletionResult::SKIPPED); + + // The invalid snapshot path should not have been used. + fs::path snapshot_invalid_dir = gArgs.GetDataDirNet() / "chainstate_snapshot_INVALID"; + BOOST_CHECK(!fs::exists(snapshot_invalid_dir)); + // chainstate_snapshot should still exist. + BOOST_CHECK(fs::exists(snapshot_chainstate_dir)); + + // Test that simulating a shutdown (reseting ChainstateManager) and then performing + // chainstate reinitializing successfully cleans up the background-validation + // chainstate data, and we end up with a single chainstate that is at tip. + ChainstateManager& chainman_restarted = this->SimulateNodeRestart(); + + BOOST_TEST_MESSAGE("Performing Load/Verify/Activate of chainstate"); + + // This call reinitializes the chainstates, and should clean up the now unnecessary + // background-validation leveldb contents. + this->LoadVerifyActivateChainstate(); + + BOOST_CHECK(!fs::exists(snapshot_invalid_dir)); + // chainstate_snapshot should now *not* exist. + BOOST_CHECK(!fs::exists(snapshot_chainstate_dir)); + + const Chainstate& active_cs2 = chainman_restarted.ActiveChainstate(); + + { + LOCK(chainman_restarted.GetMutex()); + BOOST_CHECK_EQUAL(chainman_restarted.GetAll().size(), 1); + BOOST_CHECK(!chainman_restarted.IsSnapshotActive()); + BOOST_CHECK(!chainman_restarted.IsSnapshotValidated()); + BOOST_CHECK(active_cs2.m_coinstip_cache_size_bytes > tip_cache_before_complete); + BOOST_CHECK(active_cs2.m_coinsdb_cache_size_bytes > db_cache_before_complete); + + BOOST_CHECK_EQUAL(chainman_restarted.ActiveTip()->GetBlockHash(), snapshot_tip_hash); + BOOST_CHECK_EQUAL(chainman_restarted.ActiveHeight(), 210); + } + + BOOST_TEST_MESSAGE( + "Ensure we can mine blocks on top of the \"new\" IBD chainstate"); + mineBlocks(10); + { + LOCK(chainman_restarted.GetMutex()); + BOOST_CHECK_EQUAL(chainman_restarted.ActiveHeight(), 220); + } +} + +BOOST_FIXTURE_TEST_CASE(chainstatemanager_snapshot_completion_hash_mismatch, SnapshotTestSetup) +{ + auto chainstates = this->SetupSnapshot(); + Chainstate& validation_chainstate = *std::get<0>(chainstates); + ChainstateManager& chainman = *Assert(m_node.chainman); + SnapshotCompletionResult res; + auto mock_shutdown = [](bilingual_str msg) {}; + + // Test tampering with the IBD UTXO set with an extra coin to ensure it causes + // snapshot completion to fail. + CCoinsViewCache& ibd_coins = WITH_LOCK(::cs_main, + return validation_chainstate.CoinsTip()); + Coin badcoin; + badcoin.out.nValue = InsecureRand32(); + badcoin.nHeight = 1; + badcoin.out.scriptPubKey.assign(InsecureRandBits(6), 0); + uint256 txid = InsecureRand256(); + ibd_coins.AddCoin(COutPoint(txid, 0), std::move(badcoin), false); + + fs::path snapshot_chainstate_dir = gArgs.GetDataDirNet() / "chainstate_snapshot"; + BOOST_CHECK(fs::exists(snapshot_chainstate_dir)); + + res = WITH_LOCK(::cs_main, + return chainman.MaybeCompleteSnapshotValidation(mock_shutdown)); + BOOST_CHECK_EQUAL(res, SnapshotCompletionResult::HASH_MISMATCH); + + auto all_chainstates = chainman.GetAll(); + BOOST_CHECK_EQUAL(all_chainstates.size(), 1); + BOOST_CHECK_EQUAL(all_chainstates[0], &validation_chainstate); + BOOST_CHECK_EQUAL(&chainman.ActiveChainstate(), &validation_chainstate); + + fs::path snapshot_invalid_dir = gArgs.GetDataDirNet() / "chainstate_snapshot_INVALID"; + BOOST_CHECK(fs::exists(snapshot_invalid_dir)); + + // Test that simulating a shutdown (reseting ChainstateManager) and then performing + // chainstate reinitializing successfully loads only the fully-validated + // chainstate data, and we end up with a single chainstate that is at tip. + ChainstateManager& chainman_restarted = this->SimulateNodeRestart(); + + BOOST_TEST_MESSAGE("Performing Load/Verify/Activate of chainstate"); + + // This call reinitializes the chainstates, and should clean up the now unnecessary + // background-validation leveldb contents. + this->LoadVerifyActivateChainstate(); + + BOOST_CHECK(fs::exists(snapshot_invalid_dir)); + BOOST_CHECK(!fs::exists(snapshot_chainstate_dir)); + + { + LOCK(::cs_main); + BOOST_CHECK_EQUAL(chainman_restarted.GetAll().size(), 1); + BOOST_CHECK(!chainman_restarted.IsSnapshotActive()); + BOOST_CHECK(!chainman_restarted.IsSnapshotValidated()); + BOOST_CHECK_EQUAL(chainman_restarted.ActiveHeight(), 210); + } + + BOOST_TEST_MESSAGE( + "Ensure we can mine blocks on top of the \"new\" IBD chainstate"); + mineBlocks(10); + { + LOCK(::cs_main); + BOOST_CHECK_EQUAL(chainman_restarted.ActiveHeight(), 220); + } +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/src/util/system.cpp b/src/util/system.cpp index 58afd264ae..5b1a1659bf 100644 --- a/src/util/system.cpp +++ b/src/util/system.cpp @@ -438,27 +438,6 @@ const fs::path& ArgsManager::GetDataDir(bool net_specific) const return path; } -void ArgsManager::EnsureDataDir() const -{ - /** - * "/wallets" subdirectories are created in all **new** - * datadirs, because wallet code will create new wallets in the "wallets" - * subdirectory only if exists already, otherwise it will create them in - * the top-level datadir where they could interfere with other files. - * Wallet init code currently avoids creating "wallets" directories itself - * for backwards compatibility, but this be changed in the future and - * wallet code here could go away (#16220). - */ - auto path{GetDataDir(false)}; - if (!fs::exists(path)) { - fs::create_directories(path / "wallets"); - } - path = GetDataDir(true); - if (!fs::exists(path)) { - fs::create_directories(path / "wallets"); - } -} - void ArgsManager::ClearPathCache() { LOCK(cs_args); @@ -502,25 +481,6 @@ bool ArgsManager::IsArgSet(const std::string& strArg) const return !GetSetting(strArg).isNull(); } -bool ArgsManager::InitSettings(std::string& error) -{ - EnsureDataDir(); - if (!GetSettingsPath()) { - return true; // Do nothing if settings file disabled. - } - - std::vector<std::string> errors; - if (!ReadSettingsFile(&errors)) { - error = strprintf("Failed loading settings file:\n%s\n", MakeUnorderedList(errors)); - return false; - } - if (!WriteSettingsFile(&errors)) { - error = strprintf("Failed saving settings file:\n%s\n", MakeUnorderedList(errors)); - return false; - } - return true; -} - bool ArgsManager::GetSettingsPath(fs::path* filepath, bool temp, bool backup) const { fs::path settings = GetPathArg("-settings", BITCOIN_SETTINGS_FILENAME); diff --git a/src/util/system.h b/src/util/system.h index 3eb0a0f2b8..f7bebe1f2a 100644 --- a/src/util/system.h +++ b/src/util/system.h @@ -435,13 +435,6 @@ protected: std::optional<unsigned int> GetArgFlags(const std::string& name) const; /** - * Read and update settings file with saved settings. This needs to be - * called after SelectParams() because the settings file location is - * network-specific. - */ - bool InitSettings(std::string& error); - - /** * Get settings file path, or return false if read-write settings were * disabled with -nosettings. */ @@ -480,12 +473,6 @@ protected: */ void LogArgs() const; - /** - * If datadir does not exist, create it along with wallets/ - * subdirectory(s). - */ - void EnsureDataDir() const; - private: /** * Get data directory path diff --git a/src/util/time.h b/src/util/time.h index d45baaa378..fcf85c1e03 100644 --- a/src/util/time.h +++ b/src/util/time.h @@ -8,7 +8,7 @@ #include <compat/compat.h> -#include <chrono> +#include <chrono> // IWYU pragma: export #include <cstdint> #include <string> diff --git a/src/util/translation.h b/src/util/translation.h index 05e7da0b5a..d2b49d00b0 100644 --- a/src/util/translation.h +++ b/src/util/translation.h @@ -47,11 +47,24 @@ inline bilingual_str operator+(bilingual_str lhs, const bilingual_str& rhs) /** Mark a bilingual_str as untranslated */ inline bilingual_str Untranslated(std::string original) { return {original, original}; } +// Provide an overload of tinyformat::format which can take bilingual_str arguments. namespace tinyformat { +inline std::string TranslateArg(const bilingual_str& arg, bool translated) +{ + return translated ? arg.translated : arg.original; +} + +template <typename T> +inline T const& TranslateArg(const T& arg, bool translated) +{ + return arg; +} + template <typename... Args> bilingual_str format(const bilingual_str& fmt, const Args&... args) { - return bilingual_str{format(fmt.original, args...), format(fmt.translated, args...)}; + return bilingual_str{format(fmt.original, TranslateArg(args, false)...), + format(fmt.translated, TranslateArg(args, true)...)}; } } // namespace tinyformat diff --git a/src/validation.cpp b/src/validation.cpp index 0674454883..f3c0401c0f 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -2478,12 +2478,12 @@ bool Chainstate::FlushStateToDisk( } } } - const auto nNow = GetTime<std::chrono::microseconds>(); + const auto nNow{SteadyClock::now()}; // Avoid writing/flushing immediately after startup. - if (m_last_write.count() == 0) { + if (m_last_write == decltype(m_last_write){}) { m_last_write = nNow; } - if (m_last_flush.count() == 0) { + if (m_last_flush == decltype(m_last_flush){}) { m_last_flush = nNow; } // The cache is large and we're within 10% and 10 MiB of the limit, but we have time now (not in the middle of a block processing). @@ -2544,7 +2544,7 @@ bool Chainstate::FlushStateToDisk( m_last_flush = nNow; full_flush_completed = true; TRACE5(utxocache, flush, - (int64_t)(GetTimeMicros() - nNow.count()), // in microseconds (µs) + int64_t{Ticks<std::chrono::microseconds>(SteadyClock::now() - nNow)}, (uint32_t)mode, (uint64_t)coins_count, (uint64_t)coins_mem_usage, @@ -2875,6 +2875,14 @@ bool Chainstate::ConnectTip(BlockValidationState& state, CBlockIndex* pindexNew, Ticks<SecondsDouble>(time_total), Ticks<MillisecondsDouble>(time_total) / num_blocks_total); + // If we are the background validation chainstate, check to see if we are done + // validating the snapshot (i.e. our tip has reached the snapshot's base block). + if (this != &m_chainman.ActiveChainstate()) { + // This call may set `m_disabled`, which is referenced immediately afterwards in + // ActivateBestChain, so that we stop connecting blocks past the snapshot base. + m_chainman.MaybeCompleteSnapshotValidation(); + } + connectTrace.BlockConnected(pindexNew, std::move(pthisBlock)); return true; } @@ -3097,6 +3105,14 @@ bool Chainstate::ActivateBestChain(BlockValidationState& state, std::shared_ptr< // we use m_chainstate_mutex to enforce mutual exclusion so that only one caller may execute this function at a time LOCK(m_chainstate_mutex); + // Belt-and-suspenders check that we aren't attempting to advance the background + // chainstate past the snapshot base block. + if (WITH_LOCK(::cs_main, return m_disabled)) { + LogPrintf("m_disabled is set - this chainstate should not be in operation. " /* Continued */ + "Please report this as a bug. %s\n", PACKAGE_BUGREPORT); + return false; + } + CBlockIndex *pindexMostWork = nullptr; CBlockIndex *pindexNewTip = nullptr; int nStopAtHeight = gArgs.GetIntArg("-stopatheight", DEFAULT_STOPATHEIGHT); @@ -3147,6 +3163,15 @@ bool Chainstate::ActivateBestChain(BlockValidationState& state, std::shared_ptr< assert(trace.pblock && trace.pindex); GetMainSignals().BlockConnected(trace.pblock, trace.pindex); } + + // This will have been toggled in + // ActivateBestChainStep -> ConnectTip -> MaybeCompleteSnapshotValidation, + // if at all, so we should catch it here. + // + // Break this do-while to ensure we don't advance past the base snapshot. + if (m_disabled) { + break; + } } while (!m_chain.Tip() || (starting_tip && CBlockIndexWorkComparator()(m_chain.Tip(), starting_tip))); if (!blocks_connected) return true; @@ -3167,6 +3192,11 @@ bool Chainstate::ActivateBestChain(BlockValidationState& state, std::shared_ptr< if (nStopAtHeight && pindexNewTip && pindexNewTip->nHeight >= nStopAtHeight) StartShutdown(); + if (WITH_LOCK(::cs_main, return m_disabled)) { + // Background chainstate has reached the snapshot base block, so exit. + break; + } + // We check shutdown only after giving ActivateBestChainStep a chance to run once so that we // never shutdown before connecting the genesis block during LoadChainTip(). Previously this // caused an assert() failure during shutdown in such cases as the UTXO DB flushing checks @@ -4372,6 +4402,8 @@ bool ChainstateManager::LoadBlockIndex() assert(any_chain([](auto chainstate) { return !chainstate->reliesOnAssumedValid(); })); first_assumed_valid_height = block->nHeight; + LogPrintf("Saw first assumedvalid block at height %d (%s)\n", + first_assumed_valid_height, block->ToString()); break; } } @@ -4908,12 +4940,8 @@ std::vector<Chainstate*> ChainstateManager::GetAll() LOCK(::cs_main); std::vector<Chainstate*> out; - if (!IsSnapshotValidated() && m_ibd_chainstate) { - out.push_back(m_ibd_chainstate.get()); - } - - if (m_snapshot_chainstate) { - out.push_back(m_snapshot_chainstate.get()); + for (Chainstate* cs : {m_ibd_chainstate.get(), m_snapshot_chainstate.get()}) { + if (this->IsUsable(cs)) out.push_back(cs); } return out; @@ -5099,6 +5127,19 @@ static void FlushSnapshotToDisk(CCoinsViewCache& coins_cache, bool snapshot_load coins_cache.Flush(); } +struct StopHashingException : public std::exception +{ + const char* what() const throw() override + { + return "ComputeUTXOStats interrupted by shutdown."; + } +}; + +static void SnapshotUTXOHashBreakpoint() +{ + if (ShutdownRequested()) throw StopHashingException(); +} + bool ChainstateManager::PopulateAndValidateSnapshot( Chainstate& snapshot_chainstate, AutoFile& coins_file, @@ -5222,13 +5263,18 @@ bool ChainstateManager::PopulateAndValidateSnapshot( assert(coins_cache.GetBestBlock() == base_blockhash); - auto breakpoint_fnc = [] { /* TODO insert breakpoint here? */ }; - // As above, okay to immediately release cs_main here since no other context knows // about the snapshot_chainstate. CCoinsViewDB* snapshot_coinsdb = WITH_LOCK(::cs_main, return &snapshot_chainstate.CoinsDB()); - const std::optional<CCoinsStats> maybe_stats = ComputeUTXOStats(CoinStatsHashType::HASH_SERIALIZED, snapshot_coinsdb, m_blockman, breakpoint_fnc); + std::optional<CCoinsStats> maybe_stats; + + try { + maybe_stats = ComputeUTXOStats( + CoinStatsHashType::HASH_SERIALIZED, snapshot_coinsdb, m_blockman, SnapshotUTXOHashBreakpoint); + } catch (StopHashingException const&) { + return false; + } if (!maybe_stats.has_value()) { LogPrintf("[snapshot] failed to generate coins stats\n"); return false; @@ -5296,6 +5342,149 @@ bool ChainstateManager::PopulateAndValidateSnapshot( return true; } +// Currently, this function holds cs_main for its duration, which could be for +// multiple minutes due to the ComputeUTXOStats call. This hold is necessary +// because we need to avoid advancing the background validation chainstate +// farther than the snapshot base block - and this function is also invoked +// from within ConnectTip, i.e. from within ActivateBestChain, so cs_main is +// held anyway. +// +// Eventually (TODO), we could somehow separate this function's runtime from +// maintenance of the active chain, but that will either require +// +// (i) setting `m_disabled` immediately and ensuring all chainstate accesses go +// through IsUsable() checks, or +// +// (ii) giving each chainstate its own lock instead of using cs_main for everything. +SnapshotCompletionResult ChainstateManager::MaybeCompleteSnapshotValidation( + std::function<void(bilingual_str)> shutdown_fnc) +{ + AssertLockHeld(cs_main); + if (m_ibd_chainstate.get() == &this->ActiveChainstate() || + !this->IsUsable(m_snapshot_chainstate.get()) || + !this->IsUsable(m_ibd_chainstate.get()) || + !m_ibd_chainstate->m_chain.Tip()) { + // Nothing to do - this function only applies to the background + // validation chainstate. + return SnapshotCompletionResult::SKIPPED; + } + const int snapshot_tip_height = this->ActiveHeight(); + const int snapshot_base_height = *Assert(this->GetSnapshotBaseHeight()); + const CBlockIndex& index_new = *Assert(m_ibd_chainstate->m_chain.Tip()); + + if (index_new.nHeight < snapshot_base_height) { + // Background IBD not complete yet. + return SnapshotCompletionResult::SKIPPED; + } + + assert(SnapshotBlockhash()); + uint256 snapshot_blockhash = *Assert(SnapshotBlockhash()); + + auto handle_invalid_snapshot = [&]() EXCLUSIVE_LOCKS_REQUIRED(::cs_main) { + bilingual_str user_error = strprintf(_( + "%s failed to validate the -assumeutxo snapshot state. " + "This indicates a hardware problem, or a bug in the software, or a " + "bad software modification that allowed an invalid snapshot to be " + "loaded. As a result of this, the node will shut down and stop using any " + "state that was built on the snapshot, resetting the chain height " + "from %d to %d. On the next " + "restart, the node will resume syncing from %d " + "without using any snapshot data. " + "Please report this incident to %s, including how you obtained the snapshot. " + "The invalid snapshot chainstate has been left on disk in case it is " + "helpful in diagnosing the issue that caused this error."), + PACKAGE_NAME, snapshot_tip_height, snapshot_base_height, snapshot_base_height, PACKAGE_BUGREPORT + ); + + LogPrintf("[snapshot] !!! %s\n", user_error.original); + LogPrintf("[snapshot] deleting snapshot, reverting to validated chain, and stopping node\n"); + + m_active_chainstate = m_ibd_chainstate.get(); + m_snapshot_chainstate->m_disabled = true; + assert(!this->IsUsable(m_snapshot_chainstate.get())); + assert(this->IsUsable(m_ibd_chainstate.get())); + + m_snapshot_chainstate->InvalidateCoinsDBOnDisk(); + + shutdown_fnc(user_error); + }; + + if (index_new.GetBlockHash() != snapshot_blockhash) { + LogPrintf("[snapshot] supposed base block %s does not match the " /* Continued */ + "snapshot base block %s (height %d). Snapshot is not valid.", + index_new.ToString(), snapshot_blockhash.ToString(), snapshot_base_height); + handle_invalid_snapshot(); + return SnapshotCompletionResult::BASE_BLOCKHASH_MISMATCH; + } + + assert(index_new.nHeight == snapshot_base_height); + + int curr_height = m_ibd_chainstate->m_chain.Height(); + + assert(snapshot_base_height == curr_height); + assert(snapshot_base_height == index_new.nHeight); + assert(this->IsUsable(m_snapshot_chainstate.get())); + assert(this->GetAll().size() == 2); + + CCoinsViewDB& ibd_coins_db = m_ibd_chainstate->CoinsDB(); + m_ibd_chainstate->ForceFlushStateToDisk(); + + auto maybe_au_data = ExpectedAssumeutxo(curr_height, ::Params()); + if (!maybe_au_data) { + LogPrintf("[snapshot] assumeutxo data not found for height " /* Continued */ + "(%d) - refusing to validate snapshot\n", curr_height); + handle_invalid_snapshot(); + return SnapshotCompletionResult::MISSING_CHAINPARAMS; + } + + const AssumeutxoData& au_data = *maybe_au_data; + std::optional<CCoinsStats> maybe_ibd_stats; + LogPrintf("[snapshot] computing UTXO stats for background chainstate to validate " /* Continued */ + "snapshot - this could take a few minutes\n"); + try { + maybe_ibd_stats = ComputeUTXOStats( + CoinStatsHashType::HASH_SERIALIZED, + &ibd_coins_db, + m_blockman, + SnapshotUTXOHashBreakpoint); + } catch (StopHashingException const&) { + return SnapshotCompletionResult::STATS_FAILED; + } + + // XXX note that this function is slow and will hold cs_main for potentially minutes. + if (!maybe_ibd_stats) { + LogPrintf("[snapshot] failed to generate stats for validation coins db\n"); + // While this isn't a problem with the snapshot per se, this condition + // prevents us from validating the snapshot, so we should shut down and let the + // user handle the issue manually. + handle_invalid_snapshot(); + return SnapshotCompletionResult::STATS_FAILED; + } + const auto& ibd_stats = *maybe_ibd_stats; + + // Compare the background validation chainstate's UTXO set hash against the hard-coded + // assumeutxo hash we expect. + // + // TODO: For belt-and-suspenders, we could cache the UTXO set + // hash for the snapshot when it's loaded in its chainstate's leveldb. We could then + // reference that here for an additional check. + if (AssumeutxoHash{ibd_stats.hashSerialized} != au_data.hash_serialized) { + LogPrintf("[snapshot] hash mismatch: actual=%s, expected=%s\n", + ibd_stats.hashSerialized.ToString(), + au_data.hash_serialized.ToString()); + handle_invalid_snapshot(); + return SnapshotCompletionResult::HASH_MISMATCH; + } + + LogPrintf("[snapshot] snapshot beginning at %s has been fully validated\n", + snapshot_blockhash.ToString()); + + m_ibd_chainstate->m_disabled = true; + this->MaybeRebalanceCaches(); + + return SnapshotCompletionResult::SUCCESS; +} + Chainstate& ChainstateManager::ActiveChainstate() const { LOCK(::cs_main); @@ -5312,17 +5501,22 @@ bool ChainstateManager::IsSnapshotActive() const void ChainstateManager::MaybeRebalanceCaches() { AssertLockHeld(::cs_main); - if (m_ibd_chainstate && !m_snapshot_chainstate) { + bool ibd_usable = this->IsUsable(m_ibd_chainstate.get()); + bool snapshot_usable = this->IsUsable(m_snapshot_chainstate.get()); + assert(ibd_usable || snapshot_usable); + + if (ibd_usable && !snapshot_usable) { LogPrintf("[snapshot] allocating all cache to the IBD chainstate\n"); // Allocate everything to the IBD chainstate. m_ibd_chainstate->ResizeCoinsCaches(m_total_coinstip_cache, m_total_coinsdb_cache); } - else if (m_snapshot_chainstate && !m_ibd_chainstate) { + else if (snapshot_usable && !ibd_usable) { + // If background validation has completed and snapshot is our active chain... LogPrintf("[snapshot] allocating all cache to the snapshot chainstate\n"); // Allocate everything to the snapshot chainstate. m_snapshot_chainstate->ResizeCoinsCaches(m_total_coinstip_cache, m_total_coinsdb_cache); } - else if (m_ibd_chainstate && m_snapshot_chainstate) { + else if (ibd_usable && snapshot_usable) { // If both chainstates exist, determine who needs more cache based on IBD status. // // Note: shrink caches first so that we don't inadvertently overwhelm available memory. @@ -5414,3 +5608,141 @@ bool IsBIP30Unspendable(const CBlockIndex& block_index) return (block_index.nHeight==91722 && block_index.GetBlockHash() == uint256S("0x00000000000271a2dc26e7667f8419f2e15416dc6955e5a6c6cdf3f2574dd08e")) || (block_index.nHeight==91812 && block_index.GetBlockHash() == uint256S("0x00000000000af0aed4792b1acee3d966af36cf5def14935db8de83d6f9306f2f")); } + +void Chainstate::InvalidateCoinsDBOnDisk() +{ + AssertLockHeld(::cs_main); + // Should never be called on a non-snapshot chainstate. + assert(m_from_snapshot_blockhash); + auto storage_path_maybe = this->CoinsDB().StoragePath(); + // Should never be called with a non-existent storage path. + assert(storage_path_maybe); + fs::path snapshot_datadir = *storage_path_maybe; + + // Coins views no longer usable. + m_coins_views.reset(); + + auto invalid_path = snapshot_datadir + "_INVALID"; + std::string dbpath = fs::PathToString(snapshot_datadir); + std::string target = fs::PathToString(invalid_path); + LogPrintf("[snapshot] renaming snapshot datadir %s to %s\n", dbpath, target); + + // The invalid snapshot datadir is simply moved and not deleted because we may + // want to do forensics later during issue investigation. The user is instructed + // accordingly in MaybeCompleteSnapshotValidation(). + try { + fs::rename(snapshot_datadir, invalid_path); + } catch (const fs::filesystem_error& e) { + auto src_str = fs::PathToString(snapshot_datadir); + auto dest_str = fs::PathToString(invalid_path); + + LogPrintf("%s: error renaming file '%s' -> '%s': %s\n", + __func__, src_str, dest_str, e.what()); + AbortNode(strprintf( + "Rename of '%s' -> '%s' failed. " + "You should resolve this by manually moving or deleting the invalid " + "snapshot directory %s, otherwise you will encounter the same error again " + "on the next startup.", + src_str, dest_str, src_str)); + } +} + +const CBlockIndex* ChainstateManager::GetSnapshotBaseBlock() const +{ + const auto blockhash_op = this->SnapshotBlockhash(); + if (!blockhash_op) return nullptr; + return Assert(m_blockman.LookupBlockIndex(*blockhash_op)); +} + +std::optional<int> ChainstateManager::GetSnapshotBaseHeight() const +{ + const CBlockIndex* base = this->GetSnapshotBaseBlock(); + return base ? std::make_optional(base->nHeight) : std::nullopt; +} + +bool ChainstateManager::ValidatedSnapshotCleanup() +{ + AssertLockHeld(::cs_main); + auto get_storage_path = [](auto& chainstate) EXCLUSIVE_LOCKS_REQUIRED(::cs_main) -> std::optional<fs::path> { + if (!(chainstate && chainstate->HasCoinsViews())) { + return {}; + } + return chainstate->CoinsDB().StoragePath(); + }; + std::optional<fs::path> ibd_chainstate_path_maybe = get_storage_path(m_ibd_chainstate); + std::optional<fs::path> snapshot_chainstate_path_maybe = get_storage_path(m_snapshot_chainstate); + + if (!this->IsSnapshotValidated()) { + // No need to clean up. + return false; + } + // If either path doesn't exist, that means at least one of the chainstates + // is in-memory, in which case we can't do on-disk cleanup. You'd better be + // in a unittest! + if (!ibd_chainstate_path_maybe || !snapshot_chainstate_path_maybe) { + LogPrintf("[snapshot] snapshot chainstate cleanup cannot happen with " /* Continued */ + "in-memory chainstates. You are testing, right?\n"); + return false; + } + + const auto& snapshot_chainstate_path = *snapshot_chainstate_path_maybe; + const auto& ibd_chainstate_path = *ibd_chainstate_path_maybe; + + // Since we're going to be moving around the underlying leveldb filesystem content + // for each chainstate, make sure that the chainstates (and their constituent + // CoinsViews members) have been destructed first. + // + // The caller of this method will be responsible for reinitializing chainstates + // if they want to continue operation. + this->ResetChainstates(); + + // No chainstates should be considered usable. + assert(this->GetAll().size() == 0); + + LogPrintf("[snapshot] deleting background chainstate directory (now unnecessary) (%s)\n", + fs::PathToString(ibd_chainstate_path)); + + fs::path tmp_old{ibd_chainstate_path + "_todelete"}; + + auto rename_failed_abort = []( + fs::path p_old, + fs::path p_new, + const fs::filesystem_error& err) { + LogPrintf("%s: error renaming file (%s): %s\n", + __func__, fs::PathToString(p_old), err.what()); + AbortNode(strprintf( + "Rename of '%s' -> '%s' failed. " + "Cannot clean up the background chainstate leveldb directory.", + fs::PathToString(p_old), fs::PathToString(p_new))); + }; + + try { + fs::rename(ibd_chainstate_path, tmp_old); + } catch (const fs::filesystem_error& e) { + rename_failed_abort(ibd_chainstate_path, tmp_old, e); + throw; + } + + LogPrintf("[snapshot] moving snapshot chainstate (%s) to " /* Continued */ + "default chainstate directory (%s)\n", + fs::PathToString(snapshot_chainstate_path), fs::PathToString(ibd_chainstate_path)); + + try { + fs::rename(snapshot_chainstate_path, ibd_chainstate_path); + } catch (const fs::filesystem_error& e) { + rename_failed_abort(snapshot_chainstate_path, ibd_chainstate_path, e); + throw; + } + + if (!DeleteCoinsDBFromDisk(tmp_old, /*is_snapshot=*/false)) { + // No need to AbortNode because once the unneeded bg chainstate data is + // moved, it will not interfere with subsequent initialization. + LogPrintf("Deletion of %s failed. Please remove it manually, as the " /* Continued */ + "directory is now unnecessary.\n", + fs::PathToString(tmp_old)); + } else { + LogPrintf("[snapshot] deleted background chainstate directory (%s)\n", + fs::PathToString(ibd_chainstate_path)); + } + return true; +} diff --git a/src/validation.h b/src/validation.h index 067d2ea6d2..b0cef0d37b 100644 --- a/src/validation.h +++ b/src/validation.h @@ -24,6 +24,7 @@ #include <policy/packages.h> #include <policy/policy.h> #include <script/script_error.h> +#include <shutdown.h> #include <sync.h> #include <txdb.h> #include <txmempool.h> // For CTxMemPool::cs @@ -493,6 +494,19 @@ protected: //! Manages the UTXO set, which is a reflection of the contents of `m_chain`. std::unique_ptr<CoinsViews> m_coins_views; + //! This toggle exists for use when doing background validation for UTXO + //! snapshots. + //! + //! In the expected case, it is set once the background validation chain reaches the + //! same height as the base of the snapshot and its UTXO set is found to hash to + //! the expected assumeutxo value. It signals that we should no longer connect + //! blocks to the background chainstate. When set on the background validation + //! chainstate, it signifies that we have fully validated the snapshot chainstate. + //! + //! In the unlikely case that the snapshot chainstate is found to be invalid, this + //! is set to true on the snapshot chainstate. + bool m_disabled GUARDED_BY(::cs_main) {false}; + public: //! Reference to a BlockManager instance which itself is shared across all //! Chainstate instances. @@ -560,15 +574,15 @@ public: CCoinsViewCache& CoinsTip() EXCLUSIVE_LOCKS_REQUIRED(::cs_main) { AssertLockHeld(::cs_main); - assert(m_coins_views->m_cacheview); - return *m_coins_views->m_cacheview.get(); + Assert(m_coins_views); + return *Assert(m_coins_views->m_cacheview); } //! @returns A reference to the on-disk UTXO set database. CCoinsViewDB& CoinsDB() EXCLUSIVE_LOCKS_REQUIRED(::cs_main) { AssertLockHeld(::cs_main); - return m_coins_views->m_dbview; + return Assert(m_coins_views)->m_dbview; } //! @returns A pointer to the mempool. @@ -582,12 +596,15 @@ public: CCoinsViewErrorCatcher& CoinsErrorCatcher() EXCLUSIVE_LOCKS_REQUIRED(::cs_main) { AssertLockHeld(::cs_main); - return m_coins_views->m_catcherview; + return Assert(m_coins_views)->m_catcherview; } //! Destructs all objects related to accessing the UTXO set. void ResetCoinsViews() { m_coins_views.reset(); } + //! Does this chainstate have a UTXO set attached? + bool HasCoinsViews() const { return (bool)m_coins_views; } + //! The cache size of the on-disk coins view. size_t m_coinsdb_cache_size_bytes{0}; @@ -667,6 +684,12 @@ public: * May not be called with cs_main held. May not be called in a * validationinterface callback. * + * Note that if this is called while a snapshot chainstate is active, and if + * it is called on a background chainstate whose tip has reached the base block + * of the snapshot, its execution will take *MINUTES* while it hashes the + * background UTXO set to verify the assumeutxo value the snapshot was activated + * with. `cs_main` will be held during this time. + * * @returns true unless a system error occurred */ bool ActivateBestChain( @@ -745,6 +768,12 @@ public: std::string ToString() EXCLUSIVE_LOCKS_REQUIRED(::cs_main); + //! Indirection necessary to make lock annotations work with an optional mempool. + RecursiveMutex* MempoolMutex() const LOCK_RETURNED(m_mempool->cs) + { + return m_mempool ? &m_mempool->cs : nullptr; + } + private: bool ActivateBestChainStep(BlockValidationState& state, CBlockIndex* pindexMostWork, const std::shared_ptr<const CBlock>& pblock, bool& fInvalidFound, ConnectTrace& connectTrace) EXCLUSIVE_LOCKS_REQUIRED(cs_main, m_mempool->cs); bool ConnectTip(BlockValidationState& state, CBlockIndex* pindexNew, const std::shared_ptr<const CBlock>& pblock, ConnectTrace& connectTrace, DisconnectedBlockTransactions& disconnectpool) EXCLUSIVE_LOCKS_REQUIRED(cs_main, m_mempool->cs); @@ -758,12 +787,6 @@ private: void CheckForkWarningConditions() EXCLUSIVE_LOCKS_REQUIRED(cs_main); void InvalidChainFound(CBlockIndex* pindexNew) EXCLUSIVE_LOCKS_REQUIRED(cs_main); - //! Indirection necessary to make lock annotations work with an optional mempool. - RecursiveMutex* MempoolMutex() const LOCK_RETURNED(m_mempool->cs) - { - return m_mempool ? &m_mempool->cs : nullptr; - } - /** * Make mempool consistent after a reorg, by re-adding or recursively erasing * disconnected block transactions from the mempool, and also removing any @@ -785,12 +808,40 @@ private: void UpdateTip(const CBlockIndex* pindexNew) EXCLUSIVE_LOCKS_REQUIRED(::cs_main); - std::chrono::microseconds m_last_write{0}; - std::chrono::microseconds m_last_flush{0}; + SteadyClock::time_point m_last_write{}; + SteadyClock::time_point m_last_flush{}; + + /** + * In case of an invalid snapshot, rename the coins leveldb directory so + * that it can be examined for issue diagnosis. + */ + void InvalidateCoinsDBOnDisk() EXCLUSIVE_LOCKS_REQUIRED(::cs_main); friend ChainstateManager; }; + +enum class SnapshotCompletionResult { + SUCCESS, + SKIPPED, + + // Expected assumeutxo configuration data is not found for the height of the + // base block. + MISSING_CHAINPARAMS, + + // Failed to generate UTXO statistics (to check UTXO set hash) for the background + // chainstate. + STATS_FAILED, + + // The UTXO set hash of the background validation chainstate does not match + // the one expected by assumeutxo chainparams. + HASH_MISMATCH, + + // The blockhash of the current tip of the background validation chainstate does + // not match the one expected by the snapshot chainstate. + BASE_BLOCKHASH_MISMATCH, +}; + /** * Provides an interface for creating and interacting with one or two * chainstates: an IBD chainstate generated by downloading blocks, and @@ -860,10 +911,6 @@ private: //! that call. Chainstate* m_active_chainstate GUARDED_BY(::cs_main) {nullptr}; - //! If true, the assumed-valid chainstate has been fully validated - //! by the background validation chainstate. - bool m_snapshot_validated GUARDED_BY(::cs_main){false}; - CBlockIndex* m_best_invalid GUARDED_BY(::cs_main){nullptr}; //! Internal helper for ActivateSnapshot(). @@ -889,6 +936,22 @@ private: /** Most recent headers presync progress update, for rate-limiting. */ std::chrono::time_point<std::chrono::steady_clock> m_last_presync_update GUARDED_BY(::cs_main) {}; + //! Returns nullptr if no snapshot has been loaded. + const CBlockIndex* GetSnapshotBaseBlock() const EXCLUSIVE_LOCKS_REQUIRED(::cs_main); + + //! Return the height of the base block of the snapshot in use, if one exists, else + //! nullopt. + std::optional<int> GetSnapshotBaseHeight() const EXCLUSIVE_LOCKS_REQUIRED(::cs_main); + + //! Return true if a chainstate is considered usable. + //! + //! This is false when a background validation chainstate has completed its + //! validation of an assumed-valid chainstate, or when a snapshot + //! chainstate has been found to be invalid. + bool IsUsable(const Chainstate* const cs) const EXCLUSIVE_LOCKS_REQUIRED(::cs_main) { + return cs && !cs->m_disabled; + } + public: using Options = kernel::ChainstateManagerOpts; @@ -976,6 +1039,18 @@ public: [[nodiscard]] bool ActivateSnapshot( AutoFile& coins_file, const node::SnapshotMetadata& metadata, bool in_memory); + //! Once the background validation chainstate has reached the height which + //! is the base of the UTXO snapshot in use, compare its coins to ensure + //! they match those expected by the snapshot. + //! + //! If the coins match (expected), then mark the validation chainstate for + //! deletion and continue using the snapshot chainstate as active. + //! Otherwise, revert to using the ibd chainstate and shutdown. + SnapshotCompletionResult MaybeCompleteSnapshotValidation( + std::function<void(bilingual_str)> shutdown_fnc = + [](bilingual_str msg) { AbortNode(msg.original, msg); }) + EXCLUSIVE_LOCKS_REQUIRED(::cs_main); + //! The most-work chain. Chainstate& ActiveChainstate() const; CChain& ActiveChain() const EXCLUSIVE_LOCKS_REQUIRED(GetMutex()) { return ActiveChainstate().m_chain; } @@ -1000,7 +1075,10 @@ public: std::optional<uint256> SnapshotBlockhash() const; //! Is there a snapshot in use and has it been fully validated? - bool IsSnapshotValidated() const EXCLUSIVE_LOCKS_REQUIRED(::cs_main) { return m_snapshot_validated; } + bool IsSnapshotValidated() const EXCLUSIVE_LOCKS_REQUIRED(::cs_main) + { + return m_snapshot_chainstate && m_ibd_chainstate && m_ibd_chainstate->m_disabled; + } /** * Process an incoming block. This only returns after the best known valid @@ -1080,6 +1158,17 @@ public: Chainstate& ActivateExistingSnapshot(CTxMemPool* mempool, uint256 base_blockhash) EXCLUSIVE_LOCKS_REQUIRED(::cs_main); + //! If we have validated a snapshot chain during this runtime, copy its + //! chainstate directory over to the main `chainstate` location, completing + //! validation of the snapshot. + //! + //! If the cleanup succeeds, the caller will need to ensure chainstates are + //! reinitialized, since ResetChainstates() will be called before leveldb + //! directories are moved or deleted. + //! + //! @sa node/chainstate:LoadChainstate() + bool ValidatedSnapshotCleanup() EXCLUSIVE_LOCKS_REQUIRED(::cs_main); + ~ChainstateManager(); }; diff --git a/src/wallet/rpc/backup.cpp b/src/wallet/rpc/backup.cpp index 744537cfbd..09cfc07bc2 100644 --- a/src/wallet/rpc/backup.cpp +++ b/src/wallet/rpc/backup.cpp @@ -1760,7 +1760,8 @@ RPCHelpMan listdescriptors() {RPCResult::Type::NUM, "", "Range start inclusive"}, {RPCResult::Type::NUM, "", "Range end inclusive"}, }}, - {RPCResult::Type::NUM, "next", /*optional=*/true, "The next index to generate addresses from; defined only for ranged descriptors"}, + {RPCResult::Type::NUM, "next", /*optional=*/true, "Same as next_index field. Kept for compatibility reason."}, + {RPCResult::Type::NUM, "next_index", /*optional=*/true, "The next index to generate addresses from; defined only for ranged descriptors"}, }}, }} }}, @@ -1837,6 +1838,7 @@ RPCHelpMan listdescriptors() range.push_back(info.range->second - 1); spk.pushKV("range", range); spk.pushKV("next", info.next_index); + spk.pushKV("next_index", info.next_index); } descriptors.push_back(spk); } diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index e5c03849af..61851ce4ba 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -1648,8 +1648,8 @@ bool DummySignInput(const SigningProvider& provider, CTxIn &tx_in, const CTxOut const CScript& scriptPubKey = txout.scriptPubKey; SignatureData sigdata; - // Use max sig if watch only inputs were used or if this particular input is an external input - // to ensure a sufficient fee is attained for the requested feerate. + // Use max sig if watch only inputs were used, if this particular input is an external input, + // or if this wallet uses an external signer, to ensure a sufficient fee is attained for the requested feerate. const bool use_max_sig = coin_control && (coin_control->fAllowWatchOnly || coin_control->IsExternalSelected(tx_in.prevout) || !can_grind_r); if (!ProduceSignature(provider, use_max_sig ? DUMMY_MAXIMUM_SIGNATURE_CREATOR : DUMMY_SIGNATURE_CREATOR, scriptPubKey, sigdata)) { return false; |