@aztec/sequencer-client 0.0.1-commit.27d773e65 → 0.0.1-commit.2b2662070

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 (80) 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 +49 -27
  4. package/dest/config.d.ts +25 -5
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +36 -25
  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 -50
  16. package/dest/global_variable_builder/index.d.ts +4 -2
  17. package/dest/global_variable_builder/index.d.ts.map +1 -1
  18. package/dest/global_variable_builder/index.js +2 -0
  19. package/dest/publisher/config.d.ts +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 +16 -3
  27. package/dest/publisher/sequencer-publisher.d.ts +55 -43
  28. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  29. package/dest/publisher/sequencer-publisher.js +180 -116
  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 +26 -9
  34. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  35. package/dest/sequencer/checkpoint_proposal_job.js +350 -214
  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/events.d.ts +2 -1
  43. package/dest/sequencer/events.d.ts.map +1 -1
  44. package/dest/sequencer/metrics.d.ts +9 -10
  45. package/dest/sequencer/metrics.d.ts.map +1 -1
  46. package/dest/sequencer/metrics.js +23 -20
  47. package/dest/sequencer/sequencer.d.ts +29 -11
  48. package/dest/sequencer/sequencer.d.ts.map +1 -1
  49. package/dest/sequencer/sequencer.js +137 -74
  50. package/dest/sequencer/timetable.d.ts +17 -3
  51. package/dest/sequencer/timetable.d.ts.map +1 -1
  52. package/dest/sequencer/timetable.js +51 -43
  53. package/dest/sequencer/types.d.ts +2 -2
  54. package/dest/sequencer/types.d.ts.map +1 -1
  55. package/dest/test/mock_checkpoint_builder.d.ts +7 -9
  56. package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
  57. package/dest/test/mock_checkpoint_builder.js +39 -30
  58. package/package.json +27 -28
  59. package/src/client/sequencer-client.ts +61 -28
  60. package/src/config.ts +44 -27
  61. package/src/global_variable_builder/README.md +44 -0
  62. package/src/global_variable_builder/fee_predictor.ts +172 -0
  63. package/src/global_variable_builder/fee_provider.ts +75 -0
  64. package/src/global_variable_builder/global_builder.ts +26 -62
  65. package/src/global_variable_builder/index.ts +3 -1
  66. package/src/publisher/config.ts +38 -4
  67. package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +3 -1
  68. package/src/publisher/sequencer-publisher-factory.ts +18 -6
  69. package/src/publisher/sequencer-publisher.ts +263 -167
  70. package/src/sequencer/README.md +83 -13
  71. package/src/sequencer/chain_state_overrides.ts +87 -0
  72. package/src/sequencer/checkpoint_proposal_job.ts +452 -241
  73. package/src/sequencer/checkpoint_proposal_job_metrics.ts +128 -0
  74. package/src/sequencer/checkpoint_voter.ts +1 -12
  75. package/src/sequencer/events.ts +1 -1
  76. package/src/sequencer/metrics.ts +29 -25
  77. package/src/sequencer/sequencer.ts +198 -81
  78. package/src/sequencer/timetable.ts +64 -52
  79. package/src/sequencer/types.ts +1 -1
  80. package/src/test/mock_checkpoint_builder.ts +51 -48
@@ -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,28 +28,29 @@ 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';
32
+ import { trimmedBytesLength } from '@aztec/foundation/buffer';
31
33
  import { pick } from '@aztec/foundation/collection';
32
34
  import type { Fr } from '@aztec/foundation/curves/bn254';
35
+ import { TimeoutError } from '@aztec/foundation/error';
33
36
  import { EthAddress } from '@aztec/foundation/eth-address';
34
37
  import { Signature, type ViemSignature } from '@aztec/foundation/eth-signature';
35
38
  import { type Logger, createLogger } from '@aztec/foundation/log';
36
39
  import { makeBackoff, retry } from '@aztec/foundation/retry';
40
+ import { InterruptibleSleep } from '@aztec/foundation/sleep';
37
41
  import { bufferToHex } from '@aztec/foundation/string';
38
- import { DateProvider, Timer } from '@aztec/foundation/timer';
42
+ import { type DateProvider, Timer } from '@aztec/foundation/timer';
39
43
  import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
40
44
  import { type ProposerSlashAction, encodeSlashConsensusVotes } from '@aztec/slasher';
41
45
  import { CommitteeAttestationsAndSigners, type ValidateCheckpointResult } from '@aztec/stdlib/block';
42
46
  import type { Checkpoint } from '@aztec/stdlib/checkpoint';
43
- import { SlashFactoryContract } from '@aztec/stdlib/l1-contracts';
47
+ import { getLastL1SlotTimestampForL2Slot, getNextL1SlotTimestamp } from '@aztec/stdlib/epoch-helpers';
44
48
  import type { CheckpointHeader } from '@aztec/stdlib/rollup';
45
49
  import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
46
50
  import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
47
51
 
48
52
  import {
49
53
  type Hex,
50
- type StateOverride,
51
54
  type TransactionReceipt,
52
55
  type TypedDataDefinition,
53
56
  encodeFunctionData,
@@ -60,6 +63,20 @@ import type { SequencerPublisherConfig } from './config.js';
60
63
  import { type FailedL1Tx, type L1TxFailedStore, createL1TxFailedStore } from './l1_tx_failed_store/index.js';
61
64
  import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
62
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
+
63
80
  /** Arguments to the process method of the rollup contract */
64
81
  type L1ProcessArgs = {
65
82
  /** The L2 block header. */
@@ -81,16 +98,13 @@ export const Actions = [
81
98
  'invalidate-by-insufficient-attestations',
82
99
  'propose',
83
100
  'governance-signal',
84
- 'empire-slashing-signal',
85
- 'create-empire-payload',
86
- 'execute-empire-payload',
87
101
  'vote-offenses',
88
102
  'execute-slash',
89
103
  ] as const;
90
104
 
91
105
  export type Action = (typeof Actions)[number];
92
106
 
93
- type GovernanceSignalAction = Extract<Action, 'governance-signal' | 'empire-slashing-signal'>;
107
+ type GovernanceSignalAction = Extract<Action, 'governance-signal'>;
94
108
 
95
109
  // Sorting for actions such that invalidations go before proposals, and proposals go before votes
96
110
  export const compareActions = (a: Action, b: Action) => Actions.indexOf(a) - Actions.indexOf(b);
@@ -101,6 +115,13 @@ export type InvalidateCheckpointRequest = {
101
115
  gasUsed: bigint;
102
116
  checkpointNumber: CheckpointNumber;
103
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;
104
125
  };
105
126
 
106
127
  interface RequestWithExpiry {
@@ -109,6 +130,8 @@ interface RequestWithExpiry {
109
130
  lastValidL2Slot: SlotNumber;
110
131
  gasConfig?: Pick<L1TxConfig, 'txTimeoutAt' | 'gasLimit'>;
111
132
  blobConfig?: L1BlobInputs;
133
+ /** Optional pre-send validation. If it rejects, the request is discarded. */
134
+ preCheck?: () => Promise<void>;
112
135
  checkSuccess: (
113
136
  request: L1TxRequest,
114
137
  result?: { receipt: TransactionReceipt; stats?: TransactionStats; errorMsg?: string },
@@ -131,18 +154,28 @@ export class SequencerPublisher {
131
154
 
132
155
  protected log: Logger;
133
156
  protected ethereumSlotDuration: bigint;
157
+ protected aztecSlotDuration: bigint;
158
+
159
+ /** Date provider for wall-clock time. */
160
+ private readonly dateProvider: DateProvider;
134
161
 
135
162
  private blobClient: BlobClientInterface;
136
163
 
137
164
  /** Address to use for simulations in fisherman mode (actual proposer's address) */
138
165
  private proposerAddressForSimulation?: EthAddress;
139
166
 
167
+ /** Optional callback to obtain a replacement publisher when the current one fails to send. */
168
+ private getNextPublisher?: (excludeAddresses: EthAddress[]) => Promise<L1TxUtils | undefined>;
169
+
140
170
  /** L1 fee analyzer for fisherman mode */
141
171
  private l1FeeAnalyzer?: L1FeeAnalyzer;
142
172
 
143
173
  /** Fee asset price oracle for computing price modifiers from Uniswap V4 */
144
174
  private feeAssetPriceOracle: FeeAssetPriceOracle;
145
175
 
176
+ /** Interruptible sleep used by sendRequestsAt to wait until a target timestamp. */
177
+ private readonly interruptibleSleep = new InterruptibleSleep();
178
+
146
179
  // A CALL to a cold address is 2700 gas
147
180
  public static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
148
181
 
@@ -152,8 +185,7 @@ export class SequencerPublisher {
152
185
  public l1TxUtils: L1TxUtils;
153
186
  public rollupContract: RollupContract;
154
187
  public govProposerContract: GovernanceProposerContract;
155
- public slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
156
- public slashFactoryContract: SlashFactoryContract;
188
+ public slashingProposerContract: SlashingProposerContract | undefined;
157
189
 
158
190
  public readonly tracer: Tracer;
159
191
 
@@ -161,33 +193,37 @@ export class SequencerPublisher {
161
193
 
162
194
  constructor(
163
195
  private config: Pick<SequencerPublisherConfig, 'fishermanMode' | 'l1TxFailedStore'> &
164
- Pick<L1ContractsConfig, 'ethereumSlotDuration'> & { l1ChainId: number },
196
+ Pick<L1ContractsConfig, 'ethereumSlotDuration' | 'aztecSlotDuration'> & { l1ChainId: number },
165
197
  deps: {
166
198
  telemetry?: TelemetryClient;
167
199
  blobClient: BlobClientInterface;
168
200
  l1TxUtils: L1TxUtils;
169
201
  rollupContract: RollupContract;
170
- slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
202
+ slashingProposerContract: SlashingProposerContract | undefined;
171
203
  governanceProposerContract: GovernanceProposerContract;
172
- slashFactoryContract: SlashFactoryContract;
173
204
  epochCache: EpochCache;
174
205
  dateProvider: DateProvider;
175
206
  metrics: SequencerPublisherMetrics;
176
207
  lastActions: Partial<Record<Action, SlotNumber>>;
177
208
  log?: Logger;
209
+ getNextPublisher?: (excludeAddresses: EthAddress[]) => Promise<L1TxUtils | undefined>;
178
210
  },
179
211
  ) {
180
212
  this.log = deps.log ?? createLogger('sequencer:publisher');
181
213
  this.ethereumSlotDuration = BigInt(config.ethereumSlotDuration);
214
+ this.aztecSlotDuration = BigInt(config.aztecSlotDuration);
215
+ this.dateProvider = deps.dateProvider;
182
216
  this.epochCache = deps.epochCache;
183
217
  this.lastActions = deps.lastActions;
184
218
 
185
219
  this.blobClient = deps.blobClient;
220
+ this.dateProvider = deps.dateProvider;
186
221
 
187
222
  const telemetry = deps.telemetry ?? getTelemetryClient();
188
223
  this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
189
224
  this.tracer = telemetry.getTracer('SequencerPublisher');
190
225
  this.l1TxUtils = deps.l1TxUtils;
226
+ this.getNextPublisher = deps.getNextPublisher;
191
227
 
192
228
  this.rollupContract = deps.rollupContract;
193
229
 
@@ -199,8 +235,6 @@ export class SequencerPublisher {
199
235
  const newSlashingProposer = await this.rollupContract.getSlashingProposer();
200
236
  this.slashingProposerContract = newSlashingProposer;
201
237
  });
202
- this.slashFactoryContract = deps.slashFactoryContract;
203
-
204
238
  // Initialize L1 fee analyzer for fisherman mode
205
239
  if (config.fishermanMode) {
206
240
  this.l1FeeAnalyzer = new L1FeeAnalyzer(
@@ -279,7 +313,7 @@ export class SequencerPublisher {
279
313
  }
280
314
 
281
315
  public getCurrentL2Slot(): SlotNumber {
282
- return this.epochCache.getEpochAndSlotNow().slot;
316
+ return this.epochCache.getSlotNow();
283
317
  }
284
318
 
285
319
  /**
@@ -357,9 +391,10 @@ export class SequencerPublisher {
357
391
  * - undefined if no valid requests are found OR the tx failed to send.
358
392
  */
359
393
  @trackSpan('SequencerPublisher.sendRequests')
360
- public async sendRequests() {
394
+ public async sendRequests(): Promise<SendRequestsResult | undefined> {
361
395
  const requestsToProcess = [...this.requests];
362
396
  this.requests = [];
397
+
363
398
  if (this.interrupted || requestsToProcess.length === 0) {
364
399
  return undefined;
365
400
  }
@@ -392,8 +427,8 @@ export class SequencerPublisher {
392
427
  // @note - we can only have one blob config per bundle
393
428
  // find requests with gas and blob configs
394
429
  // See https://github.com/AztecProtocol/aztec-packages/issues/11513
395
- const gasConfigs = requestsToProcess.filter(request => request.gasConfig).map(request => request.gasConfig);
396
- const blobConfigs = requestsToProcess.filter(request => request.blobConfig).map(request => request.blobConfig);
430
+ const gasConfigs = validRequests.filter(request => request.gasConfig).map(request => request.gasConfig);
431
+ const blobConfigs = validRequests.filter(request => request.blobConfig).map(request => request.blobConfig);
397
432
 
398
433
  if (blobConfigs.length > 1) {
399
434
  throw new Error('Multiple blob configs found');
@@ -437,19 +472,16 @@ export class SequencerPublisher {
437
472
  });
438
473
  const blobDataHex = blobConfig?.blobs?.map(b => toHex(b)) as Hex[] | undefined;
439
474
 
475
+ const txContext = { multicallData, blobData: blobDataHex, l1BlockNumber };
476
+
440
477
  this.log.debug('Forwarding transactions', {
441
478
  validRequests: validRequests.map(request => request.action),
442
479
  txConfig,
443
480
  });
444
- const result = await Multicall3.forward(
445
- validRequests.map(request => request.request),
446
- this.l1TxUtils,
447
- txConfig,
448
- blobConfig,
449
- this.rollupContract.address,
450
- this.log,
451
- );
452
- const txContext = { multicallData, blobData: blobDataHex, l1BlockNumber };
481
+ const result = await this.forwardWithPublisherRotation(validRequests, txConfig, blobConfig);
482
+ if (result === undefined) {
483
+ return undefined;
484
+ }
453
485
  const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(
454
486
  validRequests,
455
487
  result,
@@ -472,6 +504,94 @@ export class SequencerPublisher {
472
504
  }
473
505
  }
474
506
 
507
+ /**
508
+ * Forwards transactions via Multicall3, rotating to the next available publisher if a send
509
+ * failure occurs (i.e. the tx never reached the chain).
510
+ * On-chain reverts and simulation errors are returned as-is without rotation.
511
+ */
512
+ private async forwardWithPublisherRotation(
513
+ validRequests: RequestWithExpiry[],
514
+ txConfig: RequestWithExpiry['gasConfig'],
515
+ blobConfig: L1BlobInputs | undefined,
516
+ ) {
517
+ const triedAddresses: EthAddress[] = [];
518
+ let currentPublisher = this.l1TxUtils;
519
+
520
+ while (true) {
521
+ triedAddresses.push(currentPublisher.getSenderAddress());
522
+ try {
523
+ const result = await Multicall3.forward(
524
+ validRequests.map(r => r.request),
525
+ currentPublisher,
526
+ txConfig,
527
+ blobConfig,
528
+ this.rollupContract.address,
529
+ this.log,
530
+ );
531
+ this.l1TxUtils = currentPublisher;
532
+ return result;
533
+ } catch (err) {
534
+ if (err instanceof TimeoutError) {
535
+ throw err;
536
+ }
537
+ const viemError = formatViemError(err);
538
+ if (!this.getNextPublisher) {
539
+ this.log.error('Failed to publish bundled transactions', viemError);
540
+ return undefined;
541
+ }
542
+ this.log.warn(
543
+ `Publisher ${currentPublisher.getSenderAddress()} failed to send, rotating to next publisher`,
544
+ viemError,
545
+ );
546
+ const nextPublisher = await this.getNextPublisher([...triedAddresses]);
547
+ if (!nextPublisher) {
548
+ this.log.error('All available publishers exhausted, failed to publish bundled transactions');
549
+ return undefined;
550
+ }
551
+ currentPublisher = nextPublisher;
552
+ }
553
+ }
554
+ }
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
+
475
595
  private callbackBundledTransactions(
476
596
  requests: RequestWithExpiry[],
477
597
  result: { receipt: TransactionReceipt; errorMsg?: string } | FormattedViemError | undefined,
@@ -495,7 +615,16 @@ export class SequencerPublisher {
495
615
  });
496
616
  return { failedActions: requests.map(r => r.action) };
497
617
  } else {
498
- this.log.verbose(`Published bundled transactions (${actionsListStr})`, { result, requests });
618
+ this.log.verbose(`Published bundled transactions (${actionsListStr})`, {
619
+ result,
620
+ requests: requests.map(r => ({
621
+ ...r,
622
+ // Avoid logging large blob data
623
+ blobConfig: r.blobConfig
624
+ ? { ...r.blobConfig, blobs: r.blobConfig.blobs.map(b => ({ size: trimmedBytesLength(b) })) }
625
+ : undefined,
626
+ })),
627
+ });
499
628
  const successfulActions: Action[] = [];
500
629
  const failedActions: Action[] = [];
501
630
  for (const request of requests) {
@@ -534,22 +663,25 @@ export class SequencerPublisher {
534
663
  }
535
664
 
536
665
  /**
537
- * @notice Will call `canProposeAtNextEthBlock` to make sure that it is possible to propose
666
+ * @notice Will call `canProposeAt` to make sure that it is possible to propose
538
667
  * @param tipArchive - The archive to check
539
668
  * @returns The slot and block number if it is possible to propose, undefined otherwise
540
669
  */
541
- public canProposeAtNextEthBlock(
542
- tipArchive: Fr,
543
- msgSender: EthAddress,
544
- opts: { forcePendingCheckpointNumber?: CheckpointNumber } = {},
545
- ) {
670
+ public async canProposeAt(tipArchive: Fr, msgSender: EthAddress, simulationOverridesPlan?: SimulationOverridesPlan) {
546
671
  // TODO: #14291 - should loop through multiple keys to check if any of them can propose
547
672
  const ignoredErrors = ['SlotAlreadyInChain', 'InvalidProposer', 'InvalidArchive'];
548
673
 
674
+ const pipelined = this.epochCache.isProposerPipeliningEnabled();
675
+ const slotOffset = pipelined ? this.aztecSlotDuration : 0n;
676
+ const nextL1SlotTs = this.getNextL1SlotTimestamp() + slotOffset;
677
+
549
678
  return this.rollupContract
550
- .canProposeAtNextEthBlock(tipArchive.toBuffer(), msgSender.toString(), Number(this.ethereumSlotDuration), {
551
- forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
552
- })
679
+ .canProposeAt(
680
+ tipArchive.toBuffer(),
681
+ msgSender.toString(),
682
+ nextL1SlotTs,
683
+ await buildSimulationOverridesStateOverride(this.rollupContract, simulationOverridesPlan),
684
+ )
553
685
  .catch(err => {
554
686
  if (err instanceof FormattedViemError && ignoredErrors.find(e => err.message.includes(e))) {
555
687
  this.log.warn(`Failed canProposeAtTime check with ${ignoredErrors.find(e => err.message.includes(e))}`, {
@@ -561,6 +693,7 @@ export class SequencerPublisher {
561
693
  return undefined;
562
694
  });
563
695
  }
696
+
564
697
  /**
565
698
  * @notice Will simulate `validateHeader` to make sure that the block header is valid
566
699
  * @dev This is a convenience function that can be used by the sequencer to validate a "partial" header.
@@ -570,7 +703,7 @@ export class SequencerPublisher {
570
703
  @trackSpan('SequencerPublisher.validateBlockHeader')
571
704
  public async validateBlockHeader(
572
705
  header: CheckpointHeader,
573
- opts?: { forcePendingCheckpointNumber: CheckpointNumber | undefined },
706
+ simulationOverridesPlan?: SimulationOverridesPlan,
574
707
  ): Promise<void> {
575
708
  const flags = { ignoreDA: true, ignoreSignatures: true };
576
709
 
@@ -584,10 +717,8 @@ export class SequencerPublisher {
584
717
  flags,
585
718
  ] as const;
586
719
 
587
- const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
588
- const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(
589
- opts?.forcePendingCheckpointNumber,
590
- );
720
+ const ts = this.getSimulationTimestamp(header.slotNumber);
721
+ const stateOverrides = await buildSimulationOverridesStateOverride(this.rollupContract, simulationOverridesPlan);
591
722
  let balance = 0n;
592
723
  if (this.config.fishermanMode) {
593
724
  // In fisherman mode, we can't know where the proposer is publishing from
@@ -607,7 +738,7 @@ export class SequencerPublisher {
607
738
  data: encodeFunctionData({ abi: RollupAbi, functionName: 'validateHeaderWithAttestations', args }),
608
739
  from: MULTI_CALL_3_ADDRESS,
609
740
  },
610
- { time: ts + 1n },
741
+ { time: ts },
611
742
  stateOverrides,
612
743
  );
613
744
  this.log.debug(`Simulated validateHeader`);
@@ -660,6 +791,7 @@ export class SequencerPublisher {
660
791
  gasUsed,
661
792
  checkpointNumber,
662
793
  forcePendingCheckpointNumber: CheckpointNumber(checkpointNumber - 1),
794
+ lastArchive: validationResult.checkpoint.lastArchive,
663
795
  reason,
664
796
  };
665
797
  } catch (err) {
@@ -672,8 +804,8 @@ export class SequencerPublisher {
672
804
  `Simulation for invalidate checkpoint ${checkpointNumber} failed due to checkpoint not being in pending chain`,
673
805
  { ...logData, request, error: viemError.message },
674
806
  );
675
- const latestPendingCheckpointNumber = await this.rollupContract.getCheckpointNumber();
676
- if (latestPendingCheckpointNumber < checkpointNumber) {
807
+ const latestProposedCheckpointNumber = await this.rollupContract.getCheckpointNumber();
808
+ if (latestProposedCheckpointNumber < checkpointNumber) {
677
809
  this.log.verbose(`Checkpoint ${checkpointNumber} has already been invalidated`, { ...logData });
678
810
  return undefined;
679
811
  } else {
@@ -747,9 +879,8 @@ export class SequencerPublisher {
747
879
  checkpoint: Checkpoint,
748
880
  attestationsAndSigners: CommitteeAttestationsAndSigners,
749
881
  attestationsAndSignersSignature: Signature,
750
- options: { forcePendingCheckpointNumber?: CheckpointNumber },
751
- ): Promise<bigint> {
752
- const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
882
+ simulationOverridesPlan?: SimulationOverridesPlan,
883
+ ): Promise<void> {
753
884
  const blobFields = checkpoint.toBlobFields();
754
885
  const blobs = await getBlobsPerL1Block(blobFields);
755
886
  const blobInput = getPrefixedEthBlobCommitments(blobs);
@@ -768,13 +899,11 @@ export class SequencerPublisher {
768
899
  blobInput,
769
900
  ] as const;
770
901
 
771
- await this.simulateProposeTx(args, ts, options);
772
- return ts;
902
+ await this.simulateProposeTx(args, simulationOverridesPlan);
773
903
  }
774
904
 
775
905
  private async enqueueCastSignalHelper(
776
906
  slotNumber: SlotNumber,
777
- timestamp: bigint,
778
907
  signalType: GovernanceSignalAction,
779
908
  payload: EthAddress,
780
909
  base: IEmpireBase,
@@ -853,13 +982,17 @@ export class SequencerPublisher {
853
982
  });
854
983
 
855
984
  const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
985
+ const timestamp = this.getSimulationTimestamp(slotNumber);
856
986
 
857
987
  try {
858
988
  await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
859
989
  this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
860
990
  } catch (err) {
861
991
  const viemError = formatViemError(err);
862
- 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
+ });
863
996
  this.backupFailedTx({
864
997
  id: keccak256(request.data!),
865
998
  failureType: 'simulation',
@@ -922,19 +1055,16 @@ export class SequencerPublisher {
922
1055
  /**
923
1056
  * Enqueues a governance castSignal transaction to cast a signal for a given slot number.
924
1057
  * @param slotNumber - The slot number to cast a signal for.
925
- * @param timestamp - The timestamp of the slot to cast a signal for.
926
1058
  * @returns True if the signal was successfully enqueued, false otherwise.
927
1059
  */
928
1060
  public enqueueGovernanceCastSignal(
929
1061
  governancePayload: EthAddress,
930
1062
  slotNumber: SlotNumber,
931
- timestamp: bigint,
932
1063
  signerAddress: EthAddress,
933
1064
  signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
934
1065
  ): Promise<boolean> {
935
1066
  return this.enqueueCastSignalHelper(
936
1067
  slotNumber,
937
- timestamp,
938
1068
  'governance-signal',
939
1069
  governancePayload,
940
1070
  this.govProposerContract,
@@ -947,7 +1077,6 @@ export class SequencerPublisher {
947
1077
  public async enqueueSlashingActions(
948
1078
  actions: ProposerSlashAction[],
949
1079
  slotNumber: SlotNumber,
950
- timestamp: bigint,
951
1080
  signerAddress: EthAddress,
952
1081
  signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
953
1082
  ): Promise<boolean> {
@@ -958,58 +1087,6 @@ export class SequencerPublisher {
958
1087
 
959
1088
  for (const action of actions) {
960
1089
  switch (action.type) {
961
- case 'vote-empire-payload': {
962
- if (this.slashingProposerContract?.type !== 'empire') {
963
- this.log.error('Cannot vote for empire payload on non-empire slashing contract');
964
- break;
965
- }
966
- this.log.debug(`Enqueuing slashing vote for payload ${action.payload} at slot ${slotNumber}`, {
967
- signerAddress,
968
- });
969
- await this.enqueueCastSignalHelper(
970
- slotNumber,
971
- timestamp,
972
- 'empire-slashing-signal',
973
- action.payload,
974
- this.slashingProposerContract,
975
- signerAddress,
976
- signer,
977
- );
978
- break;
979
- }
980
-
981
- case 'create-empire-payload': {
982
- this.log.debug(`Enqueuing slashing create payload at slot ${slotNumber}`, { slotNumber, signerAddress });
983
- const request = this.slashFactoryContract.buildCreatePayloadRequest(action.data);
984
- await this.simulateAndEnqueueRequest(
985
- 'create-empire-payload',
986
- request,
987
- (receipt: TransactionReceipt) =>
988
- !!this.slashFactoryContract.tryExtractSlashPayloadCreatedEvent(receipt.logs),
989
- slotNumber,
990
- timestamp,
991
- );
992
- break;
993
- }
994
-
995
- case 'execute-empire-payload': {
996
- this.log.debug(`Enqueuing slashing execute payload at slot ${slotNumber}`, { slotNumber, signerAddress });
997
- if (this.slashingProposerContract?.type !== 'empire') {
998
- this.log.error('Cannot execute slashing payload on non-empire slashing contract');
999
- return false;
1000
- }
1001
- const empireSlashingProposer = this.slashingProposerContract as EmpireSlashingProposerContract;
1002
- const request = empireSlashingProposer.buildExecuteRoundRequest(action.round);
1003
- await this.simulateAndEnqueueRequest(
1004
- 'execute-empire-payload',
1005
- request,
1006
- (receipt: TransactionReceipt) => !!empireSlashingProposer.tryExtractPayloadSubmittedEvent(receipt.logs),
1007
- slotNumber,
1008
- timestamp,
1009
- );
1010
- break;
1011
- }
1012
-
1013
1090
  case 'vote-offenses': {
1014
1091
  this.log.debug(`Enqueuing slashing vote for ${action.votes.length} votes at slot ${slotNumber}`, {
1015
1092
  slotNumber,
@@ -1017,19 +1094,17 @@ export class SequencerPublisher {
1017
1094
  votesCount: action.votes.length,
1018
1095
  signerAddress,
1019
1096
  });
1020
- if (this.slashingProposerContract?.type !== 'tally') {
1021
- 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');
1022
1099
  return false;
1023
1100
  }
1024
- const tallySlashingProposer = this.slashingProposerContract as TallySlashingProposerContract;
1025
1101
  const votes = bufferToHex(encodeSlashConsensusVotes(action.votes));
1026
- const request = await tallySlashingProposer.buildVoteRequestFromSigner(votes, slotNumber, signer);
1102
+ const request = await this.slashingProposerContract.buildVoteRequestFromSigner(votes, slotNumber, signer);
1027
1103
  await this.simulateAndEnqueueRequest(
1028
1104
  'vote-offenses',
1029
1105
  request,
1030
- (receipt: TransactionReceipt) => !!tallySlashingProposer.tryExtractVoteCastEvent(receipt.logs),
1106
+ (receipt: TransactionReceipt) => !!this.slashingProposerContract!.tryExtractVoteCastEvent(receipt.logs),
1031
1107
  slotNumber,
1032
- timestamp,
1033
1108
  );
1034
1109
  break;
1035
1110
  }
@@ -1040,18 +1115,20 @@ export class SequencerPublisher {
1040
1115
  round: action.round,
1041
1116
  signerAddress,
1042
1117
  });
1043
- if (this.slashingProposerContract?.type !== 'tally') {
1044
- 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');
1045
1120
  return false;
1046
1121
  }
1047
- const tallySlashingProposer = this.slashingProposerContract as TallySlashingProposerContract;
1048
- const request = tallySlashingProposer.buildExecuteRoundRequest(action.round, action.committees);
1122
+ const executeRequest = this.slashingProposerContract.buildExecuteRoundRequest(
1123
+ action.round,
1124
+ action.committees,
1125
+ );
1049
1126
  await this.simulateAndEnqueueRequest(
1050
1127
  'execute-slash',
1051
- request,
1052
- (receipt: TransactionReceipt) => !!tallySlashingProposer.tryExtractRoundExecutedEvent(receipt.logs),
1128
+ executeRequest,
1129
+ (receipt: TransactionReceipt) =>
1130
+ !!this.slashingProposerContract!.tryExtractRoundExecutedEvent(receipt.logs),
1053
1131
  slotNumber,
1054
- timestamp,
1055
1132
  );
1056
1133
  break;
1057
1134
  }
@@ -1071,7 +1148,7 @@ export class SequencerPublisher {
1071
1148
  checkpoint: Checkpoint,
1072
1149
  attestationsAndSigners: CommitteeAttestationsAndSigners,
1073
1150
  attestationsAndSignersSignature: Signature,
1074
- opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
1151
+ opts: EnqueueProposeCheckpointOpts = {},
1075
1152
  ): Promise<void> {
1076
1153
  const checkpointHeader = checkpoint.header;
1077
1154
 
@@ -1087,7 +1164,9 @@ export class SequencerPublisher {
1087
1164
  feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
1088
1165
  };
1089
1166
 
1090
- let ts: bigint;
1167
+ const simulationOverridesPlan = SimulationOverridesBuilder.from(opts.simulationOverridesPlan)
1168
+ .withoutBlobCheck()
1169
+ .build();
1091
1170
 
1092
1171
  try {
1093
1172
  // @note This will make sure that we are passing the checks for our header ASSUMING that the data is also made available
@@ -1095,23 +1174,47 @@ export class SequencerPublisher {
1095
1174
  // By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which
1096
1175
  // make time consistency checks break.
1097
1176
  // TODO(palla): Check whether we're validating twice, once here and once within addProposeTx, since we call simulateProposeTx in both places.
1098
- ts = await this.validateCheckpointForSubmission(
1177
+ await this.validateCheckpointForSubmission(
1099
1178
  checkpoint,
1100
1179
  attestationsAndSigners,
1101
1180
  attestationsAndSignersSignature,
1102
- opts,
1181
+ simulationOverridesPlan,
1103
1182
  );
1104
1183
  } catch (err: any) {
1105
1184
  this.log.error(`Checkpoint validation failed. ${err instanceof Error ? err.message : 'No error message'}`, err, {
1106
1185
  ...checkpoint.getStats(),
1107
1186
  slotNumber: checkpoint.header.slotNumber,
1108
- forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
1187
+ simulationOverridesPlan,
1109
1188
  });
1110
1189
  throw err;
1111
1190
  }
1112
1191
 
1113
- this.log.verbose(`Enqueuing checkpoint propose transaction`, { ...checkpoint.toCheckpointInfo(), ...opts });
1114
- 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
+ );
1115
1218
  }
1116
1219
 
1117
1220
  public enqueueInvalidateCheckpoint(
@@ -1154,8 +1257,8 @@ export class SequencerPublisher {
1154
1257
  request: L1TxRequest,
1155
1258
  checkSuccess: (receipt: TransactionReceipt) => boolean | undefined,
1156
1259
  slotNumber: SlotNumber,
1157
- timestamp: bigint,
1158
1260
  ) {
1261
+ const timestamp = this.getSimulationTimestamp(slotNumber);
1159
1262
  const logData = { slotNumber, timestamp, gasLimit: undefined as bigint | undefined };
1160
1263
  if (this.lastActions[action] && this.lastActions[action] === slotNumber) {
1161
1264
  this.log.debug(`Skipping duplicate action ${action} for slot ${slotNumber}`);
@@ -1171,8 +1274,9 @@ export class SequencerPublisher {
1171
1274
 
1172
1275
  let gasUsed: bigint;
1173
1276
  const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
1277
+
1174
1278
  try {
1175
- ({ 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));
1176
1280
  this.log.verbose(`Simulation for ${action} succeeded`, { ...logData, request, gasUsed });
1177
1281
  } catch (err) {
1178
1282
  const viemError = formatViemError(err, simulateAbi);
@@ -1230,6 +1334,7 @@ export class SequencerPublisher {
1230
1334
  */
1231
1335
  public interrupt() {
1232
1336
  this.interrupted = true;
1337
+ this.interruptibleSleep.interrupt();
1233
1338
  this.l1TxUtils.interrupt();
1234
1339
  }
1235
1340
 
@@ -1239,11 +1344,7 @@ export class SequencerPublisher {
1239
1344
  this.l1TxUtils.restart();
1240
1345
  }
1241
1346
 
1242
- private async prepareProposeTx(
1243
- encodedData: L1ProcessArgs,
1244
- timestamp: bigint,
1245
- options: { forcePendingCheckpointNumber?: CheckpointNumber },
1246
- ) {
1347
+ private async prepareProposeTx(encodedData: L1ProcessArgs, simulationOverridesPlan?: SimulationOverridesPlan) {
1247
1348
  const kzg = Blob.getViemKzgInstance();
1248
1349
  const blobInput = getPrefixedEthBlobCommitments(encodedData.blobs);
1249
1350
  this.log.debug('Validating blob input', { blobInput });
@@ -1314,7 +1415,7 @@ export class SequencerPublisher {
1314
1415
  blobInput,
1315
1416
  ] as const;
1316
1417
 
1317
- const { rollupData, simulationResult } = await this.simulateProposeTx(args, timestamp, options);
1418
+ const { rollupData, simulationResult } = await this.simulateProposeTx(args, simulationOverridesPlan);
1318
1419
 
1319
1420
  return { args, blobEvaluationGas, rollupData, simulationResult };
1320
1421
  }
@@ -1322,7 +1423,6 @@ export class SequencerPublisher {
1322
1423
  /**
1323
1424
  * Simulates the propose tx with eth_simulateV1
1324
1425
  * @param args - The propose tx args
1325
- * @param timestamp - The timestamp to simulate proposal at
1326
1426
  * @returns The simulation result
1327
1427
  */
1328
1428
  private async simulateProposeTx(
@@ -1339,8 +1439,7 @@ export class SequencerPublisher {
1339
1439
  ViemSignature,
1340
1440
  `0x${string}`,
1341
1441
  ],
1342
- timestamp: bigint,
1343
- options: { forcePendingCheckpointNumber?: CheckpointNumber },
1442
+ simulationOverridesPlan?: SimulationOverridesPlan,
1344
1443
  ) {
1345
1444
  const rollupData = encodeFunctionData({
1346
1445
  abi: RollupAbi,
@@ -1348,23 +1447,7 @@ export class SequencerPublisher {
1348
1447
  args,
1349
1448
  });
1350
1449
 
1351
- // override the pending checkpoint number if requested
1352
- const forcePendingCheckpointNumberStateDiff = (
1353
- options.forcePendingCheckpointNumber !== undefined
1354
- ? await this.rollupContract.makePendingCheckpointNumberOverride(options.forcePendingCheckpointNumber)
1355
- : []
1356
- ).flatMap(override => override.stateDiff ?? []);
1357
-
1358
- const stateOverrides: StateOverride = [
1359
- {
1360
- address: this.rollupContract.address,
1361
- // @note we override checkBlob to false since blobs are not part simulate()
1362
- stateDiff: [
1363
- { slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true), value: toPaddedHex(0n, true) },
1364
- ...forcePendingCheckpointNumberStateDiff,
1365
- ],
1366
- },
1367
- ];
1450
+ const stateOverrides = await buildSimulationOverridesStateOverride(this.rollupContract, simulationOverridesPlan);
1368
1451
  // In fisherman mode, simulate as the proposer but with sufficient balance
1369
1452
  if (this.proposerAddressForSimulation) {
1370
1453
  stateOverrides.push({
@@ -1374,6 +1457,7 @@ export class SequencerPublisher {
1374
1457
  }
1375
1458
 
1376
1459
  const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1460
+ const simTs = this.getSimulationTimestamp(SlotNumber.fromBigInt(args[0].header.slotNumber));
1377
1461
 
1378
1462
  const simulationResult = await this.l1TxUtils
1379
1463
  .simulate(
@@ -1384,8 +1468,7 @@ export class SequencerPublisher {
1384
1468
  ...(this.proposerAddressForSimulation && { from: this.proposerAddressForSimulation.toString() }),
1385
1469
  },
1386
1470
  {
1387
- // @note we add 1n to the timestamp because geth implementation doesn't like simulation timestamp to be equal to the current block timestamp
1388
- time: timestamp + 1n,
1471
+ time: simTs,
1389
1472
  // @note reth should have a 30m gas limit per block but throws errors that this tx is beyond limit so we increase here
1390
1473
  gasLimit: MAX_L1_TX_LIMIT * 2n,
1391
1474
  },
@@ -1407,7 +1490,7 @@ export class SequencerPublisher {
1407
1490
  logs: [],
1408
1491
  };
1409
1492
  }
1410
- this.log.error(`Failed to simulate propose tx`, viemError);
1493
+ this.log.error(`Failed to simulate propose tx`, viemError, { simulationTimestamp: simTs });
1411
1494
  this.backupFailedTx({
1412
1495
  id: keccak256(rollupData),
1413
1496
  failureType: 'simulation',
@@ -1429,16 +1512,15 @@ export class SequencerPublisher {
1429
1512
  private async addProposeTx(
1430
1513
  checkpoint: Checkpoint,
1431
1514
  encodedData: L1ProcessArgs,
1432
- opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
1433
- timestamp: bigint,
1515
+ opts: EnqueueProposeCheckpointOpts = {},
1516
+ preCheck?: () => Promise<void>,
1434
1517
  ): Promise<void> {
1435
1518
  const slot = checkpoint.header.slotNumber;
1436
1519
  const timer = new Timer();
1437
1520
  const kzg = Blob.getViemKzgInstance();
1438
1521
  const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(
1439
1522
  encodedData,
1440
- timestamp,
1441
- opts,
1523
+ opts.simulationOverridesPlan,
1442
1524
  );
1443
1525
  const startBlock = await this.l1TxUtils.getBlockNumber();
1444
1526
  const gasLimit = this.l1TxUtils.bumpGasLimit(
@@ -1462,7 +1544,8 @@ export class SequencerPublisher {
1462
1544
  data: rollupData,
1463
1545
  },
1464
1546
  lastValidL2Slot: checkpoint.header.slotNumber,
1465
- gasConfig: { ...opts, gasLimit },
1547
+ gasConfig: { txTimeoutAt: opts.txTimeoutAt, gasLimit },
1548
+ preCheck,
1466
1549
  blobConfig: {
1467
1550
  blobs: encodedData.blobs.map(b => b.data),
1468
1551
  kzg,
@@ -1515,4 +1598,17 @@ export class SequencerPublisher {
1515
1598
  },
1516
1599
  });
1517
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
+ }
1518
1614
  }