mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-01-12 21:02:38 -03:00
2f71a1ea35
f19fdd47a6
test: add test for CChainState::ResizeCoinsCaches() (James O'Beirne)8ac3ef4699
add ChainstateManager::MaybeRebalanceCaches() (James O'Beirne)f36aaa6392
Add CChainState::ResizeCoinsCaches (James O'Beirne)b223111da2
txdb: add CCoinsViewDB::ChangeCacheSize (James O'Beirne) Pull request description: This is part of the [assumeutxo project](https://github.com/bitcoin/bitcoin/projects/11): Parent PR: #15606 Issue: #15605 Specification: https://github.com/jamesob/assumeutxo-docs/tree/master/proposal --- In the assumeutxo implementation draft (#15056), once a UTXO snapshot is loaded, a new chainstate object is created after initialization. This means that we have to reclaim some of the cache that we've allocated to the original chainstate (per `dbcache=`) to repurpose for the snapshot chainstate. Furthermore, it makes sense to have different cache allocations depending on which chainstate is more active. While the snapshot chainstate is working to get to the network tip (and the background validation chainstate is idle), it makes sense that the snapshot chainstate should have the majority of cache allocation. And contrariwise once the snapshot has reached network tip, most of the cache should be given to the background validation chainstate. This set of changes (detailed in the commit messages) allows us to dynamically resize the various coins caches. None of the functionality introduced here is used at the moment, but will be in the next AU PR (which introduces `ActivateSnapshot`). `ChainstateManager::MaybeRebalanceCaches()` defines the (somewhat normative) cache allocations between the snapshot and background validation chainstates. I'd be interested in feedback if anyone has thoughts on the proportions I've set there. ACKs for top commit: ajtowns: weak utACKf19fdd47a6
-- didn't find any major problems, but not super confident that I didn't miss anything fjahr: Code review ACKf19fdd4
ryanofsky: Code review ACKf19fdd47a6
. Only change since last review is constructor cleanup (no change in behavior). I think the suggestions here from ajtowns and others are good, but shouldn't delay merging the PR (and hold up assumeutxo) Tree-SHA512: fffb7847fb6993dd4a1a41cf11179b211b0b20b7eb5f7cf6266442136bfe9d43b830bbefcafd475bfd4af273f5573500594aa41fff03e0ed5c2a1e8562ff9269
409 lines
14 KiB
C++
409 lines
14 KiB
C++
// Copyright (c) 2009-2010 Satoshi Nakamoto
|
|
// Copyright (c) 2009-2020 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 <txdb.h>
|
|
|
|
#include <node/ui_interface.h>
|
|
#include <pow.h>
|
|
#include <random.h>
|
|
#include <shutdown.h>
|
|
#include <uint256.h>
|
|
#include <util/memory.h>
|
|
#include <util/system.h>
|
|
#include <util/translation.h>
|
|
#include <util/vector.h>
|
|
|
|
#include <stdint.h>
|
|
|
|
static const char DB_COIN = 'C';
|
|
static const char DB_COINS = 'c';
|
|
static const char DB_BLOCK_FILES = 'f';
|
|
static const char DB_BLOCK_INDEX = 'b';
|
|
|
|
static const char DB_BEST_BLOCK = 'B';
|
|
static const char DB_HEAD_BLOCKS = 'H';
|
|
static const char DB_FLAG = 'F';
|
|
static const char DB_REINDEX_FLAG = 'R';
|
|
static const char DB_LAST_BLOCK = 'l';
|
|
|
|
namespace {
|
|
|
|
struct CoinEntry {
|
|
COutPoint* outpoint;
|
|
char key;
|
|
explicit CoinEntry(const COutPoint* ptr) : outpoint(const_cast<COutPoint*>(ptr)), key(DB_COIN) {}
|
|
|
|
SERIALIZE_METHODS(CoinEntry, obj) { READWRITE(obj.key, obj.outpoint->hash, VARINT(obj.outpoint->n)); }
|
|
};
|
|
|
|
}
|
|
|
|
CCoinsViewDB::CCoinsViewDB(fs::path ldb_path, size_t nCacheSize, bool fMemory, bool fWipe) :
|
|
m_db(MakeUnique<CDBWrapper>(ldb_path, nCacheSize, fMemory, fWipe, true)),
|
|
m_ldb_path(ldb_path),
|
|
m_is_memory(fMemory) { }
|
|
|
|
void CCoinsViewDB::ResizeCache(size_t new_cache_size)
|
|
{
|
|
// Have to do a reset first to get the original `m_db` state to release its
|
|
// filesystem lock.
|
|
m_db.reset();
|
|
m_db = MakeUnique<CDBWrapper>(
|
|
m_ldb_path, new_cache_size, m_is_memory, /*fWipe*/ false, /*obfuscate*/ true);
|
|
}
|
|
|
|
bool CCoinsViewDB::GetCoin(const COutPoint &outpoint, Coin &coin) const {
|
|
return m_db->Read(CoinEntry(&outpoint), coin);
|
|
}
|
|
|
|
bool CCoinsViewDB::HaveCoin(const COutPoint &outpoint) const {
|
|
return m_db->Exists(CoinEntry(&outpoint));
|
|
}
|
|
|
|
uint256 CCoinsViewDB::GetBestBlock() const {
|
|
uint256 hashBestChain;
|
|
if (!m_db->Read(DB_BEST_BLOCK, hashBestChain))
|
|
return uint256();
|
|
return hashBestChain;
|
|
}
|
|
|
|
std::vector<uint256> CCoinsViewDB::GetHeadBlocks() const {
|
|
std::vector<uint256> vhashHeadBlocks;
|
|
if (!m_db->Read(DB_HEAD_BLOCKS, vhashHeadBlocks)) {
|
|
return std::vector<uint256>();
|
|
}
|
|
return vhashHeadBlocks;
|
|
}
|
|
|
|
bool CCoinsViewDB::BatchWrite(CCoinsMap &mapCoins, const uint256 &hashBlock) {
|
|
CDBBatch batch(*m_db);
|
|
size_t count = 0;
|
|
size_t changed = 0;
|
|
size_t batch_size = (size_t)gArgs.GetArg("-dbbatchsize", nDefaultDbBatchSize);
|
|
int crash_simulate = gArgs.GetArg("-dbcrashratio", 0);
|
|
assert(!hashBlock.IsNull());
|
|
|
|
uint256 old_tip = GetBestBlock();
|
|
if (old_tip.IsNull()) {
|
|
// We may be in the middle of replaying.
|
|
std::vector<uint256> old_heads = GetHeadBlocks();
|
|
if (old_heads.size() == 2) {
|
|
assert(old_heads[0] == hashBlock);
|
|
old_tip = old_heads[1];
|
|
}
|
|
}
|
|
|
|
// In the first batch, mark the database as being in the middle of a
|
|
// transition from old_tip to hashBlock.
|
|
// A vector is used for future extensibility, as we may want to support
|
|
// interrupting after partial writes from multiple independent reorgs.
|
|
batch.Erase(DB_BEST_BLOCK);
|
|
batch.Write(DB_HEAD_BLOCKS, Vector(hashBlock, old_tip));
|
|
|
|
for (CCoinsMap::iterator it = mapCoins.begin(); it != mapCoins.end();) {
|
|
if (it->second.flags & CCoinsCacheEntry::DIRTY) {
|
|
CoinEntry entry(&it->first);
|
|
if (it->second.coin.IsSpent())
|
|
batch.Erase(entry);
|
|
else
|
|
batch.Write(entry, it->second.coin);
|
|
changed++;
|
|
}
|
|
count++;
|
|
CCoinsMap::iterator itOld = it++;
|
|
mapCoins.erase(itOld);
|
|
if (batch.SizeEstimate() > batch_size) {
|
|
LogPrint(BCLog::COINDB, "Writing partial batch of %.2f MiB\n", batch.SizeEstimate() * (1.0 / 1048576.0));
|
|
m_db->WriteBatch(batch);
|
|
batch.Clear();
|
|
if (crash_simulate) {
|
|
static FastRandomContext rng;
|
|
if (rng.randrange(crash_simulate) == 0) {
|
|
LogPrintf("Simulating a crash. Goodbye.\n");
|
|
_Exit(0);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// In the last batch, mark the database as consistent with hashBlock again.
|
|
batch.Erase(DB_HEAD_BLOCKS);
|
|
batch.Write(DB_BEST_BLOCK, hashBlock);
|
|
|
|
LogPrint(BCLog::COINDB, "Writing final batch of %.2f MiB\n", batch.SizeEstimate() * (1.0 / 1048576.0));
|
|
bool ret = m_db->WriteBatch(batch);
|
|
LogPrint(BCLog::COINDB, "Committed %u changed transaction outputs (out of %u) to coin database...\n", (unsigned int)changed, (unsigned int)count);
|
|
return ret;
|
|
}
|
|
|
|
size_t CCoinsViewDB::EstimateSize() const
|
|
{
|
|
return m_db->EstimateSize(DB_COIN, (char)(DB_COIN+1));
|
|
}
|
|
|
|
CBlockTreeDB::CBlockTreeDB(size_t nCacheSize, bool fMemory, bool fWipe) : CDBWrapper(GetDataDir() / "blocks" / "index", nCacheSize, fMemory, fWipe) {
|
|
}
|
|
|
|
bool CBlockTreeDB::ReadBlockFileInfo(int nFile, CBlockFileInfo &info) {
|
|
return Read(std::make_pair(DB_BLOCK_FILES, nFile), info);
|
|
}
|
|
|
|
bool CBlockTreeDB::WriteReindexing(bool fReindexing) {
|
|
if (fReindexing)
|
|
return Write(DB_REINDEX_FLAG, '1');
|
|
else
|
|
return Erase(DB_REINDEX_FLAG);
|
|
}
|
|
|
|
void CBlockTreeDB::ReadReindexing(bool &fReindexing) {
|
|
fReindexing = Exists(DB_REINDEX_FLAG);
|
|
}
|
|
|
|
bool CBlockTreeDB::ReadLastBlockFile(int &nFile) {
|
|
return Read(DB_LAST_BLOCK, nFile);
|
|
}
|
|
|
|
CCoinsViewCursor *CCoinsViewDB::Cursor() const
|
|
{
|
|
CCoinsViewDBCursor *i = new CCoinsViewDBCursor(const_cast<CDBWrapper&>(*m_db).NewIterator(), GetBestBlock());
|
|
/* It seems that there are no "const iterators" for LevelDB. Since we
|
|
only need read operations on it, use a const-cast to get around
|
|
that restriction. */
|
|
i->pcursor->Seek(DB_COIN);
|
|
// Cache key of first record
|
|
if (i->pcursor->Valid()) {
|
|
CoinEntry entry(&i->keyTmp.second);
|
|
i->pcursor->GetKey(entry);
|
|
i->keyTmp.first = entry.key;
|
|
} else {
|
|
i->keyTmp.first = 0; // Make sure Valid() and GetKey() return false
|
|
}
|
|
return i;
|
|
}
|
|
|
|
bool CCoinsViewDBCursor::GetKey(COutPoint &key) const
|
|
{
|
|
// Return cached key
|
|
if (keyTmp.first == DB_COIN) {
|
|
key = keyTmp.second;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool CCoinsViewDBCursor::GetValue(Coin &coin) const
|
|
{
|
|
return pcursor->GetValue(coin);
|
|
}
|
|
|
|
unsigned int CCoinsViewDBCursor::GetValueSize() const
|
|
{
|
|
return pcursor->GetValueSize();
|
|
}
|
|
|
|
bool CCoinsViewDBCursor::Valid() const
|
|
{
|
|
return keyTmp.first == DB_COIN;
|
|
}
|
|
|
|
void CCoinsViewDBCursor::Next()
|
|
{
|
|
pcursor->Next();
|
|
CoinEntry entry(&keyTmp.second);
|
|
if (!pcursor->Valid() || !pcursor->GetKey(entry)) {
|
|
keyTmp.first = 0; // Invalidate cached key after last record so that Valid() and GetKey() return false
|
|
} else {
|
|
keyTmp.first = entry.key;
|
|
}
|
|
}
|
|
|
|
bool CBlockTreeDB::WriteBatchSync(const std::vector<std::pair<int, const CBlockFileInfo*> >& fileInfo, int nLastFile, const std::vector<const CBlockIndex*>& blockinfo) {
|
|
CDBBatch batch(*this);
|
|
for (std::vector<std::pair<int, const CBlockFileInfo*> >::const_iterator it=fileInfo.begin(); it != fileInfo.end(); it++) {
|
|
batch.Write(std::make_pair(DB_BLOCK_FILES, it->first), *it->second);
|
|
}
|
|
batch.Write(DB_LAST_BLOCK, nLastFile);
|
|
for (std::vector<const CBlockIndex*>::const_iterator it=blockinfo.begin(); it != blockinfo.end(); it++) {
|
|
batch.Write(std::make_pair(DB_BLOCK_INDEX, (*it)->GetBlockHash()), CDiskBlockIndex(*it));
|
|
}
|
|
return WriteBatch(batch, true);
|
|
}
|
|
|
|
bool CBlockTreeDB::WriteFlag(const std::string &name, bool fValue) {
|
|
return Write(std::make_pair(DB_FLAG, name), fValue ? '1' : '0');
|
|
}
|
|
|
|
bool CBlockTreeDB::ReadFlag(const std::string &name, bool &fValue) {
|
|
char ch;
|
|
if (!Read(std::make_pair(DB_FLAG, name), ch))
|
|
return false;
|
|
fValue = ch == '1';
|
|
return true;
|
|
}
|
|
|
|
bool CBlockTreeDB::LoadBlockIndexGuts(const Consensus::Params& consensusParams, std::function<CBlockIndex*(const uint256&)> insertBlockIndex)
|
|
{
|
|
std::unique_ptr<CDBIterator> pcursor(NewIterator());
|
|
|
|
pcursor->Seek(std::make_pair(DB_BLOCK_INDEX, uint256()));
|
|
|
|
// Load m_block_index
|
|
while (pcursor->Valid()) {
|
|
if (ShutdownRequested()) return false;
|
|
std::pair<char, uint256> key;
|
|
if (pcursor->GetKey(key) && key.first == DB_BLOCK_INDEX) {
|
|
CDiskBlockIndex diskindex;
|
|
if (pcursor->GetValue(diskindex)) {
|
|
// Construct block index object
|
|
CBlockIndex* pindexNew = insertBlockIndex(diskindex.GetBlockHash());
|
|
pindexNew->pprev = insertBlockIndex(diskindex.hashPrev);
|
|
pindexNew->nHeight = diskindex.nHeight;
|
|
pindexNew->nFile = diskindex.nFile;
|
|
pindexNew->nDataPos = diskindex.nDataPos;
|
|
pindexNew->nUndoPos = diskindex.nUndoPos;
|
|
pindexNew->nVersion = diskindex.nVersion;
|
|
pindexNew->hashMerkleRoot = diskindex.hashMerkleRoot;
|
|
pindexNew->nTime = diskindex.nTime;
|
|
pindexNew->nBits = diskindex.nBits;
|
|
pindexNew->nNonce = diskindex.nNonce;
|
|
pindexNew->nStatus = diskindex.nStatus;
|
|
pindexNew->nTx = diskindex.nTx;
|
|
|
|
if (!CheckProofOfWork(pindexNew->GetBlockHash(), pindexNew->nBits, consensusParams))
|
|
return error("%s: CheckProofOfWork failed: %s", __func__, pindexNew->ToString());
|
|
|
|
pcursor->Next();
|
|
} else {
|
|
return error("%s: failed to read value", __func__);
|
|
}
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
namespace {
|
|
|
|
//! Legacy class to deserialize pre-pertxout database entries without reindex.
|
|
class CCoins
|
|
{
|
|
public:
|
|
//! whether transaction is a coinbase
|
|
bool fCoinBase;
|
|
|
|
//! unspent transaction outputs; spent outputs are .IsNull(); spent outputs at the end of the array are dropped
|
|
std::vector<CTxOut> vout;
|
|
|
|
//! at which height this transaction was included in the active block chain
|
|
int nHeight;
|
|
|
|
//! empty constructor
|
|
CCoins() : fCoinBase(false), vout(0), nHeight(0) { }
|
|
|
|
template<typename Stream>
|
|
void Unserialize(Stream &s) {
|
|
unsigned int nCode = 0;
|
|
// version
|
|
unsigned int nVersionDummy;
|
|
::Unserialize(s, VARINT(nVersionDummy));
|
|
// header code
|
|
::Unserialize(s, VARINT(nCode));
|
|
fCoinBase = nCode & 1;
|
|
std::vector<bool> vAvail(2, false);
|
|
vAvail[0] = (nCode & 2) != 0;
|
|
vAvail[1] = (nCode & 4) != 0;
|
|
unsigned int nMaskCode = (nCode / 8) + ((nCode & 6) != 0 ? 0 : 1);
|
|
// spentness bitmask
|
|
while (nMaskCode > 0) {
|
|
unsigned char chAvail = 0;
|
|
::Unserialize(s, chAvail);
|
|
for (unsigned int p = 0; p < 8; p++) {
|
|
bool f = (chAvail & (1 << p)) != 0;
|
|
vAvail.push_back(f);
|
|
}
|
|
if (chAvail != 0)
|
|
nMaskCode--;
|
|
}
|
|
// txouts themself
|
|
vout.assign(vAvail.size(), CTxOut());
|
|
for (unsigned int i = 0; i < vAvail.size(); i++) {
|
|
if (vAvail[i])
|
|
::Unserialize(s, Using<TxOutCompression>(vout[i]));
|
|
}
|
|
// coinbase height
|
|
::Unserialize(s, VARINT_MODE(nHeight, VarIntMode::NONNEGATIVE_SIGNED));
|
|
}
|
|
};
|
|
|
|
}
|
|
|
|
/** Upgrade the database from older formats.
|
|
*
|
|
* Currently implemented: from the per-tx utxo model (0.8..0.14.x) to per-txout.
|
|
*/
|
|
bool CCoinsViewDB::Upgrade() {
|
|
std::unique_ptr<CDBIterator> pcursor(m_db->NewIterator());
|
|
pcursor->Seek(std::make_pair(DB_COINS, uint256()));
|
|
if (!pcursor->Valid()) {
|
|
return true;
|
|
}
|
|
|
|
int64_t count = 0;
|
|
LogPrintf("Upgrading utxo-set database...\n");
|
|
LogPrintf("[0%%]..."); /* Continued */
|
|
uiInterface.ShowProgress(_("Upgrading UTXO database").translated, 0, true);
|
|
size_t batch_size = 1 << 24;
|
|
CDBBatch batch(*m_db);
|
|
int reportDone = 0;
|
|
std::pair<unsigned char, uint256> key;
|
|
std::pair<unsigned char, uint256> prev_key = {DB_COINS, uint256()};
|
|
while (pcursor->Valid()) {
|
|
if (ShutdownRequested()) {
|
|
break;
|
|
}
|
|
if (pcursor->GetKey(key) && key.first == DB_COINS) {
|
|
if (count++ % 256 == 0) {
|
|
uint32_t high = 0x100 * *key.second.begin() + *(key.second.begin() + 1);
|
|
int percentageDone = (int)(high * 100.0 / 65536.0 + 0.5);
|
|
uiInterface.ShowProgress(_("Upgrading UTXO database").translated, percentageDone, true);
|
|
if (reportDone < percentageDone/10) {
|
|
// report max. every 10% step
|
|
LogPrintf("[%d%%]...", percentageDone); /* Continued */
|
|
reportDone = percentageDone/10;
|
|
}
|
|
}
|
|
CCoins old_coins;
|
|
if (!pcursor->GetValue(old_coins)) {
|
|
return error("%s: cannot parse CCoins record", __func__);
|
|
}
|
|
COutPoint outpoint(key.second, 0);
|
|
for (size_t i = 0; i < old_coins.vout.size(); ++i) {
|
|
if (!old_coins.vout[i].IsNull() && !old_coins.vout[i].scriptPubKey.IsUnspendable()) {
|
|
Coin newcoin(std::move(old_coins.vout[i]), old_coins.nHeight, old_coins.fCoinBase);
|
|
outpoint.n = i;
|
|
CoinEntry entry(&outpoint);
|
|
batch.Write(entry, newcoin);
|
|
}
|
|
}
|
|
batch.Erase(key);
|
|
if (batch.SizeEstimate() > batch_size) {
|
|
m_db->WriteBatch(batch);
|
|
batch.Clear();
|
|
m_db->CompactRange(prev_key, key);
|
|
prev_key = key;
|
|
}
|
|
pcursor->Next();
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
m_db->WriteBatch(batch);
|
|
m_db->CompactRange({DB_COINS, uint256()}, key);
|
|
uiInterface.ShowProgress("", 100, false);
|
|
LogPrintf("[%s].\n", ShutdownRequested() ? "CANCELLED" : "DONE");
|
|
return !ShutdownRequested();
|
|
}
|