diff --git a/block/base/default.go b/block/base/default.go index cb3275f3..b2372bdd 100644 --- a/block/base/default.go +++ b/block/base/default.go @@ -1,10 +1,14 @@ package base import ( + "container/list" "context" + "fmt" sdk "github.com/cosmos/cosmos-sdk/types" sdkmempool "github.com/cosmos/cosmos-sdk/types/mempool" + + signer_extraction "github.com/skip-mev/block-sdk/v2/adapters/signer_extraction_adapter" ) var ( @@ -12,85 +16,143 @@ var ( _ sdkmempool.Iterator = (*DefaultIterator)(nil) ) -// DefaultMempool implements a simple mempool that stores all transactions +// DefaultMempool implements a FIFO mempool with duplicate detection based on sender:nonce. +// It uses a linked list for FIFO ordering and a map for O(1) duplicate detection. type DefaultMempool[C comparable] struct { - txs []sdk.Tx - MaxTx int + txs *list.List // Linked list for FIFO ordering + seen map[string]*list.Element // Map from sender:nonce to list element + MaxTx int // Maximum number of transactions + signerExtractor signer_extraction.Adapter // For extracting signer info } -// NewDefaultMempool creates a new DefaultMempool -func NewDefaultMempool[C comparable](maxTxs int) *DefaultMempool[C] { +// NewDefaultMempool creates a new FIFO mempool with duplicate detection +func NewDefaultMempool[C comparable](maxTxs int, signerExtractor signer_extraction.Adapter) *DefaultMempool[C] { return &DefaultMempool[C]{ - txs: make([]sdk.Tx, 0), - MaxTx: maxTxs, + txs: list.New(), + seen: make(map[string]*list.Element), + MaxTx: maxTxs, + signerExtractor: signerExtractor, + } +} + +// getTxKey creates a unique key from sender:nonce combination +func (mp *DefaultMempool[C]) getTxKey(tx sdk.Tx) (string, error) { + signers, err := mp.signerExtractor.GetSigners(tx) + if err != nil { + return "", err } + if len(signers) == 0 { + return "", fmt.Errorf("tx must have at least one signer") + } + + sig := signers[0] + nonce := sig.Sequence + sender := sig.Signer.String() + + return fmt.Sprintf("%s:%d", sender, nonce), nil } // Insert implements MempoolInterface. func (mp *DefaultMempool[C]) Insert(_ context.Context, tx sdk.Tx) error { + if mp.MaxTx < 0 { + return nil // No-op if MaxTx is negative + } + + key, err := mp.getTxKey(tx) + if err != nil { + return fmt.Errorf("failed to get tx key for insertion: %w", err) + } + + // Check if this tx already exists + if _, exists := mp.seen[key]; exists { + // No-op: transaction with same sender:nonce already exists + return nil + } + + // Check capacity before adding new transaction if mp.MaxTx > 0 && mp.CountTx() >= mp.MaxTx { return sdkmempool.ErrMempoolTxMaxCapacity - } else if mp.MaxTx < 0 { - return nil } - mp.txs = append(mp.txs, tx) + + // Add new transaction + element := mp.txs.PushBack(tx) + mp.seen[key] = element + return nil } // Remove implements MempoolInterface. func (mp *DefaultMempool[C]) Remove(tx sdk.Tx) error { - for i, t := range mp.txs { - if t == tx { - mp.txs = append(mp.txs[:i], mp.txs[i+1:]...) - return nil - } + key, err := mp.getTxKey(tx) + if err != nil { + return fmt.Errorf("failed to get tx key for removal: %w", err) + } + + // Remove by key + if element, exists := mp.seen[key]; exists { + mp.txs.Remove(element) + delete(mp.seen, key) } + return nil } // Select implements MempoolInterface. func (mp *DefaultMempool[C]) Select(_ context.Context, _ [][]byte) sdkmempool.Iterator { - if len(mp.txs) == 0 { + if mp.txs.Len() == 0 { return nil } return &DefaultIterator{ - txs: mp.txs, - curr: 0, + current: mp.txs.Front(), } } // CountTx implements MempoolInterface. func (mp *DefaultMempool[C]) CountTx() int { - return len(mp.txs) + return mp.txs.Len() } // Contains implements MempoolInterface. func (mp *DefaultMempool[C]) Contains(tx sdk.Tx) bool { - for _, t := range mp.txs { - if t == tx { - return true - } + key, err := mp.getTxKey(tx) + if err != nil { + return false + } + + // Check if we have this sender:nonce combination + if element, exists := mp.seen[key]; exists { + // Return true only if it's the exact same transaction object + return element.Value.(sdk.Tx) == tx } + return false } -// DefaultIterator implements sdkmempool.Iterator +// DefaultIterator implements sdkmempool.Iterator for FIFO mempool type DefaultIterator struct { - txs []sdk.Tx - curr int + current *list.Element } // Next implements sdkmempool.Iterator func (i *DefaultIterator) Next() sdkmempool.Iterator { - if i.curr >= len(i.txs)-1 { + if i.current == nil { return nil } - i.curr++ + + i.current = i.current.Next() + if i.current == nil { + return nil + } + return i } // Tx implements sdkmempool.Iterator func (i *DefaultIterator) Tx() sdk.Tx { - return i.txs[i.curr] + if i.current == nil { + return nil + } + + return i.current.Value.(sdk.Tx) } diff --git a/block/base/default_test.go b/block/base/default_test.go index 8bb9dd9c..f159fb89 100644 --- a/block/base/default_test.go +++ b/block/base/default_test.go @@ -5,51 +5,25 @@ import ( "math/rand" "testing" - sdkmath "cosmossdk.io/math" + "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" sdkmempool "github.com/cosmos/cosmos-sdk/types/mempool" "github.com/stretchr/testify/require" + signer_extraction "github.com/skip-mev/block-sdk/v2/adapters/signer_extraction_adapter" "github.com/skip-mev/block-sdk/v2/block/base" "github.com/skip-mev/block-sdk/v2/testutils" ) -func TestNewDefaultMempool(t *testing.T) { - tests := []struct { - name string - maxTxs int - }{ - { - name: "unlimited capacity", - maxTxs: 0, - }, - { - name: "limited capacity", - maxTxs: 10, - }, - { - name: "negative capacity", - maxTxs: -1, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - mp := base.NewDefaultMempool[int64](tt.maxTxs) - require.NotNil(t, mp) - require.Equal(t, 0, mp.CountTx()) - }) - } -} - func TestDefaultMempool_Insert(t *testing.T) { ctx := context.Background() accounts := testutils.RandomAccounts(rand.New(rand.NewSource(1)), 3) txConfig := testutils.CreateTestEncodingConfig().TxConfig + signerExtractor := signer_extraction.NewDefaultAdapter() t.Run("insert single transaction", func(t *testing.T) { - mp := base.NewDefaultMempool[int64](0) - tx, err := testutils.CreateTx(txConfig, accounts[0], 0, 0, nil, sdk.NewCoin("stake", sdkmath.NewInt(100))) + mp := base.NewDefaultMempool[int64](0, signerExtractor) + tx, err := testutils.CreateTx(txConfig, accounts[0], 0, 0, nil, sdk.NewCoin("stake", math.NewInt(100))) require.NoError(t, err) err = mp.Insert(ctx, tx) @@ -59,10 +33,10 @@ func TestDefaultMempool_Insert(t *testing.T) { }) t.Run("insert multiple transactions", func(t *testing.T) { - mp := base.NewDefaultMempool[int64](0) + mp := base.NewDefaultMempool[int64](0, signerExtractor) for i := 0; i < 3; i++ { - tx, err := testutils.CreateTx(txConfig, accounts[i], 0, 0, nil, sdk.NewCoin("stake", sdkmath.NewInt(int64(100+i)))) + tx, err := testutils.CreateTx(txConfig, accounts[i], 0, 0, nil, sdk.NewCoin("stake", math.NewInt(int64(100+i)))) require.NoError(t, err) err = mp.Insert(ctx, tx) @@ -72,24 +46,45 @@ func TestDefaultMempool_Insert(t *testing.T) { } }) + t.Run("insert duplicate transaction (same sender:nonce) should be no-op", func(t *testing.T) { + mp := base.NewDefaultMempool[int64](0, signerExtractor) + + // Insert first transaction + tx1, err := testutils.CreateTx(txConfig, accounts[0], 0, 0, nil, sdk.NewCoin("stake", math.NewInt(100))) + require.NoError(t, err) + + err = mp.Insert(ctx, tx1) + require.NoError(t, err) + require.Equal(t, 1, mp.CountTx()) + require.True(t, mp.Contains(tx1)) + + // Insert second transaction with same sender:nonce (should be no-op) + tx2, err := testutils.CreateTx(txConfig, accounts[0], 0, 0, nil, sdk.NewCoin("stake", math.NewInt(200))) + require.NoError(t, err) + + err = mp.Insert(ctx, tx2) + require.NoError(t, err) + require.Equal(t, 1, mp.CountTx()) // Count should remain 1 + require.True(t, mp.Contains(tx1)) // Original transaction should still be there + require.False(t, mp.Contains(tx2)) // New transaction should not be added + }) + t.Run("insert with max capacity", func(t *testing.T) { maxTx := 2 - mp := base.NewDefaultMempool[int64](maxTx) + mp := base.NewDefaultMempool[int64](maxTx, signerExtractor) // Insert up to capacity - var txs []sdk.Tx for i := 0; i < maxTx; i++ { - tx, err := testutils.CreateTx(txConfig, accounts[i], 0, 0, nil, sdk.NewCoin("stake", sdkmath.NewInt(int64(100+i)))) + tx, err := testutils.CreateTx(txConfig, accounts[i], 0, 0, nil, sdk.NewCoin("stake", math.NewInt(int64(100+i)))) require.NoError(t, err) - _ = append(txs, tx) err = mp.Insert(ctx, tx) require.NoError(t, err) } require.Equal(t, maxTx, mp.CountTx()) - // Try to insert beyond capacity - overCapacityTx, err := testutils.CreateTx(txConfig, accounts[2], 0, 0, nil, sdk.NewCoin("stake", sdkmath.NewInt(300))) + // Try to insert beyond capacity (different sender:nonce) + overCapacityTx, err := testutils.CreateTx(txConfig, accounts[2], 1, 0, nil, sdk.NewCoin("stake", math.NewInt(300))) require.NoError(t, err) err = mp.Insert(ctx, overCapacityTx) @@ -98,46 +93,41 @@ func TestDefaultMempool_Insert(t *testing.T) { require.False(t, mp.Contains(overCapacityTx)) }) - t.Run("insert with negative max capacity", func(t *testing.T) { - mp := base.NewDefaultMempool[int64](-1) - tx, err := testutils.CreateTx(txConfig, accounts[0], 0, 0, nil, sdk.NewCoin("stake", sdkmath.NewInt(100))) - require.NoError(t, err) + t.Run("insert duplicate within capacity should not trigger capacity error", func(t *testing.T) { + maxTx := 1 + mp := base.NewDefaultMempool[int64](maxTx, signerExtractor) - err = mp.Insert(ctx, tx) + // Insert first transaction (fills capacity) + tx1, err := testutils.CreateTx(txConfig, accounts[0], 0, 0, nil, sdk.NewCoin("stake", math.NewInt(100))) require.NoError(t, err) - require.Equal(t, 0, mp.CountTx()) // Should not actually insert - require.False(t, mp.Contains(tx)) - }) - t.Run("insert duplicate transactions", func(t *testing.T) { - mp := base.NewDefaultMempool[int64](0) - tx, err := testutils.CreateTx(txConfig, accounts[0], 0, 0, nil, sdk.NewCoin("stake", sdkmath.NewInt(100))) + err = mp.Insert(ctx, tx1) require.NoError(t, err) + require.Equal(t, 1, mp.CountTx()) - // Insert same transaction multiple times - err = mp.Insert(ctx, tx) - require.NoError(t, err) - err = mp.Insert(ctx, tx) - require.NoError(t, err) - err = mp.Insert(ctx, tx) + // Insert duplicate (should be no-op, not trigger capacity error) + tx2, err := testutils.CreateTx(txConfig, accounts[0], 0, 0, nil, sdk.NewCoin("stake", math.NewInt(200))) require.NoError(t, err) - // Should have multiple copies - require.Equal(t, 3, mp.CountTx()) - require.True(t, mp.Contains(tx)) + err = mp.Insert(ctx, tx2) + require.NoError(t, err) // Should NOT get capacity error + require.Equal(t, 1, mp.CountTx()) + require.True(t, mp.Contains(tx1)) // Original transaction should remain + require.False(t, mp.Contains(tx2)) // Duplicate should not be added }) } func TestDefaultMempool_Remove(t *testing.T) { ctx := context.Background() - accounts := testutils.RandomAccounts(rand.New(rand.NewSource(1)), 2) + accounts := testutils.RandomAccounts(rand.New(rand.NewSource(1)), 3) txConfig := testutils.CreateTestEncodingConfig().TxConfig + signerExtractor := signer_extraction.NewDefaultAdapter() t.Run("remove existing transaction", func(t *testing.T) { - mp := base.NewDefaultMempool[int64](0) - tx1, err := testutils.CreateTx(txConfig, accounts[0], 0, 0, nil, sdk.NewCoin("stake", sdkmath.NewInt(100))) + mp := base.NewDefaultMempool[int64](0, signerExtractor) + tx1, err := testutils.CreateTx(txConfig, accounts[0], 0, 0, nil, sdk.NewCoin("stake", math.NewInt(100))) require.NoError(t, err) - tx2, err := testutils.CreateTx(txConfig, accounts[1], 0, 0, nil, sdk.NewCoin("stake", sdkmath.NewInt(200))) + tx2, err := testutils.CreateTx(txConfig, accounts[1], 0, 0, nil, sdk.NewCoin("stake", math.NewInt(200))) require.NoError(t, err) // Insert transactions @@ -156,10 +146,10 @@ func TestDefaultMempool_Remove(t *testing.T) { }) t.Run("remove non-existing transaction", func(t *testing.T) { - mp := base.NewDefaultMempool[int64](0) - tx1, err := testutils.CreateTx(txConfig, accounts[0], 0, 0, nil, sdk.NewCoin("stake", sdkmath.NewInt(100))) + mp := base.NewDefaultMempool[int64](0, signerExtractor) + tx1, err := testutils.CreateTx(txConfig, accounts[0], 0, 0, nil, sdk.NewCoin("stake", math.NewInt(100))) require.NoError(t, err) - tx2, err := testutils.CreateTx(txConfig, accounts[1], 0, 0, nil, sdk.NewCoin("stake", sdkmath.NewInt(200))) + tx2, err := testutils.CreateTx(txConfig, accounts[1], 0, 0, nil, sdk.NewCoin("stake", math.NewInt(200))) require.NoError(t, err) // Insert one transaction @@ -172,75 +162,28 @@ func TestDefaultMempool_Remove(t *testing.T) { require.Equal(t, 1, mp.CountTx()) require.True(t, mp.Contains(tx1)) }) - - t.Run("remove from empty mempool", func(t *testing.T) { - mp := base.NewDefaultMempool[int64](0) - tx, err := testutils.CreateTx(txConfig, accounts[0], 0, 0, nil, sdk.NewCoin("stake", sdkmath.NewInt(100))) - require.NoError(t, err) - - err = mp.Remove(tx) - require.NoError(t, err) // Should not error - require.Equal(t, 0, mp.CountTx()) - }) - - t.Run("remove duplicate transactions", func(t *testing.T) { - mp := base.NewDefaultMempool[int64](0) - tx, err := testutils.CreateTx(txConfig, accounts[0], 0, 0, nil, sdk.NewCoin("stake", sdkmath.NewInt(100))) - require.NoError(t, err) - - // Insert same transaction multiple times - err = mp.Insert(ctx, tx) - require.NoError(t, err) - err = mp.Insert(ctx, tx) - require.NoError(t, err) - require.Equal(t, 2, mp.CountTx()) - - // Remove should only remove first occurrence - err = mp.Remove(tx) - require.NoError(t, err) - require.Equal(t, 1, mp.CountTx()) - require.True(t, mp.Contains(tx)) // Still contains the duplicate - }) } func TestDefaultMempool_Select(t *testing.T) { ctx := context.Background() accounts := testutils.RandomAccounts(rand.New(rand.NewSource(1)), 3) txConfig := testutils.CreateTestEncodingConfig().TxConfig + signerExtractor := signer_extraction.NewDefaultAdapter() t.Run("select from empty mempool", func(t *testing.T) { - mp := base.NewDefaultMempool[int64](0) + mp := base.NewDefaultMempool[int64](0, signerExtractor) iter := mp.Select(ctx, nil) require.Nil(t, iter) }) - t.Run("select single transaction", func(t *testing.T) { - mp := base.NewDefaultMempool[int64](0) - tx, err := testutils.CreateTx(txConfig, accounts[0], 0, 0, nil, sdk.NewCoin("stake", sdkmath.NewInt(100))) - require.NoError(t, err) - - err = mp.Insert(ctx, tx) - require.NoError(t, err) - - iter := mp.Select(ctx, nil) - require.NotNil(t, iter) - - // Check first transaction - require.Equal(t, tx, iter.Tx()) - - // Check no more transactions - next := iter.Next() - require.Nil(t, next) - }) - - t.Run("select multiple transactions", func(t *testing.T) { - mp := base.NewDefaultMempool[int64](0) + t.Run("select maintains FIFO order", func(t *testing.T) { + mp := base.NewDefaultMempool[int64](0, signerExtractor) var txs []sdk.Tx - // Insert transactions + // Insert transactions in order for i := 0; i < 3; i++ { - tx, err := testutils.CreateTx(txConfig, accounts[i], 0, 0, nil, sdk.NewCoin("stake", sdkmath.NewInt(int64(100+i)))) + tx, err := testutils.CreateTx(txConfig, accounts[i], 0, 0, nil, sdk.NewCoin("stake", math.NewInt(int64(100+i)))) require.NoError(t, err) txs = append(txs, tx) @@ -254,58 +197,27 @@ func TestDefaultMempool_Select(t *testing.T) { collectedTxs = append(collectedTxs, iter.Tx()) } - // Should have all transactions in insertion order + // Should maintain FIFO order require.Equal(t, len(txs), len(collectedTxs)) for i, tx := range txs { require.Equal(t, tx, collectedTxs[i]) } }) - - t.Run("select with ignored parameters", func(t *testing.T) { - mp := base.NewDefaultMempool[int64](0) - tx, err := testutils.CreateTx(txConfig, accounts[0], 0, 0, nil, sdk.NewCoin("stake", sdkmath.NewInt(100))) - require.NoError(t, err) - - err = mp.Insert(ctx, tx) - require.NoError(t, err) - - // The txs parameter is ignored in DefaultMempool - iter := mp.Select(ctx, [][]byte{{1, 2, 3}}) - require.NotNil(t, iter) - require.Equal(t, tx, iter.Tx()) - }) } -func TestDefaultMempool_CountTx(t *testing.T) { +func TestDefaultMempool_Integration(t *testing.T) { ctx := context.Background() accounts := testutils.RandomAccounts(rand.New(rand.NewSource(1)), 5) txConfig := testutils.CreateTestEncodingConfig().TxConfig + signerExtractor := signer_extraction.NewDefaultAdapter() - t.Run("count starts at zero", func(t *testing.T) { - mp := base.NewDefaultMempool[int64](0) - require.Equal(t, 0, mp.CountTx()) - }) - - t.Run("count increases with inserts", func(t *testing.T) { - mp := base.NewDefaultMempool[int64](0) - - for i := 1; i <= 5; i++ { - tx, err := testutils.CreateTx(txConfig, accounts[i-1], 0, 0, nil, sdk.NewCoin("stake", sdkmath.NewInt(int64(100+i)))) - require.NoError(t, err) - - err = mp.Insert(ctx, tx) - require.NoError(t, err) - require.Equal(t, i, mp.CountTx()) - } - }) + t.Run("comprehensive integration test with duplicates", func(t *testing.T) { + mp := base.NewDefaultMempool[int64](10, signerExtractor) - t.Run("count decreases with removes", func(t *testing.T) { - mp := base.NewDefaultMempool[int64](0) + // Insert initial transactions var txs []sdk.Tx - - // Insert transactions for i := 0; i < 3; i++ { - tx, err := testutils.CreateTx(txConfig, accounts[i], 0, 0, nil, sdk.NewCoin("stake", sdkmath.NewInt(int64(100+i)))) + tx, err := testutils.CreateTx(txConfig, accounts[i], 0, 0, nil, sdk.NewCoin("stake", math.NewInt(int64(100+i*10)))) require.NoError(t, err) txs = append(txs, tx) @@ -314,254 +226,35 @@ func TestDefaultMempool_CountTx(t *testing.T) { } require.Equal(t, 3, mp.CountTx()) - // Remove transactions - for i, tx := range txs { - err := mp.Remove(tx) - require.NoError(t, err) - require.Equal(t, 2-i, mp.CountTx()) - } - }) -} - -func TestDefaultMempool_Contains(t *testing.T) { - ctx := context.Background() - accounts := testutils.RandomAccounts(rand.New(rand.NewSource(1)), 3) - txConfig := testutils.CreateTestEncodingConfig().TxConfig - - t.Run("contains returns false for empty mempool", func(t *testing.T) { - mp := base.NewDefaultMempool[int64](0) - tx, err := testutils.CreateTx(txConfig, accounts[0], 0, 0, nil, sdk.NewCoin("stake", sdkmath.NewInt(100))) - require.NoError(t, err) - - require.False(t, mp.Contains(tx)) - }) - - t.Run("contains returns true for inserted transaction", func(t *testing.T) { - mp := base.NewDefaultMempool[int64](0) - tx, err := testutils.CreateTx(txConfig, accounts[0], 0, 0, nil, sdk.NewCoin("stake", sdkmath.NewInt(100))) - require.NoError(t, err) - - err = mp.Insert(ctx, tx) - require.NoError(t, err) - require.True(t, mp.Contains(tx)) - }) - - t.Run("contains returns false for removed transaction", func(t *testing.T) { - mp := base.NewDefaultMempool[int64](0) - tx, err := testutils.CreateTx(txConfig, accounts[0], 0, 0, nil, sdk.NewCoin("stake", sdkmath.NewInt(100))) + // Attempt to insert duplicate transaction (should be no-op) + duplicateTx, err := testutils.CreateTx(txConfig, accounts[0], 0, 0, nil, sdk.NewCoin("stake", math.NewInt(500))) require.NoError(t, err) - err = mp.Insert(ctx, tx) - require.NoError(t, err) - require.True(t, mp.Contains(tx)) - - err = mp.Remove(tx) - require.NoError(t, err) - require.False(t, mp.Contains(tx)) - }) - - t.Run("contains works with multiple transactions", func(t *testing.T) { - mp := base.NewDefaultMempool[int64](0) - tx1, err := testutils.CreateTx(txConfig, accounts[0], 0, 0, nil, sdk.NewCoin("stake", sdkmath.NewInt(100))) - require.NoError(t, err) - tx2, err := testutils.CreateTx(txConfig, accounts[1], 0, 0, nil, sdk.NewCoin("stake", sdkmath.NewInt(200))) - require.NoError(t, err) - tx3, err := testutils.CreateTx(txConfig, accounts[2], 0, 0, nil, sdk.NewCoin("stake", sdkmath.NewInt(300))) - require.NoError(t, err) - - err = mp.Insert(ctx, tx1) - require.NoError(t, err) - err = mp.Insert(ctx, tx3) - require.NoError(t, err) - - require.True(t, mp.Contains(tx1)) - require.False(t, mp.Contains(tx2)) - require.True(t, mp.Contains(tx3)) - }) -} - -func TestDefaultIterator(t *testing.T) { - ctx := context.Background() - accounts := testutils.RandomAccounts(rand.New(rand.NewSource(1)), 4) - txConfig := testutils.CreateTestEncodingConfig().TxConfig - - t.Run("iterator next returns nil when at end", func(t *testing.T) { - mp := base.NewDefaultMempool[int64](0) - tx, err := testutils.CreateTx(txConfig, accounts[0], 0, 0, nil, sdk.NewCoin("stake", sdkmath.NewInt(100))) - require.NoError(t, err) - - err = mp.Insert(ctx, tx) + err = mp.Insert(ctx, duplicateTx) require.NoError(t, err) + require.Equal(t, 3, mp.CountTx()) // Count should remain the same + require.True(t, mp.Contains(txs[0])) // Original transaction should still be there + require.False(t, mp.Contains(duplicateTx)) // Duplicate should not be added - iter := mp.Select(ctx, nil) - require.NotNil(t, iter) - - // First transaction - require.Equal(t, tx, iter.Tx()) - - // Next should return nil (end of iterator) - next := iter.Next() - require.Nil(t, next) - }) - - t.Run("iterator maintains position correctly", func(t *testing.T) { - mp := base.NewDefaultMempool[int64](0) - var txs []sdk.Tx - - for i := 0; i < 3; i++ { - tx, err := testutils.CreateTx(txConfig, accounts[i], 0, 0, nil, sdk.NewCoin("stake", sdkmath.NewInt(int64(100+i)))) - require.NoError(t, err) - txs = append(txs, tx) - - err = mp.Insert(ctx, tx) - require.NoError(t, err) - } - - iter := mp.Select(ctx, nil) - require.NotNil(t, iter) - - // Manually iterate and check each position - for i, expectedTx := range txs { - require.Equal(t, expectedTx, iter.Tx(), "iteration %d", i) - if i < len(txs)-1 { - iter = iter.Next() - require.NotNil(t, iter, "iteration %d", i) - } - } - - // Should be at end now - next := iter.Next() - require.Nil(t, next) - }) - - t.Run("iterator returns correct transaction at each position", func(t *testing.T) { - mp := base.NewDefaultMempool[int64](0) - var txs []sdk.Tx - - for i := 0; i < 4; i++ { - tx, err := testutils.CreateTx(txConfig, accounts[i], 0, 0, nil, sdk.NewCoin("stake", sdkmath.NewInt(int64(100+i)))) - require.NoError(t, err) - txs = append(txs, tx) - - err = mp.Insert(ctx, tx) - require.NoError(t, err) - } - - iter := mp.Select(ctx, nil) - for i, expectedTx := range txs { - require.NotNil(t, iter, "position %d should have iterator", i) - require.Equal(t, expectedTx, iter.Tx(), "position %d should have correct tx", i) - iter = iter.Next() - } - require.Nil(t, iter, "should be at end after all transactions") - }) -} - -func TestDefaultMempool_Integration(t *testing.T) { - // Integration test with more complex scenarios - ctx := context.Background() - accounts := testutils.RandomAccounts(rand.New(rand.NewSource(1)), 5) - txConfig := testutils.CreateTestEncodingConfig().TxConfig - - t.Run("comprehensive integration test", func(t *testing.T) { - mp := base.NewDefaultMempool[int64](10) - - // Create various transactions - var txs []sdk.Tx - for i := 0; i < 5; i++ { - tx, err := testutils.CreateTx(txConfig, accounts[i], 0, 0, nil, sdk.NewCoin("stake", sdkmath.NewInt(int64(100+i*10)))) - require.NoError(t, err) - txs = append(txs, tx) - } - - // Test basic operations - for i, tx := range txs { - err := mp.Insert(ctx, tx) - require.NoError(t, err) - require.Equal(t, i+1, mp.CountTx()) - require.True(t, mp.Contains(tx)) - } - - // Test iterator returns all transactions + // Verify FIFO order is maintained (no changes) var collectedTxs []sdk.Tx for iter := mp.Select(ctx, nil); iter != nil; iter = iter.Next() { collectedTxs = append(collectedTxs, iter.Tx()) } - require.Len(t, collectedTxs, 5) - - // Test removal - err := mp.Remove(txs[2]) // Remove middle transaction - require.NoError(t, err) - require.Equal(t, 4, mp.CountTx()) - require.False(t, mp.Contains(txs[2])) - - // Verify other transactions still exist - for i, tx := range txs { - if i != 2 { - require.True(t, mp.Contains(tx)) - } - } - - // Test iterator after removal - collectedTxs = nil - for iter := mp.Select(ctx, nil); iter != nil; iter = iter.Next() { - collectedTxs = append(collectedTxs, iter.Tx()) - } - require.Len(t, collectedTxs, 4) + require.Len(t, collectedTxs, 3) + // Transactions should remain in original order + require.Equal(t, txs[0], collectedTxs[0]) + require.Equal(t, txs[1], collectedTxs[1]) + require.Equal(t, txs[2], collectedTxs[2]) }) } // TestDefaultMempool_ImplementsInterface verifies that DefaultMempool implements MempoolInterface func TestDefaultMempool_ImplementsInterface(t *testing.T) { + signerExtractor := signer_extraction.NewDefaultAdapter() var _ base.MempoolInterface = (*base.DefaultMempool[int64])(nil) - // Test passes if compilation succeeds -} -// Benchmark tests -func BenchmarkDefaultMempool_Insert(b *testing.B) { - accounts := testutils.RandomAccounts(rand.New(rand.NewSource(1)), 1000) - txConfig := testutils.CreateTestEncodingConfig().TxConfig - ctx := context.Background() - mp := base.NewDefaultMempool[int64](0) - - b.ResetTimer() - for i := 0; i < b.N; i++ { - tx, err := testutils.CreateTx(txConfig, accounts[i%1000], uint64(i), 0, nil, sdk.NewCoin("stake", sdkmath.NewInt(int64(100+i)))) - if err != nil { - b.Fatal(err) - } - err = mp.Insert(ctx, tx) - if err != nil { - b.Fatal(err) - } - } -} - -func BenchmarkDefaultMempool_Contains(b *testing.B) { - accounts := testutils.RandomAccounts(rand.New(rand.NewSource(1)), 1000) - txConfig := testutils.CreateTestEncodingConfig().TxConfig - ctx := context.Background() - mp := base.NewDefaultMempool[int64](0) - - // Pre-populate mempool - var txs []sdk.Tx - for i := 0; i < 1000; i++ { - tx, err := testutils.CreateTx(txConfig, accounts[i], 0, 0, nil, sdk.NewCoin("stake", sdkmath.NewInt(int64(100+i)))) - if err != nil { - b.Fatal(err) - } - err = mp.Insert(ctx, tx) - if err != nil { - b.Fatal(err) - } - txs = append(txs, tx) - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - found := mp.Contains(txs[i%1000]) - if !found { - b.Fatal("transaction not found") - } - } + // Test that we can create the mempool + mp := base.NewDefaultMempool[int64](100, signerExtractor) + require.NotNil(t, mp) } diff --git a/block/base/mempool.go b/block/base/mempool.go index 32d3cce2..2bc5f2c2 100644 --- a/block/base/mempool.go +++ b/block/base/mempool.go @@ -51,7 +51,7 @@ func NewMempool[C comparable](txPriority TxPriority[C], extractor signer_extract // NewDefaultMempool returns a new default Mempool. func NewMempoolWithDefaultOrdering[C comparable](txPriority TxPriority[C], extractor signer_extraction.Adapter, maxTx int) *Mempool[C] { return &Mempool[C]{ - index: NewDefaultMempool[C](maxTx), + index: NewDefaultMempool[C](maxTx, extractor), extractor: extractor, txPriority: txPriority, }