aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Makefile.am2
-rw-r--r--src/Makefile.test.include1
-rw-r--r--src/bitcoind.cpp27
-rw-r--r--src/common/init.cpp74
-rw-r--r--src/common/init.h39
-rw-r--r--src/index/base.cpp2
-rw-r--r--src/node/chainstate.cpp122
-rw-r--r--src/node/interface_ui.cpp13
-rw-r--r--src/node/interface_ui.h2
-rw-r--r--src/node/minisketchwrapper.cpp8
-rw-r--r--src/qt/bitcoin.cpp122
-rw-r--r--src/random.cpp14
-rw-r--r--src/rpc/rawtransaction.cpp4
-rw-r--r--src/shutdown.cpp2
-rw-r--r--src/test/translation_tests.cpp21
-rw-r--r--src/test/validation_chainstatemanager_tests.cpp171
-rw-r--r--src/util/system.cpp40
-rw-r--r--src/util/system.h13
-rw-r--r--src/util/time.h2
-rw-r--r--src/util/translation.h15
-rw-r--r--src/validation.cpp364
-rw-r--r--src/validation.h123
-rw-r--r--src/wallet/rpc/backup.cpp4
-rw-r--r--src/wallet/wallet.cpp4
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;