@aztec/sequencer-client 0.0.1-commit.29c6b1a3 → 0.0.1-commit.2d9cb6034

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 (88) hide show
  1. package/dest/client/sequencer-client.d.ts +15 -7
  2. package/dest/client/sequencer-client.d.ts.map +1 -1
  3. package/dest/client/sequencer-client.js +60 -26
  4. package/dest/config.d.ts +26 -7
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +47 -28
  7. package/dest/global_variable_builder/global_builder.d.ts +15 -11
  8. package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
  9. package/dest/global_variable_builder/global_builder.js +29 -25
  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 +47 -17
  13. package/dest/publisher/config.d.ts.map +1 -1
  14. package/dest/publisher/config.js +121 -42
  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 +11 -3
  30. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  31. package/dest/publisher/sequencer-publisher-factory.js +27 -2
  32. package/dest/publisher/sequencer-publisher-metrics.d.ts +1 -1
  33. package/dest/publisher/sequencer-publisher-metrics.d.ts.map +1 -1
  34. package/dest/publisher/sequencer-publisher-metrics.js +12 -4
  35. package/dest/publisher/sequencer-publisher.d.ts +76 -31
  36. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  37. package/dest/publisher/sequencer-publisher.js +434 -88
  38. package/dest/sequencer/checkpoint_proposal_job.d.ts +61 -13
  39. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  40. package/dest/sequencer/checkpoint_proposal_job.js +400 -201
  41. package/dest/sequencer/checkpoint_voter.d.ts +1 -2
  42. package/dest/sequencer/checkpoint_voter.d.ts.map +1 -1
  43. package/dest/sequencer/checkpoint_voter.js +2 -5
  44. package/dest/sequencer/events.d.ts +2 -1
  45. package/dest/sequencer/events.d.ts.map +1 -1
  46. package/dest/sequencer/metrics.d.ts +21 -5
  47. package/dest/sequencer/metrics.d.ts.map +1 -1
  48. package/dest/sequencer/metrics.js +122 -30
  49. package/dest/sequencer/sequencer.d.ts +42 -17
  50. package/dest/sequencer/sequencer.d.ts.map +1 -1
  51. package/dest/sequencer/sequencer.js +147 -89
  52. package/dest/sequencer/timetable.d.ts +4 -6
  53. package/dest/sequencer/timetable.d.ts.map +1 -1
  54. package/dest/sequencer/timetable.js +7 -11
  55. package/dest/sequencer/types.d.ts +2 -2
  56. package/dest/sequencer/types.d.ts.map +1 -1
  57. package/dest/test/index.d.ts +3 -5
  58. package/dest/test/index.d.ts.map +1 -1
  59. package/dest/test/mock_checkpoint_builder.d.ts +12 -12
  60. package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
  61. package/dest/test/mock_checkpoint_builder.js +45 -36
  62. package/dest/test/utils.d.ts +3 -3
  63. package/dest/test/utils.d.ts.map +1 -1
  64. package/dest/test/utils.js +5 -4
  65. package/package.json +27 -28
  66. package/src/client/sequencer-client.ts +76 -23
  67. package/src/config.ts +65 -38
  68. package/src/global_variable_builder/global_builder.ts +38 -27
  69. package/src/global_variable_builder/index.ts +1 -1
  70. package/src/publisher/config.ts +153 -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 +38 -6
  77. package/src/publisher/sequencer-publisher-metrics.ts +7 -3
  78. package/src/publisher/sequencer-publisher.ts +474 -111
  79. package/src/sequencer/checkpoint_proposal_job.ts +532 -224
  80. package/src/sequencer/checkpoint_voter.ts +1 -12
  81. package/src/sequencer/events.ts +1 -1
  82. package/src/sequencer/metrics.ts +138 -32
  83. package/src/sequencer/sequencer.ts +213 -106
  84. package/src/sequencer/timetable.ts +13 -12
  85. package/src/sequencer/types.ts +1 -1
  86. package/src/test/index.ts +2 -4
  87. package/src/test/mock_checkpoint_builder.ts +65 -53
  88. package/src/test/utils.ts +5 -2
@@ -372,24 +372,30 @@ function _apply_decs_2203_r(targetClass, memberDecs, classDecs, parentClass) {
372
372
  }
373
373
  var _dec, _dec1, _dec2, _initProto;
374
374
  import { Blob, getBlobsPerL1Block, getPrefixedEthBlobCommitments } from '@aztec/blob-lib';
375
- import { MULTI_CALL_3_ADDRESS, Multicall3, RollupContract } from '@aztec/ethereum/contracts';
375
+ import { FeeAssetPriceOracle, MULTI_CALL_3_ADDRESS, Multicall3, RollupContract } from '@aztec/ethereum/contracts';
376
376
  import { L1FeeAnalyzer } from '@aztec/ethereum/l1-fee-analysis';
377
- import { WEI_CONST } from '@aztec/ethereum/l1-tx-utils';
378
- import { FormattedViemError, formatViemError, tryExtractEvent } from '@aztec/ethereum/utils';
377
+ import { MAX_L1_TX_LIMIT, WEI_CONST } from '@aztec/ethereum/l1-tx-utils';
378
+ import { FormattedViemError, formatViemError, mergeAbis, tryExtractEvent } from '@aztec/ethereum/utils';
379
379
  import { sumBigint } from '@aztec/foundation/bigint';
380
380
  import { toHex as toPaddedHex } from '@aztec/foundation/bigint-buffer';
381
381
  import { CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
382
+ import { trimmedBytesLength } from '@aztec/foundation/buffer';
382
383
  import { pick } from '@aztec/foundation/collection';
384
+ import { TimeoutError } from '@aztec/foundation/error';
383
385
  import { EthAddress } from '@aztec/foundation/eth-address';
384
386
  import { Signature } from '@aztec/foundation/eth-signature';
385
387
  import { createLogger } from '@aztec/foundation/log';
388
+ import { makeBackoff, retry } from '@aztec/foundation/retry';
389
+ import { InterruptibleSleep } from '@aztec/foundation/sleep';
386
390
  import { bufferToHex } from '@aztec/foundation/string';
387
391
  import { Timer } from '@aztec/foundation/timer';
388
392
  import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
389
393
  import { encodeSlashConsensusVotes } from '@aztec/slasher';
390
394
  import { CommitteeAttestationsAndSigners } from '@aztec/stdlib/block';
395
+ import { getLastL1SlotTimestampForL2Slot, getNextL1SlotTimestamp } from '@aztec/stdlib/epoch-helpers';
391
396
  import { getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
392
- import { encodeFunctionData, toHex } from 'viem';
397
+ import { encodeFunctionData, keccak256, multicall3Abi, toHex } from 'viem';
398
+ import { createL1TxFailedStore } from './l1_tx_failed_store/index.js';
393
399
  import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
394
400
  export const Actions = [
395
401
  'invalidate-by-invalid-attestation',
@@ -429,19 +435,22 @@ export class SequencerPublisher {
429
435
  interrupted;
430
436
  metrics;
431
437
  epochCache;
438
+ failedTxStore;
432
439
  governanceLog;
433
440
  slashingLog;
434
441
  lastActions;
435
442
  isPayloadEmptyCache;
443
+ payloadProposedCache;
436
444
  log;
437
445
  ethereumSlotDuration;
446
+ aztecSlotDuration;
447
+ /** Date provider for wall-clock time. */ dateProvider;
438
448
  blobClient;
439
449
  /** Address to use for simulations in fisherman mode (actual proposer's address) */ proposerAddressForSimulation;
450
+ /** Optional callback to obtain a replacement publisher when the current one fails to send. */ getNextPublisher;
440
451
  /** L1 fee analyzer for fisherman mode */ l1FeeAnalyzer;
441
- // @note - with blobs, the below estimate seems too large.
442
- // Total used for full block from int_l1_pub e2e test: 1m (of which 86k is 1x blob)
443
- // Total used for emptier block from above test: 429k (of which 84k is 1x blob)
444
- static PROPOSE_GAS_GUESS = 12_000_000n;
452
+ /** Fee asset price oracle for computing price modifiers from Uniswap V4 */ feeAssetPriceOracle;
453
+ /** Interruptible sleep used by sendRequestsAt to wait until a target timestamp. */ interruptibleSleep;
445
454
  // A CALL to a cold address is 2700 gas
446
455
  static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
447
456
  // Gas report for VotingWithSigTest shows a max gas of 100k, but we've seen it cost 700k+ in testnet
@@ -460,16 +469,22 @@ export class SequencerPublisher {
460
469
  this.slashingLog = createLogger('sequencer:publisher:slashing');
461
470
  this.lastActions = {};
462
471
  this.isPayloadEmptyCache = new Map();
472
+ this.payloadProposedCache = new Set();
473
+ this.interruptibleSleep = new InterruptibleSleep();
463
474
  this.requests = [];
464
475
  this.log = deps.log ?? createLogger('sequencer:publisher');
465
476
  this.ethereumSlotDuration = BigInt(config.ethereumSlotDuration);
477
+ this.aztecSlotDuration = BigInt(config.aztecSlotDuration);
478
+ this.dateProvider = deps.dateProvider;
466
479
  this.epochCache = deps.epochCache;
467
480
  this.lastActions = deps.lastActions;
468
481
  this.blobClient = deps.blobClient;
482
+ this.dateProvider = deps.dateProvider;
469
483
  const telemetry = deps.telemetry ?? getTelemetryClient();
470
484
  this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
471
485
  this.tracer = telemetry.getTracer('SequencerPublisher');
472
486
  this.l1TxUtils = deps.l1TxUtils;
487
+ this.getNextPublisher = deps.getNextPublisher;
473
488
  this.rollupContract = deps.rollupContract;
474
489
  this.govProposerContract = deps.governanceProposerContract;
475
490
  this.slashingProposerContract = deps.slashingProposerContract;
@@ -483,10 +498,36 @@ export class SequencerPublisher {
483
498
  if (config.fishermanMode) {
484
499
  this.l1FeeAnalyzer = new L1FeeAnalyzer(this.l1TxUtils.client, deps.dateProvider, createLogger('sequencer:publisher:fee-analyzer'));
485
500
  }
501
+ // Initialize fee asset price oracle
502
+ this.feeAssetPriceOracle = new FeeAssetPriceOracle(this.l1TxUtils.client, this.rollupContract, createLogger('sequencer:publisher:price-oracle'));
503
+ // Initialize failed L1 tx store (optional, for test networks)
504
+ this.failedTxStore = createL1TxFailedStore(config.l1TxFailedStore, this.log);
505
+ }
506
+ /**
507
+ * Backs up a failed L1 transaction to the configured store for debugging.
508
+ * Does nothing if no store is configured.
509
+ */ backupFailedTx(failedTx) {
510
+ if (!this.failedTxStore) {
511
+ return;
512
+ }
513
+ const tx = {
514
+ ...failedTx,
515
+ timestamp: Date.now()
516
+ };
517
+ // Fire and forget - don't block on backup
518
+ void this.failedTxStore.then((store)=>store?.saveFailedTx(tx)).catch((err)=>{
519
+ this.log.warn(`Failed to backup failed L1 tx to store`, err);
520
+ });
486
521
  }
487
522
  getRollupContract() {
488
523
  return this.rollupContract;
489
524
  }
525
+ /**
526
+ * Gets the fee asset price modifier from the oracle.
527
+ * Returns 0n if the oracle query fails.
528
+ */ getFeeAssetPriceModifier() {
529
+ return this.feeAssetPriceOracle.computePriceModifier();
530
+ }
490
531
  getSenderAddress() {
491
532
  return this.l1TxUtils.getSenderAddress();
492
533
  }
@@ -505,7 +546,7 @@ export class SequencerPublisher {
505
546
  this.requests.push(request);
506
547
  }
507
548
  getCurrentL2Slot() {
508
- return this.epochCache.getEpochAndSlotNow().slot;
549
+ return this.epochCache.getSlotNow();
509
550
  }
510
551
  /**
511
552
  * Clears all pending requests without sending them.
@@ -544,7 +585,7 @@ export class SequencerPublisher {
544
585
  // Get the transaction requests
545
586
  const l1Requests = requestsToAnalyze.map((r)=>r.request);
546
587
  // Start the analysis
547
- const analysisId = await this.l1FeeAnalyzer.startAnalysis(l2SlotNumber, gasLimit > 0n ? gasLimit : SequencerPublisher.PROPOSE_GAS_GUESS, l1Requests, blobConfig, onComplete);
588
+ const analysisId = await this.l1FeeAnalyzer.startAnalysis(l2SlotNumber, gasLimit > 0n ? gasLimit : MAX_L1_TX_LIMIT, l1Requests, blobConfig, onComplete);
548
589
  this.log.info('Started L1 fee analysis', {
549
590
  analysisId,
550
591
  l2SlotNumber: l2SlotNumber.toString(),
@@ -594,15 +635,24 @@ export class SequencerPublisher {
594
635
  // @note - we can only have one blob config per bundle
595
636
  // find requests with gas and blob configs
596
637
  // See https://github.com/AztecProtocol/aztec-packages/issues/11513
597
- const gasConfigs = requestsToProcess.filter((request)=>request.gasConfig).map((request)=>request.gasConfig);
598
- const blobConfigs = requestsToProcess.filter((request)=>request.blobConfig).map((request)=>request.blobConfig);
638
+ const gasConfigs = validRequests.filter((request)=>request.gasConfig).map((request)=>request.gasConfig);
639
+ const blobConfigs = validRequests.filter((request)=>request.blobConfig).map((request)=>request.blobConfig);
599
640
  if (blobConfigs.length > 1) {
600
641
  throw new Error('Multiple blob configs found');
601
642
  }
602
643
  const blobConfig = blobConfigs[0];
603
644
  // Merge gasConfigs. Yields the sum of gasLimits, and the earliest txTimeoutAt, or undefined if no gasConfig sets them.
604
645
  const gasLimits = gasConfigs.map((g)=>g?.gasLimit).filter((g)=>g !== undefined);
605
- const gasLimit = gasLimits.length > 0 ? sumBigint(gasLimits) : undefined; // sum
646
+ let gasLimit = gasLimits.length > 0 ? sumBigint(gasLimits) : undefined; // sum
647
+ // Cap at L1 block gas limit so the node accepts the tx ("gas limit too high" otherwise).
648
+ const maxGas = MAX_L1_TX_LIMIT;
649
+ if (gasLimit !== undefined && gasLimit > maxGas) {
650
+ this.log.debug('Capping bundled tx gas limit to L1 max', {
651
+ requested: gasLimit,
652
+ capped: maxGas
653
+ });
654
+ gasLimit = maxGas;
655
+ }
606
656
  const txTimeoutAts = gasConfigs.map((g)=>g?.txTimeoutAt).filter((g)=>g !== undefined);
607
657
  const txTimeoutAt = txTimeoutAts.length > 0 ? new Date(Math.min(...txTimeoutAts.map((g)=>g.getTime()))) : undefined; // earliest
608
658
  const txConfig = {
@@ -613,12 +663,34 @@ export class SequencerPublisher {
613
663
  // This ensures the committee gets precomputed correctly
614
664
  validRequests.sort((a, b)=>compareActions(a.action, b.action));
615
665
  try {
666
+ // Capture context for failed tx backup before sending
667
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
668
+ const multicallData = encodeFunctionData({
669
+ abi: multicall3Abi,
670
+ functionName: 'aggregate3',
671
+ args: [
672
+ validRequests.map((r)=>({
673
+ target: r.request.to,
674
+ callData: r.request.data,
675
+ allowFailure: true
676
+ }))
677
+ ]
678
+ });
679
+ const blobDataHex = blobConfig?.blobs?.map((b)=>toHex(b));
680
+ const txContext = {
681
+ multicallData,
682
+ blobData: blobDataHex,
683
+ l1BlockNumber
684
+ };
616
685
  this.log.debug('Forwarding transactions', {
617
686
  validRequests: validRequests.map((request)=>request.action),
618
687
  txConfig
619
688
  });
620
- const result = await Multicall3.forward(validRequests.map((request)=>request.request), this.l1TxUtils, txConfig, blobConfig, this.rollupContract.address, this.log);
621
- const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(validRequests, result);
689
+ const result = await this.forwardWithPublisherRotation(validRequests, txConfig, blobConfig);
690
+ if (result === undefined) {
691
+ return undefined;
692
+ }
693
+ const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(validRequests, result, txContext);
622
694
  return {
623
695
  result,
624
696
  expiredActions,
@@ -638,17 +710,100 @@ export class SequencerPublisher {
638
710
  }
639
711
  }
640
712
  }
641
- callbackBundledTransactions(requests, result) {
713
+ /**
714
+ * Forwards transactions via Multicall3, rotating to the next available publisher if a send
715
+ * failure occurs (i.e. the tx never reached the chain).
716
+ * On-chain reverts and simulation errors are returned as-is without rotation.
717
+ */ async forwardWithPublisherRotation(validRequests, txConfig, blobConfig) {
718
+ const triedAddresses = [];
719
+ let currentPublisher = this.l1TxUtils;
720
+ while(true){
721
+ triedAddresses.push(currentPublisher.getSenderAddress());
722
+ try {
723
+ const result = await Multicall3.forward(validRequests.map((r)=>r.request), currentPublisher, txConfig, blobConfig, this.rollupContract.address, this.log);
724
+ this.l1TxUtils = currentPublisher;
725
+ return result;
726
+ } catch (err) {
727
+ if (err instanceof TimeoutError) {
728
+ throw err;
729
+ }
730
+ const viemError = formatViemError(err);
731
+ if (!this.getNextPublisher) {
732
+ this.log.error('Failed to publish bundled transactions', viemError);
733
+ return undefined;
734
+ }
735
+ this.log.warn(`Publisher ${currentPublisher.getSenderAddress()} failed to send, rotating to next publisher`, viemError);
736
+ const nextPublisher = await this.getNextPublisher([
737
+ ...triedAddresses
738
+ ]);
739
+ if (!nextPublisher) {
740
+ this.log.error('All available publishers exhausted, failed to publish bundled transactions');
741
+ return undefined;
742
+ }
743
+ currentPublisher = nextPublisher;
744
+ }
745
+ }
746
+ }
747
+ /*
748
+ * Schedules sending all enqueued requests at (or after) the given timestamp.
749
+ * Uses InterruptibleSleep so it can be cancelled via interrupt().
750
+ * Returns the promise for the L1 response (caller should NOT await this in the work loop).
751
+ */ async sendRequestsAt(submitAfter) {
752
+ const ms = submitAfter.getTime() - this.dateProvider.now();
753
+ if (ms > 0) {
754
+ this.log.debug(`Sleeping ${ms}ms before sending requests`, {
755
+ submitAfter
756
+ });
757
+ await this.interruptibleSleep.sleep(ms);
758
+ }
759
+ if (this.interrupted) {
760
+ return undefined;
761
+ }
762
+ return this.sendRequests();
763
+ }
764
+ callbackBundledTransactions(requests, result, txContext) {
642
765
  const actionsListStr = requests.map((r)=>r.action).join(', ');
643
766
  if (result instanceof FormattedViemError) {
644
767
  this.log.error(`Failed to publish bundled transactions (${actionsListStr})`, result);
768
+ this.backupFailedTx({
769
+ id: keccak256(txContext.multicallData),
770
+ failureType: 'send-error',
771
+ request: {
772
+ to: MULTI_CALL_3_ADDRESS,
773
+ data: txContext.multicallData
774
+ },
775
+ blobData: txContext.blobData,
776
+ l1BlockNumber: txContext.l1BlockNumber.toString(),
777
+ error: {
778
+ message: result.message,
779
+ name: result.name
780
+ },
781
+ context: {
782
+ actions: requests.map((r)=>r.action),
783
+ requests: requests.map((r)=>({
784
+ action: r.action,
785
+ to: r.request.to,
786
+ data: r.request.data
787
+ })),
788
+ sender: this.getSenderAddress().toString()
789
+ }
790
+ });
645
791
  return {
646
792
  failedActions: requests.map((r)=>r.action)
647
793
  };
648
794
  } else {
649
795
  this.log.verbose(`Published bundled transactions (${actionsListStr})`, {
650
796
  result,
651
- requests
797
+ requests: requests.map((r)=>({
798
+ ...r,
799
+ // Avoid logging large blob data
800
+ blobConfig: r.blobConfig ? {
801
+ ...r.blobConfig,
802
+ blobs: r.blobConfig.blobs.map((b)=>({
803
+ size: trimmedBytesLength(b)
804
+ }))
805
+ } : undefined
806
+ }))
652
807
  });
653
808
  const successfulActions = [];
654
809
  const failedActions = [];
@@ -659,6 +814,37 @@ export class SequencerPublisher {
659
814
  failedActions.push(request.action);
660
815
  }
661
816
  }
817
+ // Single backup for the whole reverted tx
818
+ if (failedActions.length > 0 && result?.receipt?.status === 'reverted') {
819
+ this.backupFailedTx({
820
+ id: result.receipt.transactionHash,
821
+ failureType: 'revert',
822
+ request: {
823
+ to: MULTI_CALL_3_ADDRESS,
824
+ data: txContext.multicallData
825
+ },
826
+ blobData: txContext.blobData,
827
+ l1BlockNumber: result.receipt.blockNumber.toString(),
828
+ receipt: {
829
+ transactionHash: result.receipt.transactionHash,
830
+ blockNumber: result.receipt.blockNumber.toString(),
831
+ gasUsed: (result.receipt.gasUsed ?? 0n).toString(),
832
+ status: 'reverted'
833
+ },
834
+ error: {
835
+ message: result.errorMsg ?? 'Transaction reverted'
836
+ },
837
+ context: {
838
+ actions: failedActions,
839
+ requests: requests.filter((r)=>failedActions.includes(r.action)).map((r)=>({
840
+ action: r.action,
841
+ to: r.request.to,
842
+ data: r.request.data
843
+ })),
844
+ sender: this.getSenderAddress().toString()
845
+ }
846
+ });
847
+ }
662
848
  return {
663
849
  successfulActions,
664
850
  failedActions
@@ -666,18 +852,22 @@ export class SequencerPublisher {
666
852
  }
667
853
  }
668
854
  /**
669
- * @notice Will call `canProposeAtNextEthBlock` to make sure that it is possible to propose
855
+ * @notice Will call `canProposeAt` to make sure that it is possible to propose
670
856
  * @param tipArchive - The archive to check
671
857
  * @returns The slot and block number if it is possible to propose, undefined otherwise
672
- */ canProposeAtNextEthBlock(tipArchive, msgSender, opts = {}) {
858
+ */ canProposeAt(tipArchive, msgSender, opts = {}) {
673
859
  // TODO: #14291 - should loop through multiple keys to check if any of them can propose
674
860
  const ignoredErrors = [
675
861
  'SlotAlreadyInChain',
676
862
  'InvalidProposer',
677
863
  'InvalidArchive'
678
864
  ];
679
- return this.rollupContract.canProposeAtNextEthBlock(tipArchive.toBuffer(), msgSender.toString(), Number(this.ethereumSlotDuration), {
680
- forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber
865
+ const pipelined = opts.pipelined ?? this.epochCache.isProposerPipeliningEnabled();
866
+ const slotOffset = pipelined ? this.aztecSlotDuration : 0n;
867
+ const nextL1SlotTs = this.getNextL1SlotTimestamp() + slotOffset;
868
+ return this.rollupContract.canProposeAt(tipArchive.toBuffer(), msgSender.toString(), nextL1SlotTs, {
869
+ forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
870
+ forceArchive: opts.forceArchive
681
871
  }).catch((err)=>{
682
872
  if (err instanceof FormattedViemError && ignoredErrors.find((e)=>err.message.includes(e))) {
683
873
  this.log.warn(`Failed canProposeAtTime check with ${ignoredErrors.find((e)=>err.message.includes(e))}`, {
@@ -708,7 +898,7 @@ export class SequencerPublisher {
708
898
  header.blobsHash.toString(),
709
899
  flags
710
900
  ];
711
- const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
901
+ const ts = this.getSimulationTimestamp(header.slotNumber);
712
902
  const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(opts?.forcePendingCheckpointNumber);
713
903
  let balance = 0n;
714
904
  if (this.config.fishermanMode) {
@@ -731,7 +921,7 @@ export class SequencerPublisher {
731
921
  }),
732
922
  from: MULTI_CALL_3_ADDRESS
733
923
  }, {
734
- time: ts + 1n
924
+ time: ts
735
925
  }, stateOverrides);
736
926
  this.log.debug(`Simulated validateHeader`);
737
927
  }
@@ -761,8 +951,12 @@ export class SequencerPublisher {
761
951
  ...logData,
762
952
  request
763
953
  });
954
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
764
955
  try {
765
- const { gasUsed } = await this.l1TxUtils.simulate(request, undefined, undefined, ErrorsAbi);
956
+ const { gasUsed } = await this.l1TxUtils.simulate(request, undefined, undefined, mergeAbis([
957
+ request.abi ?? [],
958
+ ErrorsAbi
959
+ ]));
766
960
  this.log.verbose(`Simulation for invalidate checkpoint ${checkpointNumber} succeeded`, {
767
961
  ...logData,
768
962
  request,
@@ -773,20 +967,21 @@ export class SequencerPublisher {
773
967
  gasUsed,
774
968
  checkpointNumber,
775
969
  forcePendingCheckpointNumber: CheckpointNumber(checkpointNumber - 1),
970
+ lastArchive: validationResult.checkpoint.lastArchive,
776
971
  reason
777
972
  };
778
973
  } catch (err) {
779
974
  const viemError = formatViemError(err);
780
975
  // If the error is due to the checkpoint not being in the pending chain, and it was indeed removed by someone else,
781
976
  // we can safely ignore it and return undefined so we go ahead with checkpoint building.
782
- if (viemError.message?.includes('Rollup__BlockNotInPendingChain')) {
977
+ if (viemError.message?.includes('Rollup__CheckpointNotInPendingChain')) {
783
978
  this.log.verbose(`Simulation for invalidate checkpoint ${checkpointNumber} failed due to checkpoint not being in pending chain`, {
784
979
  ...logData,
785
980
  request,
786
981
  error: viemError.message
787
982
  });
788
- const latestPendingCheckpointNumber = await this.rollupContract.getCheckpointNumber();
789
- if (latestPendingCheckpointNumber < checkpointNumber) {
983
+ const latestProposedCheckpointNumber = await this.rollupContract.getCheckpointNumber();
984
+ if (latestProposedCheckpointNumber < checkpointNumber) {
790
985
  this.log.verbose(`Checkpoint ${checkpointNumber} has already been invalidated`, {
791
986
  ...logData
792
987
  });
@@ -800,6 +995,27 @@ export class SequencerPublisher {
800
995
  }
801
996
  // Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
802
997
  this.log.error(`Simulation for invalidate checkpoint ${checkpointNumber} failed`, viemError, logData);
998
+ this.backupFailedTx({
999
+ id: keccak256(request.data),
1000
+ failureType: 'simulation',
1001
+ request: {
1002
+ to: request.to,
1003
+ data: request.data,
1004
+ value: request.value?.toString()
1005
+ },
1006
+ l1BlockNumber: l1BlockNumber.toString(),
1007
+ error: {
1008
+ message: viemError.message,
1009
+ name: viemError.name
1010
+ },
1011
+ context: {
1012
+ actions: [
1013
+ `invalidate-${reason}`
1014
+ ],
1015
+ checkpointNumber,
1016
+ sender: this.getSenderAddress().toString()
1017
+ }
1018
+ });
803
1019
  throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, {
804
1020
  cause: viemError
805
1021
  });
@@ -826,30 +1042,15 @@ export class SequencerPublisher {
826
1042
  }
827
1043
  }
828
1044
  /** Simulates `propose` to make sure that the checkpoint is valid for submission */ async validateCheckpointForSubmission(checkpoint, attestationsAndSigners, attestationsAndSignersSignature, options) {
829
- const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
830
- // TODO(palla/mbps): This should not be needed, there's no flow where we propose with zero attestations. Or is there?
831
- // If we have no attestations, we still need to provide the empty attestations
832
- // so that the committee is recalculated correctly
833
- // const ignoreSignatures = attestationsAndSigners.attestations.length === 0;
834
- // if (ignoreSignatures) {
835
- // const { committee } = await this.epochCache.getCommittee(block.header.globalVariables.slotNumber);
836
- // if (!committee) {
837
- // this.log.warn(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
838
- // throw new Error(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
839
- // }
840
- // attestationsAndSigners.attestations = committee.map(committeeMember =>
841
- // CommitteeAttestation.fromAddress(committeeMember),
842
- // );
843
- // }
844
1045
  const blobFields = checkpoint.toBlobFields();
845
- const blobs = getBlobsPerL1Block(blobFields);
1046
+ const blobs = await getBlobsPerL1Block(blobFields);
846
1047
  const blobInput = getPrefixedEthBlobCommitments(blobs);
847
1048
  const args = [
848
1049
  {
849
1050
  header: checkpoint.header.toViem(),
850
1051
  archive: toHex(checkpoint.archive.root.toBuffer()),
851
1052
  oracleInput: {
852
- feeAssetPriceModifier: 0n
1053
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier
853
1054
  }
854
1055
  },
855
1056
  attestationsAndSigners.getPackedAttestations(),
@@ -857,10 +1058,9 @@ export class SequencerPublisher {
857
1058
  attestationsAndSignersSignature.toViemSignature(),
858
1059
  blobInput
859
1060
  ];
860
- await this.simulateProposeTx(args, ts, options);
861
- return ts;
1061
+ await this.simulateProposeTx(args, options);
862
1062
  }
863
- async enqueueCastSignalHelper(slotNumber, timestamp, signalType, payload, base, signerAddress, signer) {
1063
+ async enqueueCastSignalHelper(slotNumber, signalType, payload, base, signerAddress, signer) {
864
1064
  if (this.lastActions[signalType] && this.lastActions[signalType] === slotNumber) {
865
1065
  this.log.debug(`Skipping duplicate vote cast signal ${signalType} for slot ${slotNumber}`);
866
1066
  return false;
@@ -884,6 +1084,28 @@ export class SequencerPublisher {
884
1084
  this.log.warn(`Skipping vote cast for payload with empty code`);
885
1085
  return false;
886
1086
  }
1087
+ // Check if payload was already submitted to governance
1088
+ const cacheKey = payload.toString();
1089
+ if (!this.payloadProposedCache.has(cacheKey)) {
1090
+ try {
1091
+ const l1StartBlock = await this.rollupContract.getL1StartBlock();
1092
+ const proposed = await retry(()=>base.hasPayloadBeenProposed(payload.toString(), l1StartBlock), 'Check if payload was proposed', makeBackoff([
1093
+ 0,
1094
+ 1,
1095
+ 2
1096
+ ]), this.log, true);
1097
+ if (proposed) {
1098
+ this.payloadProposedCache.add(cacheKey);
1099
+ }
1100
+ } catch (err) {
1101
+ this.log.warn(`Failed to check if payload ${payload} was proposed after retries, skipping signal`, err);
1102
+ return false;
1103
+ }
1104
+ }
1105
+ if (this.payloadProposedCache.has(cacheKey)) {
1106
+ this.log.info(`Payload ${payload} was already proposed to governance, stopping signals`);
1107
+ return false;
1108
+ }
887
1109
  const cachedLastVote = this.lastActions[signalType];
888
1110
  this.lastActions[signalType] = slotNumber;
889
1111
  const action = signalType;
@@ -894,15 +1116,45 @@ export class SequencerPublisher {
894
1116
  signer: this.l1TxUtils.client.account?.address,
895
1117
  lastValidL2Slot: slotNumber
896
1118
  });
1119
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1120
+ const timestamp = this.getSimulationTimestamp(slotNumber);
897
1121
  try {
898
1122
  await this.l1TxUtils.simulate(request, {
899
1123
  time: timestamp
900
- }, [], ErrorsAbi);
1124
+ }, [], mergeAbis([
1125
+ request.abi ?? [],
1126
+ ErrorsAbi
1127
+ ]));
901
1128
  this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, {
902
1129
  request
903
1130
  });
904
1131
  } catch (err) {
905
- this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, err);
1132
+ const viemError = formatViemError(err);
1133
+ this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, viemError, {
1134
+ simulationTimestamp: timestamp,
1135
+ l1BlockNumber
1136
+ });
1137
+ this.backupFailedTx({
1138
+ id: keccak256(request.data),
1139
+ failureType: 'simulation',
1140
+ request: {
1141
+ to: request.to,
1142
+ data: request.data,
1143
+ value: request.value?.toString()
1144
+ },
1145
+ l1BlockNumber: l1BlockNumber.toString(),
1146
+ error: {
1147
+ message: viemError.message,
1148
+ name: viemError.name
1149
+ },
1150
+ context: {
1151
+ actions: [
1152
+ action
1153
+ ],
1154
+ slot: slotNumber,
1155
+ sender: this.getSenderAddress().toString()
1156
+ }
1157
+ });
906
1158
  // Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
907
1159
  }
908
1160
  // TODO(palla/slash): All votes (governance and slashing) should txTimeoutAt at the end of the slot.
@@ -946,12 +1198,11 @@ export class SequencerPublisher {
946
1198
  /**
947
1199
  * Enqueues a governance castSignal transaction to cast a signal for a given slot number.
948
1200
  * @param slotNumber - The slot number to cast a signal for.
949
- * @param timestamp - The timestamp of the slot to cast a signal for.
950
1201
  * @returns True if the signal was successfully enqueued, false otherwise.
951
- */ enqueueGovernanceCastSignal(governancePayload, slotNumber, timestamp, signerAddress, signer) {
952
- return this.enqueueCastSignalHelper(slotNumber, timestamp, 'governance-signal', governancePayload, this.govProposerContract, signerAddress, signer);
1202
+ */ enqueueGovernanceCastSignal(governancePayload, slotNumber, signerAddress, signer) {
1203
+ return this.enqueueCastSignalHelper(slotNumber, 'governance-signal', governancePayload, this.govProposerContract, signerAddress, signer);
953
1204
  }
954
- /** Enqueues all slashing actions as returned by the slasher client. */ async enqueueSlashingActions(actions, slotNumber, timestamp, signerAddress, signer) {
1205
+ /** Enqueues all slashing actions as returned by the slasher client. */ async enqueueSlashingActions(actions, slotNumber, signerAddress, signer) {
955
1206
  if (actions.length === 0) {
956
1207
  this.log.debug(`No slashing actions to enqueue for slot ${slotNumber}`);
957
1208
  return false;
@@ -967,7 +1218,7 @@ export class SequencerPublisher {
967
1218
  this.log.debug(`Enqueuing slashing vote for payload ${action.payload} at slot ${slotNumber}`, {
968
1219
  signerAddress
969
1220
  });
970
- await this.enqueueCastSignalHelper(slotNumber, timestamp, 'empire-slashing-signal', action.payload, this.slashingProposerContract, signerAddress, signer);
1221
+ await this.enqueueCastSignalHelper(slotNumber, 'empire-slashing-signal', action.payload, this.slashingProposerContract, signerAddress, signer);
971
1222
  break;
972
1223
  }
973
1224
  case 'create-empire-payload':
@@ -977,7 +1228,7 @@ export class SequencerPublisher {
977
1228
  signerAddress
978
1229
  });
979
1230
  const request = this.slashFactoryContract.buildCreatePayloadRequest(action.data);
980
- await this.simulateAndEnqueueRequest('create-empire-payload', request, (receipt)=>!!this.slashFactoryContract.tryExtractSlashPayloadCreatedEvent(receipt.logs), slotNumber, timestamp);
1231
+ await this.simulateAndEnqueueRequest('create-empire-payload', request, (receipt)=>!!this.slashFactoryContract.tryExtractSlashPayloadCreatedEvent(receipt.logs), slotNumber);
981
1232
  break;
982
1233
  }
983
1234
  case 'execute-empire-payload':
@@ -992,7 +1243,7 @@ export class SequencerPublisher {
992
1243
  }
993
1244
  const empireSlashingProposer = this.slashingProposerContract;
994
1245
  const request = empireSlashingProposer.buildExecuteRoundRequest(action.round);
995
- await this.simulateAndEnqueueRequest('execute-empire-payload', request, (receipt)=>!!empireSlashingProposer.tryExtractPayloadSubmittedEvent(receipt.logs), slotNumber, timestamp);
1246
+ await this.simulateAndEnqueueRequest('execute-empire-payload', request, (receipt)=>!!empireSlashingProposer.tryExtractPayloadSubmittedEvent(receipt.logs), slotNumber);
996
1247
  break;
997
1248
  }
998
1249
  case 'vote-offenses':
@@ -1010,7 +1261,7 @@ export class SequencerPublisher {
1010
1261
  const tallySlashingProposer = this.slashingProposerContract;
1011
1262
  const votes = bufferToHex(encodeSlashConsensusVotes(action.votes));
1012
1263
  const request = await tallySlashingProposer.buildVoteRequestFromSigner(votes, slotNumber, signer);
1013
- await this.simulateAndEnqueueRequest('vote-offenses', request, (receipt)=>!!tallySlashingProposer.tryExtractVoteCastEvent(receipt.logs), slotNumber, timestamp);
1264
+ await this.simulateAndEnqueueRequest('vote-offenses', request, (receipt)=>!!tallySlashingProposer.tryExtractVoteCastEvent(receipt.logs), slotNumber);
1014
1265
  break;
1015
1266
  }
1016
1267
  case 'execute-slash':
@@ -1026,7 +1277,7 @@ export class SequencerPublisher {
1026
1277
  }
1027
1278
  const tallySlashingProposer = this.slashingProposerContract;
1028
1279
  const request = tallySlashingProposer.buildExecuteRoundRequest(action.round, action.committees);
1029
- await this.simulateAndEnqueueRequest('execute-slash', request, (receipt)=>!!tallySlashingProposer.tryExtractRoundExecutedEvent(receipt.logs), slotNumber, timestamp);
1280
+ await this.simulateAndEnqueueRequest('execute-slash', request, (receipt)=>!!tallySlashingProposer.tryExtractRoundExecutedEvent(receipt.logs), slotNumber);
1030
1281
  break;
1031
1282
  }
1032
1283
  default:
@@ -1041,22 +1292,22 @@ export class SequencerPublisher {
1041
1292
  /** Simulates and enqueues a proposal for a checkpoint on L1 */ async enqueueProposeCheckpoint(checkpoint, attestationsAndSigners, attestationsAndSignersSignature, opts = {}) {
1042
1293
  const checkpointHeader = checkpoint.header;
1043
1294
  const blobFields = checkpoint.toBlobFields();
1044
- const blobs = getBlobsPerL1Block(blobFields);
1295
+ const blobs = await getBlobsPerL1Block(blobFields);
1045
1296
  const proposeTxArgs = {
1046
1297
  header: checkpointHeader,
1047
1298
  archive: checkpoint.archive.root.toBuffer(),
1048
1299
  blobs,
1049
1300
  attestationsAndSigners,
1050
- attestationsAndSignersSignature
1301
+ attestationsAndSignersSignature,
1302
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier
1051
1303
  };
1052
- let ts;
1053
1304
  try {
1054
1305
  // @note This will make sure that we are passing the checks for our header ASSUMING that the data is also made available
1055
1306
  // This means that we can avoid the simulation issues in later checks.
1056
1307
  // By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which
1057
1308
  // make time consistency checks break.
1058
1309
  // TODO(palla): Check whether we're validating twice, once here and once within addProposeTx, since we call simulateProposeTx in both places.
1059
- ts = await this.validateCheckpointForSubmission(checkpoint, attestationsAndSigners, attestationsAndSignersSignature, opts);
1310
+ await this.validateCheckpointForSubmission(checkpoint, attestationsAndSigners, attestationsAndSignersSignature, opts);
1060
1311
  } catch (err) {
1061
1312
  this.log.error(`Checkpoint validation failed. ${err instanceof Error ? err.message : 'No error message'}`, err, {
1062
1313
  ...checkpoint.getStats(),
@@ -1069,7 +1320,7 @@ export class SequencerPublisher {
1069
1320
  ...checkpoint.toCheckpointInfo(),
1070
1321
  ...opts
1071
1322
  });
1072
- await this.addProposeTx(checkpoint, proposeTxArgs, opts, ts);
1323
+ await this.addProposeTx(checkpoint, proposeTxArgs, opts);
1073
1324
  }
1074
1325
  enqueueInvalidateCheckpoint(request, opts = {}) {
1075
1326
  if (!request) {
@@ -1110,7 +1361,8 @@ export class SequencerPublisher {
1110
1361
  }
1111
1362
  });
1112
1363
  }
1113
- async simulateAndEnqueueRequest(action, request, checkSuccess, slotNumber, timestamp) {
1364
+ async simulateAndEnqueueRequest(action, request, checkSuccess, slotNumber) {
1365
+ const timestamp = this.getSimulationTimestamp(slotNumber);
1114
1366
  const logData = {
1115
1367
  slotNumber,
1116
1368
  timestamp,
@@ -1123,28 +1375,60 @@ export class SequencerPublisher {
1123
1375
  const cachedLastActionSlot = this.lastActions[action];
1124
1376
  this.lastActions[action] = slotNumber;
1125
1377
  this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
1378
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1126
1379
  let gasUsed;
1380
+ const simulateAbi = mergeAbis([
1381
+ request.abi ?? [],
1382
+ ErrorsAbi
1383
+ ]);
1127
1384
  try {
1128
1385
  ({ gasUsed } = await this.l1TxUtils.simulate(request, {
1129
1386
  time: timestamp
1130
- }, [], ErrorsAbi)); // TODO(palla/slash): Check the timestamp logic
1387
+ }, [], simulateAbi));
1131
1388
  this.log.verbose(`Simulation for ${action} succeeded`, {
1132
1389
  ...logData,
1133
1390
  request,
1134
1391
  gasUsed
1135
1392
  });
1136
1393
  } catch (err) {
1137
- const viemError = formatViemError(err);
1394
+ const viemError = formatViemError(err, simulateAbi);
1138
1395
  this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
1396
+ this.backupFailedTx({
1397
+ id: keccak256(request.data),
1398
+ failureType: 'simulation',
1399
+ request: {
1400
+ to: request.to,
1401
+ data: request.data,
1402
+ value: request.value?.toString()
1403
+ },
1404
+ l1BlockNumber: l1BlockNumber.toString(),
1405
+ error: {
1406
+ message: viemError.message,
1407
+ name: viemError.name
1408
+ },
1409
+ context: {
1410
+ actions: [
1411
+ action
1412
+ ],
1413
+ slot: slotNumber,
1414
+ sender: this.getSenderAddress().toString()
1415
+ }
1416
+ });
1139
1417
  return false;
1140
1418
  }
1141
1419
  // We issued the simulation against the rollup contract, so we need to account for the overhead of the multicall3
1142
1420
  const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil(Number(gasUsed) * 64 / 63)));
1143
1421
  logData.gasLimit = gasLimit;
1422
+ // Store the ABI used for simulation on the request so Multicall3.forward can decode errors
1423
+ // when the tx is sent and a revert is diagnosed via simulation.
1424
+ const requestWithAbi = {
1425
+ ...request,
1426
+ abi: simulateAbi
1427
+ };
1144
1428
  this.log.debug(`Enqueuing ${action}`, logData);
1145
1429
  this.addRequest({
1146
1430
  action,
1147
- request,
1431
+ request: requestWithAbi,
1148
1432
  gasConfig: {
1149
1433
  gasLimit
1150
1434
  },
@@ -1175,13 +1459,14 @@ export class SequencerPublisher {
1175
1459
  * A call to `restart` is required before you can continue publishing.
1176
1460
  */ interrupt() {
1177
1461
  this.interrupted = true;
1462
+ this.interruptibleSleep.interrupt();
1178
1463
  this.l1TxUtils.interrupt();
1179
1464
  }
1180
1465
  /** Restarts the publisher after calling `interrupt`. */ restart() {
1181
1466
  this.interrupted = false;
1182
1467
  this.l1TxUtils.restart();
1183
1468
  }
1184
- async prepareProposeTx(encodedData, timestamp, options) {
1469
+ async prepareProposeTx(encodedData, options) {
1185
1470
  const kzg = Blob.getViemKzgInstance();
1186
1471
  const blobInput = getPrefixedEthBlobCommitments(encodedData.blobs);
1187
1472
  this.log.debug('Validating blob input', {
@@ -1208,10 +1493,38 @@ export class SequencerPublisher {
1208
1493
  }, {}, {
1209
1494
  blobs: encodedData.blobs.map((b)=>b.data),
1210
1495
  kzg
1211
- }).catch((err)=>{
1212
- const { message, metaMessages } = formatViemError(err);
1213
- this.log.error(`Failed to validate blobs`, message, {
1214
- metaMessages
1496
+ }).catch(async (err)=>{
1497
+ const viemError = formatViemError(err);
1498
+ this.log.error(`Failed to validate blobs`, viemError.message, {
1499
+ metaMessages: viemError.metaMessages
1500
+ });
1501
+ const validateBlobsData = encodeFunctionData({
1502
+ abi: RollupAbi,
1503
+ functionName: 'validateBlobs',
1504
+ args: [
1505
+ blobInput
1506
+ ]
1507
+ });
1508
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1509
+ this.backupFailedTx({
1510
+ id: keccak256(validateBlobsData),
1511
+ failureType: 'simulation',
1512
+ request: {
1513
+ to: this.rollupContract.address,
1514
+ data: validateBlobsData
1515
+ },
1516
+ blobData: encodedData.blobs.map((b)=>toHex(b.data)),
1517
+ l1BlockNumber: l1BlockNumber.toString(),
1518
+ error: {
1519
+ message: viemError.message,
1520
+ name: viemError.name
1521
+ },
1522
+ context: {
1523
+ actions: [
1524
+ 'validate-blobs'
1525
+ ],
1526
+ sender: this.getSenderAddress().toString()
1527
+ }
1215
1528
  });
1216
1529
  throw new Error('Failed to validate blobs');
1217
1530
  });
@@ -1222,8 +1535,7 @@ export class SequencerPublisher {
1222
1535
  header: encodedData.header.toViem(),
1223
1536
  archive: toHex(encodedData.archive),
1224
1537
  oracleInput: {
1225
- // We are currently not modifying these. See #9963
1226
- feeAssetPriceModifier: 0n
1538
+ feeAssetPriceModifier: encodedData.feeAssetPriceModifier
1227
1539
  }
1228
1540
  },
1229
1541
  encodedData.attestationsAndSigners.getPackedAttestations(),
@@ -1231,7 +1543,7 @@ export class SequencerPublisher {
1231
1543
  encodedData.attestationsAndSignersSignature.toViemSignature(),
1232
1544
  blobInput
1233
1545
  ];
1234
- const { rollupData, simulationResult } = await this.simulateProposeTx(args, timestamp, options);
1546
+ const { rollupData, simulationResult } = await this.simulateProposeTx(args, options);
1235
1547
  return {
1236
1548
  args,
1237
1549
  blobEvaluationGas,
@@ -1242,16 +1554,17 @@ export class SequencerPublisher {
1242
1554
  /**
1243
1555
  * Simulates the propose tx with eth_simulateV1
1244
1556
  * @param args - The propose tx args
1245
- * @param timestamp - The timestamp to simulate proposal at
1246
1557
  * @returns The simulation result
1247
- */ async simulateProposeTx(args, timestamp, options) {
1558
+ */ async simulateProposeTx(args, options) {
1248
1559
  const rollupData = encodeFunctionData({
1249
1560
  abi: RollupAbi,
1250
1561
  functionName: 'propose',
1251
1562
  args
1252
1563
  });
1253
- // override the pending checkpoint number if requested
1564
+ // override the proposed checkpoint number if requested
1254
1565
  const forcePendingCheckpointNumberStateDiff = (options.forcePendingCheckpointNumber !== undefined ? await this.rollupContract.makePendingCheckpointNumberOverride(options.forcePendingCheckpointNumber) : []).flatMap((override)=>override.stateDiff ?? []);
1566
+ // override the fee header for a specific checkpoint number if requested (used when pipelining)
1567
+ const forceProposedFeeHeaderStateDiff = (options.forceProposedFeeHeader !== undefined ? await this.rollupContract.makeFeeHeaderOverride(options.forceProposedFeeHeader.checkpointNumber, options.forceProposedFeeHeader.feeHeader) : []).flatMap((override)=>override.stateDiff ?? []);
1255
1568
  const stateOverrides = [
1256
1569
  {
1257
1570
  address: this.rollupContract.address,
@@ -1261,7 +1574,8 @@ export class SequencerPublisher {
1261
1574
  slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true),
1262
1575
  value: toPaddedHex(0n, true)
1263
1576
  },
1264
- ...forcePendingCheckpointNumberStateDiff
1577
+ ...forcePendingCheckpointNumberStateDiff,
1578
+ ...forceProposedFeeHeaderStateDiff
1265
1579
  ]
1266
1580
  }
1267
1581
  ];
@@ -1272,21 +1586,22 @@ export class SequencerPublisher {
1272
1586
  balance: 10n * WEI_CONST * WEI_CONST
1273
1587
  });
1274
1588
  }
1589
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1590
+ const simTs = this.getSimulationTimestamp(SlotNumber.fromBigInt(args[0].header.slotNumber));
1275
1591
  const simulationResult = await this.l1TxUtils.simulate({
1276
1592
  to: this.rollupContract.address,
1277
1593
  data: rollupData,
1278
- gas: SequencerPublisher.PROPOSE_GAS_GUESS,
1594
+ gas: MAX_L1_TX_LIMIT,
1279
1595
  ...this.proposerAddressForSimulation && {
1280
1596
  from: this.proposerAddressForSimulation.toString()
1281
1597
  }
1282
1598
  }, {
1283
- // @note we add 1n to the timestamp because geth implementation doesn't like simulation timestamp to be equal to the current block timestamp
1284
- time: timestamp + 1n,
1599
+ time: simTs,
1285
1600
  // @note reth should have a 30m gas limit per block but throws errors that this tx is beyond limit so we increase here
1286
- gasLimit: SequencerPublisher.PROPOSE_GAS_GUESS * 2n
1601
+ gasLimit: MAX_L1_TX_LIMIT * 2n
1287
1602
  }, stateOverrides, RollupAbi, {
1288
1603
  // @note fallback gas estimate to use if the node doesn't support simulation API
1289
- fallbackGasEstimate: SequencerPublisher.PROPOSE_GAS_GUESS
1604
+ fallbackGasEstimate: MAX_L1_TX_LIMIT
1290
1605
  }).catch((err)=>{
1291
1606
  // In fisherman mode, we expect ValidatorSelection__MissingProposerSignature since fisherman doesn't have proposer signature
1292
1607
  const viemError = formatViemError(err);
@@ -1294,11 +1609,33 @@ export class SequencerPublisher {
1294
1609
  this.log.debug(`Ignoring expected ValidatorSelection__MissingProposerSignature error in fisherman mode`);
1295
1610
  // Return a minimal simulation result with the fallback gas estimate
1296
1611
  return {
1297
- gasUsed: SequencerPublisher.PROPOSE_GAS_GUESS,
1612
+ gasUsed: MAX_L1_TX_LIMIT,
1298
1613
  logs: []
1299
1614
  };
1300
1615
  }
1301
- this.log.error(`Failed to simulate propose tx`, viemError);
1616
+ this.log.error(`Failed to simulate propose tx`, viemError, {
1617
+ simulationTimestamp: simTs
1618
+ });
1619
+ this.backupFailedTx({
1620
+ id: keccak256(rollupData),
1621
+ failureType: 'simulation',
1622
+ request: {
1623
+ to: this.rollupContract.address,
1624
+ data: rollupData
1625
+ },
1626
+ l1BlockNumber: l1BlockNumber.toString(),
1627
+ error: {
1628
+ message: viemError.message,
1629
+ name: viemError.name
1630
+ },
1631
+ context: {
1632
+ actions: [
1633
+ 'propose'
1634
+ ],
1635
+ slot: Number(args[0].header.slotNumber),
1636
+ sender: this.getSenderAddress().toString()
1637
+ }
1638
+ });
1302
1639
  throw err;
1303
1640
  });
1304
1641
  return {
@@ -1306,11 +1643,11 @@ export class SequencerPublisher {
1306
1643
  simulationResult
1307
1644
  };
1308
1645
  }
1309
- async addProposeTx(checkpoint, encodedData, opts = {}, timestamp) {
1646
+ async addProposeTx(checkpoint, encodedData, opts = {}) {
1310
1647
  const slot = checkpoint.header.slotNumber;
1311
1648
  const timer = new Timer();
1312
1649
  const kzg = Blob.getViemKzgInstance();
1313
- const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(encodedData, timestamp, opts);
1650
+ const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(encodedData, opts);
1314
1651
  const startBlock = await this.l1TxUtils.getBlockNumber();
1315
1652
  const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil(Number(simulationResult.gasUsed) * 64 / 63)) + blobEvaluationGas + SequencerPublisher.MULTICALL_OVERHEAD_GAS_GUESS);
1316
1653
  // Send the blobs to the blob client preemptively. This helps in tests where the sequencer mistakingly thinks that the propose
@@ -1375,4 +1712,13 @@ export class SequencerPublisher {
1375
1712
  }
1376
1713
  });
1377
1714
  }
1715
+ /** Returns the timestamp of the last L1 slot within a given L2 slot. Used as the simulation timestamp
1716
+ * for eth_simulateV1 calls, since it's guaranteed to be greater than any L1 block produced during the slot. */ getSimulationTimestamp(slot) {
1717
+ const l1Constants = this.epochCache.getL1Constants();
1718
+ return getLastL1SlotTimestampForL2Slot(slot, l1Constants);
1719
+ }
1720
+ /** Returns the timestamp of the next L1 slot boundary after now. */ getNextL1SlotTimestamp() {
1721
+ const l1Constants = this.epochCache.getL1Constants();
1722
+ return getNextL1SlotTimestamp(this.dateProvider.nowInSeconds(), l1Constants);
1723
+ }
1378
1724
  }