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.
This commit is contained in:
James O'Beirne 2019-07-24 13:23:48 -04:00
parent fae6ab6aed
commit 5693530685
7 changed files with 170 additions and 43 deletions

View file

@ -15,7 +15,6 @@
#include <blockfilter.h> #include <blockfilter.h>
#include <chain.h> #include <chain.h>
#include <chainparams.h> #include <chainparams.h>
#include <coins.h>
#include <compat/sanity.h> #include <compat/sanity.h>
#include <consensus/validation.h> #include <consensus/validation.h>
#include <fs.h> #include <fs.h>
@ -149,7 +148,6 @@ NODISCARD static bool CreatePidFile()
// shutdown thing. // shutdown thing.
// //
static std::unique_ptr<CCoinsViewErrorCatcher> pcoinscatcher;
static std::unique_ptr<ECCVerifyHandle> globalVerifyHandle; static std::unique_ptr<ECCVerifyHandle> globalVerifyHandle;
static boost::thread_group threadGroup; static boost::thread_group threadGroup;
@ -234,8 +232,11 @@ void Shutdown(InitInterfaces& interfaces)
} }
// FlushStateToDisk generates a ChainStateFlushed callback, which we should avoid missing // 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 // 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); LOCK(cs_main);
if (pcoinsTip != nullptr) { if (g_chainstate && g_chainstate->CanFlushToDisk()) {
::ChainstateActive().ForceFlushStateToDisk(); g_chainstate->ForceFlushStateToDisk();
g_chainstate->ResetCoinsViews();
} }
pcoinsTip.reset();
pcoinscatcher.reset();
pcoinsdbview.reset();
pblocktree.reset(); pblocktree.reset();
} }
for (const auto& client : interfaces.chain_clients) { for (const auto& client : interfaces.chain_clients) {
@ -1466,10 +1465,10 @@ bool AppInitMain(InitInterfaces& interfaces)
bool is_coinsview_empty; bool is_coinsview_empty;
try { try {
LOCK(cs_main); LOCK(cs_main);
// This statement makes ::ChainstateActive() usable.
g_chainstate = MakeUnique<CChainState>();
UnloadBlockIndex(); UnloadBlockIndex();
pcoinsTip.reset();
pcoinsdbview.reset();
pcoinscatcher.reset();
// new CBlockTreeDB tries to delete the existing file, which // new CBlockTreeDB tries to delete the existing file, which
// fails if it's still open from the previous loop. Close it first: // fails if it's still open from the previous loop. Close it first:
pblocktree.reset(); pblocktree.reset();
@ -1520,9 +1519,12 @@ bool AppInitMain(InitInterfaces& interfaces)
// At this point we're either in reindex or we've loaded a useful // At this point we're either in reindex or we've loaded a useful
// block tree into BlockIndex()! // block tree into BlockIndex()!
pcoinsdbview.reset(new CCoinsViewDB(nCoinDBCache, false, fReset || fReindexChainState)); ::ChainstateActive().InitCoinsDB(
pcoinscatcher.reset(new CCoinsViewErrorCatcher(pcoinsdbview.get())); /* cache_size_bytes */ nCoinDBCache,
pcoinscatcher->AddReadErrCallback([]() { /* in_memory */ false,
/* should_wipe */ fReset || fReindexChainState);
::ChainstateActive().CoinsErrorCatcher().AddReadErrCallback([]() {
uiInterface.ThreadSafeMessageBox( uiInterface.ThreadSafeMessageBox(
_("Error reading from database, shutting down.").translated, _("Error reading from database, shutting down.").translated,
"", CClientUIInterface::MSG_ERROR); "", CClientUIInterface::MSG_ERROR);
@ -1530,23 +1532,25 @@ bool AppInitMain(InitInterfaces& interfaces)
// If necessary, upgrade from older database format. // If necessary, upgrade from older database format.
// This is a no-op if we cleared the coinsviewdb with -reindex or -reindex-chainstate // 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; strLoadError = _("Error upgrading chainstate database").translated;
break; break;
} }
// ReplayBlocks is a no-op if we cleared the coinsviewdb with -reindex or -reindex-chainstate // 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; strLoadError = _("Unable to replay blocks. You will need to rebuild the database using -reindex-chainstate.").translated;
break; break;
} }
// The on-disk coinsdb is now in a good state, create the cache // 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) { 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)) { if (!LoadChainTip(chainparams)) {
strLoadError = _("Error initializing block database").translated; strLoadError = _("Error initializing block database").translated;
break; break;
@ -1588,7 +1592,7 @@ bool AppInitMain(InitInterfaces& interfaces)
break; 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))) { gArgs.GetArg("-checkblocks", DEFAULT_CHECKBLOCKS))) {
strLoadError = _("Corrupted block database detected").translated; strLoadError = _("Corrupted block database detected").translated;
break; break;

View file

@ -1062,7 +1062,7 @@ static UniValue gettxoutsetinfo(const JSONRPCRequest& request)
CCoinsStats stats; CCoinsStats stats;
::ChainstateActive().ForceFlushStateToDisk(); ::ChainstateActive().ForceFlushStateToDisk();
if (GetUTXOStats(pcoinsdbview.get(), stats)) { if (GetUTXOStats(&::ChainstateActive().CoinsDB(), stats)) {
ret.pushKV("height", (int64_t)stats.nHeight); ret.pushKV("height", (int64_t)stats.nHeight);
ret.pushKV("bestblock", stats.hashBlock.GetHex()); ret.pushKV("bestblock", stats.hashBlock.GetHex());
ret.pushKV("transactions", (int64_t)stats.nTransactions); ret.pushKV("transactions", (int64_t)stats.nTransactions);
@ -2206,7 +2206,7 @@ UniValue scantxoutset(const JSONRPCRequest& request)
{ {
LOCK(cs_main); LOCK(cs_main);
::ChainstateActive().ForceFlushStateToDisk(); ::ChainstateActive().ForceFlushStateToDisk();
pcursor = std::unique_ptr<CCoinsViewCursor>(pcoinsdbview->Cursor()); pcursor = std::unique_ptr<CCoinsViewCursor>(::ChainstateActive().CoinsDB().Cursor());
assert(pcursor); assert(pcursor);
} }
bool res = FindScriptPubKey(g_scan_progress, g_should_abort_scan, count, pcursor.get(), needles, coins); bool res = FindScriptPubKey(g_scan_progress, g_should_abort_scan, count, pcursor.get(), needles, coins);

View file

@ -85,8 +85,12 @@ TestingSetup::TestingSetup(const std::string& chainName) : BasicTestingSetup(cha
mempool.setSanityCheck(1.0); mempool.setSanityCheck(1.0);
pblocktree.reset(new CBlockTreeDB(1 << 20, true)); pblocktree.reset(new CBlockTreeDB(1 << 20, true));
pcoinsdbview.reset(new CCoinsViewDB(1 << 23, true)); g_chainstate = MakeUnique<CChainState>();
pcoinsTip.reset(new CCoinsViewCache(pcoinsdbview.get())); ::ChainstateActive().InitCoinsDB(
/* cache_size_bytes */ 1 << 23, /* in_memory */ true, /* should_wipe */ false);
assert(!::ChainstateActive().CanFlushToDisk());
::ChainstateActive().InitCoinsCache();
assert(::ChainstateActive().CanFlushToDisk());
if (!LoadGenesisBlock(chainparams)) { if (!LoadGenesisBlock(chainparams)) {
throw std::runtime_error("LoadGenesisBlock failed."); throw std::runtime_error("LoadGenesisBlock failed.");
} }
@ -113,8 +117,7 @@ TestingSetup::~TestingSetup()
g_connman.reset(); g_connman.reset();
g_banman.reset(); g_banman.reset();
UnloadBlockIndex(); UnloadBlockIndex();
pcoinsTip.reset(); g_chainstate.reset();
pcoinsdbview.reset();
pblocktree.reset(); pblocktree.reset();
} }

View file

@ -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)
{ {
} }

View file

@ -48,7 +48,10 @@ class CCoinsViewDB final : public CCoinsView
protected: protected:
CDBWrapper db; CDBWrapper db;
public: 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 GetCoin(const COutPoint &outpoint, Coin &coin) const override;
bool HaveCoin(const COutPoint &outpoint) const override; bool HaveCoin(const COutPoint &outpoint) const override;

View file

@ -82,11 +82,17 @@ namespace {
BlockManager g_blockman; BlockManager g_blockman;
} // anon namespace } // anon namespace
static CChainState g_chainstate(g_blockman); std::unique_ptr<CChainState> 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 * 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(); return chain.Genesis();
} }
std::unique_ptr<CCoinsViewDB> pcoinsdbview;
std::unique_ptr<CCoinsViewCache> pcoinsTip;
std::unique_ptr<CBlockTreeDB> pblocktree; std::unique_ptr<CBlockTreeDB> pblocktree;
// See definition for documentation // 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 // 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. // later (via coins_to_uncache) if this tx turns out to be invalid.
if (!view.HaveCoin(txin.prevout)) { if (!view.HaveCoin(txin.prevout)) {
// Are inputs missing because we already have the tx? // Are inputs missing because we already have the tx?
@ -1041,6 +1045,40 @@ CAmount GetBlockSubsidy(int nHeight, const Consensus::Params& consensusParams)
return nSubsidy; 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<CCoinsViewCache>(&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<CoinsViews>(
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 // 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 // is a performance-related implementation detail. This function must be marked
// `const` so that `CValidationInterface` clients (which are given a `const CChainState*`) // `const` so that `CValidationInterface` clients (which are given a `const CChainState*`)
@ -1982,6 +2020,7 @@ bool CChainState::FlushStateToDisk(
{ {
int64_t nMempoolUsage = mempool.DynamicMemoryUsage(); int64_t nMempoolUsage = mempool.DynamicMemoryUsage();
LOCK(cs_main); LOCK(cs_main);
assert(this->CanFlushToDisk());
static int64_t nLastWrite = 0; static int64_t nLastWrite = 0;
static int64_t nLastFlush = 0; static int64_t nLastFlush = 0;
std::set<int> setFilesToPrune; std::set<int> setFilesToPrune;

View file

@ -19,6 +19,7 @@
#include <script/script_error.h> #include <script/script_error.h>
#include <sync.h> #include <sync.h>
#include <txmempool.h> // For CTxMemPool::cs #include <txmempool.h> // For CTxMemPool::cs
#include <txdb.h>
#include <versionbits.h> #include <versionbits.h>
#include <algorithm> #include <algorithm>
@ -37,7 +38,6 @@ class CBlockIndex;
class CBlockTreeDB; class CBlockTreeDB;
class CBlockUndo; class CBlockUndo;
class CChainParams; class CChainParams;
class CCoinsViewDB;
class CInv; class CInv;
class CConnman; class CConnman;
class CScriptCheck; class CScriptCheck;
@ -505,8 +505,40 @@ public:
CBlockIndex** ppindex) EXCLUSIVE_LOCKS_REQUIRED(cs_main); CBlockIndex** ppindex) EXCLUSIVE_LOCKS_REQUIRED(cs_main);
}; };
/** Global variable that points to the active CCoinsView (protected by cs_main) */ /**
extern std::unique_ptr<CCoinsViewCache> pcoinsTip; * A convenience class for constructing the CCoinsView* hierarchy used
* to facilitate access to the UTXO set.
*
* This class consists of an arrangement of layered CCoinsView objects,
* preferring to store and retrieve coins in memory via `m_cacheview` but
* ultimately falling back on cache misses to the canonical store of UTXOs on
* disk, `m_dbview`.
*/
class CoinsViews {
public:
//! The lowest level of the CoinsViews cache hierarchy sits in a leveldb database on disk.
//! All unspent coins reside in this store.
CCoinsViewDB m_dbview;
//! This view wraps access to the leveldb instance and handles read errors gracefully.
CCoinsViewErrorCatcher m_catcherview;
//! This is the top layer of the cache hierarchy - it keeps as many coins in memory as
//! can fit per the dbcache setting.
std::unique_ptr<CCoinsViewCache> m_cacheview;
//! This constructor initializes CCoinsViewDB and CCoinsViewErrorCatcher instances, but it
//! *does not* create a CCoinsViewCache instance by default. This is done separately because the
//! presence of the cache has implications on whether or not we're allowed to flush the cache's
//! state to disk, which should not be done until the health of the database is verified.
//!
//! All arguments forwarded onto CCoinsViewDB.
CoinsViews(std::string ldb_name, size_t cache_size_bytes, bool in_memory, bool should_wipe);
//! Initialize the CCoinsViewCache member.
void InitCache();
};
/** /**
* CChainState stores and provides an API to update our local knowledge of the * CChainState stores and provides an API to update our local knowledge of the
@ -556,12 +588,39 @@ private:
//! easily as opposed to referencing a global. //! easily as opposed to referencing a global.
BlockManager& m_blockman; BlockManager& m_blockman;
//! Manages the UTXO set, which is a reflection of the contents of `m_chain`.
std::unique_ptr<CoinsViews> m_coins_views;
public: public:
CChainState(BlockManager& blockman) : m_blockman(blockman) {} CChainState(BlockManager& blockman) : m_blockman(blockman) {}
CChainState();
/**
* Initialize the CoinsViews UTXO set database management data structures. The in-memory
* cache is initialized separately.
*
* All parameters forwarded to CoinsViews.
*/
void InitCoinsDB(
size_t cache_size_bytes,
bool in_memory,
bool should_wipe,
std::string leveldb_name = "chainstate");
//! Initialize the in-memory coins cache (to be done after the health of the on-disk database
//! is verified).
void InitCoinsCache();
//! @returns whether or not the CoinsViews object has been fully initialized and we can
//! safely flush this object to disk.
bool CanFlushToDisk() {
return m_coins_views && m_coins_views->m_cacheview;
}
//! The current chain of blockheaders we consult and build on. //! The current chain of blockheaders we consult and build on.
//! @see CChain, CBlockIndex. //! @see CChain, CBlockIndex.
CChain m_chain; CChain m_chain;
/** /**
* The set of all CBlockIndex entries with BLOCK_VALID_TRANSACTIONS (for itself and all ancestors) and * The set of all CBlockIndex entries with BLOCK_VALID_TRANSACTIONS (for itself and all ancestors) and
* as good as our current tip or better. Entries may be failed, though, and pruning nodes may be * as good as our current tip or better. Entries may be failed, though, and pruning nodes may be
@ -572,9 +631,26 @@ public:
//! @returns A reference to the in-memory cache of the UTXO set. //! @returns A reference to the in-memory cache of the UTXO set.
CCoinsViewCache& CoinsTip() CCoinsViewCache& CoinsTip()
{ {
return *::pcoinsTip; assert(m_coins_views->m_cacheview);
return *m_coins_views->m_cacheview.get();
} }
//! @returns A reference to the on-disk UTXO set database.
CCoinsViewDB& CoinsDB()
{
return m_coins_views->m_dbview;
}
//! @returns A reference to a wrapped view of the in-memory UTXO set that
//! handles disk read errors gracefully.
CCoinsViewErrorCatcher& CoinsErrorCatcher()
{
return m_coins_views->m_catcherview;
}
//! Destructs all objects related to accessing the UTXO set.
void ResetCoinsViews() { m_coins_views.reset(); }
/** /**
* Update the on-disk chain state. * Update the on-disk chain state.
* The caches and indexes are flushed depending on the mode we're called with * The caches and indexes are flushed depending on the mode we're called with
@ -606,7 +682,7 @@ public:
bool ConnectBlock(const CBlock& block, CValidationState& state, CBlockIndex* pindex, bool ConnectBlock(const CBlock& block, CValidationState& state, CBlockIndex* pindex,
CCoinsViewCache& view, const CChainParams& chainparams, bool fJustCheck = false) EXCLUSIVE_LOCKS_REQUIRED(cs_main); CCoinsViewCache& view, const CChainParams& chainparams, bool fJustCheck = false) EXCLUSIVE_LOCKS_REQUIRED(cs_main);
// Block disconnection on our pcoinsTip: // Apply the effects of a block disconnection on the UTXO set.
bool DisconnectTip(CValidationState& state, const CChainParams& chainparams, DisconnectedBlockTransactions* disconnectpool) EXCLUSIVE_LOCKS_REQUIRED(cs_main, ::mempool.cs); bool DisconnectTip(CValidationState& state, const CChainParams& chainparams, DisconnectedBlockTransactions* disconnectpool) EXCLUSIVE_LOCKS_REQUIRED(cs_main, ::mempool.cs);
// Manual block validity manipulation: // Manual block validity manipulation:
@ -668,8 +744,10 @@ CChain& ChainActive();
/** @returns the global block index map. */ /** @returns the global block index map. */
BlockMap& BlockIndex(); BlockMap& BlockIndex();
/** Global variable that points to the coins database (protected by cs_main) */ // Most often ::ChainstateActive() should be used instead of this, but some code
extern std::unique_ptr<CCoinsViewDB> pcoinsdbview; // may not be able to assume that this has been initialized yet and so must use it
// directly, e.g. init.cpp.
extern std::unique_ptr<CChainState> g_chainstate;
/** Global variable that points to the active block tree (protected by cs_main) */ /** Global variable that points to the active block tree (protected by cs_main) */
extern std::unique_ptr<CBlockTreeDB> pblocktree; extern std::unique_ptr<CBlockTreeDB> pblocktree;