Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/prover/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,9 @@
"http-proxy": "^1.18.1",
"yargs": "^16.1.0",
"source-map-support": "^0.5.19",
"find-up": "^5.0.0"
"find-up": "^5.0.0",
"winston": "^3.3.3",
"winston-transport": "^4.5.0"
},
"devDependencies": {
"@types/http-proxy": "^1.17.10",
Expand Down
2 changes: 2 additions & 0 deletions packages/prover/src/cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {hideBin} from "yargs/helpers";
import {registerCommandToYargs} from "../utils/command.js";
import {getVersionData} from "../utils/version.js";
import {cmds} from "./cmds/index.js";
import {globalOptions} from "./options.js";

const {version} = getVersionData();
const topBanner = `🌟 Lodestar Prover Proxy: Ethereum RPC proxy for RPC responses, verified against the trusted block hashes.
Expand All @@ -30,6 +31,7 @@ export function getLodestarProverCli(): yargs.Argv {
// Manually processing options is typesafe tho more verbose
"dot-notation": false,
})
.options(globalOptions)
// blank scriptName so that help text doesn't display the cli name before each command
.scriptName("")
.demandCommand(1)
Expand Down
4 changes: 3 additions & 1 deletion packages/prover/src/cli/cmds/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import {CliCommand} from "../../utils/command.js";
import {GlobalArgs} from "../options.js";
import {proverProxyStartCommand} from "./start/index.js";

export const cmds = [proverProxyStartCommand];
export const cmds: Required<CliCommand<GlobalArgs, Record<never, never>>>["subcommands"] = [proverProxyStartCommand];
10 changes: 6 additions & 4 deletions packages/prover/src/cli/cmds/start/handler.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import {LCTransport} from "../../../interfaces.js";
import {createVerifiedExecutionProxy, VerifiedProxyOptions} from "../../../web3_proxy.js";
import {stdLogger} from "../../../utils/logger.js";
import {GlobalArgs, parseGlobalArgs} from "../../options.js";
import {parseStartArgs, StartArgs} from "./options.js";

/**
* Runs a beacon node.
*/
export async function proverProxyStartHandler(args: StartArgs): Promise<void> {
export async function proverProxyStartHandler(args: StartArgs & GlobalArgs): Promise<void> {
const {network, logLevel} = parseGlobalArgs(args);
const opts = parseStartArgs(args);
const {network, executionRpcUrl, port, wsCheckpoint} = opts;
const {executionRpcUrl, port, wsCheckpoint} = opts;

const options: VerifiedProxyOptions = {
logger: stdLogger,
logLevel,
network,
executionRpcUrl,
wsCheckpoint,
Expand Down
3 changes: 2 additions & 1 deletion packages/prover/src/cli/cmds/start/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import {CliCommand, CliCommandOptions} from "../../../utils/command.js";
import {GlobalArgs} from "../../options.js";
import {proverProxyStartHandler} from "./handler.js";
import {StartArgs, startOptions} from "./options.js";

export const proverProxyStartCommand: CliCommand<StartArgs> = {
export const proverProxyStartCommand: CliCommand<StartArgs, GlobalArgs> = {
command: "start",
describe: "Start proxy server",
examples: [
Expand Down
11 changes: 0 additions & 11 deletions packages/prover/src/cli/cmds/start/options.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import {NetworkName, networksChainConfig} from "@lodestar/config/networks";
import {LCTransport} from "../../../interfaces.js";
import {CliCommandOptions} from "../../../utils/command.js";

export type StartArgs = {
port: number;
network: string;
"execution-rpc-url": string;
transport: "rest" | "p2p";
"beacon-urls"?: string[];
Expand All @@ -13,7 +11,6 @@ export type StartArgs = {
};

export type StartOptions = {
network: NetworkName;
executionRpcUrl: string;
port: number;
wsCheckpoint?: string;
Expand All @@ -25,13 +22,6 @@ export const startOptions: CliCommandOptions<StartArgs> = {
type: "number",
default: 8080,
},

network: {
description: "Specify the network to connect.",
type: "string",
choices: Object.keys(networksChainConfig),
},

"execution-rpc-url": {
description: "RPC url for the execution node.",
type: "string",
Expand Down Expand Up @@ -68,7 +58,6 @@ export function parseStartArgs(args: StartArgs): StartOptions {
// Remove undefined values to allow deepmerge to inject default values downstream
return {
port: args["port"],
network: args["network"] as NetworkName,
executionRpcUrl: args["execution-rpc-url"],
transport: args["transport"] === "p2p" ? LCTransport.P2P : LCTransport.Rest,
urls: args["transport"] === "rest" ? args["beacon-urls"] ?? [] : [],
Expand Down
36 changes: 36 additions & 0 deletions packages/prover/src/cli/options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import {NetworkName, networksChainConfig} from "@lodestar/config/networks";
import {LogLevel, LogLevels} from "@lodestar/utils";
import {CliCommandOptions} from "../utils/command.js";

export type GlobalArgs = {
network: string;
"log-level": string;
};

export type GlobalOptions = {
logLevel: LogLevel;
network: NetworkName;
};

export const globalOptions: CliCommandOptions<GlobalArgs> = {
network: {
description: "Specify the network to connect.",
type: "string",
choices: Object.keys(networksChainConfig),
},

"log-level": {
description: "Set the log level.",
type: "string",
choices: LogLevels,
default: "info",
},
};

export function parseGlobalArgs(args: GlobalArgs): GlobalOptions {
// Remove undefined values to allow deepmerge to inject default values downstream
return {
network: args["network"] as NetworkName,
logLevel: args["log-level"] as LogLevel,
};
}
15 changes: 13 additions & 2 deletions packages/prover/src/interfaces.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {ChainForkConfig} from "@lodestar/config";
import {NetworkName} from "@lodestar/config/networks";
import {Logger, LogLevel} from "@lodestar/utils";
import {ProofProvider} from "./proof_provider/proof_provider.js";
import {ELRequestPayload, ELResponse} from "./types.js";

Expand All @@ -11,9 +12,10 @@ export enum LCTransport {
export type RootProviderInitOptions = {
network: NetworkName;
signal: AbortSignal;
logger: Logger;
config?: ChainForkConfig;
wsCheckpoint?: string;
} & ({transport: LCTransport.Rest; urls: string[]} | {transport: LCTransport.P2P; bootnodes: string[]});
} & ConsensusNodeOptions;

export type ELRequestMethod = (payload: ELRequestPayload) => Promise<ELResponse | undefined>;

Expand Down Expand Up @@ -47,5 +49,14 @@ export type Web3Provider = SendProvider | EthersProvider | SendAsyncProvider | R
export type ELVerifiedRequestHandler<A = unknown, R = unknown> = (opts: {
payload: ELRequestPayload<A>;
handler: ELRequestMethod;
rootProvider: ProofProvider;
proofProvider: ProofProvider;
logger: Logger;
}) => Promise<ELResponse<R>>;

// Either a logger is provided by user or user specify a log level
// If both are skipped then we don't log anything (useful for browser plugins)
export type LogOptions = {logger?: Logger; logLevel?: never} | {logLevel?: LogLevel; logger?: never};

export type ConsensusNodeOptions =
| {transport: LCTransport.Rest; urls: string[]}
| {transport: LCTransport.P2P; bootnodes: string[]};
21 changes: 18 additions & 3 deletions packages/prover/src/proof_provider/payload_store.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {Api} from "@lodestar/api";
import {allForks, capella} from "@lodestar/types";
import {Logger} from "@lodestar/utils";
import {MAX_PAYLOAD_HISTORY} from "../constants.js";
import {getExecutionPayloadForBlockNumber, getExecutionPayloads} from "../utils/consensus.js";
import {bufferToHex, hexToNumber} from "../utils/conversion.js";
Expand All @@ -23,7 +24,7 @@ export class PayloadStore {

private latestBlockRoot: BlockELRoot | null = null;

constructor(private opts: {api: Api}) {}
constructor(private opts: {api: Api; logger: Logger}) {}

get finalized(): allForks.ExecutionPayload | undefined {
const finalizedMaxRoot = this.finalizedRoots.get(this.finalizedRoots.max);
Expand Down Expand Up @@ -129,7 +130,14 @@ export class PayloadStore {
else if (finalized && !existingELRoot) {
this.payloads.set(
bufferToHex(header.execution.blockHash),
(await getExecutionPayloads(this.opts.api, blockSlot, blockSlot))[blockSlot]
(
await getExecutionPayloads({
api: this.opts.api,
startSlot: blockSlot,
endSlot: blockSlot,
logger: this.opts.logger,
})
)[blockSlot]
);
}

Expand All @@ -149,7 +157,14 @@ export class PayloadStore {
}

// We do not have the payload for this block, we need to fetch it
const payload = (await getExecutionPayloads(this.opts.api, blockSlot, blockSlot))[blockSlot];
const payload = (
await getExecutionPayloads({
api: this.opts.api,
startSlot: blockSlot,
endSlot: blockSlot,
logger: this.opts.logger,
})
)[blockSlot];
this.set(payload, false);
this.prune();
}
Expand Down
41 changes: 30 additions & 11 deletions packages/prover/src/proof_provider/proof_provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {Lightclient, LightclientEvent, RunStatusCode} from "@lodestar/light-clie
import {LightClientRestTransport} from "@lodestar/light-client/transport";
import {isForkWithdrawals} from "@lodestar/params";
import {allForks, capella} from "@lodestar/types";
import {Logger} from "@lodestar/utils";
import {LCTransport, RootProviderInitOptions} from "../interfaces.js";
import {assertLightClient} from "../utils/assertion.js";
import {
Expand All @@ -13,6 +14,7 @@ import {
getSyncCheckpoint,
getUnFinalizedRangeForPayloads,
} from "../utils/consensus.js";
import {bufferToHex} from "../utils/conversion.js";
import {PayloadStore} from "./payload_store.js";

type RootProviderOptions = Omit<RootProviderInitOptions, "transport"> & {
Expand All @@ -23,13 +25,15 @@ type RootProviderOptions = Omit<RootProviderInitOptions, "transport"> & {

export class ProofProvider {
private store: PayloadStore;
private logger: Logger;

// Make sure readyPromise doesn't throw unhandled exceptions
private readyPromise?: Promise<void>;
lightClient?: Lightclient;

constructor(private opts: RootProviderOptions) {
this.store = new PayloadStore({api: opts.api});
this.store = new PayloadStore({api: opts.api, logger: opts.logger});
this.logger = opts.logger;
}

async waitToBeReady(): Promise<void> {
Expand All @@ -40,7 +44,10 @@ export class ProofProvider {
if (opts.transport === LCTransport.P2P) {
throw new Error("P2P mode not supported yet");
}

opts.logger.info("Creating ProofProvider instance with REST APIs", {
network: opts.network,
urls: opts.urls.join(","),
});
const config = createChainForkConfig(networksChainConfig[opts.network]);
const api = getClient({urls: opts.urls}, {config});
const transport = new LightClientRestTransport(api);
Expand All @@ -66,11 +73,12 @@ export class ProofProvider {
if (this.lightClient !== undefined) {
throw Error("Light client already initialized and syncing.");
}

this.logger.info("Starting sync for proof provider");
const {api, config, transport} = this.opts;
const checkpointRoot = await getSyncCheckpoint(api, wsCheckpoint);
const genesisData = await getGenesisData(api);

this.logger.info("Initializing lightclient", {checkpointRoot: bufferToHex(checkpointRoot)});
this.lightClient = await Lightclient.initializeFromCheckpointRoot({
checkpointRoot,
config,
Expand All @@ -88,21 +96,36 @@ export class ProofProvider {
}
};
this.lightClient?.emitter.on(LightclientEvent.statusChange, lightClientStarted);
this.logger.info("Initiating lightclient");
this.lightClient?.start();
});
this.logger.info("Lightclient synced", this.getStatus());
this.registerEvents();

// Load the payloads from the CL
this.logger.info("Building EL payload history");
const {start, end} = await getUnFinalizedRangeForPayloads(this.lightClient);
const payloads = await getExecutionPayloads(this.opts.api, start, end);
const payloads = await getExecutionPayloads({
api: this.opts.api,
startSlot: start,
endSlot: end,
logger: this.logger,
});
for (const payload of Object.values(payloads)) {
this.store.set(payload, false);
}

// Load the finalized payload from the CL
const finalizedSlot = this.lightClient.getFinalized().beacon.slot;
const finalizedPayload = await getExecutionPayloads(this.opts.api, finalizedSlot, finalizedSlot);
this.logger.debug("Getting finalized slot from lightclient", {finalizedSlot});
const finalizedPayload = await getExecutionPayloads({
api: this.opts.api,
startSlot: finalizedSlot,
endSlot: finalizedSlot,
logger: this.logger,
});
this.store.set(finalizedPayload[finalizedSlot], true);
this.logger.info("Proof provider ready");
}

getStatus(): {latest: number; finalized: number; status: RunStatusCode} {
Expand Down Expand Up @@ -172,17 +195,13 @@ export class ProofProvider {

this.lightClient.emitter.on(LightclientEvent.lightClientFinalityHeader, async (data) => {
await this.processLCHeader(data, true).catch((e) => {
// Will be replaced with logger in next PR.
// eslint-disable-next-line no-console
console.error(e);
this.logger.error("Error processing finality update", null, e);
});
});

this.lightClient.emitter.on(LightclientEvent.lightClientOptimisticHeader, async (data) => {
await this.processLCHeader(data).catch((e) => {
// Will be replaced with logger in next PR.
// eslint-disable-next-line no-console
console.error(e);
this.logger.error("Error processing optimistic update", null, e);
});
});
}
Expand Down
19 changes: 13 additions & 6 deletions packages/prover/src/utils/consensus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {Api} from "@lodestar/api/beacon";
import {allForks, Bytes32, capella} from "@lodestar/types";
import {GenesisData, Lightclient} from "@lodestar/light-client";
import {ApiError} from "@lodestar/api";
import {Logger} from "@lodestar/utils";
import {MAX_PAYLOAD_HISTORY} from "../constants.js";
import {hexToBuffer} from "./conversion.js";

Expand Down Expand Up @@ -32,13 +33,19 @@ export async function getUnFinalizedRangeForPayloads(lightClient: Lightclient):
};
}

export async function getExecutionPayloads(
api: Api,
startSlot: number,
endSlot: number
): Promise<Record<number, allForks.ExecutionPayload>> {
export async function getExecutionPayloads({
api,
startSlot,
endSlot,
logger,
}: {
api: Api;
startSlot: number;
endSlot: number;
logger: Logger;
}): Promise<Record<number, allForks.ExecutionPayload>> {
[startSlot, endSlot] = [Math.min(startSlot, endSlot), Math.max(startSlot, endSlot)];

logger.debug("Fetching EL payloads", {startSlot, endSlot});
const payloads: Record<number, allForks.ExecutionPayload> = {};

let slot = endSlot;
Expand Down
Loading