@aztec/sequencer-client 0.0.1-commit.f5d02921e → 0.0.1-commit.f7ea82942

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 (69) hide show
  1. package/dest/client/sequencer-client.d.ts +3 -1
  2. package/dest/client/sequencer-client.d.ts.map +1 -1
  3. package/dest/client/sequencer-client.js +3 -4
  4. package/dest/config.d.ts +1 -1
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +8 -11
  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 +4 -9
  14. package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
  15. package/dest/global_variable_builder/global_builder.js +3 -41
  16. package/dest/global_variable_builder/index.d.ts +3 -1
  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 +1 -1
  20. package/dest/publisher/config.d.ts.map +1 -1
  21. package/dest/publisher/config.js +2 -2
  22. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts +3 -4
  23. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -1
  24. package/dest/publisher/sequencer-publisher-factory.d.ts +1 -3
  25. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  26. package/dest/publisher/sequencer-publisher-factory.js +0 -1
  27. package/dest/publisher/sequencer-publisher.d.ts +16 -35
  28. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  29. package/dest/publisher/sequencer-publisher.js +60 -92
  30. package/dest/sequencer/chain_state_overrides.d.ts +25 -0
  31. package/dest/sequencer/chain_state_overrides.d.ts.map +1 -0
  32. package/dest/sequencer/chain_state_overrides.js +39 -0
  33. package/dest/sequencer/checkpoint_proposal_job.d.ts +27 -22
  34. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  35. package/dest/sequencer/checkpoint_proposal_job.js +294 -155
  36. package/dest/sequencer/checkpoint_proposal_job_metrics.d.ts +34 -0
  37. package/dest/sequencer/checkpoint_proposal_job_metrics.d.ts.map +1 -0
  38. package/dest/sequencer/checkpoint_proposal_job_metrics.js +72 -0
  39. package/dest/sequencer/events.d.ts +6 -1
  40. package/dest/sequencer/events.d.ts.map +1 -1
  41. package/dest/sequencer/metrics.d.ts +7 -10
  42. package/dest/sequencer/metrics.d.ts.map +1 -1
  43. package/dest/sequencer/metrics.js +27 -20
  44. package/dest/sequencer/sequencer.d.ts +5 -2
  45. package/dest/sequencer/sequencer.d.ts.map +1 -1
  46. package/dest/sequencer/sequencer.js +31 -23
  47. package/dest/sequencer/timetable.d.ts +14 -1
  48. package/dest/sequencer/timetable.d.ts.map +1 -1
  49. package/dest/sequencer/timetable.js +45 -36
  50. package/package.json +27 -27
  51. package/src/client/sequencer-client.ts +5 -7
  52. package/src/config.ts +8 -11
  53. package/src/global_variable_builder/README.md +44 -0
  54. package/src/global_variable_builder/fee_predictor.ts +172 -0
  55. package/src/global_variable_builder/fee_provider.ts +75 -0
  56. package/src/global_variable_builder/global_builder.ts +7 -54
  57. package/src/global_variable_builder/index.ts +2 -0
  58. package/src/publisher/config.ts +6 -4
  59. package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +3 -1
  60. package/src/publisher/sequencer-publisher-factory.ts +0 -3
  61. package/src/publisher/sequencer-publisher.ts +106 -152
  62. package/src/sequencer/README.md +83 -13
  63. package/src/sequencer/chain_state_overrides.ts +87 -0
  64. package/src/sequencer/checkpoint_proposal_job.ts +365 -205
  65. package/src/sequencer/checkpoint_proposal_job_metrics.ts +128 -0
  66. package/src/sequencer/events.ts +5 -0
  67. package/src/sequencer/metrics.ts +35 -25
  68. package/src/sequencer/sequencer.ts +44 -26
  69. package/src/sequencer/timetable.ts +57 -45
@@ -0,0 +1,128 @@
1
+ import {
2
+ Attributes,
3
+ type Gauge,
4
+ type Histogram,
5
+ type Meter,
6
+ Metrics,
7
+ type TelemetryClient,
8
+ } from '@aztec/telemetry-client';
9
+
10
+ type CheckpointProposalJobInstruments = {
11
+ checkpointAttestationDelay: Histogram;
12
+ checkpointBuildDuration: Histogram;
13
+ checkpointStartToFirstBlockDuration: Histogram;
14
+ checkpointLastBlockToBroadcastDuration: Histogram;
15
+ pipelinedCheckpointBuildStartOffsetFromSlotBoundary: Histogram;
16
+ checkpointBlockCount: Gauge;
17
+ checkpointTxCount: Gauge;
18
+ checkpointTotalMana: Gauge;
19
+ };
20
+
21
+ /**
22
+ * Per-job recording surface used by {@link CheckpointProposalJob}.
23
+ *
24
+ * Ownership split:
25
+ * - {@link CheckpointProposalJobMetrics} owns the OpenTelemetry meter and instruments and should live for the
26
+ * lifetime of the sequencer process.
27
+ * - A recorder owns only mutable timing state for one checkpoint proposal job so overlapping jobs cannot
28
+ * overwrite each other's timing markers.
29
+ */
30
+ export interface CheckpointProposalJobMetricsRecorder {
31
+ recordCheckpointAttestationDelay(durationMs: number): void;
32
+ recordCheckpointBuild(durationMs: number, blockCount: number, txCount: number, totalMana: number): void;
33
+ recordPipelinedCheckpointBuildStartOffsetFromSlotBoundary(offsetMs: number): void;
34
+ startCheckpointTiming(nowMs: number): void;
35
+ noteCheckpointBlockBuilt(nowMs: number, opts: { isFirstBlock: boolean; isLastBlock: boolean }): void;
36
+ noteCheckpointBroadcast(nowMs: number): void;
37
+ }
38
+
39
+ /**
40
+ * Concrete per-job recorder.
41
+ *
42
+ * This class should be short-lived: create one recorder per checkpoint proposal job and discard it when the job
43
+ * completes. It intentionally does not create instruments; it only holds the job-local timestamps needed to derive
44
+ * timing metrics safely.
45
+ */
46
+ class CheckpointProposalJobMetricsRecorderImpl implements CheckpointProposalJobMetricsRecorder {
47
+ private checkpointStartedAt?: number;
48
+ private checkpointLastBlockBuiltAt?: number;
49
+
50
+ constructor(private readonly instruments: CheckpointProposalJobInstruments) {}
51
+
52
+ public recordCheckpointAttestationDelay(durationMs: number) {
53
+ this.instruments.checkpointAttestationDelay.record(Math.ceil(durationMs));
54
+ }
55
+
56
+ public recordCheckpointBuild(durationMs: number, blockCount: number, txCount: number, totalMana: number) {
57
+ this.instruments.checkpointBuildDuration.record(Math.ceil(durationMs));
58
+ this.instruments.checkpointBlockCount.record(blockCount);
59
+ this.instruments.checkpointTxCount.record(txCount);
60
+ this.instruments.checkpointTotalMana.record(totalMana);
61
+ }
62
+
63
+ public recordPipelinedCheckpointBuildStartOffsetFromSlotBoundary(offsetMs: number) {
64
+ this.instruments.pipelinedCheckpointBuildStartOffsetFromSlotBoundary.record(Math.ceil(Math.abs(offsetMs)), {
65
+ [Attributes.SLOT_BOUNDARY_SIDE]: offsetMs < 0 ? 'before' : 'after',
66
+ });
67
+ }
68
+
69
+ public startCheckpointTiming(nowMs: number) {
70
+ this.checkpointStartedAt = nowMs;
71
+ this.checkpointLastBlockBuiltAt = undefined;
72
+ }
73
+
74
+ public noteCheckpointBlockBuilt(nowMs: number, opts: { isFirstBlock: boolean; isLastBlock: boolean }) {
75
+ if (opts.isFirstBlock && this.checkpointStartedAt !== undefined) {
76
+ this.instruments.checkpointStartToFirstBlockDuration.record(Math.ceil(nowMs - this.checkpointStartedAt));
77
+ }
78
+
79
+ if (opts.isLastBlock) {
80
+ this.checkpointLastBlockBuiltAt = nowMs;
81
+ }
82
+ }
83
+
84
+ public noteCheckpointBroadcast(nowMs: number) {
85
+ if (this.checkpointLastBlockBuiltAt !== undefined) {
86
+ this.instruments.checkpointLastBlockToBroadcastDuration.record(
87
+ Math.ceil(nowMs - this.checkpointLastBlockBuiltAt),
88
+ );
89
+ }
90
+
91
+ this.checkpointLastBlockBuiltAt = undefined;
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Long-lived owner of checkpoint proposal job telemetry instruments.
97
+ *
98
+ * The sequencer should construct this once and reuse it across jobs. Each call to {@link createRecorder} returns a
99
+ * lightweight recorder with isolated mutable state for a single checkpoint proposal job.
100
+ */
101
+ export class CheckpointProposalJobMetrics {
102
+ private readonly meter: Meter;
103
+ private readonly instruments: CheckpointProposalJobInstruments;
104
+
105
+ constructor(client: TelemetryClient, name = 'CheckpointProposalJob') {
106
+ this.meter = client.getMeter(name);
107
+ this.instruments = {
108
+ checkpointAttestationDelay: this.meter.createHistogram(Metrics.SEQUENCER_CHECKPOINT_ATTESTATION_DELAY),
109
+ checkpointBuildDuration: this.meter.createHistogram(Metrics.SEQUENCER_CHECKPOINT_BUILD_DURATION),
110
+ checkpointStartToFirstBlockDuration: this.meter.createHistogram(
111
+ Metrics.SEQUENCER_CHECKPOINT_START_TO_FIRST_BLOCK_DURATION,
112
+ ),
113
+ checkpointLastBlockToBroadcastDuration: this.meter.createHistogram(
114
+ Metrics.SEQUENCER_CHECKPOINT_LAST_BLOCK_TO_BROADCAST_DURATION,
115
+ ),
116
+ pipelinedCheckpointBuildStartOffsetFromSlotBoundary: this.meter.createHistogram(
117
+ Metrics.SEQUENCER_PIPELINED_CHECKPOINT_BUILD_START_OFFSET_FROM_SLOT_BOUNDARY,
118
+ ),
119
+ checkpointBlockCount: this.meter.createGauge(Metrics.SEQUENCER_CHECKPOINT_BLOCK_COUNT),
120
+ checkpointTxCount: this.meter.createGauge(Metrics.SEQUENCER_CHECKPOINT_TX_COUNT),
121
+ checkpointTotalMana: this.meter.createGauge(Metrics.SEQUENCER_CHECKPOINT_TOTAL_MANA),
122
+ };
123
+ }
124
+
125
+ public createRecorder(): CheckpointProposalJobMetricsRecorder {
126
+ return new CheckpointProposalJobMetricsRecorderImpl(this.instruments);
127
+ }
128
+ }
@@ -24,4 +24,9 @@ export type SequencerEvents = {
24
24
  }) => void;
25
25
  ['checkpoint-published']: (args: { checkpoint: CheckpointNumber; slot: SlotNumber }) => void;
26
26
  ['checkpoint-error']: (args: { error: Error }) => void;
27
+ ['pipelined-checkpoint-discarded']: (args: {
28
+ slot: SlotNumber;
29
+ checkpointNumber: CheckpointNumber;
30
+ reason: string;
31
+ }) => void;
27
32
  };
@@ -44,13 +44,9 @@ export class SequencerMetrics {
44
44
  private checkpointProposalFailed: UpDownCounter;
45
45
  private checkpointSuccess: UpDownCounter;
46
46
  private slashingAttempts: UpDownCounter;
47
- private checkpointAttestationDelay: Histogram;
48
- private checkpointBuildDuration: Histogram;
49
- private checkpointBlockCount: Gauge;
50
- private checkpointTxCount: Gauge;
51
- private checkpointTotalMana: Gauge;
52
47
  private pipelineDepth: Gauge;
53
48
  private pipelineDiscards: UpDownCounter;
49
+ private pipelineParentCheckpointMismatches: UpDownCounter;
54
50
 
55
51
  // Fisherman fee analysis metrics
56
52
  private fishermanWouldBeIncluded: UpDownCounter;
@@ -68,6 +64,10 @@ export class SequencerMetrics {
68
64
  private fishermanMinedBlobTxPriorityFee: Histogram;
69
65
  private fishermanMinedBlobTxTotalCost: Histogram;
70
66
 
67
+ private blockInterBlockTime: Histogram;
68
+ private lastBlockBuiltTimestamp?: number;
69
+ private lastBlockBuiltSlot?: SlotNumber;
70
+
71
71
  private lastSeenSlot?: SlotNumber;
72
72
 
73
73
  constructor(
@@ -86,9 +86,9 @@ export class SequencerMetrics {
86
86
 
87
87
  this.blockBuildManaPerSecond = this.meter.createGauge(Metrics.SEQUENCER_BLOCK_BUILD_MANA_PER_SECOND);
88
88
 
89
- this.stateTransitionBufferDuration = this.meter.createHistogram(Metrics.SEQUENCER_STATE_TRANSITION_BUFFER_DURATION);
89
+ this.blockInterBlockTime = this.meter.createHistogram(Metrics.SEQUENCER_BLOCK_INTER_BLOCK_TIME);
90
90
 
91
- this.checkpointAttestationDelay = this.meter.createHistogram(Metrics.SEQUENCER_CHECKPOINT_ATTESTATION_DELAY);
91
+ this.stateTransitionBufferDuration = this.meter.createHistogram(Metrics.SEQUENCER_STATE_TRANSITION_BUFFER_DURATION);
92
92
 
93
93
  this.rewards = this.meter.createGauge(Metrics.SEQUENCER_CURRENT_SLOT_REWARDS);
94
94
 
@@ -138,15 +138,23 @@ export class SequencerMetrics {
138
138
  Metrics.SEQUENCER_CHECKPOINT_PROPOSAL_FAILED_COUNT,
139
139
  );
140
140
 
141
- this.checkpointBuildDuration = this.meter.createHistogram(Metrics.SEQUENCER_CHECKPOINT_BUILD_DURATION);
142
- this.checkpointBlockCount = this.meter.createGauge(Metrics.SEQUENCER_CHECKPOINT_BLOCK_COUNT);
143
- this.checkpointTxCount = this.meter.createGauge(Metrics.SEQUENCER_CHECKPOINT_TX_COUNT);
144
- this.checkpointTotalMana = this.meter.createGauge(Metrics.SEQUENCER_CHECKPOINT_TOTAL_MANA);
145
-
146
141
  this.slashingAttempts = createUpDownCounterWithDefault(this.meter, Metrics.SEQUENCER_SLASHING_ATTEMPTS_COUNT);
147
142
 
148
143
  this.pipelineDepth = this.meter.createGauge(Metrics.SEQUENCER_PIPELINE_DEPTH);
149
144
  this.pipelineDiscards = createUpDownCounterWithDefault(this.meter, Metrics.SEQUENCER_PIPELINE_DISCARDS_COUNT);
145
+ this.pipelineParentCheckpointMismatches = createUpDownCounterWithDefault(
146
+ this.meter,
147
+ Metrics.SEQUENCER_PIPELINE_PARENT_CHECKPOINT_MISMATCH_COUNT,
148
+ {
149
+ [Attributes.ERROR_TYPE]: [
150
+ 'archiver-sync-timeout',
151
+ 'parent-not-on-l1',
152
+ 'parent-hash-mismatch',
153
+ 'parent-invalid-attestations',
154
+ 'unexpected-parent-appeared',
155
+ ],
156
+ },
157
+ );
150
158
  this.pipelineDepth.record(0);
151
159
 
152
160
  // Fisherman fee analysis metrics
@@ -211,21 +219,25 @@ export class SequencerMetrics {
211
219
  this.timeToCollectAttestations.record(0);
212
220
  }
213
221
 
214
- public recordCheckpointAttestationDelay(duration: number) {
215
- this.checkpointAttestationDelay.record(duration);
216
- }
217
-
218
222
  public recordCollectedAttestations(count: number, durationMs: number) {
219
223
  this.collectedAttestions.record(count);
220
224
  this.timeToCollectAttestations.record(Math.ceil(durationMs));
221
225
  }
222
226
 
223
- recordBuiltBlock(buildDurationMs: number, totalMana: number) {
227
+ recordBuiltBlock(buildDurationMs: number, totalMana: number, slot: SlotNumber) {
224
228
  this.blockCounter.add(1, {
225
229
  [Attributes.STATUS]: 'built',
226
230
  });
227
231
  this.blockBuildDuration.record(Math.ceil(buildDurationMs));
228
232
  this.blockBuildManaPerSecond.record(Math.ceil((totalMana * 1000) / buildDurationMs));
233
+
234
+ // Only record inter-block time between blocks built within the same slot.
235
+ const now = Date.now();
236
+ if (this.lastBlockBuiltTimestamp !== undefined && this.lastBlockBuiltSlot === slot) {
237
+ this.blockInterBlockTime.record(now - this.lastBlockBuiltTimestamp);
238
+ }
239
+ this.lastBlockBuiltTimestamp = now;
240
+ this.lastBlockBuiltSlot = slot;
229
241
  }
230
242
 
231
243
  recordFailedBlock() {
@@ -248,6 +260,12 @@ export class SequencerMetrics {
248
260
  this.pipelineDiscards.add(count);
249
261
  }
250
262
 
263
+ recordPipelineParentCheckpointMismatch(reason: string) {
264
+ this.pipelineParentCheckpointMismatches.add(1, {
265
+ [Attributes.ERROR_TYPE]: reason,
266
+ });
267
+ }
268
+
251
269
  incOpenSlot(slot: SlotNumber, proposer: string) {
252
270
  // sequencer went through the loop a second time. Noop
253
271
  if (slot === this.lastSeenSlot) {
@@ -306,14 +324,6 @@ export class SequencerMetrics {
306
324
  });
307
325
  }
308
326
 
309
- /** Records aggregate metrics for a completed checkpoint build. */
310
- recordCheckpointBuild(durationMs: number, blockCount: number, txCount: number, totalMana: number) {
311
- this.checkpointBuildDuration.record(Math.ceil(durationMs));
312
- this.checkpointBlockCount.record(blockCount);
313
- this.checkpointTxCount.record(txCount);
314
- this.checkpointTotalMana.record(totalMana);
315
- }
316
-
317
327
  recordSlashingAttempt(actionCount: number) {
318
328
  this.slashingAttempts.add(actionCount);
319
329
  }
@@ -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';
@@ -34,6 +34,7 @@ import type { GlobalVariableBuilder } from '../global_variable_builder/global_bu
34
34
  import type { SequencerPublisherFactory } from '../publisher/sequencer-publisher-factory.js';
35
35
  import type { InvalidateCheckpointRequest, SequencerPublisher } from '../publisher/sequencer-publisher.js';
36
36
  import { CheckpointProposalJob } from './checkpoint_proposal_job.js';
37
+ import { CheckpointProposalJobMetrics } from './checkpoint_proposal_job_metrics.js';
37
38
  import { CheckpointVoter } from './checkpoint_voter.js';
38
39
  import { SequencerInterruptedError, SequencerTooSlowError } from './errors.js';
39
40
  import type { SequencerEvents } from './events.js';
@@ -56,6 +57,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
56
57
  private runningPromise?: RunningPromise;
57
58
  private state = SequencerState.STOPPED;
58
59
  private metrics: SequencerMetrics;
60
+ private checkpointProposalJobMetrics: CheckpointProposalJobMetrics;
59
61
 
60
62
  /** The last slot for which we attempted to perform our voting duties with degraded block production */
61
63
  private lastSlotForFallbackVote: SlotNumber | undefined;
@@ -73,7 +75,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
73
75
  private lastEpochForStrategyComparison: EpochNumber | undefined;
74
76
 
75
77
  /** The last checkpoint proposal job, tracked so we can await its pending L1 submission during shutdown. */
76
- private lastCheckpointProposalJob: CheckpointProposalJob | undefined;
78
+ protected lastCheckpointProposalJob: CheckpointProposalJob | undefined;
77
79
 
78
80
  /** The maximum number of seconds that the sequencer can be into a slot to transition to a particular state. */
79
81
  protected timetable!: SequencerTimetable;
@@ -107,6 +109,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
107
109
  }
108
110
 
109
111
  this.metrics = new SequencerMetrics(telemetry, this.rollupContract, 'Sequencer');
112
+ this.checkpointProposalJobMetrics = new CheckpointProposalJobMetrics(telemetry);
110
113
  this.updateConfig(config);
111
114
  }
112
115
 
@@ -115,14 +118,16 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
115
118
  const filteredConfig = pickFromSchema(config, SequencerConfigSchema);
116
119
  this.log.info(`Updated sequencer config`, omit(filteredConfig, 'txPublicSetupAllowListExtend'));
117
120
  this.config = merge(this.config, filteredConfig);
121
+ const p2pPropagationTime = this.config.attestationPropagationTime;
118
122
  this.timetable = new SequencerTimetable(
119
123
  {
120
124
  ethereumSlotDuration: this.l1Constants.ethereumSlotDuration,
121
125
  aztecSlotDuration: this.aztecSlotDuration,
122
126
  l1PublishingTime: this.l1PublishingTime,
123
- p2pPropagationTime: this.config.attestationPropagationTime,
127
+ p2pPropagationTime,
124
128
  blockDurationMs: this.config.blockDurationMs,
125
129
  enforce: this.config.enforceTimeTable,
130
+ pipelining: this.epochCache.isProposerPipeliningEnabled(),
126
131
  },
127
132
  this.metrics,
128
133
  this.log,
@@ -146,6 +151,11 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
146
151
  this.log.info('Started sequencer');
147
152
  }
148
153
 
154
+ /** Triggers an immediate run of the sequencer, bypassing the polling interval. */
155
+ public trigger() {
156
+ return this.runningPromise?.trigger();
157
+ }
158
+
149
159
  /** Stops the sequencer from building blocks and moves to STOPPED state. */
150
160
  public async stop(): Promise<void> {
151
161
  this.log.info(`Stopping sequencer`);
@@ -299,16 +309,6 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
299
309
  // Next checkpoint follows from the last synced one
300
310
  const checkpointNumber = CheckpointNumber(syncedTo.checkpointNumber + 1);
301
311
 
302
- // Guard: don't exceed 1-deep pipeline. Without a proposed checkpoint, we can only build
303
- // confirmed + 1. With a proposed checkpoint, we can build confirmed + 2.
304
- const confirmedCkpt = syncedTo.checkpointedCheckpointNumber;
305
- if (checkpointNumber > confirmedCkpt + 2) {
306
- this.log.warn(
307
- `Skipping slot ${targetSlot}: checkpoint ${checkpointNumber} exceeds max pipeline depth (confirmed=${confirmedCkpt})`,
308
- );
309
- return undefined;
310
- }
311
-
312
312
  const logCtx = {
313
313
  nowSeconds,
314
314
  syncedToL2Slot: syncedTo.syncedL2Slot,
@@ -329,6 +329,16 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
329
329
  return undefined;
330
330
  }
331
331
 
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
+
332
342
  // Check that the target slot is not taken by a block already (should never happen, since only us can propose for this slot)
333
343
  if (syncedTo.blockData && syncedTo.blockData.header.getSlot() >= targetSlot) {
334
344
  this.log.warn(
@@ -360,17 +370,13 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
360
370
  // The L1 contract reads archives[proposedCheckpointNumber] and compares it with the provided archive.
361
371
  // When invalidating or pipelining, the local archive may differ from L1's, so we adjust accordingly.
362
372
  let archiveForCheck = syncedTo.archive;
363
- const l1Overrides: {
364
- forcePendingCheckpointNumber?: CheckpointNumber;
365
- forceArchive?: { checkpointNumber: CheckpointNumber; archive: Fr };
366
- } = {};
373
+ const l1SimulationOverridesBuilder = new SimulationOverridesBuilder();
367
374
 
368
375
  if (this.epochCache.isProposerPipeliningEnabled() && syncedTo.hasProposedCheckpoint) {
369
376
  // Parent checkpoint hasn't landed on L1 yet. Override both the proposed checkpoint number
370
377
  // and the archive at that checkpoint so L1 simulation sees the correct chain tip.
371
378
  const parentCheckpointNumber = CheckpointNumber(checkpointNumber - 1);
372
- l1Overrides.forcePendingCheckpointNumber = parentCheckpointNumber;
373
- l1Overrides.forceArchive = { checkpointNumber: parentCheckpointNumber, archive: syncedTo.archive };
379
+ l1SimulationOverridesBuilder.forPendingCheckpoint(parentCheckpointNumber).withPendingArchive(syncedTo.archive);
374
380
  this.metrics.recordPipelineDepth(1);
375
381
 
376
382
  this.log.verbose(
@@ -382,13 +388,18 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
382
388
  // After invalidation, L1 will roll back to checkpoint N-1. The archive at N-1 already
383
389
  // exists on L1, so we just pass the matching archive (the lastArchive of the invalid checkpoint).
384
390
  archiveForCheck = invalidateCheckpoint.lastArchive;
385
- l1Overrides.forcePendingCheckpointNumber = invalidateCheckpoint.forcePendingCheckpointNumber;
391
+ l1SimulationOverridesBuilder.forPendingCheckpoint(invalidateCheckpoint.forcePendingCheckpointNumber);
386
392
  this.metrics.recordPipelineDepth(0);
387
393
  } else {
388
394
  this.metrics.recordPipelineDepth(0);
389
395
  }
390
396
 
391
- const canProposeCheck = await publisher.canProposeAt(archiveForCheck, proposer ?? EthAddress.ZERO, l1Overrides);
397
+ const simulationOverridesPlan = l1SimulationOverridesBuilder.build();
398
+ const canProposeCheck = await publisher.canProposeAt(
399
+ archiveForCheck,
400
+ proposer ?? EthAddress.ZERO,
401
+ simulationOverridesPlan,
402
+ );
392
403
 
393
404
  if (canProposeCheck === undefined) {
394
405
  this.log.warn(
@@ -484,6 +495,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
484
495
  this.epochCache,
485
496
  this.dateProvider,
486
497
  this.metrics,
498
+ this.checkpointProposalJobMetrics.createRecorder(),
487
499
  this,
488
500
  this.setState.bind(this),
489
501
  this.tracer,
@@ -567,12 +579,12 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
567
579
  .getL2Tips()
568
580
  .then(t => ({ proposed: t.proposed, checkpointed: t.checkpointed, proposedCheckpoint: t.proposedCheckpoint })),
569
581
  this.p2pClient.getStatus().then(p2p => p2p.syncedToL2Block),
570
- this.l1ToL2MessageSource.getL2Tips().then(t => t.proposed),
582
+ this.l1ToL2MessageSource.getL2Tips().then(t => ({ proposed: t.proposed, checkpointed: t.checkpointed })),
571
583
  this.l2BlockSource.getPendingChainValidationStatus(),
572
584
  this.l2BlockSource.getProposedCheckpointOnly(),
573
585
  ] as const);
574
586
 
575
- const [worldState, l2Tips, p2p, l1ToL2MessageSource, pendingChainValidationStatus, proposedCheckpointData] =
587
+ const [worldState, l2Tips, p2p, l1ToL2MessageSourceTips, pendingChainValidationStatus, proposedCheckpointData] =
576
588
  syncedBlocks;
577
589
 
578
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,
@@ -580,19 +592,25 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
580
592
  // TODO(palla/mbps): Fix the above. All components should be able to handle dynamic genesis block hashes.
581
593
  const result =
582
594
  (l2Tips.proposed.number === 0 &&
595
+ l2Tips.checkpointed.block.number === 0 &&
596
+ l2Tips.checkpointed.checkpoint.number === 0 &&
583
597
  worldState.number === 0 &&
584
598
  p2p.number === 0 &&
585
- l1ToL2MessageSource.number === 0) ||
599
+ l1ToL2MessageSourceTips.proposed.number === 0 &&
600
+ l1ToL2MessageSourceTips.checkpointed.block.number === 0 &&
601
+ l1ToL2MessageSourceTips.checkpointed.checkpoint.number === 0) ||
586
602
  (worldState.hash === l2Tips.proposed.hash &&
587
603
  p2p.hash === l2Tips.proposed.hash &&
588
- l1ToL2MessageSource.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);
589
607
 
590
608
  if (!result) {
591
609
  this.log.debug(`Sequencer sync check failed`, {
592
610
  worldState,
593
611
  l2BlockSource: l2Tips.proposed,
594
612
  p2p,
595
- l1ToL2MessageSource,
613
+ l1ToL2MessageSourceTips,
596
614
  });
597
615
  return undefined;
598
616
  }
@@ -2,8 +2,10 @@ import type { Logger } from '@aztec/foundation/log';
2
2
  import {
3
3
  CHECKPOINT_ASSEMBLE_TIME,
4
4
  CHECKPOINT_INITIALIZATION_TIME,
5
+ type CheckpointTiming,
5
6
  DEFAULT_P2P_PROPAGATION_TIME,
6
7
  MIN_EXECUTION_TIME,
8
+ createCheckpointTimingModel,
7
9
  } from '@aztec/stdlib/timetable';
8
10
 
9
11
  import { SequencerTooSlowError } from './errors.js';
@@ -11,6 +13,8 @@ import type { SequencerMetrics } from './metrics.js';
11
13
  import { SequencerState } from './utils.js';
12
14
 
13
15
  export class SequencerTimetable {
16
+ private readonly checkpointTiming: CheckpointTiming;
17
+
14
18
  /**
15
19
  * How late into the slot can we be to start working. Computed as the total time needed for assembling and publishing a block,
16
20
  * assuming an execution time equal to `minExecutionTime`, subtracted from the slot duration. This means that, if the proposer
@@ -70,6 +74,15 @@ export class SequencerTimetable {
70
74
  /** Maximum number of blocks that can be built in this slot configuration */
71
75
  public readonly maxNumberOfBlocks: number;
72
76
 
77
+ /** Whether pipelining is enabled (checkpoint finalization deferred to next slot). */
78
+ public readonly pipelining: boolean;
79
+
80
+ /**
81
+ * How far into the target slot attestation collection can extend when pipelining.
82
+ * Covers validator re-execution (one block duration) plus one-way attestation return.
83
+ */
84
+ public readonly pipeliningAttestationGracePeriod: number;
85
+
73
86
  constructor(
74
87
  opts: {
75
88
  ethereumSlotDuration: number;
@@ -78,6 +91,7 @@ export class SequencerTimetable {
78
91
  p2pPropagationTime?: number;
79
92
  blockDurationMs?: number;
80
93
  enforce: boolean;
94
+ pipelining?: boolean;
81
95
  },
82
96
  private readonly metrics?: SequencerMetrics,
83
97
  private readonly log?: Logger,
@@ -85,52 +99,31 @@ export class SequencerTimetable {
85
99
  this.ethereumSlotDuration = opts.ethereumSlotDuration;
86
100
  this.aztecSlotDuration = opts.aztecSlotDuration;
87
101
  this.l1PublishingTime = opts.l1PublishingTime;
88
- this.p2pPropagationTime = opts.p2pPropagationTime ?? DEFAULT_P2P_PROPAGATION_TIME;
89
102
  this.blockDuration = opts.blockDurationMs ? opts.blockDurationMs / 1000 : undefined;
90
103
  this.enforce = opts.enforce;
104
+ this.pipelining = opts.pipelining ?? false;
105
+
106
+ this.checkpointTiming = createCheckpointTimingModel({
107
+ aztecSlotDuration: this.aztecSlotDuration,
108
+ ethereumSlotDuration: this.ethereumSlotDuration,
109
+ blockDuration: this.blockDuration,
110
+ l1PublishingTime: this.l1PublishingTime,
111
+ p2pPropagationTime: opts.p2pPropagationTime ?? DEFAULT_P2P_PROPAGATION_TIME,
112
+ pipelining: this.pipelining,
113
+ });
91
114
 
92
- // Assume zero-cost propagation time and faster runs in test environments where L1 slot duration is shortened
93
- if (this.ethereumSlotDuration < 8) {
94
- this.p2pPropagationTime = 0;
95
- this.checkpointAssembleTime = 0.5;
96
- this.checkpointInitializationTime = 0.5;
97
- this.minExecutionTime = 1;
98
- }
99
-
100
- // Min execution time cannot be less than the block duration if set
101
- if (this.blockDuration !== undefined && this.minExecutionTime > this.blockDuration) {
102
- this.minExecutionTime = this.blockDuration;
103
- }
115
+ this.p2pPropagationTime = this.checkpointTiming.p2pPropagationTime;
116
+ this.checkpointAssembleTime = this.checkpointTiming.checkpointAssembleTime;
117
+ this.checkpointInitializationTime = this.checkpointTiming.checkpointInitializationTime;
118
+ this.minExecutionTime = this.checkpointTiming.minExecutionTime;
104
119
 
105
120
  // Calculate initialization offset - estimate of time needed for sync + proposer check
106
121
  // This is the baseline for all sub-slot deadlines
107
- this.initializationOffset = this.checkpointInitializationTime;
108
-
109
- // Calculate total checkpoint finalization time (assembly + attestations + L1 publishing)
110
- this.checkpointFinalizationTime =
111
- this.checkpointAssembleTime +
112
- this.p2pPropagationTime * 2 + // Round-trip propagation
113
- this.l1PublishingTime; // L1 publishing
114
-
115
- // Calculate maximum number of blocks that fit in this slot
116
- if (!this.blockDuration) {
117
- this.maxNumberOfBlocks = 1; // Single block per slot
118
- } else {
119
- const timeReservedAtEnd =
120
- this.blockDuration + // Last sub-slot for validator re-execution
121
- this.checkpointFinalizationTime; // Checkpoint finalization
122
- const timeAvailableForBlocks = this.aztecSlotDuration - this.initializationOffset - timeReservedAtEnd;
123
- this.maxNumberOfBlocks = Math.floor(timeAvailableForBlocks / this.blockDuration);
124
- }
125
-
126
- // Minimum work to do within a slot for building a block with the minimum time for execution and publishing its checkpoint
127
- const minWorkToDo =
128
- this.initializationOffset +
129
- this.minExecutionTime * 2 + // Execution and reexecution
130
- this.checkpointFinalizationTime;
131
-
132
- const initializeDeadline = this.aztecSlotDuration - minWorkToDo;
133
- this.initializeDeadline = initializeDeadline;
122
+ this.initializationOffset = this.checkpointTiming.checkpointInitializationTime;
123
+ this.checkpointFinalizationTime = this.checkpointTiming.checkpointFinalizationTime;
124
+ this.pipeliningAttestationGracePeriod = this.checkpointTiming.pipeliningAttestationGracePeriod;
125
+ this.maxNumberOfBlocks = this.checkpointTiming.calculateMaxBlocksPerSlot();
126
+ this.initializeDeadline = this.checkpointTiming.initializeDeadline;
134
127
 
135
128
  this.log?.info(
136
129
  `Sequencer timetable initialized with ${this.maxNumberOfBlocks} blocks per slot (${this.enforce ? 'enforced' : 'not enforced'})`,
@@ -144,19 +137,37 @@ export class SequencerTimetable {
144
137
  blockAssembleTime: this.checkpointAssembleTime,
145
138
  initializeDeadline: this.initializeDeadline,
146
139
  enforce: this.enforce,
147
- minWorkToDo,
140
+ pipelining: this.pipelining,
141
+ pipeliningAttestationGracePeriod: this.pipeliningAttestationGracePeriod,
142
+ minWorkToDo: this.checkpointTiming.minimumBuildSlotWork,
148
143
  blockDuration: this.blockDuration,
149
144
  maxNumberOfBlocks: this.maxNumberOfBlocks,
150
145
  },
151
146
  );
152
147
 
153
- if (initializeDeadline <= 0) {
148
+ if (this.initializeDeadline <= 0) {
154
149
  throw new Error(
155
- `Block proposal initialize deadline cannot be negative (got ${initializeDeadline} from total time needed ${minWorkToDo} and a slot duration of ${this.aztecSlotDuration}).`,
150
+ `Block proposal initialize deadline cannot be negative (got ${this.initializeDeadline} from total time needed ${this.checkpointTiming.minimumBuildSlotWork} and a slot duration of ${this.aztecSlotDuration}).`,
156
151
  );
157
152
  }
158
153
  }
159
154
 
155
+ public getCheckpointAssemblyDeadline(): number {
156
+ return this.checkpointTiming.checkpointAssemblyDeadline;
157
+ }
158
+
159
+ public getCheckpointAttestationDeadline(): number {
160
+ return this.checkpointTiming.checkpointAttestationDeadline;
161
+ }
162
+
163
+ public getCheckpointAttestationStartDeadline(): number {
164
+ return this.checkpointTiming.checkpointAttestationStartDeadline;
165
+ }
166
+
167
+ public getCheckpointPublishingDeadline(): number {
168
+ return this.checkpointTiming.checkpointPublishingDeadline;
169
+ }
170
+
160
171
  public getMaxAllowedTime(
161
172
  state: Extract<SequencerState, SequencerState.STOPPED | SequencerState.IDLE | SequencerState.SYNCHRONIZING>,
162
173
  ): undefined;
@@ -179,10 +190,11 @@ export class SequencerTimetable {
179
190
  case SequencerState.WAITING_UNTIL_NEXT_BLOCK:
180
191
  return this.initializeDeadline + this.checkpointInitializationTime;
181
192
  case SequencerState.ASSEMBLING_CHECKPOINT:
193
+ return this.getCheckpointAssemblyDeadline();
182
194
  case SequencerState.COLLECTING_ATTESTATIONS:
183
- return this.aztecSlotDuration - this.l1PublishingTime - 2 * this.p2pPropagationTime;
195
+ return this.getCheckpointAttestationStartDeadline();
184
196
  case SequencerState.PUBLISHING_CHECKPOINT:
185
- return this.aztecSlotDuration - this.l1PublishingTime;
197
+ return this.getCheckpointPublishingDeadline();
186
198
  default: {
187
199
  const _exhaustiveCheck: never = state;
188
200
  throw new Error(`Unexpected state: ${state}`);