@aztec/sequencer-client 0.0.1-commit.9d2bcf6d → 0.0.1-commit.9ef841308

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 (84) hide show
  1. package/dest/client/sequencer-client.d.ts +15 -7
  2. package/dest/client/sequencer-client.d.ts.map +1 -1
  3. package/dest/client/sequencer-client.js +60 -26
  4. package/dest/config.d.ts +26 -7
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +47 -28
  7. package/dest/global_variable_builder/global_builder.d.ts +14 -10
  8. package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
  9. package/dest/global_variable_builder/global_builder.js +22 -21
  10. package/dest/global_variable_builder/index.d.ts +2 -2
  11. package/dest/global_variable_builder/index.d.ts.map +1 -1
  12. package/dest/publisher/config.d.ts +47 -17
  13. package/dest/publisher/config.d.ts.map +1 -1
  14. package/dest/publisher/config.js +121 -42
  15. package/dest/publisher/index.d.ts +2 -1
  16. package/dest/publisher/index.d.ts.map +1 -1
  17. package/dest/publisher/l1_tx_failed_store/factory.d.ts +11 -0
  18. package/dest/publisher/l1_tx_failed_store/factory.d.ts.map +1 -0
  19. package/dest/publisher/l1_tx_failed_store/factory.js +22 -0
  20. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts +59 -0
  21. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -0
  22. package/dest/publisher/l1_tx_failed_store/failed_tx_store.js +1 -0
  23. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts +15 -0
  24. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts.map +1 -0
  25. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.js +34 -0
  26. package/dest/publisher/l1_tx_failed_store/index.d.ts +4 -0
  27. package/dest/publisher/l1_tx_failed_store/index.d.ts.map +1 -0
  28. package/dest/publisher/l1_tx_failed_store/index.js +2 -0
  29. package/dest/publisher/sequencer-publisher-factory.d.ts +11 -3
  30. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  31. package/dest/publisher/sequencer-publisher-factory.js +27 -2
  32. package/dest/publisher/sequencer-publisher.d.ts +39 -13
  33. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  34. package/dest/publisher/sequencer-publisher.js +364 -66
  35. package/dest/sequencer/checkpoint_proposal_job.d.ts +15 -7
  36. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  37. package/dest/sequencer/checkpoint_proposal_job.js +241 -140
  38. package/dest/sequencer/checkpoint_voter.d.ts +1 -2
  39. package/dest/sequencer/checkpoint_voter.d.ts.map +1 -1
  40. package/dest/sequencer/checkpoint_voter.js +2 -5
  41. package/dest/sequencer/events.d.ts +2 -1
  42. package/dest/sequencer/events.d.ts.map +1 -1
  43. package/dest/sequencer/metrics.d.ts +21 -5
  44. package/dest/sequencer/metrics.d.ts.map +1 -1
  45. package/dest/sequencer/metrics.js +97 -15
  46. package/dest/sequencer/sequencer.d.ts +28 -15
  47. package/dest/sequencer/sequencer.d.ts.map +1 -1
  48. package/dest/sequencer/sequencer.js +93 -84
  49. package/dest/sequencer/timetable.d.ts +4 -6
  50. package/dest/sequencer/timetable.d.ts.map +1 -1
  51. package/dest/sequencer/timetable.js +7 -11
  52. package/dest/sequencer/types.d.ts +2 -2
  53. package/dest/sequencer/types.d.ts.map +1 -1
  54. package/dest/test/index.d.ts +3 -5
  55. package/dest/test/index.d.ts.map +1 -1
  56. package/dest/test/mock_checkpoint_builder.d.ts +11 -11
  57. package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
  58. package/dest/test/mock_checkpoint_builder.js +45 -34
  59. package/dest/test/utils.d.ts +3 -3
  60. package/dest/test/utils.d.ts.map +1 -1
  61. package/dest/test/utils.js +5 -4
  62. package/package.json +27 -28
  63. package/src/client/sequencer-client.ts +76 -23
  64. package/src/config.ts +65 -38
  65. package/src/global_variable_builder/global_builder.ts +23 -24
  66. package/src/global_variable_builder/index.ts +1 -1
  67. package/src/publisher/config.ts +153 -43
  68. package/src/publisher/index.ts +3 -0
  69. package/src/publisher/l1_tx_failed_store/factory.ts +32 -0
  70. package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +55 -0
  71. package/src/publisher/l1_tx_failed_store/file_store_failed_tx_store.ts +46 -0
  72. package/src/publisher/l1_tx_failed_store/index.ts +3 -0
  73. package/src/publisher/sequencer-publisher-factory.ts +38 -6
  74. package/src/publisher/sequencer-publisher.ts +359 -86
  75. package/src/sequencer/checkpoint_proposal_job.ts +328 -151
  76. package/src/sequencer/checkpoint_voter.ts +1 -12
  77. package/src/sequencer/events.ts +1 -1
  78. package/src/sequencer/metrics.ts +106 -18
  79. package/src/sequencer/sequencer.ts +127 -96
  80. package/src/sequencer/timetable.ts +13 -12
  81. package/src/sequencer/types.ts +1 -1
  82. package/src/test/index.ts +2 -4
  83. package/src/test/mock_checkpoint_builder.ts +63 -49
  84. package/src/test/utils.ts +5 -2
@@ -372,24 +372,29 @@ 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
377
  import { MAX_L1_TX_LIMIT, WEI_CONST } from '@aztec/ethereum/l1-tx-utils';
378
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';
394
+ import { getLastL1SlotTimestampForL2Slot, getNextL1SlotTimestamp } from '@aztec/stdlib/epoch-helpers';
391
395
  import { getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
392
- import { encodeFunctionData, toHex } from 'viem';
396
+ import { encodeFunctionData, keccak256, multicall3Abi, toHex } from 'viem';
397
+ import { createL1TxFailedStore } from './l1_tx_failed_store/index.js';
393
398
  import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
394
399
  export const Actions = [
395
400
  'invalidate-by-invalid-attestation',
@@ -429,15 +434,21 @@ export class SequencerPublisher {
429
434
  interrupted;
430
435
  metrics;
431
436
  epochCache;
437
+ failedTxStore;
432
438
  governanceLog;
433
439
  slashingLog;
434
440
  lastActions;
435
441
  isPayloadEmptyCache;
442
+ payloadProposedCache;
436
443
  log;
437
444
  ethereumSlotDuration;
445
+ aztecSlotDuration;
446
+ dateProvider;
438
447
  blobClient;
439
448
  /** Address to use for simulations in fisherman mode (actual proposer's address) */ proposerAddressForSimulation;
449
+ /** Optional callback to obtain a replacement publisher when the current one fails to send. */ getNextPublisher;
440
450
  /** L1 fee analyzer for fisherman mode */ l1FeeAnalyzer;
451
+ /** Fee asset price oracle for computing price modifiers from Uniswap V4 */ feeAssetPriceOracle;
441
452
  // A CALL to a cold address is 2700 gas
442
453
  static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
443
454
  // Gas report for VotingWithSigTest shows a max gas of 100k, but we've seen it cost 700k+ in testnet
@@ -456,9 +467,12 @@ export class SequencerPublisher {
456
467
  this.slashingLog = createLogger('sequencer:publisher:slashing');
457
468
  this.lastActions = {};
458
469
  this.isPayloadEmptyCache = new Map();
470
+ this.payloadProposedCache = new Set();
459
471
  this.requests = [];
460
472
  this.log = deps.log ?? createLogger('sequencer:publisher');
461
473
  this.ethereumSlotDuration = BigInt(config.ethereumSlotDuration);
474
+ this.aztecSlotDuration = BigInt(config.aztecSlotDuration);
475
+ this.dateProvider = deps.dateProvider;
462
476
  this.epochCache = deps.epochCache;
463
477
  this.lastActions = deps.lastActions;
464
478
  this.blobClient = deps.blobClient;
@@ -466,6 +480,7 @@ export class SequencerPublisher {
466
480
  this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
467
481
  this.tracer = telemetry.getTracer('SequencerPublisher');
468
482
  this.l1TxUtils = deps.l1TxUtils;
483
+ this.getNextPublisher = deps.getNextPublisher;
469
484
  this.rollupContract = deps.rollupContract;
470
485
  this.govProposerContract = deps.governanceProposerContract;
471
486
  this.slashingProposerContract = deps.slashingProposerContract;
@@ -479,10 +494,36 @@ export class SequencerPublisher {
479
494
  if (config.fishermanMode) {
480
495
  this.l1FeeAnalyzer = new L1FeeAnalyzer(this.l1TxUtils.client, deps.dateProvider, createLogger('sequencer:publisher:fee-analyzer'));
481
496
  }
497
+ // Initialize fee asset price oracle
498
+ this.feeAssetPriceOracle = new FeeAssetPriceOracle(this.l1TxUtils.client, this.rollupContract, createLogger('sequencer:publisher:price-oracle'));
499
+ // Initialize failed L1 tx store (optional, for test networks)
500
+ this.failedTxStore = createL1TxFailedStore(config.l1TxFailedStore, this.log);
501
+ }
502
+ /**
503
+ * Backs up a failed L1 transaction to the configured store for debugging.
504
+ * Does nothing if no store is configured.
505
+ */ backupFailedTx(failedTx) {
506
+ if (!this.failedTxStore) {
507
+ return;
508
+ }
509
+ const tx = {
510
+ ...failedTx,
511
+ timestamp: Date.now()
512
+ };
513
+ // Fire and forget - don't block on backup
514
+ void this.failedTxStore.then((store)=>store?.saveFailedTx(tx)).catch((err)=>{
515
+ this.log.warn(`Failed to backup failed L1 tx to store`, err);
516
+ });
482
517
  }
483
518
  getRollupContract() {
484
519
  return this.rollupContract;
485
520
  }
521
+ /**
522
+ * Gets the fee asset price modifier from the oracle.
523
+ * Returns 0n if the oracle query fails.
524
+ */ getFeeAssetPriceModifier() {
525
+ return this.feeAssetPriceOracle.computePriceModifier();
526
+ }
486
527
  getSenderAddress() {
487
528
  return this.l1TxUtils.getSenderAddress();
488
529
  }
@@ -501,7 +542,7 @@ export class SequencerPublisher {
501
542
  this.requests.push(request);
502
543
  }
503
544
  getCurrentL2Slot() {
504
- return this.epochCache.getEpochAndSlotNow().slot;
545
+ return this.epochCache.getSlotNow();
505
546
  }
506
547
  /**
507
548
  * Clears all pending requests without sending them.
@@ -590,8 +631,8 @@ export class SequencerPublisher {
590
631
  // @note - we can only have one blob config per bundle
591
632
  // find requests with gas and blob configs
592
633
  // See https://github.com/AztecProtocol/aztec-packages/issues/11513
593
- const gasConfigs = requestsToProcess.filter((request)=>request.gasConfig).map((request)=>request.gasConfig);
594
- const blobConfigs = requestsToProcess.filter((request)=>request.blobConfig).map((request)=>request.blobConfig);
634
+ const gasConfigs = validRequests.filter((request)=>request.gasConfig).map((request)=>request.gasConfig);
635
+ const blobConfigs = validRequests.filter((request)=>request.blobConfig).map((request)=>request.blobConfig);
595
636
  if (blobConfigs.length > 1) {
596
637
  throw new Error('Multiple blob configs found');
597
638
  }
@@ -618,12 +659,34 @@ export class SequencerPublisher {
618
659
  // This ensures the committee gets precomputed correctly
619
660
  validRequests.sort((a, b)=>compareActions(a.action, b.action));
620
661
  try {
662
+ // Capture context for failed tx backup before sending
663
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
664
+ const multicallData = encodeFunctionData({
665
+ abi: multicall3Abi,
666
+ functionName: 'aggregate3',
667
+ args: [
668
+ validRequests.map((r)=>({
669
+ target: r.request.to,
670
+ callData: r.request.data,
671
+ allowFailure: true
672
+ }))
673
+ ]
674
+ });
675
+ const blobDataHex = blobConfig?.blobs?.map((b)=>toHex(b));
676
+ const txContext = {
677
+ multicallData,
678
+ blobData: blobDataHex,
679
+ l1BlockNumber
680
+ };
621
681
  this.log.debug('Forwarding transactions', {
622
682
  validRequests: validRequests.map((request)=>request.action),
623
683
  txConfig
624
684
  });
625
- const result = await Multicall3.forward(validRequests.map((request)=>request.request), this.l1TxUtils, txConfig, blobConfig, this.rollupContract.address, this.log);
626
- const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(validRequests, result);
685
+ const result = await this.forwardWithPublisherRotation(validRequests, txConfig, blobConfig);
686
+ if (result === undefined) {
687
+ return undefined;
688
+ }
689
+ const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(validRequests, result, txContext);
627
690
  return {
628
691
  result,
629
692
  expiredActions,
@@ -643,17 +706,83 @@ export class SequencerPublisher {
643
706
  }
644
707
  }
645
708
  }
646
- callbackBundledTransactions(requests, result) {
709
+ /**
710
+ * Forwards transactions via Multicall3, rotating to the next available publisher if a send
711
+ * failure occurs (i.e. the tx never reached the chain).
712
+ * On-chain reverts and simulation errors are returned as-is without rotation.
713
+ */ async forwardWithPublisherRotation(validRequests, txConfig, blobConfig) {
714
+ const triedAddresses = [];
715
+ let currentPublisher = this.l1TxUtils;
716
+ while(true){
717
+ triedAddresses.push(currentPublisher.getSenderAddress());
718
+ try {
719
+ const result = await Multicall3.forward(validRequests.map((r)=>r.request), currentPublisher, txConfig, blobConfig, this.rollupContract.address, this.log);
720
+ this.l1TxUtils = currentPublisher;
721
+ return result;
722
+ } catch (err) {
723
+ if (err instanceof TimeoutError) {
724
+ throw err;
725
+ }
726
+ const viemError = formatViemError(err);
727
+ if (!this.getNextPublisher) {
728
+ this.log.error('Failed to publish bundled transactions', viemError);
729
+ return undefined;
730
+ }
731
+ this.log.warn(`Publisher ${currentPublisher.getSenderAddress()} failed to send, rotating to next publisher`, viemError);
732
+ const nextPublisher = await this.getNextPublisher([
733
+ ...triedAddresses
734
+ ]);
735
+ if (!nextPublisher) {
736
+ this.log.error('All available publishers exhausted, failed to publish bundled transactions');
737
+ return undefined;
738
+ }
739
+ currentPublisher = nextPublisher;
740
+ }
741
+ }
742
+ }
743
+ callbackBundledTransactions(requests, result, txContext) {
647
744
  const actionsListStr = requests.map((r)=>r.action).join(', ');
648
745
  if (result instanceof FormattedViemError) {
649
746
  this.log.error(`Failed to publish bundled transactions (${actionsListStr})`, result);
747
+ this.backupFailedTx({
748
+ id: keccak256(txContext.multicallData),
749
+ failureType: 'send-error',
750
+ request: {
751
+ to: MULTI_CALL_3_ADDRESS,
752
+ data: txContext.multicallData
753
+ },
754
+ blobData: txContext.blobData,
755
+ l1BlockNumber: txContext.l1BlockNumber.toString(),
756
+ error: {
757
+ message: result.message,
758
+ name: result.name
759
+ },
760
+ context: {
761
+ actions: requests.map((r)=>r.action),
762
+ requests: requests.map((r)=>({
763
+ action: r.action,
764
+ to: r.request.to,
765
+ data: r.request.data
766
+ })),
767
+ sender: this.getSenderAddress().toString()
768
+ }
769
+ });
650
770
  return {
651
771
  failedActions: requests.map((r)=>r.action)
652
772
  };
653
773
  } else {
654
774
  this.log.verbose(`Published bundled transactions (${actionsListStr})`, {
655
775
  result,
656
- requests
776
+ requests: requests.map((r)=>({
777
+ ...r,
778
+ // Avoid logging large blob data
779
+ blobConfig: r.blobConfig ? {
780
+ ...r.blobConfig,
781
+ blobs: r.blobConfig.blobs.map((b)=>({
782
+ size: trimmedBytesLength(b)
783
+ }))
784
+ } : undefined
785
+ }))
657
786
  });
658
787
  const successfulActions = [];
659
788
  const failedActions = [];
@@ -664,6 +793,37 @@ export class SequencerPublisher {
664
793
  failedActions.push(request.action);
665
794
  }
666
795
  }
796
+ // Single backup for the whole reverted tx
797
+ if (failedActions.length > 0 && result?.receipt?.status === 'reverted') {
798
+ this.backupFailedTx({
799
+ id: result.receipt.transactionHash,
800
+ failureType: 'revert',
801
+ request: {
802
+ to: MULTI_CALL_3_ADDRESS,
803
+ data: txContext.multicallData
804
+ },
805
+ blobData: txContext.blobData,
806
+ l1BlockNumber: result.receipt.blockNumber.toString(),
807
+ receipt: {
808
+ transactionHash: result.receipt.transactionHash,
809
+ blockNumber: result.receipt.blockNumber.toString(),
810
+ gasUsed: (result.receipt.gasUsed ?? 0n).toString(),
811
+ status: 'reverted'
812
+ },
813
+ error: {
814
+ message: result.errorMsg ?? 'Transaction reverted'
815
+ },
816
+ context: {
817
+ actions: failedActions,
818
+ requests: requests.filter((r)=>failedActions.includes(r.action)).map((r)=>({
819
+ action: r.action,
820
+ to: r.request.to,
821
+ data: r.request.data
822
+ })),
823
+ sender: this.getSenderAddress().toString()
824
+ }
825
+ });
826
+ }
667
827
  return {
668
828
  successfulActions,
669
829
  failedActions
@@ -671,17 +831,20 @@ export class SequencerPublisher {
671
831
  }
672
832
  }
673
833
  /**
674
- * @notice Will call `canProposeAtNextEthBlock` to make sure that it is possible to propose
834
+ * @notice Will call `canProposeAt` to make sure that it is possible to propose
675
835
  * @param tipArchive - The archive to check
676
836
  * @returns The slot and block number if it is possible to propose, undefined otherwise
677
- */ canProposeAtNextEthBlock(tipArchive, msgSender, opts = {}) {
837
+ */ canProposeAt(tipArchive, msgSender, opts = {}) {
678
838
  // TODO: #14291 - should loop through multiple keys to check if any of them can propose
679
839
  const ignoredErrors = [
680
840
  'SlotAlreadyInChain',
681
841
  'InvalidProposer',
682
842
  'InvalidArchive'
683
843
  ];
684
- return this.rollupContract.canProposeAtNextEthBlock(tipArchive.toBuffer(), msgSender.toString(), Number(this.ethereumSlotDuration), {
844
+ const pipelined = opts.pipelined ?? this.epochCache.isProposerPipeliningEnabled();
845
+ const slotOffset = pipelined ? this.aztecSlotDuration : 0n;
846
+ const nextL1SlotTs = this.getNextL1SlotTimestamp() + slotOffset;
847
+ return this.rollupContract.canProposeAt(tipArchive.toBuffer(), msgSender.toString(), nextL1SlotTs, {
685
848
  forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber
686
849
  }).catch((err)=>{
687
850
  if (err instanceof FormattedViemError && ignoredErrors.find((e)=>err.message.includes(e))) {
@@ -713,7 +876,7 @@ export class SequencerPublisher {
713
876
  header.blobsHash.toString(),
714
877
  flags
715
878
  ];
716
- const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
879
+ const ts = this.getSimulationTimestamp(header.slotNumber);
717
880
  const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(opts?.forcePendingCheckpointNumber);
718
881
  let balance = 0n;
719
882
  if (this.config.fishermanMode) {
@@ -736,7 +899,7 @@ export class SequencerPublisher {
736
899
  }),
737
900
  from: MULTI_CALL_3_ADDRESS
738
901
  }, {
739
- time: ts + 1n
902
+ time: ts
740
903
  }, stateOverrides);
741
904
  this.log.debug(`Simulated validateHeader`);
742
905
  }
@@ -766,6 +929,7 @@ export class SequencerPublisher {
766
929
  ...logData,
767
930
  request
768
931
  });
932
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
769
933
  try {
770
934
  const { gasUsed } = await this.l1TxUtils.simulate(request, undefined, undefined, mergeAbis([
771
935
  request.abi ?? [],
@@ -808,6 +972,27 @@ export class SequencerPublisher {
808
972
  }
809
973
  // Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
810
974
  this.log.error(`Simulation for invalidate checkpoint ${checkpointNumber} failed`, viemError, logData);
975
+ this.backupFailedTx({
976
+ id: keccak256(request.data),
977
+ failureType: 'simulation',
978
+ request: {
979
+ to: request.to,
980
+ data: request.data,
981
+ value: request.value?.toString()
982
+ },
983
+ l1BlockNumber: l1BlockNumber.toString(),
984
+ error: {
985
+ message: viemError.message,
986
+ name: viemError.name
987
+ },
988
+ context: {
989
+ actions: [
990
+ `invalidate-${reason}`
991
+ ],
992
+ checkpointNumber,
993
+ sender: this.getSenderAddress().toString()
994
+ }
995
+ });
811
996
  throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, {
812
997
  cause: viemError
813
998
  });
@@ -834,30 +1019,15 @@ export class SequencerPublisher {
834
1019
  }
835
1020
  }
836
1021
  /** Simulates `propose` to make sure that the checkpoint is valid for submission */ async validateCheckpointForSubmission(checkpoint, attestationsAndSigners, attestationsAndSignersSignature, options) {
837
- const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
838
- // TODO(palla/mbps): This should not be needed, there's no flow where we propose with zero attestations. Or is there?
839
- // If we have no attestations, we still need to provide the empty attestations
840
- // so that the committee is recalculated correctly
841
- // const ignoreSignatures = attestationsAndSigners.attestations.length === 0;
842
- // if (ignoreSignatures) {
843
- // const { committee } = await this.epochCache.getCommittee(block.header.globalVariables.slotNumber);
844
- // if (!committee) {
845
- // this.log.warn(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
846
- // throw new Error(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
847
- // }
848
- // attestationsAndSigners.attestations = committee.map(committeeMember =>
849
- // CommitteeAttestation.fromAddress(committeeMember),
850
- // );
851
- // }
852
1022
  const blobFields = checkpoint.toBlobFields();
853
- const blobs = getBlobsPerL1Block(blobFields);
1023
+ const blobs = await getBlobsPerL1Block(blobFields);
854
1024
  const blobInput = getPrefixedEthBlobCommitments(blobs);
855
1025
  const args = [
856
1026
  {
857
1027
  header: checkpoint.header.toViem(),
858
1028
  archive: toHex(checkpoint.archive.root.toBuffer()),
859
1029
  oracleInput: {
860
- feeAssetPriceModifier: 0n
1030
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier
861
1031
  }
862
1032
  },
863
1033
  attestationsAndSigners.getPackedAttestations(),
@@ -865,10 +1035,9 @@ export class SequencerPublisher {
865
1035
  attestationsAndSignersSignature.toViemSignature(),
866
1036
  blobInput
867
1037
  ];
868
- await this.simulateProposeTx(args, ts, options);
869
- return ts;
1038
+ await this.simulateProposeTx(args, options);
870
1039
  }
871
- async enqueueCastSignalHelper(slotNumber, timestamp, signalType, payload, base, signerAddress, signer) {
1040
+ async enqueueCastSignalHelper(slotNumber, signalType, payload, base, signerAddress, signer) {
872
1041
  if (this.lastActions[signalType] && this.lastActions[signalType] === slotNumber) {
873
1042
  this.log.debug(`Skipping duplicate vote cast signal ${signalType} for slot ${slotNumber}`);
874
1043
  return false;
@@ -892,6 +1061,28 @@ export class SequencerPublisher {
892
1061
  this.log.warn(`Skipping vote cast for payload with empty code`);
893
1062
  return false;
894
1063
  }
1064
+ // Check if payload was already submitted to governance
1065
+ const cacheKey = payload.toString();
1066
+ if (!this.payloadProposedCache.has(cacheKey)) {
1067
+ try {
1068
+ const l1StartBlock = await this.rollupContract.getL1StartBlock();
1069
+ const proposed = await retry(()=>base.hasPayloadBeenProposed(payload.toString(), l1StartBlock), 'Check if payload was proposed', makeBackoff([
1070
+ 0,
1071
+ 1,
1072
+ 2
1073
+ ]), this.log, true);
1074
+ if (proposed) {
1075
+ this.payloadProposedCache.add(cacheKey);
1076
+ }
1077
+ } catch (err) {
1078
+ this.log.warn(`Failed to check if payload ${payload} was proposed after retries, skipping signal`, err);
1079
+ return false;
1080
+ }
1081
+ }
1082
+ if (this.payloadProposedCache.has(cacheKey)) {
1083
+ this.log.info(`Payload ${payload} was already proposed to governance, stopping signals`);
1084
+ return false;
1085
+ }
895
1086
  const cachedLastVote = this.lastActions[signalType];
896
1087
  this.lastActions[signalType] = slotNumber;
897
1088
  const action = signalType;
@@ -902,6 +1093,8 @@ export class SequencerPublisher {
902
1093
  signer: this.l1TxUtils.client.account?.address,
903
1094
  lastValidL2Slot: slotNumber
904
1095
  });
1096
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1097
+ const timestamp = this.getSimulationTimestamp(slotNumber);
905
1098
  try {
906
1099
  await this.l1TxUtils.simulate(request, {
907
1100
  time: timestamp
@@ -913,7 +1106,32 @@ export class SequencerPublisher {
913
1106
  request
914
1107
  });
915
1108
  } catch (err) {
916
- this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, err);
1109
+ const viemError = formatViemError(err);
1110
+ this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, viemError, {
1111
+ simulationTimestamp: timestamp,
1112
+ l1BlockNumber
1113
+ });
1114
+ this.backupFailedTx({
1115
+ id: keccak256(request.data),
1116
+ failureType: 'simulation',
1117
+ request: {
1118
+ to: request.to,
1119
+ data: request.data,
1120
+ value: request.value?.toString()
1121
+ },
1122
+ l1BlockNumber: l1BlockNumber.toString(),
1123
+ error: {
1124
+ message: viemError.message,
1125
+ name: viemError.name
1126
+ },
1127
+ context: {
1128
+ actions: [
1129
+ action
1130
+ ],
1131
+ slot: slotNumber,
1132
+ sender: this.getSenderAddress().toString()
1133
+ }
1134
+ });
917
1135
  // Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
918
1136
  }
919
1137
  // TODO(palla/slash): All votes (governance and slashing) should txTimeoutAt at the end of the slot.
@@ -957,12 +1175,11 @@ export class SequencerPublisher {
957
1175
  /**
958
1176
  * Enqueues a governance castSignal transaction to cast a signal for a given slot number.
959
1177
  * @param slotNumber - The slot number to cast a signal for.
960
- * @param timestamp - The timestamp of the slot to cast a signal for.
961
1178
  * @returns True if the signal was successfully enqueued, false otherwise.
962
- */ enqueueGovernanceCastSignal(governancePayload, slotNumber, timestamp, signerAddress, signer) {
963
- return this.enqueueCastSignalHelper(slotNumber, timestamp, 'governance-signal', governancePayload, this.govProposerContract, signerAddress, signer);
1179
+ */ enqueueGovernanceCastSignal(governancePayload, slotNumber, signerAddress, signer) {
1180
+ return this.enqueueCastSignalHelper(slotNumber, 'governance-signal', governancePayload, this.govProposerContract, signerAddress, signer);
964
1181
  }
965
- /** Enqueues all slashing actions as returned by the slasher client. */ async enqueueSlashingActions(actions, slotNumber, timestamp, signerAddress, signer) {
1182
+ /** Enqueues all slashing actions as returned by the slasher client. */ async enqueueSlashingActions(actions, slotNumber, signerAddress, signer) {
966
1183
  if (actions.length === 0) {
967
1184
  this.log.debug(`No slashing actions to enqueue for slot ${slotNumber}`);
968
1185
  return false;
@@ -978,7 +1195,7 @@ export class SequencerPublisher {
978
1195
  this.log.debug(`Enqueuing slashing vote for payload ${action.payload} at slot ${slotNumber}`, {
979
1196
  signerAddress
980
1197
  });
981
- await this.enqueueCastSignalHelper(slotNumber, timestamp, 'empire-slashing-signal', action.payload, this.slashingProposerContract, signerAddress, signer);
1198
+ await this.enqueueCastSignalHelper(slotNumber, 'empire-slashing-signal', action.payload, this.slashingProposerContract, signerAddress, signer);
982
1199
  break;
983
1200
  }
984
1201
  case 'create-empire-payload':
@@ -988,7 +1205,7 @@ export class SequencerPublisher {
988
1205
  signerAddress
989
1206
  });
990
1207
  const request = this.slashFactoryContract.buildCreatePayloadRequest(action.data);
991
- await this.simulateAndEnqueueRequest('create-empire-payload', request, (receipt)=>!!this.slashFactoryContract.tryExtractSlashPayloadCreatedEvent(receipt.logs), slotNumber, timestamp);
1208
+ await this.simulateAndEnqueueRequest('create-empire-payload', request, (receipt)=>!!this.slashFactoryContract.tryExtractSlashPayloadCreatedEvent(receipt.logs), slotNumber);
992
1209
  break;
993
1210
  }
994
1211
  case 'execute-empire-payload':
@@ -1003,7 +1220,7 @@ export class SequencerPublisher {
1003
1220
  }
1004
1221
  const empireSlashingProposer = this.slashingProposerContract;
1005
1222
  const request = empireSlashingProposer.buildExecuteRoundRequest(action.round);
1006
- await this.simulateAndEnqueueRequest('execute-empire-payload', request, (receipt)=>!!empireSlashingProposer.tryExtractPayloadSubmittedEvent(receipt.logs), slotNumber, timestamp);
1223
+ await this.simulateAndEnqueueRequest('execute-empire-payload', request, (receipt)=>!!empireSlashingProposer.tryExtractPayloadSubmittedEvent(receipt.logs), slotNumber);
1007
1224
  break;
1008
1225
  }
1009
1226
  case 'vote-offenses':
@@ -1021,7 +1238,7 @@ export class SequencerPublisher {
1021
1238
  const tallySlashingProposer = this.slashingProposerContract;
1022
1239
  const votes = bufferToHex(encodeSlashConsensusVotes(action.votes));
1023
1240
  const request = await tallySlashingProposer.buildVoteRequestFromSigner(votes, slotNumber, signer);
1024
- await this.simulateAndEnqueueRequest('vote-offenses', request, (receipt)=>!!tallySlashingProposer.tryExtractVoteCastEvent(receipt.logs), slotNumber, timestamp);
1241
+ await this.simulateAndEnqueueRequest('vote-offenses', request, (receipt)=>!!tallySlashingProposer.tryExtractVoteCastEvent(receipt.logs), slotNumber);
1025
1242
  break;
1026
1243
  }
1027
1244
  case 'execute-slash':
@@ -1037,7 +1254,7 @@ export class SequencerPublisher {
1037
1254
  }
1038
1255
  const tallySlashingProposer = this.slashingProposerContract;
1039
1256
  const request = tallySlashingProposer.buildExecuteRoundRequest(action.round, action.committees);
1040
- await this.simulateAndEnqueueRequest('execute-slash', request, (receipt)=>!!tallySlashingProposer.tryExtractRoundExecutedEvent(receipt.logs), slotNumber, timestamp);
1257
+ await this.simulateAndEnqueueRequest('execute-slash', request, (receipt)=>!!tallySlashingProposer.tryExtractRoundExecutedEvent(receipt.logs), slotNumber);
1041
1258
  break;
1042
1259
  }
1043
1260
  default:
@@ -1052,22 +1269,22 @@ export class SequencerPublisher {
1052
1269
  /** Simulates and enqueues a proposal for a checkpoint on L1 */ async enqueueProposeCheckpoint(checkpoint, attestationsAndSigners, attestationsAndSignersSignature, opts = {}) {
1053
1270
  const checkpointHeader = checkpoint.header;
1054
1271
  const blobFields = checkpoint.toBlobFields();
1055
- const blobs = getBlobsPerL1Block(blobFields);
1272
+ const blobs = await getBlobsPerL1Block(blobFields);
1056
1273
  const proposeTxArgs = {
1057
1274
  header: checkpointHeader,
1058
1275
  archive: checkpoint.archive.root.toBuffer(),
1059
1276
  blobs,
1060
1277
  attestationsAndSigners,
1061
- attestationsAndSignersSignature
1278
+ attestationsAndSignersSignature,
1279
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier
1062
1280
  };
1063
- let ts;
1064
1281
  try {
1065
1282
  // @note This will make sure that we are passing the checks for our header ASSUMING that the data is also made available
1066
1283
  // This means that we can avoid the simulation issues in later checks.
1067
1284
  // By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which
1068
1285
  // make time consistency checks break.
1069
1286
  // TODO(palla): Check whether we're validating twice, once here and once within addProposeTx, since we call simulateProposeTx in both places.
1070
- ts = await this.validateCheckpointForSubmission(checkpoint, attestationsAndSigners, attestationsAndSignersSignature, opts);
1287
+ await this.validateCheckpointForSubmission(checkpoint, attestationsAndSigners, attestationsAndSignersSignature, opts);
1071
1288
  } catch (err) {
1072
1289
  this.log.error(`Checkpoint validation failed. ${err instanceof Error ? err.message : 'No error message'}`, err, {
1073
1290
  ...checkpoint.getStats(),
@@ -1080,7 +1297,7 @@ export class SequencerPublisher {
1080
1297
  ...checkpoint.toCheckpointInfo(),
1081
1298
  ...opts
1082
1299
  });
1083
- await this.addProposeTx(checkpoint, proposeTxArgs, opts, ts);
1300
+ await this.addProposeTx(checkpoint, proposeTxArgs, opts);
1084
1301
  }
1085
1302
  enqueueInvalidateCheckpoint(request, opts = {}) {
1086
1303
  if (!request) {
@@ -1121,7 +1338,8 @@ export class SequencerPublisher {
1121
1338
  }
1122
1339
  });
1123
1340
  }
1124
- async simulateAndEnqueueRequest(action, request, checkSuccess, slotNumber, timestamp) {
1341
+ async simulateAndEnqueueRequest(action, request, checkSuccess, slotNumber) {
1342
+ const timestamp = this.getSimulationTimestamp(slotNumber);
1125
1343
  const logData = {
1126
1344
  slotNumber,
1127
1345
  timestamp,
@@ -1134,6 +1352,7 @@ export class SequencerPublisher {
1134
1352
  const cachedLastActionSlot = this.lastActions[action];
1135
1353
  this.lastActions[action] = slotNumber;
1136
1354
  this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
1355
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1137
1356
  let gasUsed;
1138
1357
  const simulateAbi = mergeAbis([
1139
1358
  request.abi ?? [],
@@ -1142,7 +1361,7 @@ export class SequencerPublisher {
1142
1361
  try {
1143
1362
  ({ gasUsed } = await this.l1TxUtils.simulate(request, {
1144
1363
  time: timestamp
1145
- }, [], simulateAbi)); // TODO(palla/slash): Check the timestamp logic
1364
+ }, [], simulateAbi));
1146
1365
  this.log.verbose(`Simulation for ${action} succeeded`, {
1147
1366
  ...logData,
1148
1367
  request,
@@ -1151,6 +1370,27 @@ export class SequencerPublisher {
1151
1370
  } catch (err) {
1152
1371
  const viemError = formatViemError(err, simulateAbi);
1153
1372
  this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
1373
+ this.backupFailedTx({
1374
+ id: keccak256(request.data),
1375
+ failureType: 'simulation',
1376
+ request: {
1377
+ to: request.to,
1378
+ data: request.data,
1379
+ value: request.value?.toString()
1380
+ },
1381
+ l1BlockNumber: l1BlockNumber.toString(),
1382
+ error: {
1383
+ message: viemError.message,
1384
+ name: viemError.name
1385
+ },
1386
+ context: {
1387
+ actions: [
1388
+ action
1389
+ ],
1390
+ slot: slotNumber,
1391
+ sender: this.getSenderAddress().toString()
1392
+ }
1393
+ });
1154
1394
  return false;
1155
1395
  }
1156
1396
  // We issued the simulation against the rollup contract, so we need to account for the overhead of the multicall3
@@ -1202,7 +1442,7 @@ export class SequencerPublisher {
1202
1442
  this.interrupted = false;
1203
1443
  this.l1TxUtils.restart();
1204
1444
  }
1205
- async prepareProposeTx(encodedData, timestamp, options) {
1445
+ async prepareProposeTx(encodedData, options) {
1206
1446
  const kzg = Blob.getViemKzgInstance();
1207
1447
  const blobInput = getPrefixedEthBlobCommitments(encodedData.blobs);
1208
1448
  this.log.debug('Validating blob input', {
@@ -1229,10 +1469,38 @@ export class SequencerPublisher {
1229
1469
  }, {}, {
1230
1470
  blobs: encodedData.blobs.map((b)=>b.data),
1231
1471
  kzg
1232
- }).catch((err)=>{
1233
- const { message, metaMessages } = formatViemError(err);
1234
- this.log.error(`Failed to validate blobs`, message, {
1235
- metaMessages
1472
+ }).catch(async (err)=>{
1473
+ const viemError = formatViemError(err);
1474
+ this.log.error(`Failed to validate blobs`, viemError.message, {
1475
+ metaMessages: viemError.metaMessages
1476
+ });
1477
+ const validateBlobsData = encodeFunctionData({
1478
+ abi: RollupAbi,
1479
+ functionName: 'validateBlobs',
1480
+ args: [
1481
+ blobInput
1482
+ ]
1483
+ });
1484
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1485
+ this.backupFailedTx({
1486
+ id: keccak256(validateBlobsData),
1487
+ failureType: 'simulation',
1488
+ request: {
1489
+ to: this.rollupContract.address,
1490
+ data: validateBlobsData
1491
+ },
1492
+ blobData: encodedData.blobs.map((b)=>toHex(b.data)),
1493
+ l1BlockNumber: l1BlockNumber.toString(),
1494
+ error: {
1495
+ message: viemError.message,
1496
+ name: viemError.name
1497
+ },
1498
+ context: {
1499
+ actions: [
1500
+ 'validate-blobs'
1501
+ ],
1502
+ sender: this.getSenderAddress().toString()
1503
+ }
1236
1504
  });
1237
1505
  throw new Error('Failed to validate blobs');
1238
1506
  });
@@ -1243,8 +1511,7 @@ export class SequencerPublisher {
1243
1511
  header: encodedData.header.toViem(),
1244
1512
  archive: toHex(encodedData.archive),
1245
1513
  oracleInput: {
1246
- // We are currently not modifying these. See #9963
1247
- feeAssetPriceModifier: 0n
1514
+ feeAssetPriceModifier: encodedData.feeAssetPriceModifier
1248
1515
  }
1249
1516
  },
1250
1517
  encodedData.attestationsAndSigners.getPackedAttestations(),
@@ -1252,7 +1519,7 @@ export class SequencerPublisher {
1252
1519
  encodedData.attestationsAndSignersSignature.toViemSignature(),
1253
1520
  blobInput
1254
1521
  ];
1255
- const { rollupData, simulationResult } = await this.simulateProposeTx(args, timestamp, options);
1522
+ const { rollupData, simulationResult } = await this.simulateProposeTx(args, options);
1256
1523
  return {
1257
1524
  args,
1258
1525
  blobEvaluationGas,
@@ -1263,9 +1530,8 @@ export class SequencerPublisher {
1263
1530
  /**
1264
1531
  * Simulates the propose tx with eth_simulateV1
1265
1532
  * @param args - The propose tx args
1266
- * @param timestamp - The timestamp to simulate proposal at
1267
1533
  * @returns The simulation result
1268
- */ async simulateProposeTx(args, timestamp, options) {
1534
+ */ async simulateProposeTx(args, options) {
1269
1535
  const rollupData = encodeFunctionData({
1270
1536
  abi: RollupAbi,
1271
1537
  functionName: 'propose',
@@ -1293,6 +1559,8 @@ export class SequencerPublisher {
1293
1559
  balance: 10n * WEI_CONST * WEI_CONST
1294
1560
  });
1295
1561
  }
1562
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1563
+ const simTs = this.getSimulationTimestamp(SlotNumber.fromBigInt(args[0].header.slotNumber));
1296
1564
  const simulationResult = await this.l1TxUtils.simulate({
1297
1565
  to: this.rollupContract.address,
1298
1566
  data: rollupData,
@@ -1301,8 +1569,7 @@ export class SequencerPublisher {
1301
1569
  from: this.proposerAddressForSimulation.toString()
1302
1570
  }
1303
1571
  }, {
1304
- // @note we add 1n to the timestamp because geth implementation doesn't like simulation timestamp to be equal to the current block timestamp
1305
- time: timestamp + 1n,
1572
+ time: simTs,
1306
1573
  // @note reth should have a 30m gas limit per block but throws errors that this tx is beyond limit so we increase here
1307
1574
  gasLimit: MAX_L1_TX_LIMIT * 2n
1308
1575
  }, stateOverrides, RollupAbi, {
@@ -1319,7 +1586,29 @@ export class SequencerPublisher {
1319
1586
  logs: []
1320
1587
  };
1321
1588
  }
1322
- this.log.error(`Failed to simulate propose tx`, viemError);
1589
+ this.log.error(`Failed to simulate propose tx`, viemError, {
1590
+ simulationTimestamp: simTs
1591
+ });
1592
+ this.backupFailedTx({
1593
+ id: keccak256(rollupData),
1594
+ failureType: 'simulation',
1595
+ request: {
1596
+ to: this.rollupContract.address,
1597
+ data: rollupData
1598
+ },
1599
+ l1BlockNumber: l1BlockNumber.toString(),
1600
+ error: {
1601
+ message: viemError.message,
1602
+ name: viemError.name
1603
+ },
1604
+ context: {
1605
+ actions: [
1606
+ 'propose'
1607
+ ],
1608
+ slot: Number(args[0].header.slotNumber),
1609
+ sender: this.getSenderAddress().toString()
1610
+ }
1611
+ });
1323
1612
  throw err;
1324
1613
  });
1325
1614
  return {
@@ -1327,11 +1616,11 @@ export class SequencerPublisher {
1327
1616
  simulationResult
1328
1617
  };
1329
1618
  }
1330
- async addProposeTx(checkpoint, encodedData, opts = {}, timestamp) {
1619
+ async addProposeTx(checkpoint, encodedData, opts = {}) {
1331
1620
  const slot = checkpoint.header.slotNumber;
1332
1621
  const timer = new Timer();
1333
1622
  const kzg = Blob.getViemKzgInstance();
1334
- const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(encodedData, timestamp, opts);
1623
+ const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(encodedData, opts);
1335
1624
  const startBlock = await this.l1TxUtils.getBlockNumber();
1336
1625
  const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil(Number(simulationResult.gasUsed) * 64 / 63)) + blobEvaluationGas + SequencerPublisher.MULTICALL_OVERHEAD_GAS_GUESS);
1337
1626
  // Send the blobs to the blob client preemptively. This helps in tests where the sequencer mistakingly thinks that the propose
@@ -1396,4 +1685,13 @@ export class SequencerPublisher {
1396
1685
  }
1397
1686
  });
1398
1687
  }
1688
+ /** Returns the timestamp of the last L1 slot within a given L2 slot. Used as the simulation timestamp
1689
+ * for eth_simulateV1 calls, since it's guaranteed to be greater than any L1 block produced during the slot. */ getSimulationTimestamp(slot) {
1690
+ const l1Constants = this.epochCache.getL1Constants();
1691
+ return getLastL1SlotTimestampForL2Slot(slot, l1Constants);
1692
+ }
1693
+ /** Returns the timestamp of the next L1 slot boundary after now. */ getNextL1SlotTimestamp() {
1694
+ const l1Constants = this.epochCache.getL1Constants();
1695
+ return getNextL1SlotTimestamp(this.dateProvider.nowInSeconds(), l1Constants);
1696
+ }
1399
1697
  }