@aztec/sequencer-client 0.0.1-commit.c2595eba → 0.0.1-commit.c2eed6949

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 (80) hide show
  1. package/dest/client/sequencer-client.d.ts +15 -7
  2. package/dest/client/sequencer-client.d.ts.map +1 -1
  3. package/dest/client/sequencer-client.js +60 -26
  4. package/dest/config.d.ts +26 -7
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +47 -28
  7. package/dest/global_variable_builder/global_builder.d.ts +14 -10
  8. package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
  9. package/dest/global_variable_builder/global_builder.js +22 -21
  10. package/dest/global_variable_builder/index.d.ts +2 -2
  11. package/dest/global_variable_builder/index.d.ts.map +1 -1
  12. package/dest/publisher/config.d.ts +47 -17
  13. package/dest/publisher/config.d.ts.map +1 -1
  14. package/dest/publisher/config.js +121 -42
  15. package/dest/publisher/index.d.ts +2 -1
  16. package/dest/publisher/index.d.ts.map +1 -1
  17. package/dest/publisher/l1_tx_failed_store/factory.d.ts +11 -0
  18. package/dest/publisher/l1_tx_failed_store/factory.d.ts.map +1 -0
  19. package/dest/publisher/l1_tx_failed_store/factory.js +22 -0
  20. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts +59 -0
  21. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -0
  22. package/dest/publisher/l1_tx_failed_store/failed_tx_store.js +1 -0
  23. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts +15 -0
  24. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts.map +1 -0
  25. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.js +34 -0
  26. package/dest/publisher/l1_tx_failed_store/index.d.ts +4 -0
  27. package/dest/publisher/l1_tx_failed_store/index.d.ts.map +1 -0
  28. package/dest/publisher/l1_tx_failed_store/index.js +2 -0
  29. package/dest/publisher/sequencer-publisher-factory.d.ts +11 -3
  30. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  31. package/dest/publisher/sequencer-publisher-factory.js +27 -2
  32. package/dest/publisher/sequencer-publisher.d.ts +33 -10
  33. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  34. package/dest/publisher/sequencer-publisher.js +371 -57
  35. package/dest/sequencer/checkpoint_proposal_job.d.ts +39 -10
  36. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  37. package/dest/sequencer/checkpoint_proposal_job.js +287 -167
  38. package/dest/sequencer/events.d.ts +2 -1
  39. package/dest/sequencer/events.d.ts.map +1 -1
  40. package/dest/sequencer/metrics.d.ts +21 -5
  41. package/dest/sequencer/metrics.d.ts.map +1 -1
  42. package/dest/sequencer/metrics.js +97 -15
  43. package/dest/sequencer/sequencer.d.ts +30 -15
  44. package/dest/sequencer/sequencer.d.ts.map +1 -1
  45. package/dest/sequencer/sequencer.js +95 -82
  46. package/dest/sequencer/timetable.d.ts +4 -6
  47. package/dest/sequencer/timetable.d.ts.map +1 -1
  48. package/dest/sequencer/timetable.js +7 -11
  49. package/dest/sequencer/types.d.ts +2 -2
  50. package/dest/sequencer/types.d.ts.map +1 -1
  51. package/dest/test/index.d.ts +3 -5
  52. package/dest/test/index.d.ts.map +1 -1
  53. package/dest/test/mock_checkpoint_builder.d.ts +12 -12
  54. package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
  55. package/dest/test/mock_checkpoint_builder.js +45 -36
  56. package/dest/test/utils.d.ts +3 -3
  57. package/dest/test/utils.d.ts.map +1 -1
  58. package/dest/test/utils.js +5 -4
  59. package/package.json +27 -28
  60. package/src/client/sequencer-client.ts +76 -23
  61. package/src/config.ts +65 -38
  62. package/src/global_variable_builder/global_builder.ts +23 -24
  63. package/src/global_variable_builder/index.ts +1 -1
  64. package/src/publisher/config.ts +153 -43
  65. package/src/publisher/index.ts +3 -0
  66. package/src/publisher/l1_tx_failed_store/factory.ts +32 -0
  67. package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +55 -0
  68. package/src/publisher/l1_tx_failed_store/file_store_failed_tx_store.ts +46 -0
  69. package/src/publisher/l1_tx_failed_store/index.ts +3 -0
  70. package/src/publisher/sequencer-publisher-factory.ts +38 -6
  71. package/src/publisher/sequencer-publisher.ts +371 -70
  72. package/src/sequencer/checkpoint_proposal_job.ts +392 -193
  73. package/src/sequencer/events.ts +1 -1
  74. package/src/sequencer/metrics.ts +106 -18
  75. package/src/sequencer/sequencer.ts +131 -94
  76. package/src/sequencer/timetable.ts +13 -12
  77. package/src/sequencer/types.ts +1 -1
  78. package/src/test/index.ts +2 -4
  79. package/src/test/mock_checkpoint_builder.ts +65 -53
  80. package/src/test/utils.ts +5 -2
@@ -4,6 +4,7 @@ import type { EpochCache } from '@aztec/epoch-cache';
4
4
  import type { L1ContractsConfig } from '@aztec/ethereum/config';
5
5
  import {
6
6
  type EmpireSlashingProposerContract,
7
+ FeeAssetPriceOracle,
7
8
  type GovernanceProposerContract,
8
9
  type IEmpireBase,
9
10
  MULTI_CALL_3_ADDRESS,
@@ -18,33 +19,48 @@ import {
18
19
  type L1BlobInputs,
19
20
  type L1TxConfig,
20
21
  type L1TxRequest,
22
+ type L1TxUtils,
23
+ MAX_L1_TX_LIMIT,
21
24
  type TransactionStats,
22
25
  WEI_CONST,
23
26
  } 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';
27
+ import { FormattedViemError, formatViemError, mergeAbis, tryExtractEvent } from '@aztec/ethereum/utils';
26
28
  import { sumBigint } from '@aztec/foundation/bigint';
27
29
  import { toHex as toPaddedHex } from '@aztec/foundation/bigint-buffer';
28
30
  import { CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
31
+ import { trimmedBytesLength } from '@aztec/foundation/buffer';
29
32
  import { pick } from '@aztec/foundation/collection';
30
33
  import type { Fr } from '@aztec/foundation/curves/bn254';
34
+ import { TimeoutError } from '@aztec/foundation/error';
31
35
  import { EthAddress } from '@aztec/foundation/eth-address';
32
36
  import { Signature, type ViemSignature } from '@aztec/foundation/eth-signature';
33
37
  import { type Logger, createLogger } from '@aztec/foundation/log';
38
+ import { makeBackoff, retry } from '@aztec/foundation/retry';
34
39
  import { bufferToHex } from '@aztec/foundation/string';
35
40
  import { DateProvider, Timer } from '@aztec/foundation/timer';
36
41
  import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
37
42
  import { type ProposerSlashAction, encodeSlashConsensusVotes } from '@aztec/slasher';
38
43
  import { CommitteeAttestationsAndSigners, type ValidateCheckpointResult } from '@aztec/stdlib/block';
39
44
  import type { Checkpoint } from '@aztec/stdlib/checkpoint';
45
+ import { getNextL1SlotTimestamp } from '@aztec/stdlib/epoch-helpers';
40
46
  import { SlashFactoryContract } from '@aztec/stdlib/l1-contracts';
41
47
  import type { CheckpointHeader } from '@aztec/stdlib/rollup';
42
48
  import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
43
49
  import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
44
50
 
45
- import { type StateOverride, type TransactionReceipt, type TypedDataDefinition, encodeFunctionData, toHex } from 'viem';
46
-
47
- import type { PublisherConfig, TxSenderConfig } from './config.js';
51
+ import {
52
+ type Hex,
53
+ type StateOverride,
54
+ type TransactionReceipt,
55
+ type TypedDataDefinition,
56
+ encodeFunctionData,
57
+ keccak256,
58
+ multicall3Abi,
59
+ toHex,
60
+ } from 'viem';
61
+
62
+ import type { SequencerPublisherConfig } from './config.js';
63
+ import { type FailedL1Tx, type L1TxFailedStore, createL1TxFailedStore } from './l1_tx_failed_store/index.js';
48
64
  import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
49
65
 
50
66
  /** Arguments to the process method of the rollup contract */
@@ -59,6 +75,8 @@ type L1ProcessArgs = {
59
75
  attestationsAndSigners: CommitteeAttestationsAndSigners;
60
76
  /** Attestations and signers signature */
61
77
  attestationsAndSignersSignature: Signature;
78
+ /** The fee asset price modifier in basis points (from oracle) */
79
+ feeAssetPriceModifier: bigint;
62
80
  };
63
81
 
64
82
  export const Actions = [
@@ -104,6 +122,7 @@ export class SequencerPublisher {
104
122
  private interrupted = false;
105
123
  private metrics: SequencerPublisherMetrics;
106
124
  public epochCache: EpochCache;
125
+ private failedTxStore?: Promise<L1TxFailedStore | undefined>;
107
126
 
108
127
  protected governanceLog = createLogger('sequencer:publisher:governance');
109
128
  protected slashingLog = createLogger('sequencer:publisher:slashing');
@@ -111,21 +130,26 @@ export class SequencerPublisher {
111
130
  protected lastActions: Partial<Record<Action, SlotNumber>> = {};
112
131
 
113
132
  private isPayloadEmptyCache: Map<string, boolean> = new Map<string, boolean>();
133
+ private payloadProposedCache: Set<string> = new Set<string>();
114
134
 
115
135
  protected log: Logger;
116
136
  protected ethereumSlotDuration: bigint;
137
+ protected aztecSlotDuration: bigint;
138
+ private dateProvider: DateProvider;
117
139
 
118
140
  private blobClient: BlobClientInterface;
119
141
 
120
142
  /** Address to use for simulations in fisherman mode (actual proposer's address) */
121
143
  private proposerAddressForSimulation?: EthAddress;
122
144
 
145
+ /** Optional callback to obtain a replacement publisher when the current one fails to send. */
146
+ private getNextPublisher?: (excludeAddresses: EthAddress[]) => Promise<L1TxUtils | undefined>;
147
+
123
148
  /** L1 fee analyzer for fisherman mode */
124
149
  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;
150
+
151
+ /** Fee asset price oracle for computing price modifiers from Uniswap V4 */
152
+ private feeAssetPriceOracle: FeeAssetPriceOracle;
129
153
 
130
154
  // A CALL to a cold address is 2700 gas
131
155
  public static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
@@ -133,7 +157,7 @@ export class SequencerPublisher {
133
157
  // Gas report for VotingWithSigTest shows a max gas of 100k, but we've seen it cost 700k+ in testnet
134
158
  public static VOTE_GAS_GUESS: bigint = 800_000n;
135
159
 
136
- public l1TxUtils: L1TxUtilsWithBlobs;
160
+ public l1TxUtils: L1TxUtils;
137
161
  public rollupContract: RollupContract;
138
162
  public govProposerContract: GovernanceProposerContract;
139
163
  public slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
@@ -144,11 +168,12 @@ export class SequencerPublisher {
144
168
  protected requests: RequestWithExpiry[] = [];
145
169
 
146
170
  constructor(
147
- private config: TxSenderConfig & PublisherConfig & Pick<L1ContractsConfig, 'ethereumSlotDuration'>,
171
+ private config: Pick<SequencerPublisherConfig, 'fishermanMode' | 'l1TxFailedStore'> &
172
+ Pick<L1ContractsConfig, 'ethereumSlotDuration' | 'aztecSlotDuration'> & { l1ChainId: number },
148
173
  deps: {
149
174
  telemetry?: TelemetryClient;
150
175
  blobClient: BlobClientInterface;
151
- l1TxUtils: L1TxUtilsWithBlobs;
176
+ l1TxUtils: L1TxUtils;
152
177
  rollupContract: RollupContract;
153
178
  slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
154
179
  governanceProposerContract: GovernanceProposerContract;
@@ -158,10 +183,13 @@ export class SequencerPublisher {
158
183
  metrics: SequencerPublisherMetrics;
159
184
  lastActions: Partial<Record<Action, SlotNumber>>;
160
185
  log?: Logger;
186
+ getNextPublisher?: (excludeAddresses: EthAddress[]) => Promise<L1TxUtils | undefined>;
161
187
  },
162
188
  ) {
163
189
  this.log = deps.log ?? createLogger('sequencer:publisher');
164
190
  this.ethereumSlotDuration = BigInt(config.ethereumSlotDuration);
191
+ this.aztecSlotDuration = BigInt(config.aztecSlotDuration);
192
+ this.dateProvider = deps.dateProvider;
165
193
  this.epochCache = deps.epochCache;
166
194
  this.lastActions = deps.lastActions;
167
195
 
@@ -171,6 +199,7 @@ export class SequencerPublisher {
171
199
  this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
172
200
  this.tracer = telemetry.getTracer('SequencerPublisher');
173
201
  this.l1TxUtils = deps.l1TxUtils;
202
+ this.getNextPublisher = deps.getNextPublisher;
174
203
 
175
204
  this.rollupContract = deps.rollupContract;
176
205
 
@@ -192,12 +221,52 @@ export class SequencerPublisher {
192
221
  createLogger('sequencer:publisher:fee-analyzer'),
193
222
  );
194
223
  }
224
+
225
+ // Initialize fee asset price oracle
226
+ this.feeAssetPriceOracle = new FeeAssetPriceOracle(
227
+ this.l1TxUtils.client,
228
+ this.rollupContract,
229
+ createLogger('sequencer:publisher:price-oracle'),
230
+ );
231
+
232
+ // Initialize failed L1 tx store (optional, for test networks)
233
+ this.failedTxStore = createL1TxFailedStore(config.l1TxFailedStore, this.log);
234
+ }
235
+
236
+ /**
237
+ * Backs up a failed L1 transaction to the configured store for debugging.
238
+ * Does nothing if no store is configured.
239
+ */
240
+ private backupFailedTx(failedTx: Omit<FailedL1Tx, 'timestamp'>): void {
241
+ if (!this.failedTxStore) {
242
+ return;
243
+ }
244
+
245
+ const tx: FailedL1Tx = {
246
+ ...failedTx,
247
+ timestamp: Date.now(),
248
+ };
249
+
250
+ // Fire and forget - don't block on backup
251
+ void this.failedTxStore
252
+ .then(store => store?.saveFailedTx(tx))
253
+ .catch(err => {
254
+ this.log.warn(`Failed to backup failed L1 tx to store`, err);
255
+ });
195
256
  }
196
257
 
197
258
  public getRollupContract(): RollupContract {
198
259
  return this.rollupContract;
199
260
  }
200
261
 
262
+ /**
263
+ * Gets the fee asset price modifier from the oracle.
264
+ * Returns 0n if the oracle query fails.
265
+ */
266
+ public getFeeAssetPriceModifier(): Promise<bigint> {
267
+ return this.feeAssetPriceOracle.computePriceModifier();
268
+ }
269
+
201
270
  public getSenderAddress() {
202
271
  return this.l1TxUtils.getSenderAddress();
203
272
  }
@@ -222,7 +291,7 @@ export class SequencerPublisher {
222
291
  }
223
292
 
224
293
  public getCurrentL2Slot(): SlotNumber {
225
- return this.epochCache.getEpochAndSlotNow().slot;
294
+ return this.epochCache.getSlotNow();
226
295
  }
227
296
 
228
297
  /**
@@ -273,7 +342,7 @@ export class SequencerPublisher {
273
342
  // Start the analysis
274
343
  const analysisId = await this.l1FeeAnalyzer.startAnalysis(
275
344
  l2SlotNumber,
276
- gasLimit > 0n ? gasLimit : SequencerPublisher.PROPOSE_GAS_GUESS,
345
+ gasLimit > 0n ? gasLimit : MAX_L1_TX_LIMIT,
277
346
  l1Requests,
278
347
  blobConfig,
279
348
  onComplete,
@@ -335,8 +404,8 @@ export class SequencerPublisher {
335
404
  // @note - we can only have one blob config per bundle
336
405
  // find requests with gas and blob configs
337
406
  // 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);
407
+ const gasConfigs = validRequests.filter(request => request.gasConfig).map(request => request.gasConfig);
408
+ const blobConfigs = validRequests.filter(request => request.blobConfig).map(request => request.blobConfig);
340
409
 
341
410
  if (blobConfigs.length > 1) {
342
411
  throw new Error('Multiple blob configs found');
@@ -346,7 +415,16 @@ export class SequencerPublisher {
346
415
 
347
416
  // Merge gasConfigs. Yields the sum of gasLimits, and the earliest txTimeoutAt, or undefined if no gasConfig sets them.
348
417
  const gasLimits = gasConfigs.map(g => g?.gasLimit).filter((g): g is bigint => g !== undefined);
349
- const gasLimit = gasLimits.length > 0 ? sumBigint(gasLimits) : undefined; // sum
418
+ let gasLimit = gasLimits.length > 0 ? sumBigint(gasLimits) : undefined; // sum
419
+ // Cap at L1 block gas limit so the node accepts the tx ("gas limit too high" otherwise).
420
+ const maxGas = MAX_L1_TX_LIMIT;
421
+ if (gasLimit !== undefined && gasLimit > maxGas) {
422
+ this.log.debug('Capping bundled tx gas limit to L1 max', {
423
+ requested: gasLimit,
424
+ capped: maxGas,
425
+ });
426
+ gasLimit = maxGas;
427
+ }
350
428
  const txTimeoutAts = gasConfigs.map(g => g?.txTimeoutAt).filter((g): g is Date => g !== undefined);
351
429
  const txTimeoutAt = txTimeoutAts.length > 0 ? new Date(Math.min(...txTimeoutAts.map(g => g.getTime()))) : undefined; // earliest
352
430
  const txConfig: RequestWithExpiry['gasConfig'] = { gasLimit, txTimeoutAt };
@@ -356,19 +434,36 @@ export class SequencerPublisher {
356
434
  validRequests.sort((a, b) => compareActions(a.action, b.action));
357
435
 
358
436
  try {
437
+ // Capture context for failed tx backup before sending
438
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
439
+ const multicallData = encodeFunctionData({
440
+ abi: multicall3Abi,
441
+ functionName: 'aggregate3',
442
+ args: [
443
+ validRequests.map(r => ({
444
+ target: r.request.to!,
445
+ callData: r.request.data!,
446
+ allowFailure: true,
447
+ })),
448
+ ],
449
+ });
450
+ const blobDataHex = blobConfig?.blobs?.map(b => toHex(b)) as Hex[] | undefined;
451
+
452
+ const txContext = { multicallData, blobData: blobDataHex, l1BlockNumber };
453
+
359
454
  this.log.debug('Forwarding transactions', {
360
455
  validRequests: validRequests.map(request => request.action),
361
456
  txConfig,
362
457
  });
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,
458
+ const result = await this.forwardWithPublisherRotation(validRequests, txConfig, blobConfig);
459
+ if (result === undefined) {
460
+ return undefined;
461
+ }
462
+ const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(
463
+ validRequests,
464
+ result,
465
+ txContext,
370
466
  );
371
- const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(validRequests, result);
372
467
  return { result, expiredActions, sentActions: validActions, successfulActions, failedActions };
373
468
  } catch (err) {
374
469
  const viemError = formatViemError(err);
@@ -386,16 +481,88 @@ export class SequencerPublisher {
386
481
  }
387
482
  }
388
483
 
484
+ /**
485
+ * Forwards transactions via Multicall3, rotating to the next available publisher if a send
486
+ * failure occurs (i.e. the tx never reached the chain).
487
+ * On-chain reverts and simulation errors are returned as-is without rotation.
488
+ */
489
+ private async forwardWithPublisherRotation(
490
+ validRequests: RequestWithExpiry[],
491
+ txConfig: RequestWithExpiry['gasConfig'],
492
+ blobConfig: L1BlobInputs | undefined,
493
+ ) {
494
+ const triedAddresses: EthAddress[] = [];
495
+ let currentPublisher = this.l1TxUtils;
496
+
497
+ while (true) {
498
+ triedAddresses.push(currentPublisher.getSenderAddress());
499
+ try {
500
+ const result = await Multicall3.forward(
501
+ validRequests.map(r => r.request),
502
+ currentPublisher,
503
+ txConfig,
504
+ blobConfig,
505
+ this.rollupContract.address,
506
+ this.log,
507
+ );
508
+ this.l1TxUtils = currentPublisher;
509
+ return result;
510
+ } catch (err) {
511
+ if (err instanceof TimeoutError) {
512
+ throw err;
513
+ }
514
+ const viemError = formatViemError(err);
515
+ if (!this.getNextPublisher) {
516
+ this.log.error('Failed to publish bundled transactions', viemError);
517
+ return undefined;
518
+ }
519
+ this.log.warn(
520
+ `Publisher ${currentPublisher.getSenderAddress()} failed to send, rotating to next publisher`,
521
+ viemError,
522
+ );
523
+ const nextPublisher = await this.getNextPublisher([...triedAddresses]);
524
+ if (!nextPublisher) {
525
+ this.log.error('All available publishers exhausted, failed to publish bundled transactions');
526
+ return undefined;
527
+ }
528
+ currentPublisher = nextPublisher;
529
+ }
530
+ }
531
+ }
532
+
389
533
  private callbackBundledTransactions(
390
534
  requests: RequestWithExpiry[],
391
- result?: { receipt: TransactionReceipt } | FormattedViemError,
535
+ result: { receipt: TransactionReceipt; errorMsg?: string } | FormattedViemError | undefined,
536
+ txContext: { multicallData: Hex; blobData?: Hex[]; l1BlockNumber: bigint },
392
537
  ) {
393
538
  const actionsListStr = requests.map(r => r.action).join(', ');
394
539
  if (result instanceof FormattedViemError) {
395
540
  this.log.error(`Failed to publish bundled transactions (${actionsListStr})`, result);
541
+ this.backupFailedTx({
542
+ id: keccak256(txContext.multicallData),
543
+ failureType: 'send-error',
544
+ request: { to: MULTI_CALL_3_ADDRESS, data: txContext.multicallData },
545
+ blobData: txContext.blobData,
546
+ l1BlockNumber: txContext.l1BlockNumber.toString(),
547
+ error: { message: result.message, name: result.name },
548
+ context: {
549
+ actions: requests.map(r => r.action),
550
+ requests: requests.map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
551
+ sender: this.getSenderAddress().toString(),
552
+ },
553
+ });
396
554
  return { failedActions: requests.map(r => r.action) };
397
555
  } else {
398
- this.log.verbose(`Published bundled transactions (${actionsListStr})`, { result, requests });
556
+ this.log.verbose(`Published bundled transactions (${actionsListStr})`, {
557
+ result,
558
+ requests: requests.map(r => ({
559
+ ...r,
560
+ // Avoid logging large blob data
561
+ blobConfig: r.blobConfig
562
+ ? { ...r.blobConfig, blobs: r.blobConfig.blobs.map(b => ({ size: trimmedBytesLength(b) })) }
563
+ : undefined,
564
+ })),
565
+ });
399
566
  const successfulActions: Action[] = [];
400
567
  const failedActions: Action[] = [];
401
568
  for (const request of requests) {
@@ -405,25 +572,53 @@ export class SequencerPublisher {
405
572
  failedActions.push(request.action);
406
573
  }
407
574
  }
575
+ // Single backup for the whole reverted tx
576
+ if (failedActions.length > 0 && result?.receipt?.status === 'reverted') {
577
+ this.backupFailedTx({
578
+ id: result.receipt.transactionHash,
579
+ failureType: 'revert',
580
+ request: { to: MULTI_CALL_3_ADDRESS, data: txContext.multicallData },
581
+ blobData: txContext.blobData,
582
+ l1BlockNumber: result.receipt.blockNumber.toString(),
583
+ receipt: {
584
+ transactionHash: result.receipt.transactionHash,
585
+ blockNumber: result.receipt.blockNumber.toString(),
586
+ gasUsed: (result.receipt.gasUsed ?? 0n).toString(),
587
+ status: 'reverted',
588
+ },
589
+ error: { message: result.errorMsg ?? 'Transaction reverted' },
590
+ context: {
591
+ actions: failedActions,
592
+ requests: requests
593
+ .filter(r => failedActions.includes(r.action))
594
+ .map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
595
+ sender: this.getSenderAddress().toString(),
596
+ },
597
+ });
598
+ }
408
599
  return { successfulActions, failedActions };
409
600
  }
410
601
  }
411
602
 
412
603
  /**
413
- * @notice Will call `canProposeAtNextEthBlock` to make sure that it is possible to propose
604
+ * @notice Will call `canProposeAt` to make sure that it is possible to propose
414
605
  * @param tipArchive - The archive to check
415
606
  * @returns The slot and block number if it is possible to propose, undefined otherwise
416
607
  */
417
- public canProposeAtNextEthBlock(
608
+ public canProposeAt(
418
609
  tipArchive: Fr,
419
610
  msgSender: EthAddress,
420
- opts: { forcePendingCheckpointNumber?: CheckpointNumber } = {},
611
+ opts: { forcePendingCheckpointNumber?: CheckpointNumber; pipelined?: boolean } = {},
421
612
  ) {
422
613
  // TODO: #14291 - should loop through multiple keys to check if any of them can propose
423
614
  const ignoredErrors = ['SlotAlreadyInChain', 'InvalidProposer', 'InvalidArchive'];
424
615
 
616
+ const pipelined = opts.pipelined ?? this.epochCache.isProposerPipeliningEnabled();
617
+ const slotOffset = pipelined ? this.aztecSlotDuration : 0n;
618
+ const nextL1SlotTs = this.getNextL1SlotTimestamp() + slotOffset;
619
+
425
620
  return this.rollupContract
426
- .canProposeAtNextEthBlock(tipArchive.toBuffer(), msgSender.toString(), Number(this.ethereumSlotDuration), {
621
+ .canProposeAt(tipArchive.toBuffer(), msgSender.toString(), nextL1SlotTs, {
427
622
  forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
428
623
  })
429
624
  .catch(err => {
@@ -437,6 +632,7 @@ export class SequencerPublisher {
437
632
  return undefined;
438
633
  });
439
634
  }
635
+
440
636
  /**
441
637
  * @notice Will simulate `validateHeader` to make sure that the block header is valid
442
638
  * @dev This is a convenience function that can be used by the sequencer to validate a "partial" header.
@@ -460,7 +656,7 @@ export class SequencerPublisher {
460
656
  flags,
461
657
  ] as const;
462
658
 
463
- const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
659
+ const ts = this.getNextL1SlotTimestamp();
464
660
  const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(
465
661
  opts?.forcePendingCheckpointNumber,
466
662
  );
@@ -516,8 +712,15 @@ export class SequencerPublisher {
516
712
  const request = this.buildInvalidateCheckpointRequest(validationResult);
517
713
  this.log.debug(`Simulating invalidate checkpoint ${checkpointNumber}`, { ...logData, request });
518
714
 
715
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
716
+
519
717
  try {
520
- const { gasUsed } = await this.l1TxUtils.simulate(request, undefined, undefined, ErrorsAbi);
718
+ const { gasUsed } = await this.l1TxUtils.simulate(
719
+ request,
720
+ undefined,
721
+ undefined,
722
+ mergeAbis([request.abi ?? [], ErrorsAbi]),
723
+ );
521
724
  this.log.verbose(`Simulation for invalidate checkpoint ${checkpointNumber} succeeded`, {
522
725
  ...logData,
523
726
  request,
@@ -536,7 +739,7 @@ export class SequencerPublisher {
536
739
 
537
740
  // If the error is due to the checkpoint not being in the pending chain, and it was indeed removed by someone else,
538
741
  // we can safely ignore it and return undefined so we go ahead with checkpoint building.
539
- if (viemError.message?.includes('Rollup__BlockNotInPendingChain')) {
742
+ if (viemError.message?.includes('Rollup__CheckpointNotInPendingChain')) {
540
743
  this.log.verbose(
541
744
  `Simulation for invalidate checkpoint ${checkpointNumber} failed due to checkpoint not being in pending chain`,
542
745
  { ...logData, request, error: viemError.message },
@@ -562,6 +765,18 @@ export class SequencerPublisher {
562
765
 
563
766
  // Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
564
767
  this.log.error(`Simulation for invalidate checkpoint ${checkpointNumber} failed`, viemError, logData);
768
+ this.backupFailedTx({
769
+ id: keccak256(request.data!),
770
+ failureType: 'simulation',
771
+ request: { to: request.to!, data: request.data!, value: request.value?.toString() },
772
+ l1BlockNumber: l1BlockNumber.toString(),
773
+ error: { message: viemError.message, name: viemError.name },
774
+ context: {
775
+ actions: [`invalidate-${reason}`],
776
+ checkpointNumber,
777
+ sender: this.getSenderAddress().toString(),
778
+ },
779
+ });
565
780
  throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, { cause: viemError });
566
781
  }
567
782
  }
@@ -606,25 +821,11 @@ export class SequencerPublisher {
606
821
  attestationsAndSignersSignature: Signature,
607
822
  options: { forcePendingCheckpointNumber?: CheckpointNumber },
608
823
  ): 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
-
824
+ // Anchor the simulation timestamp to the checkpoint's own slot start time
825
+ // rather than the current L1 block timestamp, which may overshoot into the next slot if the build ran late.
826
+ const ts = checkpoint.header.timestamp;
626
827
  const blobFields = checkpoint.toBlobFields();
627
- const blobs = getBlobsPerL1Block(blobFields);
828
+ const blobs = await getBlobsPerL1Block(blobFields);
628
829
  const blobInput = getPrefixedEthBlobCommitments(blobs);
629
830
 
630
831
  const args = [
@@ -632,7 +833,7 @@ export class SequencerPublisher {
632
833
  header: checkpoint.header.toViem(),
633
834
  archive: toHex(checkpoint.archive.root.toBuffer()),
634
835
  oracleInput: {
635
- feeAssetPriceModifier: 0n,
836
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
636
837
  },
637
838
  },
638
839
  attestationsAndSigners.getPackedAttestations(),
@@ -681,6 +882,32 @@ export class SequencerPublisher {
681
882
  return false;
682
883
  }
683
884
 
885
+ // Check if payload was already submitted to governance
886
+ const cacheKey = payload.toString();
887
+ if (!this.payloadProposedCache.has(cacheKey)) {
888
+ try {
889
+ const l1StartBlock = await this.rollupContract.getL1StartBlock();
890
+ const proposed = await retry(
891
+ () => base.hasPayloadBeenProposed(payload.toString(), l1StartBlock),
892
+ 'Check if payload was proposed',
893
+ makeBackoff([0, 1, 2]),
894
+ this.log,
895
+ true,
896
+ );
897
+ if (proposed) {
898
+ this.payloadProposedCache.add(cacheKey);
899
+ }
900
+ } catch (err) {
901
+ this.log.warn(`Failed to check if payload ${payload} was proposed after retries, skipping signal`, err);
902
+ return false;
903
+ }
904
+ }
905
+
906
+ if (this.payloadProposedCache.has(cacheKey)) {
907
+ this.log.info(`Payload ${payload} was already proposed to governance, stopping signals`);
908
+ return false;
909
+ }
910
+
684
911
  const cachedLastVote = this.lastActions[signalType];
685
912
  this.lastActions[signalType] = slotNumber;
686
913
  const action = signalType;
@@ -699,11 +926,26 @@ export class SequencerPublisher {
699
926
  lastValidL2Slot: slotNumber,
700
927
  });
701
928
 
929
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
930
+
702
931
  try {
703
- await this.l1TxUtils.simulate(request, { time: timestamp }, [], ErrorsAbi);
932
+ await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
704
933
  this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
705
934
  } catch (err) {
706
- this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, err);
935
+ const viemError = formatViemError(err);
936
+ this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, viemError);
937
+ this.backupFailedTx({
938
+ id: keccak256(request.data!),
939
+ failureType: 'simulation',
940
+ request: { to: request.to!, data: request.data!, value: request.value?.toString() },
941
+ l1BlockNumber: l1BlockNumber.toString(),
942
+ error: { message: viemError.message, name: viemError.name },
943
+ context: {
944
+ actions: [action],
945
+ slot: slotNumber,
946
+ sender: this.getSenderAddress().toString(),
947
+ },
948
+ });
707
949
  // Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
708
950
  }
709
951
 
@@ -908,14 +1150,15 @@ export class SequencerPublisher {
908
1150
  const checkpointHeader = checkpoint.header;
909
1151
 
910
1152
  const blobFields = checkpoint.toBlobFields();
911
- const blobs = getBlobsPerL1Block(blobFields);
1153
+ const blobs = await getBlobsPerL1Block(blobFields);
912
1154
 
913
- const proposeTxArgs = {
1155
+ const proposeTxArgs: L1ProcessArgs = {
914
1156
  header: checkpointHeader,
915
1157
  archive: checkpoint.archive.root.toBuffer(),
916
1158
  blobs,
917
1159
  attestationsAndSigners,
918
1160
  attestationsAndSignersSignature,
1161
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
919
1162
  };
920
1163
 
921
1164
  let ts: bigint;
@@ -998,13 +1241,30 @@ export class SequencerPublisher {
998
1241
 
999
1242
  this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
1000
1243
 
1244
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1245
+
1001
1246
  let gasUsed: bigint;
1247
+ const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
1002
1248
  try {
1003
- ({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], ErrorsAbi)); // TODO(palla/slash): Check the timestamp logic
1249
+ ({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], simulateAbi)); // TODO(palla/slash): Check the timestamp logic
1004
1250
  this.log.verbose(`Simulation for ${action} succeeded`, { ...logData, request, gasUsed });
1005
1251
  } catch (err) {
1006
- const viemError = formatViemError(err);
1252
+ const viemError = formatViemError(err, simulateAbi);
1007
1253
  this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
1254
+
1255
+ this.backupFailedTx({
1256
+ id: keccak256(request.data!),
1257
+ failureType: 'simulation',
1258
+ request: { to: request.to!, data: request.data!, value: request.value?.toString() },
1259
+ l1BlockNumber: l1BlockNumber.toString(),
1260
+ error: { message: viemError.message, name: viemError.name },
1261
+ context: {
1262
+ actions: [action],
1263
+ slot: slotNumber,
1264
+ sender: this.getSenderAddress().toString(),
1265
+ },
1266
+ });
1267
+
1008
1268
  return false;
1009
1269
  }
1010
1270
 
@@ -1012,10 +1272,14 @@ export class SequencerPublisher {
1012
1272
  const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil((Number(gasUsed) * 64) / 63)));
1013
1273
  logData.gasLimit = gasLimit;
1014
1274
 
1275
+ // Store the ABI used for simulation on the request so Multicall3.forward can decode errors
1276
+ // when the tx is sent and a revert is diagnosed via simulation.
1277
+ const requestWithAbi = { ...request, abi: simulateAbi };
1278
+
1015
1279
  this.log.debug(`Enqueuing ${action}`, logData);
1016
1280
  this.addRequest({
1017
1281
  action,
1018
- request,
1282
+ request: requestWithAbi,
1019
1283
  gasConfig: { gasLimit },
1020
1284
  lastValidL2Slot: slotNumber,
1021
1285
  checkSuccess: (_req, result) => {
@@ -1084,9 +1348,27 @@ export class SequencerPublisher {
1084
1348
  kzg,
1085
1349
  },
1086
1350
  )
1087
- .catch(err => {
1088
- const { message, metaMessages } = formatViemError(err);
1089
- this.log.error(`Failed to validate blobs`, message, { metaMessages });
1351
+ .catch(async err => {
1352
+ const viemError = formatViemError(err);
1353
+ this.log.error(`Failed to validate blobs`, viemError.message, { metaMessages: viemError.metaMessages });
1354
+ const validateBlobsData = encodeFunctionData({
1355
+ abi: RollupAbi,
1356
+ functionName: 'validateBlobs',
1357
+ args: [blobInput],
1358
+ });
1359
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1360
+ this.backupFailedTx({
1361
+ id: keccak256(validateBlobsData),
1362
+ failureType: 'simulation',
1363
+ request: { to: this.rollupContract.address as Hex, data: validateBlobsData },
1364
+ blobData: encodedData.blobs.map(b => toHex(b.data)) as Hex[],
1365
+ l1BlockNumber: l1BlockNumber.toString(),
1366
+ error: { message: viemError.message, name: viemError.name },
1367
+ context: {
1368
+ actions: ['validate-blobs'],
1369
+ sender: this.getSenderAddress().toString(),
1370
+ },
1371
+ });
1090
1372
  throw new Error('Failed to validate blobs');
1091
1373
  });
1092
1374
  }
@@ -1097,8 +1379,7 @@ export class SequencerPublisher {
1097
1379
  header: encodedData.header.toViem(),
1098
1380
  archive: toHex(encodedData.archive),
1099
1381
  oracleInput: {
1100
- // We are currently not modifying these. See #9963
1101
- feeAssetPriceModifier: 0n,
1382
+ feeAssetPriceModifier: encodedData.feeAssetPriceModifier,
1102
1383
  },
1103
1384
  },
1104
1385
  encodedData.attestationsAndSigners.getPackedAttestations(),
@@ -1124,7 +1405,7 @@ export class SequencerPublisher {
1124
1405
  readonly header: ViemHeader;
1125
1406
  readonly archive: `0x${string}`;
1126
1407
  readonly oracleInput: {
1127
- readonly feeAssetPriceModifier: 0n;
1408
+ readonly feeAssetPriceModifier: bigint;
1128
1409
  };
1129
1410
  },
1130
1411
  ViemCommitteeAttestations,
@@ -1166,25 +1447,27 @@ export class SequencerPublisher {
1166
1447
  });
1167
1448
  }
1168
1449
 
1450
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1451
+
1169
1452
  const simulationResult = await this.l1TxUtils
1170
1453
  .simulate(
1171
1454
  {
1172
1455
  to: this.rollupContract.address,
1173
1456
  data: rollupData,
1174
- gas: SequencerPublisher.PROPOSE_GAS_GUESS,
1457
+ gas: MAX_L1_TX_LIMIT,
1175
1458
  ...(this.proposerAddressForSimulation && { from: this.proposerAddressForSimulation.toString() }),
1176
1459
  },
1177
1460
  {
1178
1461
  // @note we add 1n to the timestamp because geth implementation doesn't like simulation timestamp to be equal to the current block timestamp
1179
1462
  time: timestamp + 1n,
1180
1463
  // @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,
1464
+ gasLimit: MAX_L1_TX_LIMIT * 2n,
1182
1465
  },
1183
1466
  stateOverrides,
1184
1467
  RollupAbi,
1185
1468
  {
1186
1469
  // @note fallback gas estimate to use if the node doesn't support simulation API
1187
- fallbackGasEstimate: SequencerPublisher.PROPOSE_GAS_GUESS,
1470
+ fallbackGasEstimate: MAX_L1_TX_LIMIT,
1188
1471
  },
1189
1472
  )
1190
1473
  .catch(err => {
@@ -1194,11 +1477,23 @@ export class SequencerPublisher {
1194
1477
  this.log.debug(`Ignoring expected ValidatorSelection__MissingProposerSignature error in fisherman mode`);
1195
1478
  // Return a minimal simulation result with the fallback gas estimate
1196
1479
  return {
1197
- gasUsed: SequencerPublisher.PROPOSE_GAS_GUESS,
1480
+ gasUsed: MAX_L1_TX_LIMIT,
1198
1481
  logs: [],
1199
1482
  };
1200
1483
  }
1201
1484
  this.log.error(`Failed to simulate propose tx`, viemError);
1485
+ this.backupFailedTx({
1486
+ id: keccak256(rollupData),
1487
+ failureType: 'simulation',
1488
+ request: { to: this.rollupContract.address, data: rollupData },
1489
+ l1BlockNumber: l1BlockNumber.toString(),
1490
+ error: { message: viemError.message, name: viemError.name },
1491
+ context: {
1492
+ actions: ['propose'],
1493
+ slot: Number(args[0].header.slotNumber),
1494
+ sender: this.getSenderAddress().toString(),
1495
+ },
1496
+ });
1202
1497
  throw err;
1203
1498
  });
1204
1499
 
@@ -1294,4 +1589,10 @@ export class SequencerPublisher {
1294
1589
  },
1295
1590
  });
1296
1591
  }
1592
+
1593
+ /** Returns the timestamp to use when simulating L1 proposal calls */
1594
+ private getNextL1SlotTimestamp(): bigint {
1595
+ const l1Constants = this.epochCache.getL1Constants();
1596
+ return getNextL1SlotTimestamp(this.dateProvider.nowInSeconds(), l1Constants);
1597
+ }
1297
1598
  }