@@ -3,6 +3,8 @@ package requester
33import (
44 "context"
55 "encoding/hex"
6+ "fmt"
7+ "slices"
68 "sort"
79 "sync"
810 "time"
@@ -17,6 +19,7 @@ import (
1719 "github.com/onflow/flow-evm-gateway/config"
1820 "github.com/onflow/flow-evm-gateway/metrics"
1921 "github.com/onflow/flow-evm-gateway/models"
22+ errs "github.com/onflow/flow-evm-gateway/models/errors"
2023 "github.com/onflow/flow-evm-gateway/services/requester/keystore"
2124)
2225
@@ -30,6 +33,11 @@ type pooledEvmTx struct {
3033 nonce uint64
3134}
3235
36+ type eoaActivityMetadata struct {
37+ submittedAt time.Time
38+ txHashes []gethCommon.Hash
39+ }
40+
3341// BatchTxPool is a `TxPool` implementation that collects and groups
3442// transactions based on their EOA signer, and submits them for execution
3543// using a batch.
@@ -47,7 +55,7 @@ type BatchTxPool struct {
4755 * SingleTxPool
4856 pooledTxs map [gethCommon.Address ][]pooledEvmTx
4957 txMux sync.Mutex
50- eoaActivity * expirable.LRU [gethCommon.Address , time. Time ]
58+ eoaActivity * expirable.LRU [gethCommon.Address , eoaActivityMetadata ]
5159}
5260
5361var _ TxPool = & BatchTxPool {}
@@ -73,7 +81,7 @@ func NewBatchTxPool(
7381 keystore ,
7482 )
7583
76- eoaActivity := expirable .NewLRU [gethCommon.Address , time. Time ](
84+ eoaActivity := expirable .NewLRU [gethCommon.Address , eoaActivityMetadata ](
7785 eoaActivityCacheSize ,
7886 nil ,
7987 eoaActivityCacheTTL ,
@@ -109,7 +117,6 @@ func (t *BatchTxPool) Add(
109117 if err != nil {
110118 return err
111119 }
112-
113120 txData , err := tx .MarshalBinary ()
114121 if err != nil {
115122 return err
@@ -136,8 +143,21 @@ func (t *BatchTxPool) Add(
136143 // For all 3 cases, we record the activity time for the next
137144 // transactions that might come from the same EOA.
138145 // [X] is equal to the configured `TxBatchInterval` duration.
139- lastActivityTime , found := t .eoaActivity .Get (from )
140- if ! found || time .Since (lastActivityTime ) > t .config .TxBatchInterval {
146+ lastActivity , found := t .eoaActivity .Get (from )
147+ txHash := tx .Hash ()
148+
149+ // Reject transactions that have already been submitted,
150+ // as they are *bound* to fail. Two transactions with
151+ // identical hashes, are expected to have the exact same
152+ // payload. If the first one was invalid, with a nonce
153+ // mismatch, the second one is guaranteed to be invalid
154+ // as well. No reason in wasting signing keys in this
155+ // case.
156+ if found && slices .Contains (lastActivity .txHashes , txHash ) {
157+ return fmt .Errorf ("%w: a tx with hash %s has already been submitted" , errs .ErrInvalid , txHash )
158+ }
159+
160+ if ! found || time .Since (lastActivity .submittedAt ) > t .config .TxBatchInterval {
141161 if err := t .submitSingleTransaction (ctx , hexEncodedTx ); err != nil {
142162 return err
143163 }
@@ -146,7 +166,16 @@ func (t *BatchTxPool) Add(
146166 t .pooledTxs [from ] = append (t .pooledTxs [from ], userTx )
147167 }
148168
149- t .eoaActivity .Add (from , time .Now ())
169+ // Update metadata for the last EOA activity
170+ lastActivity .submittedAt = time .Now ()
171+ lastActivity .txHashes = append (lastActivity .txHashes , txHash )
172+ // To avoid the slice of hashes from growing indefinitely,
173+ // maintain only a handful of the last tx hashes.
174+ if len (lastActivity .txHashes ) > 10 {
175+ lastActivity .txHashes = lastActivity .txHashes [1 :]
176+ }
177+
178+ t .eoaActivity .Add (from , lastActivity )
150179
151180 return nil
152181}
0 commit comments