@aztec/sequencer-client 0.0.1-commit.54489865 → 0.0.1-commit.5914bae

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 -14
  19. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  20. package/dest/publisher/sequencer-publisher.js +144 -74
  21. package/dest/sequencer/checkpoint_proposal_job.d.ts +30 -7
  22. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  23. package/dest/sequencer/checkpoint_proposal_job.js +191 -113
  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 +12 -12
  41. package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
  42. package/dest/test/mock_checkpoint_builder.js +47 -36
  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 +155 -87
  54. package/src/sequencer/checkpoint_proposal_job.ts +285 -159
  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 +67 -53
  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,25 +19,29 @@ import {
18
19
  type L1BlobInputs,
19
20
  type L1TxConfig,
20
21
  type L1TxRequest,
22
+ type L1TxUtils,
23
+ MAX_L1_TX_LIMIT,
21
24
  type TransactionStats,
22
25
  WEI_CONST,
23
26
  } from '@aztec/ethereum/l1-tx-utils';
24
- import type { L1TxUtilsWithBlobs } from '@aztec/ethereum/l1-tx-utils-with-blobs';
25
- import { FormattedViemError, formatViemError, tryExtractEvent } from '@aztec/ethereum/utils';
27
+ import { FormattedViemError, formatViemError, mergeAbis, tryExtractEvent } from '@aztec/ethereum/utils';
26
28
  import { sumBigint } from '@aztec/foundation/bigint';
27
29
  import { toHex as toPaddedHex } from '@aztec/foundation/bigint-buffer';
28
30
  import { CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
31
+ import { trimmedBytesLength } from '@aztec/foundation/buffer';
29
32
  import { pick } from '@aztec/foundation/collection';
30
33
  import type { Fr } from '@aztec/foundation/curves/bn254';
31
34
  import { EthAddress } from '@aztec/foundation/eth-address';
32
35
  import { Signature, type ViemSignature } from '@aztec/foundation/eth-signature';
33
36
  import { type Logger, createLogger } from '@aztec/foundation/log';
37
+ import { makeBackoff, retry } from '@aztec/foundation/retry';
34
38
  import { bufferToHex } from '@aztec/foundation/string';
35
39
  import { DateProvider, Timer } from '@aztec/foundation/timer';
36
40
  import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
37
41
  import { type ProposerSlashAction, encodeSlashConsensusVotes } from '@aztec/slasher';
38
42
  import { CommitteeAttestationsAndSigners, type ValidateCheckpointResult } from '@aztec/stdlib/block';
39
43
  import type { Checkpoint } from '@aztec/stdlib/checkpoint';
44
+ import { getLastL1SlotTimestampForL2Slot, getNextL1SlotTimestamp } from '@aztec/stdlib/epoch-helpers';
40
45
  import { SlashFactoryContract } from '@aztec/stdlib/l1-contracts';
41
46
  import type { CheckpointHeader } from '@aztec/stdlib/rollup';
42
47
  import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
@@ -44,7 +49,7 @@ import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from
44
49
 
45
50
  import { type StateOverride, type TransactionReceipt, type TypedDataDefinition, encodeFunctionData, toHex } from 'viem';
46
51
 
47
- import type { PublisherConfig, TxSenderConfig } from './config.js';
52
+ import type { SequencerPublisherConfig } from './config.js';
48
53
  import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
49
54
 
50
55
  /** Arguments to the process method of the rollup contract */
@@ -59,6 +64,8 @@ type L1ProcessArgs = {
59
64
  attestationsAndSigners: CommitteeAttestationsAndSigners;
60
65
  /** Attestations and signers signature */
61
66
  attestationsAndSignersSignature: Signature;
67
+ /** The fee asset price modifier in basis points (from oracle) */
68
+ feeAssetPriceModifier: bigint;
62
69
  };
63
70
 
64
71
  export const Actions = [
@@ -111,9 +118,12 @@ export class SequencerPublisher {
111
118
  protected lastActions: Partial<Record<Action, SlotNumber>> = {};
112
119
 
113
120
  private isPayloadEmptyCache: Map<string, boolean> = new Map<string, boolean>();
121
+ private payloadProposedCache: Set<string> = new Set<string>();
114
122
 
115
123
  protected log: Logger;
116
124
  protected ethereumSlotDuration: bigint;
125
+ protected aztecSlotDuration: bigint;
126
+ private dateProvider: DateProvider;
117
127
 
118
128
  private blobClient: BlobClientInterface;
119
129
 
@@ -122,10 +132,9 @@ export class SequencerPublisher {
122
132
 
123
133
  /** L1 fee analyzer for fisherman mode */
124
134
  private l1FeeAnalyzer?: L1FeeAnalyzer;
125
- // @note - with blobs, the below estimate seems too large.
126
- // Total used for full block from int_l1_pub e2e test: 1m (of which 86k is 1x blob)
127
- // Total used for emptier block from above test: 429k (of which 84k is 1x blob)
128
- public static PROPOSE_GAS_GUESS: bigint = 12_000_000n;
135
+
136
+ /** Fee asset price oracle for computing price modifiers from Uniswap V4 */
137
+ private feeAssetPriceOracle: FeeAssetPriceOracle;
129
138
 
130
139
  // A CALL to a cold address is 2700 gas
131
140
  public static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
@@ -133,7 +142,7 @@ export class SequencerPublisher {
133
142
  // Gas report for VotingWithSigTest shows a max gas of 100k, but we've seen it cost 700k+ in testnet
134
143
  public static VOTE_GAS_GUESS: bigint = 800_000n;
135
144
 
136
- public l1TxUtils: L1TxUtilsWithBlobs;
145
+ public l1TxUtils: L1TxUtils;
137
146
  public rollupContract: RollupContract;
138
147
  public govProposerContract: GovernanceProposerContract;
139
148
  public slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
@@ -144,11 +153,12 @@ export class SequencerPublisher {
144
153
  protected requests: RequestWithExpiry[] = [];
145
154
 
146
155
  constructor(
147
- private config: TxSenderConfig & PublisherConfig & Pick<L1ContractsConfig, 'ethereumSlotDuration'>,
156
+ private config: Pick<SequencerPublisherConfig, 'fishermanMode'> &
157
+ Pick<L1ContractsConfig, 'ethereumSlotDuration' | 'aztecSlotDuration'> & { l1ChainId: number },
148
158
  deps: {
149
159
  telemetry?: TelemetryClient;
150
160
  blobClient: BlobClientInterface;
151
- l1TxUtils: L1TxUtilsWithBlobs;
161
+ l1TxUtils: L1TxUtils;
152
162
  rollupContract: RollupContract;
153
163
  slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
154
164
  governanceProposerContract: GovernanceProposerContract;
@@ -162,6 +172,8 @@ export class SequencerPublisher {
162
172
  ) {
163
173
  this.log = deps.log ?? createLogger('sequencer:publisher');
164
174
  this.ethereumSlotDuration = BigInt(config.ethereumSlotDuration);
175
+ this.aztecSlotDuration = BigInt(config.aztecSlotDuration);
176
+ this.dateProvider = deps.dateProvider;
165
177
  this.epochCache = deps.epochCache;
166
178
  this.lastActions = deps.lastActions;
167
179
 
@@ -192,12 +204,27 @@ export class SequencerPublisher {
192
204
  createLogger('sequencer:publisher:fee-analyzer'),
193
205
  );
194
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
+ );
195
214
  }
196
215
 
197
216
  public getRollupContract(): RollupContract {
198
217
  return this.rollupContract;
199
218
  }
200
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
+
201
228
  public getSenderAddress() {
202
229
  return this.l1TxUtils.getSenderAddress();
203
230
  }
@@ -273,7 +300,7 @@ export class SequencerPublisher {
273
300
  // Start the analysis
274
301
  const analysisId = await this.l1FeeAnalyzer.startAnalysis(
275
302
  l2SlotNumber,
276
- gasLimit > 0n ? gasLimit : SequencerPublisher.PROPOSE_GAS_GUESS,
303
+ gasLimit > 0n ? gasLimit : MAX_L1_TX_LIMIT,
277
304
  l1Requests,
278
305
  blobConfig,
279
306
  onComplete,
@@ -335,8 +362,8 @@ export class SequencerPublisher {
335
362
  // @note - we can only have one blob config per bundle
336
363
  // find requests with gas and blob configs
337
364
  // See https://github.com/AztecProtocol/aztec-packages/issues/11513
338
- const gasConfigs = requestsToProcess.filter(request => request.gasConfig).map(request => request.gasConfig);
339
- 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);
340
367
 
341
368
  if (blobConfigs.length > 1) {
342
369
  throw new Error('Multiple blob configs found');
@@ -346,7 +373,16 @@ export class SequencerPublisher {
346
373
 
347
374
  // Merge gasConfigs. Yields the sum of gasLimits, and the earliest txTimeoutAt, or undefined if no gasConfig sets them.
348
375
  const gasLimits = gasConfigs.map(g => g?.gasLimit).filter((g): g is bigint => g !== undefined);
349
- const gasLimit = gasLimits.length > 0 ? sumBigint(gasLimits) : undefined; // sum
376
+ let gasLimit = gasLimits.length > 0 ? sumBigint(gasLimits) : undefined; // sum
377
+ // Cap at L1 block gas limit so the node accepts the tx ("gas limit too high" otherwise).
378
+ const maxGas = MAX_L1_TX_LIMIT;
379
+ if (gasLimit !== undefined && gasLimit > maxGas) {
380
+ this.log.debug('Capping bundled tx gas limit to L1 max', {
381
+ requested: gasLimit,
382
+ capped: maxGas,
383
+ });
384
+ gasLimit = maxGas;
385
+ }
350
386
  const txTimeoutAts = gasConfigs.map(g => g?.txTimeoutAt).filter((g): g is Date => g !== undefined);
351
387
  const txTimeoutAt = txTimeoutAts.length > 0 ? new Date(Math.min(...txTimeoutAts.map(g => g.getTime()))) : undefined; // earliest
352
388
  const txConfig: RequestWithExpiry['gasConfig'] = { gasLimit, txTimeoutAt };
@@ -395,7 +431,16 @@ export class SequencerPublisher {
395
431
  this.log.error(`Failed to publish bundled transactions (${actionsListStr})`, result);
396
432
  return { failedActions: requests.map(r => r.action) };
397
433
  } else {
398
- 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
+ });
399
444
  const successfulActions: Action[] = [];
400
445
  const failedActions: Action[] = [];
401
446
  for (const request of requests) {
@@ -410,20 +455,24 @@ export class SequencerPublisher {
410
455
  }
411
456
 
412
457
  /**
413
- * @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
414
459
  * @param tipArchive - The archive to check
415
460
  * @returns The slot and block number if it is possible to propose, undefined otherwise
416
461
  */
417
- public canProposeAtNextEthBlock(
462
+ public canProposeAt(
418
463
  tipArchive: Fr,
419
464
  msgSender: EthAddress,
420
- opts: { forcePendingCheckpointNumber?: CheckpointNumber } = {},
465
+ opts: { forcePendingCheckpointNumber?: CheckpointNumber; pipelined?: boolean } = {},
421
466
  ) {
422
467
  // TODO: #14291 - should loop through multiple keys to check if any of them can propose
423
468
  const ignoredErrors = ['SlotAlreadyInChain', 'InvalidProposer', 'InvalidArchive'];
424
469
 
470
+ const pipelined = opts.pipelined ?? false;
471
+ const slotOffset = pipelined ? this.aztecSlotDuration : 0n;
472
+ const nextL1SlotTs = this.getNextL1SlotTimestamp() + slotOffset;
473
+
425
474
  return this.rollupContract
426
- .canProposeAtNextEthBlock(tipArchive.toBuffer(), msgSender.toString(), Number(this.ethereumSlotDuration), {
475
+ .canProposeAt(tipArchive.toBuffer(), msgSender.toString(), nextL1SlotTs, {
427
476
  forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
428
477
  })
429
478
  .catch(err => {
@@ -460,7 +509,7 @@ export class SequencerPublisher {
460
509
  flags,
461
510
  ] as const;
462
511
 
463
- const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
512
+ const ts = this.getSimulationTimestamp(header.slotNumber);
464
513
  const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(
465
514
  opts?.forcePendingCheckpointNumber,
466
515
  );
@@ -483,7 +532,7 @@ export class SequencerPublisher {
483
532
  data: encodeFunctionData({ abi: RollupAbi, functionName: 'validateHeaderWithAttestations', args }),
484
533
  from: MULTI_CALL_3_ADDRESS,
485
534
  },
486
- { time: ts + 1n },
535
+ { time: ts },
487
536
  stateOverrides,
488
537
  );
489
538
  this.log.debug(`Simulated validateHeader`);
@@ -517,7 +566,12 @@ export class SequencerPublisher {
517
566
  this.log.debug(`Simulating invalidate checkpoint ${checkpointNumber}`, { ...logData, request });
518
567
 
519
568
  try {
520
- const { gasUsed } = await this.l1TxUtils.simulate(request, undefined, undefined, ErrorsAbi);
569
+ const { gasUsed } = await this.l1TxUtils.simulate(
570
+ request,
571
+ undefined,
572
+ undefined,
573
+ mergeAbis([request.abi ?? [], ErrorsAbi]),
574
+ );
521
575
  this.log.verbose(`Simulation for invalidate checkpoint ${checkpointNumber} succeeded`, {
522
576
  ...logData,
523
577
  request,
@@ -536,7 +590,7 @@ export class SequencerPublisher {
536
590
 
537
591
  // If the error is due to the checkpoint not being in the pending chain, and it was indeed removed by someone else,
538
592
  // we can safely ignore it and return undefined so we go ahead with checkpoint building.
539
- if (viemError.message?.includes('Rollup__BlockNotInPendingChain')) {
593
+ if (viemError.message?.includes('Rollup__CheckpointNotInPendingChain')) {
540
594
  this.log.verbose(
541
595
  `Simulation for invalidate checkpoint ${checkpointNumber} failed due to checkpoint not being in pending chain`,
542
596
  { ...logData, request, error: viemError.message },
@@ -605,26 +659,9 @@ export class SequencerPublisher {
605
659
  attestationsAndSigners: CommitteeAttestationsAndSigners,
606
660
  attestationsAndSignersSignature: Signature,
607
661
  options: { forcePendingCheckpointNumber?: CheckpointNumber },
608
- ): Promise<bigint> {
609
- const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
610
-
611
- // TODO(palla/mbps): This should not be needed, there's no flow where we propose with zero attestations. Or is there?
612
- // If we have no attestations, we still need to provide the empty attestations
613
- // so that the committee is recalculated correctly
614
- // const ignoreSignatures = attestationsAndSigners.attestations.length === 0;
615
- // if (ignoreSignatures) {
616
- // const { committee } = await this.epochCache.getCommittee(block.header.globalVariables.slotNumber);
617
- // if (!committee) {
618
- // this.log.warn(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
619
- // throw new Error(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
620
- // }
621
- // attestationsAndSigners.attestations = committee.map(committeeMember =>
622
- // CommitteeAttestation.fromAddress(committeeMember),
623
- // );
624
- // }
625
-
662
+ ): Promise<void> {
626
663
  const blobFields = checkpoint.toBlobFields();
627
- const blobs = getBlobsPerL1Block(blobFields);
664
+ const blobs = await getBlobsPerL1Block(blobFields);
628
665
  const blobInput = getPrefixedEthBlobCommitments(blobs);
629
666
 
630
667
  const args = [
@@ -632,7 +669,7 @@ export class SequencerPublisher {
632
669
  header: checkpoint.header.toViem(),
633
670
  archive: toHex(checkpoint.archive.root.toBuffer()),
634
671
  oracleInput: {
635
- feeAssetPriceModifier: 0n,
672
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
636
673
  },
637
674
  },
638
675
  attestationsAndSigners.getPackedAttestations(),
@@ -641,13 +678,11 @@ export class SequencerPublisher {
641
678
  blobInput,
642
679
  ] as const;
643
680
 
644
- await this.simulateProposeTx(args, ts, options);
645
- return ts;
681
+ await this.simulateProposeTx(args, options);
646
682
  }
647
683
 
648
684
  private async enqueueCastSignalHelper(
649
685
  slotNumber: SlotNumber,
650
- timestamp: bigint,
651
686
  signalType: GovernanceSignalAction,
652
687
  payload: EthAddress,
653
688
  base: IEmpireBase,
@@ -681,6 +716,32 @@ export class SequencerPublisher {
681
716
  return false;
682
717
  }
683
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
+
684
745
  const cachedLastVote = this.lastActions[signalType];
685
746
  this.lastActions[signalType] = slotNumber;
686
747
  const action = signalType;
@@ -699,11 +760,16 @@ export class SequencerPublisher {
699
760
  lastValidL2Slot: slotNumber,
700
761
  });
701
762
 
763
+ const timestamp = this.getSimulationTimestamp(slotNumber);
764
+
702
765
  try {
703
- await this.l1TxUtils.simulate(request, { time: timestamp }, [], ErrorsAbi);
766
+ await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
704
767
  this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
705
768
  } catch (err) {
706
- 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
+ });
707
773
  // Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
708
774
  }
709
775
 
@@ -754,19 +820,16 @@ export class SequencerPublisher {
754
820
  /**
755
821
  * Enqueues a governance castSignal transaction to cast a signal for a given slot number.
756
822
  * @param slotNumber - The slot number to cast a signal for.
757
- * @param timestamp - The timestamp of the slot to cast a signal for.
758
823
  * @returns True if the signal was successfully enqueued, false otherwise.
759
824
  */
760
825
  public enqueueGovernanceCastSignal(
761
826
  governancePayload: EthAddress,
762
827
  slotNumber: SlotNumber,
763
- timestamp: bigint,
764
828
  signerAddress: EthAddress,
765
829
  signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
766
830
  ): Promise<boolean> {
767
831
  return this.enqueueCastSignalHelper(
768
832
  slotNumber,
769
- timestamp,
770
833
  'governance-signal',
771
834
  governancePayload,
772
835
  this.govProposerContract,
@@ -779,7 +842,6 @@ export class SequencerPublisher {
779
842
  public async enqueueSlashingActions(
780
843
  actions: ProposerSlashAction[],
781
844
  slotNumber: SlotNumber,
782
- timestamp: bigint,
783
845
  signerAddress: EthAddress,
784
846
  signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
785
847
  ): Promise<boolean> {
@@ -800,7 +862,6 @@ export class SequencerPublisher {
800
862
  });
801
863
  await this.enqueueCastSignalHelper(
802
864
  slotNumber,
803
- timestamp,
804
865
  'empire-slashing-signal',
805
866
  action.payload,
806
867
  this.slashingProposerContract,
@@ -819,7 +880,6 @@ export class SequencerPublisher {
819
880
  (receipt: TransactionReceipt) =>
820
881
  !!this.slashFactoryContract.tryExtractSlashPayloadCreatedEvent(receipt.logs),
821
882
  slotNumber,
822
- timestamp,
823
883
  );
824
884
  break;
825
885
  }
@@ -837,7 +897,6 @@ export class SequencerPublisher {
837
897
  request,
838
898
  (receipt: TransactionReceipt) => !!empireSlashingProposer.tryExtractPayloadSubmittedEvent(receipt.logs),
839
899
  slotNumber,
840
- timestamp,
841
900
  );
842
901
  break;
843
902
  }
@@ -861,7 +920,6 @@ export class SequencerPublisher {
861
920
  request,
862
921
  (receipt: TransactionReceipt) => !!tallySlashingProposer.tryExtractVoteCastEvent(receipt.logs),
863
922
  slotNumber,
864
- timestamp,
865
923
  );
866
924
  break;
867
925
  }
@@ -883,7 +941,6 @@ export class SequencerPublisher {
883
941
  request,
884
942
  (receipt: TransactionReceipt) => !!tallySlashingProposer.tryExtractRoundExecutedEvent(receipt.logs),
885
943
  slotNumber,
886
- timestamp,
887
944
  );
888
945
  break;
889
946
  }
@@ -908,25 +965,24 @@ export class SequencerPublisher {
908
965
  const checkpointHeader = checkpoint.header;
909
966
 
910
967
  const blobFields = checkpoint.toBlobFields();
911
- const blobs = getBlobsPerL1Block(blobFields);
968
+ const blobs = await getBlobsPerL1Block(blobFields);
912
969
 
913
- const proposeTxArgs = {
970
+ const proposeTxArgs: L1ProcessArgs = {
914
971
  header: checkpointHeader,
915
972
  archive: checkpoint.archive.root.toBuffer(),
916
973
  blobs,
917
974
  attestationsAndSigners,
918
975
  attestationsAndSignersSignature,
976
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
919
977
  };
920
978
 
921
- let ts: bigint;
922
-
923
979
  try {
924
980
  // @note This will make sure that we are passing the checks for our header ASSUMING that the data is also made available
925
981
  // This means that we can avoid the simulation issues in later checks.
926
982
  // By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which
927
983
  // make time consistency checks break.
928
984
  // TODO(palla): Check whether we're validating twice, once here and once within addProposeTx, since we call simulateProposeTx in both places.
929
- ts = await this.validateCheckpointForSubmission(
985
+ await this.validateCheckpointForSubmission(
930
986
  checkpoint,
931
987
  attestationsAndSigners,
932
988
  attestationsAndSignersSignature,
@@ -942,7 +998,7 @@ export class SequencerPublisher {
942
998
  }
943
999
 
944
1000
  this.log.verbose(`Enqueuing checkpoint propose transaction`, { ...checkpoint.toCheckpointInfo(), ...opts });
945
- await this.addProposeTx(checkpoint, proposeTxArgs, opts, ts);
1001
+ await this.addProposeTx(checkpoint, proposeTxArgs, opts);
946
1002
  }
947
1003
 
948
1004
  public enqueueInvalidateCheckpoint(
@@ -985,8 +1041,8 @@ export class SequencerPublisher {
985
1041
  request: L1TxRequest,
986
1042
  checkSuccess: (receipt: TransactionReceipt) => boolean | undefined,
987
1043
  slotNumber: SlotNumber,
988
- timestamp: bigint,
989
1044
  ) {
1045
+ const timestamp = this.getSimulationTimestamp(slotNumber);
990
1046
  const logData = { slotNumber, timestamp, gasLimit: undefined as bigint | undefined };
991
1047
  if (this.lastActions[action] && this.lastActions[action] === slotNumber) {
992
1048
  this.log.debug(`Skipping duplicate action ${action} for slot ${slotNumber}`);
@@ -999,12 +1055,15 @@ export class SequencerPublisher {
999
1055
  this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
1000
1056
 
1001
1057
  let gasUsed: bigint;
1058
+ const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
1059
+
1002
1060
  try {
1003
- ({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], ErrorsAbi)); // TODO(palla/slash): Check the timestamp logic
1061
+ ({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], simulateAbi));
1004
1062
  this.log.verbose(`Simulation for ${action} succeeded`, { ...logData, request, gasUsed });
1005
1063
  } catch (err) {
1006
- const viemError = formatViemError(err);
1064
+ const viemError = formatViemError(err, simulateAbi);
1007
1065
  this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
1066
+
1008
1067
  return false;
1009
1068
  }
1010
1069
 
@@ -1012,10 +1071,14 @@ export class SequencerPublisher {
1012
1071
  const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil((Number(gasUsed) * 64) / 63)));
1013
1072
  logData.gasLimit = gasLimit;
1014
1073
 
1074
+ // Store the ABI used for simulation on the request so Multicall3.forward can decode errors
1075
+ // when the tx is sent and a revert is diagnosed via simulation.
1076
+ const requestWithAbi = { ...request, abi: simulateAbi };
1077
+
1015
1078
  this.log.debug(`Enqueuing ${action}`, logData);
1016
1079
  this.addRequest({
1017
1080
  action,
1018
- request,
1081
+ request: requestWithAbi,
1019
1082
  gasConfig: { gasLimit },
1020
1083
  lastValidL2Slot: slotNumber,
1021
1084
  checkSuccess: (_req, result) => {
@@ -1051,7 +1114,6 @@ export class SequencerPublisher {
1051
1114
 
1052
1115
  private async prepareProposeTx(
1053
1116
  encodedData: L1ProcessArgs,
1054
- timestamp: bigint,
1055
1117
  options: { forcePendingCheckpointNumber?: CheckpointNumber },
1056
1118
  ) {
1057
1119
  const kzg = Blob.getViemKzgInstance();
@@ -1097,8 +1159,7 @@ export class SequencerPublisher {
1097
1159
  header: encodedData.header.toViem(),
1098
1160
  archive: toHex(encodedData.archive),
1099
1161
  oracleInput: {
1100
- // We are currently not modifying these. See #9963
1101
- feeAssetPriceModifier: 0n,
1162
+ feeAssetPriceModifier: encodedData.feeAssetPriceModifier,
1102
1163
  },
1103
1164
  },
1104
1165
  encodedData.attestationsAndSigners.getPackedAttestations(),
@@ -1107,7 +1168,7 @@ export class SequencerPublisher {
1107
1168
  blobInput,
1108
1169
  ] as const;
1109
1170
 
1110
- const { rollupData, simulationResult } = await this.simulateProposeTx(args, timestamp, options);
1171
+ const { rollupData, simulationResult } = await this.simulateProposeTx(args, options);
1111
1172
 
1112
1173
  return { args, blobEvaluationGas, rollupData, simulationResult };
1113
1174
  }
@@ -1115,7 +1176,6 @@ export class SequencerPublisher {
1115
1176
  /**
1116
1177
  * Simulates the propose tx with eth_simulateV1
1117
1178
  * @param args - The propose tx args
1118
- * @param timestamp - The timestamp to simulate proposal at
1119
1179
  * @returns The simulation result
1120
1180
  */
1121
1181
  private async simulateProposeTx(
@@ -1124,7 +1184,7 @@ export class SequencerPublisher {
1124
1184
  readonly header: ViemHeader;
1125
1185
  readonly archive: `0x${string}`;
1126
1186
  readonly oracleInput: {
1127
- readonly feeAssetPriceModifier: 0n;
1187
+ readonly feeAssetPriceModifier: bigint;
1128
1188
  };
1129
1189
  },
1130
1190
  ViemCommitteeAttestations,
@@ -1132,7 +1192,6 @@ export class SequencerPublisher {
1132
1192
  ViemSignature,
1133
1193
  `0x${string}`,
1134
1194
  ],
1135
- timestamp: bigint,
1136
1195
  options: { forcePendingCheckpointNumber?: CheckpointNumber },
1137
1196
  ) {
1138
1197
  const rollupData = encodeFunctionData({
@@ -1166,25 +1225,26 @@ export class SequencerPublisher {
1166
1225
  });
1167
1226
  }
1168
1227
 
1228
+ const simTs = this.getSimulationTimestamp(SlotNumber.fromBigInt(args[0].header.slotNumber));
1229
+
1169
1230
  const simulationResult = await this.l1TxUtils
1170
1231
  .simulate(
1171
1232
  {
1172
1233
  to: this.rollupContract.address,
1173
1234
  data: rollupData,
1174
- gas: SequencerPublisher.PROPOSE_GAS_GUESS,
1235
+ gas: MAX_L1_TX_LIMIT,
1175
1236
  ...(this.proposerAddressForSimulation && { from: this.proposerAddressForSimulation.toString() }),
1176
1237
  },
1177
1238
  {
1178
- // @note we add 1n to the timestamp because geth implementation doesn't like simulation timestamp to be equal to the current block timestamp
1179
- time: timestamp + 1n,
1239
+ time: simTs,
1180
1240
  // @note reth should have a 30m gas limit per block but throws errors that this tx is beyond limit so we increase here
1181
- gasLimit: SequencerPublisher.PROPOSE_GAS_GUESS * 2n,
1241
+ gasLimit: MAX_L1_TX_LIMIT * 2n,
1182
1242
  },
1183
1243
  stateOverrides,
1184
1244
  RollupAbi,
1185
1245
  {
1186
1246
  // @note fallback gas estimate to use if the node doesn't support simulation API
1187
- fallbackGasEstimate: SequencerPublisher.PROPOSE_GAS_GUESS,
1247
+ fallbackGasEstimate: MAX_L1_TX_LIMIT,
1188
1248
  },
1189
1249
  )
1190
1250
  .catch(err => {
@@ -1194,11 +1254,11 @@ export class SequencerPublisher {
1194
1254
  this.log.debug(`Ignoring expected ValidatorSelection__MissingProposerSignature error in fisherman mode`);
1195
1255
  // Return a minimal simulation result with the fallback gas estimate
1196
1256
  return {
1197
- gasUsed: SequencerPublisher.PROPOSE_GAS_GUESS,
1257
+ gasUsed: MAX_L1_TX_LIMIT,
1198
1258
  logs: [],
1199
1259
  };
1200
1260
  }
1201
- this.log.error(`Failed to simulate propose tx`, viemError);
1261
+ this.log.error(`Failed to simulate propose tx`, viemError, { simulationTimestamp: simTs });
1202
1262
  throw err;
1203
1263
  });
1204
1264
 
@@ -1209,16 +1269,11 @@ export class SequencerPublisher {
1209
1269
  checkpoint: Checkpoint,
1210
1270
  encodedData: L1ProcessArgs,
1211
1271
  opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
1212
- timestamp: bigint,
1213
1272
  ): Promise<void> {
1214
1273
  const slot = checkpoint.header.slotNumber;
1215
1274
  const timer = new Timer();
1216
1275
  const kzg = Blob.getViemKzgInstance();
1217
- const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(
1218
- encodedData,
1219
- timestamp,
1220
- opts,
1221
- );
1276
+ const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(encodedData, opts);
1222
1277
  const startBlock = await this.l1TxUtils.getBlockNumber();
1223
1278
  const gasLimit = this.l1TxUtils.bumpGasLimit(
1224
1279
  BigInt(Math.ceil((Number(simulationResult.gasUsed) * 64) / 63)) +
@@ -1294,4 +1349,17 @@ export class SequencerPublisher {
1294
1349
  },
1295
1350
  });
1296
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
+ }
1297
1365
  }