@aztec/sequencer-client 0.0.1-commit.6d3c34e → 0.0.1-commit.7035c9bd6

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 (97) hide show
  1. package/dest/client/sequencer-client.d.ts +12 -7
  2. package/dest/client/sequencer-client.d.ts.map +1 -1
  3. package/dest/client/sequencer-client.js +56 -17
  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 +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 +30 -10
  37. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  38. package/dest/publisher/sequencer-publisher.js +362 -56
  39. package/dest/sequencer/checkpoint_proposal_job.d.ts +42 -11
  40. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  41. package/dest/sequencer/checkpoint_proposal_job.js +322 -122
  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/events.d.ts +2 -1
  46. package/dest/sequencer/events.d.ts.map +1 -1
  47. package/dest/sequencer/index.d.ts +1 -2
  48. package/dest/sequencer/index.d.ts.map +1 -1
  49. package/dest/sequencer/index.js +0 -1
  50. package/dest/sequencer/metrics.d.ts +21 -5
  51. package/dest/sequencer/metrics.d.ts.map +1 -1
  52. package/dest/sequencer/metrics.js +122 -30
  53. package/dest/sequencer/sequencer.d.ts +43 -20
  54. package/dest/sequencer/sequencer.d.ts.map +1 -1
  55. package/dest/sequencer/sequencer.js +151 -82
  56. package/dest/sequencer/timetable.d.ts +4 -6
  57. package/dest/sequencer/timetable.d.ts.map +1 -1
  58. package/dest/sequencer/timetable.js +7 -11
  59. package/dest/sequencer/types.d.ts +2 -2
  60. package/dest/sequencer/types.d.ts.map +1 -1
  61. package/dest/test/index.d.ts +3 -5
  62. package/dest/test/index.d.ts.map +1 -1
  63. package/dest/test/mock_checkpoint_builder.d.ts +23 -19
  64. package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
  65. package/dest/test/mock_checkpoint_builder.js +67 -38
  66. package/dest/test/utils.d.ts +8 -8
  67. package/dest/test/utils.d.ts.map +1 -1
  68. package/dest/test/utils.js +12 -11
  69. package/package.json +29 -28
  70. package/src/client/sequencer-client.ts +77 -18
  71. package/src/config.ts +66 -41
  72. package/src/global_variable_builder/global_builder.ts +6 -5
  73. package/src/index.ts +1 -6
  74. package/src/publisher/config.ts +121 -43
  75. package/src/publisher/index.ts +3 -0
  76. package/src/publisher/l1_tx_failed_store/factory.ts +32 -0
  77. package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +55 -0
  78. package/src/publisher/l1_tx_failed_store/file_store_failed_tx_store.ts +46 -0
  79. package/src/publisher/l1_tx_failed_store/index.ts +3 -0
  80. package/src/publisher/sequencer-publisher-factory.ts +38 -6
  81. package/src/publisher/sequencer-publisher-metrics.ts +7 -3
  82. package/src/publisher/sequencer-publisher.ts +360 -69
  83. package/src/sequencer/checkpoint_proposal_job.ts +449 -142
  84. package/src/sequencer/checkpoint_voter.ts +32 -7
  85. package/src/sequencer/events.ts +1 -1
  86. package/src/sequencer/index.ts +0 -1
  87. package/src/sequencer/metrics.ts +138 -32
  88. package/src/sequencer/sequencer.ts +200 -91
  89. package/src/sequencer/timetable.ts +13 -12
  90. package/src/sequencer/types.ts +1 -1
  91. package/src/test/index.ts +2 -4
  92. package/src/test/mock_checkpoint_builder.ts +122 -78
  93. package/src/test/utils.ts +24 -14
  94. package/dest/sequencer/block_builder.d.ts +0 -26
  95. package/dest/sequencer/block_builder.d.ts.map +0 -1
  96. package/dest/sequencer/block_builder.js +0 -129
  97. 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,20 @@ 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;
444
+ aztecSlotDuration;
438
445
  blobClient;
439
446
  /** Address to use for simulations in fisherman mode (actual proposer's address) */ proposerAddressForSimulation;
447
+ /** Optional callback to obtain a replacement publisher when the current one fails to send. */ getNextPublisher;
440
448
  /** 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;
449
+ /** Fee asset price oracle for computing price modifiers from Uniswap V4 */ feeAssetPriceOracle;
445
450
  // A CALL to a cold address is 2700 gas
446
451
  static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
447
452
  // Gas report for VotingWithSigTest shows a max gas of 100k, but we've seen it cost 700k+ in testnet
@@ -460,9 +465,11 @@ export class SequencerPublisher {
460
465
  this.slashingLog = createLogger('sequencer:publisher:slashing');
461
466
  this.lastActions = {};
462
467
  this.isPayloadEmptyCache = new Map();
468
+ this.payloadProposedCache = new Set();
463
469
  this.requests = [];
464
470
  this.log = deps.log ?? createLogger('sequencer:publisher');
465
471
  this.ethereumSlotDuration = BigInt(config.ethereumSlotDuration);
472
+ this.aztecSlotDuration = BigInt(config.aztecSlotDuration);
466
473
  this.epochCache = deps.epochCache;
467
474
  this.lastActions = deps.lastActions;
468
475
  this.blobClient = deps.blobClient;
@@ -470,6 +477,7 @@ export class SequencerPublisher {
470
477
  this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
471
478
  this.tracer = telemetry.getTracer('SequencerPublisher');
472
479
  this.l1TxUtils = deps.l1TxUtils;
480
+ this.getNextPublisher = deps.getNextPublisher;
473
481
  this.rollupContract = deps.rollupContract;
474
482
  this.govProposerContract = deps.governanceProposerContract;
475
483
  this.slashingProposerContract = deps.slashingProposerContract;
@@ -483,10 +491,36 @@ export class SequencerPublisher {
483
491
  if (config.fishermanMode) {
484
492
  this.l1FeeAnalyzer = new L1FeeAnalyzer(this.l1TxUtils.client, deps.dateProvider, createLogger('sequencer:publisher:fee-analyzer'));
485
493
  }
494
+ // Initialize fee asset price oracle
495
+ this.feeAssetPriceOracle = new FeeAssetPriceOracle(this.l1TxUtils.client, this.rollupContract, createLogger('sequencer:publisher:price-oracle'));
496
+ // Initialize failed L1 tx store (optional, for test networks)
497
+ this.failedTxStore = createL1TxFailedStore(config.l1TxFailedStore, this.log);
498
+ }
499
+ /**
500
+ * Backs up a failed L1 transaction to the configured store for debugging.
501
+ * Does nothing if no store is configured.
502
+ */ backupFailedTx(failedTx) {
503
+ if (!this.failedTxStore) {
504
+ return;
505
+ }
506
+ const tx = {
507
+ ...failedTx,
508
+ timestamp: Date.now()
509
+ };
510
+ // Fire and forget - don't block on backup
511
+ void this.failedTxStore.then((store)=>store?.saveFailedTx(tx)).catch((err)=>{
512
+ this.log.warn(`Failed to backup failed L1 tx to store`, err);
513
+ });
486
514
  }
487
515
  getRollupContract() {
488
516
  return this.rollupContract;
489
517
  }
518
+ /**
519
+ * Gets the fee asset price modifier from the oracle.
520
+ * Returns 0n if the oracle query fails.
521
+ */ getFeeAssetPriceModifier() {
522
+ return this.feeAssetPriceOracle.computePriceModifier();
523
+ }
490
524
  getSenderAddress() {
491
525
  return this.l1TxUtils.getSenderAddress();
492
526
  }
@@ -505,7 +539,7 @@ export class SequencerPublisher {
505
539
  this.requests.push(request);
506
540
  }
507
541
  getCurrentL2Slot() {
508
- return this.epochCache.getEpochAndSlotNow().slot;
542
+ return this.epochCache.getSlotNow();
509
543
  }
510
544
  /**
511
545
  * Clears all pending requests without sending them.
@@ -544,7 +578,7 @@ export class SequencerPublisher {
544
578
  // Get the transaction requests
545
579
  const l1Requests = requestsToAnalyze.map((r)=>r.request);
546
580
  // Start the analysis
547
- const analysisId = await this.l1FeeAnalyzer.startAnalysis(l2SlotNumber, gasLimit > 0n ? gasLimit : SequencerPublisher.PROPOSE_GAS_GUESS, l1Requests, blobConfig, onComplete);
581
+ const analysisId = await this.l1FeeAnalyzer.startAnalysis(l2SlotNumber, gasLimit > 0n ? gasLimit : MAX_L1_TX_LIMIT, l1Requests, blobConfig, onComplete);
548
582
  this.log.info('Started L1 fee analysis', {
549
583
  analysisId,
550
584
  l2SlotNumber: l2SlotNumber.toString(),
@@ -594,15 +628,24 @@ export class SequencerPublisher {
594
628
  // @note - we can only have one blob config per bundle
595
629
  // find requests with gas and blob configs
596
630
  // 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);
631
+ const gasConfigs = validRequests.filter((request)=>request.gasConfig).map((request)=>request.gasConfig);
632
+ const blobConfigs = validRequests.filter((request)=>request.blobConfig).map((request)=>request.blobConfig);
599
633
  if (blobConfigs.length > 1) {
600
634
  throw new Error('Multiple blob configs found');
601
635
  }
602
636
  const blobConfig = blobConfigs[0];
603
637
  // Merge gasConfigs. Yields the sum of gasLimits, and the earliest txTimeoutAt, or undefined if no gasConfig sets them.
604
638
  const gasLimits = gasConfigs.map((g)=>g?.gasLimit).filter((g)=>g !== undefined);
605
- const gasLimit = gasLimits.length > 0 ? sumBigint(gasLimits) : undefined; // sum
639
+ let gasLimit = gasLimits.length > 0 ? sumBigint(gasLimits) : undefined; // sum
640
+ // Cap at L1 block gas limit so the node accepts the tx ("gas limit too high" otherwise).
641
+ const maxGas = MAX_L1_TX_LIMIT;
642
+ if (gasLimit !== undefined && gasLimit > maxGas) {
643
+ this.log.debug('Capping bundled tx gas limit to L1 max', {
644
+ requested: gasLimit,
645
+ capped: maxGas
646
+ });
647
+ gasLimit = maxGas;
648
+ }
606
649
  const txTimeoutAts = gasConfigs.map((g)=>g?.txTimeoutAt).filter((g)=>g !== undefined);
607
650
  const txTimeoutAt = txTimeoutAts.length > 0 ? new Date(Math.min(...txTimeoutAts.map((g)=>g.getTime()))) : undefined; // earliest
608
651
  const txConfig = {
@@ -613,12 +656,34 @@ export class SequencerPublisher {
613
656
  // This ensures the committee gets precomputed correctly
614
657
  validRequests.sort((a, b)=>compareActions(a.action, b.action));
615
658
  try {
659
+ // Capture context for failed tx backup before sending
660
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
661
+ const multicallData = encodeFunctionData({
662
+ abi: multicall3Abi,
663
+ functionName: 'aggregate3',
664
+ args: [
665
+ validRequests.map((r)=>({
666
+ target: r.request.to,
667
+ callData: r.request.data,
668
+ allowFailure: true
669
+ }))
670
+ ]
671
+ });
672
+ const blobDataHex = blobConfig?.blobs?.map((b)=>toHex(b));
673
+ const txContext = {
674
+ multicallData,
675
+ blobData: blobDataHex,
676
+ l1BlockNumber
677
+ };
616
678
  this.log.debug('Forwarding transactions', {
617
679
  validRequests: validRequests.map((request)=>request.action),
618
680
  txConfig
619
681
  });
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);
682
+ const result = await this.forwardWithPublisherRotation(validRequests, txConfig, blobConfig);
683
+ if (result === undefined) {
684
+ return undefined;
685
+ }
686
+ const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(validRequests, result, txContext);
622
687
  return {
623
688
  result,
624
689
  expiredActions,
@@ -638,17 +703,83 @@ export class SequencerPublisher {
638
703
  }
639
704
  }
640
705
  }
641
- callbackBundledTransactions(requests, result) {
706
+ /**
707
+ * Forwards transactions via Multicall3, rotating to the next available publisher if a send
708
+ * failure occurs (i.e. the tx never reached the chain).
709
+ * On-chain reverts and simulation errors are returned as-is without rotation.
710
+ */ async forwardWithPublisherRotation(validRequests, txConfig, blobConfig) {
711
+ const triedAddresses = [];
712
+ let currentPublisher = this.l1TxUtils;
713
+ while(true){
714
+ triedAddresses.push(currentPublisher.getSenderAddress());
715
+ try {
716
+ const result = await Multicall3.forward(validRequests.map((r)=>r.request), currentPublisher, txConfig, blobConfig, this.rollupContract.address, this.log);
717
+ this.l1TxUtils = currentPublisher;
718
+ return result;
719
+ } catch (err) {
720
+ if (err instanceof TimeoutError) {
721
+ throw err;
722
+ }
723
+ const viemError = formatViemError(err);
724
+ if (!this.getNextPublisher) {
725
+ this.log.error('Failed to publish bundled transactions', viemError);
726
+ return undefined;
727
+ }
728
+ this.log.warn(`Publisher ${currentPublisher.getSenderAddress()} failed to send, rotating to next publisher`, viemError);
729
+ const nextPublisher = await this.getNextPublisher([
730
+ ...triedAddresses
731
+ ]);
732
+ if (!nextPublisher) {
733
+ this.log.error('All available publishers exhausted, failed to publish bundled transactions');
734
+ return undefined;
735
+ }
736
+ currentPublisher = nextPublisher;
737
+ }
738
+ }
739
+ }
740
+ callbackBundledTransactions(requests, result, txContext) {
642
741
  const actionsListStr = requests.map((r)=>r.action).join(', ');
643
742
  if (result instanceof FormattedViemError) {
644
743
  this.log.error(`Failed to publish bundled transactions (${actionsListStr})`, result);
744
+ this.backupFailedTx({
745
+ id: keccak256(txContext.multicallData),
746
+ failureType: 'send-error',
747
+ request: {
748
+ to: MULTI_CALL_3_ADDRESS,
749
+ data: txContext.multicallData
750
+ },
751
+ blobData: txContext.blobData,
752
+ l1BlockNumber: txContext.l1BlockNumber.toString(),
753
+ error: {
754
+ message: result.message,
755
+ name: result.name
756
+ },
757
+ context: {
758
+ actions: requests.map((r)=>r.action),
759
+ requests: requests.map((r)=>({
760
+ action: r.action,
761
+ to: r.request.to,
762
+ data: r.request.data
763
+ })),
764
+ sender: this.getSenderAddress().toString()
765
+ }
766
+ });
645
767
  return {
646
768
  failedActions: requests.map((r)=>r.action)
647
769
  };
648
770
  } else {
649
771
  this.log.verbose(`Published bundled transactions (${actionsListStr})`, {
650
772
  result,
651
- requests
773
+ requests: requests.map((r)=>({
774
+ ...r,
775
+ // Avoid logging large blob data
776
+ blobConfig: r.blobConfig ? {
777
+ ...r.blobConfig,
778
+ blobs: r.blobConfig.blobs.map((b)=>({
779
+ size: trimmedBytesLength(b)
780
+ }))
781
+ } : undefined
782
+ }))
652
783
  });
653
784
  const successfulActions = [];
654
785
  const failedActions = [];
@@ -659,6 +790,37 @@ export class SequencerPublisher {
659
790
  failedActions.push(request.action);
660
791
  }
661
792
  }
793
+ // Single backup for the whole reverted tx
794
+ if (failedActions.length > 0 && result?.receipt?.status === 'reverted') {
795
+ this.backupFailedTx({
796
+ id: result.receipt.transactionHash,
797
+ failureType: 'revert',
798
+ request: {
799
+ to: MULTI_CALL_3_ADDRESS,
800
+ data: txContext.multicallData
801
+ },
802
+ blobData: txContext.blobData,
803
+ l1BlockNumber: result.receipt.blockNumber.toString(),
804
+ receipt: {
805
+ transactionHash: result.receipt.transactionHash,
806
+ blockNumber: result.receipt.blockNumber.toString(),
807
+ gasUsed: (result.receipt.gasUsed ?? 0n).toString(),
808
+ status: 'reverted'
809
+ },
810
+ error: {
811
+ message: result.errorMsg ?? 'Transaction reverted'
812
+ },
813
+ context: {
814
+ actions: failedActions,
815
+ requests: requests.filter((r)=>failedActions.includes(r.action)).map((r)=>({
816
+ action: r.action,
817
+ to: r.request.to,
818
+ data: r.request.data
819
+ })),
820
+ sender: this.getSenderAddress().toString()
821
+ }
822
+ });
823
+ }
662
824
  return {
663
825
  successfulActions,
664
826
  failedActions
@@ -666,17 +828,19 @@ export class SequencerPublisher {
666
828
  }
667
829
  }
668
830
  /**
669
- * @notice Will call `canProposeAtNextEthBlock` to make sure that it is possible to propose
831
+ * @notice Will call `canProposeAt` to make sure that it is possible to propose
670
832
  * @param tipArchive - The archive to check
671
833
  * @returns The slot and block number if it is possible to propose, undefined otherwise
672
- */ canProposeAtNextEthBlock(tipArchive, msgSender, opts = {}) {
834
+ */ canProposeAt(tipArchive, msgSender, opts = {}) {
673
835
  // TODO: #14291 - should loop through multiple keys to check if any of them can propose
674
836
  const ignoredErrors = [
675
837
  'SlotAlreadyInChain',
676
838
  'InvalidProposer',
677
839
  'InvalidArchive'
678
840
  ];
679
- return this.rollupContract.canProposeAtNextEthBlock(tipArchive.toBuffer(), msgSender.toString(), Number(this.ethereumSlotDuration), {
841
+ const pipelined = opts.pipelined ?? this.epochCache.isProposerPipeliningEnabled();
842
+ const slotOffset = pipelined ? this.aztecSlotDuration : 0n;
843
+ return this.rollupContract.canProposeAt(tipArchive.toBuffer(), msgSender.toString(), this.ethereumSlotDuration, slotOffset, {
680
844
  forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber
681
845
  }).catch((err)=>{
682
846
  if (err instanceof FormattedViemError && ignoredErrors.find((e)=>err.message.includes(e))) {
@@ -761,8 +925,12 @@ export class SequencerPublisher {
761
925
  ...logData,
762
926
  request
763
927
  });
928
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
764
929
  try {
765
- const { gasUsed } = await this.l1TxUtils.simulate(request, undefined, undefined, ErrorsAbi);
930
+ const { gasUsed } = await this.l1TxUtils.simulate(request, undefined, undefined, mergeAbis([
931
+ request.abi ?? [],
932
+ ErrorsAbi
933
+ ]));
766
934
  this.log.verbose(`Simulation for invalidate checkpoint ${checkpointNumber} succeeded`, {
767
935
  ...logData,
768
936
  request,
@@ -779,7 +947,7 @@ export class SequencerPublisher {
779
947
  const viemError = formatViemError(err);
780
948
  // If the error is due to the checkpoint not being in the pending chain, and it was indeed removed by someone else,
781
949
  // we can safely ignore it and return undefined so we go ahead with checkpoint building.
782
- if (viemError.message?.includes('Rollup__BlockNotInPendingChain')) {
950
+ if (viemError.message?.includes('Rollup__CheckpointNotInPendingChain')) {
783
951
  this.log.verbose(`Simulation for invalidate checkpoint ${checkpointNumber} failed due to checkpoint not being in pending chain`, {
784
952
  ...logData,
785
953
  request,
@@ -800,6 +968,27 @@ export class SequencerPublisher {
800
968
  }
801
969
  // Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
802
970
  this.log.error(`Simulation for invalidate checkpoint ${checkpointNumber} failed`, viemError, logData);
971
+ this.backupFailedTx({
972
+ id: keccak256(request.data),
973
+ failureType: 'simulation',
974
+ request: {
975
+ to: request.to,
976
+ data: request.data,
977
+ value: request.value?.toString()
978
+ },
979
+ l1BlockNumber: l1BlockNumber.toString(),
980
+ error: {
981
+ message: viemError.message,
982
+ name: viemError.name
983
+ },
984
+ context: {
985
+ actions: [
986
+ `invalidate-${reason}`
987
+ ],
988
+ checkpointNumber,
989
+ sender: this.getSenderAddress().toString()
990
+ }
991
+ });
803
992
  throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, {
804
993
  cause: viemError
805
994
  });
@@ -826,30 +1015,18 @@ export class SequencerPublisher {
826
1015
  }
827
1016
  }
828
1017
  /** 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
- // }
1018
+ // Anchor the simulation timestamp to the checkpoint's own slot start time
1019
+ // rather than the current L1 block timestamp, which may overshoot into the next slot if the build ran late.
1020
+ const ts = checkpoint.header.timestamp;
844
1021
  const blobFields = checkpoint.toBlobFields();
845
- const blobs = getBlobsPerL1Block(blobFields);
1022
+ const blobs = await getBlobsPerL1Block(blobFields);
846
1023
  const blobInput = getPrefixedEthBlobCommitments(blobs);
847
1024
  const args = [
848
1025
  {
849
1026
  header: checkpoint.header.toViem(),
850
1027
  archive: toHex(checkpoint.archive.root.toBuffer()),
851
1028
  oracleInput: {
852
- feeAssetPriceModifier: 0n
1029
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier
853
1030
  }
854
1031
  },
855
1032
  attestationsAndSigners.getPackedAttestations(),
@@ -884,6 +1061,28 @@ export class SequencerPublisher {
884
1061
  this.log.warn(`Skipping vote cast for payload with empty code`);
885
1062
  return false;
886
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
+ }
887
1086
  const cachedLastVote = this.lastActions[signalType];
888
1087
  this.lastActions[signalType] = slotNumber;
889
1088
  const action = signalType;
@@ -894,15 +1093,41 @@ export class SequencerPublisher {
894
1093
  signer: this.l1TxUtils.client.account?.address,
895
1094
  lastValidL2Slot: slotNumber
896
1095
  });
1096
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
897
1097
  try {
898
1098
  await this.l1TxUtils.simulate(request, {
899
1099
  time: timestamp
900
- }, [], ErrorsAbi);
1100
+ }, [], mergeAbis([
1101
+ request.abi ?? [],
1102
+ ErrorsAbi
1103
+ ]));
901
1104
  this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, {
902
1105
  request
903
1106
  });
904
1107
  } catch (err) {
905
- this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, err);
1108
+ const viemError = formatViemError(err);
1109
+ this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, viemError);
1110
+ this.backupFailedTx({
1111
+ id: keccak256(request.data),
1112
+ failureType: 'simulation',
1113
+ request: {
1114
+ to: request.to,
1115
+ data: request.data,
1116
+ value: request.value?.toString()
1117
+ },
1118
+ l1BlockNumber: l1BlockNumber.toString(),
1119
+ error: {
1120
+ message: viemError.message,
1121
+ name: viemError.name
1122
+ },
1123
+ context: {
1124
+ actions: [
1125
+ action
1126
+ ],
1127
+ slot: slotNumber,
1128
+ sender: this.getSenderAddress().toString()
1129
+ }
1130
+ });
906
1131
  // Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
907
1132
  }
908
1133
  // TODO(palla/slash): All votes (governance and slashing) should txTimeoutAt at the end of the slot.
@@ -1041,13 +1266,14 @@ export class SequencerPublisher {
1041
1266
  /** Simulates and enqueues a proposal for a checkpoint on L1 */ async enqueueProposeCheckpoint(checkpoint, attestationsAndSigners, attestationsAndSignersSignature, opts = {}) {
1042
1267
  const checkpointHeader = checkpoint.header;
1043
1268
  const blobFields = checkpoint.toBlobFields();
1044
- const blobs = getBlobsPerL1Block(blobFields);
1269
+ const blobs = await getBlobsPerL1Block(blobFields);
1045
1270
  const proposeTxArgs = {
1046
1271
  header: checkpointHeader,
1047
1272
  archive: checkpoint.archive.root.toBuffer(),
1048
1273
  blobs,
1049
1274
  attestationsAndSigners,
1050
- attestationsAndSignersSignature
1275
+ attestationsAndSignersSignature,
1276
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier
1051
1277
  };
1052
1278
  let ts;
1053
1279
  try {
@@ -1123,28 +1349,60 @@ export class SequencerPublisher {
1123
1349
  const cachedLastActionSlot = this.lastActions[action];
1124
1350
  this.lastActions[action] = slotNumber;
1125
1351
  this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
1352
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1126
1353
  let gasUsed;
1354
+ const simulateAbi = mergeAbis([
1355
+ request.abi ?? [],
1356
+ ErrorsAbi
1357
+ ]);
1127
1358
  try {
1128
1359
  ({ gasUsed } = await this.l1TxUtils.simulate(request, {
1129
1360
  time: timestamp
1130
- }, [], ErrorsAbi)); // TODO(palla/slash): Check the timestamp logic
1361
+ }, [], simulateAbi)); // TODO(palla/slash): Check the timestamp logic
1131
1362
  this.log.verbose(`Simulation for ${action} succeeded`, {
1132
1363
  ...logData,
1133
1364
  request,
1134
1365
  gasUsed
1135
1366
  });
1136
1367
  } catch (err) {
1137
- const viemError = formatViemError(err);
1368
+ const viemError = formatViemError(err, simulateAbi);
1138
1369
  this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
1370
+ this.backupFailedTx({
1371
+ id: keccak256(request.data),
1372
+ failureType: 'simulation',
1373
+ request: {
1374
+ to: request.to,
1375
+ data: request.data,
1376
+ value: request.value?.toString()
1377
+ },
1378
+ l1BlockNumber: l1BlockNumber.toString(),
1379
+ error: {
1380
+ message: viemError.message,
1381
+ name: viemError.name
1382
+ },
1383
+ context: {
1384
+ actions: [
1385
+ action
1386
+ ],
1387
+ slot: slotNumber,
1388
+ sender: this.getSenderAddress().toString()
1389
+ }
1390
+ });
1139
1391
  return false;
1140
1392
  }
1141
1393
  // We issued the simulation against the rollup contract, so we need to account for the overhead of the multicall3
1142
1394
  const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil(Number(gasUsed) * 64 / 63)));
1143
1395
  logData.gasLimit = gasLimit;
1396
+ // Store the ABI used for simulation on the request so Multicall3.forward can decode errors
1397
+ // when the tx is sent and a revert is diagnosed via simulation.
1398
+ const requestWithAbi = {
1399
+ ...request,
1400
+ abi: simulateAbi
1401
+ };
1144
1402
  this.log.debug(`Enqueuing ${action}`, logData);
1145
1403
  this.addRequest({
1146
1404
  action,
1147
- request,
1405
+ request: requestWithAbi,
1148
1406
  gasConfig: {
1149
1407
  gasLimit
1150
1408
  },
@@ -1208,10 +1466,38 @@ export class SequencerPublisher {
1208
1466
  }, {}, {
1209
1467
  blobs: encodedData.blobs.map((b)=>b.data),
1210
1468
  kzg
1211
- }).catch((err)=>{
1212
- const { message, metaMessages } = formatViemError(err);
1213
- this.log.error(`Failed to validate blobs`, message, {
1214
- metaMessages
1469
+ }).catch(async (err)=>{
1470
+ const viemError = formatViemError(err);
1471
+ this.log.error(`Failed to validate blobs`, viemError.message, {
1472
+ metaMessages: viemError.metaMessages
1473
+ });
1474
+ const validateBlobsData = encodeFunctionData({
1475
+ abi: RollupAbi,
1476
+ functionName: 'validateBlobs',
1477
+ args: [
1478
+ blobInput
1479
+ ]
1480
+ });
1481
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1482
+ this.backupFailedTx({
1483
+ id: keccak256(validateBlobsData),
1484
+ failureType: 'simulation',
1485
+ request: {
1486
+ to: this.rollupContract.address,
1487
+ data: validateBlobsData
1488
+ },
1489
+ blobData: encodedData.blobs.map((b)=>toHex(b.data)),
1490
+ l1BlockNumber: l1BlockNumber.toString(),
1491
+ error: {
1492
+ message: viemError.message,
1493
+ name: viemError.name
1494
+ },
1495
+ context: {
1496
+ actions: [
1497
+ 'validate-blobs'
1498
+ ],
1499
+ sender: this.getSenderAddress().toString()
1500
+ }
1215
1501
  });
1216
1502
  throw new Error('Failed to validate blobs');
1217
1503
  });
@@ -1222,8 +1508,7 @@ export class SequencerPublisher {
1222
1508
  header: encodedData.header.toViem(),
1223
1509
  archive: toHex(encodedData.archive),
1224
1510
  oracleInput: {
1225
- // We are currently not modifying these. See #9963
1226
- feeAssetPriceModifier: 0n
1511
+ feeAssetPriceModifier: encodedData.feeAssetPriceModifier
1227
1512
  }
1228
1513
  },
1229
1514
  encodedData.attestationsAndSigners.getPackedAttestations(),
@@ -1272,10 +1557,11 @@ export class SequencerPublisher {
1272
1557
  balance: 10n * WEI_CONST * WEI_CONST
1273
1558
  });
1274
1559
  }
1560
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1275
1561
  const simulationResult = await this.l1TxUtils.simulate({
1276
1562
  to: this.rollupContract.address,
1277
1563
  data: rollupData,
1278
- gas: SequencerPublisher.PROPOSE_GAS_GUESS,
1564
+ gas: MAX_L1_TX_LIMIT,
1279
1565
  ...this.proposerAddressForSimulation && {
1280
1566
  from: this.proposerAddressForSimulation.toString()
1281
1567
  }
@@ -1283,10 +1569,10 @@ export class SequencerPublisher {
1283
1569
  // @note we add 1n to the timestamp because geth implementation doesn't like simulation timestamp to be equal to the current block timestamp
1284
1570
  time: timestamp + 1n,
1285
1571
  // @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
1572
+ gasLimit: MAX_L1_TX_LIMIT * 2n
1287
1573
  }, stateOverrides, RollupAbi, {
1288
1574
  // @note fallback gas estimate to use if the node doesn't support simulation API
1289
- fallbackGasEstimate: SequencerPublisher.PROPOSE_GAS_GUESS
1575
+ fallbackGasEstimate: MAX_L1_TX_LIMIT
1290
1576
  }).catch((err)=>{
1291
1577
  // In fisherman mode, we expect ValidatorSelection__MissingProposerSignature since fisherman doesn't have proposer signature
1292
1578
  const viemError = formatViemError(err);
@@ -1294,11 +1580,31 @@ export class SequencerPublisher {
1294
1580
  this.log.debug(`Ignoring expected ValidatorSelection__MissingProposerSignature error in fisherman mode`);
1295
1581
  // Return a minimal simulation result with the fallback gas estimate
1296
1582
  return {
1297
- gasUsed: SequencerPublisher.PROPOSE_GAS_GUESS,
1583
+ gasUsed: MAX_L1_TX_LIMIT,
1298
1584
  logs: []
1299
1585
  };
1300
1586
  }
1301
1587
  this.log.error(`Failed to simulate propose tx`, viemError);
1588
+ this.backupFailedTx({
1589
+ id: keccak256(rollupData),
1590
+ failureType: 'simulation',
1591
+ request: {
1592
+ to: this.rollupContract.address,
1593
+ data: rollupData
1594
+ },
1595
+ l1BlockNumber: l1BlockNumber.toString(),
1596
+ error: {
1597
+ message: viemError.message,
1598
+ name: viemError.name
1599
+ },
1600
+ context: {
1601
+ actions: [
1602
+ 'propose'
1603
+ ],
1604
+ slot: Number(args[0].header.slotNumber),
1605
+ sender: this.getSenderAddress().toString()
1606
+ }
1607
+ });
1302
1608
  throw err;
1303
1609
  });
1304
1610
  return {