@aztec/sequencer-client 0.0.1-commit.ec5f612 → 0.0.1-commit.ec7ac5448

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 (63) hide show
  1. package/dest/client/sequencer-client.d.ts +6 -1
  2. package/dest/client/sequencer-client.d.ts.map +1 -1
  3. package/dest/client/sequencer-client.js +49 -27
  4. package/dest/config.d.ts +25 -5
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +31 -22
  7. package/dest/global_variable_builder/global_builder.d.ts +15 -8
  8. package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
  9. package/dest/global_variable_builder/global_builder.js +26 -26
  10. package/dest/global_variable_builder/index.d.ts +2 -2
  11. package/dest/global_variable_builder/index.d.ts.map +1 -1
  12. package/dest/publisher/config.d.ts +13 -1
  13. package/dest/publisher/config.d.ts.map +1 -1
  14. package/dest/publisher/config.js +17 -2
  15. package/dest/publisher/sequencer-publisher-factory.d.ts +3 -5
  16. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  17. package/dest/publisher/sequencer-publisher-factory.js +16 -3
  18. package/dest/publisher/sequencer-publisher.d.ts +55 -43
  19. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  20. package/dest/publisher/sequencer-publisher.js +180 -116
  21. package/dest/sequencer/chain_state_overrides.d.ts +25 -0
  22. package/dest/sequencer/chain_state_overrides.d.ts.map +1 -0
  23. package/dest/sequencer/chain_state_overrides.js +39 -0
  24. package/dest/sequencer/checkpoint_proposal_job.d.ts +24 -9
  25. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  26. package/dest/sequencer/checkpoint_proposal_job.js +331 -211
  27. package/dest/sequencer/checkpoint_voter.d.ts +1 -2
  28. package/dest/sequencer/checkpoint_voter.d.ts.map +1 -1
  29. package/dest/sequencer/checkpoint_voter.js +2 -5
  30. package/dest/sequencer/events.d.ts +2 -1
  31. package/dest/sequencer/events.d.ts.map +1 -1
  32. package/dest/sequencer/metrics.d.ts +9 -2
  33. package/dest/sequencer/metrics.d.ts.map +1 -1
  34. package/dest/sequencer/metrics.js +23 -1
  35. package/dest/sequencer/sequencer.d.ts +28 -11
  36. package/dest/sequencer/sequencer.d.ts.map +1 -1
  37. package/dest/sequencer/sequencer.js +134 -74
  38. package/dest/sequencer/timetable.d.ts +17 -3
  39. package/dest/sequencer/timetable.d.ts.map +1 -1
  40. package/dest/sequencer/timetable.js +51 -43
  41. package/dest/sequencer/types.d.ts +2 -2
  42. package/dest/sequencer/types.d.ts.map +1 -1
  43. package/dest/test/mock_checkpoint_builder.d.ts +7 -9
  44. package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
  45. package/dest/test/mock_checkpoint_builder.js +39 -30
  46. package/package.json +27 -28
  47. package/src/client/sequencer-client.ts +61 -28
  48. package/src/config.ts +39 -24
  49. package/src/global_variable_builder/global_builder.ts +30 -27
  50. package/src/global_variable_builder/index.ts +1 -1
  51. package/src/publisher/config.ts +32 -0
  52. package/src/publisher/sequencer-publisher-factory.ts +18 -6
  53. package/src/publisher/sequencer-publisher.ts +263 -167
  54. package/src/sequencer/README.md +83 -13
  55. package/src/sequencer/chain_state_overrides.ts +87 -0
  56. package/src/sequencer/checkpoint_proposal_job.ts +429 -239
  57. package/src/sequencer/checkpoint_voter.ts +1 -12
  58. package/src/sequencer/events.ts +1 -1
  59. package/src/sequencer/metrics.ts +29 -1
  60. package/src/sequencer/sequencer.ts +194 -81
  61. package/src/sequencer/timetable.ts +64 -52
  62. package/src/sequencer/types.ts +1 -1
  63. package/src/test/mock_checkpoint_builder.ts +51 -48
@@ -436,25 +436,25 @@ function _apply_decs_2203_r(targetClass, memberDecs, classDecs, parentClass) {
436
436
  return (_apply_decs_2203_r = applyDecs2203RFactory())(targetClass, memberDecs, classDecs, parentClass);
437
437
  }
438
438
  var _dec, _dec1, _dec2, _dec3, _dec4, _dec5, _dec6, _dec7, _initProto;
439
- import { NUM_CHECKPOINT_END_MARKER_FIELDS, getNumBlockEndBlobFields } from '@aztec/blob-lib/encoding';
440
- import { BLOBS_PER_CHECKPOINT, FIELDS_PER_BLOB } from '@aztec/constants';
441
439
  import { BlockNumber, IndexWithinCheckpoint } from '@aztec/foundation/branded-types';
442
440
  import { randomInt } from '@aztec/foundation/crypto/random';
443
- import { Signature } from '@aztec/foundation/eth-signature';
441
+ import { flipSignature, generateRecoverableSignature, generateUnrecoverableSignature } from '@aztec/foundation/crypto/secp256k1-signer';
444
442
  import { filter } from '@aztec/foundation/iterator';
445
443
  import { createLogger } from '@aztec/foundation/log';
446
444
  import { sleep, sleepUntil } from '@aztec/foundation/sleep';
447
445
  import { Timer } from '@aztec/foundation/timer';
448
446
  import { isErrorClass, unfreeze } from '@aztec/foundation/types';
449
447
  import { CommitteeAttestationsAndSigners, MaliciousCommitteeAttestationsAndSigners } from '@aztec/stdlib/block';
450
- import { getSlotStartBuildTimestamp } from '@aztec/stdlib/epoch-helpers';
448
+ import { validateCheckpoint } from '@aztec/stdlib/checkpoint';
449
+ import { computeQuorum, getSlotStartBuildTimestamp, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
451
450
  import { Gas } from '@aztec/stdlib/gas';
452
- import { NoValidTxsError } from '@aztec/stdlib/interfaces/server';
451
+ import { InsufficientValidTxsError } from '@aztec/stdlib/interfaces/server';
453
452
  import { computeInHashFromL1ToL2Messages } from '@aztec/stdlib/messaging';
454
453
  import { orderAttestations, trimAttestations } from '@aztec/stdlib/p2p';
455
454
  import { AttestationTimeoutError } from '@aztec/stdlib/validators';
456
455
  import { Attributes, trackSpan } from '@aztec/telemetry-client';
457
456
  import { DutyAlreadySignedError, SlashingProtectionError } from '@aztec/validator-ha-signer/errors';
457
+ import { buildPipelinedParentSimulationOverridesPlan, buildSubmissionSimulationOverridesPlan } from './chain_state_overrides.js';
458
458
  import { CheckpointVoter } from './checkpoint_voter.js';
459
459
  import { SequencerInterruptedError } from './errors.js';
460
460
  import { SequencerState } from './utils.js';
@@ -463,7 +463,7 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
463
463
  return {
464
464
  // nullish operator needed for tests
465
465
  [Attributes.COINBASE]: this.validatorClient.getCoinbaseForAttestor(this.attestorAddress)?.toString(),
466
- [Attributes.SLOT_NUMBER]: this.slot
466
+ [Attributes.SLOT_NUMBER]: this.targetSlot
467
467
  };
468
468
  }), _dec2 = trackSpan('CheckpointProposalJob.buildBlocksForCheckpoint'), _dec3 = trackSpan('CheckpointProposalJob.waitUntilNextSubslot'), _dec4 = trackSpan('CheckpointProposalJob.buildSingleBlock'), _dec5 = trackSpan('CheckpointProposalJob.waitForMinTxs'), _dec6 = trackSpan('CheckpointProposalJob.waitForAttestations'), _dec7 = trackSpan('CheckpointProposalJob.waitUntilTimeInSlot');
469
469
  /**
@@ -472,8 +472,9 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
472
472
  * as well as enqueueing votes for slashing and governance proposals. This class is created from
473
473
  * the Sequencer once the check for being the proposer for the slot has succeeded.
474
474
  */ export class CheckpointProposalJob {
475
- epoch;
476
- slot;
475
+ slotNow;
476
+ targetSlot;
477
+ targetEpoch;
477
478
  checkpointNumber;
478
479
  syncedToBlockNumber;
479
480
  proposer;
@@ -498,6 +499,7 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
498
499
  eventEmitter;
499
500
  setStateFn;
500
501
  tracer;
502
+ proposedCheckpointData;
501
503
  static{
502
504
  ({ e: [_initProto] } = _apply_decs_2203_r(this, [
503
505
  [
@@ -543,10 +545,13 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
543
545
  ], []));
544
546
  }
545
547
  log;
546
- constructor(epoch, slot, checkpointNumber, syncedToBlockNumber, // TODO(palla/mbps): Can we remove the proposer in favor of attestorAddress? Need to check fisherman-node flows.
547
- proposer, publisher, attestorAddress, invalidateCheckpoint, validatorClient, globalsBuilder, p2pClient, worldState, l1ToL2MessageSource, l2BlockSource, checkpointsBuilder, blockSink, l1Constants, config, timetable, slasherClient, epochCache, dateProvider, metrics, eventEmitter, setStateFn, tracer, bindings){
548
- this.epoch = epoch;
549
- this.slot = slot;
548
+ /** Tracks the fire-and-forget L1 submission promise so it can be awaited during shutdown. */ pendingL1Submission;
549
+ /** Pipelined parent chain state used while building and later submitting this checkpoint. */ pipelinedParentSimulationOverridesPlan;
550
+ constructor(slotNow, targetSlot, targetEpoch, checkpointNumber, syncedToBlockNumber, // TODO(palla/mbps): Can we remove the proposer in favor of attestorAddress? Need to check fisherman-node flows.
551
+ proposer, publisher, attestorAddress, invalidateCheckpoint, validatorClient, globalsBuilder, p2pClient, worldState, l1ToL2MessageSource, l2BlockSource, checkpointsBuilder, blockSink, l1Constants, config, timetable, slasherClient, epochCache, dateProvider, metrics, eventEmitter, setStateFn, tracer, bindings, proposedCheckpointData){
552
+ this.slotNow = slotNow;
553
+ this.targetSlot = targetSlot;
554
+ this.targetEpoch = targetEpoch;
550
555
  this.checkpointNumber = checkpointNumber;
551
556
  this.syncedToBlockNumber = syncedToBlockNumber;
552
557
  this.proposer = proposer;
@@ -571,51 +576,139 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
571
576
  this.eventEmitter = eventEmitter;
572
577
  this.setStateFn = setStateFn;
573
578
  this.tracer = tracer;
579
+ this.proposedCheckpointData = proposedCheckpointData;
574
580
  _initProto(this);
575
581
  this.log = createLogger('sequencer:checkpoint-proposal', {
576
582
  ...bindings,
577
- instanceId: `slot-${slot}`
583
+ instanceId: `slot-${this.slotNow}`
578
584
  });
579
585
  }
586
+ /** Awaits the pending L1 submission if one is in progress. Call during shutdown. */ async awaitPendingSubmission() {
587
+ this.log.info('Awaiting pending L1 payload submission');
588
+ await this.pendingL1Submission;
589
+ }
580
590
  /**
581
591
  * Executes the checkpoint proposal job.
582
- * Returns the published checkpoint if successful, undefined otherwise.
592
+ * Builds blocks, assembles checkpoint, and broadcasts the proposal (blocking).
593
+ * Attestation collection, signing, and L1 submission are backgrounded so the
594
+ * work loop can return to IDLE immediately for consecutive slot proposals.
595
+ * Returns the built checkpoint if successful, undefined otherwise.
583
596
  */ async execute() {
584
597
  // Enqueue governance and slashing votes (returns promises that will be awaited later)
585
598
  // In fisherman mode, we simulate slashing but don't actually publish to L1
586
599
  // These are constant for the whole slot, so we only enqueue them once
587
- const votesPromises = new CheckpointVoter(this.slot, this.publisher, this.attestorAddress, this.validatorClient, this.slasherClient, this.l1Constants, this.config, this.metrics, this.log).enqueueVotes();
588
- // Build and propose the checkpoint. This will enqueue the request on the publisher if a checkpoint is built.
589
- const checkpoint = await this.proposeCheckpoint();
590
- // Wait until the voting promises have resolved, so all requests are enqueued (not sent)
591
- await Promise.all(votesPromises);
592
- if (checkpoint) {
593
- this.metrics.recordCheckpointProposalSuccess();
600
+ const votesPromises = new CheckpointVoter(this.targetSlot, this.publisher, this.attestorAddress, this.validatorClient, this.slasherClient, this.l1Constants, this.config, this.metrics, this.log).enqueueVotes();
601
+ // Build blocks, assemble checkpoint, and broadcast proposal (BLOCKING).
602
+ // Returns after broadcast — attestation collection is deferred.
603
+ const broadcast = await this.proposeCheckpoint();
604
+ if (!broadcast) {
605
+ await Promise.all(votesPromises);
606
+ // Still submit votes even without a checkpoint
607
+ if (!this.config.fishermanMode) {
608
+ this.pendingL1Submission = this.publisher.sendRequestsAt(this.dateProvider.nowAsDate()).then(()=>{});
609
+ }
610
+ return undefined;
594
611
  }
612
+ const { checkpoint } = broadcast;
613
+ this.metrics.recordCheckpointProposalSuccess();
595
614
  // Do not post anything to L1 if we are fishermen, but do perform L1 fee analysis
596
615
  if (this.config.fishermanMode) {
597
616
  await this.handleCheckpointEndAsFisherman(checkpoint);
598
- return;
617
+ return checkpoint;
599
618
  }
600
- // Then send everything to L1
601
- const l1Response = await this.publisher.sendRequests();
602
- const proposedAction = l1Response?.successfulActions.find((a)=>a === 'propose');
603
- if (proposedAction) {
604
- this.eventEmitter.emit('checkpoint-published', {
605
- checkpoint: this.checkpointNumber,
606
- slot: this.slot
619
+ // Background the attestation signing → L1 pipeline so the work loop is unblocked
620
+ this.pendingL1Submission = this.waitForAttestationsAndEnqueueSubmissionAsync(broadcast, votesPromises);
621
+ // Return the built checkpoint immediately — the work loop is now unblocked
622
+ return checkpoint;
623
+ }
624
+ /**
625
+ * Background pipeline: collects attestations, signs them, enqueues the checkpoint, and submits to L1.
626
+ * Runs as a fire-and-forget task stored in `pendingL1Submission` so the work loop is unblocked.
627
+ */ async waitForAttestationsAndEnqueueSubmissionAsync(broadcast, votesPromises) {
628
+ const { checkpoint, proposal, blockProposedAt } = broadcast;
629
+ try {
630
+ await Promise.all(votesPromises);
631
+ this.setStateFn(SequencerState.COLLECTING_ATTESTATIONS, this.targetSlot);
632
+ const attestations = await this.waitForAttestations(proposal);
633
+ this.metrics.recordCheckpointAttestationDelay(this.dateProvider.now() - blockProposedAt);
634
+ // Proposer must sign over the attestations before pushing them to L1
635
+ const signer = this.proposer ?? this.publisher.getSenderAddress();
636
+ let attestationsSignature;
637
+ try {
638
+ attestationsSignature = await this.validatorClient.signAttestationsAndSigners(attestations, signer, this.targetSlot, this.checkpointNumber);
639
+ } catch (err) {
640
+ if (this.handleHASigningError(err, 'Attestations signature')) {
641
+ return;
642
+ }
643
+ throw err;
644
+ }
645
+ // Enqueue the checkpoint for L1 submission
646
+ await this.enqueueCheckpointForSubmission({
647
+ checkpoint,
648
+ attestations,
649
+ attestationsSignature
607
650
  });
608
- const coinbase = checkpoint?.header.coinbase;
609
- await this.metrics.incFilledSlot(this.publisher.getSenderAddress().toString(), coinbase);
610
- return checkpoint;
611
- } else if (checkpoint) {
651
+ // Compute the earliest time to submit: pipeline slot start when pipelining, now otherwise.
652
+ const submitAfter = this.epochCache.isProposerPipeliningEnabled() ? new Date(Number(getTimestampForSlot(this.targetSlot, this.l1Constants)) * 1000) : new Date(this.dateProvider.now());
653
+ const l1Response = await this.publisher.sendRequestsAt(submitAfter);
654
+ const proposedAction = l1Response?.successfulActions.find((a)=>a === 'propose');
655
+ if (proposedAction) {
656
+ this.eventEmitter.emit('checkpoint-published', {
657
+ checkpoint: this.checkpointNumber,
658
+ slot: this.targetSlot
659
+ });
660
+ const coinbase = checkpoint.header.coinbase;
661
+ await this.metrics.incFilledSlot(this.publisher.getSenderAddress().toString(), coinbase);
662
+ } else {
663
+ this.eventEmitter.emit('checkpoint-publish-failed', {
664
+ ...l1Response,
665
+ slot: this.targetSlot
666
+ });
667
+ if (this.epochCache.isProposerPipeliningEnabled()) {
668
+ this.metrics.recordPipelineDiscard();
669
+ }
670
+ }
671
+ } catch (err) {
672
+ if (err instanceof SequencerInterruptedError) {
673
+ return;
674
+ }
675
+ this.log.error(`Background attestation/L1 pipeline failed for slot ${this.targetSlot}`, err);
612
676
  this.eventEmitter.emit('checkpoint-publish-failed', {
613
- ...l1Response,
614
- slot: this.slot
677
+ slot: this.targetSlot
615
678
  });
616
- return undefined;
679
+ if (this.epochCache.isProposerPipeliningEnabled()) {
680
+ this.metrics.recordPipelineDiscard();
681
+ }
617
682
  }
618
683
  }
684
+ /** Enqueues the checkpoint for L1 submission. Called after pipeline sleep in execute(). */ async enqueueCheckpointForSubmission(result) {
685
+ const { checkpoint, attestations, attestationsSignature } = result;
686
+ this.setStateFn(SequencerState.PUBLISHING_CHECKPOINT, this.targetSlot);
687
+ const aztecSlotDuration = this.l1Constants.slotDuration;
688
+ const submissionSlotStart = Number(getTimestampForSlot(this.targetSlot, this.l1Constants));
689
+ const txTimeoutAt = new Date((submissionSlotStart + aztecSlotDuration) * 1000);
690
+ // If we have been configured to potentially skip publishing checkpoint then roll the dice here
691
+ if (this.config.skipPublishingCheckpointsPercent !== undefined && this.config.skipPublishingCheckpointsPercent > 0) {
692
+ const roll = Math.max(0, randomInt(100));
693
+ if (roll < this.config.skipPublishingCheckpointsPercent) {
694
+ this.log.warn(`Skipping publishing proposal for checkpoint ${checkpoint.number}. Configured percentage: ${this.config.skipPublishingCheckpointsPercent}, generated value: ${roll}`);
695
+ return;
696
+ }
697
+ }
698
+ const isPipelining = this.epochCache.isProposerPipeliningEnabled();
699
+ const submissionSimulationOverridesPlan = buildSubmissionSimulationOverridesPlan({
700
+ pipelinedParentPlan: this.pipelinedParentSimulationOverridesPlan,
701
+ invalidateToPendingCheckpointNumber: this.invalidateCheckpoint?.forcePendingCheckpointNumber,
702
+ lastArchiveRoot: checkpoint.header.lastArchiveRoot,
703
+ pipeliningEnabled: isPipelining
704
+ });
705
+ await this.publisher.enqueueProposeCheckpoint(checkpoint, attestations, attestationsSignature, {
706
+ txTimeoutAt,
707
+ ...submissionSimulationOverridesPlan ? {
708
+ simulationOverridesPlan: submissionSimulationOverridesPlan
709
+ } : {}
710
+ });
711
+ }
619
712
  async proposeCheckpoint() {
620
713
  try {
621
714
  const env = {
@@ -628,19 +721,35 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
628
721
  const coinbase = this.validatorClient.getCoinbaseForAttestor(this.attestorAddress);
629
722
  const feeRecipient = this.validatorClient.getFeeRecipientForAttestor(this.attestorAddress);
630
723
  // Start the checkpoint
631
- this.setStateFn(SequencerState.INITIALIZING_CHECKPOINT, this.slot);
632
- this.metrics.incOpenSlot(this.slot, this.proposer?.toString() ?? 'unknown');
724
+ this.setStateFn(SequencerState.INITIALIZING_CHECKPOINT, this.targetSlot);
725
+ this.log.info(`Starting checkpoint proposal`, {
726
+ buildSlot: this.slotNow,
727
+ submissionSlot: this.targetSlot,
728
+ pipelining: this.epochCache.isProposerPipeliningEnabled(),
729
+ proposer: this.proposer?.toString(),
730
+ coinbase: coinbase.toString()
731
+ });
732
+ this.metrics.incOpenSlot(this.targetSlot, this.proposer?.toString() ?? 'unknown');
633
733
  // Enqueues checkpoint invalidation (constant for the whole slot)
634
734
  if (this.invalidateCheckpoint && !this.config.skipInvalidateBlockAsProposer) {
635
735
  this.publisher.enqueueInvalidateCheckpoint(this.invalidateCheckpoint);
636
736
  }
637
- // Create checkpoint builder for the slot
638
- const checkpointGlobalVariables = await this.globalsBuilder.buildCheckpointGlobalVariables(coinbase, feeRecipient, this.slot);
737
+ // Create checkpoint builder for the slot.
738
+ // When pipelining, force the proposed checkpoint number and fee header to our parent so the
739
+ // fee computation sees the same chain tip that L1 will see once the previous pipelined checkpoint lands.
740
+ const isPipelining = this.epochCache.isProposerPipeliningEnabled();
741
+ this.pipelinedParentSimulationOverridesPlan = isPipelining ? await buildPipelinedParentSimulationOverridesPlan({
742
+ checkpointNumber: this.checkpointNumber,
743
+ proposedCheckpointData: this.proposedCheckpointData,
744
+ rollup: this.publisher.rollupContract,
745
+ log: this.log
746
+ }) : undefined;
747
+ const checkpointGlobalVariables = await this.globalsBuilder.buildCheckpointGlobalVariables(coinbase, feeRecipient, this.targetSlot, this.pipelinedParentSimulationOverridesPlan);
639
748
  // Collect L1 to L2 messages for the checkpoint and compute their hash
640
749
  const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(this.checkpointNumber);
641
750
  const inHash = computeInHashFromL1ToL2Messages(l1ToL2Messages);
642
751
  // Collect the out hashes of all the checkpoints before this one in the same epoch
643
- const previousCheckpointOutHashes = (await this.l2BlockSource.getCheckpointsDataForEpoch(this.epoch)).filter((c)=>c.checkpointNumber < this.checkpointNumber).map((c)=>c.checkpointOutHash);
752
+ const previousCheckpointOutHashes = (await this.l2BlockSource.getCheckpointsDataForEpoch(this.targetEpoch)).filter((c)=>c.checkpointNumber < this.checkpointNumber).map((c)=>c.checkpointOutHash);
644
753
  // Get the fee asset price modifier from the oracle
645
754
  const feeAssetPriceModifier = await this.publisher.getFeeAssetPriceModifier();
646
755
  const fork = _ts_add_disposable_resource(env, await this.worldState.fork(this.syncedToBlockNumber, {
@@ -675,18 +784,18 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
675
784
  throw err;
676
785
  }
677
786
  if (blocksInCheckpoint.length === 0) {
678
- this.log.warn(`No blocks were built for slot ${this.slot}`, {
679
- slot: this.slot
787
+ this.log.warn(`No blocks were built for slot ${this.targetSlot}`, {
788
+ slot: this.targetSlot
680
789
  });
681
790
  this.eventEmitter.emit('checkpoint-empty', {
682
- slot: this.slot
791
+ slot: this.targetSlot
683
792
  });
684
793
  return undefined;
685
794
  }
686
795
  const minBlocksForCheckpoint = this.config.minBlocksForCheckpoint;
687
796
  if (minBlocksForCheckpoint !== undefined && blocksInCheckpoint.length < minBlocksForCheckpoint) {
688
797
  this.log.warn(`Checkpoint has fewer blocks than minimum (${blocksInCheckpoint.length} < ${minBlocksForCheckpoint}), skipping proposal`, {
689
- slot: this.slot,
798
+ slot: this.targetSlot,
690
799
  blocksBuilt: blocksInCheckpoint.length,
691
800
  minBlocksForCheckpoint
692
801
  });
@@ -694,65 +803,51 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
694
803
  }
695
804
  // Assemble and broadcast the checkpoint proposal, including the last block that was not
696
805
  // broadcasted yet, and wait to collect the committee attestations.
697
- this.setStateFn(SequencerState.ASSEMBLING_CHECKPOINT, this.slot);
806
+ this.setStateFn(SequencerState.ASSEMBLING_CHECKPOINT, this.targetSlot);
698
807
  const checkpoint = await checkpointBuilder.completeCheckpoint();
808
+ // Final validation: per-block limits are only checked if the operator set them explicitly.
809
+ // Otherwise, checkpoint-level budgets were already enforced by the redistribution logic.
810
+ try {
811
+ validateCheckpoint(checkpoint, {
812
+ rollupManaLimit: this.l1Constants.rollupManaLimit,
813
+ maxL2BlockGas: this.config.maxL2BlockGas,
814
+ maxDABlockGas: this.config.maxDABlockGas,
815
+ maxTxsPerBlock: this.config.maxTxsPerBlock,
816
+ maxTxsPerCheckpoint: this.config.maxTxsPerCheckpoint
817
+ });
818
+ } catch (err) {
819
+ this.log.error(`Built an invalid checkpoint at slot ${this.slotNow} (skipping proposal)`, err, {
820
+ checkpoint: checkpoint.header.toInspect()
821
+ });
822
+ return undefined;
823
+ }
699
824
  // Record checkpoint-level build metrics
700
825
  this.metrics.recordCheckpointBuild(checkpointBuildTimer.ms(), blocksInCheckpoint.length, checkpoint.getStats().txCount, Number(checkpoint.header.totalManaUsed.toBigInt()));
701
- // Do not collect attestations nor publish to L1 in fisherman mode
826
+ // In fisherman mode, return the checkpoint without broadcasting or collecting attestations
702
827
  if (this.config.fishermanMode) {
703
- this.log.info(`Built checkpoint for slot ${this.slot} with ${blocksInCheckpoint.length} blocks. ` + `Skipping proposal in fisherman mode.`, {
704
- slot: this.slot,
828
+ this.log.info(`Built checkpoint for slot ${this.targetSlot} with ${blocksInCheckpoint.length} blocks. ` + `Skipping proposal in fisherman mode.`, {
829
+ slot: this.targetSlot,
705
830
  checkpoint: checkpoint.header.toInspect(),
706
831
  blocksBuilt: blocksInCheckpoint.length
707
832
  });
708
833
  this.metrics.recordCheckpointSuccess();
709
- return checkpoint;
834
+ // Return a broadcast result with a dummy proposal — fisherman mode skips attestation collection
835
+ return {
836
+ checkpoint,
837
+ proposal: undefined,
838
+ blockProposedAt: this.dateProvider.now()
839
+ };
710
840
  }
711
- // Include the block pending broadcast in the checkpoint proposal if any
712
- const lastBlock = blockPendingBroadcast && {
713
- blockHeader: blockPendingBroadcast.block.header,
714
- indexWithinCheckpoint: blockPendingBroadcast.block.indexWithinCheckpoint,
715
- txs: blockPendingBroadcast.txs
716
- };
717
841
  // Create the checkpoint proposal and broadcast it
718
- const proposal = await this.validatorClient.createCheckpointProposal(checkpoint.header, checkpoint.archive.root, feeAssetPriceModifier, lastBlock, this.proposer, checkpointProposalOptions);
842
+ const proposal = await this.validatorClient.createCheckpointProposal(checkpoint.header, checkpoint.archive.root, feeAssetPriceModifier, blockPendingBroadcast, this.proposer, checkpointProposalOptions);
719
843
  const blockProposedAt = this.dateProvider.now();
720
844
  await this.p2pClient.broadcastCheckpointProposal(proposal);
721
- this.setStateFn(SequencerState.COLLECTING_ATTESTATIONS, this.slot);
722
- const attestations = await this.waitForAttestations(proposal);
723
- const blockAttestedAt = this.dateProvider.now();
724
- this.metrics.recordCheckpointAttestationDelay(blockAttestedAt - blockProposedAt);
725
- // Proposer must sign over the attestations before pushing them to L1
726
- const signer = this.proposer ?? this.publisher.getSenderAddress();
727
- let attestationsSignature;
728
- try {
729
- attestationsSignature = await this.validatorClient.signAttestationsAndSigners(attestations, signer, this.slot, this.checkpointNumber);
730
- } catch (err) {
731
- // We shouldn't really get here since we yield to another HA node
732
- // as soon as we see these errors when creating block or checkpoint proposals.
733
- if (this.handleHASigningError(err, 'Attestations signature')) {
734
- return undefined;
735
- }
736
- throw err;
737
- }
738
- // Enqueue publishing the checkpoint to L1
739
- this.setStateFn(SequencerState.PUBLISHING_CHECKPOINT, this.slot);
740
- const aztecSlotDuration = this.l1Constants.slotDuration;
741
- const slotStartBuildTimestamp = this.getSlotStartBuildTimestamp();
742
- const txTimeoutAt = new Date((slotStartBuildTimestamp + aztecSlotDuration) * 1000);
743
- // If we have been configured to potentially skip publishing checkpoint then roll the dice here
744
- if (this.config.skipPublishingCheckpointsPercent !== undefined && this.config.skipPublishingCheckpointsPercent > 0) {
745
- const result = Math.max(0, randomInt(100));
746
- if (result < this.config.skipPublishingCheckpointsPercent) {
747
- this.log.warn(`Skipping publishing proposal for checkpoint ${checkpoint.number}. Configured percentage: ${this.config.skipPublishingCheckpointsPercent}, generated value: ${result}`);
748
- return checkpoint;
749
- }
750
- }
751
- await this.publisher.enqueueProposeCheckpoint(checkpoint, attestations, attestationsSignature, {
752
- txTimeoutAt,
753
- forcePendingCheckpointNumber: this.invalidateCheckpoint?.forcePendingCheckpointNumber
754
- });
755
- return checkpoint;
845
+ // Return immediately after broadcast — attestation collection happens in the background
846
+ return {
847
+ checkpoint,
848
+ proposal,
849
+ blockProposedAt
850
+ };
756
851
  } catch (e) {
757
852
  env.error = e;
758
853
  env.hasError = true;
@@ -765,7 +860,7 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
765
860
  // swallow this error. It's already been logged by a function deeper in the stack
766
861
  return undefined;
767
862
  }
768
- this.log.error(`Error building checkpoint at slot ${this.slot}`, err);
863
+ this.log.error(`Error building checkpoint at slot ${this.targetSlot}`, err);
769
864
  return undefined;
770
865
  }
771
866
  }
@@ -775,8 +870,6 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
775
870
  const blocksInCheckpoint = [];
776
871
  const txHashesAlreadyIncluded = new Set();
777
872
  const initialBlockNumber = BlockNumber(this.syncedToBlockNumber + 1);
778
- // Remaining blob fields available for blocks (checkpoint end marker already subtracted)
779
- let remainingBlobFields = BLOBS_PER_CHECKPOINT * FIELDS_PER_BLOB - NUM_CHECKPOINT_END_MARKER_FIELDS;
780
873
  // Last block in the checkpoint will usually be flagged as pending broadcast, so we send it along with the checkpoint proposal
781
874
  let blockPendingBroadcast = undefined;
782
875
  while(true){
@@ -787,7 +880,7 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
787
880
  const timingInfo = this.timetable.canStartNextBlock(secondsIntoSlot);
788
881
  if (!timingInfo.canStart) {
789
882
  this.log.debug(`Not enough time left in slot to start another block`, {
790
- slot: this.slot,
883
+ slot: this.targetSlot,
791
884
  blocksBuilt,
792
885
  secondsIntoSlot
793
886
  });
@@ -802,68 +895,57 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
802
895
  buildDeadline: timingInfo.deadline ? new Date((this.getSlotStartBuildTimestamp() + timingInfo.deadline) * 1000) : undefined,
803
896
  blockNumber,
804
897
  indexWithinCheckpoint,
805
- txHashesAlreadyIncluded,
806
- remainingBlobFields
898
+ txHashesAlreadyIncluded
807
899
  });
808
- // TODO(palla/mbps): Review these conditions. We may want to keep trying in some scenarios.
809
- if (!buildResult && timingInfo.isLastBlock) {
810
- break;
811
- } else if (!buildResult && timingInfo.deadline !== undefined) {
812
- // But if there is still time for more blocks, wait until the next subslot and try again
900
+ // If we failed to build the block due to insufficient txs, we try again if there is still time left in the slot
901
+ if ('failure' in buildResult) {
902
+ // If this was the last subslot, or we're running with a single block per slot, we're done
903
+ if (timingInfo.isLastBlock || timingInfo.deadline === undefined) {
904
+ break;
905
+ }
906
+ // Otherwise, if there is still time for more blocks, we wait until the next subslot and try again
813
907
  await this.waitUntilNextSubslot(timingInfo.deadline);
814
908
  continue;
815
- } else if (!buildResult) {
816
- break;
817
- } else if ('error' in buildResult) {
818
- // If there was an error building the block, just exit the loop and give up the rest of the slot
909
+ }
910
+ // If there was an error building the block, we just exit the loop and give up the rest of the slot.
911
+ // We don't want to risk building more blocks if something went wrong.
912
+ if ('error' in buildResult) {
819
913
  if (!(buildResult.error instanceof SequencerInterruptedError)) {
820
- this.log.warn(`Halting block building for slot ${this.slot}`, {
821
- slot: this.slot,
914
+ this.log.warn(`Halting block building for slot ${this.targetSlot}`, {
915
+ slot: this.targetSlot,
822
916
  blocksBuilt,
823
917
  error: buildResult.error
824
918
  });
825
919
  }
826
920
  break;
827
921
  }
828
- const { block, usedTxs, remainingBlobFields: newRemainingBlobFields } = buildResult;
922
+ const { block, usedTxs } = buildResult;
829
923
  blocksInCheckpoint.push(block);
830
- // Update remaining blob fields for the next block
831
- remainingBlobFields = newRemainingBlobFields;
832
- // Sync the proposed block to the archiver to make it available
833
- // Note that the checkpoint builder uses its own fork so it should not need to wait for this syncing
834
- // Eventually we should refactor the checkpoint builder to not need a separate long-lived fork
835
- // Fire and forget - don't block the critical path, but log errors
836
- this.syncProposedBlockToArchiver(block).catch((err)=>{
837
- this.log.error(`Failed to sync proposed block ${block.number} to archiver`, {
838
- blockNumber: block.number,
839
- err
840
- });
841
- });
842
924
  usedTxs.forEach((tx)=>txHashesAlreadyIncluded.add(tx.txHash.toString()));
843
- // If this is the last block, exit the loop now so we start collecting attestations
925
+ // Sign the block proposal. This will throw if HA signing fails.
926
+ const proposal = await this.createBlockProposal(block, inHash, usedTxs, blockProposalOptions);
927
+ // Sync the proposed block to the archiver to make it available, only after we've managed to sign the proposal,
928
+ // so we avoid polluting our archive with a block that would fail.
929
+ // We wait for the sync to succeed, as this helps catch consistency errors, even if it means we lose some time for block-building.
930
+ // If this throws, we abort the entire checkpoint.
931
+ await this.syncProposedBlockToArchiver(block);
932
+ // If this is the last block, do not broadcast it, since it will be included in the checkpoint proposal.
844
933
  if (timingInfo.isLastBlock) {
845
- this.log.verbose(`Completed final block ${blockNumber} for slot ${this.slot}`, {
846
- slot: this.slot,
934
+ this.log.verbose(`Completed final block ${blockNumber} for slot ${this.targetSlot}`, {
935
+ slot: this.targetSlot,
847
936
  blockNumber,
848
937
  blocksBuilt
849
938
  });
850
- blockPendingBroadcast = {
851
- block,
852
- txs: usedTxs
853
- };
939
+ blockPendingBroadcast = proposal;
854
940
  break;
855
941
  }
856
- // For non-last blocks, broadcast the block proposal (unless we're in fisherman mode)
857
- // If the block is the last one, we'll broadcast it along with the checkpoint at the end of the loop
858
- if (!this.config.fishermanMode) {
859
- const proposal = await this.validatorClient.createBlockProposal(block.header, block.indexWithinCheckpoint, inHash, block.archive.root, usedTxs, this.proposer, blockProposalOptions);
860
- await this.p2pClient.broadcastProposal(proposal);
861
- }
942
+ // Once we have a signed proposal and the archiver agreed with our proposed block, then we broadcast it.
943
+ proposal && await this.p2pClient.broadcastProposal(proposal);
862
944
  // Wait until the next block's start time
863
945
  await this.waitUntilNextSubslot(timingInfo.deadline);
864
946
  }
865
- this.log.verbose(`Block building loop completed for slot ${this.slot}`, {
866
- slot: this.slot,
947
+ this.log.verbose(`Block building loop completed for slot ${this.targetSlot}`, {
948
+ slot: this.targetSlot,
867
949
  blocksBuilt: blocksInCheckpoint.length
868
950
  });
869
951
  return {
@@ -871,82 +953,92 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
871
953
  blockPendingBroadcast
872
954
  };
873
955
  }
956
+ /** Creates a block proposal for a given block via the validator client (unless in fisherman mode) */ createBlockProposal(block, inHash, usedTxs, blockProposalOptions) {
957
+ if (this.config.fishermanMode) {
958
+ this.log.info(`Skipping block proposal for block ${block.number} in fisherman mode`);
959
+ return Promise.resolve(undefined);
960
+ }
961
+ return this.validatorClient.createBlockProposal(block.header, block.indexWithinCheckpoint, inHash, block.archive.root, usedTxs, this.proposer, blockProposalOptions);
962
+ }
874
963
  /** Sleeps until it is time to produce the next block in the slot */ async waitUntilNextSubslot(nextSubslotStart) {
875
- this.setStateFn(SequencerState.WAITING_UNTIL_NEXT_BLOCK, this.slot);
964
+ this.setStateFn(SequencerState.WAITING_UNTIL_NEXT_BLOCK, this.targetSlot);
876
965
  this.log.verbose(`Waiting until time for the next block at ${nextSubslotStart}s into slot`, {
877
- slot: this.slot
966
+ slot: this.targetSlot
878
967
  });
879
968
  await this.waitUntilTimeInSlot(nextSubslotStart);
880
969
  }
881
970
  /** Builds a single block. Called from the main block building loop. */ async buildSingleBlock(checkpointBuilder, opts) {
882
- const { blockTimestamp, forceCreate, blockNumber, indexWithinCheckpoint, buildDeadline, txHashesAlreadyIncluded, remainingBlobFields } = opts;
883
- this.log.verbose(`Preparing block ${blockNumber} index ${indexWithinCheckpoint} at checkpoint ${this.checkpointNumber} for slot ${this.slot}`, {
971
+ const { blockTimestamp, forceCreate, blockNumber, indexWithinCheckpoint, buildDeadline, txHashesAlreadyIncluded } = opts;
972
+ this.log.verbose(`Preparing block ${blockNumber} index ${indexWithinCheckpoint} at checkpoint ${this.checkpointNumber} for slot ${this.targetSlot}`, {
884
973
  ...checkpointBuilder.getConstantData(),
885
974
  ...opts
886
975
  });
887
976
  try {
888
977
  // Wait until we have enough txs to build the block
889
- const minTxs = this.config.minTxsPerBlock;
890
- const { availableTxs, canStartBuilding } = await this.waitForMinTxs(opts);
978
+ const { availableTxs, canStartBuilding, minTxs } = await this.waitForMinTxs(opts);
891
979
  if (!canStartBuilding) {
892
- this.log.warn(`Not enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.slot} (got ${availableTxs} txs but needs ${minTxs})`, {
980
+ this.log.warn(`Not enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.targetSlot} (got ${availableTxs} txs but needs ${minTxs})`, {
893
981
  blockNumber,
894
- slot: this.slot,
982
+ slot: this.targetSlot,
895
983
  indexWithinCheckpoint
896
984
  });
897
985
  this.eventEmitter.emit('block-tx-count-check-failed', {
898
986
  minTxs,
899
987
  availableTxs,
900
- slot: this.slot
988
+ slot: this.targetSlot
901
989
  });
902
990
  this.metrics.recordBlockProposalFailed('insufficient_txs');
903
- return undefined;
991
+ return {
992
+ failure: 'insufficient-txs'
993
+ };
904
994
  }
905
995
  // Create iterator to pending txs. We filter out txs already included in previous blocks in the checkpoint
906
996
  // just in case p2p failed to sync the provisional block and didn't get to remove those txs from the mempool yet.
907
997
  const pendingTxs = filter(this.p2pClient.iterateEligiblePendingTxs(), (tx)=>!txHashesAlreadyIncluded.has(tx.txHash.toString()));
908
- this.log.debug(`Building block ${blockNumber} at index ${indexWithinCheckpoint} for slot ${this.slot} with ${availableTxs} available txs`, {
909
- slot: this.slot,
998
+ this.log.debug(`Building block ${blockNumber} at index ${indexWithinCheckpoint} for slot ${this.targetSlot} with ${availableTxs} available txs`, {
999
+ slot: this.targetSlot,
910
1000
  blockNumber,
911
1001
  indexWithinCheckpoint
912
1002
  });
913
- this.setStateFn(SequencerState.CREATING_BLOCK, this.slot);
914
- // Calculate blob fields limit for txs (remaining capacity - this block's end overhead)
915
- const blockEndOverhead = getNumBlockEndBlobFields(indexWithinCheckpoint === 0);
916
- const maxBlobFieldsForTxs = remainingBlobFields - blockEndOverhead;
1003
+ this.setStateFn(SequencerState.CREATING_BLOCK, this.targetSlot);
1004
+ // Per-block limits are operator overrides (from SEQ_MAX_L2_BLOCK_GAS etc.) further capped
1005
+ // by remaining checkpoint-level budgets inside CheckpointBuilder before each block is built.
1006
+ // minValidTxs is passed into the builder so it can reject the block *before* updating state.
1007
+ const minValidTxs = forceCreate ? 0 : this.config.minValidTxsPerBlock ?? minTxs;
917
1008
  const blockBuilderOptions = {
918
1009
  maxTransactions: this.config.maxTxsPerBlock,
919
- maxBlockSize: this.config.maxBlockSizeInBytes,
920
- maxBlockGas: new Gas(this.config.maxDABlockGas, this.config.maxL2BlockGas),
921
- maxBlobFields: maxBlobFieldsForTxs,
922
- deadline: buildDeadline
1010
+ maxBlockGas: this.config.maxL2BlockGas !== undefined || this.config.maxDABlockGas !== undefined ? new Gas(this.config.maxDABlockGas ?? Infinity, this.config.maxL2BlockGas ?? Infinity) : undefined,
1011
+ deadline: buildDeadline,
1012
+ isBuildingProposal: true,
1013
+ minValidTxs,
1014
+ maxBlocksPerCheckpoint: this.timetable.maxNumberOfBlocks,
1015
+ perBlockAllocationMultiplier: this.config.perBlockAllocationMultiplier
923
1016
  };
924
- // Actually build the block by executing txs
1017
+ // Actually build the block by executing txs. The builder throws InsufficientValidTxsError
1018
+ // if the number of successfully processed txs is below minValidTxs, ensuring state is not
1019
+ // updated for blocks that will be discarded.
925
1020
  const buildResult = await this.buildSingleBlockWithCheckpointBuilder(checkpointBuilder, pendingTxs, blockNumber, blockTimestamp, blockBuilderOptions);
926
1021
  // If any txs failed during execution, drop them from the mempool so we don't pick them up again
927
1022
  await this.dropFailedTxsFromP2P(buildResult.failedTxs);
928
- // Check if we have created a block with enough txs. If there were invalid txs in the pool, or if execution took
929
- // too long, then we may not get to minTxsPerBlock after executing public functions.
930
- const minValidTxs = this.config.minValidTxsPerBlock ?? minTxs;
931
- const numTxs = buildResult.status === 'no-valid-txs' ? 0 : buildResult.numTxs;
932
- if (buildResult.status === 'no-valid-txs' || !forceCreate && numTxs < minValidTxs) {
933
- this.log.warn(`Block ${blockNumber} at index ${indexWithinCheckpoint} on slot ${this.slot} has too few valid txs to be proposed`, {
934
- slot: this.slot,
1023
+ if (buildResult.status === 'insufficient-valid-txs') {
1024
+ this.log.warn(`Block ${blockNumber} at index ${indexWithinCheckpoint} on slot ${this.targetSlot} has too few valid txs to be proposed`, {
1025
+ slot: this.targetSlot,
935
1026
  blockNumber,
936
- numTxs,
1027
+ numTxs: buildResult.processedCount,
937
1028
  indexWithinCheckpoint,
938
- minValidTxs,
939
- buildResult: buildResult.status
1029
+ minValidTxs
940
1030
  });
941
1031
  this.eventEmitter.emit('block-build-failed', {
942
1032
  reason: `Insufficient valid txs`,
943
- slot: this.slot
1033
+ slot: this.targetSlot
944
1034
  });
945
1035
  this.metrics.recordBlockProposalFailed('insufficient_valid_txs');
946
- return undefined;
1036
+ return {
1037
+ failure: 'insufficient-valid-txs'
1038
+ };
947
1039
  }
948
1040
  // Block creation succeeded, emit stats and metrics
949
- const { publicGas, block, publicProcessorDuration, usedTxs, usedTxBlobFields, blockBuildDuration } = buildResult;
1041
+ const { block, publicProcessorDuration, usedTxs, blockBuildDuration, numTxs } = buildResult;
950
1042
  const blockStats = {
951
1043
  eventName: 'l2-block-built',
952
1044
  duration: blockBuildDuration,
@@ -955,31 +1047,33 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
955
1047
  };
956
1048
  const blockHash = await block.hash();
957
1049
  const txHashes = block.body.txEffects.map((tx)=>tx.txHash);
958
- const manaPerSec = publicGas.l2Gas / (blockBuildDuration / 1000);
959
- this.log.info(`Built block ${block.number} at checkpoint ${this.checkpointNumber} for slot ${this.slot} with ${numTxs} txs`, {
1050
+ const manaPerSec = block.header.totalManaUsed.toNumberUnsafe() / (blockBuildDuration / 1000);
1051
+ this.log.info(`Built block ${block.number} at checkpoint ${this.checkpointNumber} for slot ${this.targetSlot} with ${numTxs} txs`, {
960
1052
  blockHash,
961
1053
  txHashes,
962
1054
  manaPerSec,
963
1055
  ...blockStats
964
1056
  });
1057
+ // `slot` is the target/submission slot (may be one ahead when pipelining),
1058
+ // `buildSlot` is the wall-clock slot during which the block was actually built.
965
1059
  this.eventEmitter.emit('block-proposed', {
966
1060
  blockNumber: block.number,
967
- slot: this.slot
1061
+ slot: this.targetSlot,
1062
+ buildSlot: this.slotNow
968
1063
  });
969
- this.metrics.recordBuiltBlock(blockBuildDuration, publicGas.l2Gas);
1064
+ this.metrics.recordBuiltBlock(blockBuildDuration, block.header.totalManaUsed.toNumberUnsafe(), this.targetSlot);
970
1065
  return {
971
1066
  block,
972
- usedTxs,
973
- remainingBlobFields: maxBlobFieldsForTxs - usedTxBlobFields
1067
+ usedTxs
974
1068
  };
975
1069
  } catch (err) {
976
1070
  this.eventEmitter.emit('block-build-failed', {
977
1071
  reason: err.message,
978
- slot: this.slot
1072
+ slot: this.targetSlot
979
1073
  });
980
1074
  this.log.error(`Error building block`, err, {
981
1075
  blockNumber,
982
- slot: this.slot
1076
+ slot: this.targetSlot
983
1077
  });
984
1078
  this.metrics.recordBlockProposalFailed(err.name || 'unknown_error');
985
1079
  this.metrics.recordFailedBlock();
@@ -988,7 +1082,7 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
988
1082
  };
989
1083
  }
990
1084
  }
991
- /** Uses the checkpoint builder to build a block, catching specific txs */ async buildSingleBlockWithCheckpointBuilder(checkpointBuilder, pendingTxs, blockNumber, blockTimestamp, blockBuilderOptions) {
1085
+ /** Uses the checkpoint builder to build a block, catching InsufficientValidTxsError. */ async buildSingleBlockWithCheckpointBuilder(checkpointBuilder, pendingTxs, blockNumber, blockTimestamp, blockBuilderOptions) {
992
1086
  try {
993
1087
  const workTimer = new Timer();
994
1088
  const result = await checkpointBuilder.buildBlock(pendingTxs, blockNumber, blockTimestamp, blockBuilderOptions);
@@ -999,10 +1093,11 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
999
1093
  status: 'success'
1000
1094
  };
1001
1095
  } catch (err) {
1002
- if (isErrorClass(err, NoValidTxsError)) {
1096
+ if (isErrorClass(err, InsufficientValidTxsError)) {
1003
1097
  return {
1004
1098
  failedTxs: err.failedTxs,
1005
- status: 'no-valid-txs'
1099
+ processedCount: err.processedCount,
1100
+ status: 'insufficient-valid-txs'
1006
1101
  };
1007
1102
  }
1008
1103
  throw err;
@@ -1021,14 +1116,15 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
1021
1116
  if (startBuildingDeadline === undefined || now >= startBuildingDeadline) {
1022
1117
  return {
1023
1118
  canStartBuilding: false,
1024
- availableTxs: availableTxs
1119
+ availableTxs,
1120
+ minTxs
1025
1121
  };
1026
1122
  }
1027
1123
  // Wait a bit before checking again
1028
- this.setStateFn(SequencerState.WAITING_FOR_TXS, this.slot);
1029
- this.log.verbose(`Waiting for enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.slot} (have ${availableTxs} but need ${minTxs})`, {
1124
+ this.setStateFn(SequencerState.WAITING_FOR_TXS, this.targetSlot);
1125
+ this.log.verbose(`Waiting for enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.targetSlot} (have ${availableTxs} but need ${minTxs})`, {
1030
1126
  blockNumber,
1031
- slot: this.slot,
1127
+ slot: this.targetSlot,
1032
1128
  indexWithinCheckpoint
1033
1129
  });
1034
1130
  await this.waitForTxsPollingInterval();
@@ -1036,7 +1132,8 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
1036
1132
  }
1037
1133
  return {
1038
1134
  canStartBuilding: true,
1039
- availableTxs
1135
+ availableTxs,
1136
+ minTxs
1040
1137
  };
1041
1138
  }
1042
1139
  /**
@@ -1059,13 +1156,13 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
1059
1156
  committee
1060
1157
  });
1061
1158
  }
1062
- const numberOfRequiredAttestations = Math.floor(committee.length * 2 / 3) + 1;
1159
+ const numberOfRequiredAttestations = computeQuorum(committee.length);
1063
1160
  if (this.config.skipCollectingAttestations) {
1064
1161
  this.log.warn('Skipping attestation collection as per config (attesting with own keys only)');
1065
1162
  const attestations = await this.validatorClient?.collectOwnAttestations(proposal);
1066
1163
  return new CommitteeAttestationsAndSigners(orderAttestations(attestations ?? [], committee));
1067
1164
  }
1068
- const attestationTimeAllowed = this.config.enforceTimeTable ? this.timetable.getMaxAllowedTime(SequencerState.PUBLISHING_CHECKPOINT) : this.l1Constants.slotDuration;
1165
+ const attestationTimeAllowed = this.config.enforceTimeTable ? this.timetable.getCheckpointAttestationDeadline() : this.l1Constants.slotDuration;
1069
1166
  const attestationDeadline = new Date((this.getSlotStartBuildTimestamp() + attestationTimeAllowed) * 1000);
1070
1167
  this.metrics.recordRequiredAttestations(numberOfRequiredAttestations, attestationTimeAllowed);
1071
1168
  const collectAttestationsTimer = new Timer();
@@ -1082,7 +1179,7 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
1082
1179
  // Rollup contract requires that the signatures are provided in the order of the committee
1083
1180
  const sorted = orderAttestations(trimmed, committee);
1084
1181
  // Manipulate the attestations if we've been configured to do so
1085
- if (this.config.injectFakeAttestation || this.config.shuffleAttestationOrdering) {
1182
+ if (this.config.injectFakeAttestation || this.config.injectHighSValueAttestation || this.config.injectUnrecoverableSignatureAttestation || this.config.shuffleAttestationOrdering) {
1086
1183
  return this.manipulateAttestations(proposal.slotNumber, epoch, seed, committee, sorted);
1087
1184
  }
1088
1185
  return new CommitteeAttestationsAndSigners(sorted);
@@ -1099,7 +1196,7 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
1099
1196
  // Compute the proposer index in the committee, since we dont want to tweak it.
1100
1197
  // Otherwise, the L1 rollup contract will reject the block outright.
1101
1198
  const proposerIndex = Number(this.epochCache.computeProposerIndex(slotNumber, epoch, seed, BigInt(committee.length)));
1102
- if (this.config.injectFakeAttestation) {
1199
+ if (this.config.injectFakeAttestation || this.config.injectHighSValueAttestation || this.config.injectUnrecoverableSignatureAttestation) {
1103
1200
  // Find non-empty attestations that are not from the proposer
1104
1201
  const nonProposerIndices = [];
1105
1202
  for(let i = 0; i < attestations.length; i++){
@@ -1109,8 +1206,16 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
1109
1206
  }
1110
1207
  if (nonProposerIndices.length > 0) {
1111
1208
  const targetIndex = nonProposerIndices[randomInt(nonProposerIndices.length)];
1112
- this.log.warn(`Injecting fake attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`);
1113
- unfreeze(attestations[targetIndex]).signature = Signature.random();
1209
+ if (this.config.injectHighSValueAttestation) {
1210
+ this.log.warn(`Injecting high-s value attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`);
1211
+ unfreeze(attestations[targetIndex]).signature = flipSignature(attestations[targetIndex].signature);
1212
+ } else if (this.config.injectUnrecoverableSignatureAttestation) {
1213
+ this.log.warn(`Injecting unrecoverable signature attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`);
1214
+ unfreeze(attestations[targetIndex]).signature = generateUnrecoverableSignature();
1215
+ } else {
1216
+ this.log.warn(`Injecting fake attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`);
1217
+ unfreeze(attestations[targetIndex]).signature = generateRecoverableSignature();
1218
+ }
1114
1219
  }
1115
1220
  return new CommitteeAttestationsAndSigners(attestations);
1116
1221
  }
@@ -1119,14 +1224,25 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
1119
1224
  const shuffled = [
1120
1225
  ...attestations
1121
1226
  ];
1122
- const [i, j] = [
1123
- (proposerIndex + 1) % shuffled.length,
1124
- (proposerIndex + 2) % shuffled.length
1125
- ];
1126
- const valueI = shuffled[i];
1127
- const valueJ = shuffled[j];
1128
- shuffled[i] = valueJ;
1129
- shuffled[j] = valueI;
1227
+ // Find two non-proposer positions that both have non-empty signatures to swap.
1228
+ // This ensures the bitmap doesn't change, so the MaliciousCommitteeAttestationsAndSigners
1229
+ // signers array stays correctly aligned with L1's committee reconstruction.
1230
+ const swappable = [];
1231
+ for(let k = 0; k < shuffled.length; k++){
1232
+ if (!shuffled[k].signature.isEmpty() && k !== proposerIndex) {
1233
+ swappable.push(k);
1234
+ }
1235
+ }
1236
+ if (swappable.length >= 2) {
1237
+ const [i, j] = [
1238
+ swappable[0],
1239
+ swappable[1]
1240
+ ];
1241
+ [shuffled[i], shuffled[j]] = [
1242
+ shuffled[j],
1243
+ shuffled[i]
1244
+ ];
1245
+ }
1130
1246
  const signers = new CommitteeAttestationsAndSigners(attestations).getSigners();
1131
1247
  return new MaliciousCommitteeAttestationsAndSigners(shuffled, signers);
1132
1248
  }
@@ -1145,8 +1261,12 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
1145
1261
  * Adds the proposed block to the archiver so it's available via P2P.
1146
1262
  * Gossip doesn't echo messages back to the sender, so the proposer's archiver/world-state
1147
1263
  * would never receive its own block without this explicit sync.
1264
+ *
1265
+ * In fisherman mode we skip this push: the fisherman builds blocks locally for validation
1266
+ * and fee analysis only, and pushing them to the archiver causes spurious reorg cascades
1267
+ * whenever the real proposer's block arrives from L1.
1148
1268
  */ async syncProposedBlockToArchiver(block) {
1149
- if (this.config.skipPushProposedBlocksToArchiver !== false) {
1269
+ if (this.config.skipPushProposedBlocksToArchiver || this.config.fishermanMode) {
1150
1270
  this.log.warn(`Skipping push of proposed block ${block.number} to archiver`, {
1151
1271
  blockNumber: block.number,
1152
1272
  slot: block.header.globalVariables.slotNumber
@@ -1162,16 +1282,16 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
1162
1282
  /** Runs fee analysis and logs checkpoint outcome as fisherman */ async handleCheckpointEndAsFisherman(checkpoint) {
1163
1283
  // Perform L1 fee analysis before clearing requests
1164
1284
  // The callback is invoked asynchronously after the next block is mined
1165
- const feeAnalysis = await this.publisher.analyzeL1Fees(this.slot, (analysis)=>this.metrics.recordFishermanFeeAnalysis(analysis));
1285
+ const feeAnalysis = await this.publisher.analyzeL1Fees(this.targetSlot, (analysis)=>this.metrics.recordFishermanFeeAnalysis(analysis));
1166
1286
  if (checkpoint) {
1167
- this.log.info(`Validation checkpoint building SUCCEEDED for slot ${this.slot}`, {
1287
+ this.log.info(`Validation checkpoint building SUCCEEDED for slot ${this.targetSlot}`, {
1168
1288
  ...checkpoint.toCheckpointInfo(),
1169
1289
  ...checkpoint.getStats(),
1170
1290
  feeAnalysisId: feeAnalysis?.id
1171
1291
  });
1172
1292
  } else {
1173
- this.log.warn(`Validation block building FAILED for slot ${this.slot}`, {
1174
- slot: this.slot,
1293
+ this.log.warn(`Validation block building FAILED for slot ${this.targetSlot}`, {
1294
+ slot: this.targetSlot,
1175
1295
  feeAnalysisId: feeAnalysis?.id
1176
1296
  });
1177
1297
  this.metrics.recordCheckpointProposalFailed('block_build_failed');
@@ -1182,15 +1302,15 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
1182
1302
  * Helper to handle HA double-signing errors. Returns true if the error was handled (caller should yield).
1183
1303
  */ handleHASigningError(err, errorContext) {
1184
1304
  if (err instanceof DutyAlreadySignedError) {
1185
- this.log.info(`${errorContext} for slot ${this.slot} already signed by another HA node, yielding`, {
1186
- slot: this.slot,
1305
+ this.log.info(`${errorContext} for slot ${this.targetSlot} already signed by another HA node, yielding`, {
1306
+ slot: this.targetSlot,
1187
1307
  signedByNode: err.signedByNode
1188
1308
  });
1189
1309
  return true;
1190
1310
  }
1191
1311
  if (err instanceof SlashingProtectionError) {
1192
- this.log.info(`${errorContext} for slot ${this.slot} blocked by slashing protection, yielding`, {
1193
- slot: this.slot,
1312
+ this.log.info(`${errorContext} for slot ${this.targetSlot} blocked by slashing protection, yielding`, {
1313
+ slot: this.targetSlot,
1194
1314
  existingMessageHash: err.existingMessageHash,
1195
1315
  attemptedMessageHash: err.attemptedMessageHash
1196
1316
  });
@@ -1207,7 +1327,7 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
1207
1327
  await sleep(TXS_POLLING_MS);
1208
1328
  }
1209
1329
  getSlotStartBuildTimestamp() {
1210
- return getSlotStartBuildTimestamp(this.slot, this.l1Constants);
1330
+ return getSlotStartBuildTimestamp(this.slotNow, this.l1Constants);
1211
1331
  }
1212
1332
  getSecondsIntoSlot() {
1213
1333
  const slotStartTimestamp = this.getSlotStartBuildTimestamp();