*Switch to optimistic approach that uses the relay as a light proxy

This commit is contained in:
Derrick Hammer 2023-03-29 00:03:48 -04:00
parent c7c44a0cd2
commit 4ac1621e3f
Signed by: pcfreak30
GPG Key ID: C997C339BE476FF2
6 changed files with 103 additions and 147 deletions

View File

@ -9,6 +9,7 @@
"build": "npm run compile && node ./dist-build/build.mjs dev"
},
"dependencies": {
"@chainsafe/as-sha256": "^0.3.1",
"@chainsafe/bls": "^7.1.1",
"@chainsafe/blst": "^0.2.8",
"@chainsafe/ssz": "^0.10.2",
@ -32,7 +33,6 @@
"path-browserify": "^1.0.1",
"rlp": "^3.0.0",
"stream-browserify": "^3.0.0",
"ts-essentials": "^9.3.1",
"web3-core": "^1.9.0",
"web3-core-method": "^1.9.0",
"web3-eth": "^1.9.0",

View File

@ -6,7 +6,11 @@ import {
OptimisticUpdate,
VerifyWithReason,
} from "./types.js";
import { getDefaultClientConfig } from "./utils.js";
import {
concatUint8Array,
getDefaultClientConfig,
isUint8ArrayEq,
} from "./utils.js";
import { IProver } from "./interfaces.js";
import {
BEACON_SYNC_SUPER_MAJORITY,
@ -25,12 +29,12 @@ import { SyncCommitteeFast } from "@lodestar/light-client";
import bls from "@chainsafe/bls/switchable";
import { PublicKey } from "@chainsafe/bls/types.js";
import { fromHexString, toHexString } from "@chainsafe/ssz";
import { AsyncOrSync } from "ts-essentials";
import * as altair from "@lodestar/types/altair";
import * as phase0 from "@lodestar/types/phase0";
import * as bellatrix from "@lodestar/types/bellatrix";
import { init } from "@chainsafe/bls/switchable";
import { VerifyingProvider } from "./rpc/provider.js";
import { digest } from "@chainsafe/as-sha256";
export default class Client {
latestCommittee?: Uint8Array[];
@ -85,55 +89,13 @@ export default class Client {
return this._provider;
}
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.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,
};
}
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,
};
}
// returns the prover info containing the current sync
public getCurrentPeriod(): number {
return computeSyncPeriodAtSlot(
getCurrentSlot(this.config.chainConfig, this.genesisTime)
);
}
public async subscribe(callback: (ei: ExecutionInfo) => AsyncOrSync<void>) {
public async subscribe(callback: (ei: ExecutionInfo) => void) {
setInterval(async () => {
try {
await this._sync();
@ -148,10 +110,6 @@ export default class Client {
}, POLLING_DELAY);
}
optimisticUpdateFromJSON(update: any): OptimisticUpdate {
return altair.ssz.LightClientOptimisticUpdate.fromJson(update);
}
async optimisticUpdateVerify(
committee: Uint8Array[],
update: OptimisticUpdate
@ -195,7 +153,7 @@ export default class Client {
return this.getNextValidExecutionInfo(retry - 1);
}
protected async _sync() {
private async _sync() {
const currentPeriod = this.getCurrentPeriod();
if (currentPeriod > this.latestPeriod) {
this.latestCommittee = await this.syncFromGenesis();
@ -204,59 +162,47 @@ export default class Client {
}
// committee and prover index of the first honest prover
protected async syncFromGenesis(): Promise<Uint8Array[]> {
// get the tree size by currentPeriod - genesisPeriod
private async syncFromGenesis(): Promise<Uint8Array[]> {
const currentPeriod = this.getCurrentPeriod();
let startPeriod = this.genesisPeriod;
let startCommittee = this.genesisCommittee;
console.log(
`Sync started from period(${startPeriod}) to period(${currentPeriod})`
let lastCommitteeHash: Uint8Array = this.getCommitteeHash(
this.genesisCommittee
);
const { syncCommittee, period } = await this.syncProver(
startPeriod,
currentPeriod,
startCommittee
);
if (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 recieved ${updatePeriod}`
);
return false;
}
const prevCommitteeFast = this.deserializeSyncCommittee(prevCommittee);
for (let period = startPeriod + 1; period <= currentPeriod; period++) {
try {
// check if the update has valid signatures
await assertValidLightClientUpdate(
this.config.chainConfig,
prevCommitteeFast,
update
lastCommitteeHash = await this.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 update.nextSyncCommittee.pubkeys;
} catch (e) {
console.error(e);
return false;
}
}
return this.getCommittee(currentPeriod, lastCommitteeHash);
}
protected async getLatestExecution(): Promise<ExecutionInfo | null> {
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.prover.getCommittee(period);
const committeeHash = this.getCommitteeHash(committee);
if (!isUint8ArrayEq(committeeHash, expectedCommitteeHash as Uint8Array))
throw new Error("prover responded with an incorrect committee");
return committee;
}
private async getLatestExecution(): Promise<ExecutionInfo | null> {
const updateJSON = await this.prover.callback(
"/eth/v1/beacon/light_client/optimistic_update"
"consensus_optimistic_update"
);
const update = this.optimisticUpdateFromJSON(updateJSON);
const verify = await this.optimisticUpdateVerify(
@ -276,11 +222,13 @@ export default class Client {
);
}
protected async getExecutionFromBlockRoot(
private async getExecutionFromBlockRoot(
slot: bigint,
expectedBlockRoot: Bytes32
): Promise<ExecutionInfo> {
const res = await this.prover.callback(`/eth/v2/beacon/blocks/${slot}`);
const res = await this.prover.callback("consensus_block", {
block: slot,
});
const blockJSON = res.message.body;
const block = bellatrix.ssz.BeaconBlockBody.fromJson(blockJSON);
const blockRoot = toHexString(
@ -311,4 +259,10 @@ export default class Client {
private deserializePubkeys(pubkeys: Uint8Array[]): PublicKey[] {
return pubkeys.map((pk) => bls.PublicKey.fromBytes(pk));
}
private getCommitteeHash(committee: Uint8Array[]): Uint8Array {
return digest(concatUint8Array(committee));
}
private optimisticUpdateFromJSON(update: any): OptimisticUpdate {
return altair.ssz.LightClientOptimisticUpdate.fromJson(update);
}
}

View File

@ -1,11 +1,15 @@
import { AsyncOrSync } from "ts-essentials";
import { LightClientUpdate } from "./types.js";
export interface IProver {
get callback(): Function;
getSyncUpdate(
getCommittee(period: number | "latest"): Promise<Uint8Array[]>;
getCommitteeHash(
period: number,
currentPeriod: number,
cacheCount: number
): AsyncOrSync<LightClientUpdate>;
count: number
): Promise<Uint8Array>;
getSyncUpdate(period: number): Promise<LightClientUpdate>;
}

View File

@ -1,9 +1,10 @@
import * as altair from "@lodestar/types/altair";
import { IProver } from "./interfaces.js";
import { LightClientUpdate } from "./types.js";
import { CommitteeSSZ, HashesSSZ, LightClientUpdateSSZ } from "./ssz.js";
export default class Prover implements IProver {
cachedSyncUpdate: Map<number, LightClientUpdate> = new Map();
cachedHashes: Map<number, Uint8Array> = new Map();
constructor(callback: Function) {
this._callback = callback;
@ -15,28 +16,38 @@ export default class Prover implements IProver {
return this._callback;
}
async _getSyncUpdates(
startPeriod: number,
maxCount: number
): Promise<LightClientUpdate[]> {
const res = await this._callback(
`/eth/v1/beacon/light_client/updates?start_period=${startPeriod}&count=${maxCount}`
);
return res.map((u: any) => altair.ssz.LightClientUpdate.fromJson(u.data));
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(
async getSyncUpdate(period: number): Promise<LightClientUpdate> {
const res = await this.callback("consensus_committee_period", { period });
return LightClientUpdateSSZ.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<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]);
): 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.cachedSyncUpdate.get(period)!;
return this.cachedHashes.get(period)!;
}
}

View File

@ -1,6 +1,5 @@
import { ActiveQuery, addHandler, handleMessage } from "libkmodule";
import { createClient, RpcNetwork } from "@lumeweb/kernel-rpc-client";
import { ConsensusRequest, ExecutionRequest } from "./types.js";
import Client from "./client/client.js";
import { Prover } from "./client/index.js";
@ -65,27 +64,22 @@ async function handleRpcMethod(aq: ActiveQuery) {
);
}
async function consensusHandler(endpoint: string) {
let query;
async function consensusHandler(method: string, data: any) {
while (true) {
query = await rpc.simpleQuery({
let query = await rpc.simpleQuery({
query: {
module: "eth",
method: "consensus_request",
data: {
method: "GET",
path: endpoint,
} as ConsensusRequest,
method,
data,
},
options: {
relayTimeout: 10,
queryTimeout: 10,
},
});
console.log("consensusHandler", endpoint);
const ret = await query.result;
if (ret.data) {
return ret.data;
}
@ -93,6 +87,7 @@ async function consensusHandler(endpoint: string) {
}
async function executionHandler(data: Map<string, string | any>) {
while (true) {
let query = await rpc.simpleQuery({
query: {
module: "eth",
@ -101,15 +96,15 @@ async function executionHandler(data: Map<string, string | any>) {
},
});
console.log("executionHandler", data);
let ret = await query.result;
if (ret.data) {
return ret.data;
}
}
}
async function setup() {
console.time("setup");
rpc = createClient();
// @ts-ignore
await (

View File

@ -1,8 +0,0 @@
export interface ConsensusRequest extends RequestInit {
path: string;
}
export interface ExecutionRequest {
method: string;
params: string;
}