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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/dest/client/sequencer-client.d.ts +2 -1
  2. package/dest/client/sequencer-client.d.ts.map +1 -1
  3. package/dest/client/sequencer-client.js +8 -8
  4. package/dest/config.d.ts +3 -3
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +10 -12
  7. package/dest/global_variable_builder/fee_predictor.d.ts +37 -0
  8. package/dest/global_variable_builder/fee_predictor.d.ts.map +1 -0
  9. package/dest/global_variable_builder/fee_predictor.js +128 -0
  10. package/dest/global_variable_builder/fee_provider.d.ts +21 -0
  11. package/dest/global_variable_builder/fee_provider.d.ts.map +1 -0
  12. package/dest/global_variable_builder/fee_provider.js +58 -0
  13. package/dest/global_variable_builder/global_builder.d.ts +3 -8
  14. package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
  15. package/dest/global_variable_builder/global_builder.js +4 -39
  16. package/dest/global_variable_builder/index.d.ts +3 -1
  17. package/dest/global_variable_builder/index.d.ts.map +1 -1
  18. package/dest/global_variable_builder/index.js +2 -0
  19. package/dest/publisher/config.d.ts +17 -1
  20. package/dest/publisher/config.d.ts.map +1 -1
  21. package/dest/publisher/config.js +25 -5
  22. package/dest/publisher/index.d.ts +2 -1
  23. package/dest/publisher/index.d.ts.map +1 -1
  24. package/dest/publisher/l1_tx_failed_store/factory.d.ts +11 -0
  25. package/dest/publisher/l1_tx_failed_store/factory.d.ts.map +1 -0
  26. package/dest/publisher/l1_tx_failed_store/factory.js +22 -0
  27. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts +58 -0
  28. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -0
  29. package/dest/publisher/l1_tx_failed_store/failed_tx_store.js +1 -0
  30. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts +15 -0
  31. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts.map +1 -0
  32. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.js +34 -0
  33. package/dest/publisher/l1_tx_failed_store/index.d.ts +4 -0
  34. package/dest/publisher/l1_tx_failed_store/index.d.ts.map +1 -0
  35. package/dest/publisher/l1_tx_failed_store/index.js +2 -0
  36. package/dest/publisher/sequencer-publisher-factory.d.ts +3 -5
  37. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  38. package/dest/publisher/sequencer-publisher-factory.js +16 -3
  39. package/dest/publisher/sequencer-publisher.d.ts +52 -41
  40. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  41. package/dest/publisher/sequencer-publisher.js +345 -101
  42. package/dest/sequencer/chain_state_overrides.d.ts +25 -0
  43. package/dest/sequencer/chain_state_overrides.d.ts.map +1 -0
  44. package/dest/sequencer/chain_state_overrides.js +39 -0
  45. package/dest/sequencer/checkpoint_proposal_job.d.ts +38 -8
  46. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  47. package/dest/sequencer/checkpoint_proposal_job.js +427 -171
  48. package/dest/sequencer/checkpoint_proposal_job_metrics.d.ts +34 -0
  49. package/dest/sequencer/checkpoint_proposal_job_metrics.d.ts.map +1 -0
  50. package/dest/sequencer/checkpoint_proposal_job_metrics.js +72 -0
  51. package/dest/sequencer/events.d.ts +7 -1
  52. package/dest/sequencer/events.d.ts.map +1 -1
  53. package/dest/sequencer/metrics.d.ts +11 -10
  54. package/dest/sequencer/metrics.d.ts.map +1 -1
  55. package/dest/sequencer/metrics.js +38 -20
  56. package/dest/sequencer/sequencer.d.ts +17 -5
  57. package/dest/sequencer/sequencer.d.ts.map +1 -1
  58. package/dest/sequencer/sequencer.js +120 -58
  59. package/dest/sequencer/timetable.d.ts +14 -1
  60. package/dest/sequencer/timetable.d.ts.map +1 -1
  61. package/dest/sequencer/timetable.js +45 -36
  62. package/dest/test/mock_checkpoint_builder.d.ts +1 -1
  63. package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
  64. package/dest/test/mock_checkpoint_builder.js +0 -2
  65. package/package.json +27 -28
  66. package/src/client/sequencer-client.ts +8 -14
  67. package/src/config.ts +12 -11
  68. package/src/global_variable_builder/README.md +44 -0
  69. package/src/global_variable_builder/fee_predictor.ts +172 -0
  70. package/src/global_variable_builder/fee_provider.ts +75 -0
  71. package/src/global_variable_builder/global_builder.ts +8 -43
  72. package/src/global_variable_builder/index.ts +2 -0
  73. package/src/publisher/config.ts +47 -4
  74. package/src/publisher/index.ts +3 -0
  75. package/src/publisher/l1_tx_failed_store/factory.ts +32 -0
  76. package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +57 -0
  77. package/src/publisher/l1_tx_failed_store/file_store_failed_tx_store.ts +46 -0
  78. package/src/publisher/l1_tx_failed_store/index.ts +3 -0
  79. package/src/publisher/sequencer-publisher-factory.ts +18 -6
  80. package/src/publisher/sequencer-publisher.ts +389 -140
  81. package/src/sequencer/README.md +83 -13
  82. package/src/sequencer/chain_state_overrides.ts +87 -0
  83. package/src/sequencer/checkpoint_proposal_job.ts +517 -180
  84. package/src/sequencer/checkpoint_proposal_job_metrics.ts +128 -0
  85. package/src/sequencer/events.ts +6 -1
  86. package/src/sequencer/metrics.ts +49 -25
  87. package/src/sequencer/sequencer.ts +177 -63
  88. package/src/sequencer/timetable.ts +57 -45
  89. package/src/test/mock_checkpoint_builder.ts +0 -2
@@ -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,32 +28,55 @@ 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';
33
34
  import type { Fr } from '@aztec/foundation/curves/bn254';
35
+ import { TimeoutError } from '@aztec/foundation/error';
34
36
  import { EthAddress } from '@aztec/foundation/eth-address';
35
37
  import { Signature, type ViemSignature } from '@aztec/foundation/eth-signature';
36
38
  import { type Logger, createLogger } from '@aztec/foundation/log';
37
39
  import { makeBackoff, retry } from '@aztec/foundation/retry';
40
+ import { InterruptibleSleep } from '@aztec/foundation/sleep';
38
41
  import { bufferToHex } from '@aztec/foundation/string';
39
- import { DateProvider, Timer } from '@aztec/foundation/timer';
42
+ import { type DateProvider, Timer } from '@aztec/foundation/timer';
40
43
  import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
41
44
  import { type ProposerSlashAction, encodeSlashConsensusVotes } from '@aztec/slasher';
42
45
  import { CommitteeAttestationsAndSigners, type ValidateCheckpointResult } from '@aztec/stdlib/block';
43
46
  import type { Checkpoint } from '@aztec/stdlib/checkpoint';
44
47
  import { getLastL1SlotTimestampForL2Slot, getNextL1SlotTimestamp } from '@aztec/stdlib/epoch-helpers';
45
- import { SlashFactoryContract } from '@aztec/stdlib/l1-contracts';
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
- import { type StateOverride, type TransactionReceipt, type TypedDataDefinition, encodeFunctionData, toHex } from 'viem';
52
+ import {
53
+ type Hex,
54
+ type TransactionReceipt,
55
+ type TypedDataDefinition,
56
+ encodeFunctionData,
57
+ keccak256,
58
+ multicall3Abi,
59
+ toHex,
60
+ } from 'viem';
51
61
 
52
62
  import type { SequencerPublisherConfig } from './config.js';
63
+ import { type FailedL1Tx, type L1TxFailedStore, createL1TxFailedStore } from './l1_tx_failed_store/index.js';
53
64
  import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
54
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
+
55
80
  /** Arguments to the process method of the rollup contract */
56
81
  type L1ProcessArgs = {
57
82
  /** The L2 block header. */
@@ -73,16 +98,13 @@ export const Actions = [
73
98
  'invalidate-by-insufficient-attestations',
74
99
  'propose',
75
100
  'governance-signal',
76
- 'empire-slashing-signal',
77
- 'create-empire-payload',
78
- 'execute-empire-payload',
79
101
  'vote-offenses',
80
102
  'execute-slash',
81
103
  ] as const;
82
104
 
83
105
  export type Action = (typeof Actions)[number];
84
106
 
85
- type GovernanceSignalAction = Extract<Action, 'governance-signal' | 'empire-slashing-signal'>;
107
+ type GovernanceSignalAction = Extract<Action, 'governance-signal'>;
86
108
 
87
109
  // Sorting for actions such that invalidations go before proposals, and proposals go before votes
88
110
  export const compareActions = (a: Action, b: Action) => Actions.indexOf(a) - Actions.indexOf(b);
@@ -93,6 +115,13 @@ export type InvalidateCheckpointRequest = {
93
115
  gasUsed: bigint;
94
116
  checkpointNumber: CheckpointNumber;
95
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;
96
125
  };
97
126
 
98
127
  interface RequestWithExpiry {
@@ -101,6 +130,8 @@ interface RequestWithExpiry {
101
130
  lastValidL2Slot: SlotNumber;
102
131
  gasConfig?: Pick<L1TxConfig, 'txTimeoutAt' | 'gasLimit'>;
103
132
  blobConfig?: L1BlobInputs;
133
+ /** Optional pre-send validation. If it rejects, the request is discarded. */
134
+ preCheck?: () => Promise<void>;
104
135
  checkSuccess: (
105
136
  request: L1TxRequest,
106
137
  result?: { receipt: TransactionReceipt; stats?: TransactionStats; errorMsg?: string },
@@ -111,6 +142,7 @@ export class SequencerPublisher {
111
142
  private interrupted = false;
112
143
  private metrics: SequencerPublisherMetrics;
113
144
  public epochCache: EpochCache;
145
+ private failedTxStore?: Promise<L1TxFailedStore | undefined>;
114
146
 
115
147
  protected governanceLog = createLogger('sequencer:publisher:governance');
116
148
  protected slashingLog = createLogger('sequencer:publisher:slashing');
@@ -123,19 +155,27 @@ export class SequencerPublisher {
123
155
  protected log: Logger;
124
156
  protected ethereumSlotDuration: bigint;
125
157
  protected aztecSlotDuration: bigint;
126
- private dateProvider: DateProvider;
158
+
159
+ /** Date provider for wall-clock time. */
160
+ private readonly dateProvider: DateProvider;
127
161
 
128
162
  private blobClient: BlobClientInterface;
129
163
 
130
164
  /** Address to use for simulations in fisherman mode (actual proposer's address) */
131
165
  private proposerAddressForSimulation?: EthAddress;
132
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
+
133
170
  /** L1 fee analyzer for fisherman mode */
134
171
  private l1FeeAnalyzer?: L1FeeAnalyzer;
135
172
 
136
173
  /** Fee asset price oracle for computing price modifiers from Uniswap V4 */
137
174
  private feeAssetPriceOracle: FeeAssetPriceOracle;
138
175
 
176
+ /** Interruptible sleep used by sendRequestsAt to wait until a target timestamp. */
177
+ private readonly interruptibleSleep = new InterruptibleSleep();
178
+
139
179
  // A CALL to a cold address is 2700 gas
140
180
  public static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
141
181
 
@@ -145,29 +185,28 @@ export class SequencerPublisher {
145
185
  public l1TxUtils: L1TxUtils;
146
186
  public rollupContract: RollupContract;
147
187
  public govProposerContract: GovernanceProposerContract;
148
- public slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
149
- public slashFactoryContract: SlashFactoryContract;
188
+ public slashingProposerContract: SlashingProposerContract | undefined;
150
189
 
151
190
  public readonly tracer: Tracer;
152
191
 
153
192
  protected requests: RequestWithExpiry[] = [];
154
193
 
155
194
  constructor(
156
- private config: Pick<SequencerPublisherConfig, 'fishermanMode'> &
195
+ private config: Pick<SequencerPublisherConfig, 'fishermanMode' | 'l1TxFailedStore'> &
157
196
  Pick<L1ContractsConfig, 'ethereumSlotDuration' | 'aztecSlotDuration'> & { l1ChainId: number },
158
197
  deps: {
159
198
  telemetry?: TelemetryClient;
160
199
  blobClient: BlobClientInterface;
161
200
  l1TxUtils: L1TxUtils;
162
201
  rollupContract: RollupContract;
163
- slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
202
+ slashingProposerContract: SlashingProposerContract | undefined;
164
203
  governanceProposerContract: GovernanceProposerContract;
165
- slashFactoryContract: SlashFactoryContract;
166
204
  epochCache: EpochCache;
167
205
  dateProvider: DateProvider;
168
206
  metrics: SequencerPublisherMetrics;
169
207
  lastActions: Partial<Record<Action, SlotNumber>>;
170
208
  log?: Logger;
209
+ getNextPublisher?: (excludeAddresses: EthAddress[]) => Promise<L1TxUtils | undefined>;
171
210
  },
172
211
  ) {
173
212
  this.log = deps.log ?? createLogger('sequencer:publisher');
@@ -178,11 +217,13 @@ export class SequencerPublisher {
178
217
  this.lastActions = deps.lastActions;
179
218
 
180
219
  this.blobClient = deps.blobClient;
220
+ this.dateProvider = deps.dateProvider;
181
221
 
182
222
  const telemetry = deps.telemetry ?? getTelemetryClient();
183
223
  this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
184
224
  this.tracer = telemetry.getTracer('SequencerPublisher');
185
225
  this.l1TxUtils = deps.l1TxUtils;
226
+ this.getNextPublisher = deps.getNextPublisher;
186
227
 
187
228
  this.rollupContract = deps.rollupContract;
188
229
 
@@ -194,8 +235,6 @@ export class SequencerPublisher {
194
235
  const newSlashingProposer = await this.rollupContract.getSlashingProposer();
195
236
  this.slashingProposerContract = newSlashingProposer;
196
237
  });
197
- this.slashFactoryContract = deps.slashFactoryContract;
198
-
199
238
  // Initialize L1 fee analyzer for fisherman mode
200
239
  if (config.fishermanMode) {
201
240
  this.l1FeeAnalyzer = new L1FeeAnalyzer(
@@ -211,6 +250,31 @@ export class SequencerPublisher {
211
250
  this.rollupContract,
212
251
  createLogger('sequencer:publisher:price-oracle'),
213
252
  );
253
+
254
+ // Initialize failed L1 tx store (optional, for test networks)
255
+ this.failedTxStore = createL1TxFailedStore(config.l1TxFailedStore, this.log);
256
+ }
257
+
258
+ /**
259
+ * Backs up a failed L1 transaction to the configured store for debugging.
260
+ * Does nothing if no store is configured.
261
+ */
262
+ private backupFailedTx(failedTx: Omit<FailedL1Tx, 'timestamp'>): void {
263
+ if (!this.failedTxStore) {
264
+ return;
265
+ }
266
+
267
+ const tx: FailedL1Tx = {
268
+ ...failedTx,
269
+ timestamp: Date.now(),
270
+ };
271
+
272
+ // Fire and forget - don't block on backup
273
+ void this.failedTxStore
274
+ .then(store => store?.saveFailedTx(tx))
275
+ .catch(err => {
276
+ this.log.warn(`Failed to backup failed L1 tx to store`, err);
277
+ });
214
278
  }
215
279
 
216
280
  public getRollupContract(): RollupContract {
@@ -249,7 +313,7 @@ export class SequencerPublisher {
249
313
  }
250
314
 
251
315
  public getCurrentL2Slot(): SlotNumber {
252
- return this.epochCache.getEpochAndSlotNow().slot;
316
+ return this.epochCache.getSlotNow();
253
317
  }
254
318
 
255
319
  /**
@@ -327,9 +391,10 @@ export class SequencerPublisher {
327
391
  * - undefined if no valid requests are found OR the tx failed to send.
328
392
  */
329
393
  @trackSpan('SequencerPublisher.sendRequests')
330
- public async sendRequests() {
394
+ public async sendRequests(): Promise<SendRequestsResult | undefined> {
331
395
  const requestsToProcess = [...this.requests];
332
396
  this.requests = [];
397
+
333
398
  if (this.interrupted || requestsToProcess.length === 0) {
334
399
  return undefined;
335
400
  }
@@ -392,19 +457,36 @@ export class SequencerPublisher {
392
457
  validRequests.sort((a, b) => compareActions(a.action, b.action));
393
458
 
394
459
  try {
460
+ // Capture context for failed tx backup before sending
461
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
462
+ const multicallData = encodeFunctionData({
463
+ abi: multicall3Abi,
464
+ functionName: 'aggregate3',
465
+ args: [
466
+ validRequests.map(r => ({
467
+ target: r.request.to!,
468
+ callData: r.request.data!,
469
+ allowFailure: true,
470
+ })),
471
+ ],
472
+ });
473
+ const blobDataHex = blobConfig?.blobs?.map(b => toHex(b)) as Hex[] | undefined;
474
+
475
+ const txContext = { multicallData, blobData: blobDataHex, l1BlockNumber };
476
+
395
477
  this.log.debug('Forwarding transactions', {
396
478
  validRequests: validRequests.map(request => request.action),
397
479
  txConfig,
398
480
  });
399
- const result = await Multicall3.forward(
400
- validRequests.map(request => request.request),
401
- this.l1TxUtils,
402
- txConfig,
403
- blobConfig,
404
- this.rollupContract.address,
405
- this.log,
481
+ const result = await this.forwardWithPublisherRotation(validRequests, txConfig, blobConfig);
482
+ if (result === undefined) {
483
+ return undefined;
484
+ }
485
+ const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(
486
+ validRequests,
487
+ result,
488
+ txContext,
406
489
  );
407
- const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(validRequests, result);
408
490
  return { result, expiredActions, sentActions: validActions, successfulActions, failedActions };
409
491
  } catch (err) {
410
492
  const viemError = formatViemError(err);
@@ -422,13 +504,115 @@ export class SequencerPublisher {
422
504
  }
423
505
  }
424
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
+
425
595
  private callbackBundledTransactions(
426
596
  requests: RequestWithExpiry[],
427
- result?: { receipt: TransactionReceipt } | FormattedViemError,
597
+ result: { receipt: TransactionReceipt; errorMsg?: string } | FormattedViemError | undefined,
598
+ txContext: { multicallData: Hex; blobData?: Hex[]; l1BlockNumber: bigint },
428
599
  ) {
429
600
  const actionsListStr = requests.map(r => r.action).join(', ');
430
601
  if (result instanceof FormattedViemError) {
431
602
  this.log.error(`Failed to publish bundled transactions (${actionsListStr})`, result);
603
+ this.backupFailedTx({
604
+ id: keccak256(txContext.multicallData),
605
+ failureType: 'send-error',
606
+ request: { to: MULTI_CALL_3_ADDRESS, data: txContext.multicallData },
607
+ blobData: txContext.blobData,
608
+ l1BlockNumber: txContext.l1BlockNumber.toString(),
609
+ error: { message: result.message, name: result.name },
610
+ context: {
611
+ actions: requests.map(r => r.action),
612
+ requests: requests.map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
613
+ sender: this.getSenderAddress().toString(),
614
+ },
615
+ });
432
616
  return { failedActions: requests.map(r => r.action) };
433
617
  } else {
434
618
  this.log.verbose(`Published bundled transactions (${actionsListStr})`, {
@@ -450,6 +634,30 @@ export class SequencerPublisher {
450
634
  failedActions.push(request.action);
451
635
  }
452
636
  }
637
+ // Single backup for the whole reverted tx
638
+ if (failedActions.length > 0 && result?.receipt?.status === 'reverted') {
639
+ this.backupFailedTx({
640
+ id: result.receipt.transactionHash,
641
+ failureType: 'revert',
642
+ request: { to: MULTI_CALL_3_ADDRESS, data: txContext.multicallData },
643
+ blobData: txContext.blobData,
644
+ l1BlockNumber: result.receipt.blockNumber.toString(),
645
+ receipt: {
646
+ transactionHash: result.receipt.transactionHash,
647
+ blockNumber: result.receipt.blockNumber.toString(),
648
+ gasUsed: (result.receipt.gasUsed ?? 0n).toString(),
649
+ status: 'reverted',
650
+ },
651
+ error: { message: result.errorMsg ?? 'Transaction reverted' },
652
+ context: {
653
+ actions: failedActions,
654
+ requests: requests
655
+ .filter(r => failedActions.includes(r.action))
656
+ .map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
657
+ sender: this.getSenderAddress().toString(),
658
+ },
659
+ });
660
+ }
453
661
  return { successfulActions, failedActions };
454
662
  }
455
663
  }
@@ -459,22 +667,21 @@ export class SequencerPublisher {
459
667
  * @param tipArchive - The archive to check
460
668
  * @returns The slot and block number if it is possible to propose, undefined otherwise
461
669
  */
462
- public canProposeAt(
463
- tipArchive: Fr,
464
- msgSender: EthAddress,
465
- opts: { forcePendingCheckpointNumber?: CheckpointNumber; pipelined?: boolean } = {},
466
- ) {
670
+ public async canProposeAt(tipArchive: Fr, msgSender: EthAddress, simulationOverridesPlan?: SimulationOverridesPlan) {
467
671
  // TODO: #14291 - should loop through multiple keys to check if any of them can propose
468
672
  const ignoredErrors = ['SlotAlreadyInChain', 'InvalidProposer', 'InvalidArchive'];
469
673
 
470
- const pipelined = opts.pipelined ?? false;
674
+ const pipelined = this.epochCache.isProposerPipeliningEnabled();
471
675
  const slotOffset = pipelined ? this.aztecSlotDuration : 0n;
472
676
  const nextL1SlotTs = this.getNextL1SlotTimestamp() + slotOffset;
473
677
 
474
678
  return this.rollupContract
475
- .canProposeAt(tipArchive.toBuffer(), msgSender.toString(), nextL1SlotTs, {
476
- forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
477
- })
679
+ .canProposeAt(
680
+ tipArchive.toBuffer(),
681
+ msgSender.toString(),
682
+ nextL1SlotTs,
683
+ await buildSimulationOverridesStateOverride(this.rollupContract, simulationOverridesPlan),
684
+ )
478
685
  .catch(err => {
479
686
  if (err instanceof FormattedViemError && ignoredErrors.find(e => err.message.includes(e))) {
480
687
  this.log.warn(`Failed canProposeAtTime check with ${ignoredErrors.find(e => err.message.includes(e))}`, {
@@ -486,6 +693,7 @@ export class SequencerPublisher {
486
693
  return undefined;
487
694
  });
488
695
  }
696
+
489
697
  /**
490
698
  * @notice Will simulate `validateHeader` to make sure that the block header is valid
491
699
  * @dev This is a convenience function that can be used by the sequencer to validate a "partial" header.
@@ -495,7 +703,7 @@ export class SequencerPublisher {
495
703
  @trackSpan('SequencerPublisher.validateBlockHeader')
496
704
  public async validateBlockHeader(
497
705
  header: CheckpointHeader,
498
- opts?: { forcePendingCheckpointNumber: CheckpointNumber | undefined },
706
+ simulationOverridesPlan?: SimulationOverridesPlan,
499
707
  ): Promise<void> {
500
708
  const flags = { ignoreDA: true, ignoreSignatures: true };
501
709
 
@@ -510,9 +718,7 @@ export class SequencerPublisher {
510
718
  ] as const;
511
719
 
512
720
  const ts = this.getSimulationTimestamp(header.slotNumber);
513
- const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(
514
- opts?.forcePendingCheckpointNumber,
515
- );
721
+ const stateOverrides = await buildSimulationOverridesStateOverride(this.rollupContract, simulationOverridesPlan);
516
722
  let balance = 0n;
517
723
  if (this.config.fishermanMode) {
518
724
  // In fisherman mode, we can't know where the proposer is publishing from
@@ -565,6 +771,8 @@ export class SequencerPublisher {
565
771
  const request = this.buildInvalidateCheckpointRequest(validationResult);
566
772
  this.log.debug(`Simulating invalidate checkpoint ${checkpointNumber}`, { ...logData, request });
567
773
 
774
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
775
+
568
776
  try {
569
777
  const { gasUsed } = await this.l1TxUtils.simulate(
570
778
  request,
@@ -583,6 +791,7 @@ export class SequencerPublisher {
583
791
  gasUsed,
584
792
  checkpointNumber,
585
793
  forcePendingCheckpointNumber: CheckpointNumber(checkpointNumber - 1),
794
+ lastArchive: validationResult.checkpoint.lastArchive,
586
795
  reason,
587
796
  };
588
797
  } catch (err) {
@@ -595,8 +804,8 @@ export class SequencerPublisher {
595
804
  `Simulation for invalidate checkpoint ${checkpointNumber} failed due to checkpoint not being in pending chain`,
596
805
  { ...logData, request, error: viemError.message },
597
806
  );
598
- const latestPendingCheckpointNumber = await this.rollupContract.getCheckpointNumber();
599
- if (latestPendingCheckpointNumber < checkpointNumber) {
807
+ const latestProposedCheckpointNumber = await this.rollupContract.getCheckpointNumber();
808
+ if (latestProposedCheckpointNumber < checkpointNumber) {
600
809
  this.log.verbose(`Checkpoint ${checkpointNumber} has already been invalidated`, { ...logData });
601
810
  return undefined;
602
811
  } else {
@@ -616,6 +825,18 @@ export class SequencerPublisher {
616
825
 
617
826
  // Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
618
827
  this.log.error(`Simulation for invalidate checkpoint ${checkpointNumber} failed`, viemError, logData);
828
+ this.backupFailedTx({
829
+ id: keccak256(request.data!),
830
+ failureType: 'simulation',
831
+ request: { to: request.to!, data: request.data!, value: request.value?.toString() },
832
+ l1BlockNumber: l1BlockNumber.toString(),
833
+ error: { message: viemError.message, name: viemError.name },
834
+ context: {
835
+ actions: [`invalidate-${reason}`],
836
+ checkpointNumber,
837
+ sender: this.getSenderAddress().toString(),
838
+ },
839
+ });
619
840
  throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, { cause: viemError });
620
841
  }
621
842
  }
@@ -658,7 +879,7 @@ export class SequencerPublisher {
658
879
  checkpoint: Checkpoint,
659
880
  attestationsAndSigners: CommitteeAttestationsAndSigners,
660
881
  attestationsAndSignersSignature: Signature,
661
- options: { forcePendingCheckpointNumber?: CheckpointNumber },
882
+ simulationOverridesPlan?: SimulationOverridesPlan,
662
883
  ): Promise<void> {
663
884
  const blobFields = checkpoint.toBlobFields();
664
885
  const blobs = await getBlobsPerL1Block(blobFields);
@@ -678,7 +899,7 @@ export class SequencerPublisher {
678
899
  blobInput,
679
900
  ] as const;
680
901
 
681
- await this.simulateProposeTx(args, options);
902
+ await this.simulateProposeTx(args, simulationOverridesPlan);
682
903
  }
683
904
 
684
905
  private async enqueueCastSignalHelper(
@@ -760,6 +981,7 @@ export class SequencerPublisher {
760
981
  lastValidL2Slot: slotNumber,
761
982
  });
762
983
 
984
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
763
985
  const timestamp = this.getSimulationTimestamp(slotNumber);
764
986
 
765
987
  try {
@@ -769,6 +991,19 @@ export class SequencerPublisher {
769
991
  const viemError = formatViemError(err);
770
992
  this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, viemError, {
771
993
  simulationTimestamp: timestamp,
994
+ l1BlockNumber,
995
+ });
996
+ this.backupFailedTx({
997
+ id: keccak256(request.data!),
998
+ failureType: 'simulation',
999
+ request: { to: request.to!, data: request.data!, value: request.value?.toString() },
1000
+ l1BlockNumber: l1BlockNumber.toString(),
1001
+ error: { message: viemError.message, name: viemError.name },
1002
+ context: {
1003
+ actions: [action],
1004
+ slot: slotNumber,
1005
+ sender: this.getSenderAddress().toString(),
1006
+ },
772
1007
  });
773
1008
  // Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
774
1009
  }
@@ -852,55 +1087,6 @@ export class SequencerPublisher {
852
1087
 
853
1088
  for (const action of actions) {
854
1089
  switch (action.type) {
855
- case 'vote-empire-payload': {
856
- if (this.slashingProposerContract?.type !== 'empire') {
857
- this.log.error('Cannot vote for empire payload on non-empire slashing contract');
858
- break;
859
- }
860
- this.log.debug(`Enqueuing slashing vote for payload ${action.payload} at slot ${slotNumber}`, {
861
- signerAddress,
862
- });
863
- await this.enqueueCastSignalHelper(
864
- slotNumber,
865
- 'empire-slashing-signal',
866
- action.payload,
867
- this.slashingProposerContract,
868
- signerAddress,
869
- signer,
870
- );
871
- break;
872
- }
873
-
874
- case 'create-empire-payload': {
875
- this.log.debug(`Enqueuing slashing create payload at slot ${slotNumber}`, { slotNumber, signerAddress });
876
- const request = this.slashFactoryContract.buildCreatePayloadRequest(action.data);
877
- await this.simulateAndEnqueueRequest(
878
- 'create-empire-payload',
879
- request,
880
- (receipt: TransactionReceipt) =>
881
- !!this.slashFactoryContract.tryExtractSlashPayloadCreatedEvent(receipt.logs),
882
- slotNumber,
883
- );
884
- break;
885
- }
886
-
887
- case 'execute-empire-payload': {
888
- this.log.debug(`Enqueuing slashing execute payload at slot ${slotNumber}`, { slotNumber, signerAddress });
889
- if (this.slashingProposerContract?.type !== 'empire') {
890
- this.log.error('Cannot execute slashing payload on non-empire slashing contract');
891
- return false;
892
- }
893
- const empireSlashingProposer = this.slashingProposerContract as EmpireSlashingProposerContract;
894
- const request = empireSlashingProposer.buildExecuteRoundRequest(action.round);
895
- await this.simulateAndEnqueueRequest(
896
- 'execute-empire-payload',
897
- request,
898
- (receipt: TransactionReceipt) => !!empireSlashingProposer.tryExtractPayloadSubmittedEvent(receipt.logs),
899
- slotNumber,
900
- );
901
- break;
902
- }
903
-
904
1090
  case 'vote-offenses': {
905
1091
  this.log.debug(`Enqueuing slashing vote for ${action.votes.length} votes at slot ${slotNumber}`, {
906
1092
  slotNumber,
@@ -908,17 +1094,16 @@ export class SequencerPublisher {
908
1094
  votesCount: action.votes.length,
909
1095
  signerAddress,
910
1096
  });
911
- if (this.slashingProposerContract?.type !== 'tally') {
912
- 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');
913
1099
  return false;
914
1100
  }
915
- const tallySlashingProposer = this.slashingProposerContract as TallySlashingProposerContract;
916
1101
  const votes = bufferToHex(encodeSlashConsensusVotes(action.votes));
917
- const request = await tallySlashingProposer.buildVoteRequestFromSigner(votes, slotNumber, signer);
1102
+ const request = await this.slashingProposerContract.buildVoteRequestFromSigner(votes, slotNumber, signer);
918
1103
  await this.simulateAndEnqueueRequest(
919
1104
  'vote-offenses',
920
1105
  request,
921
- (receipt: TransactionReceipt) => !!tallySlashingProposer.tryExtractVoteCastEvent(receipt.logs),
1106
+ (receipt: TransactionReceipt) => !!this.slashingProposerContract!.tryExtractVoteCastEvent(receipt.logs),
922
1107
  slotNumber,
923
1108
  );
924
1109
  break;
@@ -930,16 +1115,19 @@ export class SequencerPublisher {
930
1115
  round: action.round,
931
1116
  signerAddress,
932
1117
  });
933
- if (this.slashingProposerContract?.type !== 'tally') {
934
- 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');
935
1120
  return false;
936
1121
  }
937
- const tallySlashingProposer = this.slashingProposerContract as TallySlashingProposerContract;
938
- const request = tallySlashingProposer.buildExecuteRoundRequest(action.round, action.committees);
1122
+ const executeRequest = this.slashingProposerContract.buildExecuteRoundRequest(
1123
+ action.round,
1124
+ action.committees,
1125
+ );
939
1126
  await this.simulateAndEnqueueRequest(
940
1127
  'execute-slash',
941
- request,
942
- (receipt: TransactionReceipt) => !!tallySlashingProposer.tryExtractRoundExecutedEvent(receipt.logs),
1128
+ executeRequest,
1129
+ (receipt: TransactionReceipt) =>
1130
+ !!this.slashingProposerContract!.tryExtractRoundExecutedEvent(receipt.logs),
943
1131
  slotNumber,
944
1132
  );
945
1133
  break;
@@ -960,7 +1148,7 @@ export class SequencerPublisher {
960
1148
  checkpoint: Checkpoint,
961
1149
  attestationsAndSigners: CommitteeAttestationsAndSigners,
962
1150
  attestationsAndSignersSignature: Signature,
963
- opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
1151
+ opts: EnqueueProposeCheckpointOpts = {},
964
1152
  ): Promise<void> {
965
1153
  const checkpointHeader = checkpoint.header;
966
1154
 
@@ -976,6 +1164,10 @@ export class SequencerPublisher {
976
1164
  feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
977
1165
  };
978
1166
 
1167
+ const simulationOverridesPlan = SimulationOverridesBuilder.from(opts.simulationOverridesPlan)
1168
+ .withoutBlobCheck()
1169
+ .build();
1170
+
979
1171
  try {
980
1172
  // @note This will make sure that we are passing the checks for our header ASSUMING that the data is also made available
981
1173
  // This means that we can avoid the simulation issues in later checks.
@@ -986,19 +1178,43 @@ export class SequencerPublisher {
986
1178
  checkpoint,
987
1179
  attestationsAndSigners,
988
1180
  attestationsAndSignersSignature,
989
- opts,
1181
+ simulationOverridesPlan,
990
1182
  );
991
1183
  } catch (err: any) {
992
1184
  this.log.error(`Checkpoint validation failed. ${err instanceof Error ? err.message : 'No error message'}`, err, {
993
1185
  ...checkpoint.getStats(),
994
1186
  slotNumber: checkpoint.header.slotNumber,
995
- forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
1187
+ simulationOverridesPlan,
996
1188
  });
997
1189
  throw err;
998
1190
  }
999
1191
 
1000
- this.log.verbose(`Enqueuing checkpoint propose transaction`, { ...checkpoint.toCheckpointInfo(), ...opts });
1001
- await this.addProposeTx(checkpoint, proposeTxArgs, opts);
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
+ );
1002
1218
  }
1003
1219
 
1004
1220
  public enqueueInvalidateCheckpoint(
@@ -1054,6 +1270,8 @@ export class SequencerPublisher {
1054
1270
 
1055
1271
  this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
1056
1272
 
1273
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1274
+
1057
1275
  let gasUsed: bigint;
1058
1276
  const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
1059
1277
 
@@ -1064,6 +1282,19 @@ export class SequencerPublisher {
1064
1282
  const viemError = formatViemError(err, simulateAbi);
1065
1283
  this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
1066
1284
 
1285
+ this.backupFailedTx({
1286
+ id: keccak256(request.data!),
1287
+ failureType: 'simulation',
1288
+ request: { to: request.to!, data: request.data!, value: request.value?.toString() },
1289
+ l1BlockNumber: l1BlockNumber.toString(),
1290
+ error: { message: viemError.message, name: viemError.name },
1291
+ context: {
1292
+ actions: [action],
1293
+ slot: slotNumber,
1294
+ sender: this.getSenderAddress().toString(),
1295
+ },
1296
+ });
1297
+
1067
1298
  return false;
1068
1299
  }
1069
1300
 
@@ -1103,6 +1334,7 @@ export class SequencerPublisher {
1103
1334
  */
1104
1335
  public interrupt() {
1105
1336
  this.interrupted = true;
1337
+ this.interruptibleSleep.interrupt();
1106
1338
  this.l1TxUtils.interrupt();
1107
1339
  }
1108
1340
 
@@ -1112,10 +1344,7 @@ export class SequencerPublisher {
1112
1344
  this.l1TxUtils.restart();
1113
1345
  }
1114
1346
 
1115
- private async prepareProposeTx(
1116
- encodedData: L1ProcessArgs,
1117
- options: { forcePendingCheckpointNumber?: CheckpointNumber },
1118
- ) {
1347
+ private async prepareProposeTx(encodedData: L1ProcessArgs, simulationOverridesPlan?: SimulationOverridesPlan) {
1119
1348
  const kzg = Blob.getViemKzgInstance();
1120
1349
  const blobInput = getPrefixedEthBlobCommitments(encodedData.blobs);
1121
1350
  this.log.debug('Validating blob input', { blobInput });
@@ -1146,9 +1375,27 @@ export class SequencerPublisher {
1146
1375
  kzg,
1147
1376
  },
1148
1377
  )
1149
- .catch(err => {
1150
- const { message, metaMessages } = formatViemError(err);
1151
- this.log.error(`Failed to validate blobs`, message, { metaMessages });
1378
+ .catch(async err => {
1379
+ const viemError = formatViemError(err);
1380
+ this.log.error(`Failed to validate blobs`, viemError.message, { metaMessages: viemError.metaMessages });
1381
+ const validateBlobsData = encodeFunctionData({
1382
+ abi: RollupAbi,
1383
+ functionName: 'validateBlobs',
1384
+ args: [blobInput],
1385
+ });
1386
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1387
+ this.backupFailedTx({
1388
+ id: keccak256(validateBlobsData),
1389
+ failureType: 'simulation',
1390
+ request: { to: this.rollupContract.address as Hex, data: validateBlobsData },
1391
+ blobData: encodedData.blobs.map(b => toHex(b.data)) as Hex[],
1392
+ l1BlockNumber: l1BlockNumber.toString(),
1393
+ error: { message: viemError.message, name: viemError.name },
1394
+ context: {
1395
+ actions: ['validate-blobs'],
1396
+ sender: this.getSenderAddress().toString(),
1397
+ },
1398
+ });
1152
1399
  throw new Error('Failed to validate blobs');
1153
1400
  });
1154
1401
  }
@@ -1168,7 +1415,7 @@ export class SequencerPublisher {
1168
1415
  blobInput,
1169
1416
  ] as const;
1170
1417
 
1171
- const { rollupData, simulationResult } = await this.simulateProposeTx(args, options);
1418
+ const { rollupData, simulationResult } = await this.simulateProposeTx(args, simulationOverridesPlan);
1172
1419
 
1173
1420
  return { args, blobEvaluationGas, rollupData, simulationResult };
1174
1421
  }
@@ -1192,7 +1439,7 @@ export class SequencerPublisher {
1192
1439
  ViemSignature,
1193
1440
  `0x${string}`,
1194
1441
  ],
1195
- options: { forcePendingCheckpointNumber?: CheckpointNumber },
1442
+ simulationOverridesPlan?: SimulationOverridesPlan,
1196
1443
  ) {
1197
1444
  const rollupData = encodeFunctionData({
1198
1445
  abi: RollupAbi,
@@ -1200,23 +1447,7 @@ export class SequencerPublisher {
1200
1447
  args,
1201
1448
  });
1202
1449
 
1203
- // override the pending checkpoint number if requested
1204
- const forcePendingCheckpointNumberStateDiff = (
1205
- options.forcePendingCheckpointNumber !== undefined
1206
- ? await this.rollupContract.makePendingCheckpointNumberOverride(options.forcePendingCheckpointNumber)
1207
- : []
1208
- ).flatMap(override => override.stateDiff ?? []);
1209
-
1210
- const stateOverrides: StateOverride = [
1211
- {
1212
- address: this.rollupContract.address,
1213
- // @note we override checkBlob to false since blobs are not part simulate()
1214
- stateDiff: [
1215
- { slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true), value: toPaddedHex(0n, true) },
1216
- ...forcePendingCheckpointNumberStateDiff,
1217
- ],
1218
- },
1219
- ];
1450
+ const stateOverrides = await buildSimulationOverridesStateOverride(this.rollupContract, simulationOverridesPlan);
1220
1451
  // In fisherman mode, simulate as the proposer but with sufficient balance
1221
1452
  if (this.proposerAddressForSimulation) {
1222
1453
  stateOverrides.push({
@@ -1225,6 +1456,7 @@ export class SequencerPublisher {
1225
1456
  });
1226
1457
  }
1227
1458
 
1459
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1228
1460
  const simTs = this.getSimulationTimestamp(SlotNumber.fromBigInt(args[0].header.slotNumber));
1229
1461
 
1230
1462
  const simulationResult = await this.l1TxUtils
@@ -1259,6 +1491,18 @@ export class SequencerPublisher {
1259
1491
  };
1260
1492
  }
1261
1493
  this.log.error(`Failed to simulate propose tx`, viemError, { simulationTimestamp: simTs });
1494
+ this.backupFailedTx({
1495
+ id: keccak256(rollupData),
1496
+ failureType: 'simulation',
1497
+ request: { to: this.rollupContract.address, data: rollupData },
1498
+ l1BlockNumber: l1BlockNumber.toString(),
1499
+ error: { message: viemError.message, name: viemError.name },
1500
+ context: {
1501
+ actions: ['propose'],
1502
+ slot: Number(args[0].header.slotNumber),
1503
+ sender: this.getSenderAddress().toString(),
1504
+ },
1505
+ });
1262
1506
  throw err;
1263
1507
  });
1264
1508
 
@@ -1268,12 +1512,16 @@ export class SequencerPublisher {
1268
1512
  private async addProposeTx(
1269
1513
  checkpoint: Checkpoint,
1270
1514
  encodedData: L1ProcessArgs,
1271
- opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
1515
+ opts: EnqueueProposeCheckpointOpts = {},
1516
+ preCheck?: () => Promise<void>,
1272
1517
  ): Promise<void> {
1273
1518
  const slot = checkpoint.header.slotNumber;
1274
1519
  const timer = new Timer();
1275
1520
  const kzg = Blob.getViemKzgInstance();
1276
- const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(encodedData, opts);
1521
+ const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(
1522
+ encodedData,
1523
+ opts.simulationOverridesPlan,
1524
+ );
1277
1525
  const startBlock = await this.l1TxUtils.getBlockNumber();
1278
1526
  const gasLimit = this.l1TxUtils.bumpGasLimit(
1279
1527
  BigInt(Math.ceil((Number(simulationResult.gasUsed) * 64) / 63)) +
@@ -1296,7 +1544,8 @@ export class SequencerPublisher {
1296
1544
  data: rollupData,
1297
1545
  },
1298
1546
  lastValidL2Slot: checkpoint.header.slotNumber,
1299
- gasConfig: { ...opts, gasLimit },
1547
+ gasConfig: { txTimeoutAt: opts.txTimeoutAt, gasLimit },
1548
+ preCheck,
1300
1549
  blobConfig: {
1301
1550
  blobs: encodedData.blobs.map(b => b.data),
1302
1551
  kzg,