@aztec/sequencer-client 0.0.1-commit.5914bae → 0.0.1-commit.59a0419c6

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 (89) hide show
  1. package/dest/client/sequencer-client.d.ts +2 -1
  2. package/dest/client/sequencer-client.d.ts.map +1 -1
  3. package/dest/client/sequencer-client.js +8 -8
  4. package/dest/config.d.ts +3 -3
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +10 -12
  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 +3 -8
  14. package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
  15. package/dest/global_variable_builder/global_builder.js +4 -39
  16. package/dest/global_variable_builder/index.d.ts +3 -1
  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 +17 -1
  20. package/dest/publisher/config.d.ts.map +1 -1
  21. package/dest/publisher/config.js +25 -5
  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 +3 -5
  37. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  38. package/dest/publisher/sequencer-publisher-factory.js +16 -3
  39. package/dest/publisher/sequencer-publisher.d.ts +52 -41
  40. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  41. package/dest/publisher/sequencer-publisher.js +345 -101
  42. package/dest/sequencer/chain_state_overrides.d.ts +25 -0
  43. package/dest/sequencer/chain_state_overrides.d.ts.map +1 -0
  44. package/dest/sequencer/chain_state_overrides.js +39 -0
  45. package/dest/sequencer/checkpoint_proposal_job.d.ts +38 -8
  46. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  47. package/dest/sequencer/checkpoint_proposal_job.js +427 -171
  48. package/dest/sequencer/checkpoint_proposal_job_metrics.d.ts +34 -0
  49. package/dest/sequencer/checkpoint_proposal_job_metrics.d.ts.map +1 -0
  50. package/dest/sequencer/checkpoint_proposal_job_metrics.js +72 -0
  51. package/dest/sequencer/events.d.ts +7 -1
  52. package/dest/sequencer/events.d.ts.map +1 -1
  53. package/dest/sequencer/metrics.d.ts +11 -10
  54. package/dest/sequencer/metrics.d.ts.map +1 -1
  55. package/dest/sequencer/metrics.js +38 -20
  56. package/dest/sequencer/sequencer.d.ts +17 -5
  57. package/dest/sequencer/sequencer.d.ts.map +1 -1
  58. package/dest/sequencer/sequencer.js +120 -58
  59. package/dest/sequencer/timetable.d.ts +14 -1
  60. package/dest/sequencer/timetable.d.ts.map +1 -1
  61. package/dest/sequencer/timetable.js +45 -36
  62. package/dest/test/mock_checkpoint_builder.d.ts +1 -1
  63. package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
  64. package/dest/test/mock_checkpoint_builder.js +0 -2
  65. package/package.json +27 -28
  66. package/src/client/sequencer-client.ts +8 -14
  67. package/src/config.ts +12 -11
  68. package/src/global_variable_builder/README.md +44 -0
  69. package/src/global_variable_builder/fee_predictor.ts +172 -0
  70. package/src/global_variable_builder/fee_provider.ts +75 -0
  71. package/src/global_variable_builder/global_builder.ts +8 -43
  72. package/src/global_variable_builder/index.ts +2 -0
  73. package/src/publisher/config.ts +47 -4
  74. package/src/publisher/index.ts +3 -0
  75. package/src/publisher/l1_tx_failed_store/factory.ts +32 -0
  76. package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +57 -0
  77. package/src/publisher/l1_tx_failed_store/file_store_failed_tx_store.ts +46 -0
  78. package/src/publisher/l1_tx_failed_store/index.ts +3 -0
  79. package/src/publisher/sequencer-publisher-factory.ts +18 -6
  80. package/src/publisher/sequencer-publisher.ts +389 -140
  81. package/src/sequencer/README.md +83 -13
  82. package/src/sequencer/chain_state_overrides.ts +87 -0
  83. package/src/sequencer/checkpoint_proposal_job.ts +517 -180
  84. package/src/sequencer/checkpoint_proposal_job_metrics.ts +128 -0
  85. package/src/sequencer/events.ts +6 -1
  86. package/src/sequencer/metrics.ts +49 -25
  87. package/src/sequencer/sequencer.ts +177 -63
  88. package/src/sequencer/timetable.ts +57 -45
  89. package/src/test/mock_checkpoint_builder.ts +0 -2
@@ -372,19 +372,20 @@ 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 { FeeAssetPriceOracle, MULTI_CALL_3_ADDRESS, Multicall3, RollupContract } from '@aztec/ethereum/contracts';
375
+ import { FeeAssetPriceOracle, MULTI_CALL_3_ADDRESS, Multicall3, SimulationOverridesBuilder, buildSimulationOverridesStateOverride } from '@aztec/ethereum/contracts';
376
376
  import { L1FeeAnalyzer } from '@aztec/ethereum/l1-fee-analysis';
377
377
  import { MAX_L1_TX_LIMIT, WEI_CONST } from '@aztec/ethereum/l1-tx-utils';
378
378
  import { FormattedViemError, formatViemError, mergeAbis, tryExtractEvent } from '@aztec/ethereum/utils';
379
379
  import { sumBigint } from '@aztec/foundation/bigint';
380
- import { toHex as toPaddedHex } from '@aztec/foundation/bigint-buffer';
381
380
  import { CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
382
381
  import { trimmedBytesLength } from '@aztec/foundation/buffer';
383
382
  import { pick } from '@aztec/foundation/collection';
383
+ import { TimeoutError } from '@aztec/foundation/error';
384
384
  import { EthAddress } from '@aztec/foundation/eth-address';
385
385
  import { Signature } from '@aztec/foundation/eth-signature';
386
386
  import { createLogger } from '@aztec/foundation/log';
387
387
  import { makeBackoff, retry } from '@aztec/foundation/retry';
388
+ import { InterruptibleSleep } from '@aztec/foundation/sleep';
388
389
  import { bufferToHex } from '@aztec/foundation/string';
389
390
  import { Timer } from '@aztec/foundation/timer';
390
391
  import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
@@ -392,16 +393,14 @@ import { encodeSlashConsensusVotes } from '@aztec/slasher';
392
393
  import { CommitteeAttestationsAndSigners } from '@aztec/stdlib/block';
393
394
  import { getLastL1SlotTimestampForL2Slot, getNextL1SlotTimestamp } from '@aztec/stdlib/epoch-helpers';
394
395
  import { getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
395
- import { encodeFunctionData, toHex } from 'viem';
396
+ import { encodeFunctionData, keccak256, multicall3Abi, toHex } from 'viem';
397
+ import { createL1TxFailedStore } from './l1_tx_failed_store/index.js';
396
398
  import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
397
399
  export const Actions = [
398
400
  'invalidate-by-invalid-attestation',
399
401
  'invalidate-by-insufficient-attestations',
400
402
  'propose',
401
403
  'governance-signal',
402
- 'empire-slashing-signal',
403
- 'create-empire-payload',
404
- 'execute-empire-payload',
405
404
  'vote-offenses',
406
405
  'execute-slash'
407
406
  ];
@@ -432,6 +431,7 @@ export class SequencerPublisher {
432
431
  interrupted;
433
432
  metrics;
434
433
  epochCache;
434
+ failedTxStore;
435
435
  governanceLog;
436
436
  slashingLog;
437
437
  lastActions;
@@ -440,11 +440,13 @@ export class SequencerPublisher {
440
440
  log;
441
441
  ethereumSlotDuration;
442
442
  aztecSlotDuration;
443
- dateProvider;
443
+ /** Date provider for wall-clock time. */ dateProvider;
444
444
  blobClient;
445
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;
446
447
  /** L1 fee analyzer for fisherman mode */ l1FeeAnalyzer;
447
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;
448
450
  // A CALL to a cold address is 2700 gas
449
451
  static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
450
452
  // Gas report for VotingWithSigTest shows a max gas of 100k, but we've seen it cost 700k+ in testnet
@@ -453,7 +455,6 @@ export class SequencerPublisher {
453
455
  rollupContract;
454
456
  govProposerContract;
455
457
  slashingProposerContract;
456
- slashFactoryContract;
457
458
  tracer;
458
459
  requests;
459
460
  constructor(config, deps){
@@ -464,6 +465,7 @@ export class SequencerPublisher {
464
465
  this.lastActions = {};
465
466
  this.isPayloadEmptyCache = new Map();
466
467
  this.payloadProposedCache = new Set();
468
+ this.interruptibleSleep = new InterruptibleSleep();
467
469
  this.requests = [];
468
470
  this.log = deps.log ?? createLogger('sequencer:publisher');
469
471
  this.ethereumSlotDuration = BigInt(config.ethereumSlotDuration);
@@ -472,10 +474,12 @@ export class SequencerPublisher {
472
474
  this.epochCache = deps.epochCache;
473
475
  this.lastActions = deps.lastActions;
474
476
  this.blobClient = deps.blobClient;
477
+ this.dateProvider = deps.dateProvider;
475
478
  const telemetry = deps.telemetry ?? getTelemetryClient();
476
479
  this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
477
480
  this.tracer = telemetry.getTracer('SequencerPublisher');
478
481
  this.l1TxUtils = deps.l1TxUtils;
482
+ this.getNextPublisher = deps.getNextPublisher;
479
483
  this.rollupContract = deps.rollupContract;
480
484
  this.govProposerContract = deps.governanceProposerContract;
481
485
  this.slashingProposerContract = deps.slashingProposerContract;
@@ -484,13 +488,30 @@ export class SequencerPublisher {
484
488
  const newSlashingProposer = await this.rollupContract.getSlashingProposer();
485
489
  this.slashingProposerContract = newSlashingProposer;
486
490
  });
487
- this.slashFactoryContract = deps.slashFactoryContract;
488
491
  // Initialize L1 fee analyzer for fisherman mode
489
492
  if (config.fishermanMode) {
490
493
  this.l1FeeAnalyzer = new L1FeeAnalyzer(this.l1TxUtils.client, deps.dateProvider, createLogger('sequencer:publisher:fee-analyzer'));
491
494
  }
492
495
  // Initialize fee asset price oracle
493
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
+ });
494
515
  }
495
516
  getRollupContract() {
496
517
  return this.rollupContract;
@@ -519,7 +540,7 @@ export class SequencerPublisher {
519
540
  this.requests.push(request);
520
541
  }
521
542
  getCurrentL2Slot() {
522
- return this.epochCache.getEpochAndSlotNow().slot;
543
+ return this.epochCache.getSlotNow();
523
544
  }
524
545
  /**
525
546
  * Clears all pending requests without sending them.
@@ -636,12 +657,34 @@ export class SequencerPublisher {
636
657
  // This ensures the committee gets precomputed correctly
637
658
  validRequests.sort((a, b)=>compareActions(a.action, b.action));
638
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
+ };
639
679
  this.log.debug('Forwarding transactions', {
640
680
  validRequests: validRequests.map((request)=>request.action),
641
681
  txConfig
642
682
  });
643
- const result = await Multicall3.forward(validRequests.map((request)=>request.request), this.l1TxUtils, txConfig, blobConfig, this.rollupContract.address, this.log);
644
- 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);
645
688
  return {
646
689
  result,
647
690
  expiredActions,
@@ -661,10 +704,102 @@ export class SequencerPublisher {
661
704
  }
662
705
  }
663
706
  }
664
- 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) {
665
777
  const actionsListStr = requests.map((r)=>r.action).join(', ');
666
778
  if (result instanceof FormattedViemError) {
667
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
+ });
668
803
  return {
669
804
  failedActions: requests.map((r)=>r.action)
670
805
  };
@@ -691,6 +826,37 @@ export class SequencerPublisher {
691
826
  failedActions.push(request.action);
692
827
  }
693
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
+ }
694
860
  return {
695
861
  successfulActions,
696
862
  failedActions
@@ -701,19 +867,17 @@ export class SequencerPublisher {
701
867
  * @notice Will call `canProposeAt` to make sure that it is possible to propose
702
868
  * @param tipArchive - The archive to check
703
869
  * @returns The slot and block number if it is possible to propose, undefined otherwise
704
- */ canProposeAt(tipArchive, msgSender, opts = {}) {
870
+ */ async canProposeAt(tipArchive, msgSender, simulationOverridesPlan) {
705
871
  // TODO: #14291 - should loop through multiple keys to check if any of them can propose
706
872
  const ignoredErrors = [
707
873
  'SlotAlreadyInChain',
708
874
  'InvalidProposer',
709
875
  'InvalidArchive'
710
876
  ];
711
- const pipelined = opts.pipelined ?? false;
877
+ const pipelined = this.epochCache.isProposerPipeliningEnabled();
712
878
  const slotOffset = pipelined ? this.aztecSlotDuration : 0n;
713
879
  const nextL1SlotTs = this.getNextL1SlotTimestamp() + slotOffset;
714
- return this.rollupContract.canProposeAt(tipArchive.toBuffer(), msgSender.toString(), nextL1SlotTs, {
715
- forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber
716
- }).catch((err)=>{
880
+ return this.rollupContract.canProposeAt(tipArchive.toBuffer(), msgSender.toString(), nextL1SlotTs, await buildSimulationOverridesStateOverride(this.rollupContract, simulationOverridesPlan)).catch((err)=>{
717
881
  if (err instanceof FormattedViemError && ignoredErrors.find((e)=>err.message.includes(e))) {
718
882
  this.log.warn(`Failed canProposeAtTime check with ${ignoredErrors.find((e)=>err.message.includes(e))}`, {
719
883
  error: err.message
@@ -729,7 +893,7 @@ export class SequencerPublisher {
729
893
  * @dev This is a convenience function that can be used by the sequencer to validate a "partial" header.
730
894
  * It will throw if the block header is invalid.
731
895
  * @param header - The block header to validate
732
- */ async validateBlockHeader(header, opts) {
896
+ */ async validateBlockHeader(header, simulationOverridesPlan) {
733
897
  const flags = {
734
898
  ignoreDA: true,
735
899
  ignoreSignatures: true
@@ -744,7 +908,7 @@ export class SequencerPublisher {
744
908
  flags
745
909
  ];
746
910
  const ts = this.getSimulationTimestamp(header.slotNumber);
747
- const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(opts?.forcePendingCheckpointNumber);
911
+ const stateOverrides = await buildSimulationOverridesStateOverride(this.rollupContract, simulationOverridesPlan);
748
912
  let balance = 0n;
749
913
  if (this.config.fishermanMode) {
750
914
  // In fisherman mode, we can't know where the proposer is publishing from
@@ -796,6 +960,7 @@ export class SequencerPublisher {
796
960
  ...logData,
797
961
  request
798
962
  });
963
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
799
964
  try {
800
965
  const { gasUsed } = await this.l1TxUtils.simulate(request, undefined, undefined, mergeAbis([
801
966
  request.abi ?? [],
@@ -811,6 +976,7 @@ export class SequencerPublisher {
811
976
  gasUsed,
812
977
  checkpointNumber,
813
978
  forcePendingCheckpointNumber: CheckpointNumber(checkpointNumber - 1),
979
+ lastArchive: validationResult.checkpoint.lastArchive,
814
980
  reason
815
981
  };
816
982
  } catch (err) {
@@ -823,8 +989,8 @@ export class SequencerPublisher {
823
989
  request,
824
990
  error: viemError.message
825
991
  });
826
- const latestPendingCheckpointNumber = await this.rollupContract.getCheckpointNumber();
827
- if (latestPendingCheckpointNumber < checkpointNumber) {
992
+ const latestProposedCheckpointNumber = await this.rollupContract.getCheckpointNumber();
993
+ if (latestProposedCheckpointNumber < checkpointNumber) {
828
994
  this.log.verbose(`Checkpoint ${checkpointNumber} has already been invalidated`, {
829
995
  ...logData
830
996
  });
@@ -838,6 +1004,27 @@ export class SequencerPublisher {
838
1004
  }
839
1005
  // Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
840
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
+ });
841
1028
  throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, {
842
1029
  cause: viemError
843
1030
  });
@@ -863,7 +1050,7 @@ export class SequencerPublisher {
863
1050
  throw new Error(`Unknown reason for invalidation`);
864
1051
  }
865
1052
  }
866
- /** Simulates `propose` to make sure that the checkpoint is valid for submission */ async validateCheckpointForSubmission(checkpoint, attestationsAndSigners, attestationsAndSignersSignature, options) {
1053
+ /** Simulates `propose` to make sure that the checkpoint is valid for submission */ async validateCheckpointForSubmission(checkpoint, attestationsAndSigners, attestationsAndSignersSignature, simulationOverridesPlan) {
867
1054
  const blobFields = checkpoint.toBlobFields();
868
1055
  const blobs = await getBlobsPerL1Block(blobFields);
869
1056
  const blobInput = getPrefixedEthBlobCommitments(blobs);
@@ -880,7 +1067,7 @@ export class SequencerPublisher {
880
1067
  attestationsAndSignersSignature.toViemSignature(),
881
1068
  blobInput
882
1069
  ];
883
- await this.simulateProposeTx(args, options);
1070
+ await this.simulateProposeTx(args, simulationOverridesPlan);
884
1071
  }
885
1072
  async enqueueCastSignalHelper(slotNumber, signalType, payload, base, signerAddress, signer) {
886
1073
  if (this.lastActions[signalType] && this.lastActions[signalType] === slotNumber) {
@@ -938,6 +1125,7 @@ export class SequencerPublisher {
938
1125
  signer: this.l1TxUtils.client.account?.address,
939
1126
  lastValidL2Slot: slotNumber
940
1127
  });
1128
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
941
1129
  const timestamp = this.getSimulationTimestamp(slotNumber);
942
1130
  try {
943
1131
  await this.l1TxUtils.simulate(request, {
@@ -952,7 +1140,29 @@ export class SequencerPublisher {
952
1140
  } catch (err) {
953
1141
  const viemError = formatViemError(err);
954
1142
  this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, viemError, {
955
- simulationTimestamp: timestamp
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
+ }
956
1166
  });
957
1167
  // Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
958
1168
  }
@@ -1008,43 +1218,6 @@ export class SequencerPublisher {
1008
1218
  }
1009
1219
  for (const action of actions){
1010
1220
  switch(action.type){
1011
- case 'vote-empire-payload':
1012
- {
1013
- if (this.slashingProposerContract?.type !== 'empire') {
1014
- this.log.error('Cannot vote for empire payload on non-empire slashing contract');
1015
- break;
1016
- }
1017
- this.log.debug(`Enqueuing slashing vote for payload ${action.payload} at slot ${slotNumber}`, {
1018
- signerAddress
1019
- });
1020
- await this.enqueueCastSignalHelper(slotNumber, 'empire-slashing-signal', action.payload, this.slashingProposerContract, signerAddress, signer);
1021
- break;
1022
- }
1023
- case 'create-empire-payload':
1024
- {
1025
- this.log.debug(`Enqueuing slashing create payload at slot ${slotNumber}`, {
1026
- slotNumber,
1027
- signerAddress
1028
- });
1029
- const request = this.slashFactoryContract.buildCreatePayloadRequest(action.data);
1030
- await this.simulateAndEnqueueRequest('create-empire-payload', request, (receipt)=>!!this.slashFactoryContract.tryExtractSlashPayloadCreatedEvent(receipt.logs), slotNumber);
1031
- break;
1032
- }
1033
- case 'execute-empire-payload':
1034
- {
1035
- this.log.debug(`Enqueuing slashing execute payload at slot ${slotNumber}`, {
1036
- slotNumber,
1037
- signerAddress
1038
- });
1039
- if (this.slashingProposerContract?.type !== 'empire') {
1040
- this.log.error('Cannot execute slashing payload on non-empire slashing contract');
1041
- return false;
1042
- }
1043
- const empireSlashingProposer = this.slashingProposerContract;
1044
- const request = empireSlashingProposer.buildExecuteRoundRequest(action.round);
1045
- await this.simulateAndEnqueueRequest('execute-empire-payload', request, (receipt)=>!!empireSlashingProposer.tryExtractPayloadSubmittedEvent(receipt.logs), slotNumber);
1046
- break;
1047
- }
1048
1221
  case 'vote-offenses':
1049
1222
  {
1050
1223
  this.log.debug(`Enqueuing slashing vote for ${action.votes.length} votes at slot ${slotNumber}`, {
@@ -1053,14 +1226,13 @@ export class SequencerPublisher {
1053
1226
  votesCount: action.votes.length,
1054
1227
  signerAddress
1055
1228
  });
1056
- if (this.slashingProposerContract?.type !== 'tally') {
1057
- 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');
1058
1231
  return false;
1059
1232
  }
1060
- const tallySlashingProposer = this.slashingProposerContract;
1061
1233
  const votes = bufferToHex(encodeSlashConsensusVotes(action.votes));
1062
- const request = await tallySlashingProposer.buildVoteRequestFromSigner(votes, slotNumber, signer);
1063
- await this.simulateAndEnqueueRequest('vote-offenses', request, (receipt)=>!!tallySlashingProposer.tryExtractVoteCastEvent(receipt.logs), slotNumber);
1234
+ const request = await this.slashingProposerContract.buildVoteRequestFromSigner(votes, slotNumber, signer);
1235
+ await this.simulateAndEnqueueRequest('vote-offenses', request, (receipt)=>!!this.slashingProposerContract.tryExtractVoteCastEvent(receipt.logs), slotNumber);
1064
1236
  break;
1065
1237
  }
1066
1238
  case 'execute-slash':
@@ -1070,13 +1242,12 @@ export class SequencerPublisher {
1070
1242
  round: action.round,
1071
1243
  signerAddress
1072
1244
  });
1073
- if (this.slashingProposerContract?.type !== 'tally') {
1074
- 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');
1075
1247
  return false;
1076
1248
  }
1077
- const tallySlashingProposer = this.slashingProposerContract;
1078
- const request = tallySlashingProposer.buildExecuteRoundRequest(action.round, action.committees);
1079
- await this.simulateAndEnqueueRequest('execute-slash', request, (receipt)=>!!tallySlashingProposer.tryExtractRoundExecutedEvent(receipt.logs), slotNumber);
1249
+ const executeRequest = this.slashingProposerContract.buildExecuteRoundRequest(action.round, action.committees);
1250
+ await this.simulateAndEnqueueRequest('execute-slash', executeRequest, (receipt)=>!!this.slashingProposerContract.tryExtractRoundExecutedEvent(receipt.logs), slotNumber);
1080
1251
  break;
1081
1252
  }
1082
1253
  default:
@@ -1100,26 +1271,40 @@ export class SequencerPublisher {
1100
1271
  attestationsAndSignersSignature,
1101
1272
  feeAssetPriceModifier: checkpoint.feeAssetPriceModifier
1102
1273
  };
1274
+ const simulationOverridesPlan = SimulationOverridesBuilder.from(opts.simulationOverridesPlan).withoutBlobCheck().build();
1103
1275
  try {
1104
1276
  // @note This will make sure that we are passing the checks for our header ASSUMING that the data is also made available
1105
1277
  // This means that we can avoid the simulation issues in later checks.
1106
1278
  // By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which
1107
1279
  // make time consistency checks break.
1108
1280
  // TODO(palla): Check whether we're validating twice, once here and once within addProposeTx, since we call simulateProposeTx in both places.
1109
- await this.validateCheckpointForSubmission(checkpoint, attestationsAndSigners, attestationsAndSignersSignature, opts);
1281
+ await this.validateCheckpointForSubmission(checkpoint, attestationsAndSigners, attestationsAndSignersSignature, simulationOverridesPlan);
1110
1282
  } catch (err) {
1111
1283
  this.log.error(`Checkpoint validation failed. ${err instanceof Error ? err.message : 'No error message'}`, err, {
1112
1284
  ...checkpoint.getStats(),
1113
1285
  slotNumber: checkpoint.header.slotNumber,
1114
- forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber
1286
+ simulationOverridesPlan
1115
1287
  });
1116
1288
  throw err;
1117
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
+ }
1118
1299
  this.log.verbose(`Enqueuing checkpoint propose transaction`, {
1119
1300
  ...checkpoint.toCheckpointInfo(),
1120
- ...opts
1301
+ txTimeoutAt: opts.txTimeoutAt,
1302
+ simulationOverridesPlan
1121
1303
  });
1122
- await this.addProposeTx(checkpoint, proposeTxArgs, opts);
1304
+ await this.addProposeTx(checkpoint, proposeTxArgs, {
1305
+ txTimeoutAt: opts.txTimeoutAt,
1306
+ simulationOverridesPlan
1307
+ }, preCheck);
1123
1308
  }
1124
1309
  enqueueInvalidateCheckpoint(request, opts = {}) {
1125
1310
  if (!request) {
@@ -1174,6 +1359,7 @@ export class SequencerPublisher {
1174
1359
  const cachedLastActionSlot = this.lastActions[action];
1175
1360
  this.lastActions[action] = slotNumber;
1176
1361
  this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
1362
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1177
1363
  let gasUsed;
1178
1364
  const simulateAbi = mergeAbis([
1179
1365
  request.abi ?? [],
@@ -1191,6 +1377,27 @@ export class SequencerPublisher {
1191
1377
  } catch (err) {
1192
1378
  const viemError = formatViemError(err, simulateAbi);
1193
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
+ });
1194
1401
  return false;
1195
1402
  }
1196
1403
  // We issued the simulation against the rollup contract, so we need to account for the overhead of the multicall3
@@ -1236,13 +1443,14 @@ export class SequencerPublisher {
1236
1443
  * A call to `restart` is required before you can continue publishing.
1237
1444
  */ interrupt() {
1238
1445
  this.interrupted = true;
1446
+ this.interruptibleSleep.interrupt();
1239
1447
  this.l1TxUtils.interrupt();
1240
1448
  }
1241
1449
  /** Restarts the publisher after calling `interrupt`. */ restart() {
1242
1450
  this.interrupted = false;
1243
1451
  this.l1TxUtils.restart();
1244
1452
  }
1245
- async prepareProposeTx(encodedData, options) {
1453
+ async prepareProposeTx(encodedData, simulationOverridesPlan) {
1246
1454
  const kzg = Blob.getViemKzgInstance();
1247
1455
  const blobInput = getPrefixedEthBlobCommitments(encodedData.blobs);
1248
1456
  this.log.debug('Validating blob input', {
@@ -1269,10 +1477,38 @@ export class SequencerPublisher {
1269
1477
  }, {}, {
1270
1478
  blobs: encodedData.blobs.map((b)=>b.data),
1271
1479
  kzg
1272
- }).catch((err)=>{
1273
- const { message, metaMessages } = formatViemError(err);
1274
- this.log.error(`Failed to validate blobs`, message, {
1275
- 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
+ }
1276
1512
  });
1277
1513
  throw new Error('Failed to validate blobs');
1278
1514
  });
@@ -1291,7 +1527,7 @@ export class SequencerPublisher {
1291
1527
  encodedData.attestationsAndSignersSignature.toViemSignature(),
1292
1528
  blobInput
1293
1529
  ];
1294
- const { rollupData, simulationResult } = await this.simulateProposeTx(args, options);
1530
+ const { rollupData, simulationResult } = await this.simulateProposeTx(args, simulationOverridesPlan);
1295
1531
  return {
1296
1532
  args,
1297
1533
  blobEvaluationGas,
@@ -1303,27 +1539,13 @@ export class SequencerPublisher {
1303
1539
  * Simulates the propose tx with eth_simulateV1
1304
1540
  * @param args - The propose tx args
1305
1541
  * @returns The simulation result
1306
- */ async simulateProposeTx(args, options) {
1542
+ */ async simulateProposeTx(args, simulationOverridesPlan) {
1307
1543
  const rollupData = encodeFunctionData({
1308
1544
  abi: RollupAbi,
1309
1545
  functionName: 'propose',
1310
1546
  args
1311
1547
  });
1312
- // override the pending checkpoint number if requested
1313
- const forcePendingCheckpointNumberStateDiff = (options.forcePendingCheckpointNumber !== undefined ? await this.rollupContract.makePendingCheckpointNumberOverride(options.forcePendingCheckpointNumber) : []).flatMap((override)=>override.stateDiff ?? []);
1314
- const stateOverrides = [
1315
- {
1316
- address: this.rollupContract.address,
1317
- // @note we override checkBlob to false since blobs are not part simulate()
1318
- stateDiff: [
1319
- {
1320
- slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true),
1321
- value: toPaddedHex(0n, true)
1322
- },
1323
- ...forcePendingCheckpointNumberStateDiff
1324
- ]
1325
- }
1326
- ];
1548
+ const stateOverrides = await buildSimulationOverridesStateOverride(this.rollupContract, simulationOverridesPlan);
1327
1549
  // In fisherman mode, simulate as the proposer but with sufficient balance
1328
1550
  if (this.proposerAddressForSimulation) {
1329
1551
  stateOverrides.push({
@@ -1331,6 +1553,7 @@ export class SequencerPublisher {
1331
1553
  balance: 10n * WEI_CONST * WEI_CONST
1332
1554
  });
1333
1555
  }
1556
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1334
1557
  const simTs = this.getSimulationTimestamp(SlotNumber.fromBigInt(args[0].header.slotNumber));
1335
1558
  const simulationResult = await this.l1TxUtils.simulate({
1336
1559
  to: this.rollupContract.address,
@@ -1360,6 +1583,26 @@ export class SequencerPublisher {
1360
1583
  this.log.error(`Failed to simulate propose tx`, viemError, {
1361
1584
  simulationTimestamp: simTs
1362
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
+ });
1363
1606
  throw err;
1364
1607
  });
1365
1608
  return {
@@ -1367,11 +1610,11 @@ export class SequencerPublisher {
1367
1610
  simulationResult
1368
1611
  };
1369
1612
  }
1370
- async addProposeTx(checkpoint, encodedData, opts = {}) {
1613
+ async addProposeTx(checkpoint, encodedData, opts = {}, preCheck) {
1371
1614
  const slot = checkpoint.header.slotNumber;
1372
1615
  const timer = new Timer();
1373
1616
  const kzg = Blob.getViemKzgInstance();
1374
- const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(encodedData, opts);
1617
+ const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(encodedData, opts.simulationOverridesPlan);
1375
1618
  const startBlock = await this.l1TxUtils.getBlockNumber();
1376
1619
  const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil(Number(simulationResult.gasUsed) * 64 / 63)) + blobEvaluationGas + SequencerPublisher.MULTICALL_OVERHEAD_GAS_GUESS);
1377
1620
  // Send the blobs to the blob client preemptively. This helps in tests where the sequencer mistakingly thinks that the propose
@@ -1387,9 +1630,10 @@ export class SequencerPublisher {
1387
1630
  },
1388
1631
  lastValidL2Slot: checkpoint.header.slotNumber,
1389
1632
  gasConfig: {
1390
- ...opts,
1633
+ txTimeoutAt: opts.txTimeoutAt,
1391
1634
  gasLimit
1392
1635
  },
1636
+ preCheck,
1393
1637
  blobConfig: {
1394
1638
  blobs: encodedData.blobs.map((b)=>b.data),
1395
1639
  kzg