@aztec/sequencer-client 0.0.1-commit.f504929 → 0.0.1-commit.f81dbcf

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 +22 -3
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +14 -12
  7. package/dest/publisher/config.d.ts +1 -5
  8. package/dest/publisher/config.d.ts.map +1 -1
  9. package/dest/publisher/config.js +1 -6
  10. package/dest/publisher/index.d.ts +1 -2
  11. package/dest/publisher/index.d.ts.map +1 -1
  12. package/dest/publisher/sequencer-publisher-factory.d.ts +1 -1
  13. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  14. package/dest/publisher/sequencer-publisher-factory.js +0 -14
  15. package/dest/publisher/sequencer-publisher.d.ts +2 -12
  16. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  17. package/dest/publisher/sequencer-publisher.js +9 -258
  18. package/dest/sequencer/checkpoint_proposal_job.d.ts +2 -4
  19. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  20. package/dest/sequencer/checkpoint_proposal_job.js +55 -58
  21. package/dest/sequencer/sequencer.d.ts +7 -6
  22. package/dest/sequencer/sequencer.d.ts.map +1 -1
  23. package/dest/sequencer/sequencer.js +11 -13
  24. package/dest/sequencer/timetable.d.ts +4 -3
  25. package/dest/sequencer/timetable.d.ts.map +1 -1
  26. package/dest/sequencer/timetable.js +6 -7
  27. package/dest/sequencer/types.d.ts +2 -2
  28. package/dest/sequencer/types.d.ts.map +1 -1
  29. package/dest/test/mock_checkpoint_builder.d.ts +10 -8
  30. package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
  31. package/dest/test/mock_checkpoint_builder.js +41 -30
  32. package/package.json +28 -28
  33. package/src/client/sequencer-client.ts +111 -12
  34. package/src/config.ts +17 -14
  35. package/src/publisher/config.ts +0 -9
  36. package/src/publisher/index.ts +0 -3
  37. package/src/publisher/sequencer-publisher-factory.ts +0 -15
  38. package/src/publisher/sequencer-publisher.ts +15 -237
  39. package/src/sequencer/checkpoint_proposal_job.ts +65 -65
  40. package/src/sequencer/sequencer.ts +12 -14
  41. package/src/sequencer/timetable.ts +7 -7
  42. package/src/sequencer/types.ts +1 -1
  43. package/src/test/mock_checkpoint_builder.ts +52 -47
  44. package/dest/publisher/l1_tx_failed_store/factory.d.ts +0 -11
  45. package/dest/publisher/l1_tx_failed_store/factory.d.ts.map +0 -1
  46. package/dest/publisher/l1_tx_failed_store/factory.js +0 -22
  47. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts +0 -59
  48. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +0 -1
  49. package/dest/publisher/l1_tx_failed_store/failed_tx_store.js +0 -1
  50. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts +0 -15
  51. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts.map +0 -1
  52. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.js +0 -34
  53. package/dest/publisher/l1_tx_failed_store/index.d.ts +0 -4
  54. package/dest/publisher/l1_tx_failed_store/index.d.ts.map +0 -1
  55. package/dest/publisher/l1_tx_failed_store/index.js +0 -2
  56. package/src/publisher/l1_tx_failed_store/factory.ts +0 -32
  57. package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +0 -55
  58. package/src/publisher/l1_tx_failed_store/file_store_failed_tx_store.ts +0 -46
  59. package/src/publisher/l1_tx_failed_store/index.ts +0 -3
@@ -30,7 +30,6 @@ 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';
34
33
  import { EthAddress } from '@aztec/foundation/eth-address';
35
34
  import { Signature, type ViemSignature } from '@aztec/foundation/eth-signature';
36
35
  import { type Logger, createLogger } from '@aztec/foundation/log';
@@ -46,19 +45,9 @@ import type { CheckpointHeader } from '@aztec/stdlib/rollup';
46
45
  import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
47
46
  import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
48
47
 
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';
48
+ import { type StateOverride, type TransactionReceipt, type TypedDataDefinition, encodeFunctionData, toHex } from 'viem';
59
49
 
60
50
  import type { SequencerPublisherConfig } from './config.js';
61
- import { type FailedL1Tx, type L1TxFailedStore, createL1TxFailedStore } from './l1_tx_failed_store/index.js';
62
51
  import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
63
52
 
64
53
  /** Arguments to the process method of the rollup contract */
@@ -120,7 +109,6 @@ export class SequencerPublisher {
120
109
  private interrupted = false;
121
110
  private metrics: SequencerPublisherMetrics;
122
111
  public epochCache: EpochCache;
123
- private failedTxStore?: Promise<L1TxFailedStore | undefined>;
124
112
 
125
113
  protected governanceLog = createLogger('sequencer:publisher:governance');
126
114
  protected slashingLog = createLogger('sequencer:publisher:slashing');
@@ -138,9 +126,6 @@ export class SequencerPublisher {
138
126
  /** Address to use for simulations in fisherman mode (actual proposer's address) */
139
127
  private proposerAddressForSimulation?: EthAddress;
140
128
 
141
- /** Optional callback to obtain a replacement publisher when the current one fails to send. */
142
- private getNextPublisher?: (excludeAddresses: EthAddress[]) => Promise<L1TxUtils | undefined>;
143
-
144
129
  /** L1 fee analyzer for fisherman mode */
145
130
  private l1FeeAnalyzer?: L1FeeAnalyzer;
146
131
 
@@ -164,7 +149,7 @@ export class SequencerPublisher {
164
149
  protected requests: RequestWithExpiry[] = [];
165
150
 
166
151
  constructor(
167
- private config: Pick<SequencerPublisherConfig, 'fishermanMode' | 'l1TxFailedStore'> &
152
+ private config: Pick<SequencerPublisherConfig, 'fishermanMode'> &
168
153
  Pick<L1ContractsConfig, 'ethereumSlotDuration'> & { l1ChainId: number },
169
154
  deps: {
170
155
  telemetry?: TelemetryClient;
@@ -179,7 +164,6 @@ export class SequencerPublisher {
179
164
  metrics: SequencerPublisherMetrics;
180
165
  lastActions: Partial<Record<Action, SlotNumber>>;
181
166
  log?: Logger;
182
- getNextPublisher?: (excludeAddresses: EthAddress[]) => Promise<L1TxUtils | undefined>;
183
167
  },
184
168
  ) {
185
169
  this.log = deps.log ?? createLogger('sequencer:publisher');
@@ -193,7 +177,6 @@ export class SequencerPublisher {
193
177
  this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
194
178
  this.tracer = telemetry.getTracer('SequencerPublisher');
195
179
  this.l1TxUtils = deps.l1TxUtils;
196
- this.getNextPublisher = deps.getNextPublisher;
197
180
 
198
181
  this.rollupContract = deps.rollupContract;
199
182
 
@@ -222,31 +205,6 @@ export class SequencerPublisher {
222
205
  this.rollupContract,
223
206
  createLogger('sequencer:publisher:price-oracle'),
224
207
  );
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
- });
250
208
  }
251
209
 
252
210
  public getRollupContract(): RollupContract {
@@ -428,36 +386,19 @@ export class SequencerPublisher {
428
386
  validRequests.sort((a, b) => compareActions(a.action, b.action));
429
387
 
430
388
  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
-
448
389
  this.log.debug('Forwarding transactions', {
449
390
  validRequests: validRequests.map(request => request.action),
450
391
  txConfig,
451
392
  });
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,
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,
460
400
  );
401
+ const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(validRequests, result);
461
402
  return { result, expiredActions, sentActions: validActions, successfulActions, failedActions };
462
403
  } catch (err) {
463
404
  const viemError = formatViemError(err);
@@ -475,76 +416,13 @@ export class SequencerPublisher {
475
416
  }
476
417
  }
477
418
 
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
-
527
419
  private callbackBundledTransactions(
528
420
  requests: RequestWithExpiry[],
529
- result: { receipt: TransactionReceipt; errorMsg?: string } | FormattedViemError | undefined,
530
- txContext: { multicallData: Hex; blobData?: Hex[]; l1BlockNumber: bigint },
421
+ result?: { receipt: TransactionReceipt } | FormattedViemError,
531
422
  ) {
532
423
  const actionsListStr = requests.map(r => r.action).join(', ');
533
424
  if (result instanceof FormattedViemError) {
534
425
  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
- });
548
426
  return { failedActions: requests.map(r => r.action) };
549
427
  } else {
550
428
  this.log.verbose(`Published bundled transactions (${actionsListStr})`, { result, requests });
@@ -557,30 +435,6 @@ export class SequencerPublisher {
557
435
  failedActions.push(request.action);
558
436
  }
559
437
  }
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
- }
584
438
  return { successfulActions, failedActions };
585
439
  }
586
440
  }
@@ -692,8 +546,6 @@ export class SequencerPublisher {
692
546
  const request = this.buildInvalidateCheckpointRequest(validationResult);
693
547
  this.log.debug(`Simulating invalidate checkpoint ${checkpointNumber}`, { ...logData, request });
694
548
 
695
- const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
696
-
697
549
  try {
698
550
  const { gasUsed } = await this.l1TxUtils.simulate(
699
551
  request,
@@ -745,18 +597,6 @@ export class SequencerPublisher {
745
597
 
746
598
  // Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
747
599
  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
- });
760
600
  throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, { cause: viemError });
761
601
  }
762
602
  }
@@ -904,26 +744,11 @@ export class SequencerPublisher {
904
744
  lastValidL2Slot: slotNumber,
905
745
  });
906
746
 
907
- const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
908
-
909
747
  try {
910
748
  await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
911
749
  this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
912
750
  } catch (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
- });
751
+ this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, err);
927
752
  // Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
928
753
  }
929
754
 
@@ -1219,8 +1044,6 @@ export class SequencerPublisher {
1219
1044
 
1220
1045
  this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
1221
1046
 
1222
- const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1223
-
1224
1047
  let gasUsed: bigint;
1225
1048
  const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
1226
1049
  try {
@@ -1230,19 +1053,6 @@ export class SequencerPublisher {
1230
1053
  const viemError = formatViemError(err, simulateAbi);
1231
1054
  this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
1232
1055
 
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
-
1246
1056
  return false;
1247
1057
  }
1248
1058
 
@@ -1326,27 +1136,9 @@ export class SequencerPublisher {
1326
1136
  kzg,
1327
1137
  },
1328
1138
  )
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
- });
1139
+ .catch(err => {
1140
+ const { message, metaMessages } = formatViemError(err);
1141
+ this.log.error(`Failed to validate blobs`, message, { metaMessages });
1350
1142
  throw new Error('Failed to validate blobs');
1351
1143
  });
1352
1144
  }
@@ -1425,8 +1217,6 @@ export class SequencerPublisher {
1425
1217
  });
1426
1218
  }
1427
1219
 
1428
- const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1429
-
1430
1220
  const simulationResult = await this.l1TxUtils
1431
1221
  .simulate(
1432
1222
  {
@@ -1460,18 +1250,6 @@ export class SequencerPublisher {
1460
1250
  };
1461
1251
  }
1462
1252
  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
- });
1475
1253
  throw err;
1476
1254
  });
1477
1255
 
@@ -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,
@@ -32,11 +30,11 @@ import {
32
30
  type L2BlockSource,
33
31
  MaliciousCommitteeAttestationsAndSigners,
34
32
  } from '@aztec/stdlib/block';
35
- import type { Checkpoint } from '@aztec/stdlib/checkpoint';
33
+ import { type Checkpoint, validateCheckpoint } from '@aztec/stdlib/checkpoint';
36
34
  import { getSlotStartBuildTimestamp } from '@aztec/stdlib/epoch-helpers';
37
35
  import { Gas } from '@aztec/stdlib/gas';
38
36
  import {
39
- NoValidTxsError,
37
+ InsufficientValidTxsError,
40
38
  type PublicProcessorLimits,
41
39
  type ResolvedSequencerConfig,
42
40
  type WorldStateSynchronizer,
@@ -267,6 +265,22 @@ export class CheckpointProposalJob implements Traceable {
267
265
  this.setStateFn(SequencerState.ASSEMBLING_CHECKPOINT, this.slot);
268
266
  const checkpoint = await checkpointBuilder.completeCheckpoint();
269
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
+
270
284
  // Record checkpoint-level build metrics
271
285
  this.metrics.recordCheckpointBuild(
272
286
  checkpointBuildTimer.ms(),
@@ -389,9 +403,6 @@ export class CheckpointProposalJob implements Traceable {
389
403
  const txHashesAlreadyIncluded = new Set<string>();
390
404
  const initialBlockNumber = BlockNumber(this.syncedToBlockNumber + 1);
391
405
 
392
- // Remaining blob fields available for blocks (checkpoint end marker already subtracted)
393
- let remainingBlobFields = BLOBS_PER_CHECKPOINT * FIELDS_PER_BLOB - NUM_CHECKPOINT_END_MARKER_FIELDS;
394
-
395
406
  // Last block in the checkpoint will usually be flagged as pending broadcast, so we send it along with the checkpoint proposal
396
407
  let blockPendingBroadcast: { block: L2Block; txs: Tx[] } | undefined = undefined;
397
408
 
@@ -424,7 +435,6 @@ export class CheckpointProposalJob implements Traceable {
424
435
  blockNumber,
425
436
  indexWithinCheckpoint,
426
437
  txHashesAlreadyIncluded,
427
- remainingBlobFields,
428
438
  });
429
439
 
430
440
  // TODO(palla/mbps): Review these conditions. We may want to keep trying in some scenarios.
@@ -450,12 +460,9 @@ export class CheckpointProposalJob implements Traceable {
450
460
  break;
451
461
  }
452
462
 
453
- const { block, usedTxs, remainingBlobFields: newRemainingBlobFields } = buildResult;
463
+ const { block, usedTxs } = buildResult;
454
464
  blocksInCheckpoint.push(block);
455
465
 
456
- // Update remaining blob fields for the next block
457
- remainingBlobFields = newRemainingBlobFields;
458
-
459
466
  // Sync the proposed block to the archiver to make it available
460
467
  // Note that the checkpoint builder uses its own fork so it should not need to wait for this syncing
461
468
  // Eventually we should refactor the checkpoint builder to not need a separate long-lived fork
@@ -523,18 +530,10 @@ export class CheckpointProposalJob implements Traceable {
523
530
  indexWithinCheckpoint: IndexWithinCheckpoint;
524
531
  buildDeadline: Date | undefined;
525
532
  txHashesAlreadyIncluded: Set<string>;
526
- remainingBlobFields: number;
527
533
  },
528
- ): Promise<{ block: L2Block; usedTxs: Tx[]; remainingBlobFields: number } | { error: Error } | undefined> {
529
- const {
530
- blockTimestamp,
531
- forceCreate,
532
- blockNumber,
533
- indexWithinCheckpoint,
534
- buildDeadline,
535
- txHashesAlreadyIncluded,
536
- remainingBlobFields,
537
- } = opts;
534
+ ): Promise<{ block: L2Block; usedTxs: Tx[] } | { error: Error } | undefined> {
535
+ const { blockTimestamp, forceCreate, blockNumber, indexWithinCheckpoint, buildDeadline, txHashesAlreadyIncluded } =
536
+ opts;
538
537
 
539
538
  this.log.verbose(
540
539
  `Preparing block ${blockNumber} index ${indexWithinCheckpoint} at checkpoint ${this.checkpointNumber} for slot ${this.slot}`,
@@ -543,8 +542,7 @@ export class CheckpointProposalJob implements Traceable {
543
542
 
544
543
  try {
545
544
  // Wait until we have enough txs to build the block
546
- const minTxs = this.config.minTxsPerBlock;
547
- const { availableTxs, canStartBuilding } = await this.waitForMinTxs(opts);
545
+ const { availableTxs, canStartBuilding, minTxs } = await this.waitForMinTxs(opts);
548
546
  if (!canStartBuilding) {
549
547
  this.log.warn(
550
548
  `Not enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.slot} (got ${availableTxs} txs but needs ${minTxs})`,
@@ -568,19 +566,24 @@ export class CheckpointProposalJob implements Traceable {
568
566
  );
569
567
  this.setStateFn(SequencerState.CREATING_BLOCK, this.slot);
570
568
 
571
- // Calculate blob fields limit for txs (remaining capacity - this block's end overhead)
572
- const blockEndOverhead = getNumBlockEndBlobFields(indexWithinCheckpoint === 0);
573
- const maxBlobFieldsForTxs = remainingBlobFields - blockEndOverhead;
574
-
575
- const blockBuilderOptions: PublicProcessorLimits = {
569
+ // Per-block limits derived at startup by computeBlockLimits(), further capped
570
+ // by remaining checkpoint-level budgets inside CheckpointBuilder before each block is built.
571
+ // minValidTxs is passed into the builder so it can reject the block *before* updating state.
572
+ const minValidTxs = forceCreate ? 0 : (this.config.minValidTxsPerBlock ?? minTxs);
573
+ const blockBuilderOptions: PublicProcessorLimits & { minValidTxs?: number } = {
576
574
  maxTransactions: this.config.maxTxsPerBlock,
577
- maxBlockSize: this.config.maxBlockSizeInBytes,
578
- maxBlockGas: new Gas(this.config.maxDABlockGas, this.config.maxL2BlockGas),
579
- maxBlobFields: maxBlobFieldsForTxs,
575
+ maxBlockGas:
576
+ this.config.maxL2BlockGas !== undefined || this.config.maxDABlockGas !== undefined
577
+ ? new Gas(this.config.maxDABlockGas ?? Infinity, this.config.maxL2BlockGas ?? Infinity)
578
+ : undefined,
580
579
  deadline: buildDeadline,
580
+ isBuildingProposal: true,
581
+ minValidTxs,
581
582
  };
582
583
 
583
- // Actually build the block by executing txs
584
+ // Actually build the block by executing txs. The builder throws InsufficientValidTxsError
585
+ // if the number of successfully processed txs is below minValidTxs, ensuring state is not
586
+ // updated for blocks that will be discarded.
584
587
  const buildResult = await this.buildSingleBlockWithCheckpointBuilder(
585
588
  checkpointBuilder,
586
589
  pendingTxs,
@@ -592,14 +595,16 @@ export class CheckpointProposalJob implements Traceable {
592
595
  // If any txs failed during execution, drop them from the mempool so we don't pick them up again
593
596
  await this.dropFailedTxsFromP2P(buildResult.failedTxs);
594
597
 
595
- // Check if we have created a block with enough txs. If there were invalid txs in the pool, or if execution took
596
- // too long, then we may not get to minTxsPerBlock after executing public functions.
597
- const minValidTxs = this.config.minValidTxsPerBlock ?? minTxs;
598
- const numTxs = buildResult.status === 'no-valid-txs' ? 0 : buildResult.numTxs;
599
- if (buildResult.status === 'no-valid-txs' || (!forceCreate && numTxs < minValidTxs)) {
598
+ if (buildResult.status === 'insufficient-valid-txs') {
600
599
  this.log.warn(
601
600
  `Block ${blockNumber} at index ${indexWithinCheckpoint} on slot ${this.slot} has too few valid txs to be proposed`,
602
- { slot: this.slot, blockNumber, numTxs, indexWithinCheckpoint, minValidTxs, buildResult: buildResult.status },
601
+ {
602
+ slot: this.slot,
603
+ blockNumber,
604
+ numTxs: buildResult.processedCount,
605
+ indexWithinCheckpoint,
606
+ minValidTxs,
607
+ },
603
608
  );
604
609
  this.eventEmitter.emit('block-build-failed', { reason: `Insufficient valid txs`, slot: this.slot });
605
610
  this.metrics.recordBlockProposalFailed('insufficient_valid_txs');
@@ -607,7 +612,7 @@ export class CheckpointProposalJob implements Traceable {
607
612
  }
608
613
 
609
614
  // Block creation succeeded, emit stats and metrics
610
- const { publicGas, block, publicProcessorDuration, usedTxs, usedTxBlobFields, blockBuildDuration } = buildResult;
615
+ const { block, publicProcessorDuration, usedTxs, blockBuildDuration, numTxs } = buildResult;
611
616
 
612
617
  const blockStats = {
613
618
  eventName: 'l2-block-built',
@@ -618,7 +623,7 @@ export class CheckpointProposalJob implements Traceable {
618
623
 
619
624
  const blockHash = await block.hash();
620
625
  const txHashes = block.body.txEffects.map(tx => tx.txHash);
621
- const manaPerSec = publicGas.l2Gas / (blockBuildDuration / 1000);
626
+ const manaPerSec = block.header.totalManaUsed.toNumberUnsafe() / (blockBuildDuration / 1000);
622
627
 
623
628
  this.log.info(
624
629
  `Built block ${block.number} at checkpoint ${this.checkpointNumber} for slot ${this.slot} with ${numTxs} txs`,
@@ -626,9 +631,9 @@ export class CheckpointProposalJob implements Traceable {
626
631
  );
627
632
 
628
633
  this.eventEmitter.emit('block-proposed', { blockNumber: block.number, slot: this.slot });
629
- this.metrics.recordBuiltBlock(blockBuildDuration, publicGas.l2Gas);
634
+ this.metrics.recordBuiltBlock(blockBuildDuration, block.header.totalManaUsed.toNumberUnsafe());
630
635
 
631
- return { block, usedTxs, remainingBlobFields: maxBlobFieldsForTxs - usedTxBlobFields };
636
+ return { block, usedTxs };
632
637
  } catch (err: any) {
633
638
  this.eventEmitter.emit('block-build-failed', { reason: err.message, slot: this.slot });
634
639
  this.log.error(`Error building block`, err, { blockNumber, slot: this.slot });
@@ -638,13 +643,13 @@ export class CheckpointProposalJob implements Traceable {
638
643
  }
639
644
  }
640
645
 
641
- /** Uses the checkpoint builder to build a block, catching specific txs */
646
+ /** Uses the checkpoint builder to build a block, catching InsufficientValidTxsError. */
642
647
  private async buildSingleBlockWithCheckpointBuilder(
643
648
  checkpointBuilder: CheckpointBuilder,
644
649
  pendingTxs: AsyncIterable<Tx>,
645
650
  blockNumber: BlockNumber,
646
651
  blockTimestamp: bigint,
647
- blockBuilderOptions: PublicProcessorLimits,
652
+ blockBuilderOptions: PublicProcessorLimits & { minValidTxs?: number },
648
653
  ) {
649
654
  try {
650
655
  const workTimer = new Timer();
@@ -652,8 +657,12 @@ export class CheckpointProposalJob implements Traceable {
652
657
  const blockBuildDuration = workTimer.ms();
653
658
  return { ...result, blockBuildDuration, status: 'success' as const };
654
659
  } catch (err: unknown) {
655
- if (isErrorClass(err, NoValidTxsError)) {
656
- return { failedTxs: err.failedTxs, status: 'no-valid-txs' as const };
660
+ if (isErrorClass(err, InsufficientValidTxsError)) {
661
+ return {
662
+ failedTxs: err.failedTxs,
663
+ processedCount: err.processedCount,
664
+ status: 'insufficient-valid-txs' as const,
665
+ };
657
666
  }
658
667
  throw err;
659
668
  }
@@ -666,7 +675,7 @@ export class CheckpointProposalJob implements Traceable {
666
675
  blockNumber: BlockNumber;
667
676
  indexWithinCheckpoint: IndexWithinCheckpoint;
668
677
  buildDeadline: Date | undefined;
669
- }): Promise<{ canStartBuilding: boolean; availableTxs: number }> {
678
+ }): Promise<{ canStartBuilding: boolean; availableTxs: number; minTxs: number }> {
670
679
  const { indexWithinCheckpoint, blockNumber, buildDeadline, forceCreate } = opts;
671
680
 
672
681
  // We only allow a block with 0 txs in the first block of the checkpoint
@@ -683,7 +692,7 @@ export class CheckpointProposalJob implements Traceable {
683
692
  // If we're past deadline, or we have no deadline, give up
684
693
  const now = this.dateProvider.nowAsDate();
685
694
  if (startBuildingDeadline === undefined || now >= startBuildingDeadline) {
686
- return { canStartBuilding: false, availableTxs: availableTxs };
695
+ return { canStartBuilding: false, availableTxs, minTxs };
687
696
  }
688
697
 
689
698
  // Wait a bit before checking again
@@ -696,7 +705,7 @@ export class CheckpointProposalJob implements Traceable {
696
705
  availableTxs = await this.p2pClient.getPendingTxCount();
697
706
  }
698
707
 
699
- return { canStartBuilding: true, availableTxs };
708
+ return { canStartBuilding: true, availableTxs, minTxs };
700
709
  }
701
710
 
702
711
  /**
@@ -834,20 +843,11 @@ export class CheckpointProposalJob implements Traceable {
834
843
  this.log.warn(`Shuffling attestation ordering in checkpoint for slot ${slotNumber} (proposer #${proposerIndex})`);
835
844
 
836
845
  const shuffled = [...attestations];
837
-
838
- // Find two non-proposer positions that both have non-empty signatures to swap.
839
- // This ensures the bitmap doesn't change, so the MaliciousCommitteeAttestationsAndSigners
840
- // signers array stays correctly aligned with L1's committee reconstruction.
841
- const swappable: number[] = [];
842
- for (let k = 0; k < shuffled.length; k++) {
843
- if (!shuffled[k].signature.isEmpty() && k !== proposerIndex) {
844
- swappable.push(k);
845
- }
846
- }
847
- if (swappable.length >= 2) {
848
- const [i, j] = [swappable[0], swappable[1]];
849
- [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
850
- }
846
+ const [i, j] = [(proposerIndex + 1) % shuffled.length, (proposerIndex + 2) % shuffled.length];
847
+ const valueI = shuffled[i];
848
+ const valueJ = shuffled[j];
849
+ shuffled[i] = valueJ;
850
+ shuffled[j] = valueI;
851
851
 
852
852
  const signers = new CommitteeAttestationsAndSigners(attestations).getSigners();
853
853
  return new MaliciousCommitteeAttestationsAndSigners(shuffled, signers);