@aztec/sequencer-client 0.0.1-commit.e6bd8901 → 0.0.1-commit.ec5f612

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 (74) hide show
  1. package/dest/client/sequencer-client.d.ts +12 -7
  2. package/dest/client/sequencer-client.d.ts.map +1 -1
  3. package/dest/client/sequencer-client.js +15 -4
  4. package/dest/config.d.ts +3 -4
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +17 -12
  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 +13 -2
  29. package/dest/publisher/sequencer-publisher-metrics.d.ts +1 -1
  30. package/dest/publisher/sequencer-publisher-metrics.d.ts.map +1 -1
  31. package/dest/publisher/sequencer-publisher-metrics.js +12 -4
  32. package/dest/publisher/sequencer-publisher.d.ts +22 -8
  33. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  34. package/dest/publisher/sequencer-publisher.js +297 -47
  35. package/dest/sequencer/checkpoint_proposal_job.d.ts +32 -9
  36. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  37. package/dest/sequencer/checkpoint_proposal_job.js +114 -59
  38. package/dest/sequencer/metrics.d.ts +17 -5
  39. package/dest/sequencer/metrics.d.ts.map +1 -1
  40. package/dest/sequencer/metrics.js +111 -30
  41. package/dest/sequencer/sequencer.d.ts +17 -7
  42. package/dest/sequencer/sequencer.d.ts.map +1 -1
  43. package/dest/sequencer/sequencer.js +30 -27
  44. package/dest/sequencer/timetable.d.ts +1 -4
  45. package/dest/sequencer/timetable.d.ts.map +1 -1
  46. package/dest/sequencer/timetable.js +2 -5
  47. package/dest/test/index.d.ts +3 -5
  48. package/dest/test/index.d.ts.map +1 -1
  49. package/dest/test/mock_checkpoint_builder.d.ts +10 -5
  50. package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
  51. package/dest/test/mock_checkpoint_builder.js +24 -10
  52. package/dest/test/utils.d.ts +3 -3
  53. package/dest/test/utils.d.ts.map +1 -1
  54. package/dest/test/utils.js +5 -4
  55. package/package.json +28 -28
  56. package/src/client/sequencer-client.ts +25 -7
  57. package/src/config.ts +26 -19
  58. package/src/global_variable_builder/global_builder.ts +1 -1
  59. package/src/publisher/config.ts +121 -43
  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 +23 -6
  66. package/src/publisher/sequencer-publisher-metrics.ts +7 -3
  67. package/src/publisher/sequencer-publisher.ts +274 -53
  68. package/src/sequencer/checkpoint_proposal_job.ts +159 -76
  69. package/src/sequencer/metrics.ts +124 -32
  70. package/src/sequencer/sequencer.ts +40 -32
  71. package/src/sequencer/timetable.ts +7 -6
  72. package/src/test/index.ts +2 -4
  73. package/src/test/mock_checkpoint_builder.ts +34 -9
  74. package/src/test/utils.ts +5 -2
@@ -372,10 +372,10 @@ 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';
@@ -383,13 +383,15 @@ import { pick } from '@aztec/foundation/collection';
383
383
  import { EthAddress } from '@aztec/foundation/eth-address';
384
384
  import { Signature } from '@aztec/foundation/eth-signature';
385
385
  import { createLogger } from '@aztec/foundation/log';
386
+ import { makeBackoff, retry } from '@aztec/foundation/retry';
386
387
  import { bufferToHex } from '@aztec/foundation/string';
387
388
  import { Timer } from '@aztec/foundation/timer';
388
389
  import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
389
390
  import { encodeSlashConsensusVotes } from '@aztec/slasher';
390
391
  import { CommitteeAttestationsAndSigners } from '@aztec/stdlib/block';
391
392
  import { getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
392
- import { encodeFunctionData, toHex } from 'viem';
393
+ import { encodeFunctionData, keccak256, multicall3Abi, toHex } from 'viem';
394
+ import { createL1TxFailedStore } from './l1_tx_failed_store/index.js';
393
395
  import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
394
396
  export const Actions = [
395
397
  'invalidate-by-invalid-attestation',
@@ -429,19 +431,18 @@ export class SequencerPublisher {
429
431
  interrupted;
430
432
  metrics;
431
433
  epochCache;
434
+ failedTxStore;
432
435
  governanceLog;
433
436
  slashingLog;
434
437
  lastActions;
435
438
  isPayloadEmptyCache;
439
+ payloadProposedCache;
436
440
  log;
437
441
  ethereumSlotDuration;
438
442
  blobClient;
439
443
  /** Address to use for simulations in fisherman mode (actual proposer's address) */ proposerAddressForSimulation;
440
444
  /** 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;
445
+ /** Fee asset price oracle for computing price modifiers from Uniswap V4 */ feeAssetPriceOracle;
445
446
  // A CALL to a cold address is 2700 gas
446
447
  static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
447
448
  // Gas report for VotingWithSigTest shows a max gas of 100k, but we've seen it cost 700k+ in testnet
@@ -460,6 +461,7 @@ export class SequencerPublisher {
460
461
  this.slashingLog = createLogger('sequencer:publisher:slashing');
461
462
  this.lastActions = {};
462
463
  this.isPayloadEmptyCache = new Map();
464
+ this.payloadProposedCache = new Set();
463
465
  this.requests = [];
464
466
  this.log = deps.log ?? createLogger('sequencer:publisher');
465
467
  this.ethereumSlotDuration = BigInt(config.ethereumSlotDuration);
@@ -483,10 +485,36 @@ export class SequencerPublisher {
483
485
  if (config.fishermanMode) {
484
486
  this.l1FeeAnalyzer = new L1FeeAnalyzer(this.l1TxUtils.client, deps.dateProvider, createLogger('sequencer:publisher:fee-analyzer'));
485
487
  }
488
+ // Initialize fee asset price oracle
489
+ this.feeAssetPriceOracle = new FeeAssetPriceOracle(this.l1TxUtils.client, this.rollupContract, createLogger('sequencer:publisher:price-oracle'));
490
+ // Initialize failed L1 tx store (optional, for test networks)
491
+ this.failedTxStore = createL1TxFailedStore(config.l1TxFailedStore, this.log);
492
+ }
493
+ /**
494
+ * Backs up a failed L1 transaction to the configured store for debugging.
495
+ * Does nothing if no store is configured.
496
+ */ backupFailedTx(failedTx) {
497
+ if (!this.failedTxStore) {
498
+ return;
499
+ }
500
+ const tx = {
501
+ ...failedTx,
502
+ timestamp: Date.now()
503
+ };
504
+ // Fire and forget - don't block on backup
505
+ void this.failedTxStore.then((store)=>store?.saveFailedTx(tx)).catch((err)=>{
506
+ this.log.warn(`Failed to backup failed L1 tx to store`, err);
507
+ });
486
508
  }
487
509
  getRollupContract() {
488
510
  return this.rollupContract;
489
511
  }
512
+ /**
513
+ * Gets the fee asset price modifier from the oracle.
514
+ * Returns 0n if the oracle query fails.
515
+ */ getFeeAssetPriceModifier() {
516
+ return this.feeAssetPriceOracle.computePriceModifier();
517
+ }
490
518
  getSenderAddress() {
491
519
  return this.l1TxUtils.getSenderAddress();
492
520
  }
@@ -544,7 +572,7 @@ export class SequencerPublisher {
544
572
  // Get the transaction requests
545
573
  const l1Requests = requestsToAnalyze.map((r)=>r.request);
546
574
  // Start the analysis
547
- const analysisId = await this.l1FeeAnalyzer.startAnalysis(l2SlotNumber, gasLimit > 0n ? gasLimit : SequencerPublisher.PROPOSE_GAS_GUESS, l1Requests, blobConfig, onComplete);
575
+ const analysisId = await this.l1FeeAnalyzer.startAnalysis(l2SlotNumber, gasLimit > 0n ? gasLimit : MAX_L1_TX_LIMIT, l1Requests, blobConfig, onComplete);
548
576
  this.log.info('Started L1 fee analysis', {
549
577
  analysisId,
550
578
  l2SlotNumber: l2SlotNumber.toString(),
@@ -602,7 +630,16 @@ export class SequencerPublisher {
602
630
  const blobConfig = blobConfigs[0];
603
631
  // Merge gasConfigs. Yields the sum of gasLimits, and the earliest txTimeoutAt, or undefined if no gasConfig sets them.
604
632
  const gasLimits = gasConfigs.map((g)=>g?.gasLimit).filter((g)=>g !== undefined);
605
- const gasLimit = gasLimits.length > 0 ? sumBigint(gasLimits) : undefined; // sum
633
+ let gasLimit = gasLimits.length > 0 ? sumBigint(gasLimits) : undefined; // sum
634
+ // Cap at L1 block gas limit so the node accepts the tx ("gas limit too high" otherwise).
635
+ const maxGas = MAX_L1_TX_LIMIT;
636
+ if (gasLimit !== undefined && gasLimit > maxGas) {
637
+ this.log.debug('Capping bundled tx gas limit to L1 max', {
638
+ requested: gasLimit,
639
+ capped: maxGas
640
+ });
641
+ gasLimit = maxGas;
642
+ }
606
643
  const txTimeoutAts = gasConfigs.map((g)=>g?.txTimeoutAt).filter((g)=>g !== undefined);
607
644
  const txTimeoutAt = txTimeoutAts.length > 0 ? new Date(Math.min(...txTimeoutAts.map((g)=>g.getTime()))) : undefined; // earliest
608
645
  const txConfig = {
@@ -613,12 +650,31 @@ export class SequencerPublisher {
613
650
  // This ensures the committee gets precomputed correctly
614
651
  validRequests.sort((a, b)=>compareActions(a.action, b.action));
615
652
  try {
653
+ // Capture context for failed tx backup before sending
654
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
655
+ const multicallData = encodeFunctionData({
656
+ abi: multicall3Abi,
657
+ functionName: 'aggregate3',
658
+ args: [
659
+ validRequests.map((r)=>({
660
+ target: r.request.to,
661
+ callData: r.request.data,
662
+ allowFailure: true
663
+ }))
664
+ ]
665
+ });
666
+ const blobDataHex = blobConfig?.blobs?.map((b)=>toHex(b));
616
667
  this.log.debug('Forwarding transactions', {
617
668
  validRequests: validRequests.map((request)=>request.action),
618
669
  txConfig
619
670
  });
620
671
  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);
672
+ const txContext = {
673
+ multicallData,
674
+ blobData: blobDataHex,
675
+ l1BlockNumber
676
+ };
677
+ const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(validRequests, result, txContext);
622
678
  return {
623
679
  result,
624
680
  expiredActions,
@@ -638,10 +694,33 @@ export class SequencerPublisher {
638
694
  }
639
695
  }
640
696
  }
641
- callbackBundledTransactions(requests, result) {
697
+ callbackBundledTransactions(requests, result, txContext) {
642
698
  const actionsListStr = requests.map((r)=>r.action).join(', ');
643
699
  if (result instanceof FormattedViemError) {
644
700
  this.log.error(`Failed to publish bundled transactions (${actionsListStr})`, result);
701
+ this.backupFailedTx({
702
+ id: keccak256(txContext.multicallData),
703
+ failureType: 'send-error',
704
+ request: {
705
+ to: MULTI_CALL_3_ADDRESS,
706
+ data: txContext.multicallData
707
+ },
708
+ blobData: txContext.blobData,
709
+ l1BlockNumber: txContext.l1BlockNumber.toString(),
710
+ error: {
711
+ message: result.message,
712
+ name: result.name
713
+ },
714
+ context: {
715
+ actions: requests.map((r)=>r.action),
716
+ requests: requests.map((r)=>({
717
+ action: r.action,
718
+ to: r.request.to,
719
+ data: r.request.data
720
+ })),
721
+ sender: this.getSenderAddress().toString()
722
+ }
723
+ });
645
724
  return {
646
725
  failedActions: requests.map((r)=>r.action)
647
726
  };
@@ -659,6 +738,37 @@ export class SequencerPublisher {
659
738
  failedActions.push(request.action);
660
739
  }
661
740
  }
741
+ // Single backup for the whole reverted tx
742
+ if (failedActions.length > 0 && result?.receipt?.status === 'reverted') {
743
+ this.backupFailedTx({
744
+ id: result.receipt.transactionHash,
745
+ failureType: 'revert',
746
+ request: {
747
+ to: MULTI_CALL_3_ADDRESS,
748
+ data: txContext.multicallData
749
+ },
750
+ blobData: txContext.blobData,
751
+ l1BlockNumber: result.receipt.blockNumber.toString(),
752
+ receipt: {
753
+ transactionHash: result.receipt.transactionHash,
754
+ blockNumber: result.receipt.blockNumber.toString(),
755
+ gasUsed: (result.receipt.gasUsed ?? 0n).toString(),
756
+ status: 'reverted'
757
+ },
758
+ error: {
759
+ message: result.errorMsg ?? 'Transaction reverted'
760
+ },
761
+ context: {
762
+ actions: failedActions,
763
+ requests: requests.filter((r)=>failedActions.includes(r.action)).map((r)=>({
764
+ action: r.action,
765
+ to: r.request.to,
766
+ data: r.request.data
767
+ })),
768
+ sender: this.getSenderAddress().toString()
769
+ }
770
+ });
771
+ }
662
772
  return {
663
773
  successfulActions,
664
774
  failedActions
@@ -761,8 +871,12 @@ export class SequencerPublisher {
761
871
  ...logData,
762
872
  request
763
873
  });
874
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
764
875
  try {
765
- const { gasUsed } = await this.l1TxUtils.simulate(request, undefined, undefined, ErrorsAbi);
876
+ const { gasUsed } = await this.l1TxUtils.simulate(request, undefined, undefined, mergeAbis([
877
+ request.abi ?? [],
878
+ ErrorsAbi
879
+ ]));
766
880
  this.log.verbose(`Simulation for invalidate checkpoint ${checkpointNumber} succeeded`, {
767
881
  ...logData,
768
882
  request,
@@ -779,7 +893,7 @@ export class SequencerPublisher {
779
893
  const viemError = formatViemError(err);
780
894
  // If the error is due to the checkpoint not being in the pending chain, and it was indeed removed by someone else,
781
895
  // we can safely ignore it and return undefined so we go ahead with checkpoint building.
782
- if (viemError.message?.includes('Rollup__BlockNotInPendingChain')) {
896
+ if (viemError.message?.includes('Rollup__CheckpointNotInPendingChain')) {
783
897
  this.log.verbose(`Simulation for invalidate checkpoint ${checkpointNumber} failed due to checkpoint not being in pending chain`, {
784
898
  ...logData,
785
899
  request,
@@ -800,6 +914,27 @@ export class SequencerPublisher {
800
914
  }
801
915
  // Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
802
916
  this.log.error(`Simulation for invalidate checkpoint ${checkpointNumber} failed`, viemError, logData);
917
+ this.backupFailedTx({
918
+ id: keccak256(request.data),
919
+ failureType: 'simulation',
920
+ request: {
921
+ to: request.to,
922
+ data: request.data,
923
+ value: request.value?.toString()
924
+ },
925
+ l1BlockNumber: l1BlockNumber.toString(),
926
+ error: {
927
+ message: viemError.message,
928
+ name: viemError.name
929
+ },
930
+ context: {
931
+ actions: [
932
+ `invalidate-${reason}`
933
+ ],
934
+ checkpointNumber,
935
+ sender: this.getSenderAddress().toString()
936
+ }
937
+ });
803
938
  throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, {
804
939
  cause: viemError
805
940
  });
@@ -827,29 +962,15 @@ export class SequencerPublisher {
827
962
  }
828
963
  /** Simulates `propose` to make sure that the checkpoint is valid for submission */ async validateCheckpointForSubmission(checkpoint, attestationsAndSigners, attestationsAndSignersSignature, options) {
829
964
  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
965
  const blobFields = checkpoint.toBlobFields();
845
- const blobs = getBlobsPerL1Block(blobFields);
966
+ const blobs = await getBlobsPerL1Block(blobFields);
846
967
  const blobInput = getPrefixedEthBlobCommitments(blobs);
847
968
  const args = [
848
969
  {
849
970
  header: checkpoint.header.toViem(),
850
971
  archive: toHex(checkpoint.archive.root.toBuffer()),
851
972
  oracleInput: {
852
- feeAssetPriceModifier: 0n
973
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier
853
974
  }
854
975
  },
855
976
  attestationsAndSigners.getPackedAttestations(),
@@ -884,6 +1005,28 @@ export class SequencerPublisher {
884
1005
  this.log.warn(`Skipping vote cast for payload with empty code`);
885
1006
  return false;
886
1007
  }
1008
+ // Check if payload was already submitted to governance
1009
+ const cacheKey = payload.toString();
1010
+ if (!this.payloadProposedCache.has(cacheKey)) {
1011
+ try {
1012
+ const l1StartBlock = await this.rollupContract.getL1StartBlock();
1013
+ const proposed = await retry(()=>base.hasPayloadBeenProposed(payload.toString(), l1StartBlock), 'Check if payload was proposed', makeBackoff([
1014
+ 0,
1015
+ 1,
1016
+ 2
1017
+ ]), this.log, true);
1018
+ if (proposed) {
1019
+ this.payloadProposedCache.add(cacheKey);
1020
+ }
1021
+ } catch (err) {
1022
+ this.log.warn(`Failed to check if payload ${payload} was proposed after retries, skipping signal`, err);
1023
+ return false;
1024
+ }
1025
+ }
1026
+ if (this.payloadProposedCache.has(cacheKey)) {
1027
+ this.log.info(`Payload ${payload} was already proposed to governance, stopping signals`);
1028
+ return false;
1029
+ }
887
1030
  const cachedLastVote = this.lastActions[signalType];
888
1031
  this.lastActions[signalType] = slotNumber;
889
1032
  const action = signalType;
@@ -894,15 +1037,41 @@ export class SequencerPublisher {
894
1037
  signer: this.l1TxUtils.client.account?.address,
895
1038
  lastValidL2Slot: slotNumber
896
1039
  });
1040
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
897
1041
  try {
898
1042
  await this.l1TxUtils.simulate(request, {
899
1043
  time: timestamp
900
- }, [], ErrorsAbi);
1044
+ }, [], mergeAbis([
1045
+ request.abi ?? [],
1046
+ ErrorsAbi
1047
+ ]));
901
1048
  this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, {
902
1049
  request
903
1050
  });
904
1051
  } catch (err) {
905
- this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, err);
1052
+ const viemError = formatViemError(err);
1053
+ this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, viemError);
1054
+ this.backupFailedTx({
1055
+ id: keccak256(request.data),
1056
+ failureType: 'simulation',
1057
+ request: {
1058
+ to: request.to,
1059
+ data: request.data,
1060
+ value: request.value?.toString()
1061
+ },
1062
+ l1BlockNumber: l1BlockNumber.toString(),
1063
+ error: {
1064
+ message: viemError.message,
1065
+ name: viemError.name
1066
+ },
1067
+ context: {
1068
+ actions: [
1069
+ action
1070
+ ],
1071
+ slot: slotNumber,
1072
+ sender: this.getSenderAddress().toString()
1073
+ }
1074
+ });
906
1075
  // Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
907
1076
  }
908
1077
  // TODO(palla/slash): All votes (governance and slashing) should txTimeoutAt at the end of the slot.
@@ -1041,13 +1210,14 @@ export class SequencerPublisher {
1041
1210
  /** Simulates and enqueues a proposal for a checkpoint on L1 */ async enqueueProposeCheckpoint(checkpoint, attestationsAndSigners, attestationsAndSignersSignature, opts = {}) {
1042
1211
  const checkpointHeader = checkpoint.header;
1043
1212
  const blobFields = checkpoint.toBlobFields();
1044
- const blobs = getBlobsPerL1Block(blobFields);
1213
+ const blobs = await getBlobsPerL1Block(blobFields);
1045
1214
  const proposeTxArgs = {
1046
1215
  header: checkpointHeader,
1047
1216
  archive: checkpoint.archive.root.toBuffer(),
1048
1217
  blobs,
1049
1218
  attestationsAndSigners,
1050
- attestationsAndSignersSignature
1219
+ attestationsAndSignersSignature,
1220
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier
1051
1221
  };
1052
1222
  let ts;
1053
1223
  try {
@@ -1123,28 +1293,60 @@ export class SequencerPublisher {
1123
1293
  const cachedLastActionSlot = this.lastActions[action];
1124
1294
  this.lastActions[action] = slotNumber;
1125
1295
  this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
1296
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1126
1297
  let gasUsed;
1298
+ const simulateAbi = mergeAbis([
1299
+ request.abi ?? [],
1300
+ ErrorsAbi
1301
+ ]);
1127
1302
  try {
1128
1303
  ({ gasUsed } = await this.l1TxUtils.simulate(request, {
1129
1304
  time: timestamp
1130
- }, [], ErrorsAbi)); // TODO(palla/slash): Check the timestamp logic
1305
+ }, [], simulateAbi)); // TODO(palla/slash): Check the timestamp logic
1131
1306
  this.log.verbose(`Simulation for ${action} succeeded`, {
1132
1307
  ...logData,
1133
1308
  request,
1134
1309
  gasUsed
1135
1310
  });
1136
1311
  } catch (err) {
1137
- const viemError = formatViemError(err);
1312
+ const viemError = formatViemError(err, simulateAbi);
1138
1313
  this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
1314
+ this.backupFailedTx({
1315
+ id: keccak256(request.data),
1316
+ failureType: 'simulation',
1317
+ request: {
1318
+ to: request.to,
1319
+ data: request.data,
1320
+ value: request.value?.toString()
1321
+ },
1322
+ l1BlockNumber: l1BlockNumber.toString(),
1323
+ error: {
1324
+ message: viemError.message,
1325
+ name: viemError.name
1326
+ },
1327
+ context: {
1328
+ actions: [
1329
+ action
1330
+ ],
1331
+ slot: slotNumber,
1332
+ sender: this.getSenderAddress().toString()
1333
+ }
1334
+ });
1139
1335
  return false;
1140
1336
  }
1141
1337
  // We issued the simulation against the rollup contract, so we need to account for the overhead of the multicall3
1142
1338
  const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil(Number(gasUsed) * 64 / 63)));
1143
1339
  logData.gasLimit = gasLimit;
1340
+ // Store the ABI used for simulation on the request so Multicall3.forward can decode errors
1341
+ // when the tx is sent and a revert is diagnosed via simulation.
1342
+ const requestWithAbi = {
1343
+ ...request,
1344
+ abi: simulateAbi
1345
+ };
1144
1346
  this.log.debug(`Enqueuing ${action}`, logData);
1145
1347
  this.addRequest({
1146
1348
  action,
1147
- request,
1349
+ request: requestWithAbi,
1148
1350
  gasConfig: {
1149
1351
  gasLimit
1150
1352
  },
@@ -1208,10 +1410,38 @@ export class SequencerPublisher {
1208
1410
  }, {}, {
1209
1411
  blobs: encodedData.blobs.map((b)=>b.data),
1210
1412
  kzg
1211
- }).catch((err)=>{
1212
- const { message, metaMessages } = formatViemError(err);
1213
- this.log.error(`Failed to validate blobs`, message, {
1214
- metaMessages
1413
+ }).catch(async (err)=>{
1414
+ const viemError = formatViemError(err);
1415
+ this.log.error(`Failed to validate blobs`, viemError.message, {
1416
+ metaMessages: viemError.metaMessages
1417
+ });
1418
+ const validateBlobsData = encodeFunctionData({
1419
+ abi: RollupAbi,
1420
+ functionName: 'validateBlobs',
1421
+ args: [
1422
+ blobInput
1423
+ ]
1424
+ });
1425
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1426
+ this.backupFailedTx({
1427
+ id: keccak256(validateBlobsData),
1428
+ failureType: 'simulation',
1429
+ request: {
1430
+ to: this.rollupContract.address,
1431
+ data: validateBlobsData
1432
+ },
1433
+ blobData: encodedData.blobs.map((b)=>toHex(b.data)),
1434
+ l1BlockNumber: l1BlockNumber.toString(),
1435
+ error: {
1436
+ message: viemError.message,
1437
+ name: viemError.name
1438
+ },
1439
+ context: {
1440
+ actions: [
1441
+ 'validate-blobs'
1442
+ ],
1443
+ sender: this.getSenderAddress().toString()
1444
+ }
1215
1445
  });
1216
1446
  throw new Error('Failed to validate blobs');
1217
1447
  });
@@ -1222,8 +1452,7 @@ export class SequencerPublisher {
1222
1452
  header: encodedData.header.toViem(),
1223
1453
  archive: toHex(encodedData.archive),
1224
1454
  oracleInput: {
1225
- // We are currently not modifying these. See #9963
1226
- feeAssetPriceModifier: 0n
1455
+ feeAssetPriceModifier: encodedData.feeAssetPriceModifier
1227
1456
  }
1228
1457
  },
1229
1458
  encodedData.attestationsAndSigners.getPackedAttestations(),
@@ -1272,10 +1501,11 @@ export class SequencerPublisher {
1272
1501
  balance: 10n * WEI_CONST * WEI_CONST
1273
1502
  });
1274
1503
  }
1504
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1275
1505
  const simulationResult = await this.l1TxUtils.simulate({
1276
1506
  to: this.rollupContract.address,
1277
1507
  data: rollupData,
1278
- gas: SequencerPublisher.PROPOSE_GAS_GUESS,
1508
+ gas: MAX_L1_TX_LIMIT,
1279
1509
  ...this.proposerAddressForSimulation && {
1280
1510
  from: this.proposerAddressForSimulation.toString()
1281
1511
  }
@@ -1283,10 +1513,10 @@ export class SequencerPublisher {
1283
1513
  // @note we add 1n to the timestamp because geth implementation doesn't like simulation timestamp to be equal to the current block timestamp
1284
1514
  time: timestamp + 1n,
1285
1515
  // @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
1516
+ gasLimit: MAX_L1_TX_LIMIT * 2n
1287
1517
  }, stateOverrides, RollupAbi, {
1288
1518
  // @note fallback gas estimate to use if the node doesn't support simulation API
1289
- fallbackGasEstimate: SequencerPublisher.PROPOSE_GAS_GUESS
1519
+ fallbackGasEstimate: MAX_L1_TX_LIMIT
1290
1520
  }).catch((err)=>{
1291
1521
  // In fisherman mode, we expect ValidatorSelection__MissingProposerSignature since fisherman doesn't have proposer signature
1292
1522
  const viemError = formatViemError(err);
@@ -1294,11 +1524,31 @@ export class SequencerPublisher {
1294
1524
  this.log.debug(`Ignoring expected ValidatorSelection__MissingProposerSignature error in fisherman mode`);
1295
1525
  // Return a minimal simulation result with the fallback gas estimate
1296
1526
  return {
1297
- gasUsed: SequencerPublisher.PROPOSE_GAS_GUESS,
1527
+ gasUsed: MAX_L1_TX_LIMIT,
1298
1528
  logs: []
1299
1529
  };
1300
1530
  }
1301
1531
  this.log.error(`Failed to simulate propose tx`, viemError);
1532
+ this.backupFailedTx({
1533
+ id: keccak256(rollupData),
1534
+ failureType: 'simulation',
1535
+ request: {
1536
+ to: this.rollupContract.address,
1537
+ data: rollupData
1538
+ },
1539
+ l1BlockNumber: l1BlockNumber.toString(),
1540
+ error: {
1541
+ message: viemError.message,
1542
+ name: viemError.name
1543
+ },
1544
+ context: {
1545
+ actions: [
1546
+ 'propose'
1547
+ ],
1548
+ slot: Number(args[0].header.slotNumber),
1549
+ sender: this.getSenderAddress().toString()
1550
+ }
1551
+ });
1302
1552
  throw err;
1303
1553
  });
1304
1554
  return {