Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

validation: stricter internal handling of invalid blocks #31405

Open
wants to merge 5 commits into
base: master
Choose a base branch
from

Conversation

mzumsande
Copy link
Contributor

@mzumsande mzumsande commented Dec 2, 2024

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 p2p: Implement anti-DoS headers sync #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, two more caches were added to the RPC-only InvalidateBlock() similar to the existing candidate_blocks_by_work for setBlockIndexCandidates. 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.

In this scenario we have stored a valid header with an invalid (but not
mutated!) block, so it can only be triggered by doing the
necessary PoW.
The added call ensures that descendant blocks failure flags and m_best_header
will be set correctly - at the cost of looping over the entire block
index, which, because of the mentioned necessary PoW, is not a DoS
vector.
@DrahtBot
Copy link
Contributor

DrahtBot commented Dec 2, 2024

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/31405.

Reviews

See the guideline for information on the review process.
A summary of reviews will appear here.

Conflicts

Reviewers, this pull request conflicts with the following ones:

  • #30479 (validation: Add eligible ancestors of reconsidered block to setBlockIndexCandidates by mzumsande)
  • #30342 (kernel, logging: Pass Logger instances to kernel objects by ryanofsky)

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.

Before, they would be marked as invalid only after disconnecting
multiple blocks, letting go of cs_main in the meantime.
This is in preparation for adding a check to
CheckBlockIndex() requiring that descendants of invalid block indexes
are always marked as invalid.
Before, m_best_header would be calculated only after disconnecting
multiple blocks, letting go of cs_main in the meantime.
This is in preparation for adding checks to CheckBlockIndex()
requiring that m_best_header is the most-work header not known to be invalid.
This adds checks that
1) Descendants of invalid block indexes are also marked invalid
2) m_best_header cannot be invalid, and there can be no valid
block with more work than it.
We now mark all blocks that descend from an invalid block
immediately as the invalid block is encountered
(by iterating over the entire block index).
As a result, m_failed_blocks, which was a heuristic to only
mark descendants of failed blocks as failed when necessary,
(i.e., when we have do decide whether to add another descendant or not)
is no longer required.
@mzumsande mzumsande force-pushed the 202411_stricter_invalidblock_handling branch from ce3dc73 to b2136da Compare December 2, 2024 20:47
@DrahtBot
Copy link
Contributor

DrahtBot commented Dec 2, 2024

🚧 At least one of the CI tasks failed.
Debug: https://github.com/bitcoin/bitcoin/runs/33808912013

Hints

Try to run the tests locally, according to the documentation. However, a CI failure may still
happen due to a number of reasons, for example:

  • Possibly due to a silent merge conflict (the changes in this pull request being
    incompatible with the current code in the target branch). If so, make sure to rebase on the latest
    commit of the target branch.

  • A sanitizer issue, which can only be found by compiling with the sanitizer and running the
    affected test.

  • An intermittent issue.

Leave a comment here, if you need help tracking down a confusing failure.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants