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

Commit 06dc515

Browse files
authored
Broadcast UserOperation batch to list of builders in searcher mode (#352)
1 parent 75d86dd commit 06dc515

File tree

15 files changed

+345
-139
lines changed

15 files changed

+345
-139
lines changed

go.mod

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ require (
1212
github.com/go-logr/zerologr v1.2.3
1313
github.com/go-playground/validator/v10 v10.12.0
1414
github.com/google/go-cmp v0.5.9
15-
github.com/metachris/flashbotsrpc v0.5.0
15+
github.com/metachris/flashbotsrpc v0.6.0
1616
github.com/mitchellh/mapstructure v1.5.0
1717
github.com/puzpuzpuz/xsync/v3 v3.0.1
1818
github.com/rs/zerolog v1.29.0
@@ -101,3 +101,5 @@ require (
101101
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect
102102
gopkg.in/yaml.v3 v3.0.1 // indirect
103103
)
104+
105+
replace github.com/metachris/flashbotsrpc => github.com/stackup-wallet/flashbotsrpc v0.6.1-rc1

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -300,8 +300,6 @@ github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPn
300300
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
301301
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
302302
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
303-
github.com/metachris/flashbotsrpc v0.5.0 h1:5OLpm6+6n4kXxeh3TZBeSj0PQWDxqUsOFwy7xertXQQ=
304-
github.com/metachris/flashbotsrpc v0.5.0/go.mod h1:UrS249kKA1PK27sf12M6tUxo/M4ayfFrBk7IMFY1TNw=
305303
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
306304
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
307305
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
@@ -369,6 +367,8 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
369367
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
370368
github.com/spf13/viper v1.15.0 h1:js3yy885G8xwJa6iOISGFwd+qlUo5AvyXb7CiihdtiU=
371369
github.com/spf13/viper v1.15.0/go.mod h1:fFcTBJxvhhzSJiZy8n+PeW6t8l+KeT/uTARa0jHOQLA=
370+
github.com/stackup-wallet/flashbotsrpc v0.6.1-rc1 h1:kCB1WQZgD2edUiPZh+mghl1Ir/cuH5/4bI+a03Ki6Tc=
371+
github.com/stackup-wallet/flashbotsrpc v0.6.1-rc1/go.mod h1:UrS249kKA1PK27sf12M6tUxo/M4ayfFrBk7IMFY1TNw=
372372
github.com/status-im/keycard-go v0.2.0 h1:QDLFswOQu1r5jsycloeQh3bVU8n/NatHHaZobtDnDzA=
373373
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
374374
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=

internal/config/constants.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import "math/big"
55
var (
66
EthereumChainID = big.NewInt(1)
77
GoerliChainID = big.NewInt(5)
8+
SepoliaChainID = big.NewInt(11155111)
89
ArbitrumOneChainID = big.NewInt(42161)
910
ArbitrumGoerliChainID = big.NewInt(421613)
1011
ArbitrumSepoliaChainID = big.NewInt(421614)

internal/config/values.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ type Values struct {
2626
Beneficiary string
2727

2828
// Searcher mode variables.
29-
EthBuilderUrl string
29+
EthBuilderUrls []string
3030
BlocksInTheFuture int
3131

3232
// Observability variables.
@@ -88,7 +88,7 @@ func GetValues() *Values {
8888
viper.SetDefault("erc4337_bundler_max_batch_gas_limit", 25000000)
8989
viper.SetDefault("erc4337_bundler_max_op_ttl_seconds", 180)
9090
viper.SetDefault("erc4337_bundler_max_ops_for_unstaked_sender", 4)
91-
viper.SetDefault("erc4337_bundler_blocks_in_the_future", 25)
91+
viper.SetDefault("erc4337_bundler_blocks_in_the_future", 6)
9292
viper.SetDefault("erc4337_bundler_otel_insecure_mode", false)
9393
viper.SetDefault("erc4337_bundler_debug_mode", false)
9494
viper.SetDefault("erc4337_bundler_gin_mode", gin.ReleaseMode)
@@ -117,7 +117,7 @@ func GetValues() *Values {
117117
_ = viper.BindEnv("erc4337_bundler_max_batch_gas_limit")
118118
_ = viper.BindEnv("erc4337_bundler_max_op_ttl_seconds")
119119
_ = viper.BindEnv("erc4337_bundler_max_ops_for_unstaked_sender")
120-
_ = viper.BindEnv("erc4337_bundler_eth_builder_url")
120+
_ = viper.BindEnv("erc4337_bundler_eth_builder_urls")
121121
_ = viper.BindEnv("erc4337_bundler_blocks_in_the_future")
122122
_ = viper.BindEnv("erc4337_bundler_otel_service_name")
123123
_ = viper.BindEnv("erc4337_bundler_otel_collector_headers")
@@ -147,8 +147,8 @@ func GetValues() *Values {
147147

148148
switch viper.GetString("mode") {
149149
case "searcher":
150-
if variableNotSetOrIsNil("erc4337_bundler_eth_builder_url") {
151-
panic("Fatal config error: erc4337_bundler_eth_builder_url not set")
150+
if variableNotSetOrIsNil("erc4337_bundler_eth_builder_urls") {
151+
panic("Fatal config error: erc4337_bundler_eth_builder_urls not set")
152152
}
153153
}
154154

@@ -175,7 +175,7 @@ func GetValues() *Values {
175175
maxBatchGasLimit := big.NewInt(int64(viper.GetInt("erc4337_bundler_max_batch_gas_limit")))
176176
maxOpTTL := time.Second * viper.GetDuration("erc4337_bundler_max_op_ttl_seconds")
177177
maxOpsForUnstakedSender := viper.GetInt("erc4337_bundler_max_ops_for_unstaked_sender")
178-
ethBuilderUrl := viper.GetString("erc4337_bundler_eth_builder_url")
178+
ethBuilderUrls := envArrayToStringSlice(viper.GetString("erc4337_bundler_eth_builder_urls"))
179179
blocksInTheFuture := viper.GetInt("erc4337_bundler_blocks_in_the_future")
180180
otelServiceName := viper.GetString("erc4337_bundler_otel_service_name")
181181
otelCollectorHeader := envKeyValStringToMap(viper.GetString("erc4337_bundler_otel_collector_headers"))
@@ -196,7 +196,7 @@ func GetValues() *Values {
196196
MaxBatchGasLimit: maxBatchGasLimit,
197197
MaxOpTTL: maxOpTTL,
198198
MaxOpsForUnstakedSender: maxOpsForUnstakedSender,
199-
EthBuilderUrl: ethBuilderUrl,
199+
EthBuilderUrls: ethBuilderUrls,
200200
BlocksInTheFuture: blocksInTheFuture,
201201
OTELServiceName: otelServiceName,
202202
OTELCollectorHeaders: otelCollectorHeader,

internal/start/searcher.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ func SearcherMode() {
6060

6161
eth := ethclient.NewClient(rpc)
6262

63-
fb := flashbotsrpc.NewFlashbotsRPC(conf.EthBuilderUrl)
63+
fb := flashbotsrpc.NewBuilderBroadcastRPC(conf.EthBuilderUrls)
6464

6565
chain, err := eth.ChainID(context.Background())
6666
if err != nil {
@@ -117,6 +117,7 @@ func SearcherMode() {
117117

118118
// TODO: Create separate go-routine for tracking transactions sent to the block builder.
119119
builder := builder.New(eoa, eth, fb, beneficiary, conf.BlocksInTheFuture)
120+
120121
paymaster := paymaster.New(db)
121122

122123
// Init Client
@@ -191,6 +192,7 @@ func SearcherMode() {
191192
}
192193
r.POST("/", handlers...)
193194
r.POST("/rpc", handlers...)
195+
194196
if err := r.Run(fmt.Sprintf(":%d", conf.Port)); err != nil {
195197
log.Fatal(err)
196198
}

internal/testutils/buildermock.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package testutils
2+
3+
import (
4+
"encoding/json"
5+
"net/http"
6+
"net/http/httptest"
7+
8+
"github.com/metachris/flashbotsrpc"
9+
)
10+
11+
func BadBuilderRpcMock() *httptest.Server {
12+
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
13+
res := &flashbotsrpc.RelayErrorResponse{
14+
Error: "Mock upstream builder error",
15+
}
16+
w.WriteHeader(http.StatusOK)
17+
if err := json.NewEncoder(w).Encode(res); err != nil {
18+
panic(err)
19+
}
20+
}))
21+
}

internal/testutils/constants.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ import (
55
"time"
66

77
"github.com/ethereum/go-ethereum/common"
8+
"github.com/ethereum/go-ethereum/common/hexutil"
9+
"github.com/ethereum/go-ethereum/crypto"
810
"github.com/stackup-wallet/stackup-bundler/pkg/entrypoint"
11+
"github.com/stackup-wallet/stackup-bundler/pkg/signer"
912
)
1013

1114
var (
@@ -44,4 +47,8 @@ var (
4447
UnstakeDelaySec: uint32(0),
4548
WithdrawTime: big.NewInt(0),
4649
}
50+
51+
pk, _ = crypto.GenerateKey()
52+
DummyEOA, _ = signer.New(hexutil.Encode(crypto.FromECDSA(pk))[2:])
53+
MockHash = "0xdeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddead"
4754
)

internal/testutils/ethmock.go

Lines changed: 36 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,44 @@
11
package testutils
22

33
import (
4-
"encoding/json"
5-
"fmt"
6-
"net/http"
7-
"net/http/httptest"
8-
)
4+
"math/big"
5+
"time"
96

10-
type mockReq struct {
11-
JsonRpc string `json:"jsonrpc"`
12-
ID float64 `json:"id"`
13-
Method string `json:"method"`
14-
}
7+
"github.com/ethereum/go-ethereum/common"
8+
"github.com/ethereum/go-ethereum/common/hexutil"
9+
)
1510

16-
type mockRes struct {
17-
JsonRpc string `json:"jsonrpc"`
18-
ID float64 `json:"id"`
19-
Result any `json:"result"`
11+
func NewBlockMock() map[string]any {
12+
return map[string]any{
13+
"parentHash": MockHash,
14+
"sha3Uncles": MockHash,
15+
"stateRoot": MockHash,
16+
"transactionsRoot": MockHash,
17+
"receiptsRoot": MockHash,
18+
"logsBloom": "0xdeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddead",
19+
"difficulty": "0x0",
20+
"number": "0x1",
21+
"gasLimit": hexutil.EncodeBig(big.NewInt(30000000)),
22+
"gasUsed": hexutil.EncodeBig(big.NewInt(5000000)),
23+
"timestamp": hexutil.EncodeUint64(uint64(time.Now().Unix())),
24+
"extraData": "0x",
25+
}
2026
}
2127

22-
type MethodMocks map[string]any
23-
24-
// EthMock returns a httptest.Server for mocking the return value of a JSON-RPC method call to an Ethereum node.
25-
func EthMock(mocks MethodMocks) *httptest.Server {
26-
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
27-
var req mockReq
28-
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
29-
panic(err)
30-
}
31-
32-
mock, ok := mocks[req.Method]
33-
if !ok {
34-
w.WriteHeader(http.StatusBadRequest)
35-
if _, err := w.Write([]byte(fmt.Sprintf("method not in mocks: %s", req.Method))); err != nil {
36-
panic(err)
37-
}
38-
return
39-
}
40-
41-
res := &mockRes{
42-
JsonRpc: req.JsonRpc,
43-
ID: req.ID,
44-
Result: mock,
45-
}
46-
w.WriteHeader(http.StatusOK)
47-
if err := json.NewEncoder(w).Encode(res); err != nil {
48-
panic(err)
49-
}
50-
}))
28+
func NewTransactionReceiptMock() map[string]any {
29+
return map[string]any{
30+
"blockHash": MockHash,
31+
"blockNumber": "0x1",
32+
"cumulativeGasUsed": "0x1",
33+
"effectiveGasPrice": "0x1",
34+
"from": common.HexToAddress("0x").Hex(),
35+
"gasUsed": "0x1",
36+
"logs": []any{},
37+
"logsBloom": "0xdeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddead",
38+
"status": "0x1",
39+
"to": common.HexToAddress("0x").Hex(),
40+
"transactionHash": MockHash,
41+
"transactionIndex": "0x1",
42+
"type": "0x2",
43+
}
5144
}

internal/testutils/rpcmock.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package testutils
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"net/http"
7+
"net/http/httptest"
8+
)
9+
10+
type mockReq struct {
11+
JsonRpc string `json:"jsonrpc"`
12+
ID float64 `json:"id"`
13+
Method string `json:"method"`
14+
}
15+
16+
type mockRes struct {
17+
JsonRpc string `json:"jsonrpc"`
18+
ID float64 `json:"id"`
19+
Result any `json:"result"`
20+
}
21+
22+
type MethodMocks map[string]any
23+
24+
func RpcMock(mocks MethodMocks) *httptest.Server {
25+
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
26+
var req mockReq
27+
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
28+
panic(err)
29+
}
30+
mock, ok := mocks[req.Method]
31+
if !ok {
32+
w.WriteHeader(http.StatusBadRequest)
33+
if _, err := w.Write([]byte(fmt.Sprintf("method not in mocks: %s", req.Method))); err != nil {
34+
panic(err)
35+
}
36+
return
37+
}
38+
39+
res := &mockRes{
40+
JsonRpc: req.JsonRpc,
41+
ID: req.ID,
42+
Result: mock,
43+
}
44+
w.WriteHeader(http.StatusOK)
45+
if err := json.NewEncoder(w).Encode(res); err != nil {
46+
panic(err)
47+
}
48+
}))
49+
}

pkg/entrypoint/transaction/handleops.go

Lines changed: 4 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package transaction
22

33
import (
4-
bytesPkg "bytes"
54
"context"
65
"errors"
76
"math"
@@ -11,7 +10,6 @@ import (
1110
"github.com/ethereum/go-ethereum"
1211
"github.com/ethereum/go-ethereum/accounts/abi/bind"
1312
"github.com/ethereum/go-ethereum/common"
14-
"github.com/ethereum/go-ethereum/common/hexutil"
1513
"github.com/ethereum/go-ethereum/core/types"
1614
"github.com/ethereum/go-ethereum/ethclient"
1715
"github.com/stackup-wallet/stackup-bundler/pkg/entrypoint"
@@ -38,6 +36,7 @@ type Opts struct {
3836
Tip *big.Int
3937
GasPrice *big.Int
4038
GasLimit uint64
39+
NoSend bool
4140
WaitTimeout time.Duration
4241
}
4342

@@ -104,6 +103,7 @@ func HandleOps(opts *Opts) (txn *types.Transaction, err error) {
104103
return nil, err
105104
}
106105
auth.GasLimit = opts.GasLimit
106+
auth.NoSend = opts.NoSend
107107

108108
nonce, err := opts.Eth.NonceAt(context.Background(), opts.EOA.Address, nil)
109109
if err != nil {
@@ -123,55 +123,11 @@ func HandleOps(opts *Opts) (txn *types.Transaction, err error) {
123123
txn, err = ep.HandleOps(auth, toAbiType(opts.Batch), opts.Beneficiary)
124124
if err != nil {
125125
return nil, err
126-
} else if opts.WaitTimeout == 0 {
126+
} else if opts.WaitTimeout == 0 || opts.NoSend {
127127
// Don't wait for transaction to be included. All userOps in the current batch will be dropped
128128
// regardless of the transaction status.
129129
return txn, nil
130130
}
131131

132-
ctx, cancel := context.WithTimeout(context.Background(), opts.WaitTimeout)
133-
defer cancel()
134-
if receipt, err := bind.WaitMined(ctx, opts.Eth, txn); err != nil {
135-
return nil, err
136-
} else if receipt.Status == types.ReceiptStatusFailed {
137-
// Return an error here so that the current batch stays in the mempool. In the next bundler iteration,
138-
// the offending userOps will be dropped during gas estimation.
139-
return nil, errors.New("transaction: failed status")
140-
}
141-
return txn, nil
142-
}
143-
144-
// CreateRawHandleOps returns a raw transaction string that calls handleOps() on the EntryPoint with a given
145-
// batch, gas limit, and tip.
146-
func CreateRawHandleOps(opts *Opts) (string, error) {
147-
ep, err := entrypoint.NewEntrypoint(opts.EntryPoint, opts.Eth)
148-
if err != nil {
149-
return "", err
150-
}
151-
152-
auth, err := bind.NewKeyedTransactorWithChainID(opts.EOA.PrivateKey, opts.ChainID)
153-
if err != nil {
154-
return "", err
155-
}
156-
auth.GasLimit = opts.GasLimit
157-
auth.NoSend = true
158-
if opts.BaseFee != nil {
159-
tip, err := opts.Eth.SuggestGasTipCap(context.Background())
160-
if err != nil {
161-
return "", err
162-
}
163-
164-
auth.GasTipCap = tip
165-
auth.GasFeeCap = big.NewInt(0).Add(opts.BaseFee, tip)
166-
}
167-
168-
tx, err := ep.HandleOps(auth, toAbiType(opts.Batch), opts.Beneficiary)
169-
if err != nil {
170-
return "", err
171-
}
172-
173-
ts := types.Transactions{tx}
174-
rawTxBytes := new(bytesPkg.Buffer)
175-
ts.EncodeIndex(0, rawTxBytes)
176-
return hexutil.Encode(rawTxBytes.Bytes()), nil
132+
return Wait(txn, opts.Eth, opts.WaitTimeout)
177133
}

0 commit comments

Comments
 (0)