@aztec/sequencer-client 0.0.1-commit.03f7ef2 → 0.0.1-commit.0658669b3

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 +15 -11
  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 +12 -4
  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 +40 -25
  37. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  38. package/dest/publisher/sequencer-publisher.js +740 -100
  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 +643 -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 +1 -4
  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 +29 -12
  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 +24 -7
  77. package/src/publisher/sequencer-publisher-metrics.ts +17 -69
  78. package/src/publisher/sequencer-publisher.ts +361 -130
  79. package/src/sequencer/checkpoint_proposal_job.ts +316 -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 +6 -5
  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
@@ -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,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
- blobClient?: BlobClientInterface;
149
- l1TxUtils: L1TxUtilsWithBlobs;
167
+ blobClient: BlobClientInterface;
168
+ l1TxUtils: L1TxUtils;
150
169
  rollupContract: RollupContract;
151
170
  slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
152
171
  governanceProposerContract: GovernanceProposerContract;
@@ -163,11 +182,11 @@ export class SequencerPublisher {
163
182
  this.epochCache = deps.epochCache;
164
183
  this.lastActions = deps.lastActions;
165
184
 
166
- this.blobClient =
167
- deps.blobClient ?? createBlobClient(config, { logger: createLogger('sequencer:blob-client:client') });
185
+ this.blobClient = deps.blobClient;
168
186
 
169
187
  const telemetry = deps.telemetry ?? getTelemetryClient();
170
188
  this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
189
+ this.tracer = telemetry.getTracer('SequencerPublisher');
171
190
  this.l1TxUtils = deps.l1TxUtils;
172
191
 
173
192
  this.rollupContract = deps.rollupContract;
@@ -190,12 +209,52 @@ export class SequencerPublisher {
190
209
  createLogger('sequencer:publisher:fee-analyzer'),
191
210
  );
192
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
+ });
193
244
  }
194
245
 
195
246
  public getRollupContract(): RollupContract {
196
247
  return this.rollupContract;
197
248
  }
198
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
+
199
258
  public getSenderAddress() {
200
259
  return this.l1TxUtils.getSenderAddress();
201
260
  }
@@ -271,7 +330,7 @@ export class SequencerPublisher {
271
330
  // Start the analysis
272
331
  const analysisId = await this.l1FeeAnalyzer.startAnalysis(
273
332
  l2SlotNumber,
274
- gasLimit > 0n ? gasLimit : SequencerPublisher.PROPOSE_GAS_GUESS,
333
+ gasLimit > 0n ? gasLimit : MAX_L1_TX_LIMIT,
275
334
  l1Requests,
276
335
  blobConfig,
277
336
  onComplete,
@@ -297,6 +356,7 @@ export class SequencerPublisher {
297
356
  * - a receipt and errorMsg if it failed on L1
298
357
  * - undefined if no valid requests are found OR the tx failed to send.
299
358
  */
359
+ @trackSpan('SequencerPublisher.sendRequests')
300
360
  public async sendRequests() {
301
361
  const requestsToProcess = [...this.requests];
302
362
  this.requests = [];
@@ -343,7 +403,16 @@ export class SequencerPublisher {
343
403
 
344
404
  // Merge gasConfigs. Yields the sum of gasLimits, and the earliest txTimeoutAt, or undefined if no gasConfig sets them.
345
405
  const gasLimits = gasConfigs.map(g => g?.gasLimit).filter((g): g is bigint => g !== undefined);
346
- 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
+ }
347
416
  const txTimeoutAts = gasConfigs.map(g => g?.txTimeoutAt).filter((g): g is Date => g !== undefined);
348
417
  const txTimeoutAt = txTimeoutAts.length > 0 ? new Date(Math.min(...txTimeoutAts.map(g => g.getTime()))) : undefined; // earliest
349
418
  const txConfig: RequestWithExpiry['gasConfig'] = { gasLimit, txTimeoutAt };
@@ -353,6 +422,21 @@ export class SequencerPublisher {
353
422
  validRequests.sort((a, b) => compareActions(a.action, b.action));
354
423
 
355
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
+
356
440
  this.log.debug('Forwarding transactions', {
357
441
  validRequests: validRequests.map(request => request.action),
358
442
  txConfig,
@@ -365,7 +449,12 @@ export class SequencerPublisher {
365
449
  this.rollupContract.address,
366
450
  this.log,
367
451
  );
368
- 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
+ );
369
458
  return { result, expiredActions, sentActions: validActions, successfulActions, failedActions };
370
459
  } catch (err) {
371
460
  const viemError = formatViemError(err);
@@ -385,11 +474,25 @@ export class SequencerPublisher {
385
474
 
386
475
  private callbackBundledTransactions(
387
476
  requests: RequestWithExpiry[],
388
- result?: { receipt: TransactionReceipt } | FormattedViemError,
477
+ result: { receipt: TransactionReceipt; errorMsg?: string } | FormattedViemError | undefined,
478
+ txContext: { multicallData: Hex; blobData?: Hex[]; l1BlockNumber: bigint },
389
479
  ) {
390
480
  const actionsListStr = requests.map(r => r.action).join(', ');
391
481
  if (result instanceof FormattedViemError) {
392
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
+ });
393
496
  return { failedActions: requests.map(r => r.action) };
394
497
  } else {
395
498
  this.log.verbose(`Published bundled transactions (${actionsListStr})`, { result, requests });
@@ -402,6 +505,30 @@ export class SequencerPublisher {
402
505
  failedActions.push(request.action);
403
506
  }
404
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
+ }
405
532
  return { successfulActions, failedActions };
406
533
  }
407
534
  }
@@ -414,17 +541,14 @@ export class SequencerPublisher {
414
541
  public canProposeAtNextEthBlock(
415
542
  tipArchive: Fr,
416
543
  msgSender: EthAddress,
417
- opts: { forcePendingBlockNumber?: BlockNumber } = {},
544
+ opts: { forcePendingCheckpointNumber?: CheckpointNumber } = {},
418
545
  ) {
419
546
  // TODO: #14291 - should loop through multiple keys to check if any of them can propose
420
547
  const ignoredErrors = ['SlotAlreadyInChain', 'InvalidProposer', 'InvalidArchive'];
421
548
 
422
549
  return this.rollupContract
423
550
  .canProposeAtNextEthBlock(tipArchive.toBuffer(), msgSender.toString(), Number(this.ethereumSlotDuration), {
424
- forcePendingCheckpointNumber:
425
- opts.forcePendingBlockNumber !== undefined
426
- ? CheckpointNumber.fromBlockNumber(opts.forcePendingBlockNumber)
427
- : undefined,
551
+ forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
428
552
  })
429
553
  .catch(err => {
430
554
  if (err instanceof FormattedViemError && ignoredErrors.find(e => err.message.includes(e))) {
@@ -443,10 +567,11 @@ export class SequencerPublisher {
443
567
  * It will throw if the block header is invalid.
444
568
  * @param header - The block header to validate
445
569
  */
570
+ @trackSpan('SequencerPublisher.validateBlockHeader')
446
571
  public async validateBlockHeader(
447
572
  header: CheckpointHeader,
448
- opts?: { forcePendingBlockNumber: BlockNumber | undefined },
449
- ) {
573
+ opts?: { forcePendingCheckpointNumber: CheckpointNumber | undefined },
574
+ ): Promise<void> {
450
575
  const flags = { ignoreDA: true, ignoreSignatures: true };
451
576
 
452
577
  const args = [
@@ -455,17 +580,13 @@ export class SequencerPublisher {
455
580
  [], // no signers
456
581
  Signature.empty().toViemSignature(),
457
582
  `0x${'0'.repeat(64)}`, // 32 empty bytes
458
- header.contentCommitment.blobsHash.toString(),
583
+ header.blobsHash.toString(),
459
584
  flags,
460
585
  ] as const;
461
586
 
462
587
  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
588
  const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(
468
- optsForcePendingCheckpointNumber,
589
+ opts?.forcePendingCheckpointNumber,
469
590
  );
470
591
  let balance = 0n;
471
592
  if (this.config.fishermanMode) {
@@ -493,77 +614,109 @@ export class SequencerPublisher {
493
614
  }
494
615
 
495
616
  /**
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)
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)
498
619
  */
499
- public async simulateInvalidateBlock(
500
- validationResult: ValidateBlockResult,
501
- ): Promise<InvalidateBlockRequest | undefined> {
620
+ public async simulateInvalidateCheckpoint(
621
+ validationResult: ValidateCheckpointResult,
622
+ ): Promise<InvalidateCheckpointRequest | undefined> {
502
623
  if (validationResult.valid) {
503
624
  return undefined;
504
625
  }
505
626
 
506
- const { reason, block } = validationResult;
507
- const blockNumber = block.blockNumber;
508
- const logData = { ...block, reason };
627
+ const { reason, checkpoint } = validationResult;
628
+ const checkpointNumber = checkpoint.checkpointNumber;
629
+ const logData = { ...checkpoint, reason };
509
630
 
510
- const currentBlockNumber = await this.rollupContract.getCheckpointNumber();
511
- if (currentBlockNumber < validationResult.block.blockNumber) {
631
+ const currentCheckpointNumber = await this.rollupContract.getCheckpointNumber();
632
+ if (currentCheckpointNumber < checkpointNumber) {
512
633
  this.log.verbose(
513
- `Skipping block ${blockNumber} invalidation since it has already been removed from the pending chain`,
514
- { currentBlockNumber, ...logData },
634
+ `Skipping checkpoint ${checkpointNumber} invalidation since it has already been removed from the pending chain`,
635
+ { currentCheckpointNumber, ...logData },
515
636
  );
516
637
  return undefined;
517
638
  }
518
639
 
519
- const request = this.buildInvalidateBlockRequest(validationResult);
520
- 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();
521
644
 
522
645
  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 });
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
+ });
525
657
 
526
- 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
+ };
527
665
  } catch (err) {
528
666
  const viemError = formatViemError(err);
529
667
 
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')) {
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')) {
533
671
  this.log.verbose(
534
- `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`,
535
673
  { ...logData, request, error: viemError.message },
536
674
  );
537
- const latestPendingBlockNumber = await this.rollupContract.getCheckpointNumber();
538
- if (latestPendingBlockNumber < blockNumber) {
539
- 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 });
540
678
  return undefined;
541
679
  } else {
542
680
  this.log.error(
543
- `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`,
544
682
  viemError,
545
683
  logData,
546
684
  );
547
- throw new Error(`Failed to simulate invalidate block ${blockNumber} while it is still in pending chain`, {
548
- cause: viemError,
549
- });
685
+ throw new Error(
686
+ `Failed to simulate invalidate checkpoint ${checkpointNumber} while it is still in pending chain`,
687
+ {
688
+ cause: viemError,
689
+ },
690
+ );
550
691
  }
551
692
  }
552
693
 
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 });
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 });
556
709
  }
557
710
  }
558
711
 
559
- private buildInvalidateBlockRequest(validationResult: ValidateBlockResult) {
712
+ private buildInvalidateCheckpointRequest(validationResult: ValidateCheckpointResult) {
560
713
  if (validationResult.valid) {
561
- throw new Error('Cannot invalidate a valid block');
714
+ throw new Error('Cannot invalidate a valid checkpoint');
562
715
  }
563
716
 
564
- const { block, committee, reason } = validationResult;
565
- const logData = { ...block, reason };
566
- 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);
567
720
 
568
721
  const attestationsAndSigners = new CommitteeAttestationsAndSigners(
569
722
  validationResult.attestations,
@@ -571,14 +724,14 @@ export class SequencerPublisher {
571
724
 
572
725
  if (reason === 'invalid-attestation') {
573
726
  return this.rollupContract.buildInvalidateBadAttestationRequest(
574
- CheckpointNumber.fromBlockNumber(block.blockNumber),
727
+ checkpoint.checkpointNumber,
575
728
  attestationsAndSigners,
576
729
  committee,
577
730
  validationResult.invalidIndex,
578
731
  );
579
732
  } else if (reason === 'insufficient-attestations') {
580
733
  return this.rollupContract.buildInvalidateInsufficientAttestationsRequest(
581
- CheckpointNumber.fromBlockNumber(block.blockNumber),
734
+ checkpoint.checkpointNumber,
582
735
  attestationsAndSigners,
583
736
  committee,
584
737
  );
@@ -589,31 +742,16 @@ export class SequencerPublisher {
589
742
  }
590
743
 
591
744
  /** Simulates `propose` to make sure that the checkpoint is valid for submission */
745
+ @trackSpan('SequencerPublisher.validateCheckpointForSubmission')
592
746
  public async validateCheckpointForSubmission(
593
747
  checkpoint: Checkpoint,
594
748
  attestationsAndSigners: CommitteeAttestationsAndSigners,
595
749
  attestationsAndSignersSignature: Signature,
596
- options: { forcePendingBlockNumber?: BlockNumber }, // TODO(palla/mbps): Should this be forcePendingCheckpointNumber?
750
+ options: { forcePendingCheckpointNumber?: CheckpointNumber },
597
751
  ): Promise<bigint> {
598
752
  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
753
  const blobFields = checkpoint.toBlobFields();
616
- const blobs = getBlobsPerL1Block(blobFields);
754
+ const blobs = await getBlobsPerL1Block(blobFields);
617
755
  const blobInput = getPrefixedEthBlobCommitments(blobs);
618
756
 
619
757
  const args = [
@@ -621,7 +759,7 @@ export class SequencerPublisher {
621
759
  header: checkpoint.header.toViem(),
622
760
  archive: toHex(checkpoint.archive.root.toBuffer()),
623
761
  oracleInput: {
624
- feeAssetPriceModifier: 0n,
762
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
625
763
  },
626
764
  },
627
765
  attestationsAndSigners.getPackedAttestations(),
@@ -670,6 +808,32 @@ export class SequencerPublisher {
670
808
  return false;
671
809
  }
672
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
+
673
837
  const cachedLastVote = this.lastActions[signalType];
674
838
  this.lastActions[signalType] = slotNumber;
675
839
  const action = signalType;
@@ -688,11 +852,26 @@ export class SequencerPublisher {
688
852
  lastValidL2Slot: slotNumber,
689
853
  });
690
854
 
855
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
856
+
691
857
  try {
692
- await this.l1TxUtils.simulate(request, { time: timestamp }, [], ErrorsAbi);
858
+ await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
693
859
  this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
694
860
  } catch (err) {
695
- 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
+ });
696
875
  // Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
697
876
  }
698
877
 
@@ -892,19 +1071,20 @@ export class SequencerPublisher {
892
1071
  checkpoint: Checkpoint,
893
1072
  attestationsAndSigners: CommitteeAttestationsAndSigners,
894
1073
  attestationsAndSignersSignature: Signature,
895
- opts: { txTimeoutAt?: Date; forcePendingBlockNumber?: BlockNumber } = {},
1074
+ opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
896
1075
  ): Promise<void> {
897
1076
  const checkpointHeader = checkpoint.header;
898
1077
 
899
1078
  const blobFields = checkpoint.toBlobFields();
900
- const blobs = getBlobsPerL1Block(blobFields);
1079
+ const blobs = await getBlobsPerL1Block(blobFields);
901
1080
 
902
- const proposeTxArgs = {
1081
+ const proposeTxArgs: L1ProcessArgs = {
903
1082
  header: checkpointHeader,
904
1083
  archive: checkpoint.archive.root.toBuffer(),
905
1084
  blobs,
906
1085
  attestationsAndSigners,
907
1086
  attestationsAndSignersSignature,
1087
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
908
1088
  };
909
1089
 
910
1090
  let ts: bigint;
@@ -925,7 +1105,7 @@ export class SequencerPublisher {
925
1105
  this.log.error(`Checkpoint validation failed. ${err instanceof Error ? err.message : 'No error message'}`, err, {
926
1106
  ...checkpoint.getStats(),
927
1107
  slotNumber: checkpoint.header.slotNumber,
928
- forcePendingBlockNumber: opts.forcePendingBlockNumber,
1108
+ forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
929
1109
  });
930
1110
  throw err;
931
1111
  }
@@ -934,7 +1114,10 @@ export class SequencerPublisher {
934
1114
  await this.addProposeTx(checkpoint, proposeTxArgs, opts, ts);
935
1115
  }
936
1116
 
937
- public enqueueInvalidateBlock(request: InvalidateBlockRequest | undefined, opts: { txTimeoutAt?: Date } = {}) {
1117
+ public enqueueInvalidateCheckpoint(
1118
+ request: InvalidateCheckpointRequest | undefined,
1119
+ opts: { txTimeoutAt?: Date } = {},
1120
+ ) {
938
1121
  if (!request) {
939
1122
  return;
940
1123
  }
@@ -942,9 +1125,9 @@ export class SequencerPublisher {
942
1125
  // We issued the simulation against the rollup contract, so we need to account for the overhead of the multicall3
943
1126
  const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil((Number(request.gasUsed) * 64) / 63)));
944
1127
 
945
- const { gasUsed, blockNumber } = request;
946
- const logData = { gasUsed, blockNumber, gasLimit, opts };
947
- 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);
948
1131
  this.addRequest({
949
1132
  action: `invalidate-by-${request.reason}`,
950
1133
  request: request.request,
@@ -957,9 +1140,9 @@ export class SequencerPublisher {
957
1140
  result.receipt.status === 'success' &&
958
1141
  tryExtractEvent(result.receipt.logs, this.rollupContract.address, RollupAbi, 'CheckpointInvalidated');
959
1142
  if (!success) {
960
- this.log.warn(`Invalidate block ${request.blockNumber} failed`, { ...result, ...logData });
1143
+ this.log.warn(`Invalidate checkpoint ${request.checkpointNumber} failed`, { ...result, ...logData });
961
1144
  } else {
962
- this.log.info(`Invalidate block ${request.blockNumber} succeeded`, { ...result, ...logData });
1145
+ this.log.info(`Invalidate checkpoint ${request.checkpointNumber} succeeded`, { ...result, ...logData });
963
1146
  }
964
1147
  return !!success;
965
1148
  },
@@ -984,13 +1167,30 @@ export class SequencerPublisher {
984
1167
 
985
1168
  this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
986
1169
 
1170
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1171
+
987
1172
  let gasUsed: bigint;
1173
+ const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
988
1174
  try {
989
- ({ 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
990
1176
  this.log.verbose(`Simulation for ${action} succeeded`, { ...logData, request, gasUsed });
991
1177
  } catch (err) {
992
- const viemError = formatViemError(err);
1178
+ const viemError = formatViemError(err, simulateAbi);
993
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
+
994
1194
  return false;
995
1195
  }
996
1196
 
@@ -998,10 +1198,14 @@ export class SequencerPublisher {
998
1198
  const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil((Number(gasUsed) * 64) / 63)));
999
1199
  logData.gasLimit = gasLimit;
1000
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
+
1001
1205
  this.log.debug(`Enqueuing ${action}`, logData);
1002
1206
  this.addRequest({
1003
1207
  action,
1004
- request,
1208
+ request: requestWithAbi,
1005
1209
  gasConfig: { gasLimit },
1006
1210
  lastValidL2Slot: slotNumber,
1007
1211
  checkSuccess: (_req, result) => {
@@ -1038,7 +1242,7 @@ export class SequencerPublisher {
1038
1242
  private async prepareProposeTx(
1039
1243
  encodedData: L1ProcessArgs,
1040
1244
  timestamp: bigint,
1041
- options: { forcePendingBlockNumber?: BlockNumber },
1245
+ options: { forcePendingCheckpointNumber?: CheckpointNumber },
1042
1246
  ) {
1043
1247
  const kzg = Blob.getViemKzgInstance();
1044
1248
  const blobInput = getPrefixedEthBlobCommitments(encodedData.blobs);
@@ -1070,9 +1274,27 @@ export class SequencerPublisher {
1070
1274
  kzg,
1071
1275
  },
1072
1276
  )
1073
- .catch(err => {
1074
- const { message, metaMessages } = formatViemError(err);
1075
- 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
+ });
1076
1298
  throw new Error('Failed to validate blobs');
1077
1299
  });
1078
1300
  }
@@ -1083,8 +1305,7 @@ export class SequencerPublisher {
1083
1305
  header: encodedData.header.toViem(),
1084
1306
  archive: toHex(encodedData.archive),
1085
1307
  oracleInput: {
1086
- // We are currently not modifying these. See #9963
1087
- feeAssetPriceModifier: 0n,
1308
+ feeAssetPriceModifier: encodedData.feeAssetPriceModifier,
1088
1309
  },
1089
1310
  },
1090
1311
  encodedData.attestationsAndSigners.getPackedAttestations(),
@@ -1110,7 +1331,7 @@ export class SequencerPublisher {
1110
1331
  readonly header: ViemHeader;
1111
1332
  readonly archive: `0x${string}`;
1112
1333
  readonly oracleInput: {
1113
- readonly feeAssetPriceModifier: 0n;
1334
+ readonly feeAssetPriceModifier: bigint;
1114
1335
  };
1115
1336
  },
1116
1337
  ViemCommitteeAttestations,
@@ -1119,7 +1340,7 @@ export class SequencerPublisher {
1119
1340
  `0x${string}`,
1120
1341
  ],
1121
1342
  timestamp: bigint,
1122
- options: { forcePendingBlockNumber?: BlockNumber },
1343
+ options: { forcePendingCheckpointNumber?: CheckpointNumber },
1123
1344
  ) {
1124
1345
  const rollupData = encodeFunctionData({
1125
1346
  abi: RollupAbi,
@@ -1128,13 +1349,9 @@ export class SequencerPublisher {
1128
1349
  });
1129
1350
 
1130
1351
  // override the pending checkpoint number if requested
1131
- const optsForcePendingCheckpointNumber =
1132
- options.forcePendingBlockNumber !== undefined
1133
- ? CheckpointNumber.fromBlockNumber(options.forcePendingBlockNumber)
1134
- : undefined;
1135
1352
  const forcePendingCheckpointNumberStateDiff = (
1136
- optsForcePendingCheckpointNumber !== undefined
1137
- ? await this.rollupContract.makePendingCheckpointNumberOverride(optsForcePendingCheckpointNumber)
1353
+ options.forcePendingCheckpointNumber !== undefined
1354
+ ? await this.rollupContract.makePendingCheckpointNumberOverride(options.forcePendingCheckpointNumber)
1138
1355
  : []
1139
1356
  ).flatMap(override => override.stateDiff ?? []);
1140
1357
 
@@ -1156,25 +1373,27 @@ export class SequencerPublisher {
1156
1373
  });
1157
1374
  }
1158
1375
 
1376
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1377
+
1159
1378
  const simulationResult = await this.l1TxUtils
1160
1379
  .simulate(
1161
1380
  {
1162
1381
  to: this.rollupContract.address,
1163
1382
  data: rollupData,
1164
- gas: SequencerPublisher.PROPOSE_GAS_GUESS,
1383
+ gas: MAX_L1_TX_LIMIT,
1165
1384
  ...(this.proposerAddressForSimulation && { from: this.proposerAddressForSimulation.toString() }),
1166
1385
  },
1167
1386
  {
1168
1387
  // @note we add 1n to the timestamp because geth implementation doesn't like simulation timestamp to be equal to the current block timestamp
1169
1388
  time: timestamp + 1n,
1170
1389
  // @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,
1390
+ gasLimit: MAX_L1_TX_LIMIT * 2n,
1172
1391
  },
1173
1392
  stateOverrides,
1174
1393
  RollupAbi,
1175
1394
  {
1176
1395
  // @note fallback gas estimate to use if the node doesn't support simulation API
1177
- fallbackGasEstimate: SequencerPublisher.PROPOSE_GAS_GUESS,
1396
+ fallbackGasEstimate: MAX_L1_TX_LIMIT,
1178
1397
  },
1179
1398
  )
1180
1399
  .catch(err => {
@@ -1184,11 +1403,23 @@ export class SequencerPublisher {
1184
1403
  this.log.debug(`Ignoring expected ValidatorSelection__MissingProposerSignature error in fisherman mode`);
1185
1404
  // Return a minimal simulation result with the fallback gas estimate
1186
1405
  return {
1187
- gasUsed: SequencerPublisher.PROPOSE_GAS_GUESS,
1406
+ gasUsed: MAX_L1_TX_LIMIT,
1188
1407
  logs: [],
1189
1408
  };
1190
1409
  }
1191
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
+ });
1192
1423
  throw err;
1193
1424
  });
1194
1425
 
@@ -1198,7 +1429,7 @@ export class SequencerPublisher {
1198
1429
  private async addProposeTx(
1199
1430
  checkpoint: Checkpoint,
1200
1431
  encodedData: L1ProcessArgs,
1201
- opts: { txTimeoutAt?: Date; forcePendingBlockNumber?: BlockNumber } = {},
1432
+ opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
1202
1433
  timestamp: bigint,
1203
1434
  ): Promise<void> {
1204
1435
  const slot = checkpoint.header.slotNumber;