Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(prover): support non-mutated verification provider in prover #6727

Merged
merged 12 commits into from
May 21, 2024
38 changes: 37 additions & 1 deletion packages/prover/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ 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 httpProvider = new Web3.providers.HttpProvider("https://lodestar-sepoliarpc.chainsafe.io")

const {provider, proofProvider} = createVerifiedExecutionProvider(
new Web3.providers.HttpProvider("https://lodestar-sepoliarpc.chainsafe.io"),
httpProvider,
{
transport: LCTransport.Rest,
urls: ["https://lodestar-sepolia.chainsafe.io"],
Expand All @@ -34,6 +36,34 @@ 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);

const address = "0xf97e180c050e5Ab072211Ad2C213Eb5AEE4DF134";
const balance = await web3.eth.getBalance(address, "latest");
console.log({balance, address});
```

You can also invoke the package as binary.

```bash
Expand All @@ -46,6 +76,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 on runtime to detect the correct provider type.
nazarhussain marked this conversation as resolved.
Show resolved Hide resolved

If you 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 array of your supported provider types. Your custom provider types will have higher priority on default provider types. Please see [existing provide types implementations](./src/provider_types/) to know how to implement yours own if needed.
nazarhussain marked this conversation as resolved.
Show resolved Hide resolved

## Supported Web3 Methods

✅ - Completed
Expand Down
77 changes: 34 additions & 43 deletions packages/prover/src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -30,50 +30,14 @@ export type ELRequestHandler<Params = unknown[], Response = unknown> = (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type ELRequestHandlerAny = ELRequestHandler<any, any>;

// Modern providers uses this structure e.g. Web3 4.x
export interface EIP1193Provider {
request: (payload: JsonRpcRequestOrBatch) => Promise<JsonRpcResponseOrBatch>;
}

export interface Web3jsProvider {
request: (payload: JsonRpcRequest) => Promise<JsonRpcResponse>;
}

// 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<unknown>): Promise<JsonRpcResponse>;
}

// Some legacy providers use this very old structure
export interface SendAsyncProvider {
sendAsync(payload: JsonRpcRequestOrBatch): Promise<JsonRpcResponseOrBatch>;
}

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<Params = unknown[]> = {
payload: JsonRpcRequest<Params>;
rpc: ELRpc;
rpc: ELRpcProvider;
proofProvider: ProofProvider;
logger: Logger;
};
Expand All @@ -96,4 +60,31 @@ export type RootProviderOptions = {
unverifiedWhitelist?: string[];
};

export type VerifiedExecutionInitOptions = LogOptions & ConsensusNodeOptions & NetworkOrConfig & RootProviderOptions;
export type ProviderTypeOptions<T extends boolean | undefined> = {
/**
* 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<AnyWeb3Provider>[];
/**
* 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;
jeluard marked this conversation as resolved.
Show resolved Hide resolved
};

export type VerifiedExecutionInitOptions<T extends boolean | undefined> = LogOptions &
ConsensusNodeOptions &
NetworkOrConfig &
RootProviderOptions &
ProviderTypeOptions<T>;

export type AnyWeb3Provider = object;
jeluard marked this conversation as resolved.
Show resolved Hide resolved

export interface Web3ProviderType<T extends AnyWeb3Provider> {
name: string;
matched: (provider: AnyWeb3Provider) => provider is T;
handler(provider: T): ELRpcProvider["handler"];
mutateProvider(provider: T, newHandler: ELRpcProvider["handler"]): void;
}
32 changes: 32 additions & 0 deletions packages/prover/src/provider_types/eip1193_provider_type.ts
Original file line number Diff line number Diff line change
@@ -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<JsonRpcResponseOrBatch>;
}
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<JsonRpcResponseOrBatch | undefined> => {
const response = await request(payload);
return response;
};
},
mutateProvider(provider, newHandler) {
Object.assign(provider, {
request: async function newRequest(payload: JsonRpcRequestOrBatch): Promise<JsonRpcResponseOrBatch | undefined> {
return newHandler(payload);
},
});
},
} as Web3ProviderType<EIP1193Provider>;
44 changes: 44 additions & 0 deletions packages/prover/src/provider_types/ethers_provider_type.ts
Original file line number Diff line number Diff line change
@@ -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<unknown>): Promise<JsonRpcResponse>;
}
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<JsonRpcResponseOrBatch | undefined> => {
// 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<unknown>): Promise<JsonRpcResponseOrBatch | undefined> {
return newHandler({jsonrpc: "2.0", id: 0, method, params});
},
});
},
} as Web3ProviderType<EthersProvider>;
123 changes: 123 additions & 0 deletions packages/prover/src/provider_types/legacy_provider_type.ts
Original file line number Diff line number Diff line change
@@ -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<JsonRpcResponseOrBatch>;
}

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<JsonRpcResponseOrBatch | undefined> {
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<JsonRpcResponseOrBatch | undefined> {
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<JsonRpcResponseOrBatch | undefined> {
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<LegacyProvider>;

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"
);
}
35 changes: 35 additions & 0 deletions packages/prover/src/provider_types/web3_js_provider_type.ts
Original file line number Diff line number Diff line change
@@ -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<JsonRpcResponse>;
}

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<JsonRpcResponseOrBatch | undefined> => {
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<JsonRpcResponseOrBatch | undefined> {
return newHandler(payload);
},
});
},
} as Web3ProviderType<Web3jsProvider>;
Loading
Loading