Skip to content

fix(archiver): search a hundred L1 blocks for a recovery anchor - #173

Draft
spalladino wants to merge 1 commit into
spl/fi-n7-authenticate-anchorsfrom
spl/fi-s12-100-block-window
Draft

spalladino wants to merge 1 commit into
spl/fi-n7-authenticate-anchorsfrom
spl/fi-s12-100-block-window

Conversation

@spalladino

@spalladino spalladino commented Sep 12, 2026

Copy link
Copy Markdown
Collaborator

Recovery now searches up to 100 inclusive L1 heights for the event that authenticates a stored message anchor.
This is a new change, not a port. There is no source commit SHA behind it. It is designed on top of
#25438 (ported in the rung directly below, spl/fi-n7-authenticate-anchors), and the master plan lists it
as "new change after #25438" in the FI-S01..FI-S12 table that replaces the mixed PR #25440.

Why

A stored Inbox message carries the L1 height it was first observed at. #25438 removed the finalized-height
shortcut that used to accept that height as authentication, so every recovery anchor is now a message an
event lookup positively found at the same index and the same rolling hash. What the recorded height still
is, and all it ever was, is a hint about where to look.

The lookup window around that hint was eleven L1 blocks wide. That is narrower than an ordinary L1 reorg
moves an unchanged message. A message re-mined even twenty or thirty blocks away missed every time, so the
search walked backwards past prefixes L1 still holds, the log rolled back further than it had to, and the
proposed blocks that consumed those messages were pruned even though the messages came straight back on the
refetch. The cost is liveness, not safety, but it is paid on exactly the reorgs recovery exists for.

What changes

The lookup now covers the inclusive range [h - 49, h + 50] around the recorded height h — exactly 100
L1 heights before clipping — clipped at block 1 at the bottom and at the recovery's captured head at the
top. Clipping shortens the window; it never slides it to preserve its width. A window near genesis is
simply shorter, and so is one near the head, which is what keeps an anchor from ever landing at or past the
head and leaving the rewound cursor unreachable.

Everything else about the search is untouched and deliberately so: the log query is still filtered on the
message hash by the indexed topic, a candidate found is still accepted only when its index and its
rolling hash match, the per-pass budget is still 32 candidate lookups, the pass structure is unchanged, and
the rollback is still the same single store transaction.

The window arithmetic moves into an exported messageSentSearchWindow in
ethereum/src/contracts/inbox.ts, because the archiver's FakeL1State had its own copy of the old
formula. The fake now calls the production function, so the window cannot drift away from the code under
test.

Provider limits

Some free and trial endpoints reject a 100-block eth_getLogs outright. The plan's instruction is to use
deployment credentials that support it, or the existing client chunking, without changing recovery
semantics — and above all that a provider error must leave recovery pending and must never be read as the
message being absent.

Rather than write a second splitter, the range bisection #25415 added for the forward scan
(getMessageSentEventsBisecting, private to archiver/src/l1/data_retrieval.ts) is hoisted to
ethereum/src/contracts/log.ts as fetchLogsBisectingRange and reused for the by-hash query. Both callers
now run the same code: a refused range is halved at complete-block boundaries and retried, and a single
block the provider cannot serve is rethrown. Nothing narrows the window to dodge a limit, and no failure
can turn into an empty result. The forward scan's behavior is unchanged; only the home of the helper moved.

Stack position

This is rung 10 of 25 in the Fast Inbox node stack, stacked on spl/fi-n7-authenticate-anchors.

Tests run

From yarn-project:

  • yarn build — green.
  • yarn format, yarn lint — clean.
  • Full archiver suite, JEST_MAX_WORKERS=1 yarn workspace @aztec-labs/archiver test — 23 suites,
    681 tests, exit 0 (679 at the base, plus the two new recovery fixtures).
  • archiver/src/archiver-sync.test.ts on its own, 10 consecutive runs, 106/106 every time. The flake
    that dogged the rungs below and was fixed in #25438 did not reappear.
  • ethereum/src/contracts/log.test.ts — 3/3 (new file).
  • ethereum/src/contracts/inbox.test.ts on anvil (ANVIL_PORT=8745) — 8/8, including the four new
    messageSentSearchWindow cases.

Red/green, each proven by reintroducing the specific defect and rerunning:

  • Re-mined ~40 blocks above the recorded height. New fixture places a message re-mined forty blocks above its recorded height. With the old ±5 window the lookup misses, the anchor falls back to the
    previous message and the second proposed block is pruned — the assertion fails with [1, 2] becoming
    [1]. With the window widened, both blocks survive and nothing is pruned.
  • Bottom clip at genesis. New fixture searches from the genesis block for a message recorded close to it, with messages at L1 blocks 3 and 4. The fake's by-hash query now rejects a range starting below
    block 1, the way a real endpoint does. Dropping the clamp makes the search ask for fromBlock -45 and
    the fixture fails on that rejection.
  • Top clip at the captured head. clips to the upper bound instead of sliding down to keep its width.
    Replacing the clip with a slide that preserves 100 heights returns fromBlock 911 instead of 951 and
    the case fails, as does leaves nothing to search when the upper bound is below the window.
  • Provider error inside the window is pending, not a miss. reports a single block the provider cannot serve as a failure rather than an absence of logs; making the bisection return [] at a single block
    instead of rethrowing reddens it. At the archiver level the pre-existing commits nothing when a per-message lookup fails before an anchor is chosen still holds: the pass throws, recovery stays in
    progress, and nothing is deleted or pruned.
  • 32-candidate budget. The pre-existing resumes a bounded anchor search across iterations while the head advances still pins 96 lookups across three passes and then 100, unchanged by the wider window.

Five existing fixtures were re-pinned because their L1 distances (20 to 30 blocks) now fall inside the
window — that they had to move is itself the evidence the window widened. re-mines the same messages beyond the lookup window, rolls back to the newest message still found on L1, rolls back to the deployment block ... when no lookup finds an anchor and keeps the shortened log and the delivered prune when the refetch after a rollback fails now place their re-mined messages 59 blocks away, one past the top
of the window. keeps the prefix through a message found inside the lookup window moves its message to
exactly +50, the top edge, so the two fixtures together pin the boundary. Their intent is unchanged.

Deliberately not here

  • No e2e suites were run; they are left to CI.
  • No change to the lookup budget, the pass structure, the rollback, or the finality marker. This rung
    widens the window and nothing else.
  • The deferred work stays out: no DutyBudget (the deferred whole-duty timeout work), no unverifiable/checkpoint-unverifiable or
    sentinel encoding 9 (the deferred observer and inactivity-accounting work), no R − P gossip cutoff (the deferred gossip-deadline work), no generic Rollup artifact library map
    (the deferred generic Rollup artifact-map work), no generic proposal-size/fee-modifier fix (the deferred generic proposal-size work), no generic override types or Noir-fixture
    cleanup (the deferred override-type and Noir-fixture cleanup), no proof-lifetime joins (the deferred proof-lifecycle work).
  • Developer and operator documentation for this behavior is owned by aztec-packages and is not touched
    here.

A stored message's L1 height records where it was first observed, not where
the canonical chain carries it after a reorg. The anchor search only looked
eleven L1 blocks around that height, so a message re-mined any further away
was never placed and the search walked back past prefixes L1 still holds,
discarding unchanged messages and pruning the proposed blocks that consumed
them.

The lookup now covers an inclusive hundred-block window around the recorded
height, clipped at block 1 and at the captured head. Clipping shortens the
window rather than sliding it, so a window near genesis or near the head never
reaches blocks outside the range it was asked for and an anchor can still
never sit at or past the head.

Some endpoints refuse a hundred-block eth_getLogs. The bisecting fetch the
forward scan already used is hoisted to the ethereum package and reused for
the by-hash query, so a refused range is halved and retried rather than
narrowed: a rejection still ends as an error, never as an empty result.
@spalladino
spalladino added this pull request to stack #188 September 12, 2026 04:52
@spalladino spalladino added the ci-draft Run CI on this draft PR label Sep 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci-draft Run CI on this draft PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant