@aztec/sequencer-client 0.0.1-commit.5914bae → 0.0.1-commit.59a0419c6

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 (89) hide show
  1. package/dest/client/sequencer-client.d.ts +2 -1
  2. package/dest/client/sequencer-client.d.ts.map +1 -1
  3. package/dest/client/sequencer-client.js +8 -8
  4. package/dest/config.d.ts +3 -3
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +10 -12
  7. package/dest/global_variable_builder/fee_predictor.d.ts +37 -0
  8. package/dest/global_variable_builder/fee_predictor.d.ts.map +1 -0
  9. package/dest/global_variable_builder/fee_predictor.js +128 -0
  10. package/dest/global_variable_builder/fee_provider.d.ts +21 -0
  11. package/dest/global_variable_builder/fee_provider.d.ts.map +1 -0
  12. package/dest/global_variable_builder/fee_provider.js +58 -0
  13. package/dest/global_variable_builder/global_builder.d.ts +3 -8
  14. package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
  15. package/dest/global_variable_builder/global_builder.js +4 -39
  16. package/dest/global_variable_builder/index.d.ts +3 -1
  17. package/dest/global_variable_builder/index.d.ts.map +1 -1
  18. package/dest/global_variable_builder/index.js +2 -0
  19. package/dest/publisher/config.d.ts +17 -1
  20. package/dest/publisher/config.d.ts.map +1 -1
  21. package/dest/publisher/config.js +25 -5
  22. package/dest/publisher/index.d.ts +2 -1
  23. package/dest/publisher/index.d.ts.map +1 -1
  24. package/dest/publisher/l1_tx_failed_store/factory.d.ts +11 -0
  25. package/dest/publisher/l1_tx_failed_store/factory.d.ts.map +1 -0
  26. package/dest/publisher/l1_tx_failed_store/factory.js +22 -0
  27. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts +58 -0
  28. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -0
  29. package/dest/publisher/l1_tx_failed_store/failed_tx_store.js +1 -0
  30. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts +15 -0
  31. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts.map +1 -0
  32. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.js +34 -0
  33. package/dest/publisher/l1_tx_failed_store/index.d.ts +4 -0
  34. package/dest/publisher/l1_tx_failed_store/index.d.ts.map +1 -0
  35. package/dest/publisher/l1_tx_failed_store/index.js +2 -0
  36. package/dest/publisher/sequencer-publisher-factory.d.ts +3 -5
  37. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  38. package/dest/publisher/sequencer-publisher-factory.js +16 -3
  39. package/dest/publisher/sequencer-publisher.d.ts +52 -41
  40. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  41. package/dest/publisher/sequencer-publisher.js +345 -101
  42. package/dest/sequencer/chain_state_overrides.d.ts +25 -0
  43. package/dest/sequencer/chain_state_overrides.d.ts.map +1 -0
  44. package/dest/sequencer/chain_state_overrides.js +39 -0
  45. package/dest/sequencer/checkpoint_proposal_job.d.ts +38 -8
  46. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  47. package/dest/sequencer/checkpoint_proposal_job.js +427 -171
  48. package/dest/sequencer/checkpoint_proposal_job_metrics.d.ts +34 -0
  49. package/dest/sequencer/checkpoint_proposal_job_metrics.d.ts.map +1 -0
  50. package/dest/sequencer/checkpoint_proposal_job_metrics.js +72 -0
  51. package/dest/sequencer/events.d.ts +7 -1
  52. package/dest/sequencer/events.d.ts.map +1 -1
  53. package/dest/sequencer/metrics.d.ts +11 -10
  54. package/dest/sequencer/metrics.d.ts.map +1 -1
  55. package/dest/sequencer/metrics.js +38 -20
  56. package/dest/sequencer/sequencer.d.ts +17 -5
  57. package/dest/sequencer/sequencer.d.ts.map +1 -1
  58. package/dest/sequencer/sequencer.js +120 -58
  59. package/dest/sequencer/timetable.d.ts +14 -1
  60. package/dest/sequencer/timetable.d.ts.map +1 -1
  61. package/dest/sequencer/timetable.js +45 -36
  62. package/dest/test/mock_checkpoint_builder.d.ts +1 -1
  63. package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
  64. package/dest/test/mock_checkpoint_builder.js +0 -2
  65. package/package.json +27 -28
  66. package/src/client/sequencer-client.ts +8 -14
  67. package/src/config.ts +12 -11
  68. package/src/global_variable_builder/README.md +44 -0
  69. package/src/global_variable_builder/fee_predictor.ts +172 -0
  70. package/src/global_variable_builder/fee_provider.ts +75 -0
  71. package/src/global_variable_builder/global_builder.ts +8 -43
  72. package/src/global_variable_builder/index.ts +2 -0
  73. package/src/publisher/config.ts +47 -4
  74. package/src/publisher/index.ts +3 -0
  75. package/src/publisher/l1_tx_failed_store/factory.ts +32 -0
  76. package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +57 -0
  77. package/src/publisher/l1_tx_failed_store/file_store_failed_tx_store.ts +46 -0
  78. package/src/publisher/l1_tx_failed_store/index.ts +3 -0
  79. package/src/publisher/sequencer-publisher-factory.ts +18 -6
  80. package/src/publisher/sequencer-publisher.ts +389 -140
  81. package/src/sequencer/README.md +83 -13
  82. package/src/sequencer/chain_state_overrides.ts +87 -0
  83. package/src/sequencer/checkpoint_proposal_job.ts +517 -180
  84. package/src/sequencer/checkpoint_proposal_job_metrics.ts +128 -0
  85. package/src/sequencer/events.ts +6 -1
  86. package/src/sequencer/metrics.ts +49 -25
  87. package/src/sequencer/sequencer.ts +177 -63
  88. package/src/sequencer/timetable.ts +57 -45
  89. package/src/test/mock_checkpoint_builder.ts +0 -2
@@ -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,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,
@@ -34,6 +34,7 @@ import type { GlobalVariableBuilder } from '../global_variable_builder/global_bu
34
34
  import type { SequencerPublisherFactory } from '../publisher/sequencer-publisher-factory.js';
35
35
  import type { InvalidateCheckpointRequest, SequencerPublisher } from '../publisher/sequencer-publisher.js';
36
36
  import { CheckpointProposalJob } from './checkpoint_proposal_job.js';
37
+ import { CheckpointProposalJobMetrics } from './checkpoint_proposal_job_metrics.js';
37
38
  import { CheckpointVoter } from './checkpoint_voter.js';
38
39
  import { SequencerInterruptedError, SequencerTooSlowError } from './errors.js';
39
40
  import type { SequencerEvents } from './events.js';
@@ -56,6 +57,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
56
57
  private runningPromise?: RunningPromise;
57
58
  private state = SequencerState.STOPPED;
58
59
  private metrics: SequencerMetrics;
60
+ private checkpointProposalJobMetrics: CheckpointProposalJobMetrics;
59
61
 
60
62
  /** The last slot for which we attempted to perform our voting duties with degraded block production */
61
63
  private lastSlotForFallbackVote: SlotNumber | undefined;
@@ -72,6 +74,9 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
72
74
  /** The last epoch for which we logged strategy comparison in fisherman mode. */
73
75
  private lastEpochForStrategyComparison: EpochNumber | undefined;
74
76
 
77
+ /** The last checkpoint proposal job, tracked so we can await its pending L1 submission during shutdown. */
78
+ protected lastCheckpointProposalJob: CheckpointProposalJob | undefined;
79
+
75
80
  /** The maximum number of seconds that the sequencer can be into a slot to transition to a particular state. */
76
81
  protected timetable!: SequencerTimetable;
77
82
 
@@ -104,6 +109,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
104
109
  }
105
110
 
106
111
  this.metrics = new SequencerMetrics(telemetry, this.rollupContract, 'Sequencer');
112
+ this.checkpointProposalJobMetrics = new CheckpointProposalJobMetrics(telemetry);
107
113
  this.updateConfig(config);
108
114
  }
109
115
 
@@ -112,14 +118,16 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
112
118
  const filteredConfig = pickFromSchema(config, SequencerConfigSchema);
113
119
  this.log.info(`Updated sequencer config`, omit(filteredConfig, 'txPublicSetupAllowListExtend'));
114
120
  this.config = merge(this.config, filteredConfig);
121
+ const p2pPropagationTime = this.config.attestationPropagationTime;
115
122
  this.timetable = new SequencerTimetable(
116
123
  {
117
124
  ethereumSlotDuration: this.l1Constants.ethereumSlotDuration,
118
125
  aztecSlotDuration: this.aztecSlotDuration,
119
126
  l1PublishingTime: this.l1PublishingTime,
120
- p2pPropagationTime: this.config.attestationPropagationTime,
127
+ p2pPropagationTime,
121
128
  blockDurationMs: this.config.blockDurationMs,
122
129
  enforce: this.config.enforceTimeTable,
130
+ pipelining: this.epochCache.isProposerPipeliningEnabled(),
123
131
  },
124
132
  this.metrics,
125
133
  this.log,
@@ -152,8 +160,9 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
152
160
  public async stop(): Promise<void> {
153
161
  this.log.info(`Stopping sequencer`);
154
162
  this.setState(SequencerState.STOPPING, undefined, { force: true });
155
- this.publisherFactory.interruptAll();
163
+ await this.publisherFactory.stopAll();
156
164
  await this.runningPromise?.stop();
165
+ await this.lastCheckpointProposalJob?.awaitPendingSubmission();
157
166
  this.setState(SequencerState.STOPPED, undefined, { force: true });
158
167
  this.log.info('Stopped sequencer');
159
168
  }
@@ -197,14 +206,25 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
197
206
  @trackSpan('Sequencer.work')
198
207
  protected async work() {
199
208
  this.setState(SequencerState.SYNCHRONIZING, undefined);
200
- const { slot, ts, now, epoch } = this.epochCache.getEpochAndSlotInNextL1Slot();
209
+ const { slot, ts, nowSeconds, epoch } = this.epochCache.getEpochAndSlotInNextL1Slot();
210
+ const { slot: targetSlot, epoch: targetEpoch } = this.epochCache.getTargetEpochAndSlotInNextL1Slot();
201
211
 
202
212
  // Check if we are synced and it's our slot, grab a publisher, check previous block invalidation, etc
203
- const checkpointProposalJob = await this.prepareCheckpointProposal(epoch, slot, ts, now);
213
+ const checkpointProposalJob = await this.prepareCheckpointProposal(
214
+ slot,
215
+ targetSlot,
216
+ epoch,
217
+ targetEpoch,
218
+ ts,
219
+ nowSeconds,
220
+ );
204
221
  if (!checkpointProposalJob) {
205
222
  return;
206
223
  }
207
224
 
225
+ // Track the job so we can await its pending L1 submission during shutdown
226
+ this.lastCheckpointProposalJob = checkpointProposalJob;
227
+
208
228
  // Execute the checkpoint proposal job
209
229
  const checkpoint = await checkpointProposalJob.execute();
210
230
 
@@ -213,13 +233,13 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
213
233
  this.lastCheckpointProposed = checkpoint;
214
234
  }
215
235
 
216
- // Log fee strategy comparison if on fisherman
236
+ // Log fee strategy comparison if on fisherman (uses target epoch since we mirror the proposer's perspective)
217
237
  if (
218
238
  this.config.fishermanMode &&
219
- (this.lastEpochForStrategyComparison === undefined || epoch > this.lastEpochForStrategyComparison)
239
+ (this.lastEpochForStrategyComparison === undefined || targetEpoch > this.lastEpochForStrategyComparison)
220
240
  ) {
221
- this.logStrategyComparison(epoch, checkpointProposalJob.getPublisher());
222
- this.lastEpochForStrategyComparison = epoch;
241
+ this.logStrategyComparison(targetEpoch, checkpointProposalJob.getPublisher());
242
+ this.lastEpochForStrategyComparison = targetEpoch;
223
243
  }
224
244
 
225
245
  return checkpoint;
@@ -231,44 +251,49 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
231
251
  * @returns CheckpointProposalJob if successful, undefined if we are not yet synced or are not the proposer.
232
252
  */
233
253
  @trackSpan('Sequencer.prepareCheckpointProposal')
234
- private async prepareCheckpointProposal(
235
- epoch: EpochNumber,
254
+ protected async prepareCheckpointProposal(
236
255
  slot: SlotNumber,
256
+ targetSlot: SlotNumber,
257
+ epoch: EpochNumber,
258
+ targetEpoch: EpochNumber,
237
259
  ts: bigint,
238
- now: bigint,
260
+ nowSeconds: bigint,
239
261
  ): Promise<CheckpointProposalJob | undefined> {
240
- // Check we have not already processed this slot (cheapest check)
262
+ // Check we have not already processed this target slot (cheapest check)
241
263
  // We only check this if enforce timetable is set, since we want to keep processing the same slot if we are not
242
264
  // running against actual time (eg when we use sandbox-style automining)
243
265
  if (
244
266
  this.lastSlotForCheckpointProposalJob &&
245
- this.lastSlotForCheckpointProposalJob >= slot &&
267
+ this.lastSlotForCheckpointProposalJob >= targetSlot &&
246
268
  this.config.enforceTimeTable
247
269
  ) {
248
- this.log.trace(`Slot ${slot} has already been processed`);
270
+ this.log.trace(`Target slot ${targetSlot} has already been processed`);
249
271
  return undefined;
250
272
  }
251
273
 
252
- // But if we have already proposed for this slot, the we definitely have to skip it, automining or not
253
- if (this.lastCheckpointProposed && this.lastCheckpointProposed.header.slotNumber >= slot) {
254
- this.log.trace(`Slot ${slot} has already been published as checkpoint ${this.lastCheckpointProposed.number}`);
274
+ // But if we have already proposed for this slot, then we definitely have to skip it, automining or not
275
+ if (this.lastCheckpointProposed && this.lastCheckpointProposed.header.slotNumber >= targetSlot) {
276
+ this.log.trace(
277
+ `Slot ${targetSlot} has already been published as checkpoint ${this.lastCheckpointProposed.number}`,
278
+ );
255
279
  return undefined;
256
280
  }
257
281
 
258
282
  // Check all components are synced to latest as seen by the archiver (queries all subsystems)
259
283
  const syncedTo = await this.checkSync({ ts, slot });
260
284
  if (!syncedTo) {
261
- await this.tryVoteWhenSyncFails({ slot, ts });
285
+ await this.tryVoteWhenSyncFails({ slot, targetSlot, ts });
262
286
  return undefined;
263
287
  }
264
288
 
265
- // If escape hatch is open for this epoch, do not start checkpoint proposal work and do not attempt invalidations.
289
+ // If escape hatch is open for the target epoch, do not start checkpoint proposal work and do not attempt invalidations.
266
290
  // Still perform governance/slashing voting (as proposer) once per slot.
267
- const isEscapeHatchOpen = await this.epochCache.isEscapeHatchOpen(epoch);
291
+ // When pipelining, we check the target epoch (slot+1's epoch) since that's the epoch we're building for.
292
+ const isEscapeHatchOpen = await this.epochCache.isEscapeHatchOpen(targetEpoch);
268
293
 
269
294
  if (isEscapeHatchOpen) {
270
295
  this.setState(SequencerState.PROPOSER_CHECK, slot);
271
- const [canPropose, proposer] = await this.checkCanPropose(slot);
296
+ const [canPropose, proposer] = await this.checkCanPropose(targetSlot);
272
297
  if (canPropose) {
273
298
  await this.tryVoteWhenEscapeHatchOpen({ slot, proposer });
274
299
  } else {
@@ -285,17 +310,18 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
285
310
  const checkpointNumber = CheckpointNumber(syncedTo.checkpointNumber + 1);
286
311
 
287
312
  const logCtx = {
288
- now,
313
+ nowSeconds,
289
314
  syncedToL2Slot: syncedTo.syncedL2Slot,
290
315
  slot,
316
+ targetSlot,
291
317
  slotTs: ts,
292
318
  checkpointNumber,
293
319
  isPendingChainValid: pick(syncedTo.pendingChainValidationStatus, 'valid', 'reason', 'invalidIndex'),
294
320
  };
295
321
 
296
- // Check that we are a proposer for the next slot
322
+ // Check that we are a proposer for the target slot.
297
323
  this.setState(SequencerState.PROPOSER_CHECK, slot);
298
- const [canPropose, proposer] = await this.checkCanPropose(slot);
324
+ const [canPropose, proposer] = await this.checkCanPropose(targetSlot);
299
325
 
300
326
  // If we are not a proposer check if we should invalidate an invalid checkpoint, and bail
301
327
  if (!canPropose) {
@@ -303,10 +329,20 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
303
329
  return undefined;
304
330
  }
305
331
 
306
- // Check that the slot is not taken by a block already (should never happen, since only us can propose for this slot)
307
- if (syncedTo.blockData && syncedTo.blockData.header.getSlot() >= slot) {
332
+ // Guard: don't exceed 1-deep pipeline. Without a proposed checkpoint, we can only build
333
+ // confirmed + 1. With a proposed checkpoint, we can build confirmed + 2.
334
+ const confirmedCkpt = syncedTo.checkpointedCheckpointNumber;
335
+ if (checkpointNumber > confirmedCkpt + 2) {
336
+ this.log.verbose(
337
+ `Skipping slot ${targetSlot}: checkpoint ${checkpointNumber} exceeds max pipeline depth (confirmed=${confirmedCkpt})`,
338
+ );
339
+ return undefined;
340
+ }
341
+
342
+ // Check that the target slot is not taken by a block already (should never happen, since only us can propose for this slot)
343
+ if (syncedTo.blockData && syncedTo.blockData.header.getSlot() >= targetSlot) {
308
344
  this.log.warn(
309
- `Cannot propose block at next L2 slot ${slot} since that slot was taken by block ${syncedTo.blockNumber}`,
345
+ `Cannot propose block at target slot ${targetSlot} since that slot was taken by block ${syncedTo.blockNumber}`,
310
346
  { ...logCtx, block: syncedTo.blockData.header.toInspect() },
311
347
  );
312
348
  this.metrics.recordCheckpointPrecheckFailed('slot_already_taken');
@@ -328,14 +364,41 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
328
364
  }
329
365
 
330
366
  // Prepare invalidation request if the pending chain is invalid (returns undefined if no need)
331
- const invalidateCheckpoint = await publisher.simulateInvalidateCheckpoint(syncedTo.pendingChainValidationStatus);
367
+ let invalidateCheckpoint = await publisher.simulateInvalidateCheckpoint(syncedTo.pendingChainValidationStatus);
368
+
369
+ // Determine the correct archive and L1 state overrides for the canProposeAt check.
370
+ // The L1 contract reads archives[proposedCheckpointNumber] and compares it with the provided archive.
371
+ // When invalidating or pipelining, the local archive may differ from L1's, so we adjust accordingly.
372
+ let archiveForCheck = syncedTo.archive;
373
+ const l1SimulationOverridesBuilder = new SimulationOverridesBuilder();
374
+
375
+ if (this.epochCache.isProposerPipeliningEnabled() && syncedTo.hasProposedCheckpoint) {
376
+ // Parent checkpoint hasn't landed on L1 yet. Override both the proposed checkpoint number
377
+ // and the archive at that checkpoint so L1 simulation sees the correct chain tip.
378
+ const parentCheckpointNumber = CheckpointNumber(checkpointNumber - 1);
379
+ l1SimulationOverridesBuilder.forPendingCheckpoint(parentCheckpointNumber).withPendingArchive(syncedTo.archive);
380
+ this.metrics.recordPipelineDepth(1);
381
+
382
+ this.log.verbose(
383
+ `Building on top of proposed checkpoint (pending=${syncedTo.proposedCheckpointData?.checkpointNumber})`,
384
+ );
385
+ // Clear the invalidation - the proposed checkpoint should handle it.
386
+ invalidateCheckpoint = undefined;
387
+ } else if (invalidateCheckpoint) {
388
+ // After invalidation, L1 will roll back to checkpoint N-1. The archive at N-1 already
389
+ // exists on L1, so we just pass the matching archive (the lastArchive of the invalid checkpoint).
390
+ archiveForCheck = invalidateCheckpoint.lastArchive;
391
+ l1SimulationOverridesBuilder.forPendingCheckpoint(invalidateCheckpoint.forcePendingCheckpointNumber);
392
+ this.metrics.recordPipelineDepth(0);
393
+ } else {
394
+ this.metrics.recordPipelineDepth(0);
395
+ }
332
396
 
333
- // Check with the rollup contract if we can indeed propose at the next L2 slot. This check should not fail
334
- // if all the previous checks are good, but we do it just in case.
397
+ const simulationOverridesPlan = l1SimulationOverridesBuilder.build();
335
398
  const canProposeCheck = await publisher.canProposeAt(
336
- syncedTo.archive,
399
+ archiveForCheck,
337
400
  proposer ?? EthAddress.ZERO,
338
- invalidateCheckpoint,
401
+ simulationOverridesPlan,
339
402
  );
340
403
 
341
404
  if (canProposeCheck === undefined) {
@@ -348,10 +411,10 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
348
411
  return undefined;
349
412
  }
350
413
 
351
- if (canProposeCheck.slot !== slot) {
414
+ if (canProposeCheck.slot !== targetSlot) {
352
415
  this.log.warn(
353
- `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}.`,
354
- { ...logCtx, rollup: canProposeCheck, expectedSlot: slot },
416
+ `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}.`,
417
+ { ...logCtx, rollup: canProposeCheck, expectedSlot: targetSlot },
355
418
  );
356
419
  this.emit('proposer-rollup-check-failed', { reason: 'Slot mismatch', slot });
357
420
  this.metrics.recordCheckpointPrecheckFailed('slot_mismatch');
@@ -368,36 +431,49 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
368
431
  return undefined;
369
432
  }
370
433
 
371
- this.lastSlotForCheckpointProposalJob = slot;
372
- await this.p2pClient.prepareForSlot(slot);
373
- this.log.info(`Preparing checkpoint proposal ${checkpointNumber} at slot ${slot}`, { ...logCtx, proposer });
434
+ this.lastSlotForCheckpointProposalJob = targetSlot;
435
+
436
+ await this.p2pClient.prepareForSlot(targetSlot);
437
+ this.log.info(
438
+ `Preparing checkpoint proposal ${checkpointNumber} for target slot ${targetSlot} during wall-clock slot ${slot}`,
439
+ {
440
+ ...logCtx,
441
+ proposer,
442
+ pipeliningEnabled: this.epochCache.isProposerPipeliningEnabled(),
443
+ },
444
+ );
374
445
 
375
446
  // Create and return the checkpoint proposal job
376
447
  return this.createCheckpointProposalJob(
377
- epoch,
378
448
  slot,
449
+ targetSlot,
450
+ targetEpoch,
379
451
  checkpointNumber,
380
452
  syncedTo.blockNumber,
381
453
  proposer,
382
454
  publisher,
383
455
  attestorAddress,
384
456
  invalidateCheckpoint,
457
+ syncedTo.proposedCheckpointData,
385
458
  );
386
459
  }
387
460
 
388
461
  protected createCheckpointProposalJob(
389
- epoch: EpochNumber,
390
462
  slot: SlotNumber,
463
+ targetSlot: SlotNumber,
464
+ targetEpoch: EpochNumber,
391
465
  checkpointNumber: CheckpointNumber,
392
466
  syncedToBlockNumber: BlockNumber,
393
467
  proposer: EthAddress | undefined,
394
468
  publisher: SequencerPublisher,
395
469
  attestorAddress: EthAddress,
396
470
  invalidateCheckpoint: InvalidateCheckpointRequest | undefined,
471
+ proposedCheckpointData?: ProposedCheckpointData,
397
472
  ): CheckpointProposalJob {
398
473
  return new CheckpointProposalJob(
399
- epoch,
400
474
  slot,
475
+ targetSlot,
476
+ targetEpoch,
401
477
  checkpointNumber,
402
478
  syncedToBlockNumber,
403
479
  proposer,
@@ -419,10 +495,12 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
419
495
  this.epochCache,
420
496
  this.dateProvider,
421
497
  this.metrics,
498
+ this.checkpointProposalJobMetrics.createRecorder(),
422
499
  this,
423
500
  this.setState.bind(this),
424
501
  this.tracer,
425
502
  this.log.getBindings(),
503
+ proposedCheckpointData,
426
504
  );
427
505
  }
428
506
 
@@ -497,25 +575,43 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
497
575
  number: syncSummary.latestBlockNumber,
498
576
  hash: syncSummary.latestBlockHash,
499
577
  })),
500
- this.l2BlockSource.getL2Tips().then(t => t.proposed),
578
+ this.l2BlockSource
579
+ .getL2Tips()
580
+ .then(t => ({ proposed: t.proposed, checkpointed: t.checkpointed, proposedCheckpoint: t.proposedCheckpoint })),
501
581
  this.p2pClient.getStatus().then(p2p => p2p.syncedToL2Block),
502
- this.l1ToL2MessageSource.getL2Tips().then(t => t.proposed),
582
+ this.l1ToL2MessageSource.getL2Tips().then(t => ({ proposed: t.proposed, checkpointed: t.checkpointed })),
503
583
  this.l2BlockSource.getPendingChainValidationStatus(),
584
+ this.l2BlockSource.getProposedCheckpointOnly(),
504
585
  ] as const);
505
586
 
506
- const [worldState, l2BlockSource, p2p, l1ToL2MessageSource, pendingChainValidationStatus] = syncedBlocks;
587
+ const [worldState, l2Tips, p2p, l1ToL2MessageSourceTips, pendingChainValidationStatus, proposedCheckpointData] =
588
+ syncedBlocks;
507
589
 
508
590
  // 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,
509
591
  // as the world state can compute the new genesis block hash, but other components use the hardcoded constant.
510
592
  // TODO(palla/mbps): Fix the above. All components should be able to handle dynamic genesis block hashes.
511
593
  const result =
512
- (l2BlockSource.number === 0 && worldState.number === 0 && p2p.number === 0 && l1ToL2MessageSource.number === 0) ||
513
- (worldState.hash === l2BlockSource.hash &&
514
- p2p.hash === l2BlockSource.hash &&
515
- l1ToL2MessageSource.hash === l2BlockSource.hash);
594
+ (l2Tips.proposed.number === 0 &&
595
+ l2Tips.checkpointed.block.number === 0 &&
596
+ l2Tips.checkpointed.checkpoint.number === 0 &&
597
+ worldState.number === 0 &&
598
+ p2p.number === 0 &&
599
+ l1ToL2MessageSourceTips.proposed.number === 0 &&
600
+ l1ToL2MessageSourceTips.checkpointed.block.number === 0 &&
601
+ l1ToL2MessageSourceTips.checkpointed.checkpoint.number === 0) ||
602
+ (worldState.hash === l2Tips.proposed.hash &&
603
+ p2p.hash === l2Tips.proposed.hash &&
604
+ l1ToL2MessageSourceTips.proposed.hash === l2Tips.proposed.hash &&
605
+ l1ToL2MessageSourceTips.checkpointed.block.hash === l2Tips.checkpointed.block.hash &&
606
+ l1ToL2MessageSourceTips.checkpointed.checkpoint.hash === l2Tips.checkpointed.checkpoint.hash);
516
607
 
517
608
  if (!result) {
518
- this.log.debug(`Sequencer sync check failed`, { worldState, l2BlockSource, p2p, l1ToL2MessageSource });
609
+ this.log.debug(`Sequencer sync check failed`, {
610
+ worldState,
611
+ l2BlockSource: l2Tips.proposed,
612
+ p2p,
613
+ l1ToL2MessageSourceTips,
614
+ });
519
615
  return undefined;
520
616
  }
521
617
 
@@ -525,8 +621,10 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
525
621
  const archive = new Fr((await this.worldState.getCommitted().getTreeInfo(MerkleTreeId.ARCHIVE)).root);
526
622
  return {
527
623
  checkpointNumber: CheckpointNumber.ZERO,
624
+ checkpointedCheckpointNumber: CheckpointNumber.ZERO,
528
625
  blockNumber: BlockNumber.ZERO,
529
626
  archive,
627
+ hasProposedCheckpoint: false,
530
628
  syncedL2Slot,
531
629
  pendingChainValidationStatus,
532
630
  };
@@ -539,11 +637,16 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
539
637
  return undefined;
540
638
  }
541
639
 
640
+ const hasProposedCheckpoint = l2Tips.proposedCheckpoint.checkpoint.number > l2Tips.checkpointed.checkpoint.number;
641
+
542
642
  return {
543
643
  blockData,
544
644
  blockNumber: blockData.header.getBlockNumber(),
545
645
  checkpointNumber: blockData.checkpointNumber,
646
+ checkpointedCheckpointNumber: l2Tips.checkpointed.checkpoint.number,
546
647
  archive: blockData.archive.root,
648
+ hasProposedCheckpoint,
649
+ proposedCheckpointData,
547
650
  syncedL2Slot,
548
651
  pendingChainValidationStatus,
549
652
  };
@@ -553,20 +656,20 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
553
656
  * Checks if we are the proposer for the next slot.
554
657
  * @returns True if we can propose, and the proposer address (undefined if anyone can propose)
555
658
  */
556
- protected async checkCanPropose(slot: SlotNumber): Promise<[boolean, EthAddress | undefined]> {
659
+ protected async checkCanPropose(targetSlot: SlotNumber): Promise<[boolean, EthAddress | undefined]> {
557
660
  let proposer: EthAddress | undefined;
558
661
 
559
662
  try {
560
- proposer = await this.epochCache.getProposerAttesterAddressInSlot(slot);
663
+ proposer = await this.epochCache.getProposerAttesterAddressInSlot(targetSlot);
561
664
  } catch (e) {
562
665
  if (e instanceof NoCommitteeError) {
563
- if (this.lastSlotForNoCommitteeWarning !== slot) {
564
- this.lastSlotForNoCommitteeWarning = slot;
565
- this.log.warn(`Cannot propose at next L2 slot ${slot} since the committee does not exist on L1`);
666
+ if (this.lastSlotForNoCommitteeWarning !== targetSlot) {
667
+ this.lastSlotForNoCommitteeWarning = targetSlot;
668
+ this.log.warn(`Cannot propose at target slot ${targetSlot} since the committee does not exist on L1`);
566
669
  }
567
670
  return [false, undefined];
568
671
  }
569
- this.log.error(`Error getting proposer for slot ${slot}`, e);
672
+ this.log.error(`Error getting proposer for target slot ${targetSlot}`, e);
570
673
  return [false, undefined];
571
674
  }
572
675
 
@@ -583,10 +686,18 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
583
686
  const weAreProposer = validatorAddresses.some(addr => addr.equals(proposer));
584
687
 
585
688
  if (!weAreProposer) {
586
- this.log.debug(`Cannot propose at slot ${slot} since we are not a proposer`, { validatorAddresses, proposer });
689
+ this.log.debug(`Cannot propose at target slot ${targetSlot} since we are not a proposer`, {
690
+ targetSlot,
691
+ validatorAddresses,
692
+ proposer,
693
+ });
587
694
  return [false, proposer];
588
695
  }
589
696
 
697
+ this.log.info(`We are the proposer for pipeline slot ${targetSlot}`, {
698
+ targetSlot,
699
+ proposer,
700
+ });
590
701
  return [true, proposer];
591
702
  }
592
703
 
@@ -595,8 +706,8 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
595
706
  * This allows the sequencer to participate in governance/slashing votes even when it cannot build blocks.
596
707
  */
597
708
  @trackSpan('Seqeuencer.tryVoteWhenSyncFails', ({ slot }) => ({ [Attributes.SLOT_NUMBER]: slot }))
598
- protected async tryVoteWhenSyncFails(args: { slot: SlotNumber; ts: bigint }): Promise<void> {
599
- const { slot } = args;
709
+ protected async tryVoteWhenSyncFails(args: { slot: SlotNumber; targetSlot: SlotNumber; ts: bigint }): Promise<void> {
710
+ const { slot, targetSlot } = args;
600
711
 
601
712
  // Prevent duplicate attempts in the same slot
602
713
  if (this.lastSlotForFallbackVote === slot) {
@@ -624,7 +735,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
624
735
  });
625
736
 
626
737
  // Check if we're a proposer or proposal is open
627
- const [canPropose, proposer] = await this.checkCanPropose(slot);
738
+ const [canPropose, proposer] = await this.checkCanPropose(targetSlot);
628
739
  if (!canPropose) {
629
740
  this.log.trace(`Cannot vote in slot ${slot} since we are not a proposer`, { slot, proposer });
630
741
  return;
@@ -641,9 +752,9 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
641
752
  slot,
642
753
  });
643
754
 
644
- // Enqueue governance and slashing votes
755
+ // Enqueue governance and slashing votes (voter uses the target slot for L1 submission)
645
756
  const voter = new CheckpointVoter(
646
- slot,
757
+ targetSlot,
647
758
  publisher,
648
759
  attestorAddress,
649
760
  this.validatorClient,
@@ -883,8 +994,11 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
883
994
  type SequencerSyncCheckResult = {
884
995
  blockData?: BlockData;
885
996
  checkpointNumber: CheckpointNumber;
997
+ checkpointedCheckpointNumber: CheckpointNumber;
886
998
  blockNumber: BlockNumber;
887
999
  archive: Fr;
1000
+ hasProposedCheckpoint: boolean;
1001
+ proposedCheckpointData?: ProposedCheckpointData;
888
1002
  syncedL2Slot: SlotNumber;
889
1003
  pendingChainValidationStatus: ValidateCheckpointResult;
890
1004
  };