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/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/DebugMethodHandler.ts b/packages/bundler/src/DebugMethodHandler.ts index 605fc0d6..a9c99a84 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, validateAltMempoolConfigShape } from '@account-abstraction/validation-manager' export class DebugMethodHandler { constructor ( @@ -53,19 +54,19 @@ export class DebugMethodHandler { } async dumpMempool (): Promise { - return this.mempoolMgr.dump() + return this.mempoolMgr.debugDumpMempool() } clearMempool (): void { 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,11 @@ export class DebugMethodHandler { ow.object.exactShape(DebugBundlerConfigShape) return await this.execManager._setConfiguration(config) } + + async _setAltMempoolConfig (altMempoolConfig: AltMempoolConfig): Promise { + const configCleanedUp = JSON.parse(JSON.stringify(altMempoolConfig)) + validateAltMempoolConfigShape(configCleanedUp) + await this.mempoolMgr._setAltMempoolConfig(configCleanedUp) + await this._setConfiguration({}) + } } 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 a10816fc..5b57fe22 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 = this.mempoolManager.hasAltMempools() + 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..97299c14 100644 --- a/packages/bundler/src/modules/MempoolEntry.ts +++ b/packages/bundler/src/modules/MempoolEntry.ts @@ -1,5 +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 @@ -7,8 +9,10 @@ export class MempoolEntry { constructor ( readonly userOp: OperationBase, readonly userOpHash: string, + readonly validateUserOpResult: ValidateUserOpResult, 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..88276637 100644 --- a/packages/bundler/src/modules/MempoolManager.ts +++ b/packages/bundler/src/modules/MempoolManager.ts @@ -10,25 +10,78 @@ 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' +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') -type MempoolDump = OperationBase[] +interface MempoolDump {[mempoolId: string]: 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 (typeof exception === 'string' && exception.toLowerCase() === violation.address.toLowerCase()) { + return false + } + if ( + (exception === AccountAbstractionEntity.account || + (typeof exception === 'object' && exception.role === AccountAbstractionEntity.account.toString()) + ) && + violation.entity === AccountAbstractionEntity.account) { + 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: string]: MempoolEntry[] } = {} // count entities in mempool. private _entryCount: { [addr: string]: number | undefined } = {} + constructor ( + private readonly reputationManager: ReputationManager, + private altMempoolConfig: AltMempoolConfig) { + } + + /** + * 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()] } @@ -53,10 +106,6 @@ export class MempoolManager { } } - constructor ( - readonly reputationManager: ReputationManager) { - } - count (): number { return this.mempool.length } @@ -68,24 +117,21 @@ export class MempoolManager { skipValidation: boolean, userOp: OperationBase, userOpHash: string, - validationResult: ValidationResult + validationResult: ValidateUserOpResult ): void { const entry = new MempoolEntry( userOp, userOpHash, + validationResult, validationResult.returnInfo.prefund ?? 0, - (validationResult as ValidateUserOpResult).referencedContracts, + validationResult.referencedContracts, + validationResult.ruleViolations, skipValidation, 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) { @@ -99,14 +145,31 @@ export class MempoolManager { if (userOp.factory != null) { this.incrementEntryCount(userOp.factory) } - this.mempool.push(entry) - } - if (oldEntry != null) { - this.updateSeenStatus(oldEntry.aggregator, oldEntry.userOp, validationResult.senderInfo, -1) + 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) } + 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 = mempool[index] + this.checkReplaceUserOp(oldEntry, entry) + mempool[index] = entry + this.updateSeenStatus(oldEntry.aggregator, oldEntry.userOp, entry.validateUserOpResult.senderInfo, -1) + return true + } + } + return false + } + private updateSeenStatus (aggregator: string | undefined, userOp: OperationBase, senderInfo: StakeInfo, val = 1): void { try { this.reputationManager.checkStake('account', senderInfo) @@ -199,9 +262,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 @@ -210,9 +273,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 } @@ -225,18 +288,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) @@ -247,8 +321,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 this._getAllMempoolsLoop()) { + mempoolDump[mempoolId] = mempool.map(entry => entry.userOp) + } + return mempoolDump } /** @@ -300,4 +378,51 @@ export class MempoolManager { } } } + + private tryAssignToMempool (entry: MempoolEntry): string[] { + if (entry.ruleViolations.length === 0) { + this.mempool.push(entry) + return ['0'] + } + 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)}`) + 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}`) + if (this.altMempools[mempoolId] == null) { + this.altMempools[mempoolId] = [] + } + mempoolIds.push(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..f110089b 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 "entryId" is either a contract address or alt-mempool ID. + */ +export interface ReputationEntry { + entryId: 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.entryId) }) 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 = { + entryId: 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.entryId.toLowerCase()] = { + entryId: rep.entryId.toLowerCase(), opsSeen: BigNumber.from(rep.opsSeen).toNumber(), opsIncluded: BigNumber.from(rep.opsIncluded).toNumber() } }) - return this.dump() + return this._debugDumpReputation() } /** 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..4a7a7fc5 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. @@ -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/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..f8d6a5a2 100644 --- a/packages/validation-manager/src/altmempool/AltMempoolConfig.ts +++ b/packages/validation-manager/src/altmempool/AltMempoolConfig.ts @@ -1,8 +1,10 @@ +import ow from 'ow' + import { ERC7562Rule } from '../enum/ERC7562Rule' -type Role = 'sender' | 'paymaster' | 'factory' +export type Role = 'account' | 'paymaster' | 'factory' -type EnterOpcode = 'CALL' | 'DELEGATECALL' | 'CALLCODE' | 'STATICCALL' | 'CREATE' | 'CREATE2' +export type EnterOpcode = 'CALL' | 'DELEGATECALL' | 'CALLCODE' | 'STATICCALL' | 'CREATE' | 'CREATE2' export interface AltMempoolRuleExceptionBase { role?: Role @@ -25,15 +27,50 @@ export interface BaseAltMempoolRule { } export interface AltMempoolConfig { - [mempoolId: number]: { [rule in ERC7562Rule]?: BaseAltMempoolRule } + [mempoolId: string]: { [rule in ERC7562Rule]?: BaseAltMempoolRule } +} + +const AltMempoolRuleExceptionBaseShape = ow.object.partialShape({ + 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']) + ), ow.null), + enterMethodSelector: ow.optional.any(ow.optional.string.matches(/^0x[a-fA-F0-9]+$/), ow.null) +}) + +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(['account', '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]: { enabled: true, exceptions: [ - 'sender', + 'account', '0xdeadbeef', { depths: [3], @@ -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, 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