From 54170ff57187140e339042133a5bbcbdc7327345 Mon Sep 17 00:00:00 2001 From: Alex Forshtat Date: Sun, 26 Jan 2025 20:30:35 +0100 Subject: [PATCH 1/8] AA-522: Support alt-mempool configurations --- packages/bundler/src/Config.ts | 15 +++++++ .../bundler/src/modules/ExecutionManager.ts | 7 +-- packages/bundler/src/modules/MempoolEntry.ts | 2 + .../bundler/src/modules/MempoolManager.ts | 43 ++++++++++++++++--- packages/bundler/src/modules/initServer.ts | 12 +++--- packages/bundler/src/runBundler.ts | 7 ++- packages/bundler/test/BundlerManager.test.ts | 8 ++-- packages/bundler/test/BundlerServer.test.ts | 4 +- .../bundler/test/DebugMethodHandler.test.ts | 4 +- packages/bundler/test/RIP7560Mode.test.ts | 2 +- .../bundler/test/UserOpMethodHandler.test.ts | 4 +- packages/bundler/test/ValidateManager.test.ts | 2 +- .../validation-manager/src/ERC7562Parser.ts | 6 +-- .../src/IValidationManager.ts | 3 ++ .../src/ValidationManager.ts | 5 ++- .../src/ValidationManagerRIP7560.ts | 1 + .../src/altmempool/AltMempoolConfig.ts | 39 +++++++++++++++++ packages/validation-manager/src/index.ts | 2 +- 18 files changed, 132 insertions(+), 34 deletions(-) diff --git a/packages/bundler/src/Config.ts b/packages/bundler/src/Config.ts index 8aa14b35..100ead1c 100644 --- a/packages/bundler/src/Config.ts +++ b/packages/bundler/src/Config.ts @@ -4,6 +4,7 @@ import fs from 'fs' import { BundlerConfig, bundlerConfigDefault, BundlerConfigShape } from './BundlerConfig' import { Wallet, Signer } from 'ethers' import { JsonRpcProvider } from '@ethersproject/providers' +import { AltMempoolConfig, validateAltMempoolConfigShape } from '@account-abstraction/validation-manager' function getCommandLineParams (programOpts: any): Partial { const params: any = {} @@ -60,3 +61,17 @@ export async function resolveConfiguration (programOpts: any): Promise<{ config: } return { config, provider, wallet } } + +export async function resolveAltMempoolConfig (programOpts: any): Promise { + const configFileName: string = programOpts.altMempoolConfig + if (!fs.existsSync(configFileName)) { + return {} + } + try { + const fileConfig = JSON.parse(fs.readFileSync(configFileName, 'ascii')) + validateAltMempoolConfigShape(fileConfig) + return fileConfig + } catch (e: any) { + throw new Error(`Unable to read --altMempoolConfig ${configFileName}: ${e.message as string}`) + } +} diff --git a/packages/bundler/src/modules/ExecutionManager.ts b/packages/bundler/src/modules/ExecutionManager.ts index a10816fc..95b573bf 100644 --- a/packages/bundler/src/modules/ExecutionManager.ts +++ b/packages/bundler/src/modules/ExecutionManager.ts @@ -9,7 +9,7 @@ import { ReputationManager } from './ReputationManager' import { IBundleManager } from './IBundleManager' import { EmptyValidateUserOpResult, - IValidationManager, ValidationManager + IValidationManager, ValidateUserOpResult, ValidationManager } from '@account-abstraction/validation-manager' import { DepositManager } from './DepositManager' import { BigNumberish, Signer } from 'ethers' @@ -56,7 +56,7 @@ export class ExecutionManager { await this.mutex.runExclusive(async () => { debug('sendUserOperation') this.validationManager.validateInputParameters(userOp, entryPointInput) - let validationResult = EmptyValidateUserOpResult + let validationResult: ValidateUserOpResult = EmptyValidateUserOpResult if (!skipValidation) { validationResult = await this.validationManager.validateUserOp(userOp) } @@ -150,7 +150,8 @@ export class ExecutionManager { const { configuration, entryPoint, unsafe } = this.validationManager._getDebugConfiguration() const mergedConfiguration = Object.assign({}, configuration, configOverrides) const pvgc = new PreVerificationGasCalculator(mergedConfiguration) - const erc7562Parser = new ERC7562Parser(entryPoint.address, mergedConfiguration.senderCreator ?? '') + const bailOnViolation = Object.keys(this.mempoolManager.altMempoolConfig).length === 0 + const erc7562Parser = new ERC7562Parser(bailOnViolation, entryPoint.address, mergedConfiguration.senderCreator ?? '') this.validationManager = new ValidationManager( entryPoint, unsafe, diff --git a/packages/bundler/src/modules/MempoolEntry.ts b/packages/bundler/src/modules/MempoolEntry.ts index ea62f96d..1bbbf2dc 100644 --- a/packages/bundler/src/modules/MempoolEntry.ts +++ b/packages/bundler/src/modules/MempoolEntry.ts @@ -1,5 +1,6 @@ import { BigNumber, BigNumberish } from 'ethers' import { OperationBase, ReferencedCodeHashes, UserOperation } from '@account-abstraction/utils' +import { ERC7562Violation } from '@account-abstraction/validation-manager/dist/src/ERC7562Violation' export class MempoolEntry { userOpMaxGas: BigNumber @@ -9,6 +10,7 @@ export class MempoolEntry { readonly userOpHash: string, readonly prefund: BigNumberish, readonly referencedContracts: ReferencedCodeHashes, + readonly ruleViolations: ERC7562Violation[], readonly skipValidation: boolean, readonly aggregator?: string ) { diff --git a/packages/bundler/src/modules/MempoolManager.ts b/packages/bundler/src/modules/MempoolManager.ts index 1f942d4f..4ef0994f 100644 --- a/packages/bundler/src/modules/MempoolManager.ts +++ b/packages/bundler/src/modules/MempoolManager.ts @@ -10,12 +10,14 @@ import { requireCond } from '@account-abstraction/utils' import { + AltMempoolConfig, ERC7562Rule, ValidateUserOpResult, ValidationResult } from '@account-abstraction/validation-manager' import { MempoolEntry } from './MempoolEntry' import { ReputationManager } from './ReputationManager' +import { BaseAltMempoolRule } from '@account-abstraction/validation-manager/src/altmempool/AltMempoolConfig' const debug = Debug('aa.mempool') @@ -25,10 +27,19 @@ const THROTTLED_ENTITY_MEMPOOL_COUNT = 4 export class MempoolManager { private mempool: MempoolEntry[] = [] + private altMempools: { [mempoolId: number]: MempoolEntry[] } = {} // count entities in mempool. private _entryCount: { [addr: string]: number | undefined } = {} + constructor ( + readonly reputationManager: ReputationManager, + readonly altMempoolConfig: AltMempoolConfig) { + for (const id of Object.keys(this.altMempoolConfig)){ + this.altMempools[parseInt(id)] = [] + } + } + entryCount (address: string): number | undefined { return this._entryCount[address.toLowerCase()] } @@ -53,10 +64,6 @@ export class MempoolManager { } } - constructor ( - readonly reputationManager: ReputationManager) { - } - count (): number { return this.mempool.length } @@ -68,13 +75,14 @@ export class MempoolManager { skipValidation: boolean, userOp: OperationBase, userOpHash: string, - validationResult: ValidationResult + validationResult: ValidateUserOpResult ): void { const entry = new MempoolEntry( userOp, userOpHash, validationResult.returnInfo.prefund ?? 0, - (validationResult as ValidateUserOpResult).referencedContracts, + validationResult.referencedContracts, + validationResult.ruleViolations, skipValidation, validationResult.aggregatorInfo?.addr ) @@ -99,7 +107,7 @@ export class MempoolManager { if (userOp.factory != null) { this.incrementEntryCount(userOp.factory) } - this.mempool.push(entry) + this.tryAssignToMempool(entry) } if (oldEntry != null) { this.updateSeenStatus(oldEntry.aggregator, oldEntry.userOp, validationResult.senderInfo, -1) @@ -300,4 +308,25 @@ export class MempoolManager { } } } + + private tryAssignToMempool (entry: MempoolEntry): number[] { + if (entry.ruleViolations.length === 0) { + this.mempool.push(entry) + return [0] + } + const mempoolIds: number[] = [] + for (const violation of entry.ruleViolations) { + console.log(`Violation: ${JSON.stringify(violation)}`) + for (const [id, config] of Object.entries(this.altMempoolConfig)) { + console.log(`Mempool ID: ${id}`) + for (const [erc7562Rule, override] of Object.entries(config) as [ERC7562Rule, BaseAltMempoolRule][]) { + console.log(` Rule: ${erc7562Rule}, Enabled: ${override.enabled}`) + if (violation.rule === erc7562Rule) { + console.error('MATCHES THE VIOLATION') + } + } + } + } + return mempoolIds + } } diff --git a/packages/bundler/src/modules/initServer.ts b/packages/bundler/src/modules/initServer.ts index ad7d0942..26b59034 100644 --- a/packages/bundler/src/modules/initServer.ts +++ b/packages/bundler/src/modules/initServer.ts @@ -12,7 +12,7 @@ import { AA_ENTRY_POINT, AA_NONCE_MANAGER, AA_SENDER_CREATOR, - AA_STAKE_MANAGER, + AA_STAKE_MANAGER, AltMempoolConfig, IValidationManager, ValidationManager, ValidationManagerRIP7560 @@ -31,25 +31,27 @@ import { ERC7562Parser } from '@account-abstraction/validation-manager/dist/src/ * initialize server modules. * returns the ExecutionManager and EventsManager (for handling events, to update reputation) * @param config + * @param altMempoolConfig * @param signer */ -export function initServer (config: BundlerConfig, signer: Signer): [ExecutionManager, EventsManager, ReputationManager, MempoolManager, PreVerificationGasCalculator] { +export function initServer (config: BundlerConfig, altMempoolConfig: AltMempoolConfig, 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 mempoolManager = new MempoolManager(reputationManager, altMempoolConfig) 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) { - const erc7562Parser = new ERC7562Parser(entryPoint.address, config.senderCreator) + const bailOnViolation = Object.keys(altMempoolConfig).length === 0 + const erc7562Parser = new ERC7562Parser(bailOnViolation, entryPoint.address, config.senderCreator) const tracerProvider = config.tracerRpcUrl == null ? undefined : getNetworkProvider(config.tracerRpcUrl) validationManager = new ValidationManager(entryPoint, config.unsafe, preVerificationGasCalculator, erc7562Parser, tracerProvider) bundleManager = new BundleManager(entryPoint, entryPoint.provider as JsonRpcProvider, signer, eventsManager, mempoolManager, validationManager, reputationManager, config.beneficiary, parseEther(config.minBalance), config.maxBundleGas, config.conditionalRpc) } else { - const erc7562Parser = new ERC7562Parser(AA_ENTRY_POINT, AA_SENDER_CREATOR, AA_NONCE_MANAGER) + const erc7562Parser = new ERC7562Parser(true, AA_ENTRY_POINT, AA_SENDER_CREATOR, AA_NONCE_MANAGER) const stakeManager = IRip7560StakeManager__factory.connect(AA_STAKE_MANAGER, signer) validationManager = new ValidationManagerRIP7560(stakeManager, entryPoint.provider as JsonRpcProvider, erc7562Parser, config.unsafe) bundleManager = new BundleManagerRIP7560(entryPoint.provider as JsonRpcProvider, signer, eventsManager, mempoolManager, validationManager, reputationManager, diff --git a/packages/bundler/src/runBundler.ts b/packages/bundler/src/runBundler.ts index e4d6e0f2..a86ffbbd 100644 --- a/packages/bundler/src/runBundler.ts +++ b/packages/bundler/src/runBundler.ts @@ -16,7 +16,7 @@ import { MethodHandlerERC4337 } from './MethodHandlerERC4337' import { initServer } from './modules/initServer' import { DebugMethodHandler } from './DebugMethodHandler' import { supportsDebugTraceCall, supportsNativeTracer } from '@account-abstraction/validation-manager' -import { resolveConfiguration } from './Config' +import { resolveAltMempoolConfig, resolveConfiguration } from './Config' import { bundlerConfigDefault } from './BundlerConfig' import { parseEther } from 'ethers/lib/utils' import { MethodHandlerRIP7560 } from './MethodHandlerRIP7560' @@ -32,6 +32,7 @@ ethers.BigNumber.prototype[inspectCustomSymbol] = function () { } const CONFIG_FILE_NAME = 'workdir/bundler.config.json' +const ALT_MEMPOOL_FILE_NAME = 'workdir/alt.mempool.config.json' export let showStackTraces = false @@ -89,6 +90,7 @@ export async function runBundler (argv: string[], overrideExit = true): Promise< .option('--rip7560', 'Use this bundler as an RIP-7560 node') .option('--rip7560Mode ', 'PUSH mode sends bundles to node at an interval, PULL mode waits for node to query bundle') .option('--gethDevMode', 'In PULL mode send 1 wei transaction to trigger block creation') + .option('--altMempoolConfig ', 'path to Alt-Mempool config files (overrides to ERC-7562 rules)', ALT_MEMPOOL_FILE_NAME) const programOpts = program.parse(argv).opts() showStackTraces = programOpts.showStackTraces @@ -107,6 +109,7 @@ export async function runBundler (argv: string[], overrideExit = true): Promise< process.exit(1) } const { config, provider, wallet } = await resolveConfiguration(programOpts) + const altMempoolConfig = await resolveAltMempoolConfig(programOpts) const { // name: chainName, @@ -182,7 +185,7 @@ export async function runBundler (argv: string[], overrideExit = true): Promise< reputationManager, mempoolManager, preVerificationGasCalculator - ] = initServer(execManagerConfig, wallet) + ] = initServer(execManagerConfig, altMempoolConfig, wallet) const methodHandler = new MethodHandlerERC4337( execManager, provider, diff --git a/packages/bundler/test/BundlerManager.test.ts b/packages/bundler/test/BundlerManager.test.ts index 9827b7df..3a6e8e3c 100644 --- a/packages/bundler/test/BundlerManager.test.ts +++ b/packages/bundler/test/BundlerManager.test.ts @@ -63,9 +63,9 @@ describe('#BundlerManager', () => { } const repMgr = new ReputationManager(provider, BundlerReputationParams, parseEther(config.minStake), config.minUnstakeDelay) - const mempoolMgr = new MempoolManager(repMgr) + const mempoolMgr = new MempoolManager(repMgr, {}) const preVerificationGasCalculator = new PreVerificationGasCalculator(MainnetConfig) - const erc7562Parser = new ERC7562Parser(entryPoint.address, config.senderCreator) + const erc7562Parser = new ERC7562Parser(true, entryPoint.address, config.senderCreator) const validMgr = new ValidationManager(entryPoint, config.unsafe, preVerificationGasCalculator, erc7562Parser) 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) @@ -121,9 +121,9 @@ describe('#BundlerManager', () => { eip7702Support: false } const repMgr = new ReputationManager(provider, BundlerReputationParams, parseEther(config.minStake), config.minUnstakeDelay) - const mempoolMgr = new MempoolManager(repMgr) + const mempoolMgr = new MempoolManager(repMgr, {}) const preVerificationGasCalculator = new PreVerificationGasCalculator(MainnetConfig) - const erc7562Parser = new ERC7562Parser(entryPoint.address, config.senderCreator) + const erc7562Parser = new ERC7562Parser(true, entryPoint.address, config.senderCreator) const validMgr = new ValidationManager(_entryPoint, config.unsafe, preVerificationGasCalculator, erc7562Parser) 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 cb7e30b4..428a303e 100644 --- a/packages/bundler/test/BundlerServer.test.ts +++ b/packages/bundler/test/BundlerServer.test.ts @@ -59,9 +59,9 @@ describe('BundleServer', function () { } const repMgr = new ReputationManager(provider, BundlerReputationParams, parseEther(config.minStake), config.minUnstakeDelay) - const mempoolMgr = new MempoolManager(repMgr) + const mempoolMgr = new MempoolManager(repMgr, {}) const preVerificationGasCalculator = new PreVerificationGasCalculator(MainnetConfig) - const erc7562Parser = new ERC7562Parser(entryPoint.address, config.senderCreator) + const erc7562Parser = new ERC7562Parser(true, entryPoint.address, config.senderCreator) const validMgr = new ValidationManager(entryPoint, config.unsafe, preVerificationGasCalculator, erc7562Parser) 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 761d0277..075da9ac 100644 --- a/packages/bundler/test/DebugMethodHandler.test.ts +++ b/packages/bundler/test/DebugMethodHandler.test.ts @@ -69,9 +69,9 @@ describe('#DebugMethodHandler', () => { } const repMgr = new ReputationManager(provider, BundlerReputationParams, parseEther(config.minStake), config.minUnstakeDelay) - const mempoolMgr = new MempoolManager(repMgr) + const mempoolMgr = new MempoolManager(repMgr, {}) const preVerificationGasCalculator = new PreVerificationGasCalculator(MainnetConfig) - const erc7562Parser = new ERC7562Parser(entryPoint.address, config.senderCreator) + const erc7562Parser = new ERC7562Parser(true, entryPoint.address, config.senderCreator) const validMgr = new ValidationManager(entryPoint, config.unsafe, preVerificationGasCalculator, erc7562Parser) 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/RIP7560Mode.test.ts b/packages/bundler/test/RIP7560Mode.test.ts index ec602cf2..9c8091c5 100644 --- a/packages/bundler/test/RIP7560Mode.test.ts +++ b/packages/bundler/test/RIP7560Mode.test.ts @@ -57,7 +57,7 @@ describe.skip('RIP7560Mode', function () { // fund deployment of the EntryPoint contract await signer.sendTransaction({ to: await wallet.getAddress(), value: parseEther('1') }) - const [execManager] = initServer(config, signer) + const [execManager] = initServer(config, {}, signer) // spy on the underlying ExecutionManager provider 'send' function // @ts-ignore diff --git a/packages/bundler/test/UserOpMethodHandler.test.ts b/packages/bundler/test/UserOpMethodHandler.test.ts index c47dbe96..81eda571 100644 --- a/packages/bundler/test/UserOpMethodHandler.test.ts +++ b/packages/bundler/test/UserOpMethodHandler.test.ts @@ -87,9 +87,9 @@ describe('UserOpMethodHandler', function () { } const repMgr = new ReputationManager(provider, BundlerReputationParams, parseEther(config.minStake), config.minUnstakeDelay) - mempoolMgr = new MempoolManager(repMgr) + mempoolMgr = new MempoolManager(repMgr, {}) const preVerificationGasCalculator = new PreVerificationGasCalculator(MainnetConfig) - const erc7562Parser = new ERC7562Parser(entryPoint.address, config.senderCreator) + const erc7562Parser = new ERC7562Parser(true, entryPoint.address, config.senderCreator) const validMgr = new ValidationManager(entryPoint, config.unsafe, preVerificationGasCalculator, erc7562Parser) 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 c582b104..6f3f72fb 100644 --- a/packages/bundler/test/ValidateManager.test.ts +++ b/packages/bundler/test/ValidateManager.test.ts @@ -157,7 +157,7 @@ describe('#ValidationManager', () => { const preVerificationGasCalculator = new PreVerificationGasCalculator(MainnetConfig) const senderCreator = '0xefc2c1444ebcc4db75e7613d20c6a62ff67a167c' - const erc7562Parser = new ERC7562Parser(entryPoint.address, senderCreator) + const erc7562Parser = new ERC7562Parser(true, entryPoint.address, senderCreator) vm = new ValidationManager(entryPoint, unsafe, preVerificationGasCalculator, erc7562Parser) if (!await supportsDebugTraceCall(ethers.provider, false)) { diff --git a/packages/validation-manager/src/ERC7562Parser.ts b/packages/validation-manager/src/ERC7562Parser.ts index ccdd72ee..69f26914 100644 --- a/packages/validation-manager/src/ERC7562Parser.ts +++ b/packages/validation-manager/src/ERC7562Parser.ts @@ -255,15 +255,15 @@ export class ERC7562Parser { private contractAddresses: string[] = [] private storageMap: StorageMap = {} - private bailOnViolation: boolean = false - constructor ( + public bailOnViolation: boolean, readonly entryPointAddress: string, readonly senderCreatorAddress: string, readonly nonceManagerAddress?: string ) {} /** + * TODO: remove - currently only used by 7560 * Analyzes the tracing results for the given UserOperation. * Throws an exception in case canonical ERC-7562 rule violation was detected. * @@ -284,7 +284,7 @@ export class ERC7562Parser { /** * Analyzes the tracing results for the given UserOperation. * - * Unlike {@link requireCompliance}, does not throw an exception in case a rule violation was detected. + * If {@link bailOnViolation} is true throws an exception once the first rule violation is detected. * * @returns {@link ERC7562ValidationResults} containing addresses and storage slots accessed by the UserOperation, * @returns an array of ERC-7562 rules that were violated by the UserOperation. diff --git a/packages/validation-manager/src/IValidationManager.ts b/packages/validation-manager/src/IValidationManager.ts index c24fd2c0..0b5e634b 100644 --- a/packages/validation-manager/src/IValidationManager.ts +++ b/packages/validation-manager/src/IValidationManager.ts @@ -8,6 +8,7 @@ import { StorageMap } from '@account-abstraction/utils' import { PreVerificationGasCalculatorConfig } from '@account-abstraction/sdk' +import { ERC7562Violation } from './ERC7562Violation' /** * result from successful validation @@ -28,6 +29,7 @@ export interface ValidationResult { } export interface ValidateUserOpResult extends ValidationResult { + ruleViolations: ERC7562Violation[] referencedContracts: ReferencedCodeHashes storageMap: StorageMap } @@ -49,6 +51,7 @@ export const EmptyValidateUserOpResult: ValidateUserOpResult = { addresses: [], hash: '' }, + ruleViolations: [], storageMap: {} } diff --git a/packages/validation-manager/src/ValidationManager.ts b/packages/validation-manager/src/ValidationManager.ts index 5a2a0438..117be5d4 100644 --- a/packages/validation-manager/src/ValidationManager.ts +++ b/packages/validation-manager/src/ValidationManager.ts @@ -40,6 +40,7 @@ import { ERC7562Parser } from './ERC7562Parser' import { ERC7562Call } from './ERC7562Call' import { bundlerCollectorTracer, BundlerTracerResult } from './BundlerCollectorTracer' import { tracerResultParser } from './TracerResultParser' +import { ERC7562Violation } from './ERC7562Violation' const debug = Debug('aa.mgr.validate') @@ -263,6 +264,7 @@ export class ValidationManager implements IValidationManager { } const stateOverrideForEip7702 = await this.getAuthorizationsStateOverride(authorizationList) let storageMap: StorageMap = {} + let ruleViolations: ERC7562Violation[] = [] if (!this.unsafe) { let erc7562Call: ERC7562Call | null let bundlerTracerResult: BundlerTracerResult | null @@ -273,7 +275,7 @@ export class ValidationManager implements IValidationManager { // console.dir(tracerResult, { depth: null }) let contractAddresses: string[] if (erc7562Call != null) { - ({ contractAddresses, storageMap } = this.erc7562Parser.requireCompliance(userOp, erc7562Call, res)) + ({ contractAddresses, storageMap, ruleViolations } = this.erc7562Parser.parseResults(userOp, erc7562Call, res)) } else if (bundlerTracerResult != null) { [contractAddresses, storageMap] = tracerResultParser(userOp, bundlerTracerResult, res, this.entryPoint.address) } else { @@ -318,6 +320,7 @@ export class ValidationManager implements IValidationManager { return { ...res, + ruleViolations, referencedContracts: codeHashes, storageMap } diff --git a/packages/validation-manager/src/ValidationManagerRIP7560.ts b/packages/validation-manager/src/ValidationManagerRIP7560.ts index 825fe59a..6ba4f493 100644 --- a/packages/validation-manager/src/ValidationManagerRIP7560.ts +++ b/packages/validation-manager/src/ValidationManagerRIP7560.ts @@ -130,6 +130,7 @@ export class ValidationManagerRIP7560 implements IValidationManager { addresses: [], hash: '' }, + ruleViolations: [], storageMap: {} } // throw new Error('Method not implemented.'); diff --git a/packages/validation-manager/src/altmempool/AltMempoolConfig.ts b/packages/validation-manager/src/altmempool/AltMempoolConfig.ts index 1f422ea6..9d0d4758 100644 --- a/packages/validation-manager/src/altmempool/AltMempoolConfig.ts +++ b/packages/validation-manager/src/altmempool/AltMempoolConfig.ts @@ -1,3 +1,5 @@ +import ow from 'ow' + import { ERC7562Rule } from '../enum/ERC7562Rule' type Role = 'sender' | 'paymaster' | 'factory' @@ -28,6 +30,41 @@ export interface AltMempoolConfig { [mempoolId: number]: { [rule in ERC7562Rule]?: BaseAltMempoolRule } } +const AltMempoolRuleExceptionBaseShape = ow.object.partialShape({ + role: ow.optional.string.oneOf(['sender', 'paymaster', 'factory']), + address: ow.optional.string, + depths: ow.optional.array.ofType(ow.number), + enterOpcode: ow.optional.array.ofType( + ow.string.oneOf(['CALL', 'DELEGATECALL', 'CALLCODE', 'STATICCALL', 'CREATE', 'CREATE2']) + ), + enterMethodSelector: ow.optional.string.matches(/^0x[a-fA-F0-9]+$/) +}) + +const AltMempoolRuleExceptionBannedOpcodeShape = ow.object.partialShape({ + ...AltMempoolRuleExceptionBaseShape, + opcodes: ow.array.minLength(1).ofType(ow.string), + slots: ow.array.minLength(1).ofType(ow.string.matches(/^0x[a-fA-F0-9]+$/)) +}) + +const BaseAltMempoolRuleShape = ow.object.partialShape({ + enabled: ow.optional.boolean, + exceptions: ow.optional.array.minLength(1).ofType( + ow.any( + ow.string.matches(/^0x[a-fA-F0-9]+$/), + ow.string.oneOf(['sender', 'paymaster', 'factory']), + AltMempoolRuleExceptionBaseShape, + AltMempoolRuleExceptionBannedOpcodeShape + ) + ) +}) + +const AltMempoolConfigShape = ow.object.valuesOfType(ow.object.valuesOfType(BaseAltMempoolRuleShape)) + +export function validateAltMempoolConfigShape (config: AltMempoolConfig): void { + ow(config, AltMempoolConfigShape) +} + +// TODO: remove const config: AltMempoolConfig = { 1: { [ERC7562Rule.erep010]: { @@ -46,4 +83,6 @@ const config: AltMempoolConfig = { } } +validateAltMempoolConfigShape(config) + console.log(config) diff --git a/packages/validation-manager/src/index.ts b/packages/validation-manager/src/index.ts index bea25cee..ebae1027 100644 --- a/packages/validation-manager/src/index.ts +++ b/packages/validation-manager/src/index.ts @@ -58,7 +58,7 @@ export async function checkRulesViolations ( } const entryPoint = IEntryPoint__factory.connect(entryPointAddress, provider) const senderCreator = '0xefc2c1444ebcc4db75e7613d20c6a62ff67a167c' - const erc7562Parser = new ERC7562Parser(entryPointAddress, senderCreator) + const erc7562Parser = new ERC7562Parser(true, entryPointAddress, senderCreator) const validationManager = new ValidationManager( entryPoint, false, From e0568db549a9fdb2b2b8426052bc6c41a5256eea Mon Sep 17 00:00:00 2001 From: Alex Forshtat Date: Sun, 2 Feb 2025 19:10:45 +0100 Subject: [PATCH 2/8] WIP: Implement alt-mempools in the MempoolManager --- packages/bundler/src/BundlerServer.ts | 4 + packages/bundler/src/DebugMethodHandler.ts | 15 +- packages/bundler/src/modules/EventsManager.ts | 1 + .../bundler/src/modules/ExecutionManager.ts | 2 +- packages/bundler/src/modules/MempoolEntry.ts | 2 + .../bundler/src/modules/MempoolManager.ts | 150 ++++++++++++++---- .../bundler/src/modules/ReputationManager.ts | 54 ++++--- .../src/altmempool/AltMempoolConfig.ts | 6 +- 8 files changed, 170 insertions(+), 64 deletions(-) diff --git a/packages/bundler/src/BundlerServer.ts b/packages/bundler/src/BundlerServer.ts index 372f1d84..078b39f3 100644 --- a/packages/bundler/src/BundlerServer.ts +++ b/packages/bundler/src/BundlerServer.ts @@ -334,9 +334,13 @@ export class BundlerServer { case 'debug_bundler_setConfiguration': { const pvgc = await this.debugHandler._setConfiguration(params[0]) this.methodHandler.preVerificationGasCalculator = pvgc + break } + case 'debug_bundler_setAltMempoolConfig': { + await this.debugHandler._setAltMempoolConfig(params[0]) 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 605fc0d6..f2c453fe 100644 --- a/packages/bundler/src/DebugMethodHandler.ts +++ b/packages/bundler/src/DebugMethodHandler.ts @@ -7,8 +7,9 @@ import { BundlerConfig, DebugBundlerConfigShape } from './BundlerConfig' import { EventsManager } from './modules/EventsManager' import { ExecutionManager } from './modules/ExecutionManager' import { MempoolManager } from './modules/MempoolManager' -import { ReputationDump, ReputationManager } from './modules/ReputationManager' +import { ReputationEntry, ReputationManager } from './modules/ReputationManager' import { SendBundleReturn } from './modules/BundleManager' +import { AltMempoolConfig } from '@account-abstraction/validation-manager' export class DebugMethodHandler { constructor ( @@ -60,12 +61,12 @@ export class DebugMethodHandler { this.mempoolMgr.clearState() } - setReputation (param: any): ReputationDump { - return this.repManager.setReputation(param) + setReputation (param: any): ReputationEntry[] { + return this.repManager._debugSetReputation(param) } - dumpReputation (): ReputationDump { - return this.repManager.dump() + dumpReputation (): ReputationEntry[] { + return this.repManager._debugDumpReputation() } clearReputation (): void { @@ -86,4 +87,8 @@ export class DebugMethodHandler { ow.object.exactShape(DebugBundlerConfigShape) return await this.execManager._setConfiguration(config) } + + async _setAltMempoolConfig (altMempoolConfig: AltMempoolConfig): Promise { + return await this.mempoolMgr._setAltMempoolConfig(altMempoolConfig) + } } diff --git a/packages/bundler/src/modules/EventsManager.ts b/packages/bundler/src/modules/EventsManager.ts index 6eacb91a..461f0a26 100644 --- a/packages/bundler/src/modules/EventsManager.ts +++ b/packages/bundler/src/modules/EventsManager.ts @@ -86,6 +86,7 @@ export class EventsManager { handleUserOperationEvent (ev: UserOperationEventEvent): void { const hash = ev.args.userOpHash + this.mempoolManager.includedUserOp(hash) this.mempoolManager.removeUserOp(hash) this._includedAddress(ev.args.sender) this._includedAddress(ev.args.paymaster) diff --git a/packages/bundler/src/modules/ExecutionManager.ts b/packages/bundler/src/modules/ExecutionManager.ts index 95b573bf..5b57fe22 100644 --- a/packages/bundler/src/modules/ExecutionManager.ts +++ b/packages/bundler/src/modules/ExecutionManager.ts @@ -150,7 +150,7 @@ export class ExecutionManager { const { configuration, entryPoint, unsafe } = this.validationManager._getDebugConfiguration() const mergedConfiguration = Object.assign({}, configuration, configOverrides) const pvgc = new PreVerificationGasCalculator(mergedConfiguration) - const bailOnViolation = Object.keys(this.mempoolManager.altMempoolConfig).length === 0 + const bailOnViolation = this.mempoolManager.hasAltMempools() const erc7562Parser = new ERC7562Parser(bailOnViolation, entryPoint.address, mergedConfiguration.senderCreator ?? '') this.validationManager = new ValidationManager( entryPoint, diff --git a/packages/bundler/src/modules/MempoolEntry.ts b/packages/bundler/src/modules/MempoolEntry.ts index 1bbbf2dc..97299c14 100644 --- a/packages/bundler/src/modules/MempoolEntry.ts +++ b/packages/bundler/src/modules/MempoolEntry.ts @@ -1,6 +1,7 @@ import { BigNumber, BigNumberish } from 'ethers' import { OperationBase, ReferencedCodeHashes, UserOperation } from '@account-abstraction/utils' import { ERC7562Violation } from '@account-abstraction/validation-manager/dist/src/ERC7562Violation' +import { ValidateUserOpResult } from '@account-abstraction/validation-manager' export class MempoolEntry { userOpMaxGas: BigNumber @@ -8,6 +9,7 @@ export class MempoolEntry { constructor ( readonly userOp: OperationBase, readonly userOpHash: string, + readonly validateUserOpResult: ValidateUserOpResult, readonly prefund: BigNumberish, readonly referencedContracts: ReferencedCodeHashes, readonly ruleViolations: ERC7562Violation[], diff --git a/packages/bundler/src/modules/MempoolManager.ts b/packages/bundler/src/modules/MempoolManager.ts index 4ef0994f..97e6735c 100644 --- a/packages/bundler/src/modules/MempoolManager.ts +++ b/packages/bundler/src/modules/MempoolManager.ts @@ -18,6 +18,7 @@ import { import { MempoolEntry } from './MempoolEntry' import { ReputationManager } from './ReputationManager' import { BaseAltMempoolRule } from '@account-abstraction/validation-manager/src/altmempool/AltMempoolConfig' +import { ERC7562Violation } from '@account-abstraction/validation-manager/dist/src/ERC7562Violation' const debug = Debug('aa.mempool') @@ -25,21 +26,64 @@ type MempoolDump = OperationBase[] const THROTTLED_ENTITY_MEMPOOL_COUNT = 4 +function isRuleViolated ( + userOp: OperationBase, + violation: ERC7562Violation, + config: { [rule in ERC7562Rule]?: BaseAltMempoolRule } +): boolean { + const override = config[violation.rule] + if (override == null) { + return true + } + if (override.enabled === false) { + return false + } + for (const exception of override.exceptions ?? []) { + if (exception === violation.address) { + return false + } + if (exception === 'sender' && violation.address === userOp.sender) { + return false + } + // type RuleException = `0x${string}` | Role | AltMempoolRuleExceptionBase | AltMempoolRuleExceptionBannedOpcode + // todo: match all possible exceptions + } + return true +} + export class MempoolManager { private mempool: MempoolEntry[] = [] - private altMempools: { [mempoolId: number]: MempoolEntry[] } = {} + private altMempools: { [mempoolId: string]: MempoolEntry[] } = {} // count entities in mempool. private _entryCount: { [addr: string]: number | undefined } = {} constructor ( - readonly reputationManager: ReputationManager, - readonly altMempoolConfig: AltMempoolConfig) { - for (const id of Object.keys(this.altMempoolConfig)){ + private readonly reputationManager: ReputationManager, + private altMempoolConfig: AltMempoolConfig) { + this._initializeMempools() + } + + private _initializeMempools (): void { + for (const id of Object.keys(this.altMempoolConfig)) { this.altMempools[parseInt(id)] = [] } } + /** + * Helper to allow for-of loop that of both the main and alt-mempools where possible without merging them in code. + */ + private _getAllMempoolsLoop (): Array<[string, MempoolEntry[]]> { + return Object.entries({ ...this.altMempools, 0: this.mempool }) + } + + /** + * Helper function to allow skipping resource-intensive trace parsing if there are no configured alt-mempools. + */ + hasAltMempools (): boolean { + return Object.keys(this.altMempoolConfig).length === 0 + } + entryCount (address: string): number | undefined { return this._entryCount[address.toLowerCase()] } @@ -80,6 +124,7 @@ export class MempoolManager { const entry = new MempoolEntry( userOp, userOpHash, + validationResult, validationResult.returnInfo.prefund ?? 0, validationResult.referencedContracts, validationResult.ruleViolations, @@ -87,13 +132,8 @@ export class MempoolManager { validationResult.aggregatorInfo?.addr ) const packedNonce = getPackedNonce(entry.userOp) - const index = this._findBySenderNonce(userOp.sender, packedNonce) - let oldEntry: MempoolEntry | undefined - if (index !== -1) { - oldEntry = this.mempool[index] - this.checkReplaceUserOp(oldEntry, entry) + if (this._checkReplaceByFee(entry)) { debug('replace userOp', userOp.sender, packedNonce) - this.mempool[index] = entry } else { debug('add userOp', userOp.sender, packedNonce) if (!skipValidation) { @@ -109,12 +149,25 @@ export class MempoolManager { } this.tryAssignToMempool(entry) } - if (oldEntry != null) { - this.updateSeenStatus(oldEntry.aggregator, oldEntry.userOp, validationResult.senderInfo, -1) - } this.updateSeenStatus(validationResult.aggregatorInfo?.addr, userOp, validationResult.senderInfo) } + private _checkReplaceByFee (entry: MempoolEntry): boolean { + const packedNonce = getPackedNonce(entry.userOp) + for (const [mempoolId, mempool] of this._getAllMempoolsLoop()) { + const index = this._findBySenderNonce(entry.userOp.sender, packedNonce, mempool) + let oldEntry: MempoolEntry | undefined + if (index !== -1) { + debug('replace userOp in alt-mempool', entry.userOp.sender, packedNonce, mempoolId) + oldEntry = this.mempool[index] + this.checkReplaceUserOp(oldEntry, entry) + this.mempool[index] = entry + this.updateSeenStatus(oldEntry.aggregator, oldEntry.userOp, entry.validateUserOpResult.senderInfo, -1) + } + } + return false + } + private updateSeenStatus (aggregator: string | undefined, userOp: OperationBase, senderInfo: StakeInfo, val = 1): void { try { this.reputationManager.checkStake('account', senderInfo) @@ -207,9 +260,9 @@ export class MempoolManager { return copy } - _findBySenderNonce (sender: string, nonce: BigNumberish): number { - for (let i = 0; i < this.mempool.length; i++) { - const curOp = this.mempool[i].userOp + _findBySenderNonce (sender: string, nonce: BigNumberish, mempool: MempoolEntry[]): number { + for (let i = 0; i < mempool.length; i++) { + const curOp = mempool[i].userOp const packedNonce = getPackedNonce(curOp) if (curOp.sender === sender && packedNonce.eq(nonce)) { return i @@ -218,9 +271,9 @@ export class MempoolManager { return -1 } - _findByHash (hash: string): number { - for (let i = 0; i < this.mempool.length; i++) { - const curOp = this.mempool[i] + _findByHash (hash: string, mempool: MempoolEntry[]): number { + for (let i = 0; i < mempool.length; i++) { + const curOp = mempool[i] if (curOp.userOpHash === hash) { return i } @@ -233,18 +286,29 @@ export class MempoolManager { * @param userOpOrHash */ removeUserOp (userOpOrHash: OperationBase | string): void { + for (const [mempoolId, mempool] of this._getAllMempoolsLoop()) { + this._removeUserOpInternal(userOpOrHash, mempoolId, mempool) + } + } + + _removeUserOpInternal (userOpOrHash: OperationBase | string, mempoolId: string, mempool: MempoolEntry[]): void { let index: number if (typeof userOpOrHash === 'string') { - index = this._findByHash(userOpOrHash) + index = this._findByHash(userOpOrHash, mempool) } else { const packedNonce = getPackedNonce(userOpOrHash) - index = this._findBySenderNonce(userOpOrHash.sender, packedNonce) + index = this._findBySenderNonce(userOpOrHash.sender, packedNonce, mempool) } if (index !== -1) { - const userOp = this.mempool[index].userOp + const userOp = mempool[index].userOp const packedNonce = getPackedNonce(userOp) debug('removeUserOp', userOp.sender, packedNonce) - this.mempool.splice(index, 1) + mempool.splice(index, 1) + if (mempoolId !== '0') { + // Only decrement entity counts for the main mempool + // TODO: support per-mempool entity counts + return + } this.decrementEntryCount(userOp.sender) this.decrementEntryCount(userOp.paymaster) this.decrementEntryCount(userOp.factory) @@ -315,18 +379,40 @@ export class MempoolManager { return [0] } const mempoolIds: number[] = [] - for (const violation of entry.ruleViolations) { - console.log(`Violation: ${JSON.stringify(violation)}`) - for (const [id, config] of Object.entries(this.altMempoolConfig)) { - console.log(`Mempool ID: ${id}`) - for (const [erc7562Rule, override] of Object.entries(config) as [ERC7562Rule, BaseAltMempoolRule][]) { - console.log(` Rule: ${erc7562Rule}, Enabled: ${override.enabled}`) - if (violation.rule === erc7562Rule) { - console.error('MATCHES THE VIOLATION') - } + console.log(`UserOperation ${entry.userOpHash}`) + for (const [mempoolId, config] of Object.entries(this.altMempoolConfig)) { + console.log(` Mempool ID: ${mempoolId} Config: ${JSON.stringify(config)}`) + for (const violation of entry.ruleViolations) { + console.log(` Violation: ${JSON.stringify(violation)}`) + if (isRuleViolated(entry.userOp, violation, config)) { + console.log(` Not adding to mempool ${mempoolId} - rule violated`) + continue } + console.error(` Adding to mempool ${mempoolId}`) + this.altMempools[mempoolId].push(entry) + this.reputationManager.updateSeenStatus(mempoolId) } } return mempoolIds } + + /** + * Debug only function to clean up the existing alt-mempools and set a new alt-mempools configuration. + */ + async _setAltMempoolConfig (altMempoolConfig: AltMempoolConfig): Promise { + this.altMempools = {} + this.altMempoolConfig = altMempoolConfig + } + + includedUserOp (userOpHash: string): void { + for (const [mempoolId, mempool] of Object.entries(this.altMempools)) { + const found = mempool.find((it: MempoolEntry) => { + return it.userOpHash === userOpHash + }) + if (found != null) { + console.error(`Found UserOp ${userOpHash} in the mempool ${mempoolId}, updating INCLUDED`) + this.reputationManager.updateIncludedStatus(mempoolId) + } + } + } } diff --git a/packages/bundler/src/modules/ReputationManager.ts b/packages/bundler/src/modules/ReputationManager.ts index 33a004c2..f97df2a7 100644 --- a/packages/bundler/src/modules/ReputationManager.ts +++ b/packages/bundler/src/modules/ReputationManager.ts @@ -37,15 +37,18 @@ export const NonBundlerReputationParams: ReputationParams = { banSlack: 10 } -interface ReputationEntry { - address: string +/** + * An entry whose "reputation" is tracked by the {@link ReputationManager}. + * May represent either a smart contract with a role in an ERC-4337 transaction or an alternative mempool. + * The "id" is either a contract address or alt-mempool ID. + */ +export interface ReputationEntry { + id: string opsSeen: number opsIncluded: number status?: ReputationStatus } -export type ReputationDump = ReputationEntry[] - export class ReputationManager { constructor ( readonly provider: Provider, @@ -54,18 +57,23 @@ export class ReputationManager { readonly minUnstakeDelay: number) { } - private entries: { [address: string]: ReputationEntry } = {} + /** + * A mapping of all entries whose reputation is being tracked. + * A lowercase address is used to identify smart contracts. + * An alt-mempool ID is used to identify alt-mempools. + */ + private entries: { [id: string | number]: ReputationEntry } = {} // black-listed entities - always banned readonly blackList = new Set() // white-listed entities - always OK. - readonly whitelist = new Set() + readonly whitelist = new Set() /** * debug: dump reputation map (with updated "status" for each entry) */ - dump (): ReputationDump { - Object.values(this.entries).forEach(entry => { entry.status = this.getStatus(entry.address) }) + _debugDumpReputation (): ReputationEntry[] { + Object.values(this.entries).forEach(entry => { entry.status = this.getStatus(entry.id) }) return Object.values(this.entries) } @@ -92,12 +100,12 @@ export class ReputationManager { params.forEach(item => this.blackList.add(item)) } - _getOrCreate (addr: string): ReputationEntry { - addr = addr.toLowerCase() - let entry = this.entries[addr] + _getOrCreate (id: string): ReputationEntry { + id = id.toLowerCase() + let entry = this.entries[id] if (entry == null) { - this.entries[addr] = entry = { - address: addr, + this.entries[id] = entry = { + id, opsSeen: 0, opsIncluded: 0 } @@ -107,16 +115,16 @@ export class ReputationManager { /** * address seen in the mempool triggered by the - * @param addr + * @param id * @param val increment value for "seen" status */ - updateSeenStatus (addr?: string, val = 1): void { - if (addr == null) { + updateSeenStatus (id?: string, val = 1): void { + if (id == null) { return } - const entry = this._getOrCreate(addr) + const entry = this._getOrCreate(id) entry.opsSeen = Math.max(0, entry.opsSeen + val) - debug('after seen+', val, addr, entry) + debug('after seen+', val, id, entry) } /** @@ -202,17 +210,17 @@ export class ReputationManager { /** * for debugging: put in the given reputation entries - * @param entries + * @param reputations */ - setReputation (reputations: ReputationDump): ReputationDump { + _debugSetReputation (reputations: ReputationEntry[]): ReputationEntry[] { reputations.forEach(rep => { - this.entries[rep.address.toLowerCase()] = { - address: rep.address.toLowerCase(), + this.entries[rep.id.toLowerCase()] = { + id: rep.id.toLowerCase(), opsSeen: BigNumber.from(rep.opsSeen).toNumber(), opsIncluded: BigNumber.from(rep.opsIncluded).toNumber() } }) - return this.dump() + return this._debugDumpReputation() } /** diff --git a/packages/validation-manager/src/altmempool/AltMempoolConfig.ts b/packages/validation-manager/src/altmempool/AltMempoolConfig.ts index 9d0d4758..3bcffa48 100644 --- a/packages/validation-manager/src/altmempool/AltMempoolConfig.ts +++ b/packages/validation-manager/src/altmempool/AltMempoolConfig.ts @@ -2,9 +2,9 @@ import ow from 'ow' import { ERC7562Rule } from '../enum/ERC7562Rule' -type Role = 'sender' | 'paymaster' | 'factory' +export type Role = 'sender' | 'paymaster' | 'factory' -type EnterOpcode = 'CALL' | 'DELEGATECALL' | 'CALLCODE' | 'STATICCALL' | 'CREATE' | 'CREATE2' +export type EnterOpcode = 'CALL' | 'DELEGATECALL' | 'CALLCODE' | 'STATICCALL' | 'CREATE' | 'CREATE2' export interface AltMempoolRuleExceptionBase { role?: Role @@ -27,7 +27,7 @@ export interface BaseAltMempoolRule { } export interface AltMempoolConfig { - [mempoolId: number]: { [rule in ERC7562Rule]?: BaseAltMempoolRule } + [mempoolId: string]: { [rule in ERC7562Rule]?: BaseAltMempoolRule } } const AltMempoolRuleExceptionBaseShape = ow.object.partialShape({ From 633d0f1da647472ce63e9bc5f3e888fd0dffc396 Mon Sep 17 00:00:00 2001 From: Alex Forshtat Date: Tue, 4 Feb 2025 01:13:48 +0100 Subject: [PATCH 3/8] Add alt-mempools in the debug dump mempool handler --- packages/bundler/src/DebugMethodHandler.ts | 2 +- packages/bundler/src/modules/MempoolManager.ts | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/bundler/src/DebugMethodHandler.ts b/packages/bundler/src/DebugMethodHandler.ts index f2c453fe..183f5bbd 100644 --- a/packages/bundler/src/DebugMethodHandler.ts +++ b/packages/bundler/src/DebugMethodHandler.ts @@ -54,7 +54,7 @@ export class DebugMethodHandler { } async dumpMempool (): Promise { - return this.mempoolMgr.dump() + return this.mempoolMgr.debugDumpMempool() } clearMempool (): void { diff --git a/packages/bundler/src/modules/MempoolManager.ts b/packages/bundler/src/modules/MempoolManager.ts index 97e6735c..70f5ccff 100644 --- a/packages/bundler/src/modules/MempoolManager.ts +++ b/packages/bundler/src/modules/MempoolManager.ts @@ -22,7 +22,7 @@ import { ERC7562Violation } from '@account-abstraction/validation-manager/dist/s const debug = Debug('aa.mempool') -type MempoolDump = OperationBase[] +type MempoolDump = { [mempoolId: string]: OperationBase[] } const THROTTLED_ENTITY_MEMPOOL_COUNT = 4 @@ -319,8 +319,12 @@ export class MempoolManager { /** * debug: dump mempool content */ - dump (): MempoolDump { - return this.mempool.map(entry => entry.userOp) + debugDumpMempool (): MempoolDump { + const mempoolDump: MempoolDump = {} + for (const [mempoolId, mempool] of Object.entries(this.altMempools)) { + mempoolDump[mempoolId] = mempool.map(entry => entry.userOp) + } + return mempoolDump } /** From b6b229ae16f66bcb9f0ecc9fb64da9c2480418b7 Mon Sep 17 00:00:00 2001 From: Alex Forshtat Date: Tue, 4 Feb 2025 03:29:15 +0100 Subject: [PATCH 4/8] Implement first functional check in 'isRuleViolated', minor fixes --- packages/bundler/src/DebugMethodHandler.ts | 7 +++++-- packages/bundler/src/modules/MempoolManager.ts | 16 +++++++--------- .../validation-manager/src/ERC7562Parser.ts | 2 +- .../src/altmempool/AltMempoolConfig.ts | 18 +++++++++--------- 4 files changed, 22 insertions(+), 21 deletions(-) diff --git a/packages/bundler/src/DebugMethodHandler.ts b/packages/bundler/src/DebugMethodHandler.ts index 183f5bbd..a9c99a84 100644 --- a/packages/bundler/src/DebugMethodHandler.ts +++ b/packages/bundler/src/DebugMethodHandler.ts @@ -9,7 +9,7 @@ import { ExecutionManager } from './modules/ExecutionManager' import { MempoolManager } from './modules/MempoolManager' import { ReputationEntry, ReputationManager } from './modules/ReputationManager' import { SendBundleReturn } from './modules/BundleManager' -import { AltMempoolConfig } from '@account-abstraction/validation-manager' +import { AltMempoolConfig, validateAltMempoolConfigShape } from '@account-abstraction/validation-manager' export class DebugMethodHandler { constructor ( @@ -89,6 +89,9 @@ export class DebugMethodHandler { } async _setAltMempoolConfig (altMempoolConfig: AltMempoolConfig): Promise { - return await this.mempoolMgr._setAltMempoolConfig(altMempoolConfig) + const configCleanedUp = JSON.parse(JSON.stringify(altMempoolConfig)) + validateAltMempoolConfigShape(configCleanedUp) + await this.mempoolMgr._setAltMempoolConfig(configCleanedUp) + await this._setConfiguration({}) } } diff --git a/packages/bundler/src/modules/MempoolManager.ts b/packages/bundler/src/modules/MempoolManager.ts index 70f5ccff..dba753ef 100644 --- a/packages/bundler/src/modules/MempoolManager.ts +++ b/packages/bundler/src/modules/MempoolManager.ts @@ -22,7 +22,7 @@ import { ERC7562Violation } from '@account-abstraction/validation-manager/dist/s const debug = Debug('aa.mempool') -type MempoolDump = { [mempoolId: string]: OperationBase[] } +interface MempoolDump {[mempoolId: string]: OperationBase[]} const THROTTLED_ENTITY_MEMPOOL_COUNT = 4 @@ -42,7 +42,9 @@ function isRuleViolated ( if (exception === violation.address) { return false } - if (exception === 'sender' && violation.address === userOp.sender) { + if ( + (exception === 'account' || (typeof exception === 'object' && exception.role === 'account')) && + violation.address.toLowerCase() === userOp.sender.toLowerCase()) { return false } // type RuleException = `0x${string}` | Role | AltMempoolRuleExceptionBase | AltMempoolRuleExceptionBannedOpcode @@ -61,13 +63,6 @@ export class MempoolManager { constructor ( private readonly reputationManager: ReputationManager, private altMempoolConfig: AltMempoolConfig) { - this._initializeMempools() - } - - private _initializeMempools (): void { - for (const id of Object.keys(this.altMempoolConfig)) { - this.altMempools[parseInt(id)] = [] - } } /** @@ -393,6 +388,9 @@ export class MempoolManager { continue } console.error(` Adding to mempool ${mempoolId}`) + if (this.altMempools[mempoolId] == null) { + this.altMempools[mempoolId] = [] + } this.altMempools[mempoolId].push(entry) this.reputationManager.updateSeenStatus(mempoolId) } diff --git a/packages/validation-manager/src/ERC7562Parser.ts b/packages/validation-manager/src/ERC7562Parser.ts index 69f26914..4a7a7fc5 100644 --- a/packages/validation-manager/src/ERC7562Parser.ts +++ b/packages/validation-manager/src/ERC7562Parser.ts @@ -518,7 +518,7 @@ export class ERC7562Parser { rule: ERC7562Rule.op011, depth: recursionDepth, entity: this.currentEntity, - address: erc7562Call.from, + address: erc7562Call.to, opcode, value: '0', errorCode: ValidationErrors.OpcodeValidation, diff --git a/packages/validation-manager/src/altmempool/AltMempoolConfig.ts b/packages/validation-manager/src/altmempool/AltMempoolConfig.ts index 3bcffa48..f8d6a5a2 100644 --- a/packages/validation-manager/src/altmempool/AltMempoolConfig.ts +++ b/packages/validation-manager/src/altmempool/AltMempoolConfig.ts @@ -2,7 +2,7 @@ import ow from 'ow' import { ERC7562Rule } from '../enum/ERC7562Rule' -export type Role = 'sender' | 'paymaster' | 'factory' +export type Role = 'account' | 'paymaster' | 'factory' export type EnterOpcode = 'CALL' | 'DELEGATECALL' | 'CALLCODE' | 'STATICCALL' | 'CREATE' | 'CREATE2' @@ -31,13 +31,13 @@ export interface AltMempoolConfig { } const AltMempoolRuleExceptionBaseShape = ow.object.partialShape({ - role: ow.optional.string.oneOf(['sender', 'paymaster', 'factory']), - address: ow.optional.string, - depths: ow.optional.array.ofType(ow.number), - enterOpcode: ow.optional.array.ofType( + role: ow.optional.any(ow.optional.string.oneOf(['account', 'paymaster', 'factory']), ow.null), + address: ow.optional.any(ow.string, ow.null), + depths: ow.optional.any(ow.optional.array.ofType(ow.number), ow.null), + enterOpcode: ow.optional.any(ow.optional.array.ofType( ow.string.oneOf(['CALL', 'DELEGATECALL', 'CALLCODE', 'STATICCALL', 'CREATE', 'CREATE2']) - ), - enterMethodSelector: ow.optional.string.matches(/^0x[a-fA-F0-9]+$/) + ), ow.null), + enterMethodSelector: ow.optional.any(ow.optional.string.matches(/^0x[a-fA-F0-9]+$/), ow.null) }) const AltMempoolRuleExceptionBannedOpcodeShape = ow.object.partialShape({ @@ -51,7 +51,7 @@ const BaseAltMempoolRuleShape = ow.object.partialShape({ exceptions: ow.optional.array.minLength(1).ofType( ow.any( ow.string.matches(/^0x[a-fA-F0-9]+$/), - ow.string.oneOf(['sender', 'paymaster', 'factory']), + ow.string.oneOf(['account', 'paymaster', 'factory']), AltMempoolRuleExceptionBaseShape, AltMempoolRuleExceptionBannedOpcodeShape ) @@ -70,7 +70,7 @@ const config: AltMempoolConfig = { [ERC7562Rule.erep010]: { enabled: true, exceptions: [ - 'sender', + 'account', '0xdeadbeef', { depths: [3], From 2f2e8ae39516bf7fb6ef28f47639aa785d8502da Mon Sep 17 00:00:00 2001 From: Alex Forshtat Date: Sun, 9 Feb 2025 19:39:09 +0100 Subject: [PATCH 5/8] Fix wrong rule check --- .../bundler/src/modules/MempoolManager.ts | 21 ++++++++++++------- submodules/rip7560 | 2 +- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/packages/bundler/src/modules/MempoolManager.ts b/packages/bundler/src/modules/MempoolManager.ts index dba753ef..616a3e3c 100644 --- a/packages/bundler/src/modules/MempoolManager.ts +++ b/packages/bundler/src/modules/MempoolManager.ts @@ -19,6 +19,7 @@ import { MempoolEntry } from './MempoolEntry' import { ReputationManager } from './ReputationManager' import { BaseAltMempoolRule } from '@account-abstraction/validation-manager/src/altmempool/AltMempoolConfig' import { ERC7562Violation } from '@account-abstraction/validation-manager/dist/src/ERC7562Violation' +import { AccountAbstractionEntity } from '@account-abstraction/validation-manager/dist/src/AccountAbstractionEntity' const debug = Debug('aa.mempool') @@ -39,12 +40,14 @@ function isRuleViolated ( return false } for (const exception of override.exceptions ?? []) { - if (exception === violation.address) { + if (typeof exception === 'string' && exception.toLowerCase() === violation.address.toLowerCase()) { return false } if ( - (exception === 'account' || (typeof exception === 'object' && exception.role === 'account')) && - violation.address.toLowerCase() === userOp.sender.toLowerCase()) { + (exception === AccountAbstractionEntity.account || + (typeof exception === 'object' && exception.role === AccountAbstractionEntity.account.toString()) + ) && + violation.entity === AccountAbstractionEntity.account) { return false } // type RuleException = `0x${string}` | Role | AltMempoolRuleExceptionBase | AltMempoolRuleExceptionBannedOpcode @@ -142,7 +145,10 @@ export class MempoolManager { if (userOp.factory != null) { this.incrementEntryCount(userOp.factory) } - this.tryAssignToMempool(entry) + const mempoolsAssigned = this.tryAssignToMempool(entry) + if (mempoolsAssigned.length == 0) { + throw new RpcError(`UserOperation ${userOpHash} did not match any mempools due to the following violations: ${JSON.stringify(entry.ruleViolations)}`, ValidationErrors.OpcodeValidation) + } } this.updateSeenStatus(validationResult.aggregatorInfo?.addr, userOp, validationResult.senderInfo) } @@ -372,12 +378,12 @@ export class MempoolManager { } } - private tryAssignToMempool (entry: MempoolEntry): number[] { + private tryAssignToMempool (entry: MempoolEntry): string[] { if (entry.ruleViolations.length === 0) { this.mempool.push(entry) - return [0] + return ['0'] } - const mempoolIds: number[] = [] + const mempoolIds: string[] = [] console.log(`UserOperation ${entry.userOpHash}`) for (const [mempoolId, config] of Object.entries(this.altMempoolConfig)) { console.log(` Mempool ID: ${mempoolId} Config: ${JSON.stringify(config)}`) @@ -391,6 +397,7 @@ export class MempoolManager { if (this.altMempools[mempoolId] == null) { this.altMempools[mempoolId] = [] } + mempoolIds.push(mempoolId) this.altMempools[mempoolId].push(entry) this.reputationManager.updateSeenStatus(mempoolId) } diff --git a/submodules/rip7560 b/submodules/rip7560 index b38a294a..5903e385 160000 --- a/submodules/rip7560 +++ b/submodules/rip7560 @@ -1 +1 @@ -Subproject commit b38a294a7c35007610d0ce5498f053d2ee2f58e3 +Subproject commit 5903e3850d775f2eda30b8fcccb630b1253df0a5 From 9cb042d3065c94bb4584d8d6daa2e0d271c2fde5 Mon Sep 17 00:00:00 2001 From: Alex Forshtat Date: Mon, 10 Feb 2025 14:13:23 +0100 Subject: [PATCH 6/8] Fix not returning the canonical mempool in the "dump" API --- packages/bundler/src/modules/MempoolManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bundler/src/modules/MempoolManager.ts b/packages/bundler/src/modules/MempoolManager.ts index 616a3e3c..7e0d11d6 100644 --- a/packages/bundler/src/modules/MempoolManager.ts +++ b/packages/bundler/src/modules/MempoolManager.ts @@ -322,7 +322,7 @@ export class MempoolManager { */ debugDumpMempool (): MempoolDump { const mempoolDump: MempoolDump = {} - for (const [mempoolId, mempool] of Object.entries(this.altMempools)) { + for (const [mempoolId, mempool] of this._getAllMempoolsLoop()) { mempoolDump[mempoolId] = mempool.map(entry => entry.userOp) } return mempoolDump From 8fcd3b55651e183f2ee21c84575163c3cbec55f6 Mon Sep 17 00:00:00 2001 From: Alex Forshtat Date: Mon, 10 Feb 2025 14:41:06 +0100 Subject: [PATCH 7/8] Fix replace-by-fee bugs --- packages/bundler/src/modules/MempoolManager.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/bundler/src/modules/MempoolManager.ts b/packages/bundler/src/modules/MempoolManager.ts index 7e0d11d6..88276637 100644 --- a/packages/bundler/src/modules/MempoolManager.ts +++ b/packages/bundler/src/modules/MempoolManager.ts @@ -160,10 +160,11 @@ export class MempoolManager { let oldEntry: MempoolEntry | undefined if (index !== -1) { debug('replace userOp in alt-mempool', entry.userOp.sender, packedNonce, mempoolId) - oldEntry = this.mempool[index] + oldEntry = mempool[index] this.checkReplaceUserOp(oldEntry, entry) - this.mempool[index] = entry + mempool[index] = entry this.updateSeenStatus(oldEntry.aggregator, oldEntry.userOp, entry.validateUserOpResult.senderInfo, -1) + return true } } return false From 8923f7385cdd6f5290c570accb6fd4d512b4512e Mon Sep 17 00:00:00 2001 From: Alex Forshtat Date: Mon, 10 Feb 2025 14:48:45 +0100 Subject: [PATCH 8/8] Rename 'id' to 'entryId' for better searchability --- packages/bundler/src/modules/ReputationManager.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/bundler/src/modules/ReputationManager.ts b/packages/bundler/src/modules/ReputationManager.ts index f97df2a7..f110089b 100644 --- a/packages/bundler/src/modules/ReputationManager.ts +++ b/packages/bundler/src/modules/ReputationManager.ts @@ -40,10 +40,10 @@ export const NonBundlerReputationParams: ReputationParams = { /** * An entry whose "reputation" is tracked by the {@link ReputationManager}. * May represent either a smart contract with a role in an ERC-4337 transaction or an alternative mempool. - * The "id" is either a contract address or alt-mempool ID. + * The "entryId" is either a contract address or alt-mempool ID. */ export interface ReputationEntry { - id: string + entryId: string opsSeen: number opsIncluded: number status?: ReputationStatus @@ -73,7 +73,7 @@ export class ReputationManager { * debug: dump reputation map (with updated "status" for each entry) */ _debugDumpReputation (): ReputationEntry[] { - Object.values(this.entries).forEach(entry => { entry.status = this.getStatus(entry.id) }) + Object.values(this.entries).forEach(entry => { entry.status = this.getStatus(entry.entryId) }) return Object.values(this.entries) } @@ -105,7 +105,7 @@ export class ReputationManager { let entry = this.entries[id] if (entry == null) { this.entries[id] = entry = { - id, + entryId: id, opsSeen: 0, opsIncluded: 0 } @@ -214,8 +214,8 @@ export class ReputationManager { */ _debugSetReputation (reputations: ReputationEntry[]): ReputationEntry[] { reputations.forEach(rep => { - this.entries[rep.id.toLowerCase()] = { - id: rep.id.toLowerCase(), + this.entries[rep.entryId.toLowerCase()] = { + entryId: rep.entryId.toLowerCase(), opsSeen: BigNumber.from(rep.opsSeen).toNumber(), opsIncluded: BigNumber.from(rep.opsIncluded).toNumber() }