Skip to content

Commit 86a8dac

Browse files
committed
Deduplicate transactions on BatchTxPool prior to submission
1 parent 404c556 commit 86a8dac

File tree

3 files changed

+66
-3
lines changed

3 files changed

+66
-3
lines changed

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: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,61 @@ 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+
nonce += 1
535+
536+
txHash, err := rpcTester.sendRawTx(signed)
537+
require.NoError(t, err)
538+
hashes = append(hashes, txHash)
539+
540+
for i := range 5 {
541+
// All these transactions are duplicates, since we don't change any
542+
// of the payload data. These will end up having the same tx hash
543+
// as well.
544+
signed, _, err := evmSign(big.NewInt(10), 15_000_000, eoaKey, nonce, &testAddr, nil)
545+
require.NoError(t, err)
546+
547+
// only the first transaction is valid, the rest 4 are duplicates
548+
// of the 1st one.
549+
if i == 0 {
550+
txHash, err := rpcTester.sendRawTx(signed)
551+
require.NoError(t, err)
552+
hashes = append(hashes, txHash)
553+
} else {
554+
_, err := rpcTester.sendRawTx(signed)
555+
require.Error(t, err)
556+
require.ErrorContains(t, err, "invalid: transaction already in pool")
557+
}
558+
}
559+
560+
assert.Eventually(t, func() bool {
561+
for _, h := range hashes {
562+
rcp, err := rpcTester.getReceipt(h.String())
563+
if err != nil || rcp == nil || rcp.Status != 1 {
564+
return false
565+
}
566+
}
567+
568+
return true
569+
}, time.Second*15, time.Second*1, "all transactions were not executed")
570+
}
571+
517572
func setupGatewayNode(t *testing.T) (emulator.Emulator, config.Config, func()) {
518573
srv, err := startEmulator(true)
519574
require.NoError(t, err)

0 commit comments

Comments
 (0)