@@ -69,6 +69,12 @@ type CheckpointStorage = {
6969 attestations : Buffer [ ] ;
7070} ;
7171
72+ /** Quorum-attested checkpoint awaiting L1 confirmation. Stored as a single value for atomic reads/writes. */
73+ export type PendingCheckpointStorage = {
74+ checkpointNumber : CheckpointNumber ;
75+ archive : Fr ;
76+ } ;
77+
7278export type RemoveCheckpointsResult = { blocksRemoved : L2Block [ ] | undefined } ;
7379
7480/**
@@ -111,6 +117,8 @@ export class BlockStore {
111117 /** Index mapping block archive to block number */
112118 #blockArchiveIndex: AztecAsyncMap < string , number > ;
113119
120+ #pendingCheckpoint: AztecAsyncSingleton < PendingCheckpointStorage > ;
121+
114122 #log = createLogger ( 'archiver:block_store' ) ;
115123
116124 constructor ( private db : AztecAsyncKVStore ) {
@@ -126,6 +134,7 @@ export class BlockStore {
126134 this . #pendingChainValidationStatus = db . openSingleton ( 'archiver_pending_chain_validation_status' ) ;
127135 this . #checkpoints = db . openMap ( 'archiver_checkpoints' ) ;
128136 this . #slotToCheckpoint = db . openMap ( 'archiver_slot_to_checkpoint' ) ;
137+ this . #pendingCheckpoint = db . openSingleton ( 'pending_checkpoint' ) ;
129138 }
130139
131140 /**
@@ -161,6 +170,7 @@ export class BlockStore {
161170
162171 // Extract the latest block and checkpoint numbers
163172 const previousBlockNumber = await this . getLatestBlockNumber ( ) ;
173+ const pendingCheckpointNumber = await this . getPendingCheckpointNumber ( ) ;
164174 const previousCheckpointNumber = await this . getLatestCheckpointNumber ( ) ;
165175
166176 // Verify we're not overwriting checkpointed blocks
@@ -179,9 +189,19 @@ export class BlockStore {
179189 throw new BlockNumberNotSequentialError ( blockNumber , previousBlockNumber ) ;
180190 }
181191
182- // The same check as above but for checkpoints
183- if ( ! opts . force && previousCheckpointNumber !== blockCheckpointNumber - 1 ) {
184- throw new CheckpointNumberNotSequentialError ( blockCheckpointNumber , previousCheckpointNumber ) ;
192+ // The same check as above but for checkpoints. Accept the block if either the confirmed
193+ // checkpoint or the pending (locally validated but not yet confirmed) checkpoint matches.
194+ const expectedCheckpointNumber = blockCheckpointNumber - 1 ;
195+ if (
196+ ! opts . force &&
197+ previousCheckpointNumber !== expectedCheckpointNumber &&
198+ pendingCheckpointNumber !== expectedCheckpointNumber
199+ ) {
200+ const [ reported , source ] : [ CheckpointNumber , 'confirmed' | 'pending' ] =
201+ pendingCheckpointNumber > previousCheckpointNumber
202+ ? [ pendingCheckpointNumber , 'pending' ]
203+ : [ previousCheckpointNumber , 'confirmed' ] ;
204+ throw new CheckpointNumberNotSequentialError ( blockCheckpointNumber , reported , source ) ;
185205 }
186206
187207 // Extract the previous block if there is one and see if it is for the same checkpoint or not
@@ -326,6 +346,13 @@ export class BlockStore {
326346 await this . #slotToCheckpoint. set ( checkpoint . checkpoint . header . slotNumber , checkpoint . checkpoint . number ) ;
327347 }
328348
349+ // Clear the pending checkpoint if any of the confirmed checkpoints match or supersede it
350+ const pendingCheckpointNumber = await this . getPendingCheckpointNumber ( ) ;
351+ const lastConfirmedCheckpointNumber = checkpoints [ checkpoints . length - 1 ] . checkpoint . number ;
352+ if ( pendingCheckpointNumber <= lastConfirmedCheckpointNumber ) {
353+ await this . #pendingCheckpoint. delete ( ) ;
354+ }
355+
329356 await this . #lastSynchedL1Block. set ( checkpoints [ checkpoints . length - 1 ] . l1 . blockNumber ) ;
330357 return true ;
331358 } ) ;
@@ -423,6 +450,12 @@ export class BlockStore {
423450 this . #log. debug ( `Removed checkpoint ${ c } ` ) ;
424451 }
425452
453+ // Clear any pending checkpoint that was removed
454+ const pendingCheckpointNumber = await this . getPendingCheckpointNumber ( ) ;
455+ if ( pendingCheckpointNumber > checkpointNumber ) {
456+ await this . #pendingCheckpoint. delete ( ) ;
457+ }
458+
426459 return { blocksRemoved } ;
427460 } ) ;
428461 }
@@ -576,6 +609,15 @@ export class BlockStore {
576609 return CheckpointNumber ( latestCheckpointNumber ) ;
577610 }
578611
612+ async getPendingCheckpoint ( ) : Promise < PendingCheckpointStorage | undefined > {
613+ return await this . #pendingCheckpoint. getAsync ( ) ;
614+ }
615+
616+ async getPendingCheckpointNumber ( ) : Promise < CheckpointNumber > {
617+ const pending = await this . getPendingCheckpoint ( ) ;
618+ return pending ?. checkpointNumber ?? CheckpointNumber ( INITIAL_CHECKPOINT_NUMBER - 1 ) ;
619+ }
620+
579621 async getCheckpointedBlock ( number : BlockNumber ) : Promise < CheckpointedL2Block | undefined > {
580622 const blockStorage = await this . #blocks. getAsync ( number ) ;
581623 if ( ! blockStorage ) {
@@ -950,6 +992,16 @@ export class BlockStore {
950992 return this . #lastSynchedL1Block. set ( l1BlockNumber ) ;
951993 }
952994
995+ /** Sets the pending checkpoint (quorum-attested but not yet L1-confirmed). Only updates if the new value is greater than the current one. */
996+ async setPendingCheckpoint ( pending : PendingCheckpointStorage ) {
997+ const current = await this . getPendingCheckpointNumber ( ) ;
998+ if ( pending . checkpointNumber <= current ) {
999+ this . #log. warn ( `Ignoring stale pending checkpoint number ${ pending . checkpointNumber } (current: ${ current } )` ) ;
1000+ return ;
1001+ }
1002+ await this . #pendingCheckpoint. set ( pending ) ;
1003+ }
1004+
9531005 async getProvenCheckpointNumber ( ) : Promise < CheckpointNumber > {
9541006 const [ latestCheckpointNumber , provenCheckpointNumber ] = await Promise . all ( [
9551007 this . getLatestCheckpointNumber ( ) ,
0 commit comments