Skip to content

Commit

Permalink
AA-472: Support native tracer (#230)
Browse files Browse the repository at this point in the history
* support native tracer

--tracerRpcUrl - provide a geth node that supports the native
"bundlerCollectorTracer" native tracer
main provider must support "prestateTracer"

---------

Co-authored-by: Alex Forshtat <[email protected]>
  • Loading branch information
drortirosh and forshtat authored Oct 23, 2024
1 parent f464796 commit 8bc5028
Show file tree
Hide file tree
Showing 7 changed files with 105 additions and 15 deletions.
2 changes: 2 additions & 0 deletions packages/bundler/src/BundlerConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export interface BundlerConfig {
port: string
privateApiPort: string
unsafe: boolean
tracerRpcUrl?: string
debugRpc?: boolean
conditionalRpc: boolean

Expand Down Expand Up @@ -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,

Expand Down
3 changes: 2 additions & 1 deletion packages/bundler/src/modules/initServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
34 changes: 28 additions & 6 deletions packages/bundler/src/runBundler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -80,7 +80,8 @@ export async function runBundler (argv: string[], overrideExit = true): Promise<
.option('--privateApiPort <number>', `server listening port for block builder (default: ${bundlerConfigDefault.privateApiPort})`)
.option('--config <string>', '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 <string>', '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.')
Expand Down Expand Up @@ -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) {
Expand Down
8 changes: 7 additions & 1 deletion packages/utils/src/Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
47 changes: 44 additions & 3 deletions packages/validation-manager/src/GethTracer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {...} }"
Expand All @@ -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<TransactionRequest>, options: TraceOptions): Promise<TraceResult | any> {
export async function debug_traceCall (provider: JsonRpcProvider, tx: Deferrable<TransactionRequest>, options: TraceOptions, nativeTracerProvider?: JsonRpcProvider): Promise<TraceResult | any> {
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)
Expand Down
13 changes: 11 additions & 2 deletions packages/validation-manager/src/ValidationManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
) {
}

Expand Down Expand Up @@ -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
Expand Down
13 changes: 11 additions & 2 deletions packages/validation-manager/src/index.ts
Original file line number Diff line number Diff line change
@@ -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<boolean> {
try {
await provider.send('debug_traceCall', [{}, 'latest', { tracer: nativeTracer }])
return true
} catch (e) {
return false
}
}

export async function supportsDebugTraceCall (provider: JsonRpcProvider, rip7560: boolean): Promise<boolean> {
const p = provider.send as any
if (p._clientVersion == null) {
Expand Down

0 comments on commit 8bc5028

Please sign in to comment.