|
| 1 | +import {init as initBLS, SecretKey} from "@chainsafe/bls"; |
| 2 | +import {LogLevel, WinstonLogger} from "@chainsafe/lodestar-utils"; |
| 3 | +import {Validator} from "@chainsafe/lodestar-validator"; |
| 4 | +import {CGSlashingProtection} from "../src/renderer/services/eth2/client/slashingProtection"; |
| 5 | +import sinon from "sinon"; |
| 6 | +import {computeEpochAtSlot} from "@chainsafe/lodestar-beacon-state-transition"; |
| 7 | +import {BeaconCommitteeResponse, BLSPubkey, ProposerDuty} from "@chainsafe/lodestar-types"; |
| 8 | +import {AttestationEvent, CGBeaconEventType, ICgEth2ApiClient} from "../src/renderer/services/eth2/client/interface"; |
| 9 | +import {BeaconBlockEvent, BeaconEventType} from "@chainsafe/lodestar-validator/lib/api/interface/events"; |
| 10 | +import {IBeaconConfig} from "@chainsafe/lodestar-config"; |
| 11 | +import {CgEth2ApiClient} from "../src/renderer/services/eth2/client/module"; |
| 12 | + |
| 13 | +const getCommitteesFactory = (apiClient: ICgEth2ApiClient) => async ( |
| 14 | + validatorIndex: number, |
| 15 | + blockSlot: number, |
| 16 | + ignoreBefore?: number, |
| 17 | +): Promise<BeaconCommitteeResponse[]> => { |
| 18 | + const response = await apiClient.beacon.state.getCommittees(blockSlot); |
| 19 | + return response.filter(({validators, slot}: BeaconCommitteeResponse) => |
| 20 | + [...validators].some( |
| 21 | + (index) => index === validatorIndex && (slot > ignoreBefore || ignoreBefore === undefined), |
| 22 | + ), |
| 23 | + ); |
| 24 | +}; |
| 25 | + |
| 26 | +const getProposerFactory = (apiClient: ICgEth2ApiClient) => async ( |
| 27 | + pubKey: BLSPubkey, |
| 28 | + epoch: number, |
| 29 | + index: number, |
| 30 | + ignoreBefore?: number, |
| 31 | +): Promise<ProposerDuty[]> => { |
| 32 | + const result = await apiClient.validator.getProposerDuties(epoch, [pubKey]); |
| 33 | + return [...result].filter( |
| 34 | + ({validatorIndex, slot}) => validatorIndex === index && (slot > ignoreBefore || ignoreBefore === undefined), |
| 35 | + ); |
| 36 | +}; |
| 37 | + |
| 38 | +const processAttestation = ( |
| 39 | + {data}: AttestationEvent["message"], |
| 40 | + committees: BeaconCommitteeResponse[], |
| 41 | + attestations: Map<number, boolean>, |
| 42 | +): void => { |
| 43 | + const committee = committees.find(({slot, index}) => slot === data.slot && index === data.index); |
| 44 | + if (committee) { |
| 45 | + attestations.set(committee.slot, true); |
| 46 | + } |
| 47 | +}; |
| 48 | + |
| 49 | +const processBlock = async ( |
| 50 | + {slot}: BeaconBlockEvent["message"], |
| 51 | + lastEpoch: number, |
| 52 | + firstSlot: number, |
| 53 | + validatorPublicKeyBytes: Uint8Array, |
| 54 | + validatorIndex: number, |
| 55 | + proposers: Map<number, boolean>, |
| 56 | + attestations: Map<number, boolean>, |
| 57 | + committees: BeaconCommitteeResponse[], |
| 58 | + getProposer: ReturnType<typeof getProposerFactory>, |
| 59 | + getCommittees: ReturnType<typeof getCommitteesFactory>, |
| 60 | + limit: number, |
| 61 | + config: IBeaconConfig, |
| 62 | +): Promise<{ |
| 63 | + epoch: number; |
| 64 | + lastEpoch: number; |
| 65 | + committees: BeaconCommitteeResponse[]; |
| 66 | +}> => { |
| 67 | + if (proposers.has(slot)) proposers.set(slot, true); |
| 68 | + |
| 69 | + const epoch = computeEpochAtSlot(config, slot); |
| 70 | + if (lastEpoch !== epoch && limit !== epoch) { |
| 71 | + // eslint-disable-next-line no-param-reassign |
| 72 | + lastEpoch = epoch; |
| 73 | + |
| 74 | + const proposerResponse = await getProposer(validatorPublicKeyBytes, epoch, validatorIndex, firstSlot); |
| 75 | + proposerResponse.forEach(({slot: s}) => { |
| 76 | + proposers.set(s, false); |
| 77 | + }); |
| 78 | + |
| 79 | + // eslint-disable-next-line no-param-reassign |
| 80 | + committees = await getCommittees(validatorIndex, slot, firstSlot); |
| 81 | + committees.forEach(({slot: s}) => { |
| 82 | + attestations.set(s, false); |
| 83 | + }); |
| 84 | + } |
| 85 | + |
| 86 | + return {epoch, lastEpoch, committees}; |
| 87 | +}; |
| 88 | + |
| 89 | +export const restValidation = ({ |
| 90 | + baseUrl, |
| 91 | + getValidatorPrivateKey, |
| 92 | + limit, |
| 93 | + config, |
| 94 | + ApiClient, |
| 95 | +}: { |
| 96 | + baseUrl: string; |
| 97 | + getValidatorPrivateKey: () => Promise<SecretKey>; |
| 98 | + limit: number; |
| 99 | + ApiClient: typeof CgEth2ApiClient; |
| 100 | + config: IBeaconConfig; |
| 101 | +}): Promise<{ |
| 102 | + proposer: { |
| 103 | + proposed: number; |
| 104 | + delegated: number; |
| 105 | + }; |
| 106 | + attestation: { |
| 107 | + attestations: number; |
| 108 | + delegated: number; |
| 109 | + }; |
| 110 | +}> => |
| 111 | + new Promise((resolve) => { |
| 112 | + (async (): Promise<void> => { |
| 113 | + process.env.NODE_ENV = "validator-test"; |
| 114 | + await initBLS("blst-native"); |
| 115 | + const validatorPrivateKey = await getValidatorPrivateKey(); |
| 116 | + const validatorPublicKey = validatorPrivateKey.toPublicKey(); |
| 117 | + const validatorPublicKeyBytes = validatorPublicKey.toBytes(); |
| 118 | + console.log("Starting validator " + validatorPublicKey.toHex()); |
| 119 | + |
| 120 | + const logger = new WinstonLogger({module: "ChainGuardian", level: LogLevel.verbose}); |
| 121 | + const slashingProtection = sinon.createStubInstance(CGSlashingProtection); |
| 122 | + |
| 123 | + const eth2API = new ApiClient(config, baseUrl); |
| 124 | + const validatorService = new Validator({ |
| 125 | + slashingProtection, |
| 126 | + api: eth2API, |
| 127 | + config, |
| 128 | + secretKeys: [validatorPrivateKey], |
| 129 | + logger, |
| 130 | + graffiti: "ChainGuardian", |
| 131 | + }); |
| 132 | + const validator = await eth2API.beacon.state.getStateValidator("head", validatorPublicKeyBytes); |
| 133 | + await validatorService.start(); |
| 134 | + |
| 135 | + let firstSlot: number | undefined; |
| 136 | + |
| 137 | + let startEpoch = 1; |
| 138 | + let lastEpoch = 1; |
| 139 | + |
| 140 | + const onFirstBlock = ({slot}: BeaconBlockEvent["message"]): void => { |
| 141 | + const epoch = computeEpochAtSlot(config, slot); |
| 142 | + if (!firstSlot) { |
| 143 | + firstSlot = slot; |
| 144 | + startEpoch = epoch; |
| 145 | + } |
| 146 | + }; |
| 147 | + |
| 148 | + const getCommittees = getCommitteesFactory(eth2API); |
| 149 | + let committees: BeaconCommitteeResponse[] = []; |
| 150 | + const attestations = new Map<number, boolean>(); |
| 151 | + |
| 152 | + const getProposer = getProposerFactory(eth2API); |
| 153 | + const proposers = new Map<number, boolean>(); |
| 154 | + |
| 155 | + const stream = await eth2API.events.getEventStream(([ |
| 156 | + CGBeaconEventType.BLOCK, |
| 157 | + CGBeaconEventType.ATTESTATION, |
| 158 | + ] as unknown) as BeaconEventType[]); |
| 159 | + for await (const {type, message} of stream) { |
| 160 | + switch ((type as unknown) as CGBeaconEventType) { |
| 161 | + case CGBeaconEventType.ATTESTATION: { |
| 162 | + processAttestation( |
| 163 | + (message as unknown) as AttestationEvent["message"], |
| 164 | + committees, |
| 165 | + attestations, |
| 166 | + ); |
| 167 | + break; |
| 168 | + } |
| 169 | + case CGBeaconEventType.BLOCK: { |
| 170 | + onFirstBlock((message as unknown) as BeaconBlockEvent["message"]); |
| 171 | + const {epoch, ...rest} = await processBlock( |
| 172 | + (message as unknown) as BeaconBlockEvent["message"], |
| 173 | + lastEpoch, |
| 174 | + firstSlot, |
| 175 | + validatorPublicKeyBytes, |
| 176 | + validator.index, |
| 177 | + proposers, |
| 178 | + attestations, |
| 179 | + committees, |
| 180 | + getProposer, |
| 181 | + getCommittees, |
| 182 | + startEpoch + limit, |
| 183 | + config, |
| 184 | + ); |
| 185 | + |
| 186 | + lastEpoch = rest.lastEpoch; |
| 187 | + committees = rest.committees; |
| 188 | + |
| 189 | + if (startEpoch + limit === epoch) { |
| 190 | + await validatorService.stop(); |
| 191 | + resolve({ |
| 192 | + proposer: { |
| 193 | + proposed: [...proposers.values()].reduce((p, c) => p + Number(c), 0), |
| 194 | + delegated: proposers.size, |
| 195 | + }, |
| 196 | + attestation: { |
| 197 | + attestations: [...attestations.values()].reduce((p, c) => p + Number(c), 0), |
| 198 | + delegated: attestations.size, |
| 199 | + }, |
| 200 | + }); |
| 201 | + } |
| 202 | + break; |
| 203 | + } |
| 204 | + } |
| 205 | + } |
| 206 | + })(); |
| 207 | + }); |
0 commit comments