@@ -227,21 +227,34 @@ export class BlockStore {
227227 }
228228
229229 return await this . db . transactionAsync ( async ( ) => {
230- // Check that the checkpoint immediately before the first block to be added is present in the store.
231230 const firstCheckpointNumber = checkpoints [ 0 ] . checkpoint . number ;
232231 const previousCheckpointNumber = await this . getLatestCheckpointNumber ( ) ;
233232
234- if ( previousCheckpointNumber !== firstCheckpointNumber - 1 && ! opts . force ) {
233+ // Handle already-stored checkpoints at the start of the batch.
234+ // This can happen after an L1 reorg re-includes a checkpoint in a different L1 block.
235+ // We accept them if archives match (same content) and update their L1 metadata.
236+ if ( ! opts . force && firstCheckpointNumber <= previousCheckpointNumber ) {
237+ checkpoints = await this . skipOrUpdateAlreadyStoredCheckpoints ( checkpoints , previousCheckpointNumber ) ;
238+ if ( checkpoints . length === 0 ) {
239+ return true ;
240+ }
241+ // Re-check sequentiality after skipping
242+ const newFirstNumber = checkpoints [ 0 ] . checkpoint . number ;
243+ if ( previousCheckpointNumber !== newFirstNumber - 1 ) {
244+ throw new InitialCheckpointNumberNotSequentialError ( newFirstNumber , previousCheckpointNumber ) ;
245+ }
246+ } else if ( previousCheckpointNumber !== firstCheckpointNumber - 1 && ! opts . force ) {
235247 throw new InitialCheckpointNumberNotSequentialError ( firstCheckpointNumber , previousCheckpointNumber ) ;
236248 }
237249
238250 // Extract the previous checkpoint if there is one
251+ const currentFirstCheckpointNumber = checkpoints [ 0 ] . checkpoint . number ;
239252 let previousCheckpointData : CheckpointData | undefined = undefined ;
240- if ( previousCheckpointNumber !== INITIAL_CHECKPOINT_NUMBER - 1 ) {
253+ if ( currentFirstCheckpointNumber - 1 !== INITIAL_CHECKPOINT_NUMBER - 1 ) {
241254 // There should be a previous checkpoint
242- previousCheckpointData = await this . getCheckpointData ( previousCheckpointNumber ) ;
255+ previousCheckpointData = await this . getCheckpointData ( CheckpointNumber ( currentFirstCheckpointNumber - 1 ) ) ;
243256 if ( previousCheckpointData === undefined ) {
244- throw new CheckpointNotFoundError ( previousCheckpointNumber ) ;
257+ throw new CheckpointNotFoundError ( CheckpointNumber ( currentFirstCheckpointNumber - 1 ) ) ;
245258 }
246259 }
247260
@@ -331,6 +344,50 @@ export class BlockStore {
331344 } ) ;
332345 }
333346
347+ /**
348+ * Handles checkpoints at the start of a batch that are already stored (e.g. due to L1 reorg).
349+ * Verifies the archive root matches, updates L1 metadata, and returns only the new checkpoints.
350+ */
351+ private async skipOrUpdateAlreadyStoredCheckpoints (
352+ checkpoints : PublishedCheckpoint [ ] ,
353+ latestStored : CheckpointNumber ,
354+ ) : Promise < PublishedCheckpoint [ ] > {
355+ let i = 0 ;
356+ for ( ; i < checkpoints . length && checkpoints [ i ] . checkpoint . number <= latestStored ; i ++ ) {
357+ const incoming = checkpoints [ i ] ;
358+ const stored = await this . getCheckpointData ( incoming . checkpoint . number ) ;
359+ if ( ! stored ) {
360+ // Should not happen if latestStored is correct, but be safe
361+ break ;
362+ }
363+ // Verify the checkpoint content matches (archive root)
364+ if ( ! stored . archive . root . equals ( incoming . checkpoint . archive . root ) ) {
365+ throw new Error (
366+ `Checkpoint ${ incoming . checkpoint . number } already exists in store but with a different archive root. ` +
367+ `Stored: ${ stored . archive . root } , incoming: ${ incoming . checkpoint . archive . root } ` ,
368+ ) ;
369+ }
370+ // Update L1 metadata and attestations for the already-stored checkpoint
371+ this . #log. warn (
372+ `Checkpoint ${ incoming . checkpoint . number } already stored, updating L1 info ` +
373+ `(L1 block ${ stored . l1 . blockNumber } -> ${ incoming . l1 . blockNumber } )` ,
374+ ) ;
375+ await this . #checkpoints. set ( incoming . checkpoint . number , {
376+ header : incoming . checkpoint . header . toBuffer ( ) ,
377+ archive : incoming . checkpoint . archive . toBuffer ( ) ,
378+ checkpointOutHash : incoming . checkpoint . getCheckpointOutHash ( ) . toBuffer ( ) ,
379+ l1 : incoming . l1 . toBuffer ( ) ,
380+ attestations : incoming . attestations . map ( a => a . toBuffer ( ) ) ,
381+ checkpointNumber : incoming . checkpoint . number ,
382+ startBlock : incoming . checkpoint . blocks [ 0 ] . number ,
383+ blockCount : incoming . checkpoint . blocks . length ,
384+ } ) ;
385+ // Update the sync point to reflect the new L1 block
386+ await this . #lastSynchedL1Block. set ( incoming . l1 . blockNumber ) ;
387+ }
388+ return checkpoints . slice ( i ) ;
389+ }
390+
334391 private async addBlockToDatabase ( block : L2Block , checkpointNumber : number , indexWithinCheckpoint : number ) {
335392 const blockHash = await block . hash ( ) ;
336393
0 commit comments