@lodestar/prover
Version:
A Typescript implementation of the Ethereum Consensus light client
176 lines (156 loc) • 5.38 kB
text/typescript
import http from "node:http";
import https from "node:https";
import url from "node:url";
import httpProxy from "http-proxy";
import {LogLevel} from "@lodestar/logger";
import {getNodeLogger} from "@lodestar/logger/node";
import {ELRequestHandler, VerifiedExecutionInitOptions} from "./interfaces.js";
import {ProofProvider} from "./proof_provider/proof_provider.js";
import {JsonRpcRequestOrBatch, JsonRpcRequestPayload, JsonRpcResponseOrBatch} from "./types.js";
import {getResponseForRequest, isBatchRequest} from "./utils/json_rpc.js";
import {processAndVerifyRequest} from "./utils/process.js";
import {fetchRequestPayload, fetchResponseBody} from "./utils/req_resp.js";
import {ELRpcProvider} from "./utils/rpc_provider.js";
export type VerifiedProxyOptions = Exclude<VerifiedExecutionInitOptions<false>, "mutateProvider" | "providerTypes"> & {
executionRpcUrl: string;
requestTimeout: number;
};
function createHttpHandler({
info,
signal,
}: {
signal: AbortSignal;
info: () => {port: number; host: string; timeout: number} | string;
}): ELRequestHandler {
return function handler(payload: JsonRpcRequestOrBatch): Promise<JsonRpcResponseOrBatch | undefined> {
return new Promise((resolve, reject) => {
const serverInfo = info();
if (typeof serverInfo === "string") {
return reject(new Error(serverInfo));
}
const req = http.request(
{
method: "POST",
path: "/proxy",
port: serverInfo.port,
host: serverInfo.host,
timeout: serverInfo.timeout,
signal,
headers: {
"Content-Type": "application/json",
},
},
(res) => {
fetchResponseBody(res)
.then((response) => {
resolve(response);
})
.catch(reject);
}
);
req.on("timeout", () => {
req.destroy();
reject(new Error("Request timeout"));
});
req.write(JSON.stringify(payload));
req.end();
});
};
}
export function createVerifiedExecutionProxy(opts: VerifiedProxyOptions): {
server: http.Server;
proofProvider: ProofProvider;
} {
const {executionRpcUrl, requestTimeout} = opts;
const signal = opts.signal ?? new AbortController().signal;
const logger = opts.logger ?? getNodeLogger({level: opts.logLevel ?? LogLevel.info, module: "prover"});
const proofProvider = ProofProvider.init({
...opts,
signal,
logger,
});
logger.info("Creating http proxy", {url: executionRpcUrl});
const proxy = httpProxy.createProxy({
target: executionRpcUrl,
ws: executionRpcUrl.startsWith("ws"),
agent: executionRpcUrl.startsWith("https") ? https.globalAgent : http.globalAgent,
xfwd: true,
ignorePath: true,
changeOrigin: true,
});
let proxyServerListeningAddress: {host: string; port: number} | undefined;
const rpc = new ELRpcProvider(
createHttpHandler({
signal,
info: () => {
if (!proxyServerListeningAddress) {
return "Proxy server not listening";
}
return {
port: proxyServerListeningAddress.port,
host: proxyServerListeningAddress.host,
timeout: requestTimeout,
};
},
}),
logger
);
logger.info("Creating http server");
const proxyServer = http.createServer(function proxyRequestHandler(req, res) {
if (req.url === "/proxy") {
logger.debug("Forwarding request to execution layer");
proxy.web(req, res);
return;
}
let payload: JsonRpcRequestPayload;
fetchRequestPayload(req)
.then((data) => {
payload = data;
return processAndVerifyRequest({payload, proofProvider, rpc, logger});
})
.then((response) => {
res.write(JSON.stringify(response));
res.end();
})
.catch((err) => {
logger.error("Error processing request", err);
const message = (err as Error).message;
if (isBatchRequest(payload)) {
res.write(JSON.stringify(payload.map((req) => getResponseForRequest(req, {message}))));
} else {
res.write(JSON.stringify(getResponseForRequest(payload, undefined, {message})));
}
res.end();
});
});
proxyServer.on("listening", () => {
const address = proxyServer.address();
if (address === null) {
throw new Error("Invalid proxy server address");
}
if (typeof address === "string") {
const rawUrl = url.parse(address);
if (!rawUrl.host || !rawUrl.port || !rawUrl.protocol) {
throw new Error(`Invalid proxy server address: ${address}`);
}
proxyServerListeningAddress = {host: rawUrl.host, port: parseInt(rawUrl.port)};
} else {
proxyServerListeningAddress = {host: address.address, port: address.port};
}
logger.info(
`Lodestar Prover Proxy listening on ${proxyServerListeningAddress.host}:${proxyServerListeningAddress.port}`
);
rpc.verifyCompatibility().catch((err) => {
logger.error(err);
process.exit(1);
});
});
proxyServer.on("upgrade", function proxyRequestUpgrade(req, socket, head) {
logger.debug("Upgrading the ws connection");
proxy.ws(req, socket, head);
});
signal.addEventListener("abort", () => {
proxyServer.close();
});
return {server: proxyServer, proofProvider};
}