Skip to content

test(e2e): adopt shared wait helpers - #24404

Merged
PhilWindle merged 2 commits into
merge-train/spartan-v5from
spl/e2e-helper-adoption-1
Jul 1, 2026
Merged

test(e2e): adopt shared wait helpers#24404
PhilWindle merged 2 commits into
merge-train/spartan-v5from
spl/e2e-helper-adoption-1

Conversation

@spalladino

Copy link
Copy Markdown
Contributor

Motivation

The e2e suite is littered with hand-rolled retryUntil / sleep / Promise.all(...waitForTx) polls, each flagged with a // REFACTOR: marker. Most duplicate logic that already exists in the shared fixtures/wait_helpers.ts library or on the test contexts. This PR resolves those markers by adopting the shared helpers (and adding a handful of small new ones), removing the duplicated, brittle wait code.

Stacked on top of #24345 (spl/e2e-speed-up-1), which shipped the warp/build-window helpers this PR adopts.

Approach

Every change is behavior-preserving: the original loop's timeout, polling interval, and comparator/predicate are matched exactly. The mechanism is swapped, not what the test waits for or asserts. Where an existing helper didn't quite match the original semantics, the helper was extended with an option rather than the test's behavior being changed.

  • Adoption (most families): replace a hand-rolled poll with a call to a helper that already ships in wait_helpers.ts or on SingleNodeTestContext / MultiNodeTestContext.
  • New helpers (the minority): small additions to wait_helpers.ts and RollupCheatCodes, each behavior-preserving and adopted in the same change.

33 of 50 // REFACTOR: markers are resolved. The remaining 17 are deliberately deferred (see Follow-ups) - they are single-use, read from a data source no shared helper covers, or are the riskiest swaps that warrant their own focused change.

Changes (by helper-family)

Adoption of existing wait_helpers.ts functions

  • Block-number waits to waitForBlockNumber / waitForProvenBlock: block_building, debug_trace, genesis_timestamp, l1_to_l2, fee_settings.
  • Node checkpoint waits to waitForNodeCheckpoint: genesis_timestamp, fee_settings.
  • Batch tx waits to waitForTxs: block_building (3 sites), multiple_blobs, gossip_network.
  • L2-to-L1 witness waits to waitForL2ToL1Witness: token_bridge_public, token_bridge_private.
  • Monitor checkpoint wait to MultiNodeTestContext.waitUntilCheckpointNumber: proof_boundary.
  • P2P mesh readiness to waitForP2PMeshConnectivity (replaces a fixed sleep(8000)): fee_asset_price_oracle_gossip.
  • Build-window wall-clock wait to waitForBuildWindowForSlot: high_tps.
  • Slot-search with EpochNotStable warp to existing MultiNodeTestContext.findSlotsWithProposers: invalidate_block, ha_checkpoint_handoff.

New helpers (added + adopted)

  • wait_helpers.ts: waitForTxReceipt / waitForTxStatus (tx-status transitions in block_building), waitForPendingTxCount (attested_invalid_proposal), waitForSequencerState with an after-action hook - and deleted the three duplicated waitForSequencerIdle copies in l2_to_l1, gas_estimation, and reload_keystore.
  • waitForBlockNumber extended with a compare option for the prune-backward poll in synching.
  • RollupCheatCodes.waitForEpoch / waitForSlot (poll-only, no warp), adopted in escape_hatch_vote_only and gov_proposal.

Inline cleanups (no shared helper warranted)

  • fee_asset_price_oracle: two identical price-convergence polls consolidated into one local helper.
  • upgrade_governance_proposer: while(true) + sleep(12000) quorum poll to retryUntil.
  • upload_failed_proof: local-variable upload poll to promiseWithResolvers resolved from the upload callback.
  • synching: archiver-prune sleep(3000) to retryUntil on the archiver checkpoint number.

Follow-ups (deferred markers)

These were left in place because adopting a helper would either change behavior or add a single-use abstraction:

  • proof_fails:109 - rollback detection reads the L1 rollup contract (RollupContract.getCheckpointNumber), not the node; swapping to a node helper would change the data source. Needs a rollup-contract-based wait.
  • snapshot_sync:89 - reads ChainMonitor.checkpointNumber (not the node) with > semantics; the test uses the legacy EndToEndContext, which has no waitUntilCheckpointNumber.
  • long_proving_time:74 - the loop records peak proving-job parallelism on every iteration; that sampling is the behavior under test, so it can't be replaced by a plain wait.
  • preferred_gossip_network:183 - per-node exact peer counts in a heterogeneous topology; doesn't map onto waitForP2PMeshConnectivity's uniform >= check.
  • gov_proposal:245 - ChainMonitor poll that returns a full snapshot (checkpoint + slot) consumed downstream; no helper returns that.
  • failures:100 - a two-call advanceToNextEpoch + catchUpProvenChain sequence, not a hand-rolled poll.
  • Family-13 one-offs (pruned_blocks:117, offchain_payment:215, snapshot_sync:100, l1_to_l2:130) - single-use, heterogeneous polls (error-message match, note-balance, readdir, advance-block-per-poll) that already use retryUntil rather than a sleep.
  • state_vars:439 - a chain-advance-by-sending-txs loop (side-effecting), not a poll.
  • optimistic:115 - a disposable sampler; single call-site.
  • rediscovery:61 - a sleep(2500) guarding against port conflicts between node restarts, not connectivity.
  • bridging_race.notest.ts:70/77 - in a disabled .notest.ts file.
  • fee_settings:121 - the L1-base-fee-spike + oracle-rotation loop returns a value and is fee-domain specific; warrants its own focused RollupCheatCodes change.

Verification

yarn build, yarn lint, and yarn format all pass on the touched packages (end-to-end, ethereum). The e2e tests themselves are validated by CI on this stacked PR.

@AztecBot

AztecBot commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

Flakey Tests

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

\033FLAKED\033 (8;;http://ci.aztec-labs.com/e4611ffca8244d60�e4611ffca8244d608;;�): yarn-project/end-to-end/scripts/run_test.sh ha src/composed/ha/e2e_ha_full.parallel.test.ts "should not delete recent duties via cleanupOldDuties when node clock is ahead" (36s) (code: 0)
\033FLAKED\033 (8;;http://ci.aztec-labs.com/2679af64aec6155f�2679af64aec6155f8;;�): yarn-project/end-to-end/scripts/run_test.sh ha src/composed/ha/e2e_ha_full.parallel.test.ts "should distribute work across multiple HA nodes" (138s) (code: 0)

@spalladino spalladino changed the title test(e2e): adopt shared wait helpers to resolve hand-rolled-wait REFACTOR markers test(e2e): adopt shared wait helpers Jun 30, 2026
Base automatically changed from spl/e2e-speed-up-1 to merge-train/spartan-v5 June 30, 2026 20:38
Replace hand-rolled retryUntil/sleep/Promise.all(waitForTx) polls with the shared
wait_helpers library (waitForBlockNumber, waitForProvenBlock, waitForNodeCheckpoint,
waitForTxs, waitForL2ToL1Witness) and the context waitUntilCheckpointNumber method.

Add new behavior-preserving helpers: waitForTxReceipt/waitForTxStatus,
waitForPendingTxCount, and waitForSequencerState (with an after-action hook), and
delete the duplicated waitForSequencerIdle copies in l2_to_l1/gas_estimation/reload_keystore.
…helper adoption

Add RollupCheatCodes.waitForEpoch/waitForSlot and adopt them in the
escape-hatch and gov-proposal sequencer tests. Reuse the existing
MultiNodeTestContext.findSlotsWithProposers for the invalidate_block and
ha_checkpoint_handoff slot searches. Adopt the build-window wait helper in
high_tps, extend waitForBlockNumber with a compare option for the prune-backward
poll in synching, consolidate the duplicated price-convergence polls in
fee_asset_price_oracle, convert the quorum while-loop and the upload-completion
poll to retryUntil/promiseWithResolvers.
@spalladino
spalladino force-pushed the spl/e2e-helper-adoption-1 branch from 479581e to f0af2ab Compare June 30, 2026 20:58
@PhilWindle
PhilWindle merged commit 66c2287 into merge-train/spartan-v5 Jul 1, 2026
12 checks passed
@PhilWindle
PhilWindle deleted the spl/e2e-helper-adoption-1 branch July 1, 2026 10:48
spalladino added a commit that referenced this pull request Jul 1, 2026
## Motivation

A-1178 (#24281) measures each e2e test's wall-clock as four coarse
buckets (`setupFnMs`, `beforeHooksMs`, `bodyMs`, `afterHooksMs`). That
tells us a test is slow but not *where* it loses time. Most e2e tests
spend their wall-clock in the same handful of operations — standing up
the environment, waiting for a tx to be mined/checkpointed/proven,
waiting for a committee to form or an offense to be detected,
client-side proving of seed txs, and warp scans hunting for a proposer
slot.

This decomposes that time into **named spans** so a single aggregate
query over a full CI run answers *"across the suite, how much total
wall-clock goes into proving / spinning up nodes / waiting for
checkpoints?"* — a ranked list of where to invest in speedups. It
extends A-1178 and builds on the consolidation (#24201/#24310) and
helpers (#24404) work that turned each repeated operation into one
shared definition, so wrapping it once instruments every test with no
per-test edits.

This is **instrumentation / data-gathering only** — no test behavior or
timing changes.

## Approach

- A generic `testSpan(name, fn)` / `testSpanSync(name, fn)` wrapper
(`fixtures/timing.ts`) records `{ owner, name, start, end }` into the
shared collector installed by the timing environment. When
`TEST_TIMING_FILE` is unset there is no collector, so `testSpan()` calls
`fn()` directly — exactly zero-cost, with no clock reads.
- At flush, the timing environment groups spans by owner then tag and
computes four numbers per `(owner, tag)`:
- `count` — multiplicity is itself a signal (waited for a checkpoint 14×
points at a loop to batch).
  - `totalMs` — naive sum (correct for serial repeats).
- `busyMs` — duration of the **union** of the spans' intervals;
concurrency-correct, so a `Promise.all` of 12 concurrent 3s spans reads
~3s instead of ~36s. The `busyMs ≪ totalMs` signature flags work that is
run serially where it could be parallel.
- `maxMs` — longest single occurrence, to catch one pathological wait
hiding in a cheap average.
- The aggregates attach as an additive `spans` map on each `type:"test"`
/ `type:"suite"` JSONL line. `setupFnMs` / `teardownFnMs` are kept for
back-compat, now derived from the `setup:env:<mode>` / `teardown:env`
tags.
- Tags follow a stable `category:label` taxonomy (`setup:`, `wait:`,
`tx:`, `warp:`, `wallet:`, `deploy:`, `other:`), tagged **by concept,
not by function** — e.g. every checkpoint waiter maps to
`wait:checkpoint` — so a label is a forever aggregation key regardless
of which helper a test happened to call.
- **One clock.** All spans use `performance.now()`. I verified Node's
`perf_hooks` clock is process-wide: a `performance.now()` taken inside a
fresh `vm` context (the jest sandbox realm) shares the same monotonic
origin as the host realm (delta ~0.002ms; a fresh
`perf_hooks.performance` inside a separate context anchors to the same
`timeOrigin`). Interval-merging for `busyMs` across the sandbox and host
realms is therefore valid, so **`busyMs` is included** (no fallback to
`totalMs`-only needed).
- **Background loop.** `startMempoolFeeder` runs interleaved with
arbitrary tests; without care its prove/send spans would smear onto
whichever test was current when each round fired. Its production is run
inside an `AsyncLocalStorage`-scoped owner override
(`other:mempool-feeder`), which pins every span in its async call tree
to a fixed owner — isolated from concurrent test-body spans. That owner
matches no test/suite record, so the feeder's spans are cleanly excluded
from the per-test view.

## Changes by phase

- **Phase 0 — collector + `testSpan()` + flush aggregation.** New
`fixtures/timing.ts` (`testSpan`/`testSpanSync`/`withTestSpanOwner`).
`timing_env.mjs` collector generalized from `fnSpans` to a `spans`
array; `finalizeAndFlush` groups by owner/tag, merges intervals for
`busyMs`, attaches the `spans` map, and derives the back-compat fields.
- **Phase 1 — shared waits + `setup:node`.** Wrapped the waiters in
`fixtures/wait_helpers.ts` (`wait:proposed` / `wait:proven`,
`wait:checkpoint` / `wait:proven-checkpoint`, `wait:tx-mined`,
`wait:l2-to-l1-witness`, `wait:pending-tx`, `wait:sequencer-state`) and
the wait methods on `SingleNodeTestContext` / `MultiNodeTestContext`
(`wait:epoch`, `wait:slot`, `wait:proof-window`, `wait:node-sync`,
`wait:proof-submitted`, `wait:offense`, the multi-node
`wait:checkpoint`/`wait:proven-checkpoint`/`wait:block` convergence
waiters). Wrapped `createNode`/`createProverNode` with `setup:node`.
- **Phase 2 — decompose `setup:env`.** Cracked the opaque setup in
`fixtures/setup.ts` into `setup:env:anvil`, `:l1-deploy`,
`:sequencer-start`, `:prover-node`, `:pxe`, and `wallet:create`. The
top-level `setup:env` is tagged with the prover mode (`setup:env:none` /
`:fake` / `:real`) so the three factories are comparable.
- **Phase 3 — tx leaf wraps.** `proveInteraction` → `tx:prove`,
`ProvenTx.send` → `tx:send`; everything else
(`proveTxs`/`proveAndSendTxs`/the submit helpers) aggregates through
those leaves. `startMempoolFeeder` handled as above.
- **Phase 4 — slashing/governance waiters.** Wrapped
`awaitCommitteeExists` (`wait:committee`), `awaitOffenseDetected`
(`wait:offense`), `awaitCommitteeKicked` (`wait:committee-kicked`),
`awaitProposalExecution` (`wait:slash-execution`), and the
`findUpcomingProposerSlot` / `advanceToEpochBeforeProposer` /
`findSlotsWithProposers` scans (`warp:find-proposer`).
- **Phase 5 — reporting.** The JSONL schema now carries the per-line
`spans` map, and `TEST_TIMING_SPANS=1` emits one `type:"span"` line per
occurrence (owner, name, ms) for deep dives. The repo now also ships a
`track-e2e-times` skill (`yarn-project/.claude/skills/track-e2e-times/`)
covering how to **collect and aggregate** the span timings locally: run
the suite with `TEST_TIMING_FILE` set, find the per-worker JSONL, and
print per-test sums plus the ranked span leaderboard (via the bundled
`row.sh`). Only the step of publishing aggregate numbers to a running
tracking log remains a separate out-of-band workflow. The leaderboard
rollup is a small additive jq over the new `spans` maps:

  ```
jq -rs '[ .[] | select(.type=="test" or .type=="suite") | (.spans // {})
| to_entries[] ]
    | group_by(.key)
    | map({ tag: .[0].key, count: (map(.value.count) | add),
busyMs: (map(.value.busyMs) | add), maxMs: (map(.value.maxMs) | max) })
| sort_by(-.busyMs) | .[] | "\(.busyMs)\t\(.count)\t\(.maxMs)\t\(.tag)"'
  ```

The existing `row.sh` is unaffected — it filters `type=="test"` and
reads the unchanged `setupFnMs`/`bodyMs`/etc., ignoring the new `spans`
key and `type:"span"` lines.

## Notes

- `waitForEpoch`/`waitForSlot` (added to `@aztec/ethereum`'s
`rollup_cheat_codes.ts` by #24404) are deliberately **not** instrumented
in-package: that package should not depend on the e2e timing collector,
and the `wait:epoch`/`wait:slot` concepts they cover are already
captured at the e2e-context wrapper layer (`waitUntilEpochStarts`, the
slot waiters). They have no e2e callers yet; a future call site picks
them up via its context wrapper.
- Spans do not partition `bodyMs` — a parent span includes its children,
so `sum(spans) ≠ bodyMs`. Tagging is at the leaf wait/setup/tx level
where additivity holds; the few intentional nests (e.g.
`wait:committee-kicked` contains `wait:slash-execution`) carry distinct
tags.

Verified: `yarn build`, `yarn lint end-to-end`, `yarn format --check
end-to-end` all pass. The flush aggregation (busyMs interval merge,
back-compat derivation, feeder-owner exclusion, opt-in raw lines) is
covered by unit tests in `end-to-end/src/shared/timing_env.test.ts`,
which exercise the extracted `aggregateSpans` / `foldSpansInto` pure
functions directly.

Fixes A-1179



## Example timing output

Each e2e test run in CI emits one JSONL record per test and per suite,
carrying a `spans` map keyed by `category:label` tag, where each value
is `{count, totalMs, busyMs, maxMs}` (`busyMs` is the de-overlapped
union duration, so concurrent occurrences of a span are counted once).
An illustrative `type:"test"` line from CI run 28469687883 (commit
de3635e):

```json
{
  "suite": "optimistic.parallel",
  "type": "test",
  "name": "single-node/proving/optimistic happy path proves multiple epochs via checkpoint-driven flow",
  "status": "passed",
  "setupFnMs": 4707,
  "beforeHooksMs": 4717,
  "bodyMs": 327324,
  "teardownFnMs": 615,
  "afterHooksMs": 624,
  "totalMs": 332668,
  "startedAt": "2026-06-30T19:20:58.279Z",
  "spans": {
    "setup:env:anvil": {
      "count": 1,
      "totalMs": 82,
      "busyMs": 82,
      "maxMs": 82
    },
    "setup:env:l1-deploy": {
      "count": 1,
      "totalMs": 470,
      "busyMs": 470,
      "maxMs": 470
    },
    "setup:env:sequencer-start": {
      "count": 1,
      "totalMs": 3141,
      "busyMs": 3141,
      "maxMs": 3141
    },
    "setup:env:prover-node": {
      "count": 1,
      "totalMs": 139,
      "busyMs": 139,
      "maxMs": 139
    },
    "setup:env:pxe": {
      "count": 1,
      "totalMs": 391,
      "busyMs": 391,
      "maxMs": 391
    },
    "wallet:create": {
      "count": 1,
      "totalMs": 79,
      "busyMs": 79,
      "maxMs": 79
    },
    "setup:env:fake": {
      "count": 1,
      "totalMs": 4707,
      "busyMs": 4707,
      "maxMs": 4707
    },
    "wait:epoch": {
      "count": 5,
      "totalMs": 180210,
      "busyMs": 180210,
      "maxMs": 36067
    },
    "tx:prove": {
      "count": 4,
      "totalMs": 2123,
      "busyMs": 2123,
      "maxMs": 621
    },
    "tx:send": {
      "count": 4,
      "totalMs": 144305,
      "busyMs": 144305,
      "maxMs": 36124
    },
    "wait:proven-checkpoint": {
      "count": 4,
      "totalMs": 0,
      "busyMs": 0,
      "maxMs": 0
    },
    "wait:node-sync": {
      "count": 4,
      "totalMs": 401,
      "busyMs": 401,
      "maxMs": 101
    },
    "teardown:env": {
      "count": 1,
      "totalMs": 615,
      "busyMs": 615,
      "maxMs": 615
    }
  },
  "commit": "de3635e74eb89071b52939d1583d072ba2c3852c",
  "branch": "spl/a1179-track-common-spans",
  "runId": "28469687883"
}
```
spalladino added a commit that referenced this pull request Jul 1, 2026
Finishes #24404's cleanup by resolving the 17 deferred // REFACTOR: wait-code
markers in the e2e suite. Adopts a shared helper where one fits and extracts
single-use polls into named local functions otherwise, preserving behavior.

- ChainMonitor.waitForCheckpoint gains an opt-in guard that short-circuits on a
  fresh run() snapshot, avoiding the event-vs-poll already-satisfied race.
- New RollupCheatCodes.waitForCheckpointBelow poll-only rollback waiter.
- snapshot_sync adopts ChainMonitor.waitUntilCheckpoint.
- FeesTest.waitForEpochProven encapsulates the advance-epoch + catch-up pair.
spalladino added a commit that referenced this pull request Jul 2, 2026
Finishes #24404's cleanup by resolving the 17 deferred // REFACTOR: wait-code
markers in the e2e suite. Adopts a shared helper where one fits and extracts
single-use polls into named local functions otherwise, preserving behavior.

- ChainMonitor.waitForCheckpoint gains an opt-in guard that short-circuits on a
  fresh run() snapshot, avoiding the event-vs-poll already-satisfied race.
- New RollupCheatCodes.waitForCheckpointBelow poll-only rollback waiter.
- snapshot_sync adopts ChainMonitor.waitUntilCheckpoint.
- FeesTest.waitForEpochProven encapsulates the advance-epoch + catch-up pair.
spalladino added a commit that referenced this pull request Jul 2, 2026
## Motivation

Cleanup finishing #24404's work. That PR adopted shared wait helpers
across the e2e suite but deliberately deferred 17 `// REFACTOR:`
wait-code markers as harder or ambiguous cases. This resolves all of
them, leaving zero `// REFACTOR:` markers under `end-to-end/src`.

## Approach

- Adopt a shared helper where one genuinely fits.
- Extract single-use / hard-to-adapt polls into descriptively-named
local functions in the same file, then delete the marker.
- Delete stale markers whose referenced code no longer exists.

All changes are behavior-preserving refactors: comparator direction,
timeouts (with seconds-vs-ms conversions where a helper's unit differs),
the `retryUntil` falsy-means-keep-polling contract, and side effects
(loops that send txs / advance blocks / rotate the oracle keep doing so
every tick) are all retained.

## API changes

New/changed test-only helpers (in `@aztec/ethereum` test utils and the
e2e fees harness):

- `ChainMonitor.waitForCheckpoint(match, opts)` gains an opt-in flag.
When set, it takes a fresh `run()` snapshot and short-circuits if the
current checkpoint already satisfies `match` before attaching the event
listener. This closes an event-vs-poll already-satisfied race: a
checkpoint could advance during a preceding wait and be missed by the
purely event-driven path. The existing caller is unchanged.
- New `RollupCheatCodes.waitForCheckpointBelow(checkpoint, opts)` — a
poll-only waiter (mirroring `waitForEpoch`/`waitForSlot`) that reads the
L1 rollup contract until the pending checkpoint drops below a baseline
(rollback detection). No node dependency.
- `snapshot_sync` now adopts the existing
`ChainMonitor.waitUntilCheckpoint` (which has the already-satisfied
guard) instead of a hand-rolled poll.
- New `FeesTest.waitForEpochProven()` encapsulates the recurring
`advanceToNextEpoch()` + `catchUpProvenChain()` pair (adopted in
`failures` and `private_payments`).
rangozd pushed a commit to rangozd/aztec-packages that referenced this pull request Aug 5, 2026
BEGIN_COMMIT_OVERRIDE
refactor(e2e): relocate no-node straggler tests (AztecProtocol#24344)
test(e2e): pin gas_estimation public-payment txs to one block to deflake
fee comparison (AztecProtocol#24382)
feat(archiver): event-trigger L2BlockStream sync from archiver updates
(AztecProtocol#24317)
test(e2e): speed up individual e2e tests (AztecProtocol#24345)
chore(e2e): warm blob KZGs in parallel during setup (AztecProtocol#24383)
chore: add perf as a valid PR title prefix (AztecProtocol#24412)
test(e2e): adopt shared wait helpers (AztecProtocol#24404)
test(e2e): run prover client.test.ts in CI (AztecProtocol#24399)
fix(ethereum): mine empty L1 blocks without touching the mempool
(AztecProtocol#24414)
test(e2e): deflake empty block proving test (AztecProtocol#24411)
test(e2e): allocate HA node p2p ports above the ephemeral range to
deflake e2e_ha_full (AztecProtocol#24418)
fix(sequencer): use evmMine in automine auto-settle to avoid dropping
test L1 txs (AztecProtocol#24421)
test(e2e): instrument common spans for wall-clock tracking (AztecProtocol#24407)
test(e2e): remove redundant reqresp_no_handshake e2e test (AztecProtocol#24424)
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.

3 participants