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

View File

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

View File

@ -1,9 +1,10 @@
import * as altair from "@lodestar/types/altair"; import * as altair from "@lodestar/types/altair";
import { IProver } from "./interfaces.js"; import { IProver } from "./interfaces.js";
import { LightClientUpdate } from "./types.js"; import { LightClientUpdate } from "./types.js";
import { CommitteeSSZ, HashesSSZ, LightClientUpdateSSZ } from "./ssz.js";
export default class Prover implements IProver { export default class Prover implements IProver {
cachedSyncUpdate: Map<number, LightClientUpdate> = new Map(); cachedHashes: Map<number, Uint8Array> = new Map();
constructor(callback: Function) { constructor(callback: Function) {
this._callback = callback; this._callback = callback;
@ -15,28 +16,38 @@ export default class Prover implements IProver {
return this._callback; return this._callback;
} }
async _getSyncUpdates( async getCommittee(period: number | "latest"): Promise<Uint8Array[]> {
startPeriod: number, const res = await this.callback("consensus_committee_period", { period });
maxCount: number return CommitteeSSZ.deserialize(Uint8Array.from(Object.values(res)));
): 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 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, period: number,
currentPeriod: number, currentPeriod: number,
cacheCount: number cacheCount: number
): Promise<LightClientUpdate> { ): Promise<Uint8Array> {
const _cacheCount = Math.min(currentPeriod - period + 1, cacheCount); const _count = Math.min(currentPeriod - period + 1, cacheCount);
if (!this.cachedSyncUpdate.has(period)) { if (!this.cachedHashes.has(period)) {
const vals = await this._getSyncUpdates(period, _cacheCount); const vals = await this._getHashes(period, _count);
for (let i = 0; i < _cacheCount; i++) { for (let i = 0; i < _count; i++) {
this.cachedSyncUpdate.set(period + i, vals[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 { ActiveQuery, addHandler, handleMessage } from "libkmodule";
import { createClient, RpcNetwork } from "@lumeweb/kernel-rpc-client"; import { createClient, RpcNetwork } from "@lumeweb/kernel-rpc-client";
import { ConsensusRequest, ExecutionRequest } from "./types.js";
import Client from "./client/client.js"; import Client from "./client/client.js";
import { Prover } from "./client/index.js"; import { Prover } from "./client/index.js";
@ -65,27 +64,22 @@ async function handleRpcMethod(aq: ActiveQuery) {
); );
} }
async function consensusHandler(endpoint: string) { async function consensusHandler(method: string, data: any) {
let query;
while (true) { while (true) {
query = await rpc.simpleQuery({ let query = await rpc.simpleQuery({
query: { query: {
module: "eth", module: "eth",
method: "consensus_request", method,
data: { data,
method: "GET",
path: endpoint,
} as ConsensusRequest,
}, },
options: { options: {
relayTimeout: 10, relayTimeout: 10,
queryTimeout: 10, queryTimeout: 10,
}, },
}); });
console.log("consensusHandler", endpoint);
const ret = await query.result; const ret = await query.result;
if (ret.data) { if (ret.data) {
return ret.data; return ret.data;
} }
@ -93,23 +87,24 @@ async function consensusHandler(endpoint: string) {
} }
async function executionHandler(data: Map<string, string | any>) { async function executionHandler(data: Map<string, string | any>) {
let query = await rpc.simpleQuery({ while (true) {
query: { let query = await rpc.simpleQuery({
module: "eth", query: {
method: "execution_request", module: "eth",
data, method: "execution_request",
}, data,
}); },
});
console.log("executionHandler", data); let ret = await query.result;
let ret = await query.result; if (ret.data) {
return ret.data;
return ret.data; }
}
} }
async function setup() { async function setup() {
console.time("setup");
rpc = createClient(); rpc = createClient();
// @ts-ignore // @ts-ignore
await ( await (

View File

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