@aztec/sequencer-client 0.0.1-commit.ef17749e1 → 0.0.1-commit.f1b29a41e

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 (53) hide show
  1. package/dest/client/sequencer-client.d.ts +4 -12
  2. package/dest/client/sequencer-client.d.ts.map +1 -1
  3. package/dest/client/sequencer-client.js +27 -76
  4. package/dest/config.d.ts +4 -3
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +9 -2
  7. package/dest/global_variable_builder/global_builder.d.ts +15 -9
  8. package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
  9. package/dest/global_variable_builder/global_builder.js +29 -25
  10. package/dest/global_variable_builder/index.d.ts +2 -2
  11. package/dest/global_variable_builder/index.d.ts.map +1 -1
  12. package/dest/publisher/config.d.ts +13 -1
  13. package/dest/publisher/config.d.ts.map +1 -1
  14. package/dest/publisher/config.js +17 -2
  15. package/dest/publisher/sequencer-publisher-factory.d.ts +3 -3
  16. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  17. package/dest/publisher/sequencer-publisher-factory.js +2 -2
  18. package/dest/publisher/sequencer-publisher.d.ts +52 -25
  19. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  20. package/dest/publisher/sequencer-publisher.js +98 -42
  21. package/dest/sequencer/checkpoint_proposal_job.d.ts +33 -6
  22. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  23. package/dest/sequencer/checkpoint_proposal_job.js +261 -141
  24. package/dest/sequencer/checkpoint_voter.d.ts +1 -2
  25. package/dest/sequencer/checkpoint_voter.d.ts.map +1 -1
  26. package/dest/sequencer/checkpoint_voter.js +2 -5
  27. package/dest/sequencer/events.d.ts +2 -1
  28. package/dest/sequencer/events.d.ts.map +1 -1
  29. package/dest/sequencer/metrics.d.ts +5 -1
  30. package/dest/sequencer/metrics.d.ts.map +1 -1
  31. package/dest/sequencer/metrics.js +11 -0
  32. package/dest/sequencer/sequencer.d.ts +19 -7
  33. package/dest/sequencer/sequencer.d.ts.map +1 -1
  34. package/dest/sequencer/sequencer.js +123 -68
  35. package/dest/sequencer/types.d.ts +2 -5
  36. package/dest/sequencer/types.d.ts.map +1 -1
  37. package/dest/test/mock_checkpoint_builder.d.ts +4 -4
  38. package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
  39. package/package.json +27 -28
  40. package/src/client/sequencer-client.ts +37 -101
  41. package/src/config.ts +12 -1
  42. package/src/global_variable_builder/global_builder.ts +37 -26
  43. package/src/global_variable_builder/index.ts +1 -1
  44. package/src/publisher/config.ts +32 -0
  45. package/src/publisher/sequencer-publisher-factory.ts +3 -3
  46. package/src/publisher/sequencer-publisher.ts +144 -54
  47. package/src/sequencer/checkpoint_proposal_job.ts +340 -147
  48. package/src/sequencer/checkpoint_voter.ts +1 -12
  49. package/src/sequencer/events.ts +1 -1
  50. package/src/sequencer/metrics.ts +14 -0
  51. package/src/sequencer/sequencer.ts +178 -79
  52. package/src/sequencer/types.ts +2 -5
  53. package/src/test/mock_checkpoint_builder.ts +3 -3
@@ -5,6 +5,7 @@ import type { L1ContractsConfig } from '@aztec/ethereum/config';
5
5
  import {
6
6
  type EmpireSlashingProposerContract,
7
7
  FeeAssetPriceOracle,
8
+ type FeeHeader,
8
9
  type GovernanceProposerContract,
9
10
  type IEmpireBase,
10
11
  MULTI_CALL_3_ADDRESS,
@@ -28,6 +29,7 @@ import { FormattedViemError, formatViemError, mergeAbis, tryExtractEvent } from
28
29
  import { sumBigint } from '@aztec/foundation/bigint';
29
30
  import { toHex as toPaddedHex } from '@aztec/foundation/bigint-buffer';
30
31
  import { CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
32
+ import { trimmedBytesLength } from '@aztec/foundation/buffer';
31
33
  import { pick } from '@aztec/foundation/collection';
32
34
  import type { Fr } from '@aztec/foundation/curves/bn254';
33
35
  import { TimeoutError } from '@aztec/foundation/error';
@@ -35,12 +37,14 @@ import { EthAddress } from '@aztec/foundation/eth-address';
35
37
  import { Signature, type ViemSignature } from '@aztec/foundation/eth-signature';
36
38
  import { type Logger, createLogger } from '@aztec/foundation/log';
37
39
  import { makeBackoff, retry } from '@aztec/foundation/retry';
40
+ import { InterruptibleSleep } from '@aztec/foundation/sleep';
38
41
  import { bufferToHex } from '@aztec/foundation/string';
39
- import { DateProvider, Timer } from '@aztec/foundation/timer';
42
+ import { type DateProvider, Timer } from '@aztec/foundation/timer';
40
43
  import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
41
44
  import { type ProposerSlashAction, encodeSlashConsensusVotes } from '@aztec/slasher';
42
45
  import { CommitteeAttestationsAndSigners, type ValidateCheckpointResult } from '@aztec/stdlib/block';
43
46
  import type { Checkpoint } from '@aztec/stdlib/checkpoint';
47
+ import { getLastL1SlotTimestampForL2Slot, getNextL1SlotTimestamp } from '@aztec/stdlib/epoch-helpers';
44
48
  import { SlashFactoryContract } from '@aztec/stdlib/l1-contracts';
45
49
  import type { CheckpointHeader } from '@aztec/stdlib/rollup';
46
50
  import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
@@ -61,6 +65,20 @@ import type { SequencerPublisherConfig } from './config.js';
61
65
  import { type FailedL1Tx, type L1TxFailedStore, createL1TxFailedStore } from './l1_tx_failed_store/index.js';
62
66
  import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
63
67
 
68
+ /** Result of a sendRequests call, returned by both sendRequests() and sendRequestsAt(). */
69
+ export type SendRequestsResult = {
70
+ /** The L1 transaction receipt or error from the bundled multicall. */
71
+ result: { receipt: TransactionReceipt; errorMsg?: string } | FormattedViemError;
72
+ /** Actions that expired (past their deadline) before the request was sent. */
73
+ expiredActions: Action[];
74
+ /** Actions that were included in the sent L1 transaction. */
75
+ sentActions: Action[];
76
+ /** Actions whose L1 simulation succeeded (subset of sentActions). */
77
+ successfulActions: Action[];
78
+ /** Actions whose L1 simulation failed (subset of sentActions). */
79
+ failedActions: Action[];
80
+ };
81
+
64
82
  /** Arguments to the process method of the rollup contract */
65
83
  type L1ProcessArgs = {
66
84
  /** The L2 block header. */
@@ -102,6 +120,8 @@ export type InvalidateCheckpointRequest = {
102
120
  gasUsed: bigint;
103
121
  checkpointNumber: CheckpointNumber;
104
122
  forcePendingCheckpointNumber: CheckpointNumber;
123
+ /** Archive at the rollback target checkpoint (checkpoint N-1). */
124
+ lastArchive: Fr;
105
125
  };
106
126
 
107
127
  interface RequestWithExpiry {
@@ -132,6 +152,10 @@ export class SequencerPublisher {
132
152
 
133
153
  protected log: Logger;
134
154
  protected ethereumSlotDuration: bigint;
155
+ protected aztecSlotDuration: bigint;
156
+
157
+ /** Date provider for wall-clock time. */
158
+ private readonly dateProvider: DateProvider;
135
159
 
136
160
  private blobClient: BlobClientInterface;
137
161
 
@@ -147,6 +171,9 @@ export class SequencerPublisher {
147
171
  /** Fee asset price oracle for computing price modifiers from Uniswap V4 */
148
172
  private feeAssetPriceOracle: FeeAssetPriceOracle;
149
173
 
174
+ /** Interruptible sleep used by sendRequestsAt to wait until a target timestamp. */
175
+ private readonly interruptibleSleep = new InterruptibleSleep();
176
+
150
177
  // A CALL to a cold address is 2700 gas
151
178
  public static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
152
179
 
@@ -165,7 +192,7 @@ export class SequencerPublisher {
165
192
 
166
193
  constructor(
167
194
  private config: Pick<SequencerPublisherConfig, 'fishermanMode' | 'l1TxFailedStore'> &
168
- Pick<L1ContractsConfig, 'ethereumSlotDuration'> & { l1ChainId: number },
195
+ Pick<L1ContractsConfig, 'ethereumSlotDuration' | 'aztecSlotDuration'> & { l1ChainId: number },
169
196
  deps: {
170
197
  telemetry?: TelemetryClient;
171
198
  blobClient: BlobClientInterface;
@@ -184,10 +211,13 @@ export class SequencerPublisher {
184
211
  ) {
185
212
  this.log = deps.log ?? createLogger('sequencer:publisher');
186
213
  this.ethereumSlotDuration = BigInt(config.ethereumSlotDuration);
214
+ this.aztecSlotDuration = BigInt(config.aztecSlotDuration);
215
+ this.dateProvider = deps.dateProvider;
187
216
  this.epochCache = deps.epochCache;
188
217
  this.lastActions = deps.lastActions;
189
218
 
190
219
  this.blobClient = deps.blobClient;
220
+ this.dateProvider = deps.dateProvider;
191
221
 
192
222
  const telemetry = deps.telemetry ?? getTelemetryClient();
193
223
  this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
@@ -285,7 +315,7 @@ export class SequencerPublisher {
285
315
  }
286
316
 
287
317
  public getCurrentL2Slot(): SlotNumber {
288
- return this.epochCache.getEpochAndSlotNow().slot;
318
+ return this.epochCache.getSlotNow();
289
319
  }
290
320
 
291
321
  /**
@@ -363,9 +393,10 @@ export class SequencerPublisher {
363
393
  * - undefined if no valid requests are found OR the tx failed to send.
364
394
  */
365
395
  @trackSpan('SequencerPublisher.sendRequests')
366
- public async sendRequests() {
396
+ public async sendRequests(): Promise<SendRequestsResult | undefined> {
367
397
  const requestsToProcess = [...this.requests];
368
398
  this.requests = [];
399
+
369
400
  if (this.interrupted || requestsToProcess.length === 0) {
370
401
  return undefined;
371
402
  }
@@ -398,8 +429,8 @@ export class SequencerPublisher {
398
429
  // @note - we can only have one blob config per bundle
399
430
  // find requests with gas and blob configs
400
431
  // See https://github.com/AztecProtocol/aztec-packages/issues/11513
401
- const gasConfigs = requestsToProcess.filter(request => request.gasConfig).map(request => request.gasConfig);
402
- const blobConfigs = requestsToProcess.filter(request => request.blobConfig).map(request => request.blobConfig);
432
+ const gasConfigs = validRequests.filter(request => request.gasConfig).map(request => request.gasConfig);
433
+ const blobConfigs = validRequests.filter(request => request.blobConfig).map(request => request.blobConfig);
403
434
 
404
435
  if (blobConfigs.length > 1) {
405
436
  throw new Error('Multiple blob configs found');
@@ -524,6 +555,23 @@ export class SequencerPublisher {
524
555
  }
525
556
  }
526
557
 
558
+ /*
559
+ * Schedules sending all enqueued requests at (or after) the given timestamp.
560
+ * Uses InterruptibleSleep so it can be cancelled via interrupt().
561
+ * Returns the promise for the L1 response (caller should NOT await this in the work loop).
562
+ */
563
+ public async sendRequestsAt(submitAfter: Date): Promise<SendRequestsResult | undefined> {
564
+ const ms = submitAfter.getTime() - this.dateProvider.now();
565
+ if (ms > 0) {
566
+ this.log.debug(`Sleeping ${ms}ms before sending requests`, { submitAfter });
567
+ await this.interruptibleSleep.sleep(ms);
568
+ }
569
+ if (this.interrupted) {
570
+ return undefined;
571
+ }
572
+ return this.sendRequests();
573
+ }
574
+
527
575
  private callbackBundledTransactions(
528
576
  requests: RequestWithExpiry[],
529
577
  result: { receipt: TransactionReceipt; errorMsg?: string } | FormattedViemError | undefined,
@@ -547,7 +595,16 @@ export class SequencerPublisher {
547
595
  });
548
596
  return { failedActions: requests.map(r => r.action) };
549
597
  } else {
550
- this.log.verbose(`Published bundled transactions (${actionsListStr})`, { result, requests });
598
+ this.log.verbose(`Published bundled transactions (${actionsListStr})`, {
599
+ result,
600
+ requests: requests.map(r => ({
601
+ ...r,
602
+ // Avoid logging large blob data
603
+ blobConfig: r.blobConfig
604
+ ? { ...r.blobConfig, blobs: r.blobConfig.blobs.map(b => ({ size: trimmedBytesLength(b) })) }
605
+ : undefined,
606
+ })),
607
+ });
551
608
  const successfulActions: Action[] = [];
552
609
  const failedActions: Action[] = [];
553
610
  for (const request of requests) {
@@ -586,21 +643,30 @@ export class SequencerPublisher {
586
643
  }
587
644
 
588
645
  /**
589
- * @notice Will call `canProposeAtNextEthBlock` to make sure that it is possible to propose
646
+ * @notice Will call `canProposeAt` to make sure that it is possible to propose
590
647
  * @param tipArchive - The archive to check
591
648
  * @returns The slot and block number if it is possible to propose, undefined otherwise
592
649
  */
593
- public canProposeAtNextEthBlock(
650
+ public canProposeAt(
594
651
  tipArchive: Fr,
595
652
  msgSender: EthAddress,
596
- opts: { forcePendingCheckpointNumber?: CheckpointNumber } = {},
653
+ opts: {
654
+ forcePendingCheckpointNumber?: CheckpointNumber;
655
+ forceArchive?: { checkpointNumber: CheckpointNumber; archive: Fr };
656
+ pipelined?: boolean;
657
+ } = {},
597
658
  ) {
598
659
  // TODO: #14291 - should loop through multiple keys to check if any of them can propose
599
660
  const ignoredErrors = ['SlotAlreadyInChain', 'InvalidProposer', 'InvalidArchive'];
600
661
 
662
+ const pipelined = opts.pipelined ?? this.epochCache.isProposerPipeliningEnabled();
663
+ const slotOffset = pipelined ? this.aztecSlotDuration : 0n;
664
+ const nextL1SlotTs = this.getNextL1SlotTimestamp() + slotOffset;
665
+
601
666
  return this.rollupContract
602
- .canProposeAtNextEthBlock(tipArchive.toBuffer(), msgSender.toString(), Number(this.ethereumSlotDuration), {
667
+ .canProposeAt(tipArchive.toBuffer(), msgSender.toString(), nextL1SlotTs, {
603
668
  forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
669
+ forceArchive: opts.forceArchive,
604
670
  })
605
671
  .catch(err => {
606
672
  if (err instanceof FormattedViemError && ignoredErrors.find(e => err.message.includes(e))) {
@@ -613,6 +679,7 @@ export class SequencerPublisher {
613
679
  return undefined;
614
680
  });
615
681
  }
682
+
616
683
  /**
617
684
  * @notice Will simulate `validateHeader` to make sure that the block header is valid
618
685
  * @dev This is a convenience function that can be used by the sequencer to validate a "partial" header.
@@ -636,7 +703,7 @@ export class SequencerPublisher {
636
703
  flags,
637
704
  ] as const;
638
705
 
639
- const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
706
+ const ts = this.getSimulationTimestamp(header.slotNumber);
640
707
  const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(
641
708
  opts?.forcePendingCheckpointNumber,
642
709
  );
@@ -659,7 +726,7 @@ export class SequencerPublisher {
659
726
  data: encodeFunctionData({ abi: RollupAbi, functionName: 'validateHeaderWithAttestations', args }),
660
727
  from: MULTI_CALL_3_ADDRESS,
661
728
  },
662
- { time: ts + 1n },
729
+ { time: ts },
663
730
  stateOverrides,
664
731
  );
665
732
  this.log.debug(`Simulated validateHeader`);
@@ -712,6 +779,7 @@ export class SequencerPublisher {
712
779
  gasUsed,
713
780
  checkpointNumber,
714
781
  forcePendingCheckpointNumber: CheckpointNumber(checkpointNumber - 1),
782
+ lastArchive: validationResult.checkpoint.lastArchive,
715
783
  reason,
716
784
  };
717
785
  } catch (err) {
@@ -724,8 +792,8 @@ export class SequencerPublisher {
724
792
  `Simulation for invalidate checkpoint ${checkpointNumber} failed due to checkpoint not being in pending chain`,
725
793
  { ...logData, request, error: viemError.message },
726
794
  );
727
- const latestPendingCheckpointNumber = await this.rollupContract.getCheckpointNumber();
728
- if (latestPendingCheckpointNumber < checkpointNumber) {
795
+ const latestProposedCheckpointNumber = await this.rollupContract.getCheckpointNumber();
796
+ if (latestProposedCheckpointNumber < checkpointNumber) {
729
797
  this.log.verbose(`Checkpoint ${checkpointNumber} has already been invalidated`, { ...logData });
730
798
  return undefined;
731
799
  } else {
@@ -799,9 +867,11 @@ export class SequencerPublisher {
799
867
  checkpoint: Checkpoint,
800
868
  attestationsAndSigners: CommitteeAttestationsAndSigners,
801
869
  attestationsAndSignersSignature: Signature,
802
- options: { forcePendingCheckpointNumber?: CheckpointNumber },
803
- ): Promise<bigint> {
804
- const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
870
+ options: {
871
+ forcePendingCheckpointNumber?: CheckpointNumber;
872
+ forceProposedFeeHeader?: { checkpointNumber: CheckpointNumber; feeHeader: FeeHeader };
873
+ },
874
+ ): Promise<void> {
805
875
  const blobFields = checkpoint.toBlobFields();
806
876
  const blobs = await getBlobsPerL1Block(blobFields);
807
877
  const blobInput = getPrefixedEthBlobCommitments(blobs);
@@ -820,13 +890,11 @@ export class SequencerPublisher {
820
890
  blobInput,
821
891
  ] as const;
822
892
 
823
- await this.simulateProposeTx(args, ts, options);
824
- return ts;
893
+ await this.simulateProposeTx(args, options);
825
894
  }
826
895
 
827
896
  private async enqueueCastSignalHelper(
828
897
  slotNumber: SlotNumber,
829
- timestamp: bigint,
830
898
  signalType: GovernanceSignalAction,
831
899
  payload: EthAddress,
832
900
  base: IEmpireBase,
@@ -905,13 +973,17 @@ export class SequencerPublisher {
905
973
  });
906
974
 
907
975
  const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
976
+ const timestamp = this.getSimulationTimestamp(slotNumber);
908
977
 
909
978
  try {
910
979
  await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
911
980
  this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
912
981
  } catch (err) {
913
982
  const viemError = formatViemError(err);
914
- this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, viemError);
983
+ this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, viemError, {
984
+ simulationTimestamp: timestamp,
985
+ l1BlockNumber,
986
+ });
915
987
  this.backupFailedTx({
916
988
  id: keccak256(request.data!),
917
989
  failureType: 'simulation',
@@ -974,19 +1046,16 @@ export class SequencerPublisher {
974
1046
  /**
975
1047
  * Enqueues a governance castSignal transaction to cast a signal for a given slot number.
976
1048
  * @param slotNumber - The slot number to cast a signal for.
977
- * @param timestamp - The timestamp of the slot to cast a signal for.
978
1049
  * @returns True if the signal was successfully enqueued, false otherwise.
979
1050
  */
980
1051
  public enqueueGovernanceCastSignal(
981
1052
  governancePayload: EthAddress,
982
1053
  slotNumber: SlotNumber,
983
- timestamp: bigint,
984
1054
  signerAddress: EthAddress,
985
1055
  signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
986
1056
  ): Promise<boolean> {
987
1057
  return this.enqueueCastSignalHelper(
988
1058
  slotNumber,
989
- timestamp,
990
1059
  'governance-signal',
991
1060
  governancePayload,
992
1061
  this.govProposerContract,
@@ -999,7 +1068,6 @@ export class SequencerPublisher {
999
1068
  public async enqueueSlashingActions(
1000
1069
  actions: ProposerSlashAction[],
1001
1070
  slotNumber: SlotNumber,
1002
- timestamp: bigint,
1003
1071
  signerAddress: EthAddress,
1004
1072
  signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
1005
1073
  ): Promise<boolean> {
@@ -1020,7 +1088,6 @@ export class SequencerPublisher {
1020
1088
  });
1021
1089
  await this.enqueueCastSignalHelper(
1022
1090
  slotNumber,
1023
- timestamp,
1024
1091
  'empire-slashing-signal',
1025
1092
  action.payload,
1026
1093
  this.slashingProposerContract,
@@ -1039,7 +1106,6 @@ export class SequencerPublisher {
1039
1106
  (receipt: TransactionReceipt) =>
1040
1107
  !!this.slashFactoryContract.tryExtractSlashPayloadCreatedEvent(receipt.logs),
1041
1108
  slotNumber,
1042
- timestamp,
1043
1109
  );
1044
1110
  break;
1045
1111
  }
@@ -1057,7 +1123,6 @@ export class SequencerPublisher {
1057
1123
  request,
1058
1124
  (receipt: TransactionReceipt) => !!empireSlashingProposer.tryExtractPayloadSubmittedEvent(receipt.logs),
1059
1125
  slotNumber,
1060
- timestamp,
1061
1126
  );
1062
1127
  break;
1063
1128
  }
@@ -1081,7 +1146,6 @@ export class SequencerPublisher {
1081
1146
  request,
1082
1147
  (receipt: TransactionReceipt) => !!tallySlashingProposer.tryExtractVoteCastEvent(receipt.logs),
1083
1148
  slotNumber,
1084
- timestamp,
1085
1149
  );
1086
1150
  break;
1087
1151
  }
@@ -1103,7 +1167,6 @@ export class SequencerPublisher {
1103
1167
  request,
1104
1168
  (receipt: TransactionReceipt) => !!tallySlashingProposer.tryExtractRoundExecutedEvent(receipt.logs),
1105
1169
  slotNumber,
1106
- timestamp,
1107
1170
  );
1108
1171
  break;
1109
1172
  }
@@ -1123,7 +1186,11 @@ export class SequencerPublisher {
1123
1186
  checkpoint: Checkpoint,
1124
1187
  attestationsAndSigners: CommitteeAttestationsAndSigners,
1125
1188
  attestationsAndSignersSignature: Signature,
1126
- opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
1189
+ opts: {
1190
+ txTimeoutAt?: Date;
1191
+ forcePendingCheckpointNumber?: CheckpointNumber;
1192
+ forceProposedFeeHeader?: { checkpointNumber: CheckpointNumber; feeHeader: FeeHeader };
1193
+ } = {},
1127
1194
  ): Promise<void> {
1128
1195
  const checkpointHeader = checkpoint.header;
1129
1196
 
@@ -1139,15 +1206,13 @@ export class SequencerPublisher {
1139
1206
  feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
1140
1207
  };
1141
1208
 
1142
- let ts: bigint;
1143
-
1144
1209
  try {
1145
1210
  // @note This will make sure that we are passing the checks for our header ASSUMING that the data is also made available
1146
1211
  // This means that we can avoid the simulation issues in later checks.
1147
1212
  // By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which
1148
1213
  // make time consistency checks break.
1149
1214
  // TODO(palla): Check whether we're validating twice, once here and once within addProposeTx, since we call simulateProposeTx in both places.
1150
- ts = await this.validateCheckpointForSubmission(
1215
+ await this.validateCheckpointForSubmission(
1151
1216
  checkpoint,
1152
1217
  attestationsAndSigners,
1153
1218
  attestationsAndSignersSignature,
@@ -1163,7 +1228,7 @@ export class SequencerPublisher {
1163
1228
  }
1164
1229
 
1165
1230
  this.log.verbose(`Enqueuing checkpoint propose transaction`, { ...checkpoint.toCheckpointInfo(), ...opts });
1166
- await this.addProposeTx(checkpoint, proposeTxArgs, opts, ts);
1231
+ await this.addProposeTx(checkpoint, proposeTxArgs, opts);
1167
1232
  }
1168
1233
 
1169
1234
  public enqueueInvalidateCheckpoint(
@@ -1206,8 +1271,8 @@ export class SequencerPublisher {
1206
1271
  request: L1TxRequest,
1207
1272
  checkSuccess: (receipt: TransactionReceipt) => boolean | undefined,
1208
1273
  slotNumber: SlotNumber,
1209
- timestamp: bigint,
1210
1274
  ) {
1275
+ const timestamp = this.getSimulationTimestamp(slotNumber);
1211
1276
  const logData = { slotNumber, timestamp, gasLimit: undefined as bigint | undefined };
1212
1277
  if (this.lastActions[action] && this.lastActions[action] === slotNumber) {
1213
1278
  this.log.debug(`Skipping duplicate action ${action} for slot ${slotNumber}`);
@@ -1223,8 +1288,9 @@ export class SequencerPublisher {
1223
1288
 
1224
1289
  let gasUsed: bigint;
1225
1290
  const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
1291
+
1226
1292
  try {
1227
- ({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], simulateAbi)); // TODO(palla/slash): Check the timestamp logic
1293
+ ({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], simulateAbi));
1228
1294
  this.log.verbose(`Simulation for ${action} succeeded`, { ...logData, request, gasUsed });
1229
1295
  } catch (err) {
1230
1296
  const viemError = formatViemError(err, simulateAbi);
@@ -1282,6 +1348,7 @@ export class SequencerPublisher {
1282
1348
  */
1283
1349
  public interrupt() {
1284
1350
  this.interrupted = true;
1351
+ this.interruptibleSleep.interrupt();
1285
1352
  this.l1TxUtils.interrupt();
1286
1353
  }
1287
1354
 
@@ -1293,7 +1360,6 @@ export class SequencerPublisher {
1293
1360
 
1294
1361
  private async prepareProposeTx(
1295
1362
  encodedData: L1ProcessArgs,
1296
- timestamp: bigint,
1297
1363
  options: { forcePendingCheckpointNumber?: CheckpointNumber },
1298
1364
  ) {
1299
1365
  const kzg = Blob.getViemKzgInstance();
@@ -1366,7 +1432,7 @@ export class SequencerPublisher {
1366
1432
  blobInput,
1367
1433
  ] as const;
1368
1434
 
1369
- const { rollupData, simulationResult } = await this.simulateProposeTx(args, timestamp, options);
1435
+ const { rollupData, simulationResult } = await this.simulateProposeTx(args, options);
1370
1436
 
1371
1437
  return { args, blobEvaluationGas, rollupData, simulationResult };
1372
1438
  }
@@ -1374,7 +1440,6 @@ export class SequencerPublisher {
1374
1440
  /**
1375
1441
  * Simulates the propose tx with eth_simulateV1
1376
1442
  * @param args - The propose tx args
1377
- * @param timestamp - The timestamp to simulate proposal at
1378
1443
  * @returns The simulation result
1379
1444
  */
1380
1445
  private async simulateProposeTx(
@@ -1391,8 +1456,10 @@ export class SequencerPublisher {
1391
1456
  ViemSignature,
1392
1457
  `0x${string}`,
1393
1458
  ],
1394
- timestamp: bigint,
1395
- options: { forcePendingCheckpointNumber?: CheckpointNumber },
1459
+ options: {
1460
+ forcePendingCheckpointNumber?: CheckpointNumber;
1461
+ forceProposedFeeHeader?: { checkpointNumber: CheckpointNumber; feeHeader: FeeHeader };
1462
+ },
1396
1463
  ) {
1397
1464
  const rollupData = encodeFunctionData({
1398
1465
  abi: RollupAbi,
@@ -1400,13 +1467,23 @@ export class SequencerPublisher {
1400
1467
  args,
1401
1468
  });
1402
1469
 
1403
- // override the pending checkpoint number if requested
1470
+ // override the proposed checkpoint number if requested
1404
1471
  const forcePendingCheckpointNumberStateDiff = (
1405
1472
  options.forcePendingCheckpointNumber !== undefined
1406
1473
  ? await this.rollupContract.makePendingCheckpointNumberOverride(options.forcePendingCheckpointNumber)
1407
1474
  : []
1408
1475
  ).flatMap(override => override.stateDiff ?? []);
1409
1476
 
1477
+ // override the fee header for a specific checkpoint number if requested (used when pipelining)
1478
+ const forceProposedFeeHeaderStateDiff = (
1479
+ options.forceProposedFeeHeader !== undefined
1480
+ ? await this.rollupContract.makeFeeHeaderOverride(
1481
+ options.forceProposedFeeHeader.checkpointNumber,
1482
+ options.forceProposedFeeHeader.feeHeader,
1483
+ )
1484
+ : []
1485
+ ).flatMap(override => override.stateDiff ?? []);
1486
+
1410
1487
  const stateOverrides: StateOverride = [
1411
1488
  {
1412
1489
  address: this.rollupContract.address,
@@ -1414,6 +1491,7 @@ export class SequencerPublisher {
1414
1491
  stateDiff: [
1415
1492
  { slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true), value: toPaddedHex(0n, true) },
1416
1493
  ...forcePendingCheckpointNumberStateDiff,
1494
+ ...forceProposedFeeHeaderStateDiff,
1417
1495
  ],
1418
1496
  },
1419
1497
  ];
@@ -1426,6 +1504,7 @@ export class SequencerPublisher {
1426
1504
  }
1427
1505
 
1428
1506
  const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1507
+ const simTs = this.getSimulationTimestamp(SlotNumber.fromBigInt(args[0].header.slotNumber));
1429
1508
 
1430
1509
  const simulationResult = await this.l1TxUtils
1431
1510
  .simulate(
@@ -1436,8 +1515,7 @@ export class SequencerPublisher {
1436
1515
  ...(this.proposerAddressForSimulation && { from: this.proposerAddressForSimulation.toString() }),
1437
1516
  },
1438
1517
  {
1439
- // @note we add 1n to the timestamp because geth implementation doesn't like simulation timestamp to be equal to the current block timestamp
1440
- time: timestamp + 1n,
1518
+ time: simTs,
1441
1519
  // @note reth should have a 30m gas limit per block but throws errors that this tx is beyond limit so we increase here
1442
1520
  gasLimit: MAX_L1_TX_LIMIT * 2n,
1443
1521
  },
@@ -1459,7 +1537,7 @@ export class SequencerPublisher {
1459
1537
  logs: [],
1460
1538
  };
1461
1539
  }
1462
- this.log.error(`Failed to simulate propose tx`, viemError);
1540
+ this.log.error(`Failed to simulate propose tx`, viemError, { simulationTimestamp: simTs });
1463
1541
  this.backupFailedTx({
1464
1542
  id: keccak256(rollupData),
1465
1543
  failureType: 'simulation',
@@ -1481,17 +1559,16 @@ export class SequencerPublisher {
1481
1559
  private async addProposeTx(
1482
1560
  checkpoint: Checkpoint,
1483
1561
  encodedData: L1ProcessArgs,
1484
- opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
1485
- timestamp: bigint,
1562
+ opts: {
1563
+ txTimeoutAt?: Date;
1564
+ forcePendingCheckpointNumber?: CheckpointNumber;
1565
+ forceProposedFeeHeader?: { checkpointNumber: CheckpointNumber; feeHeader: FeeHeader };
1566
+ } = {},
1486
1567
  ): Promise<void> {
1487
1568
  const slot = checkpoint.header.slotNumber;
1488
1569
  const timer = new Timer();
1489
1570
  const kzg = Blob.getViemKzgInstance();
1490
- const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(
1491
- encodedData,
1492
- timestamp,
1493
- opts,
1494
- );
1571
+ const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(encodedData, opts);
1495
1572
  const startBlock = await this.l1TxUtils.getBlockNumber();
1496
1573
  const gasLimit = this.l1TxUtils.bumpGasLimit(
1497
1574
  BigInt(Math.ceil((Number(simulationResult.gasUsed) * 64) / 63)) +
@@ -1567,4 +1644,17 @@ export class SequencerPublisher {
1567
1644
  },
1568
1645
  });
1569
1646
  }
1647
+
1648
+ /** Returns the timestamp of the last L1 slot within a given L2 slot. Used as the simulation timestamp
1649
+ * for eth_simulateV1 calls, since it's guaranteed to be greater than any L1 block produced during the slot. */
1650
+ private getSimulationTimestamp(slot: SlotNumber): bigint {
1651
+ const l1Constants = this.epochCache.getL1Constants();
1652
+ return getLastL1SlotTimestampForL2Slot(slot, l1Constants);
1653
+ }
1654
+
1655
+ /** Returns the timestamp of the next L1 slot boundary after now. */
1656
+ private getNextL1SlotTimestamp(): bigint {
1657
+ const l1Constants = this.epochCache.getL1Constants();
1658
+ return getNextL1SlotTimestamp(this.dateProvider.nowInSeconds(), l1Constants);
1659
+ }
1570
1660
  }