@aztec/sequencer-client 0.0.1-commit.e6bd8901 → 0.0.1-commit.ec5f612

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 (74) hide show
  1. package/dest/client/sequencer-client.d.ts +12 -7
  2. package/dest/client/sequencer-client.d.ts.map +1 -1
  3. package/dest/client/sequencer-client.js +15 -4
  4. package/dest/config.d.ts +3 -4
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +17 -12
  7. package/dest/global_variable_builder/global_builder.d.ts +2 -4
  8. package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
  9. package/dest/publisher/config.d.ts +35 -17
  10. package/dest/publisher/config.d.ts.map +1 -1
  11. package/dest/publisher/config.js +106 -42
  12. package/dest/publisher/index.d.ts +2 -1
  13. package/dest/publisher/index.d.ts.map +1 -1
  14. package/dest/publisher/l1_tx_failed_store/factory.d.ts +11 -0
  15. package/dest/publisher/l1_tx_failed_store/factory.d.ts.map +1 -0
  16. package/dest/publisher/l1_tx_failed_store/factory.js +22 -0
  17. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts +59 -0
  18. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -0
  19. package/dest/publisher/l1_tx_failed_store/failed_tx_store.js +1 -0
  20. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts +15 -0
  21. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts.map +1 -0
  22. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.js +34 -0
  23. package/dest/publisher/l1_tx_failed_store/index.d.ts +4 -0
  24. package/dest/publisher/l1_tx_failed_store/index.d.ts.map +1 -0
  25. package/dest/publisher/l1_tx_failed_store/index.js +2 -0
  26. package/dest/publisher/sequencer-publisher-factory.d.ts +11 -3
  27. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  28. package/dest/publisher/sequencer-publisher-factory.js +13 -2
  29. package/dest/publisher/sequencer-publisher-metrics.d.ts +1 -1
  30. package/dest/publisher/sequencer-publisher-metrics.d.ts.map +1 -1
  31. package/dest/publisher/sequencer-publisher-metrics.js +12 -4
  32. package/dest/publisher/sequencer-publisher.d.ts +22 -8
  33. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  34. package/dest/publisher/sequencer-publisher.js +297 -47
  35. package/dest/sequencer/checkpoint_proposal_job.d.ts +32 -9
  36. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  37. package/dest/sequencer/checkpoint_proposal_job.js +114 -59
  38. package/dest/sequencer/metrics.d.ts +17 -5
  39. package/dest/sequencer/metrics.d.ts.map +1 -1
  40. package/dest/sequencer/metrics.js +111 -30
  41. package/dest/sequencer/sequencer.d.ts +17 -7
  42. package/dest/sequencer/sequencer.d.ts.map +1 -1
  43. package/dest/sequencer/sequencer.js +30 -27
  44. package/dest/sequencer/timetable.d.ts +1 -4
  45. package/dest/sequencer/timetable.d.ts.map +1 -1
  46. package/dest/sequencer/timetable.js +2 -5
  47. package/dest/test/index.d.ts +3 -5
  48. package/dest/test/index.d.ts.map +1 -1
  49. package/dest/test/mock_checkpoint_builder.d.ts +10 -5
  50. package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
  51. package/dest/test/mock_checkpoint_builder.js +24 -10
  52. package/dest/test/utils.d.ts +3 -3
  53. package/dest/test/utils.d.ts.map +1 -1
  54. package/dest/test/utils.js +5 -4
  55. package/package.json +28 -28
  56. package/src/client/sequencer-client.ts +25 -7
  57. package/src/config.ts +26 -19
  58. package/src/global_variable_builder/global_builder.ts +1 -1
  59. package/src/publisher/config.ts +121 -43
  60. package/src/publisher/index.ts +3 -0
  61. package/src/publisher/l1_tx_failed_store/factory.ts +32 -0
  62. package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +55 -0
  63. package/src/publisher/l1_tx_failed_store/file_store_failed_tx_store.ts +46 -0
  64. package/src/publisher/l1_tx_failed_store/index.ts +3 -0
  65. package/src/publisher/sequencer-publisher-factory.ts +23 -6
  66. package/src/publisher/sequencer-publisher-metrics.ts +7 -3
  67. package/src/publisher/sequencer-publisher.ts +274 -53
  68. package/src/sequencer/checkpoint_proposal_job.ts +159 -76
  69. package/src/sequencer/metrics.ts +124 -32
  70. package/src/sequencer/sequencer.ts +40 -32
  71. package/src/sequencer/timetable.ts +7 -6
  72. package/src/test/index.ts +2 -4
  73. package/src/test/mock_checkpoint_builder.ts +34 -9
  74. 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,11 +19,12 @@ 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,6 +33,7 @@ import type { Fr } from '@aztec/foundation/curves/bn254';
31
33
  import { EthAddress } from '@aztec/foundation/eth-address';
32
34
  import { Signature, type ViemSignature } from '@aztec/foundation/eth-signature';
33
35
  import { type Logger, createLogger } from '@aztec/foundation/log';
36
+ import { makeBackoff, retry } from '@aztec/foundation/retry';
34
37
  import { bufferToHex } from '@aztec/foundation/string';
35
38
  import { DateProvider, Timer } from '@aztec/foundation/timer';
36
39
  import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
@@ -42,9 +45,19 @@ import type { CheckpointHeader } from '@aztec/stdlib/rollup';
42
45
  import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
43
46
  import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
44
47
 
45
- import { type StateOverride, type TransactionReceipt, type TypedDataDefinition, encodeFunctionData, toHex } from 'viem';
46
-
47
- import type { PublisherConfig, TxSenderConfig } from './config.js';
48
+ import {
49
+ type Hex,
50
+ type StateOverride,
51
+ type TransactionReceipt,
52
+ type TypedDataDefinition,
53
+ encodeFunctionData,
54
+ keccak256,
55
+ multicall3Abi,
56
+ toHex,
57
+ } from 'viem';
58
+
59
+ import type { SequencerPublisherConfig } from './config.js';
60
+ import { type FailedL1Tx, type L1TxFailedStore, createL1TxFailedStore } from './l1_tx_failed_store/index.js';
48
61
  import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
49
62
 
50
63
  /** Arguments to the process method of the rollup contract */
@@ -59,6 +72,8 @@ type L1ProcessArgs = {
59
72
  attestationsAndSigners: CommitteeAttestationsAndSigners;
60
73
  /** Attestations and signers signature */
61
74
  attestationsAndSignersSignature: Signature;
75
+ /** The fee asset price modifier in basis points (from oracle) */
76
+ feeAssetPriceModifier: bigint;
62
77
  };
63
78
 
64
79
  export const Actions = [
@@ -104,6 +119,7 @@ export class SequencerPublisher {
104
119
  private interrupted = false;
105
120
  private metrics: SequencerPublisherMetrics;
106
121
  public epochCache: EpochCache;
122
+ private failedTxStore?: Promise<L1TxFailedStore | undefined>;
107
123
 
108
124
  protected governanceLog = createLogger('sequencer:publisher:governance');
109
125
  protected slashingLog = createLogger('sequencer:publisher:slashing');
@@ -111,6 +127,7 @@ export class SequencerPublisher {
111
127
  protected lastActions: Partial<Record<Action, SlotNumber>> = {};
112
128
 
113
129
  private isPayloadEmptyCache: Map<string, boolean> = new Map<string, boolean>();
130
+ private payloadProposedCache: Set<string> = new Set<string>();
114
131
 
115
132
  protected log: Logger;
116
133
  protected ethereumSlotDuration: bigint;
@@ -122,10 +139,9 @@ export class SequencerPublisher {
122
139
 
123
140
  /** L1 fee analyzer for fisherman mode */
124
141
  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;
142
+
143
+ /** Fee asset price oracle for computing price modifiers from Uniswap V4 */
144
+ private feeAssetPriceOracle: FeeAssetPriceOracle;
129
145
 
130
146
  // A CALL to a cold address is 2700 gas
131
147
  public static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
@@ -133,7 +149,7 @@ export class SequencerPublisher {
133
149
  // Gas report for VotingWithSigTest shows a max gas of 100k, but we've seen it cost 700k+ in testnet
134
150
  public static VOTE_GAS_GUESS: bigint = 800_000n;
135
151
 
136
- public l1TxUtils: L1TxUtilsWithBlobs;
152
+ public l1TxUtils: L1TxUtils;
137
153
  public rollupContract: RollupContract;
138
154
  public govProposerContract: GovernanceProposerContract;
139
155
  public slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
@@ -144,11 +160,12 @@ export class SequencerPublisher {
144
160
  protected requests: RequestWithExpiry[] = [];
145
161
 
146
162
  constructor(
147
- private config: TxSenderConfig & PublisherConfig & Pick<L1ContractsConfig, 'ethereumSlotDuration'>,
163
+ private config: Pick<SequencerPublisherConfig, 'fishermanMode' | 'l1TxFailedStore'> &
164
+ Pick<L1ContractsConfig, 'ethereumSlotDuration'> & { l1ChainId: number },
148
165
  deps: {
149
166
  telemetry?: TelemetryClient;
150
167
  blobClient: BlobClientInterface;
151
- l1TxUtils: L1TxUtilsWithBlobs;
168
+ l1TxUtils: L1TxUtils;
152
169
  rollupContract: RollupContract;
153
170
  slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
154
171
  governanceProposerContract: GovernanceProposerContract;
@@ -192,12 +209,52 @@ export class SequencerPublisher {
192
209
  createLogger('sequencer:publisher:fee-analyzer'),
193
210
  );
194
211
  }
212
+
213
+ // Initialize fee asset price oracle
214
+ this.feeAssetPriceOracle = new FeeAssetPriceOracle(
215
+ this.l1TxUtils.client,
216
+ this.rollupContract,
217
+ createLogger('sequencer:publisher:price-oracle'),
218
+ );
219
+
220
+ // Initialize failed L1 tx store (optional, for test networks)
221
+ this.failedTxStore = createL1TxFailedStore(config.l1TxFailedStore, this.log);
222
+ }
223
+
224
+ /**
225
+ * Backs up a failed L1 transaction to the configured store for debugging.
226
+ * Does nothing if no store is configured.
227
+ */
228
+ private backupFailedTx(failedTx: Omit<FailedL1Tx, 'timestamp'>): void {
229
+ if (!this.failedTxStore) {
230
+ return;
231
+ }
232
+
233
+ const tx: FailedL1Tx = {
234
+ ...failedTx,
235
+ timestamp: Date.now(),
236
+ };
237
+
238
+ // Fire and forget - don't block on backup
239
+ void this.failedTxStore
240
+ .then(store => store?.saveFailedTx(tx))
241
+ .catch(err => {
242
+ this.log.warn(`Failed to backup failed L1 tx to store`, err);
243
+ });
195
244
  }
196
245
 
197
246
  public getRollupContract(): RollupContract {
198
247
  return this.rollupContract;
199
248
  }
200
249
 
250
+ /**
251
+ * Gets the fee asset price modifier from the oracle.
252
+ * Returns 0n if the oracle query fails.
253
+ */
254
+ public getFeeAssetPriceModifier(): Promise<bigint> {
255
+ return this.feeAssetPriceOracle.computePriceModifier();
256
+ }
257
+
201
258
  public getSenderAddress() {
202
259
  return this.l1TxUtils.getSenderAddress();
203
260
  }
@@ -273,7 +330,7 @@ export class SequencerPublisher {
273
330
  // Start the analysis
274
331
  const analysisId = await this.l1FeeAnalyzer.startAnalysis(
275
332
  l2SlotNumber,
276
- gasLimit > 0n ? gasLimit : SequencerPublisher.PROPOSE_GAS_GUESS,
333
+ gasLimit > 0n ? gasLimit : MAX_L1_TX_LIMIT,
277
334
  l1Requests,
278
335
  blobConfig,
279
336
  onComplete,
@@ -346,7 +403,16 @@ export class SequencerPublisher {
346
403
 
347
404
  // Merge gasConfigs. Yields the sum of gasLimits, and the earliest txTimeoutAt, or undefined if no gasConfig sets them.
348
405
  const gasLimits = gasConfigs.map(g => g?.gasLimit).filter((g): g is bigint => g !== undefined);
349
- const gasLimit = gasLimits.length > 0 ? sumBigint(gasLimits) : undefined; // sum
406
+ let gasLimit = gasLimits.length > 0 ? sumBigint(gasLimits) : undefined; // sum
407
+ // Cap at L1 block gas limit so the node accepts the tx ("gas limit too high" otherwise).
408
+ const maxGas = MAX_L1_TX_LIMIT;
409
+ if (gasLimit !== undefined && gasLimit > maxGas) {
410
+ this.log.debug('Capping bundled tx gas limit to L1 max', {
411
+ requested: gasLimit,
412
+ capped: maxGas,
413
+ });
414
+ gasLimit = maxGas;
415
+ }
350
416
  const txTimeoutAts = gasConfigs.map(g => g?.txTimeoutAt).filter((g): g is Date => g !== undefined);
351
417
  const txTimeoutAt = txTimeoutAts.length > 0 ? new Date(Math.min(...txTimeoutAts.map(g => g.getTime()))) : undefined; // earliest
352
418
  const txConfig: RequestWithExpiry['gasConfig'] = { gasLimit, txTimeoutAt };
@@ -356,6 +422,21 @@ export class SequencerPublisher {
356
422
  validRequests.sort((a, b) => compareActions(a.action, b.action));
357
423
 
358
424
  try {
425
+ // Capture context for failed tx backup before sending
426
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
427
+ const multicallData = encodeFunctionData({
428
+ abi: multicall3Abi,
429
+ functionName: 'aggregate3',
430
+ args: [
431
+ validRequests.map(r => ({
432
+ target: r.request.to!,
433
+ callData: r.request.data!,
434
+ allowFailure: true,
435
+ })),
436
+ ],
437
+ });
438
+ const blobDataHex = blobConfig?.blobs?.map(b => toHex(b)) as Hex[] | undefined;
439
+
359
440
  this.log.debug('Forwarding transactions', {
360
441
  validRequests: validRequests.map(request => request.action),
361
442
  txConfig,
@@ -368,7 +449,12 @@ export class SequencerPublisher {
368
449
  this.rollupContract.address,
369
450
  this.log,
370
451
  );
371
- const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(validRequests, result);
452
+ const txContext = { multicallData, blobData: blobDataHex, l1BlockNumber };
453
+ const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(
454
+ validRequests,
455
+ result,
456
+ txContext,
457
+ );
372
458
  return { result, expiredActions, sentActions: validActions, successfulActions, failedActions };
373
459
  } catch (err) {
374
460
  const viemError = formatViemError(err);
@@ -388,11 +474,25 @@ export class SequencerPublisher {
388
474
 
389
475
  private callbackBundledTransactions(
390
476
  requests: RequestWithExpiry[],
391
- result?: { receipt: TransactionReceipt } | FormattedViemError,
477
+ result: { receipt: TransactionReceipt; errorMsg?: string } | FormattedViemError | undefined,
478
+ txContext: { multicallData: Hex; blobData?: Hex[]; l1BlockNumber: bigint },
392
479
  ) {
393
480
  const actionsListStr = requests.map(r => r.action).join(', ');
394
481
  if (result instanceof FormattedViemError) {
395
482
  this.log.error(`Failed to publish bundled transactions (${actionsListStr})`, result);
483
+ this.backupFailedTx({
484
+ id: keccak256(txContext.multicallData),
485
+ failureType: 'send-error',
486
+ request: { to: MULTI_CALL_3_ADDRESS, data: txContext.multicallData },
487
+ blobData: txContext.blobData,
488
+ l1BlockNumber: txContext.l1BlockNumber.toString(),
489
+ error: { message: result.message, name: result.name },
490
+ context: {
491
+ actions: requests.map(r => r.action),
492
+ requests: requests.map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
493
+ sender: this.getSenderAddress().toString(),
494
+ },
495
+ });
396
496
  return { failedActions: requests.map(r => r.action) };
397
497
  } else {
398
498
  this.log.verbose(`Published bundled transactions (${actionsListStr})`, { result, requests });
@@ -405,6 +505,30 @@ export class SequencerPublisher {
405
505
  failedActions.push(request.action);
406
506
  }
407
507
  }
508
+ // Single backup for the whole reverted tx
509
+ if (failedActions.length > 0 && result?.receipt?.status === 'reverted') {
510
+ this.backupFailedTx({
511
+ id: result.receipt.transactionHash,
512
+ failureType: 'revert',
513
+ request: { to: MULTI_CALL_3_ADDRESS, data: txContext.multicallData },
514
+ blobData: txContext.blobData,
515
+ l1BlockNumber: result.receipt.blockNumber.toString(),
516
+ receipt: {
517
+ transactionHash: result.receipt.transactionHash,
518
+ blockNumber: result.receipt.blockNumber.toString(),
519
+ gasUsed: (result.receipt.gasUsed ?? 0n).toString(),
520
+ status: 'reverted',
521
+ },
522
+ error: { message: result.errorMsg ?? 'Transaction reverted' },
523
+ context: {
524
+ actions: failedActions,
525
+ requests: requests
526
+ .filter(r => failedActions.includes(r.action))
527
+ .map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
528
+ sender: this.getSenderAddress().toString(),
529
+ },
530
+ });
531
+ }
408
532
  return { successfulActions, failedActions };
409
533
  }
410
534
  }
@@ -516,8 +640,15 @@ export class SequencerPublisher {
516
640
  const request = this.buildInvalidateCheckpointRequest(validationResult);
517
641
  this.log.debug(`Simulating invalidate checkpoint ${checkpointNumber}`, { ...logData, request });
518
642
 
643
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
644
+
519
645
  try {
520
- const { gasUsed } = await this.l1TxUtils.simulate(request, undefined, undefined, ErrorsAbi);
646
+ const { gasUsed } = await this.l1TxUtils.simulate(
647
+ request,
648
+ undefined,
649
+ undefined,
650
+ mergeAbis([request.abi ?? [], ErrorsAbi]),
651
+ );
521
652
  this.log.verbose(`Simulation for invalidate checkpoint ${checkpointNumber} succeeded`, {
522
653
  ...logData,
523
654
  request,
@@ -536,7 +667,7 @@ export class SequencerPublisher {
536
667
 
537
668
  // If the error is due to the checkpoint not being in the pending chain, and it was indeed removed by someone else,
538
669
  // we can safely ignore it and return undefined so we go ahead with checkpoint building.
539
- if (viemError.message?.includes('Rollup__BlockNotInPendingChain')) {
670
+ if (viemError.message?.includes('Rollup__CheckpointNotInPendingChain')) {
540
671
  this.log.verbose(
541
672
  `Simulation for invalidate checkpoint ${checkpointNumber} failed due to checkpoint not being in pending chain`,
542
673
  { ...logData, request, error: viemError.message },
@@ -562,6 +693,18 @@ export class SequencerPublisher {
562
693
 
563
694
  // Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
564
695
  this.log.error(`Simulation for invalidate checkpoint ${checkpointNumber} failed`, viemError, logData);
696
+ this.backupFailedTx({
697
+ id: keccak256(request.data!),
698
+ failureType: 'simulation',
699
+ request: { to: request.to!, data: request.data!, value: request.value?.toString() },
700
+ l1BlockNumber: l1BlockNumber.toString(),
701
+ error: { message: viemError.message, name: viemError.name },
702
+ context: {
703
+ actions: [`invalidate-${reason}`],
704
+ checkpointNumber,
705
+ sender: this.getSenderAddress().toString(),
706
+ },
707
+ });
565
708
  throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, { cause: viemError });
566
709
  }
567
710
  }
@@ -607,24 +750,8 @@ export class SequencerPublisher {
607
750
  options: { forcePendingCheckpointNumber?: CheckpointNumber },
608
751
  ): Promise<bigint> {
609
752
  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
-
626
753
  const blobFields = checkpoint.toBlobFields();
627
- const blobs = getBlobsPerL1Block(blobFields);
754
+ const blobs = await getBlobsPerL1Block(blobFields);
628
755
  const blobInput = getPrefixedEthBlobCommitments(blobs);
629
756
 
630
757
  const args = [
@@ -632,7 +759,7 @@ export class SequencerPublisher {
632
759
  header: checkpoint.header.toViem(),
633
760
  archive: toHex(checkpoint.archive.root.toBuffer()),
634
761
  oracleInput: {
635
- feeAssetPriceModifier: 0n,
762
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
636
763
  },
637
764
  },
638
765
  attestationsAndSigners.getPackedAttestations(),
@@ -681,6 +808,32 @@ export class SequencerPublisher {
681
808
  return false;
682
809
  }
683
810
 
811
+ // Check if payload was already submitted to governance
812
+ const cacheKey = payload.toString();
813
+ if (!this.payloadProposedCache.has(cacheKey)) {
814
+ try {
815
+ const l1StartBlock = await this.rollupContract.getL1StartBlock();
816
+ const proposed = await retry(
817
+ () => base.hasPayloadBeenProposed(payload.toString(), l1StartBlock),
818
+ 'Check if payload was proposed',
819
+ makeBackoff([0, 1, 2]),
820
+ this.log,
821
+ true,
822
+ );
823
+ if (proposed) {
824
+ this.payloadProposedCache.add(cacheKey);
825
+ }
826
+ } catch (err) {
827
+ this.log.warn(`Failed to check if payload ${payload} was proposed after retries, skipping signal`, err);
828
+ return false;
829
+ }
830
+ }
831
+
832
+ if (this.payloadProposedCache.has(cacheKey)) {
833
+ this.log.info(`Payload ${payload} was already proposed to governance, stopping signals`);
834
+ return false;
835
+ }
836
+
684
837
  const cachedLastVote = this.lastActions[signalType];
685
838
  this.lastActions[signalType] = slotNumber;
686
839
  const action = signalType;
@@ -699,11 +852,26 @@ export class SequencerPublisher {
699
852
  lastValidL2Slot: slotNumber,
700
853
  });
701
854
 
855
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
856
+
702
857
  try {
703
- await this.l1TxUtils.simulate(request, { time: timestamp }, [], ErrorsAbi);
858
+ await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
704
859
  this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
705
860
  } catch (err) {
706
- this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, err);
861
+ const viemError = formatViemError(err);
862
+ this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, viemError);
863
+ this.backupFailedTx({
864
+ id: keccak256(request.data!),
865
+ failureType: 'simulation',
866
+ request: { to: request.to!, data: request.data!, value: request.value?.toString() },
867
+ l1BlockNumber: l1BlockNumber.toString(),
868
+ error: { message: viemError.message, name: viemError.name },
869
+ context: {
870
+ actions: [action],
871
+ slot: slotNumber,
872
+ sender: this.getSenderAddress().toString(),
873
+ },
874
+ });
707
875
  // Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
708
876
  }
709
877
 
@@ -908,14 +1076,15 @@ export class SequencerPublisher {
908
1076
  const checkpointHeader = checkpoint.header;
909
1077
 
910
1078
  const blobFields = checkpoint.toBlobFields();
911
- const blobs = getBlobsPerL1Block(blobFields);
1079
+ const blobs = await getBlobsPerL1Block(blobFields);
912
1080
 
913
- const proposeTxArgs = {
1081
+ const proposeTxArgs: L1ProcessArgs = {
914
1082
  header: checkpointHeader,
915
1083
  archive: checkpoint.archive.root.toBuffer(),
916
1084
  blobs,
917
1085
  attestationsAndSigners,
918
1086
  attestationsAndSignersSignature,
1087
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
919
1088
  };
920
1089
 
921
1090
  let ts: bigint;
@@ -998,13 +1167,30 @@ export class SequencerPublisher {
998
1167
 
999
1168
  this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
1000
1169
 
1170
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1171
+
1001
1172
  let gasUsed: bigint;
1173
+ const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
1002
1174
  try {
1003
- ({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], ErrorsAbi)); // TODO(palla/slash): Check the timestamp logic
1175
+ ({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], simulateAbi)); // TODO(palla/slash): Check the timestamp logic
1004
1176
  this.log.verbose(`Simulation for ${action} succeeded`, { ...logData, request, gasUsed });
1005
1177
  } catch (err) {
1006
- const viemError = formatViemError(err);
1178
+ const viemError = formatViemError(err, simulateAbi);
1007
1179
  this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
1180
+
1181
+ this.backupFailedTx({
1182
+ id: keccak256(request.data!),
1183
+ failureType: 'simulation',
1184
+ request: { to: request.to!, data: request.data!, value: request.value?.toString() },
1185
+ l1BlockNumber: l1BlockNumber.toString(),
1186
+ error: { message: viemError.message, name: viemError.name },
1187
+ context: {
1188
+ actions: [action],
1189
+ slot: slotNumber,
1190
+ sender: this.getSenderAddress().toString(),
1191
+ },
1192
+ });
1193
+
1008
1194
  return false;
1009
1195
  }
1010
1196
 
@@ -1012,10 +1198,14 @@ export class SequencerPublisher {
1012
1198
  const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil((Number(gasUsed) * 64) / 63)));
1013
1199
  logData.gasLimit = gasLimit;
1014
1200
 
1201
+ // Store the ABI used for simulation on the request so Multicall3.forward can decode errors
1202
+ // when the tx is sent and a revert is diagnosed via simulation.
1203
+ const requestWithAbi = { ...request, abi: simulateAbi };
1204
+
1015
1205
  this.log.debug(`Enqueuing ${action}`, logData);
1016
1206
  this.addRequest({
1017
1207
  action,
1018
- request,
1208
+ request: requestWithAbi,
1019
1209
  gasConfig: { gasLimit },
1020
1210
  lastValidL2Slot: slotNumber,
1021
1211
  checkSuccess: (_req, result) => {
@@ -1084,9 +1274,27 @@ export class SequencerPublisher {
1084
1274
  kzg,
1085
1275
  },
1086
1276
  )
1087
- .catch(err => {
1088
- const { message, metaMessages } = formatViemError(err);
1089
- this.log.error(`Failed to validate blobs`, message, { metaMessages });
1277
+ .catch(async err => {
1278
+ const viemError = formatViemError(err);
1279
+ this.log.error(`Failed to validate blobs`, viemError.message, { metaMessages: viemError.metaMessages });
1280
+ const validateBlobsData = encodeFunctionData({
1281
+ abi: RollupAbi,
1282
+ functionName: 'validateBlobs',
1283
+ args: [blobInput],
1284
+ });
1285
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1286
+ this.backupFailedTx({
1287
+ id: keccak256(validateBlobsData),
1288
+ failureType: 'simulation',
1289
+ request: { to: this.rollupContract.address as Hex, data: validateBlobsData },
1290
+ blobData: encodedData.blobs.map(b => toHex(b.data)) as Hex[],
1291
+ l1BlockNumber: l1BlockNumber.toString(),
1292
+ error: { message: viemError.message, name: viemError.name },
1293
+ context: {
1294
+ actions: ['validate-blobs'],
1295
+ sender: this.getSenderAddress().toString(),
1296
+ },
1297
+ });
1090
1298
  throw new Error('Failed to validate blobs');
1091
1299
  });
1092
1300
  }
@@ -1097,8 +1305,7 @@ export class SequencerPublisher {
1097
1305
  header: encodedData.header.toViem(),
1098
1306
  archive: toHex(encodedData.archive),
1099
1307
  oracleInput: {
1100
- // We are currently not modifying these. See #9963
1101
- feeAssetPriceModifier: 0n,
1308
+ feeAssetPriceModifier: encodedData.feeAssetPriceModifier,
1102
1309
  },
1103
1310
  },
1104
1311
  encodedData.attestationsAndSigners.getPackedAttestations(),
@@ -1124,7 +1331,7 @@ export class SequencerPublisher {
1124
1331
  readonly header: ViemHeader;
1125
1332
  readonly archive: `0x${string}`;
1126
1333
  readonly oracleInput: {
1127
- readonly feeAssetPriceModifier: 0n;
1334
+ readonly feeAssetPriceModifier: bigint;
1128
1335
  };
1129
1336
  },
1130
1337
  ViemCommitteeAttestations,
@@ -1166,25 +1373,27 @@ export class SequencerPublisher {
1166
1373
  });
1167
1374
  }
1168
1375
 
1376
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1377
+
1169
1378
  const simulationResult = await this.l1TxUtils
1170
1379
  .simulate(
1171
1380
  {
1172
1381
  to: this.rollupContract.address,
1173
1382
  data: rollupData,
1174
- gas: SequencerPublisher.PROPOSE_GAS_GUESS,
1383
+ gas: MAX_L1_TX_LIMIT,
1175
1384
  ...(this.proposerAddressForSimulation && { from: this.proposerAddressForSimulation.toString() }),
1176
1385
  },
1177
1386
  {
1178
1387
  // @note we add 1n to the timestamp because geth implementation doesn't like simulation timestamp to be equal to the current block timestamp
1179
1388
  time: timestamp + 1n,
1180
1389
  // @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,
1390
+ gasLimit: MAX_L1_TX_LIMIT * 2n,
1182
1391
  },
1183
1392
  stateOverrides,
1184
1393
  RollupAbi,
1185
1394
  {
1186
1395
  // @note fallback gas estimate to use if the node doesn't support simulation API
1187
- fallbackGasEstimate: SequencerPublisher.PROPOSE_GAS_GUESS,
1396
+ fallbackGasEstimate: MAX_L1_TX_LIMIT,
1188
1397
  },
1189
1398
  )
1190
1399
  .catch(err => {
@@ -1194,11 +1403,23 @@ export class SequencerPublisher {
1194
1403
  this.log.debug(`Ignoring expected ValidatorSelection__MissingProposerSignature error in fisherman mode`);
1195
1404
  // Return a minimal simulation result with the fallback gas estimate
1196
1405
  return {
1197
- gasUsed: SequencerPublisher.PROPOSE_GAS_GUESS,
1406
+ gasUsed: MAX_L1_TX_LIMIT,
1198
1407
  logs: [],
1199
1408
  };
1200
1409
  }
1201
1410
  this.log.error(`Failed to simulate propose tx`, viemError);
1411
+ this.backupFailedTx({
1412
+ id: keccak256(rollupData),
1413
+ failureType: 'simulation',
1414
+ request: { to: this.rollupContract.address, data: rollupData },
1415
+ l1BlockNumber: l1BlockNumber.toString(),
1416
+ error: { message: viemError.message, name: viemError.name },
1417
+ context: {
1418
+ actions: ['propose'],
1419
+ slot: Number(args[0].header.slotNumber),
1420
+ sender: this.getSenderAddress().toString(),
1421
+ },
1422
+ });
1202
1423
  throw err;
1203
1424
  });
1204
1425