Merge #19806: validation: UTXO snapshot activation

1afc0e4aa1 doc: remove potentially confusing ChainstateManager comment (James O'Beirne)
769a1ef9fd test: Add tests with maleated snapshot data (Fabian Jahr)
4d8de04f32 tests: add snapshot activation test (James O'Beirne)
31d225274f tests: add deterministic chain generation unittest fixture (James O'Beirne)
6606a4f8c6 move-onlyish: break out CreateUTXOSnapshot from dumptxoutset (James O'Beirne)
ad949ba449 txdb: don't reset during in-memory cache resize (James O'Beirne)
f6e2da5fb7 simplify ChainstateManager::SnapshotBlockhash() return semantics (James O'Beirne)
7a6c46b37e chainparams: add allowed assumeutxo values (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

  ---

  This change proposes logic for activating UTXO snapshots, which is unused at the moment aside from an included unittest. There are a few moveonyish/refactoring commits to allow for halfway decent unittests.

  Basic structure is included for specifying and checking the assumeutxo hash values used to validate activated snapshots. Initially I had specified a few height/hash pairs for mainnet in this change, but because of the security-critical nature of those parameters, I figured it was better to leave their inclusion to a future PR that includes only that change - my intent being that reviewers will be more likely to verify those parameters firsthand in a dedicated PR.

  Aside from that and the snapshot activation logic, there are a few related changes:

  - ~~allow caching the `nChainTx` value in the CCoinsViewDB; this is set during snapshot activation. Because we don't necessarily have access to the full chain at the time of snapshot load, this value is communicated through the snapshot metadata and must be cached within the chainstate to survive restarts.~~
  - break out `CreateUTXOSnapshot()` from dumptxoutset. This is essentially a move-only change to allow the reuse of snapshot creation logic from within unittests.
  - ...and a few other misc. changes that are solely related to unittests.

  The move-onlyish commit is most easily reviewed with `--color-moved=zebra`.

ACKs for top commit:
  fjahr:
    Code review ACK 1afc0e4aa1
  laanwj:
    Code review ACK 1afc0e4aa1

Tree-SHA512: a4e4f0698f00a53ec298b5e8b7ef1c9fdf0185f95139d1b1f63cfdf6cbbd6d17b8c6e51bbf1de2e5f1a946bf49f8466232698ef55acce5a012c80b067da366ea
This commit is contained in:
Wladimir J. van der Laan 2021-02-16 19:22:45 +01:00
commit 92fee79dab
No known key found for this signature in database
GPG key ID: 1E4AED62986CD25D
17 changed files with 715 additions and 30 deletions

View file

@ -163,14 +163,27 @@ public:
//! Number of transactions in this block.
//! Note: in a potential headers-first mode, this number cannot be relied upon
//! Note: this value is faked during UTXO snapshot load to ensure that
//! LoadBlockIndex() will load index entries for blocks that we lack data for.
//! @sa ActivateSnapshot
unsigned int nTx{0};
//! (memory only) Number of transactions in the chain up to and including this block.
//! This value will be non-zero only if and only if transactions for this block and all its parents are available.
//! Change to 64-bit type when necessary; won't happen before 2030
//!
//! Note: this value is faked during use of a UTXO snapshot because we don't
//! have the underlying block data available during snapshot load.
//! @sa AssumeutxoData
//! @sa ActivateSnapshot
unsigned int nChainTx{0};
//! Verification status of this block. See enum BlockStatus
//!
//! Note: this value is modified to show BLOCK_OPT_WITNESS during UTXO snapshot
//! load to avoid the block index being spuriously rewound.
//! @sa RewindBlockIndex
//! @sa ActivateSnapshot
uint32_t nStatus{0};
//! block header

View file

@ -8,7 +8,6 @@
#include <chainparamsseeds.h>
#include <consensus/merkle.h>
#include <hash.h> // for signet block challenge hash
#include <tinyformat.h>
#include <util/system.h>
#include <util/strencodings.h>
#include <versionbitsinfo.h>
@ -161,6 +160,10 @@ public:
}
};
m_assumeutxo_data = MapAssumeutxo{
// TODO to be specified in a future patch.
};
chainTxData = ChainTxData{
// Data from RPC: getchaintxstats 4096 0000000000000000000b9d2ec5a352ecba0592946514a92f14319dc2b367fc72
/* nTime */ 1603995752,
@ -250,6 +253,10 @@ public:
}
};
m_assumeutxo_data = MapAssumeutxo{
// TODO to be specified in a future patch.
};
chainTxData = ChainTxData{
// Data from RPC: getchaintxstats 4096 000000000000006433d1efec504c53ca332b64963c425395515b01977bd7b3b0
/* nTime */ 1603359686,
@ -431,6 +438,17 @@ public:
}
};
m_assumeutxo_data = MapAssumeutxo{
{
110,
{uint256S("0x76fd7334ac7c1baf57ddc0c626f073a655a35d98a4258cd1382c8cc2b8392e10"), 110},
},
{
210,
{uint256S("0x9c5ed99ef98544b34f8920b6d1802f72ac28ae6e2bd2bd4c316ff10c230df3f2"), 210},
},
};
chainTxData = ChainTxData{
0,
0,
@ -526,3 +544,9 @@ void SelectParams(const std::string& network)
SelectBaseParams(network);
globalChainParams = CreateChainParams(gArgs, network);
}
std::ostream& operator<<(std::ostream& o, const AssumeutxoData& aud)
{
o << strprintf("AssumeutxoData(%s, %s)", aud.hash_serialized.ToString(), aud.nChainTx);
return o;
}

View file

@ -30,6 +30,26 @@ struct CCheckpointData {
}
};
/**
* Holds configuration for use during UTXO snapshot load and validation. The contents
* here are security critical, since they dictate which UTXO snapshots are recognized
* as valid.
*/
struct AssumeutxoData {
//! The expected hash of the deserialized UTXO set.
const uint256 hash_serialized;
//! Used to populate the nChainTx value, which is used during BlockManager::LoadBlockIndex().
//!
//! We need to hardcode the value here because this is computed cumulatively using block data,
//! which we do not necessarily have at the time of snapshot load.
const unsigned int nChainTx;
};
std::ostream& operator<<(std::ostream& o, const AssumeutxoData& aud);
using MapAssumeutxo = std::map<int, const AssumeutxoData>;
/**
* Holds various statistics on transactions within a chain. Used to estimate
* verification progress during chain sync.
@ -90,6 +110,11 @@ public:
const std::string& Bech32HRP() const { return bech32_hrp; }
const std::vector<SeedSpec6>& FixedSeeds() const { return vFixedSeeds; }
const CCheckpointData& Checkpoints() const { return checkpointData; }
//! Get allowed assumeutxo configuration.
//! @see ChainstateManager
const MapAssumeutxo& Assumeutxo() const { return m_assumeutxo_data; }
const ChainTxData& TxData() const { return chainTxData; }
protected:
CChainParams() {}
@ -111,6 +136,7 @@ protected:
bool m_is_test_chain;
bool m_is_mockable_chain;
CCheckpointData checkpointData;
MapAssumeutxo m_assumeutxo_data;
ChainTxData chainTxData;
};

View file

@ -97,6 +97,14 @@ void CCoinsViewCache::AddCoin(const COutPoint &outpoint, Coin&& coin, bool possi
cachedCoinsUsage += it->second.coin.DynamicMemoryUsage();
}
void CCoinsViewCache::EmplaceCoinInternalDANGER(COutPoint&& outpoint, Coin&& coin) {
cachedCoinsUsage += coin.DynamicMemoryUsage();
cacheCoins.emplace(
std::piecewise_construct,
std::forward_as_tuple(std::move(outpoint)),
std::forward_as_tuple(std::move(coin), CCoinsCacheEntry::DIRTY));
}
void AddCoins(CCoinsViewCache& cache, const CTransaction &tx, int nHeight, bool check_for_overwrite) {
bool fCoinbase = tx.IsCoinBase();
const uint256& txid = tx.GetHash();

View file

@ -20,6 +20,8 @@
#include <functional>
#include <unordered_map>
class ChainstateManager;
/**
* A UTXO entry.
*
@ -125,6 +127,7 @@ struct CCoinsCacheEntry
CCoinsCacheEntry() : flags(0) {}
explicit CCoinsCacheEntry(Coin&& coin_) : coin(std::move(coin_)), flags(0) {}
CCoinsCacheEntry(Coin&& coin_, unsigned char flag) : coin(std::move(coin_)), flags(flag) {}
};
typedef std::unordered_map<COutPoint, CCoinsCacheEntry, SaltedOutpointHasher> CCoinsMap;
@ -262,6 +265,15 @@ public:
*/
void AddCoin(const COutPoint& outpoint, Coin&& coin, bool possible_overwrite);
/**
* Emplace a coin into cacheCoins without performing any checks, marking
* the emplaced coin as dirty.
*
* NOT FOR GENERAL USE. Used only when loading coins from a UTXO snapshot.
* @sa ChainstateManager::PopulateAndValidateSnapshot()
*/
void EmplaceCoinInternalDANGER(COutPoint&& outpoint, Coin&& coin);
/**
* Spend a coin. Pass moveto in order to get the deleted data.
* If no unspent output exists for the passed outpoint, this call

View file

@ -55,6 +55,18 @@ static void ApplyHash(CCoinsStats& stats, MuHash3072& muhash, const uint256& has
muhash.Insert(MakeUCharSpan(ss));
}
//! Warning: be very careful when changing this! assumeutxo and UTXO snapshot
//! validation commitments are reliant on the hash constructed by this
//! function.
//!
//! If the construction of this hash is changed, it will invalidate
//! existing UTXO snapshots. This will not result in any kind of consensus
//! failure, but it will force clients that were expecting to make use of
//! assumeutxo to do traditional IBD instead.
//!
//! It is also possible, though very unlikely, that a change in this
//! construction could cause a previously invalid (and potentially malicious)
//! UTXO snapshot to be considered valid.
template <typename T>
static void ApplyStats(CCoinsStats& stats, T& hash_obj, const uint256& hash, const std::map<uint32_t, Coin>& outputs)
{

View file

@ -2411,10 +2411,21 @@ static RPCHelpMan dumptxoutset()
FILE* file{fsbridge::fopen(temppath, "wb")};
CAutoFile afile{file, SER_DISK, CLIENT_VERSION};
NodeContext& node = EnsureNodeContext(request.context);
UniValue result = CreateUTXOSnapshot(node, node.chainman->ActiveChainstate(), afile);
fs::rename(temppath, path);
result.pushKV("path", path.string());
return result;
},
};
}
UniValue CreateUTXOSnapshot(NodeContext& node, CChainState& chainstate, CAutoFile& afile)
{
std::unique_ptr<CCoinsViewCursor> pcursor;
CCoinsStats stats;
CBlockIndex* tip;
NodeContext& node = EnsureNodeContext(request.context);
{
// We need to lock cs_main to ensure that the coinsdb isn't written to
@ -2431,13 +2442,13 @@ static RPCHelpMan dumptxoutset()
//
LOCK(::cs_main);
::ChainstateActive().ForceFlushStateToDisk();
chainstate.ForceFlushStateToDisk();
if (!GetUTXOStats(&::ChainstateActive().CoinsDB(), stats, CoinStatsHashType::NONE, node.rpc_interruption_point)) {
if (!GetUTXOStats(&chainstate.CoinsDB(), stats, CoinStatsHashType::NONE, node.rpc_interruption_point)) {
throw JSONRPCError(RPC_INTERNAL_ERROR, "Unable to read UTXO set");
}
pcursor = std::unique_ptr<CCoinsViewCursor>(::ChainstateActive().CoinsDB().Cursor());
pcursor = std::unique_ptr<CCoinsViewCursor>(chainstate.CoinsDB().Cursor());
tip = g_chainman.m_blockman.LookupBlockIndex(stats.hashBlock);
CHECK_NONFATAL(tip);
}
@ -2462,16 +2473,13 @@ static RPCHelpMan dumptxoutset()
}
afile.fclose();
fs::rename(temppath, path);
UniValue result(UniValue::VOBJ);
result.pushKV("coins_written", stats.coins_count);
result.pushKV("base_hash", tip->GetBlockHash().ToString());
result.pushKV("base_height", tip->nHeight);
result.pushKV("path", path.string());
return result;
},
};
}
void RegisterBlockchainRPCCommands(CRPCTable &t)

View file

@ -6,6 +6,7 @@
#define BITCOIN_RPC_BLOCKCHAIN_H
#include <amount.h>
#include <streams.h>
#include <sync.h>
#include <stdint.h>
@ -16,6 +17,7 @@ extern RecursiveMutex cs_main;
class CBlock;
class CBlockIndex;
class CBlockPolicyEstimator;
class CChainState;
class CTxMemPool;
class ChainstateManager;
class UniValue;
@ -57,4 +59,10 @@ CTxMemPool& EnsureMemPool(const util::Ref& context);
ChainstateManager& EnsureChainman(const util::Ref& context);
CBlockPolicyEstimator& EnsureFeeEstimator(const util::Ref& context);
/**
* Helper to create UTXO snapshots given a chainstate and a file handle.
* @return a UniValue map containing metadata about the snapshot.
*/
UniValue CreateUTXOSnapshot(NodeContext& node, CChainState& chainstate, CAutoFile& afile);
#endif

View file

@ -199,14 +199,43 @@ TestingSetup::TestingSetup(const std::string& chainName, const std::vector<const
}
}
TestChain100Setup::TestChain100Setup()
TestChain100Setup::TestChain100Setup(bool deterministic)
{
m_deterministic = deterministic;
if (m_deterministic) {
SetMockTime(1598887952);
constexpr std::array<unsigned char, 32> vchKey = {
{
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1
}
};
coinbaseKey.Set(vchKey.begin(), vchKey.end(), false);
} else {
coinbaseKey.MakeNewKey(true);
}
// Generate a 100-block chain:
coinbaseKey.MakeNewKey(true);
this->mineBlocks(COINBASE_MATURITY);
if (m_deterministic) {
LOCK(::cs_main);
assert(
m_node.chainman->ActiveChain().Tip()->GetBlockHash().ToString() ==
"49c95db1e470fed04496d801c9d8fbb78155d2c7f855232c918823d2c17d0cf6");
}
}
void TestChain100Setup::mineBlocks(int num_blocks)
{
CScript scriptPubKey = CScript() << ToByteVector(coinbaseKey.GetPubKey()) << OP_CHECKSIG;
for (int i = 0; i < COINBASE_MATURITY; i++) {
for (int i = 0; i < num_blocks; i++)
{
std::vector<CMutableTransaction> noTxns;
CBlock b = CreateAndProcessBlock(noTxns, scriptPubKey);
if (m_deterministic) {
SetMockTime(GetTime() + 1);
}
m_coinbase_txns.push_back(b.vtx[0]);
}
}
@ -234,6 +263,9 @@ CBlock TestChain100Setup::CreateAndProcessBlock(const std::vector<CMutableTransa
TestChain100Setup::~TestChain100Setup()
{
gArgs.ForceSetArg("-segwitheight", "0");
if (m_deterministic) {
SetMockTime(0);
}
}
CTxMemPoolEntry TestMemPoolEntryHelper::FromTx(const CMutableTransaction& tx) const

View file

@ -78,7 +78,6 @@ struct BasicTestingSetup {
explicit BasicTestingSetup(const std::string& chainName = CBaseChainParams::MAIN, const std::vector<const char*>& extra_args = {});
~BasicTestingSetup();
private:
const fs::path m_path_root;
};
@ -112,7 +111,7 @@ class CScript;
* Testing fixture that pre-creates a 100-block REGTEST-mode block chain
*/
struct TestChain100Setup : public RegTestingSetup {
TestChain100Setup();
TestChain100Setup(bool deterministic = false);
/**
* Create a new block with just given transactions, coinbase paying to
@ -121,12 +120,21 @@ struct TestChain100Setup : public RegTestingSetup {
CBlock CreateAndProcessBlock(const std::vector<CMutableTransaction>& txns,
const CScript& scriptPubKey);
//! Mine a series of new blocks on the active chain.
void mineBlocks(int num_blocks);
~TestChain100Setup();
bool m_deterministic;
std::vector<CTransactionRef> m_coinbase_txns; // For convenience, coinbase transactions
CKey coinbaseKey; // private/public key needed to spend coinbase transactions
};
struct TestChain100DeterministicSetup : public TestChain100Setup {
TestChain100DeterministicSetup() : TestChain100Setup(true) { }
};
class CTxMemPoolEntry;
struct TestMemPoolEntryHelper

View file

@ -4,13 +4,18 @@
//
#include <chainparams.h>
#include <consensus/validation.h>
#include <node/utxo_snapshot.h>
#include <random.h>
#include <rpc/blockchain.h>
#include <sync.h>
#include <test/util/setup_common.h>
#include <uint256.h>
#include <validation.h>
#include <validationinterface.h>
#include <tinyformat.h>
#include <univalue.h>
#include <vector>
#include <boost/test/unit_test.hpp>
@ -28,6 +33,8 @@ BOOST_AUTO_TEST_CASE(chainstatemanager)
std::vector<CChainState*> chainstates;
const CChainParams& chainparams = Params();
BOOST_CHECK(!manager.SnapshotBlockhash().has_value());
// Create a legacy (IBD) chainstate.
//
CChainState& c1 = WITH_LOCK(::cs_main, return manager.InitializeChainstate(mempool));
@ -54,10 +61,17 @@ BOOST_AUTO_TEST_CASE(chainstatemanager)
auto& validated_cs = manager.ValidatedChainstate();
BOOST_CHECK_EQUAL(&validated_cs, &c1);
BOOST_CHECK(!manager.SnapshotBlockhash().has_value());
// Create a snapshot-based chainstate.
//
CChainState& c2 = WITH_LOCK(::cs_main, return manager.InitializeChainstate(mempool, GetRandHash()));
const uint256 snapshot_blockhash = GetRandHash();
CChainState& c2 = WITH_LOCK(::cs_main, return manager.InitializeChainstate(
mempool, snapshot_blockhash));
chainstates.push_back(&c2);
BOOST_CHECK_EQUAL(manager.SnapshotBlockhash().value(), snapshot_blockhash);
c2.InitCoinsDB(
/* cache_size_bytes */ 1 << 23, /* in_memory */ true, /* should_wipe */ false);
WITH_LOCK(::cs_main, c2.InitCoinsCache(1 << 23));
@ -155,4 +169,175 @@ BOOST_AUTO_TEST_CASE(chainstatemanager_rebalance_caches)
BOOST_CHECK_CLOSE(c2.m_coinsdb_cache_size_bytes, max_cache * 0.95, 1);
}
auto NoMalleation = [](CAutoFile& file, SnapshotMetadata& meta){};
template<typename F = decltype(NoMalleation)>
static bool
CreateAndActivateUTXOSnapshot(NodeContext& node, const fs::path root, F malleation = NoMalleation)
{
// Write out a snapshot to the test's tempdir.
//
int height;
WITH_LOCK(::cs_main, height = node.chainman->ActiveHeight());
fs::path snapshot_path = root / tfm::format("test_snapshot.%d.dat", height);
FILE* outfile{fsbridge::fopen(snapshot_path, "wb")};
CAutoFile auto_outfile{outfile, SER_DISK, CLIENT_VERSION};
UniValue result = CreateUTXOSnapshot(node, node.chainman->ActiveChainstate(), auto_outfile);
BOOST_TEST_MESSAGE(
"Wrote UTXO snapshot to " << snapshot_path.make_preferred().string() << ": " << result.write());
// Read the written snapshot in and then activate it.
//
FILE* infile{fsbridge::fopen(snapshot_path, "rb")};
CAutoFile auto_infile{infile, SER_DISK, CLIENT_VERSION};
SnapshotMetadata metadata;
auto_infile >> metadata;
malleation(auto_infile, metadata);
return node.chainman->ActivateSnapshot(auto_infile, metadata, /*in_memory*/ true);
}
//! Test basic snapshot activation.
BOOST_FIXTURE_TEST_CASE(chainstatemanager_activate_snapshot, TestChain100DeterministicSetup)
{
ChainstateManager& chainman = *Assert(m_node.chainman);
size_t initial_size;
size_t initial_total_coins{100};
// Make some initial assertions about the contents of the chainstate.
{
LOCK(::cs_main);
CCoinsViewCache& ibd_coinscache = chainman.ActiveChainstate().CoinsTip();
initial_size = ibd_coinscache.GetCacheSize();
size_t total_coins{0};
for (CTransactionRef& txn : m_coinbase_txns) {
COutPoint op{txn->GetHash(), 0};
BOOST_CHECK(ibd_coinscache.HaveCoin(op));
total_coins++;
}
BOOST_CHECK_EQUAL(total_coins, initial_total_coins);
BOOST_CHECK_EQUAL(initial_size, initial_total_coins);
}
// Snapshot should refuse to load at this height.
BOOST_REQUIRE(!CreateAndActivateUTXOSnapshot(m_node, m_path_root));
BOOST_CHECK(chainman.ActiveChainstate().m_from_snapshot_blockhash.IsNull());
BOOST_CHECK_EQUAL(
chainman.ActiveChainstate().m_from_snapshot_blockhash,
chainman.SnapshotBlockhash().value_or(uint256()));
// Mine 10 more blocks, putting at us height 110 where a valid assumeutxo value can
// be found.
mineBlocks(10);
initial_size += 10;
initial_total_coins += 10;
// Should not load malleated snapshots
BOOST_REQUIRE(!CreateAndActivateUTXOSnapshot(
m_node, m_path_root, [](CAutoFile& auto_infile, SnapshotMetadata& metadata) {
// A UTXO is missing but count is correct
metadata.m_coins_count -= 1;
COutPoint outpoint;
Coin coin;
auto_infile >> outpoint;
auto_infile >> coin;
}));
BOOST_REQUIRE(!CreateAndActivateUTXOSnapshot(
m_node, m_path_root, [](CAutoFile& auto_infile, SnapshotMetadata& metadata) {
// Coins count is larger than coins in file
metadata.m_coins_count += 1;
}));
BOOST_REQUIRE(!CreateAndActivateUTXOSnapshot(
m_node, m_path_root, [](CAutoFile& auto_infile, SnapshotMetadata& metadata) {
// Coins count is smaller than coins in file
metadata.m_coins_count -= 1;
}));
BOOST_REQUIRE(CreateAndActivateUTXOSnapshot(m_node, m_path_root));
// Ensure our active chain is the snapshot chainstate.
BOOST_CHECK(!chainman.ActiveChainstate().m_from_snapshot_blockhash.IsNull());
BOOST_CHECK_EQUAL(
chainman.ActiveChainstate().m_from_snapshot_blockhash,
*chainman.SnapshotBlockhash());
// To be checked against later when we try loading a subsequent snapshot.
uint256 loaded_snapshot_blockhash{*chainman.SnapshotBlockhash()};
// Make some assertions about the both chainstates. These checks ensure the
// legacy chainstate hasn't changed and that the newly created chainstate
// reflects the expected content.
{
LOCK(::cs_main);
int chains_tested{0};
for (CChainState* chainstate : chainman.GetAll()) {
BOOST_TEST_MESSAGE("Checking coins in " << chainstate->ToString());
CCoinsViewCache& coinscache = chainstate->CoinsTip();
// Both caches will be empty initially.
BOOST_CHECK_EQUAL((unsigned int)0, coinscache.GetCacheSize());
size_t total_coins{0};
for (CTransactionRef& txn : m_coinbase_txns) {
COutPoint op{txn->GetHash(), 0};
BOOST_CHECK(coinscache.HaveCoin(op));
total_coins++;
}
BOOST_CHECK_EQUAL(initial_size , coinscache.GetCacheSize());
BOOST_CHECK_EQUAL(total_coins, initial_total_coins);
chains_tested++;
}
BOOST_CHECK_EQUAL(chains_tested, 2);
}
// Mine some new blocks on top of the activated snapshot chainstate.
constexpr size_t new_coins{100};
mineBlocks(new_coins); // Defined in TestChain100Setup.
{
LOCK(::cs_main);
size_t coins_in_active{0};
size_t coins_in_ibd{0};
size_t coins_missing_ibd{0};
for (CChainState* chainstate : chainman.GetAll()) {
BOOST_TEST_MESSAGE("Checking coins in " << chainstate->ToString());
CCoinsViewCache& coinscache = chainstate->CoinsTip();
bool is_ibd = chainman.IsBackgroundIBD(chainstate);
for (CTransactionRef& txn : m_coinbase_txns) {
COutPoint op{txn->GetHash(), 0};
if (coinscache.HaveCoin(op)) {
(is_ibd ? coins_in_ibd : coins_in_active)++;
} else if (is_ibd) {
coins_missing_ibd++;
}
}
}
BOOST_CHECK_EQUAL(coins_in_active, initial_total_coins + new_coins);
BOOST_CHECK_EQUAL(coins_in_ibd, initial_total_coins);
BOOST_CHECK_EQUAL(coins_missing_ibd, new_coins);
}
// Snapshot should refuse to load after one has already loaded.
BOOST_REQUIRE(!CreateAndActivateUTXOSnapshot(m_node, m_path_root));
// Snapshot blockhash should be unchanged.
BOOST_CHECK_EQUAL(
chainman.ActiveChainstate().m_from_snapshot_blockhash,
loaded_snapshot_blockhash);
}
BOOST_AUTO_TEST_SUITE_END()

View file

@ -5,6 +5,7 @@
#include <chainparams.h>
#include <net.h>
#include <signet.h>
#include <uint256.h>
#include <validation.h>
#include <test/util/setup_common.h>
@ -119,4 +120,27 @@ BOOST_AUTO_TEST_CASE(signet_parse_tests)
BOOST_CHECK(!CheckSignetBlockSolution(block, signet_params->GetConsensus()));
}
//! Test retrieval of valid assumeutxo values.
BOOST_AUTO_TEST_CASE(test_assumeutxo)
{
const auto params = CreateChainParams(*m_node.args, CBaseChainParams::REGTEST);
// These heights don't have assumeutxo configurations associated, per the contents
// of chainparams.cpp.
std::vector<int> bad_heights{0, 100, 111, 115, 209, 211};
for (auto empty : bad_heights) {
const auto out = ExpectedAssumeutxo(empty, *params);
BOOST_CHECK(!out);
}
const auto out110 = *ExpectedAssumeutxo(110, *params);
BOOST_CHECK_EQUAL(out110.hash_serialized, uint256S("76fd7334ac7c1baf57ddc0c626f073a655a35d98a4258cd1382c8cc2b8392e10"));
BOOST_CHECK_EQUAL(out110.nChainTx, (unsigned int)110);
const auto out210 = *ExpectedAssumeutxo(210, *params);
BOOST_CHECK_EQUAL(out210.hash_serialized, uint256S("9c5ed99ef98544b34f8920b6d1802f72ac28ae6e2bd2bd4c316ff10c230df3f2"));
BOOST_CHECK_EQUAL(out210.nChainTx, (unsigned int)210);
}
BOOST_AUTO_TEST_SUITE_END()

View file

@ -47,11 +47,15 @@ CCoinsViewDB::CCoinsViewDB(fs::path ldb_path, size_t nCacheSize, bool fMemory, b
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);
// We can't do this operation with an in-memory DB since we'll lose all the coins upon
// reset.
if (!m_is_memory) {
// 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 {

View file

@ -20,6 +20,7 @@
#include <index/txindex.h>
#include <logging.h>
#include <logging/timer.h>
#include <node/coinstats.h>
#include <node/ui_interface.h>
#include <optional.h>
#include <policy/policy.h>
@ -5147,7 +5148,8 @@ double GuessVerificationProgress(const ChainTxData& data, const CBlockIndex *pin
Optional<uint256> ChainstateManager::SnapshotBlockhash() const {
LOCK(::cs_main);
if (m_active_chainstate != nullptr) {
if (m_active_chainstate != nullptr &&
!m_active_chainstate->m_from_snapshot_blockhash.IsNull()) {
// If a snapshot chainstate exists, it will always be our active.
return m_active_chainstate->m_from_snapshot_blockhash;
}
@ -5192,6 +5194,295 @@ CChainState& ChainstateManager::InitializeChainstate(CTxMemPool& mempool, const
return *to_modify;
}
const AssumeutxoData* ExpectedAssumeutxo(
const int height, const CChainParams& chainparams)
{
const MapAssumeutxo& valid_assumeutxos_map = chainparams.Assumeutxo();
const auto assumeutxo_found = valid_assumeutxos_map.find(height);
if (assumeutxo_found != valid_assumeutxos_map.end()) {
return &assumeutxo_found->second;
}
return nullptr;
}
bool ChainstateManager::ActivateSnapshot(
CAutoFile& coins_file,
const SnapshotMetadata& metadata,
bool in_memory)
{
uint256 base_blockhash = metadata.m_base_blockhash;
if (this->SnapshotBlockhash()) {
LogPrintf("[snapshot] can't activate a snapshot-based chainstate more than once\n");
return false;
}
int64_t current_coinsdb_cache_size{0};
int64_t current_coinstip_cache_size{0};
// Cache percentages to allocate to each chainstate.
//
// These particular percentages don't matter so much since they will only be
// relevant during snapshot activation; caches are rebalanced at the conclusion of
// this function. We want to give (essentially) all available cache capacity to the
// snapshot to aid the bulk load later in this function.
static constexpr double IBD_CACHE_PERC = 0.01;
static constexpr double SNAPSHOT_CACHE_PERC = 0.99;
{
LOCK(::cs_main);
// Resize the coins caches to ensure we're not exceeding memory limits.
//
// Allocate the majority of the cache to the incoming snapshot chainstate, since
// (optimistically) getting to its tip will be the top priority. We'll need to call
// `MaybeRebalanceCaches()` once we're done with this function to ensure
// the right allocation (including the possibility that no snapshot was activated
// and that we should restore the active chainstate caches to their original size).
//
current_coinsdb_cache_size = this->ActiveChainstate().m_coinsdb_cache_size_bytes;
current_coinstip_cache_size = this->ActiveChainstate().m_coinstip_cache_size_bytes;
// Temporarily resize the active coins cache to make room for the newly-created
// snapshot chain.
this->ActiveChainstate().ResizeCoinsCaches(
static_cast<size_t>(current_coinstip_cache_size * IBD_CACHE_PERC),
static_cast<size_t>(current_coinsdb_cache_size * IBD_CACHE_PERC));
}
auto snapshot_chainstate = WITH_LOCK(::cs_main, return MakeUnique<CChainState>(
this->ActiveChainstate().m_mempool, m_blockman, base_blockhash));
{
LOCK(::cs_main);
snapshot_chainstate->InitCoinsDB(
static_cast<size_t>(current_coinsdb_cache_size * SNAPSHOT_CACHE_PERC),
in_memory, false, "chainstate");
snapshot_chainstate->InitCoinsCache(
static_cast<size_t>(current_coinstip_cache_size * SNAPSHOT_CACHE_PERC));
}
const bool snapshot_ok = this->PopulateAndValidateSnapshot(
*snapshot_chainstate, coins_file, metadata);
if (!snapshot_ok) {
WITH_LOCK(::cs_main, this->MaybeRebalanceCaches());
return false;
}
{
LOCK(::cs_main);
assert(!m_snapshot_chainstate);
m_snapshot_chainstate.swap(snapshot_chainstate);
const bool chaintip_loaded = m_snapshot_chainstate->LoadChainTip(::Params());
assert(chaintip_loaded);
m_active_chainstate = m_snapshot_chainstate.get();
LogPrintf("[snapshot] successfully activated snapshot %s\n", base_blockhash.ToString());
LogPrintf("[snapshot] (%.2f MB)\n",
m_snapshot_chainstate->CoinsTip().DynamicMemoryUsage() / (1000 * 1000));
this->MaybeRebalanceCaches();
}
return true;
}
bool ChainstateManager::PopulateAndValidateSnapshot(
CChainState& snapshot_chainstate,
CAutoFile& coins_file,
const SnapshotMetadata& metadata)
{
// It's okay to release cs_main before we're done using `coins_cache` because we know
// that nothing else will be referencing the newly created snapshot_chainstate yet.
CCoinsViewCache& coins_cache = *WITH_LOCK(::cs_main, return &snapshot_chainstate.CoinsTip());
uint256 base_blockhash = metadata.m_base_blockhash;
COutPoint outpoint;
Coin coin;
const uint64_t coins_count = metadata.m_coins_count;
uint64_t coins_left = metadata.m_coins_count;
LogPrintf("[snapshot] loading coins from snapshot %s\n", base_blockhash.ToString());
int64_t flush_now{0};
int64_t coins_processed{0};
while (coins_left > 0) {
try {
coins_file >> outpoint;
} catch (const std::ios_base::failure&) {
LogPrintf("[snapshot] bad snapshot - no coins left after deserializing %d coins\n",
coins_count - coins_left);
return false;
}
coins_file >> coin;
coins_cache.EmplaceCoinInternalDANGER(std::move(outpoint), std::move(coin));
--coins_left;
++coins_processed;
if (coins_processed % 1000000 == 0) {
LogPrintf("[snapshot] %d coins loaded (%.2f%%, %.2f MB)\n",
coins_processed,
static_cast<float>(coins_processed) * 100 / static_cast<float>(coins_count),
coins_cache.DynamicMemoryUsage() / (1000 * 1000));
}
// Batch write and flush (if we need to) every so often.
//
// If our average Coin size is roughly 41 bytes, checking every 120,000 coins
// means <5MB of memory imprecision.
if (coins_processed % 120000 == 0) {
if (ShutdownRequested()) {
return false;
}
const auto snapshot_cache_state = WITH_LOCK(::cs_main,
return snapshot_chainstate.GetCoinsCacheSizeState(&snapshot_chainstate.m_mempool));
if (snapshot_cache_state >=
CoinsCacheSizeState::CRITICAL) {
LogPrintf("[snapshot] flushing coins cache (%.2f MB)... ", /* Continued */
coins_cache.DynamicMemoryUsage() / (1000 * 1000));
flush_now = GetTimeMillis();
// This is a hack - we don't know what the actual best block is, but that
// doesn't matter for the purposes of flushing the cache here. We'll set this
// to its correct value (`base_blockhash`) below after the coins are loaded.
coins_cache.SetBestBlock(GetRandHash());
coins_cache.Flush();
LogPrintf("done (%.2fms)\n", GetTimeMillis() - flush_now);
}
}
}
// Important that we set this. This and the coins_cache accesses above are
// sort of a layer violation, but either we reach into the innards of
// CCoinsViewCache here or we have to invert some of the CChainState to
// embed them in a snapshot-activation-specific CCoinsViewCache bulk load
// method.
coins_cache.SetBestBlock(base_blockhash);
bool out_of_coins{false};
try {
coins_file >> outpoint;
} catch (const std::ios_base::failure&) {
// We expect an exception since we should be out of coins.
out_of_coins = true;
}
if (!out_of_coins) {
LogPrintf("[snapshot] bad snapshot - coins left over after deserializing %d coins\n",
coins_count);
return false;
}
LogPrintf("[snapshot] loaded %d (%.2f MB) coins from snapshot %s\n",
coins_count,
coins_cache.DynamicMemoryUsage() / (1000 * 1000),
base_blockhash.ToString());
LogPrintf("[snapshot] flushing snapshot chainstate to disk\n");
// No need to acquire cs_main since this chainstate isn't being used yet.
coins_cache.Flush(); // TODO: if #17487 is merged, add erase=false here for better performance.
assert(coins_cache.GetBestBlock() == base_blockhash);
CCoinsStats stats;
auto breakpoint_fnc = [] { /* TODO insert breakpoint here? */ };
// As above, okay to immediately release cs_main here since no other context knows
// about the snapshot_chainstate.
CCoinsViewDB* snapshot_coinsdb = WITH_LOCK(::cs_main, return &snapshot_chainstate.CoinsDB());
if (!GetUTXOStats(snapshot_coinsdb, stats, CoinStatsHashType::HASH_SERIALIZED, breakpoint_fnc)) {
LogPrintf("[snapshot] failed to generate coins stats\n");
return false;
}
// Ensure that the base blockhash appears in the known chain of valid headers. We're willing to
// wait a bit here because the snapshot may have been loaded on startup, before we've
// received headers from the network.
int max_secs_to_wait_for_headers = 60 * 10;
CBlockIndex* snapshot_start_block = nullptr;
while (max_secs_to_wait_for_headers > 0) {
snapshot_start_block = WITH_LOCK(::cs_main,
return m_blockman.LookupBlockIndex(base_blockhash));
--max_secs_to_wait_for_headers;
if (!snapshot_start_block) {
std::this_thread::sleep_for(std::chrono::seconds(1));
} else {
break;
}
}
if (snapshot_start_block == nullptr) {
LogPrintf("[snapshot] timed out waiting for snapshot start blockheader %s\n",
base_blockhash.ToString());
return false;
}
// Assert that the deserialized chainstate contents match the expected assumeutxo value.
int base_height = snapshot_start_block->nHeight;
auto maybe_au_data = ExpectedAssumeutxo(base_height, ::Params());
if (!maybe_au_data) {
LogPrintf("[snapshot] assumeutxo height in snapshot metadata not recognized " /* Continued */
"(%d) - refusing to load snapshot\n", base_height);
return false;
}
const AssumeutxoData& au_data = *maybe_au_data;
if (stats.hashSerialized != au_data.hash_serialized) {
LogPrintf("[snapshot] bad snapshot content hash: expected %s, got %s\n",
au_data.hash_serialized.ToString(), stats.hashSerialized.ToString());
return false;
}
snapshot_chainstate.m_chain.SetTip(snapshot_start_block);
// The remainder of this function requires modifying data protected by cs_main.
LOCK(::cs_main);
// Fake various pieces of CBlockIndex state:
//
// - nChainTx: so that we accurately report IBD-to-tip progress
// - nTx: so that LoadBlockIndex() loads assumed-valid CBlockIndex entries
// (among other things)
// - nStatus & BLOCK_OPT_WITNESS: so that RewindBlockIndex() doesn't zealously
// unwind the assumed-valid chain.
//
CBlockIndex* index = nullptr;
for (int i = 0; i <= snapshot_chainstate.m_chain.Height(); ++i) {
index = snapshot_chainstate.m_chain[i];
if (!index->nTx) {
index->nTx = 1;
}
index->nChainTx = index->pprev ? index->pprev->nChainTx + index->nTx : 1;
// We need to fake this flag so that CChainState::RewindBlockIndex()
// won't try to rewind the entire assumed-valid chain on startup.
if (index->pprev && ::IsWitnessEnabled(index->pprev, ::Params().GetConsensus())) {
index->nStatus |= BLOCK_OPT_WITNESS;
}
}
assert(index);
index->nChainTx = metadata.m_nchaintx;
snapshot_chainstate.setBlockIndexCandidates.insert(snapshot_start_block);
LogPrintf("[snapshot] validated snapshot (%.2f MB)\n",
coins_cache.DynamicMemoryUsage() / (1000 * 1000));
return true;
}
CChainState& ChainstateManager::ActiveChainstate() const
{
LOCK(::cs_main);

View file

@ -11,10 +11,12 @@
#endif
#include <amount.h>
#include <attributes.h>
#include <coins.h>
#include <consensus/validation.h>
#include <crypto/common.h> // for ReadLE64
#include <fs.h>
#include <node/utxo_snapshot.h>
#include <optional.h>
#include <policy/feerate.h>
#include <protocol.h> // For CMessageHeader::MessageStartChars
@ -53,6 +55,7 @@ struct ChainTxData;
struct DisconnectedBlockTransactions;
struct PrecomputedTransactionData;
struct LockPoints;
struct AssumeutxoData;
/** Default for -minrelaytxfee, minimum relay fee for transactions */
static const unsigned int DEFAULT_MIN_RELAY_TX_FEE = 1000;
@ -830,9 +833,7 @@ private:
//! using this pointer (e.g. net_processing).
//!
//! Once this pointer is set to a corresponding chainstate, it will not
//! be reset until init.cpp:Shutdown(). This means it is safe to acquire
//! the contents of this pointer with ::cs_main held, release the lock,
//! and then use the reference without concern of it being deconstructed.
//! be reset until init.cpp:Shutdown().
//!
//! This is especially important when, e.g., calling ActivateBestChain()
//! on all chainstates because we are not able to hold ::cs_main going into
@ -843,9 +844,7 @@ private:
//! non-null, it is always our active chainstate.
//!
//! Once this pointer is set to a corresponding chainstate, it will not
//! be reset until init.cpp:Shutdown(). This means it is safe to acquire
//! the contents of this pointer with ::cs_main held, release the lock,
//! and then use the reference without concern of it being deconstructed.
//! be reset until init.cpp:Shutdown().
//!
//! This is especially important when, e.g., calling ActivateBestChain()
//! on all chainstates because we are not able to hold ::cs_main going into
@ -856,9 +855,7 @@ private:
//! most-work chain.
//!
//! Once this pointer is set to a corresponding chainstate, it will not
//! be reset until init.cpp:Shutdown(). This means it is safe to acquire
//! the contents of this pointer with ::cs_main held, release the lock,
//! and then use the reference without concern of it being deconstructed.
//! be reset until init.cpp:Shutdown().
//!
//! This is especially important when, e.g., calling ActivateBestChain()
//! on all chainstates because we are not able to hold ::cs_main going into
@ -869,6 +866,12 @@ private:
//! by the background validation chainstate.
bool m_snapshot_validated{false};
//! Internal helper for ActivateSnapshot().
[[nodiscard]] bool PopulateAndValidateSnapshot(
CChainState& snapshot_chainstate,
CAutoFile& coins_file,
const SnapshotMetadata& metadata);
// For access to m_active_chainstate.
friend CChainState& ChainstateActive();
friend CChain& ChainActive();
@ -899,6 +902,22 @@ public:
//! Get all chainstates currently being used.
std::vector<CChainState*> GetAll();
//! Construct and activate a Chainstate on the basis of UTXO snapshot data.
//!
//! Steps:
//!
//! - Initialize an unused CChainState.
//! - Load its `CoinsViews` contents from `coins_file`.
//! - Verify that the hash of the resulting coinsdb matches the expected hash
//! per assumeutxo chain parameters.
//! - Wait for our headers chain to include the base block of the snapshot.
//! - "Fast forward" the tip of the new chainstate to the base of the snapshot,
//! faking nTx* block index data along the way.
//! - Move the new chainstate to `m_snapshot_chainstate` and make it our
//! ChainstateActive().
[[nodiscard]] bool ActivateSnapshot(
CAutoFile& coins_file, const SnapshotMetadata& metadata, bool in_memory);
//! The most-work chain.
CChainState& ActiveChainstate() const;
CChain& ActiveChain() const { return ActiveChainstate().m_chain; }
@ -1013,4 +1032,13 @@ inline bool IsBlockPruned(const CBlockIndex* pblockindex)
return (fHavePruned && !(pblockindex->nStatus & BLOCK_HAVE_DATA) && pblockindex->nTx > 0);
}
/**
* Return the expected assumeutxo value for a given height, if one exists.
*
* @param height[in] Get the assumeutxo value for this height.
*
* @returns empty if no assumeutxo configuration exists for the given height.
*/
const AssumeutxoData* ExpectedAssumeutxo(const int height, const CChainParams& params);
#endif // BITCOIN_VALIDATION_H

View file

@ -20,6 +20,7 @@ EXPECTED_CIRCULAR_DEPENDENCIES=(
"txmempool -> validation -> txmempool"
"wallet/fees -> wallet/wallet -> wallet/fees"
"wallet/wallet -> wallet/walletdb -> wallet/wallet"
"node/coinstats -> validation -> node/coinstats"
)
EXIT_CODE=0

View file

@ -28,6 +28,7 @@ race:BerkeleyBatch
race:BerkeleyDatabase
race:DatabaseBatch
race:leveldb::DBImpl::DeleteObsoleteFiles
race:validation_chainstatemanager_tests
race:zmq::*
race:bitcoin-qt