Skip to content

fix(stdlib): clamp L2Tips ordering#22964

Closed
spalladino wants to merge 4 commits into
merge-train/spartanfrom
spl/clamp-l2-tips
Closed

fix(stdlib): clamp L2Tips ordering#22964
spalladino wants to merge 4 commits into
merge-train/spartanfrom
spl/clamp-l2-tips

Conversation

@spalladino
Copy link
Copy Markdown
Contributor

@spalladino spalladino commented May 5, 2026

L2Tips has five fields that should satisfy finalized ≤ proven ≤ checkpointed ≤ proposedCheckpoint ≤ proposed by block number. Consumers trust this invariant, but we have run into issues where this doesn't hold and bricks the node.

This PR adds a helper to clamp each tip to the next, so that the invariant never breaks.

Fixes A-1018

@spalladino spalladino force-pushed the spl/clamp-l2-tips branch from 64a8ca6 to 864e2ee Compare May 14, 2026 13:28
@spalladino spalladino marked this pull request as ready for review May 14, 2026 13:29
@spalladino spalladino changed the title fix(stdlib): clamp L2Tips ordering at producers fix(stdlib): clamp L2Tips ordering May 14, 2026
…op 1

When `L2BlockStream` is given a `startingBlock` past the source's checkpointed
tip, its optimization fast-forwarded `nextCheckpointToEmit` without emitting any
`chain-checkpointed` event. A later `chain-proven` emission in the same poll
cycle would then leave the local store with `proven > checkpointed` — and the
cross-tier clamp in `getL2Tips()` would mask this by reporting `proven = 0`,
causing the block stream to re-emit `chain-proven` forever and timeouts on
`p2p_client.test.ts` ('handles proven and finalized chain behind starting
point', 'stops tx collection for proven blocks', and two siblings).

Emit a single catch-up `chain-checkpointed` event (with the real
`PublishedCheckpoint` fetched from the source) in that branch so local state
catches up to source.checkpointed in one shot, without spamming intermediate
checkpoints. The cross-tier `clampL2TipNumbers` stays as a safety net.

Also: `MockL2BlockSource.setProvenBlockNumber` now cascades into
`setCheckpointedBlockNumber` (and `setFinalizedBlockNumber` calls
`setProvenBlockNumber`), matching the production invariant that proving requires
prior checkpointing.
Two issues in the catch-up emission codex flagged:

- Validation failure was non-fatal: the helper returned without emitting, but
  the caller still fast-forwarded `nextCheckpointToEmit` and proceeded to emit
  `chain-proven` / `chain-finalized`, recreating the exact `proven > checkpointed`
  invariant break the catch-up was meant to prevent. Helper now returns
  `boolean`; the caller aborts the poll on `false` and retries next cycle.

- Block-hash mismatch was not checked. `getL2Tips()` and `getCheckpoints()` are
  separate reads on the source; during a source-side reorg/cache race the
  fetched checkpoint's last-block hash can diverge from the hash the source
  reported in its checkpointed tip. Emitting then would plant inconsistent
  state in the local store. Added the hash check; mismatch → return false.
@spalladino
Copy link
Copy Markdown
Contributor Author

Closing in favor of #23295

@spalladino spalladino closed this May 14, 2026
PhilWindle pushed a commit that referenced this pull request May 15, 2026
Prevents the archiver from reporting invalid L2 tips by querying all
chain tips within a db transaction. Moves the responsibility of
assembling the tip data to the block store itself to minimize the number
of queries to the db. Clamps proven and finalized tips such that an
incorrect L1 sync still results in finalized <= proven <= checkpoints.
And adds explicit assertions that tips are ordered.

Also adds a guard in the tips store that prevents from deleting block
hashes that are still alive by a given chain tip, instead of assuming
that the finalized chain tip is always the oldest one.

This should catch errors where the block stream breaks due to a
finalized chain tip running ahead of a proven chain tip.

Note that this PR does NOT enforce ordering at the L2Tips struct itself,
since consumers (ie the ones that report the "local" chain tips) may
break this contract (see A-1061). This PR is a simpler alternative to
#22964. Fixes A-1018.
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