@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/README.md +41 -0
- package/dest/block_proposal_handler.d.ts +4 -3
- package/dest/block_proposal_handler.d.ts.map +1 -1
- package/dest/block_proposal_handler.js +112 -30
- package/dest/checkpoint_builder.d.ts +14 -4
- package/dest/checkpoint_builder.d.ts.map +1 -1
- package/dest/checkpoint_builder.js +97 -29
- package/dest/config.d.ts +1 -1
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +22 -1
- package/dest/duties/validation_service.d.ts +1 -1
- package/dest/duties/validation_service.d.ts.map +1 -1
- package/dest/duties/validation_service.js +3 -9
- package/dest/factory.d.ts +3 -1
- package/dest/factory.d.ts.map +1 -1
- package/dest/factory.js +2 -2
- package/dest/key_store/ha_key_store.js +1 -1
- package/dest/validator.d.ts +5 -5
- package/dest/validator.d.ts.map +1 -1
- package/dest/validator.js +63 -46
- package/package.json +19 -19
- package/src/block_proposal_handler.ts +134 -37
- package/src/checkpoint_builder.ts +120 -34
- package/src/config.ts +22 -1
- package/src/duties/validation_service.ts +3 -9
- package/src/factory.ts +4 -1
- package/src/key_store/ha_key_store.ts +1 -1
- package/src/validator.ts +74 -54
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 {
|
|
49
|
-
|
|
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
|
|
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.
|
|
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
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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.
|
|
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
|
|
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(
|
|
487
|
-
this.log.warn(`Escape hatch open for slot ${
|
|
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 ${
|
|
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.
|
|
504
|
+
this.log.debug(`Ignoring block proposal from self for slot ${proposalSlotNumber}`, {
|
|
500
505
|
proposer: proposer.toString(),
|
|
501
|
-
|
|
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 ${
|
|
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
|
|
515
|
-
const inCommittee = await this.epochCache.filterInCommittee(
|
|
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
|
-
|
|
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 ${
|
|
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 ${
|
|
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(
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
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(
|
|
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 ${
|
|
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 {
|