@aztec/sequencer-client 0.0.1-commit.f5d02921e → 0.0.1-commit.f7ea82942

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 (69) 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 +3 -4
  4. package/dest/config.d.ts +1 -1
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +8 -11
  7. package/dest/global_variable_builder/fee_predictor.d.ts +37 -0
  8. package/dest/global_variable_builder/fee_predictor.d.ts.map +1 -0
  9. package/dest/global_variable_builder/fee_predictor.js +128 -0
  10. package/dest/global_variable_builder/fee_provider.d.ts +21 -0
  11. package/dest/global_variable_builder/fee_provider.d.ts.map +1 -0
  12. package/dest/global_variable_builder/fee_provider.js +58 -0
  13. package/dest/global_variable_builder/global_builder.d.ts +4 -9
  14. package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
  15. package/dest/global_variable_builder/global_builder.js +3 -41
  16. package/dest/global_variable_builder/index.d.ts +3 -1
  17. package/dest/global_variable_builder/index.d.ts.map +1 -1
  18. package/dest/global_variable_builder/index.js +2 -0
  19. package/dest/publisher/config.d.ts +1 -1
  20. package/dest/publisher/config.d.ts.map +1 -1
  21. package/dest/publisher/config.js +2 -2
  22. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts +3 -4
  23. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -1
  24. package/dest/publisher/sequencer-publisher-factory.d.ts +1 -3
  25. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  26. package/dest/publisher/sequencer-publisher-factory.js +0 -1
  27. package/dest/publisher/sequencer-publisher.d.ts +16 -35
  28. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  29. package/dest/publisher/sequencer-publisher.js +60 -92
  30. package/dest/sequencer/chain_state_overrides.d.ts +25 -0
  31. package/dest/sequencer/chain_state_overrides.d.ts.map +1 -0
  32. package/dest/sequencer/chain_state_overrides.js +39 -0
  33. package/dest/sequencer/checkpoint_proposal_job.d.ts +27 -22
  34. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  35. package/dest/sequencer/checkpoint_proposal_job.js +294 -155
  36. package/dest/sequencer/checkpoint_proposal_job_metrics.d.ts +34 -0
  37. package/dest/sequencer/checkpoint_proposal_job_metrics.d.ts.map +1 -0
  38. package/dest/sequencer/checkpoint_proposal_job_metrics.js +72 -0
  39. package/dest/sequencer/events.d.ts +6 -1
  40. package/dest/sequencer/events.d.ts.map +1 -1
  41. package/dest/sequencer/metrics.d.ts +7 -10
  42. package/dest/sequencer/metrics.d.ts.map +1 -1
  43. package/dest/sequencer/metrics.js +27 -20
  44. package/dest/sequencer/sequencer.d.ts +5 -2
  45. package/dest/sequencer/sequencer.d.ts.map +1 -1
  46. package/dest/sequencer/sequencer.js +31 -23
  47. package/dest/sequencer/timetable.d.ts +14 -1
  48. package/dest/sequencer/timetable.d.ts.map +1 -1
  49. package/dest/sequencer/timetable.js +45 -36
  50. package/package.json +27 -27
  51. package/src/client/sequencer-client.ts +5 -7
  52. package/src/config.ts +8 -11
  53. package/src/global_variable_builder/README.md +44 -0
  54. package/src/global_variable_builder/fee_predictor.ts +172 -0
  55. package/src/global_variable_builder/fee_provider.ts +75 -0
  56. package/src/global_variable_builder/global_builder.ts +7 -54
  57. package/src/global_variable_builder/index.ts +2 -0
  58. package/src/publisher/config.ts +6 -4
  59. package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +3 -1
  60. package/src/publisher/sequencer-publisher-factory.ts +0 -3
  61. package/src/publisher/sequencer-publisher.ts +106 -152
  62. package/src/sequencer/README.md +83 -13
  63. package/src/sequencer/chain_state_overrides.ts +87 -0
  64. package/src/sequencer/checkpoint_proposal_job.ts +365 -205
  65. package/src/sequencer/checkpoint_proposal_job_metrics.ts +128 -0
  66. package/src/sequencer/events.ts +5 -0
  67. package/src/sequencer/metrics.ts +35 -25
  68. package/src/sequencer/sequencer.ts +44 -26
  69. package/src/sequencer/timetable.ts +57 -45
@@ -436,13 +436,12 @@ 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 { RollupContract } from '@aztec/ethereum/contracts';
440
439
  import { BlockNumber, CheckpointNumber, IndexWithinCheckpoint } from '@aztec/foundation/branded-types';
441
440
  import { randomInt } from '@aztec/foundation/crypto/random';
442
441
  import { flipSignature, generateRecoverableSignature, generateUnrecoverableSignature } from '@aztec/foundation/crypto/secp256k1-signer';
443
- import { Signature } from '@aztec/foundation/eth-signature';
444
442
  import { filter } from '@aztec/foundation/iterator';
445
443
  import { createLogger } from '@aztec/foundation/log';
444
+ import { retryUntil } from '@aztec/foundation/retry';
446
445
  import { sleep, sleepUntil } from '@aztec/foundation/sleep';
447
446
  import { Timer } from '@aztec/foundation/timer';
448
447
  import { isErrorClass, unfreeze } from '@aztec/foundation/types';
@@ -456,6 +455,7 @@ import { orderAttestations, trimAttestations } from '@aztec/stdlib/p2p';
456
455
  import { AttestationTimeoutError } from '@aztec/stdlib/validators';
457
456
  import { Attributes, trackSpan } from '@aztec/telemetry-client';
458
457
  import { DutyAlreadySignedError, SlashingProtectionError } from '@aztec/validator-ha-signer/errors';
458
+ import { buildPipelinedParentSimulationOverridesPlan, buildSubmissionSimulationOverridesPlan } from './chain_state_overrides.js';
459
459
  import { CheckpointVoter } from './checkpoint_voter.js';
460
460
  import { SequencerInterruptedError } from './errors.js';
461
461
  import { SequencerState } from './utils.js';
@@ -497,6 +497,7 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
497
497
  epochCache;
498
498
  dateProvider;
499
499
  metrics;
500
+ checkpointMetrics;
500
501
  eventEmitter;
501
502
  setStateFn;
502
503
  tracer;
@@ -547,9 +548,9 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
547
548
  }
548
549
  log;
549
550
  /** Tracks the fire-and-forget L1 submission promise so it can be awaited during shutdown. */ pendingL1Submission;
550
- /** Fee header override computed during proposeCheckpoint, reused in enqueueCheckpointForSubmission. */ computedForceProposedFeeHeader;
551
+ /** Pipelined parent chain state used while building and later submitting this checkpoint. */ pipelinedParentSimulationOverridesPlan;
551
552
  constructor(slotNow, targetSlot, targetEpoch, checkpointNumber, syncedToBlockNumber, // TODO(palla/mbps): Can we remove the proposer in favor of attestorAddress? Need to check fisherman-node flows.
552
- proposer, publisher, attestorAddress, invalidateCheckpoint, validatorClient, globalsBuilder, p2pClient, worldState, l1ToL2MessageSource, l2BlockSource, checkpointsBuilder, blockSink, l1Constants, config, timetable, slasherClient, epochCache, dateProvider, metrics, eventEmitter, setStateFn, tracer, bindings, proposedCheckpointData){
553
+ proposer, publisher, attestorAddress, invalidateCheckpoint, validatorClient, globalsBuilder, p2pClient, worldState, l1ToL2MessageSource, l2BlockSource, checkpointsBuilder, blockSink, l1Constants, config, timetable, slasherClient, epochCache, dateProvider, metrics, checkpointMetrics, eventEmitter, setStateFn, tracer, bindings, proposedCheckpointData){
553
554
  this.slotNow = slotNow;
554
555
  this.targetSlot = targetSlot;
555
556
  this.targetEpoch = targetEpoch;
@@ -574,6 +575,7 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
574
575
  this.epochCache = epochCache;
575
576
  this.dateProvider = dateProvider;
576
577
  this.metrics = metrics;
578
+ this.checkpointMetrics = checkpointMetrics;
577
579
  this.eventEmitter = eventEmitter;
578
580
  this.setStateFn = setStateFn;
579
581
  this.tracer = tracer;
@@ -590,74 +592,102 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
590
592
  }
591
593
  /**
592
594
  * Executes the checkpoint proposal job.
593
- * Builds blocks, collects attestations, enqueues requests, and schedules L1 submission as a
594
- * background task so the work loop can return to IDLE immediately.
595
+ * Builds blocks, assembles checkpoint, and broadcasts the proposal (blocking).
596
+ * Attestation collection, signing, and L1 submission are backgrounded so the
597
+ * work loop can return to IDLE immediately for consecutive slot proposals.
595
598
  * Returns the built checkpoint if successful, undefined otherwise.
596
599
  */ async execute() {
597
600
  // Enqueue governance and slashing votes (returns promises that will be awaited later)
598
601
  // In fisherman mode, we simulate slashing but don't actually publish to L1
599
602
  // These are constant for the whole slot, so we only enqueue them once
600
603
  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 and propose the checkpoint. Builds blocks, broadcasts, collects attestations, and signs.
602
- // Does NOT enqueue to L1 yet that happens after the pipeline sleep.
603
- const proposalResult = await this.proposeCheckpoint();
604
- const checkpoint = proposalResult?.checkpoint;
605
- // Wait until the voting promises have resolved, so all requests are enqueued (not sent)
606
- await Promise.all(votesPromises);
607
- if (checkpoint) {
608
- this.metrics.recordCheckpointProposalSuccess();
604
+ // Build blocks, assemble checkpoint, and broadcast proposal (BLOCKING).
605
+ // Returns after broadcastattestation collection is deferred.
606
+ const broadcast = await this.proposeCheckpoint();
607
+ if (!broadcast) {
608
+ await Promise.all(votesPromises);
609
+ // Still submit votes even without a checkpoint
610
+ if (!this.config.fishermanMode) {
611
+ this.pendingL1Submission = this.publisher.sendRequestsAt(this.dateProvider.nowAsDate()).then(()=>{});
612
+ }
613
+ return undefined;
609
614
  }
615
+ const { checkpoint } = broadcast;
616
+ this.metrics.recordCheckpointProposalSuccess();
610
617
  // Do not post anything to L1 if we are fishermen, but do perform L1 fee analysis
611
618
  if (this.config.fishermanMode) {
612
619
  await this.handleCheckpointEndAsFisherman(checkpoint);
613
- return;
620
+ return checkpoint;
614
621
  }
615
- // Enqueue the checkpoint for L1 submission
616
- if (proposalResult) {
617
- try {
618
- await this.enqueueCheckpointForSubmission(proposalResult);
619
- } catch (err) {
620
- this.log.error(`Failed to enqueue checkpoint for L1 submission at slot ${this.targetSlot}`, err);
621
- // Continue to sendRequestsAt so votes are still sent
622
+ // Background the attestation signing → L1 pipeline so the work loop is unblocked
623
+ this.pendingL1Submission = this.waitForAttestationsAndEnqueueSubmissionAsync(broadcast, votesPromises);
624
+ // Return the built checkpoint immediately — the work loop is now unblocked
625
+ return checkpoint;
626
+ }
627
+ /**
628
+ * Background pipeline: collects attestations, signs them, enqueues the checkpoint, and submits to L1.
629
+ * Runs as a fire-and-forget task stored in `pendingL1Submission` so the work loop is unblocked.
630
+ */ async waitForAttestationsAndEnqueueSubmissionAsync(broadcast, votesPromises) {
631
+ const { checkpoint } = broadcast;
632
+ const isPipelining = this.epochCache.isProposerPipeliningEnabled();
633
+ try {
634
+ // Wait for all votes actions, enqueued at the beginning, to resolve
635
+ await Promise.all(votesPromises);
636
+ // Try to collect attestations from the committee
637
+ const signedAttestations = await this.getSignedCommitteeAttestations(broadcast);
638
+ // If pipelining, wait for the previous checkpoint to land on L1 before submitting,
639
+ // so we can check it matches the proposed checkpoint we used as parent, and has valid attestations.
640
+ if (signedAttestations && (!isPipelining || await this.waitForValidParentCheckpointOnL1())) {
641
+ await this.enqueueCheckpointForSubmission({
642
+ checkpoint,
643
+ ...signedAttestations
644
+ });
622
645
  }
623
- }
624
- // Compute the earliest time to submit: pipeline slot start when pipelining, now otherwise.
625
- const submitAfter = this.epochCache.isProposerPipeliningEnabled() ? new Date(Number(getTimestampForSlot(this.targetSlot, this.l1Constants)) * 1000) : new Date(this.dateProvider.now());
626
- // TODO(https://github.com/AztecProtocol/aztec-packages/pull/21250): should discard the pending submission if a reorg occurs underneath
627
- // Schedule L1 submission in the background so the work loop returns immediately.
628
- // The publisher will sleep until submitAfter, then send the bundled requests.
629
- // The promise is stored so it can be awaited during shutdown.
630
- this.pendingL1Submission = this.publisher.sendRequestsAt(submitAfter).then(async (l1Response)=>{
646
+ // If we failed to collect attestations, at least check if we need to issue an invalidation
647
+ // Note that if we are not pipelining, we enqueued the invalidation at the beginning
648
+ if (!signedAttestations && isPipelining && await this.waitForSyncedL2SlotNumber(this.slotNow)) {
649
+ const validationStatus = await this.l2BlockSource.getPendingChainValidationStatus();
650
+ if (!validationStatus.valid) {
651
+ this.log.warn(`Checkpoint ${validationStatus.checkpoint.checkpointNumber} has invalid attestations, enqueuing invalidation in spite of attestation collection failure`, {
652
+ checkpoint: validationStatus.checkpoint,
653
+ reason: validationStatus.reason
654
+ });
655
+ await this.enqueueInvalidation(validationStatus);
656
+ }
657
+ }
658
+ // Send whatever was enqueued: votes + (propose | invalidation | nothing).
659
+ // Compute the earliest time to submit: pipeline slot start when pipelining, now otherwise.
660
+ const submitAfter = isPipelining ? new Date(Number(getTimestampForSlot(this.targetSlot, this.l1Constants)) * 1000) : new Date(this.dateProvider.now());
661
+ const l1Response = await this.publisher.sendRequestsAt(submitAfter);
631
662
  const proposedAction = l1Response?.successfulActions.find((a)=>a === 'propose');
632
663
  if (proposedAction) {
633
664
  this.eventEmitter.emit('checkpoint-published', {
634
665
  checkpoint: this.checkpointNumber,
635
666
  slot: this.targetSlot
636
667
  });
637
- const coinbase = checkpoint?.header.coinbase;
668
+ const coinbase = checkpoint.header.coinbase;
638
669
  await this.metrics.incFilledSlot(this.publisher.getSenderAddress().toString(), coinbase);
639
- } else if (checkpoint) {
670
+ } else {
640
671
  this.eventEmitter.emit('checkpoint-publish-failed', {
641
672
  ...l1Response,
642
673
  slot: this.targetSlot
643
674
  });
644
- if (this.epochCache.isProposerPipeliningEnabled()) {
675
+ if (isPipelining) {
645
676
  this.metrics.recordPipelineDiscard();
646
677
  }
647
678
  }
648
- }).catch((err)=>{
649
- this.log.error(`Background L1 submission failed for slot ${this.targetSlot}`, err);
650
- if (checkpoint) {
651
- this.eventEmitter.emit('checkpoint-publish-failed', {
652
- slot: this.targetSlot
653
- });
654
- if (this.epochCache.isProposerPipeliningEnabled()) {
655
- this.metrics.recordPipelineDiscard();
656
- }
679
+ } catch (err) {
680
+ if (err instanceof SequencerInterruptedError) {
681
+ return;
657
682
  }
658
- });
659
- // Return the built checkpoint immediately — the work loop is now unblocked
660
- return checkpoint;
683
+ this.log.error(`Background attestation/L1 pipeline failed for slot ${this.targetSlot}`, err);
684
+ this.eventEmitter.emit('checkpoint-publish-failed', {
685
+ slot: this.targetSlot
686
+ });
687
+ if (isPipelining) {
688
+ this.metrics.recordPipelineDiscard();
689
+ }
690
+ }
661
691
  }
662
692
  /** Enqueues the checkpoint for L1 submission. Called after pipeline sleep in execute(). */ async enqueueCheckpointForSubmission(result) {
663
693
  const { checkpoint, attestations, attestationsSignature } = result;
@@ -673,12 +703,135 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
673
703
  return;
674
704
  }
675
705
  }
706
+ const isPipelining = this.epochCache.isProposerPipeliningEnabled();
707
+ const submissionSimulationOverridesPlan = buildSubmissionSimulationOverridesPlan({
708
+ pipelinedParentPlan: this.pipelinedParentSimulationOverridesPlan,
709
+ invalidateToPendingCheckpointNumber: this.invalidateCheckpoint?.forcePendingCheckpointNumber,
710
+ lastArchiveRoot: checkpoint.header.lastArchiveRoot,
711
+ pipeliningEnabled: isPipelining
712
+ });
676
713
  await this.publisher.enqueueProposeCheckpoint(checkpoint, attestations, attestationsSignature, {
677
714
  txTimeoutAt,
678
- forcePendingCheckpointNumber: this.invalidateCheckpoint?.forcePendingCheckpointNumber,
679
- forceProposedFeeHeader: this.computedForceProposedFeeHeader
715
+ ...submissionSimulationOverridesPlan ? {
716
+ simulationOverridesPlan: submissionSimulationOverridesPlan
717
+ } : {}
718
+ });
719
+ }
720
+ /**
721
+ * Wait until the archiver syncs past the given L2 slot number.
722
+ * The deadline is the end of `this.targetSlot`, beyond which any pipelined work would miss its
723
+ * L1 submission window and is no longer useful.
724
+ */ async waitForSyncedL2SlotNumber(waitForSlot) {
725
+ const targetSlotStart = Number(getTimestampForSlot(this.targetSlot, this.l1Constants));
726
+ const targetSlotEndMs = (targetSlotStart + this.l1Constants.slotDuration) * 1000;
727
+ const syncDelayTolerance = this.l1Constants.ethereumSlotDuration * 2 * 1000;
728
+ const timeoutSeconds = Math.max(0.1, (targetSlotEndMs + syncDelayTolerance - this.dateProvider.now()) / 1000);
729
+ try {
730
+ return await retryUntil(async ()=>{
731
+ const syncedSlot = await this.l2BlockSource.getSyncedL2SlotNumber();
732
+ return syncedSlot !== undefined && syncedSlot >= waitForSlot;
733
+ }, `archiver sync past slot ${waitForSlot}`, timeoutSeconds, 0.2);
734
+ } catch {
735
+ this.log.warn(`Archiver did not sync L1 past slot ${waitForSlot} before slot ${this.targetSlot} expired, discarding pipelined work`, {
736
+ checkpointNumber: this.checkpointNumber
737
+ });
738
+ this.emitPipelinedCheckpointDiscarded('archiver-sync-timeout');
739
+ return false;
740
+ }
741
+ }
742
+ /**
743
+ * Waits for the parent checkpoint to land on L1 before submitting a pipelined checkpoint.
744
+ * Polls until the archiver has synced L1 past the parent's slot, then verifies:
745
+ * - If we built on a proposed parent: it must have landed on L1 with matching hash and valid attestations.
746
+ * - If we built without a proposed parent: no new checkpoint must have appeared for that slot.
747
+ * If the parent has invalid attestations, enqueues an invalidation. Returns whether to proceed with the proposal.
748
+ */ async waitForValidParentCheckpointOnL1() {
749
+ const parentCheckpointNumber = CheckpointNumber(this.checkpointNumber - 1);
750
+ // Wait until archiver has synced L1 past the parent's slot (slotNow)
751
+ if (!await this.waitForSyncedL2SlotNumber(this.slotNow)) {
752
+ return false;
753
+ }
754
+ const tips = await this.l2BlockSource.getL2Tips();
755
+ const checkpointedNumber = tips.checkpointed.checkpoint.number;
756
+ // We built on top of a proposed checkpoint. Verify it landed on L1 as expected.
757
+ if (this.proposedCheckpointData) {
758
+ // After syncing from L1 we see the chain tip has invalid attestations. This means the parent checkpoint was posted
759
+ // with invalid attestations, or it built on top of something with invalid attestations and didnt invalidate them.
760
+ // Either way, we thought our parent would be valid, so we have to throw away our work. But at least we'll try and
761
+ // invalidate on L1 so we clean up the chain for the next proposer. And we'll slash them, but that's handled elsewhere.
762
+ const validationStatus = await this.l2BlockSource.getPendingChainValidationStatus();
763
+ if (!validationStatus.valid) {
764
+ this.log.warn(`Parent checkpoint ${parentCheckpointNumber} has invalid attestations, discarding pipelined work`, {
765
+ checkpointNumber: this.checkpointNumber,
766
+ reason: validationStatus.reason
767
+ });
768
+ this.emitPipelinedCheckpointDiscarded('parent-invalid-attestations');
769
+ await this.enqueueInvalidation(validationStatus);
770
+ return false;
771
+ }
772
+ // The pending chain is valid. But did the parent checkpoint land on L1 at all?
773
+ if (checkpointedNumber < parentCheckpointNumber) {
774
+ this.log.warn(`Parent checkpoint ${parentCheckpointNumber} did not land on L1, discarding pipelined work`, {
775
+ checkpointNumber: this.checkpointNumber,
776
+ checkpointedNumber
777
+ });
778
+ this.emitPipelinedCheckpointDiscarded('parent-not-on-l1');
779
+ return false;
780
+ }
781
+ // It landed. But is it the one we were expecting?
782
+ const expectedHash = this.proposedCheckpointData.header.hash().toString();
783
+ if (tips.checkpointed.checkpoint.hash !== expectedHash) {
784
+ this.log.warn(`Parent checkpoint ${parentCheckpointNumber} hash mismatch on L1, discarding pipelined work`, {
785
+ checkpointNumber: this.checkpointNumber,
786
+ expectedHash,
787
+ actualHash: tips.checkpointed.checkpoint.hash
788
+ });
789
+ this.emitPipelinedCheckpointDiscarded('parent-hash-mismatch');
790
+ return false;
791
+ }
792
+ return true;
793
+ } else {
794
+ // We didn't see a proposed checkpoint at build time, so we built on checkpointed parent from two slots ago.
795
+ // But if a new checkpoint for the previous slot appeared on L1 in the meantime, our checkpoint assumed the wrong parent,
796
+ // so we have to discard our work. This can happen if we're somehow cut off from p2p and fail to see the checkpoint
797
+ // proposal for the previous slot.
798
+ if (checkpointedNumber > parentCheckpointNumber) {
799
+ this.log.warn(`Unexpected checkpoint ${checkpointedNumber} landed on L1 after we built on top of parent ${parentCheckpointNumber}, discarding pipelined work`, {
800
+ checkpointNumber: this.checkpointNumber,
801
+ checkpointedNumber
802
+ });
803
+ this.emitPipelinedCheckpointDiscarded('unexpected-parent-appeared');
804
+ return false;
805
+ }
806
+ return true;
807
+ }
808
+ }
809
+ /** Emits the pipelined-checkpoint-discarded event and records the metric. */ emitPipelinedCheckpointDiscarded(reason) {
810
+ this.metrics.recordPipelineParentCheckpointMismatch(reason);
811
+ this.eventEmitter.emit('pipelined-checkpoint-discarded', {
812
+ slot: this.targetSlot,
813
+ checkpointNumber: this.checkpointNumber,
814
+ reason
680
815
  });
681
816
  }
817
+ /** Simulates and enqueues an invalidation request for the invalid parent checkpoint. */ async enqueueInvalidation(validationStatus) {
818
+ if (this.config.skipInvalidateBlockAsProposer) {
819
+ this.log.warn(`Skipping checkpoint invalidation as proposer due to test configuration`);
820
+ return;
821
+ }
822
+ const invalidateRequest = await this.publisher.simulateInvalidateCheckpoint(validationStatus);
823
+ if (invalidateRequest) {
824
+ const submissionSlotStart = Number(getTimestampForSlot(this.targetSlot, this.l1Constants));
825
+ const txTimeoutAt = new Date((submissionSlotStart + this.l1Constants.slotDuration) * 1000);
826
+ this.publisher.enqueueInvalidateCheckpoint(invalidateRequest, {
827
+ txTimeoutAt
828
+ });
829
+ } else {
830
+ this.log.info(`Invalidation simulation returned undefined, checkpoint may have been removed already`, {
831
+ checkpointNumber: this.checkpointNumber
832
+ });
833
+ }
834
+ }
682
835
  async proposeCheckpoint() {
683
836
  try {
684
837
  const env = {
@@ -687,6 +840,15 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
687
840
  hasError: false
688
841
  };
689
842
  try {
843
+ const now = this.dateProvider.now();
844
+ if (this.epochCache.isProposerPipeliningEnabled() && this.proposedCheckpointData) {
845
+ // Measure against the wall-clock slot whose build window we are currently using.
846
+ // In pipelining mode `targetSlot` is intentionally one slot ahead, which makes the
847
+ // target-slot boundary a full slot away from the actual build start time.
848
+ const slotBoundaryMs = Number(getTimestampForSlot(this.slotNow, this.l1Constants)) * 1000;
849
+ this.checkpointMetrics.recordPipelinedCheckpointBuildStartOffsetFromSlotBoundary(now - slotBoundaryMs);
850
+ }
851
+ this.checkpointMetrics.startCheckpointTiming(now);
690
852
  // Get operator configured coinbase and fee recipient for this attestor
691
853
  const coinbase = this.validatorClient.getCoinbaseForAttestor(this.attestorAddress);
692
854
  const feeRecipient = this.validatorClient.getFeeRecipientForAttestor(this.attestorAddress);
@@ -708,15 +870,13 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
708
870
  // When pipelining, force the proposed checkpoint number and fee header to our parent so the
709
871
  // fee computation sees the same chain tip that L1 will see once the previous pipelined checkpoint lands.
710
872
  const isPipelining = this.epochCache.isProposerPipeliningEnabled();
711
- const parentCheckpointNumber = isPipelining ? CheckpointNumber(this.checkpointNumber - 1) : undefined;
712
- // Compute the parent's fee header override when pipelining
713
- if (isPipelining && this.proposedCheckpointData) {
714
- this.computedForceProposedFeeHeader = await this.computeForceProposedFeeHeader(parentCheckpointNumber);
715
- }
716
- const checkpointGlobalVariables = await this.globalsBuilder.buildCheckpointGlobalVariables(coinbase, feeRecipient, this.targetSlot, {
717
- forcePendingCheckpointNumber: parentCheckpointNumber,
718
- forceProposedFeeHeader: this.computedForceProposedFeeHeader
719
- });
873
+ this.pipelinedParentSimulationOverridesPlan = isPipelining ? await buildPipelinedParentSimulationOverridesPlan({
874
+ checkpointNumber: this.checkpointNumber,
875
+ proposedCheckpointData: this.proposedCheckpointData,
876
+ rollup: this.publisher.rollupContract,
877
+ log: this.log
878
+ }) : undefined;
879
+ const checkpointGlobalVariables = await this.globalsBuilder.buildCheckpointGlobalVariables(coinbase, feeRecipient, this.targetSlot, this.pipelinedParentSimulationOverridesPlan);
720
880
  // Collect L1 to L2 messages for the checkpoint and compute their hash
721
881
  const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(this.checkpointNumber);
722
882
  const inHash = computeInHashFromL1ToL2Messages(l1ToL2Messages);
@@ -794,8 +954,8 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
794
954
  return undefined;
795
955
  }
796
956
  // Record checkpoint-level build metrics
797
- this.metrics.recordCheckpointBuild(checkpointBuildTimer.ms(), blocksInCheckpoint.length, checkpoint.getStats().txCount, Number(checkpoint.header.totalManaUsed.toBigInt()));
798
- // Do not collect attestations nor publish to L1 in fisherman mode
957
+ this.checkpointMetrics.recordCheckpointBuild(checkpointBuildTimer.ms(), blocksInCheckpoint.length, checkpoint.getStats().txCount, Number(checkpoint.header.totalManaUsed.toBigInt()));
958
+ // In fisherman mode, return the checkpoint without broadcasting or collecting attestations
799
959
  if (this.config.fishermanMode) {
800
960
  this.log.info(`Built checkpoint for slot ${this.targetSlot} with ${blocksInCheckpoint.length} blocks. ` + `Skipping proposal in fisherman mode.`, {
801
961
  slot: this.targetSlot,
@@ -803,44 +963,23 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
803
963
  blocksBuilt: blocksInCheckpoint.length
804
964
  });
805
965
  this.metrics.recordCheckpointSuccess();
966
+ // Return a broadcast result with a dummy proposal — fisherman mode skips attestation collection
806
967
  return {
807
968
  checkpoint,
808
- attestations: CommitteeAttestationsAndSigners.empty(),
809
- attestationsSignature: Signature.empty()
969
+ proposal: undefined,
970
+ blockProposedAt: this.dateProvider.now()
810
971
  };
811
972
  }
812
- // Include the block pending broadcast in the checkpoint proposal if any
813
- const lastBlock = blockPendingBroadcast && {
814
- blockHeader: blockPendingBroadcast.block.header,
815
- indexWithinCheckpoint: blockPendingBroadcast.block.indexWithinCheckpoint,
816
- txs: blockPendingBroadcast.txs
817
- };
818
973
  // Create the checkpoint proposal and broadcast it
819
- const proposal = await this.validatorClient.createCheckpointProposal(checkpoint.header, checkpoint.archive.root, feeAssetPriceModifier, lastBlock, this.proposer, checkpointProposalOptions);
974
+ const proposal = await this.validatorClient.createCheckpointProposal(checkpoint.header, checkpoint.archive.root, this.checkpointNumber, feeAssetPriceModifier, blockPendingBroadcast, this.proposer, checkpointProposalOptions);
820
975
  const blockProposedAt = this.dateProvider.now();
821
976
  await this.p2pClient.broadcastCheckpointProposal(proposal);
822
- this.setStateFn(SequencerState.COLLECTING_ATTESTATIONS, this.targetSlot);
823
- const attestations = await this.waitForAttestations(proposal);
824
- const blockAttestedAt = this.dateProvider.now();
825
- this.metrics.recordCheckpointAttestationDelay(blockAttestedAt - blockProposedAt);
826
- // Proposer must sign over the attestations before pushing them to L1
827
- const signer = this.proposer ?? this.publisher.getSenderAddress();
828
- let attestationsSignature;
829
- try {
830
- attestationsSignature = await this.validatorClient.signAttestationsAndSigners(attestations, signer, this.targetSlot, this.checkpointNumber);
831
- } catch (err) {
832
- // We shouldn't really get here since we yield to another HA node
833
- // as soon as we see these errors when creating block or checkpoint proposals.
834
- if (this.handleHASigningError(err, 'Attestations signature')) {
835
- return undefined;
836
- }
837
- throw err;
838
- }
839
- // Return the result for the caller to enqueue after the pipeline sleep
977
+ this.checkpointMetrics.noteCheckpointBroadcast(this.dateProvider.now());
978
+ // Return immediately after broadcast — attestation collection happens in the background
840
979
  return {
841
980
  checkpoint,
842
- attestations,
843
- attestationsSignature
981
+ proposal,
982
+ blockProposedAt
844
983
  };
845
984
  } catch (e) {
846
985
  env.error = e;
@@ -891,17 +1030,19 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
891
1030
  indexWithinCheckpoint,
892
1031
  txHashesAlreadyIncluded
893
1032
  });
894
- // TODO(palla/mbps): Review these conditions. We may want to keep trying in some scenarios.
895
- if (!buildResult && timingInfo.isLastBlock) {
896
- break;
897
- } else if (!buildResult && timingInfo.deadline !== undefined) {
898
- // But if there is still time for more blocks, wait until the next subslot and try again
1033
+ // If we failed to build the block due to insufficient txs, we try again if there is still time left in the slot
1034
+ if ('failure' in buildResult) {
1035
+ // If this was the last subslot, or we're running with a single block per slot, we're done
1036
+ if (timingInfo.isLastBlock || timingInfo.deadline === undefined) {
1037
+ break;
1038
+ }
1039
+ // Otherwise, if there is still time for more blocks, we wait until the next subslot and try again
899
1040
  await this.waitUntilNextSubslot(timingInfo.deadline);
900
1041
  continue;
901
- } else if (!buildResult) {
902
- break;
903
- } else if ('error' in buildResult) {
904
- // If there was an error building the block, just exit the loop and give up the rest of the slot
1042
+ }
1043
+ // If there was an error building the block, we just exit the loop and give up the rest of the slot.
1044
+ // We don't want to risk building more blocks if something went wrong.
1045
+ if ('error' in buildResult) {
905
1046
  if (!(buildResult.error instanceof SequencerInterruptedError)) {
906
1047
  this.log.warn(`Halting block building for slot ${this.targetSlot}`, {
907
1048
  slot: this.targetSlot,
@@ -912,32 +1053,29 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
912
1053
  break;
913
1054
  }
914
1055
  const { block, usedTxs } = buildResult;
1056
+ this.checkpointMetrics.noteCheckpointBlockBuilt(this.dateProvider.now(), {
1057
+ isFirstBlock: blocksBuilt === 0,
1058
+ isLastBlock: timingInfo.isLastBlock
1059
+ });
915
1060
  blocksInCheckpoint.push(block);
916
1061
  usedTxs.forEach((tx)=>txHashesAlreadyIncluded.add(tx.txHash.toString()));
917
- // If this is the last block, sync it to the archiver and exit the loop
918
- // so we can build the checkpoint and start collecting attestations.
1062
+ // Sign the block proposal. This will throw if HA signing fails.
1063
+ const proposal = await this.createBlockProposal(block, inHash, usedTxs, blockProposalOptions);
1064
+ // Sync the proposed block to the archiver to make it available, only after we've managed to sign the proposal,
1065
+ // so we avoid polluting our archive with a block that would fail.
1066
+ // We wait for the sync to succeed, as this helps catch consistency errors, even if it means we lose some time for block-building.
1067
+ // If this throws, we abort the entire checkpoint.
1068
+ await this.syncProposedBlockToArchiver(block);
1069
+ // If this is the last block, do not broadcast it, since it will be included in the checkpoint proposal.
919
1070
  if (timingInfo.isLastBlock) {
920
- await this.syncProposedBlockToArchiver(block);
921
1071
  this.log.verbose(`Completed final block ${blockNumber} for slot ${this.targetSlot}`, {
922
1072
  slot: this.targetSlot,
923
1073
  blockNumber,
924
1074
  blocksBuilt
925
1075
  });
926
- blockPendingBroadcast = {
927
- block,
928
- txs: usedTxs
929
- };
1076
+ blockPendingBroadcast = proposal;
930
1077
  break;
931
1078
  }
932
- // Broadcast the block proposal (unless we're in fisherman mode) unless the block is the last one,
933
- // in which case we'll broadcast it along with the checkpoint at the end of the loop.
934
- // Note that we only send the block to the archiver if we manage to create the proposal, so if there's
935
- // a HA error we don't pollute our archiver with a block that won't make it to the chain.
936
- const proposal = await this.createBlockProposal(block, inHash, usedTxs, blockProposalOptions);
937
- // Sync the proposed block to the archiver to make it available, only after we've managed to sign the proposal.
938
- // We wait for the sync to succeed, as this helps catch consistency errors, even if it means we lose some time for block-building.
939
- // If this throws, we abort the entire checkpoint.
940
- await this.syncProposedBlockToArchiver(block);
941
1079
  // Once we have a signed proposal and the archiver agreed with our proposed block, then we broadcast it.
942
1080
  proposal && await this.p2pClient.broadcastProposal(proposal);
943
1081
  // Wait until the next block's start time
@@ -957,7 +1095,7 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
957
1095
  this.log.info(`Skipping block proposal for block ${block.number} in fisherman mode`);
958
1096
  return Promise.resolve(undefined);
959
1097
  }
960
- return this.validatorClient.createBlockProposal(block.header, block.indexWithinCheckpoint, inHash, block.archive.root, usedTxs, this.proposer, blockProposalOptions);
1098
+ return this.validatorClient.createBlockProposal(block.header, this.checkpointNumber, block.indexWithinCheckpoint, inHash, block.archive.root, usedTxs, this.proposer, blockProposalOptions);
961
1099
  }
962
1100
  /** Sleeps until it is time to produce the next block in the slot */ async waitUntilNextSubslot(nextSubslotStart) {
963
1101
  this.setStateFn(SequencerState.WAITING_UNTIL_NEXT_BLOCK, this.targetSlot);
@@ -987,7 +1125,9 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
987
1125
  slot: this.targetSlot
988
1126
  });
989
1127
  this.metrics.recordBlockProposalFailed('insufficient_txs');
990
- return undefined;
1128
+ return {
1129
+ failure: 'insufficient-txs'
1130
+ };
991
1131
  }
992
1132
  // Create iterator to pending txs. We filter out txs already included in previous blocks in the checkpoint
993
1133
  // just in case p2p failed to sync the provisional block and didn't get to remove those txs from the mempool yet.
@@ -1030,7 +1170,9 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
1030
1170
  slot: this.targetSlot
1031
1171
  });
1032
1172
  this.metrics.recordBlockProposalFailed('insufficient_valid_txs');
1033
- return undefined;
1173
+ return {
1174
+ failure: 'insufficient-valid-txs'
1175
+ };
1034
1176
  }
1035
1177
  // Block creation succeeded, emit stats and metrics
1036
1178
  const { block, publicProcessorDuration, usedTxs, blockBuildDuration, numTxs } = buildResult;
@@ -1056,7 +1198,7 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
1056
1198
  slot: this.targetSlot,
1057
1199
  buildSlot: this.slotNow
1058
1200
  });
1059
- this.metrics.recordBuiltBlock(blockBuildDuration, block.header.totalManaUsed.toNumberUnsafe());
1201
+ this.metrics.recordBuiltBlock(blockBuildDuration, block.header.totalManaUsed.toNumberUnsafe(), this.targetSlot);
1060
1202
  return {
1061
1203
  block,
1062
1204
  usedTxs
@@ -1131,6 +1273,30 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
1131
1273
  minTxs
1132
1274
  };
1133
1275
  }
1276
+ async getSignedCommitteeAttestations(broadcast) {
1277
+ const { proposal, blockProposedAt } = broadcast;
1278
+ this.setStateFn(SequencerState.COLLECTING_ATTESTATIONS, this.targetSlot);
1279
+ const attestations = await this.waitForAttestations(proposal);
1280
+ if (!attestations) {
1281
+ return undefined;
1282
+ }
1283
+ this.checkpointMetrics.recordCheckpointAttestationDelay(this.dateProvider.now() - blockProposedAt);
1284
+ // Proposer must sign over the attestations before pushing them to L1
1285
+ const signer = this.proposer ?? this.publisher.getSenderAddress();
1286
+ try {
1287
+ const attestationsSignature = await this.validatorClient.signAttestationsAndSigners(attestations, signer, this.targetSlot, this.checkpointNumber);
1288
+ return {
1289
+ attestations,
1290
+ attestationsSignature
1291
+ };
1292
+ } catch (err) {
1293
+ if (this.handleHASigningError(err, 'Attestations signature')) {
1294
+ return;
1295
+ }
1296
+ this.log.error(`Error signing attestations for checkpoint proposal at slot ${proposal.slotNumber}`, err);
1297
+ return undefined;
1298
+ }
1299
+ }
1134
1300
  /**
1135
1301
  * Waits for enough attestations to be collected via p2p.
1136
1302
  * This is run after all blocks for the checkpoint have been built.
@@ -1154,16 +1320,16 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
1154
1320
  const numberOfRequiredAttestations = computeQuorum(committee.length);
1155
1321
  if (this.config.skipCollectingAttestations) {
1156
1322
  this.log.warn('Skipping attestation collection as per config (attesting with own keys only)');
1157
- const attestations = await this.validatorClient?.collectOwnAttestations(proposal);
1323
+ const attestations = await this.validatorClient?.collectOwnAttestations(proposal, this.checkpointNumber);
1158
1324
  return new CommitteeAttestationsAndSigners(orderAttestations(attestations ?? [], committee));
1159
1325
  }
1160
- const attestationTimeAllowed = this.config.enforceTimeTable ? this.timetable.getMaxAllowedTime(SequencerState.PUBLISHING_CHECKPOINT) : this.l1Constants.slotDuration;
1326
+ const attestationTimeAllowed = this.config.enforceTimeTable ? this.timetable.getCheckpointAttestationDeadline() : this.l1Constants.slotDuration;
1161
1327
  const attestationDeadline = new Date((this.getSlotStartBuildTimestamp() + attestationTimeAllowed) * 1000);
1162
1328
  this.metrics.recordRequiredAttestations(numberOfRequiredAttestations, attestationTimeAllowed);
1163
1329
  const collectAttestationsTimer = new Timer();
1164
1330
  let collectedAttestationsCount = 0;
1165
1331
  try {
1166
- const attestations = await this.validatorClient.collectAttestations(proposal, numberOfRequiredAttestations, attestationDeadline);
1332
+ const attestations = await this.validatorClient.collectAttestations(proposal, numberOfRequiredAttestations, attestationDeadline, this.checkpointNumber);
1167
1333
  collectedAttestationsCount = attestations.length;
1168
1334
  // Trim attestations to minimum required to save L1 calldata gas
1169
1335
  const localAddresses = this.validatorClient.getValidatorAddresses();
@@ -1181,8 +1347,11 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
1181
1347
  } catch (err) {
1182
1348
  if (err && err instanceof AttestationTimeoutError) {
1183
1349
  collectedAttestationsCount = err.collectedCount;
1350
+ this.log.error(`Timeout while waiting for attestations for checkpoint proposal at slot ${proposal.slotNumber} (collected ${collectedAttestationsCount}/${numberOfRequiredAttestations})`, err);
1351
+ } else {
1352
+ this.log.error(`Error collecting attestations for checkpoint proposal at slot ${proposal.slotNumber}`, err);
1184
1353
  }
1185
- throw err;
1354
+ return undefined;
1186
1355
  } finally{
1187
1356
  this.metrics.recordCollectedAttestations(collectedAttestationsCount, collectAttestationsTimer.ms());
1188
1357
  }
@@ -1256,8 +1425,12 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
1256
1425
  * Adds the proposed block to the archiver so it's available via P2P.
1257
1426
  * Gossip doesn't echo messages back to the sender, so the proposer's archiver/world-state
1258
1427
  * would never receive its own block without this explicit sync.
1428
+ *
1429
+ * In fisherman mode we skip this push: the fisherman builds blocks locally for validation
1430
+ * and fee analysis only, and pushing them to the archiver causes spurious reorg cascades
1431
+ * whenever the real proposer's block arrives from L1.
1259
1432
  */ async syncProposedBlockToArchiver(block) {
1260
- if (this.config.skipPushProposedBlocksToArchiver) {
1433
+ if (this.config.skipPushProposedBlocksToArchiver || this.config.fishermanMode) {
1261
1434
  this.log.warn(`Skipping push of proposed block ${block.number} to archiver`, {
1262
1435
  blockNumber: block.number,
1263
1436
  slot: block.header.globalVariables.slotNumber
@@ -1309,40 +1482,6 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
1309
1482
  }
1310
1483
  return false;
1311
1484
  }
1312
- /**
1313
- * In times of congestion we need to simulate using the correct fee header override for the previous block
1314
- * We calculate the correct fee header values.
1315
- *
1316
- * If we are in block 1, or the checkpoint we are querying does not exist, we return undefined. However
1317
- * If we are pipelining - where this function is called, the grandparentCheckpointNumber should always exist
1318
- * @param parentCheckpointNumber
1319
- * @returns
1320
- */ async computeForceProposedFeeHeader(parentCheckpointNumber) {
1321
- if (!this.proposedCheckpointData) {
1322
- return undefined;
1323
- }
1324
- const rollup = this.publisher.rollupContract;
1325
- const grandparentCheckpointNumber = CheckpointNumber(this.checkpointNumber - 2);
1326
- try {
1327
- const [grandparentCheckpoint, manaTarget] = await Promise.all([
1328
- rollup.getCheckpoint(grandparentCheckpointNumber),
1329
- rollup.getManaTarget()
1330
- ]);
1331
- if (!grandparentCheckpoint || !grandparentCheckpoint.feeHeader) {
1332
- this.log.error(`Grandparent checkpoint or its feeHeader is undefined for checkpointNumber=${grandparentCheckpointNumber.toString()}`);
1333
- return undefined;
1334
- } else {
1335
- const parentFeeHeader = RollupContract.computeChildFeeHeader(grandparentCheckpoint.feeHeader, this.proposedCheckpointData.totalManaUsed, this.proposedCheckpointData.feeAssetPriceModifier, manaTarget);
1336
- return {
1337
- checkpointNumber: parentCheckpointNumber,
1338
- feeHeader: parentFeeHeader
1339
- };
1340
- }
1341
- } catch (err) {
1342
- this.log.error(`Failed to fetch grandparent checkpoint or mana target for checkpointNumber=${grandparentCheckpointNumber.toString()}: ${err}`);
1343
- return undefined;
1344
- }
1345
- }
1346
1485
  /** Waits until a specific time within the current slot */ async waitUntilTimeInSlot(targetSecondsIntoSlot) {
1347
1486
  const slotStartTimestamp = this.getSlotStartBuildTimestamp();
1348
1487
  const targetTimestamp = slotStartTimestamp + targetSecondsIntoSlot;