diff --git a/packages/bundler/src/BundlerConfig.ts b/packages/bundler/src/BundlerConfig.ts index 3938286c..22ecc7e0 100644 --- a/packages/bundler/src/BundlerConfig.ts +++ b/packages/bundler/src/BundlerConfig.ts @@ -16,6 +16,7 @@ export interface BundlerConfig { port: string privateApiPort: string unsafe: boolean + tracerRpcUrl?: string debugRpc?: boolean conditionalRpc: boolean @@ -53,6 +54,7 @@ export const BundlerConfigShape = { port: ow.string, privateApiPort: ow.string, unsafe: ow.boolean, + tracerRpcUrl: ow.optional.string, debugRpc: ow.optional.boolean, conditionalRpc: ow.boolean, diff --git a/packages/bundler/src/modules/initServer.ts b/packages/bundler/src/modules/initServer.ts index d51fec68..63145e80 100644 --- a/packages/bundler/src/modules/initServer.ts +++ b/packages/bundler/src/modules/initServer.ts @@ -38,7 +38,8 @@ export function initServer (config: BundlerConfig, signer: Signer): [ExecutionMa let validationManager: IValidationManager let bundleManager: IBundleManager if (!config.rip7560) { - validationManager = new ValidationManager(entryPoint, config.unsafe, preVerificationGasCalculator) + const tracerProvider = config.tracerRpcUrl == null ? undefined : getNetworkProvider(config.tracerRpcUrl) + validationManager = new ValidationManager(entryPoint, config.unsafe, preVerificationGasCalculator, tracerProvider) bundleManager = new BundleManager(entryPoint, entryPoint.provider as JsonRpcProvider, signer, eventsManager, mempoolManager, validationManager, reputationManager, config.beneficiary, parseEther(config.minBalance), config.maxBundleGas, config.conditionalRpc) } else { diff --git a/packages/bundler/src/runBundler.ts b/packages/bundler/src/runBundler.ts index d12494f2..ff5b5bb9 100644 --- a/packages/bundler/src/runBundler.ts +++ b/packages/bundler/src/runBundler.ts @@ -15,7 +15,7 @@ import { MethodHandlerERC4337 } from './MethodHandlerERC4337' import { initServer } from './modules/initServer' import { DebugMethodHandler } from './DebugMethodHandler' -import { supportsDebugTraceCall } from '@account-abstraction/validation-manager' +import { supportsDebugTraceCall, supportsNativeTracer } from '@account-abstraction/validation-manager' import { resolveConfiguration } from './Config' import { bundlerConfigDefault } from './BundlerConfig' import { parseEther } from 'ethers/lib/utils' @@ -80,7 +80,8 @@ export async function runBundler (argv: string[], overrideExit = true): Promise< .option('--privateApiPort ', `server listening port for block builder (default: ${bundlerConfigDefault.privateApiPort})`) .option('--config ', 'path to config file', CONFIG_FILE_NAME) .option('--auto', 'automatic bundling (bypass config.autoBundleMempoolSize)', false) - .option('--unsafe', 'UNSAFE mode: no storage or opcode checks (safe mode requires geth)') + .option('--unsafe', 'UNSAFE mode: no storage or opcode checks (safe mode requires debug_traceCall)') + .option('--tracerRpcUrl ', 'run native tracer on this provider, and prestateTracer native tracer on network provider. requires unsafe=false') .option('--debugRpc', 'enable debug rpc methods (auto-enabled for test node') .option('--conditionalRpc', 'Use eth_sendRawTransactionConditional RPC)') .option('--show-stack-traces', 'Show stack traces.') @@ -130,10 +131,31 @@ export async function runBundler (argv: string[], overrideExit = true): Promise< console.error('FATAL: --conditionalRpc requires a node that support eth_sendRawTransactionConditional') process.exit(1) } - if (!config.unsafe && !await supportsDebugTraceCall(provider as any, config.rip7560)) { - const requiredApi = config.rip7560 ? 'eth_traceRip7560Validation' : 'debug_traceCall' - console.error(`FATAL: full validation requires a node with ${requiredApi}. for local UNSAFE mode: use --unsafe`) - process.exit(1) + if (config.unsafe) { + if (config.tracerRpcUrl != null) { + console.error('FATAL: --unsafe and --tracerRpcUrl are mutually exclusive') + process.exit(1) + } + } else { + if (config.tracerRpcUrl != null) { + // validate standard tracer supports "prestateTracer": + if (!await supportsNativeTracer(provider, 'prestateTracer')) { + console.error('FATAL: --tracerRpcUrl requires the network provider to support prestateTracer') + process.exit(1) + } + const tracerProvider = new ethers.providers.JsonRpcProvider(config.tracerRpcUrl) + if (!await supportsNativeTracer(tracerProvider)) { + console.error('FATAL: --tracerRpcUrl requires a provider to support bundlerCollectorTracer') + process.exit(1) + } + } else { + // check standard javascript tracer: + if (!await supportsDebugTraceCall(provider as any, config.rip7560)) { + const requiredApi = config.rip7560 ? 'eth_traceRip7560Validation' : 'debug_traceCall' + console.error(`FATAL: full validation requires a node with ${requiredApi}. for local UNSAFE mode: use --unsafe`) + process.exit(1) + } + } } if (config.rip7560) { diff --git a/packages/utils/src/Utils.ts b/packages/utils/src/Utils.ts index b5cd13d5..1bde3b0b 100644 --- a/packages/utils/src/Utils.ts +++ b/packages/utils/src/Utils.ts @@ -217,7 +217,13 @@ export function sum (...args: BigNumberish[]): BigNumber { */ export function getUserOpMaxCost (userOp: OperationBase): BigNumber { const preVerificationGas: BigNumberish = (userOp as UserOperation).preVerificationGas - return sum(preVerificationGas ?? 0, userOp.verificationGasLimit, userOp.callGasLimit, userOp.paymasterVerificationGasLimit ?? 0, userOp.paymasterPostOpGasLimit ?? 0).mul(userOp.maxFeePerGas) + return sum( + preVerificationGas ?? 0, + userOp.verificationGasLimit, + userOp.callGasLimit, + userOp.paymasterVerificationGasLimit ?? 0, + userOp.paymasterPostOpGasLimit ?? 0 + ).mul(userOp.maxFeePerGas) } export function getPackedNonce (userOp: OperationBase): BigNumber { diff --git a/packages/validation-manager/src/GethTracer.ts b/packages/validation-manager/src/GethTracer.ts index d3846e11..2c45ee4a 100644 --- a/packages/validation-manager/src/GethTracer.ts +++ b/packages/validation-manager/src/GethTracer.ts @@ -8,6 +8,10 @@ import { OperationRIP7560, RpcError } from '@account-abstraction/utils' const debug = Debug('aa.tracer') +// the name of the native tracer. +// equivalent to the javascript "bundlerCollectorTracer". +export const bundlerNativeTracerName = 'bundlerCollectorTracer' + /** * a function returning a LogTracer. * the function's body must be "{ return {...} }" @@ -17,13 +21,50 @@ const debug = Debug('aa.tracer') */ type LogTracerFunc = () => LogTracer +/** + * trace a transaction using the geth debug_traceCall method. + * @param provider the network node to trace on + * @param tx the transaction to trace + * @param options the trace options + * @param nativeTracerProvider if set, submit only preStateTracer to the network provider, and use this (second) provider with native tracer. + * if null, then use javascript tracer on the first provider. + */ + // eslint-disable-next-line @typescript-eslint/naming-convention -export async function debug_traceCall (provider: JsonRpcProvider, tx: Deferrable, options: TraceOptions): Promise { +export async function debug_traceCall (provider: JsonRpcProvider, tx: Deferrable, options: TraceOptions, nativeTracerProvider?: JsonRpcProvider): Promise { const tx1 = await resolveProperties(tx) const traceOptions = tracer2string(options) + if (nativeTracerProvider != null) { + // there is a nativeTracerProvider: use it for the native tracer, but first we need preStateTracer from the main provider: + const preState: { [addr: string]: any } = await provider.send('debug_traceCall', [tx1, 'latest', { ...traceOptions, tracer: 'prestateTracer' }]) + + // fix prestate to be valid "state overrides" + // - convert nonce's to hex + // - rename storage to state + for (const key in preState) { + if (preState[key]?.nonce != null) { + preState[key].nonce = '0x' + (preState[key].nonce.toString(16) as string) + } + if (preState[key]?.storage != null) { + // rpc expects "state" instead... + preState[key].state = preState[key].storage + delete preState[key].storage + } + } + + const ret = await nativeTracerProvider.send('debug_traceCall', [tx1, 'latest', { + tracer: bundlerNativeTracerName, + stateOverrides: preState + }]) + + return ret + } + const ret = await provider.send('debug_traceCall', [tx1, 'latest', traceOptions]).catch(e => { - debug('ex=', e.error) - debug('tracer=', traceOptions.tracer?.toString().split('\n').map((line, index) => `${index + 1}: ${line}`).join('\n')) + if (debug.enabled) { + debug('ex=', e.error) + debug('tracer=', traceOptions.tracer?.toString().split('\n').map((line, index) => `${index + 1}: ${line}`).join('\n')) + } throw e }) // return applyTracer(ret, options) diff --git a/packages/validation-manager/src/ValidationManager.ts b/packages/validation-manager/src/ValidationManager.ts index a4bcc142..9b228bf1 100644 --- a/packages/validation-manager/src/ValidationManager.ts +++ b/packages/validation-manager/src/ValidationManager.ts @@ -44,11 +44,20 @@ const VALID_UNTIL_FUTURE_SECONDS = 30 const HEX_REGEX = /^0x[a-fA-F\d]*$/i const entryPointSimulations = IEntryPointSimulations__factory.createInterface() +/** + * ValidationManager is responsible for validating UserOperations. + * @param entryPoint - the entryPoint contract + * @param unsafe - if true, skip tracer for validation rules (validate only through eth_call) + * @param preVerificationGasCalculator - helper to calculate the correct 'preVerificationGas' for the current network conditions + * @param providerForTracer - if provided, use it for native bundlerCollectorTracer, and use main provider with "preStateTracer" + * (relevant only if unsafe=false) + */ export class ValidationManager implements IValidationManager { constructor ( readonly entryPoint: IEntryPoint, readonly unsafe: boolean, - readonly preVerificationGasCalculator: PreVerificationGasCalculator + readonly preVerificationGasCalculator: PreVerificationGasCalculator, + readonly providerForTracer?: JsonRpcProvider ) { } @@ -144,7 +153,7 @@ export class ValidationManager implements IValidationManager { code: EntryPointSimulationsJson.deployedBytecode } } - }) + }, this.providerForTracer) const lastResult = tracerResult.calls.slice(-1)[0] const data = (lastResult as ExitInfo).data diff --git a/packages/validation-manager/src/index.ts b/packages/validation-manager/src/index.ts index db703e96..791d53ac 100644 --- a/packages/validation-manager/src/index.ts +++ b/packages/validation-manager/src/index.ts @@ -1,17 +1,26 @@ import { JsonRpcProvider } from '@ethersproject/providers' import { AddressZero, IEntryPoint__factory, OperationRIP7560, UserOperation } from '@account-abstraction/utils' +import { PreVerificationGasCalculator } from '@account-abstraction/sdk' +import { bundlerNativeTracerName, debug_traceCall, eth_traceRip7560Validation } from './GethTracer' import { bundlerCollectorTracer } from './BundlerCollectorTracer' -import { debug_traceCall, eth_traceRip7560Validation } from './GethTracer' import { ValidateUserOpResult } from './IValidationManager' import { ValidationManager } from './ValidationManager' -import { PreVerificationGasCalculator } from '@account-abstraction/sdk' export * from './ValidationManager' export * from './ValidationManagerRIP7560' export * from './IValidationManager' +export async function supportsNativeTracer (provider: JsonRpcProvider, nativeTracer = bundlerNativeTracerName): Promise { + try { + await provider.send('debug_traceCall', [{}, 'latest', { tracer: nativeTracer }]) + return true + } catch (e) { + return false + } +} + export async function supportsDebugTraceCall (provider: JsonRpcProvider, rip7560: boolean): Promise { const p = provider.send as any if (p._clientVersion == null) {