AA-472: Support native tracer (#230)

* 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 <forshtat1@gmail.com>
This commit is contained in:
Dror Tirosh
2024-10-23 15:40:48 +02:00
committed by GitHub
co-authored by Alex Forshtat
parent f464796938
commit 8bc5028caa
7 changed files with 105 additions and 15 deletions
+2
View File
@@ -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,
+2 -1
View File
@@ -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 {
+28 -6
View File
@@ -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 <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.')
@@ -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) {
+7 -1
View File
@@ -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 {
+44 -3
View File
@@ -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<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)
@@ -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
+11 -2
View File
@@ -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) {