@aztec/sequencer-client 0.0.1-commit.9d2bcf6d → 0.0.1-commit.9ebd450e8

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 (103) hide show
  1. package/dest/client/sequencer-client.d.ts +17 -7
  2. package/dest/client/sequencer-client.d.ts.map +1 -1
  3. package/dest/client/sequencer-client.js +63 -30
  4. package/dest/config.d.ts +26 -7
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +52 -36
  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 -16
  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 +47 -17
  20. package/dest/publisher/config.d.ts.map +1 -1
  21. package/dest/publisher/config.js +121 -42
  22. package/dest/publisher/index.d.ts +2 -1
  23. package/dest/publisher/index.d.ts.map +1 -1
  24. package/dest/publisher/l1_tx_failed_store/factory.d.ts +11 -0
  25. package/dest/publisher/l1_tx_failed_store/factory.d.ts.map +1 -0
  26. package/dest/publisher/l1_tx_failed_store/factory.js +22 -0
  27. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts +58 -0
  28. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -0
  29. package/dest/publisher/l1_tx_failed_store/failed_tx_store.js +1 -0
  30. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts +15 -0
  31. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts.map +1 -0
  32. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.js +34 -0
  33. package/dest/publisher/l1_tx_failed_store/index.d.ts +4 -0
  34. package/dest/publisher/l1_tx_failed_store/index.d.ts.map +1 -0
  35. package/dest/publisher/l1_tx_failed_store/index.js +2 -0
  36. package/dest/publisher/sequencer-publisher-factory.d.ts +11 -5
  37. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  38. package/dest/publisher/sequencer-publisher-factory.js +27 -3
  39. package/dest/publisher/sequencer-publisher.d.ts +75 -48
  40. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  41. package/dest/publisher/sequencer-publisher.js +432 -139
  42. package/dest/sequencer/chain_state_overrides.d.ts +25 -0
  43. package/dest/sequencer/chain_state_overrides.d.ts.map +1 -0
  44. package/dest/sequencer/chain_state_overrides.js +39 -0
  45. package/dest/sequencer/checkpoint_proposal_job.d.ts +42 -10
  46. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  47. package/dest/sequencer/checkpoint_proposal_job.js +531 -216
  48. package/dest/sequencer/checkpoint_proposal_job_metrics.d.ts +34 -0
  49. package/dest/sequencer/checkpoint_proposal_job_metrics.d.ts.map +1 -0
  50. package/dest/sequencer/checkpoint_proposal_job_metrics.js +72 -0
  51. package/dest/sequencer/checkpoint_voter.d.ts +1 -2
  52. package/dest/sequencer/checkpoint_voter.d.ts.map +1 -1
  53. package/dest/sequencer/checkpoint_voter.js +2 -5
  54. package/dest/sequencer/events.d.ts +7 -1
  55. package/dest/sequencer/events.d.ts.map +1 -1
  56. package/dest/sequencer/metrics.d.ts +21 -8
  57. package/dest/sequencer/metrics.d.ts.map +1 -1
  58. package/dest/sequencer/metrics.js +110 -21
  59. package/dest/sequencer/sequencer.d.ts +43 -17
  60. package/dest/sequencer/sequencer.d.ts.map +1 -1
  61. package/dest/sequencer/sequencer.js +158 -96
  62. package/dest/sequencer/timetable.d.ts +17 -6
  63. package/dest/sequencer/timetable.d.ts.map +1 -1
  64. package/dest/sequencer/timetable.js +51 -46
  65. package/dest/sequencer/types.d.ts +2 -2
  66. package/dest/sequencer/types.d.ts.map +1 -1
  67. package/dest/test/index.d.ts +3 -5
  68. package/dest/test/index.d.ts.map +1 -1
  69. package/dest/test/mock_checkpoint_builder.d.ts +11 -11
  70. package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
  71. package/dest/test/mock_checkpoint_builder.js +45 -34
  72. package/dest/test/utils.d.ts +3 -3
  73. package/dest/test/utils.d.ts.map +1 -1
  74. package/dest/test/utils.js +5 -4
  75. package/package.json +27 -28
  76. package/src/client/sequencer-client.ts +81 -30
  77. package/src/config.ts +70 -46
  78. package/src/global_variable_builder/README.md +44 -0
  79. package/src/global_variable_builder/fee_predictor.ts +172 -0
  80. package/src/global_variable_builder/fee_provider.ts +75 -0
  81. package/src/global_variable_builder/global_builder.ts +27 -63
  82. package/src/global_variable_builder/index.ts +3 -1
  83. package/src/publisher/config.ts +157 -45
  84. package/src/publisher/index.ts +3 -0
  85. package/src/publisher/l1_tx_failed_store/factory.ts +32 -0
  86. package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +57 -0
  87. package/src/publisher/l1_tx_failed_store/file_store_failed_tx_store.ts +46 -0
  88. package/src/publisher/l1_tx_failed_store/index.ts +3 -0
  89. package/src/publisher/sequencer-publisher-factory.ts +38 -9
  90. package/src/publisher/sequencer-publisher.ts +499 -198
  91. package/src/sequencer/README.md +83 -13
  92. package/src/sequencer/chain_state_overrides.ts +87 -0
  93. package/src/sequencer/checkpoint_proposal_job.ts +678 -237
  94. package/src/sequencer/checkpoint_proposal_job_metrics.ts +128 -0
  95. package/src/sequencer/checkpoint_voter.ts +1 -12
  96. package/src/sequencer/events.ts +6 -1
  97. package/src/sequencer/metrics.ts +124 -26
  98. package/src/sequencer/sequencer.ts +227 -108
  99. package/src/sequencer/timetable.ts +70 -57
  100. package/src/sequencer/types.ts +1 -1
  101. package/src/test/index.ts +2 -4
  102. package/src/test/mock_checkpoint_builder.ts +63 -49
  103. package/src/test/utils.ts +5 -2
@@ -1,7 +1,7 @@
1
1
  import { getKzg } from '@aztec/blob-lib';
2
2
  import { INITIAL_L2_BLOCK_NUM } from '@aztec/constants';
3
3
  import type { EpochCache } from '@aztec/epoch-cache';
4
- import { NoCommitteeError, type RollupContract } from '@aztec/ethereum/contracts';
4
+ import { NoCommitteeError, type RollupContract, SimulationOverridesBuilder } from '@aztec/ethereum/contracts';
5
5
  import { BlockNumber, CheckpointNumber, EpochNumber, SlotNumber } from '@aztec/foundation/branded-types';
6
6
  import { merge, omit, pick } from '@aztec/foundation/collection';
7
7
  import { Fr } from '@aztec/foundation/curves/bn254';
@@ -12,9 +12,9 @@ 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
- import type { L2Block, L2BlockSink, L2BlockSource, ValidateCheckpointResult } from '@aztec/stdlib/block';
16
- import type { Checkpoint } from '@aztec/stdlib/checkpoint';
17
- import { getSlotAtTimestamp, getSlotStartBuildTimestamp } from '@aztec/stdlib/epoch-helpers';
15
+ import type { BlockData, L2BlockSink, L2BlockSource, ValidateCheckpointResult } from '@aztec/stdlib/block';
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,
@@ -25,7 +25,7 @@ import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
25
25
  import { pickFromSchema } from '@aztec/stdlib/schemas';
26
26
  import { MerkleTreeId } from '@aztec/stdlib/trees';
27
27
  import { Attributes, type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
28
- import { FullNodeCheckpointsBuilder, type ValidatorClient } from '@aztec/validator-client';
28
+ import { FullNodeCheckpointsBuilder, NodeKeystoreAdapter, type ValidatorClient } from '@aztec/validator-client';
29
29
 
30
30
  import EventEmitter from 'node:events';
31
31
 
@@ -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,17 +74,12 @@ 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
 
78
- // This shouldn't be here as this gets re-created each time we build/propose a block.
79
- // But we have a number of tests that abuse/rely on this class having a permanent publisher.
80
- // As long as those tests only configure a single publisher they will continue to work.
81
- // This will get re-assigned every time the sequencer goes to build a new block to a publisher that is valid
82
- // for the block proposer.
83
- // TODO(palla/mbps): Remove this field and fix tests
84
- protected publisher: SequencerPublisher | undefined;
85
-
86
83
  /** Config for the sequencer */
87
84
  protected config: ResolvedSequencerConfig = DefaultSequencerConfig;
88
85
 
@@ -112,32 +109,34 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
112
109
  }
113
110
 
114
111
  this.metrics = new SequencerMetrics(telemetry, this.rollupContract, 'Sequencer');
112
+ this.checkpointProposalJobMetrics = new CheckpointProposalJobMetrics(telemetry);
115
113
  this.updateConfig(config);
116
114
  }
117
115
 
118
116
  /** Updates sequencer config by the defined values and updates the timetable */
119
117
  public updateConfig(config: Partial<SequencerConfig>) {
120
118
  const filteredConfig = pickFromSchema(config, SequencerConfigSchema);
121
- this.log.info(`Updated sequencer config`, omit(filteredConfig, 'txPublicSetupAllowList'));
119
+ this.log.info(`Updated sequencer config`, omit(filteredConfig, 'txPublicSetupAllowListExtend'));
122
120
  this.config = merge(this.config, filteredConfig);
121
+ const p2pPropagationTime = this.config.attestationPropagationTime;
123
122
  this.timetable = new SequencerTimetable(
124
123
  {
125
124
  ethereumSlotDuration: this.l1Constants.ethereumSlotDuration,
126
125
  aztecSlotDuration: this.aztecSlotDuration,
127
126
  l1PublishingTime: this.l1PublishingTime,
128
- p2pPropagationTime: this.config.attestationPropagationTime,
127
+ p2pPropagationTime,
129
128
  blockDurationMs: this.config.blockDurationMs,
130
129
  enforce: this.config.enforceTimeTable,
130
+ pipelining: this.epochCache.isProposerPipeliningEnabled(),
131
131
  },
132
132
  this.metrics,
133
133
  this.log,
134
134
  );
135
135
  }
136
136
 
137
- /** Initializes the sequencer (precomputes tables and creates a publisher). Takes about 3s. */
138
- public async init() {
137
+ /** Initializes the sequencer (precomputes tables). Takes about 3s. */
138
+ public init() {
139
139
  getKzg();
140
- this.publisher = (await this.publisherFactory.create(undefined)).publisher;
141
140
  }
142
141
 
143
142
  /** Starts the sequencer and moves to IDLE state. */
@@ -152,12 +151,18 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
152
151
  this.log.info('Started sequencer');
153
152
  }
154
153
 
154
+ /** Triggers an immediate run of the sequencer, bypassing the polling interval. */
155
+ public trigger() {
156
+ return this.runningPromise?.trigger();
157
+ }
158
+
155
159
  /** Stops the sequencer from building blocks and moves to STOPPED state. */
156
160
  public async stop(): Promise<void> {
157
161
  this.log.info(`Stopping sequencer`);
158
162
  this.setState(SequencerState.STOPPING, undefined, { force: true });
159
- this.publisher?.interrupt();
163
+ await this.publisherFactory.stopAll();
160
164
  await this.runningPromise?.stop();
165
+ await this.lastCheckpointProposalJob?.awaitPendingSubmission();
161
166
  this.setState(SequencerState.STOPPED, undefined, { force: true });
162
167
  this.log.info('Stopped sequencer');
163
168
  }
@@ -169,7 +174,6 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
169
174
  } catch (err) {
170
175
  this.emit('checkpoint-error', { error: err as Error });
171
176
  if (err instanceof SequencerTooSlowError) {
172
- // TODO(palla/mbps): Add missing states
173
177
  // Log as warn only if we had to abort halfway through the block proposal
174
178
  const logLvl = [SequencerState.INITIALIZING_CHECKPOINT, SequencerState.PROPOSER_CHECK].includes(
175
179
  err.proposedState,
@@ -202,14 +206,25 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
202
206
  @trackSpan('Sequencer.work')
203
207
  protected async work() {
204
208
  this.setState(SequencerState.SYNCHRONIZING, undefined);
205
- 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();
206
211
 
207
212
  // Check if we are synced and it's our slot, grab a publisher, check previous block invalidation, etc
208
- 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
+ );
209
221
  if (!checkpointProposalJob) {
210
222
  return;
211
223
  }
212
224
 
225
+ // Track the job so we can await its pending L1 submission during shutdown
226
+ this.lastCheckpointProposalJob = checkpointProposalJob;
227
+
213
228
  // Execute the checkpoint proposal job
214
229
  const checkpoint = await checkpointProposalJob.execute();
215
230
 
@@ -218,13 +233,13 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
218
233
  this.lastCheckpointProposed = checkpoint;
219
234
  }
220
235
 
221
- // 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)
222
237
  if (
223
238
  this.config.fishermanMode &&
224
- (this.lastEpochForStrategyComparison === undefined || epoch > this.lastEpochForStrategyComparison)
239
+ (this.lastEpochForStrategyComparison === undefined || targetEpoch > this.lastEpochForStrategyComparison)
225
240
  ) {
226
- this.logStrategyComparison(epoch, checkpointProposalJob.getPublisher());
227
- this.lastEpochForStrategyComparison = epoch;
241
+ this.logStrategyComparison(targetEpoch, checkpointProposalJob.getPublisher());
242
+ this.lastEpochForStrategyComparison = targetEpoch;
228
243
  }
229
244
 
230
245
  return checkpoint;
@@ -236,44 +251,49 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
236
251
  * @returns CheckpointProposalJob if successful, undefined if we are not yet synced or are not the proposer.
237
252
  */
238
253
  @trackSpan('Sequencer.prepareCheckpointProposal')
239
- private async prepareCheckpointProposal(
240
- epoch: EpochNumber,
254
+ protected async prepareCheckpointProposal(
241
255
  slot: SlotNumber,
256
+ targetSlot: SlotNumber,
257
+ epoch: EpochNumber,
258
+ targetEpoch: EpochNumber,
242
259
  ts: bigint,
243
- now: bigint,
260
+ nowSeconds: bigint,
244
261
  ): Promise<CheckpointProposalJob | undefined> {
245
- // Check we have not already processed this slot (cheapest check)
262
+ // Check we have not already processed this target slot (cheapest check)
246
263
  // We only check this if enforce timetable is set, since we want to keep processing the same slot if we are not
247
264
  // running against actual time (eg when we use sandbox-style automining)
248
265
  if (
249
266
  this.lastSlotForCheckpointProposalJob &&
250
- this.lastSlotForCheckpointProposalJob >= slot &&
267
+ this.lastSlotForCheckpointProposalJob >= targetSlot &&
251
268
  this.config.enforceTimeTable
252
269
  ) {
253
- this.log.trace(`Slot ${slot} has already been processed`);
270
+ this.log.trace(`Target slot ${targetSlot} has already been processed`);
254
271
  return undefined;
255
272
  }
256
273
 
257
- // But if we have already proposed for this slot, the we definitely have to skip it, automining or not
258
- if (this.lastCheckpointProposed && this.lastCheckpointProposed.header.slotNumber >= slot) {
259
- 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
+ );
260
279
  return undefined;
261
280
  }
262
281
 
263
282
  // Check all components are synced to latest as seen by the archiver (queries all subsystems)
264
283
  const syncedTo = await this.checkSync({ ts, slot });
265
284
  if (!syncedTo) {
266
- await this.tryVoteWhenSyncFails({ slot, ts });
285
+ await this.tryVoteWhenSyncFails({ slot, targetSlot, ts });
267
286
  return undefined;
268
287
  }
269
288
 
270
- // 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.
271
290
  // Still perform governance/slashing voting (as proposer) once per slot.
272
- 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);
273
293
 
274
294
  if (isEscapeHatchOpen) {
275
295
  this.setState(SequencerState.PROPOSER_CHECK, slot);
276
- const [canPropose, proposer] = await this.checkCanPropose(slot);
296
+ const [canPropose, proposer] = await this.checkCanPropose(targetSlot);
277
297
  if (canPropose) {
278
298
  await this.tryVoteWhenEscapeHatchOpen({ slot, proposer });
279
299
  } else {
@@ -290,18 +310,18 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
290
310
  const checkpointNumber = CheckpointNumber(syncedTo.checkpointNumber + 1);
291
311
 
292
312
  const logCtx = {
293
- now,
294
- syncedToL1Ts: syncedTo.l1Timestamp,
295
- syncedToL2Slot: getSlotAtTimestamp(syncedTo.l1Timestamp, this.l1Constants),
313
+ nowSeconds,
314
+ syncedToL2Slot: syncedTo.syncedL2Slot,
296
315
  slot,
316
+ targetSlot,
297
317
  slotTs: ts,
298
318
  checkpointNumber,
299
319
  isPendingChainValid: pick(syncedTo.pendingChainValidationStatus, 'valid', 'reason', 'invalidIndex'),
300
320
  };
301
321
 
302
- // Check that we are a proposer for the next slot
322
+ // Check that we are a proposer for the target slot.
303
323
  this.setState(SequencerState.PROPOSER_CHECK, slot);
304
- const [canPropose, proposer] = await this.checkCanPropose(slot);
324
+ const [canPropose, proposer] = await this.checkCanPropose(targetSlot);
305
325
 
306
326
  // If we are not a proposer check if we should invalidate an invalid checkpoint, and bail
307
327
  if (!canPropose) {
@@ -309,13 +329,23 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
309
329
  return undefined;
310
330
  }
311
331
 
312
- // Check that the slot is not taken by a block already (should never happen, since only us can propose for this slot)
313
- if (syncedTo.block && syncedTo.block.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) {
314
344
  this.log.warn(
315
- `Cannot propose block at next L2 slot ${slot} since that slot was taken by block ${syncedTo.blockNumber}`,
316
- { ...logCtx, block: syncedTo.block.header.toInspect() },
345
+ `Cannot propose block at target slot ${targetSlot} since that slot was taken by block ${syncedTo.blockNumber}`,
346
+ { ...logCtx, block: syncedTo.blockData.header.toInspect() },
317
347
  );
318
- this.metrics.recordBlockProposalPrecheckFailed('slot_already_taken');
348
+ this.metrics.recordCheckpointPrecheckFailed('slot_already_taken');
319
349
  return undefined;
320
350
  }
321
351
 
@@ -326,7 +356,6 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
326
356
  const proposerForPublisher = this.config.fishermanMode ? undefined : proposer;
327
357
  const { attestorAddress, publisher } = await this.publisherFactory.create(proposerForPublisher);
328
358
  this.log.verbose(`Created publisher at address ${publisher.getSenderAddress()} for attestor ${attestorAddress}`);
329
- this.publisher = publisher;
330
359
 
331
360
  // In fisherman mode, set the actual proposer's address for simulations
332
361
  if (this.config.fishermanMode && proposer) {
@@ -335,14 +364,41 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
335
364
  }
336
365
 
337
366
  // Prepare invalidation request if the pending chain is invalid (returns undefined if no need)
338
- 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
+ }
339
396
 
340
- // Check with the rollup contract if we can indeed propose at the next L2 slot. This check should not fail
341
- // if all the previous checks are good, but we do it just in case.
342
- const canProposeCheck = await publisher.canProposeAtNextEthBlock(
343
- syncedTo.archive,
397
+ const simulationOverridesPlan = l1SimulationOverridesBuilder.build();
398
+ const canProposeCheck = await publisher.canProposeAt(
399
+ archiveForCheck,
344
400
  proposer ?? EthAddress.ZERO,
345
- invalidateCheckpoint,
401
+ simulationOverridesPlan,
346
402
  );
347
403
 
348
404
  if (canProposeCheck === undefined) {
@@ -351,17 +407,17 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
351
407
  logCtx,
352
408
  );
353
409
  this.emit('proposer-rollup-check-failed', { reason: 'Rollup contract check failed', slot });
354
- this.metrics.recordBlockProposalPrecheckFailed('rollup_contract_check_failed');
410
+ this.metrics.recordCheckpointPrecheckFailed('rollup_contract_check_failed');
355
411
  return undefined;
356
412
  }
357
413
 
358
- if (canProposeCheck.slot !== slot) {
414
+ if (canProposeCheck.slot !== targetSlot) {
359
415
  this.log.warn(
360
- `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}.`,
361
- { ...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 },
362
418
  );
363
419
  this.emit('proposer-rollup-check-failed', { reason: 'Slot mismatch', slot });
364
- this.metrics.recordBlockProposalPrecheckFailed('slot_mismatch');
420
+ this.metrics.recordCheckpointPrecheckFailed('slot_mismatch');
365
421
  return undefined;
366
422
  }
367
423
 
@@ -371,39 +427,53 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
371
427
  { ...logCtx, rollup: canProposeCheck, expectedSlot: slot },
372
428
  );
373
429
  this.emit('proposer-rollup-check-failed', { reason: 'Block mismatch', slot });
374
- this.metrics.recordBlockProposalPrecheckFailed('block_number_mismatch');
430
+ this.metrics.recordCheckpointPrecheckFailed('block_number_mismatch');
375
431
  return undefined;
376
432
  }
377
433
 
378
- this.lastSlotForCheckpointProposalJob = slot;
379
- 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
+ );
380
445
 
381
446
  // Create and return the checkpoint proposal job
382
447
  return this.createCheckpointProposalJob(
383
- epoch,
384
448
  slot,
449
+ targetSlot,
450
+ targetEpoch,
385
451
  checkpointNumber,
386
452
  syncedTo.blockNumber,
387
453
  proposer,
388
454
  publisher,
389
455
  attestorAddress,
390
456
  invalidateCheckpoint,
457
+ syncedTo.proposedCheckpointData,
391
458
  );
392
459
  }
393
460
 
394
461
  protected createCheckpointProposalJob(
395
- epoch: EpochNumber,
396
462
  slot: SlotNumber,
463
+ targetSlot: SlotNumber,
464
+ targetEpoch: EpochNumber,
397
465
  checkpointNumber: CheckpointNumber,
398
466
  syncedToBlockNumber: BlockNumber,
399
467
  proposer: EthAddress | undefined,
400
468
  publisher: SequencerPublisher,
401
469
  attestorAddress: EthAddress,
402
470
  invalidateCheckpoint: InvalidateCheckpointRequest | undefined,
471
+ proposedCheckpointData?: ProposedCheckpointData,
403
472
  ): CheckpointProposalJob {
404
473
  return new CheckpointProposalJob(
405
- epoch,
406
474
  slot,
475
+ targetSlot,
476
+ targetEpoch,
407
477
  checkpointNumber,
408
478
  syncedToBlockNumber,
409
479
  proposer,
@@ -425,13 +495,22 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
425
495
  this.epochCache,
426
496
  this.dateProvider,
427
497
  this.metrics,
498
+ this.checkpointProposalJobMetrics.createRecorder(),
428
499
  this,
429
500
  this.setState.bind(this),
430
501
  this.tracer,
431
502
  this.log.getBindings(),
503
+ proposedCheckpointData,
432
504
  );
433
505
  }
434
506
 
507
+ /**
508
+ * Returns the current sequencer state.
509
+ */
510
+ public getState(): SequencerState {
511
+ return this.state;
512
+ }
513
+
435
514
  /**
436
515
  * Internal helper for setting the sequencer state and checks if we have enough time left in the slot to transition to the new state.
437
516
  * @param proposedState - The new state to transition to.
@@ -478,16 +557,15 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
478
557
  * We don't check against the previous block submitted since it may have been reorg'd out.
479
558
  */
480
559
  protected async checkSync(args: { ts: bigint; slot: SlotNumber }): Promise<SequencerSyncCheckResult | undefined> {
481
- // Check that the archiver and dependencies have synced to the previous L1 slot at least
482
- // TODO(#14766): Archiver reports L1 timestamp based on L1 blocks seen, which means that a missed L1 block will
483
- // cause the archiver L1 timestamp to fall behind, and cause this sequencer to start processing one L1 slot later.
484
- const l1Timestamp = await this.l2BlockSource.getL1Timestamp();
485
- const { slot, ts } = args;
486
- 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) {
487
566
  this.log.debug(`Cannot propose block at next L2 slot ${slot} due to pending sync from L1`, {
488
567
  slot,
489
- ts,
490
- l1Timestamp,
568
+ syncedL2Slot,
491
569
  });
492
570
  return undefined;
493
571
  }
@@ -497,25 +575,43 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
497
575
  number: syncSummary.latestBlockNumber,
498
576
  hash: syncSummary.latestBlockHash,
499
577
  })),
500
- this.l2BlockSource.getL2Tips().then(t => t.proposed),
578
+ this.l2BlockSource
579
+ .getL2Tips()
580
+ .then(t => ({ proposed: t.proposed, checkpointed: t.checkpointed, proposedCheckpoint: t.proposedCheckpoint })),
501
581
  this.p2pClient.getStatus().then(p2p => p2p.syncedToL2Block),
502
- this.l1ToL2MessageSource.getL2Tips().then(t => t.proposed),
582
+ this.l1ToL2MessageSource.getL2Tips().then(t => ({ proposed: t.proposed, checkpointed: t.checkpointed })),
503
583
  this.l2BlockSource.getPendingChainValidationStatus(),
584
+ this.l2BlockSource.getProposedCheckpointOnly(),
504
585
  ] as const);
505
586
 
506
- const [worldState, l2BlockSource, p2p, l1ToL2MessageSource, pendingChainValidationStatus] = syncedBlocks;
587
+ const [worldState, l2Tips, p2p, l1ToL2MessageSourceTips, pendingChainValidationStatus, proposedCheckpointData] =
588
+ syncedBlocks;
507
589
 
508
590
  // Handle zero as a special case, since the block hash won't match across services if we're changing the prefilled data for the genesis block,
509
591
  // as the world state can compute the new genesis block hash, but other components use the hardcoded constant.
510
592
  // TODO(palla/mbps): Fix the above. All components should be able to handle dynamic genesis block hashes.
511
593
  const result =
512
- (l2BlockSource.number === 0 && worldState.number === 0 && p2p.number === 0 && l1ToL2MessageSource.number === 0) ||
513
- (worldState.hash === l2BlockSource.hash &&
514
- p2p.hash === l2BlockSource.hash &&
515
- l1ToL2MessageSource.hash === l2BlockSource.hash);
594
+ (l2Tips.proposed.number === 0 &&
595
+ l2Tips.checkpointed.block.number === 0 &&
596
+ l2Tips.checkpointed.checkpoint.number === 0 &&
597
+ worldState.number === 0 &&
598
+ p2p.number === 0 &&
599
+ l1ToL2MessageSourceTips.proposed.number === 0 &&
600
+ l1ToL2MessageSourceTips.checkpointed.block.number === 0 &&
601
+ l1ToL2MessageSourceTips.checkpointed.checkpoint.number === 0) ||
602
+ (worldState.hash === l2Tips.proposed.hash &&
603
+ p2p.hash === l2Tips.proposed.hash &&
604
+ l1ToL2MessageSourceTips.proposed.hash === l2Tips.proposed.hash &&
605
+ l1ToL2MessageSourceTips.checkpointed.block.hash === l2Tips.checkpointed.block.hash &&
606
+ l1ToL2MessageSourceTips.checkpointed.checkpoint.hash === l2Tips.checkpointed.checkpoint.hash);
516
607
 
517
608
  if (!result) {
518
- this.log.debug(`Sequencer sync check failed`, { worldState, l2BlockSource, p2p, l1ToL2MessageSource });
609
+ this.log.debug(`Sequencer sync check failed`, {
610
+ worldState,
611
+ l2BlockSource: l2Tips.proposed,
612
+ p2p,
613
+ l1ToL2MessageSourceTips,
614
+ });
519
615
  return undefined;
520
616
  }
521
617
 
@@ -525,26 +621,33 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
525
621
  const archive = new Fr((await this.worldState.getCommitted().getTreeInfo(MerkleTreeId.ARCHIVE)).root);
526
622
  return {
527
623
  checkpointNumber: CheckpointNumber.ZERO,
624
+ checkpointedCheckpointNumber: CheckpointNumber.ZERO,
528
625
  blockNumber: BlockNumber.ZERO,
529
626
  archive,
530
- l1Timestamp,
627
+ hasProposedCheckpoint: false,
628
+ syncedL2Slot,
531
629
  pendingChainValidationStatus,
532
630
  };
533
631
  }
534
632
 
535
- const block = await this.l2BlockSource.getL2Block(blockNumber);
536
- if (!block) {
633
+ const blockData = await this.l2BlockSource.getBlockData(blockNumber);
634
+ if (!blockData) {
537
635
  // this shouldn't really happen because a moment ago we checked that all components were in sync
538
- this.log.error(`Failed to get L2 block ${blockNumber} from the archiver with all components in sync`);
636
+ this.log.error(`Failed to get L2 block data ${blockNumber} from the archiver with all components in sync`);
539
637
  return undefined;
540
638
  }
541
639
 
640
+ const hasProposedCheckpoint = l2Tips.proposedCheckpoint.checkpoint.number > l2Tips.checkpointed.checkpoint.number;
641
+
542
642
  return {
543
- block,
544
- blockNumber: block.number,
545
- checkpointNumber: block.checkpointNumber,
546
- archive: block.archive.root,
547
- l1Timestamp,
643
+ blockData,
644
+ blockNumber: blockData.header.getBlockNumber(),
645
+ checkpointNumber: blockData.checkpointNumber,
646
+ checkpointedCheckpointNumber: l2Tips.checkpointed.checkpoint.number,
647
+ archive: blockData.archive.root,
648
+ hasProposedCheckpoint,
649
+ proposedCheckpointData,
650
+ syncedL2Slot,
548
651
  pendingChainValidationStatus,
549
652
  };
550
653
  }
@@ -553,20 +656,20 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
553
656
  * Checks if we are the proposer for the next slot.
554
657
  * @returns True if we can propose, and the proposer address (undefined if anyone can propose)
555
658
  */
556
- protected async checkCanPropose(slot: SlotNumber): Promise<[boolean, EthAddress | undefined]> {
659
+ protected async checkCanPropose(targetSlot: SlotNumber): Promise<[boolean, EthAddress | undefined]> {
557
660
  let proposer: EthAddress | undefined;
558
661
 
559
662
  try {
560
- proposer = await this.epochCache.getProposerAttesterAddressInSlot(slot);
663
+ proposer = await this.epochCache.getProposerAttesterAddressInSlot(targetSlot);
561
664
  } catch (e) {
562
665
  if (e instanceof NoCommitteeError) {
563
- if (this.lastSlotForNoCommitteeWarning !== slot) {
564
- this.lastSlotForNoCommitteeWarning = slot;
565
- this.log.warn(`Cannot propose at next L2 slot ${slot} since the committee does not exist on L1`);
666
+ if (this.lastSlotForNoCommitteeWarning !== targetSlot) {
667
+ this.lastSlotForNoCommitteeWarning = targetSlot;
668
+ this.log.warn(`Cannot propose at target slot ${targetSlot} since the committee does not exist on L1`);
566
669
  }
567
670
  return [false, undefined];
568
671
  }
569
- this.log.error(`Error getting proposer for slot ${slot}`, e);
672
+ this.log.error(`Error getting proposer for target slot ${targetSlot}`, e);
570
673
  return [false, undefined];
571
674
  }
572
675
 
@@ -583,10 +686,18 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
583
686
  const weAreProposer = validatorAddresses.some(addr => addr.equals(proposer));
584
687
 
585
688
  if (!weAreProposer) {
586
- this.log.debug(`Cannot propose at slot ${slot} since we are not a proposer`, { validatorAddresses, proposer });
689
+ this.log.debug(`Cannot propose at target slot ${targetSlot} since we are not a proposer`, {
690
+ targetSlot,
691
+ validatorAddresses,
692
+ proposer,
693
+ });
587
694
  return [false, proposer];
588
695
  }
589
696
 
697
+ this.log.info(`We are the proposer for pipeline slot ${targetSlot}`, {
698
+ targetSlot,
699
+ proposer,
700
+ });
590
701
  return [true, proposer];
591
702
  }
592
703
 
@@ -595,8 +706,8 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
595
706
  * This allows the sequencer to participate in governance/slashing votes even when it cannot build blocks.
596
707
  */
597
708
  @trackSpan('Seqeuencer.tryVoteWhenSyncFails', ({ slot }) => ({ [Attributes.SLOT_NUMBER]: slot }))
598
- protected async tryVoteWhenSyncFails(args: { slot: SlotNumber; ts: bigint }): Promise<void> {
599
- const { slot } = args;
709
+ protected async tryVoteWhenSyncFails(args: { slot: SlotNumber; targetSlot: SlotNumber; ts: bigint }): Promise<void> {
710
+ const { slot, targetSlot } = args;
600
711
 
601
712
  // Prevent duplicate attempts in the same slot
602
713
  if (this.lastSlotForFallbackVote === slot) {
@@ -624,7 +735,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
624
735
  });
625
736
 
626
737
  // Check if we're a proposer or proposal is open
627
- const [canPropose, proposer] = await this.checkCanPropose(slot);
738
+ const [canPropose, proposer] = await this.checkCanPropose(targetSlot);
628
739
  if (!canPropose) {
629
740
  this.log.trace(`Cannot vote in slot ${slot} since we are not a proposer`, { slot, proposer });
630
741
  return;
@@ -641,9 +752,9 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
641
752
  slot,
642
753
  });
643
754
 
644
- // Enqueue governance and slashing votes
755
+ // Enqueue governance and slashing votes (voter uses the target slot for L1 submission)
645
756
  const voter = new CheckpointVoter(
646
- slot,
757
+ targetSlot,
647
758
  publisher,
648
759
  attestorAddress,
649
760
  this.validatorClient,
@@ -723,7 +834,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
723
834
  syncedTo: SequencerSyncCheckResult,
724
835
  currentSlot: SlotNumber,
725
836
  ): Promise<void> {
726
- const { pendingChainValidationStatus, l1Timestamp } = syncedTo;
837
+ const { pendingChainValidationStatus, syncedL2Slot } = syncedTo;
727
838
  if (pendingChainValidationStatus.valid) {
728
839
  return;
729
840
  }
@@ -738,7 +849,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
738
849
 
739
850
  const logData = {
740
851
  invalidL1Timestamp: invalidCheckpointTimestamp,
741
- l1Timestamp,
852
+ syncedL2Slot,
742
853
  invalidCheckpoint: pendingChainValidationStatus.checkpoint,
743
854
  secondsBeforeInvalidatingBlockAsCommitteeMember,
744
855
  secondsBeforeInvalidatingBlockAsNonCommitteeMember,
@@ -866,6 +977,11 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
866
977
  return this.validatorClient?.getValidatorAddresses();
867
978
  }
868
979
 
980
+ /** Updates the publisher factory's node keystore adapter after a keystore reload. */
981
+ public updatePublisherNodeKeyStore(adapter: NodeKeystoreAdapter): void {
982
+ this.publisherFactory.updateNodeKeyStore(adapter);
983
+ }
984
+
869
985
  public getConfig() {
870
986
  return this.config;
871
987
  }
@@ -876,10 +992,13 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
876
992
  }
877
993
 
878
994
  type SequencerSyncCheckResult = {
879
- block?: L2Block;
995
+ blockData?: BlockData;
880
996
  checkpointNumber: CheckpointNumber;
997
+ checkpointedCheckpointNumber: CheckpointNumber;
881
998
  blockNumber: BlockNumber;
882
999
  archive: Fr;
883
- l1Timestamp: bigint;
1000
+ hasProposedCheckpoint: boolean;
1001
+ proposedCheckpointData?: ProposedCheckpointData;
1002
+ syncedL2Slot: SlotNumber;
884
1003
  pendingChainValidationStatus: ValidateCheckpointResult;
885
1004
  };