@aztec/sequencer-client 0.0.1-commit.3fd054f6 → 0.0.1-commit.42ee6df9b

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.
@@ -454,6 +454,7 @@ _dec = trackSpan('Sequencer.work'), _dec1 = trackSpan('Sequencer.prepareCheckpoi
454
454
  /** The last slot for which we triggered a checkpoint proposal job, to prevent duplicate attempts. */ lastSlotForCheckpointProposalJob;
455
455
  /** Last successful checkpoint proposed */ lastCheckpointProposed;
456
456
  /** The last epoch for which we logged strategy comparison in fisherman mode. */ lastEpochForStrategyComparison;
457
+ /** The last checkpoint proposal job, tracked so we can await its pending L1 submission during shutdown. */ lastCheckpointProposalJob;
457
458
  /** The maximum number of seconds that the sequencer can be into a slot to transition to a particular state. */ timetable;
458
459
  /** Config for the sequencer */ config;
459
460
  constructor(publisherFactory, validatorClient, globalsBuilder, p2pClient, worldState, slasherClient, l2BlockSource, l1ToL2MessageSource, checkpointsBuilder, l1Constants, dateProvider, epochCache, rollupContract, config, telemetry = getTelemetryClient(), log = createLogger('sequencer')){
@@ -496,6 +497,7 @@ _dec = trackSpan('Sequencer.work'), _dec1 = trackSpan('Sequencer.prepareCheckpoi
496
497
  });
497
498
  await this.publisherFactory.stopAll();
498
499
  await this.runningPromise?.stop();
500
+ await this.lastCheckpointProposalJob?.awaitPendingSubmission();
499
501
  this.setState(SequencerState.STOPPED, undefined, {
500
502
  force: true
501
503
  });
@@ -546,6 +548,8 @@ _dec = trackSpan('Sequencer.work'), _dec1 = trackSpan('Sequencer.prepareCheckpoi
546
548
  if (!checkpointProposalJob) {
547
549
  return;
548
550
  }
551
+ // Track the job so we can await its pending L1 submission during shutdown
552
+ this.lastCheckpointProposalJob = checkpointProposalJob;
549
553
  // Execute the checkpoint proposal job
550
554
  const checkpoint = await checkpointProposalJob.execute();
551
555
  // Update last checkpoint proposed (currently unused)
@@ -612,6 +616,13 @@ _dec = trackSpan('Sequencer.work'), _dec1 = trackSpan('Sequencer.prepareCheckpoi
612
616
  }
613
617
  // Next checkpoint follows from the last synced one
614
618
  const checkpointNumber = CheckpointNumber(syncedTo.checkpointNumber + 1);
619
+ // Guard: don't exceed 1-deep pipeline. Without a proposed checkpoint, we can only build
620
+ // confirmed + 1. With a proposed checkpoint, we can build confirmed + 2.
621
+ const confirmedCkpt = syncedTo.checkpointedCheckpointNumber;
622
+ if (checkpointNumber > confirmedCkpt + 2) {
623
+ this.log.warn(`Skipping slot ${targetSlot}: checkpoint ${checkpointNumber} exceeds max pipeline depth (confirmed=${confirmedCkpt})`);
624
+ return undefined;
625
+ }
615
626
  const logCtx = {
616
627
  nowSeconds,
617
628
  syncedToL2Slot: syncedTo.syncedL2Slot,
@@ -651,12 +662,35 @@ _dec = trackSpan('Sequencer.work'), _dec1 = trackSpan('Sequencer.prepareCheckpoi
651
662
  this.log.debug(`Set proposer address ${proposer} for simulation in fisherman mode`);
652
663
  }
653
664
  // Prepare invalidation request if the pending chain is invalid (returns undefined if no need)
654
- const invalidateCheckpoint = await publisher.simulateInvalidateCheckpoint(syncedTo.pendingChainValidationStatus);
655
- // Check with the rollup contract if we can indeed propose at the target slot. This check should not fail
656
- // if all the previous checks are good, but we do it just in case.
657
- const canProposeCheck = await publisher.canProposeAt(syncedTo.archive, proposer ?? EthAddress.ZERO, {
658
- ...invalidateCheckpoint
659
- });
665
+ let invalidateCheckpoint = await publisher.simulateInvalidateCheckpoint(syncedTo.pendingChainValidationStatus);
666
+ // Determine the correct archive and L1 state overrides for the canProposeAt check.
667
+ // The L1 contract reads archives[proposedCheckpointNumber] and compares it with the provided archive.
668
+ // When invalidating or pipelining, the local archive may differ from L1's, so we adjust accordingly.
669
+ let archiveForCheck = syncedTo.archive;
670
+ const l1Overrides = {};
671
+ if (this.epochCache.isProposerPipeliningEnabled() && syncedTo.hasProposedCheckpoint) {
672
+ // Parent checkpoint hasn't landed on L1 yet. Override both the proposed checkpoint number
673
+ // and the archive at that checkpoint so L1 simulation sees the correct chain tip.
674
+ const parentCheckpointNumber = CheckpointNumber(checkpointNumber - 1);
675
+ l1Overrides.forcePendingCheckpointNumber = parentCheckpointNumber;
676
+ l1Overrides.forceArchive = {
677
+ checkpointNumber: parentCheckpointNumber,
678
+ archive: syncedTo.archive
679
+ };
680
+ this.metrics.recordPipelineDepth(1);
681
+ this.log.verbose(`Building on top of proposed checkpoint (pending=${syncedTo.proposedCheckpointData?.checkpointNumber})`);
682
+ // Clear the invalidation - the proposed checkpoint should handle it.
683
+ invalidateCheckpoint = undefined;
684
+ } else if (invalidateCheckpoint) {
685
+ // After invalidation, L1 will roll back to checkpoint N-1. The archive at N-1 already
686
+ // exists on L1, so we just pass the matching archive (the lastArchive of the invalid checkpoint).
687
+ archiveForCheck = invalidateCheckpoint.lastArchive;
688
+ l1Overrides.forcePendingCheckpointNumber = invalidateCheckpoint.forcePendingCheckpointNumber;
689
+ this.metrics.recordPipelineDepth(0);
690
+ } else {
691
+ this.metrics.recordPipelineDepth(0);
692
+ }
693
+ const canProposeCheck = await publisher.canProposeAt(archiveForCheck, proposer ?? EthAddress.ZERO, l1Overrides);
660
694
  if (canProposeCheck === undefined) {
661
695
  this.log.warn(`Cannot propose checkpoint ${checkpointNumber} at slot ${slot} due to failed rollup contract check`, logCtx);
662
696
  this.emit('proposer-rollup-check-failed', {
@@ -700,10 +734,10 @@ _dec = trackSpan('Sequencer.work'), _dec1 = trackSpan('Sequencer.prepareCheckpoi
700
734
  pipeliningEnabled: this.epochCache.isProposerPipeliningEnabled()
701
735
  });
702
736
  // Create and return the checkpoint proposal job
703
- return this.createCheckpointProposalJob(slot, targetSlot, epoch, targetEpoch, checkpointNumber, syncedTo.blockNumber, proposer, publisher, attestorAddress, invalidateCheckpoint);
737
+ return this.createCheckpointProposalJob(slot, targetSlot, targetEpoch, checkpointNumber, syncedTo.blockNumber, proposer, publisher, attestorAddress, invalidateCheckpoint, syncedTo.proposedCheckpointData);
704
738
  }
705
- createCheckpointProposalJob(slot, targetSlot, epoch, targetEpoch, checkpointNumber, syncedToBlockNumber, proposer, publisher, attestorAddress, invalidateCheckpoint) {
706
- return new CheckpointProposalJob(slot, targetSlot, epoch, targetEpoch, checkpointNumber, syncedToBlockNumber, proposer, publisher, attestorAddress, invalidateCheckpoint, this.validatorClient, this.globalsBuilder, this.p2pClient, this.worldState, this.l1ToL2MessageSource, this.l2BlockSource, this.checkpointsBuilder, this.l2BlockSource, this.l1Constants, this.config, this.timetable, this.slasherClient, this.epochCache, this.dateProvider, this.metrics, this, this.setState.bind(this), this.tracer, this.log.getBindings());
739
+ createCheckpointProposalJob(slot, targetSlot, targetEpoch, checkpointNumber, syncedToBlockNumber, proposer, publisher, attestorAddress, invalidateCheckpoint, proposedCheckpointData) {
740
+ return new CheckpointProposalJob(slot, targetSlot, targetEpoch, checkpointNumber, syncedToBlockNumber, proposer, publisher, attestorAddress, invalidateCheckpoint, this.validatorClient, this.globalsBuilder, this.p2pClient, this.worldState, this.l1ToL2MessageSource, this.l2BlockSource, this.checkpointsBuilder, this.l2BlockSource, this.l1Constants, this.config, this.timetable, this.slasherClient, this.epochCache, this.dateProvider, this.metrics, this, this.setState.bind(this), this.tracer, this.log.getBindings(), proposedCheckpointData);
707
741
  }
708
742
  /**
709
743
  * Returns the current sequencer state.
@@ -767,20 +801,25 @@ _dec = trackSpan('Sequencer.work'), _dec1 = trackSpan('Sequencer.prepareCheckpoi
767
801
  number: syncSummary.latestBlockNumber,
768
802
  hash: syncSummary.latestBlockHash
769
803
  })),
770
- this.l2BlockSource.getL2Tips().then((t)=>t.proposed),
804
+ this.l2BlockSource.getL2Tips().then((t)=>({
805
+ proposed: t.proposed,
806
+ checkpointed: t.checkpointed,
807
+ proposedCheckpoint: t.proposedCheckpoint
808
+ })),
771
809
  this.p2pClient.getStatus().then((p2p)=>p2p.syncedToL2Block),
772
810
  this.l1ToL2MessageSource.getL2Tips().then((t)=>t.proposed),
773
- this.l2BlockSource.getPendingChainValidationStatus()
811
+ this.l2BlockSource.getPendingChainValidationStatus(),
812
+ this.l2BlockSource.getProposedCheckpointOnly()
774
813
  ]);
775
- const [worldState, l2BlockSource, p2p, l1ToL2MessageSource, pendingChainValidationStatus] = syncedBlocks;
814
+ const [worldState, l2Tips, p2p, l1ToL2MessageSource, pendingChainValidationStatus, proposedCheckpointData] = syncedBlocks;
776
815
  // Handle zero as a special case, since the block hash won't match across services if we're changing the prefilled data for the genesis block,
777
816
  // as the world state can compute the new genesis block hash, but other components use the hardcoded constant.
778
817
  // TODO(palla/mbps): Fix the above. All components should be able to handle dynamic genesis block hashes.
779
- const result = l2BlockSource.number === 0 && worldState.number === 0 && p2p.number === 0 && l1ToL2MessageSource.number === 0 || worldState.hash === l2BlockSource.hash && p2p.hash === l2BlockSource.hash && l1ToL2MessageSource.hash === l2BlockSource.hash;
818
+ const result = l2Tips.proposed.number === 0 && worldState.number === 0 && p2p.number === 0 && l1ToL2MessageSource.number === 0 || worldState.hash === l2Tips.proposed.hash && p2p.hash === l2Tips.proposed.hash && l1ToL2MessageSource.hash === l2Tips.proposed.hash;
780
819
  if (!result) {
781
820
  this.log.debug(`Sequencer sync check failed`, {
782
821
  worldState,
783
- l2BlockSource,
822
+ l2BlockSource: l2Tips.proposed,
784
823
  p2p,
785
824
  l1ToL2MessageSource
786
825
  });
@@ -792,8 +831,10 @@ _dec = trackSpan('Sequencer.work'), _dec1 = trackSpan('Sequencer.prepareCheckpoi
792
831
  const archive = new Fr((await this.worldState.getCommitted().getTreeInfo(MerkleTreeId.ARCHIVE)).root);
793
832
  return {
794
833
  checkpointNumber: CheckpointNumber.ZERO,
834
+ checkpointedCheckpointNumber: CheckpointNumber.ZERO,
795
835
  blockNumber: BlockNumber.ZERO,
796
836
  archive,
837
+ hasProposedCheckpoint: false,
797
838
  syncedL2Slot,
798
839
  pendingChainValidationStatus
799
840
  };
@@ -804,11 +845,15 @@ _dec = trackSpan('Sequencer.work'), _dec1 = trackSpan('Sequencer.prepareCheckpoi
804
845
  this.log.error(`Failed to get L2 block data ${blockNumber} from the archiver with all components in sync`);
805
846
  return undefined;
806
847
  }
848
+ const hasProposedCheckpoint = l2Tips.proposedCheckpoint.checkpoint.number > l2Tips.checkpointed.checkpoint.number;
807
849
  return {
808
850
  blockData,
809
851
  blockNumber: blockData.header.getBlockNumber(),
810
852
  checkpointNumber: blockData.checkpointNumber,
853
+ checkpointedCheckpointNumber: l2Tips.checkpointed.checkpoint.number,
811
854
  archive: blockData.archive.root,
855
+ hasProposedCheckpoint,
856
+ proposedCheckpointData,
812
857
  syncedL2Slot,
813
858
  pendingChainValidationStatus
814
859
  };
@@ -864,7 +909,7 @@ _dec = trackSpan('Sequencer.work'), _dec1 = trackSpan('Sequencer.prepareCheckpoi
864
909
  proposer
865
910
  ];
866
911
  }
867
- this.log.debug(`We are the proposer for target slot ${targetSlot}`, {
912
+ this.log.info(`We are the proposer for pipeline slot ${targetSlot}`, {
868
913
  targetSlot,
869
914
  proposer
870
915
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aztec/sequencer-client",
3
- "version": "0.0.1-commit.3fd054f6",
3
+ "version": "0.0.1-commit.42ee6df9b",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./dest/index.js",
@@ -26,37 +26,37 @@
26
26
  "test:integration:run": "NODE_NO_WARNINGS=1 node --experimental-vm-modules $(yarn bin jest) --no-cache --config jest.integration.config.json"
27
27
  },
28
28
  "dependencies": {
29
- "@aztec/aztec.js": "0.0.1-commit.3fd054f6",
30
- "@aztec/bb-prover": "0.0.1-commit.3fd054f6",
31
- "@aztec/blob-client": "0.0.1-commit.3fd054f6",
32
- "@aztec/blob-lib": "0.0.1-commit.3fd054f6",
33
- "@aztec/constants": "0.0.1-commit.3fd054f6",
34
- "@aztec/epoch-cache": "0.0.1-commit.3fd054f6",
35
- "@aztec/ethereum": "0.0.1-commit.3fd054f6",
36
- "@aztec/foundation": "0.0.1-commit.3fd054f6",
37
- "@aztec/l1-artifacts": "0.0.1-commit.3fd054f6",
38
- "@aztec/node-keystore": "0.0.1-commit.3fd054f6",
39
- "@aztec/noir-acvm_js": "0.0.1-commit.3fd054f6",
40
- "@aztec/noir-contracts.js": "0.0.1-commit.3fd054f6",
41
- "@aztec/noir-protocol-circuits-types": "0.0.1-commit.3fd054f6",
42
- "@aztec/noir-types": "0.0.1-commit.3fd054f6",
43
- "@aztec/p2p": "0.0.1-commit.3fd054f6",
44
- "@aztec/protocol-contracts": "0.0.1-commit.3fd054f6",
45
- "@aztec/prover-client": "0.0.1-commit.3fd054f6",
46
- "@aztec/simulator": "0.0.1-commit.3fd054f6",
47
- "@aztec/slasher": "0.0.1-commit.3fd054f6",
48
- "@aztec/stdlib": "0.0.1-commit.3fd054f6",
49
- "@aztec/telemetry-client": "0.0.1-commit.3fd054f6",
50
- "@aztec/validator-client": "0.0.1-commit.3fd054f6",
51
- "@aztec/validator-ha-signer": "0.0.1-commit.3fd054f6",
52
- "@aztec/world-state": "0.0.1-commit.3fd054f6",
29
+ "@aztec/aztec.js": "0.0.1-commit.42ee6df9b",
30
+ "@aztec/bb-prover": "0.0.1-commit.42ee6df9b",
31
+ "@aztec/blob-client": "0.0.1-commit.42ee6df9b",
32
+ "@aztec/blob-lib": "0.0.1-commit.42ee6df9b",
33
+ "@aztec/constants": "0.0.1-commit.42ee6df9b",
34
+ "@aztec/epoch-cache": "0.0.1-commit.42ee6df9b",
35
+ "@aztec/ethereum": "0.0.1-commit.42ee6df9b",
36
+ "@aztec/foundation": "0.0.1-commit.42ee6df9b",
37
+ "@aztec/l1-artifacts": "0.0.1-commit.42ee6df9b",
38
+ "@aztec/node-keystore": "0.0.1-commit.42ee6df9b",
39
+ "@aztec/noir-acvm_js": "0.0.1-commit.42ee6df9b",
40
+ "@aztec/noir-contracts.js": "0.0.1-commit.42ee6df9b",
41
+ "@aztec/noir-protocol-circuits-types": "0.0.1-commit.42ee6df9b",
42
+ "@aztec/noir-types": "0.0.1-commit.42ee6df9b",
43
+ "@aztec/p2p": "0.0.1-commit.42ee6df9b",
44
+ "@aztec/protocol-contracts": "0.0.1-commit.42ee6df9b",
45
+ "@aztec/prover-client": "0.0.1-commit.42ee6df9b",
46
+ "@aztec/simulator": "0.0.1-commit.42ee6df9b",
47
+ "@aztec/slasher": "0.0.1-commit.42ee6df9b",
48
+ "@aztec/stdlib": "0.0.1-commit.42ee6df9b",
49
+ "@aztec/telemetry-client": "0.0.1-commit.42ee6df9b",
50
+ "@aztec/validator-client": "0.0.1-commit.42ee6df9b",
51
+ "@aztec/validator-ha-signer": "0.0.1-commit.42ee6df9b",
52
+ "@aztec/world-state": "0.0.1-commit.42ee6df9b",
53
53
  "lodash.chunk": "^4.2.0",
54
54
  "tslib": "^2.4.0",
55
55
  "viem": "npm:@aztec/viem@2.38.2"
56
56
  },
57
57
  "devDependencies": {
58
- "@aztec/archiver": "0.0.1-commit.3fd054f6",
59
- "@aztec/kv-store": "0.0.1-commit.3fd054f6",
58
+ "@aztec/archiver": "0.0.1-commit.42ee6df9b",
59
+ "@aztec/kv-store": "0.0.1-commit.42ee6df9b",
60
60
  "@electric-sql/pglite": "^0.3.14",
61
61
  "@jest/globals": "^30.0.0",
62
62
  "@types/jest": "^30.0.0",
@@ -10,6 +10,7 @@ import type { AztecAddress } from '@aztec/stdlib/aztec-address';
10
10
  import { type L1RollupConstants, getNextL1SlotTimestamp, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
11
11
  import { GasFees } from '@aztec/stdlib/gas';
12
12
  import type {
13
+ BuildCheckpointGlobalVariablesOpts,
13
14
  CheckpointGlobalVariables,
14
15
  GlobalVariableBuilder as GlobalVariableBuilderInterface,
15
16
  } from '@aztec/stdlib/tx';
@@ -119,6 +120,7 @@ export class GlobalVariableBuilder implements GlobalVariableBuilderInterface {
119
120
  coinbase: EthAddress,
120
121
  feeRecipient: AztecAddress,
121
122
  slotNumber: SlotNumber,
123
+ opts?: BuildCheckpointGlobalVariablesOpts,
122
124
  ): Promise<CheckpointGlobalVariables> {
123
125
  const { chainId, version } = this;
124
126
 
@@ -127,9 +129,19 @@ export class GlobalVariableBuilder implements GlobalVariableBuilderInterface {
127
129
  l1GenesisTime: this.l1GenesisTime,
128
130
  });
129
131
 
130
- // We can skip much of the logic in getCurrentMinFees since it we already check that we are not within a slot elsewhere.
131
- // TODO(palla/mbps): Can we use a cached value here?
132
- const gasFees = new GasFees(0, await this.rollupContract.getManaMinFeeAt(timestamp, true));
132
+ // When pipelining, force the proposed checkpoint number and fee header to the parent so that
133
+ // the fee computation matches what L1 will see when the previous pipelined checkpoint has landed.
134
+ const pendingNumberOverride = await this.rollupContract.makePendingCheckpointNumberOverride(
135
+ opts?.forcePendingCheckpointNumber,
136
+ );
137
+ const feeHeaderOverride = opts?.forceProposedFeeHeader
138
+ ? await this.rollupContract.makeFeeHeaderOverride(
139
+ opts.forceProposedFeeHeader.checkpointNumber,
140
+ opts.forceProposedFeeHeader.feeHeader,
141
+ )
142
+ : [];
143
+ const stateOverride = RollupContract.mergeStateOverrides(pendingNumberOverride, feeHeaderOverride);
144
+ const gasFees = new GasFees(0, await this.rollupContract.getManaMinFeeAt(timestamp, true, stateOverride));
133
145
 
134
146
  return { chainId, version, slotNumber, timestamp, coinbase, feeRecipient, gasFees };
135
147
  }