Conversation
|
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers. Code Coverage & BenchmarksFor details see: https://corecheck.dev/bitcoin/bitcoin/pulls/35570. ReviewsSee the guideline and AI policy for information on the review process.
If your review is incorrectly listed, please copy-paste 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. LLM Linter (✨ experimental)Possible places where named args for integral literals may be used (e.g.
2026-09-19 05:15:48 |
cc4bac4 to
8963bb6
Compare
|
🚧 At least one of the CI tasks failed. HintsTry to run the tests locally, according to the documentation. However, a CI failure may still
Leave a comment here, if you need help tracking down a confusing failure. |
8963bb6 to
d7e6035
Compare
|
While working on this PR, one instance was identified where the invariant Options to resolve this: A. Since the return value is ignored, ignore the The ignoring of flush result (A.) is also proposed in #29700 (cc: @ryanofsky ). The minor behavior-change could be also omitted from this PR, limiting strictly to a no-behavior-change refactor:
|
d7e6035 to
a3a5c03
Compare
|
Concept ACK |
|
Concept ACK. Preferring the return value over output parameters is a useful and I would say necessary improment. But could we avoid using {
BlockValidationState state;
if (...) {
state.Invalid(...);
}
return state;
}I would prefer something like: {
if (...) {
return BlockValidationState::Invalid(...);
}
return BlockValidationState::Valid();
} |
I fully agree, shorter-scoped variables mean less chance of potential interplay between calls, less complexity. I did change this in several places, but not everywhere. In some places there was a real chance for interaction between subsequent calls (e.g. error reason is set conditional of already set value), and I kept it to be safe (w.r.t. behavior changes). But I should review and change it more aggressively. |
4626ef2 to
8207ac1
Compare
|
Applied some improvements:
|
There was a problem hiding this comment.
concept ACK 8207ac1bb79f76c9005e8e2d695b725539b3c5c0
Looks good overall. I'd push this one step further and use util::Expected here, it's a natural fit and follows the PR's own motivation. The PR already cleans up the bool + state out-param inconsistency by making state the return value; util::Expected<T, BlockValidationState> does the same thing but at the type level instead of by convention. Success and failure become distinct alternatives of the type.
Concretely, a bare BlockValidationState still carries all three modes (M_VALID/M_INVALID/M_ERROR) in the return value even though M_VALID is now redundant with "the call succeeded". With util::Expected that mode collapses into has_value() and is no longer stored, the failure modes move to the error channel, and out-params like ppindex fold into the success channel. util/result.h also explicitly steers low-level functions toward util::Expected.
// pindex folded into the success channel
[[nodiscard]] util::Expected<CBlockIndex*, BlockValidationError> AcceptBlockHeader(
const CBlockHeader& block, bool min_pow_checked);
auto res = AcceptBlockHeader(header, /*min_pow_checked=*/true);
if (!res) {
if (res.error().IsInvalid()) MaybePunishNodeForBlock(...); // peer's fault
return util::Unexpected{std::move(res).error()};
}
CBlockIndex* pindex = res.value(); // only reachable on successOne open question (happy to leave it as discussion): where does M_INVALID fit best? Above I put it in the error channel next to M_ERROR, but it's arguably a successful, expected outcome of validation, the function did run and produced a verdict ("this block is invalid"), whereas M_ERROR is a genuine runtime failure that prevented producing a result. So an alternative split would keep Valid/Invalid in the value channel and reserve the error channel for runtime errors only. I don't have a strong preference.
| * @param[out] ppindex If set, the pointer will be set to point to the last new block index object for the given headers | ||
| * @returns false if AcceptBlockHeader fails on any of the headers, true otherwise (including if headers were already known) | ||
| * @returns BlockValidationState indicating the result. IsValid() returns true if all headers | ||
| * were accepted. On failure, IsInvalid() is false and the state contains the specific |
There was a problem hiding this comment.
On failure, IsInvalid() is false
Would not be "true"?
There was a problem hiding this comment.
Good catch. On failure IsValid() is false, moreover IsInvalid() is true. I state: "On failure, IsValid() is false and the state contains the specific validation failure reason. Never returns Error state.". From the last bit it follows that IsInvalid() is true, but I don't mention it here, only IsValid(), as that should be used to check for normal vs. exceptional case.
|
Concept ACK
There is a thread in #33856 where this was also brought up. I included a POC in the last comment of that thread which keeps the value channel for In general, I think stripping the runtime failure from |
9819124 to
312c4e7
Compare
|
🚧 At least one of the CI tasks failed. HintsTry to run the tests locally, according to the documentation. However, a CI failure may still
Leave a comment here, if you need help tracking down a confusing failure. |
|
Fixed IWYU CI failure (missing include for new |
312c4e7 to
a92cd68
Compare
hodlinator
left a comment
There was a problem hiding this comment.
Reviewed a92cd68
The way this PR transforms pre-existing functions one at a time is something I wish #35646 did as well.
Agree that it complements #35646, but think it should go in after that one (see inline comment), so I'll defer my Concept A-C-K until something like that lands. Please don't see my reservations as disinterest, I spend time on it because I agree the general direction is worthwhile.
Branch incorporating my suggestions into existing commits: https://github.com/hodlinator/bitcoin/tree/pr/35570_suggestions
| public: | ||
| //! Factory helper method to create an Invalid BlockValidationState | ||
| static BlockValidationState InvalidState(BlockValidationResult result, | ||
| const std::string& reject_reason = "", const std::string& debug_message = "") |
There was a problem hiding this comment.
thread #35570 (comment):
Please also fix MakeError(const std::string& reject_reason = "").
There was a problem hiding this comment.
This can be dropped in the "Refactor CheckBlock signature" commit so we only assign state once.
There was a problem hiding this comment.
Indeed, MakeInvalid can be used, and then there is no need for the state variable, only further down. Done.
| return state; | ||
| } | ||
| //! Factory helper method to create an Error BlockValidationState | ||
| static BlockValidationState ErrorState(const std::string& reject_reason = "") |
There was a problem hiding this comment.
thread #35570 (comment):
What I meant is to add both MakeInvalid() and MakeError() helpers in the first commit. Then in the "Refactor FatalError signature" and "Refactor AcceptBlock signature" commits you can switch directly to MakeError() instead of the intermediate hop through state.Error() ... return state which those commits currently do.
| // Check that the header is valid (particularly PoW). This is mostly | ||
| // redundant with the call in AcceptBlockHeader. | ||
| if (!CheckBlockHeader(block, state, consensusParams, fCheckPOW)) | ||
| state = CheckBlockHeader(block, consensusParams, fCheckPOW); |
There was a problem hiding this comment.
thread #35570 (comment):
I know many checks away in later commits but still think it's good practice to add them in intermediate commits when you are changing behavior of such critical code.
At the very least the "test ancestor commits" CI job can churn through them.
You are changing the local code behavior from maybe changing state when passed as an argument, to definitely stomping it with a return value.
I went through and added Assumes in my suggestions branch and many of them survive until the end, although I admit some are overly paranoid.
| } | ||
|
|
||
| if (!CheckBlockHeader(block, state, GetConsensus())) { | ||
| if (const auto state = CheckBlockHeader(block, GetConsensus()); !state.IsValid()) { | ||
| assert(state.IsInvalid()); |
There was a problem hiding this comment.
thread #35570 (comment):
Following the call tree on the current PR base is good to do, but not the best we can do. Adding asserts and now comments about current behavior feels like duct tape to me.
As the PRs currently stand, I much prefer #35646 be merged before this PR so invariants are enforced at compile time through types rather than by the duct tape that is runtime failures and comments. That makes it much harder for silent merge conflicts and the like to cause issues.
There was a problem hiding this comment.
470b305 Internal simplification in TestBlockValidity:
This state should be removed if we keep this commit where we call CheckBlock() etc which return it's own instances instead of overwriting state. That means we also refactor the next if-block:
if (block.hashPrevBlock != *Assert(tip->phashBlock)) {
return BlockValidationState::MakeInvalid({}, "inconclusive-not-best-prevblk");
}That also necessitates re-introducing state before calling ConnectBlock(), unless that method is also refactored to return BlockValidationState:
{
BlockValidationState state;
// Set fJustCheck to true in order to update, and not clear, validation caches.
if (!chainstate.ConnectBlock(block, state, &index_dummy, view_dummy, /*fJustCheck=*/true)) {
if (state.IsValid()) NONFATAL_UNREACHABLE();
return state;
} else {
if (!state.IsValid()) NONFATAL_UNREACHABLE();
}
}There was a problem hiding this comment.
Realized BlockValidationState::MakeInvalid({}, ... triggers the Assume(result != BlockValidationResult::BLOCK_RESULT_UNSET), so might be best to skip adding the latter for now.
a92cd68 to
1de8a2e
Compare
optout21
left a comment
There was a problem hiding this comment.
New round of changes following new review comments.
| public: | ||
| //! Factory helper method to create an Invalid BlockValidationState | ||
| static BlockValidationState InvalidState(BlockValidationResult result, | ||
| const std::string& reject_reason = "", const std::string& debug_message = "") |
There was a problem hiding this comment.
Sorry, the fell through the cracks; done.
There was a problem hiding this comment.
Indeed, MakeInvalid can be used, and then there is no need for the state variable, only further down. Done.
| return state; | ||
| } | ||
| //! Factory helper method to create an Error BlockValidationState | ||
| static BlockValidationState ErrorState(const std::string& reject_reason = "") |
There was a problem hiding this comment.
Taken. Through reordering, the double touching of FatalError and AcceptBlock can be avoided.
| // Check that the header is valid (particularly PoW). This is mostly | ||
| // redundant with the call in AcceptBlockHeader. | ||
| if (!CheckBlockHeader(block, state, consensusParams, fCheckPOW)) | ||
| state = CheckBlockHeader(block, consensusParams, fCheckPOW); |
There was a problem hiding this comment.
You are right, in the previous form with input-output parameters method had the chance to overwrite the status, but did so only in error case. I've added the proposed Assume's in every place where we overwrite the status always. Some of these Assume's disappear in later commits, but not all.
Add `MakeInvalid` static helper to create invalid `BlockValidationState` instance, for easier construction in error branches. Instead of the 3 statements of declaration, setting, and return with the exisiting non-static method, now return&construction is possible in one statement.
Change the (internal) method `CheckBlockHeader` to return the validation result in the return value instead of an output parameter.
Change the (internal) method `ContextualCheckBlockHeader` to return the validation result in the return value instead of an output parameter.
Change the method `AcceptBlockHeader` to return the `BlockValidationState` validation result in the return value instead of an output parameter.
Return BlockValidationState by value instead of using an out-parameter, similar to the TestBlockValidity refactoring in 74690f4. Remove redundant int return from btck_chainstate_manager_process_block_header. Previously returned both an int result and an output validation state parameter, creating ambiguity where non-zero could mean either invalid header or processing failure. Since ProcessNewBlockHeaders already provides complete validation info, the int return was redundant. Co-authored-by: stringintech <[email protected]> Co-authored-by: stickies-v <[email protected]>
Change the (internal) method `CheckMerkleRoot` to return the `BlockValidationState` validation result in the return value instead of an output parameter.
|
Rebased ot master, hoping to fix the |
1de8a2e to
6294fb5
Compare
|
Test failure is due to this which creates an Invalid state with |
|
Small change to fix CI failure.
|
6294fb5 to
0e8dc6b
Compare
Change the method `CheckBlock` to return the `BlockValidationState` validation result in the return value instead of an output parameter.
Change the (internal) method `CheckWitnessMalleation` to return the `BlockValidationState` validation result in the return value instead of an output parameter.
Change the (internal) method `ContextualCheckBlock` to return the `BlockValidationState` validation result in the return value instead of an output parameter.
Change the method `FatalError` to return the constructed `BlockValidationState` object in a return value instead of an output parameter. Add a helper `MakeError` factory method to the `BlockValidationState`, similar to `MakeInvalid`. Co-authored-by: Íñigo Aréjula Aísa <[email protected]>
Change the method `FlushStateToDisk` to return the `BlockValidationState` validation result in the return value instead of an output parameter.
Change the method `AcceptBlock` to return the `BlockValidationState` validation result in the return value instead of an output parameter. Co-authored-by: Íñigo Aréjula Aísa <[email protected]>
0e8dc6b to
9e3c5ba
Compare
Summary. Refactor validation result to be a return value instead of an output parameter in several
validation.cppmethods.Motivation. The benefits of the change are:
stateare inconsistentThis has grown out from #33856, mentioned in comment here and here.
Details. Many methods follow the scheme where the validation state is returned in an output parameter (
BlockValidationState& state), and and additionalboolreturn value indicating success. In success case the convention is thatstate.IsValid()and the return value are both true. After the change there is only aBlockValidationStatereturn value, which is either success (state.IsValid() == true), or an invalid/error case.This change is a highly localized refactor, but touching a sensitive file.
Relevant methods called by
ProcessNewBlockHeadersandAcceptBlock(directly and indirectly) are touched.Changes are separated into commits by touched methods, ordered by bottom-to-top in the call hierarchy.