@aztec/sequencer-client 0.0.1-commit.1142ef1 → 0.0.1-commit.11bf3dd6e

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