From 569353068568444a25b301bbd6513bb510157dc9 Mon Sep 17 00:00:00 2001 From: James O'Beirne Date: Wed, 24 Jul 2019 13:23:48 -0400 Subject: refactor: have CCoins* data managed under CChainState This change encapsulates UTXO set data within CChainState instances, removing global data `pcoinsTip` and `pcoinsviewdb`. This is necessary if we want to maintain multiple chainstates with their own rendering of the UTXO set. We introduce a class CoinsViews which consolidates the construction of a CCoins* hierarchy. Construction of its various pieces (db, coinscatcher, in-memory cache) is split up so that we avoid flushing bad state to disk if startup is interrupted. We also introduce `CChainState::CanFlushToDisk()` which tells us when it is safe to flush the chainstate based on this partial construction. This commit could be broken into smaller pieces, but it would require more ephemeral diffs to, e.g., temporarily change CCoinsViewDB's constructor invocations. Other changes: - A parameter has been added to the CCoinsViewDB constructor that allows the name of the corresponding leveldb directory to be specified. Thanks to Russell Yanofsky and Marco Falke for helpful feedback. --- src/init.cpp | 46 ++++++++++++----------- src/rpc/blockchain.cpp | 4 +- src/test/setup_common.cpp | 11 ++++-- src/txdb.cpp | 2 +- src/txdb.h | 5 ++- src/validation.cpp | 51 ++++++++++++++++++++++--- src/validation.h | 94 +++++++++++++++++++++++++++++++++++++++++++---- 7 files changed, 170 insertions(+), 43 deletions(-) diff --git a/src/init.cpp b/src/init.cpp index bb3ff8d88f..50ae0c6252 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -15,7 +15,6 @@ #include #include #include -#include #include #include #include @@ -149,7 +148,6 @@ NODISCARD static bool CreatePidFile() // shutdown thing. // -static std::unique_ptr pcoinscatcher; static std::unique_ptr globalVerifyHandle; static boost::thread_group threadGroup; @@ -234,8 +232,11 @@ void Shutdown(InitInterfaces& interfaces) } // FlushStateToDisk generates a ChainStateFlushed callback, which we should avoid missing - if (pcoinsTip != nullptr) { - ::ChainstateActive().ForceFlushStateToDisk(); + // + // g_chainstate is referenced here directly (instead of ::ChainstateActive()) because it + // may not have been initialized yet. + if (g_chainstate && g_chainstate->CanFlushToDisk()) { + g_chainstate->ForceFlushStateToDisk(); } // After there are no more peers/RPC left to give us new data which may generate @@ -250,12 +251,10 @@ void Shutdown(InitInterfaces& interfaces) { LOCK(cs_main); - if (pcoinsTip != nullptr) { - ::ChainstateActive().ForceFlushStateToDisk(); + if (g_chainstate && g_chainstate->CanFlushToDisk()) { + g_chainstate->ForceFlushStateToDisk(); + g_chainstate->ResetCoinsViews(); } - pcoinsTip.reset(); - pcoinscatcher.reset(); - pcoinsdbview.reset(); pblocktree.reset(); } for (const auto& client : interfaces.chain_clients) { @@ -1466,10 +1465,10 @@ bool AppInitMain(InitInterfaces& interfaces) bool is_coinsview_empty; try { LOCK(cs_main); + // This statement makes ::ChainstateActive() usable. + g_chainstate = MakeUnique(); UnloadBlockIndex(); - pcoinsTip.reset(); - pcoinsdbview.reset(); - pcoinscatcher.reset(); + // new CBlockTreeDB tries to delete the existing file, which // fails if it's still open from the previous loop. Close it first: pblocktree.reset(); @@ -1520,9 +1519,12 @@ bool AppInitMain(InitInterfaces& interfaces) // At this point we're either in reindex or we've loaded a useful // block tree into BlockIndex()! - pcoinsdbview.reset(new CCoinsViewDB(nCoinDBCache, false, fReset || fReindexChainState)); - pcoinscatcher.reset(new CCoinsViewErrorCatcher(pcoinsdbview.get())); - pcoinscatcher->AddReadErrCallback([]() { + ::ChainstateActive().InitCoinsDB( + /* cache_size_bytes */ nCoinDBCache, + /* in_memory */ false, + /* should_wipe */ fReset || fReindexChainState); + + ::ChainstateActive().CoinsErrorCatcher().AddReadErrCallback([]() { uiInterface.ThreadSafeMessageBox( _("Error reading from database, shutting down.").translated, "", CClientUIInterface::MSG_ERROR); @@ -1530,23 +1532,25 @@ bool AppInitMain(InitInterfaces& interfaces) // If necessary, upgrade from older database format. // This is a no-op if we cleared the coinsviewdb with -reindex or -reindex-chainstate - if (!pcoinsdbview->Upgrade()) { + if (!::ChainstateActive().CoinsDB().Upgrade()) { strLoadError = _("Error upgrading chainstate database").translated; break; } // ReplayBlocks is a no-op if we cleared the coinsviewdb with -reindex or -reindex-chainstate - if (!ReplayBlocks(chainparams, pcoinsdbview.get())) { + if (!ReplayBlocks(chainparams, &::ChainstateActive().CoinsDB())) { strLoadError = _("Unable to replay blocks. You will need to rebuild the database using -reindex-chainstate.").translated; break; } // The on-disk coinsdb is now in a good state, create the cache - pcoinsTip.reset(new CCoinsViewCache(pcoinscatcher.get())); + ::ChainstateActive().InitCoinsCache(); + assert(::ChainstateActive().CanFlushToDisk()); - is_coinsview_empty = fReset || fReindexChainState || pcoinsTip->GetBestBlock().IsNull(); + is_coinsview_empty = fReset || fReindexChainState || + ::ChainstateActive().CoinsTip().GetBestBlock().IsNull(); if (!is_coinsview_empty) { - // LoadChainTip sets ::ChainActive() based on pcoinsTip's best block + // LoadChainTip sets ::ChainActive() based on CoinsTip()'s best block if (!LoadChainTip(chainparams)) { strLoadError = _("Error initializing block database").translated; break; @@ -1588,7 +1592,7 @@ bool AppInitMain(InitInterfaces& interfaces) break; } - if (!CVerifyDB().VerifyDB(chainparams, pcoinsdbview.get(), gArgs.GetArg("-checklevel", DEFAULT_CHECKLEVEL), + if (!CVerifyDB().VerifyDB(chainparams, &::ChainstateActive().CoinsDB(), gArgs.GetArg("-checklevel", DEFAULT_CHECKLEVEL), gArgs.GetArg("-checkblocks", DEFAULT_CHECKBLOCKS))) { strLoadError = _("Corrupted block database detected").translated; break; diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index c35c478f33..8061939a26 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -1062,7 +1062,7 @@ static UniValue gettxoutsetinfo(const JSONRPCRequest& request) CCoinsStats stats; ::ChainstateActive().ForceFlushStateToDisk(); - if (GetUTXOStats(pcoinsdbview.get(), stats)) { + if (GetUTXOStats(&::ChainstateActive().CoinsDB(), stats)) { ret.pushKV("height", (int64_t)stats.nHeight); ret.pushKV("bestblock", stats.hashBlock.GetHex()); ret.pushKV("transactions", (int64_t)stats.nTransactions); @@ -2206,7 +2206,7 @@ UniValue scantxoutset(const JSONRPCRequest& request) { LOCK(cs_main); ::ChainstateActive().ForceFlushStateToDisk(); - pcursor = std::unique_ptr(pcoinsdbview->Cursor()); + pcursor = std::unique_ptr(::ChainstateActive().CoinsDB().Cursor()); assert(pcursor); } bool res = FindScriptPubKey(g_scan_progress, g_should_abort_scan, count, pcursor.get(), needles, coins); diff --git a/src/test/setup_common.cpp b/src/test/setup_common.cpp index de877fd167..0ff081e472 100644 --- a/src/test/setup_common.cpp +++ b/src/test/setup_common.cpp @@ -85,8 +85,12 @@ TestingSetup::TestingSetup(const std::string& chainName) : BasicTestingSetup(cha mempool.setSanityCheck(1.0); pblocktree.reset(new CBlockTreeDB(1 << 20, true)); - pcoinsdbview.reset(new CCoinsViewDB(1 << 23, true)); - pcoinsTip.reset(new CCoinsViewCache(pcoinsdbview.get())); + g_chainstate = MakeUnique(); + ::ChainstateActive().InitCoinsDB( + /* cache_size_bytes */ 1 << 23, /* in_memory */ true, /* should_wipe */ false); + assert(!::ChainstateActive().CanFlushToDisk()); + ::ChainstateActive().InitCoinsCache(); + assert(::ChainstateActive().CanFlushToDisk()); if (!LoadGenesisBlock(chainparams)) { throw std::runtime_error("LoadGenesisBlock failed."); } @@ -113,8 +117,7 @@ TestingSetup::~TestingSetup() g_connman.reset(); g_banman.reset(); UnloadBlockIndex(); - pcoinsTip.reset(); - pcoinsdbview.reset(); + g_chainstate.reset(); pblocktree.reset(); } diff --git a/src/txdb.cpp b/src/txdb.cpp index df9851396e..18be07e6db 100644 --- a/src/txdb.cpp +++ b/src/txdb.cpp @@ -52,7 +52,7 @@ struct CoinEntry { } -CCoinsViewDB::CCoinsViewDB(size_t nCacheSize, bool fMemory, bool fWipe) : db(GetDataDir() / "chainstate", nCacheSize, fMemory, fWipe, true) +CCoinsViewDB::CCoinsViewDB(fs::path ldb_path, size_t nCacheSize, bool fMemory, bool fWipe) : db(ldb_path, nCacheSize, fMemory, fWipe, true) { } diff --git a/src/txdb.h b/src/txdb.h index c4ece11503..140ce2c7ff 100644 --- a/src/txdb.h +++ b/src/txdb.h @@ -48,7 +48,10 @@ class CCoinsViewDB final : public CCoinsView protected: CDBWrapper db; public: - explicit CCoinsViewDB(size_t nCacheSize, bool fMemory = false, bool fWipe = false); + /** + * @param[in] ldb_path Location in the filesystem where leveldb data will be stored. + */ + explicit CCoinsViewDB(fs::path ldb_path, size_t nCacheSize, bool fMemory, bool fWipe); bool GetCoin(const COutPoint &outpoint, Coin &coin) const override; bool HaveCoin(const COutPoint &outpoint) const override; diff --git a/src/validation.cpp b/src/validation.cpp index f9d38a3099..0f24321aaf 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -82,11 +82,17 @@ namespace { BlockManager g_blockman; } // anon namespace -static CChainState g_chainstate(g_blockman); +std::unique_ptr g_chainstate; -CChainState& ChainstateActive() { return g_chainstate; } +CChainState& ChainstateActive() { + assert(g_chainstate); + return *g_chainstate; +} -CChain& ChainActive() { return g_chainstate.m_chain; } +CChain& ChainActive() { + assert(g_chainstate); + return g_chainstate->m_chain; +} /** * Mutex to guard access to validation specific variables, such as reading @@ -173,8 +179,6 @@ CBlockIndex* FindForkInGlobalIndex(const CChain& chain, const CBlockLocator& loc return chain.Genesis(); } -std::unique_ptr pcoinsdbview; -std::unique_ptr pcoinsTip; std::unique_ptr pblocktree; // See definition for documentation @@ -525,7 +529,7 @@ static bool AcceptToMemoryPoolWorker(const CChainParams& chainparams, CTxMemPool } // Note: this call may add txin.prevout to the coins cache - // (pcoinsTip.cacheCoins) by way of FetchCoin(). It should be removed + // (CoinsTip().cacheCoins) by way of FetchCoin(). It should be removed // later (via coins_to_uncache) if this tx turns out to be invalid. if (!view.HaveCoin(txin.prevout)) { // Are inputs missing because we already have the tx? @@ -1041,6 +1045,40 @@ CAmount GetBlockSubsidy(int nHeight, const Consensus::Params& consensusParams) return nSubsidy; } +CoinsViews::CoinsViews( + std::string ldb_name, + size_t cache_size_bytes, + bool in_memory, + bool should_wipe) : m_dbview( + GetDataDir() / ldb_name, cache_size_bytes, in_memory, should_wipe), + m_catcherview(&m_dbview) {} + +void CoinsViews::InitCache() +{ + m_cacheview = MakeUnique(&m_catcherview); +} + +// NOTE: for now m_blockman is set to a global, but this will be changed +// in a future commit. +CChainState::CChainState() : m_blockman(g_blockman) {} + + +void CChainState::InitCoinsDB( + size_t cache_size_bytes, + bool in_memory, + bool should_wipe, + std::string leveldb_name) +{ + m_coins_views = MakeUnique( + leveldb_name, cache_size_bytes, in_memory, should_wipe); +} + +void CChainState::InitCoinsCache() +{ + assert(m_coins_views != nullptr); + m_coins_views->InitCache(); +} + // Note that though this is marked const, we may end up modifying `m_cached_finished_ibd`, which // is a performance-related implementation detail. This function must be marked // `const` so that `CValidationInterface` clients (which are given a `const CChainState*`) @@ -1982,6 +2020,7 @@ bool CChainState::FlushStateToDisk( { int64_t nMempoolUsage = mempool.DynamicMemoryUsage(); LOCK(cs_main); + assert(this->CanFlushToDisk()); static int64_t nLastWrite = 0; static int64_t nLastFlush = 0; std::set setFilesToPrune; diff --git a/src/validation.h b/src/validation.h index 6661369bf8..2a268d8cab 100644 --- a/src/validation.h +++ b/src/validation.h @@ -19,6 +19,7 @@ #include