@aztec/sequencer-client 0.0.1-commit.e3c1de76 → 0.0.1-commit.e588bc7e5

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 +15 -11
  8. package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
  9. package/dest/global_variable_builder/global_builder.js +29 -25
  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 +76 -30
  33. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  34. package/dest/publisher/sequencer-publisher.js +396 -71
  35. package/dest/sequencer/checkpoint_proposal_job.d.ts +39 -8
  36. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  37. package/dest/sequencer/checkpoint_proposal_job.js +368 -196
  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 +42 -17
  47. package/dest/sequencer/sequencer.d.ts.map +1 -1
  48. package/dest/sequencer/sequencer.js +147 -89
  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 +38 -27
  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 +442 -95
  75. package/src/sequencer/checkpoint_proposal_job.ts +481 -202
  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 +212 -105
  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,8 @@ 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,
8
+ type FeeHeader,
7
9
  type GovernanceProposerContract,
8
10
  type IEmpireBase,
9
11
  MULTI_CALL_3_ADDRESS,
@@ -18,36 +20,65 @@ import {
18
20
  type L1BlobInputs,
19
21
  type L1TxConfig,
20
22
  type L1TxRequest,
23
+ type L1TxUtils,
21
24
  MAX_L1_TX_LIMIT,
22
25
  type TransactionStats,
23
26
  WEI_CONST,
24
27
  } from '@aztec/ethereum/l1-tx-utils';
25
- import type { L1TxUtilsWithBlobs } from '@aztec/ethereum/l1-tx-utils-with-blobs';
26
28
  import { FormattedViemError, formatViemError, mergeAbis, tryExtractEvent } from '@aztec/ethereum/utils';
27
29
  import { sumBigint } from '@aztec/foundation/bigint';
28
30
  import { toHex as toPaddedHex } from '@aztec/foundation/bigint-buffer';
29
31
  import { CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
32
+ import { trimmedBytesLength } from '@aztec/foundation/buffer';
30
33
  import { pick } from '@aztec/foundation/collection';
31
34
  import type { Fr } from '@aztec/foundation/curves/bn254';
35
+ import { TimeoutError } from '@aztec/foundation/error';
32
36
  import { EthAddress } from '@aztec/foundation/eth-address';
33
37
  import { Signature, type ViemSignature } from '@aztec/foundation/eth-signature';
34
38
  import { type Logger, createLogger } from '@aztec/foundation/log';
39
+ import { makeBackoff, retry } from '@aztec/foundation/retry';
40
+ import { InterruptibleSleep } from '@aztec/foundation/sleep';
35
41
  import { bufferToHex } from '@aztec/foundation/string';
36
- import { DateProvider, Timer } from '@aztec/foundation/timer';
42
+ import { type DateProvider, Timer } from '@aztec/foundation/timer';
37
43
  import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
38
44
  import { type ProposerSlashAction, encodeSlashConsensusVotes } from '@aztec/slasher';
39
45
  import { CommitteeAttestationsAndSigners, type ValidateCheckpointResult } from '@aztec/stdlib/block';
40
46
  import type { Checkpoint } from '@aztec/stdlib/checkpoint';
47
+ import { getLastL1SlotTimestampForL2Slot, getNextL1SlotTimestamp } from '@aztec/stdlib/epoch-helpers';
41
48
  import { SlashFactoryContract } from '@aztec/stdlib/l1-contracts';
42
49
  import type { CheckpointHeader } from '@aztec/stdlib/rollup';
43
50
  import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
44
51
  import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
45
52
 
46
- import { type StateOverride, type TransactionReceipt, type TypedDataDefinition, encodeFunctionData, toHex } from 'viem';
47
-
48
- import type { PublisherConfig, TxSenderConfig } from './config.js';
53
+ import {
54
+ type Hex,
55
+ type StateOverride,
56
+ type TransactionReceipt,
57
+ type TypedDataDefinition,
58
+ encodeFunctionData,
59
+ keccak256,
60
+ multicall3Abi,
61
+ toHex,
62
+ } from 'viem';
63
+
64
+ import type { SequencerPublisherConfig } from './config.js';
65
+ import { type FailedL1Tx, type L1TxFailedStore, createL1TxFailedStore } from './l1_tx_failed_store/index.js';
49
66
  import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
50
67
 
68
+ /** Result of a sendRequests call, returned by both sendRequests() and sendRequestsAt(). */
69
+ export type SendRequestsResult = {
70
+ /** The L1 transaction receipt or error from the bundled multicall. */
71
+ result: { receipt: TransactionReceipt; errorMsg?: string } | FormattedViemError;
72
+ /** Actions that expired (past their deadline) before the request was sent. */
73
+ expiredActions: Action[];
74
+ /** Actions that were included in the sent L1 transaction. */
75
+ sentActions: Action[];
76
+ /** Actions whose L1 simulation succeeded (subset of sentActions). */
77
+ successfulActions: Action[];
78
+ /** Actions whose L1 simulation failed (subset of sentActions). */
79
+ failedActions: Action[];
80
+ };
81
+
51
82
  /** Arguments to the process method of the rollup contract */
52
83
  type L1ProcessArgs = {
53
84
  /** The L2 block header. */
@@ -60,6 +91,8 @@ type L1ProcessArgs = {
60
91
  attestationsAndSigners: CommitteeAttestationsAndSigners;
61
92
  /** Attestations and signers signature */
62
93
  attestationsAndSignersSignature: Signature;
94
+ /** The fee asset price modifier in basis points (from oracle) */
95
+ feeAssetPriceModifier: bigint;
63
96
  };
64
97
 
65
98
  export const Actions = [
@@ -87,6 +120,8 @@ export type InvalidateCheckpointRequest = {
87
120
  gasUsed: bigint;
88
121
  checkpointNumber: CheckpointNumber;
89
122
  forcePendingCheckpointNumber: CheckpointNumber;
123
+ /** Archive at the rollback target checkpoint (checkpoint N-1). */
124
+ lastArchive: Fr;
90
125
  };
91
126
 
92
127
  interface RequestWithExpiry {
@@ -105,6 +140,7 @@ export class SequencerPublisher {
105
140
  private interrupted = false;
106
141
  private metrics: SequencerPublisherMetrics;
107
142
  public epochCache: EpochCache;
143
+ private failedTxStore?: Promise<L1TxFailedStore | undefined>;
108
144
 
109
145
  protected governanceLog = createLogger('sequencer:publisher:governance');
110
146
  protected slashingLog = createLogger('sequencer:publisher:slashing');
@@ -112,24 +148,39 @@ export class SequencerPublisher {
112
148
  protected lastActions: Partial<Record<Action, SlotNumber>> = {};
113
149
 
114
150
  private isPayloadEmptyCache: Map<string, boolean> = new Map<string, boolean>();
151
+ private payloadProposedCache: Set<string> = new Set<string>();
115
152
 
116
153
  protected log: Logger;
117
154
  protected ethereumSlotDuration: bigint;
155
+ protected aztecSlotDuration: bigint;
156
+
157
+ /** Date provider for wall-clock time. */
158
+ private readonly dateProvider: DateProvider;
118
159
 
119
160
  private blobClient: BlobClientInterface;
120
161
 
121
162
  /** Address to use for simulations in fisherman mode (actual proposer's address) */
122
163
  private proposerAddressForSimulation?: EthAddress;
123
164
 
165
+ /** Optional callback to obtain a replacement publisher when the current one fails to send. */
166
+ private getNextPublisher?: (excludeAddresses: EthAddress[]) => Promise<L1TxUtils | undefined>;
167
+
124
168
  /** L1 fee analyzer for fisherman mode */
125
169
  private l1FeeAnalyzer?: L1FeeAnalyzer;
170
+
171
+ /** Fee asset price oracle for computing price modifiers from Uniswap V4 */
172
+ private feeAssetPriceOracle: FeeAssetPriceOracle;
173
+
174
+ /** Interruptible sleep used by sendRequestsAt to wait until a target timestamp. */
175
+ private readonly interruptibleSleep = new InterruptibleSleep();
176
+
126
177
  // A CALL to a cold address is 2700 gas
127
178
  public static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
128
179
 
129
180
  // Gas report for VotingWithSigTest shows a max gas of 100k, but we've seen it cost 700k+ in testnet
130
181
  public static VOTE_GAS_GUESS: bigint = 800_000n;
131
182
 
132
- public l1TxUtils: L1TxUtilsWithBlobs;
183
+ public l1TxUtils: L1TxUtils;
133
184
  public rollupContract: RollupContract;
134
185
  public govProposerContract: GovernanceProposerContract;
135
186
  public slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
@@ -140,11 +191,12 @@ export class SequencerPublisher {
140
191
  protected requests: RequestWithExpiry[] = [];
141
192
 
142
193
  constructor(
143
- private config: TxSenderConfig & PublisherConfig & Pick<L1ContractsConfig, 'ethereumSlotDuration'>,
194
+ private config: Pick<SequencerPublisherConfig, 'fishermanMode' | 'l1TxFailedStore'> &
195
+ Pick<L1ContractsConfig, 'ethereumSlotDuration' | 'aztecSlotDuration'> & { l1ChainId: number },
144
196
  deps: {
145
197
  telemetry?: TelemetryClient;
146
198
  blobClient: BlobClientInterface;
147
- l1TxUtils: L1TxUtilsWithBlobs;
199
+ l1TxUtils: L1TxUtils;
148
200
  rollupContract: RollupContract;
149
201
  slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
150
202
  governanceProposerContract: GovernanceProposerContract;
@@ -154,19 +206,24 @@ export class SequencerPublisher {
154
206
  metrics: SequencerPublisherMetrics;
155
207
  lastActions: Partial<Record<Action, SlotNumber>>;
156
208
  log?: Logger;
209
+ getNextPublisher?: (excludeAddresses: EthAddress[]) => Promise<L1TxUtils | undefined>;
157
210
  },
158
211
  ) {
159
212
  this.log = deps.log ?? createLogger('sequencer:publisher');
160
213
  this.ethereumSlotDuration = BigInt(config.ethereumSlotDuration);
214
+ this.aztecSlotDuration = BigInt(config.aztecSlotDuration);
215
+ this.dateProvider = deps.dateProvider;
161
216
  this.epochCache = deps.epochCache;
162
217
  this.lastActions = deps.lastActions;
163
218
 
164
219
  this.blobClient = deps.blobClient;
220
+ this.dateProvider = deps.dateProvider;
165
221
 
166
222
  const telemetry = deps.telemetry ?? getTelemetryClient();
167
223
  this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
168
224
  this.tracer = telemetry.getTracer('SequencerPublisher');
169
225
  this.l1TxUtils = deps.l1TxUtils;
226
+ this.getNextPublisher = deps.getNextPublisher;
170
227
 
171
228
  this.rollupContract = deps.rollupContract;
172
229
 
@@ -188,12 +245,52 @@ export class SequencerPublisher {
188
245
  createLogger('sequencer:publisher:fee-analyzer'),
189
246
  );
190
247
  }
248
+
249
+ // Initialize fee asset price oracle
250
+ this.feeAssetPriceOracle = new FeeAssetPriceOracle(
251
+ this.l1TxUtils.client,
252
+ this.rollupContract,
253
+ createLogger('sequencer:publisher:price-oracle'),
254
+ );
255
+
256
+ // Initialize failed L1 tx store (optional, for test networks)
257
+ this.failedTxStore = createL1TxFailedStore(config.l1TxFailedStore, this.log);
258
+ }
259
+
260
+ /**
261
+ * Backs up a failed L1 transaction to the configured store for debugging.
262
+ * Does nothing if no store is configured.
263
+ */
264
+ private backupFailedTx(failedTx: Omit<FailedL1Tx, 'timestamp'>): void {
265
+ if (!this.failedTxStore) {
266
+ return;
267
+ }
268
+
269
+ const tx: FailedL1Tx = {
270
+ ...failedTx,
271
+ timestamp: Date.now(),
272
+ };
273
+
274
+ // Fire and forget - don't block on backup
275
+ void this.failedTxStore
276
+ .then(store => store?.saveFailedTx(tx))
277
+ .catch(err => {
278
+ this.log.warn(`Failed to backup failed L1 tx to store`, err);
279
+ });
191
280
  }
192
281
 
193
282
  public getRollupContract(): RollupContract {
194
283
  return this.rollupContract;
195
284
  }
196
285
 
286
+ /**
287
+ * Gets the fee asset price modifier from the oracle.
288
+ * Returns 0n if the oracle query fails.
289
+ */
290
+ public getFeeAssetPriceModifier(): Promise<bigint> {
291
+ return this.feeAssetPriceOracle.computePriceModifier();
292
+ }
293
+
197
294
  public getSenderAddress() {
198
295
  return this.l1TxUtils.getSenderAddress();
199
296
  }
@@ -218,7 +315,7 @@ export class SequencerPublisher {
218
315
  }
219
316
 
220
317
  public getCurrentL2Slot(): SlotNumber {
221
- return this.epochCache.getEpochAndSlotNow().slot;
318
+ return this.epochCache.getSlotNow();
222
319
  }
223
320
 
224
321
  /**
@@ -296,9 +393,10 @@ export class SequencerPublisher {
296
393
  * - undefined if no valid requests are found OR the tx failed to send.
297
394
  */
298
395
  @trackSpan('SequencerPublisher.sendRequests')
299
- public async sendRequests() {
396
+ public async sendRequests(): Promise<SendRequestsResult | undefined> {
300
397
  const requestsToProcess = [...this.requests];
301
398
  this.requests = [];
399
+
302
400
  if (this.interrupted || requestsToProcess.length === 0) {
303
401
  return undefined;
304
402
  }
@@ -331,8 +429,8 @@ export class SequencerPublisher {
331
429
  // @note - we can only have one blob config per bundle
332
430
  // find requests with gas and blob configs
333
431
  // 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);
432
+ const gasConfigs = validRequests.filter(request => request.gasConfig).map(request => request.gasConfig);
433
+ const blobConfigs = validRequests.filter(request => request.blobConfig).map(request => request.blobConfig);
336
434
 
337
435
  if (blobConfigs.length > 1) {
338
436
  throw new Error('Multiple blob configs found');
@@ -361,19 +459,36 @@ export class SequencerPublisher {
361
459
  validRequests.sort((a, b) => compareActions(a.action, b.action));
362
460
 
363
461
  try {
462
+ // Capture context for failed tx backup before sending
463
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
464
+ const multicallData = encodeFunctionData({
465
+ abi: multicall3Abi,
466
+ functionName: 'aggregate3',
467
+ args: [
468
+ validRequests.map(r => ({
469
+ target: r.request.to!,
470
+ callData: r.request.data!,
471
+ allowFailure: true,
472
+ })),
473
+ ],
474
+ });
475
+ const blobDataHex = blobConfig?.blobs?.map(b => toHex(b)) as Hex[] | undefined;
476
+
477
+ const txContext = { multicallData, blobData: blobDataHex, l1BlockNumber };
478
+
364
479
  this.log.debug('Forwarding transactions', {
365
480
  validRequests: validRequests.map(request => request.action),
366
481
  txConfig,
367
482
  });
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,
483
+ const result = await this.forwardWithPublisherRotation(validRequests, txConfig, blobConfig);
484
+ if (result === undefined) {
485
+ return undefined;
486
+ }
487
+ const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(
488
+ validRequests,
489
+ result,
490
+ txContext,
375
491
  );
376
- const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(validRequests, result);
377
492
  return { result, expiredActions, sentActions: validActions, successfulActions, failedActions };
378
493
  } catch (err) {
379
494
  const viemError = formatViemError(err);
@@ -391,16 +506,105 @@ export class SequencerPublisher {
391
506
  }
392
507
  }
393
508
 
509
+ /**
510
+ * Forwards transactions via Multicall3, rotating to the next available publisher if a send
511
+ * failure occurs (i.e. the tx never reached the chain).
512
+ * On-chain reverts and simulation errors are returned as-is without rotation.
513
+ */
514
+ private async forwardWithPublisherRotation(
515
+ validRequests: RequestWithExpiry[],
516
+ txConfig: RequestWithExpiry['gasConfig'],
517
+ blobConfig: L1BlobInputs | undefined,
518
+ ) {
519
+ const triedAddresses: EthAddress[] = [];
520
+ let currentPublisher = this.l1TxUtils;
521
+
522
+ while (true) {
523
+ triedAddresses.push(currentPublisher.getSenderAddress());
524
+ try {
525
+ const result = await Multicall3.forward(
526
+ validRequests.map(r => r.request),
527
+ currentPublisher,
528
+ txConfig,
529
+ blobConfig,
530
+ this.rollupContract.address,
531
+ this.log,
532
+ );
533
+ this.l1TxUtils = currentPublisher;
534
+ return result;
535
+ } catch (err) {
536
+ if (err instanceof TimeoutError) {
537
+ throw err;
538
+ }
539
+ const viemError = formatViemError(err);
540
+ if (!this.getNextPublisher) {
541
+ this.log.error('Failed to publish bundled transactions', viemError);
542
+ return undefined;
543
+ }
544
+ this.log.warn(
545
+ `Publisher ${currentPublisher.getSenderAddress()} failed to send, rotating to next publisher`,
546
+ viemError,
547
+ );
548
+ const nextPublisher = await this.getNextPublisher([...triedAddresses]);
549
+ if (!nextPublisher) {
550
+ this.log.error('All available publishers exhausted, failed to publish bundled transactions');
551
+ return undefined;
552
+ }
553
+ currentPublisher = nextPublisher;
554
+ }
555
+ }
556
+ }
557
+
558
+ /*
559
+ * Schedules sending all enqueued requests at (or after) the given timestamp.
560
+ * Uses InterruptibleSleep so it can be cancelled via interrupt().
561
+ * Returns the promise for the L1 response (caller should NOT await this in the work loop).
562
+ */
563
+ public async sendRequestsAt(submitAfter: Date): Promise<SendRequestsResult | undefined> {
564
+ const ms = submitAfter.getTime() - this.dateProvider.now();
565
+ if (ms > 0) {
566
+ this.log.debug(`Sleeping ${ms}ms before sending requests`, { submitAfter });
567
+ await this.interruptibleSleep.sleep(ms);
568
+ }
569
+ if (this.interrupted) {
570
+ return undefined;
571
+ }
572
+ return this.sendRequests();
573
+ }
574
+
394
575
  private callbackBundledTransactions(
395
576
  requests: RequestWithExpiry[],
396
- result?: { receipt: TransactionReceipt } | FormattedViemError,
577
+ result: { receipt: TransactionReceipt; errorMsg?: string } | FormattedViemError | undefined,
578
+ txContext: { multicallData: Hex; blobData?: Hex[]; l1BlockNumber: bigint },
397
579
  ) {
398
580
  const actionsListStr = requests.map(r => r.action).join(', ');
399
581
  if (result instanceof FormattedViemError) {
400
582
  this.log.error(`Failed to publish bundled transactions (${actionsListStr})`, result);
583
+ this.backupFailedTx({
584
+ id: keccak256(txContext.multicallData),
585
+ failureType: 'send-error',
586
+ request: { to: MULTI_CALL_3_ADDRESS, data: txContext.multicallData },
587
+ blobData: txContext.blobData,
588
+ l1BlockNumber: txContext.l1BlockNumber.toString(),
589
+ error: { message: result.message, name: result.name },
590
+ context: {
591
+ actions: requests.map(r => r.action),
592
+ requests: requests.map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
593
+ sender: this.getSenderAddress().toString(),
594
+ },
595
+ });
401
596
  return { failedActions: requests.map(r => r.action) };
402
597
  } else {
403
- this.log.verbose(`Published bundled transactions (${actionsListStr})`, { result, requests });
598
+ this.log.verbose(`Published bundled transactions (${actionsListStr})`, {
599
+ result,
600
+ requests: requests.map(r => ({
601
+ ...r,
602
+ // Avoid logging large blob data
603
+ blobConfig: r.blobConfig
604
+ ? { ...r.blobConfig, blobs: r.blobConfig.blobs.map(b => ({ size: trimmedBytesLength(b) })) }
605
+ : undefined,
606
+ })),
607
+ });
404
608
  const successfulActions: Action[] = [];
405
609
  const failedActions: Action[] = [];
406
610
  for (const request of requests) {
@@ -410,26 +614,59 @@ export class SequencerPublisher {
410
614
  failedActions.push(request.action);
411
615
  }
412
616
  }
617
+ // Single backup for the whole reverted tx
618
+ if (failedActions.length > 0 && result?.receipt?.status === 'reverted') {
619
+ this.backupFailedTx({
620
+ id: result.receipt.transactionHash,
621
+ failureType: 'revert',
622
+ request: { to: MULTI_CALL_3_ADDRESS, data: txContext.multicallData },
623
+ blobData: txContext.blobData,
624
+ l1BlockNumber: result.receipt.blockNumber.toString(),
625
+ receipt: {
626
+ transactionHash: result.receipt.transactionHash,
627
+ blockNumber: result.receipt.blockNumber.toString(),
628
+ gasUsed: (result.receipt.gasUsed ?? 0n).toString(),
629
+ status: 'reverted',
630
+ },
631
+ error: { message: result.errorMsg ?? 'Transaction reverted' },
632
+ context: {
633
+ actions: failedActions,
634
+ requests: requests
635
+ .filter(r => failedActions.includes(r.action))
636
+ .map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
637
+ sender: this.getSenderAddress().toString(),
638
+ },
639
+ });
640
+ }
413
641
  return { successfulActions, failedActions };
414
642
  }
415
643
  }
416
644
 
417
645
  /**
418
- * @notice Will call `canProposeAtNextEthBlock` to make sure that it is possible to propose
646
+ * @notice Will call `canProposeAt` to make sure that it is possible to propose
419
647
  * @param tipArchive - The archive to check
420
648
  * @returns The slot and block number if it is possible to propose, undefined otherwise
421
649
  */
422
- public canProposeAtNextEthBlock(
650
+ public canProposeAt(
423
651
  tipArchive: Fr,
424
652
  msgSender: EthAddress,
425
- opts: { forcePendingCheckpointNumber?: CheckpointNumber } = {},
653
+ opts: {
654
+ forcePendingCheckpointNumber?: CheckpointNumber;
655
+ forceArchive?: { checkpointNumber: CheckpointNumber; archive: Fr };
656
+ pipelined?: boolean;
657
+ } = {},
426
658
  ) {
427
659
  // TODO: #14291 - should loop through multiple keys to check if any of them can propose
428
660
  const ignoredErrors = ['SlotAlreadyInChain', 'InvalidProposer', 'InvalidArchive'];
429
661
 
662
+ const pipelined = opts.pipelined ?? this.epochCache.isProposerPipeliningEnabled();
663
+ const slotOffset = pipelined ? this.aztecSlotDuration : 0n;
664
+ const nextL1SlotTs = this.getNextL1SlotTimestamp() + slotOffset;
665
+
430
666
  return this.rollupContract
431
- .canProposeAtNextEthBlock(tipArchive.toBuffer(), msgSender.toString(), Number(this.ethereumSlotDuration), {
667
+ .canProposeAt(tipArchive.toBuffer(), msgSender.toString(), nextL1SlotTs, {
432
668
  forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
669
+ forceArchive: opts.forceArchive,
433
670
  })
434
671
  .catch(err => {
435
672
  if (err instanceof FormattedViemError && ignoredErrors.find(e => err.message.includes(e))) {
@@ -442,6 +679,7 @@ export class SequencerPublisher {
442
679
  return undefined;
443
680
  });
444
681
  }
682
+
445
683
  /**
446
684
  * @notice Will simulate `validateHeader` to make sure that the block header is valid
447
685
  * @dev This is a convenience function that can be used by the sequencer to validate a "partial" header.
@@ -465,7 +703,7 @@ export class SequencerPublisher {
465
703
  flags,
466
704
  ] as const;
467
705
 
468
- const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
706
+ const ts = this.getSimulationTimestamp(header.slotNumber);
469
707
  const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(
470
708
  opts?.forcePendingCheckpointNumber,
471
709
  );
@@ -488,7 +726,7 @@ export class SequencerPublisher {
488
726
  data: encodeFunctionData({ abi: RollupAbi, functionName: 'validateHeaderWithAttestations', args }),
489
727
  from: MULTI_CALL_3_ADDRESS,
490
728
  },
491
- { time: ts + 1n },
729
+ { time: ts },
492
730
  stateOverrides,
493
731
  );
494
732
  this.log.debug(`Simulated validateHeader`);
@@ -521,6 +759,8 @@ export class SequencerPublisher {
521
759
  const request = this.buildInvalidateCheckpointRequest(validationResult);
522
760
  this.log.debug(`Simulating invalidate checkpoint ${checkpointNumber}`, { ...logData, request });
523
761
 
762
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
763
+
524
764
  try {
525
765
  const { gasUsed } = await this.l1TxUtils.simulate(
526
766
  request,
@@ -539,6 +779,7 @@ export class SequencerPublisher {
539
779
  gasUsed,
540
780
  checkpointNumber,
541
781
  forcePendingCheckpointNumber: CheckpointNumber(checkpointNumber - 1),
782
+ lastArchive: validationResult.checkpoint.lastArchive,
542
783
  reason,
543
784
  };
544
785
  } catch (err) {
@@ -551,8 +792,8 @@ export class SequencerPublisher {
551
792
  `Simulation for invalidate checkpoint ${checkpointNumber} failed due to checkpoint not being in pending chain`,
552
793
  { ...logData, request, error: viemError.message },
553
794
  );
554
- const latestPendingCheckpointNumber = await this.rollupContract.getCheckpointNumber();
555
- if (latestPendingCheckpointNumber < checkpointNumber) {
795
+ const latestProposedCheckpointNumber = await this.rollupContract.getCheckpointNumber();
796
+ if (latestProposedCheckpointNumber < checkpointNumber) {
556
797
  this.log.verbose(`Checkpoint ${checkpointNumber} has already been invalidated`, { ...logData });
557
798
  return undefined;
558
799
  } else {
@@ -572,6 +813,18 @@ export class SequencerPublisher {
572
813
 
573
814
  // Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
574
815
  this.log.error(`Simulation for invalidate checkpoint ${checkpointNumber} failed`, viemError, logData);
816
+ this.backupFailedTx({
817
+ id: keccak256(request.data!),
818
+ failureType: 'simulation',
819
+ request: { to: request.to!, data: request.data!, value: request.value?.toString() },
820
+ l1BlockNumber: l1BlockNumber.toString(),
821
+ error: { message: viemError.message, name: viemError.name },
822
+ context: {
823
+ actions: [`invalidate-${reason}`],
824
+ checkpointNumber,
825
+ sender: this.getSenderAddress().toString(),
826
+ },
827
+ });
575
828
  throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, { cause: viemError });
576
829
  }
577
830
  }
@@ -614,27 +867,13 @@ export class SequencerPublisher {
614
867
  checkpoint: Checkpoint,
615
868
  attestationsAndSigners: CommitteeAttestationsAndSigners,
616
869
  attestationsAndSignersSignature: Signature,
617
- 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
-
870
+ options: {
871
+ forcePendingCheckpointNumber?: CheckpointNumber;
872
+ forceProposedFeeHeader?: { checkpointNumber: CheckpointNumber; feeHeader: FeeHeader };
873
+ },
874
+ ): Promise<void> {
636
875
  const blobFields = checkpoint.toBlobFields();
637
- const blobs = getBlobsPerL1Block(blobFields);
876
+ const blobs = await getBlobsPerL1Block(blobFields);
638
877
  const blobInput = getPrefixedEthBlobCommitments(blobs);
639
878
 
640
879
  const args = [
@@ -642,7 +881,7 @@ export class SequencerPublisher {
642
881
  header: checkpoint.header.toViem(),
643
882
  archive: toHex(checkpoint.archive.root.toBuffer()),
644
883
  oracleInput: {
645
- feeAssetPriceModifier: 0n,
884
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
646
885
  },
647
886
  },
648
887
  attestationsAndSigners.getPackedAttestations(),
@@ -651,13 +890,11 @@ export class SequencerPublisher {
651
890
  blobInput,
652
891
  ] as const;
653
892
 
654
- await this.simulateProposeTx(args, ts, options);
655
- return ts;
893
+ await this.simulateProposeTx(args, options);
656
894
  }
657
895
 
658
896
  private async enqueueCastSignalHelper(
659
897
  slotNumber: SlotNumber,
660
- timestamp: bigint,
661
898
  signalType: GovernanceSignalAction,
662
899
  payload: EthAddress,
663
900
  base: IEmpireBase,
@@ -691,6 +928,32 @@ export class SequencerPublisher {
691
928
  return false;
692
929
  }
693
930
 
931
+ // Check if payload was already submitted to governance
932
+ const cacheKey = payload.toString();
933
+ if (!this.payloadProposedCache.has(cacheKey)) {
934
+ try {
935
+ const l1StartBlock = await this.rollupContract.getL1StartBlock();
936
+ const proposed = await retry(
937
+ () => base.hasPayloadBeenProposed(payload.toString(), l1StartBlock),
938
+ 'Check if payload was proposed',
939
+ makeBackoff([0, 1, 2]),
940
+ this.log,
941
+ true,
942
+ );
943
+ if (proposed) {
944
+ this.payloadProposedCache.add(cacheKey);
945
+ }
946
+ } catch (err) {
947
+ this.log.warn(`Failed to check if payload ${payload} was proposed after retries, skipping signal`, err);
948
+ return false;
949
+ }
950
+ }
951
+
952
+ if (this.payloadProposedCache.has(cacheKey)) {
953
+ this.log.info(`Payload ${payload} was already proposed to governance, stopping signals`);
954
+ return false;
955
+ }
956
+
694
957
  const cachedLastVote = this.lastActions[signalType];
695
958
  this.lastActions[signalType] = slotNumber;
696
959
  const action = signalType;
@@ -709,11 +972,30 @@ export class SequencerPublisher {
709
972
  lastValidL2Slot: slotNumber,
710
973
  });
711
974
 
975
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
976
+ const timestamp = this.getSimulationTimestamp(slotNumber);
977
+
712
978
  try {
713
979
  await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
714
980
  this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
715
981
  } catch (err) {
716
- this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, err);
982
+ const viemError = formatViemError(err);
983
+ this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, viemError, {
984
+ simulationTimestamp: timestamp,
985
+ l1BlockNumber,
986
+ });
987
+ this.backupFailedTx({
988
+ id: keccak256(request.data!),
989
+ failureType: 'simulation',
990
+ request: { to: request.to!, data: request.data!, value: request.value?.toString() },
991
+ l1BlockNumber: l1BlockNumber.toString(),
992
+ error: { message: viemError.message, name: viemError.name },
993
+ context: {
994
+ actions: [action],
995
+ slot: slotNumber,
996
+ sender: this.getSenderAddress().toString(),
997
+ },
998
+ });
717
999
  // Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
718
1000
  }
719
1001
 
@@ -764,19 +1046,16 @@ export class SequencerPublisher {
764
1046
  /**
765
1047
  * Enqueues a governance castSignal transaction to cast a signal for a given slot number.
766
1048
  * @param slotNumber - The slot number to cast a signal for.
767
- * @param timestamp - The timestamp of the slot to cast a signal for.
768
1049
  * @returns True if the signal was successfully enqueued, false otherwise.
769
1050
  */
770
1051
  public enqueueGovernanceCastSignal(
771
1052
  governancePayload: EthAddress,
772
1053
  slotNumber: SlotNumber,
773
- timestamp: bigint,
774
1054
  signerAddress: EthAddress,
775
1055
  signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
776
1056
  ): Promise<boolean> {
777
1057
  return this.enqueueCastSignalHelper(
778
1058
  slotNumber,
779
- timestamp,
780
1059
  'governance-signal',
781
1060
  governancePayload,
782
1061
  this.govProposerContract,
@@ -789,7 +1068,6 @@ export class SequencerPublisher {
789
1068
  public async enqueueSlashingActions(
790
1069
  actions: ProposerSlashAction[],
791
1070
  slotNumber: SlotNumber,
792
- timestamp: bigint,
793
1071
  signerAddress: EthAddress,
794
1072
  signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
795
1073
  ): Promise<boolean> {
@@ -810,7 +1088,6 @@ export class SequencerPublisher {
810
1088
  });
811
1089
  await this.enqueueCastSignalHelper(
812
1090
  slotNumber,
813
- timestamp,
814
1091
  'empire-slashing-signal',
815
1092
  action.payload,
816
1093
  this.slashingProposerContract,
@@ -829,7 +1106,6 @@ export class SequencerPublisher {
829
1106
  (receipt: TransactionReceipt) =>
830
1107
  !!this.slashFactoryContract.tryExtractSlashPayloadCreatedEvent(receipt.logs),
831
1108
  slotNumber,
832
- timestamp,
833
1109
  );
834
1110
  break;
835
1111
  }
@@ -847,7 +1123,6 @@ export class SequencerPublisher {
847
1123
  request,
848
1124
  (receipt: TransactionReceipt) => !!empireSlashingProposer.tryExtractPayloadSubmittedEvent(receipt.logs),
849
1125
  slotNumber,
850
- timestamp,
851
1126
  );
852
1127
  break;
853
1128
  }
@@ -871,7 +1146,6 @@ export class SequencerPublisher {
871
1146
  request,
872
1147
  (receipt: TransactionReceipt) => !!tallySlashingProposer.tryExtractVoteCastEvent(receipt.logs),
873
1148
  slotNumber,
874
- timestamp,
875
1149
  );
876
1150
  break;
877
1151
  }
@@ -893,7 +1167,6 @@ export class SequencerPublisher {
893
1167
  request,
894
1168
  (receipt: TransactionReceipt) => !!tallySlashingProposer.tryExtractRoundExecutedEvent(receipt.logs),
895
1169
  slotNumber,
896
- timestamp,
897
1170
  );
898
1171
  break;
899
1172
  }
@@ -913,30 +1186,33 @@ export class SequencerPublisher {
913
1186
  checkpoint: Checkpoint,
914
1187
  attestationsAndSigners: CommitteeAttestationsAndSigners,
915
1188
  attestationsAndSignersSignature: Signature,
916
- opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
1189
+ opts: {
1190
+ txTimeoutAt?: Date;
1191
+ forcePendingCheckpointNumber?: CheckpointNumber;
1192
+ forceProposedFeeHeader?: { checkpointNumber: CheckpointNumber; feeHeader: FeeHeader };
1193
+ } = {},
917
1194
  ): Promise<void> {
918
1195
  const checkpointHeader = checkpoint.header;
919
1196
 
920
1197
  const blobFields = checkpoint.toBlobFields();
921
- const blobs = getBlobsPerL1Block(blobFields);
1198
+ const blobs = await getBlobsPerL1Block(blobFields);
922
1199
 
923
- const proposeTxArgs = {
1200
+ const proposeTxArgs: L1ProcessArgs = {
924
1201
  header: checkpointHeader,
925
1202
  archive: checkpoint.archive.root.toBuffer(),
926
1203
  blobs,
927
1204
  attestationsAndSigners,
928
1205
  attestationsAndSignersSignature,
1206
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
929
1207
  };
930
1208
 
931
- let ts: bigint;
932
-
933
1209
  try {
934
1210
  // @note This will make sure that we are passing the checks for our header ASSUMING that the data is also made available
935
1211
  // This means that we can avoid the simulation issues in later checks.
936
1212
  // By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which
937
1213
  // make time consistency checks break.
938
1214
  // 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(
1215
+ await this.validateCheckpointForSubmission(
940
1216
  checkpoint,
941
1217
  attestationsAndSigners,
942
1218
  attestationsAndSignersSignature,
@@ -952,7 +1228,7 @@ export class SequencerPublisher {
952
1228
  }
953
1229
 
954
1230
  this.log.verbose(`Enqueuing checkpoint propose transaction`, { ...checkpoint.toCheckpointInfo(), ...opts });
955
- await this.addProposeTx(checkpoint, proposeTxArgs, opts, ts);
1231
+ await this.addProposeTx(checkpoint, proposeTxArgs, opts);
956
1232
  }
957
1233
 
958
1234
  public enqueueInvalidateCheckpoint(
@@ -995,8 +1271,8 @@ export class SequencerPublisher {
995
1271
  request: L1TxRequest,
996
1272
  checkSuccess: (receipt: TransactionReceipt) => boolean | undefined,
997
1273
  slotNumber: SlotNumber,
998
- timestamp: bigint,
999
1274
  ) {
1275
+ const timestamp = this.getSimulationTimestamp(slotNumber);
1000
1276
  const logData = { slotNumber, timestamp, gasLimit: undefined as bigint | undefined };
1001
1277
  if (this.lastActions[action] && this.lastActions[action] === slotNumber) {
1002
1278
  this.log.debug(`Skipping duplicate action ${action} for slot ${slotNumber}`);
@@ -1008,15 +1284,31 @@ export class SequencerPublisher {
1008
1284
 
1009
1285
  this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
1010
1286
 
1287
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1288
+
1011
1289
  let gasUsed: bigint;
1012
1290
  const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
1291
+
1013
1292
  try {
1014
- ({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], simulateAbi)); // TODO(palla/slash): Check the timestamp logic
1293
+ ({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], simulateAbi));
1015
1294
  this.log.verbose(`Simulation for ${action} succeeded`, { ...logData, request, gasUsed });
1016
1295
  } catch (err) {
1017
1296
  const viemError = formatViemError(err, simulateAbi);
1018
1297
  this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
1019
1298
 
1299
+ this.backupFailedTx({
1300
+ id: keccak256(request.data!),
1301
+ failureType: 'simulation',
1302
+ request: { to: request.to!, data: request.data!, value: request.value?.toString() },
1303
+ l1BlockNumber: l1BlockNumber.toString(),
1304
+ error: { message: viemError.message, name: viemError.name },
1305
+ context: {
1306
+ actions: [action],
1307
+ slot: slotNumber,
1308
+ sender: this.getSenderAddress().toString(),
1309
+ },
1310
+ });
1311
+
1020
1312
  return false;
1021
1313
  }
1022
1314
 
@@ -1056,6 +1348,7 @@ export class SequencerPublisher {
1056
1348
  */
1057
1349
  public interrupt() {
1058
1350
  this.interrupted = true;
1351
+ this.interruptibleSleep.interrupt();
1059
1352
  this.l1TxUtils.interrupt();
1060
1353
  }
1061
1354
 
@@ -1067,7 +1360,6 @@ export class SequencerPublisher {
1067
1360
 
1068
1361
  private async prepareProposeTx(
1069
1362
  encodedData: L1ProcessArgs,
1070
- timestamp: bigint,
1071
1363
  options: { forcePendingCheckpointNumber?: CheckpointNumber },
1072
1364
  ) {
1073
1365
  const kzg = Blob.getViemKzgInstance();
@@ -1100,9 +1392,27 @@ export class SequencerPublisher {
1100
1392
  kzg,
1101
1393
  },
1102
1394
  )
1103
- .catch(err => {
1104
- const { message, metaMessages } = formatViemError(err);
1105
- this.log.error(`Failed to validate blobs`, message, { metaMessages });
1395
+ .catch(async err => {
1396
+ const viemError = formatViemError(err);
1397
+ this.log.error(`Failed to validate blobs`, viemError.message, { metaMessages: viemError.metaMessages });
1398
+ const validateBlobsData = encodeFunctionData({
1399
+ abi: RollupAbi,
1400
+ functionName: 'validateBlobs',
1401
+ args: [blobInput],
1402
+ });
1403
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1404
+ this.backupFailedTx({
1405
+ id: keccak256(validateBlobsData),
1406
+ failureType: 'simulation',
1407
+ request: { to: this.rollupContract.address as Hex, data: validateBlobsData },
1408
+ blobData: encodedData.blobs.map(b => toHex(b.data)) as Hex[],
1409
+ l1BlockNumber: l1BlockNumber.toString(),
1410
+ error: { message: viemError.message, name: viemError.name },
1411
+ context: {
1412
+ actions: ['validate-blobs'],
1413
+ sender: this.getSenderAddress().toString(),
1414
+ },
1415
+ });
1106
1416
  throw new Error('Failed to validate blobs');
1107
1417
  });
1108
1418
  }
@@ -1113,8 +1423,7 @@ export class SequencerPublisher {
1113
1423
  header: encodedData.header.toViem(),
1114
1424
  archive: toHex(encodedData.archive),
1115
1425
  oracleInput: {
1116
- // We are currently not modifying these. See #9963
1117
- feeAssetPriceModifier: 0n,
1426
+ feeAssetPriceModifier: encodedData.feeAssetPriceModifier,
1118
1427
  },
1119
1428
  },
1120
1429
  encodedData.attestationsAndSigners.getPackedAttestations(),
@@ -1123,7 +1432,7 @@ export class SequencerPublisher {
1123
1432
  blobInput,
1124
1433
  ] as const;
1125
1434
 
1126
- const { rollupData, simulationResult } = await this.simulateProposeTx(args, timestamp, options);
1435
+ const { rollupData, simulationResult } = await this.simulateProposeTx(args, options);
1127
1436
 
1128
1437
  return { args, blobEvaluationGas, rollupData, simulationResult };
1129
1438
  }
@@ -1131,7 +1440,6 @@ export class SequencerPublisher {
1131
1440
  /**
1132
1441
  * Simulates the propose tx with eth_simulateV1
1133
1442
  * @param args - The propose tx args
1134
- * @param timestamp - The timestamp to simulate proposal at
1135
1443
  * @returns The simulation result
1136
1444
  */
1137
1445
  private async simulateProposeTx(
@@ -1140,7 +1448,7 @@ export class SequencerPublisher {
1140
1448
  readonly header: ViemHeader;
1141
1449
  readonly archive: `0x${string}`;
1142
1450
  readonly oracleInput: {
1143
- readonly feeAssetPriceModifier: 0n;
1451
+ readonly feeAssetPriceModifier: bigint;
1144
1452
  };
1145
1453
  },
1146
1454
  ViemCommitteeAttestations,
@@ -1148,8 +1456,10 @@ export class SequencerPublisher {
1148
1456
  ViemSignature,
1149
1457
  `0x${string}`,
1150
1458
  ],
1151
- timestamp: bigint,
1152
- options: { forcePendingCheckpointNumber?: CheckpointNumber },
1459
+ options: {
1460
+ forcePendingCheckpointNumber?: CheckpointNumber;
1461
+ forceProposedFeeHeader?: { checkpointNumber: CheckpointNumber; feeHeader: FeeHeader };
1462
+ },
1153
1463
  ) {
1154
1464
  const rollupData = encodeFunctionData({
1155
1465
  abi: RollupAbi,
@@ -1157,13 +1467,23 @@ export class SequencerPublisher {
1157
1467
  args,
1158
1468
  });
1159
1469
 
1160
- // override the pending checkpoint number if requested
1470
+ // override the proposed checkpoint number if requested
1161
1471
  const forcePendingCheckpointNumberStateDiff = (
1162
1472
  options.forcePendingCheckpointNumber !== undefined
1163
1473
  ? await this.rollupContract.makePendingCheckpointNumberOverride(options.forcePendingCheckpointNumber)
1164
1474
  : []
1165
1475
  ).flatMap(override => override.stateDiff ?? []);
1166
1476
 
1477
+ // override the fee header for a specific checkpoint number if requested (used when pipelining)
1478
+ const forceProposedFeeHeaderStateDiff = (
1479
+ options.forceProposedFeeHeader !== undefined
1480
+ ? await this.rollupContract.makeFeeHeaderOverride(
1481
+ options.forceProposedFeeHeader.checkpointNumber,
1482
+ options.forceProposedFeeHeader.feeHeader,
1483
+ )
1484
+ : []
1485
+ ).flatMap(override => override.stateDiff ?? []);
1486
+
1167
1487
  const stateOverrides: StateOverride = [
1168
1488
  {
1169
1489
  address: this.rollupContract.address,
@@ -1171,6 +1491,7 @@ export class SequencerPublisher {
1171
1491
  stateDiff: [
1172
1492
  { slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true), value: toPaddedHex(0n, true) },
1173
1493
  ...forcePendingCheckpointNumberStateDiff,
1494
+ ...forceProposedFeeHeaderStateDiff,
1174
1495
  ],
1175
1496
  },
1176
1497
  ];
@@ -1182,6 +1503,9 @@ export class SequencerPublisher {
1182
1503
  });
1183
1504
  }
1184
1505
 
1506
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1507
+ const simTs = this.getSimulationTimestamp(SlotNumber.fromBigInt(args[0].header.slotNumber));
1508
+
1185
1509
  const simulationResult = await this.l1TxUtils
1186
1510
  .simulate(
1187
1511
  {
@@ -1191,8 +1515,7 @@ export class SequencerPublisher {
1191
1515
  ...(this.proposerAddressForSimulation && { from: this.proposerAddressForSimulation.toString() }),
1192
1516
  },
1193
1517
  {
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,
1518
+ time: simTs,
1196
1519
  // @note reth should have a 30m gas limit per block but throws errors that this tx is beyond limit so we increase here
1197
1520
  gasLimit: MAX_L1_TX_LIMIT * 2n,
1198
1521
  },
@@ -1214,7 +1537,19 @@ export class SequencerPublisher {
1214
1537
  logs: [],
1215
1538
  };
1216
1539
  }
1217
- this.log.error(`Failed to simulate propose tx`, viemError);
1540
+ this.log.error(`Failed to simulate propose tx`, viemError, { simulationTimestamp: simTs });
1541
+ this.backupFailedTx({
1542
+ id: keccak256(rollupData),
1543
+ failureType: 'simulation',
1544
+ request: { to: this.rollupContract.address, data: rollupData },
1545
+ l1BlockNumber: l1BlockNumber.toString(),
1546
+ error: { message: viemError.message, name: viemError.name },
1547
+ context: {
1548
+ actions: ['propose'],
1549
+ slot: Number(args[0].header.slotNumber),
1550
+ sender: this.getSenderAddress().toString(),
1551
+ },
1552
+ });
1218
1553
  throw err;
1219
1554
  });
1220
1555
 
@@ -1224,17 +1559,16 @@ export class SequencerPublisher {
1224
1559
  private async addProposeTx(
1225
1560
  checkpoint: Checkpoint,
1226
1561
  encodedData: L1ProcessArgs,
1227
- opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
1228
- timestamp: bigint,
1562
+ opts: {
1563
+ txTimeoutAt?: Date;
1564
+ forcePendingCheckpointNumber?: CheckpointNumber;
1565
+ forceProposedFeeHeader?: { checkpointNumber: CheckpointNumber; feeHeader: FeeHeader };
1566
+ } = {},
1229
1567
  ): Promise<void> {
1230
1568
  const slot = checkpoint.header.slotNumber;
1231
1569
  const timer = new Timer();
1232
1570
  const kzg = Blob.getViemKzgInstance();
1233
- const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(
1234
- encodedData,
1235
- timestamp,
1236
- opts,
1237
- );
1571
+ const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(encodedData, opts);
1238
1572
  const startBlock = await this.l1TxUtils.getBlockNumber();
1239
1573
  const gasLimit = this.l1TxUtils.bumpGasLimit(
1240
1574
  BigInt(Math.ceil((Number(simulationResult.gasUsed) * 64) / 63)) +
@@ -1310,4 +1644,17 @@ export class SequencerPublisher {
1310
1644
  },
1311
1645
  });
1312
1646
  }
1647
+
1648
+ /** Returns the timestamp of the last L1 slot within a given L2 slot. Used as the simulation timestamp
1649
+ * for eth_simulateV1 calls, since it's guaranteed to be greater than any L1 block produced during the slot. */
1650
+ private getSimulationTimestamp(slot: SlotNumber): bigint {
1651
+ const l1Constants = this.epochCache.getL1Constants();
1652
+ return getLastL1SlotTimestampForL2Slot(slot, l1Constants);
1653
+ }
1654
+
1655
+ /** Returns the timestamp of the next L1 slot boundary after now. */
1656
+ private getNextL1SlotTimestamp(): bigint {
1657
+ const l1Constants = this.epochCache.getL1Constants();
1658
+ return getNextL1SlotTimestamp(this.dateProvider.nowInSeconds(), l1Constants);
1659
+ }
1313
1660
  }