@aztec/sequencer-client 0.0.1-commit.934299a21 → 0.0.1-commit.949a33fd8

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 (84) 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 +41 -26
  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 +182 -118
  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 +46 -10
  34. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  35. package/dest/sequencer/checkpoint_proposal_job.js +671 -225
  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 +7 -1
  43. package/dest/sequencer/events.d.ts.map +1 -1
  44. package/dest/sequencer/metrics.d.ts +13 -10
  45. package/dest/sequencer/metrics.d.ts.map +1 -1
  46. package/dest/sequencer/metrics.js +45 -20
  47. package/dest/sequencer/sequencer.d.ts +38 -13
  48. package/dest/sequencer/sequencer.d.ts.map +1 -1
  49. package/dest/sequencer/sequencer.js +167 -79
  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/dest/test/utils.d.ts +1 -1
  59. package/dest/test/utils.d.ts.map +1 -1
  60. package/dest/test/utils.js +7 -6
  61. package/package.json +27 -28
  62. package/src/client/sequencer-client.ts +61 -28
  63. package/src/config.ts +49 -27
  64. package/src/global_variable_builder/README.md +44 -0
  65. package/src/global_variable_builder/fee_predictor.ts +172 -0
  66. package/src/global_variable_builder/fee_provider.ts +75 -0
  67. package/src/global_variable_builder/global_builder.ts +26 -62
  68. package/src/global_variable_builder/index.ts +3 -1
  69. package/src/publisher/config.ts +38 -4
  70. package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +3 -1
  71. package/src/publisher/sequencer-publisher-factory.ts +18 -6
  72. package/src/publisher/sequencer-publisher.ts +265 -171
  73. package/src/sequencer/README.md +82 -13
  74. package/src/sequencer/chain_state_overrides.ts +87 -0
  75. package/src/sequencer/checkpoint_proposal_job.ts +823 -251
  76. package/src/sequencer/checkpoint_proposal_job_metrics.ts +128 -0
  77. package/src/sequencer/checkpoint_voter.ts +1 -12
  78. package/src/sequencer/events.ts +6 -1
  79. package/src/sequencer/metrics.ts +57 -24
  80. package/src/sequencer/sequencer.ts +233 -88
  81. package/src/sequencer/timetable.ts +64 -52
  82. package/src/sequencer/types.ts +1 -1
  83. package/src/test/mock_checkpoint_builder.ts +51 -48
  84. package/src/test/utils.ts +28 -10
@@ -1,20 +1,21 @@
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';
8
8
  import { EthAddress } from '@aztec/foundation/eth-address';
9
- import { createLogger } from '@aztec/foundation/log';
9
+ import { type Logger, createLogger } from '@aztec/foundation/log';
10
10
  import { RunningPromise } from '@aztec/foundation/running-promise';
11
11
  import type { DateProvider } from '@aztec/foundation/timer';
12
12
  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 type { ChainConfig } from '@aztec/stdlib/config';
18
+ import { getSlotStartBuildTimestamp } from '@aztec/stdlib/epoch-helpers';
18
19
  import {
19
20
  type ResolvedSequencerConfig,
20
21
  type SequencerConfig,
@@ -22,6 +23,7 @@ import {
22
23
  type WorldStateSynchronizer,
23
24
  } from '@aztec/stdlib/interfaces/server';
24
25
  import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
26
+ import type { CoordinationSignatureContext } from '@aztec/stdlib/p2p';
25
27
  import { pickFromSchema } from '@aztec/stdlib/schemas';
26
28
  import { MerkleTreeId } from '@aztec/stdlib/trees';
27
29
  import { Attributes, type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
@@ -34,6 +36,7 @@ import type { GlobalVariableBuilder } from '../global_variable_builder/global_bu
34
36
  import type { SequencerPublisherFactory } from '../publisher/sequencer-publisher-factory.js';
35
37
  import type { InvalidateCheckpointRequest, SequencerPublisher } from '../publisher/sequencer-publisher.js';
36
38
  import { CheckpointProposalJob } from './checkpoint_proposal_job.js';
39
+ import { CheckpointProposalJobMetrics } from './checkpoint_proposal_job_metrics.js';
37
40
  import { CheckpointVoter } from './checkpoint_voter.js';
38
41
  import { SequencerInterruptedError, SequencerTooSlowError } from './errors.js';
39
42
  import type { SequencerEvents } from './events.js';
@@ -55,7 +58,11 @@ export { SequencerState };
55
58
  export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<SequencerEvents>) {
56
59
  private runningPromise?: RunningPromise;
57
60
  private state = SequencerState.STOPPED;
61
+ private stateSlotNumber: SlotNumber | undefined;
62
+ private stateEnteredAtMs = performance.now();
58
63
  private metrics: SequencerMetrics;
64
+ private checkpointProposalJobMetrics: CheckpointProposalJobMetrics;
65
+ private readonly stateLog: Logger;
59
66
 
60
67
  /** The last slot for which we attempted to perform our voting duties with degraded block production */
61
68
  private lastSlotForFallbackVote: SlotNumber | undefined;
@@ -72,11 +79,15 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
72
79
  /** The last epoch for which we logged strategy comparison in fisherman mode. */
73
80
  private lastEpochForStrategyComparison: EpochNumber | undefined;
74
81
 
82
+ /** The last checkpoint proposal job, tracked so we can await its pending L1 submission during shutdown. */
83
+ protected lastCheckpointProposalJob: CheckpointProposalJob | undefined;
84
+
75
85
  /** The maximum number of seconds that the sequencer can be into a slot to transition to a particular state. */
76
86
  protected timetable!: SequencerTimetable;
77
87
 
78
88
  /** Config for the sequencer */
79
89
  protected config: ResolvedSequencerConfig = DefaultSequencerConfig;
90
+ private readonly signatureContext: CoordinationSignatureContext;
80
91
 
81
92
  constructor(
82
93
  protected publisherFactory: SequencerPublisherFactory,
@@ -92,34 +103,42 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
92
103
  protected dateProvider: DateProvider,
93
104
  protected epochCache: EpochCache,
94
105
  protected rollupContract: RollupContract,
95
- config: SequencerConfig,
106
+ config: SequencerConfig & Pick<ChainConfig, 'l1ChainId' | 'l1Contracts'>,
96
107
  protected telemetry: TelemetryClient = getTelemetryClient(),
97
108
  protected log = createLogger('sequencer'),
98
109
  ) {
99
110
  super();
111
+ this.stateLog = log.createChild('state');
100
112
 
101
113
  // Add [FISHERMAN] prefix to logger if in fisherman mode
102
114
  if (config.fishermanMode) {
103
115
  this.log = log.createChild('[FISHERMAN]');
104
116
  }
105
117
 
118
+ this.signatureContext = {
119
+ chainId: config.l1ChainId,
120
+ rollupAddress: config.l1Contracts.rollupAddress,
121
+ };
106
122
  this.metrics = new SequencerMetrics(telemetry, this.rollupContract, 'Sequencer');
123
+ this.checkpointProposalJobMetrics = new CheckpointProposalJobMetrics(telemetry);
107
124
  this.updateConfig(config);
108
125
  }
109
126
 
110
127
  /** Updates sequencer config by the defined values and updates the timetable */
111
128
  public updateConfig(config: Partial<SequencerConfig>) {
112
129
  const filteredConfig = pickFromSchema(config, SequencerConfigSchema);
113
- this.log.info(`Updated sequencer config`, omit(filteredConfig, 'txPublicSetupAllowList'));
130
+ this.log.info(`Updated sequencer config`, omit(filteredConfig, 'txPublicSetupAllowListExtend'));
114
131
  this.config = merge(this.config, filteredConfig);
132
+ const p2pPropagationTime = this.config.attestationPropagationTime;
115
133
  this.timetable = new SequencerTimetable(
116
134
  {
117
135
  ethereumSlotDuration: this.l1Constants.ethereumSlotDuration,
118
136
  aztecSlotDuration: this.aztecSlotDuration,
119
137
  l1PublishingTime: this.l1PublishingTime,
120
- p2pPropagationTime: this.config.attestationPropagationTime,
138
+ p2pPropagationTime,
121
139
  blockDurationMs: this.config.blockDurationMs,
122
140
  enforce: this.config.enforceTimeTable,
141
+ pipelining: this.epochCache.isProposerPipeliningEnabled(),
123
142
  },
124
143
  this.metrics,
125
144
  this.log,
@@ -143,12 +162,18 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
143
162
  this.log.info('Started sequencer');
144
163
  }
145
164
 
165
+ /** Triggers an immediate run of the sequencer, bypassing the polling interval. */
166
+ public trigger() {
167
+ return this.runningPromise?.trigger();
168
+ }
169
+
146
170
  /** Stops the sequencer from building blocks and moves to STOPPED state. */
147
171
  public async stop(): Promise<void> {
148
172
  this.log.info(`Stopping sequencer`);
149
173
  this.setState(SequencerState.STOPPING, undefined, { force: true });
150
- this.publisherFactory.interruptAll();
174
+ await this.publisherFactory.stopAll();
151
175
  await this.runningPromise?.stop();
176
+ await this.lastCheckpointProposalJob?.awaitPendingSubmission();
152
177
  this.setState(SequencerState.STOPPED, undefined, { force: true });
153
178
  this.log.info('Stopped sequencer');
154
179
  }
@@ -192,14 +217,25 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
192
217
  @trackSpan('Sequencer.work')
193
218
  protected async work() {
194
219
  this.setState(SequencerState.SYNCHRONIZING, undefined);
195
- const { slot, ts, now, epoch } = this.epochCache.getEpochAndSlotInNextL1Slot();
220
+ const { slot, ts, nowSeconds, epoch } = this.epochCache.getEpochAndSlotInNextL1Slot();
221
+ const { slot: targetSlot, epoch: targetEpoch } = this.epochCache.getTargetEpochAndSlotInNextL1Slot();
196
222
 
197
223
  // 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);
224
+ const checkpointProposalJob = await this.prepareCheckpointProposal(
225
+ slot,
226
+ targetSlot,
227
+ epoch,
228
+ targetEpoch,
229
+ ts,
230
+ nowSeconds,
231
+ );
199
232
  if (!checkpointProposalJob) {
200
233
  return;
201
234
  }
202
235
 
236
+ // Track the job so we can await its pending L1 submission during shutdown
237
+ this.lastCheckpointProposalJob = checkpointProposalJob;
238
+
203
239
  // Execute the checkpoint proposal job
204
240
  const checkpoint = await checkpointProposalJob.execute();
205
241
 
@@ -208,13 +244,13 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
208
244
  this.lastCheckpointProposed = checkpoint;
209
245
  }
210
246
 
211
- // Log fee strategy comparison if on fisherman
247
+ // Log fee strategy comparison if on fisherman (uses target epoch since we mirror the proposer's perspective)
212
248
  if (
213
249
  this.config.fishermanMode &&
214
- (this.lastEpochForStrategyComparison === undefined || epoch > this.lastEpochForStrategyComparison)
250
+ (this.lastEpochForStrategyComparison === undefined || targetEpoch > this.lastEpochForStrategyComparison)
215
251
  ) {
216
- this.logStrategyComparison(epoch, checkpointProposalJob.getPublisher());
217
- this.lastEpochForStrategyComparison = epoch;
252
+ this.logStrategyComparison(targetEpoch, checkpointProposalJob.getPublisher());
253
+ this.lastEpochForStrategyComparison = targetEpoch;
218
254
  }
219
255
 
220
256
  return checkpoint;
@@ -226,44 +262,49 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
226
262
  * @returns CheckpointProposalJob if successful, undefined if we are not yet synced or are not the proposer.
227
263
  */
228
264
  @trackSpan('Sequencer.prepareCheckpointProposal')
229
- private async prepareCheckpointProposal(
230
- epoch: EpochNumber,
265
+ protected async prepareCheckpointProposal(
231
266
  slot: SlotNumber,
267
+ targetSlot: SlotNumber,
268
+ epoch: EpochNumber,
269
+ targetEpoch: EpochNumber,
232
270
  ts: bigint,
233
- now: bigint,
271
+ nowSeconds: bigint,
234
272
  ): Promise<CheckpointProposalJob | undefined> {
235
- // Check we have not already processed this slot (cheapest check)
273
+ // Check we have not already processed this target slot (cheapest check)
236
274
  // We only check this if enforce timetable is set, since we want to keep processing the same slot if we are not
237
275
  // running against actual time (eg when we use sandbox-style automining)
238
276
  if (
239
277
  this.lastSlotForCheckpointProposalJob &&
240
- this.lastSlotForCheckpointProposalJob >= slot &&
278
+ this.lastSlotForCheckpointProposalJob >= targetSlot &&
241
279
  this.config.enforceTimeTable
242
280
  ) {
243
- this.log.trace(`Slot ${slot} has already been processed`);
281
+ this.log.trace(`Target slot ${targetSlot} has already been processed`);
244
282
  return undefined;
245
283
  }
246
284
 
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}`);
285
+ // But if we have already proposed for this slot, then we definitely have to skip it, automining or not
286
+ if (this.lastCheckpointProposed && this.lastCheckpointProposed.header.slotNumber >= targetSlot) {
287
+ this.log.trace(
288
+ `Slot ${targetSlot} has already been published as checkpoint ${this.lastCheckpointProposed.number}`,
289
+ );
250
290
  return undefined;
251
291
  }
252
292
 
253
293
  // Check all components are synced to latest as seen by the archiver (queries all subsystems)
254
294
  const syncedTo = await this.checkSync({ ts, slot });
255
295
  if (!syncedTo) {
256
- await this.tryVoteWhenSyncFails({ slot, ts });
296
+ await this.tryVoteWhenSyncFails({ slot, targetSlot, ts });
257
297
  return undefined;
258
298
  }
259
299
 
260
- // If escape hatch is open for this epoch, do not start checkpoint proposal work and do not attempt invalidations.
300
+ // If escape hatch is open for the target epoch, do not start checkpoint proposal work and do not attempt invalidations.
261
301
  // Still perform governance/slashing voting (as proposer) once per slot.
262
- const isEscapeHatchOpen = await this.epochCache.isEscapeHatchOpen(epoch);
302
+ // When pipelining, we check the target epoch (slot+1's epoch) since that's the epoch we're building for.
303
+ const isEscapeHatchOpen = await this.epochCache.isEscapeHatchOpen(targetEpoch);
263
304
 
264
305
  if (isEscapeHatchOpen) {
265
306
  this.setState(SequencerState.PROPOSER_CHECK, slot);
266
- const [canPropose, proposer] = await this.checkCanPropose(slot);
307
+ const [canPropose, proposer] = await this.checkCanPropose(targetSlot);
267
308
  if (canPropose) {
268
309
  await this.tryVoteWhenEscapeHatchOpen({ slot, proposer });
269
310
  } else {
@@ -280,18 +321,18 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
280
321
  const checkpointNumber = CheckpointNumber(syncedTo.checkpointNumber + 1);
281
322
 
282
323
  const logCtx = {
283
- now,
284
- syncedToL1Ts: syncedTo.l1Timestamp,
285
- syncedToL2Slot: getSlotAtTimestamp(syncedTo.l1Timestamp, this.l1Constants),
324
+ nowSeconds,
325
+ syncedToL2Slot: syncedTo.syncedL2Slot,
286
326
  slot,
327
+ targetSlot,
287
328
  slotTs: ts,
288
329
  checkpointNumber,
289
330
  isPendingChainValid: pick(syncedTo.pendingChainValidationStatus, 'valid', 'reason', 'invalidIndex'),
290
331
  };
291
332
 
292
- // Check that we are a proposer for the next slot
333
+ // Check that we are a proposer for the target slot.
293
334
  this.setState(SequencerState.PROPOSER_CHECK, slot);
294
- const [canPropose, proposer] = await this.checkCanPropose(slot);
335
+ const [canPropose, proposer] = await this.checkCanPropose(targetSlot);
295
336
 
296
337
  // If we are not a proposer check if we should invalidate an invalid checkpoint, and bail
297
338
  if (!canPropose) {
@@ -299,10 +340,20 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
299
340
  return undefined;
300
341
  }
301
342
 
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) {
343
+ // Guard: don't exceed 1-deep pipeline. Without a proposed checkpoint, we can only build
344
+ // confirmed + 1. With a proposed checkpoint, we can build confirmed + 2.
345
+ const confirmedCkpt = syncedTo.checkpointedCheckpointNumber;
346
+ if (checkpointNumber > confirmedCkpt + 2) {
347
+ this.log.verbose(
348
+ `Skipping slot ${targetSlot}: checkpoint ${checkpointNumber} exceeds max pipeline depth (confirmed=${confirmedCkpt})`,
349
+ );
350
+ return undefined;
351
+ }
352
+
353
+ // Check that the target slot is not taken by a block already (should never happen, since only us can propose for this slot)
354
+ if (syncedTo.blockData && syncedTo.blockData.header.getSlot() >= targetSlot) {
304
355
  this.log.warn(
305
- `Cannot propose block at next L2 slot ${slot} since that slot was taken by block ${syncedTo.blockNumber}`,
356
+ `Cannot propose block at target slot ${targetSlot} since that slot was taken by block ${syncedTo.blockNumber}`,
306
357
  { ...logCtx, block: syncedTo.blockData.header.toInspect() },
307
358
  );
308
359
  this.metrics.recordCheckpointPrecheckFailed('slot_already_taken');
@@ -324,14 +375,41 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
324
375
  }
325
376
 
326
377
  // Prepare invalidation request if the pending chain is invalid (returns undefined if no need)
327
- const invalidateCheckpoint = await publisher.simulateInvalidateCheckpoint(syncedTo.pendingChainValidationStatus);
378
+ let invalidateCheckpoint = await publisher.simulateInvalidateCheckpoint(syncedTo.pendingChainValidationStatus);
379
+
380
+ // Determine the correct archive and L1 state overrides for the canProposeAt check.
381
+ // The L1 contract reads archives[proposedCheckpointNumber] and compares it with the provided archive.
382
+ // When invalidating or pipelining, the local archive may differ from L1's, so we adjust accordingly.
383
+ let archiveForCheck = syncedTo.archive;
384
+ const l1SimulationOverridesBuilder = new SimulationOverridesBuilder();
385
+
386
+ if (this.epochCache.isProposerPipeliningEnabled() && syncedTo.hasProposedCheckpoint) {
387
+ // Parent checkpoint hasn't landed on L1 yet. Override both the proposed checkpoint number
388
+ // and the archive at that checkpoint so L1 simulation sees the correct chain tip.
389
+ const parentCheckpointNumber = CheckpointNumber(checkpointNumber - 1);
390
+ l1SimulationOverridesBuilder.forPendingCheckpoint(parentCheckpointNumber).withPendingArchive(syncedTo.archive);
391
+ this.metrics.recordPipelineDepth(syncedTo.checkpointNumber - syncedTo.checkpointedCheckpointNumber);
392
+
393
+ this.log.verbose(
394
+ `Building on top of proposed checkpoint (pending=${syncedTo.proposedCheckpointData?.checkpointNumber})`,
395
+ );
396
+ // Clear the invalidation - the proposed checkpoint should handle it.
397
+ invalidateCheckpoint = undefined;
398
+ } else if (invalidateCheckpoint) {
399
+ // After invalidation, L1 will roll back to checkpoint N-1. The archive at N-1 already
400
+ // exists on L1, so we just pass the matching archive (the lastArchive of the invalid checkpoint).
401
+ archiveForCheck = invalidateCheckpoint.lastArchive;
402
+ l1SimulationOverridesBuilder.forPendingCheckpoint(invalidateCheckpoint.forcePendingCheckpointNumber);
403
+ this.metrics.recordPipelineDepth(0);
404
+ } else {
405
+ this.metrics.recordPipelineDepth(0);
406
+ }
328
407
 
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,
408
+ const simulationOverridesPlan = l1SimulationOverridesBuilder.build();
409
+ const canProposeCheck = await publisher.canProposeAt(
410
+ archiveForCheck,
333
411
  proposer ?? EthAddress.ZERO,
334
- invalidateCheckpoint,
412
+ simulationOverridesPlan,
335
413
  );
336
414
 
337
415
  if (canProposeCheck === undefined) {
@@ -344,10 +422,10 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
344
422
  return undefined;
345
423
  }
346
424
 
347
- if (canProposeCheck.slot !== slot) {
425
+ if (canProposeCheck.slot !== targetSlot) {
348
426
  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 },
427
+ `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}.`,
428
+ { ...logCtx, rollup: canProposeCheck, expectedSlot: targetSlot },
351
429
  );
352
430
  this.emit('proposer-rollup-check-failed', { reason: 'Slot mismatch', slot });
353
431
  this.metrics.recordCheckpointPrecheckFailed('slot_mismatch');
@@ -364,36 +442,49 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
364
442
  return undefined;
365
443
  }
366
444
 
367
- this.lastSlotForCheckpointProposalJob = slot;
368
- await this.p2pClient.prepareForSlot(slot);
369
- this.log.info(`Preparing checkpoint proposal ${checkpointNumber} at slot ${slot}`, { ...logCtx, proposer });
445
+ this.lastSlotForCheckpointProposalJob = targetSlot;
446
+
447
+ await this.p2pClient.prepareForSlot(targetSlot);
448
+ this.log.info(
449
+ `Preparing checkpoint proposal ${checkpointNumber} for target slot ${targetSlot} during wall-clock slot ${slot}`,
450
+ {
451
+ ...logCtx,
452
+ proposer,
453
+ pipeliningEnabled: this.epochCache.isProposerPipeliningEnabled(),
454
+ },
455
+ );
370
456
 
371
457
  // Create and return the checkpoint proposal job
372
458
  return this.createCheckpointProposalJob(
373
- epoch,
374
459
  slot,
460
+ targetSlot,
461
+ targetEpoch,
375
462
  checkpointNumber,
376
463
  syncedTo.blockNumber,
377
464
  proposer,
378
465
  publisher,
379
466
  attestorAddress,
380
467
  invalidateCheckpoint,
468
+ syncedTo.proposedCheckpointData,
381
469
  );
382
470
  }
383
471
 
384
472
  protected createCheckpointProposalJob(
385
- epoch: EpochNumber,
386
473
  slot: SlotNumber,
474
+ targetSlot: SlotNumber,
475
+ targetEpoch: EpochNumber,
387
476
  checkpointNumber: CheckpointNumber,
388
477
  syncedToBlockNumber: BlockNumber,
389
478
  proposer: EthAddress | undefined,
390
479
  publisher: SequencerPublisher,
391
480
  attestorAddress: EthAddress,
392
481
  invalidateCheckpoint: InvalidateCheckpointRequest | undefined,
482
+ proposedCheckpointData?: ProposedCheckpointData,
393
483
  ): CheckpointProposalJob {
394
484
  return new CheckpointProposalJob(
395
- epoch,
396
485
  slot,
486
+ targetSlot,
487
+ targetEpoch,
397
488
  checkpointNumber,
398
489
  syncedToBlockNumber,
399
490
  proposer,
@@ -409,16 +500,19 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
409
500
  this.checkpointsBuilder,
410
501
  this.l2BlockSource,
411
502
  this.l1Constants,
503
+ this.signatureContext,
412
504
  this.config,
413
505
  this.timetable,
414
506
  this.slasherClient,
415
507
  this.epochCache,
416
508
  this.dateProvider,
417
509
  this.metrics,
510
+ this.checkpointProposalJobMetrics.createRecorder(),
418
511
  this,
419
512
  this.setState.bind(this),
420
513
  this.tracer,
421
514
  this.log.getBindings(),
515
+ proposedCheckpointData,
422
516
  );
423
517
  }
424
518
 
@@ -454,19 +548,35 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
454
548
  this.timetable.assertTimeLeft(proposedState, secondsIntoSlot);
455
549
  }
456
550
 
551
+ const oldState = this.state;
552
+ const oldStateSlotNumber = this.stateSlotNumber;
553
+ const stateChanged = proposedState !== oldState;
554
+ const transitionAtMs = performance.now();
555
+ const stateDurationMs = transitionAtMs - this.stateEnteredAtMs;
556
+
457
557
  const boringStates = [SequencerState.IDLE, SequencerState.SYNCHRONIZING];
458
558
  const logLevel =
459
- boringStates.includes(proposedState) && boringStates.includes(this.state)
460
- ? ('trace' as const)
461
- : ('debug' as const);
462
- this.log[logLevel](`Transitioning from ${this.state} to ${proposedState}`, { slotNumber, secondsIntoSlot });
559
+ boringStates.includes(proposedState) && boringStates.includes(oldState) ? ('trace' as const) : ('debug' as const);
560
+ this.stateLog[logLevel](`Transitioning from ${oldState} to ${proposedState}`, {
561
+ oldState,
562
+ newState: proposedState,
563
+ slotNumber,
564
+ stateSlotNumber: oldStateSlotNumber,
565
+ secondsIntoSlot,
566
+ ...(stateChanged && { stateDurationMs: Math.ceil(stateDurationMs) }),
567
+ });
463
568
 
464
569
  this.emit('state-changed', {
465
- oldState: this.state,
570
+ oldState,
466
571
  newState: proposedState,
467
572
  secondsIntoSlot,
468
573
  slot: slotNumber,
469
574
  });
575
+ if (stateChanged) {
576
+ this.metrics.recordStateDuration(stateDurationMs, oldState);
577
+ this.stateEnteredAtMs = transitionAtMs;
578
+ this.stateSlotNumber = slotNumber;
579
+ }
470
580
  this.state = proposedState;
471
581
  }
472
582
 
@@ -475,16 +585,15 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
475
585
  * We don't check against the previous block submitted since it may have been reorg'd out.
476
586
  */
477
587
  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) {
588
+ // Check that the archiver has fully synced the L2 slot before the one we want to propose in.
589
+ // The archiver reports sync progress via L1 block timestamps and synced checkpoint slots.
590
+ // See getSyncedL2SlotNumber for how missed L1 blocks are handled.
591
+ const syncedL2Slot = await this.l2BlockSource.getSyncedL2SlotNumber();
592
+ const { slot } = args;
593
+ if (syncedL2Slot === undefined || syncedL2Slot + 1 < slot) {
484
594
  this.log.debug(`Cannot propose block at next L2 slot ${slot} due to pending sync from L1`, {
485
595
  slot,
486
- ts,
487
- l1Timestamp,
596
+ syncedL2Slot,
488
597
  });
489
598
  return undefined;
490
599
  }
@@ -494,25 +603,43 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
494
603
  number: syncSummary.latestBlockNumber,
495
604
  hash: syncSummary.latestBlockHash,
496
605
  })),
497
- this.l2BlockSource.getL2Tips().then(t => t.proposed),
606
+ this.l2BlockSource
607
+ .getL2Tips()
608
+ .then(t => ({ proposed: t.proposed, checkpointed: t.checkpointed, proposedCheckpoint: t.proposedCheckpoint })),
498
609
  this.p2pClient.getStatus().then(p2p => p2p.syncedToL2Block),
499
- this.l1ToL2MessageSource.getL2Tips().then(t => t.proposed),
610
+ this.l1ToL2MessageSource.getL2Tips().then(t => ({ proposed: t.proposed, checkpointed: t.checkpointed })),
500
611
  this.l2BlockSource.getPendingChainValidationStatus(),
612
+ this.l2BlockSource.getLastProposedCheckpoint(),
501
613
  ] as const);
502
614
 
503
- const [worldState, l2BlockSource, p2p, l1ToL2MessageSource, pendingChainValidationStatus] = syncedBlocks;
615
+ const [worldState, l2Tips, p2p, l1ToL2MessageSourceTips, pendingChainValidationStatus, proposedCheckpointData] =
616
+ syncedBlocks;
504
617
 
505
618
  // 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
619
  // as the world state can compute the new genesis block hash, but other components use the hardcoded constant.
507
620
  // TODO(palla/mbps): Fix the above. All components should be able to handle dynamic genesis block hashes.
508
621
  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);
622
+ (l2Tips.proposed.number === 0 &&
623
+ l2Tips.checkpointed.block.number === 0 &&
624
+ l2Tips.checkpointed.checkpoint.number === 0 &&
625
+ worldState.number === 0 &&
626
+ p2p.number === 0 &&
627
+ l1ToL2MessageSourceTips.proposed.number === 0 &&
628
+ l1ToL2MessageSourceTips.checkpointed.block.number === 0 &&
629
+ l1ToL2MessageSourceTips.checkpointed.checkpoint.number === 0) ||
630
+ (worldState.hash === l2Tips.proposed.hash &&
631
+ p2p.hash === l2Tips.proposed.hash &&
632
+ l1ToL2MessageSourceTips.proposed.hash === l2Tips.proposed.hash &&
633
+ l1ToL2MessageSourceTips.checkpointed.block.hash === l2Tips.checkpointed.block.hash &&
634
+ l1ToL2MessageSourceTips.checkpointed.checkpoint.hash === l2Tips.checkpointed.checkpoint.hash);
513
635
 
514
636
  if (!result) {
515
- this.log.debug(`Sequencer sync check failed`, { worldState, l2BlockSource, p2p, l1ToL2MessageSource });
637
+ this.log.debug(`Sequencer sync check failed`, {
638
+ worldState,
639
+ l2BlockSource: l2Tips.proposed,
640
+ p2p,
641
+ l1ToL2MessageSourceTips,
642
+ });
516
643
  return undefined;
517
644
  }
518
645
 
@@ -522,9 +649,11 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
522
649
  const archive = new Fr((await this.worldState.getCommitted().getTreeInfo(MerkleTreeId.ARCHIVE)).root);
523
650
  return {
524
651
  checkpointNumber: CheckpointNumber.ZERO,
652
+ checkpointedCheckpointNumber: CheckpointNumber.ZERO,
525
653
  blockNumber: BlockNumber.ZERO,
526
654
  archive,
527
- l1Timestamp,
655
+ hasProposedCheckpoint: false,
656
+ syncedL2Slot,
528
657
  pendingChainValidationStatus,
529
658
  };
530
659
  }
@@ -536,12 +665,17 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
536
665
  return undefined;
537
666
  }
538
667
 
668
+ const hasProposedCheckpoint = l2Tips.proposedCheckpoint.checkpoint.number > l2Tips.checkpointed.checkpoint.number;
669
+
539
670
  return {
540
671
  blockData,
541
672
  blockNumber: blockData.header.getBlockNumber(),
542
673
  checkpointNumber: blockData.checkpointNumber,
674
+ checkpointedCheckpointNumber: l2Tips.checkpointed.checkpoint.number,
543
675
  archive: blockData.archive.root,
544
- l1Timestamp,
676
+ hasProposedCheckpoint,
677
+ proposedCheckpointData,
678
+ syncedL2Slot,
545
679
  pendingChainValidationStatus,
546
680
  };
547
681
  }
@@ -550,20 +684,20 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
550
684
  * Checks if we are the proposer for the next slot.
551
685
  * @returns True if we can propose, and the proposer address (undefined if anyone can propose)
552
686
  */
553
- protected async checkCanPropose(slot: SlotNumber): Promise<[boolean, EthAddress | undefined]> {
687
+ protected async checkCanPropose(targetSlot: SlotNumber): Promise<[boolean, EthAddress | undefined]> {
554
688
  let proposer: EthAddress | undefined;
555
689
 
556
690
  try {
557
- proposer = await this.epochCache.getProposerAttesterAddressInSlot(slot);
691
+ proposer = await this.epochCache.getProposerAttesterAddressInSlot(targetSlot);
558
692
  } catch (e) {
559
693
  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`);
694
+ if (this.lastSlotForNoCommitteeWarning !== targetSlot) {
695
+ this.lastSlotForNoCommitteeWarning = targetSlot;
696
+ this.log.warn(`Cannot propose at target slot ${targetSlot} since the committee does not exist on L1`);
563
697
  }
564
698
  return [false, undefined];
565
699
  }
566
- this.log.error(`Error getting proposer for slot ${slot}`, e);
700
+ this.log.error(`Error getting proposer for target slot ${targetSlot}`, e);
567
701
  return [false, undefined];
568
702
  }
569
703
 
@@ -580,10 +714,18 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
580
714
  const weAreProposer = validatorAddresses.some(addr => addr.equals(proposer));
581
715
 
582
716
  if (!weAreProposer) {
583
- this.log.debug(`Cannot propose at slot ${slot} since we are not a proposer`, { validatorAddresses, proposer });
717
+ this.log.debug(`Cannot propose at target slot ${targetSlot} since we are not a proposer`, {
718
+ targetSlot,
719
+ validatorAddresses,
720
+ proposer,
721
+ });
584
722
  return [false, proposer];
585
723
  }
586
724
 
725
+ this.log.info(`We are the proposer for pipeline slot ${targetSlot}`, {
726
+ targetSlot,
727
+ proposer,
728
+ });
587
729
  return [true, proposer];
588
730
  }
589
731
 
@@ -592,8 +734,8 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
592
734
  * This allows the sequencer to participate in governance/slashing votes even when it cannot build blocks.
593
735
  */
594
736
  @trackSpan('Seqeuencer.tryVoteWhenSyncFails', ({ slot }) => ({ [Attributes.SLOT_NUMBER]: slot }))
595
- protected async tryVoteWhenSyncFails(args: { slot: SlotNumber; ts: bigint }): Promise<void> {
596
- const { slot } = args;
737
+ protected async tryVoteWhenSyncFails(args: { slot: SlotNumber; targetSlot: SlotNumber; ts: bigint }): Promise<void> {
738
+ const { slot, targetSlot } = args;
597
739
 
598
740
  // Prevent duplicate attempts in the same slot
599
741
  if (this.lastSlotForFallbackVote === slot) {
@@ -621,7 +763,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
621
763
  });
622
764
 
623
765
  // Check if we're a proposer or proposal is open
624
- const [canPropose, proposer] = await this.checkCanPropose(slot);
766
+ const [canPropose, proposer] = await this.checkCanPropose(targetSlot);
625
767
  if (!canPropose) {
626
768
  this.log.trace(`Cannot vote in slot ${slot} since we are not a proposer`, { slot, proposer });
627
769
  return;
@@ -638,9 +780,9 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
638
780
  slot,
639
781
  });
640
782
 
641
- // Enqueue governance and slashing votes
783
+ // Enqueue governance and slashing votes (voter uses the target slot for L1 submission)
642
784
  const voter = new CheckpointVoter(
643
- slot,
785
+ targetSlot,
644
786
  publisher,
645
787
  attestorAddress,
646
788
  this.validatorClient,
@@ -720,7 +862,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
720
862
  syncedTo: SequencerSyncCheckResult,
721
863
  currentSlot: SlotNumber,
722
864
  ): Promise<void> {
723
- const { pendingChainValidationStatus, l1Timestamp } = syncedTo;
865
+ const { pendingChainValidationStatus, syncedL2Slot } = syncedTo;
724
866
  if (pendingChainValidationStatus.valid) {
725
867
  return;
726
868
  }
@@ -735,7 +877,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
735
877
 
736
878
  const logData = {
737
879
  invalidL1Timestamp: invalidCheckpointTimestamp,
738
- l1Timestamp,
880
+ syncedL2Slot,
739
881
  invalidCheckpoint: pendingChainValidationStatus.checkpoint,
740
882
  secondsBeforeInvalidatingBlockAsCommitteeMember,
741
883
  secondsBeforeInvalidatingBlockAsNonCommitteeMember,
@@ -880,8 +1022,11 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
880
1022
  type SequencerSyncCheckResult = {
881
1023
  blockData?: BlockData;
882
1024
  checkpointNumber: CheckpointNumber;
1025
+ checkpointedCheckpointNumber: CheckpointNumber;
883
1026
  blockNumber: BlockNumber;
884
1027
  archive: Fr;
885
- l1Timestamp: bigint;
1028
+ hasProposedCheckpoint: boolean;
1029
+ proposedCheckpointData?: ProposedCheckpointData;
1030
+ syncedL2Slot: SlotNumber;
886
1031
  pendingChainValidationStatus: ValidateCheckpointResult;
887
1032
  };