Skip to content

fix(fast-inbox): replay published blocks by message count - #25413

Draft
spalladino wants to merge 6 commits into
project/fast-inboxfrom
spl/fi2-count-addressed-replay
Draft

fix(fast-inbox): replay published blocks by message count#25413
spalladino wants to merge 6 commits into
project/fast-inboxfrom
spl/fi2-count-addressed-replay

Conversation

@spalladino

@spalladino spalladino commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Context

The message-only Inbox rolling hash survives an L1 reorg that re-mines the same ordered leaves under different bucket boundaries. An already-published block stays valid in that case, but a node reading its history back could not replay it: world-state and the validator's checkpoint reconstruction resolved a block's committed leaf count to a bucket of the current partition, and a count that had stopped being a boundary silently produced an empty message bundle. World-state also treated a missing parent block as "leaf count zero", which would ask for every message ever received as the next block's bundle.

This is the first PR of the bucketless rebuild: it changes how already-published history is read back and nothing else. Live bucket-based selection, proposals and validation keep working at this head.

Approach

Historical message retrieval addresses messages by compact count. MessageStore.getL1ToL2MessagesBetweenLeafCounts reads the exact [start, end) range from the indexed message log inside one store transaction together with the synced total, and fails with a typed InboxMessageRangeNotSyncedError when the range is invalid, reaches past the synced tip or has a hole, instead of returning a partial or empty result. World-state applies the range block by block, so a range the archiver cannot serve yet stops the sync without discarding the blocks already applied, and a missing non-genesis parent fails loudly. A fresh-world-state regression replays every published block after a same-message bucket merge over unchanged leaves.

The validator derives a checkpoint's consumed bundle from the parent checkpoint's count and the last block's count alone, with no bucket lookup at either end; whether the final position is a live bucket end remains L1's publication rule, still checked by the existing censorship guard. A parent block that is unavailable locally now yields block_fetch_error (unvalidated, not slashable) rather than an empty bundle that would fail the rolling-hash recomputation as a checkpoint_header_mismatch offense.

The kv-store mapRange helper dropped a 0n bound because zero is falsy; the count-addressed reads pass compact indices through it, so the fix is carried here.

As an additive extension for the next PRs of the stack (no consumer is wired yet beyond tests), the message source grows position-addressed reads, colocated with the L1ToL2MessageSource interface in stdlib and exposed on the archiver data source and RPC schema: InboxMessagePosition, InboxMessageRange, getMessagePosition(totalMessageCount), getSyncedMessagePosition() and getL1ToL2MessageRange(start, end). The range read returns the messages and both bounding positions from one store transaction, empty ranges included, and raises the same typed availability error when the range or its starting position is unavailable. The shared archiver mocks now treat indexed leaves as the primary fixture, derive positions from them, and enforce the archiver's range contract (typed availability error, one version of the log per read).

No store schema change (ARCHIVER_DB_VERSION untouched): positions are read from the existing per-index message records.

Deferred to the node cutover PR (P3): the plan's "bucket fixtures only in L1 contract tests" migration. The bucket fixtures in the shared mocks still serve the live bucket-based paths this PR leaves untouched; they go when those paths do.

Stacked on project/fast-inbox

Part of A-1928

Fixes A-1924

Supersedes #25408

…count

The checkpoint validator resolved the proposal's final leaf count to a bucket of the current partition before
reading the consumed range, and derived an empty bundle when that lookup missed. Both bounds are counts committed
by block headers, so the range is read by count alone; whether the final position is a live bucket end stays the
publication rule enforced by L1 and the existing censorship guard. A parent block that is unavailable locally is
reported as a fetch error instead of an empty bundle, which would have failed the rolling-hash recomputation and
been classified as a slashable header mismatch.
mapRange dropped a 0n start or end because zero is falsy, turning an exclusive end of zero into an unbounded range.
…age source

Adds InboxMessagePosition and InboxMessageRange next to the L1ToL2MessageSource interface, with
getMessagePosition, getSyncedMessagePosition and getL1ToL2MessageRange on the message store, the archiver data
source, its RPC schema and the shared mocks. The range read returns the messages and both bounding positions from
one store transaction, empty ranges included, and raises the typed availability error when the range or its
starting position is unavailable. Positions are read from the existing per-index message records; no schema change.
@spalladino spalladino added the ci-draft Run CI on draft PRs. label Sep 5, 2026
…mock range read

getMessagePosition(0) handed out a shared module-level object through a mutable type, so a caller's mutation leaked
into later reads. The mock message source yielded between capturing a range's leaves and computing its hashes and
never checked the synced tip, so it could pair one version of the log with another's hash and resolve ranges the
archiver rejects. Also corrects the validator JSDoc about what the censorship guard establishes, retires the
obsolete bucket comment in MockPrefilledArchiver, and covers zero-position mutation, a concurrent removal during a
range read, and replay resumption after a failed range.
…es by leaf count

The world-state synchronizer now reads each synced block's consumed messages by leaf count, but the
publisher integration test's block-source stub still only forwarded the removed bucket lookups, so the
first block was never applied to world state and every later block failed to fork from it.
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