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

Commit 6a17959

Browse files
authored
feat: Support alternative mempools (#336)
1 parent 3c6c046 commit 6a17959

File tree

16 files changed

+564
-39
lines changed

16 files changed

+564
-39
lines changed

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ require (
1414
github.com/google/go-cmp v0.5.9
1515
github.com/metachris/flashbotsrpc v0.5.0
1616
github.com/mitchellh/mapstructure v1.5.0
17+
github.com/puzpuzpuz/xsync/v3 v3.0.1
1718
github.com/rs/zerolog v1.29.0
19+
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1
1820
github.com/spf13/cobra v1.6.1
1921
github.com/spf13/viper v1.15.0
2022
github.com/wangjia184/sortedset v0.0.0-20220209072355-af6d6d227aa7

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,8 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:
328328
github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
329329
github.com/prometheus/common v0.39.0 h1:oOyhkDq05hPZKItWVBkJ6g6AtGxi+fy7F4JvUV8uhsI=
330330
github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
331+
github.com/puzpuzpuz/xsync/v3 v3.0.1 h1:yhTYnDJlgIYp/3Bb14b43VfUPrk/QNJ1HrLYEZ8r2AE=
332+
github.com/puzpuzpuz/xsync/v3 v3.0.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
331333
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
332334
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
333335
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
@@ -342,6 +344,8 @@ github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR
342344
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
343345
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
344346
github.com/rwtodd/Go.Sed v0.0.0-20210816025313-55464686f9ef/go.mod h1:8AEUvGVi2uQ5b24BIhcr0GCcpd/RNAFWaN2CJFrWIIQ=
347+
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4=
348+
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY=
345349
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU=
346350
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
347351
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=

internal/config/values.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ type Values struct {
3636
OTELCollectorUrl string
3737
OTELInsecureMode bool
3838

39+
// Alternative mempool variables.
40+
AltMempoolIPFSGateway string
41+
AltMempoolIds []string
42+
3943
// Undocumented variables.
4044
DebugMode bool
4145
GinMode string
@@ -63,6 +67,13 @@ func envArrayToAddressSlice(s string) []common.Address {
6367
return slc
6468
}
6569

70+
func envArrayToStringSlice(s string) []string {
71+
if s == "" {
72+
return []string{}
73+
}
74+
return strings.Split(s, ",")
75+
}
76+
6677
func variableNotSetOrIsNil(env string) bool {
6778
return !viper.IsSet(env) || viper.GetString(env) == ""
6879
}
@@ -115,6 +126,8 @@ func GetValues() *Values {
115126
_ = viper.BindEnv("erc4337_bundler_otel_collector_headers")
116127
_ = viper.BindEnv("erc4337_bundler_otel_collector_url")
117128
_ = viper.BindEnv("erc4337_bundler_otel_insecure_mode")
129+
_ = viper.BindEnv("erc4337_bundler_alt_mempool_ipfs_gateway")
130+
_ = viper.BindEnv("erc4337_bundler_alt_mempool_ids")
118131
_ = viper.BindEnv("erc4337_bundler_debug_mode")
119132
_ = viper.BindEnv("erc4337_bundler_gin_mode")
120133

@@ -148,6 +161,12 @@ func GetValues() *Values {
148161
panic("Fatal config error: erc4337_bundler_otel_service_name is set without a collector URL")
149162
}
150163

164+
// Validate Alternative mempool variables
165+
if viper.IsSet("erc4337_bundler_alt_mempool_ids") &&
166+
variableNotSetOrIsNil("erc4337_bundler_alt_mempool_ipfs_gateway") {
167+
panic("Fatal config error: erc4337_bundler_alt_mempool_ids is set without specifying an IPFS gateway")
168+
}
169+
151170
// Return Values
152171
privateKey := viper.GetString("erc4337_bundler_private_key")
153172
ethClientUrl := viper.GetString("erc4337_bundler_eth_client_url")
@@ -166,6 +185,8 @@ func GetValues() *Values {
166185
otelCollectorHeader := envKeyValStringToMap(viper.GetString("erc4337_bundler_otel_collector_headers"))
167186
otelCollectorUrl := viper.GetString("erc4337_bundler_otel_collector_url")
168187
otelInsecureMode := viper.GetBool("erc4337_bundler_otel_insecure_mode")
188+
altMempoolIPFSGateway := viper.GetString("erc4337_bundler_alt_mempool_ipfs_gateway")
189+
altMempoolIds := envArrayToStringSlice(viper.GetString("erc4337_bundler_alt_mempool_ids"))
169190
debugMode := viper.GetBool("erc4337_bundler_debug_mode")
170191
ginMode := viper.GetString("erc4337_bundler_gin_mode")
171192
return &Values{
@@ -186,6 +207,8 @@ func GetValues() *Values {
186207
OTELCollectorHeaders: otelCollectorHeader,
187208
OTELCollectorUrl: otelCollectorUrl,
188209
OTELInsecureMode: otelInsecureMode,
210+
AltMempoolIPFSGateway: altMempoolIPFSGateway,
211+
AltMempoolIds: altMempoolIds,
189212
DebugMode: debugMode,
190213
GinMode: ginMode,
191214
}

internal/start/private.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/stackup-wallet/stackup-bundler/internal/config"
1616
"github.com/stackup-wallet/stackup-bundler/internal/logger"
1717
"github.com/stackup-wallet/stackup-bundler/internal/o11y"
18+
"github.com/stackup-wallet/stackup-bundler/pkg/altmempools"
1819
"github.com/stackup-wallet/stackup-bundler/pkg/bundler"
1920
"github.com/stackup-wallet/stackup-bundler/pkg/client"
2021
"github.com/stackup-wallet/stackup-bundler/pkg/gas"
@@ -105,10 +106,16 @@ func PrivateMode() {
105106
log.Fatal(err)
106107
}
107108

109+
alt, err := altmempools.NewFromIPFS(chain, conf.AltMempoolIPFSGateway, conf.AltMempoolIds)
110+
if err != nil {
111+
log.Fatal(err)
112+
}
113+
108114
check := checks.New(
109115
db,
110116
rpc,
111117
ov,
118+
alt,
112119
conf.MaxVerificationGas,
113120
conf.MaxBatchGasLimit,
114121
conf.MaxOpsForUnstakedSender,

internal/start/searcher.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/stackup-wallet/stackup-bundler/internal/config"
1717
"github.com/stackup-wallet/stackup-bundler/internal/logger"
1818
"github.com/stackup-wallet/stackup-bundler/internal/o11y"
19+
"github.com/stackup-wallet/stackup-bundler/pkg/altmempools"
1920
"github.com/stackup-wallet/stackup-bundler/pkg/bundler"
2021
"github.com/stackup-wallet/stackup-bundler/pkg/client"
2122
"github.com/stackup-wallet/stackup-bundler/pkg/gas"
@@ -97,10 +98,16 @@ func SearcherMode() {
9798
log.Fatal(err)
9899
}
99100

101+
alt, err := altmempools.NewFromIPFS(chain, conf.AltMempoolIPFSGateway, conf.AltMempoolIds)
102+
if err != nil {
103+
log.Fatal(err)
104+
}
105+
100106
check := checks.New(
101107
db,
102108
rpc,
103109
ov,
110+
alt,
104111
conf.MaxVerificationGas,
105112
conf.MaxBatchGasLimit,
106113
conf.MaxOpsForUnstakedSender,

internal/testutils/altmempoolmock.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package testutils
2+
3+
import "github.com/ethereum/go-ethereum/common/hexutil"
4+
5+
func AltMempoolMock() map[string]any {
6+
return map[string]any{
7+
"description": "Mock Alt Mempool",
8+
"chainIds": []any{hexutil.EncodeBig(ChainID)},
9+
"allowlist": []any{
10+
map[string]any{
11+
"description": "Mock forbiddenOpcode rule",
12+
"rule": "forbiddenOpcode",
13+
"entity": "account",
14+
"contract": "0x0000000000000000000000000000000000000000",
15+
"opcode": "GAS",
16+
},
17+
map[string]any{
18+
"description": "Mock forbiddenPrecompile rule",
19+
"rule": "forbiddenPrecompile",
20+
"entity": "account",
21+
"contract": "0x0000000000000000000000000000000000000000",
22+
"precompile": "0x0000000000000000000000000000000000000000",
23+
},
24+
map[string]any{
25+
"description": "Mock invalidStorageAccess rule",
26+
"rule": "invalidStorageAccess",
27+
"entity": "account",
28+
"contract": "0x0000000000000000000000000000000000000000",
29+
"slot": "0x0000000000000000000000000000000000000000",
30+
},
31+
map[string]any{
32+
"description": "Mock notStaked rule",
33+
"rule": "notStaked",
34+
"entity": "0x0000000000000000000000000000000000000000",
35+
},
36+
},
37+
}
38+
}

pkg/altmempools/directory.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package altmempools
2+
3+
import (
4+
"encoding/json"
5+
"math/big"
6+
"net/http"
7+
8+
"github.com/ethereum/go-ethereum/common/hexutil"
9+
"github.com/puzpuzpuz/xsync/v3"
10+
)
11+
12+
// Directory maintains a collection of alternative mempool configurations. It allows a consumer to check if a
13+
// known alternative mempool exists that will allow specific exceptions that the canonical mempool cannot
14+
// accept.
15+
type Directory struct {
16+
invalidStorageAccess *xsync.MapOf[string, []string]
17+
}
18+
19+
type Config struct {
20+
Id string
21+
Data map[string]any
22+
}
23+
24+
func invalidStorageAccessID(entity string, contract string, slot string) string {
25+
return entity + contract + slot
26+
}
27+
28+
func fetchMempoolConfig(url string) (map[string]any, error) {
29+
resp, err := http.Get(url)
30+
if err != nil {
31+
return nil, err
32+
}
33+
defer resp.Body.Close()
34+
35+
var data map[string]any
36+
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
37+
return nil, err
38+
}
39+
return data, nil
40+
}
41+
42+
// New accepts an array of alternative mempool configs and returns a Directory.
43+
func New(chain *big.Int, altMempools []*Config) (*Directory, error) {
44+
dir := &Directory{
45+
invalidStorageAccess: xsync.NewMapOf[string, []string](),
46+
}
47+
for _, alt := range altMempools {
48+
if err := Schema.Validate(alt.Data); err != nil {
49+
return nil, err
50+
}
51+
52+
skip := true
53+
for _, item := range alt.Data["chainIds"].([]any) {
54+
allowed, err := hexutil.DecodeBig(item.(string))
55+
if err != nil {
56+
return nil, err
57+
}
58+
59+
if chain.Cmp(allowed) == 0 {
60+
skip = false
61+
}
62+
}
63+
if skip {
64+
continue
65+
}
66+
67+
for _, item := range alt.Data["allowlist"].([]any) {
68+
config := item.(map[string]any)
69+
switch config["rule"].(string) {
70+
case "invalidStorageAccess":
71+
{
72+
isaId := invalidStorageAccessID(
73+
config["entity"].(string),
74+
config["contract"].(string),
75+
config["slot"].(string),
76+
)
77+
curr, _ := dir.invalidStorageAccess.Load(isaId)
78+
dir.invalidStorageAccess.Store(isaId, append(curr, alt.Id))
79+
}
80+
}
81+
}
82+
}
83+
84+
return dir, nil
85+
}
86+
87+
// NewFromIPFS will pull alternative mempool configs from IPFS and returns a Directory. The mempool id is
88+
// equal to an IPFS CID.
89+
func NewFromIPFS(chain *big.Int, ipfsGateway string, ids []string) (*Directory, error) {
90+
var alts []*Config
91+
for _, id := range ids {
92+
data, err := fetchMempoolConfig(ipfsGateway + "/" + id)
93+
if err != nil {
94+
return nil, err
95+
}
96+
alts = append(alts, &Config{id, data})
97+
}
98+
99+
return New(chain, alts)
100+
}
101+
102+
// HasInvalidStorageAccessException will attempt to find all mempools ids that will accept the given invalid
103+
// storage access pattern and return it. If none is found, an empty array will be returned.
104+
func (d *Directory) HasInvalidStorageAccessException(entity string, contract string, slot string) []string {
105+
ids, _ := d.invalidStorageAccess.Load(invalidStorageAccessID(entity, contract, slot))
106+
return ids
107+
}

pkg/altmempools/directory_test.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package altmempools_test
2+
3+
import (
4+
"math/big"
5+
"testing"
6+
7+
"github.com/stackup-wallet/stackup-bundler/internal/testutils"
8+
"github.com/stackup-wallet/stackup-bundler/pkg/altmempools"
9+
)
10+
11+
func TestDirectoryHasSingleInvalidStorageAccessException(t *testing.T) {
12+
id := "1"
13+
alts := []*altmempools.Config{
14+
{Id: id, Data: testutils.AltMempoolMock()},
15+
}
16+
dir, err := altmempools.New(testutils.ChainID, alts)
17+
if err != nil {
18+
t.Fatal("error initializing directory")
19+
}
20+
21+
mempools := dir.HasInvalidStorageAccessException(
22+
"account",
23+
"0x0000000000000000000000000000000000000000",
24+
"0x0000000000000000000000000000000000000000",
25+
)
26+
if len(mempools) != 1 || mempools[0] != id {
27+
t.Fatalf("got %v, want [1]", mempools)
28+
}
29+
}
30+
31+
func TestDirectoryHasManyInvalidStorageAccessExceptions(t *testing.T) {
32+
id1 := "1"
33+
id2 := "2"
34+
alts := []*altmempools.Config{
35+
{Id: id1, Data: testutils.AltMempoolMock()},
36+
{Id: id2, Data: testutils.AltMempoolMock()},
37+
}
38+
dir, err := altmempools.New(testutils.ChainID, alts)
39+
if err != nil {
40+
t.Fatal("error initializing directory")
41+
}
42+
43+
mempools := dir.HasInvalidStorageAccessException(
44+
"account",
45+
"0x0000000000000000000000000000000000000000",
46+
"0x0000000000000000000000000000000000000000",
47+
)
48+
if len(mempools) != 2 || mempools[0] != id1 && mempools[1] != id2 {
49+
t.Fatalf("got %v, want [1 2]", mempools)
50+
}
51+
}
52+
53+
func TestDirectoryHasNoInvalidStorageAccessExceptions(t *testing.T) {
54+
id := "1"
55+
alts := []*altmempools.Config{
56+
{Id: id, Data: testutils.AltMempoolMock()},
57+
}
58+
dir, err := altmempools.New(testutils.ChainID, alts)
59+
if err != nil {
60+
t.Fatal("error initializing directory")
61+
}
62+
63+
mempools := dir.HasInvalidStorageAccessException(
64+
"paymaster",
65+
"0x0000000000000000000000000000000000000000",
66+
"0x0000000000000000000000000000000000000000",
67+
)
68+
if len(mempools) != 0 {
69+
t.Fatalf("got %v, want []", mempools)
70+
}
71+
}
72+
73+
func TestDirectoryIncompatibleChain(t *testing.T) {
74+
alts := []*altmempools.Config{
75+
{Id: "1", Data: testutils.AltMempoolMock()},
76+
}
77+
dir, err := altmempools.New(big.NewInt(2), alts)
78+
if err != nil {
79+
t.Fatal("error initializing directory")
80+
}
81+
82+
mempools := dir.HasInvalidStorageAccessException(
83+
"account",
84+
"0x0000000000000000000000000000000000000000",
85+
"0x0000000000000000000000000000000000000000",
86+
)
87+
if len(mempools) != 0 {
88+
t.Fatalf("got %v, want []", mempools)
89+
}
90+
}

pkg/altmempools/docs.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// Package altmempool provides functions to pull an alternative mempool config from an IPFS gateway and
2+
// validate it against a schema.
3+
//
4+
// Schema originally written by @dancoombs: https://hackmd.io/@dancoombs/BJYRz3h8n.
5+
package altmempools

0 commit comments

Comments
 (0)