-
Notifications
You must be signed in to change notification settings - Fork 37.7k
[Refactor] CValidation State #11523
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
[Refactor] CValidation State #11523
Conversation
Replace state.DoS with state.Invalid where possible Replace state.DoS with more descriptive calls where straightforward Replace all remaining uses of state.DoS and state.Invalid with more descriptive calls Convert all nDoS usage to an enum class with named levels Remove dead CValidationState::Invalid fn Clean up one use of NonStandardTx to use default args Use a custom enum class for reporting corruption to make it more clear where corruption occurs CValidationState: Remove rejectreason default argument Drop useless ret parameter from CValidationState::DoS return false directly from CValidtationState update call sites
6cf5d00
to
863748b
Compare
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.
Another possibility is to return the validation state, for instance:
CValidationState& CheckTransaction(...)
and add a CValidationState::operator bool()
.
Even the methods like BadTx
could return the CValidationState&
so this could be possible:
return state.Critical().Reject("reason", code).Debug("message");
// or
return state.BadTx("bad-txns-vin-empty").Medium();
Either way, this patch could use the new code style.
/** Too high fee. Can not be triggered by P2P transactions */ | ||
static const unsigned int REJECT_HIGHFEE = 0x100; | ||
|
||
enum class DoS_SEVERITY : int { |
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.
enum class DoSSeverity
if (tx.vout.empty()) | ||
return state.DoS(10, false, REJECT_INVALID, "bad-txns-vout-empty"); | ||
if (tx.vin.empty()) { | ||
state.BadTx("bad-txns-vin-empty", "", DoS_SEVERITY::MEDIUM); |
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.
You could overload BadTx
(and others too) to avoid the empty string:
void BadTx(const std::string& reject_reason, const std::string& debug_message="", DoSSeverity level=DoSSeverity::CRITICAL, unsigned int reject_code=REJECT_INVALID);
void BadTx(const std::string& reject_reason, DoSSeverity level=DoSSeverity::CRITICAL, unsigned int reject_code=REJECT_INVALID);
Nit: also note the style change.
@promag Thanks for the review! I like your suggestion returning the class and calling modifiers on it -- not a fan of the use of default arguments -- but I think that it adds a bit more complexity to this patch that I'd like to avoid because I think that it makes it more difficult for future PRs working to separate this interface further. The reason why I didn't do an |
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.
ACK 863748b Seems like a good improvement.
const std::string &_strDebugMessage="", CORRUPTION_POSSIBLE corrupted=CORRUPTION_POSSIBLE::False, | ||
DoS_SEVERITY level=DoS_SEVERITY::NONE) { | ||
DoS(level, REJECT_NONSTANDARD, _strRejectReason, corrupted, _strDebugMessage); | ||
} |
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.
Each of CorruptBlockHeader, BadBlock, CorruptBlock, CorruptTx and NonStandardTx have all their callers leave level as the default, so it could be dropped as a parameter and hardcoded.
void DuplicateData(const std::string &_strRejectReason, | ||
const std::string &_strDebugMessage="") { | ||
DoS(DoS_SEVERITY::NONE, REJECT_DUPLICATE, _strRejectReason, CORRUPTION_POSSIBLE::False, _strDebugMessage); | ||
} |
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.
None of the calls to DuplicateData make use of strDebugMessage, so it could also be dropped as a param and hardcoded.
state.BadTx("bad-txns-spends-conflicting-tx", strprintf("%s spends conflicting transaction %s", | ||
hash.ToString(), | ||
hashAncestor.ToString()), DoS_SEVERITY::MEDIUM); | ||
return false; |
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.
Nit: old indentation of strprintf() args was easiers to follow...
return state.DoS(100, false, REJECT_INVALID, "bad-txns-duplicate", true, "duplicate transaction"); | ||
if (mutated) { | ||
state.CorruptTx("bad-txns-duplicate", "duplicate transaction"); | ||
return false; |
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.
Would have expected this to be CorruptBlock?
strprintf("Transaction check failed (tx hash %s) %s", tx->GetHash().ToString(), state.GetDebugMessage())); | ||
if (!CheckTransaction(*tx, state, false)) { | ||
state.SetDebugMessage(strprintf("Transaction check failed (tx hash %s) %s", tx->GetHash().ToString(), state.GetDebugMessage())); | ||
return false; |
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.
Could use a comment noting "state filled in by CheckTransaction" as per AcceptToMemoryPoolWorker
REJECT_INVALID, "bad-txns-accumulated-fee-outofrange"); | ||
error("%s: accumulated fee in the block out of range.", __func__); | ||
state.BadTx("bad-txns-accumulated-fee-outofrange"); | ||
return false; |
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.
Shouldn't this be BadBlock? I don't see how'd you reach it without hitting a BadTx first though...
strprintf("%d > %d", nFees, nAbsurdFee)); | ||
if (nAbsurdFee && nFees > nAbsurdFee) { | ||
state.RejectFee( REJECT_HIGHFEE, "absurdly-high-fee", strprintf("%d > %d", nFees, nAbsurdFee)); | ||
return false; |
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.
Nit: space after paren
Thanks, |
My 2 cents, just ideas and opinions:
|
concept ACK |
@Empact, thanks for the review. The motivation for the code changes I made is that the DoS code should eventually be completely relegated to a net_processing construct, whereas the validity code should be handled in the validation. So the goal is to make the code in validation.h as abstract as possible and descriptive of what went wrong, providing a reason, rather than ascribing a Denial of Service level. The extra lines for 'return false' don't have cost other than LoC, and the impact there is minimal. What they do help with is stricter modularization boundaries (should failure-reason code be returning information on validity?). Also returning a value from a function that always returns false is kind of stupid and makes the code harder to read. I'm indifferent on severity levels ordering. |
Fair enough. For severity levels, if you're indifferent I'd default to maintaining consistency with the prior behavior, which is using them as the leading param. |
Hmm... now that I've thought about it a bit again, I think there is value to having it be last... because with the current API we're focusing on describing what happened, not how we should treat it for DoS. Eventually, the severity param could be completely dropped, because in validity there is only a notion of valid or invalid, and not 'how invalid' something was. |
👍 I chatted with Matt out of band a bit ago and agree with waiting on #11639. |
…sing 0ff1c2a Separate reason for premature spends (coinbase/locktime) (Suhas Daftuar) 54470e7 Assert validation reasons are contextually correct (Suhas Daftuar) 2120c31 [refactor] Update some comments in validation.cpp as we arent doing DoS there (Matt Corallo) 12dbdd7 [refactor] Drop unused state.DoS(), state.GetDoS(), state.CorruptionPossible() (Matt Corallo) aa502b8 scripted-diff: Remove DoS calls to CValidationState (Matt Corallo) 7721ad6 [refactor] Prep for scripted-diff by removing some \ns which annoy sed. (Matt Corallo) 5e78c57 Allow use of state.Invalid() for all reasons (Matt Corallo) 6b34bc6 Fix handling of invalid headers (Suhas Daftuar) ef54b48 [refactor] Use Reasons directly instead of DoS codes (Matt Corallo) 9ab2a04 CorruptionPossible -> BLOCK_MUTATED (Matt Corallo) 6e55b29 CorruptionPossible -> TX_WITNESS_MUTATED (Matt Corallo) 7df16e7 LookupBlockIndex -> CACHED_INVALID (Matt Corallo) c8b0d22 [refactor] Drop redundant nDoS, corruptionPossible, SetCorruptionPossible (Matt Corallo) 34477cc [refactor] Add useful-for-dos "reason" field to CValidationState (Matt Corallo) 6a7f877 Ban all peers for all block script failures (Suhas Daftuar) 7b99910 Clean up banning levels (Matt Corallo) b8b4c80 [refactor] drop IsInvalid(nDoSOut) (Matt Corallo) 8818729 [refactor] Refactor misbehavior ban decisions to MaybePunishNode() (Matt Corallo) 00e11e6 [refactor] rename stateDummy -> orphan_state (Matt Corallo) f34fa71 Drop obsolete sigops comment (Matt Corallo) Pull request description: This is a rebase of #11639 with some fixes for the last few comments which were not yet addressed. The original PR text, with some strikethroughs of text that is no longer correct: > This cleans up an old main-carryover - it made sense that main could decide what DoS scores to assign things because the DoS scores were handled in a different part of main, but now validation is telling net_processing what DoS scores to assign to different things, which is utter nonsense. Instead, we replace CValidationState's nDoS and CorruptionPossible with a general ValidationInvalidReason, which net_processing can handle as it sees fit. I keep the behavior changes here to a minimum, but in the future we can utilize these changes for other smarter behavior, such as disconnecting/preferring to rotate outbound peers based on them providing things which are invalid due to SOFT_FORK because we shouldn't ban for such cases. > > This is somewhat complementary with, though obviously conflicts heavily with #11523, which added enums in place of DoS scores, as well as a few other cleanups (which are still relevant). > > Compared with previous bans, the following changes are made: > > Txn with empty vin/vout or null prevouts move from 10 DoS > points to 100. > Loose transactions with a dependency loop now result in a ban > instead of 10 DoS points. > ~~BIP68-violation no longer results in a ban as it is SOFT_FORK.~~ > ~~Non-SegWit SigOp violation no longer results in a ban as it > considers P2SH sigops and is thus SOFT_FORK.~~ > ~~Any script violation in a block no longer results in a ban as > it may be the result of a SOFT_FORK. This should likely be > fixed in the future by differentiating between them.~~ > Proof of work failure moves from 50 DoS points to a ban. > Blocks with timestamps under MTP now result in a ban, blocks > too far in the future continue to not result in a ban. > Inclusion of non-final transactions in a block now results in a > ban instead of 10 DoS points. Note: The change to ban all peers for consensus violations is actually NOT the change I'd like to make -- I'd prefer to only ban outbound peers in those situations. The current behavior is a bit of a mess, however, and so in the interests of advancing this PR I tried to keep the changes to a minimum. I plan to revisit the behavior in a followup PR. EDIT: One reviewer suggested I add some additional context for this PR: > The goal of this work was to make net_processing aware of the actual reasons for validation failures, rather than just deal with opaque numbers instructing it to do something. > > In the future, I'd like to make it so that we use more context to decide how to punish a peer. One example is to differentiate inbound and outbound peer misbehaviors. Another potential example is if we'd treat RECENT_CONSENSUS_CHANGE failures differently (ie after the next consensus change is implemented), and perhaps again we'd want to treat some peers differently than others. ACKs for commit 0ff1c2: jnewbery: utACK 0ff1c2a ryanofsky: utACK 0ff1c2a. Only change is dropping the first commit (f3883a3), and dropping the temporary `assert(level == GetDoS())` that was in 35ee77f (now c8b0d22) Tree-SHA512: e915a411100876398af5463d0a885920e44d473467bb6af991ef2e8f2681db6c1209bb60f848bd154be72d460f039b5653df20a6840352c5f7ea5486d9f777a3
This PR is a refactor only (i.e., any functional changes should be reported in review) which does the following:
state.DoS
with more descriptive calls where straightforward. Rather than just call DoS or Invalid, different causes of invalidity have different functions. This makes it easier to quickly find all related causes of that class of error.nDoS
usage to an enum class with named levels (none 0, low 1, medium 10, elevated 20, high 50, and critical 100)CValidtationState
update call sites. state.DoS never returns true, so it makes it easier to see that the return value is not dependent on the call.error()
as an argument to a function in DoS. error always returns false, and this is confusing for readers/reviewers.If anyone is interested, there's an unsquashed version too, but I figured this is simple enough to review squashed.
The only code quality 'decrease' is that some reject codes move from
validation.h
toconsensus/validation.h
. This abstraction barrier violation is already present (theCValidationState
class is expected to handle those reject codes appropriately) so I think that this change is a lesser evil.Motivation
I'm currently working on reworking the separation between reporting errors and DoS. As a first step in this process, I've cleaned up the interface without making any functional changes or major architecture changes. The resulting code should be easier to read and review changes to. The future plan to split
CValidationState
up into ~3 different subclasses (one to handle DoS, one to handle consensus correctness, and one to handle system errors) and then migrate the DoS completely tonet_processing.cpp
. This is a superior architecture because it better respects the boundaries between events on the network and faults in validation. I decided to start with this PR because I think it is an low hanging fruit immediate improvement independent of further modularization efforts.Thanks to @ryanofsky and @TheBlueMatt for feedback on an earlier version of this PR.