From 08f96cd74d0a6281458845d27addcd5d2ce83dfd Mon Sep 17 00:00:00 2001 From: marie-fourier Date: Thu, 11 May 2023 13:40:43 +0500 Subject: [PATCH 1/3] feat: redirect eth rpc calls --- package.json | 4 +- packages/api/src/app.ts | 21 +++++++++- packages/api/src/constants.ts | 39 +++++++++++++++++++ packages/api/src/modules/index.ts | 1 + packages/api/src/modules/redirect.ts | 29 ++++++++++++++ packages/cli/src/cmds/start/handler.ts | 3 +- packages/cli/src/cmds/start/index.ts | 8 ++++ packages/executor/src/modules/eth.ts | 29 ++++++++++---- packages/executor/src/modules/web3.ts | 2 +- .../executor/src/services/UserOpValidation.ts | 24 ++++++++++++ packages/types/src/api/interfaces.ts | 5 ++- yarn.lock | 4 +- 12 files changed, 151 insertions(+), 18 deletions(-) create mode 100644 packages/api/src/modules/redirect.ts diff --git a/package.json b/package.json index ef126507..17aac6eb 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,14 @@ { "name": "root", "private": true, - "version": "0.0.2", + "version": "0.0.3", "engines": { "node": ">=12.9.0" }, "scripts": { "clean": "rm -rf ./packages/*/lib ./packages/*/*.tsbuildinfo", "bootstrap": "lerna bootstrap & lerna link", - "build": "yarn workspace db run build & lerna run build", + "build": "yarn workspace types run build && yarn workspace db run build & lerna run build", "lint": "eslint --color --ext .ts packages/*/src/", "fix-lint": "eslint --ext .ts --fix packages/*/src/", "test:unit": "lerna run test:unit --no-bail --concurrency 1", diff --git a/packages/api/src/app.ts b/packages/api/src/app.ts index ab27e8d5..c3a6ae77 100644 --- a/packages/api/src/app.ts +++ b/packages/api/src/app.ts @@ -6,8 +6,12 @@ import RpcError from "types/lib/api/errors/rpc-error"; import * as RpcErrorCodes from "types/lib/api/errors/rpc-error-codes"; import { FastifyInstance, RouteHandler } from "fastify"; import logger from "./logger"; -import { BundlerRPCMethods, CustomRPCMethods } from "./constants"; -import { EthAPI, DebugAPI, Web3API } from "./modules"; +import { + BundlerRPCMethods, + CustomRPCMethods, + RedirectedRPCMethods, +} from "./constants"; +import { EthAPI, DebugAPI, Web3API, RedirectAPI } from "./modules"; import { deepHexlify } from "./utils"; export interface RpcHandlerOptions { @@ -21,6 +25,7 @@ export interface EtherspotBundlerOptions { config: Config; db: IDbController; testingMode: boolean; + redirectRpc: boolean; } export interface RelayerAPI { @@ -37,12 +42,14 @@ export class ApiApp { private relayers: RelayerAPI[] = []; private testingMode = false; + private redirectRpc = false; constructor(options: EtherspotBundlerOptions) { this.server = options.server; this.config = options.config; this.db = options.db; this.testingMode = options.testingMode; + this.redirectRpc = options.redirectRpc; this.setupRoutes(); } @@ -74,6 +81,7 @@ export class ApiApp { const ethApi = new EthAPI(relayer.eth); const debugApi = new DebugAPI(relayer.debug); const web3Api = new Web3API(relayer.web3); + const redirectApi = new RedirectAPI(network, this.config); this.relayers.push({ relayer, @@ -120,6 +128,15 @@ export class ApiApp { } } + if (this.redirectRpc && method in RedirectedRPCMethods) { + const body = await redirectApi.redirect(method, params); + return res.status(200).send({ + jsonrpc, + id, + ...body, + }); + } + if (result === undefined) { switch (method) { case BundlerRPCMethods.eth_supportedEntryPoints: diff --git a/packages/api/src/constants.ts b/packages/api/src/constants.ts index c5e8c7ca..8ce875e7 100644 --- a/packages/api/src/constants.ts +++ b/packages/api/src/constants.ts @@ -18,3 +18,42 @@ export const BundlerRPCMethods = { debug_bundler_setBundleInterval: "debug_bundler_setBundleInterval", debug_bundler_sendBundleNow: "debug_bundler_sendBundleNow", }; + +export const RedirectedRPCMethods = { + web3_sha3: "web3_sha3", + net_version: "net_version", + net_listening: "net_listening", + net_peerCount: "net_peerCount", + eth_protocolVersion: "eth_protocolVersion", + eth_gasPrice: "eth_gasPrice", + eth_blockNumber: "eth_blockNumber", + eth_getBalance: "eth_getBalance", + eth_getStorageAt: "eth_getStorageAt", + eth_getTransactionCount: "eth_getTransactionCount", + eth_getBlockTransactionCountByHash: "eth_getBlockTransactionCountByHash", + eth_getBlockTransactionCountByNumber: "eth_getBlockTransactionCountByNumber", + eth_getUncleCountByBlockHash: "eth_getUncleCountByBlockHash", + eth_getUncleCountByBlockNumber: "eth_getUncleCountByBlockNumber", + eth_getCode: "eth_getCode", + eth_sign: "eth_sign", + eth_call: "eth_call", + eth_estimateGas: "eth_estimateGas", + eth_getBlockByHash: "eth_getBlockByHash", + eth_getBlockByNumber: "eth_getBlockByNumber", + eth_getTransactionByHash: "eth_getTransactionByHash", + eth_getTransactionByBlockHashAndIndex: + "eth_getTransactionByBlockHashAndIndex", + eth_getTransactionByBlockNumberAndIndex: + "eth_getTransactionByBlockNumberAndIndex", + eth_getTransactionReceipt: "eth_getTransactionReceipt", + eth_getUncleByBlockHashAndIndex: "eth_getUncleByBlockHashAndIndex", + eth_getUncleByBlockNumberAndIndex: "eth_getUncleByBlockNumberAndIndex", + eth_newFilter: "eth_newFilter", + eth_newBlockFilter: "eth_newBlockFilter", + eth_newPendingTransactionFilter: "eth_newPendingTransactionFilter", + eth_uninstallFilter: "eth_uninstallFilter", + eth_getFilterChanges: "eth_getFilterChanges", + eth_getFilterLogs: "eth_getFilterLogs", + eth_getLogs: "eth_getLogs", + eth_maxPriorityFeePerGas: "eth_maxPriorityFeePerGas", +}; diff --git a/packages/api/src/modules/index.ts b/packages/api/src/modules/index.ts index c0fbfcac..c1426aa7 100644 --- a/packages/api/src/modules/index.ts +++ b/packages/api/src/modules/index.ts @@ -1,3 +1,4 @@ export * from "./debug"; export * from "./web3"; export * from "./eth"; +export * from "./redirect"; diff --git a/packages/api/src/modules/redirect.ts b/packages/api/src/modules/redirect.ts new file mode 100644 index 00000000..63979539 --- /dev/null +++ b/packages/api/src/modules/redirect.ts @@ -0,0 +1,29 @@ +import { providers } from "ethers"; +import { Config } from "executor/lib/config"; +import { NetworkName } from "types/src"; + +export class RedirectAPI { + private provider: providers.JsonRpcProvider; + + constructor(private network: NetworkName, private config: Config) { + this.provider = this.config.getNetworkProvider( + this.network + ) as providers.JsonRpcProvider; + } + + async redirect(method: string, params: any[]): Promise { + return await this.provider + .send(method, params) + .then((result) => ({ result })) + .catch((err: any) => { + if (err.body) { + try { + const body = JSON.parse(err.body); + return body; + // eslint-disable-next-line no-empty + } catch (err) {} + } + throw err; + }); + } +} diff --git a/packages/cli/src/cmds/start/handler.ts b/packages/cli/src/cmds/start/handler.ts index e2dabf99..20321c83 100644 --- a/packages/cli/src/cmds/start/handler.ts +++ b/packages/cli/src/cmds/start/handler.ts @@ -18,7 +18,7 @@ import { IBundlerArgs } from "./index"; export async function bundlerHandler( args: IBundlerArgs & IGlobalArgs ): Promise { - const { dataDir, networksFile, testingMode, unsafeMode } = args; + const { dataDir, networksFile, testingMode, unsafeMode, redirectRpc } = args; let config: Config; try { @@ -65,6 +65,7 @@ export async function bundlerHandler( config: config, db, testingMode, + redirectRpc, }); await server.listen(); diff --git a/packages/cli/src/cmds/start/index.ts b/packages/cli/src/cmds/start/index.ts index 6dd16507..2e111264 100644 --- a/packages/cli/src/cmds/start/index.ts +++ b/packages/cli/src/cmds/start/index.ts @@ -5,6 +5,7 @@ import { bundlerHandler } from "./handler"; export interface IBundlerArgs { testingMode: boolean; unsafeMode: boolean; + redirectRpc: boolean; } export const bundlerOptions = { @@ -20,6 +21,13 @@ export const bundlerOptions = { default: false, choices: [true, false], }, + redirectRpc: { + description: + "Redirect ETH-related rpc calls to the underlying execution client", + type: "boolean", + default: false, + choices: [true, false], + }, }; export const start: ICliCommand = { diff --git a/packages/executor/src/modules/eth.ts b/packages/executor/src/modules/eth.ts index 72e5d6bb..29255ab6 100644 --- a/packages/executor/src/modules/eth.ts +++ b/packages/executor/src/modules/eth.ts @@ -86,11 +86,18 @@ export class Eth { preVerificationGas: 0, verificationGasLimit: 10e6, }; + const preVerificationGas = this.calcPreVerificationGas(userOp); + userOpComplemented.preVerificationGas = preVerificationGas; + const { returnInfo } = - await this.userOpValidationService.simulateValidation( + await this.userOpValidationService.validateForEstimation( userOpComplemented, entryPoint ); + + // eslint-disable-next-line prefer-const + let { preOpGas, validAfter, validUntil } = returnInfo; + const callGasLimit = await this.provider .estimateGas({ from: entryPoint, @@ -103,17 +110,22 @@ export class Eth { err.message.match(/reason="(.*?)"/)?.at(1) ?? "execution reverted"; throw new RpcError(message, RpcErrorCodes.EXECUTION_REVERTED); }); - const preVerificationGas = this.calcPreVerificationGas(userOp); - const verificationGas = BigNumber.from(returnInfo.preOpGas).toNumber(); - let deadline: any = undefined; - if (returnInfo.deadline) { - deadline = BigNumber.from(returnInfo.deadline); + // const preVerificationGas = this.calcPreVerificationGas(userOp); + const verificationGas = BigNumber.from(preOpGas).toNumber(); + validAfter = BigNumber.from(validAfter); + validUntil = BigNumber.from(validUntil); + if (validUntil === BigNumber.from(0)) { + validUntil = undefined; + } + if (validAfter === BigNumber.from(0)) { + validAfter = undefined; } return { preVerificationGas, verificationGas, + validAfter, + validUntil, callGasLimit, - deadline: deadline, }; } @@ -292,6 +304,7 @@ export class Eth { } as any; const packed = arrayify(packUserOp(p, false)); + const lengthInWord = (packed.length + 31) / 32; const callDataCost = packed .map((x) => (x === 0 ? ov.zeroByte : ov.nonZeroByte)) .reduce((sum, x) => sum + x); @@ -299,7 +312,7 @@ export class Eth { callDataCost + ov.fixed / ov.bundleSize + ov.perUserOp + - ov.perUserOpWord * packed.length + ov.perUserOpWord * lengthInWord ); return ret; } diff --git a/packages/executor/src/modules/web3.ts b/packages/executor/src/modules/web3.ts index 1bba0184..6e86c07b 100644 --- a/packages/executor/src/modules/web3.ts +++ b/packages/executor/src/modules/web3.ts @@ -3,6 +3,6 @@ export class Web3 { constructor(private config: Config) {} clientVersion(): string { - return `skandha/${this.config.unsafeMode ? "unsafe/" : ""}0.0.1`; // TODO: get version based on commit hash + return `skandha/${this.config.unsafeMode ? "unsafe/" : ""}0.0.3`; // TODO: get version based on commit hash } } diff --git a/packages/executor/src/services/UserOpValidation.ts b/packages/executor/src/services/UserOpValidation.ts index 218b2d85..3b660464 100644 --- a/packages/executor/src/services/UserOpValidation.ts +++ b/packages/executor/src/services/UserOpValidation.ts @@ -60,6 +60,30 @@ export class UserOpValidationService { ); } + async validateForEstimation( + userOp: UserOperationStruct, + entryPoint: string + ): Promise { + const entryPointContract = EntryPoint__factory.connect( + entryPoint, + this.provider + ); + const errorResult = await entryPointContract.callStatic + .simulateValidation(userOp, { gasLimit: 10e6 }) + .catch((e: any) => e); + if (errorResult.errorName === "FailedOp") { + throw new RpcError( + errorResult.errorArgs.at(-1), + RpcErrorCodes.VALIDATION_FAILED + ); + } + if (errorResult.errorName !== "ValidationResult") { + throw errorResult; + } + + return errorResult.errorArgs; + } + async simulateValidation( userOp: UserOperationStruct, entryPoint: string, diff --git a/packages/types/src/api/interfaces.ts b/packages/types/src/api/interfaces.ts index 71c39d97..5fbca3b4 100644 --- a/packages/types/src/api/interfaces.ts +++ b/packages/types/src/api/interfaces.ts @@ -3,9 +3,10 @@ import { UserOperationStruct } from "../executor/contracts/EntryPoint"; export type EstimatedUserOperationGas = { preVerificationGas: BigNumberish; - callGasLimit: BigNumberish; verificationGas: BigNumberish; - deadline?: BigNumberish; + callGasLimit: BigNumberish; + validAfter?: BigNumberish; + validUntil?: BigNumberish; }; export type UserOperationByHashResponse = { diff --git a/yarn.lock b/yarn.lock index 8481716b..c9f453a3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -691,7 +691,7 @@ ajv-formats "^2.1.1" fast-uri "^2.0.0" -"@fastify/cors@^8.2.1": +"@fastify/cors@8.2.1": version "8.2.1" resolved "https://registry.yarnpkg.com/@fastify/cors/-/cors-8.2.1.tgz#dd348162bcbfb87dff4b492e2bef32d41244006a" integrity sha512-2H2MrDD3ea7g707g1CNNLWb9/tYbmw7HS+MK2SDcgjxwzbOFR93JortelTIO8DBFsZqFtEpKNxiZfSyrGgYcbw== @@ -2146,7 +2146,7 @@ dependencies: "@types/express" "*" -"@types/connect@*", "@types/connect@^3.4.35": +"@types/connect@*", "@types/connect@3.4.35": version "3.4.35" resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1" integrity sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ== From 56d6b854e3e0e7da297c1bad4398ef6b7ee170d1 Mon Sep 17 00:00:00 2001 From: marie-fourier Date: Thu, 11 May 2023 13:50:33 +0500 Subject: [PATCH 2/3] update readme --- README.md | 35 ++++++----------------------------- 1 file changed, 6 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index a526597c..2ee0b541 100644 --- a/README.md +++ b/README.md @@ -21,17 +21,7 @@ 2. build `yarn build && yarn bootstrap` 3. `cp config.json.default config.json` 4. edit `config.json` -5. (optional) run local geth-node -```bash -docker run --rm -ti --name geth -p 8545:8545 ethereum/client-go:v1.10.26 \ - --miner.gaslimit 12000000 \ - --http --http.api personal,eth,net,web3,debug \ - --http.vhosts '*,localhost,host.docker.internal' --http.addr "0.0.0.0" \ - --ignore-legacy-receipts --allow-insecure-unlock --rpc.allow-unprotected-txs \ - --dev \ - --nodiscover --maxpeers 0 \ - --networkid 1337 -``` +5. (optional) run local geth-node from `test/geth-dev` 6. run `./skandha` 7. Skandha will run for all chains available in `config.json` 8. Networks will be available at `http://localhost:14337/{chainId}/` (e.g. for dev `http://localhost:14337/1337/`) @@ -44,26 +34,13 @@ docker run --rm -ti --name geth -p 8545:8545 ethereum/client-go:v1.10.26 \ 4. `docker run --mount type=bind,source="$(pwd)"/config.json,target=/usr/app/config.json,readonly -dp 14337:14337 etherspot/skandha start` -### RPC Methods Checklist - -- [x] eth_chainId -- [x] eth_supportedEntryPoints -- [x] eth_sendUserOperation -- [x] eth_estimateUserOperationGas -- [x] eth_getUserOperationReceipt -- [x] eth_getUserOperationByHash -- [x] web3_clientVersion -- [x] debug_bundler_clearState -- [x] debug_bundler_dumpMempool -- [x] debug_bundler_setReputation -- [x] debug_bundler_dumpReputation -- [x] debug_bundler_setBundlingMode -- [x] debug_bundler_setBundleInterval -- [x] debug_bundler_sendBundleNow - ### Additional features -- [x] Unsafe mode - bypass opcode & stake validation (Enabled by --unsafeMode) +- [x] Unsafe mode - bypass opcode & stake validation +- [x] Redirect RPC - Redirect ETH rpc calls to the underlying execution client. This is needed if you use UserOp.js +### CLI Options +- `--unsafeMode` - enables unsafeMode +- `--redirectRpc` - enables redirecting eth rpc calls ### Relayer Configuration From 343054c8a8f7eb29b2488160ffc6cd7c9834ac3b Mon Sep 17 00:00:00 2001 From: marie-fourier Date: Thu, 11 May 2023 13:53:43 +0500 Subject: [PATCH 3/3] update readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2ee0b541..95ccc056 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ 4. `docker run --mount type=bind,source="$(pwd)"/config.json,target=/usr/app/config.json,readonly -dp 14337:14337 etherspot/skandha start` -### Additional features +## Additional features - [x] Unsafe mode - bypass opcode & stake validation - [x] Redirect RPC - Redirect ETH rpc calls to the underlying execution client. This is needed if you use UserOp.js @@ -42,7 +42,7 @@ - `--unsafeMode` - enables unsafeMode - `--redirectRpc` - enables redirecting eth rpc calls -### Relayer Configuration +## Relayer Configuration #### Config.json