@aztec/sequencer-client 0.0.1-commit.96bb3f7 → 0.0.1-commit.96dac018d

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 (91) hide show
  1. package/dest/client/sequencer-client.d.ts +12 -7
  2. package/dest/client/sequencer-client.d.ts.map +1 -1
  3. package/dest/client/sequencer-client.js +15 -4
  4. package/dest/config.d.ts +3 -4
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +17 -14
  7. package/dest/global_variable_builder/global_builder.d.ts +2 -4
  8. package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
  9. package/dest/global_variable_builder/global_builder.js +2 -2
  10. package/dest/index.d.ts +2 -2
  11. package/dest/index.d.ts.map +1 -1
  12. package/dest/index.js +1 -1
  13. package/dest/publisher/config.d.ts +35 -17
  14. package/dest/publisher/config.d.ts.map +1 -1
  15. package/dest/publisher/config.js +106 -42
  16. package/dest/publisher/index.d.ts +2 -1
  17. package/dest/publisher/index.d.ts.map +1 -1
  18. package/dest/publisher/l1_tx_failed_store/factory.d.ts +11 -0
  19. package/dest/publisher/l1_tx_failed_store/factory.d.ts.map +1 -0
  20. package/dest/publisher/l1_tx_failed_store/factory.js +22 -0
  21. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts +59 -0
  22. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -0
  23. package/dest/publisher/l1_tx_failed_store/failed_tx_store.js +1 -0
  24. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts +15 -0
  25. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts.map +1 -0
  26. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.js +34 -0
  27. package/dest/publisher/l1_tx_failed_store/index.d.ts +4 -0
  28. package/dest/publisher/l1_tx_failed_store/index.d.ts.map +1 -0
  29. package/dest/publisher/l1_tx_failed_store/index.js +2 -0
  30. package/dest/publisher/sequencer-publisher-factory.d.ts +11 -3
  31. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  32. package/dest/publisher/sequencer-publisher-factory.js +13 -2
  33. package/dest/publisher/sequencer-publisher-metrics.d.ts +1 -1
  34. package/dest/publisher/sequencer-publisher-metrics.d.ts.map +1 -1
  35. package/dest/publisher/sequencer-publisher-metrics.js +12 -4
  36. package/dest/publisher/sequencer-publisher.d.ts +22 -8
  37. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  38. package/dest/publisher/sequencer-publisher.js +297 -47
  39. package/dest/sequencer/checkpoint_proposal_job.d.ts +34 -9
  40. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  41. package/dest/sequencer/checkpoint_proposal_job.js +171 -41
  42. package/dest/sequencer/checkpoint_voter.d.ts +3 -2
  43. package/dest/sequencer/checkpoint_voter.d.ts.map +1 -1
  44. package/dest/sequencer/checkpoint_voter.js +34 -10
  45. package/dest/sequencer/index.d.ts +1 -2
  46. package/dest/sequencer/index.d.ts.map +1 -1
  47. package/dest/sequencer/index.js +0 -1
  48. package/dest/sequencer/metrics.d.ts +17 -5
  49. package/dest/sequencer/metrics.d.ts.map +1 -1
  50. package/dest/sequencer/metrics.js +111 -30
  51. package/dest/sequencer/sequencer.d.ts +31 -13
  52. package/dest/sequencer/sequencer.d.ts.map +1 -1
  53. package/dest/sequencer/sequencer.js +95 -36
  54. package/dest/sequencer/timetable.d.ts +1 -4
  55. package/dest/sequencer/timetable.d.ts.map +1 -1
  56. package/dest/sequencer/timetable.js +1 -4
  57. package/dest/test/index.d.ts +3 -5
  58. package/dest/test/index.d.ts.map +1 -1
  59. package/dest/test/mock_checkpoint_builder.d.ts +19 -13
  60. package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
  61. package/dest/test/mock_checkpoint_builder.js +31 -11
  62. package/dest/test/utils.d.ts +8 -8
  63. package/dest/test/utils.d.ts.map +1 -1
  64. package/dest/test/utils.js +12 -11
  65. package/package.json +30 -28
  66. package/src/client/sequencer-client.ts +25 -7
  67. package/src/config.ts +27 -22
  68. package/src/global_variable_builder/global_builder.ts +3 -3
  69. package/src/index.ts +1 -6
  70. package/src/publisher/config.ts +121 -43
  71. package/src/publisher/index.ts +3 -0
  72. package/src/publisher/l1_tx_failed_store/factory.ts +32 -0
  73. package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +55 -0
  74. package/src/publisher/l1_tx_failed_store/file_store_failed_tx_store.ts +46 -0
  75. package/src/publisher/l1_tx_failed_store/index.ts +3 -0
  76. package/src/publisher/sequencer-publisher-factory.ts +23 -6
  77. package/src/publisher/sequencer-publisher-metrics.ts +7 -3
  78. package/src/publisher/sequencer-publisher.ts +274 -53
  79. package/src/sequencer/checkpoint_proposal_job.ts +243 -59
  80. package/src/sequencer/checkpoint_voter.ts +32 -7
  81. package/src/sequencer/index.ts +0 -1
  82. package/src/sequencer/metrics.ts +124 -32
  83. package/src/sequencer/sequencer.ts +118 -38
  84. package/src/sequencer/timetable.ts +6 -5
  85. package/src/test/index.ts +2 -4
  86. package/src/test/mock_checkpoint_builder.ts +75 -34
  87. package/src/test/utils.ts +24 -14
  88. package/dest/sequencer/block_builder.d.ts +0 -26
  89. package/dest/sequencer/block_builder.d.ts.map +0 -1
  90. package/dest/sequencer/block_builder.js +0 -129
  91. package/src/sequencer/block_builder.ts +0 -216
@@ -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,7 +12,7 @@ import type { DateProvider } from '@aztec/foundation/timer';
12
12
  import type { TypedEventEmitter } from '@aztec/foundation/types';
13
13
  import type { P2P } from '@aztec/p2p';
14
14
  import type { SlasherClientInterface } from '@aztec/slasher';
15
- import type { L2BlockNew, L2BlockSink, L2BlockSource, ValidateCheckpointResult } from '@aztec/stdlib/block';
15
+ import type { BlockData, L2BlockSink, L2BlockSource, ValidateCheckpointResult } from '@aztec/stdlib/block';
16
16
  import type { Checkpoint } from '@aztec/stdlib/checkpoint';
17
17
  import { getSlotAtTimestamp, getSlotStartBuildTimestamp } from '@aztec/stdlib/epoch-helpers';
18
18
  import {
@@ -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
 
@@ -57,8 +57,11 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
57
57
  private state = SequencerState.STOPPED;
58
58
  private metrics: SequencerMetrics;
59
59
 
60
- /** The last slot for which we attempted to vote when sync failed, to prevent duplicate attempts. */
61
- private lastSlotForVoteWhenSyncFailed: SlotNumber | undefined;
60
+ /** The last slot for which we attempted to perform our voting duties with degraded block production */
61
+ private lastSlotForFallbackVote: SlotNumber | undefined;
62
+
63
+ /** The last slot for which we logged "no committee" warning, to avoid spam */
64
+ private lastSlotForNoCommitteeWarning: SlotNumber | undefined;
62
65
 
63
66
  /** The last slot for which we triggered a checkpoint proposal job, to prevent duplicate attempts. */
64
67
  private lastSlotForCheckpointProposalJob: SlotNumber | undefined;
@@ -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
 
@@ -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. */
@@ -153,7 +147,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
153
147
  public async stop(): Promise<void> {
154
148
  this.log.info(`Stopping sequencer`);
155
149
  this.setState(SequencerState.STOPPING, undefined, { force: true });
156
- this.publisher?.interrupt();
150
+ this.publisherFactory.interruptAll();
157
151
  await this.runningPromise?.stop();
158
152
  this.setState(SequencerState.STOPPED, undefined, { force: true });
159
153
  this.log.info('Stopped sequencer');
@@ -166,7 +160,6 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
166
160
  } catch (err) {
167
161
  this.emit('checkpoint-error', { error: err as Error });
168
162
  if (err instanceof SequencerTooSlowError) {
169
- // TODO(palla/mbps): Add missing states
170
163
  // Log as warn only if we had to abort halfway through the block proposal
171
164
  const logLvl = [SequencerState.INITIALIZING_CHECKPOINT, SequencerState.PROPOSER_CHECK].includes(
172
165
  err.proposedState,
@@ -202,7 +195,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
202
195
  const { slot, ts, now, epoch } = this.epochCache.getEpochAndSlotInNextL1Slot();
203
196
 
204
197
  // Check if we are synced and it's our slot, grab a publisher, check previous block invalidation, etc
205
- const checkpointProposalJob = await this.prepareCheckpointProposal(slot, ts, now);
198
+ const checkpointProposalJob = await this.prepareCheckpointProposal(epoch, slot, ts, now);
206
199
  if (!checkpointProposalJob) {
207
200
  return;
208
201
  }
@@ -234,6 +227,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
234
227
  */
235
228
  @trackSpan('Sequencer.prepareCheckpointProposal')
236
229
  private async prepareCheckpointProposal(
230
+ epoch: EpochNumber,
237
231
  slot: SlotNumber,
238
232
  ts: bigint,
239
233
  now: bigint,
@@ -263,6 +257,25 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
263
257
  return undefined;
264
258
  }
265
259
 
260
+ // If escape hatch is open for this epoch, do not start checkpoint proposal work and do not attempt invalidations.
261
+ // Still perform governance/slashing voting (as proposer) once per slot.
262
+ const isEscapeHatchOpen = await this.epochCache.isEscapeHatchOpen(epoch);
263
+
264
+ if (isEscapeHatchOpen) {
265
+ this.setState(SequencerState.PROPOSER_CHECK, slot);
266
+ const [canPropose, proposer] = await this.checkCanPropose(slot);
267
+ if (canPropose) {
268
+ await this.tryVoteWhenEscapeHatchOpen({ slot, proposer });
269
+ } else {
270
+ this.log.trace(`Escape hatch open but we are not proposer, skipping vote-only actions`, {
271
+ slot,
272
+ epoch,
273
+ proposer,
274
+ });
275
+ }
276
+ return undefined;
277
+ }
278
+
266
279
  // Next checkpoint follows from the last synced one
267
280
  const checkpointNumber = CheckpointNumber(syncedTo.checkpointNumber + 1);
268
281
 
@@ -287,12 +300,12 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
287
300
  }
288
301
 
289
302
  // Check that the slot is not taken by a block already (should never happen, since only us can propose for this slot)
290
- if (syncedTo.block && syncedTo.block.header.getSlot() >= slot) {
303
+ if (syncedTo.blockData && syncedTo.blockData.header.getSlot() >= slot) {
291
304
  this.log.warn(
292
305
  `Cannot propose block at next L2 slot ${slot} since that slot was taken by block ${syncedTo.blockNumber}`,
293
- { ...logCtx, block: syncedTo.block.header.toInspect() },
306
+ { ...logCtx, block: syncedTo.blockData.header.toInspect() },
294
307
  );
295
- this.metrics.recordBlockProposalPrecheckFailed('slot_already_taken');
308
+ this.metrics.recordCheckpointPrecheckFailed('slot_already_taken');
296
309
  return undefined;
297
310
  }
298
311
 
@@ -303,7 +316,6 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
303
316
  const proposerForPublisher = this.config.fishermanMode ? undefined : proposer;
304
317
  const { attestorAddress, publisher } = await this.publisherFactory.create(proposerForPublisher);
305
318
  this.log.verbose(`Created publisher at address ${publisher.getSenderAddress()} for attestor ${attestorAddress}`);
306
- this.publisher = publisher;
307
319
 
308
320
  // In fisherman mode, set the actual proposer's address for simulations
309
321
  if (this.config.fishermanMode && proposer) {
@@ -328,7 +340,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
328
340
  logCtx,
329
341
  );
330
342
  this.emit('proposer-rollup-check-failed', { reason: 'Rollup contract check failed', slot });
331
- this.metrics.recordBlockProposalPrecheckFailed('rollup_contract_check_failed');
343
+ this.metrics.recordCheckpointPrecheckFailed('rollup_contract_check_failed');
332
344
  return undefined;
333
345
  }
334
346
 
@@ -338,7 +350,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
338
350
  { ...logCtx, rollup: canProposeCheck, expectedSlot: slot },
339
351
  );
340
352
  this.emit('proposer-rollup-check-failed', { reason: 'Slot mismatch', slot });
341
- this.metrics.recordBlockProposalPrecheckFailed('slot_mismatch');
353
+ this.metrics.recordCheckpointPrecheckFailed('slot_mismatch');
342
354
  return undefined;
343
355
  }
344
356
 
@@ -348,15 +360,17 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
348
360
  { ...logCtx, rollup: canProposeCheck, expectedSlot: slot },
349
361
  );
350
362
  this.emit('proposer-rollup-check-failed', { reason: 'Block mismatch', slot });
351
- this.metrics.recordBlockProposalPrecheckFailed('block_number_mismatch');
363
+ this.metrics.recordCheckpointPrecheckFailed('block_number_mismatch');
352
364
  return undefined;
353
365
  }
354
366
 
355
367
  this.lastSlotForCheckpointProposalJob = slot;
368
+ await this.p2pClient.prepareForSlot(slot);
356
369
  this.log.info(`Preparing checkpoint proposal ${checkpointNumber} at slot ${slot}`, { ...logCtx, proposer });
357
370
 
358
371
  // Create and return the checkpoint proposal job
359
372
  return this.createCheckpointProposalJob(
373
+ epoch,
360
374
  slot,
361
375
  checkpointNumber,
362
376
  syncedTo.blockNumber,
@@ -368,6 +382,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
368
382
  }
369
383
 
370
384
  protected createCheckpointProposalJob(
385
+ epoch: EpochNumber,
371
386
  slot: SlotNumber,
372
387
  checkpointNumber: CheckpointNumber,
373
388
  syncedToBlockNumber: BlockNumber,
@@ -377,6 +392,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
377
392
  invalidateCheckpoint: InvalidateCheckpointRequest | undefined,
378
393
  ): CheckpointProposalJob {
379
394
  return new CheckpointProposalJob(
395
+ epoch,
380
396
  slot,
381
397
  checkpointNumber,
382
398
  syncedToBlockNumber,
@@ -389,6 +405,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
389
405
  this.p2pClient,
390
406
  this.worldState,
391
407
  this.l1ToL2MessageSource,
408
+ this.l2BlockSource,
392
409
  this.checkpointsBuilder,
393
410
  this.l2BlockSource,
394
411
  this.l1Constants,
@@ -400,11 +417,18 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
400
417
  this.metrics,
401
418
  this,
402
419
  this.setState.bind(this),
403
- this.log,
404
420
  this.tracer,
421
+ this.log.getBindings(),
405
422
  );
406
423
  }
407
424
 
425
+ /**
426
+ * Returns the current sequencer state.
427
+ */
428
+ public getState(): SequencerState {
429
+ return this.state;
430
+ }
431
+
408
432
  /**
409
433
  * Internal helper for setting the sequencer state and checks if we have enough time left in the slot to transition to the new state.
410
434
  * @param proposedState - The new state to transition to.
@@ -505,18 +529,18 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
505
529
  };
506
530
  }
507
531
 
508
- const block = await this.l2BlockSource.getL2BlockNew(blockNumber);
509
- if (!block) {
532
+ const blockData = await this.l2BlockSource.getBlockData(blockNumber);
533
+ if (!blockData) {
510
534
  // this shouldn't really happen because a moment ago we checked that all components were in sync
511
- this.log.error(`Failed to get L2 block ${blockNumber} from the archiver with all components in sync`);
535
+ this.log.error(`Failed to get L2 block data ${blockNumber} from the archiver with all components in sync`);
512
536
  return undefined;
513
537
  }
514
538
 
515
539
  return {
516
- block,
517
- blockNumber: block.number,
518
- checkpointNumber: block.checkpointNumber,
519
- archive: block.archive.root,
540
+ blockData,
541
+ blockNumber: blockData.header.getBlockNumber(),
542
+ checkpointNumber: blockData.checkpointNumber,
543
+ archive: blockData.archive.root,
520
544
  l1Timestamp,
521
545
  pendingChainValidationStatus,
522
546
  };
@@ -533,7 +557,10 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
533
557
  proposer = await this.epochCache.getProposerAttesterAddressInSlot(slot);
534
558
  } catch (e) {
535
559
  if (e instanceof NoCommitteeError) {
536
- this.log.warn(`Cannot propose at next L2 slot ${slot} since the committee does not exist on L1`);
560
+ if (this.lastSlotForNoCommitteeWarning !== slot) {
561
+ this.lastSlotForNoCommitteeWarning = slot;
562
+ this.log.warn(`Cannot propose at next L2 slot ${slot} since the committee does not exist on L1`);
563
+ }
537
564
  return [false, undefined];
538
565
  }
539
566
  this.log.error(`Error getting proposer for slot ${slot}`, e);
@@ -569,7 +596,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
569
596
  const { slot } = args;
570
597
 
571
598
  // Prevent duplicate attempts in the same slot
572
- if (this.lastSlotForVoteWhenSyncFailed === slot) {
599
+ if (this.lastSlotForFallbackVote === slot) {
573
600
  this.log.trace(`Already attempted to vote in slot ${slot} (skipping)`);
574
601
  return;
575
602
  }
@@ -601,7 +628,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
601
628
  }
602
629
 
603
630
  // Mark this slot as attempted
604
- this.lastSlotForVoteWhenSyncFailed = slot;
631
+ this.lastSlotForFallbackVote = slot;
605
632
 
606
633
  // Get a publisher for voting
607
634
  const { attestorAddress, publisher } = await this.publisherFactory.create(proposer);
@@ -636,7 +663,55 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
636
663
  }
637
664
 
638
665
  /**
639
- * Considers invalidating a checkpoint if the pending chain is invalid. Depends on how long the invalid checkpoint
666
+ * Tries to vote on slashing actions and governance proposals when escape hatch is open.
667
+ * This allows the sequencer to participate in voting without performing checkpoint proposal work.
668
+ */
669
+ @trackSpan('Sequencer.tryVoteWhenEscapeHatchOpen', ({ slot }) => ({ [Attributes.SLOT_NUMBER]: slot }))
670
+ protected async tryVoteWhenEscapeHatchOpen(args: {
671
+ slot: SlotNumber;
672
+ proposer: EthAddress | undefined;
673
+ }): Promise<void> {
674
+ const { slot, proposer } = args;
675
+
676
+ // Prevent duplicate attempts in the same slot
677
+ if (this.lastSlotForFallbackVote === slot) {
678
+ this.log.trace(`Already attempted to vote in slot ${slot} (escape hatch open, skipping)`);
679
+ return;
680
+ }
681
+
682
+ // Mark this slot as attempted
683
+ this.lastSlotForFallbackVote = slot;
684
+
685
+ const { attestorAddress, publisher } = await this.publisherFactory.create(proposer);
686
+
687
+ this.log.debug(`Escape hatch open for slot ${slot}, attempting vote-only actions`, { slot, attestorAddress });
688
+
689
+ const voter = new CheckpointVoter(
690
+ slot,
691
+ publisher,
692
+ attestorAddress,
693
+ this.validatorClient,
694
+ this.slasherClient,
695
+ this.l1Constants,
696
+ this.config,
697
+ this.metrics,
698
+ this.log,
699
+ );
700
+
701
+ const votesPromises = voter.enqueueVotes();
702
+ const votes = await Promise.all(votesPromises);
703
+
704
+ if (votes.every(p => !p)) {
705
+ this.log.debug(`No votes to enqueue for slot ${slot} (escape hatch open)`);
706
+ return;
707
+ }
708
+
709
+ this.log.info(`Voting in slot ${slot} (escape hatch open)`, { slot });
710
+ await publisher.sendRequests();
711
+ }
712
+
713
+ /**
714
+ * Considers invalidating a block if the pending chain is invalid. Depends on how long the invalid block
640
715
  * has been there without being invalidated and whether the sequencer is in the committee or not. We always
641
716
  * have the proposer try to invalidate, but if they fail, the sequencers in the committee are expected to try,
642
717
  * and if they fail, any sequencer will try as well.
@@ -788,6 +863,11 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
788
863
  return this.validatorClient?.getValidatorAddresses();
789
864
  }
790
865
 
866
+ /** Updates the publisher factory's node keystore adapter after a keystore reload. */
867
+ public updatePublisherNodeKeyStore(adapter: NodeKeystoreAdapter): void {
868
+ this.publisherFactory.updateNodeKeyStore(adapter);
869
+ }
870
+
791
871
  public getConfig() {
792
872
  return this.config;
793
873
  }
@@ -798,7 +878,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
798
878
  }
799
879
 
800
880
  type SequencerSyncCheckResult = {
801
- block?: L2BlockNew;
881
+ blockData?: BlockData;
802
882
  checkpointNumber: CheckpointNumber;
803
883
  blockNumber: BlockNumber;
804
884
  archive: Fr;
@@ -1,14 +1,15 @@
1
1
  import { createLogger } from '@aztec/aztec.js/log';
2
+ import {
3
+ CHECKPOINT_ASSEMBLE_TIME,
4
+ CHECKPOINT_INITIALIZATION_TIME,
5
+ DEFAULT_P2P_PROPAGATION_TIME,
6
+ MIN_EXECUTION_TIME,
7
+ } from '@aztec/stdlib/timetable';
2
8
 
3
- import { DEFAULT_ATTESTATION_PROPAGATION_TIME as DEFAULT_P2P_PROPAGATION_TIME } from '../config.js';
4
9
  import { SequencerTooSlowError } from './errors.js';
5
10
  import type { SequencerMetrics } from './metrics.js';
6
11
  import { SequencerState } from './utils.js';
7
12
 
8
- export const MIN_EXECUTION_TIME = 2;
9
- export const CHECKPOINT_INITIALIZATION_TIME = 1;
10
- export const CHECKPOINT_ASSEMBLE_TIME = 1;
11
-
12
13
  export class SequencerTimetable {
13
14
  /**
14
15
  * How late into the slot can we be to start working. Computed as the total time needed for assembling and publishing a block,
package/src/test/index.ts CHANGED
@@ -1,18 +1,16 @@
1
- import type { L1TxUtilsWithBlobs } from '@aztec/ethereum/l1-tx-utils-with-blobs';
1
+ import type { L1TxUtils } from '@aztec/ethereum/l1-tx-utils';
2
2
  import type { PublisherManager } from '@aztec/ethereum/publisher-manager';
3
3
  import type { PublicProcessorFactory } from '@aztec/simulator/server';
4
4
  import type { FullNodeCheckpointsBuilder, ValidatorClient } from '@aztec/validator-client';
5
5
 
6
6
  import { SequencerClient } from '../client/sequencer-client.js';
7
7
  import type { SequencerPublisherFactory } from '../publisher/sequencer-publisher-factory.js';
8
- import type { SequencerPublisher } from '../publisher/sequencer-publisher.js';
9
8
  import { Sequencer } from '../sequencer/sequencer.js';
10
9
  import type { SequencerTimetable } from '../sequencer/timetable.js';
11
10
 
12
11
  class TestSequencer_ extends Sequencer {
13
12
  declare public publicProcessorFactory: PublicProcessorFactory;
14
13
  declare public timetable: SequencerTimetable;
15
- declare public publisher: SequencerPublisher;
16
14
  declare public publisherFactory: SequencerPublisherFactory;
17
15
  declare public validatorClient: ValidatorClient;
18
16
  declare public checkpointsBuilder: FullNodeCheckpointsBuilder;
@@ -22,7 +20,7 @@ export type TestSequencer = TestSequencer_;
22
20
 
23
21
  class TestSequencerClient_ extends SequencerClient {
24
22
  declare public sequencer: TestSequencer;
25
- declare public publisherManager: PublisherManager<L1TxUtilsWithBlobs>;
23
+ declare public publisherManager: PublisherManager<L1TxUtils>;
26
24
  }
27
25
 
28
26
  export type TestSequencerClient = TestSequencerClient_;