@aztec/validator-client 2.1.0-rc.9 → 3.0.0-devnet.2

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/metrics.ts CHANGED
@@ -10,8 +10,9 @@ import {
10
10
 
11
11
  export class ValidatorMetrics {
12
12
  private failedReexecutionCounter: UpDownCounter;
13
- private attestationsCount: UpDownCounter;
14
- private failedAttestationsCount: UpDownCounter;
13
+ private successfulAttestationsCount: UpDownCounter;
14
+ private failedAttestationsBadProposalCount: UpDownCounter;
15
+ private failedAttestationsNodeIssueCount: UpDownCounter;
15
16
 
16
17
  private reexMana: Histogram;
17
18
  private reexTx: Histogram;
@@ -26,15 +27,26 @@ export class ValidatorMetrics {
26
27
  valueType: ValueType.INT,
27
28
  });
28
29
 
29
- this.attestationsCount = meter.createUpDownCounter(Metrics.VALIDATOR_ATTESTATION_COUNT, {
30
- description: 'The number of attestations',
30
+ this.successfulAttestationsCount = meter.createUpDownCounter(Metrics.VALIDATOR_ATTESTATION_SUCCESS_COUNT, {
31
+ description: 'The number of successful attestations',
31
32
  valueType: ValueType.INT,
32
33
  });
33
34
 
34
- this.failedAttestationsCount = meter.createUpDownCounter(Metrics.VALIDATOR_FAILED_ATTESTATION_COUNT, {
35
- description: 'The number of failed attestations',
36
- valueType: ValueType.INT,
37
- });
35
+ this.failedAttestationsBadProposalCount = meter.createUpDownCounter(
36
+ Metrics.VALIDATOR_ATTESTATION_FAILED_BAD_PROPOSAL_COUNT,
37
+ {
38
+ description: 'The number of failed attestations due to invalid block proposals',
39
+ valueType: ValueType.INT,
40
+ },
41
+ );
42
+
43
+ this.failedAttestationsNodeIssueCount = meter.createUpDownCounter(
44
+ Metrics.VALIDATOR_ATTESTATION_FAILED_NODE_ISSUE_COUNT,
45
+ {
46
+ description: 'The number of failed attestations due to node issues (timeout, missing data, etc.)',
47
+ valueType: ValueType.INT,
48
+ },
49
+ );
38
50
 
39
51
  this.reexMana = meter.createHistogram(Metrics.VALIDATOR_RE_EXECUTION_MANA, {
40
52
  description: 'The mana consumed by blocks',
@@ -62,20 +74,26 @@ export class ValidatorMetrics {
62
74
  }
63
75
 
64
76
  public recordFailedReexecution(proposal: BlockProposal) {
77
+ const proposer = proposal.getSender();
65
78
  this.failedReexecutionCounter.add(1, {
66
79
  [Attributes.STATUS]: 'failed',
67
- [Attributes.BLOCK_PROPOSER]: proposal.getSender().toString(),
80
+ [Attributes.BLOCK_PROPOSER]: proposer?.toString() ?? 'unknown',
68
81
  });
69
82
  }
70
83
 
71
- public incAttestations(num: number) {
72
- this.attestationsCount.add(num);
84
+ public incSuccessfulAttestations(num: number) {
85
+ this.successfulAttestationsCount.add(num);
86
+ }
87
+
88
+ public incFailedAttestationsBadProposal(num: number, reason: string) {
89
+ this.failedAttestationsBadProposalCount.add(num, {
90
+ [Attributes.ERROR_TYPE]: reason,
91
+ });
73
92
  }
74
93
 
75
- public incFailedAttestations(num: number, reason: string, inCommittee: boolean) {
76
- this.failedAttestationsCount.add(num, {
94
+ public incFailedAttestationsNodeIssue(num: number, reason: string) {
95
+ this.failedAttestationsNodeIssueCount.add(num, {
77
96
  [Attributes.ERROR_TYPE]: reason,
78
- [Attributes.VALIDATOR_STATUS]: inCommittee ? 'in-committee' : 'none',
79
97
  });
80
98
  }
81
99
  }
package/src/validator.ts CHANGED
@@ -2,26 +2,21 @@ import type { EpochCache } from '@aztec/epoch-cache';
2
2
  import type { EthAddress } from '@aztec/foundation/eth-address';
3
3
  import type { Signature } from '@aztec/foundation/eth-signature';
4
4
  import { Fr } from '@aztec/foundation/fields';
5
- import { createLogger } from '@aztec/foundation/log';
5
+ import { type Logger, createLogger } from '@aztec/foundation/log';
6
6
  import { RunningPromise } from '@aztec/foundation/running-promise';
7
7
  import { sleep } from '@aztec/foundation/sleep';
8
8
  import { DateProvider } from '@aztec/foundation/timer';
9
9
  import type { KeystoreManager } from '@aztec/node-keystore';
10
10
  import type { P2P, PeerId, TxProvider } from '@aztec/p2p';
11
11
  import { AuthRequest, AuthResponse, BlockProposalValidator, ReqRespSubProtocol } from '@aztec/p2p';
12
- import {
13
- OffenseType,
14
- type SlasherConfig,
15
- WANT_TO_SLASH_EVENT,
16
- type Watcher,
17
- type WatcherEmitter,
18
- } from '@aztec/slasher';
12
+ import { OffenseType, WANT_TO_SLASH_EVENT, type Watcher, type WatcherEmitter } from '@aztec/slasher';
19
13
  import type { AztecAddress } from '@aztec/stdlib/aztec-address';
20
14
  import type { CommitteeAttestationsAndSigners, L2BlockSource } from '@aztec/stdlib/block';
21
15
  import type { IFullNodeBlockBuilder, Validator, ValidatorClientFullConfig } from '@aztec/stdlib/interfaces/server';
22
16
  import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
23
17
  import type { BlockAttestation, BlockProposal, BlockProposalOptions } from '@aztec/stdlib/p2p';
24
- import type { ProposedBlockHeader, StateReference, Tx } from '@aztec/stdlib/tx';
18
+ import type { CheckpointHeader } from '@aztec/stdlib/rollup';
19
+ import type { StateReference, Tx } from '@aztec/stdlib/tx';
25
20
  import { AttestationTimeoutError } from '@aztec/stdlib/validators';
26
21
  import { type TelemetryClient, type Tracer, getTelemetryClient } from '@aztec/telemetry-client';
27
22
 
@@ -29,7 +24,6 @@ import { EventEmitter } from 'events';
29
24
  import type { TypedDataDefinition } from 'viem';
30
25
 
31
26
  import { BlockProposalHandler, type BlockProposalValidationFailureReason } from './block_proposal_handler.js';
32
- import type { ValidatorClientConfig } from './config.js';
33
27
  import { ValidationService } from './duties/validation_service.js';
34
28
  import { NodeKeystoreAdapter } from './key_store/node_keystore_adapter.js';
35
29
  import { ValidatorMetrics } from './metrics.js';
@@ -77,7 +71,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
77
71
  this.tracer = telemetry.getTracer('Validator');
78
72
  this.metrics = new ValidatorMetrics(telemetry);
79
73
 
80
- this.validationService = new ValidationService(keyStore);
74
+ this.validationService = new ValidationService(keyStore, log.createChild('validation-service'));
81
75
 
82
76
  // Refresh epoch cache every second to trigger alert if participation in committee changes
83
77
  this.epochCacheUpdateLoop = new RunningPromise(this.handleEpochCommitteeUpdate.bind(this), log, 1000);
@@ -86,15 +80,17 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
86
80
  this.log.verbose(`Initialized validator with addresses: ${myAddresses.map(a => a.toString()).join(', ')}`);
87
81
  }
88
82
 
89
- public static validateKeyStoreConfiguration(keyStoreManager: KeystoreManager) {
83
+ public static validateKeyStoreConfiguration(keyStoreManager: KeystoreManager, logger?: Logger) {
90
84
  const validatorKeyStore = NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager);
91
85
  const validatorAddresses = validatorKeyStore.getAddresses();
92
86
  // Verify that we can retrieve all required data from the key store
93
87
  for (const address of validatorAddresses) {
94
88
  // Functions throw if required data is not available
89
+ let coinbase: EthAddress;
90
+ let feeRecipient: AztecAddress;
95
91
  try {
96
- validatorKeyStore.getCoinbaseAddress(address);
97
- validatorKeyStore.getFeeRecipient(address);
92
+ coinbase = validatorKeyStore.getCoinbaseAddress(address);
93
+ feeRecipient = validatorKeyStore.getFeeRecipient(address);
98
94
  } catch (error) {
99
95
  throw new Error(`Failed to retrieve required data for validator address ${address}, error: ${error}`);
100
96
  }
@@ -103,6 +99,9 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
103
99
  if (!publisherAddresses.length) {
104
100
  throw new Error(`No publisher addresses found for validator address ${address}`);
105
101
  }
102
+ logger?.debug(
103
+ `Validator ${address.toString()} configured with coinbase ${coinbase.toString()}, feeRecipient ${feeRecipient.toString()} and publishers ${publisherAddresses.map(x => x.toString()).join()}`,
104
+ );
106
105
  }
107
106
  }
108
107
 
@@ -134,7 +133,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
134
133
  }
135
134
 
136
135
  static new(
137
- config: ValidatorClientConfig & Pick<SlasherConfig, 'slashBroadcastedInvalidBlockPenalty'>,
136
+ config: ValidatorClientFullConfig,
138
137
  blockBuilder: IFullNodeBlockBuilder,
139
138
  epochCache: EpochCache,
140
139
  p2pClient: P2P,
@@ -146,7 +145,9 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
146
145
  telemetry: TelemetryClient = getTelemetryClient(),
147
146
  ) {
148
147
  const metrics = new ValidatorMetrics(telemetry);
149
- const blockProposalValidator = new BlockProposalValidator(epochCache);
148
+ const blockProposalValidator = new BlockProposalValidator(epochCache, {
149
+ txsPermitted: !config.disableTransactions,
150
+ });
150
151
  const blockProposalHandler = new BlockProposalHandler(
151
152
  blockBuilder,
152
153
  blockSource,
@@ -183,8 +184,13 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
183
184
  }
184
185
 
185
186
  // Proxy method for backwards compatibility with tests
186
- public reExecuteTransactions(proposal: BlockProposal, txs: any[], l1ToL2Messages: Fr[]): Promise<any> {
187
- return this.blockProposalHandler.reexecuteTransactions(proposal, txs, l1ToL2Messages);
187
+ public reExecuteTransactions(
188
+ proposal: BlockProposal,
189
+ blockNumber: number,
190
+ txs: any[],
191
+ l1ToL2Messages: Fr[],
192
+ ): Promise<any> {
193
+ return this.blockProposalHandler.reexecuteTransactions(proposal, blockNumber, txs, l1ToL2Messages);
188
194
  }
189
195
 
190
196
  public signWithAddress(addr: EthAddress, msg: TypedDataDefinition) {
@@ -256,13 +262,18 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
256
262
  const slotNumber = proposal.slotNumber.toBigInt();
257
263
  const proposer = proposal.getSender();
258
264
 
265
+ // Reject proposals with invalid signatures
266
+ if (!proposer) {
267
+ this.log.warn(`Received proposal with invalid signature for slot ${slotNumber}`);
268
+ return undefined;
269
+ }
270
+
259
271
  // Check that I have any address in current committee before attesting
260
272
  const inCommittee = await this.epochCache.filterInCommittee(slotNumber, this.getValidatorAddresses());
261
273
  const partOfCommittee = inCommittee.length > 0;
262
- const incFailedAttestation = (reason: string) => this.metrics.incFailedAttestations(1, reason, partOfCommittee);
263
274
 
264
275
  const proposalInfo = { ...proposal.toBlockInfo(), proposer: proposer.toString() };
265
- this.log.info(`Received proposal for block ${proposal.blockNumber} at slot ${slotNumber}`, {
276
+ this.log.info(`Received proposal for slot ${slotNumber}`, {
266
277
  ...proposalInfo,
267
278
  txHashes: proposal.txHashes.map(t => t.toString()),
268
279
  });
@@ -283,9 +294,28 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
283
294
 
284
295
  if (!validationResult.isValid) {
285
296
  this.log.warn(`Proposal validation failed: ${validationResult.reason}`, proposalInfo);
286
- incFailedAttestation(validationResult.reason || 'unknown');
287
297
 
288
- // Slash invalid block proposals
298
+ // Only track attestation failure metrics if we're actually in the committee
299
+ if (partOfCommittee) {
300
+ const reason = validationResult.reason || 'unknown';
301
+ // Classify failure reason: bad proposal vs node issue
302
+ const badProposalReasons: BlockProposalValidationFailureReason[] = [
303
+ 'invalid_proposal',
304
+ 'state_mismatch',
305
+ 'failed_txs',
306
+ 'in_hash_mismatch',
307
+ 'parent_block_wrong_slot',
308
+ ];
309
+
310
+ if (badProposalReasons.includes(reason as BlockProposalValidationFailureReason)) {
311
+ this.metrics.incFailedAttestationsBadProposal(1, reason);
312
+ } else {
313
+ // Node issues: parent_block_not_found, block_number_already_exists, txs_not_available, timeout, unknown_error
314
+ this.metrics.incFailedAttestationsNodeIssue(1, reason);
315
+ }
316
+ }
317
+
318
+ // Slash invalid block proposals (can happen even when not in committee)
289
319
  if (
290
320
  validationResult.reason &&
291
321
  SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT.includes(validationResult.reason) &&
@@ -304,8 +334,8 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
304
334
  }
305
335
 
306
336
  // Provided all of the above checks pass, we can attest to the proposal
307
- this.log.info(`Attesting to proposal for block ${proposal.blockNumber} at slot ${slotNumber}`, proposalInfo);
308
- this.metrics.incAttestations(inCommittee.length);
337
+ this.log.info(`Attesting to proposal for slot ${slotNumber}`, proposalInfo);
338
+ this.metrics.incSuccessfulAttestations(inCommittee.length);
309
339
 
310
340
  // If the above function does not throw an error, then we can attest to the proposal
311
341
  return this.createBlockAttestationsFromProposal(proposal, inCommittee);
@@ -314,6 +344,12 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
314
344
  private slashInvalidBlock(proposal: BlockProposal) {
315
345
  const proposer = proposal.getSender();
316
346
 
347
+ // Skip if signature is invalid (shouldn't happen since we validate earlier)
348
+ if (!proposer) {
349
+ this.log.warn(`Cannot slash proposal with invalid signature`);
350
+ return;
351
+ }
352
+
317
353
  // Trim the set if it's too big.
318
354
  if (this.proposersOfInvalidBlocks.size > MAX_PROPOSERS_OF_INVALID_BLOCKS) {
319
355
  // remove oldest proposer. `values` is guaranteed to be in insertion order.
@@ -334,7 +370,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
334
370
 
335
371
  async createBlockProposal(
336
372
  blockNumber: number,
337
- header: ProposedBlockHeader,
373
+ header: CheckpointHeader,
338
374
  archive: Fr,
339
375
  stateReference: StateReference,
340
376
  txs: Tx[],
@@ -347,13 +383,12 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
347
383
  }
348
384
 
349
385
  const newProposal = await this.validationService.createBlockProposal(
350
- blockNumber,
351
386
  header,
352
387
  archive,
353
388
  stateReference,
354
389
  txs,
355
390
  proposerAddress,
356
- options,
391
+ { ...options, broadcastInvalidBlockProposal: this.config.broadcastInvalidBlockProposal },
357
392
  );
358
393
  this.previousProposal = newProposal;
359
394
  return newProposal;
@@ -365,7 +400,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
365
400
 
366
401
  async signAttestationsAndSigners(
367
402
  attestationsAndSigners: CommitteeAttestationsAndSigners,
368
- proposer: EthAddress | undefined,
403
+ proposer: EthAddress,
369
404
  ): Promise<Signature> {
370
405
  return await this.validationService.signAttestationsAndSigners(attestationsAndSigners, proposer);
371
406
  }
@@ -396,13 +431,33 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
396
431
 
397
432
  let attestations: BlockAttestation[] = [];
398
433
  while (true) {
399
- const collectedAttestations = await this.p2pClient.getAttestationsForSlot(slot, proposalId);
434
+ // Filter out attestations with a mismatching payload. This should NOT happen since we have verified
435
+ // the proposer signature (ie our own) before accepting the attestation into the pool via the p2p client.
436
+ const collectedAttestations = (await this.p2pClient.getAttestationsForSlot(slot, proposalId)).filter(
437
+ attestation => {
438
+ if (!attestation.payload.equals(proposal.payload)) {
439
+ this.log.warn(
440
+ `Received attestation for slot ${slot} with mismatched payload from ${attestation.getSender()?.toString()}`,
441
+ { attestationPayload: attestation.payload, proposalPayload: proposal.payload },
442
+ );
443
+ return false;
444
+ }
445
+ return true;
446
+ },
447
+ );
448
+
449
+ // Log new attestations we collected
400
450
  const oldSenders = attestations.map(attestation => attestation.getSender());
401
451
  for (const collected of collectedAttestations) {
402
452
  const collectedSender = collected.getSender();
453
+ // Skip attestations with invalid signatures
454
+ if (!collectedSender) {
455
+ this.log.warn(`Skipping attestation with invalid signature for slot ${slot}`);
456
+ continue;
457
+ }
403
458
  if (
404
459
  !myAddresses.some(address => address.equals(collectedSender)) &&
405
- !oldSenders.some(sender => sender.equals(collectedSender))
460
+ !oldSenders.some(sender => sender?.equals(collectedSender))
406
461
  ) {
407
462
  this.log.debug(`Received attestation for slot ${slot} from ${collectedSender.toString()}`);
408
463
  }
@@ -419,7 +474,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
419
474
  throw new AttestationTimeoutError(attestations.length, required, slot);
420
475
  }
421
476
 
422
- this.log.debug(`Collected ${attestations.length} attestations so far`);
477
+ this.log.debug(`Collected ${attestations.length} of ${required} attestations so far`);
423
478
  await sleep(this.config.attestationPollingIntervalMs);
424
479
  }
425
480
  }