@aztec/sequencer-client 0.0.1-commit.3f296a7d2 → 0.0.1-commit.3f5453c7b

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 (63) hide show
  1. package/dest/client/sequencer-client.d.ts +3 -1
  2. package/dest/client/sequencer-client.d.ts.map +1 -1
  3. package/dest/client/sequencer-client.js +3 -4
  4. package/dest/config.d.ts +1 -1
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +0 -5
  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/sequencer-publisher-factory.d.ts +1 -3
  20. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  21. package/dest/publisher/sequencer-publisher-factory.js +0 -1
  22. package/dest/publisher/sequencer-publisher.d.ts +47 -44
  23. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  24. package/dest/publisher/sequencer-publisher.js +112 -112
  25. package/dest/sequencer/chain_state_overrides.d.ts +25 -0
  26. package/dest/sequencer/chain_state_overrides.d.ts.map +1 -0
  27. package/dest/sequencer/chain_state_overrides.js +39 -0
  28. package/dest/sequencer/checkpoint_proposal_job.d.ts +21 -10
  29. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  30. package/dest/sequencer/checkpoint_proposal_job.js +197 -131
  31. package/dest/sequencer/checkpoint_proposal_job_metrics.d.ts +34 -0
  32. package/dest/sequencer/checkpoint_proposal_job_metrics.d.ts.map +1 -0
  33. package/dest/sequencer/checkpoint_proposal_job_metrics.js +72 -0
  34. package/dest/sequencer/checkpoint_voter.d.ts +1 -2
  35. package/dest/sequencer/checkpoint_voter.d.ts.map +1 -1
  36. package/dest/sequencer/checkpoint_voter.js +2 -5
  37. package/dest/sequencer/metrics.d.ts +5 -10
  38. package/dest/sequencer/metrics.d.ts.map +1 -1
  39. package/dest/sequencer/metrics.js +12 -20
  40. package/dest/sequencer/sequencer.d.ts +17 -4
  41. package/dest/sequencer/sequencer.d.ts.map +1 -1
  42. package/dest/sequencer/sequencer.js +73 -20
  43. package/dest/sequencer/timetable.d.ts +14 -1
  44. package/dest/sequencer/timetable.d.ts.map +1 -1
  45. package/dest/sequencer/timetable.js +45 -36
  46. package/package.json +27 -27
  47. package/src/client/sequencer-client.ts +5 -7
  48. package/src/config.ts +0 -5
  49. package/src/global_variable_builder/README.md +44 -0
  50. package/src/global_variable_builder/fee_predictor.ts +172 -0
  51. package/src/global_variable_builder/fee_provider.ts +75 -0
  52. package/src/global_variable_builder/global_builder.ts +8 -43
  53. package/src/global_variable_builder/index.ts +2 -0
  54. package/src/publisher/sequencer-publisher-factory.ts +0 -3
  55. package/src/publisher/sequencer-publisher.ts +174 -158
  56. package/src/sequencer/README.md +83 -13
  57. package/src/sequencer/chain_state_overrides.ts +87 -0
  58. package/src/sequencer/checkpoint_proposal_job.ts +238 -153
  59. package/src/sequencer/checkpoint_proposal_job_metrics.ts +128 -0
  60. package/src/sequencer/checkpoint_voter.ts +1 -12
  61. package/src/sequencer/metrics.ts +15 -25
  62. package/src/sequencer/sequencer.ts +110 -22
  63. 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,21 +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 { getNextL1SlotTimestamp } from '@aztec/stdlib/epoch-helpers';
46
- import { SlashFactoryContract } from '@aztec/stdlib/l1-contracts';
47
+ import { getLastL1SlotTimestampForL2Slot, getNextL1SlotTimestamp } from '@aztec/stdlib/epoch-helpers';
47
48
  import type { CheckpointHeader } from '@aztec/stdlib/rollup';
48
49
  import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
49
50
  import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
50
51
 
51
52
  import {
52
53
  type Hex,
53
- type StateOverride,
54
54
  type TransactionReceipt,
55
55
  type TypedDataDefinition,
56
56
  encodeFunctionData,
@@ -63,6 +63,20 @@ import type { SequencerPublisherConfig } from './config.js';
63
63
  import { type FailedL1Tx, type L1TxFailedStore, createL1TxFailedStore } from './l1_tx_failed_store/index.js';
64
64
  import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
65
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
+
66
80
  /** Arguments to the process method of the rollup contract */
67
81
  type L1ProcessArgs = {
68
82
  /** The L2 block header. */
@@ -84,16 +98,13 @@ export const Actions = [
84
98
  'invalidate-by-insufficient-attestations',
85
99
  'propose',
86
100
  'governance-signal',
87
- 'empire-slashing-signal',
88
- 'create-empire-payload',
89
- 'execute-empire-payload',
90
101
  'vote-offenses',
91
102
  'execute-slash',
92
103
  ] as const;
93
104
 
94
105
  export type Action = (typeof Actions)[number];
95
106
 
96
- type GovernanceSignalAction = Extract<Action, 'governance-signal' | 'empire-slashing-signal'>;
107
+ type GovernanceSignalAction = Extract<Action, 'governance-signal'>;
97
108
 
98
109
  // Sorting for actions such that invalidations go before proposals, and proposals go before votes
99
110
  export const compareActions = (a: Action, b: Action) => Actions.indexOf(a) - Actions.indexOf(b);
@@ -104,6 +115,13 @@ export type InvalidateCheckpointRequest = {
104
115
  gasUsed: bigint;
105
116
  checkpointNumber: CheckpointNumber;
106
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;
107
125
  };
108
126
 
109
127
  interface RequestWithExpiry {
@@ -112,6 +130,8 @@ interface RequestWithExpiry {
112
130
  lastValidL2Slot: SlotNumber;
113
131
  gasConfig?: Pick<L1TxConfig, 'txTimeoutAt' | 'gasLimit'>;
114
132
  blobConfig?: L1BlobInputs;
133
+ /** Optional pre-send validation. If it rejects, the request is discarded. */
134
+ preCheck?: () => Promise<void>;
115
135
  checkSuccess: (
116
136
  request: L1TxRequest,
117
137
  result?: { receipt: TransactionReceipt; stats?: TransactionStats; errorMsg?: string },
@@ -135,7 +155,9 @@ export class SequencerPublisher {
135
155
  protected log: Logger;
136
156
  protected ethereumSlotDuration: bigint;
137
157
  protected aztecSlotDuration: bigint;
138
- private dateProvider: DateProvider;
158
+
159
+ /** Date provider for wall-clock time. */
160
+ private readonly dateProvider: DateProvider;
139
161
 
140
162
  private blobClient: BlobClientInterface;
141
163
 
@@ -151,6 +173,9 @@ export class SequencerPublisher {
151
173
  /** Fee asset price oracle for computing price modifiers from Uniswap V4 */
152
174
  private feeAssetPriceOracle: FeeAssetPriceOracle;
153
175
 
176
+ /** Interruptible sleep used by sendRequestsAt to wait until a target timestamp. */
177
+ private readonly interruptibleSleep = new InterruptibleSleep();
178
+
154
179
  // A CALL to a cold address is 2700 gas
155
180
  public static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
156
181
 
@@ -160,8 +185,7 @@ export class SequencerPublisher {
160
185
  public l1TxUtils: L1TxUtils;
161
186
  public rollupContract: RollupContract;
162
187
  public govProposerContract: GovernanceProposerContract;
163
- public slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
164
- public slashFactoryContract: SlashFactoryContract;
188
+ public slashingProposerContract: SlashingProposerContract | undefined;
165
189
 
166
190
  public readonly tracer: Tracer;
167
191
 
@@ -175,9 +199,8 @@ export class SequencerPublisher {
175
199
  blobClient: BlobClientInterface;
176
200
  l1TxUtils: L1TxUtils;
177
201
  rollupContract: RollupContract;
178
- slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
202
+ slashingProposerContract: SlashingProposerContract | undefined;
179
203
  governanceProposerContract: GovernanceProposerContract;
180
- slashFactoryContract: SlashFactoryContract;
181
204
  epochCache: EpochCache;
182
205
  dateProvider: DateProvider;
183
206
  metrics: SequencerPublisherMetrics;
@@ -194,6 +217,7 @@ export class SequencerPublisher {
194
217
  this.lastActions = deps.lastActions;
195
218
 
196
219
  this.blobClient = deps.blobClient;
220
+ this.dateProvider = deps.dateProvider;
197
221
 
198
222
  const telemetry = deps.telemetry ?? getTelemetryClient();
199
223
  this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
@@ -211,8 +235,6 @@ export class SequencerPublisher {
211
235
  const newSlashingProposer = await this.rollupContract.getSlashingProposer();
212
236
  this.slashingProposerContract = newSlashingProposer;
213
237
  });
214
- this.slashFactoryContract = deps.slashFactoryContract;
215
-
216
238
  // Initialize L1 fee analyzer for fisherman mode
217
239
  if (config.fishermanMode) {
218
240
  this.l1FeeAnalyzer = new L1FeeAnalyzer(
@@ -369,9 +391,10 @@ export class SequencerPublisher {
369
391
  * - undefined if no valid requests are found OR the tx failed to send.
370
392
  */
371
393
  @trackSpan('SequencerPublisher.sendRequests')
372
- public async sendRequests() {
394
+ public async sendRequests(): Promise<SendRequestsResult | undefined> {
373
395
  const requestsToProcess = [...this.requests];
374
396
  this.requests = [];
397
+
375
398
  if (this.interrupted || requestsToProcess.length === 0) {
376
399
  return undefined;
377
400
  }
@@ -530,6 +553,45 @@ export class SequencerPublisher {
530
553
  }
531
554
  }
532
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
+
533
595
  private callbackBundledTransactions(
534
596
  requests: RequestWithExpiry[],
535
597
  result: { receipt: TransactionReceipt; errorMsg?: string } | FormattedViemError | undefined,
@@ -605,22 +667,21 @@ export class SequencerPublisher {
605
667
  * @param tipArchive - The archive to check
606
668
  * @returns The slot and block number if it is possible to propose, undefined otherwise
607
669
  */
608
- public canProposeAt(
609
- tipArchive: Fr,
610
- msgSender: EthAddress,
611
- opts: { forcePendingCheckpointNumber?: CheckpointNumber; pipelined?: boolean } = {},
612
- ) {
670
+ public async canProposeAt(tipArchive: Fr, msgSender: EthAddress, simulationOverridesPlan?: SimulationOverridesPlan) {
613
671
  // TODO: #14291 - should loop through multiple keys to check if any of them can propose
614
672
  const ignoredErrors = ['SlotAlreadyInChain', 'InvalidProposer', 'InvalidArchive'];
615
673
 
616
- const pipelined = opts.pipelined ?? this.epochCache.isProposerPipeliningEnabled();
674
+ const pipelined = this.epochCache.isProposerPipeliningEnabled();
617
675
  const slotOffset = pipelined ? this.aztecSlotDuration : 0n;
618
676
  const nextL1SlotTs = this.getNextL1SlotTimestamp() + slotOffset;
619
677
 
620
678
  return this.rollupContract
621
- .canProposeAt(tipArchive.toBuffer(), msgSender.toString(), nextL1SlotTs, {
622
- forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
623
- })
679
+ .canProposeAt(
680
+ tipArchive.toBuffer(),
681
+ msgSender.toString(),
682
+ nextL1SlotTs,
683
+ await buildSimulationOverridesStateOverride(this.rollupContract, simulationOverridesPlan),
684
+ )
624
685
  .catch(err => {
625
686
  if (err instanceof FormattedViemError && ignoredErrors.find(e => err.message.includes(e))) {
626
687
  this.log.warn(`Failed canProposeAtTime check with ${ignoredErrors.find(e => err.message.includes(e))}`, {
@@ -642,7 +703,7 @@ export class SequencerPublisher {
642
703
  @trackSpan('SequencerPublisher.validateBlockHeader')
643
704
  public async validateBlockHeader(
644
705
  header: CheckpointHeader,
645
- opts?: { forcePendingCheckpointNumber: CheckpointNumber | undefined },
706
+ simulationOverridesPlan?: SimulationOverridesPlan,
646
707
  ): Promise<void> {
647
708
  const flags = { ignoreDA: true, ignoreSignatures: true };
648
709
 
@@ -656,10 +717,8 @@ export class SequencerPublisher {
656
717
  flags,
657
718
  ] as const;
658
719
 
659
- const ts = this.getNextL1SlotTimestamp();
660
- const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(
661
- opts?.forcePendingCheckpointNumber,
662
- );
720
+ const ts = this.getSimulationTimestamp(header.slotNumber);
721
+ const stateOverrides = await buildSimulationOverridesStateOverride(this.rollupContract, simulationOverridesPlan);
663
722
  let balance = 0n;
664
723
  if (this.config.fishermanMode) {
665
724
  // In fisherman mode, we can't know where the proposer is publishing from
@@ -679,7 +738,7 @@ export class SequencerPublisher {
679
738
  data: encodeFunctionData({ abi: RollupAbi, functionName: 'validateHeaderWithAttestations', args }),
680
739
  from: MULTI_CALL_3_ADDRESS,
681
740
  },
682
- { time: ts + 1n },
741
+ { time: ts },
683
742
  stateOverrides,
684
743
  );
685
744
  this.log.debug(`Simulated validateHeader`);
@@ -732,6 +791,7 @@ export class SequencerPublisher {
732
791
  gasUsed,
733
792
  checkpointNumber,
734
793
  forcePendingCheckpointNumber: CheckpointNumber(checkpointNumber - 1),
794
+ lastArchive: validationResult.checkpoint.lastArchive,
735
795
  reason,
736
796
  };
737
797
  } catch (err) {
@@ -744,8 +804,8 @@ export class SequencerPublisher {
744
804
  `Simulation for invalidate checkpoint ${checkpointNumber} failed due to checkpoint not being in pending chain`,
745
805
  { ...logData, request, error: viemError.message },
746
806
  );
747
- const latestPendingCheckpointNumber = await this.rollupContract.getCheckpointNumber();
748
- if (latestPendingCheckpointNumber < checkpointNumber) {
807
+ const latestProposedCheckpointNumber = await this.rollupContract.getCheckpointNumber();
808
+ if (latestProposedCheckpointNumber < checkpointNumber) {
749
809
  this.log.verbose(`Checkpoint ${checkpointNumber} has already been invalidated`, { ...logData });
750
810
  return undefined;
751
811
  } else {
@@ -819,11 +879,8 @@ export class SequencerPublisher {
819
879
  checkpoint: Checkpoint,
820
880
  attestationsAndSigners: CommitteeAttestationsAndSigners,
821
881
  attestationsAndSignersSignature: Signature,
822
- options: { forcePendingCheckpointNumber?: CheckpointNumber },
823
- ): Promise<bigint> {
824
- // Anchor the simulation timestamp to the checkpoint's own slot start time
825
- // rather than the current L1 block timestamp, which may overshoot into the next slot if the build ran late.
826
- const ts = checkpoint.header.timestamp;
882
+ simulationOverridesPlan?: SimulationOverridesPlan,
883
+ ): Promise<void> {
827
884
  const blobFields = checkpoint.toBlobFields();
828
885
  const blobs = await getBlobsPerL1Block(blobFields);
829
886
  const blobInput = getPrefixedEthBlobCommitments(blobs);
@@ -842,13 +899,11 @@ export class SequencerPublisher {
842
899
  blobInput,
843
900
  ] as const;
844
901
 
845
- await this.simulateProposeTx(args, ts, options);
846
- return ts;
902
+ await this.simulateProposeTx(args, simulationOverridesPlan);
847
903
  }
848
904
 
849
905
  private async enqueueCastSignalHelper(
850
906
  slotNumber: SlotNumber,
851
- timestamp: bigint,
852
907
  signalType: GovernanceSignalAction,
853
908
  payload: EthAddress,
854
909
  base: IEmpireBase,
@@ -927,13 +982,17 @@ export class SequencerPublisher {
927
982
  });
928
983
 
929
984
  const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
985
+ const timestamp = this.getSimulationTimestamp(slotNumber);
930
986
 
931
987
  try {
932
988
  await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
933
989
  this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
934
990
  } catch (err) {
935
991
  const viemError = formatViemError(err);
936
- 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
+ });
937
996
  this.backupFailedTx({
938
997
  id: keccak256(request.data!),
939
998
  failureType: 'simulation',
@@ -996,19 +1055,16 @@ export class SequencerPublisher {
996
1055
  /**
997
1056
  * Enqueues a governance castSignal transaction to cast a signal for a given slot number.
998
1057
  * @param slotNumber - The slot number to cast a signal for.
999
- * @param timestamp - The timestamp of the slot to cast a signal for.
1000
1058
  * @returns True if the signal was successfully enqueued, false otherwise.
1001
1059
  */
1002
1060
  public enqueueGovernanceCastSignal(
1003
1061
  governancePayload: EthAddress,
1004
1062
  slotNumber: SlotNumber,
1005
- timestamp: bigint,
1006
1063
  signerAddress: EthAddress,
1007
1064
  signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
1008
1065
  ): Promise<boolean> {
1009
1066
  return this.enqueueCastSignalHelper(
1010
1067
  slotNumber,
1011
- timestamp,
1012
1068
  'governance-signal',
1013
1069
  governancePayload,
1014
1070
  this.govProposerContract,
@@ -1021,7 +1077,6 @@ export class SequencerPublisher {
1021
1077
  public async enqueueSlashingActions(
1022
1078
  actions: ProposerSlashAction[],
1023
1079
  slotNumber: SlotNumber,
1024
- timestamp: bigint,
1025
1080
  signerAddress: EthAddress,
1026
1081
  signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
1027
1082
  ): Promise<boolean> {
@@ -1032,58 +1087,6 @@ export class SequencerPublisher {
1032
1087
 
1033
1088
  for (const action of actions) {
1034
1089
  switch (action.type) {
1035
- case 'vote-empire-payload': {
1036
- if (this.slashingProposerContract?.type !== 'empire') {
1037
- this.log.error('Cannot vote for empire payload on non-empire slashing contract');
1038
- break;
1039
- }
1040
- this.log.debug(`Enqueuing slashing vote for payload ${action.payload} at slot ${slotNumber}`, {
1041
- signerAddress,
1042
- });
1043
- await this.enqueueCastSignalHelper(
1044
- slotNumber,
1045
- timestamp,
1046
- 'empire-slashing-signal',
1047
- action.payload,
1048
- this.slashingProposerContract,
1049
- signerAddress,
1050
- signer,
1051
- );
1052
- break;
1053
- }
1054
-
1055
- case 'create-empire-payload': {
1056
- this.log.debug(`Enqueuing slashing create payload at slot ${slotNumber}`, { slotNumber, signerAddress });
1057
- const request = this.slashFactoryContract.buildCreatePayloadRequest(action.data);
1058
- await this.simulateAndEnqueueRequest(
1059
- 'create-empire-payload',
1060
- request,
1061
- (receipt: TransactionReceipt) =>
1062
- !!this.slashFactoryContract.tryExtractSlashPayloadCreatedEvent(receipt.logs),
1063
- slotNumber,
1064
- timestamp,
1065
- );
1066
- break;
1067
- }
1068
-
1069
- case 'execute-empire-payload': {
1070
- this.log.debug(`Enqueuing slashing execute payload at slot ${slotNumber}`, { slotNumber, signerAddress });
1071
- if (this.slashingProposerContract?.type !== 'empire') {
1072
- this.log.error('Cannot execute slashing payload on non-empire slashing contract');
1073
- return false;
1074
- }
1075
- const empireSlashingProposer = this.slashingProposerContract as EmpireSlashingProposerContract;
1076
- const request = empireSlashingProposer.buildExecuteRoundRequest(action.round);
1077
- await this.simulateAndEnqueueRequest(
1078
- 'execute-empire-payload',
1079
- request,
1080
- (receipt: TransactionReceipt) => !!empireSlashingProposer.tryExtractPayloadSubmittedEvent(receipt.logs),
1081
- slotNumber,
1082
- timestamp,
1083
- );
1084
- break;
1085
- }
1086
-
1087
1090
  case 'vote-offenses': {
1088
1091
  this.log.debug(`Enqueuing slashing vote for ${action.votes.length} votes at slot ${slotNumber}`, {
1089
1092
  slotNumber,
@@ -1091,19 +1094,17 @@ export class SequencerPublisher {
1091
1094
  votesCount: action.votes.length,
1092
1095
  signerAddress,
1093
1096
  });
1094
- if (this.slashingProposerContract?.type !== 'tally') {
1095
- 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');
1096
1099
  return false;
1097
1100
  }
1098
- const tallySlashingProposer = this.slashingProposerContract as TallySlashingProposerContract;
1099
1101
  const votes = bufferToHex(encodeSlashConsensusVotes(action.votes));
1100
- const request = await tallySlashingProposer.buildVoteRequestFromSigner(votes, slotNumber, signer);
1102
+ const request = await this.slashingProposerContract.buildVoteRequestFromSigner(votes, slotNumber, signer);
1101
1103
  await this.simulateAndEnqueueRequest(
1102
1104
  'vote-offenses',
1103
1105
  request,
1104
- (receipt: TransactionReceipt) => !!tallySlashingProposer.tryExtractVoteCastEvent(receipt.logs),
1106
+ (receipt: TransactionReceipt) => !!this.slashingProposerContract!.tryExtractVoteCastEvent(receipt.logs),
1105
1107
  slotNumber,
1106
- timestamp,
1107
1108
  );
1108
1109
  break;
1109
1110
  }
@@ -1114,18 +1115,20 @@ export class SequencerPublisher {
1114
1115
  round: action.round,
1115
1116
  signerAddress,
1116
1117
  });
1117
- if (this.slashingProposerContract?.type !== 'tally') {
1118
- 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');
1119
1120
  return false;
1120
1121
  }
1121
- const tallySlashingProposer = this.slashingProposerContract as TallySlashingProposerContract;
1122
- const request = tallySlashingProposer.buildExecuteRoundRequest(action.round, action.committees);
1122
+ const executeRequest = this.slashingProposerContract.buildExecuteRoundRequest(
1123
+ action.round,
1124
+ action.committees,
1125
+ );
1123
1126
  await this.simulateAndEnqueueRequest(
1124
1127
  'execute-slash',
1125
- request,
1126
- (receipt: TransactionReceipt) => !!tallySlashingProposer.tryExtractRoundExecutedEvent(receipt.logs),
1128
+ executeRequest,
1129
+ (receipt: TransactionReceipt) =>
1130
+ !!this.slashingProposerContract!.tryExtractRoundExecutedEvent(receipt.logs),
1127
1131
  slotNumber,
1128
- timestamp,
1129
1132
  );
1130
1133
  break;
1131
1134
  }
@@ -1145,7 +1148,7 @@ export class SequencerPublisher {
1145
1148
  checkpoint: Checkpoint,
1146
1149
  attestationsAndSigners: CommitteeAttestationsAndSigners,
1147
1150
  attestationsAndSignersSignature: Signature,
1148
- opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
1151
+ opts: EnqueueProposeCheckpointOpts = {},
1149
1152
  ): Promise<void> {
1150
1153
  const checkpointHeader = checkpoint.header;
1151
1154
 
@@ -1161,7 +1164,9 @@ export class SequencerPublisher {
1161
1164
  feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
1162
1165
  };
1163
1166
 
1164
- let ts: bigint;
1167
+ const simulationOverridesPlan = SimulationOverridesBuilder.from(opts.simulationOverridesPlan)
1168
+ .withoutBlobCheck()
1169
+ .build();
1165
1170
 
1166
1171
  try {
1167
1172
  // @note This will make sure that we are passing the checks for our header ASSUMING that the data is also made available
@@ -1169,23 +1174,47 @@ export class SequencerPublisher {
1169
1174
  // By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which
1170
1175
  // make time consistency checks break.
1171
1176
  // TODO(palla): Check whether we're validating twice, once here and once within addProposeTx, since we call simulateProposeTx in both places.
1172
- ts = await this.validateCheckpointForSubmission(
1177
+ await this.validateCheckpointForSubmission(
1173
1178
  checkpoint,
1174
1179
  attestationsAndSigners,
1175
1180
  attestationsAndSignersSignature,
1176
- opts,
1181
+ simulationOverridesPlan,
1177
1182
  );
1178
1183
  } catch (err: any) {
1179
1184
  this.log.error(`Checkpoint validation failed. ${err instanceof Error ? err.message : 'No error message'}`, err, {
1180
1185
  ...checkpoint.getStats(),
1181
1186
  slotNumber: checkpoint.header.slotNumber,
1182
- forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
1187
+ simulationOverridesPlan,
1183
1188
  });
1184
1189
  throw err;
1185
1190
  }
1186
1191
 
1187
- this.log.verbose(`Enqueuing checkpoint propose transaction`, { ...checkpoint.toCheckpointInfo(), ...opts });
1188
- 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
+ );
1189
1218
  }
1190
1219
 
1191
1220
  public enqueueInvalidateCheckpoint(
@@ -1228,8 +1257,8 @@ export class SequencerPublisher {
1228
1257
  request: L1TxRequest,
1229
1258
  checkSuccess: (receipt: TransactionReceipt) => boolean | undefined,
1230
1259
  slotNumber: SlotNumber,
1231
- timestamp: bigint,
1232
1260
  ) {
1261
+ const timestamp = this.getSimulationTimestamp(slotNumber);
1233
1262
  const logData = { slotNumber, timestamp, gasLimit: undefined as bigint | undefined };
1234
1263
  if (this.lastActions[action] && this.lastActions[action] === slotNumber) {
1235
1264
  this.log.debug(`Skipping duplicate action ${action} for slot ${slotNumber}`);
@@ -1245,8 +1274,9 @@ export class SequencerPublisher {
1245
1274
 
1246
1275
  let gasUsed: bigint;
1247
1276
  const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
1277
+
1248
1278
  try {
1249
- ({ 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));
1250
1280
  this.log.verbose(`Simulation for ${action} succeeded`, { ...logData, request, gasUsed });
1251
1281
  } catch (err) {
1252
1282
  const viemError = formatViemError(err, simulateAbi);
@@ -1304,6 +1334,7 @@ export class SequencerPublisher {
1304
1334
  */
1305
1335
  public interrupt() {
1306
1336
  this.interrupted = true;
1337
+ this.interruptibleSleep.interrupt();
1307
1338
  this.l1TxUtils.interrupt();
1308
1339
  }
1309
1340
 
@@ -1313,11 +1344,7 @@ export class SequencerPublisher {
1313
1344
  this.l1TxUtils.restart();
1314
1345
  }
1315
1346
 
1316
- private async prepareProposeTx(
1317
- encodedData: L1ProcessArgs,
1318
- timestamp: bigint,
1319
- options: { forcePendingCheckpointNumber?: CheckpointNumber },
1320
- ) {
1347
+ private async prepareProposeTx(encodedData: L1ProcessArgs, simulationOverridesPlan?: SimulationOverridesPlan) {
1321
1348
  const kzg = Blob.getViemKzgInstance();
1322
1349
  const blobInput = getPrefixedEthBlobCommitments(encodedData.blobs);
1323
1350
  this.log.debug('Validating blob input', { blobInput });
@@ -1388,7 +1415,7 @@ export class SequencerPublisher {
1388
1415
  blobInput,
1389
1416
  ] as const;
1390
1417
 
1391
- const { rollupData, simulationResult } = await this.simulateProposeTx(args, timestamp, options);
1418
+ const { rollupData, simulationResult } = await this.simulateProposeTx(args, simulationOverridesPlan);
1392
1419
 
1393
1420
  return { args, blobEvaluationGas, rollupData, simulationResult };
1394
1421
  }
@@ -1396,7 +1423,6 @@ export class SequencerPublisher {
1396
1423
  /**
1397
1424
  * Simulates the propose tx with eth_simulateV1
1398
1425
  * @param args - The propose tx args
1399
- * @param timestamp - The timestamp to simulate proposal at
1400
1426
  * @returns The simulation result
1401
1427
  */
1402
1428
  private async simulateProposeTx(
@@ -1413,8 +1439,7 @@ export class SequencerPublisher {
1413
1439
  ViemSignature,
1414
1440
  `0x${string}`,
1415
1441
  ],
1416
- timestamp: bigint,
1417
- options: { forcePendingCheckpointNumber?: CheckpointNumber },
1442
+ simulationOverridesPlan?: SimulationOverridesPlan,
1418
1443
  ) {
1419
1444
  const rollupData = encodeFunctionData({
1420
1445
  abi: RollupAbi,
@@ -1422,23 +1447,7 @@ export class SequencerPublisher {
1422
1447
  args,
1423
1448
  });
1424
1449
 
1425
- // override the pending checkpoint number if requested
1426
- const forcePendingCheckpointNumberStateDiff = (
1427
- options.forcePendingCheckpointNumber !== undefined
1428
- ? await this.rollupContract.makePendingCheckpointNumberOverride(options.forcePendingCheckpointNumber)
1429
- : []
1430
- ).flatMap(override => override.stateDiff ?? []);
1431
-
1432
- const stateOverrides: StateOverride = [
1433
- {
1434
- address: this.rollupContract.address,
1435
- // @note we override checkBlob to false since blobs are not part simulate()
1436
- stateDiff: [
1437
- { slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true), value: toPaddedHex(0n, true) },
1438
- ...forcePendingCheckpointNumberStateDiff,
1439
- ],
1440
- },
1441
- ];
1450
+ const stateOverrides = await buildSimulationOverridesStateOverride(this.rollupContract, simulationOverridesPlan);
1442
1451
  // In fisherman mode, simulate as the proposer but with sufficient balance
1443
1452
  if (this.proposerAddressForSimulation) {
1444
1453
  stateOverrides.push({
@@ -1448,6 +1457,7 @@ export class SequencerPublisher {
1448
1457
  }
1449
1458
 
1450
1459
  const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1460
+ const simTs = this.getSimulationTimestamp(SlotNumber.fromBigInt(args[0].header.slotNumber));
1451
1461
 
1452
1462
  const simulationResult = await this.l1TxUtils
1453
1463
  .simulate(
@@ -1458,8 +1468,7 @@ export class SequencerPublisher {
1458
1468
  ...(this.proposerAddressForSimulation && { from: this.proposerAddressForSimulation.toString() }),
1459
1469
  },
1460
1470
  {
1461
- // @note we add 1n to the timestamp because geth implementation doesn't like simulation timestamp to be equal to the current block timestamp
1462
- time: timestamp + 1n,
1471
+ time: simTs,
1463
1472
  // @note reth should have a 30m gas limit per block but throws errors that this tx is beyond limit so we increase here
1464
1473
  gasLimit: MAX_L1_TX_LIMIT * 2n,
1465
1474
  },
@@ -1481,7 +1490,7 @@ export class SequencerPublisher {
1481
1490
  logs: [],
1482
1491
  };
1483
1492
  }
1484
- this.log.error(`Failed to simulate propose tx`, viemError);
1493
+ this.log.error(`Failed to simulate propose tx`, viemError, { simulationTimestamp: simTs });
1485
1494
  this.backupFailedTx({
1486
1495
  id: keccak256(rollupData),
1487
1496
  failureType: 'simulation',
@@ -1503,16 +1512,15 @@ export class SequencerPublisher {
1503
1512
  private async addProposeTx(
1504
1513
  checkpoint: Checkpoint,
1505
1514
  encodedData: L1ProcessArgs,
1506
- opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
1507
- timestamp: bigint,
1515
+ opts: EnqueueProposeCheckpointOpts = {},
1516
+ preCheck?: () => Promise<void>,
1508
1517
  ): Promise<void> {
1509
1518
  const slot = checkpoint.header.slotNumber;
1510
1519
  const timer = new Timer();
1511
1520
  const kzg = Blob.getViemKzgInstance();
1512
1521
  const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(
1513
1522
  encodedData,
1514
- timestamp,
1515
- opts,
1523
+ opts.simulationOverridesPlan,
1516
1524
  );
1517
1525
  const startBlock = await this.l1TxUtils.getBlockNumber();
1518
1526
  const gasLimit = this.l1TxUtils.bumpGasLimit(
@@ -1536,7 +1544,8 @@ export class SequencerPublisher {
1536
1544
  data: rollupData,
1537
1545
  },
1538
1546
  lastValidL2Slot: checkpoint.header.slotNumber,
1539
- gasConfig: { ...opts, gasLimit },
1547
+ gasConfig: { txTimeoutAt: opts.txTimeoutAt, gasLimit },
1548
+ preCheck,
1540
1549
  blobConfig: {
1541
1550
  blobs: encodedData.blobs.map(b => b.data),
1542
1551
  kzg,
@@ -1590,7 +1599,14 @@ export class SequencerPublisher {
1590
1599
  });
1591
1600
  }
1592
1601
 
1593
- /** Returns the timestamp to use when simulating L1 proposal calls */
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. */
1594
1610
  private getNextL1SlotTimestamp(): bigint {
1595
1611
  const l1Constants = this.epochCache.getL1Constants();
1596
1612
  return getNextL1SlotTimestamp(this.dateProvider.nowInSeconds(), l1Constants);