@aztec/sequencer-client 0.0.1-commit.fffb133c → 0.0.1-dev

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 (66) hide show
  1. package/dest/client/sequencer-client.d.ts +16 -7
  2. package/dest/client/sequencer-client.d.ts.map +1 -1
  3. package/dest/client/sequencer-client.js +56 -23
  4. package/dest/config.d.ts +25 -6
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +46 -28
  7. package/dest/global_variable_builder/global_builder.d.ts +14 -10
  8. package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
  9. package/dest/global_variable_builder/global_builder.js +22 -21
  10. package/dest/global_variable_builder/index.d.ts +2 -2
  11. package/dest/global_variable_builder/index.d.ts.map +1 -1
  12. package/dest/publisher/config.d.ts +31 -17
  13. package/dest/publisher/config.d.ts.map +1 -1
  14. package/dest/publisher/config.js +101 -42
  15. package/dest/publisher/sequencer-publisher-factory.d.ts +11 -3
  16. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  17. package/dest/publisher/sequencer-publisher-factory.js +13 -2
  18. package/dest/publisher/sequencer-publisher-metrics.d.ts +1 -1
  19. package/dest/publisher/sequencer-publisher-metrics.d.ts.map +1 -1
  20. package/dest/publisher/sequencer-publisher-metrics.js +12 -4
  21. package/dest/publisher/sequencer-publisher.d.ts +28 -14
  22. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  23. package/dest/publisher/sequencer-publisher.js +137 -74
  24. package/dest/sequencer/checkpoint_proposal_job.d.ts +35 -10
  25. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  26. package/dest/sequencer/checkpoint_proposal_job.js +212 -137
  27. package/dest/sequencer/checkpoint_voter.d.ts +1 -2
  28. package/dest/sequencer/checkpoint_voter.d.ts.map +1 -1
  29. package/dest/sequencer/checkpoint_voter.js +2 -5
  30. package/dest/sequencer/metrics.d.ts +17 -5
  31. package/dest/sequencer/metrics.d.ts.map +1 -1
  32. package/dest/sequencer/metrics.js +111 -30
  33. package/dest/sequencer/sequencer.d.ts +29 -13
  34. package/dest/sequencer/sequencer.d.ts.map +1 -1
  35. package/dest/sequencer/sequencer.js +48 -44
  36. package/dest/sequencer/timetable.d.ts +4 -6
  37. package/dest/sequencer/timetable.d.ts.map +1 -1
  38. package/dest/sequencer/timetable.js +7 -11
  39. package/dest/sequencer/types.d.ts +2 -2
  40. package/dest/sequencer/types.d.ts.map +1 -1
  41. package/dest/test/index.d.ts +3 -5
  42. package/dest/test/index.d.ts.map +1 -1
  43. package/dest/test/mock_checkpoint_builder.d.ts +12 -12
  44. package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
  45. package/dest/test/mock_checkpoint_builder.js +47 -36
  46. package/dest/test/utils.d.ts +3 -3
  47. package/dest/test/utils.d.ts.map +1 -1
  48. package/dest/test/utils.js +5 -4
  49. package/package.json +28 -28
  50. package/src/client/sequencer-client.ts +78 -21
  51. package/src/config.ts +61 -38
  52. package/src/global_variable_builder/global_builder.ts +23 -24
  53. package/src/global_variable_builder/index.ts +1 -1
  54. package/src/publisher/config.ts +112 -43
  55. package/src/publisher/sequencer-publisher-factory.ts +23 -6
  56. package/src/publisher/sequencer-publisher-metrics.ts +7 -3
  57. package/src/publisher/sequencer-publisher.ts +149 -90
  58. package/src/sequencer/checkpoint_proposal_job.ts +309 -186
  59. package/src/sequencer/checkpoint_voter.ts +1 -12
  60. package/src/sequencer/metrics.ts +124 -32
  61. package/src/sequencer/sequencer.ts +61 -50
  62. package/src/sequencer/timetable.ts +13 -12
  63. package/src/sequencer/types.ts +1 -1
  64. package/src/test/index.ts +2 -4
  65. package/src/test/mock_checkpoint_builder.ts +67 -53
  66. package/src/test/utils.ts +5 -2
@@ -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`, {
@@ -11,13 +11,13 @@ import {
11
11
  type TelemetryClient,
12
12
  type Tracer,
13
13
  type UpDownCounter,
14
+ createUpDownCounterWithDefault,
14
15
  } from '@aztec/telemetry-client';
15
16
 
16
17
  import { type Hex, formatUnits } from 'viem';
17
18
 
18
19
  import type { SequencerState } from './utils.js';
19
20
 
20
- // TODO(palla/mbps): Review all metrics and add any missing ones per checkpoint
21
21
  export class SequencerMetrics {
22
22
  public readonly tracer: Tracer;
23
23
  private meter: Meter;
@@ -39,17 +39,26 @@ export class SequencerMetrics {
39
39
  private filledSlots: UpDownCounter;
40
40
 
41
41
  private blockProposalFailed: UpDownCounter;
42
- private blockProposalSuccess: UpDownCounter;
43
- private blockProposalPrecheckFailed: UpDownCounter;
42
+ private checkpointProposalSuccess: UpDownCounter;
43
+ private checkpointPrecheckFailed: UpDownCounter;
44
+ private checkpointProposalFailed: UpDownCounter;
44
45
  private checkpointSuccess: UpDownCounter;
45
46
  private slashingAttempts: UpDownCounter;
46
47
  private checkpointAttestationDelay: Histogram;
48
+ private checkpointBuildDuration: Histogram;
49
+ private checkpointBlockCount: Gauge;
50
+ private checkpointTxCount: Gauge;
51
+ private checkpointTotalMana: Gauge;
47
52
 
48
53
  // Fisherman fee analysis metrics
49
54
  private fishermanWouldBeIncluded: UpDownCounter;
50
55
  private fishermanTimeBeforeBlock: Histogram;
51
56
  private fishermanPendingBlobTxCount: Histogram;
52
57
  private fishermanIncludedBlobTxCount: Histogram;
58
+ private fishermanPendingBlobCount: Histogram;
59
+ private fishermanIncludedBlobCount: Histogram;
60
+ private fishermanBlockBlobsFull: UpDownCounter;
61
+ private fishermanMaxBlobCapacity: Histogram;
53
62
  private fishermanCalculatedPriorityFee: Histogram;
54
63
  private fishermanPriorityFeeDelta: Histogram;
55
64
  private fishermanEstimatedCost: Histogram;
@@ -67,7 +76,9 @@ export class SequencerMetrics {
67
76
  this.meter = client.getMeter(name);
68
77
  this.tracer = client.getTracer(name);
69
78
 
70
- this.blockCounter = this.meter.createUpDownCounter(Metrics.SEQUENCER_BLOCK_COUNT);
79
+ this.blockCounter = createUpDownCounterWithDefault(this.meter, Metrics.SEQUENCER_BLOCK_COUNT, {
80
+ [Attributes.STATUS]: ['failed', 'built'],
81
+ });
71
82
 
72
83
  this.blockBuildDuration = this.meter.createHistogram(Metrics.SEQUENCER_BLOCK_BUILD_DURATION);
73
84
 
@@ -77,23 +88,15 @@ export class SequencerMetrics {
77
88
 
78
89
  this.checkpointAttestationDelay = this.meter.createHistogram(Metrics.SEQUENCER_CHECKPOINT_ATTESTATION_DELAY);
79
90
 
80
- // Init gauges and counters
81
- this.blockCounter.add(0, {
82
- [Attributes.STATUS]: 'failed',
83
- });
84
- this.blockCounter.add(0, {
85
- [Attributes.STATUS]: 'built',
86
- });
87
-
88
- this.rewards = this.meter.createGauge(Metrics.SEQUENCER_CURRENT_BLOCK_REWARDS);
91
+ this.rewards = this.meter.createGauge(Metrics.SEQUENCER_CURRENT_SLOT_REWARDS);
89
92
 
90
- this.slots = this.meter.createUpDownCounter(Metrics.SEQUENCER_SLOT_COUNT);
93
+ this.slots = createUpDownCounterWithDefault(this.meter, Metrics.SEQUENCER_SLOT_COUNT);
91
94
 
92
95
  /**
93
96
  * NOTE: we do not track missed slots as a separate metric. That would be difficult to determine
94
97
  * Instead, use a computed metric, `slots - filledSlots` to get the number of slots a sequencer has missed.
95
98
  */
96
- this.filledSlots = this.meter.createUpDownCounter(Metrics.SEQUENCER_FILLED_SLOT_COUNT);
99
+ this.filledSlots = createUpDownCounterWithDefault(this.meter, Metrics.SEQUENCER_FILLED_SLOT_COUNT);
97
100
 
98
101
  this.timeToCollectAttestations = this.meter.createGauge(Metrics.SEQUENCER_COLLECT_ATTESTATIONS_DURATION);
99
102
 
@@ -103,20 +106,52 @@ export class SequencerMetrics {
103
106
 
104
107
  this.collectedAttestions = this.meter.createGauge(Metrics.SEQUENCER_COLLECTED_ATTESTATIONS_COUNT);
105
108
 
106
- this.blockProposalFailed = this.meter.createUpDownCounter(Metrics.SEQUENCER_BLOCK_PROPOSAL_FAILED_COUNT);
109
+ this.blockProposalFailed = createUpDownCounterWithDefault(
110
+ this.meter,
111
+ Metrics.SEQUENCER_BLOCK_PROPOSAL_FAILED_COUNT,
112
+ );
107
113
 
108
- this.blockProposalSuccess = this.meter.createUpDownCounter(Metrics.SEQUENCER_BLOCK_PROPOSAL_SUCCESS_COUNT);
114
+ this.checkpointProposalSuccess = createUpDownCounterWithDefault(
115
+ this.meter,
116
+ Metrics.SEQUENCER_CHECKPOINT_PROPOSAL_SUCCESS_COUNT,
117
+ );
109
118
 
110
- this.checkpointSuccess = this.meter.createUpDownCounter(Metrics.SEQUENCER_CHECKPOINT_SUCCESS_COUNT);
119
+ this.checkpointSuccess = createUpDownCounterWithDefault(this.meter, Metrics.SEQUENCER_CHECKPOINT_SUCCESS_COUNT);
120
+
121
+ this.checkpointPrecheckFailed = createUpDownCounterWithDefault(
122
+ this.meter,
123
+ Metrics.SEQUENCER_CHECKPOINT_PRECHECK_FAILED_COUNT,
124
+ {
125
+ [Attributes.ERROR_TYPE]: [
126
+ 'slot_already_taken',
127
+ 'rollup_contract_check_failed',
128
+ 'slot_mismatch',
129
+ 'block_number_mismatch',
130
+ ],
131
+ },
132
+ );
111
133
 
112
- this.blockProposalPrecheckFailed = this.meter.createUpDownCounter(
113
- Metrics.SEQUENCER_BLOCK_PROPOSAL_PRECHECK_FAILED_COUNT,
134
+ this.checkpointProposalFailed = createUpDownCounterWithDefault(
135
+ this.meter,
136
+ Metrics.SEQUENCER_CHECKPOINT_PROPOSAL_FAILED_COUNT,
114
137
  );
115
138
 
116
- this.slashingAttempts = this.meter.createUpDownCounter(Metrics.SEQUENCER_SLASHING_ATTEMPTS_COUNT);
139
+ this.checkpointBuildDuration = this.meter.createHistogram(Metrics.SEQUENCER_CHECKPOINT_BUILD_DURATION);
140
+ this.checkpointBlockCount = this.meter.createGauge(Metrics.SEQUENCER_CHECKPOINT_BLOCK_COUNT);
141
+ this.checkpointTxCount = this.meter.createGauge(Metrics.SEQUENCER_CHECKPOINT_TX_COUNT);
142
+ this.checkpointTotalMana = this.meter.createGauge(Metrics.SEQUENCER_CHECKPOINT_TOTAL_MANA);
143
+
144
+ this.slashingAttempts = createUpDownCounterWithDefault(this.meter, Metrics.SEQUENCER_SLASHING_ATTEMPTS_COUNT);
117
145
 
118
146
  // Fisherman fee analysis metrics
119
- this.fishermanWouldBeIncluded = this.meter.createUpDownCounter(Metrics.FISHERMAN_FEE_ANALYSIS_WOULD_BE_INCLUDED);
147
+ this.fishermanWouldBeIncluded = createUpDownCounterWithDefault(
148
+ this.meter,
149
+ Metrics.FISHERMAN_FEE_ANALYSIS_WOULD_BE_INCLUDED,
150
+ {
151
+ [Attributes.OK]: [true, false],
152
+ [Attributes.BLOCK_FULL]: ['true', 'false'],
153
+ },
154
+ );
120
155
 
121
156
  this.fishermanTimeBeforeBlock = this.meter.createHistogram(Metrics.FISHERMAN_FEE_ANALYSIS_TIME_BEFORE_BLOCK);
122
157
 
@@ -145,6 +180,20 @@ export class SequencerMetrics {
145
180
  this.fishermanMinedBlobTxTotalCost = this.meter.createHistogram(
146
181
  Metrics.FISHERMAN_FEE_ANALYSIS_MINED_BLOB_TX_TOTAL_COST,
147
182
  );
183
+
184
+ this.fishermanPendingBlobCount = this.meter.createHistogram(Metrics.FISHERMAN_FEE_ANALYSIS_PENDING_BLOB_COUNT);
185
+
186
+ this.fishermanIncludedBlobCount = this.meter.createHistogram(Metrics.FISHERMAN_FEE_ANALYSIS_INCLUDED_BLOB_COUNT);
187
+
188
+ this.fishermanBlockBlobsFull = createUpDownCounterWithDefault(
189
+ this.meter,
190
+ Metrics.FISHERMAN_FEE_ANALYSIS_BLOCK_BLOBS_FULL,
191
+ {
192
+ [Attributes.OK]: [true, false],
193
+ },
194
+ );
195
+
196
+ this.fishermanMaxBlobCapacity = this.meter.createHistogram(Metrics.FISHERMAN_FEE_ANALYSIS_MAX_BLOB_CAPACITY);
148
197
  }
149
198
 
150
199
  public recordRequiredAttestations(requiredAttestationsCount: number, allowanceMs: number) {
@@ -227,16 +276,30 @@ export class SequencerMetrics {
227
276
  });
228
277
  }
229
278
 
230
- recordBlockProposalSuccess() {
231
- this.blockProposalSuccess.add(1);
279
+ recordCheckpointProposalSuccess() {
280
+ this.checkpointProposalSuccess.add(1);
232
281
  }
233
282
 
234
- recordBlockProposalPrecheckFailed(checkType: string) {
235
- this.blockProposalPrecheckFailed.add(1, {
236
- [Attributes.ERROR_TYPE]: checkType,
283
+ recordCheckpointPrecheckFailed(
284
+ checkType: 'slot_already_taken' | 'rollup_contract_check_failed' | 'slot_mismatch' | 'block_number_mismatch',
285
+ ) {
286
+ this.checkpointPrecheckFailed.add(1, { [Attributes.ERROR_TYPE]: checkType });
287
+ }
288
+
289
+ recordCheckpointProposalFailed(reason?: string) {
290
+ this.checkpointProposalFailed.add(1, {
291
+ ...(reason && { [Attributes.ERROR_TYPE]: reason }),
237
292
  });
238
293
  }
239
294
 
295
+ /** Records aggregate metrics for a completed checkpoint build. */
296
+ recordCheckpointBuild(durationMs: number, blockCount: number, txCount: number, totalMana: number) {
297
+ this.checkpointBuildDuration.record(Math.ceil(durationMs));
298
+ this.checkpointBlockCount.record(blockCount);
299
+ this.checkpointTxCount.record(txCount);
300
+ this.checkpointTotalMana.record(totalMana);
301
+ }
302
+
240
303
  recordSlashingAttempt(actionCount: number) {
241
304
  this.slashingAttempts.add(actionCount);
242
305
  }
@@ -263,10 +326,12 @@ export class SequencerMetrics {
263
326
 
264
327
  // Record pending block snapshot data (once per strategy for comparison)
265
328
  this.fishermanPendingBlobTxCount.record(analysis.pendingSnapshot.pendingBlobTxCount, strategyAttributes);
329
+ this.fishermanPendingBlobCount.record(analysis.pendingSnapshot.pendingBlobCount, strategyAttributes);
266
330
 
267
331
  // Record mined block data if available
268
332
  if (analysis.minedBlock) {
269
333
  this.fishermanIncludedBlobTxCount.record(analysis.minedBlock.includedBlobTxCount, strategyAttributes);
334
+ this.fishermanIncludedBlobCount.record(analysis.minedBlock.includedBlobCount, strategyAttributes);
270
335
 
271
336
  // Record actual fees from blob transactions in the mined block
272
337
  for (const blobTx of analysis.minedBlock.includedBlobTxs) {
@@ -300,13 +365,28 @@ export class SequencerMetrics {
300
365
  if (analysis.analysis) {
301
366
  this.fishermanTimeBeforeBlock.record(Math.ceil(analysis.analysis.timeBeforeBlockMs), strategyAttributes);
302
367
 
368
+ // Record whether the block reached 100% blob capacity
369
+ if (analysis.analysis.blockBlobsFull) {
370
+ this.fishermanBlockBlobsFull.add(1, { ...strategyAttributes, [Attributes.OK]: true });
371
+ } else {
372
+ this.fishermanBlockBlobsFull.add(1, { ...strategyAttributes, [Attributes.OK]: false });
373
+ }
374
+
375
+ // Record the max blob capacity for this block
376
+ this.fishermanMaxBlobCapacity.record(analysis.analysis.maxBlobCapacity, strategyAttributes);
377
+
303
378
  // Record strategy-specific inclusion result
304
379
  if (strategyResult.wouldBeIncluded !== undefined) {
380
+ const inclusionAttributes = {
381
+ ...strategyAttributes,
382
+ [Attributes.BLOCK_FULL]: analysis.analysis.blockBlobsFull ? 'true' : 'false',
383
+ };
384
+
305
385
  if (strategyResult.wouldBeIncluded) {
306
- this.fishermanWouldBeIncluded.add(1, { ...strategyAttributes, [Attributes.OK]: true });
386
+ this.fishermanWouldBeIncluded.add(1, { ...inclusionAttributes, [Attributes.OK]: true });
307
387
  } else {
308
388
  this.fishermanWouldBeIncluded.add(1, {
309
- ...strategyAttributes,
389
+ ...inclusionAttributes,
310
390
  [Attributes.OK]: false,
311
391
  ...(strategyResult.exclusionReason && { [Attributes.ERROR_TYPE]: strategyResult.exclusionReason }),
312
392
  });
@@ -316,17 +396,29 @@ export class SequencerMetrics {
316
396
  // Record strategy-specific priority fee delta
317
397
  if (strategyResult.priorityFeeDelta !== undefined) {
318
398
  const priorityFeeDeltaGwei = Number(strategyResult.priorityFeeDelta) / 1e9;
319
- this.fishermanPriorityFeeDelta.record(priorityFeeDeltaGwei, strategyAttributes);
399
+ const deltaAttributes = {
400
+ ...strategyAttributes,
401
+ [Attributes.BLOCK_FULL]: analysis.analysis.blockBlobsFull ? 'true' : 'false',
402
+ };
403
+ this.fishermanPriorityFeeDelta.record(priorityFeeDeltaGwei, deltaAttributes);
320
404
  }
321
405
 
322
406
  // Record estimated cost if available
323
407
  if (strategyResult.estimatedCostEth !== undefined) {
324
- this.fishermanEstimatedCost.record(strategyResult.estimatedCostEth, strategyAttributes);
408
+ const costAttributes = {
409
+ ...strategyAttributes,
410
+ [Attributes.BLOCK_FULL]: analysis.analysis.blockBlobsFull ? 'true' : 'false',
411
+ };
412
+ this.fishermanEstimatedCost.record(strategyResult.estimatedCostEth, costAttributes);
325
413
  }
326
414
 
327
415
  // Record estimated overpayment if available
328
416
  if (strategyResult.estimatedOverpaymentEth !== undefined) {
329
- this.fishermanEstimatedOverpayment.record(strategyResult.estimatedOverpaymentEth, strategyAttributes);
417
+ const overpaymentAttributes = {
418
+ ...strategyAttributes,
419
+ [Attributes.BLOCK_FULL]: analysis.analysis.blockBlobsFull ? 'true' : 'false',
420
+ };
421
+ this.fishermanEstimatedOverpayment.record(strategyResult.estimatedOverpaymentEth, overpaymentAttributes);
330
422
  }
331
423
  }
332
424
  }
@@ -12,9 +12,9 @@ import type { DateProvider } from '@aztec/foundation/timer';
12
12
  import type { TypedEventEmitter } from '@aztec/foundation/types';
13
13
  import type { P2P } from '@aztec/p2p';
14
14
  import type { SlasherClientInterface } from '@aztec/slasher';
15
- import type { L2Block, L2BlockSink, L2BlockSource, ValidateCheckpointResult } from '@aztec/stdlib/block';
15
+ import type { BlockData, L2BlockSink, L2BlockSource, ValidateCheckpointResult } from '@aztec/stdlib/block';
16
16
  import type { Checkpoint } from '@aztec/stdlib/checkpoint';
17
- import { getSlotAtTimestamp, getSlotStartBuildTimestamp } from '@aztec/stdlib/epoch-helpers';
17
+ import { getSlotStartBuildTimestamp } from '@aztec/stdlib/epoch-helpers';
18
18
  import {
19
19
  type ResolvedSequencerConfig,
20
20
  type SequencerConfig,
@@ -25,7 +25,7 @@ import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
25
25
  import { pickFromSchema } from '@aztec/stdlib/schemas';
26
26
  import { MerkleTreeId } from '@aztec/stdlib/trees';
27
27
  import { Attributes, type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
28
- import { FullNodeCheckpointsBuilder, type ValidatorClient } from '@aztec/validator-client';
28
+ import { FullNodeCheckpointsBuilder, NodeKeystoreAdapter, type ValidatorClient } from '@aztec/validator-client';
29
29
 
30
30
  import EventEmitter from 'node:events';
31
31
 
@@ -60,6 +60,9 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
60
60
  /** The last slot for which we attempted to perform our voting duties with degraded block production */
61
61
  private lastSlotForFallbackVote: SlotNumber | undefined;
62
62
 
63
+ /** The last slot for which we logged "no committee" warning, to avoid spam */
64
+ private lastSlotForNoCommitteeWarning: SlotNumber | undefined;
65
+
63
66
  /** The last slot for which we triggered a checkpoint proposal job, to prevent duplicate attempts. */
64
67
  private lastSlotForCheckpointProposalJob: SlotNumber | undefined;
65
68
 
@@ -72,14 +75,6 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
72
75
  /** The maximum number of seconds that the sequencer can be into a slot to transition to a particular state. */
73
76
  protected timetable!: SequencerTimetable;
74
77
 
75
- // This shouldn't be here as this gets re-created each time we build/propose a block.
76
- // But we have a number of tests that abuse/rely on this class having a permanent publisher.
77
- // As long as those tests only configure a single publisher they will continue to work.
78
- // This will get re-assigned every time the sequencer goes to build a new block to a publisher that is valid
79
- // for the block proposer.
80
- // TODO(palla/mbps): Remove this field and fix tests
81
- protected publisher: SequencerPublisher | undefined;
82
-
83
78
  /** Config for the sequencer */
84
79
  protected config: ResolvedSequencerConfig = DefaultSequencerConfig;
85
80
 
@@ -115,7 +110,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
115
110
  /** Updates sequencer config by the defined values and updates the timetable */
116
111
  public updateConfig(config: Partial<SequencerConfig>) {
117
112
  const filteredConfig = pickFromSchema(config, SequencerConfigSchema);
118
- this.log.info(`Updated sequencer config`, omit(filteredConfig, 'txPublicSetupAllowList'));
113
+ this.log.info(`Updated sequencer config`, omit(filteredConfig, 'txPublicSetupAllowListExtend'));
119
114
  this.config = merge(this.config, filteredConfig);
120
115
  this.timetable = new SequencerTimetable(
121
116
  {
@@ -131,10 +126,9 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
131
126
  );
132
127
  }
133
128
 
134
- /** Initializes the sequencer (precomputes tables and creates a publisher). Takes about 3s. */
135
- public async init() {
129
+ /** Initializes the sequencer (precomputes tables). Takes about 3s. */
130
+ public init() {
136
131
  getKzg();
137
- this.publisher = (await this.publisherFactory.create(undefined)).publisher;
138
132
  }
139
133
 
140
134
  /** Starts the sequencer and moves to IDLE state. */
@@ -149,11 +143,16 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
149
143
  this.log.info('Started sequencer');
150
144
  }
151
145
 
146
+ /** Triggers an immediate run of the sequencer, bypassing the polling interval. */
147
+ public trigger() {
148
+ return this.runningPromise?.trigger();
149
+ }
150
+
152
151
  /** Stops the sequencer from building blocks and moves to STOPPED state. */
153
152
  public async stop(): Promise<void> {
154
153
  this.log.info(`Stopping sequencer`);
155
154
  this.setState(SequencerState.STOPPING, undefined, { force: true });
156
- this.publisher?.interrupt();
155
+ this.publisherFactory.interruptAll();
157
156
  await this.runningPromise?.stop();
158
157
  this.setState(SequencerState.STOPPED, undefined, { force: true });
159
158
  this.log.info('Stopped sequencer');
@@ -166,7 +165,6 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
166
165
  } catch (err) {
167
166
  this.emit('checkpoint-error', { error: err as Error });
168
167
  if (err instanceof SequencerTooSlowError) {
169
- // TODO(palla/mbps): Add missing states
170
168
  // Log as warn only if we had to abort halfway through the block proposal
171
169
  const logLvl = [SequencerState.INITIALIZING_CHECKPOINT, SequencerState.PROPOSER_CHECK].includes(
172
170
  err.proposedState,
@@ -288,8 +286,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
288
286
 
289
287
  const logCtx = {
290
288
  now,
291
- syncedToL1Ts: syncedTo.l1Timestamp,
292
- syncedToL2Slot: getSlotAtTimestamp(syncedTo.l1Timestamp, this.l1Constants),
289
+ syncedToL2Slot: syncedTo.syncedL2Slot,
293
290
  slot,
294
291
  slotTs: ts,
295
292
  checkpointNumber,
@@ -307,12 +304,12 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
307
304
  }
308
305
 
309
306
  // Check that the slot is not taken by a block already (should never happen, since only us can propose for this slot)
310
- if (syncedTo.block && syncedTo.block.header.getSlot() >= slot) {
307
+ if (syncedTo.blockData && syncedTo.blockData.header.getSlot() >= slot) {
311
308
  this.log.warn(
312
309
  `Cannot propose block at next L2 slot ${slot} since that slot was taken by block ${syncedTo.blockNumber}`,
313
- { ...logCtx, block: syncedTo.block.header.toInspect() },
310
+ { ...logCtx, block: syncedTo.blockData.header.toInspect() },
314
311
  );
315
- this.metrics.recordBlockProposalPrecheckFailed('slot_already_taken');
312
+ this.metrics.recordCheckpointPrecheckFailed('slot_already_taken');
316
313
  return undefined;
317
314
  }
318
315
 
@@ -323,7 +320,6 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
323
320
  const proposerForPublisher = this.config.fishermanMode ? undefined : proposer;
324
321
  const { attestorAddress, publisher } = await this.publisherFactory.create(proposerForPublisher);
325
322
  this.log.verbose(`Created publisher at address ${publisher.getSenderAddress()} for attestor ${attestorAddress}`);
326
- this.publisher = publisher;
327
323
 
328
324
  // In fisherman mode, set the actual proposer's address for simulations
329
325
  if (this.config.fishermanMode && proposer) {
@@ -336,7 +332,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
336
332
 
337
333
  // Check with the rollup contract if we can indeed propose at the next L2 slot. This check should not fail
338
334
  // if all the previous checks are good, but we do it just in case.
339
- const canProposeCheck = await publisher.canProposeAtNextEthBlock(
335
+ const canProposeCheck = await publisher.canProposeAt(
340
336
  syncedTo.archive,
341
337
  proposer ?? EthAddress.ZERO,
342
338
  invalidateCheckpoint,
@@ -348,7 +344,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
348
344
  logCtx,
349
345
  );
350
346
  this.emit('proposer-rollup-check-failed', { reason: 'Rollup contract check failed', slot });
351
- this.metrics.recordBlockProposalPrecheckFailed('rollup_contract_check_failed');
347
+ this.metrics.recordCheckpointPrecheckFailed('rollup_contract_check_failed');
352
348
  return undefined;
353
349
  }
354
350
 
@@ -358,7 +354,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
358
354
  { ...logCtx, rollup: canProposeCheck, expectedSlot: slot },
359
355
  );
360
356
  this.emit('proposer-rollup-check-failed', { reason: 'Slot mismatch', slot });
361
- this.metrics.recordBlockProposalPrecheckFailed('slot_mismatch');
357
+ this.metrics.recordCheckpointPrecheckFailed('slot_mismatch');
362
358
  return undefined;
363
359
  }
364
360
 
@@ -368,11 +364,12 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
368
364
  { ...logCtx, rollup: canProposeCheck, expectedSlot: slot },
369
365
  );
370
366
  this.emit('proposer-rollup-check-failed', { reason: 'Block mismatch', slot });
371
- this.metrics.recordBlockProposalPrecheckFailed('block_number_mismatch');
367
+ this.metrics.recordCheckpointPrecheckFailed('block_number_mismatch');
372
368
  return undefined;
373
369
  }
374
370
 
375
371
  this.lastSlotForCheckpointProposalJob = slot;
372
+ await this.p2pClient.prepareForSlot(slot);
376
373
  this.log.info(`Preparing checkpoint proposal ${checkpointNumber} at slot ${slot}`, { ...logCtx, proposer });
377
374
 
378
375
  // Create and return the checkpoint proposal job
@@ -424,11 +421,18 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
424
421
  this.metrics,
425
422
  this,
426
423
  this.setState.bind(this),
427
- this.log,
428
424
  this.tracer,
425
+ this.log.getBindings(),
429
426
  );
430
427
  }
431
428
 
429
+ /**
430
+ * Returns the current sequencer state.
431
+ */
432
+ public getState(): SequencerState {
433
+ return this.state;
434
+ }
435
+
432
436
  /**
433
437
  * Internal helper for setting the sequencer state and checks if we have enough time left in the slot to transition to the new state.
434
438
  * @param proposedState - The new state to transition to.
@@ -475,16 +479,15 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
475
479
  * We don't check against the previous block submitted since it may have been reorg'd out.
476
480
  */
477
481
  protected async checkSync(args: { ts: bigint; slot: SlotNumber }): Promise<SequencerSyncCheckResult | undefined> {
478
- // Check that the archiver and dependencies have synced to the previous L1 slot at least
479
- // TODO(#14766): Archiver reports L1 timestamp based on L1 blocks seen, which means that a missed L1 block will
480
- // cause the archiver L1 timestamp to fall behind, and cause this sequencer to start processing one L1 slot later.
481
- const l1Timestamp = await this.l2BlockSource.getL1Timestamp();
482
- const { slot, ts } = args;
483
- if (l1Timestamp === undefined || l1Timestamp + BigInt(this.l1Constants.ethereumSlotDuration) < ts) {
482
+ // Check that the archiver has fully synced the L2 slot before the one we want to propose in.
483
+ // The archiver reports sync progress via L1 block timestamps and synced checkpoint slots.
484
+ // See getSyncedL2SlotNumber for how missed L1 blocks are handled.
485
+ const syncedL2Slot = await this.l2BlockSource.getSyncedL2SlotNumber();
486
+ const { slot } = args;
487
+ if (syncedL2Slot === undefined || syncedL2Slot + 1 < slot) {
484
488
  this.log.debug(`Cannot propose block at next L2 slot ${slot} due to pending sync from L1`, {
485
489
  slot,
486
- ts,
487
- l1Timestamp,
490
+ syncedL2Slot,
488
491
  });
489
492
  return undefined;
490
493
  }
@@ -524,24 +527,24 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
524
527
  checkpointNumber: CheckpointNumber.ZERO,
525
528
  blockNumber: BlockNumber.ZERO,
526
529
  archive,
527
- l1Timestamp,
530
+ syncedL2Slot,
528
531
  pendingChainValidationStatus,
529
532
  };
530
533
  }
531
534
 
532
- const block = await this.l2BlockSource.getL2Block(blockNumber);
533
- if (!block) {
535
+ const blockData = await this.l2BlockSource.getBlockData(blockNumber);
536
+ if (!blockData) {
534
537
  // this shouldn't really happen because a moment ago we checked that all components were in sync
535
- this.log.error(`Failed to get L2 block ${blockNumber} from the archiver with all components in sync`);
538
+ this.log.error(`Failed to get L2 block data ${blockNumber} from the archiver with all components in sync`);
536
539
  return undefined;
537
540
  }
538
541
 
539
542
  return {
540
- block,
541
- blockNumber: block.number,
542
- checkpointNumber: block.checkpointNumber,
543
- archive: block.archive.root,
544
- l1Timestamp,
543
+ blockData,
544
+ blockNumber: blockData.header.getBlockNumber(),
545
+ checkpointNumber: blockData.checkpointNumber,
546
+ archive: blockData.archive.root,
547
+ syncedL2Slot,
545
548
  pendingChainValidationStatus,
546
549
  };
547
550
  }
@@ -557,7 +560,10 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
557
560
  proposer = await this.epochCache.getProposerAttesterAddressInSlot(slot);
558
561
  } catch (e) {
559
562
  if (e instanceof NoCommitteeError) {
560
- this.log.warn(`Cannot propose at next L2 slot ${slot} since the committee does not exist on L1`);
563
+ if (this.lastSlotForNoCommitteeWarning !== slot) {
564
+ this.lastSlotForNoCommitteeWarning = slot;
565
+ this.log.warn(`Cannot propose at next L2 slot ${slot} since the committee does not exist on L1`);
566
+ }
561
567
  return [false, undefined];
562
568
  }
563
569
  this.log.error(`Error getting proposer for slot ${slot}`, e);
@@ -717,7 +723,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
717
723
  syncedTo: SequencerSyncCheckResult,
718
724
  currentSlot: SlotNumber,
719
725
  ): Promise<void> {
720
- const { pendingChainValidationStatus, l1Timestamp } = syncedTo;
726
+ const { pendingChainValidationStatus, syncedL2Slot } = syncedTo;
721
727
  if (pendingChainValidationStatus.valid) {
722
728
  return;
723
729
  }
@@ -732,7 +738,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
732
738
 
733
739
  const logData = {
734
740
  invalidL1Timestamp: invalidCheckpointTimestamp,
735
- l1Timestamp,
741
+ syncedL2Slot,
736
742
  invalidCheckpoint: pendingChainValidationStatus.checkpoint,
737
743
  secondsBeforeInvalidatingBlockAsCommitteeMember,
738
744
  secondsBeforeInvalidatingBlockAsNonCommitteeMember,
@@ -860,6 +866,11 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
860
866
  return this.validatorClient?.getValidatorAddresses();
861
867
  }
862
868
 
869
+ /** Updates the publisher factory's node keystore adapter after a keystore reload. */
870
+ public updatePublisherNodeKeyStore(adapter: NodeKeystoreAdapter): void {
871
+ this.publisherFactory.updateNodeKeyStore(adapter);
872
+ }
873
+
863
874
  public getConfig() {
864
875
  return this.config;
865
876
  }
@@ -870,10 +881,10 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
870
881
  }
871
882
 
872
883
  type SequencerSyncCheckResult = {
873
- block?: L2Block;
884
+ blockData?: BlockData;
874
885
  checkpointNumber: CheckpointNumber;
875
886
  blockNumber: BlockNumber;
876
887
  archive: Fr;
877
- l1Timestamp: bigint;
888
+ syncedL2Slot: SlotNumber;
878
889
  pendingChainValidationStatus: ValidateCheckpointResult;
879
890
  };