Skip to content

Commit 7c0cfce

Browse files
committed
Merge bitcoin/bitcoin#31405: validation: stricter internal handling of invalid blocks
f6b782f doc: Improve m_best_header documentation (Martin Zumsande) ee673b9 validation: remove m_failed_blocks (Martin Zumsande) ed764ea validation: Add more checks to CheckBlockIndex() (Martin Zumsande) 9a70883 validation: in invalidateblock, calculate m_best_header right away (Martin Zumsande) 8e39f2d validation: in invalidateblock, mark children as invalid right away (Martin Zumsande) 4c29326 validation: cache all headers with enough PoW in invalidateblock (Martin Zumsande) 15fa5b5 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-ACK f6b782f achow101: reACK f6b782f ryanofsky: Code review ACK f6b782f with only minor code & comment updates TheCharlatan: Re-ACK f6b782f Tree-SHA512: 1bee324216eeee6af401abdb683abd098b18212833f9600dbc0a46244e634cb0e6f2a320c937a5675a12af7ec4a7d10fabc1db9e9bc0d9d0712e6e6ca72d084f
2 parents 851f540 + f6b782f commit 7c0cfce

File tree

2 files changed

+52
-83
lines changed

2 files changed

+52
-83
lines changed

src/validation.cpp

Lines changed: 48 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -2093,7 +2093,6 @@ void Chainstate::InvalidBlockFound(CBlockIndex* pindex, const BlockValidationSta
20932093
AssertLockHeld(cs_main);
20942094
if (state.GetResult() != BlockValidationResult::BLOCK_MUTATED) {
20952095
pindex->nStatus |= BLOCK_FAILED_VALID;
2096-
m_chainman.m_failed_blocks.insert(pindex);
20972096
m_blockman.m_dirty_blockindex.insert(pindex);
20982097
setBlockIndexCandidates.erase(pindex);
20992098
InvalidChainFound(pindex);
@@ -3672,7 +3671,7 @@ bool Chainstate::InvalidateBlock(BlockValidationState& state, CBlockIndex* pinde
36723671
// To avoid walking the block index repeatedly in search of candidates,
36733672
// build a map once so that we can look up candidate blocks by chain
36743673
// work as we go.
3675-
std::multimap<const arith_uint256, CBlockIndex *> candidate_blocks_by_work;
3674+
std::multimap<const arith_uint256, CBlockIndex*> highpow_outofchain_headers;
36763675

36773676
{
36783677
LOCK(cs_main);
@@ -3681,13 +3680,12 @@ bool Chainstate::InvalidateBlock(BlockValidationState& state, CBlockIndex* pinde
36813680
// We don't need to put anything in our active chain into the
36823681
// multimap, because those candidates will be found and considered
36833682
// as we disconnect.
3684-
// Instead, consider only non-active-chain blocks that have at
3685-
// least as much work as where we expect the new tip to end up.
3683+
// Instead, consider only non-active-chain blocks that score
3684+
// at least as good with CBlockIndexWorkComparator as the new tip.
36863685
if (!m_chain.Contains(candidate) &&
3687-
!CBlockIndexWorkComparator()(candidate, pindex->pprev) &&
3688-
candidate->IsValid(BLOCK_VALID_TRANSACTIONS) &&
3689-
candidate->HaveNumChainTxs()) {
3690-
candidate_blocks_by_work.insert(std::make_pair(candidate->nChainWork, candidate));
3686+
!CBlockIndexWorkComparator()(candidate, pindex->pprev) &&
3687+
!(candidate->nStatus & BLOCK_FAILED_MASK)) {
3688+
highpow_outofchain_headers.insert({candidate->nChainWork, candidate});
36913689
}
36923690
}
36933691
}
@@ -3736,15 +3734,42 @@ bool Chainstate::InvalidateBlock(BlockValidationState& state, CBlockIndex* pinde
37363734
m_blockman.m_dirty_blockindex.insert(to_mark_failed);
37373735
}
37383736

3739-
// Add any equal or more work headers to setBlockIndexCandidates
3740-
auto candidate_it = candidate_blocks_by_work.lower_bound(invalid_walk_tip->pprev->nChainWork);
3741-
while (candidate_it != candidate_blocks_by_work.end()) {
3742-
if (!CBlockIndexWorkComparator()(candidate_it->second, invalid_walk_tip->pprev)) {
3743-
setBlockIndexCandidates.insert(candidate_it->second);
3744-
candidate_it = candidate_blocks_by_work.erase(candidate_it);
3745-
} else {
3746-
++candidate_it;
3737+
// Mark out-of-chain descendants of the invalidated block as invalid
3738+
// (possibly replacing a pre-existing BLOCK_FAILED_VALID with BLOCK_FAILED_CHILD)
3739+
// Add any equal or more work headers that are not invalidated to setBlockIndexCandidates
3740+
// Recalculate m_best_header if it became invalid.
3741+
auto candidate_it = highpow_outofchain_headers.lower_bound(invalid_walk_tip->pprev->nChainWork);
3742+
3743+
const bool best_header_needs_update{m_chainman.m_best_header->GetAncestor(invalid_walk_tip->nHeight) == invalid_walk_tip};
3744+
if (best_header_needs_update) {
3745+
// pprev is definitely still valid at this point, but there may be better ones
3746+
m_chainman.m_best_header = invalid_walk_tip->pprev;
3747+
}
3748+
3749+
while (candidate_it != highpow_outofchain_headers.end()) {
3750+
CBlockIndex* candidate{candidate_it->second};
3751+
if (candidate->GetAncestor(invalid_walk_tip->nHeight) == invalid_walk_tip) {
3752+
// Children of failed blocks should be marked as BLOCK_FAILED_CHILD instead.
3753+
candidate->nStatus &= ~BLOCK_FAILED_VALID;
3754+
candidate->nStatus |= BLOCK_FAILED_CHILD;
3755+
m_blockman.m_dirty_blockindex.insert(candidate);
3756+
// If invalidated, the block is irrelevant for setBlockIndexCandidates
3757+
// and for m_best_header and can be removed from the cache.
3758+
candidate_it = highpow_outofchain_headers.erase(candidate_it);
3759+
continue;
3760+
}
3761+
if (!CBlockIndexWorkComparator()(candidate, invalid_walk_tip->pprev) &&
3762+
candidate->IsValid(BLOCK_VALID_TRANSACTIONS) &&
3763+
candidate->HaveNumChainTxs()) {
3764+
setBlockIndexCandidates.insert(candidate);
3765+
// Do not remove candidate from the highpow_outofchain_headers cache, because it might be a descendant of the block being invalidated
3766+
// which needs to be marked failed later.
3767+
}
3768+
if (best_header_needs_update &&
3769+
m_chainman.m_best_header->nChainWork < candidate->nChainWork) {
3770+
m_chainman.m_best_header = candidate;
37473771
}
3772+
++candidate_it;
37483773
}
37493774

37503775
// 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
37663791
pindex->nStatus |= BLOCK_FAILED_VALID;
37673792
m_blockman.m_dirty_blockindex.insert(pindex);
37683793
setBlockIndexCandidates.erase(pindex);
3769-
m_chainman.m_failed_blocks.insert(pindex);
37703794
}
37713795

37723796
// If any new blocks somehow arrived while we were disconnecting
@@ -3837,7 +3861,6 @@ void Chainstate::ResetBlockFailureFlags(CBlockIndex *pindex) {
38373861
// Reset invalid block marker if it was pointing to one of those.
38383862
m_chainman.m_best_invalid = nullptr;
38393863
}
3840-
m_chainman.m_failed_blocks.erase(&block_index);
38413864
}
38423865
}
38433866

@@ -3846,7 +3869,6 @@ void Chainstate::ResetBlockFailureFlags(CBlockIndex *pindex) {
38463869
if (pindex->nStatus & BLOCK_FAILED_MASK) {
38473870
pindex->nStatus &= ~BLOCK_FAILED_MASK;
38483871
m_blockman.m_dirty_blockindex.insert(pindex);
3849-
m_chainman.m_failed_blocks.erase(pindex);
38503872
}
38513873
pindex = pindex->pprev;
38523874
}
@@ -4344,45 +4366,6 @@ bool ChainstateManager::AcceptBlockHeader(const CBlockHeader& block, BlockValida
43444366
LogDebug(BCLog::VALIDATION, "%s: Consensus::ContextualCheckBlockHeader: %s, %s\n", __func__, hash.ToString(), state.ToString());
43454367
return false;
43464368
}
4347-
4348-
/* Determine if this block descends from any block which has been found
4349-
* invalid (m_failed_blocks), then mark pindexPrev and any blocks between
4350-
* them as failed. For example:
4351-
*
4352-
* D3
4353-
* /
4354-
* B2 - C2
4355-
* / \
4356-
* A D2 - E2 - F2
4357-
* \
4358-
* B1 - C1 - D1 - E1
4359-
*
4360-
* In the case that we attempted to reorg from E1 to F2, only to find
4361-
* C2 to be invalid, we would mark D2, E2, and F2 as BLOCK_FAILED_CHILD
4362-
* but NOT D3 (it was not in any of our candidate sets at the time).
4363-
*
4364-
* In any case D3 will also be marked as BLOCK_FAILED_CHILD at restart
4365-
* in LoadBlockIndex.
4366-
*/
4367-
if (!pindexPrev->IsValid(BLOCK_VALID_SCRIPTS)) {
4368-
// The above does not mean "invalid": it checks if the previous block
4369-
// hasn't been validated up to BLOCK_VALID_SCRIPTS. This is a performance
4370-
// optimization, in the common case of adding a new block to the tip,
4371-
// we don't need to iterate over the failed blocks list.
4372-
for (const CBlockIndex* failedit : m_failed_blocks) {
4373-
if (pindexPrev->GetAncestor(failedit->nHeight) == failedit) {
4374-
assert(failedit->nStatus & BLOCK_FAILED_VALID);
4375-
CBlockIndex* invalid_walk = pindexPrev;
4376-
while (invalid_walk != failedit) {
4377-
invalid_walk->nStatus |= BLOCK_FAILED_CHILD;
4378-
m_blockman.m_dirty_blockindex.insert(invalid_walk);
4379-
invalid_walk = invalid_walk->pprev;
4380-
}
4381-
LogDebug(BCLog::VALIDATION, "header %s has prev block invalid: %s\n", hash.ToString(), block.hashPrevBlock.ToString());
4382-
return state.Invalid(BlockValidationResult::BLOCK_INVALID_PREV, "bad-prevblk");
4383-
}
4384-
}
4385-
}
43864369
}
43874370
if (!min_pow_checked) {
43884371
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,
45074490

45084491
if (!CheckBlock(block, state, params.GetConsensus()) ||
45094492
!ContextualCheckBlock(block, state, *this, pindex->pprev)) {
4510-
if (state.IsInvalid() && state.GetResult() != BlockValidationResult::BLOCK_MUTATED) {
4511-
pindex->nStatus |= BLOCK_FAILED_VALID;
4512-
m_blockman.m_dirty_blockindex.insert(pindex);
4493+
if (Assume(state.IsInvalid())) {
4494+
ActiveChainstate().InvalidBlockFound(pindex, state);
45134495
}
45144496
LogError("%s: %s\n", __func__, state.ToString());
45154497
return false;
@@ -5253,6 +5235,7 @@ void ChainstateManager::CheckBlockIndex() const
52535235
// are not yet validated.
52545236
CChain best_hdr_chain;
52555237
assert(m_best_header);
5238+
assert(!(m_best_header->nStatus & BLOCK_FAILED_MASK));
52565239
best_hdr_chain.SetTip(*m_best_header);
52575240

52585241
std::multimap<const CBlockIndex*, const CBlockIndex*> forward;
@@ -5366,6 +5349,8 @@ void ChainstateManager::CheckBlockIndex() const
53665349
if (pindexFirstInvalid == nullptr) {
53675350
// Checks for not-invalid blocks.
53685351
assert((pindex->nStatus & BLOCK_FAILED_MASK) == 0); // The failed mask cannot be set for blocks without invalid parents.
5352+
} else {
5353+
assert(pindex->nStatus & BLOCK_FAILED_MASK); // Invalid blocks and their descendants must be marked as invalid
53695354
}
53705355
// Make sure m_chain_tx_count sum is correctly computed.
53715356
if (!pindex->pprev) {
@@ -5379,6 +5364,8 @@ void ChainstateManager::CheckBlockIndex() const
53795364
// block, and must be set if it is.
53805365
assert((pindex->m_chain_tx_count != 0) == (pindex == snap_base));
53815366
}
5367+
// There should be no block with more work than m_best_header, unless it's known to be invalid
5368+
assert((pindex->nStatus & BLOCK_FAILED_MASK) || pindex->nChainWork <= m_best_header->nChainWork);
53825369

53835370
// Chainstate-specific checks on setBlockIndexCandidates
53845371
for (const Chainstate* c : {m_ibd_chainstate.get(), m_snapshot_chainstate.get()}) {

src/validation.h

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1037,28 +1037,10 @@ class ChainstateManager
10371037
}
10381038

10391039

1040-
/**
1041-
* In order to efficiently track invalidity of headers, we keep the set of
1042-
* blocks which we tried to connect and found to be invalid here (ie which
1043-
* were set to BLOCK_FAILED_VALID since the last restart). We can then
1044-
* walk this set and check if a new header is a descendant of something in
1045-
* this set, preventing us from having to walk m_block_index when we try
1046-
* to connect a bad block and fail.
1047-
*
1048-
* While this is more complicated than marking everything which descends
1049-
* from an invalid block as invalid at the time we discover it to be
1050-
* invalid, doing so would require walking all of m_block_index to find all
1051-
* descendants. Since this case should be very rare, keeping track of all
1052-
* BLOCK_FAILED_VALID blocks in a set should be just fine and work just as
1053-
* well.
1054-
*
1055-
* Because we already walk m_block_index in height-order at startup, we go
1056-
* ahead and mark descendants of invalid blocks as FAILED_CHILD at that time,
1057-
* instead of putting things in this set.
1058-
*/
1059-
std::set<CBlockIndex*> m_failed_blocks;
1060-
1061-
/** Best header we've seen so far (used for getheaders queries' starting points). */
1040+
/** Best header we've seen so far for which the block is not known to be invalid
1041+
(used, among others, for getheaders queries' starting points).
1042+
In case of multiple best headers with the same work, it could point to any
1043+
because CBlockIndexWorkComparator tiebreaker rules are not applied. */
10621044
CBlockIndex* m_best_header GUARDED_BY(::cs_main){nullptr};
10631045

10641046
//! The total number of bytes available for us to use across all in-memory

0 commit comments

Comments
 (0)