Skip to content

refactor: replace proposedCheckpoint tip with atomic archiver read#24008

Open
spalladino wants to merge 1 commit into
spl/a1061-thin-checkpoint-eventsfrom
spl/a978-remove-proposed-checkpoint-tip
Open

refactor: replace proposedCheckpoint tip with atomic archiver read#24008
spalladino wants to merge 1 commit into
spl/a1061-thin-checkpoint-eventsfrom
spl/a978-remove-proposed-checkpoint-tip

Conversation

@spalladino

Copy link
Copy Markdown
Contributor

Motivation

The proposedCheckpoint tip on L2Tips was the checkpoint frontier including L1-submitted-but-unconfirmed proposals, sitting between proposed (blocks) and checkpointed (confirmed). It existed only for proposer pipelining and had outlived its usefulness as a tip:

  • It was internal-only — never carried on the public RPC surface (ChainTips already excluded it), so no schema or storage migration is involved in removing it.
  • It was degenerate everywhere except the archiver: local stores (world-state, l2-tips-store) could never represent it (no stream event carries it; the cursor was always equal to checkpointed), which is why earlier work had already narrowed LocalL2Tips to omit it.
  • It was fabricated by shims purely to satisfy the type: the PXE node adapter and the TXE archiver both invented a proposedCheckpoint: checkpointed value.
  • Most importantly, the tip and its payload were read from two independent archiver snapshots — a JS-side tips cache for the tip vs. a direct store read on #proposedCheckpoints for the payload (getProposedCheckpointData). A concurrent archiver write could be observed split, which forced explicit race-detection/refuse-to-proceed code in the sequencer's checkSync.

Approach

Drop the proposedCheckpoint tip from L2Tips and replace it with a dedicated atomic archiver API:

// L2BlockSource / archiver
getProposedCheckpoint(): Promise<{ tip: L2TipId; data: ProposedCheckpointData } | undefined>;

It performs a single read-only transaction over #proposedCheckpoints, derives the tip entirely from the payload (checkpoint number, header hash, last block = startBlock + blockCount - 1, block hash from the block store), and returns undefined when no proposed checkpoint leads the checkpointed frontier. Because tip and payload come from one snapshot, they are always coherent.

The archiver keeps tracking proposed checkpoints internally — the #proposedCheckpoints payload store, addProposedCheckpoint, and getProposedCheckpointData (now used for by-number/by-slot/by-tag lookups in the node's getCheckpoint fallback) are unchanged. This is interface slimming, not data removal.

API changes (internal)

  • L2BlockSource.getProposedCheckpoint() added (also in ArchiverApiSchema). Archiver-internal + sequencer-facing; not exposed on the node RPC surface.
  • L2BlockSource.getProposedCheckpointData(query?) retained for confirmed→proposed fallback lookups by number/slot/tag.
  • proposedCheckpoint removed from L2Tips and L2TipsSchema; 'proposedCheckpoint' removed from L2BlockTag.
  • ChainTip, ChainTips, ChainTipsSchema, and LocalL2Tips now alias their L2Block* counterparts directly (the three tip shapes converge to one).
  • ProposedCheckpoint type and exported L2TipIdSchema added in stdlib.

Public node behavior is identical: 'proposed' in getCheckpointNumber / resolveCheckpointParameter still means the proposed frontier (including unconfirmed), falling back to the checkpointed frontier when no proposal leads it — only the backing read changed.

Changes

  • stdlib: removed the tip from L2Tips/L2TipsSchema/L2BlockTag; added ProposedCheckpoint, getProposedCheckpoint to the L2BlockSource interface, and L2TipIdSchema; collapsed ChainTip(s)/LocalL2Tips to aliases; added getProposedCheckpoint to ArchiverApiSchema; updated factories.ts and interface tests.
  • archiver: implemented BlockStore.getProposedCheckpoint() (atomic) and exposed it via data_source_base; removed the proposedCheckpoint cursor from getL2TipsData (tips cache); rewrote pruneOrphanProposedBlocks to derive the proposed-checkpoint frontier block number from the payload store; cleaned the trace log and the mocks.
  • sequencer-client: checkSync now consumes the atomic getProposedCheckpoint(); deleted the snapshot-race reconciliation block (the inconsistent-proposed-checkpoint refuse-to-proceed guard) that only existed because tip and payload were read separately; hasProposedCheckpoint is now derived from the atomic result. Migrated automine_sequencer next-checkpoint-number computation.
  • aztec-node: getCheckpointNumber('proposed'), resolveCheckpointParameter('proposed'), and the simulatePublicCalls target-slot computation now use getProposedCheckpoint(); the proposed-checkpoint slot is read from the payload header (no extra block fetch in the leading case). getChainTips simplified.
  • pxe / txe: deleted the proposedCheckpoint: tips.checkpointed fabrication shims in the PXE node adapter and the TXE archiver.

Fixes A-978

…oint

Replace the source-side `proposedCheckpoint` tip on `L2Tips` with a dedicated
atomic archiver read `getProposedCheckpoint()` returning `{ tip, data }` (or
undefined when no proposal leads the checkpointed frontier).

The tip and its payload were read separately (JS-side tips cache vs. a direct
store read on `#proposedCheckpoints`), forcing snapshot-race reconciliation in
the sequencer. The single atomic read makes tip and payload coherent by
construction, so that reconciliation code is deleted. `L2Tips`, `ChainTips`,
and `LocalL2Tips` collapse to one shape, and the PXE/TXE fabrication shims are
removed.

Public node behavior is preserved: `'proposed'` in getCheckpointNumber/
resolveCheckpointParameter still means the proposed frontier (incl. unconfirmed),
only the backing read changes. `getProposedCheckpoint` is archiver-internal +
sequencer-facing; it is not exposed over node RPC.

Fixes A-978
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