@aztec/sequencer-client 0.0.1-commit.e558bd1c → 0.0.1-commit.e5a3663dd

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 +27 -7
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +58 -37
  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 +434 -141
  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 +46 -10
  46. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  47. package/dest/sequencer/checkpoint_proposal_job.js +705 -223
  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 +23 -8
  57. package/dest/sequencer/metrics.d.ts.map +1 -1
  58. package/dest/sequencer/metrics.js +117 -21
  59. package/dest/sequencer/sequencer.d.ts +53 -19
  60. package/dest/sequencer/sequencer.d.ts.map +1 -1
  61. package/dest/sequencer/sequencer.js +188 -101
  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 +10 -8
  75. package/package.json +27 -28
  76. package/src/client/sequencer-client.ts +81 -30
  77. package/src/config.ts +77 -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 +501 -202
  91. package/src/sequencer/README.md +82 -13
  92. package/src/sequencer/chain_state_overrides.ts +87 -0
  93. package/src/sequencer/checkpoint_proposal_job.ts +873 -243
  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 +132 -25
  98. package/src/sequencer/sequencer.ts +262 -115
  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 +31 -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
- 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 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,10 +23,11 @@ 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';
28
- import { FullNodeCheckpointsBuilder, type ValidatorClient } from '@aztec/validator-client';
30
+ import { FullNodeCheckpointsBuilder, NodeKeystoreAdapter, type ValidatorClient } from '@aztec/validator-client';
29
31
 
30
32
  import EventEmitter from 'node:events';
31
33
 
@@ -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,19 +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
- // 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
88
  /** Config for the sequencer */
87
89
  protected config: ResolvedSequencerConfig = DefaultSequencerConfig;
90
+ private readonly signatureContext: CoordinationSignatureContext;
88
91
 
89
92
  constructor(
90
93
  protected publisherFactory: SequencerPublisherFactory,
@@ -100,44 +103,51 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
100
103
  protected dateProvider: DateProvider,
101
104
  protected epochCache: EpochCache,
102
105
  protected rollupContract: RollupContract,
103
- config: SequencerConfig,
106
+ config: SequencerConfig & Pick<ChainConfig, 'l1ChainId' | 'l1Contracts'>,
104
107
  protected telemetry: TelemetryClient = getTelemetryClient(),
105
108
  protected log = createLogger('sequencer'),
106
109
  ) {
107
110
  super();
111
+ this.stateLog = log.createChild('state');
108
112
 
109
113
  // Add [FISHERMAN] prefix to logger if in fisherman mode
110
114
  if (config.fishermanMode) {
111
115
  this.log = log.createChild('[FISHERMAN]');
112
116
  }
113
117
 
118
+ this.signatureContext = {
119
+ chainId: config.l1ChainId,
120
+ rollupAddress: config.l1Contracts.rollupAddress,
121
+ };
114
122
  this.metrics = new SequencerMetrics(telemetry, this.rollupContract, 'Sequencer');
123
+ this.checkpointProposalJobMetrics = new CheckpointProposalJobMetrics(telemetry);
115
124
  this.updateConfig(config);
116
125
  }
117
126
 
118
127
  /** Updates sequencer config by the defined values and updates the timetable */
119
128
  public updateConfig(config: Partial<SequencerConfig>) {
120
129
  const filteredConfig = pickFromSchema(config, SequencerConfigSchema);
121
- this.log.info(`Updated sequencer config`, omit(filteredConfig, 'txPublicSetupAllowList'));
130
+ this.log.info(`Updated sequencer config`, omit(filteredConfig, 'txPublicSetupAllowListExtend'));
122
131
  this.config = merge(this.config, filteredConfig);
132
+ const p2pPropagationTime = this.config.attestationPropagationTime;
123
133
  this.timetable = new SequencerTimetable(
124
134
  {
125
135
  ethereumSlotDuration: this.l1Constants.ethereumSlotDuration,
126
136
  aztecSlotDuration: this.aztecSlotDuration,
127
137
  l1PublishingTime: this.l1PublishingTime,
128
- p2pPropagationTime: this.config.attestationPropagationTime,
138
+ p2pPropagationTime,
129
139
  blockDurationMs: this.config.blockDurationMs,
130
140
  enforce: this.config.enforceTimeTable,
141
+ pipelining: this.epochCache.isProposerPipeliningEnabled(),
131
142
  },
132
143
  this.metrics,
133
144
  this.log,
134
145
  );
135
146
  }
136
147
 
137
- /** Initializes the sequencer (precomputes tables and creates a publisher). Takes about 3s. */
138
- public async init() {
148
+ /** Initializes the sequencer (precomputes tables). Takes about 3s. */
149
+ public init() {
139
150
  getKzg();
140
- this.publisher = (await this.publisherFactory.create(undefined)).publisher;
141
151
  }
142
152
 
143
153
  /** Starts the sequencer and moves to IDLE state. */
@@ -152,12 +162,18 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
152
162
  this.log.info('Started sequencer');
153
163
  }
154
164
 
165
+ /** Triggers an immediate run of the sequencer, bypassing the polling interval. */
166
+ public trigger() {
167
+ return this.runningPromise?.trigger();
168
+ }
169
+
155
170
  /** Stops the sequencer from building blocks and moves to STOPPED state. */
156
171
  public async stop(): Promise<void> {
157
172
  this.log.info(`Stopping sequencer`);
158
173
  this.setState(SequencerState.STOPPING, undefined, { force: true });
159
- this.publisher?.interrupt();
174
+ await this.publisherFactory.stopAll();
160
175
  await this.runningPromise?.stop();
176
+ await this.lastCheckpointProposalJob?.awaitPendingSubmission();
161
177
  this.setState(SequencerState.STOPPED, undefined, { force: true });
162
178
  this.log.info('Stopped sequencer');
163
179
  }
@@ -169,7 +185,6 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
169
185
  } catch (err) {
170
186
  this.emit('checkpoint-error', { error: err as Error });
171
187
  if (err instanceof SequencerTooSlowError) {
172
- // TODO(palla/mbps): Add missing states
173
188
  // Log as warn only if we had to abort halfway through the block proposal
174
189
  const logLvl = [SequencerState.INITIALIZING_CHECKPOINT, SequencerState.PROPOSER_CHECK].includes(
175
190
  err.proposedState,
@@ -202,14 +217,25 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
202
217
  @trackSpan('Sequencer.work')
203
218
  protected async work() {
204
219
  this.setState(SequencerState.SYNCHRONIZING, undefined);
205
- 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();
206
222
 
207
223
  // 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);
224
+ const checkpointProposalJob = await this.prepareCheckpointProposal(
225
+ slot,
226
+ targetSlot,
227
+ epoch,
228
+ targetEpoch,
229
+ ts,
230
+ nowSeconds,
231
+ );
209
232
  if (!checkpointProposalJob) {
210
233
  return;
211
234
  }
212
235
 
236
+ // Track the job so we can await its pending L1 submission during shutdown
237
+ this.lastCheckpointProposalJob = checkpointProposalJob;
238
+
213
239
  // Execute the checkpoint proposal job
214
240
  const checkpoint = await checkpointProposalJob.execute();
215
241
 
@@ -218,13 +244,13 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
218
244
  this.lastCheckpointProposed = checkpoint;
219
245
  }
220
246
 
221
- // 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)
222
248
  if (
223
249
  this.config.fishermanMode &&
224
- (this.lastEpochForStrategyComparison === undefined || epoch > this.lastEpochForStrategyComparison)
250
+ (this.lastEpochForStrategyComparison === undefined || targetEpoch > this.lastEpochForStrategyComparison)
225
251
  ) {
226
- this.logStrategyComparison(epoch, checkpointProposalJob.getPublisher());
227
- this.lastEpochForStrategyComparison = epoch;
252
+ this.logStrategyComparison(targetEpoch, checkpointProposalJob.getPublisher());
253
+ this.lastEpochForStrategyComparison = targetEpoch;
228
254
  }
229
255
 
230
256
  return checkpoint;
@@ -236,44 +262,49 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
236
262
  * @returns CheckpointProposalJob if successful, undefined if we are not yet synced or are not the proposer.
237
263
  */
238
264
  @trackSpan('Sequencer.prepareCheckpointProposal')
239
- private async prepareCheckpointProposal(
240
- epoch: EpochNumber,
265
+ protected async prepareCheckpointProposal(
241
266
  slot: SlotNumber,
267
+ targetSlot: SlotNumber,
268
+ epoch: EpochNumber,
269
+ targetEpoch: EpochNumber,
242
270
  ts: bigint,
243
- now: bigint,
271
+ nowSeconds: bigint,
244
272
  ): Promise<CheckpointProposalJob | undefined> {
245
- // Check we have not already processed this slot (cheapest check)
273
+ // Check we have not already processed this target slot (cheapest check)
246
274
  // We only check this if enforce timetable is set, since we want to keep processing the same slot if we are not
247
275
  // running against actual time (eg when we use sandbox-style automining)
248
276
  if (
249
277
  this.lastSlotForCheckpointProposalJob &&
250
- this.lastSlotForCheckpointProposalJob >= slot &&
278
+ this.lastSlotForCheckpointProposalJob >= targetSlot &&
251
279
  this.config.enforceTimeTable
252
280
  ) {
253
- this.log.trace(`Slot ${slot} has already been processed`);
281
+ this.log.trace(`Target slot ${targetSlot} has already been processed`);
254
282
  return undefined;
255
283
  }
256
284
 
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}`);
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
+ );
260
290
  return undefined;
261
291
  }
262
292
 
263
293
  // Check all components are synced to latest as seen by the archiver (queries all subsystems)
264
294
  const syncedTo = await this.checkSync({ ts, slot });
265
295
  if (!syncedTo) {
266
- await this.tryVoteWhenSyncFails({ slot, ts });
296
+ await this.tryVoteWhenSyncFails({ slot, targetSlot, ts });
267
297
  return undefined;
268
298
  }
269
299
 
270
- // 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.
271
301
  // Still perform governance/slashing voting (as proposer) once per slot.
272
- 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);
273
304
 
274
305
  if (isEscapeHatchOpen) {
275
306
  this.setState(SequencerState.PROPOSER_CHECK, slot);
276
- const [canPropose, proposer] = await this.checkCanPropose(slot);
307
+ const [canPropose, proposer] = await this.checkCanPropose(targetSlot);
277
308
  if (canPropose) {
278
309
  await this.tryVoteWhenEscapeHatchOpen({ slot, proposer });
279
310
  } else {
@@ -290,18 +321,18 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
290
321
  const checkpointNumber = CheckpointNumber(syncedTo.checkpointNumber + 1);
291
322
 
292
323
  const logCtx = {
293
- now,
294
- syncedToL1Ts: syncedTo.l1Timestamp,
295
- syncedToL2Slot: getSlotAtTimestamp(syncedTo.l1Timestamp, this.l1Constants),
324
+ nowSeconds,
325
+ syncedToL2Slot: syncedTo.syncedL2Slot,
296
326
  slot,
327
+ targetSlot,
297
328
  slotTs: ts,
298
329
  checkpointNumber,
299
330
  isPendingChainValid: pick(syncedTo.pendingChainValidationStatus, 'valid', 'reason', 'invalidIndex'),
300
331
  };
301
332
 
302
- // Check that we are a proposer for the next slot
333
+ // Check that we are a proposer for the target slot.
303
334
  this.setState(SequencerState.PROPOSER_CHECK, slot);
304
- const [canPropose, proposer] = await this.checkCanPropose(slot);
335
+ const [canPropose, proposer] = await this.checkCanPropose(targetSlot);
305
336
 
306
337
  // If we are not a proposer check if we should invalidate an invalid checkpoint, and bail
307
338
  if (!canPropose) {
@@ -309,13 +340,23 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
309
340
  return undefined;
310
341
  }
311
342
 
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) {
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) {
314
355
  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() },
356
+ `Cannot propose block at target slot ${targetSlot} since that slot was taken by block ${syncedTo.blockNumber}`,
357
+ { ...logCtx, block: syncedTo.blockData.header.toInspect() },
317
358
  );
318
- this.metrics.recordBlockProposalPrecheckFailed('slot_already_taken');
359
+ this.metrics.recordCheckpointPrecheckFailed('slot_already_taken');
319
360
  return undefined;
320
361
  }
321
362
 
@@ -326,7 +367,6 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
326
367
  const proposerForPublisher = this.config.fishermanMode ? undefined : proposer;
327
368
  const { attestorAddress, publisher } = await this.publisherFactory.create(proposerForPublisher);
328
369
  this.log.verbose(`Created publisher at address ${publisher.getSenderAddress()} for attestor ${attestorAddress}`);
329
- this.publisher = publisher;
330
370
 
331
371
  // In fisherman mode, set the actual proposer's address for simulations
332
372
  if (this.config.fishermanMode && proposer) {
@@ -335,14 +375,41 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
335
375
  }
336
376
 
337
377
  // Prepare invalidation request if the pending chain is invalid (returns undefined if no need)
338
- 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
+ }
339
407
 
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,
408
+ const simulationOverridesPlan = l1SimulationOverridesBuilder.build();
409
+ const canProposeCheck = await publisher.canProposeAt(
410
+ archiveForCheck,
344
411
  proposer ?? EthAddress.ZERO,
345
- invalidateCheckpoint,
412
+ simulationOverridesPlan,
346
413
  );
347
414
 
348
415
  if (canProposeCheck === undefined) {
@@ -351,17 +418,17 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
351
418
  logCtx,
352
419
  );
353
420
  this.emit('proposer-rollup-check-failed', { reason: 'Rollup contract check failed', slot });
354
- this.metrics.recordBlockProposalPrecheckFailed('rollup_contract_check_failed');
421
+ this.metrics.recordCheckpointPrecheckFailed('rollup_contract_check_failed');
355
422
  return undefined;
356
423
  }
357
424
 
358
- if (canProposeCheck.slot !== slot) {
425
+ if (canProposeCheck.slot !== targetSlot) {
359
426
  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 },
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 },
362
429
  );
363
430
  this.emit('proposer-rollup-check-failed', { reason: 'Slot mismatch', slot });
364
- this.metrics.recordBlockProposalPrecheckFailed('slot_mismatch');
431
+ this.metrics.recordCheckpointPrecheckFailed('slot_mismatch');
365
432
  return undefined;
366
433
  }
367
434
 
@@ -371,39 +438,53 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
371
438
  { ...logCtx, rollup: canProposeCheck, expectedSlot: slot },
372
439
  );
373
440
  this.emit('proposer-rollup-check-failed', { reason: 'Block mismatch', slot });
374
- this.metrics.recordBlockProposalPrecheckFailed('block_number_mismatch');
441
+ this.metrics.recordCheckpointPrecheckFailed('block_number_mismatch');
375
442
  return undefined;
376
443
  }
377
444
 
378
- this.lastSlotForCheckpointProposalJob = slot;
379
- 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
+ );
380
456
 
381
457
  // Create and return the checkpoint proposal job
382
458
  return this.createCheckpointProposalJob(
383
- epoch,
384
459
  slot,
460
+ targetSlot,
461
+ targetEpoch,
385
462
  checkpointNumber,
386
463
  syncedTo.blockNumber,
387
464
  proposer,
388
465
  publisher,
389
466
  attestorAddress,
390
467
  invalidateCheckpoint,
468
+ syncedTo.proposedCheckpointData,
391
469
  );
392
470
  }
393
471
 
394
472
  protected createCheckpointProposalJob(
395
- epoch: EpochNumber,
396
473
  slot: SlotNumber,
474
+ targetSlot: SlotNumber,
475
+ targetEpoch: EpochNumber,
397
476
  checkpointNumber: CheckpointNumber,
398
477
  syncedToBlockNumber: BlockNumber,
399
478
  proposer: EthAddress | undefined,
400
479
  publisher: SequencerPublisher,
401
480
  attestorAddress: EthAddress,
402
481
  invalidateCheckpoint: InvalidateCheckpointRequest | undefined,
482
+ proposedCheckpointData?: ProposedCheckpointData,
403
483
  ): CheckpointProposalJob {
404
484
  return new CheckpointProposalJob(
405
- epoch,
406
485
  slot,
486
+ targetSlot,
487
+ targetEpoch,
407
488
  checkpointNumber,
408
489
  syncedToBlockNumber,
409
490
  proposer,
@@ -419,19 +500,29 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
419
500
  this.checkpointsBuilder,
420
501
  this.l2BlockSource,
421
502
  this.l1Constants,
503
+ this.signatureContext,
422
504
  this.config,
423
505
  this.timetable,
424
506
  this.slasherClient,
425
507
  this.epochCache,
426
508
  this.dateProvider,
427
509
  this.metrics,
510
+ this.checkpointProposalJobMetrics.createRecorder(),
428
511
  this,
429
512
  this.setState.bind(this),
430
513
  this.tracer,
431
514
  this.log.getBindings(),
515
+ proposedCheckpointData,
432
516
  );
433
517
  }
434
518
 
519
+ /**
520
+ * Returns the current sequencer state.
521
+ */
522
+ public getState(): SequencerState {
523
+ return this.state;
524
+ }
525
+
435
526
  /**
436
527
  * 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
528
  * @param proposedState - The new state to transition to.
@@ -457,19 +548,35 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
457
548
  this.timetable.assertTimeLeft(proposedState, secondsIntoSlot);
458
549
  }
459
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
+
460
557
  const boringStates = [SequencerState.IDLE, SequencerState.SYNCHRONIZING];
461
558
  const logLevel =
462
- boringStates.includes(proposedState) && boringStates.includes(this.state)
463
- ? ('trace' as const)
464
- : ('debug' as const);
465
- 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
+ });
466
568
 
467
569
  this.emit('state-changed', {
468
- oldState: this.state,
570
+ oldState,
469
571
  newState: proposedState,
470
572
  secondsIntoSlot,
471
573
  slot: slotNumber,
472
574
  });
575
+ if (stateChanged) {
576
+ this.metrics.recordStateDuration(stateDurationMs, oldState);
577
+ this.stateEnteredAtMs = transitionAtMs;
578
+ this.stateSlotNumber = slotNumber;
579
+ }
473
580
  this.state = proposedState;
474
581
  }
475
582
 
@@ -478,16 +585,15 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
478
585
  * We don't check against the previous block submitted since it may have been reorg'd out.
479
586
  */
480
587
  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) {
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) {
487
594
  this.log.debug(`Cannot propose block at next L2 slot ${slot} due to pending sync from L1`, {
488
595
  slot,
489
- ts,
490
- l1Timestamp,
596
+ syncedL2Slot,
491
597
  });
492
598
  return undefined;
493
599
  }
@@ -497,25 +603,43 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
497
603
  number: syncSummary.latestBlockNumber,
498
604
  hash: syncSummary.latestBlockHash,
499
605
  })),
500
- this.l2BlockSource.getL2Tips().then(t => t.proposed),
606
+ this.l2BlockSource
607
+ .getL2Tips()
608
+ .then(t => ({ proposed: t.proposed, checkpointed: t.checkpointed, proposedCheckpoint: t.proposedCheckpoint })),
501
609
  this.p2pClient.getStatus().then(p2p => p2p.syncedToL2Block),
502
- this.l1ToL2MessageSource.getL2Tips().then(t => t.proposed),
610
+ this.l1ToL2MessageSource.getL2Tips().then(t => ({ proposed: t.proposed, checkpointed: t.checkpointed })),
503
611
  this.l2BlockSource.getPendingChainValidationStatus(),
612
+ this.l2BlockSource.getLastProposedCheckpoint(),
504
613
  ] as const);
505
614
 
506
- const [worldState, l2BlockSource, p2p, l1ToL2MessageSource, pendingChainValidationStatus] = syncedBlocks;
615
+ const [worldState, l2Tips, p2p, l1ToL2MessageSourceTips, pendingChainValidationStatus, proposedCheckpointData] =
616
+ syncedBlocks;
507
617
 
508
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,
509
619
  // as the world state can compute the new genesis block hash, but other components use the hardcoded constant.
510
620
  // TODO(palla/mbps): Fix the above. All components should be able to handle dynamic genesis block hashes.
511
621
  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);
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);
516
635
 
517
636
  if (!result) {
518
- 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
+ });
519
643
  return undefined;
520
644
  }
521
645
 
@@ -525,26 +649,33 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
525
649
  const archive = new Fr((await this.worldState.getCommitted().getTreeInfo(MerkleTreeId.ARCHIVE)).root);
526
650
  return {
527
651
  checkpointNumber: CheckpointNumber.ZERO,
652
+ checkpointedCheckpointNumber: CheckpointNumber.ZERO,
528
653
  blockNumber: BlockNumber.ZERO,
529
654
  archive,
530
- l1Timestamp,
655
+ hasProposedCheckpoint: false,
656
+ syncedL2Slot,
531
657
  pendingChainValidationStatus,
532
658
  };
533
659
  }
534
660
 
535
- const block = await this.l2BlockSource.getL2Block(blockNumber);
536
- if (!block) {
661
+ const blockData = await this.l2BlockSource.getBlockData(blockNumber);
662
+ if (!blockData) {
537
663
  // 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`);
664
+ this.log.error(`Failed to get L2 block data ${blockNumber} from the archiver with all components in sync`);
539
665
  return undefined;
540
666
  }
541
667
 
668
+ const hasProposedCheckpoint = l2Tips.proposedCheckpoint.checkpoint.number > l2Tips.checkpointed.checkpoint.number;
669
+
542
670
  return {
543
- block,
544
- blockNumber: block.number,
545
- checkpointNumber: block.checkpointNumber,
546
- archive: block.archive.root,
547
- l1Timestamp,
671
+ blockData,
672
+ blockNumber: blockData.header.getBlockNumber(),
673
+ checkpointNumber: blockData.checkpointNumber,
674
+ checkpointedCheckpointNumber: l2Tips.checkpointed.checkpoint.number,
675
+ archive: blockData.archive.root,
676
+ hasProposedCheckpoint,
677
+ proposedCheckpointData,
678
+ syncedL2Slot,
548
679
  pendingChainValidationStatus,
549
680
  };
550
681
  }
@@ -553,20 +684,20 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
553
684
  * Checks if we are the proposer for the next slot.
554
685
  * @returns True if we can propose, and the proposer address (undefined if anyone can propose)
555
686
  */
556
- protected async checkCanPropose(slot: SlotNumber): Promise<[boolean, EthAddress | undefined]> {
687
+ protected async checkCanPropose(targetSlot: SlotNumber): Promise<[boolean, EthAddress | undefined]> {
557
688
  let proposer: EthAddress | undefined;
558
689
 
559
690
  try {
560
- proposer = await this.epochCache.getProposerAttesterAddressInSlot(slot);
691
+ proposer = await this.epochCache.getProposerAttesterAddressInSlot(targetSlot);
561
692
  } catch (e) {
562
693
  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`);
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`);
566
697
  }
567
698
  return [false, undefined];
568
699
  }
569
- this.log.error(`Error getting proposer for slot ${slot}`, e);
700
+ this.log.error(`Error getting proposer for target slot ${targetSlot}`, e);
570
701
  return [false, undefined];
571
702
  }
572
703
 
@@ -583,10 +714,18 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
583
714
  const weAreProposer = validatorAddresses.some(addr => addr.equals(proposer));
584
715
 
585
716
  if (!weAreProposer) {
586
- 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
+ });
587
722
  return [false, proposer];
588
723
  }
589
724
 
725
+ this.log.info(`We are the proposer for pipeline slot ${targetSlot}`, {
726
+ targetSlot,
727
+ proposer,
728
+ });
590
729
  return [true, proposer];
591
730
  }
592
731
 
@@ -595,8 +734,8 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
595
734
  * This allows the sequencer to participate in governance/slashing votes even when it cannot build blocks.
596
735
  */
597
736
  @trackSpan('Seqeuencer.tryVoteWhenSyncFails', ({ slot }) => ({ [Attributes.SLOT_NUMBER]: slot }))
598
- protected async tryVoteWhenSyncFails(args: { slot: SlotNumber; ts: bigint }): Promise<void> {
599
- const { slot } = args;
737
+ protected async tryVoteWhenSyncFails(args: { slot: SlotNumber; targetSlot: SlotNumber; ts: bigint }): Promise<void> {
738
+ const { slot, targetSlot } = args;
600
739
 
601
740
  // Prevent duplicate attempts in the same slot
602
741
  if (this.lastSlotForFallbackVote === slot) {
@@ -624,7 +763,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
624
763
  });
625
764
 
626
765
  // Check if we're a proposer or proposal is open
627
- const [canPropose, proposer] = await this.checkCanPropose(slot);
766
+ const [canPropose, proposer] = await this.checkCanPropose(targetSlot);
628
767
  if (!canPropose) {
629
768
  this.log.trace(`Cannot vote in slot ${slot} since we are not a proposer`, { slot, proposer });
630
769
  return;
@@ -641,9 +780,9 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
641
780
  slot,
642
781
  });
643
782
 
644
- // Enqueue governance and slashing votes
783
+ // Enqueue governance and slashing votes (voter uses the target slot for L1 submission)
645
784
  const voter = new CheckpointVoter(
646
- slot,
785
+ targetSlot,
647
786
  publisher,
648
787
  attestorAddress,
649
788
  this.validatorClient,
@@ -723,7 +862,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
723
862
  syncedTo: SequencerSyncCheckResult,
724
863
  currentSlot: SlotNumber,
725
864
  ): Promise<void> {
726
- const { pendingChainValidationStatus, l1Timestamp } = syncedTo;
865
+ const { pendingChainValidationStatus, syncedL2Slot } = syncedTo;
727
866
  if (pendingChainValidationStatus.valid) {
728
867
  return;
729
868
  }
@@ -738,7 +877,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
738
877
 
739
878
  const logData = {
740
879
  invalidL1Timestamp: invalidCheckpointTimestamp,
741
- l1Timestamp,
880
+ syncedL2Slot,
742
881
  invalidCheckpoint: pendingChainValidationStatus.checkpoint,
743
882
  secondsBeforeInvalidatingBlockAsCommitteeMember,
744
883
  secondsBeforeInvalidatingBlockAsNonCommitteeMember,
@@ -866,6 +1005,11 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
866
1005
  return this.validatorClient?.getValidatorAddresses();
867
1006
  }
868
1007
 
1008
+ /** Updates the publisher factory's node keystore adapter after a keystore reload. */
1009
+ public updatePublisherNodeKeyStore(adapter: NodeKeystoreAdapter): void {
1010
+ this.publisherFactory.updateNodeKeyStore(adapter);
1011
+ }
1012
+
869
1013
  public getConfig() {
870
1014
  return this.config;
871
1015
  }
@@ -876,10 +1020,13 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
876
1020
  }
877
1021
 
878
1022
  type SequencerSyncCheckResult = {
879
- block?: L2Block;
1023
+ blockData?: BlockData;
880
1024
  checkpointNumber: CheckpointNumber;
1025
+ checkpointedCheckpointNumber: CheckpointNumber;
881
1026
  blockNumber: BlockNumber;
882
1027
  archive: Fr;
883
- l1Timestamp: bigint;
1028
+ hasProposedCheckpoint: boolean;
1029
+ proposedCheckpointData?: ProposedCheckpointData;
1030
+ syncedL2Slot: SlotNumber;
884
1031
  pendingChainValidationStatus: ValidateCheckpointResult;
885
1032
  };