Skip to content

Commit 7e2259f

Browse files
authored
Merge pull request #916 from onflow/mpeter/deduplicate-batched-txs
Deduplicate transactions on `BatchTxPool` prior to submission
2 parents b172ff9 + 1a66760 commit 7e2259f

3 files changed

Lines changed: 64 additions & 3 deletions

File tree

models/errors/errors.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,9 @@ var (
2828

2929
// Transaction errors
3030

31-
ErrFailedTransaction = errors.New("failed transaction")
32-
ErrInvalidTransaction = fmt.Errorf("%w: %w", ErrInvalid, ErrFailedTransaction)
31+
ErrFailedTransaction = errors.New("failed transaction")
32+
ErrInvalidTransaction = fmt.Errorf("%w: %w", ErrInvalid, ErrFailedTransaction)
33+
ErrDuplicateTransaction = fmt.Errorf("%w: %s", ErrInvalid, "transaction already in pool")
3334

3435
// Storage errors
3536

services/requester/batch_tx_pool.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package requester
33
import (
44
"context"
55
"encoding/hex"
6+
"slices"
67
"sort"
78
"sync"
89
"time"
@@ -17,13 +18,15 @@ import (
1718
"github.com/onflow/flow-evm-gateway/config"
1819
"github.com/onflow/flow-evm-gateway/metrics"
1920
"github.com/onflow/flow-evm-gateway/models"
21+
errs "github.com/onflow/flow-evm-gateway/models/errors"
2022
"github.com/onflow/flow-evm-gateway/services/requester/keystore"
2123
)
2224

2325
const eoaActivityCacheSize = 10_000
2426

2527
type pooledEvmTx struct {
2628
txPayload cadence.String
29+
txHash gethCommon.Hash
2730
nonce uint64
2831
}
2932

@@ -147,7 +150,11 @@ func (t *BatchTxPool) Add(
147150
err = t.submitSingleTransaction(ctx, hexEncodedTx)
148151
} else {
149152
// Case 3. EOA activity found AND it was less than [X] seconds ago:
150-
userTx := pooledEvmTx{txPayload: hexEncodedTx, nonce: tx.Nonce()}
153+
userTx := pooledEvmTx{txPayload: hexEncodedTx, txHash: tx.Hash(), nonce: tx.Nonce()}
154+
// Prevent submission of duplicate transactions, based on their tx hash
155+
if slices.Contains(t.pooledTxs[from], userTx) {
156+
return errs.ErrDuplicateTransaction
157+
}
151158
t.pooledTxs[from] = append(t.pooledTxs[from], userTx)
152159
}
153160

tests/tx_batching_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,59 @@ func Test_MultipleTransactionSubmissionsWithinNonRecentInterval(t *testing.T) {
514514
)
515515
}
516516

517+
func Test_MultipleTransactionSubmissionsWithDuplicates(t *testing.T) {
518+
_, cfg, stop := setupGatewayNode(t)
519+
defer stop()
520+
521+
rpcTester := &rpcTest{
522+
url: fmt.Sprintf("%s:%d", cfg.RPCHost, cfg.RPCPort),
523+
}
524+
525+
eoaKey, err := crypto.HexToECDSA(eoaTestPrivateKey)
526+
require.NoError(t, err)
527+
528+
testAddr := common.HexToAddress("55253ed90B70b96C73092D8680915aaF50081194")
529+
nonce := uint64(0)
530+
hashes := make([]common.Hash, 0)
531+
532+
signed, _, err := evmSign(big.NewInt(10), 21000, eoaKey, nonce, &testAddr, nil)
533+
require.NoError(t, err)
534+
535+
txHash, err := rpcTester.sendRawTx(signed)
536+
require.NoError(t, err)
537+
hashes = append(hashes, txHash)
538+
539+
// Increment nonce for the duplicate test transactions that follow
540+
nonce += 1
541+
dupSigned, _, err := evmSign(big.NewInt(10), 15_000_000, eoaKey, nonce, &testAddr, nil)
542+
require.NoError(t, err)
543+
544+
// Submit 5 identical transactions to test duplicate detection:
545+
// the first should succeed, the rest should be rejected as duplicates.
546+
for i := range 5 {
547+
if i == 0 {
548+
txHash, err := rpcTester.sendRawTx(dupSigned)
549+
require.NoError(t, err)
550+
hashes = append(hashes, txHash)
551+
} else {
552+
_, err := rpcTester.sendRawTx(dupSigned)
553+
require.Error(t, err)
554+
require.ErrorContains(t, err, "invalid: transaction already in pool")
555+
}
556+
}
557+
558+
assert.Eventually(t, func() bool {
559+
for _, h := range hashes {
560+
rcp, err := rpcTester.getReceipt(h.String())
561+
if err != nil || rcp == nil || rcp.Status != 1 {
562+
return false
563+
}
564+
}
565+
566+
return true
567+
}, time.Second*15, time.Second*1, "all transactions were not executed")
568+
}
569+
517570
func setupGatewayNode(t *testing.T) (emulator.Emulator, config.Config, func()) {
518571
srv, err := startEmulator(true)
519572
require.NoError(t, err)

0 commit comments

Comments
 (0)