@aztec/sequencer-client 0.0.1-commit.b655e406 → 0.0.1-commit.fce3e4f

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 (54) hide show
  1. package/dest/client/index.d.ts +1 -1
  2. package/dest/client/sequencer-client.d.ts +1 -1
  3. package/dest/client/sequencer-client.d.ts.map +1 -1
  4. package/dest/client/sequencer-client.js +2 -1
  5. package/dest/config.d.ts +1 -1
  6. package/dest/config.d.ts.map +1 -1
  7. package/dest/config.js +9 -0
  8. package/dest/global_variable_builder/global_builder.d.ts +3 -6
  9. package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
  10. package/dest/global_variable_builder/global_builder.js +9 -6
  11. package/dest/global_variable_builder/index.d.ts +1 -1
  12. package/dest/index.d.ts +1 -1
  13. package/dest/publisher/config.d.ts +3 -1
  14. package/dest/publisher/config.d.ts.map +1 -1
  15. package/dest/publisher/config.js +5 -0
  16. package/dest/publisher/index.d.ts +1 -1
  17. package/dest/publisher/sequencer-publisher-factory.d.ts +1 -1
  18. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  19. package/dest/publisher/sequencer-publisher-metrics.d.ts +1 -1
  20. package/dest/publisher/sequencer-publisher-metrics.d.ts.map +1 -1
  21. package/dest/publisher/sequencer-publisher.d.ts +29 -23
  22. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  23. package/dest/publisher/sequencer-publisher.js +108 -57
  24. package/dest/sequencer/block_builder.d.ts +1 -1
  25. package/dest/sequencer/block_builder.d.ts.map +1 -1
  26. package/dest/sequencer/block_builder.js +10 -6
  27. package/dest/sequencer/config.d.ts +1 -1
  28. package/dest/sequencer/errors.d.ts +1 -1
  29. package/dest/sequencer/errors.d.ts.map +1 -1
  30. package/dest/sequencer/index.d.ts +1 -1
  31. package/dest/sequencer/metrics.d.ts +11 -2
  32. package/dest/sequencer/metrics.d.ts.map +1 -1
  33. package/dest/sequencer/metrics.js +38 -0
  34. package/dest/sequencer/sequencer.d.ts +17 -25
  35. package/dest/sequencer/sequencer.d.ts.map +1 -1
  36. package/dest/sequencer/sequencer.js +169 -46
  37. package/dest/sequencer/timetable.d.ts +1 -1
  38. package/dest/sequencer/timetable.d.ts.map +1 -1
  39. package/dest/sequencer/utils.d.ts +1 -1
  40. package/dest/test/index.d.ts +1 -1
  41. package/dest/tx_validator/nullifier_cache.d.ts +1 -1
  42. package/dest/tx_validator/nullifier_cache.d.ts.map +1 -1
  43. package/dest/tx_validator/tx_validator_factory.d.ts +2 -2
  44. package/dest/tx_validator/tx_validator_factory.d.ts.map +1 -1
  45. package/package.json +31 -30
  46. package/src/client/sequencer-client.ts +2 -2
  47. package/src/config.ts +10 -0
  48. package/src/global_variable_builder/global_builder.ts +12 -8
  49. package/src/publisher/config.ts +8 -0
  50. package/src/publisher/sequencer-publisher-factory.ts +2 -1
  51. package/src/publisher/sequencer-publisher.ts +125 -70
  52. package/src/sequencer/block_builder.ts +10 -6
  53. package/src/sequencer/metrics.ts +51 -2
  54. package/src/sequencer/sequencer.ts +204 -60
@@ -1,7 +1,8 @@
1
1
  import { L2Block } from '@aztec/aztec.js/block';
2
- import { BLOBS_PER_BLOCK, FIELDS_PER_BLOB, INITIAL_L2_BLOCK_NUM } from '@aztec/constants';
2
+ import { BLOBS_PER_CHECKPOINT, FIELDS_PER_BLOB, INITIAL_L2_BLOCK_NUM } from '@aztec/constants';
3
3
  import type { EpochCache } from '@aztec/epoch-cache';
4
4
  import { FormattedViemError, NoCommitteeError, type RollupContract } from '@aztec/ethereum';
5
+ import { EpochNumber, SlotNumber } from '@aztec/foundation/branded-types';
5
6
  import { omit, pick } from '@aztec/foundation/collection';
6
7
  import { randomInt } from '@aztec/foundation/crypto';
7
8
  import { EthAddress } from '@aztec/foundation/eth-address';
@@ -17,6 +18,7 @@ import {
17
18
  CommitteeAttestation,
18
19
  CommitteeAttestationsAndSigners,
19
20
  type L2BlockSource,
21
+ MaliciousCommitteeAttestationsAndSigners,
20
22
  type ValidateBlockResult,
21
23
  } from '@aztec/stdlib/block';
22
24
  import { type L1RollupConstants, getSlotAtTimestamp, getSlotStartBuildTimestamp } from '@aztec/stdlib/epoch-helpers';
@@ -60,7 +62,7 @@ export type SequencerEvents = {
60
62
  oldState: SequencerState;
61
63
  newState: SequencerState;
62
64
  secondsIntoSlot?: number;
63
- slotNumber?: bigint;
65
+ slotNumber?: SlotNumber;
64
66
  }) => void;
65
67
  ['proposer-rollup-check-failed']: (args: { reason: string }) => void;
66
68
  ['tx-count-check-failed']: (args: { minTxs: number; availableTxs: number }) => void;
@@ -99,7 +101,10 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
99
101
  private governanceProposerPayload: EthAddress | undefined;
100
102
 
101
103
  /** The last slot for which we attempted to vote when sync failed, to prevent duplicate attempts. */
102
- private lastSlotForVoteWhenSyncFailed: bigint | undefined;
104
+ private lastSlotForVoteWhenSyncFailed: SlotNumber | undefined;
105
+
106
+ /** The last slot for which we built a validation block in fisherman mode, to prevent duplicate attempts. */
107
+ private lastSlotForValidationBlock: SlotNumber | undefined;
103
108
 
104
109
  /** The maximum number of seconds that the sequencer can be into a slot to transition to a particular state. */
105
110
  protected timetable!: SequencerTimetable;
@@ -132,6 +137,11 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
132
137
  ) {
133
138
  super();
134
139
 
140
+ // Add [FISHERMAN] prefix to logger if in fisherman mode
141
+ if (this.config.fishermanMode) {
142
+ this.log = log.createChild('[FISHERMAN]');
143
+ }
144
+
135
145
  this.metrics = new SequencerMetrics(telemetry, this.rollupContract, 'Sequencer');
136
146
  // Initialize config
137
147
  this.updateConfig(this.config);
@@ -288,28 +298,55 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
288
298
  this.setState(SequencerState.PROPOSER_CHECK, slot);
289
299
  const [canPropose, proposer] = await this.checkCanPropose(slot);
290
300
 
291
- // If we are not a proposer, check if we should invalidate a invalid block, and bail
301
+ // If we are not a proposer check if we should invalidate a invalid block, and bail
292
302
  if (!canPropose) {
293
303
  await this.considerInvalidatingBlock(syncedTo, slot);
294
304
  return;
295
305
  }
296
306
 
307
+ // In fisherman mode, check if we've already validated this slot to prevent duplicate attempts
308
+ if (this.config.fishermanMode) {
309
+ if (this.lastSlotForValidationBlock === slot) {
310
+ this.log.trace(`Already validated block building for slot ${slot} (skipping)`, { slot });
311
+ return;
312
+ }
313
+ this.log.debug(
314
+ `Building validation block for slot ${slot} (actual proposer: ${proposer?.toString() ?? 'none'})`,
315
+ { slot, proposer: proposer?.toString() },
316
+ );
317
+ // Mark this slot as being validated
318
+ this.lastSlotForValidationBlock = slot;
319
+ }
320
+
297
321
  // Check that the slot is not taken by a block already (should never happen, since only us can propose for this slot)
298
322
  if (syncedTo.block && syncedTo.block.header.getSlot() >= slot) {
299
323
  this.log.warn(
300
324
  `Cannot propose block at next L2 slot ${slot} since that slot was taken by block ${syncedTo.blockNumber}`,
301
325
  { ...syncLogData, block: syncedTo.block.header.toInspect() },
302
326
  );
327
+ this.metrics.recordBlockProposalPrecheckFailed('slot_already_taken');
303
328
  return;
304
329
  }
305
330
 
306
331
  // We now need to get ourselves a publisher.
307
332
  // The returned attestor will be the one we provided if we provided one.
308
333
  // Otherwise it will be a valid attestor for the returned publisher.
309
- const { attestorAddress, publisher } = await this.publisherFactory.create(proposer);
334
+ // In fisherman mode, pass undefined to use the fisherman's own keystore instead of the actual proposer's
335
+ const { attestorAddress, publisher } = await this.publisherFactory.create(
336
+ this.config.fishermanMode ? undefined : proposer,
337
+ );
310
338
  this.log.verbose(`Created publisher at address ${publisher.getSenderAddress()} for attestor ${attestorAddress}`);
311
339
  this.publisher = publisher;
312
340
 
341
+ // In fisherman mode, set the actual proposer's address for simulations
342
+ if (this.config.fishermanMode) {
343
+ if (proposer) {
344
+ publisher.setProposerAddressForSimulation(proposer);
345
+ this.log.debug(`Set proposer address ${proposer} for simulation in fisherman mode`);
346
+ }
347
+ }
348
+
349
+ // Get proposer credentials
313
350
  const coinbase = this.validatorClient!.getCoinbaseForAttestor(attestorAddress);
314
351
  const feeRecipient = this.validatorClient!.getFeeRecipientForAttestor(attestorAddress);
315
352
 
@@ -330,6 +367,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
330
367
  syncLogData,
331
368
  );
332
369
  this.emit('proposer-rollup-check-failed', { reason: 'Rollup contract check failed' });
370
+ this.metrics.recordBlockProposalPrecheckFailed('rollup_contract_check_failed');
333
371
  return;
334
372
  } else if (canProposeCheck.slot !== slot) {
335
373
  this.log.warn(
@@ -337,13 +375,15 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
337
375
  { ...syncLogData, rollup: canProposeCheck, newBlockNumber, expectedSlot: slot },
338
376
  );
339
377
  this.emit('proposer-rollup-check-failed', { reason: 'Slot mismatch' });
378
+ this.metrics.recordBlockProposalPrecheckFailed('slot_mismatch');
340
379
  return;
341
- } else if (canProposeCheck.blockNumber !== BigInt(newBlockNumber)) {
380
+ } else if (canProposeCheck.checkpointNumber !== BigInt(newBlockNumber)) {
342
381
  this.log.warn(
343
- `Cannot propose block due to block mismatch with rollup contract (this can be caused by a pending archiver sync). Expected block ${newBlockNumber} but got ${canProposeCheck.blockNumber}.`,
382
+ `Cannot propose block due to block mismatch with rollup contract (this can be caused by a pending archiver sync). Expected block ${newBlockNumber} but got ${canProposeCheck.checkpointNumber}.`,
344
383
  { ...syncLogData, rollup: canProposeCheck, newBlockNumber, expectedSlot: slot },
345
384
  );
346
385
  this.emit('proposer-rollup-check-failed', { reason: 'Block mismatch' });
386
+ this.metrics.recordBlockProposalPrecheckFailed('block_number_mismatch');
347
387
  return;
348
388
  }
349
389
 
@@ -357,6 +397,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
357
397
  );
358
398
 
359
399
  // Enqueue governance and slashing votes (returns promises that will be awaited later)
400
+ // In fisherman mode, we simulate slashing but don't actually publish to L1
360
401
  const votesPromises = this.enqueueGovernanceAndSlashingVotes(
361
402
  publisher,
362
403
  attestorAddress,
@@ -371,6 +412,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
371
412
 
372
413
  // Actual block building
373
414
  this.setState(SequencerState.INITIALIZING_PROPOSAL, slot);
415
+ this.metrics.incOpenSlot(slot, proposer?.toString() ?? 'unknown');
374
416
  const block: L2Block | undefined = await this.tryBuildBlockAndEnqueuePublish(
375
417
  slot,
376
418
  proposer,
@@ -384,15 +426,39 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
384
426
  // Wait until the voting promises have resolved, so all requests are enqueued
385
427
  await Promise.all(votesPromises);
386
428
 
387
- // And send the tx to L1
388
- const l1Response = await publisher.sendRequests();
389
- const proposedBlock = l1Response?.successfulActions.find(a => a === 'propose');
390
- if (proposedBlock) {
391
- this.lastBlockPublished = block;
392
- this.emit('block-published', { blockNumber: newBlockNumber, slot: Number(slot) });
393
- await this.metrics.incFilledSlot(publisher.getSenderAddress().toString(), coinbase);
394
- } else if (block) {
395
- this.emit('block-publish-failed', l1Response ?? {});
429
+ // In fisherman mode, we don't publish to L1
430
+ if (this.config.fishermanMode) {
431
+ // Clear pending requests
432
+ publisher.clearPendingRequests();
433
+
434
+ if (block) {
435
+ this.log.info(`Validation block building SUCCEEDED for slot ${slot}`, {
436
+ blockNumber: newBlockNumber,
437
+ slot: Number(slot),
438
+ archive: block.archive.toString(),
439
+ txCount: block.body.txEffects.length,
440
+ });
441
+ this.lastBlockPublished = block;
442
+ this.metrics.recordBlockProposalSuccess();
443
+ } else {
444
+ // Block building failed in fisherman mode
445
+ this.log.warn(`Validation block building FAILED for slot ${slot}`, {
446
+ blockNumber: newBlockNumber,
447
+ slot: Number(slot),
448
+ });
449
+ this.metrics.recordBlockProposalFailed('block_build_failed');
450
+ }
451
+ } else {
452
+ // Normal mode: send the tx to L1
453
+ const l1Response = await publisher.sendRequests();
454
+ const proposedBlock = l1Response?.successfulActions.find(a => a === 'propose');
455
+ if (proposedBlock) {
456
+ this.lastBlockPublished = block;
457
+ this.emit('block-published', { blockNumber: newBlockNumber, slot: Number(slot) });
458
+ await this.metrics.incFilledSlot(publisher.getSenderAddress().toString(), coinbase);
459
+ } else if (block) {
460
+ this.emit('block-publish-failed', l1Response ?? {});
461
+ }
396
462
  }
397
463
 
398
464
  this.setState(SequencerState.IDLE, undefined);
@@ -400,7 +466,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
400
466
 
401
467
  /** Tries building a block proposal, and if successful, enqueues it for publishing. */
402
468
  private async tryBuildBlockAndEnqueuePublish(
403
- slot: bigint,
469
+ slot: SlotNumber,
404
470
  proposer: EthAddress | undefined,
405
471
  newBlockNumber: number,
406
472
  publisher: SequencerPublisher,
@@ -421,6 +487,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
421
487
  ...newGlobalVariables,
422
488
  timestamp: newGlobalVariables.timestamp,
423
489
  lastArchiveRoot: chainTipArchive,
490
+ blockHeadersHash: Fr.ZERO,
424
491
  contentCommitment: ContentCommitment.empty(),
425
492
  totalManaUsed: Fr.ZERO,
426
493
  });
@@ -448,6 +515,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
448
515
  } else {
449
516
  this.log.error(`Error building/enqueuing block`, err, { blockNumber: newBlockNumber, slot });
450
517
  }
518
+ this.metrics.recordBlockProposalFailed(err.name || 'unknown_error');
451
519
  }
452
520
  } else {
453
521
  this.log.verbose(
@@ -455,6 +523,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
455
523
  { chainTipArchive, blockNumber: newBlockNumber, slot },
456
524
  );
457
525
  this.emit('tx-count-check-failed', { minTxs: this.minTxsPerBlock, availableTxs: pendingTxCount });
526
+ this.metrics.recordBlockProposalFailed('insufficient_txs');
458
527
  }
459
528
  return block;
460
529
  }
@@ -485,13 +554,13 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
485
554
  * @param slotNumber - The current slot number.
486
555
  * @param force - Whether to force the transition even if the sequencer is stopped.
487
556
  */
488
- setState(proposedState: SequencerStateWithSlot, slotNumber: bigint, opts?: { force?: boolean }): void;
557
+ setState(proposedState: SequencerStateWithSlot, slotNumber: SlotNumber, opts?: { force?: boolean }): void;
489
558
  setState(
490
559
  proposedState: Exclude<SequencerState, SequencerStateWithSlot>,
491
560
  slotNumber?: undefined,
492
561
  opts?: { force?: boolean },
493
562
  ): void;
494
- setState(proposedState: SequencerState, slotNumber: bigint | undefined, opts: { force?: boolean } = {}): void {
563
+ setState(proposedState: SequencerState, slotNumber: SlotNumber | undefined, opts: { force?: boolean } = {}): void {
495
564
  if (this.state === SequencerState.STOPPING && proposedState !== SequencerState.STOPPED && !opts.force) {
496
565
  this.log.warn(`Cannot set sequencer to ${proposedState} as it is stopping.`);
497
566
  throw new SequencerInterruptedError();
@@ -532,7 +601,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
532
601
  await this.p2pClient.deleteTxs(failedTxHashes);
533
602
  }
534
603
 
535
- protected getBlockBuilderOptions(slot: number): PublicProcessorLimits {
604
+ protected getBlockBuilderOptions(slot: SlotNumber): PublicProcessorLimits {
536
605
  // Deadline for processing depends on whether we're proposing a block
537
606
  const secondsIntoSlot = this.getSecondsIntoSlot(slot);
538
607
  const processingEndTimeWithinSlot = this.timetable.getBlockProposalExecTimeEnd(secondsIntoSlot);
@@ -545,7 +614,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
545
614
  maxTransactions: this.maxTxsPerBlock,
546
615
  maxBlockSize: this.maxBlockSizeInBytes,
547
616
  maxBlockGas: this.maxBlockGas,
548
- maxBlobFields: BLOBS_PER_BLOCK * FIELDS_PER_BLOB,
617
+ maxBlobFields: BLOBS_PER_CHECKPOINT * FIELDS_PER_BLOB,
549
618
  deadline,
550
619
  };
551
620
  }
@@ -575,14 +644,14 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
575
644
  await publisher.validateBlockHeader(proposalHeader, invalidateBlock);
576
645
 
577
646
  const blockNumber = newGlobalVariables.blockNumber;
578
- const slot = proposalHeader.slotNumber.toBigInt();
647
+ const slot = proposalHeader.slotNumber;
579
648
  const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(blockNumber);
580
649
 
581
650
  const workTimer = new Timer();
582
651
  this.setState(SequencerState.CREATING_BLOCK, slot);
583
652
 
584
653
  try {
585
- const blockBuilderOptions = this.getBlockBuilderOptions(Number(slot));
654
+ const blockBuilderOptions = this.getBlockBuilderOptions(slot);
586
655
  const buildBlockRes = await this.blockBuilder.buildBlock(
587
656
  pendingTxs,
588
657
  l1ToL2Messages,
@@ -630,19 +699,28 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
630
699
  },
631
700
  );
632
701
 
633
- this.log.debug('Collecting attestations');
634
- const attestations = await this.collectAttestations(block, usedTxs, proposerAddress);
635
- if (attestations !== undefined) {
636
- this.log.verbose(`Collected ${attestations.length} attestations`, { blockHash, blockNumber });
702
+ // In fisherman mode, skip attestation collection
703
+ let attestationsAndSigners: CommitteeAttestationsAndSigners;
704
+ if (this.config.fishermanMode) {
705
+ this.log.debug('Skipping attestation collection');
706
+ attestationsAndSigners = CommitteeAttestationsAndSigners.empty();
707
+ } else {
708
+ this.log.debug('Collecting attestations');
709
+ attestationsAndSigners = await this.collectAttestations(block, usedTxs, proposerAddress);
710
+ this.log.verbose(
711
+ `Collected ${attestationsAndSigners.attestations.length} attestations for block ${blockNumber} at slot ${slot}`,
712
+ { blockHash, blockNumber, slot },
713
+ );
637
714
  }
638
715
 
639
- const attestationsAndSigners = new CommitteeAttestationsAndSigners(attestations ?? []);
640
- const attestationsAndSignersSignature = this.validatorClient
641
- ? await this.validatorClient.signAttestationsAndSigners(
642
- attestationsAndSigners,
643
- proposerAddress ?? publisher.getSenderAddress(),
644
- )
645
- : Signature.empty();
716
+ // In fisherman mode, skip attestation signing
717
+ const attestationsAndSignersSignature =
718
+ this.config.fishermanMode || !this.validatorClient
719
+ ? Signature.empty()
720
+ : await this.validatorClient.signAttestationsAndSigners(
721
+ attestationsAndSigners,
722
+ proposerAddress ?? publisher.getSenderAddress(),
723
+ );
646
724
 
647
725
  await this.enqueuePublishL2Block(
648
726
  block,
@@ -668,8 +746,8 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
668
746
  block: L2Block,
669
747
  txs: Tx[],
670
748
  proposerAddress: EthAddress | undefined,
671
- ): Promise<CommitteeAttestation[] | undefined> {
672
- const { committee } = await this.epochCache.getCommittee(block.header.getSlot());
749
+ ): Promise<CommitteeAttestationsAndSigners> {
750
+ const { committee, seed, epoch } = await this.epochCache.getCommittee(block.slot);
673
751
 
674
752
  // We checked above that the committee is defined, so this should never happen.
675
753
  if (!committee) {
@@ -678,20 +756,18 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
678
756
 
679
757
  if (committee.length === 0) {
680
758
  this.log.verbose(`Attesting committee is empty`);
681
- return undefined;
759
+ return CommitteeAttestationsAndSigners.empty();
682
760
  } else {
683
761
  this.log.debug(`Attesting committee length is ${committee.length}`);
684
762
  }
685
763
 
686
764
  if (!this.validatorClient) {
687
- const msg = 'Missing validator client: Cannot collect attestations';
688
- this.log.error(msg);
689
- throw new Error(msg);
765
+ throw new Error('Missing validator client: Cannot collect attestations');
690
766
  }
691
767
 
692
768
  const numberOfRequiredAttestations = Math.floor((committee.length * 2) / 3) + 1;
693
769
 
694
- const slotNumber = block.header.globalVariables.slotNumber.toBigInt();
770
+ const slotNumber = block.header.globalVariables.slotNumber;
695
771
  this.setState(SequencerState.COLLECTING_ATTESTATIONS, slotNumber);
696
772
 
697
773
  this.log.debug('Creating block proposal for validators');
@@ -703,7 +779,6 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
703
779
  block.header.globalVariables.blockNumber,
704
780
  block.getCheckpointHeader(),
705
781
  block.archive.root,
706
- block.header.state,
707
782
  txs,
708
783
  proposerAddress,
709
784
  blockProposalOptions,
@@ -716,7 +791,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
716
791
  if (this.config.skipCollectingAttestations) {
717
792
  this.log.warn('Skipping attestation collection as per config (attesting with own keys only)');
718
793
  const attestations = await this.validatorClient?.collectOwnAttestations(proposal);
719
- return orderAttestations(attestations ?? [], committee);
794
+ return new CommitteeAttestationsAndSigners(orderAttestations(attestations ?? [], committee));
720
795
  }
721
796
 
722
797
  this.log.debug('Broadcasting block proposal to validators');
@@ -742,13 +817,13 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
742
817
 
743
818
  // note: the smart contract requires that the signatures are provided in the order of the committee
744
819
  const sorted = orderAttestations(attestations, committee);
745
- if (this.config.injectFakeAttestation) {
746
- const nonEmpty = sorted.filter(a => !a.signature.isEmpty());
747
- const randomIndex = randomInt(nonEmpty.length);
748
- this.log.warn(`Injecting fake attestation in block ${block.number}`);
749
- unfreeze(nonEmpty[randomIndex]).signature = Signature.random();
820
+
821
+ // manipulate the attestations if we've been configured to do so
822
+ if (this.config.injectFakeAttestation || this.config.shuffleAttestationOrdering) {
823
+ return this.manipulateAttestations(block, epoch, seed, committee, sorted);
750
824
  }
751
- return sorted;
825
+
826
+ return new CommitteeAttestationsAndSigners(sorted);
752
827
  } catch (err) {
753
828
  if (err && err instanceof AttestationTimeoutError) {
754
829
  collectedAttestationsCount = err.collectedCount;
@@ -759,6 +834,53 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
759
834
  }
760
835
  }
761
836
 
837
+ /** Breaks the attestations before publishing based on attack configs */
838
+ private manipulateAttestations(
839
+ block: L2Block,
840
+ epoch: EpochNumber,
841
+ seed: bigint,
842
+ committee: EthAddress[],
843
+ attestations: CommitteeAttestation[],
844
+ ) {
845
+ // Compute the proposer index in the committee, since we dont want to tweak it.
846
+ // Otherwise, the L1 rollup contract will reject the block outright.
847
+ const proposerIndex = Number(
848
+ this.epochCache.computeProposerIndex(block.slot, epoch, seed, BigInt(committee.length)),
849
+ );
850
+
851
+ if (this.config.injectFakeAttestation) {
852
+ // Find non-empty attestations that are not from the proposer
853
+ const nonProposerIndices: number[] = [];
854
+ for (let i = 0; i < attestations.length; i++) {
855
+ if (!attestations[i].signature.isEmpty() && i !== proposerIndex) {
856
+ nonProposerIndices.push(i);
857
+ }
858
+ }
859
+ if (nonProposerIndices.length > 0) {
860
+ const targetIndex = nonProposerIndices[randomInt(nonProposerIndices.length)];
861
+ this.log.warn(`Injecting fake attestation in block ${block.number} at index ${targetIndex}`);
862
+ unfreeze(attestations[targetIndex]).signature = Signature.random();
863
+ }
864
+ return new CommitteeAttestationsAndSigners(attestations);
865
+ }
866
+
867
+ if (this.config.shuffleAttestationOrdering) {
868
+ this.log.warn(`Shuffling attestation ordering in block ${block.number} (proposer index ${proposerIndex})`);
869
+
870
+ const shuffled = [...attestations];
871
+ const [i, j] = [(proposerIndex + 1) % shuffled.length, (proposerIndex + 2) % shuffled.length];
872
+ const valueI = shuffled[i];
873
+ const valueJ = shuffled[j];
874
+ shuffled[i] = valueJ;
875
+ shuffled[j] = valueI;
876
+
877
+ const signers = new CommitteeAttestationsAndSigners(attestations).getSigners();
878
+ return new MaliciousCommitteeAttestationsAndSigners(shuffled, signers);
879
+ }
880
+
881
+ return new CommitteeAttestationsAndSigners(attestations);
882
+ }
883
+
762
884
  /**
763
885
  * Publishes the L2Block to the rollup contract.
764
886
  * @param block - The L2Block to be published.
@@ -774,10 +896,10 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
774
896
  publisher: SequencerPublisher,
775
897
  ): Promise<void> {
776
898
  // Publishes new block to the network and awaits the tx to be mined
777
- this.setState(SequencerState.PUBLISHING_BLOCK, block.header.globalVariables.slotNumber.toBigInt());
899
+ this.setState(SequencerState.PUBLISHING_BLOCK, block.header.globalVariables.slotNumber);
778
900
 
779
901
  // Time out tx at the end of the slot
780
- const slot = block.header.globalVariables.slotNumber.toNumber();
902
+ const slot = block.header.globalVariables.slotNumber;
781
903
  const txTimeoutAt = new Date((this.getSlotStartBuildTimestamp(slot) + this.aztecSlotDuration) * 1000);
782
904
 
783
905
  const enqueued = await publisher.enqueueProposeL2Block(
@@ -799,7 +921,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
799
921
  * Returns whether all dependencies have caught up.
800
922
  * We don't check against the previous block submitted since it may have been reorg'd out.
801
923
  */
802
- protected async checkSync(args: { ts: bigint; slot: bigint }): Promise<
924
+ protected async checkSync(args: { ts: bigint; slot: SlotNumber }): Promise<
803
925
  | {
804
926
  block?: L2Block;
805
927
  blockNumber: number;
@@ -885,7 +1007,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
885
1007
  protected enqueueGovernanceAndSlashingVotes(
886
1008
  publisher: SequencerPublisher,
887
1009
  attestorAddress: EthAddress,
888
- slot: bigint,
1010
+ slot: SlotNumber,
889
1011
  timestamp: bigint,
890
1012
  ): [Promise<boolean> | undefined, Promise<boolean> | undefined] {
891
1013
  try {
@@ -905,7 +1027,18 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
905
1027
  const enqueueSlashingPromise = this.slasherClient
906
1028
  ? this.slasherClient
907
1029
  .getProposerActions(slot)
908
- .then(actions => publisher.enqueueSlashingActions(actions, slot, timestamp, attestorAddress, signerFn))
1030
+ .then(actions => {
1031
+ // Record metrics for fisherman mode
1032
+ if (this.config.fishermanMode && actions.length > 0) {
1033
+ this.log.debug(`Fisherman mode: simulating ${actions.length} slashing action(s) for slot ${slot}`, {
1034
+ slot,
1035
+ actionCount: actions.length,
1036
+ });
1037
+ this.metrics.recordSlashingAttempt(actions.length);
1038
+ }
1039
+ // Enqueue the actions to fully simulate L1 tx building (they won't be sent in fisherman mode)
1040
+ return publisher.enqueueSlashingActions(actions, slot, timestamp, attestorAddress, signerFn);
1041
+ })
909
1042
  .catch(err => {
910
1043
  this.log.error(`Error enqueuing slashing actions`, err, { slot });
911
1044
  return false;
@@ -923,8 +1056,9 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
923
1056
  * Checks if we are the proposer for the next slot.
924
1057
  * @returns True if we can propose, and the proposer address (undefined if anyone can propose)
925
1058
  */
926
- protected async checkCanPropose(slot: bigint): Promise<[boolean, EthAddress | undefined]> {
1059
+ protected async checkCanPropose(slot: SlotNumber): Promise<[boolean, EthAddress | undefined]> {
927
1060
  let proposer: EthAddress | undefined;
1061
+
928
1062
  try {
929
1063
  proposer = await this.epochCache.getProposerAttesterAddressInSlot(slot);
930
1064
  } catch (e) {
@@ -940,6 +1074,10 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
940
1074
  if (proposer === undefined) {
941
1075
  return [true, undefined];
942
1076
  }
1077
+ // In fisherman mode, just return the current proposer
1078
+ if (this.config.fishermanMode) {
1079
+ return [true, proposer];
1080
+ }
943
1081
 
944
1082
  const validatorAddresses = this.validatorClient!.getValidatorAddresses();
945
1083
  const weAreProposer = validatorAddresses.some(addr => addr.equals(proposer));
@@ -956,7 +1094,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
956
1094
  * Tries to vote on slashing actions and governance when the sync check fails but we're past the max time for initializing a proposal.
957
1095
  * This allows the sequencer to participate in governance/slashing votes even when it cannot build blocks.
958
1096
  */
959
- protected async tryVoteWhenSyncFails(args: { slot: bigint; ts: bigint }): Promise<void> {
1097
+ protected async tryVoteWhenSyncFails(args: { slot: SlotNumber; ts: bigint }): Promise<void> {
960
1098
  const { slot, ts } = args;
961
1099
 
962
1100
  // Prevent duplicate attempts in the same slot
@@ -1023,7 +1161,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
1023
1161
  */
1024
1162
  protected async considerInvalidatingBlock(
1025
1163
  syncedTo: NonNullable<Awaited<ReturnType<Sequencer['checkSync']>>>,
1026
- currentSlot: bigint,
1164
+ currentSlot: SlotNumber,
1027
1165
  ): Promise<void> {
1028
1166
  const { pendingChainValidationStatus, l1Timestamp } = syncedTo;
1029
1167
  if (pendingChainValidationStatus.valid) {
@@ -1084,14 +1222,20 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
1084
1222
  );
1085
1223
 
1086
1224
  publisher.enqueueInvalidateBlock(invalidateBlock);
1087
- await publisher.sendRequests();
1225
+
1226
+ if (!this.config.fishermanMode) {
1227
+ await publisher.sendRequests();
1228
+ } else {
1229
+ this.log.info('Invalidating block in fisherman mode, clearing pending requests');
1230
+ publisher.clearPendingRequests();
1231
+ }
1088
1232
  }
1089
1233
 
1090
- private getSlotStartBuildTimestamp(slotNumber: number | bigint): number {
1234
+ private getSlotStartBuildTimestamp(slotNumber: SlotNumber): number {
1091
1235
  return getSlotStartBuildTimestamp(slotNumber, this.l1Constants);
1092
1236
  }
1093
1237
 
1094
- private getSecondsIntoSlot(slotNumber: number | bigint): number {
1238
+ private getSecondsIntoSlot(slotNumber: SlotNumber): number {
1095
1239
  const slotStartTimestamp = this.getSlotStartBuildTimestamp(slotNumber);
1096
1240
  return Number((this.dateProvider.now() / 1000 - slotStartTimestamp).toFixed(3));
1097
1241
  }