@aztec/validator-client 0.0.1-commit.db765a8 → 0.0.1-commit.df81a97b5

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
@@ -24,6 +24,7 @@ import { AuthRequest, AuthResponse, BlockProposalValidator, ReqRespSubProtocol }
24
24
  import { OffenseType, WANT_TO_SLASH_EVENT, type Watcher, type WatcherEmitter } from '@aztec/slasher';
25
25
  import type { AztecAddress } from '@aztec/stdlib/aztec-address';
26
26
  import type { CommitteeAttestationsAndSigners, L2Block, L2BlockSink, L2BlockSource } from '@aztec/stdlib/block';
27
+ import { validateCheckpoint } from '@aztec/stdlib/checkpoint';
27
28
  import { getEpochAtSlot, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
28
29
  import type {
29
30
  CreateCheckpointProposalLastBlockData,
@@ -45,8 +46,12 @@ import type { CheckpointHeader } from '@aztec/stdlib/rollup';
45
46
  import type { BlockHeader, CheckpointGlobalVariables, Tx } from '@aztec/stdlib/tx';
46
47
  import { AttestationTimeoutError } from '@aztec/stdlib/validators';
47
48
  import { type TelemetryClient, type Tracer, getTelemetryClient } from '@aztec/telemetry-client';
48
- import { createHASigner } from '@aztec/validator-ha-signer/factory';
49
- import { DutyType, type SigningContext } from '@aztec/validator-ha-signer/types';
49
+ import {
50
+ createHASigner,
51
+ createLocalSignerWithProtection,
52
+ createSignerFromSharedDb,
53
+ } from '@aztec/validator-ha-signer/factory';
54
+ import { DutyType, type SigningContext, type SlashingProtectionDatabase } from '@aztec/validator-ha-signer/types';
50
55
  import type { ValidatorHASigner } from '@aztec/validator-ha-signer/validator-ha-signer';
51
56
 
52
57
  import { EventEmitter } from 'events';
@@ -108,7 +113,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
108
113
  private l1ToL2MessageSource: L1ToL2MessageSource,
109
114
  private config: ValidatorClientFullConfig,
110
115
  private blobClient: BlobClientInterface,
111
- private haSigner: ValidatorHASigner | undefined,
116
+ private slashingProtectionSigner: ValidatorHASigner,
112
117
  private dateProvider: DateProvider = new DateProvider(),
113
118
  telemetry: TelemetryClient = getTelemetryClient(),
114
119
  log = createLogger('validator'),
@@ -196,11 +201,12 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
196
201
  blobClient: BlobClientInterface,
197
202
  dateProvider: DateProvider = new DateProvider(),
198
203
  telemetry: TelemetryClient = getTelemetryClient(),
204
+ slashingProtectionDb?: SlashingProtectionDatabase,
199
205
  ) {
200
206
  const metrics = new ValidatorMetrics(telemetry);
201
207
  const blockProposalValidator = new BlockProposalValidator(epochCache, {
202
208
  txsPermitted: !config.disableTransactions,
203
- maxTxsPerBlock: config.maxTxsPerBlock,
209
+ maxTxsPerBlock: config.validateMaxTxsPerBlock,
204
210
  });
205
211
  const blockProposalHandler = new BlockProposalHandler(
206
212
  checkpointsBuilder,
@@ -217,18 +223,33 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
217
223
  );
218
224
 
219
225
  const nodeKeystoreAdapter = NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager);
220
- let validatorKeyStore: ExtendedValidatorKeyStore = nodeKeystoreAdapter;
221
- let haSigner: ValidatorHASigner | undefined;
222
- if (config.haSigningEnabled) {
226
+ let slashingProtectionSigner: ValidatorHASigner;
227
+ if (slashingProtectionDb) {
228
+ // Shared database mode: use a pre-existing database (e.g. for testing HA setups).
229
+ ({ signer: slashingProtectionSigner } = createSignerFromSharedDb(slashingProtectionDb, config, {
230
+ telemetryClient: telemetry,
231
+ dateProvider,
232
+ }));
233
+ } else if (config.haSigningEnabled) {
234
+ // Multi-node HA mode: use PostgreSQL-backed distributed locking.
223
235
  // If maxStuckDutiesAgeMs is not explicitly set, compute it from Aztec slot duration
224
236
  const haConfig = {
225
237
  ...config,
226
238
  maxStuckDutiesAgeMs: config.maxStuckDutiesAgeMs ?? epochCache.getL1Constants().slotDuration * 2 * 1000,
227
239
  };
228
- const { signer } = await createHASigner(haConfig, { telemetryClient: telemetry, dateProvider });
229
- haSigner = signer;
230
- validatorKeyStore = new HAKeyStore(nodeKeystoreAdapter, signer);
240
+ ({ signer: slashingProtectionSigner } = await createHASigner(haConfig, {
241
+ telemetryClient: telemetry,
242
+ dateProvider,
243
+ }));
244
+ } else {
245
+ // Single-node mode: use LMDB-backed local signing protection.
246
+ // This prevents double-signing if the node crashes and restarts mid-proposal.
247
+ ({ signer: slashingProtectionSigner } = await createLocalSignerWithProtection(config, {
248
+ telemetryClient: telemetry,
249
+ dateProvider,
250
+ }));
231
251
  }
252
+ const validatorKeyStore: ExtendedValidatorKeyStore = new HAKeyStore(nodeKeystoreAdapter, slashingProtectionSigner);
232
253
 
233
254
  const validator = new ValidatorClient(
234
255
  validatorKeyStore,
@@ -241,7 +262,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
241
262
  l1ToL2MessageSource,
242
263
  config,
243
264
  blobClient,
244
- haSigner,
265
+ slashingProtectionSigner,
245
266
  dateProvider,
246
267
  telemetry,
247
268
  );
@@ -280,24 +301,8 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
280
301
  }
281
302
 
282
303
  public reloadKeystore(newManager: KeystoreManager): void {
283
- if (this.config.haSigningEnabled && !this.haSigner) {
284
- this.log.warn(
285
- 'HA signing is enabled in config but was not initialized at startup. ' +
286
- 'Restart the node to enable HA signing.',
287
- );
288
- } else if (!this.config.haSigningEnabled && this.haSigner) {
289
- this.log.warn(
290
- 'HA signing was disabled via config update but the HA signer is still active. ' +
291
- 'Restart the node to fully disable HA signing.',
292
- );
293
- }
294
-
295
304
  const newAdapter = NodeKeystoreAdapter.fromKeyStoreManager(newManager);
296
- if (this.haSigner) {
297
- this.keyStore = new HAKeyStore(newAdapter, this.haSigner);
298
- } else {
299
- this.keyStore = newAdapter;
300
- }
305
+ this.keyStore = new HAKeyStore(newAdapter, this.slashingProtectionSigner);
301
306
  this.validationService = new ValidationService(this.keyStore, this.log.createChild('validation-service'));
302
307
  }
303
308
 
@@ -384,13 +389,12 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
384
389
  return false;
385
390
  }
386
391
 
387
- // Ignore proposals from ourselves (may happen in HA setups)
392
+ // Log self-proposals from HA peers (same validator key on different nodes)
388
393
  if (this.getValidatorAddresses().some(addr => addr.equals(proposer))) {
389
- this.log.warn(`Ignoring block proposal from self for slot ${slotNumber}`, {
394
+ this.log.verbose(`Processing block proposal from HA peer for slot ${slotNumber}`, {
390
395
  proposer: proposer.toString(),
391
396
  slotNumber,
392
397
  });
393
- return false;
394
398
  }
395
399
 
396
400
  // Check if we're in the committee (for metrics purposes)
@@ -422,9 +426,10 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
422
426
  );
423
427
 
424
428
  if (!validationResult.isValid) {
425
- this.log.warn(`Block proposal validation failed: ${validationResult.reason}`, proposalInfo);
426
-
427
429
  const reason = validationResult.reason || 'unknown';
430
+
431
+ this.log.warn(`Block proposal validation failed: ${reason}`, proposalInfo);
432
+
428
433
  // Classify failure reason: bad proposal vs node issue
429
434
  const badProposalReasons: BlockProposalValidationFailureReason[] = [
430
435
  'invalid_proposal',
@@ -479,26 +484,26 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
479
484
  proposal: CheckpointProposalCore,
480
485
  _proposalSender: PeerId,
481
486
  ): Promise<CheckpointAttestation[] | undefined> {
482
- const slotNumber = proposal.slotNumber;
487
+ const proposalSlotNumber = proposal.slotNumber;
483
488
  const proposer = proposal.getSender();
484
489
 
485
490
  // If escape hatch is open for this slot's epoch, do not attest.
486
- if (await this.epochCache.isEscapeHatchOpenAtSlot(slotNumber)) {
487
- this.log.warn(`Escape hatch open for slot ${slotNumber}, skipping checkpoint attestation handling`);
491
+ if (await this.epochCache.isEscapeHatchOpenAtSlot(proposalSlotNumber)) {
492
+ this.log.warn(`Escape hatch open for slot ${proposalSlotNumber}, skipping checkpoint attestation handling`);
488
493
  return undefined;
489
494
  }
490
495
 
491
496
  // Reject proposals with invalid signatures
492
497
  if (!proposer) {
493
- this.log.warn(`Received checkpoint proposal with invalid signature for slot ${slotNumber}`);
498
+ this.log.warn(`Received checkpoint proposal with invalid signature for proposal slot ${proposalSlotNumber}`);
494
499
  return undefined;
495
500
  }
496
501
 
497
502
  // Ignore proposals from ourselves (may happen in HA setups)
498
503
  if (this.getValidatorAddresses().some(addr => addr.equals(proposer))) {
499
- this.log.warn(`Ignoring block proposal from self for slot ${slotNumber}`, {
504
+ this.log.debug(`Ignoring block proposal from self for slot ${proposalSlotNumber}`, {
500
505
  proposer: proposer.toString(),
501
- slotNumber,
506
+ proposalSlotNumber,
502
507
  });
503
508
  return undefined;
504
509
  }
@@ -506,30 +511,28 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
506
511
  // Validate fee asset price modifier is within allowed range
507
512
  if (!validateFeeAssetPriceModifier(proposal.feeAssetPriceModifier)) {
508
513
  this.log.warn(
509
- `Received checkpoint proposal with invalid feeAssetPriceModifier ${proposal.feeAssetPriceModifier} for slot ${slotNumber}`,
514
+ `Received checkpoint proposal with invalid feeAssetPriceModifier ${proposal.feeAssetPriceModifier} for slot ${proposalSlotNumber}`,
510
515
  );
511
516
  return undefined;
512
517
  }
513
518
 
514
- // Check that I have any address in current committee before attesting
515
- const inCommittee = await this.epochCache.filterInCommittee(slotNumber, this.getValidatorAddresses());
519
+ // Check that I have any address in the committee where this checkpoint will land before attesting
520
+ const inCommittee = await this.epochCache.filterInCommittee(proposalSlotNumber, this.getValidatorAddresses());
516
521
  const partOfCommittee = inCommittee.length > 0;
517
522
 
518
523
  const proposalInfo = {
519
- slotNumber,
524
+ proposalSlotNumber,
520
525
  archive: proposal.archive.toString(),
521
526
  proposer: proposer.toString(),
522
- txCount: proposal.txHashes.length,
523
527
  };
524
- this.log.info(`Received checkpoint proposal for slot ${slotNumber}`, {
528
+ this.log.info(`Received checkpoint proposal for slot ${proposalSlotNumber}`, {
525
529
  ...proposalInfo,
526
- txHashes: proposal.txHashes.map(t => t.toString()),
527
530
  fishermanMode: this.config.fishermanMode || false,
528
531
  });
529
532
 
530
533
  // Validate the checkpoint proposal before attesting (unless skipCheckpointProposalValidation is set)
531
534
  if (this.config.skipCheckpointProposalValidation) {
532
- this.log.warn(`Skipping checkpoint proposal validation for slot ${slotNumber}`, proposalInfo);
535
+ this.log.warn(`Skipping checkpoint proposal validation for slot ${proposalSlotNumber}`, proposalInfo);
533
536
  } else {
534
537
  const validationResult = await this.validateCheckpointProposal(proposal, proposalInfo);
535
538
  if (!validationResult.isValid) {
@@ -551,16 +554,19 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
551
554
  }
552
555
 
553
556
  // Provided all of the above checks pass, we can attest to the proposal
554
- this.log.info(`${partOfCommittee ? 'Attesting to' : 'Validated'} checkpoint proposal for slot ${slotNumber}`, {
555
- ...proposalInfo,
556
- inCommittee: partOfCommittee,
557
- fishermanMode: this.config.fishermanMode || false,
558
- });
557
+ this.log.info(
558
+ `${partOfCommittee ? 'Attesting to' : 'Validated'} checkpoint proposal for slot ${proposalSlotNumber}`,
559
+ {
560
+ ...proposalInfo,
561
+ inCommittee: partOfCommittee,
562
+ fishermanMode: this.config.fishermanMode || false,
563
+ },
564
+ );
559
565
 
560
566
  this.metrics.incSuccessfulAttestations(inCommittee.length);
561
567
 
562
568
  // Track epoch participation per attester: count each (attester, epoch) pair at most once
563
- const proposalEpoch = getEpochAtSlot(slotNumber, this.epochCache.getL1Constants());
569
+ const proposalEpoch = getEpochAtSlot(proposalSlotNumber, this.epochCache.getL1Constants());
564
570
  for (const attester of inCommittee) {
565
571
  const key = attester.toString();
566
572
  const lastEpoch = this.lastAttestedEpochByAttester.get(key);
@@ -588,7 +594,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
588
594
 
589
595
  if (this.config.fishermanMode) {
590
596
  // bail out early and don't save attestations to the pool in fisherman mode
591
- this.log.info(`Creating checkpoint attestations for slot ${slotNumber}`, {
597
+ this.log.info(`Creating checkpoint attestations for slot ${proposalSlotNumber}`, {
592
598
  ...proposalInfo,
593
599
  attestors: attestors.map(a => a.toString()),
594
600
  });
@@ -766,6 +772,20 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
766
772
  return { isValid: false, reason: 'out_hash_mismatch' };
767
773
  }
768
774
 
775
+ // Final round of validations on the checkpoint, just in case.
776
+ try {
777
+ validateCheckpoint(computedCheckpoint, {
778
+ rollupManaLimit: this.checkpointsBuilder.getConfig().rollupManaLimit,
779
+ maxDABlockGas: this.config.validateMaxDABlockGas,
780
+ maxL2BlockGas: this.config.validateMaxL2BlockGas,
781
+ maxTxsPerBlock: this.config.validateMaxTxsPerBlock,
782
+ maxTxsPerCheckpoint: this.config.validateMaxTxsPerCheckpoint,
783
+ });
784
+ } catch (err) {
785
+ this.log.warn(`Checkpoint validation failed: ${err}`, proposalInfo);
786
+ return { isValid: false, reason: 'checkpoint_validation_failed' };
787
+ }
788
+
769
789
  this.log.verbose(`Checkpoint proposal validation successful for slot ${slot}`, proposalInfo);
770
790
  return { isValid: true };
771
791
  } finally {