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

Commit bc2321a

Browse files
authored
Add searcher mode for sending UserOperations to block builders (#94)
1 parent 3494599 commit bc2321a

File tree

13 files changed

+429
-21
lines changed

13 files changed

+429
-21
lines changed

.air.searcher-mode.toml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
root = "."
2+
testdata_dir = "testdata"
3+
tmp_dir = "tmp"
4+
5+
[build]
6+
args_bin = []
7+
bin = ";ERC4337_BUNDLER_GIN_MODE=debug ./tmp/stackup-bundler start --mode searcher"
8+
cmd = "go build -o ./tmp/stackup-bundler main.go"
9+
delay = 1000
10+
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
11+
exclude_file = []
12+
exclude_regex = ["_test.go"]
13+
exclude_unchanged = false
14+
follow_symlink = false
15+
full_bin = ""
16+
include_dir = []
17+
include_ext = ["go", "tpl", "tmpl", "html"]
18+
kill_delay = "0s"
19+
log = "build-errors.log"
20+
send_interrupt = false
21+
stop_on_error = true
22+
23+
[color]
24+
app = ""
25+
build = "yellow"
26+
main = "magenta"
27+
runner = "green"
28+
watcher = "cyan"
29+
30+
[log]
31+
time = false
32+
33+
[misc]
34+
clean_on_exit = false
35+
36+
[screen]
37+
clear_on_rebuild = false

.github/workflows/pipeline.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,5 @@ jobs:
4141

4242
- name: Lint
4343
uses: golangci/golangci-lint-action@v3
44+
with:
45+
version: v1.51.0

Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,8 @@ fetch-wallet:
1414
dev-private-mode:
1515
air -c .air.private-mode.toml
1616

17+
dev-searcher-mode:
18+
air -c .air.searcher-mode.toml
19+
1720
dev-reset-default-data-dir:
1821
rm -rf /tmp/stackup_bundler

cmd/start.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,13 @@ var startCmd = &cobra.Command{
1313
Short: "Starts an instance in the specified mode",
1414
Long: `The start command has the following modes:
1515
16-
1. private: A JSON-RPC client and bundler backed by a private mempool.`,
16+
1. private: A bundler backed by a private mempool and compatible with all EVM networks.
17+
2. searcher: A bundler backed by the P2P mempool and integrated with a Block Builder API.`,
1718
Run: func(cmd *cobra.Command, args []string) {
1819
if viper.GetString("mode") == "private" {
1920
start.PrivateMode()
21+
} else if viper.GetString("mode") == "searcher" {
22+
start.SearcherMode()
2023
} else {
2124
panic(fmt.Sprintf("Fatal flag error: \"%s\" mode not supported", viper.GetString("mode")))
2225
}

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ require (
1212
github.com/go-logr/zerologr v1.2.2
1313
github.com/go-playground/validator/v10 v10.11.1
1414
github.com/google/go-cmp v0.5.9
15+
github.com/metachris/flashbotsrpc v0.5.0
1516
github.com/mitchellh/mapstructure v1.5.0
1617
github.com/rs/zerolog v1.28.0
1718
github.com/spf13/cobra v1.6.0
@@ -24,6 +25,7 @@ require (
2425
require (
2526
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6 // indirect
2627
github.com/btcsuite/btcd/btcec/v2 v2.2.0 // indirect
28+
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 // indirect
2729
github.com/cespare/xxhash v1.1.0 // indirect
2830
github.com/cespare/xxhash/v2 v2.1.2 // indirect
2931
github.com/deckarep/golang-set v1.8.0 // indirect

go.sum

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ github.com/VictoriaMetrics/fastcache v1.6.0 h1:C/3Oi3EiBCqufydp1neRZkqcwmEiuRT9c
4646
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
4747
github.com/btcsuite/btcd/btcec/v2 v2.2.0 h1:fzn1qaOt32TuLjFlkzYSsBC35Q3KUjT1SwPxiMSCF5k=
4848
github.com/btcsuite/btcd/btcec/v2 v2.2.0/go.mod h1:U7MHm051Al6XmscBQ0BoNydpOTsFAn707034b5nY8zU=
49-
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U=
49+
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 h1:KdUfX2zKommPRa+PD0sWZUyXe9w277ABlgELO7H04IM=
50+
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
5051
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
5152
github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk=
5253
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
@@ -226,6 +227,7 @@ github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANyt
226227
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
227228
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
228229
github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
230+
github.com/jarcoal/httpmock v1.0.8 h1:8kI16SoO6LQKgPE7PvQuV+YuD/inwHd7fOOe2zMbo4k=
229231
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
230232
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
231233
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
@@ -255,6 +257,8 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k
255257
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
256258
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
257259
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
260+
github.com/metachris/flashbotsrpc v0.5.0 h1:5OLpm6+6n4kXxeh3TZBeSj0PQWDxqUsOFwy7xertXQQ=
261+
github.com/metachris/flashbotsrpc v0.5.0/go.mod h1:UrS249kKA1PK27sf12M6tUxo/M4ayfFrBk7IMFY1TNw=
258262
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
259263
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
260264
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
@@ -332,6 +336,9 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
332336
github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs=
333337
github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
334338
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY=
339+
github.com/tidwall/gjson v1.8.1 h1:8j5EE9Hrh3l9Od1OIEDAb7IpezNA20UdRngNAj5N0WU=
340+
github.com/tidwall/match v1.0.3 h1:FQUVvBImDutD8wJLN6c5eMzWtjgONK9MwIBCOrUJKeE=
341+
github.com/tidwall/pretty v1.1.0 h1:K3hMW5epkdAVwibsQEfR/7Zj0Qgt4DxtNumTq/VloO8=
335342
github.com/tklauser/go-sysconf v0.3.5 h1:uu3Xl4nkLzQfXNsWn15rPc/HQCJKObbt1dKJeWp3vU4=
336343
github.com/tklauser/go-sysconf v0.3.5/go.mod h1:MkWzOF4RMCshBAMXuhXJs64Rte09mITnppBXY/rYEFI=
337344
github.com/tklauser/numcpus v0.2.2 h1:oyhllyrScuYI6g+h/zUvNXNp1wy7x8qQy3t/piefldA=

internal/config/values.go

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ type Values struct {
2323
MaxOpsForUnstakedSender int
2424
Beneficiary string
2525

26+
// Searcher mode variables.
27+
EthBuilderUrl string
28+
BlocksInTheFuture int
29+
2630
// Undocumented variables.
2731
DebugMode bool
2832
GinMode string
@@ -39,6 +43,10 @@ func envArrayToAddressSlice(s string) []common.Address {
3943
return slc
4044
}
4145

46+
func variableNotSetOrIsNil(env string) bool {
47+
return !viper.IsSet(env) || viper.GetString(env) == ""
48+
}
49+
4250
// GetValues returns config for the bundler that has been read in from env vars. See
4351
// https://docs.stackup.sh/docs/packages/bundler/configure for details.
4452
func GetValues() *Values {
@@ -49,6 +57,7 @@ func GetValues() *Values {
4957
viper.SetDefault("erc4337_bundler_max_verification_gas", 1500000)
5058
viper.SetDefault("erc4337_bundler_max_ops_for_unstaked_sender", 4)
5159
viper.SetDefault("erc4337_bundler_debug_mode", false)
60+
viper.SetDefault("erc4337_bundler_blocks_in_the_future", 25)
5261
viper.SetDefault("erc4337_bundler_gin_mode", gin.ReleaseMode)
5362

5463
// Read in from .env file if available
@@ -73,15 +82,16 @@ func GetValues() *Values {
7382
_ = viper.BindEnv("erc4337_bundler_beneficiary")
7483
_ = viper.BindEnv("erc4337_bundler_max_verification_gas")
7584
_ = viper.BindEnv("erc4337_bundler_max_ops_for_unstaked_sender")
85+
_ = viper.BindEnv("erc4337_bundler_eth_builder_url")
86+
_ = viper.BindEnv("erc4337_bundler_blocks_in_the_future")
7687
_ = viper.BindEnv("erc4337_bundler_gin_mode")
7788

7889
// Validate required variables
79-
if !viper.IsSet("erc4337_bundler_eth_client_url") ||
80-
viper.GetString("erc4337_bundler_eth_client_url") == "" {
90+
if variableNotSetOrIsNil("erc4337_bundler_eth_client_url") {
8191
panic("Fatal config error: erc4337_bundler_eth_client_url not set")
8292
}
8393

84-
if !viper.IsSet("erc4337_bundler_private_key") || viper.GetString("erc4337_bundler_private_key") == "" {
94+
if variableNotSetOrIsNil("erc4337_bundler_private_key") {
8595
panic("Fatal config error: erc4337_bundler_private_key not set")
8696
}
8797

@@ -93,6 +103,13 @@ func GetValues() *Values {
93103
viper.SetDefault("erc4337_bundler_beneficiary", s.Address.String())
94104
}
95105

106+
switch viper.GetString("mode") {
107+
case "searcher":
108+
if variableNotSetOrIsNil("erc4337_bundler_eth_builder_url") {
109+
panic("Fatal config error: erc4337_bundler_eth_builder_url not set")
110+
}
111+
}
112+
96113
// Load js tracer from embedded file
97114
bct, err := tracer.Load()
98115
if err != nil {
@@ -108,6 +125,8 @@ func GetValues() *Values {
108125
beneficiary := viper.GetString("erc4337_bundler_beneficiary")
109126
maxVerificationGas := big.NewInt(int64(viper.GetInt("erc4337_bundler_max_verification_gas")))
110127
maxOpsForUnstakedSender := viper.GetInt("erc4337_bundler_max_ops_for_unstaked_sender")
128+
ethBuilderUrl := viper.GetString("erc4337_bundler_eth_builder_url")
129+
blocksInTheFuture := viper.GetInt("erc4337_bundler_blocks_in_the_future")
111130
debugMode := viper.GetBool("erc4337_bundler_debug_mode")
112131
ginMode := viper.GetString("erc4337_bundler_gin_mode")
113132
return &Values{
@@ -119,6 +138,8 @@ func GetValues() *Values {
119138
Beneficiary: beneficiary,
120139
MaxVerificationGas: maxVerificationGas,
121140
MaxOpsForUnstakedSender: maxOpsForUnstakedSender,
141+
EthBuilderUrl: ethBuilderUrl,
142+
BlocksInTheFuture: blocksInTheFuture,
122143
DebugMode: debugMode,
123144
GinMode: ginMode,
124145
BundlerCollectorTracer: bct,

internal/start/db.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package start
2+
3+
import (
4+
"time"
5+
6+
badger "github.com/dgraph-io/badger/v3"
7+
)
8+
9+
func runDBGarbageCollection(db *badger.DB) {
10+
go func(db *badger.DB) {
11+
ticker := time.NewTicker(5 * time.Minute)
12+
defer ticker.Stop()
13+
14+
for range ticker.C {
15+
again:
16+
err := db.RunValueLogGC(0.7)
17+
if err == nil {
18+
goto again
19+
}
20+
}
21+
}(db)
22+
}

internal/start/private.go

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"fmt"
66
"log"
77
"net/http"
8-
"time"
98

109
badger "github.com/dgraph-io/badger/v3"
1110
"github.com/ethereum/go-ethereum/common"
@@ -25,21 +24,6 @@ import (
2524
"github.com/stackup-wallet/stackup-bundler/pkg/signer"
2625
)
2726

28-
func runDBGarbageCollection(db *badger.DB) {
29-
go func(db *badger.DB) {
30-
ticker := time.NewTicker(5 * time.Minute)
31-
defer ticker.Stop()
32-
33-
for range ticker.C {
34-
again:
35-
err := db.RunValueLogGC(0.7)
36-
if err == nil {
37-
goto again
38-
}
39-
}
40-
}(db)
41-
}
42-
4327
func PrivateMode() {
4428
conf := config.GetValues()
4529

internal/start/searcher.go

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package start
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"log"
7+
"net/http"
8+
9+
badger "github.com/dgraph-io/badger/v3"
10+
"github.com/ethereum/go-ethereum/common"
11+
"github.com/ethereum/go-ethereum/ethclient"
12+
"github.com/ethereum/go-ethereum/rpc"
13+
"github.com/gin-contrib/cors"
14+
"github.com/gin-gonic/gin"
15+
"github.com/metachris/flashbotsrpc"
16+
"github.com/stackup-wallet/stackup-bundler/internal/config"
17+
"github.com/stackup-wallet/stackup-bundler/internal/logger"
18+
"github.com/stackup-wallet/stackup-bundler/pkg/bundler"
19+
"github.com/stackup-wallet/stackup-bundler/pkg/client"
20+
"github.com/stackup-wallet/stackup-bundler/pkg/jsonrpc"
21+
"github.com/stackup-wallet/stackup-bundler/pkg/mempool"
22+
"github.com/stackup-wallet/stackup-bundler/pkg/modules/builder"
23+
"github.com/stackup-wallet/stackup-bundler/pkg/modules/checks"
24+
"github.com/stackup-wallet/stackup-bundler/pkg/modules/paymaster"
25+
"github.com/stackup-wallet/stackup-bundler/pkg/signer"
26+
)
27+
28+
func SearcherMode() {
29+
conf := config.GetValues()
30+
31+
logr := logger.NewZeroLogr().
32+
WithName("stackup_bundler").
33+
WithValues("bundler_mode", "searcher")
34+
35+
eoa, err := signer.New(conf.PrivateKey)
36+
if err != nil {
37+
log.Fatal(err)
38+
}
39+
beneficiary := common.HexToAddress(conf.Beneficiary)
40+
41+
db, err := badger.Open(badger.DefaultOptions(conf.DataDirectory))
42+
if err != nil {
43+
log.Fatal(err)
44+
}
45+
defer db.Close()
46+
runDBGarbageCollection(db)
47+
48+
rpc, err := rpc.Dial(conf.EthClientUrl)
49+
if err != nil {
50+
log.Fatal(err)
51+
}
52+
53+
eth := ethclient.NewClient(rpc)
54+
55+
fb := flashbotsrpc.NewFlashbotsRPC(conf.EthBuilderUrl)
56+
57+
chain, err := eth.ChainID(context.Background())
58+
if err != nil {
59+
log.Fatal(err)
60+
}
61+
if !builder.CompatibleChainIDs.Contains(chain.Uint64()) {
62+
log.Fatalf(
63+
"error: network with chainID %d is not compatible with the Block Builder API.",
64+
chain.Uint64(),
65+
)
66+
}
67+
68+
mem, err := mempool.New(db)
69+
if err != nil {
70+
log.Fatal(err)
71+
}
72+
73+
check := checks.New(
74+
rpc,
75+
conf.MaxVerificationGas,
76+
conf.MaxOpsForUnstakedSender,
77+
conf.BundlerCollectorTracer,
78+
)
79+
// TODO: Create separate go-routine for tracking transactions sent to the block builder.
80+
builder := builder.New(eoa, eth, fb, beneficiary, conf.BlocksInTheFuture)
81+
paymaster := paymaster.New(db)
82+
83+
// Init Client
84+
c := client.New(mem, chain, conf.SupportedEntryPoints)
85+
c.SetGetUserOpReceiptFunc(client.GetUserOpReceiptWithEthClient(eth))
86+
c.SetGetSimulateValidationFunc(client.GetSimulateValidationWithRpcClient(rpc))
87+
c.SetGetCallGasEstimateFunc(client.GetCallGasEstimateWithEthClient(eth))
88+
c.SetGetUserOpByHashFunc(client.GetUserOpByHashWithEthClient(eth))
89+
c.UseLogger(logr)
90+
c.UseModules(
91+
check.ValidateOpValues(),
92+
paymaster.CheckStatus(),
93+
check.SimulateOp(),
94+
// TODO: add p2p propagation module
95+
paymaster.IncOpsSeen(),
96+
)
97+
98+
// Init Bundler
99+
b := bundler.New(mem, chain, conf.SupportedEntryPoints)
100+
b.UseLogger(logr)
101+
b.UseModules(
102+
check.PaymasterDeposit(),
103+
builder.SendUserOperation(),
104+
paymaster.IncOpsIncluded(),
105+
)
106+
if err := b.Run(); err != nil {
107+
log.Fatal(err)
108+
}
109+
110+
// init Debug
111+
var d *client.Debug
112+
if conf.DebugMode {
113+
d = client.NewDebug(eoa, eth, mem, b, chain, conf.SupportedEntryPoints[0], beneficiary)
114+
}
115+
116+
// Init HTTP server
117+
gin.SetMode(conf.GinMode)
118+
r := gin.New()
119+
if err := r.SetTrustedProxies(nil); err != nil {
120+
log.Fatal(err)
121+
}
122+
r.Use(
123+
cors.Default(),
124+
logger.WithLogr(logr),
125+
gin.Recovery(),
126+
)
127+
r.GET("/ping", func(g *gin.Context) {
128+
g.Status(http.StatusOK)
129+
})
130+
r.POST("/", jsonrpc.Controller(client.NewRpcAdapter(c, d)))
131+
if err := r.Run(fmt.Sprintf(":%d", conf.Port)); err != nil {
132+
log.Fatal(err)
133+
}
134+
}

0 commit comments

Comments
 (0)