Skip to content

fix(fast-inbox): address the review findings across archiver, sequencer, validator and prover - #25440

Draft
spalladino wants to merge 25 commits into
spl/fi2-message-only-docsfrom
spl/fi2-review-fixes
Draft

fix(fast-inbox): address the review findings across archiver, sequencer, validator and prover#25440
spalladino wants to merge 25 commits into
spl/fi2-message-only-docsfrom
spl/fi2-review-fixes

Conversation

@spalladino

@spalladino spalladino commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Context

The bucketless Fast Inbox rebuild (#25413#25414#25415) changed what the node does with L1-to-L2 messages: every
L2 block consumes the messages the proposer's archiver has observed, blocks may end at any message prefix, only a
checkpoint's final position must be a live Inbox bucket end, validators authenticate content and never penalise a
local prefix disagreement, and the archiver stores a plain message log whose reorg recovery rolls back to the newest
message still on L1 and refetches.

This PR carries the code fixes that came out of reviewing that stack, plus the behaviour-preserving cleanups. The
documentation corrections are below it in #25416; the three changes that alter node acceptance policy or proposer
packing are deliberately not here — they are stacked above as #25436, #25437 and #25438 so each can be reviewed
and taken or dropped on its own.

Review fixes

Prover lifecycle. A checkpoint's proofs can be ready before the local work that uses the sub-tree has finished: an
empty block's root rollup is enqueued as soon as the block starts, and a cached identical-input proof returns
immediately. The sub-tree result could therefore resolve while the caller still held a block's processing fork, and the
consumer it resolved to would then tear the sub-tree down underneath that caller. A block now counts as done only once
its proof output has been compared against the locally built header and archive, and the prover node joins the sub-tree
result with its own block loop before handing proofs over or starting teardown; a processing failure rejects the join,
so failure always wins over a later success. A second, narrower instance of the same race is fixed separately: the
archive snapshot has to be read from the block's fork, so it existed before close() returned — a proof landing inside
that close found every piece present and resolved with the fork still open. The snapshot is now published after the
close, which removes the window rather than guarding it.

Validator duty bounds. retryUntil only consults its deadline once an attempt returns, so an attempt that never
settles never reaches it. A per-slot DutyBudget — one absolute deadline, shared by every stage instead of each read
starting a timeout of its own — now races the whole all-nodes checkpoint callback, the direct attestation path, and
every await inside them: the published-checkpoint and checkpoint-start reads, the previous-checkpoint lookups, the
world-state fork, the reconstruction, the Inbox prefix wait including the retry loop itself, and the signer. An
already-expired budget still allows the single attempt callers rely on, but that grace is now one absolute
allowance for the whole duty rather than a fresh second per stage.

Racing a read does not stop its continuation, so the losing side is guarded too. A fork opened after the caller gave up
is disposed by the continuation that opened it, rather than leaking one world-state fork per abandoned duty; a late
reconstruction refuses to start against a fork the caller is already closing; and a block read that lands after the
deadline cannot go on to install a pipelining parent. Attestation signing is gated in the one place attestations are
produced: nothing is signed after the slot's attestation deadline, a remote signer returning past it has its output
discarded rather than pooled and gossiped, and the equivocation record is written before the request goes out so a
signer that never answers still leaves the slot protected. Bundle retries nobody is waiting for are stopped when tx
collection rejects.

Proposal send budget. The pre-gossip preflight was budgeted against the attestation deadline, which is later than
the point at which peers stop accepting proposals for the slot. It now uses a send deadline (receive deadline minus
propagation time), and the job re-checks it after the two steps that can take arbitrarily long — building the proposal
and inserting it into the archiver — so a proposal that became pointless while those ran is not gossiped.

Archiver. RPC uncertainty was indistinguishable from chain replacement: a failed lookup looked like "this block is
gone". The canonicality check is now tri-state — canonical, replaced, unknown — and any RPC error or missing hash is
unknown, which keeps progress rather than starting recovery. Only a positive replacement clears recovery state. A
lagged-view guard sits before the shorter-head truncation, and it carries all three answers rather than collapsing them
to a boolean: a certified syncpoint above the head that cannot be read is no evidence that the messages above it are
gone, so the node waits instead of truncating. An older canonical ancestor cannot prove a later one was replaced, and
deleting on that basis would prune the speculative blocks that consumed those messages for nothing. Only a syncpoint
that reads back as a different block lets the shorter head be treated as a replacement. Separately, ingestion skipped
the Inbox deployment block itself, so a message emitted in that block was never scanned; the first scan range is now
inclusive of it.

Proposer accounting. A slot this node could not assess is now recorded as such instead of being charged to the
proposer. ReexecutionOutcome gains unverifiable and ValidatorStatusInSlot gains checkpoint-unverifiable, for a
proposal that was seen but could not be checked against anything outside this node — today, a duty whose budget ran
out. Recording nothing was not an option: with no record and no checkpoint on L1, the sentinel reads the slot as one
the proposer never proposed in, which is counted against it. The new status is in neither half of the missed-proposal
rate, so unknowns can neither charge a proposer for an observer's failure nor dilute the misses that are real, and they
do not break a miss streak. Real missed and invalid proposals keep exactly the accounting they had. SentinelStore's
status encoding gains value 9, so histories written by this version are not readable by older nodes and the new status
does not decode on them.

Configuration. A node whose effective per-checkpoint block count falls below the Inbox catch-up floor cannot clear
a mandatory backlog. That is now rejected at startup — but only on a production profile: dev and e2e configurations
legitimately derive one or two blocks per slot, so those warn instead. This is a deliberate deviation from a plain
floor check, which rejected every local configuration including the e2e pipelining setup — and it is wider than the
sandbox it is for: isFastLocalProfile is a threshold on the Ethereum slot duration, not a declaration that the node
is a development one, and no such signal reaches the sequencer. A real network running short Ethereum slots is
therefore exempted too, and faster L1 slots do not raise the per-block message cap, so such a sequencer can still be
unable to clear an aged backlog. The deviation is stated in the code and its boundary is pinned by a test; it needs
explicit acceptance rather than being a completed guard.

Accounting and artifacts. The serialized proposal size undercounted the 32-byte fee modifier as 8; it is now
measured from the serialization itself. The exported generic Rollup artifact was missing EpochProofExtLib and
carried a stale ValidatorSelectionLib entry, so a generic deployer could not link it; a test now asserts the link
references and the library map agree in both directions.

Diagnostics. Every failure of the validator's Inbox bundle range read produces the same non-punitive verdict, but a
store fault is not the same thing as a node that is behind L1. Unexpected failures now carry bounded structured
context; genuine lag stays at debug and stays non-punitive.

Cleanups. An unused pre-gossip bucket hint and its fisherman dummy, a dead recovery field and an unreachable
post-continuation branch, a duplicated range read when a bundle ends exactly on the resolved endpoint, hand-restated
override field types, a Noir fixture builder with no callers, a dead single-block resolution branch, and a global
log-dedupe cache. Net removal.

Test and fixture corrections

Three fixtures asserted less than their names claimed. The archiver re-mine test moved messages between L1 blocks
without replacing any block identity, so it passed as an ordinary forward append and never reached recovery. The e2e
helper that identifies which block inserted a message sampled its lower scan bound after observing the message, so
the first block it found was where the search started, not where the insertion happened; it now bisects the chain and
confirms membership at the block and absence at its actual parent. The rollup sample generator captured data[0] — the
first block, with an empty inherited sponge — for a scenario that intends the continuation block; samples are now
selected by shape and the selector fails loudly when nothing or several things match. The committed Prover.toml is
unchanged: correcting it needs the owning generator workflow, which was not run here.

The developer proving-time simulation gated Inbox parity behind the first block root and refused zero-transaction
blocks anywhere but first. Production does neither, so the model is brought in line — parity is independent, the join
is at checkpoint-root readiness, and zero-tx blocks queue their own roots at any position. This is a developer model,
not a measured production regression.

What is deliberately not fixed here

  • The automine aged-backlog escape hatch is deferred, per the recorded decision. No fix in this PR.
  • The message-count prediction a public simulation makes can still overestimate what a checkpoint's final block
    consumes. Only the documented contract is corrected, in docs(fast-inbox): document message-prefix consumption and correct the streaming Inbox docs #25416: it is best effort, and a bound in neither direction.
    With the cursor at 0 and live buckets ending at 200 and 400, simulation appends 256 while the final block lands on
    200, so a public call reading message index 220 simulates and then fails. Sharing selection between the simulator
    and the sequencer is deferred.
  • Recovery still discards unchanged work that moved outside the bounded lookup window, since rollback precedes
    refetch. That is the accepted conservative-recovery tradeoff, now described honestly in the tests and docs.

Testing

Focused regressions accompany each fix, and the ones where a behavioural change could be demonstrated were confirmed
red before green — the prover join and the fork-close window, the send budget, the archiver canonicality and deployment
block, proposal size, the fixtures and the proving-time model.

Package suites, all passing: archiver 639, stdlib 1094, sequencer-client 293, validator-client 338, aztec-node 183,
prover-client 257, prover-node 188. The full-package archiver run exits 129 (SIGHUP) with zero failures; it does so
identically on the untouched base, so it is an artifact of the runner rather than of this branch.

yarn format --check and yarn lint exit 0. yarn build exits 1, on one error:

end-to-end/src/single-node/cross-chain/streaming_inbox_load.test.ts(12,10): error TS2305:
  Module '"@aztec/stdlib/messaging"' has no exported member 'getInboxCutoffTimestamp'

That file is untracked, unrelated, in-progress work that was deliberately left alone. No other compilation error is
reported, and no worktree or exclusion was used to manufacture a clean build — so nothing in this branch fails to
compile, but the command did not pass and is not claimed to have.

No e2e test was run — CI covers those, which leaves the findInsertingBlock correction unverified locally. The
anvil-backed @aztec/ethereum suites need single-worker execution and were not run to completion locally; the two
files this PR changes there pass. No Solidity compile, circuit proving or generator run was performed, and no
generated file or source pin moved.

F11c is only partly repaired: the sample generator now selects the continuation-shaped capture, but
noir-projects/fnd/noir-protocol-circuits/crates/rollup-block-root/Prover.toml still holds the old first-block,
empty-inherited-sponge sample. Finishing it needs regeneration through the owning AZTEC_GENERATE_TEST_DATA=1
workflow plus nargo execute verification, which was deliberately not run here.

Stacked on #25416 (spl/fi2-message-only-docs). #25436, #25437 and #25438 stack above this one.

Part of A-1928
Fixes A-1926

🤖 Generated with Claude Code

spalladino and others added 19 commits September 10, 2026 01:04
… scan the deployment block

An L1 block this node depends on now reads back as canonical, positively replaced, or unreadable. An RPC
exception, a lagging provider and a pruned range all read as unreadable, and none of them deletes messages,
restarts recovery or claims a replacement any more. A head reporting fewer messages than the local log is
also no longer truncated against while the certified syncpoint above it is still canonical, so a provider
that is merely behind cannot prune valid speculative consumers.

Ordinary ingestion now re-reads the deployment block while the scanned cursor still sits on it. The Inbox's
first message can be emitted by a later transaction in that block, and an exclusive cursor defaulting to it
skipped index 0 permanently, including on stores a zero-anchor rollback had already rewound onto it.

A missing recovery candidate no longer reports the Inbox's tip hash as the hash it expected at that count.

Original findings: SUP-01, SUP-04, SUP-06, SUP-10.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…as to link

The Rollup bytecode links against EpochProofExtLib, but neither the artifact generator's contract list nor
RollupArtifact.libraries.libraryCode carried it, so deployL1Contract threw before deploying. Add it, and drop
ValidatorSelectionLib, which is no longer a link reference and would otherwise be deployed for nothing. A unit
test now asserts the two sides of the map agree in both directions.

Also passes the generated RollupAbi to the preflight simulation directly: it already merges the combined errors
ABI at generation time, so merging ErrorsAbi into it again was a no-op.

Original findings: UMB-SOL-01, SUP-S05, SIMPLIFY C04.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
getSize counted eight bytes for feeAssetPriceModifier while toBuffer writes the 32 serializeSignedBigInt
produces, undercounting every proposal by 24 bytes. Measure the buffer instead, which cannot drift as optional
fields are added, and cover the optional proposal shapes.

Original finding: UMB-NODE-I01.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ory Inbox backlog

validateNetworkConsensusConfig applies MIN_BLOCKS_FOR_INBOX_CATCHUP to a generated profile's configured
maxBlocksPerCheckpoint, but a running proposer is bounded by the smaller of that cap and what its own slot
timings derive. Timings that shrink the derived count below the floor leave a proposer whose publications L1
always rejects, losing every one of its slots. Check the effective count at startup and before committing a
config update; a rejected update leaves the previous config and timetable in place.

Fast local/e2e profiles deliberately run one or two blocks per slot against an Inbox nobody floods, so they
warn instead of failing. isFastLocalProfile now names that condition once for both callers.

Original finding: SUP-02.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…not the attestation deadline

The pre-gossip preflight was bounded by the attestation deadline, a full ethereum slot plus a block duration
after peers stop accepting proposals for the slot. A verdict arriving in that gap led the proposer to sign and
gossip a proposal every peer refuses on ingress.

Derive the send budget from the consensus receive deadline instead, reserving one propagation budget, and reuse
that as the forced-tail hard stop rather than repeating the formula. Re-check it after signing (which may be
remote) and after the queued archiver insertion, immediately before broadcast, so neither can carry the job past
the point where a send is still useful; a signature already produced keeps its duty record, and the local
optimistic tip stays advanced. Send timeouts report as proposal_send_timeout, separately from an invalid header.

The job tests now start at the target slot's build frame opening rather than the target slot start, which is when
a proposer actually takes its turn; anchoring at the slot start put every job past the deadlines the timetable
derives.

Original finding: NODE-01.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…eads

retryUntil only consults its deadline once an attempt returns, so an attempt that never settles never reaches
it. The bound now lives where every entry path passes through it: the all-nodes checkpoint callback, which p2p
awaits before the validator's own and which is the only one a non-validator node runs. One DutyBudget per slot
races the whole callback, is threaded into the cached-verdict re-check and the readiness waits, and can be
stopped early; an already-expired budget still allows the single attempt callers rely on, bounded by a grace so
even that cannot hang.

Attestation signing is gated in the one place attestations are produced, covering the cached-verdict path.
Nothing is signed after the slot's attestation deadline, and a remote signer returning past it has its output
discarded rather than added to the pool or gossiped; the signing-protection record it produced still stands.
The proposer's own attestations run inside its publish budget and pass no deadline, so they are unaffected.

Tx collection and the block's bundle read start together. A catch handler on the loser silenced its rejection
but did not stop it: once collection fails, the bundle's retries are now cancelled instead of forcing archiver
syncs for the rest of the slot. The shared archiver run itself is never cancelled.

Original findings: NODE-03, SUP-07.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…off a sub-tree

A checkpoint's proofs can be ready before the local work that uses the sub-tree has finished. An empty block's
root rollup is enqueued from startNewBlock, and a cached identical-input proof returns immediately, so the
sub-tree result could resolve while the caller still held a block's processing fork — and the consumer it
resolved to would then tear the sub-tree down underneath that caller.

Both sides now wait for local completion, not for the pieces to be present:

- A block counts as done only once its proof output has been compared with the header and archive built
  locally. `verifyBuiltBlockAgainstSyncedState` records that, and sub-tree resolution requires it for every
  block, so parity landing during `db.close()` or verification no longer resolves early.
- The prover node joins the sub-tree result with its own block loop before resolving the proofs and starting
  teardown. A processing failure rejects the join, so failure always wins over a later success.
…schedules them

The developer proving-time simulation gated inbox parity behind the first block root and refused a
zero-transaction block anywhere but first. Production does neither: a zero-tx block enqueues its own block root
from startNewBlock at any position, and parity runs independently of block-root production, joining at
checkpoint-root readiness. The join is now reevaluated from whichever dependency finishes last and guarded
against a duplicate enqueue, with coverage for no/one/many transactions, mixed empty blocks, and parity landing
first or last.

This corrects a developer model. It is not a measured production regression, and its timings are model output.
The archiver re-mine fixture moved messages from block 100 to 120 without replacing any block identity, so it
passed as an ordinary forward append and never reached recovery. It now re-mines the descendants too, and is
split from the plain append case it was standing in for. Discarding unchanged work that moved outside the
bounded lookup window is the accepted cost of rolling back before refetching, and the test says so.

`findInsertingBlock` sampled a lower scan bound after observing the message, so the first block it found was
where the search started rather than where the message was inserted. It now bisects the whole chain and
confirms at both states — the block resolves a membership witness at the message's own index and its actual
parent resolves none — retrying a genuine timing miss a bounded number of times.

The rollup sample scenario splits 2 then 3 messages but captured `data[0]`, the first block with an empty
inherited sponge. Samples are now selected by shape — inherited message count and bundle size — and the
selector fails loudly when nothing, or more than one thing, matches. The committed sample still holds the old
capture; correcting it needs the owning generator workflow, which was not run here.
…ordinary sync lag

Every failure of the bundle range read yields the same non-punitive verdict, but a store fault or a broken
provider is not the same thing as a node that is behind L1. Unexpected failures are now reported with bounded
structured context while genuine lag stays at debug, so an operator can tell them apart without changing what
the validator does with the proposal. The classification matches on message text, since the archiver's typed
range error does not survive a JSON-RPC hop.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Behavior-preserving reductions alongside the code they belong to:

- The pre-gossip bucket hint was threaded through the broadcast result and never read; the publication
  preflight resolves its own hint fresh. Removed, along with the fisherman dummy that existed to fill it.
- Selecting a bundle that ends exactly on the resolved endpoint reuses the range the resolver already read and
  checked, instead of reading the same prefix again. A shorter or longer end still gets its own read, so no
  endpoint hash is ever carried into a different prefix.
- Recovery state carried a finalized L1 block that nothing consumed, and a synced-status downgrade after
  continuing recovery that could not be reached — continuation only ever returns pending.
- The pending-checkpoint override field types are derived from the log-override type that owns them rather
  than restated, and two consumed-message-count call sites reuse the existing leaf-count helper.
- The Noir fixed-message-subtree fixture builder and its root helper had no callers left.
The block-root proof callback re-enqueued the sub-tree resolution for a
single-block checkpoint, but verifyBuiltBlockAgainstSyncedState already
enqueues it after marking the block verified, and the resolution gate
refuses to fire until every block is verified — so the extra call could
only ever be a no-op.

Reporting an Inbox range-read failure no longer needs a module logger, a
process-wide dedupe Set and a truncation constant: the unexpected errors
carry their bounded text on the result, and the proposal handler's
existing one-shot timeout warn reports it. Sync lag stays silent and
non-punitive, and the classification still matches on message text since
the error class does not survive a JSON-RPC hop.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The archive snapshot has to be read from the block's fork, so it existed before `close()` returned while the
header and the proof output could both already be in hand. Verification runs from the block root proof callback
as well as from completion, so a proof landing inside that close found every piece present, marked the block
verified and resolved the sub-tree with the fork still open — the same handoff the completion join exists to
prevent, reached by a different path.

Capturing the snapshot into a local and publishing it after the close removes the window entirely rather than
adding another guard. Covered by a test that parks completion inside the close and releases the block root
proof into it.
…tion stage

Racing the caller only bounds that caller's wait. The stages after it — the published-checkpoint lookup, the
checkpoint start position, the previous-checkpoint reads, the world-state fork and the checkpoint rebuild — ran
unbounded, so a read that never settled held the duty open and its losing continuation could still walk into the
next stage. Each stage now runs inside the slot's budget, and recording the proposed checkpoint is barred after
its own block read rather than only before it.

The direct attestation path had no whole-duty bound at all: its preliminary epoch and committee reads and the
remote signing await sat outside every deadline. It now takes one absolute budget for the slot and passes it
through validation into signing, and the signing-protection record is written when the request goes out rather
than when it comes back, so a signer that never answers cannot leave the slot unprotected.

Running out of time is this node giving up, not an observation about the proposal, so it answers with its own
`validation_deadline_expired` reason, records no outcome and caches no verdict for the next caller.

The expired-budget grace is now one absolute allowance for the whole duty instead of a fresh second for every
call, which let a duty already past its deadline walk stage by stage through as many seconds as it had stages.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…g truncation

The guard before the shorter-head truncation asked a three-outcome question and answered it with a boolean, so a
syncpoint that could not be read came back the same as one that was positively replaced. A provider answering
`latest` at an older canonical block then truncated the log to it and pruned the speculative blocks that had
consumed the messages above it, without anything ever establishing that those messages were gone.

The guard now reports lag, positive replacement and unresolved separately. Only positive replacement lets the
head be shortened to; an unreadable syncpoint keeps the log and waits. A head that disagrees with the local log
at its own count is a different question and still goes to recovery, which finds its anchor on L1, so a chain
that really did shorten below the syncpoint is not left waiting for a block that will never come back.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e exemption reaches

`isFastLocalProfile` is a threshold on the Ethereum slot duration, not a declaration that a node is a development
one, so the exemption also covers a real network running short Ethereum slots — which faster L1 slots do not make
safe, since the per-block message cap is unchanged. Say so where the exemption is taken, and pin the boundary in
a test so the strict rejection one second slower is not lost.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…the proposer

Recording nothing for a slot is not neutral. With no record and no checkpoint on L1 the sentinel reads the slot
as one the proposer never proposed in — `checkpoint-missed`, or `blocks-missed` — and both are counted in its
missed-proposal statistics, which feed inactivity. So a node whose validation duty ran out silently charged the
proposer for its own clock.

The taxonomy gains one outcome for what actually happened: `unverifiable`, this observer could not check the
proposal against anything outside itself. It travels tracker → sentinel → statistics as
`checkpoint-unverifiable`, says a proposal was seen so the missed-proposal fallback does not apply, and is in no
missed-proposal filter. It never revises an outcome the slot already has: a duty that gave up learned nothing
that could. Real missed and invalid proposals keep exactly the accounting they had.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…nassessable-slot accounting

A second review pass over the budget work found four things the first one missed.

The reads inside the Inbox prefix wait were still unbounded — the first read, and every attempt of the retry loop,
which consults its deadline only after an attempt returns. So was the fork's archive-root read. Each now runs
inside the budget, and the attestation entry point races its whole body as well, so an await this audit missed
cannot hold p2p's callback open past the slot.

Racing a resource acquisition is not enough on its own: the losing continuation could still open a world-state
fork nobody would ever close, because the caller never reached the `await using` that owns it, and could start
rebuilding a checkpoint against a fork the caller was already disposing. Both now check the signal they are
handed, and the fork read closes what it opened.

An expiry in a stage before validation is reached never passed through the recording inside, so on a node that is
not a validator — where the all-nodes callback is the only handler there is — the slot still read as one the
proposer skipped. The outermost boundary records too.

And an unassessable slot was being left in the denominator of the missed-proposal rate while excluded from its
numerator, so one real miss among nine unverifiable slots reported as 10% missed rather than 100%. A slot nobody
could assess is not a slot survived: it now counts towards neither half, and does not break a miss streak.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@spalladino
spalladino changed the base branch from spl/fi2-bucketless-node-cutover to spl/fi2-message-only-docs September 10, 2026 13:04
@spalladino
spalladino added this pull request to stack #25451 September 10, 2026 13:05
…t cover

A proposer holding 500ms of send budget would still enter a 500ms transaction poll, spend the whole
remainder on a wait it could not act on, and then miss the send deadline during signing and archiver
insertion. Peers refuse the proposal once that deadline passes, so the poll cannot produce a block anyone
would accept. Check the remaining budget before each wait and, when a full interval will not fit, stop
waiting and let the checkpoint go with the blocks already built.

This is the defect class the send-deadline budgeting was introduced for: do not start work the budget
cannot cover. The deadline itself is unchanged.
…annot cover

Giving up on a block for want of txs falls through to a wait for the next sub-slot, which is longer than
the tx poll the previous commit stopped starting and is governed by the same budget. Without this the poll
guard just moves the overrun: the block is abandoned promptly and the slot is then spent waiting anyway,
carrying the blocks already built past the send deadline. Break out instead, so the checkpoint goes while
it can still be sent.
@spalladino
spalladino removed this pull request from stack #25451 September 10, 2026 13:53
@spalladino
spalladino changed the base branch from spl/fi2-message-only-docs to spl/fi2-bucketless-node-cutover September 10, 2026 13:54
@spalladino
spalladino changed the base branch from spl/fi2-bucketless-node-cutover to spl/fi2-message-only-docs September 10, 2026 13:54
@spalladino
spalladino added this pull request to stack #25452 September 10, 2026 13:54
@spalladino
spalladino marked this pull request as draft September 10, 2026 14:22
A block freezes its L1-to-L2 message set when it is prepared, which happens before the message's L1 tx is
mined, so a consume tx sent as soon as the archiver observes the message joins the block already building
and reverts one block early. The case retried five times, but the attempts were a slot apart and so all
landed at the same phase, repeating one experiment rather than sampling five; it passed only when
something unrelated delayed the send into the gap it needs.

Hold production while the message is sent and observed and the consume tx is queued, so the first block
prepared after the resume both inserts the message and executes the tx. The same-block condition is now
asserted rather than retried for, since a consume one block late proves nothing.
… helper

addBlock resolves once the block is stored but triggers a sync it does not await, so every fixture that adds
local blocks and then moves the L1 head backwards races the pass left in flight: recovery against the stale
head can commit after the pass for the new head and leave the old height as the synced one. Two fixtures hit
this, the second failing about twice in twelve runs. Drain once in the shared helper rather than at each call
site, so the hazard is gone for the whole block.
@spalladino
spalladino removed this pull request from stack #25452 September 10, 2026 14:31
@spalladino
spalladino changed the base branch from spl/fi2-message-only-docs to spl/fi2-bucketless-node-cutover September 10, 2026 14:31
@spalladino
spalladino changed the base branch from spl/fi2-bucketless-node-cutover to spl/fi2-message-only-docs September 10, 2026 14:31
@spalladino
spalladino added this pull request to stack #25453 September 10, 2026 14:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci-draft Run CI on draft PRs.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant