Skip to content

Commit

Permalink
AA-233: Accept state override in 'estimateGas' RPC method (WIP)
Browse files Browse the repository at this point in the history
  • Loading branch information
forshtat authored and drortirosh committed May 16, 2024
1 parent 276de79 commit 07467b0
Show file tree
Hide file tree
Showing 8 changed files with 76 additions and 23 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ You can still run the bundler with such nodes, but with `--unsafe` so it would s

If you don't have geth installed locally, you can use docker to run it:
```
docker run --rm -ti --name geth -p 8545:8545 ethereum/client-go:v1.10.26 \
docker run --rm -ti --name geth -p 8545:8545 ethereum/client-go:v1.13.5 \
--miner.gaslimit 12000000 \
--http --http.api personal,eth,net,web3,debug \
--http.vhosts '*,localhost,host.docker.internal' --http.addr "0.0.0.0" \
--ignore-legacy-receipts --allow-insecure-unlock --rpc.allow-unprotected-txs \
--allow-insecure-unlock --rpc.allow-unprotected-txs \
--dev \
--verbosity 2 \
--nodiscover --maxpeers 0 --mine --miner.threads 1 \
--nodiscover --maxpeers 0 --mine \
--networkid 1337
```

Expand Down
2 changes: 1 addition & 1 deletion packages/bundler/src/BundlerServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ export class BundlerServer {
result = await this.methodHandler.sendUserOperation(params[0], params[1])
break
case 'eth_estimateUserOperationGas':
result = await this.methodHandler.estimateUserOperationGas(params[0], params[1])
result = await this.methodHandler.estimateUserOperationGas(params[0], params[1], params[2])
break
case 'eth_getUserOperationReceipt':
result = await this.methodHandler.getUserOperationReceipt(params[0])
Expand Down
6 changes: 3 additions & 3 deletions packages/bundler/src/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import fs from 'fs'

import { BundlerConfig, bundlerConfigDefault, BundlerConfigShape } from './BundlerConfig'
import { Wallet, Signer } from 'ethers'
import { BaseProvider, JsonRpcProvider } from '@ethersproject/providers'
import { JsonRpcProvider } from '@ethersproject/providers'

function getCommandLineParams (programOpts: any): Partial<BundlerConfig> {
const params: any = {}
Expand Down Expand Up @@ -33,7 +33,7 @@ export function getNetworkProvider (url: string): JsonRpcProvider {
return new JsonRpcProvider(url)
}

export async function resolveConfiguration (programOpts: any): Promise<{ config: BundlerConfig, provider: BaseProvider, wallet: Signer }> {
export async function resolveConfiguration (programOpts: any): Promise<{ config: BundlerConfig, provider: JsonRpcProvider, wallet: Signer }> {
const commandLineParams = getCommandLineParams(programOpts)
let fileConfig: Partial<BundlerConfig> = {}
const configFileName = programOpts.config
Expand All @@ -49,7 +49,7 @@ export async function resolveConfiguration (programOpts: any): Promise<{ config:
return { config, provider, wallet: provider.getSigner() }
}

const provider: BaseProvider = getNetworkProvider(config.network)
const provider = getNetworkProvider(config.network)
let mnemonic: string
let wallet: Wallet
try {
Expand Down
25 changes: 24 additions & 1 deletion packages/bundler/src/RpcTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,32 @@ import { TransactionReceipt } from '@ethersproject/providers'
import { UserOperation } from '@account-abstraction/utils'

/**
* RPC calls return types
* RPC calls input and return types
*/

export interface StateOverride {
/**
* Fake balance to set for the account before executing the call.
*/
balance?: BigNumberish
/**
* Fake nonce to set for the account before executing the call.
*/
nonce?: BigNumberish
/**
* Fake EVM bytecode to inject into the account before executing the call.
*/
code?: string
/**
* Fake key-value mapping to override all slots in the account storage before executing the call.
*/
state?: Object
/**
* Fake key-value mapping to override individual slots in the account storage before executing the call.
*/
stateDiff?: Object
}

/**
* return value from estimateUserOpGas
*/
Expand Down
25 changes: 12 additions & 13 deletions packages/bundler/src/UserOpMethodHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,24 @@ import { JsonRpcProvider, Log, Provider } from '@ethersproject/providers'

import { BundlerConfig } from './BundlerConfig'
import {
UserOperation,
deepHexlify,
erc4337RuntimeVersion,
requireCond,
tostr,
RpcError,
ValidationErrors,
requireAddressAndFields,
packUserOp,
PackedUserOperation,
unpackUserOp,
simulationRpcParams,
decodeSimulateHandleOpResult,
AddressZero,
ValidationErrors,
RpcError,
decodeRevertReason,
mergeValidationDataValues,
UserOperationEventEvent, IEntryPoint
UserOperationEventEvent, IEntryPoint, requireCond, deepHexlify, tostr, erc4337RuntimeVersion
} from '@account-abstraction/utils'
import { ExecutionManager } from './modules/ExecutionManager'
import { UserOperationByHashResponse, UserOperationReceipt } from './RpcTypes'
import {StateOverride, UserOperationByHashResponse, UserOperationReceipt } from './RpcTypes'
import { calcPreVerificationGas } from '@account-abstraction/sdk'
import { EventFragment } from '@ethersproject/abi'
import { UserOperation } from '@account-abstraction/utils'

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

Expand Down Expand Up @@ -59,7 +55,7 @@ export interface EstimateUserOpGasResult {
export class UserOpMethodHandler {
constructor (
readonly execManager: ExecutionManager,
readonly provider: Provider,
readonly provider: JsonRpcProvider,
readonly signer: Signer,
readonly config: BundlerConfig,
readonly entryPoint: IEntryPoint
Expand Down Expand Up @@ -111,8 +107,9 @@ export class UserOpMethodHandler {
* eth_estimateUserOperationGas RPC api.
* @param userOp1 input userOp (may have gas fields missing, so they can be estimated)
* @param entryPointInput
* @param stateOverride
*/
async estimateUserOperationGas (userOp1: Partial<UserOperation>, entryPointInput: string): Promise<EstimateUserOpGasResult> {
async estimateUserOperationGas (userOp1: Partial<UserOperation>, entryPointInput: string, stateOverride?: StateOverride): Promise<EstimateUserOpGasResult> {
const userOp: UserOperation = {
// default values for missing fields.
maxFeePerGas: 0,
Expand All @@ -126,13 +123,15 @@ export class UserOpMethodHandler {
// todo: validation manager duplicate?
const provider = this.provider as JsonRpcProvider
const rpcParams = simulationRpcParams('simulateHandleOp', this.entryPoint.address, userOp, [AddressZero, '0x'],
{
stateOverride
// {
// allow estimation when account's balance is zero.
// todo: need a way to flag this, and not enable always.
// [userOp.sender]: {
// balance: hexStripZeros(parseEther('1').toHexString())
// }
})
// }
)
const ret = await provider.send('eth_call', rpcParams)
.catch((e: any) => { throw new RpcError(decodeRevertReason(e) as string, ValidationErrors.SimulateValidation) })

Expand Down
3 changes: 1 addition & 2 deletions packages/bundler/src/runBundler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import { DebugMethodHandler } from './DebugMethodHandler'
import { supportsDebugTraceCall } from '@account-abstraction/validation-manager'
import { resolveConfiguration } from './Config'
import { bundlerConfigDefault } from './BundlerConfig'
import { JsonRpcProvider } from '@ethersproject/providers'
import { parseEther } from 'ethers/lib/utils'

// this is done so that console.log outputs BigNumber as hex string instead of unreadable object
Expand Down Expand Up @@ -114,7 +113,7 @@ export async function runBundler (argv: string[], overrideExit = true): Promise<
console.log('deployed EntryPoint at', addr)
if ((await wallet.getBalance()).eq(0)) {
console.log('=== testnet: fund signer')
const signer = (provider as JsonRpcProvider).getSigner()
const signer = provider.getSigner()
await signer.sendTransaction({ to: await wallet.getAddress(), value: parseEther('1') })
}
}
Expand Down
26 changes: 26 additions & 0 deletions packages/bundler/test/UserOpMethodHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { parseEther, resolveProperties } from 'ethers/lib/utils'

import { BundlerConfig } from '../src/BundlerConfig'

import { toHex } from 'hardhat/internal/util/bigint'
import { Signer, Wallet } from 'ethers'
import { SimpleAccountAPI } from '@account-abstraction/sdk'
import { postExecutionDump } from '@account-abstraction/utils/dist/src/postExecCheck'
Expand Down Expand Up @@ -132,6 +133,31 @@ describe('UserOpMethodHandler', function () {
// and estimation doesn't perform full deploy-validate-execute cycle)
expect(ret.callGasLimit).to.be.closeTo(25000, 10000)
})

it('estimateUserOperationGas should estimate using state overrides', async function () {
const ver: string = await (provider as any).send('web3_clientVersion')
if (ver.match('go1') == null) {
console.warn('\t==WARNING: test requires state override support on Geth (go-ethereum) node available after 1.12.1; ver=' + ver)
this.skip()
}
const op = await smartAccountAPI.createSignedUserOp({
target,
data: '0xdeadface'
})
expect(await methodHandler.estimateUserOperationGas(await resolveHexlify(op), entryPoint.address).catch(e => e.message)).to.eql('FailedOp: AA21 didn\'t pay prefund')
// should estimate same UserOperation with balance override set to 1 ether
const ret = await methodHandler.estimateUserOperationGas(
await resolveHexlify(op),
entryPoint.address,
{
[await op.sender]: {
balance: toHex(1e18)
}
}
)
expect(ret.verificationGasLimit).to.be.closeTo(300000, 100000)
expect(ret.callGasLimit).to.be.closeTo(25000, 10000)
})
})

describe('sendUserOperation', function () {
Expand Down
6 changes: 6 additions & 0 deletions packages/utils/src/ERC4337Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { BigNumber, BigNumberish, BytesLike, ethers } from 'ethers'
import Debug from 'debug'
import { PackedUserOperation } from './Utils'

import { IEntryPoint__factory } from './types'

const debug = Debug('aa.utils')

// UserOperation is the first parameter of getUserOpHash
Expand Down Expand Up @@ -266,6 +268,8 @@ export function getUserOpHash (op: UserOperation, entryPoint: string, chainId: n

const ErrorSig = keccak256(Buffer.from('Error(string)')).slice(0, 10) // 0x08c379a0
const FailedOpSig = keccak256(Buffer.from('FailedOp(uint256,string)')).slice(0, 10) // 0x220266b6
// TODO: this is a temporary fix as ValidationResult is a struct starting with EntryPoint v0.7
const ValidationResultSig = IEntryPoint__factory.createInterface().getSighash('ValidationResult')

interface DecodedError {
message: string
Expand All @@ -292,6 +296,8 @@ export function decodeErrorReason (error: string | Error): DecodedError | undefi
message,
opIndex
}
} else if (error.startsWith(ValidationResultSig)) {
return IEntryPoint__factory.createInterface().decodeErrorResult('ValidationResult', error) as any
}
}

Expand Down

0 comments on commit 07467b0

Please sign in to comment.