From 4ac1621e3f9350f231232b9164b24511242cbe51 Mon Sep 17 00:00:00 2001 From: Derrick Hammer Date: Wed, 29 Mar 2023 00:03:48 -0400 Subject: [PATCH] *Switch to optimistic approach that uses the relay as a light proxy --- package.json | 2 +- src/client/client.ts | 142 +++++++++++++-------------------------- src/client/interfaces.ts | 12 ++-- src/client/prover.ts | 45 ++++++++----- src/index.ts | 41 +++++------ src/types.ts | 8 --- 6 files changed, 103 insertions(+), 147 deletions(-) delete mode 100644 src/types.ts diff --git a/package.json b/package.json index bbb0b51..2bc64a6 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/client/client.ts b/src/client/client.ts index 56ae67d..6c27222 100644 --- a/src/client/client.ts +++ b/src/client/client.ts @@ -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) { + 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 { - // get the tree size by currentPeriod - genesisPeriod + private async syncFromGenesis(): Promise { 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; + for (let period = startPeriod + 1; period <= currentPeriod; period++) { + try { + 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}` + ); + } } - throw new Error("no honest prover found"); + return this.getCommittee(currentPeriod, lastCommitteeHash); } - protected async syncUpdateVerifyGetCommittee( - prevCommittee: Uint8Array[], + async getCommittee( period: number, - update: LightClientUpdate - ): Promise { - 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); - 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; - } + expectedCommitteeHash: Uint8Array | null + ): Promise { + 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; } - protected async getLatestExecution(): Promise { + private async getLatestExecution(): Promise { 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 { - 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); + } } diff --git a/src/client/interfaces.ts b/src/client/interfaces.ts index 415b394..6c0917a 100644 --- a/src/client/interfaces.ts +++ b/src/client/interfaces.ts @@ -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; + + getCommitteeHash( period: number, currentPeriod: number, - cacheCount: number - ): AsyncOrSync; + count: number + ): Promise; + + getSyncUpdate(period: number): Promise; } diff --git a/src/client/prover.ts b/src/client/prover.ts index 32a1cce..3d89d3f 100644 --- a/src/client/prover.ts +++ b/src/client/prover.ts @@ -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 = new Map(); + cachedHashes: Map = 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 { - 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 { + const res = await this.callback("consensus_committee_period", { period }); + return CommitteeSSZ.deserialize(Uint8Array.from(Object.values(res))); } - async getSyncUpdate( + async getSyncUpdate(period: number): Promise { + const res = await this.callback("consensus_committee_period", { period }); + return LightClientUpdateSSZ.deserialize( + Uint8Array.from(Object.values(res)) + ); + } + + async _getHashes(startPeriod: number, count: number): Promise { + 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 { - 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 { + 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)!; } } diff --git a/src/index.ts b/src/index.ts index 9e9bb0c..49b8ffc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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,23 +87,24 @@ async function consensusHandler(endpoint: string) { } async function executionHandler(data: Map) { - let query = await rpc.simpleQuery({ - query: { - module: "eth", - method: "execution_request", - data, - }, - }); + while (true) { + let query = await rpc.simpleQuery({ + query: { + module: "eth", + method: "execution_request", + data, + }, + }); - console.log("executionHandler", data); + let ret = await query.result; - let ret = await query.result; - - return ret.data; + if (ret.data) { + return ret.data; + } + } } async function setup() { - console.time("setup"); rpc = createClient(); // @ts-ignore await ( diff --git a/src/types.ts b/src/types.ts deleted file mode 100644 index 9923293..0000000 --- a/src/types.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface ConsensusRequest extends RequestInit { - path: string; -} - -export interface ExecutionRequest { - method: string; - params: string; -}