Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AA-381: Create a "pull bundle" block building mode and a 'getRip7560Bundle' method #214

Merged
merged 28 commits into from
Aug 5, 2024
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
f26a19e
Create 'useRip7560Mode' and 'gethDevMode' configuration parameters
forshtat Jul 25, 2024
96ad45e
WIP: Implement "pull" mode
forshtat Jul 28, 2024
fd20fa7
Fix localconfig bundler config
forshtat Jul 28, 2024
304a358
Remove 'gethDevMode' parameter and let the tests trigger the block cr…
forshtat Jul 28, 2024
c5ac7c5
Fix local config
forshtat Jul 28, 2024
2fbe74c
WIP: Use different ports for public and private bundler APIs
forshtat Jul 28, 2024
83a9f46
WIP: Wrap the express callbacks with parser and private-public API fi…
forshtat Jul 28, 2024
6a96569
Merge branch 'main' of github.com:eth-infinitism/bundler into AA-381-…
forshtat Jul 28, 2024
d984a60
Remove 'hello' and add TODO to provide correct 'validForBlock'
forshtat Jul 29, 2024
9215666
Merge branch 'main' of github.com:eth-infinitism/bundler into AA-381-…
forshtat Jul 29, 2024
d6c58bc
Account for "minBaseFee", "maxBundleGas", "maxBundleSize" in "create…
forshtat Jul 29, 2024
5267719
Fixes
forshtat Aug 3, 2024
68eb2a9
Update packages/bundler/src/runBundler.ts
forshtat Aug 4, 2024
7f2c4f3
Move 'userOpMaxGas' calculation into the MempoolEntry class
forshtat Aug 4, 2024
e4e6d41
Check configuration for valid RIP-7560 mode parameters; explain them …
forshtat Aug 4, 2024
b42d8d4
Rename 'eth_getRip7560Bundle' to 'aa_getRip7560Bundle'
forshtat Aug 4, 2024
79577d8
Add parameter and send 1 wei transaction in 'sendBundle' (#218)
forshtat Aug 4, 2024
80197b9
Fix config
forshtat Aug 4, 2024
52baf83
Fix: remove unnecessary command line default params
forshtat Aug 4, 2024
1a0a1d5
Wtf?
forshtat Aug 4, 2024
53c3eb4
Fix depcheck
forshtat Aug 4, 2024
794e0ea
Wtf??
forshtat Aug 4, 2024
e368fa3
Fix
forshtat Aug 4, 2024
7f6451f
Remove the "short-circuit" out of 'sendBundle'
forshtat Aug 5, 2024
e566fa8
Fix check
forshtat Aug 5, 2024
f8ee2df
Wait for block in SendBundleNow
forshtat Aug 5, 2024
35e27eb
Fix
forshtat Aug 5, 2024
f4c8e56
wait for tx to get mined
drortirosh Aug 5, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"preprocess1": "yarn submodule-update && yarn && cd submodules/account-abstraction && yarn && yarn --cwd contracts prepack",
"submodule-update": "git submodule update --remote --init --recursive",
"runop-self": "ts-node ./packages/bundler/src/runner/runop.ts --deployFactory --selfBundler --unsafe",
"ci": "env && yarn depcheck && yarn preprocess ; yarn lerna-test"
"ci": "env && yarn depcheck && yarn preprocess && yarn lerna-test"
},
"dependencies": {
"@typescript-eslint/eslint-plugin": "^5.33.0",
Expand Down
6 changes: 5 additions & 1 deletion packages/bundler/localconfig/bundler.config.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"gasFactor": "1",
"port": "3000",
"privateApiPort": "3001",
"network": "http://127.0.0.1:8545",
"entryPoint": "0x0000000071727De22E5E9d8BAf0edAc6f37da032",
"beneficiary": "0xd21934eD8eAf27a67f0A70042Af50A1D6d195E81",
Expand All @@ -10,5 +11,8 @@
"minStake": "1" ,
"minUnstakeDelay": 0 ,
"autoBundleInterval": 3,
"autoBundleMempoolSize": 10
"autoBundleMempoolSize": 10,
"rip7560": false,
"rip7560Mode": "PULL",
"gethDevMode": true
}
5 changes: 4 additions & 1 deletion packages/bundler/localconfig/bundler.rip7560.config.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"gasFactor": "1",
"port": "3000",
"privateApiPort": "3001",
"network": "http://127.0.0.1:8545",
"entryPoint": "0x0000000071727De22E5E9d8BAf0edAc6f37da032",
"beneficiary": "0xd21934eD8eAf27a67f0A70042Af50A1D6d195E81",
Expand All @@ -11,5 +12,7 @@
"minUnstakeDelay": 0 ,
"autoBundleInterval": 3,
"autoBundleMempoolSize": 10,
"useRip7560Mode": true
"rip7560": true,
"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 @@ -28,6 +28,7 @@
"@account-abstraction/sdk": "^0.7.0",
"@account-abstraction/utils": "^0.7.0",
"@account-abstraction/validation-manager": "^0.7.0",
"@ethereumjs/rlp": "^5.0.2",
"@ethersproject/abi": "^5.7.0",
"@ethersproject/providers": "^5.7.0",
"@types/cors": "^2.8.12",
Expand Down
12 changes: 9 additions & 3 deletions packages/bundler/src/BundlerConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export interface BundlerConfig {
mnemonic: string
network: string
port: string
privateApiPort: string
unsafe: boolean
debugRpc?: boolean
conditionalRpc: boolean
Expand All @@ -23,7 +24,9 @@ export interface BundlerConfig {
minUnstakeDelay: number
autoBundleInterval: number
autoBundleMempoolSize: number
useRip7560Mode: boolean
rip7560: boolean
rip7560Mode: string
gethDevMode: boolean
}

// TODO: implement merging config (args -> config.js -> default) and runtime shape validation
Expand All @@ -35,6 +38,7 @@ export const BundlerConfigShape = {
mnemonic: ow.string,
network: ow.string,
port: ow.string,
privateApiPort: ow.string,
unsafe: ow.boolean,
debugRpc: ow.optional.boolean,
conditionalRpc: ow.boolean,
Expand All @@ -46,17 +50,19 @@ export const BundlerConfigShape = {
minUnstakeDelay: ow.number,
autoBundleInterval: ow.number,
autoBundleMempoolSize: ow.number,
useRip7560Mode: ow.boolean
rip7560: ow.boolean,
rip7560Mode: ow.string.oneOf(['PULL', 'PUSH']),
gethDevMode: ow.boolean
}

// TODO: consider if we want any default fields at all
// TODO: implement merging config (args -> config.js -> default) and runtime shape validation
export const bundlerConfigDefault: Partial<BundlerConfig> = {
port: '3000',
privateApiPort: '3001',
entryPoint: '0x0000000071727De22E5E9d8BAf0edAc6f37da032',
unsafe: false,
conditionalRpc: false,
useRip7560Mode: false,
minStake: MIN_STAKE_VALUE,
minUnstakeDelay: MIN_UNSTAKE_DELAY
}
144 changes: 106 additions & 38 deletions packages/bundler/src/BundlerServer.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import bodyParser from 'body-parser'
import cors from 'cors'
import express, { Express, Response, Request } from 'express'
import express, { Express, Response, Request, RequestHandler } from 'express'
import { Provider } from '@ethersproject/providers'
import { Signer, utils } from 'ethers'
import { parseEther } from 'ethers/lib/utils'
import { Server } from 'http'

import {
AddressZero, decodeRevertReason,
deepHexlify, IEntryPoint__factory,
erc4337RuntimeVersion,
packUserOp,
AddressZero,
IEntryPoint__factory,
RpcError,
UserOperation
UserOperation,
ValidationErrors,
decodeRevertReason,
deepHexlify,
erc4337RuntimeVersion,
packUserOp
} from '@account-abstraction/utils'

import { BundlerConfig } from './BundlerConfig'
Expand All @@ -23,9 +26,12 @@ import { DebugMethodHandler } from './DebugMethodHandler'
import Debug from 'debug'

const debug = Debug('aa.rpc')

export class BundlerServer {
app: Express
private readonly httpServer: Server
readonly appPublic: Express
readonly appPrivate: Express
private readonly httpServerPublic: Server
private readonly httpServerPrivate: Server
public silent = false

constructor (
Expand All @@ -36,32 +42,42 @@ export class BundlerServer {
readonly provider: Provider,
readonly wallet: Signer
) {
this.app = express()
this.app.use(cors())
this.app.use(bodyParser.json())

this.app.get('/', this.intro.bind(this))
this.app.post('/', this.intro.bind(this))

this.appPublic = express()
this.appPrivate = express()
// eslint-disable-next-line @typescript-eslint/no-misused-promises
this.initializeExpressApp(this.appPublic, this.getRpc(this.handleRpcPublic.bind(this)))
// eslint-disable-next-line @typescript-eslint/no-misused-promises
this.app.post('/rpc', this.rpc.bind(this))
this.initializeExpressApp(this.appPrivate, this.getRpc(this.handleRpcPrivate.bind(this)))

this.httpServerPublic = this.appPublic.listen(this.config.port)
this.httpServerPrivate = this.appPrivate.listen(this.config.privateApiPort)

this.httpServer = this.app.listen(this.config.port)
this.startingPromise = this._preflightCheck()
}

private initializeExpressApp (app: Express, handler: RequestHandler): void {
app.use(cors())
app.use(bodyParser.json())

app.get('/', this.intro.bind(this))
app.post('/', this.intro.bind(this))

app.post('/rpc', handler)
}

startingPromise: Promise<void>

async asyncStart (): Promise<void> {
await this.startingPromise
}

async stop (): Promise<void> {
this.httpServer.close()
this.httpServerPublic.close()
this.httpServerPrivate.close()
}

async _preflightCheck (): Promise<void> {
if (this.config.useRip7560Mode) {
if (this.config.rip7560) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably check the geth node supports eip7560...

// TODO: implement preflight checks for the RIP-7560 mode
return
}
Expand Down Expand Up @@ -107,27 +123,70 @@ export class BundlerServer {
res.send(`Account-Abstraction Bundler v.${erc4337RuntimeVersion}. please use "/rpc"`)
}

async rpc (req: Request, res: Response): Promise<void> {
let resContent: any
if (Array.isArray(req.body)) {
resContent = []
for (const reqItem of req.body) {
resContent.push(await this.handleRpc(reqItem))
// TODO: I don't see how to elegantly combine express callbacks with classes so I ended up with this spaghetti.
// This is temporary and probably should not be merged like that, we need to simplify the flow.
getRpc (handleRpc: any): any {
const rpc = async (req: Request, res: Response): Promise<void> => {
let resContent: any
if (Array.isArray(req.body)) {
resContent = []
for (const reqItem of req.body) {
resContent.push(await handleRpc(reqItem))
}
} else {
resContent = await handleRpc(req.body)
}

try {
res.send(resContent)
} catch (err: any) {
const error = {
message: err.message,
data: err.data,
code: err.code
}
this.log('failed: ', 'rpc::res.send()', 'error:', JSON.stringify(error))
}
} else {
resContent = await this.handleRpc(req.body)
}
return rpc.bind(this)
}

try {
res.send(resContent)
} catch (err: any) {
// TODO: deduplicate!
async handleRpcPublic (reqItem: any): Promise<any> {
const { method, jsonrpc, id } = reqItem

if (method === 'aa_getRip7560Bundle') {
const error = {
message: err.message,
data: err.data,
code: err.code
message: `requested RPC method (${method as string}) is not available`,
data: '',
code: ValidationErrors.InvalidRequest
}
return {
jsonrpc,
id,
error
}
this.log('failed: ', 'rpc::res.send()', 'error:', JSON.stringify(error))
}
return await this.handleRpc(reqItem)
}

// TODO: deduplicate!
async handleRpcPrivate (reqItem: any): Promise<any> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand this private/public distinction then - They just sit on different ports, but there are no restrictions on who can call the "private" api. Are we only planning to block requests on the "devops" side and not in code?
if so, shouldn't we just name it "blockBuilderRpcHandler" and "clientRpcHandler" since there is not private/public distinction in code? we basically rely on the cloud configuration to make sense of these names

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can rename it but I think in the future we may end up creating other use cases for an API that is only available "internally".
I think as long as it is trivial to prevent access to the "private" API with i.e. basic docker configuration there is no point in writing any access control code.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand why modify all existing support: we're adding a new express and handler for the (single) private API.

const { method, jsonrpc, id } = reqItem

if (method !== 'aa_getRip7560Bundle') {
const error = {
message: `requested RPC method (${method as string}) is not available`,
data: '',
code: ValidationErrors.InvalidRequest
}
return {
jsonrpc,
id,
error
}
}
return await this.handleRpc(reqItem)
}

async handleRpc (reqItem: any): Promise<any> {
Expand Down Expand Up @@ -172,18 +231,27 @@ export class BundlerServer {
let result: any
switch (method) {
/** RIP-7560 specific RPC API */
case 'aa_getRip7560Bundle': {
if (!this.config.rip7560) {
throw new RpcError(`Method ${method} is not supported`, -32601)
}
const [bundle] = await this.methodHandlerRip7560.getRip7560Bundle(
params[0].MinBaseFee, params[0].MaxBundleGas, params[0].MaxBundleSize
)
// TODO: provide a correct value for 'validForBlock'
result = { bundle, validForBlock: '0x0' }
break
}
case 'eth_sendTransaction':
if (!this.config.useRip7560Mode) {
if (!this.config.rip7560) {
throw new RpcError(`Method ${method} is not supported`, -32601)
}
if (params[0].sender != null) {
result = await this.methodHandlerRip7560.sendRIP7560Transaction(params[0])
// } else {
// result = await (this.provider as JsonRpcProvider).send(method, params)
}
break
case 'eth_getTransactionReceipt':
if (!this.config.useRip7560Mode) {
if (!this.config.rip7560) {
throw new RpcError(`Method ${method} is not supported`, -32601)
}
result = await this.methodHandlerRip7560.getRIP7560TransactionReceipt(params[0])
Expand Down
13 changes: 12 additions & 1 deletion packages/bundler/src/MethodHandlerRIP7560.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { BigNumberish } from 'ethers'
import { JsonRpcProvider, TransactionReceipt } from '@ethersproject/providers'
import {
AddressZero,
getRIP7560TransactionHash,
OperationBase,
OperationRIP7560,
StorageMap,
getRIP7560TransactionHash,
requireCond,
tostr
} from '@account-abstraction/utils'
Expand All @@ -26,6 +29,14 @@ export class MethodHandlerRIP7560 {
return getRIP7560TransactionHash(transaction)
}

async getRip7560Bundle (
minBaseFee: BigNumberish,
maxBundleGas: BigNumberish,
maxBundleSize: BigNumberish
): Promise<[OperationBase[], StorageMap]> {
return await this.execManager.createBundle(minBaseFee, maxBundleGas, maxBundleSize)
}

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