Skip to content

Commit

Permalink
Merge branch 'master' into single-auth-tuple
Browse files Browse the repository at this point in the history
  • Loading branch information
drortirosh committed Jan 2, 2025
2 parents 0ec75d0 + aebfc64 commit 426ed01
Show file tree
Hide file tree
Showing 18 changed files with 532 additions and 345 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
20 changes: 20 additions & 0 deletions packages/bundler/localconfig/bundler.eip7702.config.json
Original file line number Diff line number Diff line change
@@ -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
}
1 change: 1 addition & 0 deletions packages/bundler/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions packages/bundler/src/MethodHandlerERC4337.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
requireCond,
simulationRpcParams,
tostr,
unpackUserOp
unpackUserOp, getAuthorizationList
} from '@account-abstraction/utils'
import { BundlerConfig } from './BundlerConfig'

Expand Down Expand Up @@ -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 => {
Expand Down
117 changes: 94 additions & 23 deletions packages/bundler/src/modules/BundleManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -248,11 +275,15 @@ export class BundleManager implements IBundleManager {
async _findEntityToBlame (reasonStr: string, userOp: UserOperation): Promise<string | undefined> {
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
}
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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<void> {
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)
}
Expand Down
14 changes: 9 additions & 5 deletions packages/bundler/src/modules/MempoolManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 (
Expand Down
2 changes: 1 addition & 1 deletion packages/bundler/src/modules/ReputationManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
5 changes: 0 additions & 5 deletions packages/bundler/src/runBundler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
2 changes: 1 addition & 1 deletion packages/utils/src/ERC4337Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
2 changes: 1 addition & 1 deletion packages/utils/src/RIP7712NonceManagerUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const entryPointSalt = '0x90d8084deab30c2a37c45e8d47f49f2f7965183cb6990a9

export async function deployNonceManager (provider: JsonRpcProvider, signer = provider.getSigner()): Promise<NonceManager> {
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)
}

Expand Down
2 changes: 1 addition & 1 deletion packages/utils/src/Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]) => {
Expand Down
6 changes: 3 additions & 3 deletions packages/utils/src/interfaces/EIP7702Authorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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))
Expand Down
2 changes: 2 additions & 0 deletions packages/utils/src/interfaces/OperationBase.ts
Original file line number Diff line number Diff line change
@@ -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.
*/
Expand Down
1 change: 1 addition & 0 deletions packages/validation-manager/src/BundlerCollectorTracer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export interface TopLevelCallInfo {
contractSize: { [addr: string]: ContractSizeInfo }
extCodeAccessInfo: { [addr: string]: string }
oog?: boolean
calls?: []
}

/**
Expand Down
Loading

0 comments on commit 426ed01

Please sign in to comment.