@aztec/sequencer-client 0.0.1-commit.ff7989d6c → 0.0.1-commit.ffe5b04ea

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 (59) hide show
  1. package/dest/client/sequencer-client.d.ts +12 -1
  2. package/dest/client/sequencer-client.d.ts.map +1 -1
  3. package/dest/client/sequencer-client.js +85 -13
  4. package/dest/config.d.ts +23 -4
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +23 -16
  7. package/dest/publisher/config.d.ts +5 -1
  8. package/dest/publisher/config.d.ts.map +1 -1
  9. package/dest/publisher/config.js +6 -1
  10. package/dest/publisher/index.d.ts +2 -1
  11. package/dest/publisher/index.d.ts.map +1 -1
  12. package/dest/publisher/l1_tx_failed_store/factory.d.ts +11 -0
  13. package/dest/publisher/l1_tx_failed_store/factory.d.ts.map +1 -0
  14. package/dest/publisher/l1_tx_failed_store/factory.js +22 -0
  15. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts +59 -0
  16. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -0
  17. package/dest/publisher/l1_tx_failed_store/failed_tx_store.js +1 -0
  18. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts +15 -0
  19. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts.map +1 -0
  20. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.js +34 -0
  21. package/dest/publisher/l1_tx_failed_store/index.d.ts +4 -0
  22. package/dest/publisher/l1_tx_failed_store/index.d.ts.map +1 -0
  23. package/dest/publisher/l1_tx_failed_store/index.js +2 -0
  24. package/dest/publisher/sequencer-publisher-factory.d.ts +1 -1
  25. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  26. package/dest/publisher/sequencer-publisher-factory.js +14 -0
  27. package/dest/publisher/sequencer-publisher.d.ts +12 -2
  28. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  29. package/dest/publisher/sequencer-publisher.js +258 -9
  30. package/dest/sequencer/checkpoint_proposal_job.d.ts +2 -4
  31. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  32. package/dest/sequencer/checkpoint_proposal_job.js +68 -37
  33. package/dest/sequencer/sequencer.d.ts +9 -6
  34. package/dest/sequencer/sequencer.d.ts.map +1 -1
  35. package/dest/sequencer/sequencer.js +1 -1
  36. package/dest/sequencer/timetable.d.ts +4 -3
  37. package/dest/sequencer/timetable.d.ts.map +1 -1
  38. package/dest/sequencer/timetable.js +6 -7
  39. package/dest/sequencer/types.d.ts +5 -2
  40. package/dest/sequencer/types.d.ts.map +1 -1
  41. package/dest/test/mock_checkpoint_builder.d.ts +4 -6
  42. package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
  43. package/dest/test/mock_checkpoint_builder.js +39 -30
  44. package/package.json +28 -28
  45. package/src/client/sequencer-client.ts +111 -12
  46. package/src/config.ts +28 -19
  47. package/src/publisher/config.ts +9 -0
  48. package/src/publisher/index.ts +3 -0
  49. package/src/publisher/l1_tx_failed_store/factory.ts +32 -0
  50. package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +55 -0
  51. package/src/publisher/l1_tx_failed_store/file_store_failed_tx_store.ts +46 -0
  52. package/src/publisher/l1_tx_failed_store/index.ts +3 -0
  53. package/src/publisher/sequencer-publisher-factory.ts +15 -0
  54. package/src/publisher/sequencer-publisher.ts +237 -15
  55. package/src/sequencer/checkpoint_proposal_job.ts +90 -44
  56. package/src/sequencer/sequencer.ts +1 -1
  57. package/src/sequencer/timetable.ts +7 -7
  58. package/src/sequencer/types.ts +4 -1
  59. package/src/test/mock_checkpoint_builder.ts +48 -45
@@ -0,0 +1,3 @@
1
+ export { type FailedL1Tx, type FailedL1TxUri, type L1TxFailedStore } from './failed_tx_store.js';
2
+ export { createL1TxFailedStore } from './factory.js';
3
+ export { FileStoreL1TxFailedStore } from './file_store_failed_tx_store.js';
@@ -81,8 +81,23 @@ export class SequencerPublisherFactory {
81
81
  const rollup = this.deps.rollupContract;
82
82
  const slashingProposerContract = await rollup.getSlashingProposer();
83
83
 
84
+ const getNextPublisher = async (excludeAddresses: EthAddress[]): Promise<L1TxUtils | undefined> => {
85
+ const exclusionFilter: PublisherFilter<L1TxUtils> = (utils: L1TxUtils) => {
86
+ if (excludeAddresses.some(addr => addr.equals(utils.getSenderAddress()))) {
87
+ return false;
88
+ }
89
+ return filter(utils);
90
+ };
91
+ try {
92
+ return await this.deps.publisherManager.getAvailablePublisher(exclusionFilter);
93
+ } catch {
94
+ return undefined;
95
+ }
96
+ };
97
+
84
98
  const publisher = new SequencerPublisher(this.sequencerConfig, {
85
99
  l1TxUtils: l1Publisher,
100
+ getNextPublisher,
86
101
  telemetry: this.deps.telemetry,
87
102
  blobClient: this.deps.blobClient,
88
103
  rollupContract: this.deps.rollupContract,
@@ -30,6 +30,7 @@ import { toHex as toPaddedHex } from '@aztec/foundation/bigint-buffer';
30
30
  import { CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
31
31
  import { pick } from '@aztec/foundation/collection';
32
32
  import type { Fr } from '@aztec/foundation/curves/bn254';
33
+ import { TimeoutError } from '@aztec/foundation/error';
33
34
  import { EthAddress } from '@aztec/foundation/eth-address';
34
35
  import { Signature, type ViemSignature } from '@aztec/foundation/eth-signature';
35
36
  import { type Logger, createLogger } from '@aztec/foundation/log';
@@ -45,9 +46,19 @@ import type { CheckpointHeader } from '@aztec/stdlib/rollup';
45
46
  import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
46
47
  import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
47
48
 
48
- import { type StateOverride, type TransactionReceipt, type TypedDataDefinition, encodeFunctionData, toHex } from 'viem';
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';
49
59
 
50
60
  import type { SequencerPublisherConfig } from './config.js';
61
+ import { type FailedL1Tx, type L1TxFailedStore, createL1TxFailedStore } from './l1_tx_failed_store/index.js';
51
62
  import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
52
63
 
53
64
  /** Arguments to the process method of the rollup contract */
@@ -109,6 +120,7 @@ export class SequencerPublisher {
109
120
  private interrupted = false;
110
121
  private metrics: SequencerPublisherMetrics;
111
122
  public epochCache: EpochCache;
123
+ private failedTxStore?: Promise<L1TxFailedStore | undefined>;
112
124
 
113
125
  protected governanceLog = createLogger('sequencer:publisher:governance');
114
126
  protected slashingLog = createLogger('sequencer:publisher:slashing');
@@ -126,6 +138,9 @@ export class SequencerPublisher {
126
138
  /** Address to use for simulations in fisherman mode (actual proposer's address) */
127
139
  private proposerAddressForSimulation?: EthAddress;
128
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
+
129
144
  /** L1 fee analyzer for fisherman mode */
130
145
  private l1FeeAnalyzer?: L1FeeAnalyzer;
131
146
 
@@ -149,7 +164,7 @@ export class SequencerPublisher {
149
164
  protected requests: RequestWithExpiry[] = [];
150
165
 
151
166
  constructor(
152
- private config: Pick<SequencerPublisherConfig, 'fishermanMode'> &
167
+ private config: Pick<SequencerPublisherConfig, 'fishermanMode' | 'l1TxFailedStore'> &
153
168
  Pick<L1ContractsConfig, 'ethereumSlotDuration'> & { l1ChainId: number },
154
169
  deps: {
155
170
  telemetry?: TelemetryClient;
@@ -164,6 +179,7 @@ export class SequencerPublisher {
164
179
  metrics: SequencerPublisherMetrics;
165
180
  lastActions: Partial<Record<Action, SlotNumber>>;
166
181
  log?: Logger;
182
+ getNextPublisher?: (excludeAddresses: EthAddress[]) => Promise<L1TxUtils | undefined>;
167
183
  },
168
184
  ) {
169
185
  this.log = deps.log ?? createLogger('sequencer:publisher');
@@ -177,6 +193,7 @@ export class SequencerPublisher {
177
193
  this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
178
194
  this.tracer = telemetry.getTracer('SequencerPublisher');
179
195
  this.l1TxUtils = deps.l1TxUtils;
196
+ this.getNextPublisher = deps.getNextPublisher;
180
197
 
181
198
  this.rollupContract = deps.rollupContract;
182
199
 
@@ -205,6 +222,31 @@ export class SequencerPublisher {
205
222
  this.rollupContract,
206
223
  createLogger('sequencer:publisher:price-oracle'),
207
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
+ });
208
250
  }
209
251
 
210
252
  public getRollupContract(): RollupContract {
@@ -386,19 +428,36 @@ export class SequencerPublisher {
386
428
  validRequests.sort((a, b) => compareActions(a.action, b.action));
387
429
 
388
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
+
389
448
  this.log.debug('Forwarding transactions', {
390
449
  validRequests: validRequests.map(request => request.action),
391
450
  txConfig,
392
451
  });
393
- const result = await Multicall3.forward(
394
- validRequests.map(request => request.request),
395
- this.l1TxUtils,
396
- txConfig,
397
- blobConfig,
398
- this.rollupContract.address,
399
- 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,
400
460
  );
401
- const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(validRequests, result);
402
461
  return { result, expiredActions, sentActions: validActions, successfulActions, failedActions };
403
462
  } catch (err) {
404
463
  const viemError = formatViemError(err);
@@ -416,13 +475,76 @@ export class SequencerPublisher {
416
475
  }
417
476
  }
418
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
+
419
527
  private callbackBundledTransactions(
420
528
  requests: RequestWithExpiry[],
421
- result?: { receipt: TransactionReceipt } | FormattedViemError,
529
+ result: { receipt: TransactionReceipt; errorMsg?: string } | FormattedViemError | undefined,
530
+ txContext: { multicallData: Hex; blobData?: Hex[]; l1BlockNumber: bigint },
422
531
  ) {
423
532
  const actionsListStr = requests.map(r => r.action).join(', ');
424
533
  if (result instanceof FormattedViemError) {
425
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
+ });
426
548
  return { failedActions: requests.map(r => r.action) };
427
549
  } else {
428
550
  this.log.verbose(`Published bundled transactions (${actionsListStr})`, { result, requests });
@@ -435,6 +557,30 @@ export class SequencerPublisher {
435
557
  failedActions.push(request.action);
436
558
  }
437
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
+ }
438
584
  return { successfulActions, failedActions };
439
585
  }
440
586
  }
@@ -546,6 +692,8 @@ export class SequencerPublisher {
546
692
  const request = this.buildInvalidateCheckpointRequest(validationResult);
547
693
  this.log.debug(`Simulating invalidate checkpoint ${checkpointNumber}`, { ...logData, request });
548
694
 
695
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
696
+
549
697
  try {
550
698
  const { gasUsed } = await this.l1TxUtils.simulate(
551
699
  request,
@@ -597,6 +745,18 @@ export class SequencerPublisher {
597
745
 
598
746
  // Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
599
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
+ });
600
760
  throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, { cause: viemError });
601
761
  }
602
762
  }
@@ -744,11 +904,26 @@ export class SequencerPublisher {
744
904
  lastValidL2Slot: slotNumber,
745
905
  });
746
906
 
907
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
908
+
747
909
  try {
748
910
  await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
749
911
  this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
750
912
  } catch (err) {
751
- 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
+ });
752
927
  // Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
753
928
  }
754
929
 
@@ -1044,6 +1219,8 @@ export class SequencerPublisher {
1044
1219
 
1045
1220
  this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
1046
1221
 
1222
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1223
+
1047
1224
  let gasUsed: bigint;
1048
1225
  const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
1049
1226
  try {
@@ -1053,6 +1230,19 @@ export class SequencerPublisher {
1053
1230
  const viemError = formatViemError(err, simulateAbi);
1054
1231
  this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
1055
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
+
1056
1246
  return false;
1057
1247
  }
1058
1248
 
@@ -1136,9 +1326,27 @@ export class SequencerPublisher {
1136
1326
  kzg,
1137
1327
  },
1138
1328
  )
1139
- .catch(err => {
1140
- const { message, metaMessages } = formatViemError(err);
1141
- 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
+ });
1142
1350
  throw new Error('Failed to validate blobs');
1143
1351
  });
1144
1352
  }
@@ -1217,6 +1425,8 @@ export class SequencerPublisher {
1217
1425
  });
1218
1426
  }
1219
1427
 
1428
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1429
+
1220
1430
  const simulationResult = await this.l1TxUtils
1221
1431
  .simulate(
1222
1432
  {
@@ -1250,6 +1460,18 @@ export class SequencerPublisher {
1250
1460
  };
1251
1461
  }
1252
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
+ });
1253
1475
  throw err;
1254
1476
  });
1255
1477
 
@@ -1,5 +1,3 @@
1
- import { NUM_CHECKPOINT_END_MARKER_FIELDS, getNumBlockEndBlobFields } from '@aztec/blob-lib/encoding';
2
- import { BLOBS_PER_CHECKPOINT, FIELDS_PER_BLOB } from '@aztec/constants';
3
1
  import type { EpochCache } from '@aztec/epoch-cache';
4
2
  import {
5
3
  BlockNumber,
@@ -9,6 +7,11 @@ import {
9
7
  SlotNumber,
10
8
  } from '@aztec/foundation/branded-types';
11
9
  import { randomInt } from '@aztec/foundation/crypto/random';
10
+ import {
11
+ flipSignature,
12
+ generateRecoverableSignature,
13
+ generateUnrecoverableSignature,
14
+ } from '@aztec/foundation/crypto/secp256k1-signer';
12
15
  import { Fr } from '@aztec/foundation/curves/bn254';
13
16
  import { EthAddress } from '@aztec/foundation/eth-address';
14
17
  import { Signature } from '@aztec/foundation/eth-signature';
@@ -27,7 +30,7 @@ import {
27
30
  type L2BlockSource,
28
31
  MaliciousCommitteeAttestationsAndSigners,
29
32
  } from '@aztec/stdlib/block';
30
- import type { Checkpoint } from '@aztec/stdlib/checkpoint';
33
+ import { type Checkpoint, validateCheckpoint } from '@aztec/stdlib/checkpoint';
31
34
  import { getSlotStartBuildTimestamp } from '@aztec/stdlib/epoch-helpers';
32
35
  import { Gas } from '@aztec/stdlib/gas';
33
36
  import {
@@ -38,7 +41,7 @@ import {
38
41
  } from '@aztec/stdlib/interfaces/server';
39
42
  import { type L1ToL2MessageSource, computeInHashFromL1ToL2Messages } from '@aztec/stdlib/messaging';
40
43
  import type { BlockProposalOptions, CheckpointProposal, CheckpointProposalOptions } from '@aztec/stdlib/p2p';
41
- import { orderAttestations } from '@aztec/stdlib/p2p';
44
+ import { orderAttestations, trimAttestations } from '@aztec/stdlib/p2p';
42
45
  import type { L2BlockBuiltStats } from '@aztec/stdlib/stats';
43
46
  import { type FailedTx, Tx } from '@aztec/stdlib/tx';
44
47
  import { AttestationTimeoutError } from '@aztec/stdlib/validators';
@@ -262,6 +265,22 @@ export class CheckpointProposalJob implements Traceable {
262
265
  this.setStateFn(SequencerState.ASSEMBLING_CHECKPOINT, this.slot);
263
266
  const checkpoint = await checkpointBuilder.completeCheckpoint();
264
267
 
268
+ // Final validation round for the checkpoint before we propose it, just for safety
269
+ try {
270
+ validateCheckpoint(checkpoint, {
271
+ rollupManaLimit: this.l1Constants.rollupManaLimit,
272
+ maxL2BlockGas: this.config.maxL2BlockGas,
273
+ maxDABlockGas: this.config.maxDABlockGas,
274
+ maxTxsPerBlock: this.config.maxTxsPerBlock,
275
+ maxTxsPerCheckpoint: this.config.maxTxsPerCheckpoint,
276
+ });
277
+ } catch (err) {
278
+ this.log.error(`Built an invalid checkpoint at slot ${this.slot} (skipping proposal)`, err, {
279
+ checkpoint: checkpoint.header.toInspect(),
280
+ });
281
+ return undefined;
282
+ }
283
+
265
284
  // Record checkpoint-level build metrics
266
285
  this.metrics.recordCheckpointBuild(
267
286
  checkpointBuildTimer.ms(),
@@ -384,9 +403,6 @@ export class CheckpointProposalJob implements Traceable {
384
403
  const txHashesAlreadyIncluded = new Set<string>();
385
404
  const initialBlockNumber = BlockNumber(this.syncedToBlockNumber + 1);
386
405
 
387
- // Remaining blob fields available for blocks (checkpoint end marker already subtracted)
388
- let remainingBlobFields = BLOBS_PER_CHECKPOINT * FIELDS_PER_BLOB - NUM_CHECKPOINT_END_MARKER_FIELDS;
389
-
390
406
  // Last block in the checkpoint will usually be flagged as pending broadcast, so we send it along with the checkpoint proposal
391
407
  let blockPendingBroadcast: { block: L2Block; txs: Tx[] } | undefined = undefined;
392
408
 
@@ -419,7 +435,6 @@ export class CheckpointProposalJob implements Traceable {
419
435
  blockNumber,
420
436
  indexWithinCheckpoint,
421
437
  txHashesAlreadyIncluded,
422
- remainingBlobFields,
423
438
  });
424
439
 
425
440
  // TODO(palla/mbps): Review these conditions. We may want to keep trying in some scenarios.
@@ -445,12 +460,9 @@ export class CheckpointProposalJob implements Traceable {
445
460
  break;
446
461
  }
447
462
 
448
- const { block, usedTxs, remainingBlobFields: newRemainingBlobFields } = buildResult;
463
+ const { block, usedTxs } = buildResult;
449
464
  blocksInCheckpoint.push(block);
450
465
 
451
- // Update remaining blob fields for the next block
452
- remainingBlobFields = newRemainingBlobFields;
453
-
454
466
  // Sync the proposed block to the archiver to make it available
455
467
  // Note that the checkpoint builder uses its own fork so it should not need to wait for this syncing
456
468
  // Eventually we should refactor the checkpoint builder to not need a separate long-lived fork
@@ -518,18 +530,10 @@ export class CheckpointProposalJob implements Traceable {
518
530
  indexWithinCheckpoint: IndexWithinCheckpoint;
519
531
  buildDeadline: Date | undefined;
520
532
  txHashesAlreadyIncluded: Set<string>;
521
- remainingBlobFields: number;
522
533
  },
523
- ): Promise<{ block: L2Block; usedTxs: Tx[]; remainingBlobFields: number } | { error: Error } | undefined> {
524
- const {
525
- blockTimestamp,
526
- forceCreate,
527
- blockNumber,
528
- indexWithinCheckpoint,
529
- buildDeadline,
530
- txHashesAlreadyIncluded,
531
- remainingBlobFields,
532
- } = opts;
534
+ ): Promise<{ block: L2Block; usedTxs: Tx[] } | { error: Error } | undefined> {
535
+ const { blockTimestamp, forceCreate, blockNumber, indexWithinCheckpoint, buildDeadline, txHashesAlreadyIncluded } =
536
+ opts;
533
537
 
534
538
  this.log.verbose(
535
539
  `Preparing block ${blockNumber} index ${indexWithinCheckpoint} at checkpoint ${this.checkpointNumber} for slot ${this.slot}`,
@@ -563,16 +567,16 @@ export class CheckpointProposalJob implements Traceable {
563
567
  );
564
568
  this.setStateFn(SequencerState.CREATING_BLOCK, this.slot);
565
569
 
566
- // Calculate blob fields limit for txs (remaining capacity - this block's end overhead)
567
- const blockEndOverhead = getNumBlockEndBlobFields(indexWithinCheckpoint === 0);
568
- const maxBlobFieldsForTxs = remainingBlobFields - blockEndOverhead;
569
-
570
+ // Per-block limits derived at startup by computeBlockLimits(), further capped
571
+ // by remaining checkpoint-level budgets inside CheckpointBuilder before each block is built.
570
572
  const blockBuilderOptions: PublicProcessorLimits = {
571
573
  maxTransactions: this.config.maxTxsPerBlock,
572
- maxBlockSize: this.config.maxBlockSizeInBytes,
573
- maxBlockGas: new Gas(this.config.maxDABlockGas, this.config.maxL2BlockGas),
574
- maxBlobFields: maxBlobFieldsForTxs,
574
+ maxBlockGas:
575
+ this.config.maxL2BlockGas !== undefined || this.config.maxDABlockGas !== undefined
576
+ ? new Gas(this.config.maxDABlockGas ?? Infinity, this.config.maxL2BlockGas ?? Infinity)
577
+ : undefined,
575
578
  deadline: buildDeadline,
579
+ isBuildingProposal: true,
576
580
  };
577
581
 
578
582
  // Actually build the block by executing txs
@@ -602,7 +606,7 @@ export class CheckpointProposalJob implements Traceable {
602
606
  }
603
607
 
604
608
  // Block creation succeeded, emit stats and metrics
605
- const { publicGas, block, publicProcessorDuration, usedTxs, usedTxBlobFields, blockBuildDuration } = buildResult;
609
+ const { block, publicProcessorDuration, usedTxs, blockBuildDuration } = buildResult;
606
610
 
607
611
  const blockStats = {
608
612
  eventName: 'l2-block-built',
@@ -613,7 +617,7 @@ export class CheckpointProposalJob implements Traceable {
613
617
 
614
618
  const blockHash = await block.hash();
615
619
  const txHashes = block.body.txEffects.map(tx => tx.txHash);
616
- const manaPerSec = publicGas.l2Gas / (blockBuildDuration / 1000);
620
+ const manaPerSec = block.header.totalManaUsed.toNumberUnsafe() / (blockBuildDuration / 1000);
617
621
 
618
622
  this.log.info(
619
623
  `Built block ${block.number} at checkpoint ${this.checkpointNumber} for slot ${this.slot} with ${numTxs} txs`,
@@ -621,9 +625,9 @@ export class CheckpointProposalJob implements Traceable {
621
625
  );
622
626
 
623
627
  this.eventEmitter.emit('block-proposed', { blockNumber: block.number, slot: this.slot });
624
- this.metrics.recordBuiltBlock(blockBuildDuration, publicGas.l2Gas);
628
+ this.metrics.recordBuiltBlock(blockBuildDuration, block.header.totalManaUsed.toNumberUnsafe());
625
629
 
626
- return { block, usedTxs, remainingBlobFields: maxBlobFieldsForTxs - usedTxBlobFields };
630
+ return { block, usedTxs };
627
631
  } catch (err: any) {
628
632
  this.eventEmitter.emit('block-build-failed', { reason: err.message, slot: this.slot });
629
633
  this.log.error(`Error building block`, err, { blockNumber, slot: this.slot });
@@ -743,11 +747,28 @@ export class CheckpointProposalJob implements Traceable {
743
747
 
744
748
  collectedAttestationsCount = attestations.length;
745
749
 
750
+ // Trim attestations to minimum required to save L1 calldata gas
751
+ const localAddresses = this.validatorClient.getValidatorAddresses();
752
+ const trimmed = trimAttestations(
753
+ attestations,
754
+ numberOfRequiredAttestations,
755
+ this.attestorAddress,
756
+ localAddresses,
757
+ );
758
+ if (trimmed.length < attestations.length) {
759
+ this.log.debug(`Trimmed attestations from ${attestations.length} to ${trimmed.length} for L1 submission`);
760
+ }
761
+
746
762
  // Rollup contract requires that the signatures are provided in the order of the committee
747
- const sorted = orderAttestations(attestations, committee);
763
+ const sorted = orderAttestations(trimmed, committee);
748
764
 
749
765
  // Manipulate the attestations if we've been configured to do so
750
- if (this.config.injectFakeAttestation || this.config.shuffleAttestationOrdering) {
766
+ if (
767
+ this.config.injectFakeAttestation ||
768
+ this.config.injectHighSValueAttestation ||
769
+ this.config.injectUnrecoverableSignatureAttestation ||
770
+ this.config.shuffleAttestationOrdering
771
+ ) {
751
772
  return this.manipulateAttestations(proposal.slotNumber, epoch, seed, committee, sorted);
752
773
  }
753
774
 
@@ -776,7 +797,11 @@ export class CheckpointProposalJob implements Traceable {
776
797
  this.epochCache.computeProposerIndex(slotNumber, epoch, seed, BigInt(committee.length)),
777
798
  );
778
799
 
779
- if (this.config.injectFakeAttestation) {
800
+ if (
801
+ this.config.injectFakeAttestation ||
802
+ this.config.injectHighSValueAttestation ||
803
+ this.config.injectUnrecoverableSignatureAttestation
804
+ ) {
780
805
  // Find non-empty attestations that are not from the proposer
781
806
  const nonProposerIndices: number[] = [];
782
807
  for (let i = 0; i < attestations.length; i++) {
@@ -786,8 +811,20 @@ export class CheckpointProposalJob implements Traceable {
786
811
  }
787
812
  if (nonProposerIndices.length > 0) {
788
813
  const targetIndex = nonProposerIndices[randomInt(nonProposerIndices.length)];
789
- this.log.warn(`Injecting fake attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`);
790
- unfreeze(attestations[targetIndex]).signature = Signature.random();
814
+ if (this.config.injectHighSValueAttestation) {
815
+ this.log.warn(
816
+ `Injecting high-s value attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`,
817
+ );
818
+ unfreeze(attestations[targetIndex]).signature = flipSignature(attestations[targetIndex].signature);
819
+ } else if (this.config.injectUnrecoverableSignatureAttestation) {
820
+ this.log.warn(
821
+ `Injecting unrecoverable signature attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`,
822
+ );
823
+ unfreeze(attestations[targetIndex]).signature = generateUnrecoverableSignature();
824
+ } else {
825
+ this.log.warn(`Injecting fake attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`);
826
+ unfreeze(attestations[targetIndex]).signature = generateRecoverableSignature();
827
+ }
791
828
  }
792
829
  return new CommitteeAttestationsAndSigners(attestations);
793
830
  }
@@ -796,11 +833,20 @@ export class CheckpointProposalJob implements Traceable {
796
833
  this.log.warn(`Shuffling attestation ordering in checkpoint for slot ${slotNumber} (proposer #${proposerIndex})`);
797
834
 
798
835
  const shuffled = [...attestations];
799
- const [i, j] = [(proposerIndex + 1) % shuffled.length, (proposerIndex + 2) % shuffled.length];
800
- const valueI = shuffled[i];
801
- const valueJ = shuffled[j];
802
- shuffled[i] = valueJ;
803
- shuffled[j] = valueI;
836
+
837
+ // Find two non-proposer positions that both have non-empty signatures to swap.
838
+ // This ensures the bitmap doesn't change, so the MaliciousCommitteeAttestationsAndSigners
839
+ // signers array stays correctly aligned with L1's committee reconstruction.
840
+ const swappable: number[] = [];
841
+ for (let k = 0; k < shuffled.length; k++) {
842
+ if (!shuffled[k].signature.isEmpty() && k !== proposerIndex) {
843
+ swappable.push(k);
844
+ }
845
+ }
846
+ if (swappable.length >= 2) {
847
+ const [i, j] = [swappable[0], swappable[1]];
848
+ [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
849
+ }
804
850
 
805
851
  const signers = new CommitteeAttestationsAndSigners(attestations).getSigners();
806
852
  return new MaliciousCommitteeAttestationsAndSigners(shuffled, signers);