Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AssumeUTXO follow-ups #28562

Merged
merged 10 commits into from
Oct 7, 2023
2 changes: 1 addition & 1 deletion doc/design/assumeutxo.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# assumeutxo

Assumeutxo is a feature that allows fast bootstrapping of a validating bitcoind
instance with a very similar security model to assumevalid.
instance.

The RPC commands `dumptxoutset` and `loadtxoutset` are used to
respectively generate and load UTXO snapshots. The utility script
Expand Down
2 changes: 1 addition & 1 deletion doc/release-notes-27596.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ RPC
`loadtxoutset` has been added, which allows loading a UTXO snapshot of the format
generated by `dumptxoutset`. Once this snapshot is loaded, its contents will be
deserialized into a second chainstate data structure, which is then used to sync to
the network's tip under a security model very much like `assumevalid`.
the network's tip.

Meanwhile, the original chainstate will complete the initial block download process in
the background, eventually validating up to the block that the snapshot is based upon.
Expand Down
4 changes: 1 addition & 3 deletions src/chain.h
Original file line number Diff line number Diff line change
Expand Up @@ -280,10 +280,8 @@ class CBlockIndex
* Note that this will be true for the snapshot base block, if one is loaded (and
* all subsequent assumed-valid blocks) since its nChainTx value will have been set
* manually based on the related AssumeutxoData entry.
*
* TODO: potentially change the name of this based on the fact above.
*/
bool HaveTxsDownloaded() const { return nChainTx != 0; }
bool HaveNumChainTxs() const { return nChainTx != 0; }

NodeSeconds Time() const
{
Expand Down
4 changes: 2 additions & 2 deletions src/init.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -462,8 +462,8 @@ void SetupServerArgs(ArgsManager& argsman)
argsman.AddArg("-prune=<n>", strprintf("Reduce storage requirements by enabling pruning (deleting) of old blocks. This allows the pruneblockchain RPC to be called to delete specific blocks and enables automatic pruning of old blocks if a target size in MiB is provided. This mode is incompatible with -txindex. "
"Warning: Reverting this setting requires re-downloading the entire blockchain. "
"(default: 0 = disable pruning blocks, 1 = allow manual pruning via RPC, >=%u = automatically prune block files to stay under the specified target size in MiB)", MIN_DISK_SPACE_FOR_BLOCK_FILES / 1024 / 1024), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg("-reindex", "Rebuild chain state and block index from the blk*.dat files on disk. This will also rebuild active optional indexes.", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg("-reindex-chainstate", "Rebuild chain state from the currently indexed blocks. When in pruning mode or if blocks on disk might be corrupted, use full -reindex instead.", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg("-reindex", "If enabled, wipe chain state and block index, and rebuild them from blk*.dat files on disk. Also wipe and rebuild other optional indexes that are active. If an assumeutxo snapshot was loaded, its chainstate will be wiped as well. The snapshot can then be reloaded via RPC.", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg("-reindex-chainstate", "If enabled, wipe chain state, and rebuild it from blk*.dat files on disk. If an assumeutxo snapshot was loaded, its chainstate will be wiped as well. The snapshot can then be reloaded via RPC.", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg("-settings=<file>", strprintf("Specify path to dynamic settings data file. Can be disabled with -nosettings. File is written at runtime and not meant to be edited by users (use %s instead for custom settings). Relative paths will be prefixed by datadir location. (default: %s)", BITCOIN_CONF_FILENAME, BITCOIN_SETTINGS_FILENAME), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
#if HAVE_SYSTEM
argsman.AddArg("-startupnotify=<cmd>", "Execute command on startup.", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
Expand Down
2 changes: 1 addition & 1 deletion src/kernel/chainparams.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -495,7 +495,7 @@ class CRegTestParams : public CChainParams
{
.height = 110,
.hash_serialized = AssumeutxoHash{uint256S("0x1ebbf5850204c0bdb15bf030f47c7fe91d45c44c712697e4509ba67adb01c618")},
.nChainTx = 110,
.nChainTx = 111,
.blockhash = uint256S("0x696e92821f65549c7ee134edceeeeaaa4105647a3c4fd9f298c0aec0ab50425c")
},
{
Expand Down
6 changes: 4 additions & 2 deletions src/net_processing.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1448,7 +1448,7 @@ void PeerManagerImpl::FindNextBlocks(std::vector<const CBlockIndex*>& vBlocks, c
return;
}
if (pindex->nStatus & BLOCK_HAVE_DATA || (activeChain && activeChain->Contains(pindex))) {
if (activeChain && pindex->HaveTxsDownloaded())
if (activeChain && pindex->HaveNumChainTxs())
state->pindexLastCommonBlock = pindex;
} else if (!IsBlockRequested(pindex->GetBlockHash())) {
// The block is not already downloaded, and not yet in flight.
Expand Down Expand Up @@ -1937,6 +1937,8 @@ void PeerManagerImpl::BlockConnected(
}
}

// The following task can be skipped since we don't maintain a mempool for
// the ibd/background chainstate.
if (role == ChainstateRole::BACKGROUND) {
return;
}
Expand Down Expand Up @@ -2231,7 +2233,7 @@ void PeerManagerImpl::ProcessGetBlockData(CNode& pfrom, Peer& peer, const CInv&
LOCK(cs_main);
const CBlockIndex* pindex = m_chainman.m_blockman.LookupBlockIndex(inv.hash);
if (pindex) {
if (pindex->HaveTxsDownloaded() && !pindex->IsValid(BLOCK_VALID_SCRIPTS) &&
if (pindex->HaveNumChainTxs() && !pindex->IsValid(BLOCK_VALID_SCRIPTS) &&
pindex->IsValid(BLOCK_VALID_TREE)) {
// If we have the block and all of its parents, but have not yet validated it,
// we might be in the middle of connecting it (ie in the unlock of cs_main
Expand Down
8 changes: 5 additions & 3 deletions src/node/blockstorage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -761,12 +761,14 @@ bool BlockManager::FlushChainstateBlockFile(int tip_height)
{
LOCK(cs_LastBlockFile);
auto& cursor = m_blockfile_cursors[BlockfileTypeForHeight(tip_height)];
// If the cursor does not exist, it means an assumeutxo snapshot is loaded,
// but no blocks past the snapshot height have been written yet, so there
// is no data associated with the chainstate, and it is safe not to flush.
if (cursor) {
// The cursor may not exist after a snapshot has been loaded but before any
// blocks have been downloaded.
return FlushBlockFile(cursor->file_num, /*fFinalize=*/false, /*finalize_undo=*/false);
}
return false;
// No need to log warnings in this case.
return true;
}

uint64_t BlockManager::CalculateCurrentUsage()
Expand Down
2 changes: 1 addition & 1 deletion src/node/chainstate.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ ChainstateLoadResult LoadChainstate(ChainstateManager& chainman, const CacheSize
chainman.InitializeChainstate(options.mempool);

// Load a chain created from a UTXO snapshot, if any exist.
bool has_snapshot = chainman.DetectSnapshotChainstate(options.mempool);
bool has_snapshot = chainman.DetectSnapshotChainstate();

if (has_snapshot && (options.reindex || options.reindex_chainstate)) {
LogPrintf("[snapshot] deleting snapshot chainstate due to reindexing\n");
Expand Down
9 changes: 4 additions & 5 deletions src/rpc/blockchain.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1455,7 +1455,7 @@ static RPCHelpMan getchaintips()
} else if (block->nStatus & BLOCK_FAILED_MASK) {
// This block or one of its ancestors is invalid.
status = "invalid";
} else if (!block->HaveTxsDownloaded()) {
} else if (!block->HaveNumChainTxs()) {
// This block cannot be connected because full block data for it or one of its parents is missing.
status = "headers-only";
} else if (block->IsValid(BLOCK_VALID_SCRIPTS)) {
Expand Down Expand Up @@ -2707,7 +2707,7 @@ static RPCHelpMan loadtxoutset()
"Load the serialized UTXO set from disk.\n"
"Once this snapshot is loaded, its contents will be "
"deserialized into a second chainstate data structure, which is then used to sync to "
"the network's tip under a security model very much like `assumevalid`. "
"the network's tip. "
"Meanwhile, the original chainstate will complete the initial block download process in "
"the background, eventually validating up to the block that the snapshot is based upon.\n\n"

Expand Down Expand Up @@ -2759,7 +2759,7 @@ static RPCHelpMan loadtxoutset()
LogPrintf("[snapshot] waiting to see blockheader %s in headers chain before snapshot activation\n",
base_blockhash.ToString());

ChainstateManager& chainman = *node.chainman;
ChainstateManager& chainman = EnsureChainman(node);

while (max_secs_to_wait_for_headers > 0) {
snapshot_start_block = WITH_LOCK(::cs_main,
Expand Down Expand Up @@ -2831,8 +2831,7 @@ return RPCHelpMan{
LOCK(cs_main);
UniValue obj(UniValue::VOBJ);

NodeContext& node = EnsureAnyNodeContext(request.context);
ChainstateManager& chainman = *node.chainman;
ChainstateManager& chainman = EnsureAnyChainman(request.context);

auto make_chain_data = [&](const Chainstate& cs, bool validated) EXCLUSIVE_LOCKS_REQUIRED(::cs_main) {
AssertLockHeld(::cs_main);
Expand Down
2 changes: 1 addition & 1 deletion src/test/fuzz/chain.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ FUZZ_TARGET(chain)
(void)disk_block_index->GetBlockTimeMax();
(void)disk_block_index->GetMedianTimePast();
(void)disk_block_index->GetUndoPos();
(void)disk_block_index->HaveTxsDownloaded();
(void)disk_block_index->HaveNumChainTxs();
(void)disk_block_index->IsValid();
}

Expand Down
4 changes: 2 additions & 2 deletions src/test/validation_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -138,11 +138,11 @@ BOOST_AUTO_TEST_CASE(test_assumeutxo)

const auto out110 = *params->AssumeutxoForHeight(110);
BOOST_CHECK_EQUAL(out110.hash_serialized.ToString(), "1ebbf5850204c0bdb15bf030f47c7fe91d45c44c712697e4509ba67adb01c618");
BOOST_CHECK_EQUAL(out110.nChainTx, 110U);
BOOST_CHECK_EQUAL(out110.nChainTx, 111U);

const auto out110_2 = *params->AssumeutxoForBlockhash(uint256S("0x696e92821f65549c7ee134edceeeeaaa4105647a3c4fd9f298c0aec0ab50425c"));
BOOST_CHECK_EQUAL(out110_2.hash_serialized.ToString(), "1ebbf5850204c0bdb15bf030f47c7fe91d45c44c712697e4509ba67adb01c618");
BOOST_CHECK_EQUAL(out110_2.nChainTx, 110U);
BOOST_CHECK_EQUAL(out110_2.nChainTx, 111U);
}

BOOST_AUTO_TEST_SUITE_END()
40 changes: 22 additions & 18 deletions src/validation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@
#include <numeric>
#include <optional>
#include <string>
#include <utility>
#include <tuple>
#include <utility>

using kernel::CCoinsStats;
using kernel::CoinStatsHashType;
Expand Down Expand Up @@ -2996,7 +2996,7 @@ CBlockIndex* Chainstate::FindMostWorkChain()
CBlockIndex *pindexTest = pindexNew;
bool fInvalidAncestor = false;
while (pindexTest && !m_chain.Contains(pindexTest)) {
assert(pindexTest->HaveTxsDownloaded() || pindexTest->nHeight == 0);
assert(pindexTest->HaveNumChainTxs() || pindexTest->nHeight == 0);

// Pruned nodes may have entries in setBlockIndexCandidates for
// which block files have been deleted. Remove those as candidates
Expand Down Expand Up @@ -3351,7 +3351,7 @@ bool Chainstate::PreciousBlock(BlockValidationState& state, CBlockIndex* pindex)
// call preciousblock 2**31-1 times on the same set of tips...
m_chainman.nBlockReverseSequenceId--;
}
if (pindex->IsValid(BLOCK_VALID_TRANSACTIONS) && pindex->HaveTxsDownloaded()) {
if (pindex->IsValid(BLOCK_VALID_TRANSACTIONS) && pindex->HaveNumChainTxs()) {
setBlockIndexCandidates.insert(pindex);
PruneBlockIndexCandidates();
}
Expand Down Expand Up @@ -3399,7 +3399,7 @@ bool Chainstate::InvalidateBlock(BlockValidationState& state, CBlockIndex* pinde
if (!m_chain.Contains(candidate) &&
!CBlockIndexWorkComparator()(candidate, pindex->pprev) &&
candidate->IsValid(BLOCK_VALID_TRANSACTIONS) &&
candidate->HaveTxsDownloaded()) {
candidate->HaveNumChainTxs()) {
candidate_blocks_by_work.insert(std::make_pair(candidate->nChainWork, candidate));
}
}
Expand Down Expand Up @@ -3488,7 +3488,7 @@ bool Chainstate::InvalidateBlock(BlockValidationState& state, CBlockIndex* pinde
// Loop back over all block index entries and add any missing entries
// to setBlockIndexCandidates.
for (auto& [_, block_index] : m_blockman.m_block_index) {
if (block_index.IsValid(BLOCK_VALID_TRANSACTIONS) && block_index.HaveTxsDownloaded() && !setBlockIndexCandidates.value_comp()(&block_index, m_chain.Tip())) {
if (block_index.IsValid(BLOCK_VALID_TRANSACTIONS) && block_index.HaveNumChainTxs() && !setBlockIndexCandidates.value_comp()(&block_index, m_chain.Tip())) {
setBlockIndexCandidates.insert(&block_index);
}
}
Expand Down Expand Up @@ -3520,7 +3520,7 @@ void Chainstate::ResetBlockFailureFlags(CBlockIndex *pindex) {
if (!block_index.IsValid() && block_index.GetAncestor(nHeight) == pindex) {
block_index.nStatus &= ~BLOCK_FAILED_MASK;
m_blockman.m_dirty_blockindex.insert(&block_index);
if (block_index.IsValid(BLOCK_VALID_TRANSACTIONS) && block_index.HaveTxsDownloaded() && setBlockIndexCandidates.value_comp()(m_chain.Tip(), &block_index)) {
if (block_index.IsValid(BLOCK_VALID_TRANSACTIONS) && block_index.HaveNumChainTxs() && setBlockIndexCandidates.value_comp()(m_chain.Tip(), &block_index)) {
setBlockIndexCandidates.insert(&block_index);
}
if (&block_index == m_chainman.m_best_invalid) {
Expand Down Expand Up @@ -3583,7 +3583,7 @@ void ChainstateManager::ReceivedBlockTransactions(const CBlock& block, CBlockInd
pindexNew->RaiseValidity(BLOCK_VALID_TRANSACTIONS);
m_blockman.m_dirty_blockindex.insert(pindexNew);

if (pindexNew->pprev == nullptr || pindexNew->pprev->HaveTxsDownloaded()) {
if (pindexNew->pprev == nullptr || pindexNew->pprev->HaveNumChainTxs()) {
// If pindexNew is the genesis block or all parents are BLOCK_VALID_TRANSACTIONS.
std::deque<CBlockIndex*> queue;
queue.push_back(pindexNew);
Expand Down Expand Up @@ -4566,7 +4566,7 @@ bool ChainstateManager::LoadBlockIndex()
// here.
if (pindex == GetSnapshotBaseBlock() ||
(pindex->IsValid(BLOCK_VALID_TRANSACTIONS) &&
(pindex->HaveTxsDownloaded() || pindex->pprev == nullptr))) {
(pindex->HaveNumChainTxs() || pindex->pprev == nullptr))) {

for (Chainstate* chainstate : GetAll()) {
chainstate->TryAddBlockIndexCandidate(pindex);
Expand Down Expand Up @@ -4844,10 +4844,14 @@ void ChainstateManager::CheckBlockIndex()
CBlockIndex* pindexFirstAssumeValid = nullptr; // Oldest ancestor of pindex which has BLOCK_ASSUMED_VALID
while (pindex != nullptr) {
nNodes++;
if (pindex->pprev && pindex->nTx > 0) {
// nChainTx should increase monotonically
assert(pindex->pprev->nChainTx <= pindex->nChainTx);
}
// Make sure nChainTx sum is correctly computed.
unsigned int prev_chain_tx = pindex->pprev ? pindex->pprev->nChainTx : 0;
assert((pindex->nChainTx == pindex->nTx + prev_chain_tx)
// For testing, allow transaction counts to be completely unset.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which testing do you mean? -chain=main -checkblockindex on mainnet?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tests that are changed in the same commit.

Copy link
Member

@maflcko maflcko Oct 10, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed the "testing" check and ran the main binary (not the tests) and it crashed here.

Generally I don't think a good place to put test-only code is the main production code. Especially when it comes to consensus critical code. This makes it impossible to properly test and review the code in a production environment outside of unit tests.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed the "testing" check and ran the main binary (not the tests) and it crashed here.

Hm, can you say what exactly you removed and what you can to see the crash? I couldn't reproduce that so far.

Generally I don't think a good place to put test-only code is the main production code. Especially when it comes to consensus critical code. This makes it impossible to properly test and review the code in a production environment outside of unit tests.

I guess we should improve our test setup to include realistic nTx and nChainTx values so we can remove those checks again.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used this diff IIRC, which also fails the functional tests:

diff --git a/src/validation.cpp b/src/validation.cpp
index 9108f911f0..3bfa5091ad 100644
--- a/src/validation.cpp
+++ b/src/validation.cpp
@@ -4847,10 +4847,7 @@ void ChainstateManager::CheckBlockIndex()
         // Make sure nChainTx sum is correctly computed.
         unsigned int prev_chain_tx = pindex->pprev ? pindex->pprev->nChainTx : 0;
         assert((pindex->nChainTx == pindex->nTx + prev_chain_tx)
-               // For testing, allow transaction counts to be completely unset.
-               || (pindex->nChainTx == 0 && pindex->nTx == 0)
-               // For testing, allow this nChainTx to be unset if previous is also unset.
-               || (pindex->nChainTx == 0 && prev_chain_tx == 0 && pindex->pprev));
+                 );
 
         if (pindexFirstAssumeValid == nullptr && pindex->nStatus & BLOCK_ASSUMED_VALID) pindexFirstAssumeValid = pindex;
         if (pindexFirstInvalid == nullptr && pindex->nStatus & BLOCK_FAILED_VALID) pindexFirstInvalid = pindex;

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another report, which could replicate the crash, see ##28791

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Turned into an issue: #29261

|| (pindex->nChainTx == 0 && pindex->nTx == 0)
// For testing, allow this nChainTx to be unset if previous is also unset.
|| (pindex->nChainTx == 0 && prev_chain_tx == 0 && pindex->pprev));

if (pindexFirstAssumeValid == nullptr && pindex->nStatus & BLOCK_ASSUMED_VALID) pindexFirstAssumeValid = pindex;
if (pindexFirstInvalid == nullptr && pindex->nStatus & BLOCK_FAILED_VALID) pindexFirstInvalid = pindex;
if (pindexFirstMissing == nullptr && !(pindex->nStatus & BLOCK_HAVE_DATA)) {
Expand Down Expand Up @@ -4886,7 +4890,7 @@ void ChainstateManager::CheckBlockIndex()
}
}
}
if (!pindex->HaveTxsDownloaded()) assert(pindex->nSequenceId <= 0); // nSequenceId can't be set positive for blocks that aren't linked (negative is used for preciousblock)
if (!pindex->HaveNumChainTxs()) assert(pindex->nSequenceId <= 0); // nSequenceId can't be set positive for blocks that aren't linked (negative is used for preciousblock)
// VALID_TRANSACTIONS is equivalent to nTx > 0 for all nodes (whether or not pruning has occurred).
// HAVE_DATA is only equivalent to nTx > 0 (or VALID_TRANSACTIONS) if no pruning has occurred.
// Unless these indexes are assumed valid and pending block download on a
Expand Down Expand Up @@ -4916,9 +4920,9 @@ void ChainstateManager::CheckBlockIndex()
// actually seen a block's transactions.
assert(((pindex->nStatus & BLOCK_VALID_MASK) >= BLOCK_VALID_TRANSACTIONS) == (pindex->nTx > 0)); // This is pruning-independent.
}
// All parents having had data (at some point) is equivalent to all parents being VALID_TRANSACTIONS, which is equivalent to HaveTxsDownloaded().
assert((pindexFirstNeverProcessed == nullptr) == pindex->HaveTxsDownloaded());
assert((pindexFirstNotTransactionsValid == nullptr) == pindex->HaveTxsDownloaded());
// All parents having had data (at some point) is equivalent to all parents being VALID_TRANSACTIONS, which is equivalent to HaveNumChainTxs().
assert((pindexFirstNeverProcessed == nullptr) == pindex->HaveNumChainTxs());
assert((pindexFirstNotTransactionsValid == nullptr) == pindex->HaveNumChainTxs());
assert(pindex->nHeight == nHeight); // nHeight must be consistent.
assert(pindex->pprev == nullptr || pindex->nChainWork >= pindex->pprev->nChainWork); // For every block except the genesis block, the chainwork must be larger than the parent's.
assert(nHeight < 2 || (pindex->pskip && (pindex->pskip->nHeight < nHeight))); // The pskip pointer must point back for all but the first 2 blocks.
Expand Down Expand Up @@ -5367,7 +5371,7 @@ bool ChainstateManager::PopulateAndValidateSnapshot(
// ActivateSnapshot(), but is done so that we avoid doing the long work of staging
// a snapshot that isn't actually usable.
if (WITH_LOCK(::cs_main, return !CBlockIndexWorkComparator()(ActiveTip(), snapshot_start_block))) {
LogPrintf("[snapshot] activation failed - height does not exceed active chainstate\n");
LogPrintf("[snapshot] activation failed - work does not exceed active chainstate\n");
return false;
}

Expand Down Expand Up @@ -5768,7 +5772,7 @@ ChainstateManager::~ChainstateManager()
m_versionbitscache.Clear();
}

bool ChainstateManager::DetectSnapshotChainstate(CTxMemPool* mempool)
bool ChainstateManager::DetectSnapshotChainstate()
{
assert(!m_snapshot_chainstate);
std::optional<fs::path> path = node::FindSnapshotChainstateDir(m_options.datadir);
Expand Down