@aztec/validator-client 0.0.1-commit.358457c → 0.0.1-commit.3895657bc

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/factory.ts CHANGED
@@ -29,6 +29,7 @@ export function createBlockProposalHandler(
29
29
  const metrics = new ValidatorMetrics(deps.telemetry);
30
30
  const blockProposalValidator = new BlockProposalValidator(deps.epochCache, {
31
31
  txsPermitted: !config.disableTransactions,
32
+ maxTxsPerBlock: config.validateMaxTxsPerBlock,
32
33
  });
33
34
  return new BlockProposalHandler(
34
35
  deps.checkpointsBuilder,
package/src/metrics.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import type { EpochNumber } from '@aztec/foundation/branded-types';
2
+ import type { EthAddress } from '@aztec/foundation/eth-address';
1
3
  import type { BlockProposal } from '@aztec/stdlib/p2p';
2
4
  import {
3
5
  Attributes,
@@ -16,6 +18,8 @@ export class ValidatorMetrics {
16
18
  private successfulAttestationsCount: UpDownCounter;
17
19
  private failedAttestationsBadProposalCount: UpDownCounter;
18
20
  private failedAttestationsNodeIssueCount: UpDownCounter;
21
+ private currentEpoch: Gauge;
22
+ private attestedEpochCount: UpDownCounter;
19
23
 
20
24
  private reexMana: Histogram;
21
25
  private reexTx: Histogram;
@@ -64,6 +68,10 @@ export class ValidatorMetrics {
64
68
  },
65
69
  );
66
70
 
71
+ this.currentEpoch = meter.createGauge(Metrics.VALIDATOR_CURRENT_EPOCH);
72
+
73
+ this.attestedEpochCount = createUpDownCounterWithDefault(meter, Metrics.VALIDATOR_ATTESTED_EPOCH_COUNT);
74
+
67
75
  this.reexMana = meter.createHistogram(Metrics.VALIDATOR_RE_EXECUTION_MANA);
68
76
 
69
77
  this.reexTx = meter.createHistogram(Metrics.VALIDATOR_RE_EXECUTION_TX_COUNT);
@@ -110,4 +118,14 @@ export class ValidatorMetrics {
110
118
  [Attributes.IS_COMMITTEE_MEMBER]: inCommittee,
111
119
  });
112
120
  }
121
+
122
+ /** Update the gauge tracking the current epoch number (proxy for total epochs elapsed). */
123
+ public setCurrentEpoch(epoch: EpochNumber) {
124
+ this.currentEpoch.record(Number(epoch));
125
+ }
126
+
127
+ /** Increment the count of epochs in which the given attester submitted at least one attestation. */
128
+ public incAttestedEpochCount(attester: EthAddress) {
129
+ this.attestedEpochCount.add(1, { [Attributes.ATTESTER_ADDRESS]: attester.toString() });
130
+ }
113
131
  }
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,7 +46,7 @@ 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 { createHASigner, createLocalSignerWithProtection } from '@aztec/validator-ha-signer/factory';
49
50
  import { DutyType, type SigningContext } from '@aztec/validator-ha-signer/types';
50
51
  import type { ValidatorHASigner } from '@aztec/validator-ha-signer/validator-ha-signer';
51
52
 
@@ -89,6 +90,8 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
89
90
 
90
91
  private lastEpochForCommitteeUpdateLoop: EpochNumber | undefined;
91
92
  private epochCacheUpdateLoop: RunningPromise;
93
+ /** Tracks the last epoch in which each attester successfully submitted at least one attestation. */
94
+ private lastAttestedEpochByAttester: Map<string, EpochNumber> = new Map();
92
95
 
93
96
  private proposersOfInvalidBlocks: Set<string> = new Set();
94
97
 
@@ -106,7 +109,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
106
109
  private l1ToL2MessageSource: L1ToL2MessageSource,
107
110
  private config: ValidatorClientFullConfig,
108
111
  private blobClient: BlobClientInterface,
109
- private haSigner: ValidatorHASigner | undefined,
112
+ private slashingProtectionSigner: ValidatorHASigner,
110
113
  private dateProvider: DateProvider = new DateProvider(),
111
114
  telemetry: TelemetryClient = getTelemetryClient(),
112
115
  log = createLogger('validator'),
@@ -160,6 +163,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
160
163
  this.log.trace(`No committee found for slot`);
161
164
  return;
162
165
  }
166
+ this.metrics.setCurrentEpoch(epoch);
163
167
  if (epoch !== this.lastEpochForCommitteeUpdateLoop) {
164
168
  const me = this.getValidatorAddresses();
165
169
  const committeeSet = new Set(committee.map(v => v.toString()));
@@ -197,6 +201,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
197
201
  const metrics = new ValidatorMetrics(telemetry);
198
202
  const blockProposalValidator = new BlockProposalValidator(epochCache, {
199
203
  txsPermitted: !config.disableTransactions,
204
+ maxTxsPerBlock: config.validateMaxTxsPerBlock,
200
205
  });
201
206
  const blockProposalHandler = new BlockProposalHandler(
202
207
  checkpointsBuilder,
@@ -213,18 +218,27 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
213
218
  );
214
219
 
215
220
  const nodeKeystoreAdapter = NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager);
216
- let validatorKeyStore: ExtendedValidatorKeyStore = nodeKeystoreAdapter;
217
- let haSigner: ValidatorHASigner | undefined;
221
+ let slashingProtectionSigner: ValidatorHASigner;
218
222
  if (config.haSigningEnabled) {
223
+ // Multi-node HA mode: use PostgreSQL-backed distributed locking.
219
224
  // If maxStuckDutiesAgeMs is not explicitly set, compute it from Aztec slot duration
220
225
  const haConfig = {
221
226
  ...config,
222
227
  maxStuckDutiesAgeMs: config.maxStuckDutiesAgeMs ?? epochCache.getL1Constants().slotDuration * 2 * 1000,
223
228
  };
224
- const { signer } = await createHASigner(haConfig, { telemetryClient: telemetry, dateProvider });
225
- haSigner = signer;
226
- validatorKeyStore = new HAKeyStore(nodeKeystoreAdapter, signer);
229
+ ({ signer: slashingProtectionSigner } = await createHASigner(haConfig, {
230
+ telemetryClient: telemetry,
231
+ dateProvider,
232
+ }));
233
+ } else {
234
+ // Single-node mode: use LMDB-backed local signing protection.
235
+ // This prevents double-signing if the node crashes and restarts mid-proposal.
236
+ ({ signer: slashingProtectionSigner } = await createLocalSignerWithProtection(config, {
237
+ telemetryClient: telemetry,
238
+ dateProvider,
239
+ }));
227
240
  }
241
+ const validatorKeyStore: ExtendedValidatorKeyStore = new HAKeyStore(nodeKeystoreAdapter, slashingProtectionSigner);
228
242
 
229
243
  const validator = new ValidatorClient(
230
244
  validatorKeyStore,
@@ -237,7 +251,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
237
251
  l1ToL2MessageSource,
238
252
  config,
239
253
  blobClient,
240
- haSigner,
254
+ slashingProtectionSigner,
241
255
  dateProvider,
242
256
  telemetry,
243
257
  );
@@ -276,24 +290,8 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
276
290
  }
277
291
 
278
292
  public reloadKeystore(newManager: KeystoreManager): void {
279
- if (this.config.haSigningEnabled && !this.haSigner) {
280
- this.log.warn(
281
- 'HA signing is enabled in config but was not initialized at startup. ' +
282
- 'Restart the node to enable HA signing.',
283
- );
284
- } else if (!this.config.haSigningEnabled && this.haSigner) {
285
- this.log.warn(
286
- 'HA signing was disabled via config update but the HA signer is still active. ' +
287
- 'Restart the node to fully disable HA signing.',
288
- );
289
- }
290
-
291
293
  const newAdapter = NodeKeystoreAdapter.fromKeyStoreManager(newManager);
292
- if (this.haSigner) {
293
- this.keyStore = new HAKeyStore(newAdapter, this.haSigner);
294
- } else {
295
- this.keyStore = newAdapter;
296
- }
294
+ this.keyStore = new HAKeyStore(newAdapter, this.slashingProtectionSigner);
297
295
  this.validationService = new ValidationService(this.keyStore, this.log.createChild('validation-service'));
298
296
  }
299
297
 
@@ -515,11 +513,9 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
515
513
  slotNumber,
516
514
  archive: proposal.archive.toString(),
517
515
  proposer: proposer.toString(),
518
- txCount: proposal.txHashes.length,
519
516
  };
520
517
  this.log.info(`Received checkpoint proposal for slot ${slotNumber}`, {
521
518
  ...proposalInfo,
522
- txHashes: proposal.txHashes.map(t => t.toString()),
523
519
  fishermanMode: this.config.fishermanMode || false,
524
520
  });
525
521
 
@@ -555,6 +551,17 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
555
551
 
556
552
  this.metrics.incSuccessfulAttestations(inCommittee.length);
557
553
 
554
+ // Track epoch participation per attester: count each (attester, epoch) pair at most once
555
+ const proposalEpoch = getEpochAtSlot(slotNumber, this.epochCache.getL1Constants());
556
+ for (const attester of inCommittee) {
557
+ const key = attester.toString();
558
+ const lastEpoch = this.lastAttestedEpochByAttester.get(key);
559
+ if (lastEpoch === undefined || proposalEpoch > lastEpoch) {
560
+ this.lastAttestedEpochByAttester.set(key, proposalEpoch);
561
+ this.metrics.incAttestedEpochCount(attester);
562
+ }
563
+ }
564
+
558
565
  // Determine which validators should attest
559
566
  let attestors: EthAddress[];
560
567
  if (partOfCommittee) {
@@ -751,6 +758,20 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
751
758
  return { isValid: false, reason: 'out_hash_mismatch' };
752
759
  }
753
760
 
761
+ // Final round of validations on the checkpoint, just in case.
762
+ try {
763
+ validateCheckpoint(computedCheckpoint, {
764
+ rollupManaLimit: this.checkpointsBuilder.getConfig().rollupManaLimit,
765
+ maxDABlockGas: this.config.validateMaxDABlockGas,
766
+ maxL2BlockGas: this.config.validateMaxL2BlockGas,
767
+ maxTxsPerBlock: this.config.validateMaxTxsPerBlock,
768
+ maxTxsPerCheckpoint: this.config.validateMaxTxsPerCheckpoint,
769
+ });
770
+ } catch (err) {
771
+ this.log.warn(`Checkpoint validation failed: ${err}`, proposalInfo);
772
+ return { isValid: false, reason: 'checkpoint_validation_failed' };
773
+ }
774
+
754
775
  this.log.verbose(`Checkpoint proposal validation successful for slot ${slot}`, proposalInfo);
755
776
  return { isValid: true };
756
777
  } finally {