From af61ec42af7f9890fdb3b45aef0a6b181f0a7b26 Mon Sep 17 00:00:00 2001 From: Alex Forshtat Date: Sat, 21 Sep 2024 00:17:35 +0200 Subject: [PATCH] Add 'eip7702Support' config; prototype code for EIP-7702 bundle building --- packages/bundler/src/BundlerConfig.ts | 8 +++-- packages/bundler/src/BundlerServer.ts | 6 ++++ packages/bundler/src/MethodHandlerRIP7560.ts | 3 +- packages/bundler/src/modules/BundleManager.ts | 32 +++++++++++++++---- .../src/modules/BundleManagerRIP7560.ts | 13 ++++---- .../bundler/src/modules/ExecutionManager.ts | 2 +- .../bundler/src/modules/IBundleManager.ts | 4 +-- packages/bundler/test/BundlerManager.test.ts | 6 ++-- packages/bundler/test/BundlerServer.test.ts | 3 +- .../bundler/test/DebugMethodHandler.test.ts | 3 +- .../bundler/test/UserOpMethodHandler.test.ts | 3 +- packages/utils/src/index.ts | 2 +- packages/utils/src/interfaces/EIP7702Tuple.ts | 4 +++ 13 files changed, 65 insertions(+), 24 deletions(-) diff --git a/packages/bundler/src/BundlerConfig.ts b/packages/bundler/src/BundlerConfig.ts index 131632d4..fcfc2be9 100644 --- a/packages/bundler/src/BundlerConfig.ts +++ b/packages/bundler/src/BundlerConfig.ts @@ -27,6 +27,8 @@ export interface BundlerConfig { rip7560: boolean rip7560Mode: string gethDevMode: boolean + + eip7702Support: boolean } // TODO: implement merging config (args -> config.js -> default) and runtime shape validation @@ -52,7 +54,8 @@ export const BundlerConfigShape = { autoBundleMempoolSize: ow.number, rip7560: ow.boolean, rip7560Mode: ow.string.oneOf(['PULL', 'PUSH']), - gethDevMode: ow.boolean + gethDevMode: ow.boolean, + eip7702Support: ow.boolean } // TODO: consider if we want any default fields at all @@ -64,5 +67,6 @@ export const bundlerConfigDefault: Partial = { unsafe: false, conditionalRpc: false, minStake: MIN_STAKE_VALUE, - minUnstakeDelay: MIN_UNSTAKE_DELAY + minUnstakeDelay: MIN_UNSTAKE_DELAY, + eip7702Support: true } diff --git a/packages/bundler/src/BundlerServer.ts b/packages/bundler/src/BundlerServer.ts index 15792f7d..ca6acdfd 100644 --- a/packages/bundler/src/BundlerServer.ts +++ b/packages/bundler/src/BundlerServer.ts @@ -277,9 +277,15 @@ export class BundlerServer { result = await this.methodHandler.getSupportedEntryPoints() break case 'eth_sendUserOperation': + if (!this.config.eip7702Support && params[2] != null) { + throw new Error('EIP-7702 tuples are not supported') + } result = await this.methodHandler.sendUserOperation(params[0], params[1], params[2]) break case 'eth_estimateUserOperationGas': + if (!this.config.eip7702Support && params[2] != null) { + throw new Error('EIP-7702 tuples are not supported') + } result = await this.methodHandler.estimateUserOperationGas(params[0], params[1], params[2]) break case 'eth_getUserOperationReceipt': diff --git a/packages/bundler/src/MethodHandlerRIP7560.ts b/packages/bundler/src/MethodHandlerRIP7560.ts index 3b4f8f6c..a5f251d9 100644 --- a/packages/bundler/src/MethodHandlerRIP7560.ts +++ b/packages/bundler/src/MethodHandlerRIP7560.ts @@ -2,6 +2,7 @@ import { BigNumberish } from 'ethers' import { JsonRpcProvider, TransactionReceipt } from '@ethersproject/providers' import { AddressZero, + EIP7702Tuple, OperationBase, OperationRIP7560, StorageMap, @@ -33,7 +34,7 @@ export class MethodHandlerRIP7560 { minBaseFee: BigNumberish, maxBundleGas: BigNumberish, maxBundleSize: BigNumberish - ): Promise<[OperationBase[], StorageMap]> { + ): Promise<[OperationBase[], EIP7702Tuple[], StorageMap]> { return await this.execManager.createBundle(minBaseFee, maxBundleGas, maxBundleSize) } diff --git a/packages/bundler/src/modules/BundleManager.ts b/packages/bundler/src/modules/BundleManager.ts index 15e5df32..52459024 100644 --- a/packages/bundler/src/modules/BundleManager.ts +++ b/packages/bundler/src/modules/BundleManager.ts @@ -13,12 +13,14 @@ import { import { AddressZero, + EIP7702Tuple, IEntryPoint, OperationBase, RpcError, StorageMap, UserOperation, ValidationErrors, + getEip7702TupleSigner, mergeStorageMap, packUserOp, runContractScript @@ -35,6 +37,9 @@ const debug = Debug('aa.exec.cron') const THROTTLED_ENTITY_BUNDLE_COUNT = 4 +const TX_TYPE_EIP_7702 = 4 +const TX_TYPE_EIP_1559 = 2 + export interface SendBundleReturn { transactionHash: string userOpHashes: string[] @@ -77,12 +82,12 @@ export class BundleManager implements IBundleManager { await this.handlePastEvents() // TODO: pass correct bundle limit parameters! - const [bundle, storageMap] = await this.createBundle(0, 0, 0) + const [bundle, eip7702Tuples, storageMap] = await this.createBundle(0, 0, 0) if (bundle.length === 0) { debug('sendNextBundle - no bundle to send') } else { const beneficiary = await this._selectBeneficiary() - const ret = await this.sendBundle(bundle as UserOperation[], beneficiary, storageMap) + const ret = await this.sendBundle(bundle as UserOperation[], eip7702Tuples, beneficiary, storageMap) debug(`sendNextBundle exit - after sent a bundle of ${bundle.length} `) return ret } @@ -98,12 +103,13 @@ export class BundleManager implements IBundleManager { * after submitting the bundle, remove all UserOps from the mempool * @return SendBundleReturn the transaction and UserOp hashes on successful transaction, or null on failed transaction */ - async sendBundle (userOps: UserOperation[], beneficiary: string, storageMap: StorageMap): Promise { + async sendBundle (userOps: UserOperation[], eip7702Tuples: EIP7702Tuple[], beneficiary: string, storageMap: StorageMap): Promise { try { const feeData = await this.provider.getFeeData() // TODO: estimate is not enough. should trace with validation rules, to prevent on-chain revert. + const type = eip7702Tuples.length > 0 ? TX_TYPE_EIP_7702 : TX_TYPE_EIP_1559 const tx = await this.entryPoint.populateTransaction.handleOps(userOps.map(packUserOp), beneficiary, { - type: 2, + type, nonce: await this.signer.getTransactionCount(), maxPriorityFeePerGas: feeData.maxPriorityFeePerGas ?? 0, maxFeePerGas: feeData.maxFeePerGas ?? 0 @@ -207,9 +213,10 @@ export class BundleManager implements IBundleManager { minBaseFee?: BigNumberish, maxBundleGas?: BigNumberish, maxBundleSize?: BigNumberish - ): Promise<[OperationBase[], StorageMap]> { + ): Promise<[OperationBase[], EIP7702Tuple[], StorageMap]> { const entries = this.mempoolManager.getSortedForInclusion() const bundle: OperationBase[] = [] + const eip7702TuplesBundle: EIP7702Tuple[] = [] // paymaster deposit should be enough for all UserOps in the bundle. const paymasterDeposit: { [paymaster: string]: BigNumber } = {} @@ -331,13 +338,26 @@ export class BundleManager implements IBundleManager { } mergeStorageMap(storageMap, validationResult.storageMap) + for (const eip7702Tuple of entry.eip7702Tuples) { + const bundleTuple = eip7702TuplesBundle + .find(it => { + return getEip7702TupleSigner(it) === getEip7702TupleSigner(eip7702Tuple) + }) + if (bundleTuple != null && bundleTuple.address.toLowerCase() !== eip7702Tuple.address.toLowerCase()) { + debug('unable to add bundle as it relies on an EIP-7702 tuple that conflicts with other UserOperations') + // eslint-disable-next-line no-labels + continue mainLoop + } + } + const newBundleGas = entry.userOpMaxGas.add(bundleGas) bundleGas = newBundleGas senders.add(entry.userOp.sender) bundle.push(entry.userOp) + eip7702TuplesBundle.push(...entry.eip7702Tuples) totalGas = newTotalGas } - return [bundle, storageMap] + return [bundle, eip7702TuplesBundle, storageMap] } _handleSecondValidationException (e: any, paymaster: string | undefined, entry: MempoolEntry): void { diff --git a/packages/bundler/src/modules/BundleManagerRIP7560.ts b/packages/bundler/src/modules/BundleManagerRIP7560.ts index babb6916..8880abbf 100644 --- a/packages/bundler/src/modules/BundleManagerRIP7560.ts +++ b/packages/bundler/src/modules/BundleManagerRIP7560.ts @@ -5,6 +5,7 @@ import { RLP } from '@ethereumjs/rlp' import { hexlify } from 'ethers/lib/utils' import { + EIP7702Tuple, OperationBase, OperationRIP7560, StorageMap, @@ -51,7 +52,7 @@ export class BundleManagerRIP7560 extends BundleManager { if (bundle.length === 0) { debug('sendNextBundle - no bundle to send') } else { - return await this.sendBundle(bundle, '', {}) + return await this.sendBundle(bundle, [], '', {}) } } @@ -77,10 +78,10 @@ export class BundleManagerRIP7560 extends BundleManager { minBaseFee: BigNumberish, maxBundleGas: BigNumberish, maxBundleSize: BigNumberish - ): Promise<[OperationBase[], StorageMap]> { - const [bundle, storageMap] = await super.createBundle(minBaseFee, maxBundleGas, maxBundleSize) + ): Promise<[OperationBase[], EIP7702Tuple[], StorageMap]> { + const [bundle, _, storageMap] = await super.createBundle(minBaseFee, maxBundleGas, maxBundleSize) if (bundle.length === 0) { - return [[], {}] + return [[], [], {}] } const bundleHash = this.computeBundleHash(bundle) @@ -94,10 +95,10 @@ export class BundleManagerRIP7560 extends BundleManager { userOpHashes }) - return [bundle, storageMap] + return [bundle, [], storageMap] } - async sendBundle (userOps: OperationBase[], _beneficiary: string, _storageMap: StorageMap): Promise { + async sendBundle (userOps: OperationBase[], _eip7702Tuples: EIP7702Tuple[], _beneficiary: string, _storageMap: StorageMap): Promise { const creationBlock = await this.provider.getBlockNumber() const bundlerId = 'www.reference-bundler.fake' const userOpHashes: string[] = [] diff --git a/packages/bundler/src/modules/ExecutionManager.ts b/packages/bundler/src/modules/ExecutionManager.ts index d803de07..8091aac7 100644 --- a/packages/bundler/src/modules/ExecutionManager.ts +++ b/packages/bundler/src/modules/ExecutionManager.ts @@ -142,7 +142,7 @@ export class ExecutionManager { minBaseFee: BigNumberish, maxBundleGas: BigNumberish, maxBundleSize: BigNumberish - ): Promise<[OperationBase[], StorageMap]> { + ): Promise<[OperationBase[], EIP7702Tuple[], StorageMap]> { return await this.bundleManager.createBundle(minBaseFee, maxBundleGas, maxBundleSize) } } diff --git a/packages/bundler/src/modules/IBundleManager.ts b/packages/bundler/src/modules/IBundleManager.ts index 79136403..16b22453 100644 --- a/packages/bundler/src/modules/IBundleManager.ts +++ b/packages/bundler/src/modules/IBundleManager.ts @@ -1,6 +1,6 @@ import { BigNumber, BigNumberish } from 'ethers' -import { OperationBase, StorageMap } from '@account-abstraction/utils' +import { EIP7702Tuple, OperationBase, StorageMap } from '@account-abstraction/utils' export interface IBundleManager { @@ -14,5 +14,5 @@ export interface IBundleManager { minBaseFee: BigNumberish, maxBundleGas: BigNumberish, maxBundleSize: BigNumberish - ) => Promise<[OperationBase[], StorageMap]> + ) => Promise<[OperationBase[], EIP7702Tuple[], StorageMap]> } diff --git a/packages/bundler/test/BundlerManager.test.ts b/packages/bundler/test/BundlerManager.test.ts index f6336f31..57fefb1f 100644 --- a/packages/bundler/test/BundlerManager.test.ts +++ b/packages/bundler/test/BundlerManager.test.ts @@ -54,7 +54,8 @@ describe('#BundlerManager', () => { rip7560: false, rip7560Mode: 'PULL', gethDevMode: false, - conditionalRpc: false + conditionalRpc: false, + eip7702Support: false } const repMgr = new ReputationManager(provider, BundlerReputationParams, parseEther(config.minStake), config.minUnstakeDelay) @@ -108,7 +109,8 @@ describe('#BundlerManager', () => { rip7560: false, rip7560Mode: 'PULL', gethDevMode: false, - minUnstakeDelay: 0 + minUnstakeDelay: 0, + eip7702Support: false } const repMgr = new ReputationManager(provider, BundlerReputationParams, parseEther(config.minStake), config.minUnstakeDelay) const mempoolMgr = new MempoolManager(repMgr) diff --git a/packages/bundler/test/BundlerServer.test.ts b/packages/bundler/test/BundlerServer.test.ts index b7207625..e4a1cf5f 100644 --- a/packages/bundler/test/BundlerServer.test.ts +++ b/packages/bundler/test/BundlerServer.test.ts @@ -50,7 +50,8 @@ describe('BundleServer', function () { rip7560: false, rip7560Mode: 'PULL', gethDevMode: false, - minUnstakeDelay: 0 + minUnstakeDelay: 0, + eip7702Support: false } const repMgr = new ReputationManager(provider, BundlerReputationParams, parseEther(config.minStake), config.minUnstakeDelay) diff --git a/packages/bundler/test/DebugMethodHandler.test.ts b/packages/bundler/test/DebugMethodHandler.test.ts index af63e624..8c4268fe 100644 --- a/packages/bundler/test/DebugMethodHandler.test.ts +++ b/packages/bundler/test/DebugMethodHandler.test.ts @@ -61,7 +61,8 @@ describe('#DebugMethodHandler', () => { rip7560: false, rip7560Mode: 'PULL', gethDevMode: false, - minUnstakeDelay: 0 + minUnstakeDelay: 0, + eip7702Support: false } const repMgr = new ReputationManager(provider, BundlerReputationParams, parseEther(config.minStake), config.minUnstakeDelay) diff --git a/packages/bundler/test/UserOpMethodHandler.test.ts b/packages/bundler/test/UserOpMethodHandler.test.ts index 9c0c4f33..1c9f002b 100644 --- a/packages/bundler/test/UserOpMethodHandler.test.ts +++ b/packages/bundler/test/UserOpMethodHandler.test.ts @@ -79,7 +79,8 @@ describe('UserOpMethodHandler', function () { rip7560: false, rip7560Mode: 'PULL', gethDevMode: false, - minUnstakeDelay: 0 + minUnstakeDelay: 0, + eip7702Support: false } const repMgr = new ReputationManager(provider, BundlerReputationParams, parseEther(config.minStake), config.minUnstakeDelay) diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index ebf6bfd4..21b6ed99 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -1,4 +1,4 @@ -export { EIP7702Tuple } from './interfaces/EIP7702Tuple' +export { EIP7702Tuple, getEip7702TupleSigner } from './interfaces/EIP7702Tuple' export { UserOperation } from './interfaces/UserOperation' export { OperationBase } from './interfaces/OperationBase' export { OperationRIP7560 } from './interfaces/OperationRIP7560' diff --git a/packages/utils/src/interfaces/EIP7702Tuple.ts b/packages/utils/src/interfaces/EIP7702Tuple.ts index 72fac269..1d4b02db 100644 --- a/packages/utils/src/interfaces/EIP7702Tuple.ts +++ b/packages/utils/src/interfaces/EIP7702Tuple.ts @@ -8,3 +8,7 @@ export interface EIP7702Tuple { r: BigNumberish, s: BigNumberish } + +export function getEip7702TupleSigner (tuple: EIP7702Tuple): string { + throw new Error('Not implemented') +}