@aztec/sequencer-client 0.0.1-commit.f504929 → 0.0.1-commit.f5d02921e

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 (58) hide show
  1. package/dest/client/sequencer-client.d.ts +4 -1
  2. package/dest/client/sequencer-client.d.ts.map +1 -1
  3. package/dest/client/sequencer-client.js +46 -23
  4. package/dest/config.d.ts +25 -5
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +21 -12
  7. package/dest/global_variable_builder/global_builder.d.ts +15 -9
  8. package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
  9. package/dest/global_variable_builder/global_builder.js +29 -25
  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 -3
  16. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  17. package/dest/publisher/sequencer-publisher-factory.js +2 -2
  18. package/dest/publisher/sequencer-publisher.d.ts +52 -25
  19. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  20. package/dest/publisher/sequencer-publisher.js +98 -42
  21. package/dest/sequencer/checkpoint_proposal_job.d.ts +33 -8
  22. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  23. package/dest/sequencer/checkpoint_proposal_job.js +284 -158
  24. package/dest/sequencer/checkpoint_voter.d.ts +1 -2
  25. package/dest/sequencer/checkpoint_voter.d.ts.map +1 -1
  26. package/dest/sequencer/checkpoint_voter.js +2 -5
  27. package/dest/sequencer/events.d.ts +2 -1
  28. package/dest/sequencer/events.d.ts.map +1 -1
  29. package/dest/sequencer/metrics.d.ts +5 -1
  30. package/dest/sequencer/metrics.d.ts.map +1 -1
  31. package/dest/sequencer/metrics.js +11 -0
  32. package/dest/sequencer/sequencer.d.ts +23 -10
  33. package/dest/sequencer/sequencer.d.ts.map +1 -1
  34. package/dest/sequencer/sequencer.js +123 -68
  35. package/dest/sequencer/timetable.d.ts +4 -3
  36. package/dest/sequencer/timetable.d.ts.map +1 -1
  37. package/dest/sequencer/timetable.js +6 -7
  38. package/dest/sequencer/types.d.ts +2 -2
  39. package/dest/sequencer/types.d.ts.map +1 -1
  40. package/dest/test/mock_checkpoint_builder.d.ts +7 -9
  41. package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
  42. package/dest/test/mock_checkpoint_builder.js +39 -30
  43. package/package.json +27 -28
  44. package/src/client/sequencer-client.ts +56 -21
  45. package/src/config.ts +28 -14
  46. package/src/global_variable_builder/global_builder.ts +37 -26
  47. package/src/global_variable_builder/index.ts +1 -1
  48. package/src/publisher/config.ts +32 -0
  49. package/src/publisher/sequencer-publisher-factory.ts +3 -3
  50. package/src/publisher/sequencer-publisher.ts +144 -54
  51. package/src/sequencer/checkpoint_proposal_job.ts +367 -175
  52. package/src/sequencer/checkpoint_voter.ts +1 -12
  53. package/src/sequencer/events.ts +1 -1
  54. package/src/sequencer/metrics.ts +14 -0
  55. package/src/sequencer/sequencer.ts +178 -79
  56. package/src/sequencer/timetable.ts +7 -7
  57. package/src/sequencer/types.ts +1 -1
  58. package/src/test/mock_checkpoint_builder.ts +51 -48
@@ -2,7 +2,6 @@ import type { SlotNumber } from '@aztec/foundation/branded-types';
2
2
  import type { EthAddress } from '@aztec/foundation/eth-address';
3
3
  import type { Logger } from '@aztec/foundation/log';
4
4
  import type { SlasherClientInterface } from '@aztec/slasher';
5
- import { getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
6
5
  import type { ResolvedSequencerConfig } from '@aztec/stdlib/interfaces/server';
7
6
  import type { ValidatorClient } from '@aztec/validator-client';
8
7
  import { DutyAlreadySignedError } from '@aztec/validator-ha-signer/errors';
@@ -18,7 +17,6 @@ import type { SequencerRollupConstants } from './types.js';
18
17
  * Handles governance and slashing voting for a given slot.
19
18
  */
20
19
  export class CheckpointVoter {
21
- private slotTimestamp: bigint;
22
20
  private governanceSigner: (msg: TypedDataDefinition) => Promise<`0x${string}`>;
23
21
  private slashingSigner: (msg: TypedDataDefinition) => Promise<`0x${string}`>;
24
22
 
@@ -33,8 +31,6 @@ export class CheckpointVoter {
33
31
  private readonly metrics: SequencerMetrics,
34
32
  private readonly log: Logger,
35
33
  ) {
36
- this.slotTimestamp = getTimestampForSlot(this.slot, this.l1Constants);
37
-
38
34
  // Create separate signers with appropriate duty contexts for governance and slashing votes
39
35
  // These use HA protection to ensure only one node signs per slot/duty
40
36
  const governanceContext: SigningContext = { slot: this.slot, dutyType: DutyType.GOVERNANCE_VOTE };
@@ -77,7 +73,6 @@ export class CheckpointVoter {
77
73
  return await this.publisher.enqueueGovernanceCastSignal(
78
74
  governanceProposerPayload,
79
75
  this.slot,
80
- this.slotTimestamp,
81
76
  this.attestorAddress,
82
77
  this.governanceSigner,
83
78
  );
@@ -108,13 +103,7 @@ export class CheckpointVoter {
108
103
 
109
104
  this.metrics.recordSlashingAttempt(actions.length);
110
105
 
111
- return await this.publisher.enqueueSlashingActions(
112
- actions,
113
- this.slot,
114
- this.slotTimestamp,
115
- this.attestorAddress,
116
- this.slashingSigner,
117
- );
106
+ return await this.publisher.enqueueSlashingActions(actions, this.slot, this.attestorAddress, this.slashingSigner);
118
107
  } catch (err) {
119
108
  if (err instanceof DutyAlreadySignedError) {
120
109
  this.log.info(`Slashing vote already signed by another node`, {
@@ -13,7 +13,7 @@ export type SequencerEvents = {
13
13
  ['proposer-rollup-check-failed']: (args: { reason: string; slot: SlotNumber }) => void;
14
14
  ['block-tx-count-check-failed']: (args: { minTxs: number; availableTxs: number; slot: SlotNumber }) => void;
15
15
  ['block-build-failed']: (args: { reason: string; slot: SlotNumber }) => void;
16
- ['block-proposed']: (args: { blockNumber: BlockNumber; slot: SlotNumber }) => void;
16
+ ['block-proposed']: (args: { blockNumber: BlockNumber; slot: SlotNumber; buildSlot: SlotNumber }) => void;
17
17
  ['checkpoint-empty']: (args: { slot: SlotNumber }) => void;
18
18
  ['checkpoint-publish-failed']: (args: {
19
19
  slot: SlotNumber;
@@ -49,6 +49,8 @@ export class SequencerMetrics {
49
49
  private checkpointBlockCount: Gauge;
50
50
  private checkpointTxCount: Gauge;
51
51
  private checkpointTotalMana: Gauge;
52
+ private pipelineDepth: Gauge;
53
+ private pipelineDiscards: UpDownCounter;
52
54
 
53
55
  // Fisherman fee analysis metrics
54
56
  private fishermanWouldBeIncluded: UpDownCounter;
@@ -143,6 +145,10 @@ export class SequencerMetrics {
143
145
 
144
146
  this.slashingAttempts = createUpDownCounterWithDefault(this.meter, Metrics.SEQUENCER_SLASHING_ATTEMPTS_COUNT);
145
147
 
148
+ this.pipelineDepth = this.meter.createGauge(Metrics.SEQUENCER_PIPELINE_DEPTH);
149
+ this.pipelineDiscards = createUpDownCounterWithDefault(this.meter, Metrics.SEQUENCER_PIPELINE_DISCARDS_COUNT);
150
+ this.pipelineDepth.record(0);
151
+
146
152
  // Fisherman fee analysis metrics
147
153
  this.fishermanWouldBeIncluded = createUpDownCounterWithDefault(
148
154
  this.meter,
@@ -234,6 +240,14 @@ export class SequencerMetrics {
234
240
  });
235
241
  }
236
242
 
243
+ recordPipelineDepth(depth: number) {
244
+ this.pipelineDepth.record(depth);
245
+ }
246
+
247
+ recordPipelineDiscard(count = 1) {
248
+ this.pipelineDiscards.add(count);
249
+ }
250
+
237
251
  incOpenSlot(slot: SlotNumber, proposer: string) {
238
252
  // sequencer went through the loop a second time. Noop
239
253
  if (slot === this.lastSeenSlot) {
@@ -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
+ private lastCheckpointProposalJob: CheckpointProposalJob | undefined;
77
+
75
78
  /** The maximum number of seconds that the sequencer can be into a slot to transition to a particular state. */
76
79
  protected timetable!: SequencerTimetable;
77
80
 
@@ -147,8 +150,9 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
147
150
  public async stop(): Promise<void> {
148
151
  this.log.info(`Stopping sequencer`);
149
152
  this.setState(SequencerState.STOPPING, undefined, { force: true });
150
- this.publisherFactory.interruptAll();
153
+ await this.publisherFactory.stopAll();
151
154
  await this.runningPromise?.stop();
155
+ await this.lastCheckpointProposalJob?.awaitPendingSubmission();
152
156
  this.setState(SequencerState.STOPPED, undefined, { force: true });
153
157
  this.log.info('Stopped sequencer');
154
158
  }
@@ -192,14 +196,25 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
192
196
  @trackSpan('Sequencer.work')
193
197
  protected async work() {
194
198
  this.setState(SequencerState.SYNCHRONIZING, undefined);
195
- const { slot, ts, now, epoch } = this.epochCache.getEpochAndSlotInNextL1Slot();
199
+ const { slot, ts, nowSeconds, epoch } = this.epochCache.getEpochAndSlotInNextL1Slot();
200
+ const { slot: targetSlot, epoch: targetEpoch } = this.epochCache.getTargetEpochAndSlotInNextL1Slot();
196
201
 
197
202
  // 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);
203
+ const checkpointProposalJob = await this.prepareCheckpointProposal(
204
+ slot,
205
+ targetSlot,
206
+ epoch,
207
+ targetEpoch,
208
+ ts,
209
+ nowSeconds,
210
+ );
199
211
  if (!checkpointProposalJob) {
200
212
  return;
201
213
  }
202
214
 
215
+ // Track the job so we can await its pending L1 submission during shutdown
216
+ this.lastCheckpointProposalJob = checkpointProposalJob;
217
+
203
218
  // Execute the checkpoint proposal job
204
219
  const checkpoint = await checkpointProposalJob.execute();
205
220
 
@@ -208,13 +223,13 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
208
223
  this.lastCheckpointProposed = checkpoint;
209
224
  }
210
225
 
211
- // Log fee strategy comparison if on fisherman
226
+ // Log fee strategy comparison if on fisherman (uses target epoch since we mirror the proposer's perspective)
212
227
  if (
213
228
  this.config.fishermanMode &&
214
- (this.lastEpochForStrategyComparison === undefined || epoch > this.lastEpochForStrategyComparison)
229
+ (this.lastEpochForStrategyComparison === undefined || targetEpoch > this.lastEpochForStrategyComparison)
215
230
  ) {
216
- this.logStrategyComparison(epoch, checkpointProposalJob.getPublisher());
217
- this.lastEpochForStrategyComparison = epoch;
231
+ this.logStrategyComparison(targetEpoch, checkpointProposalJob.getPublisher());
232
+ this.lastEpochForStrategyComparison = targetEpoch;
218
233
  }
219
234
 
220
235
  return checkpoint;
@@ -226,44 +241,49 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
226
241
  * @returns CheckpointProposalJob if successful, undefined if we are not yet synced or are not the proposer.
227
242
  */
228
243
  @trackSpan('Sequencer.prepareCheckpointProposal')
229
- private async prepareCheckpointProposal(
230
- epoch: EpochNumber,
244
+ protected async prepareCheckpointProposal(
231
245
  slot: SlotNumber,
246
+ targetSlot: SlotNumber,
247
+ epoch: EpochNumber,
248
+ targetEpoch: EpochNumber,
232
249
  ts: bigint,
233
- now: bigint,
250
+ nowSeconds: bigint,
234
251
  ): Promise<CheckpointProposalJob | undefined> {
235
- // Check we have not already processed this slot (cheapest check)
252
+ // Check we have not already processed this target slot (cheapest check)
236
253
  // We only check this if enforce timetable is set, since we want to keep processing the same slot if we are not
237
254
  // running against actual time (eg when we use sandbox-style automining)
238
255
  if (
239
256
  this.lastSlotForCheckpointProposalJob &&
240
- this.lastSlotForCheckpointProposalJob >= slot &&
257
+ this.lastSlotForCheckpointProposalJob >= targetSlot &&
241
258
  this.config.enforceTimeTable
242
259
  ) {
243
- this.log.trace(`Slot ${slot} has already been processed`);
260
+ this.log.trace(`Target slot ${targetSlot} has already been processed`);
244
261
  return undefined;
245
262
  }
246
263
 
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}`);
264
+ // But if we have already proposed for this slot, then we definitely have to skip it, automining or not
265
+ if (this.lastCheckpointProposed && this.lastCheckpointProposed.header.slotNumber >= targetSlot) {
266
+ this.log.trace(
267
+ `Slot ${targetSlot} has already been published as checkpoint ${this.lastCheckpointProposed.number}`,
268
+ );
250
269
  return undefined;
251
270
  }
252
271
 
253
272
  // Check all components are synced to latest as seen by the archiver (queries all subsystems)
254
273
  const syncedTo = await this.checkSync({ ts, slot });
255
274
  if (!syncedTo) {
256
- await this.tryVoteWhenSyncFails({ slot, ts });
275
+ await this.tryVoteWhenSyncFails({ slot, targetSlot, ts });
257
276
  return undefined;
258
277
  }
259
278
 
260
- // If escape hatch is open for this epoch, do not start checkpoint proposal work and do not attempt invalidations.
279
+ // If escape hatch is open for the target epoch, do not start checkpoint proposal work and do not attempt invalidations.
261
280
  // Still perform governance/slashing voting (as proposer) once per slot.
262
- const isEscapeHatchOpen = await this.epochCache.isEscapeHatchOpen(epoch);
281
+ // When pipelining, we check the target epoch (slot+1's epoch) since that's the epoch we're building for.
282
+ const isEscapeHatchOpen = await this.epochCache.isEscapeHatchOpen(targetEpoch);
263
283
 
264
284
  if (isEscapeHatchOpen) {
265
285
  this.setState(SequencerState.PROPOSER_CHECK, slot);
266
- const [canPropose, proposer] = await this.checkCanPropose(slot);
286
+ const [canPropose, proposer] = await this.checkCanPropose(targetSlot);
267
287
  if (canPropose) {
268
288
  await this.tryVoteWhenEscapeHatchOpen({ slot, proposer });
269
289
  } else {
@@ -279,19 +299,29 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
279
299
  // Next checkpoint follows from the last synced one
280
300
  const checkpointNumber = CheckpointNumber(syncedTo.checkpointNumber + 1);
281
301
 
302
+ // Guard: don't exceed 1-deep pipeline. Without a proposed checkpoint, we can only build
303
+ // confirmed + 1. With a proposed checkpoint, we can build confirmed + 2.
304
+ const confirmedCkpt = syncedTo.checkpointedCheckpointNumber;
305
+ if (checkpointNumber > confirmedCkpt + 2) {
306
+ this.log.warn(
307
+ `Skipping slot ${targetSlot}: checkpoint ${checkpointNumber} exceeds max pipeline depth (confirmed=${confirmedCkpt})`,
308
+ );
309
+ return undefined;
310
+ }
311
+
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,10 @@ 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
+ // Check that the target slot is not taken by a block already (should never happen, since only us can propose for this slot)
333
+ if (syncedTo.blockData && syncedTo.blockData.header.getSlot() >= targetSlot) {
304
334
  this.log.warn(
305
- `Cannot propose block at next L2 slot ${slot} since that slot was taken by block ${syncedTo.blockNumber}`,
335
+ `Cannot propose block at target slot ${targetSlot} since that slot was taken by block ${syncedTo.blockNumber}`,
306
336
  { ...logCtx, block: syncedTo.blockData.header.toInspect() },
307
337
  );
308
338
  this.metrics.recordCheckpointPrecheckFailed('slot_already_taken');
@@ -324,15 +354,41 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
324
354
  }
325
355
 
326
356
  // Prepare invalidation request if the pending chain is invalid (returns undefined if no need)
327
- const invalidateCheckpoint = await publisher.simulateInvalidateCheckpoint(syncedTo.pendingChainValidationStatus);
357
+ let invalidateCheckpoint = await publisher.simulateInvalidateCheckpoint(syncedTo.pendingChainValidationStatus);
358
+
359
+ // Determine the correct archive and L1 state overrides for the canProposeAt check.
360
+ // The L1 contract reads archives[proposedCheckpointNumber] and compares it with the provided archive.
361
+ // When invalidating or pipelining, the local archive may differ from L1's, so we adjust accordingly.
362
+ let archiveForCheck = syncedTo.archive;
363
+ const l1Overrides: {
364
+ forcePendingCheckpointNumber?: CheckpointNumber;
365
+ forceArchive?: { checkpointNumber: CheckpointNumber; archive: Fr };
366
+ } = {};
367
+
368
+ if (this.epochCache.isProposerPipeliningEnabled() && syncedTo.hasProposedCheckpoint) {
369
+ // Parent checkpoint hasn't landed on L1 yet. Override both the proposed checkpoint number
370
+ // and the archive at that checkpoint so L1 simulation sees the correct chain tip.
371
+ const parentCheckpointNumber = CheckpointNumber(checkpointNumber - 1);
372
+ l1Overrides.forcePendingCheckpointNumber = parentCheckpointNumber;
373
+ l1Overrides.forceArchive = { checkpointNumber: parentCheckpointNumber, archive: syncedTo.archive };
374
+ this.metrics.recordPipelineDepth(1);
375
+
376
+ this.log.verbose(
377
+ `Building on top of proposed checkpoint (pending=${syncedTo.proposedCheckpointData?.checkpointNumber})`,
378
+ );
379
+ // Clear the invalidation - the proposed checkpoint should handle it.
380
+ invalidateCheckpoint = undefined;
381
+ } else if (invalidateCheckpoint) {
382
+ // After invalidation, L1 will roll back to checkpoint N-1. The archive at N-1 already
383
+ // exists on L1, so we just pass the matching archive (the lastArchive of the invalid checkpoint).
384
+ archiveForCheck = invalidateCheckpoint.lastArchive;
385
+ l1Overrides.forcePendingCheckpointNumber = invalidateCheckpoint.forcePendingCheckpointNumber;
386
+ this.metrics.recordPipelineDepth(0);
387
+ } else {
388
+ this.metrics.recordPipelineDepth(0);
389
+ }
328
390
 
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,
333
- proposer ?? EthAddress.ZERO,
334
- invalidateCheckpoint,
335
- );
391
+ const canProposeCheck = await publisher.canProposeAt(archiveForCheck, proposer ?? EthAddress.ZERO, l1Overrides);
336
392
 
337
393
  if (canProposeCheck === undefined) {
338
394
  this.log.warn(
@@ -344,10 +400,10 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
344
400
  return undefined;
345
401
  }
346
402
 
347
- if (canProposeCheck.slot !== slot) {
403
+ if (canProposeCheck.slot !== targetSlot) {
348
404
  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 },
405
+ `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}.`,
406
+ { ...logCtx, rollup: canProposeCheck, expectedSlot: targetSlot },
351
407
  );
352
408
  this.emit('proposer-rollup-check-failed', { reason: 'Slot mismatch', slot });
353
409
  this.metrics.recordCheckpointPrecheckFailed('slot_mismatch');
@@ -364,36 +420,49 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
364
420
  return undefined;
365
421
  }
366
422
 
367
- this.lastSlotForCheckpointProposalJob = slot;
368
- await this.p2pClient.prepareForSlot(slot);
369
- this.log.info(`Preparing checkpoint proposal ${checkpointNumber} at slot ${slot}`, { ...logCtx, proposer });
423
+ this.lastSlotForCheckpointProposalJob = targetSlot;
424
+
425
+ await this.p2pClient.prepareForSlot(targetSlot);
426
+ this.log.info(
427
+ `Preparing checkpoint proposal ${checkpointNumber} for target slot ${targetSlot} during wall-clock slot ${slot}`,
428
+ {
429
+ ...logCtx,
430
+ proposer,
431
+ pipeliningEnabled: this.epochCache.isProposerPipeliningEnabled(),
432
+ },
433
+ );
370
434
 
371
435
  // Create and return the checkpoint proposal job
372
436
  return this.createCheckpointProposalJob(
373
- epoch,
374
437
  slot,
438
+ targetSlot,
439
+ targetEpoch,
375
440
  checkpointNumber,
376
441
  syncedTo.blockNumber,
377
442
  proposer,
378
443
  publisher,
379
444
  attestorAddress,
380
445
  invalidateCheckpoint,
446
+ syncedTo.proposedCheckpointData,
381
447
  );
382
448
  }
383
449
 
384
450
  protected createCheckpointProposalJob(
385
- epoch: EpochNumber,
386
451
  slot: SlotNumber,
452
+ targetSlot: SlotNumber,
453
+ targetEpoch: EpochNumber,
387
454
  checkpointNumber: CheckpointNumber,
388
455
  syncedToBlockNumber: BlockNumber,
389
456
  proposer: EthAddress | undefined,
390
457
  publisher: SequencerPublisher,
391
458
  attestorAddress: EthAddress,
392
459
  invalidateCheckpoint: InvalidateCheckpointRequest | undefined,
460
+ proposedCheckpointData?: ProposedCheckpointData,
393
461
  ): CheckpointProposalJob {
394
462
  return new CheckpointProposalJob(
395
- epoch,
396
463
  slot,
464
+ targetSlot,
465
+ targetEpoch,
397
466
  checkpointNumber,
398
467
  syncedToBlockNumber,
399
468
  proposer,
@@ -419,6 +488,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
419
488
  this.setState.bind(this),
420
489
  this.tracer,
421
490
  this.log.getBindings(),
491
+ proposedCheckpointData,
422
492
  );
423
493
  }
424
494
 
@@ -475,16 +545,15 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
475
545
  * We don't check against the previous block submitted since it may have been reorg'd out.
476
546
  */
477
547
  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) {
548
+ // Check that the archiver has fully synced the L2 slot before the one we want to propose in.
549
+ // The archiver reports sync progress via L1 block timestamps and synced checkpoint slots.
550
+ // See getSyncedL2SlotNumber for how missed L1 blocks are handled.
551
+ const syncedL2Slot = await this.l2BlockSource.getSyncedL2SlotNumber();
552
+ const { slot } = args;
553
+ if (syncedL2Slot === undefined || syncedL2Slot + 1 < slot) {
484
554
  this.log.debug(`Cannot propose block at next L2 slot ${slot} due to pending sync from L1`, {
485
555
  slot,
486
- ts,
487
- l1Timestamp,
556
+ syncedL2Slot,
488
557
  });
489
558
  return undefined;
490
559
  }
@@ -494,25 +563,37 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
494
563
  number: syncSummary.latestBlockNumber,
495
564
  hash: syncSummary.latestBlockHash,
496
565
  })),
497
- this.l2BlockSource.getL2Tips().then(t => t.proposed),
566
+ this.l2BlockSource
567
+ .getL2Tips()
568
+ .then(t => ({ proposed: t.proposed, checkpointed: t.checkpointed, proposedCheckpoint: t.proposedCheckpoint })),
498
569
  this.p2pClient.getStatus().then(p2p => p2p.syncedToL2Block),
499
570
  this.l1ToL2MessageSource.getL2Tips().then(t => t.proposed),
500
571
  this.l2BlockSource.getPendingChainValidationStatus(),
572
+ this.l2BlockSource.getProposedCheckpointOnly(),
501
573
  ] as const);
502
574
 
503
- const [worldState, l2BlockSource, p2p, l1ToL2MessageSource, pendingChainValidationStatus] = syncedBlocks;
575
+ const [worldState, l2Tips, p2p, l1ToL2MessageSource, pendingChainValidationStatus, proposedCheckpointData] =
576
+ syncedBlocks;
504
577
 
505
578
  // Handle zero as a special case, since the block hash won't match across services if we're changing the prefilled data for the genesis block,
506
579
  // as the world state can compute the new genesis block hash, but other components use the hardcoded constant.
507
580
  // TODO(palla/mbps): Fix the above. All components should be able to handle dynamic genesis block hashes.
508
581
  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);
582
+ (l2Tips.proposed.number === 0 &&
583
+ worldState.number === 0 &&
584
+ p2p.number === 0 &&
585
+ l1ToL2MessageSource.number === 0) ||
586
+ (worldState.hash === l2Tips.proposed.hash &&
587
+ p2p.hash === l2Tips.proposed.hash &&
588
+ l1ToL2MessageSource.hash === l2Tips.proposed.hash);
513
589
 
514
590
  if (!result) {
515
- this.log.debug(`Sequencer sync check failed`, { worldState, l2BlockSource, p2p, l1ToL2MessageSource });
591
+ this.log.debug(`Sequencer sync check failed`, {
592
+ worldState,
593
+ l2BlockSource: l2Tips.proposed,
594
+ p2p,
595
+ l1ToL2MessageSource,
596
+ });
516
597
  return undefined;
517
598
  }
518
599
 
@@ -522,9 +603,11 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
522
603
  const archive = new Fr((await this.worldState.getCommitted().getTreeInfo(MerkleTreeId.ARCHIVE)).root);
523
604
  return {
524
605
  checkpointNumber: CheckpointNumber.ZERO,
606
+ checkpointedCheckpointNumber: CheckpointNumber.ZERO,
525
607
  blockNumber: BlockNumber.ZERO,
526
608
  archive,
527
- l1Timestamp,
609
+ hasProposedCheckpoint: false,
610
+ syncedL2Slot,
528
611
  pendingChainValidationStatus,
529
612
  };
530
613
  }
@@ -536,12 +619,17 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
536
619
  return undefined;
537
620
  }
538
621
 
622
+ const hasProposedCheckpoint = l2Tips.proposedCheckpoint.checkpoint.number > l2Tips.checkpointed.checkpoint.number;
623
+
539
624
  return {
540
625
  blockData,
541
626
  blockNumber: blockData.header.getBlockNumber(),
542
627
  checkpointNumber: blockData.checkpointNumber,
628
+ checkpointedCheckpointNumber: l2Tips.checkpointed.checkpoint.number,
543
629
  archive: blockData.archive.root,
544
- l1Timestamp,
630
+ hasProposedCheckpoint,
631
+ proposedCheckpointData,
632
+ syncedL2Slot,
545
633
  pendingChainValidationStatus,
546
634
  };
547
635
  }
@@ -550,20 +638,20 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
550
638
  * Checks if we are the proposer for the next slot.
551
639
  * @returns True if we can propose, and the proposer address (undefined if anyone can propose)
552
640
  */
553
- protected async checkCanPropose(slot: SlotNumber): Promise<[boolean, EthAddress | undefined]> {
641
+ protected async checkCanPropose(targetSlot: SlotNumber): Promise<[boolean, EthAddress | undefined]> {
554
642
  let proposer: EthAddress | undefined;
555
643
 
556
644
  try {
557
- proposer = await this.epochCache.getProposerAttesterAddressInSlot(slot);
645
+ proposer = await this.epochCache.getProposerAttesterAddressInSlot(targetSlot);
558
646
  } catch (e) {
559
647
  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`);
648
+ if (this.lastSlotForNoCommitteeWarning !== targetSlot) {
649
+ this.lastSlotForNoCommitteeWarning = targetSlot;
650
+ this.log.warn(`Cannot propose at target slot ${targetSlot} since the committee does not exist on L1`);
563
651
  }
564
652
  return [false, undefined];
565
653
  }
566
- this.log.error(`Error getting proposer for slot ${slot}`, e);
654
+ this.log.error(`Error getting proposer for target slot ${targetSlot}`, e);
567
655
  return [false, undefined];
568
656
  }
569
657
 
@@ -580,10 +668,18 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
580
668
  const weAreProposer = validatorAddresses.some(addr => addr.equals(proposer));
581
669
 
582
670
  if (!weAreProposer) {
583
- this.log.debug(`Cannot propose at slot ${slot} since we are not a proposer`, { validatorAddresses, proposer });
671
+ this.log.debug(`Cannot propose at target slot ${targetSlot} since we are not a proposer`, {
672
+ targetSlot,
673
+ validatorAddresses,
674
+ proposer,
675
+ });
584
676
  return [false, proposer];
585
677
  }
586
678
 
679
+ this.log.info(`We are the proposer for pipeline slot ${targetSlot}`, {
680
+ targetSlot,
681
+ proposer,
682
+ });
587
683
  return [true, proposer];
588
684
  }
589
685
 
@@ -592,8 +688,8 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
592
688
  * This allows the sequencer to participate in governance/slashing votes even when it cannot build blocks.
593
689
  */
594
690
  @trackSpan('Seqeuencer.tryVoteWhenSyncFails', ({ slot }) => ({ [Attributes.SLOT_NUMBER]: slot }))
595
- protected async tryVoteWhenSyncFails(args: { slot: SlotNumber; ts: bigint }): Promise<void> {
596
- const { slot } = args;
691
+ protected async tryVoteWhenSyncFails(args: { slot: SlotNumber; targetSlot: SlotNumber; ts: bigint }): Promise<void> {
692
+ const { slot, targetSlot } = args;
597
693
 
598
694
  // Prevent duplicate attempts in the same slot
599
695
  if (this.lastSlotForFallbackVote === slot) {
@@ -621,7 +717,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
621
717
  });
622
718
 
623
719
  // Check if we're a proposer or proposal is open
624
- const [canPropose, proposer] = await this.checkCanPropose(slot);
720
+ const [canPropose, proposer] = await this.checkCanPropose(targetSlot);
625
721
  if (!canPropose) {
626
722
  this.log.trace(`Cannot vote in slot ${slot} since we are not a proposer`, { slot, proposer });
627
723
  return;
@@ -638,9 +734,9 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
638
734
  slot,
639
735
  });
640
736
 
641
- // Enqueue governance and slashing votes
737
+ // Enqueue governance and slashing votes (voter uses the target slot for L1 submission)
642
738
  const voter = new CheckpointVoter(
643
- slot,
739
+ targetSlot,
644
740
  publisher,
645
741
  attestorAddress,
646
742
  this.validatorClient,
@@ -720,7 +816,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
720
816
  syncedTo: SequencerSyncCheckResult,
721
817
  currentSlot: SlotNumber,
722
818
  ): Promise<void> {
723
- const { pendingChainValidationStatus, l1Timestamp } = syncedTo;
819
+ const { pendingChainValidationStatus, syncedL2Slot } = syncedTo;
724
820
  if (pendingChainValidationStatus.valid) {
725
821
  return;
726
822
  }
@@ -735,7 +831,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
735
831
 
736
832
  const logData = {
737
833
  invalidL1Timestamp: invalidCheckpointTimestamp,
738
- l1Timestamp,
834
+ syncedL2Slot,
739
835
  invalidCheckpoint: pendingChainValidationStatus.checkpoint,
740
836
  secondsBeforeInvalidatingBlockAsCommitteeMember,
741
837
  secondsBeforeInvalidatingBlockAsNonCommitteeMember,
@@ -880,8 +976,11 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
880
976
  type SequencerSyncCheckResult = {
881
977
  blockData?: BlockData;
882
978
  checkpointNumber: CheckpointNumber;
979
+ checkpointedCheckpointNumber: CheckpointNumber;
883
980
  blockNumber: BlockNumber;
884
981
  archive: Fr;
885
- l1Timestamp: bigint;
982
+ hasProposedCheckpoint: boolean;
983
+ proposedCheckpointData?: ProposedCheckpointData;
984
+ syncedL2Slot: SlotNumber;
886
985
  pendingChainValidationStatus: ValidateCheckpointResult;
887
986
  };