2023-07-10 20:44:58 +00:00
|
|
|
import { ClientConfig, ExecutionInfo, IProver, IStore } from "#interfaces.js";
|
|
|
|
import { POLLING_DELAY } from "#constants.js";
|
|
|
|
import {
|
|
|
|
computeSyncPeriodAtSlot,
|
2023-07-12 21:59:56 +00:00
|
|
|
deserializeSyncCommittee,
|
2023-07-13 06:53:00 +00:00
|
|
|
getCurrentSlot,
|
2023-07-10 20:44:58 +00:00
|
|
|
} from "@lodestar/light-client/utils";
|
2023-07-13 05:46:45 +00:00
|
|
|
import bls, { init } from "@chainsafe/bls/switchable";
|
2023-07-10 20:44:58 +00:00
|
|
|
import { Mutex } from "async-mutex";
|
2023-07-12 21:40:59 +00:00
|
|
|
import { fromHexString, toHexString } from "@chainsafe/ssz";
|
2023-07-13 06:25:05 +00:00
|
|
|
import {
|
|
|
|
deserializePubkeys,
|
|
|
|
getDefaultClientConfig,
|
|
|
|
optimisticUpdateVerify,
|
|
|
|
} from "#util.js";
|
2023-07-13 06:53:00 +00:00
|
|
|
import { LightClientUpdate, OptimisticUpdateCallback } from "#types.js";
|
2023-07-13 05:46:45 +00:00
|
|
|
import { assertValidLightClientUpdate } from "@lodestar/light-client/validation";
|
2023-07-13 07:12:49 +00:00
|
|
|
import * as capella from "@lodestar/types/capella";
|
2023-07-10 20:44:58 +00:00
|
|
|
|
|
|
|
export interface BaseClientOptions {
|
|
|
|
prover: IProver;
|
2023-07-12 21:40:59 +00:00
|
|
|
store: IStore;
|
2023-07-13 06:53:00 +00:00
|
|
|
optimisticUpdateCallback: OptimisticUpdateCallback;
|
2023-07-10 20:44:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
export default abstract class BaseClient {
|
|
|
|
protected latestCommittee?: Uint8Array[];
|
|
|
|
protected latestBlockHash?: string;
|
|
|
|
protected config: ClientConfig = getDefaultClientConfig();
|
|
|
|
protected genesisCommittee: Uint8Array[] = this.config.genesis.committee.map(
|
|
|
|
(pk) => fromHexString(pk),
|
|
|
|
);
|
|
|
|
protected genesisPeriod = computeSyncPeriodAtSlot(this.config.genesis.slot);
|
|
|
|
protected booted = false;
|
|
|
|
protected options: BaseClientOptions;
|
2023-07-12 21:40:59 +00:00
|
|
|
private genesisTime = this.config.genesis.time;
|
|
|
|
private syncMutex = new Mutex();
|
2023-07-10 20:44:58 +00:00
|
|
|
|
|
|
|
constructor(options: BaseClientOptions) {
|
|
|
|
this.options = options;
|
|
|
|
}
|
|
|
|
|
2023-07-13 07:12:49 +00:00
|
|
|
private _latestOptimisticUpdate?: Uint8Array;
|
|
|
|
|
|
|
|
get latestOptimisticUpdate(): Uint8Array {
|
|
|
|
return this._latestOptimisticUpdate as Uint8Array;
|
|
|
|
}
|
|
|
|
|
2023-07-10 20:44:58 +00:00
|
|
|
private _latestPeriod: number = -1;
|
|
|
|
|
|
|
|
get latestPeriod(): number {
|
|
|
|
return this._latestPeriod;
|
|
|
|
}
|
|
|
|
|
|
|
|
public get isSynced() {
|
|
|
|
return this._latestPeriod === this.getCurrentPeriod();
|
|
|
|
}
|
|
|
|
|
2023-07-12 21:40:59 +00:00
|
|
|
public get store(): IStore {
|
|
|
|
return this.options.store as IStore;
|
|
|
|
}
|
|
|
|
|
2023-07-10 20:44:58 +00:00
|
|
|
public async sync(): Promise<void> {
|
2023-07-11 06:33:28 +00:00
|
|
|
await init("herumi");
|
2023-07-10 20:44:58 +00:00
|
|
|
|
|
|
|
await this._sync();
|
|
|
|
}
|
|
|
|
|
|
|
|
public getCurrentPeriod(): number {
|
|
|
|
return computeSyncPeriodAtSlot(
|
|
|
|
getCurrentSlot(this.config.chainConfig, this.genesisTime),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
public async getNextValidExecutionInfo(
|
|
|
|
retry: number = 10,
|
|
|
|
): Promise<ExecutionInfo> {
|
|
|
|
if (retry === 0)
|
|
|
|
throw new Error(
|
|
|
|
"no valid execution payload found in the given retry limit",
|
|
|
|
);
|
|
|
|
const ei = await this.getLatestExecution();
|
|
|
|
if (ei) return ei;
|
|
|
|
// delay for the next slot
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, POLLING_DELAY));
|
|
|
|
return this.getNextValidExecutionInfo(retry - 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
protected async _sync() {
|
|
|
|
await this.syncMutex.acquire();
|
|
|
|
|
|
|
|
const currentPeriod = this.getCurrentPeriod();
|
|
|
|
if (currentPeriod > this._latestPeriod) {
|
|
|
|
if (!this.booted) {
|
|
|
|
this.latestCommittee = await this.syncFromGenesis();
|
|
|
|
} else {
|
|
|
|
this.latestCommittee = await this.syncFromLastUpdate();
|
|
|
|
}
|
|
|
|
this._latestPeriod = currentPeriod;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.syncMutex.release();
|
|
|
|
}
|
|
|
|
|
2023-07-11 06:21:30 +00:00
|
|
|
protected async subscribe(callback?: (ei: ExecutionInfo) => void) {
|
|
|
|
setInterval(async () => {
|
|
|
|
try {
|
|
|
|
const ei = await this.getLatestExecution();
|
|
|
|
if (ei && ei.blockHash !== this.latestBlockHash) {
|
|
|
|
this.latestBlockHash = ei.blockHash;
|
|
|
|
return await callback?.(ei);
|
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
console.error(e);
|
|
|
|
}
|
|
|
|
}, POLLING_DELAY);
|
|
|
|
}
|
|
|
|
|
2023-07-12 21:40:59 +00:00
|
|
|
protected async getLatestExecution(): Promise<ExecutionInfo | null> {
|
|
|
|
await this._sync();
|
2023-07-13 06:25:05 +00:00
|
|
|
const update = await this.options.optimisticUpdateCallback();
|
|
|
|
|
|
|
|
const verify = await optimisticUpdateVerify(
|
|
|
|
this.latestCommittee as Uint8Array[],
|
|
|
|
update,
|
|
|
|
);
|
2023-07-13 07:12:49 +00:00
|
|
|
// TODO: check the update against the latest sync committee
|
2023-07-13 06:25:05 +00:00
|
|
|
if (!verify.correct) {
|
|
|
|
console.error(`Invalid Optimistic Update: ${verify.reason}`);
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
console.log(
|
|
|
|
`Optimistic update verified for slot ${update.attestedHeader.beacon.slot}`,
|
2023-07-12 21:40:59 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
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 }> {
|
2023-07-13 05:46:45 +00:00
|
|
|
try {
|
|
|
|
const updates = await this.options.prover.getSyncUpdate(
|
|
|
|
startPeriod,
|
|
|
|
currentPeriod - startPeriod,
|
|
|
|
);
|
2023-07-12 21:40:59 +00:00
|
|
|
|
2023-07-13 05:46:45 +00:00
|
|
|
for (let i = 0; i < updates.length; i++) {
|
|
|
|
const curPeriod = startPeriod + i;
|
|
|
|
const update = updates[i];
|
2023-07-12 21:40:59 +00:00
|
|
|
|
2023-07-13 05:46:45 +00:00
|
|
|
const updatePeriod = computeSyncPeriodAtSlot(
|
|
|
|
update.attestedHeader.beacon.slot,
|
|
|
|
);
|
2023-07-12 21:40:59 +00:00
|
|
|
|
2023-07-13 05:46:45 +00:00
|
|
|
const validOrCommittee = await this.syncUpdateVerifyGetCommittee(
|
|
|
|
startCommittee,
|
|
|
|
curPeriod,
|
|
|
|
update,
|
|
|
|
);
|
2023-07-12 21:40:59 +00:00
|
|
|
|
2023-07-13 05:46:45 +00:00
|
|
|
if (!(validOrCommittee as boolean)) {
|
|
|
|
console.log(`Found invalid update at period(${curPeriod})`);
|
|
|
|
return {
|
|
|
|
syncCommittee: startCommittee,
|
|
|
|
period: curPeriod,
|
|
|
|
};
|
2023-07-12 21:40:59 +00:00
|
|
|
}
|
2023-07-13 05:46:45 +00:00
|
|
|
|
|
|
|
await this.options.store.addUpdate(curPeriod, update);
|
|
|
|
|
|
|
|
startCommittee = validOrCommittee as Uint8Array[];
|
2023-07-12 21:40:59 +00:00
|
|
|
}
|
2023-07-13 05:46:45 +00:00
|
|
|
} catch (e) {
|
|
|
|
console.error(`failed to fetch sync update for period(${startPeriod})`);
|
|
|
|
return {
|
|
|
|
syncCommittee: startCommittee,
|
|
|
|
period: startPeriod,
|
|
|
|
};
|
2023-07-12 21:40:59 +00:00
|
|
|
}
|
|
|
|
return {
|
|
|
|
syncCommittee: startCommittee,
|
|
|
|
period: currentPeriod,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
2023-07-11 05:01:10 +00:00
|
|
|
}
|
|
|
|
|
2023-07-12 21:40:59 +00:00
|
|
|
protected async syncFromGenesis(): Promise<Uint8Array[]> {
|
|
|
|
return this.syncFromLastUpdate(this.genesisPeriod);
|
|
|
|
}
|
2023-07-10 20:44:58 +00:00
|
|
|
|
2023-07-12 21:40:59 +00:00
|
|
|
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})`,
|
|
|
|
);
|
2023-07-10 20:44:58 +00:00
|
|
|
|
2023-07-12 21:40:59 +00:00
|
|
|
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");
|
|
|
|
}
|
2023-07-10 20:44:58 +00:00
|
|
|
}
|