@aztec/sequencer-client 5.0.0-rc.1 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dest/client/sequencer-client.d.ts +16 -3
  2. package/dest/client/sequencer-client.d.ts.map +1 -1
  3. package/dest/client/sequencer-client.js +17 -3
  4. package/dest/config.d.ts +2 -1
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +8 -3
  7. package/dest/global_variable_builder/fee_predictor.d.ts +1 -1
  8. package/dest/global_variable_builder/fee_predictor.d.ts.map +1 -1
  9. package/dest/global_variable_builder/fee_predictor.js +11 -1
  10. package/dest/global_variable_builder/fee_provider.d.ts +1 -1
  11. package/dest/global_variable_builder/fee_provider.d.ts.map +1 -1
  12. package/dest/global_variable_builder/fee_provider.js +15 -2
  13. package/dest/global_variable_builder/global_builder.d.ts +3 -16
  14. package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
  15. package/dest/global_variable_builder/global_builder.js +2 -25
  16. package/dest/publisher/config.d.ts +5 -1
  17. package/dest/publisher/config.d.ts.map +1 -1
  18. package/dest/publisher/config.js +11 -1
  19. package/dest/publisher/l1_to_l2_messaging.d.ts +21 -0
  20. package/dest/publisher/l1_to_l2_messaging.d.ts.map +1 -0
  21. package/dest/publisher/l1_to_l2_messaging.js +70 -0
  22. package/dest/publisher/sequencer-publisher.d.ts +17 -4
  23. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  24. package/dest/publisher/sequencer-publisher.js +119 -22
  25. package/dest/publisher/write_json.d.ts +11 -0
  26. package/dest/publisher/write_json.d.ts.map +1 -0
  27. package/dest/publisher/write_json.js +57 -0
  28. package/dest/sequencer/automine/automine_sequencer.d.ts +8 -3
  29. package/dest/sequencer/automine/automine_sequencer.d.ts.map +1 -1
  30. package/dest/sequencer/automine/automine_sequencer.js +27 -14
  31. package/dest/sequencer/checkpoint_proposal_job.d.ts +4 -6
  32. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  33. package/dest/sequencer/checkpoint_proposal_job.js +28 -26
  34. package/dest/sequencer/events.d.ts +1 -2
  35. package/dest/sequencer/events.d.ts.map +1 -1
  36. package/dest/sequencer/requests_tracker.d.ts +22 -0
  37. package/dest/sequencer/requests_tracker.d.ts.map +1 -0
  38. package/dest/sequencer/requests_tracker.js +33 -0
  39. package/dest/sequencer/sequencer.d.ts +47 -10
  40. package/dest/sequencer/sequencer.d.ts.map +1 -1
  41. package/dest/sequencer/sequencer.js +125 -60
  42. package/dest/test/utils.d.ts +1 -1
  43. package/dest/test/utils.d.ts.map +1 -1
  44. package/dest/test/utils.js +2 -1
  45. package/package.json +27 -27
  46. package/src/client/sequencer-client.ts +19 -3
  47. package/src/config.ts +8 -3
  48. package/src/global_variable_builder/fee_predictor.ts +11 -1
  49. package/src/global_variable_builder/fee_provider.ts +19 -2
  50. package/src/global_variable_builder/global_builder.ts +2 -34
  51. package/src/publisher/config.ts +22 -1
  52. package/src/publisher/l1_to_l2_messaging.ts +85 -0
  53. package/src/publisher/sequencer-publisher.ts +116 -23
  54. package/src/publisher/write_json.ts +78 -0
  55. package/src/sequencer/automine/automine_sequencer.ts +27 -14
  56. package/src/sequencer/checkpoint_proposal_job.ts +43 -34
  57. package/src/sequencer/events.ts +1 -1
  58. package/src/sequencer/requests_tracker.ts +43 -0
  59. package/src/sequencer/sequencer.ts +133 -61
  60. package/src/test/utils.ts +2 -0
  61. package/dest/sequencer/chain_state_overrides.d.ts +0 -61
  62. package/dest/sequencer/chain_state_overrides.d.ts.map +0 -1
  63. package/dest/sequencer/chain_state_overrides.js +0 -98
  64. package/src/sequencer/chain_state_overrides.ts +0 -169
@@ -379,6 +379,7 @@ import { merge, omit, pick } from '@aztec/foundation/collection';
379
379
  import { EthAddress } from '@aztec/foundation/eth-address';
380
380
  import { createLogger } from '@aztec/foundation/log';
381
381
  import { RunningPromise } from '@aztec/foundation/running-promise';
382
+ import { buildCheckpointSimulationOverridesPlan } from '@aztec/stdlib/checkpoint';
382
383
  import { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
383
384
  import { MIN_PER_BLOCK_ALLOCATION_MULTIPLIER, MIN_PER_BLOCK_DA_ALLOCATION_MULTIPLIER, computeNetworkTxGasLimits } from '@aztec/stdlib/gas';
384
385
  import { SequencerConfigSchema } from '@aztec/stdlib/interfaces/server';
@@ -387,15 +388,15 @@ import { buildProposerTimetable } from '@aztec/stdlib/timetable';
387
388
  import { Attributes, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
388
389
  import EventEmitter from 'node:events';
389
390
  import { DefaultSequencerConfig } from '../config.js';
390
- import { buildCheckpointSimulationOverridesPlan } from './chain_state_overrides.js';
391
391
  import { CheckpointProposalJob } from './checkpoint_proposal_job.js';
392
392
  import { CheckpointProposalJobMetrics } from './checkpoint_proposal_job_metrics.js';
393
393
  import { CheckpointVoter } from './checkpoint_voter.js';
394
394
  import { SequencerInterruptedError } from './errors.js';
395
395
  import { SequencerMetrics } from './metrics.js';
396
+ import { RequestsTracker } from './requests_tracker.js';
396
397
  import { SequencerState } from './utils.js';
397
398
  export { SequencerState };
398
- _dec = trackSpan('Sequencer.work'), _dec1 = trackSpan('Sequencer.prepareCheckpointProposal'), _dec2 = trackSpan('Sequencer.tryVoteWhenCannotBuild', ({ slot })=>({
399
+ _dec = trackSpan('Sequencer.work'), _dec1 = trackSpan('Sequencer.prepareCheckpointProposal'), _dec2 = trackSpan('Sequencer.tryVoteAndPruneWhenCannotBuild', ({ slot })=>({
399
400
  [Attributes.SLOT_NUMBER]: slot
400
401
  })), _dec3 = trackSpan('Sequencer.tryVoteWhenEscapeHatchOpen', ({ slot })=>({
401
402
  [Attributes.SLOT_NUMBER]: slot
@@ -438,7 +439,7 @@ _dec = trackSpan('Sequencer.work'), _dec1 = trackSpan('Sequencer.prepareCheckpoi
438
439
  [
439
440
  _dec2,
440
441
  2,
441
- "tryVoteWhenCannotBuild"
442
+ "tryVoteAndPruneWhenCannotBuild"
442
443
  ],
443
444
  [
444
445
  _dec3,
@@ -454,7 +455,7 @@ _dec = trackSpan('Sequencer.work'), _dec1 = trackSpan('Sequencer.prepareCheckpoi
454
455
  metrics;
455
456
  checkpointProposalJobMetrics;
456
457
  stateLog;
457
- /** The last slot for which we attempted to perform our voting duties with degraded block production */ lastSlotForFallbackVote;
458
+ /** The last slot for which we attempted to perform our fallback duties (votes and/or prune) with degraded block production */ lastSlotForFallbackAction;
458
459
  /** The (checkpoint, slot) of the last invalidation request we successfully simulated, to prevent
459
460
  * re-simulating and re-submitting the same invalidation across the many ticks within a single slot. */ lastInvalidationAttempt;
460
461
  /** The last slot for which we logged "no committee" warning, to avoid spam */ lastSlotForNoCommitteeWarning;
@@ -462,11 +463,20 @@ _dec = trackSpan('Sequencer.work'), _dec1 = trackSpan('Sequencer.prepareCheckpoi
462
463
  /** Last successful checkpoint proposed */ lastCheckpointProposed;
463
464
  /** The last epoch for which we logged strategy comparison in fisherman mode. */ lastEpochForStrategyComparison;
464
465
  /** The last checkpoint proposal job, tracked so we can await its pending L1 submission during shutdown. */ lastCheckpointProposalJob;
466
+ /**
467
+ * In-flight fire-and-forget requests that {@link stop} interrupts and drains, and {@link pause} awaits
468
+ * untouched: the checkpoint proposal jobs' backgrounded L1 submissions (each job is handed this shared
469
+ * tracker) plus the sequencer's own fallback submissions (votes/prune when we cannot build, or
470
+ * escape-hatch votes). Each fallback send is gated by its wrapper publisher's interruptible target-slot
471
+ * sleep; the tracked interrupt wakes that sleep so the send short-circuits without publishing. A wrapper
472
+ * is created for a single send and is never restarted, so once interrupted its sleeper can never publish
473
+ * a stale-slot tx, even after the pooled publishers are restarted by a later {@link start}.
474
+ */ pendingRequests;
465
475
  /** Proposer schedule and block sub-slot timetable for the sequencer, rebuilt on every config update. */ timetable;
466
476
  /** Config for the sequencer */ config;
467
477
  signatureContext;
468
478
  constructor(publisherFactory, validatorClient, globalsBuilder, p2pClient, worldState, slasherClient, l2BlockSource, l1ToL2MessageSource, checkpointsBuilder, l1Constants, dateProvider, epochCache, rollupContract, config, telemetry = getTelemetryClient(), log = createLogger('sequencer')){
469
- super(), this.publisherFactory = publisherFactory, this.validatorClient = validatorClient, this.globalsBuilder = globalsBuilder, this.p2pClient = p2pClient, this.worldState = worldState, this.slasherClient = slasherClient, this.l2BlockSource = l2BlockSource, this.l1ToL2MessageSource = l1ToL2MessageSource, this.checkpointsBuilder = checkpointsBuilder, this.l1Constants = l1Constants, this.dateProvider = dateProvider, this.epochCache = epochCache, this.rollupContract = rollupContract, this.telemetry = telemetry, this.log = log, this.state = (_initProto(this), SequencerState.STOPPED), this.config = DefaultSequencerConfig;
479
+ super(), this.publisherFactory = publisherFactory, this.validatorClient = validatorClient, this.globalsBuilder = globalsBuilder, this.p2pClient = p2pClient, this.worldState = worldState, this.slasherClient = slasherClient, this.l2BlockSource = l2BlockSource, this.l1ToL2MessageSource = l1ToL2MessageSource, this.checkpointsBuilder = checkpointsBuilder, this.l1Constants = l1Constants, this.dateProvider = dateProvider, this.epochCache = epochCache, this.rollupContract = rollupContract, this.telemetry = telemetry, this.log = log, this.state = (_initProto(this), SequencerState.STOPPED), this.pendingRequests = new RequestsTracker(), this.config = DefaultSequencerConfig;
470
480
  this.stateLog = log.createChild('state');
471
481
  this.stateEnteredAtMs = this.dateProvider.now();
472
482
  // Add [FISHERMAN] prefix to logger if in fisherman mode
@@ -580,7 +590,21 @@ _dec = trackSpan('Sequencer.work'), _dec1 = trackSpan('Sequencer.prepareCheckpoi
580
590
  /** Initializes the sequencer (precomputes tables). Takes about 3s. */ init() {
581
591
  getKzg();
582
592
  }
583
- /** Starts the sequencer and moves to IDLE state. */ start() {
593
+ /**
594
+ * Starts (or restarts) the sequencer and moves it to the IDLE state. Idempotent: a start while already
595
+ * running is a no-op, so it never orphans the previous poll loop. A start while STOPPING throws, since the
596
+ * in-flight stop would mark the fresh loop's state STOPPED and orphan it — silently doing nothing would
597
+ * leave the caller believing the sequencer is running when it is on its way to STOPPED. Safe to call after
598
+ * a previous {@link stop} has resolved; the caller must also restart the publishers (see
599
+ * {@link SequencerClient.start}) so L1 publishing is re-enabled.
600
+ */ start() {
601
+ if (this.state === SequencerState.STOPPING) {
602
+ throw new Error('Cannot start sequencer while it is stopping');
603
+ }
604
+ if (this.runningPromise?.isRunning()) {
605
+ this.log.warn('Attempted to start sequencer that is already running');
606
+ return;
607
+ }
584
608
  this.runningPromise = new RunningPromise(this.safeWork.bind(this), this.log, this.config.sequencerPollingIntervalMS);
585
609
  this.setState(SequencerState.IDLE, undefined, {
586
610
  force: true
@@ -591,20 +615,56 @@ _dec = trackSpan('Sequencer.work'), _dec1 = trackSpan('Sequencer.prepareCheckpoi
591
615
  /** Triggers an immediate run of the sequencer, bypassing the polling interval. */ trigger() {
592
616
  return this.runningPromise?.trigger();
593
617
  }
594
- /** Stops the sequencer from building blocks and moves to STOPPED state. */ async stop() {
618
+ /**
619
+ * Stops the sequencer from building blocks and moves it to STOPPED state, interrupting the in-flight
620
+ * work() iteration and its pending L1 submissions for a fast shutdown, then draining before returning.
621
+ * Idempotent: a second call while already stopped or stopping is a no-op. Lifecycle calls are expected
622
+ * to be serialized by the caller. For a restartable pause that lets in-flight work finish untouched
623
+ * (e.g. around a test clock warp), use {@link pause} instead.
624
+ */ async stop() {
625
+ if (this.state === SequencerState.STOPPED || this.state === SequencerState.STOPPING) {
626
+ this.log.debug(`Sequencer already ${this.state.toLowerCase()}, ignoring stop`);
627
+ return;
628
+ }
595
629
  this.log.info(`Stopping sequencer`);
596
630
  this.setState(SequencerState.STOPPING, undefined, {
597
631
  force: true
598
632
  });
599
633
  this.lastCheckpointProposalJob?.interrupt();
600
634
  await this.publisherFactory.stopAll();
635
+ // Stop the poll loop and await the in-flight work() iteration. work() registers its fire-and-forget
636
+ // requests synchronously before returning, so once the loop has stopped, pendingRequests holds every
637
+ // request that will ever exist. Interrupting any earlier could miss a request registered by the last
638
+ // in-flight iteration.
601
639
  await this.runningPromise?.stop();
602
- await this.lastCheckpointProposalJob?.awaitPendingSubmission();
640
+ this.pendingRequests.interruptRequests();
641
+ await this.pendingRequests.awaitRequests();
603
642
  this.setState(SequencerState.STOPPED, undefined, {
604
643
  force: true
605
644
  });
606
645
  this.log.info('Stopped sequencer');
607
646
  }
647
+ /**
648
+ * Gracefully pauses block production so the sequencer can later be resumed with {@link start}: halts the
649
+ * poll loop and waits for the in-flight work() iteration and every pending L1 submission / fallback send
650
+ * to finish, without interrupting them. No interrupt lands mid-build, so no spurious checkpoint-error is
651
+ * emitted and no enqueued checkpoint is dropped. Deliberately does not stop inner services (validator/HA
652
+ * signer, publishers), so the slashing-protection store stays open and the sequencer stays restartable.
653
+ * Used by tests that pause sequencers around an L1 clock warp. Idempotent.
654
+ */ async pause() {
655
+ if (!this.runningPromise?.isRunning()) {
656
+ this.log.debug('Sequencer not running, ignoring pause');
657
+ return;
658
+ }
659
+ this.log.info('Pausing sequencer');
660
+ // Halt the poll loop and let the in-flight iteration finish naturally — no interrupt, and no STOPPING
661
+ // state, since entering STOPPING would make the iteration's own setState calls throw and fail it.
662
+ // work() registers its fire-and-forget requests synchronously before returning, so once the loop has
663
+ // stopped pendingRequests holds every request that will ever exist; awaiting it lets them all finish.
664
+ await this.runningPromise.stop();
665
+ await this.pendingRequests.awaitRequests();
666
+ this.log.info('Paused sequencer');
667
+ }
608
668
  /** Main sequencer loop with a try/catch */ async safeWork() {
609
669
  try {
610
670
  await this.work();
@@ -731,18 +791,6 @@ _dec = trackSpan('Sequencer.work'), _dec1 = trackSpan('Sequencer.prepareCheckpoi
731
791
  }
732
792
  return undefined;
733
793
  }
734
- // We are the proposer and the escape hatch is closed: now run the full sync check before building.
735
- const syncedTo = await this.checkSync({
736
- ts,
737
- slot
738
- });
739
- if (!syncedTo) {
740
- await this.tryVoteWhenCannotBuild({
741
- slot,
742
- targetSlot
743
- });
744
- return undefined;
745
- }
746
794
  // Explicit build-loop entry gate: if we are past the latest useful block-building start for the
747
795
  // target slot, abandon building for this slot. The proposer prioritizes the ideal L1-publish path
748
796
  // and does not plan around the late consensus-handoff path. This is the proposer build path's
@@ -759,13 +807,22 @@ _dec = trackSpan('Sequencer.work'), _dec1 = trackSpan('Sequencer.prepareCheckpoi
759
807
  });
760
808
  // Mark the slot as attempted so a deadline abort is not retried within the same slot. Vote-only actions
761
809
  // still need to run because sync can succeed even when it is too late to start building a checkpoint.
762
- await this.tryVoteWhenCannotBuild({
810
+ await this.tryVoteAndPruneWhenCannotBuild({
763
811
  slot,
764
812
  targetSlot
765
813
  });
766
814
  this.lastSlotForCheckpointProposalJob = targetSlot;
767
815
  return undefined;
768
816
  }
817
+ // We are the proposer, the escape hatch is closed, and we have time before the build start deadline.
818
+ // Now run the full sync check before building.
819
+ const syncedTo = await this.checkSync({
820
+ ts,
821
+ slot
822
+ });
823
+ if (!syncedTo) {
824
+ return undefined;
825
+ }
769
826
  // Next checkpoint follows from the last synced one
770
827
  const checkpointNumber = CheckpointNumber(syncedTo.checkpointNumber + 1);
771
828
  const logCtx = {
@@ -926,7 +983,7 @@ _dec = trackSpan('Sequencer.work'), _dec1 = trackSpan('Sequencer.prepareCheckpoi
926
983
  return this.createCheckpointProposalJob(targetSlot, targetEpoch, checkpointNumber, syncedTo.blockNumber, syncedTo.checkpointedCheckpointNumber, proposer, publisher, attestorAddress, invalidateCheckpoint, syncedTo.proposedCheckpointData);
927
984
  }
928
985
  createCheckpointProposalJob(targetSlot, targetEpoch, checkpointNumber, syncedToBlockNumber, checkpointedCheckpointNumber, proposer, publisher, attestorAddress, invalidateCheckpoint, proposedCheckpointData) {
929
- return new CheckpointProposalJob(targetSlot, targetEpoch, checkpointNumber, syncedToBlockNumber, checkpointedCheckpointNumber, proposer, publisher, attestorAddress, invalidateCheckpoint, this.validatorClient, this.globalsBuilder, this.p2pClient, this.worldState, this.l1ToL2MessageSource, this.l2BlockSource, this.checkpointsBuilder, this.l2BlockSource, this.l1Constants, this.signatureContext, this.config, this.timetable, this.slasherClient, this.epochCache, this.dateProvider, this.metrics, this.checkpointProposalJobMetrics.createRecorder(), this, this.setState.bind(this), this.tracer, this.log.getBindings(), proposedCheckpointData);
986
+ return new CheckpointProposalJob(targetSlot, targetEpoch, checkpointNumber, syncedToBlockNumber, checkpointedCheckpointNumber, proposer, publisher, attestorAddress, invalidateCheckpoint, this.validatorClient, this.globalsBuilder, this.p2pClient, this.worldState, this.l1ToL2MessageSource, this.l2BlockSource, this.checkpointsBuilder, this.l2BlockSource, this.l1Constants, this.signatureContext, this.config, this.timetable, this.slasherClient, this.epochCache, this.dateProvider, this.metrics, this.checkpointProposalJobMetrics.createRecorder(), this, this.pendingRequests, this.setState.bind(this), this.tracer, this.log.getBindings(), proposedCheckpointData);
930
987
  }
931
988
  /**
932
989
  * Returns the current sequencer state.
@@ -1008,8 +1065,7 @@ _dec = trackSpan('Sequencer.work'), _dec1 = trackSpan('Sequencer.prepareCheckpoi
1008
1065
  })),
1009
1066
  this.l2BlockSource.getL2Tips().then((t)=>({
1010
1067
  proposed: t.proposed,
1011
- checkpointed: t.checkpointed,
1012
- proposedCheckpoint: t.proposedCheckpoint
1068
+ checkpointed: t.checkpointed
1013
1069
  })),
1014
1070
  this.p2pClient.getStatus().then((p2p)=>p2p.syncedToL2Block),
1015
1071
  this.l1ToL2MessageSource.getL2Tips().then((t)=>({
@@ -1049,12 +1105,14 @@ _dec = trackSpan('Sequencer.work'), _dec1 = trackSpan('Sequencer.prepareCheckpoi
1049
1105
  // matching proposed checkpoint (e.g. it crashed before assembling it). Building on this orphan block
1050
1106
  // would fork the chain off a tip no other node can follow. The archiver prunes these orphan blocks
1051
1107
  // once their build slot ends; this guard is the correctness barrier during the grace window before.
1052
- if (blockData.checkpointNumber > l2Tips.checkpointed.checkpoint.number && (l2Tips.proposedCheckpoint.checkpoint.number !== blockData.checkpointNumber || proposedCheckpointData?.checkpointNumber !== blockData.checkpointNumber)) {
1108
+ // `getProposedCheckpointData()` returns the latest proposed checkpoint payload, which is always
1109
+ // the leading one (a proposed entry is only stored beyond the confirmed frontier and is deleted
1110
+ // on confirmation). It carries no tip, so there is no tip-vs-payload split read to reconcile.
1111
+ if (blockData.checkpointNumber > l2Tips.checkpointed.checkpoint.number && proposedCheckpointData?.checkpointNumber !== blockData.checkpointNumber) {
1053
1112
  const logCtx = {
1054
1113
  blockCheckpointNumber: blockData.checkpointNumber,
1055
1114
  checkpointedCheckpointNumber: l2Tips.checkpointed.checkpoint.number,
1056
- proposedCheckpointTipNumber: l2Tips.proposedCheckpoint.checkpoint.number,
1057
- proposedCheckpointDataNumber: proposedCheckpointData?.checkpointNumber,
1115
+ proposedCheckpointTipNumber: proposedCheckpointData?.checkpointNumber,
1058
1116
  blockNumber: blockData.header.getBlockNumber(),
1059
1117
  blockSlot: blockData.header.getSlot(),
1060
1118
  syncedL2Slot,
@@ -1063,22 +1121,7 @@ _dec = trackSpan('Sequencer.work'), _dec1 = trackSpan('Sequencer.prepareCheckpoi
1063
1121
  this.log.debug(`Waiting for proposed checkpoint to catch up with reexecuted block`, logCtx);
1064
1122
  return undefined;
1065
1123
  }
1066
- const hasProposedCheckpoint = l2Tips.proposedCheckpoint.checkpoint.number > l2Tips.checkpointed.checkpoint.number;
1067
- // The l2Tips and proposedCheckpointData reads above come from independent archiver snapshots
1068
- // (a JS-side tips cache vs. a direct store read on `#proposedCheckpoints`). A concurrent archiver
1069
- // write that mutates both can be observed split, leaving us with `hasProposedCheckpoint=true` but
1070
- // no proposedCheckpointData (or one whose number doesn't match the tip). Refuse to proceed in that
1071
- // window — the next checkSync tick will see a coherent snapshot.
1072
- if (hasProposedCheckpoint && (!proposedCheckpointData || proposedCheckpointData.checkpointNumber !== l2Tips.proposedCheckpoint.checkpoint.number)) {
1073
- this.log.warn(`Sequencer sync check failed: inconsistent proposed-checkpoint state`, {
1074
- proposedCheckpointTipNumber: l2Tips.proposedCheckpoint.checkpoint.number,
1075
- checkpointedTipNumber: l2Tips.checkpointed.checkpoint.number,
1076
- proposedCheckpointDataNumber: proposedCheckpointData?.checkpointNumber,
1077
- syncedL2Slot,
1078
- ...args
1079
- });
1080
- return undefined;
1081
- }
1124
+ const hasProposedCheckpoint = proposedCheckpointData !== undefined;
1082
1125
  // Check that the proposed checkpoint is indeed the parent of the checkpoint we'll be building
1083
1126
  // The checkpoint number to build is derived as blockData.checkpointNumber + 1
1084
1127
  if (proposedCheckpointData && proposedCheckpointData.checkpointNumber !== blockData.checkpointNumber) {
@@ -1163,13 +1206,14 @@ _dec = trackSpan('Sequencer.work'), _dec1 = trackSpan('Sequencer.prepareCheckpoi
1163
1206
  ];
1164
1207
  }
1165
1208
  /**
1166
- * Tries to vote on slashing actions and governance when we cannot build and are past the block-building window.
1167
- * This allows the sequencer to participate in governance/slashing votes even when it cannot build blocks.
1168
- */ async tryVoteWhenCannotBuild(args) {
1209
+ * Tries to vote on slashing actions and governance and to prune when we cannot build and are past the
1210
+ * block-building window. This allows the sequencer to participate in governance/slashing votes even when it
1211
+ * cannot build blocks, and to prune the pending chain so it can recover from bad data that is blocking sync.
1212
+ */ async tryVoteAndPruneWhenCannotBuild(args) {
1169
1213
  const { slot, targetSlot } = args;
1170
1214
  // Prevent duplicate attempts in the same slot
1171
- if (this.lastSlotForFallbackVote === slot) {
1172
- this.log.trace(`Already attempted to vote in slot ${slot} (skipping)`);
1215
+ if (this.lastSlotForFallbackAction === slot) {
1216
+ this.log.trace(`Already attempted fallback actions in slot ${slot} (skipping)`);
1173
1217
  return;
1174
1218
  }
1175
1219
  // Vote-only actions do not give up the slot: if sync recovers, a later work-loop iteration can still build.
@@ -1191,7 +1235,7 @@ _dec = trackSpan('Sequencer.work'), _dec1 = trackSpan('Sequencer.prepareCheckpoi
1191
1235
  return;
1192
1236
  }
1193
1237
  // Mark this slot as attempted
1194
- this.lastSlotForFallbackVote = slot;
1238
+ this.lastSlotForFallbackAction = slot;
1195
1239
  // Get a publisher for voting
1196
1240
  const { attestorAddress, publisher } = await this.publisherFactory.create(proposer);
1197
1241
  this.log.debug(`Attempting to vote despite sync failure at slot ${slot}`, {
@@ -1202,22 +1246,41 @@ _dec = trackSpan('Sequencer.work'), _dec1 = trackSpan('Sequencer.prepareCheckpoi
1202
1246
  const voter = new CheckpointVoter(targetSlot, publisher, attestorAddress, this.validatorClient, this.slasherClient, this.l1Constants, this.config, this.metrics, this.log);
1203
1247
  const votesPromises = voter.enqueueVotes();
1204
1248
  const votes = await Promise.all(votesPromises);
1205
- if (votes.every((p)=>!p)) {
1206
- this.log.debug(`No votes to enqueue for slot ${slot}`);
1249
+ // Even if we cannot build, try to prune so a stuck pending chain (e.g. bad data blocking sync) can
1250
+ // recover. prune() is permissionless, so it rides the same fallback multicall as the votes.
1251
+ const pruneEnqueued = await this.tryEnqueuePruneIfPrunable(targetSlot, publisher);
1252
+ // Bail if nothing to do
1253
+ if (votes.every((p)=>!p) && !pruneEnqueued) {
1254
+ this.log.debug(`Nothing to enqueue for slot ${slot} (no votes, not prunable)`);
1207
1255
  return;
1208
1256
  }
1209
- this.log.info(`Voting in slot ${slot} despite sync failure`, {
1210
- slot
1257
+ const [governanceVoteEnqueued, slashingVoteEnqueued] = votes;
1258
+ this.log.info(`Submitting fallback requests in slot ${slot} despite sync failure`, {
1259
+ slot,
1260
+ pruneEnqueued,
1261
+ governanceVoteEnqueued: !!governanceVoteEnqueued,
1262
+ slashingVoteEnqueued: !!slashingVoteEnqueued
1211
1263
  });
1212
1264
  // Votes are EIP-712-signed for `targetSlot` (the pipelined slot in which the multicall is
1213
1265
  // expected to mine). Delay submission to the start of `targetSlot` so the tx mines in the
1214
1266
  // slot the votes were signed for. We fire-and-forget so we don't block the sequencer's
1215
- // work loop while waiting for the target slot to start.
1216
- void publisher.sendRequestsAt(targetSlot).catch((err)=>{
1217
- this.log.error(`Failed to publish votes despite sync failure for slot ${slot}`, err, {
1267
+ // work loop while waiting for the target slot to start, but track it so stop() can drain it.
1268
+ const send = publisher.sendRequestsAt(targetSlot).catch((err)=>{
1269
+ this.log.error(`Failed to publish fallback requests despite sync failure for slot ${slot}`, err, {
1218
1270
  slot
1219
1271
  });
1220
1272
  });
1273
+ this.pendingRequests.trackRequest(send, ()=>publisher.interrupt());
1274
+ }
1275
+ async tryEnqueuePruneIfPrunable(targetSlot, publisher) {
1276
+ try {
1277
+ return await publisher.enqueuePruneIfPrunable(targetSlot);
1278
+ } catch (err) {
1279
+ this.log.error(`Failed to enqueue rollup prune for slot ${targetSlot}`, err, {
1280
+ targetSlot
1281
+ });
1282
+ return false;
1283
+ }
1221
1284
  }
1222
1285
  /**
1223
1286
  * Tries to vote on slashing actions and governance proposals when escape hatch is open.
@@ -1225,12 +1288,12 @@ _dec = trackSpan('Sequencer.work'), _dec1 = trackSpan('Sequencer.prepareCheckpoi
1225
1288
  */ async tryVoteWhenEscapeHatchOpen(args) {
1226
1289
  const { slot, targetSlot, proposer } = args;
1227
1290
  // Prevent duplicate attempts in the same slot
1228
- if (this.lastSlotForFallbackVote === slot) {
1291
+ if (this.lastSlotForFallbackAction === slot) {
1229
1292
  this.log.trace(`Already attempted to vote in slot ${slot} (escape hatch open, skipping)`);
1230
1293
  return;
1231
1294
  }
1232
1295
  // Mark this slot as attempted
1233
- this.lastSlotForFallbackVote = slot;
1296
+ this.lastSlotForFallbackAction = slot;
1234
1297
  const { attestorAddress, publisher } = await this.publisherFactory.create(proposer);
1235
1298
  this.log.debug(`Escape hatch open for slot ${slot}, attempting vote-only actions`, {
1236
1299
  slot,
@@ -1256,13 +1319,15 @@ _dec = trackSpan('Sequencer.work'), _dec1 = trackSpan('Sequencer.prepareCheckpoi
1256
1319
  // the multicall mines in the slot the votes were signed for; otherwise the L1 contract reads
1257
1320
  // `signaler = getCurrentProposer()` against the wrong slot and signature verification fails
1258
1321
  // silently inside Multicall3. Fire-and-forget so we don't block the sequencer's work loop while
1259
- // waiting for the target slot to start, mirroring tryVoteWhenCannotBuild.
1260
- void publisher.sendRequestsAt(targetSlot).catch((err)=>{
1322
+ // waiting for the target slot to start, mirroring tryVoteAndPruneWhenCannotBuild, but tracked so
1323
+ // stop() can drain it.
1324
+ const send = publisher.sendRequestsAt(targetSlot).catch((err)=>{
1261
1325
  this.log.error(`Failed to publish escape-hatch votes for slot ${slot}`, err, {
1262
1326
  slot,
1263
1327
  targetSlot
1264
1328
  });
1265
1329
  });
1330
+ this.pendingRequests.trackRequest(send, ()=>publisher.interrupt());
1266
1331
  }
1267
1332
  /**
1268
1333
  * Considers invalidating a block if the pending chain is invalid. Depends on how long the invalid block
@@ -64,4 +64,4 @@ export declare function setupTxsAndBlock(p2p: MockProxy<P2P>, globalVariables: G
64
64
  txs: Tx[];
65
65
  block: L2Block;
66
66
  }>;
67
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidXRpbHMuZC50cyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy90ZXN0L3V0aWxzLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUdBLE9BQU8sRUFBRSxlQUFlLEVBQUUsTUFBTSwyQ0FBMkMsQ0FBQztBQUM1RSxPQUFPLEVBQUUsRUFBRSxFQUFFLE1BQU0sZ0NBQWdDLENBQUM7QUFDcEQsT0FBTyxLQUFLLEVBQUUsVUFBVSxFQUFFLE1BQU0sK0JBQStCLENBQUM7QUFDaEUsT0FBTyxFQUFFLFNBQVMsRUFBRSxNQUFNLGlDQUFpQyxDQUFDO0FBQzVELE9BQU8sS0FBSyxFQUFFLEdBQUcsRUFBRSxNQUFNLFlBQVksQ0FBQztBQUV0QyxPQUFPLEVBQUUsb0JBQW9CLEVBQUUsT0FBTyxFQUFFLE1BQU0scUJBQXFCLENBQUM7QUFFcEUsT0FBTyxLQUFLLEVBQUUsaUJBQWlCLEVBQUUsTUFBTSw2QkFBNkIsQ0FBQztBQUNyRSxPQUFPLEVBQUUsYUFBYSxFQUFFLHFCQUFxQixFQUFFLGtCQUFrQixFQUFvQixNQUFNLG1CQUFtQixDQUFDO0FBTy9HLE9BQU8sRUFLTCxpQkFBaUIsRUFDbEIsTUFBTSx5QkFBeUIsQ0FBQztBQUNqQyxPQUFPLEVBQWUsZUFBZSxFQUFFLEtBQUssRUFBRSxFQUFvQyxNQUFNLGtCQUFrQixDQUFDO0FBRTNHLE9BQU8sS0FBSyxFQUFFLFNBQVMsRUFBRSxNQUFNLG9CQUFvQixDQUFDO0FBR3BELE9BQU8sRUFBRSxxQkFBcUIsRUFBRSxzQkFBc0IsRUFBRSxNQUFNLDhCQUE4QixDQUFDO0FBRTdGOzs7O0dBSUc7QUFDSCx3QkFBZ0IscUJBQXFCLENBQUMsSUFBSSxFQUFFO0lBQzFDLFdBQVcsRUFBRSxJQUFJLENBQUMsaUJBQWlCLEVBQUUsZUFBZSxHQUFHLGNBQWMsR0FBRyxzQkFBc0IsQ0FBQyxDQUFDO0lBQ2hHLGVBQWUsQ0FBQyxFQUFFLE1BQU0sQ0FBQztJQUN6QixnQkFBZ0IsQ0FBQyxFQUFFLE1BQU0sQ0FBQztJQUMxQixrQkFBa0IsQ0FBQyxFQUFFLE1BQU0sQ0FBQztJQUM1Qiw2QkFBNkIsQ0FBQyxFQUFFLE1BQU0sQ0FBQztDQUN4QyxHQUFHLGlCQUFpQixDQVNwQjtBQUVEOztHQUVHO0FBQ0gsd0JBQXNCLE1BQU0sQ0FBQyxJQUFJLENBQUMsRUFBRSxNQUFNLEVBQUUsT0FBTyxDQUFDLEVBQUUsRUFBRSxHQUFHLE9BQU8sQ0FBQyxFQUFFLENBQUMsQ0FNckU7QUFFRDs7R0FFRztBQUNILHdCQUFzQixTQUFTLENBQUMsR0FBRyxFQUFFLEVBQUUsRUFBRSxFQUFFLGVBQWUsRUFBRSxlQUFlLEdBQUcsT0FBTyxDQUFDLE9BQU8sQ0FBQyxDQWdCN0Y7QUFFRDs7R0FFRztBQUNILHdCQUFnQixjQUFjLENBQUMsR0FBRyxFQUFFLFNBQVMsQ0FBQyxHQUFHLENBQUMsRUFBRSxHQUFHLEVBQUUsRUFBRSxFQUFFLEdBQUcsSUFBSSxDQUluRTtBQUVEOztHQUVHO0FBQ0gsd0JBQXVCLGNBQWMsQ0FBQyxHQUFHLEVBQUUsT0FBTyxDQUFDLEVBQUUsRUFBRSxDQUFDLEdBQUcscUJBQXFCLENBQUMsRUFBRSxDQUFDLENBSW5GO0FBRUQ7O0dBRUc7QUFDSCx3QkFBZ0Isb0JBQW9CLENBQUMsTUFBTSxFQUFFLGVBQWUsR0FBRyxvQkFBb0IsRUFBRSxDQUdwRjtBQXVCRDs7R0FFRztBQUNILHdCQUFnQixtQkFBbUIsQ0FBQyxLQUFLLEVBQUUsT0FBTyxFQUFFLFNBQVMsRUFBRSxTQUFTLEdBQUcsYUFBYSxDQVd2RjtBQUVEOztHQUVHO0FBQ0gsd0JBQWdCLHdCQUF3QixDQUN0QyxLQUFLLEVBQUUsT0FBTyxFQUNkLG1CQUFtQixFQUFFLFNBQVMsRUFDOUIsY0FBYyxDQUFDLEVBQUUsU0FBUyxFQUMxQixxQkFBcUIsR0FBRSxNQUFXLEdBQ2pDLGtCQUFrQixDQWdCcEI7QUFFRDs7OztHQUlHO0FBQ0gsd0JBQWdCLDJCQUEyQixDQUN6QyxLQUFLLEVBQUUsT0FBTyxFQUNkLFNBQVMsRUFBRSxTQUFTLEVBQ3BCLE1BQU0sRUFBRSxVQUFVLEVBQ2xCLHFCQUFxQixHQUFFLE1BQVcsR0FDakMscUJBQXFCLENBYXZCO0FBRUQ7OztHQUdHO0FBQ0gsd0JBQXNCLGdCQUFnQixDQUNwQyxHQUFHLEVBQUUsU0FBUyxDQUFDLEdBQUcsQ0FBQyxFQUNuQixlQUFlLEVBQUUsZUFBZSxFQUNoQyxPQUFPLEVBQUUsTUFBTSxFQUNmLE9BQU8sRUFBRSxFQUFFLEdBQ1YsT0FBTyxDQUFDO0lBQUUsR0FBRyxFQUFFLEVBQUUsRUFBRSxDQUFDO0lBQUMsS0FBSyxFQUFFLE9BQU8sQ0FBQTtDQUFFLENBQUMsQ0FLeEMifQ==
67
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidXRpbHMuZC50cyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy90ZXN0L3V0aWxzLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUdBLE9BQU8sRUFBRSxlQUFlLEVBQUUsTUFBTSwyQ0FBMkMsQ0FBQztBQUM1RSxPQUFPLEVBQUUsRUFBRSxFQUFFLE1BQU0sZ0NBQWdDLENBQUM7QUFDcEQsT0FBTyxLQUFLLEVBQUUsVUFBVSxFQUFFLE1BQU0sK0JBQStCLENBQUM7QUFDaEUsT0FBTyxFQUFFLFNBQVMsRUFBRSxNQUFNLGlDQUFpQyxDQUFDO0FBQzVELE9BQU8sS0FBSyxFQUFFLEdBQUcsRUFBRSxNQUFNLFlBQVksQ0FBQztBQUV0QyxPQUFPLEVBQUUsb0JBQW9CLEVBQUUsT0FBTyxFQUFFLE1BQU0scUJBQXFCLENBQUM7QUFFcEUsT0FBTyxLQUFLLEVBQUUsaUJBQWlCLEVBQUUsTUFBTSw2QkFBNkIsQ0FBQztBQUNyRSxPQUFPLEVBQUUsYUFBYSxFQUFFLHFCQUFxQixFQUFFLGtCQUFrQixFQUFvQixNQUFNLG1CQUFtQixDQUFDO0FBTy9HLE9BQU8sRUFLTCxpQkFBaUIsRUFDbEIsTUFBTSx5QkFBeUIsQ0FBQztBQUNqQyxPQUFPLEVBQWUsZUFBZSxFQUFFLEtBQUssRUFBRSxFQUFvQyxNQUFNLGtCQUFrQixDQUFDO0FBRTNHLE9BQU8sS0FBSyxFQUFFLFNBQVMsRUFBRSxNQUFNLG9CQUFvQixDQUFDO0FBR3BELE9BQU8sRUFBRSxxQkFBcUIsRUFBRSxzQkFBc0IsRUFBRSxNQUFNLDhCQUE4QixDQUFDO0FBRTdGOzs7O0dBSUc7QUFDSCx3QkFBZ0IscUJBQXFCLENBQUMsSUFBSSxFQUFFO0lBQzFDLFdBQVcsRUFBRSxJQUFJLENBQUMsaUJBQWlCLEVBQUUsZUFBZSxHQUFHLGNBQWMsR0FBRyxzQkFBc0IsQ0FBQyxDQUFDO0lBQ2hHLGVBQWUsQ0FBQyxFQUFFLE1BQU0sQ0FBQztJQUN6QixnQkFBZ0IsQ0FBQyxFQUFFLE1BQU0sQ0FBQztJQUMxQixrQkFBa0IsQ0FBQyxFQUFFLE1BQU0sQ0FBQztJQUM1Qiw2QkFBNkIsQ0FBQyxFQUFFLE1BQU0sQ0FBQztDQUN4QyxHQUFHLGlCQUFpQixDQVNwQjtBQUVEOztHQUVHO0FBQ0gsd0JBQXNCLE1BQU0sQ0FBQyxJQUFJLENBQUMsRUFBRSxNQUFNLEVBQUUsT0FBTyxDQUFDLEVBQUUsRUFBRSxHQUFHLE9BQU8sQ0FBQyxFQUFFLENBQUMsQ0FNckU7QUFFRDs7R0FFRztBQUNILHdCQUFzQixTQUFTLENBQUMsR0FBRyxFQUFFLEVBQUUsRUFBRSxFQUFFLGVBQWUsRUFBRSxlQUFlLEdBQUcsT0FBTyxDQUFDLE9BQU8sQ0FBQyxDQWdCN0Y7QUFFRDs7R0FFRztBQUNILHdCQUFnQixjQUFjLENBQUMsR0FBRyxFQUFFLFNBQVMsQ0FBQyxHQUFHLENBQUMsRUFBRSxHQUFHLEVBQUUsRUFBRSxFQUFFLEdBQUcsSUFBSSxDQUtuRTtBQUVEOztHQUVHO0FBQ0gsd0JBQXVCLGNBQWMsQ0FBQyxHQUFHLEVBQUUsT0FBTyxDQUFDLEVBQUUsRUFBRSxDQUFDLEdBQUcscUJBQXFCLENBQUMsRUFBRSxDQUFDLENBSW5GO0FBRUQ7O0dBRUc7QUFDSCx3QkFBZ0Isb0JBQW9CLENBQUMsTUFBTSxFQUFFLGVBQWUsR0FBRyxvQkFBb0IsRUFBRSxDQUdwRjtBQXdCRDs7R0FFRztBQUNILHdCQUFnQixtQkFBbUIsQ0FBQyxLQUFLLEVBQUUsT0FBTyxFQUFFLFNBQVMsRUFBRSxTQUFTLEdBQUcsYUFBYSxDQVd2RjtBQUVEOztHQUVHO0FBQ0gsd0JBQWdCLHdCQUF3QixDQUN0QyxLQUFLLEVBQUUsT0FBTyxFQUNkLG1CQUFtQixFQUFFLFNBQVMsRUFDOUIsY0FBYyxDQUFDLEVBQUUsU0FBUyxFQUMxQixxQkFBcUIsR0FBRSxNQUFXLEdBQ2pDLGtCQUFrQixDQWdCcEI7QUFFRDs7OztHQUlHO0FBQ0gsd0JBQWdCLDJCQUEyQixDQUN6QyxLQUFLLEVBQUUsT0FBTyxFQUNkLFNBQVMsRUFBRSxTQUFTLEVBQ3BCLE1BQU0sRUFBRSxVQUFVLEVBQ2xCLHFCQUFxQixHQUFFLE1BQVcsR0FDakMscUJBQXFCLENBYXZCO0FBRUQ7OztHQUdHO0FBQ0gsd0JBQXNCLGdCQUFnQixDQUNwQyxHQUFHLEVBQUUsU0FBUyxDQUFDLEdBQUcsQ0FBQyxFQUNuQixlQUFlLEVBQUUsZUFBZSxFQUNoQyxPQUFPLEVBQUUsTUFBTSxFQUNmLE9BQU8sRUFBRSxFQUFFLEdBQ1YsT0FBTyxDQUFDO0lBQUUsR0FBRyxFQUFFLEVBQUUsRUFBRSxDQUFDO0lBQUMsS0FBSyxFQUFFLE9BQU8sQ0FBQTtDQUFFLENBQUMsQ0FLeEMifQ==
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/test/utils.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,eAAe,EAAE,MAAM,2CAA2C,CAAC;AAC5E,OAAO,EAAE,EAAE,EAAE,MAAM,gCAAgC,CAAC;AACpD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAC;AAChE,OAAO,EAAE,SAAS,EAAE,MAAM,iCAAiC,CAAC;AAC5D,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AAEtC,OAAO,EAAE,oBAAoB,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAEpE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AACrE,OAAO,EAAE,aAAa,EAAE,qBAAqB,EAAE,kBAAkB,EAAoB,MAAM,mBAAmB,CAAC;AAO/G,OAAO,EAKL,iBAAiB,EAClB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAe,eAAe,EAAE,KAAK,EAAE,EAAoC,MAAM,kBAAkB,CAAC;AAE3G,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAGpD,OAAO,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,MAAM,8BAA8B,CAAC;AAE7F;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE;IAC1C,WAAW,EAAE,IAAI,CAAC,iBAAiB,EAAE,eAAe,GAAG,cAAc,GAAG,sBAAsB,CAAC,CAAC;IAChG,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,6BAA6B,CAAC,EAAE,MAAM,CAAC;CACxC,GAAG,iBAAiB,CASpB;AAED;;GAEG;AACH,wBAAsB,MAAM,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,EAAE,GAAG,OAAO,CAAC,EAAE,CAAC,CAMrE;AAED;;GAEG;AACH,wBAAsB,SAAS,CAAC,GAAG,EAAE,EAAE,EAAE,EAAE,eAAe,EAAE,eAAe,GAAG,OAAO,CAAC,OAAO,CAAC,CAgB7F;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,SAAS,CAAC,GAAG,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE,GAAG,IAAI,CAInE;AAED;;GAEG;AACH,wBAAuB,cAAc,CAAC,GAAG,EAAE,OAAO,CAAC,EAAE,EAAE,CAAC,GAAG,qBAAqB,CAAC,EAAE,CAAC,CAInF;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,eAAe,GAAG,oBAAoB,EAAE,CAGpF;AAuBD;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,GAAG,aAAa,CAWvF;AAED;;GAEG;AACH,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,OAAO,EACd,mBAAmB,EAAE,SAAS,EAC9B,cAAc,CAAC,EAAE,SAAS,EAC1B,qBAAqB,GAAE,MAAW,GACjC,kBAAkB,CAgBpB;AAED;;;;GAIG;AACH,wBAAgB,2BAA2B,CACzC,KAAK,EAAE,OAAO,EACd,SAAS,EAAE,SAAS,EACpB,MAAM,EAAE,UAAU,EAClB,qBAAqB,GAAE,MAAW,GACjC,qBAAqB,CAavB;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,SAAS,CAAC,GAAG,CAAC,EACnB,eAAe,EAAE,eAAe,EAChC,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,EAAE,GACV,OAAO,CAAC;IAAE,GAAG,EAAE,EAAE,EAAE,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAAC,CAKxC"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/test/utils.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,eAAe,EAAE,MAAM,2CAA2C,CAAC;AAC5E,OAAO,EAAE,EAAE,EAAE,MAAM,gCAAgC,CAAC;AACpD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAC;AAChE,OAAO,EAAE,SAAS,EAAE,MAAM,iCAAiC,CAAC;AAC5D,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AAEtC,OAAO,EAAE,oBAAoB,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAEpE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AACrE,OAAO,EAAE,aAAa,EAAE,qBAAqB,EAAE,kBAAkB,EAAoB,MAAM,mBAAmB,CAAC;AAO/G,OAAO,EAKL,iBAAiB,EAClB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAe,eAAe,EAAE,KAAK,EAAE,EAAoC,MAAM,kBAAkB,CAAC;AAE3G,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAGpD,OAAO,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,MAAM,8BAA8B,CAAC;AAE7F;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE;IAC1C,WAAW,EAAE,IAAI,CAAC,iBAAiB,EAAE,eAAe,GAAG,cAAc,GAAG,sBAAsB,CAAC,CAAC;IAChG,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,6BAA6B,CAAC,EAAE,MAAM,CAAC;CACxC,GAAG,iBAAiB,CASpB;AAED;;GAEG;AACH,wBAAsB,MAAM,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,EAAE,GAAG,OAAO,CAAC,EAAE,CAAC,CAMrE;AAED;;GAEG;AACH,wBAAsB,SAAS,CAAC,GAAG,EAAE,EAAE,EAAE,EAAE,eAAe,EAAE,eAAe,GAAG,OAAO,CAAC,OAAO,CAAC,CAgB7F;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,SAAS,CAAC,GAAG,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE,GAAG,IAAI,CAKnE;AAED;;GAEG;AACH,wBAAuB,cAAc,CAAC,GAAG,EAAE,OAAO,CAAC,EAAE,EAAE,CAAC,GAAG,qBAAqB,CAAC,EAAE,CAAC,CAInF;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,eAAe,GAAG,oBAAoB,EAAE,CAGpF;AAwBD;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,GAAG,aAAa,CAWvF;AAED;;GAEG;AACH,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,OAAO,EACd,mBAAmB,EAAE,SAAS,EAC9B,cAAc,CAAC,EAAE,SAAS,EAC1B,qBAAqB,GAAE,MAAW,GACjC,kBAAkB,CAgBpB;AAED;;;;GAIG;AACH,wBAAgB,2BAA2B,CACzC,KAAK,EAAE,OAAO,EACd,SAAS,EAAE,SAAS,EACpB,MAAM,EAAE,UAAU,EAClB,qBAAqB,GAAE,MAAW,GACjC,qBAAqB,CAavB;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,SAAS,CAAC,GAAG,CAAC,EACnB,eAAe,EAAE,eAAe,EAChC,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,EAAE,GACV,OAAO,CAAC;IAAE,GAAG,EAAE,EAAE,EAAE,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAAC,CAKxC"}
@@ -51,6 +51,7 @@ export { MockCheckpointBuilder, MockCheckpointsBuilder } from './mock_checkpoint
51
51
  * Mocks the P2P client to return specific pending transactions
52
52
  */ export function mockPendingTxs(p2p, txs) {
53
53
  p2p.getPendingTxCount.mockResolvedValue(txs.length);
54
+ p2p.hasEligiblePendingTxs.mockImplementation((minCount)=>Promise.resolve(txs.length >= minCount));
54
55
  p2p.iteratePendingTxs.mockImplementation(()=>mockTxIterator(Promise.resolve(txs)));
55
56
  p2p.iterateEligiblePendingTxs.mockImplementation(()=>mockTxIterator(Promise.resolve(txs)));
56
57
  }
@@ -74,7 +75,7 @@ export { MockCheckpointBuilder, MockCheckpointsBuilder } from './mock_checkpoint
74
75
  * Uses mock values for blockHeadersHash, blobsHash and inHash since L2Block doesn't have these fields.
75
76
  */ function createCheckpointHeaderFromBlock(block) {
76
77
  const gv = block.header.globalVariables;
77
- return new CheckpointHeader(block.header.lastArchive.root, Fr.random(), Fr.random(), Fr.random(), Fr.random(), gv.slotNumber, gv.timestamp, gv.coinbase, gv.feeRecipient, gv.gasFees, block.header.totalManaUsed);
78
+ return new CheckpointHeader(block.header.lastArchive.root, Fr.random(), Fr.random(), Fr.random(), Fr.random(), gv.slotNumber, gv.timestamp, gv.coinbase, gv.feeRecipient, gv.gasFees, block.header.totalManaUsed, block.header.totalFees);
78
79
  }
79
80
  /**
80
81
  * Creates a block proposal from a block and signature
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aztec/sequencer-client",
3
- "version": "5.0.0-rc.1",
3
+ "version": "5.0.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./dest/index.js",
@@ -27,37 +27,37 @@
27
27
  "test:integration:run": "NODE_NO_WARNINGS=1 node --experimental-vm-modules $(yarn bin jest) --no-cache --config jest.integration.config.json"
28
28
  },
29
29
  "dependencies": {
30
- "@aztec/aztec.js": "5.0.0-rc.1",
31
- "@aztec/bb-prover": "5.0.0-rc.1",
32
- "@aztec/blob-client": "5.0.0-rc.1",
33
- "@aztec/blob-lib": "5.0.0-rc.1",
34
- "@aztec/constants": "5.0.0-rc.1",
35
- "@aztec/epoch-cache": "5.0.0-rc.1",
36
- "@aztec/ethereum": "5.0.0-rc.1",
37
- "@aztec/foundation": "5.0.0-rc.1",
38
- "@aztec/l1-artifacts": "5.0.0-rc.1",
39
- "@aztec/node-keystore": "5.0.0-rc.1",
40
- "@aztec/noir-acvm_js": "5.0.0-rc.1",
41
- "@aztec/noir-contracts.js": "5.0.0-rc.1",
42
- "@aztec/noir-protocol-circuits-types": "5.0.0-rc.1",
43
- "@aztec/noir-types": "5.0.0-rc.1",
44
- "@aztec/p2p": "5.0.0-rc.1",
45
- "@aztec/protocol-contracts": "5.0.0-rc.1",
46
- "@aztec/prover-client": "5.0.0-rc.1",
47
- "@aztec/simulator": "5.0.0-rc.1",
48
- "@aztec/slasher": "5.0.0-rc.1",
49
- "@aztec/stdlib": "5.0.0-rc.1",
50
- "@aztec/telemetry-client": "5.0.0-rc.1",
51
- "@aztec/validator-client": "5.0.0-rc.1",
52
- "@aztec/validator-ha-signer": "5.0.0-rc.1",
53
- "@aztec/world-state": "5.0.0-rc.1",
30
+ "@aztec/aztec.js": "5.0.0",
31
+ "@aztec/bb-prover": "5.0.0",
32
+ "@aztec/blob-client": "5.0.0",
33
+ "@aztec/blob-lib": "5.0.0",
34
+ "@aztec/constants": "5.0.0",
35
+ "@aztec/epoch-cache": "5.0.0",
36
+ "@aztec/ethereum": "5.0.0",
37
+ "@aztec/foundation": "5.0.0",
38
+ "@aztec/l1-artifacts": "5.0.0",
39
+ "@aztec/node-keystore": "5.0.0",
40
+ "@aztec/noir-acvm_js": "5.0.0",
41
+ "@aztec/noir-contracts.js": "5.0.0",
42
+ "@aztec/noir-protocol-circuits-types": "5.0.0",
43
+ "@aztec/noir-types": "5.0.0",
44
+ "@aztec/p2p": "5.0.0",
45
+ "@aztec/protocol-contracts": "5.0.0",
46
+ "@aztec/prover-client": "5.0.0",
47
+ "@aztec/simulator": "5.0.0",
48
+ "@aztec/slasher": "5.0.0",
49
+ "@aztec/stdlib": "5.0.0",
50
+ "@aztec/telemetry-client": "5.0.0",
51
+ "@aztec/validator-client": "5.0.0",
52
+ "@aztec/validator-ha-signer": "5.0.0",
53
+ "@aztec/world-state": "5.0.0",
54
54
  "lodash.chunk": "^4.2.0",
55
55
  "tslib": "^2.4.0",
56
56
  "viem": "npm:@aztec/viem@2.38.2"
57
57
  },
58
58
  "devDependencies": {
59
- "@aztec/archiver": "5.0.0-rc.1",
60
- "@aztec/kv-store": "5.0.0-rc.1",
59
+ "@aztec/archiver": "5.0.0",
60
+ "@aztec/kv-store": "5.0.0",
61
61
  "@electric-sql/pglite": "^0.3.14",
62
62
  "@jest/globals": "^30.0.0",
63
63
  "@types/jest": "^30.0.0",
@@ -180,16 +180,23 @@ export class SequencerClient {
180
180
  this.validatorClient?.updateConfig(config);
181
181
  }
182
182
 
183
- /** Starts the sequencer. */
183
+ /**
184
+ * Starts (or resumes) the sequencer, validator, publishers, and metrics. Each underlying start is
185
+ * idempotent, so this is safe to call after a previous {@link pause} to resume building and publishing.
186
+ * The publisher manager is started before the sequencer's poll loop so publishing is ready before the
187
+ * sequencer first tries to publish to L1.
188
+ */
184
189
  public async start() {
185
190
  await this.validatorClient?.start();
191
+ await this.publisherManager.start();
186
192
  this.sequencer.start();
187
193
  this.l1Metrics?.start();
188
- await this.publisherManager.start();
189
194
  }
190
195
 
191
196
  /**
192
- * Stops the sequencer from processing new txs.
197
+ * Stops the sequencer, validator, publishers, and metrics for good, draining in-flight work. This is the
198
+ * final teardown path: stopping the validator client closes its slashing-protection database. For a
199
+ * restartable pause (e.g. around a test clock warp) use {@link pause} instead.
193
200
  */
194
201
  public async stop() {
195
202
  await this.sequencer.stop();
@@ -198,6 +205,15 @@ export class SequencerClient {
198
205
  this.l1Metrics?.stop();
199
206
  }
200
207
 
208
+ /**
209
+ * Gracefully pauses block production, waiting for in-flight work to finish rather than interrupting it,
210
+ * while leaving the validator, publishers, and metrics running so the sequencer can be resumed with
211
+ * {@link start}. Used by tests that pause sequencers around an L1 clock warp.
212
+ */
213
+ public async pause() {
214
+ await this.sequencer.pause();
215
+ }
216
+
201
217
  /** Triggers an immediate run of the sequencer, bypassing the polling interval. */
202
218
  public trigger() {
203
219
  return this.sequencer.trigger();
package/src/config.ts CHANGED
@@ -62,6 +62,7 @@ export const DefaultSequencerConfig = {
62
62
  injectFakeAttestation: false,
63
63
  injectHighSValueAttestation: false,
64
64
  injectUnrecoverableSignatureAttestation: false,
65
+ injectYParityAttestation: false,
65
66
  fishermanMode: false,
66
67
  shuffleAttestationOrdering: false,
67
68
  skipPushProposedBlocksToArchiver: false,
@@ -129,7 +130,7 @@ export const sequencerConfigMappings: ConfigMappingsType<SequencerConfig> = {
129
130
  description:
130
131
  'Per-block budget multiplier applied to DA gas and blob fields in place of perBlockAllocationMultiplier.' +
131
132
  ' Defaults higher than the general multiplier so the largest contract class deploy fits a single block.',
132
- ...numberConfigHelper(DefaultSequencerConfig.perBlockDAAllocationMultiplier),
133
+ ...floatConfigHelper(DefaultSequencerConfig.perBlockDAAllocationMultiplier),
133
134
  },
134
135
  redistributeCheckpointBudget: {
135
136
  env: 'SEQ_REDISTRIBUTE_CHECKPOINT_BUDGET',
@@ -144,7 +145,7 @@ export const sequencerConfigMappings: ConfigMappingsType<SequencerConfig> = {
144
145
  },
145
146
  feeRecipient: {
146
147
  env: 'FEE_RECIPIENT',
147
- parseEnv: (val: string) => AztecAddress.fromString(val),
148
+ parseEnv: (val: string) => AztecAddress.fromStringUnsafe(val),
148
149
  description: 'Address to receive fees.',
149
150
  },
150
151
  acvmWorkingDirectory: {
@@ -225,6 +226,10 @@ export const sequencerConfigMappings: ConfigMappingsType<SequencerConfig> = {
225
226
  description: 'Inject an attestation with an unrecoverable signature (for testing only)',
226
227
  ...booleanConfigHelper(DefaultSequencerConfig.injectUnrecoverableSignatureAttestation),
227
228
  },
229
+ injectYParityAttestation: {
230
+ description: 'Inject a non-proposer attestation slot in yParity form in the packed L1 tuple (for testing only)',
231
+ ...booleanConfigHelper(DefaultSequencerConfig.injectYParityAttestation),
232
+ },
228
233
  fishermanMode: {
229
234
  env: 'FISHERMAN_MODE',
230
235
  description:
@@ -242,7 +247,7 @@ export const sequencerConfigMappings: ConfigMappingsType<SequencerConfig> = {
242
247
  ...booleanConfigHelper(DefaultSequencerConfig.buildCheckpointIfEmpty),
243
248
  },
244
249
  skipPushProposedBlocksToArchiver: {
245
- description: 'Skip pushing proposed blocks to archiver (default: true)',
250
+ description: 'Skip pushing proposed blocks to archiver (test only)',
246
251
  ...booleanConfigHelper(DefaultSequencerConfig.skipPushProposedBlocksToArchiver),
247
252
  },
248
253
  minBlocksForCheckpoint: {
@@ -61,7 +61,17 @@ export class FeePredictor {
61
61
  const blockNumber = await this.publicClient.getBlockNumber({ cacheTime: 0 });
62
62
  if (this.cachedL1BlockNumber === undefined || blockNumber > this.cachedL1BlockNumber) {
63
63
  this.cachedL1BlockNumber = blockNumber;
64
- this.cachedState = this.fetchState(blockNumber);
64
+ // Reset the cached block number on failure so a transient L1 RPC error does not leave a
65
+ // rejected promise cached for this block, which would replay the same rejection on every
66
+ // subsequent call until the next L1 block arrives. Only clear it if it still points at the
67
+ // block this attempt was for, so a stale rejection from an older block cannot wipe a marker
68
+ // a newer call already advanced (which would also defeat the monotonic block-number guard).
69
+ this.cachedState = this.fetchState(blockNumber).catch(err => {
70
+ if (this.cachedL1BlockNumber === blockNumber) {
71
+ this.cachedL1BlockNumber = undefined;
72
+ }
73
+ throw err;
74
+ });
65
75
  }
66
76
  return this.cachedState!;
67
77
  }
@@ -61,10 +61,27 @@ export class FeeProviderImpl implements FeeProvider {
61
61
  // Get the current block number
62
62
  const blockNumber = await this.publicClient.getBlockNumber({ cacheTime: 0 });
63
63
 
64
- // If the L1 block number has changed then chain a new promise to get the current min fees
64
+ // If the L1 block number has changed then chain a new promise to get the current min fees.
65
+ // We chain off the previous promise's settlement (via a swallowing catch) rather than its
66
+ // fulfillment, so a prior rejection does not short-circuit the new computation. If the new
67
+ // computation fails (e.g. a transient L1 RPC error), reset the cached block number so the
68
+ // next call recomputes instead of permanently replaying the rejected promise — otherwise a
69
+ // single transient failure would wedge fee estimation until the next L1 block arrives. Only
70
+ // clear it if it still points at the block this attempt was for, so a stale rejection from an
71
+ // older block cannot wipe a marker a newer call already advanced (which would also defeat the
72
+ // monotonic block-number guard).
65
73
  if (this.currentL1BlockNumber === undefined || blockNumber > this.currentL1BlockNumber) {
66
74
  this.currentL1BlockNumber = blockNumber;
67
- this.currentMinFees = this.currentMinFees.then(() => this.computeCurrentMinFees());
75
+ this.currentMinFees = this.currentMinFees
76
+ .catch(() => undefined)
77
+ .then(() =>
78
+ this.computeCurrentMinFees().catch(err => {
79
+ if (this.currentL1BlockNumber === blockNumber) {
80
+ this.currentL1BlockNumber = undefined;
81
+ }
82
+ throw err;
83
+ }),
84
+ );
68
85
  }
69
86
  return this.currentMinFees;
70
87
  }