Skip to content

feat: merge-train/spartan-v5#24532

Merged
AztecBot merged 12 commits into
v5-nextfrom
merge-train/spartan-v5
Jul 7, 2026
Merged

feat: merge-train/spartan-v5#24532
AztecBot merged 12 commits into
v5-nextfrom
merge-train/spartan-v5

Conversation

@AztecBot

@AztecBot AztecBot commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

BEGIN_COMMIT_OVERRIDE
perf(e2e): warp proven-checkpoint waits, tighten in-process polling, and instrument setup spans (#24452)
fix: reject aztecSlotDuration not a multiple of ethereumSlotDuration (#24481)
fix(config): fail loudly on invalid or fractional numeric config (#24536)
fix(sequencer): invalidate malicious yParity attestation checkpoints (A-1401) (#24537)
fix(ethereum): deflake monitor timeout tests in l1_tx_utils (#24545)
fix(ci): retry docker compose up on transient registry pull failures (#24546)
fix(e2e): resolve flaky proposer-not-found in equivocation_offenses duplicate_proposal test (#24557)
fix(e2e): resolve flaky checkpoint TypeError in multi_validator_node test (#24543)
fix(ethereum): prevent unhandled L1 rejections from tx monitor loops during shutdown (v5 line, partial) (#24551)
fix(sequencer): prevent unhandled rejections from in-flight L1 requests during shutdown (v5 line) (#24560)
END_COMMIT_OVERRIDE

…and instrument setup spans (#24452)

## Motivation

Span-instrumented full CI runs (since #24407 landed) now show where the
remaining e2e wall-clock goes: a large uninstrumented setup layer, a
cluster of proven-checkpoint/epoch waits that burn real time while
nothing is being built, and a 1s in-process poll cadence that adds
~0.5-0.9s of dead sleep to nearly every awaited tx. This PR takes the
low-risk warp folds and polling wins that are safe today, and adds span
instrumentation to the setup layer so the next round can attack the
~5,000s of currently-invisible beforeAll work. Everything multi-node is
validated on CI (hence `ci-no-fail-fast`); the last commit is a
deliberate slot-cut experiment kept isolated so it can be reverted on
its own.

## Changes

- **poll in-process nodes at 250ms instead of 1s** — TestWallet now
defaults its `send().wait()` poll interval to 0.25s (in-process nodes
reach CHECKPOINTED synchronously under automine and cheaply otherwise),
with the spartan/worker sites explicitly restored to the 1s default
since they talk to remote JSON-RPC nodes. The e2e-local wait helpers
(`wait_helpers.ts`, `waitForProvenChain`, gas-portal block advance,
L1->L2 message poll) drop from 1s to 0.25s. Removes ~0.5-0.9s of dead
sleep per awaited tx across ~600 tests. Production aztec.js/wallet-sdk
defaults are untouched.
- **warp past the epoch boundary in waitForProvenCheckpoint** — after
the multi-node block-production fixture stops its sequencers, warp the
L1 clock one epoch forward (forward-only, skipped if already proven) so
the epoch closes and the fake prover can prove+submit without waiting
the epoch out in real time. Targets ~716s of suite-summed
`wait:proven-checkpoint` across proposed_chain (~430s),
deploy_and_call_ordering (~143s), cross_chain_messages (~143s), plus
blob_promotion.
- **warp epoch waits in multi_proof and upload_failed_proof** — replace
`waitUntilEpochStarts` with `warpToEpochStart` in these two 12s-slot
proving tests (both passed under warp in the round-1 CI sweep).
proof_fails is deliberately left untouched.
- **register-only TestContract in automine pxe test** — the test only
calls the noinitcheck private `emit_nullifier`, so register the contract
instead of deploying it, dropping a deployment tx and its checkpoint
cycle.
- **compute genesis values on an ephemeral world state** —
`generateGenesisValues` used a full fsync-on
`NativeWorldStateService.tmp` per e2e container just to read one tree
root; switch to the fsync-off `ephemeral` backend. Adds a unit test
asserting `tmp` and `ephemeral` produce identical archive roots for a
funded-accounts genesis with non-zero timestamp (this path is
consensus-critical — CLI deploy paths compute the on-chain genesis root
through it).
- **instrument setup-layer deploys and mints with spans** — wrap the
top-offender setup helpers (fees harness token/FPC deploys + mints +
fee-juice bridge, cross-chain token/bridge deploys + mints, shared
auth-registry publish, gas-portal bridge) in `testSpan` under the #24407
taxonomy (`deploy:*`, `tx:mint`, `setup:bridge`, `setup:auth-registry`).
Zero behavior change (testSpan is a passthrough without
`TEST_TIMING_FILE`); this is the data source for round 4's attack on the
~5,000s of uninstrumented beforeAll work.
- **cut multi_validator_node slots 36s -> 16s** (experiment, last
commit) — lower `aztecSlotDuration` 36->16 and `blockDurationMs`
6000->2000 together (eth stays 8). Small expected saving; kept as the
final isolated commit so it can be reverted alone if CI shows
committee/attestation trouble on this file.

One planned item was dropped: an opt-in warp for
`ChainMonitor.waitUntilL2Slot`. All three candidate call sites turned
out to cover deliberate real-time building (live-sequencer coordination,
inactivity accumulation across an epoch, and the proof-boundary critical
window), so the opt-in API would have had no safe callers.

## Verification

Locally: full `yarn build`, `yarn format --check`, and `yarn lint` on
the touched packages all pass. The new world-state genesis-equivalence
unit test passes (`tmp` and `ephemeral` roots identical). The
`automine/pxe.test.ts` e2e passes as a smoke test for the register-only
change and the 250ms polling. Everything multi-node (the warp folds, the
timing cut) is validated on CI. Note the final commit is a deliberate
slot-cut experiment that can be reverted in isolation.



## Measured impact

Full green CI run of this PR (`9b4cc967`, CI `1782961631439529`) vs the
base-proxy full run (`d160265b`, CI `1782938936852228` — the branch
point plus one unrelated one-file test change). Identical test
populations: 2051 rows, all passed, in both runs. Sums are across
parallel processes, not wall-clock (methodology in the Linear "Times
tracking" doc).

| Bucket | Base | This PR | Δ |
|---|---|---|---|
| Overall | 7h 26m 03s | 6h 36m 20s | **−49m 43s (−11.1%)** |
| Setup (before-hooks) | 2h 14m 31s | 1h 45m 44s | −21.4% |
| — of which setup.ts | 42m 53s | 34m 13s | −20.2% |
| Body | 5h 05m 14s | 4h 45m 03s | −6.6% |
| Teardown | 6m 17s | 5m 33s | −11.6% |

By mechanism:

- **Proven-checkpoint warp**: `wait:proven-checkpoint` 14m 12s → 3m 32s
(**−75%**), worst single wait 215s → 79s. Suite deltas match span deltas
~1:1 — proposed_chain −67%, deploy_and_call_ordering −45%,
cross_chain_messages −21%. Hard attribution.
- **Epoch warps**: multi_proof −52% (wait:epoch 92s → 40s).
upload_failed_proof's warp also worked (35s → 20s) but was masked in the
suite total by one-shot proving noise in that run.
- **250ms polling + ephemeral genesis**: ~30–35 min spread across the
whole suite — 141 of 152 suites improved; every `setup:env:*` span
dropped ~16–50% with identical counts (same work, less waiting).
- **Slot-cut experiment**: multi_validator_node 103s → 84s (−18.6%) and
passed CI — the experiment survives.
- **Register-only pxe test**: 18.2s → 10.1s.
- **New setup spans**: ~44 min/run of previously untagged setup time is
now attributable (`setup:auth-registry` 16m, `setup:bridge` 10m,
`tx:mint` 8m, `deploy:token` 7m, `deploy:fpc` 2m) — the target list for
round 4.

11 suites regressed (~8 min total vs ~58 min of improvements), all in
untagged real-time slashing/proving bodies whose tagged waits are
unchanged — consistent with run-to-run variance, not PR effects
(per-suite noise floor between same-day runs is median ~11s / p90 ~52s).
spalladino and others added 6 commits July 6, 2026 12:41
…24481)

L2 slot boundaries must land on L1 slot boundaries, but nothing enforced
this at runtime. A check for exactly this relationship already existed
(`validateNetworkConsensusConfig` in stdlib), but it was only exercised
by a CLI unit test gating the three static mainnet/testnet/devnet config
files — never wired into e2e test setup or the L1-contract deployment
path itself.

## Approach

- Add `validateSlotDurations`/`assertValidSlotDurations` to
`ethereum/src/config.ts` and call it from `deployAztecL1Contracts`, the
shared choke point used by e2e tests, spartan deployments, and CLI
deploys, so a bad pairing now fails fast with a clear error.
- Refactor `validateNetworkConsensusConfig` to reuse the shared check
instead of duplicating it.
- Fix the three e2e configs this caught: all paired `aztecSlotDuration:
36` with `ethereumSlotDuration: 8` (4.5 L1 slots per L2 slot). Corrected
to `ethereumSlotDuration: 12`, matching the `36s`/`12s` convention
already used elsewhere in the same test suites.
)

## Human-written summary

Invalid numeric values in config were silently ignored and replaced with
the default. Also, config entries defined with `numberConfigHelper`
accepted non-integer values but silently truncated them.

## Context

The numeric config helpers mishandled bad input in two ways:

- `numberConfigHelper` / `optionalNumberConfigHelper` parsed with
`parseInt`, which silently truncated decimals — an integer config set to
`0.8` became `0` with no warning. In the worst case (A-1312) that turned
a fractional retention window into `0`, i.e. "delete immediately".
- `numberConfigHelper`, `floatConfigHelper` and `percentageConfigHelper`
swallowed any unparseable value and returned the config default (the
JSDoc even documented "...or is invalid"), so a typo'd env var ran
silently with an unexpected value.

## Approach

The config default is for an **unset** env var only — empty/unset is
already resolved to the default before `parseEnv` runs, so the only
thing `parseEnv` returning a default achieved was hiding bad input. The
helpers now parse strictly and throw on any set-but-invalid value:

- `numberConfigHelper` / `optionalNumberConfigHelper`: parse with
`parseFloat` and require a safe integer (no more silent truncation).
- `floatConfigHelper` / `percentageConfigHelper`: require a finite
number; percentage additionally enforces 0–1.

`bigint`, `enum` and the secret helpers already threw on invalid input.
`booleanConfigHelper` is intentionally unchanged: `parseBooleanEnv` is a
total function (unrecognized tokens map to `false`), it never
substitutes the configured default.

Auditing all ~150 `numberConfigHelper` call sites then surfaced options
that legitimately take fractional values and would now wrongly reject
them; these were moved to `floatConfigHelper`:

- p2p gossipsub tx scoring — topic weight, invalid-message-delivery
weight, and decay (default `0.5`), which feed libp2p's float
`TopicScoreParams`.
- sequencer `perBlockDAAllocationMultiplier` (default `1.5`); its
sibling `perBlockAllocationMultiplier` already used `floatConfigHelper`.
- L1 tx fee percentages — `gasLimitBufferPercentage`,
`priorityFeeBumpPercentage`, `priorityFeeRetryBumpPercentage`.
- bot `minFeePadding`, consumed as the fractional overpay factor `1 +
padding`; its zod schema no longer pins it to an integer.

## Impact

A node whose numeric config env var (or CLI flag) is set to a
non-numeric, fractional (for integer options), or out-of-range value now
fails at startup with a clear error instead of silently running with a
truncated or default value. Operators relying on the old lenient
behavior must correct the value.

Fixes A-1398
…(A-1401) (#24537)

## Context

A-1351 (#24515) fixed the honest-proposer and relay path for
non-canonical yParity attestation signatures, but a malicious selected
proposer can still hand-craft `propose()` calldata carrying a
non-proposer signature slot with `v ∈ {0,1}` (or an all-zero `(r,s,v)`
in a bitmap-marked signature slot). L1 accepts this at propose time (it
only recovers the proposer's own slot) and stores the attestations hash
over the raw bytes. Afterwards, either:

- the slot recovers to the correct member, so honest nodes deem the
checkpoint valid and nobody invalidates it — but epoch proving reverts
at `ECDSA.recover` (which rejects `v ∉ {27,28}`), a silent stall; or
- the slot is detected as invalid, but the invalidation repack via
`packAttestations` is not byte-faithful, so the recomputed hash diverges
from the stored one and `InvalidateLib` reverts — wedging the chain
until prune.

This is the malicious-proposer sibling of A-1351; it survives that fix.
Tracks aztec-claude#650.

## Approach

TS-only — no L1 changes, since this targets a fresh deployment. Two
parts:

- **Detection** — `getAttestationInfoFromPayload` no longer passes
`allowYParityAsV: true`, so TS signature validity matches L1's
`ECDSA.recover`. A non-canonical slot is now classified as an invalid
attestation, so honest nodes detect the bad checkpoint instead of
silently accepting it.
- **Byte-faithful invalidation** — the raw packed
`CommitteeAttestations` tuple from the checkpoint calldata is threaded
verbatim (as `verbatimAttestations`) through the negative
`ValidateCheckpointResult` into `buildInvalidateCheckpointRequest`,
which submits those exact bytes rather than repacking. With the original
bytes, `invalidateBadAttestation` reproduces the stored hash and L1's
`tryRecover` returns `address(0) ≠ member`, so invalidation succeeds.

`verbatimAttestations` is a required field on the negative result and is
serialized unconditionally — a repack fallback is intentionally absent,
since silently repacking would re-introduce the wedge.

The gossip sender-recovery path (`recoverCoordinationSigner`)
deliberately stays lenient (`allowYParityAsV`) so an honest node can
still attribute a yParity-encoded message and canonicalize it on
ingress; only the on-chain checkpoint validation is made strict. A
comment documents the split so it is not accidentally unified.

## Tests

An e2e regression under the invalidation suite
(`invalidate_block.parallel.test.ts`, "proposer invalidates checkpoint
with a yParity attestation slot") demonstrates the attack red→green: a
malicious proposer lands a checkpoint whose non-proposer attestation
slots carry raw yParity bytes, and the honest next proposer invalidates
it — which only succeeds when the invalidation submits the verbatim
calldata bytes (a repack diverges from the on-chain `attestationsHash`
and reverts). Plus unit coverage for the strict attestation-info
classification, the byte-faithful passthrough, and the result
round-trip.

An attester-side variant (a committee member emitting yParity
attestations) is not separately tested: the honest proposer's
`orderAttestations` normalization (A-1351) canonicalizes the byte before
the L1 bundle, so it never reaches L1 and produces the same on-chain
outcome as — and is subsumed by — the malicious-proposer case above.

Fixes A-1401
## Problem

`l1_tx_utils.test.ts` flaked in CI
([log](http://ci.aztec-labs.com/390c881d64e094b6), seen on
[#24454](#24454 (comment)),
which is unrelated to the failure). The failing test was `monitors all
sent txs`:

```
● L1TxUtils › L1TxUtils with blobs › monitors all sent txs
  TimeoutError: L1 transaction 0x82cf...aa3a timed out
      at TestL1TxUtils.monitorTransaction (l1_tx_utils/l1_tx_utils.ts:601:11)
```

## Root cause

The test creates the monitor promise and only attaches its rejection
expectation after several intervening awaits:

```ts
const monitorPromise = gasUtils.monitorTransaction(state);
await sleep(100);
await cheatCodes.mineEmptyBlock();
await expect(monitorPromise).rejects.toThrow('timed out'); // handler attached here
```

Anvil timestamps have 1s granularity, so when the wall clock crosses a
second boundary between the send and the monitor's timeout check, the
monitor (with `txTimeoutMs: 200`, `checkIntervalMs: 100`) can observe
the L1 timestamp already 1s past `sentAt` and reject **before**
`mineEmptyBlock()` completes. In the failing run the timeout fired at
`16:41:20.355`, before the empty block was mined at `.356` and before
the `.rejects` handler attached at `.357`. Node emitted
`unhandledRejection` in that window and jest 30 reported it as the test
failure — which is why the failure stack has no test-file frame and the
test's own assertions all passed in the log.

The same file already uses the safe idiom in a dozen newer tests: attach
the handler synchronously with `.catch(err => err)` and assert via
`resolves.toBeInstanceOf(TimeoutError)`.

## Fix

Apply that established pattern to the three remaining tests that expect
a monitor timeout but attach the handler only after intervening awaits:

- `stops trying after timeout once block is mined`
- `attempts to cancel timed out transactions`
- `monitors all sent txs` (the observed flake)

Test-only change; no product code touched. `TimeoutError` is exactly
what `monitorTransaction` throws on timeout, so the assertions are
equivalent-or-stricter than the previous message matching.

Evidence: CI log http://ci.aztec-labs.com/390c881d64e094b6 (search for
`monitors all sent txs`).

---
*Created by
[claudebox](https://claudebox.work/v2/sessions/0959caa9f03f59fa) ·
group: `slackbot`*
…24546)

## Problem

`e2e_ha_full.parallel.test.ts` ("should produce blocks with HA
coordination and attestations") failed in CI on an unrelated PR (#24492)
before the test even started: Docker Hub returned a transient 502 while
`docker compose up` pulled `postgres:16-alpine` for the HA compose
stack.

CI log: http://ci.aztec-labs.com/cac25b9251c99496

```
postgres Error unknown: failed to resolve reference "docker.io/library/postgres:16-alpine":
unexpected status from HEAD request to https://registry-1.docker.io/v2/library/postgres/manifests/16-alpine: 502 Bad Gateway
```

This can hit any compose-based test (`run_compose_test` is shared by the
e2e compose/web3signer/ha suites and docs examples) whenever a required
image is not in the local Docker cache and the registry blips.

## Fix

Wrap the `docker compose up -d --force-recreate` in
`ci3/run_compose_test` with the existing `ci3/retry -p <regex>` helper,
retrying only failures that match transient registry/network errors
(5xx, HEAD-request failures, TLS/connection timeouts, rate limiting).
This reuses the same pattern-gated retry convention already used for
network flakes in `l1-contracts/bootstrap.sh`,
`barretenberg/sol/bootstrap.sh`, and
`noir-projects/aztec-nr/bootstrap.sh`, so genuine failures (e.g. an
image tag that doesn't exist) still fail immediately without retrying.

Retrying `up -d --force-recreate` is idempotent: a retry recreates any
containers from a partially failed attempt.

Verified the exact error message from the CI log matches the retry
pattern (retried, then succeeds), while a `manifest not found` failure
exits immediately without retries.

No tracked issue exists for this flake.

---
*Created by
[claudebox](https://claudebox.work/v2/sessions/0959caa9f03f59fa) ·
group: `slackbot`*

---------

Co-authored-by: Santiago Palladino <santiago@aztec-labs.com>
@spalladino
spalladino requested a review from charlielye as a code owner July 6, 2026 20:48
AztecBot and others added 5 commits July 6, 2026 21:05
…uplicate_proposal test (#24557)

## Problem

`multi-node/slashing/duplicate_proposal › slashes validator who sends
duplicate proposals`
(`yarn-project/end-to-end/src/multi-node/slashing/equivocation_offenses.test.ts`)
flaked in CI on the unrelated PR #24556 with:

```
Target proposer 0x90f79bf6eb2c4f870365e785982e1f101e93b906 not found in any slot after 20 epoch attempts
```

Evidence: http://ci.aztec-labs.com/eb0b4e658e28440c (the sibling
`duplicate_attestation` case in the same file passed in the same run).

## Root cause

Pure statistics — no logic bug. `advanceToEpochBeforeProposer`
(`yarn-project/end-to-end/src/multi-node/slashing/setup.ts` on this
branch) scans upcoming epochs for one where the target validator is the
proposer, giving up after `maxAttempts = 20` epochs. The test calls it
with the default.

This suite uses a 2-slot epoch with `warmupSlots = 1`, so each epoch
attempt inspects exactly **one** candidate slot. The proposer for a slot
is `keccak256(abi.encode(epoch, slot, seed)) % committeeSize`, and with
the suite's 4-member committee (`COMMITTEE_SIZE = NUM_VALIDATORS = 4` in
`setup.ts`) each attempt is an independent 1/4 draw. Miss probability
over 20 attempts: `(3/4)^20 ≈ 0.32%`, i.e. roughly 1 in 315 runs —
matching the observed occasional flake.

## Fix

Raise the default `maxAttempts` from 20 to 50, bounding the miss
probability at `(3/4)^50 ≈ 5.7e-7` (~1 in 1.8M). Each attempt is only a
committee query plus an anvil timestamp warp, so the expected attempt
count stays ~4 and the extra headroom costs essentially nothing. The
raised default also covers this helper's other callers with the same
exposure (`broadcasted_invalid_block_proposal_slash`,
`broadcasted_invalid_checkpoint_proposal_slash`), none of which override
`maxAttempts`.

## Relationship to PR #24548

This is the same failure mode fixed by #24548 for the
`duplicate_attestation` flake, but #24548 does **not** cover this call
site: its patch touches `yarn-project/end-to-end/src/e2e_p2p/shared.ts`,
a file that does not exist on `merge-train/spartan-v5` (on this branch
the helper lives in `multi-node/slashing/setup.ts`, and there is no
`e2e_p2p/` directory). #24548's head branch was cut from a `next`-layout
tree while targeting `merge-train/spartan-v5`, which is why GitHub
reports it as dirty with ~4k changed files — it needs a rebase onto the
branch it targets, at which point its fix would land in this same file.
This PR applies the fix directly to the `merge-train/spartan-v5` copy of
the helper.

No tracked issue exists for this flake.

---
*Created by
[claudebox](https://claudebox.work/v2/sessions/0959caa9f03f59fa) ·
group: `slackbot`*
…test (#24543)

## Flake

`multi-node/block-production/multi_validator_node.parallel.test.ts`,
case "should build blocks & attest with multiple validator keys", failed
on [#24537](#24537)
(unrelated PR) with:

```
TypeError: Cannot read properties of undefined (reading 'checkpoint')
    at checkpoint (multi-node/block-production/multi_validator_node.parallel.test.ts:92:73)
```

CI log: http://ci.aztec-labs.com/0f321ffb9fa923af

## Root cause

`deployContractAndGetAttestedCheckpoint` reads the published checkpoint
from the archiver immediately after the deploy tx receipt turns mined:

```ts
const [publishedCheckpoint] = await dataStore.getCheckpoints({ from: blockData!.checkpointNumber, limit: 1 });
const payload = ConsensusPayload.fromCheckpoint(publishedCheckpoint.checkpoint, ...); // undefined here
```

The receipt turns mined as soon as the block is built locally, but the
archiver only stores the *published* checkpoint (with its attestations,
which come from the L1 `propose` calldata) once its L1 sync downloads
that tx. In the window between the two, `getCheckpoints` returns `[]`
and `publishedCheckpoint` is `undefined`.

The CI log shows the race directly: the test node published checkpoint 1
to L1 at `15:17:57.694` (`sequencer:publisher ... Published checkpoint 1
at slot 14`), and its archiver logged `Downloaded checkpoint 1` at
`15:17:57.795` — by which point the test had already failed and teardown
was running.

This is a test-side race, not a production bug: the archiver API is
behaving as documented for a not-yet-synced checkpoint.

## Fix

Poll for the checkpoint with `retryUntil` instead of reading it once,
mirroring the existing pattern in
`single-node/cross-chain/cross_chain_messaging_test.ts`
(`advanceToEpochProven`), which wraps the same lookup in
`retryUntil(..., 'archiver indexes checkpoint N', 120, 0.5)`.

Note: the `next`-line variant of this test
(`e2e_multi_validator/e2e_multi_validator_node.test.ts`) has the same
unguarded lookup in two places and could flake the same way; that file
is on a different base branch, so it is not touched here.

Based on `merge-train/spartan-v5` because the flaking file only exists
on the v5 line (it was consolidated into `multi-node/` there), and the
failing run was on a PR targeting that branch.

---
*Created by
[claudebox](https://claudebox.work/v2/sessions/0959caa9f03f59fa) ·
group: `slackbot`*
…during shutdown (v5 line, partial) (#24551)

## Flake

`multi-node/slashing/sentinel_status_slash.parallel.test.ts` ("slashes
an attestor that gets stopped after the network is running") flaked on
PR #24507 (unrelated change). CI log:
http://ci.aztec-labs.com/9bb09d2f904e290a

## What actually failed

The test itself **passed**. Immediately after, the node process crashed
with an **unhandled rejection** (`ECONNREFUSED` on anvil after teardown)
— something was still polling L1 after shutdown. Root cause:
`L1TxUtils.monitorTransaction`'s loop calls `getL1Timestamp()` at the
top of its loop, outside the loop's own try/catch, and nothing waited
for in-flight monitor iterations to actually stop during shutdown.

## Scope of this PR (rebased onto `merge-train/spartan-v5`)

This PR was originally written against `next` and touched 4 files. After
rebasing onto `merge-train/spartan-v5`, 2 of those files cherry-picked
cleanly and are included here:

- `yarn-project/ethereum/src/l1_tx_utils/l1_tx_utils.ts` — guards the
loop-top `getL1Timestamp()` call to break quietly when interrupted.
- `yarn-project/sequencer-client/src/publisher/sequencer-publisher.ts` —
adds `.catch` to a bare fire-and-forget `backupDroppedInSim(...)` call.

The other 2 files (`publisher_manager.ts`, `checkpoint_proposal_job.ts`)
have diverged in structure on this branch and needed their own
investigation rather than a blind cherry-pick — that's covered
separately in
[#24560](#24560),
which found `publisher_manager.ts` had the same defect (fixed there) and
`checkpoint_proposal_job.ts` did not (this branch already routes
fire-and-forget L1 requests through a `RequestsTracker` that attaches
its own rejection handler).

No tracked issue exists for this flake; reference only.

---
*Created by
[claudebox](https://claudebox.work/v2/sessions/0959caa9f03f59fa) ·
group: `slackbot`*
…ts during shutdown (v5 line) (#24560)

Ports the remaining part of the `sentinel_status_slash` flake fix to the
v5 line. The flake crashed the jest process after all tests passed: an
in-flight `eth_getBlockByNumber` against the torn-down anvil surfaced as
an unhandled `L1RpcError` (`ECONNREFUSED`). CI log:
http://ci.aztec-labs.com/9bb09d2f904e290a

This complements #24551, which (after retargeting to
`merge-train/spartan-v5`) now carries only the two files that
cherry-picked cleanly from `next` — `l1_tx_utils.ts` (guard the loop-top
`getL1Timestamp()` in `monitorTransaction`) and `sequencer-publisher.ts`
(`.catch` on the fire-and-forget `backupDroppedInSim`). The other two
files had diverged on this branch and are addressed here; #24551's
title/body should be updated to reflect that it covers 2 of the original
4 files, with this PR covering the rest.

### `yarn-project/ethereum/src/publisher_manager.ts` — fixed

This branch's `stop()` (which, unlike `next`, also clears the `started`
flag and supports restart via `start()`) interrupted all publishers and
the funder but returned immediately, so an in-flight tx monitor loop
could still be issuing L1 RPC calls after test teardown. Applied the
equivalent of the `next` fix, fitted to this branch's structure:
`stop()` now awaits `waitMonitoringStopped()` on every publisher and the
funder before returning. `L1TxUtils.waitMonitoringStopped(timeoutSeconds
= 10)` already exists on this branch, is self-bounded, and swallows its
own timeout with a warning, so shutdown cannot hang.

###
`yarn-project/sequencer-client/src/sequencer/checkpoint_proposal_job.ts`
— no change needed

On `next`, the votes-only path assigned a bare `pendingL1Submission =
publisher.sendRequestsAt(...)` with no rejection handler, and only the
last job's submission was awaited at shutdown, so an orphaned job's
rejection could crash the process. On this branch that field no longer
exists: both fire-and-forget paths go through
`this.pendingRequests.trackRequest(promise, () => this.interrupt())`,
backed by the sequencer's shared `RequestsTracker`
(`requests_tracker.ts`). `trackRequest` attaches a rejection handler
synchronously at track time (`promise.then(delete, delete)`), so a
tracked promise can never surface as an unhandled rejection regardless
of whether shutdown awaits it. Additionally, `Sequencer.stop()` calls
`pendingRequests.interruptRequests()` followed by `awaitRequests()`, so
all tracked requests — not just the last one — are interrupted and
drained at shutdown. The `next` bug does not exist here, so no change
was forced. (Minor observability note, not a bug: a rejection on the
votes-only path is swallowed silently by the tracker rather than
logged.)

Verified `publisher_manager.ts` parses cleanly; the full workspace
type-check runs in CI (this container lacks the bootstrapped
bb.js/noir/l1-artifacts toolchain).

No tracked issue.

---
*Created by
[claudebox](https://claudebox.work/v2/sessions/0959caa9f03f59fa) ·
group: `slackbot`*

@ludamad ludamad left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🤖 Auto-approved

@AztecBot
AztecBot enabled auto-merge July 7, 2026 03:02
@AztecBot

AztecBot commented Jul 7, 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 Jul 7, 2026
@AztecBot

AztecBot commented Jul 7, 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.

Merged via the queue into v5-next with commit 1df88ac Jul 7, 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