Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -755,6 +755,60 @@ describe('L2BlockStream', () => {
]);
});

describe('startingBlock with stale checkpoint state', () => {
// When a node restarts with startingBlock set and has local blocks but no checkpoint
// state (e.g. checkpoint tracking is new, or checkpoint state was reset), Loop 1
// should not spam checkpoint events for all historical checkpoints.

it('skips historical checkpoint events before startingBlock on restart with stale checkpoint state', async () => {
// node has blocks 1-15 locally (proposed=15) but no checkpoint state.
// Checkpoint 5 covers blocks 13-15 (the last checkpoint).
setRemoteTipsMultiBlock(15, 15);
localData.proposed.number = BlockNumber(15);
// localData.checkpointed starts at 0 - simulating stale/missing checkpoint state

blockStream = new TestL2BlockStream(blockSource, localData, handler, undefined, {
batchSize: 10,
startingBlock: 13, // start from checkpoint 5 (blocks 13-15)
});

await blockStream.work();

// Should only emit checkpoint 5 (the one containing startingBlock=13), not all 5 checkpoints
expect(handler.events).toEqual([expectCheckpointed(5)]);
// Verify we don't spam checkpoints 1-4
const checkpointEvents = handler.events.filter(e => e.type === 'chain-checkpointed');
expect(checkpointEvents).toHaveLength(1);
});

it('without startingBlock emits all historical checkpoints for already-local blocks', async () => {
// Same scenario without startingBlock: should emit all 5 checkpoints (correct catch-up behavior)
setRemoteTipsMultiBlock(15, 15);
localData.proposed.number = BlockNumber(15);
// localData.checkpointed starts at 0

await blockStream.work();

// All 5 checkpoints should be emitted since they're all for already-local blocks
const checkpointEvents = handler.events.filter(e => e.type === 'chain-checkpointed');
expect(checkpointEvents).toHaveLength(5);
});

it('does not call getCheckpointedBlocks(0) when startingBlock is 0', async () => {
// getCheckpointedBlocks rejects block 0
setRemoteTipsMultiBlock(15, 15);
blockStream = new TestL2BlockStream(blockSource, localData, handler, undefined, {
batchSize: 10,
startingBlock: 0,
});

await blockStream.work();

const calls = blockSource.getCheckpointedBlocks.mock.calls;
expect(calls.every(([blockNum]) => blockNum >= 1)).toBe(true);
});
});

describe('checkpoint prefetching', () => {
it('prefetches multiple checkpoints in a single RPC call', async () => {
// Set up: 9 blocks in 3 checkpoints
Expand Down
21 changes: 21 additions & 0 deletions yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,27 @@ export class L2BlockStream {

let nextBlockNumber = latestBlockNumber + 1;
let nextCheckpointToEmit = CheckpointNumber(localTips.checkpointed.checkpoint.number + 1);

// When startingBlock is set, also skip ahead for checkpoints.
if (
this.opts.startingBlock !== undefined &&
this.opts.startingBlock >= 1 &&
nextCheckpointToEmit <= sourceTips.checkpointed.checkpoint.number
) {
const startingBlockCheckpoints = await this.l2BlockSource.getCheckpointedBlocks(
BlockNumber(this.opts.startingBlock),
1,
);
if (startingBlockCheckpoints.length > 0) {
nextCheckpointToEmit = CheckpointNumber(
Math.max(nextCheckpointToEmit, startingBlockCheckpoints[0].checkpointNumber),
);
} else {
// startingBlock is past all checkpointed blocks; skip Loop 1 entirely.
nextCheckpointToEmit = CheckpointNumber(sourceTips.checkpointed.checkpoint.number + 1);
}
}

if (this.opts.skipFinalized) {
// When skipping finalized blocks we need to provide reliable reorg detection while fetching as few blocks as
// possible. Finalized blocks cannot be reorged by definition, so we can skip most of them. We do need the very
Expand Down
Loading