Skip to content

feat(archiver): event-trigger L2BlockStream sync from archiver updates - #24317

Merged
PhilWindle merged 6 commits into
merge-train/spartan-v5from
spl/a-1298-event-trigger-l2blockstream-sync-from-archiver-updates
Jun 30, 2026
Merged

feat(archiver): event-trigger L2BlockStream sync from archiver updates#24317
PhilWindle merged 6 commits into
merge-train/spartan-v5from
spl/a-1298-event-trigger-l2blockstream-sync-from-archiver-updates

Conversation

@spalladino

@spalladino spalladino commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Context

Archiver-driven block sync is poll-based: every subsystem block stream waits for its next poll interval to notice that the archiver advanced. This adds avoidable latency to syncing world-state, p2p, and the prover-node, especially in fast e2e runs.

Approach

Make archiver-driven block sync event-triggered without changing the correctness model. Polling stays enabled, so a missed event only affects latency, never correctness.

  • The archiver emits one aggregate l2BlockSourceUpdated event per committed sync pass, carrying fromTips, toTips, and the hydrated blocksAdded already in hand from the pass (inbound queue + L1 checkpoint payloads — no extra storage reads). It is emitted only after store transactions commit and L2TipsCache.refresh() completes, and is suppressed on a no-op pass (tips unchanged and no blocks added); prunes and tier-only movements still emit because they move the tips.
  • EventDrivenL2BlockStream wraps L2BlockStream: it subscribes to the aggregate event and threads it through RunningPromise.trigger(arg) to drive an immediate reconciliation, coalescing bursts onto a single pass while the periodic poll remains the fallback.
  • The event's hydrated blocks back a single-pass hot-block fast path: when the stream is caught up to the event's fromTips (a full local-vs-event tip comparison), the pass serves blocks from the event and reports toTips directly, so it re-reads neither block bodies nor tips from the archiver. When the stream is not caught up, every read delegates to the source, so a stale or partial event can never change the sync outcome.
  • The lenient tip comparison the stream already used internally is extracted into shared localBlockIdDiffers / localTipsMatch helpers, reused by both the stream and the wrapper.
  • RunningPromise<T> gains an optional type parameter (defaulting to void) so trigger(arg?: T) can forward per-trigger data to the polled function; existing call sites are unaffected.

Subsystems keep consuming the existing L2BlockStreamEvents; the aggregate event is handled entirely inside the wrapper. World-state, p2p, and the prover-node are switched to the wrapper; RPC/node-backed consumers (pxe) stay on the plain L2BlockStream.

Fixes A-1298

@spalladino spalladino added S-do-not-merge Status: Do not merge this PR ci-no-fail-fast Sets NO_FAIL_FAST in the CI so the run is not aborted on the first failure and removed S-do-not-merge Status: Do not merge this PR labels Jun 26, 2026
@spalladino
spalladino force-pushed the spl/a-1298-event-trigger-l2blockstream-sync-from-archiver-updates branch 2 times, most recently from 4c489e8 to 7b3f4b7 Compare June 29, 2026 15:21
Add an aggregate `l2BlockSourceUpdated` event emitted once per committed
archiver sync pass, carrying the chain tips before/after the pass and the
hydrated blocks added (and, best-effort, pruned). A new
`EventDrivenL2BlockStream` wrapper subscribes to this event to reconcile
immediately on an archiver update, while keeping periodic polling as the
correctness fallback so a missed event only affects latency.

The wrapper serves hydrated blocks from a hot-block cache so a triggered
sync does not re-read archiver block bodies, and the no-op polling
invariant is preserved: a fully-synced pass only calls `getL2Tips()`,
backed by `L2TipsCache` once warm.

World-state, p2p, and prover-node are switched to the wrapper; RPC-backed
consumers (pxe) are left on the plain `L2BlockStream`.
Drop the unused blocksPruned from the aggregate update delta/event, pass the
event into the triggered sync pass via RunningPromise.trigger's new arg, and arm
the hot-block fast path only when local tips match the event's fromTips. Extract
the block stream's lenient tip comparison into shared localBlockIdDiffers /
localTipsMatch helpers and reuse them in both the stream and the wrapper.
…ck array

Drop the L2BlockSourceUpdate type and its empty/merge/has helpers; the pass-level
accumulator is now a plain L2Block[] of added blocks threaded through the archiver
and L1 synchronizer. handleCheckpoints carries it as RollupStatus.blocksAdded.
@spalladino
spalladino force-pushed the spl/a-1298-event-trigger-l2blockstream-sync-from-archiver-updates branch from 7b3f4b7 to 0dd9e45 Compare June 29, 2026 18:42
}

/** Fast-path context for a single sync pass: blocks to serve by number, plus the tips to report as the source's. */
type ActiveUpdate = { byNumber: Map<number, L2Block>; toTips: L2Tips };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can be patched on a follow-up but presumably these 'byNumber' Maps can be keyed on branded types.

@PhilWindle
PhilWindle merged commit 4d206b0 into merge-train/spartan-v5 Jun 30, 2026
12 checks passed
@PhilWindle
PhilWindle deleted the spl/a-1298-event-trigger-l2blockstream-sync-from-archiver-updates branch June 30, 2026 09:03
rangozd pushed a commit to rangozd/aztec-packages that referenced this pull request Aug 5, 2026
BEGIN_COMMIT_OVERRIDE
refactor(e2e): relocate no-node straggler tests (AztecProtocol#24344)
test(e2e): pin gas_estimation public-payment txs to one block to deflake
fee comparison (AztecProtocol#24382)
feat(archiver): event-trigger L2BlockStream sync from archiver updates
(AztecProtocol#24317)
test(e2e): speed up individual e2e tests (AztecProtocol#24345)
chore(e2e): warm blob KZGs in parallel during setup (AztecProtocol#24383)
chore: add perf as a valid PR title prefix (AztecProtocol#24412)
test(e2e): adopt shared wait helpers (AztecProtocol#24404)
test(e2e): run prover client.test.ts in CI (AztecProtocol#24399)
fix(ethereum): mine empty L1 blocks without touching the mempool
(AztecProtocol#24414)
test(e2e): deflake empty block proving test (AztecProtocol#24411)
test(e2e): allocate HA node p2p ports above the ephemeral range to
deflake e2e_ha_full (AztecProtocol#24418)
fix(sequencer): use evmMine in automine auto-settle to avoid dropping
test L1 txs (AztecProtocol#24421)
test(e2e): instrument common spans for wall-clock tracking (AztecProtocol#24407)
test(e2e): remove redundant reqresp_no_handshake e2e test (AztecProtocol#24424)
END_COMMIT_OVERRIDE
spalladino added a commit that referenced this pull request Aug 14, 2026
…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci-no-fail-fast Sets NO_FAIL_FAST in the CI so the run is not aborted on the first failure

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants