Compare commits
No commits in common. "v0.1.0-develop.26" and "v0.1.0-develop.25" have entirely different histories.
v0.1.0-dev
...
v0.1.0-dev
|
@ -1,5 +1,3 @@
|
||||||
# [0.1.0-develop.26](https://git.lumeweb.com/LumeWeb/libethsync/compare/v0.1.0-develop.25...v0.1.0-develop.26) (2023-07-12)
|
|
||||||
|
|
||||||
# [0.1.0-develop.25](https://git.lumeweb.com/LumeWeb/libethsync/compare/v0.1.0-develop.24...v0.1.0-develop.25) (2023-07-12)
|
# [0.1.0-develop.25](https://git.lumeweb.com/LumeWeb/libethsync/compare/v0.1.0-develop.24...v0.1.0-develop.25) (2023-07-12)
|
||||||
|
|
||||||
# [0.1.0-develop.24](https://git.lumeweb.com/LumeWeb/libethsync/compare/v0.1.0-develop.23...v0.1.0-develop.24) (2023-07-11)
|
# [0.1.0-develop.24](https://git.lumeweb.com/LumeWeb/libethsync/compare/v0.1.0-develop.23...v0.1.0-develop.24) (2023-07-11)
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "@lumeweb/libethclient",
|
"name": "@lumeweb/libethclient",
|
||||||
"version": "0.1.0-develop.26",
|
"version": "0.1.0-develop.25",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@lumeweb/libethclient",
|
"name": "@lumeweb/libethclient",
|
||||||
"version": "0.1.0-develop.26",
|
"version": "0.1.0-develop.25",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chainsafe/as-sha256": "^0.3.1",
|
"@chainsafe/as-sha256": "^0.3.1",
|
||||||
"@chainsafe/bls": "7.1.1",
|
"@chainsafe/bls": "7.1.1",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@lumeweb/libethsync",
|
"name": "@lumeweb/libethsync",
|
||||||
"version": "0.1.0-develop.26",
|
"version": "0.1.0-develop.25",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
|
@ -6,16 +6,14 @@ import {
|
||||||
} from "@lodestar/light-client/utils";
|
} from "@lodestar/light-client/utils";
|
||||||
import { init } from "@chainsafe/bls/switchable";
|
import { init } from "@chainsafe/bls/switchable";
|
||||||
import { Mutex } from "async-mutex";
|
import { Mutex } from "async-mutex";
|
||||||
import { fromHexString, toHexString } from "@chainsafe/ssz";
|
import { fromHexString } from "@chainsafe/ssz";
|
||||||
import { deserializePubkeys, getDefaultClientConfig } from "#util.js";
|
import { getDefaultClientConfig } from "#util.js";
|
||||||
import { capella, LightClientUpdate } from "#types.js";
|
|
||||||
import { deserializeSyncCommittee } from "@lodestar/light-client/utils/index.js";
|
import isNode from "detect-node";
|
||||||
import bls from "@chainsafe/bls/switchable.js";
|
|
||||||
import { assertValidLightClientUpdate } from "@lodestar/light-client/validation.js";
|
|
||||||
|
|
||||||
export interface BaseClientOptions {
|
export interface BaseClientOptions {
|
||||||
prover: IProver;
|
prover: IProver;
|
||||||
store: IStore;
|
store?: IStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default abstract class BaseClient {
|
export default abstract class BaseClient {
|
||||||
|
@ -26,10 +24,10 @@ export default abstract class BaseClient {
|
||||||
(pk) => fromHexString(pk),
|
(pk) => fromHexString(pk),
|
||||||
);
|
);
|
||||||
protected genesisPeriod = computeSyncPeriodAtSlot(this.config.genesis.slot);
|
protected genesisPeriod = computeSyncPeriodAtSlot(this.config.genesis.slot);
|
||||||
protected booted = false;
|
|
||||||
protected options: BaseClientOptions;
|
|
||||||
private genesisTime = this.config.genesis.time;
|
private genesisTime = this.config.genesis.time;
|
||||||
|
protected booted = false;
|
||||||
private syncMutex = new Mutex();
|
private syncMutex = new Mutex();
|
||||||
|
protected options: BaseClientOptions;
|
||||||
|
|
||||||
constructor(options: BaseClientOptions) {
|
constructor(options: BaseClientOptions) {
|
||||||
this.options = options;
|
this.options = options;
|
||||||
|
@ -45,10 +43,6 @@ export default abstract class BaseClient {
|
||||||
return this._latestPeriod === this.getCurrentPeriod();
|
return this._latestPeriod === this.getCurrentPeriod();
|
||||||
}
|
}
|
||||||
|
|
||||||
public get store(): IStore {
|
|
||||||
return this.options.store as IStore;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async sync(): Promise<void> {
|
public async sync(): Promise<void> {
|
||||||
await init("herumi");
|
await init("herumi");
|
||||||
|
|
||||||
|
@ -94,6 +88,7 @@ export default abstract class BaseClient {
|
||||||
protected async subscribe(callback?: (ei: ExecutionInfo) => void) {
|
protected async subscribe(callback?: (ei: ExecutionInfo) => void) {
|
||||||
setInterval(async () => {
|
setInterval(async () => {
|
||||||
try {
|
try {
|
||||||
|
await this._sync();
|
||||||
const ei = await this.getLatestExecution();
|
const ei = await this.getLatestExecution();
|
||||||
if (ei && ei.blockHash !== this.latestBlockHash) {
|
if (ei && ei.blockHash !== this.latestBlockHash) {
|
||||||
this.latestBlockHash = ei.blockHash;
|
this.latestBlockHash = ei.blockHash;
|
||||||
|
@ -105,128 +100,16 @@ export default abstract class BaseClient {
|
||||||
}, POLLING_DELAY);
|
}, POLLING_DELAY);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async getLatestExecution(): Promise<ExecutionInfo | null> {
|
public get store(): IStore {
|
||||||
await this._sync();
|
return this.options.store as IStore;
|
||||||
const update = capella.ssz.LightClientUpdate.deserialize(
|
|
||||||
this.store.getUpdate(this.latestPeriod),
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
blockHash: toHexString(update.attestedHeader.execution.blockHash),
|
|
||||||
blockNumber: update.attestedHeader.execution.blockNumber,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
async syncProver(
|
|
||||||
startPeriod: number,
|
|
||||||
currentPeriod: number,
|
|
||||||
startCommittee: Uint8Array[],
|
|
||||||
): Promise<{ syncCommittee: Uint8Array[]; period: number }> {
|
|
||||||
for (let period = startPeriod; period < currentPeriod; period += 1) {
|
|
||||||
try {
|
|
||||||
const updates = await this.options.prover.getSyncUpdate(
|
|
||||||
period,
|
|
||||||
currentPeriod,
|
|
||||||
);
|
|
||||||
|
|
||||||
for (let i = 0; i < updates.length; i++) {
|
|
||||||
const curPeriod = period + i;
|
|
||||||
const update = updates[i];
|
|
||||||
|
|
||||||
const validOrCommittee = await this.syncUpdateVerifyGetCommittee(
|
|
||||||
startCommittee,
|
|
||||||
curPeriod,
|
|
||||||
update,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!(validOrCommittee as boolean)) {
|
|
||||||
console.log(`Found invalid update at period(${curPeriod})`);
|
|
||||||
return {
|
|
||||||
syncCommittee: startCommittee,
|
|
||||||
period: curPeriod,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.options.store.addUpdate(period, update);
|
// committee and prover index of the first honest prover
|
||||||
|
protected abstract syncFromGenesis(): Promise<Uint8Array[]>;
|
||||||
|
|
||||||
startCommittee = validOrCommittee as Uint8Array[];
|
protected abstract syncFromLastUpdate(
|
||||||
period = curPeriod;
|
startPeriod?: number,
|
||||||
}
|
): Promise<Uint8Array[]>;
|
||||||
} catch (e) {
|
|
||||||
console.error(`failed to fetch sync update for period(${period})`);
|
|
||||||
return {
|
|
||||||
syncCommittee: startCommittee,
|
|
||||||
period,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
syncCommittee: startCommittee,
|
|
||||||
period: currentPeriod,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async syncUpdateVerifyGetCommittee(
|
protected abstract getLatestExecution(): Promise<ExecutionInfo | null>;
|
||||||
prevCommittee: Uint8Array[],
|
|
||||||
period: number,
|
|
||||||
update: LightClientUpdate,
|
|
||||||
): Promise<false | Uint8Array[]> {
|
|
||||||
const updatePeriod = computeSyncPeriodAtSlot(
|
|
||||||
update.attestedHeader.beacon.slot,
|
|
||||||
);
|
|
||||||
if (period !== updatePeriod) {
|
|
||||||
console.error(
|
|
||||||
`Expected update with period ${period}, but received ${updatePeriod}`,
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const prevCommitteeFast = deserializeSyncCommittee({
|
|
||||||
pubkeys: prevCommittee,
|
|
||||||
aggregatePubkey: bls.PublicKey.aggregate(
|
|
||||||
deserializePubkeys(prevCommittee),
|
|
||||||
).toBytes(),
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
// check if the update has valid signatures
|
|
||||||
await assertValidLightClientUpdate(
|
|
||||||
this.config.chainConfig,
|
|
||||||
prevCommitteeFast,
|
|
||||||
update,
|
|
||||||
);
|
|
||||||
return update.nextSyncCommittee.pubkeys;
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async syncFromGenesis(): Promise<Uint8Array[]> {
|
|
||||||
return this.syncFromLastUpdate(this.genesisPeriod);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async syncFromLastUpdate(
|
|
||||||
startPeriod = this.latestPeriod,
|
|
||||||
): Promise<Uint8Array[]> {
|
|
||||||
const currentPeriod = this.getCurrentPeriod();
|
|
||||||
let startCommittee = this.genesisCommittee;
|
|
||||||
console.debug(
|
|
||||||
`Sync started from period(${startPeriod}) to period(${currentPeriod})`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const { syncCommittee, period } = await this.syncProver(
|
|
||||||
startPeriod,
|
|
||||||
currentPeriod,
|
|
||||||
startCommittee,
|
|
||||||
);
|
|
||||||
if (period === currentPeriod) {
|
|
||||||
console.debug(
|
|
||||||
`Sync completed from period(${startPeriod}) to period(${currentPeriod})`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return syncCommittee;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error("no honest prover found");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,17 @@
|
||||||
import BaseClient, { BaseClientOptions } from "#baseClient.js";
|
import BaseClient, { BaseClientOptions } from "#baseClient.js";
|
||||||
import { IProver, IVerifyingProviderConstructor } from "#interfaces.js";
|
import { ExecutionInfo, IVerifyingProviderConstructor } from "#interfaces.js";
|
||||||
|
import { DEFAULT_BATCH_SIZE } from "#constants.js";
|
||||||
|
import { IClientProver } from "#client/prover.js";
|
||||||
|
import {
|
||||||
|
getCommitteeHash,
|
||||||
|
optimisticUpdateFromJSON,
|
||||||
|
optimisticUpdateVerify,
|
||||||
|
} from "#util.js";
|
||||||
|
import { equalBytes } from "@noble/curves/abstract/utils";
|
||||||
import { IClientVerifyingProvider } from "#client/verifyingProvider.js";
|
import { IClientVerifyingProvider } from "#client/verifyingProvider.js";
|
||||||
|
|
||||||
interface Config extends BaseClientOptions {
|
interface Config extends BaseClientOptions {
|
||||||
prover: IProver;
|
prover: IClientProver;
|
||||||
provider: IVerifyingProviderConstructor<IClientVerifyingProvider>;
|
provider: IVerifyingProviderConstructor<IClientVerifyingProvider>;
|
||||||
rpcHandler: Function;
|
rpcHandler: Function;
|
||||||
}
|
}
|
||||||
|
@ -40,6 +48,74 @@ export default class Client extends BaseClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected async getLatestExecution(): Promise<ExecutionInfo | null> {
|
||||||
|
const updateJSON = await this.options.prover.callback(
|
||||||
|
"consensus_optimistic_update",
|
||||||
|
);
|
||||||
|
const update = optimisticUpdateFromJSON(updateJSON);
|
||||||
|
const verify = await optimisticUpdateVerify(
|
||||||
|
this.latestCommittee as Uint8Array[],
|
||||||
|
update,
|
||||||
|
);
|
||||||
|
if (!verify.correct) {
|
||||||
|
console.error(`Invalid Optimistic Update: ${verify.reason}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
console.log(
|
||||||
|
`Optimistic update verified for slot ${updateJSON.attested_header.beacon.slot}`,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
blockHash: updateJSON.attested_header.execution.block_hash,
|
||||||
|
blockNumber: updateJSON.attested_header.execution.block_number,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected syncFromGenesis(): Promise<Uint8Array[]> {
|
||||||
|
return this.syncFromLastUpdate(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async syncFromLastUpdate(
|
||||||
|
startPeriod = this.latestPeriod,
|
||||||
|
): Promise<Uint8Array[]> {
|
||||||
|
const currentPeriod = this.getCurrentPeriod();
|
||||||
|
|
||||||
|
let lastCommitteeHash: Uint8Array = getCommitteeHash(this.genesisCommittee);
|
||||||
|
|
||||||
|
for (let period = startPeriod + 1; period <= currentPeriod; period++) {
|
||||||
|
try {
|
||||||
|
lastCommitteeHash = await this.options.prover.getCommitteeHash(
|
||||||
|
period,
|
||||||
|
currentPeriod,
|
||||||
|
DEFAULT_BATCH_SIZE,
|
||||||
|
);
|
||||||
|
} catch (e: any) {
|
||||||
|
throw new Error(
|
||||||
|
`failed to fetch committee hash for prover at period(${period}): ${e.meessage}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.getCommittee(currentPeriod, lastCommitteeHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getCommittee(
|
||||||
|
period: number,
|
||||||
|
expectedCommitteeHash: Uint8Array | null,
|
||||||
|
): Promise<Uint8Array[]> {
|
||||||
|
if (period === this.genesisPeriod) {
|
||||||
|
return this.genesisCommittee;
|
||||||
|
}
|
||||||
|
if (!expectedCommitteeHash) {
|
||||||
|
throw new Error("expectedCommitteeHash required");
|
||||||
|
}
|
||||||
|
const committee = await this.options.prover.getCommittee(period);
|
||||||
|
const committeeHash = getCommitteeHash(committee);
|
||||||
|
if (!equalBytes(committeeHash, expectedCommitteeHash as Uint8Array)) {
|
||||||
|
throw new Error("prover responded with an incorrect committee");
|
||||||
|
}
|
||||||
|
|
||||||
|
return committee;
|
||||||
|
}
|
||||||
|
|
||||||
public async rpcCall(method: string, params: any) {
|
public async rpcCall(method: string, params: any) {
|
||||||
return this.provider?.rpcMethod(method, params);
|
return this.provider?.rpcMethod(method, params);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import Client from "./client.js";
|
import Client from "./client.js";
|
||||||
import Prover, { ProverRequestCallback } from "../prover.js";
|
import Prover, { IClientProver, ProverRequestCallback } from "./prover.js";
|
||||||
import VerifyingProvider from "./verifyingProvider.js";
|
import VerifyingProvider from "./verifyingProvider.js";
|
||||||
import Store from "#store.js";
|
|
||||||
|
|
||||||
function createDefaultClient(
|
function createDefaultClient(
|
||||||
proverHandler: ProverRequestCallback,
|
proverHandler: ProverRequestCallback,
|
||||||
|
@ -9,7 +8,6 @@ function createDefaultClient(
|
||||||
): Client {
|
): Client {
|
||||||
return new Client({
|
return new Client({
|
||||||
prover: new Prover(proverHandler),
|
prover: new Prover(proverHandler),
|
||||||
store: new Store(60 * 60),
|
|
||||||
provider: VerifyingProvider,
|
provider: VerifyingProvider,
|
||||||
rpcHandler,
|
rpcHandler,
|
||||||
});
|
});
|
||||||
|
@ -17,5 +15,5 @@ function createDefaultClient(
|
||||||
|
|
||||||
export { RPCRequest, RPCRequestRaw, RPCResponse } from "./rpc.js";
|
export { RPCRequest, RPCRequestRaw, RPCResponse } from "./rpc.js";
|
||||||
export { Client, Prover, VerifyingProvider, createDefaultClient };
|
export { Client, Prover, VerifyingProvider, createDefaultClient };
|
||||||
export { ProverRequestCallback };
|
export { IClientProver, ProverRequestCallback };
|
||||||
export * from "#interfaces.js";
|
export * from "#interfaces.js";
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
import {
|
||||||
|
ConsensusCommitteeHashesRequest,
|
||||||
|
ConsensusCommitteePeriodRequest,
|
||||||
|
IProver,
|
||||||
|
} from "#interfaces.js";
|
||||||
|
import { CommitteeSSZ, HashesSSZ } from "#ssz.js";
|
||||||
|
import { LightClientUpdate } from "#types.js";
|
||||||
|
import * as capella from "@lodestar/types/capella";
|
||||||
|
|
||||||
|
export type ProverRequestCallback = (
|
||||||
|
action: string,
|
||||||
|
args?: ConsensusCommitteeHashesRequest | ConsensusCommitteePeriodRequest,
|
||||||
|
) => Promise<any>;
|
||||||
|
|
||||||
|
export interface IClientProver extends IProver {
|
||||||
|
get callback(): ProverRequestCallback;
|
||||||
|
getCommittee(period: number | "latest"): Promise<Uint8Array[]>;
|
||||||
|
getSyncUpdate(period: number): Promise<LightClientUpdate>;
|
||||||
|
getCommitteeHash(
|
||||||
|
period: number,
|
||||||
|
currentPeriod: number,
|
||||||
|
cacheCount: number,
|
||||||
|
): Promise<Uint8Array>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class Prover implements IClientProver {
|
||||||
|
cachedHashes: Map<number, Uint8Array> = new Map();
|
||||||
|
|
||||||
|
constructor(callback: ProverRequestCallback) {
|
||||||
|
this._callback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _callback: ProverRequestCallback;
|
||||||
|
|
||||||
|
get callback(): ProverRequestCallback {
|
||||||
|
return this._callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCommittee(period: number | "latest"): Promise<Uint8Array[]> {
|
||||||
|
const res = await this.callback("consensus_committee_period", { period });
|
||||||
|
return CommitteeSSZ.deserialize(Uint8Array.from(Object.values(res)));
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSyncUpdate(period: number): Promise<LightClientUpdate> {
|
||||||
|
const res = await this.callback("consensus_committee_period", { period });
|
||||||
|
return capella.ssz.LightClientUpdate.deserialize(
|
||||||
|
Uint8Array.from(Object.values(res)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async _getHashes(startPeriod: number, count: number): Promise<Uint8Array[]> {
|
||||||
|
const res = await this.callback("consensus_committee_hashes", {
|
||||||
|
start: startPeriod,
|
||||||
|
count,
|
||||||
|
});
|
||||||
|
return HashesSSZ.deserialize(Uint8Array.from(Object.values(res)));
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCommitteeHash(
|
||||||
|
period: number,
|
||||||
|
currentPeriod: number,
|
||||||
|
cacheCount: number,
|
||||||
|
): Promise<Uint8Array> {
|
||||||
|
const _count = Math.min(currentPeriod - period + 1, cacheCount);
|
||||||
|
if (!this.cachedHashes.has(period)) {
|
||||||
|
const vals = await this._getHashes(period, _count);
|
||||||
|
for (let i = 0; i < _count; i++) {
|
||||||
|
this.cachedHashes.set(period + i, vals[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.cachedHashes.get(period)!;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,21 +1,24 @@
|
||||||
import { BeaconConfig } from "@lodestar/config";
|
import { BeaconConfig } from "@lodestar/config";
|
||||||
import { GenesisData, LightClientUpdate } from "#types.js";
|
import { GenesisData, LightClientUpdate } from "#types.js";
|
||||||
import { ProverRequestCallback } from "#client/index.js";
|
|
||||||
import BaseClient from "#baseClient.js";
|
import Provider from "#client/verifyingProvider.js";
|
||||||
|
|
||||||
export interface IProver {
|
export interface IProver {
|
||||||
get callback(): ProverRequestCallback;
|
|
||||||
set client(value: BaseClient);
|
|
||||||
getSyncUpdate(
|
getSyncUpdate(
|
||||||
startPeriod: number,
|
|
||||||
period: number,
|
period: number,
|
||||||
): Promise<LightClientUpdate[]>;
|
currentPeriod: number,
|
||||||
|
cacheCount: number,
|
||||||
|
): Promise<LightClientUpdate>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IStore {
|
export interface IStore {
|
||||||
addUpdate(period: number, update: LightClientUpdate): void;
|
addUpdate(period: number, update: LightClientUpdate): Promise<void>;
|
||||||
|
|
||||||
getUpdate(period: number): Uint8Array;
|
getUpdate(period: number): Uint8Array;
|
||||||
hasUpdate(period: number): boolean;
|
|
||||||
|
getCommittee(period: number): Uint8Array;
|
||||||
|
|
||||||
|
getCommitteeHashes(period: number, count: number): Uint8Array;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IVerifyingProvider {
|
export interface IVerifyingProvider {
|
||||||
|
@ -38,7 +41,15 @@ export interface ExecutionInfo {
|
||||||
blockNumber: number;
|
blockNumber: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConsensusCommitteeUpdateRequest {
|
export interface ConsensusCommitteeHashesRequest {
|
||||||
start: number;
|
start: number;
|
||||||
count: number;
|
count: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ConsensusCommitteePeriodRequest {
|
||||||
|
period: number | "latest";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConsensusBlockRequest {
|
||||||
|
block: number;
|
||||||
|
}
|
||||||
|
|
|
@ -42,9 +42,144 @@ export default class Client extends BaseClient {
|
||||||
this.http.defaults.baseURL = this.beaconUrl;
|
this.http.defaults.baseURL = this.beaconUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _latestOptimisticUpdate: any;
|
||||||
|
|
||||||
|
get latestOptimisticUpdate(): any {
|
||||||
|
return this._latestOptimisticUpdate;
|
||||||
|
}
|
||||||
|
|
||||||
async sync(): Promise<void> {
|
async sync(): Promise<void> {
|
||||||
await super.sync();
|
await super.sync();
|
||||||
|
|
||||||
this.subscribe();
|
this.subscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async syncProver(
|
||||||
|
startPeriod: number,
|
||||||
|
currentPeriod: number,
|
||||||
|
startCommittee: Uint8Array[],
|
||||||
|
): Promise<{ syncCommittee: Uint8Array[]; period: number }> {
|
||||||
|
for (let period = startPeriod; period < currentPeriod; period += 1) {
|
||||||
|
try {
|
||||||
|
const update = await this.options.prover.getSyncUpdate(
|
||||||
|
period,
|
||||||
|
currentPeriod,
|
||||||
|
DEFAULT_BATCH_SIZE,
|
||||||
|
);
|
||||||
|
const validOrCommittee = await this.syncUpdateVerifyGetCommittee(
|
||||||
|
startCommittee,
|
||||||
|
period,
|
||||||
|
update,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!(validOrCommittee as boolean)) {
|
||||||
|
console.log(`Found invalid update at period(${period})`);
|
||||||
|
return {
|
||||||
|
syncCommittee: startCommittee,
|
||||||
|
period,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.options.store?.addUpdate(period, update);
|
||||||
|
startCommittee = validOrCommittee as Uint8Array[];
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`failed to fetch sync update for period(${period})`);
|
||||||
|
return {
|
||||||
|
syncCommittee: startCommittee,
|
||||||
|
period,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
syncCommittee: startCommittee,
|
||||||
|
period: currentPeriod,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async getLatestExecution(): Promise<ExecutionInfo | null> {
|
||||||
|
const updateJSON = await getConsensusOptimisticUpdate();
|
||||||
|
const update = optimisticUpdateFromJSON(updateJSON);
|
||||||
|
const verify = await optimisticUpdateVerify(
|
||||||
|
this.latestCommittee as Uint8Array[],
|
||||||
|
update,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!verify.correct) {
|
||||||
|
// @ts-ignore
|
||||||
|
console.error(`Invalid Optimistic Update: ${verify?.reason}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._latestOptimisticUpdate = updateJSON;
|
||||||
|
|
||||||
|
return {
|
||||||
|
blockHash: toHexString(update.attestedHeader.execution.blockHash),
|
||||||
|
blockNumber: update.attestedHeader.execution.blockNumber,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async syncFromGenesis(): Promise<Uint8Array[]> {
|
||||||
|
return this.syncFromLastUpdate(this.genesisPeriod);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async syncFromLastUpdate(
|
||||||
|
startPeriod = this.latestPeriod,
|
||||||
|
): Promise<Uint8Array[]> {
|
||||||
|
const currentPeriod = this.getCurrentPeriod();
|
||||||
|
let startCommittee = this.genesisCommittee;
|
||||||
|
console.debug(
|
||||||
|
`Sync started from period(${startPeriod}) to period(${currentPeriod})`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { syncCommittee, period } = await this.syncProver(
|
||||||
|
startPeriod,
|
||||||
|
currentPeriod,
|
||||||
|
startCommittee,
|
||||||
|
);
|
||||||
|
if (period === currentPeriod) {
|
||||||
|
console.debug(
|
||||||
|
`Sync completed from period(${startPeriod}) to period(${currentPeriod})`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return syncCommittee;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("no honest prover found");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async syncUpdateVerifyGetCommittee(
|
||||||
|
prevCommittee: Uint8Array[],
|
||||||
|
period: number,
|
||||||
|
update: LightClientUpdate,
|
||||||
|
): Promise<false | Uint8Array[]> {
|
||||||
|
const updatePeriod = computeSyncPeriodAtSlot(
|
||||||
|
update.attestedHeader.beacon.slot,
|
||||||
|
);
|
||||||
|
if (period !== updatePeriod) {
|
||||||
|
console.error(
|
||||||
|
`Expected update with period ${period}, but received ${updatePeriod}`,
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const prevCommitteeFast = deserializeSyncCommittee({
|
||||||
|
pubkeys: prevCommittee,
|
||||||
|
aggregatePubkey: bls.PublicKey.aggregate(
|
||||||
|
deserializePubkeys(prevCommittee),
|
||||||
|
).toBytes(),
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// check if the update has valid signatures
|
||||||
|
await assertValidLightClientUpdate(
|
||||||
|
this.config.chainConfig,
|
||||||
|
prevCommitteeFast,
|
||||||
|
update,
|
||||||
|
);
|
||||||
|
return update.nextSyncCommittee.pubkeys;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +1,11 @@
|
||||||
import Client from "./client.js";
|
import Client from "./client.js";
|
||||||
import Store from "../store.js";
|
import Prover from "./prover.js";
|
||||||
import Prover from "#prover.js";
|
import Store from "./store.js";
|
||||||
import * as capella from "@lodestar/types/capella";
|
|
||||||
import { consensusClient } from "#util.js";
|
|
||||||
|
|
||||||
function createDefaultClient(beaconUrl: string): Client {
|
function createDefaultClient(beaconUrl: string): Client {
|
||||||
return new Client({
|
return new Client({
|
||||||
store: new Store(),
|
store: new Store(),
|
||||||
prover: new Prover(async (args) => {
|
prover: new Prover(),
|
||||||
const res = await consensusClient.get(
|
|
||||||
`/eth/v1/beacon/light_client/updates?start_period=${args.start}&count=${args.count}`,
|
|
||||||
);
|
|
||||||
return res.data.map((u: any) =>
|
|
||||||
capella.ssz.LightClientUpdate.fromJson(u.data),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
beaconUrl,
|
beaconUrl,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { IProver } from "#interfaces.js";
|
||||||
|
import { LightClientUpdate } from "#types.js";
|
||||||
|
import { consensusClient } from "#util.js";
|
||||||
|
import { AxiosInstance } from "axios";
|
||||||
|
import * as capella from "@lodestar/types/capella";
|
||||||
|
|
||||||
|
export default class Prover implements IProver {
|
||||||
|
cachedSyncUpdate: Map<number, LightClientUpdate> = new Map();
|
||||||
|
private http: AxiosInstance = consensusClient;
|
||||||
|
|
||||||
|
async _getSyncUpdates(
|
||||||
|
startPeriod: number,
|
||||||
|
maxCount: number,
|
||||||
|
): Promise<LightClientUpdate[]> {
|
||||||
|
const res = await this.http(
|
||||||
|
`/eth/v1/beacon/light_client/updates?start_period=${startPeriod}&count=${maxCount}`,
|
||||||
|
);
|
||||||
|
return res.data.map((u: any) =>
|
||||||
|
capella.ssz.LightClientUpdate.fromJson(u.data),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSyncUpdate(
|
||||||
|
period: number,
|
||||||
|
currentPeriod: number,
|
||||||
|
cacheCount: number,
|
||||||
|
): Promise<LightClientUpdate> {
|
||||||
|
const _cacheCount = Math.min(currentPeriod - period + 1, cacheCount);
|
||||||
|
if (!this.cachedSyncUpdate.has(period)) {
|
||||||
|
const vals = await this._getSyncUpdates(period, _cacheCount);
|
||||||
|
for (let i = 0; i < _cacheCount; i++) {
|
||||||
|
this.cachedSyncUpdate.set(period + i, vals[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.cachedSyncUpdate.get(period)!;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { digest } from "@chainsafe/as-sha256";
|
||||||
|
import { CommitteeSSZ, HashesSSZ } from "#ssz.js";
|
||||||
|
import { IStore } from "#interfaces.js";
|
||||||
|
import { concatBytes } from "@noble/hashes/utils";
|
||||||
|
import { LightClientUpdate } from "#types.js";
|
||||||
|
import * as capella from "@lodestar/types/capella";
|
||||||
|
|
||||||
|
export default class Store implements IStore {
|
||||||
|
store: {
|
||||||
|
[period: number]: {
|
||||||
|
update: Uint8Array;
|
||||||
|
nextCommittee: Uint8Array;
|
||||||
|
nextCommitteeHash: Uint8Array;
|
||||||
|
};
|
||||||
|
} = {};
|
||||||
|
|
||||||
|
async addUpdate(period: number, update: LightClientUpdate) {
|
||||||
|
try {
|
||||||
|
this.store[period] = {
|
||||||
|
update: capella.ssz.LightClientUpdate.serialize(update),
|
||||||
|
nextCommittee: CommitteeSSZ.serialize(update.nextSyncCommittee.pubkeys),
|
||||||
|
nextCommitteeHash: digest(
|
||||||
|
concatBytes(...update.nextSyncCommittee.pubkeys),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getUpdate(period: number): Uint8Array {
|
||||||
|
if (period in this.store) return this.store[period].update;
|
||||||
|
throw new Error(`update unavailable for period ${period}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
getCommittee(period: number): Uint8Array {
|
||||||
|
if (period < 1)
|
||||||
|
throw new Error("committee not unavailable for period less than 1");
|
||||||
|
const predPeriod = period - 1;
|
||||||
|
if (predPeriod in this.store) return this.store[predPeriod].nextCommittee;
|
||||||
|
throw new Error(`committee unavailable for period ${predPeriod}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
getCommitteeHashes(period: number, count: number): Uint8Array {
|
||||||
|
if (period < 1)
|
||||||
|
throw new Error("committee not unavailable for period less than 1");
|
||||||
|
const predPeriod = period - 1;
|
||||||
|
|
||||||
|
const hashes = new Array(count).fill(0).map((_, i) => {
|
||||||
|
const p = predPeriod + i;
|
||||||
|
if (p in this.store) return this.store[p].nextCommitteeHash;
|
||||||
|
throw new Error(`committee unavailable for period ${p}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
return HashesSSZ.serialize(hashes);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,72 +0,0 @@
|
||||||
import { ConsensusCommitteeUpdateRequest, IProver } from "#interfaces.js";
|
|
||||||
import { LightClientUpdate } from "#types.js";
|
|
||||||
import * as capella from "@lodestar/types/capella";
|
|
||||||
import BaseClient from "#baseClient.js";
|
|
||||||
|
|
||||||
export type ProverRequestCallback = (
|
|
||||||
args: ConsensusCommitteeUpdateRequest,
|
|
||||||
) => Promise<any>;
|
|
||||||
|
|
||||||
export default class Prover implements IProver {
|
|
||||||
constructor(callback: ProverRequestCallback) {
|
|
||||||
this._callback = callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _client?: BaseClient;
|
|
||||||
|
|
||||||
set client(value: BaseClient) {
|
|
||||||
this._client = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _callback: ProverRequestCallback;
|
|
||||||
|
|
||||||
get callback(): ProverRequestCallback {
|
|
||||||
return this._callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getSyncUpdate(
|
|
||||||
startPeriod: number,
|
|
||||||
count: number,
|
|
||||||
): Promise<LightClientUpdate[]> {
|
|
||||||
let end = startPeriod + count;
|
|
||||||
let hasStart = this.client.store.hasUpdate(startPeriod);
|
|
||||||
let hasEnd = this.client.store.hasUpdate(startPeriod + count);
|
|
||||||
|
|
||||||
let trueStart = startPeriod;
|
|
||||||
let trueCount = count;
|
|
||||||
|
|
||||||
if (hasStart && !hasEnd) {
|
|
||||||
for (let i = startPeriod; i <= end; i++) {
|
|
||||||
if (!this.client.store.hasUpdate(i)) {
|
|
||||||
trueStart = i;
|
|
||||||
trueCount = end - i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await this.callback({
|
|
||||||
start: trueStart,
|
|
||||||
count: trueCount,
|
|
||||||
});
|
|
||||||
|
|
||||||
const updates: LightClientUpdate[] = [];
|
|
||||||
|
|
||||||
if (trueStart != startPeriod) {
|
|
||||||
for (let i = 0; i < trueStart - startPeriod; i++) {
|
|
||||||
updates.push(
|
|
||||||
capella.ssz.LightClientUpdate.deserialize(
|
|
||||||
this.client.store.getUpdate(startPeriod + i),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < trueCount; i++) {
|
|
||||||
updates.push(
|
|
||||||
capella.ssz.LightClientUpdate.deserialize(res[startPeriod + i]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return updates;
|
|
||||||
}
|
|
||||||
}
|
|
45
src/store.ts
45
src/store.ts
|
@ -1,45 +0,0 @@
|
||||||
import { digest } from "@chainsafe/as-sha256";
|
|
||||||
import { CommitteeSSZ, HashesSSZ } from "#ssz.js";
|
|
||||||
import { IStore } from "#interfaces.js";
|
|
||||||
import { concatBytes } from "@noble/hashes/utils";
|
|
||||||
import { LightClientUpdate } from "#types.js";
|
|
||||||
import * as capella from "@lodestar/types/capella";
|
|
||||||
import NodeCache from "node-cache";
|
|
||||||
|
|
||||||
export interface StoreItem {
|
|
||||||
update: Uint8Array;
|
|
||||||
nextCommittee: Uint8Array;
|
|
||||||
nextCommitteeHash: Uint8Array;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class Store implements IStore {
|
|
||||||
private store = new NodeCache();
|
|
||||||
|
|
||||||
constructor(expire: number = 0) {
|
|
||||||
this.store.options.stdTTL = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
addUpdate(period: number, update: LightClientUpdate) {
|
|
||||||
try {
|
|
||||||
this.store.set(period, {
|
|
||||||
update: capella.ssz.LightClientUpdate.serialize(update),
|
|
||||||
nextCommittee: CommitteeSSZ.serialize(update.nextSyncCommittee.pubkeys),
|
|
||||||
nextCommitteeHash: digest(
|
|
||||||
concatBytes(...update.nextSyncCommittee.pubkeys),
|
|
||||||
),
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getUpdate(period: number): Uint8Array {
|
|
||||||
if (this.store.has(period)) {
|
|
||||||
return this.store.get<StoreItem>(period)?.update as Uint8Array;
|
|
||||||
}
|
|
||||||
throw new Error(`update unavailable for period ${period}`);
|
|
||||||
}
|
|
||||||
hasUpdate(period: number): boolean {
|
|
||||||
return this.store.has(period);
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue