Skip to content

fix(archiver): fully clean up removed blocks whose txs were re-included elsewhere#24732

Closed
benesjan wants to merge 1 commit into
nextfrom
fix/archiver-tx-effect-index
Closed

fix(archiver): fully clean up removed blocks whose txs were re-included elsewhere#24732
benesjan wants to merge 1 commit into
nextfrom
fix/archiver-tx-effect-index

Conversation

@benesjan

@benesjan benesjan commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Problem

BlockStore's tx-effect index (txHash → owning block hash/position) can be corrupted when the same tx exists in two stored blocks at once — routine when a checkpoint proposal expires and the sequencer re-includes its txs:

  1. Blind deletedeleteBlock removed #txEffects entries by txHash without checking the entry still points at the block being deleted. Inserting the re-included block repoints the shared tx's entry at the new block; deleting the old block then destroys the new block's entry, making that block unreadable (Could not find tx effect for tx…).
  2. Skip-on-unreadable leakremoveBlocksAfter did getBlock(bn) === undefined → warn + continue, so an unreadable block's row, tx effects, and indices were never released. addCheckpoints later overwrites the leaked row in place ("L1 data is authoritative"), leaving stale tx-effect entries pointing at coordinates now occupied by different txs.

Downstream symptoms of a corrupted index: getTxReceipt reporting positions the chain no longer has, unreadable blocks being silently dropped from getBlocks/getCheckpoints responses (consumers see a checkpoint with missing blocks and no error), and getL2ToL1MembershipWitness resolving the wrong tx and throwing The L2ToL1Message you are trying to prove inclusion of does not exist for a message that is on-chain and proven.

Fix

removeBlocksAfter now cleans up from the raw storage row, so blocks whose bodies can no longer be fully loaded still release their row, tx effects (enumerated from #blockTxs), and hash/archive indices. deleteBlock only deletes a tx-effect entry that is still owned by the block being removed (compares the entry's leading block hash), so a re-included tx's index survives the removal of its old block.

The regression test adds two proposed blocks sharing a tx effect and asserts removeBlocksAfter removes both and leaves no residue. Before this change it fails: block 1 is removed, block 2 becomes unreadable and leaks.

Verification

  • New test in block_store.test.ts (fails on the previous implementation, passes now).
  • The modified store was additionally exercised standalone against the published 5.0.0 packages (this file is identical between v5.0.0 and next): shared-tx cleanup, index integrity (hash/archive lookups, re-adding the same chain), and plain suffix-removal behavior all pass; the old implementation fails the shared-tx case.

Related observation while debugging downstream (can file separately if useful): during reorg ingestion there is a read window where getCheckpoints(n, …, { includeBlocks: true }) returns a checkpoint whose blocks are missing or empty with no warning — unreadable or not-yet-visible blocks are silently filtered out. Consumers that treat the returned payload as complete lose data; a loud error or a retryable signal would be safer.

🤖 Generated with Claude Code

…ed elsewhere

BlockStore.removeBlocksAfter had two defects that corrupt the tx-effect
index (txHash -> block position) when the same tx exists in two stored
blocks, e.g. re-included after its original proposal expired:

- deleteBlock removed #txEffects entries blindly by txHash, destroying
  the entry of a tx whose index already points at another stored block,
  which makes that block unreadable.
- removeBlocksAfter skipped cleanup entirely for blocks it could not
  reconstruct, leaking their row, tx effects, and indices; a later
  insert at the same number then overwrites the row in place, leaving
  stale tx-effect entries pointing into the new chain.

A stale entry makes getL2ToL1MembershipWitness resolve the tx at wrong
coordinates and throw 'The L2ToL1Message you are trying to prove
inclusion of does not exist' for a message in a proven block, and lets
getTxReceipt report a proven position the chain no longer has.

Cleanup now works from the raw storage row (so unreadable blocks are
still fully released) and only deletes tx-effect entries still owned by
the block being removed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@benesjan

Copy link
Copy Markdown
Contributor Author

Closed in favor of #24765

@benesjan benesjan closed this Jul 17, 2026
@benesjan
benesjan deleted the fix/archiver-tx-effect-index branch July 17, 2026 05:21
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