diff --git a/packages/bundler/src/BundlerConfig.ts b/packages/bundler/src/BundlerConfig.ts index 131632d4..fb93be5c 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,17 @@ 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 } // TODO: consider if we want any default fields at all diff --git a/packages/bundler/src/modules/initServer.ts b/packages/bundler/src/modules/initServer.ts index b839198b..d51fec68 100644 --- a/packages/bundler/src/modules/initServer.ts +++ b/packages/bundler/src/modules/initServer.ts @@ -20,7 +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 } from '@account-abstraction/sdk' +import { PreVerificationGasCalculator, ChainConfigs } from '@account-abstraction/sdk' /** * initialize server modules. @@ -33,7 +33,8 @@ export function initServer (config: BundlerConfig, signer: Signer): [ExecutionMa 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 preVerificationGasCalculator = new PreVerificationGasCalculator(0, 0, 0, 0, 0, 0, 0, 0) + const mergedPvgcConfig = Object.assign({}, ChainConfigs[config.chainId] ?? {}, config) + const preVerificationGasCalculator = new PreVerificationGasCalculator(mergedPvgcConfig) let validationManager: IValidationManager let bundleManager: IBundleManager if (!config.rip7560) { diff --git a/packages/bundler/test/BundlerManager.test.ts b/packages/bundler/test/BundlerManager.test.ts index 713b23c8..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' @@ -22,7 +23,6 @@ import { ExecutionManager } from '../src/modules/ExecutionManager' import { EventsManager } from '../src/modules/EventsManager' import { createSigner } from './testUtils' import { DepositManager } from '../src/modules/DepositManager' -import { PreVerificationGasCalculator } from '@account-abstraction/sdk' describe('#BundlerManager', () => { let bm: BundleManager @@ -37,6 +37,7 @@ describe('#BundlerManager', () => { DeterministicDeployer.init(provider) const config: BundlerConfig = { + chainId: 1337, beneficiary: await signer.getAddress(), entryPoint: entryPoint.address, gasFactor: '0.2', @@ -60,7 +61,7 @@ describe('#BundlerManager', () => { const repMgr = new ReputationManager(provider, BundlerReputationParams, parseEther(config.minStake), config.minUnstakeDelay) const mempoolMgr = new MempoolManager(repMgr) - const preVerificationGasCalculator = new PreVerificationGasCalculator(0, 0, 0, 0, 0, 0, 0, 0) + 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) @@ -92,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', @@ -114,7 +116,7 @@ describe('#BundlerManager', () => { } const repMgr = new ReputationManager(provider, BundlerReputationParams, parseEther(config.minStake), config.minUnstakeDelay) const mempoolMgr = new MempoolManager(repMgr) - const preVerificationGasCalculator = new PreVerificationGasCalculator(0, 0, 0, 0, 0, 0, 0, 0) + 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) diff --git a/packages/bundler/test/BundlerServer.test.ts b/packages/bundler/test/BundlerServer.test.ts index ecaf82d8..c8cdd4ba 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' @@ -22,7 +23,6 @@ import { ExecutionManager } from '../src/modules/ExecutionManager' import { MethodHandlerERC4337 } from '../src/MethodHandlerERC4337' import { BundlerConfig } from '../src/BundlerConfig' import { DepositManager } from '../src/modules/DepositManager' -import { PreVerificationGasCalculator } from '@account-abstraction/sdk' describe('BundleServer', function () { let entryPoint: IEntryPoint @@ -33,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', @@ -56,7 +57,7 @@ describe('BundleServer', function () { const repMgr = new ReputationManager(provider, BundlerReputationParams, parseEther(config.minStake), config.minUnstakeDelay) const mempoolMgr = new MempoolManager(repMgr) - const preVerificationGasCalculator = new PreVerificationGasCalculator(0, 0, 0, 0, 0, 0, 0, 0) + 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) diff --git a/packages/bundler/test/DebugMethodHandler.test.ts b/packages/bundler/test/DebugMethodHandler.test.ts index 077b5f6d..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 { PreVerificationGasCalculator, 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,7 @@ describe('#DebugMethodHandler', () => { const repMgr = new ReputationManager(provider, BundlerReputationParams, parseEther(config.minStake), config.minUnstakeDelay) const mempoolMgr = new MempoolManager(repMgr) - const preVerificationGasCalculator = new PreVerificationGasCalculator(0, 0, 0, 0, 0, 0, 0, 0) + 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, diff --git a/packages/bundler/test/UserOpMethodHandler.test.ts b/packages/bundler/test/UserOpMethodHandler.test.ts index fc5ccd7b..a5c233c6 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 { PreVerificationGasCalculator, 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,7 @@ describe('UserOpMethodHandler', function () { const repMgr = new ReputationManager(provider, BundlerReputationParams, parseEther(config.minStake), config.minUnstakeDelay) mempoolMgr = new MempoolManager(repMgr) - const preVerificationGasCalculator = new PreVerificationGasCalculator(0, 0, 0, 0, 0, 0, 0, 0) + 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) diff --git a/packages/bundler/test/ValidateManager.test.ts b/packages/bundler/test/ValidateManager.test.ts index e611a73f..05997442 100644 --- a/packages/bundler/test/ValidateManager.test.ts +++ b/packages/bundler/test/ValidateManager.test.ts @@ -15,7 +15,7 @@ import { checkRulesViolations, supportsDebugTraceCall } from '@account-abstraction/validation-manager' -import { PreVerificationGasCalculator } from '@account-abstraction/sdk' +import { PreVerificationGasCalculator, MainnetConfig } from '@account-abstraction/sdk' import { TestCoin, @@ -77,11 +77,11 @@ describe('#ValidationManager', () => { const pmd = pmRule === '' ? {} : { - paymaster: paymaster.address, - paymasterVerificationGasLimit: 1e5, - paymasterPostOpGasLimit: 1e5, - paymasterData: Buffer.from(pmRule) - } + paymaster: paymaster.address, + paymasterVerificationGasLimit: 1e5, + paymasterPostOpGasLimit: 1e5, + paymasterData: Buffer.from(pmRule) + } const signature = hexlify(Buffer.from(validateRule)) return { ...cEmptyUserOp, @@ -103,11 +103,11 @@ describe('#ValidationManager', () => { const pmInfo = pmRule == null ? {} : { - paymaster: paymaster.address, - paymasterVerificationGasLimit: 1e6, - paymasterPostOpGasLimit: 1e6, - paymasterData: Buffer.from(pmRule) - } + paymaster: paymaster.address, + paymasterVerificationGasLimit: 1e6, + paymasterPostOpGasLimit: 1e6, + paymasterData: Buffer.from(pmRule) + } const signature = hexlify(Buffer.from(validateRule)) const callinitCodeForAddr = await provider.call({ to: factoryAddress, @@ -153,7 +153,7 @@ describe('#ValidationManager', () => { await entryPoint.depositTo(rulesAccount.address, { value: parseEther('1') }) const unsafe = !await supportsDebugTraceCall(provider, false) - const preVerificationGasCalculator = new PreVerificationGasCalculator(0, 0, 0, 0, 0, 0, 0, 0) + const preVerificationGasCalculator = new PreVerificationGasCalculator(MainnetConfig) vm = new ValidationManager(entryPoint, unsafe, preVerificationGasCalculator) if (!await supportsDebugTraceCall(ethers.provider, false)) { diff --git a/packages/sdk/src/PreVerificationGasCalculator.ts b/packages/sdk/src/PreVerificationGasCalculator.ts index e64bce8f..0946e6f6 100644 --- a/packages/sdk/src/PreVerificationGasCalculator.ts +++ b/packages/sdk/src/PreVerificationGasCalculator.ts @@ -1,62 +1,81 @@ import { encodeUserOp, packUserOp, UserOperation } from '@account-abstraction/utils' import { arrayify, hexlify } from 'ethers/lib/utils' -import { BigNumber, BigNumberish } from 'ethers' -// export const DefaultGasOverheads: GasOverheads = { -// fixed: 21000, -// perUserOp: 18300, -// perUserOpWord: 4, -// zeroByte: 4, -// nonZeroByte: 16, -// bundleSize: 1, -// sigSize: 65 -// } +export interface PreVerificationGasCalculatorConfig { + /** + * 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 = { + fixedGasOverhead: 21000, + perUserOpGasOverhead: 18300, + 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 ( - /** - * 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 = 0 + 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, preVerificationGas: BigNumberish + userOp: UserOperation ): { isPreVerificationGasValid: boolean, minRequiredPreVerificationGas: number } { - return { isPreVerificationGasValid: false, minRequiredPreVerificationGas: 0 } + const minRequiredPreVerificationGas = this._calculate(userOp) + return { + minRequiredPreVerificationGas, + isPreVerificationGasValid: minRequiredPreVerificationGas <= userOp.preVerificationGas + } } /** - * Estimate the 'preVerificationGas' necessary for the given UserOperation. + * 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. @@ -65,18 +84,22 @@ export class PreVerificationGasCalculator { userOp: Partial ): number { const filledUserOp = this._fillUserOpWithDummyData(userOp) - const packedUserOp = arrayify(encodeUserOp(packUserOp(filledUserOp), false)) + 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.zeroByteGasCost : this.nonZeroByteGasCost) + x => x === 0 ? this.config.zeroByteGasCost : this.config.nonZeroByteGasCost) .reduce( (sum, x) => sum + x ) - const userOpDataWordsOverhead = userOpWordsLength * this.perUserOpWordGasOverhead + const userOpDataWordsOverhead = userOpWordsLength * this.config.perUserOpWordGasOverhead - const userOpSpecificOverhead = callDataCost + userOpDataWordsOverhead + this.perUserOpGasOverhead - const userOpShareOfBundleCost = this.fixedGasOverhead / this.expectedBundleSize + const userOpSpecificOverhead = callDataCost + userOpDataWordsOverhead + this.config.perUserOpGasOverhead + const userOpShareOfBundleCost = this.config.fixedGasOverhead / this.config.expectedBundleSize return Math.round(userOpSpecificOverhead + userOpShareOfBundleCost) } @@ -84,8 +107,8 @@ export class PreVerificationGasCalculator { _fillUserOpWithDummyData (userOp: Partial): UserOperation { const filledUserOp: UserOperation = Object.assign({}, userOp) as UserOperation filledUserOp.preVerificationGas = 21000 // dummy value - filledUserOp.signature = hexlify(Buffer.alloc(this.estimationSignatureSize, 0xff)) - filledUserOp.paymasterData = hexlify(Buffer.alloc(this.estimationPaymasterDataSize, 0xff)) + filledUserOp.signature = hexlify(Buffer.alloc(this.config.estimationSignatureSize, 0xff)) + filledUserOp.paymasterData = hexlify(Buffer.alloc(this.config.estimationPaymasterDataSize, 0xff)) return filledUserOp } } diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index b8e1ae1e..c0906edb 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -6,4 +6,4 @@ export { ERC4337EthersSigner } from './ERC4337EthersSigner' export { ERC4337EthersProvider } from './ERC4337EthersProvider' export { ClientConfig } from './ClientConfig' export { HttpRpcClient } from './HttpRpcClient' -export { PreVerificationGasCalculator } from './PreVerificationGasCalculator' +export { PreVerificationGasCalculator, ChainConfigs, MainnetConfig } from './PreVerificationGasCalculator' diff --git a/packages/validation-manager/src/ValidationManager.ts b/packages/validation-manager/src/ValidationManager.ts index defd8095..bd531b9c 100644 --- a/packages/validation-manager/src/ValidationManager.ts +++ b/packages/validation-manager/src/ValidationManager.ts @@ -293,10 +293,9 @@ export class ValidationManager implements IValidationManager { requireAddressAndFields(userOp, 'paymaster', ['paymasterPostOpGasLimit', 'paymasterVerificationGasLimit'], ['paymasterData']) requireAddressAndFields(userOp, 'factory', ['factoryData']) - const preVerificationGas = (userOp as UserOperation).preVerificationGas - if (preVerificationGas != null) { + if ((userOp as UserOperation).preVerificationGas != null) { const { isPreVerificationGasValid, minRequiredPreVerificationGas } = - this.preVerificationGasCalculator.validatePreVerificationGas(userOp as UserOperation, preVerificationGas) + this.preVerificationGasCalculator.validatePreVerificationGas(userOp as UserOperation) requireCond(isPreVerificationGasValid, `preVerificationGas too low: expected at least ${minRequiredPreVerificationGas}`, ValidationErrors.InvalidFields)