@aztec/sequencer-client 0.0.1-commit.21ecf947b → 0.0.1-commit.2448fdb

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 (58) hide show
  1. package/dest/client/sequencer-client.d.ts +16 -7
  2. package/dest/client/sequencer-client.d.ts.map +1 -1
  3. package/dest/client/sequencer-client.js +56 -23
  4. package/dest/config.d.ts +25 -5
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +42 -20
  7. package/dest/global_variable_builder/global_builder.d.ts +14 -10
  8. package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
  9. package/dest/global_variable_builder/global_builder.js +22 -21
  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 +31 -17
  13. package/dest/publisher/config.d.ts.map +1 -1
  14. package/dest/publisher/config.js +101 -42
  15. package/dest/publisher/sequencer-publisher-factory.d.ts +11 -3
  16. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  17. package/dest/publisher/sequencer-publisher-factory.js +13 -2
  18. package/dest/publisher/sequencer-publisher.d.ts +22 -13
  19. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  20. package/dest/publisher/sequencer-publisher.js +92 -52
  21. package/dest/sequencer/checkpoint_proposal_job.d.ts +4 -4
  22. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  23. package/dest/sequencer/checkpoint_proposal_job.js +129 -75
  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/metrics.d.ts +13 -5
  28. package/dest/sequencer/metrics.d.ts.map +1 -1
  29. package/dest/sequencer/metrics.js +32 -10
  30. package/dest/sequencer/sequencer.d.ts +27 -13
  31. package/dest/sequencer/sequencer.d.ts.map +1 -1
  32. package/dest/sequencer/sequencer.js +41 -42
  33. package/dest/sequencer/timetable.d.ts +4 -3
  34. package/dest/sequencer/timetable.d.ts.map +1 -1
  35. package/dest/sequencer/timetable.js +6 -7
  36. package/dest/sequencer/types.d.ts +2 -2
  37. package/dest/sequencer/types.d.ts.map +1 -1
  38. package/dest/test/index.d.ts +3 -5
  39. package/dest/test/index.d.ts.map +1 -1
  40. package/dest/test/mock_checkpoint_builder.d.ts +7 -9
  41. package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
  42. package/dest/test/mock_checkpoint_builder.js +41 -30
  43. package/package.json +28 -28
  44. package/src/client/sequencer-client.ts +78 -21
  45. package/src/config.ts +52 -27
  46. package/src/global_variable_builder/global_builder.ts +23 -24
  47. package/src/global_variable_builder/index.ts +1 -1
  48. package/src/publisher/config.ts +112 -43
  49. package/src/publisher/sequencer-publisher-factory.ts +23 -6
  50. package/src/publisher/sequencer-publisher.ts +96 -66
  51. package/src/sequencer/checkpoint_proposal_job.ts +200 -106
  52. package/src/sequencer/checkpoint_voter.ts +1 -12
  53. package/src/sequencer/metrics.ts +39 -13
  54. package/src/sequencer/sequencer.ts +52 -48
  55. package/src/sequencer/timetable.ts +7 -7
  56. package/src/sequencer/types.ts +1 -1
  57. package/src/test/index.ts +2 -4
  58. package/src/test/mock_checkpoint_builder.ts +53 -48
@@ -19,26 +19,29 @@ import {
19
19
  type L1BlobInputs,
20
20
  type L1TxConfig,
21
21
  type L1TxRequest,
22
+ type L1TxUtils,
22
23
  MAX_L1_TX_LIMIT,
23
24
  type TransactionStats,
24
25
  WEI_CONST,
25
26
  } from '@aztec/ethereum/l1-tx-utils';
26
- import type { L1TxUtilsWithBlobs } from '@aztec/ethereum/l1-tx-utils-with-blobs';
27
27
  import { FormattedViemError, formatViemError, mergeAbis, tryExtractEvent } from '@aztec/ethereum/utils';
28
28
  import { sumBigint } from '@aztec/foundation/bigint';
29
29
  import { toHex as toPaddedHex } from '@aztec/foundation/bigint-buffer';
30
30
  import { CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
31
+ import { trimmedBytesLength } from '@aztec/foundation/buffer';
31
32
  import { pick } from '@aztec/foundation/collection';
32
33
  import type { Fr } from '@aztec/foundation/curves/bn254';
33
34
  import { EthAddress } from '@aztec/foundation/eth-address';
34
35
  import { Signature, type ViemSignature } from '@aztec/foundation/eth-signature';
35
36
  import { type Logger, createLogger } from '@aztec/foundation/log';
37
+ import { makeBackoff, retry } from '@aztec/foundation/retry';
36
38
  import { bufferToHex } from '@aztec/foundation/string';
37
39
  import { DateProvider, Timer } from '@aztec/foundation/timer';
38
40
  import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
39
41
  import { type ProposerSlashAction, encodeSlashConsensusVotes } from '@aztec/slasher';
40
42
  import { CommitteeAttestationsAndSigners, type ValidateCheckpointResult } from '@aztec/stdlib/block';
41
43
  import type { Checkpoint } from '@aztec/stdlib/checkpoint';
44
+ import { getLastL1SlotTimestampForL2Slot, getNextL1SlotTimestamp } from '@aztec/stdlib/epoch-helpers';
42
45
  import { SlashFactoryContract } from '@aztec/stdlib/l1-contracts';
43
46
  import type { CheckpointHeader } from '@aztec/stdlib/rollup';
44
47
  import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
@@ -46,7 +49,7 @@ import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from
46
49
 
47
50
  import { type StateOverride, type TransactionReceipt, type TypedDataDefinition, encodeFunctionData, toHex } from 'viem';
48
51
 
49
- import type { PublisherConfig, TxSenderConfig } from './config.js';
52
+ import type { SequencerPublisherConfig } from './config.js';
50
53
  import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
51
54
 
52
55
  /** Arguments to the process method of the rollup contract */
@@ -115,9 +118,12 @@ export class SequencerPublisher {
115
118
  protected lastActions: Partial<Record<Action, SlotNumber>> = {};
116
119
 
117
120
  private isPayloadEmptyCache: Map<string, boolean> = new Map<string, boolean>();
121
+ private payloadProposedCache: Set<string> = new Set<string>();
118
122
 
119
123
  protected log: Logger;
120
124
  protected ethereumSlotDuration: bigint;
125
+ protected aztecSlotDuration: bigint;
126
+ private dateProvider: DateProvider;
121
127
 
122
128
  private blobClient: BlobClientInterface;
123
129
 
@@ -136,7 +142,7 @@ export class SequencerPublisher {
136
142
  // Gas report for VotingWithSigTest shows a max gas of 100k, but we've seen it cost 700k+ in testnet
137
143
  public static VOTE_GAS_GUESS: bigint = 800_000n;
138
144
 
139
- public l1TxUtils: L1TxUtilsWithBlobs;
145
+ public l1TxUtils: L1TxUtils;
140
146
  public rollupContract: RollupContract;
141
147
  public govProposerContract: GovernanceProposerContract;
142
148
  public slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
@@ -147,11 +153,12 @@ export class SequencerPublisher {
147
153
  protected requests: RequestWithExpiry[] = [];
148
154
 
149
155
  constructor(
150
- private config: TxSenderConfig & PublisherConfig & Pick<L1ContractsConfig, 'ethereumSlotDuration'>,
156
+ private config: Pick<SequencerPublisherConfig, 'fishermanMode'> &
157
+ Pick<L1ContractsConfig, 'ethereumSlotDuration' | 'aztecSlotDuration'> & { l1ChainId: number },
151
158
  deps: {
152
159
  telemetry?: TelemetryClient;
153
160
  blobClient: BlobClientInterface;
154
- l1TxUtils: L1TxUtilsWithBlobs;
161
+ l1TxUtils: L1TxUtils;
155
162
  rollupContract: RollupContract;
156
163
  slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
157
164
  governanceProposerContract: GovernanceProposerContract;
@@ -165,6 +172,8 @@ export class SequencerPublisher {
165
172
  ) {
166
173
  this.log = deps.log ?? createLogger('sequencer:publisher');
167
174
  this.ethereumSlotDuration = BigInt(config.ethereumSlotDuration);
175
+ this.aztecSlotDuration = BigInt(config.aztecSlotDuration);
176
+ this.dateProvider = deps.dateProvider;
168
177
  this.epochCache = deps.epochCache;
169
178
  this.lastActions = deps.lastActions;
170
179
 
@@ -353,8 +362,8 @@ export class SequencerPublisher {
353
362
  // @note - we can only have one blob config per bundle
354
363
  // find requests with gas and blob configs
355
364
  // See https://github.com/AztecProtocol/aztec-packages/issues/11513
356
- const gasConfigs = requestsToProcess.filter(request => request.gasConfig).map(request => request.gasConfig);
357
- const blobConfigs = requestsToProcess.filter(request => request.blobConfig).map(request => request.blobConfig);
365
+ const gasConfigs = validRequests.filter(request => request.gasConfig).map(request => request.gasConfig);
366
+ const blobConfigs = validRequests.filter(request => request.blobConfig).map(request => request.blobConfig);
358
367
 
359
368
  if (blobConfigs.length > 1) {
360
369
  throw new Error('Multiple blob configs found');
@@ -422,7 +431,16 @@ export class SequencerPublisher {
422
431
  this.log.error(`Failed to publish bundled transactions (${actionsListStr})`, result);
423
432
  return { failedActions: requests.map(r => r.action) };
424
433
  } else {
425
- this.log.verbose(`Published bundled transactions (${actionsListStr})`, { result, requests });
434
+ this.log.verbose(`Published bundled transactions (${actionsListStr})`, {
435
+ result,
436
+ requests: requests.map(r => ({
437
+ ...r,
438
+ // Avoid logging large blob data
439
+ blobConfig: r.blobConfig
440
+ ? { ...r.blobConfig, blobs: r.blobConfig.blobs.map(b => ({ size: trimmedBytesLength(b) })) }
441
+ : undefined,
442
+ })),
443
+ });
426
444
  const successfulActions: Action[] = [];
427
445
  const failedActions: Action[] = [];
428
446
  for (const request of requests) {
@@ -437,20 +455,24 @@ export class SequencerPublisher {
437
455
  }
438
456
 
439
457
  /**
440
- * @notice Will call `canProposeAtNextEthBlock` to make sure that it is possible to propose
458
+ * @notice Will call `canProposeAt` to make sure that it is possible to propose
441
459
  * @param tipArchive - The archive to check
442
460
  * @returns The slot and block number if it is possible to propose, undefined otherwise
443
461
  */
444
- public canProposeAtNextEthBlock(
462
+ public canProposeAt(
445
463
  tipArchive: Fr,
446
464
  msgSender: EthAddress,
447
- opts: { forcePendingCheckpointNumber?: CheckpointNumber } = {},
465
+ opts: { forcePendingCheckpointNumber?: CheckpointNumber; pipelined?: boolean } = {},
448
466
  ) {
449
467
  // TODO: #14291 - should loop through multiple keys to check if any of them can propose
450
468
  const ignoredErrors = ['SlotAlreadyInChain', 'InvalidProposer', 'InvalidArchive'];
451
469
 
470
+ const pipelined = opts.pipelined ?? false;
471
+ const slotOffset = pipelined ? this.aztecSlotDuration : 0n;
472
+ const nextL1SlotTs = this.getNextL1SlotTimestamp() + slotOffset;
473
+
452
474
  return this.rollupContract
453
- .canProposeAtNextEthBlock(tipArchive.toBuffer(), msgSender.toString(), Number(this.ethereumSlotDuration), {
475
+ .canProposeAt(tipArchive.toBuffer(), msgSender.toString(), nextL1SlotTs, {
454
476
  forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
455
477
  })
456
478
  .catch(err => {
@@ -487,7 +509,7 @@ export class SequencerPublisher {
487
509
  flags,
488
510
  ] as const;
489
511
 
490
- const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
512
+ const ts = this.getSimulationTimestamp(header.slotNumber);
491
513
  const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(
492
514
  opts?.forcePendingCheckpointNumber,
493
515
  );
@@ -510,7 +532,7 @@ export class SequencerPublisher {
510
532
  data: encodeFunctionData({ abi: RollupAbi, functionName: 'validateHeaderWithAttestations', args }),
511
533
  from: MULTI_CALL_3_ADDRESS,
512
534
  },
513
- { time: ts + 1n },
535
+ { time: ts },
514
536
  stateOverrides,
515
537
  );
516
538
  this.log.debug(`Simulated validateHeader`);
@@ -637,26 +659,9 @@ export class SequencerPublisher {
637
659
  attestationsAndSigners: CommitteeAttestationsAndSigners,
638
660
  attestationsAndSignersSignature: Signature,
639
661
  options: { forcePendingCheckpointNumber?: CheckpointNumber },
640
- ): Promise<bigint> {
641
- const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
642
-
643
- // TODO(palla/mbps): This should not be needed, there's no flow where we propose with zero attestations. Or is there?
644
- // If we have no attestations, we still need to provide the empty attestations
645
- // so that the committee is recalculated correctly
646
- // const ignoreSignatures = attestationsAndSigners.attestations.length === 0;
647
- // if (ignoreSignatures) {
648
- // const { committee } = await this.epochCache.getCommittee(block.header.globalVariables.slotNumber);
649
- // if (!committee) {
650
- // this.log.warn(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
651
- // throw new Error(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
652
- // }
653
- // attestationsAndSigners.attestations = committee.map(committeeMember =>
654
- // CommitteeAttestation.fromAddress(committeeMember),
655
- // );
656
- // }
657
-
662
+ ): Promise<void> {
658
663
  const blobFields = checkpoint.toBlobFields();
659
- const blobs = getBlobsPerL1Block(blobFields);
664
+ const blobs = await getBlobsPerL1Block(blobFields);
660
665
  const blobInput = getPrefixedEthBlobCommitments(blobs);
661
666
 
662
667
  const args = [
@@ -673,13 +678,11 @@ export class SequencerPublisher {
673
678
  blobInput,
674
679
  ] as const;
675
680
 
676
- await this.simulateProposeTx(args, ts, options);
677
- return ts;
681
+ await this.simulateProposeTx(args, options);
678
682
  }
679
683
 
680
684
  private async enqueueCastSignalHelper(
681
685
  slotNumber: SlotNumber,
682
- timestamp: bigint,
683
686
  signalType: GovernanceSignalAction,
684
687
  payload: EthAddress,
685
688
  base: IEmpireBase,
@@ -713,6 +716,32 @@ export class SequencerPublisher {
713
716
  return false;
714
717
  }
715
718
 
719
+ // Check if payload was already submitted to governance
720
+ const cacheKey = payload.toString();
721
+ if (!this.payloadProposedCache.has(cacheKey)) {
722
+ try {
723
+ const l1StartBlock = await this.rollupContract.getL1StartBlock();
724
+ const proposed = await retry(
725
+ () => base.hasPayloadBeenProposed(payload.toString(), l1StartBlock),
726
+ 'Check if payload was proposed',
727
+ makeBackoff([0, 1, 2]),
728
+ this.log,
729
+ true,
730
+ );
731
+ if (proposed) {
732
+ this.payloadProposedCache.add(cacheKey);
733
+ }
734
+ } catch (err) {
735
+ this.log.warn(`Failed to check if payload ${payload} was proposed after retries, skipping signal`, err);
736
+ return false;
737
+ }
738
+ }
739
+
740
+ if (this.payloadProposedCache.has(cacheKey)) {
741
+ this.log.info(`Payload ${payload} was already proposed to governance, stopping signals`);
742
+ return false;
743
+ }
744
+
716
745
  const cachedLastVote = this.lastActions[signalType];
717
746
  this.lastActions[signalType] = slotNumber;
718
747
  const action = signalType;
@@ -731,11 +760,16 @@ export class SequencerPublisher {
731
760
  lastValidL2Slot: slotNumber,
732
761
  });
733
762
 
763
+ const timestamp = this.getSimulationTimestamp(slotNumber);
764
+
734
765
  try {
735
766
  await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
736
767
  this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
737
768
  } catch (err) {
738
- this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, err);
769
+ const viemError = formatViemError(err);
770
+ this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, viemError, {
771
+ simulationTimestamp: timestamp,
772
+ });
739
773
  // Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
740
774
  }
741
775
 
@@ -786,19 +820,16 @@ export class SequencerPublisher {
786
820
  /**
787
821
  * Enqueues a governance castSignal transaction to cast a signal for a given slot number.
788
822
  * @param slotNumber - The slot number to cast a signal for.
789
- * @param timestamp - The timestamp of the slot to cast a signal for.
790
823
  * @returns True if the signal was successfully enqueued, false otherwise.
791
824
  */
792
825
  public enqueueGovernanceCastSignal(
793
826
  governancePayload: EthAddress,
794
827
  slotNumber: SlotNumber,
795
- timestamp: bigint,
796
828
  signerAddress: EthAddress,
797
829
  signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
798
830
  ): Promise<boolean> {
799
831
  return this.enqueueCastSignalHelper(
800
832
  slotNumber,
801
- timestamp,
802
833
  'governance-signal',
803
834
  governancePayload,
804
835
  this.govProposerContract,
@@ -811,7 +842,6 @@ export class SequencerPublisher {
811
842
  public async enqueueSlashingActions(
812
843
  actions: ProposerSlashAction[],
813
844
  slotNumber: SlotNumber,
814
- timestamp: bigint,
815
845
  signerAddress: EthAddress,
816
846
  signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
817
847
  ): Promise<boolean> {
@@ -832,7 +862,6 @@ export class SequencerPublisher {
832
862
  });
833
863
  await this.enqueueCastSignalHelper(
834
864
  slotNumber,
835
- timestamp,
836
865
  'empire-slashing-signal',
837
866
  action.payload,
838
867
  this.slashingProposerContract,
@@ -851,7 +880,6 @@ export class SequencerPublisher {
851
880
  (receipt: TransactionReceipt) =>
852
881
  !!this.slashFactoryContract.tryExtractSlashPayloadCreatedEvent(receipt.logs),
853
882
  slotNumber,
854
- timestamp,
855
883
  );
856
884
  break;
857
885
  }
@@ -869,7 +897,6 @@ export class SequencerPublisher {
869
897
  request,
870
898
  (receipt: TransactionReceipt) => !!empireSlashingProposer.tryExtractPayloadSubmittedEvent(receipt.logs),
871
899
  slotNumber,
872
- timestamp,
873
900
  );
874
901
  break;
875
902
  }
@@ -893,7 +920,6 @@ export class SequencerPublisher {
893
920
  request,
894
921
  (receipt: TransactionReceipt) => !!tallySlashingProposer.tryExtractVoteCastEvent(receipt.logs),
895
922
  slotNumber,
896
- timestamp,
897
923
  );
898
924
  break;
899
925
  }
@@ -915,7 +941,6 @@ export class SequencerPublisher {
915
941
  request,
916
942
  (receipt: TransactionReceipt) => !!tallySlashingProposer.tryExtractRoundExecutedEvent(receipt.logs),
917
943
  slotNumber,
918
- timestamp,
919
944
  );
920
945
  break;
921
946
  }
@@ -940,7 +965,7 @@ export class SequencerPublisher {
940
965
  const checkpointHeader = checkpoint.header;
941
966
 
942
967
  const blobFields = checkpoint.toBlobFields();
943
- const blobs = getBlobsPerL1Block(blobFields);
968
+ const blobs = await getBlobsPerL1Block(blobFields);
944
969
 
945
970
  const proposeTxArgs: L1ProcessArgs = {
946
971
  header: checkpointHeader,
@@ -951,15 +976,13 @@ export class SequencerPublisher {
951
976
  feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
952
977
  };
953
978
 
954
- let ts: bigint;
955
-
956
979
  try {
957
980
  // @note This will make sure that we are passing the checks for our header ASSUMING that the data is also made available
958
981
  // This means that we can avoid the simulation issues in later checks.
959
982
  // By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which
960
983
  // make time consistency checks break.
961
984
  // TODO(palla): Check whether we're validating twice, once here and once within addProposeTx, since we call simulateProposeTx in both places.
962
- ts = await this.validateCheckpointForSubmission(
985
+ await this.validateCheckpointForSubmission(
963
986
  checkpoint,
964
987
  attestationsAndSigners,
965
988
  attestationsAndSignersSignature,
@@ -975,7 +998,7 @@ export class SequencerPublisher {
975
998
  }
976
999
 
977
1000
  this.log.verbose(`Enqueuing checkpoint propose transaction`, { ...checkpoint.toCheckpointInfo(), ...opts });
978
- await this.addProposeTx(checkpoint, proposeTxArgs, opts, ts);
1001
+ await this.addProposeTx(checkpoint, proposeTxArgs, opts);
979
1002
  }
980
1003
 
981
1004
  public enqueueInvalidateCheckpoint(
@@ -1018,8 +1041,8 @@ export class SequencerPublisher {
1018
1041
  request: L1TxRequest,
1019
1042
  checkSuccess: (receipt: TransactionReceipt) => boolean | undefined,
1020
1043
  slotNumber: SlotNumber,
1021
- timestamp: bigint,
1022
1044
  ) {
1045
+ const timestamp = this.getSimulationTimestamp(slotNumber);
1023
1046
  const logData = { slotNumber, timestamp, gasLimit: undefined as bigint | undefined };
1024
1047
  if (this.lastActions[action] && this.lastActions[action] === slotNumber) {
1025
1048
  this.log.debug(`Skipping duplicate action ${action} for slot ${slotNumber}`);
@@ -1033,8 +1056,9 @@ export class SequencerPublisher {
1033
1056
 
1034
1057
  let gasUsed: bigint;
1035
1058
  const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
1059
+
1036
1060
  try {
1037
- ({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], simulateAbi)); // TODO(palla/slash): Check the timestamp logic
1061
+ ({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], simulateAbi));
1038
1062
  this.log.verbose(`Simulation for ${action} succeeded`, { ...logData, request, gasUsed });
1039
1063
  } catch (err) {
1040
1064
  const viemError = formatViemError(err, simulateAbi);
@@ -1090,7 +1114,6 @@ export class SequencerPublisher {
1090
1114
 
1091
1115
  private async prepareProposeTx(
1092
1116
  encodedData: L1ProcessArgs,
1093
- timestamp: bigint,
1094
1117
  options: { forcePendingCheckpointNumber?: CheckpointNumber },
1095
1118
  ) {
1096
1119
  const kzg = Blob.getViemKzgInstance();
@@ -1145,7 +1168,7 @@ export class SequencerPublisher {
1145
1168
  blobInput,
1146
1169
  ] as const;
1147
1170
 
1148
- const { rollupData, simulationResult } = await this.simulateProposeTx(args, timestamp, options);
1171
+ const { rollupData, simulationResult } = await this.simulateProposeTx(args, options);
1149
1172
 
1150
1173
  return { args, blobEvaluationGas, rollupData, simulationResult };
1151
1174
  }
@@ -1153,7 +1176,6 @@ export class SequencerPublisher {
1153
1176
  /**
1154
1177
  * Simulates the propose tx with eth_simulateV1
1155
1178
  * @param args - The propose tx args
1156
- * @param timestamp - The timestamp to simulate proposal at
1157
1179
  * @returns The simulation result
1158
1180
  */
1159
1181
  private async simulateProposeTx(
@@ -1170,7 +1192,6 @@ export class SequencerPublisher {
1170
1192
  ViemSignature,
1171
1193
  `0x${string}`,
1172
1194
  ],
1173
- timestamp: bigint,
1174
1195
  options: { forcePendingCheckpointNumber?: CheckpointNumber },
1175
1196
  ) {
1176
1197
  const rollupData = encodeFunctionData({
@@ -1204,6 +1225,8 @@ export class SequencerPublisher {
1204
1225
  });
1205
1226
  }
1206
1227
 
1228
+ const simTs = this.getSimulationTimestamp(SlotNumber.fromBigInt(args[0].header.slotNumber));
1229
+
1207
1230
  const simulationResult = await this.l1TxUtils
1208
1231
  .simulate(
1209
1232
  {
@@ -1213,8 +1236,7 @@ export class SequencerPublisher {
1213
1236
  ...(this.proposerAddressForSimulation && { from: this.proposerAddressForSimulation.toString() }),
1214
1237
  },
1215
1238
  {
1216
- // @note we add 1n to the timestamp because geth implementation doesn't like simulation timestamp to be equal to the current block timestamp
1217
- time: timestamp + 1n,
1239
+ time: simTs,
1218
1240
  // @note reth should have a 30m gas limit per block but throws errors that this tx is beyond limit so we increase here
1219
1241
  gasLimit: MAX_L1_TX_LIMIT * 2n,
1220
1242
  },
@@ -1236,7 +1258,7 @@ export class SequencerPublisher {
1236
1258
  logs: [],
1237
1259
  };
1238
1260
  }
1239
- this.log.error(`Failed to simulate propose tx`, viemError);
1261
+ this.log.error(`Failed to simulate propose tx`, viemError, { simulationTimestamp: simTs });
1240
1262
  throw err;
1241
1263
  });
1242
1264
 
@@ -1247,16 +1269,11 @@ export class SequencerPublisher {
1247
1269
  checkpoint: Checkpoint,
1248
1270
  encodedData: L1ProcessArgs,
1249
1271
  opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
1250
- timestamp: bigint,
1251
1272
  ): Promise<void> {
1252
1273
  const slot = checkpoint.header.slotNumber;
1253
1274
  const timer = new Timer();
1254
1275
  const kzg = Blob.getViemKzgInstance();
1255
- const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(
1256
- encodedData,
1257
- timestamp,
1258
- opts,
1259
- );
1276
+ const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(encodedData, opts);
1260
1277
  const startBlock = await this.l1TxUtils.getBlockNumber();
1261
1278
  const gasLimit = this.l1TxUtils.bumpGasLimit(
1262
1279
  BigInt(Math.ceil((Number(simulationResult.gasUsed) * 64) / 63)) +
@@ -1332,4 +1349,17 @@ export class SequencerPublisher {
1332
1349
  },
1333
1350
  });
1334
1351
  }
1352
+
1353
+ /** Returns the timestamp of the last L1 slot within a given L2 slot. Used as the simulation timestamp
1354
+ * for eth_simulateV1 calls, since it's guaranteed to be greater than any L1 block produced during the slot. */
1355
+ private getSimulationTimestamp(slot: SlotNumber): bigint {
1356
+ const l1Constants = this.epochCache.getL1Constants();
1357
+ return getLastL1SlotTimestampForL2Slot(slot, l1Constants);
1358
+ }
1359
+
1360
+ /** Returns the timestamp of the next L1 slot boundary after now. */
1361
+ private getNextL1SlotTimestamp(): bigint {
1362
+ const l1Constants = this.epochCache.getL1Constants();
1363
+ return getNextL1SlotTimestamp(this.dateProvider.nowInSeconds(), l1Constants);
1364
+ }
1335
1365
  }