fix(archiver): search a hundred L1 blocks for a recovery anchor - #173
Draft
spalladino wants to merge 1 commit into
Draft
spalladino wants to merge 1 commit into
spalladino wants to merge 1 commit into
Conversation
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
added this pull request to stack #188
September 12, 2026 04:52
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.
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 itas "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 heighth— exactly 100L1 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
messageSentSearchWindowinethereum/src/contracts/inbox.ts, because the archiver'sFakeL1Statehad its own copy of the oldformula. 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_getLogsoutright. The plan's instruction is to usedeployment 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 toarchiver/src/l1/data_retrieval.ts) is hoisted toethereum/src/contracts/log.tsasfetchLogsBisectingRangeand reused for the by-hash query. Both callersnow 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.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.tson its own, 10 consecutive runs, 106/106 every time. The flakethat 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.tson anvil (ANVIL_PORT=8745) — 8/8, including the four newmessageSentSearchWindowcases.Red/green, each proven by reintroducing the specific defect and rerunning:
places a message re-mined forty blocks above its recorded height. With the old ±5 window the lookup misses, the anchor falls back to theprevious 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.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 belowblock 1, the way a real endpoint does. Dropping the clamp makes the search ask for
fromBlock -45andthe fixture fails on that rejection.
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 911instead of951andthe case fails, as does
leaves nothing to search when the upper bound is below the window.reports a single block the provider cannot serve as a failure rather than an absence of logs; making the bisection return[]at a single blockinstead of rethrowing reddens it. At the archiver level the pre-existing
commits nothing when a per-message lookup fails before an anchor is chosenstill holds: the pass throws, recovery stays inprogress, and nothing is deleted or pruned.
resumes a bounded anchor search across iterations while the head advancesstill 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 anchorandkeeps the shortened log and the delivered prune when the refetch after a rollback failsnow place their re-mined messages 59 blocks away, one past the topof the window.
keeps the prefix through a message found inside the lookup windowmoves its message toexactly +50, the top edge, so the two fixtures together pin the boundary. Their intent is unchanged.
Deliberately not here
widens the window and nothing else.
DutyBudget(the deferred whole-duty timeout work), nounverifiable/checkpoint-unverifiableorsentinel encoding 9 (the deferred observer and inactivity-accounting work), no
R − Pgossip 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).
aztec-packagesand is not touchedhere.