@aztec/sequencer-client 0.0.1-commit.8f9871590 → 0.0.1-commit.9117c5f5a

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 (60) 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 +15 -7
  30. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  31. package/dest/publisher/sequencer-publisher.js +244 -24
  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 +34 -8
  35. package/dest/sequencer/metrics.d.ts +13 -5
  36. package/dest/sequencer/metrics.d.ts.map +1 -1
  37. package/dest/sequencer/metrics.js +32 -10
  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/package.json +28 -28
  45. package/src/client/sequencer-client.ts +25 -7
  46. package/src/config.ts +17 -8
  47. package/src/global_variable_builder/global_builder.ts +1 -1
  48. package/src/publisher/config.ts +121 -43
  49. package/src/publisher/index.ts +3 -0
  50. package/src/publisher/l1_tx_failed_store/factory.ts +32 -0
  51. package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +55 -0
  52. package/src/publisher/l1_tx_failed_store/file_store_failed_tx_store.ts +46 -0
  53. package/src/publisher/l1_tx_failed_store/index.ts +3 -0
  54. package/src/publisher/sequencer-publisher-factory.ts +23 -6
  55. package/src/publisher/sequencer-publisher.ts +214 -31
  56. package/src/sequencer/checkpoint_proposal_job.ts +53 -9
  57. package/src/sequencer/metrics.ts +39 -13
  58. package/src/sequencer/sequencer.ts +31 -30
  59. package/src/sequencer/timetable.ts +1 -1
  60. package/src/test/index.ts +2 -4
@@ -19,11 +19,11 @@ import {
19
19
  type L1BlobInputs,
20
20
  type L1TxConfig,
21
21
  type L1TxRequest,
22
+ type L1TxUtils,
22
23
  MAX_L1_TX_LIMIT,
23
24
  type TransactionStats,
24
25
  WEI_CONST,
25
26
  } from '@aztec/ethereum/l1-tx-utils';
26
- import type { L1TxUtilsWithBlobs } from '@aztec/ethereum/l1-tx-utils-with-blobs';
27
27
  import { FormattedViemError, formatViemError, mergeAbis, tryExtractEvent } from '@aztec/ethereum/utils';
28
28
  import { sumBigint } from '@aztec/foundation/bigint';
29
29
  import { toHex as toPaddedHex } from '@aztec/foundation/bigint-buffer';
@@ -33,6 +33,7 @@ import type { Fr } from '@aztec/foundation/curves/bn254';
33
33
  import { EthAddress } from '@aztec/foundation/eth-address';
34
34
  import { Signature, type ViemSignature } from '@aztec/foundation/eth-signature';
35
35
  import { type Logger, createLogger } from '@aztec/foundation/log';
36
+ import { makeBackoff, retry } from '@aztec/foundation/retry';
36
37
  import { bufferToHex } from '@aztec/foundation/string';
37
38
  import { DateProvider, Timer } from '@aztec/foundation/timer';
38
39
  import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
@@ -44,9 +45,19 @@ import type { CheckpointHeader } from '@aztec/stdlib/rollup';
44
45
  import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
45
46
  import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
46
47
 
47
- import { type StateOverride, type TransactionReceipt, type TypedDataDefinition, encodeFunctionData, toHex } from 'viem';
48
-
49
- 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';
50
61
  import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
51
62
 
52
63
  /** Arguments to the process method of the rollup contract */
@@ -108,6 +119,7 @@ export class SequencerPublisher {
108
119
  private interrupted = false;
109
120
  private metrics: SequencerPublisherMetrics;
110
121
  public epochCache: EpochCache;
122
+ private failedTxStore?: Promise<L1TxFailedStore | undefined>;
111
123
 
112
124
  protected governanceLog = createLogger('sequencer:publisher:governance');
113
125
  protected slashingLog = createLogger('sequencer:publisher:slashing');
@@ -115,6 +127,7 @@ export class SequencerPublisher {
115
127
  protected lastActions: Partial<Record<Action, SlotNumber>> = {};
116
128
 
117
129
  private isPayloadEmptyCache: Map<string, boolean> = new Map<string, boolean>();
130
+ private payloadProposedCache: Set<string> = new Set<string>();
118
131
 
119
132
  protected log: Logger;
120
133
  protected ethereumSlotDuration: bigint;
@@ -136,7 +149,7 @@ export class SequencerPublisher {
136
149
  // Gas report for VotingWithSigTest shows a max gas of 100k, but we've seen it cost 700k+ in testnet
137
150
  public static VOTE_GAS_GUESS: bigint = 800_000n;
138
151
 
139
- public l1TxUtils: L1TxUtilsWithBlobs;
152
+ public l1TxUtils: L1TxUtils;
140
153
  public rollupContract: RollupContract;
141
154
  public govProposerContract: GovernanceProposerContract;
142
155
  public slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
@@ -147,11 +160,12 @@ export class SequencerPublisher {
147
160
  protected requests: RequestWithExpiry[] = [];
148
161
 
149
162
  constructor(
150
- private config: TxSenderConfig & PublisherConfig & Pick<L1ContractsConfig, 'ethereumSlotDuration'>,
163
+ private config: Pick<SequencerPublisherConfig, 'fishermanMode' | 'l1TxFailedStore'> &
164
+ Pick<L1ContractsConfig, 'ethereumSlotDuration'> & { l1ChainId: number },
151
165
  deps: {
152
166
  telemetry?: TelemetryClient;
153
167
  blobClient: BlobClientInterface;
154
- l1TxUtils: L1TxUtilsWithBlobs;
168
+ l1TxUtils: L1TxUtils;
155
169
  rollupContract: RollupContract;
156
170
  slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
157
171
  governanceProposerContract: GovernanceProposerContract;
@@ -202,6 +216,31 @@ export class SequencerPublisher {
202
216
  this.rollupContract,
203
217
  createLogger('sequencer:publisher:price-oracle'),
204
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
+ });
205
244
  }
206
245
 
207
246
  public getRollupContract(): RollupContract {
@@ -383,6 +422,21 @@ export class SequencerPublisher {
383
422
  validRequests.sort((a, b) => compareActions(a.action, b.action));
384
423
 
385
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
+
386
440
  this.log.debug('Forwarding transactions', {
387
441
  validRequests: validRequests.map(request => request.action),
388
442
  txConfig,
@@ -395,7 +449,12 @@ export class SequencerPublisher {
395
449
  this.rollupContract.address,
396
450
  this.log,
397
451
  );
398
- 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
+ );
399
458
  return { result, expiredActions, sentActions: validActions, successfulActions, failedActions };
400
459
  } catch (err) {
401
460
  const viemError = formatViemError(err);
@@ -415,11 +474,25 @@ export class SequencerPublisher {
415
474
 
416
475
  private callbackBundledTransactions(
417
476
  requests: RequestWithExpiry[],
418
- result?: { receipt: TransactionReceipt } | FormattedViemError,
477
+ result: { receipt: TransactionReceipt; errorMsg?: string } | FormattedViemError | undefined,
478
+ txContext: { multicallData: Hex; blobData?: Hex[]; l1BlockNumber: bigint },
419
479
  ) {
420
480
  const actionsListStr = requests.map(r => r.action).join(', ');
421
481
  if (result instanceof FormattedViemError) {
422
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
+ });
423
496
  return { failedActions: requests.map(r => r.action) };
424
497
  } else {
425
498
  this.log.verbose(`Published bundled transactions (${actionsListStr})`, { result, requests });
@@ -432,6 +505,30 @@ export class SequencerPublisher {
432
505
  failedActions.push(request.action);
433
506
  }
434
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
+ }
435
532
  return { successfulActions, failedActions };
436
533
  }
437
534
  }
@@ -543,6 +640,8 @@ export class SequencerPublisher {
543
640
  const request = this.buildInvalidateCheckpointRequest(validationResult);
544
641
  this.log.debug(`Simulating invalidate checkpoint ${checkpointNumber}`, { ...logData, request });
545
642
 
643
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
644
+
546
645
  try {
547
646
  const { gasUsed } = await this.l1TxUtils.simulate(
548
647
  request,
@@ -594,6 +693,18 @@ export class SequencerPublisher {
594
693
 
595
694
  // Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
596
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
+ });
597
708
  throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, { cause: viemError });
598
709
  }
599
710
  }
@@ -639,24 +750,8 @@ export class SequencerPublisher {
639
750
  options: { forcePendingCheckpointNumber?: CheckpointNumber },
640
751
  ): Promise<bigint> {
641
752
  const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
642
-
643
- // TODO(palla/mbps): This should not be needed, there's no flow where we propose with zero attestations. Or is there?
644
- // If we have no attestations, we still need to provide the empty attestations
645
- // so that the committee is recalculated correctly
646
- // const ignoreSignatures = attestationsAndSigners.attestations.length === 0;
647
- // if (ignoreSignatures) {
648
- // const { committee } = await this.epochCache.getCommittee(block.header.globalVariables.slotNumber);
649
- // if (!committee) {
650
- // this.log.warn(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
651
- // throw new Error(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
652
- // }
653
- // attestationsAndSigners.attestations = committee.map(committeeMember =>
654
- // CommitteeAttestation.fromAddress(committeeMember),
655
- // );
656
- // }
657
-
658
753
  const blobFields = checkpoint.toBlobFields();
659
- const blobs = getBlobsPerL1Block(blobFields);
754
+ const blobs = await getBlobsPerL1Block(blobFields);
660
755
  const blobInput = getPrefixedEthBlobCommitments(blobs);
661
756
 
662
757
  const args = [
@@ -713,6 +808,32 @@ export class SequencerPublisher {
713
808
  return false;
714
809
  }
715
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
+
716
837
  const cachedLastVote = this.lastActions[signalType];
717
838
  this.lastActions[signalType] = slotNumber;
718
839
  const action = signalType;
@@ -731,11 +852,26 @@ export class SequencerPublisher {
731
852
  lastValidL2Slot: slotNumber,
732
853
  });
733
854
 
855
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
856
+
734
857
  try {
735
858
  await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
736
859
  this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
737
860
  } catch (err) {
738
- 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
+ });
739
875
  // Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
740
876
  }
741
877
 
@@ -940,7 +1076,7 @@ export class SequencerPublisher {
940
1076
  const checkpointHeader = checkpoint.header;
941
1077
 
942
1078
  const blobFields = checkpoint.toBlobFields();
943
- const blobs = getBlobsPerL1Block(blobFields);
1079
+ const blobs = await getBlobsPerL1Block(blobFields);
944
1080
 
945
1081
  const proposeTxArgs: L1ProcessArgs = {
946
1082
  header: checkpointHeader,
@@ -1031,6 +1167,8 @@ export class SequencerPublisher {
1031
1167
 
1032
1168
  this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
1033
1169
 
1170
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1171
+
1034
1172
  let gasUsed: bigint;
1035
1173
  const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
1036
1174
  try {
@@ -1040,6 +1178,19 @@ export class SequencerPublisher {
1040
1178
  const viemError = formatViemError(err, simulateAbi);
1041
1179
  this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
1042
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
+
1043
1194
  return false;
1044
1195
  }
1045
1196
 
@@ -1123,9 +1274,27 @@ export class SequencerPublisher {
1123
1274
  kzg,
1124
1275
  },
1125
1276
  )
1126
- .catch(err => {
1127
- const { message, metaMessages } = formatViemError(err);
1128
- 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
+ });
1129
1298
  throw new Error('Failed to validate blobs');
1130
1299
  });
1131
1300
  }
@@ -1204,6 +1373,8 @@ export class SequencerPublisher {
1204
1373
  });
1205
1374
  }
1206
1375
 
1376
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1377
+
1207
1378
  const simulationResult = await this.l1TxUtils
1208
1379
  .simulate(
1209
1380
  {
@@ -1237,6 +1408,18 @@ export class SequencerPublisher {
1237
1408
  };
1238
1409
  }
1239
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
+ });
1240
1423
  throw err;
1241
1424
  });
1242
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,16 +186,15 @@ 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);
193
192
 
194
193
  // Get the fee asset price modifier from the oracle
195
194
  const feeAssetPriceModifier = await this.publisher.getFeeAssetPriceModifier();
196
195
 
197
196
  // Create a long-lived forked world state for the checkpoint builder
198
- using fork = await this.worldState.fork(this.syncedToBlockNumber, { closeDelayMs: 12_000 });
197
+ await using fork = await this.worldState.fork(this.syncedToBlockNumber, { closeDelayMs: 12_000 });
199
198
 
200
199
  // Create checkpoint builder for the entire slot
201
200
  const checkpointBuilder = await this.checkpointsBuilder.startCheckpoint(
@@ -221,6 +220,7 @@ export class CheckpointProposalJob implements Traceable {
221
220
 
222
221
  let blocksInCheckpoint: L2Block[] = [];
223
222
  let blockPendingBroadcast: { block: L2Block; txs: Tx[] } | undefined = undefined;
223
+ const checkpointBuildTimer = new Timer();
224
224
 
225
225
  try {
226
226
  // Main loop: build blocks for the checkpoint
@@ -248,11 +248,28 @@ export class CheckpointProposalJob implements Traceable {
248
248
  return undefined;
249
249
  }
250
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
+
251
260
  // Assemble and broadcast the checkpoint proposal, including the last block that was not
252
261
  // broadcasted yet, and wait to collect the committee attestations.
253
262
  this.setStateFn(SequencerState.ASSEMBLING_CHECKPOINT, this.slot);
254
263
  const checkpoint = await checkpointBuilder.completeCheckpoint();
255
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
+
256
273
  // Do not collect attestations nor publish to L1 in fisherman mode
257
274
  if (this.config.fishermanMode) {
258
275
  this.log.info(
@@ -318,6 +335,21 @@ export class CheckpointProposalJob implements Traceable {
318
335
  const aztecSlotDuration = this.l1Constants.slotDuration;
319
336
  const slotStartBuildTimestamp = this.getSlotStartBuildTimestamp();
320
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
+
321
353
  await this.publisher.enqueueProposeCheckpoint(checkpoint, attestations, attestationsSignature, {
322
354
  txTimeoutAt,
323
355
  forcePendingCheckpointNumber: this.invalidateCheckpoint?.forcePendingCheckpointNumber,
@@ -711,8 +743,20 @@ export class CheckpointProposalJob implements Traceable {
711
743
 
712
744
  collectedAttestationsCount = attestations.length;
713
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
+
714
758
  // Rollup contract requires that the signatures are provided in the order of the committee
715
- const sorted = orderAttestations(attestations, committee);
759
+ const sorted = orderAttestations(trimmed, committee);
716
760
 
717
761
  // Manipulate the attestations if we've been configured to do so
718
762
  if (this.config.injectFakeAttestation || this.config.shuffleAttestationOrdering) {
@@ -826,7 +870,7 @@ export class CheckpointProposalJob implements Traceable {
826
870
  slot: this.slot,
827
871
  feeAnalysisId: feeAnalysis?.id,
828
872
  });
829
- this.metrics.recordBlockProposalFailed('block_build_failed');
873
+ this.metrics.recordCheckpointProposalFailed('block_build_failed');
830
874
  }
831
875
 
832
876
  this.publisher.clearPendingRequests();
@@ -18,7 +18,6 @@ import { type Hex, formatUnits } from 'viem';
18
18
 
19
19
  import type { SequencerState } from './utils.js';
20
20
 
21
- // TODO(palla/mbps): Review all metrics and add any missing ones per checkpoint
22
21
  export class SequencerMetrics {
23
22
  public readonly tracer: Tracer;
24
23
  private meter: Meter;
@@ -40,11 +39,16 @@ export class SequencerMetrics {
40
39
  private filledSlots: UpDownCounter;
41
40
 
42
41
  private blockProposalFailed: UpDownCounter;
43
- private blockProposalSuccess: UpDownCounter;
44
- private blockProposalPrecheckFailed: UpDownCounter;
42
+ private checkpointProposalSuccess: UpDownCounter;
43
+ private checkpointPrecheckFailed: UpDownCounter;
44
+ private checkpointProposalFailed: UpDownCounter;
45
45
  private checkpointSuccess: UpDownCounter;
46
46
  private slashingAttempts: UpDownCounter;
47
47
  private checkpointAttestationDelay: Histogram;
48
+ private checkpointBuildDuration: Histogram;
49
+ private checkpointBlockCount: Gauge;
50
+ private checkpointTxCount: Gauge;
51
+ private checkpointTotalMana: Gauge;
48
52
 
49
53
  // Fisherman fee analysis metrics
50
54
  private fishermanWouldBeIncluded: UpDownCounter;
@@ -84,7 +88,7 @@ export class SequencerMetrics {
84
88
 
85
89
  this.checkpointAttestationDelay = this.meter.createHistogram(Metrics.SEQUENCER_CHECKPOINT_ATTESTATION_DELAY);
86
90
 
87
- this.rewards = this.meter.createGauge(Metrics.SEQUENCER_CURRENT_BLOCK_REWARDS);
91
+ this.rewards = this.meter.createGauge(Metrics.SEQUENCER_CURRENT_SLOT_REWARDS);
88
92
 
89
93
  this.slots = createUpDownCounterWithDefault(this.meter, Metrics.SEQUENCER_SLOT_COUNT);
90
94
 
@@ -107,16 +111,16 @@ export class SequencerMetrics {
107
111
  Metrics.SEQUENCER_BLOCK_PROPOSAL_FAILED_COUNT,
108
112
  );
109
113
 
110
- this.blockProposalSuccess = createUpDownCounterWithDefault(
114
+ this.checkpointProposalSuccess = createUpDownCounterWithDefault(
111
115
  this.meter,
112
- Metrics.SEQUENCER_BLOCK_PROPOSAL_SUCCESS_COUNT,
116
+ Metrics.SEQUENCER_CHECKPOINT_PROPOSAL_SUCCESS_COUNT,
113
117
  );
114
118
 
115
119
  this.checkpointSuccess = createUpDownCounterWithDefault(this.meter, Metrics.SEQUENCER_CHECKPOINT_SUCCESS_COUNT);
116
120
 
117
- this.blockProposalPrecheckFailed = createUpDownCounterWithDefault(
121
+ this.checkpointPrecheckFailed = createUpDownCounterWithDefault(
118
122
  this.meter,
119
- Metrics.SEQUENCER_BLOCK_PROPOSAL_PRECHECK_FAILED_COUNT,
123
+ Metrics.SEQUENCER_CHECKPOINT_PRECHECK_FAILED_COUNT,
120
124
  {
121
125
  [Attributes.ERROR_TYPE]: [
122
126
  'slot_already_taken',
@@ -127,6 +131,16 @@ export class SequencerMetrics {
127
131
  },
128
132
  );
129
133
 
134
+ this.checkpointProposalFailed = createUpDownCounterWithDefault(
135
+ this.meter,
136
+ Metrics.SEQUENCER_CHECKPOINT_PROPOSAL_FAILED_COUNT,
137
+ );
138
+
139
+ this.checkpointBuildDuration = this.meter.createHistogram(Metrics.SEQUENCER_CHECKPOINT_BUILD_DURATION);
140
+ this.checkpointBlockCount = this.meter.createGauge(Metrics.SEQUENCER_CHECKPOINT_BLOCK_COUNT);
141
+ this.checkpointTxCount = this.meter.createGauge(Metrics.SEQUENCER_CHECKPOINT_TX_COUNT);
142
+ this.checkpointTotalMana = this.meter.createGauge(Metrics.SEQUENCER_CHECKPOINT_TOTAL_MANA);
143
+
130
144
  this.slashingAttempts = createUpDownCounterWithDefault(this.meter, Metrics.SEQUENCER_SLASHING_ATTEMPTS_COUNT);
131
145
 
132
146
  // Fisherman fee analysis metrics
@@ -262,18 +276,30 @@ export class SequencerMetrics {
262
276
  });
263
277
  }
264
278
 
265
- recordBlockProposalSuccess() {
266
- this.blockProposalSuccess.add(1);
279
+ recordCheckpointProposalSuccess() {
280
+ this.checkpointProposalSuccess.add(1);
267
281
  }
268
282
 
269
- recordBlockProposalPrecheckFailed(
283
+ recordCheckpointPrecheckFailed(
270
284
  checkType: 'slot_already_taken' | 'rollup_contract_check_failed' | 'slot_mismatch' | 'block_number_mismatch',
271
285
  ) {
272
- this.blockProposalPrecheckFailed.add(1, {
273
- [Attributes.ERROR_TYPE]: checkType,
286
+ this.checkpointPrecheckFailed.add(1, { [Attributes.ERROR_TYPE]: checkType });
287
+ }
288
+
289
+ recordCheckpointProposalFailed(reason?: string) {
290
+ this.checkpointProposalFailed.add(1, {
291
+ ...(reason && { [Attributes.ERROR_TYPE]: reason }),
274
292
  });
275
293
  }
276
294
 
295
+ /** Records aggregate metrics for a completed checkpoint build. */
296
+ recordCheckpointBuild(durationMs: number, blockCount: number, txCount: number, totalMana: number) {
297
+ this.checkpointBuildDuration.record(Math.ceil(durationMs));
298
+ this.checkpointBlockCount.record(blockCount);
299
+ this.checkpointTxCount.record(txCount);
300
+ this.checkpointTotalMana.record(totalMana);
301
+ }
302
+
277
303
  recordSlashingAttempt(actionCount: number) {
278
304
  this.slashingAttempts.add(actionCount);
279
305
  }