From f252e687ec94b6ccafb5bc44b7df3daeb473fdea Mon Sep 17 00:00:00 2001 From: Ryan Ofsky Date: Mon, 5 Feb 2024 16:16:14 -0500 Subject: [PATCH 1/8] assumeutxo test: Add RPC test for fake nTx and nChainTx values The fake values will be removed in an upcoming commit, so it is useful to have test coverage confirming the change in behavior. --- test/functional/feature_assumeutxo.py | 42 ++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/test/functional/feature_assumeutxo.py b/test/functional/feature_assumeutxo.py index a9ed4a09cea..a29ee8be8b1 100755 --- a/test/functional/feature_assumeutxo.py +++ b/test/functional/feature_assumeutxo.py @@ -34,6 +34,7 @@ Interesting starting states could be loading a snapshot when the current chain t """ from shutil import rmtree +from dataclasses import dataclass from test_framework.messages import tx_from_hex from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( @@ -174,10 +175,18 @@ class AssumeutxoTest(BitcoinTestFramework): # Generate a series of blocks that `n0` will have in the snapshot, # but that n1 and n2 don't yet see. + assert n0.getblockcount() == START_HEIGHT + blocks = {START_HEIGHT: Block(n0.getbestblockhash(), 1, START_HEIGHT + 1)} for i in range(100): + block_tx = 1 if i % 3 == 0: self.mini_wallet.send_self_transfer(from_node=n0) + block_tx += 1 self.generate(n0, nblocks=1, sync_fun=self.no_op) + height = n0.getblockcount() + hash = n0.getbestblockhash() + blocks[height] = Block(hash, block_tx, blocks[height-1].chain_tx + block_tx) + self.log.info("-- Testing assumeutxo + some indexes + pruning") @@ -207,7 +216,7 @@ class AssumeutxoTest(BitcoinTestFramework): assert_equal( dump_output['txoutset_hash'], "a4bf3407ccb2cc0145c49ebba8fa91199f8a3903daf0883875941497d2493c27") - assert_equal(dump_output["nchaintx"], 334) + assert_equal(dump_output["nchaintx"], blocks[SNAPSHOT_BASE_HEIGHT].chain_tx) assert_equal(n0.getblockchaininfo()["blocks"], SNAPSHOT_BASE_HEIGHT) # Mine more blocks on top of the snapshot that n1 hasn't yet seen. This @@ -228,6 +237,30 @@ class AssumeutxoTest(BitcoinTestFramework): assert_equal(loaded['coins_loaded'], SNAPSHOT_BASE_HEIGHT) assert_equal(loaded['base_height'], SNAPSHOT_BASE_HEIGHT) + def check_tx_counts(final: bool) -> None: + """Check nTx and nChainTx intermediate values right after loading + the snapshot, and final values after the snapshot is validated.""" + for height, block in blocks.items(): + tx = n1.getblockheader(block.hash)["nTx"] + chain_tx = n1.getchaintxstats(nblocks=1, blockhash=block.hash)["txcount"] + + # Intermediate nTx of the starting block should be real, but nTx of + # later blocks should be fake 1 values set by snapshot loading code. + if final or height == START_HEIGHT: + assert_equal(tx, block.tx) + else: + assert_equal(tx, 1) + + # Intermediate nChainTx of the starting block and snapshot block + # should be real, but others will be fake values set by snapshot + # loading code. + if final or height in (START_HEIGHT, SNAPSHOT_BASE_HEIGHT): + assert_equal(chain_tx, block.chain_tx) + else: + assert_equal(chain_tx, height + 1) + + check_tx_counts(final=False) + normal, snapshot = n1.getchainstates()["chainstates"] assert_equal(normal['blocks'], START_HEIGHT) assert_equal(normal.get('snapshot_blockhash'), None) @@ -291,6 +324,8 @@ class AssumeutxoTest(BitcoinTestFramework): } self.wait_until(lambda: n1.getindexinfo() == completed_idx_state) + self.log.info("Re-check nTx and nChainTx values") + check_tx_counts(final=True) for i in (0, 1): n = self.nodes[i] @@ -365,6 +400,11 @@ class AssumeutxoTest(BitcoinTestFramework): self.connect_nodes(0, 2) self.wait_until(lambda: n2.getblockcount() == FINAL_HEIGHT) +@dataclass +class Block: + hash: str + tx: int + chain_tx: int if __name__ == '__main__': AssumeutxoTest().main() From 63e8fc912c21a2f5b47e8eab10fb13c604afed85 Mon Sep 17 00:00:00 2001 From: Ryan Ofsky Date: Mon, 5 Feb 2024 16:16:14 -0500 Subject: [PATCH 2/8] ci: add getchaintxstats ubsan suppressions Add ubsan suppressions for integer overflows in the getchaintxstats RPC. getchainstatstx line "int nTxDiff = pindex->nChainTx - past_block.nChainTx" can trigger ubsan integer overflows when assumeutxo snapshots are loaded, from subtracting unsigned values and assigning the result to a signed int. The overflow behavior probably exists in current code but is hard to trigger because it would require calling getchainstatstx at the right time with specific parameters as background blocks are being downloaded. But the overflow behavior becomes easier to trigger in the upcoming commit removing fake nChainTx values, so a suppression needs to be added before then for CI to pass. getchainstatstx should probably be improved separately in another PR to not need this suppression, and handle edge cases and missing nChainTx values more carefully. --- test/sanitizer_suppressions/ubsan | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/sanitizer_suppressions/ubsan b/test/sanitizer_suppressions/ubsan index 2a2f7ca4706..482667a26a2 100644 --- a/test/sanitizer_suppressions/ubsan +++ b/test/sanitizer_suppressions/ubsan @@ -51,6 +51,7 @@ unsigned-integer-overflow:CCoinsViewCache::Uncache unsigned-integer-overflow:CompressAmount unsigned-integer-overflow:DecompressAmount unsigned-integer-overflow:crypto/ +unsigned-integer-overflow:getchaintxstats* unsigned-integer-overflow:MurmurHash3 unsigned-integer-overflow:CBlockPolicyEstimator::processBlockTx unsigned-integer-overflow:TxConfirmStats::EstimateMedianVal @@ -61,6 +62,7 @@ implicit-integer-sign-change:CBlockPolicyEstimator::processBlockTx implicit-integer-sign-change:SetStdinEcho implicit-integer-sign-change:compressor.h implicit-integer-sign-change:crypto/ +implicit-integer-sign-change:getchaintxstats* implicit-integer-sign-change:TxConfirmStats::removeTx implicit-integer-sign-change:prevector.h implicit-integer-sign-change:verify_flags From 0fd915ee6bef63bb360ccc5c039a3c11676c38e3 Mon Sep 17 00:00:00 2001 From: Ryan Ofsky Date: Thu, 8 Feb 2024 11:37:19 -0500 Subject: [PATCH 3/8] validation: Check GuessVerificationProgress is not called with disconnected block Use Assume macro as suggested https://github.com/bitcoin/bitcoin/pull/29370#discussion_r1479427801 --- src/validation.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/validation.cpp b/src/validation.cpp index 94d2680db74..02f415101e4 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -5286,6 +5286,12 @@ double GuessVerificationProgress(const ChainTxData& data, const CBlockIndex *pin if (pindex == nullptr) return 0.0; + if (!Assume(pindex->nChainTx > 0)) { + LogWarning("Internal bug detected: block %d has unset nChainTx (%s %s). Please report this issue here: %s\n", + pindex->nHeight, PACKAGE_NAME, FormatFullVersion(), PACKAGE_BUGREPORT); + return 0.0; + } + int64_t nNow = time(nullptr); double fTxTotal; From 9b97d5bbf980d657a277c85d113c2ae3e870e0ec Mon Sep 17 00:00:00 2001 From: Ryan Ofsky Date: Fri, 2 Feb 2024 10:59:24 -0500 Subject: [PATCH 4/8] doc: Improve comments describing setBlockIndexCandidates checks The checks are changing slightly in the next commit, so try to explains the ones that exist to avoid confusion (https://github.com/bitcoin/bitcoin/pull/29370#discussion_r1499519079) --- src/validation.cpp | 41 ++++++++++++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/src/validation.cpp b/src/validation.cpp index 02f415101e4..4cb004c7cc1 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -5126,19 +5126,42 @@ void ChainstateManager::CheckBlockIndex() // Chainstate-specific checks on setBlockIndexCandidates for (auto c : GetAll()) { if (c->m_chain.Tip() == nullptr) continue; + // Two main factors determine whether pindex is a candidate in + // setBlockIndexCandidates: + // + // - If pindex has less work than the chain tip, it should not be a + // candidate, and this will be asserted below. Otherwise it is a + // potential candidate. + // + // - If pindex or one of its parent blocks never downloaded + // transactions (pindexFirstNeverProcessed is non-null), it should + // not be a candidate, and this will be asserted below. Otherwise + // it is a potential candidate. if (!CBlockIndexWorkComparator()(pindex, c->m_chain.Tip()) && pindexFirstNeverProcessed == nullptr) { + // If pindex was detected as invalid (pindexFirstInvalid is + // non-null), it is not required to be in + // setBlockIndexCandidates. if (pindexFirstInvalid == nullptr) { - const bool is_active = c == &ActiveChainstate(); - // If this block sorts at least as good as the current tip and - // is valid and we have all data for its parents, it must be in - // setBlockIndexCandidates. m_chain.Tip() must also be there - // even if some data has been pruned. + // If pindex and all its parents downloaded transactions, + // and the transactions were not pruned (pindexFirstMissing + // is null), it is a potential candidate. The check + // excludes pruned blocks, because if any blocks were + // pruned between pindex the current chain tip, pindex will + // only temporarily be added to setBlockIndexCandidates, + // before being moved to m_blocks_unlinked. This check + // could be improved to verify that if all blocks between + // the chain tip and pindex have data, pindex must be a + // candidate. // + // If pindex is the chain tip, it also is a potential + // candidate. if ((pindexFirstMissing == nullptr || pindex == c->m_chain.Tip())) { - // The active chainstate should always have this block - // as a candidate, but a background chainstate should - // only have it if it is an ancestor of the snapshot base. - if (is_active || GetSnapshotBaseBlock()->GetAncestor(pindex->nHeight) == pindex) { + // If this chainstate is the active chainstate, pindex + // must be in setBlockIndexCandidates. Otherwise, this + // chainstate is a background validation chainstate, and + // pindex only needs to be added if it is an ancestor of + // the snapshot that is being validated. + if (c == &ActiveChainstate() || GetSnapshotBaseBlock()->GetAncestor(pindex->nHeight) == pindex) { assert(c->setBlockIndexCandidates.count(pindex)); } } From ef29c8b662309a438121a83f27fd7bdd1779700c Mon Sep 17 00:00:00 2001 From: Ryan Ofsky Date: Fri, 2 Feb 2024 10:59:24 -0500 Subject: [PATCH 5/8] assumeutxo: Get rid of faked nTx and nChainTx values The `PopulateAndValidateSnapshot` function introduced in f6e2da5fb7c6406c37612c838c998078ea8d2252 from #19806 has been setting fake `nTx` and `nChainTx` values that can show up in RPC results (see #29328) and make `CBlockIndex` state hard to reason about, because it is difficult to know whether the values are real or fake. Revert to previous behavior of setting `nTx` and `nChainTx` to 0 when the values are unknown, instead of faking them. This commit fixes at least two assert failures in the (pindex->nChainTx == pindex->nTx + prev_chain_tx) check that would happen previously. Tests for these failures are added separately in the next two commits. Compatibility note: This change could result in -checkblockindex failures if a snapshot was loaded by a previous version of Bitcoin Core and not fully validated, because fake nTx values will have been saved to the block index. It would be pretty easy to avoid these failures by adding some compatibility code to `LoadBlockIndex` and changing `nTx` values from 1 to 0 when they are fake (when `(pindex->nStatus & BLOCK_VALID_MASK) < BLOCK_VALID_TRANSACTIONS`), but a little simpler not to worry about being compatible in this case. --- src/chain.h | 36 +++-- src/test/util/chainstate.h | 17 ++- .../validation_chainstatemanager_tests.cpp | 2 + src/validation.cpp | 139 +++++++++++------- test/functional/feature_assumeutxo.py | 11 +- 5 files changed, 118 insertions(+), 87 deletions(-) diff --git a/src/chain.h b/src/chain.h index fa165a4aa73..7faeb25088c 100644 --- a/src/chain.h +++ b/src/chain.h @@ -98,16 +98,20 @@ enum BlockStatus : uint32_t { /** * Only first tx is coinbase, 2 <= coinbase input script length <= 100, transactions valid, no duplicate txids, - * sigops, size, merkle root. Implies all parents are at least TREE but not necessarily TRANSACTIONS. When all - * parent blocks also have TRANSACTIONS, CBlockIndex::nChainTx will be set. + * sigops, size, merkle root. Implies all parents are at least TREE but not necessarily TRANSACTIONS. + * + * If a block's validity is at least VALID_TRANSACTIONS, CBlockIndex::nTx will be set. If a block and all previous + * blocks back to the genesis block or an assumeutxo snapshot block are at least VALID_TRANSACTIONS, + * CBlockIndex::nChainTx will be set. */ BLOCK_VALID_TRANSACTIONS = 3, //! Outputs do not overspend inputs, no double spends, coinbase output ok, no immature coinbase spends, BIP30. - //! Implies all parents are either at least VALID_CHAIN, or are ASSUMED_VALID + //! Implies all previous blocks back to the genesis block or an assumeutxo snapshot block are at least VALID_CHAIN. BLOCK_VALID_CHAIN = 4, - //! Scripts & signatures ok. Implies all parents are either at least VALID_SCRIPTS, or are ASSUMED_VALID. + //! Scripts & signatures ok. Implies all previous blocks back to the genesis block or an assumeutxo snapshot block + //! are at least VALID_SCRIPTS. BLOCK_VALID_SCRIPTS = 5, //! All validity bits. @@ -173,21 +177,16 @@ public: //! (memory only) Total amount of work (expected number of hashes) in the chain up to and including this block arith_uint256 nChainWork{}; - //! Number of transactions in this block. + //! Number of transactions in this block. This will be nonzero if the block + //! reached the VALID_TRANSACTIONS level, and zero otherwise. //! 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. + //! This value will be non-zero if this block and all previous blocks back + //! to the genesis block or an assumeutxo snapshot block have reached the + //! VALID_TRANSACTIONS level. //! Change to 64-bit type before 2024 (assuming worst case of 60 byte transactions). - //! - //! 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 @@ -262,15 +261,14 @@ public: } /** - * Check whether this block's and all previous blocks' transactions have been - * downloaded (and stored to disk) at some point. + * Check whether this block and all previous blocks back to the genesis block or an assumeutxo snapshot block have + * reached VALID_TRANSACTIONS and had transactions downloaded (and stored to disk) at some point. * * Does not imply the transactions are consensus-valid (ConnectTip might fail) * Does not imply the transactions are still stored on disk. (IsBlockPruned might return true) * - * 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. + * Note that this will be true for the snapshot base block, if one is loaded, since its nChainTx value will have + * been set manually based on the related AssumeutxoData entry. */ bool HaveNumChainTxs() const { return nChainTx != 0; } diff --git a/src/test/util/chainstate.h b/src/test/util/chainstate.h index e2a88eacdda..ff95e64b7ed 100644 --- a/src/test/util/chainstate.h +++ b/src/test/util/chainstate.h @@ -91,13 +91,16 @@ CreateAndActivateUTXOSnapshot( // these blocks instead CBlockIndex *pindex = orig_tip; while (pindex && pindex != chain.m_chain.Tip()) { - pindex->nStatus &= ~BLOCK_HAVE_DATA; - pindex->nStatus &= ~BLOCK_HAVE_UNDO; - // We have to set the ASSUMED_VALID flag, because otherwise it - // would not be possible to have a block index entry without HAVE_DATA - // and with nTx > 0 (since we aren't setting the pruned flag); - // see CheckBlockIndex(). - pindex->nStatus |= BLOCK_ASSUMED_VALID; + // Remove all data and validity flags by just setting + // BLOCK_VALID_TREE. Also reset transaction counts and sequence + // ids that are set when blocks are received, to make test setup + // more realistic and satisfy consistency checks in + // CheckBlockIndex(). + assert(pindex->IsValid(BlockStatus::BLOCK_VALID_TREE)); + pindex->nStatus = BlockStatus::BLOCK_VALID_TREE; + pindex->nTx = 0; + pindex->nChainTx = 0; + pindex->nSequenceId = 0; pindex = pindex->pprev; } } diff --git a/src/test/validation_chainstatemanager_tests.cpp b/src/test/validation_chainstatemanager_tests.cpp index 4bbab1cdcd2..26f9ab59a6a 100644 --- a/src/test/validation_chainstatemanager_tests.cpp +++ b/src/test/validation_chainstatemanager_tests.cpp @@ -464,6 +464,8 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager_loadblockindex, TestChain100Setup) // Blocks with heights in range [91, 110] are marked ASSUMED_VALID if (i < last_assumed_valid_idx && i >= assumed_valid_start_idx) { index->nStatus = BlockStatus::BLOCK_VALID_TREE | BlockStatus::BLOCK_ASSUMED_VALID; + index->nTx = 0; + index->nChainTx = 0; } ++num_indexes; diff --git a/src/validation.cpp b/src/validation.cpp index 4cb004c7cc1..f27f47aea03 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -3660,7 +3660,18 @@ void ChainstateManager::ReceivedBlockTransactions(const CBlock& block, CBlockInd { AssertLockHeld(cs_main); pindexNew->nTx = block.vtx.size(); - pindexNew->nChainTx = 0; + // Typically nChainTx will be 0 at this point, but it can be nonzero if this + // is a pruned block which is being downloaded again, or if this is an + // assumeutxo snapshot block which has a hardcoded nChainTx value from the + // snapshot metadata. If the pindex is not the snapshot block and the + // nChainTx value is not zero, assert that value is actually correct. + auto prev_tx_sum = [](CBlockIndex& block) { return block.nTx + (block.pprev ? block.pprev->nChainTx : 0); }; + if (!Assume(pindexNew->nChainTx == 0 || pindexNew->nChainTx == prev_tx_sum(*pindexNew) || + pindexNew == GetSnapshotBaseBlock())) { + LogWarning("Internal bug detected: block %d has unexpected nChainTx %i that should be %i (%s %s). Please report this issue here: %s\n", + pindexNew->nHeight, pindexNew->nChainTx, prev_tx_sum(*pindexNew), PACKAGE_NAME, FormatFullVersion(), PACKAGE_BUGREPORT); + pindexNew->nChainTx = 0; + } pindexNew->nFile = pos.nFile; pindexNew->nDataPos = pos.nPos; pindexNew->nUndoPos = 0; @@ -3680,7 +3691,15 @@ void ChainstateManager::ReceivedBlockTransactions(const CBlock& block, CBlockInd while (!queue.empty()) { CBlockIndex *pindex = queue.front(); queue.pop_front(); - pindex->nChainTx = (pindex->pprev ? pindex->pprev->nChainTx : 0) + pindex->nTx; + // Before setting nChainTx, assert that it is 0 or already set to + // the correct value. This assert will fail after receiving the + // assumeutxo snapshot block if assumeutxo snapshot metadata has an + // incorrect hardcoded AssumeutxoData::nChainTx value. + if (!Assume(pindex->nChainTx == 0 || pindex->nChainTx == prev_tx_sum(*pindex))) { + LogWarning("Internal bug detected: block %d has unexpected nChainTx %i that should be %i (%s %s). Please report this issue here: %s\n", + pindex->nHeight, pindex->nChainTx, prev_tx_sum(*pindex), PACKAGE_NAME, FormatFullVersion(), PACKAGE_BUGREPORT); + } + pindex->nChainTx = prev_tx_sum(*pindex); pindex->nSequenceId = nBlockSequenceId++; for (Chainstate *c : GetAll()) { c->TryAddBlockIndexCandidate(pindex); @@ -5023,13 +5042,30 @@ void ChainstateManager::CheckBlockIndex() size_t nNodes = 0; int nHeight = 0; CBlockIndex* pindexFirstInvalid = nullptr; // Oldest ancestor of pindex which is invalid. - CBlockIndex* pindexFirstMissing = nullptr; // Oldest ancestor of pindex which does not have BLOCK_HAVE_DATA. - CBlockIndex* pindexFirstNeverProcessed = nullptr; // Oldest ancestor of pindex for which nTx == 0. + CBlockIndex* pindexFirstMissing = nullptr; // Oldest ancestor of pindex which does not have BLOCK_HAVE_DATA, since assumeutxo snapshot if used. + CBlockIndex* pindexFirstNeverProcessed = nullptr; // Oldest ancestor of pindex for which nTx == 0, since assumeutxo snapshot if used. CBlockIndex* pindexFirstNotTreeValid = nullptr; // Oldest ancestor of pindex which does not have BLOCK_VALID_TREE (regardless of being valid or not). - CBlockIndex* pindexFirstNotTransactionsValid = nullptr; // Oldest ancestor of pindex which does not have BLOCK_VALID_TRANSACTIONS (regardless of being valid or not). - CBlockIndex* pindexFirstNotChainValid = nullptr; // Oldest ancestor of pindex which does not have BLOCK_VALID_CHAIN (regardless of being valid or not). - CBlockIndex* pindexFirstNotScriptsValid = nullptr; // Oldest ancestor of pindex which does not have BLOCK_VALID_SCRIPTS (regardless of being valid or not). + CBlockIndex* pindexFirstNotTransactionsValid = nullptr; // Oldest ancestor of pindex which does not have BLOCK_VALID_TRANSACTIONS (regardless of being valid or not), since assumeutxo snapshot if used. + CBlockIndex* pindexFirstNotChainValid = nullptr; // Oldest ancestor of pindex which does not have BLOCK_VALID_CHAIN (regardless of being valid or not), since assumeutxo snapshot if used. + CBlockIndex* pindexFirstNotScriptsValid = nullptr; // Oldest ancestor of pindex which does not have BLOCK_VALID_SCRIPTS (regardless of being valid or not), since assumeutxo snapshot if used. CBlockIndex* pindexFirstAssumeValid = nullptr; // Oldest ancestor of pindex which has BLOCK_ASSUMED_VALID + + // After checking an assumeutxo snapshot block, reset pindexFirst pointers + // to earlier blocks that have not been downloaded or validated yet, so + // checks for later blocks can assume the earlier blocks were validated and + // be stricter, testing for more requirements. + const CBlockIndex* snap_base{GetSnapshotBaseBlock()}; + CBlockIndex *snap_first_missing{}, *snap_first_notx{}, *snap_first_notv{}, *snap_first_nocv{}, *snap_first_nosv{}; + auto snap_update_firsts = [&] { + if (pindex == snap_base) { + std::swap(snap_first_missing, pindexFirstMissing); + std::swap(snap_first_notx, pindexFirstNeverProcessed); + std::swap(snap_first_notv, pindexFirstNotTransactionsValid); + std::swap(snap_first_nocv, pindexFirstNotChainValid); + std::swap(snap_first_nosv, pindexFirstNotScriptsValid); + } + }; + while (pindex != nullptr) { nNodes++; if (pindexFirstAssumeValid == nullptr && pindex->nStatus & BLOCK_ASSUMED_VALID) pindexFirstAssumeValid = pindex; @@ -5040,10 +5076,7 @@ void ChainstateManager::CheckBlockIndex() if (pindexFirstNeverProcessed == nullptr && pindex->nTx == 0) pindexFirstNeverProcessed = pindex; if (pindex->pprev != nullptr && pindexFirstNotTreeValid == nullptr && (pindex->nStatus & BLOCK_VALID_MASK) < BLOCK_VALID_TREE) pindexFirstNotTreeValid = pindex; - if (pindex->pprev != nullptr && !pindex->IsAssumedValid()) { - // Skip validity flag checks for BLOCK_ASSUMED_VALID index entries, since these - // *_VALID_MASK flags will not be present for index entries we are temporarily assuming - // valid. + if (pindex->pprev != nullptr) { if (pindexFirstNotTransactionsValid == nullptr && (pindex->nStatus & BLOCK_VALID_MASK) < BLOCK_VALID_TRANSACTIONS) { pindexFirstNotTransactionsValid = pindex; @@ -5073,36 +5106,26 @@ void ChainstateManager::CheckBlockIndex() 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 - // background chainstate. - if (!m_blockman.m_have_pruned && !pindex->IsAssumedValid()) { + if (!m_blockman.m_have_pruned) { // If we've never pruned, then HAVE_DATA should be equivalent to nTx > 0 assert(!(pindex->nStatus & BLOCK_HAVE_DATA) == (pindex->nTx == 0)); - if (pindexFirstAssumeValid == nullptr) { - // If we've got some assume valid blocks, then we might have - // missing blocks (not HAVE_DATA) but still treat them as - // having been processed (with a fake nTx value). Otherwise, we - // can assert that these are the same. - assert(pindexFirstMissing == pindexFirstNeverProcessed); - } + assert(pindexFirstMissing == pindexFirstNeverProcessed); } else { // If we have pruned, then we can only say that HAVE_DATA implies nTx > 0 if (pindex->nStatus & BLOCK_HAVE_DATA) assert(pindex->nTx > 0); } if (pindex->nStatus & BLOCK_HAVE_UNDO) assert(pindex->nStatus & BLOCK_HAVE_DATA); if (pindex->IsAssumedValid()) { - // Assumed-valid blocks should have some nTx value. - assert(pindex->nTx > 0); // Assumed-valid blocks should connect to the main chain. assert((pindex->nStatus & BLOCK_VALID_MASK) >= BLOCK_VALID_TREE); - } else { - // Otherwise there should only be an nTx value if we have - // actually seen a block's transactions. - assert(((pindex->nStatus & BLOCK_VALID_MASK) >= BLOCK_VALID_TRANSACTIONS) == (pindex->nTx > 0)); // This is pruning-independent. } + // There should only be an nTx value if we have + // 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 HaveNumChainTxs(). - assert((pindexFirstNeverProcessed == nullptr) == pindex->HaveNumChainTxs()); - assert((pindexFirstNotTransactionsValid == nullptr) == pindex->HaveNumChainTxs()); + // HaveNumChainTxs will also be set in the assumeutxo snapshot block from snapshot metadata. + assert((pindexFirstNeverProcessed == nullptr || pindex == snap_base) == pindex->HaveNumChainTxs()); + assert((pindexFirstNotTransactionsValid == nullptr || pindex == snap_base) == 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. @@ -5115,14 +5138,18 @@ void ChainstateManager::CheckBlockIndex() assert((pindex->nStatus & BLOCK_FAILED_MASK) == 0); // The failed mask cannot be set for blocks without invalid parents. } // 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) - // Transaction may be completely unset - happens if only the header was accepted but the block hasn't been processed. - || (pindex->nChainTx == 0 && pindex->nTx == 0) - // nChainTx may be unset, but nTx set (if a block has been accepted, but one of its predecessors hasn't been processed yet) - || (pindex->nChainTx == 0 && prev_chain_tx == 0 && pindex->pprev) - // Transaction counts prior to snapshot are unknown. - || pindex->IsAssumedValid()); + if (!pindex->pprev) { + // If no previous block, nTx and nChainTx must be the same. + assert(pindex->nChainTx == pindex->nTx); + } else if (pindex->pprev->nChainTx > 0 && pindex->nTx > 0) { + // If previous nChainTx is set and number of transactions in block is known, sum must be set. + assert(pindex->nChainTx == pindex->nTx + pindex->pprev->nChainTx); + } else { + // Otherwise nChainTx should only be set if this is a snapshot + // block, and must be set if it is. + assert((pindex->nChainTx != 0) == (pindex == snap_base)); + } + // Chainstate-specific checks on setBlockIndexCandidates for (auto c : GetAll()) { if (c->m_chain.Tip() == nullptr) continue; @@ -5133,16 +5160,19 @@ void ChainstateManager::CheckBlockIndex() // candidate, and this will be asserted below. Otherwise it is a // potential candidate. // - // - If pindex or one of its parent blocks never downloaded - // transactions (pindexFirstNeverProcessed is non-null), it should - // not be a candidate, and this will be asserted below. Otherwise - // it is a potential candidate. - if (!CBlockIndexWorkComparator()(pindex, c->m_chain.Tip()) && pindexFirstNeverProcessed == nullptr) { + // - If pindex or one of its parent blocks back to the genesis block + // or an assumeutxo snapshot never downloaded transactions + // (pindexFirstNeverProcessed is non-null), it should not be a + // candidate, and this will be asserted below. The only exception + // is if pindex itself is an assumeutxo snapshot block. Then it is + // also a potential candidate. + if (!CBlockIndexWorkComparator()(pindex, c->m_chain.Tip()) && (pindexFirstNeverProcessed == nullptr || pindex == snap_base)) { // If pindex was detected as invalid (pindexFirstInvalid is // non-null), it is not required to be in // setBlockIndexCandidates. if (pindexFirstInvalid == nullptr) { - // If pindex and all its parents downloaded transactions, + // If pindex and all its parents back to the genesis block + // or an assumeutxo snapshot block downloaded transactions, // and the transactions were not pruned (pindexFirstMissing // is null), it is a potential candidate. The check // excludes pruned blocks, because if any blocks were @@ -5155,13 +5185,17 @@ void ChainstateManager::CheckBlockIndex() // // If pindex is the chain tip, it also is a potential // candidate. - if ((pindexFirstMissing == nullptr || pindex == c->m_chain.Tip())) { + // + // If the chainstate was loaded from a snapshot and pindex + // is the base of the snapshot, pindex is also a potential + // candidate. + if (pindexFirstMissing == nullptr || pindex == c->m_chain.Tip() || pindex == c->SnapshotBase()) { // If this chainstate is the active chainstate, pindex // must be in setBlockIndexCandidates. Otherwise, this // chainstate is a background validation chainstate, and // pindex only needs to be added if it is an ancestor of // the snapshot that is being validated. - if (c == &ActiveChainstate() || GetSnapshotBaseBlock()->GetAncestor(pindex->nHeight) == pindex) { + if (c == &ActiveChainstate() || snap_base->GetAncestor(pindex->nHeight) == pindex) { assert(c->setBlockIndexCandidates.count(pindex)); } } @@ -5192,7 +5226,7 @@ void ChainstateManager::CheckBlockIndex() if (pindexFirstMissing == nullptr) assert(!foundInUnlinked); // We aren't missing data for any parent -- cannot be in m_blocks_unlinked. if (pindex->pprev && (pindex->nStatus & BLOCK_HAVE_DATA) && pindexFirstNeverProcessed == nullptr && pindexFirstMissing != nullptr) { // We HAVE_DATA for this block, have received data for all parents at some point, but we're currently missing data for some parent. - assert(m_blockman.m_have_pruned || pindexFirstAssumeValid != nullptr); // We must have pruned, or else we're using a snapshot (causing us to have faked the received data for some parent(s)). + assert(m_blockman.m_have_pruned); // This block may have entered m_blocks_unlinked if: // - it has a descendant that at some point had more work than the // tip, and @@ -5205,7 +5239,7 @@ void ChainstateManager::CheckBlockIndex() const bool is_active = c == &ActiveChainstate(); if (!CBlockIndexWorkComparator()(pindex, c->m_chain.Tip()) && c->setBlockIndexCandidates.count(pindex) == 0) { if (pindexFirstInvalid == nullptr) { - if (is_active || GetSnapshotBaseBlock()->GetAncestor(pindex->nHeight) == pindex) { + if (is_active || snap_base->GetAncestor(pindex->nHeight) == pindex) { assert(foundInUnlinked); } } @@ -5216,6 +5250,7 @@ void ChainstateManager::CheckBlockIndex() // End: actual consistency checks. // Try descending into the first subnode. + snap_update_firsts(); std::pair::iterator,std::multimap::iterator> range = forward.equal_range(pindex); if (range.first != range.second) { // A subnode was found. @@ -5227,6 +5262,7 @@ void ChainstateManager::CheckBlockIndex() // Move upwards until we reach a node of which we have not yet visited the last child. while (pindex) { // We are going to either move to a parent or a sibling of pindex. + snap_update_firsts(); // If pindex was the first with a certain property, unset the corresponding variable. if (pindex == pindexFirstInvalid) pindexFirstInvalid = nullptr; if (pindex == pindexFirstMissing) pindexFirstMissing = nullptr; @@ -5730,14 +5766,6 @@ bool ChainstateManager::PopulateAndValidateSnapshot( for (int i = AFTER_GENESIS_START; i <= snapshot_chainstate.m_chain.Height(); ++i) { index = snapshot_chainstate.m_chain[i]; - // Fake nTx so that LoadBlockIndex() loads assumed-valid CBlockIndex - // entries (among other things) - if (!index->nTx) { - index->nTx = 1; - } - // Fake nChainTx so that GuessVerificationProgress reports accurately - index->nChainTx = index->pprev->nChainTx + index->nTx; - // Mark unvalidated block index entries beneath the snapshot base block as assumed-valid. if (!index->IsValid(BLOCK_VALID_SCRIPTS)) { // This flag will be removed once the block is fully validated by a @@ -5760,6 +5788,7 @@ bool ChainstateManager::PopulateAndValidateSnapshot( } assert(index); + assert(index == snapshot_start_block); index->nChainTx = au_data.nChainTx; snapshot_chainstate.setBlockIndexCandidates.insert(snapshot_start_block); diff --git a/test/functional/feature_assumeutxo.py b/test/functional/feature_assumeutxo.py index a29ee8be8b1..27910c39098 100755 --- a/test/functional/feature_assumeutxo.py +++ b/test/functional/feature_assumeutxo.py @@ -244,20 +244,19 @@ class AssumeutxoTest(BitcoinTestFramework): tx = n1.getblockheader(block.hash)["nTx"] chain_tx = n1.getchaintxstats(nblocks=1, blockhash=block.hash)["txcount"] - # Intermediate nTx of the starting block should be real, but nTx of - # later blocks should be fake 1 values set by snapshot loading code. + # Intermediate nTx of the starting block should be set, but nTx of + # later blocks should be 0 before they are downloaded. if final or height == START_HEIGHT: assert_equal(tx, block.tx) else: - assert_equal(tx, 1) + assert_equal(tx, 0) # Intermediate nChainTx of the starting block and snapshot block - # should be real, but others will be fake values set by snapshot - # loading code. + # should be set, but others should be 0 until they are downloaded. if final or height in (START_HEIGHT, SNAPSHOT_BASE_HEIGHT): assert_equal(chain_tx, block.chain_tx) else: - assert_equal(chain_tx, height + 1) + assert_equal(chain_tx, 0) check_tx_counts(final=False) From 0391458d767b842a7925785a7053400c0e1cb55a Mon Sep 17 00:00:00 2001 From: Ryan Ofsky Date: Fri, 2 Feb 2024 10:59:24 -0500 Subject: [PATCH 6/8] test: assumeutxo stale block CheckBlockIndex crash test Add a test for a CheckBlockIndex crash that would happen before previous "assumeutxo: Get rid of faked nTx and nChainTx values" commit. The crash was an assert failure in the (pindex->nChainTx == pindex->nTx + prev_chain_tx) check that would previously happen if a snapshot was loaded, and a block was submitted which forked from the chain before the snapshot block and after the last downloaded background chain block. This block would not be marked assumed-valid because it would not be an ancestor of the snapshot, and it would have nTx set, nChainTx unset, and prev->nChainTx set with a fake value, so the assert would fail. After the fix, prev->nChainTx is unset instead of being set to a fake value, so the assert succeeds. This test was originally posted by maflcko in https://github.com/bitcoin/bitcoin/issues/29261#issuecomment-1918947945 Co-authored-by: MarcoFalke <*~=`'#}+{/-|&$^_@721217.xyz> --- test/functional/feature_assumeutxo.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/test/functional/feature_assumeutxo.py b/test/functional/feature_assumeutxo.py index 27910c39098..04de8b299e2 100755 --- a/test/functional/feature_assumeutxo.py +++ b/test/functional/feature_assumeutxo.py @@ -186,6 +186,14 @@ class AssumeutxoTest(BitcoinTestFramework): height = n0.getblockcount() hash = n0.getbestblockhash() blocks[height] = Block(hash, block_tx, blocks[height-1].chain_tx + block_tx) + if i == 4: + # Create a stale block that forks off the main chain before the snapshot. + temp_invalid = n0.getbestblockhash() + n0.invalidateblock(temp_invalid) + stale_hash = self.generateblock(n0, output="raw(aaaa)", transactions=[], sync_fun=self.no_op)["hash"] + n0.invalidateblock(stale_hash) + n0.reconsiderblock(temp_invalid) + stale_block = n0.getblock(stale_hash, 0) self.log.info("-- Testing assumeutxo + some indexes + pruning") @@ -270,6 +278,15 @@ class AssumeutxoTest(BitcoinTestFramework): assert_equal(n1.getblockchaininfo()["blocks"], SNAPSHOT_BASE_HEIGHT) + self.log.info("Submit a stale block that forked off the chain before the snapshot") + # Normally a block like this would not be downloaded, but if it is + # submitted early before the background chain catches up to the fork + # point, it winds up in m_blocks_unlinked and triggers a corner case + # that previously crashed CheckBlockIndex. + n1.submitblock(stale_block) + n1.getchaintips() + n1.getblock(stale_hash) + self.log.info("Submit a spending transaction for a snapshot chainstate coin to the mempool") # spend the coinbase output of the first block that is not available on node1 spend_coin_blockhash = n1.getblockhash(START_HEIGHT + 1) From ef174e9ed21c08f38e5d4b537b6decfd1f646db9 Mon Sep 17 00:00:00 2001 From: Ryan Ofsky Date: Fri, 2 Feb 2024 10:59:24 -0500 Subject: [PATCH 7/8] test: assumeutxo snapshot block CheckBlockIndex crash test Add a test for a CheckBlockIndex crash that would happen before previous "assumeutxo: Get rid of faked nTx and nChainTx values" commit. The crash was an assert failure in the (pindex->nChainTx == pindex->nTx + prev_chain_tx) check that would previously happen if the snapshot block was submitted after loading the snapshot and downloading a few blocks after the snapshot. In that case ReceivedBlockTransactions() previously would overwrite the nChainTx value of the submitted snapshot block with a fake value based on the previous block, so the (pindex->nChainTx == pindex->nTx + prev_chain_tx) check would later fail on the first block after the snapshot. This test was originally posted by Martin Zumsande in https://github.com/bitcoin/bitcoin/pull/29370#issuecomment-1974096225 Co-authored-by: Martin Zumsande --- test/functional/feature_assumeutxo.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/functional/feature_assumeutxo.py b/test/functional/feature_assumeutxo.py index 04de8b299e2..eb9ea65c4f2 100755 --- a/test/functional/feature_assumeutxo.py +++ b/test/functional/feature_assumeutxo.py @@ -324,6 +324,16 @@ class AssumeutxoTest(BitcoinTestFramework): self.log.info("Restarted node before snapshot validation completed, reloading...") self.restart_node(1, extra_args=self.extra_args[1]) + + # Send snapshot block to n1 out of order. This makes the test less + # realistic because normally the snapshot block is one of the last + # blocks downloaded, but its useful to test because it triggers more + # corner cases in ReceivedBlockTransactions() and CheckBlockIndex() + # setting and testing nChainTx values, and it exposed previous bugs. + snapshot_hash = n0.getblockhash(SNAPSHOT_BASE_HEIGHT) + snapshot_block = n0.getblock(snapshot_hash, 0) + n1.submitblock(snapshot_block) + self.connect_nodes(0, 1) self.log.info(f"Ensuring snapshot chain syncs to tip. ({FINAL_HEIGHT})") From 9d9a7458a2570f7db56ab626b22010591089c312 Mon Sep 17 00:00:00 2001 From: Ryan Ofsky Date: Mon, 5 Feb 2024 17:10:27 -0500 Subject: [PATCH 8/8] assumeutxo: Remove BLOCK_ASSUMED_VALID flag Flag adds complexity and is not currently used for anything. --- doc/design/assumeutxo.md | 12 ++----- src/chain.h | 31 ++----------------- .../validation_chainstatemanager_tests.cpp | 19 +++--------- src/validation.cpp | 19 +++--------- src/validation.h | 7 +++-- 5 files changed, 17 insertions(+), 71 deletions(-) diff --git a/doc/design/assumeutxo.md b/doc/design/assumeutxo.md index abb623fc698..f527ac0f2da 100644 --- a/doc/design/assumeutxo.md +++ b/doc/design/assumeutxo.md @@ -51,18 +51,12 @@ The utility script ## Design notes -- A new block index `nStatus` flag is introduced, `BLOCK_ASSUMED_VALID`, to mark block - index entries that are required to be assumed-valid by a chainstate created - from a UTXO snapshot. This flag is used as a way to modify certain - CheckBlockIndex() logic to account for index entries that are pending validation by a - chainstate running asynchronously in the background. - - The concept of UTXO snapshots is treated as an implementation detail that lives behind the ChainstateManager interface. The external presentation of the changes required to facilitate the use of UTXO snapshots is the understanding that there are - now certain regions of the chain that can be temporarily assumed to be valid (using - the nStatus flag mentioned above). In certain cases, e.g. wallet rescanning, this is - very similar to dealing with a pruned chain. + now certain regions of the chain that can be temporarily assumed to be valid. + In certain cases, e.g. wallet rescanning, this is very similar to dealing with + a pruned chain. Logic outside ChainstateManager should try not to know about snapshots, instead preferring to work in terms of more general states like assumed-valid. diff --git a/src/chain.h b/src/chain.h index 7faeb25088c..bb70dbd8bcd 100644 --- a/src/chain.h +++ b/src/chain.h @@ -128,21 +128,8 @@ enum BlockStatus : uint32_t { BLOCK_OPT_WITNESS = 128, //!< block data in blk*.dat was received with a witness-enforcing client - /** - * If ASSUMED_VALID is set, it means that this block has not been validated - * and has validity status less than VALID_SCRIPTS. Also that it may have - * descendant blocks with VALID_SCRIPTS set, because they can be validated - * based on an assumeutxo snapshot. - * - * When an assumeutxo snapshot is loaded, the ASSUMED_VALID flag is added to - * unvalidated blocks at the snapshot height and below. Then, as the background - * validation progresses, and these blocks are validated, the ASSUMED_VALID - * flags are removed. See `doc/design/assumeutxo.md` for details. - * - * This flag is only used to implement checks in CheckBlockIndex() and - * should not be used elsewhere. - */ - BLOCK_ASSUMED_VALID = 256, + BLOCK_STATUS_RESERVED = 256, //!< Unused flag that was previously set on assumeutxo snapshot blocks and their + //!< ancestors before they were validated, and unset when they were validated. }; /** The block chain is a tree shaped structure starting with the @@ -316,14 +303,6 @@ public: return ((nStatus & BLOCK_VALID_MASK) >= nUpTo); } - //! @returns true if the block is assumed-valid; this means it is queued to be - //! validated by a background chainstate. - bool IsAssumedValid() const EXCLUSIVE_LOCKS_REQUIRED(::cs_main) - { - AssertLockHeld(::cs_main); - return nStatus & BLOCK_ASSUMED_VALID; - } - //! Raise the validity level of this block index entry. //! Returns true if the validity was changed. bool RaiseValidity(enum BlockStatus nUpTo) EXCLUSIVE_LOCKS_REQUIRED(::cs_main) @@ -333,12 +312,6 @@ public: if (nStatus & BLOCK_FAILED_MASK) return false; if ((nStatus & BLOCK_VALID_MASK) < nUpTo) { - // If this block had been marked assumed-valid and we're raising - // its validity to a certain point, there is no longer an assumption. - if (nStatus & BLOCK_ASSUMED_VALID && nUpTo >= BLOCK_VALID_SCRIPTS) { - nStatus &= ~BLOCK_ASSUMED_VALID; - } - nStatus = (nStatus & ~BLOCK_VALID_MASK) | nUpTo; return true; } diff --git a/src/test/validation_chainstatemanager_tests.cpp b/src/test/validation_chainstatemanager_tests.cpp index 26f9ab59a6a..4bf66a55ebf 100644 --- a/src/test/validation_chainstatemanager_tests.cpp +++ b/src/test/validation_chainstatemanager_tests.cpp @@ -276,9 +276,6 @@ struct SnapshotTestSetup : TestChain100Setup { BOOST_CHECK_EQUAL( *node::ReadSnapshotBaseBlockhash(found), *chainman.SnapshotBlockhash()); - - // Ensure that the genesis block was not marked assumed-valid. - BOOST_CHECK(!chainman.ActiveChain().Genesis()->IsAssumedValid()); } const auto& au_data = ::Params().AssumeutxoForHeight(snapshot_height); @@ -410,7 +407,7 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager_activate_snapshot, SnapshotTestSetup) //! - First, verify that setBlockIndexCandidates is as expected when using a single, //! fully-validating chainstate. //! -//! - Then mark a region of the chain BLOCK_ASSUMED_VALID and introduce a second chainstate +//! - Then mark a region of the chain as missing data and introduce a second chainstate //! that will tolerate assumed-valid blocks. Run LoadBlockIndex() and ensure that the first //! chainstate only contains fully validated blocks and the other chainstate contains all blocks, //! except those marked assume-valid, because those entries don't HAVE_DATA. @@ -421,7 +418,6 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager_loadblockindex, TestChain100Setup) Chainstate& cs1 = chainman.ActiveChainstate(); int num_indexes{0}; - int num_assumed_valid{0}; // Blocks in range [assumed_valid_start_idx, last_assumed_valid_idx) will be // marked as assumed-valid and not having data. const int expected_assumed_valid{20}; @@ -456,37 +452,30 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager_loadblockindex, TestChain100Setup) reload_all_block_indexes(); BOOST_CHECK_EQUAL(cs1.setBlockIndexCandidates.size(), 1); - // Mark some region of the chain assumed-valid, and remove the HAVE_DATA flag. + // Reset some region of the chain's nStatus, removing the HAVE_DATA flag. for (int i = 0; i <= cs1.m_chain.Height(); ++i) { LOCK(::cs_main); auto index = cs1.m_chain[i]; - // Blocks with heights in range [91, 110] are marked ASSUMED_VALID + // Blocks with heights in range [91, 110] are marked as missing data. if (i < last_assumed_valid_idx && i >= assumed_valid_start_idx) { - index->nStatus = BlockStatus::BLOCK_VALID_TREE | BlockStatus::BLOCK_ASSUMED_VALID; + index->nStatus = BlockStatus::BLOCK_VALID_TREE; index->nTx = 0; index->nChainTx = 0; } ++num_indexes; - if (index->IsAssumedValid()) ++num_assumed_valid; // Note the last fully-validated block as the expected validated tip. if (i == (assumed_valid_start_idx - 1)) { validated_tip = index; - BOOST_CHECK(!index->IsAssumedValid()); } // Note the last assumed valid block as the snapshot base if (i == last_assumed_valid_idx - 1) { assumed_base = index; - BOOST_CHECK(index->IsAssumedValid()); - } else if (i == last_assumed_valid_idx) { - BOOST_CHECK(!index->IsAssumedValid()); } } - BOOST_CHECK_EQUAL(expected_assumed_valid, num_assumed_valid); - // Note: cs2's tip is not set when ActivateExistingSnapshot is called. Chainstate& cs2 = WITH_LOCK(::cs_main, return chainman.ActivateExistingSnapshot(*assumed_base->phashBlock)); diff --git a/src/validation.cpp b/src/validation.cpp index f27f47aea03..a01b3da9ca2 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -5048,7 +5048,6 @@ void ChainstateManager::CheckBlockIndex() CBlockIndex* pindexFirstNotTransactionsValid = nullptr; // Oldest ancestor of pindex which does not have BLOCK_VALID_TRANSACTIONS (regardless of being valid or not), since assumeutxo snapshot if used. CBlockIndex* pindexFirstNotChainValid = nullptr; // Oldest ancestor of pindex which does not have BLOCK_VALID_CHAIN (regardless of being valid or not), since assumeutxo snapshot if used. CBlockIndex* pindexFirstNotScriptsValid = nullptr; // Oldest ancestor of pindex which does not have BLOCK_VALID_SCRIPTS (regardless of being valid or not), since assumeutxo snapshot if used. - CBlockIndex* pindexFirstAssumeValid = nullptr; // Oldest ancestor of pindex which has BLOCK_ASSUMED_VALID // After checking an assumeutxo snapshot block, reset pindexFirst pointers // to earlier blocks that have not been downloaded or validated yet, so @@ -5068,7 +5067,6 @@ void ChainstateManager::CheckBlockIndex() while (pindex != nullptr) { nNodes++; - 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)) { pindexFirstMissing = pindex; @@ -5115,7 +5113,7 @@ void ChainstateManager::CheckBlockIndex() if (pindex->nStatus & BLOCK_HAVE_DATA) assert(pindex->nTx > 0); } if (pindex->nStatus & BLOCK_HAVE_UNDO) assert(pindex->nStatus & BLOCK_HAVE_DATA); - if (pindex->IsAssumedValid()) { + if (snap_base && snap_base->GetAncestor(pindex->nHeight) == pindex) { // Assumed-valid blocks should connect to the main chain. assert((pindex->nStatus & BLOCK_VALID_MASK) >= BLOCK_VALID_TREE); } @@ -5271,7 +5269,6 @@ void ChainstateManager::CheckBlockIndex() if (pindex == pindexFirstNotTransactionsValid) pindexFirstNotTransactionsValid = nullptr; if (pindex == pindexFirstNotChainValid) pindexFirstNotChainValid = nullptr; if (pindex == pindexFirstNotScriptsValid) pindexFirstNotScriptsValid = nullptr; - if (pindex == pindexFirstAssumeValid) pindexFirstAssumeValid = nullptr; // Find our parent. CBlockIndex* pindexPar = pindex->pprev; // Find which child we just visited. @@ -5757,22 +5754,14 @@ bool ChainstateManager::PopulateAndValidateSnapshot( // Fake various pieces of CBlockIndex state: CBlockIndex* index = nullptr; - // Don't make any modifications to the genesis block. - // This is especially important because we don't want to erroneously - // apply BLOCK_ASSUMED_VALID to genesis, which would happen if we didn't skip - // it here (since it apparently isn't BLOCK_VALID_SCRIPTS). + // Don't make any modifications to the genesis block since it shouldn't be + // neccessary, and since the genesis block doesn't have normal flags like + // BLOCK_VALID_SCRIPTS set. constexpr int AFTER_GENESIS_START{1}; for (int i = AFTER_GENESIS_START; i <= snapshot_chainstate.m_chain.Height(); ++i) { index = snapshot_chainstate.m_chain[i]; - // Mark unvalidated block index entries beneath the snapshot base block as assumed-valid. - if (!index->IsValid(BLOCK_VALID_SCRIPTS)) { - // This flag will be removed once the block is fully validated by a - // background chainstate. - index->nStatus |= BLOCK_ASSUMED_VALID; - } - // Fake BLOCK_OPT_WITNESS so that Chainstate::NeedsRedownload() // won't ask to rewind the entire assumed-valid chain on startup. if (DeploymentActiveAt(*index, *this, Consensus::DEPLOYMENT_SEGWIT)) { diff --git a/src/validation.h b/src/validation.h index 71aac46f812..57e0777a2aa 100644 --- a/src/validation.h +++ b/src/validation.h @@ -583,9 +583,10 @@ public: const CBlockIndex* SnapshotBase() EXCLUSIVE_LOCKS_REQUIRED(::cs_main); /** - * The set of all CBlockIndex entries with either BLOCK_VALID_TRANSACTIONS (for - * itself and all ancestors) *or* BLOCK_ASSUMED_VALID (if using background - * chainstates) and as good as our current tip or better. Entries may be failed, + * The set of all CBlockIndex entries that have as much work as our current + * tip or more, and transaction data needed to be validated (with + * BLOCK_VALID_TRANSACTIONS for each block and its parents back to the + * genesis block or an assumeutxo snapshot block). Entries may be failed, * though, and pruning nodes may be missing the data for the block. */ std::set setBlockIndexCandidates;