Skip to content

Commit

Permalink
Improve gas estimation, upgrade to Node v20, Add tenderly support
Browse files Browse the repository at this point in the history
* improved gas estimation

* throw error if max fee per gas is 0

* automatic chain detection

* fix unused imports

* make maxPriorityFeePerGas maxFeePerGas for linea

* updated viem

* fix new types

* fixed polygon and mumbai gas price calculation

* added tenderly devnet support, fixed estimateUserOperationGas return type

* fixed estimateGas issue

* fixed crash when tenderly is used

* increase balance sent to wallets

* Boosted gas price on linear

* Increased polling interval for executor

* Increase railway timeout

* Increase polling to 1000

* Turn down polling interval to 300ms

* Update restartPolicyType to always from on_failure

* Added refill interval; setInterval to refill wallets (pimlicolabs#21)

* Update refill interval from 5 mins to 20 mins

* Added multicall engine to split missing balance payment in one transaction (pimlicolabs#22)

* Remove overbidding in gas price

* Amped up gas price

* Removed excessive refill boost

* Limit querying for user operation hash to the last 20000n blocks

* If fromBlock is less than 0, limit to 0

* Boost gas prices by 2x

* updated viem

* fix typing

* update dockerfile node version to 20

* Update gas price multiplier from 2x to 1.33x

* Updated gas price multiplier from 1.33x to 1.5x

* Lower gas price

* 1.33x to 2x

* added flush rpc method

* return correct response

* use all wallets, not just available ones

* added more logs

* fix awaiting

* Upped gas price multiplier from 2x to 2.5x

* 3x gas price multiplier

* ability to flush utility wallet too

* increase gas multiplier

* Upped gas price multiplier to 4x to quickly process backlog

* lower gas price

* don't await flush

* decrease gas price

* use fee history

* new replacement for linea

* increase gas price

* double gas price for refill

* lower gas price a bit

* update readme

* increase width

* edit readme

---------

Co-authored-by: Kristof Gazso <kristof.gazso@gmail.com>
  • Loading branch information
HilliamT and kristofgazso authored Jul 6, 2023
1 parent acda305 commit cd60f5c
Show file tree
Hide file tree
Showing 30 changed files with 919 additions and 286 deletions.
6 changes: 3 additions & 3 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
"version": "0.2.0",
"configurations": [
{
"command": "yarn test",
"name": "Build and Run Integration Tests",
"command": "pnpm start",
"name": "Run",
"request": "launch",
"type": "node-terminal"
}
]
}
}
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# production ready dockerfile that runs pnpm start
FROM node:18-alpine
FROM node:20-alpine

# set working directory
WORKDIR /app
Expand Down
6 changes: 2 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
<p align="center"><a href="https://docs.pimlico/reference/bundler"><img width="500" title="Lodestar" src='https://imgur.com/STYtirQ.png' /></a></p>
<p align="center"><a href="https://docs.pimlico/reference/bundler"><img width="1000" title="Alto" src='https://i.imgur.com/qgVAdjN.png' /></a></p>

# ⛰️ Alto ⛰️

![GitHub release (latest by date)](https://img.shields.io/github/v/release/pimlicolabs/alto)
![GitHub Workflow Status (with branch)](https://img.shields.io/github/actions/workflow/status/pimlicolabs/alto/tests.yml?branch=main)
![Node Version](https://img.shields.io/badge/node-18.x-green)
![Node Version](https://img.shields.io/badge/node-20.x-green)

Alto is a Typescript implementation of the [ERC-4337 bundler specification](https://eips.ethereum.org/EIPS/eip-4337) developed by [Pimlico](https://pimlico.io), focused on type safety and transaction inclusion reliability.

Expand Down
2 changes: 1 addition & 1 deletion packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"fastify": "^4.16.0",
"fastify-cors": "3.0.3",
"pino-http": "^8.3.3",
"viem": "^0.3.14",
"viem": "^1.0.7",
"zod": "^3.21.4",
"zod-validation-error": "^1.3.0"
}
Expand Down
85 changes: 63 additions & 22 deletions packages/api/src/rpcHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
Address,
BundlerClearStateResponseResult,
BundlerDumpMempoolResponseResult,
BundlerFlushStuckTransactionsResponseResult,
BundlerRequest,
BundlerResponse,
BundlerSendBundleNowResponseResult,
Expand All @@ -13,7 +14,6 @@ import {
ChainIdResponseResult,
EntryPointAbi,
EstimateUserOperationGasResponseResult,
ExecutionErrors,
GetUserOperationByHashResponseResult,
GetUserOperationReceiptResponseResult,
HexData32,
Expand Down Expand Up @@ -129,6 +129,11 @@ export class RpcHandler implements IRpcEndpoint {
method,
result: await this.pimlico_getUserOperationStatus(...request.params)
}
case "debug_bundler_flushStuckTransactions":
return {
method,
result: await this.debug_bundler_flushStuckTransactions(...request.params)
}
}
}

Expand All @@ -152,29 +157,23 @@ export class RpcHandler implements IRpcEndpoint {
throw new Error(`EntryPoint ${entryPoint} not supported, supported EntryPoints: ${this.config.entryPoint}`)
}

const validationResult = await this.validator.getValidationResult(userOperation)
if (userOperation.maxFeePerGas === 0n) {
throw new RpcError("user operation max fee per gas must be larger than 0 during gas estimation")
}

const executionResult = await this.validator.getExecutionResult(userOperation)

const preVerificationGas = BigInt(calcPreVerificationGas(userOperation))
const verificationGas = BigInt(validationResult.returnInfo.preOpGas)
const callGasLimit = await this.config.publicClient
.estimateGas({
to: userOperation.sender,
data: userOperation.callData,
account: entryPoint
})
.then((b) => BigInt(b))
.catch((err) => {
if (err instanceof Error) {
const message = err.message.match(/reason="(.*?)"/)?.at(1) ?? "execution reverted"
throw new RpcError(message, ExecutionErrors.UserOperationReverted)
} else {
throw err
}
})
const verificationGas = BigInt(executionResult.preOpGas)
const calculatedCallGasLimit =
executionResult.paid / userOperation.maxFeePerGas - executionResult.preOpGas + 21000n

const callGasLimit = calculatedCallGasLimit > 9000n ? calculatedCallGasLimit : 9000n

return {
preVerificationGas,
verificationGas,
verificationGasLimit: verificationGas,
callGasLimit
}
}
Expand Down Expand Up @@ -209,11 +208,22 @@ export class RpcHandler implements IRpcEndpoint {
async eth_getUserOperationByHash(userOperationHash: HexData32): Promise<GetUserOperationByHashResponseResult> {
const userOperationEventAbiItem = getAbiItem({ abi: EntryPointAbi, name: "UserOperationEvent" })

// Only query up to the last `fullBlockRange` = 20000 blocks
const latestBlock = await this.config.publicClient.getBlockNumber()
const fullBlockRange = 20000n

let fromBlock: bigint
if (this.config.usingTenderly) {
fromBlock = latestBlock - 100n
} else {
fromBlock = latestBlock - fullBlockRange
}

const filterResult = await this.config.publicClient.getLogs({
address: this.config.entryPoint,
event: userOperationEventAbiItem,
fromBlock: 0n,
toBlock: "latest",
fromBlock: fromBlock > 0n ? fromBlock : 0n,
toBlock: latestBlock,
args: {
userOpHash: userOperationHash
}
Expand Down Expand Up @@ -272,11 +282,22 @@ export class RpcHandler implements IRpcEndpoint {
async eth_getUserOperationReceipt(userOperationHash: HexData32): Promise<GetUserOperationReceiptResponseResult> {
const userOperationEventAbiItem = getAbiItem({ abi: EntryPointAbi, name: "UserOperationEvent" })

// Only query up to the last `fullBlockRange` = 20000 blocks
const latestBlock = await this.config.publicClient.getBlockNumber()
const fullBlockRange = 20000n

let fromBlock: bigint
if (this.config.usingTenderly) {
fromBlock = latestBlock - 100n
} else {
fromBlock = latestBlock - fullBlockRange
}

const filterResult = await this.config.publicClient.getLogs({
address: this.config.entryPoint,
event: userOperationEventAbiItem,
fromBlock: 0n,
toBlock: "latest",
fromBlock: fromBlock > 0n ? fromBlock : 0n,
toBlock: latestBlock,
args: {
userOpHash: userOperationHash
}
Expand All @@ -287,6 +308,19 @@ export class RpcHandler implements IRpcEndpoint {
}

const userOperationEvent = filterResult[0]
// throw if any of the members of userOperationEvent are undefined
if (
userOperationEvent.args.actualGasCost === undefined ||
userOperationEvent.args.sender === undefined ||
userOperationEvent.args.nonce === undefined ||
userOperationEvent.args.userOpHash === undefined ||
userOperationEvent.args.success === undefined ||
userOperationEvent.args.paymaster === undefined ||
userOperationEvent.args.actualGasUsed === undefined
) {
throw new Error("userOperationEvent has undefined members")
}

const txHash = userOperationEvent.transactionHash
if (txHash === null) {
// transaction pending
Expand Down Expand Up @@ -400,4 +434,11 @@ export class RpcHandler implements IRpcEndpoint {
): Promise<PimlicoGetUserOperationStatusResponseResult> {
return this.monitor.getUserOperationStatus(userOperationHash)
}

// rome-ignore lint/nursery/useCamelCase: <explanation>
async debug_bundler_flushStuckTransactions(): Promise<BundlerFlushStuckTransactionsResponseResult> {
await this.executor.flushStuckTransactions()

return "ok"
}
}
17 changes: 14 additions & 3 deletions packages/api/test/handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { z } from "zod"
import { UnsafeValidator } from "@alto/validator"
import { SimpleAccountFactoryAbi, SimpleAccountFactoryBytecode } from "@alto/types/src/contracts/SimpleAccountFactory"
import { NullExecutor } from "@alto/executor/src"
import { Monitor } from "@alto/executor"

describe("handler", () => {
let clients: Clients
Expand All @@ -29,7 +30,7 @@ describe("handler", () => {

let signer: Account

before(async function () {
beforeEach(async function () {
// destructure the return value
anvilProcess = await launchAnvil()
const privateKey = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" // first private key in anvil
Expand All @@ -45,13 +46,17 @@ describe("handler", () => {
)

const anvilChainId = await clients.public.getChainId()
const validator = new UnsafeValidator(clients.public, entryPoint)
const logger = initDebugLogger("silent")
const validator = new UnsafeValidator(clients.public, entryPoint, logger)
const rpcHandlerConfig: RpcHandlerConfig = {
publicClient: clients.public,
chainId: anvilChainId,
entryPoint: entryPoint
}
handler = new RpcHandler(rpcHandlerConfig, validator, new NullExecutor(), initDebugLogger("fatal"))

const monitor = new Monitor()

handler = new RpcHandler(rpcHandlerConfig, validator, new NullExecutor(), monitor, logger)
})

after(async function () {
Expand Down Expand Up @@ -135,6 +140,12 @@ describe("handler", () => {

const gas = await handler.eth_estimateUserOperationGas(op, entryPoint)

/*
preVerificationGas: 43852n,
verificationGas: 422484n,
callGasLimit: 21000n
*/

expect(gas).toMatchSchema(
z.object({
callGasLimit: hexNumberSchema,
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"@alto/validator": "*",
"@types/node": "^18.16.3",
"dotenv": "^16.0.3",
"viem": "^0.3.14",
"viem": "^1.0.7",
"yargs": "^17.7.1",
"zod-validation-error": "^1.3.0"
},
Expand Down
Loading

0 comments on commit cd60f5c

Please sign in to comment.