@aztec/sequencer-client 0.0.1-commit.a072138 → 0.0.1-commit.a89ec08

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 (62) hide show
  1. package/dest/client/sequencer-client.d.ts +16 -7
  2. package/dest/client/sequencer-client.d.ts.map +1 -1
  3. package/dest/client/sequencer-client.js +56 -23
  4. package/dest/config.d.ts +25 -6
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +46 -28
  7. package/dest/global_variable_builder/global_builder.d.ts +14 -10
  8. package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
  9. package/dest/global_variable_builder/global_builder.js +22 -21
  10. package/dest/global_variable_builder/index.d.ts +2 -2
  11. package/dest/global_variable_builder/index.d.ts.map +1 -1
  12. package/dest/publisher/config.d.ts +31 -17
  13. package/dest/publisher/config.d.ts.map +1 -1
  14. package/dest/publisher/config.js +101 -42
  15. package/dest/publisher/sequencer-publisher-factory.d.ts +11 -3
  16. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  17. package/dest/publisher/sequencer-publisher-factory.js +13 -2
  18. package/dest/publisher/sequencer-publisher.d.ts +29 -13
  19. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  20. package/dest/publisher/sequencer-publisher.js +106 -57
  21. package/dest/sequencer/checkpoint_proposal_job.d.ts +10 -4
  22. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  23. package/dest/sequencer/checkpoint_proposal_job.js +162 -107
  24. package/dest/sequencer/checkpoint_voter.d.ts +1 -2
  25. package/dest/sequencer/checkpoint_voter.d.ts.map +1 -1
  26. package/dest/sequencer/checkpoint_voter.js +2 -5
  27. package/dest/sequencer/metrics.d.ts +17 -5
  28. package/dest/sequencer/metrics.d.ts.map +1 -1
  29. package/dest/sequencer/metrics.js +86 -15
  30. package/dest/sequencer/sequencer.d.ts +29 -13
  31. package/dest/sequencer/sequencer.d.ts.map +1 -1
  32. package/dest/sequencer/sequencer.js +47 -43
  33. package/dest/sequencer/timetable.d.ts +4 -6
  34. package/dest/sequencer/timetable.d.ts.map +1 -1
  35. package/dest/sequencer/timetable.js +7 -11
  36. package/dest/sequencer/types.d.ts +2 -2
  37. package/dest/sequencer/types.d.ts.map +1 -1
  38. package/dest/test/index.d.ts +3 -5
  39. package/dest/test/index.d.ts.map +1 -1
  40. package/dest/test/mock_checkpoint_builder.d.ts +11 -11
  41. package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
  42. package/dest/test/mock_checkpoint_builder.js +47 -34
  43. package/dest/test/utils.d.ts +3 -3
  44. package/dest/test/utils.d.ts.map +1 -1
  45. package/dest/test/utils.js +5 -4
  46. package/package.json +28 -28
  47. package/src/client/sequencer-client.ts +78 -21
  48. package/src/config.ts +61 -38
  49. package/src/global_variable_builder/global_builder.ts +23 -24
  50. package/src/global_variable_builder/index.ts +1 -1
  51. package/src/publisher/config.ts +112 -43
  52. package/src/publisher/sequencer-publisher-factory.ts +23 -6
  53. package/src/publisher/sequencer-publisher.ts +123 -71
  54. package/src/sequencer/checkpoint_proposal_job.ts +238 -136
  55. package/src/sequencer/checkpoint_voter.ts +1 -12
  56. package/src/sequencer/metrics.ts +92 -18
  57. package/src/sequencer/sequencer.ts +60 -49
  58. package/src/sequencer/timetable.ts +13 -12
  59. package/src/sequencer/types.ts +1 -1
  60. package/src/test/index.ts +2 -4
  61. package/src/test/mock_checkpoint_builder.ts +65 -49
  62. package/src/test/utils.ts +5 -2
@@ -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,18 +30,23 @@ 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 {
34
- NoValidTxsError,
35
- type PublicProcessorLimits,
37
+ type BlockBuilderOptions,
38
+ InsufficientValidTxsError,
36
39
  type ResolvedSequencerConfig,
37
40
  type WorldStateSynchronizer,
38
41
  } from '@aztec/stdlib/interfaces/server';
39
42
  import { type L1ToL2MessageSource, computeInHashFromL1ToL2Messages } from '@aztec/stdlib/messaging';
40
- import type { BlockProposalOptions, CheckpointProposal, CheckpointProposalOptions } from '@aztec/stdlib/p2p';
41
- import { orderAttestations } from '@aztec/stdlib/p2p';
43
+ import type {
44
+ BlockProposal,
45
+ BlockProposalOptions,
46
+ CheckpointProposal,
47
+ CheckpointProposalOptions,
48
+ } from '@aztec/stdlib/p2p';
49
+ import { orderAttestations, trimAttestations } from '@aztec/stdlib/p2p';
42
50
  import type { L2BlockBuiltStats } from '@aztec/stdlib/stats';
43
51
  import { type FailedTx, Tx } from '@aztec/stdlib/tx';
44
52
  import { AttestationTimeoutError } from '@aztec/stdlib/validators';
@@ -129,7 +137,7 @@ export class CheckpointProposalJob implements Traceable {
129
137
  await Promise.all(votesPromises);
130
138
 
131
139
  if (checkpoint) {
132
- this.metrics.recordBlockProposalSuccess();
140
+ this.metrics.recordCheckpointProposalSuccess();
133
141
  }
134
142
 
135
143
  // Do not post anything to L1 if we are fishermen, but do perform L1 fee analysis
@@ -186,18 +194,21 @@ export class CheckpointProposalJob implements Traceable {
186
194
  const inHash = computeInHashFromL1ToL2Messages(l1ToL2Messages);
187
195
 
188
196
  // 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());
197
+ const previousCheckpointOutHashes = (await this.l2BlockSource.getCheckpointsDataForEpoch(this.epoch))
198
+ .filter(c => c.checkpointNumber < this.checkpointNumber)
199
+ .map(c => c.checkpointOutHash);
200
+
201
+ // Get the fee asset price modifier from the oracle
202
+ const feeAssetPriceModifier = await this.publisher.getFeeAssetPriceModifier();
193
203
 
194
204
  // Create a long-lived forked world state for the checkpoint builder
195
- using fork = await this.worldState.fork(this.syncedToBlockNumber, { closeDelayMs: 12_000 });
205
+ await using fork = await this.worldState.fork(this.syncedToBlockNumber, { closeDelayMs: 12_000 });
196
206
 
197
207
  // Create checkpoint builder for the entire slot
198
208
  const checkpointBuilder = await this.checkpointsBuilder.startCheckpoint(
199
209
  this.checkpointNumber,
200
210
  checkpointGlobalVariables,
211
+ feeAssetPriceModifier,
201
212
  l1ToL2Messages,
202
213
  previousCheckpointOutHashes,
203
214
  fork,
@@ -217,6 +228,7 @@ export class CheckpointProposalJob implements Traceable {
217
228
 
218
229
  let blocksInCheckpoint: L2Block[] = [];
219
230
  let blockPendingBroadcast: { block: L2Block; txs: Tx[] } | undefined = undefined;
231
+ const checkpointBuildTimer = new Timer();
220
232
 
221
233
  try {
222
234
  // Main loop: build blocks for the checkpoint
@@ -232,19 +244,7 @@ export class CheckpointProposalJob implements Traceable {
232
244
  // These errors are expected in HA mode, so we yield and let another HA node handle the slot
233
245
  // The only distinction between the 2 errors is SlashingProtectionError throws when the payload is different,
234
246
  // which is normal for block building (may have picked different txs)
235
- if (err instanceof DutyAlreadySignedError) {
236
- this.log.info(`Checkpoint proposal for slot ${this.slot} already signed by another HA node, yielding`, {
237
- slot: this.slot,
238
- signedByNode: err.signedByNode,
239
- });
240
- return undefined;
241
- }
242
- if (err instanceof SlashingProtectionError) {
243
- this.log.info(`Checkpoint proposal for slot ${this.slot} blocked by slashing protection, yielding`, {
244
- slot: this.slot,
245
- existingMessageHash: err.existingMessageHash,
246
- attemptedMessageHash: err.attemptedMessageHash,
247
- });
247
+ if (this.handleHASigningError(err, 'Block proposal')) {
248
248
  return undefined;
249
249
  }
250
250
  throw err;
@@ -256,11 +256,45 @@ export class CheckpointProposalJob implements Traceable {
256
256
  return undefined;
257
257
  }
258
258
 
259
+ const minBlocksForCheckpoint = this.config.minBlocksForCheckpoint;
260
+ if (minBlocksForCheckpoint !== undefined && blocksInCheckpoint.length < minBlocksForCheckpoint) {
261
+ this.log.warn(
262
+ `Checkpoint has fewer blocks than minimum (${blocksInCheckpoint.length} < ${minBlocksForCheckpoint}), skipping proposal`,
263
+ { slot: this.slot, blocksBuilt: blocksInCheckpoint.length, minBlocksForCheckpoint },
264
+ );
265
+ return undefined;
266
+ }
267
+
259
268
  // Assemble and broadcast the checkpoint proposal, including the last block that was not
260
269
  // broadcasted yet, and wait to collect the committee attestations.
261
270
  this.setStateFn(SequencerState.ASSEMBLING_CHECKPOINT, this.slot);
262
271
  const checkpoint = await checkpointBuilder.completeCheckpoint();
263
272
 
273
+ // Final validation: per-block limits are only checked if the operator set them explicitly.
274
+ // Otherwise, checkpoint-level budgets were already enforced by the redistribution logic.
275
+ try {
276
+ validateCheckpoint(checkpoint, {
277
+ rollupManaLimit: this.l1Constants.rollupManaLimit,
278
+ maxL2BlockGas: this.config.maxL2BlockGas,
279
+ maxDABlockGas: this.config.maxDABlockGas,
280
+ maxTxsPerBlock: this.config.maxTxsPerBlock,
281
+ maxTxsPerCheckpoint: this.config.maxTxsPerCheckpoint,
282
+ });
283
+ } catch (err) {
284
+ this.log.error(`Built an invalid checkpoint at slot ${this.slot} (skipping proposal)`, err, {
285
+ checkpoint: checkpoint.header.toInspect(),
286
+ });
287
+ return undefined;
288
+ }
289
+
290
+ // Record checkpoint-level build metrics
291
+ this.metrics.recordCheckpointBuild(
292
+ checkpointBuildTimer.ms(),
293
+ blocksInCheckpoint.length,
294
+ checkpoint.getStats().txCount,
295
+ Number(checkpoint.header.totalManaUsed.toBigInt()),
296
+ );
297
+
264
298
  // Do not collect attestations nor publish to L1 in fisherman mode
265
299
  if (this.config.fishermanMode) {
266
300
  this.log.info(
@@ -287,6 +321,7 @@ export class CheckpointProposalJob implements Traceable {
287
321
  const proposal = await this.validatorClient.createCheckpointProposal(
288
322
  checkpoint.header,
289
323
  checkpoint.archive.root,
324
+ feeAssetPriceModifier,
290
325
  lastBlock,
291
326
  this.proposer,
292
327
  checkpointProposalOptions,
@@ -313,20 +348,8 @@ export class CheckpointProposalJob implements Traceable {
313
348
  );
314
349
  } catch (err) {
315
350
  // We shouldn't really get here since we yield to another HA node
316
- // as soon as we see these errors when creating block proposals.
317
- if (err instanceof DutyAlreadySignedError) {
318
- this.log.info(`Attestations signature for slot ${this.slot} already signed by another HA node, yielding`, {
319
- slot: this.slot,
320
- signedByNode: err.signedByNode,
321
- });
322
- return undefined;
323
- }
324
- if (err instanceof SlashingProtectionError) {
325
- this.log.info(`Attestations signature for slot ${this.slot} blocked by slashing protection, yielding`, {
326
- slot: this.slot,
327
- existingMessageHash: err.existingMessageHash,
328
- attemptedMessageHash: err.attemptedMessageHash,
329
- });
351
+ // as soon as we see these errors when creating block or checkpoint proposals.
352
+ if (this.handleHASigningError(err, 'Attestations signature')) {
330
353
  return undefined;
331
354
  }
332
355
  throw err;
@@ -337,6 +360,21 @@ export class CheckpointProposalJob implements Traceable {
337
360
  const aztecSlotDuration = this.l1Constants.slotDuration;
338
361
  const slotStartBuildTimestamp = this.getSlotStartBuildTimestamp();
339
362
  const txTimeoutAt = new Date((slotStartBuildTimestamp + aztecSlotDuration) * 1000);
363
+
364
+ // If we have been configured to potentially skip publishing checkpoint then roll the dice here
365
+ if (
366
+ this.config.skipPublishingCheckpointsPercent !== undefined &&
367
+ this.config.skipPublishingCheckpointsPercent > 0
368
+ ) {
369
+ const result = Math.max(0, randomInt(100));
370
+ if (result < this.config.skipPublishingCheckpointsPercent) {
371
+ this.log.warn(
372
+ `Skipping publishing proposal for checkpoint ${checkpoint.number}. Configured percentage: ${this.config.skipPublishingCheckpointsPercent}, generated value: ${result}`,
373
+ );
374
+ return checkpoint;
375
+ }
376
+ }
377
+
340
378
  await this.publisher.enqueueProposeCheckpoint(checkpoint, attestations, attestationsSignature, {
341
379
  txTimeoutAt,
342
380
  forcePendingCheckpointNumber: this.invalidateCheckpoint?.forcePendingCheckpointNumber,
@@ -370,9 +408,7 @@ export class CheckpointProposalJob implements Traceable {
370
408
  const blocksInCheckpoint: L2Block[] = [];
371
409
  const txHashesAlreadyIncluded = new Set<string>();
372
410
  const initialBlockNumber = BlockNumber(this.syncedToBlockNumber + 1);
373
-
374
- // Remaining blob fields available for blocks (checkpoint end marker already subtracted)
375
- let remainingBlobFields = BLOBS_PER_CHECKPOINT * FIELDS_PER_BLOB - NUM_CHECKPOINT_END_MARKER_FIELDS;
411
+ const slot = this.slot;
376
412
 
377
413
  // Last block in the checkpoint will usually be flagged as pending broadcast, so we send it along with the checkpoint proposal
378
414
  let blockPendingBroadcast: { block: L2Block; txs: Tx[] } | undefined = undefined;
@@ -386,11 +422,7 @@ export class CheckpointProposalJob implements Traceable {
386
422
  const timingInfo = this.timetable.canStartNextBlock(secondsIntoSlot);
387
423
 
388
424
  if (!timingInfo.canStart) {
389
- this.log.debug(`Not enough time left in slot to start another block`, {
390
- slot: this.slot,
391
- blocksBuilt,
392
- secondsIntoSlot,
393
- });
425
+ this.log.debug(`Not enough time left in slot to start another block`, { slot, blocksBuilt, secondsIntoSlot });
394
426
  break;
395
427
  }
396
428
 
@@ -406,7 +438,6 @@ export class CheckpointProposalJob implements Traceable {
406
438
  blockNumber,
407
439
  indexWithinCheckpoint,
408
440
  txHashesAlreadyIncluded,
409
- remainingBlobFields,
410
441
  });
411
442
 
412
443
  // TODO(palla/mbps): Review these conditions. We may want to keep trying in some scenarios.
@@ -423,56 +454,37 @@ export class CheckpointProposalJob implements Traceable {
423
454
  } else if ('error' in buildResult) {
424
455
  // If there was an error building the block, just exit the loop and give up the rest of the slot
425
456
  if (!(buildResult.error instanceof SequencerInterruptedError)) {
426
- this.log.warn(`Halting block building for slot ${this.slot}`, {
427
- slot: this.slot,
428
- blocksBuilt,
429
- error: buildResult.error,
430
- });
457
+ this.log.warn(`Halting block building for slot ${slot}`, { slot, blocksBuilt, error: buildResult.error });
431
458
  }
432
459
  break;
433
460
  }
434
461
 
435
- const { block, usedTxs, remainingBlobFields: newRemainingBlobFields } = buildResult;
462
+ const { block, usedTxs } = buildResult;
436
463
  blocksInCheckpoint.push(block);
437
-
438
- // Update remaining blob fields for the next block
439
- remainingBlobFields = newRemainingBlobFields;
440
-
441
- // Sync the proposed block to the archiver to make it available
442
- // Note that the checkpoint builder uses its own fork so it should not need to wait for this syncing
443
- // Eventually we should refactor the checkpoint builder to not need a separate long-lived fork
444
- // Fire and forget - don't block the critical path, but log errors
445
- this.syncProposedBlockToArchiver(block).catch(err => {
446
- this.log.error(`Failed to sync proposed block ${block.number} to archiver`, { blockNumber: block.number, err });
447
- });
448
-
449
464
  usedTxs.forEach(tx => txHashesAlreadyIncluded.add(tx.txHash.toString()));
450
465
 
451
- // If this is the last block, exit the loop now so we start collecting attestations
466
+ // If this is the last block, send the proposed block to the archiver,
467
+ // and exit the loop now so we can build the checkpoint and start collecting attestations.
452
468
  if (timingInfo.isLastBlock) {
453
- this.log.verbose(`Completed final block ${blockNumber} for slot ${this.slot}`, {
454
- slot: this.slot,
455
- blockNumber,
456
- blocksBuilt,
457
- });
469
+ await this.syncProposedBlockToArchiver(block);
470
+ this.log.verbose(`Completed final block ${blockNumber} for slot ${slot}`, { slot, blockNumber, blocksBuilt });
458
471
  blockPendingBroadcast = { block, txs: usedTxs };
459
472
  break;
460
473
  }
461
474
 
462
- // For non-last blocks, broadcast the block proposal (unless we're in fisherman mode)
463
- // If the block is the last one, we'll broadcast it along with the checkpoint at the end of the loop
464
- if (!this.config.fishermanMode) {
465
- const proposal = await this.validatorClient.createBlockProposal(
466
- block.header,
467
- block.indexWithinCheckpoint,
468
- inHash,
469
- block.archive.root,
470
- usedTxs,
471
- this.proposer,
472
- blockProposalOptions,
473
- );
474
- await this.p2pClient.broadcastProposal(proposal);
475
- }
475
+ // Broadcast the block proposal (unless we're in fisherman mode) unless the block is the last one,
476
+ // in which case we'll broadcast it along with the checkpoint at the end of the loop.
477
+ // Note that we only send the block to the archiver if we manage to create the proposal, so if there's
478
+ // a HA error we don't pollute our archiver with a block that won't make it to the chain.
479
+ const proposal = await this.createBlockProposal(block, inHash, usedTxs, blockProposalOptions);
480
+
481
+ // Sync the proposed block to the archiver to make it available, only after we've managed to sign the proposal.
482
+ // We wait for the sync to succeed, as this helps catch consistency errors, even if it means we lose some time for block-building.
483
+ // If this throws, we abort the entire checkpoint.
484
+ await this.syncProposedBlockToArchiver(block);
485
+
486
+ // Once we have a signed proposal and the archiver agreed with our proposed block, then we broadcast it.
487
+ proposal && (await this.p2pClient.broadcastProposal(proposal));
476
488
 
477
489
  // Wait until the next block's start time
478
490
  await this.waitUntilNextSubslot(timingInfo.deadline);
@@ -486,6 +498,28 @@ export class CheckpointProposalJob implements Traceable {
486
498
  return { blocksInCheckpoint, blockPendingBroadcast };
487
499
  }
488
500
 
501
+ /** Creates a block proposal for a given block via the validator client (unless in fisherman mode) */
502
+ private createBlockProposal(
503
+ block: L2Block,
504
+ inHash: Fr,
505
+ usedTxs: Tx[],
506
+ blockProposalOptions: BlockProposalOptions,
507
+ ): Promise<BlockProposal | undefined> {
508
+ if (this.config.fishermanMode) {
509
+ this.log.info(`Skipping block proposal for block ${block.number} in fisherman mode`);
510
+ return Promise.resolve(undefined);
511
+ }
512
+ return this.validatorClient.createBlockProposal(
513
+ block.header,
514
+ block.indexWithinCheckpoint,
515
+ inHash,
516
+ block.archive.root,
517
+ usedTxs,
518
+ this.proposer,
519
+ blockProposalOptions,
520
+ );
521
+ }
522
+
489
523
  /** Sleeps until it is time to produce the next block in the slot */
490
524
  @trackSpan('CheckpointProposalJob.waitUntilNextSubslot')
491
525
  private async waitUntilNextSubslot(nextSubslotStart: number) {
@@ -505,18 +539,10 @@ export class CheckpointProposalJob implements Traceable {
505
539
  indexWithinCheckpoint: IndexWithinCheckpoint;
506
540
  buildDeadline: Date | undefined;
507
541
  txHashesAlreadyIncluded: Set<string>;
508
- remainingBlobFields: number;
509
542
  },
510
- ): Promise<{ block: L2Block; usedTxs: Tx[]; remainingBlobFields: number } | { error: Error } | undefined> {
511
- const {
512
- blockTimestamp,
513
- forceCreate,
514
- blockNumber,
515
- indexWithinCheckpoint,
516
- buildDeadline,
517
- txHashesAlreadyIncluded,
518
- remainingBlobFields,
519
- } = opts;
543
+ ): Promise<{ block: L2Block; usedTxs: Tx[] } | { error: Error } | undefined> {
544
+ const { blockTimestamp, forceCreate, blockNumber, indexWithinCheckpoint, buildDeadline, txHashesAlreadyIncluded } =
545
+ opts;
520
546
 
521
547
  this.log.verbose(
522
548
  `Preparing block ${blockNumber} index ${indexWithinCheckpoint} at checkpoint ${this.checkpointNumber} for slot ${this.slot}`,
@@ -525,8 +551,7 @@ export class CheckpointProposalJob implements Traceable {
525
551
 
526
552
  try {
527
553
  // Wait until we have enough txs to build the block
528
- const minTxs = this.config.minTxsPerBlock;
529
- const { availableTxs, canStartBuilding } = await this.waitForMinTxs(opts);
554
+ const { availableTxs, canStartBuilding, minTxs } = await this.waitForMinTxs(opts);
530
555
  if (!canStartBuilding) {
531
556
  this.log.warn(
532
557
  `Not enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.slot} (got ${availableTxs} txs but needs ${minTxs})`,
@@ -540,7 +565,7 @@ export class CheckpointProposalJob implements Traceable {
540
565
  // Create iterator to pending txs. We filter out txs already included in previous blocks in the checkpoint
541
566
  // just in case p2p failed to sync the provisional block and didn't get to remove those txs from the mempool yet.
542
567
  const pendingTxs = filter(
543
- this.p2pClient.iteratePendingTxs(),
568
+ this.p2pClient.iterateEligiblePendingTxs(),
544
569
  tx => !txHashesAlreadyIncluded.has(tx.txHash.toString()),
545
570
  );
546
571
 
@@ -550,19 +575,26 @@ export class CheckpointProposalJob implements Traceable {
550
575
  );
551
576
  this.setStateFn(SequencerState.CREATING_BLOCK, this.slot);
552
577
 
553
- // Calculate blob fields limit for txs (remaining capacity - this block's end overhead)
554
- const blockEndOverhead = getNumBlockEndBlobFields(indexWithinCheckpoint === 0);
555
- const maxBlobFieldsForTxs = remainingBlobFields - blockEndOverhead;
556
-
557
- const blockBuilderOptions: PublicProcessorLimits = {
578
+ // Per-block limits are operator overrides (from SEQ_MAX_L2_BLOCK_GAS etc.) further capped
579
+ // by remaining checkpoint-level budgets inside CheckpointBuilder before each block is built.
580
+ // minValidTxs is passed into the builder so it can reject the block *before* updating state.
581
+ const minValidTxs = forceCreate ? 0 : (this.config.minValidTxsPerBlock ?? minTxs);
582
+ const blockBuilderOptions: BlockBuilderOptions = {
558
583
  maxTransactions: this.config.maxTxsPerBlock,
559
- maxBlockSize: this.config.maxBlockSizeInBytes,
560
- maxBlockGas: new Gas(this.config.maxDABlockGas, this.config.maxL2BlockGas),
561
- maxBlobFields: maxBlobFieldsForTxs,
584
+ maxBlockGas:
585
+ this.config.maxL2BlockGas !== undefined || this.config.maxDABlockGas !== undefined
586
+ ? new Gas(this.config.maxDABlockGas ?? Infinity, this.config.maxL2BlockGas ?? Infinity)
587
+ : undefined,
562
588
  deadline: buildDeadline,
589
+ isBuildingProposal: true,
590
+ minValidTxs,
591
+ maxBlocksPerCheckpoint: this.timetable.maxNumberOfBlocks,
592
+ perBlockAllocationMultiplier: this.config.perBlockAllocationMultiplier,
563
593
  };
564
594
 
565
- // Actually build the block by executing txs
595
+ // Actually build the block by executing txs. The builder throws InsufficientValidTxsError
596
+ // if the number of successfully processed txs is below minValidTxs, ensuring state is not
597
+ // updated for blocks that will be discarded.
566
598
  const buildResult = await this.buildSingleBlockWithCheckpointBuilder(
567
599
  checkpointBuilder,
568
600
  pendingTxs,
@@ -574,14 +606,16 @@ export class CheckpointProposalJob implements Traceable {
574
606
  // If any txs failed during execution, drop them from the mempool so we don't pick them up again
575
607
  await this.dropFailedTxsFromP2P(buildResult.failedTxs);
576
608
 
577
- // Check if we have created a block with enough txs. If there were invalid txs in the pool, or if execution took
578
- // too long, then we may not get to minTxsPerBlock after executing public functions.
579
- const minValidTxs = this.config.minValidTxsPerBlock ?? minTxs;
580
- const numTxs = buildResult.status === 'no-valid-txs' ? 0 : buildResult.numTxs;
581
- if (buildResult.status === 'no-valid-txs' || (!forceCreate && numTxs < minValidTxs)) {
609
+ if (buildResult.status === 'insufficient-valid-txs') {
582
610
  this.log.warn(
583
611
  `Block ${blockNumber} at index ${indexWithinCheckpoint} on slot ${this.slot} has too few valid txs to be proposed`,
584
- { slot: this.slot, blockNumber, numTxs, indexWithinCheckpoint, minValidTxs, buildResult: buildResult.status },
612
+ {
613
+ slot: this.slot,
614
+ blockNumber,
615
+ numTxs: buildResult.processedCount,
616
+ indexWithinCheckpoint,
617
+ minValidTxs,
618
+ },
585
619
  );
586
620
  this.eventEmitter.emit('block-build-failed', { reason: `Insufficient valid txs`, slot: this.slot });
587
621
  this.metrics.recordBlockProposalFailed('insufficient_valid_txs');
@@ -589,7 +623,7 @@ export class CheckpointProposalJob implements Traceable {
589
623
  }
590
624
 
591
625
  // Block creation succeeded, emit stats and metrics
592
- const { publicGas, block, publicProcessorDuration, usedTxs, usedTxBlobFields, blockBuildDuration } = buildResult;
626
+ const { block, publicProcessorDuration, usedTxs, blockBuildDuration, numTxs } = buildResult;
593
627
 
594
628
  const blockStats = {
595
629
  eventName: 'l2-block-built',
@@ -600,7 +634,7 @@ export class CheckpointProposalJob implements Traceable {
600
634
 
601
635
  const blockHash = await block.hash();
602
636
  const txHashes = block.body.txEffects.map(tx => tx.txHash);
603
- const manaPerSec = publicGas.l2Gas / (blockBuildDuration / 1000);
637
+ const manaPerSec = block.header.totalManaUsed.toNumberUnsafe() / (blockBuildDuration / 1000);
604
638
 
605
639
  this.log.info(
606
640
  `Built block ${block.number} at checkpoint ${this.checkpointNumber} for slot ${this.slot} with ${numTxs} txs`,
@@ -608,9 +642,9 @@ export class CheckpointProposalJob implements Traceable {
608
642
  );
609
643
 
610
644
  this.eventEmitter.emit('block-proposed', { blockNumber: block.number, slot: this.slot });
611
- this.metrics.recordBuiltBlock(blockBuildDuration, publicGas.l2Gas);
645
+ this.metrics.recordBuiltBlock(blockBuildDuration, block.header.totalManaUsed.toNumberUnsafe());
612
646
 
613
- return { block, usedTxs, remainingBlobFields: maxBlobFieldsForTxs - usedTxBlobFields };
647
+ return { block, usedTxs };
614
648
  } catch (err: any) {
615
649
  this.eventEmitter.emit('block-build-failed', { reason: err.message, slot: this.slot });
616
650
  this.log.error(`Error building block`, err, { blockNumber, slot: this.slot });
@@ -620,13 +654,13 @@ export class CheckpointProposalJob implements Traceable {
620
654
  }
621
655
  }
622
656
 
623
- /** Uses the checkpoint builder to build a block, catching specific txs */
657
+ /** Uses the checkpoint builder to build a block, catching InsufficientValidTxsError. */
624
658
  private async buildSingleBlockWithCheckpointBuilder(
625
659
  checkpointBuilder: CheckpointBuilder,
626
660
  pendingTxs: AsyncIterable<Tx>,
627
661
  blockNumber: BlockNumber,
628
662
  blockTimestamp: bigint,
629
- blockBuilderOptions: PublicProcessorLimits,
663
+ blockBuilderOptions: BlockBuilderOptions,
630
664
  ) {
631
665
  try {
632
666
  const workTimer = new Timer();
@@ -634,8 +668,12 @@ export class CheckpointProposalJob implements Traceable {
634
668
  const blockBuildDuration = workTimer.ms();
635
669
  return { ...result, blockBuildDuration, status: 'success' as const };
636
670
  } catch (err: unknown) {
637
- if (isErrorClass(err, NoValidTxsError)) {
638
- return { failedTxs: err.failedTxs, status: 'no-valid-txs' as const };
671
+ if (isErrorClass(err, InsufficientValidTxsError)) {
672
+ return {
673
+ failedTxs: err.failedTxs,
674
+ processedCount: err.processedCount,
675
+ status: 'insufficient-valid-txs' as const,
676
+ };
639
677
  }
640
678
  throw err;
641
679
  }
@@ -648,7 +686,7 @@ export class CheckpointProposalJob implements Traceable {
648
686
  blockNumber: BlockNumber;
649
687
  indexWithinCheckpoint: IndexWithinCheckpoint;
650
688
  buildDeadline: Date | undefined;
651
- }): Promise<{ canStartBuilding: boolean; availableTxs: number }> {
689
+ }): Promise<{ canStartBuilding: boolean; availableTxs: number; minTxs: number }> {
652
690
  const { indexWithinCheckpoint, blockNumber, buildDeadline, forceCreate } = opts;
653
691
 
654
692
  // We only allow a block with 0 txs in the first block of the checkpoint
@@ -665,7 +703,7 @@ export class CheckpointProposalJob implements Traceable {
665
703
  // If we're past deadline, or we have no deadline, give up
666
704
  const now = this.dateProvider.nowAsDate();
667
705
  if (startBuildingDeadline === undefined || now >= startBuildingDeadline) {
668
- return { canStartBuilding: false, availableTxs: availableTxs };
706
+ return { canStartBuilding: false, availableTxs, minTxs };
669
707
  }
670
708
 
671
709
  // Wait a bit before checking again
@@ -674,11 +712,11 @@ export class CheckpointProposalJob implements Traceable {
674
712
  `Waiting for enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.slot} (have ${availableTxs} but need ${minTxs})`,
675
713
  { blockNumber, slot: this.slot, indexWithinCheckpoint },
676
714
  );
677
- await sleep(TXS_POLLING_MS);
715
+ await this.waitForTxsPollingInterval();
678
716
  availableTxs = await this.p2pClient.getPendingTxCount();
679
717
  }
680
718
 
681
- return { canStartBuilding: true, availableTxs };
719
+ return { canStartBuilding: true, availableTxs, minTxs };
682
720
  }
683
721
 
684
722
  /**
@@ -730,11 +768,28 @@ export class CheckpointProposalJob implements Traceable {
730
768
 
731
769
  collectedAttestationsCount = attestations.length;
732
770
 
771
+ // Trim attestations to minimum required to save L1 calldata gas
772
+ const localAddresses = this.validatorClient.getValidatorAddresses();
773
+ const trimmed = trimAttestations(
774
+ attestations,
775
+ numberOfRequiredAttestations,
776
+ this.attestorAddress,
777
+ localAddresses,
778
+ );
779
+ if (trimmed.length < attestations.length) {
780
+ this.log.debug(`Trimmed attestations from ${attestations.length} to ${trimmed.length} for L1 submission`);
781
+ }
782
+
733
783
  // Rollup contract requires that the signatures are provided in the order of the committee
734
- const sorted = orderAttestations(attestations, committee);
784
+ const sorted = orderAttestations(trimmed, committee);
735
785
 
736
786
  // Manipulate the attestations if we've been configured to do so
737
- if (this.config.injectFakeAttestation || this.config.shuffleAttestationOrdering) {
787
+ if (
788
+ this.config.injectFakeAttestation ||
789
+ this.config.injectHighSValueAttestation ||
790
+ this.config.injectUnrecoverableSignatureAttestation ||
791
+ this.config.shuffleAttestationOrdering
792
+ ) {
738
793
  return this.manipulateAttestations(proposal.slotNumber, epoch, seed, committee, sorted);
739
794
  }
740
795
 
@@ -763,7 +818,11 @@ export class CheckpointProposalJob implements Traceable {
763
818
  this.epochCache.computeProposerIndex(slotNumber, epoch, seed, BigInt(committee.length)),
764
819
  );
765
820
 
766
- if (this.config.injectFakeAttestation) {
821
+ if (
822
+ this.config.injectFakeAttestation ||
823
+ this.config.injectHighSValueAttestation ||
824
+ this.config.injectUnrecoverableSignatureAttestation
825
+ ) {
767
826
  // Find non-empty attestations that are not from the proposer
768
827
  const nonProposerIndices: number[] = [];
769
828
  for (let i = 0; i < attestations.length; i++) {
@@ -773,8 +832,20 @@ export class CheckpointProposalJob implements Traceable {
773
832
  }
774
833
  if (nonProposerIndices.length > 0) {
775
834
  const targetIndex = nonProposerIndices[randomInt(nonProposerIndices.length)];
776
- this.log.warn(`Injecting fake attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`);
777
- unfreeze(attestations[targetIndex]).signature = Signature.random();
835
+ if (this.config.injectHighSValueAttestation) {
836
+ this.log.warn(
837
+ `Injecting high-s value attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`,
838
+ );
839
+ unfreeze(attestations[targetIndex]).signature = flipSignature(attestations[targetIndex].signature);
840
+ } else if (this.config.injectUnrecoverableSignatureAttestation) {
841
+ this.log.warn(
842
+ `Injecting unrecoverable signature attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`,
843
+ );
844
+ unfreeze(attestations[targetIndex]).signature = generateUnrecoverableSignature();
845
+ } else {
846
+ this.log.warn(`Injecting fake attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`);
847
+ unfreeze(attestations[targetIndex]).signature = generateRecoverableSignature();
848
+ }
778
849
  }
779
850
  return new CommitteeAttestationsAndSigners(attestations);
780
851
  }
@@ -803,16 +874,20 @@ export class CheckpointProposalJob implements Traceable {
803
874
  const failedTxData = failedTxs.map(fail => fail.tx);
804
875
  const failedTxHashes = failedTxData.map(tx => tx.getTxHash());
805
876
  this.log.verbose(`Dropping failed txs ${failedTxHashes.join(', ')}`);
806
- await this.p2pClient.deleteTxs(failedTxHashes);
877
+ await this.p2pClient.handleFailedExecution(failedTxHashes);
807
878
  }
808
879
 
809
880
  /**
810
881
  * Adds the proposed block to the archiver so it's available via P2P.
811
882
  * Gossip doesn't echo messages back to the sender, so the proposer's archiver/world-state
812
883
  * would never receive its own block without this explicit sync.
884
+ *
885
+ * In fisherman mode we skip this push: the fisherman builds blocks locally for validation
886
+ * and fee analysis only, and pushing them to the archiver causes spurious reorg cascades
887
+ * whenever the real proposer's block arrives from L1.
813
888
  */
814
889
  private async syncProposedBlockToArchiver(block: L2Block): Promise<void> {
815
- if (this.config.skipPushProposedBlocksToArchiver !== false) {
890
+ if (this.config.skipPushProposedBlocksToArchiver !== false || this.config.fishermanMode) {
816
891
  this.log.warn(`Skipping push of proposed block ${block.number} to archiver`, {
817
892
  blockNumber: block.number,
818
893
  slot: block.header.globalVariables.slotNumber,
@@ -845,12 +920,34 @@ export class CheckpointProposalJob implements Traceable {
845
920
  slot: this.slot,
846
921
  feeAnalysisId: feeAnalysis?.id,
847
922
  });
848
- this.metrics.recordBlockProposalFailed('block_build_failed');
923
+ this.metrics.recordCheckpointProposalFailed('block_build_failed');
849
924
  }
850
925
 
851
926
  this.publisher.clearPendingRequests();
852
927
  }
853
928
 
929
+ /**
930
+ * Helper to handle HA double-signing errors. Returns true if the error was handled (caller should yield).
931
+ */
932
+ private handleHASigningError(err: any, errorContext: string): boolean {
933
+ if (err instanceof DutyAlreadySignedError) {
934
+ this.log.info(`${errorContext} for slot ${this.slot} already signed by another HA node, yielding`, {
935
+ slot: this.slot,
936
+ signedByNode: err.signedByNode,
937
+ });
938
+ return true;
939
+ }
940
+ if (err instanceof SlashingProtectionError) {
941
+ this.log.info(`${errorContext} for slot ${this.slot} blocked by slashing protection, yielding`, {
942
+ slot: this.slot,
943
+ existingMessageHash: err.existingMessageHash,
944
+ attemptedMessageHash: err.attemptedMessageHash,
945
+ });
946
+ return true;
947
+ }
948
+ return false;
949
+ }
950
+
854
951
  /** Waits until a specific time within the current slot */
855
952
  @trackSpan('CheckpointProposalJob.waitUntilTimeInSlot')
856
953
  protected async waitUntilTimeInSlot(targetSecondsIntoSlot: number): Promise<void> {
@@ -859,6 +956,11 @@ export class CheckpointProposalJob implements Traceable {
859
956
  await sleepUntil(new Date(targetTimestamp * 1000), this.dateProvider.nowAsDate());
860
957
  }
861
958
 
959
+ /** Waits the polling interval for transactions. Extracted for test overriding. */
960
+ protected async waitForTxsPollingInterval(): Promise<void> {
961
+ await sleep(TXS_POLLING_MS);
962
+ }
963
+
862
964
  private getSlotStartBuildTimestamp(): number {
863
965
  return getSlotStartBuildTimestamp(this.slot, this.l1Constants);
864
966
  }