Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix/resubmit when aa95 #284

Merged
merged 6 commits into from
Aug 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 18 additions & 9 deletions src/cli/config/bundler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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({
Expand Down
10 changes: 9 additions & 1 deletion src/cli/config/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,8 @@ export const bundlerOptions: CliCommandOptions<IBundlerArgsInput> = {
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
Expand Down Expand Up @@ -166,6 +167,13 @@ export const bundlerOptions: CliCommandOptions<IBundlerArgsInput> = {
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<ICompatibilityArgsInput> =
Expand Down
5 changes: 3 additions & 2 deletions src/cli/setupServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,8 @@ const getExecutorManager = ({
parsedArgs["max-bundle-wait"],
parsedArgs["max-gas-per-bundle"],
gasPriceManager,
eventManager
eventManager,
parsedArgs["aa95-gas-multiplier"]
)
}

Expand Down Expand Up @@ -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()
Expand Down
3 changes: 2 additions & 1 deletion src/executor/executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand Down
22 changes: 20 additions & 2 deletions src/executor/executorManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export class ExecutorManager {
private maxGasLimitPerBundle: bigint
private gasPriceManager: GasPriceManager
private eventManager: EventManager
private aa95ResubmitMultiplier: bigint

constructor(
executor: Executor,
Expand All @@ -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
Expand All @@ -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 () => {
Expand Down Expand Up @@ -378,6 +381,7 @@ export class ExecutorManager {
isVersion06,
transactionHash,
this.publicClient,
this.logger,
entryPoint
))
}))
Expand Down Expand Up @@ -460,7 +464,21 @@ export class ExecutorManager {
})

this.executor.markWalletProcessed(transactionInfo.executor)
} else if (bundlingStatus.status === "reverted") {
} else if (
bundlingStatus.status === "reverted" &&
bundlingStatus.isAA95
) {
// resubmit with more gas when bundler encounters AA95
const multiplier = this.aa95ResubmitMultiplier
transactionInfo.transactionRequest.gas =
(transactionInfo.transactionRequest.gas * multiplier) / 100n
transactionInfo.transactionRequest.nonce += 1

opInfos.map(({ userOperationHash }) => {
this.mempool.removeSubmitted(userOperationHash)
})
await this.replaceTransaction(transactionInfo, "AA95")
} else {
opInfos.map(({ userOperationHash }) => {
this.mempool.removeSubmitted(userOperationHash)
this.monitor.setUserOperationStatus(userOperationHash, {
Expand Down
45 changes: 39 additions & 6 deletions src/utils/userop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,12 @@ import {
toHex,
concat,
slice,
pad
pad,
decodeErrorResult,
parseAbi
} from "viem"
import { areAddressesEqual } from "./helpers"
import type { Logger } from "pino"

// Type predicate check if the UserOperation is V06.
export function isVersion06(
Expand Down Expand Up @@ -202,6 +205,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)
Expand All @@ -213,6 +218,7 @@ export const getBundleStatus = async (
isVersion06: boolean,
txHash: HexData32,
publicClient: PublicClient,
logger: Logger,
entryPoint: Address
): Promise<{
bundlingStatus: BundlingStatus
Expand All @@ -225,12 +231,39 @@ 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) {
try {
const match = (receipt.error as any).match(
/0x([a-fA-F0-9]+)?/
)

if (match) {
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
Expand Down
Loading