fix: poll getLogs instead of eth_newFilter for L1 event watchers - #25176
Merged
spalladino merged 5 commits intoAug 14, 2026
Conversation
The five L1 event watchers used viem's watchContractEvent, which installs a server-side filter and polls eth_getFilterChanges. Server-side filters die in the field: load-balanced RPC URLs route polls to backends that never saw the filter, and providers report a purged filter with error codes viem does not treat as "filter gone" (reth -32602, Alchemy -32600), so watchers either churn recreating filters or hammer a dead filter id at the poll rate while silently missing events. Exclude the six filter RPC methods at the transport level so viem rejects them locally and every watcher falls back to its cursor-based eth_getLogs polling strategy, which needs no server state and rotates through the fallback transports normally. The watchers now also log polling failures (throttled to avoid one warning per interval during an outage) and isolate each per-log callback so a throwing handler no longer drops the rest of the batch.
The sequencer publisher passes an async callback to listenToSlasherChanged; its rejections bypassed the per-log try/catch and became unhandled. The shared handler now catches rejections from returned promises, and the listen helpers return the callback result so those promises reach it. Also rewords the watcher jsdoc: delivery is best-effort with possible duplicates, not at-least-once.
Rolls back the transport-level filter method exclusion: relying on viem's built-in getLogs fallback meant retrying filter creation on every tick and issuing unbounded getLogs ranges when catching up after an outage. Instead, watchContractEvent in the ethereum package polls eth_blockNumber (cached by viem per client) and fetches new logs with ranged eth_getLogs requests capped at a configurable max block range (default 100), chunking wider catch-ups and advancing the cursor per successful chunk. Typed on the event via viem's abi type helpers. SlasherUpdated polls every 60s since slasher rotations are rare governance operations; the remaining watchers poll at the client's polling interval as before.
spalladino
force-pushed
the
spl/a-1695-l1-watchers-getlogs
branch
from
August 12, 2026 18:01
a2e8b6c to
d915ad4
Compare
alexghr
reviewed
Aug 14, 2026
Comment on lines
+80
to
+81
| nextBlock = latestBlock + 1n; | ||
| return; |
Contributor
There was a problem hiding this comment.
Is it intended that this loop skips the current in progress block? Doesn't this mean we risk missing events in the first L1 block after the Aztec node boots up?
Contributor
Author
There was a problem hiding this comment.
I think it's not too bad, but fair, we can just remove that +1
Contributor
Author
There was a problem hiding this comment.
Removed the +1 in 35abe17 — the block that is latest on the first poll is now the starting cursor, so its events get fetched. Events from blocks before startup are still not reported.
The watcher baselined its cursor at latestBlock + 1, so any events mined in the L1 block that was latest when the node booted were never fetched.
spalladino
enabled auto-merge (squash)
August 14, 2026 19:49
alexghr
approved these changes
Aug 14, 2026
Merged
aminsammara
added a commit
that referenced
this pull request
Aug 17, 2026
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..ee5d2d3` 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. <details> <summary>Commits added after the original constants check at 2955632</summary> `#25207` `#25222` `#25206` `#25185` `#25176` `#25163` `#25229` `#25159` `#25162` `#25231` `#25224` `#25228` `#25230` `#25242` plus their merge commits. </details> ### 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.
spalladino
added a commit
that referenced
this pull request
Aug 18, 2026
Forward-ports the fixes merged into `merge-train/spartan-v5` over the last two weeks onto the `next` line. One commit per original PR, each carrying a `cherry picked from` trailer. ## Ported - #25177 — fix(p2p): fail node startup when p2p service fails to start - #25183 — feat(p2p): warn periodically while node has zero connected peers - #25202 — fix: spurious insufficient-funds rejection when simulating L1 calls - #25207 — fix(validator): remove duplicate time-sensitive validation of block proposals - #25222 — refactor(p2p): brand ingress-validated proposals with a ValidatedProposal type - #25229 — fix: checkpoint block-shape and block-count validation gaps - #25206 — fix: block stream sync can resolve against a pass that predates it - #25185 — feat(p2p): gate slashing, proposing, health, and sendTx on p2p connectivity - #25176 — fix: poll getLogs instead of eth_newFilter for L1 event watchers Commits are ordered as they merged into the v5 line, since several build on each other (#25177 → #25183 → #25185 and #25207 → #25222 → #25229). ## Not ported - #25148 (gossip tx validation stalls behind tx pool finalization) — already on this line via #25189, which covers the same ground. The cherry-pick came out empty. - #25155 (standard-contracts historical artifacts in release image) — the whitelist entry is already present in `release-image/Dockerfile.dockerignore`. ## Conflict resolutions worth a look Four commits conflicted; all others applied clean. - **`ethereum/src/l1_tx_utils/l1_tx_utils.ts`** (#25202) — the rename #25202 called out in its own description: `getGasPrice` is `getFeesPerGas` on this line. Took the fix's side (fee fields dropped from the simulated call), so the local `feesPerGas` read is gone along with them. - **`archiver/src/modules/data_store_updater.ts`** (#25229) — the v5 version of this method also passes `maxBlocksPerCheckpoint: MAX_CAPACITY_BLOCKS_PER_CHECKPOINT` on the ingest path, which is a separate v5-only change that has not been ported. Only #25229's actual change is taken here: the shared `validateOpts` object plus `allowEmptyNonFirstBlocks: true`. Ingest on this line therefore still validates against the attestable limit; raising it to the capacity limit is a separate port. - **`ethereum/src/contracts/slashing_proposer.ts`** (#25176) — `listenToVoteCast` carries a `voteIndex` argument on this line (from #25068), which the v5 version predates. Combined both: the new `watchContractEvent` polling mechanism with the `voteIndex` callback argument retained. - **Import blocks** (#25185) — `safe_json_rpc_server.test.ts` keeps `import http` as a value import, since #25243 added a runtime `http.createServer()` call to that file. `stdlib/src/interfaces/p2p.test.ts` takes only `P2PConnectivity`; the `TxHash` / `GetTxByHashOptions` imports on the v5 side belong to v5-only API methods and would be unused here. ## Verification `yarn build` produces a byte-identical error set to `origin/merge-train/spartan` built in the same working tree (46 errors, all from stale cross-line generated artifacts locally — Noir circuit artifacts, verification-key lengths, `withWallet` on contract types). No new compile errors from the port. Unit tests for every package with a hand-resolved conflict pass: `stdlib` (52), `ethereum` (61), `foundation` (47), `slasher` (140), `validator-client` (39). Two suites (`archiver/data_store_updater.test.ts`, `sequencer-client/checkpoint_proposal_job.test.ts`) fail to *load* in this working tree on stale cross-line artifacts; the archiver one was confirmed to fail identically with the base version of the file, so CI is the first real run for those two. Labeled `ci-no-squash` to preserve one commit per ported PR.
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.
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 pollseth_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,
watchContractEventin@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 unboundedeth_getLogswhen catching up after an outage, so we now own the loop:eth_blockNumber(viem caches it per client at the client's polling interval, so concurrent watchers share one request) and fetches new logs witheth_getLogsviaclient.getContractEvents, never touching filter RPCs. Requests never span more than a configurablemaxBlockRange(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.ContractEventName/GetContractEventsReturnTypehelpers (includingstrictdecoding), and is built on foundation'sRunningPromise, so ticks never overlap and unwatching interrupts a catch-up loop.SlasherUpdatedpolls every 60s since slasher rotations are rare governance operations; the other four watchers poll at the client's polling interval as before, and only issueeth_getLogswhen the head actually advances. Alllisten*methods accept an options override.Known trade-offs, documented in the helper's jsdoc: delivery is best-effort with possible duplicates on reorgs (no
removednotifications; 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