*Switch to optimistic approach that uses the relay as a light proxy
This commit is contained in:
parent
c7c44a0cd2
commit
4ac1621e3f
|
@ -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",
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)!;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
41
src/index.ts
41
src/index.ts
|
@ -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 (
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
export interface ConsensusRequest extends RequestInit {
|
|
||||||
path: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ExecutionRequest {
|
|
||||||
method: string;
|
|
||||||
params: string;
|
|
||||||
}
|
|
Loading…
Reference in New Issue