diff --git a/.wordlist.txt b/.wordlist.txt index 17a82cd917fc..ec14a5fc7886 100644 --- a/.wordlist.txt +++ b/.wordlist.txt @@ -60,6 +60,7 @@ LTS Lerna MEV MacOS +Metamask Monorepo NPM NVM @@ -129,6 +130,7 @@ enum env envs ephemery +ethers flamegraph flamegraphs getNetworkIdentity diff --git a/packages/prover/README.md b/packages/prover/README.md index 8d43fd861473..4a0e5f96f761 100644 --- a/packages/prover/README.md +++ b/packages/prover/README.md @@ -17,15 +17,39 @@ You can use the `@lodestar/prover` in two ways, as a Web3 Provider and as proxy. import Web3 from "web3"; import {createVerifiedExecutionProvider, LCTransport} from "@lodestar/prover"; -const {provider, proofProvider} = createVerifiedExecutionProvider( - new Web3.providers.HttpProvider("https://lodestar-sepoliarpc.chainsafe.io"), - { - transport: LCTransport.Rest, - urls: ["https://lodestar-sepolia.chainsafe.io"], - network: "sepolia", - wsCheckpoint: "trusted-checkpoint", - } -); +const httpProvider = new Web3.providers.HttpProvider("https://lodestar-sepoliarpc.chainsafe.io"); + +const {provider, proofProvider} = createVerifiedExecutionProvider(httpProvider, { + transport: LCTransport.Rest, + urls: ["https://lodestar-sepolia.chainsafe.io"], + network: "sepolia", + wsCheckpoint: "trusted-checkpoint", +}); + +const web3 = new Web3(provider); + +const address = "0xf97e180c050e5Ab072211Ad2C213Eb5AEE4DF134"; +const balance = await web3.eth.getBalance(address, "latest"); +console.log({balance, address}); +``` + +In this scenario the actual provider is mutated to handle the RPC requests and verify those. So here if you use `provider` or `httpProvider` both are the same objects. This behavior is useful when you already have an application and usage of any provider across the code space and don't want to change the code. So you mutate the provider during startup. + +For some scenarios when you don't want to mutate the provider you can pass an option `mutateProvider` as `false`. In this scenario the object `httpProvider` is not mutated and you get a new object `provider`. This is useful when your provider object does not allow mutation, e.g. Metamask provider accessible through `window.ethereum`. If not provided `mutateProvider` is considered as `true` by default. In coming releases we will switch its default behavior to `false`. + +```ts +import Web3 from "web3"; +import {createVerifiedExecutionProvider, LCTransport} from "@lodestar/prover"; + +const httpProvider = new Web3.providers.HttpProvider("https://lodestar-sepoliarpc.chainsafe.io"); + +const {provider, proofProvider} = createVerifiedExecutionProvider(httpProvider, { + transport: LCTransport.Rest, + urls: ["https://lodestar-sepolia.chainsafe.io"], + network: "sepolia", + wsCheckpoint: "trusted-checkpoint", + mutateProvider: false, +}); const web3 = new Web3(provider); @@ -46,6 +70,12 @@ lodestar-prover proxy \ --port 8080 ``` +## How to detect a web3 provider + +There can be different implementations of the web3 providers and each can handle the RPC request differently. We call those different provider types. We had provided builtin support for common providers e.g. web3.js, ethers or any eip1193 compatible providers. We inspect given provider instance at runtime to detect the correct provider type. + +If your project is using some provider type which is not among above list, you have the option to register a custom provider type with the `createVerifiedExecutionProvider` with the option `providerTypes` which will be an array of your supported provider types. Your custom provider types will have higher priority than default provider types. Please see [existing provide types implementations](./src/provider_types/) to know how to implement your own if needed. + ## Supported Web3 Methods ✅ - Completed diff --git a/packages/prover/src/interfaces.ts b/packages/prover/src/interfaces.ts index 334bc48837a8..36222c1476d3 100644 --- a/packages/prover/src/interfaces.ts +++ b/packages/prover/src/interfaces.ts @@ -3,7 +3,7 @@ import {NetworkName} from "@lodestar/config/networks"; import {Logger, LogLevel} from "@lodestar/utils"; import {ProofProvider} from "./proof_provider/proof_provider.js"; import {JsonRpcRequest, JsonRpcRequestOrBatch, JsonRpcResponse, JsonRpcResponseOrBatch} from "./types.js"; -import {ELRpc} from "./utils/rpc.js"; +import {ELRpcProvider} from "./utils/rpc_provider.js"; export type {NetworkName} from "@lodestar/config/networks"; export enum LCTransport { @@ -30,50 +30,14 @@ export type ELRequestHandler = ( // eslint-disable-next-line @typescript-eslint/no-explicit-any export type ELRequestHandlerAny = ELRequestHandler; -// Modern providers uses this structure e.g. Web3 4.x -export interface EIP1193Provider { - request: (payload: JsonRpcRequestOrBatch) => Promise; -} - -export interface Web3jsProvider { - request: (payload: JsonRpcRequest) => Promise; -} - -// Some providers uses `request` instead of the `send`. e.g. Ganache -export interface RequestProvider { - request( - payload: JsonRpcRequestOrBatch, - callback: (err: Error | undefined, response: JsonRpcResponseOrBatch) => void - ): void; -} - -// The legacy Web3 1.x use this structure -export interface SendProvider { - send(payload: JsonRpcRequest, callback: (err?: Error | null, response?: JsonRpcResponse) => void): void; -} - -// Ethers provider uses this structure -export interface EthersProvider { - // Ethers provider does not have a public interface for batch requests - send(method: string, params: Array): Promise; -} - -// Some legacy providers use this very old structure -export interface SendAsyncProvider { - sendAsync(payload: JsonRpcRequestOrBatch): Promise; -} - -export type Web3Provider = - | SendProvider - | EthersProvider - | SendAsyncProvider - | RequestProvider - | EIP1193Provider - | Web3jsProvider; +/** + * @deprecated Kept for backward compatibility. Use `AnyWeb3Provider` type instead. + */ +export type Web3Provider = object; export type ELVerifiedRequestHandlerOpts = { payload: JsonRpcRequest; - rpc: ELRpc; + rpc: ELRpcProvider; proofProvider: ProofProvider; logger: Logger; }; @@ -96,4 +60,31 @@ export type RootProviderOptions = { unverifiedWhitelist?: string[]; }; -export type VerifiedExecutionInitOptions = LogOptions & ConsensusNodeOptions & NetworkOrConfig & RootProviderOptions; +export type ProviderTypeOptions = { + /** + * If user specify custom provider types we will register those at the start in given order. + * So if you provider [custom1, custom2] and we already have [web3js, ethers] then final order + * of providers will be [custom1, custom2, web3js, ethers] + */ + providerTypes?: Web3ProviderType[]; + /** + * To keep the backward compatible behavior if this option is not set we consider `true` as default. + * In coming breaking release we may set this option default to `false`. + */ + mutateProvider?: T; +}; + +export type VerifiedExecutionInitOptions = LogOptions & + ConsensusNodeOptions & + NetworkOrConfig & + RootProviderOptions & + ProviderTypeOptions; + +export type AnyWeb3Provider = object; + +export interface Web3ProviderType { + name: string; + matched: (provider: AnyWeb3Provider) => provider is T; + handler(provider: T): ELRpcProvider["handler"]; + mutateProvider(provider: T, newHandler: ELRpcProvider["handler"]): void; +} diff --git a/packages/prover/src/provider_types/eip1193_provider_type.ts b/packages/prover/src/provider_types/eip1193_provider_type.ts new file mode 100644 index 000000000000..98f41482951a --- /dev/null +++ b/packages/prover/src/provider_types/eip1193_provider_type.ts @@ -0,0 +1,32 @@ +import {Web3ProviderType} from "../interfaces.js"; +import {JsonRpcRequestOrBatch, JsonRpcResponseOrBatch} from "../types.js"; + +// Modern providers uses this structure e.g. Web3 4.x +export interface EIP1193Provider { + request: (payload: JsonRpcRequestOrBatch) => Promise; +} +export default { + name: "eip1193", + matched(provider): provider is EIP1193Provider { + return ( + "request" in provider && + typeof provider.request === "function" && + provider.request.constructor.name === "AsyncFunction" + ); + }, + handler(provider) { + const request = provider.request.bind(provider); + + return async (payload: JsonRpcRequestOrBatch): Promise => { + const response = await request(payload); + return response; + }; + }, + mutateProvider(provider, newHandler) { + Object.assign(provider, { + request: async function newRequest(payload: JsonRpcRequestOrBatch): Promise { + return newHandler(payload); + }, + }); + }, +} as Web3ProviderType; diff --git a/packages/prover/src/provider_types/ethers_provider_type.ts b/packages/prover/src/provider_types/ethers_provider_type.ts new file mode 100644 index 000000000000..208e901520fa --- /dev/null +++ b/packages/prover/src/provider_types/ethers_provider_type.ts @@ -0,0 +1,44 @@ +import {Web3ProviderType} from "../interfaces.js"; +import {JsonRpcRequestOrBatch, JsonRpcResponse, JsonRpcResponseOrBatch} from "../types.js"; +import {isBatchRequest} from "../utils/json_rpc.js"; +import web3JsProviderType from "./web3_js_provider_type.js"; + +export interface EthersProvider { + // Ethers provider does not have a public interface for batch requests + send(method: string, params: Array): Promise; +} +export default { + name: "ethers", + matched(provider): provider is EthersProvider { + return ( + !web3JsProviderType.matched(provider) && + "send" in provider && + typeof provider.send === "function" && + provider.send.length > 1 && + provider.send.constructor.name === "AsyncFunction" + ); + }, + handler(provider) { + const send = provider.send.bind(provider); + + return async (payload: JsonRpcRequestOrBatch): Promise => { + // Because ethers provider public interface does not support batch requests + // so we need to handle it manually + if (isBatchRequest(payload)) { + const responses = []; + for (const request of payload) { + responses.push(await send(request.method, request.params)); + } + return responses; + } + return send(payload.method, payload.params); + }; + }, + mutateProvider(provider, newHandler) { + Object.assign(provider, { + send: function newSend(method: string, params: Array): Promise { + return newHandler({jsonrpc: "2.0", id: 0, method, params}); + }, + }); + }, +} as Web3ProviderType; diff --git a/packages/prover/src/provider_types/legacy_provider_type.ts b/packages/prover/src/provider_types/legacy_provider_type.ts new file mode 100644 index 000000000000..7dbbe7598e66 --- /dev/null +++ b/packages/prover/src/provider_types/legacy_provider_type.ts @@ -0,0 +1,123 @@ +import {AnyWeb3Provider, Web3ProviderType} from "../interfaces.js"; +import {JsonRpcRequest, JsonRpcRequestOrBatch, JsonRpcResponse, JsonRpcResponseOrBatch} from "../types.js"; +import web3JsProviderType from "./web3_js_provider_type.js"; + +// Some providers uses `request` instead of the `send`. e.g. Ganache +interface RequestProvider { + request( + payload: JsonRpcRequestOrBatch, + callback: (err: Error | undefined, response: JsonRpcResponseOrBatch) => void + ): void; +} +// The legacy Web3 1.x use this structure +interface SendProvider { + send(payload: JsonRpcRequest, callback: (err?: Error | null, response?: JsonRpcResponse) => void): void; +} +// Some legacy providers use this very old structure +interface SendAsyncProvider { + sendAsync(payload: JsonRpcRequestOrBatch): Promise; +} + +type LegacyProvider = RequestProvider | SendProvider | SendAsyncProvider; + +export default { + name: "legacy", + matched(provider): provider is LegacyProvider { + return isRequestProvider(provider) || isSendProvider(provider) || isSendAsyncProvider(provider); + }, + handler(provider) { + if (isRequestProvider(provider)) { + const request = provider.request.bind(provider); + return function newHandler(payload: JsonRpcRequestOrBatch): Promise { + return new Promise((resolve, reject) => { + request(payload, (err, response) => { + if (err) { + reject(err); + } else { + resolve(response); + } + }); + }); + }; + } + if (isSendProvider(provider)) { + const send = provider.send.bind(provider); + return function newHandler(payload: JsonRpcRequestOrBatch): Promise { + return new Promise((resolve, reject) => { + // web3 providers supports batch requests but don't have valid types + send(payload as JsonRpcRequest, (err, response) => { + if (err) { + reject(err); + } else { + resolve(response); + } + }); + }); + }; + } + + // sendAsync provider + const sendAsync = provider.sendAsync.bind(provider); + return async function newHandler(payload: JsonRpcRequestOrBatch): Promise { + const response = await sendAsync(payload); + return response; + }; + }, + mutateProvider(provider, newHandler) { + if (isRequestProvider(provider)) { + const newRequest = function newRequest( + payload: JsonRpcRequestOrBatch, + callback: (err?: Error | null, response?: JsonRpcResponseOrBatch) => void + ): void { + newHandler(payload) + .then((response) => callback(undefined, response)) + .catch((err) => callback(err, undefined)); + }; + + Object.assign(provider, {request: newRequest}); + } + + if (isSendProvider(provider)) { + const newSend = function newSend( + payload: JsonRpcRequestOrBatch, + callback: (err?: Error | null, response?: JsonRpcResponseOrBatch) => void + ): void { + newHandler(payload) + .then((response) => callback(undefined, response)) + .catch((err) => callback(err, undefined)); + }; + + Object.assign(provider, {send: newSend}); + } + + // sendAsync provider + Object.assign(provider, {sendAsync: newHandler}); + }, +} as Web3ProviderType; + +function isSendProvider(provider: AnyWeb3Provider): provider is SendProvider { + return ( + !web3JsProviderType.matched(provider) && + "send" in provider && + typeof provider.send === "function" && + provider.send.length > 1 && + provider.send.constructor.name !== "AsyncFunction" + ); +} + +function isRequestProvider(provider: AnyWeb3Provider): provider is RequestProvider { + return ( + !web3JsProviderType.matched(provider) && + "request" in provider && + typeof provider.request === "function" && + provider.request.length > 1 + ); +} + +function isSendAsyncProvider(provider: AnyWeb3Provider): provider is SendAsyncProvider { + return ( + "sendAsync" in provider && + typeof provider.sendAsync === "function" && + provider.sendAsync.constructor.name === "AsyncFunction" + ); +} diff --git a/packages/prover/src/provider_types/web3_js_provider_type.ts b/packages/prover/src/provider_types/web3_js_provider_type.ts new file mode 100644 index 000000000000..bc95d47a6f7f --- /dev/null +++ b/packages/prover/src/provider_types/web3_js_provider_type.ts @@ -0,0 +1,35 @@ +import {AnyWeb3Provider, Web3ProviderType} from "../interfaces.js"; +import {JsonRpcRequest, JsonRpcRequestOrBatch, JsonRpcResponse, JsonRpcResponseOrBatch} from "../types.js"; +import {isBatchRequest} from "../utils/json_rpc.js"; + +export interface Web3jsProvider { + request: (payload: JsonRpcRequest) => Promise; +} + +export default { + name: "web3js", + matched(provider): provider is Web3jsProvider { + return ( + "isWeb3Provider" in provider.constructor && + (provider.constructor as {isWeb3Provider: (provider: AnyWeb3Provider) => boolean}).isWeb3Provider(provider) + ); + }, + handler(provider) { + const request = provider.request.bind(provider); + + return async (payload: JsonRpcRequestOrBatch): Promise => { + if (isBatchRequest(payload)) { + return Promise.all(payload.map((p) => request(p))); + } + + return request(payload); + }; + }, + mutateProvider(provider, newHandler) { + Object.assign(provider, { + request: async function newRequest(payload: JsonRpcRequestOrBatch): Promise { + return newHandler(payload); + }, + }); + }, +} as Web3ProviderType; diff --git a/packages/prover/src/types.ts b/packages/prover/src/types.ts index 58040cafee76..781ba1f7b207 100644 --- a/packages/prover/src/types.ts +++ b/packages/prover/src/types.ts @@ -147,6 +147,7 @@ export type ELStorageProof = Pick; /* eslint-disable @typescript-eslint/naming-convention */ export type ELApi = { + eth_getBalance: (address: string, block?: number | string) => string; eth_createAccessList: (transaction: ELTransaction, block?: ELBlockNumberOrTag) => ELAccessListResponse; eth_call: (transaction: ELTransaction, block?: ELBlockNumberOrTag) => HexString; eth_estimateGas: (transaction: ELTransaction, block?: ELBlockNumberOrTag) => HexString; diff --git a/packages/prover/src/utils/assertion.ts b/packages/prover/src/utils/assertion.ts index e302b9a9200a..82432a031646 100644 --- a/packages/prover/src/utils/assertion.ts +++ b/packages/prover/src/utils/assertion.ts @@ -1,12 +1,4 @@ import {Lightclient} from "@lodestar/light-client"; -import { - EIP1193Provider, - EthersProvider, - RequestProvider, - SendAsyncProvider, - SendProvider, - Web3Provider, -} from "../interfaces.js"; export function assertLightClient(client?: Lightclient): asserts client is Lightclient { if (!client) { @@ -14,62 +6,6 @@ export function assertLightClient(client?: Lightclient): asserts client is Light } } -/** - * Checks if the provider is a web3.js version 4x. - */ -export function isWeb3jsProvider(provider: Web3Provider): provider is EIP1193Provider { - return ( - "isWeb3Provider" in provider.constructor && - (provider.constructor as {isWeb3Provider: (provider: Web3Provider) => boolean}).isWeb3Provider(provider) - ); -} - -export function isSendProvider(provider: Web3Provider): provider is SendProvider { - return ( - !isWeb3jsProvider(provider) && - "send" in provider && - typeof provider.send === "function" && - provider.send.length > 1 && - provider.send.constructor.name !== "AsyncFunction" - ); -} - -export function isEthersProvider(provider: Web3Provider): provider is EthersProvider { - return ( - !isWeb3jsProvider(provider) && - "send" in provider && - typeof provider.send === "function" && - provider.send.length > 1 && - provider.send.constructor.name === "AsyncFunction" - ); -} - -export function isRequestProvider(provider: Web3Provider): provider is RequestProvider { - return ( - !isWeb3jsProvider(provider) && - "request" in provider && - typeof provider.request === "function" && - provider.request.length > 1 - ); -} - -export function isSendAsyncProvider(provider: Web3Provider): provider is SendAsyncProvider { - return ( - "sendAsync" in provider && - typeof provider.sendAsync === "function" && - provider.sendAsync.constructor.name === "AsyncFunction" - ); -} - -export function isEIP1193Provider(provider: Web3Provider): provider is EIP1193Provider { - return ( - !isWeb3jsProvider(provider) && - "request" in provider && - typeof provider.request === "function" && - provider.request.constructor.name === "AsyncFunction" - ); -} - export function isTruthy(value: T): value is Exclude { return value !== undefined && value !== null && value !== false; } diff --git a/packages/prover/src/utils/evm.ts b/packages/prover/src/utils/evm.ts index 0b45866ad559..bbdba907c35f 100644 --- a/packages/prover/src/utils/evm.ts +++ b/packages/prover/src/utils/evm.ts @@ -13,7 +13,7 @@ import {bufferToHex, chunkIntoN, cleanObject, hexToBigInt, hexToBuffer, numberTo import {getChainCommon, getTxType} from "./execution.js"; import {isValidResponse} from "./json_rpc.js"; import {isNullish, isValidAccount, isValidCodeHash, isValidStorageKeys} from "./validation.js"; -import {ELRpc} from "./rpc.js"; +import {ELRpcProvider} from "./rpc_provider.js"; export async function createVM({proofProvider}: {proofProvider: ProofProvider}): Promise { const common = getChainCommon(proofProvider.config.PRESET_BASE as string); @@ -39,7 +39,7 @@ export async function getVMWithState({ vm, logger, }: { - rpc: ELRpc; + rpc: ELRpcProvider; vm: VM; executionPayload: allForks.ExecutionPayload; tx: ELTransaction; @@ -160,7 +160,7 @@ export async function executeVMCall({ executionPayload, network, }: { - rpc: ELRpc; + rpc: ELRpcProvider; tx: ELTransaction; vm: VM; executionPayload: allForks.ExecutionPayload; @@ -202,7 +202,7 @@ export async function executeVMTx({ executionPayload, network, }: { - rpc: ELRpc; + rpc: ELRpcProvider; tx: ELTransaction; vm: VM; executionPayload: allForks.ExecutionPayload; diff --git a/packages/prover/src/utils/execution.ts b/packages/prover/src/utils/execution.ts index 083f15e1e5dd..8b0fac1f784e 100644 --- a/packages/prover/src/utils/execution.ts +++ b/packages/prover/src/utils/execution.ts @@ -2,11 +2,14 @@ import {Common, CustomChain, Hardfork} from "@ethereumjs/common"; import {ELApiParams, ELApiReturn, ELTransaction} from "../types.js"; import {isValidResponse} from "./json_rpc.js"; import {isBlockNumber, isPresent} from "./validation.js"; -import {ELRpc} from "./rpc.js"; +import {ELRpcProvider} from "./rpc_provider.js"; export type Optional = Omit & {[P in keyof T]?: T[P] | undefined}; -export async function getELCode(rpc: ELRpc, args: ELApiParams["eth_getCode"]): Promise { +export async function getELCode( + rpc: ELRpcProvider, + args: ELApiParams["eth_getCode"] +): Promise { const codeResult = await rpc.request("eth_getCode", args, {raiseError: false}); if (!isValidResponse(codeResult)) { @@ -16,7 +19,10 @@ export async function getELCode(rpc: ELRpc, args: ELApiParams["eth_getCode"]): P return codeResult.result; } -export async function getELProof(rpc: ELRpc, args: ELApiParams["eth_getProof"]): Promise { +export async function getELProof( + rpc: ELRpcProvider, + args: ELApiParams["eth_getProof"] +): Promise { const proof = await rpc.request("eth_getProof", args, {raiseError: false}); if (!isValidResponse(proof)) { throw new Error(`Can not find proof for address=${args[0]}`); @@ -25,7 +31,7 @@ export async function getELProof(rpc: ELRpc, args: ELApiParams["eth_getProof"]): } export async function getELBlock( - rpc: ELRpc, + rpc: ELRpcProvider, args: ELApiParams["eth_getBlockByNumber"] ): Promise { const block = await rpc.request(isBlockNumber(args[0]) ? "eth_getBlockByNumber" : "eth_getBlockByHash", args, { diff --git a/packages/prover/src/utils/process.ts b/packages/prover/src/utils/process.ts index 49bff6a840d2..26768ffce1fb 100644 --- a/packages/prover/src/utils/process.ts +++ b/packages/prover/src/utils/process.ts @@ -11,7 +11,7 @@ import {eth_call} from "../verified_requests/eth_call.js"; import {eth_estimateGas} from "../verified_requests/eth_estimateGas.js"; import {getResponseForRequest, isBatchRequest, isRequest} from "./json_rpc.js"; import {isNullish} from "./validation.js"; -import {ELRpc} from "./rpc.js"; +import {ELRpcProvider} from "./rpc_provider.js"; /* eslint-disable @typescript-eslint/naming-convention, @typescript-eslint/no-explicit-any */ export const verifiableMethodHandlers: Record> = { @@ -69,7 +69,7 @@ export async function processAndVerifyRequest({ logger, }: { payload: JsonRpcRequestOrBatch; - rpc: ELRpc; + rpc: ELRpcProvider; proofProvider: ProofProvider; logger: Logger; }): Promise { diff --git a/packages/prover/src/utils/rpc.ts b/packages/prover/src/utils/rpc_provider.ts similarity index 87% rename from packages/prover/src/utils/rpc.ts rename to packages/prover/src/utils/rpc_provider.ts index a566c2df913d..4e7a77b2ca8d 100644 --- a/packages/prover/src/utils/rpc.ts +++ b/packages/prover/src/utils/rpc_provider.ts @@ -23,7 +23,7 @@ import {isNullish} from "./validation.js"; export type Optional = Omit & {[P in keyof T]?: T[P] | undefined}; -export class ELRpc { +export class ELRpcProvider { private handler: ELRequestHandler; private logger: Logger; @@ -34,12 +34,23 @@ export class ELRpc { this.logger = logger; } + /** + * Request the EL RPC Provider + * + * @template K + * @template E + * @param {K} method - RPC Method + * @param {ELApiParams[K]} params - RPC Params + * @param {{raiseError?: E}} [opts] + * @return {*} {Promise : JsonRpcResponseWithResultPayload>} + * @memberof ELRpc + */ async request( method: K, params: ELApiParams[K], - opts: {raiseError: E} + opts?: {raiseError?: E} ): Promise : JsonRpcResponseWithResultPayload> { - const {raiseError} = opts; + const {raiseError} = opts ?? {raiseError: true}; const payload: JsonRpcRequest = {jsonrpc: "2.0", method, params, id: this.getRequestId()}; logRequest(payload, this.logger); diff --git a/packages/prover/src/utils/verification.ts b/packages/prover/src/utils/verification.ts index b168ea327e03..a468bf277eba 100644 --- a/packages/prover/src/utils/verification.ts +++ b/packages/prover/src/utils/verification.ts @@ -4,7 +4,7 @@ import {ELBlock, ELProof, HexString, JsonRpcRequest} from "../types.js"; import {bufferToHex} from "./conversion.js"; import {getELBlock, getELCode, getELProof} from "./execution.js"; import {isValidAccount, isValidBlock, isValidCodeHash, isValidStorageKeys} from "./validation.js"; -import {ELRpc} from "./rpc.js"; +import {ELRpcProvider} from "./rpc_provider.js"; type VerificationResult = {data: T; valid: true} | {valid: false; data?: undefined}; @@ -16,7 +16,7 @@ export async function verifyAccount({ block, }: { address: HexString; - rpc: ELRpc; + rpc: ELRpcProvider; proofProvider: ProofProvider; logger: Logger; block?: number | string; @@ -54,7 +54,7 @@ export async function verifyCode({ block, }: { address: HexString; - rpc: ELRpc; + rpc: ELRpcProvider; proofProvider: ProofProvider; logger: Logger; codeHash: HexString; @@ -81,7 +81,7 @@ export async function verifyBlock({ rpc, }: { payload: JsonRpcRequest<[block: string | number, hydrated: boolean]>; - rpc: ELRpc; + rpc: ELRpcProvider; proofProvider: ProofProvider; logger: Logger; }): Promise> { diff --git a/packages/prover/src/web3_provider.ts b/packages/prover/src/web3_provider.ts index 38dee22cedc7..b42c349d1017 100644 --- a/packages/prover/src/web3_provider.ts +++ b/packages/prover/src/web3_provider.ts @@ -1,41 +1,34 @@ -import {Logger} from "@lodestar/utils"; -import {getBrowserLogger} from "@lodestar/logger/browser"; import {LogLevel} from "@lodestar/logger"; -import { - EIP1193Provider, - EthersProvider, - RequestProvider, - SendAsyncProvider, - SendProvider, - VerifiedExecutionInitOptions, - Web3Provider, -} from "./interfaces.js"; +import {getBrowserLogger} from "@lodestar/logger/browser"; +import {Logger} from "@lodestar/utils"; +import {AnyWeb3Provider, ELRequestHandler, VerifiedExecutionInitOptions} from "./interfaces.js"; import {ProofProvider} from "./proof_provider/proof_provider.js"; -import {JsonRpcRequest, JsonRpcRequestOrBatch, JsonRpcResponseOrBatch} from "./types.js"; -import { - isEIP1193Provider, - isEthersProvider, - isRequestProvider, - isSendAsyncProvider, - isSendProvider, - isWeb3jsProvider, -} from "./utils/assertion.js"; +import {ELRpcProvider} from "./utils/rpc_provider.js"; +import {Web3ProviderInspector} from "./web3_provider_inspector.js"; import {processAndVerifyRequest} from "./utils/process.js"; -import {isBatchRequest} from "./utils/json_rpc.js"; -import {ELRpc} from "./utils/rpc.js"; -export type Web3ProviderTypeHandler = ( +export type Web3ProviderTypeHandler = ( provider: T, proofProvider: ProofProvider, logger: Logger -) => {provider: T; rpc: ELRpc}; +) => {provider: T; handler: ELRpcProvider["handler"]}; -export function createVerifiedExecutionProvider( - provider: T, - opts: VerifiedExecutionInitOptions -): {provider: T; proofProvider: ProofProvider} { +export function createVerifiedExecutionProvider< + T extends AnyWeb3Provider, + Mutate extends undefined | boolean = true, + Return = {provider: Mutate extends undefined | true ? T : ELRpcProvider; proofProvider: ProofProvider}, +>(provider: T, opts: VerifiedExecutionInitOptions): Return { const signal = opts.signal ?? new AbortController().signal; const logger = opts.logger ?? getBrowserLogger({level: opts.logLevel ?? LogLevel.info}); + const mutateProvider = opts.mutateProvider === undefined; + const customProviderTypes = opts.providerTypes ?? []; + + const providerInspector = Web3ProviderInspector.initWithDefault({logger}); + for (const providerType of customProviderTypes.reverse()) { + providerInspector.register(providerType, {index: 0}); + } + const providerType = providerInspector.detect(provider); + logger.debug(`Provider is detected as '${providerType.name}' provider.`); const proofProvider = ProofProvider.init({ ...opts, @@ -43,191 +36,23 @@ export function createVerifiedExecutionProvider( logger, }); - const handler = getProviderTypeHandler(provider, logger); - const {provider: newInstance, rpc} = handler(provider, proofProvider, logger); + const nonVerifiedHandler = providerType.handler(provider); + const nonVerifiedRpc = new ELRpcProvider(nonVerifiedHandler, logger); - rpc.verifyCompatibility().catch((err) => { + nonVerifiedRpc.verifyCompatibility().catch((err) => { logger.error(err); logger.error("Due to compatibility issues, verified execution may not work properly."); }); - return {provider: newInstance, proofProvider: proofProvider}; -} - -function handleSendProvider( - provider: SendProvider, - proofProvider: ProofProvider, - logger: Logger -): {provider: SendProvider; rpc: ELRpc} { - const send = provider.send.bind(provider); - const handler = (payload: JsonRpcRequestOrBatch): Promise => - new Promise((resolve, reject) => { - // web3 providers supports batch requests but don't have valid types - send(payload as JsonRpcRequest, (err, response) => { - if (err) { - reject(err); - } else { - resolve(response); - } - }); - }); - const rpc = new ELRpc(handler, logger); - - function newSend( - payload: JsonRpcRequestOrBatch, - callback: (err?: Error | null, response?: JsonRpcResponseOrBatch) => void - ): void { - processAndVerifyRequest({payload, rpc, proofProvider, logger}) - .then((response) => callback(undefined, response)) - .catch((err) => callback(err, undefined)); - } - - return {provider: Object.assign(provider, {send: newSend}), rpc}; -} - -function handleRequestProvider( - provider: RequestProvider, - proofProvider: ProofProvider, - logger: Logger -): {provider: RequestProvider; rpc: ELRpc} { - const request = provider.request.bind(provider); - const handler = (payload: JsonRpcRequestOrBatch): Promise => - new Promise((resolve, reject) => { - request(payload, (err, response) => { - if (err) { - reject(err); - } else { - resolve(response); - } - }); - }); - const rpc = new ELRpc(handler, logger); - - function newRequest( - payload: JsonRpcRequestOrBatch, - callback: (err?: Error | null, response?: JsonRpcResponseOrBatch) => void - ): void { - processAndVerifyRequest({payload, rpc, proofProvider, logger}) - .then((response) => callback(undefined, response)) - .catch((err) => callback(err, undefined)); - } - - return {provider: Object.assign(provider, {request: newRequest}), rpc}; -} - -function handleSendAsyncProvider( - provider: SendAsyncProvider, - proofProvider: ProofProvider, - logger: Logger -): {provider: SendAsyncProvider; rpc: ELRpc} { - const sendAsync = provider.sendAsync.bind(provider); - const handler = async (payload: JsonRpcRequestOrBatch): Promise => { - const response = await sendAsync(payload); - return response; + const verifiedHandler: ELRequestHandler = function newVerifiedHandler(payload) { + return processAndVerifyRequest({payload, rpc: nonVerifiedRpc, logger, proofProvider}); }; - const rpc = new ELRpc(handler, logger); - - async function newSendAsync(payload: JsonRpcRequestOrBatch): Promise { - return processAndVerifyRequest({payload, rpc, proofProvider, logger}); - } - - return {provider: Object.assign(provider, {sendAsync: newSendAsync}), rpc}; -} - -function handleEIP1193Provider( - provider: EIP1193Provider, - proofProvider: ProofProvider, - logger: Logger -): {provider: EIP1193Provider; rpc: ELRpc} { - const request = provider.request.bind(provider); - const handler = async (payload: JsonRpcRequestOrBatch): Promise => { - const response = await request(payload); - return response; - }; - const rpc = new ELRpc(handler, logger); - - async function newRequest(payload: JsonRpcRequestOrBatch): Promise { - return processAndVerifyRequest({payload, rpc, proofProvider, logger}); - } - - return {provider: Object.assign(provider, {request: newRequest}), rpc}; -} - -function handleEthersProvider( - provider: EthersProvider, - proofProvider: ProofProvider, - logger: Logger -): {provider: EthersProvider; rpc: ELRpc} { - const send = provider.send.bind(provider); - const handler = async (payload: JsonRpcRequestOrBatch): Promise => { - // Because ethers provider public interface does not support batch requests - // so we need to handle it manually - if (isBatchRequest(payload)) { - const responses = []; - for (const request of payload) { - responses.push(await send(request.method, request.params)); - } - return responses; - } - - return send(payload.method, payload.params); - }; - const rpc = new ELRpc(handler, logger); - - async function newSend(method: string, params: Array): Promise { - return processAndVerifyRequest({ - payload: {jsonrpc: "2.0", id: 0, method, params}, - rpc, - proofProvider, - logger, - }); - } - - return {provider: Object.assign(provider, {send: newSend}), rpc}; -} -/** - * - * - * @export - * @template T - * @param {T} provider - * @param {Logger} logger - * @return {*} {Web3ProviderTypeHandler} - */ -export function getProviderTypeHandler( - provider: T, - logger: Logger -): Web3ProviderTypeHandler { - if (isWeb3jsProvider(provider)) { - logger.debug("Provider is recognized as 'web3.js' provider."); - // EIP-1193 provider is fully compatible with web3.js#4x provider interface - return handleEIP1193Provider as unknown as Web3ProviderTypeHandler; - } - - if (isEthersProvider(provider)) { - logger.debug("Provider is recognized as 'ethers' provider."); - return handleEthersProvider as unknown as Web3ProviderTypeHandler; - } - - if (isEIP1193Provider(provider)) { - logger.debug("Provider is recognized as 'EIP1193' provider."); - return handleEIP1193Provider as unknown as Web3ProviderTypeHandler; - } - - if (isSendProvider(provider)) { - logger.debug("Provider is recognized as legacy provider with 'send' method."); - return handleSendProvider as unknown as Web3ProviderTypeHandler; - } - - if (isRequestProvider(provider)) { - logger.debug("Provider is recognized as legacy provider with 'request' method."); - return handleRequestProvider as unknown as Web3ProviderTypeHandler; - } - if (isSendAsyncProvider(provider)) { - logger.debug("Provider is recognized as legacy provider with 'sendAsync' method."); - return handleSendAsyncProvider as unknown as Web3ProviderTypeHandler; + if (mutateProvider) { + providerType.mutateProvider(provider, verifiedHandler); + return {provider, proofProvider} as Return; } - throw new Error("Unsupported provider type"); + // Verified RPC + return {provider: new ELRpcProvider(verifiedHandler, logger), proofProvider} as Return; } diff --git a/packages/prover/src/web3_provider_inspector.ts b/packages/prover/src/web3_provider_inspector.ts new file mode 100644 index 000000000000..c81847b64d61 --- /dev/null +++ b/packages/prover/src/web3_provider_inspector.ts @@ -0,0 +1,89 @@ +import {Logger} from "@lodestar/logger"; +import {AnyWeb3Provider, Web3ProviderType} from "./interfaces.js"; + +import web3jsProviderType from "./provider_types/web3_js_provider_type.js"; +import ethersProviderType from "./provider_types/ethers_provider_type.js"; +import eip1193ProviderType from "./provider_types/eip1193_provider_type.js"; +import legacyProviderType from "./provider_types/legacy_provider_type.js"; + +export class Web3ProviderInspector { + protected providerTypes: Web3ProviderType[] = []; + logger: Logger; + + protected constructor(opts: {logger: Logger}) { + this.logger = opts.logger; + } + + static initWithDefault(opts: {logger: Logger}): Web3ProviderInspector { + const inspector = new Web3ProviderInspector(opts); + inspector.register(web3jsProviderType, {index: 0}); + inspector.register(ethersProviderType, {index: 1}); + inspector.register(eip1193ProviderType, {index: 2}); + inspector.register(legacyProviderType, {index: 3}); + + return inspector; + } + + getProviderTypes(): Web3ProviderType[] { + // Destruct so user can not mutate the output + return [...this.providerTypes]; + } + + register(providerType: Web3ProviderType, opts?: {index?: number}): void { + // If user does not provider index, we will register the provider type to last + let index = opts?.index ?? this.providerTypes.length; + + // If index is larger, let's add type at the end + if (index > this.providerTypes.length) { + index = this.providerTypes.length; + } + + // If a lower index is provided let's add type at the start + if (index < 0) { + index = 0; + } + + if (this.providerTypes.map((p) => p.name).includes(providerType.name)) { + throw new Error(`Provider type '${providerType.name}' is already registered.`); + } + + // If some provider type is already register on that index, we will make space for new + if (this.providerTypes.at(index)) { + this.logger.debug( + `A provider type '${this.providerTypes[index].name}' already existed at index '${index}', now moved down.` + ); + this.providerTypes.splice(index, 0, providerType); + } + + this.logger.debug(`Registered provider type "${providerType.name}" at index ${index}`); + this.providerTypes[index] = providerType; + } + + unregister(indexOrName: string | number): void { + if (typeof indexOrName === "number") { + if (indexOrName > this.providerTypes.length || indexOrName < 0) { + throw new Error(`Provider type at index '${indexOrName}' is not registered.`); + } + this.providerTypes.splice(indexOrName, 1); + return; + } + + const index = this.providerTypes.findIndex((p) => p.name == indexOrName); + if (index < 0) { + throw Error(`Provider type '${indexOrName}' is not registered.`); + } + this.providerTypes.splice(index, 1); + } + + detect(provider: AnyWeb3Provider): Web3ProviderType { + for (const providerType of Object.values(this.providerTypes)) { + if (providerType.matched(provider)) { + return providerType; + } + } + + throw new Error( + `Given provider could not be detected of any type. Supported types are ${Object.keys(this.providerTypes).join()}` + ); + } +} diff --git a/packages/prover/src/web3_proxy.ts b/packages/prover/src/web3_proxy.ts index 6aa44b314953..273508852adf 100644 --- a/packages/prover/src/web3_proxy.ts +++ b/packages/prover/src/web3_proxy.ts @@ -10,9 +10,9 @@ import {JsonRpcRequestOrBatch, JsonRpcRequestPayload, JsonRpcResponseOrBatch} fr import {getResponseForRequest, isBatchRequest} from "./utils/json_rpc.js"; import {fetchRequestPayload, fetchResponseBody} from "./utils/req_resp.js"; import {processAndVerifyRequest} from "./utils/process.js"; -import {ELRpc} from "./utils/rpc.js"; +import {ELRpcProvider} from "./utils/rpc_provider.js"; -export type VerifiedProxyOptions = VerifiedExecutionInitOptions & { +export type VerifiedProxyOptions = Exclude, "mutateProvider" | "providerTypes"> & { executionRpcUrl: string; requestTimeout: number; }; @@ -86,7 +86,7 @@ export function createVerifiedExecutionProxy(opts: VerifiedProxyOptions): { }); let proxyServerListeningAddress: {host: string; port: number} | undefined; - const rpc = new ELRpc( + const rpc = new ELRpcProvider( createHttpHandler({ signal, info: () => { diff --git a/packages/prover/test/e2e/web3_provider.test.ts b/packages/prover/test/e2e/web3_provider.test.ts index ad00ae71b9fc..3d670d7ed412 100644 --- a/packages/prover/test/e2e/web3_provider.test.ts +++ b/packages/prover/test/e2e/web3_provider.test.ts @@ -43,5 +43,22 @@ describe("web3_provider", function () { await expect(provider.send("eth_getProof", [accounts[0].address, [], "latest"])).resolves.toBeDefined(); }); }); + + describe("ELRpc", () => { + it("should connect to the network and and call verified methods only", async () => { + const nonVerifiedProvider = new Web3.providers.HttpProvider(rpcUrl); + const {provider: verifiedProvider} = createVerifiedExecutionProvider(nonVerifiedProvider, { + transport: LCTransport.Rest, + urls: [beaconUrl], + config, + mutateProvider: false, + }); + + const web3 = new Web3(nonVerifiedProvider); + const accounts = await web3.eth.getAccounts(); + + await expect(verifiedProvider.request("eth_getBalance", [accounts[0], "latest"])).resolves.toBeDefined(); + }); + }); }); }); diff --git a/packages/prover/test/unit/provider_types/ethers_provider_type.test.ts b/packages/prover/test/unit/provider_types/ethers_provider_type.test.ts new file mode 100644 index 000000000000..c24252d9548f --- /dev/null +++ b/packages/prover/test/unit/provider_types/ethers_provider_type.test.ts @@ -0,0 +1,16 @@ +import {describe, it, expect} from "vitest"; +import {ethers} from "ethers"; +import {Web3} from "web3"; +import ethersProviderType from "../../../src/provider_types/ethers_provider_type.js"; + +describe("matched", () => { + it("should return false if provider is not ethers provider", () => { + const provider = new Web3.providers.HttpProvider("https://lodestar-sepoliarpc.chainsafe.io"); + expect(ethersProviderType.matched(provider)).toBe(false); + }); + + it("should return true if provider is ethers provider", () => { + const provider = new ethers.JsonRpcProvider("https://lodestar-sepoliarpc.chainsafe.io"); + expect(ethersProviderType.matched(provider)).toBe(true); + }); +}); diff --git a/packages/prover/test/unit/provider_types/legacy_provider_type.test.ts b/packages/prover/test/unit/provider_types/legacy_provider_type.test.ts new file mode 100644 index 000000000000..37ffc18e58ab --- /dev/null +++ b/packages/prover/test/unit/provider_types/legacy_provider_type.test.ts @@ -0,0 +1,27 @@ +import {describe, it, expect} from "vitest"; +import {ethers} from "ethers"; +import {Web3} from "web3"; +import legacyProviderType from "../../../src/provider_types/legacy_provider_type.js"; + +describe("send provider", () => { + describe("matched", () => { + it("should return true if provider is SendProvider", () => { + const provider = { + send: (_payload: any, _cb: () => void) => { + // Do nothing; + }, + }; + expect(legacyProviderType.matched(provider)).toBe(true); + }); + + it("should return false for ethers provider", () => { + const provider = new ethers.JsonRpcProvider("https://lodestar-sepoliarpc.chainsafe.io"); + expect(legacyProviderType.matched(provider)).toBe(false); + }); + + it("should return false for web3 provider", () => { + const provider = new Web3.providers.HttpProvider("https://lodestar-sepoliarpc.chainsafe.io"); + expect(legacyProviderType.matched(provider)).toBe(false); + }); + }); +}); diff --git a/packages/prover/test/unit/provider_types/web3js_provider_type.test.ts b/packages/prover/test/unit/provider_types/web3js_provider_type.test.ts new file mode 100644 index 000000000000..54da395bca25 --- /dev/null +++ b/packages/prover/test/unit/provider_types/web3js_provider_type.test.ts @@ -0,0 +1,16 @@ +import {describe, it, expect} from "vitest"; +import {ethers} from "ethers"; +import {Web3} from "web3"; +import web3jsProviderType from "../../../src/provider_types/web3_js_provider_type.js"; + +describe("matched", () => { + it("should return true if provider is web3.js provider", () => { + const provider = new Web3.providers.HttpProvider("https://lodestar-sepoliarpc.chainsafe.io"); + expect(web3jsProviderType.matched(provider)).toBe(true); + }); + + it("should return false if provider is not web3.js provider", () => { + const provider = new ethers.JsonRpcProvider("https://lodestar-sepoliarpc.chainsafe.io"); + expect(web3jsProviderType.matched(provider)).toBe(false); + }); +}); diff --git a/packages/prover/test/unit/utils/assertion.test.ts b/packages/prover/test/unit/utils/assertion.test.ts deleted file mode 100644 index 3ea712bab1f1..000000000000 --- a/packages/prover/test/unit/utils/assertion.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import {describe, it, expect} from "vitest"; -import {ethers} from "ethers"; -import {Web3} from "web3"; -import {isSendProvider, isWeb3jsProvider, isEthersProvider} from "../../../src/utils/assertion.js"; - -describe("utils/assertion", () => { - describe("isSendProvider", () => { - it("should return true if provider is SendProvider", () => { - const provider = { - send: (_payload: any, _cb: () => void) => { - // Do nothing; - }, - }; - expect(isSendProvider(provider)).toBe(true); - }); - - it("should return false for ethers provider", () => { - const provider = new ethers.JsonRpcProvider("https://lodestar-sepoliarpc.chainsafe.io"); - expect(isSendProvider(provider)).toBe(false); - }); - - it("should return false for web3 provider", () => { - const provider = new Web3.providers.HttpProvider("https://lodestar-sepoliarpc.chainsafe.io"); - expect(isSendProvider(provider)).toBe(false); - }); - }); - - describe("isWeb3jsProvider", () => { - it("should return true if provider is web3.js provider", () => { - const provider = new Web3.providers.HttpProvider("https://lodestar-sepoliarpc.chainsafe.io"); - expect(isWeb3jsProvider(provider)).toBe(true); - }); - - it("should return false if provider is not web3.js provider", () => { - const provider = new ethers.JsonRpcProvider("https://lodestar-sepoliarpc.chainsafe.io"); - expect(isWeb3jsProvider(provider)).toBe(false); - }); - }); - - describe("isEthersProvider", () => { - it("should return false if provider is not ethers provider", () => { - const provider = new Web3.providers.HttpProvider("https://lodestar-sepoliarpc.chainsafe.io"); - expect(isEthersProvider(provider)).toBe(false); - }); - - it("should return true if provider is ethers provider", () => { - const provider = new ethers.JsonRpcProvider("https://lodestar-sepoliarpc.chainsafe.io"); - expect(isEthersProvider(provider)).toBe(true); - }); - }); -}); diff --git a/packages/prover/test/unit/web3_provider.node.test.ts b/packages/prover/test/unit/web3_provider.node.test.ts index a83fbde4d858..d1f281175b56 100644 --- a/packages/prover/test/unit/web3_provider.node.test.ts +++ b/packages/prover/test/unit/web3_provider.node.test.ts @@ -1,8 +1,11 @@ import {describe, it, expect, afterEach, vi} from "vitest"; import {Web3} from "web3"; import {ethers} from "ethers"; -import {createVerifiedExecutionProvider, ProofProvider, LCTransport} from "@lodestar/prover/browser"; -import {ELRpc} from "../../src/utils/rpc.js"; +import {createVerifiedExecutionProvider} from "../../src/web3_provider.js"; +import {ELRpcProvider} from "../../src/utils/rpc_provider.js"; +import {ProofProvider} from "../../src/proof_provider/proof_provider.js"; +import {LCTransport, Web3ProviderType} from "../../src/interfaces.js"; +import {JsonRpcRequest, JsonRpcRequestOrBatch, JsonRpcResponse} from "../../src/types.js"; describe("web3_provider", () => { afterEach(() => { @@ -13,7 +16,7 @@ describe("web3_provider", () => { describe("web3", () => { it("should create a verified execution provider for the web3 provider", () => { // Don't invoke network in unit tests - vi.spyOn(ELRpc.prototype, "verifyCompatibility").mockResolvedValue(); + vi.spyOn(ELRpcProvider.prototype, "verifyCompatibility").mockResolvedValue(); const {provider, proofProvider} = createVerifiedExecutionProvider( new Web3.providers.HttpProvider("https://lodestar-sepoliarpc.chainsafe.io"), @@ -32,7 +35,7 @@ describe("web3_provider", () => { describe("ethers", () => { it("should create a verified execution provider for the ethers provider", () => { // Don't invoke network in unit tests - vi.spyOn(ELRpc.prototype, "verifyCompatibility").mockResolvedValue(); + vi.spyOn(ELRpcProvider.prototype, "verifyCompatibility").mockResolvedValue(); const {provider, proofProvider} = createVerifiedExecutionProvider( new ethers.JsonRpcProvider("https://lodestar-sepoliarpc.chainsafe.io"), @@ -47,5 +50,75 @@ describe("web3_provider", () => { expect(proofProvider).toBeInstanceOf(ProofProvider); }); }); + + describe("non-mutated provider", () => { + it("should create an ELRpc object from the web3 provider when non-mutate options provided", () => { + const {provider, proofProvider} = createVerifiedExecutionProvider( + new Web3.providers.HttpProvider("https://lodestar-sepoliarpc.chainsafe.io"), + { + transport: LCTransport.Rest, + urls: ["https://lodestar-sepolia.chainsafe.io"], + network: "sepolia", + mutateProvider: false, + } + ); + + expect(provider).toBeInstanceOf(ELRpcProvider); + expect(proofProvider).toBeInstanceOf(ProofProvider); + }); + }); + + describe("custom provider type", () => { + it("should be able to detect and use the custom provider", async () => { + type CustomProvider = {myrequest: (payload: JsonRpcRequest) => Promise}; + + const customProviderType: Web3ProviderType = { + name: "custom", + matched(provider): provider is CustomProvider { + return true; + }, + handler(provider) { + const handler = provider.myrequest.bind(provider); + + return function newHandler(payload: JsonRpcRequestOrBatch) { + if (Array.isArray(payload)) { + return Promise.all(payload.map((p) => handler(p))); + } + + return handler(payload); + }; + }, + mutateProvider(_provider): void { + // That's a deprecated behavior we don't want to test it + }, + }; + + const customProvider = { + myrequest: vi.fn().mockResolvedValue({result: "my-custom-result"}), + }; + + // Don't invoke network in unit tests + vi.spyOn(ELRpcProvider.prototype, "verifyCompatibility").mockResolvedValue(); + const {provider} = createVerifiedExecutionProvider(customProvider, { + transport: LCTransport.Rest, + urls: ["https://lodestar-sepolia.chainsafe.io"], + network: "sepolia", + mutateProvider: false, + providerTypes: [customProviderType], + }); + + const result = await provider.request("eth_getProof", ["nazar", [], ""]); + + expect(result).toEqual({result: "my-custom-result"}); + expect(customProvider.myrequest).toBeCalledTimes(1); + + expect(customProvider.myrequest).toHaveBeenCalledWith({ + jsonrpc: "2.0", + id: "1", + method: "eth_getProof", + params: ["nazar", [], ""], + }); + }); + }); }); }); diff --git a/packages/prover/test/unit/web3_provider_inspector.test.ts b/packages/prover/test/unit/web3_provider_inspector.test.ts new file mode 100644 index 000000000000..95df8a0d760e --- /dev/null +++ b/packages/prover/test/unit/web3_provider_inspector.test.ts @@ -0,0 +1,85 @@ +import {describe, it, beforeEach, expect} from "vitest"; +import {getEnvLogger} from "@lodestar/logger/env"; +import {LogLevel} from "@lodestar/logger"; +import {Web3ProviderInspector} from "../../src/web3_provider_inspector.js"; +import {AnyWeb3Provider, Web3ProviderType} from "../../src/interfaces.js"; +import web3JsProviderType from "../../src/provider_types/web3_js_provider_type.js"; + +describe("Web3ProviderInspector", () => { + let inspector: Web3ProviderInspector; + let customType: Web3ProviderType; + + beforeEach(() => { + customType = { + ...web3JsProviderType, + name: "custom", + }; + inspector = Web3ProviderInspector.initWithDefault({logger: getEnvLogger({level: LogLevel.debug})}); + }); + + it("should have pre-registered types", () => { + expect(inspector.getProviderTypes()).toHaveLength(4); + expect(inspector.getProviderTypes().map((t) => t.name)).toEqual(["web3js", "ethers", "eip1193", "legacy"]); + }); + + describe("register", () => { + it("should raise error if try to register pre-existing type", () => { + expect(() => inspector.register(web3JsProviderType)).toThrowError( + "Provider type 'web3js' is already registered." + ); + }); + + it("should register at max index if provided a large value", () => { + expect(() => inspector.register(customType, {index: 10})).not.toThrowError(); + expect(inspector.getProviderTypes().map((t) => t.name)).toEqual([ + "web3js", + "ethers", + "eip1193", + "legacy", + "custom", + ]); + }); + + it("should register at start index if provided a lower value", () => { + expect(() => inspector.register(customType, {index: -1})).not.toThrowError(); + expect(inspector.getProviderTypes().map((t) => t.name)).toEqual([ + "custom", + "web3js", + "ethers", + "eip1193", + "legacy", + ]); + }); + + it("should make space for existing index", () => { + expect(() => inspector.register(customType, {index: 2})).not.toThrowError(); + expect(inspector.getProviderTypes().map((t) => t.name)).toEqual([ + "web3js", + "ethers", + "custom", + "eip1193", + "legacy", + ]); + }); + }); + + describe("unregister", () => { + it("should unregister provider type given the name", () => { + inspector.unregister("ethers"); + expect(inspector.getProviderTypes().map((t) => t.name)).toEqual(["web3js", "eip1193", "legacy"]); + }); + + it("should raise error if given name is not registered", () => { + expect(() => inspector.unregister("custom")).toThrowError("Provider type 'custom' is not registered."); + }); + + it("should unregister provider type given the index", () => { + inspector.unregister(2); + expect(inspector.getProviderTypes().map((t) => t.name)).toEqual(["web3js", "ethers", "legacy"]); + }); + + it("should raise error if given index not available", () => { + expect(() => inspector.unregister(10)).toThrowError("Provider type at index '10' is not registered."); + }); + }); +});