@aztec/sequencer-client 4.0.0-devnet.2-patch.3 → 4.0.0-devnet.3-patch.0

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 (43) hide show
  1. package/dest/client/sequencer-client.d.ts +3 -1
  2. package/dest/client/sequencer-client.d.ts.map +1 -1
  3. package/dest/client/sequencer-client.js +38 -19
  4. package/dest/config.d.ts +24 -4
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +29 -16
  7. package/dest/global_variable_builder/global_builder.d.ts +13 -7
  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/sequencer-publisher.d.ts +15 -8
  13. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  14. package/dest/publisher/sequencer-publisher.js +65 -36
  15. package/dest/sequencer/checkpoint_proposal_job.d.ts +4 -4
  16. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  17. package/dest/sequencer/checkpoint_proposal_job.js +98 -68
  18. package/dest/sequencer/checkpoint_voter.d.ts +1 -2
  19. package/dest/sequencer/checkpoint_voter.d.ts.map +1 -1
  20. package/dest/sequencer/checkpoint_voter.js +2 -5
  21. package/dest/sequencer/sequencer.d.ts +12 -7
  22. package/dest/sequencer/sequencer.d.ts.map +1 -1
  23. package/dest/sequencer/sequencer.js +15 -17
  24. package/dest/sequencer/timetable.d.ts +4 -3
  25. package/dest/sequencer/timetable.d.ts.map +1 -1
  26. package/dest/sequencer/timetable.js +6 -7
  27. package/dest/sequencer/types.d.ts +2 -2
  28. package/dest/sequencer/types.d.ts.map +1 -1
  29. package/dest/test/mock_checkpoint_builder.d.ts +7 -9
  30. package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
  31. package/dest/test/mock_checkpoint_builder.js +41 -30
  32. package/package.json +28 -28
  33. package/src/client/sequencer-client.ts +48 -14
  34. package/src/config.ts +35 -19
  35. package/src/global_variable_builder/global_builder.ts +22 -23
  36. package/src/global_variable_builder/index.ts +1 -1
  37. package/src/publisher/sequencer-publisher.ts +61 -44
  38. package/src/sequencer/checkpoint_proposal_job.ts +156 -98
  39. package/src/sequencer/checkpoint_voter.ts +1 -12
  40. package/src/sequencer/sequencer.ts +16 -18
  41. package/src/sequencer/timetable.ts +7 -7
  42. package/src/sequencer/types.ts +1 -1
  43. package/src/test/mock_checkpoint_builder.ts +53 -48
@@ -1,5 +1,3 @@
1
- import { NUM_CHECKPOINT_END_MARKER_FIELDS, getNumBlockEndBlobFields } from '@aztec/blob-lib/encoding';
2
- import { BLOBS_PER_CHECKPOINT, FIELDS_PER_BLOB } from '@aztec/constants';
3
1
  import type { EpochCache } from '@aztec/epoch-cache';
4
2
  import {
5
3
  BlockNumber,
@@ -9,6 +7,11 @@ import {
9
7
  SlotNumber,
10
8
  } from '@aztec/foundation/branded-types';
11
9
  import { randomInt } from '@aztec/foundation/crypto/random';
10
+ import {
11
+ flipSignature,
12
+ generateRecoverableSignature,
13
+ generateUnrecoverableSignature,
14
+ } from '@aztec/foundation/crypto/secp256k1-signer';
12
15
  import { Fr } from '@aztec/foundation/curves/bn254';
13
16
  import { EthAddress } from '@aztec/foundation/eth-address';
14
17
  import { Signature } from '@aztec/foundation/eth-signature';
@@ -27,18 +30,23 @@ import {
27
30
  type L2BlockSource,
28
31
  MaliciousCommitteeAttestationsAndSigners,
29
32
  } from '@aztec/stdlib/block';
30
- import type { Checkpoint } from '@aztec/stdlib/checkpoint';
33
+ import { type Checkpoint, validateCheckpoint } from '@aztec/stdlib/checkpoint';
31
34
  import { getSlotStartBuildTimestamp } from '@aztec/stdlib/epoch-helpers';
32
35
  import { Gas } from '@aztec/stdlib/gas';
33
36
  import {
34
- NoValidTxsError,
35
- type PublicProcessorLimits,
37
+ type BlockBuilderOptions,
38
+ InsufficientValidTxsError,
36
39
  type ResolvedSequencerConfig,
37
40
  type WorldStateSynchronizer,
38
41
  } from '@aztec/stdlib/interfaces/server';
39
42
  import { type L1ToL2MessageSource, computeInHashFromL1ToL2Messages } from '@aztec/stdlib/messaging';
40
- import type { BlockProposalOptions, CheckpointProposal, CheckpointProposalOptions } from '@aztec/stdlib/p2p';
41
- import { orderAttestations } from '@aztec/stdlib/p2p';
43
+ import type {
44
+ BlockProposal,
45
+ BlockProposalOptions,
46
+ CheckpointProposal,
47
+ CheckpointProposalOptions,
48
+ } from '@aztec/stdlib/p2p';
49
+ import { orderAttestations, trimAttestations } from '@aztec/stdlib/p2p';
42
50
  import type { L2BlockBuiltStats } from '@aztec/stdlib/stats';
43
51
  import { type FailedTx, Tx } from '@aztec/stdlib/tx';
44
52
  import { AttestationTimeoutError } from '@aztec/stdlib/validators';
@@ -262,6 +270,23 @@ export class CheckpointProposalJob implements Traceable {
262
270
  this.setStateFn(SequencerState.ASSEMBLING_CHECKPOINT, this.slot);
263
271
  const checkpoint = await checkpointBuilder.completeCheckpoint();
264
272
 
273
+ // Final validation: per-block limits are only checked if the operator set them explicitly.
274
+ // Otherwise, checkpoint-level budgets were already enforced by the redistribution logic.
275
+ try {
276
+ validateCheckpoint(checkpoint, {
277
+ rollupManaLimit: this.l1Constants.rollupManaLimit,
278
+ maxL2BlockGas: this.config.maxL2BlockGas,
279
+ maxDABlockGas: this.config.maxDABlockGas,
280
+ maxTxsPerBlock: this.config.maxTxsPerBlock,
281
+ maxTxsPerCheckpoint: this.config.maxTxsPerCheckpoint,
282
+ });
283
+ } catch (err) {
284
+ this.log.error(`Built an invalid checkpoint at slot ${this.slot} (skipping proposal)`, err, {
285
+ checkpoint: checkpoint.header.toInspect(),
286
+ });
287
+ return undefined;
288
+ }
289
+
265
290
  // Record checkpoint-level build metrics
266
291
  this.metrics.recordCheckpointBuild(
267
292
  checkpointBuildTimer.ms(),
@@ -383,9 +408,7 @@ export class CheckpointProposalJob implements Traceable {
383
408
  const blocksInCheckpoint: L2Block[] = [];
384
409
  const txHashesAlreadyIncluded = new Set<string>();
385
410
  const initialBlockNumber = BlockNumber(this.syncedToBlockNumber + 1);
386
-
387
- // Remaining blob fields available for blocks (checkpoint end marker already subtracted)
388
- let remainingBlobFields = BLOBS_PER_CHECKPOINT * FIELDS_PER_BLOB - NUM_CHECKPOINT_END_MARKER_FIELDS;
411
+ const slot = this.slot;
389
412
 
390
413
  // Last block in the checkpoint will usually be flagged as pending broadcast, so we send it along with the checkpoint proposal
391
414
  let blockPendingBroadcast: { block: L2Block; txs: Tx[] } | undefined = undefined;
@@ -399,11 +422,7 @@ export class CheckpointProposalJob implements Traceable {
399
422
  const timingInfo = this.timetable.canStartNextBlock(secondsIntoSlot);
400
423
 
401
424
  if (!timingInfo.canStart) {
402
- this.log.debug(`Not enough time left in slot to start another block`, {
403
- slot: this.slot,
404
- blocksBuilt,
405
- secondsIntoSlot,
406
- });
425
+ this.log.debug(`Not enough time left in slot to start another block`, { slot, blocksBuilt, secondsIntoSlot });
407
426
  break;
408
427
  }
409
428
 
@@ -419,7 +438,6 @@ export class CheckpointProposalJob implements Traceable {
419
438
  blockNumber,
420
439
  indexWithinCheckpoint,
421
440
  txHashesAlreadyIncluded,
422
- remainingBlobFields,
423
441
  });
424
442
 
425
443
  // TODO(palla/mbps): Review these conditions. We may want to keep trying in some scenarios.
@@ -436,56 +454,37 @@ export class CheckpointProposalJob implements Traceable {
436
454
  } else if ('error' in buildResult) {
437
455
  // If there was an error building the block, just exit the loop and give up the rest of the slot
438
456
  if (!(buildResult.error instanceof SequencerInterruptedError)) {
439
- this.log.warn(`Halting block building for slot ${this.slot}`, {
440
- slot: this.slot,
441
- blocksBuilt,
442
- error: buildResult.error,
443
- });
457
+ this.log.warn(`Halting block building for slot ${slot}`, { slot, blocksBuilt, error: buildResult.error });
444
458
  }
445
459
  break;
446
460
  }
447
461
 
448
- const { block, usedTxs, remainingBlobFields: newRemainingBlobFields } = buildResult;
462
+ const { block, usedTxs } = buildResult;
449
463
  blocksInCheckpoint.push(block);
450
-
451
- // Update remaining blob fields for the next block
452
- remainingBlobFields = newRemainingBlobFields;
453
-
454
- // Sync the proposed block to the archiver to make it available
455
- // Note that the checkpoint builder uses its own fork so it should not need to wait for this syncing
456
- // Eventually we should refactor the checkpoint builder to not need a separate long-lived fork
457
- // Fire and forget - don't block the critical path, but log errors
458
- this.syncProposedBlockToArchiver(block).catch(err => {
459
- this.log.error(`Failed to sync proposed block ${block.number} to archiver`, { blockNumber: block.number, err });
460
- });
461
-
462
464
  usedTxs.forEach(tx => txHashesAlreadyIncluded.add(tx.txHash.toString()));
463
465
 
464
- // If this is the last block, exit the loop now so we start collecting attestations
466
+ // If this is the last block, send the proposed block to the archiver,
467
+ // and exit the loop now so we can build the checkpoint and start collecting attestations.
465
468
  if (timingInfo.isLastBlock) {
466
- this.log.verbose(`Completed final block ${blockNumber} for slot ${this.slot}`, {
467
- slot: this.slot,
468
- blockNumber,
469
- blocksBuilt,
470
- });
469
+ await this.syncProposedBlockToArchiver(block);
470
+ this.log.verbose(`Completed final block ${blockNumber} for slot ${slot}`, { slot, blockNumber, blocksBuilt });
471
471
  blockPendingBroadcast = { block, txs: usedTxs };
472
472
  break;
473
473
  }
474
474
 
475
- // For non-last blocks, broadcast the block proposal (unless we're in fisherman mode)
476
- // If the block is the last one, we'll broadcast it along with the checkpoint at the end of the loop
477
- if (!this.config.fishermanMode) {
478
- const proposal = await this.validatorClient.createBlockProposal(
479
- block.header,
480
- block.indexWithinCheckpoint,
481
- inHash,
482
- block.archive.root,
483
- usedTxs,
484
- this.proposer,
485
- blockProposalOptions,
486
- );
487
- await this.p2pClient.broadcastProposal(proposal);
488
- }
475
+ // Broadcast the block proposal (unless we're in fisherman mode) unless the block is the last one,
476
+ // in which case we'll broadcast it along with the checkpoint at the end of the loop.
477
+ // Note that we only send the block to the archiver if we manage to create the proposal, so if there's
478
+ // a HA error we don't pollute our archiver with a block that won't make it to the chain.
479
+ const proposal = await this.createBlockProposal(block, inHash, usedTxs, blockProposalOptions);
480
+
481
+ // Sync the proposed block to the archiver to make it available, only after we've managed to sign the proposal.
482
+ // We wait for the sync to succeed, as this helps catch consistency errors, even if it means we lose some time for block-building.
483
+ // If this throws, we abort the entire checkpoint.
484
+ await this.syncProposedBlockToArchiver(block);
485
+
486
+ // Once we have a signed proposal and the archiver agreed with our proposed block, then we broadcast it.
487
+ proposal && (await this.p2pClient.broadcastProposal(proposal));
489
488
 
490
489
  // Wait until the next block's start time
491
490
  await this.waitUntilNextSubslot(timingInfo.deadline);
@@ -499,6 +498,28 @@ export class CheckpointProposalJob implements Traceable {
499
498
  return { blocksInCheckpoint, blockPendingBroadcast };
500
499
  }
501
500
 
501
+ /** Creates a block proposal for a given block via the validator client (unless in fisherman mode) */
502
+ private createBlockProposal(
503
+ block: L2Block,
504
+ inHash: Fr,
505
+ usedTxs: Tx[],
506
+ blockProposalOptions: BlockProposalOptions,
507
+ ): Promise<BlockProposal | undefined> {
508
+ if (this.config.fishermanMode) {
509
+ this.log.info(`Skipping block proposal for block ${block.number} in fisherman mode`);
510
+ return Promise.resolve(undefined);
511
+ }
512
+ return this.validatorClient.createBlockProposal(
513
+ block.header,
514
+ block.indexWithinCheckpoint,
515
+ inHash,
516
+ block.archive.root,
517
+ usedTxs,
518
+ this.proposer,
519
+ blockProposalOptions,
520
+ );
521
+ }
522
+
502
523
  /** Sleeps until it is time to produce the next block in the slot */
503
524
  @trackSpan('CheckpointProposalJob.waitUntilNextSubslot')
504
525
  private async waitUntilNextSubslot(nextSubslotStart: number) {
@@ -518,18 +539,10 @@ export class CheckpointProposalJob implements Traceable {
518
539
  indexWithinCheckpoint: IndexWithinCheckpoint;
519
540
  buildDeadline: Date | undefined;
520
541
  txHashesAlreadyIncluded: Set<string>;
521
- remainingBlobFields: number;
522
542
  },
523
- ): Promise<{ block: L2Block; usedTxs: Tx[]; remainingBlobFields: number } | { error: Error } | undefined> {
524
- const {
525
- blockTimestamp,
526
- forceCreate,
527
- blockNumber,
528
- indexWithinCheckpoint,
529
- buildDeadline,
530
- txHashesAlreadyIncluded,
531
- remainingBlobFields,
532
- } = opts;
543
+ ): Promise<{ block: L2Block; usedTxs: Tx[] } | { error: Error } | undefined> {
544
+ const { blockTimestamp, forceCreate, blockNumber, indexWithinCheckpoint, buildDeadline, txHashesAlreadyIncluded } =
545
+ opts;
533
546
 
534
547
  this.log.verbose(
535
548
  `Preparing block ${blockNumber} index ${indexWithinCheckpoint} at checkpoint ${this.checkpointNumber} for slot ${this.slot}`,
@@ -538,8 +551,7 @@ export class CheckpointProposalJob implements Traceable {
538
551
 
539
552
  try {
540
553
  // Wait until we have enough txs to build the block
541
- const minTxs = this.config.minTxsPerBlock;
542
- const { availableTxs, canStartBuilding } = await this.waitForMinTxs(opts);
554
+ const { availableTxs, canStartBuilding, minTxs } = await this.waitForMinTxs(opts);
543
555
  if (!canStartBuilding) {
544
556
  this.log.warn(
545
557
  `Not enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.slot} (got ${availableTxs} txs but needs ${minTxs})`,
@@ -563,19 +575,26 @@ export class CheckpointProposalJob implements Traceable {
563
575
  );
564
576
  this.setStateFn(SequencerState.CREATING_BLOCK, this.slot);
565
577
 
566
- // Calculate blob fields limit for txs (remaining capacity - this block's end overhead)
567
- const blockEndOverhead = getNumBlockEndBlobFields(indexWithinCheckpoint === 0);
568
- const maxBlobFieldsForTxs = remainingBlobFields - blockEndOverhead;
569
-
570
- const blockBuilderOptions: PublicProcessorLimits = {
578
+ // Per-block limits are operator overrides (from SEQ_MAX_L2_BLOCK_GAS etc.) further capped
579
+ // by remaining checkpoint-level budgets inside CheckpointBuilder before each block is built.
580
+ // minValidTxs is passed into the builder so it can reject the block *before* updating state.
581
+ const minValidTxs = forceCreate ? 0 : (this.config.minValidTxsPerBlock ?? minTxs);
582
+ const blockBuilderOptions: BlockBuilderOptions = {
571
583
  maxTransactions: this.config.maxTxsPerBlock,
572
- maxBlockSize: this.config.maxBlockSizeInBytes,
573
- maxBlockGas: new Gas(this.config.maxDABlockGas, this.config.maxL2BlockGas),
574
- maxBlobFields: maxBlobFieldsForTxs,
584
+ maxBlockGas:
585
+ this.config.maxL2BlockGas !== undefined || this.config.maxDABlockGas !== undefined
586
+ ? new Gas(this.config.maxDABlockGas ?? Infinity, this.config.maxL2BlockGas ?? Infinity)
587
+ : undefined,
575
588
  deadline: buildDeadline,
589
+ isBuildingProposal: true,
590
+ minValidTxs,
591
+ maxBlocksPerCheckpoint: this.timetable.maxNumberOfBlocks,
592
+ perBlockAllocationMultiplier: this.config.perBlockAllocationMultiplier,
576
593
  };
577
594
 
578
- // Actually build the block by executing txs
595
+ // Actually build the block by executing txs. The builder throws InsufficientValidTxsError
596
+ // if the number of successfully processed txs is below minValidTxs, ensuring state is not
597
+ // updated for blocks that will be discarded.
579
598
  const buildResult = await this.buildSingleBlockWithCheckpointBuilder(
580
599
  checkpointBuilder,
581
600
  pendingTxs,
@@ -587,14 +606,16 @@ export class CheckpointProposalJob implements Traceable {
587
606
  // If any txs failed during execution, drop them from the mempool so we don't pick them up again
588
607
  await this.dropFailedTxsFromP2P(buildResult.failedTxs);
589
608
 
590
- // Check if we have created a block with enough txs. If there were invalid txs in the pool, or if execution took
591
- // too long, then we may not get to minTxsPerBlock after executing public functions.
592
- const minValidTxs = this.config.minValidTxsPerBlock ?? minTxs;
593
- const numTxs = buildResult.status === 'no-valid-txs' ? 0 : buildResult.numTxs;
594
- if (buildResult.status === 'no-valid-txs' || (!forceCreate && numTxs < minValidTxs)) {
609
+ if (buildResult.status === 'insufficient-valid-txs') {
595
610
  this.log.warn(
596
611
  `Block ${blockNumber} at index ${indexWithinCheckpoint} on slot ${this.slot} has too few valid txs to be proposed`,
597
- { slot: this.slot, blockNumber, numTxs, indexWithinCheckpoint, minValidTxs, buildResult: buildResult.status },
612
+ {
613
+ slot: this.slot,
614
+ blockNumber,
615
+ numTxs: buildResult.processedCount,
616
+ indexWithinCheckpoint,
617
+ minValidTxs,
618
+ },
598
619
  );
599
620
  this.eventEmitter.emit('block-build-failed', { reason: `Insufficient valid txs`, slot: this.slot });
600
621
  this.metrics.recordBlockProposalFailed('insufficient_valid_txs');
@@ -602,7 +623,7 @@ export class CheckpointProposalJob implements Traceable {
602
623
  }
603
624
 
604
625
  // Block creation succeeded, emit stats and metrics
605
- const { publicGas, block, publicProcessorDuration, usedTxs, usedTxBlobFields, blockBuildDuration } = buildResult;
626
+ const { block, publicProcessorDuration, usedTxs, blockBuildDuration, numTxs } = buildResult;
606
627
 
607
628
  const blockStats = {
608
629
  eventName: 'l2-block-built',
@@ -613,7 +634,7 @@ export class CheckpointProposalJob implements Traceable {
613
634
 
614
635
  const blockHash = await block.hash();
615
636
  const txHashes = block.body.txEffects.map(tx => tx.txHash);
616
- const manaPerSec = publicGas.l2Gas / (blockBuildDuration / 1000);
637
+ const manaPerSec = block.header.totalManaUsed.toNumberUnsafe() / (blockBuildDuration / 1000);
617
638
 
618
639
  this.log.info(
619
640
  `Built block ${block.number} at checkpoint ${this.checkpointNumber} for slot ${this.slot} with ${numTxs} txs`,
@@ -621,9 +642,9 @@ export class CheckpointProposalJob implements Traceable {
621
642
  );
622
643
 
623
644
  this.eventEmitter.emit('block-proposed', { blockNumber: block.number, slot: this.slot });
624
- this.metrics.recordBuiltBlock(blockBuildDuration, publicGas.l2Gas);
645
+ this.metrics.recordBuiltBlock(blockBuildDuration, block.header.totalManaUsed.toNumberUnsafe());
625
646
 
626
- return { block, usedTxs, remainingBlobFields: maxBlobFieldsForTxs - usedTxBlobFields };
647
+ return { block, usedTxs };
627
648
  } catch (err: any) {
628
649
  this.eventEmitter.emit('block-build-failed', { reason: err.message, slot: this.slot });
629
650
  this.log.error(`Error building block`, err, { blockNumber, slot: this.slot });
@@ -633,13 +654,13 @@ export class CheckpointProposalJob implements Traceable {
633
654
  }
634
655
  }
635
656
 
636
- /** Uses the checkpoint builder to build a block, catching specific txs */
657
+ /** Uses the checkpoint builder to build a block, catching InsufficientValidTxsError. */
637
658
  private async buildSingleBlockWithCheckpointBuilder(
638
659
  checkpointBuilder: CheckpointBuilder,
639
660
  pendingTxs: AsyncIterable<Tx>,
640
661
  blockNumber: BlockNumber,
641
662
  blockTimestamp: bigint,
642
- blockBuilderOptions: PublicProcessorLimits,
663
+ blockBuilderOptions: BlockBuilderOptions,
643
664
  ) {
644
665
  try {
645
666
  const workTimer = new Timer();
@@ -647,8 +668,12 @@ export class CheckpointProposalJob implements Traceable {
647
668
  const blockBuildDuration = workTimer.ms();
648
669
  return { ...result, blockBuildDuration, status: 'success' as const };
649
670
  } catch (err: unknown) {
650
- if (isErrorClass(err, NoValidTxsError)) {
651
- return { failedTxs: err.failedTxs, status: 'no-valid-txs' as const };
671
+ if (isErrorClass(err, InsufficientValidTxsError)) {
672
+ return {
673
+ failedTxs: err.failedTxs,
674
+ processedCount: err.processedCount,
675
+ status: 'insufficient-valid-txs' as const,
676
+ };
652
677
  }
653
678
  throw err;
654
679
  }
@@ -661,7 +686,7 @@ export class CheckpointProposalJob implements Traceable {
661
686
  blockNumber: BlockNumber;
662
687
  indexWithinCheckpoint: IndexWithinCheckpoint;
663
688
  buildDeadline: Date | undefined;
664
- }): Promise<{ canStartBuilding: boolean; availableTxs: number }> {
689
+ }): Promise<{ canStartBuilding: boolean; availableTxs: number; minTxs: number }> {
665
690
  const { indexWithinCheckpoint, blockNumber, buildDeadline, forceCreate } = opts;
666
691
 
667
692
  // We only allow a block with 0 txs in the first block of the checkpoint
@@ -678,7 +703,7 @@ export class CheckpointProposalJob implements Traceable {
678
703
  // If we're past deadline, or we have no deadline, give up
679
704
  const now = this.dateProvider.nowAsDate();
680
705
  if (startBuildingDeadline === undefined || now >= startBuildingDeadline) {
681
- return { canStartBuilding: false, availableTxs: availableTxs };
706
+ return { canStartBuilding: false, availableTxs, minTxs };
682
707
  }
683
708
 
684
709
  // Wait a bit before checking again
@@ -691,7 +716,7 @@ export class CheckpointProposalJob implements Traceable {
691
716
  availableTxs = await this.p2pClient.getPendingTxCount();
692
717
  }
693
718
 
694
- return { canStartBuilding: true, availableTxs };
719
+ return { canStartBuilding: true, availableTxs, minTxs };
695
720
  }
696
721
 
697
722
  /**
@@ -743,11 +768,28 @@ export class CheckpointProposalJob implements Traceable {
743
768
 
744
769
  collectedAttestationsCount = attestations.length;
745
770
 
771
+ // Trim attestations to minimum required to save L1 calldata gas
772
+ const localAddresses = this.validatorClient.getValidatorAddresses();
773
+ const trimmed = trimAttestations(
774
+ attestations,
775
+ numberOfRequiredAttestations,
776
+ this.attestorAddress,
777
+ localAddresses,
778
+ );
779
+ if (trimmed.length < attestations.length) {
780
+ this.log.debug(`Trimmed attestations from ${attestations.length} to ${trimmed.length} for L1 submission`);
781
+ }
782
+
746
783
  // Rollup contract requires that the signatures are provided in the order of the committee
747
- const sorted = orderAttestations(attestations, committee);
784
+ const sorted = orderAttestations(trimmed, committee);
748
785
 
749
786
  // Manipulate the attestations if we've been configured to do so
750
- if (this.config.injectFakeAttestation || this.config.shuffleAttestationOrdering) {
787
+ if (
788
+ this.config.injectFakeAttestation ||
789
+ this.config.injectHighSValueAttestation ||
790
+ this.config.injectUnrecoverableSignatureAttestation ||
791
+ this.config.shuffleAttestationOrdering
792
+ ) {
751
793
  return this.manipulateAttestations(proposal.slotNumber, epoch, seed, committee, sorted);
752
794
  }
753
795
 
@@ -776,7 +818,11 @@ export class CheckpointProposalJob implements Traceable {
776
818
  this.epochCache.computeProposerIndex(slotNumber, epoch, seed, BigInt(committee.length)),
777
819
  );
778
820
 
779
- if (this.config.injectFakeAttestation) {
821
+ if (
822
+ this.config.injectFakeAttestation ||
823
+ this.config.injectHighSValueAttestation ||
824
+ this.config.injectUnrecoverableSignatureAttestation
825
+ ) {
780
826
  // Find non-empty attestations that are not from the proposer
781
827
  const nonProposerIndices: number[] = [];
782
828
  for (let i = 0; i < attestations.length; i++) {
@@ -786,8 +832,20 @@ export class CheckpointProposalJob implements Traceable {
786
832
  }
787
833
  if (nonProposerIndices.length > 0) {
788
834
  const targetIndex = nonProposerIndices[randomInt(nonProposerIndices.length)];
789
- this.log.warn(`Injecting fake attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`);
790
- unfreeze(attestations[targetIndex]).signature = Signature.random();
835
+ if (this.config.injectHighSValueAttestation) {
836
+ this.log.warn(
837
+ `Injecting high-s value attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`,
838
+ );
839
+ unfreeze(attestations[targetIndex]).signature = flipSignature(attestations[targetIndex].signature);
840
+ } else if (this.config.injectUnrecoverableSignatureAttestation) {
841
+ this.log.warn(
842
+ `Injecting unrecoverable signature attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`,
843
+ );
844
+ unfreeze(attestations[targetIndex]).signature = generateUnrecoverableSignature();
845
+ } else {
846
+ this.log.warn(`Injecting fake attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`);
847
+ unfreeze(attestations[targetIndex]).signature = generateRecoverableSignature();
848
+ }
791
849
  }
792
850
  return new CommitteeAttestationsAndSigners(attestations);
793
851
  }
@@ -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`, {
@@ -14,7 +14,7 @@ import type { P2P } from '@aztec/p2p';
14
14
  import type { SlasherClientInterface } from '@aztec/slasher';
15
15
  import type { BlockData, L2BlockSink, L2BlockSource, ValidateCheckpointResult } from '@aztec/stdlib/block';
16
16
  import type { Checkpoint } 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,
@@ -110,7 +110,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
110
110
  /** Updates sequencer config by the defined values and updates the timetable */
111
111
  public updateConfig(config: Partial<SequencerConfig>) {
112
112
  const filteredConfig = pickFromSchema(config, SequencerConfigSchema);
113
- this.log.info(`Updated sequencer config`, omit(filteredConfig, 'txPublicSetupAllowList'));
113
+ this.log.info(`Updated sequencer config`, omit(filteredConfig, 'txPublicSetupAllowListExtend'));
114
114
  this.config = merge(this.config, filteredConfig);
115
115
  this.timetable = new SequencerTimetable(
116
116
  {
@@ -281,8 +281,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
281
281
 
282
282
  const logCtx = {
283
283
  now,
284
- syncedToL1Ts: syncedTo.l1Timestamp,
285
- syncedToL2Slot: getSlotAtTimestamp(syncedTo.l1Timestamp, this.l1Constants),
284
+ syncedToL2Slot: syncedTo.syncedL2Slot,
286
285
  slot,
287
286
  slotTs: ts,
288
287
  checkpointNumber,
@@ -328,7 +327,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
328
327
 
329
328
  // Check with the rollup contract if we can indeed propose at the next L2 slot. This check should not fail
330
329
  // if all the previous checks are good, but we do it just in case.
331
- const canProposeCheck = await publisher.canProposeAtNextEthBlock(
330
+ const canProposeCheck = await publisher.canProposeAt(
332
331
  syncedTo.archive,
333
332
  proposer ?? EthAddress.ZERO,
334
333
  invalidateCheckpoint,
@@ -475,16 +474,15 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
475
474
  * We don't check against the previous block submitted since it may have been reorg'd out.
476
475
  */
477
476
  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) {
477
+ // Check that the archiver has fully synced the L2 slot before the one we want to propose in.
478
+ // The archiver reports sync progress via L1 block timestamps and synced checkpoint slots.
479
+ // See getSyncedL2SlotNumber for how missed L1 blocks are handled.
480
+ const syncedL2Slot = await this.l2BlockSource.getSyncedL2SlotNumber();
481
+ const { slot } = args;
482
+ if (syncedL2Slot === undefined || syncedL2Slot + 1 < slot) {
484
483
  this.log.debug(`Cannot propose block at next L2 slot ${slot} due to pending sync from L1`, {
485
484
  slot,
486
- ts,
487
- l1Timestamp,
485
+ syncedL2Slot,
488
486
  });
489
487
  return undefined;
490
488
  }
@@ -524,7 +522,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
524
522
  checkpointNumber: CheckpointNumber.ZERO,
525
523
  blockNumber: BlockNumber.ZERO,
526
524
  archive,
527
- l1Timestamp,
525
+ syncedL2Slot,
528
526
  pendingChainValidationStatus,
529
527
  };
530
528
  }
@@ -541,7 +539,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
541
539
  blockNumber: blockData.header.getBlockNumber(),
542
540
  checkpointNumber: blockData.checkpointNumber,
543
541
  archive: blockData.archive.root,
544
- l1Timestamp,
542
+ syncedL2Slot,
545
543
  pendingChainValidationStatus,
546
544
  };
547
545
  }
@@ -720,7 +718,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
720
718
  syncedTo: SequencerSyncCheckResult,
721
719
  currentSlot: SlotNumber,
722
720
  ): Promise<void> {
723
- const { pendingChainValidationStatus, l1Timestamp } = syncedTo;
721
+ const { pendingChainValidationStatus, syncedL2Slot } = syncedTo;
724
722
  if (pendingChainValidationStatus.valid) {
725
723
  return;
726
724
  }
@@ -735,7 +733,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
735
733
 
736
734
  const logData = {
737
735
  invalidL1Timestamp: invalidCheckpointTimestamp,
738
- l1Timestamp,
736
+ syncedL2Slot,
739
737
  invalidCheckpoint: pendingChainValidationStatus.checkpoint,
740
738
  secondsBeforeInvalidatingBlockAsCommitteeMember,
741
739
  secondsBeforeInvalidatingBlockAsNonCommitteeMember,
@@ -882,6 +880,6 @@ type SequencerSyncCheckResult = {
882
880
  checkpointNumber: CheckpointNumber;
883
881
  blockNumber: BlockNumber;
884
882
  archive: Fr;
885
- l1Timestamp: bigint;
883
+ syncedL2Slot: SlotNumber;
886
884
  pendingChainValidationStatus: ValidateCheckpointResult;
887
885
  };
@@ -1,4 +1,4 @@
1
- import { createLogger } from '@aztec/aztec.js/log';
1
+ import type { Logger } from '@aztec/foundation/log';
2
2
  import {
3
3
  CHECKPOINT_ASSEMBLE_TIME,
4
4
  CHECKPOINT_INITIALIZATION_TIME,
@@ -80,7 +80,7 @@ export class SequencerTimetable {
80
80
  enforce: boolean;
81
81
  },
82
82
  private readonly metrics?: SequencerMetrics,
83
- private readonly log = createLogger('sequencer:timetable'),
83
+ private readonly log?: Logger,
84
84
  ) {
85
85
  this.ethereumSlotDuration = opts.ethereumSlotDuration;
86
86
  this.aztecSlotDuration = opts.aztecSlotDuration;
@@ -132,7 +132,7 @@ export class SequencerTimetable {
132
132
  const initializeDeadline = this.aztecSlotDuration - minWorkToDo;
133
133
  this.initializeDeadline = initializeDeadline;
134
134
 
135
- this.log.verbose(
135
+ this.log?.info(
136
136
  `Sequencer timetable initialized with ${this.maxNumberOfBlocks} blocks per slot (${this.enforce ? 'enforced' : 'not enforced'})`,
137
137
  {
138
138
  ethereumSlotDuration: this.ethereumSlotDuration,
@@ -206,7 +206,7 @@ export class SequencerTimetable {
206
206
  }
207
207
 
208
208
  this.metrics?.recordStateTransitionBufferMs(Math.floor(bufferSeconds * 1000), newState);
209
- this.log.trace(`Enough time to transition to ${newState}`, { maxAllowedTime, secondsIntoSlot });
209
+ this.log?.trace(`Enough time to transition to ${newState}`, { maxAllowedTime, secondsIntoSlot });
210
210
  }
211
211
 
212
212
  /**
@@ -242,7 +242,7 @@ export class SequencerTimetable {
242
242
  const canStart = available >= this.minExecutionTime;
243
243
  const deadline = secondsIntoSlot + available;
244
244
 
245
- this.log.verbose(
245
+ this.log?.verbose(
246
246
  `${canStart ? 'Can' : 'Cannot'} start single-block checkpoint at ${secondsIntoSlot}s into slot`,
247
247
  { secondsIntoSlot, maxAllowed, available, deadline },
248
248
  );
@@ -262,7 +262,7 @@ export class SequencerTimetable {
262
262
  // Found an available sub-slot! Is this the last one?
263
263
  const isLastBlock = subSlot === this.maxNumberOfBlocks;
264
264
 
265
- this.log.verbose(
265
+ this.log?.verbose(
266
266
  `Can start ${isLastBlock ? 'last block' : 'block'} in sub-slot ${subSlot} with deadline ${deadline}s`,
267
267
  { secondsIntoSlot, deadline, timeUntilDeadline, subSlot, maxBlocks: this.maxNumberOfBlocks },
268
268
  );
@@ -272,7 +272,7 @@ export class SequencerTimetable {
272
272
  }
273
273
 
274
274
  // No sub-slots available with enough time
275
- this.log.verbose(`No time left to start any more blocks`, {
275
+ this.log?.verbose(`No time left to start any more blocks`, {
276
276
  secondsIntoSlot,
277
277
  maxBlocks: this.maxNumberOfBlocks,
278
278
  initializationOffset: this.initializationOffset,
@@ -2,5 +2,5 @@ import type { L1RollupConstants } from '@aztec/stdlib/epoch-helpers';
2
2
 
3
3
  export type SequencerRollupConstants = Pick<
4
4
  L1RollupConstants,
5
- 'ethereumSlotDuration' | 'l1GenesisTime' | 'slotDuration'
5
+ 'ethereumSlotDuration' | 'l1GenesisTime' | 'slotDuration' | 'rollupManaLimit'
6
6
  >;