@aztec/sequencer-client 0.0.1-commit.35158ae7e → 0.0.1-commit.3750d92a7

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 (70) 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 +3 -8
  14. package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
  15. package/dest/global_variable_builder/global_builder.js +4 -39
  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 +47 -44
  28. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  29. package/dest/publisher/sequencer-publisher.js +112 -115
  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 +21 -10
  34. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  35. package/dest/sequencer/checkpoint_proposal_job.js +200 -134
  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/checkpoint_voter.d.ts +1 -2
  40. package/dest/sequencer/checkpoint_voter.d.ts.map +1 -1
  41. package/dest/sequencer/checkpoint_voter.js +2 -5
  42. package/dest/sequencer/metrics.d.ts +5 -10
  43. package/dest/sequencer/metrics.d.ts.map +1 -1
  44. package/dest/sequencer/metrics.js +12 -20
  45. package/dest/sequencer/sequencer.d.ts +17 -4
  46. package/dest/sequencer/sequencer.d.ts.map +1 -1
  47. package/dest/sequencer/sequencer.js +73 -20
  48. package/dest/sequencer/timetable.d.ts +14 -1
  49. package/dest/sequencer/timetable.d.ts.map +1 -1
  50. package/dest/sequencer/timetable.js +45 -36
  51. package/package.json +27 -27
  52. package/src/client/sequencer-client.ts +5 -7
  53. package/src/config.ts +8 -11
  54. package/src/global_variable_builder/README.md +44 -0
  55. package/src/global_variable_builder/fee_predictor.ts +172 -0
  56. package/src/global_variable_builder/fee_provider.ts +75 -0
  57. package/src/global_variable_builder/global_builder.ts +8 -43
  58. package/src/global_variable_builder/index.ts +2 -0
  59. package/src/publisher/config.ts +6 -4
  60. package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +3 -1
  61. package/src/publisher/sequencer-publisher-factory.ts +0 -3
  62. package/src/publisher/sequencer-publisher.ts +174 -163
  63. package/src/sequencer/README.md +83 -13
  64. package/src/sequencer/chain_state_overrides.ts +87 -0
  65. package/src/sequencer/checkpoint_proposal_job.ts +242 -154
  66. package/src/sequencer/checkpoint_proposal_job_metrics.ts +128 -0
  67. package/src/sequencer/checkpoint_voter.ts +1 -12
  68. package/src/sequencer/metrics.ts +15 -25
  69. package/src/sequencer/sequencer.ts +110 -22
  70. 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
+ }
@@ -2,7 +2,6 @@ import type { SlotNumber } from '@aztec/foundation/branded-types';
2
2
  import type { EthAddress } from '@aztec/foundation/eth-address';
3
3
  import type { Logger } from '@aztec/foundation/log';
4
4
  import type { SlasherClientInterface } from '@aztec/slasher';
5
- import { getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
6
5
  import type { ResolvedSequencerConfig } from '@aztec/stdlib/interfaces/server';
7
6
  import type { ValidatorClient } from '@aztec/validator-client';
8
7
  import { DutyAlreadySignedError } from '@aztec/validator-ha-signer/errors';
@@ -18,7 +17,6 @@ import type { SequencerRollupConstants } from './types.js';
18
17
  * Handles governance and slashing voting for a given slot.
19
18
  */
20
19
  export class CheckpointVoter {
21
- private slotTimestamp: bigint;
22
20
  private governanceSigner: (msg: TypedDataDefinition) => Promise<`0x${string}`>;
23
21
  private slashingSigner: (msg: TypedDataDefinition) => Promise<`0x${string}`>;
24
22
 
@@ -33,8 +31,6 @@ export class CheckpointVoter {
33
31
  private readonly metrics: SequencerMetrics,
34
32
  private readonly log: Logger,
35
33
  ) {
36
- this.slotTimestamp = getTimestampForSlot(this.slot, this.l1Constants);
37
-
38
34
  // Create separate signers with appropriate duty contexts for governance and slashing votes
39
35
  // These use HA protection to ensure only one node signs per slot/duty
40
36
  const governanceContext: SigningContext = { slot: this.slot, dutyType: DutyType.GOVERNANCE_VOTE };
@@ -77,7 +73,6 @@ export class CheckpointVoter {
77
73
  return await this.publisher.enqueueGovernanceCastSignal(
78
74
  governanceProposerPayload,
79
75
  this.slot,
80
- this.slotTimestamp,
81
76
  this.attestorAddress,
82
77
  this.governanceSigner,
83
78
  );
@@ -108,13 +103,7 @@ export class CheckpointVoter {
108
103
 
109
104
  this.metrics.recordSlashingAttempt(actions.length);
110
105
 
111
- return await this.publisher.enqueueSlashingActions(
112
- actions,
113
- this.slot,
114
- this.slotTimestamp,
115
- this.attestorAddress,
116
- this.slashingSigner,
117
- );
106
+ return await this.publisher.enqueueSlashingActions(actions, this.slot, this.attestorAddress, this.slashingSigner);
118
107
  } catch (err) {
119
108
  if (err instanceof DutyAlreadySignedError) {
120
109
  this.log.info(`Slashing vote already signed by another node`, {
@@ -44,11 +44,6 @@ 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;
54
49
 
@@ -68,6 +63,10 @@ export class SequencerMetrics {
68
63
  private fishermanMinedBlobTxPriorityFee: Histogram;
69
64
  private fishermanMinedBlobTxTotalCost: Histogram;
70
65
 
66
+ private blockInterBlockTime: Histogram;
67
+ private lastBlockBuiltTimestamp?: number;
68
+ private lastBlockBuiltSlot?: SlotNumber;
69
+
71
70
  private lastSeenSlot?: SlotNumber;
72
71
 
73
72
  constructor(
@@ -86,9 +85,9 @@ export class SequencerMetrics {
86
85
 
87
86
  this.blockBuildManaPerSecond = this.meter.createGauge(Metrics.SEQUENCER_BLOCK_BUILD_MANA_PER_SECOND);
88
87
 
89
- this.stateTransitionBufferDuration = this.meter.createHistogram(Metrics.SEQUENCER_STATE_TRANSITION_BUFFER_DURATION);
88
+ this.blockInterBlockTime = this.meter.createHistogram(Metrics.SEQUENCER_BLOCK_INTER_BLOCK_TIME);
90
89
 
91
- this.checkpointAttestationDelay = this.meter.createHistogram(Metrics.SEQUENCER_CHECKPOINT_ATTESTATION_DELAY);
90
+ this.stateTransitionBufferDuration = this.meter.createHistogram(Metrics.SEQUENCER_STATE_TRANSITION_BUFFER_DURATION);
92
91
 
93
92
  this.rewards = this.meter.createGauge(Metrics.SEQUENCER_CURRENT_SLOT_REWARDS);
94
93
 
@@ -138,11 +137,6 @@ export class SequencerMetrics {
138
137
  Metrics.SEQUENCER_CHECKPOINT_PROPOSAL_FAILED_COUNT,
139
138
  );
140
139
 
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
140
  this.slashingAttempts = createUpDownCounterWithDefault(this.meter, Metrics.SEQUENCER_SLASHING_ATTEMPTS_COUNT);
147
141
 
148
142
  this.pipelineDepth = this.meter.createGauge(Metrics.SEQUENCER_PIPELINE_DEPTH);
@@ -211,21 +205,25 @@ export class SequencerMetrics {
211
205
  this.timeToCollectAttestations.record(0);
212
206
  }
213
207
 
214
- public recordCheckpointAttestationDelay(duration: number) {
215
- this.checkpointAttestationDelay.record(duration);
216
- }
217
-
218
208
  public recordCollectedAttestations(count: number, durationMs: number) {
219
209
  this.collectedAttestions.record(count);
220
210
  this.timeToCollectAttestations.record(Math.ceil(durationMs));
221
211
  }
222
212
 
223
- recordBuiltBlock(buildDurationMs: number, totalMana: number) {
213
+ recordBuiltBlock(buildDurationMs: number, totalMana: number, slot: SlotNumber) {
224
214
  this.blockCounter.add(1, {
225
215
  [Attributes.STATUS]: 'built',
226
216
  });
227
217
  this.blockBuildDuration.record(Math.ceil(buildDurationMs));
228
218
  this.blockBuildManaPerSecond.record(Math.ceil((totalMana * 1000) / buildDurationMs));
219
+
220
+ // Only record inter-block time between blocks built within the same slot.
221
+ const now = Date.now();
222
+ if (this.lastBlockBuiltTimestamp !== undefined && this.lastBlockBuiltSlot === slot) {
223
+ this.blockInterBlockTime.record(now - this.lastBlockBuiltTimestamp);
224
+ }
225
+ this.lastBlockBuiltTimestamp = now;
226
+ this.lastBlockBuiltSlot = slot;
229
227
  }
230
228
 
231
229
  recordFailedBlock() {
@@ -306,14 +304,6 @@ export class SequencerMetrics {
306
304
  });
307
305
  }
308
306
 
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
307
  recordSlashingAttempt(actionCount: number) {
318
308
  this.slashingAttempts.add(actionCount);
319
309
  }
@@ -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';
@@ -13,7 +13,7 @@ import type { TypedEventEmitter } from '@aztec/foundation/types';
13
13
  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
- import type { Checkpoint } from '@aztec/stdlib/checkpoint';
16
+ import type { Checkpoint, ProposedCheckpointData } from '@aztec/stdlib/checkpoint';
17
17
  import { getSlotStartBuildTimestamp } from '@aztec/stdlib/epoch-helpers';
18
18
  import {
19
19
  type ResolvedSequencerConfig,
@@ -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;
@@ -72,6 +74,9 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
72
74
  /** The last epoch for which we logged strategy comparison in fisherman mode. */
73
75
  private lastEpochForStrategyComparison: EpochNumber | undefined;
74
76
 
77
+ /** The last checkpoint proposal job, tracked so we can await its pending L1 submission during shutdown. */
78
+ protected lastCheckpointProposalJob: CheckpointProposalJob | undefined;
79
+
75
80
  /** The maximum number of seconds that the sequencer can be into a slot to transition to a particular state. */
76
81
  protected timetable!: SequencerTimetable;
77
82
 
@@ -104,6 +109,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
104
109
  }
105
110
 
106
111
  this.metrics = new SequencerMetrics(telemetry, this.rollupContract, 'Sequencer');
112
+ this.checkpointProposalJobMetrics = new CheckpointProposalJobMetrics(telemetry);
107
113
  this.updateConfig(config);
108
114
  }
109
115
 
@@ -112,14 +118,16 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
112
118
  const filteredConfig = pickFromSchema(config, SequencerConfigSchema);
113
119
  this.log.info(`Updated sequencer config`, omit(filteredConfig, 'txPublicSetupAllowListExtend'));
114
120
  this.config = merge(this.config, filteredConfig);
121
+ const p2pPropagationTime = this.config.attestationPropagationTime;
115
122
  this.timetable = new SequencerTimetable(
116
123
  {
117
124
  ethereumSlotDuration: this.l1Constants.ethereumSlotDuration,
118
125
  aztecSlotDuration: this.aztecSlotDuration,
119
126
  l1PublishingTime: this.l1PublishingTime,
120
- p2pPropagationTime: this.config.attestationPropagationTime,
127
+ p2pPropagationTime,
121
128
  blockDurationMs: this.config.blockDurationMs,
122
129
  enforce: this.config.enforceTimeTable,
130
+ pipelining: this.epochCache.isProposerPipeliningEnabled(),
123
131
  },
124
132
  this.metrics,
125
133
  this.log,
@@ -143,12 +151,18 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
143
151
  this.log.info('Started sequencer');
144
152
  }
145
153
 
154
+ /** Triggers an immediate run of the sequencer, bypassing the polling interval. */
155
+ public trigger() {
156
+ return this.runningPromise?.trigger();
157
+ }
158
+
146
159
  /** Stops the sequencer from building blocks and moves to STOPPED state. */
147
160
  public async stop(): Promise<void> {
148
161
  this.log.info(`Stopping sequencer`);
149
162
  this.setState(SequencerState.STOPPING, undefined, { force: true });
150
163
  await this.publisherFactory.stopAll();
151
164
  await this.runningPromise?.stop();
165
+ await this.lastCheckpointProposalJob?.awaitPendingSubmission();
152
166
  this.setState(SequencerState.STOPPED, undefined, { force: true });
153
167
  this.log.info('Stopped sequencer');
154
168
  }
@@ -208,6 +222,9 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
208
222
  return;
209
223
  }
210
224
 
225
+ // Track the job so we can await its pending L1 submission during shutdown
226
+ this.lastCheckpointProposalJob = checkpointProposalJob;
227
+
211
228
  // Execute the checkpoint proposal job
212
229
  const checkpoint = await checkpointProposalJob.execute();
213
230
 
@@ -234,7 +251,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
234
251
  * @returns CheckpointProposalJob if successful, undefined if we are not yet synced or are not the proposer.
235
252
  */
236
253
  @trackSpan('Sequencer.prepareCheckpointProposal')
237
- private async prepareCheckpointProposal(
254
+ protected async prepareCheckpointProposal(
238
255
  slot: SlotNumber,
239
256
  targetSlot: SlotNumber,
240
257
  epoch: EpochNumber,
@@ -312,6 +329,16 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
312
329
  return undefined;
313
330
  }
314
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
+
315
342
  // Check that the target slot is not taken by a block already (should never happen, since only us can propose for this slot)
316
343
  if (syncedTo.blockData && syncedTo.blockData.header.getSlot() >= targetSlot) {
317
344
  this.log.warn(
@@ -337,13 +364,42 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
337
364
  }
338
365
 
339
366
  // Prepare invalidation request if the pending chain is invalid (returns undefined if no need)
340
- 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
+ }
341
396
 
342
- // Check with the rollup contract if we can indeed propose at the target slot. This check should not fail
343
- // if all the previous checks are good, but we do it just in case.
344
- const canProposeCheck = await publisher.canProposeAt(syncedTo.archive, proposer ?? EthAddress.ZERO, {
345
- ...invalidateCheckpoint,
346
- });
397
+ const simulationOverridesPlan = l1SimulationOverridesBuilder.build();
398
+ const canProposeCheck = await publisher.canProposeAt(
399
+ archiveForCheck,
400
+ proposer ?? EthAddress.ZERO,
401
+ simulationOverridesPlan,
402
+ );
347
403
 
348
404
  if (canProposeCheck === undefined) {
349
405
  this.log.warn(
@@ -391,7 +447,6 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
391
447
  return this.createCheckpointProposalJob(
392
448
  slot,
393
449
  targetSlot,
394
- epoch,
395
450
  targetEpoch,
396
451
  checkpointNumber,
397
452
  syncedTo.blockNumber,
@@ -399,13 +454,13 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
399
454
  publisher,
400
455
  attestorAddress,
401
456
  invalidateCheckpoint,
457
+ syncedTo.proposedCheckpointData,
402
458
  );
403
459
  }
404
460
 
405
461
  protected createCheckpointProposalJob(
406
462
  slot: SlotNumber,
407
463
  targetSlot: SlotNumber,
408
- epoch: EpochNumber,
409
464
  targetEpoch: EpochNumber,
410
465
  checkpointNumber: CheckpointNumber,
411
466
  syncedToBlockNumber: BlockNumber,
@@ -413,11 +468,11 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
413
468
  publisher: SequencerPublisher,
414
469
  attestorAddress: EthAddress,
415
470
  invalidateCheckpoint: InvalidateCheckpointRequest | undefined,
471
+ proposedCheckpointData?: ProposedCheckpointData,
416
472
  ): CheckpointProposalJob {
417
473
  return new CheckpointProposalJob(
418
474
  slot,
419
475
  targetSlot,
420
- epoch,
421
476
  targetEpoch,
422
477
  checkpointNumber,
423
478
  syncedToBlockNumber,
@@ -440,10 +495,12 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
440
495
  this.epochCache,
441
496
  this.dateProvider,
442
497
  this.metrics,
498
+ this.checkpointProposalJobMetrics.createRecorder(),
443
499
  this,
444
500
  this.setState.bind(this),
445
501
  this.tracer,
446
502
  this.log.getBindings(),
503
+ proposedCheckpointData,
447
504
  );
448
505
  }
449
506
 
@@ -518,25 +575,43 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
518
575
  number: syncSummary.latestBlockNumber,
519
576
  hash: syncSummary.latestBlockHash,
520
577
  })),
521
- this.l2BlockSource.getL2Tips().then(t => t.proposed),
578
+ this.l2BlockSource
579
+ .getL2Tips()
580
+ .then(t => ({ proposed: t.proposed, checkpointed: t.checkpointed, proposedCheckpoint: t.proposedCheckpoint })),
522
581
  this.p2pClient.getStatus().then(p2p => p2p.syncedToL2Block),
523
- this.l1ToL2MessageSource.getL2Tips().then(t => t.proposed),
582
+ this.l1ToL2MessageSource.getL2Tips().then(t => ({ proposed: t.proposed, checkpointed: t.checkpointed })),
524
583
  this.l2BlockSource.getPendingChainValidationStatus(),
584
+ this.l2BlockSource.getProposedCheckpointOnly(),
525
585
  ] as const);
526
586
 
527
- const [worldState, l2BlockSource, p2p, l1ToL2MessageSource, pendingChainValidationStatus] = syncedBlocks;
587
+ const [worldState, l2Tips, p2p, l1ToL2MessageSourceTips, pendingChainValidationStatus, proposedCheckpointData] =
588
+ syncedBlocks;
528
589
 
529
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,
530
591
  // as the world state can compute the new genesis block hash, but other components use the hardcoded constant.
531
592
  // TODO(palla/mbps): Fix the above. All components should be able to handle dynamic genesis block hashes.
532
593
  const result =
533
- (l2BlockSource.number === 0 && worldState.number === 0 && p2p.number === 0 && l1ToL2MessageSource.number === 0) ||
534
- (worldState.hash === l2BlockSource.hash &&
535
- p2p.hash === l2BlockSource.hash &&
536
- 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);
537
607
 
538
608
  if (!result) {
539
- 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
+ });
540
615
  return undefined;
541
616
  }
542
617
 
@@ -546,8 +621,10 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
546
621
  const archive = new Fr((await this.worldState.getCommitted().getTreeInfo(MerkleTreeId.ARCHIVE)).root);
547
622
  return {
548
623
  checkpointNumber: CheckpointNumber.ZERO,
624
+ checkpointedCheckpointNumber: CheckpointNumber.ZERO,
549
625
  blockNumber: BlockNumber.ZERO,
550
626
  archive,
627
+ hasProposedCheckpoint: false,
551
628
  syncedL2Slot,
552
629
  pendingChainValidationStatus,
553
630
  };
@@ -560,11 +637,16 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
560
637
  return undefined;
561
638
  }
562
639
 
640
+ const hasProposedCheckpoint = l2Tips.proposedCheckpoint.checkpoint.number > l2Tips.checkpointed.checkpoint.number;
641
+
563
642
  return {
564
643
  blockData,
565
644
  blockNumber: blockData.header.getBlockNumber(),
566
645
  checkpointNumber: blockData.checkpointNumber,
646
+ checkpointedCheckpointNumber: l2Tips.checkpointed.checkpoint.number,
567
647
  archive: blockData.archive.root,
648
+ hasProposedCheckpoint,
649
+ proposedCheckpointData,
568
650
  syncedL2Slot,
569
651
  pendingChainValidationStatus,
570
652
  };
@@ -612,7 +694,10 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
612
694
  return [false, proposer];
613
695
  }
614
696
 
615
- this.log.debug(`We are the proposer for target slot ${targetSlot}`, { targetSlot, proposer });
697
+ this.log.info(`We are the proposer for pipeline slot ${targetSlot}`, {
698
+ targetSlot,
699
+ proposer,
700
+ });
616
701
  return [true, proposer];
617
702
  }
618
703
 
@@ -909,8 +994,11 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
909
994
  type SequencerSyncCheckResult = {
910
995
  blockData?: BlockData;
911
996
  checkpointNumber: CheckpointNumber;
997
+ checkpointedCheckpointNumber: CheckpointNumber;
912
998
  blockNumber: BlockNumber;
913
999
  archive: Fr;
1000
+ hasProposedCheckpoint: boolean;
1001
+ proposedCheckpointData?: ProposedCheckpointData;
914
1002
  syncedL2Slot: SlotNumber;
915
1003
  pendingChainValidationStatus: ValidateCheckpointResult;
916
1004
  };