refactor: Use util::Result class in LoadChainstate and VerifyLoadedChainstate

This commit is contained in:
Ryan Ofsky 2024-11-01 20:14:04 -04:00
parent 6a18bcfcf8
commit 48949d1c60
5 changed files with 91 additions and 90 deletions

View file

@ -129,13 +129,13 @@ int main(int argc, char* argv[])
ChainstateManager chainman{interrupt, chainman_opts, blockman_opts};
node::ChainstateLoadOptions options;
auto [status, error] = node::LoadChainstate(chainman, cache_sizes, options);
if (status != node::ChainstateLoadStatus::SUCCESS) {
auto load_result{node::LoadChainstate(chainman, cache_sizes, options)};
if (!load_result) {
std::cerr << "Failed to load Chain state from your datadir." << std::endl;
goto epilogue;
} else {
std::tie(status, error) = node::VerifyLoadedChainstate(chainman, options);
if (status != node::ChainstateLoadStatus::SUCCESS) {
auto verify_result{node::VerifyLoadedChainstate(chainman, options)};
if (!verify_result) {
std::cerr << "Failed to verify loaded Chain state from your datadir." << std::endl;
goto epilogue;
}

View file

@ -120,12 +120,11 @@
using common::AmountErrMsg;
using common::InvalidPortErrMsg;
using common::ResolveErrMsg;
using kernel::InterruptResult;
using node::ApplyArgsManOptions;
using node::BlockManager;
using node::CalculateCacheSizes;
using node::ChainstateLoadResult;
using node::ChainstateLoadStatus;
using node::ChainstateLoadError;
using node::DEFAULT_PERSIST_MEMPOOL;
using node::DEFAULT_PRINT_MODIFIED_FEE;
using node::DEFAULT_STOPATHEIGHT;
@ -1221,7 +1220,7 @@ bool CheckHostPortOptions(const ArgsManager& args) {
// A GUI user may opt to retry once with do_reindex set if there is a failure during chainstate initialization.
// The function therefore has to support re-entry.
static ChainstateLoadResult InitAndLoadChainstate(
util::Result<kernel::InterruptResult, ChainstateLoadError> InitAndLoadChainstate(
NodeContext& node,
bool do_reindex,
const bool do_reindex_chainstate,
@ -1237,7 +1236,7 @@ static ChainstateLoadResult InitAndLoadChainstate(
bilingual_str mempool_error;
node.mempool = std::make_unique<CTxMemPool>(mempool_opts, mempool_error);
if (!mempool_error.empty()) {
return {ChainstateLoadStatus::FAILURE_FATAL, mempool_error};
return {util::Error{mempool_error}, ChainstateLoadError::FAILURE_FATAL};
}
LogPrintf("* Using %.1f MiB for in-memory UTXO set (plus up to %.1f MiB of unused mempool space)\n", cache_sizes.coins * (1.0 / 1024 / 1024), mempool_opts.max_size_bytes * (1.0 / 1024 / 1024));
ChainstateManager::Options chainman_opts{
@ -1267,12 +1266,12 @@ static ChainstateLoadResult InitAndLoadChainstate(
node.chainman = std::make_unique<ChainstateManager>(*Assert(node.shutdown_signal), chainman_opts, blockman_opts);
} catch (dbwrapper_error& e) {
LogError("%s", e.what());
return {ChainstateLoadStatus::FAILURE, _("Error opening block database")};
return {util::Error{_("Error opening block database")}, ChainstateLoadError::FAILURE};
} catch (std::exception& e) {
return {ChainstateLoadStatus::FAILURE_FATAL, Untranslated(strprintf("Failed to initialize ChainstateManager: %s", e.what()))};
return {util::Error{Untranslated(strprintf("Failed to initialize ChainstateManager: %s", e.what()))}, ChainstateLoadError::FAILURE_FATAL};
}
ChainstateManager& chainman = *node.chainman;
if (chainman.m_interrupt) return {ChainstateLoadStatus::INTERRUPTED, {}};
if (chainman.m_interrupt) return kernel::Interrupted{};
// This is defined and set here instead of inline in validation.h to avoid a hard
// dependency between validation and index/base, since the latter is not in
@ -1307,27 +1306,27 @@ static ChainstateLoadResult InitAndLoadChainstate(
"", CClientUIInterface::MSG_ERROR);
};
uiInterface.InitMessage(_("Loading block index…"));
auto catch_exceptions = [](auto&& f) -> ChainstateLoadResult {
auto catch_exceptions = [](auto&& f) -> util::Result<InterruptResult, node::ChainstateLoadError> {
try {
return f();
} catch (const std::exception& e) {
LogError("%s\n", e.what());
return std::make_tuple(node::ChainstateLoadStatus::FAILURE, _("Error loading databases"));
return {util::Error{_("Error loading databases")}, node::ChainstateLoadError::FAILURE};
}
};
auto [status, error] = catch_exceptions([&] { return LoadChainstate(chainman, cache_sizes, options); });
if (status == node::ChainstateLoadStatus::SUCCESS) {
auto result = catch_exceptions([&] { return LoadChainstate(chainman, cache_sizes, options); });
if (result && !IsInterrupted(*result)) {
uiInterface.InitMessage(_("Verifying blocks…"));
if (chainman.m_blockman.m_have_pruned && options.check_blocks > MIN_BLOCKS_TO_KEEP) {
LogWarning("pruned datadir may not have more than %d blocks; only checking available blocks\n",
MIN_BLOCKS_TO_KEEP);
}
std::tie(status, error) = catch_exceptions([&] { return VerifyLoadedChainstate(chainman, options); });
if (status == node::ChainstateLoadStatus::SUCCESS) {
result.Update(catch_exceptions([&] { return VerifyLoadedChainstate(chainman, options); }));
if (result && !IsInterrupted(*result)) {
LogInfo("Block index and chainstate loaded");
}
}
return {status, error};
return result;
};
bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info)
@ -1685,14 +1684,15 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info)
const bool do_reindex_chainstate{args.GetBoolArg("-reindex-chainstate", false)};
// Chainstate initialization and loading may be retried once with reindexing by GUI users
auto [status, error] = InitAndLoadChainstate(
auto result = InitAndLoadChainstate(
node,
do_reindex,
do_reindex_chainstate,
kernel_cache_sizes,
args);
if (status == ChainstateLoadStatus::FAILURE && !do_reindex && !ShutdownRequested(node)) {
if (!result && result.GetFailure() == ChainstateLoadError::FAILURE && !do_reindex && !ShutdownRequested(node)) {
// suggest a reindex
const auto& error = util::ErrorString(result);
bool do_retry = uiInterface.ThreadSafeQuestion(
error + Untranslated(".\n\n") + _("Do you want to rebuild the databases now?"),
error.original + ".\nPlease restart with -reindex or -reindex-chainstate to recover.",
@ -1704,15 +1704,15 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info)
if (!Assert(node.shutdown_signal)->reset()) {
LogError("Internal error: failed to reset shutdown signal.\n");
}
std::tie(status, error) = InitAndLoadChainstate(
result.Update(InitAndLoadChainstate(
node,
do_reindex,
do_reindex_chainstate,
kernel_cache_sizes,
args);
args));
}
if (status != ChainstateLoadStatus::SUCCESS && status != ChainstateLoadStatus::INTERRUPTED) {
return InitError(error);
if (!result) {
return InitError(util::ErrorString(result));
}
// As LoadBlockIndex can take several minutes, it's possible the user

View file

@ -27,35 +27,40 @@
#include <vector>
using kernel::CacheSizes;
using kernel::Interrupted;
using kernel::InterruptResult;
namespace node {
// Complete initialization of chainstates after the initial call has been made
// to ChainstateManager::InitializeChainstate().
static ChainstateLoadResult CompleteChainstateInitialization(
static util::Result<InterruptResult, ChainstateLoadError> CompleteChainstateInitialization(
ChainstateManager& chainman,
const ChainstateLoadOptions& options) EXCLUSIVE_LOCKS_REQUIRED(::cs_main)
{
if (chainman.m_interrupt) return {ChainstateLoadStatus::INTERRUPTED, {}};
if (chainman.m_interrupt) return Interrupted{};
// LoadBlockIndex will load m_have_pruned if we've ever removed a
// block file from disk.
// Note that it also sets m_blockfiles_indexed based on the disk flag!
if (!chainman.LoadBlockIndex()) {
if (chainman.m_interrupt) return {ChainstateLoadStatus::INTERRUPTED, {}};
return {ChainstateLoadStatus::FAILURE, _("Error loading block database")};
if (chainman.m_interrupt) {
return Interrupted{};
} else {
return {util::Error{_("Error loading block database")}, ChainstateLoadError::FAILURE};
}
}
if (!chainman.BlockIndex().empty() &&
!chainman.m_blockman.LookupBlockIndex(chainman.GetConsensus().hashGenesisBlock)) {
// If the loaded chain has a wrong genesis, bail out immediately
// (we're likely using a testnet datadir, or the other way around).
return {ChainstateLoadStatus::FAILURE_INCOMPATIBLE_DB, _("Incorrect or no genesis block found. Wrong datadir for network?")};
return {util::Error{_("Incorrect or no genesis block found. Wrong datadir for network?")}, ChainstateLoadError::FAILURE_INCOMPATIBLE_DB};
}
// Check for changed -prune state. What we are concerned about is a user who has pruned blocks
// in the past, but is now trying to run unpruned.
if (chainman.m_blockman.m_have_pruned && !options.prune) {
return {ChainstateLoadStatus::FAILURE, _("You need to rebuild the database using -reindex to go back to unpruned mode. This will redownload the entire blockchain")};
return {util::Error{_("You need to rebuild the database using -reindex to go back to unpruned mode. This will redownload the entire blockchain")}, ChainstateLoadError::FAILURE};
}
// At this point blocktree args are consistent with what's on disk.
@ -63,7 +68,7 @@ static ChainstateLoadResult CompleteChainstateInitialization(
// (otherwise we use the one already on disk).
// This is called again in ImportBlocks after the reindex completes.
if (chainman.m_blockman.m_blockfiles_indexed && !chainman.ActiveChainstate().LoadGenesisBlock()) {
return {ChainstateLoadStatus::FAILURE, _("Error initializing block database")};
return {util::Error{_("Error initializing block database")}, ChainstateLoadError::FAILURE};
}
auto is_coinsview_empty = [&](Chainstate* chainstate) EXCLUSIVE_LOCKS_REQUIRED(::cs_main) {
@ -93,7 +98,7 @@ static ChainstateLoadResult CompleteChainstateInitialization(
/*should_wipe=*/options.wipe_chainstate_db);
} catch (dbwrapper_error& err) {
LogError("%s\n", err.what());
return {ChainstateLoadStatus::FAILURE, _("Error opening coins database")};
return {util::Error{_("Error opening coins database")}, ChainstateLoadError::FAILURE};
}
if (options.coins_error_cb) {
@ -103,14 +108,15 @@ static ChainstateLoadResult CompleteChainstateInitialization(
// Refuse to load unsupported database format.
// This is a no-op if we cleared the coinsviewdb with -reindex or -reindex-chainstate
if (chainstate->CoinsDB().NeedsUpgrade()) {
return {ChainstateLoadStatus::FAILURE_INCOMPATIBLE_DB, _("Unsupported chainstate database format found. "
"Please restart with -reindex-chainstate. This will "
"rebuild the chainstate database.")};
return {util::Error{_("Unsupported chainstate database format found. "
"Please restart with -reindex-chainstate. This will "
"rebuild the chainstate database.")},
ChainstateLoadError::FAILURE_INCOMPATIBLE_DB};
}
// ReplayBlocks is a no-op if we cleared the coinsviewdb with -reindex or -reindex-chainstate
if (!chainstate->ReplayBlocks()) {
return {ChainstateLoadStatus::FAILURE, _("Unable to replay blocks. You will need to rebuild the database using -reindex-chainstate.")};
return {util::Error{_("Unable to replay blocks. You will need to rebuild the database using -reindex-chainstate.")}, ChainstateLoadError::FAILURE};
}
// The on-disk coinsdb is now in a good state, create the cache
@ -120,7 +126,7 @@ static ChainstateLoadResult CompleteChainstateInitialization(
if (!is_coinsview_empty(chainstate)) {
// LoadChainTip initializes the chain based on CoinsTip()'s best block
if (!chainstate->LoadChainTip()) {
return {ChainstateLoadStatus::FAILURE, _("Error initializing block database")};
return {util::Error{_("Error initializing block database")}, ChainstateLoadError::FAILURE};
}
assert(chainstate->m_chain.Tip() != nullptr);
}
@ -129,8 +135,8 @@ static ChainstateLoadResult CompleteChainstateInitialization(
auto chainstates{chainman.GetAll()};
if (std::any_of(chainstates.begin(), chainstates.end(),
[](const Chainstate* cs) EXCLUSIVE_LOCKS_REQUIRED(cs_main) { return cs->NeedsRedownload(); })) {
return {ChainstateLoadStatus::FAILURE, strprintf(_("Witness data for blocks after height %d requires validation. Please restart with -reindex."),
chainman.GetConsensus().SegwitHeight)};
return {util::Error{strprintf(_("Witness data for blocks after height %d requires validation. Please restart with -reindex."),
chainman.GetConsensus().SegwitHeight)}, ChainstateLoadError::FAILURE};
};
// Now that chainstates are loaded and we're able to flush to
@ -138,12 +144,13 @@ static ChainstateLoadResult CompleteChainstateInitialization(
// on the condition of each chainstate.
chainman.MaybeRebalanceCaches();
return {ChainstateLoadStatus::SUCCESS, {}};
return {};
}
ChainstateLoadResult LoadChainstate(ChainstateManager& chainman, const CacheSizes& cache_sizes,
const ChainstateLoadOptions& options)
util::Result<InterruptResult, ChainstateLoadError> LoadChainstate(ChainstateManager& chainman, const CacheSizes& cache_sizes,
const ChainstateLoadOptions& options)
{
util::Result<InterruptResult, ChainstateLoadError> result;
if (!chainman.AssumedValidBlock().IsNull()) {
LogPrintf("Assuming ancestors of block %s have valid signatures.\n", chainman.AssumedValidBlock().GetHex());
} else {
@ -173,13 +180,14 @@ ChainstateLoadResult LoadChainstate(ChainstateManager& chainman, const CacheSize
if (has_snapshot && options.wipe_chainstate_db) {
LogPrintf("[snapshot] deleting snapshot chainstate due to reindexing\n");
if (!chainman.DeleteSnapshotChainstate()) {
return {ChainstateLoadStatus::FAILURE_FATAL, Untranslated("Couldn't remove snapshot chainstate.")};
result.Update({util::Error{Untranslated("Couldn't remove snapshot chainstate.")}, ChainstateLoadError::FAILURE_FATAL});
return result;
}
}
auto [init_status, init_error] = CompleteChainstateInitialization(chainman, options);
if (init_status != ChainstateLoadStatus::SUCCESS) {
return {init_status, init_error};
result.Update(CompleteChainstateInitialization(chainman, options));
if (!result || IsInterrupted(*result)) {
return result;
}
// If a snapshot chainstate was fully validated by a background chainstate during
@ -197,7 +205,8 @@ ChainstateLoadResult LoadChainstate(ChainstateManager& chainman, const CacheSize
} else if (snapshot_completion == SnapshotCompletionResult::SUCCESS) {
LogPrintf("[snapshot] cleaning up unneeded background chainstate, then reinitializing\n");
if (!chainman.ValidatedSnapshotCleanup()) {
return {ChainstateLoadStatus::FAILURE_FATAL, Untranslated("Background chainstate cleanup failed unexpectedly.")};
result.Update({util::Error{Untranslated("Background chainstate cleanup failed unexpectedly.")}, ChainstateLoadError::FAILURE_FATAL});
return result;
}
// Because ValidatedSnapshotCleanup() has torn down chainstates with
@ -213,20 +222,22 @@ ChainstateLoadResult LoadChainstate(ChainstateManager& chainman, const CacheSize
// for the fully validated chainstate.
chainman.ActiveChainstate().ClearBlockIndexCandidates();
auto [init_status, init_error] = CompleteChainstateInitialization(chainman, options);
if (init_status != ChainstateLoadStatus::SUCCESS) {
return {init_status, init_error};
auto result{CompleteChainstateInitialization(chainman, options)};
if (!result || IsInterrupted(*result)) {
return result;
}
} else {
return {ChainstateLoadStatus::FAILURE_FATAL, _(
result.Update({util::Error{_(
"UTXO snapshot failed to validate. "
"Restart to resume normal initial block download, or try loading a different snapshot.")};
"Restart to resume normal initial block download, or try loading a different snapshot.")},
ChainstateLoadError::FAILURE_FATAL});
return result;
}
return {ChainstateLoadStatus::SUCCESS, {}};
return result;
}
ChainstateLoadResult VerifyLoadedChainstate(ChainstateManager& chainman, const ChainstateLoadOptions& options)
util::Result<InterruptResult, ChainstateLoadError> VerifyLoadedChainstate(ChainstateManager& chainman, const ChainstateLoadOptions& options)
{
auto is_coinsview_empty = [&](Chainstate* chainstate) EXCLUSIVE_LOCKS_REQUIRED(::cs_main) {
return options.wipe_chainstate_db || chainstate->CoinsTip().GetBestBlock().IsNull();
@ -234,36 +245,42 @@ ChainstateLoadResult VerifyLoadedChainstate(ChainstateManager& chainman, const C
LOCK(cs_main);
util::Result<InterruptResult, ChainstateLoadError> result;
for (Chainstate* chainstate : chainman.GetAll()) {
if (!is_coinsview_empty(chainstate)) {
const CBlockIndex* tip = chainstate->m_chain.Tip();
if (tip && tip->nTime > GetTime() + MAX_FUTURE_BLOCK_TIME) {
return {ChainstateLoadStatus::FAILURE, _("The block database contains a block which appears to be from the future. "
"This may be due to your computer's date and time being set incorrectly. "
"Only rebuild the block database if you are sure that your computer's date and time are correct")};
result.Update({util::Error{_("The block database contains a block which appears to be from the future. "
"This may be due to your computer's date and time being set incorrectly. "
"Only rebuild the block database if you are sure that your computer's date and time are correct")},
ChainstateLoadError::FAILURE});
return result;
}
VerifyDBResult result = CVerifyDB(chainman.GetNotifications()).VerifyDB(
VerifyDBResult verify_result = CVerifyDB(chainman.GetNotifications()).VerifyDB(
*chainstate, chainman.GetConsensus(), chainstate->CoinsDB(),
options.check_level,
options.check_blocks);
switch (result) {
switch (verify_result) {
case VerifyDBResult::SUCCESS:
case VerifyDBResult::SKIPPED_MISSING_BLOCKS:
break;
case VerifyDBResult::INTERRUPTED:
return {ChainstateLoadStatus::INTERRUPTED, _("Block verification was interrupted")};
result.Update(Interrupted{});
return result;
case VerifyDBResult::CORRUPTED_BLOCK_DB:
return {ChainstateLoadStatus::FAILURE, _("Corrupted block database detected")};
result.Update({util::Error{_("Corrupted block database detected")}, ChainstateLoadError::FAILURE});
return result;
case VerifyDBResult::SKIPPED_L3_CHECKS:
if (options.require_full_verification) {
return {ChainstateLoadStatus::FAILURE_INSUFFICIENT_DBCACHE, _("Insufficient dbcache for block verification")};
result.Update({util::Error{_("Insufficient dbcache for block verification")}, ChainstateLoadError::FAILURE_INSUFFICIENT_DBCACHE});
return result;
}
break;
} // no default case, so the compiler can warn about missing cases
}
}
return {ChainstateLoadStatus::SUCCESS, {}};
return result;
}
} // namespace node

View file

@ -5,6 +5,8 @@
#ifndef BITCOIN_NODE_CHAINSTATE_H
#define BITCOIN_NODE_CHAINSTATE_H
#include <kernel/notifications_interface.h>
#include <util/result.h>
#include <util/translation.h>
#include <validation.h>
@ -41,34 +43,16 @@ struct ChainstateLoadOptions {
//! case, and treat other cases as errors. More complex applications may want to
//! try reindexing in the generic failure case, and pass an interrupt callback
//! and exit cleanly in the interrupted case.
enum class ChainstateLoadStatus {
SUCCESS,
enum class ChainstateLoadError {
FAILURE, //!< Generic failure which reindexing may fix
FAILURE_FATAL, //!< Fatal error which should not prompt to reindex
FAILURE_INCOMPATIBLE_DB,
FAILURE_INSUFFICIENT_DBCACHE,
INTERRUPTED,
};
//! Chainstate load status code and optional error string.
using ChainstateLoadResult = std::tuple<ChainstateLoadStatus, bilingual_str>;
/** This sequence can have 4 types of outcomes:
*
* 1. Success
* 2. Shutdown requested
* - nothing failed but a shutdown was triggered in the middle of the
* sequence
* 3. Soft failure
* - a failure that might be recovered from with a reindex
* 4. Hard failure
* - a failure that definitively cannot be recovered from with a reindex
*
* LoadChainstate returns a (status code, error string) tuple.
*/
ChainstateLoadResult LoadChainstate(ChainstateManager& chainman, const kernel::CacheSizes& cache_sizes,
const ChainstateLoadOptions& options);
ChainstateLoadResult VerifyLoadedChainstate(ChainstateManager& chainman, const ChainstateLoadOptions& options);
util::Result<kernel::InterruptResult, ChainstateLoadError> LoadChainstate(ChainstateManager& chainman, const kernel::CacheSizes& cache_sizes,
const ChainstateLoadOptions& options);
util::Result<kernel::InterruptResult, ChainstateLoadError> VerifyLoadedChainstate(ChainstateManager& chainman, const ChainstateLoadOptions& options);
} // namespace node
#endif // BITCOIN_NODE_CHAINSTATE_H

View file

@ -290,11 +290,11 @@ void ChainTestingSetup::LoadVerifyActivateChainstate()
options.check_blocks = m_args.GetIntArg("-checkblocks", DEFAULT_CHECKBLOCKS);
options.check_level = m_args.GetIntArg("-checklevel", DEFAULT_CHECKLEVEL);
options.require_full_verification = m_args.IsArgSet("-checkblocks") || m_args.IsArgSet("-checklevel");
auto [status, error] = LoadChainstate(chainman, m_kernel_cache_sizes, options);
assert(status == node::ChainstateLoadStatus::SUCCESS);
auto load_result{LoadChainstate(chainman, m_kernel_cache_sizes, options)};
Assert(load_result);
std::tie(status, error) = VerifyLoadedChainstate(chainman, options);
assert(status == node::ChainstateLoadStatus::SUCCESS);
auto verify_result{VerifyLoadedChainstate(chainman, options)};
Assert(verify_result);
BlockValidationState state;
if (!chainman.ActiveChainstate().ActivateBestChain(state)) {