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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) 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 +2 -1
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +16 -14
  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 +62 -94
  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 +33 -22
  34. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  35. package/dest/sequencer/checkpoint_proposal_job.js +479 -170
  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 +9 -10
  42. package/dest/sequencer/metrics.d.ts.map +1 -1
  43. package/dest/sequencer/metrics.js +34 -20
  44. package/dest/sequencer/sequencer.d.ts +16 -5
  45. package/dest/sequencer/sequencer.d.ts.map +1 -1
  46. package/dest/sequencer/sequencer.js +63 -30
  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/dest/test/utils.d.ts +1 -1
  51. package/dest/test/utils.d.ts.map +1 -1
  52. package/dest/test/utils.js +7 -6
  53. package/package.json +27 -27
  54. package/src/client/sequencer-client.ts +5 -7
  55. package/src/config.ts +15 -11
  56. package/src/global_variable_builder/README.md +44 -0
  57. package/src/global_variable_builder/fee_predictor.ts +172 -0
  58. package/src/global_variable_builder/fee_provider.ts +75 -0
  59. package/src/global_variable_builder/global_builder.ts +7 -54
  60. package/src/global_variable_builder/index.ts +2 -0
  61. package/src/publisher/config.ts +6 -4
  62. package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +3 -1
  63. package/src/publisher/sequencer-publisher-factory.ts +0 -3
  64. package/src/publisher/sequencer-publisher.ts +108 -156
  65. package/src/sequencer/README.md +82 -13
  66. package/src/sequencer/chain_state_overrides.ts +87 -0
  67. package/src/sequencer/checkpoint_proposal_job.ts +572 -218
  68. package/src/sequencer/checkpoint_proposal_job_metrics.ts +128 -0
  69. package/src/sequencer/events.ts +5 -0
  70. package/src/sequencer/metrics.ts +43 -24
  71. package/src/sequencer/sequencer.ts +81 -35
  72. package/src/sequencer/timetable.ts +57 -45
  73. package/src/test/utils.ts +28 -10
@@ -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
  };
@@ -26,6 +26,7 @@ export class SequencerMetrics {
26
26
  private blockBuildDuration: Histogram;
27
27
  private blockBuildManaPerSecond: Gauge;
28
28
  private stateTransitionBufferDuration: Histogram;
29
+ private stateDuration: Histogram;
29
30
 
30
31
  // these are gauges because for individual sequencers building a block is not something that happens often enough to warrant a histogram
31
32
  private timeToCollectAttestations: Gauge;
@@ -44,13 +45,9 @@ export class SequencerMetrics {
44
45
  private checkpointProposalFailed: UpDownCounter;
45
46
  private checkpointSuccess: UpDownCounter;
46
47
  private slashingAttempts: UpDownCounter;
47
- private checkpointAttestationDelay: Histogram;
48
- private checkpointBuildDuration: Histogram;
49
- private checkpointBlockCount: Gauge;
50
- private checkpointTxCount: Gauge;
51
- private checkpointTotalMana: Gauge;
52
48
  private pipelineDepth: Gauge;
53
49
  private pipelineDiscards: UpDownCounter;
50
+ private pipelineParentCheckpointMismatches: UpDownCounter;
54
51
 
55
52
  // Fisherman fee analysis metrics
56
53
  private fishermanWouldBeIncluded: UpDownCounter;
@@ -68,6 +65,10 @@ export class SequencerMetrics {
68
65
  private fishermanMinedBlobTxPriorityFee: Histogram;
69
66
  private fishermanMinedBlobTxTotalCost: Histogram;
70
67
 
68
+ private blockInterBlockTime: Histogram;
69
+ private lastBlockBuiltTimestamp?: number;
70
+ private lastBlockBuiltSlot?: SlotNumber;
71
+
71
72
  private lastSeenSlot?: SlotNumber;
72
73
 
73
74
  constructor(
@@ -86,9 +87,11 @@ export class SequencerMetrics {
86
87
 
87
88
  this.blockBuildManaPerSecond = this.meter.createGauge(Metrics.SEQUENCER_BLOCK_BUILD_MANA_PER_SECOND);
88
89
 
90
+ this.blockInterBlockTime = this.meter.createHistogram(Metrics.SEQUENCER_BLOCK_INTER_BLOCK_TIME);
91
+
89
92
  this.stateTransitionBufferDuration = this.meter.createHistogram(Metrics.SEQUENCER_STATE_TRANSITION_BUFFER_DURATION);
90
93
 
91
- this.checkpointAttestationDelay = this.meter.createHistogram(Metrics.SEQUENCER_CHECKPOINT_ATTESTATION_DELAY);
94
+ this.stateDuration = this.meter.createHistogram(Metrics.SEQUENCER_STATE_DURATION);
92
95
 
93
96
  this.rewards = this.meter.createGauge(Metrics.SEQUENCER_CURRENT_SLOT_REWARDS);
94
97
 
@@ -138,15 +141,23 @@ export class SequencerMetrics {
138
141
  Metrics.SEQUENCER_CHECKPOINT_PROPOSAL_FAILED_COUNT,
139
142
  );
140
143
 
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
144
  this.slashingAttempts = createUpDownCounterWithDefault(this.meter, Metrics.SEQUENCER_SLASHING_ATTEMPTS_COUNT);
147
145
 
148
146
  this.pipelineDepth = this.meter.createGauge(Metrics.SEQUENCER_PIPELINE_DEPTH);
149
147
  this.pipelineDiscards = createUpDownCounterWithDefault(this.meter, Metrics.SEQUENCER_PIPELINE_DISCARDS_COUNT);
148
+ this.pipelineParentCheckpointMismatches = createUpDownCounterWithDefault(
149
+ this.meter,
150
+ Metrics.SEQUENCER_PIPELINE_PARENT_CHECKPOINT_MISMATCH_COUNT,
151
+ {
152
+ [Attributes.ERROR_TYPE]: [
153
+ 'archiver-sync-timeout',
154
+ 'parent-not-on-l1',
155
+ 'parent-hash-mismatch',
156
+ 'parent-invalid-attestations',
157
+ 'unexpected-parent-appeared',
158
+ ],
159
+ },
160
+ );
150
161
  this.pipelineDepth.record(0);
151
162
 
152
163
  // Fisherman fee analysis metrics
@@ -211,21 +222,25 @@ export class SequencerMetrics {
211
222
  this.timeToCollectAttestations.record(0);
212
223
  }
213
224
 
214
- public recordCheckpointAttestationDelay(duration: number) {
215
- this.checkpointAttestationDelay.record(duration);
216
- }
217
-
218
225
  public recordCollectedAttestations(count: number, durationMs: number) {
219
226
  this.collectedAttestions.record(count);
220
227
  this.timeToCollectAttestations.record(Math.ceil(durationMs));
221
228
  }
222
229
 
223
- recordBuiltBlock(buildDurationMs: number, totalMana: number) {
230
+ recordBuiltBlock(buildDurationMs: number, totalMana: number, slot: SlotNumber) {
224
231
  this.blockCounter.add(1, {
225
232
  [Attributes.STATUS]: 'built',
226
233
  });
227
234
  this.blockBuildDuration.record(Math.ceil(buildDurationMs));
228
235
  this.blockBuildManaPerSecond.record(Math.ceil((totalMana * 1000) / buildDurationMs));
236
+
237
+ // Only record inter-block time between blocks built within the same slot.
238
+ const now = Date.now();
239
+ if (this.lastBlockBuiltTimestamp !== undefined && this.lastBlockBuiltSlot === slot) {
240
+ this.blockInterBlockTime.record(now - this.lastBlockBuiltTimestamp);
241
+ }
242
+ this.lastBlockBuiltTimestamp = now;
243
+ this.lastBlockBuiltSlot = slot;
229
244
  }
230
245
 
231
246
  recordFailedBlock() {
@@ -240,6 +255,12 @@ export class SequencerMetrics {
240
255
  });
241
256
  }
242
257
 
258
+ recordStateDuration(durationMs: number, state: SequencerState) {
259
+ this.stateDuration.record(Math.ceil(durationMs), {
260
+ [Attributes.SEQUENCER_STATE]: state,
261
+ });
262
+ }
263
+
243
264
  recordPipelineDepth(depth: number) {
244
265
  this.pipelineDepth.record(depth);
245
266
  }
@@ -248,6 +269,12 @@ export class SequencerMetrics {
248
269
  this.pipelineDiscards.add(count);
249
270
  }
250
271
 
272
+ recordPipelineParentCheckpointMismatch(reason: string) {
273
+ this.pipelineParentCheckpointMismatches.add(1, {
274
+ [Attributes.ERROR_TYPE]: reason,
275
+ });
276
+ }
277
+
251
278
  incOpenSlot(slot: SlotNumber, proposer: string) {
252
279
  // sequencer went through the loop a second time. Noop
253
280
  if (slot === this.lastSeenSlot) {
@@ -306,14 +333,6 @@ export class SequencerMetrics {
306
333
  });
307
334
  }
308
335
 
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
336
  recordSlashingAttempt(actionCount: number) {
318
337
  this.slashingAttempts.add(actionCount);
319
338
  }
@@ -1,12 +1,12 @@
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';
@@ -14,6 +14,7 @@ import type { P2P } from '@aztec/p2p';
14
14
  import type { SlasherClientInterface } from '@aztec/slasher';
15
15
  import type { BlockData, L2BlockSink, L2BlockSource, ValidateCheckpointResult } from '@aztec/stdlib/block';
16
16
  import type { Checkpoint, ProposedCheckpointData } from '@aztec/stdlib/checkpoint';
17
+ import type { ChainConfig } from '@aztec/stdlib/config';
17
18
  import { getSlotStartBuildTimestamp } from '@aztec/stdlib/epoch-helpers';
18
19
  import {
19
20
  type ResolvedSequencerConfig,
@@ -22,6 +23,7 @@ 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';
@@ -34,6 +36,7 @@ import type { GlobalVariableBuilder } from '../global_variable_builder/global_bu
34
36
  import type { SequencerPublisherFactory } from '../publisher/sequencer-publisher-factory.js';
35
37
  import type { InvalidateCheckpointRequest, SequencerPublisher } from '../publisher/sequencer-publisher.js';
36
38
  import { CheckpointProposalJob } from './checkpoint_proposal_job.js';
39
+ import { CheckpointProposalJobMetrics } from './checkpoint_proposal_job_metrics.js';
37
40
  import { CheckpointVoter } from './checkpoint_voter.js';
38
41
  import { SequencerInterruptedError, SequencerTooSlowError } from './errors.js';
39
42
  import type { SequencerEvents } from './events.js';
@@ -55,7 +58,11 @@ export { SequencerState };
55
58
  export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<SequencerEvents>) {
56
59
  private runningPromise?: RunningPromise;
57
60
  private state = SequencerState.STOPPED;
61
+ private stateSlotNumber: SlotNumber | undefined;
62
+ private stateEnteredAtMs = performance.now();
58
63
  private metrics: SequencerMetrics;
64
+ private checkpointProposalJobMetrics: CheckpointProposalJobMetrics;
65
+ private readonly stateLog: Logger;
59
66
 
60
67
  /** The last slot for which we attempted to perform our voting duties with degraded block production */
61
68
  private lastSlotForFallbackVote: SlotNumber | undefined;
@@ -73,13 +80,14 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
73
80
  private lastEpochForStrategyComparison: EpochNumber | undefined;
74
81
 
75
82
  /** The last checkpoint proposal job, tracked so we can await its pending L1 submission during shutdown. */
76
- private lastCheckpointProposalJob: CheckpointProposalJob | undefined;
83
+ protected lastCheckpointProposalJob: CheckpointProposalJob | undefined;
77
84
 
78
85
  /** The maximum number of seconds that the sequencer can be into a slot to transition to a particular state. */
79
86
  protected timetable!: SequencerTimetable;
80
87
 
81
88
  /** Config for the sequencer */
82
89
  protected config: ResolvedSequencerConfig = DefaultSequencerConfig;
90
+ private readonly signatureContext: CoordinationSignatureContext;
83
91
 
84
92
  constructor(
85
93
  protected publisherFactory: SequencerPublisherFactory,
@@ -95,18 +103,24 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
95
103
  protected dateProvider: DateProvider,
96
104
  protected epochCache: EpochCache,
97
105
  protected rollupContract: RollupContract,
98
- config: SequencerConfig,
106
+ config: SequencerConfig & Pick<ChainConfig, 'l1ChainId' | 'l1Contracts'>,
99
107
  protected telemetry: TelemetryClient = getTelemetryClient(),
100
108
  protected log = createLogger('sequencer'),
101
109
  ) {
102
110
  super();
111
+ this.stateLog = log.createChild('state');
103
112
 
104
113
  // Add [FISHERMAN] prefix to logger if in fisherman mode
105
114
  if (config.fishermanMode) {
106
115
  this.log = log.createChild('[FISHERMAN]');
107
116
  }
108
117
 
118
+ this.signatureContext = {
119
+ chainId: config.l1ChainId,
120
+ rollupAddress: config.l1Contracts.rollupAddress,
121
+ };
109
122
  this.metrics = new SequencerMetrics(telemetry, this.rollupContract, 'Sequencer');
123
+ this.checkpointProposalJobMetrics = new CheckpointProposalJobMetrics(telemetry);
110
124
  this.updateConfig(config);
111
125
  }
112
126
 
@@ -115,14 +129,16 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
115
129
  const filteredConfig = pickFromSchema(config, SequencerConfigSchema);
116
130
  this.log.info(`Updated sequencer config`, omit(filteredConfig, 'txPublicSetupAllowListExtend'));
117
131
  this.config = merge(this.config, filteredConfig);
132
+ const p2pPropagationTime = this.config.attestationPropagationTime;
118
133
  this.timetable = new SequencerTimetable(
119
134
  {
120
135
  ethereumSlotDuration: this.l1Constants.ethereumSlotDuration,
121
136
  aztecSlotDuration: this.aztecSlotDuration,
122
137
  l1PublishingTime: this.l1PublishingTime,
123
- p2pPropagationTime: this.config.attestationPropagationTime,
138
+ p2pPropagationTime,
124
139
  blockDurationMs: this.config.blockDurationMs,
125
140
  enforce: this.config.enforceTimeTable,
141
+ pipelining: this.epochCache.isProposerPipeliningEnabled(),
126
142
  },
127
143
  this.metrics,
128
144
  this.log,
@@ -146,6 +162,11 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
146
162
  this.log.info('Started sequencer');
147
163
  }
148
164
 
165
+ /** Triggers an immediate run of the sequencer, bypassing the polling interval. */
166
+ public trigger() {
167
+ return this.runningPromise?.trigger();
168
+ }
169
+
149
170
  /** Stops the sequencer from building blocks and moves to STOPPED state. */
150
171
  public async stop(): Promise<void> {
151
172
  this.log.info(`Stopping sequencer`);
@@ -299,16 +320,6 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
299
320
  // Next checkpoint follows from the last synced one
300
321
  const checkpointNumber = CheckpointNumber(syncedTo.checkpointNumber + 1);
301
322
 
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
323
  const logCtx = {
313
324
  nowSeconds,
314
325
  syncedToL2Slot: syncedTo.syncedL2Slot,
@@ -329,6 +340,16 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
329
340
  return undefined;
330
341
  }
331
342
 
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
+
332
353
  // Check that the target slot is not taken by a block already (should never happen, since only us can propose for this slot)
333
354
  if (syncedTo.blockData && syncedTo.blockData.header.getSlot() >= targetSlot) {
334
355
  this.log.warn(
@@ -360,18 +381,14 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
360
381
  // The L1 contract reads archives[proposedCheckpointNumber] and compares it with the provided archive.
361
382
  // When invalidating or pipelining, the local archive may differ from L1's, so we adjust accordingly.
362
383
  let archiveForCheck = syncedTo.archive;
363
- const l1Overrides: {
364
- forcePendingCheckpointNumber?: CheckpointNumber;
365
- forceArchive?: { checkpointNumber: CheckpointNumber; archive: Fr };
366
- } = {};
384
+ const l1SimulationOverridesBuilder = new SimulationOverridesBuilder();
367
385
 
368
386
  if (this.epochCache.isProposerPipeliningEnabled() && syncedTo.hasProposedCheckpoint) {
369
387
  // Parent checkpoint hasn't landed on L1 yet. Override both the proposed checkpoint number
370
388
  // and the archive at that checkpoint so L1 simulation sees the correct chain tip.
371
389
  const parentCheckpointNumber = CheckpointNumber(checkpointNumber - 1);
372
- l1Overrides.forcePendingCheckpointNumber = parentCheckpointNumber;
373
- l1Overrides.forceArchive = { checkpointNumber: parentCheckpointNumber, archive: syncedTo.archive };
374
- this.metrics.recordPipelineDepth(1);
390
+ l1SimulationOverridesBuilder.forPendingCheckpoint(parentCheckpointNumber).withPendingArchive(syncedTo.archive);
391
+ this.metrics.recordPipelineDepth(syncedTo.checkpointNumber - syncedTo.checkpointedCheckpointNumber);
375
392
 
376
393
  this.log.verbose(
377
394
  `Building on top of proposed checkpoint (pending=${syncedTo.proposedCheckpointData?.checkpointNumber})`,
@@ -382,13 +399,18 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
382
399
  // After invalidation, L1 will roll back to checkpoint N-1. The archive at N-1 already
383
400
  // exists on L1, so we just pass the matching archive (the lastArchive of the invalid checkpoint).
384
401
  archiveForCheck = invalidateCheckpoint.lastArchive;
385
- l1Overrides.forcePendingCheckpointNumber = invalidateCheckpoint.forcePendingCheckpointNumber;
402
+ l1SimulationOverridesBuilder.forPendingCheckpoint(invalidateCheckpoint.forcePendingCheckpointNumber);
386
403
  this.metrics.recordPipelineDepth(0);
387
404
  } else {
388
405
  this.metrics.recordPipelineDepth(0);
389
406
  }
390
407
 
391
- const canProposeCheck = await publisher.canProposeAt(archiveForCheck, proposer ?? EthAddress.ZERO, l1Overrides);
408
+ const simulationOverridesPlan = l1SimulationOverridesBuilder.build();
409
+ const canProposeCheck = await publisher.canProposeAt(
410
+ archiveForCheck,
411
+ proposer ?? EthAddress.ZERO,
412
+ simulationOverridesPlan,
413
+ );
392
414
 
393
415
  if (canProposeCheck === undefined) {
394
416
  this.log.warn(
@@ -478,12 +500,14 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
478
500
  this.checkpointsBuilder,
479
501
  this.l2BlockSource,
480
502
  this.l1Constants,
503
+ this.signatureContext,
481
504
  this.config,
482
505
  this.timetable,
483
506
  this.slasherClient,
484
507
  this.epochCache,
485
508
  this.dateProvider,
486
509
  this.metrics,
510
+ this.checkpointProposalJobMetrics.createRecorder(),
487
511
  this,
488
512
  this.setState.bind(this),
489
513
  this.tracer,
@@ -524,19 +548,35 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
524
548
  this.timetable.assertTimeLeft(proposedState, secondsIntoSlot);
525
549
  }
526
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
+
527
557
  const boringStates = [SequencerState.IDLE, SequencerState.SYNCHRONIZING];
528
558
  const logLevel =
529
- boringStates.includes(proposedState) && boringStates.includes(this.state)
530
- ? ('trace' as const)
531
- : ('debug' as const);
532
- 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
+ });
533
568
 
534
569
  this.emit('state-changed', {
535
- oldState: this.state,
570
+ oldState,
536
571
  newState: proposedState,
537
572
  secondsIntoSlot,
538
573
  slot: slotNumber,
539
574
  });
575
+ if (stateChanged) {
576
+ this.metrics.recordStateDuration(stateDurationMs, oldState);
577
+ this.stateEnteredAtMs = transitionAtMs;
578
+ this.stateSlotNumber = slotNumber;
579
+ }
540
580
  this.state = proposedState;
541
581
  }
542
582
 
@@ -567,12 +607,12 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
567
607
  .getL2Tips()
568
608
  .then(t => ({ proposed: t.proposed, checkpointed: t.checkpointed, proposedCheckpoint: t.proposedCheckpoint })),
569
609
  this.p2pClient.getStatus().then(p2p => p2p.syncedToL2Block),
570
- this.l1ToL2MessageSource.getL2Tips().then(t => t.proposed),
610
+ this.l1ToL2MessageSource.getL2Tips().then(t => ({ proposed: t.proposed, checkpointed: t.checkpointed })),
571
611
  this.l2BlockSource.getPendingChainValidationStatus(),
572
- this.l2BlockSource.getProposedCheckpointOnly(),
612
+ this.l2BlockSource.getLastProposedCheckpoint(),
573
613
  ] as const);
574
614
 
575
- const [worldState, l2Tips, p2p, l1ToL2MessageSource, pendingChainValidationStatus, proposedCheckpointData] =
615
+ const [worldState, l2Tips, p2p, l1ToL2MessageSourceTips, pendingChainValidationStatus, proposedCheckpointData] =
576
616
  syncedBlocks;
577
617
 
578
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,
@@ -580,19 +620,25 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
580
620
  // TODO(palla/mbps): Fix the above. All components should be able to handle dynamic genesis block hashes.
581
621
  const result =
582
622
  (l2Tips.proposed.number === 0 &&
623
+ l2Tips.checkpointed.block.number === 0 &&
624
+ l2Tips.checkpointed.checkpoint.number === 0 &&
583
625
  worldState.number === 0 &&
584
626
  p2p.number === 0 &&
585
- l1ToL2MessageSource.number === 0) ||
627
+ l1ToL2MessageSourceTips.proposed.number === 0 &&
628
+ l1ToL2MessageSourceTips.checkpointed.block.number === 0 &&
629
+ l1ToL2MessageSourceTips.checkpointed.checkpoint.number === 0) ||
586
630
  (worldState.hash === l2Tips.proposed.hash &&
587
631
  p2p.hash === l2Tips.proposed.hash &&
588
- l1ToL2MessageSource.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);
589
635
 
590
636
  if (!result) {
591
637
  this.log.debug(`Sequencer sync check failed`, {
592
638
  worldState,
593
639
  l2BlockSource: l2Tips.proposed,
594
640
  p2p,
595
- l1ToL2MessageSource,
641
+ l1ToL2MessageSourceTips,
596
642
  });
597
643
  return undefined;
598
644
  }