diff --git a/src/mempool/mempool.ts b/src/mempool/mempool.ts index 5f788805..66329a2c 100644 --- a/src/mempool/mempool.ts +++ b/src/mempool/mempool.ts @@ -355,7 +355,7 @@ export class Mempool { }) if (conflicting) { - const { userOpInfo, reason } = conflicting + const { userOpInfo, conflictReason } = conflicting const conflictingUserOp = userOpInfo.userOp const hasHigherPriorityFee = @@ -372,10 +372,17 @@ export class Mempool { const hasHigherFees = hasHigherPriorityFee && hasHigherMaxFee if (!hasHigherFees) { - const message = - reason === "conflicting_deployment" - ? "AA10 sender already constructed: A conflicting userOperation with initCode for this sender is already in the mempool" - : "AA25 invalid account nonce: User operation already present in mempool" + let message: string + if (conflictReason === "conflicting_deployment") { + message = + "AA10 sender already constructed: A conflicting userOperation with initCode for this sender is already in the mempool" + } else if (conflictReason === "conflicting_7702_auth") { + message = + "Sender already has an inflight EIP-7702 authorization" + } else { + message = + "AA25 invalid account nonce: User operation already present in mempool" + } // Re-add to outstanding as it wasn't replaced await this.store.addOutstanding({ diff --git a/src/store/outstanding/memory.ts b/src/store/outstanding/memory.ts index 88ca5277..21e6d792 100644 --- a/src/store/outstanding/memory.ts +++ b/src/store/outstanding/memory.ts @@ -87,41 +87,56 @@ export class MemoryOutstanding implements OutstandingStore { ): Promise { const outstandingOps = this.dump() - let conflictingReason: ConflictingOutstandingType + for (const existingUserOpInfo of outstandingOps) { + const { userOp: existingUserOp, userOpHash: existingUserOpHash } = + existingUserOpInfo - for (const userOpInfo of outstandingOps) { - const { userOp: mempoolUserOp } = userOpInfo + const isSameSender = existingUserOp.sender === userOp.sender - const isSameSender = mempoolUserOp.sender === userOp.sender - if (isSameSender && mempoolUserOp.nonce === userOp.nonce) { - const removed = await this.remove([userOpInfo.userOpHash]) + // Check each conflict type. + const isConflictingNonce = + isSameSender && existingUserOp.nonce === userOp.nonce + + const isConflictingEip7702Auth = + isSameSender && userOp.eip7702Auth && existingUserOp.eip7702Auth + + const isConflictingDeployment = + isSameSender && + isDeployment(userOp) && + isDeployment(existingUserOp) + + if (isConflictingNonce) { + const removed = await this.remove([existingUserOpHash]) if (removed.length > 0) { - conflictingReason = { - reason: "conflicting_nonce", + return { + conflictReason: "conflicting_nonce", userOpInfo: removed[0] } } - break } - const isConflictingDeployment = - isSameSender && - isDeployment(userOp) && - isDeployment(mempoolUserOp) + if (isConflictingEip7702Auth) { + const removed = await this.remove([existingUserOpHash]) + if (removed.length > 0) { + return { + conflictReason: "conflicting_7702_auth", + userOpInfo: removed[0] + } + } + } if (isConflictingDeployment) { - const removed = await this.remove([userOpInfo.userOpHash]) + const removed = await this.remove([existingUserOpHash]) if (removed.length > 0) { - conflictingReason = { - reason: "conflicting_deployment", + return { + conflictReason: "conflicting_deployment", userOpInfo: removed[0] } } - break } } - return conflictingReason + return undefined } async contains(userOpHash: HexData32): Promise { diff --git a/src/store/outstanding/redis.ts b/src/store/outstanding/redis.ts index 428e41b3..24a72bbe 100644 --- a/src/store/outstanding/redis.ts +++ b/src/store/outstanding/redis.ts @@ -30,6 +30,7 @@ class RedisOutstandingQueue implements OutstandingStore { private readonly readyQueue: string // Queue of userOpHashes (sorted by composite of userOp.nonceSeq + userOp.maxFeePerGas) private readonly userOpHashMap: string // userOpHash -> boolean private readonly deploymentHashMap: string // sender -> deployment userOpHash + private readonly eip7702AuthHashMap: string // sender -> eip7702Auth userOpHash private readonly senderNonceQueueTtl: number // TTL for sender nonce queues in seconds constructor({ @@ -51,6 +52,7 @@ class RedisOutstandingQueue implements OutstandingStore { this.senderNonceKeyPrefix = `${redisPrefix}:sender` this.userOpHashMap = `${redisPrefix}:userop-hash` this.deploymentHashMap = `${redisPrefix}:deployment-senders` + this.eip7702AuthHashMap = `${redisPrefix}:eip7702-auth-senders` // Calculate TTL for sender nonce queues (10 blocks worth of time) this.senderNonceQueueTtl = config.blockTime * 10 @@ -93,7 +95,7 @@ class RedisOutstandingQueue implements OutstandingStore { if (removedUserOps.length > 0) { return { - reason: "conflicting_nonce" as const, + conflictReason: "conflicting_nonce" as const, userOpInfo: removedUserOps[0] } } @@ -113,7 +115,28 @@ class RedisOutstandingQueue implements OutstandingStore { if (removedUserOps.length > 0) { return { - reason: "conflicting_deployment" as const, + conflictReason: "conflicting_deployment" as const, + userOpInfo: removedUserOps[0] + } + } + } + } + + // Check for conflicting EIP-7702 auth. + if (userOp.eip7702Auth) { + const existingEip7702AuthHash = (await this.redis.hget( + this.eip7702AuthHashMap, + sender + )) as HexData32 | null + + if (existingEip7702AuthHash) { + const removedUserOps = await this.remove([ + existingEip7702AuthHash + ]) + + if (removedUserOps.length > 0) { + return { + conflictReason: "conflicting_7702_auth" as const, userOpInfo: removedUserOps[0] } } @@ -156,6 +179,15 @@ class RedisOutstandingQueue implements OutstandingStore { if (isDeployment(userOp)) { pipeline.hset(this.deploymentHashMap, userOp.sender, userOpHash) } + + // Add sender to eip7702Auth hash if this has eip7702Auth. + if (userOp.eip7702Auth) { + pipeline.hset( + this.eip7702AuthHashMap, + userOp.sender, + userOpHash + ) + } } await pipeline.exec() @@ -205,6 +237,11 @@ class RedisOutstandingQueue implements OutstandingStore { removalPipeline.hdel(this.deploymentHashMap, userOp.sender) } + // Remove sender from eip7702Auth hash if this had eip7702Auth. + if (userOp.eip7702Auth) { + removalPipeline.hdel(this.eip7702AuthHashMap, userOp.sender) + } + removedUserOps.push(userOpInfo) } diff --git a/src/store/outstanding/types.ts b/src/store/outstanding/types.ts index 3db86d0a..b0b4ec86 100644 --- a/src/store/outstanding/types.ts +++ b/src/store/outstanding/types.ts @@ -2,7 +2,10 @@ import type { HexData32, UserOpInfo, UserOperation } from "@alto/types" export type ConflictingOutstandingType = | { - reason: "conflicting_nonce" | "conflicting_deployment" + conflictReason: + | "conflicting_nonce" + | "conflicting_deployment" + | "conflicting_7702_auth" userOpInfo: UserOpInfo } | undefined