diff --git a/packages/bundler/src/BundlerServer.ts b/packages/bundler/src/BundlerServer.ts index db501dd0..d98d36f2 100644 --- a/packages/bundler/src/BundlerServer.ts +++ b/packages/bundler/src/BundlerServer.ts @@ -1,7 +1,7 @@ import bodyParser from 'body-parser' import cors from 'cors' import express, { Express, Response, Request, RequestHandler } from 'express' -import { Provider } from '@ethersproject/providers' +import { JsonRpcProvider, Provider } from '@ethersproject/providers' import { Signer, utils } from 'ethers' import { parseEther } from 'ethers/lib/utils' import { Server } from 'http' @@ -247,9 +247,20 @@ export class BundlerServer { throw new RpcError(`Method ${method} is not supported`, -32601) } if (params[0].sender != null) { - result = await this.methodHandlerRip7560.sendRIP7560Transaction(params[0]) + result = await this.methodHandlerRip7560.sendRIP7560Transaction(params[0], false) } break + case 'debug_bundler_sendTransactionSkipValidation': + if (!this.config.rip7560) { + throw new RpcError(`Method ${method} is not supported`, -32601) + } + if (params[0].sender != null) { + result = await this.methodHandlerRip7560.sendRIP7560Transaction(params[0], true) + } + break + case 'eth_getRip7560TransactionDebugInfo': + result = await (this.provider as JsonRpcProvider).send('eth_getRip7560TransactionDebugInfo', [params[0]]) + break case 'eth_getTransactionReceipt': if (!this.config.rip7560) { throw new RpcError(`Method ${method} is not supported`, -32601) diff --git a/packages/bundler/src/MethodHandlerERC4337.ts b/packages/bundler/src/MethodHandlerERC4337.ts index 3c9bf445..9ec56677 100644 --- a/packages/bundler/src/MethodHandlerERC4337.ts +++ b/packages/bundler/src/MethodHandlerERC4337.ts @@ -167,7 +167,7 @@ export class MethodHandlerERC4337 { await this._validateParameters(userOp, entryPointInput) console.log(`UserOperation: Sender=${userOp.sender} Nonce=${tostr(userOp.nonce)} EntryPoint=${entryPointInput} Paymaster=${userOp.paymaster ?? ''}`) - await this.execManager.sendUserOperation(userOp, entryPointInput) + await this.execManager.sendUserOperation(userOp, entryPointInput, false) return await this.entryPoint.getUserOpHash(packUserOp(userOp)) } diff --git a/packages/bundler/src/MethodHandlerRIP7560.ts b/packages/bundler/src/MethodHandlerRIP7560.ts index 4d3f5e17..c3ea77ff 100644 --- a/packages/bundler/src/MethodHandlerRIP7560.ts +++ b/packages/bundler/src/MethodHandlerRIP7560.ts @@ -22,10 +22,10 @@ export class MethodHandlerRIP7560 { readonly provider: JsonRpcProvider ) {} - async sendRIP7560Transaction (transaction: OperationRIP7560): Promise { + async sendRIP7560Transaction (transaction: OperationRIP7560, skipValidation: boolean): Promise { await this._validateParameters(transaction) console.log(`RIP7560Transaction: Sender=${transaction.sender} Nonce=${getPackedNonce(transaction).toHexString()} Paymaster=${transaction.paymaster ?? ''}`) - await this.execManager.sendUserOperation(transaction, '') + await this.execManager.sendUserOperation(transaction, '', skipValidation) return getRIP7560TransactionHash(transaction) } diff --git a/packages/bundler/src/modules/BundleManager.ts b/packages/bundler/src/modules/BundleManager.ts index 122050ef..f56553ff 100644 --- a/packages/bundler/src/modules/BundleManager.ts +++ b/packages/bundler/src/modules/BundleManager.ts @@ -5,7 +5,11 @@ import { JsonRpcProvider } from '@ethersproject/providers' import { Mutex } from 'async-mutex' import { isAddress } from 'ethers/lib/utils' -import { IValidationManager, ValidateUserOpResult } from '@account-abstraction/validation-manager' +import { + EmptyValidateUserOpResult, + IValidationManager, + ValidateUserOpResult +} from '@account-abstraction/validation-manager' import { AddressZero, @@ -260,10 +264,14 @@ export class BundleManager implements IBundleManager { // allow only a single UserOp per sender per bundle continue } - let validationResult: ValidateUserOpResult + let validationResult: ValidateUserOpResult = EmptyValidateUserOpResult try { - // re-validate UserOp. no need to check stake, since it cannot be reduced between first and 2nd validation - validationResult = await this.validationManager.validateUserOp(entry.userOp, entry.referencedContracts, false) + if (!entry.skipValidation) { + // re-validate UserOp. no need to check stake, since it cannot be reduced between first and 2nd validation + validationResult = await this.validationManager.validateUserOp(entry.userOp, entry.referencedContracts, false) + } else { + console.warn('Skipping second validation for an injected debug operation, id=', entry.userOpHash) + } } catch (e: any) { this._handleSecondValidationException(e, paymaster, entry) continue diff --git a/packages/bundler/src/modules/ExecutionManager.ts b/packages/bundler/src/modules/ExecutionManager.ts index cd61c1e9..b0168074 100644 --- a/packages/bundler/src/modules/ExecutionManager.ts +++ b/packages/bundler/src/modules/ExecutionManager.ts @@ -7,7 +7,10 @@ import { SendBundleReturn } from './BundleManager' import { MempoolManager } from './MempoolManager' import { ReputationManager } from './ReputationManager' import { IBundleManager } from './IBundleManager' -import { IValidationManager } from '@account-abstraction/validation-manager' +import { + EmptyValidateUserOpResult, + IValidationManager +} from '@account-abstraction/validation-manager' import { DepositManager } from './DepositManager' import { BigNumberish, Signer } from 'ethers' @@ -40,15 +43,21 @@ export class ExecutionManager { * send a user operation through the bundler. * @param userOp the UserOp to send. * @param entryPointInput the entryPoint passed through the RPC request. + * @param skipValidation if set to true we will not perform tracing and ERC-7562 rules compliance validation */ - async sendUserOperation (userOp: OperationBase, entryPointInput: string): Promise { + async sendUserOperation (userOp: OperationBase, entryPointInput: string, skipValidation: boolean): Promise { await this.mutex.runExclusive(async () => { debug('sendUserOperation') this.validationManager.validateInputParameters(userOp, entryPointInput) - const validationResult = await this.validationManager.validateUserOp(userOp, undefined) + let validationResult = EmptyValidateUserOpResult + if (!skipValidation) { + validationResult = await this.validationManager.validateUserOp(userOp, undefined) + } const userOpHash = await this.validationManager.getOperationHash(userOp) await this.depositManager.checkPaymasterDeposit(userOp) - this.mempoolManager.addUserOp(userOp, + this.mempoolManager.addUserOp( + skipValidation, + userOp, userOpHash, validationResult.returnInfo.prefund ?? 0, validationResult.referencedContracts, diff --git a/packages/bundler/src/modules/MempoolEntry.ts b/packages/bundler/src/modules/MempoolEntry.ts index 11807496..ea62f96d 100644 --- a/packages/bundler/src/modules/MempoolEntry.ts +++ b/packages/bundler/src/modules/MempoolEntry.ts @@ -9,6 +9,7 @@ export class MempoolEntry { readonly userOpHash: string, readonly prefund: BigNumberish, readonly referencedContracts: ReferencedCodeHashes, + readonly skipValidation: boolean, readonly aggregator?: string ) { this.userOpMaxGas = BigNumber diff --git a/packages/bundler/src/modules/MempoolManager.ts b/packages/bundler/src/modules/MempoolManager.ts index c8f13674..ea1c0dcf 100644 --- a/packages/bundler/src/modules/MempoolManager.ts +++ b/packages/bundler/src/modules/MempoolManager.ts @@ -61,6 +61,7 @@ export class MempoolManager { // replace existing, if any (and if new gas is higher) // reverts if unable to add UserOp to mempool (too many UserOps with this sender) addUserOp ( + skipValidation: boolean, userOp: OperationBase, userOpHash: string, prefund: BigNumberish, @@ -75,6 +76,7 @@ export class MempoolManager { userOpHash, prefund, referencedContracts, + skipValidation, aggregatorInfo?.addr ) const packedNonce = getPackedNonce(entry.userOp) @@ -86,8 +88,10 @@ export class MempoolManager { this.mempool[index] = entry } else { debug('add userOp', userOp.sender, packedNonce) - this.checkReputation(senderInfo, paymasterInfo, factoryInfo, aggregatorInfo) - this.checkMultipleRolesViolation(userOp) + if (!skipValidation) { + this.checkReputation(senderInfo, paymasterInfo, factoryInfo, aggregatorInfo) + this.checkMultipleRolesViolation(userOp) + } this.incrementEntryCount(userOp.sender) if (userOp.paymaster != null) { this.incrementEntryCount(userOp.paymaster) diff --git a/packages/validation-manager/src/IValidationManager.ts b/packages/validation-manager/src/IValidationManager.ts index fa28b9f2..fae75d72 100644 --- a/packages/validation-manager/src/IValidationManager.ts +++ b/packages/validation-manager/src/IValidationManager.ts @@ -1,5 +1,5 @@ import { OperationBase, ReferencedCodeHashes, StakeInfo, StorageMap } from '@account-abstraction/utils' -import { BigNumberish } from 'ethers' +import { BigNumber, BigNumberish } from 'ethers' /** * result from successful validation @@ -24,6 +24,26 @@ export interface ValidateUserOpResult extends ValidationResult { storageMap: StorageMap } +export const EmptyValidateUserOpResult: ValidateUserOpResult = { + returnInfo: { + preOpGas: BigNumber.from(0), + prefund: BigNumber.from(0), + sigFailed: false, + validAfter: 0, + validUntil: 0 + }, + senderInfo: { + addr: '', + stake: '0', + unstakeDelaySec: 0 + }, + referencedContracts: { + addresses: [], + hash: '' + }, + storageMap: {} +} + export interface IValidationManager { validateInputParameters: (operation: OperationBase, entryPointInput?: string, requireSignature?: boolean, requireGasParams?: boolean) => void