Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 13 additions & 11 deletions txcache/autoClean.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,14 +79,11 @@ func (cache *TxCache) RemoveSweepableTxs(accountsProvider common.AccountNoncePro
// we want to remove transactions with nonces < lastCommittedNonce
targetNonce := accountNonce - 1

evicted = append(evicted, sender.removeSweepableTransactionsReturnHashes(targetNonce)...)
evicted = append(evicted, sender.removeSweepableTransactionsReturnHashes(targetNonce, cache.tracker)...)
}

if len(evicted) > 0 {
txs := cache.txByHash.GetTxsBulk(evicted)

untrackedEvicted := cache.tracker.GetBulkOfUntrackedTransactions(txs)
cache.txByHash.RemoveTxsBulk(untrackedEvicted)
cache.txByHash.RemoveTxsBulk(evicted)
}

logRemove.Debug("TxCache.RemoveSweepableTxs end",
Expand Down Expand Up @@ -134,7 +131,7 @@ func shuffleSendersAddresses(senders []string, randomness uint64) {
})
}

func (listForSender *txListForSender) removeSweepableTransactionsReturnHashes(targetNonce uint64) [][]byte {
func (listForSender *txListForSender) removeSweepableTransactionsReturnHashes(targetNonce uint64, tracker *selectionTracker) [][]byte {
txHashesToEvict := make([][]byte, 0)

// We don't allow concurrent goroutines to mutate a given sender's list
Expand All @@ -144,6 +141,7 @@ func (listForSender *txListForSender) removeSweepableTransactionsReturnHashes(ta
for element := listForSender.items.Front(); element != nil; {
// finds transactions with lower nonces
tx := element.Value.(*WrappedTransaction)

txNonce := tx.Tx.GetNonce()

// nonces are sorted ascending, so we can stop as soon as we find a nonce that is higher
Expand All @@ -158,12 +156,16 @@ func (listForSender *txListForSender) removeSweepableTransactionsReturnHashes(ta
)

nextElement := element.Next()
_ = listForSender.items.Remove(element)
listForSender.onRemovedListElement(element)
element = nextElement

// Keep track of removed transactions
txHashesToEvict = append(txHashesToEvict, tx.TxHash)
if !tracker.IsTransactionTracked(tx) {
_ = listForSender.items.Remove(element)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove only if the transaction is not tracked

listForSender.onRemovedListElement(element)

// Keep track of removed transactions
txHashesToEvict = append(txHashesToEvict, tx.TxHash)
}

element = nextElement
Comment on lines +160 to +168
Copy link

Copilot AI Oct 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the previous issue, this logic only removes untracked transactions but doesn't handle tracked transactions that should be swept. If a transaction with a lower nonce is tracked, it will remain in the list even though it should be removed based on the sweeping criteria.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, added already a TODO for this discussion.

}

return txHashesToEvict
Expand Down
103 changes: 97 additions & 6 deletions txcache/autoClean_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"math"
"testing"

"github.com/multiversx/mx-chain-core-go/data/block"
"github.com/multiversx/mx-chain-go/testscommon/txcachemocks"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -88,16 +89,17 @@ func TestTxCache_GetDeterministicallyShuffledSenders_Dummy(t *testing.T) {

func Test_RemoveSweepableTransactionsReturnHashes_Dummy(t *testing.T) {
t.Run("with lower nonces", func(t *testing.T) {
txCache := newCacheToTest(maxNumBytesPerSenderUpperBoundTest, math.MaxUint32)
list := newUnconstrainedListToTest()
list.AddTx(createTx([]byte("a"), ".", 1))
list.AddTx(createTx([]byte("b"), ".", 3))
list.AddTx(createTx([]byte("c"), ".", 4))
list.AddTx(createTx([]byte("d"), ".", 2))
list.AddTx(createTx([]byte("e"), ".", 5))
list.AddTx(createTx([]byte("a"), ".", 1), txCache.tracker)
list.AddTx(createTx([]byte("b"), ".", 3), txCache.tracker)
list.AddTx(createTx([]byte("c"), ".", 4), txCache.tracker)
list.AddTx(createTx([]byte("d"), ".", 2), txCache.tracker)
list.AddTx(createTx([]byte("e"), ".", 5), txCache.tracker)

hashesBeforeEviction := list.getTxHashesAsStrings()

list.removeSweepableTransactionsReturnHashes(uint64(3))
list.removeSweepableTransactionsReturnHashes(uint64(3), txCache.tracker)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't we also check autoclean for tracked transactions and also consistency between the 2 maps?

hashesAfterEviction := list.getTxHashesAsStrings()
require.Equal(t, []string{"c", "e"}, hashesAfterEviction)

Expand Down Expand Up @@ -139,6 +141,7 @@ func TestTxCache_Cleanup(t *testing.T) {
evicted := cache.Cleanup(accountsProvider, 7, math.MaxInt, 1000*cleanupLoopMaximumDuration)

require.Equal(t, uint64(expectedNumEvicted), evicted)
require.True(t, cache.areInternalMapsConsistent())
})

t.Run("with nonce equal 0", func(t *testing.T) {
Expand Down Expand Up @@ -177,6 +180,7 @@ func TestTxCache_Cleanup(t *testing.T) {
expectedNumEvicted := 2 // only alice, because maxNum is 2
evicted := cache.Cleanup(accountsProvider, 7, 2, 1000*cleanupLoopMaximumDuration)
require.Equal(t, uint64(expectedNumEvicted), evicted)
require.True(t, cache.areInternalMapsConsistent())
})

t.Run("with cleanupLoopMaximumDuration cap reached", func(t *testing.T) {
Expand All @@ -200,6 +204,7 @@ func TestTxCache_Cleanup(t *testing.T) {
evictable := 8
evicted := cache.Cleanup(accountsProvider, 7, math.MaxInt, 1000)
require.Less(t, evicted, uint64(evictable))
require.True(t, cache.areInternalMapsConsistent())
})

t.Run("with lower nonces", func(t *testing.T) {
Expand Down Expand Up @@ -228,6 +233,92 @@ func TestTxCache_Cleanup(t *testing.T) {
expectedNumEvicted := 3 // 2 bob, 1 alice
evicted := cache.Cleanup(accountsProvider, 7, math.MaxInt, 1000*cleanupLoopMaximumDuration)
require.Equal(t, uint64(expectedNumEvicted), evicted)
require.True(t, cache.areInternalMapsConsistent())
})

t.Run("with tracked txs", func(t *testing.T) {
boundsConfig := createMockTxBoundsConfig()
cache := newUnconstrainedCacheToTest(boundsConfig)

accountsProvider := txcachemocks.NewAccountNonceAndBalanceProviderMock()
accountsProvider.SetNonce([]byte("alice"), 2)
accountsProvider.SetNonce([]byte("bob"), 42)
accountsProvider.SetNonce([]byte("carol"), 7)

// add txs into the cache
cache.AddTx(createTx([]byte("hash-alice-2"), "alice", 2))
cache.AddTx(createTx([]byte("hash-alice-3"), "alice", 3))
cache.AddTx(createTx([]byte("hash-bob-42"), "bob", 42))
cache.AddTx(createTx([]byte("hash-carol-7"), "carol", 7))
cache.AddTx(createTx([]byte("hash-carol-8"), "carol", 8))

// propose those txs
err := cache.OnProposedBlock(
[]byte("blockHash1"),
&block.Body{
MiniBlocks: []*block.MiniBlock{
{
TxHashes: [][]byte{
[]byte("hash-alice-2"),
[]byte("hash-alice-3"),
[]byte("hash-bob-42"),
[]byte("hash-carol-7"),
[]byte("hash-carol-8"),
},
},
},
},
&block.Header{
Nonce: 1,
PrevHash: []byte("blockHash0"),
},
accountsProvider,
[]byte("blockHash0"),
)
require.Nil(t, err)

expectedNumEvicted := 0
evicted := cache.Cleanup(accountsProvider, 7, math.MaxInt, 1000*cleanupLoopMaximumDuration)
require.Equal(t, uint64(expectedNumEvicted), evicted)
require.True(t, cache.areInternalMapsConsistent())

// add a transaction that has a tracked nonce
cache.AddTx(createTx([]byte("hash-carol-X"), "carol", 8))
evicted = cache.Cleanup(accountsProvider, 7, math.MaxInt, 1000*cleanupLoopMaximumDuration)
require.Equal(t, uint64(expectedNumEvicted), evicted)
require.True(t, cache.areInternalMapsConsistent())

_, ok := cache.GetByTxHash([]byte("hash-carol-X"))
require.True(t, ok)

// add a transaction with lower nonce
cache.AddTx(createTx([]byte("hash-alice-X"), "alice", 1))
expectedNumEvicted = 1
evicted = cache.Cleanup(accountsProvider, 7, math.MaxInt, 1000*cleanupLoopMaximumDuration)
require.Equal(t, uint64(expectedNumEvicted), evicted)
require.True(t, cache.areInternalMapsConsistent())

_, ok = cache.GetByTxHash([]byte("hash-alice-X"))
require.False(t, ok)

err = cache.OnExecutedBlock(
&block.Header{
Nonce: 1,
PrevHash: []byte("blockHash0"),
})
require.Nil(t, err)

cache.Remove([]byte("hash-alice-2"))
cache.Remove([]byte("hash-alice-3"))
cache.Remove([]byte("hash-bob-42"))
cache.Remove([]byte("hash-carol-7"))
// when this transaction is removed, the hash-carol-X transaction is also removed
cache.Remove([]byte("hash-carol-8"))

expectedNumEvicted = 0
evicted = cache.Cleanup(accountsProvider, 7, math.MaxInt, 1000*cleanupLoopMaximumDuration)
require.Equal(t, uint64(expectedNumEvicted), evicted)
require.True(t, cache.areInternalMapsConsistent())
})
}

Expand Down
15 changes: 6 additions & 9 deletions txcache/eviction.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,10 @@ func (cache *TxCache) evictLeastLikelyToSelectTransactions() *evictionJournal {
break
}

transactionsToEvict = append(transactionsToEvict, item.currentTransaction)
transactionsToEvictHashes = append(transactionsToEvictHashes, item.currentTransaction.TxHash)
if !cache.tracker.IsTransactionTracked(item.currentTransaction) {
transactionsToEvict = append(transactionsToEvict, item.currentTransaction)
transactionsToEvictHashes = append(transactionsToEvictHashes, item.currentTransaction.TxHash)
}

// If there are more transactions in the same bunch (same sender as the popped item),
// add the next one to the heap (to compete with the others in being "the worst").
Expand Down Expand Up @@ -157,17 +159,12 @@ func (cache *TxCache) evictLeastLikelyToSelectTransactions() *evictionJournal {
}

// Remove those transactions from "txByHash".
txs := cache.txByHash.GetTxsBulk(transactionsToEvictHashes)

logRemove.Debug("evictLeastLikelyToSelectTransactions", "pass", pass, "num evicted", len(transactionsToEvict))

untrackedTransactionsToEvictHashes := cache.tracker.GetBulkOfUntrackedTransactions(txs)
_ = cache.txByHash.RemoveTxsBulk(untrackedTransactionsToEvictHashes)
_ = cache.txByHash.RemoveTxsBulk(transactionsToEvictHashes)

journal.numEvictedByPass = append(journal.numEvictedByPass, len(transactionsToEvict))
journal.numEvicted += len(transactionsToEvict)

logRemove.Debug("evictLeastLikelyToSelectTransactions", "pass", pass, "num untracked evicted", len(untrackedTransactionsToEvictHashes))
logRemove.Debug("evictLeastLikelyToSelectTransactions", "pass", pass, "num evicted", len(transactionsToEvict))
}

return journal
Expand Down
65 changes: 65 additions & 0 deletions txcache/eviction_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"testing"

"github.com/multiversx/mx-chain-core-go/core"
"github.com/multiversx/mx-chain-core-go/data/block"
"github.com/multiversx/mx-chain-go/testscommon/txcachemocks"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -87,6 +88,70 @@ func TestTxCache_DoEviction_BecauseOfSize(t *testing.T) {
require.Equal(t, uint64(3), cache.CountTx())
}

func TestTxCache_DoEviction_WithTrackedTxs(t *testing.T) {
config := ConfigSourceMe{
Name: "untitled",
NumChunks: 16,
NumBytesThreshold: maxNumBytesUpperBound,
NumBytesPerSenderThreshold: maxNumBytesPerSenderUpperBoundTest,
CountThreshold: 4,
CountPerSenderThreshold: math.MaxUint32,
EvictionEnabled: true,
NumItemsToPreemptivelyEvict: 1,
TxCacheBoundsConfig: createMockTxBoundsConfig(),
}

host := txcachemocks.NewMempoolHostMock()

cache, err := NewTxCache(config, host)
require.Nil(t, err)
require.NotNil(t, cache)

accountsProvider := txcachemocks.NewAccountNonceAndBalanceProviderMock()
accountsProvider.SetNonce([]byte("alice"), 1)
accountsProvider.SetNonce([]byte("bob"), 1)
accountsProvider.SetNonce([]byte("carol"), 1)
accountsProvider.SetNonce([]byte("eve"), 1)
accountsProvider.SetNonce([]byte("dan"), 1)

cache.AddTx(createTx([]byte("hash-alice"), "alice", 1).withGasPrice(1 * oneBillion))
cache.AddTx(createTx([]byte("hash-bob"), "bob", 1).withGasPrice(2 * oneBillion))
cache.AddTx(createTx([]byte("hash-carol"), "carol", 1).withGasPrice(3 * oneBillion))
cache.AddTx(createTx([]byte("hash-eve"), "eve", 1).withGasPrice(4 * oneBillion))
cache.AddTx(createTx([]byte("hash-dan"), "dan", 1).withGasPrice(5 * oneBillion))

// propose those txs
err = cache.OnProposedBlock(
[]byte("blockHash1"),
&block.Body{
MiniBlocks: []*block.MiniBlock{
{
TxHashes: [][]byte{
[]byte("hash-alice"),
[]byte("hash-bob"),
[]byte("hash-carol"),
[]byte("hash-dan"),
[]byte("hash-eve"),
},
},
},
},
&block.Header{
Nonce: 1,
PrevHash: []byte("blockHash0"),
},
accountsProvider,
[]byte("blockHash0"),
)
require.Nil(t, err)

// Because all txs are tracked, nothing is evicted.
journal := cache.doEviction()
require.Equal(t, 0, journal.numEvicted)
require.Nil(t, journal.numEvictedByPass)
require.True(t, cache.areInternalMapsConsistent())
}

func TestTxCache_DoEviction_DoesNothingWhenAlreadyInProgress(t *testing.T) {
config := ConfigSourceMe{
Name: "untitled",
Expand Down
Loading
Loading