@aztec/sequencer-client 0.0.1-commit.e2b2873ed → 0.0.1-commit.e304674f1

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