@aztec/sequencer-client 0.0.1-commit.3469e52 → 0.0.1-commit.35158ae7e

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 -30
  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 +24 -23
  10. package/dest/global_variable_builder/index.d.ts +2 -2
  11. package/dest/global_variable_builder/index.d.ts.map +1 -1
  12. package/dest/publisher/config.d.ts +47 -17
  13. package/dest/publisher/config.d.ts.map +1 -1
  14. package/dest/publisher/config.js +121 -42
  15. package/dest/publisher/index.d.ts +2 -1
  16. package/dest/publisher/index.d.ts.map +1 -1
  17. package/dest/publisher/l1_tx_failed_store/factory.d.ts +11 -0
  18. package/dest/publisher/l1_tx_failed_store/factory.d.ts.map +1 -0
  19. package/dest/publisher/l1_tx_failed_store/factory.js +22 -0
  20. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts +59 -0
  21. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -0
  22. package/dest/publisher/l1_tx_failed_store/failed_tx_store.js +1 -0
  23. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts +15 -0
  24. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts.map +1 -0
  25. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.js +34 -0
  26. package/dest/publisher/l1_tx_failed_store/index.d.ts +4 -0
  27. package/dest/publisher/l1_tx_failed_store/index.d.ts.map +1 -0
  28. package/dest/publisher/l1_tx_failed_store/index.js +2 -0
  29. package/dest/publisher/sequencer-publisher-factory.d.ts +11 -3
  30. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  31. package/dest/publisher/sequencer-publisher-factory.js +27 -2
  32. package/dest/publisher/sequencer-publisher-metrics.d.ts +1 -1
  33. package/dest/publisher/sequencer-publisher-metrics.d.ts.map +1 -1
  34. package/dest/publisher/sequencer-publisher-metrics.js +12 -4
  35. package/dest/publisher/sequencer-publisher.d.ts +33 -10
  36. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  37. package/dest/publisher/sequencer-publisher.js +374 -57
  38. package/dest/sequencer/checkpoint_proposal_job.d.ts +41 -12
  39. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  40. package/dest/sequencer/checkpoint_proposal_job.js +295 -165
  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 +122 -30
  46. package/dest/sequencer/sequencer.d.ts +30 -15
  47. package/dest/sequencer/sequencer.d.ts.map +1 -1
  48. package/dest/sequencer/sequencer.js +95 -82
  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 +18 -15
  57. package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
  58. package/dest/test/mock_checkpoint_builder.js +63 -40
  59. package/dest/test/utils.d.ts +8 -8
  60. package/dest/test/utils.d.ts.map +1 -1
  61. package/dest/test/utils.js +10 -9
  62. package/package.json +27 -28
  63. package/src/client/sequencer-client.ts +76 -23
  64. package/src/config.ts +66 -41
  65. package/src/global_variable_builder/global_builder.ts +25 -26
  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-metrics.ts +7 -3
  75. package/src/publisher/sequencer-publisher.ts +376 -70
  76. package/src/sequencer/checkpoint_proposal_job.ts +409 -201
  77. package/src/sequencer/events.ts +1 -1
  78. package/src/sequencer/metrics.ts +138 -32
  79. package/src/sequencer/sequencer.ts +132 -95
  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 +93 -65
  84. package/src/test/utils.ts +22 -13
@@ -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
- 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';
394
+ import { 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,19 +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;
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;
451
+ /** Fee asset price oracle for computing price modifiers from Uniswap V4 */ feeAssetPriceOracle;
445
452
  // A CALL to a cold address is 2700 gas
446
453
  static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
447
454
  // Gas report for VotingWithSigTest shows a max gas of 100k, but we've seen it cost 700k+ in testnet
@@ -460,9 +467,12 @@ export class SequencerPublisher {
460
467
  this.slashingLog = createLogger('sequencer:publisher:slashing');
461
468
  this.lastActions = {};
462
469
  this.isPayloadEmptyCache = new Map();
470
+ this.payloadProposedCache = new Set();
463
471
  this.requests = [];
464
472
  this.log = deps.log ?? createLogger('sequencer:publisher');
465
473
  this.ethereumSlotDuration = BigInt(config.ethereumSlotDuration);
474
+ this.aztecSlotDuration = BigInt(config.aztecSlotDuration);
475
+ this.dateProvider = deps.dateProvider;
466
476
  this.epochCache = deps.epochCache;
467
477
  this.lastActions = deps.lastActions;
468
478
  this.blobClient = deps.blobClient;
@@ -470,6 +480,7 @@ export class SequencerPublisher {
470
480
  this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
471
481
  this.tracer = telemetry.getTracer('SequencerPublisher');
472
482
  this.l1TxUtils = deps.l1TxUtils;
483
+ this.getNextPublisher = deps.getNextPublisher;
473
484
  this.rollupContract = deps.rollupContract;
474
485
  this.govProposerContract = deps.governanceProposerContract;
475
486
  this.slashingProposerContract = deps.slashingProposerContract;
@@ -483,10 +494,36 @@ export class SequencerPublisher {
483
494
  if (config.fishermanMode) {
484
495
  this.l1FeeAnalyzer = new L1FeeAnalyzer(this.l1TxUtils.client, deps.dateProvider, createLogger('sequencer:publisher:fee-analyzer'));
485
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
+ });
486
517
  }
487
518
  getRollupContract() {
488
519
  return this.rollupContract;
489
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
+ }
490
527
  getSenderAddress() {
491
528
  return this.l1TxUtils.getSenderAddress();
492
529
  }
@@ -505,7 +542,7 @@ export class SequencerPublisher {
505
542
  this.requests.push(request);
506
543
  }
507
544
  getCurrentL2Slot() {
508
- return this.epochCache.getEpochAndSlotNow().slot;
545
+ return this.epochCache.getSlotNow();
509
546
  }
510
547
  /**
511
548
  * Clears all pending requests without sending them.
@@ -544,7 +581,7 @@ export class SequencerPublisher {
544
581
  // Get the transaction requests
545
582
  const l1Requests = requestsToAnalyze.map((r)=>r.request);
546
583
  // Start the analysis
547
- const analysisId = await this.l1FeeAnalyzer.startAnalysis(l2SlotNumber, gasLimit > 0n ? gasLimit : SequencerPublisher.PROPOSE_GAS_GUESS, l1Requests, blobConfig, onComplete);
584
+ const analysisId = await this.l1FeeAnalyzer.startAnalysis(l2SlotNumber, gasLimit > 0n ? gasLimit : MAX_L1_TX_LIMIT, l1Requests, blobConfig, onComplete);
548
585
  this.log.info('Started L1 fee analysis', {
549
586
  analysisId,
550
587
  l2SlotNumber: l2SlotNumber.toString(),
@@ -594,15 +631,24 @@ export class SequencerPublisher {
594
631
  // @note - we can only have one blob config per bundle
595
632
  // find requests with gas and blob configs
596
633
  // See https://github.com/AztecProtocol/aztec-packages/issues/11513
597
- const gasConfigs = requestsToProcess.filter((request)=>request.gasConfig).map((request)=>request.gasConfig);
598
- const blobConfigs = requestsToProcess.filter((request)=>request.blobConfig).map((request)=>request.blobConfig);
634
+ const gasConfigs = validRequests.filter((request)=>request.gasConfig).map((request)=>request.gasConfig);
635
+ const blobConfigs = validRequests.filter((request)=>request.blobConfig).map((request)=>request.blobConfig);
599
636
  if (blobConfigs.length > 1) {
600
637
  throw new Error('Multiple blob configs found');
601
638
  }
602
639
  const blobConfig = blobConfigs[0];
603
640
  // Merge gasConfigs. Yields the sum of gasLimits, and the earliest txTimeoutAt, or undefined if no gasConfig sets them.
604
641
  const gasLimits = gasConfigs.map((g)=>g?.gasLimit).filter((g)=>g !== undefined);
605
- const gasLimit = gasLimits.length > 0 ? sumBigint(gasLimits) : undefined; // sum
642
+ let gasLimit = gasLimits.length > 0 ? sumBigint(gasLimits) : undefined; // sum
643
+ // Cap at L1 block gas limit so the node accepts the tx ("gas limit too high" otherwise).
644
+ const maxGas = MAX_L1_TX_LIMIT;
645
+ if (gasLimit !== undefined && gasLimit > maxGas) {
646
+ this.log.debug('Capping bundled tx gas limit to L1 max', {
647
+ requested: gasLimit,
648
+ capped: maxGas
649
+ });
650
+ gasLimit = maxGas;
651
+ }
606
652
  const txTimeoutAts = gasConfigs.map((g)=>g?.txTimeoutAt).filter((g)=>g !== undefined);
607
653
  const txTimeoutAt = txTimeoutAts.length > 0 ? new Date(Math.min(...txTimeoutAts.map((g)=>g.getTime()))) : undefined; // earliest
608
654
  const txConfig = {
@@ -613,12 +659,34 @@ export class SequencerPublisher {
613
659
  // This ensures the committee gets precomputed correctly
614
660
  validRequests.sort((a, b)=>compareActions(a.action, b.action));
615
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
+ };
616
681
  this.log.debug('Forwarding transactions', {
617
682
  validRequests: validRequests.map((request)=>request.action),
618
683
  txConfig
619
684
  });
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);
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);
622
690
  return {
623
691
  result,
624
692
  expiredActions,
@@ -638,17 +706,83 @@ export class SequencerPublisher {
638
706
  }
639
707
  }
640
708
  }
641
- 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) {
642
744
  const actionsListStr = requests.map((r)=>r.action).join(', ');
643
745
  if (result instanceof FormattedViemError) {
644
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
+ });
645
770
  return {
646
771
  failedActions: requests.map((r)=>r.action)
647
772
  };
648
773
  } else {
649
774
  this.log.verbose(`Published bundled transactions (${actionsListStr})`, {
650
775
  result,
651
- 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
+ }))
652
786
  });
653
787
  const successfulActions = [];
654
788
  const failedActions = [];
@@ -659,6 +793,37 @@ export class SequencerPublisher {
659
793
  failedActions.push(request.action);
660
794
  }
661
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
+ }
662
827
  return {
663
828
  successfulActions,
664
829
  failedActions
@@ -666,17 +831,20 @@ export class SequencerPublisher {
666
831
  }
667
832
  }
668
833
  /**
669
- * @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
670
835
  * @param tipArchive - The archive to check
671
836
  * @returns The slot and block number if it is possible to propose, undefined otherwise
672
- */ canProposeAtNextEthBlock(tipArchive, msgSender, opts = {}) {
837
+ */ canProposeAt(tipArchive, msgSender, opts = {}) {
673
838
  // TODO: #14291 - should loop through multiple keys to check if any of them can propose
674
839
  const ignoredErrors = [
675
840
  'SlotAlreadyInChain',
676
841
  'InvalidProposer',
677
842
  'InvalidArchive'
678
843
  ];
679
- 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, {
680
848
  forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber
681
849
  }).catch((err)=>{
682
850
  if (err instanceof FormattedViemError && ignoredErrors.find((e)=>err.message.includes(e))) {
@@ -708,7 +876,7 @@ export class SequencerPublisher {
708
876
  header.blobsHash.toString(),
709
877
  flags
710
878
  ];
711
- const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
879
+ const ts = this.getNextL1SlotTimestamp();
712
880
  const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(opts?.forcePendingCheckpointNumber);
713
881
  let balance = 0n;
714
882
  if (this.config.fishermanMode) {
@@ -761,8 +929,12 @@ export class SequencerPublisher {
761
929
  ...logData,
762
930
  request
763
931
  });
932
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
764
933
  try {
765
- const { gasUsed } = await this.l1TxUtils.simulate(request, undefined, undefined, ErrorsAbi);
934
+ const { gasUsed } = await this.l1TxUtils.simulate(request, undefined, undefined, mergeAbis([
935
+ request.abi ?? [],
936
+ ErrorsAbi
937
+ ]));
766
938
  this.log.verbose(`Simulation for invalidate checkpoint ${checkpointNumber} succeeded`, {
767
939
  ...logData,
768
940
  request,
@@ -779,7 +951,7 @@ export class SequencerPublisher {
779
951
  const viemError = formatViemError(err);
780
952
  // If the error is due to the checkpoint not being in the pending chain, and it was indeed removed by someone else,
781
953
  // we can safely ignore it and return undefined so we go ahead with checkpoint building.
782
- if (viemError.message?.includes('Rollup__BlockNotInPendingChain')) {
954
+ if (viemError.message?.includes('Rollup__CheckpointNotInPendingChain')) {
783
955
  this.log.verbose(`Simulation for invalidate checkpoint ${checkpointNumber} failed due to checkpoint not being in pending chain`, {
784
956
  ...logData,
785
957
  request,
@@ -800,6 +972,27 @@ export class SequencerPublisher {
800
972
  }
801
973
  // Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
802
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
+ });
803
996
  throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, {
804
997
  cause: viemError
805
998
  });
@@ -826,30 +1019,21 @@ export class SequencerPublisher {
826
1019
  }
827
1020
  }
828
1021
  /** Simulates `propose` to make sure that the checkpoint is valid for submission */ async validateCheckpointForSubmission(checkpoint, attestationsAndSigners, attestationsAndSignersSignature, options) {
829
- const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
830
- // TODO(palla/mbps): This should not be needed, there's no flow where we propose with zero attestations. Or is there?
831
- // If we have no attestations, we still need to provide the empty attestations
832
- // so that the committee is recalculated correctly
833
- // const ignoreSignatures = attestationsAndSigners.attestations.length === 0;
834
- // if (ignoreSignatures) {
835
- // const { committee } = await this.epochCache.getCommittee(block.header.globalVariables.slotNumber);
836
- // if (!committee) {
837
- // this.log.warn(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
838
- // throw new Error(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
839
- // }
840
- // attestationsAndSigners.attestations = committee.map(committeeMember =>
841
- // CommitteeAttestation.fromAddress(committeeMember),
842
- // );
843
- // }
1022
+ // When pipelining, the checkpoint targets the next slot so its timestamp is in the future.
1023
+ // Without pipelining, the checkpoint targets the current slot so its timestamp is in the past
1024
+ // by the time we simulate (~24s of build time), causing eth_simulateV1 to reject it.
1025
+ // In that case, use the latest L1 block timestamp + one ethereum slot, which is just ahead
1026
+ // of L1 and still within the same L2 slot.
1027
+ const ts = this.epochCache.isProposerPipeliningEnabled() ? checkpoint.header.timestamp : (await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration;
844
1028
  const blobFields = checkpoint.toBlobFields();
845
- const blobs = getBlobsPerL1Block(blobFields);
1029
+ const blobs = await getBlobsPerL1Block(blobFields);
846
1030
  const blobInput = getPrefixedEthBlobCommitments(blobs);
847
1031
  const args = [
848
1032
  {
849
1033
  header: checkpoint.header.toViem(),
850
1034
  archive: toHex(checkpoint.archive.root.toBuffer()),
851
1035
  oracleInput: {
852
- feeAssetPriceModifier: 0n
1036
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier
853
1037
  }
854
1038
  },
855
1039
  attestationsAndSigners.getPackedAttestations(),
@@ -884,6 +1068,28 @@ export class SequencerPublisher {
884
1068
  this.log.warn(`Skipping vote cast for payload with empty code`);
885
1069
  return false;
886
1070
  }
1071
+ // Check if payload was already submitted to governance
1072
+ const cacheKey = payload.toString();
1073
+ if (!this.payloadProposedCache.has(cacheKey)) {
1074
+ try {
1075
+ const l1StartBlock = await this.rollupContract.getL1StartBlock();
1076
+ const proposed = await retry(()=>base.hasPayloadBeenProposed(payload.toString(), l1StartBlock), 'Check if payload was proposed', makeBackoff([
1077
+ 0,
1078
+ 1,
1079
+ 2
1080
+ ]), this.log, true);
1081
+ if (proposed) {
1082
+ this.payloadProposedCache.add(cacheKey);
1083
+ }
1084
+ } catch (err) {
1085
+ this.log.warn(`Failed to check if payload ${payload} was proposed after retries, skipping signal`, err);
1086
+ return false;
1087
+ }
1088
+ }
1089
+ if (this.payloadProposedCache.has(cacheKey)) {
1090
+ this.log.info(`Payload ${payload} was already proposed to governance, stopping signals`);
1091
+ return false;
1092
+ }
887
1093
  const cachedLastVote = this.lastActions[signalType];
888
1094
  this.lastActions[signalType] = slotNumber;
889
1095
  const action = signalType;
@@ -894,15 +1100,41 @@ export class SequencerPublisher {
894
1100
  signer: this.l1TxUtils.client.account?.address,
895
1101
  lastValidL2Slot: slotNumber
896
1102
  });
1103
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
897
1104
  try {
898
1105
  await this.l1TxUtils.simulate(request, {
899
1106
  time: timestamp
900
- }, [], ErrorsAbi);
1107
+ }, [], mergeAbis([
1108
+ request.abi ?? [],
1109
+ ErrorsAbi
1110
+ ]));
901
1111
  this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, {
902
1112
  request
903
1113
  });
904
1114
  } catch (err) {
905
- this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, err);
1115
+ const viemError = formatViemError(err);
1116
+ this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, viemError);
1117
+ this.backupFailedTx({
1118
+ id: keccak256(request.data),
1119
+ failureType: 'simulation',
1120
+ request: {
1121
+ to: request.to,
1122
+ data: request.data,
1123
+ value: request.value?.toString()
1124
+ },
1125
+ l1BlockNumber: l1BlockNumber.toString(),
1126
+ error: {
1127
+ message: viemError.message,
1128
+ name: viemError.name
1129
+ },
1130
+ context: {
1131
+ actions: [
1132
+ action
1133
+ ],
1134
+ slot: slotNumber,
1135
+ sender: this.getSenderAddress().toString()
1136
+ }
1137
+ });
906
1138
  // Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
907
1139
  }
908
1140
  // TODO(palla/slash): All votes (governance and slashing) should txTimeoutAt at the end of the slot.
@@ -1041,13 +1273,14 @@ export class SequencerPublisher {
1041
1273
  /** Simulates and enqueues a proposal for a checkpoint on L1 */ async enqueueProposeCheckpoint(checkpoint, attestationsAndSigners, attestationsAndSignersSignature, opts = {}) {
1042
1274
  const checkpointHeader = checkpoint.header;
1043
1275
  const blobFields = checkpoint.toBlobFields();
1044
- const blobs = getBlobsPerL1Block(blobFields);
1276
+ const blobs = await getBlobsPerL1Block(blobFields);
1045
1277
  const proposeTxArgs = {
1046
1278
  header: checkpointHeader,
1047
1279
  archive: checkpoint.archive.root.toBuffer(),
1048
1280
  blobs,
1049
1281
  attestationsAndSigners,
1050
- attestationsAndSignersSignature
1282
+ attestationsAndSignersSignature,
1283
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier
1051
1284
  };
1052
1285
  let ts;
1053
1286
  try {
@@ -1123,28 +1356,60 @@ export class SequencerPublisher {
1123
1356
  const cachedLastActionSlot = this.lastActions[action];
1124
1357
  this.lastActions[action] = slotNumber;
1125
1358
  this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
1359
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1126
1360
  let gasUsed;
1361
+ const simulateAbi = mergeAbis([
1362
+ request.abi ?? [],
1363
+ ErrorsAbi
1364
+ ]);
1127
1365
  try {
1128
1366
  ({ gasUsed } = await this.l1TxUtils.simulate(request, {
1129
1367
  time: timestamp
1130
- }, [], ErrorsAbi)); // TODO(palla/slash): Check the timestamp logic
1368
+ }, [], simulateAbi)); // TODO(palla/slash): Check the timestamp logic
1131
1369
  this.log.verbose(`Simulation for ${action} succeeded`, {
1132
1370
  ...logData,
1133
1371
  request,
1134
1372
  gasUsed
1135
1373
  });
1136
1374
  } catch (err) {
1137
- const viemError = formatViemError(err);
1375
+ const viemError = formatViemError(err, simulateAbi);
1138
1376
  this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
1377
+ this.backupFailedTx({
1378
+ id: keccak256(request.data),
1379
+ failureType: 'simulation',
1380
+ request: {
1381
+ to: request.to,
1382
+ data: request.data,
1383
+ value: request.value?.toString()
1384
+ },
1385
+ l1BlockNumber: l1BlockNumber.toString(),
1386
+ error: {
1387
+ message: viemError.message,
1388
+ name: viemError.name
1389
+ },
1390
+ context: {
1391
+ actions: [
1392
+ action
1393
+ ],
1394
+ slot: slotNumber,
1395
+ sender: this.getSenderAddress().toString()
1396
+ }
1397
+ });
1139
1398
  return false;
1140
1399
  }
1141
1400
  // We issued the simulation against the rollup contract, so we need to account for the overhead of the multicall3
1142
1401
  const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil(Number(gasUsed) * 64 / 63)));
1143
1402
  logData.gasLimit = gasLimit;
1403
+ // Store the ABI used for simulation on the request so Multicall3.forward can decode errors
1404
+ // when the tx is sent and a revert is diagnosed via simulation.
1405
+ const requestWithAbi = {
1406
+ ...request,
1407
+ abi: simulateAbi
1408
+ };
1144
1409
  this.log.debug(`Enqueuing ${action}`, logData);
1145
1410
  this.addRequest({
1146
1411
  action,
1147
- request,
1412
+ request: requestWithAbi,
1148
1413
  gasConfig: {
1149
1414
  gasLimit
1150
1415
  },
@@ -1208,10 +1473,38 @@ export class SequencerPublisher {
1208
1473
  }, {}, {
1209
1474
  blobs: encodedData.blobs.map((b)=>b.data),
1210
1475
  kzg
1211
- }).catch((err)=>{
1212
- const { message, metaMessages } = formatViemError(err);
1213
- this.log.error(`Failed to validate blobs`, message, {
1214
- metaMessages
1476
+ }).catch(async (err)=>{
1477
+ const viemError = formatViemError(err);
1478
+ this.log.error(`Failed to validate blobs`, viemError.message, {
1479
+ metaMessages: viemError.metaMessages
1480
+ });
1481
+ const validateBlobsData = encodeFunctionData({
1482
+ abi: RollupAbi,
1483
+ functionName: 'validateBlobs',
1484
+ args: [
1485
+ blobInput
1486
+ ]
1487
+ });
1488
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1489
+ this.backupFailedTx({
1490
+ id: keccak256(validateBlobsData),
1491
+ failureType: 'simulation',
1492
+ request: {
1493
+ to: this.rollupContract.address,
1494
+ data: validateBlobsData
1495
+ },
1496
+ blobData: encodedData.blobs.map((b)=>toHex(b.data)),
1497
+ l1BlockNumber: l1BlockNumber.toString(),
1498
+ error: {
1499
+ message: viemError.message,
1500
+ name: viemError.name
1501
+ },
1502
+ context: {
1503
+ actions: [
1504
+ 'validate-blobs'
1505
+ ],
1506
+ sender: this.getSenderAddress().toString()
1507
+ }
1215
1508
  });
1216
1509
  throw new Error('Failed to validate blobs');
1217
1510
  });
@@ -1222,8 +1515,7 @@ export class SequencerPublisher {
1222
1515
  header: encodedData.header.toViem(),
1223
1516
  archive: toHex(encodedData.archive),
1224
1517
  oracleInput: {
1225
- // We are currently not modifying these. See #9963
1226
- feeAssetPriceModifier: 0n
1518
+ feeAssetPriceModifier: encodedData.feeAssetPriceModifier
1227
1519
  }
1228
1520
  },
1229
1521
  encodedData.attestationsAndSigners.getPackedAttestations(),
@@ -1272,10 +1564,11 @@ export class SequencerPublisher {
1272
1564
  balance: 10n * WEI_CONST * WEI_CONST
1273
1565
  });
1274
1566
  }
1567
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1275
1568
  const simulationResult = await this.l1TxUtils.simulate({
1276
1569
  to: this.rollupContract.address,
1277
1570
  data: rollupData,
1278
- gas: SequencerPublisher.PROPOSE_GAS_GUESS,
1571
+ gas: MAX_L1_TX_LIMIT,
1279
1572
  ...this.proposerAddressForSimulation && {
1280
1573
  from: this.proposerAddressForSimulation.toString()
1281
1574
  }
@@ -1283,10 +1576,10 @@ export class SequencerPublisher {
1283
1576
  // @note we add 1n to the timestamp because geth implementation doesn't like simulation timestamp to be equal to the current block timestamp
1284
1577
  time: timestamp + 1n,
1285
1578
  // @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
1579
+ gasLimit: MAX_L1_TX_LIMIT * 2n
1287
1580
  }, stateOverrides, RollupAbi, {
1288
1581
  // @note fallback gas estimate to use if the node doesn't support simulation API
1289
- fallbackGasEstimate: SequencerPublisher.PROPOSE_GAS_GUESS
1582
+ fallbackGasEstimate: MAX_L1_TX_LIMIT
1290
1583
  }).catch((err)=>{
1291
1584
  // In fisherman mode, we expect ValidatorSelection__MissingProposerSignature since fisherman doesn't have proposer signature
1292
1585
  const viemError = formatViemError(err);
@@ -1294,11 +1587,31 @@ export class SequencerPublisher {
1294
1587
  this.log.debug(`Ignoring expected ValidatorSelection__MissingProposerSignature error in fisherman mode`);
1295
1588
  // Return a minimal simulation result with the fallback gas estimate
1296
1589
  return {
1297
- gasUsed: SequencerPublisher.PROPOSE_GAS_GUESS,
1590
+ gasUsed: MAX_L1_TX_LIMIT,
1298
1591
  logs: []
1299
1592
  };
1300
1593
  }
1301
1594
  this.log.error(`Failed to simulate propose tx`, viemError);
1595
+ this.backupFailedTx({
1596
+ id: keccak256(rollupData),
1597
+ failureType: 'simulation',
1598
+ request: {
1599
+ to: this.rollupContract.address,
1600
+ data: rollupData
1601
+ },
1602
+ l1BlockNumber: l1BlockNumber.toString(),
1603
+ error: {
1604
+ message: viemError.message,
1605
+ name: viemError.name
1606
+ },
1607
+ context: {
1608
+ actions: [
1609
+ 'propose'
1610
+ ],
1611
+ slot: Number(args[0].header.slotNumber),
1612
+ sender: this.getSenderAddress().toString()
1613
+ }
1614
+ });
1302
1615
  throw err;
1303
1616
  });
1304
1617
  return {
@@ -1375,4 +1688,8 @@ export class SequencerPublisher {
1375
1688
  }
1376
1689
  });
1377
1690
  }
1691
+ /** Returns the timestamp to use when simulating L1 proposal calls */ getNextL1SlotTimestamp() {
1692
+ const l1Constants = this.epochCache.getL1Constants();
1693
+ return getNextL1SlotTimestamp(this.dateProvider.nowInSeconds(), l1Constants);
1694
+ }
1378
1695
  }