@aztec/sequencer-client 0.0.1-commit.87a0206 → 0.0.1-commit.88c5703d4

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 (74) hide show
  1. package/dest/client/sequencer-client.d.ts +24 -7
  2. package/dest/client/sequencer-client.d.ts.map +1 -1
  3. package/dest/client/sequencer-client.js +101 -16
  4. package/dest/config.d.ts +25 -6
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +49 -28
  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 +5 -4
  10. package/dest/publisher/config.d.ts +35 -17
  11. package/dest/publisher/config.d.ts.map +1 -1
  12. package/dest/publisher/config.js +106 -42
  13. package/dest/publisher/index.d.ts +2 -1
  14. package/dest/publisher/index.d.ts.map +1 -1
  15. package/dest/publisher/l1_tx_failed_store/factory.d.ts +11 -0
  16. package/dest/publisher/l1_tx_failed_store/factory.d.ts.map +1 -0
  17. package/dest/publisher/l1_tx_failed_store/factory.js +22 -0
  18. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts +59 -0
  19. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -0
  20. package/dest/publisher/l1_tx_failed_store/failed_tx_store.js +1 -0
  21. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts +15 -0
  22. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts.map +1 -0
  23. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.js +34 -0
  24. package/dest/publisher/l1_tx_failed_store/index.d.ts +4 -0
  25. package/dest/publisher/l1_tx_failed_store/index.d.ts.map +1 -0
  26. package/dest/publisher/l1_tx_failed_store/index.js +2 -0
  27. package/dest/publisher/sequencer-publisher-factory.d.ts +11 -3
  28. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  29. package/dest/publisher/sequencer-publisher-factory.js +27 -2
  30. package/dest/publisher/sequencer-publisher.d.ts +26 -7
  31. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  32. package/dest/publisher/sequencer-publisher.js +310 -31
  33. package/dest/sequencer/checkpoint_proposal_job.d.ts +2 -4
  34. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  35. package/dest/sequencer/checkpoint_proposal_job.js +122 -73
  36. package/dest/sequencer/metrics.d.ts +17 -5
  37. package/dest/sequencer/metrics.d.ts.map +1 -1
  38. package/dest/sequencer/metrics.js +86 -15
  39. package/dest/sequencer/sequencer.d.ts +26 -13
  40. package/dest/sequencer/sequencer.d.ts.map +1 -1
  41. package/dest/sequencer/sequencer.js +36 -39
  42. package/dest/sequencer/timetable.d.ts +4 -6
  43. package/dest/sequencer/timetable.d.ts.map +1 -1
  44. package/dest/sequencer/timetable.js +7 -11
  45. package/dest/sequencer/types.d.ts +2 -2
  46. package/dest/sequencer/types.d.ts.map +1 -1
  47. package/dest/test/index.d.ts +3 -5
  48. package/dest/test/index.d.ts.map +1 -1
  49. package/dest/test/mock_checkpoint_builder.d.ts +14 -10
  50. package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
  51. package/dest/test/mock_checkpoint_builder.js +45 -34
  52. package/dest/test/utils.d.ts +3 -3
  53. package/dest/test/utils.d.ts.map +1 -1
  54. package/dest/test/utils.js +5 -4
  55. package/package.json +27 -28
  56. package/src/client/sequencer-client.ts +135 -18
  57. package/src/config.ts +64 -38
  58. package/src/global_variable_builder/global_builder.ts +4 -3
  59. package/src/publisher/config.ts +121 -43
  60. package/src/publisher/index.ts +3 -0
  61. package/src/publisher/l1_tx_failed_store/factory.ts +32 -0
  62. package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +55 -0
  63. package/src/publisher/l1_tx_failed_store/file_store_failed_tx_store.ts +46 -0
  64. package/src/publisher/l1_tx_failed_store/index.ts +3 -0
  65. package/src/publisher/sequencer-publisher-factory.ts +38 -6
  66. package/src/publisher/sequencer-publisher.ts +311 -44
  67. package/src/sequencer/checkpoint_proposal_job.ts +167 -77
  68. package/src/sequencer/metrics.ts +92 -18
  69. package/src/sequencer/sequencer.ts +45 -45
  70. package/src/sequencer/timetable.ts +13 -12
  71. package/src/sequencer/types.ts +1 -1
  72. package/src/test/index.ts +2 -4
  73. package/src/test/mock_checkpoint_builder.ts +62 -48
  74. 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,20 +19,23 @@ import {
18
19
  type L1BlobInputs,
19
20
  type L1TxConfig,
20
21
  type L1TxRequest,
22
+ type L1TxUtils,
21
23
  MAX_L1_TX_LIMIT,
22
24
  type TransactionStats,
23
25
  WEI_CONST,
24
26
  } from '@aztec/ethereum/l1-tx-utils';
25
- import type { L1TxUtilsWithBlobs } from '@aztec/ethereum/l1-tx-utils-with-blobs';
26
27
  import { FormattedViemError, formatViemError, mergeAbis, tryExtractEvent } from '@aztec/ethereum/utils';
27
28
  import { sumBigint } from '@aztec/foundation/bigint';
28
29
  import { toHex as toPaddedHex } from '@aztec/foundation/bigint-buffer';
29
30
  import { CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
31
+ import { trimmedBytesLength } from '@aztec/foundation/buffer';
30
32
  import { pick } from '@aztec/foundation/collection';
31
33
  import type { Fr } from '@aztec/foundation/curves/bn254';
34
+ import { TimeoutError } from '@aztec/foundation/error';
32
35
  import { EthAddress } from '@aztec/foundation/eth-address';
33
36
  import { Signature, type ViemSignature } from '@aztec/foundation/eth-signature';
34
37
  import { type Logger, createLogger } from '@aztec/foundation/log';
38
+ import { makeBackoff, retry } from '@aztec/foundation/retry';
35
39
  import { bufferToHex } from '@aztec/foundation/string';
36
40
  import { DateProvider, Timer } from '@aztec/foundation/timer';
37
41
  import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
@@ -43,9 +47,19 @@ import type { CheckpointHeader } from '@aztec/stdlib/rollup';
43
47
  import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
44
48
  import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
45
49
 
46
- import { type StateOverride, type TransactionReceipt, type TypedDataDefinition, encodeFunctionData, toHex } from 'viem';
47
-
48
- 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';
49
63
  import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
50
64
 
51
65
  /** Arguments to the process method of the rollup contract */
@@ -60,6 +74,8 @@ type L1ProcessArgs = {
60
74
  attestationsAndSigners: CommitteeAttestationsAndSigners;
61
75
  /** Attestations and signers signature */
62
76
  attestationsAndSignersSignature: Signature;
77
+ /** The fee asset price modifier in basis points (from oracle) */
78
+ feeAssetPriceModifier: bigint;
63
79
  };
64
80
 
65
81
  export const Actions = [
@@ -105,6 +121,7 @@ export class SequencerPublisher {
105
121
  private interrupted = false;
106
122
  private metrics: SequencerPublisherMetrics;
107
123
  public epochCache: EpochCache;
124
+ private failedTxStore?: Promise<L1TxFailedStore | undefined>;
108
125
 
109
126
  protected governanceLog = createLogger('sequencer:publisher:governance');
110
127
  protected slashingLog = createLogger('sequencer:publisher:slashing');
@@ -112,6 +129,7 @@ export class SequencerPublisher {
112
129
  protected lastActions: Partial<Record<Action, SlotNumber>> = {};
113
130
 
114
131
  private isPayloadEmptyCache: Map<string, boolean> = new Map<string, boolean>();
132
+ private payloadProposedCache: Set<string> = new Set<string>();
115
133
 
116
134
  protected log: Logger;
117
135
  protected ethereumSlotDuration: bigint;
@@ -121,15 +139,22 @@ export class SequencerPublisher {
121
139
  /** Address to use for simulations in fisherman mode (actual proposer's address) */
122
140
  private proposerAddressForSimulation?: EthAddress;
123
141
 
142
+ /** Optional callback to obtain a replacement publisher when the current one fails to send. */
143
+ private getNextPublisher?: (excludeAddresses: EthAddress[]) => Promise<L1TxUtils | undefined>;
144
+
124
145
  /** L1 fee analyzer for fisherman mode */
125
146
  private l1FeeAnalyzer?: L1FeeAnalyzer;
147
+
148
+ /** Fee asset price oracle for computing price modifiers from Uniswap V4 */
149
+ private feeAssetPriceOracle: FeeAssetPriceOracle;
150
+
126
151
  // A CALL to a cold address is 2700 gas
127
152
  public static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
128
153
 
129
154
  // Gas report for VotingWithSigTest shows a max gas of 100k, but we've seen it cost 700k+ in testnet
130
155
  public static VOTE_GAS_GUESS: bigint = 800_000n;
131
156
 
132
- public l1TxUtils: L1TxUtilsWithBlobs;
157
+ public l1TxUtils: L1TxUtils;
133
158
  public rollupContract: RollupContract;
134
159
  public govProposerContract: GovernanceProposerContract;
135
160
  public slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
@@ -140,11 +165,12 @@ export class SequencerPublisher {
140
165
  protected requests: RequestWithExpiry[] = [];
141
166
 
142
167
  constructor(
143
- private config: TxSenderConfig & PublisherConfig & Pick<L1ContractsConfig, 'ethereumSlotDuration'>,
168
+ private config: Pick<SequencerPublisherConfig, 'fishermanMode' | 'l1TxFailedStore'> &
169
+ Pick<L1ContractsConfig, 'ethereumSlotDuration'> & { l1ChainId: number },
144
170
  deps: {
145
171
  telemetry?: TelemetryClient;
146
172
  blobClient: BlobClientInterface;
147
- l1TxUtils: L1TxUtilsWithBlobs;
173
+ l1TxUtils: L1TxUtils;
148
174
  rollupContract: RollupContract;
149
175
  slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
150
176
  governanceProposerContract: GovernanceProposerContract;
@@ -154,6 +180,7 @@ export class SequencerPublisher {
154
180
  metrics: SequencerPublisherMetrics;
155
181
  lastActions: Partial<Record<Action, SlotNumber>>;
156
182
  log?: Logger;
183
+ getNextPublisher?: (excludeAddresses: EthAddress[]) => Promise<L1TxUtils | undefined>;
157
184
  },
158
185
  ) {
159
186
  this.log = deps.log ?? createLogger('sequencer:publisher');
@@ -167,6 +194,7 @@ export class SequencerPublisher {
167
194
  this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
168
195
  this.tracer = telemetry.getTracer('SequencerPublisher');
169
196
  this.l1TxUtils = deps.l1TxUtils;
197
+ this.getNextPublisher = deps.getNextPublisher;
170
198
 
171
199
  this.rollupContract = deps.rollupContract;
172
200
 
@@ -188,12 +216,52 @@ export class SequencerPublisher {
188
216
  createLogger('sequencer:publisher:fee-analyzer'),
189
217
  );
190
218
  }
219
+
220
+ // Initialize fee asset price oracle
221
+ this.feeAssetPriceOracle = new FeeAssetPriceOracle(
222
+ this.l1TxUtils.client,
223
+ this.rollupContract,
224
+ createLogger('sequencer:publisher:price-oracle'),
225
+ );
226
+
227
+ // Initialize failed L1 tx store (optional, for test networks)
228
+ this.failedTxStore = createL1TxFailedStore(config.l1TxFailedStore, this.log);
229
+ }
230
+
231
+ /**
232
+ * Backs up a failed L1 transaction to the configured store for debugging.
233
+ * Does nothing if no store is configured.
234
+ */
235
+ private backupFailedTx(failedTx: Omit<FailedL1Tx, 'timestamp'>): void {
236
+ if (!this.failedTxStore) {
237
+ return;
238
+ }
239
+
240
+ const tx: FailedL1Tx = {
241
+ ...failedTx,
242
+ timestamp: Date.now(),
243
+ };
244
+
245
+ // Fire and forget - don't block on backup
246
+ void this.failedTxStore
247
+ .then(store => store?.saveFailedTx(tx))
248
+ .catch(err => {
249
+ this.log.warn(`Failed to backup failed L1 tx to store`, err);
250
+ });
191
251
  }
192
252
 
193
253
  public getRollupContract(): RollupContract {
194
254
  return this.rollupContract;
195
255
  }
196
256
 
257
+ /**
258
+ * Gets the fee asset price modifier from the oracle.
259
+ * Returns 0n if the oracle query fails.
260
+ */
261
+ public getFeeAssetPriceModifier(): Promise<bigint> {
262
+ return this.feeAssetPriceOracle.computePriceModifier();
263
+ }
264
+
197
265
  public getSenderAddress() {
198
266
  return this.l1TxUtils.getSenderAddress();
199
267
  }
@@ -361,19 +429,36 @@ export class SequencerPublisher {
361
429
  validRequests.sort((a, b) => compareActions(a.action, b.action));
362
430
 
363
431
  try {
432
+ // Capture context for failed tx backup before sending
433
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
434
+ const multicallData = encodeFunctionData({
435
+ abi: multicall3Abi,
436
+ functionName: 'aggregate3',
437
+ args: [
438
+ validRequests.map(r => ({
439
+ target: r.request.to!,
440
+ callData: r.request.data!,
441
+ allowFailure: true,
442
+ })),
443
+ ],
444
+ });
445
+ const blobDataHex = blobConfig?.blobs?.map(b => toHex(b)) as Hex[] | undefined;
446
+
447
+ const txContext = { multicallData, blobData: blobDataHex, l1BlockNumber };
448
+
364
449
  this.log.debug('Forwarding transactions', {
365
450
  validRequests: validRequests.map(request => request.action),
366
451
  txConfig,
367
452
  });
368
- const result = await Multicall3.forward(
369
- validRequests.map(request => request.request),
370
- this.l1TxUtils,
371
- txConfig,
372
- blobConfig,
373
- this.rollupContract.address,
374
- this.log,
453
+ const result = await this.forwardWithPublisherRotation(validRequests, txConfig, blobConfig);
454
+ if (result === undefined) {
455
+ return undefined;
456
+ }
457
+ const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(
458
+ validRequests,
459
+ result,
460
+ txContext,
375
461
  );
376
- const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(validRequests, result);
377
462
  return { result, expiredActions, sentActions: validActions, successfulActions, failedActions };
378
463
  } catch (err) {
379
464
  const viemError = formatViemError(err);
@@ -391,16 +476,88 @@ export class SequencerPublisher {
391
476
  }
392
477
  }
393
478
 
479
+ /**
480
+ * Forwards transactions via Multicall3, rotating to the next available publisher if a send
481
+ * failure occurs (i.e. the tx never reached the chain).
482
+ * On-chain reverts and simulation errors are returned as-is without rotation.
483
+ */
484
+ private async forwardWithPublisherRotation(
485
+ validRequests: RequestWithExpiry[],
486
+ txConfig: RequestWithExpiry['gasConfig'],
487
+ blobConfig: L1BlobInputs | undefined,
488
+ ) {
489
+ const triedAddresses: EthAddress[] = [];
490
+ let currentPublisher = this.l1TxUtils;
491
+
492
+ while (true) {
493
+ triedAddresses.push(currentPublisher.getSenderAddress());
494
+ try {
495
+ const result = await Multicall3.forward(
496
+ validRequests.map(r => r.request),
497
+ currentPublisher,
498
+ txConfig,
499
+ blobConfig,
500
+ this.rollupContract.address,
501
+ this.log,
502
+ );
503
+ this.l1TxUtils = currentPublisher;
504
+ return result;
505
+ } catch (err) {
506
+ if (err instanceof TimeoutError) {
507
+ throw err;
508
+ }
509
+ const viemError = formatViemError(err);
510
+ if (!this.getNextPublisher) {
511
+ this.log.error('Failed to publish bundled transactions', viemError);
512
+ return undefined;
513
+ }
514
+ this.log.warn(
515
+ `Publisher ${currentPublisher.getSenderAddress()} failed to send, rotating to next publisher`,
516
+ viemError,
517
+ );
518
+ const nextPublisher = await this.getNextPublisher([...triedAddresses]);
519
+ if (!nextPublisher) {
520
+ this.log.error('All available publishers exhausted, failed to publish bundled transactions');
521
+ return undefined;
522
+ }
523
+ currentPublisher = nextPublisher;
524
+ }
525
+ }
526
+ }
527
+
394
528
  private callbackBundledTransactions(
395
529
  requests: RequestWithExpiry[],
396
- result?: { receipt: TransactionReceipt } | FormattedViemError,
530
+ result: { receipt: TransactionReceipt; errorMsg?: string } | FormattedViemError | undefined,
531
+ txContext: { multicallData: Hex; blobData?: Hex[]; l1BlockNumber: bigint },
397
532
  ) {
398
533
  const actionsListStr = requests.map(r => r.action).join(', ');
399
534
  if (result instanceof FormattedViemError) {
400
535
  this.log.error(`Failed to publish bundled transactions (${actionsListStr})`, result);
536
+ this.backupFailedTx({
537
+ id: keccak256(txContext.multicallData),
538
+ failureType: 'send-error',
539
+ request: { to: MULTI_CALL_3_ADDRESS, data: txContext.multicallData },
540
+ blobData: txContext.blobData,
541
+ l1BlockNumber: txContext.l1BlockNumber.toString(),
542
+ error: { message: result.message, name: result.name },
543
+ context: {
544
+ actions: requests.map(r => r.action),
545
+ requests: requests.map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
546
+ sender: this.getSenderAddress().toString(),
547
+ },
548
+ });
401
549
  return { failedActions: requests.map(r => r.action) };
402
550
  } else {
403
- this.log.verbose(`Published bundled transactions (${actionsListStr})`, { result, requests });
551
+ this.log.verbose(`Published bundled transactions (${actionsListStr})`, {
552
+ result,
553
+ requests: requests.map(r => ({
554
+ ...r,
555
+ // Avoid logging large blob data
556
+ blobConfig: r.blobConfig
557
+ ? { ...r.blobConfig, blobs: r.blobConfig.blobs.map(b => ({ size: trimmedBytesLength(b) })) }
558
+ : undefined,
559
+ })),
560
+ });
404
561
  const successfulActions: Action[] = [];
405
562
  const failedActions: Action[] = [];
406
563
  for (const request of requests) {
@@ -410,6 +567,30 @@ export class SequencerPublisher {
410
567
  failedActions.push(request.action);
411
568
  }
412
569
  }
570
+ // Single backup for the whole reverted tx
571
+ if (failedActions.length > 0 && result?.receipt?.status === 'reverted') {
572
+ this.backupFailedTx({
573
+ id: result.receipt.transactionHash,
574
+ failureType: 'revert',
575
+ request: { to: MULTI_CALL_3_ADDRESS, data: txContext.multicallData },
576
+ blobData: txContext.blobData,
577
+ l1BlockNumber: result.receipt.blockNumber.toString(),
578
+ receipt: {
579
+ transactionHash: result.receipt.transactionHash,
580
+ blockNumber: result.receipt.blockNumber.toString(),
581
+ gasUsed: (result.receipt.gasUsed ?? 0n).toString(),
582
+ status: 'reverted',
583
+ },
584
+ error: { message: result.errorMsg ?? 'Transaction reverted' },
585
+ context: {
586
+ actions: failedActions,
587
+ requests: requests
588
+ .filter(r => failedActions.includes(r.action))
589
+ .map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
590
+ sender: this.getSenderAddress().toString(),
591
+ },
592
+ });
593
+ }
413
594
  return { successfulActions, failedActions };
414
595
  }
415
596
  }
@@ -521,6 +702,8 @@ export class SequencerPublisher {
521
702
  const request = this.buildInvalidateCheckpointRequest(validationResult);
522
703
  this.log.debug(`Simulating invalidate checkpoint ${checkpointNumber}`, { ...logData, request });
523
704
 
705
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
706
+
524
707
  try {
525
708
  const { gasUsed } = await this.l1TxUtils.simulate(
526
709
  request,
@@ -572,6 +755,18 @@ export class SequencerPublisher {
572
755
 
573
756
  // Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
574
757
  this.log.error(`Simulation for invalidate checkpoint ${checkpointNumber} failed`, viemError, logData);
758
+ this.backupFailedTx({
759
+ id: keccak256(request.data!),
760
+ failureType: 'simulation',
761
+ request: { to: request.to!, data: request.data!, value: request.value?.toString() },
762
+ l1BlockNumber: l1BlockNumber.toString(),
763
+ error: { message: viemError.message, name: viemError.name },
764
+ context: {
765
+ actions: [`invalidate-${reason}`],
766
+ checkpointNumber,
767
+ sender: this.getSenderAddress().toString(),
768
+ },
769
+ });
575
770
  throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, { cause: viemError });
576
771
  }
577
772
  }
@@ -617,24 +812,8 @@ export class SequencerPublisher {
617
812
  options: { forcePendingCheckpointNumber?: CheckpointNumber },
618
813
  ): Promise<bigint> {
619
814
  const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
620
-
621
- // TODO(palla/mbps): This should not be needed, there's no flow where we propose with zero attestations. Or is there?
622
- // If we have no attestations, we still need to provide the empty attestations
623
- // so that the committee is recalculated correctly
624
- // const ignoreSignatures = attestationsAndSigners.attestations.length === 0;
625
- // if (ignoreSignatures) {
626
- // const { committee } = await this.epochCache.getCommittee(block.header.globalVariables.slotNumber);
627
- // if (!committee) {
628
- // this.log.warn(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
629
- // throw new Error(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
630
- // }
631
- // attestationsAndSigners.attestations = committee.map(committeeMember =>
632
- // CommitteeAttestation.fromAddress(committeeMember),
633
- // );
634
- // }
635
-
636
815
  const blobFields = checkpoint.toBlobFields();
637
- const blobs = getBlobsPerL1Block(blobFields);
816
+ const blobs = await getBlobsPerL1Block(blobFields);
638
817
  const blobInput = getPrefixedEthBlobCommitments(blobs);
639
818
 
640
819
  const args = [
@@ -642,7 +821,7 @@ export class SequencerPublisher {
642
821
  header: checkpoint.header.toViem(),
643
822
  archive: toHex(checkpoint.archive.root.toBuffer()),
644
823
  oracleInput: {
645
- feeAssetPriceModifier: 0n,
824
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
646
825
  },
647
826
  },
648
827
  attestationsAndSigners.getPackedAttestations(),
@@ -691,6 +870,32 @@ export class SequencerPublisher {
691
870
  return false;
692
871
  }
693
872
 
873
+ // Check if payload was already submitted to governance
874
+ const cacheKey = payload.toString();
875
+ if (!this.payloadProposedCache.has(cacheKey)) {
876
+ try {
877
+ const l1StartBlock = await this.rollupContract.getL1StartBlock();
878
+ const proposed = await retry(
879
+ () => base.hasPayloadBeenProposed(payload.toString(), l1StartBlock),
880
+ 'Check if payload was proposed',
881
+ makeBackoff([0, 1, 2]),
882
+ this.log,
883
+ true,
884
+ );
885
+ if (proposed) {
886
+ this.payloadProposedCache.add(cacheKey);
887
+ }
888
+ } catch (err) {
889
+ this.log.warn(`Failed to check if payload ${payload} was proposed after retries, skipping signal`, err);
890
+ return false;
891
+ }
892
+ }
893
+
894
+ if (this.payloadProposedCache.has(cacheKey)) {
895
+ this.log.info(`Payload ${payload} was already proposed to governance, stopping signals`);
896
+ return false;
897
+ }
898
+
694
899
  const cachedLastVote = this.lastActions[signalType];
695
900
  this.lastActions[signalType] = slotNumber;
696
901
  const action = signalType;
@@ -709,11 +914,26 @@ export class SequencerPublisher {
709
914
  lastValidL2Slot: slotNumber,
710
915
  });
711
916
 
917
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
918
+
712
919
  try {
713
920
  await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
714
921
  this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
715
922
  } catch (err) {
716
- this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, err);
923
+ const viemError = formatViemError(err);
924
+ this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, viemError);
925
+ this.backupFailedTx({
926
+ id: keccak256(request.data!),
927
+ failureType: 'simulation',
928
+ request: { to: request.to!, data: request.data!, value: request.value?.toString() },
929
+ l1BlockNumber: l1BlockNumber.toString(),
930
+ error: { message: viemError.message, name: viemError.name },
931
+ context: {
932
+ actions: [action],
933
+ slot: slotNumber,
934
+ sender: this.getSenderAddress().toString(),
935
+ },
936
+ });
717
937
  // Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
718
938
  }
719
939
 
@@ -918,14 +1138,15 @@ export class SequencerPublisher {
918
1138
  const checkpointHeader = checkpoint.header;
919
1139
 
920
1140
  const blobFields = checkpoint.toBlobFields();
921
- const blobs = getBlobsPerL1Block(blobFields);
1141
+ const blobs = await getBlobsPerL1Block(blobFields);
922
1142
 
923
- const proposeTxArgs = {
1143
+ const proposeTxArgs: L1ProcessArgs = {
924
1144
  header: checkpointHeader,
925
1145
  archive: checkpoint.archive.root.toBuffer(),
926
1146
  blobs,
927
1147
  attestationsAndSigners,
928
1148
  attestationsAndSignersSignature,
1149
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
929
1150
  };
930
1151
 
931
1152
  let ts: bigint;
@@ -1008,6 +1229,8 @@ export class SequencerPublisher {
1008
1229
 
1009
1230
  this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
1010
1231
 
1232
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1233
+
1011
1234
  let gasUsed: bigint;
1012
1235
  const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
1013
1236
  try {
@@ -1017,6 +1240,19 @@ export class SequencerPublisher {
1017
1240
  const viemError = formatViemError(err, simulateAbi);
1018
1241
  this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
1019
1242
 
1243
+ this.backupFailedTx({
1244
+ id: keccak256(request.data!),
1245
+ failureType: 'simulation',
1246
+ request: { to: request.to!, data: request.data!, value: request.value?.toString() },
1247
+ l1BlockNumber: l1BlockNumber.toString(),
1248
+ error: { message: viemError.message, name: viemError.name },
1249
+ context: {
1250
+ actions: [action],
1251
+ slot: slotNumber,
1252
+ sender: this.getSenderAddress().toString(),
1253
+ },
1254
+ });
1255
+
1020
1256
  return false;
1021
1257
  }
1022
1258
 
@@ -1100,9 +1336,27 @@ export class SequencerPublisher {
1100
1336
  kzg,
1101
1337
  },
1102
1338
  )
1103
- .catch(err => {
1104
- const { message, metaMessages } = formatViemError(err);
1105
- this.log.error(`Failed to validate blobs`, message, { metaMessages });
1339
+ .catch(async err => {
1340
+ const viemError = formatViemError(err);
1341
+ this.log.error(`Failed to validate blobs`, viemError.message, { metaMessages: viemError.metaMessages });
1342
+ const validateBlobsData = encodeFunctionData({
1343
+ abi: RollupAbi,
1344
+ functionName: 'validateBlobs',
1345
+ args: [blobInput],
1346
+ });
1347
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1348
+ this.backupFailedTx({
1349
+ id: keccak256(validateBlobsData),
1350
+ failureType: 'simulation',
1351
+ request: { to: this.rollupContract.address as Hex, data: validateBlobsData },
1352
+ blobData: encodedData.blobs.map(b => toHex(b.data)) as Hex[],
1353
+ l1BlockNumber: l1BlockNumber.toString(),
1354
+ error: { message: viemError.message, name: viemError.name },
1355
+ context: {
1356
+ actions: ['validate-blobs'],
1357
+ sender: this.getSenderAddress().toString(),
1358
+ },
1359
+ });
1106
1360
  throw new Error('Failed to validate blobs');
1107
1361
  });
1108
1362
  }
@@ -1113,8 +1367,7 @@ export class SequencerPublisher {
1113
1367
  header: encodedData.header.toViem(),
1114
1368
  archive: toHex(encodedData.archive),
1115
1369
  oracleInput: {
1116
- // We are currently not modifying these. See #9963
1117
- feeAssetPriceModifier: 0n,
1370
+ feeAssetPriceModifier: encodedData.feeAssetPriceModifier,
1118
1371
  },
1119
1372
  },
1120
1373
  encodedData.attestationsAndSigners.getPackedAttestations(),
@@ -1140,7 +1393,7 @@ export class SequencerPublisher {
1140
1393
  readonly header: ViemHeader;
1141
1394
  readonly archive: `0x${string}`;
1142
1395
  readonly oracleInput: {
1143
- readonly feeAssetPriceModifier: 0n;
1396
+ readonly feeAssetPriceModifier: bigint;
1144
1397
  };
1145
1398
  },
1146
1399
  ViemCommitteeAttestations,
@@ -1182,6 +1435,8 @@ export class SequencerPublisher {
1182
1435
  });
1183
1436
  }
1184
1437
 
1438
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1439
+
1185
1440
  const simulationResult = await this.l1TxUtils
1186
1441
  .simulate(
1187
1442
  {
@@ -1215,6 +1470,18 @@ export class SequencerPublisher {
1215
1470
  };
1216
1471
  }
1217
1472
  this.log.error(`Failed to simulate propose tx`, viemError);
1473
+ this.backupFailedTx({
1474
+ id: keccak256(rollupData),
1475
+ failureType: 'simulation',
1476
+ request: { to: this.rollupContract.address, data: rollupData },
1477
+ l1BlockNumber: l1BlockNumber.toString(),
1478
+ error: { message: viemError.message, name: viemError.name },
1479
+ context: {
1480
+ actions: ['propose'],
1481
+ slot: Number(args[0].header.slotNumber),
1482
+ sender: this.getSenderAddress().toString(),
1483
+ },
1484
+ });
1218
1485
  throw err;
1219
1486
  });
1220
1487