@aztec/sequencer-client 0.0.1-commit.3f296a7d2 → 0.0.1-commit.3f5453c7b

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 +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 +0 -5
  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 +3 -8
  14. package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
  15. package/dest/global_variable_builder/global_builder.js +4 -39
  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/sequencer-publisher-factory.d.ts +1 -3
  20. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  21. package/dest/publisher/sequencer-publisher-factory.js +0 -1
  22. package/dest/publisher/sequencer-publisher.d.ts +47 -44
  23. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  24. package/dest/publisher/sequencer-publisher.js +112 -112
  25. package/dest/sequencer/chain_state_overrides.d.ts +25 -0
  26. package/dest/sequencer/chain_state_overrides.d.ts.map +1 -0
  27. package/dest/sequencer/chain_state_overrides.js +39 -0
  28. package/dest/sequencer/checkpoint_proposal_job.d.ts +21 -10
  29. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  30. package/dest/sequencer/checkpoint_proposal_job.js +197 -131
  31. package/dest/sequencer/checkpoint_proposal_job_metrics.d.ts +34 -0
  32. package/dest/sequencer/checkpoint_proposal_job_metrics.d.ts.map +1 -0
  33. package/dest/sequencer/checkpoint_proposal_job_metrics.js +72 -0
  34. package/dest/sequencer/checkpoint_voter.d.ts +1 -2
  35. package/dest/sequencer/checkpoint_voter.d.ts.map +1 -1
  36. package/dest/sequencer/checkpoint_voter.js +2 -5
  37. package/dest/sequencer/metrics.d.ts +5 -10
  38. package/dest/sequencer/metrics.d.ts.map +1 -1
  39. package/dest/sequencer/metrics.js +12 -20
  40. package/dest/sequencer/sequencer.d.ts +17 -4
  41. package/dest/sequencer/sequencer.d.ts.map +1 -1
  42. package/dest/sequencer/sequencer.js +73 -20
  43. package/dest/sequencer/timetable.d.ts +14 -1
  44. package/dest/sequencer/timetable.d.ts.map +1 -1
  45. package/dest/sequencer/timetable.js +45 -36
  46. package/package.json +27 -27
  47. package/src/client/sequencer-client.ts +5 -7
  48. package/src/config.ts +0 -5
  49. package/src/global_variable_builder/README.md +44 -0
  50. package/src/global_variable_builder/fee_predictor.ts +172 -0
  51. package/src/global_variable_builder/fee_provider.ts +75 -0
  52. package/src/global_variable_builder/global_builder.ts +8 -43
  53. package/src/global_variable_builder/index.ts +2 -0
  54. package/src/publisher/sequencer-publisher-factory.ts +0 -3
  55. package/src/publisher/sequencer-publisher.ts +174 -158
  56. package/src/sequencer/README.md +83 -13
  57. package/src/sequencer/chain_state_overrides.ts +87 -0
  58. package/src/sequencer/checkpoint_proposal_job.ts +238 -153
  59. package/src/sequencer/checkpoint_proposal_job_metrics.ts +128 -0
  60. package/src/sequencer/checkpoint_voter.ts +1 -12
  61. package/src/sequencer/metrics.ts +15 -25
  62. package/src/sequencer/sequencer.ts +110 -22
  63. package/src/sequencer/timetable.ts +57 -45
@@ -446,7 +446,7 @@ import { Timer } from '@aztec/foundation/timer';
446
446
  import { isErrorClass, unfreeze } from '@aztec/foundation/types';
447
447
  import { CommitteeAttestationsAndSigners, MaliciousCommitteeAttestationsAndSigners } from '@aztec/stdlib/block';
448
448
  import { validateCheckpoint } from '@aztec/stdlib/checkpoint';
449
- import { getSlotStartBuildTimestamp, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
449
+ import { computeQuorum, getSlotStartBuildTimestamp, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
450
450
  import { Gas } from '@aztec/stdlib/gas';
451
451
  import { InsufficientValidTxsError } from '@aztec/stdlib/interfaces/server';
452
452
  import { computeInHashFromL1ToL2Messages } from '@aztec/stdlib/messaging';
@@ -454,6 +454,7 @@ import { orderAttestations, trimAttestations } from '@aztec/stdlib/p2p';
454
454
  import { AttestationTimeoutError } from '@aztec/stdlib/validators';
455
455
  import { Attributes, trackSpan } from '@aztec/telemetry-client';
456
456
  import { DutyAlreadySignedError, SlashingProtectionError } from '@aztec/validator-ha-signer/errors';
457
+ import { buildPipelinedParentSimulationOverridesPlan, buildSubmissionSimulationOverridesPlan } from './chain_state_overrides.js';
457
458
  import { CheckpointVoter } from './checkpoint_voter.js';
458
459
  import { SequencerInterruptedError } from './errors.js';
459
460
  import { SequencerState } from './utils.js';
@@ -473,7 +474,6 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
473
474
  */ export class CheckpointProposalJob {
474
475
  slotNow;
475
476
  targetSlot;
476
- epochNow;
477
477
  targetEpoch;
478
478
  checkpointNumber;
479
479
  syncedToBlockNumber;
@@ -496,9 +496,11 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
496
496
  epochCache;
497
497
  dateProvider;
498
498
  metrics;
499
+ checkpointMetrics;
499
500
  eventEmitter;
500
501
  setStateFn;
501
502
  tracer;
503
+ proposedCheckpointData;
502
504
  static{
503
505
  ({ e: [_initProto] } = _apply_decs_2203_r(this, [
504
506
  [
@@ -544,11 +546,12 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
544
546
  ], []));
545
547
  }
546
548
  log;
547
- constructor(slotNow, targetSlot, epochNow, targetEpoch, checkpointNumber, syncedToBlockNumber, // TODO(palla/mbps): Can we remove the proposer in favor of attestorAddress? Need to check fisherman-node flows.
548
- proposer, publisher, attestorAddress, invalidateCheckpoint, validatorClient, globalsBuilder, p2pClient, worldState, l1ToL2MessageSource, l2BlockSource, checkpointsBuilder, blockSink, l1Constants, config, timetable, slasherClient, epochCache, dateProvider, metrics, eventEmitter, setStateFn, tracer, bindings){
549
+ /** Tracks the fire-and-forget L1 submission promise so it can be awaited during shutdown. */ pendingL1Submission;
550
+ /** Pipelined parent chain state used while building and later submitting this checkpoint. */ pipelinedParentSimulationOverridesPlan;
551
+ 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, checkpointMetrics, eventEmitter, setStateFn, tracer, bindings, proposedCheckpointData){
549
553
  this.slotNow = slotNow;
550
554
  this.targetSlot = targetSlot;
551
- this.epochNow = epochNow;
552
555
  this.targetEpoch = targetEpoch;
553
556
  this.checkpointNumber = checkpointNumber;
554
557
  this.syncedToBlockNumber = syncedToBlockNumber;
@@ -571,77 +574,142 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
571
574
  this.epochCache = epochCache;
572
575
  this.dateProvider = dateProvider;
573
576
  this.metrics = metrics;
577
+ this.checkpointMetrics = checkpointMetrics;
574
578
  this.eventEmitter = eventEmitter;
575
579
  this.setStateFn = setStateFn;
576
580
  this.tracer = tracer;
581
+ this.proposedCheckpointData = proposedCheckpointData;
577
582
  _initProto(this);
578
583
  this.log = createLogger('sequencer:checkpoint-proposal', {
579
584
  ...bindings,
580
585
  instanceId: `slot-${this.slotNow}`
581
586
  });
582
587
  }
583
- /** The wall-clock slot during which the proposer builds. */ get slot() {
584
- return this.slotNow;
585
- }
586
- /** The wall-clock epoch. */ get epoch() {
587
- return this.epochNow;
588
+ /** Awaits the pending L1 submission if one is in progress. Call during shutdown. */ async awaitPendingSubmission() {
589
+ this.log.info('Awaiting pending L1 payload submission');
590
+ await this.pendingL1Submission;
588
591
  }
589
592
  /**
590
593
  * Executes the checkpoint proposal job.
591
- * Returns the published checkpoint if successful, undefined otherwise.
594
+ * Builds blocks, assembles checkpoint, and broadcasts the proposal (blocking).
595
+ * Attestation collection, signing, and L1 submission are backgrounded so the
596
+ * work loop can return to IDLE immediately for consecutive slot proposals.
597
+ * Returns the built checkpoint if successful, undefined otherwise.
592
598
  */ async execute() {
593
599
  // Enqueue governance and slashing votes (returns promises that will be awaited later)
594
600
  // In fisherman mode, we simulate slashing but don't actually publish to L1
595
601
  // These are constant for the whole slot, so we only enqueue them once
596
602
  const votesPromises = new CheckpointVoter(this.targetSlot, this.publisher, this.attestorAddress, this.validatorClient, this.slasherClient, this.l1Constants, this.config, this.metrics, this.log).enqueueVotes();
597
- // Build and propose the checkpoint. This will enqueue the request on the publisher if a checkpoint is built.
598
- const checkpoint = await this.proposeCheckpoint();
599
- // Wait until the voting promises have resolved, so all requests are enqueued (not sent)
600
- await Promise.all(votesPromises);
601
- if (checkpoint) {
602
- this.metrics.recordCheckpointProposalSuccess();
603
+ // Build blocks, assemble checkpoint, and broadcast proposal (BLOCKING).
604
+ // Returns after broadcast — attestation collection is deferred.
605
+ const broadcast = await this.proposeCheckpoint();
606
+ if (!broadcast) {
607
+ await Promise.all(votesPromises);
608
+ // Still submit votes even without a checkpoint
609
+ if (!this.config.fishermanMode) {
610
+ this.pendingL1Submission = this.publisher.sendRequestsAt(this.dateProvider.nowAsDate()).then(()=>{});
611
+ }
612
+ return undefined;
603
613
  }
614
+ const { checkpoint } = broadcast;
615
+ this.metrics.recordCheckpointProposalSuccess();
604
616
  // Do not post anything to L1 if we are fishermen, but do perform L1 fee analysis
605
617
  if (this.config.fishermanMode) {
606
618
  await this.handleCheckpointEndAsFisherman(checkpoint);
607
- return;
619
+ return checkpoint;
608
620
  }
609
- // If pipelining, wait until the submission slot so L1 recognizes the pipelined proposer
610
- if (this.epochCache.isProposerPipeliningEnabled()) {
611
- const submissionSlotTimestamp = getTimestampForSlot(this.targetSlot, this.l1Constants) - BigInt(this.l1Constants.ethereumSlotDuration);
612
- this.log.info(`Waiting until submission slot ${this.targetSlot} for L1 submission`, {
613
- slot: this.slot,
614
- submissionSlot: this.targetSlot,
615
- submissionSlotTimestamp
616
- });
617
- await sleepUntil(new Date(Number(submissionSlotTimestamp) * 1000), this.dateProvider.nowAsDate());
618
- // After waking, verify the parent checkpoint wasn't pruned during the sleep.
619
- // We check L1's pending tip directly instead of canProposeAt, which also validates the proposer
620
- // identity and would fail because the timestamp resolves to a different slot's proposer.
621
- const l1Tips = await this.publisher.rollupContract.getTips();
622
- if (l1Tips.pending < this.checkpointNumber - 1) {
623
- this.log.warn(`Parent checkpoint was pruned during pipelining sleep (L1 pending=${l1Tips.pending}, expected>=${this.checkpointNumber - 1}), skipping L1 submission for checkpoint ${this.checkpointNumber}`);
624
- return undefined;
621
+ // Background the attestation signing L1 pipeline so the work loop is unblocked
622
+ this.pendingL1Submission = this.waitForAttestationsAndEnqueueSubmissionAsync(broadcast, votesPromises);
623
+ // Return the built checkpoint immediately — the work loop is now unblocked
624
+ return checkpoint;
625
+ }
626
+ /**
627
+ * Background pipeline: collects attestations, signs them, enqueues the checkpoint, and submits to L1.
628
+ * Runs as a fire-and-forget task stored in `pendingL1Submission` so the work loop is unblocked.
629
+ */ async waitForAttestationsAndEnqueueSubmissionAsync(broadcast, votesPromises) {
630
+ const { checkpoint, proposal, blockProposedAt } = broadcast;
631
+ try {
632
+ await Promise.all(votesPromises);
633
+ this.setStateFn(SequencerState.COLLECTING_ATTESTATIONS, this.targetSlot);
634
+ const attestations = await this.waitForAttestations(proposal);
635
+ this.checkpointMetrics.recordCheckpointAttestationDelay(this.dateProvider.now() - blockProposedAt);
636
+ // Proposer must sign over the attestations before pushing them to L1
637
+ const signer = this.proposer ?? this.publisher.getSenderAddress();
638
+ let attestationsSignature;
639
+ try {
640
+ attestationsSignature = await this.validatorClient.signAttestationsAndSigners(attestations, signer, this.targetSlot, this.checkpointNumber);
641
+ } catch (err) {
642
+ if (this.handleHASigningError(err, 'Attestations signature')) {
643
+ return;
644
+ }
645
+ throw err;
625
646
  }
626
- }
627
- // Then send everything to L1
628
- const l1Response = await this.publisher.sendRequests();
629
- const proposedAction = l1Response?.successfulActions.find((a)=>a === 'propose');
630
- if (proposedAction) {
631
- this.eventEmitter.emit('checkpoint-published', {
632
- checkpoint: this.checkpointNumber,
633
- slot: this.slot
647
+ // Enqueue the checkpoint for L1 submission
648
+ await this.enqueueCheckpointForSubmission({
649
+ checkpoint,
650
+ attestations,
651
+ attestationsSignature
634
652
  });
635
- const coinbase = checkpoint?.header.coinbase;
636
- await this.metrics.incFilledSlot(this.publisher.getSenderAddress().toString(), coinbase);
637
- return checkpoint;
638
- } else if (checkpoint) {
653
+ // Compute the earliest time to submit: pipeline slot start when pipelining, now otherwise.
654
+ const submitAfter = this.epochCache.isProposerPipeliningEnabled() ? new Date(Number(getTimestampForSlot(this.targetSlot, this.l1Constants)) * 1000) : new Date(this.dateProvider.now());
655
+ const l1Response = await this.publisher.sendRequestsAt(submitAfter);
656
+ const proposedAction = l1Response?.successfulActions.find((a)=>a === 'propose');
657
+ if (proposedAction) {
658
+ this.eventEmitter.emit('checkpoint-published', {
659
+ checkpoint: this.checkpointNumber,
660
+ slot: this.targetSlot
661
+ });
662
+ const coinbase = checkpoint.header.coinbase;
663
+ await this.metrics.incFilledSlot(this.publisher.getSenderAddress().toString(), coinbase);
664
+ } else {
665
+ this.eventEmitter.emit('checkpoint-publish-failed', {
666
+ ...l1Response,
667
+ slot: this.targetSlot
668
+ });
669
+ if (this.epochCache.isProposerPipeliningEnabled()) {
670
+ this.metrics.recordPipelineDiscard();
671
+ }
672
+ }
673
+ } catch (err) {
674
+ if (err instanceof SequencerInterruptedError) {
675
+ return;
676
+ }
677
+ this.log.error(`Background attestation/L1 pipeline failed for slot ${this.targetSlot}`, err);
639
678
  this.eventEmitter.emit('checkpoint-publish-failed', {
640
- ...l1Response,
641
- slot: this.slot
679
+ slot: this.targetSlot
642
680
  });
643
- return undefined;
681
+ if (this.epochCache.isProposerPipeliningEnabled()) {
682
+ this.metrics.recordPipelineDiscard();
683
+ }
684
+ }
685
+ }
686
+ /** Enqueues the checkpoint for L1 submission. Called after pipeline sleep in execute(). */ async enqueueCheckpointForSubmission(result) {
687
+ const { checkpoint, attestations, attestationsSignature } = result;
688
+ this.setStateFn(SequencerState.PUBLISHING_CHECKPOINT, this.targetSlot);
689
+ const aztecSlotDuration = this.l1Constants.slotDuration;
690
+ const submissionSlotStart = Number(getTimestampForSlot(this.targetSlot, this.l1Constants));
691
+ const txTimeoutAt = new Date((submissionSlotStart + aztecSlotDuration) * 1000);
692
+ // If we have been configured to potentially skip publishing checkpoint then roll the dice here
693
+ if (this.config.skipPublishingCheckpointsPercent !== undefined && this.config.skipPublishingCheckpointsPercent > 0) {
694
+ const roll = Math.max(0, randomInt(100));
695
+ if (roll < this.config.skipPublishingCheckpointsPercent) {
696
+ this.log.warn(`Skipping publishing proposal for checkpoint ${checkpoint.number}. Configured percentage: ${this.config.skipPublishingCheckpointsPercent}, generated value: ${roll}`);
697
+ return;
698
+ }
644
699
  }
700
+ const isPipelining = this.epochCache.isProposerPipeliningEnabled();
701
+ const submissionSimulationOverridesPlan = buildSubmissionSimulationOverridesPlan({
702
+ pipelinedParentPlan: this.pipelinedParentSimulationOverridesPlan,
703
+ invalidateToPendingCheckpointNumber: this.invalidateCheckpoint?.forcePendingCheckpointNumber,
704
+ lastArchiveRoot: checkpoint.header.lastArchiveRoot,
705
+ pipeliningEnabled: isPipelining
706
+ });
707
+ await this.publisher.enqueueProposeCheckpoint(checkpoint, attestations, attestationsSignature, {
708
+ txTimeoutAt,
709
+ ...submissionSimulationOverridesPlan ? {
710
+ simulationOverridesPlan: submissionSimulationOverridesPlan
711
+ } : {}
712
+ });
645
713
  }
646
714
  async proposeCheckpoint() {
647
715
  try {
@@ -651,13 +719,22 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
651
719
  hasError: false
652
720
  };
653
721
  try {
722
+ const now = this.dateProvider.now();
723
+ if (this.epochCache.isProposerPipeliningEnabled() && this.proposedCheckpointData) {
724
+ // Measure against the wall-clock slot whose build window we are currently using.
725
+ // In pipelining mode `targetSlot` is intentionally one slot ahead, which makes the
726
+ // target-slot boundary a full slot away from the actual build start time.
727
+ const slotBoundaryMs = Number(getTimestampForSlot(this.slotNow, this.l1Constants)) * 1000;
728
+ this.checkpointMetrics.recordPipelinedCheckpointBuildStartOffsetFromSlotBoundary(now - slotBoundaryMs);
729
+ }
730
+ this.checkpointMetrics.startCheckpointTiming(now);
654
731
  // Get operator configured coinbase and fee recipient for this attestor
655
732
  const coinbase = this.validatorClient.getCoinbaseForAttestor(this.attestorAddress);
656
733
  const feeRecipient = this.validatorClient.getFeeRecipientForAttestor(this.attestorAddress);
657
734
  // Start the checkpoint
658
735
  this.setStateFn(SequencerState.INITIALIZING_CHECKPOINT, this.targetSlot);
659
736
  this.log.info(`Starting checkpoint proposal`, {
660
- buildSlot: this.slot,
737
+ buildSlot: this.slotNow,
661
738
  submissionSlot: this.targetSlot,
662
739
  pipelining: this.epochCache.isProposerPipeliningEnabled(),
663
740
  proposer: this.proposer?.toString(),
@@ -668,8 +745,17 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
668
745
  if (this.invalidateCheckpoint && !this.config.skipInvalidateBlockAsProposer) {
669
746
  this.publisher.enqueueInvalidateCheckpoint(this.invalidateCheckpoint);
670
747
  }
671
- // Create checkpoint builder for the slot
672
- const checkpointGlobalVariables = await this.globalsBuilder.buildCheckpointGlobalVariables(coinbase, feeRecipient, this.targetSlot);
748
+ // Create checkpoint builder for the slot.
749
+ // When pipelining, force the proposed checkpoint number and fee header to our parent so the
750
+ // fee computation sees the same chain tip that L1 will see once the previous pipelined checkpoint lands.
751
+ const isPipelining = this.epochCache.isProposerPipeliningEnabled();
752
+ this.pipelinedParentSimulationOverridesPlan = isPipelining ? await buildPipelinedParentSimulationOverridesPlan({
753
+ checkpointNumber: this.checkpointNumber,
754
+ proposedCheckpointData: this.proposedCheckpointData,
755
+ rollup: this.publisher.rollupContract,
756
+ log: this.log
757
+ }) : undefined;
758
+ const checkpointGlobalVariables = await this.globalsBuilder.buildCheckpointGlobalVariables(coinbase, feeRecipient, this.targetSlot, this.pipelinedParentSimulationOverridesPlan);
673
759
  // Collect L1 to L2 messages for the checkpoint and compute their hash
674
760
  const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(this.checkpointNumber);
675
761
  const inHash = computeInHashFromL1ToL2Messages(l1ToL2Messages);
@@ -741,14 +827,14 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
741
827
  maxTxsPerCheckpoint: this.config.maxTxsPerCheckpoint
742
828
  });
743
829
  } catch (err) {
744
- this.log.error(`Built an invalid checkpoint at slot ${this.slot} (skipping proposal)`, err, {
830
+ this.log.error(`Built an invalid checkpoint at slot ${this.slotNow} (skipping proposal)`, err, {
745
831
  checkpoint: checkpoint.header.toInspect()
746
832
  });
747
833
  return undefined;
748
834
  }
749
835
  // Record checkpoint-level build metrics
750
- this.metrics.recordCheckpointBuild(checkpointBuildTimer.ms(), blocksInCheckpoint.length, checkpoint.getStats().txCount, Number(checkpoint.header.totalManaUsed.toBigInt()));
751
- // Do not collect attestations nor publish to L1 in fisherman mode
836
+ this.checkpointMetrics.recordCheckpointBuild(checkpointBuildTimer.ms(), blocksInCheckpoint.length, checkpoint.getStats().txCount, Number(checkpoint.header.totalManaUsed.toBigInt()));
837
+ // In fisherman mode, return the checkpoint without broadcasting or collecting attestations
752
838
  if (this.config.fishermanMode) {
753
839
  this.log.info(`Built checkpoint for slot ${this.targetSlot} with ${blocksInCheckpoint.length} blocks. ` + `Skipping proposal in fisherman mode.`, {
754
840
  slot: this.targetSlot,
@@ -756,53 +842,24 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
756
842
  blocksBuilt: blocksInCheckpoint.length
757
843
  });
758
844
  this.metrics.recordCheckpointSuccess();
759
- return checkpoint;
845
+ // Return a broadcast result with a dummy proposal — fisherman mode skips attestation collection
846
+ return {
847
+ checkpoint,
848
+ proposal: undefined,
849
+ blockProposedAt: this.dateProvider.now()
850
+ };
760
851
  }
761
- // Include the block pending broadcast in the checkpoint proposal if any
762
- const lastBlock = blockPendingBroadcast && {
763
- blockHeader: blockPendingBroadcast.block.header,
764
- indexWithinCheckpoint: blockPendingBroadcast.block.indexWithinCheckpoint,
765
- txs: blockPendingBroadcast.txs
766
- };
767
852
  // Create the checkpoint proposal and broadcast it
768
- const proposal = await this.validatorClient.createCheckpointProposal(checkpoint.header, checkpoint.archive.root, feeAssetPriceModifier, lastBlock, this.proposer, checkpointProposalOptions);
853
+ const proposal = await this.validatorClient.createCheckpointProposal(checkpoint.header, checkpoint.archive.root, feeAssetPriceModifier, blockPendingBroadcast, this.proposer, checkpointProposalOptions);
769
854
  const blockProposedAt = this.dateProvider.now();
770
855
  await this.p2pClient.broadcastCheckpointProposal(proposal);
771
- this.setStateFn(SequencerState.COLLECTING_ATTESTATIONS, this.targetSlot);
772
- const attestations = await this.waitForAttestations(proposal);
773
- const blockAttestedAt = this.dateProvider.now();
774
- this.metrics.recordCheckpointAttestationDelay(blockAttestedAt - blockProposedAt);
775
- // Proposer must sign over the attestations before pushing them to L1
776
- const signer = this.proposer ?? this.publisher.getSenderAddress();
777
- let attestationsSignature;
778
- try {
779
- attestationsSignature = await this.validatorClient.signAttestationsAndSigners(attestations, signer, this.targetSlot, this.checkpointNumber);
780
- } catch (err) {
781
- // We shouldn't really get here since we yield to another HA node
782
- // as soon as we see these errors when creating block or checkpoint proposals.
783
- if (this.handleHASigningError(err, 'Attestations signature')) {
784
- return undefined;
785
- }
786
- throw err;
787
- }
788
- // Enqueue publishing the checkpoint to L1
789
- this.setStateFn(SequencerState.PUBLISHING_CHECKPOINT, this.targetSlot);
790
- const aztecSlotDuration = this.l1Constants.slotDuration;
791
- const submissionSlotStart = Number(getTimestampForSlot(this.targetSlot, this.l1Constants));
792
- const txTimeoutAt = new Date((submissionSlotStart + aztecSlotDuration) * 1000);
793
- // If we have been configured to potentially skip publishing checkpoint then roll the dice here
794
- if (this.config.skipPublishingCheckpointsPercent !== undefined && this.config.skipPublishingCheckpointsPercent > 0) {
795
- const result = Math.max(0, randomInt(100));
796
- if (result < this.config.skipPublishingCheckpointsPercent) {
797
- this.log.warn(`Skipping publishing proposal for checkpoint ${checkpoint.number}. Configured percentage: ${this.config.skipPublishingCheckpointsPercent}, generated value: ${result}`);
798
- return checkpoint;
799
- }
800
- }
801
- await this.publisher.enqueueProposeCheckpoint(checkpoint, attestations, attestationsSignature, {
802
- txTimeoutAt,
803
- forcePendingCheckpointNumber: this.invalidateCheckpoint?.forcePendingCheckpointNumber
804
- });
805
- return checkpoint;
856
+ this.checkpointMetrics.noteCheckpointBroadcast(this.dateProvider.now());
857
+ // Return immediately after broadcast — attestation collection happens in the background
858
+ return {
859
+ checkpoint,
860
+ proposal,
861
+ blockProposedAt
862
+ };
806
863
  } catch (e) {
807
864
  env.error = e;
808
865
  env.hasError = true;
@@ -815,7 +872,7 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
815
872
  // swallow this error. It's already been logged by a function deeper in the stack
816
873
  return undefined;
817
874
  }
818
- this.log.error(`Error building checkpoint at slot ${this.slot}`, err);
875
+ this.log.error(`Error building checkpoint at slot ${this.targetSlot}`, err);
819
876
  return undefined;
820
877
  }
821
878
  }
@@ -852,17 +909,19 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
852
909
  indexWithinCheckpoint,
853
910
  txHashesAlreadyIncluded
854
911
  });
855
- // TODO(palla/mbps): Review these conditions. We may want to keep trying in some scenarios.
856
- if (!buildResult && timingInfo.isLastBlock) {
857
- break;
858
- } else if (!buildResult && timingInfo.deadline !== undefined) {
859
- // But if there is still time for more blocks, wait until the next subslot and try again
912
+ // If we failed to build the block due to insufficient txs, we try again if there is still time left in the slot
913
+ if ('failure' in buildResult) {
914
+ // If this was the last subslot, or we're running with a single block per slot, we're done
915
+ if (timingInfo.isLastBlock || timingInfo.deadline === undefined) {
916
+ break;
917
+ }
918
+ // Otherwise, if there is still time for more blocks, we wait until the next subslot and try again
860
919
  await this.waitUntilNextSubslot(timingInfo.deadline);
861
920
  continue;
862
- } else if (!buildResult) {
863
- break;
864
- } else if ('error' in buildResult) {
865
- // If there was an error building the block, just exit the loop and give up the rest of the slot
921
+ }
922
+ // If there was an error building the block, we just exit the loop and give up the rest of the slot.
923
+ // We don't want to risk building more blocks if something went wrong.
924
+ if ('error' in buildResult) {
866
925
  if (!(buildResult.error instanceof SequencerInterruptedError)) {
867
926
  this.log.warn(`Halting block building for slot ${this.targetSlot}`, {
868
927
  slot: this.targetSlot,
@@ -873,32 +932,29 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
873
932
  break;
874
933
  }
875
934
  const { block, usedTxs } = buildResult;
935
+ this.checkpointMetrics.noteCheckpointBlockBuilt(this.dateProvider.now(), {
936
+ isFirstBlock: blocksBuilt === 0,
937
+ isLastBlock: timingInfo.isLastBlock
938
+ });
876
939
  blocksInCheckpoint.push(block);
877
940
  usedTxs.forEach((tx)=>txHashesAlreadyIncluded.add(tx.txHash.toString()));
878
- // If this is the last block, sync it to the archiver and exit the loop
879
- // so we can build the checkpoint and start collecting attestations.
941
+ // Sign the block proposal. This will throw if HA signing fails.
942
+ const proposal = await this.createBlockProposal(block, inHash, usedTxs, blockProposalOptions);
943
+ // Sync the proposed block to the archiver to make it available, only after we've managed to sign the proposal,
944
+ // so we avoid polluting our archive with a block that would fail.
945
+ // We wait for the sync to succeed, as this helps catch consistency errors, even if it means we lose some time for block-building.
946
+ // If this throws, we abort the entire checkpoint.
947
+ await this.syncProposedBlockToArchiver(block);
948
+ // If this is the last block, do not broadcast it, since it will be included in the checkpoint proposal.
880
949
  if (timingInfo.isLastBlock) {
881
- await this.syncProposedBlockToArchiver(block);
882
950
  this.log.verbose(`Completed final block ${blockNumber} for slot ${this.targetSlot}`, {
883
951
  slot: this.targetSlot,
884
952
  blockNumber,
885
953
  blocksBuilt
886
954
  });
887
- blockPendingBroadcast = {
888
- block,
889
- txs: usedTxs
890
- };
955
+ blockPendingBroadcast = proposal;
891
956
  break;
892
957
  }
893
- // Broadcast the block proposal (unless we're in fisherman mode) unless the block is the last one,
894
- // in which case we'll broadcast it along with the checkpoint at the end of the loop.
895
- // Note that we only send the block to the archiver if we manage to create the proposal, so if there's
896
- // a HA error we don't pollute our archiver with a block that won't make it to the chain.
897
- const proposal = await this.createBlockProposal(block, inHash, usedTxs, blockProposalOptions);
898
- // Sync the proposed block to the archiver to make it available, only after we've managed to sign the proposal.
899
- // We wait for the sync to succeed, as this helps catch consistency errors, even if it means we lose some time for block-building.
900
- // If this throws, we abort the entire checkpoint.
901
- await this.syncProposedBlockToArchiver(block);
902
958
  // Once we have a signed proposal and the archiver agreed with our proposed block, then we broadcast it.
903
959
  proposal && await this.p2pClient.broadcastProposal(proposal);
904
960
  // Wait until the next block's start time
@@ -948,7 +1004,9 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
948
1004
  slot: this.targetSlot
949
1005
  });
950
1006
  this.metrics.recordBlockProposalFailed('insufficient_txs');
951
- return undefined;
1007
+ return {
1008
+ failure: 'insufficient-txs'
1009
+ };
952
1010
  }
953
1011
  // Create iterator to pending txs. We filter out txs already included in previous blocks in the checkpoint
954
1012
  // just in case p2p failed to sync the provisional block and didn't get to remove those txs from the mempool yet.
@@ -991,7 +1049,9 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
991
1049
  slot: this.targetSlot
992
1050
  });
993
1051
  this.metrics.recordBlockProposalFailed('insufficient_valid_txs');
994
- return undefined;
1052
+ return {
1053
+ failure: 'insufficient-valid-txs'
1054
+ };
995
1055
  }
996
1056
  // Block creation succeeded, emit stats and metrics
997
1057
  const { block, publicProcessorDuration, usedTxs, blockBuildDuration, numTxs } = buildResult;
@@ -1010,12 +1070,14 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
1010
1070
  manaPerSec,
1011
1071
  ...blockStats
1012
1072
  });
1073
+ // `slot` is the target/submission slot (may be one ahead when pipelining),
1074
+ // `buildSlot` is the wall-clock slot during which the block was actually built.
1013
1075
  this.eventEmitter.emit('block-proposed', {
1014
1076
  blockNumber: block.number,
1015
1077
  slot: this.targetSlot,
1016
1078
  buildSlot: this.slotNow
1017
1079
  });
1018
- this.metrics.recordBuiltBlock(blockBuildDuration, block.header.totalManaUsed.toNumberUnsafe());
1080
+ this.metrics.recordBuiltBlock(blockBuildDuration, block.header.totalManaUsed.toNumberUnsafe(), this.targetSlot);
1019
1081
  return {
1020
1082
  block,
1021
1083
  usedTxs
@@ -1110,13 +1172,13 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
1110
1172
  committee
1111
1173
  });
1112
1174
  }
1113
- const numberOfRequiredAttestations = Math.floor(committee.length * 2 / 3) + 1;
1175
+ const numberOfRequiredAttestations = computeQuorum(committee.length);
1114
1176
  if (this.config.skipCollectingAttestations) {
1115
1177
  this.log.warn('Skipping attestation collection as per config (attesting with own keys only)');
1116
1178
  const attestations = await this.validatorClient?.collectOwnAttestations(proposal);
1117
1179
  return new CommitteeAttestationsAndSigners(orderAttestations(attestations ?? [], committee));
1118
1180
  }
1119
- const attestationTimeAllowed = this.config.enforceTimeTable ? this.timetable.getMaxAllowedTime(SequencerState.PUBLISHING_CHECKPOINT) : this.l1Constants.slotDuration;
1181
+ const attestationTimeAllowed = this.config.enforceTimeTable ? this.timetable.getCheckpointAttestationDeadline() : this.l1Constants.slotDuration;
1120
1182
  const attestationDeadline = new Date((this.getSlotStartBuildTimestamp() + attestationTimeAllowed) * 1000);
1121
1183
  this.metrics.recordRequiredAttestations(numberOfRequiredAttestations, attestationTimeAllowed);
1122
1184
  const collectAttestationsTimer = new Timer();
@@ -1215,8 +1277,12 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
1215
1277
  * Adds the proposed block to the archiver so it's available via P2P.
1216
1278
  * Gossip doesn't echo messages back to the sender, so the proposer's archiver/world-state
1217
1279
  * would never receive its own block without this explicit sync.
1280
+ *
1281
+ * In fisherman mode we skip this push: the fisherman builds blocks locally for validation
1282
+ * and fee analysis only, and pushing them to the archiver causes spurious reorg cascades
1283
+ * whenever the real proposer's block arrives from L1.
1218
1284
  */ async syncProposedBlockToArchiver(block) {
1219
- if (this.config.skipPushProposedBlocksToArchiver !== false) {
1285
+ if (this.config.skipPushProposedBlocksToArchiver || this.config.fishermanMode) {
1220
1286
  this.log.warn(`Skipping push of proposed block ${block.number} to archiver`, {
1221
1287
  blockNumber: block.number,
1222
1288
  slot: block.header.globalVariables.slotNumber
@@ -1277,7 +1343,7 @@ _dec = trackSpan('CheckpointProposalJob.execute'), _dec1 = trackSpan('Checkpoint
1277
1343
  await sleep(TXS_POLLING_MS);
1278
1344
  }
1279
1345
  getSlotStartBuildTimestamp() {
1280
- return getSlotStartBuildTimestamp(this.slot, this.l1Constants);
1346
+ return getSlotStartBuildTimestamp(this.slotNow, this.l1Constants);
1281
1347
  }
1282
1348
  getSecondsIntoSlot() {
1283
1349
  const slotStartTimestamp = this.getSlotStartBuildTimestamp();
@@ -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"}