@@ -14,14 +14,19 @@ import { RunningPromise, makeLoggingErrorHandler } from '@aztec/foundation/runni
1414import { DateProvider , elapsed } from '@aztec/foundation/timer' ;
1515import {
1616 type ArchiverEmitter ,
17+ type BlockData ,
1718 type BlockHash ,
1819 L2Block ,
1920 type L2BlockSink ,
2021 L2BlockSourceEvents ,
2122 type L2Tips ,
2223 type ValidateCheckpointResult ,
2324} from '@aztec/stdlib/block' ;
24- import { type ProposedCheckpointInput , PublishedCheckpoint } from '@aztec/stdlib/checkpoint' ;
25+ import {
26+ type ProposedCheckpointData ,
27+ type ProposedCheckpointInput ,
28+ PublishedCheckpoint ,
29+ } from '@aztec/stdlib/checkpoint' ;
2530import {
2631 type L1RollupConstants ,
2732 getEpochAtSlot ,
@@ -346,12 +351,12 @@ export class Archiver extends ArchiverDataSourceBase implements L2BlockSink, Tra
346351 private async sync ( ) {
347352 // Process any queued blocks first, before doing L1 sync
348353 await this . processInboundQueue ( ) ;
354+ // Now perform L1 sync
355+ await this . syncFromL1 ( ) ;
349356 // Prune orphan proposed blocks (block-only tips with no matching proposed checkpoint) on wall-clock
350357 // time. Runs after the queue is drained so freshly-arrived proposed checkpoints are seen first, and
351- // before L1 sync so it fires even when L1 has not advanced .
358+ // after L1 sync so a canonical checkpoint that already landed on L1 is promoted before local rollback .
352359 await this . pruneOrphanProposedBlocks ( ) ;
353- // Now perform L1 sync
354- await this . syncFromL1 ( ) ;
355360 }
356361
357362 /**
@@ -364,9 +369,8 @@ export class Archiver extends ArchiverDataSourceBase implements L2BlockSink, Tra
364369 * should have arrived. This runs on wall-clock time (not L1 block advancement) so it fires during
365370 * quiet L1 periods, and is the liveness counterpart to the sequencer's checkSync guard.
366371 *
367- * Only the first uncheckpointed block is inspected: if its checkpoint is backed by a proposed
368- * checkpoint, the tip is legitimate and left for promotion (or for the L1-sync prune to clear if it
369- * later goes stale); if not, every uncheckpointed block chains off the orphan and is pruned.
372+ * The uncheckpointed suffix is scanned in order. Blocks covered by proposed checkpoints are left in
373+ * place; the first block not covered by a proposed checkpoint starts the orphan suffix to prune.
370374 */
371375 private async pruneOrphanProposedBlocks ( ) : Promise < void > {
372376 const [ lastCheckpointedBlockNumber , lastProposedBlockNumber ] = await Promise . all ( [
@@ -379,39 +383,30 @@ export class Archiver extends ArchiverDataSourceBase implements L2BlockSink, Tra
379383 return ;
380384 }
381385
382- const firstUncheckpointedBlockNumber = BlockNumber ( lastCheckpointedBlockNumber + 1 ) ;
383- const firstUncheckpointedBlockData = await this . stores . blocks . getBlockData ( {
384- number : firstUncheckpointedBlockNumber ,
386+ const uncheckpointedBlockData = await this . stores . blocks . getBlocksData ( {
387+ from : BlockNumber ( lastCheckpointedBlockNumber + 1 ) ,
388+ limit : lastProposedBlockNumber - lastCheckpointedBlockNumber ,
385389 } ) ;
386- if ( firstUncheckpointedBlockData === undefined ) {
390+ if ( uncheckpointedBlockData . length === 0 ) {
387391 return ;
388392 }
389393
390- const blockCheckpointNumber = firstUncheckpointedBlockData . checkpointNumber ;
391- const blockSlot = firstUncheckpointedBlockData . header . getSlot ( ) ;
392-
393- // A proposed checkpoint covering this block's checkpoint means the tip is not an orphan.
394- const proposedCheckpoint = await this . stores . blocks . getProposedCheckpointByNumber ( blockCheckpointNumber ) ;
395- if ( proposedCheckpoint !== undefined ) {
394+ const candidate = await this . findOrphanProposedBlockPruneCandidate (
395+ uncheckpointedBlockData ,
396+ lastCheckpointedBlockNumber ,
397+ ) ;
398+ if ( candidate === undefined ) {
396399 return ;
397400 }
398401
399- // The proposed checkpoint should have landed by the start of the slot after the block's build slot
400- // (build slot = blockSlot - pipeliningOffset). Wait a grace period beyond that to tolerate propagation.
401- const pipeliningOffset = this . epochCache . pipeliningOffset ( ) ;
402- const deadlineSlot = SlotNumber ( Number ( blockSlot ) - pipeliningOffset + 1 ) ;
403- const pruneAfter =
404- getTimestampForSlot ( deadlineSlot , this . l1Constants ) + BigInt ( this . config . orphanProposedBlockPruneGraceSeconds ) ;
405- const now = BigInt ( this . dateProvider . nowInSeconds ( ) ) ;
406- if ( now < pruneAfter ) {
407- return ;
408- }
402+ const { blockCheckpointNumber, blockSlot, deadlineSlot, pruneAfter, now, pipeliningOffset, pruneAfterBlockNumber } =
403+ candidate ;
409404
410405 this . log . warn (
411- `Pruning orphan blocks after block ${ lastCheckpointedBlockNumber } : block at slot ${ blockSlot } belongs to ` +
406+ `Pruning orphan blocks after block ${ pruneAfterBlockNumber } : block at slot ${ blockSlot } belongs to ` +
412407 `checkpoint ${ blockCheckpointNumber } which has no matching proposed checkpoint` ,
413408 {
414- firstUncheckpointedBlockHeader : firstUncheckpointedBlockData . header . toInspect ( ) ,
409+ firstUncheckpointedBlockHeader : candidate . blockData . header . toInspect ( ) ,
415410 blockCheckpointNumber,
416411 blockSlot,
417412 pipeliningOffset,
@@ -421,7 +416,7 @@ export class Archiver extends ArchiverDataSourceBase implements L2BlockSink, Tra
421416 } ,
422417 ) ;
423418
424- const prunedBlocks = await this . updater . removeUncheckpointedBlocksAfter ( lastCheckpointedBlockNumber ) ;
419+ const prunedBlocks = await this . updater . removeUncheckpointedBlocksAfter ( pruneAfterBlockNumber ) ;
425420 if ( prunedBlocks . length > 0 ) {
426421 this . events . emit ( L2BlockSourceEvents . L2PruneUncheckpointed , {
427422 type : L2BlockSourceEvents . L2PruneUncheckpointed ,
@@ -431,6 +426,59 @@ export class Archiver extends ArchiverDataSourceBase implements L2BlockSink, Tra
431426 }
432427 }
433428
429+ private async findOrphanProposedBlockPruneCandidate (
430+ uncheckpointedBlockData : BlockData [ ] ,
431+ lastCheckpointedBlockNumber : BlockNumber ,
432+ ) {
433+ const proposedCheckpoints = new Map < number , ProposedCheckpointData | undefined > ( ) ;
434+ let pruneAfterBlockNumber = lastCheckpointedBlockNumber ;
435+
436+ for ( const blockData of uncheckpointedBlockData ) {
437+ const blockNumber = blockData . header . getBlockNumber ( ) ;
438+ const blockCheckpointNumber = blockData . checkpointNumber ;
439+ const checkpointKey = Number ( blockCheckpointNumber ) ;
440+ let proposedCheckpoint = proposedCheckpoints . get ( checkpointKey ) ;
441+ if ( ! proposedCheckpoints . has ( checkpointKey ) ) {
442+ proposedCheckpoint = await this . stores . blocks . getProposedCheckpointByNumber ( blockCheckpointNumber ) ;
443+ proposedCheckpoints . set ( checkpointKey , proposedCheckpoint ) ;
444+ }
445+
446+ if (
447+ proposedCheckpoint !== undefined &&
448+ blockNumber >= proposedCheckpoint . startBlock &&
449+ blockNumber < proposedCheckpoint . startBlock + proposedCheckpoint . blockCount
450+ ) {
451+ pruneAfterBlockNumber = blockNumber ;
452+ continue ;
453+ }
454+
455+ // The proposed checkpoint should have landed by the start of the slot after the block's build slot
456+ // (build slot = blockSlot - pipeliningOffset). Wait a grace period beyond that to tolerate propagation.
457+ const blockSlot = blockData . header . getSlot ( ) ;
458+ const pipeliningOffset = this . epochCache . pipeliningOffset ( ) ;
459+ const deadlineSlot = SlotNumber ( Number ( blockSlot ) - pipeliningOffset + 1 ) ;
460+ const pruneAfter =
461+ getTimestampForSlot ( deadlineSlot , this . l1Constants ) + BigInt ( this . config . orphanProposedBlockPruneGraceSeconds ) ;
462+ const now = BigInt ( this . dateProvider . nowInSeconds ( ) ) ;
463+ if ( now < pruneAfter ) {
464+ return undefined ;
465+ }
466+
467+ return {
468+ blockData,
469+ blockCheckpointNumber,
470+ blockSlot,
471+ deadlineSlot,
472+ pruneAfter,
473+ now,
474+ pipeliningOffset,
475+ pruneAfterBlockNumber,
476+ } ;
477+ }
478+
479+ return undefined ;
480+ }
481+
434482 private async syncFromL1 ( ) {
435483 // Delegate to the L1 synchronizer
436484 await this . synchronizer . syncFromL1 ( this . initialSyncComplete ) ;
0 commit comments