Skip to content

Commit

Permalink
refactor: Use util::Result class in LoadChainstate and VerifyLoadedCh…
Browse files Browse the repository at this point in the history
…ainstate
  • Loading branch information
ryanofsky committed Oct 18, 2023
1 parent c003dbe commit 9297afc
Show file tree
Hide file tree
Showing 5 changed files with 66 additions and 77 deletions.
8 changes: 4 additions & 4 deletions src/bitcoin-chainstate.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -133,13 +133,13 @@ int main(int argc, char* argv[])
cache_sizes.coins = (450 << 20) - (2 << 20) - (2 << 22);
node::ChainstateLoadOptions options;
options.check_interrupt = [] { return false; };
auto [status, error] = node::LoadChainstate(chainman, cache_sizes, options);
if (status != node::ChainstateLoadStatus::SUCCESS) {
auto result = node::LoadChainstate(chainman, cache_sizes, options);
if (!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) {
result = node::VerifyLoadedChainstate(chainman, options);
if (!result) {
std::cerr << "Failed to verify loaded Chain state from your datadir." << std::endl;
goto epilogue;
}
Expand Down
19 changes: 11 additions & 8 deletions src/init.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1529,33 +1529,36 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info)

uiInterface.InitMessage(_("Loading block index…").translated);
const auto load_block_index_start_time{SteadyClock::now()};
auto catch_exceptions = [](auto&& f) {
auto catch_exceptions = [](auto&& f) -> util::Result<void, node::ChainstateLoadError> {
try {
return f();
} catch (const std::exception& e) {
LogPrintf("%s\n", e.what());
return std::make_tuple(node::ChainstateLoadStatus::FAILURE, _("Error opening block database"));
return {util::Error{_("Error opening block database")}, 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) {
uiInterface.InitMessage(_("Verifying blocks…").translated);
if (chainman.m_blockman.m_have_pruned && options.check_blocks > MIN_BLOCKS_TO_KEEP) {
LogPrintfCategory(BCLog::PRUNE, "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 = catch_exceptions([&]{ return VerifyLoadedChainstate(chainman, options);});
if (result) {
fLoaded = true;
LogPrintf(" block index %15dms\n", Ticks<std::chrono::milliseconds>(SteadyClock::now() - load_block_index_start_time));
}
}

if (status == node::ChainstateLoadStatus::FAILURE_FATAL || status == node::ChainstateLoadStatus::FAILURE_INCOMPATIBLE_DB || status == node::ChainstateLoadStatus::FAILURE_INSUFFICIENT_DBCACHE) {
return InitError(error);
if (!result && (result.GetFailure() == node::ChainstateLoadError::FAILURE_FATAL ||
result.GetFailure() == node::ChainstateLoadError::FAILURE_INCOMPATIBLE_DB ||
result.GetFailure() == node::ChainstateLoadError::FAILURE_INSUFFICIENT_DBCACHE)) {
return InitError(util::ErrorString(result));
}

if (!fLoaded && !ShutdownRequested()) {
bilingual_str error = util::ErrorString(result);
// first suggest a reindex
if (!options.reindex) {
bool fRet = uiInterface.ThreadSafeQuestion(
Expand Down
74 changes: 38 additions & 36 deletions src/node/chainstate.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
namespace node {
// Complete initialization of chainstates after the initial call has been made
// to ChainstateManager::InitializeChainstate().
static ChainstateLoadResult CompleteChainstateInitialization(
static util::Result<void, ChainstateLoadError> CompleteChainstateInitialization(
ChainstateManager& chainman,
const CacheSizes& cache_sizes,
const ChainstateLoadOptions& options) EXCLUSIVE_LOCKS_REQUIRED(::cs_main)
Expand All @@ -55,36 +55,36 @@ static ChainstateLoadResult CompleteChainstateInitialization(
}
}

if (options.check_interrupt && options.check_interrupt()) return {ChainstateLoadStatus::INTERRUPTED, {}};
if (options.check_interrupt && options.check_interrupt()) return {util::Error{}, ChainstateLoadError::INTERRUPTED};

// LoadBlockIndex will load m_have_pruned if we've ever removed a
// block file from disk.
// Note that it also sets fReindex global based on the disk flag!
// From here on, fReindex and options.reindex values may be different!
if (!chainman.LoadBlockIndex()) {
if (options.check_interrupt && options.check_interrupt()) return {ChainstateLoadStatus::INTERRUPTED, {}};
return {ChainstateLoadStatus::FAILURE, _("Error loading block database")};
if (options.check_interrupt && options.check_interrupt()) return {util::Error{}, ChainstateLoadError::INTERRUPTED};
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.
// If we're not mid-reindex (based on disk + args), add a genesis block on disk
// (otherwise we use the one already on disk).
// This is called again in ImportBlocks after the reindex completes.
if (!fReindex && !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) {
Expand Down Expand Up @@ -118,14 +118,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
Expand All @@ -135,7 +136,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);
}
Expand All @@ -145,8 +146,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};
};
}

Expand All @@ -155,11 +156,11 @@ 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<void, ChainstateLoadError> 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());
Expand Down Expand Up @@ -190,13 +191,13 @@ ChainstateLoadResult LoadChainstate(ChainstateManager& chainman, const CacheSize
if (has_snapshot && (options.reindex || options.reindex_chainstate)) {
LogPrintf("[snapshot] deleting snapshot chainstate due to reindexing\n");
if (!chainman.DeleteSnapshotChainstate()) {
return {ChainstateLoadStatus::FAILURE_FATAL, Untranslated("Couldn't remove snapshot chainstate.")};
return {util::Error{Untranslated("Couldn't remove snapshot chainstate.")}, ChainstateLoadError::FAILURE_FATAL};
}
}

auto [init_status, init_error] = CompleteChainstateInitialization(chainman, cache_sizes, options);
if (init_status != ChainstateLoadStatus::SUCCESS) {
return {init_status, init_error};
auto result{CompleteChainstateInitialization(chainman, cache_sizes, options)};
if (!result) {
return result;
}

// If a snapshot chainstate was fully validated by a background chainstate during
Expand All @@ -214,7 +215,7 @@ 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.")};
return {util::Error{Untranslated("Background chainstate cleanup failed unexpectedly.")}, ChainstateLoadError::FAILURE_FATAL};
}

// Because ValidatedSnapshotCleanup() has torn down chainstates with
Expand All @@ -230,20 +231,20 @@ ChainstateLoadResult LoadChainstate(ChainstateManager& chainman, const CacheSize
// for the fully validated chainstate.
chainman.ActiveChainstate().ClearBlockIndexCandidates();

auto [init_status, init_error] = CompleteChainstateInitialization(chainman, cache_sizes, options);
if (init_status != ChainstateLoadStatus::SUCCESS) {
return {init_status, init_error};
auto result{CompleteChainstateInitialization(chainman, cache_sizes, options)};
if (!result) {
return result;
}
} else {
return {ChainstateLoadStatus::FAILURE, _(
"UTXO snapshot failed to validate. "
return util::Error{
_("UTXO snapshot failed to validate. "
"Restart to resume normal initial block download, or try loading a different snapshot.")};
}

return {ChainstateLoadStatus::SUCCESS, {}};
return {};
}

ChainstateLoadResult VerifyLoadedChainstate(ChainstateManager& chainman, const ChainstateLoadOptions& options)
util::Result<void, ChainstateLoadError> VerifyLoadedChainstate(ChainstateManager& chainman, const ChainstateLoadOptions& options)
{
auto is_coinsview_empty = [&](Chainstate* chainstate) EXCLUSIVE_LOCKS_REQUIRED(::cs_main) {
return options.reindex || options.reindex_chainstate || chainstate->CoinsTip().GetBestBlock().IsNull();
Expand All @@ -255,9 +256,10 @@ ChainstateLoadResult VerifyLoadedChainstate(ChainstateManager& chainman, const C
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")};
return {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};
}

VerifyDBResult result = CVerifyDB(chainman.GetNotifications()).VerifyDB(
Expand All @@ -269,18 +271,18 @@ ChainstateLoadResult VerifyLoadedChainstate(ChainstateManager& chainman, const C
case VerifyDBResult::SKIPPED_MISSING_BLOCKS:
break;
case VerifyDBResult::INTERRUPTED:
return {ChainstateLoadStatus::INTERRUPTED, _("Block verification was interrupted")};
return {util::Error{_("Block verification was interrupted")}, ChainstateLoadError::INTERRUPTED};
case VerifyDBResult::CORRUPTED_BLOCK_DB:
return {ChainstateLoadStatus::FAILURE, _("Corrupted block database detected")};
return {util::Error{_("Corrupted block database detected")}, ChainstateLoadError::FAILURE};
case VerifyDBResult::SKIPPED_L3_CHECKS:
if (options.require_full_verification) {
return {ChainstateLoadStatus::FAILURE_INSUFFICIENT_DBCACHE, _("Insufficient dbcache for block verification")};
return {util::Error{_("Insufficient dbcache for block verification")}, ChainstateLoadError::FAILURE_INSUFFICIENT_DBCACHE};
}
break;
} // no default case, so the compiler can warn about missing cases
}
}

return {ChainstateLoadStatus::SUCCESS, {}};
return {};
}
} // namespace node
34 changes: 9 additions & 25 deletions src/node/chainstate.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#ifndef BITCOIN_NODE_CHAINSTATE_H
#define BITCOIN_NODE_CHAINSTATE_H

#include <util/result.h>
#include <util/translation.h>
#include <validation.h>

Expand Down Expand Up @@ -36,38 +37,21 @@ struct ChainstateLoadOptions {
std::function<void()> coins_error_cb;
};

//! Chainstate load status. Simple applications can just check for the success
//! 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,
//! Chainstate load errors. Simple applications can just treat all errors as
//! failures. More complex applications may want to try reindexing in the
//! generic error case, and pass an interrupt callback and exit cleanly in the
//! interrupted case.
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 CacheSizes& cache_sizes,
const ChainstateLoadOptions& options);
ChainstateLoadResult VerifyLoadedChainstate(ChainstateManager& chainman, const ChainstateLoadOptions& options);
util::Result<void, ChainstateLoadError> LoadChainstate(ChainstateManager& chainman, const CacheSizes& cache_sizes,
const ChainstateLoadOptions& options);
util::Result<void, ChainstateLoadError> VerifyLoadedChainstate(ChainstateManager& chainman, const ChainstateLoadOptions& options);
} // namespace node

#endif // BITCOIN_NODE_CHAINSTATE_H
8 changes: 4 additions & 4 deletions src/test/util/setup_common.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -221,11 +221,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_cache_sizes, options);
assert(status == node::ChainstateLoadStatus::SUCCESS);
auto result = LoadChainstate(chainman, m_cache_sizes, options);
assert(result);

std::tie(status, error) = VerifyLoadedChainstate(chainman, options);
assert(status == node::ChainstateLoadStatus::SUCCESS);
result = VerifyLoadedChainstate(chainman, options);
assert(result);

BlockValidationState state;
if (!chainman.ActiveChainstate().ActivateBestChain(state)) {
Expand Down

0 comments on commit 9297afc

Please sign in to comment.