From 83bf607d1d7a0b18d4f58f1ecb7c9c8480ff974a Mon Sep 17 00:00:00 2001 From: marie-fourier Date: Thu, 14 Sep 2023 19:56:15 +0500 Subject: [PATCH] retrofit new features --- packages/cli/package.json | 1 + packages/cli/src/cmds/node/handler.ts | 2 + packages/cli/src/cmds/standalone/handler.ts | 2 + packages/cli/src/cmds/start/handler.ts | 86 --- .../executor/src/services/BundlingService.ts | 1 - .../executor/src/services/UserOpValidation.ts | 596 ------------------ 6 files changed, 5 insertions(+), 683 deletions(-) delete mode 100644 packages/cli/src/cmds/start/handler.ts delete mode 100644 packages/executor/src/services/UserOpValidation.ts diff --git a/packages/cli/package.json b/packages/cli/package.json index db9fe8c8..32f5756a 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -34,6 +34,7 @@ "api": "^0.0.44", "db": "^0.0.44", "executor": "^0.0.44", + "node": "*", "find-up": "5.0.0", "got": "12.5.3", "js-yaml": "4.1.0", diff --git a/packages/cli/src/cmds/node/handler.ts b/packages/cli/src/cmds/node/handler.ts index 06550215..f9b25468 100644 --- a/packages/cli/src/cmds/node/handler.ts +++ b/packages/cli/src/cmds/node/handler.ts @@ -31,6 +31,7 @@ export async function nodeHandler(args: IGlobalArgs): Promise { networks: configOptions.networks, testingMode: params.testingMode, unsafeMode: params.unsafeMode, + redirectRpc: params.redirectRpc, }); } catch (err) { logger.info("Config file not found. Proceeding with env vars..."); @@ -38,6 +39,7 @@ export async function nodeHandler(args: IGlobalArgs): Promise { networks: {}, testingMode: false, unsafeMode: false, + redirectRpc: params.redirectRpc, }); } diff --git a/packages/cli/src/cmds/standalone/handler.ts b/packages/cli/src/cmds/standalone/handler.ts index 4ed8fae1..72cd33dc 100644 --- a/packages/cli/src/cmds/standalone/handler.ts +++ b/packages/cli/src/cmds/standalone/handler.ts @@ -38,6 +38,7 @@ export async function bundlerHandler( networks: configOptions.networks, testingMode, unsafeMode, + redirectRpc, }); } catch (err) { logger.debug("Config file not found. Proceeding with env vars..."); @@ -45,6 +46,7 @@ export async function bundlerHandler( networks: {}, testingMode, unsafeMode, + redirectRpc, }); } diff --git a/packages/cli/src/cmds/start/handler.ts b/packages/cli/src/cmds/start/handler.ts deleted file mode 100644 index dc263e37..00000000 --- a/packages/cli/src/cmds/start/handler.ts +++ /dev/null @@ -1,86 +0,0 @@ -/* eslint-disable no-console */ -import path, { resolve } from "node:path"; -import { Server } from "api/lib/server"; -import { ApiApp } from "api/lib/app"; -import logger from "api/lib/logger"; -import { Config } from "executor/lib/config"; -import { - Namespace, - getNamespaceByValue, - RocksDbController, - LocalDbController, -} from "db/lib"; -import { ConfigOptions } from "executor/lib/interfaces"; -import { IDbController } from "types/lib"; -import { mkdir, readFile } from "../../util"; -import { IGlobalArgs } from "../../options"; -import { IBundlerArgs } from "./index"; - -export async function bundlerHandler( - args: IBundlerArgs & IGlobalArgs -): Promise { - const { dataDir, networksFile, testingMode, unsafeMode, redirectRpc } = args; - - let config: Config; - try { - const configPath = path.resolve(dataDir, "..", networksFile); - const configOptions = readFile(configPath) as ConfigOptions; - config = new Config({ - networks: configOptions.networks, - testingMode, - unsafeMode, - redirectRpc, - }); - } catch (err) { - logger.debug("Config file not found. Proceeding with env vars..."); - config = new Config({ - networks: {}, - testingMode, - unsafeMode, - redirectRpc, - }); - } - - if (unsafeMode) { - logger.warn( - "WARNING: Running in unsafe mode, skips opcode check and stake check" - ); - } - if (redirectRpc) { - logger.warn( - "WARNING: RPC redirecting is enabled, redirects RPC whitelisted calls to RPC" - ); - } - - let db: IDbController; - - if (testingMode) { - db = new LocalDbController(getNamespaceByValue(Namespace.userOps)); - } else { - const dbPath = resolve(dataDir); - mkdir(dbPath); - - db = new RocksDbController( - resolve(dataDir), - getNamespaceByValue(Namespace.userOps) - ); - await db.start(); - } - - const server = await Server.init({ - enableRequestLogging: args["api.enableRequestLogging"], - port: args["api.port"], - host: args["api.address"], - cors: args["api.cors"], - }); - - new ApiApp({ - server: server.application, - config: config, - db, - testingMode, - redirectRpc, - }); - - await server.listen(); -} diff --git a/packages/executor/src/services/BundlingService.ts b/packages/executor/src/services/BundlingService.ts index 768c848b..19287ead 100644 --- a/packages/executor/src/services/BundlingService.ts +++ b/packages/executor/src/services/BundlingService.ts @@ -320,7 +320,6 @@ export class BundlingService { await this.userOpValidationService.simulateValidation( entry.userOp, entry.entryPoint, - false /* not estimating gas */, entry.hash ); } catch (e: any) { diff --git a/packages/executor/src/services/UserOpValidation.ts b/packages/executor/src/services/UserOpValidation.ts deleted file mode 100644 index d2ed2bc6..00000000 --- a/packages/executor/src/services/UserOpValidation.ts +++ /dev/null @@ -1,596 +0,0 @@ -import { BigNumberish, BytesLike, ethers, providers } from "ethers"; -import { Interface, getAddress, hexZeroPad } from "ethers/lib/utils"; -import * as RpcErrorCodes from "types/lib/api/errors/rpc-error-codes"; -import RpcError from "types/lib/api/errors/rpc-error"; -import { EntryPoint__factory } from "types/lib/executor/contracts/factories"; -import { - IAccount__factory, - IAggregatedAccount__factory, - IAggregator__factory, -} from "types/lib/executor/contracts"; -import { IPaymaster__factory } from "types/lib/executor/contracts/factories/IPaymaster__factory"; -import { - EntryPoint, - UserOperationStruct, -} from "types/lib/executor/contracts/EntryPoint"; -import { BannedContracts } from "params/lib"; -import { NetworkName } from "types/lib"; -import { AddressZero, BytesZero } from "params/lib"; -import { WhitelistedEntities } from "params/lib/whitelisted-entities"; -import { getAddr } from "../utils"; -import { Logger, NetworkConfig, TracerCall, TracerResult } from "../interfaces"; -import { Config } from "../config"; -import { ReputationService } from "./ReputationService"; -import { GethTracer } from "./GethTracer"; - -export interface ReferencedCodeHashes { - // addresses accessed during this user operation - addresses: string[]; - // keccak over the code of all referenced addresses - hash: string; -} - -export interface UserOpValidationResult { - returnInfo: { - preOpGas: BigNumberish; - prefund: BigNumberish; - deadline: number; - sigFailed: boolean; - }; - - senderInfo: StakeInfo; - factoryInfo: StakeInfo | null; - paymasterInfo: StakeInfo | null; - aggregatorInfo: StakeInfo | null; - referencedContracts?: ReferencedCodeHashes; -} - -export interface StakeInfo { - addr: string; - stake: BigNumberish; - unstakeDelaySec: BigNumberish; -} - -export class UserOpValidationService { - private gethTracer: GethTracer; - private networkConfig: NetworkConfig; - - constructor( - private provider: providers.Provider, - private reputationService: ReputationService, - private network: NetworkName, - private config: Config, - private logger: Logger - ) { - this.gethTracer = new GethTracer( - this.provider as providers.JsonRpcProvider - ); - const networkConfig = config.getNetworkConfig(network); - if (!networkConfig) { - throw new Error(`No config found for ${network}`); - } - this.networkConfig = networkConfig; - } - - async validateForEstimation( - userOp: UserOperationStruct, - entryPoint: string - ): Promise { - const entryPointContract = EntryPoint__factory.connect( - entryPoint, - this.provider - ); - - const tx = { - to: entryPoint, - data: entryPointContract.interface.encodeFunctionData( - "simulateHandleOp", - [userOp, AddressZero, BytesZero] - ), - }; - - const errorResult = await entryPointContract.callStatic - .simulateHandleOp(userOp, AddressZero, BytesZero) - .catch((e: any) => this.nethermindErrorHandler(entryPointContract, e)); - - if (errorResult.errorName === "FailedOp") { - this.logger.debug(tx); - throw new RpcError( - errorResult.errorArgs.at(-1), - RpcErrorCodes.VALIDATION_FAILED - ); - } - - if (errorResult.errorName !== "ExecutionResult") { - this.logger.debug(tx); - throw errorResult; - } - - return errorResult.errorArgs; - } - - async simulateValidation( - userOp: UserOperationStruct, - entryPoint: string, - estimatingGas = false, - codehash?: string - ): Promise { - if (this.config.unsafeMode) { - return this.simulateUnsafeValidation(userOp, entryPoint); - } - return this.simulateSafeValidation( - userOp, - entryPoint, - estimatingGas, - codehash - ); - } - - async simulateUnsafeValidation( - userOp: UserOperationStruct, - entryPoint: string - ): Promise { - const { validationGasLimit } = this.networkConfig; - const entryPointContract = EntryPoint__factory.connect( - entryPoint, - this.provider - ); - const errorResult = await entryPointContract.callStatic - .simulateValidation(userOp, { - gasLimit: validationGasLimit, - }) - .catch((e: any) => this.nethermindErrorHandler(entryPointContract, e)); - return this.parseErrorResult(userOp, errorResult); - } - - async simulateSafeValidation( - userOp: UserOperationStruct, - entryPoint: string, - estimatingGas: boolean, - codehash?: string - ): Promise { - const { validationGasLimit } = this.networkConfig; - - entryPoint = entryPoint.toLowerCase(); - const entryPointContract = EntryPoint__factory.connect( - entryPoint, - this.provider - ); - const tx = { - to: entryPoint, - data: entryPointContract.interface.encodeFunctionData( - "simulateValidation", - [userOp] - ), - gasLimit: 6e6, - }; - const traceCall: TracerResult = await this.gethTracer.debug_traceCall(tx); - if (traceCall == null || traceCall.calls == undefined) { - throw new Error( - "Could not validate transaction. Tracing is not available" - ); - } - - // TODO: restrict calling EntryPoint methods except fallback and depositTo if depth > 2 - - const lastCall = traceCall.calls.at(-1); - if (!lastCall || lastCall.type !== "REVERT") { - throw new Error("Invalid response. simulateCall must revert"); - } - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const errorResult = await entryPointContract.callStatic - .simulateValidation(userOp, { gasLimit: validationGasLimit }) - .catch((e: any) => e); - // const errorResult = entryPointContract.interface.parseError(lastCall.data!); - const validationResult = this.parseErrorResult(userOp, errorResult); - const stakeInfoEntities = { - factory: validationResult.factoryInfo, - account: validationResult.senderInfo, - paymaster: validationResult.paymasterInfo, - }; - - // OPCODE VALIDATION - // eslint-disable-next-line prefer-const - for (let [address, trace] of Object.entries(traceCall.trace)) { - address = address.toLowerCase(); - const title = this.numberToEntityTitle( - trace?.number - ) as keyof typeof stakeInfoEntities; - // OPCODE RULES - const violation = trace.violation || {}; - - // Skip whitelisted entities - const whitelist = WhitelistedEntities[title]; - if ( - whitelist && - whitelist[this.network] && - whitelist[this.network]!.some((addr) => addr === getAddress(address)) - ) { - this.logger.debug( - "Paymaster is in whitelist. Skipping opcode validation..." - ); - continue; - } - - for (const [opcode, count] of Object.entries(violation)) { - if (opcode === "CREATE2" && Number(count) < 2 && title === "factory") { - continue; - } - throw new RpcError( - `${title} uses banned opcode: ${opcode}`, - RpcErrorCodes.INVALID_OPCODE - ); - } - const value = trace.value ?? 0; - if (value > 0 && entryPoint !== address) { - throw new RpcError( - "May not may CALL with value", - RpcErrorCodes.INVALID_OPCODE - ); - } - } - - // SLOT & STAKE VALIDATION - // eslint-disable-next-line prefer-const - for (let [address, trace] of Object.entries(traceCall.trace)) { - address = address.toLowerCase(); - const title = this.numberToEntityTitle( - trace?.number - ) as keyof typeof stakeInfoEntities; - const entity = stakeInfoEntities[title]; - - const isSlotAssociatedWith = (slot: string, addr: string): boolean => { - if (!trace.keccak) { - return false; - } - const addrPadded = hexZeroPad(addr.toLowerCase(), 32); - const keccak = Object.keys(trace.keccak).find((k) => - k.startsWith(addrPadded) - ); - if (!keccak) { - return false; - } - const kSlot = ethers.BigNumber.from(`0x${trace.keccak[keccak]}`); - const bnSlot = ethers.BigNumber.from(`0x${slot}`); - return bnSlot.gte(kSlot) && bnSlot.lt(kSlot.add(128)); - }; - const { paymaster, account } = stakeInfoEntities; - if (address === entryPoint) { - continue; - } - if (address === account.addr.toLowerCase()) { - continue; - } - - let validationFailed = false; - if (address === paymaster?.addr.toLowerCase()) { - if (trace.storage && Object.values(trace.storage).length) { - validationFailed = true; - } - } - - if (!trace.storage) { - continue; - } - - if (!validationFailed) { - for (const slot of Object.keys(trace.storage)) { - if (isSlotAssociatedWith(slot, account.addr)) { - validationFailed = userOp.initCode.length > 2; - } else if (isSlotAssociatedWith(slot, entity?.addr ?? "")) { - validationFailed = true; - } else if (address.toLowerCase() === entity?.addr!.toLowerCase()) { - validationFailed = true; - } else { - throw new RpcError( - `unstaked ${title} entity ${address} accessed slot`, - RpcErrorCodes.INVALID_OPCODE, - { - [title]: address, - } - ); - } - } - } - - if (validationFailed) { - const unstaked = - !entity || (await this.reputationService.checkStake(entity)); - if (unstaked != null) { - throw new RpcError( - `unstaked ${title} entity ${address} accessed slot`, - RpcErrorCodes.INVALID_OPCODE, - { - [title]: address, - } - ); - } - } - } - - const parsedCalls = this.parseCalls(traceCall.calls); - - const { paymaster } = stakeInfoEntities; - for (const call of parsedCalls) { - if (!call.to) { - continue; - } - - if (call.to.toLowerCase() === paymaster?.addr.toLowerCase()) { - // unstaked paymaster must not return context - if ( - call.method === "validatePaymasterUserOp" && - call.return?.context !== "0x" - ) { - const checkStake = await this.reputationService.checkStake( - paymaster! - ); - if (checkStake) { - throw new RpcError( - "unstaked paymaster must not return context", - RpcErrorCodes.INVALID_OPCODE, - { - paymaster: paymaster!.addr, - } - ); - } - } - } - - if (this.isContractBanned(this.network, call.to)) { - throw new RpcError( - `access to restricted precompiled contract ${call.to}`, - RpcErrorCodes.VALIDATION_FAILED - ); - } - } - - if (validationResult.returnInfo.sigFailed) { - throw new RpcError( - "Invalid UserOp signature or paymaster signature", - RpcErrorCodes.INVALID_SIGNATURE - ); - } - - if ( - validationResult.returnInfo.deadline != null || - validationResult.returnInfo.deadline + 30 >= Date.now() / 1000 - ) { - throw new RpcError("expires too soon", RpcErrorCodes.USEROP_EXPIRED); - } - - if (validationResult.aggregatorInfo) { - const stakeErr = await this.reputationService.checkStake( - validationResult.aggregatorInfo - ); - if (stakeErr) { - throw new RpcError(stakeErr, RpcErrorCodes.VALIDATION_FAILED); - } - } - - const prestateTrace = await this.gethTracer.debug_traceCallPrestate(tx); - const addresses = Object.keys(prestateTrace) - .sort() - .filter((addr) => traceCall!.trace[addr]); - const code = addresses.map((addr) => prestateTrace[addr]?.code).join(";"); - const hash = ethers.utils.keccak256( - ethers.utils.hexlify(ethers.utils.toUtf8Bytes(code)) - ); - - if (codehash && codehash !== hash) { - throw new RpcError( - "modified code after first validation", - RpcErrorCodes.INVALID_OPCODE - ); - } - - return { - ...validationResult, - referencedContracts: { - addresses, - hash, - }, - }; - } - - parseErrorResult( - userOp: UserOperationStruct, - errorResult: { errorName: string; errorArgs: any } - ): UserOpValidationResult { - if (!errorResult?.errorName?.startsWith("ValidationResult")) { - // parse it as FailedOp - // if its FailedOp, then we have the paymaster param... otherwise its an Error(string) - let paymaster = errorResult.errorArgs?.paymaster; - if (paymaster === AddressZero) { - paymaster = undefined; - } - // eslint-disable-next-line - const msg: string = - errorResult.errorArgs?.reason ?? errorResult.toString(); - - if (paymaster == null) { - throw new RpcError(msg, RpcErrorCodes.VALIDATION_FAILED); - } else { - throw new RpcError(msg, RpcErrorCodes.REJECTED_BY_PAYMASTER, { - paymaster, - }); - } - } - - const { - returnInfo, - senderInfo, - factoryInfo, - paymasterInfo, - aggregatorInfo, // may be missing (exists only SimulationResultWithAggregator - } = errorResult.errorArgs; - - // extract address from "data" (first 20 bytes) - // add it as "addr" member to the "stakeinfo" struct - // if no address, then return "undefined" instead of struct. - function fillEntity(data: BytesLike, info: StakeInfo): StakeInfo | null { - const addr = getAddr(data); - return addr == null - ? null - : { - ...info, - addr, - }; - } - - return { - returnInfo, - senderInfo: { - ...senderInfo, - addr: userOp.sender, - }, - factoryInfo: fillEntity(userOp.initCode, factoryInfo), - paymasterInfo: fillEntity(userOp.paymasterAndData, paymasterInfo), - aggregatorInfo: fillEntity( - aggregatorInfo?.actualAggregator, - aggregatorInfo?.stakeInfo - ), - }; - } - - private parseCallsABI = Object.values( - [ - ...EntryPoint__factory.abi, - ...IAccount__factory.abi, - ...IAggregatedAccount__factory.abi, - ...IAggregator__factory.abi, - ...IPaymaster__factory.abi, - ].reduce((set, entry: any) => { - const key = `${entry.name}(${entry?.inputs - ?.map((i: any) => i.type) - .join(",")})`; - return { - ...set, - [key]: entry, - }; - }, {}) - ) as any; - - private parseCallXfaces = new Interface(this.parseCallsABI); - - parseCalls(calls: TracerCall[]): TracerCall[] { - function callCatch(x: () => T, def: T1): T | T1 { - try { - return x(); - } catch { - return def; - } - } - - const out: TracerCall[] = []; - const stack: any[] = []; - calls - .filter((x) => !x.type.startsWith("depth")) - .forEach((c) => { - if (c.type.match(/REVERT|RETURN/) != null) { - const top = stack.splice(-1)[0] ?? { - type: "top", - method: "validateUserOp", - }; - const returnData: string = (c as any).data; - if (top.type.match(/CREATE/) != null) { - out.push({ - to: top.to, - type: top.type, - method: "", - return: `len=${returnData.length}`, - }); - } else { - const method = callCatch( - () => this.parseCallXfaces.getFunction(top.method), - top.method - ); - if (c.type === "REVERT") { - const parsedError = callCatch( - () => this.parseCallXfaces.parseError(returnData), - returnData - ); - out.push({ - to: top.to, - type: top.type, - method: method.name, - value: top.value, - revert: parsedError, - }); - } else { - const ret = callCatch( - () => - this.parseCallXfaces.decodeFunctionResult(method, returnData), - returnData - ); - out.push({ - to: top.to, - type: top.type, - method: method.name ?? method, - return: ret, - }); - } - } - } else { - stack.push(c); - } - }); - - // TODO: verify that stack is empty at the end. - - return out; - } - - numberToEntityTitle(id?: number): string { - if (id == null) { - id = 0; - } - const map: { [id: number]: string } = { - 0: "factory", - 1: "account", - 2: "paymaster", - }; - return map[id] || map[0]!; - } - - isContractBanned(network: NetworkName, address: string): boolean { - const bannedList = BannedContracts[network]; - if (!bannedList || bannedList.length == 0) { - return false; - } - try { - address = ethers.utils.getAddress(ethers.utils.hexZeroPad(address, 20)); - return ( - bannedList.findIndex((addr) => { - return ( - ethers.utils.getAddress(ethers.utils.hexZeroPad(addr, 20)) == - address - ); - }) !== -1 - ); - } catch (err) { - return false; - } - } - - nethermindErrorHandler(epContract: EntryPoint, errorResult: any): any { - try { - let { error } = errorResult; - if (error.error) { - error = error.error; - } - if (error && error.code == -32015 && error.data.startsWith("Reverted ")) { - const parsed = epContract.interface.parseError(error.data.slice(9)); - errorResult = { - ...parsed, - errorName: parsed.name, - errorArgs: parsed.args, - }; - } - } catch (err) { - /* empty */ - } - return errorResult; - } -}