@aztec/sequencer-client 0.0.1-commit.3f296a7d2 → 0.0.1-commit.42ee6df9b

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,7 +13,7 @@ import type { TypedEventEmitter } from '@aztec/foundation/types';
13
13
  import type { P2P } from '@aztec/p2p';
14
14
  import type { SlasherClientInterface } from '@aztec/slasher';
15
15
  import type { BlockData, L2BlockSink, L2BlockSource, ValidateCheckpointResult } from '@aztec/stdlib/block';
16
- import type { Checkpoint } from '@aztec/stdlib/checkpoint';
16
+ import type { Checkpoint, ProposedCheckpointData } from '@aztec/stdlib/checkpoint';
17
17
  import { getSlotStartBuildTimestamp } from '@aztec/stdlib/epoch-helpers';
18
18
  import {
19
19
  type ResolvedSequencerConfig,
@@ -72,6 +72,9 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
72
72
  /** The last epoch for which we logged strategy comparison in fisherman mode. */
73
73
  private lastEpochForStrategyComparison: EpochNumber | undefined;
74
74
 
75
+ /** The last checkpoint proposal job, tracked so we can await its pending L1 submission during shutdown. */
76
+ private lastCheckpointProposalJob: CheckpointProposalJob | undefined;
77
+
75
78
  /** The maximum number of seconds that the sequencer can be into a slot to transition to a particular state. */
76
79
  protected timetable!: SequencerTimetable;
77
80
 
@@ -149,6 +152,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
149
152
  this.setState(SequencerState.STOPPING, undefined, { force: true });
150
153
  await this.publisherFactory.stopAll();
151
154
  await this.runningPromise?.stop();
155
+ await this.lastCheckpointProposalJob?.awaitPendingSubmission();
152
156
  this.setState(SequencerState.STOPPED, undefined, { force: true });
153
157
  this.log.info('Stopped sequencer');
154
158
  }
@@ -208,6 +212,9 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
208
212
  return;
209
213
  }
210
214
 
215
+ // Track the job so we can await its pending L1 submission during shutdown
216
+ this.lastCheckpointProposalJob = checkpointProposalJob;
217
+
211
218
  // Execute the checkpoint proposal job
212
219
  const checkpoint = await checkpointProposalJob.execute();
213
220
 
@@ -234,7 +241,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
234
241
  * @returns CheckpointProposalJob if successful, undefined if we are not yet synced or are not the proposer.
235
242
  */
236
243
  @trackSpan('Sequencer.prepareCheckpointProposal')
237
- private async prepareCheckpointProposal(
244
+ protected async prepareCheckpointProposal(
238
245
  slot: SlotNumber,
239
246
  targetSlot: SlotNumber,
240
247
  epoch: EpochNumber,
@@ -292,6 +299,16 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
292
299
  // Next checkpoint follows from the last synced one
293
300
  const checkpointNumber = CheckpointNumber(syncedTo.checkpointNumber + 1);
294
301
 
302
+ // Guard: don't exceed 1-deep pipeline. Without a proposed checkpoint, we can only build
303
+ // confirmed + 1. With a proposed checkpoint, we can build confirmed + 2.
304
+ const confirmedCkpt = syncedTo.checkpointedCheckpointNumber;
305
+ if (checkpointNumber > confirmedCkpt + 2) {
306
+ this.log.warn(
307
+ `Skipping slot ${targetSlot}: checkpoint ${checkpointNumber} exceeds max pipeline depth (confirmed=${confirmedCkpt})`,
308
+ );
309
+ return undefined;
310
+ }
311
+
295
312
  const logCtx = {
296
313
  nowSeconds,
297
314
  syncedToL2Slot: syncedTo.syncedL2Slot,
@@ -337,13 +354,41 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
337
354
  }
338
355
 
339
356
  // Prepare invalidation request if the pending chain is invalid (returns undefined if no need)
340
- const invalidateCheckpoint = await publisher.simulateInvalidateCheckpoint(syncedTo.pendingChainValidationStatus);
357
+ let invalidateCheckpoint = await publisher.simulateInvalidateCheckpoint(syncedTo.pendingChainValidationStatus);
358
+
359
+ // Determine the correct archive and L1 state overrides for the canProposeAt check.
360
+ // The L1 contract reads archives[proposedCheckpointNumber] and compares it with the provided archive.
361
+ // When invalidating or pipelining, the local archive may differ from L1's, so we adjust accordingly.
362
+ let archiveForCheck = syncedTo.archive;
363
+ const l1Overrides: {
364
+ forcePendingCheckpointNumber?: CheckpointNumber;
365
+ forceArchive?: { checkpointNumber: CheckpointNumber; archive: Fr };
366
+ } = {};
367
+
368
+ if (this.epochCache.isProposerPipeliningEnabled() && syncedTo.hasProposedCheckpoint) {
369
+ // Parent checkpoint hasn't landed on L1 yet. Override both the proposed checkpoint number
370
+ // and the archive at that checkpoint so L1 simulation sees the correct chain tip.
371
+ const parentCheckpointNumber = CheckpointNumber(checkpointNumber - 1);
372
+ l1Overrides.forcePendingCheckpointNumber = parentCheckpointNumber;
373
+ l1Overrides.forceArchive = { checkpointNumber: parentCheckpointNumber, archive: syncedTo.archive };
374
+ this.metrics.recordPipelineDepth(1);
375
+
376
+ this.log.verbose(
377
+ `Building on top of proposed checkpoint (pending=${syncedTo.proposedCheckpointData?.checkpointNumber})`,
378
+ );
379
+ // Clear the invalidation - the proposed checkpoint should handle it.
380
+ invalidateCheckpoint = undefined;
381
+ } else if (invalidateCheckpoint) {
382
+ // After invalidation, L1 will roll back to checkpoint N-1. The archive at N-1 already
383
+ // exists on L1, so we just pass the matching archive (the lastArchive of the invalid checkpoint).
384
+ archiveForCheck = invalidateCheckpoint.lastArchive;
385
+ l1Overrides.forcePendingCheckpointNumber = invalidateCheckpoint.forcePendingCheckpointNumber;
386
+ this.metrics.recordPipelineDepth(0);
387
+ } else {
388
+ this.metrics.recordPipelineDepth(0);
389
+ }
341
390
 
342
- // Check with the rollup contract if we can indeed propose at the target slot. This check should not fail
343
- // if all the previous checks are good, but we do it just in case.
344
- const canProposeCheck = await publisher.canProposeAt(syncedTo.archive, proposer ?? EthAddress.ZERO, {
345
- ...invalidateCheckpoint,
346
- });
391
+ const canProposeCheck = await publisher.canProposeAt(archiveForCheck, proposer ?? EthAddress.ZERO, l1Overrides);
347
392
 
348
393
  if (canProposeCheck === undefined) {
349
394
  this.log.warn(
@@ -391,7 +436,6 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
391
436
  return this.createCheckpointProposalJob(
392
437
  slot,
393
438
  targetSlot,
394
- epoch,
395
439
  targetEpoch,
396
440
  checkpointNumber,
397
441
  syncedTo.blockNumber,
@@ -399,13 +443,13 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
399
443
  publisher,
400
444
  attestorAddress,
401
445
  invalidateCheckpoint,
446
+ syncedTo.proposedCheckpointData,
402
447
  );
403
448
  }
404
449
 
405
450
  protected createCheckpointProposalJob(
406
451
  slot: SlotNumber,
407
452
  targetSlot: SlotNumber,
408
- epoch: EpochNumber,
409
453
  targetEpoch: EpochNumber,
410
454
  checkpointNumber: CheckpointNumber,
411
455
  syncedToBlockNumber: BlockNumber,
@@ -413,11 +457,11 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
413
457
  publisher: SequencerPublisher,
414
458
  attestorAddress: EthAddress,
415
459
  invalidateCheckpoint: InvalidateCheckpointRequest | undefined,
460
+ proposedCheckpointData?: ProposedCheckpointData,
416
461
  ): CheckpointProposalJob {
417
462
  return new CheckpointProposalJob(
418
463
  slot,
419
464
  targetSlot,
420
- epoch,
421
465
  targetEpoch,
422
466
  checkpointNumber,
423
467
  syncedToBlockNumber,
@@ -444,6 +488,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
444
488
  this.setState.bind(this),
445
489
  this.tracer,
446
490
  this.log.getBindings(),
491
+ proposedCheckpointData,
447
492
  );
448
493
  }
449
494
 
@@ -518,25 +563,37 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
518
563
  number: syncSummary.latestBlockNumber,
519
564
  hash: syncSummary.latestBlockHash,
520
565
  })),
521
- this.l2BlockSource.getL2Tips().then(t => t.proposed),
566
+ this.l2BlockSource
567
+ .getL2Tips()
568
+ .then(t => ({ proposed: t.proposed, checkpointed: t.checkpointed, proposedCheckpoint: t.proposedCheckpoint })),
522
569
  this.p2pClient.getStatus().then(p2p => p2p.syncedToL2Block),
523
570
  this.l1ToL2MessageSource.getL2Tips().then(t => t.proposed),
524
571
  this.l2BlockSource.getPendingChainValidationStatus(),
572
+ this.l2BlockSource.getProposedCheckpointOnly(),
525
573
  ] as const);
526
574
 
527
- const [worldState, l2BlockSource, p2p, l1ToL2MessageSource, pendingChainValidationStatus] = syncedBlocks;
575
+ const [worldState, l2Tips, p2p, l1ToL2MessageSource, pendingChainValidationStatus, proposedCheckpointData] =
576
+ syncedBlocks;
528
577
 
529
578
  // Handle zero as a special case, since the block hash won't match across services if we're changing the prefilled data for the genesis block,
530
579
  // as the world state can compute the new genesis block hash, but other components use the hardcoded constant.
531
580
  // TODO(palla/mbps): Fix the above. All components should be able to handle dynamic genesis block hashes.
532
581
  const result =
533
- (l2BlockSource.number === 0 && worldState.number === 0 && p2p.number === 0 && l1ToL2MessageSource.number === 0) ||
534
- (worldState.hash === l2BlockSource.hash &&
535
- p2p.hash === l2BlockSource.hash &&
536
- l1ToL2MessageSource.hash === l2BlockSource.hash);
582
+ (l2Tips.proposed.number === 0 &&
583
+ worldState.number === 0 &&
584
+ p2p.number === 0 &&
585
+ l1ToL2MessageSource.number === 0) ||
586
+ (worldState.hash === l2Tips.proposed.hash &&
587
+ p2p.hash === l2Tips.proposed.hash &&
588
+ l1ToL2MessageSource.hash === l2Tips.proposed.hash);
537
589
 
538
590
  if (!result) {
539
- this.log.debug(`Sequencer sync check failed`, { worldState, l2BlockSource, p2p, l1ToL2MessageSource });
591
+ this.log.debug(`Sequencer sync check failed`, {
592
+ worldState,
593
+ l2BlockSource: l2Tips.proposed,
594
+ p2p,
595
+ l1ToL2MessageSource,
596
+ });
540
597
  return undefined;
541
598
  }
542
599
 
@@ -546,8 +603,10 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
546
603
  const archive = new Fr((await this.worldState.getCommitted().getTreeInfo(MerkleTreeId.ARCHIVE)).root);
547
604
  return {
548
605
  checkpointNumber: CheckpointNumber.ZERO,
606
+ checkpointedCheckpointNumber: CheckpointNumber.ZERO,
549
607
  blockNumber: BlockNumber.ZERO,
550
608
  archive,
609
+ hasProposedCheckpoint: false,
551
610
  syncedL2Slot,
552
611
  pendingChainValidationStatus,
553
612
  };
@@ -560,11 +619,16 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
560
619
  return undefined;
561
620
  }
562
621
 
622
+ const hasProposedCheckpoint = l2Tips.proposedCheckpoint.checkpoint.number > l2Tips.checkpointed.checkpoint.number;
623
+
563
624
  return {
564
625
  blockData,
565
626
  blockNumber: blockData.header.getBlockNumber(),
566
627
  checkpointNumber: blockData.checkpointNumber,
628
+ checkpointedCheckpointNumber: l2Tips.checkpointed.checkpoint.number,
567
629
  archive: blockData.archive.root,
630
+ hasProposedCheckpoint,
631
+ proposedCheckpointData,
568
632
  syncedL2Slot,
569
633
  pendingChainValidationStatus,
570
634
  };
@@ -612,7 +676,10 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
612
676
  return [false, proposer];
613
677
  }
614
678
 
615
- this.log.debug(`We are the proposer for target slot ${targetSlot}`, { targetSlot, proposer });
679
+ this.log.info(`We are the proposer for pipeline slot ${targetSlot}`, {
680
+ targetSlot,
681
+ proposer,
682
+ });
616
683
  return [true, proposer];
617
684
  }
618
685
 
@@ -909,8 +976,11 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
909
976
  type SequencerSyncCheckResult = {
910
977
  blockData?: BlockData;
911
978
  checkpointNumber: CheckpointNumber;
979
+ checkpointedCheckpointNumber: CheckpointNumber;
912
980
  blockNumber: BlockNumber;
913
981
  archive: Fr;
982
+ hasProposedCheckpoint: boolean;
983
+ proposedCheckpointData?: ProposedCheckpointData;
914
984
  syncedL2Slot: SlotNumber;
915
985
  pendingChainValidationStatus: ValidateCheckpointResult;
916
986
  };