Skip to content

test: complete Fast Inbox end-to-end coverage - #243

Merged
spalladino merged 12 commits into
mainfrom
spl/a-2019-complete-inbox-coverage
Sep 18, 2026
Merged

spalladino merged 12 commits into
mainfrom
spl/a-2019-complete-inbox-coverage

Conversation

@spalladino

Copy link
Copy Markdown
Collaborator

Complete the missing multi-validator Inbox reorg scenario and strengthen the existing streaming and placement-only reorg tests.

Context

The earlier coverage layer consolidated the suite to 12 cases, but did not test how a committee rejects a signed block after its consumed message prefix changes on L1. The streaming and placement-only reorg cases also needed stronger timing and canonical-chain evidence.

This extends the coverage in #189 as a new layer on the Fast Inbox stack. Stacked on #239.

Approach

  • Extend the existing multi-node L1-to-L2 case to replace a message while a signed block is held, observe the named validator's initial mismatch and final non-slashable rejection, and verify recovery with its signature and canonical message witnesses.
  • Keep the streaming consume transaction and placement-only reorg tied to the original block identities through simulated proving. Authenticate message synchronization against canonical L1 block hashes.
  • Make the test gate use the proposer's clock and timetable, cover watchdog and release behavior, and verify replacement-transaction gas handling.
  • Add optional in-process observations and fixture dependency wiring. No new RPC methods, serialized configuration, e2e jobs, or e2e cases are introduced.

Proving uses the existing simulated prover and production orchestration; these tests do not claim cryptographic proof verification.

Local validation: all 12 end-to-end cases across the five existing files passed sequentially, along with 423 focused tests. The full TypeScript build, formatting check, and lint passed. Before publication, this branch was fetched and rebased onto the updated #239 tip; its gas fix was preserved, and the gas-helper tests and full-project checks were rerun.

Fixes A-2019

@spalladino
spalladino added this pull request to stack #217 September 17, 2026 21:52
@spalladino
spalladino marked this pull request as ready for review September 17, 2026 22:31
@greptile-apps

greptile-apps Bot commented Sep 17, 2026

Copy link
Copy Markdown

RetriggerConfidence Score: 4/5

The behavior appears sound, but the explicit repository rules must be satisfied before merging.

Fix All in CodexFindings

  1. P2 Tests bypass type checks
  2. P2 Comment names its caller
Summary

Fast Inbox tests now cover L1 message replacement during a multi-validator checkpoint, including the validator’s non-slashable rejection and recovery. The PR also makes held-checkpoint tests use live proposer timing and canonical L1 block hashes, so streaming and placement-only reorg cases stay tied to the exact blocks they exercise.

  • Adds a multi-validator Inbox prefix reorg scenario.
  • Adds live timing, watchdog, and release controls for held checkpoint tests.
  • Confirms message syncpoints against canonical L1 block hashes.
  • Strengthens streaming, backlog, placement-only reorg, and replacement-gas tests.
Diagram
sequenceDiagram
    participant Test
    participant Proposer
    participant L1
    participant Archiver
    participant Validator
    participant Prover

    Test->>Proposer: Arm block-ready gate
    Proposer->>Proposer: Build, sign, and store block
    Proposer-->>Test: Hold before gossip
    Test->>L1: Replace consumed Inbox message
    L1-->>Archiver: Canonical reorg and new block hash
    Test->>Archiver: Wait for canonical message syncpoint
    Test->>Proposer: Release held block
    Proposer->>Validator: Gossip stale signed block
    Validator->>Archiver: Check signed Inbox prefix
    Archiver-->>Validator: Prefix mismatch
    Validator-->>Test: Reject without slashing
    Proposer-->>Test: Report abandoned checkpoint
    Proposer->>L1: Build and publish recovery checkpoint
    Validator->>Proposer: Sign recovery checkpoint
    Prover->>L1: Submit simulated proof
    Test->>Archiver: Check canonical replacement witnesses
Loading

Reviews (1) · Last reviewed commit: "fix(test): count the abandoned slot's ab..."

const { proposal, blockHandler } = await setupStreamingProposal(signedRef, { observers });
mockLocalView(new Fr(0xdead));

const result = await blockHandler.handleBlockProposal(proposal, {} as any, true);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 These new tests turn off type checks with as any. The repository guide says to avoid casts and never use as any. Use typed peer and re-execution fixtures instead. The same pattern appears at lines 1798 and 1814, and elsewhere in this block.

Context Used: yarn-project/CLAUDE.md (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Codex Fix in Claude Code

* `remainingBuildSubslots` is a snapshot taken here: a hook that holds the job spends the slot's real budget, so a
* caller that needs to know whether another block can still be built has to re-check `proposalSendDeadline`
* against the clock before it releases.
* caller that needs to know whether another block can still be built asks the `schedule` view instead. That view

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 This comment describes what a caller should do. The repository guide says comments must not refer to callers. State the timing rule directly, such as: “schedule reflects the loop's next iteration.”

Context Used: yarn-project/CLAUDE.md (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Codex Fix in Claude Code

@spalladino
spalladino force-pushed the spl/a-2020-enable-inbox-bot-next-net-staging branch 2 times, most recently from ec90a7e to 8f55058 Compare September 18, 2026 13:02
@spalladino
spalladino force-pushed the spl/a-2019-complete-inbox-coverage branch from 38e4d4d to 55c9f56 Compare September 18, 2026 14:52
@spalladino
spalladino removed this pull request from stack #217 September 18, 2026 18:59
@spalladino
spalladino force-pushed the spl/a-2020-enable-inbox-bot-next-net-staging branch from 8f55058 to e9705be Compare September 18, 2026 19:40
Base automatically changed from spl/a-2020-enable-inbox-bot-next-net-staging to main September 18, 2026 19:40
The gate's budget question was only ever 'is the proposal send deadline
open', which stays true for a whole block sub-slot after the last one a
proposer can start, and its snapshot of remaining sub-slots went stale
the moment a hold began spending them. The phase event now carries the
proposer's own scheduling view, so a held test asks ProposerTimetable
what is still startable instead of repeating its arithmetic.

Also closes the lifecycle holes around the watchdog: a timeout now fails
the match and completion channels rather than leaving consumers waiting
on an outcome that can no longer arrive, withHold races the failure
channel against the whole held body instead of only the match, and a
body that outlived its hold refuses further chain work rather than
mutating state behind a resumed proposer.
…tion

canStartAnotherBlock asked the timetable at the current instant, which
still returns the sub-slot the block was just built in whenever the
build finished early, and compared it against indexWithinCheckpoint,
which drifts from the sub-slot index as soon as a sub-slot produces no
block. Both made an early-built block look like the last one possible.

The event now reports the sub-slot it was built in, and the schedule
answers for the iteration the loop will actually run: no earlier than
the held sub-slot's deadline, and bounded by the checkpoint's block
count cap as well as by time.

Releasing before any phase matched now settles the match as a
cancellation instead of leaving a withHold body waiting for a phase the
gate has stopped listening for.
…reconciliation

E1 proved its public consume while holding a live proposer, spending the
slot's budget on a client-side proof rather than on the L1 round trips
the scenario needs. Nothing else sends to this Inbox, so the compact
index is known before the hold and the proof is done up front; the send
then asserts it got that index, so a competing sender fails the premise
rather than the assertion. The same-block and proof claims now pin the
block by hash, not by number, which a prune could reuse.

E3 armed its gate after both L1 sends, so a running proposer could
consume them before there was anything to hold it, and waited for
reconciliation on a message count and rolling hash that a placement-only
reorg leaves unchanged — a wait that was already satisfied when it
started. Production is now stopped across the sends and the gate armed
before it resumes, and the wait is on the archiver's message syncpoint
naming the replacement L1 chain. Its never-pruned claim is backed by
prune events collected across the whole window instead of a read of the
final chain, and the prover's next submission is deferred past the
replacement rather than being left to race it.
The syncpoint wait accepted any height above the replacement anchor
without checking its hash, and the pre-reorg syncpoint normally sits
above that anchor — so the wait was satisfied before any reconciliation
had happened. It now requires the canonical chain to actually hold the
block the syncpoint names, at its own height, on every path; a syncpoint
left on the abandoned suffix fails that whether it is at the same height
or higher. Extracted with its own unit regressions.

The gate was also armed after the sequencer restarted rather than
before, and the prover deferral was installed after the window had
already been scanned, so neither prevented what it claimed to. The
window scan missed checkpoint publications and proofs entirely: both go
through the Multicall3 forwarder, not straight to the rollup.

E1's background readiness poll now runs against a view the case can
cancel, so a failure inside the hold settles it instead of leaving it
polling a node the fixture is tearing down.
…ted block decision

A multi-node reorg test cannot tell a prefix mismatch that recovered
from one that persisted: the metadata helper retries a local-view
mismatch until its deadline, so both look the same by the time a verdict
exists. Two optional in-process observations make the difference
visible — the first comparison, and the decision once classification and
any slashing side effect have run — with slashability read from the
production table rather than a second one kept for tests.

Injected through the node factory like the existing checkpoint hooks, so
they are absent from every path that does not pass them and unreachable
over RPC. Each test node gets its own object, which is what lets a
multi-node test hold one proposer and assert on a named validator.

Also surfaces the streaming-Inbox checkpoint abort as an event beside
the metrics and log line it already produced, and lets the multi-node
proof helper name expected sequencer failures instead of clearing the
list.
…to-L2 case

The happy path already has single-node coverage. What only multi-node
can show is that a validator reading a changed message prefix treats it
as its own view moving rather than as proposer misconduct, that the
proposer gives up the abandoned slot for the right reason, and that the
committee recovers quorum on a replacement built from the canonical
prefix.

The stale block is signed and stored before L1 changes and gossiped
after, so the validator's mismatch is against a parent it still holds.
Every claim is read from an observation attached before the release: the
first metadata comparison, the completed decision, the prune events, the
slashing emitter and the proposer's abort — not from logs, and not from
the disabled slasher API.
…al controls

The first draft asserted on the message payload rather than the leaf
hash the Inbox emitted, picked its replacement checkpoint with a helper
that can return an older happy-path one, allowed every failure at the
abandoned slot, and relied on a post-hoc scan of the reorg window
instead of stopping anything from entering it.

Now: the replacement's hash comes from the MessageSent event its
replacement block emitted; the checkpoint is the first past the parent
whose blocks actually insert it, with the abandoned slot excluded across
the whole published range and the pinned block identity rechecked after
proving; the named validator's signature is recovered from its
attestation over the published archive rather than read off address
metadata; only the abandoned slot's own inbox_prefix_reorged abort is
allowed through the sequencer failure list; and the window is protected
by pausing every sequencer between checkpoints, deferring the prover's
next submission before anything is sent, and using a dedicated L1
sender, with the scan left as confirmation.

The held work also checks the job's live schedule and ingress budget
immediately before releasing, guards each mutation against a watchdog
that already fired, and awaits the completion channel instead of
swallowing it.

The validator's decision observation reports the node's real answer: an
open escape hatch rejects a proposal that validated, so it is reported
alongside the verdict rather than after it, and the observers are now
exercised through ValidatorClient.validateBlockProposal and its actual
offense emitter.
…org replacement keep its gas limit

Every deadline the checkpoint gate reports comes from the job's
DateProvider, but the gate compared them against Date.now(). The e2e
provider runs at a fixed offset from wall clock, so a held block could
be certified as having budget left in a slot the proposer had already
spent — the guard passing exactly when it should fail. The schedule now
exposes the job's own clock and every budget, next-sub-slot and
block-count answer defaults to it. The watchdog still measures real
elapsed time, since it guards the test process rather than the protocol.

An unsigned anvil_reorg replacement also has to carry its gas limit.
Anvil estimates when none is given, and it estimates against the chain
before the rollback: the Inbox sends these tests replay are cheap there
because their bucket is already open, and far more expensive once
re-mined into cold state. Measured against a local anvil: with the limit
serialized the replacement declares the 900000 it asked for, without it
anvil substitutes its own 36492 estimate.
Both reorg cases paused production, sent their messages, armed the gate
and restarted wherever the sends had left the clock. Resuming late in a
build frame leaves one sub-slot, so the block that first consumes the
messages is the checkpoint's final block rather than a standalone one:
the gate skips it, an unheld checkpoint consumes and publishes the
messages, and the reorg then invalidates a publication nobody was
holding. In the observed run that took the held block down with it and
republished its checkpoint at a later slot.

They now wait for the next build window with the fixture's existing
timetable helper, which waits in real time rather than warping, so the
first checkpoint after the pinned parent is the one held. The
placement-only case also asserts that premise directly — the held
checkpoint is exactly the published parent's successor, and nothing
published into the reorg window — so a recurrence fails as a premise
rather than as a confusing downstream identity mismatch.
…ble to attest

The case names a non-proposer validator and requires its signature on
the replacement checkpoint, but picked it by node index. The wide-slot
profile seats 3 of the 4 registered validators, so that choice was not
guaranteed to be in the committee at all — a missing signature would
have been a setup artefact rather than a protocol result.

The case now seats all four and picks the named validator from the
committee the rollup reports for the held slot, checks it is still
seated at the replacement slot, and measures quorum against that
committee rather than against the node count. The signature itself is
still recovered from its attestation over the published archive.
…f helper stops the sequencers

The check ran after waitForProvenCheckpoint returned, and that helper
stops every sequencer: a proposer interrupted mid-build reports
'Sequencer was interrupted' two milliseconds later, so the assertion
could only ever observe teardown noise. It now asserts on the failures
the scenario itself produced, snapshotted before the stop, and requires
the abandoned slot's streaming abort to be present exactly once rather
than merely permitted.
…ords it

The abort presence check was made against the generic sequencer failure
list, which never carries checkpoint-build-aborted: the fixture's
watcher registers a fixed set of failure events and this one is not
among them, which is why the scenario collects it separately in the
first place. The assertion could not have passed even on a healthy run.

It now counts the scenario's own abort collector, and pins all four
identifying fields — the abandoned slot, its checkpoint, the proposer
that gave it up, and the inbox_prefix_reorged reason — so a coincidental
abort elsewhere cannot satisfy it.
@spalladino
spalladino force-pushed the spl/a-2019-complete-inbox-coverage branch from 55c9f56 to 0478e6b Compare September 18, 2026 19:40
@spalladino
spalladino merged commit 3dba9ad into main Sep 18, 2026
3 of 4 checks passed
@spalladino
spalladino deleted the spl/a-2019-complete-inbox-coverage branch September 18, 2026 19:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant