Skip to content

Commit 6f1b87e

Browse files
committed
Add unit tests for ErrTxPoolFull and the flush failure/success merge branches
1 parent aafa055 commit 6f1b87e

2 files changed

Lines changed: 206 additions & 2 deletions

File tree

services/requester/batch_tx_pool.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,16 @@ type BatchTxPool struct {
9696
nonceProvider NonceProvider
9797
txQueues map[gethCommon.Address]*txQueue
9898
txQueuesMux sync.Mutex
99+
100+
// submitBatch exists as a field so tests can inject a fake. It returns the
101+
// ID of the wrapping Cadence transaction (zero on early build failure
102+
// before a Flow tx is signed) so logSubmission can record it, letting an
103+
// operator correlate a wedged EVM nonce to the specific Cadence tx.
104+
submitBatch func(
105+
ctx context.Context,
106+
block *flow.BlockHeader,
107+
txs []pooledEvmTx,
108+
) (flow.Identifier, error)
99109
}
100110

101111
// txQueue tracks the pooled transactions and submission state for one EOA.
@@ -287,6 +297,7 @@ func NewBatchTxPool(
287297
txQueues: make(map[gethCommon.Address]*txQueue),
288298
txQueuesMux: sync.Mutex{},
289299
}
300+
batchPool.submitBatch = batchPool.batchSubmitTransactionsForSameAddress
290301

291302
go batchPool.processPooledTransactions(ctx)
292303

@@ -462,7 +473,7 @@ func (t *BatchTxPool) processPooledTransactions(ctx context.Context) {
462473
t.txQueuesMux.Unlock()
463474

464475
for address, batch := range txBatchByAddress {
465-
flowTxID, err := t.batchSubmitTransactionsForSameAddress(
476+
flowTxID, err := t.submitBatch(
466477
ctx,
467478
t.getReferenceBlock(),
468479
batch.txs,
@@ -622,7 +633,7 @@ func (t *BatchTxPool) eoaEnqueueTxs(
622633
) (*txQueue, bool) {
623634
queue := t.eoaQueueEntry(address)
624635
if queue.retries >= maxSubmissionRetries {
625-
return nil, false
636+
return queue, false
626637
}
627638
for _, tx := range txs {
628639
if _, exists := queue.txs[tx.nonce]; exists {

services/requester/batch_tx_pool_test.go

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,24 @@
11
package requester
22

33
import (
4+
"context"
5+
"fmt"
46
"testing"
57
"time"
68

79
gethCommon "github.com/ethereum/go-ethereum/common"
10+
gethTypes "github.com/ethereum/go-ethereum/core/types"
11+
"github.com/ethereum/go-ethereum/crypto"
12+
"github.com/onflow/flow-evm-gateway/config"
13+
"github.com/onflow/flow-evm-gateway/metrics"
14+
"github.com/onflow/flow-evm-gateway/models"
15+
errs "github.com/onflow/flow-evm-gateway/models/errors"
16+
"github.com/onflow/flow-go-sdk"
17+
flowGo "github.com/onflow/flow-go/model/flow"
818
"github.com/rs/zerolog"
919
"github.com/stretchr/testify/assert"
20+
"github.com/stretchr/testify/require"
21+
"golang.org/x/sync/errgroup"
1022
)
1123

1224
func makePooledTx(nonce uint64) pooledEvmTx {
@@ -252,3 +264,184 @@ func Test_BatchTxPool_EnqueueFillsMissingNonces(t *testing.T) {
252264
assert.Equal(t, uint64(2), q.txs[2].nonce)
253265
assert.Equal(t, uint64(3), q.txs[3].nonce)
254266
}
267+
268+
// Test_BatchTxPool_ErrTxPoolFull asserts that `Add()` bounds the per-EOA
269+
// queue to a configured max number of entries.
270+
func Test_BatchTxPool_ErrTxPoolFull(t *testing.T) {
271+
pool := &BatchTxPool{
272+
SingleTxPool: &SingleTxPool{
273+
logger: zerolog.Nop(),
274+
txPublisher: models.NewPublisher[*gethTypes.Transaction](),
275+
collector: metrics.NopCollector,
276+
},
277+
nonceProvider: &fakeNonceProvider{nonce: 0},
278+
txQueues: map[gethCommon.Address]*txQueue{},
279+
}
280+
281+
key, err := crypto.GenerateKey()
282+
require.NoError(t, err)
283+
284+
for i := range maxEOAQueueSize {
285+
tx := signedTestTx(t, key, uint64(i+1), 1)
286+
require.NoError(t, pool.Add(context.Background(), tx))
287+
}
288+
289+
tx := signedTestTx(t, key, uint64(maxEOAQueueSize+1), 1)
290+
err = pool.Add(context.Background(), tx)
291+
require.Error(t, err)
292+
require.ErrorContains(t, err, errs.ErrTxPoolFull.Error())
293+
}
294+
295+
// Test_BatchTxPool_SubmissionFailureNonceReservationRollback asserts that
296+
// a submission failure on a batch will rollback any nonce reservation, while
297+
// guarding any advances made by a concurrent `Add()`.
298+
func Test_BatchTxPool_SubmissionFailureNonceReservationRollback(t *testing.T) {
299+
pool := &BatchTxPool{
300+
SingleTxPool: &SingleTxPool{
301+
logger: zerolog.Nop(),
302+
txPublisher: models.NewPublisher[*gethTypes.Transaction](),
303+
collector: metrics.NopCollector,
304+
config: config.Config{
305+
TxBatchMode: true,
306+
TxBatchInterval: time.Second,
307+
FlowNetworkID: flowGo.Emulator,
308+
},
309+
},
310+
nonceProvider: &fakeNonceProvider{nonce: 2},
311+
txQueues: map[gethCommon.Address]*txQueue{},
312+
}
313+
pool.submitBatch = func(
314+
ctx context.Context,
315+
block *flow.BlockHeader,
316+
txs []pooledEvmTx,
317+
) (flow.Identifier, error) {
318+
return flow.Identifier{}, fmt.Errorf("failed to submit batch Flow transaction")
319+
}
320+
321+
key, err := crypto.GenerateKey()
322+
require.NoError(t, err)
323+
addr := crypto.PubkeyToAddress(key.PublicKey)
324+
325+
pool.eoaEnqueueTxs(addr, []pooledEvmTx{
326+
makePooledTx(2),
327+
makePooledTx(3),
328+
makePooledTx(4),
329+
})
330+
331+
q := pool.eoaQueueEntry(addr)
332+
assert.Len(t, q.txs, 3)
333+
lastSubmittedAt := time.Now()
334+
lastSubmittedNonce := uint64(1)
335+
q.lastSubmittedAt = lastSubmittedAt
336+
q.lastSubmittedNonce = lastSubmittedNonce
337+
assert.Equal(t, uint64(2), q.txs[2].nonce)
338+
assert.Equal(t, uint64(3), q.txs[3].nonce)
339+
assert.Equal(t, uint64(4), q.txs[4].nonce)
340+
assert.Equal(t, lastSubmittedNonce, q.lastSubmittedNonce)
341+
342+
ctx := context.Background()
343+
submitCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
344+
345+
g := errgroup.Group{}
346+
nonce := uint64(5)
347+
g.Go(func() error {
348+
for {
349+
select {
350+
case <-submitCtx.Done():
351+
return nil
352+
default:
353+
tx := signedTestTx(t, key, nonce, 1)
354+
require.NoError(t, pool.Add(context.Background(), tx))
355+
nonce += 1
356+
time.Sleep(time.Millisecond * 500)
357+
}
358+
}
359+
})
360+
361+
pool.processPooledTransactions(submitCtx)
362+
363+
cancel()
364+
err = g.Wait()
365+
require.NoError(t, err)
366+
367+
assert.Equal(t, lastSubmittedAt, q.lastSubmittedAt)
368+
assert.Equal(t, lastSubmittedNonce, q.lastSubmittedNonce)
369+
}
370+
371+
// Test_BatchTxPool_SubmissionSuccessNonRegressionMerge asserts that on successful
372+
// batch submission, we perform non-regression merge, to account for any advances
373+
// by concurrent `Add()`.
374+
func Test_BatchTxPool_SubmissionSuccessNonRegressionMerge(t *testing.T) {
375+
pool := &BatchTxPool{
376+
SingleTxPool: &SingleTxPool{
377+
logger: zerolog.Nop(),
378+
txPublisher: models.NewPublisher[*gethTypes.Transaction](),
379+
collector: metrics.NopCollector,
380+
config: config.Config{
381+
TxBatchMode: true,
382+
TxBatchInterval: time.Second,
383+
FlowNetworkID: flowGo.Emulator,
384+
},
385+
},
386+
nonceProvider: &fakeNonceProvider{nonce: 2},
387+
txQueues: map[gethCommon.Address]*txQueue{},
388+
}
389+
pool.submitBatch = func(
390+
ctx context.Context,
391+
block *flow.BlockHeader,
392+
txs []pooledEvmTx,
393+
) (flow.Identifier, error) {
394+
return flow.HexToID(
395+
"2222222222222222222222222222222222222222222222222222222222222222",
396+
), nil
397+
}
398+
399+
key, err := crypto.GenerateKey()
400+
require.NoError(t, err)
401+
addr := crypto.PubkeyToAddress(key.PublicKey)
402+
403+
pool.eoaEnqueueTxs(addr, []pooledEvmTx{
404+
makePooledTx(2),
405+
makePooledTx(3),
406+
makePooledTx(4),
407+
})
408+
409+
q := pool.eoaQueueEntry(addr)
410+
assert.Len(t, q.txs, 3)
411+
lastSubmittedAt := time.Now()
412+
lastSubmittedNonce := uint64(1)
413+
q.lastSubmittedAt = lastSubmittedAt
414+
q.lastSubmittedNonce = lastSubmittedNonce
415+
assert.Equal(t, uint64(2), q.txs[2].nonce)
416+
assert.Equal(t, uint64(3), q.txs[3].nonce)
417+
assert.Equal(t, uint64(4), q.txs[4].nonce)
418+
assert.Equal(t, lastSubmittedNonce, q.lastSubmittedNonce)
419+
420+
ctx := context.Background()
421+
submitCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
422+
423+
g := errgroup.Group{}
424+
nonce := uint64(5)
425+
g.Go(func() error {
426+
for {
427+
select {
428+
case <-submitCtx.Done():
429+
return nil
430+
default:
431+
tx := signedTestTx(t, key, nonce, 1)
432+
require.NoError(t, pool.Add(context.Background(), tx))
433+
nonce += 1
434+
time.Sleep(time.Millisecond * 500)
435+
}
436+
}
437+
})
438+
439+
pool.processPooledTransactions(submitCtx)
440+
441+
cancel()
442+
err = g.Wait()
443+
require.NoError(t, err)
444+
445+
assert.Greater(t, q.lastSubmittedAt, lastSubmittedAt)
446+
assert.Greater(t, q.lastSubmittedNonce, lastSubmittedNonce)
447+
}

0 commit comments

Comments
 (0)