@aztec/sequencer-client 0.0.1-commit.29c6b1a3 → 0.0.1-commit.2c0ee1788

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 (107) hide show
  1. package/dest/client/sequencer-client.d.ts +17 -7
  2. package/dest/client/sequencer-client.d.ts.map +1 -1
  3. package/dest/client/sequencer-client.js +63 -30
  4. package/dest/config.d.ts +27 -7
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +58 -37
  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 -16
  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 +47 -17
  20. package/dest/publisher/config.d.ts.map +1 -1
  21. package/dest/publisher/config.js +121 -42
  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 +11 -5
  37. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  38. package/dest/publisher/sequencer-publisher-factory.js +27 -3
  39. package/dest/publisher/sequencer-publisher-metrics.d.ts +1 -1
  40. package/dest/publisher/sequencer-publisher-metrics.d.ts.map +1 -1
  41. package/dest/publisher/sequencer-publisher-metrics.js +12 -4
  42. package/dest/publisher/sequencer-publisher.d.ts +75 -49
  43. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  44. package/dest/publisher/sequencer-publisher.js +472 -158
  45. package/dest/sequencer/chain_state_overrides.d.ts +25 -0
  46. package/dest/sequencer/chain_state_overrides.d.ts.map +1 -0
  47. package/dest/sequencer/chain_state_overrides.js +39 -0
  48. package/dest/sequencer/checkpoint_proposal_job.d.ts +73 -14
  49. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  50. package/dest/sequencer/checkpoint_proposal_job.js +757 -249
  51. package/dest/sequencer/checkpoint_proposal_job_metrics.d.ts +34 -0
  52. package/dest/sequencer/checkpoint_proposal_job_metrics.d.ts.map +1 -0
  53. package/dest/sequencer/checkpoint_proposal_job_metrics.js +72 -0
  54. package/dest/sequencer/checkpoint_voter.d.ts +1 -2
  55. package/dest/sequencer/checkpoint_voter.d.ts.map +1 -1
  56. package/dest/sequencer/checkpoint_voter.js +2 -5
  57. package/dest/sequencer/events.d.ts +7 -1
  58. package/dest/sequencer/events.d.ts.map +1 -1
  59. package/dest/sequencer/metrics.d.ts +23 -8
  60. package/dest/sequencer/metrics.d.ts.map +1 -1
  61. package/dest/sequencer/metrics.js +142 -36
  62. package/dest/sequencer/sequencer.d.ts +55 -19
  63. package/dest/sequencer/sequencer.d.ts.map +1 -1
  64. package/dest/sequencer/sequencer.js +190 -99
  65. package/dest/sequencer/timetable.d.ts +17 -6
  66. package/dest/sequencer/timetable.d.ts.map +1 -1
  67. package/dest/sequencer/timetable.js +51 -46
  68. package/dest/sequencer/types.d.ts +2 -2
  69. package/dest/sequencer/types.d.ts.map +1 -1
  70. package/dest/test/index.d.ts +3 -5
  71. package/dest/test/index.d.ts.map +1 -1
  72. package/dest/test/mock_checkpoint_builder.d.ts +12 -12
  73. package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
  74. package/dest/test/mock_checkpoint_builder.js +45 -36
  75. package/dest/test/utils.d.ts +3 -3
  76. package/dest/test/utils.d.ts.map +1 -1
  77. package/dest/test/utils.js +10 -8
  78. package/package.json +27 -28
  79. package/src/client/sequencer-client.ts +81 -30
  80. package/src/config.ts +77 -46
  81. package/src/global_variable_builder/README.md +44 -0
  82. package/src/global_variable_builder/fee_predictor.ts +172 -0
  83. package/src/global_variable_builder/fee_provider.ts +75 -0
  84. package/src/global_variable_builder/global_builder.ts +27 -63
  85. package/src/global_variable_builder/index.ts +3 -1
  86. package/src/publisher/config.ts +157 -45
  87. package/src/publisher/index.ts +3 -0
  88. package/src/publisher/l1_tx_failed_store/factory.ts +32 -0
  89. package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +57 -0
  90. package/src/publisher/l1_tx_failed_store/file_store_failed_tx_store.ts +46 -0
  91. package/src/publisher/l1_tx_failed_store/index.ts +3 -0
  92. package/src/publisher/sequencer-publisher-factory.ts +38 -9
  93. package/src/publisher/sequencer-publisher-metrics.ts +7 -3
  94. package/src/publisher/sequencer-publisher.ts +533 -218
  95. package/src/sequencer/README.md +82 -13
  96. package/src/sequencer/chain_state_overrides.ts +87 -0
  97. package/src/sequencer/checkpoint_proposal_job.ts +948 -286
  98. package/src/sequencer/checkpoint_proposal_job_metrics.ts +128 -0
  99. package/src/sequencer/checkpoint_voter.ts +1 -12
  100. package/src/sequencer/events.ts +6 -1
  101. package/src/sequencer/metrics.ts +164 -39
  102. package/src/sequencer/sequencer.ts +267 -114
  103. package/src/sequencer/timetable.ts +70 -57
  104. package/src/sequencer/types.ts +1 -1
  105. package/src/test/index.ts +2 -4
  106. package/src/test/mock_checkpoint_builder.ts +65 -53
  107. package/src/test/utils.ts +31 -10
@@ -3,50 +3,80 @@ 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,
6
+ FeeAssetPriceOracle,
7
7
  type GovernanceProposerContract,
8
8
  type IEmpireBase,
9
9
  MULTI_CALL_3_ADDRESS,
10
10
  Multicall3,
11
11
  RollupContract,
12
- type TallySlashingProposerContract,
12
+ SimulationOverridesBuilder,
13
+ type SimulationOverridesPlan,
14
+ type SlashingProposerContract,
13
15
  type ViemCommitteeAttestations,
14
16
  type ViemHeader,
17
+ buildSimulationOverridesStateOverride,
15
18
  } from '@aztec/ethereum/contracts';
16
19
  import { type L1FeeAnalysisResult, L1FeeAnalyzer } from '@aztec/ethereum/l1-fee-analysis';
17
20
  import {
18
21
  type L1BlobInputs,
19
22
  type L1TxConfig,
20
23
  type L1TxRequest,
24
+ type L1TxUtils,
25
+ MAX_L1_TX_LIMIT,
21
26
  type TransactionStats,
22
27
  WEI_CONST,
23
28
  } from '@aztec/ethereum/l1-tx-utils';
24
- import type { L1TxUtilsWithBlobs } from '@aztec/ethereum/l1-tx-utils-with-blobs';
25
- import { FormattedViemError, formatViemError, tryExtractEvent } from '@aztec/ethereum/utils';
29
+ import { FormattedViemError, formatViemError, mergeAbis, tryExtractEvent } from '@aztec/ethereum/utils';
26
30
  import { sumBigint } from '@aztec/foundation/bigint';
27
- import { toHex as toPaddedHex } from '@aztec/foundation/bigint-buffer';
28
31
  import { CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
32
+ import { trimmedBytesLength } from '@aztec/foundation/buffer';
29
33
  import { pick } from '@aztec/foundation/collection';
30
34
  import type { Fr } from '@aztec/foundation/curves/bn254';
35
+ import { TimeoutError } from '@aztec/foundation/error';
31
36
  import { EthAddress } from '@aztec/foundation/eth-address';
32
37
  import { Signature, type ViemSignature } from '@aztec/foundation/eth-signature';
33
38
  import { type Logger, createLogger } from '@aztec/foundation/log';
39
+ import { makeBackoff, retry } from '@aztec/foundation/retry';
40
+ import { InterruptibleSleep } from '@aztec/foundation/sleep';
34
41
  import { bufferToHex } from '@aztec/foundation/string';
35
- import { DateProvider, Timer } from '@aztec/foundation/timer';
42
+ import { type DateProvider, Timer } from '@aztec/foundation/timer';
36
43
  import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
37
44
  import { type ProposerSlashAction, encodeSlashConsensusVotes } from '@aztec/slasher';
38
45
  import { CommitteeAttestationsAndSigners, type ValidateCheckpointResult } from '@aztec/stdlib/block';
39
46
  import type { Checkpoint } from '@aztec/stdlib/checkpoint';
40
- import { SlashFactoryContract } from '@aztec/stdlib/l1-contracts';
47
+ import { getLastL1SlotTimestampForL2Slot, getNextL1SlotTimestamp } from '@aztec/stdlib/epoch-helpers';
41
48
  import type { CheckpointHeader } from '@aztec/stdlib/rollup';
42
49
  import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
43
50
  import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
44
51
 
45
- import { type StateOverride, type TransactionReceipt, type TypedDataDefinition, encodeFunctionData, toHex } from 'viem';
46
-
47
- import type { PublisherConfig, TxSenderConfig } from './config.js';
52
+ import {
53
+ type Hex,
54
+ type TransactionReceipt,
55
+ type TypedDataDefinition,
56
+ encodeFunctionData,
57
+ keccak256,
58
+ multicall3Abi,
59
+ toHex,
60
+ } from 'viem';
61
+
62
+ import type { SequencerPublisherConfig } from './config.js';
63
+ import { type FailedL1Tx, type L1TxFailedStore, createL1TxFailedStore } from './l1_tx_failed_store/index.js';
48
64
  import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
49
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
+
50
80
  /** Arguments to the process method of the rollup contract */
51
81
  type L1ProcessArgs = {
52
82
  /** The L2 block header. */
@@ -59,6 +89,8 @@ type L1ProcessArgs = {
59
89
  attestationsAndSigners: CommitteeAttestationsAndSigners;
60
90
  /** Attestations and signers signature */
61
91
  attestationsAndSignersSignature: Signature;
92
+ /** The fee asset price modifier in basis points (from oracle) */
93
+ feeAssetPriceModifier: bigint;
62
94
  };
63
95
 
64
96
  export const Actions = [
@@ -66,16 +98,13 @@ export const Actions = [
66
98
  'invalidate-by-insufficient-attestations',
67
99
  'propose',
68
100
  'governance-signal',
69
- 'empire-slashing-signal',
70
- 'create-empire-payload',
71
- 'execute-empire-payload',
72
101
  'vote-offenses',
73
102
  'execute-slash',
74
103
  ] as const;
75
104
 
76
105
  export type Action = (typeof Actions)[number];
77
106
 
78
- type GovernanceSignalAction = Extract<Action, 'governance-signal' | 'empire-slashing-signal'>;
107
+ type GovernanceSignalAction = Extract<Action, 'governance-signal'>;
79
108
 
80
109
  // Sorting for actions such that invalidations go before proposals, and proposals go before votes
81
110
  export const compareActions = (a: Action, b: Action) => Actions.indexOf(a) - Actions.indexOf(b);
@@ -86,6 +115,13 @@ export type InvalidateCheckpointRequest = {
86
115
  gasUsed: bigint;
87
116
  checkpointNumber: CheckpointNumber;
88
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;
89
125
  };
90
126
 
91
127
  interface RequestWithExpiry {
@@ -94,6 +130,8 @@ interface RequestWithExpiry {
94
130
  lastValidL2Slot: SlotNumber;
95
131
  gasConfig?: Pick<L1TxConfig, 'txTimeoutAt' | 'gasLimit'>;
96
132
  blobConfig?: L1BlobInputs;
133
+ /** Optional pre-send validation. If it rejects, the request is discarded. */
134
+ preCheck?: () => Promise<void>;
97
135
  checkSuccess: (
98
136
  request: L1TxRequest,
99
137
  result?: { receipt: TransactionReceipt; stats?: TransactionStats; errorMsg?: string },
@@ -104,6 +142,7 @@ export class SequencerPublisher {
104
142
  private interrupted = false;
105
143
  private metrics: SequencerPublisherMetrics;
106
144
  public epochCache: EpochCache;
145
+ private failedTxStore?: Promise<L1TxFailedStore | undefined>;
107
146
 
108
147
  protected governanceLog = createLogger('sequencer:publisher:governance');
109
148
  protected slashingLog = createLogger('sequencer:publisher:slashing');
@@ -111,21 +150,31 @@ export class SequencerPublisher {
111
150
  protected lastActions: Partial<Record<Action, SlotNumber>> = {};
112
151
 
113
152
  private isPayloadEmptyCache: Map<string, boolean> = new Map<string, boolean>();
153
+ private payloadProposedCache: Set<string> = new Set<string>();
114
154
 
115
155
  protected log: Logger;
116
156
  protected ethereumSlotDuration: bigint;
157
+ protected aztecSlotDuration: bigint;
158
+
159
+ /** Date provider for wall-clock time. */
160
+ private readonly dateProvider: DateProvider;
117
161
 
118
162
  private blobClient: BlobClientInterface;
119
163
 
120
164
  /** Address to use for simulations in fisherman mode (actual proposer's address) */
121
165
  private proposerAddressForSimulation?: EthAddress;
122
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
+
123
170
  /** L1 fee analyzer for fisherman mode */
124
171
  private l1FeeAnalyzer?: L1FeeAnalyzer;
125
- // @note - with blobs, the below estimate seems too large.
126
- // Total used for full block from int_l1_pub e2e test: 1m (of which 86k is 1x blob)
127
- // Total used for emptier block from above test: 429k (of which 84k is 1x blob)
128
- public static PROPOSE_GAS_GUESS: bigint = 12_000_000n;
172
+
173
+ /** Fee asset price oracle for computing price modifiers from Uniswap V4 */
174
+ private feeAssetPriceOracle: FeeAssetPriceOracle;
175
+
176
+ /** Interruptible sleep used by sendRequestsAt to wait until a target timestamp. */
177
+ private readonly interruptibleSleep = new InterruptibleSleep();
129
178
 
130
179
  // A CALL to a cold address is 2700 gas
131
180
  public static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
@@ -133,44 +182,48 @@ export class SequencerPublisher {
133
182
  // Gas report for VotingWithSigTest shows a max gas of 100k, but we've seen it cost 700k+ in testnet
134
183
  public static VOTE_GAS_GUESS: bigint = 800_000n;
135
184
 
136
- public l1TxUtils: L1TxUtilsWithBlobs;
185
+ public l1TxUtils: L1TxUtils;
137
186
  public rollupContract: RollupContract;
138
187
  public govProposerContract: GovernanceProposerContract;
139
- public slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
140
- public slashFactoryContract: SlashFactoryContract;
188
+ public slashingProposerContract: SlashingProposerContract | undefined;
141
189
 
142
190
  public readonly tracer: Tracer;
143
191
 
144
192
  protected requests: RequestWithExpiry[] = [];
145
193
 
146
194
  constructor(
147
- private config: TxSenderConfig & PublisherConfig & Pick<L1ContractsConfig, 'ethereumSlotDuration'>,
195
+ private config: Pick<SequencerPublisherConfig, 'fishermanMode' | 'l1TxFailedStore'> &
196
+ Pick<L1ContractsConfig, 'ethereumSlotDuration' | 'aztecSlotDuration'> & { l1ChainId: number },
148
197
  deps: {
149
198
  telemetry?: TelemetryClient;
150
199
  blobClient: BlobClientInterface;
151
- l1TxUtils: L1TxUtilsWithBlobs;
200
+ l1TxUtils: L1TxUtils;
152
201
  rollupContract: RollupContract;
153
- slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
202
+ slashingProposerContract: SlashingProposerContract | undefined;
154
203
  governanceProposerContract: GovernanceProposerContract;
155
- slashFactoryContract: SlashFactoryContract;
156
204
  epochCache: EpochCache;
157
205
  dateProvider: DateProvider;
158
206
  metrics: SequencerPublisherMetrics;
159
207
  lastActions: Partial<Record<Action, SlotNumber>>;
160
208
  log?: Logger;
209
+ getNextPublisher?: (excludeAddresses: EthAddress[]) => Promise<L1TxUtils | undefined>;
161
210
  },
162
211
  ) {
163
212
  this.log = deps.log ?? createLogger('sequencer:publisher');
164
213
  this.ethereumSlotDuration = BigInt(config.ethereumSlotDuration);
214
+ this.aztecSlotDuration = BigInt(config.aztecSlotDuration);
215
+ this.dateProvider = deps.dateProvider;
165
216
  this.epochCache = deps.epochCache;
166
217
  this.lastActions = deps.lastActions;
167
218
 
168
219
  this.blobClient = deps.blobClient;
220
+ this.dateProvider = deps.dateProvider;
169
221
 
170
222
  const telemetry = deps.telemetry ?? getTelemetryClient();
171
223
  this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
172
224
  this.tracer = telemetry.getTracer('SequencerPublisher');
173
225
  this.l1TxUtils = deps.l1TxUtils;
226
+ this.getNextPublisher = deps.getNextPublisher;
174
227
 
175
228
  this.rollupContract = deps.rollupContract;
176
229
 
@@ -182,8 +235,6 @@ export class SequencerPublisher {
182
235
  const newSlashingProposer = await this.rollupContract.getSlashingProposer();
183
236
  this.slashingProposerContract = newSlashingProposer;
184
237
  });
185
- this.slashFactoryContract = deps.slashFactoryContract;
186
-
187
238
  // Initialize L1 fee analyzer for fisherman mode
188
239
  if (config.fishermanMode) {
189
240
  this.l1FeeAnalyzer = new L1FeeAnalyzer(
@@ -192,12 +243,52 @@ export class SequencerPublisher {
192
243
  createLogger('sequencer:publisher:fee-analyzer'),
193
244
  );
194
245
  }
246
+
247
+ // Initialize fee asset price oracle
248
+ this.feeAssetPriceOracle = new FeeAssetPriceOracle(
249
+ this.l1TxUtils.client,
250
+ this.rollupContract,
251
+ createLogger('sequencer:publisher:price-oracle'),
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
+ });
195
278
  }
196
279
 
197
280
  public getRollupContract(): RollupContract {
198
281
  return this.rollupContract;
199
282
  }
200
283
 
284
+ /**
285
+ * Gets the fee asset price modifier from the oracle.
286
+ * Returns 0n if the oracle query fails.
287
+ */
288
+ public getFeeAssetPriceModifier(): Promise<bigint> {
289
+ return this.feeAssetPriceOracle.computePriceModifier();
290
+ }
291
+
201
292
  public getSenderAddress() {
202
293
  return this.l1TxUtils.getSenderAddress();
203
294
  }
@@ -222,7 +313,7 @@ export class SequencerPublisher {
222
313
  }
223
314
 
224
315
  public getCurrentL2Slot(): SlotNumber {
225
- return this.epochCache.getEpochAndSlotNow().slot;
316
+ return this.epochCache.getSlotNow();
226
317
  }
227
318
 
228
319
  /**
@@ -273,7 +364,7 @@ export class SequencerPublisher {
273
364
  // Start the analysis
274
365
  const analysisId = await this.l1FeeAnalyzer.startAnalysis(
275
366
  l2SlotNumber,
276
- gasLimit > 0n ? gasLimit : SequencerPublisher.PROPOSE_GAS_GUESS,
367
+ gasLimit > 0n ? gasLimit : MAX_L1_TX_LIMIT,
277
368
  l1Requests,
278
369
  blobConfig,
279
370
  onComplete,
@@ -300,9 +391,10 @@ export class SequencerPublisher {
300
391
  * - undefined if no valid requests are found OR the tx failed to send.
301
392
  */
302
393
  @trackSpan('SequencerPublisher.sendRequests')
303
- public async sendRequests() {
394
+ public async sendRequests(): Promise<SendRequestsResult | undefined> {
304
395
  const requestsToProcess = [...this.requests];
305
396
  this.requests = [];
397
+
306
398
  if (this.interrupted || requestsToProcess.length === 0) {
307
399
  return undefined;
308
400
  }
@@ -335,8 +427,8 @@ export class SequencerPublisher {
335
427
  // @note - we can only have one blob config per bundle
336
428
  // find requests with gas and blob configs
337
429
  // See https://github.com/AztecProtocol/aztec-packages/issues/11513
338
- const gasConfigs = requestsToProcess.filter(request => request.gasConfig).map(request => request.gasConfig);
339
- 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);
340
432
 
341
433
  if (blobConfigs.length > 1) {
342
434
  throw new Error('Multiple blob configs found');
@@ -346,7 +438,16 @@ export class SequencerPublisher {
346
438
 
347
439
  // Merge gasConfigs. Yields the sum of gasLimits, and the earliest txTimeoutAt, or undefined if no gasConfig sets them.
348
440
  const gasLimits = gasConfigs.map(g => g?.gasLimit).filter((g): g is bigint => g !== undefined);
349
- const gasLimit = gasLimits.length > 0 ? sumBigint(gasLimits) : undefined; // sum
441
+ let gasLimit = gasLimits.length > 0 ? sumBigint(gasLimits) : undefined; // sum
442
+ // Cap at L1 block gas limit so the node accepts the tx ("gas limit too high" otherwise).
443
+ const maxGas = MAX_L1_TX_LIMIT;
444
+ if (gasLimit !== undefined && gasLimit > maxGas) {
445
+ this.log.debug('Capping bundled tx gas limit to L1 max', {
446
+ requested: gasLimit,
447
+ capped: maxGas,
448
+ });
449
+ gasLimit = maxGas;
450
+ }
350
451
  const txTimeoutAts = gasConfigs.map(g => g?.txTimeoutAt).filter((g): g is Date => g !== undefined);
351
452
  const txTimeoutAt = txTimeoutAts.length > 0 ? new Date(Math.min(...txTimeoutAts.map(g => g.getTime()))) : undefined; // earliest
352
453
  const txConfig: RequestWithExpiry['gasConfig'] = { gasLimit, txTimeoutAt };
@@ -356,19 +457,36 @@ export class SequencerPublisher {
356
457
  validRequests.sort((a, b) => compareActions(a.action, b.action));
357
458
 
358
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
+
359
477
  this.log.debug('Forwarding transactions', {
360
478
  validRequests: validRequests.map(request => request.action),
361
479
  txConfig,
362
480
  });
363
- const result = await Multicall3.forward(
364
- validRequests.map(request => request.request),
365
- this.l1TxUtils,
366
- txConfig,
367
- blobConfig,
368
- this.rollupContract.address,
369
- 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,
370
489
  );
371
- const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(validRequests, result);
372
490
  return { result, expiredActions, sentActions: validActions, successfulActions, failedActions };
373
491
  } catch (err) {
374
492
  const viemError = formatViemError(err);
@@ -386,16 +504,127 @@ export class SequencerPublisher {
386
504
  }
387
505
  }
388
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
+
389
595
  private callbackBundledTransactions(
390
596
  requests: RequestWithExpiry[],
391
- result?: { receipt: TransactionReceipt } | FormattedViemError,
597
+ result: { receipt: TransactionReceipt; errorMsg?: string } | FormattedViemError | undefined,
598
+ txContext: { multicallData: Hex; blobData?: Hex[]; l1BlockNumber: bigint },
392
599
  ) {
393
600
  const actionsListStr = requests.map(r => r.action).join(', ');
394
601
  if (result instanceof FormattedViemError) {
395
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
+ });
396
616
  return { failedActions: requests.map(r => r.action) };
397
617
  } else {
398
- 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
+ });
399
628
  const successfulActions: Action[] = [];
400
629
  const failedActions: Action[] = [];
401
630
  for (const request of requests) {
@@ -405,27 +634,54 @@ export class SequencerPublisher {
405
634
  failedActions.push(request.action);
406
635
  }
407
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
+ }
408
661
  return { successfulActions, failedActions };
409
662
  }
410
663
  }
411
664
 
412
665
  /**
413
- * @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
414
667
  * @param tipArchive - The archive to check
415
668
  * @returns The slot and block number if it is possible to propose, undefined otherwise
416
669
  */
417
- public canProposeAtNextEthBlock(
418
- tipArchive: Fr,
419
- msgSender: EthAddress,
420
- opts: { forcePendingCheckpointNumber?: CheckpointNumber } = {},
421
- ) {
670
+ public async canProposeAt(tipArchive: Fr, msgSender: EthAddress, simulationOverridesPlan?: SimulationOverridesPlan) {
422
671
  // TODO: #14291 - should loop through multiple keys to check if any of them can propose
423
672
  const ignoredErrors = ['SlotAlreadyInChain', 'InvalidProposer', 'InvalidArchive'];
424
673
 
674
+ const pipelined = this.epochCache.isProposerPipeliningEnabled();
675
+ const slotOffset = pipelined ? this.aztecSlotDuration : 0n;
676
+ const nextL1SlotTs = this.getNextL1SlotTimestamp() + slotOffset;
677
+
425
678
  return this.rollupContract
426
- .canProposeAtNextEthBlock(tipArchive.toBuffer(), msgSender.toString(), Number(this.ethereumSlotDuration), {
427
- forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
428
- })
679
+ .canProposeAt(
680
+ tipArchive.toBuffer(),
681
+ msgSender.toString(),
682
+ nextL1SlotTs,
683
+ await buildSimulationOverridesStateOverride(this.rollupContract, simulationOverridesPlan),
684
+ )
429
685
  .catch(err => {
430
686
  if (err instanceof FormattedViemError && ignoredErrors.find(e => err.message.includes(e))) {
431
687
  this.log.warn(`Failed canProposeAtTime check with ${ignoredErrors.find(e => err.message.includes(e))}`, {
@@ -437,6 +693,7 @@ export class SequencerPublisher {
437
693
  return undefined;
438
694
  });
439
695
  }
696
+
440
697
  /**
441
698
  * @notice Will simulate `validateHeader` to make sure that the block header is valid
442
699
  * @dev This is a convenience function that can be used by the sequencer to validate a "partial" header.
@@ -446,13 +703,13 @@ export class SequencerPublisher {
446
703
  @trackSpan('SequencerPublisher.validateBlockHeader')
447
704
  public async validateBlockHeader(
448
705
  header: CheckpointHeader,
449
- opts?: { forcePendingCheckpointNumber: CheckpointNumber | undefined },
706
+ simulationOverridesPlan?: SimulationOverridesPlan,
450
707
  ): Promise<void> {
451
708
  const flags = { ignoreDA: true, ignoreSignatures: true };
452
709
 
453
710
  const args = [
454
711
  header.toViem(),
455
- CommitteeAttestationsAndSigners.empty().getPackedAttestations(),
712
+ CommitteeAttestationsAndSigners.packAttestations([]),
456
713
  [], // no signers
457
714
  Signature.empty().toViemSignature(),
458
715
  `0x${'0'.repeat(64)}`, // 32 empty bytes
@@ -460,10 +717,8 @@ export class SequencerPublisher {
460
717
  flags,
461
718
  ] as const;
462
719
 
463
- const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
464
- const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(
465
- opts?.forcePendingCheckpointNumber,
466
- );
720
+ const ts = this.getSimulationTimestamp(header.slotNumber);
721
+ const stateOverrides = await buildSimulationOverridesStateOverride(this.rollupContract, simulationOverridesPlan);
467
722
  let balance = 0n;
468
723
  if (this.config.fishermanMode) {
469
724
  // In fisherman mode, we can't know where the proposer is publishing from
@@ -483,7 +738,7 @@ export class SequencerPublisher {
483
738
  data: encodeFunctionData({ abi: RollupAbi, functionName: 'validateHeaderWithAttestations', args }),
484
739
  from: MULTI_CALL_3_ADDRESS,
485
740
  },
486
- { time: ts + 1n },
741
+ { time: ts },
487
742
  stateOverrides,
488
743
  );
489
744
  this.log.debug(`Simulated validateHeader`);
@@ -516,8 +771,15 @@ export class SequencerPublisher {
516
771
  const request = this.buildInvalidateCheckpointRequest(validationResult);
517
772
  this.log.debug(`Simulating invalidate checkpoint ${checkpointNumber}`, { ...logData, request });
518
773
 
774
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
775
+
519
776
  try {
520
- const { gasUsed } = await this.l1TxUtils.simulate(request, undefined, undefined, ErrorsAbi);
777
+ const { gasUsed } = await this.l1TxUtils.simulate(
778
+ request,
779
+ undefined,
780
+ undefined,
781
+ mergeAbis([request.abi ?? [], ErrorsAbi]),
782
+ );
521
783
  this.log.verbose(`Simulation for invalidate checkpoint ${checkpointNumber} succeeded`, {
522
784
  ...logData,
523
785
  request,
@@ -529,6 +791,7 @@ export class SequencerPublisher {
529
791
  gasUsed,
530
792
  checkpointNumber,
531
793
  forcePendingCheckpointNumber: CheckpointNumber(checkpointNumber - 1),
794
+ lastArchive: validationResult.checkpoint.lastArchive,
532
795
  reason,
533
796
  };
534
797
  } catch (err) {
@@ -536,13 +799,13 @@ export class SequencerPublisher {
536
799
 
537
800
  // If the error is due to the checkpoint not being in the pending chain, and it was indeed removed by someone else,
538
801
  // we can safely ignore it and return undefined so we go ahead with checkpoint building.
539
- if (viemError.message?.includes('Rollup__BlockNotInPendingChain')) {
802
+ if (viemError.message?.includes('Rollup__CheckpointNotInPendingChain')) {
540
803
  this.log.verbose(
541
804
  `Simulation for invalidate checkpoint ${checkpointNumber} failed due to checkpoint not being in pending chain`,
542
805
  { ...logData, request, error: viemError.message },
543
806
  );
544
- const latestPendingCheckpointNumber = await this.rollupContract.getCheckpointNumber();
545
- if (latestPendingCheckpointNumber < checkpointNumber) {
807
+ const latestProposedCheckpointNumber = await this.rollupContract.getCheckpointNumber();
808
+ if (latestProposedCheckpointNumber < checkpointNumber) {
546
809
  this.log.verbose(`Checkpoint ${checkpointNumber} has already been invalidated`, { ...logData });
547
810
  return undefined;
548
811
  } else {
@@ -562,6 +825,18 @@ export class SequencerPublisher {
562
825
 
563
826
  // Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
564
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
+ });
565
840
  throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, { cause: viemError });
566
841
  }
567
842
  }
@@ -575,9 +850,7 @@ export class SequencerPublisher {
575
850
  const logData = { ...checkpoint, reason };
576
851
  this.log.debug(`Building invalidate checkpoint ${checkpoint.checkpointNumber} request`, logData);
577
852
 
578
- const attestationsAndSigners = new CommitteeAttestationsAndSigners(
579
- validationResult.attestations,
580
- ).getPackedAttestations();
853
+ const attestationsAndSigners = CommitteeAttestationsAndSigners.packAttestations(validationResult.attestations);
581
854
 
582
855
  if (reason === 'invalid-attestation') {
583
856
  return this.rollupContract.buildInvalidateBadAttestationRequest(
@@ -604,27 +877,10 @@ export class SequencerPublisher {
604
877
  checkpoint: Checkpoint,
605
878
  attestationsAndSigners: CommitteeAttestationsAndSigners,
606
879
  attestationsAndSignersSignature: Signature,
607
- options: { forcePendingCheckpointNumber?: CheckpointNumber },
608
- ): Promise<bigint> {
609
- const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
610
-
611
- // TODO(palla/mbps): This should not be needed, there's no flow where we propose with zero attestations. Or is there?
612
- // If we have no attestations, we still need to provide the empty attestations
613
- // so that the committee is recalculated correctly
614
- // const ignoreSignatures = attestationsAndSigners.attestations.length === 0;
615
- // if (ignoreSignatures) {
616
- // const { committee } = await this.epochCache.getCommittee(block.header.globalVariables.slotNumber);
617
- // if (!committee) {
618
- // this.log.warn(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
619
- // throw new Error(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
620
- // }
621
- // attestationsAndSigners.attestations = committee.map(committeeMember =>
622
- // CommitteeAttestation.fromAddress(committeeMember),
623
- // );
624
- // }
625
-
880
+ simulationOverridesPlan?: SimulationOverridesPlan,
881
+ ): Promise<void> {
626
882
  const blobFields = checkpoint.toBlobFields();
627
- const blobs = getBlobsPerL1Block(blobFields);
883
+ const blobs = await getBlobsPerL1Block(blobFields);
628
884
  const blobInput = getPrefixedEthBlobCommitments(blobs);
629
885
 
630
886
  const args = [
@@ -632,7 +888,7 @@ export class SequencerPublisher {
632
888
  header: checkpoint.header.toViem(),
633
889
  archive: toHex(checkpoint.archive.root.toBuffer()),
634
890
  oracleInput: {
635
- feeAssetPriceModifier: 0n,
891
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
636
892
  },
637
893
  },
638
894
  attestationsAndSigners.getPackedAttestations(),
@@ -641,13 +897,11 @@ export class SequencerPublisher {
641
897
  blobInput,
642
898
  ] as const;
643
899
 
644
- await this.simulateProposeTx(args, ts, options);
645
- return ts;
900
+ await this.simulateProposeTx(args, simulationOverridesPlan);
646
901
  }
647
902
 
648
903
  private async enqueueCastSignalHelper(
649
904
  slotNumber: SlotNumber,
650
- timestamp: bigint,
651
905
  signalType: GovernanceSignalAction,
652
906
  payload: EthAddress,
653
907
  base: IEmpireBase,
@@ -681,6 +935,32 @@ export class SequencerPublisher {
681
935
  return false;
682
936
  }
683
937
 
938
+ // Check if payload was already submitted to governance
939
+ const cacheKey = payload.toString();
940
+ if (!this.payloadProposedCache.has(cacheKey)) {
941
+ try {
942
+ const l1StartBlock = await this.rollupContract.getL1StartBlock();
943
+ const proposed = await retry(
944
+ () => base.hasPayloadBeenProposed(payload.toString(), l1StartBlock),
945
+ 'Check if payload was proposed',
946
+ makeBackoff([0, 1, 2]),
947
+ this.log,
948
+ true,
949
+ );
950
+ if (proposed) {
951
+ this.payloadProposedCache.add(cacheKey);
952
+ }
953
+ } catch (err) {
954
+ this.log.warn(`Failed to check if payload ${payload} was proposed after retries, skipping signal`, err);
955
+ return false;
956
+ }
957
+ }
958
+
959
+ if (this.payloadProposedCache.has(cacheKey)) {
960
+ this.log.info(`Payload ${payload} was already proposed to governance, stopping signals`);
961
+ return false;
962
+ }
963
+
684
964
  const cachedLastVote = this.lastActions[signalType];
685
965
  this.lastActions[signalType] = slotNumber;
686
966
  const action = signalType;
@@ -699,11 +979,30 @@ export class SequencerPublisher {
699
979
  lastValidL2Slot: slotNumber,
700
980
  });
701
981
 
982
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
983
+ const timestamp = this.getSimulationTimestamp(slotNumber);
984
+
702
985
  try {
703
- await this.l1TxUtils.simulate(request, { time: timestamp }, [], ErrorsAbi);
986
+ await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
704
987
  this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
705
988
  } catch (err) {
706
- this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, err);
989
+ const viemError = formatViemError(err);
990
+ this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, viemError, {
991
+ simulationTimestamp: timestamp,
992
+ l1BlockNumber,
993
+ });
994
+ this.backupFailedTx({
995
+ id: keccak256(request.data!),
996
+ failureType: 'simulation',
997
+ request: { to: request.to!, data: request.data!, value: request.value?.toString() },
998
+ l1BlockNumber: l1BlockNumber.toString(),
999
+ error: { message: viemError.message, name: viemError.name },
1000
+ context: {
1001
+ actions: [action],
1002
+ slot: slotNumber,
1003
+ sender: this.getSenderAddress().toString(),
1004
+ },
1005
+ });
707
1006
  // Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
708
1007
  }
709
1008
 
@@ -754,19 +1053,16 @@ export class SequencerPublisher {
754
1053
  /**
755
1054
  * Enqueues a governance castSignal transaction to cast a signal for a given slot number.
756
1055
  * @param slotNumber - The slot number to cast a signal for.
757
- * @param timestamp - The timestamp of the slot to cast a signal for.
758
1056
  * @returns True if the signal was successfully enqueued, false otherwise.
759
1057
  */
760
1058
  public enqueueGovernanceCastSignal(
761
1059
  governancePayload: EthAddress,
762
1060
  slotNumber: SlotNumber,
763
- timestamp: bigint,
764
1061
  signerAddress: EthAddress,
765
1062
  signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
766
1063
  ): Promise<boolean> {
767
1064
  return this.enqueueCastSignalHelper(
768
1065
  slotNumber,
769
- timestamp,
770
1066
  'governance-signal',
771
1067
  governancePayload,
772
1068
  this.govProposerContract,
@@ -779,7 +1075,6 @@ export class SequencerPublisher {
779
1075
  public async enqueueSlashingActions(
780
1076
  actions: ProposerSlashAction[],
781
1077
  slotNumber: SlotNumber,
782
- timestamp: bigint,
783
1078
  signerAddress: EthAddress,
784
1079
  signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
785
1080
  ): Promise<boolean> {
@@ -790,58 +1085,6 @@ export class SequencerPublisher {
790
1085
 
791
1086
  for (const action of actions) {
792
1087
  switch (action.type) {
793
- case 'vote-empire-payload': {
794
- if (this.slashingProposerContract?.type !== 'empire') {
795
- this.log.error('Cannot vote for empire payload on non-empire slashing contract');
796
- break;
797
- }
798
- this.log.debug(`Enqueuing slashing vote for payload ${action.payload} at slot ${slotNumber}`, {
799
- signerAddress,
800
- });
801
- await this.enqueueCastSignalHelper(
802
- slotNumber,
803
- timestamp,
804
- 'empire-slashing-signal',
805
- action.payload,
806
- this.slashingProposerContract,
807
- signerAddress,
808
- signer,
809
- );
810
- break;
811
- }
812
-
813
- case 'create-empire-payload': {
814
- this.log.debug(`Enqueuing slashing create payload at slot ${slotNumber}`, { slotNumber, signerAddress });
815
- const request = this.slashFactoryContract.buildCreatePayloadRequest(action.data);
816
- await this.simulateAndEnqueueRequest(
817
- 'create-empire-payload',
818
- request,
819
- (receipt: TransactionReceipt) =>
820
- !!this.slashFactoryContract.tryExtractSlashPayloadCreatedEvent(receipt.logs),
821
- slotNumber,
822
- timestamp,
823
- );
824
- break;
825
- }
826
-
827
- case 'execute-empire-payload': {
828
- this.log.debug(`Enqueuing slashing execute payload at slot ${slotNumber}`, { slotNumber, signerAddress });
829
- if (this.slashingProposerContract?.type !== 'empire') {
830
- this.log.error('Cannot execute slashing payload on non-empire slashing contract');
831
- return false;
832
- }
833
- const empireSlashingProposer = this.slashingProposerContract as EmpireSlashingProposerContract;
834
- const request = empireSlashingProposer.buildExecuteRoundRequest(action.round);
835
- await this.simulateAndEnqueueRequest(
836
- 'execute-empire-payload',
837
- request,
838
- (receipt: TransactionReceipt) => !!empireSlashingProposer.tryExtractPayloadSubmittedEvent(receipt.logs),
839
- slotNumber,
840
- timestamp,
841
- );
842
- break;
843
- }
844
-
845
1088
  case 'vote-offenses': {
846
1089
  this.log.debug(`Enqueuing slashing vote for ${action.votes.length} votes at slot ${slotNumber}`, {
847
1090
  slotNumber,
@@ -849,19 +1092,17 @@ export class SequencerPublisher {
849
1092
  votesCount: action.votes.length,
850
1093
  signerAddress,
851
1094
  });
852
- if (this.slashingProposerContract?.type !== 'tally') {
853
- this.log.error('Cannot vote for slashing offenses on non-tally slashing contract');
1095
+ if (!this.slashingProposerContract) {
1096
+ this.log.error('No slashing proposer contract available');
854
1097
  return false;
855
1098
  }
856
- const tallySlashingProposer = this.slashingProposerContract as TallySlashingProposerContract;
857
1099
  const votes = bufferToHex(encodeSlashConsensusVotes(action.votes));
858
- const request = await tallySlashingProposer.buildVoteRequestFromSigner(votes, slotNumber, signer);
1100
+ const request = await this.slashingProposerContract.buildVoteRequestFromSigner(votes, slotNumber, signer);
859
1101
  await this.simulateAndEnqueueRequest(
860
1102
  'vote-offenses',
861
1103
  request,
862
- (receipt: TransactionReceipt) => !!tallySlashingProposer.tryExtractVoteCastEvent(receipt.logs),
1104
+ (receipt: TransactionReceipt) => !!this.slashingProposerContract!.tryExtractVoteCastEvent(receipt.logs),
863
1105
  slotNumber,
864
- timestamp,
865
1106
  );
866
1107
  break;
867
1108
  }
@@ -872,18 +1113,20 @@ export class SequencerPublisher {
872
1113
  round: action.round,
873
1114
  signerAddress,
874
1115
  });
875
- if (this.slashingProposerContract?.type !== 'tally') {
876
- this.log.error('Cannot execute slashing offenses on non-tally slashing contract');
1116
+ if (!this.slashingProposerContract) {
1117
+ this.log.error('No slashing proposer contract available');
877
1118
  return false;
878
1119
  }
879
- const tallySlashingProposer = this.slashingProposerContract as TallySlashingProposerContract;
880
- const request = tallySlashingProposer.buildExecuteRoundRequest(action.round, action.committees);
1120
+ const executeRequest = this.slashingProposerContract.buildExecuteRoundRequest(
1121
+ action.round,
1122
+ action.committees,
1123
+ );
881
1124
  await this.simulateAndEnqueueRequest(
882
1125
  'execute-slash',
883
- request,
884
- (receipt: TransactionReceipt) => !!tallySlashingProposer.tryExtractRoundExecutedEvent(receipt.logs),
1126
+ executeRequest,
1127
+ (receipt: TransactionReceipt) =>
1128
+ !!this.slashingProposerContract!.tryExtractRoundExecutedEvent(receipt.logs),
885
1129
  slotNumber,
886
- timestamp,
887
1130
  );
888
1131
  break;
889
1132
  }
@@ -903,22 +1146,25 @@ export class SequencerPublisher {
903
1146
  checkpoint: Checkpoint,
904
1147
  attestationsAndSigners: CommitteeAttestationsAndSigners,
905
1148
  attestationsAndSignersSignature: Signature,
906
- opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
1149
+ opts: EnqueueProposeCheckpointOpts = {},
907
1150
  ): Promise<void> {
908
1151
  const checkpointHeader = checkpoint.header;
909
1152
 
910
1153
  const blobFields = checkpoint.toBlobFields();
911
- const blobs = getBlobsPerL1Block(blobFields);
1154
+ const blobs = await getBlobsPerL1Block(blobFields);
912
1155
 
913
- const proposeTxArgs = {
1156
+ const proposeTxArgs: L1ProcessArgs = {
914
1157
  header: checkpointHeader,
915
1158
  archive: checkpoint.archive.root.toBuffer(),
916
1159
  blobs,
917
1160
  attestationsAndSigners,
918
1161
  attestationsAndSignersSignature,
1162
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
919
1163
  };
920
1164
 
921
- let ts: bigint;
1165
+ const simulationOverridesPlan = SimulationOverridesBuilder.from(opts.simulationOverridesPlan)
1166
+ .withoutBlobCheck()
1167
+ .build();
922
1168
 
923
1169
  try {
924
1170
  // @note This will make sure that we are passing the checks for our header ASSUMING that the data is also made available
@@ -926,23 +1172,47 @@ export class SequencerPublisher {
926
1172
  // By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which
927
1173
  // make time consistency checks break.
928
1174
  // TODO(palla): Check whether we're validating twice, once here and once within addProposeTx, since we call simulateProposeTx in both places.
929
- ts = await this.validateCheckpointForSubmission(
1175
+ await this.validateCheckpointForSubmission(
930
1176
  checkpoint,
931
1177
  attestationsAndSigners,
932
1178
  attestationsAndSignersSignature,
933
- opts,
1179
+ simulationOverridesPlan,
934
1180
  );
935
1181
  } catch (err: any) {
936
1182
  this.log.error(`Checkpoint validation failed. ${err instanceof Error ? err.message : 'No error message'}`, err, {
937
1183
  ...checkpoint.getStats(),
938
1184
  slotNumber: checkpoint.header.slotNumber,
939
- forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
1185
+ simulationOverridesPlan,
940
1186
  });
941
1187
  throw err;
942
1188
  }
943
1189
 
944
- this.log.verbose(`Enqueuing checkpoint propose transaction`, { ...checkpoint.toCheckpointInfo(), ...opts });
945
- await this.addProposeTx(checkpoint, proposeTxArgs, opts, ts);
1190
+ // Build a pre-check callback that re-validates the checkpoint before L1 submission.
1191
+ // During pipelining this catches stale proposals due to prunes or L1 reorgs that occur during the pipeline sleep.
1192
+ let preCheck = undefined;
1193
+ if (this.epochCache.isProposerPipeliningEnabled()) {
1194
+ preCheck = async () => {
1195
+ this.log.debug(`Re-validating checkpoint ${checkpoint.number} before L1 submission`);
1196
+ await this.validateCheckpointForSubmission(
1197
+ checkpoint,
1198
+ attestationsAndSigners,
1199
+ attestationsAndSignersSignature,
1200
+ simulationOverridesPlan,
1201
+ );
1202
+ };
1203
+ }
1204
+
1205
+ this.log.verbose(`Enqueuing checkpoint propose transaction`, {
1206
+ ...checkpoint.toCheckpointInfo(),
1207
+ txTimeoutAt: opts.txTimeoutAt,
1208
+ simulationOverridesPlan,
1209
+ });
1210
+ await this.addProposeTx(
1211
+ checkpoint,
1212
+ proposeTxArgs,
1213
+ { txTimeoutAt: opts.txTimeoutAt, simulationOverridesPlan },
1214
+ preCheck,
1215
+ );
946
1216
  }
947
1217
 
948
1218
  public enqueueInvalidateCheckpoint(
@@ -985,8 +1255,8 @@ export class SequencerPublisher {
985
1255
  request: L1TxRequest,
986
1256
  checkSuccess: (receipt: TransactionReceipt) => boolean | undefined,
987
1257
  slotNumber: SlotNumber,
988
- timestamp: bigint,
989
1258
  ) {
1259
+ const timestamp = this.getSimulationTimestamp(slotNumber);
990
1260
  const logData = { slotNumber, timestamp, gasLimit: undefined as bigint | undefined };
991
1261
  if (this.lastActions[action] && this.lastActions[action] === slotNumber) {
992
1262
  this.log.debug(`Skipping duplicate action ${action} for slot ${slotNumber}`);
@@ -998,13 +1268,31 @@ export class SequencerPublisher {
998
1268
 
999
1269
  this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
1000
1270
 
1271
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1272
+
1001
1273
  let gasUsed: bigint;
1274
+ const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
1275
+
1002
1276
  try {
1003
- ({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], ErrorsAbi)); // TODO(palla/slash): Check the timestamp logic
1277
+ ({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], simulateAbi));
1004
1278
  this.log.verbose(`Simulation for ${action} succeeded`, { ...logData, request, gasUsed });
1005
1279
  } catch (err) {
1006
- const viemError = formatViemError(err);
1280
+ const viemError = formatViemError(err, simulateAbi);
1007
1281
  this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
1282
+
1283
+ this.backupFailedTx({
1284
+ id: keccak256(request.data!),
1285
+ failureType: 'simulation',
1286
+ request: { to: request.to!, data: request.data!, value: request.value?.toString() },
1287
+ l1BlockNumber: l1BlockNumber.toString(),
1288
+ error: { message: viemError.message, name: viemError.name },
1289
+ context: {
1290
+ actions: [action],
1291
+ slot: slotNumber,
1292
+ sender: this.getSenderAddress().toString(),
1293
+ },
1294
+ });
1295
+
1008
1296
  return false;
1009
1297
  }
1010
1298
 
@@ -1012,10 +1300,14 @@ export class SequencerPublisher {
1012
1300
  const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil((Number(gasUsed) * 64) / 63)));
1013
1301
  logData.gasLimit = gasLimit;
1014
1302
 
1303
+ // Store the ABI used for simulation on the request so Multicall3.forward can decode errors
1304
+ // when the tx is sent and a revert is diagnosed via simulation.
1305
+ const requestWithAbi = { ...request, abi: simulateAbi };
1306
+
1015
1307
  this.log.debug(`Enqueuing ${action}`, logData);
1016
1308
  this.addRequest({
1017
1309
  action,
1018
- request,
1310
+ request: requestWithAbi,
1019
1311
  gasConfig: { gasLimit },
1020
1312
  lastValidL2Slot: slotNumber,
1021
1313
  checkSuccess: (_req, result) => {
@@ -1040,6 +1332,7 @@ export class SequencerPublisher {
1040
1332
  */
1041
1333
  public interrupt() {
1042
1334
  this.interrupted = true;
1335
+ this.interruptibleSleep.interrupt();
1043
1336
  this.l1TxUtils.interrupt();
1044
1337
  }
1045
1338
 
@@ -1049,11 +1342,7 @@ export class SequencerPublisher {
1049
1342
  this.l1TxUtils.restart();
1050
1343
  }
1051
1344
 
1052
- private async prepareProposeTx(
1053
- encodedData: L1ProcessArgs,
1054
- timestamp: bigint,
1055
- options: { forcePendingCheckpointNumber?: CheckpointNumber },
1056
- ) {
1345
+ private async prepareProposeTx(encodedData: L1ProcessArgs, simulationOverridesPlan?: SimulationOverridesPlan) {
1057
1346
  const kzg = Blob.getViemKzgInstance();
1058
1347
  const blobInput = getPrefixedEthBlobCommitments(encodedData.blobs);
1059
1348
  this.log.debug('Validating blob input', { blobInput });
@@ -1084,9 +1373,27 @@ export class SequencerPublisher {
1084
1373
  kzg,
1085
1374
  },
1086
1375
  )
1087
- .catch(err => {
1088
- const { message, metaMessages } = formatViemError(err);
1089
- this.log.error(`Failed to validate blobs`, message, { metaMessages });
1376
+ .catch(async err => {
1377
+ const viemError = formatViemError(err);
1378
+ this.log.error(`Failed to validate blobs`, viemError.message, { metaMessages: viemError.metaMessages });
1379
+ const validateBlobsData = encodeFunctionData({
1380
+ abi: RollupAbi,
1381
+ functionName: 'validateBlobs',
1382
+ args: [blobInput],
1383
+ });
1384
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1385
+ this.backupFailedTx({
1386
+ id: keccak256(validateBlobsData),
1387
+ failureType: 'simulation',
1388
+ request: { to: this.rollupContract.address as Hex, data: validateBlobsData },
1389
+ blobData: encodedData.blobs.map(b => toHex(b.data)) as Hex[],
1390
+ l1BlockNumber: l1BlockNumber.toString(),
1391
+ error: { message: viemError.message, name: viemError.name },
1392
+ context: {
1393
+ actions: ['validate-blobs'],
1394
+ sender: this.getSenderAddress().toString(),
1395
+ },
1396
+ });
1090
1397
  throw new Error('Failed to validate blobs');
1091
1398
  });
1092
1399
  }
@@ -1097,8 +1404,7 @@ export class SequencerPublisher {
1097
1404
  header: encodedData.header.toViem(),
1098
1405
  archive: toHex(encodedData.archive),
1099
1406
  oracleInput: {
1100
- // We are currently not modifying these. See #9963
1101
- feeAssetPriceModifier: 0n,
1407
+ feeAssetPriceModifier: encodedData.feeAssetPriceModifier,
1102
1408
  },
1103
1409
  },
1104
1410
  encodedData.attestationsAndSigners.getPackedAttestations(),
@@ -1107,7 +1413,7 @@ export class SequencerPublisher {
1107
1413
  blobInput,
1108
1414
  ] as const;
1109
1415
 
1110
- const { rollupData, simulationResult } = await this.simulateProposeTx(args, timestamp, options);
1416
+ const { rollupData, simulationResult } = await this.simulateProposeTx(args, simulationOverridesPlan);
1111
1417
 
1112
1418
  return { args, blobEvaluationGas, rollupData, simulationResult };
1113
1419
  }
@@ -1115,7 +1421,6 @@ export class SequencerPublisher {
1115
1421
  /**
1116
1422
  * Simulates the propose tx with eth_simulateV1
1117
1423
  * @param args - The propose tx args
1118
- * @param timestamp - The timestamp to simulate proposal at
1119
1424
  * @returns The simulation result
1120
1425
  */
1121
1426
  private async simulateProposeTx(
@@ -1124,7 +1429,7 @@ export class SequencerPublisher {
1124
1429
  readonly header: ViemHeader;
1125
1430
  readonly archive: `0x${string}`;
1126
1431
  readonly oracleInput: {
1127
- readonly feeAssetPriceModifier: 0n;
1432
+ readonly feeAssetPriceModifier: bigint;
1128
1433
  };
1129
1434
  },
1130
1435
  ViemCommitteeAttestations,
@@ -1132,8 +1437,7 @@ export class SequencerPublisher {
1132
1437
  ViemSignature,
1133
1438
  `0x${string}`,
1134
1439
  ],
1135
- timestamp: bigint,
1136
- options: { forcePendingCheckpointNumber?: CheckpointNumber },
1440
+ simulationOverridesPlan?: SimulationOverridesPlan,
1137
1441
  ) {
1138
1442
  const rollupData = encodeFunctionData({
1139
1443
  abi: RollupAbi,
@@ -1141,23 +1445,7 @@ export class SequencerPublisher {
1141
1445
  args,
1142
1446
  });
1143
1447
 
1144
- // override the pending checkpoint number if requested
1145
- const forcePendingCheckpointNumberStateDiff = (
1146
- options.forcePendingCheckpointNumber !== undefined
1147
- ? await this.rollupContract.makePendingCheckpointNumberOverride(options.forcePendingCheckpointNumber)
1148
- : []
1149
- ).flatMap(override => override.stateDiff ?? []);
1150
-
1151
- const stateOverrides: StateOverride = [
1152
- {
1153
- address: this.rollupContract.address,
1154
- // @note we override checkBlob to false since blobs are not part simulate()
1155
- stateDiff: [
1156
- { slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true), value: toPaddedHex(0n, true) },
1157
- ...forcePendingCheckpointNumberStateDiff,
1158
- ],
1159
- },
1160
- ];
1448
+ const stateOverrides = await buildSimulationOverridesStateOverride(this.rollupContract, simulationOverridesPlan);
1161
1449
  // In fisherman mode, simulate as the proposer but with sufficient balance
1162
1450
  if (this.proposerAddressForSimulation) {
1163
1451
  stateOverrides.push({
@@ -1166,25 +1454,27 @@ export class SequencerPublisher {
1166
1454
  });
1167
1455
  }
1168
1456
 
1457
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1458
+ const simTs = this.getSimulationTimestamp(SlotNumber.fromBigInt(args[0].header.slotNumber));
1459
+
1169
1460
  const simulationResult = await this.l1TxUtils
1170
1461
  .simulate(
1171
1462
  {
1172
1463
  to: this.rollupContract.address,
1173
1464
  data: rollupData,
1174
- gas: SequencerPublisher.PROPOSE_GAS_GUESS,
1465
+ gas: MAX_L1_TX_LIMIT,
1175
1466
  ...(this.proposerAddressForSimulation && { from: this.proposerAddressForSimulation.toString() }),
1176
1467
  },
1177
1468
  {
1178
- // @note we add 1n to the timestamp because geth implementation doesn't like simulation timestamp to be equal to the current block timestamp
1179
- time: timestamp + 1n,
1469
+ time: simTs,
1180
1470
  // @note reth should have a 30m gas limit per block but throws errors that this tx is beyond limit so we increase here
1181
- gasLimit: SequencerPublisher.PROPOSE_GAS_GUESS * 2n,
1471
+ gasLimit: MAX_L1_TX_LIMIT * 2n,
1182
1472
  },
1183
1473
  stateOverrides,
1184
1474
  RollupAbi,
1185
1475
  {
1186
1476
  // @note fallback gas estimate to use if the node doesn't support simulation API
1187
- fallbackGasEstimate: SequencerPublisher.PROPOSE_GAS_GUESS,
1477
+ fallbackGasEstimate: MAX_L1_TX_LIMIT,
1188
1478
  },
1189
1479
  )
1190
1480
  .catch(err => {
@@ -1194,11 +1484,23 @@ export class SequencerPublisher {
1194
1484
  this.log.debug(`Ignoring expected ValidatorSelection__MissingProposerSignature error in fisherman mode`);
1195
1485
  // Return a minimal simulation result with the fallback gas estimate
1196
1486
  return {
1197
- gasUsed: SequencerPublisher.PROPOSE_GAS_GUESS,
1487
+ gasUsed: MAX_L1_TX_LIMIT,
1198
1488
  logs: [],
1199
1489
  };
1200
1490
  }
1201
- this.log.error(`Failed to simulate propose tx`, viemError);
1491
+ this.log.error(`Failed to simulate propose tx`, viemError, { simulationTimestamp: simTs });
1492
+ this.backupFailedTx({
1493
+ id: keccak256(rollupData),
1494
+ failureType: 'simulation',
1495
+ request: { to: this.rollupContract.address, data: rollupData },
1496
+ l1BlockNumber: l1BlockNumber.toString(),
1497
+ error: { message: viemError.message, name: viemError.name },
1498
+ context: {
1499
+ actions: ['propose'],
1500
+ slot: Number(args[0].header.slotNumber),
1501
+ sender: this.getSenderAddress().toString(),
1502
+ },
1503
+ });
1202
1504
  throw err;
1203
1505
  });
1204
1506
 
@@ -1208,16 +1510,15 @@ export class SequencerPublisher {
1208
1510
  private async addProposeTx(
1209
1511
  checkpoint: Checkpoint,
1210
1512
  encodedData: L1ProcessArgs,
1211
- opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
1212
- timestamp: bigint,
1513
+ opts: EnqueueProposeCheckpointOpts = {},
1514
+ preCheck?: () => Promise<void>,
1213
1515
  ): Promise<void> {
1214
1516
  const slot = checkpoint.header.slotNumber;
1215
1517
  const timer = new Timer();
1216
1518
  const kzg = Blob.getViemKzgInstance();
1217
1519
  const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(
1218
1520
  encodedData,
1219
- timestamp,
1220
- opts,
1521
+ opts.simulationOverridesPlan,
1221
1522
  );
1222
1523
  const startBlock = await this.l1TxUtils.getBlockNumber();
1223
1524
  const gasLimit = this.l1TxUtils.bumpGasLimit(
@@ -1241,7 +1542,8 @@ export class SequencerPublisher {
1241
1542
  data: rollupData,
1242
1543
  },
1243
1544
  lastValidL2Slot: checkpoint.header.slotNumber,
1244
- gasConfig: { ...opts, gasLimit },
1545
+ gasConfig: { txTimeoutAt: opts.txTimeoutAt, gasLimit },
1546
+ preCheck,
1245
1547
  blobConfig: {
1246
1548
  blobs: encodedData.blobs.map(b => b.data),
1247
1549
  kzg,
@@ -1294,4 +1596,17 @@ export class SequencerPublisher {
1294
1596
  },
1295
1597
  });
1296
1598
  }
1599
+
1600
+ /** Returns the timestamp of the last L1 slot within a given L2 slot. Used as the simulation timestamp
1601
+ * for eth_simulateV1 calls, since it's guaranteed to be greater than any L1 block produced during the slot. */
1602
+ private getSimulationTimestamp(slot: SlotNumber): bigint {
1603
+ const l1Constants = this.epochCache.getL1Constants();
1604
+ return getLastL1SlotTimestampForL2Slot(slot, l1Constants);
1605
+ }
1606
+
1607
+ /** Returns the timestamp of the next L1 slot boundary after now. */
1608
+ private getNextL1SlotTimestamp(): bigint {
1609
+ const l1Constants = this.epochCache.getL1Constants();
1610
+ return getNextL1SlotTimestamp(this.dateProvider.nowInSeconds(), l1Constants);
1611
+ }
1297
1612
  }