chore(release): 5.2.0 - #25225
Merged
Merged
Conversation
…bmission-window expiry Decouple "a checkpoint prover failed" (a fact) from "the epoch failed" (a decision). A proving or L1-submission fault now settles the EpochSession in the non-declaring terminal 'stopped' instead of 'failed'; the reconciler rebuilds the epoch over current canonical content each tick (retry-to-converge), cheap because the broker reuses already-completed sub-proofs. An epoch is declared terminally failed — with its post-mortem upload — only when its L1 proof-submission window closes with the proven tip settled and the epoch still unproven, from ProverNode.expireEpoch. This removes the racy, lagging-replica "was this a prune?" classification entirely. Deletes lastTickEpoch (the epoch-keyed anti-retry gate), the checkpointsMatch upload-suppression in SessionManager.runSession, and the onSessionFailed callback. The post-mortem upload moves to tryUploadEpochFailure(epoch, checkpoints), built from the store's last-known canonical provers.
…fault path directly Address review feedback on the retry-to-converge change: - checkEpochExpiry was called from both handleBlockStreamEvent and the periodic ticker, so two sweeps could interleave and both upload a post-mortem for the same epoch before either advanced lastExpiredEpoch. Drop the inline block-stream call: the ticker (a RunningPromise, which never overlaps its own runs) is now the sole driver, so the high-water mark advances — and each epoch uploads — exactly once. Expiry is a background sweep keyed off the archiver's synced slot; it never needed to be on the event path. The A-1041 tips-unadvanced guard now covers only the registration/prune handling that genuinely needs it. - Add a checkpoint-prover test for the actual data-plane race: dbProvider.fork rejecting mid-proof rejects whenBlockProofsReady(), which the EpochSession maps to 'stopped'. Point the expiry unit tests at checkEpochExpiry directly rather than through a block-stream event.
The expiry sweep no longer runs from handleBlockStreamEvent. Rename "Per-event expiry sweep" to "Periodic expiry sweep", redraw the diagram around the expiryTicker (RunningPromise) as the sole driver, and fix the prose: the high-water mark advances per sweep (not per event) and is seeded from resolveLastFullyProvenEpoch. Drop the stale getCheckpointsData and computeStartupState references (expireEpoch uses getBlocks; there is no computeStartupState).
… re-proved every tick Retry-to-converge was naively per-tick: for an epoch that keeps failing, every tick cleared the stopped session and re-created a fresh one, re-running proving work until the deadline for no benefit. Key the retry off content instead, per the original design: record the content key of a full session that ends in 'stopped', and have the tick skip an epoch whose current canonical content matches an already-failed attempt. Recovery is unaffected — it flows through the ungated checkpoint/prune triggers, which fire on a genuine change (a re-add or reorg, including an identical-content re-add whose world-state has resettled) and reopen the epoch regardless. The gate resets when the epoch is proven, expires, or the proven frontier passes it. Only full sessions are affected: partials are opened solely by an explicit startProof and are never reopened by the tick or by events, so they never entered the re-spin loop.
…er mark Replace the content-keyed retry gate (a per-epoch content-key map plus two helpers and record/clear bookkeeping) with the monotonic lastTickEpoch high-water mark: the tick opens an epoch once and does not re-create a session for it every tick. Recovery from a genuine change still flows through the ungated checkpoint/prune triggers, so a pruned-then-re-added epoch recovers exactly as before. The tradeoff — a transient failure on an already-complete epoch waits for the deadline rather than being auto-retried by the tick — is unchanged from the content-keyed version, at a fraction of the machinery.
…nt prover Replace the lastTickEpoch high-water mark with a check at the point of construction: a CheckpointProver whose block proofs rejected for a non-cancel reason (a sub-tree fault or a prune-induced fork fault) now records isFailed(), and the SessionManager refuses to open (or rebuild) an EpochSession over any set that contains a failed prover. A stuck epoch is therefore skipped cheaply each tick — no session, no re-proving — rather than being gated by per-epoch bookkeeping. This keeps the resiliency and drops the tick gate: a pruned/re-added epoch recovers because the re-add installs a fresh (non-failed) prover, and a session that stops with healthy provers (a transient top-tree/submit error) is still retried by the next tick. The failure lives on the prover, where it happened, with the store as the single source of truth.
…upload eagerly, not at expiry
The expiry-time post-mortem upload could never fire for a persistently-failing epoch:
by the time its window closes, its checkpoint provers have been pruned, so there was
nothing to upload (the upload_failed_proof e2e test hung as a result).
Give EpochSession a genuine-failure state, told apart by the checkpoint provers'
isFailed() flag:
- a fault while a prover under it failed → 'stopped' (maybe a prune): not uploaded,
not retried over the failed prover, recovered on re-add.
- the session's own top-tree/submit work failed while every prover was healthy →
'failed' (hasFailed()): definitively not a prune, so it is race-free. The reconciler
retains such a full session (so the tick doesn't re-prove a deterministic failure)
and uploads a post-mortem once, eagerly, from the session's checkpoints.
Removes the fail-at-expiry upload (expireEpoch is back to chonk-release + reap only);
reinstates the onSessionFailed → tryUploadEpochFailure wiring on the genuine-failure
path. Reverts the e2e test to warp-to-epoch-1 + the eager upload trigger.
…st per failed session A checkpoint prover that fails to produce its block proofs (a sub-tree fault or a prune-induced fork fault) now fires an onFailed callback, and ProverNode uploads a snapshot for that single checkpoint via tryUploadCheckpointFailure. This captures a genuine checkpoint proving failure that ends its session in 'stopped' — which the session-level upload (only on a session's own 'failed') deliberately does not cover. The checkpoint upload fires for prune-induced faults too, on purpose: a prune-caused checkpoint snapshot is harmless, and not trying to tell prune from genuine failure is what keeps it race-free. A cancelled prover (control-plane prune / shutdown) is not a failure and does not upload.
…kpoint upload Add rerunCheckpointProvingJob: reuses the epoch rerun's offline setup (world state + archiver snapshot, local broker/prover, replaying tx provider) but rebuilds just the one checkpoint's sub-tree prover and awaits its block proofs — no epoch top-tree or L1 submit. Extract the shared setup into createRerunContext / buildCheckpointProver. Add a test-only checkpointProveOverride hook (CheckpointProverDeps → CheckpointStore setTestHooks → ProverNode.setCheckpointHooks) so a test can force a sub-tree failure, mirroring the existing session topTreeProveOverride hook. Extend upload_failed_proof.e2e with a second test: force a checkpoint sub-tree failure, capture the eager per-checkpoint upload URL via tryUploadCheckpointFailure, download, and re-prove that single checkpoint with rerunCheckpointProvingJob.
…cle flags; drop public isCompleted completed/failed/cancelled are three orthogonal facts, not a single status — a prover can be completed+cancelled (routine teardown) or completed+failed (enqueued then the sub-tree faulted); only failed+cancelled is excluded. Add a comment explaining why they aren't one enum, with per-field docs. isCompleted() had no callers outside tests (internally the `completed` field is used directly), so remove the public getter and the two secondary test assertions that used it.
…ke-epoch-proving-robust-to-prune-induced-fork
…l/a-1418-prover-node-make-epoch-proving-robust-to-prune-induced-fork
…s executed A mainnet sequencer kept signalling a governance payload whose proposal had already been executed, wasting ~100k gas per slot on a signal the canonical rollup rejected. Three fixes: - Fix the `ProposalState` enum, which was missing `Droppable` (Solidity `IGovernance` has 9 states). The mismatch made Solidity `Expired` decode as out-of-range and throw in `asProposalState`, so the publisher failed open and signalled anyway. Add an explicit `LIVE_PROPOSAL_STATES` set for the sweep. - Replace `hasActiveProposalWithPayload` (boolean) with `getPayloadProposalStatus` returning `'live' | 'executed' | 'none'`, matching a proposal by its stored payload directly or via its GSEPayload wrapper. The publisher now stops signalling an executed payload (memoized in-process, live takes precedence over executed) unless `GOVERNANCE_PROPOSER_FORCE_PAYLOAD_VOTE` is set, for payloads designed to be re-executed. - Add a canonicality guard: resolve the canonical rollup instance once and skip the signal when the configured rollup is not canonical, reusing that instance for round accounting and the EIP-712 digest so the read cannot race a canonical-rollup change.
…ser API Resolve the canonical rollup via getRollupAddress() instead of the getInstance() wrapper, dropping the now-unused instance parameter threaded through createSignalRequestWithSignature. Since the guard returns early when the configured rollup is not canonical, round accounting and the EIP-712 digest can just use the configured rollup address. Also drop the redundant sawExecuted flag from getPayloadProposalStatus (the executed verdict is already memoized in the set).
getL2ToL1MembershipWitness assembled its witness from several non-atomic archiver reads (getTxEffect, then the epoch blocks, target block and checkpoint metadata read inside computeL2ToL1MembershipWitness). A store commit landing between those reads could splice together two different chain states and surface a spurious "message does not exist" error even when the tx-effect index was healthy. Wrap the witness assembly in a single store.transactionAsync so every archiver read binds to one write transaction and observes a consistent snapshot (the store serializes writers, so no commit can interleave). The L1 Outbox roots fetch stays outside the transaction to avoid holding the archiver's writer lock across a network round-trip, and the tx effect is re-read inside the snapshot so the receipt's block number and tx index stay consistent with the block data. Add a kv-store test asserting reads inside a transaction see a consistent snapshot while a concurrent write is queued behind it.
…ed elsewhere BlockStore.removeBlocksAfter had two defects that corrupt the tx-effect index (txHash -> block position) when the same tx exists in two stored blocks, e.g. re-included after its original proposal expired: - deleteBlock removed #txEffects entries blindly by txHash, destroying the entry of a tx whose index already points at another stored block, which makes that block unreadable. - removeBlocksAfter skipped cleanup entirely for blocks it could not reconstruct, leaking their row, tx effects, and indices; a later insert at the same number then overwrites the row in place, leaving stale tx-effect entries pointing into the new chain. A stale entry makes getL2ToL1MembershipWitness resolve the tx at wrong coordinates and throw 'The L2ToL1Message you are trying to prove inclusion of does not exist' for a message in a proven block, and lets getTxReceipt report a proven position the chain no longer has. Cleanup now works from the raw storage row (so unreadable blocks are still fully released) and only deletes tx-effect entries still owned by the block being removed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…oval The ownership-checked delete silently skipped entries owned by another block. That state (two stored blocks sharing a tx) should be unreachable for honest chains, so skipping it silently hides direct evidence of an upstream bug. Warn on both anomalies (foreign-owned entry and missing entry) and correct the comment that attributed the duplication to routine proposal expiry, which cannot produce it.
The lazy KZG singleton (getKzg) builds its precomputation tables synchronously on first use, blocking the event loop for ~2s locally and 12-15s under production CPU limits. On an RPC node the first use is the archiver reconstructing blobs or the proposal handler uploading them, right after a checkpoint arrives, so the stalled loop overruns the gossipsub mcache window and attestation forwarding is skipped. Warm it in the node factory alongside the bb.js singleton, before any subsystem runs, keeping the cost off the gossip path.
Move the trusted-setup load timing and logging out of the node factory and into getKzg itself, gated on an optional logger argument, using elapsedSync for the measurement. Keeps the factory call site to a plain getKzg(log) and makes the timing available to any warm-up caller.
… jobId uploadEpochProofFailure already prefixes the upload path with the epoch number, so the epoch in the jobId was redundant — and the epoch-only string dropped the per-upload uniqueness the original session.getId() UUID gave. Use each entity's own id instead: the session's id for a session (epoch) failure, and the prover's content-addressed id for a checkpoint failure. Drop the now-unused epoch param from tryUploadEpochFailure.
The socket backend gave bb a hard 5s budget shared between socket file creation and connect, so a loaded prover spawning many bb processes at once (epoch top-tree) could fail startup with 'Timeout connecting to bb socket: unknown' before a single connect attempt was made, and the resulting plain Error was reported retry=false, costing the epoch. The backend now waits as long as the bb process is alive, failing fast with the real cause if it dies, with a single generous 60s backstop for a wedged process. Startup failures are wrapped as retryable ProvingErrors and the bb_prover wrap sites preserve the retry flag.
Unpinned, typescript now resolves to 7.x, which no longer ships lib/_tsc.js and crashes Yarn 4's builtin compat/typescript patch during install, failing docs/examples/bootstrap.sh before any example runs. Matches the existing pin in docs/examples/ts/bootstrap.sh.
Two prune-vs-failure gaps remained after decoupling checkpoint failure from epoch failure: - Session classification only checked isFailed(), missing a control-plane cancel that reaches start()'s catch before the reconcile marks the session 'cancelled'. Such a cancelled-but-not-failed prover was misclassified as the session's own 'failed', triggering a spurious full-snapshot upload for a prune. Treat a cancelled prover as prune-ambiguous too. - A data-plane fork fault reaches the checkpoint-level onFailed upload indistinguishable from a genuine sub-tree failure (no cancel has landed yet). Gate the upload on the archiver: a pruned checkpoint's last block is no longer canonical there, so skip the expensive world-state + archiver snapshot when it has been pruned out.
…24840) ## Problem The safe JSON-RPC server built success responses as `{ jsonrpc, id, result }`. When a handler returns `undefined`, `JSON.stringify` drops the `result` key, producing `{"jsonrpc":"2.0","id":N}` — a response with **neither `result` nor `error`**, which violates JSON-RPC 2.0 and leaves raw/external callers unable to tell "not found" from a malformed reply. Surfaced via `node_getContract` on an undeployed (but valid) address: the response had no `result` field at all. ## Fix One line in the shared server (`safe_json_rpc_server.ts`): coerce an `undefined` return to `null` (`result: result ?? null`, so `0`/`false`/`''` are preserved). The `result` key is now always present. ## Blast radius Framework-level, so it fixes every method that can return `undefined` — ~20 on the node interface (`getContract`, `getContractClass`, `getTxEffect`, `getTxByHash`, `getBlock`, the witness getters, synced slot/epoch, validator stats, …) plus every other server on this framework (PXE, prover-node, archiver). Void methods now also return `result: null`. ## Why it's safe for existing clients The TS client short-circuits `null`/`undefined`/`'null'`/`'undefined'` **before** schema validation, so it never runs `null` through an `.optional()` schema — omitted vs `result: null` both resolve to `undefined` client-side. Only raw/external callers change, and they now get a spec-compliant response. ## Testing - Added a test: a handler returning `undefined` now yields `{ jsonrpc, result: null }` (red before, green after). - Updated the existing void-method (`clear`) assertions, single and batch, to the new shape. - Added integration tests (`test/integration.test.ts`) pinning that optional result schemas tolerate `null` responses: - end-to-end: the wire response for an undefined return carries an explicit `result: null`, and the safe client resolves it to `undefined`; - client-isolated: a stubbed fetch returning `result: null` verbatim resolves to `undefined` against an `.optional()` schema instead of throwing a `ZodError`. Both go red if the client's null short-circuit is removed (verified: `Invalid input: expected object, received null`). - Full `json-rpc` suite: 102 passed; `foundation` lint clean.
Bumps `.release-please-manifest.json` on `v5-next` from **5.1.0 → 5.2.0** so v5-next nightlies track the next minor after v5.1.0 is cut. One-line change, mirrors the previous bump (`ee3716277a chore(release): 5.1.0`). Independent of the v5.1.0 merge (#24897) — that branch is frozen at 5.1.0. If preferred, this can instead be applied via a bypass push with the release bot and this PR closed.
aminsammara
requested review from
IlyasRidhuan,
charlielye and
sirasistant
as code owners
August 14, 2026 12:42
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
…osal type (#25222) Follow-up to #25207. That PR removed the duplicate downstream re-validation of inbound block and checkpoint proposals, leaving p2p ingress (gossipsub topic validation) as the single place proposals are validated. The downstream handlers document that precondition in prose; this PR enforces it with the type system. - Adds branded `ValidatedBlockProposal` and `ValidatedCheckpointProposalCore` types (plus their minting functions) in `stdlib/src/p2p/validated_proposal.ts`, following the existing `Branded<T, Brand>` convention used by `BlockNumber` and friends. - The p2p received-proposal callbacks (`P2PBlockReceivedCallback`, `P2PCheckpointReceivedCallback`) and the downstream consumers (`ValidatorClient.validateBlockProposal` / `attestToCheckpointProposal`, `ProposalHandler.handleBlockProposal` / `handleCheckpointProposal`, and the `Validator` interface) now take the branded types, so a raw inbound `BlockProposal` / `CheckpointProposalCore` cannot reach them. - The brands are minted only in `libp2p_service.ts`, at the three points where the topic validator has already returned `Accept`: the block-proposal topic path, the checkpoint-embedded block path (`processBlock` is only set after Accept), and the checkpoint path. - Purely a compile-time marker: no runtime validation is added and there is no behavior change. The only non-type edits are in tests, which mint validated proposals from constructed ones. Related to A-1703.
- Removes specific workflows, labels, and modes that we originally had introduced to test backwards compatibility. Instead, compat tests are enumerated via `compat_test_cmds` based on what is found in the `legacy-contracts` folder. - The `legacy-contracts` folder, by convention, holds legacy contract artifacts of historical versions we're interested in running compat ci against. It's up to each branch to determine that: after this PR is merged, `v5-next` will run compat tests on v5.0.1 and v5.1.0, which are the stable releases of the stack under protocol v5. `next` (after porting this PR), will keep `legacy-contracts` empty until a stable version of the stack is released under protocol v6. - Compat tests only run in `ci-full` mode, and according to my measurements they increase `ci-full` running time by ~10 minutes. For two stable versions (v5.0.1, v5.1.0), that comes down to around 5 minutes per stable release. Once we release v5.2.0, we can expect the overhead to be ~15 minutes. This is why I increased the AWS shutdown limit. - `ci-fast` runs are unaffected by these changes. - In `ci-full` mode, adds a check to enforce that the expected artifact tars are in the `legacy-contracts` folder. Note this will cause the first `ci-full` run **after** a stable release to fail with a hopefully clear enough message that artifacts for the new release need to be committed moving forward. This is purposedly designed not to block or interfere with releases.
Fixes two independent validation gaps around checkpoint shape. ## 1. A zero-tx block after the first in a checkpoint There is no circuit for proving a non-first block with zero transactions. The zero-tx block-root circuit (`rollup_block_root_first_empty_tx`) exists only so that an epoch can be proven when there are no transactions at all, and it is pinned to index 0: it starts a fresh sponge blob and carries a non-zero `in_hash`, both of which the block-merge continuity checks forbid at any later index. Validation methods had upper bounds on block contents but no lower bound, so a checkpoint carrying an empty block past the first would pass every node-side check and then be unprovable. **Fix**: `validateCheckpointStructure` now requires every block after the first to carry at least one tx, and the sequencer floors `minValidTxs` at 1 for any block past index 0 so it never builds one. The archiver passes `allowEmptyNonFirstBlocks` on the L1-ingest path, since such a checkpoint is already final on L1 and refusing to ingest it would only stall sync. ## 2. Block-count ceiling in network config Network configuration allowed `maxBlocksPerCheckpoint` to be set higher than the attestable limit, which would produce block indices that p2p and checkpoint validation reject. It is now capped at `MAX_ATTESTABLE_BLOCKS_PER_CHECKPOINT`. Fixes A-1614
Add metrics to the JSON-RPC server. Fix A-1677.
Backport staging PR. Body will be updated with commit list.
See [merge-train-readme.md](https://github.com/AztecProtocol/aztec-packages/blob/next/.github/workflows/merge-train-readme.md). This is a merge-train.
…25206) Fixes a race where `worldState.syncImmediate(N)` failed with `block_not_available` even though the archiver had block N, alerting `Eviction rule FeePayerBalanceEviction failed` on production nodes. ## Context `EventDrivenL2BlockStream.sync()` goes through `RunningPromise.trigger()`, whose pending-request slot was only cleared after the pass serving it completed. Since the world-state stream is event-driven, nearly every block-processing pass serves a request, so a `sync()` arriving mid-pass attached to a pass that started before the archiver committed the target block and resolved at N-1. Additionally, a trigger coalescing onto a pending request silently dropped its event payload, so the next pass could run armed with an older event's hot-block cache, which capped `getL2Tips()` below the archiver's real tip. Both mechanisms produce the observed error; the 100ms fallback poll self-healed right after. ## Approach - `RunningPromise` now captures and clears the request slot when a pass starts, so `trigger()` resolves only after a full run that started after the call; unserved requests are rejected when the loop exits instead of hanging; the per-trigger argument is removed entirely. - `EventDrivenL2BlockStream` is reduced to a doorbell: the archiver's aggregate event just triggers an immediate reconciliation pass, and every pass reads tips and blocks authoritatively from the source. The `HotBlockSourceAdapter` fast path and the event's `blocksAdded` payload are deleted — their arming conditions were the source of the race, and their only real saving was block-body re-hydration on event-triggered passes. - The latency win that motivated the event-driven stream (#24317) is preserved; if the removed body-read saving ever shows up in profiles, it should be recovered inside the archiver with a content-addressed block cache instead (tracked separately). Fixes A-1659
…tivity (#25185) Gates p2p-dependent operations on actual gossip connectivity, so a node running with a dead libp2p stack (zero peers) can no longer file false slashing accusations, burn its proposer slots, or silently blackhole txs. Follows an incident where a validator whose TCP listener failed to bind kept running peerless and voted `DATA_WITHHOLDING` against entire committees. One commit per concern (squashed on merge): 1. **`feat(p2p): expose p2p connectivity`** — new `getP2PConnectivity(): { enabled, connectedPeers }` on the p2p service, client, and node RPC API. The dummy (p2p-disabled) implementation reports `enabled: false`, so sandbox and single-node setups are vacuously healthy for every gate below. 2. **`fix(slasher): do not file data-withholding offenses while peerless`** — the data-withholding watcher infers offenses from the *absence* of txs in the local pool, which is invalid evidence without gossip connectivity. It now permanently skips (never backfills) slots probed while p2p is enabled with zero peers — after reconnecting, mined txs may already be evicted from the pool, so a late probe would still false-positive. 3. **`fix(sequencer): skip proposing when node has no connected peers`** — a peerless proposer broadcasts into the void (burns the slot) or, with enough own committee seats, publishes a checkpoint whose data nobody has. It now skips building below `minPeersToPropose` (default 1, `SEQ_MIN_PEERS_TO_PROPOSE`, 0 disables) while keeping L1 duties (votes, prune, invalidation) via the existing fallback. 4. **`feat(node): report per-component health on GET /status`** — `/status` now returns `{ ok, components: { p2p: { healthy, enabled, connectedPeers }, ... } }`. The `p2pHealthMinPeers` floor (`P2P_HEALTH_MIN_PEERS`) defaults to 0 (report-only) so fresh-network bootstrap nodes aren't failed by default; operators opt in. 5. **`fix(node): reject sendTx when node has no peers`** — instead of accepting a tx and gossiping it to nobody, the node rejects immediately so callers can retry against a healthy node. 6. **`test: keep single-node mock-gossip e2e working`** — `registerPhantomGossipPeer` test helper so the one single-node e2e test on the in-memory mock gossip bus presents one connected peer to the gates. 7. **`fix(slasher): do not file data-withholding offenses when p2p is disabled`** — a node with no p2p stack at all sees strictly less than a peerless one, so it must not slash either. The runtime gate drops `enabled` (any node with zero peers skips), and the watcher isn't constructed when `p2pEnabled` is false. Unlike the other gates, this one infers guilt from *absence*, which is never valid without gossip; the propose/`/status`/`sendTx` gates keep treating `enabled: false` as vacuously fine because they gate the ability to *act*, which a deliberately p2p-less node has by design. Strongly suggest to **review commit by commit**. Related PRs (independent, same incident): #25177 (fail node startup when the p2p service fails to start), #25183 (periodic zero-peers warning). Part of A-1701.
) ## Context The node's five L1 event watchers (slasher rotation, slashing votes/rounds, checkpoint invalidation, slash events) used viem's `watchContractEvent`, which installs a server-side filter and polls `eth_getFilterChanges`. Server-side filters die in the field: load balancers route polls to backends that never saw the filter, and providers report purged filters with error codes viem does not treat as "filter gone" (reth -32602, Alchemy -32600; erigon returns an empty result with no error). Watchers then either churn recreating filters or hammer a dead filter id at the poll rate — operators reported 30-40 req/s of spam — while silently missing slashing and governance events. ## Approach Replace the viem watchers with a custom poller, `watchContractEvent` in `@aztec/ethereum`. An earlier revision of this PR instead excluded the filter RPC methods at the transport level to force viem into its built-in getLogs fallback, but that fallback retries filter creation on every tick and issues a single unbounded `eth_getLogs` when catching up after an outage, so we now own the loop: - Each tick polls `eth_blockNumber` (viem caches it per client at the client's polling interval, so concurrent watchers share one request) and fetches new logs with `eth_getLogs` via `client.getContractEvents`, never touching filter RPCs. Requests never span more than a configurable `maxBlockRange` (default 100) blocks: catch-up after downtime is chunked into multiple bounded requests, and the cursor advances per successful chunk so a mid-catch-up failure retries from where it left off instead of losing blocks. - The helper is strongly typed on the event via viem's `ContractEventName` / `GetContractEventsReturnType` helpers (including `strict` decoding), and is built on foundation's `RunningPromise`, so ticks never overlap and unwatching interrupts a catch-up loop. - Per-log callback isolation (a throwing callback or a rejected returned promise is logged without dropping the rest of the batch), and every failed poll logs a warning. - Poll intervals per event: `SlasherUpdated` polls every 60s since slasher rotations are rare governance operations; the other four watchers poll at the client's polling interval as before, and only issue `eth_getLogs` when the head actually advances. All `listen*` methods accept an options override. - Tests cover baseline semantics (only events mined after subscription), chunk boundaries, failed-chunk retry from the same cursor, callback isolation, stop-on-unwatch, and an anvil integration test behind a request-recording proxy asserting events are delivered with zero filter RPCs on the wire. Known trade-offs, documented in the helper's jsdoc: delivery is best-effort with possible duplicates on reorgs (no `removed` notifications; production consumers treat events as triggers and re-read chain state), and events mined within ~1 polling interval of subscribing may be missed (filter mode had nearly the same window). Fixes A-1695
See [merge-train-readme.md](https://github.com/AztecProtocol/aztec-packages/blob/next/.github/workflows/merge-train-readme.md). This is a merge-train.
Merged
spalladino
approved these changes
Aug 17, 2026
alexghr
approved these changes
Aug 17, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Promotes
v5-nextontov5for the v5.2.0 release.Frozen at
ee5d2d367e— thev5-nexttip at cut time. Bothmerge-train/spartan-v5andmerge-train/fairies-v5are drained to that same commit, so nothing staged is left behind.Testnet validation
This branch was cut at
a4db216abf, which is byte-identical to thev5.2.0-nightly.20260815tag — same commit, same tree (0b22572eace2419ede6bbab173514d6965d4e0d0). That nightly's CI3 run is green, its artifacts are published to npm and Docker Hub, and it has been running healthily on testnet since 2026-08-15:testnetns)aztecprotocol/aztec:5.2.0-nightly.20260815v5.testnet.rpc.aztec-labs.comnodeVersion=5.2.0-nightly.20260815canonical.testnet.rpc.aztec-labs.comnodeVersion=5.2.0-nightly.20260815Chain advancing normally, no prunes or reorgs, no WARN/ERROR across node pods since rollout.
The branch has since been fast-forwarded to
ee5d2d367eto pick up #25242 (configurable RPC server HTTP timeouts and CORS allowed-headers). That is the only delta from the soaked tree — 8 files, +131/−10, no nightly covers it yet. Its config defaults were checked against Node's built-ins (keepAliveTimeout5000 ms,headersTimeout60000 ms) and match exactly, and the CORS default path resolves to the samecors()call as before, so a node that sets none of the new env vars behaves identically. It touches no circuits, protocol contracts, or generated constants.Manifest
.release-please-manifest.jsonreads5.2.0on this branch, which is the released version — correct as-is, no change needed in this PR.v5-nextmoves to5.3.0separately in #25240; this branch is frozen and cannot pick that up, so the two can merge in either order.v5is a strict ancestor ofv5-nextthis cycle, so there was no manifest conflict to pre-resolve.Scope
139 commits (86 non-merge, 23 PR-level) spanning 2026-07-14 to 2026-08-17.
prover-node/prover-clientpxep2p/validator-clientsendTxgated on p2p connectivity (#25185); duplicate time-sensitive proposal validation removed (#25207);ValidatedProposalbranding (#25222)ethereum/aztec-nodegetLogsinstead ofeth_newFilter(#25176); block stream sync no longer resolves against an earlier pass (#25206)stdlib/foundationarchiveraztec@aztec/aztec/deploy(#24685)slasherv1.0.0-beta.25(#24907)One breaking change, inherited from the Noir bump: note types declared directly inside a
contractmodule must now bepub. Everything else is additive or internal.Protocol constants
Built from source and compared against
v5— a cache-free rebuild of every protocol circuit with each ref's own nargo and bb, then regeneration ofvk_tree.tsandprotocol_contract_data.ts:vkTreeRoot=0x2b3b6ea4412b9c8f6457a37f91a2870306f8641e07e16a49b68bda6f8bc02892— unchanged from v5.1.0protocolContractsHash=0x2c075866eafc88a1f6f9addc7e337c6e64e45d1cb7fd7c0d612ebcec72aab2ca— unchanged from v5.1.0The Noir beta.24 → beta.25 bump does not reach the circuits: the release build consumes the committed
pinned-build.tar.gz, which is bit-identical between v5.1.0 and this commit (blob3bedcb1fd1…), so the protocol-circuit bytecode is frozen rather than recompiled. The 47 verification keys were recomputed locally from that pinned bytecode with the cache disabled, and all 47check_pinned_vkchecks passed.protocolContractsHashis likewise backed by the newpinned-protocol-contracts.tar.gz, whose three artifacts were byte-compared against the build. Both values were also confirmed inside the published@aztec/protocol-contractsand@aztec/noir-protocol-circuits-typespackages for5.2.0-nightly.20260815.That makes v5.2.0 a drop-in upgrade against the current rollup rather than a coordinated one.
✅ Gate re-run against
ee5d2d367e(the current head) and passed — both values reproduced exactly from a build at this commit, withvkTreeRootnumerically evaluated rather than inferred.29556326ce..ee5d2d367etouches nonoir-projects/**,l1-contracts/src/**,ConstantsGen.sol,constants.gen.tsorconstants.nr. Scope of the check: it verifies that the pinned circuit bytecode plus locally recomputed VKs agree with the pin — not that a from-source recompile reproduces v5.1.0's bytecode.Commits added after the original constants check at 2955632
#25207#25222#25206#25185#25176#25163#25229#25159#25162#25231#25224#25228#25230#25242plus their merge commits.Known gap
Migration notes carry entries under
## TBDthat arrived with the docs baseline backport (#25017) and describenext-line changes not present on the v5 line — protocol contracts removed from@aztec/noir-contracts.js, and theat(wallet)→withWallet(wallet)deprecation. Neither exists on this branch. There is also no## 5.1.0heading, and the entries that ship in v5.2.0 sit under## TBDrather than a version heading.This ships in the release docs, so it is worth correcting on
v5-nextand backporting tov5before the tag, rather than after.