Skip to content

test(e2e): deflake optimistic-proving by anchoring epoch and asserting on checkpoint header slot - #24364

Merged
PhilWindle merged 1 commit into
merge-train/spartan-v5from
spl/deflake-optimistic-proving-cold-start
Jun 29, 2026
Merged

test(e2e): deflake optimistic-proving by anchoring epoch and asserting on checkpoint header slot#24364
PhilWindle merged 1 commit into
merge-train/spartan-v5from
spl/deflake-optimistic-proving-cold-start

Conversation

@spalladino

Copy link
Copy Markdown
Contributor

Deflakes the single-node/proving/optimistic › happy path › proves an epoch via checkpoint-driven flow e2e test, which intermittently fails on the v5 line (e.g. CI run 901b2552713b08a6, Optimistic proving start slots by epoch: [[0,5]]Expected: < 5 / Received: 5).

Root cause

The test asserts the prover-node began proving the epoch optimistically — that a CheckpointProver for the epoch was registered for a checkpoint before the epoch's last L2 slot. A CheckpointProver only appears in the CheckpointStore when the sequencer publishes a checkpoint for that epoch on chain, so the observed slot is the slot of the epoch's first checkpoint.

On a cold-started chain, epoch 0 begins the instant genesis is set, but setup() mines ~25 L1-deploy blocks that each warp the simulated clock forward by one ethereum slot (12s) while the sequencer is still starting and syncing. On a slow CI box the sequencer only becomes active around simulated slot 4-5, so epoch 0 gets its only checkpoint at slot 5 (the epoch's last slot) and there is nothing to prove mid-epoch. The recent deploy-speedup work (#24316) shifted this setup timing, which is why the flake started surfacing on this line. The prover behaved correctly — this is a test-harness flake, not a product regression.

A secondary fragility: the sampler recorded the wall-clock slot observed at the 100ms tick rather than the prover's own checkpoint header slot, so even with a warm epoch a checkpoint with header slot N could first be observed in wall-clock slot N+1.

Fix

  • Anchor both happy-path tests on a freshly-started epoch via await test.waitUntilNextEpochStarts() (the same pattern the reorg describe blocks already use) before starting the sampler and landing the tx. This guarantees the sequencer is warm and the epoch under test gets checkpoints from near its start — the premise the optimistic check requires.
  • Reframe the sampler to record, per epoch, the lowest checkpoint header slot of any registered CheckpointProver (prover.slotNumber, i.e. checkpoint.header.slotNumber) and assert that header slot < lastSlot. The sampler still runs live (provers are reaped once the proof window closes), but it now keys off the deterministic property the assertion actually cares about, eliminating the 100ms-tick race.

This preserves the behavioral intent (proving begins mid-epoch, not after the epoch ends) rather than relaxing it.

Notes

  • Confined to a single test file (+49/−29); no production code changes.
  • Base is merge-train/spartan-v5: this test is v5-line-only (it does not exist on next) and lives at its single-node/proving/ path only after the e2e-consolidation refactor, which is on this line.
  • Diagnosis was reviewed by a second (adversarial) agent and by Codex (gpt-5.5); both agreed it is a test flake and that anchoring plus the header-slot assertion is the correct fix. The e2e test itself was not run locally (the flake is non-deterministic and needs the CI anvil/deploy harness); the fix is justified by the source + log root cause, and CI will validate.

…g on checkpoint header slot

The 'proves an epoch via checkpoint-driven flow' test asserts the prover-node
started proving an epoch mid-epoch (a CheckpointProver registered before the
epoch's last slot). On a cold-started chain, epoch 0 begins at genesis but the
L1-contract deploy phase warps the simulated clock forward several slots while the
sequencer is still starting up; on a slow CI box the sequencer only goes live near
slot 4-5, so epoch 0 gets its only checkpoint at the last slot and there is nothing
to prove mid-epoch. This surfaced after the deploy-speedup work shifted setup
timing (CI run 901b2552713b08a6: 'start slots by epoch: [[0,5]]', lastSlot 5).

- Anchor both happy-path tests on a freshly-started epoch via waitUntilNextEpochStarts()
  (the pattern the reorg tests already use) so the sequencer is warm and the epoch
  under test gets checkpoints from its start.
- Reframe the sampler to record the lowest checkpoint *header* slot per epoch
  (prover.slotNumber) instead of the wall-clock slot at observation time, and assert
  that header slot < lastSlot. The header slot is the deterministic property the
  assertion actually cares about and does not race the 100ms sampler tick.
@AztecBot

Copy link
Copy Markdown
Collaborator

Flakey Tests

🤖 says: This CI run detected 1 tests that failed, but were tolerated due to a .test_patterns.yml entry.

\033FLAKED\033 (8;;http://ci.aztec-labs.com/bf69dd0dd1a4d93c�bf69dd0dd1a4d93c8;;�):  yarn-project/end-to-end/scripts/run_test.sh simple src/single-node/fees/fee_settings.test.ts (286s) (code: 0) group:e2e-p2p-epoch-flakes

// until slot 4-5 of epoch 0, so its first — and only — checkpoint lands at the epoch's last
// slot and there is nothing to prove mid-epoch. Anchoring guarantees a full epoch with
// checkpoints produced from its start, which is the premise the optimistic check needs.
await test.waitUntilNextEpochStarts();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'll probably change this to a warp instead of wait in an upcoming PR

@PhilWindle
PhilWindle merged commit 86658c4 into merge-train/spartan-v5 Jun 29, 2026
12 checks passed
@PhilWindle
PhilWindle deleted the spl/deflake-optimistic-proving-cold-start branch June 29, 2026 16:19
spalladino added a commit that referenced this pull request Jun 29, 2026
… prune predicate (#24379)

## Flake

`single-node/recovery/sync_after_reorg.test.ts` → "new node can sync
world-state after unpruned reorg" was flagged FLAKED on
`merge-train/spartan-v5` (CI hash `32e4c65996158955`, group
`e2e-p2p-epoch-flakes`). The first attempt failed; the retry passed.

First-attempt failure:

```
expect(received).toEqual(expected) // deep equality
Expected: 0
Received: 5
> 54 |     expect(await node.getBlockNumber()).toEqual(0);
```

The fresh node synced all 5 unproven blocks instead of pruning them back
to genesis.

## Root cause

The test stops the sequencer, opens a reorg window, then asserts a fresh
node prunes the unproven checkpoints back to block 0 on initial sync.
The fresh archiver only prunes when `rollup.canPruneAtTime` is true
(`l1_synchronizer.ts` `handleEpochPrune`). The contract allows pruning
once the current epoch reaches `getEpochForCheckpoint(proven + 1) +
proofSubmissionEpochs + 1` — with `proofSubmissionEpochs = 1` that is
`oldestPendingEpoch + 2`, where `oldestPendingEpoch` is the epoch of the
first (oldest pending) checkpoint, since nothing is ever proven (no
prover node).

The test hardcoded `waitUntilEpochStarts(2)`, which is only correct when
the first checkpoint lands in epoch 0. On a freshly-deployed chain the
L1-contract deploy warps the sim clock (~17×12s) and the sequencer comes
up late, so the first checkpoint can land in epoch 0 or epoch 1
depending on cold-start timing:

- Passing CI run: first checkpoint at L2 slot 3 → epoch 0 → deadline
epoch 2 → waiting for epoch 2 made it prunable → "Removed 5 checkpoints
after checkpoint 0 due to predicted reorg" → block 0.
- Failing CI run: first checkpoint at L2 slot 6 → epoch 1 → deadline
epoch 3 → waiting only for epoch 2 left proofs still acceptable →
`canPruneAtTime` false → no prune → fresh node kept all 5 blocks.

This is the same cold-start clock-warp family as the already-merged
proving-test fixes (#24364, #24372), but this reorg-recovery test is not
covered by them.

## Fix

Replace the hardcoded `waitUntilEpochStarts(2)` with a poll of the
rollup's own prune predicate (`canPruneAtTime` at the current L1 block
timestamp) before creating the fresh node. This is robust to where the
cold-start warp lands the first checkpoint, and tests the exact
precondition the fresh archiver's `handleEpochPrune` checks. Evaluating
at the current block timestamp is stricter than the archiver's check
(which looks one L1 block further ahead), so once the wait returns the
archiver's prune is guaranteed to fire. The behavioral assertion (fresh
node prunes to block 0 during a prunable reorg window) is preserved.

## Verification

- Full `yarn build` passes.
- Ran the test locally — passes (the fresh archiver waits on the new
poll, prunes the 5 checkpoints, and reaches block 0). The failing path
requires the specific cold-start timing that only manifests under CI
load (here the first checkpoint landed in epoch 0), so the red case is
reasoned from the two CI logs and the contract math rather than
reproduced locally.
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.

3 participants