@aztec/sequencer-client 3.0.0-nightly.20251111 → 3.0.0-nightly.20251113

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.
@@ -102,6 +102,9 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
102
102
  /** The last slot for which we attempted to vote when sync failed, to prevent duplicate attempts. */
103
103
  private lastSlotForVoteWhenSyncFailed: bigint | undefined;
104
104
 
105
+ /** The last slot for which we built a validation block in fisherman mode, to prevent duplicate attempts. */
106
+ private lastSlotForValidationBlock: bigint | undefined;
107
+
105
108
  /** The maximum number of seconds that the sequencer can be into a slot to transition to a particular state. */
106
109
  protected timetable!: SequencerTimetable;
107
110
  protected enforceTimeTable: boolean = false;
@@ -133,6 +136,11 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
133
136
  ) {
134
137
  super();
135
138
 
139
+ // Add [FISHERMAN] prefix to logger if in fisherman mode
140
+ if (this.config.fishermanMode) {
141
+ this.log = log.createChild('[FISHERMAN]');
142
+ }
143
+
136
144
  this.metrics = new SequencerMetrics(telemetry, this.rollupContract, 'Sequencer');
137
145
  // Initialize config
138
146
  this.updateConfig(this.config);
@@ -289,28 +297,55 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
289
297
  this.setState(SequencerState.PROPOSER_CHECK, slot);
290
298
  const [canPropose, proposer] = await this.checkCanPropose(slot);
291
299
 
292
- // If we are not a proposer, check if we should invalidate a invalid block, and bail
300
+ // If we are not a proposer check if we should invalidate a invalid block, and bail
293
301
  if (!canPropose) {
294
302
  await this.considerInvalidatingBlock(syncedTo, slot);
295
303
  return;
296
304
  }
297
305
 
306
+ // In fisherman mode, check if we've already validated this slot to prevent duplicate attempts
307
+ if (this.config.fishermanMode) {
308
+ if (this.lastSlotForValidationBlock === slot) {
309
+ this.log.trace(`Already validated block building for slot ${slot} (skipping)`, { slot });
310
+ return;
311
+ }
312
+ this.log.debug(
313
+ `Building validation block for slot ${slot} (actual proposer: ${proposer?.toString() ?? 'none'})`,
314
+ { slot, proposer: proposer?.toString() },
315
+ );
316
+ // Mark this slot as being validated
317
+ this.lastSlotForValidationBlock = slot;
318
+ }
319
+
298
320
  // Check that the slot is not taken by a block already (should never happen, since only us can propose for this slot)
299
321
  if (syncedTo.block && syncedTo.block.header.getSlot() >= slot) {
300
322
  this.log.warn(
301
323
  `Cannot propose block at next L2 slot ${slot} since that slot was taken by block ${syncedTo.blockNumber}`,
302
324
  { ...syncLogData, block: syncedTo.block.header.toInspect() },
303
325
  );
326
+ this.metrics.recordBlockProposalPrecheckFailed('slot_already_taken');
304
327
  return;
305
328
  }
306
329
 
307
330
  // We now need to get ourselves a publisher.
308
331
  // The returned attestor will be the one we provided if we provided one.
309
332
  // Otherwise it will be a valid attestor for the returned publisher.
310
- const { attestorAddress, publisher } = await this.publisherFactory.create(proposer);
333
+ // In fisherman mode, pass undefined to use the fisherman's own keystore instead of the actual proposer's
334
+ const { attestorAddress, publisher } = await this.publisherFactory.create(
335
+ this.config.fishermanMode ? undefined : proposer,
336
+ );
311
337
  this.log.verbose(`Created publisher at address ${publisher.getSenderAddress()} for attestor ${attestorAddress}`);
312
338
  this.publisher = publisher;
313
339
 
340
+ // In fisherman mode, set the actual proposer's address for simulations
341
+ if (this.config.fishermanMode) {
342
+ if (proposer) {
343
+ publisher.setProposerAddressForSimulation(proposer);
344
+ this.log.debug(`Set proposer address ${proposer} for simulation in fisherman mode`);
345
+ }
346
+ }
347
+
348
+ // Get proposer credentials
314
349
  const coinbase = this.validatorClient!.getCoinbaseForAttestor(attestorAddress);
315
350
  const feeRecipient = this.validatorClient!.getFeeRecipientForAttestor(attestorAddress);
316
351
 
@@ -331,6 +366,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
331
366
  syncLogData,
332
367
  );
333
368
  this.emit('proposer-rollup-check-failed', { reason: 'Rollup contract check failed' });
369
+ this.metrics.recordBlockProposalPrecheckFailed('rollup_contract_check_failed');
334
370
  return;
335
371
  } else if (canProposeCheck.slot !== slot) {
336
372
  this.log.warn(
@@ -338,6 +374,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
338
374
  { ...syncLogData, rollup: canProposeCheck, newBlockNumber, expectedSlot: slot },
339
375
  );
340
376
  this.emit('proposer-rollup-check-failed', { reason: 'Slot mismatch' });
377
+ this.metrics.recordBlockProposalPrecheckFailed('slot_mismatch');
341
378
  return;
342
379
  } else if (canProposeCheck.blockNumber !== BigInt(newBlockNumber)) {
343
380
  this.log.warn(
@@ -345,6 +382,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
345
382
  { ...syncLogData, rollup: canProposeCheck, newBlockNumber, expectedSlot: slot },
346
383
  );
347
384
  this.emit('proposer-rollup-check-failed', { reason: 'Block mismatch' });
385
+ this.metrics.recordBlockProposalPrecheckFailed('block_number_mismatch');
348
386
  return;
349
387
  }
350
388
 
@@ -358,6 +396,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
358
396
  );
359
397
 
360
398
  // Enqueue governance and slashing votes (returns promises that will be awaited later)
399
+ // In fisherman mode, we simulate slashing but don't actually publish to L1
361
400
  const votesPromises = this.enqueueGovernanceAndSlashingVotes(
362
401
  publisher,
363
402
  attestorAddress,
@@ -385,15 +424,39 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
385
424
  // Wait until the voting promises have resolved, so all requests are enqueued
386
425
  await Promise.all(votesPromises);
387
426
 
388
- // And send the tx to L1
389
- const l1Response = await publisher.sendRequests();
390
- const proposedBlock = l1Response?.successfulActions.find(a => a === 'propose');
391
- if (proposedBlock) {
392
- this.lastBlockPublished = block;
393
- this.emit('block-published', { blockNumber: newBlockNumber, slot: Number(slot) });
394
- await this.metrics.incFilledSlot(publisher.getSenderAddress().toString(), coinbase);
395
- } else if (block) {
396
- this.emit('block-publish-failed', l1Response ?? {});
427
+ // In fisherman mode, we don't publish to L1
428
+ if (this.config.fishermanMode) {
429
+ // Clear pending requests
430
+ publisher.clearPendingRequests();
431
+
432
+ if (block) {
433
+ this.log.info(`Validation block building SUCCEEDED for slot ${slot}`, {
434
+ blockNumber: newBlockNumber,
435
+ slot: Number(slot),
436
+ archive: block.archive.toString(),
437
+ txCount: block.body.txEffects.length,
438
+ });
439
+ this.lastBlockPublished = block;
440
+ this.metrics.recordBlockProposalSuccess();
441
+ } else {
442
+ // Block building failed in fisherman mode
443
+ this.log.warn(`Validation block building FAILED for slot ${slot}`, {
444
+ blockNumber: newBlockNumber,
445
+ slot: Number(slot),
446
+ });
447
+ this.metrics.recordBlockProposalFailed('block_build_failed');
448
+ }
449
+ } else {
450
+ // Normal mode: send the tx to L1
451
+ const l1Response = await publisher.sendRequests();
452
+ const proposedBlock = l1Response?.successfulActions.find(a => a === 'propose');
453
+ if (proposedBlock) {
454
+ this.lastBlockPublished = block;
455
+ this.emit('block-published', { blockNumber: newBlockNumber, slot: Number(slot) });
456
+ await this.metrics.incFilledSlot(publisher.getSenderAddress().toString(), coinbase);
457
+ } else if (block) {
458
+ this.emit('block-publish-failed', l1Response ?? {});
459
+ }
397
460
  }
398
461
 
399
462
  this.setState(SequencerState.IDLE, undefined);
@@ -449,6 +512,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
449
512
  } else {
450
513
  this.log.error(`Error building/enqueuing block`, err, { blockNumber: newBlockNumber, slot });
451
514
  }
515
+ this.metrics.recordBlockProposalFailed(err.name || 'unknown_error');
452
516
  }
453
517
  } else {
454
518
  this.log.verbose(
@@ -456,6 +520,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
456
520
  { chainTipArchive, blockNumber: newBlockNumber, slot },
457
521
  );
458
522
  this.emit('tx-count-check-failed', { minTxs: this.minTxsPerBlock, availableTxs: pendingTxCount });
523
+ this.metrics.recordBlockProposalFailed('insufficient_txs');
459
524
  }
460
525
  return block;
461
526
  }
@@ -631,18 +696,28 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
631
696
  },
632
697
  );
633
698
 
634
- this.log.debug('Collecting attestations');
635
- const attestationsAndSigners = await this.collectAttestations(block, usedTxs, proposerAddress);
636
- this.log.verbose(
637
- `Collected ${attestationsAndSigners.attestations.length} attestations for block ${blockNumber} at slot ${slot}`,
638
- { blockHash, blockNumber, slot },
639
- );
699
+ // In fisherman mode, skip attestation collection
700
+ let attestationsAndSigners: CommitteeAttestationsAndSigners;
701
+ if (this.config.fishermanMode) {
702
+ this.log.debug('Skipping attestation collection');
703
+ attestationsAndSigners = CommitteeAttestationsAndSigners.empty();
704
+ } else {
705
+ this.log.debug('Collecting attestations');
706
+ attestationsAndSigners = await this.collectAttestations(block, usedTxs, proposerAddress);
707
+ this.log.verbose(
708
+ `Collected ${attestationsAndSigners.attestations.length} attestations for block ${blockNumber} at slot ${slot}`,
709
+ { blockHash, blockNumber, slot },
710
+ );
711
+ }
640
712
 
713
+ // In fisherman mode, skip attestation signing
641
714
  const attestationsAndSignersSignature =
642
- (await this.validatorClient?.signAttestationsAndSigners(
643
- attestationsAndSigners,
644
- proposerAddress ?? publisher.getSenderAddress(),
645
- )) ?? Signature.empty();
715
+ this.config.fishermanMode || !this.validatorClient
716
+ ? Signature.empty()
717
+ : await this.validatorClient.signAttestationsAndSigners(
718
+ attestationsAndSigners,
719
+ proposerAddress ?? publisher.getSenderAddress(),
720
+ );
646
721
 
647
722
  await this.enqueuePublishL2Block(
648
723
  block,
@@ -950,7 +1025,18 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
950
1025
  const enqueueSlashingPromise = this.slasherClient
951
1026
  ? this.slasherClient
952
1027
  .getProposerActions(slot)
953
- .then(actions => publisher.enqueueSlashingActions(actions, slot, timestamp, attestorAddress, signerFn))
1028
+ .then(actions => {
1029
+ // Record metrics for fisherman mode
1030
+ if (this.config.fishermanMode && actions.length > 0) {
1031
+ this.log.debug(`Fisherman mode: simulating ${actions.length} slashing action(s) for slot ${slot}`, {
1032
+ slot,
1033
+ actionCount: actions.length,
1034
+ });
1035
+ this.metrics.recordSlashingAttempt(actions.length);
1036
+ }
1037
+ // Enqueue the actions to fully simulate L1 tx building (they won't be sent in fisherman mode)
1038
+ return publisher.enqueueSlashingActions(actions, slot, timestamp, attestorAddress, signerFn);
1039
+ })
954
1040
  .catch(err => {
955
1041
  this.log.error(`Error enqueuing slashing actions`, err, { slot });
956
1042
  return false;
@@ -970,6 +1056,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
970
1056
  */
971
1057
  protected async checkCanPropose(slot: bigint): Promise<[boolean, EthAddress | undefined]> {
972
1058
  let proposer: EthAddress | undefined;
1059
+
973
1060
  try {
974
1061
  proposer = await this.epochCache.getProposerAttesterAddressInSlot(slot);
975
1062
  } catch (e) {
@@ -985,6 +1072,10 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
985
1072
  if (proposer === undefined) {
986
1073
  return [true, undefined];
987
1074
  }
1075
+ // In fisherman mode, just return the current proposer
1076
+ if (this.config.fishermanMode) {
1077
+ return [true, proposer];
1078
+ }
988
1079
 
989
1080
  const validatorAddresses = this.validatorClient!.getValidatorAddresses();
990
1081
  const weAreProposer = validatorAddresses.some(addr => addr.equals(proposer));
@@ -1129,7 +1220,13 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
1129
1220
  );
1130
1221
 
1131
1222
  publisher.enqueueInvalidateBlock(invalidateBlock);
1132
- await publisher.sendRequests();
1223
+
1224
+ if (!this.config.fishermanMode) {
1225
+ await publisher.sendRequests();
1226
+ } else {
1227
+ this.log.info('Invalidating block in fisherman mode, clearing pending requests');
1228
+ publisher.clearPendingRequests();
1229
+ }
1133
1230
  }
1134
1231
 
1135
1232
  private getSlotStartBuildTimestamp(slotNumber: number | bigint): number {