Skip to content

fix(slasher): anchor watcher scans at archiver synced L2 slot#23394

Merged
spalladino merged 2 commits into
merge-train/spartanfrom
pw/a-709-slashing-synced-slot
May 19, 2026
Merged

fix(slasher): anchor watcher scans at archiver synced L2 slot#23394
spalladino merged 2 commits into
merge-train/spartanfrom
pw/a-709-slashing-synced-slot

Conversation

@PhilWindle
Copy link
Copy Markdown
Collaborator

Summary

A-709 audit found two slashing watchers that drove their periodic scans off the wallclock rather than the archiver's last fully-synced L2 slot. An L1 stall (e.g. the fusaka scenario) would let them speculate into slots whose L1 windows had not yet been ingested, producing false-positive slashes or scanning gaps. This PR moves both onto archiver.getSyncedL2SlotNumber() ?? epochCache.getSlotNow(), matching the pattern already in DataWithholdingWatcher.

Changes

  • Sentinel — all four getSlotNow() call sites (init, work, computeStats, getValidatorStats) now go through a new getCurrentSlot() helper that prefers the archiver synced slot, with wallclock fallback only at cold start. The redundant synced gate inside isReadyToProcess is kept as a defensive guard for the fallback path.
  • BroadcastedInvalidCheckpointProposalWatcher — constructor now takes a Pick<L2BlockSource, 'getSyncedL2SlotNumber'> (wired to archiver at the construction site); scan() uses it instead of epochCache.getCurrentAndNextSlot().

Audit

Other WANT_TO_SLASH emitters were checked and already synced-safe:

  • DataWithholdingWatcher — already uses synced slot.
  • AttestationsBlockWatcher — event-driven from archiver InvalidAttestationsCheckpointDetected.
  • validator-client emitters (BROADCASTED_INVALID_BLOCK_PROPOSAL, ATTESTED_TO_INVALID_CHECKPOINT_PROPOSAL, DUPLICATE_PROPOSAL, DUPLICATE_ATTESTATION) — event-driven from live p2p observations.

Test plan

  • Sentinel: added a new describe('init') with two red/green cases covering synced-slot floor and wallclock fallback. Red/green verified.
  • Watcher: added three new tests — anchors at synced slot, falls back to wallclock when synced is undefined, does not expand window when L1 stalls but wallclock keeps moving. Red/green verified by surgically reverting the synced-slot read.
  • Full sentinel suite (48 tests) and watcher suite (12 tests) pass; slasher and aztec-node TypeScript builds clean.

Sentinel and BroadcastedInvalidCheckpointProposalWatcher both drove their
periodic scans off the wallclock, so an L1 stall would let them speculate
into slots whose L1 windows had not been ingested yet. Anchor all four
sentinel call sites and the watcher's scan loop at
`archiver.getSyncedL2SlotNumber() ?? epochCache.getSlotNow()`, mirroring
the pattern already in DataWithholdingWatcher.

Resolves A-709.
* L1 actually is.
*/
protected async getCurrentSlot(): Promise<SlotNumber> {
return (await this.archiver.getSyncedL2SlotNumber()) ?? this.epochCache.getSlotNow();
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.

Should we start storing the "last L1 synced timestamp" in the archiver's db rather than just in memory, so we don't need the fallback?

@spalladino spalladino merged commit e3b608e into merge-train/spartan May 19, 2026
14 checks passed
@spalladino spalladino deleted the pw/a-709-slashing-synced-slot branch May 19, 2026 13:35
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.

2 participants