-
Notifications
You must be signed in to change notification settings - Fork 37.8k
Return BlockValidationState from ProcessNewBlock if CheckBlock/AcceptBlock fails #17479
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
Return BlockValidationState from ProcessNewBlock if CheckBlock/AcceptBlock fails #17479
Conversation
This is the first two commits from #16279. Thanks for your work, @TheBlueMatt! |
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers. ConflictsReviewers, this pull request conflicts with the following ones:
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. |
111d7b4
to
7186766
Compare
I've added a commit that deduplicates post-block-checking code, which makes the main commit in this PR smaller and (I hope) easier to understand. |
I've taken the final (behaviour change) commit from #16279 and PR'ed it as #17485. That gives some justification for this PR. If we get the anti-dos / mutation checks back from validation immediately, we can use that state to mark the block as no longer in-flight, rather than by indirectly checking that the block has been written to disk. Separating into two PRs makes this easier to review. This PR should result in no behaviour changes (modulo the |
I found the last commit in this PR (7186766) a bit hard to parse, so I split it up in my branch here: master...dongcarl:2019-11-processnewblock-early-return-redo-last. The main change in the last commit on this branch resides in f46102b on mine, and f46102b also retains the same commit message. Feel free to use/use only to review/not use. |
src/validation.h
Outdated
* a CValidationInterface (see validationinterface.h) - this will have its | ||
* BlockChecked method called whenever *any* block completes validation (note |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps I'm understanding wrong but this shouldn't apply anymore?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
BlockChecked is called for any block that we try to connect to the chain. See the call in ConnectTip()
:
- if the call to
ConnectBlock()
failed, then we'll callBlockChecked()
with a state that is NOTIsValid()
. - if the call to
ConnectBlock()
succeeded, then the block has been connected and is part of the main chain, and we'll callBlockChecked()
with a state that ISIsValid()
.
Note that we may never even call ConnectTip()
if we don't try to connect the block to the most work chain.
7186766
to
0bf87d2
Compare
Thanks for the branch @dongcarl ! I've reset to that and made a couple of minor comment changes. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Concept ACK, I think commit a49a153 can be made a bit better.
src/net_processing.cpp
Outdated
// This clears the block from mapBlockSource. | ||
BlockChecked(*pblock, state, connman); | ||
} else if (!new_block) { | ||
// Block was valid but we've seen it before. Clear it from mapBlockSource. | ||
LOCK(cs_main); | ||
::mapBlockSource.erase(pblock->GetHash()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can't this branch combined with BlockChecked
by passing new_block as param? Even wonder if we still need fNewBlock
in PNB, as BlockChecked
erase block from mapBlockSource
independently of validity/novelty
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
BlockChecked()
will only be called from BlockProcessed()
if the block failed CheckBlock()
/AcceptBlock()
. This else branch is catching the case where the block didn't fail, but we've seen it before.
If you think this can be cleaned up further, perhaps we can do it in a follow-up PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
src/net_processing.cpp
Outdated
* A block has been processed. If the block was failed anti-dos / mutation checks, then | ||
* call BlockChecked() to maybe punish peer. If this is the first time we've seen the block, | ||
* update the node's nLastBlockTime. Otherwise erase it from mapBlockSource. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The documentation strikes me as repetitive with the code. Perhaps it could be phrased differently such that if the code changes the comment doesn't need to be updated.
nit: s/was failed/failed
0bf87d2
to
e48152f
Compare
Thanks for the review @jkczyz . I've updated the function comment to be less redundant. |
e48152f
to
b2ee50d
Compare
rebased |
Concept ACK, will review after rebase. |
50048f4
to
cd4a987
Compare
cd4a987
to
6f1505d
Compare
rebased |
Took some time to prove to myself that there are no unintended behaviour changes. It wasn't obvious to me that 5e5d58c is trivially correct, so I'm most likely missing some context here. My understanding is that prior to 5e5d58c, However, it seems that after 5e5d58c, it is possible for both Here's the diff I used for testing, and it seems to fail at diff --git a/src/validation.cpp b/src/validation.cpp
index 524ae94b9a..3f5737fd35 100644
--- a/src/validation.cpp
+++ b/src/validation.cpp
@@ -3856,9 +3856,11 @@ bool ChainstateManager::ProcessNewBlock(const CChainParams& chainparams, const s
ret = ::ChainstateActive().AcceptBlock(pblock, dos_state, chainparams, &pindex, fForceProcessing, nullptr, fNewBlock);
}
if (!ret) {
+ assert(!dos_state.IsValid()); // We should always trigger BlockChecked in BlockProcessed in this codepath
return error("%s: AcceptBlock FAILED (%s)", __func__, dos_state.ToString());
}
}
+ assert(dos_state.IsValid()); // We should NOT trigger BlockChecked in BlockProcessed in this codepath
NotifyHeaderTip();
|
6f1505d
to
2adc864
Compare
The first commit has now been merged in #19533. I've rebased the remaining commits on master. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Almost ACK all commits except am still reviewing removal of the call to BlockChecked() in PNB in 372913c and other reasoning about this that @dongcarl states well in #17479 (comment). A few notes below. Code review, debug-built and ran tests at each commit; running a node on the branch ATM and observing behavior.
*/ | ||
bool ProcessNewBlock(const CChainParams& chainparams, const std::shared_ptr<const CBlock> pblock, bool fForceProcessing, bool* fNewBlock) LOCKS_EXCLUDED(cs_main); | ||
bool ProcessNewBlock(const CChainParams& chainparams, const std::shared_ptr<const CBlock> pblock, BlockValidationState& state, bool fForceProcessing, bool* fNewBlock) LOCKS_EXCLUDED(cs_main); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-
params order: can the
[out]
paramstate
be inserted after the[in]
params? -
should
@param[in] chainparams
be added to the doxygen comment? -
maybe I'm missing something obvious; any reason to not name
state
/dos_state
consistently throughout, while adding it?
/** | ||
* A block has been processed. Handle potential peer punishment and housekeeping. | ||
*/ | ||
void static BlockProcessed(CNode& pfrom, CConnman& connman, CBlock& pblock, BlockValidationState& state, bool new_block) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LOCK(cs_main); | ||
::mapBlockSource.erase(pblock.GetHash()); | ||
} else { | ||
// Block is valid and we haven't seen it before. set nLastBlockTime for this peer. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
372913c nits:
-
slightly clearer, more differentiating wording could be
s/we've/we have/
and
s/we haven't/we have not/
-
why is past tense used above ("block failed", "was valid") and here present tense ("is valid")
-
s/set/Set/
|
||
// CheckBlock() does not support multi-threaded block validation because CBlock::fChecked can cause data race. | ||
// Therefore, the following critical section must include the CheckBlock() call as well. | ||
LOCK(cs_main); | ||
|
||
// Ensure that CheckBlock() passes before calling AcceptBlock, as | ||
// belt-and-suspenders. | ||
bool ret = CheckBlock(*pblock, state, chainparams.GetConsensus()); | ||
bool ret = CheckBlock(*pblock, dos_state, chainparams.GetConsensus()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1dd78f4 Is there a reason to rename state
to dos_state
throughout this function? Doing so adds a fair amount of noise.
BlockValidationState state; // Only used to report errors, not invalidity - ignore it | ||
if (!::ChainstateActive().ActivateBestChain(state, chainparams, pblock)) | ||
return error("%s: ActivateBestChain failed (%s)", __func__, state.ToString()); | ||
BlockValidationState dummy_state; // Only used to report errors, not invalidity - ignore it |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
5f552a0 This change could be done in the same commit 1dd78f4 that adds state/dos_state
to the params. That said, is it strictly necessary? (a) The change adds noise and ISTM the comment and code position make it clear enough; (b) dummy is the kind of naming, like certain others (e.g. master/slave, whitelist/blacklist, etc.) that is perhaps a bit in decline nowadays and being deprecated, so perhaps no need to add it.
Co-authored-by: Carl Dong <contact@carldong.me>
This is a pure refactor commit. This commit enables the caller of ProcessNewBlock to access the final BlockValidationState passed around between CheckBlock(), AcceptBlock(), and BlockChecked() inside ProcessNewBlock(). This is useful because in a future commit, we will move the BlockChecked() call out of ProcessNewBlock(), and BlockChecked() still needs to be able to access the BlockValidationState. Co-authored-by: John Newbery <john@johnnewbery.com> Co-authored-by: Carl Dong <contact@carldong.me>
This is a pure refactor commit. Since BlockChecked() doesn't actually depend on all of PeerLogicValidation but just PeerLogicValidation's CConnman, we can make a standalone, static function that simply has an extra CConnman parameter and have the non-static version call the static one. This also means that, in a future commit, when we move the BlockChecked() call out of ProcessNewBlock(), the caller of ProcessNewBlock() can call BlockChecked() directly even if they only have a CConnman. Co-authored-by: John Newbery <john@johnnewbery.com> Co-authored-by: Carl Dong <contact@carldong.me>
…ProcessNewBlock Net processing now passes a BlockValidationState object into ProcessNewBlock(). If CheckBlock() or AcceptBlock() fails, then PNB returns to net processing without calling the (asynchronous) BlockChecked Validation Interface method. net processing can use the invalid BlockValidationState returned to punish peers. CheckBlock() and AcceptBlock() represent the DoS checks on a block (ie PoW and malleability). Net processing wants to know about those failed checks immediately and shouldn't have to wait on a callback. Other validation interface clients don't care about net processing submitting bogus malleated blocks to validation, so they don't need to be notified of BlockChecked. Furthermore, if PNB returns a valid BlockValidationState, we never need to try to process (non-malleated) copies of the block from other peers. That makes it much easier to move the best chain activation logic to a background thread in future work. Co-authored-by: John Newbery <john@johnnewbery.com> Co-authored-by: Carl Dong <contact@carldong.me>
This is a pure refactor commit. Co-authored-by: John Newbery <john@johnnewbery.com> Co-authored-by: Carl Dong <contact@carldong.me>
Co-authored-by: John Newbery <john@johnnewbery.com> Co-authored-by: Carl Dong <contact@carldong.me>
rebased |
2adc864
to
17170a5
Compare
🐙 This pull request conflicts with the target branch and needs rebase. Want to unsubscribe from rebase notifications on this pull request? Just convert this pull request to a "draft". |
I'm focused on other things right now, so I'm going to close this with the intention of opening it again at some point in the future. |
Net processing now passes a
BlockValidationState
object intoProcessNewBlock()
. IfCheckBlock()
orAcceptBlock()
fails, then PNB returns to net processing without calling the (asynchronous)BlockChecked
Validation Interface method. net processing can use the invalidBlockValidationState
returned to punish peers.CheckBlock()
andAcceptBlock()
represent the DoS checks on a block (ie PoW and malleability). Net processing wants to know about those failed checks immediately and shouldn't have to wait on a callback. Other validation interface clients don't care about net processing submitting bogus malleated blocks to validation, so they don't need to be notified ofBlockChecked
.Furthermore, if PNB returns a valid
BlockValidationState
, we never need to try to process (non-malleated) copies of the block from other peers. That makes it much easier to move the best chain activation logic to a background thread in future work.