Skip to content

refactor(sequencer): reuse the resolved range when a block ends on the endpoint - #181

Draft
spalladino wants to merge 1 commit into
spl/fi-s09-drop-bucket-hintfrom
spl/fi-s11-reuse-exact-range
Draft

spalladino wants to merge 1 commit into
spl/fi-s09-drop-bucket-hintfrom
spl/fi-s11-reuse-exact-range

Conversation

@spalladino

@spalladino spalladino commented Sep 12, 2026

Copy link
Copy Markdown
Collaborator

When a streaming block has to consult L1, selectStreamingBundle resolves a live Inbox
bucket end and then decides where the block actually stops: the final block lands on the
resolved endpoint, any other block takes the further of that endpoint and the safe local
step. Whatever it picks, the block then re-read the prefix from the local log, even when the
end it picked was the endpoint the resolver had just read.

The resolver already reads [cursor, endpointTotal) from one snapshot and checks both ends
of it — the start against the cursor's rolling hash, the end against the live bucket's hash.
When selection stops exactly at endpointTotal, that range is by construction the range the
re-read would return, so the block now takes the resolver's snapshot as its bundle. A block
whose end falls short of or past the endpoint still reads its own range.

Why the equality guard is the safety property, not an optimization

The bundle carries both the messages and the position the cursor advances to, and that
position's rolling hash is what the block signs. Handing back the endpoint's range for a
block that ends somewhere else would consume the wrong messages and sign the hash at the
endpoint instead of the hash at end. So the reuse is only sound because end === endpointTotal proves the two ranges have identical bounds:

  • resolveEndpoint calls getL1ToL2MessageRange(cursor.totalMessageCount, endpointTotal)
    and returns that result as range, with endpoint set to range.end.
  • The archiver's contract for that method fixes range.end.totalMessageCount at the
    requested upper bound (an empty range returns start, whose count equals it), so
    endpointTotal in the job is exactly the bound the range was read at.
  • The resolver has already rejected a range whose start.rollingHash disagrees with the
    cursor, which is the one check readStreamingRange adds on top of the read.

The only difference that remains is the moment of the read, and the reuse is the stricter
option there: one snapshot is used for both the endpoint authentication and the bundle,
rather than two reads that could straddle a local log change. The forced tail-block path in
the same file already consumed resolved.range.messages directly, so this makes the ordinary
selection path consistent with it.

JSDoc correction

The method's doc said the block "then ends at the further of what the lookup allows and the
safe local step… a block that consulted L1 may legitimately end inside a bucket". That is
only true of a non-final block. The final block ends exactly on the resolved boundary, so
when the last live boundary within reach sits behind the safe local step it consumes fewer
messages than the local log alone would allow — the opposite of "never consumes less". The
paragraph now states both cases; "Nothing is retained: the next attempt decides again" is
unchanged.

Stack position

This is rung 18 of 25 in the Fast Inbox node stack, stacked on spl/fi-s09-drop-bucket-hint.

Source

Reconstructs the final range-read and comment hunks of 5397c7d61f ("refactor: drop unused
streaming Inbox plumbing") from old PR #25440. The destination carried the source's exact
pre-image for both hunks, so the upstream wording and condition port over unchanged.

This completes the four-way split of 5397c7d61f in this stack: its archiver hunks landed
as FI-S10, its pre-gossip bucket-hint hunks as FI-S09, and its range-reuse hunks here. The
fourth part — the ethereum/src/contracts/chain_state_override.ts override-type derivation
and the rollup_fixture_builder.nr fixed-message-subtree fixture deletion — belongs to
the deferred override-type and Noir-fixture cleanup and stays out of this stack entirely.

Validation

This changes which range a block returns, so red/green applies. Three tests were added to
checkpoint_proposal_job.test.ts, one per case, asserting the bounds of every local
message-range read alongside the bundle sizes and signed prefixes:

  • ends exactly on the resolved endpoint — the final block lands on 300 and [256, 300) is
    read once. On the unchanged code it was read twice, so this case is the red one: it fails
    before the change and passes after.
  • ends short of the resolved endpoint — block 4 resolves 1024, takes a full block to 956,
    and reads [700, 1024) then [700, 956), signing the hash at 956.
  • ends past the resolved endpoint — the last live end within reach is 750 while the safe
    local step reaches 768, so the block reads [700, 750) then [700, 768) and signs 768.

The last two pass on the unchanged code by design: they are the controls that catch the
mutation that matters. Making the reuse unconditional — returning resolved.range with no
equality guard, which is the change that would sign a wrong hash — turns both of them red
along with three pre-existing selection tests (five failures in total): the short block
consumes 324 messages instead of 256 + 68, and the past-the-endpoint block consumes 50
instead of 68 and signs the boundary at 750 rather than 768. The mutation was reverted.

yarn workspace @aztec-labs/sequencer-client test (full package): 12 suites, 302 passed, 1
skipped at the base; 12 suites, 305 passed, 1 skipped after — the three added tests and
nothing else. yarn build, yarn format and yarn lint are clean from yarn-project.

No e2e suites were run; they are left to CI.

…e endpoint

A block that ends exactly on the endpoint the resolver read takes that
snapshot as its bundle instead of reading the same prefix again. The
resolver reads `[cursor, endpointTotal)` and checks it against the cursor's
rolling hash, so at that end it is the range `readStreamingRange` would
produce. An end short of or past the endpoint still gets its own read, so
no endpoint hash is ever carried into a different prefix.

Also corrects the selection JSDoc: ending at the further of the lookup and
the safe local step describes a non-final block; the final block lands on
the resolved boundary and may consume fewer messages than the local log
alone would allow.
@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
@spalladino
spalladino removed this pull request from stack #188 September 15, 2026 22:42
@spalladino
spalladino added this pull request to stack #217 September 15, 2026 22:43
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