@aztec/sequencer-client 0.0.1-commit.1bea0213 → 0.0.1-commit.1dcfe2301

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