fix(sequencer): reserve blob space for a transaction-less checkpoint tail block - #182
Draft
spalladino wants to merge 1 commit into
Draft
spalladino wants to merge 1 commit into
spalladino wants to merge 1 commit into
Conversation
…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
added this pull request to stack #188
September 12, 2026 04:52
spalladino
removed this pull request from stack #188
September 15, 2026 22:42
spalladino
added this pull request to stack #217
September 15, 2026 22:43
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.
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 realtransaction-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) ande546fe1c315a3ad7d5b176f31aa9f31063cebff5(a follow-up that scopesremainingBlocksback to the proposerbranch). 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: 1fallback bought nothing,and
Math.max(1, n) > 1andn > 1agree, so packing is identical either way. The destination carried thesource's byte-exact pre-image for every hunk in both files, so nothing had to be adapted.
Only
validator-client/src/checkpoint_builder.tsand its test are touched.Tests
yarn workspace @aztec-labs/validator-client test(full package): base on this rung's parent measured at10 suites / 283 passed / 3 skipped; after the change, 10 suites / 291 passed / 3 skipped — +8, exactly the
new cases.
yarn build,yarn format,yarn lintfromyarn-project: all clean.Red/green, covering all five validations the plan requires plus the validator-mode carve-out:
L2Block.random(..., { txsPerBlock: 0 })is serialized and itstoBlobFields().lengthmeasured, then asserted equal to the sharedgetNumBlockEndBlobFields()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).and a block that packs the full allowance leaves exactly a tail block's fields free. Red before the fix.
is still zero rather than −1. Red before the fix (7 and 6 respectively); dropping the
Math.max(0, …)flooralone yields −1, which is the negative allowance the floor exists to prevent.
block's fields. Weakening the guard from
> 1to>= 1reddens this and the companion case that pins thereleased delta at a tail block alone, not a second checkpoint end marker.
transaction-less tail keeps the checkpoint within
BLOBS_PER_CHECKPOINT * FIELDS_PER_BLOB. On the unchangedcode 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.
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
here: no proposal-size computation is touched.
DA_BYTES_PER_FIELDappears only in a test assertion expressing7 fields as 224 bytes.
DutyBudget, nounverifiable/checkpoint-unverifiablestatus or sentinel encoding 9, noR − Phardgossip send cutoff, no generic Rollup artifact library map, no generic override-type or Noir-fixture cleanup.
spartan/or docs changes.Open items
None. Nothing in this rung required a judgement call that was left unresolved.