From 50ee3fb2faecdc7191b852cca3c0fe73b926a932 Mon Sep 17 00:00:00 2001 From: mouseless <97399882+mouseless-eth@users.noreply.github.com> Date: Sun, 26 Jan 2025 03:54:09 +0000 Subject: [PATCH 01/34] cleanup --- src/cli/config/bundler.ts | 1 - src/cli/config/options.ts | 7 - src/executor/executor.ts | 320 ++++++++++---------------------- src/executor/executorManager.ts | 11 +- src/executor/utils.ts | 289 +++++++++++++--------------- src/handlers/eventManager.ts | 18 +- src/mempool/mempool.ts | 9 +- src/types/mempool.ts | 54 ++---- 8 files changed, 269 insertions(+), 440 deletions(-) diff --git a/src/cli/config/bundler.ts b/src/cli/config/bundler.ts index c66957af..de12ad0c 100644 --- a/src/cli/config/bundler.ts +++ b/src/cli/config/bundler.ts @@ -147,7 +147,6 @@ export const compatibilityArgsSchema = z.object({ .optional() .transform((val) => val as ApiVersion), "balance-override": z.boolean(), - "local-gas-limit-calculation": z.boolean(), "flush-stuck-transactions-during-startup": z.boolean(), "fixed-gas-limit-for-estimation": z .string() diff --git a/src/cli/config/options.ts b/src/cli/config/options.ts index 3b4fc3c0..efed1dc8 100644 --- a/src/cli/config/options.ts +++ b/src/cli/config/options.ts @@ -339,13 +339,6 @@ export const compatibilityOptions: CliCommandOptions = require: true, default: true }, - "local-gas-limit-calculation": { - description: - "Calculate the bundle transaction gas limits locally instead of using the RPC gas limit estimation", - type: "boolean", - require: true, - default: false - }, "flush-stuck-transactions-during-startup": { description: "Flush stuck transactions with old nonces during bundler startup", diff --git a/src/executor/executor.ts b/src/executor/executor.ts index d5d17622..2df99b84 100644 --- a/src/executor/executor.ts +++ b/src/executor/executor.ts @@ -15,7 +15,6 @@ import { } from "@alto/types" import type { Logger, Metrics } from "@alto/utils" import { - getRequiredPrefund, getUserOperationHash, isVersion06, maxBigInt, @@ -41,7 +40,6 @@ import { import { filterOpsAndEstimateGas, flushStuckTransaction, - simulatedOpsToResults, isTransactionUnderpricedError, getAuthorizationList } from "./utils" @@ -143,7 +141,6 @@ export class Executor { await this.gasPriceManager.tryGetNetworkGasPrice() } catch (err) { this.logger.error({ error: err }, "Failed to get network gas price") - this.markWalletProcessed(transactionInfo.executor) return { status: "failed" } } @@ -151,7 +148,6 @@ export class Executor { gasPriceParameters.maxFeePerGas, 115n ) - newRequest.maxPriorityFeePerGas = scaleBigIntByPercent( gasPriceParameters.maxPriorityFeePerGas, 115n @@ -173,7 +169,7 @@ export class Executor { } ) - const [isUserOpVersion06, entryPoint] = opsWithHashes.reduce( + const [isUserOpV06, entryPoint] = opsWithHashes.reduce( (acc, owh) => { if ( acc[0] !== isVersion06(owh.userOperation) || @@ -192,7 +188,7 @@ export class Executor { ) const ep = getContract({ - abi: isUserOpVersion06 ? EntryPointV06Abi : EntryPointV07Abi, + abi: isUserOpV06 ? EntryPointV06Abi : EntryPointV07Abi, address: entryPoint, client: { public: this.config.publicClient, @@ -200,89 +196,51 @@ export class Executor { } }) - let { simulatedOps, gasLimit } = await filterOpsAndEstimateGas( - transactionInfo.entryPoint, - ep, - transactionInfo.executor, - opsWithHashes, - newRequest.nonce, - newRequest.maxFeePerGas, - newRequest.maxPriorityFeePerGas, - this.config.blockTagSupport ? "latest" : undefined, - this.config.legacyTransactions, - this.config.fixedGasLimitForEstimation, - this.reputationManager, - this.logger - ) - const childLogger = this.logger.child({ transactionHash: transactionInfo.transactionHash, executor: transactionInfo.executor.address }) - if (simulatedOps.length === 0) { - childLogger.warn("no ops to bundle") - this.markWalletProcessed(transactionInfo.executor) + let bundleResult = await filterOpsAndEstimateGas({ + ep, + isUserOpV06, + wallet: newRequest.account, + ops: opsWithHashes, + nonce: newRequest.nonce, + maxFeePerGas: newRequest.maxFeePerGas, + maxPriorityFeePerGas: newRequest.maxPriorityFeePerGas, + reputationManager: this.reputationManager, + config: this.config, + logger: childLogger + }) + + if (bundleResult.status === "unexpectedFailure") { return { status: "failed" } } - if ( - simulatedOps.every( - (op) => - op.reason === "AA25 invalid account nonce" || - op.reason === "AA10 sender already constructed" - ) - ) { + let { opsToBundle, failedOps, gasLimit } = bundleResult + + const allOpsFailed = (opsToBundle.length = 0) + const potentiallyIncluded = failedOps.every( + (op) => + op.reason === "AA25 invalid account nonce" || + op.reason === "AA10 sender already constructed" + ) + + if (allOpsFailed && potentiallyIncluded) { childLogger.trace( - { reasons: simulatedOps.map((sop) => sop.reason) }, + { reasons: failedOps.map((sop) => sop.reason) }, "all ops failed simulation with nonce error" ) return { status: "potentially_already_included" } } - if (simulatedOps.every((op) => op.reason !== undefined)) { + if (allOpsFailed) { + childLogger.warn("no ops to bundle") childLogger.warn("all ops failed simulation") - this.markWalletProcessed(transactionInfo.executor) return { status: "failed" } } - const opsToBundle = simulatedOps - .filter((op) => op.reason === undefined) - .map((op) => { - const opInfo = transactionInfo.userOperationInfos.find( - (info) => - info.userOperationHash === op.owh.userOperationHash - ) - if (!opInfo) { - throw new Error("opInfo not found") - } - return opInfo - }) - - if (this.config.localGasLimitCalculation) { - gasLimit = opsToBundle.reduce((acc, opInfo) => { - const userOperation = opInfo.userOperation - return ( - acc + - userOperation.preVerificationGas + - 3n * userOperation.verificationGasLimit + - userOperation.callGasLimit - ) - }, 0n) - } - - // https://github.com/eth-infinitism/account-abstraction/blob/fa61290d37d079e928d92d53a122efcc63822214/contracts/core/EntryPoint.sol#L236 - let innerHandleOpFloor = 0n - for (const owh of opsToBundle) { - const op = owh.userOperation - innerHandleOpFloor += - op.callGasLimit + op.verificationGasLimit + 5000n - } - - if (gasLimit < innerHandleOpFloor) { - gasLimit += innerHandleOpFloor - } - // sometimes the estimation rounds down, adding a fixed constant accounts for this gasLimit += 10_000n @@ -293,13 +251,13 @@ export class Executor { let txParam: HandleOpsTxParam const userOps = opsToBundle.map((op) => - isUserOpVersion06 + isUserOpV06 ? op.userOperation : toPackedUserOperation(op.userOperation as UserOperationV07) ) as PackedUserOperation[] txParam = { - isUserOpVersion06, + isUserOpVersion06: isUserOpV06, isReplacementTx: true, ops: userOps, entryPoint: transactionInfo.entryPoint @@ -314,7 +272,7 @@ export class Executor { chain: undefined }, executor: newRequest.account.address, - opsToBundle: opsToBundle.map( + ooooops: opsToBundle.map( (opInfo) => opInfo.userOperationHash ) }, @@ -339,16 +297,10 @@ export class Executor { } }) - opsToBundle.map(({ entryPoint, userOperation }) => { - const chainId = this.config.publicClient.chain?.id - const opHash = getUserOperationHash( - userOperation, - entryPoint, - chainId as number - ) - - this.eventManager.emitSubmitted(opHash, txHash) - }) + this.eventManager.emitSubmitted( + opsToBundle.map((op) => op.userOperationHash), + txHash + ) const newTxInfo: TransactionInfo = { ...transactionInfo, @@ -411,8 +363,6 @@ export class Executor { } childLogger.warn({ error: e }, "error replacing transaction") - this.markWalletProcessed(transactionInfo.executor) - return { status: "failed" } } } @@ -634,7 +584,7 @@ export class Executor { async bundle( entryPoint: Address, ops: UserOperation[] - ): Promise { + ): Promise { const wallet = await this.senderManager.getWallet() const opsWithHashes = ops.map((userOperation) => { @@ -648,17 +598,18 @@ export class Executor { } }) - const isUserOpVersion06 = opsWithHashes.reduce((acc, op) => { - if (acc !== isVersion06(op.userOperation)) { - throw new Error( - "All user operations must be of the same version" - ) - } - return acc - }, isVersion06(opsWithHashes[0].userOperation)) + // Find bundle EntryPoint version. + const firstOpVersion = isVersion06(ops[0]) + const allSameVersion = ops.every( + (op) => isVersion06(op) === firstOpVersion + ) + if (!allSameVersion) { + throw new Error("All user operations must be of the same version") + } + const isUserOpV06 = firstOpVersion const ep = getContract({ - abi: isUserOpVersion06 ? EntryPointV06Abi : EntryPointV07Abi, + abi: isUserOpV06 ? EntryPointV06Abi : EntryPointV07Abi, address: entryPoint, client: { public: this.config.publicClient, @@ -689,121 +640,64 @@ export class Executor { "Failed to get parameters for bundling" ) this.markWalletProcessed(wallet) - return opsWithHashes.map((owh) => { - return { - status: "resubmit", - info: { - entryPoint, - userOpHash: owh.userOperationHash, - userOperation: owh.userOperation, - reason: "Failed to get parameters for bundling" - } - } - }) + return { + status: "resubmit", + reason: "Failed to get parameters for bundling", + userOperations: opsWithHashes.map((owh) => owh.userOperation) + } } - let { gasLimit, simulatedOps } = await filterOpsAndEstimateGas( - entryPoint, + let estimateResult = await filterOpsAndEstimateGas({ + isUserOpV06, ep, wallet, - opsWithHashes, + ops: opsWithHashes, nonce, - gasPriceParameters.maxFeePerGas, - gasPriceParameters.maxPriorityFeePerGas, - this.config.blockTagSupport ? "pending" : undefined, - this.config.legacyTransactions, - this.config.fixedGasLimitForEstimation, - this.reputationManager, - childLogger, - getAuthorizationList( - opsWithHashes.map(({ userOperation }) => userOperation) - ) - ) + maxFeePerGas: gasPriceParameters.maxFeePerGas, + maxPriorityFeePerGas: gasPriceParameters.maxPriorityFeePerGas, + reputationManager: this.reputationManager, + config: this.config, + logger: childLogger + }) - if (simulatedOps.length === 0) { + if (estimateResult.status === "unexpectedFailure") { childLogger.error( "gas limit simulation encountered unexpected failure" ) this.markWalletProcessed(wallet) - return opsWithHashes.map((owh) => { - return { - status: "failure", - error: { - entryPoint, - userOpHash: owh.userOperationHash, - userOperation: owh.userOperation, - reason: "INTERNAL FAILURE" - } - } - }) + return { + status: "failure", + reason: "INTERNAL FAILURE", + userOperations: ops + } } - if (simulatedOps.every((op) => op.reason !== undefined)) { + let { gasLimit, opsToBundle, failedOps } = estimateResult + + if (opsToBundle.length === 0) { childLogger.warn("all ops failed simulation") this.markWalletProcessed(wallet) - return simulatedOps.map(({ reason, owh }) => { - return { - status: "failure", - error: { - entryPoint, - userOpHash: owh.userOperationHash, - userOperation: owh.userOperation, - reason: reason as string - } - } - }) + return { + status: "failure", + reason: "INTERNAL FAILURE", + // TODO: we want to log the failure reason + userOperations: ops + } } - const opsWithHashToBundle = simulatedOps - .filter((op) => op.reason === undefined) - .map((op) => op.owh) - childLogger = this.logger.child({ - userOperations: opsWithHashToBundle.map( - (owh) => owh.userOperationHash - ), + userOperations: opsToBundle.map((owh) => owh.userOperationHash), entryPoint }) - // https://github.com/eth-infinitism/account-abstraction/blob/fa61290d37d079e928d92d53a122efcc63822214/contracts/core/EntryPoint.sol#L236 - let innerHandleOpFloor = 0n - let totalBeneficiaryFees = 0n - for (const owh of opsWithHashToBundle) { - const op = owh.userOperation - innerHandleOpFloor += - op.callGasLimit + op.verificationGasLimit + 5000n - - totalBeneficiaryFees += getRequiredPrefund(op) - } - - if (gasLimit < innerHandleOpFloor) { - gasLimit += innerHandleOpFloor - } - // sometimes the estimation rounds down, adding a fixed constant accounts for this gasLimit += 10_000n - childLogger.debug({ gasLimit }, "got gas limit") - let transactionHash: HexData32 try { const isLegacyTransaction = this.config.legacyTransactions - - if (this.config.noProfitBundling) { - const gasPrice = totalBeneficiaryFees / gasLimit - if (isLegacyTransaction) { - gasPriceParameters.maxFeePerGas = gasPrice - gasPriceParameters.maxPriorityFeePerGas = gasPrice - } else { - gasPriceParameters.maxFeePerGas = maxBigInt( - gasPrice, - gasPriceParameters.maxFeePerGas || 0n - ) - } - } - const authorizationList = getAuthorizationList( - opsWithHashToBundle.map(({ userOperation }) => userOperation) + opsToBundle.map(({ userOperation }) => userOperation) ) let opts: SendTransactionOptions @@ -838,8 +732,8 @@ export class Executor { } } - const userOps = opsWithHashToBundle.map(({ userOperation }) => { - if (isUserOpVersion06) { + const userOps = opsToBundle.map(({ userOperation }) => { + if (isUserOpV06) { return userOperation } @@ -850,13 +744,13 @@ export class Executor { txParam: { ops: userOps, isReplacementTx: false, - isUserOpVersion06, + isUserOpVersion06: isUserOpV06, entryPoint }, opts }) - opsWithHashToBundle.map(({ userOperationHash }) => { + opsToBundle.map(({ userOperationHash }) => { this.eventManager.emitSubmitted( userOperationHash, transactionHash @@ -870,17 +764,11 @@ export class Executor { "insufficient funds, not submitting transaction" ) this.markWalletProcessed(wallet) - return opsWithHashToBundle.map((owh) => { - return { - status: "resubmit", - info: { - entryPoint, - userOpHash: owh.userOperationHash, - userOperation: owh.userOperation, - reason: InsufficientFundsError.name - } - } - }) + return { + status: "resubmit", + reason: InsufficientFundsError.name, + userOperations: opsToBundle.map((owh) => owh.userOperation) + } } sentry.captureException(err) @@ -889,20 +777,14 @@ export class Executor { "error submitting bundle transaction" ) this.markWalletProcessed(wallet) - return opsWithHashes.map((owh) => { - return { - status: "failure", - error: { - entryPoint, - userOpHash: owh.userOperationHash, - userOperation: owh.userOperation, - reason: "INTERNAL FAILURE" - } - } - }) + return { + status: "failure", + reason: "INTERNAL FAILURE", + userOperations: opsWithHashes.map((owh) => owh.userOperation) + } } - const userOperationInfos = opsWithHashToBundle.map((op) => { + const userOperationInfos = opsToBundle.map((op) => { return { entryPoint, userOperation: op.userOperation, @@ -914,7 +796,7 @@ export class Executor { const transactionInfo: TransactionInfo = { entryPoint, - isVersion06: isUserOpVersion06, + isVersion06: isUserOpV06, transactionHash: transactionHash, previousTransactionHashes: [], transactionRequest: { @@ -933,10 +815,14 @@ export class Executor { timesPotentiallyIncluded: 0 } - const userOperationResults: BundleResult[] = simulatedOpsToResults( - simulatedOps, + const userOperationResults: BundleResult = { + status: "success", + userOperations: opsToBundle.map((sop) => sop.userOperation), + rejectedUserOperations: failedOps.map( + (sop) => sop.userOperationWithHash.userOperation + ), transactionInfo - ) + } childLogger.info( { @@ -945,9 +831,7 @@ export class Executor { abi: undefined }, txHash: transactionHash, - opHashes: opsWithHashToBundle.map( - (owh) => owh.userOperationHash - ) + opHashes: opsToBundle.map((owh) => owh.userOperationHash) }, "submitted bundle transaction" ) diff --git a/src/executor/executorManager.ts b/src/executor/executorManager.ts index 09fdb265..ca8f2f38 100644 --- a/src/executor/executorManager.ts +++ b/src/executor/executorManager.ts @@ -209,12 +209,10 @@ export class ExecutorManager { return txHashes } - async sendToExecutor(entryPoint: Address, mempoolOps: UserOperation[]) { - const ops = mempoolOps.map((op) => op as UserOperation) - + async sendToExecutor(entryPoint: Address, userOps: UserOperation[]) { const bundles: BundleResult[][] = [] - if (ops.length > 0) { - bundles.push(await this.executor.bundle(entryPoint, ops)) + if (userOps.length > 0) { + bundles.push(await this.executor.bundle(entryPoint, userOps)) } for (const bundle of bundles) { @@ -244,7 +242,7 @@ export class ExecutorManager { const results = bundles.flat() - const filteredOutOps = mempoolOps.length - results.length + const filteredOutOps = userOps.length - results.length if (filteredOutOps > 0) { this.logger.debug( { filteredOutOps }, @@ -870,6 +868,7 @@ export class ExecutorManager { "user operation rejected" ) + this.executor.markWalletProcessed(txInfo.executor) this.mempool.removeSubmitted(opInfo.userOperationHash) }) diff --git a/src/executor/utils.ts b/src/executor/utils.ts index 554af530..684b1bc5 100644 --- a/src/executor/utils.ts +++ b/src/executor/utils.ts @@ -1,11 +1,9 @@ import type { InterfaceReputationManager } from "@alto/mempool" import { - type BundleResult, EntryPointV06Abi, EntryPointV07Abi, type FailedOp, type FailedOpWithRevert, - type TransactionInfo, type UserOperation, type UserOperationV07, type UserOperationWithHash, @@ -15,7 +13,6 @@ import { import type { Logger } from "@alto/utils" import { getRevertErrorData, - isVersion06, parseViemError, scaleBigIntByPercent, toPackedUserOperation @@ -24,7 +21,6 @@ import { import * as sentry from "@sentry/node" import { type Account, - type Address, type Chain, ContractFunctionRevertedError, EstimateGasExecutionError, @@ -38,6 +34,8 @@ import { BaseError } from "viem" import { SignedAuthorizationList } from "viem/experimental" +import { AltoConfig } from "../createConfig" +import { z } from "zod" export const isTransactionUnderpricedError = (e: BaseError) => { return e?.details @@ -60,194 +58,165 @@ export const getAuthorizationList = ( return authorizationList.length > 0 ? authorizationList : undefined } -export function simulatedOpsToResults( - simulatedOps: { - owh: UserOperationWithHash - reason: string | undefined - }[], - transactionInfo: TransactionInfo -): BundleResult[] { - return simulatedOps.map(({ reason, owh }) => { - if (reason === undefined) { - return { - status: "success", - value: { - userOperation: { - entryPoint: transactionInfo.entryPoint, - userOperation: owh.userOperation, - userOperationHash: owh.userOperationHash, - lastReplaced: Date.now(), - firstSubmitted: Date.now() - }, - transactionInfo - } - } - } - return { - status: "failure", - error: { - entryPoint: transactionInfo.entryPoint, - userOperation: owh.userOperation, - userOpHash: owh.userOperationHash, - reason: reason as string - } - } - }) +type FailedOpWithReason = { + userOperationWithHash: UserOperationWithHash + reason: string } -export type DefaultFilterOpsAndEstimateGasParams = {} - -export async function filterOpsAndEstimateGas( - entryPoint: Address, +export type FilterOpsAndEstimateGasResult = + | { + status: "success" + opsToBundle: UserOperationWithHash[] + failedOps: FailedOpWithReason[] + gasLimit: bigint + } + | { + status: "unexpectedFailure" + reason: string + } + +// Attempt to create a handleOps bundle + estimate bundling tx gas. +export async function filterOpsAndEstimateGas({ + ep, + isUserOpV06, + wallet, + ops, + nonce, + maxFeePerGas, + maxPriorityFeePerGas, + reputationManager, + config, + logger +}: { ep: GetContractReturnType< typeof EntryPointV06Abi | typeof EntryPointV07Abi, { public: PublicClient wallet: WalletClient } - >, - wallet: Account, - ops: UserOperationWithHash[], - nonce: number, - maxFeePerGas: bigint, - maxPriorityFeePerGas: bigint, - blockTag: "latest" | "pending" | undefined, - onlyPre1559: boolean, - fixedGasLimitForEstimation: bigint | undefined, - reputationManager: InterfaceReputationManager, - logger: Logger, - authorizationList?: SignedAuthorizationList -) { - const simulatedOps: { - owh: UserOperationWithHash - reason: string | undefined - }[] = ops.map((owh) => { - return { owh, reason: undefined } - }) - - let gasLimit: bigint + > + isUserOpV06: boolean + wallet: Account + ops: UserOperationWithHash[] + nonce: number + maxFeePerGas: bigint + maxPriorityFeePerGas: bigint + reputationManager: InterfaceReputationManager + config: AltoConfig + logger: Logger +}): Promise { + let { legacyTransactions, fixedGasLimitForEstimation, blockTagSupport } = + config - const isUserOpV06 = isVersion06( - simulatedOps[0].owh.userOperation as UserOperation - ) + // Keep track of invalid and valid ops + const opsToBundle = [...ops] + const failedOps: FailedOpWithReason[] = [] - const gasOptions = onlyPre1559 + // Prepare bundling tx params + const gasOptions = legacyTransactions ? { gasPrice: maxFeePerGas } : { maxFeePerGas, maxPriorityFeePerGas } + const blockTag = blockTagSupport ? "latest" : undefined - let fixedEstimationGasLimit: bigint | undefined = fixedGasLimitForEstimation + let gasLimit: bigint let retriesLeft = 5 - while (simulatedOps.filter((op) => op.reason === undefined).length > 0) { + while (opsToBundle.length > 0 && retriesLeft > 0) { try { - const opsToSend = simulatedOps - .filter((op) => op.reason === undefined) - .map(({ owh }) => { - const op = owh.userOperation - return isUserOpV06 - ? op - : toPackedUserOperation(op as UserOperationV07) - }) + const encodedOps = opsToBundle.map(({ userOperation }) => { + return isUserOpV06 + ? userOperation + : toPackedUserOperation(userOperation as UserOperationV07) + }) + + const authorizationList = getAuthorizationList( + opsToBundle.map((owh) => owh.userOperation) + ) gasLimit = await ep.estimateGas.handleOps( // @ts-ignore - ep is set correctly for opsToSend, but typescript doesn't know that - [opsToSend, wallet.address], + [encodedOps, wallet.address], { account: wallet, nonce: nonce, - blockTag: blockTag, - ...(fixedEstimationGasLimit !== undefined && { - gas: fixedEstimationGasLimit + blockTag, + ...(fixedGasLimitForEstimation && { + gas: fixedGasLimitForEstimation }), - ...(authorizationList !== undefined && { + ...(authorizationList && { authorizationList }), ...gasOptions } ) - return { simulatedOps, gasLimit } + return { + status: "success", + opsToBundle, + failedOps, + gasLimit + } } catch (err: unknown) { - logger.error({ err, blockTag }, "error estimating gas") + logger.error({ err, blockTag }, "handling error estimating gas") const e = parseViemError(err) if (e instanceof ContractFunctionRevertedError) { - const failedOpError = failedOpErrorSchema.safeParse(e.data) - const failedOpWithRevertError = - failedOpWithRevertErrorSchema.safeParse(e.data) - - let errorData: FailedOp | FailedOpWithRevert | undefined = - undefined + let parseResult = z + .union([failedOpErrorSchema, failedOpWithRevertErrorSchema]) + .safeParse(e.data) - if (failedOpError.success) { - errorData = failedOpError.data.args - } - if (failedOpWithRevertError.success) { - errorData = failedOpWithRevertError.data.args + if (!parseResult.success) { + sentry.captureException(err) + logger.error( + { + error: parseResult.error + }, + "failed to parse failedOpError" + ) + return { + status: "unexpectedFailure", + reason: "failed to parse failedOpError" + } } + const errorData = parseResult.data.args + if (errorData) { - if ( - errorData.reason.indexOf("AA95 out of gas") !== -1 && - retriesLeft > 0 - ) { - retriesLeft-- - fixedEstimationGasLimit = scaleBigIntByPercent( - fixedEstimationGasLimit || BigInt(30_000_000), + if (errorData.reason.includes("AA95 out of gas")) { + fixedGasLimitForEstimation = scaleBigIntByPercent( + fixedGasLimitForEstimation || BigInt(30_000_000), 110n ) + retriesLeft-- continue } - logger.debug( - { - errorData, - userOpHashes: simulatedOps - .filter((op) => op.reason === undefined) - .map((op) => op.owh.userOperationHash) - }, - "user op in batch invalid" - ) - - const failingOp = simulatedOps.filter( - (op) => op.reason === undefined - )[Number(errorData.opIndex)] - - failingOp.reason = `${errorData.reason}${ - (errorData as FailedOpWithRevert)?.inner - ? ` - ${(errorData as FailedOpWithRevert).inner}` - : "" - }` + const failingOp = { + userOperationWithHash: + opsToBundle[Number(errorData.opIndex)], + reason: `${errorData.reason}${ + (errorData as FailedOpWithRevert)?.inner + ? ` - ${ + (errorData as FailedOpWithRevert).inner + }` + : "" + }` + } + opsToBundle.splice(Number(errorData.opIndex), 1) reputationManager.crashedHandleOps( - failingOp.owh.userOperation, - entryPoint, + failingOp.userOperationWithHash.userOperation, + ep.address, failingOp.reason ) - } - if ( - !(failedOpError.success || failedOpWithRevertError.success) - ) { - sentry.captureException(err) - logger.error( - { - error: `${failedOpError.error} ${failedOpWithRevertError.error}` - }, - "failed to parse failedOpError" - ) - return { - simulatedOps: [], - gasLimit: 0n - } + failedOps.push(failingOp) } } else if ( e instanceof EstimateGasExecutionError || err instanceof EstimateGasExecutionError ) { - if (e?.cause instanceof FeeCapTooLowError && retriesLeft > 0) { - retriesLeft-- - + if (e?.cause instanceof FeeCapTooLowError) { logger.info( { error: e.shortMessage }, "error estimating gas due to max fee < basefee" @@ -272,6 +241,7 @@ export async function filterOpsAndEstimateGas( 125n ) } + retriesLeft-- continue } @@ -287,16 +257,6 @@ export async function filterOpsAndEstimateGas( abi: isUserOpV06 ? EntryPointV06Abi : EntryPointV07Abi, data: errorHexData }) - logger.debug( - { - errorName: errorResult.errorName, - args: errorResult.args, - userOpHashes: simulatedOps - .filter((op) => op.reason === undefined) - .map((op) => op.owh.userOperationHash) - }, - "user op in batch invalid" - ) if ( errorResult.errorName !== "FailedOpWithRevert" && @@ -310,24 +270,29 @@ export async function filterOpsAndEstimateGas( "unexpected error result" ) return { - simulatedOps: [], - gasLimit: 0n + status: "unexpectedFailure", + reason: "unexpected error result" } } - const failingOp = simulatedOps.filter( - (op) => op.reason === undefined - )[Number(errorResult.args[0])] + const failedOpIndex = Number(errorResult.args[0]) + const failingOp = { + userOperationWithHash: opsToBundle[failedOpIndex], + reason: errorResult.args[1] + } + + failedOps.push(failingOp) + opsToBundle.splice(Number(errorResult.args[0]), 1) - failingOp.reason = errorResult.args[1] + continue } catch (e: unknown) { logger.error( { error: JSON.stringify(err) }, "failed to parse error result" ) return { - simulatedOps: [], - gasLimit: 0n + status: "unexpectedFailure", + reason: "failed to parse error result" } } } else { @@ -336,11 +301,15 @@ export async function filterOpsAndEstimateGas( { error: JSON.stringify(err), blockTag }, "error estimating gas" ) - return { simulatedOps: [], gasLimit: 0n } + return { + status: "unexpectedFailure", + reason: "error estimating gas" + } } } } - return { simulatedOps, gasLimit: 0n } + + return { status: "unexpectedFailure", reason: "All ops failed simulation" } } export async function flushStuckTransaction( diff --git a/src/handlers/eventManager.ts b/src/handlers/eventManager.ts index 83ce0aaa..e24e2298 100644 --- a/src/handlers/eventManager.ts +++ b/src/handlers/eventManager.ts @@ -164,14 +164,16 @@ export class EventManager { } // emits when the userOperation has been submitted to the network - async emitSubmitted(userOperationHash: Hex, transactionHash: Hex) { - await this.emitEvent({ - userOperationHash, - event: { - eventType: "submitted", - transactionHash - } - }) + async emitSubmitted(userOperationHashes: Hex[], transactionHash: Hex) { + for (const userOperationHash of userOperationHashes) { + await this.emitEvent({ + userOperationHash, + event: { + eventType: "submitted", + transactionHash + } + }) + } } // emits when the userOperation was dropped from the internal mempool diff --git a/src/mempool/mempool.ts b/src/mempool/mempool.ts index 6413ae1c..21cfda84 100644 --- a/src/mempool/mempool.ts +++ b/src/mempool/mempool.ts @@ -671,6 +671,7 @@ export class MemoryMempool { } } + // Returns a bundle of userOperations in array format. async process( maxGasLimit: bigint, minOps?: number @@ -737,16 +738,16 @@ export class MemoryMempool { senders, storageMap ) + if (skipResult.skip) { + continue + } + paymasterDeposit = skipResult.paymasterDeposit stakedEntityCount = skipResult.stakedEntityCount knownEntities = skipResult.knownEntities senders = skipResult.senders storageMap = skipResult.storageMap - if (skipResult.skip) { - continue - } - this.reputationManager.decreaseUserOperationCount(op) this.store.removeOutstanding(opInfo.userOperationHash) this.store.addProcessing(opInfo) diff --git a/src/types/mempool.ts b/src/types/mempool.ts index 3abd06cf..8b763aad 100644 --- a/src/types/mempool.ts +++ b/src/types/mempool.ts @@ -33,8 +33,8 @@ export type TransactionInfo = { export type UserOperationInfo = { userOperation: UserOperation - entryPoint: Address userOperationHash: HexData32 + entryPoint: Address lastReplaced: number firstSubmitted: number referencedContracts?: ReferencedCodeHashes @@ -52,38 +52,20 @@ export type SubmittedUserOperation = { transactionInfo: TransactionInfo } -type Result = Success | Failure | Resubmit - -interface Success { - status: "success" - value: T -} - -interface Failure { - status: "failure" - error: E -} - -interface Resubmit { - status: "resubmit" - info: R -} - -export type BundleResult = Result< - { - userOperation: UserOperationInfo - transactionInfo: TransactionInfo - }, - { - reason: string - userOpHash: HexData32 - entryPoint: Address - userOperation: UserOperation - }, - { - reason: string - userOpHash: HexData32 - entryPoint: Address - userOperation: UserOperation - } -> +export type BundleResult = + | { + status: "success" + userOperations: UserOperation[] + rejectedUserOperations: UserOperation[] + transactionInfo: TransactionInfo + } + | { + status: "failure" + reason: string + userOperations: UserOperation[] + } + | { + status: "resubmit" + reason: string + userOperations: UserOperation[] + } From 48e1bcbce62aca08365824e563fafef5970dd6db Mon Sep 17 00:00:00 2001 From: mouseless <97399882+mouseless-eth@users.noreply.github.com> Date: Sun, 26 Jan 2025 04:32:26 +0000 Subject: [PATCH 02/34] move filterOpsAndEstimateGas to own file --- src/executor/executor.ts | 159 ++++++------- src/executor/filterOpsAndEStimateGas.ts | 282 ++++++++++++++++++++++++ src/executor/utils.ts | 281 +---------------------- 3 files changed, 347 insertions(+), 375 deletions(-) create mode 100644 src/executor/filterOpsAndEStimateGas.ts diff --git a/src/executor/executor.ts b/src/executor/executor.ts index 2df99b84..2b620f52 100644 --- a/src/executor/executor.ts +++ b/src/executor/executor.ts @@ -38,7 +38,6 @@ import { NonceTooHighError } from "viem" import { - filterOpsAndEstimateGas, flushStuckTransaction, isTransactionUnderpricedError, getAuthorizationList @@ -47,6 +46,7 @@ import type { SendTransactionErrorType } from "viem" import type { AltoConfig } from "../createConfig" import type { SendTransactionOptions } from "./types" import { sendPflConditional } from "./fastlane" +import { filterOpsAndEstimateGas } from "./filterOpsAndEStimateGas" export interface GasEstimateResult { preverificationGas: bigint @@ -133,7 +133,13 @@ export class Executor { async replaceTransaction( transactionInfo: TransactionInfo ): Promise { - const newRequest = { ...transactionInfo.transactionRequest } + const { + isVersion06, + entryPoint, + transactionRequest, + executor, + userOperationInfos + } = transactionInfo let gasPriceParameters: GasPriceParameters try { @@ -144,51 +150,25 @@ export class Executor { return { status: "failed" } } - newRequest.maxFeePerGas = scaleBigIntByPercent( - gasPriceParameters.maxFeePerGas, - 115n - ) - newRequest.maxPriorityFeePerGas = scaleBigIntByPercent( - gasPriceParameters.maxPriorityFeePerGas, - 115n - ) - newRequest.account = transactionInfo.executor - - const opsWithHashes = transactionInfo.userOperationInfos.map( - (opInfo) => { - const op = opInfo.userOperation - return { - userOperation: opInfo.userOperation, - userOperationHash: getUserOperationHash( - op, - transactionInfo.entryPoint, - this.config.walletClient.chain.id - ), - entryPoint: opInfo.entryPoint - } - } - ) + const newRequest = { + ...transactionRequest, + account: executor, + maxFeePerGas: scaleBigIntByPercent( + gasPriceParameters.maxFeePerGas, + 115n + ), + maxPriorityFeePerGas: scaleBigIntByPercent( + gasPriceParameters.maxPriorityFeePerGas, + 115n + ) + } - const [isUserOpV06, entryPoint] = opsWithHashes.reduce( - (acc, owh) => { - if ( - acc[0] !== isVersion06(owh.userOperation) || - acc[1] !== owh.entryPoint - ) { - throw new Error( - "All user operations must be of the same version" - ) - } - return acc - }, - [ - isVersion06(opsWithHashes[0].userOperation), - opsWithHashes[0].entryPoint - ] + const opsToResubmit = userOperationInfos.map( + (optr) => optr.userOperation ) const ep = getContract({ - abi: isUserOpV06 ? EntryPointV06Abi : EntryPointV07Abi, + abi: isVersion06 ? EntryPointV06Abi : EntryPointV07Abi, address: entryPoint, client: { public: this.config.publicClient, @@ -203,9 +183,9 @@ export class Executor { let bundleResult = await filterOpsAndEstimateGas({ ep, - isUserOpV06, + isUserOpV06: isVersion06, wallet: newRequest.account, - ops: opsWithHashes, + ops: opsToResubmit, nonce: newRequest.nonce, maxFeePerGas: newRequest.maxFeePerGas, maxPriorityFeePerGas: newRequest.maxPriorityFeePerGas, @@ -251,13 +231,11 @@ export class Executor { let txParam: HandleOpsTxParam const userOps = opsToBundle.map((op) => - isUserOpV06 - ? op.userOperation - : toPackedUserOperation(op.userOperation as UserOperationV07) + isVersion06 ? op : toPackedUserOperation(op as UserOperationV07) ) as PackedUserOperation[] txParam = { - isUserOpVersion06: isUserOpV06, + isUserOpVersion06: isVersion06, isReplacementTx: true, ops: userOps, entryPoint: transactionInfo.entryPoint @@ -272,9 +250,7 @@ export class Executor { chain: undefined }, executor: newRequest.account.address, - ooooops: opsToBundle.map( - (opInfo) => opInfo.userOperationHash - ) + userOperations: this.getOpHashes(opsToBundle) }, "replacing transaction" ) @@ -298,7 +274,7 @@ export class Executor { }) this.eventManager.emitSubmitted( - opsToBundle.map((op) => op.userOperationHash), + this.getOpHashes(opsToBundle), txHash ) @@ -311,13 +287,13 @@ export class Executor { ...transactionInfo.previousTransactionHashes ], lastReplaced: Date.now(), - userOperationInfos: opsToBundle.map((opInfo) => { + userOperationInfos: opsToBundle.map((op) => { return { - entryPoint: opInfo.entryPoint, - userOperation: opInfo.userOperation, - userOperationHash: opInfo.userOperationHash, + entryPoint, + userOperation: op, + userOperationHash: this.getOpHashes([op])[0], lastReplaced: Date.now(), - firstSubmitted: opInfo.firstSubmitted + firstSubmitted: transactionInfo.firstSubmitted } }) } @@ -367,6 +343,16 @@ export class Executor { } } + getOpHashes(userOperations: UserOperation[]): HexData32[] { + return userOperations.map((userOperation) => { + return getUserOperationHash( + userOperation, + this.config.entrypoints[0], + this.config.publicClient.chain.id + ) + }) + } + async flushStuckTransactions(): Promise { const allWallets = new Set(this.senderManager.wallets) @@ -587,17 +573,6 @@ export class Executor { ): Promise { const wallet = await this.senderManager.getWallet() - const opsWithHashes = ops.map((userOperation) => { - return { - userOperation, - userOperationHash: getUserOperationHash( - userOperation, - entryPoint, - this.config.walletClient.chain.id - ) - } - }) - // Find bundle EntryPoint version. const firstOpVersion = isVersion06(ops[0]) const allSameVersion = ops.every( @@ -618,7 +593,7 @@ export class Executor { }) let childLogger = this.logger.child({ - userOperations: opsWithHashes.map((oh) => oh.userOperationHash), + userOperations: this.getOpHashes(ops), entryPoint }) childLogger.debug("bundling user operation") @@ -643,7 +618,7 @@ export class Executor { return { status: "resubmit", reason: "Failed to get parameters for bundling", - userOperations: opsWithHashes.map((owh) => owh.userOperation) + userOperations: ops } } @@ -651,7 +626,7 @@ export class Executor { isUserOpV06, ep, wallet, - ops: opsWithHashes, + ops, nonce, maxFeePerGas: gasPriceParameters.maxFeePerGas, maxPriorityFeePerGas: gasPriceParameters.maxPriorityFeePerGas, @@ -686,7 +661,7 @@ export class Executor { } childLogger = this.logger.child({ - userOperations: opsToBundle.map((owh) => owh.userOperationHash), + userOperations: this.getOpHashes(opsToBundle), entryPoint }) @@ -696,9 +671,7 @@ export class Executor { let transactionHash: HexData32 try { const isLegacyTransaction = this.config.legacyTransactions - const authorizationList = getAuthorizationList( - opsToBundle.map(({ userOperation }) => userOperation) - ) + const authorizationList = getAuthorizationList(opsToBundle) let opts: SendTransactionOptions if (isLegacyTransaction) { @@ -732,12 +705,12 @@ export class Executor { } } - const userOps = opsToBundle.map(({ userOperation }) => { + // TODO: move this to a seperate utility + const userOps = opsToBundle.map((op) => { if (isUserOpV06) { - return userOperation + return op } - - return toPackedUserOperation(userOperation as UserOperationV07) + return toPackedUserOperation(op as UserOperationV07) }) as PackedUserOperation[] transactionHash = await this.sendHandleOpsTransaction({ @@ -750,12 +723,10 @@ export class Executor { opts }) - opsToBundle.map(({ userOperationHash }) => { - this.eventManager.emitSubmitted( - userOperationHash, - transactionHash - ) - }) + this.eventManager.emitSubmitted( + this.getOpHashes(opsToBundle), + transactionHash + ) } catch (err: unknown) { const e = parseViemError(err) if (e instanceof InsufficientFundsError) { @@ -767,7 +738,7 @@ export class Executor { return { status: "resubmit", reason: InsufficientFundsError.name, - userOperations: opsToBundle.map((owh) => owh.userOperation) + userOperations: ops } } @@ -780,15 +751,15 @@ export class Executor { return { status: "failure", reason: "INTERNAL FAILURE", - userOperations: opsWithHashes.map((owh) => owh.userOperation) + userOperations: ops } } const userOperationInfos = opsToBundle.map((op) => { return { entryPoint, - userOperation: op.userOperation, - userOperationHash: op.userOperationHash, + userOperation: op, + userOperationHash: this.getOpHashes([op])[0], lastReplaced: Date.now(), firstSubmitted: Date.now() } @@ -817,10 +788,8 @@ export class Executor { const userOperationResults: BundleResult = { status: "success", - userOperations: opsToBundle.map((sop) => sop.userOperation), - rejectedUserOperations: failedOps.map( - (sop) => sop.userOperationWithHash.userOperation - ), + userOperations: opsToBundle, + rejectedUserOperations: failedOps.map((sop) => sop.userOperation), transactionInfo } @@ -831,7 +800,7 @@ export class Executor { abi: undefined }, txHash: transactionHash, - opHashes: opsToBundle.map((owh) => owh.userOperationHash) + opHashes: this.getOpHashes(opsToBundle) }, "submitted bundle transaction" ) diff --git a/src/executor/filterOpsAndEStimateGas.ts b/src/executor/filterOpsAndEStimateGas.ts new file mode 100644 index 00000000..ec29601b --- /dev/null +++ b/src/executor/filterOpsAndEStimateGas.ts @@ -0,0 +1,282 @@ +import { InterfaceReputationManager } from "@alto/mempool" +import { + EntryPointV06Abi, + EntryPointV07Abi, + FailedOpWithRevert, + UserOperation, + UserOperationV07, + failedOpErrorSchema, + failedOpWithRevertErrorSchema +} from "@alto/types" +import { + Account, + ContractFunctionRevertedError, + EstimateGasExecutionError, + FeeCapTooLowError, + GetContractReturnType, + Hex, + PublicClient, + WalletClient, + decodeErrorResult +} from "viem" +import { AltoConfig } from "../createConfig" +import { + Logger, + getRevertErrorData, + parseViemError, + scaleBigIntByPercent, + toPackedUserOperation +} from "@alto/utils" +import { z } from "zod" +import { getAuthorizationList } from "./utils" +import * as sentry from "@sentry/node" + +type FailedOpWithReason = { + userOperation: UserOperation + reason: string +} + +export type FilterOpsAndEstimateGasResult = + | { + status: "success" + opsToBundle: UserOperation[] + failedOps: FailedOpWithReason[] + gasLimit: bigint + } + | { + status: "unexpectedFailure" + reason: string + } + +// Attempt to create a handleOps bundle + estimate bundling tx gas. +export async function filterOpsAndEstimateGas({ + ep, + isUserOpV06, + wallet, + ops, + nonce, + maxFeePerGas, + maxPriorityFeePerGas, + reputationManager, + config, + logger +}: { + ep: GetContractReturnType< + typeof EntryPointV06Abi | typeof EntryPointV07Abi, + { + public: PublicClient + wallet: WalletClient + } + > + isUserOpV06: boolean + wallet: Account + ops: UserOperation[] + nonce: number + maxFeePerGas: bigint + maxPriorityFeePerGas: bigint + reputationManager: InterfaceReputationManager + config: AltoConfig + logger: Logger +}): Promise { + let { legacyTransactions, fixedGasLimitForEstimation, blockTagSupport } = + config + + // Keep track of invalid and valid ops + const opsToBundle = [...ops] + const failedOps: FailedOpWithReason[] = [] + + // Prepare bundling tx params + const gasOptions = legacyTransactions + ? { gasPrice: maxFeePerGas } + : { maxFeePerGas, maxPriorityFeePerGas } + const blockTag = blockTagSupport ? "latest" : undefined + + let gasLimit: bigint + let retriesLeft = 5 + + while (opsToBundle.length > 0 && retriesLeft > 0) { + try { + const encodedOps = opsToBundle.map((userOperation) => { + return isUserOpV06 + ? userOperation + : toPackedUserOperation(userOperation as UserOperationV07) + }) + + const authorizationList = getAuthorizationList(opsToBundle) + + gasLimit = await ep.estimateGas.handleOps( + // @ts-ignore - ep is set correctly for opsToSend, but typescript doesn't know that + [encodedOps, wallet.address], + { + account: wallet, + nonce: nonce, + blockTag, + ...(fixedGasLimitForEstimation && { + gas: fixedGasLimitForEstimation + }), + ...(authorizationList && { + authorizationList + }), + ...gasOptions + } + ) + + return { + status: "success", + opsToBundle, + failedOps, + gasLimit + } + } catch (err: unknown) { + logger.error({ err, blockTag }, "handling error estimating gas") + const e = parseViemError(err) + + if (e instanceof ContractFunctionRevertedError) { + let parseResult = z + .union([failedOpErrorSchema, failedOpWithRevertErrorSchema]) + .safeParse(e.data) + + if (!parseResult.success) { + sentry.captureException(err) + logger.error( + { + error: parseResult.error + }, + "failed to parse failedOpError" + ) + return { + status: "unexpectedFailure", + reason: "failed to parse failedOpError" + } + } + + const errorData = parseResult.data.args + + if (errorData) { + if (errorData.reason.includes("AA95 out of gas")) { + fixedGasLimitForEstimation = scaleBigIntByPercent( + fixedGasLimitForEstimation || BigInt(30_000_000), + 110n + ) + retriesLeft-- + continue + } + + const innerError = (errorData as FailedOpWithRevert)?.inner + const reason = innerError + ? `${errorData.reason} - ${innerError}` + : errorData.reason + + const failingOp = { + userOperation: opsToBundle[Number(errorData.opIndex)], + reason + } + opsToBundle.splice(Number(errorData.opIndex), 1) + + reputationManager.crashedHandleOps( + failingOp.userOperation, + ep.address, + failingOp.reason + ) + + failedOps.push(failingOp) + } + } else if ( + e instanceof EstimateGasExecutionError || + err instanceof EstimateGasExecutionError + ) { + if (e?.cause instanceof FeeCapTooLowError) { + logger.info( + { error: e.shortMessage }, + "error estimating gas due to max fee < basefee" + ) + + if ("gasPrice" in gasOptions) { + gasOptions.gasPrice = scaleBigIntByPercent( + gasOptions.gasPrice || maxFeePerGas, + 125n + ) + } + if ("maxFeePerGas" in gasOptions) { + gasOptions.maxFeePerGas = scaleBigIntByPercent( + gasOptions.maxFeePerGas || maxFeePerGas, + 125n + ) + } + if ("maxPriorityFeePerGas" in gasOptions) { + gasOptions.maxPriorityFeePerGas = scaleBigIntByPercent( + gasOptions.maxPriorityFeePerGas || + maxPriorityFeePerGas, + 125n + ) + } + retriesLeft-- + continue + } + + try { + let errorHexData: Hex = "0x" + + if (err instanceof EstimateGasExecutionError) { + errorHexData = getRevertErrorData(err) as Hex + } else { + errorHexData = e?.details.split("Reverted ")[1] as Hex + } + const errorResult = decodeErrorResult({ + abi: isUserOpV06 ? EntryPointV06Abi : EntryPointV07Abi, + data: errorHexData + }) + + if ( + errorResult.errorName !== "FailedOpWithRevert" && + errorResult.errorName !== "FailedOp" + ) { + logger.error( + { + errorName: errorResult.errorName, + args: errorResult.args + }, + "unexpected error result" + ) + return { + status: "unexpectedFailure", + reason: "unexpected error result" + } + } + + const failedOpIndex = Number(errorResult.args[0]) + const failingOp = { + userOperation: opsToBundle[failedOpIndex], + reason: errorResult.args[1] + } + + failedOps.push(failingOp) + opsToBundle.splice(Number(errorResult.args[0]), 1) + + continue + } catch (e: unknown) { + logger.error( + { error: JSON.stringify(err) }, + "failed to parse error result" + ) + return { + status: "unexpectedFailure", + reason: "failed to parse error result" + } + } + } else { + sentry.captureException(err) + logger.error( + { error: JSON.stringify(err), blockTag }, + "error estimating gas" + ) + return { + status: "unexpectedFailure", + reason: "error estimating gas" + } + } + } + } + + return { status: "unexpectedFailure", reason: "All ops failed simulation" } +} diff --git a/src/executor/utils.ts b/src/executor/utils.ts index 684b1bc5..ba003e20 100644 --- a/src/executor/utils.ts +++ b/src/executor/utils.ts @@ -1,41 +1,16 @@ -import type { InterfaceReputationManager } from "@alto/mempool" -import { - EntryPointV06Abi, - EntryPointV07Abi, - type FailedOp, - type FailedOpWithRevert, - type UserOperation, - type UserOperationV07, - type UserOperationWithHash, - failedOpErrorSchema, - failedOpWithRevertErrorSchema -} from "@alto/types" +import { type UserOperation } from "@alto/types" import type { Logger } from "@alto/utils" -import { - getRevertErrorData, - parseViemError, - scaleBigIntByPercent, - toPackedUserOperation -} from "@alto/utils" // biome-ignore lint/style/noNamespaceImport: explicitly make it clear when sentry is used import * as sentry from "@sentry/node" import { type Account, type Chain, - ContractFunctionRevertedError, - EstimateGasExecutionError, - FeeCapTooLowError, - type GetContractReturnType, - type Hex, type PublicClient, type Transport, type WalletClient, - decodeErrorResult, BaseError } from "viem" import { SignedAuthorizationList } from "viem/experimental" -import { AltoConfig } from "../createConfig" -import { z } from "zod" export const isTransactionUnderpricedError = (e: BaseError) => { return e?.details @@ -58,260 +33,6 @@ export const getAuthorizationList = ( return authorizationList.length > 0 ? authorizationList : undefined } -type FailedOpWithReason = { - userOperationWithHash: UserOperationWithHash - reason: string -} - -export type FilterOpsAndEstimateGasResult = - | { - status: "success" - opsToBundle: UserOperationWithHash[] - failedOps: FailedOpWithReason[] - gasLimit: bigint - } - | { - status: "unexpectedFailure" - reason: string - } - -// Attempt to create a handleOps bundle + estimate bundling tx gas. -export async function filterOpsAndEstimateGas({ - ep, - isUserOpV06, - wallet, - ops, - nonce, - maxFeePerGas, - maxPriorityFeePerGas, - reputationManager, - config, - logger -}: { - ep: GetContractReturnType< - typeof EntryPointV06Abi | typeof EntryPointV07Abi, - { - public: PublicClient - wallet: WalletClient - } - > - isUserOpV06: boolean - wallet: Account - ops: UserOperationWithHash[] - nonce: number - maxFeePerGas: bigint - maxPriorityFeePerGas: bigint - reputationManager: InterfaceReputationManager - config: AltoConfig - logger: Logger -}): Promise { - let { legacyTransactions, fixedGasLimitForEstimation, blockTagSupport } = - config - - // Keep track of invalid and valid ops - const opsToBundle = [...ops] - const failedOps: FailedOpWithReason[] = [] - - // Prepare bundling tx params - const gasOptions = legacyTransactions - ? { gasPrice: maxFeePerGas } - : { maxFeePerGas, maxPriorityFeePerGas } - const blockTag = blockTagSupport ? "latest" : undefined - - let gasLimit: bigint - let retriesLeft = 5 - - while (opsToBundle.length > 0 && retriesLeft > 0) { - try { - const encodedOps = opsToBundle.map(({ userOperation }) => { - return isUserOpV06 - ? userOperation - : toPackedUserOperation(userOperation as UserOperationV07) - }) - - const authorizationList = getAuthorizationList( - opsToBundle.map((owh) => owh.userOperation) - ) - - gasLimit = await ep.estimateGas.handleOps( - // @ts-ignore - ep is set correctly for opsToSend, but typescript doesn't know that - [encodedOps, wallet.address], - { - account: wallet, - nonce: nonce, - blockTag, - ...(fixedGasLimitForEstimation && { - gas: fixedGasLimitForEstimation - }), - ...(authorizationList && { - authorizationList - }), - ...gasOptions - } - ) - - return { - status: "success", - opsToBundle, - failedOps, - gasLimit - } - } catch (err: unknown) { - logger.error({ err, blockTag }, "handling error estimating gas") - const e = parseViemError(err) - - if (e instanceof ContractFunctionRevertedError) { - let parseResult = z - .union([failedOpErrorSchema, failedOpWithRevertErrorSchema]) - .safeParse(e.data) - - if (!parseResult.success) { - sentry.captureException(err) - logger.error( - { - error: parseResult.error - }, - "failed to parse failedOpError" - ) - return { - status: "unexpectedFailure", - reason: "failed to parse failedOpError" - } - } - - const errorData = parseResult.data.args - - if (errorData) { - if (errorData.reason.includes("AA95 out of gas")) { - fixedGasLimitForEstimation = scaleBigIntByPercent( - fixedGasLimitForEstimation || BigInt(30_000_000), - 110n - ) - retriesLeft-- - continue - } - - const failingOp = { - userOperationWithHash: - opsToBundle[Number(errorData.opIndex)], - reason: `${errorData.reason}${ - (errorData as FailedOpWithRevert)?.inner - ? ` - ${ - (errorData as FailedOpWithRevert).inner - }` - : "" - }` - } - opsToBundle.splice(Number(errorData.opIndex), 1) - - reputationManager.crashedHandleOps( - failingOp.userOperationWithHash.userOperation, - ep.address, - failingOp.reason - ) - - failedOps.push(failingOp) - } - } else if ( - e instanceof EstimateGasExecutionError || - err instanceof EstimateGasExecutionError - ) { - if (e?.cause instanceof FeeCapTooLowError) { - logger.info( - { error: e.shortMessage }, - "error estimating gas due to max fee < basefee" - ) - - if ("gasPrice" in gasOptions) { - gasOptions.gasPrice = scaleBigIntByPercent( - gasOptions.gasPrice || maxFeePerGas, - 125n - ) - } - if ("maxFeePerGas" in gasOptions) { - gasOptions.maxFeePerGas = scaleBigIntByPercent( - gasOptions.maxFeePerGas || maxFeePerGas, - 125n - ) - } - if ("maxPriorityFeePerGas" in gasOptions) { - gasOptions.maxPriorityFeePerGas = scaleBigIntByPercent( - gasOptions.maxPriorityFeePerGas || - maxPriorityFeePerGas, - 125n - ) - } - retriesLeft-- - continue - } - - try { - let errorHexData: Hex = "0x" - - if (err instanceof EstimateGasExecutionError) { - errorHexData = getRevertErrorData(err) as Hex - } else { - errorHexData = e?.details.split("Reverted ")[1] as Hex - } - const errorResult = decodeErrorResult({ - abi: isUserOpV06 ? EntryPointV06Abi : EntryPointV07Abi, - data: errorHexData - }) - - if ( - errorResult.errorName !== "FailedOpWithRevert" && - errorResult.errorName !== "FailedOp" - ) { - logger.error( - { - errorName: errorResult.errorName, - args: errorResult.args - }, - "unexpected error result" - ) - return { - status: "unexpectedFailure", - reason: "unexpected error result" - } - } - - const failedOpIndex = Number(errorResult.args[0]) - const failingOp = { - userOperationWithHash: opsToBundle[failedOpIndex], - reason: errorResult.args[1] - } - - failedOps.push(failingOp) - opsToBundle.splice(Number(errorResult.args[0]), 1) - - continue - } catch (e: unknown) { - logger.error( - { error: JSON.stringify(err) }, - "failed to parse error result" - ) - return { - status: "unexpectedFailure", - reason: "failed to parse error result" - } - } - } else { - sentry.captureException(err) - logger.error( - { error: JSON.stringify(err), blockTag }, - "error estimating gas" - ) - return { - status: "unexpectedFailure", - reason: "error estimating gas" - } - } - } - } - - return { status: "unexpectedFailure", reason: "All ops failed simulation" } -} - export async function flushStuckTransaction( publicClient: PublicClient, walletClient: WalletClient, From 24facc0c317a0e02cde1a9be4e826dd2a645c2cf Mon Sep 17 00:00:00 2001 From: mouseless <97399882+mouseless-eth@users.noreply.github.com> Date: Sun, 26 Jan 2025 05:15:09 +0000 Subject: [PATCH 03/34] cleanup --- src/executor/executor.ts | 52 +++---- src/executor/executorManager.ts | 244 ++++++++++++++++++-------------- src/handlers/eventManager.ts | 7 +- src/rpc/rpcHandler.ts | 3 +- src/types/mempool.ts | 23 +-- 5 files changed, 188 insertions(+), 141 deletions(-) diff --git a/src/executor/executor.ts b/src/executor/executor.ts index 2b620f52..776ca488 100644 --- a/src/executor/executor.ts +++ b/src/executor/executor.ts @@ -137,7 +137,6 @@ export class Executor { isVersion06, entryPoint, transactionRequest, - executor, userOperationInfos } = transactionInfo @@ -152,7 +151,6 @@ export class Executor { const newRequest = { ...transactionRequest, - account: executor, maxFeePerGas: scaleBigIntByPercent( gasPriceParameters.maxFeePerGas, 115n @@ -178,7 +176,7 @@ export class Executor { const childLogger = this.logger.child({ transactionHash: transactionInfo.transactionHash, - executor: transactionInfo.executor.address + executor: transactionInfo.transactionRequest.account.address }) let bundleResult = await filterOpsAndEstimateGas({ @@ -273,10 +271,10 @@ export class Executor { } }) - this.eventManager.emitSubmitted( - this.getOpHashes(opsToBundle), - txHash - ) + this.eventManager.emitSubmitted({ + userOpHashes: this.getOpHashes(opsToBundle), + transactionHash: txHash + }) const newTxInfo: TransactionInfo = { ...transactionInfo, @@ -546,9 +544,10 @@ export class Executor { const conflictingOps = submitted .filter((submitted) => { const tx = submitted.transactionInfo + const txSender = tx.transactionRequest.account.address return ( - tx.executor.address === executor.address && + txSender === executor.address && tx.transactionRequest.nonce === nonce ) }) @@ -616,9 +615,9 @@ export class Executor { ) this.markWalletProcessed(wallet) return { - status: "resubmit", + status: "bundle_resubmit", reason: "Failed to get parameters for bundling", - userOperations: ops + userOpsBundled: ops } } @@ -641,9 +640,9 @@ export class Executor { ) this.markWalletProcessed(wallet) return { - status: "failure", + status: "bundle_failure", reason: "INTERNAL FAILURE", - userOperations: ops + userOpsBundled: ops } } @@ -653,13 +652,14 @@ export class Executor { childLogger.warn("all ops failed simulation") this.markWalletProcessed(wallet) return { - status: "failure", + status: "bundle_failure", reason: "INTERNAL FAILURE", // TODO: we want to log the failure reason - userOperations: ops + userOpsBundled: ops } } + // Update child logger with userOperations being sent for bundling. childLogger = this.logger.child({ userOperations: this.getOpHashes(opsToBundle), entryPoint @@ -723,10 +723,10 @@ export class Executor { opts }) - this.eventManager.emitSubmitted( - this.getOpHashes(opsToBundle), + this.eventManager.emitSubmitted({ + userOpHashes: this.getOpHashes(opsToBundle), transactionHash - ) + }) } catch (err: unknown) { const e = parseViemError(err) if (e instanceof InsufficientFundsError) { @@ -736,9 +736,9 @@ export class Executor { ) this.markWalletProcessed(wallet) return { - status: "resubmit", + status: "bundle_resubmit", reason: InsufficientFundsError.name, - userOperations: ops + userOpsBundled: ops } } @@ -749,9 +749,9 @@ export class Executor { ) this.markWalletProcessed(wallet) return { - status: "failure", + status: "bundle_failure", reason: "INTERNAL FAILURE", - userOperations: ops + userOpsBundled: ops } } @@ -779,7 +779,6 @@ export class Executor { maxPriorityFeePerGas: gasPriceParameters.maxPriorityFeePerGas, nonce: nonce }, - executor: wallet, userOperationInfos, lastReplaced: Date.now(), firstSubmitted: Date.now(), @@ -787,9 +786,12 @@ export class Executor { } const userOperationResults: BundleResult = { - status: "success", - userOperations: opsToBundle, - rejectedUserOperations: failedOps.map((sop) => sop.userOperation), + status: "bundle_success", + userOpsBundled: opsToBundle, + rejectedUserOperations: failedOps.map((sop) => ({ + userOperation: sop.userOperation, + reason: sop.reason + })), transactionInfo } diff --git a/src/executor/executorManager.ts b/src/executor/executorManager.ts index ca8f2f38..536fc959 100644 --- a/src/executor/executorManager.ts +++ b/src/executor/executorManager.ts @@ -18,6 +18,7 @@ import type { BundlingStatus, Logger, Metrics } from "@alto/utils" import { getAAError, getBundleStatus, + getUserOperationHash, parseUserOperationReceipt, scaleBigIntByPercent } from "@alto/utils" @@ -28,7 +29,8 @@ import { type TransactionReceipt, TransactionReceiptNotFoundError, type WatchBlocksReturnType, - getAbiItem + getAbiItem, + Hex } from "viem" import type { Executor, ReplaceTransactionResult } from "./executor" import type { AltoConfig } from "../createConfig" @@ -184,19 +186,19 @@ export class ExecutorManager { opEntryPointMap.get(op.entryPoint)?.push(op.userOperation) } - const txHashes: Hash[] = [] + const bundleTxHashes: Hash[] = [] await Promise.all( this.config.entrypoints.map(async (entryPoint) => { const ops = opEntryPointMap.get(entryPoint) if (ops) { - const txHash = await this.sendToExecutor(entryPoint, ops) + const txHashes = await this.sendToExecutor(entryPoint, ops) - if (!txHash) { + if (txHashes.length === 0) { throw new Error("no tx hash") } - txHashes.push(txHash) + bundleTxHashes.push(...txHashes) } else { this.logger.warn( { entryPoint }, @@ -206,122 +208,150 @@ export class ExecutorManager { }) ) - return txHashes + return bundleTxHashes + } + + getOpHash(userOperation: UserOperation): HexData32 { + return getUserOperationHash( + userOperation, + this.config.entrypoints[0], + this.config.publicClient.chain.id + ) } - async sendToExecutor(entryPoint: Address, userOps: UserOperation[]) { - const bundles: BundleResult[][] = [] + async sendToExecutor( + entryPoint: Address, + userOps: UserOperation[] + ): Promise { + const bundles: BundleResult[] = [] if (userOps.length > 0) { bundles.push(await this.executor.bundle(entryPoint, userOps)) } + let txHashes: Hex[] = [] for (const bundle of bundles) { - const isBundleSuccess = bundle.every( - (result) => result.status === "success" - ) - const isBundleResubmit = bundle.every( - (result) => result.status === "resubmit" - ) - const isBundleFailed = bundle.every( - (result) => result.status === "failure" - ) - if (isBundleSuccess) { - this.metrics.bundlesSubmitted - .labels({ status: "success" }) - .inc() - } - if (isBundleResubmit) { - this.metrics.bundlesSubmitted - .labels({ status: "resubmit" }) - .inc() + switch (bundle.status) { + case "bundle_success": + this.metrics.bundlesSubmitted + .labels({ status: "success" }) + .inc() + break + case "bundle_failure": + this.metrics.bundlesSubmitted + .labels({ status: "failed" }) + .inc() + break + case "bundle_resubmit": + this.metrics.bundlesSubmitted + .labels({ status: "resubmit" }) + .inc() + break } - if (isBundleFailed) { - this.metrics.bundlesSubmitted.labels({ status: "failed" }).inc() + + if (bundle.status === "bundle_resubmit") { + const { userOpsBundled: userOperations, reason } = bundle + + userOperations.map((op) => { + this.logger.info( + { + userOpHash: this.getOpHash(op), + reason + }, + "resubmitting user operation" + ) + this.mempool.removeProcessing(this.getOpHash(op)) + this.mempool.add(op, entryPoint) + this.metrics.userOperationsResubmitted.inc() + }) } - } - const results = bundles.flat() + if (bundle.status === "bundle_failure") { + const { userOpsBundled: userOperations, reason } = bundle - const filteredOutOps = userOps.length - results.length - if (filteredOutOps > 0) { - this.logger.debug( - { filteredOutOps }, - "user operations filtered out" - ) - this.metrics.userOperationsSubmitted - .labels({ status: "filtered" }) - .inc(filteredOutOps) - } + userOperations.map((op) => { + const userOpHash = this.getOpHash(op) + this.mempool.removeProcessing(userOpHash) + this.eventManager.emitDropped( + userOpHash, + reason, + getAAError(reason) + ) + this.monitor.setUserOperationStatus(userOpHash, { + status: "rejected", + transactionHash: null + }) + this.logger.warn( + { + userOperation: JSON.stringify(op, (_k, v) => + typeof v === "bigint" ? v.toString() : v + ), + userOpHash, + reason + }, + "user operation rejected" + ) + this.metrics.userOperationsSubmitted + .labels({ status: "failed" }) + .inc() + }) + } - let txHash: HexData32 | undefined = undefined - for (const result of results) { - if (result.status === "success") { - const res = result.value + if (bundle.status === "bundle_success") { + const { + userOpsBundled: userOperations, + rejectedUserOperations, + transactionInfo + } = bundle + txHashes.push(transactionInfo.transactionHash) - this.mempool.markSubmitted( - res.userOperation.userOperationHash, - res.transactionInfo - ) + userOperations.map((op) => { + const opHash = this.getOpHash(op) - this.monitor.setUserOperationStatus( - res.userOperation.userOperationHash, - { + this.mempool.markSubmitted(opHash, transactionInfo) + + this.monitor.setUserOperationStatus(opHash, { status: "submitted", - transactionHash: res.transactionInfo.transactionHash - } - ) + transactionHash: transactionInfo.transactionHash + }) - txHash = res.transactionInfo.transactionHash - this.startWatchingBlocks(this.handleBlock.bind(this)) - this.metrics.userOperationsSubmitted - .labels({ status: "success" }) - .inc() - } - if (result.status === "failure") { - const { userOpHash, reason } = result.error - this.mempool.removeProcessing(userOpHash) - this.eventManager.emitDropped( - userOpHash, - reason, - getAAError(reason) - ) - this.monitor.setUserOperationStatus(userOpHash, { - status: "rejected", - transactionHash: null + this.startWatchingBlocks(this.handleBlock.bind(this)) + this.metrics.userOperationsSubmitted + .labels({ status: "success" }) + .inc() }) - this.logger.warn( - { - userOperation: JSON.stringify( - result.error.userOperation, - (_k, v) => - typeof v === "bigint" ? v.toString() : v - ), + + rejectedUserOperations.map(({ userOperation, reason }) => { + const userOpHash = this.getOpHash(userOperation) + this.mempool.removeProcessing(userOpHash) + this.eventManager.emitDropped( userOpHash, - reason - }, - "user operation rejected" - ) - this.metrics.userOperationsSubmitted - .labels({ status: "failed" }) - .inc() - } - if (result.status === "resubmit") { - this.logger.info( - { - userOpHash: result.info.userOpHash, - reason: result.info.reason - }, - "resubmitting user operation" - ) - this.mempool.removeProcessing(result.info.userOpHash) - this.mempool.add( - result.info.userOperation, - result.info.entryPoint - ) - this.metrics.userOperationsResubmitted.inc() + reason, + getAAError(reason) + ) + this.monitor.setUserOperationStatus(userOpHash, { + status: "rejected", + transactionHash: null + }) + this.logger.warn( + { + userOperation: JSON.stringify( + userOperation, + (_k, v) => + typeof v === "bigint" ? v.toString() : v + ), + userOpHash, + reason + }, + "user operation rejected" + ) + this.metrics.userOperationsSubmitted + .labels({ status: "failed" }) + .inc() + }) } } - return txHash + + return txHashes } async bundle(opsToBundle: UserOperationInfo[][] = []) { @@ -503,7 +533,8 @@ export class ExecutorManager { ) }) - this.executor.markWalletProcessed(transactionInfo.executor) + const txSender = transactionInfo.transactionRequest.account + this.executor.markWalletProcessed(txSender) } else if ( bundlingStatus.status === "reverted" && bundlingStatus.isAA95 @@ -530,7 +561,8 @@ export class ExecutorManager { opInfos.map(({ userOperationHash }) => { this.mempool.removeSubmitted(userOperationHash) }) - this.executor.markWalletProcessed(transactionInfo.executor) + const txSender = transactionInfo.transactionRequest.account + this.executor.markWalletProcessed(txSender) } } @@ -868,7 +900,8 @@ export class ExecutorManager { "user operation rejected" ) - this.executor.markWalletProcessed(txInfo.executor) + const txSender = txInfo.transactionRequest.account + this.executor.markWalletProcessed(txSender) this.mempool.removeSubmitted(opInfo.userOperationHash) }) @@ -891,7 +924,8 @@ export class ExecutorManager { txInfo.userOperationInfos.map((opInfo) => { this.mempool.removeSubmitted(opInfo.userOperationHash) }) - this.executor.markWalletProcessed(txInfo.executor) + const txSender = txInfo.transactionRequest.account + this.executor.markWalletProcessed(txSender) this.logger.warn( { oldTxHash: txInfo.transactionHash, reason }, diff --git a/src/handlers/eventManager.ts b/src/handlers/eventManager.ts index e24e2298..8313275d 100644 --- a/src/handlers/eventManager.ts +++ b/src/handlers/eventManager.ts @@ -164,8 +164,11 @@ export class EventManager { } // emits when the userOperation has been submitted to the network - async emitSubmitted(userOperationHashes: Hex[], transactionHash: Hex) { - for (const userOperationHash of userOperationHashes) { + async emitSubmitted({ + userOpHashes, + transactionHash + }: { userOpHashes: Hex[]; transactionHash: Hex }) { + for (const userOperationHash of userOpHashes) { await this.emitEvent({ userOperationHash, event: { diff --git a/src/rpc/rpcHandler.ts b/src/rpc/rpcHandler.ts index a160c426..7e7935a7 100644 --- a/src/rpc/rpcHandler.ts +++ b/src/rpc/rpcHandler.ts @@ -901,7 +901,8 @@ export class RpcHandler implements IRpcEndpoint { } } - this.executor.markWalletProcessed(res.value.transactionInfo.executor) + const txSender = res.value.transactionInfo.transactionRequest.account + this.executor.markWalletProcessed(txSender) // wait for receipt const receipt = diff --git a/src/types/mempool.ts b/src/types/mempool.ts index 8b763aad..ef3a6e66 100644 --- a/src/types/mempool.ts +++ b/src/types/mempool.ts @@ -24,7 +24,6 @@ export type TransactionInfo = { maxPriorityFeePerGas: bigint nonce: number } - executor: Account userOperationInfos: UserOperationInfo[] lastReplaced: number firstSubmitted: number @@ -52,20 +51,28 @@ export type SubmittedUserOperation = { transactionInfo: TransactionInfo } +export type RejectedUserOperation = { + userOperation: UserOperation + reason: string +} + export type BundleResult = | { - status: "success" - userOperations: UserOperation[] - rejectedUserOperations: UserOperation[] + // Successfully bundled user operations. + status: "bundle_success" + userOpsBundled: UserOperation[] + rejectedUserOperations: RejectedUserOperation[] transactionInfo: TransactionInfo } | { - status: "failure" + // Encountered error whilst trying to bundle user operations. + status: "bundle_failure" reason: string - userOperations: UserOperation[] + userOpsBundled: UserOperation[] } | { - status: "resubmit" + // Encountered recoverable error whilst trying to bundle user operations. + status: "bundle_resubmit" reason: string - userOperations: UserOperation[] + userOpsBundled: UserOperation[] } From 25daacd45137b7c5d2c1d98dd2aa50c09e17c392 Mon Sep 17 00:00:00 2001 From: mouseless <97399882+mouseless-eth@users.noreply.github.com> Date: Sun, 26 Jan 2025 05:30:54 +0000 Subject: [PATCH 04/34] cleanup --- src/cli/setupServer.ts | 10 +++--- src/executor/executor.ts | 55 ++------------------------------- src/executor/executorManager.ts | 24 +++++++++++--- src/executor/senderManager.ts | 40 ++++++++++++++++++++++++ 4 files changed, 66 insertions(+), 63 deletions(-) diff --git a/src/cli/setupServer.ts b/src/cli/setupServer.ts index 8e88253d..41e55cda 100644 --- a/src/cli/setupServer.ts +++ b/src/cli/setupServer.ts @@ -97,7 +97,6 @@ const getEventManager = ({ const getExecutor = ({ mempool, config, - senderManager, reputationManager, metrics, gasPriceManager, @@ -105,7 +104,6 @@ const getExecutor = ({ }: { mempool: MemoryMempool config: AltoConfig - senderManager: SenderManager reputationManager: InterfaceReputationManager metrics: Metrics gasPriceManager: GasPriceManager @@ -114,7 +112,6 @@ const getExecutor = ({ return new Executor({ mempool, config, - senderManager, reputationManager, metrics, gasPriceManager, @@ -127,6 +124,7 @@ const getExecutorManager = ({ executor, mempool, monitor, + senderManager, reputationManager, metrics, gasPriceManager, @@ -137,6 +135,7 @@ const getExecutorManager = ({ mempool: MemoryMempool monitor: Monitor reputationManager: InterfaceReputationManager + senderManager: SenderManager metrics: Metrics gasPriceManager: GasPriceManager eventManager: EventManager @@ -146,6 +145,7 @@ const getExecutorManager = ({ executor, mempool, monitor, + senderManager, reputationManager, metrics, gasPriceManager, @@ -275,7 +275,6 @@ export const setupServer = async ({ const executor = getExecutor({ mempool, config, - senderManager, reputationManager, metrics, gasPriceManager, @@ -287,6 +286,7 @@ export const setupServer = async ({ executor, mempool, monitor, + senderManager, reputationManager, metrics, gasPriceManager, @@ -314,7 +314,7 @@ export const setupServer = async ({ }) if (config.flushStuckTransactionsDuringStartup) { - executor.flushStuckTransactions() + senderManager.flushOnStartUp() } const rootLogger = config.getLogger( diff --git a/src/executor/executor.ts b/src/executor/executor.ts index 776ca488..d7e6450a 100644 --- a/src/executor/executor.ts +++ b/src/executor/executor.ts @@ -1,4 +1,3 @@ -import type { SenderManager } from "@alto/executor" import type { EventManager, GasPriceManager } from "@alto/handlers" import type { InterfaceReputationManager, MemoryMempool } from "@alto/mempool" import { @@ -37,11 +36,7 @@ import { BaseError, NonceTooHighError } from "viem" -import { - flushStuckTransaction, - isTransactionUnderpricedError, - getAuthorizationList -} from "./utils" +import { isTransactionUnderpricedError, getAuthorizationList } from "./utils" import type { SendTransactionErrorType } from "viem" import type { AltoConfig } from "../createConfig" import type { SendTransactionOptions } from "./types" @@ -76,7 +71,6 @@ export type ReplaceTransactionResult = export class Executor { // private unWatch: WatchBlocksReturnType | undefined config: AltoConfig - senderManager: SenderManager logger: Logger metrics: Metrics reputationManager: InterfaceReputationManager @@ -88,7 +82,6 @@ export class Executor { constructor({ config, mempool, - senderManager, reputationManager, metrics, gasPriceManager, @@ -96,7 +89,6 @@ export class Executor { }: { config: AltoConfig mempool: MemoryMempool - senderManager: SenderManager reputationManager: InterfaceReputationManager metrics: Metrics gasPriceManager: GasPriceManager @@ -104,7 +96,6 @@ export class Executor { }) { this.config = config this.mempool = mempool - this.senderManager = senderManager this.reputationManager = reputationManager this.logger = config.getLogger( { module: "executor" }, @@ -123,13 +114,6 @@ export class Executor { throw new Error("Method not implemented.") } - markWalletProcessed(executor: Account) { - if (!this.senderManager.availableWallets.includes(executor)) { - this.senderManager.pushWallet(executor) - } - return Promise.resolve() - } - async replaceTransaction( transactionInfo: TransactionInfo ): Promise { @@ -351,38 +335,6 @@ export class Executor { }) } - async flushStuckTransactions(): Promise { - const allWallets = new Set(this.senderManager.wallets) - - const utilityWallet = this.senderManager.utilityAccount - if (utilityWallet) { - allWallets.add(utilityWallet) - } - - const wallets = Array.from(allWallets) - - const gasPrice = await this.gasPriceManager.tryGetNetworkGasPrice() - - const promises = wallets.map((wallet) => { - try { - flushStuckTransaction( - this.config.publicClient, - this.config.walletClient, - wallet, - gasPrice.maxFeePerGas * 5n, - this.logger - ) - } catch (e) { - this.logger.error( - { error: e }, - "error flushing stuck transaction" - ) - } - }) - - await Promise.all(promises) - } - async sendHandleOpsTransaction({ txParam, opts @@ -567,11 +519,10 @@ export class Executor { } async bundle( + wallet: Account, entryPoint: Address, ops: UserOperation[] ): Promise { - const wallet = await this.senderManager.getWallet() - // Find bundle EntryPoint version. const firstOpVersion = isVersion06(ops[0]) const allSameVersion = ops.every( @@ -613,7 +564,6 @@ export class Executor { { error: err }, "Failed to get parameters for bundling" ) - this.markWalletProcessed(wallet) return { status: "bundle_resubmit", reason: "Failed to get parameters for bundling", @@ -654,7 +604,6 @@ export class Executor { return { status: "bundle_failure", reason: "INTERNAL FAILURE", - // TODO: we want to log the failure reason userOpsBundled: ops } } diff --git a/src/executor/executorManager.ts b/src/executor/executorManager.ts index 536fc959..2260b601 100644 --- a/src/executor/executorManager.ts +++ b/src/executor/executorManager.ts @@ -30,10 +30,12 @@ import { TransactionReceiptNotFoundError, type WatchBlocksReturnType, getAbiItem, - Hex + Hex, + Account } from "viem" import type { Executor, ReplaceTransactionResult } from "./executor" import type { AltoConfig } from "../createConfig" +import { SenderManager } from "./senderManager" function getTransactionsFromUserOperationEntries( entries: SubmittedUserOperation[] @@ -53,6 +55,7 @@ const SCALE_FACTOR = 10 // Interval increases by 5ms per task per minute const RPM_WINDOW = 60000 // 1 minute window in ms export class ExecutorManager { + private senderManager: SenderManager private config: AltoConfig private executor: Executor private mempool: MemoryMempool @@ -75,7 +78,8 @@ export class ExecutorManager { reputationManager, metrics, gasPriceManager, - eventManager + eventManager, + senderManager }: { config: AltoConfig executor: Executor @@ -85,6 +89,7 @@ export class ExecutorManager { metrics: Metrics gasPriceManager: GasPriceManager eventManager: EventManager + senderManager: SenderManager }) { this.config = config this.reputationManager = reputationManager @@ -100,6 +105,7 @@ export class ExecutorManager { this.metrics = metrics this.gasPriceManager = gasPriceManager this.eventManager = eventManager + this.senderManager = senderManager this.bundlingMode = this.config.bundleMode @@ -223,13 +229,21 @@ export class ExecutorManager { entryPoint: Address, userOps: UserOperation[] ): Promise { - const bundles: BundleResult[] = [] + if (userOps.length === 0) { + return [] + } + + const bundles: { wallet: Account; bundle: BundleResult }[] = [] if (userOps.length > 0) { - bundles.push(await this.executor.bundle(entryPoint, userOps)) + const wallet = await this.senderManager.getWallet() + bundles.push({ + wallet, + bundle: await this.executor.bundle(wallet, entryPoint, userOps) + }) } let txHashes: Hex[] = [] - for (const bundle of bundles) { + for (const { wallet, bundle } of bundles) { switch (bundle.status) { case "bundle_success": this.metrics.bundlesSubmitted diff --git a/src/executor/senderManager.ts b/src/executor/senderManager.ts index 4562eac3..c13cb8dc 100644 --- a/src/executor/senderManager.ts +++ b/src/executor/senderManager.ts @@ -15,6 +15,7 @@ import { getContract } from "viem" import type { AltoConfig } from "../createConfig" +import { flushStuckTransaction } from "./utils" const waitForTransactionReceipt = async ( publicClient: PublicClient, @@ -261,4 +262,43 @@ export class SenderManager { this.metrics.walletsAvailable.set(this.availableWallets.length) return } + + public markWalletProcessed(executor: Account) { + if (!this.availableWallets.includes(executor)) { + this.pushWallet(executor) + } + return Promise.resolve() + } + + async flushOnStartUp(): Promise { + const allWallets = new Set(this.wallets) + + const utilityWallet = this.utilityAccount + if (utilityWallet) { + allWallets.add(utilityWallet) + } + + const wallets = Array.from(allWallets) + + const gasPrice = await this.gasPriceManager.tryGetNetworkGasPrice() + + const promises = wallets.map((wallet) => { + try { + flushStuckTransaction( + this.config.publicClient, + this.config.walletClient, + wallet, + gasPrice.maxFeePerGas * 5n, + this.logger + ) + } catch (e) { + this.logger.error( + { error: e }, + "error flushing stuck transaction" + ) + } + }) + + await Promise.all(promises) + } } From 255ec6374cb28a6da42827e1a28d67d506be3312 Mon Sep 17 00:00:00 2001 From: mouseless <97399882+mouseless-eth@users.noreply.github.com> Date: Sun, 26 Jan 2025 05:39:37 +0000 Subject: [PATCH 05/34] cleanup markWalletProcessed flow --- src/executor/executor.ts | 4 ---- src/executor/executorManager.ts | 8 ++++++++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/executor/executor.ts b/src/executor/executor.ts index d7e6450a..794d674d 100644 --- a/src/executor/executor.ts +++ b/src/executor/executor.ts @@ -588,7 +588,6 @@ export class Executor { childLogger.error( "gas limit simulation encountered unexpected failure" ) - this.markWalletProcessed(wallet) return { status: "bundle_failure", reason: "INTERNAL FAILURE", @@ -600,7 +599,6 @@ export class Executor { if (opsToBundle.length === 0) { childLogger.warn("all ops failed simulation") - this.markWalletProcessed(wallet) return { status: "bundle_failure", reason: "INTERNAL FAILURE", @@ -683,7 +681,6 @@ export class Executor { { error: e }, "insufficient funds, not submitting transaction" ) - this.markWalletProcessed(wallet) return { status: "bundle_resubmit", reason: InsufficientFundsError.name, @@ -696,7 +693,6 @@ export class Executor { { error: JSON.stringify(err) }, "error submitting bundle transaction" ) - this.markWalletProcessed(wallet) return { status: "bundle_failure", reason: "INTERNAL FAILURE", diff --git a/src/executor/executorManager.ts b/src/executor/executorManager.ts index 2260b601..72fdc953 100644 --- a/src/executor/executorManager.ts +++ b/src/executor/executorManager.ts @@ -262,6 +262,14 @@ export class ExecutorManager { break } + // Free wallet if the wallet did not make a succesful bundle tx. + if ( + bundle.status === "bundle_failure" || + bundle.status === "bundle_resubmit" + ) { + this.senderManager.markWalletProcessed(wallet) + } + if (bundle.status === "bundle_resubmit") { const { userOpsBundled: userOperations, reason } = bundle From e2a3202ec5d192c4e545a96003f64d7665819862 Mon Sep 17 00:00:00 2001 From: mouseless <97399882+mouseless-eth@users.noreply.github.com> Date: Sun, 26 Jan 2025 16:59:17 +0000 Subject: [PATCH 06/34] create helpers to keep track of bundling status --- src/executor/executor.ts | 35 ++--- src/executor/executorManager.ts | 228 ++++++++++++++------------------ src/rpc/rpcHandler.ts | 2 +- src/types/mempool.ts | 6 +- 4 files changed, 121 insertions(+), 150 deletions(-) diff --git a/src/executor/executor.ts b/src/executor/executor.ts index 794d674d..4033e44e 100644 --- a/src/executor/executor.ts +++ b/src/executor/executor.ts @@ -121,7 +121,9 @@ export class Executor { isVersion06, entryPoint, transactionRequest, - userOperationInfos + userOperationInfos, + executor, + transactionHash } = transactionInfo let gasPriceParameters: GasPriceParameters @@ -159,14 +161,14 @@ export class Executor { }) const childLogger = this.logger.child({ - transactionHash: transactionInfo.transactionHash, - executor: transactionInfo.transactionRequest.account.address + transactionHash, + executor: executor }) let bundleResult = await filterOpsAndEstimateGas({ ep, isUserOpV06: isVersion06, - wallet: newRequest.account, + wallet: executor, ops: opsToResubmit, nonce: newRequest.nonce, maxFeePerGas: newRequest.maxFeePerGas, @@ -231,7 +233,7 @@ export class Executor { abi: undefined, chain: undefined }, - executor: newRequest.account.address, + executor, userOperations: this.getOpHashes(opsToBundle) }, "replacing transaction" @@ -241,13 +243,13 @@ export class Executor { txParam, opts: this.config.legacyTransactions ? { - account: newRequest.account, + account: executor, gasPrice: newRequest.maxFeePerGas, gas: newRequest.gas, nonce: newRequest.nonce } : { - account: newRequest.account, + account: executor, maxFeePerGas: newRequest.maxFeePerGas, maxPriorityFeePerGas: newRequest.maxPriorityFeePerGas, gas: newRequest.gas, @@ -495,12 +497,12 @@ export class Executor { const conflictingOps = submitted .filter((submitted) => { - const tx = submitted.transactionInfo - const txSender = tx.transactionRequest.account.address + const txInfo = submitted.transactionInfo + const txSender = txInfo.executor.address return ( txSender === executor.address && - tx.transactionRequest.nonce === nonce + txInfo.transactionRequest.nonce === nonce ) }) .map(({ userOperation }) => userOperation) @@ -514,6 +516,7 @@ export class Executor { }) if (conflictingOps.length > 0) { + // TODO: what to do here? this.markWalletProcessed(executor) } } @@ -567,7 +570,7 @@ export class Executor { return { status: "bundle_resubmit", reason: "Failed to get parameters for bundling", - userOpsBundled: ops + userOps: ops } } @@ -591,7 +594,7 @@ export class Executor { return { status: "bundle_failure", reason: "INTERNAL FAILURE", - userOpsBundled: ops + userOps: ops } } @@ -602,7 +605,7 @@ export class Executor { return { status: "bundle_failure", reason: "INTERNAL FAILURE", - userOpsBundled: ops + userOps: ops } } @@ -684,7 +687,7 @@ export class Executor { return { status: "bundle_resubmit", reason: InsufficientFundsError.name, - userOpsBundled: ops + userOps: ops } } @@ -696,7 +699,7 @@ export class Executor { return { status: "bundle_failure", reason: "INTERNAL FAILURE", - userOpsBundled: ops + userOps: ops } } @@ -716,7 +719,6 @@ export class Executor { transactionHash: transactionHash, previousTransactionHashes: [], transactionRequest: { - account: wallet, to: ep.address, gas: gasLimit, chain: this.config.walletClient.chain, @@ -724,6 +726,7 @@ export class Executor { maxPriorityFeePerGas: gasPriceParameters.maxPriorityFeePerGas, nonce: nonce }, + executor: wallet, userOperationInfos, lastReplaced: Date.now(), firstSubmitted: Date.now(), diff --git a/src/executor/executorManager.ts b/src/executor/executorManager.ts index 72fdc953..350904f6 100644 --- a/src/executor/executorManager.ts +++ b/src/executor/executorManager.ts @@ -12,7 +12,8 @@ import { type UserOperation, type SubmittedUserOperation, type TransactionInfo, - type UserOperationInfo + type UserOperationInfo, + RejectedUserOperation } from "@alto/types" import type { BundlingStatus, Logger, Metrics } from "@alto/utils" import { @@ -271,105 +272,34 @@ export class ExecutorManager { } if (bundle.status === "bundle_resubmit") { - const { userOpsBundled: userOperations, reason } = bundle - - userOperations.map((op) => { - this.logger.info( - { - userOpHash: this.getOpHash(op), - reason - }, - "resubmitting user operation" - ) - this.mempool.removeProcessing(this.getOpHash(op)) - this.mempool.add(op, entryPoint) - this.metrics.userOperationsResubmitted.inc() - }) + const { userOps: userOperations, reason } = bundle + this.resubmitUserOperations(userOperations, entryPoint, reason) } if (bundle.status === "bundle_failure") { - const { userOpsBundled: userOperations, reason } = bundle - - userOperations.map((op) => { - const userOpHash = this.getOpHash(op) - this.mempool.removeProcessing(userOpHash) - this.eventManager.emitDropped( - userOpHash, - reason, - getAAError(reason) - ) - this.monitor.setUserOperationStatus(userOpHash, { - status: "rejected", - transactionHash: null - }) - this.logger.warn( - { - userOperation: JSON.stringify(op, (_k, v) => - typeof v === "bigint" ? v.toString() : v - ), - userOpHash, - reason - }, - "user operation rejected" - ) - this.metrics.userOperationsSubmitted - .labels({ status: "failed" }) - .inc() - }) + const { userOps, reason } = bundle + + const droppedUserOperations = userOps.map((op) => ({ + userOperation: op, + reason + })) + this.dropUserOperations(droppedUserOperations) } if (bundle.status === "bundle_success") { const { - userOpsBundled: userOperations, + userOpsBundled, rejectedUserOperations, transactionInfo } = bundle txHashes.push(transactionInfo.transactionHash) - userOperations.map((op) => { - const opHash = this.getOpHash(op) - - this.mempool.markSubmitted(opHash, transactionInfo) - - this.monitor.setUserOperationStatus(opHash, { - status: "submitted", - transactionHash: transactionInfo.transactionHash - }) - - this.startWatchingBlocks(this.handleBlock.bind(this)) - this.metrics.userOperationsSubmitted - .labels({ status: "success" }) - .inc() - }) + this.markUserOperationsAsSubmitted( + userOpsBundled, + transactionInfo + ) - rejectedUserOperations.map(({ userOperation, reason }) => { - const userOpHash = this.getOpHash(userOperation) - this.mempool.removeProcessing(userOpHash) - this.eventManager.emitDropped( - userOpHash, - reason, - getAAError(reason) - ) - this.monitor.setUserOperationStatus(userOpHash, { - status: "rejected", - transactionHash: null - }) - this.logger.warn( - { - userOperation: JSON.stringify( - userOperation, - (_k, v) => - typeof v === "bigint" ? v.toString() : v - ), - userOpHash, - reason - }, - "user operation rejected" - ) - this.metrics.userOperationsSubmitted - .labels({ status: "failed" }) - .inc() - }) + this.dropUserOperations(rejectedUserOperations) } } @@ -414,18 +344,6 @@ export class ExecutorManager { } this.unWatch = this.config.publicClient.watchBlocks({ onBlock: handleBlock, - // onBlock: async (block) => { - // // Use an arrow function to ensure correct binding of `this` - // this.checkAndReplaceTransactions(block) - // .then(() => { - // this.logger.trace("block handled") - // // Handle the resolution of the promise here, if needed - // }) - // .catch((error) => { - // // Handle any errors that occur during the execution of the promise - // this.logger.error({ error }, "error while handling block") - // }) - // }, onError: (error) => { this.logger.error({ error }, "error while watching blocks") }, @@ -555,8 +473,7 @@ export class ExecutorManager { ) }) - const txSender = transactionInfo.transactionRequest.account - this.executor.markWalletProcessed(txSender) + this.senderManager.markWalletProcessed(transactionInfo.executor) } else if ( bundlingStatus.status === "reverted" && bundlingStatus.isAA95 @@ -583,8 +500,7 @@ export class ExecutorManager { opInfos.map(({ userOperationHash }) => { this.mempool.removeSubmitted(userOperationHash) }) - const txSender = transactionInfo.transactionRequest.account - this.executor.markWalletProcessed(txSender) + this.senderManager.markWalletProcessed(transactionInfo.executor) } } @@ -903,35 +819,19 @@ export class ExecutorManager { } if (replaceResult.status === "failed") { - txInfo.userOperationInfos.map((opInfo) => { - const userOperation = opInfo.userOperation - - this.eventManager.emitDropped( - opInfo.userOperationHash, - "Failed to replace transaction" - ) - - this.logger.warn( - { - userOperation: JSON.stringify(userOperation, (_k, v) => - typeof v === "bigint" ? v.toString() : v - ), - userOpHash: opInfo.userOperationHash, - reason - }, - "user operation rejected" - ) - - const txSender = txInfo.transactionRequest.account - this.executor.markWalletProcessed(txSender) - this.mempool.removeSubmitted(opInfo.userOperationHash) - }) - this.logger.warn( { oldTxHash: txInfo.transactionHash, reason }, "failed to replace transaction" ) + const droppedUserOperations = txInfo.userOperationInfos.map( + (opInfo) => ({ + userOperation: opInfo.userOperation, + reason: "Failed to replace transaction" + }) + ) + this.dropUserOperations(droppedUserOperations) + this.senderManager.markWalletProcessed(txInfo.executor) return } @@ -946,9 +846,8 @@ export class ExecutorManager { txInfo.userOperationInfos.map((opInfo) => { this.mempool.removeSubmitted(opInfo.userOperationHash) }) - const txSender = txInfo.transactionRequest.account - this.executor.markWalletProcessed(txSender) - + const txSender = txInfo.executor + this.senderManager.markWalletProcessed(txSender) this.logger.warn( { oldTxHash: txInfo.transactionHash, reason }, "transaction potentially already included too many times, removing" @@ -1000,4 +899,73 @@ export class ExecutorManager { return } + + markUserOperationsAsSubmitted( + userOperations: UserOperation[], + transactionInfo: TransactionInfo + ) { + userOperations.map((op) => { + const opHash = this.getOpHash(op) + + this.mempool.markSubmitted(opHash, transactionInfo) + + this.monitor.setUserOperationStatus(opHash, { + status: "submitted", + transactionHash: transactionInfo.transactionHash + }) + + this.startWatchingBlocks(this.handleBlock.bind(this)) + this.metrics.userOperationsSubmitted + .labels({ status: "success" }) + .inc() + }) + } + + resubmitUserOperations( + userOperations: UserOperation[], + entryPoint: Address, + reason: string + ) { + userOperations.map((op) => { + this.logger.info( + { + userOpHash: this.getOpHash(op), + reason + }, + "resubmitting user operation" + ) + this.mempool.removeProcessing(this.getOpHash(op)) + this.mempool.add(op, entryPoint) + this.metrics.userOperationsResubmitted.inc() + }) + } + + dropUserOperations(rejectedUserOperations: RejectedUserOperation[]) { + rejectedUserOperations.map(({ userOperation, reason }) => { + const userOpHash = this.getOpHash(userOperation) + this.mempool.removeProcessing(userOpHash) + this.eventManager.emitDropped( + userOpHash, + reason, + getAAError(reason) + ) + this.monitor.setUserOperationStatus(userOpHash, { + status: "rejected", + transactionHash: null + }) + this.logger.warn( + { + userOperation: JSON.stringify(userOperation, (_k, v) => + typeof v === "bigint" ? v.toString() : v + ), + userOpHash, + reason + }, + "user operation rejected" + ) + this.metrics.userOperationsSubmitted + .labels({ status: "failed" }) + .inc() + }) + } } diff --git a/src/rpc/rpcHandler.ts b/src/rpc/rpcHandler.ts index 7e7935a7..00213930 100644 --- a/src/rpc/rpcHandler.ts +++ b/src/rpc/rpcHandler.ts @@ -901,7 +901,7 @@ export class RpcHandler implements IRpcEndpoint { } } - const txSender = res.value.transactionInfo.transactionRequest.account + const txSender = res.value.transactionInfo.executor.address this.executor.markWalletProcessed(txSender) // wait for receipt diff --git a/src/types/mempool.ts b/src/types/mempool.ts index ef3a6e66..881f9644 100644 --- a/src/types/mempool.ts +++ b/src/types/mempool.ts @@ -16,7 +16,6 @@ export type TransactionInfo = { entryPoint: Address isVersion06: boolean transactionRequest: { - account: Account to: Address gas: bigint chain: Chain @@ -24,6 +23,7 @@ export type TransactionInfo = { maxPriorityFeePerGas: bigint nonce: number } + executor: Account userOperationInfos: UserOperationInfo[] lastReplaced: number firstSubmitted: number @@ -68,11 +68,11 @@ export type BundleResult = // Encountered error whilst trying to bundle user operations. status: "bundle_failure" reason: string - userOpsBundled: UserOperation[] + userOps: UserOperation[] } | { // Encountered recoverable error whilst trying to bundle user operations. status: "bundle_resubmit" reason: string - userOpsBundled: UserOperation[] + userOps: UserOperation[] } From 8b2248da76ba250a5a7b11fcb9c6485ba253270c Mon Sep 17 00:00:00 2001 From: mouseless <97399882+mouseless-eth@users.noreply.github.com> Date: Sun, 26 Jan 2025 21:02:24 +0000 Subject: [PATCH 07/34] cleanup creating bundles from mempool --- src/executor/executor.ts | 8 +- src/executor/executorManager.ts | 149 ++++++++++--------------- src/handlers/eventManager.ts | 4 +- src/mempool/mempool.ts | 185 +++++++++++++++++--------------- src/rpc/rpcHandler.ts | 5 +- src/types/mempool.ts | 6 +- 6 files changed, 163 insertions(+), 194 deletions(-) diff --git a/src/executor/executor.ts b/src/executor/executor.ts index 4033e44e..4246132a 100644 --- a/src/executor/executor.ts +++ b/src/executor/executor.ts @@ -275,9 +275,7 @@ export class Executor { return { entryPoint, userOperation: op, - userOperationHash: this.getOpHashes([op])[0], - lastReplaced: Date.now(), - firstSubmitted: transactionInfo.firstSubmitted + userOperationHash: this.getOpHashes([op])[0] } }) } @@ -517,7 +515,7 @@ export class Executor { if (conflictingOps.length > 0) { // TODO: what to do here? - this.markWalletProcessed(executor) + // this.markWalletProcessed(executor) } } @@ -719,7 +717,6 @@ export class Executor { transactionHash: transactionHash, previousTransactionHashes: [], transactionRequest: { - to: ep.address, gas: gasLimit, chain: this.config.walletClient.chain, maxFeePerGas: gasPriceParameters.maxFeePerGas, @@ -729,7 +726,6 @@ export class Executor { executor: wallet, userOperationInfos, lastReplaced: Date.now(), - firstSubmitted: Date.now(), timesPotentiallyIncluded: 0 } diff --git a/src/executor/executorManager.ts b/src/executor/executorManager.ts index 350904f6..f722c8d3 100644 --- a/src/executor/executorManager.ts +++ b/src/executor/executorManager.ts @@ -135,14 +135,16 @@ export class ExecutorManager { (timestamp) => now - timestamp < RPM_WINDOW ) - const opsToBundle = await this.getOpsToBundle() + const bundles = await this.getMempoolBundles() - if (opsToBundle.length > 0) { - const opsCount: number = opsToBundle.length + if (bundles.length > 0) { + const opsCount: number = bundles + .map(({ userOperations }) => userOperations.length) + .reduce((a, b) => a + b) const timestamp: number = Date.now() this.opsCount.push(...Array(opsCount).fill(timestamp)) // Add timestamps for each task - await this.bundle(opsToBundle) + await this.sendBundles(bundles) } const rpm: number = this.opsCount.length @@ -151,71 +153,55 @@ export class ExecutorManager { MIN_INTERVAL + rpm * SCALE_FACTOR, // Linear scaling MAX_INTERVAL // Cap at 1000ms ) + if (this.bundlingMode === "auto") { setTimeout(this.autoScalingBundling.bind(this), nextInterval) } } - async getOpsToBundle() { - const opsToBundle: UserOperationInfo[][] = [] + async getMempoolBundles(maxBundleCount?: number) { + const bundlePromises = this.config.entrypoints.map( + async (entryPoint) => { + const mempoolBundles = await this.mempool.process({ + entryPoint, + maxGasLimit: this.config.maxGasPerBundle, + minOpsPerBundle: 1, + maxBundleCount + }) - while (true) { - const ops = await this.mempool.process( - this.config.maxGasPerBundle, - 1 - ) - if (ops?.length > 0) { - opsToBundle.push(ops) - } else { - break + return mempoolBundles.map((userOperations) => ({ + entryPoint, + userOperations + })) } - } + ) - if (opsToBundle.length === 0) { - return [] - } + const bundlesNested = await Promise.all(bundlePromises) + const bundles = bundlesNested.flat() - return opsToBundle + return bundles } - async bundleNow(): Promise { - const ops = await this.mempool.process(this.config.maxGasPerBundle, 1) - if (ops.length === 0) { + // Debug endpoint + async sendBundleNow(): Promise { + const bundle = (await this.getMempoolBundles(1))[0] + + const { entryPoint, userOperations } = bundle + if (userOperations.length === 0) { throw new Error("no ops to bundle") } - const opEntryPointMap = new Map() + const txHashes = await this.sendBundleToExecutor( + entryPoint, + userOperations.map((op) => op.userOperation) + ) + const txHash = txHashes[0] - for (const op of ops) { - if (!opEntryPointMap.has(op.entryPoint)) { - opEntryPointMap.set(op.entryPoint, []) - } - opEntryPointMap.get(op.entryPoint)?.push(op.userOperation) + if (!txHash) { + throw new Error("no tx hash") } - const bundleTxHashes: Hash[] = [] - - await Promise.all( - this.config.entrypoints.map(async (entryPoint) => { - const ops = opEntryPointMap.get(entryPoint) - if (ops) { - const txHashes = await this.sendToExecutor(entryPoint, ops) - - if (txHashes.length === 0) { - throw new Error("no tx hash") - } - - bundleTxHashes.push(...txHashes) - } else { - this.logger.warn( - { entryPoint }, - "no user operations for entry point" - ) - } - }) - ) - - return bundleTxHashes + return txHash } getOpHash(userOperation: UserOperation): HexData32 { @@ -226,11 +212,11 @@ export class ExecutorManager { ) } - async sendToExecutor( - entryPoint: Address, + async sendBundleToExecutor(bundle: { + entryPoint: Address userOps: UserOperation[] - ): Promise { - if (userOps.length === 0) { + }): Promise { + if (bundle.userOps.length === 0) { return [] } @@ -306,33 +292,18 @@ export class ExecutorManager { return txHashes } - async bundle(opsToBundle: UserOperationInfo[][] = []) { + async sendBundles( + bundles: { + entryPoint: Address + userOperations: UserOperationInfo[] + }[] = [] + ) { await Promise.all( - opsToBundle.map(async (ops) => { - const opEntryPointMap = new Map() - - for (const op of ops) { - if (!opEntryPointMap.has(op.entryPoint)) { - opEntryPointMap.set(op.entryPoint, []) - } - opEntryPointMap.get(op.entryPoint)?.push(op.userOperation) - } - - await Promise.all( - this.config.entrypoints.map(async (entryPoint) => { - const userOperations = opEntryPointMap.get(entryPoint) - if (userOperations) { - await this.sendToExecutor( - entryPoint, - userOperations - ) - } else { - this.logger.warn( - { entryPoint }, - "no user operations for entry point" - ) - } - }) + bundles.map(async (bundle) => { + const { entryPoint, userOperations } = bundle + await this.sendBundleToExecutor( + entryPoint, + userOperations.map((op) => op.userOperation) ) }) ) @@ -429,17 +400,13 @@ export class ExecutorManager { const { userOperationDetails } = bundlingStatus opInfos.map((opInfo) => { - const { - userOperation, - userOperationHash, - entryPoint, - firstSubmitted - } = opInfo + const { userOperation, userOperationHash, entryPoint } = opInfo const opDetails = userOperationDetails[userOperationHash] - this.metrics.userOperationInclusionDuration.observe( - (Date.now() - firstSubmitted) / 1000 - ) + // TODO: keep this metric + //this.metrics.userOperationInclusionDuration.observe( + // (Date.now() - firstSubmitted) / 1000 + //) this.mempool.removeSubmitted(userOperationHash) this.reputationManager.updateUserOperationIncludedStatus( userOperation, diff --git a/src/handlers/eventManager.ts b/src/handlers/eventManager.ts index 8313275d..6bab6192 100644 --- a/src/handlers/eventManager.ts +++ b/src/handlers/eventManager.ts @@ -168,9 +168,9 @@ export class EventManager { userOpHashes, transactionHash }: { userOpHashes: Hex[]; transactionHash: Hex }) { - for (const userOperationHash of userOpHashes) { + for (const hash of userOpHashes) { await this.emitEvent({ - userOperationHash, + userOperationHash: hash, event: { eventType: "submitted", transactionHash diff --git a/src/mempool/mempool.ts b/src/mempool/mempool.ts index 21cfda84..d292dbb8 100644 --- a/src/mempool/mempool.ts +++ b/src/mempool/mempool.ts @@ -16,7 +16,6 @@ import { ValidationErrors, type ValidationResult } from "@alto/types" -import type { HexData32 } from "@alto/types" import type { Metrics } from "@alto/utils" import type { Logger } from "@alto/utils" import { @@ -412,8 +411,6 @@ export class MemoryMempool { userOperation, entryPoint, userOperationHash: opHash, - firstSubmitted: oldUserOp ? oldUserOp.firstSubmitted : Date.now(), - lastReplaced: Date.now(), referencedContracts }) this.monitor.setUserOperationStatus(opHash, { @@ -672,107 +669,119 @@ export class MemoryMempool { } // Returns a bundle of userOperations in array format. - async process( - maxGasLimit: bigint, - minOps?: number - ): Promise { - const outstandingUserOperations = this.store.dumpOutstanding().slice() - - // Sort userops before the execution - // Decide the order of the userops based on the sender and nonce - // If sender is the same, sort by nonce key - outstandingUserOperations.sort((a, b) => { - const aUserOp = a.userOperation - const bUserOp = b.userOperation - - if (aUserOp.sender === bUserOp.sender) { - const [aNonceKey, aNonceValue] = getNonceKeyAndValue( - aUserOp.nonce - ) - const [bNonceKey, bNonceValue] = getNonceKeyAndValue( - bUserOp.nonce - ) + async process({ + maxGasLimit, + entryPoint, + minOpsPerBundle, + maxBundleCount + }: { + maxGasLimit: bigint + entryPoint: Address + minOpsPerBundle: number + maxBundleCount?: number + }): Promise { + let outstandingUserOperations = this.store + .dumpOutstanding() + .filter((op) => op.entryPoint === entryPoint) + .sort((a, b) => { + // Sort userops before the execution + // Decide the order of the userops based on the sender and nonce + // If sender is the same, sort by nonce key + const aUserOp = a.userOperation + const bUserOp = b.userOperation + + if (aUserOp.sender === bUserOp.sender) { + const [aNonceKey, aNonceValue] = getNonceKeyAndValue( + aUserOp.nonce + ) + const [bNonceKey, bNonceValue] = getNonceKeyAndValue( + bUserOp.nonce + ) - if (aNonceKey === bNonceKey) { - return Number(aNonceValue - bNonceValue) + if (aNonceKey === bNonceKey) { + return Number(aNonceValue - bNonceValue) + } + + return Number(aNonceKey - bNonceKey) } - return Number(aNonceKey - bNonceKey) - } + return 0 + }) + .slice() - return 0 - }) + const bundles: UserOperationInfo[][] = [] - let opsTaken = 0 - let gasUsed = 0n - const result: UserOperationInfo[] = [] + // Process all outstanding ops. + while (outstandingUserOperations.length > 0) { + // If maxBundles is set and we reached the limit, break. + if (maxBundleCount && bundles.length >= maxBundleCount) { + break + } - // paymaster deposit should be enough for all UserOps in the bundle. - let paymasterDeposit: { [paymaster: string]: bigint } = {} - // throttled paymasters and factories are allowed only small UserOps per bundle. - let stakedEntityCount: { [addr: string]: number } = {} - // each sender is allowed only once per bundle - let senders = new Set() - let knownEntities = this.getKnownEntities() + // Reset state per bundle + const currentBundle: UserOperationInfo[] = [] + let gasUsed = 0n - let storageMap: StorageMap = {} + let paymasterDeposit: { [paymaster: string]: bigint } = {} // paymaster deposit should be enough for all UserOps in the bundle. + let stakedEntityCount: { [addr: string]: number } = {} // throttled paymasters and factories are allowed only small UserOps per bundle. + let senders = new Set() // each sender is allowed only once per bundle + let knownEntities = this.getKnownEntities() + let storageMap: StorageMap = {} - for (const opInfo of outstandingUserOperations) { - const op = opInfo.userOperation - gasUsed += op.callGasLimit + op.verificationGasLimit + // Keep adding ops to current bundle. + while (outstandingUserOperations.length > 0) { + const opInfo = outstandingUserOperations.shift() + if (!opInfo) break - if (isVersion07(op)) { - gasUsed += - (op.paymasterPostOpGasLimit ?? 0n) + - (op.paymasterVerificationGasLimit ?? 0n) - } + const skipResult = await this.shouldSkip( + opInfo, + paymasterDeposit, + stakedEntityCount, + knownEntities, + senders, + storageMap + ) + if (skipResult.skip) continue - if (gasUsed > maxGasLimit && opsTaken >= (minOps || 0)) { - break - } - const skipResult = await this.shouldSkip( - opInfo, - paymasterDeposit, - stakedEntityCount, - knownEntities, - senders, - storageMap - ) - if (skipResult.skip) { - continue - } + const op = opInfo.userOperation + gasUsed += + op.callGasLimit + + op.verificationGasLimit + + (isVersion07(op) + ? (op.paymasterPostOpGasLimit || 0n) + + (op.paymasterVerificationGasLimit || 0n) + : 0n) + + // Only break on gas limit if we've hit minOpsPerBundle. + if ( + gasUsed > maxGasLimit && + currentBundle.length >= minOpsPerBundle + ) { + outstandingUserOperations.unshift(opInfo) // re-add op to front of queue + break + } - paymasterDeposit = skipResult.paymasterDeposit - stakedEntityCount = skipResult.stakedEntityCount - knownEntities = skipResult.knownEntities - senders = skipResult.senders - storageMap = skipResult.storageMap + // Update state based on skip result + paymasterDeposit = skipResult.paymasterDeposit + stakedEntityCount = skipResult.stakedEntityCount + knownEntities = skipResult.knownEntities + senders = skipResult.senders + storageMap = skipResult.storageMap - this.reputationManager.decreaseUserOperationCount(op) - this.store.removeOutstanding(opInfo.userOperationHash) - this.store.addProcessing(opInfo) - result.push(opInfo) - opsTaken++ - } - return result - } + this.reputationManager.decreaseUserOperationCount(op) + this.store.removeOutstanding(opInfo.userOperationHash) + this.store.addProcessing(opInfo) - get(opHash: HexData32): UserOperation | null { - const outstanding = this.store - .dumpOutstanding() - .find((op) => op.userOperationHash === opHash) - if (outstanding) { - return outstanding.userOperation - } + // Add op to current bundle + currentBundle.push(opInfo) + } - const submitted = this.store - .dumpSubmitted() - .find((op) => op.userOperation.userOperationHash === opHash) - if (submitted) { - return submitted.userOperation.userOperation + if (currentBundle.length > 0) { + bundles.push(currentBundle) + } } - return null + return bundles } // For a specfic user operation, get all the queued user operations diff --git a/src/rpc/rpcHandler.ts b/src/rpc/rpcHandler.ts index 00213930..9aa57a10 100644 --- a/src/rpc/rpcHandler.ts +++ b/src/rpc/rpcHandler.ts @@ -585,9 +585,8 @@ export class RpcHandler implements IRpcEndpoint { async debug_bundler_sendBundleNow(): Promise { this.ensureDebugEndpointsAreEnabled("debug_bundler_sendBundleNow") - - const transactions = await this.executorManager.bundleNow() - return transactions[0] + const transaction = await this.executorManager.sendBundleNow() + return transaction } async debug_bundler_setBundlingMode( diff --git a/src/types/mempool.ts b/src/types/mempool.ts index 881f9644..4fdf6e1a 100644 --- a/src/types/mempool.ts +++ b/src/types/mempool.ts @@ -16,7 +16,6 @@ export type TransactionInfo = { entryPoint: Address isVersion06: boolean transactionRequest: { - to: Address gas: bigint chain: Chain maxFeePerGas: bigint @@ -26,7 +25,6 @@ export type TransactionInfo = { executor: Account userOperationInfos: UserOperationInfo[] lastReplaced: number - firstSubmitted: number timesPotentiallyIncluded: number } @@ -34,8 +32,6 @@ export type UserOperationInfo = { userOperation: UserOperation userOperationHash: HexData32 entryPoint: Address - lastReplaced: number - firstSubmitted: number referencedContracts?: ReferencedCodeHashes } @@ -76,3 +72,5 @@ export type BundleResult = reason: string userOps: UserOperation[] } + +export type BundleRequest = {} From e571611924401ac560bb796a64cb5095fbbaa73c Mon Sep 17 00:00:00 2001 From: mouseless <97399882+mouseless-eth@users.noreply.github.com> Date: Sun, 26 Jan 2025 21:49:01 +0000 Subject: [PATCH 08/34] create UserOperationBundle type --- src/executor/executor.ts | 40 +++----- src/executor/executorManager.ts | 173 +++++++++++++------------------- src/mempool/mempool.ts | 32 ++++-- src/types/mempool.ts | 6 ++ 4 files changed, 114 insertions(+), 137 deletions(-) diff --git a/src/executor/executor.ts b/src/executor/executor.ts index 4246132a..6e3314e6 100644 --- a/src/executor/executor.ts +++ b/src/executor/executor.ts @@ -10,7 +10,8 @@ import { type TransactionInfo, type UserOperation, type UserOperationV07, - type GasPriceParameters + type GasPriceParameters, + UserOperationBundle } from "@alto/types" import type { Logger, Metrics } from "@alto/utils" import { @@ -521,21 +522,12 @@ export class Executor { async bundle( wallet: Account, - entryPoint: Address, - ops: UserOperation[] + bundle: UserOperationBundle ): Promise { - // Find bundle EntryPoint version. - const firstOpVersion = isVersion06(ops[0]) - const allSameVersion = ops.every( - (op) => isVersion06(op) === firstOpVersion - ) - if (!allSameVersion) { - throw new Error("All user operations must be of the same version") - } - const isUserOpV06 = firstOpVersion + const { entryPoint, userOperations, version } = bundle const ep = getContract({ - abi: isUserOpV06 ? EntryPointV06Abi : EntryPointV07Abi, + abi: version === "0.6" ? EntryPointV06Abi : EntryPointV07Abi, address: entryPoint, client: { public: this.config.publicClient, @@ -544,7 +536,7 @@ export class Executor { }) let childLogger = this.logger.child({ - userOperations: this.getOpHashes(ops), + userOperations: this.getOpHashes(userOperations), entryPoint }) childLogger.debug("bundling user operation") @@ -568,15 +560,15 @@ export class Executor { return { status: "bundle_resubmit", reason: "Failed to get parameters for bundling", - userOps: ops + userOps: userOperations } } let estimateResult = await filterOpsAndEstimateGas({ - isUserOpV06, + isUserOpV06: version === "0.6", + ops: userOperations, ep, wallet, - ops, nonce, maxFeePerGas: gasPriceParameters.maxFeePerGas, maxPriorityFeePerGas: gasPriceParameters.maxPriorityFeePerGas, @@ -592,7 +584,7 @@ export class Executor { return { status: "bundle_failure", reason: "INTERNAL FAILURE", - userOps: ops + userOps: userOperations } } @@ -603,7 +595,7 @@ export class Executor { return { status: "bundle_failure", reason: "INTERNAL FAILURE", - userOps: ops + userOps: userOperations } } @@ -655,7 +647,7 @@ export class Executor { // TODO: move this to a seperate utility const userOps = opsToBundle.map((op) => { - if (isUserOpV06) { + if (version === "0.6") { return op } return toPackedUserOperation(op as UserOperationV07) @@ -665,7 +657,7 @@ export class Executor { txParam: { ops: userOps, isReplacementTx: false, - isUserOpVersion06: isUserOpV06, + isUserOpVersion06: version === "0.6", entryPoint }, opts @@ -685,7 +677,7 @@ export class Executor { return { status: "bundle_resubmit", reason: InsufficientFundsError.name, - userOps: ops + userOps: userOperations } } @@ -697,7 +689,7 @@ export class Executor { return { status: "bundle_failure", reason: "INTERNAL FAILURE", - userOps: ops + userOps: userOperations } } @@ -713,7 +705,7 @@ export class Executor { const transactionInfo: TransactionInfo = { entryPoint, - isVersion06: isUserOpV06, + isVersion06: version === "0.6", transactionHash: transactionHash, previousTransactionHashes: [], transactionRequest: { diff --git a/src/executor/executorManager.ts b/src/executor/executorManager.ts index f722c8d3..a9fe80d3 100644 --- a/src/executor/executorManager.ts +++ b/src/executor/executorManager.ts @@ -5,15 +5,14 @@ import type { Monitor } from "@alto/mempool" import { - type BundleResult, type BundlingMode, EntryPointV06Abi, type HexData32, type UserOperation, type SubmittedUserOperation, type TransactionInfo, - type UserOperationInfo, - RejectedUserOperation + RejectedUserOperation, + UserOperationBundle } from "@alto/types" import type { BundlingStatus, Logger, Metrics } from "@alto/utils" import { @@ -31,8 +30,7 @@ import { TransactionReceiptNotFoundError, type WatchBlocksReturnType, getAbiItem, - Hex, - Account + Hex } from "viem" import type { Executor, ReplaceTransactionResult } from "./executor" import type { AltoConfig } from "../createConfig" @@ -141,10 +139,17 @@ export class ExecutorManager { const opsCount: number = bundles .map(({ userOperations }) => userOperations.length) .reduce((a, b) => a + b) - const timestamp: number = Date.now() - this.opsCount.push(...Array(opsCount).fill(timestamp)) // Add timestamps for each task - await this.sendBundles(bundles) + // Add timestamps for each task + const timestamp = Date.now() + this.opsCount.push(...Array(opsCount).fill(timestamp)) + + // Send bundles to executor + await Promise.all( + bundles.map(async (bundle) => { + await this.sendBundleToExecutor(bundle) + }) + ) } const rpm: number = this.opsCount.length @@ -159,20 +164,17 @@ export class ExecutorManager { } } - async getMempoolBundles(maxBundleCount?: number) { + async getMempoolBundles( + maxBundleCount?: number + ): Promise { const bundlePromises = this.config.entrypoints.map( async (entryPoint) => { - const mempoolBundles = await this.mempool.process({ + return await this.mempool.process({ entryPoint, maxGasLimit: this.config.maxGasPerBundle, minOpsPerBundle: 1, maxBundleCount }) - - return mempoolBundles.map((userOperations) => ({ - entryPoint, - userOperations - })) } ) @@ -186,16 +188,11 @@ export class ExecutorManager { async sendBundleNow(): Promise { const bundle = (await this.getMempoolBundles(1))[0] - const { entryPoint, userOperations } = bundle - if (userOperations.length === 0) { + if (bundle.userOperations.length === 0) { throw new Error("no ops to bundle") } - const txHashes = await this.sendBundleToExecutor( - entryPoint, - userOperations.map((op) => op.userOperation) - ) - const txHash = txHashes[0] + const txHash = await this.sendBundleToExecutor(bundle) if (!txHash) { throw new Error("no tx hash") @@ -212,101 +209,67 @@ export class ExecutorManager { ) } - async sendBundleToExecutor(bundle: { - entryPoint: Address - userOps: UserOperation[] - }): Promise { - if (bundle.userOps.length === 0) { - return [] + async sendBundleToExecutor( + bundleToSend: UserOperationBundle + ): Promise { + const { entryPoint, userOperations } = bundleToSend + if (userOperations.length === 0) { + return undefined } - const bundles: { wallet: Account; bundle: BundleResult }[] = [] - if (userOps.length > 0) { - const wallet = await this.senderManager.getWallet() - bundles.push({ - wallet, - bundle: await this.executor.bundle(wallet, entryPoint, userOps) - }) + const wallet = await this.senderManager.getWallet() + const bundle = await this.executor.bundle(wallet, bundleToSend) + + switch (bundle.status) { + case "bundle_success": + this.metrics.bundlesSubmitted + .labels({ status: "success" }) + .inc() + break + case "bundle_failure": + this.metrics.bundlesSubmitted.labels({ status: "failed" }).inc() + break + case "bundle_resubmit": + this.metrics.bundlesSubmitted + .labels({ status: "resubmit" }) + .inc() + break } - let txHashes: Hex[] = [] - for (const { wallet, bundle } of bundles) { - switch (bundle.status) { - case "bundle_success": - this.metrics.bundlesSubmitted - .labels({ status: "success" }) - .inc() - break - case "bundle_failure": - this.metrics.bundlesSubmitted - .labels({ status: "failed" }) - .inc() - break - case "bundle_resubmit": - this.metrics.bundlesSubmitted - .labels({ status: "resubmit" }) - .inc() - break - } + // Free wallet if the wallet did not make a succesful bundle tx. + if ( + bundle.status === "bundle_failure" || + bundle.status === "bundle_resubmit" + ) { + this.senderManager.markWalletProcessed(wallet) + } - // Free wallet if the wallet did not make a succesful bundle tx. - if ( - bundle.status === "bundle_failure" || - bundle.status === "bundle_resubmit" - ) { - this.senderManager.markWalletProcessed(wallet) - } + if (bundle.status === "bundle_resubmit") { + const { userOps: userOperations, reason } = bundle + this.resubmitUserOperations(userOperations, entryPoint, reason) + } - if (bundle.status === "bundle_resubmit") { - const { userOps: userOperations, reason } = bundle - this.resubmitUserOperations(userOperations, entryPoint, reason) - } + if (bundle.status === "bundle_failure") { + const { userOps, reason } = bundle - if (bundle.status === "bundle_failure") { - const { userOps, reason } = bundle + const droppedUserOperations = userOps.map((op) => ({ + userOperation: op, + reason + })) + this.dropUserOperations(droppedUserOperations) + } - const droppedUserOperations = userOps.map((op) => ({ - userOperation: op, - reason - })) - this.dropUserOperations(droppedUserOperations) - } + if (bundle.status === "bundle_success") { + const { userOpsBundled, rejectedUserOperations, transactionInfo } = + bundle - if (bundle.status === "bundle_success") { - const { - userOpsBundled, - rejectedUserOperations, - transactionInfo - } = bundle - txHashes.push(transactionInfo.transactionHash) - - this.markUserOperationsAsSubmitted( - userOpsBundled, - transactionInfo - ) + this.markUserOperationsAsSubmitted(userOpsBundled, transactionInfo) + this.dropUserOperations(rejectedUserOperations) - this.dropUserOperations(rejectedUserOperations) - } + return transactionInfo.transactionHash } - return txHashes - } - - async sendBundles( - bundles: { - entryPoint: Address - userOperations: UserOperationInfo[] - }[] = [] - ) { - await Promise.all( - bundles.map(async (bundle) => { - const { entryPoint, userOperations } = bundle - await this.sendBundleToExecutor( - entryPoint, - userOperations.map((op) => op.userOperation) - ) - }) - ) + return undefined } startWatchingBlocks(handleBlock: (block: Block) => void): void { diff --git a/src/mempool/mempool.ts b/src/mempool/mempool.ts index d292dbb8..727da852 100644 --- a/src/mempool/mempool.ts +++ b/src/mempool/mempool.ts @@ -14,7 +14,8 @@ import { type UserOperation, type UserOperationInfo, ValidationErrors, - type ValidationResult + type ValidationResult, + UserOperationBundle } from "@alto/types" import type { Metrics } from "@alto/utils" import type { Logger } from "@alto/utils" @@ -679,7 +680,7 @@ export class MemoryMempool { entryPoint: Address minOpsPerBundle: number maxBundleCount?: number - }): Promise { + }): Promise { let outstandingUserOperations = this.store .dumpOutstanding() .filter((op) => op.entryPoint === entryPoint) @@ -709,7 +710,18 @@ export class MemoryMempool { }) .slice() - const bundles: UserOperationInfo[][] = [] + // Get EntryPoint version. (Ideally version should be derived from EntryPoint) + const isV6 = isVersion06(outstandingUserOperations[0].userOperation) + const allSameVersion = outstandingUserOperations.every( + ({ userOperation }) => isVersion06(userOperation) === isV6 + ) + if (!allSameVersion) { + throw new Error( + "All user operations from same EntryPoint must be of the same version" + ) + } + + const bundles: UserOperationBundle[] = [] // Process all outstanding ops. while (outstandingUserOperations.length > 0) { @@ -718,8 +730,12 @@ export class MemoryMempool { break } - // Reset state per bundle - const currentBundle: UserOperationInfo[] = [] + // Setup for next bundle. + const currentBundle: UserOperationBundle = { + entryPoint, + version: isV6 ? "0.6" : "0.7", + userOperations: [] + } let gasUsed = 0n let paymasterDeposit: { [paymaster: string]: bigint } = {} // paymaster deposit should be enough for all UserOps in the bundle. @@ -755,7 +771,7 @@ export class MemoryMempool { // Only break on gas limit if we've hit minOpsPerBundle. if ( gasUsed > maxGasLimit && - currentBundle.length >= minOpsPerBundle + currentBundle.userOperations.length >= minOpsPerBundle ) { outstandingUserOperations.unshift(opInfo) // re-add op to front of queue break @@ -773,10 +789,10 @@ export class MemoryMempool { this.store.addProcessing(opInfo) // Add op to current bundle - currentBundle.push(opInfo) + currentBundle.userOperations.push(op) } - if (currentBundle.length > 0) { + if (currentBundle.userOperations.length > 0) { bundles.push(currentBundle) } } diff --git a/src/types/mempool.ts b/src/types/mempool.ts index 4fdf6e1a..d17c6a32 100644 --- a/src/types/mempool.ts +++ b/src/types/mempool.ts @@ -28,6 +28,12 @@ export type TransactionInfo = { timesPotentiallyIncluded: number } +export type UserOperationBundle = { + entryPoint: Address + version: "0.6" | "0.7" + userOperations: UserOperation[] +} + export type UserOperationInfo = { userOperation: UserOperation userOperationHash: HexData32 From c51dfe2d415234a2ee4f0414df50d7128d0859b2 Mon Sep 17 00:00:00 2001 From: mouseless <97399882+mouseless-eth@users.noreply.github.com> Date: Sun, 26 Jan 2025 22:14:39 +0000 Subject: [PATCH 09/34] cleanup --- src/executor/executor.ts | 49 ++++-------- src/executor/executorManager.ts | 134 +++++++++++++++++--------------- src/types/mempool.ts | 4 +- 3 files changed, 88 insertions(+), 99 deletions(-) diff --git a/src/executor/executor.ts b/src/executor/executor.ts index 6e3314e6..2ede873b 100644 --- a/src/executor/executor.ts +++ b/src/executor/executor.ts @@ -16,7 +16,6 @@ import { import type { Logger, Metrics } from "@alto/utils" import { getUserOperationHash, - isVersion06, maxBigInt, parseViemError, scaleBigIntByPercent, @@ -118,14 +117,11 @@ export class Executor { async replaceTransaction( transactionInfo: TransactionInfo ): Promise { - const { - isVersion06, - entryPoint, - transactionRequest, - userOperationInfos, - executor, - transactionHash - } = transactionInfo + const { transactionRequest, executor, transactionHash, bundle } = + transactionInfo + + const { userOperations, version, entryPoint } = bundle + const isVersion06 = version === "0.6" let gasPriceParameters: GasPriceParameters try { @@ -148,9 +144,7 @@ export class Executor { ) } - const opsToResubmit = userOperationInfos.map( - (optr) => optr.userOperation - ) + const opsToResubmit = userOperations const ep = getContract({ abi: isVersion06 ? EntryPointV06Abi : EntryPointV07Abi, @@ -223,7 +217,7 @@ export class Executor { isUserOpVersion06: isVersion06, isReplacementTx: true, ops: userOps, - entryPoint: transactionInfo.entryPoint + entryPoint: transactionInfo.bundle.entryPoint } try { @@ -272,13 +266,10 @@ export class Executor { ...transactionInfo.previousTransactionHashes ], lastReplaced: Date.now(), - userOperationInfos: opsToBundle.map((op) => { - return { - entryPoint, - userOperation: op, - userOperationHash: this.getOpHashes([op])[0] - } - }) + bundle: { + ...transactionInfo.bundle, + userOperations: opsToBundle + } } return { @@ -693,19 +684,12 @@ export class Executor { } } - const userOperationInfos = opsToBundle.map((op) => { - return { - entryPoint, - userOperation: op, - userOperationHash: this.getOpHashes([op])[0], - lastReplaced: Date.now(), - firstSubmitted: Date.now() - } - }) - const transactionInfo: TransactionInfo = { - entryPoint, - isVersion06: version === "0.6", + bundle: { + entryPoint, + version, + userOperations: opsToBundle + }, transactionHash: transactionHash, previousTransactionHashes: [], transactionRequest: { @@ -716,7 +700,6 @@ export class Executor { nonce: nonce }, executor: wallet, - userOperationInfos, lastReplaced: Date.now(), timesPotentiallyIncluded: 0 } diff --git a/src/executor/executorManager.ts b/src/executor/executorManager.ts index a9fe80d3..3066afb1 100644 --- a/src/executor/executorManager.ts +++ b/src/executor/executorManager.ts @@ -201,10 +201,10 @@ export class ExecutorManager { return txHash } - getOpHash(userOperation: UserOperation): HexData32 { + getOpHash(userOperation: UserOperation, entryPoint: Address): HexData32 { return getUserOperationHash( userOperation, - this.config.entrypoints[0], + entryPoint, this.config.publicClient.chain.id ) } @@ -256,7 +256,7 @@ export class ExecutorManager { userOperation: op, reason })) - this.dropUserOperations(droppedUserOperations) + this.dropUserOperations(droppedUserOperations, entryPoint) } if (bundle.status === "bundle_success") { @@ -264,7 +264,7 @@ export class ExecutorManager { bundle this.markUserOperationsAsSubmitted(userOpsBundled, transactionInfo) - this.dropUserOperations(rejectedUserOperations) + this.dropUserOperations(rejectedUserOperations, entryPoint) return transactionInfo.transactionHash } @@ -304,10 +304,11 @@ export class ExecutorManager { ) { const { transactionHash: currentTransactionHash, - userOperationInfos: opInfos, - previousTransactionHashes, - isVersion06 + bundle, + previousTransactionHashes } = transactionInfo + const { userOperations, version } = bundle + const isVersion06 = version === "0.6" const txHashesToCheck = [ currentTransactionHash, @@ -337,15 +338,6 @@ export class ExecutorManager { const finalizedTransaction = mined ?? reverted if (!finalizedTransaction) { - for (const { userOperationHash } of opInfos) { - this.logger.trace( - { - userOperationHash, - currentTransactionHash - }, - "user op still pending" - ) - } return } @@ -359,11 +351,14 @@ export class ExecutorManager { if (bundlingStatus.status === "included") { this.metrics.userOperationsOnChain .labels({ status: bundlingStatus.status }) - .inc(opInfos.length) + .inc(userOperations.length) const { userOperationDetails } = bundlingStatus - opInfos.map((opInfo) => { - const { userOperation, userOperationHash, entryPoint } = opInfo + userOperations.map((userOperation) => { + const userOperationHash = this.getOpHash( + userOperation, + entryPoint + ) const opDetails = userOperationDetails[userOperationHash] // TODO: keep this metric @@ -418,17 +413,22 @@ export class ExecutorManager { await this.replaceTransaction(transactionInfo, "AA95") } else { await Promise.all( - opInfos.map(({ userOperationHash }) => { + userOperations.map((userOperation) => { this.checkFrontrun({ - userOperationHash, + userOperationHash: this.getOpHash( + userOperation, + entryPoint + ), transactionHash, blockNumber }) }) ) - opInfos.map(({ userOperationHash }) => { - this.mempool.removeSubmitted(userOperationHash) + userOperations.map((userOperation) => { + this.mempool.removeSubmitted( + this.getOpHash(userOperation, entryPoint) + ) }) this.senderManager.markWalletProcessed(transactionInfo.executor) } @@ -749,18 +749,20 @@ export class ExecutorManager { } if (replaceResult.status === "failed") { + const { transactionHash, bundle } = txInfo + this.logger.warn( - { oldTxHash: txInfo.transactionHash, reason }, + { oldTxHash: transactionHash, reason }, "failed to replace transaction" ) - const droppedUserOperations = txInfo.userOperationInfos.map( - (opInfo) => ({ - userOperation: opInfo.userOperation, + const droppedUserOperations = bundle.userOperations.map( + (userOperation) => ({ + userOperation, reason: "Failed to replace transaction" }) ) - this.dropUserOperations(droppedUserOperations) + this.dropUserOperations(droppedUserOperations, bundle.entryPoint) this.senderManager.markWalletProcessed(txInfo.executor) return } @@ -773,8 +775,10 @@ export class ExecutorManager { txInfo.timesPotentiallyIncluded += 1 if (txInfo.timesPotentiallyIncluded >= 3) { - txInfo.userOperationInfos.map((opInfo) => { - this.mempool.removeSubmitted(opInfo.userOperationHash) + txInfo.bundle.userOperations.map((userOperation) => { + this.mempool.removeSubmitted( + this.getOpHash(userOperation, txInfo.bundle.entryPoint) + ) }) const txSender = txInfo.executor this.senderManager.markWalletProcessed(txSender) @@ -789,34 +793,35 @@ export class ExecutorManager { const newTxInfo = replaceResult.transactionInfo - const missingOps = txInfo.userOperationInfos.filter( - (info) => - !newTxInfo.userOperationInfos - .map((ni) => ni.userOperationHash) - .includes(info.userOperationHash) - ) - - const matchingOps = txInfo.userOperationInfos.filter((info) => - newTxInfo.userOperationInfos - .map((ni) => ni.userOperationHash) - .includes(info.userOperationHash) - ) - - matchingOps.map((opInfo) => { - this.mempool.replaceSubmitted(opInfo, newTxInfo) - }) - - missingOps.map((opInfo) => { - this.mempool.removeSubmitted(opInfo.userOperationHash) - this.logger.warn( - { - oldTxHash: txInfo.transactionHash, - newTxHash: newTxInfo.transactionHash, - reason - }, - "missing op in new tx" - ) - }) + // TODO: FIX THIS USING BUDNLE_RESULT SUCCESS opsBundles + opsRejected + //const missingOps = txInfo.bundle.userOperations.filter( + // (info) => + // !newTxInfo.userOperationInfos + // .map((ni) => ni.userOperationHash) + // .includes(info.userOperationHash) + //) + + //const matchingOps = txInfo.userOperationInfos.filter((info) => + // newTxInfo.userOperationInfos + // .map((ni) => ni.userOperationHash) + // .includes(info.userOperationHash) + //) + + //matchingOps.map((opInfo) => { + // this.mempool.replaceSubmitted(opInfo, newTxInfo) + //}) + + //missingOps.map((opInfo) => { + // this.mempool.removeSubmitted(opInfo.userOperationHash) + // this.logger.warn( + // { + // oldTxHash: txInfo.transactionHash, + // newTxHash: newTxInfo.transactionHash, + // reason + // }, + // "missing op in new tx" + // ) + //}) this.logger.info( { @@ -835,7 +840,7 @@ export class ExecutorManager { transactionInfo: TransactionInfo ) { userOperations.map((op) => { - const opHash = this.getOpHash(op) + const opHash = this.getOpHash(op, transactionInfo.bundle.entryPoint) this.mempool.markSubmitted(opHash, transactionInfo) @@ -859,20 +864,23 @@ export class ExecutorManager { userOperations.map((op) => { this.logger.info( { - userOpHash: this.getOpHash(op), + userOpHash: this.getOpHash(op, entryPoint), reason }, "resubmitting user operation" ) - this.mempool.removeProcessing(this.getOpHash(op)) + this.mempool.removeProcessing(this.getOpHash(op, entryPoint)) this.mempool.add(op, entryPoint) this.metrics.userOperationsResubmitted.inc() }) } - dropUserOperations(rejectedUserOperations: RejectedUserOperation[]) { + dropUserOperations( + rejectedUserOperations: RejectedUserOperation[], + entryPoint: Address + ) { rejectedUserOperations.map(({ userOperation, reason }) => { - const userOpHash = this.getOpHash(userOperation) + const userOpHash = this.getOpHash(userOperation, entryPoint) this.mempool.removeProcessing(userOpHash) this.eventManager.emitDropped( userOpHash, diff --git a/src/types/mempool.ts b/src/types/mempool.ts index d17c6a32..ece9e365 100644 --- a/src/types/mempool.ts +++ b/src/types/mempool.ts @@ -13,8 +13,6 @@ export interface ReferencedCodeHashes { export type TransactionInfo = { transactionHash: HexData32 previousTransactionHashes: HexData32[] - entryPoint: Address - isVersion06: boolean transactionRequest: { gas: bigint chain: Chain @@ -22,8 +20,8 @@ export type TransactionInfo = { maxPriorityFeePerGas: bigint nonce: number } + bundle: UserOperationBundle executor: Account - userOperationInfos: UserOperationInfo[] lastReplaced: number timesPotentiallyIncluded: number } From 81a6bb093b8876999d43273c1747b8d4f850b692 Mon Sep 17 00:00:00 2001 From: mouseless <97399882+mouseless-eth@users.noreply.github.com> Date: Mon, 27 Jan 2025 00:57:22 +0000 Subject: [PATCH 10/34] cleanup executor bundle method --- src/executor/executor.ts | 128 +++++++--------------- src/executor/executorManager.ts | 117 ++++++++++++-------- src/executor/filterOpsAndEStimateGas.ts | 7 +- src/rpc/rpcHandler.ts | 137 +++++++++++------------- src/types/mempool.ts | 24 +++-- 5 files changed, 202 insertions(+), 211 deletions(-) diff --git a/src/executor/executor.ts b/src/executor/executor.ts index 2ede873b..95ebb55b 100644 --- a/src/executor/executor.ts +++ b/src/executor/executor.ts @@ -50,7 +50,7 @@ export interface GasEstimateResult { } export type HandleOpsTxParam = { - ops: PackedUserOperation[] + ops: UserOperation[] isUserOpVersion06: boolean isReplacementTx: boolean entryPoint: Address @@ -209,14 +209,10 @@ export class Executor { // update calldata to include only ops that pass simulation let txParam: HandleOpsTxParam - const userOps = opsToBundle.map((op) => - isVersion06 ? op : toPackedUserOperation(op as UserOperationV07) - ) as PackedUserOperation[] - txParam = { isUserOpVersion06: isVersion06, isReplacementTx: true, - ops: userOps, + ops: opsToBundle, entryPoint: transactionInfo.bundle.entryPoint } @@ -350,20 +346,24 @@ export class Executor { nonce: number } }) { - let data: Hex - let to: Address - const { isUserOpVersion06, ops, entryPoint } = txParam - data = encodeFunctionData({ + + const packedOps = ops.map((op) => { + if (isUserOpVersion06) { + return op + } + return toPackedUserOperation(op as UserOperationV07) + }) as PackedUserOperation[] + + const data = encodeFunctionData({ abi: isUserOpVersion06 ? EntryPointV06Abi : EntryPointV07Abi, functionName: "handleOps", - args: [ops, opts.account.address] + args: [packedOps, opts.account.address] }) - to = entryPoint const request = await this.config.walletClient.prepareTransactionRequest({ - to, + to: entryPoint, data, ...opts }) @@ -511,10 +511,17 @@ export class Executor { } } - async bundle( - wallet: Account, + async bundle({ + wallet, + bundle, + nonce, + gasPriceParameters + }: { + wallet: Account bundle: UserOperationBundle - ): Promise { + nonce: number + gasPriceParameters: GasPriceParameters + }): Promise { const { entryPoint, userOperations, version } = bundle const ep = getContract({ @@ -530,30 +537,6 @@ export class Executor { userOperations: this.getOpHashes(userOperations), entryPoint }) - childLogger.debug("bundling user operation") - - // These calls can throw, so we try/catch them to mark wallet as processed in event of error. - let nonce: number - let gasPriceParameters: GasPriceParameters - try { - ;[gasPriceParameters, nonce] = await Promise.all([ - this.gasPriceManager.tryGetNetworkGasPrice(), - this.config.publicClient.getTransactionCount({ - address: wallet.address, - blockTag: "pending" - }) - ]) - } catch (err) { - childLogger.error( - { error: err }, - "Failed to get parameters for bundling" - ) - return { - status: "bundle_resubmit", - reason: "Failed to get parameters for bundling", - userOps: userOperations - } - } let estimateResult = await filterOpsAndEstimateGas({ isUserOpV06: version === "0.6", @@ -573,23 +556,22 @@ export class Executor { "gas limit simulation encountered unexpected failure" ) return { - status: "bundle_failure", + status: "unhandled_simulation_failure", reason: "INTERNAL FAILURE", userOps: userOperations } } - let { gasLimit, opsToBundle, failedOps } = estimateResult - - if (opsToBundle.length === 0) { + if (estimateResult.status === "allOpsFailedSimulation") { childLogger.warn("all ops failed simulation") return { - status: "bundle_failure", - reason: "INTERNAL FAILURE", - userOps: userOperations + status: "all_ops_failed_simulation", + rejectedUserOps: estimateResult.failedOps } } + let { gasLimit, opsToBundle, failedOps } = estimateResult + // Update child logger with userOperations being sent for bundling. childLogger = this.logger.child({ userOperations: this.getOpHashes(opsToBundle), @@ -636,17 +618,9 @@ export class Executor { } } - // TODO: move this to a seperate utility - const userOps = opsToBundle.map((op) => { - if (version === "0.6") { - return op - } - return toPackedUserOperation(op as UserOperationV07) - }) as PackedUserOperation[] - transactionHash = await this.sendHandleOpsTransaction({ txParam: { - ops: userOps, + ops: opsToBundle, isReplacementTx: false, isUserOpVersion06: version === "0.6", entryPoint @@ -666,7 +640,7 @@ export class Executor { "insufficient funds, not submitting transaction" ) return { - status: "bundle_resubmit", + status: "bundle_submission_failure", reason: InsufficientFundsError.name, userOps: userOperations } @@ -678,54 +652,34 @@ export class Executor { "error submitting bundle transaction" ) return { - status: "bundle_failure", + status: "bundle_submission_failure", reason: "INTERNAL FAILURE", userOps: userOperations } } - const transactionInfo: TransactionInfo = { - bundle: { - entryPoint, - version, - userOperations: opsToBundle - }, - transactionHash: transactionHash, - previousTransactionHashes: [], + const bundleResult: BundleResult = { + status: "bundle_success", + userOpsBundled: opsToBundle, + rejectedUserOperations: failedOps, + transactionHash, transactionRequest: { gas: gasLimit, - chain: this.config.walletClient.chain, maxFeePerGas: gasPriceParameters.maxFeePerGas, maxPriorityFeePerGas: gasPriceParameters.maxPriorityFeePerGas, - nonce: nonce - }, - executor: wallet, - lastReplaced: Date.now(), - timesPotentiallyIncluded: 0 - } - - const userOperationResults: BundleResult = { - status: "bundle_success", - userOpsBundled: opsToBundle, - rejectedUserOperations: failedOps.map((sop) => ({ - userOperation: sop.userOperation, - reason: sop.reason - })), - transactionInfo + nonce + } } childLogger.info( { - transactionRequest: { - ...transactionInfo.transactionRequest, - abi: undefined - }, + transactionRequest: bundleResult.transactionRequest, txHash: transactionHash, opHashes: this.getOpHashes(opsToBundle) }, "submitted bundle transaction" ) - return userOperationResults + return bundleResult } } diff --git a/src/executor/executorManager.ts b/src/executor/executorManager.ts index 3066afb1..a5b2293e 100644 --- a/src/executor/executorManager.ts +++ b/src/executor/executorManager.ts @@ -12,7 +12,8 @@ import { type SubmittedUserOperation, type TransactionInfo, RejectedUserOperation, - UserOperationBundle + UserOperationBundle, + GasPriceParameters } from "@alto/types" import type { BundlingStatus, Logger, Metrics } from "@alto/utils" import { @@ -30,7 +31,8 @@ import { TransactionReceiptNotFoundError, type WatchBlocksReturnType, getAbiItem, - Hex + Hex, + InsufficientFundsError } from "viem" import type { Executor, ReplaceTransactionResult } from "./executor" import type { AltoConfig } from "../createConfig" @@ -210,63 +212,101 @@ export class ExecutorManager { } async sendBundleToExecutor( - bundleToSend: UserOperationBundle + bundle: UserOperationBundle ): Promise { - const { entryPoint, userOperations } = bundleToSend + const { entryPoint, userOperations, version } = bundle if (userOperations.length === 0) { return undefined } const wallet = await this.senderManager.getWallet() - const bundle = await this.executor.bundle(wallet, bundleToSend) - - switch (bundle.status) { - case "bundle_success": - this.metrics.bundlesSubmitted - .labels({ status: "success" }) - .inc() - break - case "bundle_failure": - this.metrics.bundlesSubmitted.labels({ status: "failed" }).inc() - break - case "bundle_resubmit": - this.metrics.bundlesSubmitted - .labels({ status: "resubmit" }) - .inc() - break + + let nonce: number + let gasPriceParameters: GasPriceParameters + try { + ;[gasPriceParameters, nonce] = await Promise.all([ + this.gasPriceManager.tryGetNetworkGasPrice(), + this.config.publicClient.getTransactionCount({ + address: wallet.address, + blockTag: "latest" + }) + ]) + } catch (err) { + this.logger.error( + { error: err }, + "Failed to get parameters for bundling" + ) + this.senderManager.markWalletProcessed(wallet) + return undefined } - // Free wallet if the wallet did not make a succesful bundle tx. - if ( - bundle.status === "bundle_failure" || - bundle.status === "bundle_resubmit" - ) { + const bundleResult = await this.executor.bundle({ + wallet, + bundle, + nonce, + gasPriceParameters + }) + + // Free wallet if no bundle was sent. + if (bundleResult.status !== "bundle_success") { this.senderManager.markWalletProcessed(wallet) } - if (bundle.status === "bundle_resubmit") { - const { userOps: userOperations, reason } = bundle - this.resubmitUserOperations(userOperations, entryPoint, reason) + // All ops failed simulation, drop them and return. + if (bundleResult.status === "all_ops_failed_simulation") { + const { rejectedUserOps } = bundleResult + this.dropUserOperations(rejectedUserOps, entryPoint) + return undefined } - if (bundle.status === "bundle_failure") { - const { userOps, reason } = bundle + // Resubmit if chosen executor has insufficient funds. + if ( + bundleResult.status === "bundle_submission_failure" && + bundleResult.reason === InsufficientFundsError.name + ) { + const { userOps: userOperations, reason } = bundleResult + this.resubmitUserOperations(userOperations, entryPoint, reason) + this.metrics.bundlesSubmitted.labels({ status: "resubmit" }).inc() + return undefined + } + if (bundleResult.status === "bundle_submission_failure") { + const { userOps } = bundleResult const droppedUserOperations = userOps.map((op) => ({ userOperation: op, - reason + reason: "INTERNAL FAILURE" })) this.dropUserOperations(droppedUserOperations, entryPoint) + this.metrics.bundlesSubmitted.labels({ status: "failed" }).inc() } - if (bundle.status === "bundle_success") { - const { userOpsBundled, rejectedUserOperations, transactionInfo } = - bundle + if (bundleResult.status === "bundle_success") { + const { + userOpsBundled, + rejectedUserOperations, + transactionRequest, + transactionHash + } = bundleResult + + const transactionInfo: TransactionInfo = { + executor: wallet, + transactionHash, + transactionRequest, + bundle: { + entryPoint, + version, + userOperations: userOpsBundled + }, + previousTransactionHashes: [], + lastReplaced: Date.now(), + timesPotentiallyIncluded: 0 + } this.markUserOperationsAsSubmitted(userOpsBundled, transactionInfo) this.dropUserOperations(rejectedUserOperations, entryPoint) + this.metrics.bundlesSubmitted.labels({ status: "success" }).inc() - return transactionInfo.transactionHash + return transactionHash } return undefined @@ -841,14 +881,7 @@ export class ExecutorManager { ) { userOperations.map((op) => { const opHash = this.getOpHash(op, transactionInfo.bundle.entryPoint) - this.mempool.markSubmitted(opHash, transactionInfo) - - this.monitor.setUserOperationStatus(opHash, { - status: "submitted", - transactionHash: transactionInfo.transactionHash - }) - this.startWatchingBlocks(this.handleBlock.bind(this)) this.metrics.userOperationsSubmitted .labels({ status: "success" }) diff --git a/src/executor/filterOpsAndEStimateGas.ts b/src/executor/filterOpsAndEStimateGas.ts index ec29601b..1398dcb8 100644 --- a/src/executor/filterOpsAndEStimateGas.ts +++ b/src/executor/filterOpsAndEStimateGas.ts @@ -47,6 +47,10 @@ export type FilterOpsAndEstimateGasResult = status: "unexpectedFailure" reason: string } + | { + status: "allOpsFailedSimulation" + failedOps: FailedOpWithReason[] + } // Attempt to create a handleOps bundle + estimate bundling tx gas. export async function filterOpsAndEstimateGas({ @@ -210,6 +214,7 @@ export async function filterOpsAndEstimateGas({ 125n ) } + retriesLeft-- continue } @@ -278,5 +283,5 @@ export async function filterOpsAndEstimateGas({ } } - return { status: "unexpectedFailure", reason: "All ops failed simulation" } + return { status: "allOpsFailedSimulation", failedOps } } diff --git a/src/rpc/rpcHandler.ts b/src/rpc/rpcHandler.ts index 9aa57a10..0c2b12d6 100644 --- a/src/rpc/rpcHandler.ts +++ b/src/rpc/rpcHandler.ts @@ -839,80 +839,69 @@ export class RpcHandler implements IRpcEndpoint { userOperation: UserOperation, entryPoint: Address ) { - if (!this.config.enableInstantBundlingEndpoint) { - throw new RpcError( - "pimlico_sendUserOperationNow endpoint is not enabled", - ValidationErrors.InvalidFields - ) - } - - this.ensureEntryPointIsSupported(entryPoint) - - const opHash = getUserOperationHash( - userOperation, - entryPoint, - this.config.publicClient.chain.id - ) - - await this.preMempoolChecks( - opHash, - userOperation, - apiVersion, - entryPoint - ) - - const result = ( - await this.executor.bundle(entryPoint, [userOperation]) - )[0] - - if (result.status === "failure") { - const { userOpHash, reason } = result.error - this.monitor.setUserOperationStatus(userOpHash, { - status: "rejected", - transactionHash: null - }) - this.logger.warn( - { - userOperation: JSON.stringify( - result.error.userOperation, - (_k, v) => (typeof v === "bigint" ? v.toString() : v) - ), - userOpHash, - reason - }, - "user operation rejected" - ) - this.metrics.userOperationsSubmitted - .labels({ status: "failed" }) - .inc() - - const { error } = result - throw new RpcError( - `userOperation reverted during simulation with reason: ${error.reason}` - ) - } - - const res = result as unknown as { - status: "success" - value: { - userOperation: UserOperationInfo - transactionInfo: TransactionInfo - } - } - - const txSender = res.value.transactionInfo.executor.address - this.executor.markWalletProcessed(txSender) - - // wait for receipt - const receipt = - await this.config.publicClient.waitForTransactionReceipt({ - hash: res.value.transactionInfo.transactionHash, - pollingInterval: 100 - }) - - const userOperationReceipt = parseUserOperationReceipt(opHash, receipt) - - return userOperationReceipt + //if (!this.config.enableInstantBundlingEndpoint) { + // throw new RpcError( + // "pimlico_sendUserOperationNow endpoint is not enabled", + // ValidationErrors.InvalidFields + // ) + //} + //this.ensureEntryPointIsSupported(entryPoint) + //const opHash = getUserOperationHash( + // userOperation, + // entryPoint, + // this.config.publicClient.chain.id + //) + //await this.preMempoolChecks( + // opHash, + // userOperation, + // apiVersion, + // entryPoint + //) + //const result = ( + // await this.executor.bundle(entryPoint, [userOperation]) + //)[0] + //if (result.status === "failure") { + // const { userOpHash, reason } = result.error + // this.monitor.setUserOperationStatus(userOpHash, { + // status: "rejected", + // transactionHash: null + // }) + // this.logger.warn( + // { + // userOperation: JSON.stringify( + // result.error.userOperation, + // (_k, v) => (typeof v === "bigint" ? v.toString() : v) + // ), + // userOpHash, + // reason + // }, + // "user operation rejected" + // ) + // this.metrics.userOperationsSubmitted + // .labels({ status: "failed" }) + // .inc() + // const { error } = result + // throw new RpcError( + // `userOperation reverted during simulation with reason: ${error.reason}` + // ) + //} + //const res = result as unknown as { + // status: "success" + // value: { + // userOperation: UserOperationInfo + // transactionInfo: TransactionInfo + // } + //} + //const txSender = res.value.transactionInfo.executor.address + //this.executor.markWalletProcessed(txSender) + //// wait for receipt + //const receipt = + // await this.config.publicClient.waitForTransactionReceipt({ + // hash: res.value.transactionInfo.transactionHash, + // pollingInterval: 100 + // }) + //const userOperationReceipt = parseUserOperationReceipt(opHash, receipt) + //return userOperationReceipt } async getNonceValue(userOperation: UserOperation, entryPoint: Address) { diff --git a/src/types/mempool.ts b/src/types/mempool.ts index ece9e365..225e6256 100644 --- a/src/types/mempool.ts +++ b/src/types/mempool.ts @@ -1,4 +1,4 @@ -import type { Address, Chain } from "viem" +import type { Address } from "viem" import type { Account } from "viem/accounts" import type { HexData32, UserOperation } from "." @@ -15,7 +15,6 @@ export type TransactionInfo = { previousTransactionHashes: HexData32[] transactionRequest: { gas: bigint - chain: Chain maxFeePerGas: bigint maxPriorityFeePerGas: bigint nonce: number @@ -62,17 +61,28 @@ export type BundleResult = status: "bundle_success" userOpsBundled: UserOperation[] rejectedUserOperations: RejectedUserOperation[] - transactionInfo: TransactionInfo + transactionHash: HexData32 + transactionRequest: { + gas: bigint + maxFeePerGas: bigint + maxPriorityFeePerGas: bigint + nonce: number + } } | { - // Encountered error whilst trying to bundle user operations. - status: "bundle_failure" + // Encountered unhandled error during bundle simulation. + status: "unhandled_simulation_failure" reason: string userOps: UserOperation[] } | { - // Encountered recoverable error whilst trying to bundle user operations. - status: "bundle_resubmit" + // All user operations failed simulation. + status: "all_ops_failed_simulation" + rejectedUserOps: RejectedUserOperation[] + } + | { + // Encountered error whilst trying to bundle user operations. + status: "bundle_submission_failure" reason: string userOps: UserOperation[] } From a7a1f46649ffec81fa48e54c6f093de8e0bb70c9 Mon Sep 17 00:00:00 2001 From: mouseless <97399882+mouseless-eth@users.noreply.github.com> Date: Mon, 27 Jan 2025 02:26:41 +0000 Subject: [PATCH 11/34] cleanup replacement flow --- src/executor/executor.ts | 214 ++-------------------------- src/executor/executorManager.ts | 243 ++++++++++++++++++++++++++------ src/types/mempool.ts | 4 +- 3 files changed, 211 insertions(+), 250 deletions(-) diff --git a/src/executor/executor.ts b/src/executor/executor.ts index 95ebb55b..8e3c57c9 100644 --- a/src/executor/executor.ts +++ b/src/executor/executor.ts @@ -114,205 +114,6 @@ export class Executor { throw new Error("Method not implemented.") } - async replaceTransaction( - transactionInfo: TransactionInfo - ): Promise { - const { transactionRequest, executor, transactionHash, bundle } = - transactionInfo - - const { userOperations, version, entryPoint } = bundle - const isVersion06 = version === "0.6" - - let gasPriceParameters: GasPriceParameters - try { - gasPriceParameters = - await this.gasPriceManager.tryGetNetworkGasPrice() - } catch (err) { - this.logger.error({ error: err }, "Failed to get network gas price") - return { status: "failed" } - } - - const newRequest = { - ...transactionRequest, - maxFeePerGas: scaleBigIntByPercent( - gasPriceParameters.maxFeePerGas, - 115n - ), - maxPriorityFeePerGas: scaleBigIntByPercent( - gasPriceParameters.maxPriorityFeePerGas, - 115n - ) - } - - const opsToResubmit = userOperations - - const ep = getContract({ - abi: isVersion06 ? EntryPointV06Abi : EntryPointV07Abi, - address: entryPoint, - client: { - public: this.config.publicClient, - wallet: this.config.walletClient - } - }) - - const childLogger = this.logger.child({ - transactionHash, - executor: executor - }) - - let bundleResult = await filterOpsAndEstimateGas({ - ep, - isUserOpV06: isVersion06, - wallet: executor, - ops: opsToResubmit, - nonce: newRequest.nonce, - maxFeePerGas: newRequest.maxFeePerGas, - maxPriorityFeePerGas: newRequest.maxPriorityFeePerGas, - reputationManager: this.reputationManager, - config: this.config, - logger: childLogger - }) - - if (bundleResult.status === "unexpectedFailure") { - return { status: "failed" } - } - - let { opsToBundle, failedOps, gasLimit } = bundleResult - - const allOpsFailed = (opsToBundle.length = 0) - const potentiallyIncluded = failedOps.every( - (op) => - op.reason === "AA25 invalid account nonce" || - op.reason === "AA10 sender already constructed" - ) - - if (allOpsFailed && potentiallyIncluded) { - childLogger.trace( - { reasons: failedOps.map((sop) => sop.reason) }, - "all ops failed simulation with nonce error" - ) - return { status: "potentially_already_included" } - } - - if (allOpsFailed) { - childLogger.warn("no ops to bundle") - childLogger.warn("all ops failed simulation") - return { status: "failed" } - } - - // sometimes the estimation rounds down, adding a fixed constant accounts for this - gasLimit += 10_000n - - // 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 - let txParam: HandleOpsTxParam - - txParam = { - isUserOpVersion06: isVersion06, - isReplacementTx: true, - ops: opsToBundle, - entryPoint: transactionInfo.bundle.entryPoint - } - - try { - childLogger.info( - { - newRequest: { - ...newRequest, - abi: undefined, - chain: undefined - }, - executor, - userOperations: this.getOpHashes(opsToBundle) - }, - "replacing transaction" - ) - - const txHash = await this.sendHandleOpsTransaction({ - txParam, - opts: this.config.legacyTransactions - ? { - account: executor, - gasPrice: newRequest.maxFeePerGas, - gas: newRequest.gas, - nonce: newRequest.nonce - } - : { - account: executor, - maxFeePerGas: newRequest.maxFeePerGas, - maxPriorityFeePerGas: newRequest.maxPriorityFeePerGas, - gas: newRequest.gas, - nonce: newRequest.nonce - } - }) - - this.eventManager.emitSubmitted({ - userOpHashes: this.getOpHashes(opsToBundle), - transactionHash: txHash - }) - - const newTxInfo: TransactionInfo = { - ...transactionInfo, - transactionRequest: newRequest, - transactionHash: txHash, - previousTransactionHashes: [ - transactionInfo.transactionHash, - ...transactionInfo.previousTransactionHashes - ], - lastReplaced: Date.now(), - bundle: { - ...transactionInfo.bundle, - userOperations: opsToBundle - } - } - - return { - status: "replaced", - transactionInfo: newTxInfo - } - } catch (err: unknown) { - const e = parseViemError(err) - if (!e) { - sentry.captureException(err) - childLogger.error( - { error: err }, - "unknown error replacing transaction" - ) - } - - if (e instanceof NonceTooLowError) { - childLogger.trace( - { error: e }, - "nonce too low, potentially already included" - ) - return { status: "potentially_already_included" } - } - - if (e instanceof FeeCapTooLowError) { - childLogger.warn({ error: e }, "fee cap too low, not replacing") - } - - if (e instanceof InsufficientFundsError) { - childLogger.warn( - { error: e }, - "insufficient funds, not replacing" - ) - } - - if (e instanceof IntrinsicGasTooLowError) { - childLogger.warn( - { error: e }, - "intrinsic gas too low, not replacing" - ) - } - - childLogger.warn({ error: e }, "error replacing transaction") - return { status: "failed" } - } - } - getOpHashes(userOperations: UserOperation[]): HexData32[] { return userOperations.map((userOperation) => { return getUserOperationHash( @@ -515,12 +316,14 @@ export class Executor { wallet, bundle, nonce, - gasPriceParameters + gasPriceParameters, + gasLimitSuggestion }: { wallet: Account bundle: UserOperationBundle nonce: number gasPriceParameters: GasPriceParameters + gasLimitSuggestion?: bigint }): Promise { const { entryPoint, userOperations, version } = bundle @@ -580,6 +383,9 @@ export class Executor { // sometimes the estimation rounds down, adding a fixed constant accounts for this gasLimit += 10_000n + gasLimit = gasLimitSuggestion + ? maxBigInt(gasLimit, gasLimitSuggestion) + : gasLimit let transactionHash: HexData32 try { @@ -634,14 +440,10 @@ export class Executor { }) } catch (err: unknown) { const e = parseViemError(err) - if (e instanceof InsufficientFundsError) { - childLogger.error( - { error: e }, - "insufficient funds, not submitting transaction" - ) + if (e) { return { status: "bundle_submission_failure", - reason: InsufficientFundsError.name, + reason: e, userOps: userOperations } } diff --git a/src/executor/executorManager.ts b/src/executor/executorManager.ts index a5b2293e..2d3e0695 100644 --- a/src/executor/executorManager.ts +++ b/src/executor/executorManager.ts @@ -13,7 +13,8 @@ import { type TransactionInfo, RejectedUserOperation, UserOperationBundle, - GasPriceParameters + GasPriceParameters, + BundleResult } from "@alto/types" import type { BundlingStatus, Logger, Metrics } from "@alto/utils" import { @@ -32,9 +33,10 @@ import { type WatchBlocksReturnType, getAbiItem, Hex, - InsufficientFundsError + InsufficientFundsError, + NonceTooLowError } from "viem" -import type { Executor, ReplaceTransactionResult } from "./executor" +import type { Executor } from "./executor" import type { AltoConfig } from "../createConfig" import { SenderManager } from "./senderManager" @@ -259,17 +261,30 @@ export class ExecutorManager { return undefined } - // Resubmit if chosen executor has insufficient funds. + // Unhandled error during simulation + if (bundleResult.status === "unhandled_simulation_failure") { + const { reason, userOps } = bundleResult + const rejectedUserOps = userOps.map((op) => ({ + userOperation: op, + reason + })) + this.dropUserOperations(rejectedUserOps, entryPoint) + this.metrics.bundlesSubmitted.labels({ status: "failed" }).inc() + return undefined + } + + // Resubmit if executor has insufficient funds. if ( bundleResult.status === "bundle_submission_failure" && - bundleResult.reason === InsufficientFundsError.name + bundleResult.reason instanceof InsufficientFundsError ) { - const { userOps: userOperations, reason } = bundleResult - this.resubmitUserOperations(userOperations, entryPoint, reason) + const { userOps, reason } = bundleResult + this.resubmitUserOperations(userOps, entryPoint, reason.name) this.metrics.bundlesSubmitted.labels({ status: "resubmit" }).inc() return undefined } + // All other bundle submission errors are unhandled. if (bundleResult.status === "bundle_submission_failure") { const { userOps } = bundleResult const droppedUserOperations = userOps.map((op) => ({ @@ -778,17 +793,69 @@ export class ExecutorManager { txInfo: TransactionInfo, reason: string ): Promise { - let replaceResult: ReplaceTransactionResult | undefined = undefined + const { logger, gasPriceManager } = this + + let gasPriceParameters: GasPriceParameters + try { + gasPriceParameters = await gasPriceManager.tryGetNetworkGasPrice() + } catch (err) { + logger.error({ error: err }, "Failed to get network gas price") + const { transactionHash, bundle } = txInfo + + this.logger.warn( + { oldTxHash: transactionHash, reason }, + "failed to replace transaction" + ) + const droppedUserOperations = bundle.userOperations.map( + (userOperation) => ({ + userOperation, + reason: "Failed to replace transaction" + }) + ) + this.dropUserOperations(droppedUserOperations, bundle.entryPoint) + this.senderManager.markWalletProcessed(txInfo.executor) + return + } + + const { bundle, executor, transactionRequest } = txInfo + + let bundleResult: BundleResult | undefined = undefined try { - replaceResult = await this.executor.replaceTransaction(txInfo) + bundleResult = await this.executor.bundle({ + wallet: executor, + bundle, + nonce: transactionRequest.nonce, + gasPriceParameters: { + maxFeePerGas: scaleBigIntByPercent( + gasPriceParameters.maxFeePerGas, + 115n + ), + maxPriorityFeePerGas: scaleBigIntByPercent( + gasPriceParameters.maxPriorityFeePerGas, + 115n + ) + }, + gasLimitSuggestion: transactionRequest.gas + }) } finally { + const replaceStatus = + bundleResult && bundleResult.status === "bundle_success" + ? "succeeded" + : "failed" + this.metrics.replacedTransactions - .labels({ reason, status: replaceResult?.status || "failed" }) + .labels({ reason, status: replaceStatus }) .inc() } - if (replaceResult.status === "failed") { + // Mark wallet processed if failed to bundle. + if (bundleResult.status !== "bundle_success") { + // TODO: only mark processed if not potentially included. + this.senderManager.markWalletProcessed(txInfo.executor) + } + + if (bundleResult.status === "unhandled_simulation_failure") { const { transactionHash, bundle } = txInfo this.logger.warn( @@ -803,11 +870,39 @@ export class ExecutorManager { }) ) this.dropUserOperations(droppedUserOperations, bundle.entryPoint) - this.senderManager.markWalletProcessed(txInfo.executor) return } - if (replaceResult.status === "potentially_already_included") { + if (bundleResult.status === "all_ops_failed_simulation") { + const { rejectedUserOps } = bundleResult + + const potentiallyIncluded = rejectedUserOps.every( + (op) => + op.reason === "AA25 invalid account nonce" || + op.reason === "AA10 sender already constructed" + ) + + if (!potentiallyIncluded) { + this.logger.warn( + { oldTxHash: txInfo.transactionHash, reason }, + "failed to replace transaction" + ) + + const droppedUserOperations = bundle.userOperations.map( + (userOperation) => ({ + userOperation, + reason: "Failed to replace transaction" + }) + ) + + this.dropUserOperations( + droppedUserOperations, + bundle.entryPoint + ) + + return + } + this.logger.info( { oldTxHash: txInfo.transactionHash, reason }, "transaction potentially already included" @@ -831,37 +926,101 @@ export class ExecutorManager { return } - const newTxInfo = replaceResult.transactionInfo + if (bundleResult.status === "bundle_submission_failure") { + if (bundleResult.reason instanceof NonceTooLowError) { + this.logger.info( + { oldTxHash: txInfo.transactionHash, reason }, + "transaction potentially already included" + ) + txInfo.timesPotentiallyIncluded += 1 + + if (txInfo.timesPotentiallyIncluded >= 3) { + txInfo.bundle.userOperations.map((userOperation) => { + this.mempool.removeSubmitted( + this.getOpHash( + userOperation, + txInfo.bundle.entryPoint + ) + ) + }) + const txSender = txInfo.executor + this.senderManager.markWalletProcessed(txSender) + this.logger.warn( + { oldTxHash: txInfo.transactionHash, reason }, + "transaction potentially already included too many times, removing" + ) + } + + return + } + + this.logger.warn( + { oldTxHash: txInfo.transactionHash, reason }, + "failed to replace transaction" + ) + + const droppedUserOperations = bundle.userOperations.map( + (userOperation) => ({ + userOperation, + reason: "Failed to replace transaction" + }) + ) + + this.dropUserOperations(droppedUserOperations, bundle.entryPoint) + + return + } + + const { + rejectedUserOperations, + userOpsBundled, + transactionRequest: newTransactionRequest, + transactionHash + } = bundleResult + + const newTxInfo: TransactionInfo = { + ...txInfo, + transactionRequest: newTransactionRequest, + transactionHash, + previousTransactionHashes: [ + txInfo.transactionHash, + ...txInfo.previousTransactionHashes + ], + lastReplaced: Date.now(), + bundle: { + ...txInfo.bundle, + userOperations: userOpsBundled + } + } // TODO: FIX THIS USING BUDNLE_RESULT SUCCESS opsBundles + opsRejected - //const missingOps = txInfo.bundle.userOperations.filter( - // (info) => - // !newTxInfo.userOperationInfos - // .map((ni) => ni.userOperationHash) - // .includes(info.userOperationHash) - //) - - //const matchingOps = txInfo.userOperationInfos.filter((info) => - // newTxInfo.userOperationInfos - // .map((ni) => ni.userOperationHash) - // .includes(info.userOperationHash) - //) - - //matchingOps.map((opInfo) => { - // this.mempool.replaceSubmitted(opInfo, newTxInfo) - //}) - - //missingOps.map((opInfo) => { - // this.mempool.removeSubmitted(opInfo.userOperationHash) - // this.logger.warn( - // { - // oldTxHash: txInfo.transactionHash, - // newTxHash: newTxInfo.transactionHash, - // reason - // }, - // "missing op in new tx" - // ) - //}) + userOpsBundled.map((userOperation) => { + const userOperationInfo = { + userOperation, + userOperationHash: this.getOpHash( + userOperation, + txInfo.bundle.entryPoint + ), + entryPoint: txInfo.bundle.entryPoint + } + this.mempool.replaceSubmitted(userOperationInfo, newTxInfo) + }) + + rejectedUserOperations.map((opInfo) => { + const userOpHash = this.getOpHash( + opInfo.userOperation, + txInfo.bundle.entryPoint + ) + this.mempool.removeSubmitted(userOpHash) + this.logger.warn( + { + oldTxHash: txInfo.transactionHash, + newTxHash: newTxInfo.transactionHash, + reason + }, + "Rejected during replacement" + ) + }) this.logger.info( { diff --git a/src/types/mempool.ts b/src/types/mempool.ts index 225e6256..5e704738 100644 --- a/src/types/mempool.ts +++ b/src/types/mempool.ts @@ -1,4 +1,4 @@ -import type { Address } from "viem" +import type { Address, BaseError } from "viem" import type { Account } from "viem/accounts" import type { HexData32, UserOperation } from "." @@ -83,7 +83,7 @@ export type BundleResult = | { // Encountered error whilst trying to bundle user operations. status: "bundle_submission_failure" - reason: string + reason: BaseError | "INTERNAL FAILURE" userOps: UserOperation[] } From d6f74a0622c32da9d8012f21e89143746988442d Mon Sep 17 00:00:00 2001 From: mouseless <97399882+mouseless-eth@users.noreply.github.com> Date: Mon, 27 Jan 2025 10:23:23 +0000 Subject: [PATCH 12/34] add UserOperationWithHash type --- src/executor/executor.ts | 83 ++------------------ src/executor/executorManager.ts | 100 ++++++++---------------- src/executor/filterOpsAndEStimateGas.ts | 8 +- src/mempool/mempool.ts | 5 +- src/types/mempool.ts | 24 +++--- 5 files changed, 59 insertions(+), 161 deletions(-) diff --git a/src/executor/executor.ts b/src/executor/executor.ts index 8e3c57c9..edb02bcb 100644 --- a/src/executor/executor.ts +++ b/src/executor/executor.ts @@ -15,7 +15,6 @@ import { } from "@alto/types" import type { Logger, Metrics } from "@alto/utils" import { - getUserOperationHash, maxBigInt, parseViemError, scaleBigIntByPercent, @@ -24,8 +23,6 @@ import { import * as sentry from "@sentry/node" import { Mutex } from "async-mutex" import { - FeeCapTooLowError, - InsufficientFundsError, IntrinsicGasTooLowError, NonceTooLowError, TransactionExecutionError, @@ -33,10 +30,9 @@ import { getContract, type Account, type Hex, - BaseError, NonceTooHighError } from "viem" -import { isTransactionUnderpricedError, getAuthorizationList } from "./utils" +import { getAuthorizationList } from "./utils" import type { SendTransactionErrorType } from "viem" import type { AltoConfig } from "../createConfig" import type { SendTransactionOptions } from "./types" @@ -114,16 +110,6 @@ export class Executor { throw new Error("Method not implemented.") } - getOpHashes(userOperations: UserOperation[]): HexData32[] { - return userOperations.map((userOperation) => { - return getUserOperationHash( - userOperation, - this.config.entrypoints[0], - this.config.publicClient.chain.id - ) - }) - } - async sendHandleOpsTransaction({ txParam, opts @@ -174,7 +160,6 @@ export class Executor { this.config.executorGasMultiplier ) - let isTransactionUnderPriced = false let attempts = 0 let transactionHash: Hex | undefined const maxAttempts = 3 @@ -206,24 +191,6 @@ export class Executor { break } catch (e: unknown) { - isTransactionUnderPriced = false - - if (e instanceof BaseError) { - if (isTransactionUnderpricedError(e)) { - this.logger.warn("Transaction underpriced, retrying") - - request.maxFeePerGas = scaleBigIntByPercent( - request.maxFeePerGas, - 150n - ) - request.maxPriorityFeePerGas = scaleBigIntByPercent( - request.maxPriorityFeePerGas, - 150n - ) - isTransactionUnderPriced = true - } - } - const error = e as SendTransactionErrorType if (error instanceof TransactionExecutionError) { @@ -264,13 +231,6 @@ export class Executor { } } - if (isTransactionUnderPriced) { - await this.handleTransactionUnderPriced({ - nonce: request.nonce, - executor: request.account - }) - } - // needed for TS if (!transactionHash) { throw new Error("Transaction hash not assigned") @@ -279,39 +239,6 @@ export class Executor { return transactionHash as Hex } - // Occurs when tx was sent with conflicting nonce, we want to resubmit all conflicting ops - async handleTransactionUnderPriced({ - nonce, - executor - }: { nonce: number; executor: Account }) { - const submitted = this.mempool.dumpSubmittedOps() - - const conflictingOps = submitted - .filter((submitted) => { - const txInfo = submitted.transactionInfo - const txSender = txInfo.executor.address - - return ( - txSender === executor.address && - txInfo.transactionRequest.nonce === nonce - ) - }) - .map(({ userOperation }) => userOperation) - - conflictingOps.map((op) => { - this.logger.info( - `Resubmitting ${op.userOperationHash} due to transaction underpriced` - ) - this.mempool.removeSubmitted(op.userOperationHash) - this.mempool.add(op.userOperation, op.entryPoint) - }) - - if (conflictingOps.length > 0) { - // TODO: what to do here? - // this.markWalletProcessed(executor) - } - } - async bundle({ wallet, bundle, @@ -337,7 +264,7 @@ export class Executor { }) let childLogger = this.logger.child({ - userOperations: this.getOpHashes(userOperations), + userOperations: userOperations.map((op) => op.hash), entryPoint }) @@ -377,7 +304,7 @@ export class Executor { // Update child logger with userOperations being sent for bundling. childLogger = this.logger.child({ - userOperations: this.getOpHashes(opsToBundle), + userOperations: opsToBundle.map((op) => op.hash), entryPoint }) @@ -435,7 +362,7 @@ export class Executor { }) this.eventManager.emitSubmitted({ - userOpHashes: this.getOpHashes(opsToBundle), + userOpHashes: opsToBundle.map((op) => op.hash), transactionHash }) } catch (err: unknown) { @@ -477,7 +404,7 @@ export class Executor { { transactionRequest: bundleResult.transactionRequest, txHash: transactionHash, - opHashes: this.getOpHashes(opsToBundle) + opHashes: opsToBundle.map((op) => op.hash) }, "submitted bundle transaction" ) diff --git a/src/executor/executorManager.ts b/src/executor/executorManager.ts index 2d3e0695..40ae8092 100644 --- a/src/executor/executorManager.ts +++ b/src/executor/executorManager.ts @@ -8,19 +8,18 @@ import { type BundlingMode, EntryPointV06Abi, type HexData32, - type UserOperation, type SubmittedUserOperation, type TransactionInfo, RejectedUserOperation, UserOperationBundle, GasPriceParameters, - BundleResult + BundleResult, + UserOperationWithHash } from "@alto/types" import type { BundlingStatus, Logger, Metrics } from "@alto/utils" import { getAAError, getBundleStatus, - getUserOperationHash, parseUserOperationReceipt, scaleBigIntByPercent } from "@alto/utils" @@ -205,14 +204,6 @@ export class ExecutorManager { return txHash } - getOpHash(userOperation: UserOperation, entryPoint: Address): HexData32 { - return getUserOperationHash( - userOperation, - entryPoint, - this.config.publicClient.chain.id - ) - } - async sendBundleToExecutor( bundle: UserOperationBundle ): Promise { @@ -257,7 +248,7 @@ export class ExecutorManager { // All ops failed simulation, drop them and return. if (bundleResult.status === "all_ops_failed_simulation") { const { rejectedUserOps } = bundleResult - this.dropUserOperations(rejectedUserOps, entryPoint) + this.dropUserOperations(rejectedUserOps) return undefined } @@ -268,7 +259,7 @@ export class ExecutorManager { userOperation: op, reason })) - this.dropUserOperations(rejectedUserOps, entryPoint) + this.dropUserOperations(rejectedUserOps) this.metrics.bundlesSubmitted.labels({ status: "failed" }).inc() return undefined } @@ -291,7 +282,7 @@ export class ExecutorManager { userOperation: op, reason: "INTERNAL FAILURE" })) - this.dropUserOperations(droppedUserOperations, entryPoint) + this.dropUserOperations(droppedUserOperations) this.metrics.bundlesSubmitted.labels({ status: "failed" }).inc() } @@ -318,7 +309,7 @@ export class ExecutorManager { } this.markUserOperationsAsSubmitted(userOpsBundled, transactionInfo) - this.dropUserOperations(rejectedUserOperations, entryPoint) + this.dropUserOperations(rejectedUserOperations) this.metrics.bundlesSubmitted.labels({ status: "success" }).inc() return transactionHash @@ -410,17 +401,14 @@ export class ExecutorManager { const { userOperationDetails } = bundlingStatus userOperations.map((userOperation) => { - const userOperationHash = this.getOpHash( - userOperation, - entryPoint - ) - const opDetails = userOperationDetails[userOperationHash] + const userOpHash = userOperation.hash + const opDetails = userOperationDetails[userOpHash] // TODO: keep this metric //this.metrics.userOperationInclusionDuration.observe( // (Date.now() - firstSubmitted) / 1000 //) - this.mempool.removeSubmitted(userOperationHash) + this.mempool.removeSubmitted(userOpHash) this.reputationManager.updateUserOperationIncludedStatus( userOperation, entryPoint, @@ -428,25 +416,25 @@ export class ExecutorManager { ) if (opDetails.status === "succesful") { this.eventManager.emitIncludedOnChain( - userOperationHash, + userOpHash, transactionHash, blockNumber as bigint ) } else { this.eventManager.emitExecutionRevertedOnChain( - userOperationHash, + userOpHash, transactionHash, opDetails.revertReason || "0x", blockNumber as bigint ) } - this.monitor.setUserOperationStatus(userOperationHash, { + this.monitor.setUserOperationStatus(userOpHash, { status: "included", transactionHash }) this.logger.info( { - userOperationHash, + opHash: userOpHash, transactionHash }, "user op included" @@ -470,10 +458,7 @@ export class ExecutorManager { await Promise.all( userOperations.map((userOperation) => { this.checkFrontrun({ - userOperationHash: this.getOpHash( - userOperation, - entryPoint - ), + userOperationHash: userOperation.hash, transactionHash, blockNumber }) @@ -481,9 +466,7 @@ export class ExecutorManager { ) userOperations.map((userOperation) => { - this.mempool.removeSubmitted( - this.getOpHash(userOperation, entryPoint) - ) + this.mempool.removeSubmitted(userOperation.hash) }) this.senderManager.markWalletProcessed(transactionInfo.executor) } @@ -812,7 +795,7 @@ export class ExecutorManager { reason: "Failed to replace transaction" }) ) - this.dropUserOperations(droppedUserOperations, bundle.entryPoint) + this.dropUserOperations(droppedUserOperations) this.senderManager.markWalletProcessed(txInfo.executor) return } @@ -869,7 +852,7 @@ export class ExecutorManager { reason: "Failed to replace transaction" }) ) - this.dropUserOperations(droppedUserOperations, bundle.entryPoint) + this.dropUserOperations(droppedUserOperations) return } @@ -895,10 +878,7 @@ export class ExecutorManager { }) ) - this.dropUserOperations( - droppedUserOperations, - bundle.entryPoint - ) + this.dropUserOperations(droppedUserOperations) return } @@ -911,9 +891,7 @@ export class ExecutorManager { if (txInfo.timesPotentiallyIncluded >= 3) { txInfo.bundle.userOperations.map((userOperation) => { - this.mempool.removeSubmitted( - this.getOpHash(userOperation, txInfo.bundle.entryPoint) - ) + this.mempool.removeSubmitted(userOperation.hash) }) const txSender = txInfo.executor this.senderManager.markWalletProcessed(txSender) @@ -936,12 +914,7 @@ export class ExecutorManager { if (txInfo.timesPotentiallyIncluded >= 3) { txInfo.bundle.userOperations.map((userOperation) => { - this.mempool.removeSubmitted( - this.getOpHash( - userOperation, - txInfo.bundle.entryPoint - ) - ) + this.mempool.removeSubmitted(userOperation.hash) }) const txSender = txInfo.executor this.senderManager.markWalletProcessed(txSender) @@ -966,7 +939,7 @@ export class ExecutorManager { }) ) - this.dropUserOperations(droppedUserOperations, bundle.entryPoint) + this.dropUserOperations(droppedUserOperations) return } @@ -997,20 +970,14 @@ export class ExecutorManager { userOpsBundled.map((userOperation) => { const userOperationInfo = { userOperation, - userOperationHash: this.getOpHash( - userOperation, - txInfo.bundle.entryPoint - ), + userOperationHash: userOperation.hash, entryPoint: txInfo.bundle.entryPoint } this.mempool.replaceSubmitted(userOperationInfo, newTxInfo) }) rejectedUserOperations.map((opInfo) => { - const userOpHash = this.getOpHash( - opInfo.userOperation, - txInfo.bundle.entryPoint - ) + const userOpHash = opInfo.userOperation.hash this.mempool.removeSubmitted(userOpHash) this.logger.warn( { @@ -1035,11 +1002,11 @@ export class ExecutorManager { } markUserOperationsAsSubmitted( - userOperations: UserOperation[], + userOperations: UserOperationWithHash[], transactionInfo: TransactionInfo ) { userOperations.map((op) => { - const opHash = this.getOpHash(op, transactionInfo.bundle.entryPoint) + const opHash = op.hash this.mempool.markSubmitted(opHash, transactionInfo) this.startWatchingBlocks(this.handleBlock.bind(this)) this.metrics.userOperationsSubmitted @@ -1049,30 +1016,29 @@ export class ExecutorManager { } resubmitUserOperations( - userOperations: UserOperation[], + userOperations: UserOperationWithHash[], entryPoint: Address, reason: string ) { userOperations.map((op) => { + const userOpHash = op.hash this.logger.info( { - userOpHash: this.getOpHash(op, entryPoint), + userOpHash, reason }, "resubmitting user operation" ) - this.mempool.removeProcessing(this.getOpHash(op, entryPoint)) + this.mempool.removeProcessing(userOpHash) this.mempool.add(op, entryPoint) this.metrics.userOperationsResubmitted.inc() }) } - dropUserOperations( - rejectedUserOperations: RejectedUserOperation[], - entryPoint: Address - ) { - rejectedUserOperations.map(({ userOperation, reason }) => { - const userOpHash = this.getOpHash(userOperation, entryPoint) + dropUserOperations(rejectedUserOperations: RejectedUserOperation[]) { + rejectedUserOperations.map((rejectedUserOperation) => { + const { userOperation, reason } = rejectedUserOperation + const userOpHash = userOperation.hash this.mempool.removeProcessing(userOpHash) this.eventManager.emitDropped( userOpHash, diff --git a/src/executor/filterOpsAndEStimateGas.ts b/src/executor/filterOpsAndEStimateGas.ts index 1398dcb8..92a4958d 100644 --- a/src/executor/filterOpsAndEStimateGas.ts +++ b/src/executor/filterOpsAndEStimateGas.ts @@ -3,8 +3,8 @@ import { EntryPointV06Abi, EntryPointV07Abi, FailedOpWithRevert, - UserOperation, UserOperationV07, + UserOperationWithHash, failedOpErrorSchema, failedOpWithRevertErrorSchema } from "@alto/types" @@ -32,14 +32,14 @@ import { getAuthorizationList } from "./utils" import * as sentry from "@sentry/node" type FailedOpWithReason = { - userOperation: UserOperation + userOperation: UserOperationWithHash reason: string } export type FilterOpsAndEstimateGasResult = | { status: "success" - opsToBundle: UserOperation[] + opsToBundle: UserOperationWithHash[] failedOps: FailedOpWithReason[] gasLimit: bigint } @@ -74,7 +74,7 @@ export async function filterOpsAndEstimateGas({ > isUserOpV06: boolean wallet: Account - ops: UserOperation[] + ops: UserOperationWithHash[] nonce: number maxFeePerGas: bigint maxPriorityFeePerGas: bigint diff --git a/src/mempool/mempool.ts b/src/mempool/mempool.ts index 727da852..37c8f677 100644 --- a/src/mempool/mempool.ts +++ b/src/mempool/mempool.ts @@ -789,7 +789,10 @@ export class MemoryMempool { this.store.addProcessing(opInfo) // Add op to current bundle - currentBundle.userOperations.push(op) + const chainId = this.config.publicClient.chain.id + const opHash = getUserOperationHash(op, entryPoint, chainId) + const opWithHash = { ...op, hash: opHash } + currentBundle.userOperations.push(opWithHash) } if (currentBundle.userOperations.length > 0) { diff --git a/src/types/mempool.ts b/src/types/mempool.ts index 5e704738..bacd23d5 100644 --- a/src/types/mempool.ts +++ b/src/types/mempool.ts @@ -1,4 +1,4 @@ -import type { Address, BaseError } from "viem" +import type { Address, BaseError, Hex } from "viem" import type { Account } from "viem/accounts" import type { HexData32, UserOperation } from "." @@ -28,7 +28,11 @@ export type TransactionInfo = { export type UserOperationBundle = { entryPoint: Address version: "0.6" | "0.7" - userOperations: UserOperation[] + userOperations: UserOperationWithHash[] +} + +export type UserOperationWithHash = UserOperation & { + hash: Hex } export type UserOperationInfo = { @@ -51,15 +55,15 @@ export type SubmittedUserOperation = { } export type RejectedUserOperation = { - userOperation: UserOperation + userOperation: UserOperationWithHash reason: string } export type BundleResult = | { - // Successfully bundled user operations. + // Successfully sent bundle. status: "bundle_success" - userOpsBundled: UserOperation[] + userOpsBundled: UserOperationWithHash[] rejectedUserOperations: RejectedUserOperation[] transactionHash: HexData32 transactionRequest: { @@ -73,18 +77,16 @@ export type BundleResult = // Encountered unhandled error during bundle simulation. status: "unhandled_simulation_failure" reason: string - userOps: UserOperation[] + userOps: UserOperationWithHash[] } | { - // All user operations failed simulation. + // All user operations failed during simulation. status: "all_ops_failed_simulation" rejectedUserOps: RejectedUserOperation[] } | { - // Encountered error whilst trying to bundle user operations. + // Encountered error whilst trying to send bundle. status: "bundle_submission_failure" reason: BaseError | "INTERNAL FAILURE" - userOps: UserOperation[] + userOps: UserOperationWithHash[] } - -export type BundleRequest = {} From cc2254b95cc655075313a5ec3e0946d5386d9c97 Mon Sep 17 00:00:00 2001 From: mouseless <97399882+mouseless-eth@users.noreply.github.com> Date: Mon, 27 Jan 2025 10:35:52 +0000 Subject: [PATCH 13/34] fix pimlico_sendUserOperationNow endpoint --- src/executor/executorManager.ts | 16 ++--- src/rpc/rpcHandler.ts | 120 +++++++++++++++----------------- src/types/mempool.ts | 1 + src/types/schemas.ts | 5 -- 4 files changed, 62 insertions(+), 80 deletions(-) diff --git a/src/executor/executorManager.ts b/src/executor/executorManager.ts index 40ae8092..d3450609 100644 --- a/src/executor/executorManager.ts +++ b/src/executor/executorManager.ts @@ -305,6 +305,7 @@ export class ExecutorManager { }, previousTransactionHashes: [], lastReplaced: Date.now(), + firstSubmitted: Date.now(), timesPotentiallyIncluded: 0 } @@ -399,15 +400,15 @@ export class ExecutorManager { .labels({ status: bundlingStatus.status }) .inc(userOperations.length) + const firstSubmitted = transactionInfo.firstSubmitted const { userOperationDetails } = bundlingStatus userOperations.map((userOperation) => { const userOpHash = userOperation.hash const opDetails = userOperationDetails[userOpHash] - // TODO: keep this metric - //this.metrics.userOperationInclusionDuration.observe( - // (Date.now() - firstSubmitted) / 1000 - //) + this.metrics.userOperationInclusionDuration.observe( + (Date.now() - firstSubmitted) / 1000 + ) this.mempool.removeSubmitted(userOpHash) this.reputationManager.updateUserOperationIncludedStatus( userOperation, @@ -728,10 +729,6 @@ export class ExecutorManager { // for all still not included check if needs to be replaced (based on gas price) const gasPriceParameters = await this.gasPriceManager.tryGetNetworkGasPrice() - this.logger.trace( - { gasPriceParameters }, - "fetched gas price parameters" - ) const transactionInfos = getTransactionsFromUserOperationEntries( this.mempool.dumpSubmittedOps() @@ -966,7 +963,6 @@ export class ExecutorManager { } } - // TODO: FIX THIS USING BUDNLE_RESULT SUCCESS opsBundles + opsRejected userOpsBundled.map((userOperation) => { const userOperationInfo = { userOperation, @@ -985,7 +981,7 @@ export class ExecutorManager { newTxHash: newTxInfo.transactionHash, reason }, - "Rejected during replacement" + "rejected during replacement" ) }) diff --git a/src/rpc/rpcHandler.ts b/src/rpc/rpcHandler.ts index 0c2b12d6..7fc3b127 100644 --- a/src/rpc/rpcHandler.ts +++ b/src/rpc/rpcHandler.ts @@ -9,8 +9,6 @@ import type { ApiVersion, PackedUserOperation, StateOverrides, - TransactionInfo, - UserOperationInfo, UserOperationV06, UserOperationV07 } from "@alto/types" @@ -839,69 +837,61 @@ export class RpcHandler implements IRpcEndpoint { userOperation: UserOperation, entryPoint: Address ) { - //if (!this.config.enableInstantBundlingEndpoint) { - // throw new RpcError( - // "pimlico_sendUserOperationNow endpoint is not enabled", - // ValidationErrors.InvalidFields - // ) - //} - //this.ensureEntryPointIsSupported(entryPoint) - //const opHash = getUserOperationHash( - // userOperation, - // entryPoint, - // this.config.publicClient.chain.id - //) - //await this.preMempoolChecks( - // opHash, - // userOperation, - // apiVersion, - // entryPoint - //) - //const result = ( - // await this.executor.bundle(entryPoint, [userOperation]) - //)[0] - //if (result.status === "failure") { - // const { userOpHash, reason } = result.error - // this.monitor.setUserOperationStatus(userOpHash, { - // status: "rejected", - // transactionHash: null - // }) - // this.logger.warn( - // { - // userOperation: JSON.stringify( - // result.error.userOperation, - // (_k, v) => (typeof v === "bigint" ? v.toString() : v) - // ), - // userOpHash, - // reason - // }, - // "user operation rejected" - // ) - // this.metrics.userOperationsSubmitted - // .labels({ status: "failed" }) - // .inc() - // const { error } = result - // throw new RpcError( - // `userOperation reverted during simulation with reason: ${error.reason}` - // ) - //} - //const res = result as unknown as { - // status: "success" - // value: { - // userOperation: UserOperationInfo - // transactionInfo: TransactionInfo - // } - //} - //const txSender = res.value.transactionInfo.executor.address - //this.executor.markWalletProcessed(txSender) - //// wait for receipt - //const receipt = - // await this.config.publicClient.waitForTransactionReceipt({ - // hash: res.value.transactionInfo.transactionHash, - // pollingInterval: 100 - // }) - //const userOperationReceipt = parseUserOperationReceipt(opHash, receipt) - //return userOperationReceipt + if (!this.config.enableInstantBundlingEndpoint) { + throw new RpcError( + "pimlico_sendUserOperationNow endpoint is not enabled", + ValidationErrors.InvalidFields + ) + } + + this.ensureEntryPointIsSupported(entryPoint) + const opHash = getUserOperationHash( + userOperation, + entryPoint, + this.config.publicClient.chain.id + ) + + await this.preMempoolChecks( + opHash, + userOperation, + apiVersion, + entryPoint + ) + + // Prepare bundle + const userOperationWithHash = { + ...userOperation, + hash: getUserOperationHash( + userOperation, + entryPoint, + this.config.publicClient.chain.id + ) + } + const bundle = { + entryPoint, + userOperations: [userOperationWithHash], + version: isVersion06(userOperation) + ? ("0.6" as const) + : ("0.7" as const) + } + const result = await this.executorManager.sendBundleToExecutor(bundle) + + if (!result) { + throw new RpcError( + "unhandled error during bundle submission", + ValidationErrors.InvalidFields + ) + } + + // Wait for receipt. + const receipt = + await this.config.publicClient.waitForTransactionReceipt({ + hash: result, + pollingInterval: 100 + }) + + const userOperationReceipt = parseUserOperationReceipt(opHash, receipt) + return userOperationReceipt } async getNonceValue(userOperation: UserOperation, entryPoint: Address) { diff --git a/src/types/mempool.ts b/src/types/mempool.ts index bacd23d5..432fb97c 100644 --- a/src/types/mempool.ts +++ b/src/types/mempool.ts @@ -22,6 +22,7 @@ export type TransactionInfo = { bundle: UserOperationBundle executor: Account lastReplaced: number + firstSubmitted: number timesPotentiallyIncluded: number } diff --git a/src/types/schemas.ts b/src/types/schemas.ts index 65e94f83..75f88b4a 100644 --- a/src/types/schemas.ts +++ b/src/types/schemas.ts @@ -200,11 +200,6 @@ export type UserOperationRequest = { entryPoint: Address } -export type UserOperationWithHash = { - userOperation: UserOperation - userOperationHash: HexData32 -} - const jsonRpcSchema = z .object({ jsonrpc: z.literal("2.0"), From 3091c2f1b0320b9743735242d0e3161060d7aef6 Mon Sep 17 00:00:00 2001 From: mouseless <97399882+mouseless-eth@users.noreply.github.com> Date: Mon, 27 Jan 2025 11:07:56 +0000 Subject: [PATCH 14/34] add failedToReplaceTransaction helper --- src/executor/executorManager.ts | 132 +++++++++++++++----------------- 1 file changed, 60 insertions(+), 72 deletions(-) diff --git a/src/executor/executorManager.ts b/src/executor/executorManager.ts index d3450609..071dacce 100644 --- a/src/executor/executorManager.ts +++ b/src/executor/executorManager.ts @@ -38,6 +38,7 @@ import { import type { Executor } from "./executor" import type { AltoConfig } from "../createConfig" import { SenderManager } from "./senderManager" +import { BaseError } from "abitype" function getTransactionsFromUserOperationEntries( entries: SubmittedUserOperation[] @@ -518,7 +519,7 @@ export class ExecutorManager { .inc(1) } else { this.monitor.setUserOperationStatus(userOperationHash, { - status: "rejected", + status: "failed", transactionHash }) this.eventManager.emitFailedOnChain( @@ -533,7 +534,6 @@ export class ExecutorManager { }, "user op failed onchain" ) - this.metrics.userOperationsOnChain .labels({ status: "reverted" }) .inc(1) @@ -773,26 +773,22 @@ export class ExecutorManager { txInfo: TransactionInfo, reason: string ): Promise { - const { logger, gasPriceManager } = this - let gasPriceParameters: GasPriceParameters try { - gasPriceParameters = await gasPriceManager.tryGetNetworkGasPrice() + gasPriceParameters = + await this.gasPriceManager.tryGetNetworkGasPrice() } catch (err) { - logger.error({ error: err }, "Failed to get network gas price") const { transactionHash, bundle } = txInfo - - this.logger.warn( - { oldTxHash: transactionHash, reason }, - "failed to replace transaction" - ) - const droppedUserOperations = bundle.userOperations.map( - (userOperation) => ({ - userOperation, - reason: "Failed to replace transaction" - }) - ) - this.dropUserOperations(droppedUserOperations) + this.failedToReplaceTransaction({ + oldTxHash: transactionHash, + reason: "Failed to get network gas price", + rejectedUserOperations: bundle.userOperations.map( + (userOperation) => ({ + userOperation, + reason: "Failed to replace transaction" + }) + ) + }) this.senderManager.markWalletProcessed(txInfo.executor) return } @@ -836,20 +832,18 @@ export class ExecutorManager { } if (bundleResult.status === "unhandled_simulation_failure") { - const { transactionHash, bundle } = txInfo - - this.logger.warn( - { oldTxHash: transactionHash, reason }, - "failed to replace transaction" - ) + const { transactionHash } = txInfo - const droppedUserOperations = bundle.userOperations.map( - (userOperation) => ({ - userOperation, - reason: "Failed to replace transaction" - }) - ) - this.dropUserOperations(droppedUserOperations) + this.failedToReplaceTransaction({ + oldTxHash: transactionHash, + reason: bundleResult.reason, + rejectedUserOperations: bundleResult.userOps.map( + (userOperation) => ({ + userOperation, + reason: "Failed to replace transaction" + }) + ) + }) return } @@ -863,20 +857,11 @@ export class ExecutorManager { ) if (!potentiallyIncluded) { - this.logger.warn( - { oldTxHash: txInfo.transactionHash, reason }, - "failed to replace transaction" - ) - - const droppedUserOperations = bundle.userOperations.map( - (userOperation) => ({ - userOperation, - reason: "Failed to replace transaction" - }) - ) - - this.dropUserOperations(droppedUserOperations) - + this.failedToReplaceTransaction({ + oldTxHash: txInfo.transactionHash, + reason: "all ops failed simulation", + rejectedUserOperations: bundleResult.rejectedUserOps + }) return } @@ -924,19 +909,19 @@ export class ExecutorManager { return } - this.logger.warn( - { oldTxHash: txInfo.transactionHash, reason }, - "failed to replace transaction" - ) - - const droppedUserOperations = bundle.userOperations.map( - (userOperation) => ({ - userOperation, - reason: "Failed to replace transaction" - }) - ) - - this.dropUserOperations(droppedUserOperations) + this.failedToReplaceTransaction({ + oldTxHash: txInfo.transactionHash, + reason: + bundleResult.reason instanceof BaseError + ? bundleResult.reason.name + : "INTERNAL FAILURE", + rejectedUserOperations: bundle.userOperations.map( + (userOperation) => ({ + userOperation, + reason: "Failed to replace transaction" + }) + ) + }) return } @@ -945,13 +930,13 @@ export class ExecutorManager { rejectedUserOperations, userOpsBundled, transactionRequest: newTransactionRequest, - transactionHash + transactionHash: newTransactionHash } = bundleResult const newTxInfo: TransactionInfo = { ...txInfo, transactionRequest: newTransactionRequest, - transactionHash, + transactionHash: newTransactionHash, previousTransactionHashes: [ txInfo.transactionHash, ...txInfo.previousTransactionHashes @@ -972,18 +957,7 @@ export class ExecutorManager { this.mempool.replaceSubmitted(userOperationInfo, newTxInfo) }) - rejectedUserOperations.map((opInfo) => { - const userOpHash = opInfo.userOperation.hash - this.mempool.removeSubmitted(userOpHash) - this.logger.warn( - { - oldTxHash: txInfo.transactionHash, - newTxHash: newTxInfo.transactionHash, - reason - }, - "rejected during replacement" - ) - }) + this.dropUserOperations(rejectedUserOperations) this.logger.info( { @@ -1031,11 +1005,25 @@ export class ExecutorManager { }) } + failedToReplaceTransaction({ + oldTxHash, + reason, + rejectedUserOperations + }: { + oldTxHash: Hex + reason: string + rejectedUserOperations: RejectedUserOperation[] + }) { + this.logger.warn({ oldTxHash, reason }, "failed to replace transaction") + this.dropUserOperations(rejectedUserOperations) + } + dropUserOperations(rejectedUserOperations: RejectedUserOperation[]) { rejectedUserOperations.map((rejectedUserOperation) => { const { userOperation, reason } = rejectedUserOperation const userOpHash = userOperation.hash this.mempool.removeProcessing(userOpHash) + this.mempool.removeSubmitted(userOpHash) this.eventManager.emitDropped( userOpHash, reason, From 36885a21ab402969e0e519a7ba3737cc10f34785 Mon Sep 17 00:00:00 2001 From: mouseless <97399882+mouseless-eth@users.noreply.github.com> Date: Mon, 27 Jan 2025 11:52:32 +0000 Subject: [PATCH 15/34] cleanup replaceTransaction flow --- src/executor/executorManager.ts | 231 ++++++++++++++------------------ 1 file changed, 103 insertions(+), 128 deletions(-) diff --git a/src/executor/executorManager.ts b/src/executor/executorManager.ts index 071dacce..52fbb579 100644 --- a/src/executor/executorManager.ts +++ b/src/executor/executorManager.ts @@ -13,7 +13,6 @@ import { RejectedUserOperation, UserOperationBundle, GasPriceParameters, - BundleResult, UserOperationWithHash } from "@alto/types" import type { BundlingStatus, Logger, Metrics } from "@alto/utils" @@ -249,7 +248,7 @@ export class ExecutorManager { // All ops failed simulation, drop them and return. if (bundleResult.status === "all_ops_failed_simulation") { const { rejectedUserOps } = bundleResult - this.dropUserOperations(rejectedUserOps) + this.dropUserOps(rejectedUserOps) return undefined } @@ -260,7 +259,7 @@ export class ExecutorManager { userOperation: op, reason })) - this.dropUserOperations(rejectedUserOps) + this.dropUserOps(rejectedUserOps) this.metrics.bundlesSubmitted.labels({ status: "failed" }).inc() return undefined } @@ -283,7 +282,7 @@ export class ExecutorManager { userOperation: op, reason: "INTERNAL FAILURE" })) - this.dropUserOperations(droppedUserOperations) + this.dropUserOps(droppedUserOperations) this.metrics.bundlesSubmitted.labels({ status: "failed" }).inc() } @@ -311,7 +310,7 @@ export class ExecutorManager { } this.markUserOperationsAsSubmitted(userOpsBundled, transactionInfo) - this.dropUserOperations(rejectedUserOperations) + this.dropUserOps(rejectedUserOperations) this.metrics.bundlesSubmitted.labels({ status: "success" }).inc() return transactionHash @@ -778,16 +777,9 @@ export class ExecutorManager { gasPriceParameters = await this.gasPriceManager.tryGetNetworkGasPrice() } catch (err) { - const { transactionHash, bundle } = txInfo this.failedToReplaceTransaction({ - oldTxHash: transactionHash, - reason: "Failed to get network gas price", - rejectedUserOperations: bundle.userOperations.map( - (userOperation) => ({ - userOperation, - reason: "Failed to replace transaction" - }) - ) + txInfo, + reason: "Failed to get network gas price" }) this.senderManager.markWalletProcessed(txInfo.executor) return @@ -795,132 +787,80 @@ export class ExecutorManager { const { bundle, executor, transactionRequest } = txInfo - let bundleResult: BundleResult | undefined = undefined + const bundleResult = await this.executor.bundle({ + wallet: executor, + bundle, + nonce: transactionRequest.nonce, + gasPriceParameters: { + maxFeePerGas: scaleBigIntByPercent( + gasPriceParameters.maxFeePerGas, + 115n + ), + maxPriorityFeePerGas: scaleBigIntByPercent( + gasPriceParameters.maxPriorityFeePerGas, + 115n + ) + }, + gasLimitSuggestion: transactionRequest.gas + }) - try { - bundleResult = await this.executor.bundle({ - wallet: executor, - bundle, - nonce: transactionRequest.nonce, - gasPriceParameters: { - maxFeePerGas: scaleBigIntByPercent( - gasPriceParameters.maxFeePerGas, - 115n - ), - maxPriorityFeePerGas: scaleBigIntByPercent( - gasPriceParameters.maxPriorityFeePerGas, - 115n - ) - }, - gasLimitSuggestion: transactionRequest.gas - }) - } finally { - const replaceStatus = - bundleResult && bundleResult.status === "bundle_success" - ? "succeeded" - : "failed" - - this.metrics.replacedTransactions - .labels({ reason, status: replaceStatus }) - .inc() + const replaceStatus = + bundleResult && bundleResult.status === "bundle_success" + ? "succeeded" + : "failed" + + this.metrics.replacedTransactions + .labels({ reason, status: replaceStatus }) + .inc() + + // Check if the transaction is potentially included. + const nonceTooLow = + bundleResult.status === "bundle_submission_failure" && + bundleResult.reason instanceof NonceTooLowError + const allOpsFailedSimulation = + bundleResult.status === "all_ops_failed_simulation" && + bundleResult.rejectedUserOps.every( + (op) => + op.reason === "AA25 invalid account nonce" || + op.reason === "AA10 sender already constructed" + ) + const potentiallyIncluded = nonceTooLow || allOpsFailedSimulation + + if (potentiallyIncluded) { + this.handlePotentiallyIncluded({ txInfo }) + return } - // Mark wallet processed if failed to bundle. if (bundleResult.status !== "bundle_success") { - // TODO: only mark processed if not potentially included. this.senderManager.markWalletProcessed(txInfo.executor) } if (bundleResult.status === "unhandled_simulation_failure") { - const { transactionHash } = txInfo - this.failedToReplaceTransaction({ - oldTxHash: transactionHash, - reason: bundleResult.reason, - rejectedUserOperations: bundleResult.userOps.map( - (userOperation) => ({ - userOperation, - reason: "Failed to replace transaction" - }) - ) + txInfo, + reason: bundleResult.reason }) return } if (bundleResult.status === "all_ops_failed_simulation") { - const { rejectedUserOps } = bundleResult - - const potentiallyIncluded = rejectedUserOps.every( - (op) => - op.reason === "AA25 invalid account nonce" || - op.reason === "AA10 sender already constructed" - ) - - if (!potentiallyIncluded) { - this.failedToReplaceTransaction({ - oldTxHash: txInfo.transactionHash, - reason: "all ops failed simulation", - rejectedUserOperations: bundleResult.rejectedUserOps - }) - return - } - - this.logger.info( - { oldTxHash: txInfo.transactionHash, reason }, - "transaction potentially already included" - ) - txInfo.timesPotentiallyIncluded += 1 - - if (txInfo.timesPotentiallyIncluded >= 3) { - txInfo.bundle.userOperations.map((userOperation) => { - this.mempool.removeSubmitted(userOperation.hash) - }) - const txSender = txInfo.executor - this.senderManager.markWalletProcessed(txSender) - this.logger.warn( - { oldTxHash: txInfo.transactionHash, reason }, - "transaction potentially already included too many times, removing" - ) - } - + this.failedToReplaceTransaction({ + txInfo, + reason: "all ops failed simulation", + rejectedUserOperations: bundleResult.rejectedUserOps + }) return } if (bundleResult.status === "bundle_submission_failure") { - if (bundleResult.reason instanceof NonceTooLowError) { - this.logger.info( - { oldTxHash: txInfo.transactionHash, reason }, - "transaction potentially already included" - ) - txInfo.timesPotentiallyIncluded += 1 - - if (txInfo.timesPotentiallyIncluded >= 3) { - txInfo.bundle.userOperations.map((userOperation) => { - this.mempool.removeSubmitted(userOperation.hash) - }) - const txSender = txInfo.executor - this.senderManager.markWalletProcessed(txSender) - this.logger.warn( - { oldTxHash: txInfo.transactionHash, reason }, - "transaction potentially already included too many times, removing" - ) - } - - return - } + const reason = + bundleResult.reason instanceof BaseError + ? bundleResult.reason.name + : "INTERNAL FAILURE" this.failedToReplaceTransaction({ - oldTxHash: txInfo.transactionHash, - reason: - bundleResult.reason instanceof BaseError - ? bundleResult.reason.name - : "INTERNAL FAILURE", - rejectedUserOperations: bundle.userOperations.map( - (userOperation) => ({ - userOperation, - reason: "Failed to replace transaction" - }) - ) + txInfo, + reason }) return @@ -957,7 +897,8 @@ export class ExecutorManager { this.mempool.replaceSubmitted(userOperationInfo, newTxInfo) }) - this.dropUserOperations(rejectedUserOperations) + // Drop all userOperations that were rejected during simulation. + this.dropUserOps(rejectedUserOperations) this.logger.info( { @@ -1005,20 +946,54 @@ export class ExecutorManager { }) } + handlePotentiallyIncluded({ + txInfo + }: { + txInfo: TransactionInfo + }) { + const { bundle, transactionHash: oldTxHash, executor } = txInfo + + this.logger.info( + { oldTxHash }, + "transaction potentially already included" + ) + txInfo.timesPotentiallyIncluded += 1 + + if (txInfo.timesPotentiallyIncluded >= 3) { + bundle.userOperations.map((userOperation) => { + this.mempool.removeSubmitted(userOperation.hash) + }) + this.logger.warn( + { oldTxHash }, + "transaction potentially already included too many times, removing" + ) + this.senderManager.markWalletProcessed(executor) + } + } + failedToReplaceTransaction({ - oldTxHash, - reason, - rejectedUserOperations + txInfo, + rejectedUserOperations, + reason }: { - oldTxHash: Hex + txInfo: TransactionInfo + rejectedUserOperations?: RejectedUserOperation[] reason: string - rejectedUserOperations: RejectedUserOperation[] }) { + const { executor, transactionHash: oldTxHash } = txInfo this.logger.warn({ oldTxHash, reason }, "failed to replace transaction") - this.dropUserOperations(rejectedUserOperations) + this.senderManager.markWalletProcessed(executor) + + const opsToDrop = + rejectedUserOperations ?? + txInfo.bundle.userOperations.map((userOperation) => ({ + userOperation, + reason: "Failed to replace transaction" + })) + this.dropUserOps(opsToDrop) } - dropUserOperations(rejectedUserOperations: RejectedUserOperation[]) { + dropUserOps(rejectedUserOperations: RejectedUserOperation[]) { rejectedUserOperations.map((rejectedUserOperation) => { const { userOperation, reason } = rejectedUserOperation const userOpHash = userOperation.hash From 78537465e7fefde4e10cd82b9d669c4b2f36f71d Mon Sep 17 00:00:00 2001 From: mouseless <97399882+mouseless-eth@users.noreply.github.com> Date: Mon, 27 Jan 2025 12:13:25 +0000 Subject: [PATCH 16/34] cleanup --- src/executor/executorManager.ts | 93 +++++++++++++++------------------ 1 file changed, 43 insertions(+), 50 deletions(-) diff --git a/src/executor/executorManager.ts b/src/executor/executorManager.ts index 52fbb579..625ccae0 100644 --- a/src/executor/executorManager.ts +++ b/src/executor/executorManager.ts @@ -214,24 +214,23 @@ export class ExecutorManager { const wallet = await this.senderManager.getWallet() - let nonce: number - let gasPriceParameters: GasPriceParameters - try { - ;[gasPriceParameters, nonce] = await Promise.all([ - this.gasPriceManager.tryGetNetworkGasPrice(), - this.config.publicClient.getTransactionCount({ - address: wallet.address, - blockTag: "latest" - }) - ]) - } catch (err) { - this.logger.error( - { error: err }, + const [gasPriceParameters, nonce] = await Promise.all([ + this.gasPriceManager.tryGetNetworkGasPrice(), + this.config.publicClient.getTransactionCount({ + address: wallet.address, + blockTag: "latest" + }) + ]).catch((_) => { + this.resubmitUserOperations( + bundle.userOperations, + bundle.entryPoint, "Failed to get parameters for bundling" ) this.senderManager.markWalletProcessed(wallet) - return undefined - } + return [] + }) + + if (!gasPriceParameters || nonce === undefined) return undefined const bundleResult = await this.executor.bundle({ wallet, @@ -252,7 +251,7 @@ export class ExecutorManager { return undefined } - // Unhandled error during simulation + // Unhandled error during simulation. if (bundleResult.status === "unhandled_simulation_failure") { const { reason, userOps } = bundleResult const rejectedUserOps = userOps.map((op) => ({ @@ -275,7 +274,7 @@ export class ExecutorManager { return undefined } - // All other bundle submission errors are unhandled. + // Encountered unhandled error during bundle simulation. if (bundleResult.status === "bundle_submission_failure") { const { userOps } = bundleResult const droppedUserOperations = userOps.map((op) => ({ @@ -712,7 +711,6 @@ export class ExecutorManager { } this.currentlyHandlingBlock = true - this.logger.debug({ blockNumber: block.number }, "handling block") const submittedEntries = this.mempool.dumpSubmittedOps() @@ -735,33 +733,27 @@ export class ExecutorManager { await Promise.all( transactionInfos.map(async (txInfo) => { - if ( - txInfo.transactionRequest.maxFeePerGas >= - gasPriceParameters.maxFeePerGas && - txInfo.transactionRequest.maxPriorityFeePerGas >= - gasPriceParameters.maxPriorityFeePerGas - ) { - return - } + const isMaxFeeTooLow = + txInfo.transactionRequest.maxFeePerGas < + gasPriceParameters.maxFeePerGas - await this.replaceTransaction(txInfo, "gas_price") - }) - ) + const isPriorityFeeTooLow = + txInfo.transactionRequest.maxPriorityFeePerGas < + gasPriceParameters.maxPriorityFeePerGas - // for any left check if enough time has passed, if so replace - const transactionInfos2 = getTransactionsFromUserOperationEntries( - this.mempool.dumpSubmittedOps() - ) - await Promise.all( - transactionInfos2.map(async (txInfo) => { - if ( - Date.now() - txInfo.lastReplaced < + const isStuck = + Date.now() - txInfo.lastReplaced > this.config.resubmitStuckTimeout - ) { + + if (isMaxFeeTooLow || isPriorityFeeTooLow) { + await this.replaceTransaction(txInfo, "gas_price") return } - await this.replaceTransaction(txInfo, "stuck") + if (isStuck) { + await this.replaceTransaction(txInfo, "stuck") + return + } }) ) @@ -772,18 +764,18 @@ export class ExecutorManager { txInfo: TransactionInfo, reason: string ): Promise { - let gasPriceParameters: GasPriceParameters - try { - gasPriceParameters = - await this.gasPriceManager.tryGetNetworkGasPrice() - } catch (err) { - this.failedToReplaceTransaction({ - txInfo, - reason: "Failed to get network gas price" + const gasPriceParameters = await this.gasPriceManager + .tryGetNetworkGasPrice() + .catch((_) => { + this.failedToReplaceTransaction({ + txInfo, + reason: "Failed to get network gas price" + }) + this.senderManager.markWalletProcessed(txInfo.executor) + return }) - this.senderManager.markWalletProcessed(txInfo.executor) - return - } + + if (!gasPriceParameters) return const { bundle, executor, transactionRequest } = txInfo @@ -831,6 +823,7 @@ export class ExecutorManager { return } + // Free wallet if no bundle was sent. if (bundleResult.status !== "bundle_success") { this.senderManager.markWalletProcessed(txInfo.executor) } From 06079fbfe948b0b2eb1ca7fc3a639b132c8d2356 Mon Sep 17 00:00:00 2001 From: mouseless <97399882+mouseless-eth@users.noreply.github.com> Date: Mon, 27 Jan 2025 12:14:50 +0000 Subject: [PATCH 17/34] fix build --- src/executor/executorManager.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/executor/executorManager.ts b/src/executor/executorManager.ts index 625ccae0..46dd1884 100644 --- a/src/executor/executorManager.ts +++ b/src/executor/executorManager.ts @@ -12,7 +12,6 @@ import { type TransactionInfo, RejectedUserOperation, UserOperationBundle, - GasPriceParameters, UserOperationWithHash } from "@alto/types" import type { BundlingStatus, Logger, Metrics } from "@alto/utils" From d9fa60392e1dfe2d9ccca441b373b02e3637fba0 Mon Sep 17 00:00:00 2001 From: mouseless <97399882+mouseless-eth@users.noreply.github.com> Date: Mon, 27 Jan 2025 13:23:05 +0000 Subject: [PATCH 18/34] get rid of UserOperationWithHash type --- src/executor/executorManager.ts | 13 +- src/executor/filterOpsAndEStimateGas.ts | 8 +- src/mempool/mempool.ts | 158 +++++++++++------------- src/mempool/store.ts | 17 +-- src/rpc/rpcHandler.ts | 9 +- src/types/mempool.ts | 17 +-- 6 files changed, 96 insertions(+), 126 deletions(-) diff --git a/src/executor/executorManager.ts b/src/executor/executorManager.ts index 46dd1884..19895ca3 100644 --- a/src/executor/executorManager.ts +++ b/src/executor/executorManager.ts @@ -12,7 +12,7 @@ import { type TransactionInfo, RejectedUserOperation, UserOperationBundle, - UserOperationWithHash + UserOperationInfo } from "@alto/types" import type { BundlingStatus, Logger, Metrics } from "@alto/utils" import { @@ -881,12 +881,7 @@ export class ExecutorManager { } userOpsBundled.map((userOperation) => { - const userOperationInfo = { - userOperation, - userOperationHash: userOperation.hash, - entryPoint: txInfo.bundle.entryPoint - } - this.mempool.replaceSubmitted(userOperationInfo, newTxInfo) + this.mempool.replaceSubmitted(userOperation, newTxInfo) }) // Drop all userOperations that were rejected during simulation. @@ -905,7 +900,7 @@ export class ExecutorManager { } markUserOperationsAsSubmitted( - userOperations: UserOperationWithHash[], + userOperations: UserOperationInfo[], transactionInfo: TransactionInfo ) { userOperations.map((op) => { @@ -919,7 +914,7 @@ export class ExecutorManager { } resubmitUserOperations( - userOperations: UserOperationWithHash[], + userOperations: UserOperationInfo[], entryPoint: Address, reason: string ) { diff --git a/src/executor/filterOpsAndEStimateGas.ts b/src/executor/filterOpsAndEStimateGas.ts index 92a4958d..98e3e661 100644 --- a/src/executor/filterOpsAndEStimateGas.ts +++ b/src/executor/filterOpsAndEStimateGas.ts @@ -3,8 +3,8 @@ import { EntryPointV06Abi, EntryPointV07Abi, FailedOpWithRevert, + UserOperationInfo, UserOperationV07, - UserOperationWithHash, failedOpErrorSchema, failedOpWithRevertErrorSchema } from "@alto/types" @@ -32,14 +32,14 @@ import { getAuthorizationList } from "./utils" import * as sentry from "@sentry/node" type FailedOpWithReason = { - userOperation: UserOperationWithHash + userOperation: UserOperationInfo reason: string } export type FilterOpsAndEstimateGasResult = | { status: "success" - opsToBundle: UserOperationWithHash[] + opsToBundle: UserOperationInfo[] failedOps: FailedOpWithReason[] gasLimit: bigint } @@ -74,7 +74,7 @@ export async function filterOpsAndEstimateGas({ > isUserOpV06: boolean wallet: Account - ops: UserOperationWithHash[] + ops: UserOperationInfo[] nonce: number maxFeePerGas: bigint maxPriorityFeePerGas: bigint diff --git a/src/mempool/mempool.ts b/src/mempool/mempool.ts index 37c8f677..0a458131 100644 --- a/src/mempool/mempool.ts +++ b/src/mempool/mempool.ts @@ -81,24 +81,17 @@ export class MemoryMempool { ): void { const op = this.store .dumpSubmitted() - .find( - (op) => - op.userOperation.userOperationHash === - userOperation.userOperationHash - ) + .find((op) => op.userOperation.hash === userOperation.hash) if (op) { - this.store.removeSubmitted(userOperation.userOperationHash) + this.store.removeSubmitted(userOperation.hash) this.store.addSubmitted({ userOperation, transactionInfo }) - this.monitor.setUserOperationStatus( - userOperation.userOperationHash, - { - status: "submitted", - transactionHash: transactionInfo.transactionHash - } - ) + this.monitor.setUserOperationStatus(userOperation.hash, { + status: "submitted", + transactionHash: transactionInfo.transactionHash + }) } } @@ -108,7 +101,7 @@ export class MemoryMempool { ): void { const op = this.store .dumpProcessing() - .find((op) => op.userOperationHash === userOpHash) + .find((op) => op.hash === userOpHash) if (op) { this.store.removeProcessing(userOpHash) this.store.addSubmitted({ @@ -122,7 +115,7 @@ export class MemoryMempool { } } - dumpOutstanding(): UserOperationInfo[] { + dumpOutstanding(): UserOperation[] { return this.store.dumpOutstanding() } @@ -207,8 +200,7 @@ export class MemoryMempool { factories: new Set() } - for (const mempoolOp of allOps) { - const op = mempoolOp.userOperation + for (const op of allOps) { entities.sender.add(op.sender) const isUserOpV06 = isVersion06(op) @@ -259,15 +251,14 @@ export class MemoryMempool { const existingUserOperation = [ ...outstandingOps, ...processedOrSubmittedOps - ].find(({ userOperationHash }) => userOperationHash === opHash) + ].find((userOperation) => userOperation.hash === opHash) if (existingUserOperation) { return [false, "Already known"] } if ( - processedOrSubmittedOps.find((opInfo) => { - const mempoolUserOp = opInfo.userOperation + processedOrSubmittedOps.find((mempoolUserOp) => { return ( mempoolUserOp.sender === userOperation.sender && mempoolUserOp.nonce === userOperation.nonce @@ -285,9 +276,7 @@ export class MemoryMempool { entryPoint ) const oldUserOp = [...outstandingOps, ...processedOrSubmittedOps].find( - (opInfo) => { - const mempoolUserOp = opInfo.userOperation - + (mempoolUserOp) => { const isSameSender = mempoolUserOp.sender === userOperation.sender @@ -331,17 +320,14 @@ export class MemoryMempool { ) const isOldUserOpProcessingOrSubmitted = processedOrSubmittedOps.some( - (submittedOp) => - submittedOp.userOperationHash === oldUserOp?.userOperationHash + (submittedOp) => submittedOp.hash === oldUserOp?.hash ) if (oldUserOp) { - const oldOp = oldUserOp.userOperation - let reason = "AA10 sender already constructed: A conflicting userOperation with initCode for this sender is already in the mempool. bump the gas price by minimum 10%" - if (oldOp.nonce === userOperation.nonce) { + if (oldUserOp.nonce === userOperation.nonce) { reason = "AA25 invalid account nonce: User operation already present in mempool, bump the gas price by minimum 10%" } @@ -351,9 +337,9 @@ export class MemoryMempool { return [false, reason] } - const oldMaxPriorityFeePerGas = oldOp.maxPriorityFeePerGas + const oldMaxPriorityFeePerGas = oldUserOp.maxPriorityFeePerGas const newMaxPriorityFeePerGas = userOperation.maxPriorityFeePerGas - const oldMaxFeePerGas = oldOp.maxFeePerGas + const oldMaxFeePerGas = oldUserOp.maxFeePerGas const newMaxFeePerGas = userOperation.maxFeePerGas const incrementMaxPriorityFeePerGas = @@ -369,14 +355,13 @@ export class MemoryMempool { return [false, reason] } - this.store.removeOutstanding(oldUserOp.userOperationHash) + this.store.removeOutstanding(oldUserOp.hash) } // Check if mempool already includes max amount of parallel user operations const parallelUserOperationsCount = this.store .dumpOutstanding() - .filter((userOpInfo) => { - const userOp = userOpInfo.userOperation + .filter((userOp) => { return userOp.sender === userOperation.sender }).length @@ -391,8 +376,7 @@ export class MemoryMempool { const [nonceKey] = getNonceKeyAndValue(userOperation.nonce) const queuedUserOperationsCount = this.store .dumpOutstanding() - .filter((userOpInfo) => { - const userOp = userOpInfo.userOperation + .filter((userOp) => { const [opNonceKey] = getNonceKeyAndValue(userOp.nonce) return ( @@ -409,9 +393,9 @@ export class MemoryMempool { } this.store.addOutstanding({ - userOperation, + ...userOperation, entryPoint, - userOperationHash: opHash, + hash: opHash, referencedContracts }) this.monitor.setUserOperationStatus(opHash, { @@ -425,7 +409,7 @@ export class MemoryMempool { // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: async shouldSkip( - opInfo: UserOperationInfo, + userOperation: UserOperationInfo, paymasterDeposit: { [paymaster: string]: bigint }, stakedEntityCount: { [addr: string]: number }, knownEntities: { @@ -447,7 +431,6 @@ export class MemoryMempool { senders: Set storageMap: StorageMap }> { - const op = opInfo.userOperation if (!this.config.safeMode) { return { skip: false, @@ -459,20 +442,22 @@ export class MemoryMempool { } } - const isUserOpV06 = isVersion06(op) + const isUserOpV06 = isVersion06(userOperation) const paymaster = isUserOpV06 - ? getAddressFromInitCodeOrPaymasterAndData(op.paymasterAndData) - : op.paymaster + ? getAddressFromInitCodeOrPaymasterAndData( + userOperation.paymasterAndData + ) + : userOperation.paymaster const factory = isUserOpV06 - ? getAddressFromInitCodeOrPaymasterAndData(op.initCode) - : op.factory + ? getAddressFromInitCodeOrPaymasterAndData(userOperation.initCode) + : userOperation.factory const paymasterStatus = this.reputationManager.getStatus( - opInfo.entryPoint, + userOperation.entryPoint, paymaster ) const factoryStatus = this.reputationManager.getStatus( - opInfo.entryPoint, + userOperation.entryPoint, factory ) @@ -480,7 +465,7 @@ export class MemoryMempool { paymasterStatus === ReputationStatuses.banned || factoryStatus === ReputationStatuses.banned ) { - this.store.removeOutstanding(opInfo.userOperationHash) + this.store.removeOutstanding(userOperation.hash) return { skip: true, paymasterDeposit, @@ -499,7 +484,7 @@ export class MemoryMempool { this.logger.trace( { paymaster, - opHash: opInfo.userOperationHash + opHash: userOperation.hash }, "Throttled paymaster skipped" ) @@ -521,7 +506,7 @@ export class MemoryMempool { this.logger.trace( { factory, - opHash: opInfo.userOperationHash + opHash: userOperation.hash }, "Throttled factory skipped" ) @@ -536,13 +521,13 @@ export class MemoryMempool { } if ( - senders.has(op.sender) && + senders.has(userOperation.sender) && this.config.enforceUniqueSendersPerBundle ) { this.logger.trace( { - sender: op.sender, - opHash: opInfo.userOperationHash + sender: userOperation.sender, + opHash: userOperation.hash }, "Sender skipped because already included in bundle" ) @@ -563,27 +548,27 @@ export class MemoryMempool { if (!isUserOpV06) { queuedUserOperations = await this.getQueuedUserOperations( - op, - opInfo.entryPoint + userOperation, + userOperation.entryPoint ) } validationResult = await this.validator.validateUserOperation({ shouldCheckPrefund: false, - userOperation: op, + userOperation: userOperation, queuedUserOperations, - entryPoint: opInfo.entryPoint, - referencedContracts: opInfo.referencedContracts + entryPoint: userOperation.entryPoint, + referencedContracts: userOperation.referencedContracts }) } catch (e) { this.logger.error( { - opHash: opInfo.userOperationHash, + opHash: userOperation.hash, error: JSON.stringify(e) }, "2nd Validation error" ) - this.store.removeOutstanding(opInfo.userOperationHash) + this.store.removeOutstanding(userOperation.hash) return { skip: true, paymasterDeposit, @@ -597,11 +582,14 @@ export class MemoryMempool { for (const storageAddress of Object.keys(validationResult.storageMap)) { const address = getAddress(storageAddress) - if (address !== op.sender && knownEntities.sender.has(address)) { + if ( + address !== userOperation.sender && + knownEntities.sender.has(address) + ) { this.logger.trace( { storageAddress, - opHash: opInfo.userOperationHash + opHash: userOperation.hash }, "Storage address skipped" ) @@ -620,7 +608,7 @@ export class MemoryMempool { if (paymasterDeposit[paymaster] === undefined) { const entryPointContract = getContract({ abi: isUserOpV06 ? EntryPointV06Abi : EntryPointV07Abi, - address: opInfo.entryPoint, + address: userOperation.entryPoint, client: { public: this.config.publicClient } @@ -635,7 +623,7 @@ export class MemoryMempool { this.logger.trace( { paymaster, - opHash: opInfo.userOperationHash + opHash: userOperation.hash }, "Paymaster skipped because of insufficient balance left to sponsor all user ops in the bundle" ) @@ -657,7 +645,7 @@ export class MemoryMempool { stakedEntityCount[factory] = (stakedEntityCount[factory] ?? 0) + 1 } - senders.add(op.sender) + senders.add(userOperation.sender) return { skip: false, @@ -684,13 +672,10 @@ export class MemoryMempool { let outstandingUserOperations = this.store .dumpOutstanding() .filter((op) => op.entryPoint === entryPoint) - .sort((a, b) => { + .sort((aUserOp, bUserOp) => { // Sort userops before the execution // Decide the order of the userops based on the sender and nonce // If sender is the same, sort by nonce key - const aUserOp = a.userOperation - const bUserOp = b.userOperation - if (aUserOp.sender === bUserOp.sender) { const [aNonceKey, aNonceValue] = getNonceKeyAndValue( aUserOp.nonce @@ -710,10 +695,10 @@ export class MemoryMempool { }) .slice() - // Get EntryPoint version. (Ideally version should be derived from EntryPoint) - const isV6 = isVersion06(outstandingUserOperations[0].userOperation) + // Get EntryPoint version. (Ideally version should be derived from CLI flags) + const isV6 = isVersion06(outstandingUserOperations[0]) const allSameVersion = outstandingUserOperations.every( - ({ userOperation }) => isVersion06(userOperation) === isV6 + (userOperation) => isVersion06(userOperation) === isV6 ) if (!allSameVersion) { throw new Error( @@ -746,11 +731,11 @@ export class MemoryMempool { // Keep adding ops to current bundle. while (outstandingUserOperations.length > 0) { - const opInfo = outstandingUserOperations.shift() - if (!opInfo) break + const userOperation = outstandingUserOperations.shift() + if (!userOperation) break const skipResult = await this.shouldSkip( - opInfo, + userOperation, paymasterDeposit, stakedEntityCount, knownEntities, @@ -759,13 +744,12 @@ export class MemoryMempool { ) if (skipResult.skip) continue - const op = opInfo.userOperation gasUsed += - op.callGasLimit + - op.verificationGasLimit + - (isVersion07(op) - ? (op.paymasterPostOpGasLimit || 0n) + - (op.paymasterVerificationGasLimit || 0n) + userOperation.callGasLimit + + userOperation.verificationGasLimit + + (isVersion07(userOperation) + ? (userOperation.paymasterPostOpGasLimit || 0n) + + (userOperation.paymasterVerificationGasLimit || 0n) : 0n) // Only break on gas limit if we've hit minOpsPerBundle. @@ -773,7 +757,7 @@ export class MemoryMempool { gasUsed > maxGasLimit && currentBundle.userOperations.length >= minOpsPerBundle ) { - outstandingUserOperations.unshift(opInfo) // re-add op to front of queue + outstandingUserOperations.unshift(userOperation) // re-add op to front of queue break } @@ -784,15 +768,12 @@ export class MemoryMempool { senders = skipResult.senders storageMap = skipResult.storageMap - this.reputationManager.decreaseUserOperationCount(op) - this.store.removeOutstanding(opInfo.userOperationHash) - this.store.addProcessing(opInfo) + this.reputationManager.decreaseUserOperationCount(userOperation) + this.store.removeOutstanding(userOperation.hash) + this.store.addProcessing(userOperation) // Add op to current bundle - const chainId = this.config.publicClient.chain.id - const opHash = getUserOperationHash(op, entryPoint, chainId) - const opWithHash = { ...op, hash: opHash } - currentBundle.userOperations.push(opWithHash) + currentBundle.userOperations.push(userOperation) } if (currentBundle.userOperations.length > 0) { @@ -842,7 +823,6 @@ export class MemoryMempool { const outstanding = this.store .dumpOutstanding() - .map(({ userOperation }) => userOperation) .filter((mempoolUserOp) => { const [opNonceKey, opNonceValue] = getNonceKeyAndValue( mempoolUserOp.nonce diff --git a/src/mempool/store.ts b/src/mempool/store.ts index 7d8d23dd..d970d7cb 100644 --- a/src/mempool/store.ts +++ b/src/mempool/store.ts @@ -25,7 +25,7 @@ export class MemoryStore { store.push(op) this.logger.debug( - { userOpHash: op.userOperationHash, store: "outstanding" }, + { userOpHash: op.hash, store: "outstanding" }, "added user op to mempool" ) this.metrics.userOperationsInMempool @@ -40,7 +40,7 @@ export class MemoryStore { store.push(op) this.logger.debug( - { userOpHash: op.userOperationHash, store: "processing" }, + { userOpHash: op.hash, store: "processing" }, "added user op to mempool" ) this.metrics.userOperationsInMempool @@ -50,13 +50,14 @@ export class MemoryStore { .inc() } - addSubmitted(op: SubmittedUserOperation) { + addSubmitted(submittedInfo: SubmittedUserOperation) { + const { userOperation } = submittedInfo const store = this.submittedUserOperations - store.push(op) + store.push(submittedInfo) this.logger.debug( { - userOpHash: op.userOperation.userOperationHash, + userOpHash: userOperation.hash, store: "submitted" }, "added user op to submitted mempool" @@ -70,7 +71,7 @@ export class MemoryStore { removeOutstanding(userOpHash: HexData32) { const index = this.outstandingUserOperations.findIndex( - (op) => op.userOperationHash === userOpHash + (op) => op.hash === userOpHash ) if (index === -1) { this.logger.warn( @@ -94,7 +95,7 @@ export class MemoryStore { removeProcessing(userOpHash: HexData32) { const index = this.processingUserOperations.findIndex( - (op) => op.userOperationHash === userOpHash + (op) => op.hash === userOpHash ) if (index === -1) { this.logger.warn( @@ -118,7 +119,7 @@ export class MemoryStore { removeSubmitted(userOpHash: HexData32) { const index = this.submittedUserOperations.findIndex( - (op) => op.userOperation.userOperationHash === userOpHash + (op) => op.userOperation.hash === userOpHash ) if (index === -1) { this.logger.warn( diff --git a/src/rpc/rpcHandler.ts b/src/rpc/rpcHandler.ts index 7fc3b127..abbf260c 100644 --- a/src/rpc/rpcHandler.ts +++ b/src/rpc/rpcHandler.ts @@ -576,9 +576,7 @@ export class RpcHandler implements IRpcEndpoint { this.ensureDebugEndpointsAreEnabled("debug_bundler_dumpMempool") this.ensureEntryPointIsSupported(entryPoint) - return this.mempool - .dumpOutstanding() - .map(({ userOperation }) => userOperation) + return this.mempool.dumpOutstanding() } async debug_bundler_sendBundleNow(): Promise { @@ -859,8 +857,9 @@ export class RpcHandler implements IRpcEndpoint { ) // Prepare bundle - const userOperationWithHash = { + const userOperationInfo = { ...userOperation, + entryPoint, hash: getUserOperationHash( userOperation, entryPoint, @@ -869,7 +868,7 @@ export class RpcHandler implements IRpcEndpoint { } const bundle = { entryPoint, - userOperations: [userOperationWithHash], + userOperations: [userOperationInfo], version: isVersion06(userOperation) ? ("0.6" as const) : ("0.7" as const) diff --git a/src/types/mempool.ts b/src/types/mempool.ts index 432fb97c..1085357f 100644 --- a/src/types/mempool.ts +++ b/src/types/mempool.ts @@ -29,16 +29,11 @@ export type TransactionInfo = { export type UserOperationBundle = { entryPoint: Address version: "0.6" | "0.7" - userOperations: UserOperationWithHash[] + userOperations: UserOperationInfo[] } -export type UserOperationWithHash = UserOperation & { +export type UserOperationInfo = UserOperation & { hash: Hex -} - -export type UserOperationInfo = { - userOperation: UserOperation - userOperationHash: HexData32 entryPoint: Address referencedContracts?: ReferencedCodeHashes } @@ -56,7 +51,7 @@ export type SubmittedUserOperation = { } export type RejectedUserOperation = { - userOperation: UserOperationWithHash + userOperation: UserOperationInfo reason: string } @@ -64,7 +59,7 @@ export type BundleResult = | { // Successfully sent bundle. status: "bundle_success" - userOpsBundled: UserOperationWithHash[] + userOpsBundled: UserOperationInfo[] rejectedUserOperations: RejectedUserOperation[] transactionHash: HexData32 transactionRequest: { @@ -78,7 +73,7 @@ export type BundleResult = // Encountered unhandled error during bundle simulation. status: "unhandled_simulation_failure" reason: string - userOps: UserOperationWithHash[] + userOps: UserOperationInfo[] } | { // All user operations failed during simulation. @@ -89,5 +84,5 @@ export type BundleResult = // Encountered error whilst trying to send bundle. status: "bundle_submission_failure" reason: BaseError | "INTERNAL FAILURE" - userOps: UserOperationWithHash[] + userOps: UserOperationInfo[] } From a2cee31c195a3e0fb251a4d275c9d73453f8fe07 Mon Sep 17 00:00:00 2001 From: mouseless <97399882+mouseless-eth@users.noreply.github.com> Date: Mon, 27 Jan 2025 13:37:00 +0000 Subject: [PATCH 19/34] fix --- src/mempool/mempool.ts | 2 ++ test/e2e/deploy-contracts/index.ts | 2 +- test/e2e/package.json | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/mempool/mempool.ts b/src/mempool/mempool.ts index 0a458131..2d9151b6 100644 --- a/src/mempool/mempool.ts +++ b/src/mempool/mempool.ts @@ -695,6 +695,8 @@ export class MemoryMempool { }) .slice() + if (outstandingUserOperations.length === 0) return [] + // Get EntryPoint version. (Ideally version should be derived from CLI flags) const isV6 = isVersion06(outstandingUserOperations[0]) const allSameVersion = outstandingUserOperations.every( diff --git a/test/e2e/deploy-contracts/index.ts b/test/e2e/deploy-contracts/index.ts index 9b637053..6bd8b7e9 100644 --- a/test/e2e/deploy-contracts/index.ts +++ b/test/e2e/deploy-contracts/index.ts @@ -22,7 +22,7 @@ const verifyDeployed = async ({ client }: { addresses: Address[]; client: PublicClient }) => { for (const address of addresses) { - const bytecode = await client.getCode({ + const bytecode = await client.getBytecode({ address }) diff --git a/test/e2e/package.json b/test/e2e/package.json index 9dba2f7d..6bd14b99 100644 --- a/test/e2e/package.json +++ b/test/e2e/package.json @@ -16,7 +16,7 @@ "permissionless": "^0.2.1", "prool": "^0.0.16", "ts-node": "^10.9.2", - "viem": "^2.9.5", + "viem": "^2.22.15", "vitest": "^1.6.0", "wait-port": "^1.1.0" } From a098631b1de9abc8f7d22842f4f80ebd9a5bb8b2 Mon Sep 17 00:00:00 2001 From: mouseless <97399882+mouseless-eth@users.noreply.github.com> Date: Mon, 27 Jan 2025 15:50:00 +0000 Subject: [PATCH 20/34] revert e2e test changes --- test/e2e/deploy-contracts/index.ts | 2 +- test/e2e/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/e2e/deploy-contracts/index.ts b/test/e2e/deploy-contracts/index.ts index 6bd8b7e9..9b637053 100644 --- a/test/e2e/deploy-contracts/index.ts +++ b/test/e2e/deploy-contracts/index.ts @@ -22,7 +22,7 @@ const verifyDeployed = async ({ client }: { addresses: Address[]; client: PublicClient }) => { for (const address of addresses) { - const bytecode = await client.getBytecode({ + const bytecode = await client.getCode({ address }) diff --git a/test/e2e/package.json b/test/e2e/package.json index 6bd14b99..9dba2f7d 100644 --- a/test/e2e/package.json +++ b/test/e2e/package.json @@ -16,7 +16,7 @@ "permissionless": "^0.2.1", "prool": "^0.0.16", "ts-node": "^10.9.2", - "viem": "^2.22.15", + "viem": "^2.9.5", "vitest": "^1.6.0", "wait-port": "^1.1.0" } From fc42a221d8596306f561a00af2d486028ff8955c Mon Sep 17 00:00:00 2001 From: mouseless <97399882+mouseless-eth@users.noreply.github.com> Date: Mon, 27 Jan 2025 15:56:02 +0000 Subject: [PATCH 21/34] fix --- test/e2e/deploy-contracts/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/deploy-contracts/index.ts b/test/e2e/deploy-contracts/index.ts index 9b637053..6bd8b7e9 100644 --- a/test/e2e/deploy-contracts/index.ts +++ b/test/e2e/deploy-contracts/index.ts @@ -22,7 +22,7 @@ const verifyDeployed = async ({ client }: { addresses: Address[]; client: PublicClient }) => { for (const address of addresses) { - const bytecode = await client.getCode({ + const bytecode = await client.getBytecode({ address }) From 843144e08fbac2cb2fc661ba366128610de985bb Mon Sep 17 00:00:00 2001 From: mouseless <97399882+mouseless-eth@users.noreply.github.com> Date: Mon, 27 Jan 2025 16:12:46 +0000 Subject: [PATCH 22/34] add comments --- src/executor/executorManager.ts | 2 +- src/mempool/mempool.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/executor/executorManager.ts b/src/executor/executorManager.ts index 19895ca3..5898082f 100644 --- a/src/executor/executorManager.ts +++ b/src/executor/executorManager.ts @@ -223,7 +223,7 @@ export class ExecutorManager { this.resubmitUserOperations( bundle.userOperations, bundle.entryPoint, - "Failed to get parameters for bundling" + "Failed to get nonce and gas parameters for bundling" ) this.senderManager.markWalletProcessed(wallet) return [] diff --git a/src/mempool/mempool.ts b/src/mempool/mempool.ts index 2d9151b6..1447dbcd 100644 --- a/src/mempool/mempool.ts +++ b/src/mempool/mempool.ts @@ -736,6 +736,8 @@ export class MemoryMempool { const userOperation = outstandingUserOperations.shift() if (!userOperation) break + // NOTE: currently if a userOp is skipped due to sender enforceUniqueSendersPerBundle it will be picked up + // again the next time mempool.process is called. const skipResult = await this.shouldSkip( userOperation, paymasterDeposit, From 584ccade76732cb60afb944feb810e4e2c1860bc Mon Sep 17 00:00:00 2001 From: mouseless <97399882+mouseless-eth@users.noreply.github.com> Date: Tue, 28 Jan 2025 12:31:11 +0000 Subject: [PATCH 23/34] cleanup --- src/executor/executor.ts | 74 +++++-- src/executor/executorManager.ts | 256 ++++++++++-------------- src/executor/filterOpsAndEStimateGas.ts | 14 +- src/mempool/mempool.ts | 20 ++ src/types/mempool.ts | 6 +- 5 files changed, 190 insertions(+), 180 deletions(-) diff --git a/src/executor/executor.ts b/src/executor/executor.ts index 78f58fbb..cf996459 100644 --- a/src/executor/executor.ts +++ b/src/executor/executor.ts @@ -30,9 +30,10 @@ import { getContract, type Account, type Hex, - NonceTooHighError + NonceTooHighError, + BaseError } from "viem" -import { getAuthorizationList } from "./utils" +import { getAuthorizationList, isTransactionUnderpricedError } from "./utils" import type { SendTransactionErrorType } from "viem" import type { AltoConfig } from "../createConfig" import type { SendTransactionOptions } from "./types" @@ -47,7 +48,7 @@ export interface GasEstimateResult { export type HandleOpsTxParam = { ops: UserOperation[] - isUserOpVersion06: boolean + isUserOpV06: boolean isReplacementTx: boolean entryPoint: Address } @@ -109,6 +110,7 @@ export class Executor { cancelOps(_entryPoint: Address, _ops: UserOperation[]): Promise { throw new Error("Method not implemented.") } + async sendHandleOpsTransaction({ txParam, opts @@ -132,17 +134,17 @@ export class Executor { nonce: number } }) { - const { isUserOpVersion06, ops, entryPoint } = txParam + const { isUserOpV06, ops, entryPoint } = txParam const packedOps = ops.map((op) => { - if (isUserOpVersion06) { + if (isUserOpV06) { return op } return toPackedUserOperation(op as UserOperationV07) }) as PackedUserOperation[] const data = encodeFunctionData({ - abi: isUserOpVersion06 ? EntryPointV06Abi : EntryPointV07Abi, + abi: isUserOpV06 ? EntryPointV06Abi : EntryPointV07Abi, functionName: "handleOps", args: [packedOps, opts.account.address] }) @@ -168,7 +170,7 @@ export class Executor { try { if ( this.config.enableFastlane && - isUserOpVersion06 && + isUserOpV06 && !txParam.isReplacementTx && attempts === 0 ) { @@ -190,6 +192,21 @@ export class Executor { break } catch (e: unknown) { + if (e instanceof BaseError) { + if (isTransactionUnderpricedError(e)) { + this.logger.warn("Transaction underpriced, retrying") + + request.maxFeePerGas = scaleBigIntByPercent( + request.maxFeePerGas, + 150n + ) + request.maxPriorityFeePerGas = scaleBigIntByPercent( + request.maxPriorityFeePerGas, + 150n + ) + } + } + const error = e as SendTransactionErrorType if (error instanceof TransactionExecutionError) { @@ -243,18 +260,21 @@ export class Executor { bundle, nonce, gasPriceParameters, - gasLimitSuggestion + gasLimitSuggestion, + isReplacementTx }: { wallet: Account bundle: UserOperationBundle nonce: number gasPriceParameters: GasPriceParameters gasLimitSuggestion?: bigint + isReplacementTx: boolean }): Promise { const { entryPoint, userOperations, version } = bundle + const isUserOpV06 = version === "0.6" const ep = getContract({ - abi: version === "0.6" ? EntryPointV06Abi : EntryPointV07Abi, + abi: isUserOpV06 ? EntryPointV06Abi : EntryPointV07Abi, address: entryPoint, client: { public: this.config.publicClient, @@ -268,7 +288,7 @@ export class Executor { }) let estimateResult = await filterOpsAndEstimateGas({ - isUserOpV06: version === "0.6", + isUserOpV06, ops: userOperations, ep, wallet, @@ -280,18 +300,17 @@ export class Executor { logger: childLogger }) - if (estimateResult.status === "unexpectedFailure") { + if (estimateResult.status === "unexpected_failure") { childLogger.error( "gas limit simulation encountered unexpected failure" ) return { status: "unhandled_simulation_failure", - reason: "INTERNAL FAILURE", - userOps: userOperations + reason: "INTERNAL FAILURE" } } - if (estimateResult.status === "allOpsFailedSimulation") { + if (estimateResult.status === "all_ops_failed_simulation") { childLogger.warn("all ops failed simulation") return { status: "all_ops_failed_simulation", @@ -307,6 +326,17 @@ export class Executor { entryPoint }) + // https://github.com/eth-infinitism/account-abstraction/blob/fa61290d37d079e928d92d53a122efcc63822214/contracts/core/EntryPoint.sol#L236 + let innerHandleOpFloor = 0n + for (const op of opsToBundle) { + innerHandleOpFloor += + op.callGasLimit + op.verificationGasLimit + 5000n + } + + if (gasLimit < innerHandleOpFloor) { + gasLimit += innerHandleOpFloor + } + // sometimes the estimation rounds down, adding a fixed constant accounts for this gasLimit += 10_000n gasLimit = gasLimitSuggestion @@ -353,8 +383,8 @@ export class Executor { transactionHash = await this.sendHandleOpsTransaction({ txParam: { ops: opsToBundle, - isReplacementTx: false, - isUserOpVersion06: version === "0.6", + isReplacementTx, + isUserOpV06, entryPoint }, opts @@ -366,11 +396,14 @@ export class Executor { }) } catch (err: unknown) { const e = parseViemError(err) + + const { failedOps, opsToBundle } = estimateResult if (e) { return { + rejectedUserOps: failedOps, + userOpsToBundle: opsToBundle, status: "bundle_submission_failure", - reason: e, - userOps: userOperations + reason: e } } @@ -380,9 +413,10 @@ export class Executor { "error submitting bundle transaction" ) return { + rejectedUserOps: failedOps, + userOpsToBundle: opsToBundle, status: "bundle_submission_failure", - reason: "INTERNAL FAILURE", - userOps: userOperations + reason: "INTERNAL FAILURE" } } diff --git a/src/executor/executorManager.ts b/src/executor/executorManager.ts index bf686f17..38b418d6 100644 --- a/src/executor/executorManager.ts +++ b/src/executor/executorManager.ts @@ -135,7 +135,7 @@ export class ExecutorManager { (timestamp) => now - timestamp < RPM_WINDOW ) - const bundles = await this.getMempoolBundles() + const bundles = await this.mempool.getBundles() if (bundles.length > 0) { const opsCount: number = bundles @@ -166,29 +166,9 @@ export class ExecutorManager { } } - async getMempoolBundles( - maxBundleCount?: number - ): Promise { - const bundlePromises = this.config.entrypoints.map( - async (entryPoint) => { - return await this.mempool.process({ - entryPoint, - maxGasLimit: this.config.maxGasPerBundle, - minOpsPerBundle: 1, - maxBundleCount - }) - } - ) - - const bundlesNested = await Promise.all(bundlePromises) - const bundles = bundlesNested.flat() - - return bundles - } - // Debug endpoint async sendBundleNow(): Promise { - const bundle = (await this.getMempoolBundles(1))[0] + const bundle = (await this.mempool.getBundles(1))[0] if (bundle.userOperations.length === 0) { throw new Error("no ops to bundle") @@ -220,22 +200,26 @@ export class ExecutorManager { blockTag: "latest" }) ]).catch((_) => { + return [] + }) + + if (!gasPriceParameters || nonce === undefined) { this.resubmitUserOperations( bundle.userOperations, bundle.entryPoint, "Failed to get nonce and gas parameters for bundling" ) + // Free executor if failed to get initial params. this.senderManager.markWalletProcessed(wallet) - return [] - }) - - if (!gasPriceParameters || nonce === undefined) return undefined + return undefined + } const bundleResult = await this.executor.bundle({ wallet, bundle, nonce, - gasPriceParameters + gasPriceParameters, + isReplacementTx: false }) // Free wallet if no bundle was sent. @@ -250,10 +234,11 @@ export class ExecutorManager { return undefined } - // Unhandled error during simulation. + // Unhandled error during simulation, drop all ops. if (bundleResult.status === "unhandled_simulation_failure") { - const { reason, userOps } = bundleResult - const rejectedUserOps = userOps.map((op) => ({ + const { reason } = bundleResult + const { userOperations } = bundle + const rejectedUserOps = userOperations.map((op) => ({ userOperation: op, reason })) @@ -267,27 +252,35 @@ export class ExecutorManager { bundleResult.status === "bundle_submission_failure" && bundleResult.reason instanceof InsufficientFundsError ) { - const { userOps, reason } = bundleResult - this.resubmitUserOperations(userOps, entryPoint, reason.name) + const { reason, userOpsToBundle, rejectedUserOps } = bundleResult + this.dropUserOps(rejectedUserOps) + this.resubmitUserOperations( + userOpsToBundle, + entryPoint, + reason.name + ) this.metrics.bundlesSubmitted.labels({ status: "resubmit" }).inc() return undefined } // Encountered unhandled error during bundle simulation. if (bundleResult.status === "bundle_submission_failure") { - const { userOps } = bundleResult - const droppedUserOperations = userOps.map((op) => ({ - userOperation: op, - reason: "INTERNAL FAILURE" - })) - this.dropUserOps(droppedUserOperations) + const { rejectedUserOps, userOpsToBundle, reason } = bundleResult + // NOTE: these ops passed validation but dropped due to error during bundling + this.dropUserOps(rejectedUserOps) + this.resubmitUserOperations( + userOpsToBundle, + entryPoint, + reason instanceof BaseError ? reason.name : "INTERNAL FAILURE" + ) this.metrics.bundlesSubmitted.labels({ status: "failed" }).inc() + return undefined } if (bundleResult.status === "bundle_success") { const { userOpsBundled, - rejectedUserOperations, + rejectedUserOps: rejectedUserOperations, transactionRequest, transactionHash } = bundleResult @@ -343,16 +336,13 @@ export class ExecutorManager { } // update the current status of the bundling transaction/s - private async refreshTransactionStatus( - entryPoint: Address, - transactionInfo: TransactionInfo - ) { + private async refreshTransactionStatus(transactionInfo: TransactionInfo) { const { transactionHash: currentTransactionHash, bundle, previousTransactionHashes } = transactionInfo - const { userOperations, version } = bundle + const { userOperations, version, entryPoint } = bundle const isVersion06 = version === "0.6" const txHashesToCheck = [ @@ -393,6 +383,24 @@ export class ExecutorManager { transactionHash: `0x${string}` } + // TODO: there has to be a better way of solving onchain AA95 errors. + if (bundlingStatus.status === "reverted" && bundlingStatus.isAA95) { + // resubmit with more gas when bundler encounters AA95 + transactionInfo.transactionRequest.gas = scaleBigIntByPercent( + transactionInfo.transactionRequest.gas, + this.config.aa95GasMultiplier + ) + transactionInfo.transactionRequest.nonce += 1 + + await this.replaceTransaction(transactionInfo, "AA95") + return + } + + // Free executor if tx landed onchain + if (bundlingStatus.status !== "not_found") { + this.senderManager.markWalletProcessed(transactionInfo.executor) + } + if (bundlingStatus.status === "included") { this.metrics.userOperationsOnChain .labels({ status: bundlingStatus.status }) @@ -439,21 +447,9 @@ export class ExecutorManager { "user op included" ) }) + } - this.senderManager.markWalletProcessed(transactionInfo.executor) - } else if ( - bundlingStatus.status === "reverted" && - bundlingStatus.isAA95 - ) { - // resubmit with more gas when bundler encounters AA95 - transactionInfo.transactionRequest.gas = scaleBigIntByPercent( - transactionInfo.transactionRequest.gas, - this.config.aa95GasMultiplier - ) - transactionInfo.transactionRequest.nonce += 1 - - await this.replaceTransaction(transactionInfo, "AA95") - } else { + if (bundlingStatus.status === "reverted") { await Promise.all( userOperations.map((userOperation) => { this.checkFrontrun({ @@ -463,11 +459,7 @@ export class ExecutorManager { }) }) ) - - userOperations.map((userOperation) => { - this.mempool.removeSubmitted(userOperation.hash) - }) - this.senderManager.markWalletProcessed(transactionInfo.executor) + this.removeSubmitted(userOperations) } } @@ -667,43 +659,6 @@ export class ExecutorManager { return userOperationReceipt } - async refreshUserOperationStatuses(): Promise { - const ops = this.mempool.dumpSubmittedOps() - - const opEntryPointMap = new Map() - - for (const op of ops) { - if (!opEntryPointMap.has(op.userOperation.entryPoint)) { - opEntryPointMap.set(op.userOperation.entryPoint, []) - } - opEntryPointMap.get(op.userOperation.entryPoint)?.push(op) - } - - await Promise.all( - this.config.entrypoints.map(async (entryPoint) => { - const ops = opEntryPointMap.get(entryPoint) - - if (ops) { - const txs = getTransactionsFromUserOperationEntries(ops) - - await Promise.all( - txs.map(async (txInfo) => { - await this.refreshTransactionStatus( - entryPoint, - txInfo - ) - }) - ) - } else { - this.logger.warn( - { entryPoint }, - "no user operations for entry point" - ) - } - }) - ) - } - async handleBlock(block: Block) { if (this.currentlyHandlingBlock) { return @@ -720,23 +675,19 @@ export class ExecutorManager { } // refresh op statuses - await this.refreshUserOperationStatuses() + const ops = this.mempool.dumpSubmittedOps() + const txs = getTransactionsFromUserOperationEntries(ops) + await Promise.all( + txs.map((txInfo) => this.refreshTransactionStatus(txInfo)) + ) // for all still not included check if needs to be replaced (based on gas price) - let gasPriceParameters: { - maxFeePerGas: bigint - maxPriorityFeePerGas: bigint - } - - try { - gasPriceParameters = - await this.gasPriceManager.tryGetNetworkGasPrice() - } catch { - gasPriceParameters = { + const gasPriceParameters = await this.gasPriceManager + .tryGetNetworkGasPrice() + .catch(() => ({ maxFeePerGas: 0n, maxPriorityFeePerGas: 0n - } - } + })) const transactionInfos = getTransactionsFromUserOperationEntries( this.mempool.dumpSubmittedOps() @@ -778,17 +729,22 @@ export class ExecutorManager { const gasPriceParameters = await this.gasPriceManager .tryGetNetworkGasPrice() .catch((_) => { - this.failedToReplaceTransaction({ - txInfo, - reason: "Failed to get network gas price" - }) - this.senderManager.markWalletProcessed(txInfo.executor) - return + return undefined }) - if (!gasPriceParameters) return + if (!gasPriceParameters) { + this.failedToReplaceTransaction({ + txInfo, + reason: "Failed to get network gas price during replacement" + }) + // Free executor if failed to get initial params. + this.senderManager.markWalletProcessed(txInfo.executor) + return + } + // Setup vars const { bundle, executor, transactionRequest } = txInfo + const oldTxHash = txInfo.transactionHash const bundleResult = await this.executor.bundle({ wallet: executor, @@ -804,9 +760,11 @@ export class ExecutorManager { 115n ) }, - gasLimitSuggestion: transactionRequest.gas + gasLimitSuggestion: transactionRequest.gas, + isReplacementTx: true }) + // Log metrics. const replaceStatus = bundleResult && bundleResult.status === "bundle_success" ? "succeeded" @@ -820,6 +778,7 @@ export class ExecutorManager { const nonceTooLow = bundleResult.status === "bundle_submission_failure" && bundleResult.reason instanceof NonceTooLowError + const allOpsFailedSimulation = bundleResult.status === "all_ops_failed_simulation" && bundleResult.rejectedUserOps.every( @@ -827,10 +786,25 @@ export class ExecutorManager { op.reason === "AA25 invalid account nonce" || op.reason === "AA10 sender already constructed" ) + const potentiallyIncluded = nonceTooLow || allOpsFailedSimulation if (potentiallyIncluded) { - this.handlePotentiallyIncluded({ txInfo }) + this.logger.info( + { oldTxHash }, + "transaction potentially already included" + ) + txInfo.timesPotentiallyIncluded += 1 + + if (txInfo.timesPotentiallyIncluded >= 3) { + this.removeSubmitted(bundle.userOperations) + this.logger.warn( + { oldTxHash }, + "transaction potentially already included too many times, removing" + ) + this.senderManager.markWalletProcessed(executor) + } + return } @@ -844,6 +818,7 @@ export class ExecutorManager { txInfo, reason: bundleResult.reason }) + return } @@ -853,6 +828,7 @@ export class ExecutorManager { reason: "all ops failed simulation", rejectedUserOperations: bundleResult.rejectedUserOps }) + return } @@ -871,7 +847,7 @@ export class ExecutorManager { } const { - rejectedUserOperations, + rejectedUserOps: rejectedUserOperations, userOpsBundled, transactionRequest: newTransactionRequest, transactionHash: newTransactionHash @@ -945,31 +921,6 @@ export class ExecutorManager { }) } - handlePotentiallyIncluded({ - txInfo - }: { - txInfo: TransactionInfo - }) { - const { bundle, transactionHash: oldTxHash, executor } = txInfo - - this.logger.info( - { oldTxHash }, - "transaction potentially already included" - ) - txInfo.timesPotentiallyIncluded += 1 - - if (txInfo.timesPotentiallyIncluded >= 3) { - bundle.userOperations.map((userOperation) => { - this.mempool.removeSubmitted(userOperation.hash) - }) - this.logger.warn( - { oldTxHash }, - "transaction potentially already included too many times, removing" - ) - this.senderManager.markWalletProcessed(executor) - } - } - failedToReplaceTransaction({ txInfo, rejectedUserOperations, @@ -979,9 +930,8 @@ export class ExecutorManager { rejectedUserOperations?: RejectedUserOperation[] reason: string }) { - const { executor, transactionHash: oldTxHash } = txInfo + const { transactionHash: oldTxHash } = txInfo this.logger.warn({ oldTxHash, reason }, "failed to replace transaction") - this.senderManager.markWalletProcessed(executor) const opsToDrop = rejectedUserOperations ?? @@ -992,6 +942,12 @@ export class ExecutorManager { this.dropUserOps(opsToDrop) } + removeSubmitted(userOperations: UserOperationInfo[]) { + userOperations.map((userOperation) => { + this.mempool.removeSubmitted(userOperation.hash) + }) + } + dropUserOps(rejectedUserOperations: RejectedUserOperation[]) { rejectedUserOperations.map((rejectedUserOperation) => { const { userOperation, reason } = rejectedUserOperation diff --git a/src/executor/filterOpsAndEStimateGas.ts b/src/executor/filterOpsAndEStimateGas.ts index 98e3e661..0b1123e9 100644 --- a/src/executor/filterOpsAndEStimateGas.ts +++ b/src/executor/filterOpsAndEStimateGas.ts @@ -44,11 +44,11 @@ export type FilterOpsAndEstimateGasResult = gasLimit: bigint } | { - status: "unexpectedFailure" + status: "unexpected_failure" reason: string } | { - status: "allOpsFailedSimulation" + status: "all_ops_failed_simulation" failedOps: FailedOpWithReason[] } @@ -149,7 +149,7 @@ export async function filterOpsAndEstimateGas({ "failed to parse failedOpError" ) return { - status: "unexpectedFailure", + status: "unexpected_failure", reason: "failed to parse failedOpError" } } @@ -244,7 +244,7 @@ export async function filterOpsAndEstimateGas({ "unexpected error result" ) return { - status: "unexpectedFailure", + status: "unexpected_failure", reason: "unexpected error result" } } @@ -265,7 +265,7 @@ export async function filterOpsAndEstimateGas({ "failed to parse error result" ) return { - status: "unexpectedFailure", + status: "unexpected_failure", reason: "failed to parse error result" } } @@ -276,12 +276,12 @@ export async function filterOpsAndEstimateGas({ "error estimating gas" ) return { - status: "unexpectedFailure", + status: "unexpected_failure", reason: "error estimating gas" } } } } - return { status: "allOpsFailedSimulation", failedOps } + return { status: "all_ops_failed_simulation", failedOps } } diff --git a/src/mempool/mempool.ts b/src/mempool/mempool.ts index 1447dbcd..857c7c2d 100644 --- a/src/mempool/mempool.ts +++ b/src/mempool/mempool.ts @@ -657,6 +657,26 @@ export class MemoryMempool { } } + public async getBundles( + maxBundleCount?: number + ): Promise { + const bundlePromises = this.config.entrypoints.map( + async (entryPoint) => { + return await this.process({ + entryPoint, + maxGasLimit: this.config.maxGasPerBundle, + minOpsPerBundle: 1, + maxBundleCount + }) + } + ) + + const bundlesNested = await Promise.all(bundlePromises) + const bundles = bundlesNested.flat() + + return bundles + } + // Returns a bundle of userOperations in array format. async process({ maxGasLimit, diff --git a/src/types/mempool.ts b/src/types/mempool.ts index 1085357f..946b5529 100644 --- a/src/types/mempool.ts +++ b/src/types/mempool.ts @@ -60,7 +60,7 @@ export type BundleResult = // Successfully sent bundle. status: "bundle_success" userOpsBundled: UserOperationInfo[] - rejectedUserOperations: RejectedUserOperation[] + rejectedUserOps: RejectedUserOperation[] transactionHash: HexData32 transactionRequest: { gas: bigint @@ -73,7 +73,6 @@ export type BundleResult = // Encountered unhandled error during bundle simulation. status: "unhandled_simulation_failure" reason: string - userOps: UserOperationInfo[] } | { // All user operations failed during simulation. @@ -84,5 +83,6 @@ export type BundleResult = // Encountered error whilst trying to send bundle. status: "bundle_submission_failure" reason: BaseError | "INTERNAL FAILURE" - userOps: UserOperationInfo[] + userOpsToBundle: UserOperationInfo[] + rejectedUserOps: RejectedUserOperation[] } From 4cc2503b12e030da61800d1beb5fe20a90672101 Mon Sep 17 00:00:00 2001 From: mouseless <97399882+mouseless-eth@users.noreply.github.com> Date: Tue, 28 Jan 2025 12:33:12 +0000 Subject: [PATCH 24/34] fix --- src/executor/executor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/executor/executor.ts b/src/executor/executor.ts index cf996459..533114cb 100644 --- a/src/executor/executor.ts +++ b/src/executor/executor.ts @@ -423,7 +423,7 @@ export class Executor { const bundleResult: BundleResult = { status: "bundle_success", userOpsBundled: opsToBundle, - rejectedUserOperations: failedOps, + rejectedUserOps: failedOps, transactionHash, transactionRequest: { gas: gasLimit, From 193081f25e109a8e477c4debe4332a89cf7ff151 Mon Sep 17 00:00:00 2001 From: mouseless <97399882+mouseless-eth@users.noreply.github.com> Date: Tue, 28 Jan 2025 12:45:32 +0000 Subject: [PATCH 25/34] cleanup innerHandleOpFloor calculations --- src/executor/executor.ts | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/executor/executor.ts b/src/executor/executor.ts index 533114cb..2ecf9f09 100644 --- a/src/executor/executor.ts +++ b/src/executor/executor.ts @@ -15,6 +15,7 @@ import { } from "@alto/types" import type { Logger, Metrics } from "@alto/utils" import { + isVersion07, maxBigInt, parseViemError, scaleBigIntByPercent, @@ -326,15 +327,24 @@ export class Executor { entryPoint }) - // https://github.com/eth-infinitism/account-abstraction/blob/fa61290d37d079e928d92d53a122efcc63822214/contracts/core/EntryPoint.sol#L236 - let innerHandleOpFloor = 0n + // Ensure that we don't submit with gas too low leading to AA95. + // V6 source: https://github.com/eth-infinitism/account-abstraction/blob/fa61290d37d079e928d92d53a122efcc63822214/contracts/core/EntryPoint.sol#L236 + // V7 source: https://github.com/eth-infinitism/account-abstraction/blob/releases/v0.7/contracts/core/EntryPoint.sol + let gasFloor = 0n for (const op of opsToBundle) { - innerHandleOpFloor += - op.callGasLimit + op.verificationGasLimit + 5000n + if (isVersion07(op)) { + const totalGas = + op.callGasLimit + + (op.paymasterPostOpGasLimit || 0n) + + 10_000n + gasFloor += (totalGas * 64n) / 63n + } else { + gasFloor += op.callGasLimit + op.verificationGasLimit + 5000n + } } - if (gasLimit < innerHandleOpFloor) { - gasLimit += innerHandleOpFloor + if (gasLimit < gasFloor) { + gasLimit += gasFloor } // sometimes the estimation rounds down, adding a fixed constant accounts for this From 012a320dcbc13eb236bd394eecf053fb71f1f04d Mon Sep 17 00:00:00 2001 From: mouseless <97399882+mouseless-eth@users.noreply.github.com> Date: Tue, 28 Jan 2025 13:14:13 +0000 Subject: [PATCH 26/34] fix comment --- src/executor/executorManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/executor/executorManager.ts b/src/executor/executorManager.ts index 38b418d6..709707c8 100644 --- a/src/executor/executorManager.ts +++ b/src/executor/executorManager.ts @@ -266,8 +266,8 @@ export class ExecutorManager { // Encountered unhandled error during bundle simulation. if (bundleResult.status === "bundle_submission_failure") { const { rejectedUserOps, userOpsToBundle, reason } = bundleResult - // NOTE: these ops passed validation but dropped due to error during bundling this.dropUserOps(rejectedUserOps) + // NOTE: these ops passed validation, so we can try resubmitting them this.resubmitUserOperations( userOpsToBundle, entryPoint, From d29434ce8820f50ae027fce15fceb82089574702 Mon Sep 17 00:00:00 2001 From: mouseless <97399882+mouseless-eth@users.noreply.github.com> Date: Tue, 28 Jan 2025 17:49:48 +0000 Subject: [PATCH 27/34] remove gas floor check --- src/executor/executor.ts | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/src/executor/executor.ts b/src/executor/executor.ts index 2ecf9f09..15fc7612 100644 --- a/src/executor/executor.ts +++ b/src/executor/executor.ts @@ -15,7 +15,6 @@ import { } from "@alto/types" import type { Logger, Metrics } from "@alto/utils" import { - isVersion07, maxBigInt, parseViemError, scaleBigIntByPercent, @@ -327,26 +326,6 @@ export class Executor { entryPoint }) - // Ensure that we don't submit with gas too low leading to AA95. - // V6 source: https://github.com/eth-infinitism/account-abstraction/blob/fa61290d37d079e928d92d53a122efcc63822214/contracts/core/EntryPoint.sol#L236 - // V7 source: https://github.com/eth-infinitism/account-abstraction/blob/releases/v0.7/contracts/core/EntryPoint.sol - let gasFloor = 0n - for (const op of opsToBundle) { - if (isVersion07(op)) { - const totalGas = - op.callGasLimit + - (op.paymasterPostOpGasLimit || 0n) + - 10_000n - gasFloor += (totalGas * 64n) / 63n - } else { - gasFloor += op.callGasLimit + op.verificationGasLimit + 5000n - } - } - - if (gasLimit < gasFloor) { - gasLimit += gasFloor - } - // sometimes the estimation rounds down, adding a fixed constant accounts for this gasLimit += 10_000n gasLimit = gasLimitSuggestion From caa00a367f0b3e73c05d112d591dc6c8eea7d963 Mon Sep 17 00:00:00 2001 From: mouseless <97399882+mouseless-eth@users.noreply.github.com> Date: Wed, 29 Jan 2025 11:39:56 +0000 Subject: [PATCH 28/34] fix --- src/executor/filterOpsAndEStimateGas.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/executor/filterOpsAndEStimateGas.ts b/src/executor/filterOpsAndEStimateGas.ts index 0b1123e9..6d4c0970 100644 --- a/src/executor/filterOpsAndEStimateGas.ts +++ b/src/executor/filterOpsAndEStimateGas.ts @@ -98,7 +98,14 @@ export async function filterOpsAndEstimateGas({ let gasLimit: bigint let retriesLeft = 5 - while (opsToBundle.length > 0 && retriesLeft > 0) { + while (opsToBundle.length > 0) { + if (retriesLeft === 0) { + return { + status: "unexpected_failure", + reason: "max retries reached" + } + } + try { const encodedOps = opsToBundle.map((userOperation) => { return isUserOpV06 From fe3a37c966103934ee01be7be1414808d5acf4b9 Mon Sep 17 00:00:00 2001 From: mouseless <97399882+mouseless-eth@users.noreply.github.com> Date: Wed, 29 Jan 2025 14:59:20 +0000 Subject: [PATCH 29/34] keep gas floor check --- src/executor/executor.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/executor/executor.ts b/src/executor/executor.ts index 15fc7612..5c6a0372 100644 --- a/src/executor/executor.ts +++ b/src/executor/executor.ts @@ -326,6 +326,26 @@ export class Executor { entryPoint }) + // Ensure that we don't submit with gas too low leading to AA95. + // V6 source: https://github.com/eth-infinitism/account-abstraction/blob/fa61290d37d079e928d92d53a122efcc63822214/contracts/core/EntryPoint.sol#L236 + // V7 source: https://github.com/eth-infinitism/account-abstraction/blob/releases/v0.7/contracts/core/EntryPoint.sol + let gasFloor = 0n + for (const op of opsToBundle) { + if (isVersion07(op)) { + const totalGas = + op.callGasLimit + + (op.paymasterPostOpGasLimit || 0n) + + 10_000n + gasFloor += (totalGas * 64n) / 63n + } else { + gasFloor += op.callGasLimit + op.verificationGasLimit + 5000n + } + } + + if (gasLimit < gasFloor) { + gasLimit += gasFloor + } + // sometimes the estimation rounds down, adding a fixed constant accounts for this gasLimit += 10_000n gasLimit = gasLimitSuggestion From 008a5a0a8ad7e4e39467eaa60963939c87ede845 Mon Sep 17 00:00:00 2001 From: mouseless <97399882+mouseless-eth@users.noreply.github.com> Date: Wed, 29 Jan 2025 15:00:59 +0000 Subject: [PATCH 30/34] fix --- src/executor/executor.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/executor/executor.ts b/src/executor/executor.ts index 5c6a0372..2ecf9f09 100644 --- a/src/executor/executor.ts +++ b/src/executor/executor.ts @@ -15,6 +15,7 @@ import { } from "@alto/types" import type { Logger, Metrics } from "@alto/utils" import { + isVersion07, maxBigInt, parseViemError, scaleBigIntByPercent, From a5fc67b0f03112770a00b3bc6d4ba34ac490d98b Mon Sep 17 00:00:00 2001 From: mouseless <97399882+mouseless-eth@users.noreply.github.com> Date: Thu, 30 Jan 2025 02:23:08 +0000 Subject: [PATCH 31/34] restore userOp with hash type --- src/executor/executor.ts | 253 +++++++--------- src/executor/executorManager.ts | 374 +++++++++++++----------- src/executor/filterOpsAndEStimateGas.ts | 197 +++++++------ src/executor/types.ts | 28 -- src/executor/utils.ts | 88 +++++- src/mempool/mempool.ts | 371 +++++++++++------------ src/mempool/store.ts | 42 ++- src/rpc/nonceQueuer.ts | 4 +- src/rpc/rpcHandler.ts | 25 +- src/types/mempool.ts | 37 ++- src/utils/userop.ts | 28 +- 11 files changed, 748 insertions(+), 699 deletions(-) delete mode 100644 src/executor/types.ts diff --git a/src/executor/executor.ts b/src/executor/executor.ts index 2ecf9f09..59ddd41f 100644 --- a/src/executor/executor.ts +++ b/src/executor/executor.ts @@ -3,77 +3,74 @@ import type { InterfaceReputationManager, MemoryMempool } from "@alto/mempool" import { type Address, type BundleResult, - EntryPointV06Abi, - EntryPointV07Abi, type HexData32, - type PackedUserOperation, - type TransactionInfo, type UserOperation, - type UserOperationV07, type GasPriceParameters, - UserOperationBundle + UserOperationBundle, + UserOpInfo } from "@alto/types" import type { Logger, Metrics } from "@alto/utils" -import { - isVersion07, - maxBigInt, - parseViemError, - scaleBigIntByPercent, - toPackedUserOperation -} from "@alto/utils" +import { maxBigInt, parseViemError, scaleBigIntByPercent } from "@alto/utils" import * as sentry from "@sentry/node" -import { Mutex } from "async-mutex" import { IntrinsicGasTooLowError, NonceTooLowError, TransactionExecutionError, - encodeFunctionData, - getContract, type Account, type Hex, NonceTooHighError, BaseError } from "viem" -import { getAuthorizationList, isTransactionUnderpricedError } from "./utils" +import { + calculateAA95GasFloor, + encodeHandleOpsCalldata, + getAuthorizationList, + getUserOpHashes, + isTransactionUnderpricedError +} from "./utils" import type { SendTransactionErrorType } from "viem" import type { AltoConfig } from "../createConfig" -import type { SendTransactionOptions } from "./types" import { sendPflConditional } from "./fastlane" import { filterOpsAndEstimateGas } from "./filterOpsAndEStimateGas" +import { SignedAuthorizationList } from "viem/experimental" -export interface GasEstimateResult { - preverificationGas: bigint - verificationGasLimit: bigint - callGasLimit: bigint -} - -export type HandleOpsTxParam = { - ops: UserOperation[] +type HandleOpsTxParams = { + gas: bigint + account: Account + nonce: number + userOps: UserOpInfo[] isUserOpV06: boolean isReplacementTx: boolean entryPoint: Address } -export type ReplaceTransactionResult = +type HandleOpsGasParams = | { - status: "replaced" - transactionInfo: TransactionInfo + type: "legacy" + gasPrice: bigint + maxFeePerGas?: undefined + maxPriorityFeePerGas?: undefined } | { - status: "potentially_already_included" + type: "eip1559" + maxFeePerGas: bigint + maxPriorityFeePerGas: bigint + gasPrice?: undefined } | { - status: "failed" + type: "eip7702" + maxFeePerGas: bigint + maxPriorityFeePerGas: bigint + gasPrice?: undefined + authorizationList: SignedAuthorizationList } export class Executor { - // private unWatch: WatchBlocksReturnType | undefined config: AltoConfig logger: Logger metrics: Metrics reputationManager: InterfaceReputationManager gasPriceManager: GasPriceManager - mutex: Mutex mempool: MemoryMempool eventManager: EventManager @@ -104,8 +101,6 @@ export class Executor { this.metrics = metrics this.gasPriceManager = gasPriceManager this.eventManager = eventManager - - this.mutex = new Mutex() } cancelOps(_entryPoint: Address, _ops: UserOperation[]): Promise { @@ -114,47 +109,21 @@ export class Executor { async sendHandleOpsTransaction({ txParam, - opts + gasOpts }: { - txParam: HandleOpsTxParam - opts: - | { - gasPrice: bigint - maxFeePerGas?: undefined - maxPriorityFeePerGas?: undefined - account: Account - gas: bigint - nonce: number - } - | { - maxFeePerGas: bigint - maxPriorityFeePerGas: bigint - gasPrice?: undefined - account: Account - gas: bigint - nonce: number - } + txParam: HandleOpsTxParams + gasOpts: HandleOpsGasParams }) { - const { isUserOpV06, ops, entryPoint } = txParam + const { isUserOpV06, entryPoint, userOps } = txParam - const packedOps = ops.map((op) => { - if (isUserOpV06) { - return op - } - return toPackedUserOperation(op as UserOperationV07) - }) as PackedUserOperation[] - - const data = encodeFunctionData({ - abi: isUserOpV06 ? EntryPointV06Abi : EntryPointV07Abi, - functionName: "handleOps", - args: [packedOps, opts.account.address] - }) + const handleOpsCalldata = encodeHandleOpsCalldata(userOps, entryPoint) const request = await this.config.walletClient.prepareTransactionRequest({ to: entryPoint, - data, - ...opts + data: handleOpsCalldata, + ...txParam, + ...gasOpts }) request.gas = scaleBigIntByPercent( @@ -257,56 +226,47 @@ export class Executor { } async bundle({ - wallet, - bundle, + executor, + userOpBundle, nonce, - gasPriceParameters, + gasPriceParams, gasLimitSuggestion, isReplacementTx }: { - wallet: Account - bundle: UserOperationBundle + executor: Account + userOpBundle: UserOperationBundle nonce: number - gasPriceParameters: GasPriceParameters + gasPriceParams: GasPriceParameters gasLimitSuggestion?: bigint isReplacementTx: boolean }): Promise { - const { entryPoint, userOperations, version } = bundle - + const { entryPoint, userOps, version } = userOpBundle + const { maxFeePerGas, maxPriorityFeePerGas } = gasPriceParams const isUserOpV06 = version === "0.6" - const ep = getContract({ - abi: isUserOpV06 ? EntryPointV06Abi : EntryPointV07Abi, - address: entryPoint, - client: { - public: this.config.publicClient, - wallet: this.config.walletClient - } - }) let childLogger = this.logger.child({ - userOperations: userOperations.map((op) => op.hash), + userOperations: getUserOpHashes(userOps), entryPoint }) let estimateResult = await filterOpsAndEstimateGas({ - isUserOpV06, - ops: userOperations, - ep, - wallet, + userOpBundle, + executor, nonce, - maxFeePerGas: gasPriceParameters.maxFeePerGas, - maxPriorityFeePerGas: gasPriceParameters.maxPriorityFeePerGas, + maxFeePerGas, + maxPriorityFeePerGas, reputationManager: this.reputationManager, config: this.config, logger: childLogger }) - if (estimateResult.status === "unexpected_failure") { + if (estimateResult.status === "unhandled_failure") { childLogger.error( "gas limit simulation encountered unexpected failure" ) return { status: "unhandled_simulation_failure", + rejectedUserOps: estimateResult.rejectedUserOps, reason: "INTERNAL FAILURE" } } @@ -315,36 +275,23 @@ export class Executor { childLogger.warn("all ops failed simulation") return { status: "all_ops_failed_simulation", - rejectedUserOps: estimateResult.failedOps + rejectedUserOps: estimateResult.rejectedUserOps } } - let { gasLimit, opsToBundle, failedOps } = estimateResult + let { gasLimit, userOpsToBundle, rejectedUserOps } = estimateResult // Update child logger with userOperations being sent for bundling. childLogger = this.logger.child({ - userOperations: opsToBundle.map((op) => op.hash), + userOperations: getUserOpHashes(userOpsToBundle), entryPoint }) // Ensure that we don't submit with gas too low leading to AA95. - // V6 source: https://github.com/eth-infinitism/account-abstraction/blob/fa61290d37d079e928d92d53a122efcc63822214/contracts/core/EntryPoint.sol#L236 - // V7 source: https://github.com/eth-infinitism/account-abstraction/blob/releases/v0.7/contracts/core/EntryPoint.sol - let gasFloor = 0n - for (const op of opsToBundle) { - if (isVersion07(op)) { - const totalGas = - op.callGasLimit + - (op.paymasterPostOpGasLimit || 0n) + - 10_000n - gasFloor += (totalGas * 64n) / 63n - } else { - gasFloor += op.callGasLimit + op.verificationGasLimit + 5000n - } - } + const aa95GasFloor = calculateAA95GasFloor(userOpsToBundle) - if (gasLimit < gasFloor) { - gasLimit += gasFloor + if (gasLimit < aa95GasFloor) { + gasLimit += aa95GasFloor } // sometimes the estimation rounds down, adding a fixed constant accounts for this @@ -356,89 +303,85 @@ export class Executor { let transactionHash: HexData32 try { const isLegacyTransaction = this.config.legacyTransactions - const authorizationList = getAuthorizationList(opsToBundle) + const authorizationList = getAuthorizationList(userOpsToBundle) + const { maxFeePerGas, maxPriorityFeePerGas } = gasPriceParams - let opts: SendTransactionOptions + let gasOpts: HandleOpsGasParams if (isLegacyTransaction) { - opts = { + gasOpts = { type: "legacy", - gasPrice: gasPriceParameters.maxFeePerGas, - account: wallet, - gas: gasLimit, - nonce + gasPrice: maxFeePerGas } } else if (authorizationList) { - opts = { + gasOpts = { type: "eip7702", - maxFeePerGas: gasPriceParameters.maxFeePerGas, - maxPriorityFeePerGas: - gasPriceParameters.maxPriorityFeePerGas, - account: wallet, - gas: gasLimit, - nonce, + maxFeePerGas, + maxPriorityFeePerGas, authorizationList } } else { - opts = { + gasOpts = { type: "eip1559", - maxFeePerGas: gasPriceParameters.maxFeePerGas, - maxPriorityFeePerGas: - gasPriceParameters.maxPriorityFeePerGas, - account: wallet, - gas: gasLimit, - nonce + maxFeePerGas, + maxPriorityFeePerGas } } transactionHash = await this.sendHandleOpsTransaction({ txParam: { - ops: opsToBundle, + account: executor, + nonce, + gas: gasLimit, + userOps: userOpsToBundle, isReplacementTx, isUserOpV06, entryPoint }, - opts + gasOpts }) this.eventManager.emitSubmitted({ - userOpHashes: opsToBundle.map((op) => op.hash), + userOpHashes: getUserOpHashes(userOpsToBundle), transactionHash }) } catch (err: unknown) { const e = parseViemError(err) - - const { failedOps, opsToBundle } = estimateResult - if (e) { + const { rejectedUserOps, userOpsToBundle } = estimateResult + + // if unknown error, return INTERNAL FAILURE + if (!e) { + sentry.captureException(err) + childLogger.error( + { error: JSON.stringify(err) }, + "error submitting bundle transaction" + ) return { - rejectedUserOps: failedOps, - userOpsToBundle: opsToBundle, + rejectedUserOps, + userOpsToBundle, status: "bundle_submission_failure", - reason: e + reason: "INTERNAL FAILURE" } } - sentry.captureException(err) - childLogger.error( - { error: JSON.stringify(err) }, - "error submitting bundle transaction" - ) return { - rejectedUserOps: failedOps, - userOpsToBundle: opsToBundle, + rejectedUserOps, + userOpsToBundle, status: "bundle_submission_failure", - reason: "INTERNAL FAILURE" + reason: e } } + const userOpsBundled = userOpsToBundle + const bundleResult: BundleResult = { status: "bundle_success", - userOpsBundled: opsToBundle, - rejectedUserOps: failedOps, + userOpsBundled, + rejectedUserOps, transactionHash, transactionRequest: { gas: gasLimit, - maxFeePerGas: gasPriceParameters.maxFeePerGas, - maxPriorityFeePerGas: gasPriceParameters.maxPriorityFeePerGas, + maxFeePerGas: gasPriceParams.maxFeePerGas, + maxPriorityFeePerGas: gasPriceParams.maxPriorityFeePerGas, nonce } } @@ -447,7 +390,7 @@ export class Executor { { transactionRequest: bundleResult.transactionRequest, txHash: transactionHash, - opHashes: opsToBundle.map((op) => op.hash) + opHashes: getUserOpHashes(userOpsBundled) }, "submitted bundle transaction" ) diff --git a/src/executor/executorManager.ts b/src/executor/executorManager.ts index 709707c8..3c2ccc2b 100644 --- a/src/executor/executorManager.ts +++ b/src/executor/executorManager.ts @@ -8,11 +8,11 @@ import { type BundlingMode, EntryPointV06Abi, type HexData32, - type SubmittedUserOperation, + type SubmittedUserOp, type TransactionInfo, - RejectedUserOperation, + RejectedUserOp, UserOperationBundle, - UserOperationInfo + UserOpInfo } from "@alto/types" import type { BundlingStatus, Logger, Metrics } from "@alto/utils" import { @@ -39,15 +39,14 @@ import { SenderManager } from "./senderManager" import { BaseError } from "abitype" function getTransactionsFromUserOperationEntries( - entries: SubmittedUserOperation[] + submittedOps: SubmittedUserOp[] ): TransactionInfo[] { - return Array.from( - new Set( - entries.map((entry) => { - return entry.transactionInfo - }) - ) + const transactionInfos = submittedOps.map( + (userOpInfo) => userOpInfo.transactionInfo ) + + // Remove duplicates + return Array.from(new Set(transactionInfos)) } const MIN_INTERVAL = 100 // 0.1 seconds (100ms) @@ -139,7 +138,7 @@ export class ExecutorManager { if (bundles.length > 0) { const opsCount: number = bundles - .map(({ userOperations }) => userOperations.length) + .map(({ userOps }) => userOps.length) .reduce((a, b) => a + b) // Add timestamps for each task @@ -168,9 +167,10 @@ export class ExecutorManager { // Debug endpoint async sendBundleNow(): Promise { - const bundle = (await this.mempool.getBundles(1))[0] + const bundles = await this.mempool.getBundles(1) + const bundle = bundles[0] - if (bundle.userOperations.length === 0) { + if (bundle.userOps.length === 0) { throw new Error("no ops to bundle") } @@ -184,16 +184,16 @@ export class ExecutorManager { } async sendBundleToExecutor( - bundle: UserOperationBundle + userOpBundle: UserOperationBundle ): Promise { - const { entryPoint, userOperations, version } = bundle - if (userOperations.length === 0) { + const { entryPoint, userOps, version } = userOpBundle + if (userOps.length === 0) { return undefined } const wallet = await this.senderManager.getWallet() - const [gasPriceParameters, nonce] = await Promise.all([ + const [gasPriceParams, nonce] = await Promise.all([ this.gasPriceManager.tryGetNetworkGasPrice(), this.config.publicClient.getTransactionCount({ address: wallet.address, @@ -203,10 +203,10 @@ export class ExecutorManager { return [] }) - if (!gasPriceParameters || nonce === undefined) { + if (!gasPriceParams || nonce === undefined) { this.resubmitUserOperations( - bundle.userOperations, - bundle.entryPoint, + userOps, + entryPoint, "Failed to get nonce and gas parameters for bundling" ) // Free executor if failed to get initial params. @@ -215,10 +215,10 @@ export class ExecutorManager { } const bundleResult = await this.executor.bundle({ - wallet, - bundle, + executor: wallet, + userOpBundle, nonce, - gasPriceParameters, + gasPriceParams, isReplacementTx: false }) @@ -236,12 +236,7 @@ export class ExecutorManager { // Unhandled error during simulation, drop all ops. if (bundleResult.status === "unhandled_simulation_failure") { - const { reason } = bundleResult - const { userOperations } = bundle - const rejectedUserOps = userOperations.map((op) => ({ - userOperation: op, - reason - })) + const { rejectedUserOps } = bundleResult this.dropUserOps(rejectedUserOps) this.metrics.bundlesSubmitted.labels({ status: "failed" }).inc() return undefined @@ -280,7 +275,7 @@ export class ExecutorManager { if (bundleResult.status === "bundle_success") { const { userOpsBundled, - rejectedUserOps: rejectedUserOperations, + rejectedUserOps, transactionRequest, transactionHash } = bundleResult @@ -292,7 +287,7 @@ export class ExecutorManager { bundle: { entryPoint, version, - userOperations: userOpsBundled + userOps: userOpsBundled }, previousTransactionHashes: [], lastReplaced: Date.now(), @@ -301,7 +296,7 @@ export class ExecutorManager { } this.markUserOperationsAsSubmitted(userOpsBundled, transactionInfo) - this.dropUserOps(rejectedUserOperations) + this.dropUserOps(rejectedUserOps) this.metrics.bundlesSubmitted.labels({ status: "success" }).inc() return transactionHash @@ -338,28 +333,22 @@ export class ExecutorManager { // update the current status of the bundling transaction/s private async refreshTransactionStatus(transactionInfo: TransactionInfo) { const { - transactionHash: currentTransactionHash, + transactionHash: currentTxhash, bundle, previousTransactionHashes } = transactionInfo - const { userOperations, version, entryPoint } = bundle - const isVersion06 = version === "0.6" - const txHashesToCheck = [ - currentTransactionHash, - ...previousTransactionHashes - ] + const { userOps, entryPoint } = bundle + const txHashesToCheck = [currentTxhash, ...previousTransactionHashes] const transactionDetails = await Promise.all( txHashesToCheck.map(async (transactionHash) => ({ transactionHash, - ...(await getBundleStatus( - isVersion06, - transactionHash, - this.config.publicClient, - this.logger, - entryPoint - )) + ...(await getBundleStatus({ + transactionInfo, + publicClient: this.config.publicClient, + logger: this.logger + })) })) ) @@ -402,73 +391,37 @@ export class ExecutorManager { } if (bundlingStatus.status === "included") { - this.metrics.userOperationsOnChain - .labels({ status: bundlingStatus.status }) - .inc(userOperations.length) - - const firstSubmitted = transactionInfo.firstSubmitted const { userOperationDetails } = bundlingStatus - userOperations.map((userOperation) => { - const userOpHash = userOperation.hash - const opDetails = userOperationDetails[userOpHash] - - this.metrics.userOperationInclusionDuration.observe( - (Date.now() - firstSubmitted) / 1000 - ) - this.mempool.removeSubmitted(userOpHash) - this.reputationManager.updateUserOperationIncludedStatus( - userOperation, - entryPoint, - opDetails.accountDeployed - ) - if (opDetails.status === "succesful") { - this.eventManager.emitIncludedOnChain( - userOpHash, - transactionHash, - blockNumber as bigint - ) - } else { - this.eventManager.emitExecutionRevertedOnChain( - userOpHash, - transactionHash, - opDetails.revertReason || "0x", - blockNumber as bigint - ) - } - this.monitor.setUserOperationStatus(userOpHash, { - status: "included", - transactionHash - }) - this.logger.info( - { - opHash: userOpHash, - transactionHash - }, - "user op included" - ) - }) + this.markUserOpsIncluded( + userOps, + entryPoint, + blockNumber, + transactionHash, + userOperationDetails + ) } if (bundlingStatus.status === "reverted") { await Promise.all( - userOperations.map((userOperation) => { + userOps.map((userOpInfo) => { + const { userOpHash } = userOpInfo this.checkFrontrun({ - userOperationHash: userOperation.hash, + userOpHash: userOpHash, transactionHash, blockNumber }) }) ) - this.removeSubmitted(userOperations) + this.removeSubmitted(userOps) } } checkFrontrun({ - userOperationHash, + userOpHash, transactionHash, blockNumber }: { - userOperationHash: HexData32 + userOpHash: HexData32 transactionHash: Hash blockNumber: bigint }) { @@ -476,7 +429,7 @@ export class ExecutorManager { onBlockNumber: async (currentBlockNumber) => { if (currentBlockNumber > blockNumber + 1n) { const userOperationReceipt = - await this.getUserOperationReceipt(userOperationHash) + await this.getUserOperationReceipt(userOpHash) if (userOperationReceipt) { const transactionHash = @@ -484,20 +437,21 @@ export class ExecutorManager { const blockNumber = userOperationReceipt.receipt.blockNumber - this.monitor.setUserOperationStatus(userOperationHash, { + this.mempool.removeSubmitted(userOpHash) + this.monitor.setUserOperationStatus(userOpHash, { status: "included", transactionHash }) this.eventManager.emitFrontranOnChain( - userOperationHash, + userOpHash, transactionHash, blockNumber ) this.logger.info( { - userOpHash: userOperationHash, + userOpHash: userOpHash, transactionHash }, "user op frontrun onchain" @@ -507,18 +461,18 @@ export class ExecutorManager { .labels({ status: "frontran" }) .inc(1) } else { - this.monitor.setUserOperationStatus(userOperationHash, { + this.monitor.setUserOperationStatus(userOpHash, { status: "failed", transactionHash }) this.eventManager.emitFailedOnChain( - userOperationHash, + userOpHash, transactionHash, blockNumber ) this.logger.info( { - userOpHash: userOperationHash, + userOpHash: userOpHash, transactionHash }, "user op failed onchain" @@ -695,12 +649,15 @@ export class ExecutorManager { await Promise.all( transactionInfos.map(async (txInfo) => { + const { transactionRequest } = txInfo + const { maxFeePerGas, maxPriorityFeePerGas } = + transactionRequest + const isMaxFeeTooLow = - txInfo.transactionRequest.maxFeePerGas < - gasPriceParameters.maxFeePerGas + maxFeePerGas < gasPriceParameters.maxFeePerGas const isPriorityFeeTooLow = - txInfo.transactionRequest.maxPriorityFeePerGas < + maxPriorityFeePerGas < gasPriceParameters.maxPriorityFeePerGas const isStuck = @@ -726,6 +683,10 @@ export class ExecutorManager { txInfo: TransactionInfo, reason: string ): Promise { + // Setup vars + const { bundle, executor, transactionRequest } = txInfo + const oldTxHash = txInfo.transactionHash + const gasPriceParameters = await this.gasPriceManager .tryGetNetworkGasPrice() .catch((_) => { @@ -733,8 +694,15 @@ export class ExecutorManager { }) if (!gasPriceParameters) { + const { bundle } = txInfo + const { userOps } = bundle + const rejectedUserOps = userOps.map((userOpInfo) => ({ + ...userOpInfo, + reason: "Failed to get network gas price during replacement" + })) this.failedToReplaceTransaction({ - txInfo, + rejectedUserOps, + oldTxHash, reason: "Failed to get network gas price during replacement" }) // Free executor if failed to get initial params. @@ -742,15 +710,11 @@ export class ExecutorManager { return } - // Setup vars - const { bundle, executor, transactionRequest } = txInfo - const oldTxHash = txInfo.transactionHash - const bundleResult = await this.executor.bundle({ - wallet: executor, - bundle, + executor: executor, + userOpBundle: bundle, nonce: transactionRequest.nonce, - gasPriceParameters: { + gasPriceParams: { maxFeePerGas: scaleBigIntByPercent( gasPriceParameters.maxFeePerGas, 115n @@ -764,6 +728,25 @@ export class ExecutorManager { isReplacementTx: true }) + // Free wallet and return if potentially included too many times. + if (txInfo.timesPotentiallyIncluded >= 3) { + if (txInfo.timesPotentiallyIncluded >= 3) { + this.removeSubmitted(bundle.userOps) + this.logger.warn( + { oldTxHash }, + "transaction potentially already included too many times, removing" + ) + } + + this.senderManager.markWalletProcessed(txInfo.executor) + return + } + + // Free wallet if no bundle was sent or potentially included. + if (bundleResult.status !== "bundle_success") { + this.senderManager.markWalletProcessed(txInfo.executor) + } + // Log metrics. const replaceStatus = bundleResult && bundleResult.status === "bundle_success" @@ -795,68 +778,54 @@ export class ExecutorManager { "transaction potentially already included" ) txInfo.timesPotentiallyIncluded += 1 - - if (txInfo.timesPotentiallyIncluded >= 3) { - this.removeSubmitted(bundle.userOperations) - this.logger.warn( - { oldTxHash }, - "transaction potentially already included too many times, removing" - ) - this.senderManager.markWalletProcessed(executor) - } - return } - // Free wallet if no bundle was sent. - if (bundleResult.status !== "bundle_success") { - this.senderManager.markWalletProcessed(txInfo.executor) - } - if (bundleResult.status === "unhandled_simulation_failure") { + const { rejectedUserOps, reason } = bundleResult this.failedToReplaceTransaction({ - txInfo, - reason: bundleResult.reason + oldTxHash, + reason, + rejectedUserOps }) - return } if (bundleResult.status === "all_ops_failed_simulation") { this.failedToReplaceTransaction({ - txInfo, + oldTxHash, reason: "all ops failed simulation", - rejectedUserOperations: bundleResult.rejectedUserOps + rejectedUserOps: bundleResult.rejectedUserOps }) - return } if (bundleResult.status === "bundle_submission_failure") { - const reason = - bundleResult.reason instanceof BaseError - ? bundleResult.reason.name - : "INTERNAL FAILURE" + const { reason, rejectedUserOps } = bundleResult + const submissionFailureReason = + reason instanceof BaseError ? reason.name : "INTERNAL FAILURE" this.failedToReplaceTransaction({ - txInfo, - reason + oldTxHash, + rejectedUserOps, + reason: submissionFailureReason }) - return } const { - rejectedUserOps: rejectedUserOperations, + rejectedUserOps, userOpsBundled, transactionRequest: newTransactionRequest, - transactionHash: newTransactionHash + transactionHash: newTxHash } = bundleResult + const userOpsReplaced = userOpsBundled + const newTxInfo: TransactionInfo = { ...txInfo, transactionRequest: newTransactionRequest, - transactionHash: newTransactionHash, + transactionHash: newTxHash, previousTransactionHashes: [ txInfo.transactionHash, ...txInfo.previousTransactionHashes @@ -864,21 +833,21 @@ export class ExecutorManager { lastReplaced: Date.now(), bundle: { ...txInfo.bundle, - userOperations: userOpsBundled + userOps: userOpsReplaced } } - userOpsBundled.map((userOperation) => { - this.mempool.replaceSubmitted(userOperation, newTxInfo) + userOpsReplaced.map((userOp) => { + this.mempool.replaceSubmitted(userOp, newTxInfo) }) // Drop all userOperations that were rejected during simulation. - this.dropUserOps(rejectedUserOperations) + this.dropUserOps(rejectedUserOps) this.logger.info( { - oldTxHash: txInfo.transactionHash, - newTxHash: newTxInfo.transactionHash, + oldTxHash, + newTxHash, reason }, "replaced transaction" @@ -888,12 +857,12 @@ export class ExecutorManager { } markUserOperationsAsSubmitted( - userOperations: UserOperationInfo[], + userOpInfos: UserOpInfo[], transactionInfo: TransactionInfo ) { - userOperations.map((op) => { - const opHash = op.hash - this.mempool.markSubmitted(opHash, transactionInfo) + userOpInfos.map((userOpInfo) => { + const { userOpHash } = userOpInfo + this.mempool.markSubmitted(userOpHash, transactionInfo) this.startWatchingBlocks(this.handleBlock.bind(this)) this.metrics.userOperationsSubmitted .labels({ status: "success" }) @@ -902,12 +871,12 @@ export class ExecutorManager { } resubmitUserOperations( - userOperations: UserOperationInfo[], + mempoolUserOps: UserOpInfo[], entryPoint: Address, reason: string ) { - userOperations.map((op) => { - const userOpHash = op.hash + mempoolUserOps.map((mempoolUserOp) => { + const { userOpHash, userOp } = mempoolUserOp this.logger.info( { userOpHash, @@ -916,42 +885,91 @@ export class ExecutorManager { "resubmitting user operation" ) this.mempool.removeProcessing(userOpHash) - this.mempool.add(op, entryPoint) + this.mempool.add(userOp, entryPoint) this.metrics.userOperationsResubmitted.inc() }) } failedToReplaceTransaction({ - txInfo, - rejectedUserOperations, + oldTxHash, + rejectedUserOps, reason }: { - txInfo: TransactionInfo - rejectedUserOperations?: RejectedUserOperation[] + oldTxHash: Hex + rejectedUserOps: RejectedUserOp[] reason: string }) { - const { transactionHash: oldTxHash } = txInfo this.logger.warn({ oldTxHash, reason }, "failed to replace transaction") + this.dropUserOps(rejectedUserOps) + } - const opsToDrop = - rejectedUserOperations ?? - txInfo.bundle.userOperations.map((userOperation) => ({ - userOperation, - reason: "Failed to replace transaction" - })) - this.dropUserOps(opsToDrop) + removeSubmitted(mempoolUserOps: UserOpInfo[]) { + mempoolUserOps.map((mempoolOp) => { + const { userOpHash } = mempoolOp + this.mempool.removeSubmitted(userOpHash) + }) } - removeSubmitted(userOperations: UserOperationInfo[]) { - userOperations.map((userOperation) => { - this.mempool.removeSubmitted(userOperation.hash) + markUserOpsIncluded( + userOps: UserOpInfo[], + entryPoint: Address, + blockNumber: bigint, + transactionHash: Hash, + userOperationDetails: Record + ) { + userOps.map((userOpInfo) => { + this.metrics.userOperationsOnChain + .labels({ status: "included" }) + .inc() + + const { userOpHash, userOp } = userOpInfo + const opDetails = userOperationDetails[userOpHash] + + const firstSubmitted = userOpInfo.addedToMempool + this.metrics.userOperationInclusionDuration.observe( + (Date.now() - firstSubmitted) / 1000 + ) + + this.mempool.removeSubmitted(userOpHash) + this.reputationManager.updateUserOperationIncludedStatus( + userOp, + entryPoint, + opDetails.accountDeployed + ) + + if (opDetails.status === "succesful") { + this.eventManager.emitIncludedOnChain( + userOpHash, + transactionHash, + blockNumber as bigint + ) + } else { + this.eventManager.emitExecutionRevertedOnChain( + userOpHash, + transactionHash, + opDetails.revertReason || "0x", + blockNumber as bigint + ) + } + + this.monitor.setUserOperationStatus(userOpHash, { + status: "included", + transactionHash + }) + + this.logger.info( + { + opHash: userOpHash, + transactionHash + }, + "user op included" + ) }) } - dropUserOps(rejectedUserOperations: RejectedUserOperation[]) { - rejectedUserOperations.map((rejectedUserOperation) => { - const { userOperation, reason } = rejectedUserOperation - const userOpHash = userOperation.hash + dropUserOps(rejectedUserOps: RejectedUserOp[]) { + rejectedUserOps.map((rejectedUserOp) => { + const { userOp, reason, userOpHash } = rejectedUserOp this.mempool.removeProcessing(userOpHash) this.mempool.removeSubmitted(userOpHash) this.eventManager.emitDropped( @@ -965,7 +983,7 @@ export class ExecutorManager { }) this.logger.warn( { - userOperation: JSON.stringify(userOperation, (_k, v) => + userOperation: JSON.stringify(userOp, (_k, v) => typeof v === "bigint" ? v.toString() : v ), userOpHash, diff --git a/src/executor/filterOpsAndEStimateGas.ts b/src/executor/filterOpsAndEStimateGas.ts index 6d4c0970..5fb36d79 100644 --- a/src/executor/filterOpsAndEStimateGas.ts +++ b/src/executor/filterOpsAndEStimateGas.ts @@ -3,8 +3,9 @@ import { EntryPointV06Abi, EntryPointV07Abi, FailedOpWithRevert, - UserOperationInfo, - UserOperationV07, + RejectedUserOp, + UserOpInfo, + UserOperationBundle, failedOpErrorSchema, failedOpWithRevertErrorSchema } from "@alto/types" @@ -13,51 +14,55 @@ import { ContractFunctionRevertedError, EstimateGasExecutionError, FeeCapTooLowError, - GetContractReturnType, Hex, - PublicClient, - WalletClient, - decodeErrorResult + decodeErrorResult, + getContract } from "viem" import { AltoConfig } from "../createConfig" import { Logger, getRevertErrorData, parseViemError, - scaleBigIntByPercent, - toPackedUserOperation + scaleBigIntByPercent } from "@alto/utils" import { z } from "zod" -import { getAuthorizationList } from "./utils" +import { getAuthorizationList, packUserOps } from "./utils" import * as sentry from "@sentry/node" -type FailedOpWithReason = { - userOperation: UserOperationInfo - reason: string -} - export type FilterOpsAndEstimateGasResult = | { status: "success" - opsToBundle: UserOperationInfo[] - failedOps: FailedOpWithReason[] + userOpsToBundle: UserOpInfo[] + rejectedUserOps: RejectedUserOp[] gasLimit: bigint } | { - status: "unexpected_failure" - reason: string + status: "unhandled_failure" + rejectedUserOps: RejectedUserOp[] } | { status: "all_ops_failed_simulation" - failedOps: FailedOpWithReason[] + rejectedUserOps: RejectedUserOp[] } +function rejectUserOp(userOpInfo: UserOpInfo, reason: string): RejectedUserOp { + return { + ...userOpInfo, + reason + } +} + +function rejectUserOps( + userOpInfos: UserOpInfo[], + reason: string +): RejectedUserOp[] { + return userOpInfos.map((userOpInfo) => rejectUserOp(userOpInfo, reason)) +} + // Attempt to create a handleOps bundle + estimate bundling tx gas. export async function filterOpsAndEstimateGas({ - ep, - isUserOpV06, - wallet, - ops, + executor, + userOpBundle, nonce, maxFeePerGas, maxPriorityFeePerGas, @@ -65,16 +70,8 @@ export async function filterOpsAndEstimateGas({ config, logger }: { - ep: GetContractReturnType< - typeof EntryPointV06Abi | typeof EntryPointV07Abi, - { - public: PublicClient - wallet: WalletClient - } - > - isUserOpV06: boolean - wallet: Account - ops: UserOperationInfo[] + executor: Account + userOpBundle: UserOperationBundle nonce: number maxFeePerGas: bigint maxPriorityFeePerGas: bigint @@ -82,12 +79,28 @@ export async function filterOpsAndEstimateGas({ config: AltoConfig logger: Logger }): Promise { - let { legacyTransactions, fixedGasLimitForEstimation, blockTagSupport } = - config + const { userOps: userOperations, version, entryPoint } = userOpBundle + let { + fixedGasLimitForEstimation, + legacyTransactions, + blockTagSupport, + publicClient, + walletClient + } = config + + const isUserOpV06 = version === "0.6" + const epContract = getContract({ + abi: isUserOpV06 ? EntryPointV06Abi : EntryPointV07Abi, + address: entryPoint, + client: { + public: publicClient, + wallet: walletClient + } + }) // Keep track of invalid and valid ops - const opsToBundle = [...ops] - const failedOps: FailedOpWithReason[] = [] + const userOpsToBundle = [...userOperations] + const rejectedUserOps: RejectedUserOp[] = [] // Prepare bundling tx params const gasOptions = legacyTransactions @@ -98,28 +111,27 @@ export async function filterOpsAndEstimateGas({ let gasLimit: bigint let retriesLeft = 5 - while (opsToBundle.length > 0) { + while (userOpsToBundle.length > 0) { if (retriesLeft === 0) { + logger.error("max retries reached") return { - status: "unexpected_failure", - reason: "max retries reached" + status: "unhandled_failure", + rejectedUserOps: [ + ...rejectedUserOps, + ...rejectUserOps(userOpsToBundle, "INTERNAL FAILURE") + ] } } try { - const encodedOps = opsToBundle.map((userOperation) => { - return isUserOpV06 - ? userOperation - : toPackedUserOperation(userOperation as UserOperationV07) - }) + const packedUserOps = packUserOps(userOpsToBundle) + const authorizationList = getAuthorizationList(userOpsToBundle) - const authorizationList = getAuthorizationList(opsToBundle) - - gasLimit = await ep.estimateGas.handleOps( + gasLimit = await epContract.estimateGas.handleOps( // @ts-ignore - ep is set correctly for opsToSend, but typescript doesn't know that - [encodedOps, wallet.address], + [packedUserOps, executor.address], { - account: wallet, + account: executor, nonce: nonce, blockTag, ...(fixedGasLimitForEstimation && { @@ -134,8 +146,8 @@ export async function filterOpsAndEstimateGas({ return { status: "success", - opsToBundle, - failedOps, + userOpsToBundle, + rejectedUserOps, gasLimit } } catch (err: unknown) { @@ -156,8 +168,14 @@ export async function filterOpsAndEstimateGas({ "failed to parse failedOpError" ) return { - status: "unexpected_failure", - reason: "failed to parse failedOpError" + status: "unhandled_failure", + rejectedUserOps: [ + ...rejectedUserOps, + ...rejectUserOps( + userOpsToBundle, + "INTERNAL FAILURE" + ) + ] } } @@ -173,24 +191,24 @@ export async function filterOpsAndEstimateGas({ continue } + const failingOpIndex = Number(errorData.opIndex) + const failingUserOp = userOpsToBundle[failingOpIndex] + userOpsToBundle.splice(failingOpIndex, 1) + + reputationManager.crashedHandleOps( + failingUserOp.userOp, + epContract.address, + errorData.reason + ) + const innerError = (errorData as FailedOpWithRevert)?.inner - const reason = innerError + const revertReason = innerError ? `${errorData.reason} - ${innerError}` : errorData.reason - const failingOp = { - userOperation: opsToBundle[Number(errorData.opIndex)], - reason - } - opsToBundle.splice(Number(errorData.opIndex), 1) - - reputationManager.crashedHandleOps( - failingOp.userOperation, - ep.address, - failingOp.reason + rejectedUserOps.push( + rejectUserOp(failingUserOp, revertReason) ) - - failedOps.push(failingOp) } } else if ( e instanceof EstimateGasExecutionError || @@ -251,29 +269,38 @@ export async function filterOpsAndEstimateGas({ "unexpected error result" ) return { - status: "unexpected_failure", - reason: "unexpected error result" + status: "unhandled_failure", + rejectedUserOps: [ + ...rejectedUserOps, + ...rejectUserOps( + userOpsToBundle, + "INTERNAL FAILURE" + ) + ] } } - const failedOpIndex = Number(errorResult.args[0]) - const failingOp = { - userOperation: opsToBundle[failedOpIndex], - reason: errorResult.args[1] - } + const [opIndex, reason] = errorResult.args - failedOps.push(failingOp) - opsToBundle.splice(Number(errorResult.args[0]), 1) + const failedOpIndex = Number(opIndex) + const failingUserOp = userOpsToBundle[failedOpIndex] - continue + rejectedUserOps.push(rejectUserOp(failingUserOp, reason)) + userOpsToBundle.splice(failedOpIndex, 1) } catch (e: unknown) { logger.error( { error: JSON.stringify(err) }, "failed to parse error result" ) return { - status: "unexpected_failure", - reason: "failed to parse error result" + status: "unhandled_failure", + rejectedUserOps: [ + ...rejectedUserOps, + ...rejectUserOps( + userOpsToBundle, + "INTERNAL FAILURE" + ) + ] } } } else { @@ -283,12 +310,18 @@ export async function filterOpsAndEstimateGas({ "error estimating gas" ) return { - status: "unexpected_failure", - reason: "error estimating gas" + status: "unhandled_failure", + rejectedUserOps: [ + ...rejectedUserOps, + ...rejectUserOps(userOpsToBundle, "INTERNAL FAILURE") + ] } } } } - return { status: "all_ops_failed_simulation", failedOps } + return { + status: "all_ops_failed_simulation", + rejectedUserOps + } } diff --git a/src/executor/types.ts b/src/executor/types.ts deleted file mode 100644 index 4d43cc8d..00000000 --- a/src/executor/types.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Account } from "viem" -import { SignedAuthorizationList } from "viem/experimental" - -export type SendTransactionOptions = - | { - type: "legacy" - gasPrice: bigint - account: Account - gas: bigint - nonce: number - } - | { - type: "eip1559" - maxFeePerGas: bigint - maxPriorityFeePerGas: bigint - account: Account - gas: bigint - nonce: number - } - | { - type: "eip7702" - maxFeePerGas: bigint - maxPriorityFeePerGas: bigint - account: Account - gas: bigint - nonce: number - authorizationList: SignedAuthorizationList - } diff --git a/src/executor/utils.ts b/src/executor/utils.ts index ba003e20..d34a1e9c 100644 --- a/src/executor/utils.ts +++ b/src/executor/utils.ts @@ -1,5 +1,16 @@ -import { type UserOperation } from "@alto/types" -import type { Logger } from "@alto/utils" +import { + EntryPointV06Abi, + EntryPointV07Abi, + PackedUserOperation, + UserOpInfo, + UserOperationV07 +} from "@alto/types" +import { + isVersion06, + toPackedUserOperation, + type Logger, + isVersion07 +} from "@alto/utils" // biome-ignore lint/style/noNamespaceImport: explicitly make it clear when sentry is used import * as sentry from "@sentry/node" import { @@ -8,7 +19,10 @@ import { type PublicClient, type Transport, type WalletClient, - BaseError + BaseError, + encodeFunctionData, + Address, + Hex } from "viem" import { SignedAuthorizationList } from "viem/experimental" @@ -18,19 +32,65 @@ export const isTransactionUnderpricedError = (e: BaseError) => { .includes("replacement transaction underpriced") } +// V7 source: https://github.com/eth-infinitism/account-abstraction/blob/releases/v0.7/contracts/core/EntryPoint.sol +// V6 source: https://github.com/eth-infinitism/account-abstraction/blob/fa61290d37d079e928d92d53a122efcc63822214/contracts/core/EntryPoint.sol#L236 +export function calculateAA95GasFloor(userOps: UserOpInfo[]): bigint { + let gasFloor = 0n + + for (const userOpInfo of userOps) { + const { userOp } = userOpInfo + if (isVersion07(userOp)) { + const totalGas = + userOp.callGasLimit + + (userOp.paymasterPostOpGasLimit || 0n) + + 10_000n + gasFloor += (totalGas * 64n) / 63n + } else { + gasFloor += + userOp.callGasLimit + userOp.verificationGasLimit + 5000n + } + } + + return gasFloor +} + +export const getUserOpHashes = (userOpInfos: UserOpInfo[]) => { + return userOpInfos.map(({ userOpHash }) => userOpHash) +} + +export const packUserOps = (userOpInfos: UserOpInfo[]) => { + const userOps = userOpInfos.map(({ userOp }) => userOp) + const isV06 = isVersion06(userOps[0]) + const packedUserOps = isV06 + ? userOps + : userOps.map((op) => toPackedUserOperation(op as UserOperationV07)) + return packedUserOps as PackedUserOperation[] +} + +export const encodeHandleOpsCalldata = ( + userOpInfos: UserOpInfo[], + beneficiary: Address +): Hex => { + const userOps = userOpInfos.map(({ userOp }) => userOp) + const isV06 = isVersion06(userOps[0]) + const packedUserOps = packUserOps(userOpInfos) + + return encodeFunctionData({ + abi: isV06 ? EntryPointV06Abi : EntryPointV07Abi, + functionName: "handleOps", + args: [packedUserOps, beneficiary] + }) +} + export const getAuthorizationList = ( - userOperations: UserOperation[] + userOpInfos: UserOpInfo[] ): SignedAuthorizationList | undefined => { - const authorizationList = userOperations - .map((op) => { - if (op.eip7702Auth) { - return op.eip7702Auth - } - return undefined - }) - .filter((auth) => auth !== undefined) as SignedAuthorizationList - - return authorizationList.length > 0 ? authorizationList : undefined + const authList = userOpInfos + .map(({ userOp }) => userOp) + .map(({ eip7702Auth }) => eip7702Auth) + .filter(Boolean) as SignedAuthorizationList + + return authList.length ? authList : undefined } export async function flushStuckTransaction( diff --git a/src/mempool/mempool.ts b/src/mempool/mempool.ts index 857c7c2d..f1aa2003 100644 --- a/src/mempool/mempool.ts +++ b/src/mempool/mempool.ts @@ -9,22 +9,23 @@ import { type ReferencedCodeHashes, RpcError, type StorageMap, - type SubmittedUserOperation, + type SubmittedUserOp, type TransactionInfo, type UserOperation, - type UserOperationInfo, ValidationErrors, type ValidationResult, - UserOperationBundle + UserOperationBundle, + UserOpInfo } from "@alto/types" import type { Metrics } from "@alto/utils" import type { Logger } from "@alto/utils" import { getAddressFromInitCodeOrPaymasterAndData, - getNonceKeyAndValue, + getNonceKeyAndSequence, getUserOperationHash, isVersion06, - isVersion07 + isVersion07, + scaleBigIntByPercent } from "@alto/utils" import { type Address, getAddress, getContract } from "viem" import type { Monitor } from "./monitoring" @@ -76,19 +77,21 @@ export class MemoryMempool { } replaceSubmitted( - userOperation: UserOperationInfo, + userOpInfo: UserOpInfo, transactionInfo: TransactionInfo ): void { - const op = this.store + const { userOpHash } = userOpInfo + const existingUserOpToReplace = this.store .dumpSubmitted() - .find((op) => op.userOperation.hash === userOperation.hash) - if (op) { - this.store.removeSubmitted(userOperation.hash) + .find((userOpInfo) => userOpInfo.userOpHash === userOpHash) + + if (existingUserOpToReplace) { + this.store.removeSubmitted(userOpHash) this.store.addSubmitted({ - userOperation, + ...existingUserOpToReplace, transactionInfo }) - this.monitor.setUserOperationStatus(userOperation.hash, { + this.monitor.setUserOperationStatus(userOpHash, { status: "submitted", transactionHash: transactionInfo.transactionHash }) @@ -99,13 +102,14 @@ export class MemoryMempool { userOpHash: `0x${string}`, transactionInfo: TransactionInfo ): void { - const op = this.store + const processingUserOp = this.store .dumpProcessing() - .find((op) => op.hash === userOpHash) - if (op) { + .find((userOpInfo) => userOpInfo.userOpHash === userOpHash) + + if (processingUserOp) { this.store.removeProcessing(userOpHash) this.store.addSubmitted({ - userOperation: op, + ...processingUserOp, transactionInfo }) this.monitor.setUserOperationStatus(userOpHash, { @@ -116,14 +120,16 @@ export class MemoryMempool { } dumpOutstanding(): UserOperation[] { - return this.store.dumpOutstanding() + return this.store + .dumpOutstanding() + .map((userOpInfo) => userOpInfo.userOp) } - dumpProcessing(): UserOperationInfo[] { + dumpProcessing(): UserOpInfo[] { return this.store.dumpProcessing() } - dumpSubmittedOps(): SubmittedUserOperation[] { + dumpSubmittedOps(): SubmittedUserOp[] { return this.store.dumpSubmitted() } @@ -200,22 +206,25 @@ export class MemoryMempool { factories: new Set() } - for (const op of allOps) { - entities.sender.add(op.sender) + for (const userOpInfo of allOps) { + const { userOp } = userOpInfo + entities.sender.add(userOp.sender) - const isUserOpV06 = isVersion06(op) + const isUserOpV06 = isVersion06(userOp) const paymaster = isUserOpV06 - ? getAddressFromInitCodeOrPaymasterAndData(op.paymasterAndData) - : op.paymaster + ? getAddressFromInitCodeOrPaymasterAndData( + userOp.paymasterAndData + ) + : userOp.paymaster if (paymaster) { entities.paymasters.add(paymaster) } const factory = isUserOpV06 - ? getAddressFromInitCodeOrPaymasterAndData(op.initCode) - : op.factory + ? getAddressFromInitCodeOrPaymasterAndData(userOp.initCode) + : userOp.factory if (factory) { entities.factories.add(factory) @@ -228,12 +237,12 @@ export class MemoryMempool { // TODO: add check for adding a userop with conflicting nonce // In case of concurrent requests add( - userOperation: UserOperation, + userOp: UserOperation, entryPoint: Address, referencedContracts?: ReferencedCodeHashes ): [boolean, string] { const opHash = getUserOperationHash( - userOperation, + userOp, entryPoint, this.config.publicClient.chain.id ) @@ -242,26 +251,25 @@ export class MemoryMempool { const processedOrSubmittedOps = [ ...this.store.dumpProcessing(), - ...this.store - .dumpSubmitted() - .map(({ userOperation }) => userOperation) + ...this.store.dumpSubmitted() ] // Check if the exact same userOperation is already in the mempool. const existingUserOperation = [ ...outstandingOps, ...processedOrSubmittedOps - ].find((userOperation) => userOperation.hash === opHash) + ].find((userOpInfo) => userOpInfo.userOpHash === opHash) if (existingUserOperation) { return [false, "Already known"] } if ( - processedOrSubmittedOps.find((mempoolUserOp) => { + processedOrSubmittedOps.find((userOpInfo) => { + const { userOp: mempoolUserOp } = userOpInfo return ( - mempoolUserOp.sender === userOperation.sender && - mempoolUserOp.nonce === userOperation.nonce + mempoolUserOp.sender === userOp.sender && + mempoolUserOp.nonce === userOp.nonce ) }) ) { @@ -271,63 +279,59 @@ export class MemoryMempool { ] } - this.reputationManager.updateUserOperationSeenStatus( - userOperation, - entryPoint - ) - const oldUserOp = [...outstandingOps, ...processedOrSubmittedOps].find( - (mempoolUserOp) => { - const isSameSender = - mempoolUserOp.sender === userOperation.sender - - if ( - isSameSender && - mempoolUserOp.nonce === userOperation.nonce - ) { - return true - } + this.reputationManager.updateUserOperationSeenStatus(userOp, entryPoint) + const oldUserOpInfo = [ + ...outstandingOps, + ...processedOrSubmittedOps + ].find((userOpInfo) => { + const { userOp: mempoolUserOp } = userOpInfo - // Check if there is already a userOperation with initCode + same sender (stops rejected ops due to AA10). - if ( - isVersion06(mempoolUserOp) && - isVersion06(userOperation) && - userOperation.initCode && - userOperation.initCode !== "0x" - ) { - return ( - isSameSender && - mempoolUserOp.initCode && - mempoolUserOp.initCode !== "0x" - ) - } + const isSameSender = mempoolUserOp.sender === userOp.sender + if (isSameSender && mempoolUserOp.nonce === userOp.nonce) { + return true + } - // Check if there is already a userOperation with factory + same sender (stops rejected ops due to AA10). - if ( - isVersion07(mempoolUserOp) && - isVersion07(userOperation) && - userOperation.factory && - userOperation.factory !== "0x" - ) { - return ( - isSameSender && - mempoolUserOp.factory && - mempoolUserOp.factory !== "0x" - ) - } + // Check if there is already a userOperation with initCode + same sender (stops rejected ops due to AA10). + if ( + isVersion06(mempoolUserOp) && + isVersion06(userOp) && + userOp.initCode && + userOp.initCode !== "0x" + ) { + return ( + isSameSender && + mempoolUserOp.initCode && + mempoolUserOp.initCode !== "0x" + ) + } - return false + // Check if there is already a userOperation with factory + same sender (stops rejected ops due to AA10). + if ( + isVersion07(mempoolUserOp) && + isVersion07(userOp) && + userOp.factory && + userOp.factory !== "0x" + ) { + return ( + isSameSender && + mempoolUserOp.factory && + mempoolUserOp.factory !== "0x" + ) } - ) + + return false + }) const isOldUserOpProcessingOrSubmitted = processedOrSubmittedOps.some( - (submittedOp) => submittedOp.hash === oldUserOp?.hash + (userOpInfo) => userOpInfo.userOpHash === oldUserOpInfo?.userOpHash ) - if (oldUserOp) { + if (oldUserOpInfo) { + const { userOp: oldUserOp } = oldUserOpInfo let reason = "AA10 sender already constructed: A conflicting userOperation with initCode for this sender is already in the mempool. bump the gas price by minimum 10%" - if (oldUserOp.nonce === userOperation.nonce) { + if (oldUserOp.nonce === userOp.nonce) { reason = "AA25 invalid account nonce: User operation already present in mempool, bump the gas price by minimum 10%" } @@ -337,32 +341,32 @@ export class MemoryMempool { return [false, reason] } - const oldMaxPriorityFeePerGas = oldUserOp.maxPriorityFeePerGas - const newMaxPriorityFeePerGas = userOperation.maxPriorityFeePerGas - const oldMaxFeePerGas = oldUserOp.maxFeePerGas - const newMaxFeePerGas = userOperation.maxFeePerGas + const oldOp = oldUserOp + const newOp = userOp - const incrementMaxPriorityFeePerGas = - (oldMaxPriorityFeePerGas * BigInt(10)) / BigInt(100) - const incrementMaxFeePerGas = - (oldMaxFeePerGas * BigInt(10)) / BigInt(100) + const hasHigherPriorityFee = + newOp.maxPriorityFeePerGas >= + scaleBigIntByPercent(oldOp.maxPriorityFeePerGas, 110n) - if ( - newMaxPriorityFeePerGas < - oldMaxPriorityFeePerGas + incrementMaxPriorityFeePerGas || - newMaxFeePerGas < oldMaxFeePerGas + incrementMaxFeePerGas - ) { + const hasHigherMaxFee = + newOp.maxFeePerGas >= + scaleBigIntByPercent(oldOp.maxFeePerGas, 110n) + + const hasHigherFees = hasHigherPriorityFee || hasHigherMaxFee + + if (!hasHigherFees) { return [false, reason] } - this.store.removeOutstanding(oldUserOp.hash) + this.store.removeOutstanding(oldUserOpInfo.userOpHash) } // Check if mempool already includes max amount of parallel user operations const parallelUserOperationsCount = this.store .dumpOutstanding() - .filter((userOp) => { - return userOp.sender === userOperation.sender + .filter((userOpInfo) => { + const { userOp: mempoolUserOp } = userOpInfo + return mempoolUserOp.sender === userOp.sender }).length if (parallelUserOperationsCount > this.config.mempoolMaxParallelOps) { @@ -373,14 +377,15 @@ export class MemoryMempool { } // Check if mempool already includes max amount of queued user operations - const [nonceKey] = getNonceKeyAndValue(userOperation.nonce) + const [nonceKey] = getNonceKeyAndSequence(userOp.nonce) const queuedUserOperationsCount = this.store .dumpOutstanding() - .filter((userOp) => { - const [opNonceKey] = getNonceKeyAndValue(userOp.nonce) + .filter((userOpInfo) => { + const { userOp: mempoolUserOp } = userOpInfo + const [opNonceKey] = getNonceKeyAndSequence(mempoolUserOp.nonce) return ( - userOp.sender === userOperation.sender && + mempoolUserOp.sender === userOp.sender && opNonceKey === nonceKey ) }).length @@ -393,10 +398,11 @@ export class MemoryMempool { } this.store.addOutstanding({ - ...userOperation, + userOp, entryPoint, - hash: opHash, - referencedContracts + userOpHash: opHash, + referencedContracts, + addedToMempool: Date.now() }) this.monitor.setUserOperationStatus(opHash, { status: "not_submitted", @@ -409,7 +415,7 @@ export class MemoryMempool { // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: async shouldSkip( - userOperation: UserOperationInfo, + userOpInfo: UserOpInfo, paymasterDeposit: { [paymaster: string]: bigint }, stakedEntityCount: { [addr: string]: number }, knownEntities: { @@ -442,22 +448,23 @@ export class MemoryMempool { } } - const isUserOpV06 = isVersion06(userOperation) + const { userOp, entryPoint, userOpHash, referencedContracts } = + userOpInfo + + const isUserOpV06 = isVersion06(userOp) const paymaster = isUserOpV06 - ? getAddressFromInitCodeOrPaymasterAndData( - userOperation.paymasterAndData - ) - : userOperation.paymaster + ? getAddressFromInitCodeOrPaymasterAndData(userOp.paymasterAndData) + : userOp.paymaster const factory = isUserOpV06 - ? getAddressFromInitCodeOrPaymasterAndData(userOperation.initCode) - : userOperation.factory + ? getAddressFromInitCodeOrPaymasterAndData(userOp.initCode) + : userOp.factory const paymasterStatus = this.reputationManager.getStatus( - userOperation.entryPoint, + entryPoint, paymaster ) const factoryStatus = this.reputationManager.getStatus( - userOperation.entryPoint, + entryPoint, factory ) @@ -465,7 +472,7 @@ export class MemoryMempool { paymasterStatus === ReputationStatuses.banned || factoryStatus === ReputationStatuses.banned ) { - this.store.removeOutstanding(userOperation.hash) + this.store.removeOutstanding(userOpHash) return { skip: true, paymasterDeposit, @@ -484,7 +491,7 @@ export class MemoryMempool { this.logger.trace( { paymaster, - opHash: userOperation.hash + userOpHash }, "Throttled paymaster skipped" ) @@ -506,7 +513,7 @@ export class MemoryMempool { this.logger.trace( { factory, - opHash: userOperation.hash + userOpHash }, "Throttled factory skipped" ) @@ -521,13 +528,13 @@ export class MemoryMempool { } if ( - senders.has(userOperation.sender) && + senders.has(userOp.sender) && this.config.enforceUniqueSendersPerBundle ) { this.logger.trace( { - sender: userOperation.sender, - opHash: userOperation.hash + sender: userOp.sender, + userOpHash }, "Sender skipped because already included in bundle" ) @@ -548,27 +555,27 @@ export class MemoryMempool { if (!isUserOpV06) { queuedUserOperations = await this.getQueuedUserOperations( - userOperation, - userOperation.entryPoint + userOp, + entryPoint ) } validationResult = await this.validator.validateUserOperation({ shouldCheckPrefund: false, - userOperation: userOperation, + userOperation: userOp, queuedUserOperations, - entryPoint: userOperation.entryPoint, - referencedContracts: userOperation.referencedContracts + entryPoint, + referencedContracts }) } catch (e) { this.logger.error( { - opHash: userOperation.hash, + userOpHash, error: JSON.stringify(e) }, "2nd Validation error" ) - this.store.removeOutstanding(userOperation.hash) + this.store.removeOutstanding(userOpHash) return { skip: true, paymasterDeposit, @@ -583,13 +590,13 @@ export class MemoryMempool { const address = getAddress(storageAddress) if ( - address !== userOperation.sender && + address !== userOp.sender && knownEntities.sender.has(address) ) { this.logger.trace( { storageAddress, - opHash: userOperation.hash + userOpHash }, "Storage address skipped" ) @@ -608,7 +615,7 @@ export class MemoryMempool { if (paymasterDeposit[paymaster] === undefined) { const entryPointContract = getContract({ abi: isUserOpV06 ? EntryPointV06Abi : EntryPointV07Abi, - address: userOperation.entryPoint, + address: entryPoint, client: { public: this.config.publicClient } @@ -623,7 +630,7 @@ export class MemoryMempool { this.logger.trace( { paymaster, - opHash: userOperation.hash + userOpHash }, "Paymaster skipped because of insufficient balance left to sponsor all user ops in the bundle" ) @@ -645,7 +652,7 @@ export class MemoryMempool { stakedEntityCount[factory] = (stakedEntityCount[factory] ?? 0) + 1 } - senders.add(userOperation.sender) + senders.add(userOp.sender) return { skip: false, @@ -689,18 +696,21 @@ export class MemoryMempool { minOpsPerBundle: number maxBundleCount?: number }): Promise { - let outstandingUserOperations = this.store + let outstandingUserOps = this.store .dumpOutstanding() .filter((op) => op.entryPoint === entryPoint) - .sort((aUserOp, bUserOp) => { + .sort((aUserOpInfo, bUserOpInfo) => { // Sort userops before the execution // Decide the order of the userops based on the sender and nonce // If sender is the same, sort by nonce key + const aUserOp = aUserOpInfo.userOp + const bUserOp = bUserOpInfo.userOp + if (aUserOp.sender === bUserOp.sender) { - const [aNonceKey, aNonceValue] = getNonceKeyAndValue( + const [aNonceKey, aNonceValue] = getNonceKeyAndSequence( aUserOp.nonce ) - const [bNonceKey, bNonceValue] = getNonceKeyAndValue( + const [bNonceKey, bNonceValue] = getNonceKeyAndSequence( bUserOp.nonce ) @@ -715,13 +725,14 @@ export class MemoryMempool { }) .slice() - if (outstandingUserOperations.length === 0) return [] + if (outstandingUserOps.length === 0) return [] // Get EntryPoint version. (Ideally version should be derived from CLI flags) - const isV6 = isVersion06(outstandingUserOperations[0]) - const allSameVersion = outstandingUserOperations.every( - (userOperation) => isVersion06(userOperation) === isV6 - ) + const isV6 = isVersion06(outstandingUserOps[0].userOp) + const allSameVersion = outstandingUserOps.every((userOpInfo) => { + const { userOp } = userOpInfo + return isVersion06(userOp) === isV6 + }) if (!allSameVersion) { throw new Error( "All user operations from same EntryPoint must be of the same version" @@ -731,7 +742,7 @@ export class MemoryMempool { const bundles: UserOperationBundle[] = [] // Process all outstanding ops. - while (outstandingUserOperations.length > 0) { + while (outstandingUserOps.length > 0) { // If maxBundles is set and we reached the limit, break. if (maxBundleCount && bundles.length >= maxBundleCount) { break @@ -741,7 +752,7 @@ export class MemoryMempool { const currentBundle: UserOperationBundle = { entryPoint, version: isV6 ? "0.6" : "0.7", - userOperations: [] + userOps: [] } let gasUsed = 0n @@ -752,14 +763,16 @@ export class MemoryMempool { let storageMap: StorageMap = {} // Keep adding ops to current bundle. - while (outstandingUserOperations.length > 0) { - const userOperation = outstandingUserOperations.shift() - if (!userOperation) break + while (outstandingUserOps.length > 0) { + const userOpInfo = outstandingUserOps.shift() + if (!userOpInfo) break + + const { userOp, userOpHash } = userOpInfo // NOTE: currently if a userOp is skipped due to sender enforceUniqueSendersPerBundle it will be picked up // again the next time mempool.process is called. const skipResult = await this.shouldSkip( - userOperation, + userOpInfo, paymasterDeposit, stakedEntityCount, knownEntities, @@ -769,19 +782,19 @@ export class MemoryMempool { if (skipResult.skip) continue gasUsed += - userOperation.callGasLimit + - userOperation.verificationGasLimit + - (isVersion07(userOperation) - ? (userOperation.paymasterPostOpGasLimit || 0n) + - (userOperation.paymasterVerificationGasLimit || 0n) + userOp.callGasLimit + + userOp.verificationGasLimit + + (isVersion07(userOp) + ? (userOp.paymasterPostOpGasLimit || 0n) + + (userOp.paymasterVerificationGasLimit || 0n) : 0n) // Only break on gas limit if we've hit minOpsPerBundle. if ( gasUsed > maxGasLimit && - currentBundle.userOperations.length >= minOpsPerBundle + currentBundle.userOps.length >= minOpsPerBundle ) { - outstandingUserOperations.unshift(userOperation) // re-add op to front of queue + outstandingUserOps.unshift(userOpInfo) // re-add op to front of queue break } @@ -792,15 +805,15 @@ export class MemoryMempool { senders = skipResult.senders storageMap = skipResult.storageMap - this.reputationManager.decreaseUserOperationCount(userOperation) - this.store.removeOutstanding(userOperation.hash) - this.store.addProcessing(userOperation) + this.reputationManager.decreaseUserOperationCount(userOp) + this.store.removeOutstanding(userOpHash) + this.store.addProcessing(userOpInfo) // Add op to current bundle - currentBundle.userOperations.push(userOperation) + currentBundle.userOps.push(userOpInfo) } - if (currentBundle.userOperations.length > 0) { + if (currentBundle.userOps.length > 0) { bundles.push(currentBundle) } } @@ -812,23 +825,19 @@ export class MemoryMempool { // They should be executed first, ordered by nonce value // If cuurentNonceValue is not provided, it will be fetched from the chain async getQueuedUserOperations( - userOperation: UserOperation, + userOp: UserOperation, entryPoint: Address, _currentNonceValue?: bigint ): Promise { const entryPointContract = getContract({ address: entryPoint, - abi: isVersion06(userOperation) - ? EntryPointV06Abi - : EntryPointV07Abi, + abi: isVersion06(userOp) ? EntryPointV06Abi : EntryPointV07Abi, client: { public: this.config.publicClient } }) - const [nonceKey, userOperationNonceValue] = getNonceKeyAndValue( - userOperation.nonce - ) + const [nonceKey, nonceSequence] = getNonceKeyAndSequence(userOp.nonce) let currentNonceValue: bigint = BigInt(0) @@ -836,38 +845,42 @@ export class MemoryMempool { currentNonceValue = _currentNonceValue } else { const getNonceResult = await entryPointContract.read.getNonce( - [userOperation.sender, nonceKey], + [userOp.sender, nonceKey], { blockTag: "latest" } ) - currentNonceValue = getNonceKeyAndValue(getNonceResult)[1] + currentNonceValue = getNonceKeyAndSequence(getNonceResult)[1] } const outstanding = this.store .dumpOutstanding() - .filter((mempoolUserOp) => { - const [opNonceKey, opNonceValue] = getNonceKeyAndValue( - mempoolUserOp.nonce - ) + .filter((userOpInfo) => { + const { userOp: mempoolUserOp } = userOpInfo + + const [mempoolNonceKey, mempoolNonceSequence] = + getNonceKeyAndSequence(mempoolUserOp.nonce) return ( - mempoolUserOp.sender === userOperation.sender && - opNonceKey === nonceKey && - opNonceValue >= currentNonceValue && - opNonceValue < userOperationNonceValue + mempoolUserOp.sender === userOp.sender && + mempoolNonceKey === nonceKey && + mempoolNonceSequence >= currentNonceValue && + mempoolNonceSequence < nonceSequence ) }) outstanding.sort((a, b) => { - const [, aNonceValue] = getNonceKeyAndValue(a.nonce) - const [, bNonceValue] = getNonceKeyAndValue(b.nonce) + const aUserOp = a.userOp + const bUserOp = b.userOp + + const [, aNonceValue] = getNonceKeyAndSequence(aUserOp.nonce) + const [, bNonceValue] = getNonceKeyAndSequence(bUserOp.nonce) return Number(aNonceValue - bNonceValue) }) - return outstanding + return outstanding.map((userOpInfo) => userOpInfo.userOp) } clear(): void { diff --git a/src/mempool/store.ts b/src/mempool/store.ts index d970d7cb..153bf4fd 100644 --- a/src/mempool/store.ts +++ b/src/mempool/store.ts @@ -1,16 +1,12 @@ -import type { - HexData32, - SubmittedUserOperation, - UserOperationInfo -} from "@alto/types" +import type { HexData32, SubmittedUserOp, UserOpInfo } from "@alto/types" import type { Metrics } from "@alto/utils" import type { Logger } from "@alto/utils" export class MemoryStore { // private monitoredTransactions: Map = new Map() // tx hash to info - private outstandingUserOperations: UserOperationInfo[] = [] - private processingUserOperations: UserOperationInfo[] = [] - private submittedUserOperations: SubmittedUserOperation[] = [] + private outstandingUserOperations: UserOpInfo[] = [] + private processingUserOperations: UserOpInfo[] = [] + private submittedUserOperations: SubmittedUserOp[] = [] private logger: Logger private metrics: Metrics @@ -20,12 +16,12 @@ export class MemoryStore { this.metrics = metrics } - addOutstanding(op: UserOperationInfo) { + addOutstanding(userOpInfo: UserOpInfo) { const store = this.outstandingUserOperations - store.push(op) + store.push(userOpInfo) this.logger.debug( - { userOpHash: op.hash, store: "outstanding" }, + { userOpHash: userOpInfo.userOpHash, store: "outstanding" }, "added user op to mempool" ) this.metrics.userOperationsInMempool @@ -35,12 +31,12 @@ export class MemoryStore { .inc() } - addProcessing(op: UserOperationInfo) { + addProcessing(userOpInfo: UserOpInfo) { const store = this.processingUserOperations - store.push(op) + store.push(userOpInfo) this.logger.debug( - { userOpHash: op.hash, store: "processing" }, + { userOpHash: userOpInfo.userOpHash, store: "processing" }, "added user op to mempool" ) this.metrics.userOperationsInMempool @@ -50,14 +46,14 @@ export class MemoryStore { .inc() } - addSubmitted(submittedInfo: SubmittedUserOperation) { - const { userOperation } = submittedInfo + addSubmitted(submittedInfo: SubmittedUserOp) { + const { userOpHash } = submittedInfo const store = this.submittedUserOperations store.push(submittedInfo) this.logger.debug( { - userOpHash: userOperation.hash, + userOpHash, store: "submitted" }, "added user op to submitted mempool" @@ -71,7 +67,7 @@ export class MemoryStore { removeOutstanding(userOpHash: HexData32) { const index = this.outstandingUserOperations.findIndex( - (op) => op.hash === userOpHash + (userOpInfo) => userOpInfo.userOpHash === userOpHash ) if (index === -1) { this.logger.warn( @@ -95,7 +91,7 @@ export class MemoryStore { removeProcessing(userOpHash: HexData32) { const index = this.processingUserOperations.findIndex( - (op) => op.hash === userOpHash + (userOpInfo) => userOpInfo.userOpHash === userOpHash ) if (index === -1) { this.logger.warn( @@ -119,7 +115,7 @@ export class MemoryStore { removeSubmitted(userOpHash: HexData32) { const index = this.submittedUserOperations.findIndex( - (op) => op.userOperation.hash === userOpHash + (userOpInfo) => userOpInfo.userOpHash === userOpHash ) if (index === -1) { this.logger.warn( @@ -141,7 +137,7 @@ export class MemoryStore { .dec() } - dumpOutstanding(): UserOperationInfo[] { + dumpOutstanding(): UserOpInfo[] { this.logger.trace( { store: "outstanding", @@ -152,7 +148,7 @@ export class MemoryStore { return this.outstandingUserOperations } - dumpProcessing(): UserOperationInfo[] { + dumpProcessing(): UserOpInfo[] { this.logger.trace( { store: "processing", @@ -163,7 +159,7 @@ export class MemoryStore { return this.processingUserOperations } - dumpSubmitted(): SubmittedUserOperation[] { + dumpSubmitted(): SubmittedUserOp[] { this.logger.trace( { store: "submitted", length: this.submittedUserOperations.length }, "dumping mempool" diff --git a/src/rpc/nonceQueuer.ts b/src/rpc/nonceQueuer.ts index 3c12b013..edc5ac38 100644 --- a/src/rpc/nonceQueuer.ts +++ b/src/rpc/nonceQueuer.ts @@ -4,7 +4,7 @@ import { EntryPointV06Abi, EntryPointV07Abi, UserOperation } from "@alto/types" import type { Logger } from "@alto/utils" import { encodeNonce, - getNonceKeyAndValue, + getNonceKeyAndSequence, getUserOperationHash, isVersion06 } from "@alto/utils" @@ -93,7 +93,7 @@ export class NonceQueuer { } add(userOperation: UserOperation, entryPoint: Address) { - const [nonceKey, nonceSequence] = getNonceKeyAndValue( + const [nonceKey, nonceSequence] = getNonceKeyAndSequence( userOperation.nonce ) diff --git a/src/rpc/rpcHandler.ts b/src/rpc/rpcHandler.ts index c47437e9..7562891c 100644 --- a/src/rpc/rpcHandler.ts +++ b/src/rpc/rpcHandler.ts @@ -9,6 +9,8 @@ import type { ApiVersion, PackedUserOperation, StateOverrides, + UserOpInfo, + UserOperationBundle, UserOperationV06, UserOperationV07 } from "@alto/types" @@ -48,7 +50,7 @@ import { calcVerificationGasAndCallGasLimit, deepHexlify, getAAError, - getNonceKeyAndValue, + getNonceKeyAndSequence, getUserOperationHash, isVersion06, isVersion07, @@ -687,7 +689,7 @@ export class RpcHandler implements IRpcEndpoint { userOperation, entryPoint ) - const [, userOperationNonceValue] = getNonceKeyAndValue( + const [, userOperationNonceValue] = getNonceKeyAndSequence( userOperation.nonce ) @@ -861,18 +863,19 @@ export class RpcHandler implements IRpcEndpoint { ) // Prepare bundle - const userOperationInfo = { - ...userOperation, + const userOperationInfo: UserOpInfo = { + userOp: userOperation, entryPoint, - hash: getUserOperationHash( + userOpHash: getUserOperationHash( userOperation, entryPoint, this.config.publicClient.chain.id - ) + ), + addedToMempool: Date.now() } - const bundle = { + const bundle: UserOperationBundle = { entryPoint, - userOperations: [userOperationInfo], + userOps: [userOperationInfo], version: isVersion06(userOperation) ? ("0.6" as const) : ("0.7" as const) @@ -945,7 +948,7 @@ export class RpcHandler implements IRpcEndpoint { } }) - const [nonceKey] = getNonceKeyAndValue(userOperation.nonce) + const [nonceKey] = getNonceKeyAndSequence(userOperation.nonce) const getNonceResult = await entryPointContract.read.getNonce( [userOperation.sender, nonceKey], @@ -954,7 +957,7 @@ export class RpcHandler implements IRpcEndpoint { } ) - const [_, currentNonceValue] = getNonceKeyAndValue(getNonceResult) + const [_, currentNonceValue] = getNonceKeyAndSequence(getNonceResult) return currentNonceValue } @@ -985,7 +988,7 @@ export class RpcHandler implements IRpcEndpoint { userOperation, entryPoint ) - const [, userOperationNonceValue] = getNonceKeyAndValue( + const [, userOperationNonceValue] = getNonceKeyAndSequence( userOperation.nonce ) diff --git a/src/types/mempool.ts b/src/types/mempool.ts index 946b5529..57d2a885 100644 --- a/src/types/mempool.ts +++ b/src/types/mempool.ts @@ -29,13 +29,7 @@ export type TransactionInfo = { export type UserOperationBundle = { entryPoint: Address version: "0.6" | "0.7" - userOperations: UserOperationInfo[] -} - -export type UserOperationInfo = UserOperation & { - hash: Hex - entryPoint: Address - referencedContracts?: ReferencedCodeHashes + userOps: UserOpInfo[] } export enum SubmissionStatus { @@ -45,13 +39,23 @@ export enum SubmissionStatus { Included = "included" } -export type SubmittedUserOperation = { - userOperation: UserOperationInfo +export type UserOpDetails = { + userOpHash: Hex + entryPoint: Address + // timestamp when the bundling process begins (when it leaves outstanding mempool) + addedToMempool: number + referencedContracts?: ReferencedCodeHashes +} + +export type UserOpInfo = { + userOp: UserOperation +} & UserOpDetails + +export type SubmittedUserOp = UserOpInfo & { transactionInfo: TransactionInfo } -export type RejectedUserOperation = { - userOperation: UserOperationInfo +export type RejectedUserOp = UserOpInfo & { reason: string } @@ -59,8 +63,8 @@ export type BundleResult = | { // Successfully sent bundle. status: "bundle_success" - userOpsBundled: UserOperationInfo[] - rejectedUserOps: RejectedUserOperation[] + userOpsBundled: UserOpInfo[] + rejectedUserOps: RejectedUserOp[] transactionHash: HexData32 transactionRequest: { gas: bigint @@ -72,17 +76,18 @@ export type BundleResult = | { // Encountered unhandled error during bundle simulation. status: "unhandled_simulation_failure" + rejectedUserOps: RejectedUserOp[] reason: string } | { // All user operations failed during simulation. status: "all_ops_failed_simulation" - rejectedUserOps: RejectedUserOperation[] + rejectedUserOps: RejectedUserOp[] } | { // Encountered error whilst trying to send bundle. status: "bundle_submission_failure" reason: BaseError | "INTERNAL FAILURE" - userOpsToBundle: UserOperationInfo[] - rejectedUserOps: RejectedUserOperation[] + userOpsToBundle: UserOpInfo[] + rejectedUserOps: RejectedUserOp[] } diff --git a/src/utils/userop.ts b/src/utils/userop.ts index f32f1b15..2dccc480 100644 --- a/src/utils/userop.ts +++ b/src/utils/userop.ts @@ -2,13 +2,13 @@ import { EntryPointV06Abi, EntryPointV07Abi, type GetUserOperationReceiptResponseResult, - type HexData32, type PackedUserOperation, type UserOperation, type UserOperationV06, type UserOperationV07, logSchema, - receiptSchema + receiptSchema, + TransactionInfo } from "@alto/types" import * as sentry from "@sentry/node" import type { Logger } from "pino" @@ -227,19 +227,25 @@ export type BundlingStatus = } // Return the status of the bundling transaction. -export const getBundleStatus = async ( - isVersion06: boolean, - txHash: HexData32, - publicClient: PublicClient, - logger: Logger, - entryPoint: Address -): Promise<{ +export const getBundleStatus = async ({ + transactionInfo, + publicClient, + logger +}: { + transactionInfo: TransactionInfo + publicClient: PublicClient + logger: Logger +}): Promise<{ bundlingStatus: BundlingStatus blockNumber: bigint | undefined }> => { try { + const { transactionHash, bundle } = transactionInfo + const { entryPoint, version } = bundle + const isVersion06 = version === "0.6" + const receipt = await publicClient.getTransactionReceipt({ - hash: txHash + hash: transactionHash }) const blockNumber = receipt.blockNumber @@ -528,7 +534,7 @@ export const getUserOperationHash = ( ) } -export const getNonceKeyAndValue = (nonce: bigint) => { +export const getNonceKeyAndSequence = (nonce: bigint) => { const nonceKey = nonce >> 64n // first 192 bits of nonce const nonceSequence = nonce & 0xffffffffffffffffn // last 64 bits of nonce From a1fcc92dc0f3b7d70a01431a6dd611bca31ea531 Mon Sep 17 00:00:00 2001 From: mouseless <97399882+mouseless-eth@users.noreply.github.com> Date: Thu, 30 Jan 2025 12:12:08 +0000 Subject: [PATCH 32/34] use correct port --- src/mempool/mempool.ts | 19 +++++++------------ test/e2e/setup.ts | 2 +- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/src/mempool/mempool.ts b/src/mempool/mempool.ts index f1aa2003..48108144 100644 --- a/src/mempool/mempool.ts +++ b/src/mempool/mempool.ts @@ -1,7 +1,4 @@ import type { EventManager } from "@alto/handlers" -// import { MongoClient, Collection, Filter } from "mongodb" -// import { PublicClient, getContract } from "viem" -// import { EntryPointAbi } from "../types/EntryPoint" import { EntryPointV06Abi, EntryPointV07Abi, @@ -88,7 +85,7 @@ export class MemoryMempool { if (existingUserOpToReplace) { this.store.removeSubmitted(userOpHash) this.store.addSubmitted({ - ...existingUserOpToReplace, + ...userOpInfo, transactionInfo }) this.monitor.setUserOperationStatus(userOpHash, { @@ -120,9 +117,7 @@ export class MemoryMempool { } dumpOutstanding(): UserOperation[] { - return this.store - .dumpOutstanding() - .map((userOpInfo) => userOpInfo.userOp) + return this.store.dumpOutstanding().map(({ userOp }) => userOp) } dumpProcessing(): UserOpInfo[] { @@ -241,7 +236,7 @@ export class MemoryMempool { entryPoint: Address, referencedContracts?: ReferencedCodeHashes ): [boolean, string] { - const opHash = getUserOperationHash( + const userOpHash = getUserOperationHash( userOp, entryPoint, this.config.publicClient.chain.id @@ -258,7 +253,7 @@ export class MemoryMempool { const existingUserOperation = [ ...outstandingOps, ...processedOrSubmittedOps - ].find((userOpInfo) => userOpInfo.userOpHash === opHash) + ].find((userOpInfo) => userOpInfo.userOpHash === userOpHash) if (existingUserOperation) { return [false, "Already known"] @@ -400,16 +395,16 @@ export class MemoryMempool { this.store.addOutstanding({ userOp, entryPoint, - userOpHash: opHash, + userOpHash: userOpHash, referencedContracts, addedToMempool: Date.now() }) - this.monitor.setUserOperationStatus(opHash, { + this.monitor.setUserOperationStatus(userOpHash, { status: "not_submitted", transactionHash: null }) - this.eventManager.emitAddedToMempool(opHash) + this.eventManager.emitAddedToMempool(userOpHash) return [true, ""] } diff --git a/test/e2e/setup.ts b/test/e2e/setup.ts index 5c1c9146..f1fe2ffc 100644 --- a/test/e2e/setup.ts +++ b/test/e2e/setup.ts @@ -116,7 +116,7 @@ export default async function setup({ provide }) { const anvilInstance = anvil({ chainId: foundry.id, - port: 8485, + port: 8545, codeSizeLimit: 1000_000 }) await anvilInstance.start() From fc199f359d058148d5204a649f7ad27c7d969d80 Mon Sep 17 00:00:00 2001 From: mouseless <97399882+mouseless-eth@users.noreply.github.com> Date: Thu, 30 Jan 2025 12:48:23 +0000 Subject: [PATCH 33/34] cleanuo --- src/executor/executorManager.ts | 18 +++++++++--------- src/executor/filterOpsAndEStimateGas.ts | 6 ++++-- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/executor/executorManager.ts b/src/executor/executorManager.ts index 3c2ccc2b..6682bfcf 100644 --- a/src/executor/executorManager.ts +++ b/src/executor/executorManager.ts @@ -406,7 +406,7 @@ export class ExecutorManager { userOps.map((userOpInfo) => { const { userOpHash } = userOpInfo this.checkFrontrun({ - userOpHash: userOpHash, + userOpHash, transactionHash, blockNumber }) @@ -451,7 +451,7 @@ export class ExecutorManager { this.logger.info( { - userOpHash: userOpHash, + userOpHash, transactionHash }, "user op frontrun onchain" @@ -472,7 +472,7 @@ export class ExecutorManager { ) this.logger.info( { - userOpHash: userOpHash, + userOpHash, transactionHash }, "user op failed onchain" @@ -871,12 +871,12 @@ export class ExecutorManager { } resubmitUserOperations( - mempoolUserOps: UserOpInfo[], + userOps: UserOpInfo[], entryPoint: Address, reason: string ) { - mempoolUserOps.map((mempoolUserOp) => { - const { userOpHash, userOp } = mempoolUserOp + userOps.map((userOpInfo) => { + const { userOpHash, userOp } = userOpInfo this.logger.info( { userOpHash, @@ -903,9 +903,9 @@ export class ExecutorManager { this.dropUserOps(rejectedUserOps) } - removeSubmitted(mempoolUserOps: UserOpInfo[]) { - mempoolUserOps.map((mempoolOp) => { - const { userOpHash } = mempoolOp + removeSubmitted(userOps: UserOpInfo[]) { + userOps.map((userOpInfo) => { + const { userOpHash } = userOpInfo this.mempool.removeSubmitted(userOpHash) }) } diff --git a/src/executor/filterOpsAndEStimateGas.ts b/src/executor/filterOpsAndEStimateGas.ts index 5fb36d79..14fa3abb 100644 --- a/src/executor/filterOpsAndEStimateGas.ts +++ b/src/executor/filterOpsAndEStimateGas.ts @@ -79,7 +79,7 @@ export async function filterOpsAndEstimateGas({ config: AltoConfig logger: Logger }): Promise { - const { userOps: userOperations, version, entryPoint } = userOpBundle + const { userOps, version, entryPoint } = userOpBundle let { fixedGasLimitForEstimation, legacyTransactions, @@ -99,7 +99,7 @@ export async function filterOpsAndEstimateGas({ }) // Keep track of invalid and valid ops - const userOpsToBundle = [...userOperations] + const userOpsToBundle = [...userOps] const rejectedUserOps: RejectedUserOp[] = [] // Prepare bundling tx params @@ -287,6 +287,8 @@ export async function filterOpsAndEstimateGas({ rejectedUserOps.push(rejectUserOp(failingUserOp, reason)) userOpsToBundle.splice(failedOpIndex, 1) + + continue } catch (e: unknown) { logger.error( { error: JSON.stringify(err) }, From 61c81d6cb2f840bac0a749622b2dd475bacce59f Mon Sep 17 00:00:00 2001 From: mouseless <97399882+mouseless-eth@users.noreply.github.com> Date: Thu, 30 Jan 2025 13:03:05 +0000 Subject: [PATCH 34/34] fix bug --- src/executor/executor.ts | 5 ++++- src/executor/utils.ts | 15 +++++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/executor/executor.ts b/src/executor/executor.ts index 59ddd41f..ed152a57 100644 --- a/src/executor/executor.ts +++ b/src/executor/executor.ts @@ -116,7 +116,10 @@ export class Executor { }) { const { isUserOpV06, entryPoint, userOps } = txParam - const handleOpsCalldata = encodeHandleOpsCalldata(userOps, entryPoint) + const handleOpsCalldata = encodeHandleOpsCalldata({ + userOps, + beneficiary: txParam.account.address + }) const request = await this.config.walletClient.prepareTransactionRequest({ diff --git a/src/executor/utils.ts b/src/executor/utils.ts index d34a1e9c..24d3a40b 100644 --- a/src/executor/utils.ts +++ b/src/executor/utils.ts @@ -67,13 +67,16 @@ export const packUserOps = (userOpInfos: UserOpInfo[]) => { return packedUserOps as PackedUserOperation[] } -export const encodeHandleOpsCalldata = ( - userOpInfos: UserOpInfo[], +export const encodeHandleOpsCalldata = ({ + userOps, + beneficiary +}: { + userOps: UserOpInfo[] beneficiary: Address -): Hex => { - const userOps = userOpInfos.map(({ userOp }) => userOp) - const isV06 = isVersion06(userOps[0]) - const packedUserOps = packUserOps(userOpInfos) +}): Hex => { + const ops = userOps.map(({ userOp }) => userOp) + const isV06 = isVersion06(ops[0]) + const packedUserOps = packUserOps(userOps) return encodeFunctionData({ abi: isV06 ? EntryPointV06Abi : EntryPointV07Abi,