@aztec/sequencer-client 0.0.1-commit.a072138 → 0.0.1-commit.a89ec08

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 (62) 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 -6
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +46 -28
  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 +29 -13
  19. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  20. package/dest/publisher/sequencer-publisher.js +106 -57
  21. package/dest/sequencer/checkpoint_proposal_job.d.ts +10 -4
  22. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  23. package/dest/sequencer/checkpoint_proposal_job.js +162 -107
  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 +17 -5
  28. package/dest/sequencer/metrics.d.ts.map +1 -1
  29. package/dest/sequencer/metrics.js +86 -15
  30. package/dest/sequencer/sequencer.d.ts +29 -13
  31. package/dest/sequencer/sequencer.d.ts.map +1 -1
  32. package/dest/sequencer/sequencer.js +47 -43
  33. package/dest/sequencer/timetable.d.ts +4 -6
  34. package/dest/sequencer/timetable.d.ts.map +1 -1
  35. package/dest/sequencer/timetable.js +7 -11
  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 +11 -11
  41. package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
  42. package/dest/test/mock_checkpoint_builder.js +47 -34
  43. package/dest/test/utils.d.ts +3 -3
  44. package/dest/test/utils.d.ts.map +1 -1
  45. package/dest/test/utils.js +5 -4
  46. package/package.json +28 -28
  47. package/src/client/sequencer-client.ts +78 -21
  48. package/src/config.ts +61 -38
  49. package/src/global_variable_builder/global_builder.ts +23 -24
  50. package/src/global_variable_builder/index.ts +1 -1
  51. package/src/publisher/config.ts +112 -43
  52. package/src/publisher/sequencer-publisher-factory.ts +23 -6
  53. package/src/publisher/sequencer-publisher.ts +123 -71
  54. package/src/sequencer/checkpoint_proposal_job.ts +238 -136
  55. package/src/sequencer/checkpoint_voter.ts +1 -12
  56. package/src/sequencer/metrics.ts +92 -18
  57. package/src/sequencer/sequencer.ts +60 -49
  58. package/src/sequencer/timetable.ts +13 -12
  59. package/src/sequencer/types.ts +1 -1
  60. package/src/test/index.ts +2 -4
  61. package/src/test/mock_checkpoint_builder.ts +65 -49
  62. package/src/test/utils.ts +5 -2
@@ -4,6 +4,7 @@ import type { EpochCache } from '@aztec/epoch-cache';
4
4
  import type { L1ContractsConfig } from '@aztec/ethereum/config';
5
5
  import {
6
6
  type EmpireSlashingProposerContract,
7
+ FeeAssetPriceOracle,
7
8
  type GovernanceProposerContract,
8
9
  type IEmpireBase,
9
10
  MULTI_CALL_3_ADDRESS,
@@ -18,26 +19,29 @@ import {
18
19
  type L1BlobInputs,
19
20
  type L1TxConfig,
20
21
  type L1TxRequest,
22
+ type L1TxUtils,
21
23
  MAX_L1_TX_LIMIT,
22
24
  type TransactionStats,
23
25
  WEI_CONST,
24
26
  } from '@aztec/ethereum/l1-tx-utils';
25
- import type { L1TxUtilsWithBlobs } from '@aztec/ethereum/l1-tx-utils-with-blobs';
26
27
  import { FormattedViemError, formatViemError, mergeAbis, tryExtractEvent } from '@aztec/ethereum/utils';
27
28
  import { sumBigint } from '@aztec/foundation/bigint';
28
29
  import { toHex as toPaddedHex } from '@aztec/foundation/bigint-buffer';
29
30
  import { CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
31
+ import { trimmedBytesLength } from '@aztec/foundation/buffer';
30
32
  import { pick } from '@aztec/foundation/collection';
31
33
  import type { Fr } from '@aztec/foundation/curves/bn254';
32
34
  import { EthAddress } from '@aztec/foundation/eth-address';
33
35
  import { Signature, type ViemSignature } from '@aztec/foundation/eth-signature';
34
36
  import { type Logger, createLogger } from '@aztec/foundation/log';
37
+ import { makeBackoff, retry } from '@aztec/foundation/retry';
35
38
  import { bufferToHex } from '@aztec/foundation/string';
36
39
  import { DateProvider, Timer } from '@aztec/foundation/timer';
37
40
  import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
38
41
  import { type ProposerSlashAction, encodeSlashConsensusVotes } from '@aztec/slasher';
39
42
  import { CommitteeAttestationsAndSigners, type ValidateCheckpointResult } from '@aztec/stdlib/block';
40
43
  import type { Checkpoint } from '@aztec/stdlib/checkpoint';
44
+ import { getLastL1SlotTimestampForL2Slot, getNextL1SlotTimestamp } from '@aztec/stdlib/epoch-helpers';
41
45
  import { SlashFactoryContract } from '@aztec/stdlib/l1-contracts';
42
46
  import type { CheckpointHeader } from '@aztec/stdlib/rollup';
43
47
  import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
@@ -45,7 +49,7 @@ import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from
45
49
 
46
50
  import { type StateOverride, type TransactionReceipt, type TypedDataDefinition, encodeFunctionData, toHex } from 'viem';
47
51
 
48
- import type { PublisherConfig, TxSenderConfig } from './config.js';
52
+ import type { SequencerPublisherConfig } from './config.js';
49
53
  import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
50
54
 
51
55
  /** Arguments to the process method of the rollup contract */
@@ -60,6 +64,8 @@ type L1ProcessArgs = {
60
64
  attestationsAndSigners: CommitteeAttestationsAndSigners;
61
65
  /** Attestations and signers signature */
62
66
  attestationsAndSignersSignature: Signature;
67
+ /** The fee asset price modifier in basis points (from oracle) */
68
+ feeAssetPriceModifier: bigint;
63
69
  };
64
70
 
65
71
  export const Actions = [
@@ -112,9 +118,12 @@ export class SequencerPublisher {
112
118
  protected lastActions: Partial<Record<Action, SlotNumber>> = {};
113
119
 
114
120
  private isPayloadEmptyCache: Map<string, boolean> = new Map<string, boolean>();
121
+ private payloadProposedCache: Set<string> = new Set<string>();
115
122
 
116
123
  protected log: Logger;
117
124
  protected ethereumSlotDuration: bigint;
125
+ protected aztecSlotDuration: bigint;
126
+ private dateProvider: DateProvider;
118
127
 
119
128
  private blobClient: BlobClientInterface;
120
129
 
@@ -123,13 +132,17 @@ export class SequencerPublisher {
123
132
 
124
133
  /** L1 fee analyzer for fisherman mode */
125
134
  private l1FeeAnalyzer?: L1FeeAnalyzer;
135
+
136
+ /** Fee asset price oracle for computing price modifiers from Uniswap V4 */
137
+ private feeAssetPriceOracle: FeeAssetPriceOracle;
138
+
126
139
  // A CALL to a cold address is 2700 gas
127
140
  public static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
128
141
 
129
142
  // Gas report for VotingWithSigTest shows a max gas of 100k, but we've seen it cost 700k+ in testnet
130
143
  public static VOTE_GAS_GUESS: bigint = 800_000n;
131
144
 
132
- public l1TxUtils: L1TxUtilsWithBlobs;
145
+ public l1TxUtils: L1TxUtils;
133
146
  public rollupContract: RollupContract;
134
147
  public govProposerContract: GovernanceProposerContract;
135
148
  public slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
@@ -140,11 +153,12 @@ export class SequencerPublisher {
140
153
  protected requests: RequestWithExpiry[] = [];
141
154
 
142
155
  constructor(
143
- private config: TxSenderConfig & PublisherConfig & Pick<L1ContractsConfig, 'ethereumSlotDuration'>,
156
+ private config: Pick<SequencerPublisherConfig, 'fishermanMode'> &
157
+ Pick<L1ContractsConfig, 'ethereumSlotDuration' | 'aztecSlotDuration'> & { l1ChainId: number },
144
158
  deps: {
145
159
  telemetry?: TelemetryClient;
146
160
  blobClient: BlobClientInterface;
147
- l1TxUtils: L1TxUtilsWithBlobs;
161
+ l1TxUtils: L1TxUtils;
148
162
  rollupContract: RollupContract;
149
163
  slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
150
164
  governanceProposerContract: GovernanceProposerContract;
@@ -158,6 +172,8 @@ export class SequencerPublisher {
158
172
  ) {
159
173
  this.log = deps.log ?? createLogger('sequencer:publisher');
160
174
  this.ethereumSlotDuration = BigInt(config.ethereumSlotDuration);
175
+ this.aztecSlotDuration = BigInt(config.aztecSlotDuration);
176
+ this.dateProvider = deps.dateProvider;
161
177
  this.epochCache = deps.epochCache;
162
178
  this.lastActions = deps.lastActions;
163
179
 
@@ -188,12 +204,27 @@ export class SequencerPublisher {
188
204
  createLogger('sequencer:publisher:fee-analyzer'),
189
205
  );
190
206
  }
207
+
208
+ // Initialize fee asset price oracle
209
+ this.feeAssetPriceOracle = new FeeAssetPriceOracle(
210
+ this.l1TxUtils.client,
211
+ this.rollupContract,
212
+ createLogger('sequencer:publisher:price-oracle'),
213
+ );
191
214
  }
192
215
 
193
216
  public getRollupContract(): RollupContract {
194
217
  return this.rollupContract;
195
218
  }
196
219
 
220
+ /**
221
+ * Gets the fee asset price modifier from the oracle.
222
+ * Returns 0n if the oracle query fails.
223
+ */
224
+ public getFeeAssetPriceModifier(): Promise<bigint> {
225
+ return this.feeAssetPriceOracle.computePriceModifier();
226
+ }
227
+
197
228
  public getSenderAddress() {
198
229
  return this.l1TxUtils.getSenderAddress();
199
230
  }
@@ -331,8 +362,8 @@ export class SequencerPublisher {
331
362
  // @note - we can only have one blob config per bundle
332
363
  // find requests with gas and blob configs
333
364
  // See https://github.com/AztecProtocol/aztec-packages/issues/11513
334
- const gasConfigs = requestsToProcess.filter(request => request.gasConfig).map(request => request.gasConfig);
335
- 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);
336
367
 
337
368
  if (blobConfigs.length > 1) {
338
369
  throw new Error('Multiple blob configs found');
@@ -400,7 +431,16 @@ export class SequencerPublisher {
400
431
  this.log.error(`Failed to publish bundled transactions (${actionsListStr})`, result);
401
432
  return { failedActions: requests.map(r => r.action) };
402
433
  } else {
403
- 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
+ });
404
444
  const successfulActions: Action[] = [];
405
445
  const failedActions: Action[] = [];
406
446
  for (const request of requests) {
@@ -415,20 +455,24 @@ export class SequencerPublisher {
415
455
  }
416
456
 
417
457
  /**
418
- * @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
419
459
  * @param tipArchive - The archive to check
420
460
  * @returns The slot and block number if it is possible to propose, undefined otherwise
421
461
  */
422
- public canProposeAtNextEthBlock(
462
+ public canProposeAt(
423
463
  tipArchive: Fr,
424
464
  msgSender: EthAddress,
425
- opts: { forcePendingCheckpointNumber?: CheckpointNumber } = {},
465
+ opts: { forcePendingCheckpointNumber?: CheckpointNumber; pipelined?: boolean } = {},
426
466
  ) {
427
467
  // TODO: #14291 - should loop through multiple keys to check if any of them can propose
428
468
  const ignoredErrors = ['SlotAlreadyInChain', 'InvalidProposer', 'InvalidArchive'];
429
469
 
470
+ const pipelined = opts.pipelined ?? false;
471
+ const slotOffset = pipelined ? this.aztecSlotDuration : 0n;
472
+ const nextL1SlotTs = this.getNextL1SlotTimestamp() + slotOffset;
473
+
430
474
  return this.rollupContract
431
- .canProposeAtNextEthBlock(tipArchive.toBuffer(), msgSender.toString(), Number(this.ethereumSlotDuration), {
475
+ .canProposeAt(tipArchive.toBuffer(), msgSender.toString(), nextL1SlotTs, {
432
476
  forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
433
477
  })
434
478
  .catch(err => {
@@ -465,7 +509,7 @@ export class SequencerPublisher {
465
509
  flags,
466
510
  ] as const;
467
511
 
468
- const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
512
+ const ts = this.getSimulationTimestamp(header.slotNumber);
469
513
  const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(
470
514
  opts?.forcePendingCheckpointNumber,
471
515
  );
@@ -488,7 +532,7 @@ export class SequencerPublisher {
488
532
  data: encodeFunctionData({ abi: RollupAbi, functionName: 'validateHeaderWithAttestations', args }),
489
533
  from: MULTI_CALL_3_ADDRESS,
490
534
  },
491
- { time: ts + 1n },
535
+ { time: ts },
492
536
  stateOverrides,
493
537
  );
494
538
  this.log.debug(`Simulated validateHeader`);
@@ -615,26 +659,9 @@ export class SequencerPublisher {
615
659
  attestationsAndSigners: CommitteeAttestationsAndSigners,
616
660
  attestationsAndSignersSignature: Signature,
617
661
  options: { forcePendingCheckpointNumber?: CheckpointNumber },
618
- ): Promise<bigint> {
619
- const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
620
-
621
- // TODO(palla/mbps): This should not be needed, there's no flow where we propose with zero attestations. Or is there?
622
- // If we have no attestations, we still need to provide the empty attestations
623
- // so that the committee is recalculated correctly
624
- // const ignoreSignatures = attestationsAndSigners.attestations.length === 0;
625
- // if (ignoreSignatures) {
626
- // const { committee } = await this.epochCache.getCommittee(block.header.globalVariables.slotNumber);
627
- // if (!committee) {
628
- // this.log.warn(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
629
- // throw new Error(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
630
- // }
631
- // attestationsAndSigners.attestations = committee.map(committeeMember =>
632
- // CommitteeAttestation.fromAddress(committeeMember),
633
- // );
634
- // }
635
-
662
+ ): Promise<void> {
636
663
  const blobFields = checkpoint.toBlobFields();
637
- const blobs = getBlobsPerL1Block(blobFields);
664
+ const blobs = await getBlobsPerL1Block(blobFields);
638
665
  const blobInput = getPrefixedEthBlobCommitments(blobs);
639
666
 
640
667
  const args = [
@@ -642,7 +669,7 @@ export class SequencerPublisher {
642
669
  header: checkpoint.header.toViem(),
643
670
  archive: toHex(checkpoint.archive.root.toBuffer()),
644
671
  oracleInput: {
645
- feeAssetPriceModifier: 0n,
672
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
646
673
  },
647
674
  },
648
675
  attestationsAndSigners.getPackedAttestations(),
@@ -651,13 +678,11 @@ export class SequencerPublisher {
651
678
  blobInput,
652
679
  ] as const;
653
680
 
654
- await this.simulateProposeTx(args, ts, options);
655
- return ts;
681
+ await this.simulateProposeTx(args, options);
656
682
  }
657
683
 
658
684
  private async enqueueCastSignalHelper(
659
685
  slotNumber: SlotNumber,
660
- timestamp: bigint,
661
686
  signalType: GovernanceSignalAction,
662
687
  payload: EthAddress,
663
688
  base: IEmpireBase,
@@ -691,6 +716,32 @@ export class SequencerPublisher {
691
716
  return false;
692
717
  }
693
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
+
694
745
  const cachedLastVote = this.lastActions[signalType];
695
746
  this.lastActions[signalType] = slotNumber;
696
747
  const action = signalType;
@@ -709,11 +760,16 @@ export class SequencerPublisher {
709
760
  lastValidL2Slot: slotNumber,
710
761
  });
711
762
 
763
+ const timestamp = this.getSimulationTimestamp(slotNumber);
764
+
712
765
  try {
713
766
  await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
714
767
  this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
715
768
  } catch (err) {
716
- 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
+ });
717
773
  // Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
718
774
  }
719
775
 
@@ -764,19 +820,16 @@ export class SequencerPublisher {
764
820
  /**
765
821
  * Enqueues a governance castSignal transaction to cast a signal for a given slot number.
766
822
  * @param slotNumber - The slot number to cast a signal for.
767
- * @param timestamp - The timestamp of the slot to cast a signal for.
768
823
  * @returns True if the signal was successfully enqueued, false otherwise.
769
824
  */
770
825
  public enqueueGovernanceCastSignal(
771
826
  governancePayload: EthAddress,
772
827
  slotNumber: SlotNumber,
773
- timestamp: bigint,
774
828
  signerAddress: EthAddress,
775
829
  signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
776
830
  ): Promise<boolean> {
777
831
  return this.enqueueCastSignalHelper(
778
832
  slotNumber,
779
- timestamp,
780
833
  'governance-signal',
781
834
  governancePayload,
782
835
  this.govProposerContract,
@@ -789,7 +842,6 @@ export class SequencerPublisher {
789
842
  public async enqueueSlashingActions(
790
843
  actions: ProposerSlashAction[],
791
844
  slotNumber: SlotNumber,
792
- timestamp: bigint,
793
845
  signerAddress: EthAddress,
794
846
  signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
795
847
  ): Promise<boolean> {
@@ -810,7 +862,6 @@ export class SequencerPublisher {
810
862
  });
811
863
  await this.enqueueCastSignalHelper(
812
864
  slotNumber,
813
- timestamp,
814
865
  'empire-slashing-signal',
815
866
  action.payload,
816
867
  this.slashingProposerContract,
@@ -829,7 +880,6 @@ export class SequencerPublisher {
829
880
  (receipt: TransactionReceipt) =>
830
881
  !!this.slashFactoryContract.tryExtractSlashPayloadCreatedEvent(receipt.logs),
831
882
  slotNumber,
832
- timestamp,
833
883
  );
834
884
  break;
835
885
  }
@@ -847,7 +897,6 @@ export class SequencerPublisher {
847
897
  request,
848
898
  (receipt: TransactionReceipt) => !!empireSlashingProposer.tryExtractPayloadSubmittedEvent(receipt.logs),
849
899
  slotNumber,
850
- timestamp,
851
900
  );
852
901
  break;
853
902
  }
@@ -871,7 +920,6 @@ export class SequencerPublisher {
871
920
  request,
872
921
  (receipt: TransactionReceipt) => !!tallySlashingProposer.tryExtractVoteCastEvent(receipt.logs),
873
922
  slotNumber,
874
- timestamp,
875
923
  );
876
924
  break;
877
925
  }
@@ -893,7 +941,6 @@ export class SequencerPublisher {
893
941
  request,
894
942
  (receipt: TransactionReceipt) => !!tallySlashingProposer.tryExtractRoundExecutedEvent(receipt.logs),
895
943
  slotNumber,
896
- timestamp,
897
944
  );
898
945
  break;
899
946
  }
@@ -918,25 +965,24 @@ export class SequencerPublisher {
918
965
  const checkpointHeader = checkpoint.header;
919
966
 
920
967
  const blobFields = checkpoint.toBlobFields();
921
- const blobs = getBlobsPerL1Block(blobFields);
968
+ const blobs = await getBlobsPerL1Block(blobFields);
922
969
 
923
- const proposeTxArgs = {
970
+ const proposeTxArgs: L1ProcessArgs = {
924
971
  header: checkpointHeader,
925
972
  archive: checkpoint.archive.root.toBuffer(),
926
973
  blobs,
927
974
  attestationsAndSigners,
928
975
  attestationsAndSignersSignature,
976
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
929
977
  };
930
978
 
931
- let ts: bigint;
932
-
933
979
  try {
934
980
  // @note This will make sure that we are passing the checks for our header ASSUMING that the data is also made available
935
981
  // This means that we can avoid the simulation issues in later checks.
936
982
  // By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which
937
983
  // make time consistency checks break.
938
984
  // TODO(palla): Check whether we're validating twice, once here and once within addProposeTx, since we call simulateProposeTx in both places.
939
- ts = await this.validateCheckpointForSubmission(
985
+ await this.validateCheckpointForSubmission(
940
986
  checkpoint,
941
987
  attestationsAndSigners,
942
988
  attestationsAndSignersSignature,
@@ -952,7 +998,7 @@ export class SequencerPublisher {
952
998
  }
953
999
 
954
1000
  this.log.verbose(`Enqueuing checkpoint propose transaction`, { ...checkpoint.toCheckpointInfo(), ...opts });
955
- await this.addProposeTx(checkpoint, proposeTxArgs, opts, ts);
1001
+ await this.addProposeTx(checkpoint, proposeTxArgs, opts);
956
1002
  }
957
1003
 
958
1004
  public enqueueInvalidateCheckpoint(
@@ -995,8 +1041,8 @@ export class SequencerPublisher {
995
1041
  request: L1TxRequest,
996
1042
  checkSuccess: (receipt: TransactionReceipt) => boolean | undefined,
997
1043
  slotNumber: SlotNumber,
998
- timestamp: bigint,
999
1044
  ) {
1045
+ const timestamp = this.getSimulationTimestamp(slotNumber);
1000
1046
  const logData = { slotNumber, timestamp, gasLimit: undefined as bigint | undefined };
1001
1047
  if (this.lastActions[action] && this.lastActions[action] === slotNumber) {
1002
1048
  this.log.debug(`Skipping duplicate action ${action} for slot ${slotNumber}`);
@@ -1010,8 +1056,9 @@ export class SequencerPublisher {
1010
1056
 
1011
1057
  let gasUsed: bigint;
1012
1058
  const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
1059
+
1013
1060
  try {
1014
- ({ 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));
1015
1062
  this.log.verbose(`Simulation for ${action} succeeded`, { ...logData, request, gasUsed });
1016
1063
  } catch (err) {
1017
1064
  const viemError = formatViemError(err, simulateAbi);
@@ -1067,7 +1114,6 @@ export class SequencerPublisher {
1067
1114
 
1068
1115
  private async prepareProposeTx(
1069
1116
  encodedData: L1ProcessArgs,
1070
- timestamp: bigint,
1071
1117
  options: { forcePendingCheckpointNumber?: CheckpointNumber },
1072
1118
  ) {
1073
1119
  const kzg = Blob.getViemKzgInstance();
@@ -1113,8 +1159,7 @@ export class SequencerPublisher {
1113
1159
  header: encodedData.header.toViem(),
1114
1160
  archive: toHex(encodedData.archive),
1115
1161
  oracleInput: {
1116
- // We are currently not modifying these. See #9963
1117
- feeAssetPriceModifier: 0n,
1162
+ feeAssetPriceModifier: encodedData.feeAssetPriceModifier,
1118
1163
  },
1119
1164
  },
1120
1165
  encodedData.attestationsAndSigners.getPackedAttestations(),
@@ -1123,7 +1168,7 @@ export class SequencerPublisher {
1123
1168
  blobInput,
1124
1169
  ] as const;
1125
1170
 
1126
- const { rollupData, simulationResult } = await this.simulateProposeTx(args, timestamp, options);
1171
+ const { rollupData, simulationResult } = await this.simulateProposeTx(args, options);
1127
1172
 
1128
1173
  return { args, blobEvaluationGas, rollupData, simulationResult };
1129
1174
  }
@@ -1131,7 +1176,6 @@ export class SequencerPublisher {
1131
1176
  /**
1132
1177
  * Simulates the propose tx with eth_simulateV1
1133
1178
  * @param args - The propose tx args
1134
- * @param timestamp - The timestamp to simulate proposal at
1135
1179
  * @returns The simulation result
1136
1180
  */
1137
1181
  private async simulateProposeTx(
@@ -1140,7 +1184,7 @@ export class SequencerPublisher {
1140
1184
  readonly header: ViemHeader;
1141
1185
  readonly archive: `0x${string}`;
1142
1186
  readonly oracleInput: {
1143
- readonly feeAssetPriceModifier: 0n;
1187
+ readonly feeAssetPriceModifier: bigint;
1144
1188
  };
1145
1189
  },
1146
1190
  ViemCommitteeAttestations,
@@ -1148,7 +1192,6 @@ export class SequencerPublisher {
1148
1192
  ViemSignature,
1149
1193
  `0x${string}`,
1150
1194
  ],
1151
- timestamp: bigint,
1152
1195
  options: { forcePendingCheckpointNumber?: CheckpointNumber },
1153
1196
  ) {
1154
1197
  const rollupData = encodeFunctionData({
@@ -1182,6 +1225,8 @@ export class SequencerPublisher {
1182
1225
  });
1183
1226
  }
1184
1227
 
1228
+ const simTs = this.getSimulationTimestamp(SlotNumber.fromBigInt(args[0].header.slotNumber));
1229
+
1185
1230
  const simulationResult = await this.l1TxUtils
1186
1231
  .simulate(
1187
1232
  {
@@ -1191,8 +1236,7 @@ export class SequencerPublisher {
1191
1236
  ...(this.proposerAddressForSimulation && { from: this.proposerAddressForSimulation.toString() }),
1192
1237
  },
1193
1238
  {
1194
- // @note we add 1n to the timestamp because geth implementation doesn't like simulation timestamp to be equal to the current block timestamp
1195
- time: timestamp + 1n,
1239
+ time: simTs,
1196
1240
  // @note reth should have a 30m gas limit per block but throws errors that this tx is beyond limit so we increase here
1197
1241
  gasLimit: MAX_L1_TX_LIMIT * 2n,
1198
1242
  },
@@ -1214,7 +1258,7 @@ export class SequencerPublisher {
1214
1258
  logs: [],
1215
1259
  };
1216
1260
  }
1217
- this.log.error(`Failed to simulate propose tx`, viemError);
1261
+ this.log.error(`Failed to simulate propose tx`, viemError, { simulationTimestamp: simTs });
1218
1262
  throw err;
1219
1263
  });
1220
1264
 
@@ -1225,16 +1269,11 @@ export class SequencerPublisher {
1225
1269
  checkpoint: Checkpoint,
1226
1270
  encodedData: L1ProcessArgs,
1227
1271
  opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
1228
- timestamp: bigint,
1229
1272
  ): Promise<void> {
1230
1273
  const slot = checkpoint.header.slotNumber;
1231
1274
  const timer = new Timer();
1232
1275
  const kzg = Blob.getViemKzgInstance();
1233
- const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(
1234
- encodedData,
1235
- timestamp,
1236
- opts,
1237
- );
1276
+ const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(encodedData, opts);
1238
1277
  const startBlock = await this.l1TxUtils.getBlockNumber();
1239
1278
  const gasLimit = this.l1TxUtils.bumpGasLimit(
1240
1279
  BigInt(Math.ceil((Number(simulationResult.gasUsed) * 64) / 63)) +
@@ -1310,4 +1349,17 @@ export class SequencerPublisher {
1310
1349
  },
1311
1350
  });
1312
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
+ }
1313
1365
  }