@aztec/sequencer-client 0.0.1-commit.0c875d939 → 0.0.1-commit.10bd49492

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 (69) hide show
  1. package/dest/client/sequencer-client.d.ts +23 -7
  2. package/dest/client/sequencer-client.d.ts.map +1 -1
  3. package/dest/client/sequencer-client.js +99 -16
  4. package/dest/config.d.ts +24 -5
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +36 -20
  7. package/dest/global_variable_builder/global_builder.d.ts +2 -4
  8. package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
  9. package/dest/publisher/config.d.ts +35 -17
  10. package/dest/publisher/config.d.ts.map +1 -1
  11. package/dest/publisher/config.js +106 -42
  12. package/dest/publisher/index.d.ts +2 -1
  13. package/dest/publisher/index.d.ts.map +1 -1
  14. package/dest/publisher/l1_tx_failed_store/factory.d.ts +11 -0
  15. package/dest/publisher/l1_tx_failed_store/factory.d.ts.map +1 -0
  16. package/dest/publisher/l1_tx_failed_store/factory.js +22 -0
  17. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts +59 -0
  18. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -0
  19. package/dest/publisher/l1_tx_failed_store/failed_tx_store.js +1 -0
  20. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts +15 -0
  21. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts.map +1 -0
  22. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.js +34 -0
  23. package/dest/publisher/l1_tx_failed_store/index.d.ts +4 -0
  24. package/dest/publisher/l1_tx_failed_store/index.d.ts.map +1 -0
  25. package/dest/publisher/l1_tx_failed_store/index.js +2 -0
  26. package/dest/publisher/sequencer-publisher-factory.d.ts +11 -3
  27. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  28. package/dest/publisher/sequencer-publisher-factory.js +27 -2
  29. package/dest/publisher/sequencer-publisher.d.ts +19 -7
  30. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  31. package/dest/publisher/sequencer-publisher.js +285 -25
  32. package/dest/sequencer/checkpoint_proposal_job.d.ts +2 -4
  33. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  34. package/dest/sequencer/checkpoint_proposal_job.js +99 -47
  35. package/dest/sequencer/metrics.d.ts +14 -5
  36. package/dest/sequencer/metrics.d.ts.map +1 -1
  37. package/dest/sequencer/metrics.js +61 -15
  38. package/dest/sequencer/sequencer.d.ts +24 -13
  39. package/dest/sequencer/sequencer.d.ts.map +1 -1
  40. package/dest/sequencer/sequencer.js +35 -39
  41. package/dest/sequencer/timetable.d.ts +4 -3
  42. package/dest/sequencer/timetable.d.ts.map +1 -1
  43. package/dest/sequencer/timetable.js +6 -7
  44. package/dest/sequencer/types.d.ts +2 -2
  45. package/dest/sequencer/types.d.ts.map +1 -1
  46. package/dest/test/index.d.ts +3 -5
  47. package/dest/test/index.d.ts.map +1 -1
  48. package/dest/test/mock_checkpoint_builder.d.ts +4 -6
  49. package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
  50. package/dest/test/mock_checkpoint_builder.js +39 -30
  51. package/package.json +28 -28
  52. package/src/client/sequencer-client.ts +135 -18
  53. package/src/config.ts +45 -27
  54. package/src/global_variable_builder/global_builder.ts +1 -1
  55. package/src/publisher/config.ts +121 -43
  56. package/src/publisher/index.ts +3 -0
  57. package/src/publisher/l1_tx_failed_store/factory.ts +32 -0
  58. package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +55 -0
  59. package/src/publisher/l1_tx_failed_store/file_store_failed_tx_store.ts +46 -0
  60. package/src/publisher/l1_tx_failed_store/index.ts +3 -0
  61. package/src/publisher/sequencer-publisher-factory.ts +38 -6
  62. package/src/publisher/sequencer-publisher.ts +273 -38
  63. package/src/sequencer/checkpoint_proposal_job.ts +133 -56
  64. package/src/sequencer/metrics.ts +68 -18
  65. package/src/sequencer/sequencer.ts +44 -45
  66. package/src/sequencer/timetable.ts +7 -7
  67. package/src/sequencer/types.ts +1 -1
  68. package/src/test/index.ts +2 -4
  69. package/src/test/mock_checkpoint_builder.ts +48 -45
@@ -19,20 +19,22 @@ import {
19
19
  type L1BlobInputs,
20
20
  type L1TxConfig,
21
21
  type L1TxRequest,
22
+ type L1TxUtils,
22
23
  MAX_L1_TX_LIMIT,
23
24
  type TransactionStats,
24
25
  WEI_CONST,
25
26
  } from '@aztec/ethereum/l1-tx-utils';
26
- import type { L1TxUtilsWithBlobs } from '@aztec/ethereum/l1-tx-utils-with-blobs';
27
27
  import { FormattedViemError, formatViemError, mergeAbis, tryExtractEvent } from '@aztec/ethereum/utils';
28
28
  import { sumBigint } from '@aztec/foundation/bigint';
29
29
  import { toHex as toPaddedHex } from '@aztec/foundation/bigint-buffer';
30
30
  import { CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
31
31
  import { pick } from '@aztec/foundation/collection';
32
32
  import type { Fr } from '@aztec/foundation/curves/bn254';
33
+ import { TimeoutError } from '@aztec/foundation/error';
33
34
  import { EthAddress } from '@aztec/foundation/eth-address';
34
35
  import { Signature, type ViemSignature } from '@aztec/foundation/eth-signature';
35
36
  import { type Logger, createLogger } from '@aztec/foundation/log';
37
+ import { makeBackoff, retry } from '@aztec/foundation/retry';
36
38
  import { bufferToHex } from '@aztec/foundation/string';
37
39
  import { DateProvider, Timer } from '@aztec/foundation/timer';
38
40
  import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
@@ -44,9 +46,19 @@ import type { CheckpointHeader } from '@aztec/stdlib/rollup';
44
46
  import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
45
47
  import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
46
48
 
47
- import { type StateOverride, type TransactionReceipt, type TypedDataDefinition, encodeFunctionData, toHex } from 'viem';
48
-
49
- import type { PublisherConfig, TxSenderConfig } from './config.js';
49
+ import {
50
+ type Hex,
51
+ type StateOverride,
52
+ type TransactionReceipt,
53
+ type TypedDataDefinition,
54
+ encodeFunctionData,
55
+ keccak256,
56
+ multicall3Abi,
57
+ toHex,
58
+ } from 'viem';
59
+
60
+ import type { SequencerPublisherConfig } from './config.js';
61
+ import { type FailedL1Tx, type L1TxFailedStore, createL1TxFailedStore } from './l1_tx_failed_store/index.js';
50
62
  import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
51
63
 
52
64
  /** Arguments to the process method of the rollup contract */
@@ -108,6 +120,7 @@ export class SequencerPublisher {
108
120
  private interrupted = false;
109
121
  private metrics: SequencerPublisherMetrics;
110
122
  public epochCache: EpochCache;
123
+ private failedTxStore?: Promise<L1TxFailedStore | undefined>;
111
124
 
112
125
  protected governanceLog = createLogger('sequencer:publisher:governance');
113
126
  protected slashingLog = createLogger('sequencer:publisher:slashing');
@@ -115,6 +128,7 @@ export class SequencerPublisher {
115
128
  protected lastActions: Partial<Record<Action, SlotNumber>> = {};
116
129
 
117
130
  private isPayloadEmptyCache: Map<string, boolean> = new Map<string, boolean>();
131
+ private payloadProposedCache: Set<string> = new Set<string>();
118
132
 
119
133
  protected log: Logger;
120
134
  protected ethereumSlotDuration: bigint;
@@ -124,6 +138,9 @@ export class SequencerPublisher {
124
138
  /** Address to use for simulations in fisherman mode (actual proposer's address) */
125
139
  private proposerAddressForSimulation?: EthAddress;
126
140
 
141
+ /** Optional callback to obtain a replacement publisher when the current one fails to send. */
142
+ private getNextPublisher?: (excludeAddresses: EthAddress[]) => Promise<L1TxUtils | undefined>;
143
+
127
144
  /** L1 fee analyzer for fisherman mode */
128
145
  private l1FeeAnalyzer?: L1FeeAnalyzer;
129
146
 
@@ -136,7 +153,7 @@ export class SequencerPublisher {
136
153
  // Gas report for VotingWithSigTest shows a max gas of 100k, but we've seen it cost 700k+ in testnet
137
154
  public static VOTE_GAS_GUESS: bigint = 800_000n;
138
155
 
139
- public l1TxUtils: L1TxUtilsWithBlobs;
156
+ public l1TxUtils: L1TxUtils;
140
157
  public rollupContract: RollupContract;
141
158
  public govProposerContract: GovernanceProposerContract;
142
159
  public slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
@@ -147,11 +164,12 @@ export class SequencerPublisher {
147
164
  protected requests: RequestWithExpiry[] = [];
148
165
 
149
166
  constructor(
150
- private config: TxSenderConfig & PublisherConfig & Pick<L1ContractsConfig, 'ethereumSlotDuration'>,
167
+ private config: Pick<SequencerPublisherConfig, 'fishermanMode' | 'l1TxFailedStore'> &
168
+ Pick<L1ContractsConfig, 'ethereumSlotDuration'> & { l1ChainId: number },
151
169
  deps: {
152
170
  telemetry?: TelemetryClient;
153
171
  blobClient: BlobClientInterface;
154
- l1TxUtils: L1TxUtilsWithBlobs;
172
+ l1TxUtils: L1TxUtils;
155
173
  rollupContract: RollupContract;
156
174
  slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
157
175
  governanceProposerContract: GovernanceProposerContract;
@@ -161,6 +179,7 @@ export class SequencerPublisher {
161
179
  metrics: SequencerPublisherMetrics;
162
180
  lastActions: Partial<Record<Action, SlotNumber>>;
163
181
  log?: Logger;
182
+ getNextPublisher?: (excludeAddresses: EthAddress[]) => Promise<L1TxUtils | undefined>;
164
183
  },
165
184
  ) {
166
185
  this.log = deps.log ?? createLogger('sequencer:publisher');
@@ -174,6 +193,7 @@ export class SequencerPublisher {
174
193
  this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
175
194
  this.tracer = telemetry.getTracer('SequencerPublisher');
176
195
  this.l1TxUtils = deps.l1TxUtils;
196
+ this.getNextPublisher = deps.getNextPublisher;
177
197
 
178
198
  this.rollupContract = deps.rollupContract;
179
199
 
@@ -202,6 +222,31 @@ export class SequencerPublisher {
202
222
  this.rollupContract,
203
223
  createLogger('sequencer:publisher:price-oracle'),
204
224
  );
225
+
226
+ // Initialize failed L1 tx store (optional, for test networks)
227
+ this.failedTxStore = createL1TxFailedStore(config.l1TxFailedStore, this.log);
228
+ }
229
+
230
+ /**
231
+ * Backs up a failed L1 transaction to the configured store for debugging.
232
+ * Does nothing if no store is configured.
233
+ */
234
+ private backupFailedTx(failedTx: Omit<FailedL1Tx, 'timestamp'>): void {
235
+ if (!this.failedTxStore) {
236
+ return;
237
+ }
238
+
239
+ const tx: FailedL1Tx = {
240
+ ...failedTx,
241
+ timestamp: Date.now(),
242
+ };
243
+
244
+ // Fire and forget - don't block on backup
245
+ void this.failedTxStore
246
+ .then(store => store?.saveFailedTx(tx))
247
+ .catch(err => {
248
+ this.log.warn(`Failed to backup failed L1 tx to store`, err);
249
+ });
205
250
  }
206
251
 
207
252
  public getRollupContract(): RollupContract {
@@ -383,19 +428,36 @@ export class SequencerPublisher {
383
428
  validRequests.sort((a, b) => compareActions(a.action, b.action));
384
429
 
385
430
  try {
431
+ // Capture context for failed tx backup before sending
432
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
433
+ const multicallData = encodeFunctionData({
434
+ abi: multicall3Abi,
435
+ functionName: 'aggregate3',
436
+ args: [
437
+ validRequests.map(r => ({
438
+ target: r.request.to!,
439
+ callData: r.request.data!,
440
+ allowFailure: true,
441
+ })),
442
+ ],
443
+ });
444
+ const blobDataHex = blobConfig?.blobs?.map(b => toHex(b)) as Hex[] | undefined;
445
+
446
+ const txContext = { multicallData, blobData: blobDataHex, l1BlockNumber };
447
+
386
448
  this.log.debug('Forwarding transactions', {
387
449
  validRequests: validRequests.map(request => request.action),
388
450
  txConfig,
389
451
  });
390
- const result = await Multicall3.forward(
391
- validRequests.map(request => request.request),
392
- this.l1TxUtils,
393
- txConfig,
394
- blobConfig,
395
- this.rollupContract.address,
396
- this.log,
452
+ const result = await this.forwardWithPublisherRotation(validRequests, txConfig, blobConfig);
453
+ if (result === undefined) {
454
+ return undefined;
455
+ }
456
+ const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(
457
+ validRequests,
458
+ result,
459
+ txContext,
397
460
  );
398
- const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(validRequests, result);
399
461
  return { result, expiredActions, sentActions: validActions, successfulActions, failedActions };
400
462
  } catch (err) {
401
463
  const viemError = formatViemError(err);
@@ -413,13 +475,76 @@ export class SequencerPublisher {
413
475
  }
414
476
  }
415
477
 
478
+ /**
479
+ * Forwards transactions via Multicall3, rotating to the next available publisher if a send
480
+ * failure occurs (i.e. the tx never reached the chain).
481
+ * On-chain reverts and simulation errors are returned as-is without rotation.
482
+ */
483
+ private async forwardWithPublisherRotation(
484
+ validRequests: RequestWithExpiry[],
485
+ txConfig: RequestWithExpiry['gasConfig'],
486
+ blobConfig: L1BlobInputs | undefined,
487
+ ) {
488
+ const triedAddresses: EthAddress[] = [];
489
+ let currentPublisher = this.l1TxUtils;
490
+
491
+ while (true) {
492
+ triedAddresses.push(currentPublisher.getSenderAddress());
493
+ try {
494
+ const result = await Multicall3.forward(
495
+ validRequests.map(r => r.request),
496
+ currentPublisher,
497
+ txConfig,
498
+ blobConfig,
499
+ this.rollupContract.address,
500
+ this.log,
501
+ );
502
+ this.l1TxUtils = currentPublisher;
503
+ return result;
504
+ } catch (err) {
505
+ if (err instanceof TimeoutError) {
506
+ throw err;
507
+ }
508
+ const viemError = formatViemError(err);
509
+ if (!this.getNextPublisher) {
510
+ this.log.error('Failed to publish bundled transactions', viemError);
511
+ return undefined;
512
+ }
513
+ this.log.warn(
514
+ `Publisher ${currentPublisher.getSenderAddress()} failed to send, rotating to next publisher`,
515
+ viemError,
516
+ );
517
+ const nextPublisher = await this.getNextPublisher([...triedAddresses]);
518
+ if (!nextPublisher) {
519
+ this.log.error('All available publishers exhausted, failed to publish bundled transactions');
520
+ return undefined;
521
+ }
522
+ currentPublisher = nextPublisher;
523
+ }
524
+ }
525
+ }
526
+
416
527
  private callbackBundledTransactions(
417
528
  requests: RequestWithExpiry[],
418
- result?: { receipt: TransactionReceipt } | FormattedViemError,
529
+ result: { receipt: TransactionReceipt; errorMsg?: string } | FormattedViemError | undefined,
530
+ txContext: { multicallData: Hex; blobData?: Hex[]; l1BlockNumber: bigint },
419
531
  ) {
420
532
  const actionsListStr = requests.map(r => r.action).join(', ');
421
533
  if (result instanceof FormattedViemError) {
422
534
  this.log.error(`Failed to publish bundled transactions (${actionsListStr})`, result);
535
+ this.backupFailedTx({
536
+ id: keccak256(txContext.multicallData),
537
+ failureType: 'send-error',
538
+ request: { to: MULTI_CALL_3_ADDRESS, data: txContext.multicallData },
539
+ blobData: txContext.blobData,
540
+ l1BlockNumber: txContext.l1BlockNumber.toString(),
541
+ error: { message: result.message, name: result.name },
542
+ context: {
543
+ actions: requests.map(r => r.action),
544
+ requests: requests.map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
545
+ sender: this.getSenderAddress().toString(),
546
+ },
547
+ });
423
548
  return { failedActions: requests.map(r => r.action) };
424
549
  } else {
425
550
  this.log.verbose(`Published bundled transactions (${actionsListStr})`, { result, requests });
@@ -432,6 +557,30 @@ export class SequencerPublisher {
432
557
  failedActions.push(request.action);
433
558
  }
434
559
  }
560
+ // Single backup for the whole reverted tx
561
+ if (failedActions.length > 0 && result?.receipt?.status === 'reverted') {
562
+ this.backupFailedTx({
563
+ id: result.receipt.transactionHash,
564
+ failureType: 'revert',
565
+ request: { to: MULTI_CALL_3_ADDRESS, data: txContext.multicallData },
566
+ blobData: txContext.blobData,
567
+ l1BlockNumber: result.receipt.blockNumber.toString(),
568
+ receipt: {
569
+ transactionHash: result.receipt.transactionHash,
570
+ blockNumber: result.receipt.blockNumber.toString(),
571
+ gasUsed: (result.receipt.gasUsed ?? 0n).toString(),
572
+ status: 'reverted',
573
+ },
574
+ error: { message: result.errorMsg ?? 'Transaction reverted' },
575
+ context: {
576
+ actions: failedActions,
577
+ requests: requests
578
+ .filter(r => failedActions.includes(r.action))
579
+ .map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
580
+ sender: this.getSenderAddress().toString(),
581
+ },
582
+ });
583
+ }
435
584
  return { successfulActions, failedActions };
436
585
  }
437
586
  }
@@ -543,6 +692,8 @@ export class SequencerPublisher {
543
692
  const request = this.buildInvalidateCheckpointRequest(validationResult);
544
693
  this.log.debug(`Simulating invalidate checkpoint ${checkpointNumber}`, { ...logData, request });
545
694
 
695
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
696
+
546
697
  try {
547
698
  const { gasUsed } = await this.l1TxUtils.simulate(
548
699
  request,
@@ -594,6 +745,18 @@ export class SequencerPublisher {
594
745
 
595
746
  // Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
596
747
  this.log.error(`Simulation for invalidate checkpoint ${checkpointNumber} failed`, viemError, logData);
748
+ this.backupFailedTx({
749
+ id: keccak256(request.data!),
750
+ failureType: 'simulation',
751
+ request: { to: request.to!, data: request.data!, value: request.value?.toString() },
752
+ l1BlockNumber: l1BlockNumber.toString(),
753
+ error: { message: viemError.message, name: viemError.name },
754
+ context: {
755
+ actions: [`invalidate-${reason}`],
756
+ checkpointNumber,
757
+ sender: this.getSenderAddress().toString(),
758
+ },
759
+ });
597
760
  throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, { cause: viemError });
598
761
  }
599
762
  }
@@ -639,24 +802,8 @@ export class SequencerPublisher {
639
802
  options: { forcePendingCheckpointNumber?: CheckpointNumber },
640
803
  ): Promise<bigint> {
641
804
  const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
642
-
643
- // TODO(palla/mbps): This should not be needed, there's no flow where we propose with zero attestations. Or is there?
644
- // If we have no attestations, we still need to provide the empty attestations
645
- // so that the committee is recalculated correctly
646
- // const ignoreSignatures = attestationsAndSigners.attestations.length === 0;
647
- // if (ignoreSignatures) {
648
- // const { committee } = await this.epochCache.getCommittee(block.header.globalVariables.slotNumber);
649
- // if (!committee) {
650
- // this.log.warn(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
651
- // throw new Error(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
652
- // }
653
- // attestationsAndSigners.attestations = committee.map(committeeMember =>
654
- // CommitteeAttestation.fromAddress(committeeMember),
655
- // );
656
- // }
657
-
658
805
  const blobFields = checkpoint.toBlobFields();
659
- const blobs = getBlobsPerL1Block(blobFields);
806
+ const blobs = await getBlobsPerL1Block(blobFields);
660
807
  const blobInput = getPrefixedEthBlobCommitments(blobs);
661
808
 
662
809
  const args = [
@@ -713,6 +860,32 @@ export class SequencerPublisher {
713
860
  return false;
714
861
  }
715
862
 
863
+ // Check if payload was already submitted to governance
864
+ const cacheKey = payload.toString();
865
+ if (!this.payloadProposedCache.has(cacheKey)) {
866
+ try {
867
+ const l1StartBlock = await this.rollupContract.getL1StartBlock();
868
+ const proposed = await retry(
869
+ () => base.hasPayloadBeenProposed(payload.toString(), l1StartBlock),
870
+ 'Check if payload was proposed',
871
+ makeBackoff([0, 1, 2]),
872
+ this.log,
873
+ true,
874
+ );
875
+ if (proposed) {
876
+ this.payloadProposedCache.add(cacheKey);
877
+ }
878
+ } catch (err) {
879
+ this.log.warn(`Failed to check if payload ${payload} was proposed after retries, skipping signal`, err);
880
+ return false;
881
+ }
882
+ }
883
+
884
+ if (this.payloadProposedCache.has(cacheKey)) {
885
+ this.log.info(`Payload ${payload} was already proposed to governance, stopping signals`);
886
+ return false;
887
+ }
888
+
716
889
  const cachedLastVote = this.lastActions[signalType];
717
890
  this.lastActions[signalType] = slotNumber;
718
891
  const action = signalType;
@@ -731,11 +904,26 @@ export class SequencerPublisher {
731
904
  lastValidL2Slot: slotNumber,
732
905
  });
733
906
 
907
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
908
+
734
909
  try {
735
910
  await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
736
911
  this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
737
912
  } catch (err) {
738
- this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, err);
913
+ const viemError = formatViemError(err);
914
+ this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, viemError);
915
+ this.backupFailedTx({
916
+ id: keccak256(request.data!),
917
+ failureType: 'simulation',
918
+ request: { to: request.to!, data: request.data!, value: request.value?.toString() },
919
+ l1BlockNumber: l1BlockNumber.toString(),
920
+ error: { message: viemError.message, name: viemError.name },
921
+ context: {
922
+ actions: [action],
923
+ slot: slotNumber,
924
+ sender: this.getSenderAddress().toString(),
925
+ },
926
+ });
739
927
  // Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
740
928
  }
741
929
 
@@ -940,7 +1128,7 @@ export class SequencerPublisher {
940
1128
  const checkpointHeader = checkpoint.header;
941
1129
 
942
1130
  const blobFields = checkpoint.toBlobFields();
943
- const blobs = getBlobsPerL1Block(blobFields);
1131
+ const blobs = await getBlobsPerL1Block(blobFields);
944
1132
 
945
1133
  const proposeTxArgs: L1ProcessArgs = {
946
1134
  header: checkpointHeader,
@@ -1031,6 +1219,8 @@ export class SequencerPublisher {
1031
1219
 
1032
1220
  this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
1033
1221
 
1222
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1223
+
1034
1224
  let gasUsed: bigint;
1035
1225
  const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
1036
1226
  try {
@@ -1040,6 +1230,19 @@ export class SequencerPublisher {
1040
1230
  const viemError = formatViemError(err, simulateAbi);
1041
1231
  this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
1042
1232
 
1233
+ this.backupFailedTx({
1234
+ id: keccak256(request.data!),
1235
+ failureType: 'simulation',
1236
+ request: { to: request.to!, data: request.data!, value: request.value?.toString() },
1237
+ l1BlockNumber: l1BlockNumber.toString(),
1238
+ error: { message: viemError.message, name: viemError.name },
1239
+ context: {
1240
+ actions: [action],
1241
+ slot: slotNumber,
1242
+ sender: this.getSenderAddress().toString(),
1243
+ },
1244
+ });
1245
+
1043
1246
  return false;
1044
1247
  }
1045
1248
 
@@ -1123,9 +1326,27 @@ export class SequencerPublisher {
1123
1326
  kzg,
1124
1327
  },
1125
1328
  )
1126
- .catch(err => {
1127
- const { message, metaMessages } = formatViemError(err);
1128
- this.log.error(`Failed to validate blobs`, message, { metaMessages });
1329
+ .catch(async err => {
1330
+ const viemError = formatViemError(err);
1331
+ this.log.error(`Failed to validate blobs`, viemError.message, { metaMessages: viemError.metaMessages });
1332
+ const validateBlobsData = encodeFunctionData({
1333
+ abi: RollupAbi,
1334
+ functionName: 'validateBlobs',
1335
+ args: [blobInput],
1336
+ });
1337
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1338
+ this.backupFailedTx({
1339
+ id: keccak256(validateBlobsData),
1340
+ failureType: 'simulation',
1341
+ request: { to: this.rollupContract.address as Hex, data: validateBlobsData },
1342
+ blobData: encodedData.blobs.map(b => toHex(b.data)) as Hex[],
1343
+ l1BlockNumber: l1BlockNumber.toString(),
1344
+ error: { message: viemError.message, name: viemError.name },
1345
+ context: {
1346
+ actions: ['validate-blobs'],
1347
+ sender: this.getSenderAddress().toString(),
1348
+ },
1349
+ });
1129
1350
  throw new Error('Failed to validate blobs');
1130
1351
  });
1131
1352
  }
@@ -1204,6 +1425,8 @@ export class SequencerPublisher {
1204
1425
  });
1205
1426
  }
1206
1427
 
1428
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1429
+
1207
1430
  const simulationResult = await this.l1TxUtils
1208
1431
  .simulate(
1209
1432
  {
@@ -1237,6 +1460,18 @@ export class SequencerPublisher {
1237
1460
  };
1238
1461
  }
1239
1462
  this.log.error(`Failed to simulate propose tx`, viemError);
1463
+ this.backupFailedTx({
1464
+ id: keccak256(rollupData),
1465
+ failureType: 'simulation',
1466
+ request: { to: this.rollupContract.address, data: rollupData },
1467
+ l1BlockNumber: l1BlockNumber.toString(),
1468
+ error: { message: viemError.message, name: viemError.name },
1469
+ context: {
1470
+ actions: ['propose'],
1471
+ slot: Number(args[0].header.slotNumber),
1472
+ sender: this.getSenderAddress().toString(),
1473
+ },
1474
+ });
1240
1475
  throw err;
1241
1476
  });
1242
1477