BlockValidationState currently carries two unrelated kinds of failure: consensus/policy invalidity which is the actual purpose and runtime errors via a M_ERROR mode in ModeState enum inside ValidationState class. This PR removes M_ERROR and routes runtime errors through util::Expected<T, std::string> instead, making the two failure modes distinct.
Motivation
BlockValidationState exists to describe why a block or transaction is invalid. It holds a consensus/policy reject reason. A disk write or system runtime error is not that, yet it was folded into the same object as a third state i.e. M_ERROR.
This conflation has two costs:
Wrong abstraction for non-validating functions.
FlushStateToDisk,DisconnectTip,ActivateBestChain,PreciousBlock, andInvalidateBlockdo no block validation, but each takes aBlockValidationStateout-param to report any runtime error.Three outcomes in one object. Functions that can both validate and hit a runtime error (
ConnectBlock,AcceptBlock,ProcessNewBlock) holds valid, invalid, and fatal into a singleBlockValidationState, forcing every caller to unwrap.
As noted in the original discussion here:
Functions like FlushStateToDisk, ActivateBestChain aren't exactly about validating a certain block, yet they take a BlockValidationState& out-param just to absorb runtime errors, which feels like the wrong abstraction.
And also discussion here to remove M_ERROR value.
- This would also pave a long term fix for #35570, which returns
BlockValidationStatefrom validation methods instead of boolean value.
util::Expected is a good option for this reason, it keeps the error handling in the same return value style (system/runtime errors and ValidationState for consensus correctness) and separates-out runtime errors from ValidationState.
Runtime errors could be signaled either by a return value or by throwing. This PR keeps them as return values as it's a minimal change. M_ERROR was already a return-value mechanism included in ValidationState with consensus verdict. This PR doesn't open the exceptions vs returns question, it keeps the existing return-value style and just moves the fatal error into a proper Expected channel.