Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lighthouse eth2 implementation #346

Merged
merged 22 commits into from
Dec 18, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion scripts/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {CGDatabase} from "../src/renderer/services/db/api";
import {LevelDbController} from "../src/main/db/controller";
import rimraf from "rimraf";
import {LighthouseEth2ApiClient} from "../src/renderer/services/eth2/client/lighthouse/lighthouse";
import {config} from "@chainsafe/lodestar-config/lib/presets/minimal";
import {config} from "@chainsafe/lodestar-config/lib/presets/mainnet";
import {LogLevel, WinstonLogger} from "@chainsafe/lodestar-utils";
import {getInteropKey} from "../src/renderer/services/validator/interop_keys";

Expand Down
63 changes: 2 additions & 61 deletions src/renderer/ducks/network/sagas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,9 @@ import {BeaconNode, BeaconNodes} from "../../models/beaconNode";
import database from "../../services/db/api/database";
import * as logger from "electron-log";
import {IEth2ChainHead} from "../../models/types/head";
import {
saveBeaconNode,
loadedValidatorBeaconNodes,
removeBeaconNode,
loadValidatorBeaconNodes,
subscribeToBlockListening,
} from "./actions";
import {saveBeaconNode, loadedValidatorBeaconNodes, removeBeaconNode, loadValidatorBeaconNodes} from "./actions";
import {CGAccount} from "../../models/account";
import {getRegisterSigningKey} from "../register/selectors";
import {getValidatorBeaconNodes, getValidatorBlockSubscription} from "./selectors";
import {getAuthAccount} from "../auth/selectors";

function* saveBeaconNodeSaga({
Expand Down Expand Up @@ -50,7 +43,7 @@ function* removeBeaconNodeSaga({
}

export function* loadValidatorBeaconNodesSaga({
payload: {subscribe, validator},
payload: {validator},
}: ReturnType<typeof loadValidatorBeaconNodes>): Generator<
| SelectEffect
| CallEffect
Expand All @@ -67,58 +60,6 @@ export function* loadValidatorBeaconNodesSaga({
}
const validatorBeaconNodes: BeaconNode[] = yield call(account.getValidatorBeaconNodes, validator);
logger.info(`Found ${validatorBeaconNodes.length} beacon nodes for validator ${validator}.`);
yield all(
validatorBeaconNodes.map(function* (validatorBN) {
if (validatorBN.client) {
try {
const chainHead: IEth2ChainHead = yield validatorBN.client.beacon.state.getBlockHeader(
"head",
"head",
);
const refreshFnWithContext = refreshBeaconNodeStatus.bind(null, validator);
yield call(refreshFnWithContext, chainHead);

if (subscribe) {
const existingTimeout = yield select((state) =>
getValidatorBlockSubscription(state, {validator}),
);
if (!existingTimeout) {
const timeoutId = validatorBN.client.onNewChainHead(refreshFnWithContext);
yield put(subscribeToBlockListening(timeoutId, validator));
}
}
} catch (e) {
yield put(loadedValidatorBeaconNodes(validatorBeaconNodes, validator));
logger.warn("Error while fetching chainhead from beacon node... ", e.message);
}
}
}),
);
}

function* refreshBeaconNodeStatus(
validator: string,
chainHead: IEth2ChainHead,
): Generator<SelectEffect | PutEffect | AllEffect<Promise<BeaconNode>>, void, BeaconNode[]> {
const validatorBeaconNodes = yield select((state) => getValidatorBeaconNodes(state, {validator}));
const beaconNodes: BeaconNode[] = yield all(
validatorBeaconNodes.map(async (validatorBN: BeaconNode) => {
try {
if (!validatorBN.client) {
throw new Error("No ETH2 API client");
}
return {
...validatorBN,
isSyncing: (await validatorBN.client.node.getSyncingStatus()).syncDistance === BigInt(0),
currentSlot: String(chainHead.slot),
};
} catch (e) {
logger.warn(`Error while trying to fetch beacon node status... ${e.message}`);
return validatorBN;
}
}),
);
yield put(loadedValidatorBeaconNodes(beaconNodes, validator));
}

export function* networkSagaWatcher(): Generator {
Expand Down
74 changes: 44 additions & 30 deletions src/renderer/ducks/validator/sagas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {EthersNotifier} from "../../services/deposit/ethers";
import {getValidatorStatus, ValidatorStatus} from "../../services/validator/status";
import {ValidatorLogger} from "../../services/eth2/client/logger";
import database, {cgDbController} from "../../services/db/api/database";
import {config} from "@chainsafe/lodestar-config/lib/presets/minimal";
import {config as mainnetConfig} from "@chainsafe/lodestar-config/lib/presets/mainnet";
import {IByPublicKey, IValidator} from "./slice";
import {
loadValidators,
Expand All @@ -16,7 +16,6 @@ import {
startValidatorService,
stopValidatorService,
loadValidatorStatus,
loadedValidatorsBalance,
stopActiveValidatorService,
startNewValidatorService,
updateValidatorsFromChain,
Expand All @@ -38,8 +37,13 @@ import {ValidatorResponse} from "@chainsafe/lodestar-types";
import * as logger from "electron-log";
import {getAuthAccount} from "../auth/selectors";
import {getBeaconNodes} from "../network/selectors";
import {getValidators} from "./selectors";
import {getValidatorBeaconNodes, getValidators} from "./selectors";
import {ValidatorBeaconNodes} from "../../models/validatorBeaconNodes";
import {CgEth2ApiClient} from "../../services/eth2/client/eth2ApiClient";
import {WinstonLogger} from "@chainsafe/lodestar-utils";
import {Beacon} from "../beacon/slice";
import {readBeaconChainNetwork} from "../../services/eth2/client";
import {INetworkConfig} from "../../services/interfaces";

interface IValidatorServices {
[validatorAddress: string]: Validator;
Expand Down Expand Up @@ -122,14 +126,7 @@ function* loadValidatorsFromChain(
const validatorBeaconNodes: IValidatorBeaconNodes = yield select(getBeaconNodes);
const beaconNodes = validatorBeaconNodes[action.payload[0]];
if (beaconNodes && beaconNodes.length > 0) {
// TODO: Use any working beacon node instead of first one
const client = beaconNodes[0].client;
try {
const response = yield client.beacon.state.getValidators("head", action.payload);
yield put(loadedValidatorsBalance(response));
} catch (e) {
logger.warn("Error while fetching validator balance...", e.message);
}
logger.warn("Error while fetching validator balance...");
}
}

Expand All @@ -153,29 +150,46 @@ function* loadValidatorStatusSaga(

function* startService(
action: ReturnType<typeof startNewValidatorService>,
): Generator<SelectEffect | PutEffect | Promise<void>, void, IValidatorBeaconNodes> {
const logger = new ValidatorLogger();
const validatorBeaconNodes = yield select(getBeaconNodes);
const publicKey = action.payload.publicKey.toHex();
// TODO: Use beacon chain proxy instead of first node
const eth2API = validatorBeaconNodes[publicKey][0].client;
): Generator<
SelectEffect | PutEffect | Promise<void> | Promise<INetworkConfig | null>,
void,
Beacon[] & (INetworkConfig | null)
> {
try {
const publicKey = action.payload.publicKey.toHex();
const beaconNodes = yield select(getValidatorBeaconNodes, {publicKey});
if (!beaconNodes.length) {
throw new Error("missing beacon node");
}

if (!validatorServices[publicKey]) {
validatorServices[publicKey] = new Validator({
slashingProtection: new SlashingProtection({
config,
controller: cgDbController,
}),
api: eth2API,
const config = (yield readBeaconChainNetwork(beaconNodes[0].url))?.eth2Config || mainnetConfig;

// TODO: Use beacon chain proxy instead of first node
const eth2API = new CgEth2ApiClient(config, beaconNodes[0].url);

const slashingProtection = new SlashingProtection({
config,
secretKeys: [action.payload.privateKey],
logger,
graffiti: "ChainGuardian",
controller: cgDbController,
});
}
yield validatorServices[publicKey].start();

yield put(startValidatorService(logger, publicKey));
const logger = new WinstonLogger() as ValidatorLogger;

if (!validatorServices[publicKey]) {
validatorServices[publicKey] = new Validator({
slashingProtection,
api: eth2API,
config,
secretKeys: [action.payload.privateKey],
logger,
graffiti: "ChainGuardian",
});
}
yield validatorServices[publicKey].start();

yield put(startValidatorService(logger, publicKey));
} catch (e) {
logger.error("Failed to start validator", e.message);
}
}

function* stopService(action: ReturnType<typeof stopActiveValidatorService>): Generator<PutEffect | Promise<void>> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import {ICGEth2BeaconApi, ICGETH2BeaconBlocksApi} from "../interface";
import {IBeaconPoolApi, IBeaconStateApi} from "@chainsafe/lodestar-validator/lib/api/interface/beacon";
import {Genesis} from "@chainsafe/lodestar-types";
import {HttpClient} from "../../../api";
import {IBeaconConfig} from "@chainsafe/lodestar-config";
import {Json} from "@chainsafe/ssz";
import {CgEth2BeaconBlocksApi} from "./cgEth2BeaconBlocksApi";
import {CgEth2BeaconStateApi} from "./cgEth2BeaconStateApi";
import {CgEth2BeaconPoolApi} from "./cgEth2BeaconPoolApi";

export class CgEth2BeaconApi implements ICGEth2BeaconApi {
public blocks: ICGETH2BeaconBlocksApi;
public state: IBeaconStateApi;
public pool: IBeaconPoolApi;

private readonly httpClient: HttpClient;
private readonly config: IBeaconConfig;
// TODO: implement logger;
public constructor(config: IBeaconConfig, httpClient: HttpClient) {
this.config = config;
this.httpClient = httpClient;

this.blocks = new CgEth2BeaconBlocksApi(config, httpClient);
this.state = new CgEth2BeaconStateApi(config, httpClient);
this.pool = new CgEth2BeaconPoolApi(config, httpClient);
}

public getGenesis = async (): Promise<Genesis | null> => {
try {
const genesisResponse = await this.httpClient.get<{data: Json}>("/eth/v1/beacon/genesis");
return this.config.types.Genesis.fromJson(genesisResponse.data, {case: "snake"});
} catch (e) {
// TODO: implement logger;
console.error("Failed to obtain genesis time", {error: e.message});
return null;
}
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import {HttpClient} from "../../../api";
import {IBeaconConfig} from "@chainsafe/lodestar-config";
import {SignedBeaconBlock} from "@chainsafe/lodestar-types";
import {Json} from "@chainsafe/ssz";
import {ICGETH2BeaconBlocksApi} from "../interface";

export class CgEth2BeaconBlocksApi implements ICGETH2BeaconBlocksApi {
private readonly httpClient: HttpClient;
private readonly config: IBeaconConfig;
public constructor(config: IBeaconConfig, httpClient: HttpClient) {
this.config = config;
this.httpClient = httpClient;
}

public publishBlock = async (block: SignedBeaconBlock): Promise<void> => {
await this.httpClient.post(
"/eth/v1/beacon/blocks",
this.config.types.SignedBeaconBlock.toJson(block, {case: "snake"}),
);
};

public getBlock = async (blockId: "head" | "genesis" | "finalized" | number): Promise<SignedBeaconBlock> => {
const blocksResponse = await this.httpClient.get<{data: Json}>(`/eth/v1/beacon/blocks/${blockId}`);
return this.config.types.SignedBeaconBlock.fromJson(blocksResponse.data, {case: "snake"});
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {IBeaconPoolApi} from "@chainsafe/lodestar-validator/lib/api/interface/beacon";
import {HttpClient} from "../../../api";
import {IBeaconConfig} from "@chainsafe/lodestar-config";
import {Attestation} from "@chainsafe/lodestar-types";

export class CgEth2BeaconPoolApi implements IBeaconPoolApi {
private readonly httpClient: HttpClient;
private readonly config: IBeaconConfig;
public constructor(config: IBeaconConfig, httpClient: HttpClient) {
this.config = config;
this.httpClient = httpClient;
}

public submitAttestation = async (attestation: Attestation): Promise<void> => {
await this.httpClient.post("/eth/v1/beacon/pool/attestations", [
this.config.types.Attestation.toJson(attestation, {case: "snake"}),
]);
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {BLSPubkey, Fork, ValidatorIndex, ValidatorResponse} from "@chainsafe/lodestar-types";
import {HttpClient} from "../../../api";
import {IBeaconConfig} from "@chainsafe/lodestar-config";
import {Json} from "@chainsafe/ssz";
import {IBeaconStateApi} from "@chainsafe/lodestar-validator/lib/api/interface/beacon";

export class CgEth2BeaconStateApi implements IBeaconStateApi {
private readonly httpClient: HttpClient;
private readonly config: IBeaconConfig;
// TODO: implement logger;
public constructor(config: IBeaconConfig, httpClient: HttpClient) {
this.config = config;
this.httpClient = httpClient;
}

public getFork = async (stateId: "head"): Promise<Fork | null> => {
try {
const forkResponse = await this.httpClient.get<{data: Json}>(`/eth/v1/beacon/states/${stateId}/fork`);
return this.config.types.Fork.fromJson(forkResponse.data, {case: "snake"});
} catch (e) {
// TODO: implement logger;
console.error("Failed to fetch head fork version", {error: e.message});
return null;
}
};

public getStateValidator = async (
stateId: "head",
validatorId: ValidatorIndex | BLSPubkey,
): Promise<ValidatorResponse | null> => {
const id =
typeof validatorId === "number"
? validatorId.toString()
: this.config.types.BLSPubkey.toJson(validatorId)?.toString() ?? "";
try {
const url = `/eth/v1/beacon/states/${stateId}/validators/${id}`;
const stateValidatorResponse = await this.httpClient.get<{data: Json}>(url);
// TODO: remove hack after ssz is updated
// @ts-ignore
stateValidatorResponse.data.pubkey = stateValidatorResponse.data.validator.pubkey;
return this.config.types.ValidatorResponse.fromJson(stateValidatorResponse.data, {case: "snake"});
} catch (e) {
// TODO: implement logger;
console.error("Failed to fetch validator", {validatorId: id, error: e.message});
return null;
}
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import {BeaconEvent, BeaconEventType, IEventsApi} from "@chainsafe/lodestar-validator/lib/api/interface/events";
import {IStoppableEventIterable, LodestarEventIterator} from "@chainsafe/lodestar-utils";
import {IBeaconConfig} from "@chainsafe/lodestar-config";
import {ContainerType} from "@chainsafe/ssz";

export class CgEth2EventsApi implements IEventsApi {
private readonly baseUrl: string;
private readonly config: IBeaconConfig;
public constructor(config: IBeaconConfig, baseUrl: string) {
this.config = config;
this.baseUrl = baseUrl;
}

public getEventStream = (topics: BeaconEventType[]): IStoppableEventIterable<BeaconEvent> => {
const topicsQuery = topics.filter((topic) => topic !== "chain_reorg").join(",");
const url = new URL(`/eth/v1/events?topics=${topicsQuery}`, this.baseUrl);
const eventSource = new EventSource(url.href);
return new LodestarEventIterator(({push}): (() => void) => {
eventSource.onmessage = (event): void => {
if (topics.includes(event.type as BeaconEventType)) {
push(this.deserializeBeaconEventMessage(event));
}
};
return (): void => {
eventSource.close();
};
});
};

private deserializeBeaconEventMessage = (msg: MessageEvent): BeaconEvent => {
switch (msg.type) {
case BeaconEventType.BLOCK:
return {
type: BeaconEventType.BLOCK,
message: this.deserializeEventData(this.config.types.BlockEventPayload, msg.data),
};
case BeaconEventType.CHAIN_REORG:
return {
type: BeaconEventType.CHAIN_REORG,
message: this.deserializeEventData(this.config.types.ChainReorg, msg.data),
};
default:
throw new Error("Unsupported beacon event type " + msg.type);
}
};

private deserializeEventData = <T extends BeaconEvent["message"]>(type: ContainerType<T>, data: string): T => {
return type.fromJson(JSON.parse(data));
};
}
Loading