@aztec/sequencer-client 0.0.1-commit.c7c42ec → 0.0.1-commit.c949de6bc

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 (103) hide show
  1. package/dest/client/sequencer-client.d.ts +14 -10
  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 +22 -12
  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 +11 -3
  31. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  32. package/dest/publisher/sequencer-publisher-factory.js +13 -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 +38 -23
  37. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  38. package/dest/publisher/sequencer-publisher.js +739 -96
  39. package/dest/sequencer/checkpoint_proposal_job.d.ts +40 -12
  40. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  41. package/dest/sequencer/checkpoint_proposal_job.js +641 -64
  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 +38 -18
  52. package/dest/sequencer/sequencer.d.ts.map +1 -1
  53. package/dest/sequencer/sequencer.js +513 -66
  54. package/dest/sequencer/timetable.d.ts +1 -4
  55. package/dest/sequencer/timetable.d.ts.map +1 -1
  56. package/dest/sequencer/timetable.js +2 -5
  57. package/dest/test/index.d.ts +4 -7
  58. package/dest/test/index.d.ts.map +1 -1
  59. package/dest/test/mock_checkpoint_builder.d.ts +25 -11
  60. package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
  61. package/dest/test/mock_checkpoint_builder.js +52 -9
  62. package/dest/test/utils.d.ts +13 -9
  63. package/dest/test/utils.d.ts.map +1 -1
  64. package/dest/test/utils.js +27 -17
  65. package/package.json +30 -28
  66. package/src/client/sequencer-client.ts +28 -11
  67. package/src/config.ts +31 -19
  68. package/src/global_variable_builder/global_builder.ts +14 -14
  69. package/src/index.ts +1 -9
  70. package/src/publisher/config.ts +121 -43
  71. package/src/publisher/index.ts +3 -0
  72. package/src/publisher/l1_tx_failed_store/factory.ts +32 -0
  73. package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +55 -0
  74. package/src/publisher/l1_tx_failed_store/file_store_failed_tx_store.ts +46 -0
  75. package/src/publisher/l1_tx_failed_store/index.ts +3 -0
  76. package/src/publisher/sequencer-publisher-factory.ts +23 -6
  77. package/src/publisher/sequencer-publisher-metrics.ts +17 -69
  78. package/src/publisher/sequencer-publisher.ts +358 -126
  79. package/src/sequencer/checkpoint_proposal_job.ts +313 -93
  80. package/src/sequencer/checkpoint_voter.ts +32 -7
  81. package/src/sequencer/index.ts +0 -2
  82. package/src/sequencer/metrics.ts +132 -148
  83. package/src/sequencer/sequencer.ts +159 -68
  84. package/src/sequencer/timetable.ts +7 -6
  85. package/src/test/index.ts +3 -6
  86. package/src/test/mock_checkpoint_builder.ts +102 -29
  87. package/src/test/utils.ts +58 -28
  88. package/dest/sequencer/block_builder.d.ts +0 -26
  89. package/dest/sequencer/block_builder.d.ts.map +0 -1
  90. package/dest/sequencer/block_builder.js +0 -129
  91. package/dest/sequencer/checkpoint_builder.d.ts +0 -63
  92. package/dest/sequencer/checkpoint_builder.d.ts.map +0 -1
  93. package/dest/sequencer/checkpoint_builder.js +0 -131
  94. package/dest/tx_validator/nullifier_cache.d.ts +0 -14
  95. package/dest/tx_validator/nullifier_cache.d.ts.map +0 -1
  96. package/dest/tx_validator/nullifier_cache.js +0 -24
  97. package/dest/tx_validator/tx_validator_factory.d.ts +0 -18
  98. package/dest/tx_validator/tx_validator_factory.d.ts.map +0 -1
  99. package/dest/tx_validator/tx_validator_factory.js +0 -53
  100. package/src/sequencer/block_builder.ts +0 -217
  101. package/src/sequencer/checkpoint_builder.ts +0 -217
  102. package/src/tx_validator/nullifier_cache.ts +0 -30
  103. package/src/tx_validator/tx_validator_factory.ts +0 -133
@@ -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,33 +19,45 @@ 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';
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';
37
40
  import { type ProposerSlashAction, encodeSlashConsensusVotes } from '@aztec/slasher';
38
- import { CommitteeAttestationsAndSigners, type ValidateBlockResult } from '@aztec/stdlib/block';
41
+ import { CommitteeAttestationsAndSigners, type ValidateCheckpointResult } from '@aztec/stdlib/block';
39
42
  import type { Checkpoint } from '@aztec/stdlib/checkpoint';
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';
43
- import { type TelemetryClient, getTelemetryClient } from '@aztec/telemetry-client';
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 = [
@@ -80,12 +95,12 @@ type GovernanceSignalAction = Extract<Action, 'governance-signal' | 'empire-slas
80
95
  // Sorting for actions such that invalidations go before proposals, and proposals go before votes
81
96
  export const compareActions = (a: Action, b: Action) => Actions.indexOf(a) - Actions.indexOf(b);
82
97
 
83
- export type InvalidateBlockRequest = {
98
+ export type InvalidateCheckpointRequest = {
84
99
  request: L1TxRequest;
85
100
  reason: 'invalid-attestation' | 'insufficient-attestations';
86
101
  gasUsed: bigint;
87
- blockNumber: BlockNumber;
88
- forcePendingBlockNumber: BlockNumber;
102
+ checkpointNumber: CheckpointNumber;
103
+ forcePendingCheckpointNumber: CheckpointNumber;
89
104
  };
90
105
 
91
106
  interface RequestWithExpiry {
@@ -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,20 +149,23 @@ 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;
140
156
  public slashFactoryContract: SlashFactoryContract;
141
157
 
158
+ public readonly tracer: Tracer;
159
+
142
160
  protected requests: RequestWithExpiry[] = [];
143
161
 
144
162
  constructor(
145
- private config: TxSenderConfig & PublisherConfig & Pick<L1ContractsConfig, 'ethereumSlotDuration'>,
163
+ private config: Pick<SequencerPublisherConfig, 'fishermanMode' | 'l1TxFailedStore'> &
164
+ Pick<L1ContractsConfig, 'ethereumSlotDuration'> & { l1ChainId: number },
146
165
  deps: {
147
166
  telemetry?: TelemetryClient;
148
167
  blobClient: BlobClientInterface;
149
- l1TxUtils: L1TxUtilsWithBlobs;
168
+ l1TxUtils: L1TxUtils;
150
169
  rollupContract: RollupContract;
151
170
  slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
152
171
  governanceProposerContract: GovernanceProposerContract;
@@ -167,6 +186,7 @@ export class SequencerPublisher {
167
186
 
168
187
  const telemetry = deps.telemetry ?? getTelemetryClient();
169
188
  this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
189
+ this.tracer = telemetry.getTracer('SequencerPublisher');
170
190
  this.l1TxUtils = deps.l1TxUtils;
171
191
 
172
192
  this.rollupContract = deps.rollupContract;
@@ -189,12 +209,52 @@ export class SequencerPublisher {
189
209
  createLogger('sequencer:publisher:fee-analyzer'),
190
210
  );
191
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
+ });
192
244
  }
193
245
 
194
246
  public getRollupContract(): RollupContract {
195
247
  return this.rollupContract;
196
248
  }
197
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
+
198
258
  public getSenderAddress() {
199
259
  return this.l1TxUtils.getSenderAddress();
200
260
  }
@@ -270,7 +330,7 @@ export class SequencerPublisher {
270
330
  // Start the analysis
271
331
  const analysisId = await this.l1FeeAnalyzer.startAnalysis(
272
332
  l2SlotNumber,
273
- gasLimit > 0n ? gasLimit : SequencerPublisher.PROPOSE_GAS_GUESS,
333
+ gasLimit > 0n ? gasLimit : MAX_L1_TX_LIMIT,
274
334
  l1Requests,
275
335
  blobConfig,
276
336
  onComplete,
@@ -296,6 +356,7 @@ export class SequencerPublisher {
296
356
  * - a receipt and errorMsg if it failed on L1
297
357
  * - undefined if no valid requests are found OR the tx failed to send.
298
358
  */
359
+ @trackSpan('SequencerPublisher.sendRequests')
299
360
  public async sendRequests() {
300
361
  const requestsToProcess = [...this.requests];
301
362
  this.requests = [];
@@ -342,7 +403,16 @@ export class SequencerPublisher {
342
403
 
343
404
  // Merge gasConfigs. Yields the sum of gasLimits, and the earliest txTimeoutAt, or undefined if no gasConfig sets them.
344
405
  const gasLimits = gasConfigs.map(g => g?.gasLimit).filter((g): g is bigint => g !== undefined);
345
- 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
+ }
346
416
  const txTimeoutAts = gasConfigs.map(g => g?.txTimeoutAt).filter((g): g is Date => g !== undefined);
347
417
  const txTimeoutAt = txTimeoutAts.length > 0 ? new Date(Math.min(...txTimeoutAts.map(g => g.getTime()))) : undefined; // earliest
348
418
  const txConfig: RequestWithExpiry['gasConfig'] = { gasLimit, txTimeoutAt };
@@ -352,6 +422,21 @@ export class SequencerPublisher {
352
422
  validRequests.sort((a, b) => compareActions(a.action, b.action));
353
423
 
354
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
+
355
440
  this.log.debug('Forwarding transactions', {
356
441
  validRequests: validRequests.map(request => request.action),
357
442
  txConfig,
@@ -364,7 +449,12 @@ export class SequencerPublisher {
364
449
  this.rollupContract.address,
365
450
  this.log,
366
451
  );
367
- 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
+ );
368
458
  return { result, expiredActions, sentActions: validActions, successfulActions, failedActions };
369
459
  } catch (err) {
370
460
  const viemError = formatViemError(err);
@@ -384,11 +474,25 @@ export class SequencerPublisher {
384
474
 
385
475
  private callbackBundledTransactions(
386
476
  requests: RequestWithExpiry[],
387
- result?: { receipt: TransactionReceipt } | FormattedViemError,
477
+ result: { receipt: TransactionReceipt; errorMsg?: string } | FormattedViemError | undefined,
478
+ txContext: { multicallData: Hex; blobData?: Hex[]; l1BlockNumber: bigint },
388
479
  ) {
389
480
  const actionsListStr = requests.map(r => r.action).join(', ');
390
481
  if (result instanceof FormattedViemError) {
391
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
+ });
392
496
  return { failedActions: requests.map(r => r.action) };
393
497
  } else {
394
498
  this.log.verbose(`Published bundled transactions (${actionsListStr})`, { result, requests });
@@ -401,6 +505,30 @@ export class SequencerPublisher {
401
505
  failedActions.push(request.action);
402
506
  }
403
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
+ }
404
532
  return { successfulActions, failedActions };
405
533
  }
406
534
  }
@@ -413,17 +541,14 @@ export class SequencerPublisher {
413
541
  public canProposeAtNextEthBlock(
414
542
  tipArchive: Fr,
415
543
  msgSender: EthAddress,
416
- opts: { forcePendingBlockNumber?: BlockNumber } = {},
544
+ opts: { forcePendingCheckpointNumber?: CheckpointNumber } = {},
417
545
  ) {
418
546
  // TODO: #14291 - should loop through multiple keys to check if any of them can propose
419
547
  const ignoredErrors = ['SlotAlreadyInChain', 'InvalidProposer', 'InvalidArchive'];
420
548
 
421
549
  return this.rollupContract
422
550
  .canProposeAtNextEthBlock(tipArchive.toBuffer(), msgSender.toString(), Number(this.ethereumSlotDuration), {
423
- forcePendingCheckpointNumber:
424
- opts.forcePendingBlockNumber !== undefined
425
- ? CheckpointNumber.fromBlockNumber(opts.forcePendingBlockNumber)
426
- : undefined,
551
+ forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
427
552
  })
428
553
  .catch(err => {
429
554
  if (err instanceof FormattedViemError && ignoredErrors.find(e => err.message.includes(e))) {
@@ -442,10 +567,11 @@ export class SequencerPublisher {
442
567
  * It will throw if the block header is invalid.
443
568
  * @param header - The block header to validate
444
569
  */
570
+ @trackSpan('SequencerPublisher.validateBlockHeader')
445
571
  public async validateBlockHeader(
446
572
  header: CheckpointHeader,
447
- opts?: { forcePendingBlockNumber: BlockNumber | undefined },
448
- ) {
573
+ opts?: { forcePendingCheckpointNumber: CheckpointNumber | undefined },
574
+ ): Promise<void> {
449
575
  const flags = { ignoreDA: true, ignoreSignatures: true };
450
576
 
451
577
  const args = [
@@ -454,17 +580,13 @@ export class SequencerPublisher {
454
580
  [], // no signers
455
581
  Signature.empty().toViemSignature(),
456
582
  `0x${'0'.repeat(64)}`, // 32 empty bytes
457
- header.contentCommitment.blobsHash.toString(),
583
+ header.blobsHash.toString(),
458
584
  flags,
459
585
  ] as const;
460
586
 
461
587
  const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
462
- const optsForcePendingCheckpointNumber =
463
- opts?.forcePendingBlockNumber !== undefined
464
- ? CheckpointNumber.fromBlockNumber(opts.forcePendingBlockNumber)
465
- : undefined;
466
588
  const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(
467
- optsForcePendingCheckpointNumber,
589
+ opts?.forcePendingCheckpointNumber,
468
590
  );
469
591
  let balance = 0n;
470
592
  if (this.config.fishermanMode) {
@@ -492,77 +614,109 @@ export class SequencerPublisher {
492
614
  }
493
615
 
494
616
  /**
495
- * Simulate making a call to invalidate a block with invalid attestations. Returns undefined if no need to invalidate.
496
- * @param block - The block to invalidate and the criteria for invalidation (as returned by the archiver)
617
+ * Simulate making a call to invalidate a checkpoint with invalid attestations. Returns undefined if no need to invalidate.
618
+ * @param validationResult - The validation result indicating which checkpoint to invalidate (as returned by the archiver)
497
619
  */
498
- public async simulateInvalidateBlock(
499
- validationResult: ValidateBlockResult,
500
- ): Promise<InvalidateBlockRequest | undefined> {
620
+ public async simulateInvalidateCheckpoint(
621
+ validationResult: ValidateCheckpointResult,
622
+ ): Promise<InvalidateCheckpointRequest | undefined> {
501
623
  if (validationResult.valid) {
502
624
  return undefined;
503
625
  }
504
626
 
505
- const { reason, block } = validationResult;
506
- const blockNumber = block.blockNumber;
507
- const logData = { ...block, reason };
627
+ const { reason, checkpoint } = validationResult;
628
+ const checkpointNumber = checkpoint.checkpointNumber;
629
+ const logData = { ...checkpoint, reason };
508
630
 
509
- const currentBlockNumber = await this.rollupContract.getCheckpointNumber();
510
- if (currentBlockNumber < validationResult.block.blockNumber) {
631
+ const currentCheckpointNumber = await this.rollupContract.getCheckpointNumber();
632
+ if (currentCheckpointNumber < checkpointNumber) {
511
633
  this.log.verbose(
512
- `Skipping block ${blockNumber} invalidation since it has already been removed from the pending chain`,
513
- { currentBlockNumber, ...logData },
634
+ `Skipping checkpoint ${checkpointNumber} invalidation since it has already been removed from the pending chain`,
635
+ { currentCheckpointNumber, ...logData },
514
636
  );
515
637
  return undefined;
516
638
  }
517
639
 
518
- const request = this.buildInvalidateBlockRequest(validationResult);
519
- this.log.debug(`Simulating invalidate block ${blockNumber}`, { ...logData, request });
640
+ const request = this.buildInvalidateCheckpointRequest(validationResult);
641
+ this.log.debug(`Simulating invalidate checkpoint ${checkpointNumber}`, { ...logData, request });
642
+
643
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
520
644
 
521
645
  try {
522
- const { gasUsed } = await this.l1TxUtils.simulate(request, undefined, undefined, ErrorsAbi);
523
- this.log.verbose(`Simulation for invalidate block ${blockNumber} succeeded`, { ...logData, request, gasUsed });
646
+ const { gasUsed } = await this.l1TxUtils.simulate(
647
+ request,
648
+ undefined,
649
+ undefined,
650
+ mergeAbis([request.abi ?? [], ErrorsAbi]),
651
+ );
652
+ this.log.verbose(`Simulation for invalidate checkpoint ${checkpointNumber} succeeded`, {
653
+ ...logData,
654
+ request,
655
+ gasUsed,
656
+ });
524
657
 
525
- return { request, gasUsed, blockNumber, forcePendingBlockNumber: BlockNumber(blockNumber - 1), reason };
658
+ return {
659
+ request,
660
+ gasUsed,
661
+ checkpointNumber,
662
+ forcePendingCheckpointNumber: CheckpointNumber(checkpointNumber - 1),
663
+ reason,
664
+ };
526
665
  } catch (err) {
527
666
  const viemError = formatViemError(err);
528
667
 
529
- // If the error is due to the block not being in the pending chain, and it was indeed removed by someone else,
530
- // we can safely ignore it and return undefined so we go ahead with block building.
531
- if (viemError.message?.includes('Rollup__BlockNotInPendingChain')) {
668
+ // If the error is due to the checkpoint not being in the pending chain, and it was indeed removed by someone else,
669
+ // we can safely ignore it and return undefined so we go ahead with checkpoint building.
670
+ if (viemError.message?.includes('Rollup__CheckpointNotInPendingChain')) {
532
671
  this.log.verbose(
533
- `Simulation for invalidate block ${blockNumber} failed due to block not being in pending chain`,
672
+ `Simulation for invalidate checkpoint ${checkpointNumber} failed due to checkpoint not being in pending chain`,
534
673
  { ...logData, request, error: viemError.message },
535
674
  );
536
- const latestPendingBlockNumber = await this.rollupContract.getCheckpointNumber();
537
- if (latestPendingBlockNumber < blockNumber) {
538
- this.log.verbose(`Block number ${blockNumber} has already been invalidated`, { ...logData });
675
+ const latestPendingCheckpointNumber = await this.rollupContract.getCheckpointNumber();
676
+ if (latestPendingCheckpointNumber < checkpointNumber) {
677
+ this.log.verbose(`Checkpoint ${checkpointNumber} has already been invalidated`, { ...logData });
539
678
  return undefined;
540
679
  } else {
541
680
  this.log.error(
542
- `Simulation for invalidate ${blockNumber} failed and it is still in pending chain`,
681
+ `Simulation for invalidate checkpoint ${checkpointNumber} failed and it is still in pending chain`,
543
682
  viemError,
544
683
  logData,
545
684
  );
546
- throw new Error(`Failed to simulate invalidate block ${blockNumber} while it is still in pending chain`, {
547
- cause: viemError,
548
- });
685
+ throw new Error(
686
+ `Failed to simulate invalidate checkpoint ${checkpointNumber} while it is still in pending chain`,
687
+ {
688
+ cause: viemError,
689
+ },
690
+ );
549
691
  }
550
692
  }
551
693
 
552
- // Otherwise, throw. We cannot build the next block if we cannot invalidate the previous one.
553
- this.log.error(`Simulation for invalidate block ${blockNumber} failed`, viemError, logData);
554
- throw new Error(`Failed to simulate invalidate block ${blockNumber}`, { cause: viemError });
694
+ // Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
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
+ });
708
+ throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, { cause: viemError });
555
709
  }
556
710
  }
557
711
 
558
- private buildInvalidateBlockRequest(validationResult: ValidateBlockResult) {
712
+ private buildInvalidateCheckpointRequest(validationResult: ValidateCheckpointResult) {
559
713
  if (validationResult.valid) {
560
- throw new Error('Cannot invalidate a valid block');
714
+ throw new Error('Cannot invalidate a valid checkpoint');
561
715
  }
562
716
 
563
- const { block, committee, reason } = validationResult;
564
- const logData = { ...block, reason };
565
- this.log.debug(`Simulating invalidate block ${block.blockNumber}`, logData);
717
+ const { checkpoint, committee, reason } = validationResult;
718
+ const logData = { ...checkpoint, reason };
719
+ this.log.debug(`Building invalidate checkpoint ${checkpoint.checkpointNumber} request`, logData);
566
720
 
567
721
  const attestationsAndSigners = new CommitteeAttestationsAndSigners(
568
722
  validationResult.attestations,
@@ -570,14 +724,14 @@ export class SequencerPublisher {
570
724
 
571
725
  if (reason === 'invalid-attestation') {
572
726
  return this.rollupContract.buildInvalidateBadAttestationRequest(
573
- CheckpointNumber.fromBlockNumber(block.blockNumber),
727
+ checkpoint.checkpointNumber,
574
728
  attestationsAndSigners,
575
729
  committee,
576
730
  validationResult.invalidIndex,
577
731
  );
578
732
  } else if (reason === 'insufficient-attestations') {
579
733
  return this.rollupContract.buildInvalidateInsufficientAttestationsRequest(
580
- CheckpointNumber.fromBlockNumber(block.blockNumber),
734
+ checkpoint.checkpointNumber,
581
735
  attestationsAndSigners,
582
736
  committee,
583
737
  );
@@ -588,31 +742,16 @@ export class SequencerPublisher {
588
742
  }
589
743
 
590
744
  /** Simulates `propose` to make sure that the checkpoint is valid for submission */
745
+ @trackSpan('SequencerPublisher.validateCheckpointForSubmission')
591
746
  public async validateCheckpointForSubmission(
592
747
  checkpoint: Checkpoint,
593
748
  attestationsAndSigners: CommitteeAttestationsAndSigners,
594
749
  attestationsAndSignersSignature: Signature,
595
- options: { forcePendingBlockNumber?: BlockNumber }, // TODO(palla/mbps): Should this be forcePendingCheckpointNumber?
750
+ options: { forcePendingCheckpointNumber?: CheckpointNumber },
596
751
  ): Promise<bigint> {
597
752
  const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
598
-
599
- // TODO(palla/mbps): This should not be needed, there's no flow where we propose with zero attestations. Or is there?
600
- // If we have no attestations, we still need to provide the empty attestations
601
- // so that the committee is recalculated correctly
602
- // const ignoreSignatures = attestationsAndSigners.attestations.length === 0;
603
- // if (ignoreSignatures) {
604
- // const { committee } = await this.epochCache.getCommittee(block.header.globalVariables.slotNumber);
605
- // if (!committee) {
606
- // this.log.warn(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
607
- // throw new Error(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
608
- // }
609
- // attestationsAndSigners.attestations = committee.map(committeeMember =>
610
- // CommitteeAttestation.fromAddress(committeeMember),
611
- // );
612
- // }
613
-
614
753
  const blobFields = checkpoint.toBlobFields();
615
- const blobs = getBlobsPerL1Block(blobFields);
754
+ const blobs = await getBlobsPerL1Block(blobFields);
616
755
  const blobInput = getPrefixedEthBlobCommitments(blobs);
617
756
 
618
757
  const args = [
@@ -620,7 +759,7 @@ export class SequencerPublisher {
620
759
  header: checkpoint.header.toViem(),
621
760
  archive: toHex(checkpoint.archive.root.toBuffer()),
622
761
  oracleInput: {
623
- feeAssetPriceModifier: 0n,
762
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
624
763
  },
625
764
  },
626
765
  attestationsAndSigners.getPackedAttestations(),
@@ -669,6 +808,32 @@ export class SequencerPublisher {
669
808
  return false;
670
809
  }
671
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
+
672
837
  const cachedLastVote = this.lastActions[signalType];
673
838
  this.lastActions[signalType] = slotNumber;
674
839
  const action = signalType;
@@ -687,11 +852,26 @@ export class SequencerPublisher {
687
852
  lastValidL2Slot: slotNumber,
688
853
  });
689
854
 
855
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
856
+
690
857
  try {
691
- await this.l1TxUtils.simulate(request, { time: timestamp }, [], ErrorsAbi);
858
+ await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
692
859
  this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
693
860
  } catch (err) {
694
- 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
+ });
695
875
  // Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
696
876
  }
697
877
 
@@ -891,19 +1071,20 @@ export class SequencerPublisher {
891
1071
  checkpoint: Checkpoint,
892
1072
  attestationsAndSigners: CommitteeAttestationsAndSigners,
893
1073
  attestationsAndSignersSignature: Signature,
894
- opts: { txTimeoutAt?: Date; forcePendingBlockNumber?: BlockNumber } = {},
1074
+ opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
895
1075
  ): Promise<void> {
896
1076
  const checkpointHeader = checkpoint.header;
897
1077
 
898
1078
  const blobFields = checkpoint.toBlobFields();
899
- const blobs = getBlobsPerL1Block(blobFields);
1079
+ const blobs = await getBlobsPerL1Block(blobFields);
900
1080
 
901
- const proposeTxArgs = {
1081
+ const proposeTxArgs: L1ProcessArgs = {
902
1082
  header: checkpointHeader,
903
1083
  archive: checkpoint.archive.root.toBuffer(),
904
1084
  blobs,
905
1085
  attestationsAndSigners,
906
1086
  attestationsAndSignersSignature,
1087
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
907
1088
  };
908
1089
 
909
1090
  let ts: bigint;
@@ -924,7 +1105,7 @@ export class SequencerPublisher {
924
1105
  this.log.error(`Checkpoint validation failed. ${err instanceof Error ? err.message : 'No error message'}`, err, {
925
1106
  ...checkpoint.getStats(),
926
1107
  slotNumber: checkpoint.header.slotNumber,
927
- forcePendingBlockNumber: opts.forcePendingBlockNumber,
1108
+ forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
928
1109
  });
929
1110
  throw err;
930
1111
  }
@@ -933,7 +1114,10 @@ export class SequencerPublisher {
933
1114
  await this.addProposeTx(checkpoint, proposeTxArgs, opts, ts);
934
1115
  }
935
1116
 
936
- public enqueueInvalidateBlock(request: InvalidateBlockRequest | undefined, opts: { txTimeoutAt?: Date } = {}) {
1117
+ public enqueueInvalidateCheckpoint(
1118
+ request: InvalidateCheckpointRequest | undefined,
1119
+ opts: { txTimeoutAt?: Date } = {},
1120
+ ) {
937
1121
  if (!request) {
938
1122
  return;
939
1123
  }
@@ -941,9 +1125,9 @@ export class SequencerPublisher {
941
1125
  // We issued the simulation against the rollup contract, so we need to account for the overhead of the multicall3
942
1126
  const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil((Number(request.gasUsed) * 64) / 63)));
943
1127
 
944
- const { gasUsed, blockNumber } = request;
945
- const logData = { gasUsed, blockNumber, gasLimit, opts };
946
- this.log.verbose(`Enqueuing invalidate block request`, logData);
1128
+ const { gasUsed, checkpointNumber } = request;
1129
+ const logData = { gasUsed, checkpointNumber, gasLimit, opts };
1130
+ this.log.verbose(`Enqueuing invalidate checkpoint request`, logData);
947
1131
  this.addRequest({
948
1132
  action: `invalidate-by-${request.reason}`,
949
1133
  request: request.request,
@@ -956,9 +1140,9 @@ export class SequencerPublisher {
956
1140
  result.receipt.status === 'success' &&
957
1141
  tryExtractEvent(result.receipt.logs, this.rollupContract.address, RollupAbi, 'CheckpointInvalidated');
958
1142
  if (!success) {
959
- this.log.warn(`Invalidate block ${request.blockNumber} failed`, { ...result, ...logData });
1143
+ this.log.warn(`Invalidate checkpoint ${request.checkpointNumber} failed`, { ...result, ...logData });
960
1144
  } else {
961
- this.log.info(`Invalidate block ${request.blockNumber} succeeded`, { ...result, ...logData });
1145
+ this.log.info(`Invalidate checkpoint ${request.checkpointNumber} succeeded`, { ...result, ...logData });
962
1146
  }
963
1147
  return !!success;
964
1148
  },
@@ -983,13 +1167,30 @@ export class SequencerPublisher {
983
1167
 
984
1168
  this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
985
1169
 
1170
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1171
+
986
1172
  let gasUsed: bigint;
1173
+ const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
987
1174
  try {
988
- ({ 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
989
1176
  this.log.verbose(`Simulation for ${action} succeeded`, { ...logData, request, gasUsed });
990
1177
  } catch (err) {
991
- const viemError = formatViemError(err);
1178
+ const viemError = formatViemError(err, simulateAbi);
992
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
+
993
1194
  return false;
994
1195
  }
995
1196
 
@@ -997,10 +1198,14 @@ export class SequencerPublisher {
997
1198
  const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil((Number(gasUsed) * 64) / 63)));
998
1199
  logData.gasLimit = gasLimit;
999
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
+
1000
1205
  this.log.debug(`Enqueuing ${action}`, logData);
1001
1206
  this.addRequest({
1002
1207
  action,
1003
- request,
1208
+ request: requestWithAbi,
1004
1209
  gasConfig: { gasLimit },
1005
1210
  lastValidL2Slot: slotNumber,
1006
1211
  checkSuccess: (_req, result) => {
@@ -1037,7 +1242,7 @@ export class SequencerPublisher {
1037
1242
  private async prepareProposeTx(
1038
1243
  encodedData: L1ProcessArgs,
1039
1244
  timestamp: bigint,
1040
- options: { forcePendingBlockNumber?: BlockNumber },
1245
+ options: { forcePendingCheckpointNumber?: CheckpointNumber },
1041
1246
  ) {
1042
1247
  const kzg = Blob.getViemKzgInstance();
1043
1248
  const blobInput = getPrefixedEthBlobCommitments(encodedData.blobs);
@@ -1069,9 +1274,27 @@ export class SequencerPublisher {
1069
1274
  kzg,
1070
1275
  },
1071
1276
  )
1072
- .catch(err => {
1073
- const { message, metaMessages } = formatViemError(err);
1074
- 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
+ });
1075
1298
  throw new Error('Failed to validate blobs');
1076
1299
  });
1077
1300
  }
@@ -1082,8 +1305,7 @@ export class SequencerPublisher {
1082
1305
  header: encodedData.header.toViem(),
1083
1306
  archive: toHex(encodedData.archive),
1084
1307
  oracleInput: {
1085
- // We are currently not modifying these. See #9963
1086
- feeAssetPriceModifier: 0n,
1308
+ feeAssetPriceModifier: encodedData.feeAssetPriceModifier,
1087
1309
  },
1088
1310
  },
1089
1311
  encodedData.attestationsAndSigners.getPackedAttestations(),
@@ -1109,7 +1331,7 @@ export class SequencerPublisher {
1109
1331
  readonly header: ViemHeader;
1110
1332
  readonly archive: `0x${string}`;
1111
1333
  readonly oracleInput: {
1112
- readonly feeAssetPriceModifier: 0n;
1334
+ readonly feeAssetPriceModifier: bigint;
1113
1335
  };
1114
1336
  },
1115
1337
  ViemCommitteeAttestations,
@@ -1118,7 +1340,7 @@ export class SequencerPublisher {
1118
1340
  `0x${string}`,
1119
1341
  ],
1120
1342
  timestamp: bigint,
1121
- options: { forcePendingBlockNumber?: BlockNumber },
1343
+ options: { forcePendingCheckpointNumber?: CheckpointNumber },
1122
1344
  ) {
1123
1345
  const rollupData = encodeFunctionData({
1124
1346
  abi: RollupAbi,
@@ -1127,13 +1349,9 @@ export class SequencerPublisher {
1127
1349
  });
1128
1350
 
1129
1351
  // override the pending checkpoint number if requested
1130
- const optsForcePendingCheckpointNumber =
1131
- options.forcePendingBlockNumber !== undefined
1132
- ? CheckpointNumber.fromBlockNumber(options.forcePendingBlockNumber)
1133
- : undefined;
1134
1352
  const forcePendingCheckpointNumberStateDiff = (
1135
- optsForcePendingCheckpointNumber !== undefined
1136
- ? await this.rollupContract.makePendingCheckpointNumberOverride(optsForcePendingCheckpointNumber)
1353
+ options.forcePendingCheckpointNumber !== undefined
1354
+ ? await this.rollupContract.makePendingCheckpointNumberOverride(options.forcePendingCheckpointNumber)
1137
1355
  : []
1138
1356
  ).flatMap(override => override.stateDiff ?? []);
1139
1357
 
@@ -1155,25 +1373,27 @@ export class SequencerPublisher {
1155
1373
  });
1156
1374
  }
1157
1375
 
1376
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1377
+
1158
1378
  const simulationResult = await this.l1TxUtils
1159
1379
  .simulate(
1160
1380
  {
1161
1381
  to: this.rollupContract.address,
1162
1382
  data: rollupData,
1163
- gas: SequencerPublisher.PROPOSE_GAS_GUESS,
1383
+ gas: MAX_L1_TX_LIMIT,
1164
1384
  ...(this.proposerAddressForSimulation && { from: this.proposerAddressForSimulation.toString() }),
1165
1385
  },
1166
1386
  {
1167
1387
  // @note we add 1n to the timestamp because geth implementation doesn't like simulation timestamp to be equal to the current block timestamp
1168
1388
  time: timestamp + 1n,
1169
1389
  // @note reth should have a 30m gas limit per block but throws errors that this tx is beyond limit so we increase here
1170
- gasLimit: SequencerPublisher.PROPOSE_GAS_GUESS * 2n,
1390
+ gasLimit: MAX_L1_TX_LIMIT * 2n,
1171
1391
  },
1172
1392
  stateOverrides,
1173
1393
  RollupAbi,
1174
1394
  {
1175
1395
  // @note fallback gas estimate to use if the node doesn't support simulation API
1176
- fallbackGasEstimate: SequencerPublisher.PROPOSE_GAS_GUESS,
1396
+ fallbackGasEstimate: MAX_L1_TX_LIMIT,
1177
1397
  },
1178
1398
  )
1179
1399
  .catch(err => {
@@ -1183,11 +1403,23 @@ export class SequencerPublisher {
1183
1403
  this.log.debug(`Ignoring expected ValidatorSelection__MissingProposerSignature error in fisherman mode`);
1184
1404
  // Return a minimal simulation result with the fallback gas estimate
1185
1405
  return {
1186
- gasUsed: SequencerPublisher.PROPOSE_GAS_GUESS,
1406
+ gasUsed: MAX_L1_TX_LIMIT,
1187
1407
  logs: [],
1188
1408
  };
1189
1409
  }
1190
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
+ });
1191
1423
  throw err;
1192
1424
  });
1193
1425
 
@@ -1197,7 +1429,7 @@ export class SequencerPublisher {
1197
1429
  private async addProposeTx(
1198
1430
  checkpoint: Checkpoint,
1199
1431
  encodedData: L1ProcessArgs,
1200
- opts: { txTimeoutAt?: Date; forcePendingBlockNumber?: BlockNumber } = {},
1432
+ opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
1201
1433
  timestamp: bigint,
1202
1434
  ): Promise<void> {
1203
1435
  const slot = checkpoint.header.slotNumber;