test: complete Fast Inbox end-to-end coverage - #243
Conversation
|
| const { proposal, blockHandler } = await setupStreamingProposal(signedRef, { observers }); | ||
| mockLocalView(new Fr(0xdead)); | ||
|
|
||
| const result = await blockHandler.handleBlockProposal(proposal, {} as any, true); |
There was a problem hiding this comment.
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!
| * `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 |
There was a problem hiding this comment.
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!
ec90a7e to
8f55058
Compare
38e4d4d to
55c9f56
Compare
8f55058 to
e9705be
Compare
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.
55c9f56 to
0478e6b
Compare
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
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