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

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 (88) 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-metrics.d.ts +1 -1
  33. package/dest/publisher/sequencer-publisher-metrics.d.ts.map +1 -1
  34. package/dest/publisher/sequencer-publisher-metrics.js +12 -4
  35. package/dest/publisher/sequencer-publisher.d.ts +76 -31
  36. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  37. package/dest/publisher/sequencer-publisher.js +434 -88
  38. package/dest/sequencer/checkpoint_proposal_job.d.ts +61 -13
  39. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  40. package/dest/sequencer/checkpoint_proposal_job.js +400 -201
  41. package/dest/sequencer/checkpoint_voter.d.ts +1 -2
  42. package/dest/sequencer/checkpoint_voter.d.ts.map +1 -1
  43. package/dest/sequencer/checkpoint_voter.js +2 -5
  44. package/dest/sequencer/events.d.ts +2 -1
  45. package/dest/sequencer/events.d.ts.map +1 -1
  46. package/dest/sequencer/metrics.d.ts +21 -5
  47. package/dest/sequencer/metrics.d.ts.map +1 -1
  48. package/dest/sequencer/metrics.js +122 -30
  49. package/dest/sequencer/sequencer.d.ts +42 -17
  50. package/dest/sequencer/sequencer.d.ts.map +1 -1
  51. package/dest/sequencer/sequencer.js +147 -89
  52. package/dest/sequencer/timetable.d.ts +4 -6
  53. package/dest/sequencer/timetable.d.ts.map +1 -1
  54. package/dest/sequencer/timetable.js +7 -11
  55. package/dest/sequencer/types.d.ts +2 -2
  56. package/dest/sequencer/types.d.ts.map +1 -1
  57. package/dest/test/index.d.ts +3 -5
  58. package/dest/test/index.d.ts.map +1 -1
  59. package/dest/test/mock_checkpoint_builder.d.ts +12 -12
  60. package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
  61. package/dest/test/mock_checkpoint_builder.js +45 -36
  62. package/dest/test/utils.d.ts +3 -3
  63. package/dest/test/utils.d.ts.map +1 -1
  64. package/dest/test/utils.js +5 -4
  65. package/package.json +27 -28
  66. package/src/client/sequencer-client.ts +76 -23
  67. package/src/config.ts +65 -38
  68. package/src/global_variable_builder/global_builder.ts +38 -27
  69. package/src/global_variable_builder/index.ts +1 -1
  70. package/src/publisher/config.ts +153 -43
  71. package/src/publisher/index.ts +3 -0
  72. package/src/publisher/l1_tx_failed_store/factory.ts +32 -0
  73. package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +55 -0
  74. package/src/publisher/l1_tx_failed_store/file_store_failed_tx_store.ts +46 -0
  75. package/src/publisher/l1_tx_failed_store/index.ts +3 -0
  76. package/src/publisher/sequencer-publisher-factory.ts +38 -6
  77. package/src/publisher/sequencer-publisher-metrics.ts +7 -3
  78. package/src/publisher/sequencer-publisher.ts +474 -111
  79. package/src/sequencer/checkpoint_proposal_job.ts +532 -224
  80. package/src/sequencer/checkpoint_voter.ts +1 -12
  81. package/src/sequencer/events.ts +1 -1
  82. package/src/sequencer/metrics.ts +138 -32
  83. package/src/sequencer/sequencer.ts +213 -106
  84. package/src/sequencer/timetable.ts +13 -12
  85. package/src/sequencer/types.ts +1 -1
  86. package/src/test/index.ts +2 -4
  87. package/src/test/mock_checkpoint_builder.ts +65 -53
  88. 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,35 +20,65 @@ import {
18
20
  type L1BlobInputs,
19
21
  type L1TxConfig,
20
22
  type L1TxRequest,
23
+ type L1TxUtils,
24
+ MAX_L1_TX_LIMIT,
21
25
  type TransactionStats,
22
26
  WEI_CONST,
23
27
  } from '@aztec/ethereum/l1-tx-utils';
24
- import type { L1TxUtilsWithBlobs } from '@aztec/ethereum/l1-tx-utils-with-blobs';
25
- import { FormattedViemError, formatViemError, tryExtractEvent } from '@aztec/ethereum/utils';
28
+ import { FormattedViemError, formatViemError, mergeAbis, tryExtractEvent } from '@aztec/ethereum/utils';
26
29
  import { sumBigint } from '@aztec/foundation/bigint';
27
30
  import { toHex as toPaddedHex } from '@aztec/foundation/bigint-buffer';
28
31
  import { CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
32
+ import { trimmedBytesLength } from '@aztec/foundation/buffer';
29
33
  import { pick } from '@aztec/foundation/collection';
30
34
  import type { Fr } from '@aztec/foundation/curves/bn254';
35
+ import { TimeoutError } from '@aztec/foundation/error';
31
36
  import { EthAddress } from '@aztec/foundation/eth-address';
32
37
  import { Signature, type ViemSignature } from '@aztec/foundation/eth-signature';
33
38
  import { type Logger, createLogger } from '@aztec/foundation/log';
39
+ import { makeBackoff, retry } from '@aztec/foundation/retry';
40
+ import { InterruptibleSleep } from '@aztec/foundation/sleep';
34
41
  import { bufferToHex } from '@aztec/foundation/string';
35
- import { DateProvider, Timer } from '@aztec/foundation/timer';
42
+ import { type DateProvider, Timer } from '@aztec/foundation/timer';
36
43
  import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
37
44
  import { type ProposerSlashAction, encodeSlashConsensusVotes } from '@aztec/slasher';
38
45
  import { CommitteeAttestationsAndSigners, type ValidateCheckpointResult } from '@aztec/stdlib/block';
39
46
  import type { Checkpoint } from '@aztec/stdlib/checkpoint';
47
+ import { getLastL1SlotTimestampForL2Slot, getNextL1SlotTimestamp } from '@aztec/stdlib/epoch-helpers';
40
48
  import { SlashFactoryContract } from '@aztec/stdlib/l1-contracts';
41
49
  import type { CheckpointHeader } from '@aztec/stdlib/rollup';
42
50
  import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
43
51
  import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
44
52
 
45
- import { type StateOverride, type TransactionReceipt, type TypedDataDefinition, encodeFunctionData, toHex } from 'viem';
46
-
47
- 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';
48
66
  import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
49
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
+
50
82
  /** Arguments to the process method of the rollup contract */
51
83
  type L1ProcessArgs = {
52
84
  /** The L2 block header. */
@@ -59,6 +91,8 @@ type L1ProcessArgs = {
59
91
  attestationsAndSigners: CommitteeAttestationsAndSigners;
60
92
  /** Attestations and signers signature */
61
93
  attestationsAndSignersSignature: Signature;
94
+ /** The fee asset price modifier in basis points (from oracle) */
95
+ feeAssetPriceModifier: bigint;
62
96
  };
63
97
 
64
98
  export const Actions = [
@@ -86,6 +120,8 @@ export type InvalidateCheckpointRequest = {
86
120
  gasUsed: bigint;
87
121
  checkpointNumber: CheckpointNumber;
88
122
  forcePendingCheckpointNumber: CheckpointNumber;
123
+ /** Archive at the rollback target checkpoint (checkpoint N-1). */
124
+ lastArchive: Fr;
89
125
  };
90
126
 
91
127
  interface RequestWithExpiry {
@@ -104,6 +140,7 @@ export class SequencerPublisher {
104
140
  private interrupted = false;
105
141
  private metrics: SequencerPublisherMetrics;
106
142
  public epochCache: EpochCache;
143
+ private failedTxStore?: Promise<L1TxFailedStore | undefined>;
107
144
 
108
145
  protected governanceLog = createLogger('sequencer:publisher:governance');
109
146
  protected slashingLog = createLogger('sequencer:publisher:slashing');
@@ -111,21 +148,31 @@ export class SequencerPublisher {
111
148
  protected lastActions: Partial<Record<Action, SlotNumber>> = {};
112
149
 
113
150
  private isPayloadEmptyCache: Map<string, boolean> = new Map<string, boolean>();
151
+ private payloadProposedCache: Set<string> = new Set<string>();
114
152
 
115
153
  protected log: Logger;
116
154
  protected ethereumSlotDuration: bigint;
155
+ protected aztecSlotDuration: bigint;
156
+
157
+ /** Date provider for wall-clock time. */
158
+ private readonly dateProvider: DateProvider;
117
159
 
118
160
  private blobClient: BlobClientInterface;
119
161
 
120
162
  /** Address to use for simulations in fisherman mode (actual proposer's address) */
121
163
  private proposerAddressForSimulation?: EthAddress;
122
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
+
123
168
  /** L1 fee analyzer for fisherman mode */
124
169
  private l1FeeAnalyzer?: L1FeeAnalyzer;
125
- // @note - with blobs, the below estimate seems too large.
126
- // Total used for full block from int_l1_pub e2e test: 1m (of which 86k is 1x blob)
127
- // Total used for emptier block from above test: 429k (of which 84k is 1x blob)
128
- public static PROPOSE_GAS_GUESS: bigint = 12_000_000n;
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();
129
176
 
130
177
  // A CALL to a cold address is 2700 gas
131
178
  public static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
@@ -133,7 +180,7 @@ export class SequencerPublisher {
133
180
  // Gas report for VotingWithSigTest shows a max gas of 100k, but we've seen it cost 700k+ in testnet
134
181
  public static VOTE_GAS_GUESS: bigint = 800_000n;
135
182
 
136
- public l1TxUtils: L1TxUtilsWithBlobs;
183
+ public l1TxUtils: L1TxUtils;
137
184
  public rollupContract: RollupContract;
138
185
  public govProposerContract: GovernanceProposerContract;
139
186
  public slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
@@ -144,11 +191,12 @@ export class SequencerPublisher {
144
191
  protected requests: RequestWithExpiry[] = [];
145
192
 
146
193
  constructor(
147
- private config: TxSenderConfig & PublisherConfig & Pick<L1ContractsConfig, 'ethereumSlotDuration'>,
194
+ private config: Pick<SequencerPublisherConfig, 'fishermanMode' | 'l1TxFailedStore'> &
195
+ Pick<L1ContractsConfig, 'ethereumSlotDuration' | 'aztecSlotDuration'> & { l1ChainId: number },
148
196
  deps: {
149
197
  telemetry?: TelemetryClient;
150
198
  blobClient: BlobClientInterface;
151
- l1TxUtils: L1TxUtilsWithBlobs;
199
+ l1TxUtils: L1TxUtils;
152
200
  rollupContract: RollupContract;
153
201
  slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
154
202
  governanceProposerContract: GovernanceProposerContract;
@@ -158,19 +206,24 @@ export class SequencerPublisher {
158
206
  metrics: SequencerPublisherMetrics;
159
207
  lastActions: Partial<Record<Action, SlotNumber>>;
160
208
  log?: Logger;
209
+ getNextPublisher?: (excludeAddresses: EthAddress[]) => Promise<L1TxUtils | undefined>;
161
210
  },
162
211
  ) {
163
212
  this.log = deps.log ?? createLogger('sequencer:publisher');
164
213
  this.ethereumSlotDuration = BigInt(config.ethereumSlotDuration);
214
+ this.aztecSlotDuration = BigInt(config.aztecSlotDuration);
215
+ this.dateProvider = deps.dateProvider;
165
216
  this.epochCache = deps.epochCache;
166
217
  this.lastActions = deps.lastActions;
167
218
 
168
219
  this.blobClient = deps.blobClient;
220
+ this.dateProvider = deps.dateProvider;
169
221
 
170
222
  const telemetry = deps.telemetry ?? getTelemetryClient();
171
223
  this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
172
224
  this.tracer = telemetry.getTracer('SequencerPublisher');
173
225
  this.l1TxUtils = deps.l1TxUtils;
226
+ this.getNextPublisher = deps.getNextPublisher;
174
227
 
175
228
  this.rollupContract = deps.rollupContract;
176
229
 
@@ -192,12 +245,52 @@ export class SequencerPublisher {
192
245
  createLogger('sequencer:publisher:fee-analyzer'),
193
246
  );
194
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
+ });
195
280
  }
196
281
 
197
282
  public getRollupContract(): RollupContract {
198
283
  return this.rollupContract;
199
284
  }
200
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
+
201
294
  public getSenderAddress() {
202
295
  return this.l1TxUtils.getSenderAddress();
203
296
  }
@@ -222,7 +315,7 @@ export class SequencerPublisher {
222
315
  }
223
316
 
224
317
  public getCurrentL2Slot(): SlotNumber {
225
- return this.epochCache.getEpochAndSlotNow().slot;
318
+ return this.epochCache.getSlotNow();
226
319
  }
227
320
 
228
321
  /**
@@ -273,7 +366,7 @@ export class SequencerPublisher {
273
366
  // Start the analysis
274
367
  const analysisId = await this.l1FeeAnalyzer.startAnalysis(
275
368
  l2SlotNumber,
276
- gasLimit > 0n ? gasLimit : SequencerPublisher.PROPOSE_GAS_GUESS,
369
+ gasLimit > 0n ? gasLimit : MAX_L1_TX_LIMIT,
277
370
  l1Requests,
278
371
  blobConfig,
279
372
  onComplete,
@@ -300,9 +393,10 @@ export class SequencerPublisher {
300
393
  * - undefined if no valid requests are found OR the tx failed to send.
301
394
  */
302
395
  @trackSpan('SequencerPublisher.sendRequests')
303
- public async sendRequests() {
396
+ public async sendRequests(): Promise<SendRequestsResult | undefined> {
304
397
  const requestsToProcess = [...this.requests];
305
398
  this.requests = [];
399
+
306
400
  if (this.interrupted || requestsToProcess.length === 0) {
307
401
  return undefined;
308
402
  }
@@ -335,8 +429,8 @@ export class SequencerPublisher {
335
429
  // @note - we can only have one blob config per bundle
336
430
  // find requests with gas and blob configs
337
431
  // See https://github.com/AztecProtocol/aztec-packages/issues/11513
338
- const gasConfigs = requestsToProcess.filter(request => request.gasConfig).map(request => request.gasConfig);
339
- const blobConfigs = requestsToProcess.filter(request => request.blobConfig).map(request => request.blobConfig);
432
+ const gasConfigs = validRequests.filter(request => request.gasConfig).map(request => request.gasConfig);
433
+ const blobConfigs = validRequests.filter(request => request.blobConfig).map(request => request.blobConfig);
340
434
 
341
435
  if (blobConfigs.length > 1) {
342
436
  throw new Error('Multiple blob configs found');
@@ -346,7 +440,16 @@ export class SequencerPublisher {
346
440
 
347
441
  // Merge gasConfigs. Yields the sum of gasLimits, and the earliest txTimeoutAt, or undefined if no gasConfig sets them.
348
442
  const gasLimits = gasConfigs.map(g => g?.gasLimit).filter((g): g is bigint => g !== undefined);
349
- const gasLimit = gasLimits.length > 0 ? sumBigint(gasLimits) : undefined; // sum
443
+ let gasLimit = gasLimits.length > 0 ? sumBigint(gasLimits) : undefined; // sum
444
+ // Cap at L1 block gas limit so the node accepts the tx ("gas limit too high" otherwise).
445
+ const maxGas = MAX_L1_TX_LIMIT;
446
+ if (gasLimit !== undefined && gasLimit > maxGas) {
447
+ this.log.debug('Capping bundled tx gas limit to L1 max', {
448
+ requested: gasLimit,
449
+ capped: maxGas,
450
+ });
451
+ gasLimit = maxGas;
452
+ }
350
453
  const txTimeoutAts = gasConfigs.map(g => g?.txTimeoutAt).filter((g): g is Date => g !== undefined);
351
454
  const txTimeoutAt = txTimeoutAts.length > 0 ? new Date(Math.min(...txTimeoutAts.map(g => g.getTime()))) : undefined; // earliest
352
455
  const txConfig: RequestWithExpiry['gasConfig'] = { gasLimit, txTimeoutAt };
@@ -356,19 +459,36 @@ export class SequencerPublisher {
356
459
  validRequests.sort((a, b) => compareActions(a.action, b.action));
357
460
 
358
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
+
359
479
  this.log.debug('Forwarding transactions', {
360
480
  validRequests: validRequests.map(request => request.action),
361
481
  txConfig,
362
482
  });
363
- const result = await Multicall3.forward(
364
- validRequests.map(request => request.request),
365
- this.l1TxUtils,
366
- txConfig,
367
- blobConfig,
368
- this.rollupContract.address,
369
- this.log,
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,
370
491
  );
371
- const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(validRequests, result);
372
492
  return { result, expiredActions, sentActions: validActions, successfulActions, failedActions };
373
493
  } catch (err) {
374
494
  const viemError = formatViemError(err);
@@ -386,16 +506,105 @@ export class SequencerPublisher {
386
506
  }
387
507
  }
388
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
+
389
575
  private callbackBundledTransactions(
390
576
  requests: RequestWithExpiry[],
391
- result?: { receipt: TransactionReceipt } | FormattedViemError,
577
+ result: { receipt: TransactionReceipt; errorMsg?: string } | FormattedViemError | undefined,
578
+ txContext: { multicallData: Hex; blobData?: Hex[]; l1BlockNumber: bigint },
392
579
  ) {
393
580
  const actionsListStr = requests.map(r => r.action).join(', ');
394
581
  if (result instanceof FormattedViemError) {
395
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
+ });
396
596
  return { failedActions: requests.map(r => r.action) };
397
597
  } else {
398
- 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
+ });
399
608
  const successfulActions: Action[] = [];
400
609
  const failedActions: Action[] = [];
401
610
  for (const request of requests) {
@@ -405,26 +614,59 @@ export class SequencerPublisher {
405
614
  failedActions.push(request.action);
406
615
  }
407
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
+ }
408
641
  return { successfulActions, failedActions };
409
642
  }
410
643
  }
411
644
 
412
645
  /**
413
- * @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
414
647
  * @param tipArchive - The archive to check
415
648
  * @returns The slot and block number if it is possible to propose, undefined otherwise
416
649
  */
417
- public canProposeAtNextEthBlock(
650
+ public canProposeAt(
418
651
  tipArchive: Fr,
419
652
  msgSender: EthAddress,
420
- opts: { forcePendingCheckpointNumber?: CheckpointNumber } = {},
653
+ opts: {
654
+ forcePendingCheckpointNumber?: CheckpointNumber;
655
+ forceArchive?: { checkpointNumber: CheckpointNumber; archive: Fr };
656
+ pipelined?: boolean;
657
+ } = {},
421
658
  ) {
422
659
  // TODO: #14291 - should loop through multiple keys to check if any of them can propose
423
660
  const ignoredErrors = ['SlotAlreadyInChain', 'InvalidProposer', 'InvalidArchive'];
424
661
 
662
+ const pipelined = opts.pipelined ?? this.epochCache.isProposerPipeliningEnabled();
663
+ const slotOffset = pipelined ? this.aztecSlotDuration : 0n;
664
+ const nextL1SlotTs = this.getNextL1SlotTimestamp() + slotOffset;
665
+
425
666
  return this.rollupContract
426
- .canProposeAtNextEthBlock(tipArchive.toBuffer(), msgSender.toString(), Number(this.ethereumSlotDuration), {
667
+ .canProposeAt(tipArchive.toBuffer(), msgSender.toString(), nextL1SlotTs, {
427
668
  forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
669
+ forceArchive: opts.forceArchive,
428
670
  })
429
671
  .catch(err => {
430
672
  if (err instanceof FormattedViemError && ignoredErrors.find(e => err.message.includes(e))) {
@@ -437,6 +679,7 @@ export class SequencerPublisher {
437
679
  return undefined;
438
680
  });
439
681
  }
682
+
440
683
  /**
441
684
  * @notice Will simulate `validateHeader` to make sure that the block header is valid
442
685
  * @dev This is a convenience function that can be used by the sequencer to validate a "partial" header.
@@ -460,7 +703,7 @@ export class SequencerPublisher {
460
703
  flags,
461
704
  ] as const;
462
705
 
463
- const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
706
+ const ts = this.getSimulationTimestamp(header.slotNumber);
464
707
  const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(
465
708
  opts?.forcePendingCheckpointNumber,
466
709
  );
@@ -483,7 +726,7 @@ export class SequencerPublisher {
483
726
  data: encodeFunctionData({ abi: RollupAbi, functionName: 'validateHeaderWithAttestations', args }),
484
727
  from: MULTI_CALL_3_ADDRESS,
485
728
  },
486
- { time: ts + 1n },
729
+ { time: ts },
487
730
  stateOverrides,
488
731
  );
489
732
  this.log.debug(`Simulated validateHeader`);
@@ -516,8 +759,15 @@ export class SequencerPublisher {
516
759
  const request = this.buildInvalidateCheckpointRequest(validationResult);
517
760
  this.log.debug(`Simulating invalidate checkpoint ${checkpointNumber}`, { ...logData, request });
518
761
 
762
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
763
+
519
764
  try {
520
- const { gasUsed } = await this.l1TxUtils.simulate(request, undefined, undefined, ErrorsAbi);
765
+ const { gasUsed } = await this.l1TxUtils.simulate(
766
+ request,
767
+ undefined,
768
+ undefined,
769
+ mergeAbis([request.abi ?? [], ErrorsAbi]),
770
+ );
521
771
  this.log.verbose(`Simulation for invalidate checkpoint ${checkpointNumber} succeeded`, {
522
772
  ...logData,
523
773
  request,
@@ -529,6 +779,7 @@ export class SequencerPublisher {
529
779
  gasUsed,
530
780
  checkpointNumber,
531
781
  forcePendingCheckpointNumber: CheckpointNumber(checkpointNumber - 1),
782
+ lastArchive: validationResult.checkpoint.lastArchive,
532
783
  reason,
533
784
  };
534
785
  } catch (err) {
@@ -536,13 +787,13 @@ export class SequencerPublisher {
536
787
 
537
788
  // If the error is due to the checkpoint not being in the pending chain, and it was indeed removed by someone else,
538
789
  // we can safely ignore it and return undefined so we go ahead with checkpoint building.
539
- if (viemError.message?.includes('Rollup__BlockNotInPendingChain')) {
790
+ if (viemError.message?.includes('Rollup__CheckpointNotInPendingChain')) {
540
791
  this.log.verbose(
541
792
  `Simulation for invalidate checkpoint ${checkpointNumber} failed due to checkpoint not being in pending chain`,
542
793
  { ...logData, request, error: viemError.message },
543
794
  );
544
- const latestPendingCheckpointNumber = await this.rollupContract.getCheckpointNumber();
545
- if (latestPendingCheckpointNumber < checkpointNumber) {
795
+ const latestProposedCheckpointNumber = await this.rollupContract.getCheckpointNumber();
796
+ if (latestProposedCheckpointNumber < checkpointNumber) {
546
797
  this.log.verbose(`Checkpoint ${checkpointNumber} has already been invalidated`, { ...logData });
547
798
  return undefined;
548
799
  } else {
@@ -562,6 +813,18 @@ export class SequencerPublisher {
562
813
 
563
814
  // Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
564
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
+ });
565
828
  throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, { cause: viemError });
566
829
  }
567
830
  }
@@ -604,27 +867,13 @@ export class SequencerPublisher {
604
867
  checkpoint: Checkpoint,
605
868
  attestationsAndSigners: CommitteeAttestationsAndSigners,
606
869
  attestationsAndSignersSignature: Signature,
607
- options: { forcePendingCheckpointNumber?: CheckpointNumber },
608
- ): Promise<bigint> {
609
- const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
610
-
611
- // TODO(palla/mbps): This should not be needed, there's no flow where we propose with zero attestations. Or is there?
612
- // If we have no attestations, we still need to provide the empty attestations
613
- // so that the committee is recalculated correctly
614
- // const ignoreSignatures = attestationsAndSigners.attestations.length === 0;
615
- // if (ignoreSignatures) {
616
- // const { committee } = await this.epochCache.getCommittee(block.header.globalVariables.slotNumber);
617
- // if (!committee) {
618
- // this.log.warn(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
619
- // throw new Error(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
620
- // }
621
- // attestationsAndSigners.attestations = committee.map(committeeMember =>
622
- // CommitteeAttestation.fromAddress(committeeMember),
623
- // );
624
- // }
625
-
870
+ options: {
871
+ forcePendingCheckpointNumber?: CheckpointNumber;
872
+ forceProposedFeeHeader?: { checkpointNumber: CheckpointNumber; feeHeader: FeeHeader };
873
+ },
874
+ ): Promise<void> {
626
875
  const blobFields = checkpoint.toBlobFields();
627
- const blobs = getBlobsPerL1Block(blobFields);
876
+ const blobs = await getBlobsPerL1Block(blobFields);
628
877
  const blobInput = getPrefixedEthBlobCommitments(blobs);
629
878
 
630
879
  const args = [
@@ -632,7 +881,7 @@ export class SequencerPublisher {
632
881
  header: checkpoint.header.toViem(),
633
882
  archive: toHex(checkpoint.archive.root.toBuffer()),
634
883
  oracleInput: {
635
- feeAssetPriceModifier: 0n,
884
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
636
885
  },
637
886
  },
638
887
  attestationsAndSigners.getPackedAttestations(),
@@ -641,13 +890,11 @@ export class SequencerPublisher {
641
890
  blobInput,
642
891
  ] as const;
643
892
 
644
- await this.simulateProposeTx(args, ts, options);
645
- return ts;
893
+ await this.simulateProposeTx(args, options);
646
894
  }
647
895
 
648
896
  private async enqueueCastSignalHelper(
649
897
  slotNumber: SlotNumber,
650
- timestamp: bigint,
651
898
  signalType: GovernanceSignalAction,
652
899
  payload: EthAddress,
653
900
  base: IEmpireBase,
@@ -681,6 +928,32 @@ export class SequencerPublisher {
681
928
  return false;
682
929
  }
683
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
+
684
957
  const cachedLastVote = this.lastActions[signalType];
685
958
  this.lastActions[signalType] = slotNumber;
686
959
  const action = signalType;
@@ -699,11 +972,30 @@ export class SequencerPublisher {
699
972
  lastValidL2Slot: slotNumber,
700
973
  });
701
974
 
975
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
976
+ const timestamp = this.getSimulationTimestamp(slotNumber);
977
+
702
978
  try {
703
- await this.l1TxUtils.simulate(request, { time: timestamp }, [], ErrorsAbi);
979
+ await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
704
980
  this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
705
981
  } catch (err) {
706
- 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
+ });
707
999
  // Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
708
1000
  }
709
1001
 
@@ -754,19 +1046,16 @@ export class SequencerPublisher {
754
1046
  /**
755
1047
  * Enqueues a governance castSignal transaction to cast a signal for a given slot number.
756
1048
  * @param slotNumber - The slot number to cast a signal for.
757
- * @param timestamp - The timestamp of the slot to cast a signal for.
758
1049
  * @returns True if the signal was successfully enqueued, false otherwise.
759
1050
  */
760
1051
  public enqueueGovernanceCastSignal(
761
1052
  governancePayload: EthAddress,
762
1053
  slotNumber: SlotNumber,
763
- timestamp: bigint,
764
1054
  signerAddress: EthAddress,
765
1055
  signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
766
1056
  ): Promise<boolean> {
767
1057
  return this.enqueueCastSignalHelper(
768
1058
  slotNumber,
769
- timestamp,
770
1059
  'governance-signal',
771
1060
  governancePayload,
772
1061
  this.govProposerContract,
@@ -779,7 +1068,6 @@ export class SequencerPublisher {
779
1068
  public async enqueueSlashingActions(
780
1069
  actions: ProposerSlashAction[],
781
1070
  slotNumber: SlotNumber,
782
- timestamp: bigint,
783
1071
  signerAddress: EthAddress,
784
1072
  signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
785
1073
  ): Promise<boolean> {
@@ -800,7 +1088,6 @@ export class SequencerPublisher {
800
1088
  });
801
1089
  await this.enqueueCastSignalHelper(
802
1090
  slotNumber,
803
- timestamp,
804
1091
  'empire-slashing-signal',
805
1092
  action.payload,
806
1093
  this.slashingProposerContract,
@@ -819,7 +1106,6 @@ export class SequencerPublisher {
819
1106
  (receipt: TransactionReceipt) =>
820
1107
  !!this.slashFactoryContract.tryExtractSlashPayloadCreatedEvent(receipt.logs),
821
1108
  slotNumber,
822
- timestamp,
823
1109
  );
824
1110
  break;
825
1111
  }
@@ -837,7 +1123,6 @@ export class SequencerPublisher {
837
1123
  request,
838
1124
  (receipt: TransactionReceipt) => !!empireSlashingProposer.tryExtractPayloadSubmittedEvent(receipt.logs),
839
1125
  slotNumber,
840
- timestamp,
841
1126
  );
842
1127
  break;
843
1128
  }
@@ -861,7 +1146,6 @@ export class SequencerPublisher {
861
1146
  request,
862
1147
  (receipt: TransactionReceipt) => !!tallySlashingProposer.tryExtractVoteCastEvent(receipt.logs),
863
1148
  slotNumber,
864
- timestamp,
865
1149
  );
866
1150
  break;
867
1151
  }
@@ -883,7 +1167,6 @@ export class SequencerPublisher {
883
1167
  request,
884
1168
  (receipt: TransactionReceipt) => !!tallySlashingProposer.tryExtractRoundExecutedEvent(receipt.logs),
885
1169
  slotNumber,
886
- timestamp,
887
1170
  );
888
1171
  break;
889
1172
  }
@@ -903,30 +1186,33 @@ export class SequencerPublisher {
903
1186
  checkpoint: Checkpoint,
904
1187
  attestationsAndSigners: CommitteeAttestationsAndSigners,
905
1188
  attestationsAndSignersSignature: Signature,
906
- opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
1189
+ opts: {
1190
+ txTimeoutAt?: Date;
1191
+ forcePendingCheckpointNumber?: CheckpointNumber;
1192
+ forceProposedFeeHeader?: { checkpointNumber: CheckpointNumber; feeHeader: FeeHeader };
1193
+ } = {},
907
1194
  ): Promise<void> {
908
1195
  const checkpointHeader = checkpoint.header;
909
1196
 
910
1197
  const blobFields = checkpoint.toBlobFields();
911
- const blobs = getBlobsPerL1Block(blobFields);
1198
+ const blobs = await getBlobsPerL1Block(blobFields);
912
1199
 
913
- const proposeTxArgs = {
1200
+ const proposeTxArgs: L1ProcessArgs = {
914
1201
  header: checkpointHeader,
915
1202
  archive: checkpoint.archive.root.toBuffer(),
916
1203
  blobs,
917
1204
  attestationsAndSigners,
918
1205
  attestationsAndSignersSignature,
1206
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
919
1207
  };
920
1208
 
921
- let ts: bigint;
922
-
923
1209
  try {
924
1210
  // @note This will make sure that we are passing the checks for our header ASSUMING that the data is also made available
925
1211
  // This means that we can avoid the simulation issues in later checks.
926
1212
  // By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which
927
1213
  // make time consistency checks break.
928
1214
  // TODO(palla): Check whether we're validating twice, once here and once within addProposeTx, since we call simulateProposeTx in both places.
929
- ts = await this.validateCheckpointForSubmission(
1215
+ await this.validateCheckpointForSubmission(
930
1216
  checkpoint,
931
1217
  attestationsAndSigners,
932
1218
  attestationsAndSignersSignature,
@@ -942,7 +1228,7 @@ export class SequencerPublisher {
942
1228
  }
943
1229
 
944
1230
  this.log.verbose(`Enqueuing checkpoint propose transaction`, { ...checkpoint.toCheckpointInfo(), ...opts });
945
- await this.addProposeTx(checkpoint, proposeTxArgs, opts, ts);
1231
+ await this.addProposeTx(checkpoint, proposeTxArgs, opts);
946
1232
  }
947
1233
 
948
1234
  public enqueueInvalidateCheckpoint(
@@ -985,8 +1271,8 @@ export class SequencerPublisher {
985
1271
  request: L1TxRequest,
986
1272
  checkSuccess: (receipt: TransactionReceipt) => boolean | undefined,
987
1273
  slotNumber: SlotNumber,
988
- timestamp: bigint,
989
1274
  ) {
1275
+ const timestamp = this.getSimulationTimestamp(slotNumber);
990
1276
  const logData = { slotNumber, timestamp, gasLimit: undefined as bigint | undefined };
991
1277
  if (this.lastActions[action] && this.lastActions[action] === slotNumber) {
992
1278
  this.log.debug(`Skipping duplicate action ${action} for slot ${slotNumber}`);
@@ -998,13 +1284,31 @@ export class SequencerPublisher {
998
1284
 
999
1285
  this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
1000
1286
 
1287
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1288
+
1001
1289
  let gasUsed: bigint;
1290
+ const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
1291
+
1002
1292
  try {
1003
- ({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], ErrorsAbi)); // TODO(palla/slash): Check the timestamp logic
1293
+ ({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], simulateAbi));
1004
1294
  this.log.verbose(`Simulation for ${action} succeeded`, { ...logData, request, gasUsed });
1005
1295
  } catch (err) {
1006
- const viemError = formatViemError(err);
1296
+ const viemError = formatViemError(err, simulateAbi);
1007
1297
  this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
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
+
1008
1312
  return false;
1009
1313
  }
1010
1314
 
@@ -1012,10 +1316,14 @@ export class SequencerPublisher {
1012
1316
  const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil((Number(gasUsed) * 64) / 63)));
1013
1317
  logData.gasLimit = gasLimit;
1014
1318
 
1319
+ // Store the ABI used for simulation on the request so Multicall3.forward can decode errors
1320
+ // when the tx is sent and a revert is diagnosed via simulation.
1321
+ const requestWithAbi = { ...request, abi: simulateAbi };
1322
+
1015
1323
  this.log.debug(`Enqueuing ${action}`, logData);
1016
1324
  this.addRequest({
1017
1325
  action,
1018
- request,
1326
+ request: requestWithAbi,
1019
1327
  gasConfig: { gasLimit },
1020
1328
  lastValidL2Slot: slotNumber,
1021
1329
  checkSuccess: (_req, result) => {
@@ -1040,6 +1348,7 @@ export class SequencerPublisher {
1040
1348
  */
1041
1349
  public interrupt() {
1042
1350
  this.interrupted = true;
1351
+ this.interruptibleSleep.interrupt();
1043
1352
  this.l1TxUtils.interrupt();
1044
1353
  }
1045
1354
 
@@ -1051,7 +1360,6 @@ export class SequencerPublisher {
1051
1360
 
1052
1361
  private async prepareProposeTx(
1053
1362
  encodedData: L1ProcessArgs,
1054
- timestamp: bigint,
1055
1363
  options: { forcePendingCheckpointNumber?: CheckpointNumber },
1056
1364
  ) {
1057
1365
  const kzg = Blob.getViemKzgInstance();
@@ -1084,9 +1392,27 @@ export class SequencerPublisher {
1084
1392
  kzg,
1085
1393
  },
1086
1394
  )
1087
- .catch(err => {
1088
- const { message, metaMessages } = formatViemError(err);
1089
- 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
+ });
1090
1416
  throw new Error('Failed to validate blobs');
1091
1417
  });
1092
1418
  }
@@ -1097,8 +1423,7 @@ export class SequencerPublisher {
1097
1423
  header: encodedData.header.toViem(),
1098
1424
  archive: toHex(encodedData.archive),
1099
1425
  oracleInput: {
1100
- // We are currently not modifying these. See #9963
1101
- feeAssetPriceModifier: 0n,
1426
+ feeAssetPriceModifier: encodedData.feeAssetPriceModifier,
1102
1427
  },
1103
1428
  },
1104
1429
  encodedData.attestationsAndSigners.getPackedAttestations(),
@@ -1107,7 +1432,7 @@ export class SequencerPublisher {
1107
1432
  blobInput,
1108
1433
  ] as const;
1109
1434
 
1110
- const { rollupData, simulationResult } = await this.simulateProposeTx(args, timestamp, options);
1435
+ const { rollupData, simulationResult } = await this.simulateProposeTx(args, options);
1111
1436
 
1112
1437
  return { args, blobEvaluationGas, rollupData, simulationResult };
1113
1438
  }
@@ -1115,7 +1440,6 @@ export class SequencerPublisher {
1115
1440
  /**
1116
1441
  * Simulates the propose tx with eth_simulateV1
1117
1442
  * @param args - The propose tx args
1118
- * @param timestamp - The timestamp to simulate proposal at
1119
1443
  * @returns The simulation result
1120
1444
  */
1121
1445
  private async simulateProposeTx(
@@ -1124,7 +1448,7 @@ export class SequencerPublisher {
1124
1448
  readonly header: ViemHeader;
1125
1449
  readonly archive: `0x${string}`;
1126
1450
  readonly oracleInput: {
1127
- readonly feeAssetPriceModifier: 0n;
1451
+ readonly feeAssetPriceModifier: bigint;
1128
1452
  };
1129
1453
  },
1130
1454
  ViemCommitteeAttestations,
@@ -1132,8 +1456,10 @@ export class SequencerPublisher {
1132
1456
  ViemSignature,
1133
1457
  `0x${string}`,
1134
1458
  ],
1135
- timestamp: bigint,
1136
- options: { forcePendingCheckpointNumber?: CheckpointNumber },
1459
+ options: {
1460
+ forcePendingCheckpointNumber?: CheckpointNumber;
1461
+ forceProposedFeeHeader?: { checkpointNumber: CheckpointNumber; feeHeader: FeeHeader };
1462
+ },
1137
1463
  ) {
1138
1464
  const rollupData = encodeFunctionData({
1139
1465
  abi: RollupAbi,
@@ -1141,13 +1467,23 @@ export class SequencerPublisher {
1141
1467
  args,
1142
1468
  });
1143
1469
 
1144
- // override the pending checkpoint number if requested
1470
+ // override the proposed checkpoint number if requested
1145
1471
  const forcePendingCheckpointNumberStateDiff = (
1146
1472
  options.forcePendingCheckpointNumber !== undefined
1147
1473
  ? await this.rollupContract.makePendingCheckpointNumberOverride(options.forcePendingCheckpointNumber)
1148
1474
  : []
1149
1475
  ).flatMap(override => override.stateDiff ?? []);
1150
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
+
1151
1487
  const stateOverrides: StateOverride = [
1152
1488
  {
1153
1489
  address: this.rollupContract.address,
@@ -1155,6 +1491,7 @@ export class SequencerPublisher {
1155
1491
  stateDiff: [
1156
1492
  { slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true), value: toPaddedHex(0n, true) },
1157
1493
  ...forcePendingCheckpointNumberStateDiff,
1494
+ ...forceProposedFeeHeaderStateDiff,
1158
1495
  ],
1159
1496
  },
1160
1497
  ];
@@ -1166,25 +1503,27 @@ export class SequencerPublisher {
1166
1503
  });
1167
1504
  }
1168
1505
 
1506
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1507
+ const simTs = this.getSimulationTimestamp(SlotNumber.fromBigInt(args[0].header.slotNumber));
1508
+
1169
1509
  const simulationResult = await this.l1TxUtils
1170
1510
  .simulate(
1171
1511
  {
1172
1512
  to: this.rollupContract.address,
1173
1513
  data: rollupData,
1174
- gas: SequencerPublisher.PROPOSE_GAS_GUESS,
1514
+ gas: MAX_L1_TX_LIMIT,
1175
1515
  ...(this.proposerAddressForSimulation && { from: this.proposerAddressForSimulation.toString() }),
1176
1516
  },
1177
1517
  {
1178
- // @note we add 1n to the timestamp because geth implementation doesn't like simulation timestamp to be equal to the current block timestamp
1179
- time: timestamp + 1n,
1518
+ time: simTs,
1180
1519
  // @note reth should have a 30m gas limit per block but throws errors that this tx is beyond limit so we increase here
1181
- gasLimit: SequencerPublisher.PROPOSE_GAS_GUESS * 2n,
1520
+ gasLimit: MAX_L1_TX_LIMIT * 2n,
1182
1521
  },
1183
1522
  stateOverrides,
1184
1523
  RollupAbi,
1185
1524
  {
1186
1525
  // @note fallback gas estimate to use if the node doesn't support simulation API
1187
- fallbackGasEstimate: SequencerPublisher.PROPOSE_GAS_GUESS,
1526
+ fallbackGasEstimate: MAX_L1_TX_LIMIT,
1188
1527
  },
1189
1528
  )
1190
1529
  .catch(err => {
@@ -1194,11 +1533,23 @@ export class SequencerPublisher {
1194
1533
  this.log.debug(`Ignoring expected ValidatorSelection__MissingProposerSignature error in fisherman mode`);
1195
1534
  // Return a minimal simulation result with the fallback gas estimate
1196
1535
  return {
1197
- gasUsed: SequencerPublisher.PROPOSE_GAS_GUESS,
1536
+ gasUsed: MAX_L1_TX_LIMIT,
1198
1537
  logs: [],
1199
1538
  };
1200
1539
  }
1201
- 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
+ });
1202
1553
  throw err;
1203
1554
  });
1204
1555
 
@@ -1208,17 +1559,16 @@ export class SequencerPublisher {
1208
1559
  private async addProposeTx(
1209
1560
  checkpoint: Checkpoint,
1210
1561
  encodedData: L1ProcessArgs,
1211
- opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
1212
- timestamp: bigint,
1562
+ opts: {
1563
+ txTimeoutAt?: Date;
1564
+ forcePendingCheckpointNumber?: CheckpointNumber;
1565
+ forceProposedFeeHeader?: { checkpointNumber: CheckpointNumber; feeHeader: FeeHeader };
1566
+ } = {},
1213
1567
  ): Promise<void> {
1214
1568
  const slot = checkpoint.header.slotNumber;
1215
1569
  const timer = new Timer();
1216
1570
  const kzg = Blob.getViemKzgInstance();
1217
- const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(
1218
- encodedData,
1219
- timestamp,
1220
- opts,
1221
- );
1571
+ const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(encodedData, opts);
1222
1572
  const startBlock = await this.l1TxUtils.getBlockNumber();
1223
1573
  const gasLimit = this.l1TxUtils.bumpGasLimit(
1224
1574
  BigInt(Math.ceil((Number(simulationResult.gasUsed) * 64) / 63)) +
@@ -1294,4 +1644,17 @@ export class SequencerPublisher {
1294
1644
  },
1295
1645
  });
1296
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
+ }
1297
1660
  }