validation: invalid block handling followups #32843

pull mzumsande wants to merge 2 commits into bitcoin:master from mzumsande:202506_invalid_blocks_followup changing 2 files +21 −13
  1. mzumsande commented at 10:10 pm on June 30, 2025: contributor

    Some follow-ups from #31405:

    • avoid duplicate recalculation of failure flags and m_best_header in InvalidateBlock(), which already does the accounting for these quantities while disconnecting blocks, so recalcuation is not needed in the final InvalidChainFound call (https://github.com/bitcoin/bitcoin/pull/31405#discussion_r2138368309)
    • make it clearer that all block indexes with a CBlockIndexWorkComparator score at least as good as the tip (and no others) are expected in setBlockIndexCandidates. People think of nChainWork when the term work is used and not of the (slightly arbitrary) CBlockIndexWorkComparator tiebreaker rules, which makes the current documentation imprecise ( #31405 (review) )
    • add a comment that nChainWork, not CBlockIndexWorkComparator is used for m_best_header (https://github.com/bitcoin/bitcoin/pull/31405#discussion_r2138368441)
  2. validation: Don't repeat work in InvalidateBlock
    In InvalidateBlock, both the block failure flags
    and m_best_header are kept up to date with each block
    that is disconnected, so it's not necessary to recalculate
    them at the end of this function.
    30b190eeba
  3. DrahtBot added the label Validation on Jun 30, 2025
  4. DrahtBot commented at 10:10 pm on June 30, 2025: contributor

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    Code Coverage & Benchmarks

    For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/32843.

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    ACK stratospher, ryanofsky

    If your review is incorrectly listed, please react with 👎 to this comment and the bot will ignore it on the next update.

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #32950 (validation: remove BLOCK_FAILED_CHILD by stratospher)

    If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

  5. doc: Improve doc for candidate blocks
    It is not true in general that all blocks with the same work
    (as defined via nChainWork) as the tip are candidates expected to be in
    setBlockIndexCandidates - only ones with a better tiebreak than the tip.
    Make that clear in various spots.
    
    Also explain that we don't use CBlockIndexWorkComparator tiebreak rules
    for m_best_header.
    e32e72039f
  6. mzumsande force-pushed on Jun 30, 2025
  7. stratospher commented at 6:38 am on July 8, 2025: contributor

    In InvalidateBlock, both the block failure flags and m_best_header are kept up to date with each block that is disconnected, so it’s not necessary to recalculate them at the end of this function.

    maybe I’m missing something but even without the calc_flags_and_header=false flag, this means that the condition cannot hit true and RecalculateBestHeader() in InvalidChainFound() cannot be called right?

    1. to_mark_failed essentially tracks the last disconnected block which used to be part of current chain (m_chain)
    2. the possible values to_mark_failed can take are only the values which invalid_walk_tip can take (this line).
    3. we have very judiciously set m_best_header when invalid_walk_tip is invalid using related logic in:
    0best_header_needs_update{m_chainman.m_best_header->GetAncestor(invalid_walk_tip->nHeight) == invalid_walk_tip}
    
    1. so when we call InvalidChainFound() on to_mark_failed, even without passing the false flag, this condition will be false: (because to_mark_failed can only be invalid_walk_tip and we already handled that in 3.)
    0if (m_chainman.m_best_header != nullptr && m_chainman.m_best_header->GetAncestor(pindexNew->nHeight) == pindexNew) {
    
  8. mzumsande commented at 7:26 pm on July 8, 2025: contributor

    maybe I’m missing something but even without the calc_flags_and_header=false flag, this means that the condition cannot hit true and RecalculateBestHeader() in InvalidChainFound() cannot be called right?

    Yes, that’s true, good point! To phrase it in my words, the condition that checks whether m_best_header needs update will not be fulfilled, because m_best_header was already successfully updated.

    I guess that means we could, instead of calc_flags_and_header, just use a bool calc_flags (for which we can’t do a similar check). Do you think that would be clearer?

  9. stratospher commented at 5:35 pm on July 9, 2025: contributor

    I guess that means we could, instead of calc_flags_and_header, just use a bool calc_flags (for which we can’t do a similar check). Do you think that would be clearer?

    yes sounds good! (whenever you retouch next)

    Ah missed the fact that iteration of entire block index is done in SetBlockFailureFlags() as well. I originally thought that iteration of entire block index is done only for best header calculation and calc_flags_and_header wasn’t useful. Hence the confusion.

  10. stratospher approved
  11. stratospher commented at 7:17 am on July 10, 2025: contributor
    ACK e32e72039f02ea0b565e0ef5ab7fa40faeb65e4c.
  12. achow101 requested review from sipa on Oct 22, 2025
  13. achow101 requested review from ryanofsky on Oct 22, 2025
  14. achow101 requested review from stickies-v on Oct 22, 2025
  15. in src/validation.cpp:2067 in e32e72039f
    2063@@ -2064,15 +2064,18 @@ void Chainstate::CheckForkWarningConditions()
    2064 }
    2065 
    2066 // Called both upon regular invalid block discovery *and* InvalidateBlock
    2067-void Chainstate::InvalidChainFound(CBlockIndex* pindexNew)
    2068+void Chainstate::InvalidChainFound(CBlockIndex* pindexNew, bool calc_flags_and_header = true)
    


    stickies-v commented at 12:30 pm on October 24, 2025:
    Default values are best defined in the header file

    stickies-v commented at 1:16 pm on October 24, 2025:

    Apologies for mostly coming with stop energy here, but I think I’m approach NACK on 30b190eebab9df9273586e0a4b6462847883d9d1, and I would prefer dropping the commit. While the duplicated header calculation and flag setting is wasteful, I don’t think we should complicate our validation logic further for the very rare case that is invalidateblock.

    I don’t have a suggested alternative yet, but it seems to me that reorganizing this code into different, more natural functions might be an alternative worth exploring. I don’t conceptually understand why we have different functions for InvalidChainFound and InvalidBlockFound, and in practice it seems they exist for reasons unrelated to their difference in name. Similarly, SetBlockFailureFlags is a confusing function that doesn’t actually set that block’s failure flags.

    Even without an alternative, I would rather make no change than this change, I don’t think anything actually needs fixing here.


    ryanofsky commented at 5:11 pm on October 27, 2025:

    In commit “validation: Don’t repeat work in InvalidateBlock” (30b190eebab9df9273586e0a4b6462847883d9d1)

    re: #32843 (review)

    Apologies for mostly coming with stop energy here, but I think I’m approach NACK on 30b190e

    I tend to disagree and think the code is clearer with this change than without it. Without it, intent is not possible to discern. The code is doing the same work twice without an indication of why it is doing that. With this change, behavior actually makes sense and intent is clear.

    I do agree code could be reorganized better and made less fragile, but I don’t see the new code here as being more fragile than current code, just more explicit.

    I do share some concern about the default value, though, and I think it would be better if InvalidChainFound was not commenting on invalidateblock behavior, which could change at some point. So might suggest:

     0--- a/src/validation.cpp
     1+++ b/src/validation.cpp
     2@@ -2064,13 +2064,12 @@ void Chainstate::CheckForkWarningConditions()
     3 }
     4 
     5 // Called both upon regular invalid block discovery *and* InvalidateBlock
     6-void Chainstate::InvalidChainFound(CBlockIndex* pindexNew, bool calc_flags_and_header = true)
     7+void Chainstate::InvalidChainFound(CBlockIndex* pindexNew, bool calc_flags_and_header)
     8 {
     9     AssertLockHeld(cs_main);
    10     if (!m_chainman.m_best_invalid || pindexNew->nChainWork > m_chainman.m_best_invalid->nChainWork) {
    11         m_chainman.m_best_invalid = pindexNew;
    12     }
    13-    // Unnecessary in the invalidateblock case, which has its own accounting for the failure flags and best header.
    14     if (calc_flags_and_header) {
    15         SetBlockFailureFlags(pindexNew);
    16         if (m_chainman.m_best_header != nullptr && m_chainman.m_best_header->GetAncestor(pindexNew->nHeight) == pindexNew) {
    17@@ -2098,7 +2097,7 @@ void Chainstate::InvalidBlockFound(CBlockIndex* pindex, const BlockValidationSta
    18         pindex->nStatus |= BLOCK_FAILED_VALID;
    19         m_blockman.m_dirty_blockindex.insert(pindex);
    20         setBlockIndexCandidates.erase(pindex);
    21-        InvalidChainFound(pindex);
    22+        InvalidChainFound(pindex, /*calc_flags_and_header=*/true);
    23     }
    24 }
    25 
    26@@ -3376,7 +3375,7 @@ bool Chainstate::ActivateBestChainStep(BlockValidationState& state, CBlockIndex*
    27                 if (state.IsInvalid()) {
    28                     // The block violates a consensus rule.
    29                     if (state.GetResult() != BlockValidationResult::BLOCK_MUTATED) {
    30-                        InvalidChainFound(vpindexToConnect.front());
    31+                        InvalidChainFound(vpindexToConnect.front(), /*calc_flags_and_header=*/true);
    32                     }
    33                     state = BlockValidationState();
    34                     fInvalidFound = true;
    35@@ -3809,6 +3808,8 @@ bool Chainstate::InvalidateBlock(BlockValidationState& state, CBlockIndex* pinde
    36             }
    37         }
    38 
    39+        // Record invalid chain, avoiding recomputing m_best_header and applying
    40+        // BLOCK_FAILED_CHILD flags since these things were done above.
    41         InvalidChainFound(to_mark_failed, /*calc_flags_and_header=*/false);
    42     }
    43 
    

    Or if this commit will be dropped, I think there should at least be a comment added before the InvalidChainFound call in InvalidateBlock saying InvalidChainFound will repeat some work that was already before cs_main was released, to make it clear the duplication was at least not an accident.

  16. in src/validation.cpp:3771 in e32e72039f
    3767@@ -3768,6 +3768,7 @@ bool Chainstate::InvalidateBlock(BlockValidationState& state, CBlockIndex* pinde
    3768                 // Do not remove candidate from the highpow_outofchain_headers cache, because it might be a descendant of the block being invalidated
    3769                 // which needs to be marked failed later.
    3770             }
    3771+            // Best header updating currently doesn't use CBlockIndexWorkComparator, so nChainWork is used for consistency.
    


    ryanofsky commented at 5:34 pm on October 27, 2025:

    In commit “doc: Improve doc for candidate blocks” (e32e72039f02ea0b565e0ef5ab7fa40faeb65e4c)

    Would maybe s/nChainWork/only nChainWork/ since using nChainWork shouldn’t be surprising here, but only using nChainWork might be.


    ryanofsky commented at 5:52 pm on October 27, 2025:

    In commit “doc: Improve doc for candidate blocks” (e32e72039f02ea0b565e0ef5ab7fa40faeb65e4c)

    To be complete, this could also mention that when there is a tie, this will set m_best_header to whichever block happens to come first in m_block_index std::unordered_map iteration order. This ordering is consistent with the ordering used in RecalculateBestHeader, but inconsistent with the ordering used in AddToBlockIndex, which is based on the order headers are received.

    I’m not sure how much detail is necessary here, so also just making these notes for myself.

  17. in src/validation.cpp:3774 in e32e72039f
    3767@@ -3768,6 +3768,7 @@ bool Chainstate::InvalidateBlock(BlockValidationState& state, CBlockIndex* pinde
    3768                 // Do not remove candidate from the highpow_outofchain_headers cache, because it might be a descendant of the block being invalidated
    3769                 // which needs to be marked failed later.
    3770             }
    3771+            // Best header updating currently doesn't use CBlockIndexWorkComparator, so nChainWork is used for consistency.
    3772             if (best_header_needs_update &&
    3773                 m_chainman.m_best_header->nChainWork < candidate->nChainWork) {
    3774                 m_chainman.m_best_header = candidate;
    


    ryanofsky commented at 5:40 pm on October 27, 2025:

    In commit “doc: Improve doc for candidate blocks” (e32e72039f02ea0b565e0ef5ab7fa40faeb65e4c)

    Note for reviewers: best description I’ve seen for why it’s not currently safe to assume m_best_header is consistent with CBlockIndexWorkComparator ordering is here #31405 (review)

  18. ryanofsky approved
  19. ryanofsky commented at 6:01 pm on October 27, 2025: contributor
    Code review ACK e32e72039f02ea0b565e0ef5ab7fa40faeb65e4c. Thanks for these followups, I think they should make it easier to remember what this code is trying to do.

github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2025-11-09 21:13 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me