@aztec/sequencer-client 0.0.1-commit.4d79d1f2d → 0.0.1-commit.5358163d3

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 (68) 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 +15 -4
  4. package/dest/config.d.ts +3 -3
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +26 -12
  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/publisher/config.d.ts +35 -17
  10. package/dest/publisher/config.d.ts.map +1 -1
  11. package/dest/publisher/config.js +106 -42
  12. package/dest/publisher/index.d.ts +2 -1
  13. package/dest/publisher/index.d.ts.map +1 -1
  14. package/dest/publisher/l1_tx_failed_store/factory.d.ts +11 -0
  15. package/dest/publisher/l1_tx_failed_store/factory.d.ts.map +1 -0
  16. package/dest/publisher/l1_tx_failed_store/factory.js +22 -0
  17. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts +59 -0
  18. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -0
  19. package/dest/publisher/l1_tx_failed_store/failed_tx_store.js +1 -0
  20. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts +15 -0
  21. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts.map +1 -0
  22. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.js +34 -0
  23. package/dest/publisher/l1_tx_failed_store/index.d.ts +4 -0
  24. package/dest/publisher/l1_tx_failed_store/index.d.ts.map +1 -0
  25. package/dest/publisher/l1_tx_failed_store/index.js +2 -0
  26. package/dest/publisher/sequencer-publisher-factory.d.ts +11 -3
  27. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  28. package/dest/publisher/sequencer-publisher-factory.js +27 -2
  29. package/dest/publisher/sequencer-publisher.d.ts +26 -7
  30. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  31. package/dest/publisher/sequencer-publisher.js +299 -30
  32. package/dest/sequencer/checkpoint_proposal_job.d.ts +1 -1
  33. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  34. package/dest/sequencer/checkpoint_proposal_job.js +71 -24
  35. package/dest/sequencer/metrics.d.ts +17 -5
  36. package/dest/sequencer/metrics.d.ts.map +1 -1
  37. package/dest/sequencer/metrics.js +86 -15
  38. package/dest/sequencer/sequencer.d.ts +18 -8
  39. package/dest/sequencer/sequencer.d.ts.map +1 -1
  40. package/dest/sequencer/sequencer.js +24 -26
  41. package/dest/sequencer/timetable.js +1 -1
  42. package/dest/test/index.d.ts +3 -5
  43. package/dest/test/index.d.ts.map +1 -1
  44. package/dest/test/mock_checkpoint_builder.d.ts +5 -3
  45. package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
  46. package/dest/test/mock_checkpoint_builder.js +6 -4
  47. package/dest/test/utils.d.ts +3 -3
  48. package/dest/test/utils.d.ts.map +1 -1
  49. package/dest/test/utils.js +5 -4
  50. package/package.json +28 -28
  51. package/src/client/sequencer-client.ts +25 -7
  52. package/src/config.ts +31 -16
  53. package/src/global_variable_builder/global_builder.ts +1 -1
  54. package/src/publisher/config.ts +121 -43
  55. package/src/publisher/index.ts +3 -0
  56. package/src/publisher/l1_tx_failed_store/factory.ts +32 -0
  57. package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +55 -0
  58. package/src/publisher/l1_tx_failed_store/file_store_failed_tx_store.ts +46 -0
  59. package/src/publisher/l1_tx_failed_store/index.ts +3 -0
  60. package/src/publisher/sequencer-publisher-factory.ts +38 -6
  61. package/src/publisher/sequencer-publisher.ts +300 -43
  62. package/src/sequencer/checkpoint_proposal_job.ts +103 -19
  63. package/src/sequencer/metrics.ts +92 -18
  64. package/src/sequencer/sequencer.ts +32 -31
  65. package/src/sequencer/timetable.ts +1 -1
  66. package/src/test/index.ts +2 -4
  67. package/src/test/mock_checkpoint_builder.ts +12 -1
  68. 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,22 @@ 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';
30
31
  import { pick } from '@aztec/foundation/collection';
31
32
  import type { Fr } from '@aztec/foundation/curves/bn254';
33
+ import { TimeoutError } from '@aztec/foundation/error';
32
34
  import { EthAddress } from '@aztec/foundation/eth-address';
33
35
  import { Signature, type ViemSignature } from '@aztec/foundation/eth-signature';
34
36
  import { type Logger, createLogger } from '@aztec/foundation/log';
37
+ import { makeBackoff, retry } from '@aztec/foundation/retry';
35
38
  import { bufferToHex } from '@aztec/foundation/string';
36
39
  import { DateProvider, Timer } from '@aztec/foundation/timer';
37
40
  import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
@@ -43,9 +46,19 @@ import type { CheckpointHeader } from '@aztec/stdlib/rollup';
43
46
  import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
44
47
  import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
45
48
 
46
- import { type StateOverride, type TransactionReceipt, type TypedDataDefinition, encodeFunctionData, toHex } from 'viem';
47
-
48
- import type { PublisherConfig, TxSenderConfig } from './config.js';
49
+ import {
50
+ type Hex,
51
+ type StateOverride,
52
+ type TransactionReceipt,
53
+ type TypedDataDefinition,
54
+ encodeFunctionData,
55
+ keccak256,
56
+ multicall3Abi,
57
+ toHex,
58
+ } from 'viem';
59
+
60
+ import type { SequencerPublisherConfig } from './config.js';
61
+ import { type FailedL1Tx, type L1TxFailedStore, createL1TxFailedStore } from './l1_tx_failed_store/index.js';
49
62
  import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
50
63
 
51
64
  /** Arguments to the process method of the rollup contract */
@@ -60,6 +73,8 @@ type L1ProcessArgs = {
60
73
  attestationsAndSigners: CommitteeAttestationsAndSigners;
61
74
  /** Attestations and signers signature */
62
75
  attestationsAndSignersSignature: Signature;
76
+ /** The fee asset price modifier in basis points (from oracle) */
77
+ feeAssetPriceModifier: bigint;
63
78
  };
64
79
 
65
80
  export const Actions = [
@@ -105,6 +120,7 @@ export class SequencerPublisher {
105
120
  private interrupted = false;
106
121
  private metrics: SequencerPublisherMetrics;
107
122
  public epochCache: EpochCache;
123
+ private failedTxStore?: Promise<L1TxFailedStore | undefined>;
108
124
 
109
125
  protected governanceLog = createLogger('sequencer:publisher:governance');
110
126
  protected slashingLog = createLogger('sequencer:publisher:slashing');
@@ -112,6 +128,7 @@ export class SequencerPublisher {
112
128
  protected lastActions: Partial<Record<Action, SlotNumber>> = {};
113
129
 
114
130
  private isPayloadEmptyCache: Map<string, boolean> = new Map<string, boolean>();
131
+ private payloadProposedCache: Set<string> = new Set<string>();
115
132
 
116
133
  protected log: Logger;
117
134
  protected ethereumSlotDuration: bigint;
@@ -121,15 +138,22 @@ export class SequencerPublisher {
121
138
  /** Address to use for simulations in fisherman mode (actual proposer's address) */
122
139
  private proposerAddressForSimulation?: EthAddress;
123
140
 
141
+ /** Optional callback to obtain a replacement publisher when the current one fails to send. */
142
+ private getNextPublisher?: (excludeAddresses: EthAddress[]) => Promise<L1TxUtils | undefined>;
143
+
124
144
  /** L1 fee analyzer for fisherman mode */
125
145
  private l1FeeAnalyzer?: L1FeeAnalyzer;
146
+
147
+ /** Fee asset price oracle for computing price modifiers from Uniswap V4 */
148
+ private feeAssetPriceOracle: FeeAssetPriceOracle;
149
+
126
150
  // A CALL to a cold address is 2700 gas
127
151
  public static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
128
152
 
129
153
  // Gas report for VotingWithSigTest shows a max gas of 100k, but we've seen it cost 700k+ in testnet
130
154
  public static VOTE_GAS_GUESS: bigint = 800_000n;
131
155
 
132
- public l1TxUtils: L1TxUtilsWithBlobs;
156
+ public l1TxUtils: L1TxUtils;
133
157
  public rollupContract: RollupContract;
134
158
  public govProposerContract: GovernanceProposerContract;
135
159
  public slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
@@ -140,11 +164,12 @@ export class SequencerPublisher {
140
164
  protected requests: RequestWithExpiry[] = [];
141
165
 
142
166
  constructor(
143
- private config: TxSenderConfig & PublisherConfig & Pick<L1ContractsConfig, 'ethereumSlotDuration'>,
167
+ private config: Pick<SequencerPublisherConfig, 'fishermanMode' | 'l1TxFailedStore'> &
168
+ Pick<L1ContractsConfig, 'ethereumSlotDuration'> & { l1ChainId: number },
144
169
  deps: {
145
170
  telemetry?: TelemetryClient;
146
171
  blobClient: BlobClientInterface;
147
- l1TxUtils: L1TxUtilsWithBlobs;
172
+ l1TxUtils: L1TxUtils;
148
173
  rollupContract: RollupContract;
149
174
  slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
150
175
  governanceProposerContract: GovernanceProposerContract;
@@ -154,6 +179,7 @@ export class SequencerPublisher {
154
179
  metrics: SequencerPublisherMetrics;
155
180
  lastActions: Partial<Record<Action, SlotNumber>>;
156
181
  log?: Logger;
182
+ getNextPublisher?: (excludeAddresses: EthAddress[]) => Promise<L1TxUtils | undefined>;
157
183
  },
158
184
  ) {
159
185
  this.log = deps.log ?? createLogger('sequencer:publisher');
@@ -167,6 +193,7 @@ export class SequencerPublisher {
167
193
  this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
168
194
  this.tracer = telemetry.getTracer('SequencerPublisher');
169
195
  this.l1TxUtils = deps.l1TxUtils;
196
+ this.getNextPublisher = deps.getNextPublisher;
170
197
 
171
198
  this.rollupContract = deps.rollupContract;
172
199
 
@@ -188,12 +215,52 @@ export class SequencerPublisher {
188
215
  createLogger('sequencer:publisher:fee-analyzer'),
189
216
  );
190
217
  }
218
+
219
+ // Initialize fee asset price oracle
220
+ this.feeAssetPriceOracle = new FeeAssetPriceOracle(
221
+ this.l1TxUtils.client,
222
+ this.rollupContract,
223
+ createLogger('sequencer:publisher:price-oracle'),
224
+ );
225
+
226
+ // Initialize failed L1 tx store (optional, for test networks)
227
+ this.failedTxStore = createL1TxFailedStore(config.l1TxFailedStore, this.log);
228
+ }
229
+
230
+ /**
231
+ * Backs up a failed L1 transaction to the configured store for debugging.
232
+ * Does nothing if no store is configured.
233
+ */
234
+ private backupFailedTx(failedTx: Omit<FailedL1Tx, 'timestamp'>): void {
235
+ if (!this.failedTxStore) {
236
+ return;
237
+ }
238
+
239
+ const tx: FailedL1Tx = {
240
+ ...failedTx,
241
+ timestamp: Date.now(),
242
+ };
243
+
244
+ // Fire and forget - don't block on backup
245
+ void this.failedTxStore
246
+ .then(store => store?.saveFailedTx(tx))
247
+ .catch(err => {
248
+ this.log.warn(`Failed to backup failed L1 tx to store`, err);
249
+ });
191
250
  }
192
251
 
193
252
  public getRollupContract(): RollupContract {
194
253
  return this.rollupContract;
195
254
  }
196
255
 
256
+ /**
257
+ * Gets the fee asset price modifier from the oracle.
258
+ * Returns 0n if the oracle query fails.
259
+ */
260
+ public getFeeAssetPriceModifier(): Promise<bigint> {
261
+ return this.feeAssetPriceOracle.computePriceModifier();
262
+ }
263
+
197
264
  public getSenderAddress() {
198
265
  return this.l1TxUtils.getSenderAddress();
199
266
  }
@@ -361,19 +428,36 @@ export class SequencerPublisher {
361
428
  validRequests.sort((a, b) => compareActions(a.action, b.action));
362
429
 
363
430
  try {
431
+ // Capture context for failed tx backup before sending
432
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
433
+ const multicallData = encodeFunctionData({
434
+ abi: multicall3Abi,
435
+ functionName: 'aggregate3',
436
+ args: [
437
+ validRequests.map(r => ({
438
+ target: r.request.to!,
439
+ callData: r.request.data!,
440
+ allowFailure: true,
441
+ })),
442
+ ],
443
+ });
444
+ const blobDataHex = blobConfig?.blobs?.map(b => toHex(b)) as Hex[] | undefined;
445
+
446
+ const txContext = { multicallData, blobData: blobDataHex, l1BlockNumber };
447
+
364
448
  this.log.debug('Forwarding transactions', {
365
449
  validRequests: validRequests.map(request => request.action),
366
450
  txConfig,
367
451
  });
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,
452
+ const result = await this.forwardWithPublisherRotation(validRequests, txConfig, blobConfig);
453
+ if (result === undefined) {
454
+ return undefined;
455
+ }
456
+ const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(
457
+ validRequests,
458
+ result,
459
+ txContext,
375
460
  );
376
- const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(validRequests, result);
377
461
  return { result, expiredActions, sentActions: validActions, successfulActions, failedActions };
378
462
  } catch (err) {
379
463
  const viemError = formatViemError(err);
@@ -391,13 +475,76 @@ export class SequencerPublisher {
391
475
  }
392
476
  }
393
477
 
478
+ /**
479
+ * Forwards transactions via Multicall3, rotating to the next available publisher if a send
480
+ * failure occurs (i.e. the tx never reached the chain).
481
+ * On-chain reverts and simulation errors are returned as-is without rotation.
482
+ */
483
+ private async forwardWithPublisherRotation(
484
+ validRequests: RequestWithExpiry[],
485
+ txConfig: RequestWithExpiry['gasConfig'],
486
+ blobConfig: L1BlobInputs | undefined,
487
+ ) {
488
+ const triedAddresses: EthAddress[] = [];
489
+ let currentPublisher = this.l1TxUtils;
490
+
491
+ while (true) {
492
+ triedAddresses.push(currentPublisher.getSenderAddress());
493
+ try {
494
+ const result = await Multicall3.forward(
495
+ validRequests.map(r => r.request),
496
+ currentPublisher,
497
+ txConfig,
498
+ blobConfig,
499
+ this.rollupContract.address,
500
+ this.log,
501
+ );
502
+ this.l1TxUtils = currentPublisher;
503
+ return result;
504
+ } catch (err) {
505
+ if (err instanceof TimeoutError) {
506
+ throw err;
507
+ }
508
+ const viemError = formatViemError(err);
509
+ if (!this.getNextPublisher) {
510
+ this.log.error('Failed to publish bundled transactions', viemError);
511
+ return undefined;
512
+ }
513
+ this.log.warn(
514
+ `Publisher ${currentPublisher.getSenderAddress()} failed to send, rotating to next publisher`,
515
+ viemError,
516
+ );
517
+ const nextPublisher = await this.getNextPublisher([...triedAddresses]);
518
+ if (!nextPublisher) {
519
+ this.log.error('All available publishers exhausted, failed to publish bundled transactions');
520
+ return undefined;
521
+ }
522
+ currentPublisher = nextPublisher;
523
+ }
524
+ }
525
+ }
526
+
394
527
  private callbackBundledTransactions(
395
528
  requests: RequestWithExpiry[],
396
- result?: { receipt: TransactionReceipt } | FormattedViemError,
529
+ result: { receipt: TransactionReceipt; errorMsg?: string } | FormattedViemError | undefined,
530
+ txContext: { multicallData: Hex; blobData?: Hex[]; l1BlockNumber: bigint },
397
531
  ) {
398
532
  const actionsListStr = requests.map(r => r.action).join(', ');
399
533
  if (result instanceof FormattedViemError) {
400
534
  this.log.error(`Failed to publish bundled transactions (${actionsListStr})`, result);
535
+ this.backupFailedTx({
536
+ id: keccak256(txContext.multicallData),
537
+ failureType: 'send-error',
538
+ request: { to: MULTI_CALL_3_ADDRESS, data: txContext.multicallData },
539
+ blobData: txContext.blobData,
540
+ l1BlockNumber: txContext.l1BlockNumber.toString(),
541
+ error: { message: result.message, name: result.name },
542
+ context: {
543
+ actions: requests.map(r => r.action),
544
+ requests: requests.map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
545
+ sender: this.getSenderAddress().toString(),
546
+ },
547
+ });
401
548
  return { failedActions: requests.map(r => r.action) };
402
549
  } else {
403
550
  this.log.verbose(`Published bundled transactions (${actionsListStr})`, { result, requests });
@@ -410,6 +557,30 @@ export class SequencerPublisher {
410
557
  failedActions.push(request.action);
411
558
  }
412
559
  }
560
+ // Single backup for the whole reverted tx
561
+ if (failedActions.length > 0 && result?.receipt?.status === 'reverted') {
562
+ this.backupFailedTx({
563
+ id: result.receipt.transactionHash,
564
+ failureType: 'revert',
565
+ request: { to: MULTI_CALL_3_ADDRESS, data: txContext.multicallData },
566
+ blobData: txContext.blobData,
567
+ l1BlockNumber: result.receipt.blockNumber.toString(),
568
+ receipt: {
569
+ transactionHash: result.receipt.transactionHash,
570
+ blockNumber: result.receipt.blockNumber.toString(),
571
+ gasUsed: (result.receipt.gasUsed ?? 0n).toString(),
572
+ status: 'reverted',
573
+ },
574
+ error: { message: result.errorMsg ?? 'Transaction reverted' },
575
+ context: {
576
+ actions: failedActions,
577
+ requests: requests
578
+ .filter(r => failedActions.includes(r.action))
579
+ .map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
580
+ sender: this.getSenderAddress().toString(),
581
+ },
582
+ });
583
+ }
413
584
  return { successfulActions, failedActions };
414
585
  }
415
586
  }
@@ -521,6 +692,8 @@ export class SequencerPublisher {
521
692
  const request = this.buildInvalidateCheckpointRequest(validationResult);
522
693
  this.log.debug(`Simulating invalidate checkpoint ${checkpointNumber}`, { ...logData, request });
523
694
 
695
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
696
+
524
697
  try {
525
698
  const { gasUsed } = await this.l1TxUtils.simulate(
526
699
  request,
@@ -572,6 +745,18 @@ export class SequencerPublisher {
572
745
 
573
746
  // Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
574
747
  this.log.error(`Simulation for invalidate checkpoint ${checkpointNumber} failed`, viemError, logData);
748
+ this.backupFailedTx({
749
+ id: keccak256(request.data!),
750
+ failureType: 'simulation',
751
+ request: { to: request.to!, data: request.data!, value: request.value?.toString() },
752
+ l1BlockNumber: l1BlockNumber.toString(),
753
+ error: { message: viemError.message, name: viemError.name },
754
+ context: {
755
+ actions: [`invalidate-${reason}`],
756
+ checkpointNumber,
757
+ sender: this.getSenderAddress().toString(),
758
+ },
759
+ });
575
760
  throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, { cause: viemError });
576
761
  }
577
762
  }
@@ -617,24 +802,8 @@ export class SequencerPublisher {
617
802
  options: { forcePendingCheckpointNumber?: CheckpointNumber },
618
803
  ): Promise<bigint> {
619
804
  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
805
  const blobFields = checkpoint.toBlobFields();
637
- const blobs = getBlobsPerL1Block(blobFields);
806
+ const blobs = await getBlobsPerL1Block(blobFields);
638
807
  const blobInput = getPrefixedEthBlobCommitments(blobs);
639
808
 
640
809
  const args = [
@@ -642,7 +811,7 @@ export class SequencerPublisher {
642
811
  header: checkpoint.header.toViem(),
643
812
  archive: toHex(checkpoint.archive.root.toBuffer()),
644
813
  oracleInput: {
645
- feeAssetPriceModifier: 0n,
814
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
646
815
  },
647
816
  },
648
817
  attestationsAndSigners.getPackedAttestations(),
@@ -691,6 +860,32 @@ export class SequencerPublisher {
691
860
  return false;
692
861
  }
693
862
 
863
+ // Check if payload was already submitted to governance
864
+ const cacheKey = payload.toString();
865
+ if (!this.payloadProposedCache.has(cacheKey)) {
866
+ try {
867
+ const l1StartBlock = await this.rollupContract.getL1StartBlock();
868
+ const proposed = await retry(
869
+ () => base.hasPayloadBeenProposed(payload.toString(), l1StartBlock),
870
+ 'Check if payload was proposed',
871
+ makeBackoff([0, 1, 2]),
872
+ this.log,
873
+ true,
874
+ );
875
+ if (proposed) {
876
+ this.payloadProposedCache.add(cacheKey);
877
+ }
878
+ } catch (err) {
879
+ this.log.warn(`Failed to check if payload ${payload} was proposed after retries, skipping signal`, err);
880
+ return false;
881
+ }
882
+ }
883
+
884
+ if (this.payloadProposedCache.has(cacheKey)) {
885
+ this.log.info(`Payload ${payload} was already proposed to governance, stopping signals`);
886
+ return false;
887
+ }
888
+
694
889
  const cachedLastVote = this.lastActions[signalType];
695
890
  this.lastActions[signalType] = slotNumber;
696
891
  const action = signalType;
@@ -709,11 +904,26 @@ export class SequencerPublisher {
709
904
  lastValidL2Slot: slotNumber,
710
905
  });
711
906
 
907
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
908
+
712
909
  try {
713
910
  await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
714
911
  this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
715
912
  } catch (err) {
716
- this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, err);
913
+ const viemError = formatViemError(err);
914
+ this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, viemError);
915
+ this.backupFailedTx({
916
+ id: keccak256(request.data!),
917
+ failureType: 'simulation',
918
+ request: { to: request.to!, data: request.data!, value: request.value?.toString() },
919
+ l1BlockNumber: l1BlockNumber.toString(),
920
+ error: { message: viemError.message, name: viemError.name },
921
+ context: {
922
+ actions: [action],
923
+ slot: slotNumber,
924
+ sender: this.getSenderAddress().toString(),
925
+ },
926
+ });
717
927
  // Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
718
928
  }
719
929
 
@@ -918,14 +1128,15 @@ export class SequencerPublisher {
918
1128
  const checkpointHeader = checkpoint.header;
919
1129
 
920
1130
  const blobFields = checkpoint.toBlobFields();
921
- const blobs = getBlobsPerL1Block(blobFields);
1131
+ const blobs = await getBlobsPerL1Block(blobFields);
922
1132
 
923
- const proposeTxArgs = {
1133
+ const proposeTxArgs: L1ProcessArgs = {
924
1134
  header: checkpointHeader,
925
1135
  archive: checkpoint.archive.root.toBuffer(),
926
1136
  blobs,
927
1137
  attestationsAndSigners,
928
1138
  attestationsAndSignersSignature,
1139
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
929
1140
  };
930
1141
 
931
1142
  let ts: bigint;
@@ -1008,6 +1219,8 @@ export class SequencerPublisher {
1008
1219
 
1009
1220
  this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
1010
1221
 
1222
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1223
+
1011
1224
  let gasUsed: bigint;
1012
1225
  const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
1013
1226
  try {
@@ -1017,6 +1230,19 @@ export class SequencerPublisher {
1017
1230
  const viemError = formatViemError(err, simulateAbi);
1018
1231
  this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
1019
1232
 
1233
+ this.backupFailedTx({
1234
+ id: keccak256(request.data!),
1235
+ failureType: 'simulation',
1236
+ request: { to: request.to!, data: request.data!, value: request.value?.toString() },
1237
+ l1BlockNumber: l1BlockNumber.toString(),
1238
+ error: { message: viemError.message, name: viemError.name },
1239
+ context: {
1240
+ actions: [action],
1241
+ slot: slotNumber,
1242
+ sender: this.getSenderAddress().toString(),
1243
+ },
1244
+ });
1245
+
1020
1246
  return false;
1021
1247
  }
1022
1248
 
@@ -1100,9 +1326,27 @@ export class SequencerPublisher {
1100
1326
  kzg,
1101
1327
  },
1102
1328
  )
1103
- .catch(err => {
1104
- const { message, metaMessages } = formatViemError(err);
1105
- this.log.error(`Failed to validate blobs`, message, { metaMessages });
1329
+ .catch(async err => {
1330
+ const viemError = formatViemError(err);
1331
+ this.log.error(`Failed to validate blobs`, viemError.message, { metaMessages: viemError.metaMessages });
1332
+ const validateBlobsData = encodeFunctionData({
1333
+ abi: RollupAbi,
1334
+ functionName: 'validateBlobs',
1335
+ args: [blobInput],
1336
+ });
1337
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1338
+ this.backupFailedTx({
1339
+ id: keccak256(validateBlobsData),
1340
+ failureType: 'simulation',
1341
+ request: { to: this.rollupContract.address as Hex, data: validateBlobsData },
1342
+ blobData: encodedData.blobs.map(b => toHex(b.data)) as Hex[],
1343
+ l1BlockNumber: l1BlockNumber.toString(),
1344
+ error: { message: viemError.message, name: viemError.name },
1345
+ context: {
1346
+ actions: ['validate-blobs'],
1347
+ sender: this.getSenderAddress().toString(),
1348
+ },
1349
+ });
1106
1350
  throw new Error('Failed to validate blobs');
1107
1351
  });
1108
1352
  }
@@ -1113,8 +1357,7 @@ export class SequencerPublisher {
1113
1357
  header: encodedData.header.toViem(),
1114
1358
  archive: toHex(encodedData.archive),
1115
1359
  oracleInput: {
1116
- // We are currently not modifying these. See #9963
1117
- feeAssetPriceModifier: 0n,
1360
+ feeAssetPriceModifier: encodedData.feeAssetPriceModifier,
1118
1361
  },
1119
1362
  },
1120
1363
  encodedData.attestationsAndSigners.getPackedAttestations(),
@@ -1140,7 +1383,7 @@ export class SequencerPublisher {
1140
1383
  readonly header: ViemHeader;
1141
1384
  readonly archive: `0x${string}`;
1142
1385
  readonly oracleInput: {
1143
- readonly feeAssetPriceModifier: 0n;
1386
+ readonly feeAssetPriceModifier: bigint;
1144
1387
  };
1145
1388
  },
1146
1389
  ViemCommitteeAttestations,
@@ -1182,6 +1425,8 @@ export class SequencerPublisher {
1182
1425
  });
1183
1426
  }
1184
1427
 
1428
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1429
+
1185
1430
  const simulationResult = await this.l1TxUtils
1186
1431
  .simulate(
1187
1432
  {
@@ -1215,6 +1460,18 @@ export class SequencerPublisher {
1215
1460
  };
1216
1461
  }
1217
1462
  this.log.error(`Failed to simulate propose tx`, viemError);
1463
+ this.backupFailedTx({
1464
+ id: keccak256(rollupData),
1465
+ failureType: 'simulation',
1466
+ request: { to: this.rollupContract.address, data: rollupData },
1467
+ l1BlockNumber: l1BlockNumber.toString(),
1468
+ error: { message: viemError.message, name: viemError.name },
1469
+ context: {
1470
+ actions: ['propose'],
1471
+ slot: Number(args[0].header.slotNumber),
1472
+ sender: this.getSenderAddress().toString(),
1473
+ },
1474
+ });
1218
1475
  throw err;
1219
1476
  });
1220
1477