@aztec/sequencer-client 0.0.1-commit.cd76b27 → 0.0.1-commit.ce4f8c4f2

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 (73) hide show
  1. package/dest/client/sequencer-client.d.ts +4 -1
  2. package/dest/client/sequencer-client.d.ts.map +1 -1
  3. package/dest/client/sequencer-client.js +46 -23
  4. package/dest/config.d.ts +25 -5
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +31 -17
  7. package/dest/global_variable_builder/global_builder.d.ts +13 -7
  8. package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
  9. package/dest/global_variable_builder/global_builder.js +22 -21
  10. package/dest/global_variable_builder/index.d.ts +2 -2
  11. package/dest/global_variable_builder/index.d.ts.map +1 -1
  12. package/dest/publisher/config.d.ts +17 -1
  13. package/dest/publisher/config.d.ts.map +1 -1
  14. package/dest/publisher/config.js +23 -3
  15. package/dest/publisher/index.d.ts +2 -1
  16. package/dest/publisher/index.d.ts.map +1 -1
  17. package/dest/publisher/l1_tx_failed_store/factory.d.ts +11 -0
  18. package/dest/publisher/l1_tx_failed_store/factory.d.ts.map +1 -0
  19. package/dest/publisher/l1_tx_failed_store/factory.js +22 -0
  20. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts +59 -0
  21. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -0
  22. package/dest/publisher/l1_tx_failed_store/failed_tx_store.js +1 -0
  23. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts +15 -0
  24. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts.map +1 -0
  25. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.js +34 -0
  26. package/dest/publisher/l1_tx_failed_store/index.d.ts +4 -0
  27. package/dest/publisher/l1_tx_failed_store/index.d.ts.map +1 -0
  28. package/dest/publisher/l1_tx_failed_store/index.js +2 -0
  29. package/dest/publisher/sequencer-publisher-factory.d.ts +3 -3
  30. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  31. package/dest/publisher/sequencer-publisher-factory.js +16 -2
  32. package/dest/publisher/sequencer-publisher.d.ts +19 -4
  33. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  34. package/dest/publisher/sequencer-publisher.js +294 -18
  35. package/dest/sequencer/checkpoint_proposal_job.d.ts +13 -7
  36. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  37. package/dest/sequencer/checkpoint_proposal_job.js +206 -130
  38. package/dest/sequencer/events.d.ts +2 -1
  39. package/dest/sequencer/events.d.ts.map +1 -1
  40. package/dest/sequencer/metrics.d.ts +5 -1
  41. package/dest/sequencer/metrics.d.ts.map +1 -1
  42. package/dest/sequencer/metrics.js +11 -0
  43. package/dest/sequencer/sequencer.d.ts +18 -9
  44. package/dest/sequencer/sequencer.d.ts.map +1 -1
  45. package/dest/sequencer/sequencer.js +77 -62
  46. package/dest/sequencer/timetable.d.ts +4 -3
  47. package/dest/sequencer/timetable.d.ts.map +1 -1
  48. package/dest/sequencer/timetable.js +6 -7
  49. package/dest/sequencer/types.d.ts +2 -2
  50. package/dest/sequencer/types.d.ts.map +1 -1
  51. package/dest/test/mock_checkpoint_builder.d.ts +7 -9
  52. package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
  53. package/dest/test/mock_checkpoint_builder.js +39 -30
  54. package/package.json +27 -28
  55. package/src/client/sequencer-client.ts +56 -21
  56. package/src/config.ts +39 -19
  57. package/src/global_variable_builder/global_builder.ts +22 -23
  58. package/src/global_variable_builder/index.ts +1 -1
  59. package/src/publisher/config.ts +41 -0
  60. package/src/publisher/index.ts +3 -0
  61. package/src/publisher/l1_tx_failed_store/factory.ts +32 -0
  62. package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +55 -0
  63. package/src/publisher/l1_tx_failed_store/file_store_failed_tx_store.ts +46 -0
  64. package/src/publisher/l1_tx_failed_store/index.ts +3 -0
  65. package/src/publisher/sequencer-publisher-factory.ts +18 -3
  66. package/src/publisher/sequencer-publisher.ts +281 -26
  67. package/src/sequencer/checkpoint_proposal_job.ts +277 -142
  68. package/src/sequencer/events.ts +1 -1
  69. package/src/sequencer/metrics.ts +14 -0
  70. package/src/sequencer/sequencer.ts +105 -69
  71. package/src/sequencer/timetable.ts +7 -7
  72. package/src/sequencer/types.ts +1 -1
  73. package/src/test/mock_checkpoint_builder.ts +51 -48
@@ -28,8 +28,10 @@ import { FormattedViemError, formatViemError, mergeAbis, tryExtractEvent } from
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
+ import { trimmedBytesLength } from '@aztec/foundation/buffer';
31
32
  import { pick } from '@aztec/foundation/collection';
32
33
  import type { Fr } from '@aztec/foundation/curves/bn254';
34
+ import { TimeoutError } from '@aztec/foundation/error';
33
35
  import { EthAddress } from '@aztec/foundation/eth-address';
34
36
  import { Signature, type ViemSignature } from '@aztec/foundation/eth-signature';
35
37
  import { type Logger, createLogger } from '@aztec/foundation/log';
@@ -40,14 +42,25 @@ import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
40
42
  import { type ProposerSlashAction, encodeSlashConsensusVotes } from '@aztec/slasher';
41
43
  import { CommitteeAttestationsAndSigners, type ValidateCheckpointResult } from '@aztec/stdlib/block';
42
44
  import type { Checkpoint } from '@aztec/stdlib/checkpoint';
45
+ import { getNextL1SlotTimestamp } from '@aztec/stdlib/epoch-helpers';
43
46
  import { SlashFactoryContract } from '@aztec/stdlib/l1-contracts';
44
47
  import type { CheckpointHeader } from '@aztec/stdlib/rollup';
45
48
  import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
46
49
  import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
47
50
 
48
- import { type StateOverride, type TransactionReceipt, type TypedDataDefinition, encodeFunctionData, toHex } from 'viem';
51
+ import {
52
+ type Hex,
53
+ type StateOverride,
54
+ type TransactionReceipt,
55
+ type TypedDataDefinition,
56
+ encodeFunctionData,
57
+ keccak256,
58
+ multicall3Abi,
59
+ toHex,
60
+ } from 'viem';
49
61
 
50
62
  import type { SequencerPublisherConfig } from './config.js';
63
+ import { type FailedL1Tx, type L1TxFailedStore, createL1TxFailedStore } from './l1_tx_failed_store/index.js';
51
64
  import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
52
65
 
53
66
  /** Arguments to the process method of the rollup contract */
@@ -109,6 +122,7 @@ export class SequencerPublisher {
109
122
  private interrupted = false;
110
123
  private metrics: SequencerPublisherMetrics;
111
124
  public epochCache: EpochCache;
125
+ private failedTxStore?: Promise<L1TxFailedStore | undefined>;
112
126
 
113
127
  protected governanceLog = createLogger('sequencer:publisher:governance');
114
128
  protected slashingLog = createLogger('sequencer:publisher:slashing');
@@ -120,12 +134,17 @@ export class SequencerPublisher {
120
134
 
121
135
  protected log: Logger;
122
136
  protected ethereumSlotDuration: bigint;
137
+ protected aztecSlotDuration: bigint;
138
+ private dateProvider: DateProvider;
123
139
 
124
140
  private blobClient: BlobClientInterface;
125
141
 
126
142
  /** Address to use for simulations in fisherman mode (actual proposer's address) */
127
143
  private proposerAddressForSimulation?: EthAddress;
128
144
 
145
+ /** Optional callback to obtain a replacement publisher when the current one fails to send. */
146
+ private getNextPublisher?: (excludeAddresses: EthAddress[]) => Promise<L1TxUtils | undefined>;
147
+
129
148
  /** L1 fee analyzer for fisherman mode */
130
149
  private l1FeeAnalyzer?: L1FeeAnalyzer;
131
150
 
@@ -149,8 +168,8 @@ export class SequencerPublisher {
149
168
  protected requests: RequestWithExpiry[] = [];
150
169
 
151
170
  constructor(
152
- private config: Pick<SequencerPublisherConfig, 'fishermanMode'> &
153
- Pick<L1ContractsConfig, 'ethereumSlotDuration'> & { l1ChainId: number },
171
+ private config: Pick<SequencerPublisherConfig, 'fishermanMode' | 'l1TxFailedStore'> &
172
+ Pick<L1ContractsConfig, 'ethereumSlotDuration' | 'aztecSlotDuration'> & { l1ChainId: number },
154
173
  deps: {
155
174
  telemetry?: TelemetryClient;
156
175
  blobClient: BlobClientInterface;
@@ -164,10 +183,13 @@ export class SequencerPublisher {
164
183
  metrics: SequencerPublisherMetrics;
165
184
  lastActions: Partial<Record<Action, SlotNumber>>;
166
185
  log?: Logger;
186
+ getNextPublisher?: (excludeAddresses: EthAddress[]) => Promise<L1TxUtils | undefined>;
167
187
  },
168
188
  ) {
169
189
  this.log = deps.log ?? createLogger('sequencer:publisher');
170
190
  this.ethereumSlotDuration = BigInt(config.ethereumSlotDuration);
191
+ this.aztecSlotDuration = BigInt(config.aztecSlotDuration);
192
+ this.dateProvider = deps.dateProvider;
171
193
  this.epochCache = deps.epochCache;
172
194
  this.lastActions = deps.lastActions;
173
195
 
@@ -177,6 +199,7 @@ export class SequencerPublisher {
177
199
  this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
178
200
  this.tracer = telemetry.getTracer('SequencerPublisher');
179
201
  this.l1TxUtils = deps.l1TxUtils;
202
+ this.getNextPublisher = deps.getNextPublisher;
180
203
 
181
204
  this.rollupContract = deps.rollupContract;
182
205
 
@@ -205,6 +228,31 @@ export class SequencerPublisher {
205
228
  this.rollupContract,
206
229
  createLogger('sequencer:publisher:price-oracle'),
207
230
  );
231
+
232
+ // Initialize failed L1 tx store (optional, for test networks)
233
+ this.failedTxStore = createL1TxFailedStore(config.l1TxFailedStore, this.log);
234
+ }
235
+
236
+ /**
237
+ * Backs up a failed L1 transaction to the configured store for debugging.
238
+ * Does nothing if no store is configured.
239
+ */
240
+ private backupFailedTx(failedTx: Omit<FailedL1Tx, 'timestamp'>): void {
241
+ if (!this.failedTxStore) {
242
+ return;
243
+ }
244
+
245
+ const tx: FailedL1Tx = {
246
+ ...failedTx,
247
+ timestamp: Date.now(),
248
+ };
249
+
250
+ // Fire and forget - don't block on backup
251
+ void this.failedTxStore
252
+ .then(store => store?.saveFailedTx(tx))
253
+ .catch(err => {
254
+ this.log.warn(`Failed to backup failed L1 tx to store`, err);
255
+ });
208
256
  }
209
257
 
210
258
  public getRollupContract(): RollupContract {
@@ -243,7 +291,7 @@ export class SequencerPublisher {
243
291
  }
244
292
 
245
293
  public getCurrentL2Slot(): SlotNumber {
246
- return this.epochCache.getEpochAndSlotNow().slot;
294
+ return this.epochCache.getSlotNow();
247
295
  }
248
296
 
249
297
  /**
@@ -356,8 +404,8 @@ export class SequencerPublisher {
356
404
  // @note - we can only have one blob config per bundle
357
405
  // find requests with gas and blob configs
358
406
  // See https://github.com/AztecProtocol/aztec-packages/issues/11513
359
- const gasConfigs = requestsToProcess.filter(request => request.gasConfig).map(request => request.gasConfig);
360
- const blobConfigs = requestsToProcess.filter(request => request.blobConfig).map(request => request.blobConfig);
407
+ const gasConfigs = validRequests.filter(request => request.gasConfig).map(request => request.gasConfig);
408
+ const blobConfigs = validRequests.filter(request => request.blobConfig).map(request => request.blobConfig);
361
409
 
362
410
  if (blobConfigs.length > 1) {
363
411
  throw new Error('Multiple blob configs found');
@@ -386,19 +434,36 @@ export class SequencerPublisher {
386
434
  validRequests.sort((a, b) => compareActions(a.action, b.action));
387
435
 
388
436
  try {
437
+ // Capture context for failed tx backup before sending
438
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
439
+ const multicallData = encodeFunctionData({
440
+ abi: multicall3Abi,
441
+ functionName: 'aggregate3',
442
+ args: [
443
+ validRequests.map(r => ({
444
+ target: r.request.to!,
445
+ callData: r.request.data!,
446
+ allowFailure: true,
447
+ })),
448
+ ],
449
+ });
450
+ const blobDataHex = blobConfig?.blobs?.map(b => toHex(b)) as Hex[] | undefined;
451
+
452
+ const txContext = { multicallData, blobData: blobDataHex, l1BlockNumber };
453
+
389
454
  this.log.debug('Forwarding transactions', {
390
455
  validRequests: validRequests.map(request => request.action),
391
456
  txConfig,
392
457
  });
393
- const result = await Multicall3.forward(
394
- validRequests.map(request => request.request),
395
- this.l1TxUtils,
396
- txConfig,
397
- blobConfig,
398
- this.rollupContract.address,
399
- this.log,
458
+ const result = await this.forwardWithPublisherRotation(validRequests, txConfig, blobConfig);
459
+ if (result === undefined) {
460
+ return undefined;
461
+ }
462
+ const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(
463
+ validRequests,
464
+ result,
465
+ txContext,
400
466
  );
401
- const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(validRequests, result);
402
467
  return { result, expiredActions, sentActions: validActions, successfulActions, failedActions };
403
468
  } catch (err) {
404
469
  const viemError = formatViemError(err);
@@ -416,16 +481,88 @@ export class SequencerPublisher {
416
481
  }
417
482
  }
418
483
 
484
+ /**
485
+ * Forwards transactions via Multicall3, rotating to the next available publisher if a send
486
+ * failure occurs (i.e. the tx never reached the chain).
487
+ * On-chain reverts and simulation errors are returned as-is without rotation.
488
+ */
489
+ private async forwardWithPublisherRotation(
490
+ validRequests: RequestWithExpiry[],
491
+ txConfig: RequestWithExpiry['gasConfig'],
492
+ blobConfig: L1BlobInputs | undefined,
493
+ ) {
494
+ const triedAddresses: EthAddress[] = [];
495
+ let currentPublisher = this.l1TxUtils;
496
+
497
+ while (true) {
498
+ triedAddresses.push(currentPublisher.getSenderAddress());
499
+ try {
500
+ const result = await Multicall3.forward(
501
+ validRequests.map(r => r.request),
502
+ currentPublisher,
503
+ txConfig,
504
+ blobConfig,
505
+ this.rollupContract.address,
506
+ this.log,
507
+ );
508
+ this.l1TxUtils = currentPublisher;
509
+ return result;
510
+ } catch (err) {
511
+ if (err instanceof TimeoutError) {
512
+ throw err;
513
+ }
514
+ const viemError = formatViemError(err);
515
+ if (!this.getNextPublisher) {
516
+ this.log.error('Failed to publish bundled transactions', viemError);
517
+ return undefined;
518
+ }
519
+ this.log.warn(
520
+ `Publisher ${currentPublisher.getSenderAddress()} failed to send, rotating to next publisher`,
521
+ viemError,
522
+ );
523
+ const nextPublisher = await this.getNextPublisher([...triedAddresses]);
524
+ if (!nextPublisher) {
525
+ this.log.error('All available publishers exhausted, failed to publish bundled transactions');
526
+ return undefined;
527
+ }
528
+ currentPublisher = nextPublisher;
529
+ }
530
+ }
531
+ }
532
+
419
533
  private callbackBundledTransactions(
420
534
  requests: RequestWithExpiry[],
421
- result?: { receipt: TransactionReceipt } | FormattedViemError,
535
+ result: { receipt: TransactionReceipt; errorMsg?: string } | FormattedViemError | undefined,
536
+ txContext: { multicallData: Hex; blobData?: Hex[]; l1BlockNumber: bigint },
422
537
  ) {
423
538
  const actionsListStr = requests.map(r => r.action).join(', ');
424
539
  if (result instanceof FormattedViemError) {
425
540
  this.log.error(`Failed to publish bundled transactions (${actionsListStr})`, result);
541
+ this.backupFailedTx({
542
+ id: keccak256(txContext.multicallData),
543
+ failureType: 'send-error',
544
+ request: { to: MULTI_CALL_3_ADDRESS, data: txContext.multicallData },
545
+ blobData: txContext.blobData,
546
+ l1BlockNumber: txContext.l1BlockNumber.toString(),
547
+ error: { message: result.message, name: result.name },
548
+ context: {
549
+ actions: requests.map(r => r.action),
550
+ requests: requests.map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
551
+ sender: this.getSenderAddress().toString(),
552
+ },
553
+ });
426
554
  return { failedActions: requests.map(r => r.action) };
427
555
  } else {
428
- this.log.verbose(`Published bundled transactions (${actionsListStr})`, { result, requests });
556
+ this.log.verbose(`Published bundled transactions (${actionsListStr})`, {
557
+ result,
558
+ requests: requests.map(r => ({
559
+ ...r,
560
+ // Avoid logging large blob data
561
+ blobConfig: r.blobConfig
562
+ ? { ...r.blobConfig, blobs: r.blobConfig.blobs.map(b => ({ size: trimmedBytesLength(b) })) }
563
+ : undefined,
564
+ })),
565
+ });
429
566
  const successfulActions: Action[] = [];
430
567
  const failedActions: Action[] = [];
431
568
  for (const request of requests) {
@@ -435,25 +572,53 @@ export class SequencerPublisher {
435
572
  failedActions.push(request.action);
436
573
  }
437
574
  }
575
+ // Single backup for the whole reverted tx
576
+ if (failedActions.length > 0 && result?.receipt?.status === 'reverted') {
577
+ this.backupFailedTx({
578
+ id: result.receipt.transactionHash,
579
+ failureType: 'revert',
580
+ request: { to: MULTI_CALL_3_ADDRESS, data: txContext.multicallData },
581
+ blobData: txContext.blobData,
582
+ l1BlockNumber: result.receipt.blockNumber.toString(),
583
+ receipt: {
584
+ transactionHash: result.receipt.transactionHash,
585
+ blockNumber: result.receipt.blockNumber.toString(),
586
+ gasUsed: (result.receipt.gasUsed ?? 0n).toString(),
587
+ status: 'reverted',
588
+ },
589
+ error: { message: result.errorMsg ?? 'Transaction reverted' },
590
+ context: {
591
+ actions: failedActions,
592
+ requests: requests
593
+ .filter(r => failedActions.includes(r.action))
594
+ .map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
595
+ sender: this.getSenderAddress().toString(),
596
+ },
597
+ });
598
+ }
438
599
  return { successfulActions, failedActions };
439
600
  }
440
601
  }
441
602
 
442
603
  /**
443
- * @notice Will call `canProposeAtNextEthBlock` to make sure that it is possible to propose
604
+ * @notice Will call `canProposeAt` to make sure that it is possible to propose
444
605
  * @param tipArchive - The archive to check
445
606
  * @returns The slot and block number if it is possible to propose, undefined otherwise
446
607
  */
447
- public canProposeAtNextEthBlock(
608
+ public canProposeAt(
448
609
  tipArchive: Fr,
449
610
  msgSender: EthAddress,
450
- opts: { forcePendingCheckpointNumber?: CheckpointNumber } = {},
611
+ opts: { forcePendingCheckpointNumber?: CheckpointNumber; pipelined?: boolean } = {},
451
612
  ) {
452
613
  // TODO: #14291 - should loop through multiple keys to check if any of them can propose
453
614
  const ignoredErrors = ['SlotAlreadyInChain', 'InvalidProposer', 'InvalidArchive'];
454
615
 
616
+ const pipelined = opts.pipelined ?? this.epochCache.isProposerPipeliningEnabled();
617
+ const slotOffset = pipelined ? this.aztecSlotDuration : 0n;
618
+ const nextL1SlotTs = this.getNextL1SlotTimestamp() + slotOffset;
619
+
455
620
  return this.rollupContract
456
- .canProposeAtNextEthBlock(tipArchive.toBuffer(), msgSender.toString(), Number(this.ethereumSlotDuration), {
621
+ .canProposeAt(tipArchive.toBuffer(), msgSender.toString(), nextL1SlotTs, {
457
622
  forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
458
623
  })
459
624
  .catch(err => {
@@ -467,6 +632,7 @@ export class SequencerPublisher {
467
632
  return undefined;
468
633
  });
469
634
  }
635
+
470
636
  /**
471
637
  * @notice Will simulate `validateHeader` to make sure that the block header is valid
472
638
  * @dev This is a convenience function that can be used by the sequencer to validate a "partial" header.
@@ -490,7 +656,7 @@ export class SequencerPublisher {
490
656
  flags,
491
657
  ] as const;
492
658
 
493
- const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
659
+ const ts = this.getNextL1SlotTimestamp();
494
660
  const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(
495
661
  opts?.forcePendingCheckpointNumber,
496
662
  );
@@ -546,6 +712,8 @@ export class SequencerPublisher {
546
712
  const request = this.buildInvalidateCheckpointRequest(validationResult);
547
713
  this.log.debug(`Simulating invalidate checkpoint ${checkpointNumber}`, { ...logData, request });
548
714
 
715
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
716
+
549
717
  try {
550
718
  const { gasUsed } = await this.l1TxUtils.simulate(
551
719
  request,
@@ -597,6 +765,18 @@ export class SequencerPublisher {
597
765
 
598
766
  // Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
599
767
  this.log.error(`Simulation for invalidate checkpoint ${checkpointNumber} failed`, viemError, logData);
768
+ this.backupFailedTx({
769
+ id: keccak256(request.data!),
770
+ failureType: 'simulation',
771
+ request: { to: request.to!, data: request.data!, value: request.value?.toString() },
772
+ l1BlockNumber: l1BlockNumber.toString(),
773
+ error: { message: viemError.message, name: viemError.name },
774
+ context: {
775
+ actions: [`invalidate-${reason}`],
776
+ checkpointNumber,
777
+ sender: this.getSenderAddress().toString(),
778
+ },
779
+ });
600
780
  throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, { cause: viemError });
601
781
  }
602
782
  }
@@ -641,7 +821,14 @@ export class SequencerPublisher {
641
821
  attestationsAndSignersSignature: Signature,
642
822
  options: { forcePendingCheckpointNumber?: CheckpointNumber },
643
823
  ): Promise<bigint> {
644
- const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
824
+ // When pipelining, the checkpoint targets the next slot so its timestamp is in the future.
825
+ // Without pipelining, the checkpoint targets the current slot so its timestamp is in the past
826
+ // by the time we simulate (~24s of build time), causing eth_simulateV1 to reject it.
827
+ // In that case, use the latest L1 block timestamp + one ethereum slot, which is just ahead
828
+ // of L1 and still within the same L2 slot.
829
+ const ts = this.epochCache.isProposerPipeliningEnabled()
830
+ ? checkpoint.header.timestamp
831
+ : (await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration;
645
832
  const blobFields = checkpoint.toBlobFields();
646
833
  const blobs = await getBlobsPerL1Block(blobFields);
647
834
  const blobInput = getPrefixedEthBlobCommitments(blobs);
@@ -744,11 +931,26 @@ export class SequencerPublisher {
744
931
  lastValidL2Slot: slotNumber,
745
932
  });
746
933
 
934
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
935
+
747
936
  try {
748
937
  await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
749
938
  this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
750
939
  } catch (err) {
751
- this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, err);
940
+ const viemError = formatViemError(err);
941
+ this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, viemError);
942
+ this.backupFailedTx({
943
+ id: keccak256(request.data!),
944
+ failureType: 'simulation',
945
+ request: { to: request.to!, data: request.data!, value: request.value?.toString() },
946
+ l1BlockNumber: l1BlockNumber.toString(),
947
+ error: { message: viemError.message, name: viemError.name },
948
+ context: {
949
+ actions: [action],
950
+ slot: slotNumber,
951
+ sender: this.getSenderAddress().toString(),
952
+ },
953
+ });
752
954
  // Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
753
955
  }
754
956
 
@@ -1044,6 +1246,8 @@ export class SequencerPublisher {
1044
1246
 
1045
1247
  this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
1046
1248
 
1249
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1250
+
1047
1251
  let gasUsed: bigint;
1048
1252
  const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
1049
1253
  try {
@@ -1053,6 +1257,19 @@ export class SequencerPublisher {
1053
1257
  const viemError = formatViemError(err, simulateAbi);
1054
1258
  this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
1055
1259
 
1260
+ this.backupFailedTx({
1261
+ id: keccak256(request.data!),
1262
+ failureType: 'simulation',
1263
+ request: { to: request.to!, data: request.data!, value: request.value?.toString() },
1264
+ l1BlockNumber: l1BlockNumber.toString(),
1265
+ error: { message: viemError.message, name: viemError.name },
1266
+ context: {
1267
+ actions: [action],
1268
+ slot: slotNumber,
1269
+ sender: this.getSenderAddress().toString(),
1270
+ },
1271
+ });
1272
+
1056
1273
  return false;
1057
1274
  }
1058
1275
 
@@ -1136,9 +1353,27 @@ export class SequencerPublisher {
1136
1353
  kzg,
1137
1354
  },
1138
1355
  )
1139
- .catch(err => {
1140
- const { message, metaMessages } = formatViemError(err);
1141
- this.log.error(`Failed to validate blobs`, message, { metaMessages });
1356
+ .catch(async err => {
1357
+ const viemError = formatViemError(err);
1358
+ this.log.error(`Failed to validate blobs`, viemError.message, { metaMessages: viemError.metaMessages });
1359
+ const validateBlobsData = encodeFunctionData({
1360
+ abi: RollupAbi,
1361
+ functionName: 'validateBlobs',
1362
+ args: [blobInput],
1363
+ });
1364
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1365
+ this.backupFailedTx({
1366
+ id: keccak256(validateBlobsData),
1367
+ failureType: 'simulation',
1368
+ request: { to: this.rollupContract.address as Hex, data: validateBlobsData },
1369
+ blobData: encodedData.blobs.map(b => toHex(b.data)) as Hex[],
1370
+ l1BlockNumber: l1BlockNumber.toString(),
1371
+ error: { message: viemError.message, name: viemError.name },
1372
+ context: {
1373
+ actions: ['validate-blobs'],
1374
+ sender: this.getSenderAddress().toString(),
1375
+ },
1376
+ });
1142
1377
  throw new Error('Failed to validate blobs');
1143
1378
  });
1144
1379
  }
@@ -1217,6 +1452,8 @@ export class SequencerPublisher {
1217
1452
  });
1218
1453
  }
1219
1454
 
1455
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1456
+
1220
1457
  const simulationResult = await this.l1TxUtils
1221
1458
  .simulate(
1222
1459
  {
@@ -1250,6 +1487,18 @@ export class SequencerPublisher {
1250
1487
  };
1251
1488
  }
1252
1489
  this.log.error(`Failed to simulate propose tx`, viemError);
1490
+ this.backupFailedTx({
1491
+ id: keccak256(rollupData),
1492
+ failureType: 'simulation',
1493
+ request: { to: this.rollupContract.address, data: rollupData },
1494
+ l1BlockNumber: l1BlockNumber.toString(),
1495
+ error: { message: viemError.message, name: viemError.name },
1496
+ context: {
1497
+ actions: ['propose'],
1498
+ slot: Number(args[0].header.slotNumber),
1499
+ sender: this.getSenderAddress().toString(),
1500
+ },
1501
+ });
1253
1502
  throw err;
1254
1503
  });
1255
1504
 
@@ -1345,4 +1594,10 @@ export class SequencerPublisher {
1345
1594
  },
1346
1595
  });
1347
1596
  }
1597
+
1598
+ /** Returns the timestamp to use when simulating L1 proposal calls */
1599
+ private getNextL1SlotTimestamp(): bigint {
1600
+ const l1Constants = this.epochCache.getL1Constants();
1601
+ return getNextL1SlotTimestamp(this.dateProvider.nowInSeconds(), l1Constants);
1602
+ }
1348
1603
  }