@aztec/sequencer-client 0.0.1-commit.358457c → 0.0.1-commit.3750d92a7

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 (80) 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 +36 -25
  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 +15 -14
  14. package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
  15. package/dest/global_variable_builder/global_builder.js +16 -50
  16. package/dest/global_variable_builder/index.d.ts +4 -2
  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 +13 -1
  20. package/dest/publisher/config.d.ts.map +1 -1
  21. package/dest/publisher/config.js +19 -4
  22. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts +3 -4
  23. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -1
  24. package/dest/publisher/sequencer-publisher-factory.d.ts +3 -5
  25. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  26. package/dest/publisher/sequencer-publisher-factory.js +16 -3
  27. package/dest/publisher/sequencer-publisher.d.ts +55 -43
  28. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  29. package/dest/publisher/sequencer-publisher.js +180 -116
  30. package/dest/sequencer/chain_state_overrides.d.ts +25 -0
  31. package/dest/sequencer/chain_state_overrides.d.ts.map +1 -0
  32. package/dest/sequencer/chain_state_overrides.js +39 -0
  33. package/dest/sequencer/checkpoint_proposal_job.d.ts +26 -9
  34. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  35. package/dest/sequencer/checkpoint_proposal_job.js +350 -214
  36. package/dest/sequencer/checkpoint_proposal_job_metrics.d.ts +34 -0
  37. package/dest/sequencer/checkpoint_proposal_job_metrics.d.ts.map +1 -0
  38. package/dest/sequencer/checkpoint_proposal_job_metrics.js +72 -0
  39. package/dest/sequencer/checkpoint_voter.d.ts +1 -2
  40. package/dest/sequencer/checkpoint_voter.d.ts.map +1 -1
  41. package/dest/sequencer/checkpoint_voter.js +2 -5
  42. package/dest/sequencer/events.d.ts +2 -1
  43. package/dest/sequencer/events.d.ts.map +1 -1
  44. package/dest/sequencer/metrics.d.ts +9 -10
  45. package/dest/sequencer/metrics.d.ts.map +1 -1
  46. package/dest/sequencer/metrics.js +23 -20
  47. package/dest/sequencer/sequencer.d.ts +29 -11
  48. package/dest/sequencer/sequencer.d.ts.map +1 -1
  49. package/dest/sequencer/sequencer.js +137 -74
  50. package/dest/sequencer/timetable.d.ts +17 -3
  51. package/dest/sequencer/timetable.d.ts.map +1 -1
  52. package/dest/sequencer/timetable.js +51 -43
  53. package/dest/sequencer/types.d.ts +2 -2
  54. package/dest/sequencer/types.d.ts.map +1 -1
  55. package/dest/test/mock_checkpoint_builder.d.ts +7 -9
  56. package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
  57. package/dest/test/mock_checkpoint_builder.js +39 -30
  58. package/package.json +27 -28
  59. package/src/client/sequencer-client.ts +61 -28
  60. package/src/config.ts +44 -27
  61. package/src/global_variable_builder/README.md +44 -0
  62. package/src/global_variable_builder/fee_predictor.ts +172 -0
  63. package/src/global_variable_builder/fee_provider.ts +75 -0
  64. package/src/global_variable_builder/global_builder.ts +26 -62
  65. package/src/global_variable_builder/index.ts +3 -1
  66. package/src/publisher/config.ts +38 -4
  67. package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +3 -1
  68. package/src/publisher/sequencer-publisher-factory.ts +18 -6
  69. package/src/publisher/sequencer-publisher.ts +263 -167
  70. package/src/sequencer/README.md +83 -13
  71. package/src/sequencer/chain_state_overrides.ts +87 -0
  72. package/src/sequencer/checkpoint_proposal_job.ts +452 -241
  73. package/src/sequencer/checkpoint_proposal_job_metrics.ts +128 -0
  74. package/src/sequencer/checkpoint_voter.ts +1 -12
  75. package/src/sequencer/events.ts +1 -1
  76. package/src/sequencer/metrics.ts +29 -25
  77. package/src/sequencer/sequencer.ts +198 -81
  78. package/src/sequencer/timetable.ts +64 -52
  79. package/src/sequencer/types.ts +1 -1
  80. 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,
@@ -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,22 +109,25 @@ 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
 
110
116
  /** Updates sequencer config by the defined values and updates the timetable */
111
117
  public updateConfig(config: Partial<SequencerConfig>) {
112
118
  const filteredConfig = pickFromSchema(config, SequencerConfigSchema);
113
- this.log.info(`Updated sequencer config`, omit(filteredConfig, 'txPublicSetupAllowList'));
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,
@@ -143,12 +151,18 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
143
151
  this.log.info('Started sequencer');
144
152
  }
145
153
 
154
+ /** Triggers an immediate run of the sequencer, bypassing the polling interval. */
155
+ public trigger() {
156
+ return this.runningPromise?.trigger();
157
+ }
158
+
146
159
  /** Stops the sequencer from building blocks and moves to STOPPED state. */
147
160
  public async stop(): Promise<void> {
148
161
  this.log.info(`Stopping sequencer`);
149
162
  this.setState(SequencerState.STOPPING, undefined, { force: true });
150
- this.publisherFactory.interruptAll();
163
+ await this.publisherFactory.stopAll();
151
164
  await this.runningPromise?.stop();
165
+ await this.lastCheckpointProposalJob?.awaitPendingSubmission();
152
166
  this.setState(SequencerState.STOPPED, undefined, { force: true });
153
167
  this.log.info('Stopped sequencer');
154
168
  }
@@ -192,14 +206,25 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
192
206
  @trackSpan('Sequencer.work')
193
207
  protected async work() {
194
208
  this.setState(SequencerState.SYNCHRONIZING, undefined);
195
- 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();
196
211
 
197
212
  // 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);
213
+ const checkpointProposalJob = await this.prepareCheckpointProposal(
214
+ slot,
215
+ targetSlot,
216
+ epoch,
217
+ targetEpoch,
218
+ ts,
219
+ nowSeconds,
220
+ );
199
221
  if (!checkpointProposalJob) {
200
222
  return;
201
223
  }
202
224
 
225
+ // Track the job so we can await its pending L1 submission during shutdown
226
+ this.lastCheckpointProposalJob = checkpointProposalJob;
227
+
203
228
  // Execute the checkpoint proposal job
204
229
  const checkpoint = await checkpointProposalJob.execute();
205
230
 
@@ -208,13 +233,13 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
208
233
  this.lastCheckpointProposed = checkpoint;
209
234
  }
210
235
 
211
- // 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)
212
237
  if (
213
238
  this.config.fishermanMode &&
214
- (this.lastEpochForStrategyComparison === undefined || epoch > this.lastEpochForStrategyComparison)
239
+ (this.lastEpochForStrategyComparison === undefined || targetEpoch > this.lastEpochForStrategyComparison)
215
240
  ) {
216
- this.logStrategyComparison(epoch, checkpointProposalJob.getPublisher());
217
- this.lastEpochForStrategyComparison = epoch;
241
+ this.logStrategyComparison(targetEpoch, checkpointProposalJob.getPublisher());
242
+ this.lastEpochForStrategyComparison = targetEpoch;
218
243
  }
219
244
 
220
245
  return checkpoint;
@@ -226,44 +251,49 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
226
251
  * @returns CheckpointProposalJob if successful, undefined if we are not yet synced or are not the proposer.
227
252
  */
228
253
  @trackSpan('Sequencer.prepareCheckpointProposal')
229
- private async prepareCheckpointProposal(
230
- epoch: EpochNumber,
254
+ protected async prepareCheckpointProposal(
231
255
  slot: SlotNumber,
256
+ targetSlot: SlotNumber,
257
+ epoch: EpochNumber,
258
+ targetEpoch: EpochNumber,
232
259
  ts: bigint,
233
- now: bigint,
260
+ nowSeconds: bigint,
234
261
  ): Promise<CheckpointProposalJob | undefined> {
235
- // Check we have not already processed this slot (cheapest check)
262
+ // Check we have not already processed this target slot (cheapest check)
236
263
  // We only check this if enforce timetable is set, since we want to keep processing the same slot if we are not
237
264
  // running against actual time (eg when we use sandbox-style automining)
238
265
  if (
239
266
  this.lastSlotForCheckpointProposalJob &&
240
- this.lastSlotForCheckpointProposalJob >= slot &&
267
+ this.lastSlotForCheckpointProposalJob >= targetSlot &&
241
268
  this.config.enforceTimeTable
242
269
  ) {
243
- this.log.trace(`Slot ${slot} has already been processed`);
270
+ this.log.trace(`Target slot ${targetSlot} has already been processed`);
244
271
  return undefined;
245
272
  }
246
273
 
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}`);
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
+ );
250
279
  return undefined;
251
280
  }
252
281
 
253
282
  // Check all components are synced to latest as seen by the archiver (queries all subsystems)
254
283
  const syncedTo = await this.checkSync({ ts, slot });
255
284
  if (!syncedTo) {
256
- await this.tryVoteWhenSyncFails({ slot, ts });
285
+ await this.tryVoteWhenSyncFails({ slot, targetSlot, ts });
257
286
  return undefined;
258
287
  }
259
288
 
260
- // 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.
261
290
  // Still perform governance/slashing voting (as proposer) once per slot.
262
- 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);
263
293
 
264
294
  if (isEscapeHatchOpen) {
265
295
  this.setState(SequencerState.PROPOSER_CHECK, slot);
266
- const [canPropose, proposer] = await this.checkCanPropose(slot);
296
+ const [canPropose, proposer] = await this.checkCanPropose(targetSlot);
267
297
  if (canPropose) {
268
298
  await this.tryVoteWhenEscapeHatchOpen({ slot, proposer });
269
299
  } else {
@@ -280,18 +310,18 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
280
310
  const checkpointNumber = CheckpointNumber(syncedTo.checkpointNumber + 1);
281
311
 
282
312
  const logCtx = {
283
- now,
284
- syncedToL1Ts: syncedTo.l1Timestamp,
285
- syncedToL2Slot: getSlotAtTimestamp(syncedTo.l1Timestamp, this.l1Constants),
313
+ nowSeconds,
314
+ syncedToL2Slot: syncedTo.syncedL2Slot,
286
315
  slot,
316
+ targetSlot,
287
317
  slotTs: ts,
288
318
  checkpointNumber,
289
319
  isPendingChainValid: pick(syncedTo.pendingChainValidationStatus, 'valid', 'reason', 'invalidIndex'),
290
320
  };
291
321
 
292
- // Check that we are a proposer for the next slot
322
+ // Check that we are a proposer for the target slot.
293
323
  this.setState(SequencerState.PROPOSER_CHECK, slot);
294
- const [canPropose, proposer] = await this.checkCanPropose(slot);
324
+ const [canPropose, proposer] = await this.checkCanPropose(targetSlot);
295
325
 
296
326
  // If we are not a proposer check if we should invalidate an invalid checkpoint, and bail
297
327
  if (!canPropose) {
@@ -299,10 +329,20 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
299
329
  return undefined;
300
330
  }
301
331
 
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) {
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) {
304
344
  this.log.warn(
305
- `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}`,
306
346
  { ...logCtx, block: syncedTo.blockData.header.toInspect() },
307
347
  );
308
348
  this.metrics.recordCheckpointPrecheckFailed('slot_already_taken');
@@ -324,14 +364,41 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
324
364
  }
325
365
 
326
366
  // Prepare invalidation request if the pending chain is invalid (returns undefined if no need)
327
- 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
+ }
328
396
 
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,
397
+ const simulationOverridesPlan = l1SimulationOverridesBuilder.build();
398
+ const canProposeCheck = await publisher.canProposeAt(
399
+ archiveForCheck,
333
400
  proposer ?? EthAddress.ZERO,
334
- invalidateCheckpoint,
401
+ simulationOverridesPlan,
335
402
  );
336
403
 
337
404
  if (canProposeCheck === undefined) {
@@ -344,10 +411,10 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
344
411
  return undefined;
345
412
  }
346
413
 
347
- if (canProposeCheck.slot !== slot) {
414
+ if (canProposeCheck.slot !== targetSlot) {
348
415
  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 },
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 },
351
418
  );
352
419
  this.emit('proposer-rollup-check-failed', { reason: 'Slot mismatch', slot });
353
420
  this.metrics.recordCheckpointPrecheckFailed('slot_mismatch');
@@ -364,36 +431,49 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
364
431
  return undefined;
365
432
  }
366
433
 
367
- this.lastSlotForCheckpointProposalJob = slot;
368
- await this.p2pClient.prepareForSlot(slot);
369
- 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
+ );
370
445
 
371
446
  // Create and return the checkpoint proposal job
372
447
  return this.createCheckpointProposalJob(
373
- epoch,
374
448
  slot,
449
+ targetSlot,
450
+ targetEpoch,
375
451
  checkpointNumber,
376
452
  syncedTo.blockNumber,
377
453
  proposer,
378
454
  publisher,
379
455
  attestorAddress,
380
456
  invalidateCheckpoint,
457
+ syncedTo.proposedCheckpointData,
381
458
  );
382
459
  }
383
460
 
384
461
  protected createCheckpointProposalJob(
385
- epoch: EpochNumber,
386
462
  slot: SlotNumber,
463
+ targetSlot: SlotNumber,
464
+ targetEpoch: EpochNumber,
387
465
  checkpointNumber: CheckpointNumber,
388
466
  syncedToBlockNumber: BlockNumber,
389
467
  proposer: EthAddress | undefined,
390
468
  publisher: SequencerPublisher,
391
469
  attestorAddress: EthAddress,
392
470
  invalidateCheckpoint: InvalidateCheckpointRequest | undefined,
471
+ proposedCheckpointData?: ProposedCheckpointData,
393
472
  ): CheckpointProposalJob {
394
473
  return new CheckpointProposalJob(
395
- epoch,
396
474
  slot,
475
+ targetSlot,
476
+ targetEpoch,
397
477
  checkpointNumber,
398
478
  syncedToBlockNumber,
399
479
  proposer,
@@ -415,10 +495,12 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
415
495
  this.epochCache,
416
496
  this.dateProvider,
417
497
  this.metrics,
498
+ this.checkpointProposalJobMetrics.createRecorder(),
418
499
  this,
419
500
  this.setState.bind(this),
420
501
  this.tracer,
421
502
  this.log.getBindings(),
503
+ proposedCheckpointData,
422
504
  );
423
505
  }
424
506
 
@@ -475,16 +557,15 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
475
557
  * We don't check against the previous block submitted since it may have been reorg'd out.
476
558
  */
477
559
  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) {
560
+ // Check that the archiver has fully synced the L2 slot before the one we want to propose in.
561
+ // The archiver reports sync progress via L1 block timestamps and synced checkpoint slots.
562
+ // See getSyncedL2SlotNumber for how missed L1 blocks are handled.
563
+ const syncedL2Slot = await this.l2BlockSource.getSyncedL2SlotNumber();
564
+ const { slot } = args;
565
+ if (syncedL2Slot === undefined || syncedL2Slot + 1 < slot) {
484
566
  this.log.debug(`Cannot propose block at next L2 slot ${slot} due to pending sync from L1`, {
485
567
  slot,
486
- ts,
487
- l1Timestamp,
568
+ syncedL2Slot,
488
569
  });
489
570
  return undefined;
490
571
  }
@@ -494,25 +575,43 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
494
575
  number: syncSummary.latestBlockNumber,
495
576
  hash: syncSummary.latestBlockHash,
496
577
  })),
497
- this.l2BlockSource.getL2Tips().then(t => t.proposed),
578
+ this.l2BlockSource
579
+ .getL2Tips()
580
+ .then(t => ({ proposed: t.proposed, checkpointed: t.checkpointed, proposedCheckpoint: t.proposedCheckpoint })),
498
581
  this.p2pClient.getStatus().then(p2p => p2p.syncedToL2Block),
499
- this.l1ToL2MessageSource.getL2Tips().then(t => t.proposed),
582
+ this.l1ToL2MessageSource.getL2Tips().then(t => ({ proposed: t.proposed, checkpointed: t.checkpointed })),
500
583
  this.l2BlockSource.getPendingChainValidationStatus(),
584
+ this.l2BlockSource.getProposedCheckpointOnly(),
501
585
  ] as const);
502
586
 
503
- const [worldState, l2BlockSource, p2p, l1ToL2MessageSource, pendingChainValidationStatus] = syncedBlocks;
587
+ const [worldState, l2Tips, p2p, l1ToL2MessageSourceTips, pendingChainValidationStatus, proposedCheckpointData] =
588
+ syncedBlocks;
504
589
 
505
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,
506
591
  // as the world state can compute the new genesis block hash, but other components use the hardcoded constant.
507
592
  // TODO(palla/mbps): Fix the above. All components should be able to handle dynamic genesis block hashes.
508
593
  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);
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);
513
607
 
514
608
  if (!result) {
515
- 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
+ });
516
615
  return undefined;
517
616
  }
518
617
 
@@ -522,9 +621,11 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
522
621
  const archive = new Fr((await this.worldState.getCommitted().getTreeInfo(MerkleTreeId.ARCHIVE)).root);
523
622
  return {
524
623
  checkpointNumber: CheckpointNumber.ZERO,
624
+ checkpointedCheckpointNumber: CheckpointNumber.ZERO,
525
625
  blockNumber: BlockNumber.ZERO,
526
626
  archive,
527
- l1Timestamp,
627
+ hasProposedCheckpoint: false,
628
+ syncedL2Slot,
528
629
  pendingChainValidationStatus,
529
630
  };
530
631
  }
@@ -536,12 +637,17 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
536
637
  return undefined;
537
638
  }
538
639
 
640
+ const hasProposedCheckpoint = l2Tips.proposedCheckpoint.checkpoint.number > l2Tips.checkpointed.checkpoint.number;
641
+
539
642
  return {
540
643
  blockData,
541
644
  blockNumber: blockData.header.getBlockNumber(),
542
645
  checkpointNumber: blockData.checkpointNumber,
646
+ checkpointedCheckpointNumber: l2Tips.checkpointed.checkpoint.number,
543
647
  archive: blockData.archive.root,
544
- l1Timestamp,
648
+ hasProposedCheckpoint,
649
+ proposedCheckpointData,
650
+ syncedL2Slot,
545
651
  pendingChainValidationStatus,
546
652
  };
547
653
  }
@@ -550,20 +656,20 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
550
656
  * Checks if we are the proposer for the next slot.
551
657
  * @returns True if we can propose, and the proposer address (undefined if anyone can propose)
552
658
  */
553
- protected async checkCanPropose(slot: SlotNumber): Promise<[boolean, EthAddress | undefined]> {
659
+ protected async checkCanPropose(targetSlot: SlotNumber): Promise<[boolean, EthAddress | undefined]> {
554
660
  let proposer: EthAddress | undefined;
555
661
 
556
662
  try {
557
- proposer = await this.epochCache.getProposerAttesterAddressInSlot(slot);
663
+ proposer = await this.epochCache.getProposerAttesterAddressInSlot(targetSlot);
558
664
  } catch (e) {
559
665
  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`);
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`);
563
669
  }
564
670
  return [false, undefined];
565
671
  }
566
- this.log.error(`Error getting proposer for slot ${slot}`, e);
672
+ this.log.error(`Error getting proposer for target slot ${targetSlot}`, e);
567
673
  return [false, undefined];
568
674
  }
569
675
 
@@ -580,10 +686,18 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
580
686
  const weAreProposer = validatorAddresses.some(addr => addr.equals(proposer));
581
687
 
582
688
  if (!weAreProposer) {
583
- 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
+ });
584
694
  return [false, proposer];
585
695
  }
586
696
 
697
+ this.log.info(`We are the proposer for pipeline slot ${targetSlot}`, {
698
+ targetSlot,
699
+ proposer,
700
+ });
587
701
  return [true, proposer];
588
702
  }
589
703
 
@@ -592,8 +706,8 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
592
706
  * This allows the sequencer to participate in governance/slashing votes even when it cannot build blocks.
593
707
  */
594
708
  @trackSpan('Seqeuencer.tryVoteWhenSyncFails', ({ slot }) => ({ [Attributes.SLOT_NUMBER]: slot }))
595
- protected async tryVoteWhenSyncFails(args: { slot: SlotNumber; ts: bigint }): Promise<void> {
596
- const { slot } = args;
709
+ protected async tryVoteWhenSyncFails(args: { slot: SlotNumber; targetSlot: SlotNumber; ts: bigint }): Promise<void> {
710
+ const { slot, targetSlot } = args;
597
711
 
598
712
  // Prevent duplicate attempts in the same slot
599
713
  if (this.lastSlotForFallbackVote === slot) {
@@ -621,7 +735,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
621
735
  });
622
736
 
623
737
  // Check if we're a proposer or proposal is open
624
- const [canPropose, proposer] = await this.checkCanPropose(slot);
738
+ const [canPropose, proposer] = await this.checkCanPropose(targetSlot);
625
739
  if (!canPropose) {
626
740
  this.log.trace(`Cannot vote in slot ${slot} since we are not a proposer`, { slot, proposer });
627
741
  return;
@@ -638,9 +752,9 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
638
752
  slot,
639
753
  });
640
754
 
641
- // Enqueue governance and slashing votes
755
+ // Enqueue governance and slashing votes (voter uses the target slot for L1 submission)
642
756
  const voter = new CheckpointVoter(
643
- slot,
757
+ targetSlot,
644
758
  publisher,
645
759
  attestorAddress,
646
760
  this.validatorClient,
@@ -720,7 +834,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
720
834
  syncedTo: SequencerSyncCheckResult,
721
835
  currentSlot: SlotNumber,
722
836
  ): Promise<void> {
723
- const { pendingChainValidationStatus, l1Timestamp } = syncedTo;
837
+ const { pendingChainValidationStatus, syncedL2Slot } = syncedTo;
724
838
  if (pendingChainValidationStatus.valid) {
725
839
  return;
726
840
  }
@@ -735,7 +849,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
735
849
 
736
850
  const logData = {
737
851
  invalidL1Timestamp: invalidCheckpointTimestamp,
738
- l1Timestamp,
852
+ syncedL2Slot,
739
853
  invalidCheckpoint: pendingChainValidationStatus.checkpoint,
740
854
  secondsBeforeInvalidatingBlockAsCommitteeMember,
741
855
  secondsBeforeInvalidatingBlockAsNonCommitteeMember,
@@ -880,8 +994,11 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
880
994
  type SequencerSyncCheckResult = {
881
995
  blockData?: BlockData;
882
996
  checkpointNumber: CheckpointNumber;
997
+ checkpointedCheckpointNumber: CheckpointNumber;
883
998
  blockNumber: BlockNumber;
884
999
  archive: Fr;
885
- l1Timestamp: bigint;
1000
+ hasProposedCheckpoint: boolean;
1001
+ proposedCheckpointData?: ProposedCheckpointData;
1002
+ syncedL2Slot: SlotNumber;
886
1003
  pendingChainValidationStatus: ValidateCheckpointResult;
887
1004
  };