3
3
package relay
4
4
5
5
import (
6
- "fmt"
7
6
"math/big"
8
- "net/http"
9
7
"time"
10
8
11
- "github.com/dgraph-io/badger/v3"
12
9
"github.com/ethereum/go-ethereum/common"
13
10
"github.com/ethereum/go-ethereum/ethclient"
14
- "github.com/gin-gonic/gin"
15
11
"github.com/go-logr/logr"
16
- "github.com/stackup-wallet/stackup-bundler/internal/ginutils"
17
12
"github.com/stackup-wallet/stackup-bundler/pkg/entrypoint/transaction"
18
13
"github.com/stackup-wallet/stackup-bundler/pkg/modules"
19
14
"github.com/stackup-wallet/stackup-bundler/pkg/signer"
20
- "github.com/stackup-wallet/stackup-bundler/pkg/userop"
21
15
)
22
16
23
17
// Relayer provides a module that can relay batches with a regular EOA. Relaying batches to the EntryPoint
24
18
// through a regular transaction comes with several important notes:
25
19
//
26
20
// - The bundler will NOT be operating as a block builder.
27
21
// - 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.
30
22
//
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.
38
26
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
48
33
}
49
34
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.
51
36
func New (
52
- db * badger.DB ,
53
37
eoa * signer.EOA ,
54
38
eth * ethclient.Client ,
55
39
chainID * big.Int ,
56
40
beneficiary common.Address ,
57
41
l logr.Logger ,
58
42
) * Relayer {
59
43
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 ,
69
50
}
70
51
}
71
52
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
-
85
53
// SetWaitTimeout sets the total time to wait for a transaction to be included. When a timeout is reached, the
86
54
// BatchHandler will throw an error if the transaction has not been included or has been included but with a
87
55
// failed status.
@@ -91,118 +59,10 @@ func (r *Relayer) SetWaitTimeout(timeout time.Duration) {
91
59
r .waitTimeout = timeout
92
60
}
93
61
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
-
198
62
// 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.
201
64
func (r * Relayer ) SendUserOperation () modules.BatchHandlerFunc {
202
65
return func (ctx * modules.BatchHandlerCtx ) error {
203
- // TODO: Increment badger nextTxnTs to read latest data from MapUserOpHashToClientID.
204
- time .Sleep (5 * time .Millisecond )
205
-
206
66
opts := transaction.Opts {
207
67
EOA : r .eoa ,
208
68
Eth : r .eth ,
@@ -216,63 +76,31 @@ func (r *Relayer) SendUserOperation() modules.BatchHandlerFunc {
216
76
GasLimit : 0 ,
217
77
WaitTimeout : r .waitTimeout ,
218
78
}
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 )
250
83
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
259
92
}
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
267
93
}
94
+ ctx .Data ["relayer_est_revert_reasons" ] = estRev
268
95
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
+ }
276
104
}
277
105
278
106
return nil
0 commit comments