feat(node): serve fee RPCs from a background L1 fee snapshot - #24970
Draft
spalladino wants to merge 7 commits into
Draft
feat(node): serve fee RPCs from a background L1 fee snapshot#24970spalladino wants to merge 7 commits into
spalladino wants to merge 7 commits into
Conversation
Serve getCurrentMinFees/getPredictedMinFees (and the p2p mempool fee policy) from an in-memory FeeSnapshotService refreshed per L1 block, so warm calls issue zero L1 requests. - ethereum: L1SyncSnapshot type; RollupContract getManaMinFeeAt options object, blockNumber option on getTips, non-memoized pinned governance reads, and a Multicall3-batched pinned fee reader (readFeeInputs) with a parallel-call fallback. - archiver: atomic getL1SyncSnapshot() published with the existing L1 identity fields. - sequencer-client: FeeSnapshotService with provisional window, two pinned multicall waves, wave-2 tips consistency check, exact-coverage top-up, complete precomputed per-candidate quotes, keyed single-flight with a first-snapshot promise, three-bound staleness policy, and slot-enumerating drift-window selection with symmetric miss handling and an RPC-side archiver identity check. FeePredictor math is extracted to fee_prediction.ts; the legacy computation is refactored into an explicit-input test-only equivalence oracle; the legacy per-request eth_blockNumber caches are removed. - aztec-node: wire the service in the factory (started resources) and stop it from AztecNodeService before the archiver.
Add a small HTTP JSON-RPC reverse proxy fixture (configurable delay, per-method counters, async-disposable teardown) plus a focused unit test. Extend node_rpc_perf to run the node's L1 client through the proxy: warm getPredictedMinFees x20 sequential and x20 concurrent under 100ms injected latency, gated on zero read-triggered refreshes (fee-path L1 request counter) and warm p95 within tolerance of a same-run local RPC baseline; the boundary refresh cost is reported non-gating.
…ency stance Describe how the fee RPCs are served from the background snapshot: the two anchor rules (current vs prediction floors), the drift window with symmetric coverage misses, the three staleness bounds, and the honest consistency stance including the impossible-mixed-state and weaker parallel-call-fallback residuals.
Routing the node's L1 client through a front latency proxy started before setup conflicts with the shared e2e setup's ownership of the Anvil clock and mining, which stalled block production and failed the whole suite. Keep setup owning Anvil and gate the zero-fee-path-L1 property directly on the FeeSnapshotService readTriggeredRefreshes counter (authoritative regardless of L1 latency), bounding warm fee-path latency against a same-run local-RPC baseline. The latency proxy fixture and its unit test are retained for standalone use.
The exact-coverage top-up only fetched the drift-window endpoint slots, while the read path enumerates every slot between them. With a capped provisional window, intermediate wanted slots could stay unmaterialized and reads would coverage-miss into an error no refresh repairs. Enumerate the same range in both places.
…s ordering, and backoff - publish() no longer gates on block number: L1 identity is hash-authoritative and the height can move backwards (reorg or lagging fallback backend); the guard discarded every rebuild after a rollback and wedged reads. Serial refreshes plus the pre-publish identity re-check make it unnecessary. - The read path checks archiver identity and coverage before the staleness bounds, and repairs a stale computation age with one refresh instead of failing closed; head-age and future-head still fail immediately since the archiver has nothing newer. - Read-triggered refreshes respect the failure backoff so RPC traffic cannot hammer a failing L1, and exceeding the refresh restart bound now counts as a failure and engages backoff. - Snapshots publish their actual materialized slot bounds rather than the provisional window, so a capped window plus top-up no longer makes the poll loop refresh continuously. - Fail-closed reads (computation-stale, head-stale, future-head, coverage timeout) now have counters.
spalladino
marked this pull request as draft
July 24, 2026 18:53
…ment Extend the fee equivalence and invariant suites to the parts of plan v4's matrix that were missing: - fee_snapshot_equivalence: add cases with a written pending checkpoint beyond the proven tip (pending != proven), exercising both effective-parent branches — canPrune=false selects the pending checkpoint, canPrune=true selects the proven checkpoint — each verified via canPruneAtTime and getEffectivePendingCheckpoint before comparing snapshot vs legacy on identical (blockNumber, now). - fee_snapshot_equivalence: replace the single oracle-update case with an explicit rotation placed below, inside, and above the prediction window; the placement is confirmed via getL1FeesAt on the window slots before each comparison. - rollup_fee_reads: add a checkpoint-slot invariant case with a pending checkpoint written at a non-zero slot, asserting pendingCheckpointSlot <= pinnedSlot across advances. State is created with anvil setStorageAt against the Rollup tips + temp-checkpoint-log slots and restored to genesis after each case so it does not leak into other tests.
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.
Makes warm
getPredictedMinFees/getCurrentMinFeescalls zero-L1-request in-memory reads, so quote latency no longer scales with L1 RPC latency.Context
Every
getPredictedMinFeescall paid two uncachedeth_blockNumberround trips, and each new L1 block added up to ~8 sequentialeth_callwaves on the request path. With 100-150 ms of L1 RPC latency the quote RPC took ~800 ms — right when a wallet is preparing a transaction. Prediction fee configuration (manaTarget, proving cost) was also memoized for process lifetime, so a running node never saw governance updates to it.Approach
FeeSnapshotService(owned by the node factory, shared by the fee RPCs and the p2p mempool fee policy) refreshes once per L1 block, pinned to the archiver's atomically published L1 identity (newArchiver.getL1SyncSnapshot()), and publishes an immutable snapshot of complete precomputed quotes (current fee + all threeManaUsageEstimateprediction arrays) per candidate slot. Warm reads are pure memory lookups.eth_calls — tips + governance values + per-slotgetManaMinFeeAt/canPruneAtTime/getL1FeesAt, then both checkpoint fee headers plus agetTipsre-read that discards the refresh on cross-wave mismatch — followed by an exact-coverage top-up and an archiver identity re-check before the atomic publish. Falls back to parallel pinned individual calls where Multicall3 is not deployed.getManaMinFeeAtstays canonical for the current fee; predictions reuse the existing TS fee math. Coverage misses trigger a shared single-flight refresh and never substitute a different slot's answer.(blockNumber, now)test oracle; an Anvil equivalence suite compares snapshot vs legacy on identical inputs across state/clock cases, and locks the within-slot fee constancy and checkpoint-slot invariant the design relies on. The RPC bench gates warm calls on the service counter (zero read-triggered refreshes) and includes a JSON-RPC latency-proxy fixture.API changes
getCurrentMinFees/getPredictedMinFeescan now throw typedFeeSnapshot*Errors when the L1 view is stale or unavailable, instead of silently serving unboundedly stale data.RollupContract.getManaMinFeeAtthird parameter changed fromstateOverrideto an options object{ blockNumber?, stateOverride? };getTipsgains an optional{ blockNumber }.Fixes A-1416
Review fixes
A second-model review of the implementation surfaced three service-state defects, fixed in follow-up commits:
publish()gated on block number and would wedge after an L1 height rollback (reorg or lagging fallback backend); staleness bounds were checked before the archiver identity, failing reads that one refresh would repair; and read-triggered refreshes bypassed the failure backoff, turning RPC traffic into L1 load during outages. Snapshots also now publish their actual materialized slot bounds, and fail-closed reads have counters.Follow-ups (deliberately out of scope here)
AztecNodeConfig/env vars (defaults are currently derived from L1 constants and overridable only in code).FeeSnapshotStatscounters and structured log fields to OpenTelemetry meters.