@aztec/sequencer-client 2.1.7 → 2.1.8

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