@aztec/sequencer-client 0.0.1-commit.fffb133c → 0.0.1-dev

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 (66) 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-metrics.d.ts +1 -1
  19. package/dest/publisher/sequencer-publisher-metrics.d.ts.map +1 -1
  20. package/dest/publisher/sequencer-publisher-metrics.js +12 -4
  21. package/dest/publisher/sequencer-publisher.d.ts +28 -14
  22. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  23. package/dest/publisher/sequencer-publisher.js +137 -74
  24. package/dest/sequencer/checkpoint_proposal_job.d.ts +35 -10
  25. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  26. package/dest/sequencer/checkpoint_proposal_job.js +212 -137
  27. package/dest/sequencer/checkpoint_voter.d.ts +1 -2
  28. package/dest/sequencer/checkpoint_voter.d.ts.map +1 -1
  29. package/dest/sequencer/checkpoint_voter.js +2 -5
  30. package/dest/sequencer/metrics.d.ts +17 -5
  31. package/dest/sequencer/metrics.d.ts.map +1 -1
  32. package/dest/sequencer/metrics.js +111 -30
  33. package/dest/sequencer/sequencer.d.ts +29 -13
  34. package/dest/sequencer/sequencer.d.ts.map +1 -1
  35. package/dest/sequencer/sequencer.js +48 -44
  36. package/dest/sequencer/timetable.d.ts +4 -6
  37. package/dest/sequencer/timetable.d.ts.map +1 -1
  38. package/dest/sequencer/timetable.js +7 -11
  39. package/dest/sequencer/types.d.ts +2 -2
  40. package/dest/sequencer/types.d.ts.map +1 -1
  41. package/dest/test/index.d.ts +3 -5
  42. package/dest/test/index.d.ts.map +1 -1
  43. package/dest/test/mock_checkpoint_builder.d.ts +12 -12
  44. package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
  45. package/dest/test/mock_checkpoint_builder.js +47 -36
  46. package/dest/test/utils.d.ts +3 -3
  47. package/dest/test/utils.d.ts.map +1 -1
  48. package/dest/test/utils.js +5 -4
  49. package/package.json +28 -28
  50. package/src/client/sequencer-client.ts +78 -21
  51. package/src/config.ts +61 -38
  52. package/src/global_variable_builder/global_builder.ts +23 -24
  53. package/src/global_variable_builder/index.ts +1 -1
  54. package/src/publisher/config.ts +112 -43
  55. package/src/publisher/sequencer-publisher-factory.ts +23 -6
  56. package/src/publisher/sequencer-publisher-metrics.ts +7 -3
  57. package/src/publisher/sequencer-publisher.ts +149 -90
  58. package/src/sequencer/checkpoint_proposal_job.ts +309 -186
  59. package/src/sequencer/checkpoint_voter.ts +1 -12
  60. package/src/sequencer/metrics.ts +124 -32
  61. package/src/sequencer/sequencer.ts +61 -50
  62. package/src/sequencer/timetable.ts +13 -12
  63. package/src/sequencer/types.ts +1 -1
  64. package/src/test/index.ts +2 -4
  65. package/src/test/mock_checkpoint_builder.ts +67 -53
  66. package/src/test/utils.ts +5 -2
@@ -4,8 +4,8 @@ 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
- type IEmpireBase,
9
9
  MULTI_CALL_3_ADDRESS,
10
10
  Multicall3,
11
11
  RollupContract,
@@ -18,14 +18,16 @@ import {
18
18
  type L1BlobInputs,
19
19
  type L1TxConfig,
20
20
  type L1TxRequest,
21
+ type L1TxUtils,
22
+ MAX_L1_TX_LIMIT,
21
23
  type TransactionStats,
22
24
  WEI_CONST,
23
25
  } 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';
26
+ import { FormattedViemError, formatViemError, mergeAbis, tryExtractEvent } from '@aztec/ethereum/utils';
26
27
  import { sumBigint } from '@aztec/foundation/bigint';
27
28
  import { toHex as toPaddedHex } from '@aztec/foundation/bigint-buffer';
28
29
  import { CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
30
+ import { trimmedBytesLength } from '@aztec/foundation/buffer';
29
31
  import { pick } from '@aztec/foundation/collection';
30
32
  import type { Fr } from '@aztec/foundation/curves/bn254';
31
33
  import { EthAddress } from '@aztec/foundation/eth-address';
@@ -37,6 +39,7 @@ import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
37
39
  import { type ProposerSlashAction, encodeSlashConsensusVotes } from '@aztec/slasher';
38
40
  import { CommitteeAttestationsAndSigners, type ValidateCheckpointResult } from '@aztec/stdlib/block';
39
41
  import type { Checkpoint } from '@aztec/stdlib/checkpoint';
42
+ import { getLastL1SlotTimestampForL2Slot, getNextL1SlotTimestamp } from '@aztec/stdlib/epoch-helpers';
40
43
  import { SlashFactoryContract } from '@aztec/stdlib/l1-contracts';
41
44
  import type { CheckpointHeader } from '@aztec/stdlib/rollup';
42
45
  import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
@@ -44,7 +47,7 @@ import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from
44
47
 
45
48
  import { type StateOverride, type TransactionReceipt, type TypedDataDefinition, encodeFunctionData, toHex } from 'viem';
46
49
 
47
- import type { PublisherConfig, TxSenderConfig } from './config.js';
50
+ import type { SequencerPublisherConfig } from './config.js';
48
51
  import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
49
52
 
50
53
  /** Arguments to the process method of the rollup contract */
@@ -59,6 +62,8 @@ type L1ProcessArgs = {
59
62
  attestationsAndSigners: CommitteeAttestationsAndSigners;
60
63
  /** Attestations and signers signature */
61
64
  attestationsAndSignersSignature: Signature;
65
+ /** The fee asset price modifier in basis points (from oracle) */
66
+ feeAssetPriceModifier: bigint;
62
67
  };
63
68
 
64
69
  export const Actions = [
@@ -114,6 +119,8 @@ export class SequencerPublisher {
114
119
 
115
120
  protected log: Logger;
116
121
  protected ethereumSlotDuration: bigint;
122
+ protected aztecSlotDuration: bigint;
123
+ private dateProvider: DateProvider;
117
124
 
118
125
  private blobClient: BlobClientInterface;
119
126
 
@@ -122,10 +129,9 @@ export class SequencerPublisher {
122
129
 
123
130
  /** L1 fee analyzer for fisherman mode */
124
131
  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;
132
+
133
+ /** Fee asset price oracle for computing price modifiers from Uniswap V4 */
134
+ private feeAssetPriceOracle: FeeAssetPriceOracle;
129
135
 
130
136
  // A CALL to a cold address is 2700 gas
131
137
  public static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
@@ -133,7 +139,7 @@ export class SequencerPublisher {
133
139
  // Gas report for VotingWithSigTest shows a max gas of 100k, but we've seen it cost 700k+ in testnet
134
140
  public static VOTE_GAS_GUESS: bigint = 800_000n;
135
141
 
136
- public l1TxUtils: L1TxUtilsWithBlobs;
142
+ public l1TxUtils: L1TxUtils;
137
143
  public rollupContract: RollupContract;
138
144
  public govProposerContract: GovernanceProposerContract;
139
145
  public slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
@@ -144,11 +150,12 @@ export class SequencerPublisher {
144
150
  protected requests: RequestWithExpiry[] = [];
145
151
 
146
152
  constructor(
147
- private config: TxSenderConfig & PublisherConfig & Pick<L1ContractsConfig, 'ethereumSlotDuration'>,
153
+ private config: Pick<SequencerPublisherConfig, 'fishermanMode'> &
154
+ Pick<L1ContractsConfig, 'ethereumSlotDuration' | 'aztecSlotDuration'> & { l1ChainId: number },
148
155
  deps: {
149
156
  telemetry?: TelemetryClient;
150
157
  blobClient: BlobClientInterface;
151
- l1TxUtils: L1TxUtilsWithBlobs;
158
+ l1TxUtils: L1TxUtils;
152
159
  rollupContract: RollupContract;
153
160
  slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
154
161
  governanceProposerContract: GovernanceProposerContract;
@@ -162,6 +169,8 @@ export class SequencerPublisher {
162
169
  ) {
163
170
  this.log = deps.log ?? createLogger('sequencer:publisher');
164
171
  this.ethereumSlotDuration = BigInt(config.ethereumSlotDuration);
172
+ this.aztecSlotDuration = BigInt(config.aztecSlotDuration);
173
+ this.dateProvider = deps.dateProvider;
165
174
  this.epochCache = deps.epochCache;
166
175
  this.lastActions = deps.lastActions;
167
176
 
@@ -192,12 +201,27 @@ export class SequencerPublisher {
192
201
  createLogger('sequencer:publisher:fee-analyzer'),
193
202
  );
194
203
  }
204
+
205
+ // Initialize fee asset price oracle
206
+ this.feeAssetPriceOracle = new FeeAssetPriceOracle(
207
+ this.l1TxUtils.client,
208
+ this.rollupContract,
209
+ createLogger('sequencer:publisher:price-oracle'),
210
+ );
195
211
  }
196
212
 
197
213
  public getRollupContract(): RollupContract {
198
214
  return this.rollupContract;
199
215
  }
200
216
 
217
+ /**
218
+ * Gets the fee asset price modifier from the oracle.
219
+ * Returns 0n if the oracle query fails.
220
+ */
221
+ public getFeeAssetPriceModifier(): Promise<bigint> {
222
+ return this.feeAssetPriceOracle.computePriceModifier();
223
+ }
224
+
201
225
  public getSenderAddress() {
202
226
  return this.l1TxUtils.getSenderAddress();
203
227
  }
@@ -273,7 +297,7 @@ export class SequencerPublisher {
273
297
  // Start the analysis
274
298
  const analysisId = await this.l1FeeAnalyzer.startAnalysis(
275
299
  l2SlotNumber,
276
- gasLimit > 0n ? gasLimit : SequencerPublisher.PROPOSE_GAS_GUESS,
300
+ gasLimit > 0n ? gasLimit : MAX_L1_TX_LIMIT,
277
301
  l1Requests,
278
302
  blobConfig,
279
303
  onComplete,
@@ -335,8 +359,8 @@ export class SequencerPublisher {
335
359
  // @note - we can only have one blob config per bundle
336
360
  // find requests with gas and blob configs
337
361
  // 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);
362
+ const gasConfigs = validRequests.filter(request => request.gasConfig).map(request => request.gasConfig);
363
+ const blobConfigs = validRequests.filter(request => request.blobConfig).map(request => request.blobConfig);
340
364
 
341
365
  if (blobConfigs.length > 1) {
342
366
  throw new Error('Multiple blob configs found');
@@ -346,7 +370,16 @@ export class SequencerPublisher {
346
370
 
347
371
  // Merge gasConfigs. Yields the sum of gasLimits, and the earliest txTimeoutAt, or undefined if no gasConfig sets them.
348
372
  const gasLimits = gasConfigs.map(g => g?.gasLimit).filter((g): g is bigint => g !== undefined);
349
- const gasLimit = gasLimits.length > 0 ? sumBigint(gasLimits) : undefined; // sum
373
+ let gasLimit = gasLimits.length > 0 ? sumBigint(gasLimits) : undefined; // sum
374
+ // Cap at L1 block gas limit so the node accepts the tx ("gas limit too high" otherwise).
375
+ const maxGas = MAX_L1_TX_LIMIT;
376
+ if (gasLimit !== undefined && gasLimit > maxGas) {
377
+ this.log.debug('Capping bundled tx gas limit to L1 max', {
378
+ requested: gasLimit,
379
+ capped: maxGas,
380
+ });
381
+ gasLimit = maxGas;
382
+ }
350
383
  const txTimeoutAts = gasConfigs.map(g => g?.txTimeoutAt).filter((g): g is Date => g !== undefined);
351
384
  const txTimeoutAt = txTimeoutAts.length > 0 ? new Date(Math.min(...txTimeoutAts.map(g => g.getTime()))) : undefined; // earliest
352
385
  const txConfig: RequestWithExpiry['gasConfig'] = { gasLimit, txTimeoutAt };
@@ -395,7 +428,16 @@ export class SequencerPublisher {
395
428
  this.log.error(`Failed to publish bundled transactions (${actionsListStr})`, result);
396
429
  return { failedActions: requests.map(r => r.action) };
397
430
  } else {
398
- this.log.verbose(`Published bundled transactions (${actionsListStr})`, { result, requests });
431
+ this.log.verbose(`Published bundled transactions (${actionsListStr})`, {
432
+ result,
433
+ requests: requests.map(r => ({
434
+ ...r,
435
+ // Avoid logging large blob data
436
+ blobConfig: r.blobConfig
437
+ ? { ...r.blobConfig, blobs: r.blobConfig.blobs.map(b => ({ size: trimmedBytesLength(b) })) }
438
+ : undefined,
439
+ })),
440
+ });
399
441
  const successfulActions: Action[] = [];
400
442
  const failedActions: Action[] = [];
401
443
  for (const request of requests) {
@@ -410,20 +452,24 @@ export class SequencerPublisher {
410
452
  }
411
453
 
412
454
  /**
413
- * @notice Will call `canProposeAtNextEthBlock` to make sure that it is possible to propose
455
+ * @notice Will call `canProposeAt` to make sure that it is possible to propose
414
456
  * @param tipArchive - The archive to check
415
457
  * @returns The slot and block number if it is possible to propose, undefined otherwise
416
458
  */
417
- public canProposeAtNextEthBlock(
459
+ public canProposeAt(
418
460
  tipArchive: Fr,
419
461
  msgSender: EthAddress,
420
- opts: { forcePendingCheckpointNumber?: CheckpointNumber } = {},
462
+ opts: { forcePendingCheckpointNumber?: CheckpointNumber; pipelined?: boolean } = {},
421
463
  ) {
422
464
  // TODO: #14291 - should loop through multiple keys to check if any of them can propose
423
465
  const ignoredErrors = ['SlotAlreadyInChain', 'InvalidProposer', 'InvalidArchive'];
424
466
 
467
+ const pipelined = opts.pipelined ?? false;
468
+ const slotOffset = pipelined ? this.aztecSlotDuration : 0n;
469
+ const nextL1SlotTs = this.getNextL1SlotTimestamp() + slotOffset;
470
+
425
471
  return this.rollupContract
426
- .canProposeAtNextEthBlock(tipArchive.toBuffer(), msgSender.toString(), Number(this.ethereumSlotDuration), {
472
+ .canProposeAt(tipArchive.toBuffer(), msgSender.toString(), nextL1SlotTs, {
427
473
  forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
428
474
  })
429
475
  .catch(err => {
@@ -460,7 +506,7 @@ export class SequencerPublisher {
460
506
  flags,
461
507
  ] as const;
462
508
 
463
- const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
509
+ const ts = this.getSimulationTimestamp(header.slotNumber);
464
510
  const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(
465
511
  opts?.forcePendingCheckpointNumber,
466
512
  );
@@ -483,7 +529,7 @@ export class SequencerPublisher {
483
529
  data: encodeFunctionData({ abi: RollupAbi, functionName: 'validateHeaderWithAttestations', args }),
484
530
  from: MULTI_CALL_3_ADDRESS,
485
531
  },
486
- { time: ts + 1n },
532
+ { time: ts },
487
533
  stateOverrides,
488
534
  );
489
535
  this.log.debug(`Simulated validateHeader`);
@@ -517,7 +563,12 @@ export class SequencerPublisher {
517
563
  this.log.debug(`Simulating invalidate checkpoint ${checkpointNumber}`, { ...logData, request });
518
564
 
519
565
  try {
520
- const { gasUsed } = await this.l1TxUtils.simulate(request, undefined, undefined, ErrorsAbi);
566
+ const { gasUsed } = await this.l1TxUtils.simulate(
567
+ request,
568
+ undefined,
569
+ undefined,
570
+ mergeAbis([request.abi ?? [], ErrorsAbi]),
571
+ );
521
572
  this.log.verbose(`Simulation for invalidate checkpoint ${checkpointNumber} succeeded`, {
522
573
  ...logData,
523
574
  request,
@@ -536,7 +587,7 @@ export class SequencerPublisher {
536
587
 
537
588
  // If the error is due to the checkpoint not being in the pending chain, and it was indeed removed by someone else,
538
589
  // we can safely ignore it and return undefined so we go ahead with checkpoint building.
539
- if (viemError.message?.includes('Rollup__BlockNotInPendingChain')) {
590
+ if (viemError.message?.includes('Rollup__CheckpointNotInPendingChain')) {
540
591
  this.log.verbose(
541
592
  `Simulation for invalidate checkpoint ${checkpointNumber} failed due to checkpoint not being in pending chain`,
542
593
  { ...logData, request, error: viemError.message },
@@ -605,26 +656,9 @@ export class SequencerPublisher {
605
656
  attestationsAndSigners: CommitteeAttestationsAndSigners,
606
657
  attestationsAndSignersSignature: Signature,
607
658
  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
-
659
+ ): Promise<void> {
626
660
  const blobFields = checkpoint.toBlobFields();
627
- const blobs = getBlobsPerL1Block(blobFields);
661
+ const blobs = await getBlobsPerL1Block(blobFields);
628
662
  const blobInput = getPrefixedEthBlobCommitments(blobs);
629
663
 
630
664
  const args = [
@@ -632,7 +666,7 @@ export class SequencerPublisher {
632
666
  header: checkpoint.header.toViem(),
633
667
  archive: toHex(checkpoint.archive.root.toBuffer()),
634
668
  oracleInput: {
635
- feeAssetPriceModifier: 0n,
669
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
636
670
  },
637
671
  },
638
672
  attestationsAndSigners.getPackedAttestations(),
@@ -641,16 +675,14 @@ export class SequencerPublisher {
641
675
  blobInput,
642
676
  ] as const;
643
677
 
644
- await this.simulateProposeTx(args, ts, options);
645
- return ts;
678
+ await this.simulateProposeTx(args, options);
646
679
  }
647
680
 
648
681
  private async enqueueCastSignalHelper(
649
682
  slotNumber: SlotNumber,
650
- timestamp: bigint,
651
683
  signalType: GovernanceSignalAction,
652
684
  payload: EthAddress,
653
- base: IEmpireBase,
685
+ base: GovernanceProposerContract,
654
686
  signerAddress: EthAddress,
655
687
  signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
656
688
  ): Promise<boolean> {
@@ -681,6 +713,26 @@ export class SequencerPublisher {
681
713
  return false;
682
714
  }
683
715
 
716
+ // Skip signaling if there is already a live (non-terminal) Governance proposal for this
717
+ // payload. This is intentionally not cached: a previously-live proposal may transition to
718
+ // a terminal state (Dropped/Rejected/Expired/Executed), at which point we may want to re-signal
719
+ // the same payload in a future round.
720
+ let proposed = false;
721
+ try {
722
+ proposed = await base.hasActiveProposalWithPayload(payload.toString());
723
+ } catch (err) {
724
+ // We deliberately swallow the error and proceed to signal. Failing closed (skipping the
725
+ // signal) on transient RPC errors would let a flaky L1 endpoint silence governance
726
+ // participation entirely; failing open at worst produces a duplicate signal that the
727
+ // contract will simply count alongside others in the round.
728
+ this.log.error(`Failed to check if payload ${payload} was already proposed (signalling anyway)`, err);
729
+ }
730
+
731
+ if (proposed) {
732
+ this.log.info(`Payload ${payload} has a live governance proposal, stopping signals`);
733
+ return false;
734
+ }
735
+
684
736
  const cachedLastVote = this.lastActions[signalType];
685
737
  this.lastActions[signalType] = slotNumber;
686
738
  const action = signalType;
@@ -699,11 +751,16 @@ export class SequencerPublisher {
699
751
  lastValidL2Slot: slotNumber,
700
752
  });
701
753
 
754
+ const timestamp = this.getSimulationTimestamp(slotNumber);
755
+
702
756
  try {
703
- await this.l1TxUtils.simulate(request, { time: timestamp }, [], ErrorsAbi);
757
+ await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
704
758
  this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
705
759
  } catch (err) {
706
- this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, err);
760
+ const viemError = formatViemError(err);
761
+ this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, viemError, {
762
+ simulationTimestamp: timestamp,
763
+ });
707
764
  // Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
708
765
  }
709
766
 
@@ -754,19 +811,16 @@ export class SequencerPublisher {
754
811
  /**
755
812
  * Enqueues a governance castSignal transaction to cast a signal for a given slot number.
756
813
  * @param slotNumber - The slot number to cast a signal for.
757
- * @param timestamp - The timestamp of the slot to cast a signal for.
758
814
  * @returns True if the signal was successfully enqueued, false otherwise.
759
815
  */
760
816
  public enqueueGovernanceCastSignal(
761
817
  governancePayload: EthAddress,
762
818
  slotNumber: SlotNumber,
763
- timestamp: bigint,
764
819
  signerAddress: EthAddress,
765
820
  signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
766
821
  ): Promise<boolean> {
767
822
  return this.enqueueCastSignalHelper(
768
823
  slotNumber,
769
- timestamp,
770
824
  'governance-signal',
771
825
  governancePayload,
772
826
  this.govProposerContract,
@@ -779,7 +833,6 @@ export class SequencerPublisher {
779
833
  public async enqueueSlashingActions(
780
834
  actions: ProposerSlashAction[],
781
835
  slotNumber: SlotNumber,
782
- timestamp: bigint,
783
836
  signerAddress: EthAddress,
784
837
  signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
785
838
  ): Promise<boolean> {
@@ -800,10 +853,9 @@ export class SequencerPublisher {
800
853
  });
801
854
  await this.enqueueCastSignalHelper(
802
855
  slotNumber,
803
- timestamp,
804
856
  'empire-slashing-signal',
805
857
  action.payload,
806
- this.slashingProposerContract,
858
+ this.slashingProposerContract as any,
807
859
  signerAddress,
808
860
  signer,
809
861
  );
@@ -819,7 +871,6 @@ export class SequencerPublisher {
819
871
  (receipt: TransactionReceipt) =>
820
872
  !!this.slashFactoryContract.tryExtractSlashPayloadCreatedEvent(receipt.logs),
821
873
  slotNumber,
822
- timestamp,
823
874
  );
824
875
  break;
825
876
  }
@@ -837,7 +888,6 @@ export class SequencerPublisher {
837
888
  request,
838
889
  (receipt: TransactionReceipt) => !!empireSlashingProposer.tryExtractPayloadSubmittedEvent(receipt.logs),
839
890
  slotNumber,
840
- timestamp,
841
891
  );
842
892
  break;
843
893
  }
@@ -861,7 +911,6 @@ export class SequencerPublisher {
861
911
  request,
862
912
  (receipt: TransactionReceipt) => !!tallySlashingProposer.tryExtractVoteCastEvent(receipt.logs),
863
913
  slotNumber,
864
- timestamp,
865
914
  );
866
915
  break;
867
916
  }
@@ -883,7 +932,6 @@ export class SequencerPublisher {
883
932
  request,
884
933
  (receipt: TransactionReceipt) => !!tallySlashingProposer.tryExtractRoundExecutedEvent(receipt.logs),
885
934
  slotNumber,
886
- timestamp,
887
935
  );
888
936
  break;
889
937
  }
@@ -908,25 +956,24 @@ export class SequencerPublisher {
908
956
  const checkpointHeader = checkpoint.header;
909
957
 
910
958
  const blobFields = checkpoint.toBlobFields();
911
- const blobs = getBlobsPerL1Block(blobFields);
959
+ const blobs = await getBlobsPerL1Block(blobFields);
912
960
 
913
- const proposeTxArgs = {
961
+ const proposeTxArgs: L1ProcessArgs = {
914
962
  header: checkpointHeader,
915
963
  archive: checkpoint.archive.root.toBuffer(),
916
964
  blobs,
917
965
  attestationsAndSigners,
918
966
  attestationsAndSignersSignature,
967
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
919
968
  };
920
969
 
921
- let ts: bigint;
922
-
923
970
  try {
924
971
  // @note This will make sure that we are passing the checks for our header ASSUMING that the data is also made available
925
972
  // This means that we can avoid the simulation issues in later checks.
926
973
  // By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which
927
974
  // make time consistency checks break.
928
975
  // 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(
976
+ await this.validateCheckpointForSubmission(
930
977
  checkpoint,
931
978
  attestationsAndSigners,
932
979
  attestationsAndSignersSignature,
@@ -942,7 +989,7 @@ export class SequencerPublisher {
942
989
  }
943
990
 
944
991
  this.log.verbose(`Enqueuing checkpoint propose transaction`, { ...checkpoint.toCheckpointInfo(), ...opts });
945
- await this.addProposeTx(checkpoint, proposeTxArgs, opts, ts);
992
+ await this.addProposeTx(checkpoint, proposeTxArgs, opts);
946
993
  }
947
994
 
948
995
  public enqueueInvalidateCheckpoint(
@@ -985,8 +1032,8 @@ export class SequencerPublisher {
985
1032
  request: L1TxRequest,
986
1033
  checkSuccess: (receipt: TransactionReceipt) => boolean | undefined,
987
1034
  slotNumber: SlotNumber,
988
- timestamp: bigint,
989
1035
  ) {
1036
+ const timestamp = this.getSimulationTimestamp(slotNumber);
990
1037
  const logData = { slotNumber, timestamp, gasLimit: undefined as bigint | undefined };
991
1038
  if (this.lastActions[action] && this.lastActions[action] === slotNumber) {
992
1039
  this.log.debug(`Skipping duplicate action ${action} for slot ${slotNumber}`);
@@ -999,12 +1046,15 @@ export class SequencerPublisher {
999
1046
  this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
1000
1047
 
1001
1048
  let gasUsed: bigint;
1049
+ const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
1050
+
1002
1051
  try {
1003
- ({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], ErrorsAbi)); // TODO(palla/slash): Check the timestamp logic
1052
+ ({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], simulateAbi));
1004
1053
  this.log.verbose(`Simulation for ${action} succeeded`, { ...logData, request, gasUsed });
1005
1054
  } catch (err) {
1006
- const viemError = formatViemError(err);
1055
+ const viemError = formatViemError(err, simulateAbi);
1007
1056
  this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
1057
+
1008
1058
  return false;
1009
1059
  }
1010
1060
 
@@ -1012,10 +1062,14 @@ export class SequencerPublisher {
1012
1062
  const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil((Number(gasUsed) * 64) / 63)));
1013
1063
  logData.gasLimit = gasLimit;
1014
1064
 
1065
+ // Store the ABI used for simulation on the request so Multicall3.forward can decode errors
1066
+ // when the tx is sent and a revert is diagnosed via simulation.
1067
+ const requestWithAbi = { ...request, abi: simulateAbi };
1068
+
1015
1069
  this.log.debug(`Enqueuing ${action}`, logData);
1016
1070
  this.addRequest({
1017
1071
  action,
1018
- request,
1072
+ request: requestWithAbi,
1019
1073
  gasConfig: { gasLimit },
1020
1074
  lastValidL2Slot: slotNumber,
1021
1075
  checkSuccess: (_req, result) => {
@@ -1051,7 +1105,6 @@ export class SequencerPublisher {
1051
1105
 
1052
1106
  private async prepareProposeTx(
1053
1107
  encodedData: L1ProcessArgs,
1054
- timestamp: bigint,
1055
1108
  options: { forcePendingCheckpointNumber?: CheckpointNumber },
1056
1109
  ) {
1057
1110
  const kzg = Blob.getViemKzgInstance();
@@ -1097,8 +1150,7 @@ export class SequencerPublisher {
1097
1150
  header: encodedData.header.toViem(),
1098
1151
  archive: toHex(encodedData.archive),
1099
1152
  oracleInput: {
1100
- // We are currently not modifying these. See #9963
1101
- feeAssetPriceModifier: 0n,
1153
+ feeAssetPriceModifier: encodedData.feeAssetPriceModifier,
1102
1154
  },
1103
1155
  },
1104
1156
  encodedData.attestationsAndSigners.getPackedAttestations(),
@@ -1107,7 +1159,7 @@ export class SequencerPublisher {
1107
1159
  blobInput,
1108
1160
  ] as const;
1109
1161
 
1110
- const { rollupData, simulationResult } = await this.simulateProposeTx(args, timestamp, options);
1162
+ const { rollupData, simulationResult } = await this.simulateProposeTx(args, options);
1111
1163
 
1112
1164
  return { args, blobEvaluationGas, rollupData, simulationResult };
1113
1165
  }
@@ -1115,7 +1167,6 @@ export class SequencerPublisher {
1115
1167
  /**
1116
1168
  * Simulates the propose tx with eth_simulateV1
1117
1169
  * @param args - The propose tx args
1118
- * @param timestamp - The timestamp to simulate proposal at
1119
1170
  * @returns The simulation result
1120
1171
  */
1121
1172
  private async simulateProposeTx(
@@ -1124,7 +1175,7 @@ export class SequencerPublisher {
1124
1175
  readonly header: ViemHeader;
1125
1176
  readonly archive: `0x${string}`;
1126
1177
  readonly oracleInput: {
1127
- readonly feeAssetPriceModifier: 0n;
1178
+ readonly feeAssetPriceModifier: bigint;
1128
1179
  };
1129
1180
  },
1130
1181
  ViemCommitteeAttestations,
@@ -1132,7 +1183,6 @@ export class SequencerPublisher {
1132
1183
  ViemSignature,
1133
1184
  `0x${string}`,
1134
1185
  ],
1135
- timestamp: bigint,
1136
1186
  options: { forcePendingCheckpointNumber?: CheckpointNumber },
1137
1187
  ) {
1138
1188
  const rollupData = encodeFunctionData({
@@ -1166,25 +1216,26 @@ export class SequencerPublisher {
1166
1216
  });
1167
1217
  }
1168
1218
 
1219
+ const simTs = this.getSimulationTimestamp(SlotNumber.fromBigInt(args[0].header.slotNumber));
1220
+
1169
1221
  const simulationResult = await this.l1TxUtils
1170
1222
  .simulate(
1171
1223
  {
1172
1224
  to: this.rollupContract.address,
1173
1225
  data: rollupData,
1174
- gas: SequencerPublisher.PROPOSE_GAS_GUESS,
1226
+ gas: MAX_L1_TX_LIMIT,
1175
1227
  ...(this.proposerAddressForSimulation && { from: this.proposerAddressForSimulation.toString() }),
1176
1228
  },
1177
1229
  {
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,
1230
+ time: simTs,
1180
1231
  // @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,
1232
+ gasLimit: MAX_L1_TX_LIMIT * 2n,
1182
1233
  },
1183
1234
  stateOverrides,
1184
1235
  RollupAbi,
1185
1236
  {
1186
1237
  // @note fallback gas estimate to use if the node doesn't support simulation API
1187
- fallbackGasEstimate: SequencerPublisher.PROPOSE_GAS_GUESS,
1238
+ fallbackGasEstimate: MAX_L1_TX_LIMIT,
1188
1239
  },
1189
1240
  )
1190
1241
  .catch(err => {
@@ -1194,11 +1245,11 @@ export class SequencerPublisher {
1194
1245
  this.log.debug(`Ignoring expected ValidatorSelection__MissingProposerSignature error in fisherman mode`);
1195
1246
  // Return a minimal simulation result with the fallback gas estimate
1196
1247
  return {
1197
- gasUsed: SequencerPublisher.PROPOSE_GAS_GUESS,
1248
+ gasUsed: MAX_L1_TX_LIMIT,
1198
1249
  logs: [],
1199
1250
  };
1200
1251
  }
1201
- this.log.error(`Failed to simulate propose tx`, viemError);
1252
+ this.log.error(`Failed to simulate propose tx`, viemError, { simulationTimestamp: simTs });
1202
1253
  throw err;
1203
1254
  });
1204
1255
 
@@ -1209,16 +1260,11 @@ export class SequencerPublisher {
1209
1260
  checkpoint: Checkpoint,
1210
1261
  encodedData: L1ProcessArgs,
1211
1262
  opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
1212
- timestamp: bigint,
1213
1263
  ): Promise<void> {
1214
1264
  const slot = checkpoint.header.slotNumber;
1215
1265
  const timer = new Timer();
1216
1266
  const kzg = Blob.getViemKzgInstance();
1217
- const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(
1218
- encodedData,
1219
- timestamp,
1220
- opts,
1221
- );
1267
+ const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(encodedData, opts);
1222
1268
  const startBlock = await this.l1TxUtils.getBlockNumber();
1223
1269
  const gasLimit = this.l1TxUtils.bumpGasLimit(
1224
1270
  BigInt(Math.ceil((Number(simulationResult.gasUsed) * 64) / 63)) +
@@ -1294,4 +1340,17 @@ export class SequencerPublisher {
1294
1340
  },
1295
1341
  });
1296
1342
  }
1343
+
1344
+ /** Returns the timestamp of the last L1 slot within a given L2 slot. Used as the simulation timestamp
1345
+ * for eth_simulateV1 calls, since it's guaranteed to be greater than any L1 block produced during the slot. */
1346
+ private getSimulationTimestamp(slot: SlotNumber): bigint {
1347
+ const l1Constants = this.epochCache.getL1Constants();
1348
+ return getLastL1SlotTimestampForL2Slot(slot, l1Constants);
1349
+ }
1350
+
1351
+ /** Returns the timestamp of the next L1 slot boundary after now. */
1352
+ private getNextL1SlotTimestamp(): bigint {
1353
+ const l1Constants = this.epochCache.getL1Constants();
1354
+ return getNextL1SlotTimestamp(this.dateProvider.nowInSeconds(), l1Constants);
1355
+ }
1297
1356
  }