Skip to content

Commit

Permalink
WIP: Create a RIP-7560 bundling mode
Browse files Browse the repository at this point in the history
  • Loading branch information
forshtat committed Feb 11, 2024
1 parent 84b7ab9 commit 1917021
Show file tree
Hide file tree
Showing 20 changed files with 558 additions and 183 deletions.
10 changes: 8 additions & 2 deletions packages/bundler/src/BundlerConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import ow from 'ow'

const MIN_UNSTAKE_DELAY = 86400
const MIN_STAKE_VALUE = 1e18.toString()

export interface BundlerConfig {
beneficiary: string
entryPoint: string
Expand All @@ -22,6 +23,8 @@ export interface BundlerConfig {
minUnstakeDelay: number
autoBundleInterval: number
autoBundleMempoolSize: number

useRip7650Mode: boolean
}

// TODO: implement merging config (args -> config.js -> default) and runtime shape validation
Expand All @@ -43,7 +46,9 @@ export const BundlerConfigShape = {
minStake: ow.string,
minUnstakeDelay: ow.number,
autoBundleInterval: ow.number,
autoBundleMempoolSize: ow.number
autoBundleMempoolSize: ow.number,

useRip7650Mode: ow.boolean
}

// TODO: consider if we want any default fields at all
Expand All @@ -54,5 +59,6 @@ export const bundlerConfigDefault: Partial<BundlerConfig> = {
unsafe: false,
conditionalRpc: false,
minStake: MIN_STAKE_VALUE,
minUnstakeDelay: MIN_UNSTAKE_DELAY
minUnstakeDelay: MIN_UNSTAKE_DELAY,
useRip7650Mode: false
}
31 changes: 29 additions & 2 deletions packages/bundler/src/BundlerServer.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import bodyParser from 'body-parser'
import cors from 'cors'
import express, { Express, Response, Request } from 'express'
import { Provider } from '@ethersproject/providers'
import { JsonRpcProvider, Provider } from '@ethersproject/providers'
import { Signer, utils } from 'ethers'
import { parseEther } from 'ethers/lib/utils'

Expand All @@ -21,15 +21,18 @@ import { EntryPoint__factory } from '@account-abstraction/contracts'
import { DebugMethodHandler } from './DebugMethodHandler'

import Debug from 'debug'
import { RIP7560MethodHandler } from './RIP7560MethodHandler'

const debug = Debug('aa.rpc')

export class BundlerServer {
app: Express
private readonly httpServer: Server
public silent = false

constructor (
readonly methodHandler: UserOpMethodHandler,
readonly rip7560methodHandler: RIP7560MethodHandler,
readonly debugHandler: DebugMethodHandler,
readonly config: BundlerConfig,
readonly provider: Provider,
Expand Down Expand Up @@ -60,6 +63,9 @@ export class BundlerServer {
}

async _preflightCheck (): Promise<void> {
if (this.config.useRip7650Mode) {
return
}
if (await this.provider.getCode(this.config.entryPoint) === '0x') {
this.fatal(`entrypoint not deployed at ${this.config.entryPoint}`)
}
Expand Down Expand Up @@ -161,6 +167,24 @@ export class BundlerServer {
async handleMethod (method: string, params: any[]): Promise<any> {
let result: any
switch (method) {
/** RIP-7560 specific RPC API */
case 'eth_sendTransaction':
if (!this.config.useRip7650Mode) {
throw new RpcError(`Method ${method} is not supported`, -32601)
}
if (params[0].sender != null) {
result = await this.rip7560methodHandler.sendRIP7560Transaction(params[0])
} else {
result = await (this.provider as JsonRpcProvider).send(method, params)
}
break
case 'eth_getTransactionReceipt':
if (!this.config.useRip7650Mode) {
throw new RpcError(`Method ${method} is not supported`, -32601)
}
result = await this.rip7560methodHandler.getRIP7560TransactionReceipt(params[0])
break
/** EIP-4337 specific RPC API */
case 'eth_chainId':
// eslint-disable-next-line no-case-declarations
const { chainId } = await this.provider.getNetwork()
Expand Down Expand Up @@ -224,7 +248,10 @@ export class BundlerServer {
result = await this.debugHandler.getStakeStatus(params[0], params[1])
break
default:
throw new RpcError(`Method ${method} is not supported`, -32601)
// TODO: separate providers in tests
result = await (this.provider as JsonRpcProvider).send(method, params)
break
// throw new RpcError(`Method ${method} is not supported`, -32601)
}
return result
}
Expand Down
39 changes: 39 additions & 0 deletions packages/bundler/src/RIP7560MethodHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { JsonRpcProvider, TransactionReceipt } from '@ethersproject/providers'
import {
getRIP7560TransactionHash,
requireCond,
RIP7560Transaction,
tostr
} from '@account-abstraction/utils'
import { HEX_REGEX } from './UserOpMethodHandler'
import { ExecutionManager } from './modules/ExecutionManager'

export interface RIP7560TransactionReceipt extends TransactionReceipt {

}

export class RIP7560MethodHandler {
constructor (
readonly execManager: ExecutionManager,
readonly provider: JsonRpcProvider
) {}

async sendRIP7560Transaction (transaction: RIP7560Transaction): Promise<string> {
await this._validateParameters(transaction)
console.log(`RIP7560Transaction: Sender=${transaction.sender} Nonce=${tostr(transaction.nonce)} Paymaster=${transaction.paymaster ?? ''}`)
await this.execManager.sendUserOperation(transaction)
return getRIP7560TransactionHash(transaction)
}

async getRIP7560TransactionReceipt (txHash: string): Promise<RIP7560TransactionReceipt | null> {
requireCond(txHash?.toString()?.match(HEX_REGEX) != null, 'Missing/invalid userOpHash', -32601)
return await this.provider.getTransactionReceipt(txHash)
}

// TODO: align parameter names across 4337 and 7560
async _validateParameters (transaction: RIP7560Transaction): Promise<void> {
transaction.callGasLimit = transaction.callGasLimit ?? (transaction as any).gas
transaction.verificationGasLimit = transaction.verificationGasLimit ?? (transaction as any).validationGas
transaction.paymasterVerificationGasLimit = transaction.paymasterVerificationGasLimit ?? (transaction as any).paymasterGas
}
}
2 changes: 1 addition & 1 deletion packages/bundler/src/UserOpMethodHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { ExecutionManager } from './modules/ExecutionManager'
import { UserOperationByHashResponse, UserOperationReceipt } from './RpcTypes'
import { calcPreVerificationGas } from '@account-abstraction/sdk'

const HEX_REGEX = /^0x[a-fA-F\d]*$/i
export const HEX_REGEX = /^0x[a-fA-F\d]*$/i

/**
* return value from estimateUserOpGas
Expand Down
153 changes: 153 additions & 0 deletions packages/bundler/src/modules/BaseBundleManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { mergeStorageMap, StorageMap, BaseOperation } from '@account-abstraction/utils'
import { BigNumber, BigNumberish } from 'ethers'
import Debug from 'debug'

import { ValidateUserOpResult, IValidationManager } from '@account-abstraction/validation-manager'

import { ReputationManager, ReputationStatus } from './ReputationManager'
import { MempoolManager } from './MempoolManager'
import { IBundleManager } from './IBundleManager'

const debug = Debug('aa.exec.cron')

const THROTTLED_ENTITY_BUNDLE_COUNT = 4

export abstract class BaseBundleManager implements IBundleManager {
protected constructor (
readonly mempoolManager: MempoolManager,
readonly validationManager: IValidationManager,
readonly reputationManager: ReputationManager,
readonly maxBundleGas: number
) {}

abstract sendNextBundle (): Promise<any>

abstract handlePastEvents (): Promise<void>

abstract getPaymasterBalance (paymaster: string): Promise<BigNumber>

async _validatePaymasterBalanceSufficient (
paymaster: string,
requiredBalance: BigNumberish,
paymasterDeposit: { [paymaster: string]: BigNumber },
stakedEntityCount: { [addr: string]: number }
): Promise<boolean> {
if (paymasterDeposit[paymaster] == null) {
paymasterDeposit[paymaster] = await this.getPaymasterBalance(paymaster)
}
if (paymasterDeposit[paymaster].lt(requiredBalance)) {
return false
}
stakedEntityCount[paymaster] = (stakedEntityCount[paymaster] ?? 0) + 1
paymasterDeposit[paymaster] = paymasterDeposit[paymaster].sub(requiredBalance)
return true
}

async _createBundle (): Promise<[BaseOperation[], StorageMap]> {
const entries = this.mempoolManager.getSortedForInclusion()
const bundle: BaseOperation[] = []

// paymaster deposit should be enough for all UserOps in the bundle.
const paymasterDeposit: { [paymaster: string]: BigNumber } = {}
// throttled paymasters and deployers are allowed only small UserOps per bundle.
const stakedEntityCount: { [addr: string]: number } = {}
// each sender is allowed only once per bundle
const senders = new Set<string>()

// all entities that are known to be valid senders in the mempool
const knownSenders = this.mempoolManager.getKnownSenders()

const storageMap: StorageMap = {}
let totalGas = BigNumber.from(0)
debug('got mempool of ', entries.length)
// eslint-disable-next-line no-labels
mainLoop:
for (const entry of entries) {
const paymaster = entry.userOp.paymaster
const factory = entry.userOp.factory
const paymasterStatus = this.reputationManager.getStatus(paymaster)
const deployerStatus = this.reputationManager.getStatus(factory)
if (paymasterStatus === ReputationStatus.BANNED || deployerStatus === ReputationStatus.BANNED) {
this.mempoolManager.removeUserOp(entry.userOp)
continue
}
// [SREP-030]
if (paymaster != null && (paymasterStatus === ReputationStatus.THROTTLED ?? (stakedEntityCount[paymaster] ?? 0) > THROTTLED_ENTITY_BUNDLE_COUNT)) {
debug('skipping throttled paymaster', entry.userOp.sender, entry.userOp.nonce)
continue
}
// [SREP-030]
if (factory != null && (deployerStatus === ReputationStatus.THROTTLED ?? (stakedEntityCount[factory] ?? 0) > THROTTLED_ENTITY_BUNDLE_COUNT)) {
debug('skipping throttled factory', entry.userOp.sender, entry.userOp.nonce)
continue
}
if (senders.has(entry.userOp.sender)) {
debug('skipping already included sender', entry.userOp.sender, entry.userOp.nonce)
// allow only a single UserOp per sender per bundle
continue
}
let validationResult: ValidateUserOpResult
try {
// re-validate UserOp. no need to check stake, since it cannot be reduced between first and 2nd validation
validationResult = await this.validationManager.validateOperation(entry.userOp, entry.referencedContracts)
} catch (e: any) {
debug('failed 2nd validation:', e.message)
// failed validation. don't try anymore
this.mempoolManager.removeUserOp(entry.userOp)
continue
}

for (const storageAddress of Object.keys(validationResult.storageMap)) {
if (
storageAddress.toLowerCase() !== entry.userOp.sender.toLowerCase() &&
knownSenders.includes(storageAddress.toLowerCase())
) {
console.debug(`UserOperation from ${entry.userOp.sender} sender accessed a storage of another known sender ${storageAddress}`)
// eslint-disable-next-line no-labels
continue mainLoop
}
}

// todo: we take UserOp's callGasLimit, even though it will probably require less (but we don't
// attempt to estimate it to check)
// which means we could "cram" more UserOps into a bundle.
const userOpGasCost = BigNumber.from(validationResult.returnInfo.preOpGas).add(entry.userOp.callGasLimit)
const newTotalGas = totalGas.add(userOpGasCost)
if (newTotalGas.gt(this.maxBundleGas)) {
break
}

if (paymaster != null) {
const isSufficient = await this._validatePaymasterBalanceSufficient(
paymaster,
validationResult.returnInfo.prefund,
paymasterDeposit,
stakedEntityCount
)
if (!isSufficient) {
// not enough balance in paymaster to pay for all UserOps
// (but it passed validation, so it can sponsor them separately
continue
}
}
if (factory != null) {
stakedEntityCount[factory] = (stakedEntityCount[factory] ?? 0) + 1
}

// If sender's account already exist: replace with its storage root hash
// TODO: UNCOMMENT THESE LINES THESE SEEM IMPORTANT
// if (this.mergeToAccountRootHash && this.conditionalRpc && entry.userOp.factory == null) {
// const { storageHash } = await this.provider.send('eth_getProof', [entry.userOp.sender, [], 'latest'])
// storageMap[entry.userOp.sender.toLowerCase()] = storageHash
// }
mergeStorageMap(storageMap, validationResult.storageMap)

senders.add(entry.userOp.sender)
bundle.push(entry.userOp)
totalGas = newTotalGas
}
return [bundle, storageMap]
}

}

Loading

0 comments on commit 1917021

Please sign in to comment.