@aztec/sequencer-client 0.0.1-commit.9d2bcf6d → 0.0.1-commit.9ef841308

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 (84) hide show
  1. package/dest/client/sequencer-client.d.ts +15 -7
  2. package/dest/client/sequencer-client.d.ts.map +1 -1
  3. package/dest/client/sequencer-client.js +60 -26
  4. package/dest/config.d.ts +26 -7
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +47 -28
  7. package/dest/global_variable_builder/global_builder.d.ts +14 -10
  8. package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
  9. package/dest/global_variable_builder/global_builder.js +22 -21
  10. package/dest/global_variable_builder/index.d.ts +2 -2
  11. package/dest/global_variable_builder/index.d.ts.map +1 -1
  12. package/dest/publisher/config.d.ts +47 -17
  13. package/dest/publisher/config.d.ts.map +1 -1
  14. package/dest/publisher/config.js +121 -42
  15. package/dest/publisher/index.d.ts +2 -1
  16. package/dest/publisher/index.d.ts.map +1 -1
  17. package/dest/publisher/l1_tx_failed_store/factory.d.ts +11 -0
  18. package/dest/publisher/l1_tx_failed_store/factory.d.ts.map +1 -0
  19. package/dest/publisher/l1_tx_failed_store/factory.js +22 -0
  20. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts +59 -0
  21. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -0
  22. package/dest/publisher/l1_tx_failed_store/failed_tx_store.js +1 -0
  23. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts +15 -0
  24. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts.map +1 -0
  25. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.js +34 -0
  26. package/dest/publisher/l1_tx_failed_store/index.d.ts +4 -0
  27. package/dest/publisher/l1_tx_failed_store/index.d.ts.map +1 -0
  28. package/dest/publisher/l1_tx_failed_store/index.js +2 -0
  29. package/dest/publisher/sequencer-publisher-factory.d.ts +11 -3
  30. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  31. package/dest/publisher/sequencer-publisher-factory.js +27 -2
  32. package/dest/publisher/sequencer-publisher.d.ts +39 -13
  33. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  34. package/dest/publisher/sequencer-publisher.js +364 -66
  35. package/dest/sequencer/checkpoint_proposal_job.d.ts +15 -7
  36. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  37. package/dest/sequencer/checkpoint_proposal_job.js +241 -140
  38. package/dest/sequencer/checkpoint_voter.d.ts +1 -2
  39. package/dest/sequencer/checkpoint_voter.d.ts.map +1 -1
  40. package/dest/sequencer/checkpoint_voter.js +2 -5
  41. package/dest/sequencer/events.d.ts +2 -1
  42. package/dest/sequencer/events.d.ts.map +1 -1
  43. package/dest/sequencer/metrics.d.ts +21 -5
  44. package/dest/sequencer/metrics.d.ts.map +1 -1
  45. package/dest/sequencer/metrics.js +97 -15
  46. package/dest/sequencer/sequencer.d.ts +28 -15
  47. package/dest/sequencer/sequencer.d.ts.map +1 -1
  48. package/dest/sequencer/sequencer.js +93 -84
  49. package/dest/sequencer/timetable.d.ts +4 -6
  50. package/dest/sequencer/timetable.d.ts.map +1 -1
  51. package/dest/sequencer/timetable.js +7 -11
  52. package/dest/sequencer/types.d.ts +2 -2
  53. package/dest/sequencer/types.d.ts.map +1 -1
  54. package/dest/test/index.d.ts +3 -5
  55. package/dest/test/index.d.ts.map +1 -1
  56. package/dest/test/mock_checkpoint_builder.d.ts +11 -11
  57. package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
  58. package/dest/test/mock_checkpoint_builder.js +45 -34
  59. package/dest/test/utils.d.ts +3 -3
  60. package/dest/test/utils.d.ts.map +1 -1
  61. package/dest/test/utils.js +5 -4
  62. package/package.json +27 -28
  63. package/src/client/sequencer-client.ts +76 -23
  64. package/src/config.ts +65 -38
  65. package/src/global_variable_builder/global_builder.ts +23 -24
  66. package/src/global_variable_builder/index.ts +1 -1
  67. package/src/publisher/config.ts +153 -43
  68. package/src/publisher/index.ts +3 -0
  69. package/src/publisher/l1_tx_failed_store/factory.ts +32 -0
  70. package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +55 -0
  71. package/src/publisher/l1_tx_failed_store/file_store_failed_tx_store.ts +46 -0
  72. package/src/publisher/l1_tx_failed_store/index.ts +3 -0
  73. package/src/publisher/sequencer-publisher-factory.ts +38 -6
  74. package/src/publisher/sequencer-publisher.ts +359 -86
  75. package/src/sequencer/checkpoint_proposal_job.ts +328 -151
  76. package/src/sequencer/checkpoint_voter.ts +1 -12
  77. package/src/sequencer/events.ts +1 -1
  78. package/src/sequencer/metrics.ts +106 -18
  79. package/src/sequencer/sequencer.ts +127 -96
  80. package/src/sequencer/timetable.ts +13 -12
  81. package/src/sequencer/types.ts +1 -1
  82. package/src/test/index.ts +2 -4
  83. package/src/test/mock_checkpoint_builder.ts +63 -49
  84. package/src/test/utils.ts +5 -2
@@ -4,6 +4,7 @@ import type { EpochCache } from '@aztec/epoch-cache';
4
4
  import type { L1ContractsConfig } from '@aztec/ethereum/config';
5
5
  import {
6
6
  type EmpireSlashingProposerContract,
7
+ FeeAssetPriceOracle,
7
8
  type GovernanceProposerContract,
8
9
  type IEmpireBase,
9
10
  MULTI_CALL_3_ADDRESS,
@@ -18,34 +19,48 @@ import {
18
19
  type L1BlobInputs,
19
20
  type L1TxConfig,
20
21
  type L1TxRequest,
22
+ type L1TxUtils,
21
23
  MAX_L1_TX_LIMIT,
22
24
  type TransactionStats,
23
25
  WEI_CONST,
24
26
  } from '@aztec/ethereum/l1-tx-utils';
25
- import type { L1TxUtilsWithBlobs } from '@aztec/ethereum/l1-tx-utils-with-blobs';
26
27
  import { FormattedViemError, formatViemError, mergeAbis, tryExtractEvent } from '@aztec/ethereum/utils';
27
28
  import { sumBigint } from '@aztec/foundation/bigint';
28
29
  import { toHex as toPaddedHex } from '@aztec/foundation/bigint-buffer';
29
30
  import { CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
31
+ import { trimmedBytesLength } from '@aztec/foundation/buffer';
30
32
  import { pick } from '@aztec/foundation/collection';
31
33
  import type { Fr } from '@aztec/foundation/curves/bn254';
34
+ import { TimeoutError } from '@aztec/foundation/error';
32
35
  import { EthAddress } from '@aztec/foundation/eth-address';
33
36
  import { Signature, type ViemSignature } from '@aztec/foundation/eth-signature';
34
37
  import { type Logger, createLogger } from '@aztec/foundation/log';
38
+ import { makeBackoff, retry } from '@aztec/foundation/retry';
35
39
  import { bufferToHex } from '@aztec/foundation/string';
36
40
  import { DateProvider, Timer } from '@aztec/foundation/timer';
37
41
  import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
38
42
  import { type ProposerSlashAction, encodeSlashConsensusVotes } from '@aztec/slasher';
39
43
  import { CommitteeAttestationsAndSigners, type ValidateCheckpointResult } from '@aztec/stdlib/block';
40
44
  import type { Checkpoint } from '@aztec/stdlib/checkpoint';
45
+ import { getLastL1SlotTimestampForL2Slot, getNextL1SlotTimestamp } from '@aztec/stdlib/epoch-helpers';
41
46
  import { SlashFactoryContract } from '@aztec/stdlib/l1-contracts';
42
47
  import type { CheckpointHeader } from '@aztec/stdlib/rollup';
43
48
  import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
44
49
  import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
45
50
 
46
- import { type StateOverride, type TransactionReceipt, type TypedDataDefinition, encodeFunctionData, toHex } from 'viem';
47
-
48
- import type { PublisherConfig, TxSenderConfig } from './config.js';
51
+ import {
52
+ type Hex,
53
+ type StateOverride,
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';
49
64
  import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
50
65
 
51
66
  /** Arguments to the process method of the rollup contract */
@@ -60,6 +75,8 @@ type L1ProcessArgs = {
60
75
  attestationsAndSigners: CommitteeAttestationsAndSigners;
61
76
  /** Attestations and signers signature */
62
77
  attestationsAndSignersSignature: Signature;
78
+ /** The fee asset price modifier in basis points (from oracle) */
79
+ feeAssetPriceModifier: bigint;
63
80
  };
64
81
 
65
82
  export const Actions = [
@@ -105,6 +122,7 @@ export class SequencerPublisher {
105
122
  private interrupted = false;
106
123
  private metrics: SequencerPublisherMetrics;
107
124
  public epochCache: EpochCache;
125
+ private failedTxStore?: Promise<L1TxFailedStore | undefined>;
108
126
 
109
127
  protected governanceLog = createLogger('sequencer:publisher:governance');
110
128
  protected slashingLog = createLogger('sequencer:publisher:slashing');
@@ -112,24 +130,34 @@ export class SequencerPublisher {
112
130
  protected lastActions: Partial<Record<Action, SlotNumber>> = {};
113
131
 
114
132
  private isPayloadEmptyCache: Map<string, boolean> = new Map<string, boolean>();
133
+ private payloadProposedCache: Set<string> = new Set<string>();
115
134
 
116
135
  protected log: Logger;
117
136
  protected ethereumSlotDuration: bigint;
137
+ protected aztecSlotDuration: bigint;
138
+ private dateProvider: DateProvider;
118
139
 
119
140
  private blobClient: BlobClientInterface;
120
141
 
121
142
  /** Address to use for simulations in fisherman mode (actual proposer's address) */
122
143
  private proposerAddressForSimulation?: EthAddress;
123
144
 
145
+ /** Optional callback to obtain a replacement publisher when the current one fails to send. */
146
+ private getNextPublisher?: (excludeAddresses: EthAddress[]) => Promise<L1TxUtils | undefined>;
147
+
124
148
  /** L1 fee analyzer for fisherman mode */
125
149
  private l1FeeAnalyzer?: L1FeeAnalyzer;
150
+
151
+ /** Fee asset price oracle for computing price modifiers from Uniswap V4 */
152
+ private feeAssetPriceOracle: FeeAssetPriceOracle;
153
+
126
154
  // A CALL to a cold address is 2700 gas
127
155
  public static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
128
156
 
129
157
  // Gas report for VotingWithSigTest shows a max gas of 100k, but we've seen it cost 700k+ in testnet
130
158
  public static VOTE_GAS_GUESS: bigint = 800_000n;
131
159
 
132
- public l1TxUtils: L1TxUtilsWithBlobs;
160
+ public l1TxUtils: L1TxUtils;
133
161
  public rollupContract: RollupContract;
134
162
  public govProposerContract: GovernanceProposerContract;
135
163
  public slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
@@ -140,11 +168,12 @@ export class SequencerPublisher {
140
168
  protected requests: RequestWithExpiry[] = [];
141
169
 
142
170
  constructor(
143
- private config: TxSenderConfig & PublisherConfig & Pick<L1ContractsConfig, 'ethereumSlotDuration'>,
171
+ private config: Pick<SequencerPublisherConfig, 'fishermanMode' | 'l1TxFailedStore'> &
172
+ Pick<L1ContractsConfig, 'ethereumSlotDuration' | 'aztecSlotDuration'> & { l1ChainId: number },
144
173
  deps: {
145
174
  telemetry?: TelemetryClient;
146
175
  blobClient: BlobClientInterface;
147
- l1TxUtils: L1TxUtilsWithBlobs;
176
+ l1TxUtils: L1TxUtils;
148
177
  rollupContract: RollupContract;
149
178
  slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
150
179
  governanceProposerContract: GovernanceProposerContract;
@@ -154,10 +183,13 @@ export class SequencerPublisher {
154
183
  metrics: SequencerPublisherMetrics;
155
184
  lastActions: Partial<Record<Action, SlotNumber>>;
156
185
  log?: Logger;
186
+ getNextPublisher?: (excludeAddresses: EthAddress[]) => Promise<L1TxUtils | undefined>;
157
187
  },
158
188
  ) {
159
189
  this.log = deps.log ?? createLogger('sequencer:publisher');
160
190
  this.ethereumSlotDuration = BigInt(config.ethereumSlotDuration);
191
+ this.aztecSlotDuration = BigInt(config.aztecSlotDuration);
192
+ this.dateProvider = deps.dateProvider;
161
193
  this.epochCache = deps.epochCache;
162
194
  this.lastActions = deps.lastActions;
163
195
 
@@ -167,6 +199,7 @@ export class SequencerPublisher {
167
199
  this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
168
200
  this.tracer = telemetry.getTracer('SequencerPublisher');
169
201
  this.l1TxUtils = deps.l1TxUtils;
202
+ this.getNextPublisher = deps.getNextPublisher;
170
203
 
171
204
  this.rollupContract = deps.rollupContract;
172
205
 
@@ -188,12 +221,52 @@ export class SequencerPublisher {
188
221
  createLogger('sequencer:publisher:fee-analyzer'),
189
222
  );
190
223
  }
224
+
225
+ // Initialize fee asset price oracle
226
+ this.feeAssetPriceOracle = new FeeAssetPriceOracle(
227
+ this.l1TxUtils.client,
228
+ this.rollupContract,
229
+ createLogger('sequencer:publisher:price-oracle'),
230
+ );
231
+
232
+ // Initialize failed L1 tx store (optional, for test networks)
233
+ this.failedTxStore = createL1TxFailedStore(config.l1TxFailedStore, this.log);
234
+ }
235
+
236
+ /**
237
+ * Backs up a failed L1 transaction to the configured store for debugging.
238
+ * Does nothing if no store is configured.
239
+ */
240
+ private backupFailedTx(failedTx: Omit<FailedL1Tx, 'timestamp'>): void {
241
+ if (!this.failedTxStore) {
242
+ return;
243
+ }
244
+
245
+ const tx: FailedL1Tx = {
246
+ ...failedTx,
247
+ timestamp: Date.now(),
248
+ };
249
+
250
+ // Fire and forget - don't block on backup
251
+ void this.failedTxStore
252
+ .then(store => store?.saveFailedTx(tx))
253
+ .catch(err => {
254
+ this.log.warn(`Failed to backup failed L1 tx to store`, err);
255
+ });
191
256
  }
192
257
 
193
258
  public getRollupContract(): RollupContract {
194
259
  return this.rollupContract;
195
260
  }
196
261
 
262
+ /**
263
+ * Gets the fee asset price modifier from the oracle.
264
+ * Returns 0n if the oracle query fails.
265
+ */
266
+ public getFeeAssetPriceModifier(): Promise<bigint> {
267
+ return this.feeAssetPriceOracle.computePriceModifier();
268
+ }
269
+
197
270
  public getSenderAddress() {
198
271
  return this.l1TxUtils.getSenderAddress();
199
272
  }
@@ -218,7 +291,7 @@ export class SequencerPublisher {
218
291
  }
219
292
 
220
293
  public getCurrentL2Slot(): SlotNumber {
221
- return this.epochCache.getEpochAndSlotNow().slot;
294
+ return this.epochCache.getSlotNow();
222
295
  }
223
296
 
224
297
  /**
@@ -331,8 +404,8 @@ export class SequencerPublisher {
331
404
  // @note - we can only have one blob config per bundle
332
405
  // find requests with gas and blob configs
333
406
  // See https://github.com/AztecProtocol/aztec-packages/issues/11513
334
- const gasConfigs = requestsToProcess.filter(request => request.gasConfig).map(request => request.gasConfig);
335
- const blobConfigs = requestsToProcess.filter(request => request.blobConfig).map(request => request.blobConfig);
407
+ const gasConfigs = validRequests.filter(request => request.gasConfig).map(request => request.gasConfig);
408
+ const blobConfigs = validRequests.filter(request => request.blobConfig).map(request => request.blobConfig);
336
409
 
337
410
  if (blobConfigs.length > 1) {
338
411
  throw new Error('Multiple blob configs found');
@@ -361,19 +434,36 @@ export class SequencerPublisher {
361
434
  validRequests.sort((a, b) => compareActions(a.action, b.action));
362
435
 
363
436
  try {
437
+ // Capture context for failed tx backup before sending
438
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
439
+ const multicallData = encodeFunctionData({
440
+ abi: multicall3Abi,
441
+ functionName: 'aggregate3',
442
+ args: [
443
+ validRequests.map(r => ({
444
+ target: r.request.to!,
445
+ callData: r.request.data!,
446
+ allowFailure: true,
447
+ })),
448
+ ],
449
+ });
450
+ const blobDataHex = blobConfig?.blobs?.map(b => toHex(b)) as Hex[] | undefined;
451
+
452
+ const txContext = { multicallData, blobData: blobDataHex, l1BlockNumber };
453
+
364
454
  this.log.debug('Forwarding transactions', {
365
455
  validRequests: validRequests.map(request => request.action),
366
456
  txConfig,
367
457
  });
368
- const result = await Multicall3.forward(
369
- validRequests.map(request => request.request),
370
- this.l1TxUtils,
371
- txConfig,
372
- blobConfig,
373
- this.rollupContract.address,
374
- this.log,
458
+ const result = await this.forwardWithPublisherRotation(validRequests, txConfig, blobConfig);
459
+ if (result === undefined) {
460
+ return undefined;
461
+ }
462
+ const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(
463
+ validRequests,
464
+ result,
465
+ txContext,
375
466
  );
376
- const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(validRequests, result);
377
467
  return { result, expiredActions, sentActions: validActions, successfulActions, failedActions };
378
468
  } catch (err) {
379
469
  const viemError = formatViemError(err);
@@ -391,16 +481,88 @@ export class SequencerPublisher {
391
481
  }
392
482
  }
393
483
 
484
+ /**
485
+ * Forwards transactions via Multicall3, rotating to the next available publisher if a send
486
+ * failure occurs (i.e. the tx never reached the chain).
487
+ * On-chain reverts and simulation errors are returned as-is without rotation.
488
+ */
489
+ private async forwardWithPublisherRotation(
490
+ validRequests: RequestWithExpiry[],
491
+ txConfig: RequestWithExpiry['gasConfig'],
492
+ blobConfig: L1BlobInputs | undefined,
493
+ ) {
494
+ const triedAddresses: EthAddress[] = [];
495
+ let currentPublisher = this.l1TxUtils;
496
+
497
+ while (true) {
498
+ triedAddresses.push(currentPublisher.getSenderAddress());
499
+ try {
500
+ const result = await Multicall3.forward(
501
+ validRequests.map(r => r.request),
502
+ currentPublisher,
503
+ txConfig,
504
+ blobConfig,
505
+ this.rollupContract.address,
506
+ this.log,
507
+ );
508
+ this.l1TxUtils = currentPublisher;
509
+ return result;
510
+ } catch (err) {
511
+ if (err instanceof TimeoutError) {
512
+ throw err;
513
+ }
514
+ const viemError = formatViemError(err);
515
+ if (!this.getNextPublisher) {
516
+ this.log.error('Failed to publish bundled transactions', viemError);
517
+ return undefined;
518
+ }
519
+ this.log.warn(
520
+ `Publisher ${currentPublisher.getSenderAddress()} failed to send, rotating to next publisher`,
521
+ viemError,
522
+ );
523
+ const nextPublisher = await this.getNextPublisher([...triedAddresses]);
524
+ if (!nextPublisher) {
525
+ this.log.error('All available publishers exhausted, failed to publish bundled transactions');
526
+ return undefined;
527
+ }
528
+ currentPublisher = nextPublisher;
529
+ }
530
+ }
531
+ }
532
+
394
533
  private callbackBundledTransactions(
395
534
  requests: RequestWithExpiry[],
396
- result?: { receipt: TransactionReceipt } | FormattedViemError,
535
+ result: { receipt: TransactionReceipt; errorMsg?: string } | FormattedViemError | undefined,
536
+ txContext: { multicallData: Hex; blobData?: Hex[]; l1BlockNumber: bigint },
397
537
  ) {
398
538
  const actionsListStr = requests.map(r => r.action).join(', ');
399
539
  if (result instanceof FormattedViemError) {
400
540
  this.log.error(`Failed to publish bundled transactions (${actionsListStr})`, result);
541
+ this.backupFailedTx({
542
+ id: keccak256(txContext.multicallData),
543
+ failureType: 'send-error',
544
+ request: { to: MULTI_CALL_3_ADDRESS, data: txContext.multicallData },
545
+ blobData: txContext.blobData,
546
+ l1BlockNumber: txContext.l1BlockNumber.toString(),
547
+ error: { message: result.message, name: result.name },
548
+ context: {
549
+ actions: requests.map(r => r.action),
550
+ requests: requests.map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
551
+ sender: this.getSenderAddress().toString(),
552
+ },
553
+ });
401
554
  return { failedActions: requests.map(r => r.action) };
402
555
  } else {
403
- this.log.verbose(`Published bundled transactions (${actionsListStr})`, { result, requests });
556
+ this.log.verbose(`Published bundled transactions (${actionsListStr})`, {
557
+ result,
558
+ requests: requests.map(r => ({
559
+ ...r,
560
+ // Avoid logging large blob data
561
+ blobConfig: r.blobConfig
562
+ ? { ...r.blobConfig, blobs: r.blobConfig.blobs.map(b => ({ size: trimmedBytesLength(b) })) }
563
+ : undefined,
564
+ })),
565
+ });
404
566
  const successfulActions: Action[] = [];
405
567
  const failedActions: Action[] = [];
406
568
  for (const request of requests) {
@@ -410,25 +572,53 @@ export class SequencerPublisher {
410
572
  failedActions.push(request.action);
411
573
  }
412
574
  }
575
+ // Single backup for the whole reverted tx
576
+ if (failedActions.length > 0 && result?.receipt?.status === 'reverted') {
577
+ this.backupFailedTx({
578
+ id: result.receipt.transactionHash,
579
+ failureType: 'revert',
580
+ request: { to: MULTI_CALL_3_ADDRESS, data: txContext.multicallData },
581
+ blobData: txContext.blobData,
582
+ l1BlockNumber: result.receipt.blockNumber.toString(),
583
+ receipt: {
584
+ transactionHash: result.receipt.transactionHash,
585
+ blockNumber: result.receipt.blockNumber.toString(),
586
+ gasUsed: (result.receipt.gasUsed ?? 0n).toString(),
587
+ status: 'reverted',
588
+ },
589
+ error: { message: result.errorMsg ?? 'Transaction reverted' },
590
+ context: {
591
+ actions: failedActions,
592
+ requests: requests
593
+ .filter(r => failedActions.includes(r.action))
594
+ .map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
595
+ sender: this.getSenderAddress().toString(),
596
+ },
597
+ });
598
+ }
413
599
  return { successfulActions, failedActions };
414
600
  }
415
601
  }
416
602
 
417
603
  /**
418
- * @notice Will call `canProposeAtNextEthBlock` to make sure that it is possible to propose
604
+ * @notice Will call `canProposeAt` to make sure that it is possible to propose
419
605
  * @param tipArchive - The archive to check
420
606
  * @returns The slot and block number if it is possible to propose, undefined otherwise
421
607
  */
422
- public canProposeAtNextEthBlock(
608
+ public canProposeAt(
423
609
  tipArchive: Fr,
424
610
  msgSender: EthAddress,
425
- opts: { forcePendingCheckpointNumber?: CheckpointNumber } = {},
611
+ opts: { forcePendingCheckpointNumber?: CheckpointNumber; pipelined?: boolean } = {},
426
612
  ) {
427
613
  // TODO: #14291 - should loop through multiple keys to check if any of them can propose
428
614
  const ignoredErrors = ['SlotAlreadyInChain', 'InvalidProposer', 'InvalidArchive'];
429
615
 
616
+ const pipelined = opts.pipelined ?? this.epochCache.isProposerPipeliningEnabled();
617
+ const slotOffset = pipelined ? this.aztecSlotDuration : 0n;
618
+ const nextL1SlotTs = this.getNextL1SlotTimestamp() + slotOffset;
619
+
430
620
  return this.rollupContract
431
- .canProposeAtNextEthBlock(tipArchive.toBuffer(), msgSender.toString(), Number(this.ethereumSlotDuration), {
621
+ .canProposeAt(tipArchive.toBuffer(), msgSender.toString(), nextL1SlotTs, {
432
622
  forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
433
623
  })
434
624
  .catch(err => {
@@ -442,6 +632,7 @@ export class SequencerPublisher {
442
632
  return undefined;
443
633
  });
444
634
  }
635
+
445
636
  /**
446
637
  * @notice Will simulate `validateHeader` to make sure that the block header is valid
447
638
  * @dev This is a convenience function that can be used by the sequencer to validate a "partial" header.
@@ -465,7 +656,7 @@ export class SequencerPublisher {
465
656
  flags,
466
657
  ] as const;
467
658
 
468
- const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
659
+ const ts = this.getSimulationTimestamp(header.slotNumber);
469
660
  const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(
470
661
  opts?.forcePendingCheckpointNumber,
471
662
  );
@@ -488,7 +679,7 @@ export class SequencerPublisher {
488
679
  data: encodeFunctionData({ abi: RollupAbi, functionName: 'validateHeaderWithAttestations', args }),
489
680
  from: MULTI_CALL_3_ADDRESS,
490
681
  },
491
- { time: ts + 1n },
682
+ { time: ts },
492
683
  stateOverrides,
493
684
  );
494
685
  this.log.debug(`Simulated validateHeader`);
@@ -521,6 +712,8 @@ export class SequencerPublisher {
521
712
  const request = this.buildInvalidateCheckpointRequest(validationResult);
522
713
  this.log.debug(`Simulating invalidate checkpoint ${checkpointNumber}`, { ...logData, request });
523
714
 
715
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
716
+
524
717
  try {
525
718
  const { gasUsed } = await this.l1TxUtils.simulate(
526
719
  request,
@@ -572,6 +765,18 @@ export class SequencerPublisher {
572
765
 
573
766
  // Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
574
767
  this.log.error(`Simulation for invalidate checkpoint ${checkpointNumber} failed`, viemError, logData);
768
+ this.backupFailedTx({
769
+ id: keccak256(request.data!),
770
+ failureType: 'simulation',
771
+ request: { to: request.to!, data: request.data!, value: request.value?.toString() },
772
+ l1BlockNumber: l1BlockNumber.toString(),
773
+ error: { message: viemError.message, name: viemError.name },
774
+ context: {
775
+ actions: [`invalidate-${reason}`],
776
+ checkpointNumber,
777
+ sender: this.getSenderAddress().toString(),
778
+ },
779
+ });
575
780
  throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, { cause: viemError });
576
781
  }
577
782
  }
@@ -615,26 +820,9 @@ export class SequencerPublisher {
615
820
  attestationsAndSigners: CommitteeAttestationsAndSigners,
616
821
  attestationsAndSignersSignature: Signature,
617
822
  options: { forcePendingCheckpointNumber?: CheckpointNumber },
618
- ): Promise<bigint> {
619
- const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
620
-
621
- // TODO(palla/mbps): This should not be needed, there's no flow where we propose with zero attestations. Or is there?
622
- // If we have no attestations, we still need to provide the empty attestations
623
- // so that the committee is recalculated correctly
624
- // const ignoreSignatures = attestationsAndSigners.attestations.length === 0;
625
- // if (ignoreSignatures) {
626
- // const { committee } = await this.epochCache.getCommittee(block.header.globalVariables.slotNumber);
627
- // if (!committee) {
628
- // this.log.warn(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
629
- // throw new Error(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
630
- // }
631
- // attestationsAndSigners.attestations = committee.map(committeeMember =>
632
- // CommitteeAttestation.fromAddress(committeeMember),
633
- // );
634
- // }
635
-
823
+ ): Promise<void> {
636
824
  const blobFields = checkpoint.toBlobFields();
637
- const blobs = getBlobsPerL1Block(blobFields);
825
+ const blobs = await getBlobsPerL1Block(blobFields);
638
826
  const blobInput = getPrefixedEthBlobCommitments(blobs);
639
827
 
640
828
  const args = [
@@ -642,7 +830,7 @@ export class SequencerPublisher {
642
830
  header: checkpoint.header.toViem(),
643
831
  archive: toHex(checkpoint.archive.root.toBuffer()),
644
832
  oracleInput: {
645
- feeAssetPriceModifier: 0n,
833
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
646
834
  },
647
835
  },
648
836
  attestationsAndSigners.getPackedAttestations(),
@@ -651,13 +839,11 @@ export class SequencerPublisher {
651
839
  blobInput,
652
840
  ] as const;
653
841
 
654
- await this.simulateProposeTx(args, ts, options);
655
- return ts;
842
+ await this.simulateProposeTx(args, options);
656
843
  }
657
844
 
658
845
  private async enqueueCastSignalHelper(
659
846
  slotNumber: SlotNumber,
660
- timestamp: bigint,
661
847
  signalType: GovernanceSignalAction,
662
848
  payload: EthAddress,
663
849
  base: IEmpireBase,
@@ -691,6 +877,32 @@ export class SequencerPublisher {
691
877
  return false;
692
878
  }
693
879
 
880
+ // Check if payload was already submitted to governance
881
+ const cacheKey = payload.toString();
882
+ if (!this.payloadProposedCache.has(cacheKey)) {
883
+ try {
884
+ const l1StartBlock = await this.rollupContract.getL1StartBlock();
885
+ const proposed = await retry(
886
+ () => base.hasPayloadBeenProposed(payload.toString(), l1StartBlock),
887
+ 'Check if payload was proposed',
888
+ makeBackoff([0, 1, 2]),
889
+ this.log,
890
+ true,
891
+ );
892
+ if (proposed) {
893
+ this.payloadProposedCache.add(cacheKey);
894
+ }
895
+ } catch (err) {
896
+ this.log.warn(`Failed to check if payload ${payload} was proposed after retries, skipping signal`, err);
897
+ return false;
898
+ }
899
+ }
900
+
901
+ if (this.payloadProposedCache.has(cacheKey)) {
902
+ this.log.info(`Payload ${payload} was already proposed to governance, stopping signals`);
903
+ return false;
904
+ }
905
+
694
906
  const cachedLastVote = this.lastActions[signalType];
695
907
  this.lastActions[signalType] = slotNumber;
696
908
  const action = signalType;
@@ -709,11 +921,30 @@ export class SequencerPublisher {
709
921
  lastValidL2Slot: slotNumber,
710
922
  });
711
923
 
924
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
925
+ const timestamp = this.getSimulationTimestamp(slotNumber);
926
+
712
927
  try {
713
928
  await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
714
929
  this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
715
930
  } catch (err) {
716
- this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, err);
931
+ const viemError = formatViemError(err);
932
+ this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, viemError, {
933
+ simulationTimestamp: timestamp,
934
+ l1BlockNumber,
935
+ });
936
+ this.backupFailedTx({
937
+ id: keccak256(request.data!),
938
+ failureType: 'simulation',
939
+ request: { to: request.to!, data: request.data!, value: request.value?.toString() },
940
+ l1BlockNumber: l1BlockNumber.toString(),
941
+ error: { message: viemError.message, name: viemError.name },
942
+ context: {
943
+ actions: [action],
944
+ slot: slotNumber,
945
+ sender: this.getSenderAddress().toString(),
946
+ },
947
+ });
717
948
  // Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
718
949
  }
719
950
 
@@ -764,19 +995,16 @@ export class SequencerPublisher {
764
995
  /**
765
996
  * Enqueues a governance castSignal transaction to cast a signal for a given slot number.
766
997
  * @param slotNumber - The slot number to cast a signal for.
767
- * @param timestamp - The timestamp of the slot to cast a signal for.
768
998
  * @returns True if the signal was successfully enqueued, false otherwise.
769
999
  */
770
1000
  public enqueueGovernanceCastSignal(
771
1001
  governancePayload: EthAddress,
772
1002
  slotNumber: SlotNumber,
773
- timestamp: bigint,
774
1003
  signerAddress: EthAddress,
775
1004
  signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
776
1005
  ): Promise<boolean> {
777
1006
  return this.enqueueCastSignalHelper(
778
1007
  slotNumber,
779
- timestamp,
780
1008
  'governance-signal',
781
1009
  governancePayload,
782
1010
  this.govProposerContract,
@@ -789,7 +1017,6 @@ export class SequencerPublisher {
789
1017
  public async enqueueSlashingActions(
790
1018
  actions: ProposerSlashAction[],
791
1019
  slotNumber: SlotNumber,
792
- timestamp: bigint,
793
1020
  signerAddress: EthAddress,
794
1021
  signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
795
1022
  ): Promise<boolean> {
@@ -810,7 +1037,6 @@ export class SequencerPublisher {
810
1037
  });
811
1038
  await this.enqueueCastSignalHelper(
812
1039
  slotNumber,
813
- timestamp,
814
1040
  'empire-slashing-signal',
815
1041
  action.payload,
816
1042
  this.slashingProposerContract,
@@ -829,7 +1055,6 @@ export class SequencerPublisher {
829
1055
  (receipt: TransactionReceipt) =>
830
1056
  !!this.slashFactoryContract.tryExtractSlashPayloadCreatedEvent(receipt.logs),
831
1057
  slotNumber,
832
- timestamp,
833
1058
  );
834
1059
  break;
835
1060
  }
@@ -847,7 +1072,6 @@ export class SequencerPublisher {
847
1072
  request,
848
1073
  (receipt: TransactionReceipt) => !!empireSlashingProposer.tryExtractPayloadSubmittedEvent(receipt.logs),
849
1074
  slotNumber,
850
- timestamp,
851
1075
  );
852
1076
  break;
853
1077
  }
@@ -871,7 +1095,6 @@ export class SequencerPublisher {
871
1095
  request,
872
1096
  (receipt: TransactionReceipt) => !!tallySlashingProposer.tryExtractVoteCastEvent(receipt.logs),
873
1097
  slotNumber,
874
- timestamp,
875
1098
  );
876
1099
  break;
877
1100
  }
@@ -893,7 +1116,6 @@ export class SequencerPublisher {
893
1116
  request,
894
1117
  (receipt: TransactionReceipt) => !!tallySlashingProposer.tryExtractRoundExecutedEvent(receipt.logs),
895
1118
  slotNumber,
896
- timestamp,
897
1119
  );
898
1120
  break;
899
1121
  }
@@ -918,25 +1140,24 @@ export class SequencerPublisher {
918
1140
  const checkpointHeader = checkpoint.header;
919
1141
 
920
1142
  const blobFields = checkpoint.toBlobFields();
921
- const blobs = getBlobsPerL1Block(blobFields);
1143
+ const blobs = await getBlobsPerL1Block(blobFields);
922
1144
 
923
- const proposeTxArgs = {
1145
+ const proposeTxArgs: L1ProcessArgs = {
924
1146
  header: checkpointHeader,
925
1147
  archive: checkpoint.archive.root.toBuffer(),
926
1148
  blobs,
927
1149
  attestationsAndSigners,
928
1150
  attestationsAndSignersSignature,
1151
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
929
1152
  };
930
1153
 
931
- let ts: bigint;
932
-
933
1154
  try {
934
1155
  // @note This will make sure that we are passing the checks for our header ASSUMING that the data is also made available
935
1156
  // This means that we can avoid the simulation issues in later checks.
936
1157
  // By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which
937
1158
  // make time consistency checks break.
938
1159
  // TODO(palla): Check whether we're validating twice, once here and once within addProposeTx, since we call simulateProposeTx in both places.
939
- ts = await this.validateCheckpointForSubmission(
1160
+ await this.validateCheckpointForSubmission(
940
1161
  checkpoint,
941
1162
  attestationsAndSigners,
942
1163
  attestationsAndSignersSignature,
@@ -952,7 +1173,7 @@ export class SequencerPublisher {
952
1173
  }
953
1174
 
954
1175
  this.log.verbose(`Enqueuing checkpoint propose transaction`, { ...checkpoint.toCheckpointInfo(), ...opts });
955
- await this.addProposeTx(checkpoint, proposeTxArgs, opts, ts);
1176
+ await this.addProposeTx(checkpoint, proposeTxArgs, opts);
956
1177
  }
957
1178
 
958
1179
  public enqueueInvalidateCheckpoint(
@@ -995,8 +1216,8 @@ export class SequencerPublisher {
995
1216
  request: L1TxRequest,
996
1217
  checkSuccess: (receipt: TransactionReceipt) => boolean | undefined,
997
1218
  slotNumber: SlotNumber,
998
- timestamp: bigint,
999
1219
  ) {
1220
+ const timestamp = this.getSimulationTimestamp(slotNumber);
1000
1221
  const logData = { slotNumber, timestamp, gasLimit: undefined as bigint | undefined };
1001
1222
  if (this.lastActions[action] && this.lastActions[action] === slotNumber) {
1002
1223
  this.log.debug(`Skipping duplicate action ${action} for slot ${slotNumber}`);
@@ -1008,15 +1229,31 @@ export class SequencerPublisher {
1008
1229
 
1009
1230
  this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
1010
1231
 
1232
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1233
+
1011
1234
  let gasUsed: bigint;
1012
1235
  const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
1236
+
1013
1237
  try {
1014
- ({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], simulateAbi)); // TODO(palla/slash): Check the timestamp logic
1238
+ ({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], simulateAbi));
1015
1239
  this.log.verbose(`Simulation for ${action} succeeded`, { ...logData, request, gasUsed });
1016
1240
  } catch (err) {
1017
1241
  const viemError = formatViemError(err, simulateAbi);
1018
1242
  this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
1019
1243
 
1244
+ this.backupFailedTx({
1245
+ id: keccak256(request.data!),
1246
+ failureType: 'simulation',
1247
+ request: { to: request.to!, data: request.data!, value: request.value?.toString() },
1248
+ l1BlockNumber: l1BlockNumber.toString(),
1249
+ error: { message: viemError.message, name: viemError.name },
1250
+ context: {
1251
+ actions: [action],
1252
+ slot: slotNumber,
1253
+ sender: this.getSenderAddress().toString(),
1254
+ },
1255
+ });
1256
+
1020
1257
  return false;
1021
1258
  }
1022
1259
 
@@ -1067,7 +1304,6 @@ export class SequencerPublisher {
1067
1304
 
1068
1305
  private async prepareProposeTx(
1069
1306
  encodedData: L1ProcessArgs,
1070
- timestamp: bigint,
1071
1307
  options: { forcePendingCheckpointNumber?: CheckpointNumber },
1072
1308
  ) {
1073
1309
  const kzg = Blob.getViemKzgInstance();
@@ -1100,9 +1336,27 @@ export class SequencerPublisher {
1100
1336
  kzg,
1101
1337
  },
1102
1338
  )
1103
- .catch(err => {
1104
- const { message, metaMessages } = formatViemError(err);
1105
- this.log.error(`Failed to validate blobs`, message, { metaMessages });
1339
+ .catch(async err => {
1340
+ const viemError = formatViemError(err);
1341
+ this.log.error(`Failed to validate blobs`, viemError.message, { metaMessages: viemError.metaMessages });
1342
+ const validateBlobsData = encodeFunctionData({
1343
+ abi: RollupAbi,
1344
+ functionName: 'validateBlobs',
1345
+ args: [blobInput],
1346
+ });
1347
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1348
+ this.backupFailedTx({
1349
+ id: keccak256(validateBlobsData),
1350
+ failureType: 'simulation',
1351
+ request: { to: this.rollupContract.address as Hex, data: validateBlobsData },
1352
+ blobData: encodedData.blobs.map(b => toHex(b.data)) as Hex[],
1353
+ l1BlockNumber: l1BlockNumber.toString(),
1354
+ error: { message: viemError.message, name: viemError.name },
1355
+ context: {
1356
+ actions: ['validate-blobs'],
1357
+ sender: this.getSenderAddress().toString(),
1358
+ },
1359
+ });
1106
1360
  throw new Error('Failed to validate blobs');
1107
1361
  });
1108
1362
  }
@@ -1113,8 +1367,7 @@ export class SequencerPublisher {
1113
1367
  header: encodedData.header.toViem(),
1114
1368
  archive: toHex(encodedData.archive),
1115
1369
  oracleInput: {
1116
- // We are currently not modifying these. See #9963
1117
- feeAssetPriceModifier: 0n,
1370
+ feeAssetPriceModifier: encodedData.feeAssetPriceModifier,
1118
1371
  },
1119
1372
  },
1120
1373
  encodedData.attestationsAndSigners.getPackedAttestations(),
@@ -1123,7 +1376,7 @@ export class SequencerPublisher {
1123
1376
  blobInput,
1124
1377
  ] as const;
1125
1378
 
1126
- const { rollupData, simulationResult } = await this.simulateProposeTx(args, timestamp, options);
1379
+ const { rollupData, simulationResult } = await this.simulateProposeTx(args, options);
1127
1380
 
1128
1381
  return { args, blobEvaluationGas, rollupData, simulationResult };
1129
1382
  }
@@ -1131,7 +1384,6 @@ export class SequencerPublisher {
1131
1384
  /**
1132
1385
  * Simulates the propose tx with eth_simulateV1
1133
1386
  * @param args - The propose tx args
1134
- * @param timestamp - The timestamp to simulate proposal at
1135
1387
  * @returns The simulation result
1136
1388
  */
1137
1389
  private async simulateProposeTx(
@@ -1140,7 +1392,7 @@ export class SequencerPublisher {
1140
1392
  readonly header: ViemHeader;
1141
1393
  readonly archive: `0x${string}`;
1142
1394
  readonly oracleInput: {
1143
- readonly feeAssetPriceModifier: 0n;
1395
+ readonly feeAssetPriceModifier: bigint;
1144
1396
  };
1145
1397
  },
1146
1398
  ViemCommitteeAttestations,
@@ -1148,7 +1400,6 @@ export class SequencerPublisher {
1148
1400
  ViemSignature,
1149
1401
  `0x${string}`,
1150
1402
  ],
1151
- timestamp: bigint,
1152
1403
  options: { forcePendingCheckpointNumber?: CheckpointNumber },
1153
1404
  ) {
1154
1405
  const rollupData = encodeFunctionData({
@@ -1182,6 +1433,9 @@ export class SequencerPublisher {
1182
1433
  });
1183
1434
  }
1184
1435
 
1436
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1437
+ const simTs = this.getSimulationTimestamp(SlotNumber.fromBigInt(args[0].header.slotNumber));
1438
+
1185
1439
  const simulationResult = await this.l1TxUtils
1186
1440
  .simulate(
1187
1441
  {
@@ -1191,8 +1445,7 @@ export class SequencerPublisher {
1191
1445
  ...(this.proposerAddressForSimulation && { from: this.proposerAddressForSimulation.toString() }),
1192
1446
  },
1193
1447
  {
1194
- // @note we add 1n to the timestamp because geth implementation doesn't like simulation timestamp to be equal to the current block timestamp
1195
- time: timestamp + 1n,
1448
+ time: simTs,
1196
1449
  // @note reth should have a 30m gas limit per block but throws errors that this tx is beyond limit so we increase here
1197
1450
  gasLimit: MAX_L1_TX_LIMIT * 2n,
1198
1451
  },
@@ -1214,7 +1467,19 @@ export class SequencerPublisher {
1214
1467
  logs: [],
1215
1468
  };
1216
1469
  }
1217
- this.log.error(`Failed to simulate propose tx`, viemError);
1470
+ this.log.error(`Failed to simulate propose tx`, viemError, { simulationTimestamp: simTs });
1471
+ this.backupFailedTx({
1472
+ id: keccak256(rollupData),
1473
+ failureType: 'simulation',
1474
+ request: { to: this.rollupContract.address, data: rollupData },
1475
+ l1BlockNumber: l1BlockNumber.toString(),
1476
+ error: { message: viemError.message, name: viemError.name },
1477
+ context: {
1478
+ actions: ['propose'],
1479
+ slot: Number(args[0].header.slotNumber),
1480
+ sender: this.getSenderAddress().toString(),
1481
+ },
1482
+ });
1218
1483
  throw err;
1219
1484
  });
1220
1485
 
@@ -1225,16 +1490,11 @@ export class SequencerPublisher {
1225
1490
  checkpoint: Checkpoint,
1226
1491
  encodedData: L1ProcessArgs,
1227
1492
  opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
1228
- timestamp: bigint,
1229
1493
  ): Promise<void> {
1230
1494
  const slot = checkpoint.header.slotNumber;
1231
1495
  const timer = new Timer();
1232
1496
  const kzg = Blob.getViemKzgInstance();
1233
- const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(
1234
- encodedData,
1235
- timestamp,
1236
- opts,
1237
- );
1497
+ const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(encodedData, opts);
1238
1498
  const startBlock = await this.l1TxUtils.getBlockNumber();
1239
1499
  const gasLimit = this.l1TxUtils.bumpGasLimit(
1240
1500
  BigInt(Math.ceil((Number(simulationResult.gasUsed) * 64) / 63)) +
@@ -1310,4 +1570,17 @@ export class SequencerPublisher {
1310
1570
  },
1311
1571
  });
1312
1572
  }
1573
+
1574
+ /** Returns the timestamp of the last L1 slot within a given L2 slot. Used as the simulation timestamp
1575
+ * for eth_simulateV1 calls, since it's guaranteed to be greater than any L1 block produced during the slot. */
1576
+ private getSimulationTimestamp(slot: SlotNumber): bigint {
1577
+ const l1Constants = this.epochCache.getL1Constants();
1578
+ return getLastL1SlotTimestampForL2Slot(slot, l1Constants);
1579
+ }
1580
+
1581
+ /** Returns the timestamp of the next L1 slot boundary after now. */
1582
+ private getNextL1SlotTimestamp(): bigint {
1583
+ const l1Constants = this.epochCache.getL1Constants();
1584
+ return getNextL1SlotTimestamp(this.dateProvider.nowInSeconds(), l1Constants);
1585
+ }
1313
1586
  }