@aztec/sequencer-client 0.0.1-commit.6d3c34e → 0.0.1-commit.7035c9bd6

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