@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/dest/block_proposal_handler.d.ts +23 -18
- package/dest/block_proposal_handler.d.ts.map +1 -1
- package/dest/block_proposal_handler.js +89 -60
- package/dest/duties/validation_service.d.ts +9 -7
- package/dest/duties/validation_service.d.ts.map +1 -1
- package/dest/duties/validation_service.js +15 -10
- package/dest/factory.d.ts.map +1 -1
- package/dest/factory.js +3 -1
- package/dest/metrics.d.ts +6 -4
- package/dest/metrics.d.ts.map +1 -1
- package/dest/metrics.js +23 -13
- package/dest/validator.d.ts +10 -9
- package/dest/validator.d.ts.map +1 -1
- package/dest/validator.js +68 -17
- package/package.json +12 -12
- package/src/block_proposal_handler.ts +116 -87
- package/src/duties/validation_service.ts +21 -15
- package/src/factory.ts +3 -1
- package/src/metrics.ts +32 -14
- package/src/validator.ts +86 -31
package/src/metrics.ts
CHANGED
|
@@ -10,8 +10,9 @@ import {
|
|
|
10
10
|
|
|
11
11
|
export class ValidatorMetrics {
|
|
12
12
|
private failedReexecutionCounter: UpDownCounter;
|
|
13
|
-
private
|
|
14
|
-
private
|
|
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.
|
|
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.
|
|
35
|
-
|
|
36
|
-
|
|
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]:
|
|
80
|
+
[Attributes.BLOCK_PROPOSER]: proposer?.toString() ?? 'unknown',
|
|
68
81
|
});
|
|
69
82
|
}
|
|
70
83
|
|
|
71
|
-
public
|
|
72
|
-
this.
|
|
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
|
|
76
|
-
this.
|
|
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 {
|
|
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:
|
|
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(
|
|
187
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
|
308
|
-
this.metrics.
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
}
|