Skip to content
This repository was archived by the owner on Oct 20, 2024. It is now read-only.

Commit 0bb9e26

Browse files
authored
Add tracing to callGasLimit estimate (#144)
1 parent b3cd3a0 commit 0bb9e26

File tree

13 files changed

+198
-45
lines changed

13 files changed

+198
-45
lines changed

internal/config/values.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ type Values struct {
3636
DebugMode bool
3737
GinMode string
3838
BundlerCollectorTracer string
39+
BundlerErrorTracer string
3940
}
4041

4142
func envArrayToAddressSlice(s string) []common.Address {
@@ -118,8 +119,8 @@ func GetValues() *Values {
118119
}
119120
}
120121

121-
// Load js tracer from embedded file
122-
bct, err := tracer.Load()
122+
// Load js tracers from embedded file
123+
trc, err := tracer.NewTracers()
123124
if err != nil {
124125
panic(err)
125126
}
@@ -154,6 +155,7 @@ func GetValues() *Values {
154155
BlocksInTheFuture: blocksInTheFuture,
155156
DebugMode: debugMode,
156157
GinMode: ginMode,
157-
BundlerCollectorTracer: bct,
158+
BundlerCollectorTracer: trc.BundlerCollectorTracer,
159+
BundlerErrorTracer: trc.BundlerErrorTracer,
158160
}
159161
}

internal/start/private.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ func PrivateMode() {
8383
c := client.New(mem, chain, conf.SupportedEntryPoints)
8484
c.SetGetUserOpReceiptFunc(client.GetUserOpReceiptWithEthClient(eth))
8585
c.SetGetSimulateValidationFunc(client.GetSimulateValidationWithRpcClient(rpc))
86-
c.SetGetCallGasEstimateFunc(client.GetCallGasEstimateWithEthClient(eth))
86+
c.SetGetCallGasEstimateFunc(client.GetCallGasEstimateWithEthClient(rpc, chain, conf.BundlerErrorTracer))
8787
c.SetGetUserOpByHashFunc(client.GetUserOpByHashWithEthClient(eth))
8888
c.UseLogger(logr)
8989
c.UseModules(

internal/start/searcher.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ func SearcherMode() {
8585
c := client.New(mem, chain, conf.SupportedEntryPoints)
8686
c.SetGetUserOpReceiptFunc(client.GetUserOpReceiptWithEthClient(eth))
8787
c.SetGetSimulateValidationFunc(client.GetSimulateValidationWithRpcClient(rpc))
88-
c.SetGetCallGasEstimateFunc(client.GetCallGasEstimateWithEthClient(eth))
88+
c.SetGetCallGasEstimateFunc(client.GetCallGasEstimateWithEthClient(rpc, chain, conf.BundlerErrorTracer))
8989
c.SetGetUserOpByHashFunc(client.GetUserOpByHashWithEthClient(eth))
9090
c.UseLogger(logr)
9191
c.UseModules(

pkg/client/utils.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,9 @@ func getCallGasEstimateNoop() GetCallGasEstimateFunc {
6565

6666
// GetCallGasEstimateWithEthClient returns an implementation of GetCallGasEstimateFunc that relies on an eth
6767
// client to fetch an estimate for callGasLimit.
68-
func GetCallGasEstimateWithEthClient(eth *ethclient.Client) GetCallGasEstimateFunc {
68+
func GetCallGasEstimateWithEthClient(rpc *rpc.Client, chain *big.Int, tracer string) GetCallGasEstimateFunc {
6969
return func(ep common.Address, op *userop.UserOperation) (uint64, error) {
70-
return gas.CallGasEstimate(eth, ep, op)
70+
return gas.CallGasEstimate(rpc, ep, op, chain, tracer)
7171
}
7272
}
7373

pkg/entrypoint/execution/simulate.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,21 @@ import (
55

66
"github.com/ethereum/go-ethereum/common"
77
"github.com/ethereum/go-ethereum/ethclient"
8+
"github.com/ethereum/go-ethereum/rpc"
89
"github.com/stackup-wallet/stackup-bundler/pkg/entrypoint"
910
"github.com/stackup-wallet/stackup-bundler/pkg/entrypoint/reverts"
1011
"github.com/stackup-wallet/stackup-bundler/pkg/errors"
1112
"github.com/stackup-wallet/stackup-bundler/pkg/userop"
1213
)
1314

1415
func SimulateHandleOp(
15-
eth *ethclient.Client,
16+
rpc *rpc.Client,
1617
entryPoint common.Address,
1718
op *userop.UserOperation,
1819
target common.Address,
1920
data []byte,
2021
) (*reverts.ExecutionResultRevert, error) {
21-
ep, err := entrypoint.NewEntrypoint(entryPoint, eth)
22+
ep, err := entrypoint.NewEntrypoint(entryPoint, ethclient.NewClient(rpc))
2223
if err != nil {
2324
return nil, err
2425
}

pkg/entrypoint/execution/trace.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package execution
2+
3+
import (
4+
"context"
5+
"math"
6+
"math/big"
7+
8+
"github.com/ethereum/go-ethereum/accounts/abi/bind"
9+
"github.com/ethereum/go-ethereum/common"
10+
"github.com/ethereum/go-ethereum/common/hexutil"
11+
"github.com/ethereum/go-ethereum/ethclient"
12+
"github.com/ethereum/go-ethereum/rpc"
13+
"github.com/stackup-wallet/stackup-bundler/pkg/entrypoint"
14+
"github.com/stackup-wallet/stackup-bundler/pkg/entrypoint/utils"
15+
"github.com/stackup-wallet/stackup-bundler/pkg/errors"
16+
"github.com/stackup-wallet/stackup-bundler/pkg/tracer"
17+
"github.com/stackup-wallet/stackup-bundler/pkg/userop"
18+
)
19+
20+
func TraceSimulateHandleOp(
21+
rpc *rpc.Client,
22+
entryPoint common.Address,
23+
op *userop.UserOperation,
24+
chainID *big.Int,
25+
customTracer string,
26+
target common.Address,
27+
data []byte,
28+
) error {
29+
ep, err := entrypoint.NewEntrypoint(entryPoint, ethclient.NewClient(rpc))
30+
if err != nil {
31+
return err
32+
}
33+
auth, err := bind.NewKeyedTransactorWithChainID(utils.DummyPk, chainID)
34+
if err != nil {
35+
return err
36+
}
37+
auth.GasLimit = math.MaxUint64
38+
auth.NoSend = true
39+
tx, err := ep.SimulateHandleOp(auth, entrypoint.UserOperation(*op), target, data)
40+
if err != nil {
41+
return err
42+
}
43+
44+
var res tracer.BundlerErrorReturn
45+
req := utils.TraceCallReq{
46+
From: common.HexToAddress("0x"),
47+
To: entryPoint,
48+
Data: tx.Data(),
49+
}
50+
opts := utils.TraceCallOpts{
51+
Tracer: customTracer,
52+
}
53+
if err := rpc.CallContext(context.Background(), &res, "debug_traceCall", &req, "latest", &opts); err != nil {
54+
return err
55+
}
56+
57+
if len(res.Reverts) != 0 {
58+
data, err := hexutil.Decode(res.Reverts[len(res.Reverts)-1])
59+
if err != nil {
60+
return err
61+
}
62+
63+
reason, err := errors.DecodeRevert(data)
64+
if err != nil {
65+
return err
66+
}
67+
return errors.NewRPCError(errors.EXECUTION_REVERTED, reason, reason)
68+
}
69+
return nil
70+
}

pkg/entrypoint/simulation/simulateutils.go

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,13 @@ package simulation
33
import (
44
mapset "github.com/deckarep/golang-set/v2"
55
"github.com/ethereum/go-ethereum/common"
6-
"github.com/ethereum/go-ethereum/common/hexutil"
7-
"github.com/ethereum/go-ethereum/crypto"
86
"github.com/stackup-wallet/stackup-bundler/pkg/entrypoint"
97
)
108

119
// EntityStakes provides a mapping for encountered entity addresses and their stake info on the EntryPoint.
1210
type EntityStakes map[common.Address]*entrypoint.IStakeManagerDepositInfo
1311

14-
type traceCallReq struct {
15-
From common.Address `json:"from"`
16-
To common.Address `json:"to"`
17-
Data hexutil.Bytes `json:"data"`
18-
}
19-
20-
type traceCallOpts struct {
21-
Tracer string `json:"tracer"`
22-
}
23-
2412
var (
25-
// A dummy private key used to build *bind.TransactOpts for simulation.
26-
dummyPk, _ = crypto.GenerateKey()
27-
2813
// Up to the first number marker represents factory validation.
2914
factoryNumberLevel = 0
3015

pkg/entrypoint/simulation/tracevalidation.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/ethereum/go-ethereum/rpc"
1515
"github.com/stackup-wallet/stackup-bundler/pkg/entrypoint"
1616
"github.com/stackup-wallet/stackup-bundler/pkg/entrypoint/methods"
17+
"github.com/stackup-wallet/stackup-bundler/pkg/entrypoint/utils"
1718
"github.com/stackup-wallet/stackup-bundler/pkg/tracer"
1819
"github.com/stackup-wallet/stackup-bundler/pkg/userop"
1920
)
@@ -32,7 +33,7 @@ func TraceSimulateValidation(
3233
if err != nil {
3334
return nil, err
3435
}
35-
auth, err := bind.NewKeyedTransactorWithChainID(dummyPk, chainID)
36+
auth, err := bind.NewKeyedTransactorWithChainID(utils.DummyPk, chainID)
3637
if err != nil {
3738
return nil, err
3839
}
@@ -44,12 +45,12 @@ func TraceSimulateValidation(
4445
}
4546

4647
var res tracer.BundlerCollectorReturn
47-
req := traceCallReq{
48+
req := utils.TraceCallReq{
4849
From: common.HexToAddress("0x"),
4950
To: entryPoint,
5051
Data: tx.Data(),
5152
}
52-
opts := traceCallOpts{
53+
opts := utils.TraceCallOpts{
5354
Tracer: customTracer,
5455
}
5556
if err := rpc.CallContext(context.Background(), &res, "debug_traceCall", &req, "latest", &opts); err != nil {

pkg/entrypoint/utils/tracing.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package utils
2+
3+
import (
4+
"github.com/ethereum/go-ethereum/common"
5+
"github.com/ethereum/go-ethereum/common/hexutil"
6+
"github.com/ethereum/go-ethereum/crypto"
7+
)
8+
9+
type TraceCallReq struct {
10+
From common.Address `json:"from"`
11+
To common.Address `json:"to"`
12+
Data hexutil.Bytes `json:"data"`
13+
}
14+
15+
type TraceCallOpts struct {
16+
Tracer string `json:"tracer"`
17+
}
18+
19+
var (
20+
// A dummy private key used to build *bind.TransactOpts for simulation.
21+
DummyPk, _ = crypto.GenerateKey()
22+
)

pkg/gas/callgas.go

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,21 @@ import (
55

66
"github.com/ethereum/go-ethereum/common"
77
"github.com/ethereum/go-ethereum/common/hexutil"
8-
"github.com/ethereum/go-ethereum/ethclient"
8+
"github.com/ethereum/go-ethereum/rpc"
99
"github.com/stackup-wallet/stackup-bundler/pkg/entrypoint/execution"
10-
"github.com/stackup-wallet/stackup-bundler/pkg/errors"
1110
"github.com/stackup-wallet/stackup-bundler/pkg/userop"
1211
)
1312

13+
// CallGasEstimate uses the simulateHandleOp method on the EntryPoint to derive an estimate for callGasLimit.
14+
//
15+
// TODO: This function requires an eth_call and a debug_traceCall. It could probably be optimized further by
16+
// just using a debug_traceCall.
1417
func CallGasEstimate(
15-
eth *ethclient.Client,
18+
rpc *rpc.Client,
1619
from common.Address,
1720
op *userop.UserOperation,
21+
chainID *big.Int,
22+
tracer string,
1823
) (uint64, error) {
1924
data, err := op.ToMap()
2025
if err != nil {
@@ -28,19 +33,20 @@ func CallGasEstimate(
2833
return 0, err
2934
}
3035

31-
sim, err := execution.SimulateHandleOp(eth, from, simOp, op.Sender, op.CallData)
36+
sim, err := execution.SimulateHandleOp(rpc, from, simOp, common.Address{}, []byte{})
3237
if err != nil {
3338
return 0, err
3439
}
35-
if !sim.TargetSuccess {
36-
reason, err := errors.DecodeRevert(sim.TargetResult)
37-
if err != nil {
38-
return 0, err
39-
}
40-
return 0, errors.NewRPCError(errors.EXECUTION_REVERTED, reason, reason)
40+
41+
if err := execution.TraceSimulateHandleOp(rpc, from, op, chainID, tracer, common.Address{}, []byte{}); err != nil {
42+
return 0, err
4143
}
4244

4345
tg := big.NewInt(0).Div(sim.Paid, op.MaxFeePerGas)
4446
cgl := big.NewInt(0).Sub(tg, sim.PreOpGas)
45-
return cgl.Uint64(), nil
47+
call := NewDefaultOverhead().NonZeroValueCall()
48+
if cgl.Cmp(call) >= 1 {
49+
return cgl.Uint64(), nil
50+
}
51+
return call.Uint64(), nil
4652
}

pkg/tracer/BundlerErrorTracer.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
var tracer = {
2+
reverts: [],
3+
numberCounter: 0,
4+
5+
fault: function fault(log, db) {},
6+
result: function result(ctx, db) {
7+
return {
8+
reverts: this.reverts,
9+
};
10+
},
11+
12+
enter: function enter(frame) {},
13+
exit: function exit(frame) {
14+
if (frame.getError() !== undefined && this.numberCounter === 3) {
15+
this.reverts.push(toHex(frame.getOutput()));
16+
}
17+
},
18+
19+
step: function step(log, db) {
20+
var opcode = log.op.toString();
21+
if (log.getDepth() === 1 && opcode === "NUMBER") this.numberCounter++;
22+
},
23+
};

pkg/tracer/load.go

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
)
1111

1212
//go:embed *BundlerCollectorTracer.js
13+
//go:embed *BundlerErrorTracer.js
1314
var files embed.FS
1415
var (
1516
commentRegex = regexp.MustCompile("(?m)^.*//.*$[\r\n]+")
@@ -28,11 +29,42 @@ func parse(code string) string {
2829
return m
2930
}
3031

31-
// Load reads the BundlerCollectorTracer.js file and returns a string that can be passed to a debug RPC
32-
// method as a custom tracer.
33-
func Load() (string, error) {
34-
var t string
35-
err := fs.WalkDir(files, ".", func(path string, d fs.DirEntry, err error) error {
32+
type Tracers struct {
33+
BundlerCollectorTracer string
34+
BundlerErrorTracer string
35+
}
36+
37+
// NewBundlerTracers reads the *Tracer.js files and returns a collection of strings that can be passed to a
38+
// debug RPC method as a custom tracer.
39+
func NewTracers() (*Tracers, error) {
40+
var bct string
41+
err := fs.WalkDir(
42+
files,
43+
"BundlerCollectorTracer.js",
44+
func(path string, d fs.DirEntry, err error) error {
45+
if err != nil {
46+
return err
47+
}
48+
49+
if d.IsDir() {
50+
return nil
51+
}
52+
53+
b, err := fs.ReadFile(files, path)
54+
if err != nil {
55+
return err
56+
}
57+
58+
bct = parse(string(b))
59+
return nil
60+
},
61+
)
62+
if err != nil {
63+
return nil, err
64+
}
65+
66+
var et string
67+
err = fs.WalkDir(files, "BundlerErrorTracer.js", func(path string, d fs.DirEntry, err error) error {
3668
if err != nil {
3769
return err
3870
}
@@ -46,9 +78,15 @@ func Load() (string, error) {
4678
return err
4779
}
4880

49-
t = parse(string(b))
81+
et = parse(string(b))
5082
return nil
5183
})
84+
if err != nil {
85+
return nil, err
86+
}
5287

53-
return t, err
88+
return &Tracers{
89+
BundlerCollectorTracer: bct,
90+
BundlerErrorTracer: et,
91+
}, nil
5492
}

pkg/tracer/types.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,8 @@ type BundlerCollectorReturn struct {
5353
Logs []LogInfo `json:"logs"`
5454
Debug []any `json:"debug"`
5555
}
56+
57+
// BundlerErrorReturn is the return value from performing an EVM trace with BundlerErrorTracer.js.
58+
type BundlerErrorReturn struct {
59+
Reverts []string `json:"reverts"`
60+
}

0 commit comments

Comments
 (0)