@aztec/sequencer-client 0.0.1-commit.03f7ef2 → 0.0.1-commit.04852196a

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 (106) hide show
  1. package/dest/client/sequencer-client.d.ts +26 -11
  2. package/dest/client/sequencer-client.d.ts.map +1 -1
  3. package/dest/client/sequencer-client.js +99 -16
  4. package/dest/config.d.ts +24 -6
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +45 -28
  7. package/dest/global_variable_builder/global_builder.d.ts +5 -7
  8. package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
  9. package/dest/global_variable_builder/global_builder.js +13 -13
  10. package/dest/index.d.ts +2 -3
  11. package/dest/index.d.ts.map +1 -1
  12. package/dest/index.js +1 -2
  13. package/dest/publisher/config.d.ts +35 -17
  14. package/dest/publisher/config.d.ts.map +1 -1
  15. package/dest/publisher/config.js +106 -42
  16. package/dest/publisher/index.d.ts +2 -1
  17. package/dest/publisher/index.d.ts.map +1 -1
  18. package/dest/publisher/l1_tx_failed_store/factory.d.ts +11 -0
  19. package/dest/publisher/l1_tx_failed_store/factory.d.ts.map +1 -0
  20. package/dest/publisher/l1_tx_failed_store/factory.js +22 -0
  21. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts +59 -0
  22. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -0
  23. package/dest/publisher/l1_tx_failed_store/failed_tx_store.js +1 -0
  24. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts +15 -0
  25. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts.map +1 -0
  26. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.js +34 -0
  27. package/dest/publisher/l1_tx_failed_store/index.d.ts +4 -0
  28. package/dest/publisher/l1_tx_failed_store/index.d.ts.map +1 -0
  29. package/dest/publisher/l1_tx_failed_store/index.js +2 -0
  30. package/dest/publisher/sequencer-publisher-factory.d.ts +12 -4
  31. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  32. package/dest/publisher/sequencer-publisher-factory.js +27 -2
  33. package/dest/publisher/sequencer-publisher-metrics.d.ts +1 -1
  34. package/dest/publisher/sequencer-publisher-metrics.d.ts.map +1 -1
  35. package/dest/publisher/sequencer-publisher-metrics.js +23 -86
  36. package/dest/publisher/sequencer-publisher.d.ts +44 -25
  37. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  38. package/dest/publisher/sequencer-publisher.js +781 -101
  39. package/dest/sequencer/checkpoint_proposal_job.d.ts +39 -13
  40. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  41. package/dest/sequencer/checkpoint_proposal_job.js +683 -79
  42. package/dest/sequencer/checkpoint_voter.d.ts +3 -2
  43. package/dest/sequencer/checkpoint_voter.d.ts.map +1 -1
  44. package/dest/sequencer/checkpoint_voter.js +34 -10
  45. package/dest/sequencer/index.d.ts +1 -3
  46. package/dest/sequencer/index.d.ts.map +1 -1
  47. package/dest/sequencer/index.js +0 -2
  48. package/dest/sequencer/metrics.d.ts +19 -7
  49. package/dest/sequencer/metrics.d.ts.map +1 -1
  50. package/dest/sequencer/metrics.js +131 -141
  51. package/dest/sequencer/sequencer.d.ts +46 -23
  52. package/dest/sequencer/sequencer.d.ts.map +1 -1
  53. package/dest/sequencer/sequencer.js +514 -67
  54. package/dest/sequencer/timetable.d.ts +4 -6
  55. package/dest/sequencer/timetable.d.ts.map +1 -1
  56. package/dest/sequencer/timetable.js +7 -11
  57. package/dest/sequencer/types.d.ts +5 -2
  58. package/dest/sequencer/types.d.ts.map +1 -1
  59. package/dest/test/index.d.ts +4 -7
  60. package/dest/test/index.d.ts.map +1 -1
  61. package/dest/test/mock_checkpoint_builder.d.ts +28 -16
  62. package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
  63. package/dest/test/mock_checkpoint_builder.js +86 -34
  64. package/dest/test/utils.d.ts +13 -9
  65. package/dest/test/utils.d.ts.map +1 -1
  66. package/dest/test/utils.js +27 -17
  67. package/package.json +30 -28
  68. package/src/client/sequencer-client.ts +139 -23
  69. package/src/config.ts +59 -38
  70. package/src/global_variable_builder/global_builder.ts +14 -14
  71. package/src/index.ts +1 -9
  72. package/src/publisher/config.ts +121 -43
  73. package/src/publisher/index.ts +3 -0
  74. package/src/publisher/l1_tx_failed_store/factory.ts +32 -0
  75. package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +55 -0
  76. package/src/publisher/l1_tx_failed_store/file_store_failed_tx_store.ts +46 -0
  77. package/src/publisher/l1_tx_failed_store/index.ts +3 -0
  78. package/src/publisher/sequencer-publisher-factory.ts +39 -7
  79. package/src/publisher/sequencer-publisher-metrics.ts +17 -69
  80. package/src/publisher/sequencer-publisher.ts +420 -137
  81. package/src/sequencer/checkpoint_proposal_job.ts +361 -104
  82. package/src/sequencer/checkpoint_voter.ts +32 -7
  83. package/src/sequencer/index.ts +0 -2
  84. package/src/sequencer/metrics.ts +132 -148
  85. package/src/sequencer/sequencer.ts +160 -69
  86. package/src/sequencer/timetable.ts +13 -12
  87. package/src/sequencer/types.ts +4 -1
  88. package/src/test/index.ts +3 -6
  89. package/src/test/mock_checkpoint_builder.ts +147 -71
  90. package/src/test/utils.ts +58 -28
  91. package/dest/sequencer/block_builder.d.ts +0 -26
  92. package/dest/sequencer/block_builder.d.ts.map +0 -1
  93. package/dest/sequencer/block_builder.js +0 -129
  94. package/dest/sequencer/checkpoint_builder.d.ts +0 -63
  95. package/dest/sequencer/checkpoint_builder.d.ts.map +0 -1
  96. package/dest/sequencer/checkpoint_builder.js +0 -131
  97. package/dest/tx_validator/nullifier_cache.d.ts +0 -14
  98. package/dest/tx_validator/nullifier_cache.d.ts.map +0 -1
  99. package/dest/tx_validator/nullifier_cache.js +0 -24
  100. package/dest/tx_validator/tx_validator_factory.d.ts +0 -18
  101. package/dest/tx_validator/tx_validator_factory.d.ts.map +0 -1
  102. package/dest/tx_validator/tx_validator_factory.js +0 -53
  103. package/src/sequencer/block_builder.ts +0 -217
  104. package/src/sequencer/checkpoint_builder.ts +0 -217
  105. package/src/tx_validator/nullifier_cache.ts +0 -30
  106. package/src/tx_validator/tx_validator_factory.ts +0 -133
@@ -1,9 +1,10 @@
1
- import { type BlobClientInterface, createBlobClient } from '@aztec/blob-client/client';
1
+ import type { BlobClientInterface } from '@aztec/blob-client/client';
2
2
  import { Blob, getBlobsPerL1Block, getPrefixedEthBlobCommitments } from '@aztec/blob-lib';
3
3
  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,33 +19,46 @@ 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
- import { BlockNumber, CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
30
+ import { CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
29
31
  import { pick } from '@aztec/foundation/collection';
30
32
  import type { Fr } from '@aztec/foundation/curves/bn254';
33
+ import { TimeoutError } from '@aztec/foundation/error';
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
- import { CommitteeAttestationsAndSigners, type ValidateBlockResult } from '@aztec/stdlib/block';
42
+ import { CommitteeAttestationsAndSigners, type ValidateCheckpointResult } from '@aztec/stdlib/block';
39
43
  import type { Checkpoint } from '@aztec/stdlib/checkpoint';
40
44
  import { SlashFactoryContract } from '@aztec/stdlib/l1-contracts';
41
45
  import type { CheckpointHeader } from '@aztec/stdlib/rollup';
42
46
  import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
43
- import { type TelemetryClient, getTelemetryClient } from '@aztec/telemetry-client';
47
+ import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
44
48
 
45
- import { type StateOverride, type TransactionReceipt, type TypedDataDefinition, encodeFunctionData, toHex } from 'viem';
46
-
47
- import type { PublisherConfig, TxSenderConfig } from './config.js';
49
+ import {
50
+ type Hex,
51
+ type StateOverride,
52
+ type TransactionReceipt,
53
+ type TypedDataDefinition,
54
+ encodeFunctionData,
55
+ keccak256,
56
+ multicall3Abi,
57
+ toHex,
58
+ } from 'viem';
59
+
60
+ import type { SequencerPublisherConfig } from './config.js';
61
+ import { type FailedL1Tx, type L1TxFailedStore, createL1TxFailedStore } from './l1_tx_failed_store/index.js';
48
62
  import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
49
63
 
50
64
  /** Arguments to the process method of the rollup contract */
@@ -59,6 +73,8 @@ type L1ProcessArgs = {
59
73
  attestationsAndSigners: CommitteeAttestationsAndSigners;
60
74
  /** Attestations and signers signature */
61
75
  attestationsAndSignersSignature: Signature;
76
+ /** The fee asset price modifier in basis points (from oracle) */
77
+ feeAssetPriceModifier: bigint;
62
78
  };
63
79
 
64
80
  export const Actions = [
@@ -80,12 +96,12 @@ type GovernanceSignalAction = Extract<Action, 'governance-signal' | 'empire-slas
80
96
  // Sorting for actions such that invalidations go before proposals, and proposals go before votes
81
97
  export const compareActions = (a: Action, b: Action) => Actions.indexOf(a) - Actions.indexOf(b);
82
98
 
83
- export type InvalidateBlockRequest = {
99
+ export type InvalidateCheckpointRequest = {
84
100
  request: L1TxRequest;
85
101
  reason: 'invalid-attestation' | 'insufficient-attestations';
86
102
  gasUsed: bigint;
87
- blockNumber: BlockNumber;
88
- forcePendingBlockNumber: BlockNumber;
103
+ checkpointNumber: CheckpointNumber;
104
+ forcePendingCheckpointNumber: CheckpointNumber;
89
105
  };
90
106
 
91
107
  interface RequestWithExpiry {
@@ -104,6 +120,7 @@ export class SequencerPublisher {
104
120
  private interrupted = false;
105
121
  private metrics: SequencerPublisherMetrics;
106
122
  public epochCache: EpochCache;
123
+ private failedTxStore?: Promise<L1TxFailedStore | undefined>;
107
124
 
108
125
  protected governanceLog = createLogger('sequencer:publisher:governance');
109
126
  protected slashingLog = createLogger('sequencer:publisher:slashing');
@@ -111,6 +128,7 @@ export class SequencerPublisher {
111
128
  protected lastActions: Partial<Record<Action, SlotNumber>> = {};
112
129
 
113
130
  private isPayloadEmptyCache: Map<string, boolean> = new Map<string, boolean>();
131
+ private payloadProposedCache: Set<string> = new Set<string>();
114
132
 
115
133
  protected log: Logger;
116
134
  protected ethereumSlotDuration: bigint;
@@ -120,12 +138,14 @@ export class SequencerPublisher {
120
138
  /** Address to use for simulations in fisherman mode (actual proposer's address) */
121
139
  private proposerAddressForSimulation?: EthAddress;
122
140
 
141
+ /** Optional callback to obtain a replacement publisher when the current one fails to send. */
142
+ private getNextPublisher?: (excludeAddresses: EthAddress[]) => Promise<L1TxUtils | undefined>;
143
+
123
144
  /** L1 fee analyzer for fisherman mode */
124
145
  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;
146
+
147
+ /** Fee asset price oracle for computing price modifiers from Uniswap V4 */
148
+ private feeAssetPriceOracle: FeeAssetPriceOracle;
129
149
 
130
150
  // A CALL to a cold address is 2700 gas
131
151
  public static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
@@ -133,20 +153,23 @@ export class SequencerPublisher {
133
153
  // Gas report for VotingWithSigTest shows a max gas of 100k, but we've seen it cost 700k+ in testnet
134
154
  public static VOTE_GAS_GUESS: bigint = 800_000n;
135
155
 
136
- public l1TxUtils: L1TxUtilsWithBlobs;
156
+ public l1TxUtils: L1TxUtils;
137
157
  public rollupContract: RollupContract;
138
158
  public govProposerContract: GovernanceProposerContract;
139
159
  public slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
140
160
  public slashFactoryContract: SlashFactoryContract;
141
161
 
162
+ public readonly tracer: Tracer;
163
+
142
164
  protected requests: RequestWithExpiry[] = [];
143
165
 
144
166
  constructor(
145
- private config: TxSenderConfig & PublisherConfig & Pick<L1ContractsConfig, 'ethereumSlotDuration'>,
167
+ private config: Pick<SequencerPublisherConfig, 'fishermanMode' | 'l1TxFailedStore'> &
168
+ Pick<L1ContractsConfig, 'ethereumSlotDuration'> & { l1ChainId: number },
146
169
  deps: {
147
170
  telemetry?: TelemetryClient;
148
- blobClient?: BlobClientInterface;
149
- l1TxUtils: L1TxUtilsWithBlobs;
171
+ blobClient: BlobClientInterface;
172
+ l1TxUtils: L1TxUtils;
150
173
  rollupContract: RollupContract;
151
174
  slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
152
175
  governanceProposerContract: GovernanceProposerContract;
@@ -156,6 +179,7 @@ export class SequencerPublisher {
156
179
  metrics: SequencerPublisherMetrics;
157
180
  lastActions: Partial<Record<Action, SlotNumber>>;
158
181
  log?: Logger;
182
+ getNextPublisher?: (excludeAddresses: EthAddress[]) => Promise<L1TxUtils | undefined>;
159
183
  },
160
184
  ) {
161
185
  this.log = deps.log ?? createLogger('sequencer:publisher');
@@ -163,12 +187,13 @@ export class SequencerPublisher {
163
187
  this.epochCache = deps.epochCache;
164
188
  this.lastActions = deps.lastActions;
165
189
 
166
- this.blobClient =
167
- deps.blobClient ?? createBlobClient(config, { logger: createLogger('sequencer:blob-client:client') });
190
+ this.blobClient = deps.blobClient;
168
191
 
169
192
  const telemetry = deps.telemetry ?? getTelemetryClient();
170
193
  this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
194
+ this.tracer = telemetry.getTracer('SequencerPublisher');
171
195
  this.l1TxUtils = deps.l1TxUtils;
196
+ this.getNextPublisher = deps.getNextPublisher;
172
197
 
173
198
  this.rollupContract = deps.rollupContract;
174
199
 
@@ -190,12 +215,52 @@ export class SequencerPublisher {
190
215
  createLogger('sequencer:publisher:fee-analyzer'),
191
216
  );
192
217
  }
218
+
219
+ // Initialize fee asset price oracle
220
+ this.feeAssetPriceOracle = new FeeAssetPriceOracle(
221
+ this.l1TxUtils.client,
222
+ this.rollupContract,
223
+ createLogger('sequencer:publisher:price-oracle'),
224
+ );
225
+
226
+ // Initialize failed L1 tx store (optional, for test networks)
227
+ this.failedTxStore = createL1TxFailedStore(config.l1TxFailedStore, this.log);
228
+ }
229
+
230
+ /**
231
+ * Backs up a failed L1 transaction to the configured store for debugging.
232
+ * Does nothing if no store is configured.
233
+ */
234
+ private backupFailedTx(failedTx: Omit<FailedL1Tx, 'timestamp'>): void {
235
+ if (!this.failedTxStore) {
236
+ return;
237
+ }
238
+
239
+ const tx: FailedL1Tx = {
240
+ ...failedTx,
241
+ timestamp: Date.now(),
242
+ };
243
+
244
+ // Fire and forget - don't block on backup
245
+ void this.failedTxStore
246
+ .then(store => store?.saveFailedTx(tx))
247
+ .catch(err => {
248
+ this.log.warn(`Failed to backup failed L1 tx to store`, err);
249
+ });
193
250
  }
194
251
 
195
252
  public getRollupContract(): RollupContract {
196
253
  return this.rollupContract;
197
254
  }
198
255
 
256
+ /**
257
+ * Gets the fee asset price modifier from the oracle.
258
+ * Returns 0n if the oracle query fails.
259
+ */
260
+ public getFeeAssetPriceModifier(): Promise<bigint> {
261
+ return this.feeAssetPriceOracle.computePriceModifier();
262
+ }
263
+
199
264
  public getSenderAddress() {
200
265
  return this.l1TxUtils.getSenderAddress();
201
266
  }
@@ -271,7 +336,7 @@ export class SequencerPublisher {
271
336
  // Start the analysis
272
337
  const analysisId = await this.l1FeeAnalyzer.startAnalysis(
273
338
  l2SlotNumber,
274
- gasLimit > 0n ? gasLimit : SequencerPublisher.PROPOSE_GAS_GUESS,
339
+ gasLimit > 0n ? gasLimit : MAX_L1_TX_LIMIT,
275
340
  l1Requests,
276
341
  blobConfig,
277
342
  onComplete,
@@ -297,6 +362,7 @@ export class SequencerPublisher {
297
362
  * - a receipt and errorMsg if it failed on L1
298
363
  * - undefined if no valid requests are found OR the tx failed to send.
299
364
  */
365
+ @trackSpan('SequencerPublisher.sendRequests')
300
366
  public async sendRequests() {
301
367
  const requestsToProcess = [...this.requests];
302
368
  this.requests = [];
@@ -343,7 +409,16 @@ export class SequencerPublisher {
343
409
 
344
410
  // Merge gasConfigs. Yields the sum of gasLimits, and the earliest txTimeoutAt, or undefined if no gasConfig sets them.
345
411
  const gasLimits = gasConfigs.map(g => g?.gasLimit).filter((g): g is bigint => g !== undefined);
346
- const gasLimit = gasLimits.length > 0 ? sumBigint(gasLimits) : undefined; // sum
412
+ let gasLimit = gasLimits.length > 0 ? sumBigint(gasLimits) : undefined; // sum
413
+ // Cap at L1 block gas limit so the node accepts the tx ("gas limit too high" otherwise).
414
+ const maxGas = MAX_L1_TX_LIMIT;
415
+ if (gasLimit !== undefined && gasLimit > maxGas) {
416
+ this.log.debug('Capping bundled tx gas limit to L1 max', {
417
+ requested: gasLimit,
418
+ capped: maxGas,
419
+ });
420
+ gasLimit = maxGas;
421
+ }
347
422
  const txTimeoutAts = gasConfigs.map(g => g?.txTimeoutAt).filter((g): g is Date => g !== undefined);
348
423
  const txTimeoutAt = txTimeoutAts.length > 0 ? new Date(Math.min(...txTimeoutAts.map(g => g.getTime()))) : undefined; // earliest
349
424
  const txConfig: RequestWithExpiry['gasConfig'] = { gasLimit, txTimeoutAt };
@@ -353,19 +428,36 @@ export class SequencerPublisher {
353
428
  validRequests.sort((a, b) => compareActions(a.action, b.action));
354
429
 
355
430
  try {
431
+ // Capture context for failed tx backup before sending
432
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
433
+ const multicallData = encodeFunctionData({
434
+ abi: multicall3Abi,
435
+ functionName: 'aggregate3',
436
+ args: [
437
+ validRequests.map(r => ({
438
+ target: r.request.to!,
439
+ callData: r.request.data!,
440
+ allowFailure: true,
441
+ })),
442
+ ],
443
+ });
444
+ const blobDataHex = blobConfig?.blobs?.map(b => toHex(b)) as Hex[] | undefined;
445
+
446
+ const txContext = { multicallData, blobData: blobDataHex, l1BlockNumber };
447
+
356
448
  this.log.debug('Forwarding transactions', {
357
449
  validRequests: validRequests.map(request => request.action),
358
450
  txConfig,
359
451
  });
360
- const result = await Multicall3.forward(
361
- validRequests.map(request => request.request),
362
- this.l1TxUtils,
363
- txConfig,
364
- blobConfig,
365
- this.rollupContract.address,
366
- this.log,
452
+ const result = await this.forwardWithPublisherRotation(validRequests, txConfig, blobConfig);
453
+ if (result === undefined) {
454
+ return undefined;
455
+ }
456
+ const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(
457
+ validRequests,
458
+ result,
459
+ txContext,
367
460
  );
368
- const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(validRequests, result);
369
461
  return { result, expiredActions, sentActions: validActions, successfulActions, failedActions };
370
462
  } catch (err) {
371
463
  const viemError = formatViemError(err);
@@ -383,13 +475,76 @@ export class SequencerPublisher {
383
475
  }
384
476
  }
385
477
 
478
+ /**
479
+ * Forwards transactions via Multicall3, rotating to the next available publisher if a send
480
+ * failure occurs (i.e. the tx never reached the chain).
481
+ * On-chain reverts and simulation errors are returned as-is without rotation.
482
+ */
483
+ private async forwardWithPublisherRotation(
484
+ validRequests: RequestWithExpiry[],
485
+ txConfig: RequestWithExpiry['gasConfig'],
486
+ blobConfig: L1BlobInputs | undefined,
487
+ ) {
488
+ const triedAddresses: EthAddress[] = [];
489
+ let currentPublisher = this.l1TxUtils;
490
+
491
+ while (true) {
492
+ triedAddresses.push(currentPublisher.getSenderAddress());
493
+ try {
494
+ const result = await Multicall3.forward(
495
+ validRequests.map(r => r.request),
496
+ currentPublisher,
497
+ txConfig,
498
+ blobConfig,
499
+ this.rollupContract.address,
500
+ this.log,
501
+ );
502
+ this.l1TxUtils = currentPublisher;
503
+ return result;
504
+ } catch (err) {
505
+ if (err instanceof TimeoutError) {
506
+ throw err;
507
+ }
508
+ const viemError = formatViemError(err);
509
+ if (!this.getNextPublisher) {
510
+ this.log.error('Failed to publish bundled transactions', viemError);
511
+ return undefined;
512
+ }
513
+ this.log.warn(
514
+ `Publisher ${currentPublisher.getSenderAddress()} failed to send, rotating to next publisher`,
515
+ viemError,
516
+ );
517
+ const nextPublisher = await this.getNextPublisher([...triedAddresses]);
518
+ if (!nextPublisher) {
519
+ this.log.error('All available publishers exhausted, failed to publish bundled transactions');
520
+ return undefined;
521
+ }
522
+ currentPublisher = nextPublisher;
523
+ }
524
+ }
525
+ }
526
+
386
527
  private callbackBundledTransactions(
387
528
  requests: RequestWithExpiry[],
388
- result?: { receipt: TransactionReceipt } | FormattedViemError,
529
+ result: { receipt: TransactionReceipt; errorMsg?: string } | FormattedViemError | undefined,
530
+ txContext: { multicallData: Hex; blobData?: Hex[]; l1BlockNumber: bigint },
389
531
  ) {
390
532
  const actionsListStr = requests.map(r => r.action).join(', ');
391
533
  if (result instanceof FormattedViemError) {
392
534
  this.log.error(`Failed to publish bundled transactions (${actionsListStr})`, result);
535
+ this.backupFailedTx({
536
+ id: keccak256(txContext.multicallData),
537
+ failureType: 'send-error',
538
+ request: { to: MULTI_CALL_3_ADDRESS, data: txContext.multicallData },
539
+ blobData: txContext.blobData,
540
+ l1BlockNumber: txContext.l1BlockNumber.toString(),
541
+ error: { message: result.message, name: result.name },
542
+ context: {
543
+ actions: requests.map(r => r.action),
544
+ requests: requests.map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
545
+ sender: this.getSenderAddress().toString(),
546
+ },
547
+ });
393
548
  return { failedActions: requests.map(r => r.action) };
394
549
  } else {
395
550
  this.log.verbose(`Published bundled transactions (${actionsListStr})`, { result, requests });
@@ -402,6 +557,30 @@ export class SequencerPublisher {
402
557
  failedActions.push(request.action);
403
558
  }
404
559
  }
560
+ // Single backup for the whole reverted tx
561
+ if (failedActions.length > 0 && result?.receipt?.status === 'reverted') {
562
+ this.backupFailedTx({
563
+ id: result.receipt.transactionHash,
564
+ failureType: 'revert',
565
+ request: { to: MULTI_CALL_3_ADDRESS, data: txContext.multicallData },
566
+ blobData: txContext.blobData,
567
+ l1BlockNumber: result.receipt.blockNumber.toString(),
568
+ receipt: {
569
+ transactionHash: result.receipt.transactionHash,
570
+ blockNumber: result.receipt.blockNumber.toString(),
571
+ gasUsed: (result.receipt.gasUsed ?? 0n).toString(),
572
+ status: 'reverted',
573
+ },
574
+ error: { message: result.errorMsg ?? 'Transaction reverted' },
575
+ context: {
576
+ actions: failedActions,
577
+ requests: requests
578
+ .filter(r => failedActions.includes(r.action))
579
+ .map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
580
+ sender: this.getSenderAddress().toString(),
581
+ },
582
+ });
583
+ }
405
584
  return { successfulActions, failedActions };
406
585
  }
407
586
  }
@@ -414,17 +593,14 @@ export class SequencerPublisher {
414
593
  public canProposeAtNextEthBlock(
415
594
  tipArchive: Fr,
416
595
  msgSender: EthAddress,
417
- opts: { forcePendingBlockNumber?: BlockNumber } = {},
596
+ opts: { forcePendingCheckpointNumber?: CheckpointNumber } = {},
418
597
  ) {
419
598
  // TODO: #14291 - should loop through multiple keys to check if any of them can propose
420
599
  const ignoredErrors = ['SlotAlreadyInChain', 'InvalidProposer', 'InvalidArchive'];
421
600
 
422
601
  return this.rollupContract
423
602
  .canProposeAtNextEthBlock(tipArchive.toBuffer(), msgSender.toString(), Number(this.ethereumSlotDuration), {
424
- forcePendingCheckpointNumber:
425
- opts.forcePendingBlockNumber !== undefined
426
- ? CheckpointNumber.fromBlockNumber(opts.forcePendingBlockNumber)
427
- : undefined,
603
+ forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
428
604
  })
429
605
  .catch(err => {
430
606
  if (err instanceof FormattedViemError && ignoredErrors.find(e => err.message.includes(e))) {
@@ -443,10 +619,11 @@ export class SequencerPublisher {
443
619
  * It will throw if the block header is invalid.
444
620
  * @param header - The block header to validate
445
621
  */
622
+ @trackSpan('SequencerPublisher.validateBlockHeader')
446
623
  public async validateBlockHeader(
447
624
  header: CheckpointHeader,
448
- opts?: { forcePendingBlockNumber: BlockNumber | undefined },
449
- ) {
625
+ opts?: { forcePendingCheckpointNumber: CheckpointNumber | undefined },
626
+ ): Promise<void> {
450
627
  const flags = { ignoreDA: true, ignoreSignatures: true };
451
628
 
452
629
  const args = [
@@ -455,17 +632,13 @@ export class SequencerPublisher {
455
632
  [], // no signers
456
633
  Signature.empty().toViemSignature(),
457
634
  `0x${'0'.repeat(64)}`, // 32 empty bytes
458
- header.contentCommitment.blobsHash.toString(),
635
+ header.blobsHash.toString(),
459
636
  flags,
460
637
  ] as const;
461
638
 
462
639
  const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
463
- const optsForcePendingCheckpointNumber =
464
- opts?.forcePendingBlockNumber !== undefined
465
- ? CheckpointNumber.fromBlockNumber(opts.forcePendingBlockNumber)
466
- : undefined;
467
640
  const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(
468
- optsForcePendingCheckpointNumber,
641
+ opts?.forcePendingCheckpointNumber,
469
642
  );
470
643
  let balance = 0n;
471
644
  if (this.config.fishermanMode) {
@@ -493,77 +666,109 @@ export class SequencerPublisher {
493
666
  }
494
667
 
495
668
  /**
496
- * Simulate making a call to invalidate a block with invalid attestations. Returns undefined if no need to invalidate.
497
- * @param block - The block to invalidate and the criteria for invalidation (as returned by the archiver)
669
+ * Simulate making a call to invalidate a checkpoint with invalid attestations. Returns undefined if no need to invalidate.
670
+ * @param validationResult - The validation result indicating which checkpoint to invalidate (as returned by the archiver)
498
671
  */
499
- public async simulateInvalidateBlock(
500
- validationResult: ValidateBlockResult,
501
- ): Promise<InvalidateBlockRequest | undefined> {
672
+ public async simulateInvalidateCheckpoint(
673
+ validationResult: ValidateCheckpointResult,
674
+ ): Promise<InvalidateCheckpointRequest | undefined> {
502
675
  if (validationResult.valid) {
503
676
  return undefined;
504
677
  }
505
678
 
506
- const { reason, block } = validationResult;
507
- const blockNumber = block.blockNumber;
508
- const logData = { ...block, reason };
679
+ const { reason, checkpoint } = validationResult;
680
+ const checkpointNumber = checkpoint.checkpointNumber;
681
+ const logData = { ...checkpoint, reason };
509
682
 
510
- const currentBlockNumber = await this.rollupContract.getCheckpointNumber();
511
- if (currentBlockNumber < validationResult.block.blockNumber) {
683
+ const currentCheckpointNumber = await this.rollupContract.getCheckpointNumber();
684
+ if (currentCheckpointNumber < checkpointNumber) {
512
685
  this.log.verbose(
513
- `Skipping block ${blockNumber} invalidation since it has already been removed from the pending chain`,
514
- { currentBlockNumber, ...logData },
686
+ `Skipping checkpoint ${checkpointNumber} invalidation since it has already been removed from the pending chain`,
687
+ { currentCheckpointNumber, ...logData },
515
688
  );
516
689
  return undefined;
517
690
  }
518
691
 
519
- const request = this.buildInvalidateBlockRequest(validationResult);
520
- this.log.debug(`Simulating invalidate block ${blockNumber}`, { ...logData, request });
692
+ const request = this.buildInvalidateCheckpointRequest(validationResult);
693
+ this.log.debug(`Simulating invalidate checkpoint ${checkpointNumber}`, { ...logData, request });
694
+
695
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
521
696
 
522
697
  try {
523
- const { gasUsed } = await this.l1TxUtils.simulate(request, undefined, undefined, ErrorsAbi);
524
- this.log.verbose(`Simulation for invalidate block ${blockNumber} succeeded`, { ...logData, request, gasUsed });
698
+ const { gasUsed } = await this.l1TxUtils.simulate(
699
+ request,
700
+ undefined,
701
+ undefined,
702
+ mergeAbis([request.abi ?? [], ErrorsAbi]),
703
+ );
704
+ this.log.verbose(`Simulation for invalidate checkpoint ${checkpointNumber} succeeded`, {
705
+ ...logData,
706
+ request,
707
+ gasUsed,
708
+ });
525
709
 
526
- return { request, gasUsed, blockNumber, forcePendingBlockNumber: BlockNumber(blockNumber - 1), reason };
710
+ return {
711
+ request,
712
+ gasUsed,
713
+ checkpointNumber,
714
+ forcePendingCheckpointNumber: CheckpointNumber(checkpointNumber - 1),
715
+ reason,
716
+ };
527
717
  } catch (err) {
528
718
  const viemError = formatViemError(err);
529
719
 
530
- // If the error is due to the block not being in the pending chain, and it was indeed removed by someone else,
531
- // we can safely ignore it and return undefined so we go ahead with block building.
532
- if (viemError.message?.includes('Rollup__BlockNotInPendingChain')) {
720
+ // If the error is due to the checkpoint not being in the pending chain, and it was indeed removed by someone else,
721
+ // we can safely ignore it and return undefined so we go ahead with checkpoint building.
722
+ if (viemError.message?.includes('Rollup__CheckpointNotInPendingChain')) {
533
723
  this.log.verbose(
534
- `Simulation for invalidate block ${blockNumber} failed due to block not being in pending chain`,
724
+ `Simulation for invalidate checkpoint ${checkpointNumber} failed due to checkpoint not being in pending chain`,
535
725
  { ...logData, request, error: viemError.message },
536
726
  );
537
- const latestPendingBlockNumber = await this.rollupContract.getCheckpointNumber();
538
- if (latestPendingBlockNumber < blockNumber) {
539
- this.log.verbose(`Block number ${blockNumber} has already been invalidated`, { ...logData });
727
+ const latestPendingCheckpointNumber = await this.rollupContract.getCheckpointNumber();
728
+ if (latestPendingCheckpointNumber < checkpointNumber) {
729
+ this.log.verbose(`Checkpoint ${checkpointNumber} has already been invalidated`, { ...logData });
540
730
  return undefined;
541
731
  } else {
542
732
  this.log.error(
543
- `Simulation for invalidate ${blockNumber} failed and it is still in pending chain`,
733
+ `Simulation for invalidate checkpoint ${checkpointNumber} failed and it is still in pending chain`,
544
734
  viemError,
545
735
  logData,
546
736
  );
547
- throw new Error(`Failed to simulate invalidate block ${blockNumber} while it is still in pending chain`, {
548
- cause: viemError,
549
- });
737
+ throw new Error(
738
+ `Failed to simulate invalidate checkpoint ${checkpointNumber} while it is still in pending chain`,
739
+ {
740
+ cause: viemError,
741
+ },
742
+ );
550
743
  }
551
744
  }
552
745
 
553
- // Otherwise, throw. We cannot build the next block if we cannot invalidate the previous one.
554
- this.log.error(`Simulation for invalidate block ${blockNumber} failed`, viemError, logData);
555
- throw new Error(`Failed to simulate invalidate block ${blockNumber}`, { cause: viemError });
746
+ // Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
747
+ this.log.error(`Simulation for invalidate checkpoint ${checkpointNumber} failed`, viemError, logData);
748
+ this.backupFailedTx({
749
+ id: keccak256(request.data!),
750
+ failureType: 'simulation',
751
+ request: { to: request.to!, data: request.data!, value: request.value?.toString() },
752
+ l1BlockNumber: l1BlockNumber.toString(),
753
+ error: { message: viemError.message, name: viemError.name },
754
+ context: {
755
+ actions: [`invalidate-${reason}`],
756
+ checkpointNumber,
757
+ sender: this.getSenderAddress().toString(),
758
+ },
759
+ });
760
+ throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, { cause: viemError });
556
761
  }
557
762
  }
558
763
 
559
- private buildInvalidateBlockRequest(validationResult: ValidateBlockResult) {
764
+ private buildInvalidateCheckpointRequest(validationResult: ValidateCheckpointResult) {
560
765
  if (validationResult.valid) {
561
- throw new Error('Cannot invalidate a valid block');
766
+ throw new Error('Cannot invalidate a valid checkpoint');
562
767
  }
563
768
 
564
- const { block, committee, reason } = validationResult;
565
- const logData = { ...block, reason };
566
- this.log.debug(`Simulating invalidate block ${block.blockNumber}`, logData);
769
+ const { checkpoint, committee, reason } = validationResult;
770
+ const logData = { ...checkpoint, reason };
771
+ this.log.debug(`Building invalidate checkpoint ${checkpoint.checkpointNumber} request`, logData);
567
772
 
568
773
  const attestationsAndSigners = new CommitteeAttestationsAndSigners(
569
774
  validationResult.attestations,
@@ -571,14 +776,14 @@ export class SequencerPublisher {
571
776
 
572
777
  if (reason === 'invalid-attestation') {
573
778
  return this.rollupContract.buildInvalidateBadAttestationRequest(
574
- CheckpointNumber.fromBlockNumber(block.blockNumber),
779
+ checkpoint.checkpointNumber,
575
780
  attestationsAndSigners,
576
781
  committee,
577
782
  validationResult.invalidIndex,
578
783
  );
579
784
  } else if (reason === 'insufficient-attestations') {
580
785
  return this.rollupContract.buildInvalidateInsufficientAttestationsRequest(
581
- CheckpointNumber.fromBlockNumber(block.blockNumber),
786
+ checkpoint.checkpointNumber,
582
787
  attestationsAndSigners,
583
788
  committee,
584
789
  );
@@ -589,31 +794,16 @@ export class SequencerPublisher {
589
794
  }
590
795
 
591
796
  /** Simulates `propose` to make sure that the checkpoint is valid for submission */
797
+ @trackSpan('SequencerPublisher.validateCheckpointForSubmission')
592
798
  public async validateCheckpointForSubmission(
593
799
  checkpoint: Checkpoint,
594
800
  attestationsAndSigners: CommitteeAttestationsAndSigners,
595
801
  attestationsAndSignersSignature: Signature,
596
- options: { forcePendingBlockNumber?: BlockNumber }, // TODO(palla/mbps): Should this be forcePendingCheckpointNumber?
802
+ options: { forcePendingCheckpointNumber?: CheckpointNumber },
597
803
  ): Promise<bigint> {
598
804
  const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
599
-
600
- // TODO(palla/mbps): This should not be needed, there's no flow where we propose with zero attestations. Or is there?
601
- // If we have no attestations, we still need to provide the empty attestations
602
- // so that the committee is recalculated correctly
603
- // const ignoreSignatures = attestationsAndSigners.attestations.length === 0;
604
- // if (ignoreSignatures) {
605
- // const { committee } = await this.epochCache.getCommittee(block.header.globalVariables.slotNumber);
606
- // if (!committee) {
607
- // this.log.warn(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
608
- // throw new Error(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
609
- // }
610
- // attestationsAndSigners.attestations = committee.map(committeeMember =>
611
- // CommitteeAttestation.fromAddress(committeeMember),
612
- // );
613
- // }
614
-
615
805
  const blobFields = checkpoint.toBlobFields();
616
- const blobs = getBlobsPerL1Block(blobFields);
806
+ const blobs = await getBlobsPerL1Block(blobFields);
617
807
  const blobInput = getPrefixedEthBlobCommitments(blobs);
618
808
 
619
809
  const args = [
@@ -621,7 +811,7 @@ export class SequencerPublisher {
621
811
  header: checkpoint.header.toViem(),
622
812
  archive: toHex(checkpoint.archive.root.toBuffer()),
623
813
  oracleInput: {
624
- feeAssetPriceModifier: 0n,
814
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
625
815
  },
626
816
  },
627
817
  attestationsAndSigners.getPackedAttestations(),
@@ -670,6 +860,32 @@ export class SequencerPublisher {
670
860
  return false;
671
861
  }
672
862
 
863
+ // Check if payload was already submitted to governance
864
+ const cacheKey = payload.toString();
865
+ if (!this.payloadProposedCache.has(cacheKey)) {
866
+ try {
867
+ const l1StartBlock = await this.rollupContract.getL1StartBlock();
868
+ const proposed = await retry(
869
+ () => base.hasPayloadBeenProposed(payload.toString(), l1StartBlock),
870
+ 'Check if payload was proposed',
871
+ makeBackoff([0, 1, 2]),
872
+ this.log,
873
+ true,
874
+ );
875
+ if (proposed) {
876
+ this.payloadProposedCache.add(cacheKey);
877
+ }
878
+ } catch (err) {
879
+ this.log.warn(`Failed to check if payload ${payload} was proposed after retries, skipping signal`, err);
880
+ return false;
881
+ }
882
+ }
883
+
884
+ if (this.payloadProposedCache.has(cacheKey)) {
885
+ this.log.info(`Payload ${payload} was already proposed to governance, stopping signals`);
886
+ return false;
887
+ }
888
+
673
889
  const cachedLastVote = this.lastActions[signalType];
674
890
  this.lastActions[signalType] = slotNumber;
675
891
  const action = signalType;
@@ -688,11 +904,26 @@ export class SequencerPublisher {
688
904
  lastValidL2Slot: slotNumber,
689
905
  });
690
906
 
907
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
908
+
691
909
  try {
692
- await this.l1TxUtils.simulate(request, { time: timestamp }, [], ErrorsAbi);
910
+ await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
693
911
  this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
694
912
  } catch (err) {
695
- this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, err);
913
+ const viemError = formatViemError(err);
914
+ this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, viemError);
915
+ this.backupFailedTx({
916
+ id: keccak256(request.data!),
917
+ failureType: 'simulation',
918
+ request: { to: request.to!, data: request.data!, value: request.value?.toString() },
919
+ l1BlockNumber: l1BlockNumber.toString(),
920
+ error: { message: viemError.message, name: viemError.name },
921
+ context: {
922
+ actions: [action],
923
+ slot: slotNumber,
924
+ sender: this.getSenderAddress().toString(),
925
+ },
926
+ });
696
927
  // Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
697
928
  }
698
929
 
@@ -892,19 +1123,20 @@ export class SequencerPublisher {
892
1123
  checkpoint: Checkpoint,
893
1124
  attestationsAndSigners: CommitteeAttestationsAndSigners,
894
1125
  attestationsAndSignersSignature: Signature,
895
- opts: { txTimeoutAt?: Date; forcePendingBlockNumber?: BlockNumber } = {},
1126
+ opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
896
1127
  ): Promise<void> {
897
1128
  const checkpointHeader = checkpoint.header;
898
1129
 
899
1130
  const blobFields = checkpoint.toBlobFields();
900
- const blobs = getBlobsPerL1Block(blobFields);
1131
+ const blobs = await getBlobsPerL1Block(blobFields);
901
1132
 
902
- const proposeTxArgs = {
1133
+ const proposeTxArgs: L1ProcessArgs = {
903
1134
  header: checkpointHeader,
904
1135
  archive: checkpoint.archive.root.toBuffer(),
905
1136
  blobs,
906
1137
  attestationsAndSigners,
907
1138
  attestationsAndSignersSignature,
1139
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
908
1140
  };
909
1141
 
910
1142
  let ts: bigint;
@@ -925,7 +1157,7 @@ export class SequencerPublisher {
925
1157
  this.log.error(`Checkpoint validation failed. ${err instanceof Error ? err.message : 'No error message'}`, err, {
926
1158
  ...checkpoint.getStats(),
927
1159
  slotNumber: checkpoint.header.slotNumber,
928
- forcePendingBlockNumber: opts.forcePendingBlockNumber,
1160
+ forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
929
1161
  });
930
1162
  throw err;
931
1163
  }
@@ -934,7 +1166,10 @@ export class SequencerPublisher {
934
1166
  await this.addProposeTx(checkpoint, proposeTxArgs, opts, ts);
935
1167
  }
936
1168
 
937
- public enqueueInvalidateBlock(request: InvalidateBlockRequest | undefined, opts: { txTimeoutAt?: Date } = {}) {
1169
+ public enqueueInvalidateCheckpoint(
1170
+ request: InvalidateCheckpointRequest | undefined,
1171
+ opts: { txTimeoutAt?: Date } = {},
1172
+ ) {
938
1173
  if (!request) {
939
1174
  return;
940
1175
  }
@@ -942,9 +1177,9 @@ export class SequencerPublisher {
942
1177
  // We issued the simulation against the rollup contract, so we need to account for the overhead of the multicall3
943
1178
  const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil((Number(request.gasUsed) * 64) / 63)));
944
1179
 
945
- const { gasUsed, blockNumber } = request;
946
- const logData = { gasUsed, blockNumber, gasLimit, opts };
947
- this.log.verbose(`Enqueuing invalidate block request`, logData);
1180
+ const { gasUsed, checkpointNumber } = request;
1181
+ const logData = { gasUsed, checkpointNumber, gasLimit, opts };
1182
+ this.log.verbose(`Enqueuing invalidate checkpoint request`, logData);
948
1183
  this.addRequest({
949
1184
  action: `invalidate-by-${request.reason}`,
950
1185
  request: request.request,
@@ -957,9 +1192,9 @@ export class SequencerPublisher {
957
1192
  result.receipt.status === 'success' &&
958
1193
  tryExtractEvent(result.receipt.logs, this.rollupContract.address, RollupAbi, 'CheckpointInvalidated');
959
1194
  if (!success) {
960
- this.log.warn(`Invalidate block ${request.blockNumber} failed`, { ...result, ...logData });
1195
+ this.log.warn(`Invalidate checkpoint ${request.checkpointNumber} failed`, { ...result, ...logData });
961
1196
  } else {
962
- this.log.info(`Invalidate block ${request.blockNumber} succeeded`, { ...result, ...logData });
1197
+ this.log.info(`Invalidate checkpoint ${request.checkpointNumber} succeeded`, { ...result, ...logData });
963
1198
  }
964
1199
  return !!success;
965
1200
  },
@@ -984,13 +1219,30 @@ export class SequencerPublisher {
984
1219
 
985
1220
  this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
986
1221
 
1222
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1223
+
987
1224
  let gasUsed: bigint;
1225
+ const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
988
1226
  try {
989
- ({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], ErrorsAbi)); // TODO(palla/slash): Check the timestamp logic
1227
+ ({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], simulateAbi)); // TODO(palla/slash): Check the timestamp logic
990
1228
  this.log.verbose(`Simulation for ${action} succeeded`, { ...logData, request, gasUsed });
991
1229
  } catch (err) {
992
- const viemError = formatViemError(err);
1230
+ const viemError = formatViemError(err, simulateAbi);
993
1231
  this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
1232
+
1233
+ this.backupFailedTx({
1234
+ id: keccak256(request.data!),
1235
+ failureType: 'simulation',
1236
+ request: { to: request.to!, data: request.data!, value: request.value?.toString() },
1237
+ l1BlockNumber: l1BlockNumber.toString(),
1238
+ error: { message: viemError.message, name: viemError.name },
1239
+ context: {
1240
+ actions: [action],
1241
+ slot: slotNumber,
1242
+ sender: this.getSenderAddress().toString(),
1243
+ },
1244
+ });
1245
+
994
1246
  return false;
995
1247
  }
996
1248
 
@@ -998,10 +1250,14 @@ export class SequencerPublisher {
998
1250
  const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil((Number(gasUsed) * 64) / 63)));
999
1251
  logData.gasLimit = gasLimit;
1000
1252
 
1253
+ // Store the ABI used for simulation on the request so Multicall3.forward can decode errors
1254
+ // when the tx is sent and a revert is diagnosed via simulation.
1255
+ const requestWithAbi = { ...request, abi: simulateAbi };
1256
+
1001
1257
  this.log.debug(`Enqueuing ${action}`, logData);
1002
1258
  this.addRequest({
1003
1259
  action,
1004
- request,
1260
+ request: requestWithAbi,
1005
1261
  gasConfig: { gasLimit },
1006
1262
  lastValidL2Slot: slotNumber,
1007
1263
  checkSuccess: (_req, result) => {
@@ -1038,7 +1294,7 @@ export class SequencerPublisher {
1038
1294
  private async prepareProposeTx(
1039
1295
  encodedData: L1ProcessArgs,
1040
1296
  timestamp: bigint,
1041
- options: { forcePendingBlockNumber?: BlockNumber },
1297
+ options: { forcePendingCheckpointNumber?: CheckpointNumber },
1042
1298
  ) {
1043
1299
  const kzg = Blob.getViemKzgInstance();
1044
1300
  const blobInput = getPrefixedEthBlobCommitments(encodedData.blobs);
@@ -1070,9 +1326,27 @@ export class SequencerPublisher {
1070
1326
  kzg,
1071
1327
  },
1072
1328
  )
1073
- .catch(err => {
1074
- const { message, metaMessages } = formatViemError(err);
1075
- this.log.error(`Failed to validate blobs`, message, { metaMessages });
1329
+ .catch(async err => {
1330
+ const viemError = formatViemError(err);
1331
+ this.log.error(`Failed to validate blobs`, viemError.message, { metaMessages: viemError.metaMessages });
1332
+ const validateBlobsData = encodeFunctionData({
1333
+ abi: RollupAbi,
1334
+ functionName: 'validateBlobs',
1335
+ args: [blobInput],
1336
+ });
1337
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1338
+ this.backupFailedTx({
1339
+ id: keccak256(validateBlobsData),
1340
+ failureType: 'simulation',
1341
+ request: { to: this.rollupContract.address as Hex, data: validateBlobsData },
1342
+ blobData: encodedData.blobs.map(b => toHex(b.data)) as Hex[],
1343
+ l1BlockNumber: l1BlockNumber.toString(),
1344
+ error: { message: viemError.message, name: viemError.name },
1345
+ context: {
1346
+ actions: ['validate-blobs'],
1347
+ sender: this.getSenderAddress().toString(),
1348
+ },
1349
+ });
1076
1350
  throw new Error('Failed to validate blobs');
1077
1351
  });
1078
1352
  }
@@ -1083,8 +1357,7 @@ export class SequencerPublisher {
1083
1357
  header: encodedData.header.toViem(),
1084
1358
  archive: toHex(encodedData.archive),
1085
1359
  oracleInput: {
1086
- // We are currently not modifying these. See #9963
1087
- feeAssetPriceModifier: 0n,
1360
+ feeAssetPriceModifier: encodedData.feeAssetPriceModifier,
1088
1361
  },
1089
1362
  },
1090
1363
  encodedData.attestationsAndSigners.getPackedAttestations(),
@@ -1110,7 +1383,7 @@ export class SequencerPublisher {
1110
1383
  readonly header: ViemHeader;
1111
1384
  readonly archive: `0x${string}`;
1112
1385
  readonly oracleInput: {
1113
- readonly feeAssetPriceModifier: 0n;
1386
+ readonly feeAssetPriceModifier: bigint;
1114
1387
  };
1115
1388
  },
1116
1389
  ViemCommitteeAttestations,
@@ -1119,7 +1392,7 @@ export class SequencerPublisher {
1119
1392
  `0x${string}`,
1120
1393
  ],
1121
1394
  timestamp: bigint,
1122
- options: { forcePendingBlockNumber?: BlockNumber },
1395
+ options: { forcePendingCheckpointNumber?: CheckpointNumber },
1123
1396
  ) {
1124
1397
  const rollupData = encodeFunctionData({
1125
1398
  abi: RollupAbi,
@@ -1128,13 +1401,9 @@ export class SequencerPublisher {
1128
1401
  });
1129
1402
 
1130
1403
  // override the pending checkpoint number if requested
1131
- const optsForcePendingCheckpointNumber =
1132
- options.forcePendingBlockNumber !== undefined
1133
- ? CheckpointNumber.fromBlockNumber(options.forcePendingBlockNumber)
1134
- : undefined;
1135
1404
  const forcePendingCheckpointNumberStateDiff = (
1136
- optsForcePendingCheckpointNumber !== undefined
1137
- ? await this.rollupContract.makePendingCheckpointNumberOverride(optsForcePendingCheckpointNumber)
1405
+ options.forcePendingCheckpointNumber !== undefined
1406
+ ? await this.rollupContract.makePendingCheckpointNumberOverride(options.forcePendingCheckpointNumber)
1138
1407
  : []
1139
1408
  ).flatMap(override => override.stateDiff ?? []);
1140
1409
 
@@ -1156,25 +1425,27 @@ export class SequencerPublisher {
1156
1425
  });
1157
1426
  }
1158
1427
 
1428
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1429
+
1159
1430
  const simulationResult = await this.l1TxUtils
1160
1431
  .simulate(
1161
1432
  {
1162
1433
  to: this.rollupContract.address,
1163
1434
  data: rollupData,
1164
- gas: SequencerPublisher.PROPOSE_GAS_GUESS,
1435
+ gas: MAX_L1_TX_LIMIT,
1165
1436
  ...(this.proposerAddressForSimulation && { from: this.proposerAddressForSimulation.toString() }),
1166
1437
  },
1167
1438
  {
1168
1439
  // @note we add 1n to the timestamp because geth implementation doesn't like simulation timestamp to be equal to the current block timestamp
1169
1440
  time: timestamp + 1n,
1170
1441
  // @note reth should have a 30m gas limit per block but throws errors that this tx is beyond limit so we increase here
1171
- gasLimit: SequencerPublisher.PROPOSE_GAS_GUESS * 2n,
1442
+ gasLimit: MAX_L1_TX_LIMIT * 2n,
1172
1443
  },
1173
1444
  stateOverrides,
1174
1445
  RollupAbi,
1175
1446
  {
1176
1447
  // @note fallback gas estimate to use if the node doesn't support simulation API
1177
- fallbackGasEstimate: SequencerPublisher.PROPOSE_GAS_GUESS,
1448
+ fallbackGasEstimate: MAX_L1_TX_LIMIT,
1178
1449
  },
1179
1450
  )
1180
1451
  .catch(err => {
@@ -1184,11 +1455,23 @@ export class SequencerPublisher {
1184
1455
  this.log.debug(`Ignoring expected ValidatorSelection__MissingProposerSignature error in fisherman mode`);
1185
1456
  // Return a minimal simulation result with the fallback gas estimate
1186
1457
  return {
1187
- gasUsed: SequencerPublisher.PROPOSE_GAS_GUESS,
1458
+ gasUsed: MAX_L1_TX_LIMIT,
1188
1459
  logs: [],
1189
1460
  };
1190
1461
  }
1191
1462
  this.log.error(`Failed to simulate propose tx`, viemError);
1463
+ this.backupFailedTx({
1464
+ id: keccak256(rollupData),
1465
+ failureType: 'simulation',
1466
+ request: { to: this.rollupContract.address, data: rollupData },
1467
+ l1BlockNumber: l1BlockNumber.toString(),
1468
+ error: { message: viemError.message, name: viemError.name },
1469
+ context: {
1470
+ actions: ['propose'],
1471
+ slot: Number(args[0].header.slotNumber),
1472
+ sender: this.getSenderAddress().toString(),
1473
+ },
1474
+ });
1192
1475
  throw err;
1193
1476
  });
1194
1477
 
@@ -1198,7 +1481,7 @@ export class SequencerPublisher {
1198
1481
  private async addProposeTx(
1199
1482
  checkpoint: Checkpoint,
1200
1483
  encodedData: L1ProcessArgs,
1201
- opts: { txTimeoutAt?: Date; forcePendingBlockNumber?: BlockNumber } = {},
1484
+ opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
1202
1485
  timestamp: bigint,
1203
1486
  ): Promise<void> {
1204
1487
  const slot = checkpoint.header.slotNumber;