fix(fast-inbox): replay published blocks by message count - #25413
Draft
spalladino wants to merge 6 commits into
Draft
fix(fast-inbox): replay published blocks by message count#25413spalladino wants to merge 6 commits into
spalladino wants to merge 6 commits into
Conversation
…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.
…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.
This was referenced Sep 5, 2026
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.getL1ToL2MessagesBetweenLeafCountsreads the exact[start, end)range from the indexed message log inside one store transaction together with the synced total, and fails with a typedInboxMessageRangeNotSyncedErrorwhen 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 acheckpoint_header_mismatchoffense.The
kv-storemapRangehelper dropped a0nbound 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
L1ToL2MessageSourceinterface in stdlib and exposed on the archiver data source and RPC schema:InboxMessagePosition,InboxMessageRange,getMessagePosition(totalMessageCount),getSyncedMessagePosition()andgetL1ToL2MessageRange(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_VERSIONuntouched): 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