@aztec/sequencer-client 0.0.1-commit.9372f48 → 0.0.1-commit.949a33fd8

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