@aztec/sequencer-client 0.0.1-commit.e6bd8901 → 0.0.1-commit.ec7ac5448

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 (93) 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 +47 -33
  7. package/dest/global_variable_builder/global_builder.d.ts +15 -10
  8. package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
  9. package/dest/global_variable_builder/global_builder.js +26 -26
  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 +47 -17
  13. package/dest/publisher/config.d.ts.map +1 -1
  14. package/dest/publisher/config.js +121 -42
  15. package/dest/publisher/index.d.ts +2 -1
  16. package/dest/publisher/index.d.ts.map +1 -1
  17. package/dest/publisher/l1_tx_failed_store/factory.d.ts +11 -0
  18. package/dest/publisher/l1_tx_failed_store/factory.d.ts.map +1 -0
  19. package/dest/publisher/l1_tx_failed_store/factory.js +22 -0
  20. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts +59 -0
  21. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -0
  22. package/dest/publisher/l1_tx_failed_store/failed_tx_store.js +1 -0
  23. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts +15 -0
  24. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts.map +1 -0
  25. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.js +34 -0
  26. package/dest/publisher/l1_tx_failed_store/index.d.ts +4 -0
  27. package/dest/publisher/l1_tx_failed_store/index.d.ts.map +1 -0
  28. package/dest/publisher/l1_tx_failed_store/index.js +2 -0
  29. package/dest/publisher/sequencer-publisher-factory.d.ts +11 -5
  30. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  31. package/dest/publisher/sequencer-publisher-factory.js +27 -3
  32. package/dest/publisher/sequencer-publisher-metrics.d.ts +1 -1
  33. package/dest/publisher/sequencer-publisher-metrics.d.ts.map +1 -1
  34. package/dest/publisher/sequencer-publisher-metrics.js +12 -4
  35. package/dest/publisher/sequencer-publisher.d.ts +75 -49
  36. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  37. package/dest/publisher/sequencer-publisher.js +470 -156
  38. package/dest/sequencer/chain_state_overrides.d.ts +25 -0
  39. package/dest/sequencer/chain_state_overrides.d.ts.map +1 -0
  40. package/dest/sequencer/chain_state_overrides.js +39 -0
  41. package/dest/sequencer/checkpoint_proposal_job.d.ts +51 -13
  42. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  43. package/dest/sequencer/checkpoint_proposal_job.js +414 -239
  44. package/dest/sequencer/checkpoint_voter.d.ts +1 -2
  45. package/dest/sequencer/checkpoint_voter.d.ts.map +1 -1
  46. package/dest/sequencer/checkpoint_voter.js +2 -5
  47. package/dest/sequencer/events.d.ts +2 -1
  48. package/dest/sequencer/events.d.ts.map +1 -1
  49. package/dest/sequencer/metrics.d.ts +25 -6
  50. package/dest/sequencer/metrics.d.ts.map +1 -1
  51. package/dest/sequencer/metrics.js +134 -31
  52. package/dest/sequencer/sequencer.d.ts +44 -17
  53. package/dest/sequencer/sequencer.d.ts.map +1 -1
  54. package/dest/sequencer/sequencer.js +157 -94
  55. package/dest/sequencer/timetable.d.ts +17 -6
  56. package/dest/sequencer/timetable.d.ts.map +1 -1
  57. package/dest/sequencer/timetable.js +51 -46
  58. package/dest/sequencer/types.d.ts +2 -2
  59. package/dest/sequencer/types.d.ts.map +1 -1
  60. package/dest/test/index.d.ts +3 -5
  61. package/dest/test/index.d.ts.map +1 -1
  62. package/dest/test/mock_checkpoint_builder.d.ts +14 -11
  63. package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
  64. package/dest/test/mock_checkpoint_builder.js +63 -40
  65. package/dest/test/utils.d.ts +3 -3
  66. package/dest/test/utils.d.ts.map +1 -1
  67. package/dest/test/utils.js +5 -4
  68. package/package.json +27 -28
  69. package/src/client/sequencer-client.ts +81 -30
  70. package/src/config.ts +65 -43
  71. package/src/global_variable_builder/global_builder.ts +31 -28
  72. package/src/global_variable_builder/index.ts +1 -1
  73. package/src/publisher/config.ts +153 -43
  74. package/src/publisher/index.ts +3 -0
  75. package/src/publisher/l1_tx_failed_store/factory.ts +32 -0
  76. package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +55 -0
  77. package/src/publisher/l1_tx_failed_store/file_store_failed_tx_store.ts +46 -0
  78. package/src/publisher/l1_tx_failed_store/index.ts +3 -0
  79. package/src/publisher/sequencer-publisher-factory.ts +38 -9
  80. package/src/publisher/sequencer-publisher-metrics.ts +7 -3
  81. package/src/publisher/sequencer-publisher.ts +531 -214
  82. package/src/sequencer/README.md +83 -13
  83. package/src/sequencer/chain_state_overrides.ts +87 -0
  84. package/src/sequencer/checkpoint_proposal_job.ts +547 -274
  85. package/src/sequencer/checkpoint_voter.ts +1 -12
  86. package/src/sequencer/events.ts +1 -1
  87. package/src/sequencer/metrics.ts +153 -33
  88. package/src/sequencer/sequencer.ts +228 -107
  89. package/src/sequencer/timetable.ts +70 -57
  90. package/src/sequencer/types.ts +1 -1
  91. package/src/test/index.ts +2 -4
  92. package/src/test/mock_checkpoint_builder.ts +84 -56
  93. 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
 
@@ -60,6 +60,9 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
60
60
  /** The last slot for which we attempted to perform our voting duties with degraded block production */
61
61
  private lastSlotForFallbackVote: SlotNumber | undefined;
62
62
 
63
+ /** The last slot for which we logged "no committee" warning, to avoid spam */
64
+ private lastSlotForNoCommitteeWarning: SlotNumber | undefined;
65
+
63
66
  /** The last slot for which we triggered a checkpoint proposal job, to prevent duplicate attempts. */
64
67
  private lastSlotForCheckpointProposalJob: SlotNumber | undefined;
65
68
 
@@ -69,17 +72,12 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
69
72
  /** The last epoch for which we logged strategy comparison in fisherman mode. */
70
73
  private lastEpochForStrategyComparison: EpochNumber | undefined;
71
74
 
75
+ /** The last checkpoint proposal job, tracked so we can await its pending L1 submission during shutdown. */
76
+ protected lastCheckpointProposalJob: CheckpointProposalJob | undefined;
77
+
72
78
  /** The maximum number of seconds that the sequencer can be into a slot to transition to a particular state. */
73
79
  protected timetable!: SequencerTimetable;
74
80
 
75
- // This shouldn't be here as this gets re-created each time we build/propose a block.
76
- // But we have a number of tests that abuse/rely on this class having a permanent publisher.
77
- // As long as those tests only configure a single publisher they will continue to work.
78
- // This will get re-assigned every time the sequencer goes to build a new block to a publisher that is valid
79
- // for the block proposer.
80
- // TODO(palla/mbps): Remove this field and fix tests
81
- protected publisher: SequencerPublisher | undefined;
82
-
83
81
  /** Config for the sequencer */
84
82
  protected config: ResolvedSequencerConfig = DefaultSequencerConfig;
85
83
 
@@ -115,26 +113,27 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
115
113
  /** Updates sequencer config by the defined values and updates the timetable */
116
114
  public updateConfig(config: Partial<SequencerConfig>) {
117
115
  const filteredConfig = pickFromSchema(config, SequencerConfigSchema);
118
- this.log.info(`Updated sequencer config`, omit(filteredConfig, 'txPublicSetupAllowList'));
116
+ this.log.info(`Updated sequencer config`, omit(filteredConfig, 'txPublicSetupAllowListExtend'));
119
117
  this.config = merge(this.config, filteredConfig);
118
+ const p2pPropagationTime = this.config.attestationPropagationTime;
120
119
  this.timetable = new SequencerTimetable(
121
120
  {
122
121
  ethereumSlotDuration: this.l1Constants.ethereumSlotDuration,
123
122
  aztecSlotDuration: this.aztecSlotDuration,
124
123
  l1PublishingTime: this.l1PublishingTime,
125
- p2pPropagationTime: this.config.attestationPropagationTime,
124
+ p2pPropagationTime,
126
125
  blockDurationMs: this.config.blockDurationMs,
127
126
  enforce: this.config.enforceTimeTable,
127
+ pipelining: this.epochCache.isProposerPipeliningEnabled(),
128
128
  },
129
129
  this.metrics,
130
130
  this.log,
131
131
  );
132
132
  }
133
133
 
134
- /** Initializes the sequencer (precomputes tables and creates a publisher). Takes about 3s. */
135
- public async init() {
134
+ /** Initializes the sequencer (precomputes tables). Takes about 3s. */
135
+ public init() {
136
136
  getKzg();
137
- this.publisher = (await this.publisherFactory.create(undefined)).publisher;
138
137
  }
139
138
 
140
139
  /** Starts the sequencer and moves to IDLE state. */
@@ -149,12 +148,18 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
149
148
  this.log.info('Started sequencer');
150
149
  }
151
150
 
151
+ /** Triggers an immediate run of the sequencer, bypassing the polling interval. */
152
+ public trigger() {
153
+ return this.runningPromise?.trigger();
154
+ }
155
+
152
156
  /** Stops the sequencer from building blocks and moves to STOPPED state. */
153
157
  public async stop(): Promise<void> {
154
158
  this.log.info(`Stopping sequencer`);
155
159
  this.setState(SequencerState.STOPPING, undefined, { force: true });
156
- this.publisher?.interrupt();
160
+ await this.publisherFactory.stopAll();
157
161
  await this.runningPromise?.stop();
162
+ await this.lastCheckpointProposalJob?.awaitPendingSubmission();
158
163
  this.setState(SequencerState.STOPPED, undefined, { force: true });
159
164
  this.log.info('Stopped sequencer');
160
165
  }
@@ -166,7 +171,6 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
166
171
  } catch (err) {
167
172
  this.emit('checkpoint-error', { error: err as Error });
168
173
  if (err instanceof SequencerTooSlowError) {
169
- // TODO(palla/mbps): Add missing states
170
174
  // Log as warn only if we had to abort halfway through the block proposal
171
175
  const logLvl = [SequencerState.INITIALIZING_CHECKPOINT, SequencerState.PROPOSER_CHECK].includes(
172
176
  err.proposedState,
@@ -199,14 +203,25 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
199
203
  @trackSpan('Sequencer.work')
200
204
  protected async work() {
201
205
  this.setState(SequencerState.SYNCHRONIZING, undefined);
202
- const { slot, ts, now, epoch } = this.epochCache.getEpochAndSlotInNextL1Slot();
206
+ const { slot, ts, nowSeconds, epoch } = this.epochCache.getEpochAndSlotInNextL1Slot();
207
+ const { slot: targetSlot, epoch: targetEpoch } = this.epochCache.getTargetEpochAndSlotInNextL1Slot();
203
208
 
204
209
  // Check if we are synced and it's our slot, grab a publisher, check previous block invalidation, etc
205
- const checkpointProposalJob = await this.prepareCheckpointProposal(epoch, slot, ts, now);
210
+ const checkpointProposalJob = await this.prepareCheckpointProposal(
211
+ slot,
212
+ targetSlot,
213
+ epoch,
214
+ targetEpoch,
215
+ ts,
216
+ nowSeconds,
217
+ );
206
218
  if (!checkpointProposalJob) {
207
219
  return;
208
220
  }
209
221
 
222
+ // Track the job so we can await its pending L1 submission during shutdown
223
+ this.lastCheckpointProposalJob = checkpointProposalJob;
224
+
210
225
  // Execute the checkpoint proposal job
211
226
  const checkpoint = await checkpointProposalJob.execute();
212
227
 
@@ -215,13 +230,13 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
215
230
  this.lastCheckpointProposed = checkpoint;
216
231
  }
217
232
 
218
- // Log fee strategy comparison if on fisherman
233
+ // Log fee strategy comparison if on fisherman (uses target epoch since we mirror the proposer's perspective)
219
234
  if (
220
235
  this.config.fishermanMode &&
221
- (this.lastEpochForStrategyComparison === undefined || epoch > this.lastEpochForStrategyComparison)
236
+ (this.lastEpochForStrategyComparison === undefined || targetEpoch > this.lastEpochForStrategyComparison)
222
237
  ) {
223
- this.logStrategyComparison(epoch, checkpointProposalJob.getPublisher());
224
- this.lastEpochForStrategyComparison = epoch;
238
+ this.logStrategyComparison(targetEpoch, checkpointProposalJob.getPublisher());
239
+ this.lastEpochForStrategyComparison = targetEpoch;
225
240
  }
226
241
 
227
242
  return checkpoint;
@@ -233,44 +248,49 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
233
248
  * @returns CheckpointProposalJob if successful, undefined if we are not yet synced or are not the proposer.
234
249
  */
235
250
  @trackSpan('Sequencer.prepareCheckpointProposal')
236
- private async prepareCheckpointProposal(
237
- epoch: EpochNumber,
251
+ protected async prepareCheckpointProposal(
238
252
  slot: SlotNumber,
253
+ targetSlot: SlotNumber,
254
+ epoch: EpochNumber,
255
+ targetEpoch: EpochNumber,
239
256
  ts: bigint,
240
- now: bigint,
257
+ nowSeconds: bigint,
241
258
  ): Promise<CheckpointProposalJob | undefined> {
242
- // Check we have not already processed this slot (cheapest check)
259
+ // Check we have not already processed this target slot (cheapest check)
243
260
  // We only check this if enforce timetable is set, since we want to keep processing the same slot if we are not
244
261
  // running against actual time (eg when we use sandbox-style automining)
245
262
  if (
246
263
  this.lastSlotForCheckpointProposalJob &&
247
- this.lastSlotForCheckpointProposalJob >= slot &&
264
+ this.lastSlotForCheckpointProposalJob >= targetSlot &&
248
265
  this.config.enforceTimeTable
249
266
  ) {
250
- this.log.trace(`Slot ${slot} has already been processed`);
267
+ this.log.trace(`Target slot ${targetSlot} has already been processed`);
251
268
  return undefined;
252
269
  }
253
270
 
254
- // But if we have already proposed for this slot, the we definitely have to skip it, automining or not
255
- if (this.lastCheckpointProposed && this.lastCheckpointProposed.header.slotNumber >= slot) {
256
- this.log.trace(`Slot ${slot} has already been published as checkpoint ${this.lastCheckpointProposed.number}`);
271
+ // But if we have already proposed for this slot, then we definitely have to skip it, automining or not
272
+ if (this.lastCheckpointProposed && this.lastCheckpointProposed.header.slotNumber >= targetSlot) {
273
+ this.log.trace(
274
+ `Slot ${targetSlot} has already been published as checkpoint ${this.lastCheckpointProposed.number}`,
275
+ );
257
276
  return undefined;
258
277
  }
259
278
 
260
279
  // Check all components are synced to latest as seen by the archiver (queries all subsystems)
261
280
  const syncedTo = await this.checkSync({ ts, slot });
262
281
  if (!syncedTo) {
263
- await this.tryVoteWhenSyncFails({ slot, ts });
282
+ await this.tryVoteWhenSyncFails({ slot, targetSlot, ts });
264
283
  return undefined;
265
284
  }
266
285
 
267
- // If escape hatch is open for this epoch, do not start checkpoint proposal work and do not attempt invalidations.
286
+ // If escape hatch is open for the target epoch, do not start checkpoint proposal work and do not attempt invalidations.
268
287
  // Still perform governance/slashing voting (as proposer) once per slot.
269
- const isEscapeHatchOpen = await this.epochCache.isEscapeHatchOpen(epoch);
288
+ // When pipelining, we check the target epoch (slot+1's epoch) since that's the epoch we're building for.
289
+ const isEscapeHatchOpen = await this.epochCache.isEscapeHatchOpen(targetEpoch);
270
290
 
271
291
  if (isEscapeHatchOpen) {
272
292
  this.setState(SequencerState.PROPOSER_CHECK, slot);
273
- const [canPropose, proposer] = await this.checkCanPropose(slot);
293
+ const [canPropose, proposer] = await this.checkCanPropose(targetSlot);
274
294
  if (canPropose) {
275
295
  await this.tryVoteWhenEscapeHatchOpen({ slot, proposer });
276
296
  } else {
@@ -287,18 +307,18 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
287
307
  const checkpointNumber = CheckpointNumber(syncedTo.checkpointNumber + 1);
288
308
 
289
309
  const logCtx = {
290
- now,
291
- syncedToL1Ts: syncedTo.l1Timestamp,
292
- syncedToL2Slot: getSlotAtTimestamp(syncedTo.l1Timestamp, this.l1Constants),
310
+ nowSeconds,
311
+ syncedToL2Slot: syncedTo.syncedL2Slot,
293
312
  slot,
313
+ targetSlot,
294
314
  slotTs: ts,
295
315
  checkpointNumber,
296
316
  isPendingChainValid: pick(syncedTo.pendingChainValidationStatus, 'valid', 'reason', 'invalidIndex'),
297
317
  };
298
318
 
299
- // Check that we are a proposer for the next slot
319
+ // Check that we are a proposer for the target slot.
300
320
  this.setState(SequencerState.PROPOSER_CHECK, slot);
301
- const [canPropose, proposer] = await this.checkCanPropose(slot);
321
+ const [canPropose, proposer] = await this.checkCanPropose(targetSlot);
302
322
 
303
323
  // If we are not a proposer check if we should invalidate an invalid checkpoint, and bail
304
324
  if (!canPropose) {
@@ -306,13 +326,23 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
306
326
  return undefined;
307
327
  }
308
328
 
309
- // Check that the slot is not taken by a block already (should never happen, since only us can propose for this slot)
310
- if (syncedTo.block && syncedTo.block.header.getSlot() >= slot) {
329
+ // Guard: don't exceed 1-deep pipeline. Without a proposed checkpoint, we can only build
330
+ // confirmed + 1. With a proposed checkpoint, we can build confirmed + 2.
331
+ const confirmedCkpt = syncedTo.checkpointedCheckpointNumber;
332
+ if (checkpointNumber > confirmedCkpt + 2) {
333
+ this.log.verbose(
334
+ `Skipping slot ${targetSlot}: checkpoint ${checkpointNumber} exceeds max pipeline depth (confirmed=${confirmedCkpt})`,
335
+ );
336
+ return undefined;
337
+ }
338
+
339
+ // Check that the target slot is not taken by a block already (should never happen, since only us can propose for this slot)
340
+ if (syncedTo.blockData && syncedTo.blockData.header.getSlot() >= targetSlot) {
311
341
  this.log.warn(
312
- `Cannot propose block at next L2 slot ${slot} since that slot was taken by block ${syncedTo.blockNumber}`,
313
- { ...logCtx, block: syncedTo.block.header.toInspect() },
342
+ `Cannot propose block at target slot ${targetSlot} since that slot was taken by block ${syncedTo.blockNumber}`,
343
+ { ...logCtx, block: syncedTo.blockData.header.toInspect() },
314
344
  );
315
- this.metrics.recordBlockProposalPrecheckFailed('slot_already_taken');
345
+ this.metrics.recordCheckpointPrecheckFailed('slot_already_taken');
316
346
  return undefined;
317
347
  }
318
348
 
@@ -323,7 +353,6 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
323
353
  const proposerForPublisher = this.config.fishermanMode ? undefined : proposer;
324
354
  const { attestorAddress, publisher } = await this.publisherFactory.create(proposerForPublisher);
325
355
  this.log.verbose(`Created publisher at address ${publisher.getSenderAddress()} for attestor ${attestorAddress}`);
326
- this.publisher = publisher;
327
356
 
328
357
  // In fisherman mode, set the actual proposer's address for simulations
329
358
  if (this.config.fishermanMode && proposer) {
@@ -332,14 +361,41 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
332
361
  }
333
362
 
334
363
  // Prepare invalidation request if the pending chain is invalid (returns undefined if no need)
335
- const invalidateCheckpoint = await publisher.simulateInvalidateCheckpoint(syncedTo.pendingChainValidationStatus);
364
+ let invalidateCheckpoint = await publisher.simulateInvalidateCheckpoint(syncedTo.pendingChainValidationStatus);
365
+
366
+ // Determine the correct archive and L1 state overrides for the canProposeAt check.
367
+ // The L1 contract reads archives[proposedCheckpointNumber] and compares it with the provided archive.
368
+ // When invalidating or pipelining, the local archive may differ from L1's, so we adjust accordingly.
369
+ let archiveForCheck = syncedTo.archive;
370
+ const l1SimulationOverridesBuilder = new SimulationOverridesBuilder();
371
+
372
+ if (this.epochCache.isProposerPipeliningEnabled() && syncedTo.hasProposedCheckpoint) {
373
+ // Parent checkpoint hasn't landed on L1 yet. Override both the proposed checkpoint number
374
+ // and the archive at that checkpoint so L1 simulation sees the correct chain tip.
375
+ const parentCheckpointNumber = CheckpointNumber(checkpointNumber - 1);
376
+ l1SimulationOverridesBuilder.forPendingCheckpoint(parentCheckpointNumber).withPendingArchive(syncedTo.archive);
377
+ this.metrics.recordPipelineDepth(1);
378
+
379
+ this.log.verbose(
380
+ `Building on top of proposed checkpoint (pending=${syncedTo.proposedCheckpointData?.checkpointNumber})`,
381
+ );
382
+ // Clear the invalidation - the proposed checkpoint should handle it.
383
+ invalidateCheckpoint = undefined;
384
+ } else if (invalidateCheckpoint) {
385
+ // After invalidation, L1 will roll back to checkpoint N-1. The archive at N-1 already
386
+ // exists on L1, so we just pass the matching archive (the lastArchive of the invalid checkpoint).
387
+ archiveForCheck = invalidateCheckpoint.lastArchive;
388
+ l1SimulationOverridesBuilder.forPendingCheckpoint(invalidateCheckpoint.forcePendingCheckpointNumber);
389
+ this.metrics.recordPipelineDepth(0);
390
+ } else {
391
+ this.metrics.recordPipelineDepth(0);
392
+ }
336
393
 
337
- // Check with the rollup contract if we can indeed propose at the next L2 slot. This check should not fail
338
- // if all the previous checks are good, but we do it just in case.
339
- const canProposeCheck = await publisher.canProposeAtNextEthBlock(
340
- syncedTo.archive,
394
+ const simulationOverridesPlan = l1SimulationOverridesBuilder.build();
395
+ const canProposeCheck = await publisher.canProposeAt(
396
+ archiveForCheck,
341
397
  proposer ?? EthAddress.ZERO,
342
- invalidateCheckpoint,
398
+ simulationOverridesPlan,
343
399
  );
344
400
 
345
401
  if (canProposeCheck === undefined) {
@@ -348,17 +404,17 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
348
404
  logCtx,
349
405
  );
350
406
  this.emit('proposer-rollup-check-failed', { reason: 'Rollup contract check failed', slot });
351
- this.metrics.recordBlockProposalPrecheckFailed('rollup_contract_check_failed');
407
+ this.metrics.recordCheckpointPrecheckFailed('rollup_contract_check_failed');
352
408
  return undefined;
353
409
  }
354
410
 
355
- if (canProposeCheck.slot !== slot) {
411
+ if (canProposeCheck.slot !== targetSlot) {
356
412
  this.log.warn(
357
- `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}.`,
358
- { ...logCtx, rollup: canProposeCheck, expectedSlot: slot },
413
+ `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}.`,
414
+ { ...logCtx, rollup: canProposeCheck, expectedSlot: targetSlot },
359
415
  );
360
416
  this.emit('proposer-rollup-check-failed', { reason: 'Slot mismatch', slot });
361
- this.metrics.recordBlockProposalPrecheckFailed('slot_mismatch');
417
+ this.metrics.recordCheckpointPrecheckFailed('slot_mismatch');
362
418
  return undefined;
363
419
  }
364
420
 
@@ -368,39 +424,53 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
368
424
  { ...logCtx, rollup: canProposeCheck, expectedSlot: slot },
369
425
  );
370
426
  this.emit('proposer-rollup-check-failed', { reason: 'Block mismatch', slot });
371
- this.metrics.recordBlockProposalPrecheckFailed('block_number_mismatch');
427
+ this.metrics.recordCheckpointPrecheckFailed('block_number_mismatch');
372
428
  return undefined;
373
429
  }
374
430
 
375
- this.lastSlotForCheckpointProposalJob = slot;
376
- this.log.info(`Preparing checkpoint proposal ${checkpointNumber} at slot ${slot}`, { ...logCtx, proposer });
431
+ this.lastSlotForCheckpointProposalJob = targetSlot;
432
+
433
+ await this.p2pClient.prepareForSlot(targetSlot);
434
+ this.log.info(
435
+ `Preparing checkpoint proposal ${checkpointNumber} for target slot ${targetSlot} during wall-clock slot ${slot}`,
436
+ {
437
+ ...logCtx,
438
+ proposer,
439
+ pipeliningEnabled: this.epochCache.isProposerPipeliningEnabled(),
440
+ },
441
+ );
377
442
 
378
443
  // Create and return the checkpoint proposal job
379
444
  return this.createCheckpointProposalJob(
380
- epoch,
381
445
  slot,
446
+ targetSlot,
447
+ targetEpoch,
382
448
  checkpointNumber,
383
449
  syncedTo.blockNumber,
384
450
  proposer,
385
451
  publisher,
386
452
  attestorAddress,
387
453
  invalidateCheckpoint,
454
+ syncedTo.proposedCheckpointData,
388
455
  );
389
456
  }
390
457
 
391
458
  protected createCheckpointProposalJob(
392
- epoch: EpochNumber,
393
459
  slot: SlotNumber,
460
+ targetSlot: SlotNumber,
461
+ targetEpoch: EpochNumber,
394
462
  checkpointNumber: CheckpointNumber,
395
463
  syncedToBlockNumber: BlockNumber,
396
464
  proposer: EthAddress | undefined,
397
465
  publisher: SequencerPublisher,
398
466
  attestorAddress: EthAddress,
399
467
  invalidateCheckpoint: InvalidateCheckpointRequest | undefined,
468
+ proposedCheckpointData?: ProposedCheckpointData,
400
469
  ): CheckpointProposalJob {
401
470
  return new CheckpointProposalJob(
402
- epoch,
403
471
  slot,
472
+ targetSlot,
473
+ targetEpoch,
404
474
  checkpointNumber,
405
475
  syncedToBlockNumber,
406
476
  proposer,
@@ -424,11 +494,19 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
424
494
  this.metrics,
425
495
  this,
426
496
  this.setState.bind(this),
427
- this.log,
428
497
  this.tracer,
498
+ this.log.getBindings(),
499
+ proposedCheckpointData,
429
500
  );
430
501
  }
431
502
 
503
+ /**
504
+ * Returns the current sequencer state.
505
+ */
506
+ public getState(): SequencerState {
507
+ return this.state;
508
+ }
509
+
432
510
  /**
433
511
  * Internal helper for setting the sequencer state and checks if we have enough time left in the slot to transition to the new state.
434
512
  * @param proposedState - The new state to transition to.
@@ -475,16 +553,15 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
475
553
  * We don't check against the previous block submitted since it may have been reorg'd out.
476
554
  */
477
555
  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) {
556
+ // Check that the archiver has fully synced the L2 slot before the one we want to propose in.
557
+ // The archiver reports sync progress via L1 block timestamps and synced checkpoint slots.
558
+ // See getSyncedL2SlotNumber for how missed L1 blocks are handled.
559
+ const syncedL2Slot = await this.l2BlockSource.getSyncedL2SlotNumber();
560
+ const { slot } = args;
561
+ if (syncedL2Slot === undefined || syncedL2Slot + 1 < slot) {
484
562
  this.log.debug(`Cannot propose block at next L2 slot ${slot} due to pending sync from L1`, {
485
563
  slot,
486
- ts,
487
- l1Timestamp,
564
+ syncedL2Slot,
488
565
  });
489
566
  return undefined;
490
567
  }
@@ -494,25 +571,43 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
494
571
  number: syncSummary.latestBlockNumber,
495
572
  hash: syncSummary.latestBlockHash,
496
573
  })),
497
- this.l2BlockSource.getL2Tips().then(t => t.proposed),
574
+ this.l2BlockSource
575
+ .getL2Tips()
576
+ .then(t => ({ proposed: t.proposed, checkpointed: t.checkpointed, proposedCheckpoint: t.proposedCheckpoint })),
498
577
  this.p2pClient.getStatus().then(p2p => p2p.syncedToL2Block),
499
- this.l1ToL2MessageSource.getL2Tips().then(t => t.proposed),
578
+ this.l1ToL2MessageSource.getL2Tips().then(t => ({ proposed: t.proposed, checkpointed: t.checkpointed })),
500
579
  this.l2BlockSource.getPendingChainValidationStatus(),
580
+ this.l2BlockSource.getProposedCheckpointOnly(),
501
581
  ] as const);
502
582
 
503
- const [worldState, l2BlockSource, p2p, l1ToL2MessageSource, pendingChainValidationStatus] = syncedBlocks;
583
+ const [worldState, l2Tips, p2p, l1ToL2MessageSourceTips, pendingChainValidationStatus, proposedCheckpointData] =
584
+ syncedBlocks;
504
585
 
505
586
  // 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
587
  // as the world state can compute the new genesis block hash, but other components use the hardcoded constant.
507
588
  // TODO(palla/mbps): Fix the above. All components should be able to handle dynamic genesis block hashes.
508
589
  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);
590
+ (l2Tips.proposed.number === 0 &&
591
+ l2Tips.checkpointed.block.number === 0 &&
592
+ l2Tips.checkpointed.checkpoint.number === 0 &&
593
+ worldState.number === 0 &&
594
+ p2p.number === 0 &&
595
+ l1ToL2MessageSourceTips.proposed.number === 0 &&
596
+ l1ToL2MessageSourceTips.checkpointed.block.number === 0 &&
597
+ l1ToL2MessageSourceTips.checkpointed.checkpoint.number === 0) ||
598
+ (worldState.hash === l2Tips.proposed.hash &&
599
+ p2p.hash === l2Tips.proposed.hash &&
600
+ l1ToL2MessageSourceTips.proposed.hash === l2Tips.proposed.hash &&
601
+ l1ToL2MessageSourceTips.checkpointed.block.hash === l2Tips.checkpointed.block.hash &&
602
+ l1ToL2MessageSourceTips.checkpointed.checkpoint.hash === l2Tips.checkpointed.checkpoint.hash);
513
603
 
514
604
  if (!result) {
515
- this.log.debug(`Sequencer sync check failed`, { worldState, l2BlockSource, p2p, l1ToL2MessageSource });
605
+ this.log.debug(`Sequencer sync check failed`, {
606
+ worldState,
607
+ l2BlockSource: l2Tips.proposed,
608
+ p2p,
609
+ l1ToL2MessageSourceTips,
610
+ });
516
611
  return undefined;
517
612
  }
518
613
 
@@ -522,26 +617,33 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
522
617
  const archive = new Fr((await this.worldState.getCommitted().getTreeInfo(MerkleTreeId.ARCHIVE)).root);
523
618
  return {
524
619
  checkpointNumber: CheckpointNumber.ZERO,
620
+ checkpointedCheckpointNumber: CheckpointNumber.ZERO,
525
621
  blockNumber: BlockNumber.ZERO,
526
622
  archive,
527
- l1Timestamp,
623
+ hasProposedCheckpoint: false,
624
+ syncedL2Slot,
528
625
  pendingChainValidationStatus,
529
626
  };
530
627
  }
531
628
 
532
- const block = await this.l2BlockSource.getL2Block(blockNumber);
533
- if (!block) {
629
+ const blockData = await this.l2BlockSource.getBlockData(blockNumber);
630
+ if (!blockData) {
534
631
  // this shouldn't really happen because a moment ago we checked that all components were in sync
535
- this.log.error(`Failed to get L2 block ${blockNumber} from the archiver with all components in sync`);
632
+ this.log.error(`Failed to get L2 block data ${blockNumber} from the archiver with all components in sync`);
536
633
  return undefined;
537
634
  }
538
635
 
636
+ const hasProposedCheckpoint = l2Tips.proposedCheckpoint.checkpoint.number > l2Tips.checkpointed.checkpoint.number;
637
+
539
638
  return {
540
- block,
541
- blockNumber: block.number,
542
- checkpointNumber: block.checkpointNumber,
543
- archive: block.archive.root,
544
- l1Timestamp,
639
+ blockData,
640
+ blockNumber: blockData.header.getBlockNumber(),
641
+ checkpointNumber: blockData.checkpointNumber,
642
+ checkpointedCheckpointNumber: l2Tips.checkpointed.checkpoint.number,
643
+ archive: blockData.archive.root,
644
+ hasProposedCheckpoint,
645
+ proposedCheckpointData,
646
+ syncedL2Slot,
545
647
  pendingChainValidationStatus,
546
648
  };
547
649
  }
@@ -550,17 +652,20 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
550
652
  * Checks if we are the proposer for the next slot.
551
653
  * @returns True if we can propose, and the proposer address (undefined if anyone can propose)
552
654
  */
553
- protected async checkCanPropose(slot: SlotNumber): Promise<[boolean, EthAddress | undefined]> {
655
+ protected async checkCanPropose(targetSlot: SlotNumber): Promise<[boolean, EthAddress | undefined]> {
554
656
  let proposer: EthAddress | undefined;
555
657
 
556
658
  try {
557
- proposer = await this.epochCache.getProposerAttesterAddressInSlot(slot);
659
+ proposer = await this.epochCache.getProposerAttesterAddressInSlot(targetSlot);
558
660
  } catch (e) {
559
661
  if (e instanceof NoCommitteeError) {
560
- this.log.warn(`Cannot propose at next L2 slot ${slot} since the committee does not exist on L1`);
662
+ if (this.lastSlotForNoCommitteeWarning !== targetSlot) {
663
+ this.lastSlotForNoCommitteeWarning = targetSlot;
664
+ this.log.warn(`Cannot propose at target slot ${targetSlot} since the committee does not exist on L1`);
665
+ }
561
666
  return [false, undefined];
562
667
  }
563
- this.log.error(`Error getting proposer for slot ${slot}`, e);
668
+ this.log.error(`Error getting proposer for target slot ${targetSlot}`, e);
564
669
  return [false, undefined];
565
670
  }
566
671
 
@@ -577,10 +682,18 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
577
682
  const weAreProposer = validatorAddresses.some(addr => addr.equals(proposer));
578
683
 
579
684
  if (!weAreProposer) {
580
- this.log.debug(`Cannot propose at slot ${slot} since we are not a proposer`, { validatorAddresses, proposer });
685
+ this.log.debug(`Cannot propose at target slot ${targetSlot} since we are not a proposer`, {
686
+ targetSlot,
687
+ validatorAddresses,
688
+ proposer,
689
+ });
581
690
  return [false, proposer];
582
691
  }
583
692
 
693
+ this.log.info(`We are the proposer for pipeline slot ${targetSlot}`, {
694
+ targetSlot,
695
+ proposer,
696
+ });
584
697
  return [true, proposer];
585
698
  }
586
699
 
@@ -589,8 +702,8 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
589
702
  * This allows the sequencer to participate in governance/slashing votes even when it cannot build blocks.
590
703
  */
591
704
  @trackSpan('Seqeuencer.tryVoteWhenSyncFails', ({ slot }) => ({ [Attributes.SLOT_NUMBER]: slot }))
592
- protected async tryVoteWhenSyncFails(args: { slot: SlotNumber; ts: bigint }): Promise<void> {
593
- const { slot } = args;
705
+ protected async tryVoteWhenSyncFails(args: { slot: SlotNumber; targetSlot: SlotNumber; ts: bigint }): Promise<void> {
706
+ const { slot, targetSlot } = args;
594
707
 
595
708
  // Prevent duplicate attempts in the same slot
596
709
  if (this.lastSlotForFallbackVote === slot) {
@@ -618,7 +731,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
618
731
  });
619
732
 
620
733
  // Check if we're a proposer or proposal is open
621
- const [canPropose, proposer] = await this.checkCanPropose(slot);
734
+ const [canPropose, proposer] = await this.checkCanPropose(targetSlot);
622
735
  if (!canPropose) {
623
736
  this.log.trace(`Cannot vote in slot ${slot} since we are not a proposer`, { slot, proposer });
624
737
  return;
@@ -635,9 +748,9 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
635
748
  slot,
636
749
  });
637
750
 
638
- // Enqueue governance and slashing votes
751
+ // Enqueue governance and slashing votes (voter uses the target slot for L1 submission)
639
752
  const voter = new CheckpointVoter(
640
- slot,
753
+ targetSlot,
641
754
  publisher,
642
755
  attestorAddress,
643
756
  this.validatorClient,
@@ -717,7 +830,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
717
830
  syncedTo: SequencerSyncCheckResult,
718
831
  currentSlot: SlotNumber,
719
832
  ): Promise<void> {
720
- const { pendingChainValidationStatus, l1Timestamp } = syncedTo;
833
+ const { pendingChainValidationStatus, syncedL2Slot } = syncedTo;
721
834
  if (pendingChainValidationStatus.valid) {
722
835
  return;
723
836
  }
@@ -732,7 +845,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
732
845
 
733
846
  const logData = {
734
847
  invalidL1Timestamp: invalidCheckpointTimestamp,
735
- l1Timestamp,
848
+ syncedL2Slot,
736
849
  invalidCheckpoint: pendingChainValidationStatus.checkpoint,
737
850
  secondsBeforeInvalidatingBlockAsCommitteeMember,
738
851
  secondsBeforeInvalidatingBlockAsNonCommitteeMember,
@@ -860,6 +973,11 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
860
973
  return this.validatorClient?.getValidatorAddresses();
861
974
  }
862
975
 
976
+ /** Updates the publisher factory's node keystore adapter after a keystore reload. */
977
+ public updatePublisherNodeKeyStore(adapter: NodeKeystoreAdapter): void {
978
+ this.publisherFactory.updateNodeKeyStore(adapter);
979
+ }
980
+
863
981
  public getConfig() {
864
982
  return this.config;
865
983
  }
@@ -870,10 +988,13 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
870
988
  }
871
989
 
872
990
  type SequencerSyncCheckResult = {
873
- block?: L2Block;
991
+ blockData?: BlockData;
874
992
  checkpointNumber: CheckpointNumber;
993
+ checkpointedCheckpointNumber: CheckpointNumber;
875
994
  blockNumber: BlockNumber;
876
995
  archive: Fr;
877
- l1Timestamp: bigint;
996
+ hasProposedCheckpoint: boolean;
997
+ proposedCheckpointData?: ProposedCheckpointData;
998
+ syncedL2Slot: SlotNumber;
878
999
  pendingChainValidationStatus: ValidateCheckpointResult;
879
1000
  };