From d70f5603c1209b8ff59ae0582614c66e0d4e7825 Mon Sep 17 00:00:00 2001 From: Garvit Khatri Date: Fri, 14 Feb 2025 17:08:02 +0530 Subject: [PATCH 1/5] Make bundler 100% compliant --- src/cli/config/options.ts | 2 +- src/cli/handler.ts | 2 +- src/executor/executorManager.ts | 6 +- src/executor/utils.ts | 2 +- src/mempool/mempool.ts | 67 ++++--- src/mempool/reputationManager.ts | 165 ++++++++++++++++-- src/rpc/estimation/gasEstimationsV06.ts | 4 +- src/rpc/estimation/gasEstimationsV07.ts | 12 +- src/rpc/rpcHandler.ts | 47 +++-- .../validation/BundlerCollectorTracerV06.ts | 9 +- .../validation/BundlerCollectorTracerV07.ts | 9 +- src/rpc/validation/SafeValidator.ts | 12 +- src/rpc/validation/TracerResultParserV07.ts | 56 ++++-- src/rpc/validation/UnsafeValidator.ts | 8 +- src/types/schemas.ts | 25 ++- src/types/utils.ts | 3 +- src/utils/validation.ts | 2 +- 17 files changed, 333 insertions(+), 98 deletions(-) diff --git a/src/cli/config/options.ts b/src/cli/config/options.ts index 9a4b9c89..9eb36704 100644 --- a/src/cli/config/options.ts +++ b/src/cli/config/options.ts @@ -347,7 +347,7 @@ export const compatibilityOptions: CliCommandOptions = description: "Default API version", type: "string", require: false, - default: "v1" + default: "v2" } } diff --git a/src/cli/handler.ts b/src/cli/handler.ts index 3dd3989c..29f2bcd2 100644 --- a/src/cli/handler.ts +++ b/src/cli/handler.ts @@ -83,7 +83,7 @@ export async function bundlerHandler(args_: IOptionsInput): Promise { const chain: Chain = { id: chainId, - name: 'chain-name', // isn't important, never used + name: "chain-name", // isn't important, never used nativeCurrency: { name: "ETH", symbol: "ETH", diff --git a/src/executor/executorManager.ts b/src/executor/executorManager.ts index 40d6510d..e5d915aa 100644 --- a/src/executor/executorManager.ts +++ b/src/executor/executorManager.ts @@ -167,12 +167,12 @@ export class ExecutorManager { } // Debug endpoint - async sendBundleNow(): Promise { + async sendBundleNow(): Promise { const bundles = await this.mempool.getBundles(1) const bundle = bundles[0] - if (bundle.userOps.length === 0) { - throw new Error("no ops to bundle") + if (bundles.length === 0 || bundle.userOps.length === 0) { + return } const txHash = await this.sendBundleToExecutor(bundle) diff --git a/src/executor/utils.ts b/src/executor/utils.ts index 24d3a40b..116e7401 100644 --- a/src/executor/utils.ts +++ b/src/executor/utils.ts @@ -90,7 +90,7 @@ export const getAuthorizationList = ( ): SignedAuthorizationList | undefined => { const authList = userOpInfos .map(({ userOp }) => userOp) - .map(({ eip7702Auth }) => eip7702Auth) + .map(({ eip7702auth }) => eip7702auth) .filter(Boolean) as SignedAuthorizationList return authList.length ? authList : undefined diff --git a/src/mempool/mempool.ts b/src/mempool/mempool.ts index 48108144..4e022f26 100644 --- a/src/mempool/mempool.ts +++ b/src/mempool/mempool.ts @@ -11,8 +11,8 @@ import { type UserOperation, ValidationErrors, type ValidationResult, - UserOperationBundle, - UserOpInfo + type UserOperationBundle, + type UserOpInfo } from "@alto/types" import type { Metrics } from "@alto/utils" import type { Logger } from "@alto/utils" @@ -274,7 +274,6 @@ export class MemoryMempool { ] } - this.reputationManager.updateUserOperationSeenStatus(userOp, entryPoint) const oldUserOpInfo = [ ...outstandingOps, ...processedOrSubmittedOps @@ -347,15 +346,24 @@ export class MemoryMempool { newOp.maxFeePerGas >= scaleBigIntByPercent(oldOp.maxFeePerGas, 110n) - const hasHigherFees = hasHigherPriorityFee || hasHigherMaxFee + const hasHigherFees = hasHigherPriorityFee && hasHigherMaxFee if (!hasHigherFees) { return [false, reason] } this.store.removeOutstanding(oldUserOpInfo.userOpHash) + this.reputationManager.replaceUserOperationSeenStatus( + oldOp, + entryPoint + ) } + this.reputationManager.increaseUserOperationSeenStatus( + userOp, + entryPoint + ) + // Check if mempool already includes max amount of parallel user operations const parallelUserOperationsCount = this.store .dumpOutstanding() @@ -571,6 +579,11 @@ export class MemoryMempool { "2nd Validation error" ) this.store.removeOutstanding(userOpHash) + this.reputationManager.decreaseUserOperationSeenStatus( + userOp, + entryPoint, + e instanceof RpcError ? e.message : JSON.stringify(e) + ) return { skip: true, paymasterDeposit, @@ -834,10 +847,10 @@ export class MemoryMempool { const [nonceKey, nonceSequence] = getNonceKeyAndSequence(userOp.nonce) - let currentNonceValue: bigint = BigInt(0) + let currentNonceSequence: bigint = BigInt(0) if (_currentNonceValue) { - currentNonceValue = _currentNonceValue + currentNonceSequence = _currentNonceValue } else { const getNonceResult = await entryPointContract.read.getNonce( [userOp.sender, nonceKey], @@ -846,7 +859,7 @@ export class MemoryMempool { } ) - currentNonceValue = getNonceKeyAndSequence(getNonceResult)[1] + currentNonceSequence = getNonceKeyAndSequence(getNonceResult)[1] } const outstanding = this.store @@ -857,25 +870,39 @@ export class MemoryMempool { const [mempoolNonceKey, mempoolNonceSequence] = getNonceKeyAndSequence(mempoolUserOp.nonce) + let isPaymasterSame = false + + if (isVersion07(userOp) && isVersion07(mempoolUserOp)) { + isPaymasterSame = + mempoolUserOp.paymaster === userOp.paymaster && + !( + mempoolUserOp.sender === userOp.sender && + mempoolNonceKey === nonceKey && + mempoolNonceSequence === nonceSequence + ) && + userOp.paymaster !== null + } + return ( - mempoolUserOp.sender === userOp.sender && - mempoolNonceKey === nonceKey && - mempoolNonceSequence >= currentNonceValue && - mempoolNonceSequence < nonceSequence + (mempoolUserOp.sender === userOp.sender && + mempoolNonceKey === nonceKey && + mempoolNonceSequence >= currentNonceSequence && + mempoolNonceSequence < nonceSequence) || + isPaymasterSame ) }) - outstanding.sort((a, b) => { - const aUserOp = a.userOp - const bUserOp = b.userOp + return outstanding + .sort((a, b) => { + const aUserOp = a.userOp + const bUserOp = b.userOp - const [, aNonceValue] = getNonceKeyAndSequence(aUserOp.nonce) - const [, bNonceValue] = getNonceKeyAndSequence(bUserOp.nonce) + const [, aNonceValue] = getNonceKeyAndSequence(aUserOp.nonce) + const [, bNonceValue] = getNonceKeyAndSequence(bUserOp.nonce) - return Number(aNonceValue - bNonceValue) - }) - - return outstanding.map((userOpInfo) => userOpInfo.userOp) + return Number(aNonceValue - bNonceValue) + }) + .map((userOpInfo) => userOpInfo.userOp) } clear(): void { diff --git a/src/mempool/reputationManager.ts b/src/mempool/reputationManager.ts index c0c9235e..761e3bfe 100644 --- a/src/mempool/reputationManager.ts +++ b/src/mempool/reputationManager.ts @@ -21,10 +21,19 @@ export interface InterfaceReputationManager { entryPoint: Address, validationResult: ValidationResult | ValidationResultWithAggregation ): Promise - updateUserOperationSeenStatus( + increaseUserOperationSeenStatus( userOperation: UserOperation, entryPoint: Address - ): void + ): Promise + replaceUserOperationSeenStatus( + userOperation: UserOperation, + entryPoint: Address + ): Promise + decreaseUserOperationSeenStatus( + userOperation: UserOperation, + entryPoint: Address, + error: string + ): Promise increaseUserOperationCount(userOperation: UserOperation): void decreaseUserOperationCount(userOperation: UserOperation): void getStatus(entryPoint: Address, address: Address | null): ReputationStatus @@ -112,11 +121,26 @@ export class NullReputationManager implements InterfaceReputationManager { return } - updateUserOperationSeenStatus( + increaseUserOperationSeenStatus( _: UserOperation, _entryPoint: Address - ): void { - return + ): Promise { + return Promise.resolve() + } + + replaceUserOperationSeenStatus( + _: UserOperation, + _entryPoint: Address + ): Promise { + return Promise.resolve() + } + + decreaseUserOperationSeenStatus( + _: UserOperation, + _entryPoint: Address, + _error: string + ): Promise { + return Promise.resolve() } updateUserOperationIncludedStatus( @@ -350,17 +374,25 @@ export class ReputationManager implements InterfaceReputationManager { entry.opsSeen++ } + decreaseSeen(entryPoint: Address, address: Address): void { + const entry = this.entries[entryPoint][address] + if (!entry) { + return + } + entry.opsSeen-- + } + updateCrashedHandleOps(entryPoint: Address, address: Address): void { const entry = this.entries[entryPoint][address] if (!entry) { this.entries[entryPoint][address] = { address, - opsSeen: 1000n, + opsSeen: 10000n, opsIncluded: 0n } return } - entry.opsSeen = 1000n + entry.opsSeen = 10000n entry.opsIncluded = 0n } @@ -370,6 +402,7 @@ export class ReputationManager implements InterfaceReputationManager { reason: string ): void { const isUserOpV06 = isVersion06(op) + if (reason.startsWith("AA3")) { // paymaster const paymaster = isUserOpV06 @@ -379,9 +412,17 @@ export class ReputationManager implements InterfaceReputationManager { this.updateCrashedHandleOps(entryPoint, paymaster) } } else if (reason.startsWith("AA2")) { - // sender - const sender = op.sender - this.updateCrashedHandleOps(entryPoint, sender) + const factory = isUserOpV06 + ? undefined + : (op.factory as Address | undefined) + + if (factory) { + this.updateCrashedHandleOps(entryPoint, factory) + } else { + // sender + const sender = op.sender + this.updateCrashedHandleOps(entryPoint, sender) + } } else if (reason.startsWith("AA1")) { // init code const factory = isUserOpV06 @@ -438,12 +479,18 @@ export class ReputationManager implements InterfaceReputationManager { } } - updateUserOperationSeenStatus( + async increaseUserOperationSeenStatus( userOperation: UserOperation, entryPoint: Address - ): void { + ): Promise { const sender = userOperation.sender - this.increaseSeen(entryPoint, sender) + + const stakeInfo = await this.getStakeStatus(entryPoint, sender) + + if (stakeInfo.isStaked) { + this.increaseSeen(entryPoint, sender) + } + const isUserOpV06 = isVersion06(userOperation) const paymaster = isUserOpV06 @@ -465,7 +512,7 @@ export class ReputationManager implements InterfaceReputationManager { this.logger.debug( { userOperation, factory }, - "updateUserOperationSeenStatus" + "increaseUserOperationSeenStatus" ) if (factory) { @@ -473,6 +520,96 @@ export class ReputationManager implements InterfaceReputationManager { } } + async replaceUserOperationSeenStatus( + userOperation: UserOperation, + entryPoint: Address + ): Promise { + const sender = userOperation.sender + + const stakeInfo = await this.getStakeStatus(entryPoint, sender) + + if (stakeInfo.isStaked) { + this.decreaseSeen(entryPoint, sender) + } + + const isUserOpV06 = isVersion06(userOperation) + + const paymaster = isUserOpV06 + ? getAddressFromInitCodeOrPaymasterAndData( + userOperation.paymasterAndData + ) + : (userOperation.paymaster as Address | undefined) + if (paymaster) { + this.decreaseSeen(entryPoint, paymaster) + } + + const factory = ( + isUserOpV06 + ? getAddressFromInitCodeOrPaymasterAndData( + userOperation.initCode + ) + : userOperation.factory + ) as Address | undefined + + this.logger.debug( + { userOperation, factory }, + "increaseUserOperationSeenStatus" + ) + + if (factory) { + this.decreaseSeen(entryPoint, factory) + } + } + + async decreaseUserOperationSeenStatus( + userOperation: UserOperation, + entryPoint: Address, + errorReason: string + ): Promise { + const sender = userOperation.sender + + const senderStakeInfo = await this.getStakeStatus(entryPoint, sender) + + if (!senderStakeInfo.isStaked) { + this.decreaseSeen(entryPoint, sender) + } + + const isUserOpV06 = isVersion06(userOperation) + + const paymaster = isUserOpV06 + ? getAddressFromInitCodeOrPaymasterAndData( + userOperation.paymasterAndData + ) + : (userOperation.paymaster as Address | undefined) + + // can decrease if senderStakeInfo.isStaked is true + // or when error does not include aa3 + if ( + paymaster && + (senderStakeInfo.isStaked || + !errorReason.toLowerCase().includes("aa3")) + ) { + this.decreaseSeen(entryPoint, paymaster) + } + + const factory = ( + isUserOpV06 + ? getAddressFromInitCodeOrPaymasterAndData( + userOperation.initCode + ) + : userOperation.factory + ) as Address | undefined + + this.logger.debug( + { userOperation, factory }, + "decreaseUserOperationSeenStatus" + ) + + if (factory) { + this.decreaseSeen(entryPoint, factory) + } + } + increaseUserOperationCount(userOperation: UserOperation) { const sender = userOperation.sender this.entityCount[sender] = (this.entityCount[sender] ?? 0n) + 1n diff --git a/src/rpc/estimation/gasEstimationsV06.ts b/src/rpc/estimation/gasEstimationsV06.ts index b9e7d598..2ce6450c 100644 --- a/src/rpc/estimation/gasEstimationsV06.ts +++ b/src/rpc/estimation/gasEstimationsV06.ts @@ -147,10 +147,10 @@ export class GasEstimatorV06 { stateOverrides = undefined } - if (userOperation.eip7702Auth) { + if (userOperation.eip7702auth) { stateOverrides = await addAuthorizationStateOverrides({ stateOverrides, - authorizationList: [userOperation.eip7702Auth], + authorizationList: [userOperation.eip7702auth], publicClient }) } diff --git a/src/rpc/estimation/gasEstimationsV07.ts b/src/rpc/estimation/gasEstimationsV07.ts index c8ae4734..5ebb7d8b 100644 --- a/src/rpc/estimation/gasEstimationsV07.ts +++ b/src/rpc/estimation/gasEstimationsV07.ts @@ -66,8 +66,8 @@ export class GasEstimatorV07 { }) let authorizationList: SignedAuthorizationList = [] - if (userOperation.eip7702Auth) { - authorizationList = [userOperation.eip7702Auth] + if (userOperation.eip7702auth) { + authorizationList = [userOperation.eip7702auth] } const errorResult = await this.callPimlicoEntryPointSimulations({ @@ -241,8 +241,8 @@ export class GasEstimatorV07 { }) let authorizationList: SignedAuthorizationList = [] - if (targetOp.eip7702Auth) { - authorizationList = [targetOp.eip7702Auth] + if (targetOp.eip7702auth) { + authorizationList = [targetOp.eip7702auth] } let cause = await this.callPimlicoEntryPointSimulations({ @@ -350,8 +350,8 @@ export class GasEstimatorV07 { }) let authorizationList: SignedAuthorizationList = [] - if (userOperation.eip7702Auth) { - authorizationList = [userOperation.eip7702Auth] + if (userOperation.eip7702auth) { + authorizationList = [userOperation.eip7702auth] } let cause: readonly [Hex, Hex, Hex | null, Hex] diff --git a/src/rpc/rpcHandler.ts b/src/rpc/rpcHandler.ts index f0add14c..ba1099fe 100644 --- a/src/rpc/rpcHandler.ts +++ b/src/rpc/rpcHandler.ts @@ -7,6 +7,7 @@ import type { } from "@alto/mempool" import type { ApiVersion, + BundlerClearReputationResponseResult, PackedUserOperation, StateOverrides, UserOpInfo, @@ -239,6 +240,13 @@ export class RpcHandler implements IRpcEndpoint { ...request.params ) } + case "debug_bundler_clearReputation": + return { + method, + result: this.debug_bundler_clearReputation( + ...request.params + ) + } case "debug_bundler_setReputation": return { method, @@ -333,7 +341,7 @@ export class RpcHandler implements IRpcEndpoint { throw new RpcError(reason) } - if (apiVersion !== "v1") { + if (apiVersion !== "v1" && !this.config.safeMode) { await this.gasPriceManager.validateGasPrice({ maxFeePerGas: userOperation.maxFeePerGas, maxPriorityFeePerGas: userOperation.maxPriorityFeePerGas @@ -541,7 +549,9 @@ export class RpcHandler implements IRpcEndpoint { } const result: GetUserOperationByHashResponseResult = { - userOperation: op, + userOperation: Object.fromEntries( + Object.entries(op).filter(([_, v]) => v !== null) + ) as UserOperation, entryPoint: getAddress(tx.to), transactionHash: txHash, blockHash: tx.blockHash ?? "0x", @@ -573,19 +583,19 @@ export class RpcHandler implements IRpcEndpoint { return "ok" } - async debug_bundler_dumpMempool( + debug_bundler_dumpMempool( entryPoint: Address ): Promise { this.ensureDebugEndpointsAreEnabled("debug_bundler_dumpMempool") this.ensureEntryPointIsSupported(entryPoint) - return this.mempool.dumpOutstanding() + return Promise.resolve(this.mempool.dumpOutstanding()) } async debug_bundler_sendBundleNow(): Promise { this.ensureDebugEndpointsAreEnabled("debug_bundler_sendBundleNow") - const transaction = await this.executorManager.sendBundleNow() - return transaction + await this.executorManager.sendBundleNow() + return "ok" } async debug_bundler_setBundlingMode( @@ -631,6 +641,13 @@ export class RpcHandler implements IRpcEndpoint { return "ok" } + debug_bundler_clearReputation(): BundlerClearReputationResponseResult { + this.ensureDebugEndpointsAreEnabled("debug_bundler_clearReputation") + + this.reputationManager.clear() + return "ok" + } + pimlico_getUserOperationStatus( userOperationHash: HexData32 ): PimlicoGetUserOperationStatusResponseResult { @@ -706,17 +723,12 @@ export class RpcHandler implements IRpcEndpoint { throw new RpcError(reason, ValidationErrors.InvalidFields) } - let queuedUserOperations: UserOperation[] = [] - if ( - userOperationNonceValue > currentNonceValue && - isVersion07(userOperation) - ) { - queuedUserOperations = await this.mempool.getQueuedUserOperations( + const queuedUserOperations: UserOperation[] = + await this.mempool.getQueuedUserOperations( userOperation, entryPoint, currentNonceValue ) - } if ( userOperationNonceValue > @@ -748,6 +760,7 @@ export class RpcHandler implements IRpcEndpoint { entryPoint }) } + await this.mempool.checkEntityMultipleRoleViolation(userOperation) // V1 api doesn't check prefund. const shouldCheckPrefund = @@ -765,8 +778,6 @@ export class RpcHandler implements IRpcEndpoint { validationResult ) - await this.mempool.checkEntityMultipleRoleViolation(userOperation) - const [success, errorReason] = this.mempool.add( userOperation, entryPoint, @@ -896,16 +907,16 @@ export class RpcHandler implements IRpcEndpoint { } async validateEip7702Auth(userOperation: UserOperation) { - if (!userOperation.eip7702Auth) { + if (!userOperation.eip7702auth) { throw new RpcError( - "UserOperation is missing eip7702Auth", + "UserOperation is missing eip7702auth", ValidationErrors.InvalidFields ) } // Check that auth is valid. const sender = await recoverAuthorizationAddress({ - authorization: userOperation.eip7702Auth + authorization: userOperation.eip7702auth }) if (sender !== userOperation.sender) { throw new RpcError( diff --git a/src/rpc/validation/BundlerCollectorTracerV06.ts b/src/rpc/validation/BundlerCollectorTracerV06.ts index fcac44b0..0a8ae6ab 100644 --- a/src/rpc/validation/BundlerCollectorTracerV06.ts +++ b/src/rpc/validation/BundlerCollectorTracerV06.ts @@ -354,7 +354,12 @@ export function bundlerCollectorTracer(): BundlerCollectorTracer { } this.lastOp = opcode - if (opcode === "SLOAD" || opcode === "SSTORE") { + if ( + opcode === "SLOAD" || + opcode === "SSTORE" || + opcode === "TLOAD" || + opcode === "TSTORE" + ) { const slot = toWord(log.stack.peek(0).toString(16)) const slotHex = toHex(slot) const addr = log.contract.getAddress() @@ -367,7 +372,7 @@ export function bundlerCollectorTracer(): BundlerCollectorTracer { } this.currentLevel.access[addrHex] = access } - if (opcode === "SLOAD") { + if (opcode === "SLOAD" || opcode === "TLOAD") { // read slot values before this UserOp was created // (so saving it if it was written before the first read) if ( diff --git a/src/rpc/validation/BundlerCollectorTracerV07.ts b/src/rpc/validation/BundlerCollectorTracerV07.ts index 5a1ca653..7b8a1324 100644 --- a/src/rpc/validation/BundlerCollectorTracerV07.ts +++ b/src/rpc/validation/BundlerCollectorTracerV07.ts @@ -354,7 +354,12 @@ export function bundlerCollectorTracer(): BundlerCollectorTracer { } this.lastOp = opcode - if (opcode === "SLOAD" || opcode === "SSTORE") { + if ( + opcode === "SLOAD" || + opcode === "SSTORE" || + opcode === "TLOAD" || + opcode === "TSTORE" + ) { const slot = toWord(log.stack.peek(0).toString(16)) const slotHex = toHex(slot) const addr = log.contract.getAddress() @@ -367,7 +372,7 @@ export function bundlerCollectorTracer(): BundlerCollectorTracer { } this.currentLevel.access[addrHex] = access } - if (opcode === "SLOAD") { + if (opcode === "SLOAD" || opcode === "TLOAD") { // read slot values before this UserOp was created // (so saving it if it was written before the first read) if ( diff --git a/src/rpc/validation/SafeValidator.ts b/src/rpc/validation/SafeValidator.ts index 0a6c9aa2..3d7048fe 100644 --- a/src/rpc/validation/SafeValidator.ts +++ b/src/rpc/validation/SafeValidator.ts @@ -189,6 +189,7 @@ export class SafeValidator async getValidationResultV07({ userOperation, + queuedUserOperations, entryPoint, preCodeHashes }: { @@ -214,6 +215,7 @@ export class SafeValidator const [res, tracerResult] = await this.getValidationResultWithTracerV07( userOperation, + queuedUserOperations, entryPoint ) @@ -485,14 +487,18 @@ export class SafeValidator async getValidationResultWithTracerV07( userOperation: UserOperationV07, + queuedUserOperations: UserOperationV07[], entryPoint: Address ): Promise<[ValidationResultV07, BundlerTracerResult]> { const packedUserOperation = toPackedUserOperation(userOperation) + const packedQueuedUserOperations = queuedUserOperations.map((uop) => + toPackedUserOperation(uop) + ) const entryPointSimulationsCallData = encodeFunctionData({ abi: EntryPointV07SimulationsAbi, functionName: "simulateValidationLast", - args: [[packedUserOperation]] + args: [[...packedQueuedUserOperations, packedUserOperation]] }) const callData = encodeFunctionData({ @@ -538,6 +544,10 @@ export class SafeValidator errorCode = ValidationErrors.InvalidSignature } + if (errorMessage.includes("AA31")) { + errorCode = ValidationErrors.PaymasterDepositTooLow + } + throw new RpcError(errorMessage, errorCode) } diff --git a/src/rpc/validation/TracerResultParserV07.ts b/src/rpc/validation/TracerResultParserV07.ts index 21cb5669..3ef5afe8 100644 --- a/src/rpc/validation/TracerResultParserV07.ts +++ b/src/rpc/validation/TracerResultParserV07.ts @@ -437,18 +437,15 @@ export function tracerResultParserV07( "BASEFEE", "BLOCKHASH", "NUMBER", - "SELFBALANCE", - "BALANCE", "ORIGIN", "GAS", - "CREATE", "COINBASE", "SELFDESTRUCT", "RANDOM", "PREVRANDAO", "INVALID", - "TSTORE", - "TLOAD" + "SELFBALANCE", + "BALANCE" ]) // eslint-disable-next-line @typescript-eslint/no-base-to-string @@ -506,6 +503,8 @@ export function tracerResultParserV07( tracerResults.keccak as Hex[] ) + let isCreateOpcodeUsed = false + for (const [title, entStakes] of Object.entries(stakeInfoEntities)) { const entityTitle = title as keyof StakeInfoEntities const entityAddr = (entStakes?.addr ?? "").toLowerCase() @@ -523,6 +522,7 @@ export function tracerResultParserV07( continue } const opcodes = currentNumLevel.opcodes + const access = currentNumLevel.access // address => { reads, writes } // [OP-020] @@ -535,32 +535,47 @@ export function tracerResultParserV07( // opcodes from [OP-011] for (const opcode of Object.keys(opcodes)) { - if ( - bannedOpCodes.has(opcode) && - !( - (opcode === "BALANCE" || opcode === "SELFBALANCE") && - isStaked(entStakes) - ) - ) { + if (bannedOpCodes.has(opcode) && !isStaked(entStakes)) { throw new RpcError( `${entityTitle} uses banned opcode: ${opcode}`, ValidationErrors.OpcodeValidation ) } } + + const createOpcode = (opcodes.CREATE2 ?? 0) + (opcodes.CREATE ?? 0) + + if (isCreateOpcodeUsed && createOpcode > 1) { + throw new RpcError( + `${entityTitle} uses banned opcode: CREATE2`, + ValidationErrors.OpcodeValidation + ) + } + // [OP-031] if (entityTitle === "factory") { - if ((opcodes.CREATE2 ?? 0) > 1) { + if ((createOpcode ?? 0) > 1) { throw new RpcError( `${entityTitle} with too many CREATE2`, ValidationErrors.OpcodeValidation ) } - } else if (opcodes.CREATE2) { - throw new RpcError( - `${entityTitle} uses banned opcode: CREATE2`, - ValidationErrors.OpcodeValidation - ) + } else if (createOpcode) { + const isFactoryStaked = + stakeInfoEntities.factory && isStaked(stakeInfoEntities.factory) + + const skip = stakeInfoEntities.factory && opcodes.CREATE + + if (!(isFactoryStaked || skip)) { + throw new RpcError( + `${entityTitle} uses banned opcode: CREATE2`, + ValidationErrors.OpcodeValidation + ) + } + } + + if (createOpcode > 0) { + isCreateOpcodeUsed = true } for (const [addr, { reads, writes }] of Object.entries(access)) { @@ -595,9 +610,12 @@ export function tracerResultParserV07( if (userOperation.factory) { // special case: account.validateUserOp is allowed to use assoc storage if factory is staked. // [STO-022], [STO-021] + if ( !( - entityAddr === sender && + (entityAddr === sender || + entityAddr === + userOperation.paymaster?.toLowerCase()) && isStaked(stakeInfoEntities.factory) ) ) { diff --git a/src/rpc/validation/UnsafeValidator.ts b/src/rpc/validation/UnsafeValidator.ts index 0f8f14e0..244992d0 100644 --- a/src/rpc/validation/UnsafeValidator.ts +++ b/src/rpc/validation/UnsafeValidator.ts @@ -176,9 +176,15 @@ export class UnsafeValidator implements InterfaceValidator { }) if (error.result === "failed") { + let errorCode: number = ExecutionErrors.UserOperationReverted + + if (error.data.toString().includes("AA23")) { + errorCode = ValidationErrors.SimulateValidation + } + throw new RpcError( `UserOperation reverted during simulation with reason: ${error.data}`, - ExecutionErrors.UserOperationReverted + errorCode ) } diff --git a/src/types/schemas.ts b/src/types/schemas.ts index 75f88b4a..875008fe 100644 --- a/src/types/schemas.ts +++ b/src/types/schemas.ts @@ -57,7 +57,7 @@ const userOperationV06Schema = z maxFeePerGas: hexNumberSchema, paymasterAndData: hexDataSchema, signature: hexDataSchema, - eip7702Auth: signedAuthorizationSchema.optional() + eip7702auth: signedAuthorizationSchema.optional().nullable() }) .strict() .transform((val) => { @@ -99,7 +99,7 @@ const userOperationV07Schema = z .optional() .transform((val) => val ?? null), signature: hexDataSchema, - eip7702Auth: signedAuthorizationSchema.optional() + eip7702auth: signedAuthorizationSchema.optional().nullable() }) .strict() .transform((val) => val) @@ -117,7 +117,7 @@ const partialUserOperationV06Schema = z maxFeePerGas: hexNumberSchema.default(1n), paymasterAndData: hexDataSchema, signature: hexDataSchema, - eip7702Auth: signedAuthorizationSchema.optional() + eip7702auth: signedAuthorizationSchema.optional() }) .strict() .transform((val) => { @@ -159,7 +159,7 @@ const partialUserOperationV07Schema = z .optional() .transform((val) => val ?? null), signature: hexDataSchema, - eip7702Auth: signedAuthorizationSchema.optional() + eip7702auth: signedAuthorizationSchema.optional().nullable() }) .strict() .transform((val) => val) @@ -324,6 +324,11 @@ const bundlerDumpReputationsRequestSchema = z.object({ params: z.tuple([addressSchema]) }) +const bundlerClearReputationRequestSchema = z.object({ + method: z.literal("debug_bundler_clearReputation"), + params: z.tuple([]) +}) + const pimlicoGetStakeStatusRequestSchema = z.object({ method: z.literal("debug_bundler_getStakeStatus"), params: z.tuple([addressSchema, addressSchema]) @@ -378,6 +383,7 @@ const bundlerRequestSchema = z.discriminatedUnion("method", [ bundlerSetBundlingModeRequestSchema, bundlerSetReputationsRequestSchema, bundlerDumpReputationsRequestSchema, + bundlerClearReputationRequestSchema, pimlicoGetStakeStatusRequestSchema, pimlicoGetUserOperationStatusRequestSchema, pimlicoGetUserOperationGasPriceRequestSchema, @@ -521,7 +527,7 @@ const bundlerGetStakeStatusResponseSchema = z.object({ const bundlerSendBundleNowResponseSchema = z.object({ method: z.literal("debug_bundler_sendBundleNow"), - result: hexData32Schema + result: z.literal("ok") }) const bundlerSetBundlingModeResponseSchema = z.object({ @@ -547,6 +553,11 @@ const bundlerDumpReputationsResponseSchema = z.object({ ) }) +const bundlerClearReputationResponseSchema = z.object({ + method: z.literal("debug_bundler_clearReputation"), + result: z.literal("ok") +}) + const userOperationStatus = z.object({ status: z.enum([ "not_found", @@ -631,6 +642,7 @@ const bundlerResponseSchema = z.discriminatedUnion("method", [ bundlerSetBundlingModeResponseSchema, bundlerSetReputationsResponseSchema, bundlerDumpReputationsResponseSchema, + bundlerClearReputationResponseSchema, pimlicoGetUserOperationStatusResponseSchema, pimlicoGetUserOperationGasPriceResponseSchema, pimlicoSendUserOperationNowResponseSchema, @@ -729,6 +741,9 @@ export type BundlerSendBundleNowResponseResult = z.infer< export type BundlerSetBundlingModeResponseResult = z.infer< typeof bundlerSetBundlingModeResponseSchema >["result"] +export type BundlerClearReputationResponseResult = z.infer< + typeof bundlerClearReputationResponseSchema +>["result"] export type BundlerSetReputationsResponseResult = z.infer< typeof bundlerSetReputationsResponseSchema >["result"] diff --git a/src/types/utils.ts b/src/types/utils.ts index 91f479cd..17405225 100644 --- a/src/types/utils.ts +++ b/src/types/utils.ts @@ -8,7 +8,8 @@ export enum ValidationErrors { Reputation = -32504, InsufficientStake = -32505, UnsupportedSignatureAggregator = -32506, - InvalidSignature = -32507 + InvalidSignature = -32507, + PaymasterDepositTooLow = -32508 } export enum ExecutionErrors { diff --git a/src/utils/validation.ts b/src/utils/validation.ts index 8a830a92..bbd2e41e 100644 --- a/src/utils/validation.ts +++ b/src/utils/validation.ts @@ -427,7 +427,7 @@ export function calcDefaultPreVerificationGas( .map((x) => (x === 0 ? ov.zeroByte : ov.nonZeroByte)) .reduce((sum, x) => sum + x) - const authorizationCost = userOperation.eip7702Auth + const authorizationCost = userOperation.eip7702auth ? 37500 // overhead for PER_EMPTY_ACCOUNT_COST + PER_AUTH_BASE_COST : 0 From 53fe7c3b150da5b38ad874d9216edac841068e12 Mon Sep 17 00:00:00 2001 From: Garvit Khatri Date: Fri, 14 Feb 2025 17:12:37 +0530 Subject: [PATCH 2/5] tests depend on v1 default --- src/cli/config/options.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/config/options.ts b/src/cli/config/options.ts index 9eb36704..9a4b9c89 100644 --- a/src/cli/config/options.ts +++ b/src/cli/config/options.ts @@ -347,7 +347,7 @@ export const compatibilityOptions: CliCommandOptions = description: "Default API version", type: "string", require: false, - default: "v2" + default: "v1" } } From 0f85e4a22a95196dfc8a1d008bd8dc2510b678e7 Mon Sep 17 00:00:00 2001 From: Garvit Khatri Date: Fri, 14 Feb 2025 19:07:39 +0530 Subject: [PATCH 3/5] Fix --- src/rpc/validation/TracerResultParserV07.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/rpc/validation/TracerResultParserV07.ts b/src/rpc/validation/TracerResultParserV07.ts index 3ef5afe8..bd327fb7 100644 --- a/src/rpc/validation/TracerResultParserV07.ts +++ b/src/rpc/validation/TracerResultParserV07.ts @@ -489,6 +489,7 @@ export function tracerResultParserV07( ) } + const paymaster = userOperation.paymaster?.toLowerCase() const sender = userOperation.sender.toLowerCase() // stake info per "number" level (factory, sender, paymaster) // we only use stake info if we notice a memory reference that require stake @@ -614,8 +615,7 @@ export function tracerResultParserV07( if ( !( (entityAddr === sender || - entityAddr === - userOperation.paymaster?.toLowerCase()) && + entityAddr === paymaster) && isStaked(stakeInfoEntities.factory) ) ) { From ec359ea69f81dc37afda1712020295cde9850b42 Mon Sep 17 00:00:00 2001 From: Garvit Khatri Date: Fri, 14 Feb 2025 19:35:49 +0530 Subject: [PATCH 4/5] use areAddressesEqual --- src/rpc/validation/TracerResultParserV07.ts | 19 ++++++++++++------- src/utils/helpers.ts | 14 +++++++++----- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/rpc/validation/TracerResultParserV07.ts b/src/rpc/validation/TracerResultParserV07.ts index bd327fb7..7117a1eb 100644 --- a/src/rpc/validation/TracerResultParserV07.ts +++ b/src/rpc/validation/TracerResultParserV07.ts @@ -24,6 +24,7 @@ import { pad } from "viem" import type { BundlerTracerResult } from "./BundlerCollectorTracerV07" +import { areAddressesEqual } from "@alto/utils" interface CallEntry { to: string @@ -489,7 +490,6 @@ export function tracerResultParserV07( ) } - const paymaster = userOperation.paymaster?.toLowerCase() const sender = userOperation.sender.toLowerCase() // stake info per "number" level (factory, sender, paymaster) // we only use stake info if we notice a memory reference that require stake @@ -581,7 +581,7 @@ export function tracerResultParserV07( for (const [addr, { reads, writes }] of Object.entries(access)) { // testing read/write access on contract "addr" - if (addr === sender) { + if (areAddressesEqual(addr, userOperation.sender)) { // allowed to access sender's storage // [STO-010] continue @@ -614,8 +614,14 @@ export function tracerResultParserV07( if ( !( - (entityAddr === sender || - entityAddr === paymaster) && + (areAddressesEqual( + entityAddr, + userOperation.sender + ) || + areAddressesEqual( + entityAddr, + userOperation.paymaster ?? "" + )) && isStaked(stakeInfoEntities.factory) ) ) { @@ -658,9 +664,8 @@ export function tracerResultParserV07( // 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() + Object.entries(stakeInfoEntities).find(([_title, info]) => + areAddressesEqual(info?.addr ?? "", addr) ) ?? [] return title ?? addr diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index e31076a2..7a7fafad 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -1,19 +1,23 @@ -import { StateOverrides } from "@alto/types" +import type { StateOverrides } from "@alto/types" import { type Address, BaseError, type RawContractError, getAddress, - PublicClient + type PublicClient } from "viem" import { - SignedAuthorizationList, + type SignedAuthorizationList, recoverAuthorizationAddress } from "viem/experimental" /// Ensure proper equality by converting both addresses into their checksum type -export const areAddressesEqual = (a: Address, b: Address) => { - return getAddress(a) === getAddress(b) +export const areAddressesEqual = (a: string, b: string) => { + try { + return getAddress(a) === getAddress(b) + } catch { + return false + } } export function getRevertErrorData(err: unknown) { From 75acc797a1ff6ef1f821b539b072b63b5e23d8e8 Mon Sep 17 00:00:00 2001 From: Garvit Khatri Date: Fri, 14 Feb 2025 19:45:00 +0530 Subject: [PATCH 5/5] Fix build --- src/utils/helpers.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index 7a7fafad..b7a42b42 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -1,6 +1,5 @@ import type { StateOverrides } from "@alto/types" import { - type Address, BaseError, type RawContractError, getAddress,