mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-07-18 07:25:17 +02:00
Merge bitcoin/bitcoin#31405: validation: stricter internal handling of invalid blocks
f6b782f3aa
doc: Improve m_best_header documentation (Martin Zumsande)ee673b9aa0
validation: remove m_failed_blocks (Martin Zumsande)ed764ea2b4
validation: Add more checks to CheckBlockIndex() (Martin Zumsande)9a70883002
validation: in invalidateblock, calculate m_best_header right away (Martin Zumsande)8e39f2d20d
validation: in invalidateblock, mark children as invalid right away (Martin Zumsande)4c29326183
validation: cache all headers with enough PoW in invalidateblock (Martin Zumsande)15fa5b5a90
validation: call InvalidBlockFound also from AcceptBlock (Martin Zumsande) Pull request description: Some fields in validation are set opportunistically by "best effort": - The `BLOCK_FAILED_CHILD` status (which means that the block index has an invalid predecessor) - `m_best_header` (the most-work header not known to be invalid). This means that there are known situations in which these fields are not set when they should be, or set to wrong values. This is tolerated because the fields are not used for anything consensus-critical and triggering these situations involved creating invalid blocks with valid PoW header, so would have a cost attached. Also, having stricter guarantees for these fields requires iterating over the entire block index, which has some DoS potential, especially with any header above the checkpoint being accepted int he past (see e.g. #11531). However, there are reasons to change this now: - RPCs use these fields and can report wrong results - There is the constant possibility that someone could add code that expects these fields to be correct, especially because it is not well documented that these fields cannot always be relied upon. - DoS concerns have become less of an issue after #25717 - now an attacker would need to invest much more work because they can't fork off the last checkpoint anymore This PR continues the work from #30666 to ensure that `BLOCK_FAILED_CHILD` status and `m_best_header` are always correct: - it adds a call to `InvalidChainFound()` in `AcceptBlock()`. - it adds checks for `BLOCK_FAILED_CHILD` and `m_best_header` to `CheckBlockIndex()`. In order to be able to do this, the existing cache in the RPC-only `InvalidateBlock()` is adjusted to handle these as well. These are performance optimizations with the goal of avoiding having a call of `InvalidChainFound()` / looping over the block index after each disconnected block. I also wrote a fuzz test to find possible edge cases violating `CheckBlockIndex`, which I will PR separately soon. - it removes the `m_failed_blocks` set, which was a heuristic necessary when we couldn't be sure if a given block index had an invalid predecessor or not. Now that we have that guarantee, the set is no longer needed. ACKs for top commit: stickies-v: re-ACKf6b782f3aa
achow101: reACKf6b782f3aa
ryanofsky: Code review ACKf6b782f3aa
with only minor code & comment updates TheCharlatan: Re-ACKf6b782f3aa
Tree-SHA512: 1bee324216eeee6af401abdb683abd098b18212833f9600dbc0a46244e634cb0e6f2a320c937a5675a12af7ec4a7d10fabc1db9e9bc0d9d0712e6e6ca72d084f
This commit is contained in:
@ -2093,7 +2093,6 @@ void Chainstate::InvalidBlockFound(CBlockIndex* pindex, const BlockValidationSta
|
||||
AssertLockHeld(cs_main);
|
||||
if (state.GetResult() != BlockValidationResult::BLOCK_MUTATED) {
|
||||
pindex->nStatus |= BLOCK_FAILED_VALID;
|
||||
m_chainman.m_failed_blocks.insert(pindex);
|
||||
m_blockman.m_dirty_blockindex.insert(pindex);
|
||||
setBlockIndexCandidates.erase(pindex);
|
||||
InvalidChainFound(pindex);
|
||||
@ -3672,7 +3671,7 @@ bool Chainstate::InvalidateBlock(BlockValidationState& state, CBlockIndex* pinde
|
||||
// To avoid walking the block index repeatedly in search of candidates,
|
||||
// build a map once so that we can look up candidate blocks by chain
|
||||
// work as we go.
|
||||
std::multimap<const arith_uint256, CBlockIndex *> candidate_blocks_by_work;
|
||||
std::multimap<const arith_uint256, CBlockIndex*> highpow_outofchain_headers;
|
||||
|
||||
{
|
||||
LOCK(cs_main);
|
||||
@ -3681,13 +3680,12 @@ bool Chainstate::InvalidateBlock(BlockValidationState& state, CBlockIndex* pinde
|
||||
// We don't need to put anything in our active chain into the
|
||||
// multimap, because those candidates will be found and considered
|
||||
// as we disconnect.
|
||||
// Instead, consider only non-active-chain blocks that have at
|
||||
// least as much work as where we expect the new tip to end up.
|
||||
// Instead, consider only non-active-chain blocks that score
|
||||
// at least as good with CBlockIndexWorkComparator as the new tip.
|
||||
if (!m_chain.Contains(candidate) &&
|
||||
!CBlockIndexWorkComparator()(candidate, pindex->pprev) &&
|
||||
candidate->IsValid(BLOCK_VALID_TRANSACTIONS) &&
|
||||
candidate->HaveNumChainTxs()) {
|
||||
candidate_blocks_by_work.insert(std::make_pair(candidate->nChainWork, candidate));
|
||||
!CBlockIndexWorkComparator()(candidate, pindex->pprev) &&
|
||||
!(candidate->nStatus & BLOCK_FAILED_MASK)) {
|
||||
highpow_outofchain_headers.insert({candidate->nChainWork, candidate});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3736,15 +3734,42 @@ bool Chainstate::InvalidateBlock(BlockValidationState& state, CBlockIndex* pinde
|
||||
m_blockman.m_dirty_blockindex.insert(to_mark_failed);
|
||||
}
|
||||
|
||||
// Add any equal or more work headers to setBlockIndexCandidates
|
||||
auto candidate_it = candidate_blocks_by_work.lower_bound(invalid_walk_tip->pprev->nChainWork);
|
||||
while (candidate_it != candidate_blocks_by_work.end()) {
|
||||
if (!CBlockIndexWorkComparator()(candidate_it->second, invalid_walk_tip->pprev)) {
|
||||
setBlockIndexCandidates.insert(candidate_it->second);
|
||||
candidate_it = candidate_blocks_by_work.erase(candidate_it);
|
||||
} else {
|
||||
++candidate_it;
|
||||
// Mark out-of-chain descendants of the invalidated block as invalid
|
||||
// (possibly replacing a pre-existing BLOCK_FAILED_VALID with BLOCK_FAILED_CHILD)
|
||||
// Add any equal or more work headers that are not invalidated to setBlockIndexCandidates
|
||||
// Recalculate m_best_header if it became invalid.
|
||||
auto candidate_it = highpow_outofchain_headers.lower_bound(invalid_walk_tip->pprev->nChainWork);
|
||||
|
||||
const bool best_header_needs_update{m_chainman.m_best_header->GetAncestor(invalid_walk_tip->nHeight) == invalid_walk_tip};
|
||||
if (best_header_needs_update) {
|
||||
// pprev is definitely still valid at this point, but there may be better ones
|
||||
m_chainman.m_best_header = invalid_walk_tip->pprev;
|
||||
}
|
||||
|
||||
while (candidate_it != highpow_outofchain_headers.end()) {
|
||||
CBlockIndex* candidate{candidate_it->second};
|
||||
if (candidate->GetAncestor(invalid_walk_tip->nHeight) == invalid_walk_tip) {
|
||||
// Children of failed blocks should be marked as BLOCK_FAILED_CHILD instead.
|
||||
candidate->nStatus &= ~BLOCK_FAILED_VALID;
|
||||
candidate->nStatus |= BLOCK_FAILED_CHILD;
|
||||
m_blockman.m_dirty_blockindex.insert(candidate);
|
||||
// If invalidated, the block is irrelevant for setBlockIndexCandidates
|
||||
// and for m_best_header and can be removed from the cache.
|
||||
candidate_it = highpow_outofchain_headers.erase(candidate_it);
|
||||
continue;
|
||||
}
|
||||
if (!CBlockIndexWorkComparator()(candidate, invalid_walk_tip->pprev) &&
|
||||
candidate->IsValid(BLOCK_VALID_TRANSACTIONS) &&
|
||||
candidate->HaveNumChainTxs()) {
|
||||
setBlockIndexCandidates.insert(candidate);
|
||||
// Do not remove candidate from the highpow_outofchain_headers cache, because it might be a descendant of the block being invalidated
|
||||
// which needs to be marked failed later.
|
||||
}
|
||||
if (best_header_needs_update &&
|
||||
m_chainman.m_best_header->nChainWork < candidate->nChainWork) {
|
||||
m_chainman.m_best_header = candidate;
|
||||
}
|
||||
++candidate_it;
|
||||
}
|
||||
|
||||
// Track the last disconnected block, so we can correct its BLOCK_FAILED_CHILD status in future
|
||||
@ -3766,7 +3791,6 @@ bool Chainstate::InvalidateBlock(BlockValidationState& state, CBlockIndex* pinde
|
||||
pindex->nStatus |= BLOCK_FAILED_VALID;
|
||||
m_blockman.m_dirty_blockindex.insert(pindex);
|
||||
setBlockIndexCandidates.erase(pindex);
|
||||
m_chainman.m_failed_blocks.insert(pindex);
|
||||
}
|
||||
|
||||
// If any new blocks somehow arrived while we were disconnecting
|
||||
@ -3837,7 +3861,6 @@ void Chainstate::ResetBlockFailureFlags(CBlockIndex *pindex) {
|
||||
// Reset invalid block marker if it was pointing to one of those.
|
||||
m_chainman.m_best_invalid = nullptr;
|
||||
}
|
||||
m_chainman.m_failed_blocks.erase(&block_index);
|
||||
}
|
||||
}
|
||||
|
||||
@ -3846,7 +3869,6 @@ void Chainstate::ResetBlockFailureFlags(CBlockIndex *pindex) {
|
||||
if (pindex->nStatus & BLOCK_FAILED_MASK) {
|
||||
pindex->nStatus &= ~BLOCK_FAILED_MASK;
|
||||
m_blockman.m_dirty_blockindex.insert(pindex);
|
||||
m_chainman.m_failed_blocks.erase(pindex);
|
||||
}
|
||||
pindex = pindex->pprev;
|
||||
}
|
||||
@ -4344,45 +4366,6 @@ bool ChainstateManager::AcceptBlockHeader(const CBlockHeader& block, BlockValida
|
||||
LogDebug(BCLog::VALIDATION, "%s: Consensus::ContextualCheckBlockHeader: %s, %s\n", __func__, hash.ToString(), state.ToString());
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Determine if this block descends from any block which has been found
|
||||
* invalid (m_failed_blocks), then mark pindexPrev and any blocks between
|
||||
* them as failed. For example:
|
||||
*
|
||||
* D3
|
||||
* /
|
||||
* B2 - C2
|
||||
* / \
|
||||
* A D2 - E2 - F2
|
||||
* \
|
||||
* B1 - C1 - D1 - E1
|
||||
*
|
||||
* In the case that we attempted to reorg from E1 to F2, only to find
|
||||
* C2 to be invalid, we would mark D2, E2, and F2 as BLOCK_FAILED_CHILD
|
||||
* but NOT D3 (it was not in any of our candidate sets at the time).
|
||||
*
|
||||
* In any case D3 will also be marked as BLOCK_FAILED_CHILD at restart
|
||||
* in LoadBlockIndex.
|
||||
*/
|
||||
if (!pindexPrev->IsValid(BLOCK_VALID_SCRIPTS)) {
|
||||
// The above does not mean "invalid": it checks if the previous block
|
||||
// hasn't been validated up to BLOCK_VALID_SCRIPTS. This is a performance
|
||||
// optimization, in the common case of adding a new block to the tip,
|
||||
// we don't need to iterate over the failed blocks list.
|
||||
for (const CBlockIndex* failedit : m_failed_blocks) {
|
||||
if (pindexPrev->GetAncestor(failedit->nHeight) == failedit) {
|
||||
assert(failedit->nStatus & BLOCK_FAILED_VALID);
|
||||
CBlockIndex* invalid_walk = pindexPrev;
|
||||
while (invalid_walk != failedit) {
|
||||
invalid_walk->nStatus |= BLOCK_FAILED_CHILD;
|
||||
m_blockman.m_dirty_blockindex.insert(invalid_walk);
|
||||
invalid_walk = invalid_walk->pprev;
|
||||
}
|
||||
LogDebug(BCLog::VALIDATION, "header %s has prev block invalid: %s\n", hash.ToString(), block.hashPrevBlock.ToString());
|
||||
return state.Invalid(BlockValidationResult::BLOCK_INVALID_PREV, "bad-prevblk");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!min_pow_checked) {
|
||||
LogDebug(BCLog::VALIDATION, "%s: not adding new block header %s, missing anti-dos proof-of-work validation\n", __func__, hash.ToString());
|
||||
@ -4507,9 +4490,8 @@ bool ChainstateManager::AcceptBlock(const std::shared_ptr<const CBlock>& pblock,
|
||||
|
||||
if (!CheckBlock(block, state, params.GetConsensus()) ||
|
||||
!ContextualCheckBlock(block, state, *this, pindex->pprev)) {
|
||||
if (state.IsInvalid() && state.GetResult() != BlockValidationResult::BLOCK_MUTATED) {
|
||||
pindex->nStatus |= BLOCK_FAILED_VALID;
|
||||
m_blockman.m_dirty_blockindex.insert(pindex);
|
||||
if (Assume(state.IsInvalid())) {
|
||||
ActiveChainstate().InvalidBlockFound(pindex, state);
|
||||
}
|
||||
LogError("%s: %s\n", __func__, state.ToString());
|
||||
return false;
|
||||
@ -5253,6 +5235,7 @@ void ChainstateManager::CheckBlockIndex() const
|
||||
// are not yet validated.
|
||||
CChain best_hdr_chain;
|
||||
assert(m_best_header);
|
||||
assert(!(m_best_header->nStatus & BLOCK_FAILED_MASK));
|
||||
best_hdr_chain.SetTip(*m_best_header);
|
||||
|
||||
std::multimap<const CBlockIndex*, const CBlockIndex*> forward;
|
||||
@ -5366,6 +5349,8 @@ void ChainstateManager::CheckBlockIndex() const
|
||||
if (pindexFirstInvalid == nullptr) {
|
||||
// Checks for not-invalid blocks.
|
||||
assert((pindex->nStatus & BLOCK_FAILED_MASK) == 0); // The failed mask cannot be set for blocks without invalid parents.
|
||||
} else {
|
||||
assert(pindex->nStatus & BLOCK_FAILED_MASK); // Invalid blocks and their descendants must be marked as invalid
|
||||
}
|
||||
// Make sure m_chain_tx_count sum is correctly computed.
|
||||
if (!pindex->pprev) {
|
||||
@ -5379,6 +5364,8 @@ void ChainstateManager::CheckBlockIndex() const
|
||||
// block, and must be set if it is.
|
||||
assert((pindex->m_chain_tx_count != 0) == (pindex == snap_base));
|
||||
}
|
||||
// There should be no block with more work than m_best_header, unless it's known to be invalid
|
||||
assert((pindex->nStatus & BLOCK_FAILED_MASK) || pindex->nChainWork <= m_best_header->nChainWork);
|
||||
|
||||
// Chainstate-specific checks on setBlockIndexCandidates
|
||||
for (const Chainstate* c : {m_ibd_chainstate.get(), m_snapshot_chainstate.get()}) {
|
||||
|
@ -1037,28 +1037,10 @@ public:
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* In order to efficiently track invalidity of headers, we keep the set of
|
||||
* blocks which we tried to connect and found to be invalid here (ie which
|
||||
* were set to BLOCK_FAILED_VALID since the last restart). We can then
|
||||
* walk this set and check if a new header is a descendant of something in
|
||||
* this set, preventing us from having to walk m_block_index when we try
|
||||
* to connect a bad block and fail.
|
||||
*
|
||||
* While this is more complicated than marking everything which descends
|
||||
* from an invalid block as invalid at the time we discover it to be
|
||||
* invalid, doing so would require walking all of m_block_index to find all
|
||||
* descendants. Since this case should be very rare, keeping track of all
|
||||
* BLOCK_FAILED_VALID blocks in a set should be just fine and work just as
|
||||
* well.
|
||||
*
|
||||
* Because we already walk m_block_index in height-order at startup, we go
|
||||
* ahead and mark descendants of invalid blocks as FAILED_CHILD at that time,
|
||||
* instead of putting things in this set.
|
||||
*/
|
||||
std::set<CBlockIndex*> m_failed_blocks;
|
||||
|
||||
/** Best header we've seen so far (used for getheaders queries' starting points). */
|
||||
/** Best header we've seen so far for which the block is not known to be invalid
|
||||
(used, among others, for getheaders queries' starting points).
|
||||
In case of multiple best headers with the same work, it could point to any
|
||||
because CBlockIndexWorkComparator tiebreaker rules are not applied. */
|
||||
CBlockIndex* m_best_header GUARDED_BY(::cs_main){nullptr};
|
||||
|
||||
//! The total number of bytes available for us to use across all in-memory
|
||||
|
Reference in New Issue
Block a user