Skip to content

feat(slasher): slash checkpoint equivocation between P2P and L1 (A-980)#23436

Merged
PhilWindle merged 4 commits into
merge-train/spartanfrom
phil/a-980-slash-by-equivocation-if-l1-synced-checkpoint-does-not-match
May 22, 2026
Merged

feat(slasher): slash checkpoint equivocation between P2P and L1 (A-980)#23436
PhilWindle merged 4 commits into
merge-train/spartanfrom
phil/a-980-slash-by-equivocation-if-l1-synced-checkpoint-does-not-match

Conversation

@PhilWindle

Copy link
Copy Markdown
Collaborator

Summary

  • The archiver already detects when a locally-stored proposed checkpoint disagrees with the L1-confirmed checkpoint at the same slot (in tryBuildPublishedCheckpointFromProposed), but until now it only logged the divergence and evicted the local entry. Both sides are signed by the slot proposer, so the proposer equivocated.
  • Emit a new `CheckpointEquivocationDetected` event from the archiver when this divergence is detected.
  • Add `CheckpointEquivocationWatcher` in the slasher that subscribes to the event, resolves the slot proposer via the epoch cache, and emits a `DUPLICATE_PROPOSAL` want-to-slash event.
  • This closes the gap where a proposer broadcasts one checkpoint via gossip and submits a different one to L1 — the existing `AttestationPool` P2P duplicate detection can't see this because only one of the two proposals is gossiped.

Fixes A-980.

Test plan

  • New unit tests in `yarn-project/slasher/src/watchers/checkpoint_equivocation_watcher.test.ts` cover: emit on divergence, no-emit on missing proposer / zero penalty / after stop, dedup of repeat events, separate offenses per slot.
  • The end-to-end scenario already exists in `yarn-project/end-to-end/src/e2e_epochs/epochs_equivocation.test.ts`. Its TODO around asserting the slash is still gated on enabling the slasher in the harness (separate plumbing) — comment updated to reflect that detection is now implemented.

The archiver's L1 synchronizer already detects when a locally-stored
proposed checkpoint disagrees with the L1-confirmed checkpoint at the same
slot, but it only logged the divergence and evicted the local entry. Both
sides are signed by the slot proposer, so the proposer equivocated.

Emit a CheckpointEquivocationDetected event from the archiver when this
divergence is detected, and add a CheckpointEquivocationWatcher in the
slasher that subscribes to it, resolves the slot proposer via the epoch
cache, and emits a DUPLICATE_PROPOSAL want-to-slash event.

This closes the gap where a proposer broadcasts one checkpoint via gossip
and submits a different one to L1 — the existing AttestationPool duplicate
detection can't see this because only one of the two proposals is gossiped.
@PhilWindle PhilWindle marked this pull request as ready for review May 20, 2026 16:54
Enable the slasher in the equivocation harness with a non-zero
slashDuplicateProposalPenalty and poll getSlashOffenses across the live
validator nodes after the network heals. The offense fires on B/C as soon
as L1 sync exposes A's checkpoint, well before the healing assertions
complete, so the wait window is short.
…-980)

Tighten the assertion: instead of accepting any observer recording the
DUPLICATE_PROPOSAL offense, require both B and C to have it for the
submission slot.

@spalladino spalladino 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.

LGTM! I'd just add the slot check before merging.

Comment on lines +1079 to +1087
// Both the locally-proposed checkpoint and the L1-confirmed one are signed by the
// slot proposer; emit a divergence event so the slasher can attribute equivocation.
this.events.emit(L2BlockSourceEvents.CheckpointEquivocationDetected, {
type: L2BlockSourceEvents.CheckpointEquivocationDetected,
slotNumber: calldataCheckpoint.header.slotNumber,
checkpointNumber: calldataCheckpoint.checkpointNumber,
l1ArchiveRoot: calldataCheckpoint.archiveRoot,
proposedArchiveRoot: proposed.archive.root,
});

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.

Just defensively, I wouldn't trigger this if the checkpoints' slots are not equal. I think it can't happen, since we prune uncheckpointed checkpoints before getting here, but just in case.

@@ -763,6 +765,11 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb
watchers.push(broadcastedInvalidCheckpointProposalWatcher);
}

if (config.slashDuplicateProposalPenalty > 0n) {

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.

I was thinking we should remove all these conditionals for the codebase, so the offenses get created even if they are not penalized, just for tracking purposes. But that's for another PR.

Comment on lines +108 to +111
private markAsNewOffense(args: WantToSlashArgs): boolean {
const key = `${args.validator.toString()}-${args.offenseType}-${args.epochOrSlot}`;
return this.emittedOffenses.addIfAbsent(key);
}

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.

I've just checked and the offenses store already deduplicates based on this exact same criteria, so we don't need to track it in the watcher.

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 just remove this in favor of adding the offense directly from the L1 synchronizer? Sometimes I wonder whether we're over-using events, and just calling into whatever class we want is simpler to reason about. I tend to go from one approach to the other, not sure what's best here.

- Guard the equivocation event emission with a slot-equality check so the
  archiver cannot mis-attribute a slash if the proposed/L1 checkpoints
  somehow disagree on slot.
- Drop the FifoSet-based dedup from CheckpointEquivocationWatcher: the
  offenses store already keys offenses by (validator, type, epochOrSlot)
  and silently drops repeat additions.
@PhilWindle

Copy link
Copy Markdown
Collaborator Author

LGTM! I'd just add the slot check before merging.
Should be done

@PhilWindle PhilWindle merged commit 7bbece1 into merge-train/spartan May 22, 2026
14 checks passed
@PhilWindle PhilWindle deleted the phil/a-980-slash-by-equivocation-if-l1-synced-checkpoint-does-not-match branch May 22, 2026 10:07
danielntmd pushed a commit to danielntmd/aztec-packages that referenced this pull request Jun 4, 2026
BEGIN_COMMIT_OVERRIDE
refactor(p2p): merge FastTxCollection into TxCollection with sequential
pipeline (AztecProtocol#23245)
refactor(publisher): bundle-level simulate; drop per-action enqueue sims
(AztecProtocol#23165)
refactor(stdlib): remove deprecated RevertCode/TxExecutionResult aliases
(AztecProtocol#23249)
test(e2e): fix race in 'proposer invalidates multiple checkpoints'
(AztecProtocol#23259)
fix: clean up old jobs regardless of pending status (AztecProtocol#23260)
refactor(p2p): remove unused sendBatchRequest (AztecProtocol#23273)
chore(p2p): remove proposal_tx_collector leftovers (AztecProtocol#23276)
feat: slash truncated checkpoint proposals (AztecProtocol#23250)
refactor: remove unused map in attestation pool (AztecProtocol#23284)
chore(p2p): assert last block in checkpoint proposal is correct (AztecProtocol#23274)
refactor(l1-tx-utils): use DateProvider for fail-fast timeout check
(AztecProtocol#23257)
feat(sandbox): support proposer pipelining in local network (AztecProtocol#23277)
test(e2e): fix race in broadcasted_invalid_block_proposal_slash under
pipelining (AztecProtocol#23302)
fix(archiver): atomic getter for L2 tips (AztecProtocol#23295)
fix(sequencer): use targetSlot in tryVoteWhenEscapeHatchOpen under
pipelining (AztecProtocol#23296)
fix(world-state): make fork close idempotent for pruned forks (AztecProtocol#23298)
test(e2e): migrate passing tests to proposer pipelining (AztecProtocol#23275)
chore: update dashboard (AztecProtocol#23312)
chore: Revert "feat(sandbox): support proposer pipelining in local
network" (AztecProtocol#23313)
test: slash on bad attestation (AztecProtocol#23184)
feat(slasher): per-slot data-withholding watcher (A-523, A-525) (AztecProtocol#23116)
test(e2e): enable pipelining on e2e debug trace (AztecProtocol#23301)
test(e2e): enable pipelining on l1-to-l2 test (AztecProtocol#23300)
test(e2e): switch fee_settings to organic fee bumps under pipelining
(AztecProtocol#23303)
fix(ci): retry sqlite3mc-wasm download on transient DNS/TLS failures
(AztecProtocol#23333)
test(e2e): wait for real oracle rotation in fee_settings inflate helper
(AztecProtocol#23334)
test(e2e): anchor e2e_amm PXE to checkpointed tip under pipelining
(AztecProtocol#23336)
fix(spartan-bench): tolerate older node images in SlasherConfig schema
(AztecProtocol#23351)
fix: interrupt prover jobs in stop (AztecProtocol#23358)
test(e2e): enable pipelining on bot, fees, and avm simulator tests
(AztecProtocol#23329)
feat(sentinel): end-of-epoch evaluation with re-execution outcomes
(AztecProtocol#23286)
feat: slash for invalid checkpoint proposals (AztecProtocol#23270)
fix: fork closure in epoch proving jobs (AztecProtocol#23390)
fix(slasher): anchor watcher scans at archiver synced L2 slot (AztecProtocol#23394)
fix: avoid npm uplink for aztec-up local publishes (AztecProtocol#23396)
test(e2e): ignore benign 'Insufficient valid txs' block-build-failed in
epochs tests (AztecProtocol#23424)
chore: refactor weekly proving test wait (AztecProtocol#23395)
refactor: add fifo set (AztecProtocol#23271)
feat(sandbox): support proposer pipelining in local network (AztecProtocol#23327)
fix(p2p): validate BLOCK_TXS in BatchTxRequester (AztecProtocol#23371)
chore(p2p): simplify IBatchRequestTxValidator (AztecProtocol#23373)
feat(sequencer): AutomineSequencer for single-sequencer e2e tests
(AztecProtocol#23354)
fix(prover): wait for previous epoch to be proven (AztecProtocol#23458)
chore: collocate provers (AztecProtocol#23439)
chore: rm staging-ignition (AztecProtocol#23440)
chore: rm unused networks (AztecProtocol#23441)
test(e2e): migrate block_building, multi_validator_node,
publisher_funding, invalid_checkpoint_proposal to pipelining (AztecProtocol#23414)
fix(archiver): reconcile local blocks with L1 checkpoints by block
number (AztecProtocol#23461)
feat: Updated slash conditions on block proposals (AztecProtocol#23466)
test(e2e): migrate HA full test to pipelining (AztecProtocol#23463)
chore: update resource profiles (AztecProtocol#23442)
chore: update debug log levels (AztecProtocol#23456)
test: fix flaky sentinel_status_slash by asserting the fault on the
checkpoint slot (AztecProtocol#23483)
feat(slasher): slash checkpoint equivocation between P2P and L1 (A-980)
(AztecProtocol#23436)
refactor(slasher): rename ATTESTED_DESCENDANT_OF_INVALID ->
PROPOSED_DESCENDANT_OF_CHECKPOINT_WITH_INVALID_ATTESTATIONS (AztecProtocol#23468)
fix: reject block proposals in poisoned slots (AztecProtocol#23411)
fix: retry nargo dep + solc downloads to survive transient DNS drops
(AztecProtocol#23490)
fix: enrich json-rpc tracing (AztecProtocol#23412)
feat: add trace export controls (AztecProtocol#23413)
test(e2e): assert no equivocation offenses in HA full test (AztecProtocol#23496)
test: cover invalid checkpoint proposal slashing (AztecProtocol#23503)
test(e2e): migrate more e2e suites to proposer pipelining (AztecProtocol#23482)
test: flag e2e_slashing_attested_invalid_proposal as flake under
pipelining (AztecProtocol#23501)
test: flag e2e_p2p_duplicate_proposal_slash as flake under pipelining
(AztecProtocol#23515)
test(e2e): require cross-observer agreement on sentinel fault slot
(AztecProtocol#23513)
test: flag e2e_ha_full afterAll hook timeout as flake under pipelining
(AztecProtocol#23524)
fix(e2e): propagate l1ContractsArgs into node config so archiver matches
L1 (AztecProtocol#23514)
test: flag e2e_multi_validator_node_key_store P2P tx-dropped failure as
flake (AztecProtocol#23528)
test(cheat-codes): retry warpL2TimeAtLeastTo in-current-slot test on L1
race (AztecProtocol#23533)
test(e2e_ha_full): parallel HA peer node teardown with per-node deadline
(AztecProtocol#23539)
test: flag e2e_ha_full as flake under HA pipelining (AztecProtocol#23541)
test(ci): skip e2e_ha_full entirely on merge-train/spartan (AztecProtocol#23542)
test(ci): skip e2e_multi_validator_node_key_store entirely on
merge-train/spartan (AztecProtocol#23544)
END_COMMIT_OVERRIDE
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