Skip to content

fix(sequencer): reserve blob space for a transaction-less checkpoint tail block - #182

Draft
spalladino wants to merge 1 commit into
spl/fi-s11-reuse-exact-rangefrom
spl/fi-n5-rescue-tail-blobs
Draft

spalladino wants to merge 1 commit into
spl/fi-s11-reuse-exact-rangefrom
spl/fi-n5-rescue-tail-blobs

Conversation

@spalladino

@spalladino spalladino commented Sep 12, 2026

Copy link
Copy Markdown
Collaborator

When a proposer runs out of sub-slots with the consumption cursor sitting at a message prefix that is not a live
L1 Inbox bucket end, it appends one transaction-less block to reach one, so the checkpoint can be published at
all. That tail block still writes its own block-end fields, but checkpoint blob accounting only ever reserved the
current block's end fields. Ordinary packing could therefore fill the checkpoint right up to capacity and leave
the tail with nowhere to go.

Packing now holds back one block's worth of end fields — getNumBlockEndBlobFields(), which a real
transaction-less block serializes into exactly, at 7 fields and 224 bytes — for as long as the checkpoint can
still gain another block. The last block the checkpoint can hold releases the reservation, since nothing can
follow it, and the actual tail then consumes it by writing its own end fields. The checkpoint end marker is
already deducted once from the total capacity, so a tail appended to an existing checkpoint costs seven fields
rather than eight. The transaction allowance is floored at zero, so a full checkpoint reports no room instead of
a negative one.

This is local proposer packing policy only. A validator re-executing a peer's proposal reserves nothing, so
no otherwise-valid proposal becomes rejectable over this. And reserving blob space does not reserve build time or
guarantee the extra block can be built — it only guarantees that if the proposer decides to append a tail, the
blob room is there. Message, per-block, block-count, timing and total blob limits are all unchanged.

Stack position

This is rung 19 of 25 in the Fast Inbox node stack, stacked on spl/fi-s11-reuse-exact-range.

Provenance

Reconstructs old PR #25436, from source commits 07cae305a4ca007eb1d34115e87da0e54bf5f12a (the behavior) and
e546fe1c315a3ad7d5b176f31aa9f31063cebff5 (a follow-up that scopes remainingBlocks back to the proposer
branch). The second commit's final shape was implemented directly rather than porting the hoisted local and then
inlining it: the guard already requires isBuildingProposal, so the hoist and its : 1 fallback bought nothing,
and Math.max(1, n) > 1 and n > 1 agree, so packing is identical either way. The destination carried the
source's byte-exact pre-image for every hunk in both files, so nothing had to be adapted.

Only validator-client/src/checkpoint_builder.ts and its test are touched.

Tests

  • yarn workspace @aztec-labs/validator-client test (full package): base on this rung's parent measured at
    10 suites / 283 passed / 3 skipped; after the change, 10 suites / 291 passed / 3 skipped — +8, exactly the
    new cases.
  • yarn build, yarn format, yarn lint from yarn-project: all clean.
  • No e2e suites were run; this rung has no e2e surface and CI owns those.

Red/green, covering all five validations the plan requires plus the validator-mode carve-out:

  • Real serialization size. A real L2Block.random(..., { txsPerBlock: 0 }) is serialized and its
    toBlobFields().length measured, then asserted equal to the shared getNumBlockEndBlobFields() constant, to 7,
    and to 224 bytes at DA_BYTES_PER_FIELD. The figure is measured, not transcribed. Proven to catch drift:
    forcing the shared constant to 8 while the encoder still writes 7 reddens it (Expected: 8, Received: 7).
  • Exact fit. With the checkpoint one tail plus ten fields from capacity, the allowance is those ten fields,
    and a block that packs the full allowance leaves exactly a tail block's fields free. Red before the fix.
  • Insufficient capacity. With only a tail's room left the allowance is zero, and one field short of that it
    is still zero rather than −1. Red before the fix (7 and 6 respectively); dropping the Math.max(0, …) floor
    alone yields −1, which is the negative allowance the floor exists to prevent.
  • Final-block release. On the last block the checkpoint can hold, the allowance grows back by exactly a tail
    block's fields. Weakening the guard from > 1 to >= 1 reddens this and the companion case that pins the
    released delta at a tail block alone, not a second checkpoint end marker.
  • Headline case, end to end. Packing an ordinary block to its allowance and then appending a real
    transaction-less tail keeps the checkpoint within BLOBS_PER_CHECKPOINT * FIELDS_PER_BLOB. On the unchanged
    code this overflows by exactly seven fields (24583 against 24576) — the tail does not fit. The same test also
    pins that building the tail itself reports no room for txs rather than charging its overhead twice.
  • Validator mode. Re-executing a peer's proposal gets the full unreserved allowance. Mutating the guard so
    re-execution also reserves the tail reddens this test (and four pre-existing validator-mode budget tests),
    which is the evidence the carve-out bites: without it, a valid peer proposal would become rejectable.

Overall, 5 of the 8 new cases fail against the unchanged builder; the other 3 are deliberate controls that must
stay green (the serialization measurement, the final-block release and the validator-mode carve-out), and each
was separately shown to redden under a targeted mutation.

Deliberately left out

  • None of the seven deferred work families. In particular, the generic serialized proposal-size / fee-modifier 32-vs-8-byte fix is adjacent subject matter — blob and field sizing — and is not
    here: no proposal-size computation is touched. DA_BYTES_PER_FIELD appears only in a test assertion expressing
    7 fields as 224 bytes.
  • No DutyBudget, no unverifiable / checkpoint-unverifiable status or sentinel encoding 9, no R − P hard
    gossip send cutoff, no generic Rollup artifact library map, no generic override-type or Noir-fixture cleanup.
  • No L1/Solidity, Noir, barretenberg, spartan/ or docs changes.
  • No e2e coverage added or run; left to CI.

Open items

None. Nothing in this rung required a judgement call that was left unresolved.

…tail block

When a proposer runs out of sub-slots with the consumption cursor at a message prefix that is not a live L1
Inbox bucket end, it appends one transaction-less block to reach one so the checkpoint can be published at all.
That block still writes its own block-end fields, but checkpoint blob accounting only ever reserved the current
block's end fields, so ordinary packing could fill the checkpoint until the tail no longer fits.

Packing now holds back one block's worth of end fields — `getNumBlockEndBlobFields()`, measured at 7 fields and
224 bytes for a real transaction-less block — while the checkpoint can still gain another block, releases it on
the last block the checkpoint can hold, and lets the actual tail consume it by writing its own end fields. The
checkpoint end marker is still deducted once from the total, so a tail appended to an existing checkpoint costs
seven fields rather than eight. The transaction allowance is floored at zero so a full checkpoint reports no
room instead of a negative one.

This is local proposer packing policy: re-executing a peer's proposal reserves nothing, so no otherwise valid
proposal becomes rejectable. Reserving blob space does not reserve build time or guarantee the extra block can
be built. Message, per-block, block-count, timing and total blob limits are unchanged.
@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