@aztec/sequencer-client 0.0.1-commit.aada20e3 → 0.0.1-commit.b2a5d0dd1

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 (103) hide show
  1. package/dest/client/sequencer-client.d.ts +17 -7
  2. package/dest/client/sequencer-client.d.ts.map +1 -1
  3. package/dest/client/sequencer-client.js +63 -30
  4. package/dest/config.d.ts +26 -7
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +52 -36
  7. package/dest/global_variable_builder/fee_predictor.d.ts +37 -0
  8. package/dest/global_variable_builder/fee_predictor.d.ts.map +1 -0
  9. package/dest/global_variable_builder/fee_predictor.js +128 -0
  10. package/dest/global_variable_builder/fee_provider.d.ts +21 -0
  11. package/dest/global_variable_builder/fee_provider.d.ts.map +1 -0
  12. package/dest/global_variable_builder/fee_provider.js +58 -0
  13. package/dest/global_variable_builder/global_builder.d.ts +15 -16
  14. package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
  15. package/dest/global_variable_builder/global_builder.js +16 -50
  16. package/dest/global_variable_builder/index.d.ts +4 -2
  17. package/dest/global_variable_builder/index.d.ts.map +1 -1
  18. package/dest/global_variable_builder/index.js +2 -0
  19. package/dest/publisher/config.d.ts +47 -17
  20. package/dest/publisher/config.d.ts.map +1 -1
  21. package/dest/publisher/config.js +121 -42
  22. package/dest/publisher/index.d.ts +2 -1
  23. package/dest/publisher/index.d.ts.map +1 -1
  24. package/dest/publisher/l1_tx_failed_store/factory.d.ts +11 -0
  25. package/dest/publisher/l1_tx_failed_store/factory.d.ts.map +1 -0
  26. package/dest/publisher/l1_tx_failed_store/factory.js +22 -0
  27. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts +58 -0
  28. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -0
  29. package/dest/publisher/l1_tx_failed_store/failed_tx_store.js +1 -0
  30. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts +15 -0
  31. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts.map +1 -0
  32. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.js +34 -0
  33. package/dest/publisher/l1_tx_failed_store/index.d.ts +4 -0
  34. package/dest/publisher/l1_tx_failed_store/index.d.ts.map +1 -0
  35. package/dest/publisher/l1_tx_failed_store/index.js +2 -0
  36. package/dest/publisher/sequencer-publisher-factory.d.ts +11 -5
  37. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  38. package/dest/publisher/sequencer-publisher-factory.js +27 -3
  39. package/dest/publisher/sequencer-publisher.d.ts +75 -48
  40. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  41. package/dest/publisher/sequencer-publisher.js +432 -139
  42. package/dest/sequencer/chain_state_overrides.d.ts +25 -0
  43. package/dest/sequencer/chain_state_overrides.d.ts.map +1 -0
  44. package/dest/sequencer/chain_state_overrides.js +39 -0
  45. package/dest/sequencer/checkpoint_proposal_job.d.ts +40 -10
  46. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  47. package/dest/sequencer/checkpoint_proposal_job.js +527 -215
  48. package/dest/sequencer/checkpoint_proposal_job_metrics.d.ts +34 -0
  49. package/dest/sequencer/checkpoint_proposal_job_metrics.d.ts.map +1 -0
  50. package/dest/sequencer/checkpoint_proposal_job_metrics.js +72 -0
  51. package/dest/sequencer/checkpoint_voter.d.ts +1 -2
  52. package/dest/sequencer/checkpoint_voter.d.ts.map +1 -1
  53. package/dest/sequencer/checkpoint_voter.js +2 -5
  54. package/dest/sequencer/events.d.ts +7 -1
  55. package/dest/sequencer/events.d.ts.map +1 -1
  56. package/dest/sequencer/metrics.d.ts +21 -8
  57. package/dest/sequencer/metrics.d.ts.map +1 -1
  58. package/dest/sequencer/metrics.js +110 -21
  59. package/dest/sequencer/sequencer.d.ts +43 -17
  60. package/dest/sequencer/sequencer.d.ts.map +1 -1
  61. package/dest/sequencer/sequencer.js +158 -96
  62. package/dest/sequencer/timetable.d.ts +17 -6
  63. package/dest/sequencer/timetable.d.ts.map +1 -1
  64. package/dest/sequencer/timetable.js +51 -46
  65. package/dest/sequencer/types.d.ts +2 -2
  66. package/dest/sequencer/types.d.ts.map +1 -1
  67. package/dest/test/index.d.ts +3 -5
  68. package/dest/test/index.d.ts.map +1 -1
  69. package/dest/test/mock_checkpoint_builder.d.ts +11 -11
  70. package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
  71. package/dest/test/mock_checkpoint_builder.js +45 -34
  72. package/dest/test/utils.d.ts +3 -3
  73. package/dest/test/utils.d.ts.map +1 -1
  74. package/dest/test/utils.js +5 -4
  75. package/package.json +27 -28
  76. package/src/client/sequencer-client.ts +81 -30
  77. package/src/config.ts +70 -46
  78. package/src/global_variable_builder/README.md +44 -0
  79. package/src/global_variable_builder/fee_predictor.ts +172 -0
  80. package/src/global_variable_builder/fee_provider.ts +75 -0
  81. package/src/global_variable_builder/global_builder.ts +27 -63
  82. package/src/global_variable_builder/index.ts +3 -1
  83. package/src/publisher/config.ts +157 -45
  84. package/src/publisher/index.ts +3 -0
  85. package/src/publisher/l1_tx_failed_store/factory.ts +32 -0
  86. package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +57 -0
  87. package/src/publisher/l1_tx_failed_store/file_store_failed_tx_store.ts +46 -0
  88. package/src/publisher/l1_tx_failed_store/index.ts +3 -0
  89. package/src/publisher/sequencer-publisher-factory.ts +38 -9
  90. package/src/publisher/sequencer-publisher.ts +499 -198
  91. package/src/sequencer/README.md +83 -13
  92. package/src/sequencer/chain_state_overrides.ts +87 -0
  93. package/src/sequencer/checkpoint_proposal_job.ts +672 -236
  94. package/src/sequencer/checkpoint_proposal_job_metrics.ts +128 -0
  95. package/src/sequencer/checkpoint_voter.ts +1 -12
  96. package/src/sequencer/events.ts +6 -1
  97. package/src/sequencer/metrics.ts +124 -26
  98. package/src/sequencer/sequencer.ts +227 -108
  99. package/src/sequencer/timetable.ts +70 -57
  100. package/src/sequencer/types.ts +1 -1
  101. package/src/test/index.ts +2 -4
  102. package/src/test/mock_checkpoint_builder.ts +63 -49
  103. package/src/test/utils.ts +5 -2
@@ -372,33 +372,35 @@ 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, SimulationOverridesBuilder, buildSimulationOverridesStateOverride } 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
- import { toHex as toPaddedHex } from '@aztec/foundation/bigint-buffer';
381
380
  import { CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
381
+ import { trimmedBytesLength } from '@aztec/foundation/buffer';
382
382
  import { pick } from '@aztec/foundation/collection';
383
+ import { TimeoutError } from '@aztec/foundation/error';
383
384
  import { EthAddress } from '@aztec/foundation/eth-address';
384
385
  import { Signature } from '@aztec/foundation/eth-signature';
385
386
  import { createLogger } from '@aztec/foundation/log';
387
+ import { makeBackoff, retry } from '@aztec/foundation/retry';
388
+ import { InterruptibleSleep } from '@aztec/foundation/sleep';
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',
396
401
  'invalidate-by-insufficient-attestations',
397
402
  'propose',
398
403
  'governance-signal',
399
- 'empire-slashing-signal',
400
- 'create-empire-payload',
401
- 'execute-empire-payload',
402
404
  'vote-offenses',
403
405
  'execute-slash'
404
406
  ];
@@ -429,15 +431,22 @@ export class SequencerPublisher {
429
431
  interrupted;
430
432
  metrics;
431
433
  epochCache;
434
+ failedTxStore;
432
435
  governanceLog;
433
436
  slashingLog;
434
437
  lastActions;
435
438
  isPayloadEmptyCache;
439
+ payloadProposedCache;
436
440
  log;
437
441
  ethereumSlotDuration;
442
+ aztecSlotDuration;
443
+ /** Date provider for wall-clock time. */ dateProvider;
438
444
  blobClient;
439
445
  /** Address to use for simulations in fisherman mode (actual proposer's address) */ proposerAddressForSimulation;
446
+ /** Optional callback to obtain a replacement publisher when the current one fails to send. */ getNextPublisher;
440
447
  /** L1 fee analyzer for fisherman mode */ l1FeeAnalyzer;
448
+ /** Fee asset price oracle for computing price modifiers from Uniswap V4 */ feeAssetPriceOracle;
449
+ /** Interruptible sleep used by sendRequestsAt to wait until a target timestamp. */ interruptibleSleep;
441
450
  // A CALL to a cold address is 2700 gas
442
451
  static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
443
452
  // Gas report for VotingWithSigTest shows a max gas of 100k, but we've seen it cost 700k+ in testnet
@@ -446,7 +455,6 @@ export class SequencerPublisher {
446
455
  rollupContract;
447
456
  govProposerContract;
448
457
  slashingProposerContract;
449
- slashFactoryContract;
450
458
  tracer;
451
459
  requests;
452
460
  constructor(config, deps){
@@ -456,16 +464,22 @@ export class SequencerPublisher {
456
464
  this.slashingLog = createLogger('sequencer:publisher:slashing');
457
465
  this.lastActions = {};
458
466
  this.isPayloadEmptyCache = new Map();
467
+ this.payloadProposedCache = new Set();
468
+ this.interruptibleSleep = new InterruptibleSleep();
459
469
  this.requests = [];
460
470
  this.log = deps.log ?? createLogger('sequencer:publisher');
461
471
  this.ethereumSlotDuration = BigInt(config.ethereumSlotDuration);
472
+ this.aztecSlotDuration = BigInt(config.aztecSlotDuration);
473
+ this.dateProvider = deps.dateProvider;
462
474
  this.epochCache = deps.epochCache;
463
475
  this.lastActions = deps.lastActions;
464
476
  this.blobClient = deps.blobClient;
477
+ this.dateProvider = deps.dateProvider;
465
478
  const telemetry = deps.telemetry ?? getTelemetryClient();
466
479
  this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
467
480
  this.tracer = telemetry.getTracer('SequencerPublisher');
468
481
  this.l1TxUtils = deps.l1TxUtils;
482
+ this.getNextPublisher = deps.getNextPublisher;
469
483
  this.rollupContract = deps.rollupContract;
470
484
  this.govProposerContract = deps.governanceProposerContract;
471
485
  this.slashingProposerContract = deps.slashingProposerContract;
@@ -474,15 +488,40 @@ export class SequencerPublisher {
474
488
  const newSlashingProposer = await this.rollupContract.getSlashingProposer();
475
489
  this.slashingProposerContract = newSlashingProposer;
476
490
  });
477
- this.slashFactoryContract = deps.slashFactoryContract;
478
491
  // Initialize L1 fee analyzer for fisherman mode
479
492
  if (config.fishermanMode) {
480
493
  this.l1FeeAnalyzer = new L1FeeAnalyzer(this.l1TxUtils.client, deps.dateProvider, createLogger('sequencer:publisher:fee-analyzer'));
481
494
  }
495
+ // Initialize fee asset price oracle
496
+ this.feeAssetPriceOracle = new FeeAssetPriceOracle(this.l1TxUtils.client, this.rollupContract, createLogger('sequencer:publisher:price-oracle'));
497
+ // Initialize failed L1 tx store (optional, for test networks)
498
+ this.failedTxStore = createL1TxFailedStore(config.l1TxFailedStore, this.log);
499
+ }
500
+ /**
501
+ * Backs up a failed L1 transaction to the configured store for debugging.
502
+ * Does nothing if no store is configured.
503
+ */ backupFailedTx(failedTx) {
504
+ if (!this.failedTxStore) {
505
+ return;
506
+ }
507
+ const tx = {
508
+ ...failedTx,
509
+ timestamp: Date.now()
510
+ };
511
+ // Fire and forget - don't block on backup
512
+ void this.failedTxStore.then((store)=>store?.saveFailedTx(tx)).catch((err)=>{
513
+ this.log.warn(`Failed to backup failed L1 tx to store`, err);
514
+ });
482
515
  }
483
516
  getRollupContract() {
484
517
  return this.rollupContract;
485
518
  }
519
+ /**
520
+ * Gets the fee asset price modifier from the oracle.
521
+ * Returns 0n if the oracle query fails.
522
+ */ getFeeAssetPriceModifier() {
523
+ return this.feeAssetPriceOracle.computePriceModifier();
524
+ }
486
525
  getSenderAddress() {
487
526
  return this.l1TxUtils.getSenderAddress();
488
527
  }
@@ -501,7 +540,7 @@ export class SequencerPublisher {
501
540
  this.requests.push(request);
502
541
  }
503
542
  getCurrentL2Slot() {
504
- return this.epochCache.getEpochAndSlotNow().slot;
543
+ return this.epochCache.getSlotNow();
505
544
  }
506
545
  /**
507
546
  * Clears all pending requests without sending them.
@@ -590,8 +629,8 @@ export class SequencerPublisher {
590
629
  // @note - we can only have one blob config per bundle
591
630
  // find requests with gas and blob configs
592
631
  // 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);
632
+ const gasConfigs = validRequests.filter((request)=>request.gasConfig).map((request)=>request.gasConfig);
633
+ const blobConfigs = validRequests.filter((request)=>request.blobConfig).map((request)=>request.blobConfig);
595
634
  if (blobConfigs.length > 1) {
596
635
  throw new Error('Multiple blob configs found');
597
636
  }
@@ -618,12 +657,34 @@ export class SequencerPublisher {
618
657
  // This ensures the committee gets precomputed correctly
619
658
  validRequests.sort((a, b)=>compareActions(a.action, b.action));
620
659
  try {
660
+ // Capture context for failed tx backup before sending
661
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
662
+ const multicallData = encodeFunctionData({
663
+ abi: multicall3Abi,
664
+ functionName: 'aggregate3',
665
+ args: [
666
+ validRequests.map((r)=>({
667
+ target: r.request.to,
668
+ callData: r.request.data,
669
+ allowFailure: true
670
+ }))
671
+ ]
672
+ });
673
+ const blobDataHex = blobConfig?.blobs?.map((b)=>toHex(b));
674
+ const txContext = {
675
+ multicallData,
676
+ blobData: blobDataHex,
677
+ l1BlockNumber
678
+ };
621
679
  this.log.debug('Forwarding transactions', {
622
680
  validRequests: validRequests.map((request)=>request.action),
623
681
  txConfig
624
682
  });
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);
683
+ const result = await this.forwardWithPublisherRotation(validRequests, txConfig, blobConfig);
684
+ if (result === undefined) {
685
+ return undefined;
686
+ }
687
+ const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(validRequests, result, txContext);
627
688
  return {
628
689
  result,
629
690
  expiredActions,
@@ -643,17 +704,118 @@ export class SequencerPublisher {
643
704
  }
644
705
  }
645
706
  }
646
- callbackBundledTransactions(requests, result) {
707
+ /**
708
+ * Forwards transactions via Multicall3, rotating to the next available publisher if a send
709
+ * failure occurs (i.e. the tx never reached the chain).
710
+ * On-chain reverts and simulation errors are returned as-is without rotation.
711
+ */ async forwardWithPublisherRotation(validRequests, txConfig, blobConfig) {
712
+ const triedAddresses = [];
713
+ let currentPublisher = this.l1TxUtils;
714
+ while(true){
715
+ triedAddresses.push(currentPublisher.getSenderAddress());
716
+ try {
717
+ const result = await Multicall3.forward(validRequests.map((r)=>r.request), currentPublisher, txConfig, blobConfig, this.rollupContract.address, this.log);
718
+ this.l1TxUtils = currentPublisher;
719
+ return result;
720
+ } catch (err) {
721
+ if (err instanceof TimeoutError) {
722
+ throw err;
723
+ }
724
+ const viemError = formatViemError(err);
725
+ if (!this.getNextPublisher) {
726
+ this.log.error('Failed to publish bundled transactions', viemError);
727
+ return undefined;
728
+ }
729
+ this.log.warn(`Publisher ${currentPublisher.getSenderAddress()} failed to send, rotating to next publisher`, viemError);
730
+ const nextPublisher = await this.getNextPublisher([
731
+ ...triedAddresses
732
+ ]);
733
+ if (!nextPublisher) {
734
+ this.log.error('All available publishers exhausted, failed to publish bundled transactions');
735
+ return undefined;
736
+ }
737
+ currentPublisher = nextPublisher;
738
+ }
739
+ }
740
+ }
741
+ /*
742
+ * Schedules sending all enqueued requests at (or after) the given timestamp.
743
+ * Uses InterruptibleSleep so it can be cancelled via interrupt().
744
+ * Returns the promise for the L1 response (caller should NOT await this in the work loop).
745
+ */ async sendRequestsAt(submitAfter) {
746
+ const ms = submitAfter.getTime() - this.dateProvider.now();
747
+ if (ms > 0) {
748
+ this.log.debug(`Sleeping ${ms}ms before sending requests`, {
749
+ submitAfter
750
+ });
751
+ await this.interruptibleSleep.sleep(ms);
752
+ }
753
+ if (this.interrupted) {
754
+ return undefined;
755
+ }
756
+ // Re-validate enqueued requests after the sleep (state may have changed, e.g. prune or L1 reorg)
757
+ const validRequests = [];
758
+ for (const request of this.requests){
759
+ if (!request.preCheck) {
760
+ validRequests.push(request);
761
+ continue;
762
+ }
763
+ try {
764
+ await request.preCheck();
765
+ validRequests.push(request);
766
+ } catch (err) {
767
+ this.log.warn(`Pre-send validation failed for ${request.action}, discarding request`, err);
768
+ }
769
+ }
770
+ this.requests = validRequests;
771
+ if (this.requests.length === 0) {
772
+ return undefined;
773
+ }
774
+ return this.sendRequests();
775
+ }
776
+ callbackBundledTransactions(requests, result, txContext) {
647
777
  const actionsListStr = requests.map((r)=>r.action).join(', ');
648
778
  if (result instanceof FormattedViemError) {
649
779
  this.log.error(`Failed to publish bundled transactions (${actionsListStr})`, result);
780
+ this.backupFailedTx({
781
+ id: keccak256(txContext.multicallData),
782
+ failureType: 'send-error',
783
+ request: {
784
+ to: MULTI_CALL_3_ADDRESS,
785
+ data: txContext.multicallData
786
+ },
787
+ blobData: txContext.blobData,
788
+ l1BlockNumber: txContext.l1BlockNumber.toString(),
789
+ error: {
790
+ message: result.message,
791
+ name: result.name
792
+ },
793
+ context: {
794
+ actions: requests.map((r)=>r.action),
795
+ requests: requests.map((r)=>({
796
+ action: r.action,
797
+ to: r.request.to,
798
+ data: r.request.data
799
+ })),
800
+ sender: this.getSenderAddress().toString()
801
+ }
802
+ });
650
803
  return {
651
804
  failedActions: requests.map((r)=>r.action)
652
805
  };
653
806
  } else {
654
807
  this.log.verbose(`Published bundled transactions (${actionsListStr})`, {
655
808
  result,
656
- requests
809
+ requests: requests.map((r)=>({
810
+ ...r,
811
+ // Avoid logging large blob data
812
+ blobConfig: r.blobConfig ? {
813
+ ...r.blobConfig,
814
+ blobs: r.blobConfig.blobs.map((b)=>({
815
+ size: trimmedBytesLength(b)
816
+ }))
817
+ } : undefined
818
+ }))
657
819
  });
658
820
  const successfulActions = [];
659
821
  const failedActions = [];
@@ -664,6 +826,37 @@ export class SequencerPublisher {
664
826
  failedActions.push(request.action);
665
827
  }
666
828
  }
829
+ // Single backup for the whole reverted tx
830
+ if (failedActions.length > 0 && result?.receipt?.status === 'reverted') {
831
+ this.backupFailedTx({
832
+ id: result.receipt.transactionHash,
833
+ failureType: 'revert',
834
+ request: {
835
+ to: MULTI_CALL_3_ADDRESS,
836
+ data: txContext.multicallData
837
+ },
838
+ blobData: txContext.blobData,
839
+ l1BlockNumber: result.receipt.blockNumber.toString(),
840
+ receipt: {
841
+ transactionHash: result.receipt.transactionHash,
842
+ blockNumber: result.receipt.blockNumber.toString(),
843
+ gasUsed: (result.receipt.gasUsed ?? 0n).toString(),
844
+ status: 'reverted'
845
+ },
846
+ error: {
847
+ message: result.errorMsg ?? 'Transaction reverted'
848
+ },
849
+ context: {
850
+ actions: failedActions,
851
+ requests: requests.filter((r)=>failedActions.includes(r.action)).map((r)=>({
852
+ action: r.action,
853
+ to: r.request.to,
854
+ data: r.request.data
855
+ })),
856
+ sender: this.getSenderAddress().toString()
857
+ }
858
+ });
859
+ }
667
860
  return {
668
861
  successfulActions,
669
862
  failedActions
@@ -671,19 +864,20 @@ export class SequencerPublisher {
671
864
  }
672
865
  }
673
866
  /**
674
- * @notice Will call `canProposeAtNextEthBlock` to make sure that it is possible to propose
867
+ * @notice Will call `canProposeAt` to make sure that it is possible to propose
675
868
  * @param tipArchive - The archive to check
676
869
  * @returns The slot and block number if it is possible to propose, undefined otherwise
677
- */ canProposeAtNextEthBlock(tipArchive, msgSender, opts = {}) {
870
+ */ async canProposeAt(tipArchive, msgSender, simulationOverridesPlan) {
678
871
  // TODO: #14291 - should loop through multiple keys to check if any of them can propose
679
872
  const ignoredErrors = [
680
873
  'SlotAlreadyInChain',
681
874
  'InvalidProposer',
682
875
  'InvalidArchive'
683
876
  ];
684
- return this.rollupContract.canProposeAtNextEthBlock(tipArchive.toBuffer(), msgSender.toString(), Number(this.ethereumSlotDuration), {
685
- forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber
686
- }).catch((err)=>{
877
+ const pipelined = this.epochCache.isProposerPipeliningEnabled();
878
+ const slotOffset = pipelined ? this.aztecSlotDuration : 0n;
879
+ const nextL1SlotTs = this.getNextL1SlotTimestamp() + slotOffset;
880
+ return this.rollupContract.canProposeAt(tipArchive.toBuffer(), msgSender.toString(), nextL1SlotTs, await buildSimulationOverridesStateOverride(this.rollupContract, simulationOverridesPlan)).catch((err)=>{
687
881
  if (err instanceof FormattedViemError && ignoredErrors.find((e)=>err.message.includes(e))) {
688
882
  this.log.warn(`Failed canProposeAtTime check with ${ignoredErrors.find((e)=>err.message.includes(e))}`, {
689
883
  error: err.message
@@ -699,7 +893,7 @@ export class SequencerPublisher {
699
893
  * @dev This is a convenience function that can be used by the sequencer to validate a "partial" header.
700
894
  * It will throw if the block header is invalid.
701
895
  * @param header - The block header to validate
702
- */ async validateBlockHeader(header, opts) {
896
+ */ async validateBlockHeader(header, simulationOverridesPlan) {
703
897
  const flags = {
704
898
  ignoreDA: true,
705
899
  ignoreSignatures: true
@@ -713,8 +907,8 @@ export class SequencerPublisher {
713
907
  header.blobsHash.toString(),
714
908
  flags
715
909
  ];
716
- const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
717
- const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(opts?.forcePendingCheckpointNumber);
910
+ const ts = this.getSimulationTimestamp(header.slotNumber);
911
+ const stateOverrides = await buildSimulationOverridesStateOverride(this.rollupContract, simulationOverridesPlan);
718
912
  let balance = 0n;
719
913
  if (this.config.fishermanMode) {
720
914
  // In fisherman mode, we can't know where the proposer is publishing from
@@ -736,7 +930,7 @@ export class SequencerPublisher {
736
930
  }),
737
931
  from: MULTI_CALL_3_ADDRESS
738
932
  }, {
739
- time: ts + 1n
933
+ time: ts
740
934
  }, stateOverrides);
741
935
  this.log.debug(`Simulated validateHeader`);
742
936
  }
@@ -766,6 +960,7 @@ export class SequencerPublisher {
766
960
  ...logData,
767
961
  request
768
962
  });
963
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
769
964
  try {
770
965
  const { gasUsed } = await this.l1TxUtils.simulate(request, undefined, undefined, mergeAbis([
771
966
  request.abi ?? [],
@@ -781,6 +976,7 @@ export class SequencerPublisher {
781
976
  gasUsed,
782
977
  checkpointNumber,
783
978
  forcePendingCheckpointNumber: CheckpointNumber(checkpointNumber - 1),
979
+ lastArchive: validationResult.checkpoint.lastArchive,
784
980
  reason
785
981
  };
786
982
  } catch (err) {
@@ -793,8 +989,8 @@ export class SequencerPublisher {
793
989
  request,
794
990
  error: viemError.message
795
991
  });
796
- const latestPendingCheckpointNumber = await this.rollupContract.getCheckpointNumber();
797
- if (latestPendingCheckpointNumber < checkpointNumber) {
992
+ const latestProposedCheckpointNumber = await this.rollupContract.getCheckpointNumber();
993
+ if (latestProposedCheckpointNumber < checkpointNumber) {
798
994
  this.log.verbose(`Checkpoint ${checkpointNumber} has already been invalidated`, {
799
995
  ...logData
800
996
  });
@@ -808,6 +1004,27 @@ export class SequencerPublisher {
808
1004
  }
809
1005
  // Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
810
1006
  this.log.error(`Simulation for invalidate checkpoint ${checkpointNumber} failed`, viemError, logData);
1007
+ this.backupFailedTx({
1008
+ id: keccak256(request.data),
1009
+ failureType: 'simulation',
1010
+ request: {
1011
+ to: request.to,
1012
+ data: request.data,
1013
+ value: request.value?.toString()
1014
+ },
1015
+ l1BlockNumber: l1BlockNumber.toString(),
1016
+ error: {
1017
+ message: viemError.message,
1018
+ name: viemError.name
1019
+ },
1020
+ context: {
1021
+ actions: [
1022
+ `invalidate-${reason}`
1023
+ ],
1024
+ checkpointNumber,
1025
+ sender: this.getSenderAddress().toString()
1026
+ }
1027
+ });
811
1028
  throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, {
812
1029
  cause: viemError
813
1030
  });
@@ -833,31 +1050,16 @@ export class SequencerPublisher {
833
1050
  throw new Error(`Unknown reason for invalidation`);
834
1051
  }
835
1052
  }
836
- /** 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
- // }
1053
+ /** Simulates `propose` to make sure that the checkpoint is valid for submission */ async validateCheckpointForSubmission(checkpoint, attestationsAndSigners, attestationsAndSignersSignature, simulationOverridesPlan) {
852
1054
  const blobFields = checkpoint.toBlobFields();
853
- const blobs = getBlobsPerL1Block(blobFields);
1055
+ const blobs = await getBlobsPerL1Block(blobFields);
854
1056
  const blobInput = getPrefixedEthBlobCommitments(blobs);
855
1057
  const args = [
856
1058
  {
857
1059
  header: checkpoint.header.toViem(),
858
1060
  archive: toHex(checkpoint.archive.root.toBuffer()),
859
1061
  oracleInput: {
860
- feeAssetPriceModifier: 0n
1062
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier
861
1063
  }
862
1064
  },
863
1065
  attestationsAndSigners.getPackedAttestations(),
@@ -865,10 +1067,9 @@ export class SequencerPublisher {
865
1067
  attestationsAndSignersSignature.toViemSignature(),
866
1068
  blobInput
867
1069
  ];
868
- await this.simulateProposeTx(args, ts, options);
869
- return ts;
1070
+ await this.simulateProposeTx(args, simulationOverridesPlan);
870
1071
  }
871
- async enqueueCastSignalHelper(slotNumber, timestamp, signalType, payload, base, signerAddress, signer) {
1072
+ async enqueueCastSignalHelper(slotNumber, signalType, payload, base, signerAddress, signer) {
872
1073
  if (this.lastActions[signalType] && this.lastActions[signalType] === slotNumber) {
873
1074
  this.log.debug(`Skipping duplicate vote cast signal ${signalType} for slot ${slotNumber}`);
874
1075
  return false;
@@ -892,6 +1093,28 @@ export class SequencerPublisher {
892
1093
  this.log.warn(`Skipping vote cast for payload with empty code`);
893
1094
  return false;
894
1095
  }
1096
+ // Check if payload was already submitted to governance
1097
+ const cacheKey = payload.toString();
1098
+ if (!this.payloadProposedCache.has(cacheKey)) {
1099
+ try {
1100
+ const l1StartBlock = await this.rollupContract.getL1StartBlock();
1101
+ const proposed = await retry(()=>base.hasPayloadBeenProposed(payload.toString(), l1StartBlock), 'Check if payload was proposed', makeBackoff([
1102
+ 0,
1103
+ 1,
1104
+ 2
1105
+ ]), this.log, true);
1106
+ if (proposed) {
1107
+ this.payloadProposedCache.add(cacheKey);
1108
+ }
1109
+ } catch (err) {
1110
+ this.log.warn(`Failed to check if payload ${payload} was proposed after retries, skipping signal`, err);
1111
+ return false;
1112
+ }
1113
+ }
1114
+ if (this.payloadProposedCache.has(cacheKey)) {
1115
+ this.log.info(`Payload ${payload} was already proposed to governance, stopping signals`);
1116
+ return false;
1117
+ }
895
1118
  const cachedLastVote = this.lastActions[signalType];
896
1119
  this.lastActions[signalType] = slotNumber;
897
1120
  const action = signalType;
@@ -902,6 +1125,8 @@ export class SequencerPublisher {
902
1125
  signer: this.l1TxUtils.client.account?.address,
903
1126
  lastValidL2Slot: slotNumber
904
1127
  });
1128
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1129
+ const timestamp = this.getSimulationTimestamp(slotNumber);
905
1130
  try {
906
1131
  await this.l1TxUtils.simulate(request, {
907
1132
  time: timestamp
@@ -913,7 +1138,32 @@ export class SequencerPublisher {
913
1138
  request
914
1139
  });
915
1140
  } catch (err) {
916
- this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, err);
1141
+ const viemError = formatViemError(err);
1142
+ this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, viemError, {
1143
+ simulationTimestamp: timestamp,
1144
+ l1BlockNumber
1145
+ });
1146
+ this.backupFailedTx({
1147
+ id: keccak256(request.data),
1148
+ failureType: 'simulation',
1149
+ request: {
1150
+ to: request.to,
1151
+ data: request.data,
1152
+ value: request.value?.toString()
1153
+ },
1154
+ l1BlockNumber: l1BlockNumber.toString(),
1155
+ error: {
1156
+ message: viemError.message,
1157
+ name: viemError.name
1158
+ },
1159
+ context: {
1160
+ actions: [
1161
+ action
1162
+ ],
1163
+ slot: slotNumber,
1164
+ sender: this.getSenderAddress().toString()
1165
+ }
1166
+ });
917
1167
  // Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
918
1168
  }
919
1169
  // TODO(palla/slash): All votes (governance and slashing) should txTimeoutAt at the end of the slot.
@@ -957,55 +1207,17 @@ export class SequencerPublisher {
957
1207
  /**
958
1208
  * Enqueues a governance castSignal transaction to cast a signal for a given slot number.
959
1209
  * @param slotNumber - The slot number to cast a signal for.
960
- * @param timestamp - The timestamp of the slot to cast a signal for.
961
1210
  * @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);
1211
+ */ enqueueGovernanceCastSignal(governancePayload, slotNumber, signerAddress, signer) {
1212
+ return this.enqueueCastSignalHelper(slotNumber, 'governance-signal', governancePayload, this.govProposerContract, signerAddress, signer);
964
1213
  }
965
- /** Enqueues all slashing actions as returned by the slasher client. */ async enqueueSlashingActions(actions, slotNumber, timestamp, signerAddress, signer) {
1214
+ /** Enqueues all slashing actions as returned by the slasher client. */ async enqueueSlashingActions(actions, slotNumber, signerAddress, signer) {
966
1215
  if (actions.length === 0) {
967
1216
  this.log.debug(`No slashing actions to enqueue for slot ${slotNumber}`);
968
1217
  return false;
969
1218
  }
970
1219
  for (const action of actions){
971
1220
  switch(action.type){
972
- case 'vote-empire-payload':
973
- {
974
- if (this.slashingProposerContract?.type !== 'empire') {
975
- this.log.error('Cannot vote for empire payload on non-empire slashing contract');
976
- break;
977
- }
978
- this.log.debug(`Enqueuing slashing vote for payload ${action.payload} at slot ${slotNumber}`, {
979
- signerAddress
980
- });
981
- await this.enqueueCastSignalHelper(slotNumber, timestamp, 'empire-slashing-signal', action.payload, this.slashingProposerContract, signerAddress, signer);
982
- break;
983
- }
984
- case 'create-empire-payload':
985
- {
986
- this.log.debug(`Enqueuing slashing create payload at slot ${slotNumber}`, {
987
- slotNumber,
988
- signerAddress
989
- });
990
- const request = this.slashFactoryContract.buildCreatePayloadRequest(action.data);
991
- await this.simulateAndEnqueueRequest('create-empire-payload', request, (receipt)=>!!this.slashFactoryContract.tryExtractSlashPayloadCreatedEvent(receipt.logs), slotNumber, timestamp);
992
- break;
993
- }
994
- case 'execute-empire-payload':
995
- {
996
- this.log.debug(`Enqueuing slashing execute payload at slot ${slotNumber}`, {
997
- slotNumber,
998
- signerAddress
999
- });
1000
- if (this.slashingProposerContract?.type !== 'empire') {
1001
- this.log.error('Cannot execute slashing payload on non-empire slashing contract');
1002
- return false;
1003
- }
1004
- const empireSlashingProposer = this.slashingProposerContract;
1005
- const request = empireSlashingProposer.buildExecuteRoundRequest(action.round);
1006
- await this.simulateAndEnqueueRequest('execute-empire-payload', request, (receipt)=>!!empireSlashingProposer.tryExtractPayloadSubmittedEvent(receipt.logs), slotNumber, timestamp);
1007
- break;
1008
- }
1009
1221
  case 'vote-offenses':
1010
1222
  {
1011
1223
  this.log.debug(`Enqueuing slashing vote for ${action.votes.length} votes at slot ${slotNumber}`, {
@@ -1014,14 +1226,13 @@ export class SequencerPublisher {
1014
1226
  votesCount: action.votes.length,
1015
1227
  signerAddress
1016
1228
  });
1017
- if (this.slashingProposerContract?.type !== 'tally') {
1018
- this.log.error('Cannot vote for slashing offenses on non-tally slashing contract');
1229
+ if (!this.slashingProposerContract) {
1230
+ this.log.error('No slashing proposer contract available');
1019
1231
  return false;
1020
1232
  }
1021
- const tallySlashingProposer = this.slashingProposerContract;
1022
1233
  const votes = bufferToHex(encodeSlashConsensusVotes(action.votes));
1023
- const request = await tallySlashingProposer.buildVoteRequestFromSigner(votes, slotNumber, signer);
1024
- await this.simulateAndEnqueueRequest('vote-offenses', request, (receipt)=>!!tallySlashingProposer.tryExtractVoteCastEvent(receipt.logs), slotNumber, timestamp);
1234
+ const request = await this.slashingProposerContract.buildVoteRequestFromSigner(votes, slotNumber, signer);
1235
+ await this.simulateAndEnqueueRequest('vote-offenses', request, (receipt)=>!!this.slashingProposerContract.tryExtractVoteCastEvent(receipt.logs), slotNumber);
1025
1236
  break;
1026
1237
  }
1027
1238
  case 'execute-slash':
@@ -1031,13 +1242,12 @@ export class SequencerPublisher {
1031
1242
  round: action.round,
1032
1243
  signerAddress
1033
1244
  });
1034
- if (this.slashingProposerContract?.type !== 'tally') {
1035
- this.log.error('Cannot execute slashing offenses on non-tally slashing contract');
1245
+ if (!this.slashingProposerContract) {
1246
+ this.log.error('No slashing proposer contract available');
1036
1247
  return false;
1037
1248
  }
1038
- const tallySlashingProposer = this.slashingProposerContract;
1039
- const request = tallySlashingProposer.buildExecuteRoundRequest(action.round, action.committees);
1040
- await this.simulateAndEnqueueRequest('execute-slash', request, (receipt)=>!!tallySlashingProposer.tryExtractRoundExecutedEvent(receipt.logs), slotNumber, timestamp);
1249
+ const executeRequest = this.slashingProposerContract.buildExecuteRoundRequest(action.round, action.committees);
1250
+ await this.simulateAndEnqueueRequest('execute-slash', executeRequest, (receipt)=>!!this.slashingProposerContract.tryExtractRoundExecutedEvent(receipt.logs), slotNumber);
1041
1251
  break;
1042
1252
  }
1043
1253
  default:
@@ -1052,35 +1262,49 @@ export class SequencerPublisher {
1052
1262
  /** Simulates and enqueues a proposal for a checkpoint on L1 */ async enqueueProposeCheckpoint(checkpoint, attestationsAndSigners, attestationsAndSignersSignature, opts = {}) {
1053
1263
  const checkpointHeader = checkpoint.header;
1054
1264
  const blobFields = checkpoint.toBlobFields();
1055
- const blobs = getBlobsPerL1Block(blobFields);
1265
+ const blobs = await getBlobsPerL1Block(blobFields);
1056
1266
  const proposeTxArgs = {
1057
1267
  header: checkpointHeader,
1058
1268
  archive: checkpoint.archive.root.toBuffer(),
1059
1269
  blobs,
1060
1270
  attestationsAndSigners,
1061
- attestationsAndSignersSignature
1271
+ attestationsAndSignersSignature,
1272
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier
1062
1273
  };
1063
- let ts;
1274
+ const simulationOverridesPlan = SimulationOverridesBuilder.from(opts.simulationOverridesPlan).withoutBlobCheck().build();
1064
1275
  try {
1065
1276
  // @note This will make sure that we are passing the checks for our header ASSUMING that the data is also made available
1066
1277
  // This means that we can avoid the simulation issues in later checks.
1067
1278
  // By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which
1068
1279
  // make time consistency checks break.
1069
1280
  // 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);
1281
+ await this.validateCheckpointForSubmission(checkpoint, attestationsAndSigners, attestationsAndSignersSignature, simulationOverridesPlan);
1071
1282
  } catch (err) {
1072
1283
  this.log.error(`Checkpoint validation failed. ${err instanceof Error ? err.message : 'No error message'}`, err, {
1073
1284
  ...checkpoint.getStats(),
1074
1285
  slotNumber: checkpoint.header.slotNumber,
1075
- forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber
1286
+ simulationOverridesPlan
1076
1287
  });
1077
1288
  throw err;
1078
1289
  }
1290
+ // Build a pre-check callback that re-validates the checkpoint before L1 submission.
1291
+ // During pipelining this catches stale proposals due to prunes or L1 reorgs that occur during the pipeline sleep.
1292
+ let preCheck = undefined;
1293
+ if (this.epochCache.isProposerPipeliningEnabled()) {
1294
+ preCheck = async ()=>{
1295
+ this.log.debug(`Re-validating checkpoint ${checkpoint.number} before L1 submission`);
1296
+ await this.validateCheckpointForSubmission(checkpoint, attestationsAndSigners, attestationsAndSignersSignature, simulationOverridesPlan);
1297
+ };
1298
+ }
1079
1299
  this.log.verbose(`Enqueuing checkpoint propose transaction`, {
1080
1300
  ...checkpoint.toCheckpointInfo(),
1081
- ...opts
1301
+ txTimeoutAt: opts.txTimeoutAt,
1302
+ simulationOverridesPlan
1082
1303
  });
1083
- await this.addProposeTx(checkpoint, proposeTxArgs, opts, ts);
1304
+ await this.addProposeTx(checkpoint, proposeTxArgs, {
1305
+ txTimeoutAt: opts.txTimeoutAt,
1306
+ simulationOverridesPlan
1307
+ }, preCheck);
1084
1308
  }
1085
1309
  enqueueInvalidateCheckpoint(request, opts = {}) {
1086
1310
  if (!request) {
@@ -1121,7 +1345,8 @@ export class SequencerPublisher {
1121
1345
  }
1122
1346
  });
1123
1347
  }
1124
- async simulateAndEnqueueRequest(action, request, checkSuccess, slotNumber, timestamp) {
1348
+ async simulateAndEnqueueRequest(action, request, checkSuccess, slotNumber) {
1349
+ const timestamp = this.getSimulationTimestamp(slotNumber);
1125
1350
  const logData = {
1126
1351
  slotNumber,
1127
1352
  timestamp,
@@ -1134,6 +1359,7 @@ export class SequencerPublisher {
1134
1359
  const cachedLastActionSlot = this.lastActions[action];
1135
1360
  this.lastActions[action] = slotNumber;
1136
1361
  this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
1362
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1137
1363
  let gasUsed;
1138
1364
  const simulateAbi = mergeAbis([
1139
1365
  request.abi ?? [],
@@ -1142,7 +1368,7 @@ export class SequencerPublisher {
1142
1368
  try {
1143
1369
  ({ gasUsed } = await this.l1TxUtils.simulate(request, {
1144
1370
  time: timestamp
1145
- }, [], simulateAbi)); // TODO(palla/slash): Check the timestamp logic
1371
+ }, [], simulateAbi));
1146
1372
  this.log.verbose(`Simulation for ${action} succeeded`, {
1147
1373
  ...logData,
1148
1374
  request,
@@ -1151,6 +1377,27 @@ export class SequencerPublisher {
1151
1377
  } catch (err) {
1152
1378
  const viemError = formatViemError(err, simulateAbi);
1153
1379
  this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
1380
+ this.backupFailedTx({
1381
+ id: keccak256(request.data),
1382
+ failureType: 'simulation',
1383
+ request: {
1384
+ to: request.to,
1385
+ data: request.data,
1386
+ value: request.value?.toString()
1387
+ },
1388
+ l1BlockNumber: l1BlockNumber.toString(),
1389
+ error: {
1390
+ message: viemError.message,
1391
+ name: viemError.name
1392
+ },
1393
+ context: {
1394
+ actions: [
1395
+ action
1396
+ ],
1397
+ slot: slotNumber,
1398
+ sender: this.getSenderAddress().toString()
1399
+ }
1400
+ });
1154
1401
  return false;
1155
1402
  }
1156
1403
  // We issued the simulation against the rollup contract, so we need to account for the overhead of the multicall3
@@ -1196,13 +1443,14 @@ export class SequencerPublisher {
1196
1443
  * A call to `restart` is required before you can continue publishing.
1197
1444
  */ interrupt() {
1198
1445
  this.interrupted = true;
1446
+ this.interruptibleSleep.interrupt();
1199
1447
  this.l1TxUtils.interrupt();
1200
1448
  }
1201
1449
  /** Restarts the publisher after calling `interrupt`. */ restart() {
1202
1450
  this.interrupted = false;
1203
1451
  this.l1TxUtils.restart();
1204
1452
  }
1205
- async prepareProposeTx(encodedData, timestamp, options) {
1453
+ async prepareProposeTx(encodedData, simulationOverridesPlan) {
1206
1454
  const kzg = Blob.getViemKzgInstance();
1207
1455
  const blobInput = getPrefixedEthBlobCommitments(encodedData.blobs);
1208
1456
  this.log.debug('Validating blob input', {
@@ -1229,10 +1477,38 @@ export class SequencerPublisher {
1229
1477
  }, {}, {
1230
1478
  blobs: encodedData.blobs.map((b)=>b.data),
1231
1479
  kzg
1232
- }).catch((err)=>{
1233
- const { message, metaMessages } = formatViemError(err);
1234
- this.log.error(`Failed to validate blobs`, message, {
1235
- metaMessages
1480
+ }).catch(async (err)=>{
1481
+ const viemError = formatViemError(err);
1482
+ this.log.error(`Failed to validate blobs`, viemError.message, {
1483
+ metaMessages: viemError.metaMessages
1484
+ });
1485
+ const validateBlobsData = encodeFunctionData({
1486
+ abi: RollupAbi,
1487
+ functionName: 'validateBlobs',
1488
+ args: [
1489
+ blobInput
1490
+ ]
1491
+ });
1492
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1493
+ this.backupFailedTx({
1494
+ id: keccak256(validateBlobsData),
1495
+ failureType: 'simulation',
1496
+ request: {
1497
+ to: this.rollupContract.address,
1498
+ data: validateBlobsData
1499
+ },
1500
+ blobData: encodedData.blobs.map((b)=>toHex(b.data)),
1501
+ l1BlockNumber: l1BlockNumber.toString(),
1502
+ error: {
1503
+ message: viemError.message,
1504
+ name: viemError.name
1505
+ },
1506
+ context: {
1507
+ actions: [
1508
+ 'validate-blobs'
1509
+ ],
1510
+ sender: this.getSenderAddress().toString()
1511
+ }
1236
1512
  });
1237
1513
  throw new Error('Failed to validate blobs');
1238
1514
  });
@@ -1243,8 +1519,7 @@ export class SequencerPublisher {
1243
1519
  header: encodedData.header.toViem(),
1244
1520
  archive: toHex(encodedData.archive),
1245
1521
  oracleInput: {
1246
- // We are currently not modifying these. See #9963
1247
- feeAssetPriceModifier: 0n
1522
+ feeAssetPriceModifier: encodedData.feeAssetPriceModifier
1248
1523
  }
1249
1524
  },
1250
1525
  encodedData.attestationsAndSigners.getPackedAttestations(),
@@ -1252,7 +1527,7 @@ export class SequencerPublisher {
1252
1527
  encodedData.attestationsAndSignersSignature.toViemSignature(),
1253
1528
  blobInput
1254
1529
  ];
1255
- const { rollupData, simulationResult } = await this.simulateProposeTx(args, timestamp, options);
1530
+ const { rollupData, simulationResult } = await this.simulateProposeTx(args, simulationOverridesPlan);
1256
1531
  return {
1257
1532
  args,
1258
1533
  blobEvaluationGas,
@@ -1263,29 +1538,14 @@ export class SequencerPublisher {
1263
1538
  /**
1264
1539
  * Simulates the propose tx with eth_simulateV1
1265
1540
  * @param args - The propose tx args
1266
- * @param timestamp - The timestamp to simulate proposal at
1267
1541
  * @returns The simulation result
1268
- */ async simulateProposeTx(args, timestamp, options) {
1542
+ */ async simulateProposeTx(args, simulationOverridesPlan) {
1269
1543
  const rollupData = encodeFunctionData({
1270
1544
  abi: RollupAbi,
1271
1545
  functionName: 'propose',
1272
1546
  args
1273
1547
  });
1274
- // override the pending checkpoint number if requested
1275
- const forcePendingCheckpointNumberStateDiff = (options.forcePendingCheckpointNumber !== undefined ? await this.rollupContract.makePendingCheckpointNumberOverride(options.forcePendingCheckpointNumber) : []).flatMap((override)=>override.stateDiff ?? []);
1276
- const stateOverrides = [
1277
- {
1278
- address: this.rollupContract.address,
1279
- // @note we override checkBlob to false since blobs are not part simulate()
1280
- stateDiff: [
1281
- {
1282
- slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true),
1283
- value: toPaddedHex(0n, true)
1284
- },
1285
- ...forcePendingCheckpointNumberStateDiff
1286
- ]
1287
- }
1288
- ];
1548
+ const stateOverrides = await buildSimulationOverridesStateOverride(this.rollupContract, simulationOverridesPlan);
1289
1549
  // In fisherman mode, simulate as the proposer but with sufficient balance
1290
1550
  if (this.proposerAddressForSimulation) {
1291
1551
  stateOverrides.push({
@@ -1293,6 +1553,8 @@ export class SequencerPublisher {
1293
1553
  balance: 10n * WEI_CONST * WEI_CONST
1294
1554
  });
1295
1555
  }
1556
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1557
+ const simTs = this.getSimulationTimestamp(SlotNumber.fromBigInt(args[0].header.slotNumber));
1296
1558
  const simulationResult = await this.l1TxUtils.simulate({
1297
1559
  to: this.rollupContract.address,
1298
1560
  data: rollupData,
@@ -1301,8 +1563,7 @@ export class SequencerPublisher {
1301
1563
  from: this.proposerAddressForSimulation.toString()
1302
1564
  }
1303
1565
  }, {
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,
1566
+ time: simTs,
1306
1567
  // @note reth should have a 30m gas limit per block but throws errors that this tx is beyond limit so we increase here
1307
1568
  gasLimit: MAX_L1_TX_LIMIT * 2n
1308
1569
  }, stateOverrides, RollupAbi, {
@@ -1319,7 +1580,29 @@ export class SequencerPublisher {
1319
1580
  logs: []
1320
1581
  };
1321
1582
  }
1322
- this.log.error(`Failed to simulate propose tx`, viemError);
1583
+ this.log.error(`Failed to simulate propose tx`, viemError, {
1584
+ simulationTimestamp: simTs
1585
+ });
1586
+ this.backupFailedTx({
1587
+ id: keccak256(rollupData),
1588
+ failureType: 'simulation',
1589
+ request: {
1590
+ to: this.rollupContract.address,
1591
+ data: rollupData
1592
+ },
1593
+ l1BlockNumber: l1BlockNumber.toString(),
1594
+ error: {
1595
+ message: viemError.message,
1596
+ name: viemError.name
1597
+ },
1598
+ context: {
1599
+ actions: [
1600
+ 'propose'
1601
+ ],
1602
+ slot: Number(args[0].header.slotNumber),
1603
+ sender: this.getSenderAddress().toString()
1604
+ }
1605
+ });
1323
1606
  throw err;
1324
1607
  });
1325
1608
  return {
@@ -1327,11 +1610,11 @@ export class SequencerPublisher {
1327
1610
  simulationResult
1328
1611
  };
1329
1612
  }
1330
- async addProposeTx(checkpoint, encodedData, opts = {}, timestamp) {
1613
+ async addProposeTx(checkpoint, encodedData, opts = {}, preCheck) {
1331
1614
  const slot = checkpoint.header.slotNumber;
1332
1615
  const timer = new Timer();
1333
1616
  const kzg = Blob.getViemKzgInstance();
1334
- const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(encodedData, timestamp, opts);
1617
+ const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(encodedData, opts.simulationOverridesPlan);
1335
1618
  const startBlock = await this.l1TxUtils.getBlockNumber();
1336
1619
  const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil(Number(simulationResult.gasUsed) * 64 / 63)) + blobEvaluationGas + SequencerPublisher.MULTICALL_OVERHEAD_GAS_GUESS);
1337
1620
  // Send the blobs to the blob client preemptively. This helps in tests where the sequencer mistakingly thinks that the propose
@@ -1347,9 +1630,10 @@ export class SequencerPublisher {
1347
1630
  },
1348
1631
  lastValidL2Slot: checkpoint.header.slotNumber,
1349
1632
  gasConfig: {
1350
- ...opts,
1633
+ txTimeoutAt: opts.txTimeoutAt,
1351
1634
  gasLimit
1352
1635
  },
1636
+ preCheck,
1353
1637
  blobConfig: {
1354
1638
  blobs: encodedData.blobs.map((b)=>b.data),
1355
1639
  kzg
@@ -1396,4 +1680,13 @@ export class SequencerPublisher {
1396
1680
  }
1397
1681
  });
1398
1682
  }
1683
+ /** Returns the timestamp of the last L1 slot within a given L2 slot. Used as the simulation timestamp
1684
+ * for eth_simulateV1 calls, since it's guaranteed to be greater than any L1 block produced during the slot. */ getSimulationTimestamp(slot) {
1685
+ const l1Constants = this.epochCache.getL1Constants();
1686
+ return getLastL1SlotTimestampForL2Slot(slot, l1Constants);
1687
+ }
1688
+ /** Returns the timestamp of the next L1 slot boundary after now. */ getNextL1SlotTimestamp() {
1689
+ const l1Constants = this.epochCache.getL1Constants();
1690
+ return getNextL1SlotTimestamp(this.dateProvider.nowInSeconds(), l1Constants);
1691
+ }
1399
1692
  }