From f5d667491c90ce4aa3b6e9e8014c312183dcf961 Mon Sep 17 00:00:00 2001 From: mouseless <97399882+mouseless-eth@users.noreply.github.com> Date: Wed, 29 May 2024 15:34:27 +0100 Subject: [PATCH 1/8] capture reverts from compressed ops --- src/executor/utils.ts | 65 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 61 insertions(+), 4 deletions(-) diff --git a/src/executor/utils.ts b/src/executor/utils.ts index 352a1fcc..62dc5f7a 100644 --- a/src/executor/utils.ts +++ b/src/executor/utils.ts @@ -37,7 +37,9 @@ import { concat, decodeErrorResult, hexToBytes, - numberToHex + numberToHex, + formatTransactionRequest, + RpcRequestError } from "viem" export function simulatedOpsToResults( @@ -192,15 +194,19 @@ export async function filterOpsAndEstimateGas( .mempoolUserOperation as CompressedUserOperation ) - gasLimit = await publicClient.estimateGas({ + const tx = formatTransactionRequest({ to: bundleBulker, - account: wallet, + from: wallet.address, data: createCompressedCalldata(opsToSend, perOpInflatorId), gas: fixedGasLimitForEstimation, nonce: nonce, - blockTag, ...gasOptions }) + + gasLimit = await publicClient.request({ + method: "eth_estimateGas", + params: [tx, blockTag] + }) } return { simulatedOps, gasLimit, resubmitAllOps: false } @@ -321,6 +327,57 @@ export async function filterOpsAndEstimateGas( { error: JSON.stringify(err) }, "failed to parse error result" ) + return { + simulatedOps: [], + gasLimit: 0n, + resubmitAllOps: false + } + } + } else if (err instanceof RpcRequestError) { + try { + const errorHexData = (err.cause as unknown as any).data + + const errorResult = decodeErrorResult({ + 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 !== "FailedOp") { + logger.error( + { + errorName: errorResult.errorName, + args: errorResult.args + }, + "unexpected error result" + ) + return { + simulatedOps: [], + gasLimit: 0n, + resubmitAllOps: false + } + } + + const failingOp = simulatedOps.filter( + (op) => op.reason === undefined + )[Number(errorResult.args[0])] + + failingOp.reason = errorResult.args[1] + } catch (e: unknown) { + logger.error( + { error: JSON.stringify(e) }, + "failed to parse error result" + ) + return { simulatedOps: [], gasLimit: 0n, From 6f9852773b093d7ff49dc5c26b7e36746daeb31d Mon Sep 17 00:00:00 2001 From: mouseless <97399882+mouseless-eth@users.noreply.github.com> Date: Wed, 29 May 2024 21:35:22 +0100 Subject: [PATCH 2/8] fix kinto e2e tests --- src/executor/utils.ts | 7 +++- test/kinto-e2e/src/index.ts | 70 +++++++++++++++++++-------------- test/kinto-e2e/src/setupAlto.ts | 13 ++++-- test/kinto-e2e/src/utils.ts | 21 +++++++++- 4 files changed, 75 insertions(+), 36 deletions(-) diff --git a/src/executor/utils.ts b/src/executor/utils.ts index 62dc5f7a..1504b776 100644 --- a/src/executor/utils.ts +++ b/src/executor/utils.ts @@ -39,7 +39,8 @@ import { hexToBytes, numberToHex, formatTransactionRequest, - RpcRequestError + RpcRequestError, + hexToBigInt } from "viem" export function simulatedOpsToResults( @@ -203,10 +204,12 @@ export async function filterOpsAndEstimateGas( ...gasOptions }) - gasLimit = await publicClient.request({ + const rpcResponse = await publicClient.request({ method: "eth_estimateGas", params: [tx, blockTag] }) + + gasLimit = hexToBigInt(rpcResponse) } return { simulatedOps, gasLimit, resubmitAllOps: false } diff --git a/test/kinto-e2e/src/index.ts b/test/kinto-e2e/src/index.ts index 3b06d7d7..a5a115ab 100644 --- a/test/kinto-e2e/src/index.ts +++ b/test/kinto-e2e/src/index.ts @@ -1,5 +1,4 @@ import { - type Address, type Hex, createPublicClient, decodeEventLog, @@ -7,9 +6,10 @@ import { http, parseAbi, parseAbiItem, - getAddress + slice, + hexToNumber } from "viem" -import { BundleBulkerAbi, handleOpsAbi } from "./abi" +import { handleOpsAbi } from "./abi" import { type Pool, createPool } from "@viem/anvil" import type { UserOperation } from "permissionless" import { createPimlicoBundlerClient } from "permissionless/clients/pimlico" @@ -18,22 +18,13 @@ import { KINTO_ENTRYPOINT, kintoMainnet, prettyPrintTxHash, - sleep + sleep, + type OpInfoType, + type CompressedOp, + isCompressed } from "./utils" import { startAlto } from "./setupAlto" -type CompressedOp = { - compressedBytes: Hex - inflator: Address -} - -type OpInfoType = { - opHash: Hex - txHash: Hex - blockNum: bigint - opParams: UserOperation | CompressedOp -} - const canReplayUserOperation = async ({ anvilPool, anvilId, @@ -57,7 +48,11 @@ const canReplayUserOperation = async ({ const anvilRpc = `http://${anvil.host}:${anvil.port}` // spin up new alto instance - const altoProcess = await startAlto(anvilRpc, altoPort.toString()) + const altoProcess = await startAlto( + anvilRpc, + altoPort.toString(), + "inflator" in opInfo.opParams + ) // resend userOperation and that it gets mined const bundlerClient = createPimlicoBundlerClient({ @@ -65,7 +60,8 @@ const canReplayUserOperation = async ({ }) let hash: Hex - if ("inflator" in opParams) { + if (isCompressed(opParams)) { + console.log("sending compressed UserOperation") hash = await bundlerClient.sendCompressedUserOperation({ compressedUserOperation: opParams.compressedBytes, inflatorAddress: opParams.inflator, @@ -122,7 +118,8 @@ const main = async () => { let userOperationEvents = await publicClient.getLogs({ address: KINTO_ENTRYPOINT, event: parseAbiItem(userOperationEventAbi), - fromBlock: latestBlock - 10_000n + fromBlock: latestBlock - 10_000n, + toBlock: latestBlock }) userOperationEvents = userOperationEvents.reverse() @@ -163,15 +160,24 @@ const main = async () => { data: rawTx.input }).args[0][0] } catch { - const compressedBytes = decodeFunctionData({ - abi: BundleBulkerAbi, - data: rawTx.input - }).args[0] + // Extract first compressedUserOperation (compressedBytes) + // slice of 9 bytes: + // - 4 Bytes BundleBulker Payload (PerOpInflator Id) + // - 1 Bytes PerOpInflator Payload (number of ops) + // - 4 Bytes PerOpInflator Payload (inflator id) + const bytes = slice(rawTx.input, 9, undefined) + + const compressedLength = hexToNumber(slice(bytes, 0, 2)) + + const compressedBytes = slice( + bytes, + 2, + 2 + compressedLength + ) + opParams = { compressedBytes, - inflator: getAddress( - "0x336a76a7A2a1e97CE20c420F39FC08c441234aa2" - ) + inflator: "0x336a76a7A2a1e97CE20c420F39FC08c441234aa2" } } @@ -229,7 +235,7 @@ const main = async () => { const endTime = performance.now() const elapsedTime = (endTime - startTime) / 1000 - // biome-ignore lint/suspicious/noConsoleLog: + // biome-ignore lint/suspicious/noConsoleLog: console.log( `Processed ${processed}/${totalOps} operations. (processed in ${elapsedTime.toFixed( 2 @@ -240,11 +246,15 @@ const main = async () => { // if any ops failed, print them and exit with 1 if (failedOps.length > 0) { for (const f of failedOps) { + let opType = "uncompressed" + if (isCompressed(f.opParams)) { + opType = "compressed" + } // biome-ignore lint/suspicious/noConsoleLog: console.log( - `FAILED OP: ${f.opHash} (txhash: ${prettyPrintTxHash( - f.txHash - )})` + `[${opType}] FAILED OP: ${ + f.opHash + } (txhash: ${prettyPrintTxHash(f.txHash)})` ) } process.exit(1) diff --git a/test/kinto-e2e/src/setupAlto.ts b/test/kinto-e2e/src/setupAlto.ts index ad231480..3e650c2b 100644 --- a/test/kinto-e2e/src/setupAlto.ts +++ b/test/kinto-e2e/src/setupAlto.ts @@ -5,7 +5,11 @@ import waitPort from "wait-port" import { sleep } from "./utils" // skip docker wait times, just start locally -export const startAlto = async (rpc: string, altoPort: string) => { +export const startAlto = async ( + rpc: string, + altoPort: string, + print?: boolean +) => { const anvil = createTestClient({ transport: http(rpc), mode: "anvil" @@ -46,9 +50,12 @@ export const startAlto = async (rpc: string, altoPort: string) => { const alto = spawn(command, args, options) + if (print) { + alto.stdout.on("data", (data) => console.log(data.toString())) + alto.stderr.on("data", (data) => console.log(data.toString())) + } + // [USE FOR DEBUGGING] - //alto.stdout.on("data", (data) => console.log(data.toString())) - //alto.stderr.on("data", (data) => console.log(data.toString())) await waitPort({ host: "127.0.0.1", diff --git a/test/kinto-e2e/src/utils.ts b/test/kinto-e2e/src/utils.ts index f75b052a..05b26adf 100644 --- a/test/kinto-e2e/src/utils.ts +++ b/test/kinto-e2e/src/utils.ts @@ -1,9 +1,28 @@ -import { defineChain, type Hash } from "viem" +import type { UserOperation } from "permissionless" +import { defineChain, type Hex, type Hash, type Address } from "viem" export const prettyPrintTxHash = (hash: Hash) => { return `https://kintoscan.io/tx/${hash}` } +export type CompressedOp = { + compressedBytes: Hex + inflator: Address +} + +export type OpInfoType = { + opHash: Hex + txHash: Hex + blockNum: bigint + opParams: UserOperation | CompressedOp +} + +export const isCompressed = ( + op: UserOperation | CompressedOp +): op is CompressedOp => { + return "inflator" in op +} + export const kintoMainnet = defineChain({ id: 7887, name: "Kinto Mainnet", From ed933c2bfee27673df28cef3068c6efd4b05bdb4 Mon Sep 17 00:00:00 2001 From: mouseless <97399882+mouseless-eth@users.noreply.github.com> Date: Wed, 29 May 2024 21:36:04 +0100 Subject: [PATCH 3/8] remove console log --- test/kinto-e2e/src/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/kinto-e2e/src/index.ts b/test/kinto-e2e/src/index.ts index a5a115ab..9a808424 100644 --- a/test/kinto-e2e/src/index.ts +++ b/test/kinto-e2e/src/index.ts @@ -61,7 +61,6 @@ const canReplayUserOperation = async ({ let hash: Hex if (isCompressed(opParams)) { - console.log("sending compressed UserOperation") hash = await bundlerClient.sendCompressedUserOperation({ compressedUserOperation: opParams.compressedBytes, inflatorAddress: opParams.inflator, From 533549f33dc60e427e27d38c0c0248ae2db21131 Mon Sep 17 00:00:00 2001 From: mouseless <97399882+mouseless-eth@users.noreply.github.com> Date: Wed, 29 May 2024 21:53:03 +0100 Subject: [PATCH 4/8] cleanup logic --- src/executor/utils.ts | 66 ++++++++----------------------------------- 1 file changed, 12 insertions(+), 54 deletions(-) diff --git a/src/executor/utils.ts b/src/executor/utils.ts index 1504b776..939f6eb2 100644 --- a/src/executor/utils.ts +++ b/src/executor/utils.ts @@ -275,8 +275,11 @@ export async function filterOpsAndEstimateGas( resubmitAllOps: false } } - } else if (e instanceof EstimateGasExecutionError) { - if (e.cause instanceof FeeCapTooLowError) { + } else if ( + e instanceof EstimateGasExecutionError || + err instanceof RpcRequestError + ) { + if (e?.cause instanceof FeeCapTooLowError) { logger.info( { error: e.shortMessage }, "error estimating gas due to max fee < basefee" @@ -289,57 +292,13 @@ export async function filterOpsAndEstimateGas( } try { - const errorHexData = e.details.split("Reverted ")[1] as Hex - const errorResult = decodeErrorResult({ - 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" - ) + let errorHexData: Hex - if (errorResult.errorName !== "FailedOp") { - logger.error( - { - errorName: errorResult.errorName, - args: errorResult.args - }, - "unexpected error result" - ) - return { - simulatedOps: [], - gasLimit: 0n, - resubmitAllOps: false - } - } - - const failingOp = simulatedOps.filter( - (op) => op.reason === undefined - )[Number(errorResult.args[0])] - - failingOp.reason = errorResult.args[1] - } catch (_e: unknown) { - logger.error( - { error: JSON.stringify(err) }, - "failed to parse error result" - ) - return { - simulatedOps: [], - gasLimit: 0n, - resubmitAllOps: false + if (err instanceof RpcRequestError) { + errorHexData = (err.cause as unknown as any).data + } else { + errorHexData = e?.details.split("Reverted ")[1] as Hex } - } - } else if (err instanceof RpcRequestError) { - try { - const errorHexData = (err.cause as unknown as any).data - const errorResult = decodeErrorResult({ abi: isUserOpV06 ? EntryPointV06Abi : EntryPointV07Abi, data: errorHexData @@ -375,12 +334,11 @@ export async function filterOpsAndEstimateGas( )[Number(errorResult.args[0])] failingOp.reason = errorResult.args[1] - } catch (e: unknown) { + } catch (_e: unknown) { logger.error( - { error: JSON.stringify(e) }, + { error: JSON.stringify(err) }, "failed to parse error result" ) - return { simulatedOps: [], gasLimit: 0n, From f43537f38b57b9fddaae1cbc0c6948debea894a3 Mon Sep 17 00:00:00 2001 From: mouseless <97399882+mouseless-eth@users.noreply.github.com> Date: Wed, 29 May 2024 21:56:30 +0100 Subject: [PATCH 5/8] remove dbeugging functions --- test/kinto-e2e/src/index.ts | 6 +----- test/kinto-e2e/src/setupAlto.ts | 13 +++---------- 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/test/kinto-e2e/src/index.ts b/test/kinto-e2e/src/index.ts index 9a808424..7af23377 100644 --- a/test/kinto-e2e/src/index.ts +++ b/test/kinto-e2e/src/index.ts @@ -48,11 +48,7 @@ const canReplayUserOperation = async ({ const anvilRpc = `http://${anvil.host}:${anvil.port}` // spin up new alto instance - const altoProcess = await startAlto( - anvilRpc, - altoPort.toString(), - "inflator" in opInfo.opParams - ) + const altoProcess = await startAlto(anvilRpc, altoPort.toString()) // resend userOperation and that it gets mined const bundlerClient = createPimlicoBundlerClient({ diff --git a/test/kinto-e2e/src/setupAlto.ts b/test/kinto-e2e/src/setupAlto.ts index 3e650c2b..ad231480 100644 --- a/test/kinto-e2e/src/setupAlto.ts +++ b/test/kinto-e2e/src/setupAlto.ts @@ -5,11 +5,7 @@ import waitPort from "wait-port" import { sleep } from "./utils" // skip docker wait times, just start locally -export const startAlto = async ( - rpc: string, - altoPort: string, - print?: boolean -) => { +export const startAlto = async (rpc: string, altoPort: string) => { const anvil = createTestClient({ transport: http(rpc), mode: "anvil" @@ -50,12 +46,9 @@ export const startAlto = async ( const alto = spawn(command, args, options) - if (print) { - alto.stdout.on("data", (data) => console.log(data.toString())) - alto.stderr.on("data", (data) => console.log(data.toString())) - } - // [USE FOR DEBUGGING] + //alto.stdout.on("data", (data) => console.log(data.toString())) + //alto.stderr.on("data", (data) => console.log(data.toString())) await waitPort({ host: "127.0.0.1", From 8249d7accac817d30953773ab3da27be6d8d3b33 Mon Sep 17 00:00:00 2001 From: mouseless <97399882+mouseless-eth@users.noreply.github.com> Date: Thu, 30 May 2024 10:50:08 +0100 Subject: [PATCH 6/8] use err.walk --- src/executor/utils.ts | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/src/executor/utils.ts b/src/executor/utils.ts index 939f6eb2..c4490864 100644 --- a/src/executor/utils.ts +++ b/src/executor/utils.ts @@ -40,7 +40,9 @@ import { numberToHex, formatTransactionRequest, RpcRequestError, - hexToBigInt + hexToBigInt, + BaseError, + RpcError } from "viem" export function simulatedOpsToResults( @@ -195,27 +197,22 @@ export async function filterOpsAndEstimateGas( .mempoolUserOperation as CompressedUserOperation ) - const tx = formatTransactionRequest({ + gasLimit = await publicClient.estimateGas({ to: bundleBulker, - from: wallet.address, + account: wallet, data: createCompressedCalldata(opsToSend, perOpInflatorId), gas: fixedGasLimitForEstimation, nonce: nonce, + blockTag, ...gasOptions }) - - const rpcResponse = await publicClient.request({ - method: "eth_estimateGas", - params: [tx, blockTag] - }) - - gasLimit = hexToBigInt(rpcResponse) } return { simulatedOps, gasLimit, resubmitAllOps: false } } catch (err: unknown) { logger.error({ err }, "error estimating gas") const e = parseViemError(err) + if (e instanceof ContractFunctionRevertedError) { const failedOpError = failedOpErrorSchema.safeParse(e.data) const failedOpWithRevertError = @@ -277,7 +274,7 @@ export async function filterOpsAndEstimateGas( } } else if ( e instanceof EstimateGasExecutionError || - err instanceof RpcRequestError + e === undefined ) { if (e?.cause instanceof FeeCapTooLowError) { logger.info( @@ -292,10 +289,17 @@ export async function filterOpsAndEstimateGas( } try { - let errorHexData: Hex + let errorHexData: Hex = "0x" - if (err instanceof RpcRequestError) { - errorHexData = (err.cause as unknown as any).data + if (e === undefined && err instanceof BaseError) { + // if undefined, unwrap till we get inner RpcRequestError type + const s = err.walk( + (error) => error instanceof RpcRequestError + ) + + if (s instanceof RpcRequestError) { + errorHexData = (s?.cause as unknown as any).data + } } else { errorHexData = e?.details.split("Reverted ")[1] as Hex } From 03e75b0d51a374679367c128e028ac3b3cf70a5c Mon Sep 17 00:00:00 2001 From: mouseless <97399882+mouseless-eth@users.noreply.github.com> Date: Thu, 30 May 2024 13:04:51 +0100 Subject: [PATCH 7/8] extract revert bytes if compressed op fails --- src/executor/utils.ts | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/src/executor/utils.ts b/src/executor/utils.ts index c4490864..ddcf0ecb 100644 --- a/src/executor/utils.ts +++ b/src/executor/utils.ts @@ -37,13 +37,9 @@ import { concat, decodeErrorResult, hexToBytes, - numberToHex, - formatTransactionRequest, - RpcRequestError, - hexToBigInt, - BaseError, - RpcError + numberToHex } from "viem" +import { getRevertErrorData } from "viem/_types/actions/public/call" export function simulatedOpsToResults( simulatedOps: { @@ -274,7 +270,7 @@ export async function filterOpsAndEstimateGas( } } else if ( e instanceof EstimateGasExecutionError || - e === undefined + err instanceof EstimateGasExecutionError ) { if (e?.cause instanceof FeeCapTooLowError) { logger.info( @@ -291,15 +287,8 @@ export async function filterOpsAndEstimateGas( try { let errorHexData: Hex = "0x" - if (e === undefined && err instanceof BaseError) { - // if undefined, unwrap till we get inner RpcRequestError type - const s = err.walk( - (error) => error instanceof RpcRequestError - ) - - if (s instanceof RpcRequestError) { - errorHexData = (s?.cause as unknown as any).data - } + if (err instanceof EstimateGasExecutionError) { + errorHexData = getRevertErrorData(err) as Hex } else { errorHexData = e?.details.split("Reverted ")[1] as Hex } @@ -338,7 +327,7 @@ export async function filterOpsAndEstimateGas( )[Number(errorResult.args[0])] failingOp.reason = errorResult.args[1] - } catch (_e: unknown) { + } catch (e: unknown) { logger.error( { error: JSON.stringify(err) }, "failed to parse error result" From a1f66a2d02bd079c6322bbc0961b3d322a35bc42 Mon Sep 17 00:00:00 2001 From: mouseless <97399882+mouseless-eth@users.noreply.github.com> Date: Thu, 30 May 2024 13:21:37 +0100 Subject: [PATCH 8/8] fix build --- src/executor/utils.ts | 2 +- src/utils/helpers.ts | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/executor/utils.ts b/src/executor/utils.ts index ddcf0ecb..9c6235d4 100644 --- a/src/executor/utils.ts +++ b/src/executor/utils.ts @@ -16,6 +16,7 @@ import { } from "@alto/types" import type { Logger } from "@alto/utils" import { + getRevertErrorData, isVersion06, parseViemError, toPackedUserOperation, @@ -39,7 +40,6 @@ import { hexToBytes, numberToHex } from "viem" -import { getRevertErrorData } from "viem/_types/actions/public/call" export function simulatedOpsToResults( simulatedOps: { diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index e55d1ff0..d71d3794 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -1,6 +1,18 @@ -import { type Address, getAddress } from "viem" +import { + type Address, + getAddress, + BaseError, + type RawContractError +} from "viem" /// Ensure proper equality by converting both addresses into their checksum type export const areAddressesEqual = (a: Address, b: Address) => { return getAddress(a) === getAddress(b) } + +export function getRevertErrorData(err: unknown) { + // biome-ignore lint/style/useBlockStatements: + if (!(err instanceof BaseError)) return undefined + const error = err.walk() as RawContractError + return typeof error?.data === "object" ? error.data?.data : error.data +}