@aztec/sequencer-client 0.0.1-commit.3469e52 → 0.0.1-commit.3895657bc

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 (78) hide show
  1. package/dest/client/sequencer-client.d.ts +23 -7
  2. package/dest/client/sequencer-client.d.ts.map +1 -1
  3. package/dest/client/sequencer-client.js +99 -16
  4. package/dest/config.d.ts +24 -6
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +40 -30
  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/global_variable_builder/global_builder.js +2 -2
  10. package/dest/publisher/config.d.ts +35 -17
  11. package/dest/publisher/config.d.ts.map +1 -1
  12. package/dest/publisher/config.js +106 -42
  13. package/dest/publisher/index.d.ts +2 -1
  14. package/dest/publisher/index.d.ts.map +1 -1
  15. package/dest/publisher/l1_tx_failed_store/factory.d.ts +11 -0
  16. package/dest/publisher/l1_tx_failed_store/factory.d.ts.map +1 -0
  17. package/dest/publisher/l1_tx_failed_store/factory.js +22 -0
  18. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts +59 -0
  19. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -0
  20. package/dest/publisher/l1_tx_failed_store/failed_tx_store.js +1 -0
  21. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts +15 -0
  22. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts.map +1 -0
  23. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.js +34 -0
  24. package/dest/publisher/l1_tx_failed_store/index.d.ts +4 -0
  25. package/dest/publisher/l1_tx_failed_store/index.d.ts.map +1 -0
  26. package/dest/publisher/l1_tx_failed_store/index.js +2 -0
  27. package/dest/publisher/sequencer-publisher-factory.d.ts +11 -3
  28. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  29. package/dest/publisher/sequencer-publisher-factory.js +27 -2
  30. package/dest/publisher/sequencer-publisher-metrics.d.ts +1 -1
  31. package/dest/publisher/sequencer-publisher-metrics.d.ts.map +1 -1
  32. package/dest/publisher/sequencer-publisher-metrics.js +12 -4
  33. package/dest/publisher/sequencer-publisher.d.ts +26 -8
  34. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  35. package/dest/publisher/sequencer-publisher.js +338 -48
  36. package/dest/sequencer/checkpoint_proposal_job.d.ts +31 -10
  37. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  38. package/dest/sequencer/checkpoint_proposal_job.js +180 -95
  39. package/dest/sequencer/metrics.d.ts +17 -5
  40. package/dest/sequencer/metrics.d.ts.map +1 -1
  41. package/dest/sequencer/metrics.js +111 -30
  42. package/dest/sequencer/sequencer.d.ts +25 -12
  43. package/dest/sequencer/sequencer.d.ts.map +1 -1
  44. package/dest/sequencer/sequencer.js +31 -28
  45. package/dest/sequencer/timetable.d.ts +4 -6
  46. package/dest/sequencer/timetable.d.ts.map +1 -1
  47. package/dest/sequencer/timetable.js +7 -11
  48. package/dest/sequencer/types.d.ts +5 -2
  49. package/dest/sequencer/types.d.ts.map +1 -1
  50. package/dest/test/index.d.ts +3 -5
  51. package/dest/test/index.d.ts.map +1 -1
  52. package/dest/test/mock_checkpoint_builder.d.ts +17 -14
  53. package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
  54. package/dest/test/mock_checkpoint_builder.js +63 -40
  55. package/dest/test/utils.d.ts +8 -8
  56. package/dest/test/utils.d.ts.map +1 -1
  57. package/dest/test/utils.js +10 -9
  58. package/package.json +28 -28
  59. package/src/client/sequencer-client.ts +135 -18
  60. package/src/config.ts +55 -41
  61. package/src/global_variable_builder/global_builder.ts +3 -3
  62. package/src/publisher/config.ts +121 -43
  63. package/src/publisher/index.ts +3 -0
  64. package/src/publisher/l1_tx_failed_store/factory.ts +32 -0
  65. package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +55 -0
  66. package/src/publisher/l1_tx_failed_store/file_store_failed_tx_store.ts +46 -0
  67. package/src/publisher/l1_tx_failed_store/index.ts +3 -0
  68. package/src/publisher/sequencer-publisher-factory.ts +38 -6
  69. package/src/publisher/sequencer-publisher-metrics.ts +7 -3
  70. package/src/publisher/sequencer-publisher.ts +333 -60
  71. package/src/sequencer/checkpoint_proposal_job.ts +246 -127
  72. package/src/sequencer/metrics.ts +124 -32
  73. package/src/sequencer/sequencer.ts +41 -33
  74. package/src/sequencer/timetable.ts +13 -12
  75. package/src/sequencer/types.ts +4 -1
  76. package/src/test/index.ts +2 -4
  77. package/src/test/mock_checkpoint_builder.ts +90 -62
  78. package/src/test/utils.ts +22 -13
@@ -372,24 +372,27 @@ 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
382
  import { pick } from '@aztec/foundation/collection';
383
+ import { TimeoutError } from '@aztec/foundation/error';
383
384
  import { EthAddress } from '@aztec/foundation/eth-address';
384
385
  import { Signature } from '@aztec/foundation/eth-signature';
385
386
  import { createLogger } from '@aztec/foundation/log';
387
+ import { makeBackoff, retry } from '@aztec/foundation/retry';
386
388
  import { bufferToHex } from '@aztec/foundation/string';
387
389
  import { Timer } from '@aztec/foundation/timer';
388
390
  import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
389
391
  import { encodeSlashConsensusVotes } from '@aztec/slasher';
390
392
  import { CommitteeAttestationsAndSigners } from '@aztec/stdlib/block';
391
393
  import { getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
392
- import { encodeFunctionData, toHex } from 'viem';
394
+ import { encodeFunctionData, keccak256, multicall3Abi, toHex } from 'viem';
395
+ import { createL1TxFailedStore } from './l1_tx_failed_store/index.js';
393
396
  import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
394
397
  export const Actions = [
395
398
  'invalidate-by-invalid-attestation',
@@ -429,19 +432,19 @@ export class SequencerPublisher {
429
432
  interrupted;
430
433
  metrics;
431
434
  epochCache;
435
+ failedTxStore;
432
436
  governanceLog;
433
437
  slashingLog;
434
438
  lastActions;
435
439
  isPayloadEmptyCache;
440
+ payloadProposedCache;
436
441
  log;
437
442
  ethereumSlotDuration;
438
443
  blobClient;
439
444
  /** Address to use for simulations in fisherman mode (actual proposer's address) */ proposerAddressForSimulation;
445
+ /** Optional callback to obtain a replacement publisher when the current one fails to send. */ getNextPublisher;
440
446
  /** 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;
447
+ /** Fee asset price oracle for computing price modifiers from Uniswap V4 */ feeAssetPriceOracle;
445
448
  // A CALL to a cold address is 2700 gas
446
449
  static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
447
450
  // Gas report for VotingWithSigTest shows a max gas of 100k, but we've seen it cost 700k+ in testnet
@@ -460,6 +463,7 @@ export class SequencerPublisher {
460
463
  this.slashingLog = createLogger('sequencer:publisher:slashing');
461
464
  this.lastActions = {};
462
465
  this.isPayloadEmptyCache = new Map();
466
+ this.payloadProposedCache = new Set();
463
467
  this.requests = [];
464
468
  this.log = deps.log ?? createLogger('sequencer:publisher');
465
469
  this.ethereumSlotDuration = BigInt(config.ethereumSlotDuration);
@@ -470,6 +474,7 @@ export class SequencerPublisher {
470
474
  this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
471
475
  this.tracer = telemetry.getTracer('SequencerPublisher');
472
476
  this.l1TxUtils = deps.l1TxUtils;
477
+ this.getNextPublisher = deps.getNextPublisher;
473
478
  this.rollupContract = deps.rollupContract;
474
479
  this.govProposerContract = deps.governanceProposerContract;
475
480
  this.slashingProposerContract = deps.slashingProposerContract;
@@ -483,10 +488,36 @@ export class SequencerPublisher {
483
488
  if (config.fishermanMode) {
484
489
  this.l1FeeAnalyzer = new L1FeeAnalyzer(this.l1TxUtils.client, deps.dateProvider, createLogger('sequencer:publisher:fee-analyzer'));
485
490
  }
491
+ // Initialize fee asset price oracle
492
+ this.feeAssetPriceOracle = new FeeAssetPriceOracle(this.l1TxUtils.client, this.rollupContract, createLogger('sequencer:publisher:price-oracle'));
493
+ // Initialize failed L1 tx store (optional, for test networks)
494
+ this.failedTxStore = createL1TxFailedStore(config.l1TxFailedStore, this.log);
495
+ }
496
+ /**
497
+ * Backs up a failed L1 transaction to the configured store for debugging.
498
+ * Does nothing if no store is configured.
499
+ */ backupFailedTx(failedTx) {
500
+ if (!this.failedTxStore) {
501
+ return;
502
+ }
503
+ const tx = {
504
+ ...failedTx,
505
+ timestamp: Date.now()
506
+ };
507
+ // Fire and forget - don't block on backup
508
+ void this.failedTxStore.then((store)=>store?.saveFailedTx(tx)).catch((err)=>{
509
+ this.log.warn(`Failed to backup failed L1 tx to store`, err);
510
+ });
486
511
  }
487
512
  getRollupContract() {
488
513
  return this.rollupContract;
489
514
  }
515
+ /**
516
+ * Gets the fee asset price modifier from the oracle.
517
+ * Returns 0n if the oracle query fails.
518
+ */ getFeeAssetPriceModifier() {
519
+ return this.feeAssetPriceOracle.computePriceModifier();
520
+ }
490
521
  getSenderAddress() {
491
522
  return this.l1TxUtils.getSenderAddress();
492
523
  }
@@ -544,7 +575,7 @@ export class SequencerPublisher {
544
575
  // Get the transaction requests
545
576
  const l1Requests = requestsToAnalyze.map((r)=>r.request);
546
577
  // Start the analysis
547
- const analysisId = await this.l1FeeAnalyzer.startAnalysis(l2SlotNumber, gasLimit > 0n ? gasLimit : SequencerPublisher.PROPOSE_GAS_GUESS, l1Requests, blobConfig, onComplete);
578
+ const analysisId = await this.l1FeeAnalyzer.startAnalysis(l2SlotNumber, gasLimit > 0n ? gasLimit : MAX_L1_TX_LIMIT, l1Requests, blobConfig, onComplete);
548
579
  this.log.info('Started L1 fee analysis', {
549
580
  analysisId,
550
581
  l2SlotNumber: l2SlotNumber.toString(),
@@ -602,7 +633,16 @@ export class SequencerPublisher {
602
633
  const blobConfig = blobConfigs[0];
603
634
  // Merge gasConfigs. Yields the sum of gasLimits, and the earliest txTimeoutAt, or undefined if no gasConfig sets them.
604
635
  const gasLimits = gasConfigs.map((g)=>g?.gasLimit).filter((g)=>g !== undefined);
605
- const gasLimit = gasLimits.length > 0 ? sumBigint(gasLimits) : undefined; // sum
636
+ let gasLimit = gasLimits.length > 0 ? sumBigint(gasLimits) : undefined; // sum
637
+ // Cap at L1 block gas limit so the node accepts the tx ("gas limit too high" otherwise).
638
+ const maxGas = MAX_L1_TX_LIMIT;
639
+ if (gasLimit !== undefined && gasLimit > maxGas) {
640
+ this.log.debug('Capping bundled tx gas limit to L1 max', {
641
+ requested: gasLimit,
642
+ capped: maxGas
643
+ });
644
+ gasLimit = maxGas;
645
+ }
606
646
  const txTimeoutAts = gasConfigs.map((g)=>g?.txTimeoutAt).filter((g)=>g !== undefined);
607
647
  const txTimeoutAt = txTimeoutAts.length > 0 ? new Date(Math.min(...txTimeoutAts.map((g)=>g.getTime()))) : undefined; // earliest
608
648
  const txConfig = {
@@ -613,12 +653,34 @@ export class SequencerPublisher {
613
653
  // This ensures the committee gets precomputed correctly
614
654
  validRequests.sort((a, b)=>compareActions(a.action, b.action));
615
655
  try {
656
+ // Capture context for failed tx backup before sending
657
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
658
+ const multicallData = encodeFunctionData({
659
+ abi: multicall3Abi,
660
+ functionName: 'aggregate3',
661
+ args: [
662
+ validRequests.map((r)=>({
663
+ target: r.request.to,
664
+ callData: r.request.data,
665
+ allowFailure: true
666
+ }))
667
+ ]
668
+ });
669
+ const blobDataHex = blobConfig?.blobs?.map((b)=>toHex(b));
670
+ const txContext = {
671
+ multicallData,
672
+ blobData: blobDataHex,
673
+ l1BlockNumber
674
+ };
616
675
  this.log.debug('Forwarding transactions', {
617
676
  validRequests: validRequests.map((request)=>request.action),
618
677
  txConfig
619
678
  });
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);
679
+ const result = await this.forwardWithPublisherRotation(validRequests, txConfig, blobConfig);
680
+ if (result === undefined) {
681
+ return undefined;
682
+ }
683
+ const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(validRequests, result, txContext);
622
684
  return {
623
685
  result,
624
686
  expiredActions,
@@ -638,10 +700,67 @@ export class SequencerPublisher {
638
700
  }
639
701
  }
640
702
  }
641
- callbackBundledTransactions(requests, result) {
703
+ /**
704
+ * Forwards transactions via Multicall3, rotating to the next available publisher if a send
705
+ * failure occurs (i.e. the tx never reached the chain).
706
+ * On-chain reverts and simulation errors are returned as-is without rotation.
707
+ */ async forwardWithPublisherRotation(validRequests, txConfig, blobConfig) {
708
+ const triedAddresses = [];
709
+ let currentPublisher = this.l1TxUtils;
710
+ while(true){
711
+ triedAddresses.push(currentPublisher.getSenderAddress());
712
+ try {
713
+ const result = await Multicall3.forward(validRequests.map((r)=>r.request), currentPublisher, txConfig, blobConfig, this.rollupContract.address, this.log);
714
+ this.l1TxUtils = currentPublisher;
715
+ return result;
716
+ } catch (err) {
717
+ if (err instanceof TimeoutError) {
718
+ throw err;
719
+ }
720
+ const viemError = formatViemError(err);
721
+ if (!this.getNextPublisher) {
722
+ this.log.error('Failed to publish bundled transactions', viemError);
723
+ return undefined;
724
+ }
725
+ this.log.warn(`Publisher ${currentPublisher.getSenderAddress()} failed to send, rotating to next publisher`, viemError);
726
+ const nextPublisher = await this.getNextPublisher([
727
+ ...triedAddresses
728
+ ]);
729
+ if (!nextPublisher) {
730
+ this.log.error('All available publishers exhausted, failed to publish bundled transactions');
731
+ return undefined;
732
+ }
733
+ currentPublisher = nextPublisher;
734
+ }
735
+ }
736
+ }
737
+ callbackBundledTransactions(requests, result, txContext) {
642
738
  const actionsListStr = requests.map((r)=>r.action).join(', ');
643
739
  if (result instanceof FormattedViemError) {
644
740
  this.log.error(`Failed to publish bundled transactions (${actionsListStr})`, result);
741
+ this.backupFailedTx({
742
+ id: keccak256(txContext.multicallData),
743
+ failureType: 'send-error',
744
+ request: {
745
+ to: MULTI_CALL_3_ADDRESS,
746
+ data: txContext.multicallData
747
+ },
748
+ blobData: txContext.blobData,
749
+ l1BlockNumber: txContext.l1BlockNumber.toString(),
750
+ error: {
751
+ message: result.message,
752
+ name: result.name
753
+ },
754
+ context: {
755
+ actions: requests.map((r)=>r.action),
756
+ requests: requests.map((r)=>({
757
+ action: r.action,
758
+ to: r.request.to,
759
+ data: r.request.data
760
+ })),
761
+ sender: this.getSenderAddress().toString()
762
+ }
763
+ });
645
764
  return {
646
765
  failedActions: requests.map((r)=>r.action)
647
766
  };
@@ -659,6 +778,37 @@ export class SequencerPublisher {
659
778
  failedActions.push(request.action);
660
779
  }
661
780
  }
781
+ // Single backup for the whole reverted tx
782
+ if (failedActions.length > 0 && result?.receipt?.status === 'reverted') {
783
+ this.backupFailedTx({
784
+ id: result.receipt.transactionHash,
785
+ failureType: 'revert',
786
+ request: {
787
+ to: MULTI_CALL_3_ADDRESS,
788
+ data: txContext.multicallData
789
+ },
790
+ blobData: txContext.blobData,
791
+ l1BlockNumber: result.receipt.blockNumber.toString(),
792
+ receipt: {
793
+ transactionHash: result.receipt.transactionHash,
794
+ blockNumber: result.receipt.blockNumber.toString(),
795
+ gasUsed: (result.receipt.gasUsed ?? 0n).toString(),
796
+ status: 'reverted'
797
+ },
798
+ error: {
799
+ message: result.errorMsg ?? 'Transaction reverted'
800
+ },
801
+ context: {
802
+ actions: failedActions,
803
+ requests: requests.filter((r)=>failedActions.includes(r.action)).map((r)=>({
804
+ action: r.action,
805
+ to: r.request.to,
806
+ data: r.request.data
807
+ })),
808
+ sender: this.getSenderAddress().toString()
809
+ }
810
+ });
811
+ }
662
812
  return {
663
813
  successfulActions,
664
814
  failedActions
@@ -761,8 +911,12 @@ export class SequencerPublisher {
761
911
  ...logData,
762
912
  request
763
913
  });
914
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
764
915
  try {
765
- const { gasUsed } = await this.l1TxUtils.simulate(request, undefined, undefined, ErrorsAbi);
916
+ const { gasUsed } = await this.l1TxUtils.simulate(request, undefined, undefined, mergeAbis([
917
+ request.abi ?? [],
918
+ ErrorsAbi
919
+ ]));
766
920
  this.log.verbose(`Simulation for invalidate checkpoint ${checkpointNumber} succeeded`, {
767
921
  ...logData,
768
922
  request,
@@ -779,7 +933,7 @@ export class SequencerPublisher {
779
933
  const viemError = formatViemError(err);
780
934
  // If the error is due to the checkpoint not being in the pending chain, and it was indeed removed by someone else,
781
935
  // we can safely ignore it and return undefined so we go ahead with checkpoint building.
782
- if (viemError.message?.includes('Rollup__BlockNotInPendingChain')) {
936
+ if (viemError.message?.includes('Rollup__CheckpointNotInPendingChain')) {
783
937
  this.log.verbose(`Simulation for invalidate checkpoint ${checkpointNumber} failed due to checkpoint not being in pending chain`, {
784
938
  ...logData,
785
939
  request,
@@ -800,6 +954,27 @@ export class SequencerPublisher {
800
954
  }
801
955
  // Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
802
956
  this.log.error(`Simulation for invalidate checkpoint ${checkpointNumber} failed`, viemError, logData);
957
+ this.backupFailedTx({
958
+ id: keccak256(request.data),
959
+ failureType: 'simulation',
960
+ request: {
961
+ to: request.to,
962
+ data: request.data,
963
+ value: request.value?.toString()
964
+ },
965
+ l1BlockNumber: l1BlockNumber.toString(),
966
+ error: {
967
+ message: viemError.message,
968
+ name: viemError.name
969
+ },
970
+ context: {
971
+ actions: [
972
+ `invalidate-${reason}`
973
+ ],
974
+ checkpointNumber,
975
+ sender: this.getSenderAddress().toString()
976
+ }
977
+ });
803
978
  throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, {
804
979
  cause: viemError
805
980
  });
@@ -827,29 +1002,15 @@ export class SequencerPublisher {
827
1002
  }
828
1003
  /** Simulates `propose` to make sure that the checkpoint is valid for submission */ async validateCheckpointForSubmission(checkpoint, attestationsAndSigners, attestationsAndSignersSignature, options) {
829
1004
  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
1005
  const blobFields = checkpoint.toBlobFields();
845
- const blobs = getBlobsPerL1Block(blobFields);
1006
+ const blobs = await getBlobsPerL1Block(blobFields);
846
1007
  const blobInput = getPrefixedEthBlobCommitments(blobs);
847
1008
  const args = [
848
1009
  {
849
1010
  header: checkpoint.header.toViem(),
850
1011
  archive: toHex(checkpoint.archive.root.toBuffer()),
851
1012
  oracleInput: {
852
- feeAssetPriceModifier: 0n
1013
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier
853
1014
  }
854
1015
  },
855
1016
  attestationsAndSigners.getPackedAttestations(),
@@ -884,6 +1045,28 @@ export class SequencerPublisher {
884
1045
  this.log.warn(`Skipping vote cast for payload with empty code`);
885
1046
  return false;
886
1047
  }
1048
+ // Check if payload was already submitted to governance
1049
+ const cacheKey = payload.toString();
1050
+ if (!this.payloadProposedCache.has(cacheKey)) {
1051
+ try {
1052
+ const l1StartBlock = await this.rollupContract.getL1StartBlock();
1053
+ const proposed = await retry(()=>base.hasPayloadBeenProposed(payload.toString(), l1StartBlock), 'Check if payload was proposed', makeBackoff([
1054
+ 0,
1055
+ 1,
1056
+ 2
1057
+ ]), this.log, true);
1058
+ if (proposed) {
1059
+ this.payloadProposedCache.add(cacheKey);
1060
+ }
1061
+ } catch (err) {
1062
+ this.log.warn(`Failed to check if payload ${payload} was proposed after retries, skipping signal`, err);
1063
+ return false;
1064
+ }
1065
+ }
1066
+ if (this.payloadProposedCache.has(cacheKey)) {
1067
+ this.log.info(`Payload ${payload} was already proposed to governance, stopping signals`);
1068
+ return false;
1069
+ }
887
1070
  const cachedLastVote = this.lastActions[signalType];
888
1071
  this.lastActions[signalType] = slotNumber;
889
1072
  const action = signalType;
@@ -894,15 +1077,41 @@ export class SequencerPublisher {
894
1077
  signer: this.l1TxUtils.client.account?.address,
895
1078
  lastValidL2Slot: slotNumber
896
1079
  });
1080
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
897
1081
  try {
898
1082
  await this.l1TxUtils.simulate(request, {
899
1083
  time: timestamp
900
- }, [], ErrorsAbi);
1084
+ }, [], mergeAbis([
1085
+ request.abi ?? [],
1086
+ ErrorsAbi
1087
+ ]));
901
1088
  this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, {
902
1089
  request
903
1090
  });
904
1091
  } catch (err) {
905
- this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, err);
1092
+ const viemError = formatViemError(err);
1093
+ this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, viemError);
1094
+ this.backupFailedTx({
1095
+ id: keccak256(request.data),
1096
+ failureType: 'simulation',
1097
+ request: {
1098
+ to: request.to,
1099
+ data: request.data,
1100
+ value: request.value?.toString()
1101
+ },
1102
+ l1BlockNumber: l1BlockNumber.toString(),
1103
+ error: {
1104
+ message: viemError.message,
1105
+ name: viemError.name
1106
+ },
1107
+ context: {
1108
+ actions: [
1109
+ action
1110
+ ],
1111
+ slot: slotNumber,
1112
+ sender: this.getSenderAddress().toString()
1113
+ }
1114
+ });
906
1115
  // Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
907
1116
  }
908
1117
  // TODO(palla/slash): All votes (governance and slashing) should txTimeoutAt at the end of the slot.
@@ -1041,13 +1250,14 @@ export class SequencerPublisher {
1041
1250
  /** Simulates and enqueues a proposal for a checkpoint on L1 */ async enqueueProposeCheckpoint(checkpoint, attestationsAndSigners, attestationsAndSignersSignature, opts = {}) {
1042
1251
  const checkpointHeader = checkpoint.header;
1043
1252
  const blobFields = checkpoint.toBlobFields();
1044
- const blobs = getBlobsPerL1Block(blobFields);
1253
+ const blobs = await getBlobsPerL1Block(blobFields);
1045
1254
  const proposeTxArgs = {
1046
1255
  header: checkpointHeader,
1047
1256
  archive: checkpoint.archive.root.toBuffer(),
1048
1257
  blobs,
1049
1258
  attestationsAndSigners,
1050
- attestationsAndSignersSignature
1259
+ attestationsAndSignersSignature,
1260
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier
1051
1261
  };
1052
1262
  let ts;
1053
1263
  try {
@@ -1123,28 +1333,60 @@ export class SequencerPublisher {
1123
1333
  const cachedLastActionSlot = this.lastActions[action];
1124
1334
  this.lastActions[action] = slotNumber;
1125
1335
  this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
1336
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1126
1337
  let gasUsed;
1338
+ const simulateAbi = mergeAbis([
1339
+ request.abi ?? [],
1340
+ ErrorsAbi
1341
+ ]);
1127
1342
  try {
1128
1343
  ({ gasUsed } = await this.l1TxUtils.simulate(request, {
1129
1344
  time: timestamp
1130
- }, [], ErrorsAbi)); // TODO(palla/slash): Check the timestamp logic
1345
+ }, [], simulateAbi)); // TODO(palla/slash): Check the timestamp logic
1131
1346
  this.log.verbose(`Simulation for ${action} succeeded`, {
1132
1347
  ...logData,
1133
1348
  request,
1134
1349
  gasUsed
1135
1350
  });
1136
1351
  } catch (err) {
1137
- const viemError = formatViemError(err);
1352
+ const viemError = formatViemError(err, simulateAbi);
1138
1353
  this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
1354
+ this.backupFailedTx({
1355
+ id: keccak256(request.data),
1356
+ failureType: 'simulation',
1357
+ request: {
1358
+ to: request.to,
1359
+ data: request.data,
1360
+ value: request.value?.toString()
1361
+ },
1362
+ l1BlockNumber: l1BlockNumber.toString(),
1363
+ error: {
1364
+ message: viemError.message,
1365
+ name: viemError.name
1366
+ },
1367
+ context: {
1368
+ actions: [
1369
+ action
1370
+ ],
1371
+ slot: slotNumber,
1372
+ sender: this.getSenderAddress().toString()
1373
+ }
1374
+ });
1139
1375
  return false;
1140
1376
  }
1141
1377
  // We issued the simulation against the rollup contract, so we need to account for the overhead of the multicall3
1142
1378
  const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil(Number(gasUsed) * 64 / 63)));
1143
1379
  logData.gasLimit = gasLimit;
1380
+ // Store the ABI used for simulation on the request so Multicall3.forward can decode errors
1381
+ // when the tx is sent and a revert is diagnosed via simulation.
1382
+ const requestWithAbi = {
1383
+ ...request,
1384
+ abi: simulateAbi
1385
+ };
1144
1386
  this.log.debug(`Enqueuing ${action}`, logData);
1145
1387
  this.addRequest({
1146
1388
  action,
1147
- request,
1389
+ request: requestWithAbi,
1148
1390
  gasConfig: {
1149
1391
  gasLimit
1150
1392
  },
@@ -1208,10 +1450,38 @@ export class SequencerPublisher {
1208
1450
  }, {}, {
1209
1451
  blobs: encodedData.blobs.map((b)=>b.data),
1210
1452
  kzg
1211
- }).catch((err)=>{
1212
- const { message, metaMessages } = formatViemError(err);
1213
- this.log.error(`Failed to validate blobs`, message, {
1214
- metaMessages
1453
+ }).catch(async (err)=>{
1454
+ const viemError = formatViemError(err);
1455
+ this.log.error(`Failed to validate blobs`, viemError.message, {
1456
+ metaMessages: viemError.metaMessages
1457
+ });
1458
+ const validateBlobsData = encodeFunctionData({
1459
+ abi: RollupAbi,
1460
+ functionName: 'validateBlobs',
1461
+ args: [
1462
+ blobInput
1463
+ ]
1464
+ });
1465
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1466
+ this.backupFailedTx({
1467
+ id: keccak256(validateBlobsData),
1468
+ failureType: 'simulation',
1469
+ request: {
1470
+ to: this.rollupContract.address,
1471
+ data: validateBlobsData
1472
+ },
1473
+ blobData: encodedData.blobs.map((b)=>toHex(b.data)),
1474
+ l1BlockNumber: l1BlockNumber.toString(),
1475
+ error: {
1476
+ message: viemError.message,
1477
+ name: viemError.name
1478
+ },
1479
+ context: {
1480
+ actions: [
1481
+ 'validate-blobs'
1482
+ ],
1483
+ sender: this.getSenderAddress().toString()
1484
+ }
1215
1485
  });
1216
1486
  throw new Error('Failed to validate blobs');
1217
1487
  });
@@ -1222,8 +1492,7 @@ export class SequencerPublisher {
1222
1492
  header: encodedData.header.toViem(),
1223
1493
  archive: toHex(encodedData.archive),
1224
1494
  oracleInput: {
1225
- // We are currently not modifying these. See #9963
1226
- feeAssetPriceModifier: 0n
1495
+ feeAssetPriceModifier: encodedData.feeAssetPriceModifier
1227
1496
  }
1228
1497
  },
1229
1498
  encodedData.attestationsAndSigners.getPackedAttestations(),
@@ -1272,10 +1541,11 @@ export class SequencerPublisher {
1272
1541
  balance: 10n * WEI_CONST * WEI_CONST
1273
1542
  });
1274
1543
  }
1544
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1275
1545
  const simulationResult = await this.l1TxUtils.simulate({
1276
1546
  to: this.rollupContract.address,
1277
1547
  data: rollupData,
1278
- gas: SequencerPublisher.PROPOSE_GAS_GUESS,
1548
+ gas: MAX_L1_TX_LIMIT,
1279
1549
  ...this.proposerAddressForSimulation && {
1280
1550
  from: this.proposerAddressForSimulation.toString()
1281
1551
  }
@@ -1283,10 +1553,10 @@ export class SequencerPublisher {
1283
1553
  // @note we add 1n to the timestamp because geth implementation doesn't like simulation timestamp to be equal to the current block timestamp
1284
1554
  time: timestamp + 1n,
1285
1555
  // @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
1556
+ gasLimit: MAX_L1_TX_LIMIT * 2n
1287
1557
  }, stateOverrides, RollupAbi, {
1288
1558
  // @note fallback gas estimate to use if the node doesn't support simulation API
1289
- fallbackGasEstimate: SequencerPublisher.PROPOSE_GAS_GUESS
1559
+ fallbackGasEstimate: MAX_L1_TX_LIMIT
1290
1560
  }).catch((err)=>{
1291
1561
  // In fisherman mode, we expect ValidatorSelection__MissingProposerSignature since fisherman doesn't have proposer signature
1292
1562
  const viemError = formatViemError(err);
@@ -1294,11 +1564,31 @@ export class SequencerPublisher {
1294
1564
  this.log.debug(`Ignoring expected ValidatorSelection__MissingProposerSignature error in fisherman mode`);
1295
1565
  // Return a minimal simulation result with the fallback gas estimate
1296
1566
  return {
1297
- gasUsed: SequencerPublisher.PROPOSE_GAS_GUESS,
1567
+ gasUsed: MAX_L1_TX_LIMIT,
1298
1568
  logs: []
1299
1569
  };
1300
1570
  }
1301
1571
  this.log.error(`Failed to simulate propose tx`, viemError);
1572
+ this.backupFailedTx({
1573
+ id: keccak256(rollupData),
1574
+ failureType: 'simulation',
1575
+ request: {
1576
+ to: this.rollupContract.address,
1577
+ data: rollupData
1578
+ },
1579
+ l1BlockNumber: l1BlockNumber.toString(),
1580
+ error: {
1581
+ message: viemError.message,
1582
+ name: viemError.name
1583
+ },
1584
+ context: {
1585
+ actions: [
1586
+ 'propose'
1587
+ ],
1588
+ slot: Number(args[0].header.slotNumber),
1589
+ sender: this.getSenderAddress().toString()
1590
+ }
1591
+ });
1302
1592
  throw err;
1303
1593
  });
1304
1594
  return {