@aztec/sequencer-client 0.0.1-commit.7035c9bd6 → 0.0.1-commit.71324e566

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 (70) hide show
  1. package/dest/client/sequencer-client.d.ts +6 -1
  2. package/dest/client/sequencer-client.d.ts.map +1 -1
  3. package/dest/client/sequencer-client.js +11 -17
  4. package/dest/config.d.ts +1 -1
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +8 -11
  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 -14
  14. package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
  15. package/dest/global_variable_builder/global_builder.js +16 -51
  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 +13 -1
  20. package/dest/publisher/config.d.ts.map +1 -1
  21. package/dest/publisher/config.js +19 -4
  22. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts +3 -4
  23. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -1
  24. package/dest/publisher/sequencer-publisher-factory.d.ts +3 -5
  25. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  26. package/dest/publisher/sequencer-publisher-factory.js +2 -3
  27. package/dest/publisher/sequencer-publisher.d.ts +48 -42
  28. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  29. package/dest/publisher/sequencer-publisher.js +117 -109
  30. package/dest/sequencer/chain_state_overrides.d.ts +25 -0
  31. package/dest/sequencer/chain_state_overrides.d.ts.map +1 -0
  32. package/dest/sequencer/chain_state_overrides.js +39 -0
  33. package/dest/sequencer/checkpoint_proposal_job.d.ts +21 -10
  34. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  35. package/dest/sequencer/checkpoint_proposal_job.js +200 -134
  36. package/dest/sequencer/checkpoint_proposal_job_metrics.d.ts +34 -0
  37. package/dest/sequencer/checkpoint_proposal_job_metrics.d.ts.map +1 -0
  38. package/dest/sequencer/checkpoint_proposal_job_metrics.js +72 -0
  39. package/dest/sequencer/checkpoint_voter.d.ts +1 -2
  40. package/dest/sequencer/checkpoint_voter.d.ts.map +1 -1
  41. package/dest/sequencer/checkpoint_voter.js +2 -5
  42. package/dest/sequencer/metrics.d.ts +5 -10
  43. package/dest/sequencer/metrics.d.ts.map +1 -1
  44. package/dest/sequencer/metrics.js +12 -20
  45. package/dest/sequencer/sequencer.d.ts +17 -4
  46. package/dest/sequencer/sequencer.d.ts.map +1 -1
  47. package/dest/sequencer/sequencer.js +76 -23
  48. package/dest/sequencer/timetable.d.ts +14 -1
  49. package/dest/sequencer/timetable.d.ts.map +1 -1
  50. package/dest/sequencer/timetable.js +45 -36
  51. package/package.json +27 -27
  52. package/src/client/sequencer-client.ts +16 -24
  53. package/src/config.ts +8 -11
  54. package/src/global_variable_builder/README.md +44 -0
  55. package/src/global_variable_builder/fee_predictor.ts +172 -0
  56. package/src/global_variable_builder/fee_provider.ts +75 -0
  57. package/src/global_variable_builder/global_builder.ts +26 -63
  58. package/src/global_variable_builder/index.ts +3 -1
  59. package/src/publisher/config.ts +38 -4
  60. package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +3 -1
  61. package/src/publisher/sequencer-publisher-factory.ts +3 -6
  62. package/src/publisher/sequencer-publisher.ts +181 -155
  63. package/src/sequencer/README.md +83 -13
  64. package/src/sequencer/chain_state_overrides.ts +87 -0
  65. package/src/sequencer/checkpoint_proposal_job.ts +242 -154
  66. package/src/sequencer/checkpoint_proposal_job_metrics.ts +128 -0
  67. package/src/sequencer/checkpoint_voter.ts +1 -12
  68. package/src/sequencer/metrics.ts +15 -25
  69. package/src/sequencer/sequencer.ts +113 -25
  70. package/src/sequencer/timetable.ts +57 -45
@@ -3,16 +3,18 @@ import { Blob, getBlobsPerL1Block, getPrefixedEthBlobCommitments } from '@aztec/
3
3
  import type { EpochCache } from '@aztec/epoch-cache';
4
4
  import type { L1ContractsConfig } from '@aztec/ethereum/config';
5
5
  import {
6
- type EmpireSlashingProposerContract,
7
6
  FeeAssetPriceOracle,
8
7
  type GovernanceProposerContract,
9
8
  type IEmpireBase,
10
9
  MULTI_CALL_3_ADDRESS,
11
10
  Multicall3,
12
11
  RollupContract,
13
- type TallySlashingProposerContract,
12
+ SimulationOverridesBuilder,
13
+ type SimulationOverridesPlan,
14
+ type SlashingProposerContract,
14
15
  type ViemCommitteeAttestations,
15
16
  type ViemHeader,
17
+ buildSimulationOverridesStateOverride,
16
18
  } from '@aztec/ethereum/contracts';
17
19
  import { type L1FeeAnalysisResult, L1FeeAnalyzer } from '@aztec/ethereum/l1-fee-analysis';
18
20
  import {
@@ -26,7 +28,6 @@ import {
26
28
  } from '@aztec/ethereum/l1-tx-utils';
27
29
  import { FormattedViemError, formatViemError, mergeAbis, tryExtractEvent } from '@aztec/ethereum/utils';
28
30
  import { sumBigint } from '@aztec/foundation/bigint';
29
- import { toHex as toPaddedHex } from '@aztec/foundation/bigint-buffer';
30
31
  import { CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
31
32
  import { trimmedBytesLength } from '@aztec/foundation/buffer';
32
33
  import { pick } from '@aztec/foundation/collection';
@@ -36,20 +37,20 @@ import { EthAddress } from '@aztec/foundation/eth-address';
36
37
  import { Signature, type ViemSignature } from '@aztec/foundation/eth-signature';
37
38
  import { type Logger, createLogger } from '@aztec/foundation/log';
38
39
  import { makeBackoff, retry } from '@aztec/foundation/retry';
40
+ import { InterruptibleSleep } from '@aztec/foundation/sleep';
39
41
  import { bufferToHex } from '@aztec/foundation/string';
40
- import { DateProvider, Timer } from '@aztec/foundation/timer';
42
+ import { type DateProvider, Timer } from '@aztec/foundation/timer';
41
43
  import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
42
44
  import { type ProposerSlashAction, encodeSlashConsensusVotes } from '@aztec/slasher';
43
45
  import { CommitteeAttestationsAndSigners, type ValidateCheckpointResult } from '@aztec/stdlib/block';
44
46
  import type { Checkpoint } from '@aztec/stdlib/checkpoint';
45
- import { SlashFactoryContract } from '@aztec/stdlib/l1-contracts';
47
+ import { getLastL1SlotTimestampForL2Slot, getNextL1SlotTimestamp } from '@aztec/stdlib/epoch-helpers';
46
48
  import type { CheckpointHeader } from '@aztec/stdlib/rollup';
47
49
  import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
48
50
  import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
49
51
 
50
52
  import {
51
53
  type Hex,
52
- type StateOverride,
53
54
  type TransactionReceipt,
54
55
  type TypedDataDefinition,
55
56
  encodeFunctionData,
@@ -62,6 +63,20 @@ import type { SequencerPublisherConfig } from './config.js';
62
63
  import { type FailedL1Tx, type L1TxFailedStore, createL1TxFailedStore } from './l1_tx_failed_store/index.js';
63
64
  import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
64
65
 
66
+ /** Result of a sendRequests call, returned by both sendRequests() and sendRequestsAt(). */
67
+ export type SendRequestsResult = {
68
+ /** The L1 transaction receipt or error from the bundled multicall. */
69
+ result: { receipt: TransactionReceipt; errorMsg?: string } | FormattedViemError;
70
+ /** Actions that expired (past their deadline) before the request was sent. */
71
+ expiredActions: Action[];
72
+ /** Actions that were included in the sent L1 transaction. */
73
+ sentActions: Action[];
74
+ /** Actions whose L1 simulation succeeded (subset of sentActions). */
75
+ successfulActions: Action[];
76
+ /** Actions whose L1 simulation failed (subset of sentActions). */
77
+ failedActions: Action[];
78
+ };
79
+
65
80
  /** Arguments to the process method of the rollup contract */
66
81
  type L1ProcessArgs = {
67
82
  /** The L2 block header. */
@@ -83,16 +98,13 @@ export const Actions = [
83
98
  'invalidate-by-insufficient-attestations',
84
99
  'propose',
85
100
  'governance-signal',
86
- 'empire-slashing-signal',
87
- 'create-empire-payload',
88
- 'execute-empire-payload',
89
101
  'vote-offenses',
90
102
  'execute-slash',
91
103
  ] as const;
92
104
 
93
105
  export type Action = (typeof Actions)[number];
94
106
 
95
- type GovernanceSignalAction = Extract<Action, 'governance-signal' | 'empire-slashing-signal'>;
107
+ type GovernanceSignalAction = Extract<Action, 'governance-signal'>;
96
108
 
97
109
  // Sorting for actions such that invalidations go before proposals, and proposals go before votes
98
110
  export const compareActions = (a: Action, b: Action) => Actions.indexOf(a) - Actions.indexOf(b);
@@ -103,6 +115,13 @@ export type InvalidateCheckpointRequest = {
103
115
  gasUsed: bigint;
104
116
  checkpointNumber: CheckpointNumber;
105
117
  forcePendingCheckpointNumber: CheckpointNumber;
118
+ /** Archive at the rollback target checkpoint (checkpoint N-1). */
119
+ lastArchive: Fr;
120
+ };
121
+
122
+ type EnqueueProposeCheckpointOpts = {
123
+ txTimeoutAt?: Date;
124
+ simulationOverridesPlan?: SimulationOverridesPlan;
106
125
  };
107
126
 
108
127
  interface RequestWithExpiry {
@@ -111,6 +130,8 @@ interface RequestWithExpiry {
111
130
  lastValidL2Slot: SlotNumber;
112
131
  gasConfig?: Pick<L1TxConfig, 'txTimeoutAt' | 'gasLimit'>;
113
132
  blobConfig?: L1BlobInputs;
133
+ /** Optional pre-send validation. If it rejects, the request is discarded. */
134
+ preCheck?: () => Promise<void>;
114
135
  checkSuccess: (
115
136
  request: L1TxRequest,
116
137
  result?: { receipt: TransactionReceipt; stats?: TransactionStats; errorMsg?: string },
@@ -135,6 +156,9 @@ export class SequencerPublisher {
135
156
  protected ethereumSlotDuration: bigint;
136
157
  protected aztecSlotDuration: bigint;
137
158
 
159
+ /** Date provider for wall-clock time. */
160
+ private readonly dateProvider: DateProvider;
161
+
138
162
  private blobClient: BlobClientInterface;
139
163
 
140
164
  /** Address to use for simulations in fisherman mode (actual proposer's address) */
@@ -149,6 +173,9 @@ export class SequencerPublisher {
149
173
  /** Fee asset price oracle for computing price modifiers from Uniswap V4 */
150
174
  private feeAssetPriceOracle: FeeAssetPriceOracle;
151
175
 
176
+ /** Interruptible sleep used by sendRequestsAt to wait until a target timestamp. */
177
+ private readonly interruptibleSleep = new InterruptibleSleep();
178
+
152
179
  // A CALL to a cold address is 2700 gas
153
180
  public static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
154
181
 
@@ -158,8 +185,7 @@ export class SequencerPublisher {
158
185
  public l1TxUtils: L1TxUtils;
159
186
  public rollupContract: RollupContract;
160
187
  public govProposerContract: GovernanceProposerContract;
161
- public slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
162
- public slashFactoryContract: SlashFactoryContract;
188
+ public slashingProposerContract: SlashingProposerContract | undefined;
163
189
 
164
190
  public readonly tracer: Tracer;
165
191
 
@@ -173,9 +199,8 @@ export class SequencerPublisher {
173
199
  blobClient: BlobClientInterface;
174
200
  l1TxUtils: L1TxUtils;
175
201
  rollupContract: RollupContract;
176
- slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
202
+ slashingProposerContract: SlashingProposerContract | undefined;
177
203
  governanceProposerContract: GovernanceProposerContract;
178
- slashFactoryContract: SlashFactoryContract;
179
204
  epochCache: EpochCache;
180
205
  dateProvider: DateProvider;
181
206
  metrics: SequencerPublisherMetrics;
@@ -187,10 +212,12 @@ export class SequencerPublisher {
187
212
  this.log = deps.log ?? createLogger('sequencer:publisher');
188
213
  this.ethereumSlotDuration = BigInt(config.ethereumSlotDuration);
189
214
  this.aztecSlotDuration = BigInt(config.aztecSlotDuration);
215
+ this.dateProvider = deps.dateProvider;
190
216
  this.epochCache = deps.epochCache;
191
217
  this.lastActions = deps.lastActions;
192
218
 
193
219
  this.blobClient = deps.blobClient;
220
+ this.dateProvider = deps.dateProvider;
194
221
 
195
222
  const telemetry = deps.telemetry ?? getTelemetryClient();
196
223
  this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
@@ -208,8 +235,6 @@ export class SequencerPublisher {
208
235
  const newSlashingProposer = await this.rollupContract.getSlashingProposer();
209
236
  this.slashingProposerContract = newSlashingProposer;
210
237
  });
211
- this.slashFactoryContract = deps.slashFactoryContract;
212
-
213
238
  // Initialize L1 fee analyzer for fisherman mode
214
239
  if (config.fishermanMode) {
215
240
  this.l1FeeAnalyzer = new L1FeeAnalyzer(
@@ -366,9 +391,10 @@ export class SequencerPublisher {
366
391
  * - undefined if no valid requests are found OR the tx failed to send.
367
392
  */
368
393
  @trackSpan('SequencerPublisher.sendRequests')
369
- public async sendRequests() {
394
+ public async sendRequests(): Promise<SendRequestsResult | undefined> {
370
395
  const requestsToProcess = [...this.requests];
371
396
  this.requests = [];
397
+
372
398
  if (this.interrupted || requestsToProcess.length === 0) {
373
399
  return undefined;
374
400
  }
@@ -527,6 +553,45 @@ export class SequencerPublisher {
527
553
  }
528
554
  }
529
555
 
556
+ /*
557
+ * Schedules sending all enqueued requests at (or after) the given timestamp.
558
+ * Uses InterruptibleSleep so it can be cancelled via interrupt().
559
+ * Returns the promise for the L1 response (caller should NOT await this in the work loop).
560
+ */
561
+ public async sendRequestsAt(submitAfter: Date): Promise<SendRequestsResult | undefined> {
562
+ const ms = submitAfter.getTime() - this.dateProvider.now();
563
+ if (ms > 0) {
564
+ this.log.debug(`Sleeping ${ms}ms before sending requests`, { submitAfter });
565
+ await this.interruptibleSleep.sleep(ms);
566
+ }
567
+ if (this.interrupted) {
568
+ return undefined;
569
+ }
570
+
571
+ // Re-validate enqueued requests after the sleep (state may have changed, e.g. prune or L1 reorg)
572
+ const validRequests: RequestWithExpiry[] = [];
573
+ for (const request of this.requests) {
574
+ if (!request.preCheck) {
575
+ validRequests.push(request);
576
+ continue;
577
+ }
578
+
579
+ try {
580
+ await request.preCheck();
581
+ validRequests.push(request);
582
+ } catch (err) {
583
+ this.log.warn(`Pre-send validation failed for ${request.action}, discarding request`, err);
584
+ }
585
+ }
586
+
587
+ this.requests = validRequests;
588
+ if (this.requests.length === 0) {
589
+ return undefined;
590
+ }
591
+
592
+ return this.sendRequests();
593
+ }
594
+
530
595
  private callbackBundledTransactions(
531
596
  requests: RequestWithExpiry[],
532
597
  result: { receipt: TransactionReceipt; errorMsg?: string } | FormattedViemError | undefined,
@@ -602,21 +667,21 @@ export class SequencerPublisher {
602
667
  * @param tipArchive - The archive to check
603
668
  * @returns The slot and block number if it is possible to propose, undefined otherwise
604
669
  */
605
- public canProposeAt(
606
- tipArchive: Fr,
607
- msgSender: EthAddress,
608
- opts: { forcePendingCheckpointNumber?: CheckpointNumber; pipelined?: boolean } = {},
609
- ) {
670
+ public async canProposeAt(tipArchive: Fr, msgSender: EthAddress, simulationOverridesPlan?: SimulationOverridesPlan) {
610
671
  // TODO: #14291 - should loop through multiple keys to check if any of them can propose
611
672
  const ignoredErrors = ['SlotAlreadyInChain', 'InvalidProposer', 'InvalidArchive'];
612
673
 
613
- const pipelined = opts.pipelined ?? this.epochCache.isProposerPipeliningEnabled();
674
+ const pipelined = this.epochCache.isProposerPipeliningEnabled();
614
675
  const slotOffset = pipelined ? this.aztecSlotDuration : 0n;
676
+ const nextL1SlotTs = this.getNextL1SlotTimestamp() + slotOffset;
615
677
 
616
678
  return this.rollupContract
617
- .canProposeAt(tipArchive.toBuffer(), msgSender.toString(), this.ethereumSlotDuration, slotOffset, {
618
- forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
619
- })
679
+ .canProposeAt(
680
+ tipArchive.toBuffer(),
681
+ msgSender.toString(),
682
+ nextL1SlotTs,
683
+ await buildSimulationOverridesStateOverride(this.rollupContract, simulationOverridesPlan),
684
+ )
620
685
  .catch(err => {
621
686
  if (err instanceof FormattedViemError && ignoredErrors.find(e => err.message.includes(e))) {
622
687
  this.log.warn(`Failed canProposeAtTime check with ${ignoredErrors.find(e => err.message.includes(e))}`, {
@@ -638,7 +703,7 @@ export class SequencerPublisher {
638
703
  @trackSpan('SequencerPublisher.validateBlockHeader')
639
704
  public async validateBlockHeader(
640
705
  header: CheckpointHeader,
641
- opts?: { forcePendingCheckpointNumber: CheckpointNumber | undefined },
706
+ simulationOverridesPlan?: SimulationOverridesPlan,
642
707
  ): Promise<void> {
643
708
  const flags = { ignoreDA: true, ignoreSignatures: true };
644
709
 
@@ -652,10 +717,8 @@ export class SequencerPublisher {
652
717
  flags,
653
718
  ] as const;
654
719
 
655
- const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
656
- const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(
657
- opts?.forcePendingCheckpointNumber,
658
- );
720
+ const ts = this.getSimulationTimestamp(header.slotNumber);
721
+ const stateOverrides = await buildSimulationOverridesStateOverride(this.rollupContract, simulationOverridesPlan);
659
722
  let balance = 0n;
660
723
  if (this.config.fishermanMode) {
661
724
  // In fisherman mode, we can't know where the proposer is publishing from
@@ -675,7 +738,7 @@ export class SequencerPublisher {
675
738
  data: encodeFunctionData({ abi: RollupAbi, functionName: 'validateHeaderWithAttestations', args }),
676
739
  from: MULTI_CALL_3_ADDRESS,
677
740
  },
678
- { time: ts + 1n },
741
+ { time: ts },
679
742
  stateOverrides,
680
743
  );
681
744
  this.log.debug(`Simulated validateHeader`);
@@ -728,6 +791,7 @@ export class SequencerPublisher {
728
791
  gasUsed,
729
792
  checkpointNumber,
730
793
  forcePendingCheckpointNumber: CheckpointNumber(checkpointNumber - 1),
794
+ lastArchive: validationResult.checkpoint.lastArchive,
731
795
  reason,
732
796
  };
733
797
  } catch (err) {
@@ -740,8 +804,8 @@ export class SequencerPublisher {
740
804
  `Simulation for invalidate checkpoint ${checkpointNumber} failed due to checkpoint not being in pending chain`,
741
805
  { ...logData, request, error: viemError.message },
742
806
  );
743
- const latestPendingCheckpointNumber = await this.rollupContract.getCheckpointNumber();
744
- if (latestPendingCheckpointNumber < checkpointNumber) {
807
+ const latestProposedCheckpointNumber = await this.rollupContract.getCheckpointNumber();
808
+ if (latestProposedCheckpointNumber < checkpointNumber) {
745
809
  this.log.verbose(`Checkpoint ${checkpointNumber} has already been invalidated`, { ...logData });
746
810
  return undefined;
747
811
  } else {
@@ -815,11 +879,8 @@ export class SequencerPublisher {
815
879
  checkpoint: Checkpoint,
816
880
  attestationsAndSigners: CommitteeAttestationsAndSigners,
817
881
  attestationsAndSignersSignature: Signature,
818
- options: { forcePendingCheckpointNumber?: CheckpointNumber },
819
- ): Promise<bigint> {
820
- // Anchor the simulation timestamp to the checkpoint's own slot start time
821
- // rather than the current L1 block timestamp, which may overshoot into the next slot if the build ran late.
822
- const ts = checkpoint.header.timestamp;
882
+ simulationOverridesPlan?: SimulationOverridesPlan,
883
+ ): Promise<void> {
823
884
  const blobFields = checkpoint.toBlobFields();
824
885
  const blobs = await getBlobsPerL1Block(blobFields);
825
886
  const blobInput = getPrefixedEthBlobCommitments(blobs);
@@ -838,13 +899,11 @@ export class SequencerPublisher {
838
899
  blobInput,
839
900
  ] as const;
840
901
 
841
- await this.simulateProposeTx(args, ts, options);
842
- return ts;
902
+ await this.simulateProposeTx(args, simulationOverridesPlan);
843
903
  }
844
904
 
845
905
  private async enqueueCastSignalHelper(
846
906
  slotNumber: SlotNumber,
847
- timestamp: bigint,
848
907
  signalType: GovernanceSignalAction,
849
908
  payload: EthAddress,
850
909
  base: IEmpireBase,
@@ -923,13 +982,17 @@ export class SequencerPublisher {
923
982
  });
924
983
 
925
984
  const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
985
+ const timestamp = this.getSimulationTimestamp(slotNumber);
926
986
 
927
987
  try {
928
988
  await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
929
989
  this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
930
990
  } catch (err) {
931
991
  const viemError = formatViemError(err);
932
- this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, viemError);
992
+ this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, viemError, {
993
+ simulationTimestamp: timestamp,
994
+ l1BlockNumber,
995
+ });
933
996
  this.backupFailedTx({
934
997
  id: keccak256(request.data!),
935
998
  failureType: 'simulation',
@@ -992,19 +1055,16 @@ export class SequencerPublisher {
992
1055
  /**
993
1056
  * Enqueues a governance castSignal transaction to cast a signal for a given slot number.
994
1057
  * @param slotNumber - The slot number to cast a signal for.
995
- * @param timestamp - The timestamp of the slot to cast a signal for.
996
1058
  * @returns True if the signal was successfully enqueued, false otherwise.
997
1059
  */
998
1060
  public enqueueGovernanceCastSignal(
999
1061
  governancePayload: EthAddress,
1000
1062
  slotNumber: SlotNumber,
1001
- timestamp: bigint,
1002
1063
  signerAddress: EthAddress,
1003
1064
  signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
1004
1065
  ): Promise<boolean> {
1005
1066
  return this.enqueueCastSignalHelper(
1006
1067
  slotNumber,
1007
- timestamp,
1008
1068
  'governance-signal',
1009
1069
  governancePayload,
1010
1070
  this.govProposerContract,
@@ -1017,7 +1077,6 @@ export class SequencerPublisher {
1017
1077
  public async enqueueSlashingActions(
1018
1078
  actions: ProposerSlashAction[],
1019
1079
  slotNumber: SlotNumber,
1020
- timestamp: bigint,
1021
1080
  signerAddress: EthAddress,
1022
1081
  signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
1023
1082
  ): Promise<boolean> {
@@ -1028,58 +1087,6 @@ export class SequencerPublisher {
1028
1087
 
1029
1088
  for (const action of actions) {
1030
1089
  switch (action.type) {
1031
- case 'vote-empire-payload': {
1032
- if (this.slashingProposerContract?.type !== 'empire') {
1033
- this.log.error('Cannot vote for empire payload on non-empire slashing contract');
1034
- break;
1035
- }
1036
- this.log.debug(`Enqueuing slashing vote for payload ${action.payload} at slot ${slotNumber}`, {
1037
- signerAddress,
1038
- });
1039
- await this.enqueueCastSignalHelper(
1040
- slotNumber,
1041
- timestamp,
1042
- 'empire-slashing-signal',
1043
- action.payload,
1044
- this.slashingProposerContract,
1045
- signerAddress,
1046
- signer,
1047
- );
1048
- break;
1049
- }
1050
-
1051
- case 'create-empire-payload': {
1052
- this.log.debug(`Enqueuing slashing create payload at slot ${slotNumber}`, { slotNumber, signerAddress });
1053
- const request = this.slashFactoryContract.buildCreatePayloadRequest(action.data);
1054
- await this.simulateAndEnqueueRequest(
1055
- 'create-empire-payload',
1056
- request,
1057
- (receipt: TransactionReceipt) =>
1058
- !!this.slashFactoryContract.tryExtractSlashPayloadCreatedEvent(receipt.logs),
1059
- slotNumber,
1060
- timestamp,
1061
- );
1062
- break;
1063
- }
1064
-
1065
- case 'execute-empire-payload': {
1066
- this.log.debug(`Enqueuing slashing execute payload at slot ${slotNumber}`, { slotNumber, signerAddress });
1067
- if (this.slashingProposerContract?.type !== 'empire') {
1068
- this.log.error('Cannot execute slashing payload on non-empire slashing contract');
1069
- return false;
1070
- }
1071
- const empireSlashingProposer = this.slashingProposerContract as EmpireSlashingProposerContract;
1072
- const request = empireSlashingProposer.buildExecuteRoundRequest(action.round);
1073
- await this.simulateAndEnqueueRequest(
1074
- 'execute-empire-payload',
1075
- request,
1076
- (receipt: TransactionReceipt) => !!empireSlashingProposer.tryExtractPayloadSubmittedEvent(receipt.logs),
1077
- slotNumber,
1078
- timestamp,
1079
- );
1080
- break;
1081
- }
1082
-
1083
1090
  case 'vote-offenses': {
1084
1091
  this.log.debug(`Enqueuing slashing vote for ${action.votes.length} votes at slot ${slotNumber}`, {
1085
1092
  slotNumber,
@@ -1087,19 +1094,17 @@ export class SequencerPublisher {
1087
1094
  votesCount: action.votes.length,
1088
1095
  signerAddress,
1089
1096
  });
1090
- if (this.slashingProposerContract?.type !== 'tally') {
1091
- this.log.error('Cannot vote for slashing offenses on non-tally slashing contract');
1097
+ if (!this.slashingProposerContract) {
1098
+ this.log.error('No slashing proposer contract available');
1092
1099
  return false;
1093
1100
  }
1094
- const tallySlashingProposer = this.slashingProposerContract as TallySlashingProposerContract;
1095
1101
  const votes = bufferToHex(encodeSlashConsensusVotes(action.votes));
1096
- const request = await tallySlashingProposer.buildVoteRequestFromSigner(votes, slotNumber, signer);
1102
+ const request = await this.slashingProposerContract.buildVoteRequestFromSigner(votes, slotNumber, signer);
1097
1103
  await this.simulateAndEnqueueRequest(
1098
1104
  'vote-offenses',
1099
1105
  request,
1100
- (receipt: TransactionReceipt) => !!tallySlashingProposer.tryExtractVoteCastEvent(receipt.logs),
1106
+ (receipt: TransactionReceipt) => !!this.slashingProposerContract!.tryExtractVoteCastEvent(receipt.logs),
1101
1107
  slotNumber,
1102
- timestamp,
1103
1108
  );
1104
1109
  break;
1105
1110
  }
@@ -1110,18 +1115,20 @@ export class SequencerPublisher {
1110
1115
  round: action.round,
1111
1116
  signerAddress,
1112
1117
  });
1113
- if (this.slashingProposerContract?.type !== 'tally') {
1114
- this.log.error('Cannot execute slashing offenses on non-tally slashing contract');
1118
+ if (!this.slashingProposerContract) {
1119
+ this.log.error('No slashing proposer contract available');
1115
1120
  return false;
1116
1121
  }
1117
- const tallySlashingProposer = this.slashingProposerContract as TallySlashingProposerContract;
1118
- const request = tallySlashingProposer.buildExecuteRoundRequest(action.round, action.committees);
1122
+ const executeRequest = this.slashingProposerContract.buildExecuteRoundRequest(
1123
+ action.round,
1124
+ action.committees,
1125
+ );
1119
1126
  await this.simulateAndEnqueueRequest(
1120
1127
  'execute-slash',
1121
- request,
1122
- (receipt: TransactionReceipt) => !!tallySlashingProposer.tryExtractRoundExecutedEvent(receipt.logs),
1128
+ executeRequest,
1129
+ (receipt: TransactionReceipt) =>
1130
+ !!this.slashingProposerContract!.tryExtractRoundExecutedEvent(receipt.logs),
1123
1131
  slotNumber,
1124
- timestamp,
1125
1132
  );
1126
1133
  break;
1127
1134
  }
@@ -1141,7 +1148,7 @@ export class SequencerPublisher {
1141
1148
  checkpoint: Checkpoint,
1142
1149
  attestationsAndSigners: CommitteeAttestationsAndSigners,
1143
1150
  attestationsAndSignersSignature: Signature,
1144
- opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
1151
+ opts: EnqueueProposeCheckpointOpts = {},
1145
1152
  ): Promise<void> {
1146
1153
  const checkpointHeader = checkpoint.header;
1147
1154
 
@@ -1157,7 +1164,9 @@ export class SequencerPublisher {
1157
1164
  feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
1158
1165
  };
1159
1166
 
1160
- let ts: bigint;
1167
+ const simulationOverridesPlan = SimulationOverridesBuilder.from(opts.simulationOverridesPlan)
1168
+ .withoutBlobCheck()
1169
+ .build();
1161
1170
 
1162
1171
  try {
1163
1172
  // @note This will make sure that we are passing the checks for our header ASSUMING that the data is also made available
@@ -1165,23 +1174,47 @@ export class SequencerPublisher {
1165
1174
  // By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which
1166
1175
  // make time consistency checks break.
1167
1176
  // TODO(palla): Check whether we're validating twice, once here and once within addProposeTx, since we call simulateProposeTx in both places.
1168
- ts = await this.validateCheckpointForSubmission(
1177
+ await this.validateCheckpointForSubmission(
1169
1178
  checkpoint,
1170
1179
  attestationsAndSigners,
1171
1180
  attestationsAndSignersSignature,
1172
- opts,
1181
+ simulationOverridesPlan,
1173
1182
  );
1174
1183
  } catch (err: any) {
1175
1184
  this.log.error(`Checkpoint validation failed. ${err instanceof Error ? err.message : 'No error message'}`, err, {
1176
1185
  ...checkpoint.getStats(),
1177
1186
  slotNumber: checkpoint.header.slotNumber,
1178
- forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
1187
+ simulationOverridesPlan,
1179
1188
  });
1180
1189
  throw err;
1181
1190
  }
1182
1191
 
1183
- this.log.verbose(`Enqueuing checkpoint propose transaction`, { ...checkpoint.toCheckpointInfo(), ...opts });
1184
- await this.addProposeTx(checkpoint, proposeTxArgs, opts, ts);
1192
+ // Build a pre-check callback that re-validates the checkpoint before L1 submission.
1193
+ // During pipelining this catches stale proposals due to prunes or L1 reorgs that occur during the pipeline sleep.
1194
+ let preCheck = undefined;
1195
+ if (this.epochCache.isProposerPipeliningEnabled()) {
1196
+ preCheck = async () => {
1197
+ this.log.debug(`Re-validating checkpoint ${checkpoint.number} before L1 submission`);
1198
+ await this.validateCheckpointForSubmission(
1199
+ checkpoint,
1200
+ attestationsAndSigners,
1201
+ attestationsAndSignersSignature,
1202
+ simulationOverridesPlan,
1203
+ );
1204
+ };
1205
+ }
1206
+
1207
+ this.log.verbose(`Enqueuing checkpoint propose transaction`, {
1208
+ ...checkpoint.toCheckpointInfo(),
1209
+ txTimeoutAt: opts.txTimeoutAt,
1210
+ simulationOverridesPlan,
1211
+ });
1212
+ await this.addProposeTx(
1213
+ checkpoint,
1214
+ proposeTxArgs,
1215
+ { txTimeoutAt: opts.txTimeoutAt, simulationOverridesPlan },
1216
+ preCheck,
1217
+ );
1185
1218
  }
1186
1219
 
1187
1220
  public enqueueInvalidateCheckpoint(
@@ -1224,8 +1257,8 @@ export class SequencerPublisher {
1224
1257
  request: L1TxRequest,
1225
1258
  checkSuccess: (receipt: TransactionReceipt) => boolean | undefined,
1226
1259
  slotNumber: SlotNumber,
1227
- timestamp: bigint,
1228
1260
  ) {
1261
+ const timestamp = this.getSimulationTimestamp(slotNumber);
1229
1262
  const logData = { slotNumber, timestamp, gasLimit: undefined as bigint | undefined };
1230
1263
  if (this.lastActions[action] && this.lastActions[action] === slotNumber) {
1231
1264
  this.log.debug(`Skipping duplicate action ${action} for slot ${slotNumber}`);
@@ -1241,8 +1274,9 @@ export class SequencerPublisher {
1241
1274
 
1242
1275
  let gasUsed: bigint;
1243
1276
  const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
1277
+
1244
1278
  try {
1245
- ({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], simulateAbi)); // TODO(palla/slash): Check the timestamp logic
1279
+ ({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], simulateAbi));
1246
1280
  this.log.verbose(`Simulation for ${action} succeeded`, { ...logData, request, gasUsed });
1247
1281
  } catch (err) {
1248
1282
  const viemError = formatViemError(err, simulateAbi);
@@ -1300,6 +1334,7 @@ export class SequencerPublisher {
1300
1334
  */
1301
1335
  public interrupt() {
1302
1336
  this.interrupted = true;
1337
+ this.interruptibleSleep.interrupt();
1303
1338
  this.l1TxUtils.interrupt();
1304
1339
  }
1305
1340
 
@@ -1309,11 +1344,7 @@ export class SequencerPublisher {
1309
1344
  this.l1TxUtils.restart();
1310
1345
  }
1311
1346
 
1312
- private async prepareProposeTx(
1313
- encodedData: L1ProcessArgs,
1314
- timestamp: bigint,
1315
- options: { forcePendingCheckpointNumber?: CheckpointNumber },
1316
- ) {
1347
+ private async prepareProposeTx(encodedData: L1ProcessArgs, simulationOverridesPlan?: SimulationOverridesPlan) {
1317
1348
  const kzg = Blob.getViemKzgInstance();
1318
1349
  const blobInput = getPrefixedEthBlobCommitments(encodedData.blobs);
1319
1350
  this.log.debug('Validating blob input', { blobInput });
@@ -1384,7 +1415,7 @@ export class SequencerPublisher {
1384
1415
  blobInput,
1385
1416
  ] as const;
1386
1417
 
1387
- const { rollupData, simulationResult } = await this.simulateProposeTx(args, timestamp, options);
1418
+ const { rollupData, simulationResult } = await this.simulateProposeTx(args, simulationOverridesPlan);
1388
1419
 
1389
1420
  return { args, blobEvaluationGas, rollupData, simulationResult };
1390
1421
  }
@@ -1392,7 +1423,6 @@ export class SequencerPublisher {
1392
1423
  /**
1393
1424
  * Simulates the propose tx with eth_simulateV1
1394
1425
  * @param args - The propose tx args
1395
- * @param timestamp - The timestamp to simulate proposal at
1396
1426
  * @returns The simulation result
1397
1427
  */
1398
1428
  private async simulateProposeTx(
@@ -1409,8 +1439,7 @@ export class SequencerPublisher {
1409
1439
  ViemSignature,
1410
1440
  `0x${string}`,
1411
1441
  ],
1412
- timestamp: bigint,
1413
- options: { forcePendingCheckpointNumber?: CheckpointNumber },
1442
+ simulationOverridesPlan?: SimulationOverridesPlan,
1414
1443
  ) {
1415
1444
  const rollupData = encodeFunctionData({
1416
1445
  abi: RollupAbi,
@@ -1418,23 +1447,7 @@ export class SequencerPublisher {
1418
1447
  args,
1419
1448
  });
1420
1449
 
1421
- // override the pending checkpoint number if requested
1422
- const forcePendingCheckpointNumberStateDiff = (
1423
- options.forcePendingCheckpointNumber !== undefined
1424
- ? await this.rollupContract.makePendingCheckpointNumberOverride(options.forcePendingCheckpointNumber)
1425
- : []
1426
- ).flatMap(override => override.stateDiff ?? []);
1427
-
1428
- const stateOverrides: StateOverride = [
1429
- {
1430
- address: this.rollupContract.address,
1431
- // @note we override checkBlob to false since blobs are not part simulate()
1432
- stateDiff: [
1433
- { slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true), value: toPaddedHex(0n, true) },
1434
- ...forcePendingCheckpointNumberStateDiff,
1435
- ],
1436
- },
1437
- ];
1450
+ const stateOverrides = await buildSimulationOverridesStateOverride(this.rollupContract, simulationOverridesPlan);
1438
1451
  // In fisherman mode, simulate as the proposer but with sufficient balance
1439
1452
  if (this.proposerAddressForSimulation) {
1440
1453
  stateOverrides.push({
@@ -1444,6 +1457,7 @@ export class SequencerPublisher {
1444
1457
  }
1445
1458
 
1446
1459
  const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1460
+ const simTs = this.getSimulationTimestamp(SlotNumber.fromBigInt(args[0].header.slotNumber));
1447
1461
 
1448
1462
  const simulationResult = await this.l1TxUtils
1449
1463
  .simulate(
@@ -1454,8 +1468,7 @@ export class SequencerPublisher {
1454
1468
  ...(this.proposerAddressForSimulation && { from: this.proposerAddressForSimulation.toString() }),
1455
1469
  },
1456
1470
  {
1457
- // @note we add 1n to the timestamp because geth implementation doesn't like simulation timestamp to be equal to the current block timestamp
1458
- time: timestamp + 1n,
1471
+ time: simTs,
1459
1472
  // @note reth should have a 30m gas limit per block but throws errors that this tx is beyond limit so we increase here
1460
1473
  gasLimit: MAX_L1_TX_LIMIT * 2n,
1461
1474
  },
@@ -1477,7 +1490,7 @@ export class SequencerPublisher {
1477
1490
  logs: [],
1478
1491
  };
1479
1492
  }
1480
- this.log.error(`Failed to simulate propose tx`, viemError);
1493
+ this.log.error(`Failed to simulate propose tx`, viemError, { simulationTimestamp: simTs });
1481
1494
  this.backupFailedTx({
1482
1495
  id: keccak256(rollupData),
1483
1496
  failureType: 'simulation',
@@ -1499,16 +1512,15 @@ export class SequencerPublisher {
1499
1512
  private async addProposeTx(
1500
1513
  checkpoint: Checkpoint,
1501
1514
  encodedData: L1ProcessArgs,
1502
- opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
1503
- timestamp: bigint,
1515
+ opts: EnqueueProposeCheckpointOpts = {},
1516
+ preCheck?: () => Promise<void>,
1504
1517
  ): Promise<void> {
1505
1518
  const slot = checkpoint.header.slotNumber;
1506
1519
  const timer = new Timer();
1507
1520
  const kzg = Blob.getViemKzgInstance();
1508
1521
  const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(
1509
1522
  encodedData,
1510
- timestamp,
1511
- opts,
1523
+ opts.simulationOverridesPlan,
1512
1524
  );
1513
1525
  const startBlock = await this.l1TxUtils.getBlockNumber();
1514
1526
  const gasLimit = this.l1TxUtils.bumpGasLimit(
@@ -1532,7 +1544,8 @@ export class SequencerPublisher {
1532
1544
  data: rollupData,
1533
1545
  },
1534
1546
  lastValidL2Slot: checkpoint.header.slotNumber,
1535
- gasConfig: { ...opts, gasLimit },
1547
+ gasConfig: { txTimeoutAt: opts.txTimeoutAt, gasLimit },
1548
+ preCheck,
1536
1549
  blobConfig: {
1537
1550
  blobs: encodedData.blobs.map(b => b.data),
1538
1551
  kzg,
@@ -1585,4 +1598,17 @@ export class SequencerPublisher {
1585
1598
  },
1586
1599
  });
1587
1600
  }
1601
+
1602
+ /** Returns the timestamp of the last L1 slot within a given L2 slot. Used as the simulation timestamp
1603
+ * for eth_simulateV1 calls, since it's guaranteed to be greater than any L1 block produced during the slot. */
1604
+ private getSimulationTimestamp(slot: SlotNumber): bigint {
1605
+ const l1Constants = this.epochCache.getL1Constants();
1606
+ return getLastL1SlotTimestampForL2Slot(slot, l1Constants);
1607
+ }
1608
+
1609
+ /** Returns the timestamp of the next L1 slot boundary after now. */
1610
+ private getNextL1SlotTimestamp(): bigint {
1611
+ const l1Constants = this.epochCache.getL1Constants();
1612
+ return getNextL1SlotTimestamp(this.dateProvider.nowInSeconds(), l1Constants);
1613
+ }
1588
1614
  }