@aztec/sequencer-client 0.0.1-commit.42ee6df9b → 0.0.1-commit.431c48d

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 (109) hide show
  1. package/README.md +283 -21
  2. package/dest/client/sequencer-client.d.ts +20 -5
  3. package/dest/client/sequencer-client.d.ts.map +1 -1
  4. package/dest/client/sequencer-client.js +26 -22
  5. package/dest/config.d.ts +13 -4
  6. package/dest/config.d.ts.map +1 -1
  7. package/dest/config.js +67 -26
  8. package/dest/global_variable_builder/fee_predictor.d.ts +37 -0
  9. package/dest/global_variable_builder/fee_predictor.d.ts.map +1 -0
  10. package/dest/global_variable_builder/fee_predictor.js +138 -0
  11. package/dest/global_variable_builder/fee_provider.d.ts +21 -0
  12. package/dest/global_variable_builder/fee_provider.d.ts.map +1 -0
  13. package/dest/global_variable_builder/fee_provider.js +80 -0
  14. package/dest/global_variable_builder/global_builder.d.ts +7 -26
  15. package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
  16. package/dest/global_variable_builder/global_builder.js +6 -67
  17. package/dest/global_variable_builder/index.d.ts +3 -1
  18. package/dest/global_variable_builder/index.d.ts.map +1 -1
  19. package/dest/global_variable_builder/index.js +2 -0
  20. package/dest/publisher/config.d.ts +7 -3
  21. package/dest/publisher/config.d.ts.map +1 -1
  22. package/dest/publisher/config.js +13 -3
  23. package/dest/publisher/l1_to_l2_messaging.d.ts +21 -0
  24. package/dest/publisher/l1_to_l2_messaging.d.ts.map +1 -0
  25. package/dest/publisher/l1_to_l2_messaging.js +70 -0
  26. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts +3 -4
  27. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -1
  28. package/dest/publisher/sequencer-bundle-simulator.d.ts +96 -0
  29. package/dest/publisher/sequencer-bundle-simulator.d.ts.map +1 -0
  30. package/dest/publisher/sequencer-bundle-simulator.js +198 -0
  31. package/dest/publisher/sequencer-publisher-factory.d.ts +1 -3
  32. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  33. package/dest/publisher/sequencer-publisher-factory.js +0 -1
  34. package/dest/publisher/sequencer-publisher.d.ts +65 -70
  35. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  36. package/dest/publisher/sequencer-publisher.js +413 -559
  37. package/dest/publisher/write_json.d.ts +11 -0
  38. package/dest/publisher/write_json.d.ts.map +1 -0
  39. package/dest/publisher/write_json.js +57 -0
  40. package/dest/sequencer/automine/automine_factory.d.ts +56 -0
  41. package/dest/sequencer/automine/automine_factory.d.ts.map +1 -0
  42. package/dest/sequencer/automine/automine_factory.js +85 -0
  43. package/dest/sequencer/automine/automine_sequencer.d.ts +189 -0
  44. package/dest/sequencer/automine/automine_sequencer.d.ts.map +1 -0
  45. package/dest/sequencer/automine/automine_sequencer.js +696 -0
  46. package/dest/sequencer/automine/index.d.ts +3 -0
  47. package/dest/sequencer/automine/index.d.ts.map +1 -0
  48. package/dest/sequencer/automine/index.js +2 -0
  49. package/dest/sequencer/checkpoint_proposal_job.d.ts +67 -36
  50. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  51. package/dest/sequencer/checkpoint_proposal_job.js +705 -256
  52. package/dest/sequencer/checkpoint_proposal_job_metrics.d.ts +34 -0
  53. package/dest/sequencer/checkpoint_proposal_job_metrics.d.ts.map +1 -0
  54. package/dest/sequencer/checkpoint_proposal_job_metrics.js +72 -0
  55. package/dest/sequencer/errors.d.ts +1 -8
  56. package/dest/sequencer/errors.d.ts.map +1 -1
  57. package/dest/sequencer/errors.js +0 -9
  58. package/dest/sequencer/events.d.ts +61 -5
  59. package/dest/sequencer/events.d.ts.map +1 -1
  60. package/dest/sequencer/metrics.d.ts +10 -11
  61. package/dest/sequencer/metrics.d.ts.map +1 -1
  62. package/dest/sequencer/metrics.js +34 -20
  63. package/dest/sequencer/requests_tracker.d.ts +22 -0
  64. package/dest/sequencer/requests_tracker.d.ts.map +1 -0
  65. package/dest/sequencer/requests_tracker.js +33 -0
  66. package/dest/sequencer/sequencer.d.ts +147 -33
  67. package/dest/sequencer/sequencer.d.ts.map +1 -1
  68. package/dest/sequencer/sequencer.js +543 -180
  69. package/dest/sequencer/types.d.ts +2 -2
  70. package/dest/sequencer/types.d.ts.map +1 -1
  71. package/dest/test/index.d.ts +3 -3
  72. package/dest/test/index.d.ts.map +1 -1
  73. package/dest/test/utils.d.ts +15 -1
  74. package/dest/test/utils.d.ts.map +1 -1
  75. package/dest/test/utils.js +25 -7
  76. package/package.json +28 -27
  77. package/src/client/sequencer-client.ts +37 -27
  78. package/src/config.ts +79 -26
  79. package/src/global_variable_builder/README.md +44 -0
  80. package/src/global_variable_builder/fee_predictor.ts +182 -0
  81. package/src/global_variable_builder/fee_provider.ts +97 -0
  82. package/src/global_variable_builder/global_builder.ts +11 -91
  83. package/src/global_variable_builder/index.ts +2 -0
  84. package/src/publisher/config.ts +30 -7
  85. package/src/publisher/l1_to_l2_messaging.ts +85 -0
  86. package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +3 -1
  87. package/src/publisher/sequencer-bundle-simulator.ts +254 -0
  88. package/src/publisher/sequencer-publisher-factory.ts +0 -3
  89. package/src/publisher/sequencer-publisher.ts +458 -622
  90. package/src/publisher/write_json.ts +78 -0
  91. package/src/sequencer/automine/README.md +60 -0
  92. package/src/sequencer/automine/automine_factory.ts +152 -0
  93. package/src/sequencer/automine/automine_sequencer.ts +800 -0
  94. package/src/sequencer/automine/index.ts +6 -0
  95. package/src/sequencer/checkpoint_proposal_job.ts +823 -316
  96. package/src/sequencer/checkpoint_proposal_job_metrics.ts +128 -0
  97. package/src/sequencer/errors.ts +0 -15
  98. package/src/sequencer/events.ts +66 -5
  99. package/src/sequencer/metrics.ts +49 -25
  100. package/src/sequencer/requests_tracker.ts +43 -0
  101. package/src/sequencer/sequencer.ts +607 -206
  102. package/src/sequencer/types.ts +1 -1
  103. package/src/test/index.ts +2 -2
  104. package/src/test/utils.ts +61 -10
  105. package/dest/sequencer/timetable.d.ts +0 -88
  106. package/dest/sequencer/timetable.d.ts.map +0 -1
  107. package/dest/sequencer/timetable.js +0 -222
  108. package/src/sequencer/README.md +0 -531
  109. package/src/sequencer/timetable.ts +0 -283
@@ -370,14 +370,12 @@ function applyDecs2203RFactory() {
370
370
  function _apply_decs_2203_r(targetClass, memberDecs, classDecs, parentClass) {
371
371
  return (_apply_decs_2203_r = applyDecs2203RFactory())(targetClass, memberDecs, classDecs, parentClass);
372
372
  }
373
- var _dec, _dec1, _dec2, _initProto;
373
+ var _dec, _dec1, _initProto;
374
374
  import { Blob, getBlobsPerL1Block, getPrefixedEthBlobCommitments } from '@aztec/blob-lib';
375
- import { FeeAssetPriceOracle, MULTI_CALL_3_ADDRESS, Multicall3, RollupContract } from '@aztec/ethereum/contracts';
375
+ import { FeeAssetPriceOracle, MULTI_CALL_3_ADDRESS, Multicall3, MulticallForwarderRevertedError, 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
- import { FormattedViemError, formatViemError, mergeAbis, tryExtractEvent } from '@aztec/ethereum/utils';
379
- import { sumBigint } from '@aztec/foundation/bigint';
380
- import { toHex as toPaddedHex } from '@aztec/foundation/bigint-buffer';
378
+ import { FormattedViemError, formatViemError, mergeAbis, tryDecodeRevertReason, tryExtractEvent } from '@aztec/ethereum/utils';
381
379
  import { CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
382
380
  import { trimmedBytesLength } from '@aztec/foundation/buffer';
383
381
  import { pick } from '@aztec/foundation/collection';
@@ -385,32 +383,40 @@ import { TimeoutError } from '@aztec/foundation/error';
385
383
  import { EthAddress } from '@aztec/foundation/eth-address';
386
384
  import { Signature } from '@aztec/foundation/eth-signature';
387
385
  import { createLogger } from '@aztec/foundation/log';
388
- import { makeBackoff, retry } from '@aztec/foundation/retry';
389
386
  import { InterruptibleSleep } from '@aztec/foundation/sleep';
390
387
  import { bufferToHex } from '@aztec/foundation/string';
391
388
  import { Timer } from '@aztec/foundation/timer';
392
- import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
389
+ import { EmpireBaseAbi, ErrorsAbi, RollupAbi, SlashingProposerAbi } from '@aztec/l1-artifacts';
393
390
  import { encodeSlashConsensusVotes } from '@aztec/slasher';
394
391
  import { CommitteeAttestationsAndSigners } from '@aztec/stdlib/block';
395
- import { getLastL1SlotTimestampForL2Slot, getNextL1SlotTimestamp } from '@aztec/stdlib/epoch-helpers';
392
+ import { getLastL1SlotTimestampForL2Slot, getNextL1SlotTimestamp, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
396
393
  import { getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
397
- import { encodeFunctionData, keccak256, multicall3Abi, toHex } from 'viem';
394
+ import { encodeFunctionData, keccak256, toHex } from 'viem';
398
395
  import { createL1TxFailedStore } from './l1_tx_failed_store/index.js';
396
+ import { SequencerBundleSimulator } from './sequencer-bundle-simulator.js';
399
397
  import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
398
+ /**
399
+ * Returns true if the receipt indicates a successful send AND the expected event was emitted
400
+ * by the target contract. Both pieces are required: an aggregate3 entry that reverted will
401
+ * have receipt.status === 'success' but no event log.
402
+ */ function extractEventSuccess(receipt, opts) {
403
+ if (!receipt || receipt.status !== 'success') {
404
+ return false;
405
+ }
406
+ return !!tryExtractEvent(receipt.logs, opts.address.toString(), opts.abi, opts.eventName);
407
+ }
400
408
  export const Actions = [
401
409
  'invalidate-by-invalid-attestation',
402
410
  'invalidate-by-insufficient-attestations',
411
+ 'prune',
403
412
  'propose',
404
413
  'governance-signal',
405
- 'empire-slashing-signal',
406
- 'create-empire-payload',
407
- 'execute-empire-payload',
408
414
  'vote-offenses',
409
415
  'execute-slash'
410
416
  ];
411
417
  // Sorting for actions such that invalidations go before proposals, and proposals go before votes
412
418
  export const compareActions = (a, b)=>Actions.indexOf(a) - Actions.indexOf(b);
413
- _dec = trackSpan('SequencerPublisher.sendRequests'), _dec1 = trackSpan('SequencerPublisher.validateBlockHeader'), _dec2 = trackSpan('SequencerPublisher.validateCheckpointForSubmission');
419
+ _dec = trackSpan('SequencerPublisher.sendRequests'), _dec1 = trackSpan('SequencerPublisher.validateCheckpointHeader');
414
420
  export class SequencerPublisher {
415
421
  config;
416
422
  static{
@@ -423,58 +429,55 @@ export class SequencerPublisher {
423
429
  [
424
430
  _dec1,
425
431
  2,
426
- "validateBlockHeader"
427
- ],
428
- [
429
- _dec2,
430
- 2,
431
- "validateCheckpointForSubmission"
432
+ "validateCheckpointHeader"
432
433
  ]
433
434
  ], []));
434
435
  }
435
436
  interrupted;
436
437
  metrics;
438
+ bundleSimulator;
437
439
  epochCache;
438
440
  failedTxStore;
439
- governanceLog;
440
- slashingLog;
441
+ /**
442
+ * ABI used to decode raw revert payloads from dropped bundle entries when the original
443
+ * request did not carry an abi (e.g. the propose request). Merges every contract the
444
+ * publisher can route to so any of their custom errors decode against it.
445
+ */ revertDecoderAbi;
441
446
  lastActions;
442
- isPayloadEmptyCache;
443
- payloadProposedCache;
444
447
  log;
445
448
  ethereumSlotDuration;
446
449
  aztecSlotDuration;
450
+ previousL1BlockWaitTimeoutMs;
451
+ previousL1BlockWaitPollIntervalMs;
447
452
  /** Date provider for wall-clock time. */ dateProvider;
448
453
  blobClient;
449
- /** Address to use for simulations in fisherman mode (actual proposer's address) */ proposerAddressForSimulation;
450
454
  /** Optional callback to obtain a replacement publisher when the current one fails to send. */ getNextPublisher;
451
455
  /** L1 fee analyzer for fisherman mode */ l1FeeAnalyzer;
452
456
  /** Fee asset price oracle for computing price modifiers from Uniswap V4 */ feeAssetPriceOracle;
453
457
  /** Interruptible sleep used by sendRequestsAt to wait until a target timestamp. */ interruptibleSleep;
454
- // A CALL to a cold address is 2700 gas
455
- static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
456
- // Gas report for VotingWithSigTest shows a max gas of 100k, but we've seen it cost 700k+ in testnet
457
- static VOTE_GAS_GUESS = 800_000n;
458
458
  l1TxUtils;
459
459
  rollupContract;
460
460
  govProposerContract;
461
461
  slashingProposerContract;
462
- slashFactoryContract;
463
462
  tracer;
464
463
  requests;
465
464
  constructor(config, deps){
466
465
  this.config = config;
467
466
  this.interrupted = (_initProto(this), false);
468
- this.governanceLog = createLogger('sequencer:publisher:governance');
469
- this.slashingLog = createLogger('sequencer:publisher:slashing');
467
+ this.revertDecoderAbi = mergeAbis([
468
+ RollupAbi,
469
+ SlashingProposerAbi,
470
+ EmpireBaseAbi,
471
+ ErrorsAbi
472
+ ]);
470
473
  this.lastActions = {};
471
- this.isPayloadEmptyCache = new Map();
472
- this.payloadProposedCache = new Set();
473
474
  this.interruptibleSleep = new InterruptibleSleep();
474
475
  this.requests = [];
475
476
  this.log = deps.log ?? createLogger('sequencer:publisher');
476
477
  this.ethereumSlotDuration = BigInt(config.ethereumSlotDuration);
477
478
  this.aztecSlotDuration = BigInt(config.aztecSlotDuration);
479
+ this.previousL1BlockWaitTimeoutMs = config.sequencerPublisherPreviousL1BlockWaitTimeoutMs;
480
+ this.previousL1BlockWaitPollIntervalMs = config.sequencerPublisherPreviousL1BlockWaitPollIntervalMs;
478
481
  this.dateProvider = deps.dateProvider;
479
482
  this.epochCache = deps.epochCache;
480
483
  this.lastActions = deps.lastActions;
@@ -493,15 +496,20 @@ export class SequencerPublisher {
493
496
  const newSlashingProposer = await this.rollupContract.getSlashingProposer();
494
497
  this.slashingProposerContract = newSlashingProposer;
495
498
  });
496
- this.slashFactoryContract = deps.slashFactoryContract;
497
499
  // Initialize L1 fee analyzer for fisherman mode
498
500
  if (config.fishermanMode) {
499
- this.l1FeeAnalyzer = new L1FeeAnalyzer(this.l1TxUtils.client, deps.dateProvider, createLogger('sequencer:publisher:fee-analyzer'));
501
+ this.l1FeeAnalyzer = new L1FeeAnalyzer(this.l1TxUtils.client, deps.dateProvider, this.log.createChild('fee-analyzer'));
500
502
  }
501
503
  // Initialize fee asset price oracle
502
- this.feeAssetPriceOracle = new FeeAssetPriceOracle(this.l1TxUtils.client, this.rollupContract, createLogger('sequencer:publisher:price-oracle'));
504
+ this.feeAssetPriceOracle = new FeeAssetPriceOracle(this.l1TxUtils.client, this.rollupContract, this.log.createChild('price-oracle'));
503
505
  // Initialize failed L1 tx store (optional, for test networks)
504
506
  this.failedTxStore = createL1TxFailedStore(config.l1TxFailedStore, this.log);
507
+ this.bundleSimulator = new SequencerBundleSimulator({
508
+ getL1TxUtils: ()=>this.l1TxUtils,
509
+ rollupContract: this.rollupContract,
510
+ epochCache: this.epochCache,
511
+ log: this.log.createChild('bundle-simulator')
512
+ });
505
513
  }
506
514
  /**
507
515
  * Backs up a failed L1 transaction to the configured store for debugging.
@@ -524,9 +532,13 @@ export class SequencerPublisher {
524
532
  }
525
533
  /**
526
534
  * Gets the fee asset price modifier from the oracle.
527
- * Returns 0n if the oracle query fails.
528
- */ getFeeAssetPriceModifier() {
529
- return this.feeAssetPriceOracle.computePriceModifier();
535
+ *
536
+ * @param predictedParentEthPerFeeAssetE12 - Optional predicted parent eth-per-fee-asset (E12).
537
+ * Pipelined proposers should pass the value from the predicted parent fee header so the
538
+ * modifier matches the parent L1 will use when applying it.
539
+ * @returns The fee asset price modifier in basis points, or 0n if the oracle query fails.
540
+ */ getFeeAssetPriceModifier(predictedParentEthPerFeeAssetE12) {
541
+ return this.feeAssetPriceOracle.computePriceModifier(predictedParentEthPerFeeAssetE12);
530
542
  }
531
543
  getSenderAddress() {
532
544
  return this.l1TxUtils.getSenderAddress();
@@ -536,12 +548,6 @@ export class SequencerPublisher {
536
548
  */ getL1FeeAnalyzer() {
537
549
  return this.l1FeeAnalyzer;
538
550
  }
539
- /**
540
- * Sets the proposer address to use for simulations in fisherman mode.
541
- * @param proposerAddress - The actual proposer's address to use for balance lookups in simulations
542
- */ setProposerAddressForSimulation(proposerAddress) {
543
- this.proposerAddressForSimulation = proposerAddress;
544
- }
545
551
  addRequest(request) {
546
552
  this.requests.push(request);
547
553
  }
@@ -599,11 +605,15 @@ export class SequencerPublisher {
599
605
  }
600
606
  /**
601
607
  * Sends all requests that are still valid.
608
+ * @param targetSlot - The target L2 slot for this send. When provided (the production path, via
609
+ * sendRequestsAt), it is threaded into bundleSimulate so the block.timestamp override matches
610
+ * the slot the propose is built for. When omitted, falls back to getCurrentL2Slot() for the
611
+ * AutomineSequencer, which publishes synchronously within the current slot.
602
612
  * @returns one of:
603
613
  * - A receipt and stats if the tx succeeded
604
614
  * - a receipt and errorMsg if it failed on L1
605
615
  * - undefined if no valid requests are found OR the tx failed to send.
606
- */ async sendRequests() {
616
+ */ async sendRequests(targetSlot) {
607
617
  const requestsToProcess = [
608
618
  ...this.requests
609
619
  ];
@@ -611,10 +621,9 @@ export class SequencerPublisher {
611
621
  if (this.interrupted || requestsToProcess.length === 0) {
612
622
  return undefined;
613
623
  }
614
- const currentL2Slot = this.getCurrentL2Slot();
624
+ const currentL2Slot = targetSlot ?? this.getCurrentL2Slot();
615
625
  this.log.debug(`Sending requests on L2 slot ${currentL2Slot}`);
616
626
  const validRequests = requestsToProcess.filter((request)=>request.lastValidL2Slot >= currentL2Slot);
617
- const validActions = validRequests.map((x)=>x.action);
618
627
  const expiredActions = requestsToProcess.filter((request)=>request.lastValidL2Slot < currentL2Slot).map((x)=>x.action);
619
628
  if (validRequests.length !== requestsToProcess.length) {
620
629
  this.log.warn(`Some requests were expired for slot ${currentL2Slot}`, {
@@ -632,71 +641,53 @@ export class SequencerPublisher {
632
641
  this.log.debug(`No valid requests to send`);
633
642
  return undefined;
634
643
  }
635
- // @note - we can only have one blob config per bundle
636
- // find requests with gas and blob configs
637
- // See https://github.com/AztecProtocol/aztec-packages/issues/11513
644
+ // Collect earliest txTimeoutAt across all requests.
638
645
  const gasConfigs = validRequests.filter((request)=>request.gasConfig).map((request)=>request.gasConfig);
639
- const blobConfigs = validRequests.filter((request)=>request.blobConfig).map((request)=>request.blobConfig);
640
- if (blobConfigs.length > 1) {
641
- throw new Error('Multiple blob configs found');
642
- }
643
- const blobConfig = blobConfigs[0];
644
- // Merge gasConfigs. Yields the sum of gasLimits, and the earliest txTimeoutAt, or undefined if no gasConfig sets them.
645
- const gasLimits = gasConfigs.map((g)=>g?.gasLimit).filter((g)=>g !== undefined);
646
- let gasLimit = gasLimits.length > 0 ? sumBigint(gasLimits) : undefined; // sum
647
- // Cap at L1 block gas limit so the node accepts the tx ("gas limit too high" otherwise).
648
- const maxGas = MAX_L1_TX_LIMIT;
649
- if (gasLimit !== undefined && gasLimit > maxGas) {
650
- this.log.debug('Capping bundled tx gas limit to L1 max', {
651
- requested: gasLimit,
652
- capped: maxGas
653
- });
654
- gasLimit = maxGas;
655
- }
656
646
  const txTimeoutAts = gasConfigs.map((g)=>g?.txTimeoutAt).filter((g)=>g !== undefined);
657
- const txTimeoutAt = txTimeoutAts.length > 0 ? new Date(Math.min(...txTimeoutAts.map((g)=>g.getTime()))) : undefined; // earliest
658
- const txConfig = {
659
- gasLimit,
660
- txTimeoutAt
661
- };
647
+ const txTimeoutAt = txTimeoutAts.length > 0 ? new Date(Math.min(...txTimeoutAts.map((g)=>g.getTime()))) : undefined;
662
648
  // Sort the requests so that proposals always go first
663
649
  // This ensures the committee gets precomputed correctly
664
650
  validRequests.sort((a, b)=>compareActions(a.action, b.action));
665
651
  try {
666
- // Capture context for failed tx backup before sending
667
- const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
668
- const multicallData = encodeFunctionData({
669
- abi: multicall3Abi,
670
- functionName: 'aggregate3',
671
- args: [
672
- validRequests.map((r)=>({
673
- target: r.request.to,
674
- callData: r.request.data,
675
- allowFailure: true
676
- }))
677
- ]
678
- });
679
- const blobDataHex = blobConfig?.blobs?.map((b)=>toHex(b));
680
- const txContext = {
681
- multicallData,
682
- blobData: blobDataHex,
683
- l1BlockNumber
652
+ // Bundle-level eth_simulateV1: filters out entries that revert and derives the gasLimit.
653
+ const bundleResult = await this.bundleSimulator.simulate(validRequests, currentL2Slot);
654
+ if (bundleResult.kind === 'aborted') {
655
+ this.logDroppedInSim(bundleResult.droppedRequests);
656
+ void this.backupDroppedInSim(bundleResult.droppedRequests).catch((err)=>this.log.error(`Failed to backup requests dropped in simulation`, err));
657
+ return undefined;
658
+ }
659
+ const { requests, droppedRequests, gasLimit } = bundleResult.kind === 'fallback' ? {
660
+ requests: bundleResult.requests,
661
+ droppedRequests: bundleResult.droppedRequests,
662
+ gasLimit: MAX_L1_TX_LIMIT
663
+ } : bundleResult;
664
+ this.logDroppedInSim(droppedRequests);
665
+ // Compute blobConfig from survivors (not original validRequests) so that if the propose
666
+ // entry was dropped by bundleSimulate we don't attach a blob-typed config to a non-blob tx.
667
+ const [blobConfig] = requests.filter((r)=>r.blobConfig).map((r)=>r.blobConfig);
668
+ const txConfig = {
669
+ gasLimit,
670
+ txTimeoutAt
684
671
  };
685
672
  this.log.debug('Forwarding transactions', {
686
- validRequests: validRequests.map((request)=>request.action),
673
+ requests: requests.map((request)=>request.action),
687
674
  txConfig
688
675
  });
689
- const result = await this.forwardWithPublisherRotation(validRequests, txConfig, blobConfig);
676
+ const result = await this.forwardWithPublisherRotation(requests, txConfig, blobConfig);
690
677
  if (result === undefined) {
691
678
  return undefined;
692
679
  }
693
- const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(validRequests, result, txContext);
680
+ const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(requests, result);
681
+ const allFailedActions = [
682
+ ...failedActions,
683
+ ...droppedRequests.map((d)=>d.request.action)
684
+ ];
694
685
  return {
695
686
  result,
696
687
  expiredActions,
697
- sentActions: validActions,
688
+ sentActions: requests.map((x)=>x.action),
698
689
  successfulActions,
699
- failedActions
690
+ failedActions: allFailedActions
700
691
  };
701
692
  } catch (err) {
702
693
  const viemError = formatViemError(err);
@@ -710,23 +701,78 @@ export class SequencerPublisher {
710
701
  }
711
702
  }
712
703
  }
704
+ /** Logs entries dropped by bundle simulation as warnings on the publisher's logger. */ logDroppedInSim(dropped) {
705
+ for (const drop of dropped){
706
+ const revertReasonDecoded = drop.revertReason ?? tryDecodeRevertReason(drop.returnData, this.revertDecoderAbi);
707
+ this.log.warn('Bundle entry dropped: action reverted in sim', {
708
+ action: drop.request.action,
709
+ revertReason: revertReasonDecoded ?? drop.returnData,
710
+ revertReasonDecoded,
711
+ returnData: drop.returnData
712
+ });
713
+ }
714
+ }
715
+ /** Backs up entries dropped by bundle simulation, one record per dropped action. */ async backupDroppedInSim(dropped) {
716
+ if (dropped.length === 0) {
717
+ return;
718
+ }
719
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
720
+ for (const { request: req } of dropped){
721
+ this.backupFailedTx({
722
+ id: keccak256(req.request.data),
723
+ failureType: 'simulation',
724
+ request: {
725
+ to: req.request.to,
726
+ data: req.request.data
727
+ },
728
+ l1BlockNumber: l1BlockNumber.toString(),
729
+ error: {
730
+ message: 'Bundle entry dropped: action reverted in sim'
731
+ },
732
+ context: {
733
+ actions: [
734
+ req.action
735
+ ],
736
+ sender: this.getSenderAddress().toString()
737
+ }
738
+ });
739
+ }
740
+ }
713
741
  /**
714
742
  * Forwards transactions via Multicall3, rotating to the next available publisher if a send
715
743
  * failure occurs (i.e. the tx never reached the chain).
716
744
  * On-chain reverts and simulation errors are returned as-is without rotation.
717
745
  */ async forwardWithPublisherRotation(validRequests, txConfig, blobConfig) {
746
+ if (!txConfig?.gasLimit) {
747
+ throw new Error('gasLimit is required for bundled transactions');
748
+ }
749
+ const txConfigWithGasLimit = txConfig;
718
750
  const triedAddresses = [];
719
751
  let currentPublisher = this.l1TxUtils;
720
752
  while(true){
753
+ if (txConfig.txTimeoutAt && new Date() > txConfig.txTimeoutAt) {
754
+ this.log.warn(`Tx timeout (${txConfig.txTimeoutAt.toISOString()}) elapsed; stopping publisher rotation`, {
755
+ triedAddresses: triedAddresses.map((a)=>a.toString())
756
+ });
757
+ return undefined;
758
+ }
721
759
  triedAddresses.push(currentPublisher.getSenderAddress());
722
760
  try {
723
- const result = await Multicall3.forward(validRequests.map((r)=>r.request), currentPublisher, txConfig, blobConfig, this.rollupContract.address, this.log);
761
+ const result = await Multicall3.forward(validRequests.map((r)=>r.request), currentPublisher, txConfigWithGasLimit, blobConfig, {
762
+ gasLimitRequired: true
763
+ });
724
764
  this.l1TxUtils = currentPublisher;
725
765
  return result;
726
766
  } catch (err) {
727
767
  if (err instanceof TimeoutError) {
728
768
  throw err;
729
769
  }
770
+ if (err instanceof MulticallForwarderRevertedError) {
771
+ this.log.error('Forwarder transaction reverted on-chain; not rotating publisher', err, {
772
+ transactionHash: err.receipt.transactionHash
773
+ });
774
+ return undefined;
775
+ }
730
776
  const viemError = formatViemError(err);
731
777
  if (!this.getNextPublisher) {
732
778
  this.log.error('Failed to publish bundled transactions', viemError);
@@ -737,7 +783,9 @@ export class SequencerPublisher {
737
783
  ...triedAddresses
738
784
  ]);
739
785
  if (!nextPublisher) {
740
- this.log.error('All available publishers exhausted, failed to publish bundled transactions');
786
+ this.log.error(`All available publishers exhausted (tried ${triedAddresses.length}), failed to publish bundled transactions`, viemError, {
787
+ triedAddresses: triedAddresses.map((a)=>a.toString())
788
+ });
741
789
  return undefined;
742
790
  }
743
791
  currentPublisher = nextPublisher;
@@ -745,132 +793,122 @@ export class SequencerPublisher {
745
793
  }
746
794
  }
747
795
  /*
748
- * Schedules sending all enqueued requests at (or after) the given timestamp.
749
- * Uses InterruptibleSleep so it can be cancelled via interrupt().
750
- * Returns the promise for the L1 response (caller should NOT await this in the work loop).
751
- */ async sendRequestsAt(submitAfter) {
752
- const ms = submitAfter.getTime() - this.dateProvider.now();
753
- if (ms > 0) {
754
- this.log.debug(`Sleeping ${ms}ms before sending requests`, {
755
- submitAfter
756
- });
757
- await this.interruptibleSleep.sleep(ms);
758
- }
796
+ * Schedules sending all enqueued requests at (or after) the start of the given L2 slot.
797
+ */ async sendRequestsAt(targetSlot) {
798
+ await this.waitForTargetSlot(targetSlot);
759
799
  if (this.interrupted) {
760
800
  return undefined;
761
801
  }
762
- return this.sendRequests();
802
+ return this.sendRequests(targetSlot);
763
803
  }
764
- callbackBundledTransactions(requests, result, txContext) {
765
- const actionsListStr = requests.map((r)=>r.action).join(', ');
766
- if (result instanceof FormattedViemError) {
767
- this.log.error(`Failed to publish bundled transactions (${actionsListStr})`, result);
768
- this.backupFailedTx({
769
- id: keccak256(txContext.multicallData),
770
- failureType: 'send-error',
771
- request: {
772
- to: MULTI_CALL_3_ADDRESS,
773
- data: txContext.multicallData
774
- },
775
- blobData: txContext.blobData,
776
- l1BlockNumber: txContext.l1BlockNumber.toString(),
777
- error: {
778
- message: result.message,
779
- name: result.name
780
- },
781
- context: {
782
- actions: requests.map((r)=>r.action),
783
- requests: requests.map((r)=>({
784
- action: r.action,
785
- to: r.request.to,
786
- data: r.request.data
787
- })),
788
- sender: this.getSenderAddress().toString()
804
+ /**
805
+ * Sleeps until one L1 slot before the L2 slot boundary, and then waits for that L1 block
806
+ * to be mined, so we don't risk being included in it. If that block never gets mined after
807
+ * a timeout, we assume it got skipped on L1, so we send the tx anyway.
808
+ */ async waitForTargetSlot(targetSlot) {
809
+ const l1Constants = this.epochCache.getL1Constants();
810
+ const nowInSeconds = this.dateProvider.nowInSeconds();
811
+ const startOfTargetSlotTs = getTimestampForSlot(targetSlot, l1Constants);
812
+ const previousL1BlockTs = startOfTargetSlotTs - this.ethereumSlotDuration;
813
+ const waitDeadlineTs = previousL1BlockTs + BigInt(this.previousL1BlockWaitTimeoutMs / 1000);
814
+ const logCtx = {
815
+ targetSlot,
816
+ startOfTargetSlotTs,
817
+ nowInSeconds,
818
+ previousL1BlockTs,
819
+ waitDeadlineTs
820
+ };
821
+ // Check if we are already past time
822
+ if (nowInSeconds >= startOfTargetSlotTs) {
823
+ this.log.verbose(`Target slot ${targetSlot} already started, sending requests immediately`, logCtx);
824
+ return;
825
+ }
826
+ // Otherwise we wait
827
+ this.log.debug(`Waiting for slot ${targetSlot} before sending requests`, logCtx);
828
+ // Wait until previous L1 block timestamp first
829
+ const sleepMs = (Number(previousL1BlockTs) - nowInSeconds) * 1000;
830
+ if (sleepMs > 0 && !this.interrupted) {
831
+ this.log.trace(`Sleeping ${sleepMs}ms before waiting for previous L1 block`, logCtx);
832
+ await this.interruptibleSleep.sleep(sleepMs);
833
+ }
834
+ // Then loop until we see the previous L1 block, so we know that we cannot be included in it.
835
+ // We time out after a while, once we are sure that that block is skipped in L1.
836
+ while(!this.interrupted){
837
+ try {
838
+ const nowInSeconds = this.dateProvider.nowInSeconds();
839
+ logCtx.nowInSeconds = nowInSeconds;
840
+ if (nowInSeconds >= waitDeadlineTs) {
841
+ this.log.warn(`Timed out waiting for previous L1 block before sending requests, proceeding`, logCtx);
842
+ return;
789
843
  }
790
- });
791
- return {
792
- failedActions: requests.map((r)=>r.action)
793
- };
794
- } else {
795
- this.log.verbose(`Published bundled transactions (${actionsListStr})`, {
796
- result,
797
- requests: requests.map((r)=>({
798
- ...r,
799
- // Avoid logging large blob data
800
- blobConfig: r.blobConfig ? {
801
- ...r.blobConfig,
802
- blobs: r.blobConfig.blobs.map((b)=>({
803
- size: trimmedBytesLength(b)
804
- }))
805
- } : undefined
806
- }))
807
- });
808
- const successfulActions = [];
809
- const failedActions = [];
810
- for (const request of requests){
811
- if (request.checkSuccess(request.request, result)) {
812
- successfulActions.push(request.action);
813
- } else {
814
- failedActions.push(request.action);
844
+ const latestBlockTs = await this.l1TxUtils.getBlock().then((b)=>b.timestamp);
845
+ if (latestBlockTs >= previousL1BlockTs) {
846
+ this.log.debug(`Previous L1 block mined, proceeding to send requests`, {
847
+ ...logCtx,
848
+ latestBlockTs
849
+ });
850
+ return;
815
851
  }
816
- }
817
- // Single backup for the whole reverted tx
818
- if (failedActions.length > 0 && result?.receipt?.status === 'reverted') {
819
- this.backupFailedTx({
820
- id: result.receipt.transactionHash,
821
- failureType: 'revert',
822
- request: {
823
- to: MULTI_CALL_3_ADDRESS,
824
- data: txContext.multicallData
825
- },
826
- blobData: txContext.blobData,
827
- l1BlockNumber: result.receipt.blockNumber.toString(),
828
- receipt: {
829
- transactionHash: result.receipt.transactionHash,
830
- blockNumber: result.receipt.blockNumber.toString(),
831
- gasUsed: (result.receipt.gasUsed ?? 0n).toString(),
832
- status: 'reverted'
833
- },
834
- error: {
835
- message: result.errorMsg ?? 'Transaction reverted'
836
- },
837
- context: {
838
- actions: failedActions,
839
- requests: requests.filter((r)=>failedActions.includes(r.action)).map((r)=>({
840
- action: r.action,
841
- to: r.request.to,
842
- data: r.request.data
843
- })),
844
- sender: this.getSenderAddress().toString()
845
- }
852
+ this.log.trace(`Previous L1 block not mined yet, continuing to wait`, {
853
+ ...logCtx,
854
+ latestBlockTs
846
855
  });
856
+ } catch (err) {
857
+ this.log.error(`Error while waiting for previous L1 block before sending requests; retrying`, err, logCtx);
858
+ } finally{
859
+ await this.interruptibleSleep.sleep(this.previousL1BlockWaitPollIntervalMs);
847
860
  }
848
- return {
849
- successfulActions,
850
- failedActions
851
- };
852
861
  }
853
862
  }
863
+ callbackBundledTransactions(requests, result) {
864
+ const actionsListStr = requests.map((r)=>r.action).join(', ');
865
+ this.log.verbose(`Published bundled transactions (${actionsListStr})`, {
866
+ result,
867
+ requests: requests.map((r)=>({
868
+ ...r,
869
+ // Avoid logging large blob data
870
+ blobConfig: r.blobConfig ? {
871
+ ...r.blobConfig,
872
+ blobs: r.blobConfig.blobs.map((b)=>({
873
+ size: trimmedBytesLength(b)
874
+ }))
875
+ } : undefined
876
+ }))
877
+ });
878
+ const successfulActions = [];
879
+ const failedActions = [];
880
+ for (const request of requests){
881
+ if (request.checkSuccess(request.request, result)) {
882
+ successfulActions.push(request.action);
883
+ } else {
884
+ failedActions.push(request.action);
885
+ }
886
+ }
887
+ return {
888
+ successfulActions,
889
+ failedActions
890
+ };
891
+ }
854
892
  /**
855
893
  * @notice Will call `canProposeAt` to make sure that it is possible to propose
856
894
  * @param tipArchive - The archive to check
857
895
  * @returns The slot and block number if it is possible to propose, undefined otherwise
858
- */ canProposeAt(tipArchive, msgSender, opts = {}) {
896
+ */ async canProposeAt(tipArchive, msgSender, simulationOverridesPlan) {
859
897
  // TODO: #14291 - should loop through multiple keys to check if any of them can propose
860
- const ignoredErrors = [
898
+ // These errors are expected when we cannot actually propose right now — usually because our
899
+ // local view of the chain is ahead of L1 (proposed parent hasn't landed yet, or someone
900
+ // else has just landed the slot, or the archive override doesn't match). We log a warn and
901
+ // skip the proposal; we do NOT treat these as bugs.
902
+ const expectedErrors = [
861
903
  'SlotAlreadyInChain',
862
904
  'InvalidProposer',
863
905
  'InvalidArchive'
864
906
  ];
865
- const pipelined = opts.pipelined ?? this.epochCache.isProposerPipeliningEnabled();
866
- const slotOffset = pipelined ? this.aztecSlotDuration : 0n;
907
+ const slotOffset = this.aztecSlotDuration;
867
908
  const nextL1SlotTs = this.getNextL1SlotTimestamp() + slotOffset;
868
- return this.rollupContract.canProposeAt(tipArchive.toBuffer(), msgSender.toString(), nextL1SlotTs, {
869
- forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
870
- forceArchive: opts.forceArchive
871
- }).catch((err)=>{
872
- if (err instanceof FormattedViemError && ignoredErrors.find((e)=>err.message.includes(e))) {
873
- this.log.warn(`Failed canProposeAtTime check with ${ignoredErrors.find((e)=>err.message.includes(e))}`, {
909
+ return this.rollupContract.canProposeAt(tipArchive.toBuffer(), msgSender.toString(), nextL1SlotTs, await buildSimulationOverridesStateOverride(this.rollupContract, simulationOverridesPlan)).catch((err)=>{
910
+ if (err instanceof FormattedViemError && expectedErrors.find((e)=>err.message.includes(e))) {
911
+ this.log.warn(`Failed canProposeAtTime check with ${expectedErrors.find((e)=>err.message.includes(e))}`, {
874
912
  error: err.message
875
913
  });
876
914
  } else {
@@ -880,37 +918,31 @@ export class SequencerPublisher {
880
918
  });
881
919
  }
882
920
  /**
883
- * @notice Will simulate `validateHeader` to make sure that the block header is valid
884
- * @dev This is a convenience function that can be used by the sequencer to validate a "partial" header.
885
- * It will throw if the block header is invalid.
886
- * @param header - The block header to validate
887
- */ async validateBlockHeader(header, opts) {
921
+ * @notice Will simulate the rollup's `validateHeaderWithAttestations` to make sure the checkpoint header is valid
922
+ * @dev This is a convenience function that can be used by the sequencer to validate a "partial" header,
923
+ * skipping the DA and signature checks. It will throw if the checkpoint header is invalid.
924
+ * @param header - The checkpoint header to validate
925
+ */ async validateCheckpointHeader(header, simulationOverridesPlan) {
888
926
  const flags = {
889
927
  ignoreDA: true,
890
928
  ignoreSignatures: true
891
929
  };
892
930
  const args = [
893
931
  header.toViem(),
894
- CommitteeAttestationsAndSigners.empty().getPackedAttestations(),
932
+ CommitteeAttestationsAndSigners.packAttestations([]),
895
933
  [],
896
934
  Signature.empty().toViemSignature(),
897
935
  `0x${'0'.repeat(64)}`,
898
936
  header.blobsHash.toString(),
899
937
  flags
900
938
  ];
901
- const ts = this.getSimulationTimestamp(header.slotNumber);
902
- const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(opts?.forcePendingCheckpointNumber);
903
- let balance = 0n;
904
- if (this.config.fishermanMode) {
905
- // In fisherman mode, we can't know where the proposer is publishing from
906
- // so we just add sufficient balance to the multicall3 address
907
- balance = 10n * WEI_CONST * WEI_CONST; // 10 ETH
908
- } else {
909
- balance = await this.l1TxUtils.getSenderBalance();
910
- }
939
+ const l1Constants = this.epochCache.getL1Constants();
940
+ const ts = getLastL1SlotTimestampForL2Slot(header.slotNumber, l1Constants);
941
+ const stateOverrides = await buildSimulationOverridesStateOverride(this.rollupContract, simulationOverridesPlan);
942
+ // Balance override for compatibility with providers that apply an upfront funds check to simulated calls.
911
943
  stateOverrides.push({
912
944
  address: MULTI_CALL_3_ADDRESS,
913
- balance
945
+ balance: 10n * WEI_CONST * WEI_CONST
914
946
  });
915
947
  await this.l1TxUtils.simulate({
916
948
  to: this.rollupContract.address,
@@ -1031,7 +1063,11 @@ export class SequencerPublisher {
1031
1063
  reason
1032
1064
  };
1033
1065
  this.log.debug(`Building invalidate checkpoint ${checkpoint.checkpointNumber} request`, logData);
1034
- const attestationsAndSigners = new CommitteeAttestationsAndSigners(validationResult.attestations).getPackedAttestations();
1066
+ // Use the exact packed tuple posted to L1 verbatim. A repack via `packAttestations` is not a
1067
+ // byte-faithful inverse of `fromPacked` (a canonicalized yParity byte or an all-zero signature slot
1068
+ // round-trips differently), so it would diverge from the stored `attestationsHash` and revert the
1069
+ // invalidation.
1070
+ const attestationsAndSigners = validationResult.verbatimAttestations;
1035
1071
  if (reason === 'invalid-attestation') {
1036
1072
  return this.rollupContract.buildInvalidateBadAttestationRequest(checkpoint.checkpointNumber, attestationsAndSigners, committee, validationResult.invalidIndex);
1037
1073
  } else if (reason === 'insufficient-attestations') {
@@ -1041,25 +1077,6 @@ export class SequencerPublisher {
1041
1077
  throw new Error(`Unknown reason for invalidation`);
1042
1078
  }
1043
1079
  }
1044
- /** Simulates `propose` to make sure that the checkpoint is valid for submission */ async validateCheckpointForSubmission(checkpoint, attestationsAndSigners, attestationsAndSignersSignature, options) {
1045
- const blobFields = checkpoint.toBlobFields();
1046
- const blobs = await getBlobsPerL1Block(blobFields);
1047
- const blobInput = getPrefixedEthBlobCommitments(blobs);
1048
- const args = [
1049
- {
1050
- header: checkpoint.header.toViem(),
1051
- archive: toHex(checkpoint.archive.root.toBuffer()),
1052
- oracleInput: {
1053
- feeAssetPriceModifier: checkpoint.feeAssetPriceModifier
1054
- }
1055
- },
1056
- attestationsAndSigners.getPackedAttestations(),
1057
- attestationsAndSigners.getSigners().map((signer)=>signer.toString()),
1058
- attestationsAndSignersSignature.toViemSignature(),
1059
- blobInput
1060
- ];
1061
- await this.simulateProposeTx(args, options);
1062
- }
1063
1080
  async enqueueCastSignalHelper(slotNumber, signalType, payload, base, signerAddress, signer) {
1064
1081
  if (this.lastActions[signalType] && this.lastActions[signalType] === slotNumber) {
1065
1082
  this.log.debug(`Skipping duplicate vote cast signal ${signalType} for slot ${slotNumber}`);
@@ -1072,6 +1089,17 @@ export class SequencerPublisher {
1072
1089
  this.log.warn(`Cannot enqueue vote cast signal ${signalType} for address zero at slot ${slotNumber}`);
1073
1090
  return false;
1074
1091
  }
1092
+ const canonicalRollup = await base.getRollupAddress();
1093
+ if (!canonicalRollup.equals(EthAddress.fromString(this.rollupContract.address))) {
1094
+ this.log.warn(`Rollup ${this.rollupContract.address} is not canonical, skipping governance signal`, {
1095
+ slotNumber,
1096
+ signalType,
1097
+ canonicalRollup,
1098
+ targetRollup: this.rollupContract.address,
1099
+ payload: payload.toString()
1100
+ });
1101
+ return false;
1102
+ }
1075
1103
  const round = await base.computeRound(slotNumber);
1076
1104
  const roundInfo = await base.getRoundInfo(this.rollupContract.address, round);
1077
1105
  if (roundInfo.quorumReached) {
@@ -1080,30 +1108,40 @@ export class SequencerPublisher {
1080
1108
  if (roundInfo.lastSignalSlot >= slotNumber) {
1081
1109
  return false;
1082
1110
  }
1083
- if (await this.isPayloadEmpty(payload)) {
1111
+ if (await base.isPayloadEmpty(payload)) {
1084
1112
  this.log.warn(`Skipping vote cast for payload with empty code`);
1085
1113
  return false;
1086
1114
  }
1087
- // Check if payload was already submitted to governance
1088
- const cacheKey = payload.toString();
1089
- if (!this.payloadProposedCache.has(cacheKey)) {
1090
- try {
1091
- const l1StartBlock = await this.rollupContract.getL1StartBlock();
1092
- const proposed = await retry(()=>base.hasPayloadBeenProposed(payload.toString(), l1StartBlock), 'Check if payload was proposed', makeBackoff([
1093
- 0,
1094
- 1,
1095
- 2
1096
- ]), this.log, true);
1097
- if (proposed) {
1098
- this.payloadProposedCache.add(cacheKey);
1099
- }
1100
- } catch (err) {
1101
- this.log.warn(`Failed to check if payload ${payload} was proposed after retries, skipping signal`, err);
1102
- return false;
1103
- }
1115
+ // Classify the payload against the Governance proposal history so we stop signalling once its
1116
+ // proposal is live or was already executed, while still re-signalling one whose proposal was
1117
+ // merely rejected/dropped/expired.
1118
+ let status = 'none';
1119
+ try {
1120
+ status = await base.getPayloadProposalStatus(payload.toString());
1121
+ } catch (err) {
1122
+ // We deliberately swallow the error and proceed to signal. Failing closed (skipping the
1123
+ // signal) on transient RPC errors would let a flaky L1 endpoint silence governance
1124
+ // participation entirely; failing open at worst produces a duplicate signal that the
1125
+ // contract will simply count alongside others in the round.
1126
+ this.log.error(`Failed to check governance proposal status for payload ${payload} (signalling anyway)`, err, {
1127
+ slotNumber,
1128
+ signalType
1129
+ });
1104
1130
  }
1105
- if (this.payloadProposedCache.has(cacheKey)) {
1106
- this.log.info(`Payload ${payload} was already proposed to governance, stopping signals`);
1131
+ if (status === 'live') {
1132
+ this.log.info(`Payload ${payload} has a live governance proposal, stopping signals`, {
1133
+ slotNumber,
1134
+ signalType,
1135
+ payload: payload.toString()
1136
+ });
1137
+ return false;
1138
+ }
1139
+ if (status === 'executed' && !this.config.governanceProposerForcePayloadVote) {
1140
+ this.log.info(`Payload ${payload} was executed by governance within lookback, stopping signals ` + `(set GOVERNANCE_PROPOSER_FORCE_PAYLOAD_VOTE to re-signal)`, {
1141
+ slotNumber,
1142
+ signalType,
1143
+ payload: payload.toString()
1144
+ });
1107
1145
  return false;
1108
1146
  }
1109
1147
  const cachedLastVote = this.lastActions[signalType];
@@ -1116,57 +1154,17 @@ export class SequencerPublisher {
1116
1154
  signer: this.l1TxUtils.client.account?.address,
1117
1155
  lastValidL2Slot: slotNumber
1118
1156
  });
1119
- const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1120
- const timestamp = this.getSimulationTimestamp(slotNumber);
1121
- try {
1122
- await this.l1TxUtils.simulate(request, {
1123
- time: timestamp
1124
- }, [], mergeAbis([
1125
- request.abi ?? [],
1126
- ErrorsAbi
1127
- ]));
1128
- this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, {
1129
- request
1130
- });
1131
- } catch (err) {
1132
- const viemError = formatViemError(err);
1133
- this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, viemError, {
1134
- simulationTimestamp: timestamp,
1135
- l1BlockNumber
1136
- });
1137
- this.backupFailedTx({
1138
- id: keccak256(request.data),
1139
- failureType: 'simulation',
1140
- request: {
1141
- to: request.to,
1142
- data: request.data,
1143
- value: request.value?.toString()
1144
- },
1145
- l1BlockNumber: l1BlockNumber.toString(),
1146
- error: {
1147
- message: viemError.message,
1148
- name: viemError.name
1149
- },
1150
- context: {
1151
- actions: [
1152
- action
1153
- ],
1154
- slot: slotNumber,
1155
- sender: this.getSenderAddress().toString()
1156
- }
1157
- });
1158
- // Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
1159
- }
1160
1157
  // TODO(palla/slash): All votes (governance and slashing) should txTimeoutAt at the end of the slot.
1161
1158
  this.addRequest({
1162
- gasConfig: {
1163
- gasLimit: SequencerPublisher.VOTE_GAS_GUESS
1164
- },
1165
1159
  action,
1166
1160
  request,
1167
1161
  lastValidL2Slot: slotNumber,
1168
1162
  checkSuccess: (_request, result)=>{
1169
- const success = result && result.receipt && result.receipt.status === 'success' && tryExtractEvent(result.receipt.logs, base.address.toString(), EmpireBaseAbi, 'SignalCast');
1163
+ const success = result && extractEventSuccess(result.receipt, {
1164
+ address: base.address.toString(),
1165
+ abi: EmpireBaseAbi,
1166
+ eventName: 'SignalCast'
1167
+ });
1170
1168
  const logData = {
1171
1169
  ...result,
1172
1170
  slotNumber,
@@ -1185,16 +1183,6 @@ export class SequencerPublisher {
1185
1183
  });
1186
1184
  return true;
1187
1185
  }
1188
- async isPayloadEmpty(payload) {
1189
- const key = payload.toString();
1190
- const cached = this.isPayloadEmptyCache.get(key);
1191
- if (cached) {
1192
- return cached;
1193
- }
1194
- const isEmpty = !await this.l1TxUtils.getCode(payload);
1195
- this.isPayloadEmptyCache.set(key, isEmpty);
1196
- return isEmpty;
1197
- }
1198
1186
  /**
1199
1187
  * Enqueues a governance castSignal transaction to cast a signal for a given slot number.
1200
1188
  * @param slotNumber - The slot number to cast a signal for.
@@ -1202,6 +1190,56 @@ export class SequencerPublisher {
1202
1190
  */ enqueueGovernanceCastSignal(governancePayload, slotNumber, signerAddress, signer) {
1203
1191
  return this.enqueueCastSignalHelper(slotNumber, 'governance-signal', governancePayload, this.govProposerContract, signerAddress, signer);
1204
1192
  }
1193
+ /**
1194
+ * Enqueues a `prune()` transaction if the rollup is prunable at the given slot's L1 timestamp.
1195
+ * `prune()` is permissionless and idempotent — if the chain is no longer prunable by send time the
1196
+ * bundle simulation usually drops the entry; on a node without `eth_simulateV1` the bundle is sent
1197
+ * as-is and the prune reverts `Rollup__NothingToPrune` inside `aggregate3(allowFailure: true)`
1198
+ * (a failed action, never a whole-tx revert). Used by the failed-sync fallback so a stuck pending
1199
+ * chain (e.g. bad data blocking sync) can be wound back to recover.
1200
+ * @returns true if a prune request was enqueued, false otherwise.
1201
+ */ async enqueuePruneIfPrunable(slotNumber) {
1202
+ if (this.lastActions['prune'] === slotNumber) {
1203
+ this.log.debug(`Skipping duplicate prune for slot ${slotNumber}`, {
1204
+ slotNumber
1205
+ });
1206
+ return false;
1207
+ }
1208
+ // Use the SAME timestamp the bundle simulator overrides block.timestamp with at send time
1209
+ // (sequencer-bundle-simulator.ts) so this upfront check and the send-time sim agree. Slot-start
1210
+ // and last-L1-slot both fall within the same L2 slot (and epoch, which is what `canPruneAtTime`
1211
+ // derives), so they agree today; matching the simulator keeps it robust if the contract ever uses
1212
+ // the timestamp more granularly.
1213
+ const ts = getLastL1SlotTimestampForL2Slot(slotNumber, this.epochCache.getL1Constants());
1214
+ const canPrune = await this.rollupContract.canPruneAtTime(ts).catch((err)=>{
1215
+ this.log.error(`Failed to check canPruneAtTime for slot ${slotNumber}`, err, {
1216
+ slotNumber
1217
+ });
1218
+ return false;
1219
+ });
1220
+ if (!canPrune) {
1221
+ this.log.debug(`Rollup not prunable at slot ${slotNumber}`, {
1222
+ slotNumber
1223
+ });
1224
+ return false;
1225
+ }
1226
+ const request = {
1227
+ to: this.rollupContract.address,
1228
+ data: encodeFunctionData({
1229
+ abi: RollupAbi,
1230
+ functionName: 'prune',
1231
+ args: []
1232
+ })
1233
+ };
1234
+ this.log.info(`Enqueuing rollup prune for slot ${slotNumber}`, {
1235
+ slotNumber
1236
+ });
1237
+ return this.enqueueRequest('prune', request, {
1238
+ address: this.rollupContract.address,
1239
+ abi: RollupAbi,
1240
+ eventName: 'PrunedPending'
1241
+ }, slotNumber);
1242
+ }
1205
1243
  /** Enqueues all slashing actions as returned by the slasher client. */ async enqueueSlashingActions(actions, slotNumber, signerAddress, signer) {
1206
1244
  if (actions.length === 0) {
1207
1245
  this.log.debug(`No slashing actions to enqueue for slot ${slotNumber}`);
@@ -1209,43 +1247,6 @@ export class SequencerPublisher {
1209
1247
  }
1210
1248
  for (const action of actions){
1211
1249
  switch(action.type){
1212
- case 'vote-empire-payload':
1213
- {
1214
- if (this.slashingProposerContract?.type !== 'empire') {
1215
- this.log.error('Cannot vote for empire payload on non-empire slashing contract');
1216
- break;
1217
- }
1218
- this.log.debug(`Enqueuing slashing vote for payload ${action.payload} at slot ${slotNumber}`, {
1219
- signerAddress
1220
- });
1221
- await this.enqueueCastSignalHelper(slotNumber, 'empire-slashing-signal', action.payload, this.slashingProposerContract, signerAddress, signer);
1222
- break;
1223
- }
1224
- case 'create-empire-payload':
1225
- {
1226
- this.log.debug(`Enqueuing slashing create payload at slot ${slotNumber}`, {
1227
- slotNumber,
1228
- signerAddress
1229
- });
1230
- const request = this.slashFactoryContract.buildCreatePayloadRequest(action.data);
1231
- await this.simulateAndEnqueueRequest('create-empire-payload', request, (receipt)=>!!this.slashFactoryContract.tryExtractSlashPayloadCreatedEvent(receipt.logs), slotNumber);
1232
- break;
1233
- }
1234
- case 'execute-empire-payload':
1235
- {
1236
- this.log.debug(`Enqueuing slashing execute payload at slot ${slotNumber}`, {
1237
- slotNumber,
1238
- signerAddress
1239
- });
1240
- if (this.slashingProposerContract?.type !== 'empire') {
1241
- this.log.error('Cannot execute slashing payload on non-empire slashing contract');
1242
- return false;
1243
- }
1244
- const empireSlashingProposer = this.slashingProposerContract;
1245
- const request = empireSlashingProposer.buildExecuteRoundRequest(action.round);
1246
- await this.simulateAndEnqueueRequest('execute-empire-payload', request, (receipt)=>!!empireSlashingProposer.tryExtractPayloadSubmittedEvent(receipt.logs), slotNumber);
1247
- break;
1248
- }
1249
1250
  case 'vote-offenses':
1250
1251
  {
1251
1252
  this.log.debug(`Enqueuing slashing vote for ${action.votes.length} votes at slot ${slotNumber}`, {
@@ -1254,14 +1255,17 @@ export class SequencerPublisher {
1254
1255
  votesCount: action.votes.length,
1255
1256
  signerAddress
1256
1257
  });
1257
- if (this.slashingProposerContract?.type !== 'tally') {
1258
- this.log.error('Cannot vote for slashing offenses on non-tally slashing contract');
1258
+ if (!this.slashingProposerContract) {
1259
+ this.log.error('No slashing proposer contract available');
1259
1260
  return false;
1260
1261
  }
1261
- const tallySlashingProposer = this.slashingProposerContract;
1262
1262
  const votes = bufferToHex(encodeSlashConsensusVotes(action.votes));
1263
- const request = await tallySlashingProposer.buildVoteRequestFromSigner(votes, slotNumber, signer);
1264
- await this.simulateAndEnqueueRequest('vote-offenses', request, (receipt)=>!!tallySlashingProposer.tryExtractVoteCastEvent(receipt.logs), slotNumber);
1263
+ const request = await this.slashingProposerContract.buildVoteRequestFromSigner(votes, slotNumber, signer);
1264
+ this.enqueueRequest('vote-offenses', request, {
1265
+ address: this.slashingProposerContract.address.toString(),
1266
+ abi: SlashingProposerAbi,
1267
+ eventName: 'VoteCast'
1268
+ }, slotNumber);
1265
1269
  break;
1266
1270
  }
1267
1271
  case 'execute-slash':
@@ -1271,13 +1275,16 @@ export class SequencerPublisher {
1271
1275
  round: action.round,
1272
1276
  signerAddress
1273
1277
  });
1274
- if (this.slashingProposerContract?.type !== 'tally') {
1275
- this.log.error('Cannot execute slashing offenses on non-tally slashing contract');
1278
+ if (!this.slashingProposerContract) {
1279
+ this.log.error('No slashing proposer contract available');
1276
1280
  return false;
1277
1281
  }
1278
- const tallySlashingProposer = this.slashingProposerContract;
1279
- const request = tallySlashingProposer.buildExecuteRoundRequest(action.round, action.committees);
1280
- await this.simulateAndEnqueueRequest('execute-slash', request, (receipt)=>!!tallySlashingProposer.tryExtractRoundExecutedEvent(receipt.logs), slotNumber);
1282
+ const executeRequest = this.slashingProposerContract.buildExecuteRoundRequest(action.round, action.committees);
1283
+ this.enqueueRequest('execute-slash', executeRequest, {
1284
+ address: this.slashingProposerContract.address.toString(),
1285
+ abi: SlashingProposerAbi,
1286
+ eventName: 'RoundExecuted'
1287
+ }, slotNumber);
1281
1288
  break;
1282
1289
  }
1283
1290
  default:
@@ -1289,7 +1296,7 @@ export class SequencerPublisher {
1289
1296
  }
1290
1297
  return true;
1291
1298
  }
1292
- /** Simulates and enqueues a proposal for a checkpoint on L1 */ async enqueueProposeCheckpoint(checkpoint, attestationsAndSigners, attestationsAndSignersSignature, opts = {}) {
1299
+ /** Enqueues a proposal for a checkpoint on L1 */ async enqueueProposeCheckpoint(checkpoint, attestationsAndSigners, attestationsAndSignersSignature, opts = {}) {
1293
1300
  const checkpointHeader = checkpoint.header;
1294
1301
  const blobFields = checkpoint.toBlobFields();
1295
1302
  const blobs = await getBlobsPerL1Block(blobFields);
@@ -1301,51 +1308,38 @@ export class SequencerPublisher {
1301
1308
  attestationsAndSignersSignature,
1302
1309
  feeAssetPriceModifier: checkpoint.feeAssetPriceModifier
1303
1310
  };
1304
- try {
1305
- // @note This will make sure that we are passing the checks for our header ASSUMING that the data is also made available
1306
- // This means that we can avoid the simulation issues in later checks.
1307
- // By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which
1308
- // make time consistency checks break.
1309
- // TODO(palla): Check whether we're validating twice, once here and once within addProposeTx, since we call simulateProposeTx in both places.
1310
- await this.validateCheckpointForSubmission(checkpoint, attestationsAndSigners, attestationsAndSignersSignature, opts);
1311
- } catch (err) {
1312
- this.log.error(`Checkpoint validation failed. ${err instanceof Error ? err.message : 'No error message'}`, err, {
1313
- ...checkpoint.getStats(),
1314
- slotNumber: checkpoint.header.slotNumber,
1315
- forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber
1316
- });
1317
- throw err;
1318
- }
1319
1311
  this.log.verbose(`Enqueuing checkpoint propose transaction`, {
1320
1312
  ...checkpoint.toCheckpointInfo(),
1321
- ...opts
1313
+ txTimeoutAt: opts.txTimeoutAt
1314
+ });
1315
+ await this.addProposeTx(checkpoint, proposeTxArgs, {
1316
+ txTimeoutAt: opts.txTimeoutAt
1322
1317
  });
1323
- await this.addProposeTx(checkpoint, proposeTxArgs, opts);
1324
1318
  }
1325
1319
  enqueueInvalidateCheckpoint(request, opts = {}) {
1326
1320
  if (!request) {
1327
1321
  return;
1328
1322
  }
1329
- // We issued the simulation against the rollup contract, so we need to account for the overhead of the multicall3
1330
- const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil(Number(request.gasUsed) * 64 / 63)));
1331
1323
  const { gasUsed, checkpointNumber } = request;
1332
1324
  const logData = {
1333
1325
  gasUsed,
1334
1326
  checkpointNumber,
1335
- gasLimit,
1336
1327
  opts
1337
1328
  };
1338
1329
  this.log.verbose(`Enqueuing invalidate checkpoint request`, logData);
1339
1330
  this.addRequest({
1340
1331
  action: `invalidate-by-${request.reason}`,
1341
1332
  request: request.request,
1342
- gasConfig: {
1343
- gasLimit,
1333
+ gasConfig: opts.txTimeoutAt ? {
1344
1334
  txTimeoutAt: opts.txTimeoutAt
1345
- },
1335
+ } : undefined,
1346
1336
  lastValidL2Slot: SlotNumber(this.getCurrentL2Slot() + 2),
1347
1337
  checkSuccess: (_req, result)=>{
1348
- const success = result && result.receipt && result.receipt.status === 'success' && tryExtractEvent(result.receipt.logs, this.rollupContract.address, RollupAbi, 'CheckpointInvalidated');
1338
+ const success = result && extractEventSuccess(result.receipt, {
1339
+ address: this.rollupContract.address,
1340
+ abi: RollupAbi,
1341
+ eventName: 'CheckpointInvalidated'
1342
+ });
1349
1343
  if (!success) {
1350
1344
  this.log.warn(`Invalidate checkpoint ${request.checkpointNumber} failed`, {
1351
1345
  ...result,
@@ -1361,90 +1355,36 @@ export class SequencerPublisher {
1361
1355
  }
1362
1356
  });
1363
1357
  }
1364
- async simulateAndEnqueueRequest(action, request, checkSuccess, slotNumber) {
1365
- const timestamp = this.getSimulationTimestamp(slotNumber);
1366
- const logData = {
1367
- slotNumber,
1368
- timestamp,
1369
- gasLimit: undefined
1370
- };
1358
+ /**
1359
+ * Dedup-checked enqueue helper for actions that are simulated at bundle-send time rather
1360
+ * than at enqueue time. Validates the (action, slot) dedup key, sets `lastActions`, and
1361
+ * enqueues without a gasLimit so the bundle simulate sets the only gasLimit that matters.
1362
+ */ enqueueRequest(action, request, eventOpts, slotNumber) {
1371
1363
  if (this.lastActions[action] && this.lastActions[action] === slotNumber) {
1372
1364
  this.log.debug(`Skipping duplicate action ${action} for slot ${slotNumber}`);
1373
1365
  return false;
1374
1366
  }
1375
1367
  const cachedLastActionSlot = this.lastActions[action];
1376
1368
  this.lastActions[action] = slotNumber;
1377
- this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
1378
- const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1379
- let gasUsed;
1380
- const simulateAbi = mergeAbis([
1381
- request.abi ?? [],
1382
- ErrorsAbi
1383
- ]);
1384
- try {
1385
- ({ gasUsed } = await this.l1TxUtils.simulate(request, {
1386
- time: timestamp
1387
- }, [], simulateAbi));
1388
- this.log.verbose(`Simulation for ${action} succeeded`, {
1389
- ...logData,
1390
- request,
1391
- gasUsed
1392
- });
1393
- } catch (err) {
1394
- const viemError = formatViemError(err, simulateAbi);
1395
- this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
1396
- this.backupFailedTx({
1397
- id: keccak256(request.data),
1398
- failureType: 'simulation',
1399
- request: {
1400
- to: request.to,
1401
- data: request.data,
1402
- value: request.value?.toString()
1403
- },
1404
- l1BlockNumber: l1BlockNumber.toString(),
1405
- error: {
1406
- message: viemError.message,
1407
- name: viemError.name
1408
- },
1409
- context: {
1410
- actions: [
1411
- action
1412
- ],
1413
- slot: slotNumber,
1414
- sender: this.getSenderAddress().toString()
1415
- }
1416
- });
1417
- return false;
1418
- }
1419
- // We issued the simulation against the rollup contract, so we need to account for the overhead of the multicall3
1420
- const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil(Number(gasUsed) * 64 / 63)));
1421
- logData.gasLimit = gasLimit;
1422
- // Store the ABI used for simulation on the request so Multicall3.forward can decode errors
1423
- // when the tx is sent and a revert is diagnosed via simulation.
1424
- const requestWithAbi = {
1425
- ...request,
1426
- abi: simulateAbi
1427
- };
1428
- this.log.debug(`Enqueuing ${action}`, logData);
1369
+ this.log.debug(`Enqueuing ${action}`, {
1370
+ slotNumber
1371
+ });
1429
1372
  this.addRequest({
1430
1373
  action,
1431
- request: requestWithAbi,
1432
- gasConfig: {
1433
- gasLimit
1434
- },
1374
+ request,
1435
1375
  lastValidL2Slot: slotNumber,
1436
- checkSuccess: (_req, result)=>{
1437
- const success = result && result.receipt && result.receipt.status === 'success' && checkSuccess(result.receipt);
1376
+ checkSuccess: (_request, result)=>{
1377
+ const success = result && extractEventSuccess(result.receipt, eventOpts);
1438
1378
  if (!success) {
1439
1379
  this.log.warn(`Action ${action} at ${slotNumber} failed`, {
1440
1380
  ...result,
1441
- ...logData
1381
+ slotNumber
1442
1382
  });
1443
1383
  this.lastActions[action] = cachedLastActionSlot;
1444
1384
  } else {
1445
1385
  this.log.info(`Action ${action} at ${slotNumber} succeeded`, {
1446
1386
  ...result,
1447
- ...logData
1387
+ slotNumber
1448
1388
  });
1449
1389
  }
1450
1390
  return !!success;
@@ -1466,7 +1406,7 @@ export class SequencerPublisher {
1466
1406
  this.interrupted = false;
1467
1407
  this.l1TxUtils.restart();
1468
1408
  }
1469
- async prepareProposeTx(encodedData, options) {
1409
+ async prepareProposeTx(encodedData) {
1470
1410
  const kzg = Blob.getViemKzgInstance();
1471
1411
  const blobInput = getPrefixedEthBlobCommitments(encodedData.blobs);
1472
1412
  this.log.debug('Validating blob input', {
@@ -1480,7 +1420,11 @@ export class SequencerPublisher {
1480
1420
  blobEvaluationGas = BigInt(encodedData.blobs.length) * 21_000n;
1481
1421
  this.log.debug(`Using fixed blob evaluation gas estimate in fisherman mode: ${blobEvaluationGas}`);
1482
1422
  } else {
1483
- // Normal mode - use estimateGas with blob inputs
1423
+ // We call validateBlobs via estimateGas with real blob+kzg sidecars as a consistency check
1424
+ // that our locally-built blob commitments match the blob data. The bundle simulate at send
1425
+ // time uses eth_simulateV1, which cannot carry blob inputs, so the rollup's on-chain blob
1426
+ // check is forced off there — making this the only pre-flight detector of a commitment/data
1427
+ // mismatch. The returned gas estimate is stashed on the request for the bundle path to read.
1484
1428
  blobEvaluationGas = await this.l1TxUtils.estimateGas(this.getSenderAddress().toString(), {
1485
1429
  to: this.rollupContract.address,
1486
1430
  data: encodeFunctionData({
@@ -1543,113 +1487,23 @@ export class SequencerPublisher {
1543
1487
  encodedData.attestationsAndSignersSignature.toViemSignature(),
1544
1488
  blobInput
1545
1489
  ];
1546
- const { rollupData, simulationResult } = await this.simulateProposeTx(args, options);
1547
- return {
1548
- args,
1549
- blobEvaluationGas,
1550
- rollupData,
1551
- simulationResult
1552
- };
1553
- }
1554
- /**
1555
- * Simulates the propose tx with eth_simulateV1
1556
- * @param args - The propose tx args
1557
- * @returns The simulation result
1558
- */ async simulateProposeTx(args, options) {
1559
1490
  const rollupData = encodeFunctionData({
1560
1491
  abi: RollupAbi,
1561
1492
  functionName: 'propose',
1562
1493
  args
1563
1494
  });
1564
- // override the proposed checkpoint number if requested
1565
- const forcePendingCheckpointNumberStateDiff = (options.forcePendingCheckpointNumber !== undefined ? await this.rollupContract.makePendingCheckpointNumberOverride(options.forcePendingCheckpointNumber) : []).flatMap((override)=>override.stateDiff ?? []);
1566
- // override the fee header for a specific checkpoint number if requested (used when pipelining)
1567
- const forceProposedFeeHeaderStateDiff = (options.forceProposedFeeHeader !== undefined ? await this.rollupContract.makeFeeHeaderOverride(options.forceProposedFeeHeader.checkpointNumber, options.forceProposedFeeHeader.feeHeader) : []).flatMap((override)=>override.stateDiff ?? []);
1568
- const stateOverrides = [
1569
- {
1570
- address: this.rollupContract.address,
1571
- // @note we override checkBlob to false since blobs are not part simulate()
1572
- stateDiff: [
1573
- {
1574
- slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true),
1575
- value: toPaddedHex(0n, true)
1576
- },
1577
- ...forcePendingCheckpointNumberStateDiff,
1578
- ...forceProposedFeeHeaderStateDiff
1579
- ]
1580
- }
1581
- ];
1582
- // In fisherman mode, simulate as the proposer but with sufficient balance
1583
- if (this.proposerAddressForSimulation) {
1584
- stateOverrides.push({
1585
- address: this.proposerAddressForSimulation.toString(),
1586
- balance: 10n * WEI_CONST * WEI_CONST
1587
- });
1588
- }
1589
- const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1590
- const simTs = this.getSimulationTimestamp(SlotNumber.fromBigInt(args[0].header.slotNumber));
1591
- const simulationResult = await this.l1TxUtils.simulate({
1592
- to: this.rollupContract.address,
1593
- data: rollupData,
1594
- gas: MAX_L1_TX_LIMIT,
1595
- ...this.proposerAddressForSimulation && {
1596
- from: this.proposerAddressForSimulation.toString()
1597
- }
1598
- }, {
1599
- time: simTs,
1600
- // @note reth should have a 30m gas limit per block but throws errors that this tx is beyond limit so we increase here
1601
- gasLimit: MAX_L1_TX_LIMIT * 2n
1602
- }, stateOverrides, RollupAbi, {
1603
- // @note fallback gas estimate to use if the node doesn't support simulation API
1604
- fallbackGasEstimate: MAX_L1_TX_LIMIT
1605
- }).catch((err)=>{
1606
- // In fisherman mode, we expect ValidatorSelection__MissingProposerSignature since fisherman doesn't have proposer signature
1607
- const viemError = formatViemError(err);
1608
- if (this.config.fishermanMode && viemError.message?.includes('ValidatorSelection__MissingProposerSignature')) {
1609
- this.log.debug(`Ignoring expected ValidatorSelection__MissingProposerSignature error in fisherman mode`);
1610
- // Return a minimal simulation result with the fallback gas estimate
1611
- return {
1612
- gasUsed: MAX_L1_TX_LIMIT,
1613
- logs: []
1614
- };
1615
- }
1616
- this.log.error(`Failed to simulate propose tx`, viemError, {
1617
- simulationTimestamp: simTs
1618
- });
1619
- this.backupFailedTx({
1620
- id: keccak256(rollupData),
1621
- failureType: 'simulation',
1622
- request: {
1623
- to: this.rollupContract.address,
1624
- data: rollupData
1625
- },
1626
- l1BlockNumber: l1BlockNumber.toString(),
1627
- error: {
1628
- message: viemError.message,
1629
- name: viemError.name
1630
- },
1631
- context: {
1632
- actions: [
1633
- 'propose'
1634
- ],
1635
- slot: Number(args[0].header.slotNumber),
1636
- sender: this.getSenderAddress().toString()
1637
- }
1638
- });
1639
- throw err;
1640
- });
1641
1495
  return {
1642
- rollupData,
1643
- simulationResult
1496
+ args,
1497
+ blobEvaluationGas,
1498
+ rollupData
1644
1499
  };
1645
1500
  }
1646
1501
  async addProposeTx(checkpoint, encodedData, opts = {}) {
1647
1502
  const slot = checkpoint.header.slotNumber;
1648
1503
  const timer = new Timer();
1649
1504
  const kzg = Blob.getViemKzgInstance();
1650
- const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(encodedData, opts);
1505
+ const { rollupData, blobEvaluationGas } = await this.prepareProposeTx(encodedData);
1651
1506
  const startBlock = await this.l1TxUtils.getBlockNumber();
1652
- const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil(Number(simulationResult.gasUsed) * 64 / 63)) + blobEvaluationGas + SequencerPublisher.MULTICALL_OVERHEAD_GAS_GUESS);
1653
1507
  // Send the blobs to the blob client preemptively. This helps in tests where the sequencer mistakingly thinks that the propose
1654
1508
  // tx fails but it does get mined. We make sure that the blobs are sent to the blob client regardless of the tx outcome.
1655
1509
  void Promise.resolve().then(()=>this.blobClient.sendBlobsToFilestore(encodedData.blobs).catch((_err)=>{
@@ -1663,9 +1517,10 @@ export class SequencerPublisher {
1663
1517
  },
1664
1518
  lastValidL2Slot: checkpoint.header.slotNumber,
1665
1519
  gasConfig: {
1666
- ...opts,
1667
- gasLimit
1520
+ txTimeoutAt: opts.txTimeoutAt,
1521
+ gasLimit: undefined
1668
1522
  },
1523
+ blobEvaluationGas,
1669
1524
  blobConfig: {
1670
1525
  blobs: encodedData.blobs.map((b)=>b.data),
1671
1526
  kzg
@@ -1675,7 +1530,11 @@ export class SequencerPublisher {
1675
1530
  return false;
1676
1531
  }
1677
1532
  const { receipt, stats, errorMsg } = result;
1678
- const success = receipt && receipt.status === 'success' && tryExtractEvent(receipt.logs, this.rollupContract.address, RollupAbi, 'CheckpointProposed');
1533
+ const success = extractEventSuccess(receipt, {
1534
+ address: this.rollupContract.address,
1535
+ abi: RollupAbi,
1536
+ eventName: 'CheckpointProposed'
1537
+ });
1679
1538
  if (success) {
1680
1539
  const endBlock = receipt.blockNumber;
1681
1540
  const inclusionBlocks = Number(endBlock - startBlock);
@@ -1712,11 +1571,6 @@ export class SequencerPublisher {
1712
1571
  }
1713
1572
  });
1714
1573
  }
1715
- /** Returns the timestamp of the last L1 slot within a given L2 slot. Used as the simulation timestamp
1716
- * for eth_simulateV1 calls, since it's guaranteed to be greater than any L1 block produced during the slot. */ getSimulationTimestamp(slot) {
1717
- const l1Constants = this.epochCache.getL1Constants();
1718
- return getLastL1SlotTimestampForL2Slot(slot, l1Constants);
1719
- }
1720
1574
  /** Returns the timestamp of the next L1 slot boundary after now. */ getNextL1SlotTimestamp() {
1721
1575
  const l1Constants = this.epochCache.getL1Constants();
1722
1576
  return getNextL1SlotTimestamp(this.dateProvider.nowInSeconds(), l1Constants);