33package relay
44
55import (
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.
3826type 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.
5136func 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.
20164func (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