From 3b22c35cb1d5db7b8ab015392bd93aa8ddfc1a25 Mon Sep 17 00:00:00 2001 From: shahafn Date: Wed, 25 Sep 2024 12:07:37 +0300 Subject: [PATCH 1/4] AA-308 Checking if another bundler already tried to deploy contracts (#227) * Checking if another bundler already tried to deploy contracts * lint --- packages/bundler/src/runBundler.ts | 9 +++++++-- submodules/rip7560 | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/bundler/src/runBundler.ts b/packages/bundler/src/runBundler.ts index c8ca6445..0e0c918b 100644 --- a/packages/bundler/src/runBundler.ts +++ b/packages/bundler/src/runBundler.ts @@ -137,8 +137,13 @@ export async function runBundler (argv: string[], overrideExit = true): Promise< } if (config.rip7560) { - await deployNonceManager(provider, wallet as any) - await deployStakeManager(provider, wallet as any) + try { + await deployNonceManager(provider, wallet as any) + await deployStakeManager(provider, wallet as any) + } catch (e: any) { + console.warn(e) + if (!(e.message as string).includes('replacement fee too low') && !(e.message as string).includes('already known')) throw e + } } const { diff --git a/submodules/rip7560 b/submodules/rip7560 index f47361b7..eae66dc9 160000 --- a/submodules/rip7560 +++ b/submodules/rip7560 @@ -1 +1 @@ -Subproject commit f47361b75460e0ab6bcfbd02477aca9498d76aa3 +Subproject commit eae66dc9620436369f6316a843eb587941be1fa3 From 5dd266b898d2c42b3f45e5189d6d8f6bcfbc4c32 Mon Sep 17 00:00:00 2001 From: Alex Forshtat Date: Sun, 20 Oct 2024 14:00:30 +0200 Subject: [PATCH 2/4] AA-398: Standardize 'preVerificationGas' verification & create 'debug_bundler_setConfiguration' (#228) * AA-398: (WIP) Standardize 'preVerificationGas' verification * Enable configuring PVG calculation overrides * Implement 'debug_bundler_setConfiguration' * Separate 'fixedGasOverhead' and 'transactionGasStipend' chain config params * Remove deprecated 'GetUserOpHashes' and 'BundlerHelper' solidity code --- packages/bundler/contracts/BundlerHelper.sol | 20 --- .../bundler/localconfig/bundler.config.json | 5 +- .../localconfig/bundler.rip7560.config.json | 5 +- packages/bundler/src/BundlerConfig.ts | 39 +++++- packages/bundler/src/BundlerServer.ts | 9 +- packages/bundler/src/DebugMethodHandler.ts | 16 ++- packages/bundler/src/MethodHandlerERC4337.ts | 43 ++++--- packages/bundler/src/modules/BundleManager.ts | 10 +- .../bundler/src/modules/ExecutionManager.ts | 17 ++- packages/bundler/src/modules/initServer.ts | 9 +- packages/bundler/src/runBundler.ts | 11 +- packages/bundler/src/runner/runop.ts | 5 +- packages/bundler/test/BundlerManager.test.ts | 12 +- packages/bundler/test/BundlerServer.test.ts | 10 +- .../bundler/test/DebugMethodHandler.test.ts | 9 +- .../bundler/test/UserOpMethodHandler.test.ts | 15 ++- packages/bundler/test/ValidateManager.test.ts | 12 +- packages/sdk/src/BaseAccountAPI.ts | 14 +-- .../sdk/src/PreVerificationGasCalculator.ts | 119 ++++++++++++++++++ packages/sdk/src/calcPreVerificationGas.ts | 79 ------------ packages/sdk/src/index.ts | 4 +- .../sdk/test/4-calcPreVerificationGas.test.ts | 35 ------ packages/utils/src/DeterministicDeployer.ts | 2 +- packages/utils/src/ERC4337Utils.ts | 1 - packages/utils/src/RIP7560Utils.ts | 2 +- packages/utils/src/Utils.ts | 4 +- .../src/IValidationManager.ts | 9 +- .../src/ValidationManager.ts | 27 +++- .../src/ValidationManagerRIP7560.ts | 23 +++- packages/validation-manager/src/index.ts | 4 +- 30 files changed, 343 insertions(+), 227 deletions(-) delete mode 100644 packages/bundler/contracts/BundlerHelper.sol create mode 100644 packages/sdk/src/PreVerificationGasCalculator.ts delete mode 100644 packages/sdk/src/calcPreVerificationGas.ts delete mode 100644 packages/sdk/test/4-calcPreVerificationGas.test.ts diff --git a/packages/bundler/contracts/BundlerHelper.sol b/packages/bundler/contracts/BundlerHelper.sol deleted file mode 100644 index 13b281ab..00000000 --- a/packages/bundler/contracts/BundlerHelper.sol +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.15; - -import "@account-abstraction/contracts/interfaces/IEntryPoint.sol"; - -contract GetUserOpHashes { - error UserOpHashesResult(bytes32[] userOpHashes); - - constructor(IEntryPoint entryPoint, PackedUserOperation[] memory userOps) { - revert UserOpHashesResult( - getUserOpHashes(entryPoint, userOps)); - } - - function getUserOpHashes(IEntryPoint entryPoint, PackedUserOperation[] memory userOps) public view returns (bytes32[] memory ret) { - ret = new bytes32[](userOps.length); - for (uint i = 0; i < userOps.length; i++) { - ret[i] = entryPoint.getUserOpHash(userOps[i]); - } - } -} diff --git a/packages/bundler/localconfig/bundler.config.json b/packages/bundler/localconfig/bundler.config.json index f798cd6f..e59bd70d 100644 --- a/packages/bundler/localconfig/bundler.config.json +++ b/packages/bundler/localconfig/bundler.config.json @@ -1,4 +1,5 @@ { + "chainId": 1337, "gasFactor": "1", "port": "3000", "privateApiPort": "3001", @@ -8,8 +9,8 @@ "minBalance": "1", "mnemonic": "./localconfig/mnemonic.txt", "maxBundleGas": 5e6, - "minStake": "1" , - "minUnstakeDelay": 0 , + "minStake": "1", + "minUnstakeDelay": 0, "autoBundleInterval": 3, "autoBundleMempoolSize": 10, "rip7560": false, diff --git a/packages/bundler/localconfig/bundler.rip7560.config.json b/packages/bundler/localconfig/bundler.rip7560.config.json index 8db4570e..8bfc823b 100644 --- a/packages/bundler/localconfig/bundler.rip7560.config.json +++ b/packages/bundler/localconfig/bundler.rip7560.config.json @@ -1,4 +1,5 @@ { + "chainId": 1337, "gasFactor": "1", "port": "3000", "privateApiPort": "3001", @@ -8,8 +9,8 @@ "minBalance": "1", "mnemonic": "./localconfig/mnemonic.txt", "maxBundleGas": 30e6, - "minStake": "1" , - "minUnstakeDelay": 0 , + "minStake": "1", + "minUnstakeDelay": 0, "autoBundleInterval": 3, "autoBundleMempoolSize": 10, "rip7560": true, diff --git a/packages/bundler/src/BundlerConfig.ts b/packages/bundler/src/BundlerConfig.ts index 131632d4..3938286c 100644 --- a/packages/bundler/src/BundlerConfig.ts +++ b/packages/bundler/src/BundlerConfig.ts @@ -4,7 +4,9 @@ import ow from 'ow' // RIP-7560 EntyPoint address const MIN_UNSTAKE_DELAY = 86400 const MIN_STAKE_VALUE = 1e18.toString() + export interface BundlerConfig { + chainId: number beneficiary: string entryPoint: string gasFactor: string @@ -27,10 +29,21 @@ export interface BundlerConfig { rip7560: boolean rip7560Mode: string gethDevMode: boolean + + // Config overrides for PreVerificationGas calculation + fixedGasOverhead?: number + perUserOpGasOverhead?: number + perUserOpWordGasOverhead?: number + zeroByteGasCost?: number + nonZeroByteGasCost?: number + expectedBundleSize?: number + estimationSignatureSize?: number + estimationPaymasterDataSize?: number } // TODO: implement merging config (args -> config.js -> default) and runtime shape validation export const BundlerConfigShape = { + chainId: ow.number, beneficiary: ow.string, entryPoint: ow.string, gasFactor: ow.string, @@ -52,7 +65,31 @@ export const BundlerConfigShape = { autoBundleMempoolSize: ow.number, rip7560: ow.boolean, rip7560Mode: ow.string.oneOf(['PULL', 'PUSH']), - gethDevMode: ow.boolean + gethDevMode: ow.boolean, + + // Config overrides for PreVerificationGas calculation + fixedGasOverhead: ow.optional.number, + perUserOpGasOverhead: ow.optional.number, + perUserOpWordGasOverhead: ow.optional.number, + zeroByteGasCost: ow.optional.number, + nonZeroByteGasCost: ow.optional.number, + expectedBundleSize: ow.optional.number, + estimationSignatureSize: ow.optional.number, + estimationPaymasterDataSize: ow.optional.number +} + +/** + * Only parameters in this object can be provided by a 'debug_bundler_setConfiguration' API. + */ +export const DebugBundlerConfigShape = { + fixedGasOverhead: ow.optional.number, + perUserOpGasOverhead: ow.optional.number, + perUserOpWordGasOverhead: ow.optional.number, + zeroByteGasCost: ow.optional.number, + nonZeroByteGasCost: ow.optional.number, + expectedBundleSize: ow.optional.number, + estimationSignatureSize: ow.optional.number, + estimationPaymasterDataSize: ow.optional.number } // TODO: consider if we want any default fields at all diff --git a/packages/bundler/src/BundlerServer.ts b/packages/bundler/src/BundlerServer.ts index d98d36f2..d9ca8aed 100644 --- a/packages/bundler/src/BundlerServer.ts +++ b/packages/bundler/src/BundlerServer.ts @@ -198,7 +198,8 @@ export class BundlerServer { } = reqItem debug('>>', { jsonrpc, id, method, params }) try { - const result = deepHexlify(await this.handleMethod(method, params)) + const handleResult = await this.handleMethod(method, params) + const result = deepHexlify(handleResult) debug('sent', method, '-', result) debug('<<', { jsonrpc, id, result }) return { @@ -330,6 +331,12 @@ export class BundlerServer { case 'debug_bundler_getStakeStatus': result = await this.debugHandler.getStakeStatus(params[0], params[1]) break + case 'debug_bundler_setConfiguration': { + const pvgc = await this.debugHandler._setConfiguration(params[0]) + this.methodHandler.preVerificationGasCalculator = pvgc + } + result = {} + break default: throw new RpcError(`Method ${method} is not supported`, -32601) } diff --git a/packages/bundler/src/DebugMethodHandler.ts b/packages/bundler/src/DebugMethodHandler.ts index 8a0b1abf..605fc0d6 100644 --- a/packages/bundler/src/DebugMethodHandler.ts +++ b/packages/bundler/src/DebugMethodHandler.ts @@ -1,9 +1,14 @@ +import ow from 'ow' + +import { StakeInfo } from '@account-abstraction/utils' +import { PreVerificationGasCalculator } from '@account-abstraction/sdk' + +import { BundlerConfig, DebugBundlerConfigShape } from './BundlerConfig' +import { EventsManager } from './modules/EventsManager' import { ExecutionManager } from './modules/ExecutionManager' -import { ReputationDump, ReputationManager } from './modules/ReputationManager' import { MempoolManager } from './modules/MempoolManager' +import { ReputationDump, ReputationManager } from './modules/ReputationManager' import { SendBundleReturn } from './modules/BundleManager' -import { EventsManager } from './modules/EventsManager' -import { StakeInfo } from '@account-abstraction/utils' export class DebugMethodHandler { constructor ( @@ -76,4 +81,9 @@ export class DebugMethodHandler { }> { return await this.repManager.getStakeStatus(address, entryPoint) } + + async _setConfiguration (config: Partial): Promise { + ow.object.exactShape(DebugBundlerConfigShape) + return await this.execManager._setConfiguration(config) + } } diff --git a/packages/bundler/src/MethodHandlerERC4337.ts b/packages/bundler/src/MethodHandlerERC4337.ts index 9ec56677..b1af4518 100644 --- a/packages/bundler/src/MethodHandlerERC4337.ts +++ b/packages/bundler/src/MethodHandlerERC4337.ts @@ -1,26 +1,34 @@ +import debug from 'debug' import { BigNumber, BigNumberish, Signer } from 'ethers' import { JsonRpcProvider, Log } from '@ethersproject/providers' +import { EventFragment } from '@ethersproject/abi' + +import { MainnetConfig, PreVerificationGasCalculator } from '@account-abstraction/sdk' -import { BundlerConfig } from './BundlerConfig' import { + AddressZero, + IEntryPoint, + PackedUserOperation, RpcError, + UserOperation, + UserOperationEventEvent, ValidationErrors, - requireAddressAndFields, - packUserOp, - PackedUserOperation, - unpackUserOp, - simulationRpcParams, - decodeSimulateHandleOpResult, - AddressZero, decodeRevertReason, + decodeSimulateHandleOpResult, + deepHexlify, + erc4337RuntimeVersion, mergeValidationDataValues, - UserOperationEventEvent, IEntryPoint, requireCond, deepHexlify, tostr, erc4337RuntimeVersion - , UserOperation + packUserOp, + requireAddressAndFields, + requireCond, + simulationRpcParams, + tostr, + unpackUserOp } from '@account-abstraction/utils' +import { BundlerConfig } from './BundlerConfig' + import { ExecutionManager } from './modules/ExecutionManager' import { StateOverride, UserOperationByHashResponse, UserOperationReceipt } from './RpcTypes' -import { calcPreVerificationGas } from '@account-abstraction/sdk' -import { EventFragment } from '@ethersproject/abi' export const HEX_REGEX = /^0x[a-fA-F\d]*$/i @@ -58,7 +66,8 @@ export class MethodHandlerERC4337 { readonly provider: JsonRpcProvider, readonly signer: Signer, readonly config: BundlerConfig, - readonly entryPoint: IEntryPoint + readonly entryPoint: IEntryPoint, + public preVerificationGasCalculator: PreVerificationGasCalculator ) { } @@ -143,7 +152,7 @@ export class MethodHandlerERC4337 { } = returnInfo // todo: use simulateHandleOp for this too... - const callGasLimit = await this.provider.estimateGas({ + let callGasLimit = await this.provider.estimateGas({ from: this.entryPoint.address, to: userOp.sender, data: userOp.callData @@ -151,8 +160,10 @@ export class MethodHandlerERC4337 { const message = err.message.match(/reason="(.*?)"/)?.at(1) ?? 'execution reverted' throw new RpcError(message, ValidationErrors.UserOperationReverted) }) + // Results from 'estimateGas' assume making a standalone transaction and paying 21'000 gas extra for it + callGasLimit -= MainnetConfig.transactionGasStipend - const preVerificationGas = calcPreVerificationGas(userOp) + const preVerificationGas = this.preVerificationGasCalculator.estimatePreVerificationGas(userOp) const verificationGasLimit = BigNumber.from(preOpGas).toNumber() return { preVerificationGas, @@ -166,7 +177,7 @@ export class MethodHandlerERC4337 { async sendUserOperation (userOp: UserOperation, entryPointInput: string): Promise { await this._validateParameters(userOp, entryPointInput) - console.log(`UserOperation: Sender=${userOp.sender} Nonce=${tostr(userOp.nonce)} EntryPoint=${entryPointInput} Paymaster=${userOp.paymaster ?? ''}`) + debug(`UserOperation: Sender=${userOp.sender} Nonce=${tostr(userOp.nonce)} EntryPoint=${entryPointInput} Paymaster=${userOp.paymaster ?? ''}`) await this.execManager.sendUserOperation(userOp, entryPointInput, false) return await this.entryPoint.getUserOpHash(packUserOp(userOp)) } diff --git a/packages/bundler/src/modules/BundleManager.ts b/packages/bundler/src/modules/BundleManager.ts index 3b3dc191..219de843 100644 --- a/packages/bundler/src/modules/BundleManager.ts +++ b/packages/bundler/src/modules/BundleManager.ts @@ -21,11 +21,10 @@ import { ValidationErrors, mergeStorageMap, packUserOp, - runContractScript + getUserOpHash } from '@account-abstraction/utils' import { EventsManager } from './EventsManager' -import { GetUserOpHashes__factory } from '../types' import { IBundleManager } from './IBundleManager' import { MempoolEntry } from './MempoolEntry' import { MempoolManager } from './MempoolManager' @@ -373,11 +372,8 @@ export class BundleManager implements IBundleManager { // helper function to get hashes of all UserOps async getUserOpHashes (userOps: UserOperation[]): Promise { - const { userOpHashes } = await runContractScript(this.entryPoint.provider, - new GetUserOpHashes__factory(), - [this.entryPoint.address, userOps.map(packUserOp)]) - - return userOpHashes + const network = await this.entryPoint.provider.getNetwork() + return userOps.map(it => getUserOpHash(it, this.entryPoint.address, network.chainId)) } async getPaymasterBalance (paymaster: string): Promise { diff --git a/packages/bundler/src/modules/ExecutionManager.ts b/packages/bundler/src/modules/ExecutionManager.ts index b0168074..5f28e1a0 100644 --- a/packages/bundler/src/modules/ExecutionManager.ts +++ b/packages/bundler/src/modules/ExecutionManager.ts @@ -9,10 +9,12 @@ import { ReputationManager } from './ReputationManager' import { IBundleManager } from './IBundleManager' import { EmptyValidateUserOpResult, - IValidationManager + IValidationManager, ValidationManager } from '@account-abstraction/validation-manager' import { DepositManager } from './DepositManager' import { BigNumberish, Signer } from 'ethers' +import { BundlerConfig } from '../BundlerConfig' +import { PreVerificationGasCalculator } from '@account-abstraction/sdk' const debug = Debug('aa.exec') @@ -30,7 +32,7 @@ export class ExecutionManager { constructor (private readonly reputationManager: ReputationManager, private readonly mempoolManager: MempoolManager, private readonly bundleManager: IBundleManager, - private readonly validationManager: IValidationManager, + private validationManager: IValidationManager, private readonly depositManager: DepositManager, private readonly signer: Signer, private readonly rip7560: boolean, @@ -143,4 +145,15 @@ export class ExecutionManager { ): Promise<[OperationBase[], StorageMap]> { return await this.bundleManager.createBundle(minBaseFee, maxBundleGas, maxBundleSize) } + + async _setConfiguration (configOverrides: Partial): Promise { + const { configuration, entryPoint, unsafe } = this.validationManager._getDebugConfiguration() + const pvgc = new PreVerificationGasCalculator(Object.assign({}, configuration, configOverrides)) + this.validationManager = new ValidationManager( + entryPoint, + unsafe, + pvgc + ) + return pvgc + } } diff --git a/packages/bundler/src/modules/initServer.ts b/packages/bundler/src/modules/initServer.ts index 590aba6a..d51fec68 100644 --- a/packages/bundler/src/modules/initServer.ts +++ b/packages/bundler/src/modules/initServer.ts @@ -20,6 +20,7 @@ import { BundleManagerRIP7560 } from './BundleManagerRIP7560' import { IBundleManager } from './IBundleManager' import { DepositManager } from './DepositManager' import { IRip7560StakeManager__factory } from '@account-abstraction/utils/dist/src/types' +import { PreVerificationGasCalculator, ChainConfigs } from '@account-abstraction/sdk' /** * initialize server modules. @@ -27,15 +28,17 @@ import { IRip7560StakeManager__factory } from '@account-abstraction/utils/dist/s * @param config * @param signer */ -export function initServer (config: BundlerConfig, signer: Signer): [ExecutionManager, EventsManager, ReputationManager, MempoolManager] { +export function initServer (config: BundlerConfig, signer: Signer): [ExecutionManager, EventsManager, ReputationManager, MempoolManager, PreVerificationGasCalculator] { const entryPoint = IEntryPoint__factory.connect(config.entryPoint, signer) const reputationManager = new ReputationManager(getNetworkProvider(config.network), BundlerReputationParams, parseEther(config.minStake), config.minUnstakeDelay) const mempoolManager = new MempoolManager(reputationManager) const eventsManager = new EventsManager(entryPoint, mempoolManager, reputationManager) + const mergedPvgcConfig = Object.assign({}, ChainConfigs[config.chainId] ?? {}, config) + const preVerificationGasCalculator = new PreVerificationGasCalculator(mergedPvgcConfig) let validationManager: IValidationManager let bundleManager: IBundleManager if (!config.rip7560) { - validationManager = new ValidationManager(entryPoint, config.unsafe) + validationManager = new ValidationManager(entryPoint, config.unsafe, preVerificationGasCalculator) bundleManager = new BundleManager(entryPoint, entryPoint.provider as JsonRpcProvider, signer, eventsManager, mempoolManager, validationManager, reputationManager, config.beneficiary, parseEther(config.minBalance), config.maxBundleGas, config.conditionalRpc) } else { @@ -52,5 +55,5 @@ export function initServer (config: BundlerConfig, signer: Signer): [ExecutionMa if (config.rip7560 && config.rip7560Mode === 'PUSH') { executionManager.setAutoBundler(config.autoBundleInterval, config.autoBundleMempoolSize) } - return [executionManager, eventsManager, reputationManager, mempoolManager] + return [executionManager, eventsManager, reputationManager, mempoolManager, preVerificationGasCalculator] } diff --git a/packages/bundler/src/runBundler.ts b/packages/bundler/src/runBundler.ts index 0e0c918b..d12494f2 100644 --- a/packages/bundler/src/runBundler.ts +++ b/packages/bundler/src/runBundler.ts @@ -159,14 +159,21 @@ export async function runBundler (argv: string[], overrideExit = true): Promise< execManagerConfig.autoBundleInterval = 0 } - const [execManager, eventsManager, reputationManager, mempoolManager] = initServer(execManagerConfig, wallet) + const [ + execManager, + eventsManager, + reputationManager, + mempoolManager, + preVerificationGasCalculator + ] = initServer(execManagerConfig, wallet) const methodHandler = new MethodHandlerERC4337( execManager, provider, wallet, config, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - entryPoint! + entryPoint!, + preVerificationGasCalculator ) const methodHandlerRip7560 = new MethodHandlerRIP7560( execManager, diff --git a/packages/bundler/src/runner/runop.ts b/packages/bundler/src/runner/runop.ts index 505ca4d4..8418e4c4 100644 --- a/packages/bundler/src/runner/runop.ts +++ b/packages/bundler/src/runner/runop.ts @@ -63,10 +63,7 @@ class Runner { entryPointAddress: this.entryPointAddress, factoryAddress: accountDeployer, owner: this.accountOwner, - index: this.index, - overheads: { - // perUserOp: 100000 - } + index: this.index }) return this } diff --git a/packages/bundler/test/BundlerManager.test.ts b/packages/bundler/test/BundlerManager.test.ts index f6336f31..6e64604e 100644 --- a/packages/bundler/test/BundlerManager.test.ts +++ b/packages/bundler/test/BundlerManager.test.ts @@ -11,6 +11,7 @@ import { UserOperation, deployEntryPoint, IEntryPoint, DeterministicDeployer } from '@account-abstraction/utils' +import { PreVerificationGasCalculator, MainnetConfig } from '@account-abstraction/sdk' import { ValidationManager, supportsDebugTraceCall } from '@account-abstraction/validation-manager' import { MempoolManager } from '../src/modules/MempoolManager' @@ -36,6 +37,7 @@ describe('#BundlerManager', () => { DeterministicDeployer.init(provider) const config: BundlerConfig = { + chainId: 1337, beneficiary: await signer.getAddress(), entryPoint: entryPoint.address, gasFactor: '0.2', @@ -59,7 +61,8 @@ describe('#BundlerManager', () => { const repMgr = new ReputationManager(provider, BundlerReputationParams, parseEther(config.minStake), config.minUnstakeDelay) const mempoolMgr = new MempoolManager(repMgr) - const validMgr = new ValidationManager(entryPoint, config.unsafe) + const preVerificationGasCalculator = new PreVerificationGasCalculator(MainnetConfig) + const validMgr = new ValidationManager(entryPoint, config.unsafe, preVerificationGasCalculator) const evMgr = new EventsManager(entryPoint, mempoolMgr, repMgr) bm = new BundleManager(entryPoint, entryPoint.provider as JsonRpcProvider, entryPoint.signer, evMgr, mempoolMgr, validMgr, repMgr, config.beneficiary, parseEther(config.minBalance), config.maxBundleGas, config.conditionalRpc) }) @@ -90,6 +93,7 @@ describe('#BundlerManager', () => { const bundlerSigner = await createSigner() const _entryPoint = entryPoint.connect(bundlerSigner) const config: BundlerConfig = { + chainId: 1337, beneficiary: await bundlerSigner.getAddress(), entryPoint: _entryPoint.address, gasFactor: '0.2', @@ -112,7 +116,8 @@ describe('#BundlerManager', () => { } const repMgr = new ReputationManager(provider, BundlerReputationParams, parseEther(config.minStake), config.minUnstakeDelay) const mempoolMgr = new MempoolManager(repMgr) - const validMgr = new ValidationManager(_entryPoint, config.unsafe) + const preVerificationGasCalculator = new PreVerificationGasCalculator(MainnetConfig) + const validMgr = new ValidationManager(_entryPoint, config.unsafe, preVerificationGasCalculator) const evMgr = new EventsManager(_entryPoint, mempoolMgr, repMgr) bundleMgr = new BundleManager(_entryPoint, _entryPoint.provider as JsonRpcProvider, _entryPoint.signer, evMgr, mempoolMgr, validMgr, repMgr, config.beneficiary, parseEther(config.minBalance), config.maxBundleGas, false) const depositManager = new DepositManager(entryPoint, mempoolMgr, bundleMgr) @@ -124,7 +129,8 @@ describe('#BundlerManager', () => { provider, bundlerSigner, config, - _entryPoint + _entryPoint, + preVerificationGasCalculator ) }) diff --git a/packages/bundler/test/BundlerServer.test.ts b/packages/bundler/test/BundlerServer.test.ts index b7207625..5eda1c51 100644 --- a/packages/bundler/test/BundlerServer.test.ts +++ b/packages/bundler/test/BundlerServer.test.ts @@ -11,6 +11,7 @@ import { deployEntryPoint } from '@account-abstraction/utils' import { supportsDebugTraceCall, ValidationManager } from '@account-abstraction/validation-manager' +import { PreVerificationGasCalculator, MainnetConfig } from '@account-abstraction/sdk' import { BundlerServer } from '../src/BundlerServer' import { createSigner } from './testUtils' @@ -32,6 +33,7 @@ describe('BundleServer', function () { entryPoint = await deployEntryPoint(provider) const config: BundlerConfig = { + chainId: 1337, beneficiary: await signer.getAddress(), entryPoint: entryPoint.address, gasFactor: '0.2', @@ -55,7 +57,8 @@ describe('BundleServer', function () { const repMgr = new ReputationManager(provider, BundlerReputationParams, parseEther(config.minStake), config.minUnstakeDelay) const mempoolMgr = new MempoolManager(repMgr) - const validMgr = new ValidationManager(entryPoint, config.unsafe) + const preVerificationGasCalculator = new PreVerificationGasCalculator(MainnetConfig) + const validMgr = new ValidationManager(entryPoint, config.unsafe, preVerificationGasCalculator) const evMgr = new EventsManager(entryPoint, mempoolMgr, repMgr) const bundleMgr = new BundleManager(entryPoint, entryPoint.provider as JsonRpcProvider, entryPoint.signer, evMgr, mempoolMgr, validMgr, repMgr, config.beneficiary, parseEther(config.minBalance), config.maxBundleGas, false) const depositManager = new DepositManager(entryPoint, mempoolMgr, bundleMgr) @@ -65,7 +68,8 @@ describe('BundleServer', function () { provider, signer, config, - entryPoint + entryPoint, + preVerificationGasCalculator ) const None: any = {} server = new BundlerServer(methodHandler, None, None, None, None, None) @@ -97,7 +101,7 @@ describe('BundleServer', function () { callData: '0x', callGasLimit: 1e6, verificationGasLimit: 1e6, - preVerificationGas: 50000, + preVerificationGas: 60000, maxFeePerGas: 1e6, maxPriorityFeePerGas: 1e6, signature: '0x' diff --git a/packages/bundler/test/DebugMethodHandler.test.ts b/packages/bundler/test/DebugMethodHandler.test.ts index af63e624..3626e315 100644 --- a/packages/bundler/test/DebugMethodHandler.test.ts +++ b/packages/bundler/test/DebugMethodHandler.test.ts @@ -1,5 +1,5 @@ import { ethers } from 'hardhat' -import { SimpleAccountAPI } from '@account-abstraction/sdk' +import { MainnetConfig, PreVerificationGasCalculator, SimpleAccountAPI } from '@account-abstraction/sdk' import { Signer, Wallet } from 'ethers' import { parseEther } from 'ethers/lib/utils' import { expect } from 'chai' @@ -43,6 +43,7 @@ describe('#DebugMethodHandler', () => { DeterministicDeployer.init(provider) const config: BundlerConfig = { + chainId: 1337, beneficiary: await signer.getAddress(), entryPoint: entryPoint.address, gasFactor: '0.2', @@ -66,7 +67,8 @@ describe('#DebugMethodHandler', () => { const repMgr = new ReputationManager(provider, BundlerReputationParams, parseEther(config.minStake), config.minUnstakeDelay) const mempoolMgr = new MempoolManager(repMgr) - const validMgr = new ValidationManager(entryPoint, config.unsafe) + const preVerificationGasCalculator = new PreVerificationGasCalculator(MainnetConfig) + const validMgr = new ValidationManager(entryPoint, config.unsafe, preVerificationGasCalculator) const eventsManager = new EventsManager(entryPoint, mempoolMgr, repMgr) const bundleMgr = new BundleManager(entryPoint, entryPoint.provider as JsonRpcProvider, entryPoint.signer, eventsManager, mempoolMgr, validMgr, repMgr, config.beneficiary, parseEther(config.minBalance), config.maxBundleGas, false) @@ -77,7 +79,8 @@ describe('#DebugMethodHandler', () => { provider, signer, config, - entryPoint + entryPoint, + preVerificationGasCalculator ) debugMethodHandler = new DebugMethodHandler(execManager, eventsManager, repMgr, mempoolMgr) diff --git a/packages/bundler/test/UserOpMethodHandler.test.ts b/packages/bundler/test/UserOpMethodHandler.test.ts index 73bfe19c..f8b396cf 100644 --- a/packages/bundler/test/UserOpMethodHandler.test.ts +++ b/packages/bundler/test/UserOpMethodHandler.test.ts @@ -6,7 +6,7 @@ import { BundlerConfig } from '../src/BundlerConfig' import { toHex } from 'hardhat/internal/util/bigint' import { Signer, Wallet } from 'ethers' -import { SimpleAccountAPI } from '@account-abstraction/sdk' +import { MainnetConfig, PreVerificationGasCalculator, SimpleAccountAPI } from '@account-abstraction/sdk' import { postExecutionDump } from '@account-abstraction/utils/dist/src/postExecCheck' import { SampleRecipient, @@ -61,6 +61,7 @@ describe('UserOpMethodHandler', function () { sampleRecipient = await sampleRecipientFactory.deploy() const config: BundlerConfig = { + chainId: 1337, beneficiary: await signer.getAddress(), entryPoint: entryPoint.address, gasFactor: '0.2', @@ -84,7 +85,8 @@ describe('UserOpMethodHandler', function () { const repMgr = new ReputationManager(provider, BundlerReputationParams, parseEther(config.minStake), config.minUnstakeDelay) mempoolMgr = new MempoolManager(repMgr) - const validMgr = new ValidationManager(entryPoint, config.unsafe) + const preVerificationGasCalculator = new PreVerificationGasCalculator(MainnetConfig) + const validMgr = new ValidationManager(entryPoint, config.unsafe, preVerificationGasCalculator) const evMgr = new EventsManager(entryPoint, mempoolMgr, repMgr) const bundleMgr = new BundleManager(entryPoint, entryPoint.provider as JsonRpcProvider, entryPoint.signer, evMgr, mempoolMgr, validMgr, repMgr, config.beneficiary, parseEther(config.minBalance), config.maxBundleGas, false) const depositManager = new DepositManager(entryPoint, mempoolMgr, bundleMgr) @@ -94,7 +96,8 @@ describe('UserOpMethodHandler', function () { provider, signer, config, - entryPoint + entryPoint, + preVerificationGasCalculator ) }) @@ -137,7 +140,7 @@ describe('UserOpMethodHandler', function () { // execution should be quite low. // (NOTE: actual execution should revert: it only succeeds because the wallet is NOT deployed yet, // and estimation doesn't perform full deploy-validate-execute cycle) - expect(ret.callGasLimit).to.be.closeTo(25000, 10000) + expect(ret.callGasLimit).to.be.closeTo(1000, 50) }) it('estimateUserOperationGas should estimate using state overrides', async function () { @@ -272,13 +275,13 @@ describe('UserOpMethodHandler', function () { provider, entryPointAddress: entryPoint.address, accountAddress, - owner: accountSigner, - overheads: { perUserOp: 0 } + owner: accountSigner }) const op = await api.createSignedUserOp({ data: sampleRecipient.interface.encodeFunctionData('something', [helloWorld]), target: sampleRecipient.address }) + op.preVerificationGas = 1000 try { await methodHandler.sendUserOperation(await resolveHexlify(op), entryPoint.address) throw new Error('expected to revert') diff --git a/packages/bundler/test/ValidateManager.test.ts b/packages/bundler/test/ValidateManager.test.ts index aca75487..129869bf 100644 --- a/packages/bundler/test/ValidateManager.test.ts +++ b/packages/bundler/test/ValidateManager.test.ts @@ -15,6 +15,7 @@ import { checkRulesViolations, supportsDebugTraceCall } from '@account-abstraction/validation-manager' +import { PreVerificationGasCalculator, MainnetConfig } from '@account-abstraction/sdk' import { TestCoin, @@ -58,12 +59,16 @@ describe('#ValidationManager', () => { let rulesAccount: TestRulesAccount let storageAccount: TestStorageAccount - async function testUserOp (validateRule: string = '', pmRule?: string, initFunc?: string, factoryAddress = opcodeFactory.address): Promise { + async function testUserOp (validateRule: string = '', pmRule?: string, initFunc?: string, factoryAddress = opcodeFactory.address): Promise { const userOp = await createTestUserOp(validateRule, pmRule, initFunc, factoryAddress) return { userOp, ...await vm.validateUserOp(userOp) } } - async function testExistingUserOp (validateRule: string = '', pmRule = ''): Promise { + async function testExistingUserOp (validateRule: string = '', pmRule = ''): Promise { const userOp = await existingStorageAccountUserOp(validateRule, pmRule) return { userOp, ...await vm.validateUserOp(userOp) } } @@ -148,7 +153,8 @@ describe('#ValidationManager', () => { await entryPoint.depositTo(rulesAccount.address, { value: parseEther('1') }) const unsafe = !await supportsDebugTraceCall(provider, false) - vm = new ValidationManager(entryPoint, unsafe) + const preVerificationGasCalculator = new PreVerificationGasCalculator(MainnetConfig) + vm = new ValidationManager(entryPoint, unsafe, preVerificationGasCalculator) if (!await supportsDebugTraceCall(ethers.provider, false)) { console.log('WARNING: opcode banning tests can only run with geth') diff --git a/packages/sdk/src/BaseAccountAPI.ts b/packages/sdk/src/BaseAccountAPI.ts index d990a72e..de5e2c10 100644 --- a/packages/sdk/src/BaseAccountAPI.ts +++ b/packages/sdk/src/BaseAccountAPI.ts @@ -5,7 +5,6 @@ import { TransactionDetailsForUserOp } from './TransactionDetailsForUserOp' import { defaultAbiCoder } from 'ethers/lib/utils' import { PaymasterAPI } from './PaymasterAPI' import { encodeUserOp, getUserOpHash, IEntryPoint, IEntryPoint__factory, UserOperation } from '@account-abstraction/utils' -import { calcPreVerificationGas, GasOverheads } from './calcPreVerificationGas' export interface FactoryParams { factory: string @@ -16,7 +15,6 @@ export interface BaseApiParams { provider: Provider entryPointAddress: string accountAddress?: string - overheads?: Partial paymasterAPI?: PaymasterAPI } @@ -45,7 +43,6 @@ export abstract class BaseAccountAPI { private readonly entryPointView: IEntryPoint provider: Provider - overheads?: Partial entryPointAddress: string accountAddress?: string paymasterAPI?: PaymasterAPI @@ -56,7 +53,6 @@ export abstract class BaseAccountAPI { */ protected constructor (params: BaseApiParams) { this.provider = params.provider - this.overheads = params.overheads this.entryPointAddress = params.entryPointAddress this.accountAddress = params.accountAddress this.paymasterAPI = params.paymasterAPI @@ -152,14 +148,6 @@ export abstract class BaseAccountAPI { return 100000 } - /** - * should cover cost of putting calldata on-chain, and some overhead. - * actual overhead depends on the expected bundle size - */ - async getPreVerificationGas (userOp: Partial): Promise { - return calcPreVerificationGas(userOp, this.overheads) - } - /** * ABI-encode a user operation. used for calldata cost estimation */ @@ -278,7 +266,7 @@ export abstract class BaseAccountAPI { } return { ...partialUserOp, - preVerificationGas: await this.getPreVerificationGas(partialUserOp), + preVerificationGas: 60000, signature: '' } } diff --git a/packages/sdk/src/PreVerificationGasCalculator.ts b/packages/sdk/src/PreVerificationGasCalculator.ts new file mode 100644 index 00000000..9a6d87c9 --- /dev/null +++ b/packages/sdk/src/PreVerificationGasCalculator.ts @@ -0,0 +1,119 @@ +import { encodeUserOp, packUserOp, UserOperation } from '@account-abstraction/utils' +import { arrayify, hexlify } from 'ethers/lib/utils' + +export interface PreVerificationGasCalculatorConfig { + /** + * Cost of sending a basic transaction on the current chain. + */ + readonly transactionGasStipend: number + /** + * Gas overhead is added to entire 'handleOp' bundle. + */ + readonly fixedGasOverhead: number + /** + * Gas overhead per UserOperation is added on top of the above fixed per-bundle. + */ + readonly perUserOpGasOverhead: number + /** + * Gas overhead per single "word" (32 bytes) of an ABI-encoding of the UserOperation. + */ + readonly perUserOpWordGasOverhead: number + /** + * The gas cost of a single zero byte an ABI-encoding of the UserOperation. + */ + readonly zeroByteGasCost: number + /** + * The gas cost of a single zero byte an ABI-encoding of the UserOperation. + */ + readonly nonZeroByteGasCost: number + /** + * The expected average size of a bundle in current network conditions. + * This value is used to split the bundle gas overhead between all ops. + */ + readonly expectedBundleSize: number + /** + * The size of the dummy 'signature' parameter to be used during estimation. + */ + readonly estimationSignatureSize: number + /** + * The size of the dummy 'paymasterData' parameter to be used during estimation. + */ + readonly estimationPaymasterDataSize: number +} + +export const MainnetConfig: PreVerificationGasCalculatorConfig = { + transactionGasStipend: 21000, + fixedGasOverhead: 38000, + perUserOpGasOverhead: 11000, + perUserOpWordGasOverhead: 4, + zeroByteGasCost: 4, + nonZeroByteGasCost: 16, + expectedBundleSize: 1, + estimationSignatureSize: 65, + estimationPaymasterDataSize: 0 +} + +export const ChainConfigs: { [key: number]: PreVerificationGasCalculatorConfig } = { + 1: MainnetConfig, + 1337: MainnetConfig +} + +export class PreVerificationGasCalculator { + constructor ( + readonly config: PreVerificationGasCalculatorConfig + ) {} + + /** + * When accepting a UserOperation from a user to a mempool bundler validates the amount of 'preVerificationGas'. + * If the proposed value is lower that the one expected by the bundler the UserOperation may not be profitable. + * Notice that in order to participate in a P2P UserOperations mempool all bundlers must use the same configuration. + * @param userOp - the complete and signed UserOperation received from the user. + */ + validatePreVerificationGas ( + userOp: UserOperation + ): { isPreVerificationGasValid: boolean, minRequiredPreVerificationGas: number } { + const minRequiredPreVerificationGas = this._calculate(userOp) + return { + minRequiredPreVerificationGas, + isPreVerificationGasValid: minRequiredPreVerificationGas <= parseInt((userOp.preVerificationGas as any).toString()) + } + } + + /** + * While filling the partial UserOperation bundler estimate the 'preVerificationGas' necessary for it to be accepted. + * Value of the 'preVerificationGas' is the cost overhead that cannot be calculated precisely or accessed on-chain. + * It depends on blockchain parameters that are defined by the protocol for all transactions. + * @param userOp - the UserOperation object that may be missing the 'signature' and 'paymasterData' fields. + */ + estimatePreVerificationGas ( + userOp: Partial + ): number { + const filledUserOp = this._fillUserOpWithDummyData(userOp) + return this._calculate(filledUserOp) + } + + _calculate (userOp: UserOperation): number { + const packedUserOp = arrayify(encodeUserOp(packUserOp(userOp), false)) + const userOpWordsLength = (packedUserOp.length + 31) / 32 + const callDataCost = packedUserOp + .map( + x => x === 0 ? this.config.zeroByteGasCost : this.config.nonZeroByteGasCost) + .reduce( + (sum, x) => sum + x + ) + const userOpDataWordsOverhead = userOpWordsLength * this.config.perUserOpWordGasOverhead + + const userOpSpecificOverhead = callDataCost + userOpDataWordsOverhead + this.config.perUserOpGasOverhead + const userOpShareOfBundleCost = this.config.fixedGasOverhead / this.config.expectedBundleSize + + return Math.round(userOpSpecificOverhead + userOpShareOfBundleCost) + } + + _fillUserOpWithDummyData (userOp: Partial): UserOperation { + const filledUserOp: UserOperation = Object.assign({}, userOp) as UserOperation + filledUserOp.preVerificationGas = filledUserOp.preVerificationGas ?? 21000 + filledUserOp.signature = filledUserOp.signature ?? hexlify(Buffer.alloc(this.config.estimationSignatureSize, 0xff)) + filledUserOp.paymasterData = filledUserOp.paymasterData ?? hexlify(Buffer.alloc(this.config.estimationPaymasterDataSize, 0xff)) + return filledUserOp + } +} diff --git a/packages/sdk/src/calcPreVerificationGas.ts b/packages/sdk/src/calcPreVerificationGas.ts deleted file mode 100644 index 98c90c3c..00000000 --- a/packages/sdk/src/calcPreVerificationGas.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { encodeUserOp, packUserOp, UserOperation } from '@account-abstraction/utils' -import { arrayify, hexlify } from 'ethers/lib/utils' - -export interface GasOverheads { - /** - * fixed overhead for entire handleOp bundle. - */ - fixed: number - - /** - * per userOp overhead, added on top of the above fixed per-bundle. - */ - perUserOp: number - - /** - * overhead for userOp word (32 bytes) block - */ - perUserOpWord: number - - // perCallDataWord: number - - /** - * zero byte cost, for calldata gas cost calculations - */ - zeroByte: number - - /** - * non-zero byte cost, for calldata gas cost calculations - */ - nonZeroByte: number - - /** - * expected bundle size, to split per-bundle overhead between all ops. - */ - bundleSize: number - - /** - * expected length of the userOp signature. - */ - sigSize: number -} - -export const DefaultGasOverheads: GasOverheads = { - fixed: 21000, - perUserOp: 18300, - perUserOpWord: 4, - zeroByte: 4, - nonZeroByte: 16, - bundleSize: 1, - sigSize: 65 -} - -/** - * calculate the preVerificationGas of the given UserOperation - * preVerificationGas (by definition) is the cost overhead that can't be calculated on-chain. - * it is based on parameters that are defined by the Ethereum protocol for external transactions. - * @param userOp filled userOp to calculate. The only possible missing fields can be the signature and preVerificationGas itself - * @param overheads gas overheads to use, to override the default values - */ -export function calcPreVerificationGas (userOp: Partial, overheads?: Partial): number { - const ov = { ...DefaultGasOverheads, ...(overheads ?? {}) } - const p: UserOperation = { - // dummy values, in case the UserOp is incomplete. - preVerificationGas: 21000, // dummy value, just for calldata cost - signature: hexlify(Buffer.alloc(ov.sigSize, 1)), // dummy signature - ...userOp - } as any - - const packed = arrayify(encodeUserOp(packUserOp(p), false)) - const lengthInWord = (packed.length + 31) / 32 - const callDataCost = packed.map(x => x === 0 ? ov.zeroByte : ov.nonZeroByte).reduce((sum, x) => sum + x) - const ret = Math.round( - callDataCost + - ov.fixed / ov.bundleSize + - ov.perUserOp + - ov.perUserOpWord * lengthInWord - ) - return ret -} diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index f86de66d..8a7572bf 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -6,4 +6,6 @@ export { ERC4337EthersSigner } from './ERC4337EthersSigner' export { ERC4337EthersProvider } from './ERC4337EthersProvider' export { ClientConfig } from './ClientConfig' export { HttpRpcClient } from './HttpRpcClient' -export * from './calcPreVerificationGas' +export { + PreVerificationGasCalculator, PreVerificationGasCalculatorConfig, ChainConfigs, MainnetConfig +} from './PreVerificationGasCalculator' diff --git a/packages/sdk/test/4-calcPreVerificationGas.test.ts b/packages/sdk/test/4-calcPreVerificationGas.test.ts deleted file mode 100644 index 759bd41e..00000000 --- a/packages/sdk/test/4-calcPreVerificationGas.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { expect } from 'chai' -import { hexlify } from 'ethers/lib/utils' -import { calcPreVerificationGas } from '../src/calcPreVerificationGas' - -describe('#calcPreVerificationGas', () => { - const userOp = { - sender: '0x'.padEnd(42, '1'), - nonce: 0, - initCode: '0x3333', - callData: '0x4444', - callGasLimit: 5, - verificationGasLimit: 6, - maxFeePerGas: 8, - maxPriorityFeePerGas: 9, - paymasterAndData: '0xaaaaaa' - } - - it('returns a gas value proportional to sigSize', async () => { - const pvg1 = calcPreVerificationGas(userOp, { sigSize: 0 }) - const pvg2 = calcPreVerificationGas(userOp, { sigSize: 65 }) - - expect(pvg2).to.be.greaterThan(pvg1) - }) - - it('returns a gas value that ignores sigSize if userOp already signed', async () => { - const userOpWithSig = { - ...userOp, - signature: hexlify(Buffer.alloc(65, 1)) - } - - const pvg1 = calcPreVerificationGas(userOpWithSig, { sigSize: 0 }) - const pvg2 = calcPreVerificationGas(userOpWithSig) - expect(pvg2).to.equal(pvg1) - }) -}) diff --git a/packages/utils/src/DeterministicDeployer.ts b/packages/utils/src/DeterministicDeployer.ts index ea879acc..527c41b8 100644 --- a/packages/utils/src/DeterministicDeployer.ts +++ b/packages/utils/src/DeterministicDeployer.ts @@ -67,7 +67,7 @@ export class DeterministicDeployer { to: DeterministicDeployer.deploymentSignerAddress, value: neededBalance, gasLimit: DeterministicDeployer.deploymentGasLimit - }).then(t=>t.wait()) + }).then(async t => await t.wait()) } const tx = await this.provider.send('eth_sendRawTransaction', [DeterministicDeployer.deploymentTransaction]) await this.provider.waitForTransaction(tx) diff --git a/packages/utils/src/ERC4337Utils.ts b/packages/utils/src/ERC4337Utils.ts index b4149e47..13f2815d 100644 --- a/packages/utils/src/ERC4337Utils.ts +++ b/packages/utils/src/ERC4337Utils.ts @@ -30,7 +30,6 @@ export type NotPromise = { [P in keyof T]: Exclude> } - // todo: remove this wrapper method? export function packAccountGasLimits (validationGasLimit: BigNumberish, callGasLimit: BigNumberish): string { return packUint(validationGasLimit, callGasLimit) diff --git a/packages/utils/src/RIP7560Utils.ts b/packages/utils/src/RIP7560Utils.ts index 4f2201a3..60b391bf 100644 --- a/packages/utils/src/RIP7560Utils.ts +++ b/packages/utils/src/RIP7560Utils.ts @@ -15,7 +15,7 @@ export function getRIP7560TransactionHash (op: OperationRIP7560, forSignature = return keccak256(rlpEncoded) } -function nonZeroAddr(addr?: string): Buffer { +function nonZeroAddr (addr?: string): Buffer { if (addr == null || addr === AddressZero) { return Buffer.from([]) } diff --git a/packages/utils/src/Utils.ts b/packages/utils/src/Utils.ts index b3e3544a..b5cd13d5 100644 --- a/packages/utils/src/Utils.ts +++ b/packages/utils/src/Utils.ts @@ -1,7 +1,7 @@ // misc utilities for the various modules. import { BytesLike, ContractFactory, BigNumber, ethers } from 'ethers' -import { defaultAbiCoder, hexlify, hexZeroPad, Result } from 'ethers/lib/utils' +import { hexlify, hexZeroPad, Result } from 'ethers/lib/utils' import { Provider, JsonRpcProvider } from '@ethersproject/providers' import { BigNumberish } from 'ethers/lib/ethers' @@ -226,7 +226,7 @@ export function getPackedNonce (userOp: OperationBase): BigNumber { // Either not RIP-7560 operation or not using RIP-7712 nonce return BigNumber.from(userOp.nonce) } - const packed = ethers.utils.solidityPack(["uint192", "uint64"], [nonceKey, userOp.nonce]) + const packed = ethers.utils.solidityPack(['uint192', 'uint64'], [nonceKey, userOp.nonce]) const bigNumberNonce = BigNumber.from(packed) return bigNumberNonce } diff --git a/packages/validation-manager/src/IValidationManager.ts b/packages/validation-manager/src/IValidationManager.ts index fae75d72..0f537609 100644 --- a/packages/validation-manager/src/IValidationManager.ts +++ b/packages/validation-manager/src/IValidationManager.ts @@ -1,6 +1,8 @@ -import { OperationBase, ReferencedCodeHashes, StakeInfo, StorageMap } from '@account-abstraction/utils' import { BigNumber, BigNumberish } from 'ethers' +import { IEntryPoint, OperationBase, ReferencedCodeHashes, StakeInfo, StorageMap } from '@account-abstraction/utils' +import { PreVerificationGasCalculatorConfig } from '@account-abstraction/sdk' + /** * result from successful validation */ @@ -52,4 +54,9 @@ export interface IValidationManager { getOperationHash: (userOp: OperationBase) => Promise + _getDebugConfiguration: () => { + configuration: PreVerificationGasCalculatorConfig + entryPoint: IEntryPoint + unsafe: boolean + } } diff --git a/packages/validation-manager/src/ValidationManager.ts b/packages/validation-manager/src/ValidationManager.ts index 723030c5..a4bcc142 100644 --- a/packages/validation-manager/src/ValidationManager.ts +++ b/packages/validation-manager/src/ValidationManager.ts @@ -3,7 +3,7 @@ import { BigNumber } from 'ethers' import { JsonRpcProvider } from '@ethersproject/providers' import Debug from 'debug' -import { calcPreVerificationGas } from '@account-abstraction/sdk' +import { PreVerificationGasCalculator, PreVerificationGasCalculatorConfig } from '@account-abstraction/sdk' import { AddressZero, @@ -47,10 +47,23 @@ const entryPointSimulations = IEntryPointSimulations__factory.createInterface() export class ValidationManager implements IValidationManager { constructor ( readonly entryPoint: IEntryPoint, - readonly unsafe: boolean + readonly unsafe: boolean, + readonly preVerificationGasCalculator: PreVerificationGasCalculator ) { } + _getDebugConfiguration (): { + configuration: PreVerificationGasCalculatorConfig + entryPoint: IEntryPoint + unsafe: boolean + } { + return { + configuration: this.preVerificationGasCalculator.config, + entryPoint: this.entryPoint, + unsafe: this.unsafe + } + } + parseValidationResult (userOp: UserOperation, res: ValidationResultStructOutput): ValidationResult { const mergedValidation = mergeValidationDataValues(res.returnInfo.accountValidationData, res.returnInfo.paymasterValidationData) @@ -292,10 +305,12 @@ export class ValidationManager implements IValidationManager { requireAddressAndFields(userOp, 'paymaster', ['paymasterPostOpGasLimit', 'paymasterVerificationGasLimit'], ['paymasterData']) requireAddressAndFields(userOp, 'factory', ['factoryData']) - if ((userOp as UserOperation).preVerificationGas != null) { - const calcPreVerificationGas1 = calcPreVerificationGas(userOp) - requireCond(BigNumber.from((userOp as UserOperation).preVerificationGas).gte(calcPreVerificationGas1), - `preVerificationGas too low: expected at least ${calcPreVerificationGas1}`, + const preVerificationGas = (userOp as UserOperation).preVerificationGas + if (preVerificationGas != null) { + const { isPreVerificationGasValid, minRequiredPreVerificationGas } = + this.preVerificationGasCalculator.validatePreVerificationGas(userOp as UserOperation) + requireCond(isPreVerificationGasValid, + `preVerificationGas too low: expected at least ${minRequiredPreVerificationGas}, provided only ${parseInt(preVerificationGas as string)}`, ValidationErrors.InvalidFields) } } diff --git a/packages/validation-manager/src/ValidationManagerRIP7560.ts b/packages/validation-manager/src/ValidationManagerRIP7560.ts index 21c05395..5121555a 100644 --- a/packages/validation-manager/src/ValidationManagerRIP7560.ts +++ b/packages/validation-manager/src/ValidationManagerRIP7560.ts @@ -1,18 +1,19 @@ import { JsonRpcProvider } from '@ethersproject/providers' +import debug from 'debug' +import { isAddress } from 'ethers/lib/utils' import { OperationBase, OperationRIP7560, ReferencedCodeHashes, - getRIP7560TransactionHash, StakeInfo + getRIP7560TransactionHash, StakeInfo, IEntryPoint } from '@account-abstraction/utils' +import { IRip7560StakeManager } from '@account-abstraction/utils/dist/src/types' +import { PreVerificationGasCalculatorConfig } from '@account-abstraction/sdk' import { IValidationManager, ValidateUserOpResult, ValidationResult } from './IValidationManager' import { eth_traceRip7560Validation } from './GethTracer' import { tracerResultParser } from './TracerResultParser' -import debug from 'debug' -import { isAddress } from 'ethers/lib/utils' -import { IRip7560StakeManager } from '@account-abstraction/utils/dist/src/types' export const AA_ENTRY_POINT = '0x0000000000000000000000000000000000007560' export const AA_STAKE_MANAGER = '0x570Aa568b6cf62ff08c6C3a3b3DB1a0438E871Fb' @@ -25,11 +26,23 @@ export class ValidationManagerRIP7560 implements IValidationManager { ) { } + _getDebugConfiguration (): { + configuration: PreVerificationGasCalculatorConfig + entryPoint: IEntryPoint + unsafe: boolean + } { + throw new Error('Method not implemented.') + } + validateInputParameters (_operation: OperationBase, _entryPointInput?: string): void { // TODO } - async _getStakesInfo (operation: OperationBase): Promise<{ senderInfo: StakeInfo, paymasterInfo?: StakeInfo, factoryInfo?: StakeInfo }> { + async _getStakesInfo (operation: OperationBase): Promise<{ + senderInfo: StakeInfo + paymasterInfo?: StakeInfo + factoryInfo?: StakeInfo + }> { const addresses = [operation.sender] let paymasterInfo, factoryInfo if (operation.paymaster != null && isAddress(operation.paymaster)) { diff --git a/packages/validation-manager/src/index.ts b/packages/validation-manager/src/index.ts index 8a8f018a..db703e96 100644 --- a/packages/validation-manager/src/index.ts +++ b/packages/validation-manager/src/index.ts @@ -6,6 +6,7 @@ import { bundlerCollectorTracer } from './BundlerCollectorTracer' import { debug_traceCall, eth_traceRip7560Validation } from './GethTracer' import { ValidateUserOpResult } from './IValidationManager' import { ValidationManager } from './ValidationManager' +import { PreVerificationGasCalculator } from '@account-abstraction/sdk' export * from './ValidationManager' export * from './ValidationManagerRIP7560' @@ -68,7 +69,8 @@ export async function checkRulesViolations ( const entryPoint = IEntryPoint__factory.connect(entryPointAddress, provider) const validationManager = new ValidationManager( entryPoint, - false + false, + Object.assign({}) as PreVerificationGasCalculator ) return await validationManager.validateUserOp(userOperation) } From f4647969386aa859b7edf608467168d140e5f92c Mon Sep 17 00:00:00 2001 From: Alex Forshtat Date: Mon, 21 Oct 2024 16:32:21 +0200 Subject: [PATCH 3/4] AA-446: Update 'rip7560' submodule (#231) * Update 'rip7560' submodule * Print deploed NonceManager address --- packages/utils/src/RIP7712NonceManagerUtils.ts | 1 + submodules/rip7560 | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/utils/src/RIP7712NonceManagerUtils.ts b/packages/utils/src/RIP7712NonceManagerUtils.ts index b5d740be..dd5d49a2 100644 --- a/packages/utils/src/RIP7712NonceManagerUtils.ts +++ b/packages/utils/src/RIP7712NonceManagerUtils.ts @@ -9,6 +9,7 @@ export const entryPointSalt = '0x90d8084deab30c2a37c45e8d47f49f2f7965183cb6990a9 export async function deployNonceManager (provider: JsonRpcProvider, signer = provider.getSigner()): Promise { const addr = await new DeterministicDeployer(provider, signer).deterministicDeploy(nonceManagerByteCode, entryPointSalt) + console.log("Deployed NonceManager contract at: ", addr) return NonceManager__factory.connect(addr, signer) } diff --git a/submodules/rip7560 b/submodules/rip7560 index eae66dc9..b38a294a 160000 --- a/submodules/rip7560 +++ b/submodules/rip7560 @@ -1 +1 @@ -Subproject commit eae66dc9620436369f6316a843eb587941be1fa3 +Subproject commit b38a294a7c35007610d0ce5498f053d2ee2f58e3 From 8bc5028caaf2b380c12e057110f9bba950e49b09 Mon Sep 17 00:00:00 2001 From: Dror Tirosh Date: Wed, 23 Oct 2024 16:40:48 +0300 Subject: [PATCH 4/4] AA-472: Support native tracer (#230) * support native tracer --tracerRpcUrl - provide a geth node that supports the native "bundlerCollectorTracer" native tracer main provider must support "prestateTracer" --------- Co-authored-by: Alex Forshtat --- packages/bundler/src/BundlerConfig.ts | 2 + packages/bundler/src/modules/initServer.ts | 3 +- packages/bundler/src/runBundler.ts | 34 +++++++++++--- packages/utils/src/Utils.ts | 8 +++- packages/validation-manager/src/GethTracer.ts | 47 +++++++++++++++++-- .../src/ValidationManager.ts | 13 ++++- packages/validation-manager/src/index.ts | 13 ++++- 7 files changed, 105 insertions(+), 15 deletions(-) diff --git a/packages/bundler/src/BundlerConfig.ts b/packages/bundler/src/BundlerConfig.ts index 3938286c..22ecc7e0 100644 --- a/packages/bundler/src/BundlerConfig.ts +++ b/packages/bundler/src/BundlerConfig.ts @@ -16,6 +16,7 @@ export interface BundlerConfig { port: string privateApiPort: string unsafe: boolean + tracerRpcUrl?: string debugRpc?: boolean conditionalRpc: boolean @@ -53,6 +54,7 @@ export const BundlerConfigShape = { port: ow.string, privateApiPort: ow.string, unsafe: ow.boolean, + tracerRpcUrl: ow.optional.string, debugRpc: ow.optional.boolean, conditionalRpc: ow.boolean, diff --git a/packages/bundler/src/modules/initServer.ts b/packages/bundler/src/modules/initServer.ts index d51fec68..63145e80 100644 --- a/packages/bundler/src/modules/initServer.ts +++ b/packages/bundler/src/modules/initServer.ts @@ -38,7 +38,8 @@ export function initServer (config: BundlerConfig, signer: Signer): [ExecutionMa let validationManager: IValidationManager let bundleManager: IBundleManager if (!config.rip7560) { - validationManager = new ValidationManager(entryPoint, config.unsafe, preVerificationGasCalculator) + const tracerProvider = config.tracerRpcUrl == null ? undefined : getNetworkProvider(config.tracerRpcUrl) + validationManager = new ValidationManager(entryPoint, config.unsafe, preVerificationGasCalculator, tracerProvider) bundleManager = new BundleManager(entryPoint, entryPoint.provider as JsonRpcProvider, signer, eventsManager, mempoolManager, validationManager, reputationManager, config.beneficiary, parseEther(config.minBalance), config.maxBundleGas, config.conditionalRpc) } else { diff --git a/packages/bundler/src/runBundler.ts b/packages/bundler/src/runBundler.ts index d12494f2..ff5b5bb9 100644 --- a/packages/bundler/src/runBundler.ts +++ b/packages/bundler/src/runBundler.ts @@ -15,7 +15,7 @@ import { MethodHandlerERC4337 } from './MethodHandlerERC4337' import { initServer } from './modules/initServer' import { DebugMethodHandler } from './DebugMethodHandler' -import { supportsDebugTraceCall } from '@account-abstraction/validation-manager' +import { supportsDebugTraceCall, supportsNativeTracer } from '@account-abstraction/validation-manager' import { resolveConfiguration } from './Config' import { bundlerConfigDefault } from './BundlerConfig' import { parseEther } from 'ethers/lib/utils' @@ -80,7 +80,8 @@ export async function runBundler (argv: string[], overrideExit = true): Promise< .option('--privateApiPort ', `server listening port for block builder (default: ${bundlerConfigDefault.privateApiPort})`) .option('--config ', 'path to config file', CONFIG_FILE_NAME) .option('--auto', 'automatic bundling (bypass config.autoBundleMempoolSize)', false) - .option('--unsafe', 'UNSAFE mode: no storage or opcode checks (safe mode requires geth)') + .option('--unsafe', 'UNSAFE mode: no storage or opcode checks (safe mode requires debug_traceCall)') + .option('--tracerRpcUrl ', 'run native tracer on this provider, and prestateTracer native tracer on network provider. requires unsafe=false') .option('--debugRpc', 'enable debug rpc methods (auto-enabled for test node') .option('--conditionalRpc', 'Use eth_sendRawTransactionConditional RPC)') .option('--show-stack-traces', 'Show stack traces.') @@ -130,10 +131,31 @@ export async function runBundler (argv: string[], overrideExit = true): Promise< console.error('FATAL: --conditionalRpc requires a node that support eth_sendRawTransactionConditional') process.exit(1) } - if (!config.unsafe && !await supportsDebugTraceCall(provider as any, config.rip7560)) { - const requiredApi = config.rip7560 ? 'eth_traceRip7560Validation' : 'debug_traceCall' - console.error(`FATAL: full validation requires a node with ${requiredApi}. for local UNSAFE mode: use --unsafe`) - process.exit(1) + if (config.unsafe) { + if (config.tracerRpcUrl != null) { + console.error('FATAL: --unsafe and --tracerRpcUrl are mutually exclusive') + process.exit(1) + } + } else { + if (config.tracerRpcUrl != null) { + // validate standard tracer supports "prestateTracer": + if (!await supportsNativeTracer(provider, 'prestateTracer')) { + console.error('FATAL: --tracerRpcUrl requires the network provider to support prestateTracer') + process.exit(1) + } + const tracerProvider = new ethers.providers.JsonRpcProvider(config.tracerRpcUrl) + if (!await supportsNativeTracer(tracerProvider)) { + console.error('FATAL: --tracerRpcUrl requires a provider to support bundlerCollectorTracer') + process.exit(1) + } + } else { + // check standard javascript tracer: + if (!await supportsDebugTraceCall(provider as any, config.rip7560)) { + const requiredApi = config.rip7560 ? 'eth_traceRip7560Validation' : 'debug_traceCall' + console.error(`FATAL: full validation requires a node with ${requiredApi}. for local UNSAFE mode: use --unsafe`) + process.exit(1) + } + } } if (config.rip7560) { diff --git a/packages/utils/src/Utils.ts b/packages/utils/src/Utils.ts index b5cd13d5..1bde3b0b 100644 --- a/packages/utils/src/Utils.ts +++ b/packages/utils/src/Utils.ts @@ -217,7 +217,13 @@ export function sum (...args: BigNumberish[]): BigNumber { */ export function getUserOpMaxCost (userOp: OperationBase): BigNumber { const preVerificationGas: BigNumberish = (userOp as UserOperation).preVerificationGas - return sum(preVerificationGas ?? 0, userOp.verificationGasLimit, userOp.callGasLimit, userOp.paymasterVerificationGasLimit ?? 0, userOp.paymasterPostOpGasLimit ?? 0).mul(userOp.maxFeePerGas) + return sum( + preVerificationGas ?? 0, + userOp.verificationGasLimit, + userOp.callGasLimit, + userOp.paymasterVerificationGasLimit ?? 0, + userOp.paymasterPostOpGasLimit ?? 0 + ).mul(userOp.maxFeePerGas) } export function getPackedNonce (userOp: OperationBase): BigNumber { diff --git a/packages/validation-manager/src/GethTracer.ts b/packages/validation-manager/src/GethTracer.ts index d3846e11..2c45ee4a 100644 --- a/packages/validation-manager/src/GethTracer.ts +++ b/packages/validation-manager/src/GethTracer.ts @@ -8,6 +8,10 @@ import { OperationRIP7560, RpcError } from '@account-abstraction/utils' const debug = Debug('aa.tracer') +// the name of the native tracer. +// equivalent to the javascript "bundlerCollectorTracer". +export const bundlerNativeTracerName = 'bundlerCollectorTracer' + /** * a function returning a LogTracer. * the function's body must be "{ return {...} }" @@ -17,13 +21,50 @@ const debug = Debug('aa.tracer') */ type LogTracerFunc = () => LogTracer +/** + * trace a transaction using the geth debug_traceCall method. + * @param provider the network node to trace on + * @param tx the transaction to trace + * @param options the trace options + * @param nativeTracerProvider if set, submit only preStateTracer to the network provider, and use this (second) provider with native tracer. + * if null, then use javascript tracer on the first provider. + */ + // eslint-disable-next-line @typescript-eslint/naming-convention -export async function debug_traceCall (provider: JsonRpcProvider, tx: Deferrable, options: TraceOptions): Promise { +export async function debug_traceCall (provider: JsonRpcProvider, tx: Deferrable, options: TraceOptions, nativeTracerProvider?: JsonRpcProvider): Promise { const tx1 = await resolveProperties(tx) const traceOptions = tracer2string(options) + if (nativeTracerProvider != null) { + // there is a nativeTracerProvider: use it for the native tracer, but first we need preStateTracer from the main provider: + const preState: { [addr: string]: any } = await provider.send('debug_traceCall', [tx1, 'latest', { ...traceOptions, tracer: 'prestateTracer' }]) + + // fix prestate to be valid "state overrides" + // - convert nonce's to hex + // - rename storage to state + for (const key in preState) { + if (preState[key]?.nonce != null) { + preState[key].nonce = '0x' + (preState[key].nonce.toString(16) as string) + } + if (preState[key]?.storage != null) { + // rpc expects "state" instead... + preState[key].state = preState[key].storage + delete preState[key].storage + } + } + + const ret = await nativeTracerProvider.send('debug_traceCall', [tx1, 'latest', { + tracer: bundlerNativeTracerName, + stateOverrides: preState + }]) + + return ret + } + const ret = await provider.send('debug_traceCall', [tx1, 'latest', traceOptions]).catch(e => { - debug('ex=', e.error) - debug('tracer=', traceOptions.tracer?.toString().split('\n').map((line, index) => `${index + 1}: ${line}`).join('\n')) + if (debug.enabled) { + debug('ex=', e.error) + debug('tracer=', traceOptions.tracer?.toString().split('\n').map((line, index) => `${index + 1}: ${line}`).join('\n')) + } throw e }) // return applyTracer(ret, options) diff --git a/packages/validation-manager/src/ValidationManager.ts b/packages/validation-manager/src/ValidationManager.ts index a4bcc142..9b228bf1 100644 --- a/packages/validation-manager/src/ValidationManager.ts +++ b/packages/validation-manager/src/ValidationManager.ts @@ -44,11 +44,20 @@ const VALID_UNTIL_FUTURE_SECONDS = 30 const HEX_REGEX = /^0x[a-fA-F\d]*$/i const entryPointSimulations = IEntryPointSimulations__factory.createInterface() +/** + * ValidationManager is responsible for validating UserOperations. + * @param entryPoint - the entryPoint contract + * @param unsafe - if true, skip tracer for validation rules (validate only through eth_call) + * @param preVerificationGasCalculator - helper to calculate the correct 'preVerificationGas' for the current network conditions + * @param providerForTracer - if provided, use it for native bundlerCollectorTracer, and use main provider with "preStateTracer" + * (relevant only if unsafe=false) + */ export class ValidationManager implements IValidationManager { constructor ( readonly entryPoint: IEntryPoint, readonly unsafe: boolean, - readonly preVerificationGasCalculator: PreVerificationGasCalculator + readonly preVerificationGasCalculator: PreVerificationGasCalculator, + readonly providerForTracer?: JsonRpcProvider ) { } @@ -144,7 +153,7 @@ export class ValidationManager implements IValidationManager { code: EntryPointSimulationsJson.deployedBytecode } } - }) + }, this.providerForTracer) const lastResult = tracerResult.calls.slice(-1)[0] const data = (lastResult as ExitInfo).data diff --git a/packages/validation-manager/src/index.ts b/packages/validation-manager/src/index.ts index db703e96..791d53ac 100644 --- a/packages/validation-manager/src/index.ts +++ b/packages/validation-manager/src/index.ts @@ -1,17 +1,26 @@ import { JsonRpcProvider } from '@ethersproject/providers' import { AddressZero, IEntryPoint__factory, OperationRIP7560, UserOperation } from '@account-abstraction/utils' +import { PreVerificationGasCalculator } from '@account-abstraction/sdk' +import { bundlerNativeTracerName, debug_traceCall, eth_traceRip7560Validation } from './GethTracer' import { bundlerCollectorTracer } from './BundlerCollectorTracer' -import { debug_traceCall, eth_traceRip7560Validation } from './GethTracer' import { ValidateUserOpResult } from './IValidationManager' import { ValidationManager } from './ValidationManager' -import { PreVerificationGasCalculator } from '@account-abstraction/sdk' export * from './ValidationManager' export * from './ValidationManagerRIP7560' export * from './IValidationManager' +export async function supportsNativeTracer (provider: JsonRpcProvider, nativeTracer = bundlerNativeTracerName): Promise { + try { + await provider.send('debug_traceCall', [{}, 'latest', { tracer: nativeTracer }]) + return true + } catch (e) { + return false + } +} + export async function supportsDebugTraceCall (provider: JsonRpcProvider, rip7560: boolean): Promise { const p = provider.send as any if (p._clientVersion == null) {