fix(validator): do not turn stale proposal re-validation into slashable offenses - #25201
Closed
spalladino wants to merge 1 commit into
Closed
Conversation
…le offenses Block proposals are gated at p2p ingress on a wall-clock receive window. The validator-client re-ran the exact same validation downstream, so a proposal that arrived on time could fail the window check purely because this node started processing it late — and that rejection was reported as `invalid_proposal`, a slashable offense, manufacturing false evidence against an honest proposer. Split the validator so the time-of-check-sensitive part cannot be repeated: `ProposalValidator.validate` keeps the current ingress behavior, while a new `validateStableFields` runs every check that is a property of the signed payload (signature context, signature, expected proposer, block index) and omits the receive window. `BlockProposalValidator.validateStableFields` layers the full transaction-field validation on top, and the proposal handler calls that instead. Downstream re-validation failures now return a new `proposal_revalidation_failed` reason that is absent from the slashable set and counted as a node issue rather than a bad proposal. Once this node accepted a signed proposal on arrival, a contradictory re-check reflects a changed local view (committee lookup, clock, or config), never proposer misconduct. This also fixes a latent false-slash path where a `NoCommitteeError` at processing time produced a slashable rejection. `invalid_proposal` stays slashable; it now only comes from the structural checkpoint-consistency checks, which are properties of the payload. Reject results also carry a machine-readable code for the failing sub-check, logged when a proposal is skipped so a rejection can be traced to its cause.
spalladino
force-pushed
the
spl/a-1703-prevent-slow-proposal-validation-from-causing-false-slashing
branch
from
August 12, 2026 21:06
03c8ee0 to
e86b6de
Compare
Contributor
Author
|
Closing in favor of #25207 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
On mainnet (2026-08-11, slot 50415) a validator falsely accused an honest proposer of broadcasting an invalid block proposal, and the slot-wide invalid marker then implicated every attester of the slot. The block was canonical.
This was caused by a massive delay (already fixed) between the time we first validated the proposal on the p2p boundary, and we revalidated on the validator. The defense-in-depth revalidation (which we could remove in a future PR) now skips time-dependent checks.
Context
Block proposals are gated at p2p ingress on a wall-clock receive window, but the validator client re-ran the exact same validation downstream before re-execution. On the incident node, processing stalled ~50s behind the tx-pool serial queue held by epoch finalization (A-1656, fixed separately in #25148), so an on-time proposal failed the repeated window check purely because processing started late — and that rejection was collapsed into
invalid_proposal, a slashable offense. Local latency must never convert into slashing evidence against other validators.Approach
ProposalValidatorintovalidate()(ingress behavior unchanged, including the receive-window check and peer penalization for genuinely-late proposals) andvalidateStableFields(), which runs every check that is a property of the signed payload (signature context, signature, expected proposer, block index, transaction fields) and omits the time-of-check-sensitive window. The proposal handler now calls the stable variant, so arrival gating cannot be repeated after the fact.proposal_revalidation_failedreason that is absent from the slashable set and counted as a node issue in metrics: once this node accepted the exact signed payload at ingress, a contradictory re-check reflects a changed local view (clock, epoch-cache state, or option drift), never proposer misconduct. This also removes a latent false-slash path where aNoCommitteeErrorat processing time produced a slashable rejection.invalid_proposalstays slashable and now only comes from the structural checkpoint-consistency checks.The remaining attester-side issue (offenses keyed only by slot rather than by the signed checkpoint payload) is tracked in A-1711.
Fixes A-1703
Review guide with context, change summary, and pointers into the diff: https://claude.ai/code/artifact/e64d378d-1d4d-43f8-aa25-1de6e118ada3