@aztec/sequencer-client 0.0.1-commit.e0f15ab9b → 0.0.1-commit.e304674f1

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.
Files changed (34) hide show
  1. package/dest/client/sequencer-client.d.ts +1 -1
  2. package/dest/client/sequencer-client.d.ts.map +1 -1
  3. package/dest/client/sequencer-client.js +0 -4
  4. package/dest/global_variable_builder/global_builder.d.ts +3 -3
  5. package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
  6. package/dest/global_variable_builder/global_builder.js +7 -4
  7. package/dest/publisher/sequencer-publisher-factory.d.ts +1 -3
  8. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  9. package/dest/publisher/sequencer-publisher-factory.js +0 -1
  10. package/dest/publisher/sequencer-publisher.d.ts +52 -31
  11. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  12. package/dest/publisher/sequencer-publisher.js +106 -87
  13. package/dest/sequencer/checkpoint_proposal_job.d.ts +31 -10
  14. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  15. package/dest/sequencer/checkpoint_proposal_job.js +179 -108
  16. package/dest/sequencer/checkpoint_voter.d.ts +1 -2
  17. package/dest/sequencer/checkpoint_voter.d.ts.map +1 -1
  18. package/dest/sequencer/checkpoint_voter.js +2 -5
  19. package/dest/sequencer/sequencer.d.ts +14 -4
  20. package/dest/sequencer/sequencer.d.ts.map +1 -1
  21. package/dest/sequencer/sequencer.js +67 -18
  22. package/dest/sequencer/timetable.d.ts +4 -1
  23. package/dest/sequencer/timetable.d.ts.map +1 -1
  24. package/dest/sequencer/timetable.js +15 -5
  25. package/package.json +27 -27
  26. package/src/client/sequencer-client.ts +0 -7
  27. package/src/global_variable_builder/global_builder.ts +15 -3
  28. package/src/publisher/sequencer-publisher-factory.ts +0 -3
  29. package/src/publisher/sequencer-publisher.ts +174 -124
  30. package/src/sequencer/README.md +81 -12
  31. package/src/sequencer/checkpoint_proposal_job.ts +215 -117
  32. package/src/sequencer/checkpoint_voter.ts +1 -12
  33. package/src/sequencer/sequencer.ts +97 -20
  34. package/src/sequencer/timetable.ts +19 -8
@@ -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
 
@@ -120,6 +123,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
120
123
  p2pPropagationTime: this.config.attestationPropagationTime,
121
124
  blockDurationMs: this.config.blockDurationMs,
122
125
  enforce: this.config.enforceTimeTable,
126
+ pipelining: this.epochCache.isProposerPipeliningEnabled(),
123
127
  },
124
128
  this.metrics,
125
129
  this.log,
@@ -149,6 +153,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
149
153
  this.setState(SequencerState.STOPPING, undefined, { force: true });
150
154
  await this.publisherFactory.stopAll();
151
155
  await this.runningPromise?.stop();
156
+ await this.lastCheckpointProposalJob?.awaitPendingSubmission();
152
157
  this.setState(SequencerState.STOPPED, undefined, { force: true });
153
158
  this.log.info('Stopped sequencer');
154
159
  }
@@ -208,6 +213,9 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
208
213
  return;
209
214
  }
210
215
 
216
+ // Track the job so we can await its pending L1 submission during shutdown
217
+ this.lastCheckpointProposalJob = checkpointProposalJob;
218
+
211
219
  // Execute the checkpoint proposal job
212
220
  const checkpoint = await checkpointProposalJob.execute();
213
221
 
@@ -234,7 +242,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
234
242
  * @returns CheckpointProposalJob if successful, undefined if we are not yet synced or are not the proposer.
235
243
  */
236
244
  @trackSpan('Sequencer.prepareCheckpointProposal')
237
- private async prepareCheckpointProposal(
245
+ protected async prepareCheckpointProposal(
238
246
  slot: SlotNumber,
239
247
  targetSlot: SlotNumber,
240
248
  epoch: EpochNumber,
@@ -312,6 +320,16 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
312
320
  return undefined;
313
321
  }
314
322
 
323
+ // Guard: don't exceed 1-deep pipeline. Without a proposed checkpoint, we can only build
324
+ // confirmed + 1. With a proposed checkpoint, we can build confirmed + 2.
325
+ const confirmedCkpt = syncedTo.checkpointedCheckpointNumber;
326
+ if (checkpointNumber > confirmedCkpt + 2) {
327
+ this.log.verbose(
328
+ `Skipping slot ${targetSlot}: checkpoint ${checkpointNumber} exceeds max pipeline depth (confirmed=${confirmedCkpt})`,
329
+ );
330
+ return undefined;
331
+ }
332
+
315
333
  // Check that the target slot is not taken by a block already (should never happen, since only us can propose for this slot)
316
334
  if (syncedTo.blockData && syncedTo.blockData.header.getSlot() >= targetSlot) {
317
335
  this.log.warn(
@@ -337,13 +355,41 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
337
355
  }
338
356
 
339
357
  // Prepare invalidation request if the pending chain is invalid (returns undefined if no need)
340
- const invalidateCheckpoint = await publisher.simulateInvalidateCheckpoint(syncedTo.pendingChainValidationStatus);
358
+ let invalidateCheckpoint = await publisher.simulateInvalidateCheckpoint(syncedTo.pendingChainValidationStatus);
359
+
360
+ // Determine the correct archive and L1 state overrides for the canProposeAt check.
361
+ // The L1 contract reads archives[proposedCheckpointNumber] and compares it with the provided archive.
362
+ // When invalidating or pipelining, the local archive may differ from L1's, so we adjust accordingly.
363
+ let archiveForCheck = syncedTo.archive;
364
+ const l1Overrides: {
365
+ forcePendingCheckpointNumber?: CheckpointNumber;
366
+ forceArchive?: { checkpointNumber: CheckpointNumber; archive: Fr };
367
+ } = {};
368
+
369
+ if (this.epochCache.isProposerPipeliningEnabled() && syncedTo.hasProposedCheckpoint) {
370
+ // Parent checkpoint hasn't landed on L1 yet. Override both the proposed checkpoint number
371
+ // and the archive at that checkpoint so L1 simulation sees the correct chain tip.
372
+ const parentCheckpointNumber = CheckpointNumber(checkpointNumber - 1);
373
+ l1Overrides.forcePendingCheckpointNumber = parentCheckpointNumber;
374
+ l1Overrides.forceArchive = { checkpointNumber: parentCheckpointNumber, archive: syncedTo.archive };
375
+ this.metrics.recordPipelineDepth(1);
376
+
377
+ this.log.verbose(
378
+ `Building on top of proposed checkpoint (pending=${syncedTo.proposedCheckpointData?.checkpointNumber})`,
379
+ );
380
+ // Clear the invalidation - the proposed checkpoint should handle it.
381
+ invalidateCheckpoint = undefined;
382
+ } else if (invalidateCheckpoint) {
383
+ // After invalidation, L1 will roll back to checkpoint N-1. The archive at N-1 already
384
+ // exists on L1, so we just pass the matching archive (the lastArchive of the invalid checkpoint).
385
+ archiveForCheck = invalidateCheckpoint.lastArchive;
386
+ l1Overrides.forcePendingCheckpointNumber = invalidateCheckpoint.forcePendingCheckpointNumber;
387
+ this.metrics.recordPipelineDepth(0);
388
+ } else {
389
+ this.metrics.recordPipelineDepth(0);
390
+ }
341
391
 
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
- });
392
+ const canProposeCheck = await publisher.canProposeAt(archiveForCheck, proposer ?? EthAddress.ZERO, l1Overrides);
347
393
 
348
394
  if (canProposeCheck === undefined) {
349
395
  this.log.warn(
@@ -391,7 +437,6 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
391
437
  return this.createCheckpointProposalJob(
392
438
  slot,
393
439
  targetSlot,
394
- epoch,
395
440
  targetEpoch,
396
441
  checkpointNumber,
397
442
  syncedTo.blockNumber,
@@ -399,13 +444,13 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
399
444
  publisher,
400
445
  attestorAddress,
401
446
  invalidateCheckpoint,
447
+ syncedTo.proposedCheckpointData,
402
448
  );
403
449
  }
404
450
 
405
451
  protected createCheckpointProposalJob(
406
452
  slot: SlotNumber,
407
453
  targetSlot: SlotNumber,
408
- epoch: EpochNumber,
409
454
  targetEpoch: EpochNumber,
410
455
  checkpointNumber: CheckpointNumber,
411
456
  syncedToBlockNumber: BlockNumber,
@@ -413,11 +458,11 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
413
458
  publisher: SequencerPublisher,
414
459
  attestorAddress: EthAddress,
415
460
  invalidateCheckpoint: InvalidateCheckpointRequest | undefined,
461
+ proposedCheckpointData?: ProposedCheckpointData,
416
462
  ): CheckpointProposalJob {
417
463
  return new CheckpointProposalJob(
418
464
  slot,
419
465
  targetSlot,
420
- epoch,
421
466
  targetEpoch,
422
467
  checkpointNumber,
423
468
  syncedToBlockNumber,
@@ -444,6 +489,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
444
489
  this.setState.bind(this),
445
490
  this.tracer,
446
491
  this.log.getBindings(),
492
+ proposedCheckpointData,
447
493
  );
448
494
  }
449
495
 
@@ -518,25 +564,43 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
518
564
  number: syncSummary.latestBlockNumber,
519
565
  hash: syncSummary.latestBlockHash,
520
566
  })),
521
- this.l2BlockSource.getL2Tips().then(t => t.proposed),
567
+ this.l2BlockSource
568
+ .getL2Tips()
569
+ .then(t => ({ proposed: t.proposed, checkpointed: t.checkpointed, proposedCheckpoint: t.proposedCheckpoint })),
522
570
  this.p2pClient.getStatus().then(p2p => p2p.syncedToL2Block),
523
- this.l1ToL2MessageSource.getL2Tips().then(t => t.proposed),
571
+ this.l1ToL2MessageSource.getL2Tips().then(t => ({ proposed: t.proposed, checkpointed: t.checkpointed })),
524
572
  this.l2BlockSource.getPendingChainValidationStatus(),
573
+ this.l2BlockSource.getProposedCheckpointOnly(),
525
574
  ] as const);
526
575
 
527
- const [worldState, l2BlockSource, p2p, l1ToL2MessageSource, pendingChainValidationStatus] = syncedBlocks;
576
+ const [worldState, l2Tips, p2p, l1ToL2MessageSourceTips, pendingChainValidationStatus, proposedCheckpointData] =
577
+ syncedBlocks;
528
578
 
529
579
  // 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
580
  // as the world state can compute the new genesis block hash, but other components use the hardcoded constant.
531
581
  // TODO(palla/mbps): Fix the above. All components should be able to handle dynamic genesis block hashes.
532
582
  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);
583
+ (l2Tips.proposed.number === 0 &&
584
+ l2Tips.checkpointed.block.number === 0 &&
585
+ l2Tips.checkpointed.checkpoint.number === 0 &&
586
+ worldState.number === 0 &&
587
+ p2p.number === 0 &&
588
+ l1ToL2MessageSourceTips.proposed.number === 0 &&
589
+ l1ToL2MessageSourceTips.checkpointed.block.number === 0 &&
590
+ l1ToL2MessageSourceTips.checkpointed.checkpoint.number === 0) ||
591
+ (worldState.hash === l2Tips.proposed.hash &&
592
+ p2p.hash === l2Tips.proposed.hash &&
593
+ l1ToL2MessageSourceTips.proposed.hash === l2Tips.proposed.hash &&
594
+ l1ToL2MessageSourceTips.checkpointed.block.hash === l2Tips.checkpointed.block.hash &&
595
+ l1ToL2MessageSourceTips.checkpointed.checkpoint.hash === l2Tips.checkpointed.checkpoint.hash);
537
596
 
538
597
  if (!result) {
539
- this.log.debug(`Sequencer sync check failed`, { worldState, l2BlockSource, p2p, l1ToL2MessageSource });
598
+ this.log.debug(`Sequencer sync check failed`, {
599
+ worldState,
600
+ l2BlockSource: l2Tips.proposed,
601
+ p2p,
602
+ l1ToL2MessageSourceTips,
603
+ });
540
604
  return undefined;
541
605
  }
542
606
 
@@ -546,8 +610,10 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
546
610
  const archive = new Fr((await this.worldState.getCommitted().getTreeInfo(MerkleTreeId.ARCHIVE)).root);
547
611
  return {
548
612
  checkpointNumber: CheckpointNumber.ZERO,
613
+ checkpointedCheckpointNumber: CheckpointNumber.ZERO,
549
614
  blockNumber: BlockNumber.ZERO,
550
615
  archive,
616
+ hasProposedCheckpoint: false,
551
617
  syncedL2Slot,
552
618
  pendingChainValidationStatus,
553
619
  };
@@ -560,11 +626,16 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
560
626
  return undefined;
561
627
  }
562
628
 
629
+ const hasProposedCheckpoint = l2Tips.proposedCheckpoint.checkpoint.number > l2Tips.checkpointed.checkpoint.number;
630
+
563
631
  return {
564
632
  blockData,
565
633
  blockNumber: blockData.header.getBlockNumber(),
566
634
  checkpointNumber: blockData.checkpointNumber,
635
+ checkpointedCheckpointNumber: l2Tips.checkpointed.checkpoint.number,
567
636
  archive: blockData.archive.root,
637
+ hasProposedCheckpoint,
638
+ proposedCheckpointData,
568
639
  syncedL2Slot,
569
640
  pendingChainValidationStatus,
570
641
  };
@@ -612,7 +683,10 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
612
683
  return [false, proposer];
613
684
  }
614
685
 
615
- this.log.debug(`We are the proposer for target slot ${targetSlot}`, { targetSlot, proposer });
686
+ this.log.info(`We are the proposer for pipeline slot ${targetSlot}`, {
687
+ targetSlot,
688
+ proposer,
689
+ });
616
690
  return [true, proposer];
617
691
  }
618
692
 
@@ -909,8 +983,11 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
909
983
  type SequencerSyncCheckResult = {
910
984
  blockData?: BlockData;
911
985
  checkpointNumber: CheckpointNumber;
986
+ checkpointedCheckpointNumber: CheckpointNumber;
912
987
  blockNumber: BlockNumber;
913
988
  archive: Fr;
989
+ hasProposedCheckpoint: boolean;
990
+ proposedCheckpointData?: ProposedCheckpointData;
914
991
  syncedL2Slot: SlotNumber;
915
992
  pendingChainValidationStatus: ValidateCheckpointResult;
916
993
  };
@@ -70,6 +70,9 @@ export class SequencerTimetable {
70
70
  /** Maximum number of blocks that can be built in this slot configuration */
71
71
  public readonly maxNumberOfBlocks: number;
72
72
 
73
+ /** Whether pipelining is enabled (checkpoint finalization deferred to next slot). */
74
+ public readonly pipelining: boolean;
75
+
73
76
  constructor(
74
77
  opts: {
75
78
  ethereumSlotDuration: number;
@@ -78,6 +81,7 @@ export class SequencerTimetable {
78
81
  p2pPropagationTime?: number;
79
82
  blockDurationMs?: number;
80
83
  enforce: boolean;
84
+ pipelining?: boolean;
81
85
  },
82
86
  private readonly metrics?: SequencerMetrics,
83
87
  private readonly log?: Logger,
@@ -88,6 +92,7 @@ export class SequencerTimetable {
88
92
  this.p2pPropagationTime = opts.p2pPropagationTime ?? DEFAULT_P2P_PROPAGATION_TIME;
89
93
  this.blockDuration = opts.blockDurationMs ? opts.blockDurationMs / 1000 : undefined;
90
94
  this.enforce = opts.enforce;
95
+ this.pipelining = opts.pipelining ?? false;
91
96
 
92
97
  // Assume zero-cost propagation time and faster runs in test environments where L1 slot duration is shortened
93
98
  if (this.ethereumSlotDuration < 8) {
@@ -116,18 +121,23 @@ export class SequencerTimetable {
116
121
  if (!this.blockDuration) {
117
122
  this.maxNumberOfBlocks = 1; // Single block per slot
118
123
  } else {
119
- const timeReservedAtEnd =
120
- this.blockDuration + // Last sub-slot for validator re-execution
121
- this.checkpointFinalizationTime; // Checkpoint finalization
124
+ // When pipelining, finalization is deferred to the next slot, but we still need
125
+ // a sub-slot for validator re-execution so they can produce attestations.
126
+ let timeReservedAtEnd = this.blockDuration; // Validatior re-execution only
127
+ if (!this.pipelining) {
128
+ timeReservedAtEnd += this.checkpointFinalizationTime;
129
+ }
130
+
122
131
  const timeAvailableForBlocks = this.aztecSlotDuration - this.initializationOffset - timeReservedAtEnd;
123
132
  this.maxNumberOfBlocks = Math.floor(timeAvailableForBlocks / this.blockDuration);
124
133
  }
125
134
 
126
- // Minimum work to do within a slot for building a block with the minimum time for execution and publishing its checkpoint
127
- const minWorkToDo =
128
- this.initializationOffset +
129
- this.minExecutionTime * 2 + // Execution and reexecution
130
- this.checkpointFinalizationTime;
135
+ // Minimum work to do within a slot for building a block with the minimum time for execution and publishing its checkpoint.
136
+ // When pipelining, finalization is deferred, but we still need time for execution and validator re-execution.
137
+ let minWorkToDo = this.initializationOffset + this.minExecutionTime * 2;
138
+ if (!this.pipelining) {
139
+ minWorkToDo += this.checkpointFinalizationTime;
140
+ }
131
141
 
132
142
  const initializeDeadline = this.aztecSlotDuration - minWorkToDo;
133
143
  this.initializeDeadline = initializeDeadline;
@@ -144,6 +154,7 @@ export class SequencerTimetable {
144
154
  blockAssembleTime: this.checkpointAssembleTime,
145
155
  initializeDeadline: this.initializeDeadline,
146
156
  enforce: this.enforce,
157
+ pipelining: this.pipelining,
147
158
  minWorkToDo,
148
159
  blockDuration: this.blockDuration,
149
160
  maxNumberOfBlocks: this.maxNumberOfBlocks,