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

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 (53) hide show
  1. package/dest/client/sequencer-client.js +1 -1
  2. package/dest/config.d.ts +1 -1
  3. package/dest/config.d.ts.map +1 -1
  4. package/dest/config.js +1 -3
  5. package/dest/global_variable_builder/global_builder.js +2 -2
  6. package/dest/index.d.ts +2 -2
  7. package/dest/index.d.ts.map +1 -1
  8. package/dest/index.js +1 -1
  9. package/dest/publisher/sequencer-publisher-metrics.d.ts +1 -1
  10. package/dest/publisher/sequencer-publisher-metrics.d.ts.map +1 -1
  11. package/dest/publisher/sequencer-publisher-metrics.js +12 -4
  12. package/dest/publisher/sequencer-publisher.d.ts +1 -2
  13. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  14. package/dest/publisher/sequencer-publisher.js +39 -18
  15. package/dest/sequencer/checkpoint_proposal_job.d.ts +28 -9
  16. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  17. package/dest/sequencer/checkpoint_proposal_job.js +134 -31
  18. package/dest/sequencer/checkpoint_voter.d.ts +3 -2
  19. package/dest/sequencer/checkpoint_voter.d.ts.map +1 -1
  20. package/dest/sequencer/checkpoint_voter.js +34 -10
  21. package/dest/sequencer/index.d.ts +1 -2
  22. package/dest/sequencer/index.d.ts.map +1 -1
  23. package/dest/sequencer/index.js +0 -1
  24. package/dest/sequencer/metrics.d.ts +2 -2
  25. package/dest/sequencer/metrics.d.ts.map +1 -1
  26. package/dest/sequencer/metrics.js +27 -17
  27. package/dest/sequencer/sequencer.d.ts +17 -9
  28. package/dest/sequencer/sequencer.d.ts.map +1 -1
  29. package/dest/sequencer/sequencer.js +67 -11
  30. package/dest/test/mock_checkpoint_builder.d.ts +17 -13
  31. package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
  32. package/dest/test/mock_checkpoint_builder.js +28 -10
  33. package/dest/test/utils.d.ts +8 -8
  34. package/dest/test/utils.d.ts.map +1 -1
  35. package/dest/test/utils.js +7 -7
  36. package/package.json +30 -28
  37. package/src/client/sequencer-client.ts +1 -1
  38. package/src/config.ts +1 -3
  39. package/src/global_variable_builder/global_builder.ts +2 -2
  40. package/src/index.ts +1 -6
  41. package/src/publisher/sequencer-publisher-metrics.ts +7 -3
  42. package/src/publisher/sequencer-publisher.ts +34 -18
  43. package/src/sequencer/checkpoint_proposal_job.ts +183 -51
  44. package/src/sequencer/checkpoint_voter.ts +32 -7
  45. package/src/sequencer/index.ts +0 -1
  46. package/src/sequencer/metrics.ts +36 -18
  47. package/src/sequencer/sequencer.ts +82 -10
  48. package/src/test/mock_checkpoint_builder.ts +64 -34
  49. package/src/test/utils.ts +19 -12
  50. package/dest/sequencer/block_builder.d.ts +0 -26
  51. package/dest/sequencer/block_builder.d.ts.map +0 -1
  52. package/dest/sequencer/block_builder.js +0 -129
  53. package/src/sequencer/block_builder.ts +0 -216
@@ -1,31 +1,40 @@
1
+ import { NUM_CHECKPOINT_END_MARKER_FIELDS, getNumBlockEndBlobFields } from '@aztec/blob-lib/encoding';
1
2
  import { BLOBS_PER_CHECKPOINT, FIELDS_PER_BLOB } from '@aztec/constants';
2
3
  import type { EpochCache } from '@aztec/epoch-cache';
3
- import { BlockNumber, CheckpointNumber, EpochNumber, SlotNumber } from '@aztec/foundation/branded-types';
4
+ import {
5
+ BlockNumber,
6
+ CheckpointNumber,
7
+ EpochNumber,
8
+ IndexWithinCheckpoint,
9
+ SlotNumber,
10
+ } from '@aztec/foundation/branded-types';
4
11
  import { randomInt } from '@aztec/foundation/crypto/random';
5
12
  import { Fr } from '@aztec/foundation/curves/bn254';
6
13
  import { EthAddress } from '@aztec/foundation/eth-address';
7
14
  import { Signature } from '@aztec/foundation/eth-signature';
8
15
  import { filter } from '@aztec/foundation/iterator';
9
- import type { Logger } from '@aztec/foundation/log';
16
+ import { type Logger, type LoggerBindings, createLogger } from '@aztec/foundation/log';
10
17
  import { sleep, sleepUntil } from '@aztec/foundation/sleep';
11
18
  import { type DateProvider, Timer } from '@aztec/foundation/timer';
12
- import { type TypedEventEmitter, unfreeze } from '@aztec/foundation/types';
19
+ import { type TypedEventEmitter, isErrorClass, unfreeze } from '@aztec/foundation/types';
13
20
  import type { P2P } from '@aztec/p2p';
14
21
  import type { SlasherClientInterface } from '@aztec/slasher';
15
22
  import {
16
23
  CommitteeAttestation,
17
24
  CommitteeAttestationsAndSigners,
18
- L2BlockNew,
25
+ L2Block,
19
26
  type L2BlockSink,
27
+ type L2BlockSource,
20
28
  MaliciousCommitteeAttestationsAndSigners,
21
29
  } from '@aztec/stdlib/block';
22
30
  import type { Checkpoint } from '@aztec/stdlib/checkpoint';
23
31
  import { getSlotStartBuildTimestamp } from '@aztec/stdlib/epoch-helpers';
24
32
  import { Gas } from '@aztec/stdlib/gas';
25
- import type {
26
- PublicProcessorLimits,
27
- ResolvedSequencerConfig,
28
- WorldStateSynchronizer,
33
+ import {
34
+ NoValidTxsError,
35
+ type PublicProcessorLimits,
36
+ type ResolvedSequencerConfig,
37
+ type WorldStateSynchronizer,
29
38
  } from '@aztec/stdlib/interfaces/server';
30
39
  import { type L1ToL2MessageSource, computeInHashFromL1ToL2Messages } from '@aztec/stdlib/messaging';
31
40
  import type { BlockProposalOptions, CheckpointProposal, CheckpointProposalOptions } from '@aztec/stdlib/p2p';
@@ -35,6 +44,7 @@ import { type FailedTx, Tx } from '@aztec/stdlib/tx';
35
44
  import { AttestationTimeoutError } from '@aztec/stdlib/validators';
36
45
  import { Attributes, type Traceable, type Tracer, trackSpan } from '@aztec/telemetry-client';
37
46
  import { CheckpointBuilder, type FullNodeCheckpointsBuilder, type ValidatorClient } from '@aztec/validator-client';
47
+ import { DutyAlreadySignedError, SlashingProtectionError } from '@aztec/validator-ha-signer/errors';
38
48
 
39
49
  import type { GlobalVariableBuilder } from '../global_variable_builder/global_builder.js';
40
50
  import type { InvalidateCheckpointRequest, SequencerPublisher } from '../publisher/sequencer-publisher.js';
@@ -56,7 +66,10 @@ const TXS_POLLING_MS = 500;
56
66
  * the Sequencer once the check for being the proposer for the slot has succeeded.
57
67
  */
58
68
  export class CheckpointProposalJob implements Traceable {
69
+ protected readonly log: Logger;
70
+
59
71
  constructor(
72
+ private readonly epoch: EpochNumber,
60
73
  private readonly slot: SlotNumber,
61
74
  private readonly checkpointNumber: CheckpointNumber,
62
75
  private readonly syncedToBlockNumber: BlockNumber,
@@ -70,6 +83,7 @@ export class CheckpointProposalJob implements Traceable {
70
83
  private readonly p2pClient: P2P,
71
84
  private readonly worldState: WorldStateSynchronizer,
72
85
  private readonly l1ToL2MessageSource: L1ToL2MessageSource,
86
+ private readonly l2BlockSource: L2BlockSource,
73
87
  private readonly checkpointsBuilder: FullNodeCheckpointsBuilder,
74
88
  private readonly blockSink: L2BlockSink,
75
89
  private readonly l1Constants: SequencerRollupConstants,
@@ -81,9 +95,11 @@ export class CheckpointProposalJob implements Traceable {
81
95
  private readonly metrics: SequencerMetrics,
82
96
  private readonly eventEmitter: TypedEventEmitter<SequencerEvents>,
83
97
  private readonly setStateFn: (state: SequencerState, slot?: SlotNumber) => void,
84
- protected readonly log: Logger,
85
98
  public readonly tracer: Tracer,
86
- ) {}
99
+ bindings?: LoggerBindings,
100
+ ) {
101
+ this.log = createLogger('sequencer:checkpoint-proposal', { ...bindings, instanceId: `slot-${slot}` });
102
+ }
87
103
 
88
104
  /**
89
105
  * Executes the checkpoint proposal job.
@@ -169,6 +185,12 @@ export class CheckpointProposalJob implements Traceable {
169
185
  const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(this.checkpointNumber);
170
186
  const inHash = computeInHashFromL1ToL2Messages(l1ToL2Messages);
171
187
 
188
+ // Collect the out hashes of all the checkpoints before this one in the same epoch
189
+ const previousCheckpoints = (await this.l2BlockSource.getCheckpointsForEpoch(this.epoch)).filter(
190
+ c => c.number < this.checkpointNumber,
191
+ );
192
+ const previousCheckpointOutHashes = previousCheckpoints.map(c => c.getCheckpointOutHash());
193
+
172
194
  // Create a long-lived forked world state for the checkpoint builder
173
195
  using fork = await this.worldState.fork(this.syncedToBlockNumber, { closeDelayMs: 12_000 });
174
196
 
@@ -177,7 +199,9 @@ export class CheckpointProposalJob implements Traceable {
177
199
  this.checkpointNumber,
178
200
  checkpointGlobalVariables,
179
201
  l1ToL2Messages,
202
+ previousCheckpointOutHashes,
180
203
  fork,
204
+ this.log.getBindings(),
181
205
  );
182
206
 
183
207
  // Options for the validator client when creating block and checkpoint proposals
@@ -191,13 +215,40 @@ export class CheckpointProposalJob implements Traceable {
191
215
  broadcastInvalidCheckpointProposal: this.config.broadcastInvalidBlockProposal,
192
216
  };
193
217
 
194
- // Main loop: build blocks for the checkpoint
195
- const { blocksInCheckpoint, blockPendingBroadcast } = await this.buildBlocksForCheckpoint(
196
- checkpointBuilder,
197
- checkpointGlobalVariables.timestamp,
198
- inHash,
199
- blockProposalOptions,
200
- );
218
+ let blocksInCheckpoint: L2Block[] = [];
219
+ let blockPendingBroadcast: { block: L2Block; txs: Tx[] } | undefined = undefined;
220
+
221
+ try {
222
+ // Main loop: build blocks for the checkpoint
223
+ const result = await this.buildBlocksForCheckpoint(
224
+ checkpointBuilder,
225
+ checkpointGlobalVariables.timestamp,
226
+ inHash,
227
+ blockProposalOptions,
228
+ );
229
+ blocksInCheckpoint = result.blocksInCheckpoint;
230
+ blockPendingBroadcast = result.blockPendingBroadcast;
231
+ } catch (err) {
232
+ // These errors are expected in HA mode, so we yield and let another HA node handle the slot
233
+ // The only distinction between the 2 errors is SlashingProtectionError throws when the payload is different,
234
+ // which is normal for block building (may have picked different txs)
235
+ if (err instanceof DutyAlreadySignedError) {
236
+ this.log.info(`Checkpoint proposal for slot ${this.slot} already signed by another HA node, yielding`, {
237
+ slot: this.slot,
238
+ signedByNode: err.signedByNode,
239
+ });
240
+ return undefined;
241
+ }
242
+ if (err instanceof SlashingProtectionError) {
243
+ this.log.info(`Checkpoint proposal for slot ${this.slot} blocked by slashing protection, yielding`, {
244
+ slot: this.slot,
245
+ existingMessageHash: err.existingMessageHash,
246
+ attemptedMessageHash: err.attemptedMessageHash,
247
+ });
248
+ return undefined;
249
+ }
250
+ throw err;
251
+ }
201
252
 
202
253
  if (blocksInCheckpoint.length === 0) {
203
254
  this.log.warn(`No blocks were built for slot ${this.slot}`, { slot: this.slot });
@@ -252,7 +303,34 @@ export class CheckpointProposalJob implements Traceable {
252
303
 
253
304
  // Proposer must sign over the attestations before pushing them to L1
254
305
  const signer = this.proposer ?? this.publisher.getSenderAddress();
255
- const attestationsSignature = await this.validatorClient.signAttestationsAndSigners(attestations, signer);
306
+ let attestationsSignature: Signature;
307
+ try {
308
+ attestationsSignature = await this.validatorClient.signAttestationsAndSigners(
309
+ attestations,
310
+ signer,
311
+ this.slot,
312
+ this.checkpointNumber,
313
+ );
314
+ } catch (err) {
315
+ // We shouldn't really get here since we yield to another HA node
316
+ // as soon as we see these errors when creating block proposals.
317
+ if (err instanceof DutyAlreadySignedError) {
318
+ this.log.info(`Attestations signature for slot ${this.slot} already signed by another HA node, yielding`, {
319
+ slot: this.slot,
320
+ signedByNode: err.signedByNode,
321
+ });
322
+ return undefined;
323
+ }
324
+ if (err instanceof SlashingProtectionError) {
325
+ this.log.info(`Attestations signature for slot ${this.slot} blocked by slashing protection, yielding`, {
326
+ slot: this.slot,
327
+ existingMessageHash: err.existingMessageHash,
328
+ attemptedMessageHash: err.attemptedMessageHash,
329
+ });
330
+ return undefined;
331
+ }
332
+ throw err;
333
+ }
256
334
 
257
335
  // Enqueue publishing the checkpoint to L1
258
336
  this.setStateFn(SequencerState.PUBLISHING_CHECKPOINT, this.slot);
@@ -266,6 +344,11 @@ export class CheckpointProposalJob implements Traceable {
266
344
 
267
345
  return checkpoint;
268
346
  } catch (err) {
347
+ if (err && (err instanceof DutyAlreadySignedError || err instanceof SlashingProtectionError)) {
348
+ // swallow this error. It's already been logged by a function deeper in the stack
349
+ return undefined;
350
+ }
351
+
269
352
  this.log.error(`Error building checkpoint at slot ${this.slot}`, err);
270
353
  return undefined;
271
354
  }
@@ -281,19 +364,22 @@ export class CheckpointProposalJob implements Traceable {
281
364
  inHash: Fr,
282
365
  blockProposalOptions: BlockProposalOptions,
283
366
  ): Promise<{
284
- blocksInCheckpoint: L2BlockNew[];
285
- blockPendingBroadcast: { block: L2BlockNew; txs: Tx[] } | undefined;
367
+ blocksInCheckpoint: L2Block[];
368
+ blockPendingBroadcast: { block: L2Block; txs: Tx[] } | undefined;
286
369
  }> {
287
- const blocksInCheckpoint: L2BlockNew[] = [];
370
+ const blocksInCheckpoint: L2Block[] = [];
288
371
  const txHashesAlreadyIncluded = new Set<string>();
289
372
  const initialBlockNumber = BlockNumber(this.syncedToBlockNumber + 1);
290
373
 
374
+ // Remaining blob fields available for blocks (checkpoint end marker already subtracted)
375
+ let remainingBlobFields = BLOBS_PER_CHECKPOINT * FIELDS_PER_BLOB - NUM_CHECKPOINT_END_MARKER_FIELDS;
376
+
291
377
  // Last block in the checkpoint will usually be flagged as pending broadcast, so we send it along with the checkpoint proposal
292
- let blockPendingBroadcast: { block: L2BlockNew; txs: Tx[] } | undefined = undefined;
378
+ let blockPendingBroadcast: { block: L2Block; txs: Tx[] } | undefined = undefined;
293
379
 
294
380
  while (true) {
295
381
  const blocksBuilt = blocksInCheckpoint.length;
296
- const indexWithinCheckpoint = blocksBuilt;
382
+ const indexWithinCheckpoint = IndexWithinCheckpoint(blocksBuilt);
297
383
  const blockNumber = BlockNumber(initialBlockNumber + blocksBuilt);
298
384
 
299
385
  const secondsIntoSlot = this.getSecondsIntoSlot();
@@ -320,8 +406,10 @@ export class CheckpointProposalJob implements Traceable {
320
406
  blockNumber,
321
407
  indexWithinCheckpoint,
322
408
  txHashesAlreadyIncluded,
409
+ remainingBlobFields,
323
410
  });
324
411
 
412
+ // TODO(palla/mbps): Review these conditions. We may want to keep trying in some scenarios.
325
413
  if (!buildResult && timingInfo.isLastBlock) {
326
414
  // If no block was produced due to not enough txs and this was the last subslot, exit
327
415
  break;
@@ -344,13 +432,21 @@ export class CheckpointProposalJob implements Traceable {
344
432
  break;
345
433
  }
346
434
 
347
- const { block, usedTxs } = buildResult;
435
+ const { block, usedTxs, remainingBlobFields: newRemainingBlobFields } = buildResult;
348
436
  blocksInCheckpoint.push(block);
349
437
 
438
+ // Update remaining blob fields for the next block
439
+ remainingBlobFields = newRemainingBlobFields;
440
+
350
441
  // Sync the proposed block to the archiver to make it available
351
442
  // Note that the checkpoint builder uses its own fork so it should not need to wait for this syncing
352
443
  // Eventually we should refactor the checkpoint builder to not need a separate long-lived fork
353
- await this.syncProposedBlockToArchiver(block);
444
+ // Fire and forget - don't block the critical path, but log errors
445
+ this.syncProposedBlockToArchiver(block).catch(err => {
446
+ this.log.error(`Failed to sync proposed block ${block.number} to archiver`, { blockNumber: block.number, err });
447
+ });
448
+
449
+ usedTxs.forEach(tx => txHashesAlreadyIncluded.add(tx.txHash.toString()));
354
450
 
355
451
  // If this is the last block, exit the loop now so we start collecting attestations
356
452
  if (timingInfo.isLastBlock) {
@@ -400,19 +496,27 @@ export class CheckpointProposalJob implements Traceable {
400
496
 
401
497
  /** Builds a single block. Called from the main block building loop. */
402
498
  @trackSpan('CheckpointProposalJob.buildSingleBlock')
403
- private async buildSingleBlock(
499
+ protected async buildSingleBlock(
404
500
  checkpointBuilder: CheckpointBuilder,
405
501
  opts: {
406
502
  forceCreate?: boolean;
407
503
  blockTimestamp: bigint;
408
504
  blockNumber: BlockNumber;
409
- indexWithinCheckpoint: number;
505
+ indexWithinCheckpoint: IndexWithinCheckpoint;
410
506
  buildDeadline: Date | undefined;
411
507
  txHashesAlreadyIncluded: Set<string>;
508
+ remainingBlobFields: number;
412
509
  },
413
- ): Promise<{ block: L2BlockNew; usedTxs: Tx[] } | { error: Error } | undefined> {
414
- const { blockTimestamp, forceCreate, blockNumber, indexWithinCheckpoint, buildDeadline, txHashesAlreadyIncluded } =
415
- opts;
510
+ ): Promise<{ block: L2Block; usedTxs: Tx[]; remainingBlobFields: number } | { error: Error } | undefined> {
511
+ const {
512
+ blockTimestamp,
513
+ forceCreate,
514
+ blockNumber,
515
+ indexWithinCheckpoint,
516
+ buildDeadline,
517
+ txHashesAlreadyIncluded,
518
+ remainingBlobFields,
519
+ } = opts;
416
520
 
417
521
  this.log.verbose(
418
522
  `Preparing block ${blockNumber} index ${indexWithinCheckpoint} at checkpoint ${this.checkpointNumber} for slot ${this.slot}`,
@@ -445,46 +549,52 @@ export class CheckpointProposalJob implements Traceable {
445
549
  { slot: this.slot, blockNumber, indexWithinCheckpoint },
446
550
  );
447
551
  this.setStateFn(SequencerState.CREATING_BLOCK, this.slot);
552
+
553
+ // Calculate blob fields limit for txs (remaining capacity - this block's end overhead)
554
+ const blockEndOverhead = getNumBlockEndBlobFields(indexWithinCheckpoint === 0);
555
+ const maxBlobFieldsForTxs = remainingBlobFields - blockEndOverhead;
556
+
448
557
  const blockBuilderOptions: PublicProcessorLimits = {
449
558
  maxTransactions: this.config.maxTxsPerBlock,
450
559
  maxBlockSize: this.config.maxBlockSizeInBytes,
451
560
  maxBlockGas: new Gas(this.config.maxDABlockGas, this.config.maxL2BlockGas),
452
- maxBlobFields: BLOBS_PER_CHECKPOINT * FIELDS_PER_BLOB,
561
+ maxBlobFields: maxBlobFieldsForTxs,
453
562
  deadline: buildDeadline,
454
563
  };
455
564
 
456
565
  // Actually build the block by executing txs
457
- const workTimer = new Timer();
458
- const { publicGas, block, publicProcessorDuration, numTxs, blockBuildingTimer, usedTxs, failedTxs } =
459
- await checkpointBuilder.buildBlock(pendingTxs, blockNumber, blockTimestamp, blockBuilderOptions);
460
- const blockBuildDuration = workTimer.ms();
566
+ const buildResult = await this.buildSingleBlockWithCheckpointBuilder(
567
+ checkpointBuilder,
568
+ pendingTxs,
569
+ blockNumber,
570
+ blockTimestamp,
571
+ blockBuilderOptions,
572
+ );
461
573
 
462
574
  // If any txs failed during execution, drop them from the mempool so we don't pick them up again
463
- await this.dropFailedTxsFromP2P(failedTxs);
575
+ await this.dropFailedTxsFromP2P(buildResult.failedTxs);
464
576
 
465
577
  // Check if we have created a block with enough txs. If there were invalid txs in the pool, or if execution took
466
578
  // too long, then we may not get to minTxsPerBlock after executing public functions.
467
579
  const minValidTxs = this.config.minValidTxsPerBlock ?? minTxs;
468
- if (!forceCreate && numTxs < minValidTxs) {
580
+ const numTxs = buildResult.status === 'no-valid-txs' ? 0 : buildResult.numTxs;
581
+ if (buildResult.status === 'no-valid-txs' || (!forceCreate && numTxs < minValidTxs)) {
469
582
  this.log.warn(
470
- `Block ${blockNumber} at index ${indexWithinCheckpoint} on slot ${this.slot} has too few valid txs to be proposed (got ${numTxs} but required ${minValidTxs})`,
471
- { slot: this.slot, blockNumber, numTxs, indexWithinCheckpoint },
583
+ `Block ${blockNumber} at index ${indexWithinCheckpoint} on slot ${this.slot} has too few valid txs to be proposed`,
584
+ { slot: this.slot, blockNumber, numTxs, indexWithinCheckpoint, minValidTxs, buildResult: buildResult.status },
472
585
  );
473
- this.eventEmitter.emit('block-tx-count-check-failed', {
474
- minTxs: minValidTxs,
475
- availableTxs: numTxs,
476
- slot: this.slot,
477
- });
586
+ this.eventEmitter.emit('block-build-failed', { reason: `Insufficient valid txs`, slot: this.slot });
478
587
  this.metrics.recordBlockProposalFailed('insufficient_valid_txs');
479
588
  return undefined;
480
589
  }
481
590
 
482
591
  // Block creation succeeded, emit stats and metrics
592
+ const { publicGas, block, publicProcessorDuration, usedTxs, usedTxBlobFields, blockBuildDuration } = buildResult;
593
+
483
594
  const blockStats = {
484
595
  eventName: 'l2-block-built',
485
596
  duration: blockBuildDuration,
486
597
  publicProcessDuration: publicProcessorDuration,
487
- rollupCircuitsDuration: blockBuildingTimer.ms(),
488
598
  ...block.getStats(),
489
599
  } satisfies L2BlockBuiltStats;
490
600
 
@@ -500,7 +610,7 @@ export class CheckpointProposalJob implements Traceable {
500
610
  this.eventEmitter.emit('block-proposed', { blockNumber: block.number, slot: this.slot });
501
611
  this.metrics.recordBuiltBlock(blockBuildDuration, publicGas.l2Gas);
502
612
 
503
- return { block, usedTxs };
613
+ return { block, usedTxs, remainingBlobFields: maxBlobFieldsForTxs - usedTxBlobFields };
504
614
  } catch (err: any) {
505
615
  this.eventEmitter.emit('block-build-failed', { reason: err.message, slot: this.slot });
506
616
  this.log.error(`Error building block`, err, { blockNumber, slot: this.slot });
@@ -510,17 +620,40 @@ export class CheckpointProposalJob implements Traceable {
510
620
  }
511
621
  }
512
622
 
623
+ /** Uses the checkpoint builder to build a block, catching specific txs */
624
+ private async buildSingleBlockWithCheckpointBuilder(
625
+ checkpointBuilder: CheckpointBuilder,
626
+ pendingTxs: AsyncIterable<Tx>,
627
+ blockNumber: BlockNumber,
628
+ blockTimestamp: bigint,
629
+ blockBuilderOptions: PublicProcessorLimits,
630
+ ) {
631
+ try {
632
+ const workTimer = new Timer();
633
+ const result = await checkpointBuilder.buildBlock(pendingTxs, blockNumber, blockTimestamp, blockBuilderOptions);
634
+ const blockBuildDuration = workTimer.ms();
635
+ return { ...result, blockBuildDuration, status: 'success' as const };
636
+ } catch (err: unknown) {
637
+ if (isErrorClass(err, NoValidTxsError)) {
638
+ return { failedTxs: err.failedTxs, status: 'no-valid-txs' as const };
639
+ }
640
+ throw err;
641
+ }
642
+ }
643
+
513
644
  /** Waits until minTxs are available on the pool for building a block. */
514
645
  @trackSpan('CheckpointProposalJob.waitForMinTxs')
515
646
  private async waitForMinTxs(opts: {
516
647
  forceCreate?: boolean;
517
648
  blockNumber: BlockNumber;
518
- indexWithinCheckpoint: number;
649
+ indexWithinCheckpoint: IndexWithinCheckpoint;
519
650
  buildDeadline: Date | undefined;
520
651
  }): Promise<{ canStartBuilding: boolean; availableTxs: number }> {
521
- const minTxs = this.config.minTxsPerBlock;
522
652
  const { indexWithinCheckpoint, blockNumber, buildDeadline, forceCreate } = opts;
523
653
 
654
+ // We only allow a block with 0 txs in the first block of the checkpoint
655
+ const minTxs = indexWithinCheckpoint > 0 && this.config.minTxsPerBlock === 0 ? 1 : this.config.minTxsPerBlock;
656
+
524
657
  // Deadline is undefined if we are not enforcing the timetable, meaning we'll exit immediately when out of time
525
658
  const startBuildingDeadline = buildDeadline
526
659
  ? new Date(buildDeadline.getTime() - this.timetable.minExecutionTime * 1000)
@@ -582,7 +715,7 @@ export class CheckpointProposalJob implements Traceable {
582
715
  const attestationTimeAllowed = this.config.enforceTimeTable
583
716
  ? this.timetable.getMaxAllowedTime(SequencerState.PUBLISHING_CHECKPOINT)!
584
717
  : this.l1Constants.slotDuration;
585
- const attestationDeadline = new Date(this.dateProvider.now() + attestationTimeAllowed * 1000);
718
+ const attestationDeadline = new Date((this.getSlotStartBuildTimestamp() + attestationTimeAllowed) * 1000);
586
719
 
587
720
  this.metrics.recordRequiredAttestations(numberOfRequiredAttestations, attestationTimeAllowed);
588
721
 
@@ -678,8 +811,7 @@ export class CheckpointProposalJob implements Traceable {
678
811
  * Gossip doesn't echo messages back to the sender, so the proposer's archiver/world-state
679
812
  * would never receive its own block without this explicit sync.
680
813
  */
681
- private async syncProposedBlockToArchiver(block: L2BlockNew): Promise<void> {
682
- // TODO(palla/mbps): Change default to false once block sync is stable.
814
+ private async syncProposedBlockToArchiver(block: L2Block): Promise<void> {
683
815
  if (this.config.skipPushProposedBlocksToArchiver !== false) {
684
816
  this.log.warn(`Skipping push of proposed block ${block.number} to archiver`, {
685
817
  blockNumber: block.number,
@@ -5,6 +5,8 @@ import type { SlasherClientInterface } from '@aztec/slasher';
5
5
  import { getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
6
6
  import type { ResolvedSequencerConfig } from '@aztec/stdlib/interfaces/server';
7
7
  import type { ValidatorClient } from '@aztec/validator-client';
8
+ import { DutyAlreadySignedError } from '@aztec/validator-ha-signer/errors';
9
+ import { DutyType, type SigningContext } from '@aztec/validator-ha-signer/types';
8
10
 
9
11
  import type { TypedDataDefinition } from 'viem';
10
12
 
@@ -17,7 +19,8 @@ import type { SequencerRollupConstants } from './types.js';
17
19
  */
18
20
  export class CheckpointVoter {
19
21
  private slotTimestamp: bigint;
20
- private signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>;
22
+ private governanceSigner: (msg: TypedDataDefinition) => Promise<`0x${string}`>;
23
+ private slashingSigner: (msg: TypedDataDefinition) => Promise<`0x${string}`>;
21
24
 
22
25
  constructor(
23
26
  private readonly slot: SlotNumber,
@@ -31,8 +34,16 @@ export class CheckpointVoter {
31
34
  private readonly log: Logger,
32
35
  ) {
33
36
  this.slotTimestamp = getTimestampForSlot(this.slot, this.l1Constants);
34
- this.signer = (msg: TypedDataDefinition) =>
35
- this.validatorClient.signWithAddress(this.attestorAddress, msg).then(s => s.toString());
37
+
38
+ // Create separate signers with appropriate duty contexts for governance and slashing votes
39
+ // These use HA protection to ensure only one node signs per slot/duty
40
+ const governanceContext: SigningContext = { slot: this.slot, dutyType: DutyType.GOVERNANCE_VOTE };
41
+ this.governanceSigner = (msg: TypedDataDefinition) =>
42
+ this.validatorClient.signWithAddress(this.attestorAddress, msg, governanceContext).then(s => s.toString());
43
+
44
+ const slashingContext: SigningContext = { slot: this.slot, dutyType: DutyType.SLASHING_VOTE };
45
+ this.slashingSigner = (msg: TypedDataDefinition) =>
46
+ this.validatorClient.signWithAddress(this.attestorAddress, msg, slashingContext).then(s => s.toString());
36
47
  }
37
48
 
38
49
  /**
@@ -68,10 +79,17 @@ export class CheckpointVoter {
68
79
  this.slot,
69
80
  this.slotTimestamp,
70
81
  this.attestorAddress,
71
- this.signer,
82
+ this.governanceSigner,
72
83
  );
73
84
  } catch (err) {
74
- this.log.error(`Error enqueuing governance vote`, err, { slot: this.slot });
85
+ if (err instanceof DutyAlreadySignedError) {
86
+ this.log.info(`Governance vote already signed by another node`, {
87
+ slot: this.slot,
88
+ signedByNode: err.signedByNode,
89
+ });
90
+ } else {
91
+ this.log.error(`Error enqueueing governance vote`, err);
92
+ }
75
93
  return false;
76
94
  }
77
95
  }
@@ -95,10 +113,17 @@ export class CheckpointVoter {
95
113
  this.slot,
96
114
  this.slotTimestamp,
97
115
  this.attestorAddress,
98
- this.signer,
116
+ this.slashingSigner,
99
117
  );
100
118
  } catch (err) {
101
- this.log.error(`Error enqueuing slashing vote`, err, { slot: this.slot });
119
+ if (err instanceof DutyAlreadySignedError) {
120
+ this.log.info(`Slashing vote already signed by another node`, {
121
+ slot: this.slot,
122
+ signedByNode: err.signedByNode,
123
+ });
124
+ } else {
125
+ this.log.error(`Error enqueueing slashing vote`, err);
126
+ }
102
127
  return false;
103
128
  }
104
129
  }
@@ -1,4 +1,3 @@
1
- export * from './block_builder.js';
2
1
  export * from './checkpoint_proposal_job.js';
3
2
  export * from './checkpoint_voter.js';
4
3
  export * from './config.js';
@@ -11,6 +11,7 @@ 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';
@@ -67,7 +68,9 @@ export class SequencerMetrics {
67
68
  this.meter = client.getMeter(name);
68
69
  this.tracer = client.getTracer(name);
69
70
 
70
- this.blockCounter = this.meter.createUpDownCounter(Metrics.SEQUENCER_BLOCK_COUNT);
71
+ this.blockCounter = createUpDownCounterWithDefault(this.meter, Metrics.SEQUENCER_BLOCK_COUNT, {
72
+ [Attributes.STATUS]: ['failed', 'built'],
73
+ });
71
74
 
72
75
  this.blockBuildDuration = this.meter.createHistogram(Metrics.SEQUENCER_BLOCK_BUILD_DURATION);
73
76
 
@@ -77,23 +80,15 @@ export class SequencerMetrics {
77
80
 
78
81
  this.checkpointAttestationDelay = this.meter.createHistogram(Metrics.SEQUENCER_CHECKPOINT_ATTESTATION_DELAY);
79
82
 
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
83
  this.rewards = this.meter.createGauge(Metrics.SEQUENCER_CURRENT_BLOCK_REWARDS);
89
84
 
90
- this.slots = this.meter.createUpDownCounter(Metrics.SEQUENCER_SLOT_COUNT);
85
+ this.slots = createUpDownCounterWithDefault(this.meter, Metrics.SEQUENCER_SLOT_COUNT);
91
86
 
92
87
  /**
93
88
  * NOTE: we do not track missed slots as a separate metric. That would be difficult to determine
94
89
  * Instead, use a computed metric, `slots - filledSlots` to get the number of slots a sequencer has missed.
95
90
  */
96
- this.filledSlots = this.meter.createUpDownCounter(Metrics.SEQUENCER_FILLED_SLOT_COUNT);
91
+ this.filledSlots = createUpDownCounterWithDefault(this.meter, Metrics.SEQUENCER_FILLED_SLOT_COUNT);
97
92
 
98
93
  this.timeToCollectAttestations = this.meter.createGauge(Metrics.SEQUENCER_COLLECT_ATTESTATIONS_DURATION);
99
94
 
@@ -103,20 +98,41 @@ export class SequencerMetrics {
103
98
 
104
99
  this.collectedAttestions = this.meter.createGauge(Metrics.SEQUENCER_COLLECTED_ATTESTATIONS_COUNT);
105
100
 
106
- this.blockProposalFailed = this.meter.createUpDownCounter(Metrics.SEQUENCER_BLOCK_PROPOSAL_FAILED_COUNT);
101
+ this.blockProposalFailed = createUpDownCounterWithDefault(
102
+ this.meter,
103
+ Metrics.SEQUENCER_BLOCK_PROPOSAL_FAILED_COUNT,
104
+ );
107
105
 
108
- this.blockProposalSuccess = this.meter.createUpDownCounter(Metrics.SEQUENCER_BLOCK_PROPOSAL_SUCCESS_COUNT);
106
+ this.blockProposalSuccess = createUpDownCounterWithDefault(
107
+ this.meter,
108
+ Metrics.SEQUENCER_BLOCK_PROPOSAL_SUCCESS_COUNT,
109
+ );
109
110
 
110
- this.checkpointSuccess = this.meter.createUpDownCounter(Metrics.SEQUENCER_CHECKPOINT_SUCCESS_COUNT);
111
+ this.checkpointSuccess = createUpDownCounterWithDefault(this.meter, Metrics.SEQUENCER_CHECKPOINT_SUCCESS_COUNT);
111
112
 
112
- this.blockProposalPrecheckFailed = this.meter.createUpDownCounter(
113
+ this.blockProposalPrecheckFailed = createUpDownCounterWithDefault(
114
+ this.meter,
113
115
  Metrics.SEQUENCER_BLOCK_PROPOSAL_PRECHECK_FAILED_COUNT,
116
+ {
117
+ [Attributes.ERROR_TYPE]: [
118
+ 'slot_already_taken',
119
+ 'rollup_contract_check_failed',
120
+ 'slot_mismatch',
121
+ 'block_number_mismatch',
122
+ ],
123
+ },
114
124
  );
115
125
 
116
- this.slashingAttempts = this.meter.createUpDownCounter(Metrics.SEQUENCER_SLASHING_ATTEMPTS_COUNT);
126
+ this.slashingAttempts = createUpDownCounterWithDefault(this.meter, Metrics.SEQUENCER_SLASHING_ATTEMPTS_COUNT);
117
127
 
118
128
  // Fisherman fee analysis metrics
119
- this.fishermanWouldBeIncluded = this.meter.createUpDownCounter(Metrics.FISHERMAN_FEE_ANALYSIS_WOULD_BE_INCLUDED);
129
+ this.fishermanWouldBeIncluded = createUpDownCounterWithDefault(
130
+ this.meter,
131
+ Metrics.FISHERMAN_FEE_ANALYSIS_WOULD_BE_INCLUDED,
132
+ {
133
+ [Attributes.OK]: [true, false],
134
+ },
135
+ );
120
136
 
121
137
  this.fishermanTimeBeforeBlock = this.meter.createHistogram(Metrics.FISHERMAN_FEE_ANALYSIS_TIME_BEFORE_BLOCK);
122
138
 
@@ -231,7 +247,9 @@ export class SequencerMetrics {
231
247
  this.blockProposalSuccess.add(1);
232
248
  }
233
249
 
234
- recordBlockProposalPrecheckFailed(checkType: string) {
250
+ recordBlockProposalPrecheckFailed(
251
+ checkType: 'slot_already_taken' | 'rollup_contract_check_failed' | 'slot_mismatch' | 'block_number_mismatch',
252
+ ) {
235
253
  this.blockProposalPrecheckFailed.add(1, {
236
254
  [Attributes.ERROR_TYPE]: checkType,
237
255
  });