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

Commit c78316b

Browse files
authored
Remove IP throttling from relayer (#244)
1 parent 77da1eb commit c78316b

File tree

4 files changed

+40
-361
lines changed

4 files changed

+40
-361
lines changed

internal/config/values.go

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"fmt"
55
"math/big"
66
"strings"
7-
"time"
87

98
"github.com/ethereum/go-ethereum/common"
109
"github.com/gin-gonic/gin"
@@ -24,10 +23,6 @@ type Values struct {
2423
MaxOpsForUnstakedSender int
2524
Beneficiary string
2625

27-
// Private mode variables.
28-
RelayerBannedThreshold int
29-
RelayerBannedTimeWindow time.Duration
30-
3126
// Searcher mode variables.
3227
EthBuilderUrl string
3328
BlocksInTheFuture int
@@ -107,8 +102,6 @@ func GetValues() *Values {
107102
_ = viper.BindEnv("erc4337_bundler_max_verification_gas")
108103
_ = viper.BindEnv("erc4337_bundler_max_batch_gas_limit")
109104
_ = viper.BindEnv("erc4337_bundler_max_ops_for_unstaked_sender")
110-
_ = viper.BindEnv("erc4337_bundler_relayer_banned_threshold")
111-
_ = viper.BindEnv("erc4337_bundler_relayer_banned_time_window")
112105
_ = viper.BindEnv("erc4337_bundler_eth_builder_url")
113106
_ = viper.BindEnv("erc4337_bundler_blocks_in_the_future")
114107
_ = viper.BindEnv("erc4337_bundler_otel_service_name")
@@ -158,8 +151,6 @@ func GetValues() *Values {
158151
maxVerificationGas := big.NewInt(int64(viper.GetInt("erc4337_bundler_max_verification_gas")))
159152
maxBatchGasLimit := big.NewInt(int64(viper.GetInt("erc4337_bundler_max_batch_gas_limit")))
160153
maxOpsForUnstakedSender := viper.GetInt("erc4337_bundler_max_ops_for_unstaked_sender")
161-
relayerBannedThreshold := viper.GetInt("erc4337_bundler_relayer_banned_threshold")
162-
relayerBannedTimeWindow := viper.GetInt("erc4337_bundler_relayer_banned_time_window") * int(time.Second)
163154
ethBuilderUrl := viper.GetString("erc4337_bundler_eth_builder_url")
164155
blocksInTheFuture := viper.GetInt("erc4337_bundler_blocks_in_the_future")
165156
otelServiceName := viper.GetString("erc4337_bundler_otel_service_name")
@@ -178,8 +169,6 @@ func GetValues() *Values {
178169
MaxVerificationGas: maxVerificationGas,
179170
MaxBatchGasLimit: maxBatchGasLimit,
180171
MaxOpsForUnstakedSender: maxOpsForUnstakedSender,
181-
RelayerBannedThreshold: relayerBannedThreshold,
182-
RelayerBannedTimeWindow: time.Duration(relayerBannedTimeWindow),
183172
EthBuilderUrl: ethBuilderUrl,
184173
BlocksInTheFuture: blocksInTheFuture,
185174
OTELServiceName: otelServiceName,

internal/start/private.go

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -107,13 +107,7 @@ func PrivateMode() {
107107
conf.MaxOpsForUnstakedSender,
108108
)
109109

110-
relayer := relay.New(db, eoa, eth, chain, beneficiary, logr)
111-
if conf.RelayerBannedThreshold > 0 {
112-
relayer.SetBannedThreshold(conf.RelayerBannedThreshold)
113-
}
114-
if conf.RelayerBannedTimeWindow > 0 {
115-
relayer.SetBannedTimeWindow(conf.RelayerBannedTimeWindow)
116-
}
110+
relayer := relay.New(eoa, eth, chain, beneficiary, logr)
117111

118112
paymaster := paymaster.New(db)
119113

@@ -159,7 +153,6 @@ func PrivateMode() {
159153
if conf.DebugMode {
160154
d = client.NewDebug(eoa, eth, mem, b, chain, conf.SupportedEntryPoints[0], beneficiary)
161155
b.SetMaxBatch(1)
162-
relayer.SetBannedThreshold(relay.NoBanThreshold)
163156
relayer.SetWaitTimeout(0)
164157
}
165158

@@ -181,10 +174,8 @@ func PrivateMode() {
181174
g.Status(http.StatusOK)
182175
})
183176
handlers := []gin.HandlerFunc{
184-
relayer.FilterByClientID(),
185177
jsonrpc.Controller(client.NewRpcAdapter(c, d)),
186178
jsonrpc.WithOTELTracerAttributes(),
187-
relayer.MapUserOpHashToClientID(),
188179
}
189180
r.POST("/", handlers...)
190181
r.POST("/rpc", handlers...)

pkg/modules/relay/relayer.go

Lines changed: 38 additions & 210 deletions
Original file line numberDiff line numberDiff line change
@@ -3,85 +3,53 @@
33
package relay
44

55
import (
6-
"fmt"
76
"math/big"
8-
"net/http"
97
"time"
108

11-
"github.com/dgraph-io/badger/v3"
129
"github.com/ethereum/go-ethereum/common"
1310
"github.com/ethereum/go-ethereum/ethclient"
14-
"github.com/gin-gonic/gin"
1511
"github.com/go-logr/logr"
16-
"github.com/stackup-wallet/stackup-bundler/internal/ginutils"
1712
"github.com/stackup-wallet/stackup-bundler/pkg/entrypoint/transaction"
1813
"github.com/stackup-wallet/stackup-bundler/pkg/modules"
1914
"github.com/stackup-wallet/stackup-bundler/pkg/signer"
20-
"github.com/stackup-wallet/stackup-bundler/pkg/userop"
2115
)
2216

2317
// Relayer provides a module that can relay batches with a regular EOA. Relaying batches to the EntryPoint
2418
// through a regular transaction comes with several important notes:
2519
//
2620
// - The bundler will NOT be operating as a block builder.
2721
// - This opens the bundler up to frontrunning.
28-
// - In a naive solution, attackers can send a valid op and frontrun the batch to make that op invalid.
29-
// - This invalidates the entire batch and the bundler will have to pay for the failed transaction.
3022
//
31-
// In this case, the mitigation strategy is to throttle the sender by a unique identifier or IP address.
32-
// If a sender submits a UserOperation that causes the batch to revert, then its ID is banned from sending
33-
// anymore ops to the mempool. This is optimistic in the sense that it will not prevent every case but will
34-
// mitigate malicious senders spamming the mempool.
35-
//
36-
// This will only work in the case of a private mempool and will not work in the P2P case where ops are
37-
// propagated through the network and it is impossible to trust a sender's identifier.
23+
// This module only works in the case of a private mempool and will not work in the P2P case where ops are
24+
// propagated through the network and it is impossible to prevent collisions from multiple bundlers trying to
25+
// relay the same ops.
3826
type Relayer struct {
39-
db *badger.DB
40-
eoa *signer.EOA
41-
eth *ethclient.Client
42-
chainID *big.Int
43-
beneficiary common.Address
44-
logger logr.Logger
45-
bannedThreshold int
46-
bannedTimeWindow time.Duration
47-
waitTimeout time.Duration
27+
eoa *signer.EOA
28+
eth *ethclient.Client
29+
chainID *big.Int
30+
beneficiary common.Address
31+
logger logr.Logger
32+
waitTimeout time.Duration
4833
}
4934

50-
// New initializes a new EOA relayer for sending batches to the EntryPoint with IP throttling protection.
35+
// New initializes a new EOA relayer for sending batches to the EntryPoint.
5136
func New(
52-
db *badger.DB,
5337
eoa *signer.EOA,
5438
eth *ethclient.Client,
5539
chainID *big.Int,
5640
beneficiary common.Address,
5741
l logr.Logger,
5842
) *Relayer {
5943
return &Relayer{
60-
db: db,
61-
eoa: eoa,
62-
eth: eth,
63-
chainID: chainID,
64-
beneficiary: beneficiary,
65-
logger: l.WithName("relayer"),
66-
bannedThreshold: DefaultBanThreshold,
67-
bannedTimeWindow: DefaultBanTimeWindow,
68-
waitTimeout: DefaultWaitTimeout,
44+
eoa: eoa,
45+
eth: eth,
46+
chainID: chainID,
47+
beneficiary: beneficiary,
48+
logger: l.WithName("relayer"),
49+
waitTimeout: DefaultWaitTimeout,
6950
}
7051
}
7152

72-
// SetBannedThreshold sets the limit for how many ops can be seen from a client without being included in a
73-
// batch before it is banned. Default value is 3. A value of 0 will effectively disable client banning, which
74-
// is useful for debugging.
75-
func (r *Relayer) SetBannedThreshold(limit int) {
76-
r.bannedThreshold = limit
77-
}
78-
79-
// SetBannedTimeWindow sets the limit for how long a banned client will be rejected for. The default value is
80-
// 24 hours.
81-
func (r *Relayer) SetBannedTimeWindow(limit time.Duration) {
82-
r.bannedTimeWindow = limit
83-
}
84-
8553
// SetWaitTimeout sets the total time to wait for a transaction to be included. When a timeout is reached, the
8654
// BatchHandler will throw an error if the transaction has not been included or has been included but with a
8755
// failed status.
@@ -91,118 +59,10 @@ func (r *Relayer) SetWaitTimeout(timeout time.Duration) {
9159
r.waitTimeout = timeout
9260
}
9361

94-
// FilterByClientID is a custom Gin middleware used to prevent requests from banned clients from adding their
95-
// userOps to the mempool. Identifiers are prioritized by the following values:
96-
// 1. X-Forwarded-By header: The first IP address in the array which is assumed to be the client
97-
// 2. Request.RemoteAddr: The remote IP address
98-
//
99-
// This should be the first middleware on the RPC path.
100-
func (r *Relayer) FilterByClientID() gin.HandlerFunc {
101-
return func(c *gin.Context) {
102-
l := r.logger.WithName("filter_by_client")
103-
104-
isBanned := false
105-
var os, oi int
106-
cid := ginutils.GetClientIPFromXFF(c)
107-
err := r.db.View(func(txn *badger.Txn) error {
108-
opsSeen, opsIncluded, err := getOpsCountByClientID(txn, cid)
109-
if err != nil {
110-
return err
111-
}
112-
l = l.
113-
WithValues("client_id", cid).
114-
WithValues("opsSeen", opsSeen).
115-
WithValues("opsIncluded", opsIncluded)
116-
117-
OpsFailed := opsSeen - opsIncluded
118-
if r.bannedThreshold == NoBanThreshold || OpsFailed < r.bannedThreshold {
119-
return nil
120-
}
121-
122-
isBanned = true
123-
os = opsSeen
124-
oi = opsIncluded
125-
return nil
126-
})
127-
if err != nil {
128-
l.Error(err, "filter_by_client failed")
129-
c.Status(http.StatusInternalServerError)
130-
c.Abort()
131-
}
132-
133-
if isBanned {
134-
l.Info("client banned")
135-
c.Status(http.StatusForbidden)
136-
c.JSON(
137-
http.StatusForbidden,
138-
gin.H{
139-
"error": fmt.Sprintf(
140-
"opsSeen (%d) exceeds opsIncluded (%d) by allowed threshold (%d). Wait %s to retry.",
141-
os,
142-
oi,
143-
r.bannedThreshold,
144-
r.bannedTimeWindow,
145-
),
146-
},
147-
)
148-
c.Abort()
149-
} else {
150-
l.Info("client ok")
151-
}
152-
}
153-
}
154-
155-
// MapUserOpHashToClientID is a custom Gin middleware used to map a userOpHash to a clientID. This
156-
// should be placed after the main method call on the RPC path.
157-
func (r *Relayer) MapUserOpHashToClientID() gin.HandlerFunc {
158-
return func(c *gin.Context) {
159-
l := r.logger.WithName("map_userop_hash_to_client_id")
160-
161-
req, _ := c.Get("json-rpc-request")
162-
json := req.(map[string]any)
163-
if json["method"] != "eth_sendUserOperation" {
164-
return
165-
}
166-
167-
params := json["params"].([]any)
168-
data := params[0].(map[string]any)
169-
ep := params[1].(string)
170-
op, err := userop.New(data)
171-
if err != nil {
172-
l.Error(err, "map_userop_hash_to_client_id failed")
173-
c.Status(http.StatusInternalServerError)
174-
return
175-
}
176-
177-
hash := op.GetUserOpHash(common.HexToAddress(ep), r.chainID).String()
178-
cid := ginutils.GetClientIPFromXFF(c)
179-
l = l.
180-
WithValues("userop_hash", hash).
181-
WithValues("client_id", cid)
182-
err = r.db.Update(func(txn *badger.Txn) error {
183-
err := mapUserOpHashToClientID(txn, hash, cid)
184-
if err != nil {
185-
return err
186-
}
187-
188-
return incrementOpsSeenByClientID(txn, cid, r.bannedTimeWindow)
189-
})
190-
if err != nil {
191-
l.Error(err, "map_userop_hash_to_client_id failed")
192-
c.Status(http.StatusInternalServerError)
193-
return
194-
}
195-
}
196-
}
197-
19862
// SendUserOperation returns a BatchHandler that is used by the Bundler to send batches in a regular EOA
199-
// transaction. It uses the mapping of userOpHash to client ID created by the Gin middleware in order to
200-
// mitigate DoS attacks.
63+
// transaction.
20164
func (r *Relayer) SendUserOperation() modules.BatchHandlerFunc {
20265
return func(ctx *modules.BatchHandlerCtx) error {
203-
// TODO: Increment badger nextTxnTs to read latest data from MapUserOpHashToClientID.
204-
time.Sleep(5 * time.Millisecond)
205-
20666
opts := transaction.Opts{
20767
EOA: r.eoa,
20868
Eth: r.eth,
@@ -216,63 +76,31 @@ func (r *Relayer) SendUserOperation() modules.BatchHandlerFunc {
21676
GasLimit: 0,
21777
WaitTimeout: r.waitTimeout,
21878
}
219-
var del []string
220-
err := r.db.Update(func(txn *badger.Txn) error {
221-
// Delete any userOpHash entries from dropped userOps.
222-
if len(ctx.PendingRemoval) > 0 {
223-
hashes := getUserOpHashesFromOps(ctx.EntryPoint, ctx.ChainID, ctx.PendingRemoval...)
224-
if err := removeUserOpHashEntries(txn, hashes...); err != nil {
225-
return err
226-
}
227-
}
228-
229-
// Estimate gas for handleOps() and drop all userOps that cause unexpected reverts.
230-
estRev := []string{}
231-
for len(ctx.Batch) > 0 {
232-
est, revert, err := transaction.EstimateHandleOpsGas(&opts)
233-
234-
if err != nil {
235-
return err
236-
} else if revert != nil {
237-
ctx.MarkOpIndexForRemoval(revert.OpIndex)
238-
estRev = append(estRev, revert.Reason)
239-
240-
hashes := getUserOpHashesFromOps(ctx.EntryPoint, ctx.ChainID, ctx.PendingRemoval...)
241-
if err := removeUserOpHashEntries(txn, hashes...); err != nil {
242-
return err
243-
}
244-
} else {
245-
opts.GasLimit = est
246-
break
247-
}
248-
}
249-
ctx.Data["relayer_est_revert_reasons"] = estRev
79+
// Estimate gas for handleOps() and drop all userOps that cause unexpected reverts.
80+
estRev := []string{}
81+
for len(ctx.Batch) > 0 {
82+
est, revert, err := transaction.EstimateHandleOpsGas(&opts)
25083

251-
// Call handleOps() with gas estimate. Any userOps that cause a revert at this stage will be
252-
// caught and dropped in the next iteration.
253-
if len(ctx.Batch) > 0 {
254-
if txn, err := transaction.HandleOps(&opts); err != nil {
255-
return err
256-
} else {
257-
ctx.Data["txn_hash"] = txn.Hash().String()
258-
}
84+
if err != nil {
85+
return err
86+
} else if revert != nil {
87+
ctx.MarkOpIndexForRemoval(revert.OpIndex)
88+
estRev = append(estRev, revert.Reason)
89+
} else {
90+
opts.GasLimit = est
91+
break
25992
}
260-
261-
hashes := getUserOpHashesFromOps(ctx.EntryPoint, ctx.ChainID, ctx.Batch...)
262-
del = append([]string{}, hashes...)
263-
return incrementOpsIncludedByUserOpHashes(txn, r.bannedTimeWindow, hashes...)
264-
})
265-
if err != nil {
266-
return err
26793
}
94+
ctx.Data["relayer_est_revert_reasons"] = estRev
26895

269-
// Delete remaining userOpHash entries from submitted userOps.
270-
// Perform update in new txn to avoid db conflicts.
271-
err = r.db.Update(func(txn *badger.Txn) error {
272-
return removeUserOpHashEntries(txn, del...)
273-
})
274-
if err != nil {
275-
return err
96+
// Call handleOps() with gas estimate. Any userOps that cause a revert at this stage will be
97+
// caught and dropped in the next iteration.
98+
if len(ctx.Batch) > 0 {
99+
if txn, err := transaction.HandleOps(&opts); err != nil {
100+
return err
101+
} else {
102+
ctx.Data["txn_hash"] = txn.Hash().String()
103+
}
276104
}
277105

278106
return nil

0 commit comments

Comments
 (0)