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

Commit

Permalink
feat: Support alternative mempools (#336)
Browse files Browse the repository at this point in the history
  • Loading branch information
hazim-j authored Nov 6, 2023
1 parent 3c6c046 commit 6a17959
Show file tree
Hide file tree
Showing 16 changed files with 564 additions and 39 deletions.
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ require (
github.com/google/go-cmp v0.5.9
github.com/metachris/flashbotsrpc v0.5.0
github.com/mitchellh/mapstructure v1.5.0
github.com/puzpuzpuz/xsync/v3 v3.0.1
github.com/rs/zerolog v1.29.0
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1
github.com/spf13/cobra v1.6.1
github.com/spf13/viper v1.15.0
github.com/wangjia184/sortedset v0.0.0-20220209072355-af6d6d227aa7
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,8 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:
github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
github.com/prometheus/common v0.39.0 h1:oOyhkDq05hPZKItWVBkJ6g6AtGxi+fy7F4JvUV8uhsI=
github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
github.com/puzpuzpuz/xsync/v3 v3.0.1 h1:yhTYnDJlgIYp/3Bb14b43VfUPrk/QNJ1HrLYEZ8r2AE=
github.com/puzpuzpuz/xsync/v3 v3.0.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
Expand All @@ -342,6 +344,8 @@ github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/rwtodd/Go.Sed v0.0.0-20210816025313-55464686f9ef/go.mod h1:8AEUvGVi2uQ5b24BIhcr0GCcpd/RNAFWaN2CJFrWIIQ=
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4=
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY=
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU=
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
Expand Down
23 changes: 23 additions & 0 deletions internal/config/values.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ type Values struct {
OTELCollectorUrl string
OTELInsecureMode bool

// Alternative mempool variables.
AltMempoolIPFSGateway string
AltMempoolIds []string

// Undocumented variables.
DebugMode bool
GinMode string
Expand Down Expand Up @@ -63,6 +67,13 @@ func envArrayToAddressSlice(s string) []common.Address {
return slc
}

func envArrayToStringSlice(s string) []string {
if s == "" {
return []string{}
}
return strings.Split(s, ",")
}

func variableNotSetOrIsNil(env string) bool {
return !viper.IsSet(env) || viper.GetString(env) == ""
}
Expand Down Expand Up @@ -115,6 +126,8 @@ func GetValues() *Values {
_ = viper.BindEnv("erc4337_bundler_otel_collector_headers")
_ = viper.BindEnv("erc4337_bundler_otel_collector_url")
_ = viper.BindEnv("erc4337_bundler_otel_insecure_mode")
_ = viper.BindEnv("erc4337_bundler_alt_mempool_ipfs_gateway")
_ = viper.BindEnv("erc4337_bundler_alt_mempool_ids")
_ = viper.BindEnv("erc4337_bundler_debug_mode")
_ = viper.BindEnv("erc4337_bundler_gin_mode")

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

// Validate Alternative mempool variables
if viper.IsSet("erc4337_bundler_alt_mempool_ids") &&
variableNotSetOrIsNil("erc4337_bundler_alt_mempool_ipfs_gateway") {
panic("Fatal config error: erc4337_bundler_alt_mempool_ids is set without specifying an IPFS gateway")
}

// Return Values
privateKey := viper.GetString("erc4337_bundler_private_key")
ethClientUrl := viper.GetString("erc4337_bundler_eth_client_url")
Expand All @@ -166,6 +185,8 @@ func GetValues() *Values {
otelCollectorHeader := envKeyValStringToMap(viper.GetString("erc4337_bundler_otel_collector_headers"))
otelCollectorUrl := viper.GetString("erc4337_bundler_otel_collector_url")
otelInsecureMode := viper.GetBool("erc4337_bundler_otel_insecure_mode")
altMempoolIPFSGateway := viper.GetString("erc4337_bundler_alt_mempool_ipfs_gateway")
altMempoolIds := envArrayToStringSlice(viper.GetString("erc4337_bundler_alt_mempool_ids"))
debugMode := viper.GetBool("erc4337_bundler_debug_mode")
ginMode := viper.GetString("erc4337_bundler_gin_mode")
return &Values{
Expand All @@ -186,6 +207,8 @@ func GetValues() *Values {
OTELCollectorHeaders: otelCollectorHeader,
OTELCollectorUrl: otelCollectorUrl,
OTELInsecureMode: otelInsecureMode,
AltMempoolIPFSGateway: altMempoolIPFSGateway,
AltMempoolIds: altMempoolIds,
DebugMode: debugMode,
GinMode: ginMode,
}
Expand Down
7 changes: 7 additions & 0 deletions internal/start/private.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/stackup-wallet/stackup-bundler/internal/config"
"github.com/stackup-wallet/stackup-bundler/internal/logger"
"github.com/stackup-wallet/stackup-bundler/internal/o11y"
"github.com/stackup-wallet/stackup-bundler/pkg/altmempools"
"github.com/stackup-wallet/stackup-bundler/pkg/bundler"
"github.com/stackup-wallet/stackup-bundler/pkg/client"
"github.com/stackup-wallet/stackup-bundler/pkg/gas"
Expand Down Expand Up @@ -105,10 +106,16 @@ func PrivateMode() {
log.Fatal(err)
}

alt, err := altmempools.NewFromIPFS(chain, conf.AltMempoolIPFSGateway, conf.AltMempoolIds)
if err != nil {
log.Fatal(err)
}

check := checks.New(
db,
rpc,
ov,
alt,
conf.MaxVerificationGas,
conf.MaxBatchGasLimit,
conf.MaxOpsForUnstakedSender,
Expand Down
7 changes: 7 additions & 0 deletions internal/start/searcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/stackup-wallet/stackup-bundler/internal/config"
"github.com/stackup-wallet/stackup-bundler/internal/logger"
"github.com/stackup-wallet/stackup-bundler/internal/o11y"
"github.com/stackup-wallet/stackup-bundler/pkg/altmempools"
"github.com/stackup-wallet/stackup-bundler/pkg/bundler"
"github.com/stackup-wallet/stackup-bundler/pkg/client"
"github.com/stackup-wallet/stackup-bundler/pkg/gas"
Expand Down Expand Up @@ -97,10 +98,16 @@ func SearcherMode() {
log.Fatal(err)
}

alt, err := altmempools.NewFromIPFS(chain, conf.AltMempoolIPFSGateway, conf.AltMempoolIds)
if err != nil {
log.Fatal(err)
}

check := checks.New(
db,
rpc,
ov,
alt,
conf.MaxVerificationGas,
conf.MaxBatchGasLimit,
conf.MaxOpsForUnstakedSender,
Expand Down
38 changes: 38 additions & 0 deletions internal/testutils/altmempoolmock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package testutils

import "github.com/ethereum/go-ethereum/common/hexutil"

func AltMempoolMock() map[string]any {
return map[string]any{
"description": "Mock Alt Mempool",
"chainIds": []any{hexutil.EncodeBig(ChainID)},
"allowlist": []any{
map[string]any{
"description": "Mock forbiddenOpcode rule",
"rule": "forbiddenOpcode",
"entity": "account",
"contract": "0x0000000000000000000000000000000000000000",
"opcode": "GAS",
},
map[string]any{
"description": "Mock forbiddenPrecompile rule",
"rule": "forbiddenPrecompile",
"entity": "account",
"contract": "0x0000000000000000000000000000000000000000",
"precompile": "0x0000000000000000000000000000000000000000",
},
map[string]any{
"description": "Mock invalidStorageAccess rule",
"rule": "invalidStorageAccess",
"entity": "account",
"contract": "0x0000000000000000000000000000000000000000",
"slot": "0x0000000000000000000000000000000000000000",
},
map[string]any{
"description": "Mock notStaked rule",
"rule": "notStaked",
"entity": "0x0000000000000000000000000000000000000000",
},
},
}
}
107 changes: 107 additions & 0 deletions pkg/altmempools/directory.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package altmempools

import (
"encoding/json"
"math/big"
"net/http"

"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/puzpuzpuz/xsync/v3"
)

// Directory maintains a collection of alternative mempool configurations. It allows a consumer to check if a
// known alternative mempool exists that will allow specific exceptions that the canonical mempool cannot
// accept.
type Directory struct {
invalidStorageAccess *xsync.MapOf[string, []string]
}

type Config struct {
Id string
Data map[string]any
}

func invalidStorageAccessID(entity string, contract string, slot string) string {
return entity + contract + slot
}

func fetchMempoolConfig(url string) (map[string]any, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()

var data map[string]any
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
return nil, err
}
return data, nil
}

// New accepts an array of alternative mempool configs and returns a Directory.
func New(chain *big.Int, altMempools []*Config) (*Directory, error) {
dir := &Directory{
invalidStorageAccess: xsync.NewMapOf[string, []string](),
}
for _, alt := range altMempools {
if err := Schema.Validate(alt.Data); err != nil {
return nil, err
}

skip := true
for _, item := range alt.Data["chainIds"].([]any) {
allowed, err := hexutil.DecodeBig(item.(string))
if err != nil {
return nil, err
}

if chain.Cmp(allowed) == 0 {
skip = false
}
}
if skip {
continue
}

for _, item := range alt.Data["allowlist"].([]any) {
config := item.(map[string]any)
switch config["rule"].(string) {
case "invalidStorageAccess":
{
isaId := invalidStorageAccessID(
config["entity"].(string),
config["contract"].(string),
config["slot"].(string),
)
curr, _ := dir.invalidStorageAccess.Load(isaId)
dir.invalidStorageAccess.Store(isaId, append(curr, alt.Id))
}
}
}
}

return dir, nil
}

// NewFromIPFS will pull alternative mempool configs from IPFS and returns a Directory. The mempool id is
// equal to an IPFS CID.
func NewFromIPFS(chain *big.Int, ipfsGateway string, ids []string) (*Directory, error) {
var alts []*Config
for _, id := range ids {
data, err := fetchMempoolConfig(ipfsGateway + "/" + id)
if err != nil {
return nil, err
}
alts = append(alts, &Config{id, data})
}

return New(chain, alts)
}

// HasInvalidStorageAccessException will attempt to find all mempools ids that will accept the given invalid
// storage access pattern and return it. If none is found, an empty array will be returned.
func (d *Directory) HasInvalidStorageAccessException(entity string, contract string, slot string) []string {
ids, _ := d.invalidStorageAccess.Load(invalidStorageAccessID(entity, contract, slot))
return ids
}
90 changes: 90 additions & 0 deletions pkg/altmempools/directory_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package altmempools_test

import (
"math/big"
"testing"

"github.com/stackup-wallet/stackup-bundler/internal/testutils"
"github.com/stackup-wallet/stackup-bundler/pkg/altmempools"
)

func TestDirectoryHasSingleInvalidStorageAccessException(t *testing.T) {
id := "1"
alts := []*altmempools.Config{
{Id: id, Data: testutils.AltMempoolMock()},
}
dir, err := altmempools.New(testutils.ChainID, alts)
if err != nil {
t.Fatal("error initializing directory")
}

mempools := dir.HasInvalidStorageAccessException(
"account",
"0x0000000000000000000000000000000000000000",
"0x0000000000000000000000000000000000000000",
)
if len(mempools) != 1 || mempools[0] != id {
t.Fatalf("got %v, want [1]", mempools)
}
}

func TestDirectoryHasManyInvalidStorageAccessExceptions(t *testing.T) {
id1 := "1"
id2 := "2"
alts := []*altmempools.Config{
{Id: id1, Data: testutils.AltMempoolMock()},
{Id: id2, Data: testutils.AltMempoolMock()},
}
dir, err := altmempools.New(testutils.ChainID, alts)
if err != nil {
t.Fatal("error initializing directory")
}

mempools := dir.HasInvalidStorageAccessException(
"account",
"0x0000000000000000000000000000000000000000",
"0x0000000000000000000000000000000000000000",
)
if len(mempools) != 2 || mempools[0] != id1 && mempools[1] != id2 {
t.Fatalf("got %v, want [1 2]", mempools)
}
}

func TestDirectoryHasNoInvalidStorageAccessExceptions(t *testing.T) {
id := "1"
alts := []*altmempools.Config{
{Id: id, Data: testutils.AltMempoolMock()},
}
dir, err := altmempools.New(testutils.ChainID, alts)
if err != nil {
t.Fatal("error initializing directory")
}

mempools := dir.HasInvalidStorageAccessException(
"paymaster",
"0x0000000000000000000000000000000000000000",
"0x0000000000000000000000000000000000000000",
)
if len(mempools) != 0 {
t.Fatalf("got %v, want []", mempools)
}
}

func TestDirectoryIncompatibleChain(t *testing.T) {
alts := []*altmempools.Config{
{Id: "1", Data: testutils.AltMempoolMock()},
}
dir, err := altmempools.New(big.NewInt(2), alts)
if err != nil {
t.Fatal("error initializing directory")
}

mempools := dir.HasInvalidStorageAccessException(
"account",
"0x0000000000000000000000000000000000000000",
"0x0000000000000000000000000000000000000000",
)
if len(mempools) != 0 {
t.Fatalf("got %v, want []", mempools)
}
}
5 changes: 5 additions & 0 deletions pkg/altmempools/docs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Package altmempool provides functions to pull an alternative mempool config from an IPFS gateway and
// validate it against a schema.
//
// Schema originally written by @dancoombs: https://hackmd.io/@dancoombs/BJYRz3h8n.
package altmempools
Loading

0 comments on commit 6a17959

Please sign in to comment.