From 250b901ce1de4ddf9a97e3077dbdfa99b81f43c5 Mon Sep 17 00:00:00 2001 From: mouseless <97399882+mouseless-eth@users.noreply.github.com> Date: Thu, 22 Aug 2024 16:33:03 +0100 Subject: [PATCH 1/6] detect aa95 onchain --- src/executor/executorManager.ts | 1 + src/utils/userop.ts | 44 ++++++++++++++++++++++++++++----- 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/src/executor/executorManager.ts b/src/executor/executorManager.ts index 98df8c79..7d8f0ecb 100644 --- a/src/executor/executorManager.ts +++ b/src/executor/executorManager.ts @@ -378,6 +378,7 @@ export class ExecutorManager { isVersion06, transactionHash, this.publicClient, + this.logger, entryPoint )) })) diff --git a/src/utils/userop.ts b/src/utils/userop.ts index 1e907bd0..c4fb1390 100644 --- a/src/utils/userop.ts +++ b/src/utils/userop.ts @@ -20,9 +20,13 @@ import { toHex, concat, slice, - pad + pad, + decodeErrorResult, + parseAbi } from "viem" import { areAddressesEqual } from "./helpers" +import { join } from "node:path" +import { Logger } from "pino" // Type predicate check if the UserOperation is V06. export function isVersion06( @@ -202,6 +206,8 @@ type BundlingStatus = | { // The tx reverted due to a op in the bundle failing EntryPoint validation status: "reverted" + // biome-ignore lint/style/useNamingConvention: use double upper case for AA errors + isAA95: boolean } | { // The tx could not be found (pending or invalid hash) @@ -213,6 +219,7 @@ export const getBundleStatus = async ( isVersion06: boolean, txHash: HexData32, publicClient: PublicClient, + logger: Logger, entryPoint: Address ): Promise<{ bundlingStatus: BundlingStatus @@ -225,12 +232,37 @@ export const getBundleStatus = async ( const blockNumber = receipt.blockNumber if (receipt.status === "reverted") { - return { - bundlingStatus: { - status: "reverted" - }, - blockNumber + const bundlingStatus: { status: "reverted"; isAA95: boolean } = { + status: "reverted", + isAA95: false } + + if ("error" in receipt) { + const match = (receipt.error as any).match(/0x([a-fA-F0-9]+)?/) + + if (match) { + try { + const revertReason = match[0] as Hex + const decoded = decodeErrorResult({ + data: revertReason, + abi: parseAbi([ + "error FailedOp(uint256 opIndex, string reason)" + ]) + }) + + if (decoded.args[1] === "AA95 out of gas") { + bundlingStatus.isAA95 = true + } + } catch (e) { + logger.error( + "Failed to decode userOperation revert reason due to ", + e + ) + } + } + } + + return { bundlingStatus, blockNumber } } const userOperationDetails = receipt.logs From e6c1cf194e679c29b8c31ae0721d3b9dd0f29131 Mon Sep 17 00:00:00 2001 From: mouseless <97399882+mouseless-eth@users.noreply.github.com> Date: Thu, 22 Aug 2024 17:29:07 +0100 Subject: [PATCH 2/6] add replace tx logic --- src/executor/executor.ts | 3 ++- src/executor/executorManager.ts | 11 ++++++++++- src/utils/userop.ts | 3 +-- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/executor/executor.ts b/src/executor/executor.ts index 7b2a07b3..b00c61b4 100644 --- a/src/executor/executor.ts +++ b/src/executor/executor.ts @@ -355,7 +355,8 @@ export class Executor { // sometimes the estimation rounds down, adding a fixed constant accounts for this gasLimit += 10_000n - newRequest.gas = gasLimit + // ensures that we don't submit again with too low of a gas value + newRequest.gas = maxBigInt(newRequest.gas, gasLimit) // update calldata to include only ops that pass simulation if (transactionInfo.transactionType === "default") { diff --git a/src/executor/executorManager.ts b/src/executor/executorManager.ts index 7d8f0ecb..c8892fee 100644 --- a/src/executor/executorManager.ts +++ b/src/executor/executorManager.ts @@ -461,7 +461,16 @@ export class ExecutorManager { }) this.executor.markWalletProcessed(transactionInfo.executor) - } else if (bundlingStatus.status === "reverted") { + } else if ( + bundlingStatus.status === "reverted" && + bundlingStatus.isAA95 + ) { + // resubmit with 150% more gas when bundler encounters AA95 + transactionInfo.transactionRequest.gas = + (transactionInfo.transactionRequest.gas * 150n) / 100n + + await this.replaceTransaction(transactionInfo, "AA95") + } else { opInfos.map(({ userOperationHash }) => { this.mempool.removeSubmitted(userOperationHash) this.monitor.setUserOperationStatus(userOperationHash, { diff --git a/src/utils/userop.ts b/src/utils/userop.ts index c4fb1390..50987abc 100644 --- a/src/utils/userop.ts +++ b/src/utils/userop.ts @@ -25,8 +25,7 @@ import { parseAbi } from "viem" import { areAddressesEqual } from "./helpers" -import { join } from "node:path" -import { Logger } from "pino" +import type { Logger } from "pino" // Type predicate check if the UserOperation is V06. export function isVersion06( From 711aaaa064d7bd5437dede83a0025f25ede16585 Mon Sep 17 00:00:00 2001 From: mouseless <97399882+mouseless-eth@users.noreply.github.com> Date: Thu, 22 Aug 2024 17:36:43 +0100 Subject: [PATCH 3/6] increment nonce --- src/executor/executorManager.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/executor/executorManager.ts b/src/executor/executorManager.ts index c8892fee..9e467cfb 100644 --- a/src/executor/executorManager.ts +++ b/src/executor/executorManager.ts @@ -468,6 +468,7 @@ export class ExecutorManager { // resubmit with 150% more gas when bundler encounters AA95 transactionInfo.transactionRequest.gas = (transactionInfo.transactionRequest.gas * 150n) / 100n + transactionInfo.transactionRequest.nonce += 1 await this.replaceTransaction(transactionInfo, "AA95") } else { From 9201b5131e996972a7365b78c73bd5bd69c5f1be Mon Sep 17 00:00:00 2001 From: mouseless <97399882+mouseless-eth@users.noreply.github.com> Date: Thu, 22 Aug 2024 18:31:09 +0100 Subject: [PATCH 4/6] remove op from submitted if failed due to AA95 --- src/executor/executorManager.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/executor/executorManager.ts b/src/executor/executorManager.ts index 9e467cfb..4101d663 100644 --- a/src/executor/executorManager.ts +++ b/src/executor/executorManager.ts @@ -470,6 +470,9 @@ export class ExecutorManager { (transactionInfo.transactionRequest.gas * 150n) / 100n transactionInfo.transactionRequest.nonce += 1 + opInfos.map(({ userOperationHash }) => { + this.mempool.removeSubmitted(userOperationHash) + }) await this.replaceTransaction(transactionInfo, "AA95") } else { opInfos.map(({ userOperationHash }) => { From e1b70d0e7cf6738483c61121a55d21dfcdf49cd8 Mon Sep 17 00:00:00 2001 From: mouseless <97399882+mouseless-eth@users.noreply.github.com> Date: Thu, 22 Aug 2024 18:49:15 +0100 Subject: [PATCH 5/6] move aa95 multiplier to flags --- src/cli/config/bundler.ts | 27 ++++++++++++++++++--------- src/cli/config/options.ts | 10 +++++++++- src/cli/setupServer.ts | 5 +++-- src/executor/executorManager.ts | 10 +++++++--- 4 files changed, 37 insertions(+), 15 deletions(-) diff --git a/src/cli/config/bundler.ts b/src/cli/config/bundler.ts index 3f2b94d2..ff5cd886 100644 --- a/src/cli/config/bundler.ts +++ b/src/cli/config/bundler.ts @@ -11,8 +11,9 @@ import { z } from "zod" const logLevel = z.enum(["trace", "debug", "info", "warn", "error", "fatal"]) -const rpcMethodNames = bundlerRequestSchema.options - .map((s) => s.shape.method._def.value) as [string, ...string[]] +const rpcMethodNames = bundlerRequestSchema.options.map( + (s) => s.shape.method._def.value +) as [string, ...string[]] export const bundlerArgsSchema = z.object({ entrypoints: z @@ -93,21 +94,29 @@ export const bundlerArgsSchema = z.object({ .string() .nullable() .transform((val: string | null) => { - if (val === null) return null; + if (val === null) return null return val.split(",") }) .refine((values) => { - if (values === null) return true; + if (values === null) return true return values.length > 0 }, "Must contain at least one method if specified") - .refine((values) => { - if (values === null) return true; - - return values.every((value: string) => rpcMethodNames.includes(value)) - }, `Unknown method specified, available methods: ${rpcMethodNames.join(",")}`), + .refine( + (values) => { + if (values === null) return true + + return values.every((value: string) => + rpcMethodNames.includes(value) + ) + }, + `Unknown method specified, available methods: ${rpcMethodNames.join( + "," + )}` + ), "refilling-wallets": z.boolean().default(true), + "aa95-gas-multiplier": z.string().transform((val) => BigInt(val)) }) export const compatibilityArgsSchema = z.object({ diff --git a/src/cli/config/options.ts b/src/cli/config/options.ts index e5685e13..32848d87 100644 --- a/src/cli/config/options.ts +++ b/src/cli/config/options.ts @@ -122,7 +122,8 @@ export const bundlerOptions: CliCommandOptions = { default: "100,100,100" }, "gas-price-refresh-interval": { - description: "How to often to refresh the gas prices (seconds). If 0, then gas prices are refreshed on every request", + description: + "How to often to refresh the gas prices (seconds). If 0, then gas prices are refreshed on every request", type: "number", require: false, default: 0 @@ -166,6 +167,13 @@ export const bundlerOptions: CliCommandOptions = { require: false, default: true }, + "aa95-gas-multiplier": { + description: + "Amount to multiply the current gas limit by if the bundling tx fails due to AA95", + type: "string", + require: false, + default: "125" + } } export const compatibilityOptions: CliCommandOptions = diff --git a/src/cli/setupServer.ts b/src/cli/setupServer.ts index 5c648108..8ae95c96 100644 --- a/src/cli/setupServer.ts +++ b/src/cli/setupServer.ts @@ -277,7 +277,8 @@ const getExecutorManager = ({ parsedArgs["max-bundle-wait"], parsedArgs["max-gas-per-bundle"], gasPriceManager, - eventManager + eventManager, + parsedArgs["aa95-gas-multiplier"] ) } @@ -460,7 +461,7 @@ export const setupServer = async ({ walletClient, parsedArgs["min-executor-balance"] ) - }, parsedArgs["executor-refill-interval"] * 1000) + }, parsedArgs["executor-refill-interval"] * 1000) } const monitor = getMonitor() diff --git a/src/executor/executorManager.ts b/src/executor/executorManager.ts index 4101d663..3fc0eaa8 100644 --- a/src/executor/executorManager.ts +++ b/src/executor/executorManager.ts @@ -59,6 +59,7 @@ export class ExecutorManager { private maxGasLimitPerBundle: bigint private gasPriceManager: GasPriceManager private eventManager: EventManager + private aa95ResubmitMultiplier: bigint constructor( executor: Executor, @@ -74,7 +75,8 @@ export class ExecutorManager { bundlerFrequency: number, maxGasLimitPerBundle: bigint, gasPriceManager: GasPriceManager, - eventManager: EventManager + eventManager: EventManager, + aa95ResubmitMultiplier: bigint ) { this.entryPoints = entryPoints this.reputationManager = reputationManager @@ -89,6 +91,7 @@ export class ExecutorManager { this.maxGasLimitPerBundle = maxGasLimitPerBundle this.gasPriceManager = gasPriceManager this.eventManager = eventManager + this.aa95ResubmitMultiplier = aa95ResubmitMultiplier if (bundleMode === "auto") { this.timer = setInterval(async () => { @@ -465,9 +468,10 @@ export class ExecutorManager { bundlingStatus.status === "reverted" && bundlingStatus.isAA95 ) { - // resubmit with 150% more gas when bundler encounters AA95 + // resubmit with more gas when bundler encounters AA95 + const multiplier = this.aa95ResubmitMultiplier transactionInfo.transactionRequest.gas = - (transactionInfo.transactionRequest.gas * 150n) / 100n + (transactionInfo.transactionRequest.gas * multiplier) / 100n transactionInfo.transactionRequest.nonce += 1 opInfos.map(({ userOperationHash }) => { From 2632b3d093943eea2d833c1ce4552846e7762f08 Mon Sep 17 00:00:00 2001 From: mouseless <97399882+mouseless-eth@users.noreply.github.com> Date: Thu, 22 Aug 2024 18:52:16 +0100 Subject: [PATCH 6/6] move try/catch --- src/utils/userop.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/utils/userop.ts b/src/utils/userop.ts index 50987abc..40f2d5b4 100644 --- a/src/utils/userop.ts +++ b/src/utils/userop.ts @@ -237,10 +237,12 @@ export const getBundleStatus = async ( } if ("error" in receipt) { - const match = (receipt.error as any).match(/0x([a-fA-F0-9]+)?/) + try { + const match = (receipt.error as any).match( + /0x([a-fA-F0-9]+)?/ + ) - if (match) { - try { + if (match) { const revertReason = match[0] as Hex const decoded = decodeErrorResult({ data: revertReason, @@ -252,12 +254,12 @@ export const getBundleStatus = async ( if (decoded.args[1] === "AA95 out of gas") { bundlingStatus.isAA95 = true } - } catch (e) { - logger.error( - "Failed to decode userOperation revert reason due to ", - e - ) } + } catch (e) { + logger.error( + "Failed to decode userOperation revert reason due to ", + e + ) } }