Skip to content

chore(release): 5.2.0 - #25225

Merged
aminsammara merged 139 commits into
v5from
release-v5.2.0-rc.0
Aug 17, 2026
Merged

chore(release): 5.2.0#25225
aminsammara merged 139 commits into
v5from
release-v5.2.0-rc.0

Conversation

@aminsammara

@aminsammara aminsammara commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Promotes v5-next onto v5 for the v5.2.0 release.

Frozen at ee5d2d367e — the v5-next tip at cut time. Both merge-train/spartan-v5 and merge-train/fairies-v5 are drained to that same commit, so nothing staged is left behind.

Testnet validation

This branch was cut at a4db216abf, which is byte-identical to the v5.2.0-nightly.20260815 tag — 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:

Component Image / reported version
validators, prover node, prover broker, prover agents (testnet ns) aztecprotocol/aztec:5.2.0-nightly.20260815
v5.testnet.rpc.aztec-labs.com nodeVersion=5.2.0-nightly.20260815
canonical.testnet.rpc.aztec-labs.com nodeVersion=5.2.0-nightly.20260815

Chain advancing normally, no prunes or reorgs, no WARN/ERROR across node pods since rollout.

The branch has since been fast-forwarded to ee5d2d367e to 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 (keepAliveTimeout 5000 ms, headersTimeout 60000 ms) and match exactly, and the CORS default path resolves to the same cors() 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.json reads 5.2.0 on this branch, which is the released version — correct as-is, no change needed in this PR. v5-next moves to 5.3.0 separately in #25240; this branch is frozen and cannot pick that up, so the two can merge in either order.

v5 is a strict ancestor of v5-next this 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.

Area Theme
prover-node / prover-client Epoch-proving robustness: retry-to-converge with failure declared only at submission-window expiry, ticker-driven expiry, per-checkpoint post-mortem upload, checkpoint-only re-proving, prune-induced fault handling (#24678, #24982, #24983, #24990, #25027)
pxe Sync performance: hash-pinned node read cache (#24969), anchor-bounded tag log caching (#25074), note/event validation from cached tx data (#25076), constrained tag sync (#24275), sender tagging finalization from log blocks (#25045)
p2p / validator-client Gossip tx validation no longer stalls behind tx-pool finalization (#25148); startup fails when p2p fails to start (#25177); slashing/proposing/health/sendTx gated on p2p connectivity (#25185); duplicate time-sensitive proposal validation removed (#25207); ValidatedProposal branding (#25222)
ethereum / aztec-node L1 watchers poll getLogs instead of eth_newFilter (#25176); block stream sync no longer resolves against an earlier pass (#25206)
stdlib / foundation Deserialization bounds hardening (#25026, #25028, #25029, #25109); checkpoint block-shape and block-count validation (#25229); JSON-RPC cookies (#25231)
archiver Removed-block cleanup and ownership-checked tx-effect deletes (#24765); L2→L1 witness from a single store snapshot (#24754)
aztec Declarative deployment framework at @aztec/aztec/deploy (#24685)
slasher Own-validator slash-target warnings and metrics (#25058)
telemetry JSON-RPC metrics (#25159)
JSON-RPC server Configurable HTTP keep-alive / headers timeouts and CORS allowed-headers, defaults preserving current behaviour (#25242)
toolchain Noir bumped to v1.0.0-beta.25 (#24907)

One breaking change, inherited from the Noir bump: note types declared directly inside a contract module must now be pub. 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 of vk_tree.ts and protocol_contract_data.ts:

  • vkTreeRoot = 0x2b3b6ea4412b9c8f6457a37f91a2870306f8641e07e16a49b68bda6f8bc02892 — unchanged from v5.1.0
  • protocolContractsHash = 0x2c075866eafc88a1f6f9addc7e337c6e64e45d1cb7fd7c0d612ebcec72aab2ca — unchanged from v5.1.0

The 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 (blob 3bedcb1fd1…), 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 47 check_pinned_vk checks passed. protocolContractsHash is likewise backed by the new pinned-protocol-contracts.tar.gz, whose three artifacts were byte-compared against the build. Both values were also confirmed inside the published @aztec/protocol-contracts and @aztec/noir-protocol-circuits-types packages for 5.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, with vkTreeRoot numerically evaluated rather than inferred. 29556326ce..ee5d2d367e touches no noir-projects/**, l1-contracts/src/**, ConstantsGen.sol, constants.gen.ts or constants.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 #25242 plus their merge commits.

Known gap

Migration notes carry entries under ## TBD that arrived with the docs baseline backport (#25017) and describe next-line changes not present on the v5 line — protocol contracts removed from @aztec/noir-contracts.js, and the at(wallet)withWallet(wallet) deprecation. Neither exists on this branch. There is also no ## 5.1.0 heading, and the entries that ship in v5.2.0 sit under ## TBD rather than a version heading.

This ships in the release docs, so it is worth correcting on v5-next and backporting to v5 before the tag, rather than after.

PhilWindle and others added 30 commits July 14, 2026 10:30
…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.
@socket-security

socket-security Bot commented Aug 14, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addednpm/​@​signalwire/​docusaurus-plugin-llms-txt@​1.2.28710010087100
Updatednpm/​@​aztec/​noir-noir_js@​1.0.0-beta.22 ⏵ 1.0.0-beta.25N/AN/AN/AN/AN/A

View full report

alexghr and others added 16 commits August 14, 2026 12:45
…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.
…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
@aminsammara aminsammara changed the title chore(release): 5.2.0-rc.0 chore(release): 5.2.0 Aug 16, 2026
@aminsammara
aminsammara merged commit 49a5921 into v5 Aug 17, 2026
33 of 34 checks passed
@aminsammara
aminsammara deleted the release-v5.2.0-rc.0 branch August 17, 2026 08:18
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.