@aztec/sequencer-client 0.0.1-commit.1142ef1 → 0.0.1-commit.125b3452

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