@aztec/validator-client 0.0.1-commit.18ccd8f0 → 0.0.1-commit.1bb068fb5
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 +21 -18
- package/dest/block_proposal_handler.d.ts +1 -1
- package/dest/block_proposal_handler.d.ts.map +1 -1
- package/dest/block_proposal_handler.js +1 -1
- package/dest/checkpoint_builder.d.ts +5 -9
- package/dest/checkpoint_builder.d.ts.map +1 -1
- package/dest/checkpoint_builder.js +22 -14
- package/dest/config.d.ts +1 -1
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +4 -0
- package/dest/duties/validation_service.d.ts +2 -2
- package/dest/duties/validation_service.d.ts.map +1 -1
- package/dest/duties/validation_service.js +3 -3
- package/dest/key_store/ha_key_store.d.ts +1 -1
- package/dest/key_store/ha_key_store.d.ts.map +1 -1
- package/dest/key_store/ha_key_store.js +2 -2
- package/dest/validator.d.ts +24 -5
- package/dest/validator.d.ts.map +1 -1
- package/dest/validator.js +126 -16
- package/package.json +19 -19
- package/src/block_proposal_handler.ts +1 -0
- package/src/checkpoint_builder.ts +23 -12
- package/src/config.ts +4 -0
- package/src/duties/validation_service.ts +9 -2
- package/src/key_store/ha_key_store.ts +2 -2
- package/src/validator.ts +175 -22
package/src/validator.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { BlobClientInterface } from '@aztec/blob-client/client';
|
|
2
2
|
import { type Blob, getBlobsPerL1Block } from '@aztec/blob-lib';
|
|
3
3
|
import type { EpochCache } from '@aztec/epoch-cache';
|
|
4
|
+
import { validateFeeAssetPriceModifier } from '@aztec/ethereum/contracts';
|
|
4
5
|
import {
|
|
5
6
|
BlockNumber,
|
|
6
7
|
CheckpointNumber,
|
|
@@ -18,12 +19,12 @@ import { RunningPromise } from '@aztec/foundation/running-promise';
|
|
|
18
19
|
import { sleep } from '@aztec/foundation/sleep';
|
|
19
20
|
import { DateProvider } from '@aztec/foundation/timer';
|
|
20
21
|
import type { KeystoreManager } from '@aztec/node-keystore';
|
|
21
|
-
import type { P2P, PeerId } from '@aztec/p2p';
|
|
22
|
+
import type { DuplicateAttestationInfo, DuplicateProposalInfo, P2P, PeerId } from '@aztec/p2p';
|
|
22
23
|
import { AuthRequest, AuthResponse, BlockProposalValidator, ReqRespSubProtocol } from '@aztec/p2p';
|
|
23
24
|
import { OffenseType, WANT_TO_SLASH_EVENT, type Watcher, type WatcherEmitter } from '@aztec/slasher';
|
|
24
25
|
import type { AztecAddress } from '@aztec/stdlib/aztec-address';
|
|
25
26
|
import type { CommitteeAttestationsAndSigners, L2Block, L2BlockSink, L2BlockSource } from '@aztec/stdlib/block';
|
|
26
|
-
import { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
|
|
27
|
+
import { getEpochAtSlot, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
|
|
27
28
|
import type {
|
|
28
29
|
CreateCheckpointProposalLastBlockData,
|
|
29
30
|
ITxProvider,
|
|
@@ -32,14 +33,14 @@ import type {
|
|
|
32
33
|
WorldStateSynchronizer,
|
|
33
34
|
} from '@aztec/stdlib/interfaces/server';
|
|
34
35
|
import { type L1ToL2MessageSource, accumulateCheckpointOutHashes } from '@aztec/stdlib/messaging';
|
|
35
|
-
import
|
|
36
|
-
BlockProposal,
|
|
37
|
-
BlockProposalOptions,
|
|
38
|
-
CheckpointAttestation,
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
import {
|
|
37
|
+
type BlockProposal,
|
|
38
|
+
type BlockProposalOptions,
|
|
39
|
+
type CheckpointAttestation,
|
|
40
|
+
CheckpointProposal,
|
|
41
|
+
type CheckpointProposalCore,
|
|
42
|
+
type CheckpointProposalOptions,
|
|
41
43
|
} from '@aztec/stdlib/p2p';
|
|
42
|
-
import { CheckpointProposal } from '@aztec/stdlib/p2p';
|
|
43
44
|
import type { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
44
45
|
import type { BlockHeader, CheckpointGlobalVariables, Tx } from '@aztec/stdlib/tx';
|
|
45
46
|
import { AttestationTimeoutError } from '@aztec/stdlib/validators';
|
|
@@ -80,14 +81,20 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
80
81
|
// Whether it has already registered handlers on the p2p client
|
|
81
82
|
private hasRegisteredHandlers = false;
|
|
82
83
|
|
|
83
|
-
|
|
84
|
-
private
|
|
84
|
+
/** Tracks the last block proposal we created, to detect duplicate proposal attempts. */
|
|
85
|
+
private lastProposedBlock?: BlockProposal;
|
|
86
|
+
|
|
87
|
+
/** Tracks the last checkpoint proposal we created. */
|
|
88
|
+
private lastProposedCheckpoint?: CheckpointProposal;
|
|
85
89
|
|
|
86
90
|
private lastEpochForCommitteeUpdateLoop: EpochNumber | undefined;
|
|
87
91
|
private epochCacheUpdateLoop: RunningPromise;
|
|
88
92
|
|
|
89
93
|
private proposersOfInvalidBlocks: Set<string> = new Set();
|
|
90
94
|
|
|
95
|
+
/** Tracks the last checkpoint proposal we attested to, to prevent equivocation. */
|
|
96
|
+
private lastAttestedProposal?: CheckpointProposalCore;
|
|
97
|
+
|
|
91
98
|
protected constructor(
|
|
92
99
|
private keyStore: ExtendedValidatorKeyStore,
|
|
93
100
|
private epochCache: EpochCache,
|
|
@@ -309,6 +316,16 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
309
316
|
): Promise<CheckpointAttestation[] | undefined> => this.attestToCheckpointProposal(checkpoint, proposalSender);
|
|
310
317
|
this.p2pClient.registerCheckpointProposalHandler(checkpointHandler);
|
|
311
318
|
|
|
319
|
+
// Duplicate proposal handler - triggers slashing for equivocation
|
|
320
|
+
this.p2pClient.registerDuplicateProposalCallback((info: DuplicateProposalInfo) => {
|
|
321
|
+
this.handleDuplicateProposal(info);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// Duplicate attestation handler - triggers slashing for attestation equivocation
|
|
325
|
+
this.p2pClient.registerDuplicateAttestationCallback((info: DuplicateAttestationInfo) => {
|
|
326
|
+
this.handleDuplicateAttestation(info);
|
|
327
|
+
});
|
|
328
|
+
|
|
312
329
|
const myAddresses = this.getValidatorAddresses();
|
|
313
330
|
this.p2pClient.registerThisValidatorAddresses(myAddresses);
|
|
314
331
|
|
|
@@ -336,6 +353,15 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
336
353
|
return false;
|
|
337
354
|
}
|
|
338
355
|
|
|
356
|
+
// Ignore proposals from ourselves (may happen in HA setups)
|
|
357
|
+
if (this.getValidatorAddresses().some(addr => addr.equals(proposer))) {
|
|
358
|
+
this.log.warn(`Ignoring block proposal from self for slot ${slotNumber}`, {
|
|
359
|
+
proposer: proposer.toString(),
|
|
360
|
+
slotNumber,
|
|
361
|
+
});
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
|
|
339
365
|
// Check if we're in the committee (for metrics purposes)
|
|
340
366
|
const inCommittee = await this.epochCache.filterInCommittee(slotNumber, this.getValidatorAddresses());
|
|
341
367
|
const partOfCommittee = inCommittee.length > 0;
|
|
@@ -437,6 +463,23 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
437
463
|
return undefined;
|
|
438
464
|
}
|
|
439
465
|
|
|
466
|
+
// Ignore proposals from ourselves (may happen in HA setups)
|
|
467
|
+
if (this.getValidatorAddresses().some(addr => addr.equals(proposer))) {
|
|
468
|
+
this.log.warn(`Ignoring block proposal from self for slot ${slotNumber}`, {
|
|
469
|
+
proposer: proposer.toString(),
|
|
470
|
+
slotNumber,
|
|
471
|
+
});
|
|
472
|
+
return undefined;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Validate fee asset price modifier is within allowed range
|
|
476
|
+
if (!validateFeeAssetPriceModifier(proposal.feeAssetPriceModifier)) {
|
|
477
|
+
this.log.warn(
|
|
478
|
+
`Received checkpoint proposal with invalid feeAssetPriceModifier ${proposal.feeAssetPriceModifier} for slot ${slotNumber}`,
|
|
479
|
+
);
|
|
480
|
+
return undefined;
|
|
481
|
+
}
|
|
482
|
+
|
|
440
483
|
// Check that I have any address in current committee before attesting
|
|
441
484
|
const inCommittee = await this.epochCache.filterInCommittee(slotNumber, this.getValidatorAddresses());
|
|
442
485
|
const partOfCommittee = inCommittee.length > 0;
|
|
@@ -510,15 +553,45 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
510
553
|
return undefined;
|
|
511
554
|
}
|
|
512
555
|
|
|
513
|
-
return this.createCheckpointAttestationsFromProposal(proposal, attestors);
|
|
556
|
+
return await this.createCheckpointAttestationsFromProposal(proposal, attestors);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Checks if we should attest to a slot based on equivocation prevention rules.
|
|
561
|
+
* @returns true if we should attest, false if we should skip
|
|
562
|
+
*/
|
|
563
|
+
private shouldAttestToSlot(slotNumber: SlotNumber): boolean {
|
|
564
|
+
// If attestToEquivocatedProposals is true, always allow
|
|
565
|
+
if (this.config.attestToEquivocatedProposals) {
|
|
566
|
+
return true;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Check if incoming slot is strictly greater than last attested
|
|
570
|
+
if (this.lastAttestedProposal && slotNumber <= this.lastAttestedProposal.slotNumber) {
|
|
571
|
+
this.log.warn(
|
|
572
|
+
`Refusing to process a proposal for slot ${slotNumber} given we already attested to a proposal for slot ${this.lastAttestedProposal.slotNumber}`,
|
|
573
|
+
);
|
|
574
|
+
return false;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
return true;
|
|
514
578
|
}
|
|
515
579
|
|
|
516
580
|
private async createCheckpointAttestationsFromProposal(
|
|
517
581
|
proposal: CheckpointProposalCore,
|
|
518
582
|
attestors: EthAddress[] = [],
|
|
519
|
-
): Promise<CheckpointAttestation[]> {
|
|
583
|
+
): Promise<CheckpointAttestation[] | undefined> {
|
|
584
|
+
// Equivocation check: must happen right before signing to minimize the race window
|
|
585
|
+
if (!this.shouldAttestToSlot(proposal.slotNumber)) {
|
|
586
|
+
return undefined;
|
|
587
|
+
}
|
|
588
|
+
|
|
520
589
|
const attestations = await this.validationService.attestToCheckpointProposal(proposal, attestors);
|
|
521
|
-
|
|
590
|
+
|
|
591
|
+
// Track the proposal we attested to (to prevent equivocation)
|
|
592
|
+
this.lastAttestedProposal = proposal;
|
|
593
|
+
|
|
594
|
+
await this.p2pClient.addOwnCheckpointAttestations(attestations);
|
|
522
595
|
return attestations;
|
|
523
596
|
}
|
|
524
597
|
|
|
@@ -531,7 +604,11 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
531
604
|
proposalInfo: LogData,
|
|
532
605
|
): Promise<{ isValid: true } | { isValid: false; reason: string }> {
|
|
533
606
|
const slot = proposal.slotNumber;
|
|
534
|
-
|
|
607
|
+
|
|
608
|
+
// Timeout block syncing at the start of the next slot
|
|
609
|
+
const config = this.checkpointsBuilder.getConfig();
|
|
610
|
+
const nextSlotTimestampSeconds = Number(getTimestampForSlot(SlotNumber(slot + 1), config));
|
|
611
|
+
const timeoutSeconds = Math.max(1, nextSlotTimestampSeconds - Math.floor(this.dateProvider.now() / 1000));
|
|
535
612
|
|
|
536
613
|
// Wait for last block to sync by archive
|
|
537
614
|
let lastBlockHeader: BlockHeader | undefined;
|
|
@@ -597,6 +674,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
597
674
|
const checkpointBuilder = await this.checkpointsBuilder.openCheckpoint(
|
|
598
675
|
checkpointNumber,
|
|
599
676
|
constants,
|
|
677
|
+
proposal.feeAssetPriceModifier,
|
|
600
678
|
l1ToL2Messages,
|
|
601
679
|
previousCheckpointOutHashes,
|
|
602
680
|
fork,
|
|
@@ -721,6 +799,52 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
721
799
|
]);
|
|
722
800
|
}
|
|
723
801
|
|
|
802
|
+
/**
|
|
803
|
+
* Handle detection of a duplicate proposal (equivocation).
|
|
804
|
+
* Emits a slash event when a proposer sends multiple proposals for the same position.
|
|
805
|
+
*/
|
|
806
|
+
private handleDuplicateProposal(info: DuplicateProposalInfo): void {
|
|
807
|
+
const { slot, proposer, type } = info;
|
|
808
|
+
|
|
809
|
+
this.log.warn(`Triggering slash event for duplicate ${type} proposal from ${proposer.toString()} at slot ${slot}`, {
|
|
810
|
+
proposer: proposer.toString(),
|
|
811
|
+
slot,
|
|
812
|
+
type,
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
// Emit slash event
|
|
816
|
+
this.emit(WANT_TO_SLASH_EVENT, [
|
|
817
|
+
{
|
|
818
|
+
validator: proposer,
|
|
819
|
+
amount: this.config.slashDuplicateProposalPenalty,
|
|
820
|
+
offenseType: OffenseType.DUPLICATE_PROPOSAL,
|
|
821
|
+
epochOrSlot: BigInt(slot),
|
|
822
|
+
},
|
|
823
|
+
]);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/**
|
|
827
|
+
* Handle detection of a duplicate attestation (equivocation).
|
|
828
|
+
* Emits a slash event when an attester signs attestations for different proposals at the same slot.
|
|
829
|
+
*/
|
|
830
|
+
private handleDuplicateAttestation(info: DuplicateAttestationInfo): void {
|
|
831
|
+
const { slot, attester } = info;
|
|
832
|
+
|
|
833
|
+
this.log.warn(`Triggering slash event for duplicate attestation from ${attester.toString()} at slot ${slot}`, {
|
|
834
|
+
attester: attester.toString(),
|
|
835
|
+
slot,
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
this.emit(WANT_TO_SLASH_EVENT, [
|
|
839
|
+
{
|
|
840
|
+
validator: attester,
|
|
841
|
+
amount: this.config.slashDuplicateAttestationPenalty,
|
|
842
|
+
offenseType: OffenseType.DUPLICATE_ATTESTATION,
|
|
843
|
+
epochOrSlot: BigInt(slot),
|
|
844
|
+
},
|
|
845
|
+
]);
|
|
846
|
+
}
|
|
847
|
+
|
|
724
848
|
async createBlockProposal(
|
|
725
849
|
blockHeader: BlockHeader,
|
|
726
850
|
indexWithinCheckpoint: IndexWithinCheckpoint,
|
|
@@ -730,11 +854,19 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
730
854
|
proposerAddress: EthAddress | undefined,
|
|
731
855
|
options: BlockProposalOptions = {},
|
|
732
856
|
): Promise<BlockProposal> {
|
|
733
|
-
//
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
857
|
+
// Validate that we're not creating a proposal for an older or equal position
|
|
858
|
+
if (this.lastProposedBlock) {
|
|
859
|
+
const lastSlot = this.lastProposedBlock.slotNumber;
|
|
860
|
+
const lastIndex = this.lastProposedBlock.indexWithinCheckpoint;
|
|
861
|
+
const newSlot = blockHeader.globalVariables.slotNumber;
|
|
862
|
+
|
|
863
|
+
if (newSlot < lastSlot || (newSlot === lastSlot && indexWithinCheckpoint <= lastIndex)) {
|
|
864
|
+
throw new Error(
|
|
865
|
+
`Cannot create block proposal for slot ${newSlot} index ${indexWithinCheckpoint}: ` +
|
|
866
|
+
`already proposed block for slot ${lastSlot} index ${lastIndex}`,
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
738
870
|
|
|
739
871
|
this.log.info(
|
|
740
872
|
`Assembling block proposal for block ${blockHeader.globalVariables.blockNumber} slot ${blockHeader.globalVariables.slotNumber}`,
|
|
@@ -751,25 +883,42 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
751
883
|
broadcastInvalidBlockProposal: this.config.broadcastInvalidBlockProposal,
|
|
752
884
|
},
|
|
753
885
|
);
|
|
754
|
-
this.
|
|
886
|
+
this.lastProposedBlock = newProposal;
|
|
755
887
|
return newProposal;
|
|
756
888
|
}
|
|
757
889
|
|
|
758
890
|
async createCheckpointProposal(
|
|
759
891
|
checkpointHeader: CheckpointHeader,
|
|
760
892
|
archive: Fr,
|
|
893
|
+
feeAssetPriceModifier: bigint,
|
|
761
894
|
lastBlockInfo: CreateCheckpointProposalLastBlockData | undefined,
|
|
762
895
|
proposerAddress: EthAddress | undefined,
|
|
763
896
|
options: CheckpointProposalOptions = {},
|
|
764
897
|
): Promise<CheckpointProposal> {
|
|
898
|
+
// Validate that we're not creating a proposal for an older or equal slot
|
|
899
|
+
if (this.lastProposedCheckpoint) {
|
|
900
|
+
const lastSlot = this.lastProposedCheckpoint.slotNumber;
|
|
901
|
+
const newSlot = checkpointHeader.slotNumber;
|
|
902
|
+
|
|
903
|
+
if (newSlot <= lastSlot) {
|
|
904
|
+
throw new Error(
|
|
905
|
+
`Cannot create checkpoint proposal for slot ${newSlot}: ` +
|
|
906
|
+
`already proposed checkpoint for slot ${lastSlot}`,
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
765
911
|
this.log.info(`Assembling checkpoint proposal for slot ${checkpointHeader.slotNumber}`);
|
|
766
|
-
|
|
912
|
+
const newProposal = await this.validationService.createCheckpointProposal(
|
|
767
913
|
checkpointHeader,
|
|
768
914
|
archive,
|
|
915
|
+
feeAssetPriceModifier,
|
|
769
916
|
lastBlockInfo,
|
|
770
917
|
proposerAddress,
|
|
771
918
|
options,
|
|
772
919
|
);
|
|
920
|
+
this.lastProposedCheckpoint = newProposal;
|
|
921
|
+
return newProposal;
|
|
773
922
|
}
|
|
774
923
|
|
|
775
924
|
async broadcastBlockProposal(proposal: BlockProposal): Promise<void> {
|
|
@@ -791,6 +940,10 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
791
940
|
this.log.debug(`Collecting ${inCommittee.length} self-attestations for slot ${slot}`, { inCommittee });
|
|
792
941
|
const attestations = await this.createCheckpointAttestationsFromProposal(proposal, inCommittee);
|
|
793
942
|
|
|
943
|
+
if (!attestations) {
|
|
944
|
+
return [];
|
|
945
|
+
}
|
|
946
|
+
|
|
794
947
|
// We broadcast our own attestations to our peers so, in case our block does not get mined on L1,
|
|
795
948
|
// other nodes can see that our validators did attest to this block proposal, and do not slash us
|
|
796
949
|
// due to inactivity for missed attestations.
|