@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.
@@ -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,
@@ -36,13 +37,14 @@ import { EthAddress } from '@aztec/foundation/eth-address';
36
37
  import { Signature, type ViemSignature } from '@aztec/foundation/eth-signature';
37
38
  import { type Logger, createLogger } from '@aztec/foundation/log';
38
39
  import { makeBackoff, retry } from '@aztec/foundation/retry';
40
+ import { InterruptibleSleep } from '@aztec/foundation/sleep';
39
41
  import { bufferToHex } from '@aztec/foundation/string';
40
- import { DateProvider, Timer } from '@aztec/foundation/timer';
42
+ import { type DateProvider, Timer } from '@aztec/foundation/timer';
41
43
  import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
42
44
  import { type ProposerSlashAction, encodeSlashConsensusVotes } from '@aztec/slasher';
43
45
  import { CommitteeAttestationsAndSigners, type ValidateCheckpointResult } from '@aztec/stdlib/block';
44
46
  import type { Checkpoint } from '@aztec/stdlib/checkpoint';
45
- import { getNextL1SlotTimestamp } from '@aztec/stdlib/epoch-helpers';
47
+ import { getLastL1SlotTimestampForL2Slot, getNextL1SlotTimestamp } from '@aztec/stdlib/epoch-helpers';
46
48
  import { SlashFactoryContract } from '@aztec/stdlib/l1-contracts';
47
49
  import type { CheckpointHeader } from '@aztec/stdlib/rollup';
48
50
  import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
@@ -63,6 +65,20 @@ import type { SequencerPublisherConfig } from './config.js';
63
65
  import { type FailedL1Tx, type L1TxFailedStore, createL1TxFailedStore } from './l1_tx_failed_store/index.js';
64
66
  import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
65
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
+
66
82
  /** Arguments to the process method of the rollup contract */
67
83
  type L1ProcessArgs = {
68
84
  /** The L2 block header. */
@@ -104,6 +120,8 @@ export type InvalidateCheckpointRequest = {
104
120
  gasUsed: bigint;
105
121
  checkpointNumber: CheckpointNumber;
106
122
  forcePendingCheckpointNumber: CheckpointNumber;
123
+ /** Archive at the rollback target checkpoint (checkpoint N-1). */
124
+ lastArchive: Fr;
107
125
  };
108
126
 
109
127
  interface RequestWithExpiry {
@@ -135,7 +153,9 @@ export class SequencerPublisher {
135
153
  protected log: Logger;
136
154
  protected ethereumSlotDuration: bigint;
137
155
  protected aztecSlotDuration: bigint;
138
- private dateProvider: DateProvider;
156
+
157
+ /** Date provider for wall-clock time. */
158
+ private readonly dateProvider: DateProvider;
139
159
 
140
160
  private blobClient: BlobClientInterface;
141
161
 
@@ -151,6 +171,9 @@ export class SequencerPublisher {
151
171
  /** Fee asset price oracle for computing price modifiers from Uniswap V4 */
152
172
  private feeAssetPriceOracle: FeeAssetPriceOracle;
153
173
 
174
+ /** Interruptible sleep used by sendRequestsAt to wait until a target timestamp. */
175
+ private readonly interruptibleSleep = new InterruptibleSleep();
176
+
154
177
  // A CALL to a cold address is 2700 gas
155
178
  public static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
156
179
 
@@ -194,6 +217,7 @@ export class SequencerPublisher {
194
217
  this.lastActions = deps.lastActions;
195
218
 
196
219
  this.blobClient = deps.blobClient;
220
+ this.dateProvider = deps.dateProvider;
197
221
 
198
222
  const telemetry = deps.telemetry ?? getTelemetryClient();
199
223
  this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
@@ -369,9 +393,10 @@ export class SequencerPublisher {
369
393
  * - undefined if no valid requests are found OR the tx failed to send.
370
394
  */
371
395
  @trackSpan('SequencerPublisher.sendRequests')
372
- public async sendRequests() {
396
+ public async sendRequests(): Promise<SendRequestsResult | undefined> {
373
397
  const requestsToProcess = [...this.requests];
374
398
  this.requests = [];
399
+
375
400
  if (this.interrupted || requestsToProcess.length === 0) {
376
401
  return undefined;
377
402
  }
@@ -530,6 +555,23 @@ export class SequencerPublisher {
530
555
  }
531
556
  }
532
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
+
533
575
  private callbackBundledTransactions(
534
576
  requests: RequestWithExpiry[],
535
577
  result: { receipt: TransactionReceipt; errorMsg?: string } | FormattedViemError | undefined,
@@ -608,7 +650,11 @@ export class SequencerPublisher {
608
650
  public canProposeAt(
609
651
  tipArchive: Fr,
610
652
  msgSender: EthAddress,
611
- opts: { forcePendingCheckpointNumber?: CheckpointNumber; pipelined?: boolean } = {},
653
+ opts: {
654
+ forcePendingCheckpointNumber?: CheckpointNumber;
655
+ forceArchive?: { checkpointNumber: CheckpointNumber; archive: Fr };
656
+ pipelined?: boolean;
657
+ } = {},
612
658
  ) {
613
659
  // TODO: #14291 - should loop through multiple keys to check if any of them can propose
614
660
  const ignoredErrors = ['SlotAlreadyInChain', 'InvalidProposer', 'InvalidArchive'];
@@ -620,6 +666,7 @@ export class SequencerPublisher {
620
666
  return this.rollupContract
621
667
  .canProposeAt(tipArchive.toBuffer(), msgSender.toString(), nextL1SlotTs, {
622
668
  forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
669
+ forceArchive: opts.forceArchive,
623
670
  })
624
671
  .catch(err => {
625
672
  if (err instanceof FormattedViemError && ignoredErrors.find(e => err.message.includes(e))) {
@@ -656,7 +703,7 @@ export class SequencerPublisher {
656
703
  flags,
657
704
  ] as const;
658
705
 
659
- const ts = this.getNextL1SlotTimestamp();
706
+ const ts = this.getSimulationTimestamp(header.slotNumber);
660
707
  const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(
661
708
  opts?.forcePendingCheckpointNumber,
662
709
  );
@@ -679,7 +726,7 @@ export class SequencerPublisher {
679
726
  data: encodeFunctionData({ abi: RollupAbi, functionName: 'validateHeaderWithAttestations', args }),
680
727
  from: MULTI_CALL_3_ADDRESS,
681
728
  },
682
- { time: ts + 1n },
729
+ { time: ts },
683
730
  stateOverrides,
684
731
  );
685
732
  this.log.debug(`Simulated validateHeader`);
@@ -732,6 +779,7 @@ export class SequencerPublisher {
732
779
  gasUsed,
733
780
  checkpointNumber,
734
781
  forcePendingCheckpointNumber: CheckpointNumber(checkpointNumber - 1),
782
+ lastArchive: validationResult.checkpoint.lastArchive,
735
783
  reason,
736
784
  };
737
785
  } catch (err) {
@@ -744,8 +792,8 @@ export class SequencerPublisher {
744
792
  `Simulation for invalidate checkpoint ${checkpointNumber} failed due to checkpoint not being in pending chain`,
745
793
  { ...logData, request, error: viemError.message },
746
794
  );
747
- const latestPendingCheckpointNumber = await this.rollupContract.getCheckpointNumber();
748
- if (latestPendingCheckpointNumber < checkpointNumber) {
795
+ const latestProposedCheckpointNumber = await this.rollupContract.getCheckpointNumber();
796
+ if (latestProposedCheckpointNumber < checkpointNumber) {
749
797
  this.log.verbose(`Checkpoint ${checkpointNumber} has already been invalidated`, { ...logData });
750
798
  return undefined;
751
799
  } else {
@@ -819,11 +867,11 @@ export class SequencerPublisher {
819
867
  checkpoint: Checkpoint,
820
868
  attestationsAndSigners: CommitteeAttestationsAndSigners,
821
869
  attestationsAndSignersSignature: Signature,
822
- options: { forcePendingCheckpointNumber?: CheckpointNumber },
823
- ): Promise<bigint> {
824
- // Anchor the simulation timestamp to the checkpoint's own slot start time
825
- // rather than the current L1 block timestamp, which may overshoot into the next slot if the build ran late.
826
- const ts = checkpoint.header.timestamp;
870
+ options: {
871
+ forcePendingCheckpointNumber?: CheckpointNumber;
872
+ forceProposedFeeHeader?: { checkpointNumber: CheckpointNumber; feeHeader: FeeHeader };
873
+ },
874
+ ): Promise<void> {
827
875
  const blobFields = checkpoint.toBlobFields();
828
876
  const blobs = await getBlobsPerL1Block(blobFields);
829
877
  const blobInput = getPrefixedEthBlobCommitments(blobs);
@@ -842,13 +890,11 @@ export class SequencerPublisher {
842
890
  blobInput,
843
891
  ] as const;
844
892
 
845
- await this.simulateProposeTx(args, ts, options);
846
- return ts;
893
+ await this.simulateProposeTx(args, options);
847
894
  }
848
895
 
849
896
  private async enqueueCastSignalHelper(
850
897
  slotNumber: SlotNumber,
851
- timestamp: bigint,
852
898
  signalType: GovernanceSignalAction,
853
899
  payload: EthAddress,
854
900
  base: IEmpireBase,
@@ -927,13 +973,17 @@ export class SequencerPublisher {
927
973
  });
928
974
 
929
975
  const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
976
+ const timestamp = this.getSimulationTimestamp(slotNumber);
930
977
 
931
978
  try {
932
979
  await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
933
980
  this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
934
981
  } catch (err) {
935
982
  const viemError = formatViemError(err);
936
- 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
+ });
937
987
  this.backupFailedTx({
938
988
  id: keccak256(request.data!),
939
989
  failureType: 'simulation',
@@ -996,19 +1046,16 @@ export class SequencerPublisher {
996
1046
  /**
997
1047
  * Enqueues a governance castSignal transaction to cast a signal for a given slot number.
998
1048
  * @param slotNumber - The slot number to cast a signal for.
999
- * @param timestamp - The timestamp of the slot to cast a signal for.
1000
1049
  * @returns True if the signal was successfully enqueued, false otherwise.
1001
1050
  */
1002
1051
  public enqueueGovernanceCastSignal(
1003
1052
  governancePayload: EthAddress,
1004
1053
  slotNumber: SlotNumber,
1005
- timestamp: bigint,
1006
1054
  signerAddress: EthAddress,
1007
1055
  signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
1008
1056
  ): Promise<boolean> {
1009
1057
  return this.enqueueCastSignalHelper(
1010
1058
  slotNumber,
1011
- timestamp,
1012
1059
  'governance-signal',
1013
1060
  governancePayload,
1014
1061
  this.govProposerContract,
@@ -1021,7 +1068,6 @@ export class SequencerPublisher {
1021
1068
  public async enqueueSlashingActions(
1022
1069
  actions: ProposerSlashAction[],
1023
1070
  slotNumber: SlotNumber,
1024
- timestamp: bigint,
1025
1071
  signerAddress: EthAddress,
1026
1072
  signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
1027
1073
  ): Promise<boolean> {
@@ -1042,7 +1088,6 @@ export class SequencerPublisher {
1042
1088
  });
1043
1089
  await this.enqueueCastSignalHelper(
1044
1090
  slotNumber,
1045
- timestamp,
1046
1091
  'empire-slashing-signal',
1047
1092
  action.payload,
1048
1093
  this.slashingProposerContract,
@@ -1061,7 +1106,6 @@ export class SequencerPublisher {
1061
1106
  (receipt: TransactionReceipt) =>
1062
1107
  !!this.slashFactoryContract.tryExtractSlashPayloadCreatedEvent(receipt.logs),
1063
1108
  slotNumber,
1064
- timestamp,
1065
1109
  );
1066
1110
  break;
1067
1111
  }
@@ -1079,7 +1123,6 @@ export class SequencerPublisher {
1079
1123
  request,
1080
1124
  (receipt: TransactionReceipt) => !!empireSlashingProposer.tryExtractPayloadSubmittedEvent(receipt.logs),
1081
1125
  slotNumber,
1082
- timestamp,
1083
1126
  );
1084
1127
  break;
1085
1128
  }
@@ -1103,7 +1146,6 @@ export class SequencerPublisher {
1103
1146
  request,
1104
1147
  (receipt: TransactionReceipt) => !!tallySlashingProposer.tryExtractVoteCastEvent(receipt.logs),
1105
1148
  slotNumber,
1106
- timestamp,
1107
1149
  );
1108
1150
  break;
1109
1151
  }
@@ -1125,7 +1167,6 @@ export class SequencerPublisher {
1125
1167
  request,
1126
1168
  (receipt: TransactionReceipt) => !!tallySlashingProposer.tryExtractRoundExecutedEvent(receipt.logs),
1127
1169
  slotNumber,
1128
- timestamp,
1129
1170
  );
1130
1171
  break;
1131
1172
  }
@@ -1145,7 +1186,11 @@ export class SequencerPublisher {
1145
1186
  checkpoint: Checkpoint,
1146
1187
  attestationsAndSigners: CommitteeAttestationsAndSigners,
1147
1188
  attestationsAndSignersSignature: Signature,
1148
- opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
1189
+ opts: {
1190
+ txTimeoutAt?: Date;
1191
+ forcePendingCheckpointNumber?: CheckpointNumber;
1192
+ forceProposedFeeHeader?: { checkpointNumber: CheckpointNumber; feeHeader: FeeHeader };
1193
+ } = {},
1149
1194
  ): Promise<void> {
1150
1195
  const checkpointHeader = checkpoint.header;
1151
1196
 
@@ -1161,15 +1206,13 @@ export class SequencerPublisher {
1161
1206
  feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
1162
1207
  };
1163
1208
 
1164
- let ts: bigint;
1165
-
1166
1209
  try {
1167
1210
  // @note This will make sure that we are passing the checks for our header ASSUMING that the data is also made available
1168
1211
  // This means that we can avoid the simulation issues in later checks.
1169
1212
  // By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which
1170
1213
  // make time consistency checks break.
1171
1214
  // TODO(palla): Check whether we're validating twice, once here and once within addProposeTx, since we call simulateProposeTx in both places.
1172
- ts = await this.validateCheckpointForSubmission(
1215
+ await this.validateCheckpointForSubmission(
1173
1216
  checkpoint,
1174
1217
  attestationsAndSigners,
1175
1218
  attestationsAndSignersSignature,
@@ -1185,7 +1228,7 @@ export class SequencerPublisher {
1185
1228
  }
1186
1229
 
1187
1230
  this.log.verbose(`Enqueuing checkpoint propose transaction`, { ...checkpoint.toCheckpointInfo(), ...opts });
1188
- await this.addProposeTx(checkpoint, proposeTxArgs, opts, ts);
1231
+ await this.addProposeTx(checkpoint, proposeTxArgs, opts);
1189
1232
  }
1190
1233
 
1191
1234
  public enqueueInvalidateCheckpoint(
@@ -1228,8 +1271,8 @@ export class SequencerPublisher {
1228
1271
  request: L1TxRequest,
1229
1272
  checkSuccess: (receipt: TransactionReceipt) => boolean | undefined,
1230
1273
  slotNumber: SlotNumber,
1231
- timestamp: bigint,
1232
1274
  ) {
1275
+ const timestamp = this.getSimulationTimestamp(slotNumber);
1233
1276
  const logData = { slotNumber, timestamp, gasLimit: undefined as bigint | undefined };
1234
1277
  if (this.lastActions[action] && this.lastActions[action] === slotNumber) {
1235
1278
  this.log.debug(`Skipping duplicate action ${action} for slot ${slotNumber}`);
@@ -1245,8 +1288,9 @@ export class SequencerPublisher {
1245
1288
 
1246
1289
  let gasUsed: bigint;
1247
1290
  const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
1291
+
1248
1292
  try {
1249
- ({ 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));
1250
1294
  this.log.verbose(`Simulation for ${action} succeeded`, { ...logData, request, gasUsed });
1251
1295
  } catch (err) {
1252
1296
  const viemError = formatViemError(err, simulateAbi);
@@ -1304,6 +1348,7 @@ export class SequencerPublisher {
1304
1348
  */
1305
1349
  public interrupt() {
1306
1350
  this.interrupted = true;
1351
+ this.interruptibleSleep.interrupt();
1307
1352
  this.l1TxUtils.interrupt();
1308
1353
  }
1309
1354
 
@@ -1315,7 +1360,6 @@ export class SequencerPublisher {
1315
1360
 
1316
1361
  private async prepareProposeTx(
1317
1362
  encodedData: L1ProcessArgs,
1318
- timestamp: bigint,
1319
1363
  options: { forcePendingCheckpointNumber?: CheckpointNumber },
1320
1364
  ) {
1321
1365
  const kzg = Blob.getViemKzgInstance();
@@ -1388,7 +1432,7 @@ export class SequencerPublisher {
1388
1432
  blobInput,
1389
1433
  ] as const;
1390
1434
 
1391
- const { rollupData, simulationResult } = await this.simulateProposeTx(args, timestamp, options);
1435
+ const { rollupData, simulationResult } = await this.simulateProposeTx(args, options);
1392
1436
 
1393
1437
  return { args, blobEvaluationGas, rollupData, simulationResult };
1394
1438
  }
@@ -1396,7 +1440,6 @@ export class SequencerPublisher {
1396
1440
  /**
1397
1441
  * Simulates the propose tx with eth_simulateV1
1398
1442
  * @param args - The propose tx args
1399
- * @param timestamp - The timestamp to simulate proposal at
1400
1443
  * @returns The simulation result
1401
1444
  */
1402
1445
  private async simulateProposeTx(
@@ -1413,8 +1456,10 @@ export class SequencerPublisher {
1413
1456
  ViemSignature,
1414
1457
  `0x${string}`,
1415
1458
  ],
1416
- timestamp: bigint,
1417
- options: { forcePendingCheckpointNumber?: CheckpointNumber },
1459
+ options: {
1460
+ forcePendingCheckpointNumber?: CheckpointNumber;
1461
+ forceProposedFeeHeader?: { checkpointNumber: CheckpointNumber; feeHeader: FeeHeader };
1462
+ },
1418
1463
  ) {
1419
1464
  const rollupData = encodeFunctionData({
1420
1465
  abi: RollupAbi,
@@ -1422,13 +1467,23 @@ export class SequencerPublisher {
1422
1467
  args,
1423
1468
  });
1424
1469
 
1425
- // override the pending checkpoint number if requested
1470
+ // override the proposed checkpoint number if requested
1426
1471
  const forcePendingCheckpointNumberStateDiff = (
1427
1472
  options.forcePendingCheckpointNumber !== undefined
1428
1473
  ? await this.rollupContract.makePendingCheckpointNumberOverride(options.forcePendingCheckpointNumber)
1429
1474
  : []
1430
1475
  ).flatMap(override => override.stateDiff ?? []);
1431
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
+
1432
1487
  const stateOverrides: StateOverride = [
1433
1488
  {
1434
1489
  address: this.rollupContract.address,
@@ -1436,6 +1491,7 @@ export class SequencerPublisher {
1436
1491
  stateDiff: [
1437
1492
  { slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true), value: toPaddedHex(0n, true) },
1438
1493
  ...forcePendingCheckpointNumberStateDiff,
1494
+ ...forceProposedFeeHeaderStateDiff,
1439
1495
  ],
1440
1496
  },
1441
1497
  ];
@@ -1448,6 +1504,7 @@ export class SequencerPublisher {
1448
1504
  }
1449
1505
 
1450
1506
  const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1507
+ const simTs = this.getSimulationTimestamp(SlotNumber.fromBigInt(args[0].header.slotNumber));
1451
1508
 
1452
1509
  const simulationResult = await this.l1TxUtils
1453
1510
  .simulate(
@@ -1458,8 +1515,7 @@ export class SequencerPublisher {
1458
1515
  ...(this.proposerAddressForSimulation && { from: this.proposerAddressForSimulation.toString() }),
1459
1516
  },
1460
1517
  {
1461
- // @note we add 1n to the timestamp because geth implementation doesn't like simulation timestamp to be equal to the current block timestamp
1462
- time: timestamp + 1n,
1518
+ time: simTs,
1463
1519
  // @note reth should have a 30m gas limit per block but throws errors that this tx is beyond limit so we increase here
1464
1520
  gasLimit: MAX_L1_TX_LIMIT * 2n,
1465
1521
  },
@@ -1481,7 +1537,7 @@ export class SequencerPublisher {
1481
1537
  logs: [],
1482
1538
  };
1483
1539
  }
1484
- this.log.error(`Failed to simulate propose tx`, viemError);
1540
+ this.log.error(`Failed to simulate propose tx`, viemError, { simulationTimestamp: simTs });
1485
1541
  this.backupFailedTx({
1486
1542
  id: keccak256(rollupData),
1487
1543
  failureType: 'simulation',
@@ -1503,17 +1559,16 @@ export class SequencerPublisher {
1503
1559
  private async addProposeTx(
1504
1560
  checkpoint: Checkpoint,
1505
1561
  encodedData: L1ProcessArgs,
1506
- opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
1507
- timestamp: bigint,
1562
+ opts: {
1563
+ txTimeoutAt?: Date;
1564
+ forcePendingCheckpointNumber?: CheckpointNumber;
1565
+ forceProposedFeeHeader?: { checkpointNumber: CheckpointNumber; feeHeader: FeeHeader };
1566
+ } = {},
1508
1567
  ): Promise<void> {
1509
1568
  const slot = checkpoint.header.slotNumber;
1510
1569
  const timer = new Timer();
1511
1570
  const kzg = Blob.getViemKzgInstance();
1512
- const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(
1513
- encodedData,
1514
- timestamp,
1515
- opts,
1516
- );
1571
+ const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(encodedData, opts);
1517
1572
  const startBlock = await this.l1TxUtils.getBlockNumber();
1518
1573
  const gasLimit = this.l1TxUtils.bumpGasLimit(
1519
1574
  BigInt(Math.ceil((Number(simulationResult.gasUsed) * 64) / 63)) +
@@ -1590,7 +1645,14 @@ export class SequencerPublisher {
1590
1645
  });
1591
1646
  }
1592
1647
 
1593
- /** Returns the timestamp to use when simulating L1 proposal calls */
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. */
1594
1656
  private getNextL1SlotTimestamp(): bigint {
1595
1657
  const l1Constants = this.epochCache.getL1Constants();
1596
1658
  return getNextL1SlotTimestamp(this.dateProvider.nowInSeconds(), l1Constants);