@aztec/sequencer-client 0.0.1-commit.f650c0a5c → 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 (65) 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 -0
  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.d.ts +10 -28
  25. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  26. package/dest/publisher/sequencer-publisher.js +24 -43
  27. package/dest/sequencer/chain_state_overrides.d.ts +25 -0
  28. package/dest/sequencer/chain_state_overrides.d.ts.map +1 -0
  29. package/dest/sequencer/chain_state_overrides.js +39 -0
  30. package/dest/sequencer/checkpoint_proposal_job.d.ts +24 -21
  31. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  32. package/dest/sequencer/checkpoint_proposal_job.js +263 -120
  33. package/dest/sequencer/checkpoint_proposal_job_metrics.d.ts +34 -0
  34. package/dest/sequencer/checkpoint_proposal_job_metrics.d.ts.map +1 -0
  35. package/dest/sequencer/checkpoint_proposal_job_metrics.js +72 -0
  36. package/dest/sequencer/events.d.ts +6 -1
  37. package/dest/sequencer/events.d.ts.map +1 -1
  38. package/dest/sequencer/metrics.d.ts +7 -10
  39. package/dest/sequencer/metrics.d.ts.map +1 -1
  40. package/dest/sequencer/metrics.js +27 -20
  41. package/dest/sequencer/sequencer.d.ts +5 -2
  42. package/dest/sequencer/sequencer.d.ts.map +1 -1
  43. package/dest/sequencer/sequencer.js +15 -11
  44. package/dest/sequencer/timetable.d.ts +11 -1
  45. package/dest/sequencer/timetable.d.ts.map +1 -1
  46. package/dest/sequencer/timetable.js +42 -43
  47. package/package.json +27 -27
  48. package/src/client/sequencer-client.ts +5 -0
  49. package/src/config.ts +8 -11
  50. package/src/global_variable_builder/README.md +44 -0
  51. package/src/global_variable_builder/fee_predictor.ts +172 -0
  52. package/src/global_variable_builder/fee_provider.ts +75 -0
  53. package/src/global_variable_builder/global_builder.ts +7 -54
  54. package/src/global_variable_builder/index.ts +2 -0
  55. package/src/publisher/config.ts +6 -4
  56. package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +3 -1
  57. package/src/publisher/sequencer-publisher.ts +49 -83
  58. package/src/sequencer/README.md +9 -8
  59. package/src/sequencer/chain_state_overrides.ts +87 -0
  60. package/src/sequencer/checkpoint_proposal_job.ts +328 -162
  61. package/src/sequencer/checkpoint_proposal_job_metrics.ts +128 -0
  62. package/src/sequencer/events.ts +5 -0
  63. package/src/sequencer/metrics.ts +35 -25
  64. package/src/sequencer/sequencer.ts +22 -11
  65. package/src/sequencer/timetable.ts +50 -49
@@ -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,73 +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
- // Schedule L1 submission in the background so the work loop returns immediately.
627
- // The publisher will sleep until submitAfter, then send the bundled requests.
628
- // The promise is stored so it can be awaited during shutdown.
629
- 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);
630
662
  const proposedAction = l1Response?.successfulActions.find((a)=>a === 'propose');
631
663
  if (proposedAction) {
632
664
  this.eventEmitter.emit('checkpoint-published', {
633
665
  checkpoint: this.checkpointNumber,
634
666
  slot: this.targetSlot
635
667
  });
636
- const coinbase = checkpoint?.header.coinbase;
668
+ const coinbase = checkpoint.header.coinbase;
637
669
  await this.metrics.incFilledSlot(this.publisher.getSenderAddress().toString(), coinbase);
638
- } else if (checkpoint) {
670
+ } else {
639
671
  this.eventEmitter.emit('checkpoint-publish-failed', {
640
672
  ...l1Response,
641
673
  slot: this.targetSlot
642
674
  });
643
- if (this.epochCache.isProposerPipeliningEnabled()) {
675
+ if (isPipelining) {
644
676
  this.metrics.recordPipelineDiscard();
645
677
  }
646
678
  }
647
- }).catch((err)=>{
648
- this.log.error(`Background L1 submission failed for slot ${this.targetSlot}`, err);
649
- if (checkpoint) {
650
- this.eventEmitter.emit('checkpoint-publish-failed', {
651
- slot: this.targetSlot
652
- });
653
- if (this.epochCache.isProposerPipeliningEnabled()) {
654
- this.metrics.recordPipelineDiscard();
655
- }
679
+ } catch (err) {
680
+ if (err instanceof SequencerInterruptedError) {
681
+ return;
656
682
  }
657
- });
658
- // Return the built checkpoint immediately — the work loop is now unblocked
659
- 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
+ }
660
691
  }
661
692
  /** Enqueues the checkpoint for L1 submission. Called after pipeline sleep in execute(). */ async enqueueCheckpointForSubmission(result) {
662
693
  const { checkpoint, attestations, attestationsSignature } = result;
@@ -672,12 +703,135 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
672
703
  return;
673
704
  }
674
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
+ });
675
713
  await this.publisher.enqueueProposeCheckpoint(checkpoint, attestations, attestationsSignature, {
676
714
  txTimeoutAt,
677
- forcePendingCheckpointNumber: this.invalidateCheckpoint?.forcePendingCheckpointNumber,
678
- 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
679
815
  });
680
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
+ }
681
835
  async proposeCheckpoint() {
682
836
  try {
683
837
  const env = {
@@ -686,6 +840,15 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
686
840
  hasError: false
687
841
  };
688
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);
689
852
  // Get operator configured coinbase and fee recipient for this attestor
690
853
  const coinbase = this.validatorClient.getCoinbaseForAttestor(this.attestorAddress);
691
854
  const feeRecipient = this.validatorClient.getFeeRecipientForAttestor(this.attestorAddress);
@@ -707,15 +870,13 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
707
870
  // When pipelining, force the proposed checkpoint number and fee header to our parent so the
708
871
  // fee computation sees the same chain tip that L1 will see once the previous pipelined checkpoint lands.
709
872
  const isPipelining = this.epochCache.isProposerPipeliningEnabled();
710
- const parentCheckpointNumber = isPipelining ? CheckpointNumber(this.checkpointNumber - 1) : undefined;
711
- // Compute the parent's fee header override when pipelining
712
- if (isPipelining && this.proposedCheckpointData) {
713
- this.computedForceProposedFeeHeader = await this.computeForceProposedFeeHeader(parentCheckpointNumber);
714
- }
715
- const checkpointGlobalVariables = await this.globalsBuilder.buildCheckpointGlobalVariables(coinbase, feeRecipient, this.targetSlot, {
716
- forcePendingCheckpointNumber: parentCheckpointNumber,
717
- forceProposedFeeHeader: this.computedForceProposedFeeHeader
718
- });
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);
719
880
  // Collect L1 to L2 messages for the checkpoint and compute their hash
720
881
  const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(this.checkpointNumber);
721
882
  const inHash = computeInHashFromL1ToL2Messages(l1ToL2Messages);
@@ -793,8 +954,8 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
793
954
  return undefined;
794
955
  }
795
956
  // Record checkpoint-level build metrics
796
- this.metrics.recordCheckpointBuild(checkpointBuildTimer.ms(), blocksInCheckpoint.length, checkpoint.getStats().txCount, Number(checkpoint.header.totalManaUsed.toBigInt()));
797
- // 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
798
959
  if (this.config.fishermanMode) {
799
960
  this.log.info(`Built checkpoint for slot ${this.targetSlot} with ${blocksInCheckpoint.length} blocks. ` + `Skipping proposal in fisherman mode.`, {
800
961
  slot: this.targetSlot,
@@ -802,38 +963,23 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
802
963
  blocksBuilt: blocksInCheckpoint.length
803
964
  });
804
965
  this.metrics.recordCheckpointSuccess();
966
+ // Return a broadcast result with a dummy proposal — fisherman mode skips attestation collection
805
967
  return {
806
968
  checkpoint,
807
- attestations: CommitteeAttestationsAndSigners.empty(),
808
- attestationsSignature: Signature.empty()
969
+ proposal: undefined,
970
+ blockProposedAt: this.dateProvider.now()
809
971
  };
810
972
  }
811
973
  // Create the checkpoint proposal and broadcast it
812
- const proposal = await this.validatorClient.createCheckpointProposal(checkpoint.header, checkpoint.archive.root, feeAssetPriceModifier, blockPendingBroadcast, this.proposer, checkpointProposalOptions);
974
+ const proposal = await this.validatorClient.createCheckpointProposal(checkpoint.header, checkpoint.archive.root, this.checkpointNumber, feeAssetPriceModifier, blockPendingBroadcast, this.proposer, checkpointProposalOptions);
813
975
  const blockProposedAt = this.dateProvider.now();
814
976
  await this.p2pClient.broadcastCheckpointProposal(proposal);
815
- this.setStateFn(SequencerState.COLLECTING_ATTESTATIONS, this.targetSlot);
816
- const attestations = await this.waitForAttestations(proposal);
817
- const blockAttestedAt = this.dateProvider.now();
818
- this.metrics.recordCheckpointAttestationDelay(blockAttestedAt - blockProposedAt);
819
- // Proposer must sign over the attestations before pushing them to L1
820
- const signer = this.proposer ?? this.publisher.getSenderAddress();
821
- let attestationsSignature;
822
- try {
823
- attestationsSignature = await this.validatorClient.signAttestationsAndSigners(attestations, signer, this.targetSlot, this.checkpointNumber);
824
- } catch (err) {
825
- // We shouldn't really get here since we yield to another HA node
826
- // as soon as we see these errors when creating block or checkpoint proposals.
827
- if (this.handleHASigningError(err, 'Attestations signature')) {
828
- return undefined;
829
- }
830
- throw err;
831
- }
832
- // 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
833
979
  return {
834
980
  checkpoint,
835
- attestations,
836
- attestationsSignature
981
+ proposal,
982
+ blockProposedAt
837
983
  };
838
984
  } catch (e) {
839
985
  env.error = e;
@@ -907,6 +1053,10 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
907
1053
  break;
908
1054
  }
909
1055
  const { block, usedTxs } = buildResult;
1056
+ this.checkpointMetrics.noteCheckpointBlockBuilt(this.dateProvider.now(), {
1057
+ isFirstBlock: blocksBuilt === 0,
1058
+ isLastBlock: timingInfo.isLastBlock
1059
+ });
910
1060
  blocksInCheckpoint.push(block);
911
1061
  usedTxs.forEach((tx)=>txHashesAlreadyIncluded.add(tx.txHash.toString()));
912
1062
  // Sign the block proposal. This will throw if HA signing fails.
@@ -945,7 +1095,7 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
945
1095
  this.log.info(`Skipping block proposal for block ${block.number} in fisherman mode`);
946
1096
  return Promise.resolve(undefined);
947
1097
  }
948
- 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);
949
1099
  }
950
1100
  /** Sleeps until it is time to produce the next block in the slot */ async waitUntilNextSubslot(nextSubslotStart) {
951
1101
  this.setStateFn(SequencerState.WAITING_UNTIL_NEXT_BLOCK, this.targetSlot);
@@ -1048,7 +1198,7 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
1048
1198
  slot: this.targetSlot,
1049
1199
  buildSlot: this.slotNow
1050
1200
  });
1051
- this.metrics.recordBuiltBlock(blockBuildDuration, block.header.totalManaUsed.toNumberUnsafe());
1201
+ this.metrics.recordBuiltBlock(blockBuildDuration, block.header.totalManaUsed.toNumberUnsafe(), this.targetSlot);
1052
1202
  return {
1053
1203
  block,
1054
1204
  usedTxs
@@ -1123,6 +1273,30 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
1123
1273
  minTxs
1124
1274
  };
1125
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
+ }
1126
1300
  /**
1127
1301
  * Waits for enough attestations to be collected via p2p.
1128
1302
  * This is run after all blocks for the checkpoint have been built.
@@ -1146,16 +1320,16 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
1146
1320
  const numberOfRequiredAttestations = computeQuorum(committee.length);
1147
1321
  if (this.config.skipCollectingAttestations) {
1148
1322
  this.log.warn('Skipping attestation collection as per config (attesting with own keys only)');
1149
- const attestations = await this.validatorClient?.collectOwnAttestations(proposal);
1323
+ const attestations = await this.validatorClient?.collectOwnAttestations(proposal, this.checkpointNumber);
1150
1324
  return new CommitteeAttestationsAndSigners(orderAttestations(attestations ?? [], committee));
1151
1325
  }
1152
- 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;
1153
1327
  const attestationDeadline = new Date((this.getSlotStartBuildTimestamp() + attestationTimeAllowed) * 1000);
1154
1328
  this.metrics.recordRequiredAttestations(numberOfRequiredAttestations, attestationTimeAllowed);
1155
1329
  const collectAttestationsTimer = new Timer();
1156
1330
  let collectedAttestationsCount = 0;
1157
1331
  try {
1158
- const attestations = await this.validatorClient.collectAttestations(proposal, numberOfRequiredAttestations, attestationDeadline);
1332
+ const attestations = await this.validatorClient.collectAttestations(proposal, numberOfRequiredAttestations, attestationDeadline, this.checkpointNumber);
1159
1333
  collectedAttestationsCount = attestations.length;
1160
1334
  // Trim attestations to minimum required to save L1 calldata gas
1161
1335
  const localAddresses = this.validatorClient.getValidatorAddresses();
@@ -1173,8 +1347,11 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
1173
1347
  } catch (err) {
1174
1348
  if (err && err instanceof AttestationTimeoutError) {
1175
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);
1176
1353
  }
1177
- throw err;
1354
+ return undefined;
1178
1355
  } finally{
1179
1356
  this.metrics.recordCollectedAttestations(collectedAttestationsCount, collectAttestationsTimer.ms());
1180
1357
  }
@@ -1305,40 +1482,6 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
1305
1482
  }
1306
1483
  return false;
1307
1484
  }
1308
- /**
1309
- * In times of congestion we need to simulate using the correct fee header override for the previous block
1310
- * We calculate the correct fee header values.
1311
- *
1312
- * If we are in block 1, or the checkpoint we are querying does not exist, we return undefined. However
1313
- * If we are pipelining - where this function is called, the grandparentCheckpointNumber should always exist
1314
- * @param parentCheckpointNumber
1315
- * @returns
1316
- */ async computeForceProposedFeeHeader(parentCheckpointNumber) {
1317
- if (!this.proposedCheckpointData) {
1318
- return undefined;
1319
- }
1320
- const rollup = this.publisher.rollupContract;
1321
- const grandparentCheckpointNumber = CheckpointNumber(this.checkpointNumber - 2);
1322
- try {
1323
- const [grandparentCheckpoint, manaTarget] = await Promise.all([
1324
- rollup.getCheckpoint(grandparentCheckpointNumber),
1325
- rollup.getManaTarget()
1326
- ]);
1327
- if (!grandparentCheckpoint || !grandparentCheckpoint.feeHeader) {
1328
- this.log.error(`Grandparent checkpoint or its feeHeader is undefined for checkpointNumber=${grandparentCheckpointNumber.toString()}`);
1329
- return undefined;
1330
- } else {
1331
- const parentFeeHeader = RollupContract.computeChildFeeHeader(grandparentCheckpoint.feeHeader, this.proposedCheckpointData.totalManaUsed, this.proposedCheckpointData.feeAssetPriceModifier, manaTarget);
1332
- return {
1333
- checkpointNumber: parentCheckpointNumber,
1334
- feeHeader: parentFeeHeader
1335
- };
1336
- }
1337
- } catch (err) {
1338
- this.log.error(`Failed to fetch grandparent checkpoint or mana target for checkpointNumber=${grandparentCheckpointNumber.toString()}: ${err}`);
1339
- return undefined;
1340
- }
1341
- }
1342
1485
  /** Waits until a specific time within the current slot */ async waitUntilTimeInSlot(targetSecondsIntoSlot) {
1343
1486
  const slotStartTimestamp = this.getSlotStartBuildTimestamp();
1344
1487
  const targetTimestamp = slotStartTimestamp + targetSecondsIntoSlot;
@@ -0,0 +1,34 @@
1
+ import { type TelemetryClient } from '@aztec/telemetry-client';
2
+ /**
3
+ * Per-job recording surface used by {@link CheckpointProposalJob}.
4
+ *
5
+ * Ownership split:
6
+ * - {@link CheckpointProposalJobMetrics} owns the OpenTelemetry meter and instruments and should live for the
7
+ * lifetime of the sequencer process.
8
+ * - A recorder owns only mutable timing state for one checkpoint proposal job so overlapping jobs cannot
9
+ * overwrite each other's timing markers.
10
+ */
11
+ export interface CheckpointProposalJobMetricsRecorder {
12
+ recordCheckpointAttestationDelay(durationMs: number): void;
13
+ recordCheckpointBuild(durationMs: number, blockCount: number, txCount: number, totalMana: number): void;
14
+ recordPipelinedCheckpointBuildStartOffsetFromSlotBoundary(offsetMs: number): void;
15
+ startCheckpointTiming(nowMs: number): void;
16
+ noteCheckpointBlockBuilt(nowMs: number, opts: {
17
+ isFirstBlock: boolean;
18
+ isLastBlock: boolean;
19
+ }): void;
20
+ noteCheckpointBroadcast(nowMs: number): void;
21
+ }
22
+ /**
23
+ * Long-lived owner of checkpoint proposal job telemetry instruments.
24
+ *
25
+ * The sequencer should construct this once and reuse it across jobs. Each call to {@link createRecorder} returns a
26
+ * lightweight recorder with isolated mutable state for a single checkpoint proposal job.
27
+ */
28
+ export declare class CheckpointProposalJobMetrics {
29
+ private readonly meter;
30
+ private readonly instruments;
31
+ constructor(client: TelemetryClient, name?: string);
32
+ createRecorder(): CheckpointProposalJobMetricsRecorder;
33
+ }
34
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2hlY2twb2ludF9wcm9wb3NhbF9qb2JfbWV0cmljcy5kLnRzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL3NlcXVlbmNlci9jaGVja3BvaW50X3Byb3Bvc2FsX2pvYl9tZXRyaWNzLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFNTCxLQUFLLGVBQWUsRUFDckIsTUFBTSx5QkFBeUIsQ0FBQztBQWFqQzs7Ozs7Ozs7R0FRRztBQUNILE1BQU0sV0FBVyxvQ0FBb0M7SUFDbkQsZ0NBQWdDLENBQUMsVUFBVSxFQUFFLE1BQU0sR0FBRyxJQUFJLENBQUM7SUFDM0QscUJBQXFCLENBQUMsVUFBVSxFQUFFLE1BQU0sRUFBRSxVQUFVLEVBQUUsTUFBTSxFQUFFLE9BQU8sRUFBRSxNQUFNLEVBQUUsU0FBUyxFQUFFLE1BQU0sR0FBRyxJQUFJLENBQUM7SUFDeEcseURBQXlELENBQUMsUUFBUSxFQUFFLE1BQU0sR0FBRyxJQUFJLENBQUM7SUFDbEYscUJBQXFCLENBQUMsS0FBSyxFQUFFLE1BQU0sR0FBRyxJQUFJLENBQUM7SUFDM0Msd0JBQXdCLENBQUMsS0FBSyxFQUFFLE1BQU0sRUFBRSxJQUFJLEVBQUU7UUFBRSxZQUFZLEVBQUUsT0FBTyxDQUFDO1FBQUMsV0FBVyxFQUFFLE9BQU8sQ0FBQTtLQUFFLEdBQUcsSUFBSSxDQUFDO0lBQ3JHLHVCQUF1QixDQUFDLEtBQUssRUFBRSxNQUFNLEdBQUcsSUFBSSxDQUFDO0NBQzlDO0FBMEREOzs7OztHQUtHO0FBQ0gscUJBQWEsNEJBQTRCO0lBQ3ZDLE9BQU8sQ0FBQyxRQUFRLENBQUMsS0FBSyxDQUFRO0lBQzlCLE9BQU8sQ0FBQyxRQUFRLENBQUMsV0FBVyxDQUFtQztJQUUvRCxZQUFZLE1BQU0sRUFBRSxlQUFlLEVBQUUsSUFBSSxTQUEwQixFQWtCbEU7SUFFTSxjQUFjLElBQUksb0NBQW9DLENBRTVEO0NBQ0YifQ==
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checkpoint_proposal_job_metrics.d.ts","sourceRoot":"","sources":["../../src/sequencer/checkpoint_proposal_job_metrics.ts"],"names":[],"mappings":"AAAA,OAAO,EAML,KAAK,eAAe,EACrB,MAAM,yBAAyB,CAAC;AAajC;;;;;;;;GAQG;AACH,MAAM,WAAW,oCAAoC;IACnD,gCAAgC,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3D,qBAAqB,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACxG,yDAAyD,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IAClF,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3C,wBAAwB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE;QAAE,YAAY,EAAE,OAAO,CAAC;QAAC,WAAW,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAAC;IACrG,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9C;AA0DD;;;;;GAKG;AACH,qBAAa,4BAA4B;IACvC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAQ;IAC9B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAmC;IAE/D,YAAY,MAAM,EAAE,eAAe,EAAE,IAAI,SAA0B,EAkBlE;IAEM,cAAc,IAAI,oCAAoC,CAE5D;CACF"}