@aztec/sequencer-client 0.0.1-commit.ec5f612 → 0.0.1-commit.ec7ac5448

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 (63) hide show
  1. package/dest/client/sequencer-client.d.ts +6 -1
  2. package/dest/client/sequencer-client.d.ts.map +1 -1
  3. package/dest/client/sequencer-client.js +49 -27
  4. package/dest/config.d.ts +25 -5
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +31 -22
  7. package/dest/global_variable_builder/global_builder.d.ts +15 -8
  8. package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
  9. package/dest/global_variable_builder/global_builder.js +26 -26
  10. package/dest/global_variable_builder/index.d.ts +2 -2
  11. package/dest/global_variable_builder/index.d.ts.map +1 -1
  12. package/dest/publisher/config.d.ts +13 -1
  13. package/dest/publisher/config.d.ts.map +1 -1
  14. package/dest/publisher/config.js +17 -2
  15. package/dest/publisher/sequencer-publisher-factory.d.ts +3 -5
  16. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  17. package/dest/publisher/sequencer-publisher-factory.js +16 -3
  18. package/dest/publisher/sequencer-publisher.d.ts +55 -43
  19. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  20. package/dest/publisher/sequencer-publisher.js +180 -116
  21. package/dest/sequencer/chain_state_overrides.d.ts +25 -0
  22. package/dest/sequencer/chain_state_overrides.d.ts.map +1 -0
  23. package/dest/sequencer/chain_state_overrides.js +39 -0
  24. package/dest/sequencer/checkpoint_proposal_job.d.ts +24 -9
  25. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  26. package/dest/sequencer/checkpoint_proposal_job.js +331 -211
  27. package/dest/sequencer/checkpoint_voter.d.ts +1 -2
  28. package/dest/sequencer/checkpoint_voter.d.ts.map +1 -1
  29. package/dest/sequencer/checkpoint_voter.js +2 -5
  30. package/dest/sequencer/events.d.ts +2 -1
  31. package/dest/sequencer/events.d.ts.map +1 -1
  32. package/dest/sequencer/metrics.d.ts +9 -2
  33. package/dest/sequencer/metrics.d.ts.map +1 -1
  34. package/dest/sequencer/metrics.js +23 -1
  35. package/dest/sequencer/sequencer.d.ts +28 -11
  36. package/dest/sequencer/sequencer.d.ts.map +1 -1
  37. package/dest/sequencer/sequencer.js +134 -74
  38. package/dest/sequencer/timetable.d.ts +17 -3
  39. package/dest/sequencer/timetable.d.ts.map +1 -1
  40. package/dest/sequencer/timetable.js +51 -43
  41. package/dest/sequencer/types.d.ts +2 -2
  42. package/dest/sequencer/types.d.ts.map +1 -1
  43. package/dest/test/mock_checkpoint_builder.d.ts +7 -9
  44. package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
  45. package/dest/test/mock_checkpoint_builder.js +39 -30
  46. package/package.json +27 -28
  47. package/src/client/sequencer-client.ts +61 -28
  48. package/src/config.ts +39 -24
  49. package/src/global_variable_builder/global_builder.ts +30 -27
  50. package/src/global_variable_builder/index.ts +1 -1
  51. package/src/publisher/config.ts +32 -0
  52. package/src/publisher/sequencer-publisher-factory.ts +18 -6
  53. package/src/publisher/sequencer-publisher.ts +263 -167
  54. package/src/sequencer/README.md +83 -13
  55. package/src/sequencer/chain_state_overrides.ts +87 -0
  56. package/src/sequencer/checkpoint_proposal_job.ts +429 -239
  57. package/src/sequencer/checkpoint_voter.ts +1 -12
  58. package/src/sequencer/events.ts +1 -1
  59. package/src/sequencer/metrics.ts +29 -1
  60. package/src/sequencer/sequencer.ts +194 -81
  61. package/src/sequencer/timetable.ts +64 -52
  62. package/src/sequencer/types.ts +1 -1
  63. package/src/test/mock_checkpoint_builder.ts +51 -48
@@ -1,7 +1,7 @@
1
1
  import { getKzg } from '@aztec/blob-lib';
2
2
  import { INITIAL_L2_BLOCK_NUM } from '@aztec/constants';
3
3
  import type { EpochCache } from '@aztec/epoch-cache';
4
- import { NoCommitteeError, type RollupContract } from '@aztec/ethereum/contracts';
4
+ import { NoCommitteeError, type RollupContract, SimulationOverridesBuilder } from '@aztec/ethereum/contracts';
5
5
  import { BlockNumber, CheckpointNumber, EpochNumber, SlotNumber } from '@aztec/foundation/branded-types';
6
6
  import { merge, omit, pick } from '@aztec/foundation/collection';
7
7
  import { Fr } from '@aztec/foundation/curves/bn254';
@@ -13,8 +13,8 @@ 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';
17
- import { getSlotAtTimestamp, getSlotStartBuildTimestamp } from '@aztec/stdlib/epoch-helpers';
16
+ import type { Checkpoint, ProposedCheckpointData } from '@aztec/stdlib/checkpoint';
17
+ import { getSlotStartBuildTimestamp } from '@aztec/stdlib/epoch-helpers';
18
18
  import {
19
19
  type ResolvedSequencerConfig,
20
20
  type SequencerConfig,
@@ -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
+ protected 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
 
@@ -110,16 +113,18 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
110
113
  /** Updates sequencer config by the defined values and updates the timetable */
111
114
  public updateConfig(config: Partial<SequencerConfig>) {
112
115
  const filteredConfig = pickFromSchema(config, SequencerConfigSchema);
113
- this.log.info(`Updated sequencer config`, omit(filteredConfig, 'txPublicSetupAllowList'));
116
+ this.log.info(`Updated sequencer config`, omit(filteredConfig, 'txPublicSetupAllowListExtend'));
114
117
  this.config = merge(this.config, filteredConfig);
118
+ const p2pPropagationTime = this.config.attestationPropagationTime;
115
119
  this.timetable = new SequencerTimetable(
116
120
  {
117
121
  ethereumSlotDuration: this.l1Constants.ethereumSlotDuration,
118
122
  aztecSlotDuration: this.aztecSlotDuration,
119
123
  l1PublishingTime: this.l1PublishingTime,
120
- p2pPropagationTime: this.config.attestationPropagationTime,
124
+ p2pPropagationTime,
121
125
  blockDurationMs: this.config.blockDurationMs,
122
126
  enforce: this.config.enforceTimeTable,
127
+ pipelining: this.epochCache.isProposerPipeliningEnabled(),
123
128
  },
124
129
  this.metrics,
125
130
  this.log,
@@ -143,12 +148,18 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
143
148
  this.log.info('Started sequencer');
144
149
  }
145
150
 
151
+ /** Triggers an immediate run of the sequencer, bypassing the polling interval. */
152
+ public trigger() {
153
+ return this.runningPromise?.trigger();
154
+ }
155
+
146
156
  /** Stops the sequencer from building blocks and moves to STOPPED state. */
147
157
  public async stop(): Promise<void> {
148
158
  this.log.info(`Stopping sequencer`);
149
159
  this.setState(SequencerState.STOPPING, undefined, { force: true });
150
- this.publisherFactory.interruptAll();
160
+ await this.publisherFactory.stopAll();
151
161
  await this.runningPromise?.stop();
162
+ await this.lastCheckpointProposalJob?.awaitPendingSubmission();
152
163
  this.setState(SequencerState.STOPPED, undefined, { force: true });
153
164
  this.log.info('Stopped sequencer');
154
165
  }
@@ -192,14 +203,25 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
192
203
  @trackSpan('Sequencer.work')
193
204
  protected async work() {
194
205
  this.setState(SequencerState.SYNCHRONIZING, undefined);
195
- const { slot, ts, now, epoch } = this.epochCache.getEpochAndSlotInNextL1Slot();
206
+ const { slot, ts, nowSeconds, epoch } = this.epochCache.getEpochAndSlotInNextL1Slot();
207
+ const { slot: targetSlot, epoch: targetEpoch } = this.epochCache.getTargetEpochAndSlotInNextL1Slot();
196
208
 
197
209
  // Check if we are synced and it's our slot, grab a publisher, check previous block invalidation, etc
198
- const checkpointProposalJob = await this.prepareCheckpointProposal(epoch, slot, ts, now);
210
+ const checkpointProposalJob = await this.prepareCheckpointProposal(
211
+ slot,
212
+ targetSlot,
213
+ epoch,
214
+ targetEpoch,
215
+ ts,
216
+ nowSeconds,
217
+ );
199
218
  if (!checkpointProposalJob) {
200
219
  return;
201
220
  }
202
221
 
222
+ // Track the job so we can await its pending L1 submission during shutdown
223
+ this.lastCheckpointProposalJob = checkpointProposalJob;
224
+
203
225
  // Execute the checkpoint proposal job
204
226
  const checkpoint = await checkpointProposalJob.execute();
205
227
 
@@ -208,13 +230,13 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
208
230
  this.lastCheckpointProposed = checkpoint;
209
231
  }
210
232
 
211
- // Log fee strategy comparison if on fisherman
233
+ // Log fee strategy comparison if on fisherman (uses target epoch since we mirror the proposer's perspective)
212
234
  if (
213
235
  this.config.fishermanMode &&
214
- (this.lastEpochForStrategyComparison === undefined || epoch > this.lastEpochForStrategyComparison)
236
+ (this.lastEpochForStrategyComparison === undefined || targetEpoch > this.lastEpochForStrategyComparison)
215
237
  ) {
216
- this.logStrategyComparison(epoch, checkpointProposalJob.getPublisher());
217
- this.lastEpochForStrategyComparison = epoch;
238
+ this.logStrategyComparison(targetEpoch, checkpointProposalJob.getPublisher());
239
+ this.lastEpochForStrategyComparison = targetEpoch;
218
240
  }
219
241
 
220
242
  return checkpoint;
@@ -226,44 +248,49 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
226
248
  * @returns CheckpointProposalJob if successful, undefined if we are not yet synced or are not the proposer.
227
249
  */
228
250
  @trackSpan('Sequencer.prepareCheckpointProposal')
229
- private async prepareCheckpointProposal(
230
- epoch: EpochNumber,
251
+ protected async prepareCheckpointProposal(
231
252
  slot: SlotNumber,
253
+ targetSlot: SlotNumber,
254
+ epoch: EpochNumber,
255
+ targetEpoch: EpochNumber,
232
256
  ts: bigint,
233
- now: bigint,
257
+ nowSeconds: bigint,
234
258
  ): Promise<CheckpointProposalJob | undefined> {
235
- // Check we have not already processed this slot (cheapest check)
259
+ // Check we have not already processed this target slot (cheapest check)
236
260
  // We only check this if enforce timetable is set, since we want to keep processing the same slot if we are not
237
261
  // running against actual time (eg when we use sandbox-style automining)
238
262
  if (
239
263
  this.lastSlotForCheckpointProposalJob &&
240
- this.lastSlotForCheckpointProposalJob >= slot &&
264
+ this.lastSlotForCheckpointProposalJob >= targetSlot &&
241
265
  this.config.enforceTimeTable
242
266
  ) {
243
- this.log.trace(`Slot ${slot} has already been processed`);
267
+ this.log.trace(`Target slot ${targetSlot} has already been processed`);
244
268
  return undefined;
245
269
  }
246
270
 
247
- // But if we have already proposed for this slot, the we definitely have to skip it, automining or not
248
- if (this.lastCheckpointProposed && this.lastCheckpointProposed.header.slotNumber >= slot) {
249
- this.log.trace(`Slot ${slot} has already been published as checkpoint ${this.lastCheckpointProposed.number}`);
271
+ // But if we have already proposed for this slot, then we definitely have to skip it, automining or not
272
+ if (this.lastCheckpointProposed && this.lastCheckpointProposed.header.slotNumber >= targetSlot) {
273
+ this.log.trace(
274
+ `Slot ${targetSlot} has already been published as checkpoint ${this.lastCheckpointProposed.number}`,
275
+ );
250
276
  return undefined;
251
277
  }
252
278
 
253
279
  // Check all components are synced to latest as seen by the archiver (queries all subsystems)
254
280
  const syncedTo = await this.checkSync({ ts, slot });
255
281
  if (!syncedTo) {
256
- await this.tryVoteWhenSyncFails({ slot, ts });
282
+ await this.tryVoteWhenSyncFails({ slot, targetSlot, ts });
257
283
  return undefined;
258
284
  }
259
285
 
260
- // If escape hatch is open for this epoch, do not start checkpoint proposal work and do not attempt invalidations.
286
+ // If escape hatch is open for the target epoch, do not start checkpoint proposal work and do not attempt invalidations.
261
287
  // Still perform governance/slashing voting (as proposer) once per slot.
262
- const isEscapeHatchOpen = await this.epochCache.isEscapeHatchOpen(epoch);
288
+ // When pipelining, we check the target epoch (slot+1's epoch) since that's the epoch we're building for.
289
+ const isEscapeHatchOpen = await this.epochCache.isEscapeHatchOpen(targetEpoch);
263
290
 
264
291
  if (isEscapeHatchOpen) {
265
292
  this.setState(SequencerState.PROPOSER_CHECK, slot);
266
- const [canPropose, proposer] = await this.checkCanPropose(slot);
293
+ const [canPropose, proposer] = await this.checkCanPropose(targetSlot);
267
294
  if (canPropose) {
268
295
  await this.tryVoteWhenEscapeHatchOpen({ slot, proposer });
269
296
  } else {
@@ -280,18 +307,18 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
280
307
  const checkpointNumber = CheckpointNumber(syncedTo.checkpointNumber + 1);
281
308
 
282
309
  const logCtx = {
283
- now,
284
- syncedToL1Ts: syncedTo.l1Timestamp,
285
- syncedToL2Slot: getSlotAtTimestamp(syncedTo.l1Timestamp, this.l1Constants),
310
+ nowSeconds,
311
+ syncedToL2Slot: syncedTo.syncedL2Slot,
286
312
  slot,
313
+ targetSlot,
287
314
  slotTs: ts,
288
315
  checkpointNumber,
289
316
  isPendingChainValid: pick(syncedTo.pendingChainValidationStatus, 'valid', 'reason', 'invalidIndex'),
290
317
  };
291
318
 
292
- // Check that we are a proposer for the next slot
319
+ // Check that we are a proposer for the target slot.
293
320
  this.setState(SequencerState.PROPOSER_CHECK, slot);
294
- const [canPropose, proposer] = await this.checkCanPropose(slot);
321
+ const [canPropose, proposer] = await this.checkCanPropose(targetSlot);
295
322
 
296
323
  // If we are not a proposer check if we should invalidate an invalid checkpoint, and bail
297
324
  if (!canPropose) {
@@ -299,10 +326,20 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
299
326
  return undefined;
300
327
  }
301
328
 
302
- // Check that the slot is not taken by a block already (should never happen, since only us can propose for this slot)
303
- if (syncedTo.blockData && syncedTo.blockData.header.getSlot() >= slot) {
329
+ // Guard: don't exceed 1-deep pipeline. Without a proposed checkpoint, we can only build
330
+ // confirmed + 1. With a proposed checkpoint, we can build confirmed + 2.
331
+ const confirmedCkpt = syncedTo.checkpointedCheckpointNumber;
332
+ if (checkpointNumber > confirmedCkpt + 2) {
333
+ this.log.verbose(
334
+ `Skipping slot ${targetSlot}: checkpoint ${checkpointNumber} exceeds max pipeline depth (confirmed=${confirmedCkpt})`,
335
+ );
336
+ return undefined;
337
+ }
338
+
339
+ // Check that the target slot is not taken by a block already (should never happen, since only us can propose for this slot)
340
+ if (syncedTo.blockData && syncedTo.blockData.header.getSlot() >= targetSlot) {
304
341
  this.log.warn(
305
- `Cannot propose block at next L2 slot ${slot} since that slot was taken by block ${syncedTo.blockNumber}`,
342
+ `Cannot propose block at target slot ${targetSlot} since that slot was taken by block ${syncedTo.blockNumber}`,
306
343
  { ...logCtx, block: syncedTo.blockData.header.toInspect() },
307
344
  );
308
345
  this.metrics.recordCheckpointPrecheckFailed('slot_already_taken');
@@ -324,14 +361,41 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
324
361
  }
325
362
 
326
363
  // Prepare invalidation request if the pending chain is invalid (returns undefined if no need)
327
- const invalidateCheckpoint = await publisher.simulateInvalidateCheckpoint(syncedTo.pendingChainValidationStatus);
364
+ let invalidateCheckpoint = await publisher.simulateInvalidateCheckpoint(syncedTo.pendingChainValidationStatus);
365
+
366
+ // Determine the correct archive and L1 state overrides for the canProposeAt check.
367
+ // The L1 contract reads archives[proposedCheckpointNumber] and compares it with the provided archive.
368
+ // When invalidating or pipelining, the local archive may differ from L1's, so we adjust accordingly.
369
+ let archiveForCheck = syncedTo.archive;
370
+ const l1SimulationOverridesBuilder = new SimulationOverridesBuilder();
371
+
372
+ if (this.epochCache.isProposerPipeliningEnabled() && syncedTo.hasProposedCheckpoint) {
373
+ // Parent checkpoint hasn't landed on L1 yet. Override both the proposed checkpoint number
374
+ // and the archive at that checkpoint so L1 simulation sees the correct chain tip.
375
+ const parentCheckpointNumber = CheckpointNumber(checkpointNumber - 1);
376
+ l1SimulationOverridesBuilder.forPendingCheckpoint(parentCheckpointNumber).withPendingArchive(syncedTo.archive);
377
+ this.metrics.recordPipelineDepth(1);
378
+
379
+ this.log.verbose(
380
+ `Building on top of proposed checkpoint (pending=${syncedTo.proposedCheckpointData?.checkpointNumber})`,
381
+ );
382
+ // Clear the invalidation - the proposed checkpoint should handle it.
383
+ invalidateCheckpoint = undefined;
384
+ } else if (invalidateCheckpoint) {
385
+ // After invalidation, L1 will roll back to checkpoint N-1. The archive at N-1 already
386
+ // exists on L1, so we just pass the matching archive (the lastArchive of the invalid checkpoint).
387
+ archiveForCheck = invalidateCheckpoint.lastArchive;
388
+ l1SimulationOverridesBuilder.forPendingCheckpoint(invalidateCheckpoint.forcePendingCheckpointNumber);
389
+ this.metrics.recordPipelineDepth(0);
390
+ } else {
391
+ this.metrics.recordPipelineDepth(0);
392
+ }
328
393
 
329
- // Check with the rollup contract if we can indeed propose at the next L2 slot. This check should not fail
330
- // if all the previous checks are good, but we do it just in case.
331
- const canProposeCheck = await publisher.canProposeAtNextEthBlock(
332
- syncedTo.archive,
394
+ const simulationOverridesPlan = l1SimulationOverridesBuilder.build();
395
+ const canProposeCheck = await publisher.canProposeAt(
396
+ archiveForCheck,
333
397
  proposer ?? EthAddress.ZERO,
334
- invalidateCheckpoint,
398
+ simulationOverridesPlan,
335
399
  );
336
400
 
337
401
  if (canProposeCheck === undefined) {
@@ -344,10 +408,10 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
344
408
  return undefined;
345
409
  }
346
410
 
347
- if (canProposeCheck.slot !== slot) {
411
+ if (canProposeCheck.slot !== targetSlot) {
348
412
  this.log.warn(
349
- `Cannot propose block due to slot mismatch with rollup contract (this can be caused by a clock out of sync). Expected slot ${slot} but got ${canProposeCheck.slot}.`,
350
- { ...logCtx, rollup: canProposeCheck, expectedSlot: slot },
413
+ `Cannot propose block due to slot mismatch with rollup contract (this can be caused by a clock out of sync). Expected slot ${targetSlot} but got ${canProposeCheck.slot}.`,
414
+ { ...logCtx, rollup: canProposeCheck, expectedSlot: targetSlot },
351
415
  );
352
416
  this.emit('proposer-rollup-check-failed', { reason: 'Slot mismatch', slot });
353
417
  this.metrics.recordCheckpointPrecheckFailed('slot_mismatch');
@@ -364,36 +428,49 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
364
428
  return undefined;
365
429
  }
366
430
 
367
- this.lastSlotForCheckpointProposalJob = slot;
368
- await this.p2pClient.prepareForSlot(slot);
369
- this.log.info(`Preparing checkpoint proposal ${checkpointNumber} at slot ${slot}`, { ...logCtx, proposer });
431
+ this.lastSlotForCheckpointProposalJob = targetSlot;
432
+
433
+ await this.p2pClient.prepareForSlot(targetSlot);
434
+ this.log.info(
435
+ `Preparing checkpoint proposal ${checkpointNumber} for target slot ${targetSlot} during wall-clock slot ${slot}`,
436
+ {
437
+ ...logCtx,
438
+ proposer,
439
+ pipeliningEnabled: this.epochCache.isProposerPipeliningEnabled(),
440
+ },
441
+ );
370
442
 
371
443
  // Create and return the checkpoint proposal job
372
444
  return this.createCheckpointProposalJob(
373
- epoch,
374
445
  slot,
446
+ targetSlot,
447
+ targetEpoch,
375
448
  checkpointNumber,
376
449
  syncedTo.blockNumber,
377
450
  proposer,
378
451
  publisher,
379
452
  attestorAddress,
380
453
  invalidateCheckpoint,
454
+ syncedTo.proposedCheckpointData,
381
455
  );
382
456
  }
383
457
 
384
458
  protected createCheckpointProposalJob(
385
- epoch: EpochNumber,
386
459
  slot: SlotNumber,
460
+ targetSlot: SlotNumber,
461
+ targetEpoch: EpochNumber,
387
462
  checkpointNumber: CheckpointNumber,
388
463
  syncedToBlockNumber: BlockNumber,
389
464
  proposer: EthAddress | undefined,
390
465
  publisher: SequencerPublisher,
391
466
  attestorAddress: EthAddress,
392
467
  invalidateCheckpoint: InvalidateCheckpointRequest | undefined,
468
+ proposedCheckpointData?: ProposedCheckpointData,
393
469
  ): CheckpointProposalJob {
394
470
  return new CheckpointProposalJob(
395
- epoch,
396
471
  slot,
472
+ targetSlot,
473
+ targetEpoch,
397
474
  checkpointNumber,
398
475
  syncedToBlockNumber,
399
476
  proposer,
@@ -419,6 +496,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
419
496
  this.setState.bind(this),
420
497
  this.tracer,
421
498
  this.log.getBindings(),
499
+ proposedCheckpointData,
422
500
  );
423
501
  }
424
502
 
@@ -475,16 +553,15 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
475
553
  * We don't check against the previous block submitted since it may have been reorg'd out.
476
554
  */
477
555
  protected async checkSync(args: { ts: bigint; slot: SlotNumber }): Promise<SequencerSyncCheckResult | undefined> {
478
- // Check that the archiver and dependencies have synced to the previous L1 slot at least
479
- // TODO(#14766): Archiver reports L1 timestamp based on L1 blocks seen, which means that a missed L1 block will
480
- // cause the archiver L1 timestamp to fall behind, and cause this sequencer to start processing one L1 slot later.
481
- const l1Timestamp = await this.l2BlockSource.getL1Timestamp();
482
- const { slot, ts } = args;
483
- if (l1Timestamp === undefined || l1Timestamp + BigInt(this.l1Constants.ethereumSlotDuration) < ts) {
556
+ // Check that the archiver has fully synced the L2 slot before the one we want to propose in.
557
+ // The archiver reports sync progress via L1 block timestamps and synced checkpoint slots.
558
+ // See getSyncedL2SlotNumber for how missed L1 blocks are handled.
559
+ const syncedL2Slot = await this.l2BlockSource.getSyncedL2SlotNumber();
560
+ const { slot } = args;
561
+ if (syncedL2Slot === undefined || syncedL2Slot + 1 < slot) {
484
562
  this.log.debug(`Cannot propose block at next L2 slot ${slot} due to pending sync from L1`, {
485
563
  slot,
486
- ts,
487
- l1Timestamp,
564
+ syncedL2Slot,
488
565
  });
489
566
  return undefined;
490
567
  }
@@ -494,25 +571,43 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
494
571
  number: syncSummary.latestBlockNumber,
495
572
  hash: syncSummary.latestBlockHash,
496
573
  })),
497
- this.l2BlockSource.getL2Tips().then(t => t.proposed),
574
+ this.l2BlockSource
575
+ .getL2Tips()
576
+ .then(t => ({ proposed: t.proposed, checkpointed: t.checkpointed, proposedCheckpoint: t.proposedCheckpoint })),
498
577
  this.p2pClient.getStatus().then(p2p => p2p.syncedToL2Block),
499
- this.l1ToL2MessageSource.getL2Tips().then(t => t.proposed),
578
+ this.l1ToL2MessageSource.getL2Tips().then(t => ({ proposed: t.proposed, checkpointed: t.checkpointed })),
500
579
  this.l2BlockSource.getPendingChainValidationStatus(),
580
+ this.l2BlockSource.getProposedCheckpointOnly(),
501
581
  ] as const);
502
582
 
503
- const [worldState, l2BlockSource, p2p, l1ToL2MessageSource, pendingChainValidationStatus] = syncedBlocks;
583
+ const [worldState, l2Tips, p2p, l1ToL2MessageSourceTips, pendingChainValidationStatus, proposedCheckpointData] =
584
+ syncedBlocks;
504
585
 
505
586
  // 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,
506
587
  // as the world state can compute the new genesis block hash, but other components use the hardcoded constant.
507
588
  // TODO(palla/mbps): Fix the above. All components should be able to handle dynamic genesis block hashes.
508
589
  const result =
509
- (l2BlockSource.number === 0 && worldState.number === 0 && p2p.number === 0 && l1ToL2MessageSource.number === 0) ||
510
- (worldState.hash === l2BlockSource.hash &&
511
- p2p.hash === l2BlockSource.hash &&
512
- l1ToL2MessageSource.hash === l2BlockSource.hash);
590
+ (l2Tips.proposed.number === 0 &&
591
+ l2Tips.checkpointed.block.number === 0 &&
592
+ l2Tips.checkpointed.checkpoint.number === 0 &&
593
+ worldState.number === 0 &&
594
+ p2p.number === 0 &&
595
+ l1ToL2MessageSourceTips.proposed.number === 0 &&
596
+ l1ToL2MessageSourceTips.checkpointed.block.number === 0 &&
597
+ l1ToL2MessageSourceTips.checkpointed.checkpoint.number === 0) ||
598
+ (worldState.hash === l2Tips.proposed.hash &&
599
+ p2p.hash === l2Tips.proposed.hash &&
600
+ l1ToL2MessageSourceTips.proposed.hash === l2Tips.proposed.hash &&
601
+ l1ToL2MessageSourceTips.checkpointed.block.hash === l2Tips.checkpointed.block.hash &&
602
+ l1ToL2MessageSourceTips.checkpointed.checkpoint.hash === l2Tips.checkpointed.checkpoint.hash);
513
603
 
514
604
  if (!result) {
515
- this.log.debug(`Sequencer sync check failed`, { worldState, l2BlockSource, p2p, l1ToL2MessageSource });
605
+ this.log.debug(`Sequencer sync check failed`, {
606
+ worldState,
607
+ l2BlockSource: l2Tips.proposed,
608
+ p2p,
609
+ l1ToL2MessageSourceTips,
610
+ });
516
611
  return undefined;
517
612
  }
518
613
 
@@ -522,9 +617,11 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
522
617
  const archive = new Fr((await this.worldState.getCommitted().getTreeInfo(MerkleTreeId.ARCHIVE)).root);
523
618
  return {
524
619
  checkpointNumber: CheckpointNumber.ZERO,
620
+ checkpointedCheckpointNumber: CheckpointNumber.ZERO,
525
621
  blockNumber: BlockNumber.ZERO,
526
622
  archive,
527
- l1Timestamp,
623
+ hasProposedCheckpoint: false,
624
+ syncedL2Slot,
528
625
  pendingChainValidationStatus,
529
626
  };
530
627
  }
@@ -536,12 +633,17 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
536
633
  return undefined;
537
634
  }
538
635
 
636
+ const hasProposedCheckpoint = l2Tips.proposedCheckpoint.checkpoint.number > l2Tips.checkpointed.checkpoint.number;
637
+
539
638
  return {
540
639
  blockData,
541
640
  blockNumber: blockData.header.getBlockNumber(),
542
641
  checkpointNumber: blockData.checkpointNumber,
642
+ checkpointedCheckpointNumber: l2Tips.checkpointed.checkpoint.number,
543
643
  archive: blockData.archive.root,
544
- l1Timestamp,
644
+ hasProposedCheckpoint,
645
+ proposedCheckpointData,
646
+ syncedL2Slot,
545
647
  pendingChainValidationStatus,
546
648
  };
547
649
  }
@@ -550,20 +652,20 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
550
652
  * Checks if we are the proposer for the next slot.
551
653
  * @returns True if we can propose, and the proposer address (undefined if anyone can propose)
552
654
  */
553
- protected async checkCanPropose(slot: SlotNumber): Promise<[boolean, EthAddress | undefined]> {
655
+ protected async checkCanPropose(targetSlot: SlotNumber): Promise<[boolean, EthAddress | undefined]> {
554
656
  let proposer: EthAddress | undefined;
555
657
 
556
658
  try {
557
- proposer = await this.epochCache.getProposerAttesterAddressInSlot(slot);
659
+ proposer = await this.epochCache.getProposerAttesterAddressInSlot(targetSlot);
558
660
  } catch (e) {
559
661
  if (e instanceof NoCommitteeError) {
560
- if (this.lastSlotForNoCommitteeWarning !== slot) {
561
- this.lastSlotForNoCommitteeWarning = slot;
562
- this.log.warn(`Cannot propose at next L2 slot ${slot} since the committee does not exist on L1`);
662
+ if (this.lastSlotForNoCommitteeWarning !== targetSlot) {
663
+ this.lastSlotForNoCommitteeWarning = targetSlot;
664
+ this.log.warn(`Cannot propose at target slot ${targetSlot} since the committee does not exist on L1`);
563
665
  }
564
666
  return [false, undefined];
565
667
  }
566
- this.log.error(`Error getting proposer for slot ${slot}`, e);
668
+ this.log.error(`Error getting proposer for target slot ${targetSlot}`, e);
567
669
  return [false, undefined];
568
670
  }
569
671
 
@@ -580,10 +682,18 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
580
682
  const weAreProposer = validatorAddresses.some(addr => addr.equals(proposer));
581
683
 
582
684
  if (!weAreProposer) {
583
- this.log.debug(`Cannot propose at slot ${slot} since we are not a proposer`, { validatorAddresses, proposer });
685
+ this.log.debug(`Cannot propose at target slot ${targetSlot} since we are not a proposer`, {
686
+ targetSlot,
687
+ validatorAddresses,
688
+ proposer,
689
+ });
584
690
  return [false, proposer];
585
691
  }
586
692
 
693
+ this.log.info(`We are the proposer for pipeline slot ${targetSlot}`, {
694
+ targetSlot,
695
+ proposer,
696
+ });
587
697
  return [true, proposer];
588
698
  }
589
699
 
@@ -592,8 +702,8 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
592
702
  * This allows the sequencer to participate in governance/slashing votes even when it cannot build blocks.
593
703
  */
594
704
  @trackSpan('Seqeuencer.tryVoteWhenSyncFails', ({ slot }) => ({ [Attributes.SLOT_NUMBER]: slot }))
595
- protected async tryVoteWhenSyncFails(args: { slot: SlotNumber; ts: bigint }): Promise<void> {
596
- const { slot } = args;
705
+ protected async tryVoteWhenSyncFails(args: { slot: SlotNumber; targetSlot: SlotNumber; ts: bigint }): Promise<void> {
706
+ const { slot, targetSlot } = args;
597
707
 
598
708
  // Prevent duplicate attempts in the same slot
599
709
  if (this.lastSlotForFallbackVote === slot) {
@@ -621,7 +731,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
621
731
  });
622
732
 
623
733
  // Check if we're a proposer or proposal is open
624
- const [canPropose, proposer] = await this.checkCanPropose(slot);
734
+ const [canPropose, proposer] = await this.checkCanPropose(targetSlot);
625
735
  if (!canPropose) {
626
736
  this.log.trace(`Cannot vote in slot ${slot} since we are not a proposer`, { slot, proposer });
627
737
  return;
@@ -638,9 +748,9 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
638
748
  slot,
639
749
  });
640
750
 
641
- // Enqueue governance and slashing votes
751
+ // Enqueue governance and slashing votes (voter uses the target slot for L1 submission)
642
752
  const voter = new CheckpointVoter(
643
- slot,
753
+ targetSlot,
644
754
  publisher,
645
755
  attestorAddress,
646
756
  this.validatorClient,
@@ -720,7 +830,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
720
830
  syncedTo: SequencerSyncCheckResult,
721
831
  currentSlot: SlotNumber,
722
832
  ): Promise<void> {
723
- const { pendingChainValidationStatus, l1Timestamp } = syncedTo;
833
+ const { pendingChainValidationStatus, syncedL2Slot } = syncedTo;
724
834
  if (pendingChainValidationStatus.valid) {
725
835
  return;
726
836
  }
@@ -735,7 +845,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
735
845
 
736
846
  const logData = {
737
847
  invalidL1Timestamp: invalidCheckpointTimestamp,
738
- l1Timestamp,
848
+ syncedL2Slot,
739
849
  invalidCheckpoint: pendingChainValidationStatus.checkpoint,
740
850
  secondsBeforeInvalidatingBlockAsCommitteeMember,
741
851
  secondsBeforeInvalidatingBlockAsNonCommitteeMember,
@@ -880,8 +990,11 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
880
990
  type SequencerSyncCheckResult = {
881
991
  blockData?: BlockData;
882
992
  checkpointNumber: CheckpointNumber;
993
+ checkpointedCheckpointNumber: CheckpointNumber;
883
994
  blockNumber: BlockNumber;
884
995
  archive: Fr;
885
- l1Timestamp: bigint;
996
+ hasProposedCheckpoint: boolean;
997
+ proposedCheckpointData?: ProposedCheckpointData;
998
+ syncedL2Slot: SlotNumber;
886
999
  pendingChainValidationStatus: ValidateCheckpointResult;
887
1000
  };