Skip to content

feat: merge-train/spartan-v5 - #25062

Merged
spalladino merged 3 commits into
v5-nextfrom
merge-train/spartan-v5
Aug 4, 2026
Merged

spalladino merged 3 commits into
v5-nextfrom
merge-train/spartan-v5

Conversation

@AztecBot

@AztecBot AztecBot commented Jul 30, 2026 •

Copy link
Copy Markdown
Collaborator

BEGIN_COMMIT_OVERRIDE
fix(validator): classify invalid proposal txs as slashable block failures (#25031)
feat(slasher): warn and expose metrics when own validators are slash targets (#25058)
END_COMMIT_OVERRIDE

…ures (#25031)

## Context

A selected proposer can attach signed txs to a block proposal that pass
the proposal-level checks (each tx hash is listed in `txHashes` and each
tx recomputes to its own hash) but fail minimum tx integrity validation
— bad proof, metadata, size, data, or contract instances. Tx collection
threw a bare `Error('Invalid tx detected')` out of the validator's
block-received callback before `handleBlockProposal` could return a
typed result, so `validateBlockProposal` never reached the branch that
emits `BROADCASTED_INVALID_BLOCK_PROPOSAL` and marks the slot invalid.
The proposer avoided invalid-block accountability, and observers missed
the invalid slot that the delayed attested-to-invalid-proposal watcher
relies on.

The same callback rejection also leaked tx-pool protections:
`processValidBlockProposal` protects the proposal's txs before awaiting
the callback but only unprotected them when the callback *returned*
false.

## Approach

- `validateTxsReceivedInBlockProposal` now throws a typed
`InvalidBlockProposalTxsError` carrying each offending tx hash and its
validation reasons, instead of a bare error.
- The proposal handler catches that error around tx collection (in a
`collectProposalTxs` helper) and returns a new `invalid_embedded_txs`
failure reason, which is in
`SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT`, so both the validator and
non-validator handlers emit the offense and mark the slot invalid —
still gated on the escape hatch being closed. Any other collection error
keeps propagating: only proposer misbehavior gets a typed, slashable
reason, so a local pool or network failure is not mistaken for an
invalid proposal. The error crosses a package boundary, so it is matched
with `isErrorClass` rather than `instanceof`, and sets its `name`
accordingly.
- `processValidBlockProposal` releases the protections it created when
the callback rejects, not just when it returns false. A
`tryBlockReceivedCallback` helper reports a throwing callback as a
rejection — the proposal is no more usable than one explicitly rejected,
and the error would otherwise escape with the protections still held —
leaving one release path for both outcomes. The outer gossip handler
only logged such errors anyway; one visible consequence is that an
unexpected error while processing a checkpoint's embedded block no
longer skips the checkpoint proposal itself.
- A proposal that lists the same tx hash twice is rejected with a new
slashable `duplicate_txs` reason. The repeated tx makes the block
unbuildable (it would emit nullifiers the first copy already emitted),
and tx collection reconciles a deduplicated hash set against the full
list, so a duplicate that is not available locally threw the invariant
error `Error collecting txs for proposal with N txs: found X and flagged
Y as missing` out of `getTxsForBlockProposal` — the same
unattributable-exception shape this PR removes. The check lives in the
proposal handler rather than in gossip validation: a duplicate hash is
proposer misbehavior, not a relaying-peer fault, so rejecting it at
ingress would penalize the forwarding peer and short-circuit before the
block-received callback, leaving the offense unrecorded. This mirrors
how an over-limit `indexWithinCheckpoint` is handled. The handler runs
the check before tx collection, so the invariant error is unreachable,
and the terminal block carried in a checkpoint proposal goes through the
same handler.

Slashing here is attributable to the proposer. The proposal is validated
for the right signing domain, slot window, and expected-proposer
signature before any tx is looked at, and `validateTxs` requires every
embedded tx to be listed in the signed `txHashes` with a self-consistent
hash. `BlockProposal.getSender()` additionally returns undefined unless
the embedded `SignedTxs` bundle shares the outer proposal's signature
context and recovers to the same signer. A relaying peer can strip txs
from the body, which surfaces as the non-slashable `txs_not_available`,
but cannot graft foreign txs into an honest proposer's proposal.

Two limitations worth noting, both pre-existing and left as-is:

- `TxProvider.extractFromProposal` only considers hashes missing from
the local pool, so a tx carried in the proposal whose hash is already
pooled is never integrity-checked. Detection is therefore
mempool-dependent. Closing that gap means proof-verifying every carried
tx on the attestation path, which is a separate performance call.
- `SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT`, `badProposalReasons` in
`validator.ts`, and the metric label seeds in `metrics.ts` are three
hand-maintained lists of the same taxonomy and have already drifted
(`global_variables_mismatch` is slashable but counted as a node issue).
The new reason is added to all three; consolidating them into one
exhaustive record would be a good follow-up.

Fixes A-1525

@ludamad ludamad left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Auto-approved

@AztecBot
AztecBot added this pull request to the merge queue Jul 30, 2026
@AztecBot

Copy link
Copy Markdown
Collaborator Author

🤖 Auto-merge enabled after 4 hours of inactivity. This PR will be merged automatically once all checks pass.

@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jul 30, 2026
…targets (#25058)

Closes A-1443. Reworked version of #24923 targeting the v5 line,
addressing the review feedback there.

Operators currently get no signal when the network starts voting to
slash their validators — during the v5 inactivity-slashing incident,
operators found out only after stake was lost. This PR makes the node
detect when its own validators are targeted by onchain slashing, at two
points in the lifecycle:

- **Vote time (early warning)**: a new `OwnValidatorSlashMonitor`
(following the `SlashRoundMonitor`/`SlashOffensesCollector`
decomposition) subscribes to `VoteCast` events — only when the node runs
validators — and warns on every vote that names one of them, with the
running tally against the quorum: `Own validator 0x… targeted by
slashing vote (7 of 65 votes needed to slash)`. Quorum needs a majority
of a round's slots, so warnings start well inside the window an operator
has to react.
- **Round execution**: `handleRoundExecuted` filters the already-fetched
`Slashed` events to own validators and emits a WARN with the exact
slashed amount. No additional L1 reads.

Five node-level metrics (no per-validator labels, so one series per node
regardless of attester count):
`aztec.slasher.own_validator.targeted_count`, `.current_round_votes_max`
(highest tally against any committee position held by an own validator,
reset each round), `aztec.slasher.quorum_size` to compare it against,
and `.slashed_count` / `.slashed_amount` for executed slashes. The alert
is `current_round_votes_max` approaching `quorum_size`; the WARN
identifies which validator via a structured log field.

Design notes:

- **Votes are read by index, events are only triggers.** `VoteCast`
carries no vote index, so the monitor keeps a per-round cursor: on each
event it reads the round's `voteCount` and processes votes `[cursor,
voteCount)` via a new `getVoteAt(round, index)`. Duplicate or batched
deliveries are no-ops, backlogs are caught up by the next event, and a
failed read is retried because the cursor only advances after a vote is
successfully processed.
- **All processing is serialized** through a single queue, with the
round revalidated after every await, so concurrent event handlers cannot
double-count, drop votes into the wrong round, or emit out-of-order
tallies. `stop()` drains the queue and suppresses any late
warns/metrics.
- **Startup baseline instead of replay.** On start the monitor reads the
current `voteCount` (before subscribing) and skips everything before it:
replaying old votes would double-count cumulative counters across
restarts. Votes cast while the node is offline or starting up are not
counted, and L1 reorgs can cause small drift within a round — both
documented on the metrics.
- **The tally is per flattened committee position** because that is the
unit the contract tallies quorum by: a validator sitting in several of
the round's committees is named once per position by a single vote, each
position racing quorum independently. Warnings and `targeted_count`
remain per (vote, validator), reporting the validator at its highest
position tally.
- Own validator addresses were already passed to `createSlasherFacade`
for `slashValidatorsNever`; they are now also threaded as
`ownValidators` into `SlasherClient`, independent of `slashSelfAllowed`.
`getSlashingAmounts` is memoized (the amounts are Solidity `immutable`),
and `SlashVote` is renamed to the singular `SlashVoteTarget`.

Testing: 22 unit tests for the monitor (per-vote warning with pinned
message/context, per-position tallying, multi-validator nodes, round
rollover in both directions and quiet-round reset, closed-round and
foreign-validator votes, cursor semantics under duplicate/batched/missed
events, failed-read retry, drain serialization, mid-drain rollover,
startup baseline and its failure fallback, stop/restart lifecycle,
subscription gating), 3 client wiring tests, and an anvil-backed
`getVoteAt` decoding test in the ethereum package.
@spalladino
spalladino added this pull request to the merge queue Aug 1, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Aug 1, 2026
@AztecBot

AztecBot commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator Author

🤖 Auto-merge enabled after 4 hours of inactivity. This PR will be merged automatically once all checks pass.

@AztecBot
AztecBot added this pull request to the merge queue Aug 3, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Aug 3, 2026
@spalladino
spalladino added this pull request to the merge queue Aug 4, 2026
Merged via the queue into v5-next with commit 0a45e4d Aug 4, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants