@aztec/sequencer-client 0.0.1-commit.23b0eb0 → 0.0.1-commit.2448fdb

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 +6 -4
  22. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  23. package/dest/sequencer/checkpoint_proposal_job.js +139 -80
  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 +27 -13
  31. package/dest/sequencer/sequencer.d.ts.map +1 -1
  32. package/dest/sequencer/sequencer.js +42 -42
  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 +213 -109
  55. package/src/sequencer/checkpoint_voter.ts +1 -12
  56. package/src/sequencer/metrics.ts +92 -18
  57. package/src/sequencer/sequencer.ts +53 -48
  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
@@ -244,11 +256,45 @@ export class CheckpointProposalJob implements Traceable {
244
256
  return undefined;
245
257
  }
246
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
+
247
268
  // Assemble and broadcast the checkpoint proposal, including the last block that was not
248
269
  // broadcasted yet, and wait to collect the committee attestations.
249
270
  this.setStateFn(SequencerState.ASSEMBLING_CHECKPOINT, this.slot);
250
271
  const checkpoint = await checkpointBuilder.completeCheckpoint();
251
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
+
252
298
  // Do not collect attestations nor publish to L1 in fisherman mode
253
299
  if (this.config.fishermanMode) {
254
300
  this.log.info(
@@ -275,6 +321,7 @@ export class CheckpointProposalJob implements Traceable {
275
321
  const proposal = await this.validatorClient.createCheckpointProposal(
276
322
  checkpoint.header,
277
323
  checkpoint.archive.root,
324
+ feeAssetPriceModifier,
278
325
  lastBlock,
279
326
  this.proposer,
280
327
  checkpointProposalOptions,
@@ -313,6 +360,21 @@ export class CheckpointProposalJob implements Traceable {
313
360
  const aztecSlotDuration = this.l1Constants.slotDuration;
314
361
  const slotStartBuildTimestamp = this.getSlotStartBuildTimestamp();
315
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
+
316
378
  await this.publisher.enqueueProposeCheckpoint(checkpoint, attestations, attestationsSignature, {
317
379
  txTimeoutAt,
318
380
  forcePendingCheckpointNumber: this.invalidateCheckpoint?.forcePendingCheckpointNumber,
@@ -346,9 +408,7 @@ export class CheckpointProposalJob implements Traceable {
346
408
  const blocksInCheckpoint: L2Block[] = [];
347
409
  const txHashesAlreadyIncluded = new Set<string>();
348
410
  const initialBlockNumber = BlockNumber(this.syncedToBlockNumber + 1);
349
-
350
- // Remaining blob fields available for blocks (checkpoint end marker already subtracted)
351
- let remainingBlobFields = BLOBS_PER_CHECKPOINT * FIELDS_PER_BLOB - NUM_CHECKPOINT_END_MARKER_FIELDS;
411
+ const slot = this.slot;
352
412
 
353
413
  // Last block in the checkpoint will usually be flagged as pending broadcast, so we send it along with the checkpoint proposal
354
414
  let blockPendingBroadcast: { block: L2Block; txs: Tx[] } | undefined = undefined;
@@ -362,11 +422,7 @@ export class CheckpointProposalJob implements Traceable {
362
422
  const timingInfo = this.timetable.canStartNextBlock(secondsIntoSlot);
363
423
 
364
424
  if (!timingInfo.canStart) {
365
- this.log.debug(`Not enough time left in slot to start another block`, {
366
- slot: this.slot,
367
- blocksBuilt,
368
- secondsIntoSlot,
369
- });
425
+ this.log.debug(`Not enough time left in slot to start another block`, { slot, blocksBuilt, secondsIntoSlot });
370
426
  break;
371
427
  }
372
428
 
@@ -382,7 +438,6 @@ export class CheckpointProposalJob implements Traceable {
382
438
  blockNumber,
383
439
  indexWithinCheckpoint,
384
440
  txHashesAlreadyIncluded,
385
- remainingBlobFields,
386
441
  });
387
442
 
388
443
  // TODO(palla/mbps): Review these conditions. We may want to keep trying in some scenarios.
@@ -399,56 +454,37 @@ export class CheckpointProposalJob implements Traceable {
399
454
  } else if ('error' in buildResult) {
400
455
  // If there was an error building the block, just exit the loop and give up the rest of the slot
401
456
  if (!(buildResult.error instanceof SequencerInterruptedError)) {
402
- this.log.warn(`Halting block building for slot ${this.slot}`, {
403
- slot: this.slot,
404
- blocksBuilt,
405
- error: buildResult.error,
406
- });
457
+ this.log.warn(`Halting block building for slot ${slot}`, { slot, blocksBuilt, error: buildResult.error });
407
458
  }
408
459
  break;
409
460
  }
410
461
 
411
- const { block, usedTxs, remainingBlobFields: newRemainingBlobFields } = buildResult;
462
+ const { block, usedTxs } = buildResult;
412
463
  blocksInCheckpoint.push(block);
413
-
414
- // Update remaining blob fields for the next block
415
- remainingBlobFields = newRemainingBlobFields;
416
-
417
- // Sync the proposed block to the archiver to make it available
418
- // Note that the checkpoint builder uses its own fork so it should not need to wait for this syncing
419
- // Eventually we should refactor the checkpoint builder to not need a separate long-lived fork
420
- // Fire and forget - don't block the critical path, but log errors
421
- this.syncProposedBlockToArchiver(block).catch(err => {
422
- this.log.error(`Failed to sync proposed block ${block.number} to archiver`, { blockNumber: block.number, err });
423
- });
424
-
425
464
  usedTxs.forEach(tx => txHashesAlreadyIncluded.add(tx.txHash.toString()));
426
465
 
427
- // 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.
428
468
  if (timingInfo.isLastBlock) {
429
- this.log.verbose(`Completed final block ${blockNumber} for slot ${this.slot}`, {
430
- slot: this.slot,
431
- blockNumber,
432
- blocksBuilt,
433
- });
469
+ await this.syncProposedBlockToArchiver(block);
470
+ this.log.verbose(`Completed final block ${blockNumber} for slot ${slot}`, { slot, blockNumber, blocksBuilt });
434
471
  blockPendingBroadcast = { block, txs: usedTxs };
435
472
  break;
436
473
  }
437
474
 
438
- // For non-last blocks, broadcast the block proposal (unless we're in fisherman mode)
439
- // If the block is the last one, we'll broadcast it along with the checkpoint at the end of the loop
440
- if (!this.config.fishermanMode) {
441
- const proposal = await this.validatorClient.createBlockProposal(
442
- block.header,
443
- block.indexWithinCheckpoint,
444
- inHash,
445
- block.archive.root,
446
- usedTxs,
447
- this.proposer,
448
- blockProposalOptions,
449
- );
450
- await this.p2pClient.broadcastProposal(proposal);
451
- }
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));
452
488
 
453
489
  // Wait until the next block's start time
454
490
  await this.waitUntilNextSubslot(timingInfo.deadline);
@@ -462,6 +498,28 @@ export class CheckpointProposalJob implements Traceable {
462
498
  return { blocksInCheckpoint, blockPendingBroadcast };
463
499
  }
464
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
+
465
523
  /** Sleeps until it is time to produce the next block in the slot */
466
524
  @trackSpan('CheckpointProposalJob.waitUntilNextSubslot')
467
525
  private async waitUntilNextSubslot(nextSubslotStart: number) {
@@ -481,18 +539,10 @@ export class CheckpointProposalJob implements Traceable {
481
539
  indexWithinCheckpoint: IndexWithinCheckpoint;
482
540
  buildDeadline: Date | undefined;
483
541
  txHashesAlreadyIncluded: Set<string>;
484
- remainingBlobFields: number;
485
542
  },
486
- ): Promise<{ block: L2Block; usedTxs: Tx[]; remainingBlobFields: number } | { error: Error } | undefined> {
487
- const {
488
- blockTimestamp,
489
- forceCreate,
490
- blockNumber,
491
- indexWithinCheckpoint,
492
- buildDeadline,
493
- txHashesAlreadyIncluded,
494
- remainingBlobFields,
495
- } = opts;
543
+ ): Promise<{ block: L2Block; usedTxs: Tx[] } | { error: Error } | undefined> {
544
+ const { blockTimestamp, forceCreate, blockNumber, indexWithinCheckpoint, buildDeadline, txHashesAlreadyIncluded } =
545
+ opts;
496
546
 
497
547
  this.log.verbose(
498
548
  `Preparing block ${blockNumber} index ${indexWithinCheckpoint} at checkpoint ${this.checkpointNumber} for slot ${this.slot}`,
@@ -501,8 +551,7 @@ export class CheckpointProposalJob implements Traceable {
501
551
 
502
552
  try {
503
553
  // Wait until we have enough txs to build the block
504
- const minTxs = this.config.minTxsPerBlock;
505
- const { availableTxs, canStartBuilding } = await this.waitForMinTxs(opts);
554
+ const { availableTxs, canStartBuilding, minTxs } = await this.waitForMinTxs(opts);
506
555
  if (!canStartBuilding) {
507
556
  this.log.warn(
508
557
  `Not enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.slot} (got ${availableTxs} txs but needs ${minTxs})`,
@@ -516,7 +565,7 @@ export class CheckpointProposalJob implements Traceable {
516
565
  // Create iterator to pending txs. We filter out txs already included in previous blocks in the checkpoint
517
566
  // just in case p2p failed to sync the provisional block and didn't get to remove those txs from the mempool yet.
518
567
  const pendingTxs = filter(
519
- this.p2pClient.iteratePendingTxs(),
568
+ this.p2pClient.iterateEligiblePendingTxs(),
520
569
  tx => !txHashesAlreadyIncluded.has(tx.txHash.toString()),
521
570
  );
522
571
 
@@ -526,19 +575,26 @@ export class CheckpointProposalJob implements Traceable {
526
575
  );
527
576
  this.setStateFn(SequencerState.CREATING_BLOCK, this.slot);
528
577
 
529
- // Calculate blob fields limit for txs (remaining capacity - this block's end overhead)
530
- const blockEndOverhead = getNumBlockEndBlobFields(indexWithinCheckpoint === 0);
531
- const maxBlobFieldsForTxs = remainingBlobFields - blockEndOverhead;
532
-
533
- 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 = {
534
583
  maxTransactions: this.config.maxTxsPerBlock,
535
- maxBlockSize: this.config.maxBlockSizeInBytes,
536
- maxBlockGas: new Gas(this.config.maxDABlockGas, this.config.maxL2BlockGas),
537
- 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,
538
588
  deadline: buildDeadline,
589
+ isBuildingProposal: true,
590
+ minValidTxs,
591
+ maxBlocksPerCheckpoint: this.timetable.maxNumberOfBlocks,
592
+ perBlockAllocationMultiplier: this.config.perBlockAllocationMultiplier,
539
593
  };
540
594
 
541
- // 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.
542
598
  const buildResult = await this.buildSingleBlockWithCheckpointBuilder(
543
599
  checkpointBuilder,
544
600
  pendingTxs,
@@ -550,14 +606,16 @@ export class CheckpointProposalJob implements Traceable {
550
606
  // If any txs failed during execution, drop them from the mempool so we don't pick them up again
551
607
  await this.dropFailedTxsFromP2P(buildResult.failedTxs);
552
608
 
553
- // Check if we have created a block with enough txs. If there were invalid txs in the pool, or if execution took
554
- // too long, then we may not get to minTxsPerBlock after executing public functions.
555
- const minValidTxs = this.config.minValidTxsPerBlock ?? minTxs;
556
- const numTxs = buildResult.status === 'no-valid-txs' ? 0 : buildResult.numTxs;
557
- if (buildResult.status === 'no-valid-txs' || (!forceCreate && numTxs < minValidTxs)) {
609
+ if (buildResult.status === 'insufficient-valid-txs') {
558
610
  this.log.warn(
559
611
  `Block ${blockNumber} at index ${indexWithinCheckpoint} on slot ${this.slot} has too few valid txs to be proposed`,
560
- { 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
+ },
561
619
  );
562
620
  this.eventEmitter.emit('block-build-failed', { reason: `Insufficient valid txs`, slot: this.slot });
563
621
  this.metrics.recordBlockProposalFailed('insufficient_valid_txs');
@@ -565,7 +623,7 @@ export class CheckpointProposalJob implements Traceable {
565
623
  }
566
624
 
567
625
  // Block creation succeeded, emit stats and metrics
568
- const { publicGas, block, publicProcessorDuration, usedTxs, usedTxBlobFields, blockBuildDuration } = buildResult;
626
+ const { block, publicProcessorDuration, usedTxs, blockBuildDuration, numTxs } = buildResult;
569
627
 
570
628
  const blockStats = {
571
629
  eventName: 'l2-block-built',
@@ -576,7 +634,7 @@ export class CheckpointProposalJob implements Traceable {
576
634
 
577
635
  const blockHash = await block.hash();
578
636
  const txHashes = block.body.txEffects.map(tx => tx.txHash);
579
- const manaPerSec = publicGas.l2Gas / (blockBuildDuration / 1000);
637
+ const manaPerSec = block.header.totalManaUsed.toNumberUnsafe() / (blockBuildDuration / 1000);
580
638
 
581
639
  this.log.info(
582
640
  `Built block ${block.number} at checkpoint ${this.checkpointNumber} for slot ${this.slot} with ${numTxs} txs`,
@@ -584,9 +642,9 @@ export class CheckpointProposalJob implements Traceable {
584
642
  );
585
643
 
586
644
  this.eventEmitter.emit('block-proposed', { blockNumber: block.number, slot: this.slot });
587
- this.metrics.recordBuiltBlock(blockBuildDuration, publicGas.l2Gas);
645
+ this.metrics.recordBuiltBlock(blockBuildDuration, block.header.totalManaUsed.toNumberUnsafe());
588
646
 
589
- return { block, usedTxs, remainingBlobFields: maxBlobFieldsForTxs - usedTxBlobFields };
647
+ return { block, usedTxs };
590
648
  } catch (err: any) {
591
649
  this.eventEmitter.emit('block-build-failed', { reason: err.message, slot: this.slot });
592
650
  this.log.error(`Error building block`, err, { blockNumber, slot: this.slot });
@@ -596,13 +654,13 @@ export class CheckpointProposalJob implements Traceable {
596
654
  }
597
655
  }
598
656
 
599
- /** Uses the checkpoint builder to build a block, catching specific txs */
657
+ /** Uses the checkpoint builder to build a block, catching InsufficientValidTxsError. */
600
658
  private async buildSingleBlockWithCheckpointBuilder(
601
659
  checkpointBuilder: CheckpointBuilder,
602
660
  pendingTxs: AsyncIterable<Tx>,
603
661
  blockNumber: BlockNumber,
604
662
  blockTimestamp: bigint,
605
- blockBuilderOptions: PublicProcessorLimits,
663
+ blockBuilderOptions: BlockBuilderOptions,
606
664
  ) {
607
665
  try {
608
666
  const workTimer = new Timer();
@@ -610,8 +668,12 @@ export class CheckpointProposalJob implements Traceable {
610
668
  const blockBuildDuration = workTimer.ms();
611
669
  return { ...result, blockBuildDuration, status: 'success' as const };
612
670
  } catch (err: unknown) {
613
- if (isErrorClass(err, NoValidTxsError)) {
614
- 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
+ };
615
677
  }
616
678
  throw err;
617
679
  }
@@ -624,7 +686,7 @@ export class CheckpointProposalJob implements Traceable {
624
686
  blockNumber: BlockNumber;
625
687
  indexWithinCheckpoint: IndexWithinCheckpoint;
626
688
  buildDeadline: Date | undefined;
627
- }): Promise<{ canStartBuilding: boolean; availableTxs: number }> {
689
+ }): Promise<{ canStartBuilding: boolean; availableTxs: number; minTxs: number }> {
628
690
  const { indexWithinCheckpoint, blockNumber, buildDeadline, forceCreate } = opts;
629
691
 
630
692
  // We only allow a block with 0 txs in the first block of the checkpoint
@@ -641,7 +703,7 @@ export class CheckpointProposalJob implements Traceable {
641
703
  // If we're past deadline, or we have no deadline, give up
642
704
  const now = this.dateProvider.nowAsDate();
643
705
  if (startBuildingDeadline === undefined || now >= startBuildingDeadline) {
644
- return { canStartBuilding: false, availableTxs: availableTxs };
706
+ return { canStartBuilding: false, availableTxs, minTxs };
645
707
  }
646
708
 
647
709
  // Wait a bit before checking again
@@ -650,11 +712,11 @@ export class CheckpointProposalJob implements Traceable {
650
712
  `Waiting for enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.slot} (have ${availableTxs} but need ${minTxs})`,
651
713
  { blockNumber, slot: this.slot, indexWithinCheckpoint },
652
714
  );
653
- await sleep(TXS_POLLING_MS);
715
+ await this.waitForTxsPollingInterval();
654
716
  availableTxs = await this.p2pClient.getPendingTxCount();
655
717
  }
656
718
 
657
- return { canStartBuilding: true, availableTxs };
719
+ return { canStartBuilding: true, availableTxs, minTxs };
658
720
  }
659
721
 
660
722
  /**
@@ -706,11 +768,28 @@ export class CheckpointProposalJob implements Traceable {
706
768
 
707
769
  collectedAttestationsCount = attestations.length;
708
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
+
709
783
  // Rollup contract requires that the signatures are provided in the order of the committee
710
- const sorted = orderAttestations(attestations, committee);
784
+ const sorted = orderAttestations(trimmed, committee);
711
785
 
712
786
  // Manipulate the attestations if we've been configured to do so
713
- 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
+ ) {
714
793
  return this.manipulateAttestations(proposal.slotNumber, epoch, seed, committee, sorted);
715
794
  }
716
795
 
@@ -739,7 +818,11 @@ export class CheckpointProposalJob implements Traceable {
739
818
  this.epochCache.computeProposerIndex(slotNumber, epoch, seed, BigInt(committee.length)),
740
819
  );
741
820
 
742
- if (this.config.injectFakeAttestation) {
821
+ if (
822
+ this.config.injectFakeAttestation ||
823
+ this.config.injectHighSValueAttestation ||
824
+ this.config.injectUnrecoverableSignatureAttestation
825
+ ) {
743
826
  // Find non-empty attestations that are not from the proposer
744
827
  const nonProposerIndices: number[] = [];
745
828
  for (let i = 0; i < attestations.length; i++) {
@@ -749,8 +832,20 @@ export class CheckpointProposalJob implements Traceable {
749
832
  }
750
833
  if (nonProposerIndices.length > 0) {
751
834
  const targetIndex = nonProposerIndices[randomInt(nonProposerIndices.length)];
752
- this.log.warn(`Injecting fake attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`);
753
- 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
+ }
754
849
  }
755
850
  return new CommitteeAttestationsAndSigners(attestations);
756
851
  }
@@ -779,16 +874,20 @@ export class CheckpointProposalJob implements Traceable {
779
874
  const failedTxData = failedTxs.map(fail => fail.tx);
780
875
  const failedTxHashes = failedTxData.map(tx => tx.getTxHash());
781
876
  this.log.verbose(`Dropping failed txs ${failedTxHashes.join(', ')}`);
782
- await this.p2pClient.deleteTxs(failedTxHashes);
877
+ await this.p2pClient.handleFailedExecution(failedTxHashes);
783
878
  }
784
879
 
785
880
  /**
786
881
  * Adds the proposed block to the archiver so it's available via P2P.
787
882
  * Gossip doesn't echo messages back to the sender, so the proposer's archiver/world-state
788
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.
789
888
  */
790
889
  private async syncProposedBlockToArchiver(block: L2Block): Promise<void> {
791
- if (this.config.skipPushProposedBlocksToArchiver !== false) {
890
+ if (this.config.skipPushProposedBlocksToArchiver !== false || this.config.fishermanMode) {
792
891
  this.log.warn(`Skipping push of proposed block ${block.number} to archiver`, {
793
892
  blockNumber: block.number,
794
893
  slot: block.header.globalVariables.slotNumber,
@@ -821,7 +920,7 @@ export class CheckpointProposalJob implements Traceable {
821
920
  slot: this.slot,
822
921
  feeAnalysisId: feeAnalysis?.id,
823
922
  });
824
- this.metrics.recordBlockProposalFailed('block_build_failed');
923
+ this.metrics.recordCheckpointProposalFailed('block_build_failed');
825
924
  }
826
925
 
827
926
  this.publisher.clearPendingRequests();
@@ -857,6 +956,11 @@ export class CheckpointProposalJob implements Traceable {
857
956
  await sleepUntil(new Date(targetTimestamp * 1000), this.dateProvider.nowAsDate());
858
957
  }
859
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
+
860
964
  private getSlotStartBuildTimestamp(): number {
861
965
  return getSlotStartBuildTimestamp(this.slot, this.l1Constants);
862
966
  }
@@ -2,7 +2,6 @@ import type { SlotNumber } from '@aztec/foundation/branded-types';
2
2
  import type { EthAddress } from '@aztec/foundation/eth-address';
3
3
  import type { Logger } from '@aztec/foundation/log';
4
4
  import type { SlasherClientInterface } from '@aztec/slasher';
5
- import { getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
6
5
  import type { ResolvedSequencerConfig } from '@aztec/stdlib/interfaces/server';
7
6
  import type { ValidatorClient } from '@aztec/validator-client';
8
7
  import { DutyAlreadySignedError } from '@aztec/validator-ha-signer/errors';
@@ -18,7 +17,6 @@ import type { SequencerRollupConstants } from './types.js';
18
17
  * Handles governance and slashing voting for a given slot.
19
18
  */
20
19
  export class CheckpointVoter {
21
- private slotTimestamp: bigint;
22
20
  private governanceSigner: (msg: TypedDataDefinition) => Promise<`0x${string}`>;
23
21
  private slashingSigner: (msg: TypedDataDefinition) => Promise<`0x${string}`>;
24
22
 
@@ -33,8 +31,6 @@ export class CheckpointVoter {
33
31
  private readonly metrics: SequencerMetrics,
34
32
  private readonly log: Logger,
35
33
  ) {
36
- this.slotTimestamp = getTimestampForSlot(this.slot, this.l1Constants);
37
-
38
34
  // Create separate signers with appropriate duty contexts for governance and slashing votes
39
35
  // These use HA protection to ensure only one node signs per slot/duty
40
36
  const governanceContext: SigningContext = { slot: this.slot, dutyType: DutyType.GOVERNANCE_VOTE };
@@ -77,7 +73,6 @@ export class CheckpointVoter {
77
73
  return await this.publisher.enqueueGovernanceCastSignal(
78
74
  governanceProposerPayload,
79
75
  this.slot,
80
- this.slotTimestamp,
81
76
  this.attestorAddress,
82
77
  this.governanceSigner,
83
78
  );
@@ -108,13 +103,7 @@ export class CheckpointVoter {
108
103
 
109
104
  this.metrics.recordSlashingAttempt(actions.length);
110
105
 
111
- return await this.publisher.enqueueSlashingActions(
112
- actions,
113
- this.slot,
114
- this.slotTimestamp,
115
- this.attestorAddress,
116
- this.slashingSigner,
117
- );
106
+ return await this.publisher.enqueueSlashingActions(actions, this.slot, this.attestorAddress, this.slashingSigner);
118
107
  } catch (err) {
119
108
  if (err instanceof DutyAlreadySignedError) {
120
109
  this.log.info(`Slashing vote already signed by another node`, {