@@ -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,24 @@ 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.
153+ // - If the 1st was invalid, with a nonce mismatch, the 2nd
154+ // is guaranteed to be invalid as well.
155+ // - If the 1st was valid, the 2nd is also guaranteed to be
156+ // invalid with a nonce mismatch.
157+ // In both cases, there is no reason to wasting any signing
158+ // keys.
159+ if found && slices .Contains (lastActivity .txHashes , txHash ) {
160+ return fmt .Errorf ("%w: a tx with hash %s has already been submitted" , errs .ErrInvalid , txHash )
161+ }
162+
163+ if ! found || time .Since (lastActivity .submittedAt ) > t .config .TxBatchInterval {
141164 if err := t .submitSingleTransaction (ctx , hexEncodedTx ); err != nil {
142165 return err
143166 }
@@ -146,7 +169,16 @@ func (t *BatchTxPool) Add(
146169 t .pooledTxs [from ] = append (t .pooledTxs [from ], userTx )
147170 }
148171
149- t .eoaActivity .Add (from , time .Now ())
172+ // Update metadata for the last EOA activity
173+ lastActivity .submittedAt = time .Now ()
174+ lastActivity .txHashes = append (lastActivity .txHashes , txHash )
175+ // To avoid the slice of hashes from growing indefinitely,
176+ // maintain only a handful of the last tx hashes.
177+ if len (lastActivity .txHashes ) > 10 {
178+ lastActivity .txHashes = lastActivity .txHashes [1 :]
179+ }
180+
181+ t .eoaActivity .Add (from , lastActivity )
150182
151183 return nil
152184}
0 commit comments