Skip to content

fix(validator): do not turn stale proposal re-validation into slashable offenses - #25201

Closed
spalladino wants to merge 1 commit into
merge-train/spartan-v5from
spl/a-1703-prevent-slow-proposal-validation-from-causing-false-slashing
Closed

fix(validator): do not turn stale proposal re-validation into slashable offenses#25201
spalladino wants to merge 1 commit into
merge-train/spartan-v5from
spl/a-1703-prevent-slow-proposal-validation-from-causing-false-slashing

Conversation

@spalladino

@spalladino spalladino commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

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

  • Split ProposalValidator into validate() (ingress behavior unchanged, including the receive-window check and peer penalization for genuinely-late proposals) and validateStableFields(), 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.
  • Downstream re-validation failures return a new proposal_revalidation_failed reason 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 a NoCommitteeError at processing time produced a slashable rejection. invalid_proposal stays slashable and now only comes from the structural checkpoint-consistency checks.
  • Reject results carry a machine-readable code for the failing sub-check, logged when a proposal is skipped, so any rejection can be traced to its cause.

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

…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
spalladino force-pushed the spl/a-1703-prevent-slow-proposal-validation-from-causing-false-slashing branch from 03c8ee0 to e86b6de Compare August 12, 2026 21:06
@spalladino

Copy link
Copy Markdown
Contributor Author

Closing in favor of #25207

@spalladino spalladino closed this Aug 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant