diff --git a/package.json b/package.json index 4fc60570..6aaa6259 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "scripts": { "bundler": "yarn --cwd packages/bundler bundler", "bundler-rip7560": "yarn --cwd packages/bundler bundler-rip7560", + "bundler-eip7702": "yarn --cwd packages/bundler bundler-eip7702", "runop": "yarn --cwd packages/bundler runop", "runop-goerli": "yarn runop --network goerli --unsafe", "create-all-deps": "jq '.dependencies,.devDependencies' packages/*/package.json |sort -u > all.deps", diff --git a/packages/bundler/localconfig/bundler.eip7702.config.json b/packages/bundler/localconfig/bundler.eip7702.config.json new file mode 100644 index 00000000..d2519d16 --- /dev/null +++ b/packages/bundler/localconfig/bundler.eip7702.config.json @@ -0,0 +1,20 @@ +{ + "chainId": 1337, + "gasFactor": "1", + "port": "3000", + "privateApiPort": "3001", + "network": "http://127.0.0.1:8545", + "tracerRpcUrl": "http://127.0.0.1:8545", + "entryPoint": "0x0000000071727De22E5E9d8BAf0edAc6f37da032", + "beneficiary": "0xd21934eD8eAf27a67f0A70042Af50A1D6d195E81", + "minBalance": "1", + "mnemonic": "./localconfig/mnemonic.txt", + "maxBundleGas": 30e6, + "minStake": "1", + "minUnstakeDelay": 0, + "autoBundleInterval": 3, + "autoBundleMempoolSize": 10, + "rip7560": false, + "rip7560Mode": "PULL", + "gethDevMode": true +} diff --git a/packages/bundler/package.json b/packages/bundler/package.json index 278c05f7..62c8b7c7 100644 --- a/packages/bundler/package.json +++ b/packages/bundler/package.json @@ -12,6 +12,7 @@ "runop": "ts-node ./src/runner/runop.ts", "bundler": "TS_NODE_TRANSPILE_ONLY=1 ts-node ./src/exec.ts --config ./localconfig/bundler.config.json", "bundler-rip7560": "ts-node ./src/exec.ts --config ./localconfig/bundler.rip7560.config.json", + "bundler-eip7702": "ts-node ./src/exec.ts --config ./localconfig/bundler.eip7702.config.json", "clear": "rm -rf dist artifacts cache src/types", "hardhat-compile": "hardhat compile", "hardhat-node": "npx hardhat node --no-deploy", diff --git a/packages/bundler/src/MethodHandlerERC4337.ts b/packages/bundler/src/MethodHandlerERC4337.ts index 0070a804..953470a1 100644 --- a/packages/bundler/src/MethodHandlerERC4337.ts +++ b/packages/bundler/src/MethodHandlerERC4337.ts @@ -24,7 +24,7 @@ import { requireCond, simulationRpcParams, tostr, - unpackUserOp + unpackUserOp, getAuthorizationList } from '@account-abstraction/utils' import { BundlerConfig } from './BundlerConfig' @@ -170,7 +170,7 @@ export class MethodHandlerERC4337 { to: userOp.sender, data: userOp.callData, // @ts-ignore - authorizationList: userOp.authorizationList + authorizationList: getAuthorizationList(userOp) } ] ).then(b => toNumber(b)).catch(err => { diff --git a/packages/bundler/src/modules/BundleManager.ts b/packages/bundler/src/modules/BundleManager.ts index a753a16e..45f6697b 100644 --- a/packages/bundler/src/modules/BundleManager.ts +++ b/packages/bundler/src/modules/BundleManager.ts @@ -102,6 +102,42 @@ export class BundleManager implements IBundleManager { await this.eventsManager.handlePastEvents() } + // parse revert from FailedOp(index,str) or FailedOpWithRevert(uint256 opIndex, string reason, bytes inner); + // return undefined values on failure. + parseFailedOpRevert (e: any): { opIndex?: number, reasonStr?: string } { + if (e.message != null) { + const match = e.message.match(/FailedOp\w*\((\d+),"(.*?)"/) + if (match != null) { + return { + opIndex: parseInt(match[1]), + reasonStr: match[2] + } + } + } + let parsedError: ErrorDescription + try { + let data = e.data?.data ?? e.data + // geth error body, packed in ethers exception object + const body = e?.error?.error?.body + if (body != null) { + const jsonBody = JSON.parse(body) + data = jsonBody.error.data?.data ?? jsonBody.error.data + } + + parsedError = this.entryPoint.interface.parseError(data) + } catch (e1) { + return { opIndex: undefined, reasonStr: undefined } + } + const { + opIndex, + reason + } = parsedError.args + return { + opIndex, + reasonStr: reason.toString() + } + } + /** * submit a bundle. * after submitting the bundle, remove all UserOps from the mempool @@ -149,31 +185,22 @@ export class BundleManager implements IBundleManager { userOpHashes: hashes } } catch (e: any) { - let parsedError: ErrorDescription - try { - let data = e.data?.data ?? e.data - // geth error body, packed in ethers exception object - const body = e?.error?.error?.body - if (body != null) { - const jsonbody = JSON.parse(body) - data = jsonbody.error.data?.data ?? jsonbody.error.data - } - - parsedError = this.entryPoint.interface.parseError(data) - } catch (e1) { + const { + opIndex, + reasonStr + } = this.parseFailedOpRevert(e) + if (opIndex == null || reasonStr == null) { this.checkFatal(e) console.warn('Failed handleOps, but non-FailedOp error', e) return } - const { - opIndex, - reason - } = parsedError.args const userOp = userOps[opIndex] - const reasonStr: string = reason.toString() const addr = await this._findEntityToBlame(reasonStr, userOp) if (addr != null) { + this.reputationManager.updateSeenStatus(userOp.sender, -1) + this.reputationManager.updateSeenStatus(userOp.paymaster, -1) + this.reputationManager.updateSeenStatus(userOp.factory, -1) this.mempoolManager.removeBannedAddr(addr) this.reputationManager.crashedHandleOps(addr) } else { @@ -248,11 +275,15 @@ export class BundleManager implements IBundleManager { async _findEntityToBlame (reasonStr: string, userOp: UserOperation): Promise { if (reasonStr.startsWith('AA3')) { // [EREP-030] A staked account is accountable for failure in any entity + console.log(`${reasonStr}: staked account ${await this.isAccountStaked(userOp)} ? sender ${userOp.sender} : pm ${userOp.paymaster}`) return await this.isAccountStaked(userOp) ? userOp.sender : userOp.paymaster } else if (reasonStr.startsWith('AA2')) { // [EREP-020] A staked factory is "accountable" for account + // [EREP-015]: paymaster is not blamed for account/factory failure + console.log(`${reasonStr}: staked factory ${await this.isFactoryStaked(userOp)} ? factory ${userOp.factory} : sender ${userOp.sender}`) return await this.isFactoryStaked(userOp) ? userOp.factory : userOp.sender } else if (reasonStr.startsWith('AA1')) { + // [EREP-015]: paymaster is not blamed for account/factory failure // (can't have staked account during its creation) return userOp.factory } @@ -352,7 +383,7 @@ export class BundleManager implements IBundleManager { console.warn('Skipping second validation for an injected debug operation, id=', entry.userOpHash) } } catch (e: any) { - this._handleSecondValidationException(e, paymaster, entry) + await this._handleSecondValidationException(e, paymaster, entry) continue } @@ -434,13 +465,53 @@ export class BundleManager implements IBundleManager { return [bundle, sharedAuthorizationList, storageMap] } - _handleSecondValidationException (e: any, paymaster: string | undefined, entry: MempoolEntry): void { + /** + * Merges the EIP-7702 authorizations from the given mempool entry into the provided authorization list. + * + * @param {MempoolEntry} entry - The mempool entry containing a list of UserOperation authorizations to be checked. + * @param {EIP7702Authorization[]} authList - The list of existing EIP-7702 authorizations to update. + * @return {boolean} - Returns `true` if the authorizations were successfully merged, otherwise `false`. + */ + mergeEip7702Authorizations (entry: MempoolEntry, authList: EIP7702Authorization[]): boolean { + // TODO: need to replace + for (const eip7702Authorization of getAuthorizationList(entry.userOp)) { + const existingAuthorization = authList + .find(it => { + return getEip7702AuthorizationSigner(it) === getEip7702AuthorizationSigner(eip7702Authorization) + }) + if (existingAuthorization != null && existingAuthorization.address.toLowerCase() !== eip7702Authorization.address.toLowerCase()) { + return false + } + // if (existingAuthorization == null && entry.userOp.authorizationList != null) { + // authList.push(...getAuthorizationList(entry.userOp)) + // } + } + return true + } + + async _handleSecondValidationException (e: any, paymaster: string | undefined, entry: MempoolEntry): Promise { debug('failed 2nd validation:', e.message) - // EREP-015: special case: if it is account/factory failure, then decreases paymaster's opsSeen - if (paymaster != null && this._isAccountOrFactoryError(e)) { - debug('don\'t blame paymaster', paymaster, ' for account/factory failure', e.message) - this.reputationManager.updateSeenStatus(paymaster, -1) + + const { + opIndex, + reasonStr + } = this.parseFailedOpRevert(e) + if (opIndex == null || reasonStr == null) { + this.checkFatal(e) + console.warn('Failed validation, but non-FailedOp error', e) + this.mempoolManager.removeUserOp(entry.userOp) + return } + + const addr = await this._findEntityToBlame(reasonStr, entry.userOp as UserOperation) + if (addr !== null) { + // undo all "updateSeen" of all entities, and only blame "addr": + this.reputationManager.updateSeenStatus(entry.userOp.sender, -1) + this.reputationManager.updateSeenStatus(entry.userOp.paymaster, -1) + this.reputationManager.updateSeenStatus(entry.userOp.factory, -1) + this.reputationManager.updateSeenStatus(addr, 1) + } + // failed validation. don't try anymore this userop this.mempoolManager.removeUserOp(entry.userOp) } diff --git a/packages/bundler/src/modules/MempoolManager.ts b/packages/bundler/src/modules/MempoolManager.ts index d6c4755a..1f942d4f 100644 --- a/packages/bundler/src/modules/MempoolManager.ts +++ b/packages/bundler/src/modules/MempoolManager.ts @@ -80,8 +80,9 @@ export class MempoolManager { ) const packedNonce = getPackedNonce(entry.userOp) const index = this._findBySenderNonce(userOp.sender, packedNonce) + let oldEntry: MempoolEntry | undefined if (index !== -1) { - const oldEntry = this.mempool[index] + oldEntry = this.mempool[index] this.checkReplaceUserOp(oldEntry, entry) debug('replace userOp', userOp.sender, packedNonce) this.mempool[index] = entry @@ -100,19 +101,22 @@ export class MempoolManager { } this.mempool.push(entry) } + if (oldEntry != null) { + this.updateSeenStatus(oldEntry.aggregator, oldEntry.userOp, validationResult.senderInfo, -1) + } this.updateSeenStatus(validationResult.aggregatorInfo?.addr, userOp, validationResult.senderInfo) } - private updateSeenStatus (aggregator: string | undefined, userOp: OperationBase, senderInfo: StakeInfo): void { + private updateSeenStatus (aggregator: string | undefined, userOp: OperationBase, senderInfo: StakeInfo, val = 1): void { try { this.reputationManager.checkStake('account', senderInfo) this.reputationManager.updateSeenStatus(userOp.sender) } catch (e: any) { if (!(e instanceof RpcError)) throw e } - this.reputationManager.updateSeenStatus(aggregator) - this.reputationManager.updateSeenStatus(userOp.paymaster) - this.reputationManager.updateSeenStatus(userOp.factory) + this.reputationManager.updateSeenStatus(aggregator, val) + this.reputationManager.updateSeenStatus(userOp.paymaster, val) + this.reputationManager.updateSeenStatus(userOp.factory, val) } private checkReputation ( diff --git a/packages/bundler/src/modules/ReputationManager.ts b/packages/bundler/src/modules/ReputationManager.ts index a840e52c..33a004c2 100644 --- a/packages/bundler/src/modules/ReputationManager.ts +++ b/packages/bundler/src/modules/ReputationManager.ts @@ -115,7 +115,7 @@ export class ReputationManager { return } const entry = this._getOrCreate(addr) - entry.opsSeen += val + entry.opsSeen = Math.max(0, entry.opsSeen + val) debug('after seen+', val, addr, entry) } diff --git a/packages/bundler/src/runBundler.ts b/packages/bundler/src/runBundler.ts index ff5b5bb9..e4d6e0f2 100644 --- a/packages/bundler/src/runBundler.ts +++ b/packages/bundler/src/runBundler.ts @@ -143,11 +143,6 @@ export async function runBundler (argv: string[], overrideExit = true): Promise< console.error('FATAL: --tracerRpcUrl requires the network provider to support prestateTracer') process.exit(1) } - const tracerProvider = new ethers.providers.JsonRpcProvider(config.tracerRpcUrl) - if (!await supportsNativeTracer(tracerProvider)) { - console.error('FATAL: --tracerRpcUrl requires a provider to support bundlerCollectorTracer') - process.exit(1) - } } else { // check standard javascript tracer: if (!await supportsDebugTraceCall(provider as any, config.rip7560)) { diff --git a/packages/utils/src/ERC4337Utils.ts b/packages/utils/src/ERC4337Utils.ts index 13f2815d..76a1328c 100644 --- a/packages/utils/src/ERC4337Utils.ts +++ b/packages/utils/src/ERC4337Utils.ts @@ -200,7 +200,7 @@ export function unpackUserOp (packed: PackedUserOperation): UserOperation { /** * abi-encode the userOperation - * @param op a PackedUserOp + * @param op1 a PackedUserOp * @param forSignature "true" if the hash is needed to calculate the getUserOpHash() * "false" to pack entire UserOp, for calculating the calldata cost of putting it on-chain. */ diff --git a/packages/utils/src/RIP7712NonceManagerUtils.ts b/packages/utils/src/RIP7712NonceManagerUtils.ts index dd5d49a2..056762f5 100644 --- a/packages/utils/src/RIP7712NonceManagerUtils.ts +++ b/packages/utils/src/RIP7712NonceManagerUtils.ts @@ -9,7 +9,7 @@ export const entryPointSalt = '0x90d8084deab30c2a37c45e8d47f49f2f7965183cb6990a9 export async function deployNonceManager (provider: JsonRpcProvider, signer = provider.getSigner()): Promise { const addr = await new DeterministicDeployer(provider, signer).deterministicDeploy(nonceManagerByteCode, entryPointSalt) - console.log("Deployed NonceManager contract at: ", addr) + console.log('Deployed NonceManager contract at: ', addr) return NonceManager__factory.connect(addr, signer) } diff --git a/packages/utils/src/Utils.ts b/packages/utils/src/Utils.ts index 25893e13..ded57377 100644 --- a/packages/utils/src/Utils.ts +++ b/packages/utils/src/Utils.ts @@ -172,7 +172,7 @@ export function mergeStorageMap (mergedStorageMap: StorageMap, validationStorage if (mergedStorageMap[addr] == null) { slots = mergedStorageMap[addr] = {} } else { - slots = mergedStorageMap[addr] as SlotMap + slots = mergedStorageMap[addr] } Object.entries(validationEntry).forEach(([slot, val]) => { diff --git a/packages/utils/src/interfaces/EIP7702Authorization.ts b/packages/utils/src/interfaces/EIP7702Authorization.ts index 998e601b..e84376ee 100644 --- a/packages/utils/src/interfaces/EIP7702Authorization.ts +++ b/packages/utils/src/interfaces/EIP7702Authorization.ts @@ -3,7 +3,6 @@ import RLP from 'rlp' import { bytesToHex, ecrecover, hexToBigInt, hexToBytes, PrefixedHexString, pubToAddress } from '@ethereumjs/util' import { AddressZero } from '../ERC4337Utils' import { keccak256 } from '@ethersproject/keccak256' -import { hexlify } from 'ethers/lib/utils' export interface EIP7702Authorization { chainId: BigNumberish @@ -31,12 +30,13 @@ export function getEip7702AuthorizationSigner (authorization: EIP7702Authorizati ) ] const messageHash = keccak256(rlpEncode) as `0x${string}` - // console.log('getEip7702AuthorizationSigner RLP:\n', hexlify(rlpEncode), rlpEncode.length) - // console.log('getEip7702AuthorizationSigner hash:\n', messageHash) const senderPubKey = ecrecover( hexToBytes(messageHash), + // eslint-disable-next-line @typescript-eslint/no-base-to-string hexToBigInt(authorization.yParity.toString() as `0x${string}`), + // eslint-disable-next-line @typescript-eslint/no-base-to-string hexToBytes(authorization.r.toString() as `0x${string}`), + // eslint-disable-next-line @typescript-eslint/no-base-to-string hexToBytes(authorization.s.toString() as `0x${string}`) ) const sender = bytesToHex(pubToAddress(senderPubKey)) diff --git a/packages/utils/src/interfaces/OperationBase.ts b/packages/utils/src/interfaces/OperationBase.ts index cddf756f..0523c30d 100644 --- a/packages/utils/src/interfaces/OperationBase.ts +++ b/packages/utils/src/interfaces/OperationBase.ts @@ -1,5 +1,7 @@ import { BigNumberish, BytesLike } from 'ethers' +import { EIP7702Authorization } from './EIP7702Authorization' + /** * The operation interface that is shared by ERC-4337 and RIP-7560 types. */ diff --git a/packages/validation-manager/src/BundlerCollectorTracer.ts b/packages/validation-manager/src/BundlerCollectorTracer.ts index 99226e2e..cc8bf564 100644 --- a/packages/validation-manager/src/BundlerCollectorTracer.ts +++ b/packages/validation-manager/src/BundlerCollectorTracer.ts @@ -59,6 +59,7 @@ export interface TopLevelCallInfo { contractSize: { [addr: string]: ContractSizeInfo } extCodeAccessInfo: { [addr: string]: string } oog?: boolean + calls?: [] } /** diff --git a/packages/validation-manager/src/GethTracer.ts b/packages/validation-manager/src/GethTracer.ts index 2c45ee4a..53e29d15 100644 --- a/packages/validation-manager/src/GethTracer.ts +++ b/packages/validation-manager/src/GethTracer.ts @@ -4,13 +4,15 @@ import { Deferrable } from '@ethersproject/properties' import { JsonRpcProvider, TransactionRequest } from '@ethersproject/providers' import { resolveProperties } from 'ethers/lib/utils' import { OperationRIP7560, RpcError } from '@account-abstraction/utils' +import { bundlerCollectorTracer } from './BundlerCollectorTracer' // from:https://geth.ethereum.org/docs/rpc/ns-debug#javascript-based-tracing const debug = Debug('aa.tracer') // the name of the native tracer. // equivalent to the javascript "bundlerCollectorTracer". -export const bundlerNativeTracerName = 'bundlerCollectorTracer' +export const bundlerJSTracerName = 'bundlerCollectorTracer' +export const GethNativeTracerName = 'erc7562Tracer' /** * a function returning a LogTracer. @@ -26,16 +28,17 @@ type LogTracerFunc = () => LogTracer * @param provider the network node to trace on * @param tx the transaction to trace * @param options the trace options - * @param nativeTracerProvider if set, submit only preStateTracer to the network provider, and use this (second) provider with native tracer. + * @param prestateTracerProvider if set, submit only preStateTracer to the network provider, and use this (second) provider with native tracer. * if null, then use javascript tracer on the first provider. */ // eslint-disable-next-line @typescript-eslint/naming-convention -export async function debug_traceCall (provider: JsonRpcProvider, tx: Deferrable, options: TraceOptions, nativeTracerProvider?: JsonRpcProvider): Promise { +export async function debug_traceCall (provider: JsonRpcProvider, tx: Deferrable, options: TraceOptions, prestateTracerProvider?: JsonRpcProvider): Promise { const tx1 = await resolveProperties(tx) - const traceOptions = tracer2string(options) - if (nativeTracerProvider != null) { - // there is a nativeTracerProvider: use it for the native tracer, but first we need preStateTracer from the main provider: + let traceOptions: TraceOptions + if (prestateTracerProvider != null) { + traceOptions = tracer2string(options) + // there is a prestateTracerProvider: use it for the native tracer, but first we need preStateTracer from the main provider: const preState: { [addr: string]: any } = await provider.send('debug_traceCall', [tx1, 'latest', { ...traceOptions, tracer: 'prestateTracer' }]) // fix prestate to be valid "state overrides" @@ -51,15 +54,18 @@ export async function debug_traceCall (provider: JsonRpcProvider, tx: Deferrable delete preState[key].storage } } - - const ret = await nativeTracerProvider.send('debug_traceCall', [tx1, 'latest', { - tracer: bundlerNativeTracerName, - stateOverrides: preState + traceOptions.tracer = getTracerBodyString(bundlerCollectorTracer) + traceOptions.stateOverrides = preState + const ret = await prestateTracerProvider.send('debug_traceCall', [tx1, 'latest', { + ...traceOptions }]) - return ret + } else if (options.tracer != null) { + traceOptions = tracer2string(options) + } else { + traceOptions = options + traceOptions.tracer = GethNativeTracerName } - const ret = await provider.send('debug_traceCall', [tx1, 'latest', traceOptions]).catch(e => { if (debug.enabled) { debug('ex=', e.error) diff --git a/packages/validation-manager/src/TracerResultParser.ts b/packages/validation-manager/src/TracerResultParser.ts index ee1c3e47..1478394a 100644 --- a/packages/validation-manager/src/TracerResultParser.ts +++ b/packages/validation-manager/src/TracerResultParser.ts @@ -2,125 +2,26 @@ // where xxx is OP/STO/COD/EP/SREP/EREP/UREP/ALT, and ### is a number // the validation rules are defined in erc-aa-validation.md import Debug from 'debug' -import { BigNumber, BigNumberish } from 'ethers' -import { hexZeroPad, Interface, keccak256 } from 'ethers/lib/utils' +import { BigNumber } from 'ethers' +import { hexZeroPad, keccak256 } from 'ethers/lib/utils' import { inspect } from 'util' -import { BundlerTracerResult } from './BundlerCollectorTracer' +import { AccessInfo, BundlerTracerResult, MethodInfo, TopLevelCallInfo } from './BundlerCollectorTracer' import { - IEntryPoint__factory, - IPaymaster__factory, OperationBase, RpcError, - SenderCreator__factory, StakeInfo, StorageMap, ValidationErrors, mapOf, requireCond, - toBytes32, AddressZero + toBytes32, AddressZero, UserOperation } from '@account-abstraction/utils' import { ValidationResult } from './IValidationManager' const debug = Debug('aa.handler.opcodes') -interface CallEntry { - to: string - from: string - type: string // call opcode - method: string // parsed method, or signash if unparsed - revert?: any // parsed output from REVERT - return?: any // parsed method output. - value?: BigNumberish -} - -const abi = Object.values([ - ...SenderCreator__factory.abi, - ...IEntryPoint__factory.abi, - ...IPaymaster__factory.abi -].reduce((set, entry) => { - const key = `${entry.name}(${entry.inputs.map(i => i.type).join(',')})` - // console.log('key=', key, keccak256(Buffer.from(key)).slice(0,10)) - return { - ...set, - [key]: entry - } -}, {})) as any - -/** - * parse all call operation in the trace. - * notes: - * - entries are ordered by the return (so nested call appears before its outer call - * - last entry is top-level return from "simulateValidation". it as ret and rettype, but no type or address - * @param tracerResults - */ -function parseCallStack ( - tracerResults: BundlerTracerResult -): CallEntry[] { - const xfaces = new Interface(abi) - - function callCatch (x: () => T, def: T1): T | T1 { - try { - return x() - } catch { - return def - } - } - - const out: CallEntry[] = [] - const stack: any[] = [] - tracerResults.calls - .filter(x => !x.type.startsWith('depth')) - .forEach(c => { - if (c.type.match(/REVERT|RETURN/) != null) { - const top = stack.splice(-1)[0] ?? { - type: 'top', - method: 'validateUserOp' - } - const returnData: string = (c as any).data - if (top.type.match(/CREATE/) != null) { - out.push({ - to: top.to, - from: top.from, - type: top.type, - method: '', - return: `len=${returnData.length}` - }) - } else { - const method = callCatch(() => xfaces.getFunction(top.method), top.method) - if (c.type === 'REVERT') { - const parsedError = callCatch(() => xfaces.parseError(returnData), returnData) - out.push({ - to: top.to, - from: top.from, - type: top.type, - method: method.name, - value: top.value, - revert: parsedError - }) - } else { - const ret = callCatch(() => xfaces.decodeFunctionResult(method, returnData), returnData) - out.push({ - to: top.to, - from: top.from, - type: top.type, - value: top.value, - method: method.name ?? method, - return: ret - }) - } - } - } else { - stack.push(c) - } - }) - - // TODO: verify that stack is empty at the end. - - return out -} - /** * slots associated with each entity. * keccak( A || ...) is associated with "A" @@ -189,6 +90,11 @@ function getEntityTitle (userOp: OperationBase, entityAddress: string): string { } } +// opcodes from [OP-011] +const bannedOpCodes = new Set(['GASPRICE', 'GASLIMIT', 'DIFFICULTY', 'TIMESTAMP', 'BASEFEE', 'BLOCKHASH', 'NUMBER', 'ORIGIN', 'GAS', 'CREATE', 'COINBASE', 'SELFDESTRUCT', 'RANDOM', 'PREVRANDAO', 'INVALID']) +// opcodes allowed in staked entities [OP-080] +const opcodesOnlyInStakedEntities = new Set(['BALANCE', 'SELFBALANCE']) + /** * parse collected simulation traces and revert if they break our rules * @param userOp the userOperation that was used in this simulation @@ -206,16 +112,12 @@ export function tracerResultParser ( debug('=== simulation result:', inspect(tracerResults, true, 10, true)) // todo: block access to no-code addresses (might need update to tracer) - // opcodes from [OP-011] - const bannedOpCodes = new Set(['GASPRICE', 'GASLIMIT', 'DIFFICULTY', 'TIMESTAMP', 'BASEFEE', 'BLOCKHASH', 'NUMBER', 'ORIGIN', 'GAS', 'CREATE', 'COINBASE', 'SELFDESTRUCT', 'RANDOM', 'PREVRANDAO', 'INVALID']) - // opcodes allowed in staked entities [OP-080] - const opcodesOnlyInStakedEntities = new Set(['BALANCE', 'SELFBALANCE']) // eslint-disable-next-line @typescript-eslint/no-base-to-string if (Object.values(tracerResults.callsFromEntryPoint).length < 1) { throw new Error('Unexpected traceCall result: no calls from entrypoint.') } if (tracerResults.calls != null) { - const callStack = parseCallStack(tracerResults) + const callStack = tracerResults.calls.filter((call: any) => call.topLevelTargetAddress == null) as MethodInfo[] // [OP-052], [OP-053] const callInfoEntryPoint = callStack.find(call => call.to?.toLowerCase() === entryPointAddress?.toLowerCase() && call.from?.toLowerCase() !== entryPointAddress?.toLowerCase() && @@ -255,196 +157,211 @@ export function tracerResultParser ( } const entitySlots: { [addr: string]: Set } = parseEntitySlots(stakeInfoEntities, tracerResults.keccak) - Object.entries(stakeInfoEntities).forEach(([entityAddress, entStakes]) => { const entityTitle = getEntityTitle(userOp, entityAddress) - const currentNumLevel = tracerResults.callsFromEntryPoint.find(info => info.topLevelTargetAddress.toLowerCase() === entityAddress.toLowerCase()) - if (currentNumLevel == null) { - if (entityAddress.toLowerCase() === userOp.sender.toLowerCase()) { - // should never happen... only factory, paymaster are optional. - throw new RpcError('missing trace into account validation', ValidationErrors.InvalidFields) + const entityCallsFromEntryPoint = tracerResults.callsFromEntryPoint.filter( + call => call.topLevelTargetAddress != null && call.topLevelTargetAddress.toLowerCase() === entityAddress.toLowerCase()) + entityCallsFromEntryPoint.forEach((entityCall) => { + if (entityCall == null) { + if (entityAddress.toLowerCase() === userOp.sender.toLowerCase()) { + // should never happen... only factory, paymaster are optional. + throw new RpcError('missing trace into account validation', ValidationErrors.InvalidFields) + } + return } - return - } - const opcodes = currentNumLevel.opcodes - const access = currentNumLevel.access - - // [OP-020] - requireCond(!(currentNumLevel.oog ?? false), - `${entityTitle} internally reverts on oog`, ValidationErrors.OpcodeValidation) - - // opcodes from [OP-011] - Object.keys(opcodes).forEach(opcode => { - requireCond(!bannedOpCodes.has(opcode), `${entityTitle} uses banned opcode: ${opcode}`, ValidationErrors.OpcodeValidation) - // [OP-080] - requireCond(!opcodesOnlyInStakedEntities.has(opcode) || isStaked(entStakes), `unstaked ${entityTitle} uses banned opcode: ${opcode}`, ValidationErrors.OpcodeValidation) + processEntityCall(entityCall, entityAddress, entityTitle, entStakes, entitySlots, userOp as UserOperation, stakeInfoEntities, entryPointAddress, tracerResults) + }) + }) + // return list of contract addresses by this UserOp. already known not to contain zero-sized addresses. + const addresses = tracerResults.callsFromEntryPoint.flatMap(level => Object.keys(level.contractSize)) + const storageMap: StorageMap = {} + tracerResults.callsFromEntryPoint.forEach(level => { + Object.keys(level.access).forEach(addr => { + storageMap[addr] = storageMap[addr] ?? level.access[addr].reads }) - // [OP-031] - if (entityTitle === 'factory') { - requireCond((opcodes.CREATE2 ?? 0) <= 1, `${entityTitle} with too many CREATE2`, ValidationErrors.OpcodeValidation) - } else { - requireCond(opcodes.CREATE2 == null, `${entityTitle} uses banned opcode: CREATE2`, ValidationErrors.OpcodeValidation) + }) + return [addresses, storageMap] +} + +function processEntityCall (entityCall: TopLevelCallInfo, entityAddress: string, entityTitle: string, entStakes: StakeInfo, entitySlots: { [addr: string]: Set }, userOp: UserOperation, stakeInfoEntities: {[addr: string]: StakeInfo}, entryPointAddress: string, tracerResults: BundlerTracerResult): void { + const opcodes = entityCall.opcodes + const access = entityCall.access + + // [OP-020] + requireCond(!(entityCall.oog ?? false), + `${entityTitle} internally reverts on oog`, ValidationErrors.OpcodeValidation) + + // opcodes from [OP-011] + Object.keys(opcodes).forEach(opcode => { + requireCond(!bannedOpCodes.has(opcode), `${entityTitle} uses banned opcode: ${opcode}`, ValidationErrors.OpcodeValidation) + // [OP-080] + requireCond(!opcodesOnlyInStakedEntities.has(opcode) || isStaked(entStakes), + `unstaked ${entityTitle} uses banned opcode: ${opcode}`, ValidationErrors.OpcodeValidation) + }) + // [OP-031] + if (entityTitle === 'factory') { + requireCond((opcodes.CREATE2 ?? 0) <= 1, `${entityTitle} with too many CREATE2`, ValidationErrors.OpcodeValidation) + } else { + requireCond(opcodes.CREATE2 == null, `${entityTitle} uses banned opcode: CREATE2`, ValidationErrors.OpcodeValidation) + } + Object.entries(access).forEach(([addr, { + reads, + writes, + transientReads, + transientWrites + }]) => { + // testing read/write access on contract "addr" + if (addr.toLowerCase() === userOp.sender.toLowerCase()) { + // allowed to access sender's storage + // [STO-010] + return } - Object.entries(access).forEach(([addr, { - reads, - writes, - transientReads, - transientWrites - }]) => { - // testing read/write access on contract "addr" - if (addr.toLowerCase() === sender.toLowerCase()) { - // allowed to access sender's storage - // [STO-010] - return - } + if (addr.toLowerCase() === entryPointAddress.toLowerCase()) { + // ignore storage access on entryPoint (balance/deposit of entities. + // we block them on method calls: only allowed to deposit, never to read + return + } - if (addr.toLowerCase() === entryPointAddress.toLowerCase()) { - // ignore storage access on entryPoint (balance/deposit of entities. - // we block them on method calls: only allowed to deposit, never to read - return - } + debug('dump keccak calculations and reads', { + entityTitle, + entityAddress, + k: mapOf(tracerResults.keccak, k => keccak256(k)), + reads + }) - // return true if the given slot is associated with the given address, given the known keccak operations: - // @param slot the SLOAD/SSTORE slot address we're testing - // @param addr - the address we try to check for association with - // @param reverseKeccak - a mapping we built for keccak values that contained the address - function associatedWith (slot: string, addr: string, entitySlots: { [addr: string]: Set }): boolean { - const addrPadded = hexZeroPad(addr, 32).toLowerCase() - if (slot === addrPadded) { - return true - } - const k = entitySlots[addr] - if (k == null) { - return false - } - const slotN = BigNumber.from(slot) - // scan all slot entries to check of the given slot is within a structure, starting at that offset. - // assume a maximum size on a (static) structure size. - for (const k1 of k.keys()) { - const kn = BigNumber.from(k1) - if (slotN.gte(kn) && slotN.lt(kn.add(128))) { - return true + // [OP-070]: treat transient storage (TLOAD/TSTORE) just like storage. + // scan all slots. find a referenced slot + // at the end of the scan, we will check if the entity has stake, and report that slot if not. + let requireStakeSlot: string | undefined + [ + ...Object.keys(writes), + ...Object.keys(reads), + ...Object.keys(transientWrites ?? {}), + ...Object.keys(transientReads ?? {}) + ].forEach(slot => { + // slot associated with sender is allowed (e.g. token.balanceOf(sender) + // but during initial UserOp (where there is an initCode), it is allowed only for staked entity + if (associatedWith(slot, userOp.sender.toLowerCase(), entitySlots)) { + if (userOp.factory != null && userOp.factory !== AddressZero) { + // special case: account.validateUserOp is allowed to use assoc storage if factory is staked. + // [STO-022], [STO-021] + if (!(entityAddress.toLowerCase() === userOp.sender.toLowerCase() && isStaked(stakeInfoEntities[userOp.factory.toLowerCase()]))) { + requireStakeSlot = slot } } - return false + } else if (associatedWith(slot, entityAddress, entitySlots)) { + // [STO-032] + // accessing a slot associated with entityAddr (e.g. token.balanceOf(paymaster) + requireStakeSlot = slot + } else if (addr.toLowerCase() === entityAddress.toLowerCase()) { + // [STO-031] + // accessing storage member of entity itself requires stake. + requireStakeSlot = slot + } else if (writes[slot] == null && transientWrites[slot] == null) { + // [STO-033]: staked entity have read-only access to any storage in non-entity contract. + requireStakeSlot = slot + } else { + // accessing arbitrary storage of another contract is not allowed + const isWrite = Object.keys(writes).includes(slot) || Object.keys(transientWrites ?? {}).includes(slot) + const isTransient = Object.keys(transientReads ?? {}).includes(slot) || Object.keys(transientWrites ?? {}).includes(slot) + const readWrite = isWrite ? 'write to' : 'read from' + const transientStr = isTransient ? 'transient ' : '' + requireCond(false, + `${entityTitle} has forbidden ${readWrite} ${transientStr}${nameAddr(addr, stakeInfoEntities)} slot ${slot}`, + ValidationErrors.OpcodeValidation, { [entityTitle]: entStakes?.addr }) } + }) - debug('dump keccak calculations and reads', { - entityTitle, - entityAddress, - k: mapOf(tracerResults.keccak, k => keccak256(k)), - reads - }) - - // [OP-070]: treat transient storage (TLOAD/TSTORE) just like storage. - // scan all slots. find a referenced slot - // at the end of the scan, we will check if the entity has stake, and report that slot if not. - let requireStakeSlot: string | undefined - [ - ...Object.keys(writes), - ...Object.keys(reads), - ...Object.keys(transientWrites ?? {}), - ...Object.keys(transientReads ?? {}) - ].forEach(slot => { - // slot associated with sender is allowed (e.g. token.balanceOf(sender) - // but during initial UserOp (where there is an initCode), it is allowed only for staked entity - if (associatedWith(slot, sender, entitySlots)) { - if (userOp.factory != null && userOp.factory !== AddressZero) { - // special case: account.validateUserOp is allowed to use assoc storage if factory is staked. - // [STO-022], [STO-021] - if (!(entityAddress.toLowerCase() === sender.toLowerCase() && isStaked(stakeInfoEntities[userOp.factory.toLowerCase()]))) { - requireStakeSlot = slot - } - } - } else if (associatedWith(slot, entityAddress, entitySlots)) { - // [STO-032] - // accessing a slot associated with entityAddr (e.g. token.balanceOf(paymaster) - requireStakeSlot = slot - } else if (addr.toLowerCase() === entityAddress.toLowerCase()) { - // [STO-031] - // accessing storage member of entity itself requires stake. - requireStakeSlot = slot - } else if (writes[slot] == null && transientWrites[slot] == null) { - // [STO-033]: staked entity have read-only access to any storage in non-entity contract. - requireStakeSlot = slot - } else { - // accessing arbitrary storage of another contract is not allowed - const isWrite = Object.keys(writes).includes(slot) || Object.keys(transientWrites ?? {}).includes(slot) - const isTransient = Object.keys(transientReads ?? {}).includes(slot) || Object.keys(transientWrites ?? {}).includes(slot) - const readWrite = isWrite ? 'write to' : 'read from' - const transientStr = isTransient ? 'transient ' : '' - requireCond(false, - `${entityTitle} has forbidden ${readWrite} ${transientStr}${nameAddr(addr, entityTitle)} slot ${slot}`, - ValidationErrors.OpcodeValidation, { [entityTitle]: entStakes?.addr }) - } - }) - - // if addr is current account/paymaster/factory, then return that title - // otherwise, return addr as-is - function nameAddr (addr: string, currentEntity: string): string { - const [title] = Object.entries(stakeInfoEntities).find(([title, info]) => - info?.addr.toLowerCase() === addr.toLowerCase()) ?? [] + requireCondAndStake(requireStakeSlot != null, entStakes, + `unstaked ${entityTitle} accessed ${nameAddr(addr, stakeInfoEntities)} slot ${requireStakeSlot}`, entityTitle, access) + }) - return title ?? addr - } - requireCondAndStake(requireStakeSlot != null, entStakes, - `unstaked ${entityTitle} accessed ${nameAddr(addr, entityTitle)} slot ${requireStakeSlot}`) + // the only contract we allow to access before its deployment is the "sender" itself, which gets created. + let illegalZeroCodeAccess: any + for (const addr of Object.keys(entityCall.contractSize)) { + // [OP-042] + if (addr.toLowerCase() !== userOp.sender.toLowerCase() && addr.toLowerCase() !== entryPointAddress.toLowerCase() && entityCall.contractSize[addr].contractSize <= 2) { + illegalZeroCodeAccess = entityCall.contractSize[addr] + illegalZeroCodeAccess.address = addr + break + } + } + // [OP-041] + requireCond( + illegalZeroCodeAccess == null, + `${entityTitle} accesses un-deployed contract address ${illegalZeroCodeAccess?.address as string} with opcode ${illegalZeroCodeAccess?.opcode as string}`, + ValidationErrors.OpcodeValidation) + + let illegalEntryPointCodeAccess + for (const addr of Object.keys(entityCall.extCodeAccessInfo)) { + if (addr.toLowerCase() === entryPointAddress.toLowerCase()) { + illegalEntryPointCodeAccess = entityCall.extCodeAccessInfo[addr] + break + } + } + requireCond( + illegalEntryPointCodeAccess == null, + `${entityTitle} accesses EntryPoint contract address ${entryPointAddress} with opcode ${illegalEntryPointCodeAccess}`, + ValidationErrors.OpcodeValidation) + + // Recursively handling all subcalls to check validation rules + if (entityCall.calls != null) { + entityCall.calls.forEach((call: any) => { + processEntityCall(call, entityAddress, entityTitle, entStakes, entitySlots, userOp, stakeInfoEntities, entryPointAddress, tracerResults) }) + } +} - // check if the given entity is staked - function isStaked (entStake?: StakeInfo): boolean { - return entStake != null && BigNumber.from(1).lte(entStake.stake) && BigNumber.from(1).lte(entStake.unstakeDelaySec) - } +// helper method: if condition is true, then entity must be staked. +function requireCondAndStake (cond: boolean, entStake: StakeInfo | undefined, failureMessage: string, entityTitle: string, access: { [address: string]: AccessInfo }): void { + if (!cond) { + return + } + if (entStake == null) { + throw new Error(`internal: ${entityTitle} not in userOp, but has storage accesses in ${JSON.stringify(access)}`) + } + requireCond(isStaked(entStake), + failureMessage, ValidationErrors.OpcodeValidation, { [entityTitle]: entStake?.addr }) - // helper method: if condition is true, then entity must be staked. - function requireCondAndStake (cond: boolean, entStake: StakeInfo | undefined, failureMessage: string): void { - if (!cond) { - return - } - if (entStake == null) { - throw new Error(`internal: ${entityTitle} not in userOp, but has storage accesses in ${JSON.stringify(access)}`) - } - requireCond(isStaked(entStake), - failureMessage, ValidationErrors.OpcodeValidation, { [entityTitle]: entStakes?.addr }) + // TODO: check real minimum stake values +} - // TODO: check real minimum stake values - } +// check if the given entity is staked +function isStaked (entStake?: StakeInfo): boolean { + return entStake != null && BigNumber.from(1).lte(entStake.stake) && BigNumber.from(1).lte(entStake.unstakeDelaySec) +} - // the only contract we allow to access before its deployment is the "sender" itself, which gets created. - let illegalZeroCodeAccess: any - for (const addr of Object.keys(currentNumLevel.contractSize)) { - // [OP-042] - if (addr !== sender && addr.toLowerCase() !== entryPointAddress.toLowerCase() && currentNumLevel.contractSize[addr].contractSize <= 2) { - illegalZeroCodeAccess = currentNumLevel.contractSize[addr] - illegalZeroCodeAccess.address = addr - break - } - } - // [OP-041] - requireCond( - illegalZeroCodeAccess == null, - `${entityTitle} accesses un-deployed contract address ${illegalZeroCodeAccess?.address as string} with opcode ${illegalZeroCodeAccess?.opcode as string}`, ValidationErrors.OpcodeValidation) - - let illegalEntryPointCodeAccess - for (const addr of Object.keys(currentNumLevel.extCodeAccessInfo)) { - if (addr.toLowerCase() === entryPointAddress.toLowerCase()) { - illegalEntryPointCodeAccess = currentNumLevel.extCodeAccessInfo[addr] - break - } - } - requireCond( - illegalEntryPointCodeAccess == null, - `${entityTitle} accesses EntryPoint contract address ${entryPointAddress} with opcode ${illegalEntryPointCodeAccess}`, ValidationErrors.OpcodeValidation) - }) +// if addr is current account/paymaster/factory, then return that title +// otherwise, return addr as-is +function nameAddr (addr: string, stakeInfoEntities: {[addr: string]: StakeInfo}): string { + const [title] = Object.entries(stakeInfoEntities).find(([title, info]) => + info?.addr.toLowerCase() === addr.toLowerCase()) ?? [] - // return list of contract addresses by this UserOp. already known not to contain zero-sized addresses. - const addresses = tracerResults.callsFromEntryPoint.flatMap(level => Object.keys(level.contractSize)) - const storageMap: StorageMap = {} - tracerResults.callsFromEntryPoint.forEach(level => { - Object.keys(level.access).forEach(addr => { - storageMap[addr] = storageMap[addr] ?? level.access[addr].reads - }) - }) - return [addresses, storageMap] + return title ?? addr +} + +// return true if the given slot is associated with the given address, given the known keccak operations: +// @param slot the SLOAD/SSTORE slot address we're testing +// @param addr - the address we try to check for association with +// @param reverseKeccak - a mapping we built for keccak values that contained the address +function associatedWith (slot: string, addr: string, entitySlots: { [addr: string]: Set }): boolean { + const addrPadded = hexZeroPad(addr, 32).toLowerCase() + if (slot === addrPadded) { + return true + } + const k = entitySlots[addr] + if (k == null) { + return false + } + const slotN = BigNumber.from(slot) + // scan all slot entries to check of the given slot is within a structure, starting at that offset. + // assume a maximum size on a (static) structure size. + for (const k1 of k.keys()) { + const kn = BigNumber.from(k1) + if (slotN.gte(kn) && slotN.lt(kn.add(128))) { + return true + } + } + return false } diff --git a/packages/validation-manager/src/ValidationManager.ts b/packages/validation-manager/src/ValidationManager.ts index cd56b406..187d7fa3 100644 --- a/packages/validation-manager/src/ValidationManager.ts +++ b/packages/validation-manager/src/ValidationManager.ts @@ -28,15 +28,16 @@ import { packUserOp, requireAddressAndFields, requireCond, - runContractScript, getAuthorizationList + runContractScript, getAuthorizationList, SenderCreator__factory, IEntryPoint__factory, IPaymaster__factory } from '@account-abstraction/utils' import { tracerResultParser } from './TracerResultParser' -import { BundlerTracerResult, bundlerCollectorTracer, ExitInfo } from './BundlerCollectorTracer' +import { bundlerCollectorTracer, BundlerTracerResult, ExitInfo } from './BundlerCollectorTracer' import { debug_traceCall } from './GethTracer' import EntryPointSimulationsJson from '@account-abstraction/contracts/artifacts/EntryPointSimulations.json' import { IValidationManager, ValidateUserOpResult, ValidationResult } from './IValidationManager' +import { Interface } from 'ethers/lib/utils' const debug = Debug('aa.mgr.validate') @@ -152,22 +153,36 @@ export class ValidationManager implements IValidationManager { }, ...stateOverride } - const tracerResult: BundlerTracerResult = await debug_traceCall(provider, { + let tracer + if (!this.usingErc7562NativeTracer()) { + tracer = bundlerCollectorTracer + } + const tracerResult = await debug_traceCall(provider, { from: AddressZero, to: this.entryPoint.address, data: simulateCall, gasLimit: simulationGas }, { - tracer: bundlerCollectorTracer, + tracer, stateOverrides }, this.providerForTracer ) - const lastResult = tracerResult.calls.slice(-1)[0] - const data = (lastResult as ExitInfo).data - if (lastResult.type === 'REVERT') { - throw new RpcError(decodeRevertReason(data, false) as string, ValidationErrors.SimulateValidation) + let data: any + if (!this.usingErc7562NativeTracer()) { + // Using preState tracer + JS tracer + const lastResult = tracerResult.calls.slice(-1)[0] + data = (lastResult as ExitInfo).data + if (lastResult.type === 'REVERT') { + throw new RpcError(decodeRevertReason(data, false) as string, ValidationErrors.SimulateValidation) + } + } else { + // Using Native tracer + data = tracerResult.output + if (tracerResult.error != null && (tracerResult.error as string).includes('execution reverted')) { + throw new RpcError(decodeRevertReason(data, false) as string, ValidationErrors.SimulateValidation) + } } // // Hack to handle SELFDESTRUCT until we fix entrypoint // if (data === '0x') { @@ -229,6 +244,13 @@ export class ValidationManager implements IValidationManager { [res, tracerResult] = await this._geth_traceCall_SimulateValidation(userOp, stateOverrideForEip7702).catch(e => { throw e }) + // console.log('validation res', res) + // todo fix + if (this.usingErc7562NativeTracer()) { + this.convertTracerResult(tracerResult, userOp) + } + // console.log('tracer res') + // console.dir(tracerResult, { depth: null }) let contractAddresses: string[] [contractAddresses, storageMap] = tracerResultParser(userOp, tracerResult, res, this.entryPoint.address) // if no previous contract hashes, then calculate hashes of contracts @@ -369,4 +391,151 @@ export class ValidationManager implements IValidationManager { async getOperationHash (userOp: OperationBase): Promise { return await this.entryPoint.getUserOpHash(packUserOp(userOp as UserOperation)) } + + // todo fix rest of the code to work with the new tracer result instead of adjusting it here + convertTracerResult (tracerResult: any, userOp: UserOperation): BundlerTracerResult { + const SENDER_CREATOR = '0xefc2c1444ebcc4db75e7613d20c6a62ff67a167c'.toLowerCase() + // Before flattening we add top level addresses for calls from EntryPoint and from SENDER_CREATOR + tracerResult.calls.forEach((call: {calls: any, to: any, topLevelTargetAddress: any}) => { + call.topLevelTargetAddress = call.to + if (call.to.toLowerCase() === SENDER_CREATOR && call.calls != null) { + call.calls.forEach((subcall: any) => { + subcall.topLevelTargetAddress = subcall.to + }) + } + }) + tracerResult.calls = this.flattenCalls(tracerResult.calls) + tracerResult.calls.forEach((call: { topLevelTargetAddress: any, method: any, input: any, to: any, from: any, opcodes: any, usedOpcodes: any, access: any, accessedSlots: any, extCodeAccessInfo: any, outOfGas: any, oog: any }) => { + call.opcodes = {} + if (call.usedOpcodes != null) { + Object.keys(call.usedOpcodes).forEach((opcode: string) => { + call.opcodes[this.getOpcodeName(parseInt(opcode))] = call.usedOpcodes[opcode] + }) + } + + if (call.access == null) { + call.access = {} + } + if (call.accessedSlots != null) { + call.access[call.to] = { + reads: call.accessedSlots.reads ?? {}, + writes: call.accessedSlots.writes ?? {}, + transientReads: call.accessedSlots.transientReads ?? {}, + transientWrites: call.accessedSlots.transientWrites ?? {} + } + Object.keys(call.access[call.to].reads).forEach((slot) => { + if (call.access[call.to].reads[slot] != null && call.access[call.to].reads[slot].length > 0) { + call.access[call.to].reads[slot] = call.access[call.to].reads[slot][0] + } + }) + } + if (call.extCodeAccessInfo == null) { + call.extCodeAccessInfo = {} + } + const newExtCode: any = {} + if (Array.isArray(call.extCodeAccessInfo)) { + call.extCodeAccessInfo.forEach((addr: any) => { + newExtCode[addr] = 1 + }) + } + call.extCodeAccessInfo = newExtCode + call.oog = call.outOfGas + + // Adding method name + if (call.topLevelTargetAddress == null && call.to.toLowerCase() === this.entryPoint.address.toLowerCase()) { + if (call.input.length <= 2) { + call.method = '0x' + } else { + const mergedAbi = Object.values([ + ...SenderCreator__factory.abi, + ...IEntryPoint__factory.abi, + ...IPaymaster__factory.abi + ].reduce((set, entry) => { + const key = `${entry.name}(${entry.inputs.map(i => i.type).join(',')})` + // console.log('key=', key, keccak256(Buffer.from(key)).slice(0,10)) + return { + ...set, + [key]: entry + } + }, {})) as any + const AbiInterfaces = new Interface(mergedAbi) + + function callCatch (x: () => T, def: T1): T | T1 { + try { + return x() + } catch { + return def + } + } + const methodSig = call.input.slice(0, 10) + const method = callCatch(() => AbiInterfaces.getFunction(methodSig), methodSig) + call.method = method.name + } + } + }) + // TODO: This is a hardcoded address of SenderCreator immutable member in EntryPoint. Any change in EntryPoint's code + // requires a change of this address. + // TODO check why the filter fails test_ban_user_op_access_other_ops_sender_in_bundle + tracerResult.callsFromEntryPoint = tracerResult.calls // .filter((call: { from: string }) => call.from.toLowerCase() === this.entryPoint.address.toLowerCase() || call.from.toLowerCase() === SENDER_CREATOR) + + return tracerResult + } + + flattenCalls (calls: any[]): any[] { + return calls.reduce((acc: any, call: any) => { + acc.push(call) // Add the current call to the accumulator + if (call.calls != null) { + acc.push(...this.flattenCalls(call.calls)) // Recursively flatten the nested calls + } + return acc + }, []) + } + + getOpcodeName (opcodeNumber: number): string | number { + const opcodeNames: { [key: number]: string } = { + 0x30: 'ADDRESS', + 0x31: 'BALANCE', + 0x32: 'ORIGIN', + 0x3A: 'GASPRICE', + 0x40: 'BLOCKHASH', + 0x41: 'COINBASE', + 0x42: 'TIMESTAMP', + 0x43: 'NUMBER', + 0x44: 'DIFFICULTY', // PREVRANDAO, RANDOM + 0x45: 'GASLIMIT', + 0x47: 'SELFBALANCE', + 0x48: 'BASEFEE', + 0x49: 'BLOBHASH', + 0x4A: 'BLOBBASEFEE', + 0x5A: 'GAS', + 0xF0: 'CREATE', + 0xFF: 'SELFDESTRUCT', + 0x3B: 'EXTCODESIZE', + 0x3C: 'EXTCODECOPY', + 0x3F: 'EXTCODEHASH', + 0x46: 'CHAINID', + 0x51: 'MLOAD', + 0x52: 'MSTORE', + 0x53: 'MSTORE8', + 0x54: 'SLOAD', + 0x55: 'SSTORE', + 0x5B: 'JUMPDEST', + 0x5C: 'TLOAD', + 0x5D: 'TSTORE', + 0x5E: 'MCOPY', + 0x5F: 'PUSH0', + 0xF1: 'CALL', + 0xF3: 'RETURN', + 0xF4: 'DELEGATECALL', + 0xF5: 'CREATE2', + 0xFA: 'STATICCALL', + 0xFD: 'REVERT', + 0xFE: 'INVALID' + } + return opcodeNames[opcodeNumber] ?? opcodeNumber + } + + usingErc7562NativeTracer (): boolean { + return this.providerForTracer == null + } } diff --git a/packages/validation-manager/src/index.ts b/packages/validation-manager/src/index.ts index 3adbe5fb..3698a76d 100644 --- a/packages/validation-manager/src/index.ts +++ b/packages/validation-manager/src/index.ts @@ -8,7 +8,7 @@ import { } from '@account-abstraction/utils' import { PreVerificationGasCalculator } from '@account-abstraction/sdk' -import { bundlerNativeTracerName, debug_traceCall, eth_traceRip7560Validation } from './GethTracer' +import { bundlerJSTracerName, debug_traceCall, eth_traceRip7560Validation } from './GethTracer' import { bundlerCollectorTracer } from './BundlerCollectorTracer' import { ValidateUserOpResult } from './IValidationManager' import { ValidationManager } from './ValidationManager' @@ -17,7 +17,7 @@ export * from './ValidationManager' export * from './ValidationManagerRIP7560' export * from './IValidationManager' -export async function supportsNativeTracer (provider: JsonRpcProvider, nativeTracer = bundlerNativeTracerName): Promise { +export async function supportsNativeTracer (provider: JsonRpcProvider, nativeTracer = bundlerJSTracerName): Promise { try { await provider.send('debug_traceCall', [{}, 'latest', { tracer: nativeTracer }]) return true