@aztec/sequencer-client 0.0.1-commit.d6f2b3f94 → 0.0.1-commit.dbf9cec

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 +13 -4
  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 +13 -2
  29. package/dest/publisher/sequencer-publisher.d.ts +22 -7
  30. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  31. package/dest/publisher/sequencer-publisher.js +258 -29
  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 +39 -11
  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 +15 -7
  39. package/dest/sequencer/sequencer.d.ts.map +1 -1
  40. package/dest/sequencer/sequencer.js +23 -25
  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 +17 -8
  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 +23 -6
  61. package/src/publisher/sequencer-publisher.ts +241 -36
  62. package/src/sequencer/checkpoint_proposal_job.ts +59 -10
  63. package/src/sequencer/metrics.ts +92 -18
  64. package/src/sequencer/sequencer.ts +31 -30
  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,11 +19,11 @@ 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';
@@ -32,6 +33,7 @@ import type { Fr } from '@aztec/foundation/curves/bn254';
32
33
  import { EthAddress } from '@aztec/foundation/eth-address';
33
34
  import { Signature, type ViemSignature } from '@aztec/foundation/eth-signature';
34
35
  import { type Logger, createLogger } from '@aztec/foundation/log';
36
+ import { makeBackoff, retry } from '@aztec/foundation/retry';
35
37
  import { bufferToHex } from '@aztec/foundation/string';
36
38
  import { DateProvider, Timer } from '@aztec/foundation/timer';
37
39
  import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
@@ -43,9 +45,19 @@ import type { CheckpointHeader } from '@aztec/stdlib/rollup';
43
45
  import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
44
46
  import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
45
47
 
46
- import { type StateOverride, type TransactionReceipt, type TypedDataDefinition, encodeFunctionData, toHex } from 'viem';
47
-
48
- import type { PublisherConfig, TxSenderConfig } from './config.js';
48
+ import {
49
+ type Hex,
50
+ type StateOverride,
51
+ type TransactionReceipt,
52
+ type TypedDataDefinition,
53
+ encodeFunctionData,
54
+ keccak256,
55
+ multicall3Abi,
56
+ toHex,
57
+ } from 'viem';
58
+
59
+ import type { SequencerPublisherConfig } from './config.js';
60
+ import { type FailedL1Tx, type L1TxFailedStore, createL1TxFailedStore } from './l1_tx_failed_store/index.js';
49
61
  import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
50
62
 
51
63
  /** Arguments to the process method of the rollup contract */
@@ -60,6 +72,8 @@ type L1ProcessArgs = {
60
72
  attestationsAndSigners: CommitteeAttestationsAndSigners;
61
73
  /** Attestations and signers signature */
62
74
  attestationsAndSignersSignature: Signature;
75
+ /** The fee asset price modifier in basis points (from oracle) */
76
+ feeAssetPriceModifier: bigint;
63
77
  };
64
78
 
65
79
  export const Actions = [
@@ -105,6 +119,7 @@ export class SequencerPublisher {
105
119
  private interrupted = false;
106
120
  private metrics: SequencerPublisherMetrics;
107
121
  public epochCache: EpochCache;
122
+ private failedTxStore?: Promise<L1TxFailedStore | undefined>;
108
123
 
109
124
  protected governanceLog = createLogger('sequencer:publisher:governance');
110
125
  protected slashingLog = createLogger('sequencer:publisher:slashing');
@@ -112,6 +127,7 @@ export class SequencerPublisher {
112
127
  protected lastActions: Partial<Record<Action, SlotNumber>> = {};
113
128
 
114
129
  private isPayloadEmptyCache: Map<string, boolean> = new Map<string, boolean>();
130
+ private payloadProposedCache: Set<string> = new Set<string>();
115
131
 
116
132
  protected log: Logger;
117
133
  protected ethereumSlotDuration: bigint;
@@ -123,13 +139,17 @@ export class SequencerPublisher {
123
139
 
124
140
  /** L1 fee analyzer for fisherman mode */
125
141
  private l1FeeAnalyzer?: L1FeeAnalyzer;
142
+
143
+ /** Fee asset price oracle for computing price modifiers from Uniswap V4 */
144
+ private feeAssetPriceOracle: FeeAssetPriceOracle;
145
+
126
146
  // A CALL to a cold address is 2700 gas
127
147
  public static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
128
148
 
129
149
  // Gas report for VotingWithSigTest shows a max gas of 100k, but we've seen it cost 700k+ in testnet
130
150
  public static VOTE_GAS_GUESS: bigint = 800_000n;
131
151
 
132
- public l1TxUtils: L1TxUtilsWithBlobs;
152
+ public l1TxUtils: L1TxUtils;
133
153
  public rollupContract: RollupContract;
134
154
  public govProposerContract: GovernanceProposerContract;
135
155
  public slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
@@ -140,11 +160,12 @@ export class SequencerPublisher {
140
160
  protected requests: RequestWithExpiry[] = [];
141
161
 
142
162
  constructor(
143
- private config: TxSenderConfig & PublisherConfig & Pick<L1ContractsConfig, 'ethereumSlotDuration'>,
163
+ private config: Pick<SequencerPublisherConfig, 'fishermanMode' | 'l1TxFailedStore'> &
164
+ Pick<L1ContractsConfig, 'ethereumSlotDuration'> & { l1ChainId: number },
144
165
  deps: {
145
166
  telemetry?: TelemetryClient;
146
167
  blobClient: BlobClientInterface;
147
- l1TxUtils: L1TxUtilsWithBlobs;
168
+ l1TxUtils: L1TxUtils;
148
169
  rollupContract: RollupContract;
149
170
  slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
150
171
  governanceProposerContract: GovernanceProposerContract;
@@ -188,12 +209,52 @@ export class SequencerPublisher {
188
209
  createLogger('sequencer:publisher:fee-analyzer'),
189
210
  );
190
211
  }
212
+
213
+ // Initialize fee asset price oracle
214
+ this.feeAssetPriceOracle = new FeeAssetPriceOracle(
215
+ this.l1TxUtils.client,
216
+ this.rollupContract,
217
+ createLogger('sequencer:publisher:price-oracle'),
218
+ );
219
+
220
+ // Initialize failed L1 tx store (optional, for test networks)
221
+ this.failedTxStore = createL1TxFailedStore(config.l1TxFailedStore, this.log);
222
+ }
223
+
224
+ /**
225
+ * Backs up a failed L1 transaction to the configured store for debugging.
226
+ * Does nothing if no store is configured.
227
+ */
228
+ private backupFailedTx(failedTx: Omit<FailedL1Tx, 'timestamp'>): void {
229
+ if (!this.failedTxStore) {
230
+ return;
231
+ }
232
+
233
+ const tx: FailedL1Tx = {
234
+ ...failedTx,
235
+ timestamp: Date.now(),
236
+ };
237
+
238
+ // Fire and forget - don't block on backup
239
+ void this.failedTxStore
240
+ .then(store => store?.saveFailedTx(tx))
241
+ .catch(err => {
242
+ this.log.warn(`Failed to backup failed L1 tx to store`, err);
243
+ });
191
244
  }
192
245
 
193
246
  public getRollupContract(): RollupContract {
194
247
  return this.rollupContract;
195
248
  }
196
249
 
250
+ /**
251
+ * Gets the fee asset price modifier from the oracle.
252
+ * Returns 0n if the oracle query fails.
253
+ */
254
+ public getFeeAssetPriceModifier(): Promise<bigint> {
255
+ return this.feeAssetPriceOracle.computePriceModifier();
256
+ }
257
+
197
258
  public getSenderAddress() {
198
259
  return this.l1TxUtils.getSenderAddress();
199
260
  }
@@ -361,6 +422,21 @@ export class SequencerPublisher {
361
422
  validRequests.sort((a, b) => compareActions(a.action, b.action));
362
423
 
363
424
  try {
425
+ // Capture context for failed tx backup before sending
426
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
427
+ const multicallData = encodeFunctionData({
428
+ abi: multicall3Abi,
429
+ functionName: 'aggregate3',
430
+ args: [
431
+ validRequests.map(r => ({
432
+ target: r.request.to!,
433
+ callData: r.request.data!,
434
+ allowFailure: true,
435
+ })),
436
+ ],
437
+ });
438
+ const blobDataHex = blobConfig?.blobs?.map(b => toHex(b)) as Hex[] | undefined;
439
+
364
440
  this.log.debug('Forwarding transactions', {
365
441
  validRequests: validRequests.map(request => request.action),
366
442
  txConfig,
@@ -373,7 +449,12 @@ export class SequencerPublisher {
373
449
  this.rollupContract.address,
374
450
  this.log,
375
451
  );
376
- const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(validRequests, result);
452
+ const txContext = { multicallData, blobData: blobDataHex, l1BlockNumber };
453
+ const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(
454
+ validRequests,
455
+ result,
456
+ txContext,
457
+ );
377
458
  return { result, expiredActions, sentActions: validActions, successfulActions, failedActions };
378
459
  } catch (err) {
379
460
  const viemError = formatViemError(err);
@@ -393,11 +474,25 @@ export class SequencerPublisher {
393
474
 
394
475
  private callbackBundledTransactions(
395
476
  requests: RequestWithExpiry[],
396
- result?: { receipt: TransactionReceipt } | FormattedViemError,
477
+ result: { receipt: TransactionReceipt; errorMsg?: string } | FormattedViemError | undefined,
478
+ txContext: { multicallData: Hex; blobData?: Hex[]; l1BlockNumber: bigint },
397
479
  ) {
398
480
  const actionsListStr = requests.map(r => r.action).join(', ');
399
481
  if (result instanceof FormattedViemError) {
400
482
  this.log.error(`Failed to publish bundled transactions (${actionsListStr})`, result);
483
+ this.backupFailedTx({
484
+ id: keccak256(txContext.multicallData),
485
+ failureType: 'send-error',
486
+ request: { to: MULTI_CALL_3_ADDRESS, data: txContext.multicallData },
487
+ blobData: txContext.blobData,
488
+ l1BlockNumber: txContext.l1BlockNumber.toString(),
489
+ error: { message: result.message, name: result.name },
490
+ context: {
491
+ actions: requests.map(r => r.action),
492
+ requests: requests.map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
493
+ sender: this.getSenderAddress().toString(),
494
+ },
495
+ });
401
496
  return { failedActions: requests.map(r => r.action) };
402
497
  } else {
403
498
  this.log.verbose(`Published bundled transactions (${actionsListStr})`, { result, requests });
@@ -410,6 +505,30 @@ export class SequencerPublisher {
410
505
  failedActions.push(request.action);
411
506
  }
412
507
  }
508
+ // Single backup for the whole reverted tx
509
+ if (failedActions.length > 0 && result?.receipt?.status === 'reverted') {
510
+ this.backupFailedTx({
511
+ id: result.receipt.transactionHash,
512
+ failureType: 'revert',
513
+ request: { to: MULTI_CALL_3_ADDRESS, data: txContext.multicallData },
514
+ blobData: txContext.blobData,
515
+ l1BlockNumber: result.receipt.blockNumber.toString(),
516
+ receipt: {
517
+ transactionHash: result.receipt.transactionHash,
518
+ blockNumber: result.receipt.blockNumber.toString(),
519
+ gasUsed: (result.receipt.gasUsed ?? 0n).toString(),
520
+ status: 'reverted',
521
+ },
522
+ error: { message: result.errorMsg ?? 'Transaction reverted' },
523
+ context: {
524
+ actions: failedActions,
525
+ requests: requests
526
+ .filter(r => failedActions.includes(r.action))
527
+ .map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
528
+ sender: this.getSenderAddress().toString(),
529
+ },
530
+ });
531
+ }
413
532
  return { successfulActions, failedActions };
414
533
  }
415
534
  }
@@ -521,6 +640,8 @@ export class SequencerPublisher {
521
640
  const request = this.buildInvalidateCheckpointRequest(validationResult);
522
641
  this.log.debug(`Simulating invalidate checkpoint ${checkpointNumber}`, { ...logData, request });
523
642
 
643
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
644
+
524
645
  try {
525
646
  const { gasUsed } = await this.l1TxUtils.simulate(
526
647
  request,
@@ -572,6 +693,18 @@ export class SequencerPublisher {
572
693
 
573
694
  // Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
574
695
  this.log.error(`Simulation for invalidate checkpoint ${checkpointNumber} failed`, viemError, logData);
696
+ this.backupFailedTx({
697
+ id: keccak256(request.data!),
698
+ failureType: 'simulation',
699
+ request: { to: request.to!, data: request.data!, value: request.value?.toString() },
700
+ l1BlockNumber: l1BlockNumber.toString(),
701
+ error: { message: viemError.message, name: viemError.name },
702
+ context: {
703
+ actions: [`invalidate-${reason}`],
704
+ checkpointNumber,
705
+ sender: this.getSenderAddress().toString(),
706
+ },
707
+ });
575
708
  throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, { cause: viemError });
576
709
  }
577
710
  }
@@ -617,24 +750,8 @@ export class SequencerPublisher {
617
750
  options: { forcePendingCheckpointNumber?: CheckpointNumber },
618
751
  ): Promise<bigint> {
619
752
  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
753
  const blobFields = checkpoint.toBlobFields();
637
- const blobs = getBlobsPerL1Block(blobFields);
754
+ const blobs = await getBlobsPerL1Block(blobFields);
638
755
  const blobInput = getPrefixedEthBlobCommitments(blobs);
639
756
 
640
757
  const args = [
@@ -642,7 +759,7 @@ export class SequencerPublisher {
642
759
  header: checkpoint.header.toViem(),
643
760
  archive: toHex(checkpoint.archive.root.toBuffer()),
644
761
  oracleInput: {
645
- feeAssetPriceModifier: 0n,
762
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
646
763
  },
647
764
  },
648
765
  attestationsAndSigners.getPackedAttestations(),
@@ -691,6 +808,32 @@ export class SequencerPublisher {
691
808
  return false;
692
809
  }
693
810
 
811
+ // Check if payload was already submitted to governance
812
+ const cacheKey = payload.toString();
813
+ if (!this.payloadProposedCache.has(cacheKey)) {
814
+ try {
815
+ const l1StartBlock = await this.rollupContract.getL1StartBlock();
816
+ const proposed = await retry(
817
+ () => base.hasPayloadBeenProposed(payload.toString(), l1StartBlock),
818
+ 'Check if payload was proposed',
819
+ makeBackoff([0, 1, 2]),
820
+ this.log,
821
+ true,
822
+ );
823
+ if (proposed) {
824
+ this.payloadProposedCache.add(cacheKey);
825
+ }
826
+ } catch (err) {
827
+ this.log.warn(`Failed to check if payload ${payload} was proposed after retries, skipping signal`, err);
828
+ return false;
829
+ }
830
+ }
831
+
832
+ if (this.payloadProposedCache.has(cacheKey)) {
833
+ this.log.info(`Payload ${payload} was already proposed to governance, stopping signals`);
834
+ return false;
835
+ }
836
+
694
837
  const cachedLastVote = this.lastActions[signalType];
695
838
  this.lastActions[signalType] = slotNumber;
696
839
  const action = signalType;
@@ -709,11 +852,26 @@ export class SequencerPublisher {
709
852
  lastValidL2Slot: slotNumber,
710
853
  });
711
854
 
855
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
856
+
712
857
  try {
713
858
  await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
714
859
  this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
715
860
  } catch (err) {
716
- this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, err);
861
+ const viemError = formatViemError(err);
862
+ this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, viemError);
863
+ this.backupFailedTx({
864
+ id: keccak256(request.data!),
865
+ failureType: 'simulation',
866
+ request: { to: request.to!, data: request.data!, value: request.value?.toString() },
867
+ l1BlockNumber: l1BlockNumber.toString(),
868
+ error: { message: viemError.message, name: viemError.name },
869
+ context: {
870
+ actions: [action],
871
+ slot: slotNumber,
872
+ sender: this.getSenderAddress().toString(),
873
+ },
874
+ });
717
875
  // Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
718
876
  }
719
877
 
@@ -918,14 +1076,15 @@ export class SequencerPublisher {
918
1076
  const checkpointHeader = checkpoint.header;
919
1077
 
920
1078
  const blobFields = checkpoint.toBlobFields();
921
- const blobs = getBlobsPerL1Block(blobFields);
1079
+ const blobs = await getBlobsPerL1Block(blobFields);
922
1080
 
923
- const proposeTxArgs = {
1081
+ const proposeTxArgs: L1ProcessArgs = {
924
1082
  header: checkpointHeader,
925
1083
  archive: checkpoint.archive.root.toBuffer(),
926
1084
  blobs,
927
1085
  attestationsAndSigners,
928
1086
  attestationsAndSignersSignature,
1087
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
929
1088
  };
930
1089
 
931
1090
  let ts: bigint;
@@ -1008,6 +1167,8 @@ export class SequencerPublisher {
1008
1167
 
1009
1168
  this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
1010
1169
 
1170
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1171
+
1011
1172
  let gasUsed: bigint;
1012
1173
  const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
1013
1174
  try {
@@ -1017,6 +1178,19 @@ export class SequencerPublisher {
1017
1178
  const viemError = formatViemError(err, simulateAbi);
1018
1179
  this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
1019
1180
 
1181
+ this.backupFailedTx({
1182
+ id: keccak256(request.data!),
1183
+ failureType: 'simulation',
1184
+ request: { to: request.to!, data: request.data!, value: request.value?.toString() },
1185
+ l1BlockNumber: l1BlockNumber.toString(),
1186
+ error: { message: viemError.message, name: viemError.name },
1187
+ context: {
1188
+ actions: [action],
1189
+ slot: slotNumber,
1190
+ sender: this.getSenderAddress().toString(),
1191
+ },
1192
+ });
1193
+
1020
1194
  return false;
1021
1195
  }
1022
1196
 
@@ -1100,9 +1274,27 @@ export class SequencerPublisher {
1100
1274
  kzg,
1101
1275
  },
1102
1276
  )
1103
- .catch(err => {
1104
- const { message, metaMessages } = formatViemError(err);
1105
- this.log.error(`Failed to validate blobs`, message, { metaMessages });
1277
+ .catch(async err => {
1278
+ const viemError = formatViemError(err);
1279
+ this.log.error(`Failed to validate blobs`, viemError.message, { metaMessages: viemError.metaMessages });
1280
+ const validateBlobsData = encodeFunctionData({
1281
+ abi: RollupAbi,
1282
+ functionName: 'validateBlobs',
1283
+ args: [blobInput],
1284
+ });
1285
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1286
+ this.backupFailedTx({
1287
+ id: keccak256(validateBlobsData),
1288
+ failureType: 'simulation',
1289
+ request: { to: this.rollupContract.address as Hex, data: validateBlobsData },
1290
+ blobData: encodedData.blobs.map(b => toHex(b.data)) as Hex[],
1291
+ l1BlockNumber: l1BlockNumber.toString(),
1292
+ error: { message: viemError.message, name: viemError.name },
1293
+ context: {
1294
+ actions: ['validate-blobs'],
1295
+ sender: this.getSenderAddress().toString(),
1296
+ },
1297
+ });
1106
1298
  throw new Error('Failed to validate blobs');
1107
1299
  });
1108
1300
  }
@@ -1113,8 +1305,7 @@ export class SequencerPublisher {
1113
1305
  header: encodedData.header.toViem(),
1114
1306
  archive: toHex(encodedData.archive),
1115
1307
  oracleInput: {
1116
- // We are currently not modifying these. See #9963
1117
- feeAssetPriceModifier: 0n,
1308
+ feeAssetPriceModifier: encodedData.feeAssetPriceModifier,
1118
1309
  },
1119
1310
  },
1120
1311
  encodedData.attestationsAndSigners.getPackedAttestations(),
@@ -1140,7 +1331,7 @@ export class SequencerPublisher {
1140
1331
  readonly header: ViemHeader;
1141
1332
  readonly archive: `0x${string}`;
1142
1333
  readonly oracleInput: {
1143
- readonly feeAssetPriceModifier: 0n;
1334
+ readonly feeAssetPriceModifier: bigint;
1144
1335
  };
1145
1336
  },
1146
1337
  ViemCommitteeAttestations,
@@ -1182,6 +1373,8 @@ export class SequencerPublisher {
1182
1373
  });
1183
1374
  }
1184
1375
 
1376
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1377
+
1185
1378
  const simulationResult = await this.l1TxUtils
1186
1379
  .simulate(
1187
1380
  {
@@ -1215,6 +1408,18 @@ export class SequencerPublisher {
1215
1408
  };
1216
1409
  }
1217
1410
  this.log.error(`Failed to simulate propose tx`, viemError);
1411
+ this.backupFailedTx({
1412
+ id: keccak256(rollupData),
1413
+ failureType: 'simulation',
1414
+ request: { to: this.rollupContract.address, data: rollupData },
1415
+ l1BlockNumber: l1BlockNumber.toString(),
1416
+ error: { message: viemError.message, name: viemError.name },
1417
+ context: {
1418
+ actions: ['propose'],
1419
+ slot: Number(args[0].header.slotNumber),
1420
+ sender: this.getSenderAddress().toString(),
1421
+ },
1422
+ });
1218
1423
  throw err;
1219
1424
  });
1220
1425
 
@@ -38,7 +38,7 @@ import {
38
38
  } from '@aztec/stdlib/interfaces/server';
39
39
  import { type L1ToL2MessageSource, computeInHashFromL1ToL2Messages } from '@aztec/stdlib/messaging';
40
40
  import type { BlockProposalOptions, CheckpointProposal, CheckpointProposalOptions } from '@aztec/stdlib/p2p';
41
- import { orderAttestations } from '@aztec/stdlib/p2p';
41
+ import { orderAttestations, trimAttestations } from '@aztec/stdlib/p2p';
42
42
  import type { L2BlockBuiltStats } from '@aztec/stdlib/stats';
43
43
  import { type FailedTx, Tx } from '@aztec/stdlib/tx';
44
44
  import { AttestationTimeoutError } from '@aztec/stdlib/validators';
@@ -129,7 +129,7 @@ export class CheckpointProposalJob implements Traceable {
129
129
  await Promise.all(votesPromises);
130
130
 
131
131
  if (checkpoint) {
132
- this.metrics.recordBlockProposalSuccess();
132
+ this.metrics.recordCheckpointProposalSuccess();
133
133
  }
134
134
 
135
135
  // Do not post anything to L1 if we are fishermen, but do perform L1 fee analysis
@@ -186,18 +186,21 @@ export class CheckpointProposalJob implements Traceable {
186
186
  const inHash = computeInHashFromL1ToL2Messages(l1ToL2Messages);
187
187
 
188
188
  // Collect the out hashes of all the checkpoints before this one in the same epoch
189
- const previousCheckpoints = (await this.l2BlockSource.getCheckpointsForEpoch(this.epoch)).filter(
190
- c => c.number < this.checkpointNumber,
191
- );
192
- const previousCheckpointOutHashes = previousCheckpoints.map(c => c.getCheckpointOutHash());
189
+ const previousCheckpointOutHashes = (await this.l2BlockSource.getCheckpointsDataForEpoch(this.epoch))
190
+ .filter(c => c.checkpointNumber < this.checkpointNumber)
191
+ .map(c => c.checkpointOutHash);
192
+
193
+ // Get the fee asset price modifier from the oracle
194
+ const feeAssetPriceModifier = await this.publisher.getFeeAssetPriceModifier();
193
195
 
194
196
  // Create a long-lived forked world state for the checkpoint builder
195
- using fork = await this.worldState.fork(this.syncedToBlockNumber, { closeDelayMs: 12_000 });
197
+ await using fork = await this.worldState.fork(this.syncedToBlockNumber, { closeDelayMs: 12_000 });
196
198
 
197
199
  // Create checkpoint builder for the entire slot
198
200
  const checkpointBuilder = await this.checkpointsBuilder.startCheckpoint(
199
201
  this.checkpointNumber,
200
202
  checkpointGlobalVariables,
203
+ feeAssetPriceModifier,
201
204
  l1ToL2Messages,
202
205
  previousCheckpointOutHashes,
203
206
  fork,
@@ -217,6 +220,7 @@ export class CheckpointProposalJob implements Traceable {
217
220
 
218
221
  let blocksInCheckpoint: L2Block[] = [];
219
222
  let blockPendingBroadcast: { block: L2Block; txs: Tx[] } | undefined = undefined;
223
+ const checkpointBuildTimer = new Timer();
220
224
 
221
225
  try {
222
226
  // Main loop: build blocks for the checkpoint
@@ -244,11 +248,28 @@ export class CheckpointProposalJob implements Traceable {
244
248
  return undefined;
245
249
  }
246
250
 
251
+ const minBlocksForCheckpoint = this.config.minBlocksForCheckpoint;
252
+ if (minBlocksForCheckpoint !== undefined && blocksInCheckpoint.length < minBlocksForCheckpoint) {
253
+ this.log.warn(
254
+ `Checkpoint has fewer blocks than minimum (${blocksInCheckpoint.length} < ${minBlocksForCheckpoint}), skipping proposal`,
255
+ { slot: this.slot, blocksBuilt: blocksInCheckpoint.length, minBlocksForCheckpoint },
256
+ );
257
+ return undefined;
258
+ }
259
+
247
260
  // Assemble and broadcast the checkpoint proposal, including the last block that was not
248
261
  // broadcasted yet, and wait to collect the committee attestations.
249
262
  this.setStateFn(SequencerState.ASSEMBLING_CHECKPOINT, this.slot);
250
263
  const checkpoint = await checkpointBuilder.completeCheckpoint();
251
264
 
265
+ // Record checkpoint-level build metrics
266
+ this.metrics.recordCheckpointBuild(
267
+ checkpointBuildTimer.ms(),
268
+ blocksInCheckpoint.length,
269
+ checkpoint.getStats().txCount,
270
+ Number(checkpoint.header.totalManaUsed.toBigInt()),
271
+ );
272
+
252
273
  // Do not collect attestations nor publish to L1 in fisherman mode
253
274
  if (this.config.fishermanMode) {
254
275
  this.log.info(
@@ -275,6 +296,7 @@ export class CheckpointProposalJob implements Traceable {
275
296
  const proposal = await this.validatorClient.createCheckpointProposal(
276
297
  checkpoint.header,
277
298
  checkpoint.archive.root,
299
+ feeAssetPriceModifier,
278
300
  lastBlock,
279
301
  this.proposer,
280
302
  checkpointProposalOptions,
@@ -313,6 +335,21 @@ export class CheckpointProposalJob implements Traceable {
313
335
  const aztecSlotDuration = this.l1Constants.slotDuration;
314
336
  const slotStartBuildTimestamp = this.getSlotStartBuildTimestamp();
315
337
  const txTimeoutAt = new Date((slotStartBuildTimestamp + aztecSlotDuration) * 1000);
338
+
339
+ // If we have been configured to potentially skip publishing checkpoint then roll the dice here
340
+ if (
341
+ this.config.skipPublishingCheckpointsPercent !== undefined &&
342
+ this.config.skipPublishingCheckpointsPercent > 0
343
+ ) {
344
+ const result = Math.max(0, randomInt(100));
345
+ if (result < this.config.skipPublishingCheckpointsPercent) {
346
+ this.log.warn(
347
+ `Skipping publishing proposal for checkpoint ${checkpoint.number}. Configured percentage: ${this.config.skipPublishingCheckpointsPercent}, generated value: ${result}`,
348
+ );
349
+ return checkpoint;
350
+ }
351
+ }
352
+
316
353
  await this.publisher.enqueueProposeCheckpoint(checkpoint, attestations, attestationsSignature, {
317
354
  txTimeoutAt,
318
355
  forcePendingCheckpointNumber: this.invalidateCheckpoint?.forcePendingCheckpointNumber,
@@ -516,7 +553,7 @@ export class CheckpointProposalJob implements Traceable {
516
553
  // Create iterator to pending txs. We filter out txs already included in previous blocks in the checkpoint
517
554
  // just in case p2p failed to sync the provisional block and didn't get to remove those txs from the mempool yet.
518
555
  const pendingTxs = filter(
519
- this.p2pClient.iteratePendingTxs(),
556
+ this.p2pClient.iterateEligiblePendingTxs(),
520
557
  tx => !txHashesAlreadyIncluded.has(tx.txHash.toString()),
521
558
  );
522
559
 
@@ -706,8 +743,20 @@ export class CheckpointProposalJob implements Traceable {
706
743
 
707
744
  collectedAttestationsCount = attestations.length;
708
745
 
746
+ // Trim attestations to minimum required to save L1 calldata gas
747
+ const localAddresses = this.validatorClient.getValidatorAddresses();
748
+ const trimmed = trimAttestations(
749
+ attestations,
750
+ numberOfRequiredAttestations,
751
+ this.attestorAddress,
752
+ localAddresses,
753
+ );
754
+ if (trimmed.length < attestations.length) {
755
+ this.log.debug(`Trimmed attestations from ${attestations.length} to ${trimmed.length} for L1 submission`);
756
+ }
757
+
709
758
  // Rollup contract requires that the signatures are provided in the order of the committee
710
- const sorted = orderAttestations(attestations, committee);
759
+ const sorted = orderAttestations(trimmed, committee);
711
760
 
712
761
  // Manipulate the attestations if we've been configured to do so
713
762
  if (this.config.injectFakeAttestation || this.config.shuffleAttestationOrdering) {
@@ -821,7 +870,7 @@ export class CheckpointProposalJob implements Traceable {
821
870
  slot: this.slot,
822
871
  feeAnalysisId: feeAnalysis?.id,
823
872
  });
824
- this.metrics.recordBlockProposalFailed('block_build_failed');
873
+ this.metrics.recordCheckpointProposalFailed('block_build_failed');
825
874
  }
826
875
 
827
876
  this.publisher.clearPendingRequests();