@aztec/validator-client 0.0.1-commit.8c0b8ff → 0.0.1-commit.8cb2d04d8

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.
package/src/validator.ts CHANGED
@@ -1,16 +1,11 @@
1
1
  import type { BlobClientInterface } from '@aztec/blob-client/client';
2
+ import { type Blob, getBlobsPerL1Block } from '@aztec/blob-lib';
2
3
  import type { EpochCache } from '@aztec/epoch-cache';
3
- import {
4
- BlockNumber,
5
- CheckpointNumber,
6
- EpochNumber,
7
- IndexWithinCheckpoint,
8
- SlotNumber,
9
- } from '@aztec/foundation/branded-types';
4
+ import { CheckpointNumber, EpochNumber, IndexWithinCheckpoint, SlotNumber } from '@aztec/foundation/branded-types';
10
5
  import { Fr } from '@aztec/foundation/curves/bn254';
11
6
  import type { EthAddress } from '@aztec/foundation/eth-address';
12
7
  import type { Signature } from '@aztec/foundation/eth-signature';
13
- import { type Logger, createLogger } from '@aztec/foundation/log';
8
+ import { type LogData, type Logger, createLogger } from '@aztec/foundation/log';
14
9
  import { RunningPromise } from '@aztec/foundation/running-promise';
15
10
  import { sleep } from '@aztec/foundation/sleep';
16
11
  import { DateProvider } from '@aztec/foundation/timer';
@@ -22,7 +17,6 @@ import type { AztecAddress } from '@aztec/stdlib/aztec-address';
22
17
  import type { CommitteeAttestationsAndSigners, L2BlockSink, L2BlockSource } from '@aztec/stdlib/block';
23
18
  import { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
24
19
  import type {
25
- CreateCheckpointProposalLastBlockData,
26
20
  ITxProvider,
27
21
  Validator,
28
22
  ValidatorClientFullConfig,
@@ -41,7 +35,11 @@ import type { CheckpointHeader } from '@aztec/stdlib/rollup';
41
35
  import type { BlockHeader, Tx } from '@aztec/stdlib/tx';
42
36
  import { AttestationTimeoutError } from '@aztec/stdlib/validators';
43
37
  import { type TelemetryClient, type Tracer, getTelemetryClient } from '@aztec/telemetry-client';
44
- import { createHASigner, createSignerFromSharedDb } from '@aztec/validator-ha-signer/factory';
38
+ import {
39
+ createHASigner,
40
+ createLocalSignerWithProtection,
41
+ createSignerFromSharedDb,
42
+ } from '@aztec/validator-ha-signer/factory';
45
43
  import { DutyType, type SigningContext, type SlashingProtectionDatabase } from '@aztec/validator-ha-signer/types';
46
44
  import type { ValidatorHASigner } from '@aztec/validator-ha-signer/validator-ha-signer';
47
45
 
@@ -98,9 +96,13 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
98
96
  private epochCache: EpochCache,
99
97
  private p2pClient: P2P,
100
98
  private proposalHandler: ProposalHandler,
99
+ private blockSource: L2BlockSource,
100
+ private checkpointsBuilder: FullNodeCheckpointsBuilder,
101
+ private worldState: WorldStateSynchronizer,
102
+ private l1ToL2MessageSource: L1ToL2MessageSource,
101
103
  private config: ValidatorClientFullConfig,
102
104
  private blobClient: BlobClientInterface,
103
- private haSigner: ValidatorHASigner | undefined,
105
+ private slashingProtectionSigner: ValidatorHASigner,
104
106
  private dateProvider: DateProvider = new DateProvider(),
105
107
  telemetry: TelemetryClient = getTelemetryClient(),
106
108
  log = createLogger('validator'),
@@ -208,35 +210,50 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
208
210
  metrics,
209
211
  dateProvider,
210
212
  telemetry,
213
+ undefined,
211
214
  );
212
215
 
213
216
  const nodeKeystoreAdapter = NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager);
214
- let validatorKeyStore: ExtendedValidatorKeyStore = nodeKeystoreAdapter;
215
- let haSigner: ValidatorHASigner | undefined;
217
+ let slashingProtectionSigner: ValidatorHASigner;
216
218
  if (slashingProtectionDb) {
217
219
  // Shared database mode: use a pre-existing database (e.g. for testing HA setups).
218
- const { signer } = createSignerFromSharedDb(slashingProtectionDb, config);
219
- haSigner = signer;
220
- validatorKeyStore = new HAKeyStore(nodeKeystoreAdapter, signer);
220
+ ({ signer: slashingProtectionSigner } = createSignerFromSharedDb(slashingProtectionDb, config, {
221
+ telemetryClient: telemetry,
222
+ dateProvider,
223
+ }));
221
224
  } else if (config.haSigningEnabled) {
225
+ // Multi-node HA mode: use PostgreSQL-backed distributed locking.
222
226
  // If maxStuckDutiesAgeMs is not explicitly set, compute it from Aztec slot duration
223
227
  const haConfig = {
224
228
  ...config,
225
229
  maxStuckDutiesAgeMs: config.maxStuckDutiesAgeMs ?? epochCache.getL1Constants().slotDuration * 2 * 1000,
226
230
  };
227
- const { signer } = await createHASigner(haConfig);
228
- haSigner = signer;
229
- validatorKeyStore = new HAKeyStore(nodeKeystoreAdapter, signer);
231
+ ({ signer: slashingProtectionSigner } = await createHASigner(haConfig, {
232
+ telemetryClient: telemetry,
233
+ dateProvider,
234
+ }));
235
+ } else {
236
+ // Single-node mode: use LMDB-backed local signing protection.
237
+ // This prevents double-signing if the node crashes and restarts mid-proposal.
238
+ ({ signer: slashingProtectionSigner } = await createLocalSignerWithProtection(config, {
239
+ telemetryClient: telemetry,
240
+ dateProvider,
241
+ }));
230
242
  }
243
+ const validatorKeyStore: ExtendedValidatorKeyStore = new HAKeyStore(nodeKeystoreAdapter, slashingProtectionSigner);
231
244
 
232
245
  const validator = new ValidatorClient(
233
246
  validatorKeyStore,
234
247
  epochCache,
235
248
  p2pClient,
236
249
  proposalHandler,
250
+ blockSource,
251
+ checkpointsBuilder,
252
+ worldState,
253
+ l1ToL2MessageSource,
237
254
  config,
238
255
  blobClient,
239
- haSigner,
256
+ slashingProtectionSigner,
240
257
  dateProvider,
241
258
  telemetry,
242
259
  );
@@ -275,24 +292,8 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
275
292
  }
276
293
 
277
294
  public reloadKeystore(newManager: KeystoreManager): void {
278
- if (this.config.haSigningEnabled && !this.haSigner) {
279
- this.log.warn(
280
- 'HA signing is enabled in config but was not initialized at startup. ' +
281
- 'Restart the node to enable HA signing.',
282
- );
283
- } else if (!this.config.haSigningEnabled && this.haSigner) {
284
- this.log.warn(
285
- 'HA signing was disabled via config update but the HA signer is still active. ' +
286
- 'Restart the node to fully disable HA signing.',
287
- );
288
- }
289
-
290
295
  const newAdapter = NodeKeystoreAdapter.fromKeyStoreManager(newManager);
291
- if (this.haSigner) {
292
- this.keyStore = new HAKeyStore(newAdapter, this.haSigner);
293
- } else {
294
- this.keyStore = newAdapter;
295
- }
296
+ this.keyStore = new HAKeyStore(newAdapter, this.slashingProtectionSigner);
296
297
  this.validationService = new ValidationService(this.keyStore, this.log.createChild('validation-service'));
297
298
  }
298
299
 
@@ -340,7 +341,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
340
341
  checkpoint: CheckpointProposalCore,
341
342
  proposalSender: PeerId,
342
343
  ): Promise<CheckpointAttestation[] | undefined> => this.attestToCheckpointProposal(checkpoint, proposalSender);
343
- this.p2pClient.registerCheckpointProposalHandler(checkpointHandler);
344
+ this.p2pClient.registerValidatorCheckpointProposalHandler(checkpointHandler);
344
345
 
345
346
  // Duplicate proposal handler - triggers slashing for equivocation
346
347
  this.p2pClient.registerDuplicateProposalCallback((info: DuplicateProposalInfo) => {
@@ -400,12 +401,11 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
400
401
 
401
402
  // Reexecute txs if we are part of the committee, or if slashing is enabled, or if we are configured to always reexecute.
402
403
  // In fisherman mode, we always reexecute to validate proposals.
403
- const { validatorReexecute, slashBroadcastedInvalidBlockPenalty, alwaysReexecuteBlockProposals, fishermanMode } =
404
- this.config;
404
+ const { slashBroadcastedInvalidBlockPenalty, alwaysReexecuteBlockProposals, fishermanMode } = this.config;
405
405
  const shouldReexecute =
406
406
  fishermanMode ||
407
- (slashBroadcastedInvalidBlockPenalty > 0n && validatorReexecute) ||
408
- (partOfCommittee && validatorReexecute) ||
407
+ slashBroadcastedInvalidBlockPenalty > 0n ||
408
+ partOfCommittee ||
409
409
  alwaysReexecuteBlockProposals ||
410
410
  this.blobClient.canUpload();
411
411
 
@@ -474,47 +474,51 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
474
474
  proposal: CheckpointProposalCore,
475
475
  _proposalSender: PeerId,
476
476
  ): Promise<CheckpointAttestation[] | undefined> {
477
- const slotNumber = proposal.slotNumber;
477
+ const proposalSlotNumber = proposal.slotNumber;
478
478
  const proposer = proposal.getSender();
479
479
 
480
480
  // If escape hatch is open for this slot's epoch, do not attest.
481
- if (await this.epochCache.isEscapeHatchOpenAtSlot(slotNumber)) {
482
- this.log.warn(`Escape hatch open for slot ${slotNumber}, skipping checkpoint attestation handling`);
481
+ if (await this.epochCache.isEscapeHatchOpenAtSlot(proposalSlotNumber)) {
482
+ this.log.warn(`Escape hatch open for slot ${proposalSlotNumber}, skipping checkpoint attestation handling`);
483
483
  return undefined;
484
484
  }
485
485
 
486
486
  // Ignore proposals from ourselves (may happen in HA setups)
487
487
  if (proposer && this.getValidatorAddresses().some(addr => addr.equals(proposer))) {
488
- this.log.debug(`Ignoring block proposal from self for slot ${slotNumber}`, {
488
+ this.log.debug(`Ignoring block proposal from self for slot ${proposalSlotNumber}`, {
489
489
  proposer: proposer.toString(),
490
- slotNumber,
490
+ proposalSlotNumber,
491
491
  });
492
492
  return undefined;
493
493
  }
494
494
 
495
495
  // Check that I have any address in the committee where this checkpoint will land before attesting
496
- const inCommittee = await this.epochCache.filterInCommittee(slotNumber, this.getValidatorAddresses());
496
+ const inCommittee = await this.epochCache.filterInCommittee(proposalSlotNumber, this.getValidatorAddresses());
497
497
  const partOfCommittee = inCommittee.length > 0;
498
498
 
499
499
  const proposalInfo = {
500
- slotNumber,
500
+ proposalSlotNumber,
501
501
  archive: proposal.archive.toString(),
502
502
  proposer: proposer?.toString(),
503
503
  };
504
- this.log.info(`Received checkpoint proposal for slot ${slotNumber}`, {
504
+ this.log.info(`Received checkpoint proposal for slot ${proposalSlotNumber}`, {
505
505
  ...proposalInfo,
506
506
  fishermanMode: this.config.fishermanMode || false,
507
507
  });
508
508
 
509
- // Validate the checkpoint proposal and upload blobs (unless skipCheckpointProposalValidation is set)
509
+ // Validate the checkpoint proposal before attesting (unless skipCheckpointProposalValidation is set).
510
+ // Uses the cached result from the all-nodes callback if available (avoids double validation).
511
+ let checkpointNumber: CheckpointNumber;
510
512
  if (this.config.skipCheckpointProposalValidation) {
511
- this.log.warn(`Skipping checkpoint proposal validation for slot ${slotNumber}`, proposalInfo);
513
+ this.log.warn(`Skipping checkpoint proposal validation for slot ${proposalSlotNumber}`, proposalInfo);
514
+ checkpointNumber = CheckpointNumber(0);
512
515
  } else {
513
516
  const validationResult = await this.proposalHandler.handleCheckpointProposal(proposal, proposalInfo);
514
517
  if (!validationResult.isValid) {
515
518
  this.log.warn(`Checkpoint proposal validation failed: ${validationResult.reason}`, proposalInfo);
516
519
  return undefined;
517
520
  }
521
+ checkpointNumber = validationResult.checkpointNumber;
518
522
  }
519
523
 
520
524
  // Check that I have any address in current committee before attesting
@@ -525,16 +529,19 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
525
529
  }
526
530
 
527
531
  // Provided all of the above checks pass, we can attest to the proposal
528
- this.log.info(`${partOfCommittee ? 'Attesting to' : 'Validated'} checkpoint proposal for slot ${slotNumber}`, {
529
- ...proposalInfo,
530
- inCommittee: partOfCommittee,
531
- fishermanMode: this.config.fishermanMode || false,
532
- });
532
+ this.log.info(
533
+ `${partOfCommittee ? 'Attesting to' : 'Validated'} checkpoint proposal for slot ${proposalSlotNumber}`,
534
+ {
535
+ ...proposalInfo,
536
+ inCommittee: partOfCommittee,
537
+ fishermanMode: this.config.fishermanMode || false,
538
+ },
539
+ );
533
540
 
534
541
  this.metrics.incSuccessfulAttestations(inCommittee.length);
535
542
 
536
543
  // Track epoch participation per attester: count each (attester, epoch) pair at most once
537
- const proposalEpoch = getEpochAtSlot(slotNumber, this.epochCache.getL1Constants());
544
+ const proposalEpoch = getEpochAtSlot(proposalSlotNumber, this.epochCache.getL1Constants());
538
545
  for (const attester of inCommittee) {
539
546
  const key = attester.toString();
540
547
  const lastEpoch = this.lastAttestedEpochByAttester.get(key);
@@ -562,14 +569,14 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
562
569
 
563
570
  if (this.config.fishermanMode) {
564
571
  // bail out early and don't save attestations to the pool in fisherman mode
565
- this.log.info(`Creating checkpoint attestations for slot ${slotNumber}`, {
572
+ this.log.info(`Creating checkpoint attestations for slot ${proposalSlotNumber}`, {
566
573
  ...proposalInfo,
567
574
  attestors: attestors.map(a => a.toString()),
568
575
  });
569
576
  return undefined;
570
577
  }
571
578
 
572
- return await this.createCheckpointAttestationsFromProposal(proposal, attestors);
579
+ return await this.createCheckpointAttestationsFromProposal(proposal, attestors, checkpointNumber);
573
580
  }
574
581
 
575
582
  /**
@@ -596,13 +603,14 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
596
603
  private async createCheckpointAttestationsFromProposal(
597
604
  proposal: CheckpointProposalCore,
598
605
  attestors: EthAddress[] = [],
606
+ checkpointNumber: CheckpointNumber,
599
607
  ): Promise<CheckpointAttestation[] | undefined> {
600
608
  // Equivocation check: must happen right before signing to minimize the race window
601
609
  if (!this.shouldAttestToSlot(proposal.slotNumber)) {
602
610
  return undefined;
603
611
  }
604
612
 
605
- const attestations = await this.validationService.attestToCheckpointProposal(proposal, attestors);
613
+ const attestations = await this.validationService.attestToCheckpointProposal(proposal, attestors, checkpointNumber);
606
614
 
607
615
  // Track the proposal we attested to (to prevent equivocation)
608
616
  this.lastAttestedProposal = proposal;
@@ -611,6 +619,35 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
611
619
  return attestations;
612
620
  }
613
621
 
622
+ /**
623
+ * Uploads blobs for a checkpoint to the filestore (fire and forget).
624
+ */
625
+ protected async uploadBlobsForCheckpoint(proposal: CheckpointProposalCore, proposalInfo: LogData): Promise<void> {
626
+ try {
627
+ const lastBlockHeader = await this.blockSource.getBlockHeaderByArchive(proposal.archive);
628
+ if (!lastBlockHeader) {
629
+ this.log.warn(`Failed to get last block header for blob upload`, proposalInfo);
630
+ return;
631
+ }
632
+
633
+ const blocks = await this.blockSource.getBlocksForSlot(proposal.slotNumber);
634
+ if (blocks.length === 0) {
635
+ this.log.warn(`No blocks found for blob upload`, proposalInfo);
636
+ return;
637
+ }
638
+
639
+ const blobFields = blocks.flatMap(b => b.toBlobFields());
640
+ const blobs: Blob[] = await getBlobsPerL1Block(blobFields);
641
+ await this.blobClient.sendBlobsToFilestore(blobs);
642
+ this.log.debug(`Uploaded ${blobs.length} blobs to filestore for checkpoint at slot ${proposal.slotNumber}`, {
643
+ ...proposalInfo,
644
+ numBlobs: blobs.length,
645
+ });
646
+ } catch (err) {
647
+ this.log.warn(`Failed to upload blobs for checkpoint: ${err}`, proposalInfo);
648
+ }
649
+ }
650
+
614
651
  private slashInvalidBlock(proposal: BlockProposal) {
615
652
  const proposer = proposal.getSender();
616
653
 
@@ -686,6 +723,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
686
723
 
687
724
  async createBlockProposal(
688
725
  blockHeader: BlockHeader,
726
+ checkpointNumber: CheckpointNumber,
689
727
  indexWithinCheckpoint: IndexWithinCheckpoint,
690
728
  inHash: Fr,
691
729
  archive: Fr,
@@ -712,6 +750,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
712
750
  );
713
751
  const newProposal = await this.validationService.createBlockProposal(
714
752
  blockHeader,
753
+ checkpointNumber,
715
754
  indexWithinCheckpoint,
716
755
  inHash,
717
756
  archive,
@@ -729,8 +768,9 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
729
768
  async createCheckpointProposal(
730
769
  checkpointHeader: CheckpointHeader,
731
770
  archive: Fr,
771
+ checkpointNumber: CheckpointNumber,
732
772
  feeAssetPriceModifier: bigint,
733
- lastBlockInfo: CreateCheckpointProposalLastBlockData | undefined,
773
+ lastBlockProposal: BlockProposal | undefined,
734
774
  proposerAddress: EthAddress | undefined,
735
775
  options: CheckpointProposalOptions = {},
736
776
  ): Promise<CheckpointProposal> {
@@ -751,8 +791,9 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
751
791
  const newProposal = await this.validationService.createCheckpointProposal(
752
792
  checkpointHeader,
753
793
  archive,
794
+ checkpointNumber,
754
795
  feeAssetPriceModifier,
755
- lastBlockInfo,
796
+ lastBlockProposal,
756
797
  proposerAddress,
757
798
  options,
758
799
  );
@@ -768,16 +809,24 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
768
809
  attestationsAndSigners: CommitteeAttestationsAndSigners,
769
810
  proposer: EthAddress,
770
811
  slot: SlotNumber,
771
- blockNumber: BlockNumber | CheckpointNumber,
812
+ checkpointNumber: CheckpointNumber,
772
813
  ): Promise<Signature> {
773
- return await this.validationService.signAttestationsAndSigners(attestationsAndSigners, proposer, slot, blockNumber);
814
+ return await this.validationService.signAttestationsAndSigners(
815
+ attestationsAndSigners,
816
+ proposer,
817
+ slot,
818
+ checkpointNumber,
819
+ );
774
820
  }
775
821
 
776
- async collectOwnAttestations(proposal: CheckpointProposal): Promise<CheckpointAttestation[]> {
822
+ async collectOwnAttestations(
823
+ proposal: CheckpointProposal,
824
+ checkpointNumber: CheckpointNumber,
825
+ ): Promise<CheckpointAttestation[]> {
777
826
  const slot = proposal.slotNumber;
778
827
  const inCommittee = await this.epochCache.filterInCommittee(slot, this.getValidatorAddresses());
779
828
  this.log.debug(`Collecting ${inCommittee.length} self-attestations for slot ${slot}`, { inCommittee });
780
- const attestations = await this.createCheckpointAttestationsFromProposal(proposal, inCommittee);
829
+ const attestations = await this.createCheckpointAttestationsFromProposal(proposal, inCommittee, checkpointNumber);
781
830
 
782
831
  if (!attestations) {
783
832
  return [];
@@ -796,6 +845,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
796
845
  proposal: CheckpointProposal,
797
846
  required: number,
798
847
  deadline: Date,
848
+ checkpointNumber: CheckpointNumber,
799
849
  ): Promise<CheckpointAttestation[]> {
800
850
  // Wait and poll the p2pClient's attestation pool for this checkpoint until we have enough attestations
801
851
  const slot = proposal.slotNumber;
@@ -808,7 +858,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
808
858
  throw new AttestationTimeoutError(0, required, slot);
809
859
  }
810
860
 
811
- await this.collectOwnAttestations(proposal);
861
+ await this.collectOwnAttestations(proposal, checkpointNumber);
812
862
 
813
863
  const proposalId = proposal.archive.toString();
814
864
  const myAddresses = this.getValidatorAddresses();