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

Commit 404ebbd

Browse files
authored
Gas price optimisations for timely inclusion (#194)
1 parent 55d142e commit 404ebbd

34 files changed

+941
-282
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ 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/google/uuid v1.3.0
1615
github.com/metachris/flashbotsrpc v0.5.0
1716
github.com/mitchellh/mapstructure v1.5.0
1817
github.com/rs/zerolog v1.29.0
@@ -47,6 +46,7 @@ require (
4746
github.com/golang/protobuf v1.5.2 // indirect
4847
github.com/golang/snappy v0.0.4 // indirect
4948
github.com/google/flatbuffers v1.12.1 // indirect
49+
github.com/google/uuid v1.3.0 // indirect
5050
github.com/gorilla/websocket v1.4.2 // indirect
5151
github.com/hashicorp/hcl v1.0.0 // indirect
5252
github.com/holiman/uint256 v1.2.0 // indirect

internal/start/private.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ import (
1919
"github.com/stackup-wallet/stackup-bundler/pkg/gas"
2020
"github.com/stackup-wallet/stackup-bundler/pkg/jsonrpc"
2121
"github.com/stackup-wallet/stackup-bundler/pkg/mempool"
22+
"github.com/stackup-wallet/stackup-bundler/pkg/modules/batch"
2223
"github.com/stackup-wallet/stackup-bundler/pkg/modules/checks"
24+
"github.com/stackup-wallet/stackup-bundler/pkg/modules/gasprice"
2325
"github.com/stackup-wallet/stackup-bundler/pkg/modules/paymaster"
2426
"github.com/stackup-wallet/stackup-bundler/pkg/modules/relay"
2527
"github.com/stackup-wallet/stackup-bundler/pkg/signer"
@@ -108,8 +110,13 @@ func PrivateMode() {
108110

109111
// Init Bundler
110112
b := bundler.New(mem, chain, conf.SupportedEntryPoints)
113+
b.SetGetBaseFeeFunc(gasprice.GetBaseFeeWithEthClient(eth))
114+
b.SetGetLegacyGasPriceFunc(gasprice.GetLegacyGasPriceWithEthClient(eth))
111115
b.UseLogger(logr)
112116
b.UseModules(
117+
gasprice.SortByGasPrice(),
118+
gasprice.FilterUnderpriced(),
119+
batch.SortByNonce(),
113120
check.CodeHashes(),
114121
check.PaymasterDeposit(),
115122
relayer.SendUserOperation(),
@@ -126,6 +133,7 @@ func PrivateMode() {
126133
d = client.NewDebug(eoa, eth, mem, b, chain, conf.SupportedEntryPoints[0], beneficiary)
127134
b.SetMaxBatch(1)
128135
relayer.SetBannedThreshold(relay.NoBanThreshold)
136+
relayer.SetWaitTimeout(0)
129137
}
130138

131139
// Init HTTP server

internal/start/searcher.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@ import (
2020
"github.com/stackup-wallet/stackup-bundler/pkg/gas"
2121
"github.com/stackup-wallet/stackup-bundler/pkg/jsonrpc"
2222
"github.com/stackup-wallet/stackup-bundler/pkg/mempool"
23+
"github.com/stackup-wallet/stackup-bundler/pkg/modules/batch"
2324
"github.com/stackup-wallet/stackup-bundler/pkg/modules/builder"
2425
"github.com/stackup-wallet/stackup-bundler/pkg/modules/checks"
26+
"github.com/stackup-wallet/stackup-bundler/pkg/modules/gasprice"
2527
"github.com/stackup-wallet/stackup-bundler/pkg/modules/paymaster"
2628
"github.com/stackup-wallet/stackup-bundler/pkg/signer"
2729
)
@@ -101,8 +103,13 @@ func SearcherMode() {
101103

102104
// Init Bundler
103105
b := bundler.New(mem, chain, conf.SupportedEntryPoints)
106+
b.SetGetBaseFeeFunc(gasprice.GetBaseFeeWithEthClient(eth))
107+
b.SetGetLegacyGasPriceFunc(gasprice.GetLegacyGasPriceWithEthClient(eth))
104108
b.UseLogger(logr)
105109
b.UseModules(
110+
gasprice.SortByGasPrice(),
111+
gasprice.FilterUnderpriced(),
112+
batch.SortByNonce(),
106113
check.CodeHashes(),
107114
check.PaymasterDeposit(),
108115
builder.SendUserOperation(),

internal/testutils/constants.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ var (
1313
DefaultUnstakeDelaySec = uint32(86400)
1414
ValidAddress1 = common.HexToAddress("0x7357b8a705328FC283dF72D7Ac546895B596DC12")
1515
ValidAddress2 = common.HexToAddress("0x7357c9504B8686c008CCcD6ea47f1c21B7475dE3")
16+
ValidAddress3 = common.HexToAddress("0x7357C8D931e8cde8ea1b777Cf8578f4A7071f100")
1617
ChainID = big.NewInt(1)
1718
MaxOpsForUnstakedSender = 1
1819
StakedDepositInfo = &entrypoint.IStakeManagerDepositInfo{

internal/testutils/opmock.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ var (
1919
"maxFeePerGas": "0xa862145e",
2020
"maxPriorityFeePerGas": "0xa8621440",
2121
"paymasterAndData": "0x",
22-
"preVerificationGas": "0xc650",
22+
"preVerificationGas": "0xc869",
2323
"signature": "0xa925dcc5e5131636e244d4405334c25f034ebdd85c0cb12e8cdb13c15249c2d466d0bade18e2cafd3513497f7f968dcbb63e519acd9b76dcae7acd61f11aa8421b",
2424
}
2525
MockByteCode = common.Hex2Bytes("6080604052")
Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1-
package gas
1+
package utils
22

33
import "math/big"
44

5-
func addBuffer(amt *big.Int, factor int64) *big.Int {
5+
func AddBuffer(amt *big.Int, factor int64) *big.Int {
6+
if amt == nil {
7+
return nil
8+
}
9+
610
a := big.NewInt(0).Mul(amt, big.NewInt(1000+(factor*10)))
711
return big.NewInt(0).Div(a, big.NewInt(1000))
812
}

internal/utils/buffer_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package utils_test
2+
3+
import (
4+
"math/big"
5+
"testing"
6+
7+
"github.com/stackup-wallet/stackup-bundler/internal/utils"
8+
)
9+
10+
// TestAddBuffer verifies that AddBuffer returns a new big.Int that is increased by a given percent value.
11+
func TestAddBuffer(t *testing.T) {
12+
factor := int64(10)
13+
amt := big.NewInt(10)
14+
want := big.NewInt(11)
15+
if utils.AddBuffer(amt, factor).Cmp(want) != 0 {
16+
t.Fatalf("got %d, want %d", utils.AddBuffer(amt, factor).Int64(), want.Int64())
17+
}
18+
}
19+
20+
// TestAddBufferZeroFactor verifies that AddBuffer returns a new big.Int that is unchanged when the factor is
21+
// 0.
22+
func TestAddBufferZeroFactor(t *testing.T) {
23+
factor := int64(0)
24+
amt := big.NewInt(100)
25+
if utils.AddBuffer(amt, factor).Cmp(amt) != 0 {
26+
t.Fatalf("got %d, want %d", utils.AddBuffer(amt, factor).Int64(), amt.Int64())
27+
}
28+
}
29+
30+
// TestAddBufferNilAmt verifies that AddBuffer returns nil if amt is nil.
31+
func TestAddBufferNilAmt(t *testing.T) {
32+
factor := int64(10)
33+
if utils.AddBuffer(nil, factor) != nil {
34+
t.Fatalf("got %d, want nil", utils.AddBuffer(nil, factor).Int64())
35+
}
36+
}

pkg/bundler/bundler.go

Lines changed: 48 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/stackup-wallet/stackup-bundler/internal/logger"
1111
"github.com/stackup-wallet/stackup-bundler/pkg/mempool"
1212
"github.com/stackup-wallet/stackup-bundler/pkg/modules"
13+
"github.com/stackup-wallet/stackup-bundler/pkg/modules/gasprice"
1314
"github.com/stackup-wallet/stackup-bundler/pkg/modules/noop"
1415
"github.com/stackup-wallet/stackup-bundler/pkg/userop"
1516
)
@@ -23,10 +24,11 @@ type Bundler struct {
2324
batchHandler modules.BatchHandlerFunc
2425
logger logr.Logger
2526
isRunning bool
26-
stop chan bool
27-
watch chan bool
28-
onStop func()
27+
done chan bool
28+
stop func()
2929
maxBatch int
30+
gbf gasprice.GetBaseFeeFunc
31+
ggp gasprice.GetLegacyGasPriceFunc
3032
}
3133

3234
// New initializes a new EIP-4337 bundler which can be extended with modules for validating batches and
@@ -39,10 +41,11 @@ func New(mempool *mempool.Mempool, chainID *big.Int, supportedEntryPoints []comm
3941
batchHandler: noop.BatchHandler,
4042
logger: logger.NewZeroLogr().WithName("bundler"),
4143
isRunning: false,
42-
stop: make(chan bool),
43-
watch: make(chan bool),
44-
onStop: func() {},
44+
done: make(chan bool),
45+
stop: func() {},
4546
maxBatch: 0,
47+
gbf: gasprice.NoopGetBaseFeeFunc(),
48+
ggp: gasprice.NoopGetLegacyGasPriceFunc(),
4649
}
4750
}
4851

@@ -51,6 +54,17 @@ func (i *Bundler) SetMaxBatch(max int) {
5154
i.maxBatch = max
5255
}
5356

57+
// SetGetBaseFeeFunc defines the function used to retrieve an estimate for basefee during each bundler run.
58+
func (i *Bundler) SetGetBaseFeeFunc(gbf gasprice.GetBaseFeeFunc) {
59+
i.gbf = gbf
60+
}
61+
62+
// SetGetLegacyGasPriceFunc defines the function used to retrieve an estimate for gas price during each
63+
// bundler run.
64+
func (i *Bundler) SetGetLegacyGasPriceFunc(ggp gasprice.GetLegacyGasPriceFunc) {
65+
i.ggp = ggp
66+
}
67+
5468
// UseLogger defines the logger object used by the Bundler instance based on the go-logr/logr interface.
5569
func (i *Bundler) UseLogger(logger logr.Logger) {
5670
i.logger = logger.WithName("bundler")
@@ -63,13 +77,30 @@ func (i *Bundler) UseModules(handlers ...modules.BatchHandlerFunc) {
6377

6478
// Process will create a batch from the mempool and send it through to the EntryPoint.
6579
func (i *Bundler) Process(ep common.Address) (*modules.BatchHandlerCtx, error) {
80+
// Init logger
6681
start := time.Now()
6782
l := i.logger.
6883
WithName("run").
6984
WithValues("entrypoint", ep.String()).
7085
WithValues("chain_id", i.chainID.String())
7186

72-
batch, err := i.mempool.BundleOps(ep)
87+
// Get current block basefee
88+
bf, err := i.gbf()
89+
if err != nil {
90+
l.Error(err, "bundler run error")
91+
return nil, err
92+
}
93+
94+
// Get suggested gas price (for networks that don't support EIP-1559)
95+
gp, err := i.ggp()
96+
if err != nil {
97+
l.Error(err, "bundler run error")
98+
return nil, err
99+
}
100+
101+
// Get all pending userOps from the mempool. This will be in FIFO order. Downstream modules should sort it
102+
// based on more specific strategies.
103+
batch, err := i.mempool.Dump(ep)
73104
if err != nil {
74105
l.Error(err, "bundler run error")
75106
return nil, err
@@ -79,19 +110,22 @@ func (i *Bundler) Process(ep common.Address) (*modules.BatchHandlerCtx, error) {
79110
}
80111
batch = adjustBatchSize(i.maxBatch, batch)
81112

82-
ctx := modules.NewBatchHandlerContext(batch, ep, i.chainID)
113+
// Create context and execute modules.
114+
ctx := modules.NewBatchHandlerContext(batch, ep, i.chainID, bf, gp)
83115
if err := i.batchHandler(ctx); err != nil {
84116
l.Error(err, "bundler run error")
85117
return nil, err
86118
}
87119

120+
// Remove userOps that remain in the context from mempool.
88121
rmOps := append([]*userop.UserOperation{}, ctx.Batch...)
89122
rmOps = append(rmOps, ctx.PendingRemoval...)
90123
if err := i.mempool.RemoveOps(ep, rmOps...); err != nil {
91124
l.Error(err, "bundler run error")
92125
return nil, err
93126
}
94127

128+
// Update logs for the current run.
95129
bat := []string{}
96130
for _, op := range ctx.Batch {
97131
bat = append(bat, op.GetUserOpHash(ep, i.chainID).String())
@@ -118,12 +152,13 @@ func (i *Bundler) Run() error {
118152
return nil
119153
}
120154

155+
ticker := time.NewTicker(1 * time.Second)
121156
go func(i *Bundler) {
122157
for {
123158
select {
124-
case <-i.stop:
159+
case <-i.done:
125160
return
126-
case <-i.watch:
161+
case <-ticker.C:
127162
for _, ep := range i.supportedEntryPoints {
128163
_, err := i.Process(ep)
129164
if err != nil {
@@ -136,7 +171,7 @@ func (i *Bundler) Run() error {
136171
}(i)
137172

138173
i.isRunning = true
139-
i.onStop = i.mempool.OnAdd(i.watch)
174+
i.stop = ticker.Stop
140175
return nil
141176
}
142177

@@ -147,6 +182,6 @@ func (i *Bundler) Stop() {
147182
}
148183

149184
i.isRunning = false
150-
i.stop <- true
151-
i.onStop()
185+
i.stop()
186+
i.done <- true
152187
}

pkg/entrypoint/transaction/gas.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package transaction
2+
3+
import (
4+
"context"
5+
"math/big"
6+
7+
"github.com/ethereum/go-ethereum/common"
8+
"github.com/ethereum/go-ethereum/ethclient"
9+
"github.com/stackup-wallet/stackup-bundler/pkg/userop"
10+
)
11+
12+
// SuggestMeanGasTipCap suggests a Max Priority Fee for an EIP-1559 transaction to submit a batch of
13+
// UserOperations to the EntryPoint. It returns the larger value between the gas tip suggested by the
14+
// underlying node (i.e. eth_maxPriorityFeePerGas) or the average maxPriorityFeePerGas of the entire batch.
15+
func SuggestMeanGasTipCap(eth *ethclient.Client, batch []*userop.UserOperation) (*big.Int, error) {
16+
tip, err := eth.SuggestGasTipCap(context.Background())
17+
if err != nil {
18+
return nil, err
19+
}
20+
21+
sum := big.NewInt(0)
22+
for _, op := range batch {
23+
sum = big.NewInt(0).Add(sum, op.MaxPriorityFeePerGas)
24+
}
25+
avg := big.NewInt(0).Div(sum, big.NewInt(int64(len(batch))))
26+
27+
if avg.Cmp(tip) == 1 {
28+
return avg, nil
29+
}
30+
return tip, nil
31+
}
32+
33+
// SuggestMeanGasFeeCap suggests a Max Fee for an EIP-1559 transaction to submit a batch of UserOperations to
34+
// the EntryPoint. It returns the larger value between the recommended max fee or the average maxFeePerGas of
35+
// the entire batch.
36+
func SuggestMeanGasFeeCap(basefee *big.Int, batch []*userop.UserOperation) *big.Int {
37+
mf := big.NewInt(0).Mul(basefee, common.Big2)
38+
39+
sum := big.NewInt(0)
40+
for _, op := range batch {
41+
sum = big.NewInt(0).Add(sum, op.MaxFeePerGas)
42+
}
43+
avg := big.NewInt(0).Div(sum, big.NewInt(int64(len(batch))))
44+
45+
if avg.Cmp(mf) == 1 {
46+
return avg
47+
}
48+
return mf
49+
}
50+
51+
// SuggestMeanGasPrice suggests a Gas Price for a legacy transaction to submit a batch of UserOperations to
52+
// the EntryPoint. It returns the larger value between a given gas price or the average maxFeePerGas of the
53+
// entire batch.
54+
func SuggestMeanGasPrice(gasPrice *big.Int, batch []*userop.UserOperation) *big.Int {
55+
sum := big.NewInt(0)
56+
for _, op := range batch {
57+
sum = big.NewInt(0).Add(sum, op.MaxFeePerGas)
58+
}
59+
avg := big.NewInt(0).Div(sum, big.NewInt(int64(len(batch))))
60+
61+
if avg.Cmp(gasPrice) == 1 {
62+
return avg
63+
}
64+
return gasPrice
65+
}

0 commit comments

Comments
 (0)