From 2231c3d3bfbdef6b9ce158fc7ac355d4e3cd24a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Wed, 13 Nov 2024 22:52:10 +0200 Subject: [PATCH 01/34] Don't notify about account nonces, don't forget etc. --- txcache/crossTxCache.go | 8 ---- txcache/disabledCache.go | 8 ---- txcache/txCache_test.go | 65 ------------------------------- txcache/txListBySenderMap.go | 17 -------- txcache/txListBySenderMap_test.go | 16 -------- txcache/txListForSender.go | 36 ++--------------- txcache/txListForSender_test.go | 19 --------- 7 files changed, 4 insertions(+), 165 deletions(-) diff --git a/txcache/crossTxCache.go b/txcache/crossTxCache.go index e39723e4..f2ca9afe 100644 --- a/txcache/crossTxCache.go +++ b/txcache/crossTxCache.go @@ -115,14 +115,6 @@ func (cache *CrossTxCache) GetTransactionsPoolForSender(_ string) []*WrappedTran return make([]*WrappedTransaction, 0) } -// NotifyAccountNonce does nothing, only to respect the interface -func (cache *CrossTxCache) NotifyAccountNonce(_ []byte, _ uint64) { -} - -// ForgetAllAccountNonces does nothing, only to respect the interface -func (cache *CrossTxCache) ForgetAllAccountNonces() { -} - // IsInterfaceNil returns true if there is no value under the interface func (cache *CrossTxCache) IsInterfaceNil() bool { return cache == nil diff --git a/txcache/disabledCache.go b/txcache/disabledCache.go index 805b3164..d448ba59 100644 --- a/txcache/disabledCache.go +++ b/txcache/disabledCache.go @@ -105,14 +105,6 @@ func (cache *DisabledCache) RegisterHandler(func(key []byte, value interface{}), func (cache *DisabledCache) UnRegisterHandler(string) { } -// NotifyAccountNonce does nothing -func (cache *DisabledCache) NotifyAccountNonce(_ []byte, _ uint64) { -} - -// ForgetAllAccountNonces does nothing -func (cache *DisabledCache) ForgetAllAccountNonces() { -} - // ImmunizeTxsAgainstEviction does nothing func (cache *DisabledCache) ImmunizeTxsAgainstEviction(_ [][]byte) { } diff --git a/txcache/txCache_test.go b/txcache/txCache_test.go index 085a85c4..e9038543 100644 --- a/txcache/txCache_test.go +++ b/txcache/txCache_test.go @@ -9,7 +9,6 @@ import ( "testing" "time" - "github.com/multiversx/mx-chain-core-go/core" "github.com/multiversx/mx-chain-core-go/core/check" "github.com/multiversx/mx-chain-storage-go/common" "github.com/multiversx/mx-chain-storage-go/testscommon/txcachemocks" @@ -553,70 +552,6 @@ func TestTxCache_NoCriticalInconsistency_WhenConcurrentAdditionsAndRemovals(t *t } } -func TestTxCache_ForgetAllAccountNonces(t *testing.T) { - config := ConfigSourceMe{ - Name: "untitled", - NumChunks: 16, - NumBytesThreshold: 1000000000, - NumBytesPerSenderThreshold: maxNumBytesPerSenderUpperBound, - CountThreshold: 300001, - CountPerSenderThreshold: math.MaxUint32, - EvictionEnabled: false, - NumItemsToPreemptivelyEvict: 1, - } - - txGasHandler := txcachemocks.NewTxGasHandlerMock() - - sw := core.NewStopWatch() - - t.Run("numSenders = 100000, numTransactions = 1", func(t *testing.T) { - cache, err := NewTxCache(config, txGasHandler) - require.Nil(t, err) - - addManyTransactionsWithUniformDistribution(cache, 100_000, 1) - require.Equal(t, 100000, int(cache.CountTx())) - - sw.Start(t.Name()) - cache.ForgetAllAccountNonces() - sw.Stop(t.Name()) - - cache.txListBySender.backingMap.IterCb(func(key string, item interface{}) { - require.False(t, item.(*txListForSender).accountNonceKnown.IsSet()) - }) - }) - - t.Run("numSenders = 300000, numTransactions = 1", func(t *testing.T) { - cache, err := NewTxCache(config, txGasHandler) - require.Nil(t, err) - - addManyTransactionsWithUniformDistribution(cache, 300_000, 1) - require.Equal(t, 300000, int(cache.CountTx())) - - sw.Start(t.Name()) - cache.ForgetAllAccountNonces() - sw.Stop(t.Name()) - - cache.txListBySender.backingMap.IterCb(func(key string, item interface{}) { - require.False(t, item.(*txListForSender).accountNonceKnown.IsSet()) - }) - }) - - for name, measurement := range sw.GetMeasurementsMap() { - fmt.Printf("%fs (%s)\n", measurement, name) - } - - // (1) - // Vendor ID: GenuineIntel - // Model name: 11th Gen Intel(R) Core(TM) i7-1165G7 @ 2.80GHz - // CPU family: 6 - // Model: 140 - // Thread(s) per core: 2 - // Core(s) per socket: 4 - // - // 0.004712s (TestTxCache_ForgetAllAccountNonces/numSenders_=_100000,_numTransactions_=_1) - // 0.015129s (TestTxCache_ForgetAllAccountNonces/numSenders_=_300000,_numTransactions_=_1) -} - func newUnconstrainedCacheToTest() *TxCache { txGasHandler := txcachemocks.NewTxGasHandlerMock() cache, err := NewTxCache(ConfigSourceMe{ diff --git a/txcache/txListBySenderMap.go b/txcache/txListBySenderMap.go index 1aa36108..50993268 100644 --- a/txcache/txListBySenderMap.go +++ b/txcache/txListBySenderMap.go @@ -123,23 +123,6 @@ func (txMap *txListBySenderMap) RemoveSendersBulk(senders []string) uint32 { return numRemoved } -func (txMap *txListBySenderMap) notifyAccountNonce(accountKey []byte, nonce uint64) { - sender := string(accountKey) - listForSender, ok := txMap.getListForSender(sender) - if !ok { - return - } - - listForSender.notifyAccountNonce(nonce) -} - -func (txMap *txListBySenderMap) forgetAllAccountNonces() { - txMap.backingMap.IterCb(func(key string, item interface{}) { - listForSender := item.(*txListForSender) - listForSender.forgetAccountNonce() - }) -} - // removeTransactionsWithHigherOrEqualNonce removes transactions with nonces higher or equal to the given nonce. // Useful for the eviction flow. func (txMap *txListBySenderMap) removeTransactionsWithHigherOrEqualNonce(accountKey []byte, nonce uint64) { diff --git a/txcache/txListBySenderMap_test.go b/txcache/txListBySenderMap_test.go index 083925fb..b7f8998d 100644 --- a/txcache/txListBySenderMap_test.go +++ b/txcache/txListBySenderMap_test.go @@ -96,22 +96,6 @@ func TestSendersMap_RemoveSendersBulk_ConcurrentWithAddition(t *testing.T) { wg.Wait() } -func TestSendersMap_notifyAccountNonce(t *testing.T) { - myMap := newSendersMapToTest() - - // Discarded notification, since sender not added yet - myMap.notifyAccountNonce([]byte("alice"), 42) - - _, _ = myMap.addTxReturnEvicted(createTx([]byte("tx-42"), "alice", 42)) - alice, _ := myMap.getListForSender("alice") - require.Equal(t, uint64(0), alice.accountNonce.Get()) - require.False(t, alice.accountNonceKnown.IsSet()) - - myMap.notifyAccountNonce([]byte("alice"), 42) - require.Equal(t, uint64(42), alice.accountNonce.Get()) - require.True(t, alice.accountNonceKnown.IsSet()) -} - func newSendersMapToTest() *txListBySenderMap { return newTxListBySenderMap(4, senderConstraints{ maxNumBytes: math.MaxUint32, diff --git a/txcache/txListForSender.go b/txcache/txListForSender.go index b6b43e12..11cb0044 100644 --- a/txcache/txListForSender.go +++ b/txcache/txListForSender.go @@ -11,12 +11,10 @@ import ( // txListForSender represents a sorted list of transactions of a particular sender type txListForSender struct { - sender string - accountNonce atomic.Uint64 - accountNonceKnown atomic.Flag - items *list.List - totalBytes atomic.Counter - constraints *senderConstraints + sender string + items *list.List + totalBytes atomic.Counter + constraints *senderConstraints mutex sync.RWMutex } @@ -192,9 +190,6 @@ func (listForSender *txListForSender) getSequentialTxs() []*WrappedTransaction { listForSender.mutex.RLock() defer listForSender.mutex.RUnlock() - accountNonce := listForSender.accountNonce.Get() - accountNonceKnown := listForSender.accountNonceKnown.IsSet() - result := make([]*WrappedTransaction, 0, listForSender.countTx()) previousNonce := uint64(0) @@ -204,17 +199,6 @@ func (listForSender *txListForSender) getSequentialTxs() []*WrappedTransaction { isFirstTx := len(result) == 0 if isFirstTx { - // Handle lower nonces. - if accountNonceKnown && accountNonce > nonce { - log.Trace("txListForSender.getSequentialTxs, lower nonce", "sender", listForSender.sender, "nonce", nonce, "accountNonce", accountNonce) - continue - } - - // Handle initial gaps. - if accountNonceKnown && accountNonce < nonce { - log.Trace("txListForSender.getSequentialTxs, initial gap", "sender", listForSender.sender, "nonce", nonce, "accountNonce", accountNonce) - break - } } else { // Handle duplicates (only transactions with the highest gas price are included; see "findInsertionPlace"). if nonce == previousNonce { @@ -247,18 +231,6 @@ func (listForSender *txListForSender) countTxWithLock() uint64 { return uint64(listForSender.items.Len()) } -// notifyAccountNonce sets the known account nonce, removes the transactions with lower nonces, and returns their hashes -func (listForSender *txListForSender) notifyAccountNonce(nonce uint64) { - listForSender.accountNonce.Set(nonce) - _ = listForSender.accountNonceKnown.SetReturningPrevious() -} - -// forgetAccountNonce resets the known account nonce -func (listForSender *txListForSender) forgetAccountNonce() { - listForSender.accountNonce.Set(0) - listForSender.accountNonceKnown.Reset() -} - // removeTransactionsWithLowerOrEqualNonceReturnHashes removes transactions with nonces lower or equal to the given nonce func (listForSender *txListForSender) removeTransactionsWithLowerOrEqualNonceReturnHashes(targetNonce uint64) [][]byte { evictedTxHashes := make([][]byte, 0) diff --git a/txcache/txListForSender_test.go b/txcache/txListForSender_test.go index 0b8892db..b9addd4a 100644 --- a/txcache/txListForSender_test.go +++ b/txcache/txListForSender_test.go @@ -107,18 +107,6 @@ func TestListForSender_AddTx_AppliesSizeConstraintsForNumBytes(t *testing.T) { require.Equal(t, []string{"tx4"}, hashesAsStrings(evicted)) } -func TestListForSender_NotifyAccountNonce(t *testing.T) { - list := newUnconstrainedListToTest() - - require.Equal(t, uint64(0), list.accountNonce.Get()) - require.False(t, list.accountNonceKnown.IsSet()) - - list.notifyAccountNonce(42) - - require.Equal(t, uint64(42), list.accountNonce.Get()) - require.True(t, list.accountNonceKnown.IsSet()) -} - func TestListForSender_removeTransactionsWithLowerOrEqualNonceReturnHashes(t *testing.T) { list := newUnconstrainedListToTest() @@ -142,7 +130,6 @@ func TestListForSender_removeTransactionsWithLowerOrEqualNonceReturnHashes(t *te func TestListForSender_getTxs(t *testing.T) { t.Run("no transactions", func(t *testing.T) { list := newUnconstrainedListToTest() - list.notifyAccountNonce(42) require.Len(t, list.getTxs(), 0) require.Len(t, list.getTxsReversed(), 0) @@ -151,7 +138,6 @@ func TestListForSender_getTxs(t *testing.T) { t.Run("one transaction, one gap", func(t *testing.T) { list := newUnconstrainedListToTest() - list.notifyAccountNonce(42) // Gap list.AddTx(createTx([]byte("tx-43"), ".", 43)) @@ -175,7 +161,6 @@ func TestListForSender_getTxs(t *testing.T) { t.Run("with nonce duplicates", func(t *testing.T) { list := newUnconstrainedListToTest() - list.notifyAccountNonce(42) list.AddTx(createTx([]byte("tx-42"), ".", 42)) list.AddTx(createTx([]byte("tx-43"), ".", 43)) @@ -203,7 +188,6 @@ func TestListForSender_getTxs(t *testing.T) { t.Run("with lower nonces", func(t *testing.T) { list := newUnconstrainedListToTest() - list.notifyAccountNonce(43) list.AddTx(createTx([]byte("tx-42"), ".", 42)) list.AddTx(createTx([]byte("tx-43"), ".", 43)) @@ -213,8 +197,6 @@ func TestListForSender_getTxs(t *testing.T) { require.Len(t, list.getSequentialTxs(), 1) require.Equal(t, []byte("tx-43"), list.getSequentialTxs()[0].TxHash) - list.forgetAccountNonce() - require.Len(t, list.getTxs(), 2) require.Len(t, list.getTxsReversed(), 2) require.Len(t, list.getSequentialTxs(), 2) @@ -233,7 +215,6 @@ func TestListForSender_DetectRaceConditions(t *testing.T) { _ = list.getTxsReversed() _ = list.getSequentialTxs() _ = list.countTxWithLock() - list.notifyAccountNonce(42) _, _ = list.AddTx(createTx([]byte("test"), ".", 42)) wg.Done() From b22177e99145462ee8d9652ea4b72b1e16eaf807 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Wed, 13 Nov 2024 22:52:39 +0200 Subject: [PATCH 02/34] When selecting transactions, receive an account nonce provider. --- txcache/interface.go | 6 ++++++ txcache/selection.go | 2 +- txcache/txCache.go | 20 ++------------------ 3 files changed, 9 insertions(+), 19 deletions(-) diff --git a/txcache/interface.go b/txcache/interface.go index f09dc457..9dab7cf0 100644 --- a/txcache/interface.go +++ b/txcache/interface.go @@ -12,5 +12,11 @@ type TxGasHandler interface { IsInterfaceNil() bool } +// AccountNonceProvider defines the behavior of a component able to provide the nonce for an account +type AccountNonceProvider interface { + GetAccountNonce(accountKey []byte) (uint64, error) + IsInterfaceNil() bool +} + // ForEachTransaction is an iterator callback type ForEachTransaction func(txHash []byte, value *WrappedTransaction) diff --git a/txcache/selection.go b/txcache/selection.go index 0d9739bd..71ea3790 100644 --- a/txcache/selection.go +++ b/txcache/selection.go @@ -4,7 +4,7 @@ import ( "container/heap" ) -func (cache *TxCache) doSelectTransactions(gasRequested uint64, maxNum int) (bunchOfTransactions, uint64) { +func (cache *TxCache) doSelectTransactions(accountNonceProvider AccountNonceProvider, gasRequested uint64, maxNum int) (bunchOfTransactions, uint64) { senders := cache.getSenders() bunches := make([]bunchOfTransactions, 0, len(senders)) diff --git a/txcache/txCache.go b/txcache/txCache.go index fe6f37f3..23c9809e 100644 --- a/txcache/txCache.go +++ b/txcache/txCache.go @@ -99,7 +99,7 @@ func (cache *TxCache) GetByTxHash(txHash []byte) (*WrappedTransaction, bool) { // SelectTransactions selects the best transactions to be included in the next miniblock. // It returns up to "maxNum" transactions, with total gas <= "gasRequested". -func (cache *TxCache) SelectTransactions(gasRequested uint64, maxNum int) ([]*WrappedTransaction, uint64) { +func (cache *TxCache) SelectTransactions(accountNonceProvider AccountNonceProvider, gasRequested uint64, maxNum int) ([]*WrappedTransaction, uint64) { stopWatch := core.NewStopWatch() stopWatch.Start("selection") @@ -110,7 +110,7 @@ func (cache *TxCache) SelectTransactions(gasRequested uint64, maxNum int) ([]*Wr "num senders", cache.CountSenders(), ) - transactions, accumulatedGas := cache.doSelectTransactions(gasRequested, maxNum) + transactions, accumulatedGas := cache.doSelectTransactions(accountNonceProvider, gasRequested, maxNum) stopWatch.Stop("selection") @@ -274,22 +274,6 @@ func (cache *TxCache) UnRegisterHandler(string) { log.Error("TxCache.UnRegisterHandler is not implemented") } -// NotifyAccountNonce should be called by external components (such as interceptors and transactions processor) -// in order to inform the cache about initial nonce gap phenomena -func (cache *TxCache) NotifyAccountNonce(accountKey []byte, nonce uint64) { - log.Trace("TxCache.NotifyAccountNonce", "account", accountKey, "nonce", nonce) - - cache.txListBySender.notifyAccountNonce(accountKey, nonce) -} - -// ForgetAllAccountNonces clears all known account nonces. -// Should be called when a block is reverted. -func (cache *TxCache) ForgetAllAccountNonces() { - log.Debug("TxCache.ForgetAllAccountNonces", "name", cache.name) - - cache.txListBySender.forgetAllAccountNonces() -} - // ImmunizeTxsAgainstEviction does nothing for this type of cache func (cache *TxCache) ImmunizeTxsAgainstEviction(_ [][]byte) { } From 3714684a13969f05254b79adcfeb40d227f3fa9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Wed, 13 Nov 2024 23:05:28 +0200 Subject: [PATCH 03/34] Refactor, pass "AccountNonceProvider" in constructor. --- common/errors.go | 4 ++ .../txcachemocks/accountNonceProviderMock.go | 28 ++++++++++++ txcache/eviction_test.go | 22 ++++++--- txcache/selection.go | 2 +- txcache/selection_test.go | 7 +-- txcache/txCache.go | 10 +++-- txcache/txCache_test.go | 45 ++++++++++++------- 7 files changed, 87 insertions(+), 31 deletions(-) create mode 100644 testscommon/txcachemocks/accountNonceProviderMock.go diff --git a/common/errors.go b/common/errors.go index 6342eb4f..326c7d78 100644 --- a/common/errors.go +++ b/common/errors.go @@ -2,6 +2,7 @@ package common import ( "errors" + "github.com/multiversx/mx-chain-core-go/core" ) @@ -74,6 +75,9 @@ var ErrNilTimeCache = errors.New("nil time cache") // ErrNilTxGasHandler signals that a nil tx gas handler was provided var ErrNilTxGasHandler = errors.New("nil tx gas handler") +// ErrNilAccountNonceProvider signals that a nil account nonce provider was provided +var ErrNilAccountNonceProvider = errors.New("nil account nonce provider") + // ErrNilStoredDataFactory signals that a nil stored data factory has been provided var ErrNilStoredDataFactory = errors.New("nil stored data factory") diff --git a/testscommon/txcachemocks/accountNonceProviderMock.go b/testscommon/txcachemocks/accountNonceProviderMock.go new file mode 100644 index 00000000..1b222de4 --- /dev/null +++ b/testscommon/txcachemocks/accountNonceProviderMock.go @@ -0,0 +1,28 @@ +package txcachemocks + +import ( + "errors" +) + +type accountNonceProviderMock struct { + GetAccountNonceCalled func(address []byte) (uint64, error) +} + +// NewAccountNonceProviderMock - +func NewAccountNonceProviderMock() *accountNonceProviderMock { + return &accountNonceProviderMock{} +} + +// GetAccountNonce - +func (stub *accountNonceProviderMock) GetAccountNonce(address []byte) (uint64, error) { + if stub.GetAccountNonceCalled != nil { + return stub.GetAccountNonceCalled(address) + } + + return 0, errors.New("GetAccountNonceCalled is not set") +} + +// IsInterfaceNil - +func (stub *accountNonceProviderMock) IsInterfaceNil() bool { + return stub == nil +} diff --git a/txcache/eviction_test.go b/txcache/eviction_test.go index fe3de7d3..d3305810 100644 --- a/txcache/eviction_test.go +++ b/txcache/eviction_test.go @@ -21,8 +21,11 @@ func TestTxCache_DoEviction_BecauseOfCount(t *testing.T) { EvictionEnabled: true, NumItemsToPreemptivelyEvict: 1, } + txGasHandler := txcachemocks.NewTxGasHandlerMock() - cache, err := NewTxCache(config, txGasHandler) + accountNonceProvider := txcachemocks.NewAccountNonceProviderMock() + + cache, err := NewTxCache(config, txGasHandler, accountNonceProvider) require.Nil(t, err) require.NotNil(t, cache) @@ -56,7 +59,9 @@ func TestTxCache_DoEviction_BecauseOfSize(t *testing.T) { } txGasHandler := txcachemocks.NewTxGasHandlerMock() - cache, err := NewTxCache(config, txGasHandler) + accountNonceProvider := txcachemocks.NewAccountNonceProviderMock() + + cache, err := NewTxCache(config, txGasHandler, accountNonceProvider) require.Nil(t, err) require.NotNil(t, cache) @@ -91,7 +96,9 @@ func TestTxCache_DoEviction_DoesNothingWhenAlreadyInProgress(t *testing.T) { } txGasHandler := txcachemocks.NewTxGasHandlerMock() - cache, err := NewTxCache(config, txGasHandler) + accountNonceProvider := txcachemocks.NewAccountNonceProviderMock() + + cache, err := NewTxCache(config, txGasHandler, accountNonceProvider) require.Nil(t, err) require.NotNil(t, cache) @@ -129,11 +136,12 @@ func TestBenchmarkTxCache_DoEviction(t *testing.T) { } txGasHandler := txcachemocks.NewTxGasHandlerMock() + accountNonceProvider := txcachemocks.NewAccountNonceProviderMock() sw := core.NewStopWatch() t.Run("numSenders = 35000, numTransactions = 10", func(t *testing.T) { - cache, err := NewTxCache(config, txGasHandler) + cache, err := NewTxCache(config, txGasHandler, accountNonceProvider) require.Nil(t, err) cache.config.EvictionEnabled = false @@ -151,7 +159,7 @@ func TestBenchmarkTxCache_DoEviction(t *testing.T) { }) t.Run("numSenders = 100000, numTransactions = 5", func(t *testing.T) { - cache, err := NewTxCache(config, txGasHandler) + cache, err := NewTxCache(config, txGasHandler, accountNonceProvider) require.Nil(t, err) cache.config.EvictionEnabled = false @@ -169,7 +177,7 @@ func TestBenchmarkTxCache_DoEviction(t *testing.T) { }) t.Run("numSenders = 400000, numTransactions = 1", func(t *testing.T) { - cache, err := NewTxCache(config, txGasHandler) + cache, err := NewTxCache(config, txGasHandler, accountNonceProvider) require.Nil(t, err) cache.config.EvictionEnabled = false @@ -187,7 +195,7 @@ func TestBenchmarkTxCache_DoEviction(t *testing.T) { }) t.Run("numSenders = 10000, numTransactions = 100", func(t *testing.T) { - cache, err := NewTxCache(config, txGasHandler) + cache, err := NewTxCache(config, txGasHandler, accountNonceProvider) require.Nil(t, err) cache.config.EvictionEnabled = false diff --git a/txcache/selection.go b/txcache/selection.go index 71ea3790..0d9739bd 100644 --- a/txcache/selection.go +++ b/txcache/selection.go @@ -4,7 +4,7 @@ import ( "container/heap" ) -func (cache *TxCache) doSelectTransactions(accountNonceProvider AccountNonceProvider, gasRequested uint64, maxNum int) (bunchOfTransactions, uint64) { +func (cache *TxCache) doSelectTransactions(gasRequested uint64, maxNum int) (bunchOfTransactions, uint64) { senders := cache.getSenders() bunches := make([]bunchOfTransactions, 0, len(senders)) diff --git a/txcache/selection_test.go b/txcache/selection_test.go index 5d358e26..30da61b8 100644 --- a/txcache/selection_test.go +++ b/txcache/selection_test.go @@ -243,11 +243,12 @@ func TestBenchmarktTxCache_doSelectTransactions(t *testing.T) { } txGasHandler := txcachemocks.NewTxGasHandlerMock() + accountNonceProvider := txcachemocks.NewAccountNonceProviderMock() sw := core.NewStopWatch() t.Run("numSenders = 50000, numTransactions = 2, maxNum = 50_000", func(t *testing.T) { - cache, err := NewTxCache(config, txGasHandler) + cache, err := NewTxCache(config, txGasHandler, accountNonceProvider) require.Nil(t, err) addManyTransactionsWithUniformDistribution(cache, 50000, 2) @@ -263,7 +264,7 @@ func TestBenchmarktTxCache_doSelectTransactions(t *testing.T) { }) t.Run("numSenders = 100000, numTransactions = 1, maxNum = 50_000", func(t *testing.T) { - cache, err := NewTxCache(config, txGasHandler) + cache, err := NewTxCache(config, txGasHandler, accountNonceProvider) require.Nil(t, err) addManyTransactionsWithUniformDistribution(cache, 100000, 1) @@ -279,7 +280,7 @@ func TestBenchmarktTxCache_doSelectTransactions(t *testing.T) { }) t.Run("numSenders = 300000, numTransactions = 1, maxNum = 50_000", func(t *testing.T) { - cache, err := NewTxCache(config, txGasHandler) + cache, err := NewTxCache(config, txGasHandler, accountNonceProvider) require.Nil(t, err) addManyTransactionsWithUniformDistribution(cache, 300000, 1) diff --git a/txcache/txCache.go b/txcache/txCache.go index 23c9809e..77c02caa 100644 --- a/txcache/txCache.go +++ b/txcache/txCache.go @@ -20,13 +20,14 @@ type TxCache struct { txByHash *txByHashMap config ConfigSourceMe txGasHandler TxGasHandler + accountNonceProvider AccountNonceProvider evictionMutex sync.Mutex isEvictionInProgress atomic.Flag mutTxOperation sync.Mutex } // NewTxCache creates a new transaction cache -func NewTxCache(config ConfigSourceMe, txGasHandler TxGasHandler) (*TxCache, error) { +func NewTxCache(config ConfigSourceMe, txGasHandler TxGasHandler, accountNonceProvider AccountNonceProvider) (*TxCache, error) { log.Debug("NewTxCache", "config", config.String()) monitoring.MonitorNewCache(config.Name, uint64(config.NumBytesThreshold)) @@ -37,6 +38,9 @@ func NewTxCache(config ConfigSourceMe, txGasHandler TxGasHandler) (*TxCache, err if check.IfNil(txGasHandler) { return nil, common.ErrNilTxGasHandler } + if check.IfNil(accountNonceProvider) { + return nil, common.ErrNilAccountNonceProvider + } // Note: for simplicity, we use the same "numChunks" for both internal concurrent maps numChunks := config.NumChunks @@ -99,7 +103,7 @@ func (cache *TxCache) GetByTxHash(txHash []byte) (*WrappedTransaction, bool) { // SelectTransactions selects the best transactions to be included in the next miniblock. // It returns up to "maxNum" transactions, with total gas <= "gasRequested". -func (cache *TxCache) SelectTransactions(accountNonceProvider AccountNonceProvider, gasRequested uint64, maxNum int) ([]*WrappedTransaction, uint64) { +func (cache *TxCache) SelectTransactions(gasRequested uint64, maxNum int) ([]*WrappedTransaction, uint64) { stopWatch := core.NewStopWatch() stopWatch.Start("selection") @@ -110,7 +114,7 @@ func (cache *TxCache) SelectTransactions(accountNonceProvider AccountNonceProvid "num senders", cache.CountSenders(), ) - transactions, accumulatedGas := cache.doSelectTransactions(accountNonceProvider, gasRequested, maxNum) + transactions, accumulatedGas := cache.doSelectTransactions(gasRequested, maxNum) stopWatch.Stop("selection") diff --git a/txcache/txCache_test.go b/txcache/txCache_test.go index e9038543..d7c7dd4e 100644 --- a/txcache/txCache_test.go +++ b/txcache/txCache_test.go @@ -29,43 +29,49 @@ func Test_NewTxCache(t *testing.T) { } txGasHandler := txcachemocks.NewTxGasHandlerMock() + accountNonceProvider := txcachemocks.NewAccountNonceProviderMock() - cache, err := NewTxCache(config, txGasHandler) + cache, err := NewTxCache(config, txGasHandler, accountNonceProvider) require.Nil(t, err) require.NotNil(t, cache) badConfig := config badConfig.Name = "" - requireErrorOnNewTxCache(t, badConfig, common.ErrInvalidConfig, "config.Name", txGasHandler) + requireErrorOnNewTxCache(t, badConfig, common.ErrInvalidConfig, "config.Name", txGasHandler, accountNonceProvider) badConfig = config badConfig.NumChunks = 0 - requireErrorOnNewTxCache(t, badConfig, common.ErrInvalidConfig, "config.NumChunks", txGasHandler) + requireErrorOnNewTxCache(t, badConfig, common.ErrInvalidConfig, "config.NumChunks", txGasHandler, accountNonceProvider) badConfig = config badConfig.NumBytesPerSenderThreshold = 0 - requireErrorOnNewTxCache(t, badConfig, common.ErrInvalidConfig, "config.NumBytesPerSenderThreshold", txGasHandler) + requireErrorOnNewTxCache(t, badConfig, common.ErrInvalidConfig, "config.NumBytesPerSenderThreshold", txGasHandler, accountNonceProvider) badConfig = config badConfig.CountPerSenderThreshold = 0 - requireErrorOnNewTxCache(t, badConfig, common.ErrInvalidConfig, "config.CountPerSenderThreshold", txGasHandler) + requireErrorOnNewTxCache(t, badConfig, common.ErrInvalidConfig, "config.CountPerSenderThreshold", txGasHandler, accountNonceProvider) badConfig = config - cache, err = NewTxCache(config, nil) + cache, err = NewTxCache(config, nil, accountNonceProvider) require.Nil(t, cache) require.Equal(t, common.ErrNilTxGasHandler, err) + badConfig = config + cache, err = NewTxCache(config, txGasHandler, nil) + require.Nil(t, cache) + require.Equal(t, common.ErrNilAccountNonceProvider, err) + badConfig = config badConfig.NumBytesThreshold = 0 - requireErrorOnNewTxCache(t, badConfig, common.ErrInvalidConfig, "config.NumBytesThreshold", txGasHandler) + requireErrorOnNewTxCache(t, badConfig, common.ErrInvalidConfig, "config.NumBytesThreshold", txGasHandler, accountNonceProvider) badConfig = config badConfig.CountThreshold = 0 - requireErrorOnNewTxCache(t, badConfig, common.ErrInvalidConfig, "config.CountThreshold", txGasHandler) + requireErrorOnNewTxCache(t, badConfig, common.ErrInvalidConfig, "config.CountThreshold", txGasHandler, accountNonceProvider) } -func requireErrorOnNewTxCache(t *testing.T, config ConfigSourceMe, errExpected error, errPartialMessage string, txGasHandler TxGasHandler) { - cache, errReceived := NewTxCache(config, txGasHandler) +func requireErrorOnNewTxCache(t *testing.T, config ConfigSourceMe, errExpected error, errPartialMessage string, txGasHandler TxGasHandler, accountNonceProvider AccountNonceProvider) { + cache, errReceived := NewTxCache(config, txGasHandler, accountNonceProvider) require.Nil(t, cache) require.True(t, errors.Is(errReceived, errExpected)) require.Contains(t, errReceived.Error(), errPartialMessage) @@ -312,6 +318,7 @@ func Test_Keys(t *testing.T) { func Test_AddWithEviction_UniformDistributionOfTxsPerSender(t *testing.T) { txGasHandler := txcachemocks.NewTxGasHandlerMock() + accountNonceProvider := txcachemocks.NewAccountNonceProviderMock() t.Run("numSenders = 11, numTransactions = 10, countThreshold = 100, numItemsToPreemptivelyEvict = 1", func(t *testing.T) { config := ConfigSourceMe{ @@ -325,7 +332,7 @@ func Test_AddWithEviction_UniformDistributionOfTxsPerSender(t *testing.T) { NumItemsToPreemptivelyEvict: 1, } - cache, err := NewTxCache(config, txGasHandler) + cache, err := NewTxCache(config, txGasHandler, accountNonceProvider) require.Nil(t, err) require.NotNil(t, cache) @@ -349,7 +356,7 @@ func Test_AddWithEviction_UniformDistributionOfTxsPerSender(t *testing.T) { NumItemsToPreemptivelyEvict: 3, } - cache, err := NewTxCache(config, txGasHandler) + cache, err := NewTxCache(config, txGasHandler, accountNonceProvider) require.Nil(t, err) require.NotNil(t, cache) @@ -369,7 +376,7 @@ func Test_AddWithEviction_UniformDistributionOfTxsPerSender(t *testing.T) { NumItemsToPreemptivelyEvict: 2, } - cache, err := NewTxCache(config, txGasHandler) + cache, err := NewTxCache(config, txGasHandler, accountNonceProvider) require.Nil(t, err) require.NotNil(t, cache) @@ -389,7 +396,7 @@ func Test_AddWithEviction_UniformDistributionOfTxsPerSender(t *testing.T) { NumItemsToPreemptivelyEvict: 1, } - cache, err := NewTxCache(config, txGasHandler) + cache, err := NewTxCache(config, txGasHandler, accountNonceProvider) require.Nil(t, err) require.NotNil(t, cache) @@ -409,7 +416,7 @@ func Test_AddWithEviction_UniformDistributionOfTxsPerSender(t *testing.T) { NumItemsToPreemptivelyEvict: 10000, } - cache, err := NewTxCache(config, txGasHandler) + cache, err := NewTxCache(config, txGasHandler, accountNonceProvider) require.Nil(t, err) require.NotNil(t, cache) @@ -554,6 +561,8 @@ func TestTxCache_NoCriticalInconsistency_WhenConcurrentAdditionsAndRemovals(t *t func newUnconstrainedCacheToTest() *TxCache { txGasHandler := txcachemocks.NewTxGasHandlerMock() + accountNonceProvider := txcachemocks.NewAccountNonceProviderMock() + cache, err := NewTxCache(ConfigSourceMe{ Name: "test", NumChunks: 16, @@ -563,7 +572,7 @@ func newUnconstrainedCacheToTest() *TxCache { CountPerSenderThreshold: math.MaxUint32, EvictionEnabled: false, NumItemsToPreemptivelyEvict: 1, - }, txGasHandler) + }, txGasHandler, accountNonceProvider) if err != nil { panic(fmt.Sprintf("newUnconstrainedCacheToTest(): %s", err)) } @@ -573,6 +582,8 @@ func newUnconstrainedCacheToTest() *TxCache { func newCacheToTest(numBytesPerSenderThreshold uint32, countPerSenderThreshold uint32) *TxCache { txGasHandler := txcachemocks.NewTxGasHandlerMock() + accountNonceProvider := txcachemocks.NewAccountNonceProviderMock() + cache, err := NewTxCache(ConfigSourceMe{ Name: "test", NumChunks: 16, @@ -582,7 +593,7 @@ func newCacheToTest(numBytesPerSenderThreshold uint32, countPerSenderThreshold u CountPerSenderThreshold: countPerSenderThreshold, EvictionEnabled: false, NumItemsToPreemptivelyEvict: 1, - }, txGasHandler) + }, txGasHandler, accountNonceProvider) if err != nil { panic(fmt.Sprintf("newCacheToTest(): %s", err)) } From 74e7ac5a203ddb22a1b157a9d37b5f36c0fba22d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Thu, 14 Nov 2024 00:18:37 +0200 Subject: [PATCH 04/34] Quick sketch of selection changes. --- txcache/selection.go | 38 +++++++++++++++++++++++++++++++++---- txcache/selection_test.go | 15 ++++++++++----- txcache/transactionsHeap.go | 7 ++++++- txcache/txCache.go | 11 ++++++----- 4 files changed, 56 insertions(+), 15 deletions(-) diff --git a/txcache/selection.go b/txcache/selection.go index 0d9739bd..12b7a11c 100644 --- a/txcache/selection.go +++ b/txcache/selection.go @@ -12,11 +12,11 @@ func (cache *TxCache) doSelectTransactions(gasRequested uint64, maxNum int) (bun bunches = append(bunches, sender.getSequentialTxs()) } - return selectTransactionsFromBunches(bunches, gasRequested, maxNum) + return cache.selectTransactionsFromBunches(bunches, gasRequested, maxNum) } // Selection tolerates concurrent transaction additions / removals. -func selectTransactionsFromBunches(bunches []bunchOfTransactions, gasRequested uint64, maxNum int) (bunchOfTransactions, uint64) { +func (cache *TxCache) selectTransactionsFromBunches(bunches []bunchOfTransactions, gasRequested uint64, maxNum int) (bunchOfTransactions, uint64) { selectedTransactions := make(bunchOfTransactions, 0, initialCapacityOfSelectionSlice) // Items popped from the heap are added to "selectedTransactions". @@ -45,6 +45,7 @@ func selectTransactionsFromBunches(bunches []bunchOfTransactions, gasRequested u // Always pick the best transaction. item := heap.Pop(transactionsHeap).(*transactionsHeapItem) gasLimit := item.transaction.Tx.GetGasLimit() + nonce := item.transaction.Tx.GetNonce() if accumulatedGas+gasLimit > gasRequested { break @@ -52,9 +53,38 @@ func selectTransactionsFromBunches(bunches []bunchOfTransactions, gasRequested u if len(selectedTransactions) >= maxNum { break } + if !item.senderNonceAsked { + item.senderNonceAsked = true - accumulatedGas += gasLimit - selectedTransactions = append(selectedTransactions, item.transaction) + sender := item.transaction.Tx.GetSndAddr() + senderNonce, err := cache.accountNonceProvider.GetAccountNonce(sender) + if err != nil { + // Hazardous; should never happen. + logSelect.Debug("TxCache.selectTransactionsFromBunches: nonce not available", "sender", sender, "err", err) + } else { + item.senderNonceTold = true + item.senderNonce = senderNonce + } + } + + isInitialGap := item.transactionIndex == 0 && item.senderNonceTold && nonce > item.senderNonce + if isInitialGap { + sender := item.transaction.Tx.GetSndAddr() + log.Trace("TxCache.selectTransactionsFromBunches, initial gap", "sender", sender, "nonce", nonce, "senderNonce", item.senderNonce) + + // Item was popped from the heap, but not used downstream. + // Therefore, the sender is completely ignored in the current selection session. + continue + } + + isLowerNonce := item.senderNonceTold && nonce < item.senderNonce + if isLowerNonce { + sender := item.transaction.Tx.GetSndAddr() + log.Trace("TxCache.selectTransactionsFromBunches, lower nonce", "sender", sender, "nonce", nonce, "senderNonce", item.senderNonce) + } else { + accumulatedGas += gasLimit + selectedTransactions = append(selectedTransactions, item.transaction) + } // 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). diff --git a/txcache/selection_test.go b/txcache/selection_test.go index 30da61b8..3672c3e0 100644 --- a/txcache/selection_test.go +++ b/txcache/selection_test.go @@ -158,7 +158,8 @@ func TestTxCache_SelectTransactions_WhenTransactionsAddedInReversedNonceOrder(t func TestTxCache_selectTransactionsFromBunches(t *testing.T) { t.Run("empty cache", func(t *testing.T) { - merged, accumulatedGas := selectTransactionsFromBunches([]bunchOfTransactions{}, 10_000_000_000, math.MaxInt) + cache := newUnconstrainedCacheToTest() + merged, accumulatedGas := cache.selectTransactionsFromBunches([]bunchOfTransactions{}, 10_000_000_000, math.MaxInt) require.Equal(t, 0, len(merged)) require.Equal(t, uint64(0), accumulatedGas) @@ -169,10 +170,11 @@ func TestBenchmarkTxCache_selectTransactionsFromBunches(t *testing.T) { sw := core.NewStopWatch() t.Run("numSenders = 1000, numTransactions = 1000", func(t *testing.T) { + cache := newUnconstrainedCacheToTest() bunches := createBunchesOfTransactionsWithUniformDistribution(1000, 1000) sw.Start(t.Name()) - merged, accumulatedGas := selectTransactionsFromBunches(bunches, 10_000_000_000, math.MaxInt) + merged, accumulatedGas := cache.selectTransactionsFromBunches(bunches, 10_000_000_000, math.MaxInt) sw.Stop(t.Name()) require.Equal(t, 200000, len(merged)) @@ -180,10 +182,11 @@ func TestBenchmarkTxCache_selectTransactionsFromBunches(t *testing.T) { }) t.Run("numSenders = 10000, numTransactions = 100", func(t *testing.T) { + cache := newUnconstrainedCacheToTest() bunches := createBunchesOfTransactionsWithUniformDistribution(1000, 1000) sw.Start(t.Name()) - merged, accumulatedGas := selectTransactionsFromBunches(bunches, 10_000_000_000, math.MaxInt) + merged, accumulatedGas := cache.selectTransactionsFromBunches(bunches, 10_000_000_000, math.MaxInt) sw.Stop(t.Name()) require.Equal(t, 200000, len(merged)) @@ -191,10 +194,11 @@ func TestBenchmarkTxCache_selectTransactionsFromBunches(t *testing.T) { }) t.Run("numSenders = 100000, numTransactions = 3", func(t *testing.T) { + cache := newUnconstrainedCacheToTest() bunches := createBunchesOfTransactionsWithUniformDistribution(100000, 3) sw.Start(t.Name()) - merged, accumulatedGas := selectTransactionsFromBunches(bunches, 10_000_000_000, math.MaxInt) + merged, accumulatedGas := cache.selectTransactionsFromBunches(bunches, 10_000_000_000, math.MaxInt) sw.Stop(t.Name()) require.Equal(t, 200000, len(merged)) @@ -202,10 +206,11 @@ func TestBenchmarkTxCache_selectTransactionsFromBunches(t *testing.T) { }) t.Run("numSenders = 300000, numTransactions = 1", func(t *testing.T) { + cache := newUnconstrainedCacheToTest() bunches := createBunchesOfTransactionsWithUniformDistribution(300000, 1) sw.Start(t.Name()) - merged, accumulatedGas := selectTransactionsFromBunches(bunches, 10_000_000_000, math.MaxInt) + merged, accumulatedGas := cache.selectTransactionsFromBunches(bunches, 10_000_000_000, math.MaxInt) sw.Stop(t.Name()) require.Equal(t, 200000, len(merged)) diff --git a/txcache/transactionsHeap.go b/txcache/transactionsHeap.go index 9693f10b..099ee5d5 100644 --- a/txcache/transactionsHeap.go +++ b/txcache/transactionsHeap.go @@ -6,7 +6,12 @@ type transactionsHeap struct { } type transactionsHeapItem struct { - senderIndex int + senderIndex int + + senderNonceAsked bool + senderNonceTold bool + senderNonce uint64 + transactionIndex int transaction *WrappedTransaction } diff --git a/txcache/txCache.go b/txcache/txCache.go index 77c02caa..c9414bb2 100644 --- a/txcache/txCache.go +++ b/txcache/txCache.go @@ -47,11 +47,12 @@ func NewTxCache(config ConfigSourceMe, txGasHandler TxGasHandler, accountNoncePr senderConstraintsObj := config.getSenderConstraints() txCache := &TxCache{ - name: config.Name, - txListBySender: newTxListBySenderMap(numChunks, senderConstraintsObj), - txByHash: newTxByHashMap(numChunks), - config: config, - txGasHandler: txGasHandler, + name: config.Name, + txListBySender: newTxListBySenderMap(numChunks, senderConstraintsObj), + txByHash: newTxByHashMap(numChunks), + config: config, + txGasHandler: txGasHandler, + accountNonceProvider: accountNonceProvider, } return txCache, nil From d583eaee4097dfa8a455905afa14aff11d0c03a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Thu, 14 Nov 2024 00:24:56 +0200 Subject: [PATCH 05/34] Fix tests and comments. --- txcache/txListForSender.go | 8 +++---- txcache/txListForSender_test.go | 42 +++++++++++++-------------------- 2 files changed, 21 insertions(+), 29 deletions(-) diff --git a/txcache/txListForSender.go b/txcache/txListForSender.go index 11cb0044..6277e3c1 100644 --- a/txcache/txListForSender.go +++ b/txcache/txListForSender.go @@ -185,7 +185,8 @@ func (listForSender *txListForSender) getTxsReversed() []*WrappedTransaction { } // getSequentialTxs returns the transactions of the sender, in the context of transactions selection. -// Thus, gaps and duplicates are handled (affected transactions are excluded). +// Middle gaps and duplicates are handled (affected transactions are excluded). +// Initial gaps and lower nonces are not handled (not enough information); they are detected a bit later, within the selection loop. func (listForSender *txListForSender) getSequentialTxs() []*WrappedTransaction { listForSender.mutex.RLock() defer listForSender.mutex.RUnlock() @@ -196,10 +197,9 @@ func (listForSender *txListForSender) getSequentialTxs() []*WrappedTransaction { for element := listForSender.items.Front(); element != nil; element = element.Next() { value := element.Value.(*WrappedTransaction) nonce := value.Tx.GetNonce() - isFirstTx := len(result) == 0 - if isFirstTx { - } else { + isFirstTx := len(result) == 0 + if !isFirstTx { // Handle duplicates (only transactions with the highest gas price are included; see "findInsertionPlace"). if nonce == previousNonce { log.Trace("txListForSender.getSequentialTxs, duplicate", "sender", listForSender.sender, "nonce", nonce) diff --git a/txcache/txListForSender_test.go b/txcache/txListForSender_test.go index b9addd4a..b59e8a65 100644 --- a/txcache/txListForSender_test.go +++ b/txcache/txListForSender_test.go @@ -136,27 +136,35 @@ func TestListForSender_getTxs(t *testing.T) { require.Len(t, list.getSequentialTxs(), 0) }) - t.Run("one transaction, one gap", func(t *testing.T) { + t.Run("with middle gaps", func(t *testing.T) { list := newUnconstrainedListToTest() - // Gap - list.AddTx(createTx([]byte("tx-43"), ".", 43)) + // One transaction (no information about gaps) + list.AddTx(createTx([]byte("tx-42"), ".", 42)) require.Len(t, list.getTxs(), 1) require.Len(t, list.getTxsReversed(), 1) - require.Len(t, list.getSequentialTxs(), 0) + require.Len(t, list.getSequentialTxs(), 1) - // Resolve gap - list.AddTx(createTx([]byte("tx-42"), ".", 42)) + // Middle gap + list.AddTx(createTx([]byte("tx-44"), ".", 44)) require.Len(t, list.getTxs(), 2) require.Len(t, list.getTxsReversed(), 2) - require.Len(t, list.getSequentialTxs(), 2) + require.Len(t, list.getSequentialTxs(), 1) + + // Resolve gap + list.AddTx(createTx([]byte("tx-43"), ".", 43)) + require.Len(t, list.getTxs(), 3) + require.Len(t, list.getTxsReversed(), 3) + require.Len(t, list.getSequentialTxs(), 3) require.Equal(t, []byte("tx-42"), list.getTxs()[0].TxHash) require.Equal(t, []byte("tx-43"), list.getTxs()[1].TxHash) + require.Equal(t, []byte("tx-44"), list.getTxs()[2].TxHash) require.Equal(t, list.getTxs(), list.getSequentialTxs()) - require.Equal(t, []byte("tx-43"), list.getTxsReversed()[0].TxHash) - require.Equal(t, []byte("tx-42"), list.getTxsReversed()[1].TxHash) + require.Equal(t, []byte("tx-44"), list.getTxsReversed()[0].TxHash) + require.Equal(t, []byte("tx-43"), list.getTxsReversed()[1].TxHash) + require.Equal(t, []byte("tx-42"), list.getTxsReversed()[2].TxHash) }) t.Run("with nonce duplicates", func(t *testing.T) { @@ -185,22 +193,6 @@ func TestListForSender_getTxs(t *testing.T) { require.Equal(t, []byte("tx-42"), list.getTxsReversed()[2].TxHash) require.Equal(t, []byte("tx-42++"), list.getTxsReversed()[3].TxHash) }) - - t.Run("with lower nonces", func(t *testing.T) { - list := newUnconstrainedListToTest() - - list.AddTx(createTx([]byte("tx-42"), ".", 42)) - list.AddTx(createTx([]byte("tx-43"), ".", 43)) - - require.Len(t, list.getTxs(), 2) - require.Len(t, list.getTxsReversed(), 2) - require.Len(t, list.getSequentialTxs(), 1) - require.Equal(t, []byte("tx-43"), list.getSequentialTxs()[0].TxHash) - - require.Len(t, list.getTxs(), 2) - require.Len(t, list.getTxsReversed(), 2) - require.Len(t, list.getSequentialTxs(), 2) - }) } func TestListForSender_DetectRaceConditions(t *testing.T) { From 94131c745ba71464b3efb6b20aade44edd389c18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Thu, 14 Nov 2024 00:29:59 +0200 Subject: [PATCH 06/34] Refactoring. --- txcache/selection.go | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/txcache/selection.go b/txcache/selection.go index 12b7a11c..4860aadc 100644 --- a/txcache/selection.go +++ b/txcache/selection.go @@ -53,19 +53,8 @@ func (cache *TxCache) selectTransactionsFromBunches(bunches []bunchOfTransaction if len(selectedTransactions) >= maxNum { break } - if !item.senderNonceAsked { - item.senderNonceAsked = true - sender := item.transaction.Tx.GetSndAddr() - senderNonce, err := cache.accountNonceProvider.GetAccountNonce(sender) - if err != nil { - // Hazardous; should never happen. - logSelect.Debug("TxCache.selectTransactionsFromBunches: nonce not available", "sender", sender, "err", err) - } else { - item.senderNonceTold = true - item.senderNonce = senderNonce - } - } + cache.askAboutAccountNonceIfNecessary(item) isInitialGap := item.transactionIndex == 0 && item.senderNonceTold && nonce > item.senderNonce if isInitialGap { @@ -81,6 +70,8 @@ func (cache *TxCache) selectTransactionsFromBunches(bunches []bunchOfTransaction if isLowerNonce { sender := item.transaction.Tx.GetSndAddr() log.Trace("TxCache.selectTransactionsFromBunches, lower nonce", "sender", sender, "nonce", nonce, "senderNonce", item.senderNonce) + + // Transaction isn't selected, but the sender is still in the game (will contribute with other transactions). } else { accumulatedGas += gasLimit selectedTransactions = append(selectedTransactions, item.transaction) @@ -99,3 +90,22 @@ func (cache *TxCache) selectTransactionsFromBunches(bunches []bunchOfTransaction return selectedTransactions, accumulatedGas } + +func (cache *TxCache) askAboutAccountNonceIfNecessary(item *transactionsHeapItem) { + if item.senderNonceAsked { + return + } + + item.senderNonceAsked = true + + sender := item.transaction.Tx.GetSndAddr() + senderNonce, err := cache.accountNonceProvider.GetAccountNonce(sender) + if err != nil { + // Hazardous; should never happen. + logSelect.Debug("TxCache.selectTransactionsFromBunches: nonce not available", "sender", sender, "err", err) + return + } + + item.senderNonceTold = true + item.senderNonce = senderNonce +} From 4ca46fd08e118a17239b0f6169ac4f86e40a1c83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Thu, 14 Nov 2024 00:50:50 +0200 Subject: [PATCH 07/34] Refactor, optimize. --- txcache/selection.go | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/txcache/selection.go b/txcache/selection.go index 4860aadc..34bd40b6 100644 --- a/txcache/selection.go +++ b/txcache/selection.go @@ -2,6 +2,8 @@ package txcache import ( "container/heap" + + logger "github.com/multiversx/mx-chain-logger-go" ) func (cache *TxCache) doSelectTransactions(gasRequested uint64, maxNum int) (bunchOfTransactions, uint64) { @@ -58,8 +60,14 @@ func (cache *TxCache) selectTransactionsFromBunches(bunches []bunchOfTransaction isInitialGap := item.transactionIndex == 0 && item.senderNonceTold && nonce > item.senderNonce if isInitialGap { - sender := item.transaction.Tx.GetSndAddr() - log.Trace("TxCache.selectTransactionsFromBunches, initial gap", "sender", sender, "nonce", nonce, "senderNonce", item.senderNonce) + if logSelect.GetLevel() <= logger.LogTrace { + logSelect.Trace("TxCache.selectTransactionsFromBunches, initial gap", + "tx", item.transaction.TxHash, + "nonce", nonce, + "sender", item.transaction.Tx.GetSndAddr(), + "senderNonce", item.senderNonce, + ) + } // Item was popped from the heap, but not used downstream. // Therefore, the sender is completely ignored in the current selection session. @@ -68,8 +76,14 @@ func (cache *TxCache) selectTransactionsFromBunches(bunches []bunchOfTransaction isLowerNonce := item.senderNonceTold && nonce < item.senderNonce if isLowerNonce { - sender := item.transaction.Tx.GetSndAddr() - log.Trace("TxCache.selectTransactionsFromBunches, lower nonce", "sender", sender, "nonce", nonce, "senderNonce", item.senderNonce) + if logSelect.GetLevel() <= logger.LogTrace { + logSelect.Trace("TxCache.selectTransactionsFromBunches, lower nonce", + "tx", item.transaction.TxHash, + "nonce", nonce, + "sender", item.transaction.Tx.GetSndAddr(), + "senderNonce", item.senderNonce, + ) + } // Transaction isn't selected, but the sender is still in the game (will contribute with other transactions). } else { @@ -102,7 +116,7 @@ func (cache *TxCache) askAboutAccountNonceIfNecessary(item *transactionsHeapItem senderNonce, err := cache.accountNonceProvider.GetAccountNonce(sender) if err != nil { // Hazardous; should never happen. - logSelect.Debug("TxCache.selectTransactionsFromBunches: nonce not available", "sender", sender, "err", err) + logSelect.Debug("TxCache.askAboutAccountNonceIfNecessary: nonce not available", "sender", sender, "err", err) return } From 20d4ba45f9ed48698d281e767176a2ad1d3606ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Thu, 14 Nov 2024 10:15:24 +0200 Subject: [PATCH 08/34] Fix after self-review. --- txcache/transactionsHeap.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/txcache/transactionsHeap.go b/txcache/transactionsHeap.go index 099ee5d5..2e066521 100644 --- a/txcache/transactionsHeap.go +++ b/txcache/transactionsHeap.go @@ -8,9 +8,12 @@ type transactionsHeap struct { type transactionsHeapItem struct { senderIndex int + // Whether the sender's nonce has been asked within a selection session. senderNonceAsked bool - senderNonceTold bool - senderNonce uint64 + // Whether the sender's nonce has been asked and told (with success) within a selection session. + senderNonceTold bool + // The sender's nonce (if asked and told). + senderNonce uint64 transactionIndex int transaction *WrappedTransaction From ee6476d5403b4beb8bd3db662cc18d8a25c4396b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Thu, 14 Nov 2024 12:30:20 +0200 Subject: [PATCH 09/34] Update readme. --- txcache/README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/txcache/README.md b/txcache/README.md index 6330b033..39de2196 100644 --- a/txcache/README.md +++ b/txcache/README.md @@ -114,7 +114,7 @@ The mempool selects transactions as follows (pseudo-code): func selectTransactions(gasRequested, maxNum): // Setup phase senders := list of all current senders in the mempool, in an arbitrary order - bunchesOfTransactions := sourced from senders; nonces-gap-free, duplicates-free, nicely sorted by nonce + bunchesOfTransactions := sourced from senders; middle-nonces-gap-free, duplicates-free, nicely sorted by nonce // Holds selected transactions selectedTransactions := empty @@ -154,7 +154,7 @@ Thus, the mempool selects transactions using an efficient and value-driven algor - **Organize transactions into bunches:** - For each sender, collect all their pending transactions and organize them into a "bunch." - Each bunch is: - - **Nonce-gap-free:** There are no missing nonces between transactions. + - **Middle-nonces-gap-free:** There are no missing nonces between transactions. - **Duplicates-free:** No duplicate transactions are included. - **Sorted by nonce:** Transactions are ordered in ascending order based on their nonce values. @@ -181,6 +181,10 @@ Thus, the mempool selects transactions using an efficient and value-driven algor - The accumulated gas of selected transactions meets or exceeds `gasRequested`. - The number of selected transactions reaches `maxNum`. +**Additional notes:** + - Within the selection loop, the current nonce of the sender is queryied from the blockchain, if necessary. + - If an initial nonce gap is detected, the sender is excluded from the selection process. + - Transactions with nonces lower than the current nonce of the sender are skipped (not included in the selection). ### Paragraph 5 From 0f01e5c5f3c863ec2326235625400705ba1b4bff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Thu, 14 Nov 2024 14:26:18 +0200 Subject: [PATCH 10/34] Additional tests. --- .../txcachemocks/accountNonceProviderMock.go | 21 ++- txcache/selection_test.go | 125 +++++++++++++++--- 2 files changed, 120 insertions(+), 26 deletions(-) diff --git a/testscommon/txcachemocks/accountNonceProviderMock.go b/testscommon/txcachemocks/accountNonceProviderMock.go index 1b222de4..bc0abe71 100644 --- a/testscommon/txcachemocks/accountNonceProviderMock.go +++ b/testscommon/txcachemocks/accountNonceProviderMock.go @@ -4,25 +4,34 @@ import ( "errors" ) -type accountNonceProviderMock struct { +// AccountNonceProviderMock - +type AccountNonceProviderMock struct { + NoncesByAddress map[string]uint64 GetAccountNonceCalled func(address []byte) (uint64, error) } // NewAccountNonceProviderMock - -func NewAccountNonceProviderMock() *accountNonceProviderMock { - return &accountNonceProviderMock{} +func NewAccountNonceProviderMock() *AccountNonceProviderMock { + return &AccountNonceProviderMock{ + NoncesByAddress: make(map[string]uint64), + } } // GetAccountNonce - -func (stub *accountNonceProviderMock) GetAccountNonce(address []byte) (uint64, error) { +func (stub *AccountNonceProviderMock) GetAccountNonce(address []byte) (uint64, error) { if stub.GetAccountNonceCalled != nil { return stub.GetAccountNonceCalled(address) } - return 0, errors.New("GetAccountNonceCalled is not set") + nonce, ok := stub.NoncesByAddress[string(address)] + if !ok { + return 0, errors.New("cannot get nonce") + } + + return nonce, nil } // IsInterfaceNil - -func (stub *accountNonceProviderMock) IsInterfaceNil() bool { +func (stub *AccountNonceProviderMock) IsInterfaceNil() bool { return stub == nil } diff --git a/txcache/selection_test.go b/txcache/selection_test.go index b4ee8dd0..6c10f61c 100644 --- a/txcache/selection_test.go +++ b/txcache/selection_test.go @@ -82,26 +82,111 @@ func TestTxCache_SelectTransactionsWithBandwidth_Dummy(t *testing.T) { }) } -func TestTxCache_SelectTransactions_BreaksAtNonceGaps(t *testing.T) { +func TestTxCache_SelectTransactions_HandlesGapsAndLowerNonces(t *testing.T) { + t.Run("with middle gaps", func(t *testing.T) { + cache := newUnconstrainedCacheToTest() + + cache.AddTx(createTx([]byte("hash-alice-1"), "alice", 1)) + cache.AddTx(createTx([]byte("hash-alice-2"), "alice", 2)) + cache.AddTx(createTx([]byte("hash-alice-3"), "alice", 3)) + cache.AddTx(createTx([]byte("hash-alice-5"), "alice", 5)) // gap + cache.AddTx(createTx([]byte("hash-bob-42"), "bob", 42)) + cache.AddTx(createTx([]byte("hash-bob-44"), "bob", 44)) // gap + cache.AddTx(createTx([]byte("hash-bob-45"), "bob", 45)) + cache.AddTx(createTx([]byte("hash-carol-7"), "carol", 7)) + cache.AddTx(createTx([]byte("hash-carol-8"), "carol", 8)) + cache.AddTx(createTx([]byte("hash-carol-10"), "carol", 10)) // gap + cache.AddTx(createTx([]byte("hash-carol-11"), "carol", 11)) + + sorted, accumulatedGas := cache.SelectTransactions(math.MaxUint64, math.MaxInt) + expectedNumSelected := 3 + 1 + 2 // 3 alice + 1 bob + 2 carol + require.Len(t, sorted, expectedNumSelected) + require.Equal(t, 300000, int(accumulatedGas)) + }) + + t.Run("with initial gaps", func(t *testing.T) { + cache := newUnconstrainedCacheToTest() + noncesByAddress := cache.accountNonceProvider.(*txcachemocks.AccountNonceProviderMock).NoncesByAddress + noncesByAddress["alice"] = 1 + noncesByAddress["bob"] = 42 + + // No gap + cache.AddTx(createTx([]byte("hash-alice-1"), "alice", 1)) + cache.AddTx(createTx([]byte("hash-alice-2"), "alice", 2)) + cache.AddTx(createTx([]byte("hash-alice-3"), "alice", 3)) + + // Initial gap + cache.AddTx(createTx([]byte("hash-bob-42"), "bob", 44)) + cache.AddTx(createTx([]byte("hash-bob-43"), "bob", 45)) + cache.AddTx(createTx([]byte("hash-bob-44"), "bob", 46)) + + // Unknown + cache.AddTx(createTx([]byte("hash-carol-7"), "carol", 7)) + cache.AddTx(createTx([]byte("hash-carol-8"), "carol", 8)) + + sorted, accumulatedGas := cache.SelectTransactions(math.MaxUint64, math.MaxInt) + expectedNumSelected := 3 + 0 + 2 // 3 alice + 0 bob + 2 carol + require.Len(t, sorted, expectedNumSelected) + require.Equal(t, 250000, int(accumulatedGas)) + }) + + t.Run("with lower nonces", func(t *testing.T) { + cache := newUnconstrainedCacheToTest() + noncesByAddress := cache.accountNonceProvider.(*txcachemocks.AccountNonceProviderMock).NoncesByAddress + noncesByAddress["alice"] = 1 + noncesByAddress["bob"] = 42 + + // Good sequence + cache.AddTx(createTx([]byte("hash-alice-1"), "alice", 1)) + cache.AddTx(createTx([]byte("hash-alice-2"), "alice", 2)) + cache.AddTx(createTx([]byte("hash-alice-3"), "alice", 3)) + + // A few with lower nonce + cache.AddTx(createTx([]byte("hash-bob-42"), "bob", 40)) + cache.AddTx(createTx([]byte("hash-bob-43"), "bob", 41)) + cache.AddTx(createTx([]byte("hash-bob-44"), "bob", 42)) + + // Unknown + cache.AddTx(createTx([]byte("hash-carol-7"), "carol", 7)) + cache.AddTx(createTx([]byte("hash-carol-8"), "carol", 8)) + + sorted, accumulatedGas := cache.SelectTransactions(math.MaxUint64, math.MaxInt) + expectedNumSelected := 3 + 1 + 2 // 3 alice + 1 bob + 2 carol + require.Len(t, sorted, expectedNumSelected) + require.Equal(t, 300000, int(accumulatedGas)) + }) +} + +func TestTxCache_askAboutAccountNonceIfNecessary(t *testing.T) { cache := newUnconstrainedCacheToTest() + noncesByAddress := cache.accountNonceProvider.(*txcachemocks.AccountNonceProviderMock).NoncesByAddress + noncesByAddress["alice"] = 7 + noncesByAddress["bob"] = 42 - cache.AddTx(createTx([]byte("hash-alice-1"), "alice", 1)) - cache.AddTx(createTx([]byte("hash-alice-2"), "alice", 2)) - cache.AddTx(createTx([]byte("hash-alice-3"), "alice", 3)) - cache.AddTx(createTx([]byte("hash-alice-5"), "alice", 5)) - cache.AddTx(createTx([]byte("hash-bob-42"), "bob", 42)) - cache.AddTx(createTx([]byte("hash-bob-44"), "bob", 44)) - cache.AddTx(createTx([]byte("hash-bob-45"), "bob", 45)) - cache.AddTx(createTx([]byte("hash-carol-7"), "carol", 7)) - cache.AddTx(createTx([]byte("hash-carol-8"), "carol", 8)) - cache.AddTx(createTx([]byte("hash-carol-10"), "carol", 10)) - cache.AddTx(createTx([]byte("hash-carol-11"), "carol", 11)) + a := &transactionsHeapItem{ + transaction: createTx([]byte("hash-alice-1"), "alice", 1), + } - numSelected := 3 + 1 + 2 // 3 alice + 1 bob + 2 carol + b := &transactionsHeapItem{ + transaction: createTx([]byte("hash-bob-1"), "bob", 1), + } - sorted, accumulatedGas := cache.SelectTransactions(math.MaxUint64, math.MaxInt) - require.Len(t, sorted, numSelected) - require.Equal(t, 300000, int(accumulatedGas)) + c := &transactionsHeapItem{} + + cache.askAboutAccountNonceIfNecessary(a) + cache.askAboutAccountNonceIfNecessary(b) + + require.True(t, a.senderNonceAsked) + require.True(t, a.senderNonceTold) + require.Equal(t, uint64(7), a.senderNonce) + + require.True(t, b.senderNonceAsked) + require.True(t, b.senderNonceTold) + require.Equal(t, uint64(42), b.senderNonce) + + require.False(t, c.senderNonceAsked) + require.False(t, c.senderNonceTold) + require.Equal(t, uint64(0), c.senderNonce) } func TestTxCache_SelectTransactions_WhenTransactionsAddedInReversedNonceOrder(t *testing.T) { @@ -220,7 +305,7 @@ func TestBenchmarkTxCache_selectTransactionsFromBunches(t *testing.T) { // 0.219072s (TestTxCache_selectTransactionsFromBunches/numSenders_=_300000,_numTransactions_=_1) } -func TestBenchmarktTxCache_doSelectTransactions(t *testing.T) { +func TestBenchmarkTxCache_doSelectTransactions(t *testing.T) { config := ConfigSourceMe{ Name: "untitled", NumChunks: 16, @@ -297,7 +382,7 @@ func TestBenchmarktTxCache_doSelectTransactions(t *testing.T) { // Thread(s) per core: 2 // Core(s) per socket: 4 // - // 0.060508s (TestBenchmarktTxCache_doSelectTransactions/numSenders_=_50000,_numTransactions_=_2,_maxNum_=_50_000) - // 0.103369s (TestBenchmarktTxCache_doSelectTransactions/numSenders_=_100000,_numTransactions_=_1,_maxNum_=_50_000) - // 0.245621s (TestBenchmarktTxCache_doSelectTransactions/numSenders_=_300000,_numTransactions_=_1,_maxNum_=_50_000) + // 0.060508s (TestBenchmarkTxCache_doSelectTransactions/numSenders_=_50000,_numTransactions_=_2,_maxNum_=_50_000) + // 0.103369s (TestBenchmarkTxCache_doSelectTransactions/numSenders_=_100000,_numTransactions_=_1,_maxNum_=_50_000) + // 0.245621s (TestBenchmarkTxCache_doSelectTransactions/numSenders_=_300000,_numTransactions_=_1,_maxNum_=_50_000) } From 80b497930cda9b321cfa1b7c7af603828ceb52d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Thu, 14 Nov 2024 16:29:22 +0200 Subject: [PATCH 11/34] Fix after review. --- txcache/selection.go | 16 ++++++++-------- txcache/selection_test.go | 18 +++++++++--------- txcache/transactionsHeap.go | 10 +++++----- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/txcache/selection.go b/txcache/selection.go index 34bd40b6..e84c02df 100644 --- a/txcache/selection.go +++ b/txcache/selection.go @@ -56,9 +56,9 @@ func (cache *TxCache) selectTransactionsFromBunches(bunches []bunchOfTransaction break } - cache.askAboutAccountNonceIfNecessary(item) + cache.requestAccountNonceIfNecessary(item) - isInitialGap := item.transactionIndex == 0 && item.senderNonceTold && nonce > item.senderNonce + isInitialGap := item.transactionIndex == 0 && item.senderNonceProvided && nonce > item.senderNonce if isInitialGap { if logSelect.GetLevel() <= logger.LogTrace { logSelect.Trace("TxCache.selectTransactionsFromBunches, initial gap", @@ -74,7 +74,7 @@ func (cache *TxCache) selectTransactionsFromBunches(bunches []bunchOfTransaction continue } - isLowerNonce := item.senderNonceTold && nonce < item.senderNonce + isLowerNonce := item.senderNonceProvided && nonce < item.senderNonce if isLowerNonce { if logSelect.GetLevel() <= logger.LogTrace { logSelect.Trace("TxCache.selectTransactionsFromBunches, lower nonce", @@ -105,21 +105,21 @@ func (cache *TxCache) selectTransactionsFromBunches(bunches []bunchOfTransaction return selectedTransactions, accumulatedGas } -func (cache *TxCache) askAboutAccountNonceIfNecessary(item *transactionsHeapItem) { - if item.senderNonceAsked { +func (cache *TxCache) requestAccountNonceIfNecessary(item *transactionsHeapItem) { + if item.senderNonceRequested { return } - item.senderNonceAsked = true + item.senderNonceRequested = true sender := item.transaction.Tx.GetSndAddr() senderNonce, err := cache.accountNonceProvider.GetAccountNonce(sender) if err != nil { // Hazardous; should never happen. - logSelect.Debug("TxCache.askAboutAccountNonceIfNecessary: nonce not available", "sender", sender, "err", err) + logSelect.Debug("TxCache.requestAccountNonceIfNecessary: nonce not available", "sender", sender, "err", err) return } - item.senderNonceTold = true + item.senderNonceProvided = true item.senderNonce = senderNonce } diff --git a/txcache/selection_test.go b/txcache/selection_test.go index 6c10f61c..e1796244 100644 --- a/txcache/selection_test.go +++ b/txcache/selection_test.go @@ -157,7 +157,7 @@ func TestTxCache_SelectTransactions_HandlesGapsAndLowerNonces(t *testing.T) { }) } -func TestTxCache_askAboutAccountNonceIfNecessary(t *testing.T) { +func TestTxCache_requestAccountNonceIfNecessary(t *testing.T) { cache := newUnconstrainedCacheToTest() noncesByAddress := cache.accountNonceProvider.(*txcachemocks.AccountNonceProviderMock).NoncesByAddress noncesByAddress["alice"] = 7 @@ -173,19 +173,19 @@ func TestTxCache_askAboutAccountNonceIfNecessary(t *testing.T) { c := &transactionsHeapItem{} - cache.askAboutAccountNonceIfNecessary(a) - cache.askAboutAccountNonceIfNecessary(b) + cache.requestAccountNonceIfNecessary(a) + cache.requestAccountNonceIfNecessary(b) - require.True(t, a.senderNonceAsked) - require.True(t, a.senderNonceTold) + require.True(t, a.senderNonceRequested) + require.True(t, a.senderNonceProvided) require.Equal(t, uint64(7), a.senderNonce) - require.True(t, b.senderNonceAsked) - require.True(t, b.senderNonceTold) + require.True(t, b.senderNonceRequested) + require.True(t, b.senderNonceProvided) require.Equal(t, uint64(42), b.senderNonce) - require.False(t, c.senderNonceAsked) - require.False(t, c.senderNonceTold) + require.False(t, c.senderNonceRequested) + require.False(t, c.senderNonceProvided) require.Equal(t, uint64(0), c.senderNonce) } diff --git a/txcache/transactionsHeap.go b/txcache/transactionsHeap.go index 2e066521..d0d8416f 100644 --- a/txcache/transactionsHeap.go +++ b/txcache/transactionsHeap.go @@ -8,11 +8,11 @@ type transactionsHeap struct { type transactionsHeapItem struct { senderIndex int - // Whether the sender's nonce has been asked within a selection session. - senderNonceAsked bool - // Whether the sender's nonce has been asked and told (with success) within a selection session. - senderNonceTold bool - // The sender's nonce (if asked and told). + // Whether the sender's nonce has been requested within a selection session. + senderNonceRequested bool + // Whether the sender's nonce has been requested and provided (with success) within a selection session. + senderNonceProvided bool + // The sender's nonce (if requested and provided). senderNonce uint64 transactionIndex int From f97351c25ea77c67f3b2d169c471bccbdbbc877b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Fri, 15 Nov 2024 09:24:18 +0200 Subject: [PATCH 12/34] Receive the nonce provider in SelectTransactions, instead of receiving it in constructor. This way, we can receive the "real" processing accounts adapter (not the slower API accounts adapter). --- txcache/diagnosis.go | 11 ------- txcache/eviction_test.go | 18 ++++------- txcache/selection.go | 12 +++---- txcache/selection_test.go | 68 ++++++++++++++++++++++----------------- txcache/txCache.go | 26 +++++++-------- txcache/txCache_test.go | 46 +++++++++++--------------- 6 files changed, 84 insertions(+), 97 deletions(-) diff --git a/txcache/diagnosis.go b/txcache/diagnosis.go index 7bd7f2da..ff05d885 100644 --- a/txcache/diagnosis.go +++ b/txcache/diagnosis.go @@ -4,7 +4,6 @@ import ( "encoding/hex" "encoding/json" "fmt" - "math" "strings" "github.com/multiversx/mx-chain-core-go/core" @@ -26,7 +25,6 @@ type printedTransaction struct { func (cache *TxCache) Diagnose(_ bool) { cache.diagnoseCounters() cache.diagnoseTransactions() - cache.diagnoseSelection() } func (cache *TxCache) diagnoseCounters() { @@ -108,15 +106,6 @@ func convertWrappedTransactionToPrintedTransaction(wrappedTx *WrappedTransaction } } -func (cache *TxCache) diagnoseSelection() { - if logDiagnoseSelection.GetLevel() > logger.LogDebug { - return - } - - transactions, _ := cache.doSelectTransactions(diagnosisSelectionGasRequested, math.MaxInt) - displaySelectionOutcome(logDiagnoseSelection, "diagnoseSelection", transactions) -} - func displaySelectionOutcome(contextualLogger logger.Logger, linePrefix string, transactions []*WrappedTransaction) { if contextualLogger.GetLevel() > logger.LogTrace { return diff --git a/txcache/eviction_test.go b/txcache/eviction_test.go index d3305810..df7fcf22 100644 --- a/txcache/eviction_test.go +++ b/txcache/eviction_test.go @@ -23,9 +23,8 @@ func TestTxCache_DoEviction_BecauseOfCount(t *testing.T) { } txGasHandler := txcachemocks.NewTxGasHandlerMock() - accountNonceProvider := txcachemocks.NewAccountNonceProviderMock() - cache, err := NewTxCache(config, txGasHandler, accountNonceProvider) + cache, err := NewTxCache(config, txGasHandler) require.Nil(t, err) require.NotNil(t, cache) @@ -59,9 +58,8 @@ func TestTxCache_DoEviction_BecauseOfSize(t *testing.T) { } txGasHandler := txcachemocks.NewTxGasHandlerMock() - accountNonceProvider := txcachemocks.NewAccountNonceProviderMock() - cache, err := NewTxCache(config, txGasHandler, accountNonceProvider) + cache, err := NewTxCache(config, txGasHandler) require.Nil(t, err) require.NotNil(t, cache) @@ -96,9 +94,8 @@ func TestTxCache_DoEviction_DoesNothingWhenAlreadyInProgress(t *testing.T) { } txGasHandler := txcachemocks.NewTxGasHandlerMock() - accountNonceProvider := txcachemocks.NewAccountNonceProviderMock() - cache, err := NewTxCache(config, txGasHandler, accountNonceProvider) + cache, err := NewTxCache(config, txGasHandler) require.Nil(t, err) require.NotNil(t, cache) @@ -136,12 +133,11 @@ func TestBenchmarkTxCache_DoEviction(t *testing.T) { } txGasHandler := txcachemocks.NewTxGasHandlerMock() - accountNonceProvider := txcachemocks.NewAccountNonceProviderMock() sw := core.NewStopWatch() t.Run("numSenders = 35000, numTransactions = 10", func(t *testing.T) { - cache, err := NewTxCache(config, txGasHandler, accountNonceProvider) + cache, err := NewTxCache(config, txGasHandler) require.Nil(t, err) cache.config.EvictionEnabled = false @@ -159,7 +155,7 @@ func TestBenchmarkTxCache_DoEviction(t *testing.T) { }) t.Run("numSenders = 100000, numTransactions = 5", func(t *testing.T) { - cache, err := NewTxCache(config, txGasHandler, accountNonceProvider) + cache, err := NewTxCache(config, txGasHandler) require.Nil(t, err) cache.config.EvictionEnabled = false @@ -177,7 +173,7 @@ func TestBenchmarkTxCache_DoEviction(t *testing.T) { }) t.Run("numSenders = 400000, numTransactions = 1", func(t *testing.T) { - cache, err := NewTxCache(config, txGasHandler, accountNonceProvider) + cache, err := NewTxCache(config, txGasHandler) require.Nil(t, err) cache.config.EvictionEnabled = false @@ -195,7 +191,7 @@ func TestBenchmarkTxCache_DoEviction(t *testing.T) { }) t.Run("numSenders = 10000, numTransactions = 100", func(t *testing.T) { - cache, err := NewTxCache(config, txGasHandler, accountNonceProvider) + cache, err := NewTxCache(config, txGasHandler) require.Nil(t, err) cache.config.EvictionEnabled = false diff --git a/txcache/selection.go b/txcache/selection.go index e84c02df..7fd248ef 100644 --- a/txcache/selection.go +++ b/txcache/selection.go @@ -6,7 +6,7 @@ import ( logger "github.com/multiversx/mx-chain-logger-go" ) -func (cache *TxCache) doSelectTransactions(gasRequested uint64, maxNum int) (bunchOfTransactions, uint64) { +func (cache *TxCache) doSelectTransactions(accountNonceProvider AccountNonceProvider, gasRequested uint64, maxNum int) (bunchOfTransactions, uint64) { senders := cache.getSenders() bunches := make([]bunchOfTransactions, 0, len(senders)) @@ -14,11 +14,11 @@ func (cache *TxCache) doSelectTransactions(gasRequested uint64, maxNum int) (bun bunches = append(bunches, sender.getSequentialTxs()) } - return cache.selectTransactionsFromBunches(bunches, gasRequested, maxNum) + return selectTransactionsFromBunches(accountNonceProvider, bunches, gasRequested, maxNum) } // Selection tolerates concurrent transaction additions / removals. -func (cache *TxCache) selectTransactionsFromBunches(bunches []bunchOfTransactions, gasRequested uint64, maxNum int) (bunchOfTransactions, uint64) { +func selectTransactionsFromBunches(accountNonceProvider AccountNonceProvider, bunches []bunchOfTransactions, gasRequested uint64, maxNum int) (bunchOfTransactions, uint64) { selectedTransactions := make(bunchOfTransactions, 0, initialCapacityOfSelectionSlice) // Items popped from the heap are added to "selectedTransactions". @@ -56,7 +56,7 @@ func (cache *TxCache) selectTransactionsFromBunches(bunches []bunchOfTransaction break } - cache.requestAccountNonceIfNecessary(item) + requestAccountNonceIfNecessary(accountNonceProvider, item) isInitialGap := item.transactionIndex == 0 && item.senderNonceProvided && nonce > item.senderNonce if isInitialGap { @@ -105,7 +105,7 @@ func (cache *TxCache) selectTransactionsFromBunches(bunches []bunchOfTransaction return selectedTransactions, accumulatedGas } -func (cache *TxCache) requestAccountNonceIfNecessary(item *transactionsHeapItem) { +func requestAccountNonceIfNecessary(accountNonceProvider AccountNonceProvider, item *transactionsHeapItem) { if item.senderNonceRequested { return } @@ -113,7 +113,7 @@ func (cache *TxCache) requestAccountNonceIfNecessary(item *transactionsHeapItem) item.senderNonceRequested = true sender := item.transaction.Tx.GetSndAddr() - senderNonce, err := cache.accountNonceProvider.GetAccountNonce(sender) + senderNonce, err := accountNonceProvider.GetAccountNonce(sender) if err != nil { // Hazardous; should never happen. logSelect.Debug("TxCache.requestAccountNonceIfNecessary: nonce not available", "sender", sender, "err", err) diff --git a/txcache/selection_test.go b/txcache/selection_test.go index e1796244..d04dcf41 100644 --- a/txcache/selection_test.go +++ b/txcache/selection_test.go @@ -13,6 +13,7 @@ import ( func TestTxCache_SelectTransactions_Dummy(t *testing.T) { t.Run("all having same PPU", func(t *testing.T) { cache := newUnconstrainedCacheToTest() + accountNonceProvider := txcachemocks.NewAccountNonceProviderMock() cache.AddTx(createTx([]byte("hash-alice-4"), "alice", 4)) cache.AddTx(createTx([]byte("hash-alice-3"), "alice", 3)) @@ -23,7 +24,7 @@ func TestTxCache_SelectTransactions_Dummy(t *testing.T) { cache.AddTx(createTx([]byte("hash-bob-5"), "bob", 5)) cache.AddTx(createTx([]byte("hash-carol-1"), "carol", 1)) - selected, accumulatedGas := cache.SelectTransactions(math.MaxUint64, math.MaxInt) + selected, accumulatedGas := cache.SelectTransactions(accountNonceProvider, math.MaxUint64, math.MaxInt) require.Len(t, selected, 8) require.Equal(t, 400000, int(accumulatedGas)) @@ -40,12 +41,13 @@ func TestTxCache_SelectTransactions_Dummy(t *testing.T) { t.Run("alice > carol > bob", func(t *testing.T) { cache := newUnconstrainedCacheToTest() + accountNonceProvider := txcachemocks.NewAccountNonceProviderMock() cache.AddTx(createTx([]byte("hash-alice-1"), "alice", 1).withGasPrice(100)) cache.AddTx(createTx([]byte("hash-bob-5"), "bob", 5).withGasPrice(50)) cache.AddTx(createTx([]byte("hash-carol-3"), "carol", 3).withGasPrice(75)) - selected, accumulatedGas := cache.SelectTransactions(math.MaxUint64, math.MaxInt) + selected, accumulatedGas := cache.SelectTransactions(accountNonceProvider, math.MaxUint64, math.MaxInt) require.Len(t, selected, 3) require.Equal(t, 150000, int(accumulatedGas)) @@ -59,6 +61,7 @@ func TestTxCache_SelectTransactions_Dummy(t *testing.T) { func TestTxCache_SelectTransactionsWithBandwidth_Dummy(t *testing.T) { t.Run("transactions with no data field", func(t *testing.T) { cache := newUnconstrainedCacheToTest() + accountNonceProvider := txcachemocks.NewAccountNonceProviderMock() cache.AddTx(createTx([]byte("hash-alice-4"), "alice", 4).withGasLimit(100000)) cache.AddTx(createTx([]byte("hash-alice-3"), "alice", 3).withGasLimit(100000)) @@ -69,7 +72,7 @@ func TestTxCache_SelectTransactionsWithBandwidth_Dummy(t *testing.T) { cache.AddTx(createTx([]byte("hash-bob-5"), "bob", 5).withGasLimit(50000)) cache.AddTx(createTx([]byte("hash-carol-1"), "carol", 1).withGasLimit(50000)) - selected, accumulatedGas := cache.SelectTransactions(760000, math.MaxInt) + selected, accumulatedGas := cache.SelectTransactions(accountNonceProvider, 760000, math.MaxInt) require.Len(t, selected, 5) require.Equal(t, 750000, int(accumulatedGas)) @@ -85,6 +88,7 @@ func TestTxCache_SelectTransactionsWithBandwidth_Dummy(t *testing.T) { func TestTxCache_SelectTransactions_HandlesGapsAndLowerNonces(t *testing.T) { t.Run("with middle gaps", func(t *testing.T) { cache := newUnconstrainedCacheToTest() + accountNonceProvider := txcachemocks.NewAccountNonceProviderMock() cache.AddTx(createTx([]byte("hash-alice-1"), "alice", 1)) cache.AddTx(createTx([]byte("hash-alice-2"), "alice", 2)) @@ -98,7 +102,7 @@ func TestTxCache_SelectTransactions_HandlesGapsAndLowerNonces(t *testing.T) { cache.AddTx(createTx([]byte("hash-carol-10"), "carol", 10)) // gap cache.AddTx(createTx([]byte("hash-carol-11"), "carol", 11)) - sorted, accumulatedGas := cache.SelectTransactions(math.MaxUint64, math.MaxInt) + sorted, accumulatedGas := cache.SelectTransactions(accountNonceProvider, math.MaxUint64, math.MaxInt) expectedNumSelected := 3 + 1 + 2 // 3 alice + 1 bob + 2 carol require.Len(t, sorted, expectedNumSelected) require.Equal(t, 300000, int(accumulatedGas)) @@ -106,7 +110,9 @@ func TestTxCache_SelectTransactions_HandlesGapsAndLowerNonces(t *testing.T) { t.Run("with initial gaps", func(t *testing.T) { cache := newUnconstrainedCacheToTest() - noncesByAddress := cache.accountNonceProvider.(*txcachemocks.AccountNonceProviderMock).NoncesByAddress + accountNonceProvider := txcachemocks.NewAccountNonceProviderMock() + + noncesByAddress := accountNonceProvider.NoncesByAddress noncesByAddress["alice"] = 1 noncesByAddress["bob"] = 42 @@ -124,7 +130,7 @@ func TestTxCache_SelectTransactions_HandlesGapsAndLowerNonces(t *testing.T) { cache.AddTx(createTx([]byte("hash-carol-7"), "carol", 7)) cache.AddTx(createTx([]byte("hash-carol-8"), "carol", 8)) - sorted, accumulatedGas := cache.SelectTransactions(math.MaxUint64, math.MaxInt) + sorted, accumulatedGas := cache.SelectTransactions(accountNonceProvider, math.MaxUint64, math.MaxInt) expectedNumSelected := 3 + 0 + 2 // 3 alice + 0 bob + 2 carol require.Len(t, sorted, expectedNumSelected) require.Equal(t, 250000, int(accumulatedGas)) @@ -132,7 +138,9 @@ func TestTxCache_SelectTransactions_HandlesGapsAndLowerNonces(t *testing.T) { t.Run("with lower nonces", func(t *testing.T) { cache := newUnconstrainedCacheToTest() - noncesByAddress := cache.accountNonceProvider.(*txcachemocks.AccountNonceProviderMock).NoncesByAddress + accountNonceProvider := txcachemocks.NewAccountNonceProviderMock() + + noncesByAddress := accountNonceProvider.NoncesByAddress noncesByAddress["alice"] = 1 noncesByAddress["bob"] = 42 @@ -150,7 +158,7 @@ func TestTxCache_SelectTransactions_HandlesGapsAndLowerNonces(t *testing.T) { cache.AddTx(createTx([]byte("hash-carol-7"), "carol", 7)) cache.AddTx(createTx([]byte("hash-carol-8"), "carol", 8)) - sorted, accumulatedGas := cache.SelectTransactions(math.MaxUint64, math.MaxInt) + sorted, accumulatedGas := cache.SelectTransactions(accountNonceProvider, math.MaxUint64, math.MaxInt) expectedNumSelected := 3 + 1 + 2 // 3 alice + 1 bob + 2 carol require.Len(t, sorted, expectedNumSelected) require.Equal(t, 300000, int(accumulatedGas)) @@ -158,8 +166,9 @@ func TestTxCache_SelectTransactions_HandlesGapsAndLowerNonces(t *testing.T) { } func TestTxCache_requestAccountNonceIfNecessary(t *testing.T) { - cache := newUnconstrainedCacheToTest() - noncesByAddress := cache.accountNonceProvider.(*txcachemocks.AccountNonceProviderMock).NoncesByAddress + accountNonceProvider := txcachemocks.NewAccountNonceProviderMock() + + noncesByAddress := accountNonceProvider.NoncesByAddress noncesByAddress["alice"] = 7 noncesByAddress["bob"] = 42 @@ -173,8 +182,8 @@ func TestTxCache_requestAccountNonceIfNecessary(t *testing.T) { c := &transactionsHeapItem{} - cache.requestAccountNonceIfNecessary(a) - cache.requestAccountNonceIfNecessary(b) + requestAccountNonceIfNecessary(accountNonceProvider, a) + requestAccountNonceIfNecessary(accountNonceProvider, b) require.True(t, a.senderNonceRequested) require.True(t, a.senderNonceProvided) @@ -191,6 +200,7 @@ func TestTxCache_requestAccountNonceIfNecessary(t *testing.T) { func TestTxCache_SelectTransactions_WhenTransactionsAddedInReversedNonceOrder(t *testing.T) { cache := newUnconstrainedCacheToTest() + accountNonceProvider := txcachemocks.NewAccountNonceProviderMock() // Add "nSenders" * "nTransactionsPerSender" transactions in the cache (in reversed nonce order) nSenders := 1000 @@ -209,7 +219,7 @@ func TestTxCache_SelectTransactions_WhenTransactionsAddedInReversedNonceOrder(t require.Equal(t, uint64(nTotalTransactions), cache.CountTx()) - sorted, accumulatedGas := cache.SelectTransactions(math.MaxUint64, math.MaxInt) + sorted, accumulatedGas := cache.SelectTransactions(accountNonceProvider, math.MaxUint64, math.MaxInt) require.Len(t, sorted, nTotalTransactions) require.Equal(t, 5_000_000_000, int(accumulatedGas)) @@ -228,8 +238,8 @@ func TestTxCache_SelectTransactions_WhenTransactionsAddedInReversedNonceOrder(t func TestTxCache_selectTransactionsFromBunches(t *testing.T) { t.Run("empty cache", func(t *testing.T) { - cache := newUnconstrainedCacheToTest() - merged, accumulatedGas := cache.selectTransactionsFromBunches([]bunchOfTransactions{}, 10_000_000_000, math.MaxInt) + accountNonceProvider := txcachemocks.NewAccountNonceProviderMock() + merged, accumulatedGas := selectTransactionsFromBunches(accountNonceProvider, []bunchOfTransactions{}, 10_000_000_000, math.MaxInt) require.Equal(t, 0, len(merged)) require.Equal(t, uint64(0), accumulatedGas) @@ -240,11 +250,11 @@ func TestBenchmarkTxCache_selectTransactionsFromBunches(t *testing.T) { sw := core.NewStopWatch() t.Run("numSenders = 1000, numTransactions = 1000", func(t *testing.T) { - cache := newUnconstrainedCacheToTest() + accountNonceProvider := txcachemocks.NewAccountNonceProviderMock() bunches := createBunchesOfTransactionsWithUniformDistribution(1000, 1000) sw.Start(t.Name()) - merged, accumulatedGas := cache.selectTransactionsFromBunches(bunches, 10_000_000_000, math.MaxInt) + merged, accumulatedGas := selectTransactionsFromBunches(accountNonceProvider, bunches, 10_000_000_000, math.MaxInt) sw.Stop(t.Name()) require.Equal(t, 200000, len(merged)) @@ -252,11 +262,11 @@ func TestBenchmarkTxCache_selectTransactionsFromBunches(t *testing.T) { }) t.Run("numSenders = 10000, numTransactions = 100", func(t *testing.T) { - cache := newUnconstrainedCacheToTest() + accountNonceProvider := txcachemocks.NewAccountNonceProviderMock() bunches := createBunchesOfTransactionsWithUniformDistribution(1000, 1000) sw.Start(t.Name()) - merged, accumulatedGas := cache.selectTransactionsFromBunches(bunches, 10_000_000_000, math.MaxInt) + merged, accumulatedGas := selectTransactionsFromBunches(accountNonceProvider, bunches, 10_000_000_000, math.MaxInt) sw.Stop(t.Name()) require.Equal(t, 200000, len(merged)) @@ -264,11 +274,11 @@ func TestBenchmarkTxCache_selectTransactionsFromBunches(t *testing.T) { }) t.Run("numSenders = 100000, numTransactions = 3", func(t *testing.T) { - cache := newUnconstrainedCacheToTest() + accountNonceProvider := txcachemocks.NewAccountNonceProviderMock() bunches := createBunchesOfTransactionsWithUniformDistribution(100000, 3) sw.Start(t.Name()) - merged, accumulatedGas := cache.selectTransactionsFromBunches(bunches, 10_000_000_000, math.MaxInt) + merged, accumulatedGas := selectTransactionsFromBunches(accountNonceProvider, bunches, 10_000_000_000, math.MaxInt) sw.Stop(t.Name()) require.Equal(t, 200000, len(merged)) @@ -276,11 +286,11 @@ func TestBenchmarkTxCache_selectTransactionsFromBunches(t *testing.T) { }) t.Run("numSenders = 300000, numTransactions = 1", func(t *testing.T) { - cache := newUnconstrainedCacheToTest() + accountNonceProvider := txcachemocks.NewAccountNonceProviderMock() bunches := createBunchesOfTransactionsWithUniformDistribution(300000, 1) sw.Start(t.Name()) - merged, accumulatedGas := cache.selectTransactionsFromBunches(bunches, 10_000_000_000, math.MaxInt) + merged, accumulatedGas := selectTransactionsFromBunches(accountNonceProvider, bunches, 10_000_000_000, math.MaxInt) sw.Stop(t.Name()) require.Equal(t, 200000, len(merged)) @@ -323,7 +333,7 @@ func TestBenchmarkTxCache_doSelectTransactions(t *testing.T) { sw := core.NewStopWatch() t.Run("numSenders = 50000, numTransactions = 2, maxNum = 50_000", func(t *testing.T) { - cache, err := NewTxCache(config, txGasHandler, accountNonceProvider) + cache, err := NewTxCache(config, txGasHandler) require.Nil(t, err) addManyTransactionsWithUniformDistribution(cache, 50000, 2) @@ -331,7 +341,7 @@ func TestBenchmarkTxCache_doSelectTransactions(t *testing.T) { require.Equal(t, 100000, int(cache.CountTx())) sw.Start(t.Name()) - merged, accumulatedGas := cache.SelectTransactions(10_000_000_000, 50_000) + merged, accumulatedGas := cache.SelectTransactions(accountNonceProvider, 10_000_000_000, 50_000) sw.Stop(t.Name()) require.Equal(t, 50000, len(merged)) @@ -339,7 +349,7 @@ func TestBenchmarkTxCache_doSelectTransactions(t *testing.T) { }) t.Run("numSenders = 100000, numTransactions = 1, maxNum = 50_000", func(t *testing.T) { - cache, err := NewTxCache(config, txGasHandler, accountNonceProvider) + cache, err := NewTxCache(config, txGasHandler) require.Nil(t, err) addManyTransactionsWithUniformDistribution(cache, 100000, 1) @@ -347,7 +357,7 @@ func TestBenchmarkTxCache_doSelectTransactions(t *testing.T) { require.Equal(t, 100000, int(cache.CountTx())) sw.Start(t.Name()) - merged, accumulatedGas := cache.SelectTransactions(10_000_000_000, 50_000) + merged, accumulatedGas := cache.SelectTransactions(accountNonceProvider, 10_000_000_000, 50_000) sw.Stop(t.Name()) require.Equal(t, 50000, len(merged)) @@ -355,7 +365,7 @@ func TestBenchmarkTxCache_doSelectTransactions(t *testing.T) { }) t.Run("numSenders = 300000, numTransactions = 1, maxNum = 50_000", func(t *testing.T) { - cache, err := NewTxCache(config, txGasHandler, accountNonceProvider) + cache, err := NewTxCache(config, txGasHandler) require.Nil(t, err) addManyTransactionsWithUniformDistribution(cache, 300000, 1) @@ -363,7 +373,7 @@ func TestBenchmarkTxCache_doSelectTransactions(t *testing.T) { require.Equal(t, 300000, int(cache.CountTx())) sw.Start(t.Name()) - merged, accumulatedGas := cache.SelectTransactions(10_000_000_000, 50_000) + merged, accumulatedGas := cache.SelectTransactions(accountNonceProvider, 10_000_000_000, 50_000) sw.Stop(t.Name()) require.Equal(t, 50000, len(merged)) diff --git a/txcache/txCache.go b/txcache/txCache.go index c9414bb2..0ee247e6 100644 --- a/txcache/txCache.go +++ b/txcache/txCache.go @@ -20,14 +20,13 @@ type TxCache struct { txByHash *txByHashMap config ConfigSourceMe txGasHandler TxGasHandler - accountNonceProvider AccountNonceProvider evictionMutex sync.Mutex isEvictionInProgress atomic.Flag mutTxOperation sync.Mutex } // NewTxCache creates a new transaction cache -func NewTxCache(config ConfigSourceMe, txGasHandler TxGasHandler, accountNonceProvider AccountNonceProvider) (*TxCache, error) { +func NewTxCache(config ConfigSourceMe, txGasHandler TxGasHandler) (*TxCache, error) { log.Debug("NewTxCache", "config", config.String()) monitoring.MonitorNewCache(config.Name, uint64(config.NumBytesThreshold)) @@ -38,21 +37,17 @@ func NewTxCache(config ConfigSourceMe, txGasHandler TxGasHandler, accountNoncePr if check.IfNil(txGasHandler) { return nil, common.ErrNilTxGasHandler } - if check.IfNil(accountNonceProvider) { - return nil, common.ErrNilAccountNonceProvider - } // Note: for simplicity, we use the same "numChunks" for both internal concurrent maps numChunks := config.NumChunks senderConstraintsObj := config.getSenderConstraints() txCache := &TxCache{ - name: config.Name, - txListBySender: newTxListBySenderMap(numChunks, senderConstraintsObj), - txByHash: newTxByHashMap(numChunks), - config: config, - txGasHandler: txGasHandler, - accountNonceProvider: accountNonceProvider, + name: config.Name, + txListBySender: newTxListBySenderMap(numChunks, senderConstraintsObj), + txByHash: newTxByHashMap(numChunks), + config: config, + txGasHandler: txGasHandler, } return txCache, nil @@ -104,7 +99,12 @@ func (cache *TxCache) GetByTxHash(txHash []byte) (*WrappedTransaction, bool) { // SelectTransactions selects the best transactions to be included in the next miniblock. // It returns up to "maxNum" transactions, with total gas <= "gasRequested". -func (cache *TxCache) SelectTransactions(gasRequested uint64, maxNum int) ([]*WrappedTransaction, uint64) { +func (cache *TxCache) SelectTransactions(accountNonceProvider AccountNonceProvider, gasRequested uint64, maxNum int) ([]*WrappedTransaction, uint64) { + if check.IfNil(accountNonceProvider) { + log.Error("TxCache.SelectTransactions", "err", common.ErrNilAccountNonceProvider) + return nil, 0 + } + stopWatch := core.NewStopWatch() stopWatch.Start("selection") @@ -115,7 +115,7 @@ func (cache *TxCache) SelectTransactions(gasRequested uint64, maxNum int) ([]*Wr "num senders", cache.CountSenders(), ) - transactions, accumulatedGas := cache.doSelectTransactions(gasRequested, maxNum) + transactions, accumulatedGas := cache.doSelectTransactions(accountNonceProvider, gasRequested, maxNum) stopWatch.Stop("selection") diff --git a/txcache/txCache_test.go b/txcache/txCache_test.go index d7c7dd4e..de6daf15 100644 --- a/txcache/txCache_test.go +++ b/txcache/txCache_test.go @@ -29,49 +29,43 @@ func Test_NewTxCache(t *testing.T) { } txGasHandler := txcachemocks.NewTxGasHandlerMock() - accountNonceProvider := txcachemocks.NewAccountNonceProviderMock() - cache, err := NewTxCache(config, txGasHandler, accountNonceProvider) + cache, err := NewTxCache(config, txGasHandler) require.Nil(t, err) require.NotNil(t, cache) badConfig := config badConfig.Name = "" - requireErrorOnNewTxCache(t, badConfig, common.ErrInvalidConfig, "config.Name", txGasHandler, accountNonceProvider) + requireErrorOnNewTxCache(t, badConfig, common.ErrInvalidConfig, "config.Name", txGasHandler) badConfig = config badConfig.NumChunks = 0 - requireErrorOnNewTxCache(t, badConfig, common.ErrInvalidConfig, "config.NumChunks", txGasHandler, accountNonceProvider) + requireErrorOnNewTxCache(t, badConfig, common.ErrInvalidConfig, "config.NumChunks", txGasHandler) badConfig = config badConfig.NumBytesPerSenderThreshold = 0 - requireErrorOnNewTxCache(t, badConfig, common.ErrInvalidConfig, "config.NumBytesPerSenderThreshold", txGasHandler, accountNonceProvider) + requireErrorOnNewTxCache(t, badConfig, common.ErrInvalidConfig, "config.NumBytesPerSenderThreshold", txGasHandler) badConfig = config badConfig.CountPerSenderThreshold = 0 - requireErrorOnNewTxCache(t, badConfig, common.ErrInvalidConfig, "config.CountPerSenderThreshold", txGasHandler, accountNonceProvider) + requireErrorOnNewTxCache(t, badConfig, common.ErrInvalidConfig, "config.CountPerSenderThreshold", txGasHandler) badConfig = config - cache, err = NewTxCache(config, nil, accountNonceProvider) + cache, err = NewTxCache(config, nil) require.Nil(t, cache) require.Equal(t, common.ErrNilTxGasHandler, err) - badConfig = config - cache, err = NewTxCache(config, txGasHandler, nil) - require.Nil(t, cache) - require.Equal(t, common.ErrNilAccountNonceProvider, err) - badConfig = config badConfig.NumBytesThreshold = 0 - requireErrorOnNewTxCache(t, badConfig, common.ErrInvalidConfig, "config.NumBytesThreshold", txGasHandler, accountNonceProvider) + requireErrorOnNewTxCache(t, badConfig, common.ErrInvalidConfig, "config.NumBytesThreshold", txGasHandler) badConfig = config badConfig.CountThreshold = 0 - requireErrorOnNewTxCache(t, badConfig, common.ErrInvalidConfig, "config.CountThreshold", txGasHandler, accountNonceProvider) + requireErrorOnNewTxCache(t, badConfig, common.ErrInvalidConfig, "config.CountThreshold", txGasHandler) } -func requireErrorOnNewTxCache(t *testing.T, config ConfigSourceMe, errExpected error, errPartialMessage string, txGasHandler TxGasHandler, accountNonceProvider AccountNonceProvider) { - cache, errReceived := NewTxCache(config, txGasHandler, accountNonceProvider) +func requireErrorOnNewTxCache(t *testing.T, config ConfigSourceMe, errExpected error, errPartialMessage string, txGasHandler TxGasHandler) { + cache, errReceived := NewTxCache(config, txGasHandler) require.Nil(t, cache) require.True(t, errors.Is(errReceived, errExpected)) require.Contains(t, errReceived.Error(), errPartialMessage) @@ -318,7 +312,6 @@ func Test_Keys(t *testing.T) { func Test_AddWithEviction_UniformDistributionOfTxsPerSender(t *testing.T) { txGasHandler := txcachemocks.NewTxGasHandlerMock() - accountNonceProvider := txcachemocks.NewAccountNonceProviderMock() t.Run("numSenders = 11, numTransactions = 10, countThreshold = 100, numItemsToPreemptivelyEvict = 1", func(t *testing.T) { config := ConfigSourceMe{ @@ -332,7 +325,7 @@ func Test_AddWithEviction_UniformDistributionOfTxsPerSender(t *testing.T) { NumItemsToPreemptivelyEvict: 1, } - cache, err := NewTxCache(config, txGasHandler, accountNonceProvider) + cache, err := NewTxCache(config, txGasHandler) require.Nil(t, err) require.NotNil(t, cache) @@ -356,7 +349,7 @@ func Test_AddWithEviction_UniformDistributionOfTxsPerSender(t *testing.T) { NumItemsToPreemptivelyEvict: 3, } - cache, err := NewTxCache(config, txGasHandler, accountNonceProvider) + cache, err := NewTxCache(config, txGasHandler) require.Nil(t, err) require.NotNil(t, cache) @@ -376,7 +369,7 @@ func Test_AddWithEviction_UniformDistributionOfTxsPerSender(t *testing.T) { NumItemsToPreemptivelyEvict: 2, } - cache, err := NewTxCache(config, txGasHandler, accountNonceProvider) + cache, err := NewTxCache(config, txGasHandler) require.Nil(t, err) require.NotNil(t, cache) @@ -396,7 +389,7 @@ func Test_AddWithEviction_UniformDistributionOfTxsPerSender(t *testing.T) { NumItemsToPreemptivelyEvict: 1, } - cache, err := NewTxCache(config, txGasHandler, accountNonceProvider) + cache, err := NewTxCache(config, txGasHandler) require.Nil(t, err) require.NotNil(t, cache) @@ -416,7 +409,7 @@ func Test_AddWithEviction_UniformDistributionOfTxsPerSender(t *testing.T) { NumItemsToPreemptivelyEvict: 10000, } - cache, err := NewTxCache(config, txGasHandler, accountNonceProvider) + cache, err := NewTxCache(config, txGasHandler) require.Nil(t, err) require.NotNil(t, cache) @@ -455,6 +448,7 @@ func Test_IsInterfaceNil(t *testing.T) { func TestTxCache_ConcurrentMutationAndSelection(t *testing.T) { cache := newUnconstrainedCacheToTest() + accountNonceProvider := txcachemocks.NewAccountNonceProviderMock() // Alice will quickly move between two score buckets (chunks) cheapTransaction := createTx([]byte("alice-x-o"), "alice", 0).withDataLength(1).withGasLimit(300000000).withGasPrice(oneBillion) @@ -469,7 +463,7 @@ func TestTxCache_ConcurrentMutationAndSelection(t *testing.T) { go func() { for i := 0; i < 100; i++ { fmt.Println("Selection", i) - _, _ = cache.SelectTransactions(math.MaxUint64, math.MaxInt) + _, _ = cache.SelectTransactions(accountNonceProvider, math.MaxUint64, math.MaxInt) } wg.Done() @@ -561,7 +555,6 @@ func TestTxCache_NoCriticalInconsistency_WhenConcurrentAdditionsAndRemovals(t *t func newUnconstrainedCacheToTest() *TxCache { txGasHandler := txcachemocks.NewTxGasHandlerMock() - accountNonceProvider := txcachemocks.NewAccountNonceProviderMock() cache, err := NewTxCache(ConfigSourceMe{ Name: "test", @@ -572,7 +565,7 @@ func newUnconstrainedCacheToTest() *TxCache { CountPerSenderThreshold: math.MaxUint32, EvictionEnabled: false, NumItemsToPreemptivelyEvict: 1, - }, txGasHandler, accountNonceProvider) + }, txGasHandler) if err != nil { panic(fmt.Sprintf("newUnconstrainedCacheToTest(): %s", err)) } @@ -582,7 +575,6 @@ func newUnconstrainedCacheToTest() *TxCache { func newCacheToTest(numBytesPerSenderThreshold uint32, countPerSenderThreshold uint32) *TxCache { txGasHandler := txcachemocks.NewTxGasHandlerMock() - accountNonceProvider := txcachemocks.NewAccountNonceProviderMock() cache, err := NewTxCache(ConfigSourceMe{ Name: "test", @@ -593,7 +585,7 @@ func newCacheToTest(numBytesPerSenderThreshold uint32, countPerSenderThreshold u CountPerSenderThreshold: countPerSenderThreshold, EvictionEnabled: false, NumItemsToPreemptivelyEvict: 1, - }, txGasHandler, accountNonceProvider) + }, txGasHandler) if err != nil { panic(fmt.Sprintf("newCacheToTest(): %s", err)) } From 0db9d397dbb43a1dfd72dfc6816c6868c2947daf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Fri, 15 Nov 2024 14:03:08 +0200 Subject: [PATCH 13/34] Break the selection loop if it takes too long. --- txcache/constants.go | 1 + txcache/selection.go | 14 +++++++++++--- txcache/selection_test.go | 30 +++++++++++++++--------------- txcache/testutils_test.go | 1 + txcache/txCache.go | 5 +++-- txcache/txCache_test.go | 2 +- 6 files changed, 32 insertions(+), 21 deletions(-) diff --git a/txcache/constants.go b/txcache/constants.go index 0ff0b536..d44a8ef4 100644 --- a/txcache/constants.go +++ b/txcache/constants.go @@ -3,3 +3,4 @@ package txcache const diagnosisMaxTransactionsToDisplay = 10000 const diagnosisSelectionGasRequested = 10_000_000_000 const initialCapacityOfSelectionSlice = 30000 +const selectionLoopDurationCheckInterval = 64 diff --git a/txcache/selection.go b/txcache/selection.go index 7fd248ef..9aa9a351 100644 --- a/txcache/selection.go +++ b/txcache/selection.go @@ -2,11 +2,12 @@ package txcache import ( "container/heap" + "time" logger "github.com/multiversx/mx-chain-logger-go" ) -func (cache *TxCache) doSelectTransactions(accountNonceProvider AccountNonceProvider, gasRequested uint64, maxNum int) (bunchOfTransactions, uint64) { +func (cache *TxCache) doSelectTransactions(accountNonceProvider AccountNonceProvider, gasRequested uint64, maxNum int, selectionLoopMaximumDuration time.Duration) (bunchOfTransactions, uint64) { senders := cache.getSenders() bunches := make([]bunchOfTransactions, 0, len(senders)) @@ -14,11 +15,11 @@ func (cache *TxCache) doSelectTransactions(accountNonceProvider AccountNonceProv bunches = append(bunches, sender.getSequentialTxs()) } - return selectTransactionsFromBunches(accountNonceProvider, bunches, gasRequested, maxNum) + return selectTransactionsFromBunches(accountNonceProvider, bunches, gasRequested, maxNum, selectionLoopMaximumDuration) } // Selection tolerates concurrent transaction additions / removals. -func selectTransactionsFromBunches(accountNonceProvider AccountNonceProvider, bunches []bunchOfTransactions, gasRequested uint64, maxNum int) (bunchOfTransactions, uint64) { +func selectTransactionsFromBunches(accountNonceProvider AccountNonceProvider, bunches []bunchOfTransactions, gasRequested uint64, maxNum int, selectionLoopMaximumDuration time.Duration) (bunchOfTransactions, uint64) { selectedTransactions := make(bunchOfTransactions, 0, initialCapacityOfSelectionSlice) // Items popped from the heap are added to "selectedTransactions". @@ -41,6 +42,7 @@ func selectTransactionsFromBunches(accountNonceProvider AccountNonceProvider, bu } accumulatedGas := uint64(0) + selectionLoopStartTime := time.Now() // Select transactions (sorted). for transactionsHeap.Len() > 0 { @@ -55,6 +57,12 @@ func selectTransactionsFromBunches(accountNonceProvider AccountNonceProvider, bu if len(selectedTransactions) >= maxNum { break } + if len(selectedTransactions)%selectionLoopDurationCheckInterval == 0 { + if time.Since(selectionLoopStartTime) > selectionLoopMaximumDuration { + logSelect.Debug("TxCache.selectTransactionsFromBunches, selection loop timeout", "duration", time.Since(selectionLoopStartTime)) + break + } + } requestAccountNonceIfNecessary(accountNonceProvider, item) diff --git a/txcache/selection_test.go b/txcache/selection_test.go index d04dcf41..37e706d5 100644 --- a/txcache/selection_test.go +++ b/txcache/selection_test.go @@ -24,7 +24,7 @@ func TestTxCache_SelectTransactions_Dummy(t *testing.T) { cache.AddTx(createTx([]byte("hash-bob-5"), "bob", 5)) cache.AddTx(createTx([]byte("hash-carol-1"), "carol", 1)) - selected, accumulatedGas := cache.SelectTransactions(accountNonceProvider, math.MaxUint64, math.MaxInt) + selected, accumulatedGas := cache.SelectTransactions(accountNonceProvider, math.MaxUint64, math.MaxInt, oneSecond) require.Len(t, selected, 8) require.Equal(t, 400000, int(accumulatedGas)) @@ -47,7 +47,7 @@ func TestTxCache_SelectTransactions_Dummy(t *testing.T) { cache.AddTx(createTx([]byte("hash-bob-5"), "bob", 5).withGasPrice(50)) cache.AddTx(createTx([]byte("hash-carol-3"), "carol", 3).withGasPrice(75)) - selected, accumulatedGas := cache.SelectTransactions(accountNonceProvider, math.MaxUint64, math.MaxInt) + selected, accumulatedGas := cache.SelectTransactions(accountNonceProvider, math.MaxUint64, math.MaxInt, oneSecond) require.Len(t, selected, 3) require.Equal(t, 150000, int(accumulatedGas)) @@ -72,7 +72,7 @@ func TestTxCache_SelectTransactionsWithBandwidth_Dummy(t *testing.T) { cache.AddTx(createTx([]byte("hash-bob-5"), "bob", 5).withGasLimit(50000)) cache.AddTx(createTx([]byte("hash-carol-1"), "carol", 1).withGasLimit(50000)) - selected, accumulatedGas := cache.SelectTransactions(accountNonceProvider, 760000, math.MaxInt) + selected, accumulatedGas := cache.SelectTransactions(accountNonceProvider, 760000, math.MaxInt, oneSecond) require.Len(t, selected, 5) require.Equal(t, 750000, int(accumulatedGas)) @@ -102,7 +102,7 @@ func TestTxCache_SelectTransactions_HandlesGapsAndLowerNonces(t *testing.T) { cache.AddTx(createTx([]byte("hash-carol-10"), "carol", 10)) // gap cache.AddTx(createTx([]byte("hash-carol-11"), "carol", 11)) - sorted, accumulatedGas := cache.SelectTransactions(accountNonceProvider, math.MaxUint64, math.MaxInt) + sorted, accumulatedGas := cache.SelectTransactions(accountNonceProvider, math.MaxUint64, math.MaxInt, oneSecond) expectedNumSelected := 3 + 1 + 2 // 3 alice + 1 bob + 2 carol require.Len(t, sorted, expectedNumSelected) require.Equal(t, 300000, int(accumulatedGas)) @@ -130,7 +130,7 @@ func TestTxCache_SelectTransactions_HandlesGapsAndLowerNonces(t *testing.T) { cache.AddTx(createTx([]byte("hash-carol-7"), "carol", 7)) cache.AddTx(createTx([]byte("hash-carol-8"), "carol", 8)) - sorted, accumulatedGas := cache.SelectTransactions(accountNonceProvider, math.MaxUint64, math.MaxInt) + sorted, accumulatedGas := cache.SelectTransactions(accountNonceProvider, math.MaxUint64, math.MaxInt, oneSecond) expectedNumSelected := 3 + 0 + 2 // 3 alice + 0 bob + 2 carol require.Len(t, sorted, expectedNumSelected) require.Equal(t, 250000, int(accumulatedGas)) @@ -158,7 +158,7 @@ func TestTxCache_SelectTransactions_HandlesGapsAndLowerNonces(t *testing.T) { cache.AddTx(createTx([]byte("hash-carol-7"), "carol", 7)) cache.AddTx(createTx([]byte("hash-carol-8"), "carol", 8)) - sorted, accumulatedGas := cache.SelectTransactions(accountNonceProvider, math.MaxUint64, math.MaxInt) + sorted, accumulatedGas := cache.SelectTransactions(accountNonceProvider, math.MaxUint64, math.MaxInt, oneSecond) expectedNumSelected := 3 + 1 + 2 // 3 alice + 1 bob + 2 carol require.Len(t, sorted, expectedNumSelected) require.Equal(t, 300000, int(accumulatedGas)) @@ -219,7 +219,7 @@ func TestTxCache_SelectTransactions_WhenTransactionsAddedInReversedNonceOrder(t require.Equal(t, uint64(nTotalTransactions), cache.CountTx()) - sorted, accumulatedGas := cache.SelectTransactions(accountNonceProvider, math.MaxUint64, math.MaxInt) + sorted, accumulatedGas := cache.SelectTransactions(accountNonceProvider, math.MaxUint64, math.MaxInt, oneSecond) require.Len(t, sorted, nTotalTransactions) require.Equal(t, 5_000_000_000, int(accumulatedGas)) @@ -239,7 +239,7 @@ func TestTxCache_SelectTransactions_WhenTransactionsAddedInReversedNonceOrder(t func TestTxCache_selectTransactionsFromBunches(t *testing.T) { t.Run("empty cache", func(t *testing.T) { accountNonceProvider := txcachemocks.NewAccountNonceProviderMock() - merged, accumulatedGas := selectTransactionsFromBunches(accountNonceProvider, []bunchOfTransactions{}, 10_000_000_000, math.MaxInt) + merged, accumulatedGas := selectTransactionsFromBunches(accountNonceProvider, []bunchOfTransactions{}, 10_000_000_000, math.MaxInt, oneSecond) require.Equal(t, 0, len(merged)) require.Equal(t, uint64(0), accumulatedGas) @@ -254,7 +254,7 @@ func TestBenchmarkTxCache_selectTransactionsFromBunches(t *testing.T) { bunches := createBunchesOfTransactionsWithUniformDistribution(1000, 1000) sw.Start(t.Name()) - merged, accumulatedGas := selectTransactionsFromBunches(accountNonceProvider, bunches, 10_000_000_000, math.MaxInt) + merged, accumulatedGas := selectTransactionsFromBunches(accountNonceProvider, bunches, 10_000_000_000, math.MaxInt, oneSecond) sw.Stop(t.Name()) require.Equal(t, 200000, len(merged)) @@ -266,7 +266,7 @@ func TestBenchmarkTxCache_selectTransactionsFromBunches(t *testing.T) { bunches := createBunchesOfTransactionsWithUniformDistribution(1000, 1000) sw.Start(t.Name()) - merged, accumulatedGas := selectTransactionsFromBunches(accountNonceProvider, bunches, 10_000_000_000, math.MaxInt) + merged, accumulatedGas := selectTransactionsFromBunches(accountNonceProvider, bunches, 10_000_000_000, math.MaxInt, oneSecond) sw.Stop(t.Name()) require.Equal(t, 200000, len(merged)) @@ -278,7 +278,7 @@ func TestBenchmarkTxCache_selectTransactionsFromBunches(t *testing.T) { bunches := createBunchesOfTransactionsWithUniformDistribution(100000, 3) sw.Start(t.Name()) - merged, accumulatedGas := selectTransactionsFromBunches(accountNonceProvider, bunches, 10_000_000_000, math.MaxInt) + merged, accumulatedGas := selectTransactionsFromBunches(accountNonceProvider, bunches, 10_000_000_000, math.MaxInt, oneSecond) sw.Stop(t.Name()) require.Equal(t, 200000, len(merged)) @@ -290,7 +290,7 @@ func TestBenchmarkTxCache_selectTransactionsFromBunches(t *testing.T) { bunches := createBunchesOfTransactionsWithUniformDistribution(300000, 1) sw.Start(t.Name()) - merged, accumulatedGas := selectTransactionsFromBunches(accountNonceProvider, bunches, 10_000_000_000, math.MaxInt) + merged, accumulatedGas := selectTransactionsFromBunches(accountNonceProvider, bunches, 10_000_000_000, math.MaxInt, oneSecond) sw.Stop(t.Name()) require.Equal(t, 200000, len(merged)) @@ -341,7 +341,7 @@ func TestBenchmarkTxCache_doSelectTransactions(t *testing.T) { require.Equal(t, 100000, int(cache.CountTx())) sw.Start(t.Name()) - merged, accumulatedGas := cache.SelectTransactions(accountNonceProvider, 10_000_000_000, 50_000) + merged, accumulatedGas := cache.SelectTransactions(accountNonceProvider, 10_000_000_000, 50_000, oneSecond) sw.Stop(t.Name()) require.Equal(t, 50000, len(merged)) @@ -357,7 +357,7 @@ func TestBenchmarkTxCache_doSelectTransactions(t *testing.T) { require.Equal(t, 100000, int(cache.CountTx())) sw.Start(t.Name()) - merged, accumulatedGas := cache.SelectTransactions(accountNonceProvider, 10_000_000_000, 50_000) + merged, accumulatedGas := cache.SelectTransactions(accountNonceProvider, 10_000_000_000, 50_000, oneSecond) sw.Stop(t.Name()) require.Equal(t, 50000, len(merged)) @@ -373,7 +373,7 @@ func TestBenchmarkTxCache_doSelectTransactions(t *testing.T) { require.Equal(t, 300000, int(cache.CountTx())) sw.Start(t.Name()) - merged, accumulatedGas := cache.SelectTransactions(accountNonceProvider, 10_000_000_000, 50_000) + merged, accumulatedGas := cache.SelectTransactions(accountNonceProvider, 10_000_000_000, 50_000, oneSecond) sw.Stop(t.Name()) require.Equal(t, 50000, len(merged)) diff --git a/txcache/testutils_test.go b/txcache/testutils_test.go index a2405be5..41b37683 100644 --- a/txcache/testutils_test.go +++ b/txcache/testutils_test.go @@ -13,6 +13,7 @@ import ( const oneMilion = 1000000 const oneBillion = oneMilion * 1000 const estimatedSizeOfBoundedTxFields = uint64(128) +const oneSecond = time.Second func (cache *TxCache) areInternalMapsConsistent() bool { internalMapByHash := cache.txByHash diff --git a/txcache/txCache.go b/txcache/txCache.go index 0ee247e6..a489e69f 100644 --- a/txcache/txCache.go +++ b/txcache/txCache.go @@ -2,6 +2,7 @@ package txcache import ( "sync" + "time" "github.com/multiversx/mx-chain-core-go/core" "github.com/multiversx/mx-chain-core-go/core/atomic" @@ -99,7 +100,7 @@ func (cache *TxCache) GetByTxHash(txHash []byte) (*WrappedTransaction, bool) { // SelectTransactions selects the best transactions to be included in the next miniblock. // It returns up to "maxNum" transactions, with total gas <= "gasRequested". -func (cache *TxCache) SelectTransactions(accountNonceProvider AccountNonceProvider, gasRequested uint64, maxNum int) ([]*WrappedTransaction, uint64) { +func (cache *TxCache) SelectTransactions(accountNonceProvider AccountNonceProvider, gasRequested uint64, maxNum int, selectionLoopMaximumDuration time.Duration) ([]*WrappedTransaction, uint64) { if check.IfNil(accountNonceProvider) { log.Error("TxCache.SelectTransactions", "err", common.ErrNilAccountNonceProvider) return nil, 0 @@ -115,7 +116,7 @@ func (cache *TxCache) SelectTransactions(accountNonceProvider AccountNonceProvid "num senders", cache.CountSenders(), ) - transactions, accumulatedGas := cache.doSelectTransactions(accountNonceProvider, gasRequested, maxNum) + transactions, accumulatedGas := cache.doSelectTransactions(accountNonceProvider, gasRequested, maxNum, selectionLoopMaximumDuration) stopWatch.Stop("selection") diff --git a/txcache/txCache_test.go b/txcache/txCache_test.go index de6daf15..42c0f978 100644 --- a/txcache/txCache_test.go +++ b/txcache/txCache_test.go @@ -463,7 +463,7 @@ func TestTxCache_ConcurrentMutationAndSelection(t *testing.T) { go func() { for i := 0; i < 100; i++ { fmt.Println("Selection", i) - _, _ = cache.SelectTransactions(accountNonceProvider, math.MaxUint64, math.MaxInt) + _, _ = cache.SelectTransactions(accountNonceProvider, math.MaxUint64, math.MaxInt, oneSecond) } wg.Done() From 3bbf408cc8f550022cb9985edeae2db81c8ebd89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Mon, 18 Nov 2024 15:11:20 +0200 Subject: [PATCH 14/34] AccountNonceProvider becomes AccountStateProvider (more information from account state is necessary). --- common/errors.go | 4 +- .../txcachemocks/accountNonceProviderMock.go | 37 ----- .../txcachemocks/accountStateProviderMock.go | 39 +++++ txcache/constants.go | 2 +- txcache/interface.go | 7 +- txcache/selection.go | 30 ++-- txcache/selection_test.go | 141 +++++++++++------- txcache/transactionsHeap.go | 18 ++- txcache/txCache.go | 8 +- txcache/txCache_test.go | 4 +- types/accountState.go | 9 ++ 11 files changed, 171 insertions(+), 128 deletions(-) delete mode 100644 testscommon/txcachemocks/accountNonceProviderMock.go create mode 100644 testscommon/txcachemocks/accountStateProviderMock.go create mode 100644 types/accountState.go diff --git a/common/errors.go b/common/errors.go index 326c7d78..1cedf57c 100644 --- a/common/errors.go +++ b/common/errors.go @@ -75,8 +75,8 @@ var ErrNilTimeCache = errors.New("nil time cache") // ErrNilTxGasHandler signals that a nil tx gas handler was provided var ErrNilTxGasHandler = errors.New("nil tx gas handler") -// ErrNilAccountNonceProvider signals that a nil account nonce provider was provided -var ErrNilAccountNonceProvider = errors.New("nil account nonce provider") +// ErrNilAccountStateProvider signals that a nil account state provider was provided +var ErrNilAccountStateProvider = errors.New("nil account state provider") // ErrNilStoredDataFactory signals that a nil stored data factory has been provided var ErrNilStoredDataFactory = errors.New("nil stored data factory") diff --git a/testscommon/txcachemocks/accountNonceProviderMock.go b/testscommon/txcachemocks/accountNonceProviderMock.go deleted file mode 100644 index bc0abe71..00000000 --- a/testscommon/txcachemocks/accountNonceProviderMock.go +++ /dev/null @@ -1,37 +0,0 @@ -package txcachemocks - -import ( - "errors" -) - -// AccountNonceProviderMock - -type AccountNonceProviderMock struct { - NoncesByAddress map[string]uint64 - GetAccountNonceCalled func(address []byte) (uint64, error) -} - -// NewAccountNonceProviderMock - -func NewAccountNonceProviderMock() *AccountNonceProviderMock { - return &AccountNonceProviderMock{ - NoncesByAddress: make(map[string]uint64), - } -} - -// GetAccountNonce - -func (stub *AccountNonceProviderMock) GetAccountNonce(address []byte) (uint64, error) { - if stub.GetAccountNonceCalled != nil { - return stub.GetAccountNonceCalled(address) - } - - nonce, ok := stub.NoncesByAddress[string(address)] - if !ok { - return 0, errors.New("cannot get nonce") - } - - return nonce, nil -} - -// IsInterfaceNil - -func (stub *AccountNonceProviderMock) IsInterfaceNil() bool { - return stub == nil -} diff --git a/testscommon/txcachemocks/accountStateProviderMock.go b/testscommon/txcachemocks/accountStateProviderMock.go new file mode 100644 index 00000000..c770d0e8 --- /dev/null +++ b/testscommon/txcachemocks/accountStateProviderMock.go @@ -0,0 +1,39 @@ +package txcachemocks + +import ( + "errors" + + "github.com/multiversx/mx-chain-storage-go/types" +) + +// AccountStateProviderMock - +type AccountStateProviderMock struct { + AccountStateByAddress map[string]*types.AccountState + GetAccountStateCalled func(address []byte) (*types.AccountState, error) +} + +// NewAccountStateProviderMock - +func NewAccountStateProviderMock() *AccountStateProviderMock { + return &AccountStateProviderMock{ + AccountStateByAddress: make(map[string]*types.AccountState), + } +} + +// GetAccountState - +func (stub *AccountStateProviderMock) GetAccountState(address []byte) (*types.AccountState, error) { + if stub.GetAccountStateCalled != nil { + return stub.GetAccountStateCalled(address) + } + + state, ok := stub.AccountStateByAddress[string(address)] + if !ok { + return nil, errors.New("cannot get state") + } + + return state, nil +} + +// IsInterfaceNil - +func (stub *AccountStateProviderMock) IsInterfaceNil() bool { + return stub == nil +} diff --git a/txcache/constants.go b/txcache/constants.go index d44a8ef4..811cd4b5 100644 --- a/txcache/constants.go +++ b/txcache/constants.go @@ -3,4 +3,4 @@ package txcache const diagnosisMaxTransactionsToDisplay = 10000 const diagnosisSelectionGasRequested = 10_000_000_000 const initialCapacityOfSelectionSlice = 30000 -const selectionLoopDurationCheckInterval = 64 +const selectionLoopDurationCheckInterval = 16 diff --git a/txcache/interface.go b/txcache/interface.go index 9dab7cf0..ddad55fa 100644 --- a/txcache/interface.go +++ b/txcache/interface.go @@ -4,6 +4,7 @@ import ( "math/big" "github.com/multiversx/mx-chain-core-go/data" + "github.com/multiversx/mx-chain-storage-go/types" ) // TxGasHandler handles a transaction gas and gas cost @@ -12,9 +13,9 @@ type TxGasHandler interface { IsInterfaceNil() bool } -// AccountNonceProvider defines the behavior of a component able to provide the nonce for an account -type AccountNonceProvider interface { - GetAccountNonce(accountKey []byte) (uint64, error) +// AccountStateProvider defines the behavior of a component able to provide the state of an account +type AccountStateProvider interface { + GetAccountState(accountKey []byte) (*types.AccountState, error) IsInterfaceNil() bool } diff --git a/txcache/selection.go b/txcache/selection.go index 9aa9a351..d328cc58 100644 --- a/txcache/selection.go +++ b/txcache/selection.go @@ -7,7 +7,7 @@ import ( logger "github.com/multiversx/mx-chain-logger-go" ) -func (cache *TxCache) doSelectTransactions(accountNonceProvider AccountNonceProvider, gasRequested uint64, maxNum int, selectionLoopMaximumDuration time.Duration) (bunchOfTransactions, uint64) { +func (cache *TxCache) doSelectTransactions(accountStateProvider AccountStateProvider, gasRequested uint64, maxNum int, selectionLoopMaximumDuration time.Duration) (bunchOfTransactions, uint64) { senders := cache.getSenders() bunches := make([]bunchOfTransactions, 0, len(senders)) @@ -15,11 +15,11 @@ func (cache *TxCache) doSelectTransactions(accountNonceProvider AccountNonceProv bunches = append(bunches, sender.getSequentialTxs()) } - return selectTransactionsFromBunches(accountNonceProvider, bunches, gasRequested, maxNum, selectionLoopMaximumDuration) + return selectTransactionsFromBunches(accountStateProvider, bunches, gasRequested, maxNum, selectionLoopMaximumDuration) } // Selection tolerates concurrent transaction additions / removals. -func selectTransactionsFromBunches(accountNonceProvider AccountNonceProvider, bunches []bunchOfTransactions, gasRequested uint64, maxNum int, selectionLoopMaximumDuration time.Duration) (bunchOfTransactions, uint64) { +func selectTransactionsFromBunches(accountStateProvider AccountStateProvider, bunches []bunchOfTransactions, gasRequested uint64, maxNum int, selectionLoopMaximumDuration time.Duration) (bunchOfTransactions, uint64) { selectedTransactions := make(bunchOfTransactions, 0, initialCapacityOfSelectionSlice) // Items popped from the heap are added to "selectedTransactions". @@ -64,16 +64,16 @@ func selectTransactionsFromBunches(accountNonceProvider AccountNonceProvider, bu } } - requestAccountNonceIfNecessary(accountNonceProvider, item) + requestAccountStateIfNecessary(accountStateProvider, item) - isInitialGap := item.transactionIndex == 0 && item.senderNonceProvided && nonce > item.senderNonce + isInitialGap := item.transactionIndex == 0 && item.senderStateProvided && nonce > item.senderState.Nonce if isInitialGap { if logSelect.GetLevel() <= logger.LogTrace { logSelect.Trace("TxCache.selectTransactionsFromBunches, initial gap", "tx", item.transaction.TxHash, "nonce", nonce, "sender", item.transaction.Tx.GetSndAddr(), - "senderNonce", item.senderNonce, + "senderState.Nonce", item.senderState.Nonce, ) } @@ -82,14 +82,14 @@ func selectTransactionsFromBunches(accountNonceProvider AccountNonceProvider, bu continue } - isLowerNonce := item.senderNonceProvided && nonce < item.senderNonce + isLowerNonce := item.senderStateProvided && nonce < item.senderState.Nonce if isLowerNonce { if logSelect.GetLevel() <= logger.LogTrace { logSelect.Trace("TxCache.selectTransactionsFromBunches, lower nonce", "tx", item.transaction.TxHash, "nonce", nonce, "sender", item.transaction.Tx.GetSndAddr(), - "senderNonce", item.senderNonce, + "senderState.Nonce", item.senderState.Nonce, ) } @@ -113,21 +113,21 @@ func selectTransactionsFromBunches(accountNonceProvider AccountNonceProvider, bu return selectedTransactions, accumulatedGas } -func requestAccountNonceIfNecessary(accountNonceProvider AccountNonceProvider, item *transactionsHeapItem) { - if item.senderNonceRequested { +func requestAccountStateIfNecessary(accountStateProvider AccountStateProvider, item *transactionsHeapItem) { + if item.senderStateRequested { return } - item.senderNonceRequested = true + item.senderStateRequested = true sender := item.transaction.Tx.GetSndAddr() - senderNonce, err := accountNonceProvider.GetAccountNonce(sender) + senderState, err := accountStateProvider.GetAccountState(sender) if err != nil { // Hazardous; should never happen. - logSelect.Debug("TxCache.requestAccountNonceIfNecessary: nonce not available", "sender", sender, "err", err) + logSelect.Debug("TxCache.requestAccountStateIfNecessary: nonce not available", "sender", sender, "err", err) return } - item.senderNonceProvided = true - item.senderNonce = senderNonce + item.senderStateProvided = true + item.senderState = senderState } diff --git a/txcache/selection_test.go b/txcache/selection_test.go index 37e706d5..9044ea58 100644 --- a/txcache/selection_test.go +++ b/txcache/selection_test.go @@ -4,16 +4,18 @@ import ( "fmt" "math" "testing" + "time" "github.com/multiversx/mx-chain-core-go/core" "github.com/multiversx/mx-chain-storage-go/testscommon/txcachemocks" + "github.com/multiversx/mx-chain-storage-go/types" "github.com/stretchr/testify/require" ) func TestTxCache_SelectTransactions_Dummy(t *testing.T) { t.Run("all having same PPU", func(t *testing.T) { cache := newUnconstrainedCacheToTest() - accountNonceProvider := txcachemocks.NewAccountNonceProviderMock() + accountStateProvider := txcachemocks.NewAccountStateProviderMock() cache.AddTx(createTx([]byte("hash-alice-4"), "alice", 4)) cache.AddTx(createTx([]byte("hash-alice-3"), "alice", 3)) @@ -24,7 +26,7 @@ func TestTxCache_SelectTransactions_Dummy(t *testing.T) { cache.AddTx(createTx([]byte("hash-bob-5"), "bob", 5)) cache.AddTx(createTx([]byte("hash-carol-1"), "carol", 1)) - selected, accumulatedGas := cache.SelectTransactions(accountNonceProvider, math.MaxUint64, math.MaxInt, oneSecond) + selected, accumulatedGas := cache.SelectTransactions(accountStateProvider, math.MaxUint64, math.MaxInt, oneSecond) require.Len(t, selected, 8) require.Equal(t, 400000, int(accumulatedGas)) @@ -41,13 +43,13 @@ func TestTxCache_SelectTransactions_Dummy(t *testing.T) { t.Run("alice > carol > bob", func(t *testing.T) { cache := newUnconstrainedCacheToTest() - accountNonceProvider := txcachemocks.NewAccountNonceProviderMock() + accountStateProvider := txcachemocks.NewAccountStateProviderMock() cache.AddTx(createTx([]byte("hash-alice-1"), "alice", 1).withGasPrice(100)) cache.AddTx(createTx([]byte("hash-bob-5"), "bob", 5).withGasPrice(50)) cache.AddTx(createTx([]byte("hash-carol-3"), "carol", 3).withGasPrice(75)) - selected, accumulatedGas := cache.SelectTransactions(accountNonceProvider, math.MaxUint64, math.MaxInt, oneSecond) + selected, accumulatedGas := cache.SelectTransactions(accountStateProvider, math.MaxUint64, math.MaxInt, oneSecond) require.Len(t, selected, 3) require.Equal(t, 150000, int(accumulatedGas)) @@ -61,7 +63,7 @@ func TestTxCache_SelectTransactions_Dummy(t *testing.T) { func TestTxCache_SelectTransactionsWithBandwidth_Dummy(t *testing.T) { t.Run("transactions with no data field", func(t *testing.T) { cache := newUnconstrainedCacheToTest() - accountNonceProvider := txcachemocks.NewAccountNonceProviderMock() + accountStateProvider := txcachemocks.NewAccountStateProviderMock() cache.AddTx(createTx([]byte("hash-alice-4"), "alice", 4).withGasLimit(100000)) cache.AddTx(createTx([]byte("hash-alice-3"), "alice", 3).withGasLimit(100000)) @@ -72,7 +74,7 @@ func TestTxCache_SelectTransactionsWithBandwidth_Dummy(t *testing.T) { cache.AddTx(createTx([]byte("hash-bob-5"), "bob", 5).withGasLimit(50000)) cache.AddTx(createTx([]byte("hash-carol-1"), "carol", 1).withGasLimit(50000)) - selected, accumulatedGas := cache.SelectTransactions(accountNonceProvider, 760000, math.MaxInt, oneSecond) + selected, accumulatedGas := cache.SelectTransactions(accountStateProvider, 760000, math.MaxInt, oneSecond) require.Len(t, selected, 5) require.Equal(t, 750000, int(accumulatedGas)) @@ -88,7 +90,7 @@ func TestTxCache_SelectTransactionsWithBandwidth_Dummy(t *testing.T) { func TestTxCache_SelectTransactions_HandlesGapsAndLowerNonces(t *testing.T) { t.Run("with middle gaps", func(t *testing.T) { cache := newUnconstrainedCacheToTest() - accountNonceProvider := txcachemocks.NewAccountNonceProviderMock() + accountStateProvider := txcachemocks.NewAccountStateProviderMock() cache.AddTx(createTx([]byte("hash-alice-1"), "alice", 1)) cache.AddTx(createTx([]byte("hash-alice-2"), "alice", 2)) @@ -102,7 +104,7 @@ func TestTxCache_SelectTransactions_HandlesGapsAndLowerNonces(t *testing.T) { cache.AddTx(createTx([]byte("hash-carol-10"), "carol", 10)) // gap cache.AddTx(createTx([]byte("hash-carol-11"), "carol", 11)) - sorted, accumulatedGas := cache.SelectTransactions(accountNonceProvider, math.MaxUint64, math.MaxInt, oneSecond) + sorted, accumulatedGas := cache.SelectTransactions(accountStateProvider, math.MaxUint64, math.MaxInt, oneSecond) expectedNumSelected := 3 + 1 + 2 // 3 alice + 1 bob + 2 carol require.Len(t, sorted, expectedNumSelected) require.Equal(t, 300000, int(accumulatedGas)) @@ -110,11 +112,15 @@ func TestTxCache_SelectTransactions_HandlesGapsAndLowerNonces(t *testing.T) { t.Run("with initial gaps", func(t *testing.T) { cache := newUnconstrainedCacheToTest() - accountNonceProvider := txcachemocks.NewAccountNonceProviderMock() + accountStateProvider := txcachemocks.NewAccountStateProviderMock() - noncesByAddress := accountNonceProvider.NoncesByAddress - noncesByAddress["alice"] = 1 - noncesByAddress["bob"] = 42 + noncesByAddress := accountStateProvider.AccountStateByAddress + noncesByAddress["alice"] = &types.AccountState{ + Nonce: 1, + } + noncesByAddress["bob"] = &types.AccountState{ + Nonce: 42, + } // No gap cache.AddTx(createTx([]byte("hash-alice-1"), "alice", 1)) @@ -130,7 +136,7 @@ func TestTxCache_SelectTransactions_HandlesGapsAndLowerNonces(t *testing.T) { cache.AddTx(createTx([]byte("hash-carol-7"), "carol", 7)) cache.AddTx(createTx([]byte("hash-carol-8"), "carol", 8)) - sorted, accumulatedGas := cache.SelectTransactions(accountNonceProvider, math.MaxUint64, math.MaxInt, oneSecond) + sorted, accumulatedGas := cache.SelectTransactions(accountStateProvider, math.MaxUint64, math.MaxInt, oneSecond) expectedNumSelected := 3 + 0 + 2 // 3 alice + 0 bob + 2 carol require.Len(t, sorted, expectedNumSelected) require.Equal(t, 250000, int(accumulatedGas)) @@ -138,11 +144,15 @@ func TestTxCache_SelectTransactions_HandlesGapsAndLowerNonces(t *testing.T) { t.Run("with lower nonces", func(t *testing.T) { cache := newUnconstrainedCacheToTest() - accountNonceProvider := txcachemocks.NewAccountNonceProviderMock() + accountStateProvider := txcachemocks.NewAccountStateProviderMock() - noncesByAddress := accountNonceProvider.NoncesByAddress - noncesByAddress["alice"] = 1 - noncesByAddress["bob"] = 42 + noncesByAddress := accountStateProvider.AccountStateByAddress + noncesByAddress["alice"] = &types.AccountState{ + Nonce: 1, + } + noncesByAddress["bob"] = &types.AccountState{ + Nonce: 42, + } // Good sequence cache.AddTx(createTx([]byte("hash-alice-1"), "alice", 1)) @@ -158,19 +168,23 @@ func TestTxCache_SelectTransactions_HandlesGapsAndLowerNonces(t *testing.T) { cache.AddTx(createTx([]byte("hash-carol-7"), "carol", 7)) cache.AddTx(createTx([]byte("hash-carol-8"), "carol", 8)) - sorted, accumulatedGas := cache.SelectTransactions(accountNonceProvider, math.MaxUint64, math.MaxInt, oneSecond) + sorted, accumulatedGas := cache.SelectTransactions(accountStateProvider, math.MaxUint64, math.MaxInt, oneSecond) expectedNumSelected := 3 + 1 + 2 // 3 alice + 1 bob + 2 carol require.Len(t, sorted, expectedNumSelected) require.Equal(t, 300000, int(accumulatedGas)) }) } -func TestTxCache_requestAccountNonceIfNecessary(t *testing.T) { - accountNonceProvider := txcachemocks.NewAccountNonceProviderMock() +func TestTxCache_requestAccountStateIfNecessary(t *testing.T) { + accountStateProvider := txcachemocks.NewAccountStateProviderMock() - noncesByAddress := accountNonceProvider.NoncesByAddress - noncesByAddress["alice"] = 7 - noncesByAddress["bob"] = 42 + noncesByAddress := accountStateProvider.AccountStateByAddress + noncesByAddress["alice"] = &types.AccountState{ + Nonce: 7, + } + noncesByAddress["bob"] = &types.AccountState{ + Nonce: 42, + } a := &transactionsHeapItem{ transaction: createTx([]byte("hash-alice-1"), "alice", 1), @@ -182,25 +196,25 @@ func TestTxCache_requestAccountNonceIfNecessary(t *testing.T) { c := &transactionsHeapItem{} - requestAccountNonceIfNecessary(accountNonceProvider, a) - requestAccountNonceIfNecessary(accountNonceProvider, b) + requestAccountStateIfNecessary(accountStateProvider, a) + requestAccountStateIfNecessary(accountStateProvider, b) - require.True(t, a.senderNonceRequested) - require.True(t, a.senderNonceProvided) - require.Equal(t, uint64(7), a.senderNonce) + require.True(t, a.senderStateRequested) + require.True(t, a.senderStateProvided) + require.Equal(t, uint64(7), a.senderState.Nonce) - require.True(t, b.senderNonceRequested) - require.True(t, b.senderNonceProvided) - require.Equal(t, uint64(42), b.senderNonce) + require.True(t, b.senderStateRequested) + require.True(t, b.senderStateProvided) + require.Equal(t, uint64(42), b.senderState.Nonce) - require.False(t, c.senderNonceRequested) - require.False(t, c.senderNonceProvided) - require.Equal(t, uint64(0), c.senderNonce) + require.False(t, c.senderStateRequested) + require.False(t, c.senderStateProvided) + require.Nil(t, c.senderState) } func TestTxCache_SelectTransactions_WhenTransactionsAddedInReversedNonceOrder(t *testing.T) { cache := newUnconstrainedCacheToTest() - accountNonceProvider := txcachemocks.NewAccountNonceProviderMock() + accountStateProvider := txcachemocks.NewAccountStateProviderMock() // Add "nSenders" * "nTransactionsPerSender" transactions in the cache (in reversed nonce order) nSenders := 1000 @@ -219,7 +233,7 @@ func TestTxCache_SelectTransactions_WhenTransactionsAddedInReversedNonceOrder(t require.Equal(t, uint64(nTotalTransactions), cache.CountTx()) - sorted, accumulatedGas := cache.SelectTransactions(accountNonceProvider, math.MaxUint64, math.MaxInt, oneSecond) + sorted, accumulatedGas := cache.SelectTransactions(accountStateProvider, math.MaxUint64, math.MaxInt, oneSecond) require.Len(t, sorted, nTotalTransactions) require.Equal(t, 5_000_000_000, int(accumulatedGas)) @@ -238,10 +252,10 @@ func TestTxCache_SelectTransactions_WhenTransactionsAddedInReversedNonceOrder(t func TestTxCache_selectTransactionsFromBunches(t *testing.T) { t.Run("empty cache", func(t *testing.T) { - accountNonceProvider := txcachemocks.NewAccountNonceProviderMock() - merged, accumulatedGas := selectTransactionsFromBunches(accountNonceProvider, []bunchOfTransactions{}, 10_000_000_000, math.MaxInt, oneSecond) + accountStateProvider := txcachemocks.NewAccountStateProviderMock() + selected, accumulatedGas := selectTransactionsFromBunches(accountStateProvider, []bunchOfTransactions{}, 10_000_000_000, math.MaxInt, oneSecond) - require.Equal(t, 0, len(merged)) + require.Equal(t, 0, len(selected)) require.Equal(t, uint64(0), accumulatedGas) }) } @@ -250,50 +264,50 @@ func TestBenchmarkTxCache_selectTransactionsFromBunches(t *testing.T) { sw := core.NewStopWatch() t.Run("numSenders = 1000, numTransactions = 1000", func(t *testing.T) { - accountNonceProvider := txcachemocks.NewAccountNonceProviderMock() + accountStateProvider := txcachemocks.NewAccountStateProviderMock() bunches := createBunchesOfTransactionsWithUniformDistribution(1000, 1000) sw.Start(t.Name()) - merged, accumulatedGas := selectTransactionsFromBunches(accountNonceProvider, bunches, 10_000_000_000, math.MaxInt, oneSecond) + selected, accumulatedGas := selectTransactionsFromBunches(accountStateProvider, bunches, 10_000_000_000, math.MaxInt, oneSecond) sw.Stop(t.Name()) - require.Equal(t, 200000, len(merged)) + require.Equal(t, 200000, len(selected)) require.Equal(t, uint64(10_000_000_000), accumulatedGas) }) t.Run("numSenders = 10000, numTransactions = 100", func(t *testing.T) { - accountNonceProvider := txcachemocks.NewAccountNonceProviderMock() + accountStateProvider := txcachemocks.NewAccountStateProviderMock() bunches := createBunchesOfTransactionsWithUniformDistribution(1000, 1000) sw.Start(t.Name()) - merged, accumulatedGas := selectTransactionsFromBunches(accountNonceProvider, bunches, 10_000_000_000, math.MaxInt, oneSecond) + selected, accumulatedGas := selectTransactionsFromBunches(accountStateProvider, bunches, 10_000_000_000, math.MaxInt, oneSecond) sw.Stop(t.Name()) - require.Equal(t, 200000, len(merged)) + require.Equal(t, 200000, len(selected)) require.Equal(t, uint64(10_000_000_000), accumulatedGas) }) t.Run("numSenders = 100000, numTransactions = 3", func(t *testing.T) { - accountNonceProvider := txcachemocks.NewAccountNonceProviderMock() + accountStateProvider := txcachemocks.NewAccountStateProviderMock() bunches := createBunchesOfTransactionsWithUniformDistribution(100000, 3) sw.Start(t.Name()) - merged, accumulatedGas := selectTransactionsFromBunches(accountNonceProvider, bunches, 10_000_000_000, math.MaxInt, oneSecond) + selected, accumulatedGas := selectTransactionsFromBunches(accountStateProvider, bunches, 10_000_000_000, math.MaxInt, oneSecond) sw.Stop(t.Name()) - require.Equal(t, 200000, len(merged)) + require.Equal(t, 200000, len(selected)) require.Equal(t, uint64(10_000_000_000), accumulatedGas) }) t.Run("numSenders = 300000, numTransactions = 1", func(t *testing.T) { - accountNonceProvider := txcachemocks.NewAccountNonceProviderMock() + accountStateProvider := txcachemocks.NewAccountStateProviderMock() bunches := createBunchesOfTransactionsWithUniformDistribution(300000, 1) sw.Start(t.Name()) - merged, accumulatedGas := selectTransactionsFromBunches(accountNonceProvider, bunches, 10_000_000_000, math.MaxInt, oneSecond) + selected, accumulatedGas := selectTransactionsFromBunches(accountStateProvider, bunches, 10_000_000_000, math.MaxInt, oneSecond) sw.Stop(t.Name()) - require.Equal(t, 200000, len(merged)) + require.Equal(t, 200000, len(selected)) require.Equal(t, uint64(10_000_000_000), accumulatedGas) }) @@ -315,6 +329,17 @@ func TestBenchmarkTxCache_selectTransactionsFromBunches(t *testing.T) { // 0.219072s (TestTxCache_selectTransactionsFromBunches/numSenders_=_300000,_numTransactions_=_1) } +func TestTxCache_selectTransactionsFromBunches_lookBreaks_whenTakesTooLong(t *testing.T) { + t.Run("numSenders = 300000, numTransactions = 1", func(t *testing.T) { + accountStateProvider := txcachemocks.NewAccountStateProviderMock() + bunches := createBunchesOfTransactionsWithUniformDistribution(300000, 1) + selected, accumulatedGas := selectTransactionsFromBunches(accountStateProvider, bunches, 10_000_000_000, 50_000, 1*time.Millisecond) + + require.Less(t, len(selected), 50_000) + require.Less(t, int(accumulatedGas), 10_000_000_000) + }) +} + func TestBenchmarkTxCache_doSelectTransactions(t *testing.T) { config := ConfigSourceMe{ Name: "untitled", @@ -328,7 +353,7 @@ func TestBenchmarkTxCache_doSelectTransactions(t *testing.T) { } txGasHandler := txcachemocks.NewTxGasHandlerMock() - accountNonceProvider := txcachemocks.NewAccountNonceProviderMock() + accountStateProvider := txcachemocks.NewAccountStateProviderMock() sw := core.NewStopWatch() @@ -341,10 +366,10 @@ func TestBenchmarkTxCache_doSelectTransactions(t *testing.T) { require.Equal(t, 100000, int(cache.CountTx())) sw.Start(t.Name()) - merged, accumulatedGas := cache.SelectTransactions(accountNonceProvider, 10_000_000_000, 50_000, oneSecond) + selected, accumulatedGas := cache.SelectTransactions(accountStateProvider, 10_000_000_000, 50_000, oneSecond) sw.Stop(t.Name()) - require.Equal(t, 50000, len(merged)) + require.Equal(t, 50000, len(selected)) require.Equal(t, uint64(2_500_000_000), accumulatedGas) }) @@ -357,10 +382,10 @@ func TestBenchmarkTxCache_doSelectTransactions(t *testing.T) { require.Equal(t, 100000, int(cache.CountTx())) sw.Start(t.Name()) - merged, accumulatedGas := cache.SelectTransactions(accountNonceProvider, 10_000_000_000, 50_000, oneSecond) + selected, accumulatedGas := cache.SelectTransactions(accountStateProvider, 10_000_000_000, 50_000, oneSecond) sw.Stop(t.Name()) - require.Equal(t, 50000, len(merged)) + require.Equal(t, 50000, len(selected)) require.Equal(t, uint64(2_500_000_000), accumulatedGas) }) @@ -373,10 +398,10 @@ func TestBenchmarkTxCache_doSelectTransactions(t *testing.T) { require.Equal(t, 300000, int(cache.CountTx())) sw.Start(t.Name()) - merged, accumulatedGas := cache.SelectTransactions(accountNonceProvider, 10_000_000_000, 50_000, oneSecond) + selected, accumulatedGas := cache.SelectTransactions(accountStateProvider, 10_000_000_000, 50_000, oneSecond) sw.Stop(t.Name()) - require.Equal(t, 50000, len(merged)) + require.Equal(t, 50000, len(selected)) require.Equal(t, uint64(2_500_000_000), accumulatedGas) }) diff --git a/txcache/transactionsHeap.go b/txcache/transactionsHeap.go index d0d8416f..d34054af 100644 --- a/txcache/transactionsHeap.go +++ b/txcache/transactionsHeap.go @@ -1,5 +1,11 @@ package txcache +import ( + "math/big" + + "github.com/multiversx/mx-chain-storage-go/types" +) + type transactionsHeap struct { items []*transactionsHeapItem less func(i, j int) bool @@ -8,12 +14,12 @@ type transactionsHeap struct { type transactionsHeapItem struct { senderIndex int - // Whether the sender's nonce has been requested within a selection session. - senderNonceRequested bool - // Whether the sender's nonce has been requested and provided (with success) within a selection session. - senderNonceProvided bool - // The sender's nonce (if requested and provided). - senderNonce uint64 + // Whether the sender's state has been requested within a selection session. + senderStateRequested bool + // Whether the sender's state has been requested and provided (with success) within a selection session. + senderStateProvided bool + // The sender's state (if requested and provided). + senderState *types.AccountState transactionIndex int transaction *WrappedTransaction diff --git a/txcache/txCache.go b/txcache/txCache.go index a489e69f..92796d60 100644 --- a/txcache/txCache.go +++ b/txcache/txCache.go @@ -100,9 +100,9 @@ func (cache *TxCache) GetByTxHash(txHash []byte) (*WrappedTransaction, bool) { // SelectTransactions selects the best transactions to be included in the next miniblock. // It returns up to "maxNum" transactions, with total gas <= "gasRequested". -func (cache *TxCache) SelectTransactions(accountNonceProvider AccountNonceProvider, gasRequested uint64, maxNum int, selectionLoopMaximumDuration time.Duration) ([]*WrappedTransaction, uint64) { - if check.IfNil(accountNonceProvider) { - log.Error("TxCache.SelectTransactions", "err", common.ErrNilAccountNonceProvider) +func (cache *TxCache) SelectTransactions(accountStateProvider AccountStateProvider, gasRequested uint64, maxNum int, selectionLoopMaximumDuration time.Duration) ([]*WrappedTransaction, uint64) { + if check.IfNil(accountStateProvider) { + log.Error("TxCache.SelectTransactions", "err", common.ErrNilAccountStateProvider) return nil, 0 } @@ -116,7 +116,7 @@ func (cache *TxCache) SelectTransactions(accountNonceProvider AccountNonceProvid "num senders", cache.CountSenders(), ) - transactions, accumulatedGas := cache.doSelectTransactions(accountNonceProvider, gasRequested, maxNum, selectionLoopMaximumDuration) + transactions, accumulatedGas := cache.doSelectTransactions(accountStateProvider, gasRequested, maxNum, selectionLoopMaximumDuration) stopWatch.Stop("selection") diff --git a/txcache/txCache_test.go b/txcache/txCache_test.go index 42c0f978..263d697a 100644 --- a/txcache/txCache_test.go +++ b/txcache/txCache_test.go @@ -448,7 +448,7 @@ func Test_IsInterfaceNil(t *testing.T) { func TestTxCache_ConcurrentMutationAndSelection(t *testing.T) { cache := newUnconstrainedCacheToTest() - accountNonceProvider := txcachemocks.NewAccountNonceProviderMock() + accountStateProvider := txcachemocks.NewAccountStateProviderMock() // Alice will quickly move between two score buckets (chunks) cheapTransaction := createTx([]byte("alice-x-o"), "alice", 0).withDataLength(1).withGasLimit(300000000).withGasPrice(oneBillion) @@ -463,7 +463,7 @@ func TestTxCache_ConcurrentMutationAndSelection(t *testing.T) { go func() { for i := 0; i < 100; i++ { fmt.Println("Selection", i) - _, _ = cache.SelectTransactions(accountNonceProvider, math.MaxUint64, math.MaxInt, oneSecond) + _, _ = cache.SelectTransactions(accountStateProvider, math.MaxUint64, math.MaxInt, oneSecond) } wg.Done() diff --git a/types/accountState.go b/types/accountState.go new file mode 100644 index 00000000..13c3f326 --- /dev/null +++ b/types/accountState.go @@ -0,0 +1,9 @@ +package types + +import "math/big" + +// AccountState represents the state of an account, as seen by the mempool +type AccountState struct { + Nonce uint64 + Balance *big.Int +} From bae6b4335c64ab29fc4476912214b66b0f0e6122 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Mon, 18 Nov 2024 22:03:23 +0200 Subject: [PATCH 15/34] Additional logs on cross tx cache. --- txcache/crossTxCache.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/txcache/crossTxCache.go b/txcache/crossTxCache.go index f2ca9afe..1a64e77b 100644 --- a/txcache/crossTxCache.go +++ b/txcache/crossTxCache.go @@ -57,6 +57,7 @@ func (cache *CrossTxCache) ImmunizeTxsAgainstEviction(keys [][]byte) { // AddTx adds a transaction in the cache func (cache *CrossTxCache) AddTx(tx *WrappedTransaction) (has, added bool) { + log.Trace("CrossTxCache.AddTx", "name", cache.config.Name, "txHash", tx.TxHash) return cache.HasOrAdd(tx.TxHash, tx, int(tx.Size)) } @@ -93,6 +94,7 @@ func (cache *CrossTxCache) Peek(key []byte) (value interface{}, ok bool) { // RemoveTxByHash removes tx by hash func (cache *CrossTxCache) RemoveTxByHash(txHash []byte) bool { + log.Trace("CrossTxCache.RemoveTxByHash", "name", cache.config.Name, "txHash", txHash) return cache.RemoveWithResult(txHash) } From 613f5ba5c661efa11586e5f93b2f8b09c4e12196 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Mon, 18 Nov 2024 22:19:56 +0200 Subject: [PATCH 16/34] Hold fee on tx, handle accumulated fees. Avoid non-executable transactions on missing balance for fees. Refactor. --- txcache/eviction.go | 6 +----- txcache/selection.go | 28 ++++++++++++++++++++------- txcache/selection_test.go | 19 ++++++++++++------ txcache/transactionsHeap.go | 36 +++++++++++++++++++++++++++++++++++ txcache/wrappedTransaction.go | 7 +++++-- 5 files changed, 76 insertions(+), 20 deletions(-) diff --git a/txcache/eviction.go b/txcache/eviction.go index d82da786..b0d7ed40 100644 --- a/txcache/eviction.go +++ b/txcache/eviction.go @@ -109,11 +109,7 @@ func (cache *TxCache) evictLeastLikelyToSelectTransactions() *evictionJournal { } // Items will be reused (see below). Each sender gets one (and only one) item in the heap. - heap.Push(transactionsHeap, &transactionsHeapItem{ - senderIndex: i, - transactionIndex: 0, - transaction: bunch[0], - }) + heap.Push(transactionsHeap, newTransactionsHeapItem(i, bunch[0])) } for pass := 0; cache.isCapacityExceeded(); pass++ { diff --git a/txcache/selection.go b/txcache/selection.go index d328cc58..5cce6ac7 100644 --- a/txcache/selection.go +++ b/txcache/selection.go @@ -34,11 +34,7 @@ func selectTransactionsFromBunches(accountStateProvider AccountStateProvider, bu } // Items will be reused (see below). Each sender gets one (and only one) item in the heap. - heap.Push(transactionsHeap, &transactionsHeapItem{ - senderIndex: i, - transactionIndex: 0, - transaction: bunch[0], - }) + heap.Push(transactionsHeap, newTransactionsHeapItem(i, bunch[0])) } accumulatedGas := uint64(0) @@ -66,7 +62,7 @@ func selectTransactionsFromBunches(accountStateProvider AccountStateProvider, bu requestAccountStateIfNecessary(accountStateProvider, item) - isInitialGap := item.transactionIndex == 0 && item.senderStateProvided && nonce > item.senderState.Nonce + isInitialGap := item.hasInitialGap() if isInitialGap { if logSelect.GetLevel() <= logger.LogTrace { logSelect.Trace("TxCache.selectTransactionsFromBunches, initial gap", @@ -82,7 +78,23 @@ func selectTransactionsFromBunches(accountStateProvider AccountStateProvider, bu continue } - isLowerNonce := item.senderStateProvided && nonce < item.senderState.Nonce + hasFeeExceededBalance := item.hasFeeExceededBalance() + if hasFeeExceededBalance { + if logSelect.GetLevel() <= logger.LogTrace { + logSelect.Trace("TxCache.selectTransactionsFromBunches, fee exceeded balance", + "tx", item.transaction.TxHash, + "sender", item.transaction.Tx.GetSndAddr(), + "balance", item.senderState.Balance, + "accumulatedFee", item.accumulatedFee, + ) + } + + // Item was popped from the heap, but not used downstream. + // Therefore, the sender is ignored (from now on) in the current selection session. + continue + } + + isLowerNonce := item.isLowerNonce() if isLowerNonce { if logSelect.GetLevel() <= logger.LogTrace { logSelect.Trace("TxCache.selectTransactionsFromBunches, lower nonce", @@ -95,6 +107,8 @@ func selectTransactionsFromBunches(accountStateProvider AccountStateProvider, bu // Transaction isn't selected, but the sender is still in the game (will contribute with other transactions). } else { + item.accumulateFee() + accumulatedGas += gasLimit selectedTransactions = append(selectedTransactions, item.transaction) } diff --git a/txcache/selection_test.go b/txcache/selection_test.go index 9044ea58..407d19ce 100644 --- a/txcache/selection_test.go +++ b/txcache/selection_test.go @@ -3,6 +3,7 @@ package txcache import ( "fmt" "math" + "math/big" "testing" "time" @@ -116,10 +117,12 @@ func TestTxCache_SelectTransactions_HandlesGapsAndLowerNonces(t *testing.T) { noncesByAddress := accountStateProvider.AccountStateByAddress noncesByAddress["alice"] = &types.AccountState{ - Nonce: 1, + Nonce: 1, + Balance: big.NewInt(1000000000000000000), } noncesByAddress["bob"] = &types.AccountState{ - Nonce: 42, + Nonce: 42, + Balance: big.NewInt(1000000000000000000), } // No gap @@ -148,10 +151,12 @@ func TestTxCache_SelectTransactions_HandlesGapsAndLowerNonces(t *testing.T) { noncesByAddress := accountStateProvider.AccountStateByAddress noncesByAddress["alice"] = &types.AccountState{ - Nonce: 1, + Nonce: 1, + Balance: big.NewInt(1000000000000000000), } noncesByAddress["bob"] = &types.AccountState{ - Nonce: 42, + Nonce: 42, + Balance: big.NewInt(1000000000000000000), } // Good sequence @@ -180,10 +185,12 @@ func TestTxCache_requestAccountStateIfNecessary(t *testing.T) { noncesByAddress := accountStateProvider.AccountStateByAddress noncesByAddress["alice"] = &types.AccountState{ - Nonce: 7, + Nonce: 7, + Balance: big.NewInt(1000000000000000000), } noncesByAddress["bob"] = &types.AccountState{ - Nonce: 42, + Nonce: 42, + Balance: big.NewInt(1000000000000000000), } a := &transactionsHeapItem{ diff --git a/txcache/transactionsHeap.go b/txcache/transactionsHeap.go index d34054af..652ffc7c 100644 --- a/txcache/transactionsHeap.go +++ b/txcache/transactionsHeap.go @@ -23,6 +23,42 @@ type transactionsHeapItem struct { transactionIndex int transaction *WrappedTransaction + + accumulatedFee *big.Int +} + +func newTransactionsHeapItem(senderIndex int, firstTransaction *WrappedTransaction) *transactionsHeapItem { + return &transactionsHeapItem{ + senderIndex: senderIndex, + senderStateRequested: false, + senderStateProvided: false, + senderState: nil, + transactionIndex: 0, + transaction: firstTransaction, + accumulatedFee: big.NewInt(0), + } +} + +func (item *transactionsHeapItem) hasInitialGap() bool { + return item.transactionIndex == 0 && item.senderStateProvided && item.transaction.Tx.GetNonce() > item.senderState.Nonce +} + +func (item *transactionsHeapItem) isLowerNonce() bool { + return item.senderStateProvided && item.transaction.Tx.GetNonce() < item.senderState.Nonce +} + +func (item *transactionsHeapItem) hasFeeExceededBalance() bool { + return item.senderStateProvided && item.accumulatedFee.Cmp(item.senderState.Balance) > 0 +} + +func (item *transactionsHeapItem) accumulateFee() { + fee := item.transaction.Fee.Load() + if fee == nil { + // This should never happen. + return + } + + item.accumulatedFee.Add(item.accumulatedFee, fee) } func newMinTransactionsHeap(capacity int) *transactionsHeap { diff --git a/txcache/wrappedTransaction.go b/txcache/wrappedTransaction.go index d5d652fb..0f546493 100644 --- a/txcache/wrappedTransaction.go +++ b/txcache/wrappedTransaction.go @@ -2,6 +2,7 @@ package txcache import ( "bytes" + "math/big" "sync/atomic" "github.com/multiversx/mx-chain-core-go/data" @@ -18,19 +19,21 @@ type WrappedTransaction struct { ReceiverShardID uint32 Size int64 + Fee atomic.Pointer[big.Int] PricePerUnit atomic.Uint64 } // precomputeFields computes (and caches) the (average) price per gas unit. func (wrappedTx *WrappedTransaction) precomputeFields(txGasHandler TxGasHandler) { - fee := txGasHandler.ComputeTxFee(wrappedTx.Tx).Uint64() + fee := txGasHandler.ComputeTxFee(wrappedTx.Tx) gasLimit := wrappedTx.Tx.GetGasLimit() if gasLimit == 0 { return } - wrappedTx.PricePerUnit.Store(fee / gasLimit) + wrappedTx.Fee.Store(fee) + wrappedTx.PricePerUnit.Store(fee.Uint64() / gasLimit) } // Equality is out of scope (not possible in our case). From 95a888a9d0d2979fc3627336503568465544b492 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Mon, 18 Nov 2024 22:22:37 +0200 Subject: [PATCH 17/34] Reference new core-go. --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 5e658ad6..f01532d9 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.20 require ( github.com/hashicorp/golang-lru v0.6.0 github.com/multiversx/concurrent-map v0.1.4 - github.com/multiversx/mx-chain-core-go v1.2.21 + github.com/multiversx/mx-chain-core-go v1.2.23 github.com/multiversx/mx-chain-logger-go v1.0.15 github.com/stretchr/testify v1.7.2 github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d diff --git a/go.sum b/go.sum index 7f61e942..f98609ea 100644 --- a/go.sum +++ b/go.sum @@ -41,8 +41,8 @@ github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= github.com/multiversx/concurrent-map v0.1.4 h1:hdnbM8VE4b0KYJaGY5yJS2aNIW9TFFsUYwbO0993uPI= github.com/multiversx/concurrent-map v0.1.4/go.mod h1:8cWFRJDOrWHOTNSqgYCUvwT7c7eFQ4U2vKMOp4A/9+o= -github.com/multiversx/mx-chain-core-go v1.2.21 h1:+XVKznPTlUU5EFS1A8chtS8fStW60upRIyF4Pgml19I= -github.com/multiversx/mx-chain-core-go v1.2.21/go.mod h1:B5zU4MFyJezmEzCsAHE9YNULmGCm2zbPHvl9hazNxmE= +github.com/multiversx/mx-chain-core-go v1.2.23 h1:8WlCGqJHR2HQ0vN4feJwb7W4VrCwBGIzPPHunOOg5Wc= +github.com/multiversx/mx-chain-core-go v1.2.23/go.mod h1:B5zU4MFyJezmEzCsAHE9YNULmGCm2zbPHvl9hazNxmE= github.com/multiversx/mx-chain-logger-go v1.0.15 h1:HlNdK8etyJyL9NQ+6mIXyKPEBo+wRqOwi3n+m2QIHXc= github.com/multiversx/mx-chain-logger-go v1.0.15/go.mod h1:t3PRKaWB1M+i6gUfD27KXgzLJJC+mAQiN+FLlL1yoGQ= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= From ebe0e12d6cebd8fa9146b6eac82e9831bd36f550 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Mon, 18 Nov 2024 22:49:10 +0200 Subject: [PATCH 18/34] Handle guarded transactions with same nonce. --- txcache/txListForSender.go | 14 ++++++++------ txcache/wrappedTransaction.go | 8 ++++++++ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/txcache/txListForSender.go b/txcache/txListForSender.go index 6277e3c1..66c39aff 100644 --- a/txcache/txListForSender.go +++ b/txcache/txListForSender.go @@ -202,12 +202,14 @@ func (listForSender *txListForSender) getSequentialTxs() []*WrappedTransaction { if !isFirstTx { // Handle duplicates (only transactions with the highest gas price are included; see "findInsertionPlace"). if nonce == previousNonce { - log.Trace("txListForSender.getSequentialTxs, duplicate", "sender", listForSender.sender, "nonce", nonce) - continue - } - - // Handle middle gaps. - if nonce != previousNonce+1 { + if value.IsGuarded { + log.Trace("txListForSender.getSequentialTxs, duplicate, but guarded, will not skip", "sender", listForSender.sender, "nonce", nonce) + } else { + log.Trace("txListForSender.getSequentialTxs, duplicate, will skip", "sender", listForSender.sender, "nonce", nonce) + continue + } + } else if nonce > previousNonce+1 { + // Handle middle gaps. log.Trace("txListForSender.getSequentialTxs, middle gap", "sender", listForSender.sender, "nonce", nonce, "previousNonce", previousNonce) break } diff --git a/txcache/wrappedTransaction.go b/txcache/wrappedTransaction.go index 0f546493..a450a1b3 100644 --- a/txcache/wrappedTransaction.go +++ b/txcache/wrappedTransaction.go @@ -21,6 +21,7 @@ type WrappedTransaction struct { Fee atomic.Pointer[big.Int] PricePerUnit atomic.Uint64 + IsGuarded bool } // precomputeFields computes (and caches) the (average) price per gas unit. @@ -34,6 +35,13 @@ func (wrappedTx *WrappedTransaction) precomputeFields(txGasHandler TxGasHandler) wrappedTx.Fee.Store(fee) wrappedTx.PricePerUnit.Store(fee.Uint64() / gasLimit) + + txAsGuardedTransaction, ok := wrappedTx.Tx.(data.GuardedTransactionHandler) + if !ok { + return + } + + wrappedTx.IsGuarded = len(txAsGuardedTransaction.GetGuardianAddr()) > 0 } // Equality is out of scope (not possible in our case). From c61ce2aabc459e59e39f0b75b173c21754379011 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Mon, 18 Nov 2024 23:25:28 +0200 Subject: [PATCH 19/34] Better readme etc. --- txcache/README.md | 12 +++++++++++- txcache/txListForSender.go | 3 ++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/txcache/README.md b/txcache/README.md index be609ffe..3ddee918 100644 --- a/txcache/README.md +++ b/txcache/README.md @@ -114,7 +114,7 @@ The mempool selects transactions as follows (pseudo-code): func selectTransactions(gasRequested, maxNum): // Setup phase senders := list of all current senders in the mempool, in an arbitrary order - bunchesOfTransactions := sourced from senders; middle-nonces-gap-free, duplicates-free, nicely sorted by nonce + bunchesOfTransactions := sourced from senders; middle-nonces-gap-free, (almost) nonce-duplicates-free, nicely sorted by nonce // Holds selected transactions selectedTransactions := empty @@ -189,3 +189,13 @@ Thus, the mempool selects transactions using an efficient and value-driven algor ### Paragraph 5 On the node's side, the selected transactions are shuffled using a deterministic algorithm. This shuffling ensures that the transaction order remains unpredictable to the proposer, effectively preventing _front-running attacks_. Therefore, being selected first by the mempool does not guarantee that a transaction will be included first in the block. Additionally, selection by the mempool does not ensure inclusion in the very next block, as the proposer has the final authority on which transactions to include, based on **the remaining space available** in the block. + +### Order of transactions of the same sender + +Transactions from the same sender are organized based on specific rules to ensure proper sequencing for the selection flow: + +1. **Nonce ascending**: transactions are primarily sorted by their nonce values in ascending order. This sequence ensures that the transactions are processed in the order intended by the sender, as the nonce represents the transaction number in the sender's sequence. + +2. **Gas price descending (same nonce)**: if multiple transactions share the same nonce, they are sorted by their gas prices in descending order - transactions offering higher gas prices are prioritized. This mechanism allows one to easily override a pending transaction with a higher gas price. + +3. **Hash ascending (same nonce and gas price)**: for transactions that have identical nonce and gas price, the tie is broken by sorting them based on their transaction hash in ascending order. This provides a consistent and deterministic ordering when other factors are equal. While this ordering isn't a critical aspect of the mempool's operation, it ensures logical consistency. diff --git a/txcache/txListForSender.go b/txcache/txListForSender.go index 66c39aff..c3ddc2ec 100644 --- a/txcache/txListForSender.go +++ b/txcache/txListForSender.go @@ -200,8 +200,9 @@ func (listForSender *txListForSender) getSequentialTxs() []*WrappedTransaction { isFirstTx := len(result) == 0 if !isFirstTx { - // Handle duplicates (only transactions with the highest gas price are included; see "findInsertionPlace"). if nonce == previousNonce { + // Handle duplicates. + // Only transactions with the highest gas price are included (with an exception around guarded transactions), see "findInsertionPlace". if value.IsGuarded { log.Trace("txListForSender.getSequentialTxs, duplicate, but guarded, will not skip", "sender", listForSender.sender, "nonce", nonce) } else { From 26f0189dfab720356350f6b134ca6d6031138886 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Tue, 19 Nov 2024 15:09:50 +0200 Subject: [PATCH 20/34] Fix tests. --- txcache/selection_test.go | 30 +++++++++++++++--------------- txcache/testutils_test.go | 4 +++- txcache/txCache_test.go | 2 +- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/txcache/selection_test.go b/txcache/selection_test.go index 407d19ce..ecfb0a13 100644 --- a/txcache/selection_test.go +++ b/txcache/selection_test.go @@ -27,7 +27,7 @@ func TestTxCache_SelectTransactions_Dummy(t *testing.T) { cache.AddTx(createTx([]byte("hash-bob-5"), "bob", 5)) cache.AddTx(createTx([]byte("hash-carol-1"), "carol", 1)) - selected, accumulatedGas := cache.SelectTransactions(accountStateProvider, math.MaxUint64, math.MaxInt, oneSecond) + selected, accumulatedGas := cache.SelectTransactions(accountStateProvider, math.MaxUint64, math.MaxInt, selectionLoopMaximumDuration) require.Len(t, selected, 8) require.Equal(t, 400000, int(accumulatedGas)) @@ -50,7 +50,7 @@ func TestTxCache_SelectTransactions_Dummy(t *testing.T) { cache.AddTx(createTx([]byte("hash-bob-5"), "bob", 5).withGasPrice(50)) cache.AddTx(createTx([]byte("hash-carol-3"), "carol", 3).withGasPrice(75)) - selected, accumulatedGas := cache.SelectTransactions(accountStateProvider, math.MaxUint64, math.MaxInt, oneSecond) + selected, accumulatedGas := cache.SelectTransactions(accountStateProvider, math.MaxUint64, math.MaxInt, selectionLoopMaximumDuration) require.Len(t, selected, 3) require.Equal(t, 150000, int(accumulatedGas)) @@ -75,7 +75,7 @@ func TestTxCache_SelectTransactionsWithBandwidth_Dummy(t *testing.T) { cache.AddTx(createTx([]byte("hash-bob-5"), "bob", 5).withGasLimit(50000)) cache.AddTx(createTx([]byte("hash-carol-1"), "carol", 1).withGasLimit(50000)) - selected, accumulatedGas := cache.SelectTransactions(accountStateProvider, 760000, math.MaxInt, oneSecond) + selected, accumulatedGas := cache.SelectTransactions(accountStateProvider, 760000, math.MaxInt, selectionLoopMaximumDuration) require.Len(t, selected, 5) require.Equal(t, 750000, int(accumulatedGas)) @@ -105,7 +105,7 @@ func TestTxCache_SelectTransactions_HandlesGapsAndLowerNonces(t *testing.T) { cache.AddTx(createTx([]byte("hash-carol-10"), "carol", 10)) // gap cache.AddTx(createTx([]byte("hash-carol-11"), "carol", 11)) - sorted, accumulatedGas := cache.SelectTransactions(accountStateProvider, math.MaxUint64, math.MaxInt, oneSecond) + sorted, accumulatedGas := cache.SelectTransactions(accountStateProvider, math.MaxUint64, math.MaxInt, selectionLoopMaximumDuration) expectedNumSelected := 3 + 1 + 2 // 3 alice + 1 bob + 2 carol require.Len(t, sorted, expectedNumSelected) require.Equal(t, 300000, int(accumulatedGas)) @@ -139,7 +139,7 @@ func TestTxCache_SelectTransactions_HandlesGapsAndLowerNonces(t *testing.T) { cache.AddTx(createTx([]byte("hash-carol-7"), "carol", 7)) cache.AddTx(createTx([]byte("hash-carol-8"), "carol", 8)) - sorted, accumulatedGas := cache.SelectTransactions(accountStateProvider, math.MaxUint64, math.MaxInt, oneSecond) + sorted, accumulatedGas := cache.SelectTransactions(accountStateProvider, math.MaxUint64, math.MaxInt, selectionLoopMaximumDuration) expectedNumSelected := 3 + 0 + 2 // 3 alice + 0 bob + 2 carol require.Len(t, sorted, expectedNumSelected) require.Equal(t, 250000, int(accumulatedGas)) @@ -173,7 +173,7 @@ func TestTxCache_SelectTransactions_HandlesGapsAndLowerNonces(t *testing.T) { cache.AddTx(createTx([]byte("hash-carol-7"), "carol", 7)) cache.AddTx(createTx([]byte("hash-carol-8"), "carol", 8)) - sorted, accumulatedGas := cache.SelectTransactions(accountStateProvider, math.MaxUint64, math.MaxInt, oneSecond) + sorted, accumulatedGas := cache.SelectTransactions(accountStateProvider, math.MaxUint64, math.MaxInt, selectionLoopMaximumDuration) expectedNumSelected := 3 + 1 + 2 // 3 alice + 1 bob + 2 carol require.Len(t, sorted, expectedNumSelected) require.Equal(t, 300000, int(accumulatedGas)) @@ -240,7 +240,7 @@ func TestTxCache_SelectTransactions_WhenTransactionsAddedInReversedNonceOrder(t require.Equal(t, uint64(nTotalTransactions), cache.CountTx()) - sorted, accumulatedGas := cache.SelectTransactions(accountStateProvider, math.MaxUint64, math.MaxInt, oneSecond) + sorted, accumulatedGas := cache.SelectTransactions(accountStateProvider, math.MaxUint64, math.MaxInt, selectionLoopMaximumDuration) require.Len(t, sorted, nTotalTransactions) require.Equal(t, 5_000_000_000, int(accumulatedGas)) @@ -260,7 +260,7 @@ func TestTxCache_SelectTransactions_WhenTransactionsAddedInReversedNonceOrder(t func TestTxCache_selectTransactionsFromBunches(t *testing.T) { t.Run("empty cache", func(t *testing.T) { accountStateProvider := txcachemocks.NewAccountStateProviderMock() - selected, accumulatedGas := selectTransactionsFromBunches(accountStateProvider, []bunchOfTransactions{}, 10_000_000_000, math.MaxInt, oneSecond) + selected, accumulatedGas := selectTransactionsFromBunches(accountStateProvider, []bunchOfTransactions{}, 10_000_000_000, math.MaxInt, selectionLoopMaximumDuration) require.Equal(t, 0, len(selected)) require.Equal(t, uint64(0), accumulatedGas) @@ -275,7 +275,7 @@ func TestBenchmarkTxCache_selectTransactionsFromBunches(t *testing.T) { bunches := createBunchesOfTransactionsWithUniformDistribution(1000, 1000) sw.Start(t.Name()) - selected, accumulatedGas := selectTransactionsFromBunches(accountStateProvider, bunches, 10_000_000_000, math.MaxInt, oneSecond) + selected, accumulatedGas := selectTransactionsFromBunches(accountStateProvider, bunches, 10_000_000_000, math.MaxInt, selectionLoopMaximumDuration) sw.Stop(t.Name()) require.Equal(t, 200000, len(selected)) @@ -287,7 +287,7 @@ func TestBenchmarkTxCache_selectTransactionsFromBunches(t *testing.T) { bunches := createBunchesOfTransactionsWithUniformDistribution(1000, 1000) sw.Start(t.Name()) - selected, accumulatedGas := selectTransactionsFromBunches(accountStateProvider, bunches, 10_000_000_000, math.MaxInt, oneSecond) + selected, accumulatedGas := selectTransactionsFromBunches(accountStateProvider, bunches, 10_000_000_000, math.MaxInt, selectionLoopMaximumDuration) sw.Stop(t.Name()) require.Equal(t, 200000, len(selected)) @@ -299,7 +299,7 @@ func TestBenchmarkTxCache_selectTransactionsFromBunches(t *testing.T) { bunches := createBunchesOfTransactionsWithUniformDistribution(100000, 3) sw.Start(t.Name()) - selected, accumulatedGas := selectTransactionsFromBunches(accountStateProvider, bunches, 10_000_000_000, math.MaxInt, oneSecond) + selected, accumulatedGas := selectTransactionsFromBunches(accountStateProvider, bunches, 10_000_000_000, math.MaxInt, selectionLoopMaximumDuration) sw.Stop(t.Name()) require.Equal(t, 200000, len(selected)) @@ -311,7 +311,7 @@ func TestBenchmarkTxCache_selectTransactionsFromBunches(t *testing.T) { bunches := createBunchesOfTransactionsWithUniformDistribution(300000, 1) sw.Start(t.Name()) - selected, accumulatedGas := selectTransactionsFromBunches(accountStateProvider, bunches, 10_000_000_000, math.MaxInt, oneSecond) + selected, accumulatedGas := selectTransactionsFromBunches(accountStateProvider, bunches, 10_000_000_000, math.MaxInt, selectionLoopMaximumDuration) sw.Stop(t.Name()) require.Equal(t, 200000, len(selected)) @@ -373,7 +373,7 @@ func TestBenchmarkTxCache_doSelectTransactions(t *testing.T) { require.Equal(t, 100000, int(cache.CountTx())) sw.Start(t.Name()) - selected, accumulatedGas := cache.SelectTransactions(accountStateProvider, 10_000_000_000, 50_000, oneSecond) + selected, accumulatedGas := cache.SelectTransactions(accountStateProvider, 10_000_000_000, 50_000, selectionLoopMaximumDuration) sw.Stop(t.Name()) require.Equal(t, 50000, len(selected)) @@ -389,7 +389,7 @@ func TestBenchmarkTxCache_doSelectTransactions(t *testing.T) { require.Equal(t, 100000, int(cache.CountTx())) sw.Start(t.Name()) - selected, accumulatedGas := cache.SelectTransactions(accountStateProvider, 10_000_000_000, 50_000, oneSecond) + selected, accumulatedGas := cache.SelectTransactions(accountStateProvider, 10_000_000_000, 50_000, selectionLoopMaximumDuration) sw.Stop(t.Name()) require.Equal(t, 50000, len(selected)) @@ -405,7 +405,7 @@ func TestBenchmarkTxCache_doSelectTransactions(t *testing.T) { require.Equal(t, 300000, int(cache.CountTx())) sw.Start(t.Name()) - selected, accumulatedGas := cache.SelectTransactions(accountStateProvider, 10_000_000_000, 50_000, oneSecond) + selected, accumulatedGas := cache.SelectTransactions(accountStateProvider, 10_000_000_000, 50_000, selectionLoopMaximumDuration) sw.Stop(t.Name()) require.Equal(t, 50000, len(selected)) diff --git a/txcache/testutils_test.go b/txcache/testutils_test.go index 41b37683..fb6b5db6 100644 --- a/txcache/testutils_test.go +++ b/txcache/testutils_test.go @@ -13,7 +13,9 @@ import ( const oneMilion = 1000000 const oneBillion = oneMilion * 1000 const estimatedSizeOfBoundedTxFields = uint64(128) -const oneSecond = time.Second + +// The GitHub Actions runners aren't fast. +const selectionLoopMaximumDuration = 3 * time.Second func (cache *TxCache) areInternalMapsConsistent() bool { internalMapByHash := cache.txByHash diff --git a/txcache/txCache_test.go b/txcache/txCache_test.go index 263d697a..ecea6bde 100644 --- a/txcache/txCache_test.go +++ b/txcache/txCache_test.go @@ -463,7 +463,7 @@ func TestTxCache_ConcurrentMutationAndSelection(t *testing.T) { go func() { for i := 0; i < 100; i++ { fmt.Println("Selection", i) - _, _ = cache.SelectTransactions(accountStateProvider, math.MaxUint64, math.MaxInt, oneSecond) + _, _ = cache.SelectTransactions(accountStateProvider, math.MaxUint64, math.MaxInt, selectionLoopMaximumDuration) } wg.Done() From 03c5adb907fcc58e4388aa32d6aa136005d3e054 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Wed, 20 Nov 2024 11:48:26 +0200 Subject: [PATCH 21/34] Better handling of not-executable transactions. --- txcache/eviction.go | 15 +-- txcache/selection.go | 98 +++++++---------- txcache/selection_test.go | 4 +- txcache/transactionsHeap.go | 60 +--------- txcache/transactionsHeapItem.go | 188 ++++++++++++++++++++++++++++++++ txcache/txListForSender.go | 39 ------- txcache/txListForSender_test.go | 41 +------ txcache/wrappedTransaction.go | 5 +- types/accountState.go | 5 +- 9 files changed, 248 insertions(+), 207 deletions(-) create mode 100644 txcache/transactionsHeapItem.go diff --git a/txcache/eviction.go b/txcache/eviction.go index b0d7ed40..0c2cbcf1 100644 --- a/txcache/eviction.go +++ b/txcache/eviction.go @@ -102,14 +102,14 @@ func (cache *TxCache) evictLeastLikelyToSelectTransactions() *evictionJournal { heap.Init(transactionsHeap) // Initialize the heap with the first transaction of each bunch - for i, bunch := range bunches { + for _, bunch := range bunches { if len(bunch) == 0 { // Some senders may have no transaction anymore (hazardous concurrent removals). continue } // Items will be reused (see below). Each sender gets one (and only one) item in the heap. - heap.Push(transactionsHeap, newTransactionsHeapItem(i, bunch[0])) + heap.Push(transactionsHeap, newTransactionsHeapItem(bunch)) } for pass := 0; cache.isCapacityExceeded(); pass++ { @@ -126,16 +126,13 @@ func (cache *TxCache) evictLeastLikelyToSelectTransactions() *evictionJournal { break } - transactionsToEvict = append(transactionsToEvict, item.transaction) - transactionsToEvictHashes = append(transactionsToEvictHashes, item.transaction.TxHash) + 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"). - item.transactionIndex++ - - if item.transactionIndex < len(bunches[item.senderIndex]) { - // Item is reused (same originating sender), pushed back on the heap. - item.transaction = bunches[item.senderIndex][item.transactionIndex] + // Item is reused (same originating sender), pushed back on the heap. + if item.gotoNextTransaction() { heap.Push(transactionsHeap, item) } } diff --git a/txcache/selection.go b/txcache/selection.go index 5cce6ac7..246124eb 100644 --- a/txcache/selection.go +++ b/txcache/selection.go @@ -3,8 +3,6 @@ package txcache import ( "container/heap" "time" - - logger "github.com/multiversx/mx-chain-logger-go" ) func (cache *TxCache) doSelectTransactions(accountStateProvider AccountStateProvider, gasRequested uint64, maxNum int, selectionLoopMaximumDuration time.Duration) (bunchOfTransactions, uint64) { @@ -12,7 +10,7 @@ func (cache *TxCache) doSelectTransactions(accountStateProvider AccountStateProv bunches := make([]bunchOfTransactions, 0, len(senders)) for _, sender := range senders { - bunches = append(bunches, sender.getSequentialTxs()) + bunches = append(bunches, sender.getTxs()) } return selectTransactionsFromBunches(accountStateProvider, bunches, gasRequested, maxNum, selectionLoopMaximumDuration) @@ -27,14 +25,14 @@ func selectTransactionsFromBunches(accountStateProvider AccountStateProvider, bu heap.Init(transactionsHeap) // Initialize the heap with the first transaction of each bunch - for i, bunch := range bunches { + for _, bunch := range bunches { if len(bunch) == 0 { - // Some senders may have no eligible transactions (initial gaps). + // Some senders may have no transactions (hazardous). continue } // Items will be reused (see below). Each sender gets one (and only one) item in the heap. - heap.Push(transactionsHeap, newTransactionsHeapItem(i, bunch[0])) + heap.Push(transactionsHeap, newTransactionsHeapItem(bunch)) } accumulatedGas := uint64(0) @@ -44,8 +42,7 @@ func selectTransactionsFromBunches(accountStateProvider AccountStateProvider, bu for transactionsHeap.Len() > 0 { // Always pick the best transaction. item := heap.Pop(transactionsHeap).(*transactionsHeapItem) - gasLimit := item.transaction.Tx.GetGasLimit() - nonce := item.transaction.Tx.GetNonce() + gasLimit := item.currentTransaction.Tx.GetGasLimit() if accumulatedGas+gasLimit > gasRequested { break @@ -62,64 +59,25 @@ func selectTransactionsFromBunches(accountStateProvider AccountStateProvider, bu requestAccountStateIfNecessary(accountStateProvider, item) - isInitialGap := item.hasInitialGap() - if isInitialGap { - if logSelect.GetLevel() <= logger.LogTrace { - logSelect.Trace("TxCache.selectTransactionsFromBunches, initial gap", - "tx", item.transaction.TxHash, - "nonce", nonce, - "sender", item.transaction.Tx.GetSndAddr(), - "senderState.Nonce", item.senderState.Nonce, - ) - } - + shouldSkipSender := detectSkippableSender(item) + if shouldSkipSender { // Item was popped from the heap, but not used downstream. - // Therefore, the sender is completely ignored in the current selection session. + // Therefore, the sender is completely ignored (from now on) in the current selection session. continue } - hasFeeExceededBalance := item.hasFeeExceededBalance() - if hasFeeExceededBalance { - if logSelect.GetLevel() <= logger.LogTrace { - logSelect.Trace("TxCache.selectTransactionsFromBunches, fee exceeded balance", - "tx", item.transaction.TxHash, - "sender", item.transaction.Tx.GetSndAddr(), - "balance", item.senderState.Balance, - "accumulatedFee", item.accumulatedFee, - ) - } - - // Item was popped from the heap, but not used downstream. - // Therefore, the sender is ignored (from now on) in the current selection session. - continue - } - - isLowerNonce := item.isLowerNonce() - if isLowerNonce { - if logSelect.GetLevel() <= logger.LogTrace { - logSelect.Trace("TxCache.selectTransactionsFromBunches, lower nonce", - "tx", item.transaction.TxHash, - "nonce", nonce, - "sender", item.transaction.Tx.GetSndAddr(), - "senderState.Nonce", item.senderState.Nonce, - ) - } - + shouldSkipTransaction := detectSkippableTransaction(item) + if shouldSkipTransaction { // Transaction isn't selected, but the sender is still in the game (will contribute with other transactions). } else { - item.accumulateFee() - accumulatedGas += gasLimit - selectedTransactions = append(selectedTransactions, item.transaction) + selectedTransactions = append(selectedTransactions, item.selectTransaction()) } // 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). - item.transactionIndex++ - - if item.transactionIndex < len(bunches[item.senderIndex]) { - // Item is reused (same originating sender), pushed back on the heap. - item.transaction = bunches[item.senderIndex][item.transactionIndex] + // Heap item is reused (same originating sender), pushed back on the heap. + if item.gotoNextTransaction() { heap.Push(transactionsHeap, item) } } @@ -134,7 +92,7 @@ func requestAccountStateIfNecessary(accountStateProvider AccountStateProvider, i item.senderStateRequested = true - sender := item.transaction.Tx.GetSndAddr() + sender := item.currentTransaction.Tx.GetSndAddr() senderState, err := accountStateProvider.GetAccountState(sender) if err != nil { // Hazardous; should never happen. @@ -145,3 +103,31 @@ func requestAccountStateIfNecessary(accountStateProvider AccountStateProvider, i item.senderStateProvided = true item.senderState = senderState } + +func detectSkippableSender(item *transactionsHeapItem) bool { + if item.detectInitialGap() { + return true + } + if item.detectMiddleGap() { + return true + } + if item.detectFeeExceededBalance() { + return true + } + + return false +} + +func detectSkippableTransaction(item *transactionsHeapItem) bool { + if item.detectLowerNonce() { + return true + } + if item.detectBadlyGuarded() { + return true + } + if item.detectNonceDuplicate() { + return true + } + + return false +} diff --git a/txcache/selection_test.go b/txcache/selection_test.go index ecfb0a13..5feca35a 100644 --- a/txcache/selection_test.go +++ b/txcache/selection_test.go @@ -194,11 +194,11 @@ func TestTxCache_requestAccountStateIfNecessary(t *testing.T) { } a := &transactionsHeapItem{ - transaction: createTx([]byte("hash-alice-1"), "alice", 1), + currentTransaction: createTx([]byte("hash-alice-1"), "alice", 1), } b := &transactionsHeapItem{ - transaction: createTx([]byte("hash-bob-1"), "bob", 1), + currentTransaction: createTx([]byte("hash-bob-1"), "bob", 1), } c := &transactionsHeapItem{} diff --git a/txcache/transactionsHeap.go b/txcache/transactionsHeap.go index 652ffc7c..fef11698 100644 --- a/txcache/transactionsHeap.go +++ b/txcache/transactionsHeap.go @@ -1,73 +1,17 @@ package txcache -import ( - "math/big" - - "github.com/multiversx/mx-chain-storage-go/types" -) - type transactionsHeap struct { items []*transactionsHeapItem less func(i, j int) bool } -type transactionsHeapItem struct { - senderIndex int - - // Whether the sender's state has been requested within a selection session. - senderStateRequested bool - // Whether the sender's state has been requested and provided (with success) within a selection session. - senderStateProvided bool - // The sender's state (if requested and provided). - senderState *types.AccountState - - transactionIndex int - transaction *WrappedTransaction - - accumulatedFee *big.Int -} - -func newTransactionsHeapItem(senderIndex int, firstTransaction *WrappedTransaction) *transactionsHeapItem { - return &transactionsHeapItem{ - senderIndex: senderIndex, - senderStateRequested: false, - senderStateProvided: false, - senderState: nil, - transactionIndex: 0, - transaction: firstTransaction, - accumulatedFee: big.NewInt(0), - } -} - -func (item *transactionsHeapItem) hasInitialGap() bool { - return item.transactionIndex == 0 && item.senderStateProvided && item.transaction.Tx.GetNonce() > item.senderState.Nonce -} - -func (item *transactionsHeapItem) isLowerNonce() bool { - return item.senderStateProvided && item.transaction.Tx.GetNonce() < item.senderState.Nonce -} - -func (item *transactionsHeapItem) hasFeeExceededBalance() bool { - return item.senderStateProvided && item.accumulatedFee.Cmp(item.senderState.Balance) > 0 -} - -func (item *transactionsHeapItem) accumulateFee() { - fee := item.transaction.Fee.Load() - if fee == nil { - // This should never happen. - return - } - - item.accumulatedFee.Add(item.accumulatedFee, fee) -} - func newMinTransactionsHeap(capacity int) *transactionsHeap { h := transactionsHeap{ items: make([]*transactionsHeapItem, 0, capacity), } h.less = func(i, j int) bool { - return h.items[j].transaction.isTransactionMoreValuableForNetwork(h.items[i].transaction) + return h.items[j].currentTransaction.isTransactionMoreValuableForNetwork(h.items[i].currentTransaction) } return &h @@ -79,7 +23,7 @@ func newMaxTransactionsHeap(capacity int) *transactionsHeap { } h.less = func(i, j int) bool { - return h.items[i].transaction.isTransactionMoreValuableForNetwork(h.items[j].transaction) + return h.items[i].currentTransaction.isTransactionMoreValuableForNetwork(h.items[j].currentTransaction) } return &h diff --git a/txcache/transactionsHeapItem.go b/txcache/transactionsHeapItem.go new file mode 100644 index 00000000..b9663536 --- /dev/null +++ b/txcache/transactionsHeapItem.go @@ -0,0 +1,188 @@ +package txcache + +import ( + "bytes" + "math/big" + + logger "github.com/multiversx/mx-chain-logger-go" + "github.com/multiversx/mx-chain-storage-go/types" +) + +type transactionsHeapItem struct { + // Whether the sender's state has been requested within a selection session. + senderStateRequested bool + // Whether the sender's state has been requested and provided (with success) within a selection session. + senderStateProvided bool + // The sender's state (if requested and provided). + senderState *types.AccountState + + bunch bunchOfTransactions + currentTransactionIndex int + currentTransaction *WrappedTransaction + latestSelectedTransaction *WrappedTransaction + + accumulatedFee *big.Int +} + +func newTransactionsHeapItem(bunch bunchOfTransactions) *transactionsHeapItem { + return &transactionsHeapItem{ + senderStateRequested: false, + senderStateProvided: false, + senderState: nil, + + bunch: bunch, + currentTransactionIndex: 0, + currentTransaction: bunch[0], + accumulatedFee: big.NewInt(0), + } +} + +func (item *transactionsHeapItem) selectTransaction() *WrappedTransaction { + item.accumulateFee() + item.latestSelectedTransaction = item.currentTransaction + return item.currentTransaction +} + +func (item *transactionsHeapItem) accumulateFee() { + fee := item.currentTransaction.Fee.Load() + if fee == nil { + // This should never happen during selection. + return + } + + item.accumulatedFee.Add(item.accumulatedFee, fee) +} + +func (item *transactionsHeapItem) gotoNextTransaction() bool { + item.currentTransactionIndex++ + + if item.currentTransactionIndex >= len(item.bunch) { + return false + } + + item.currentTransaction = item.bunch[item.currentTransactionIndex] + return true +} + +func (item *transactionsHeapItem) detectInitialGap() bool { + if item.latestSelectedTransaction != nil { + return false + } + if !item.senderStateProvided { + // This should never happen during selection. + return false + } + + hasInitialGap := item.currentTransaction.Tx.GetNonce() > item.senderState.Nonce + if hasInitialGap && logSelect.GetLevel() <= logger.LogTrace { + logSelect.Trace("transactionsHeapItem.detectGap, initial gap", + "tx", item.currentTransaction.TxHash, + "nonce", item.currentTransaction.Tx.GetNonce(), + "sender", item.currentTransaction.Tx.GetSndAddr(), + "senderState.Nonce", item.senderState.Nonce, + ) + } + + return hasInitialGap +} + +func (item *transactionsHeapItem) detectMiddleGap() bool { + if item.latestSelectedTransaction == nil { + return false + } + + // Detect middle gap. + previouslySelectedTransactionNonce := item.latestSelectedTransaction.Tx.GetNonce() + + hasMiddleGap := item.currentTransaction.Tx.GetNonce() > previouslySelectedTransactionNonce+1 + if hasMiddleGap && logSelect.GetLevel() <= logger.LogTrace { + logSelect.Trace("transactionsHeapItem.detectGap, middle gap", + "tx", item.currentTransaction.TxHash, + "nonce", item.currentTransaction.Tx.GetNonce(), + "sender", item.currentTransaction.Tx.GetSndAddr(), + "previousSelectedNonce", previouslySelectedTransactionNonce, + ) + } + + return hasMiddleGap +} + +func (item *transactionsHeapItem) detectFeeExceededBalance() bool { + if !item.senderStateProvided { + // This should never happen during selection. + return false + } + + hasFeeExceededBalance := item.accumulatedFee.Cmp(item.senderState.Balance) > 0 + if hasFeeExceededBalance && logSelect.GetLevel() <= logger.LogTrace { + logSelect.Trace("transactionsHeapItem.detectFeeExceededBalance", + "tx", item.currentTransaction.TxHash, + "sender", item.currentTransaction.Tx.GetSndAddr(), + "balance", item.senderState.Balance, + "accumulatedFee", item.accumulatedFee, + ) + } + + return hasFeeExceededBalance +} + +func (item *transactionsHeapItem) detectLowerNonce() bool { + if !item.senderStateProvided { + // This should never happen during selection. + return false + } + + isLowerNonce := item.currentTransaction.Tx.GetNonce() < item.senderState.Nonce + if isLowerNonce && logSelect.GetLevel() <= logger.LogTrace { + logSelect.Trace("transactionsHeapItem.detectLowerNonce", + "tx", item.currentTransaction.TxHash, + "nonce", item.currentTransaction.Tx.GetNonce(), + "sender", item.currentTransaction.Tx.GetSndAddr(), + "senderState.Nonce", item.senderState.Nonce, + ) + } + + return isLowerNonce +} + +func (item *transactionsHeapItem) detectBadlyGuarded() bool { + if !item.senderStateProvided { + // This should never happen during selection. + return false + } + + transactionGuardian := *item.currentTransaction.Guardian.Load() + accountGuardian := item.senderState.Guardian + isBadlyGuarded := bytes.Compare(transactionGuardian, accountGuardian) != 0 + if isBadlyGuarded && logSelect.GetLevel() <= logger.LogTrace { + logSelect.Trace("transactionsHeapItem.detectBadlyGuarded", + "tx", item.currentTransaction.TxHash, + "sender", item.currentTransaction.Tx.GetSndAddr(), + "transactionGuardian", transactionGuardian, + "accountGuardian", accountGuardian, + ) + } + + return isBadlyGuarded +} + +func (item *transactionsHeapItem) detectNonceDuplicate() bool { + if item.latestSelectedTransaction == nil { + return false + } + if !item.senderStateProvided { + // This should never happen during selection. + return false + } + + isDuplicate := item.currentTransaction.Tx.GetNonce() == item.latestSelectedTransaction.Tx.GetNonce() + if isDuplicate && logSelect.GetLevel() <= logger.LogTrace { + logSelect.Trace("transactionsHeapItem.detectNonceDuplicate", + "tx", item.currentTransaction.TxHash, + "sender", item.currentTransaction.Tx.GetSndAddr(), + "nonce", item.currentTransaction.Tx.GetNonce(), + ) + } + + return isDuplicate +} diff --git a/txcache/txListForSender.go b/txcache/txListForSender.go index c3ddc2ec..7127971a 100644 --- a/txcache/txListForSender.go +++ b/txcache/txListForSender.go @@ -184,45 +184,6 @@ func (listForSender *txListForSender) getTxsReversed() []*WrappedTransaction { return result } -// getSequentialTxs returns the transactions of the sender, in the context of transactions selection. -// Middle gaps and duplicates are handled (affected transactions are excluded). -// Initial gaps and lower nonces are not handled (not enough information); they are detected a bit later, within the selection loop. -func (listForSender *txListForSender) getSequentialTxs() []*WrappedTransaction { - listForSender.mutex.RLock() - defer listForSender.mutex.RUnlock() - - result := make([]*WrappedTransaction, 0, listForSender.countTx()) - previousNonce := uint64(0) - - for element := listForSender.items.Front(); element != nil; element = element.Next() { - value := element.Value.(*WrappedTransaction) - nonce := value.Tx.GetNonce() - - isFirstTx := len(result) == 0 - if !isFirstTx { - if nonce == previousNonce { - // Handle duplicates. - // Only transactions with the highest gas price are included (with an exception around guarded transactions), see "findInsertionPlace". - if value.IsGuarded { - log.Trace("txListForSender.getSequentialTxs, duplicate, but guarded, will not skip", "sender", listForSender.sender, "nonce", nonce) - } else { - log.Trace("txListForSender.getSequentialTxs, duplicate, will skip", "sender", listForSender.sender, "nonce", nonce) - continue - } - } else if nonce > previousNonce+1 { - // Handle middle gaps. - log.Trace("txListForSender.getSequentialTxs, middle gap", "sender", listForSender.sender, "nonce", nonce, "previousNonce", previousNonce) - break - } - } - - result = append(result, value) - previousNonce = nonce - } - - return result -} - // This function should only be used in critical section (listForSender.mutex) func (listForSender *txListForSender) countTx() uint64 { return uint64(listForSender.items.Len()) diff --git a/txcache/txListForSender_test.go b/txcache/txListForSender_test.go index b59e8a65..da4bbfad 100644 --- a/txcache/txListForSender_test.go +++ b/txcache/txListForSender_test.go @@ -128,71 +128,35 @@ func TestListForSender_removeTransactionsWithLowerOrEqualNonceReturnHashes(t *te } func TestListForSender_getTxs(t *testing.T) { - t.Run("no transactions", func(t *testing.T) { + t.Run("without transactions", func(t *testing.T) { list := newUnconstrainedListToTest() require.Len(t, list.getTxs(), 0) require.Len(t, list.getTxsReversed(), 0) - require.Len(t, list.getSequentialTxs(), 0) }) - t.Run("with middle gaps", func(t *testing.T) { + t.Run("with transactions", func(t *testing.T) { list := newUnconstrainedListToTest() - // One transaction (no information about gaps) list.AddTx(createTx([]byte("tx-42"), ".", 42)) require.Len(t, list.getTxs(), 1) require.Len(t, list.getTxsReversed(), 1) - require.Len(t, list.getSequentialTxs(), 1) - // Middle gap list.AddTx(createTx([]byte("tx-44"), ".", 44)) require.Len(t, list.getTxs(), 2) require.Len(t, list.getTxsReversed(), 2) - require.Len(t, list.getSequentialTxs(), 1) - // Resolve gap list.AddTx(createTx([]byte("tx-43"), ".", 43)) require.Len(t, list.getTxs(), 3) require.Len(t, list.getTxsReversed(), 3) - require.Len(t, list.getSequentialTxs(), 3) require.Equal(t, []byte("tx-42"), list.getTxs()[0].TxHash) require.Equal(t, []byte("tx-43"), list.getTxs()[1].TxHash) require.Equal(t, []byte("tx-44"), list.getTxs()[2].TxHash) - require.Equal(t, list.getTxs(), list.getSequentialTxs()) - require.Equal(t, []byte("tx-44"), list.getTxsReversed()[0].TxHash) require.Equal(t, []byte("tx-43"), list.getTxsReversed()[1].TxHash) require.Equal(t, []byte("tx-42"), list.getTxsReversed()[2].TxHash) }) - - t.Run("with nonce duplicates", func(t *testing.T) { - list := newUnconstrainedListToTest() - - list.AddTx(createTx([]byte("tx-42"), ".", 42)) - list.AddTx(createTx([]byte("tx-43"), ".", 43)) - - list.AddTx(createTx([]byte("tx-42++"), ".", 42).withGasPrice(1.1 * oneBillion)) - list.AddTx(createTx([]byte("tx-43++"), ".", 43).withGasPrice(1.1 * oneBillion)) - - require.Len(t, list.getTxs(), 4) - require.Len(t, list.getTxsReversed(), 4) - require.Len(t, list.getSequentialTxs(), 2) - - require.Equal(t, []byte("tx-42++"), list.getSequentialTxs()[0].TxHash) - require.Equal(t, []byte("tx-43++"), list.getSequentialTxs()[1].TxHash) - - require.Equal(t, []byte("tx-42++"), list.getTxs()[0].TxHash) - require.Equal(t, []byte("tx-42"), list.getTxs()[1].TxHash) - require.Equal(t, []byte("tx-43++"), list.getTxs()[2].TxHash) - require.Equal(t, []byte("tx-43"), list.getTxs()[3].TxHash) - - require.Equal(t, []byte("tx-43"), list.getTxsReversed()[0].TxHash) - require.Equal(t, []byte("tx-43++"), list.getTxsReversed()[1].TxHash) - require.Equal(t, []byte("tx-42"), list.getTxsReversed()[2].TxHash) - require.Equal(t, []byte("tx-42++"), list.getTxsReversed()[3].TxHash) - }) } func TestListForSender_DetectRaceConditions(t *testing.T) { @@ -205,7 +169,6 @@ func TestListForSender_DetectRaceConditions(t *testing.T) { _ = list.IsEmpty() _ = list.getTxs() _ = list.getTxsReversed() - _ = list.getSequentialTxs() _ = list.countTxWithLock() _, _ = list.AddTx(createTx([]byte("test"), ".", 42)) diff --git a/txcache/wrappedTransaction.go b/txcache/wrappedTransaction.go index a450a1b3..883f54df 100644 --- a/txcache/wrappedTransaction.go +++ b/txcache/wrappedTransaction.go @@ -21,7 +21,7 @@ type WrappedTransaction struct { Fee atomic.Pointer[big.Int] PricePerUnit atomic.Uint64 - IsGuarded bool + Guardian atomic.Pointer[[]byte] } // precomputeFields computes (and caches) the (average) price per gas unit. @@ -41,7 +41,8 @@ func (wrappedTx *WrappedTransaction) precomputeFields(txGasHandler TxGasHandler) return } - wrappedTx.IsGuarded = len(txAsGuardedTransaction.GetGuardianAddr()) > 0 + guardian := txAsGuardedTransaction.GetGuardianAddr() + wrappedTx.Guardian.Store(&guardian) } // Equality is out of scope (not possible in our case). diff --git a/types/accountState.go b/types/accountState.go index 13c3f326..d424244b 100644 --- a/types/accountState.go +++ b/types/accountState.go @@ -4,6 +4,7 @@ import "math/big" // AccountState represents the state of an account, as seen by the mempool type AccountState struct { - Nonce uint64 - Balance *big.Int + Nonce uint64 + Balance *big.Int + Guardian []byte } From 679a46567f75663f01491e6859f0c86080c89d1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Wed, 20 Nov 2024 12:13:28 +0200 Subject: [PATCH 22/34] A few optimizations. --- txcache/selection.go | 6 +-- txcache/selection_test.go | 4 +- txcache/transactionsHeapItem.go | 82 +++++++++++++++++++-------------- 3 files changed, 51 insertions(+), 41 deletions(-) diff --git a/txcache/selection.go b/txcache/selection.go index 246124eb..95f07888 100644 --- a/txcache/selection.go +++ b/txcache/selection.go @@ -91,12 +91,10 @@ func requestAccountStateIfNecessary(accountStateProvider AccountStateProvider, i } item.senderStateRequested = true - - sender := item.currentTransaction.Tx.GetSndAddr() - senderState, err := accountStateProvider.GetAccountState(sender) + senderState, err := accountStateProvider.GetAccountState(item.sender) if err != nil { // Hazardous; should never happen. - logSelect.Debug("TxCache.requestAccountStateIfNecessary: nonce not available", "sender", sender, "err", err) + logSelect.Debug("TxCache.requestAccountStateIfNecessary: nonce not available", "sender", item.sender, "err", err) return } diff --git a/txcache/selection_test.go b/txcache/selection_test.go index 5feca35a..7fe0b2ca 100644 --- a/txcache/selection_test.go +++ b/txcache/selection_test.go @@ -194,11 +194,11 @@ func TestTxCache_requestAccountStateIfNecessary(t *testing.T) { } a := &transactionsHeapItem{ - currentTransaction: createTx([]byte("hash-alice-1"), "alice", 1), + sender: []byte("alice"), } b := &transactionsHeapItem{ - currentTransaction: createTx([]byte("hash-bob-1"), "bob", 1), + sender: []byte("bob"), } c := &transactionsHeapItem{} diff --git a/txcache/transactionsHeapItem.go b/txcache/transactionsHeapItem.go index b9663536..26195d69 100644 --- a/txcache/transactionsHeapItem.go +++ b/txcache/transactionsHeapItem.go @@ -4,11 +4,13 @@ import ( "bytes" "math/big" - logger "github.com/multiversx/mx-chain-logger-go" "github.com/multiversx/mx-chain-storage-go/types" ) type transactionsHeapItem struct { + sender []byte + bunch bunchOfTransactions + // Whether the sender's state has been requested within a selection session. senderStateRequested bool // Whether the sender's state has been requested and provided (with success) within a selection session. @@ -16,30 +18,42 @@ type transactionsHeapItem struct { // The sender's state (if requested and provided). senderState *types.AccountState - bunch bunchOfTransactions - currentTransactionIndex int - currentTransaction *WrappedTransaction - latestSelectedTransaction *WrappedTransaction + currentTransactionIndex int + currentTransaction *WrappedTransaction + currentTransactionNonce uint64 + latestSelectedTransaction *WrappedTransaction + latestSelectedTransactionNonce uint64 accumulatedFee *big.Int } func newTransactionsHeapItem(bunch bunchOfTransactions) *transactionsHeapItem { + firstTransaction := bunch[0] + sender := firstTransaction.Tx.GetSndAddr() + return &transactionsHeapItem{ + sender: sender, + bunch: bunch, + senderStateRequested: false, senderStateProvided: false, senderState: nil, - bunch: bunch, - currentTransactionIndex: 0, - currentTransaction: bunch[0], - accumulatedFee: big.NewInt(0), + currentTransactionIndex: 0, + currentTransaction: firstTransaction, + currentTransactionNonce: firstTransaction.Tx.GetNonce(), + latestSelectedTransaction: nil, + + accumulatedFee: big.NewInt(0), } } func (item *transactionsHeapItem) selectTransaction() *WrappedTransaction { item.accumulateFee() + item.latestSelectedTransaction = item.currentTransaction + item.latestSelectedTransactionNonce = item.currentTransactionNonce + return item.currentTransaction } @@ -54,13 +68,13 @@ func (item *transactionsHeapItem) accumulateFee() { } func (item *transactionsHeapItem) gotoNextTransaction() bool { - item.currentTransactionIndex++ - - if item.currentTransactionIndex >= len(item.bunch) { + if item.currentTransactionIndex+1 >= len(item.bunch) { return false } + item.currentTransactionIndex++ item.currentTransaction = item.bunch[item.currentTransactionIndex] + item.currentTransactionNonce = item.currentTransaction.Tx.GetNonce() return true } @@ -73,12 +87,12 @@ func (item *transactionsHeapItem) detectInitialGap() bool { return false } - hasInitialGap := item.currentTransaction.Tx.GetNonce() > item.senderState.Nonce - if hasInitialGap && logSelect.GetLevel() <= logger.LogTrace { + hasInitialGap := item.currentTransactionNonce > item.senderState.Nonce + if hasInitialGap { logSelect.Trace("transactionsHeapItem.detectGap, initial gap", "tx", item.currentTransaction.TxHash, - "nonce", item.currentTransaction.Tx.GetNonce(), - "sender", item.currentTransaction.Tx.GetSndAddr(), + "nonce", item.currentTransactionNonce, + "sender", item.sender, "senderState.Nonce", item.senderState.Nonce, ) } @@ -92,15 +106,13 @@ func (item *transactionsHeapItem) detectMiddleGap() bool { } // Detect middle gap. - previouslySelectedTransactionNonce := item.latestSelectedTransaction.Tx.GetNonce() - - hasMiddleGap := item.currentTransaction.Tx.GetNonce() > previouslySelectedTransactionNonce+1 - if hasMiddleGap && logSelect.GetLevel() <= logger.LogTrace { + hasMiddleGap := item.currentTransactionNonce > item.latestSelectedTransactionNonce+1 + if hasMiddleGap { logSelect.Trace("transactionsHeapItem.detectGap, middle gap", "tx", item.currentTransaction.TxHash, - "nonce", item.currentTransaction.Tx.GetNonce(), - "sender", item.currentTransaction.Tx.GetSndAddr(), - "previousSelectedNonce", previouslySelectedTransactionNonce, + "nonce", item.currentTransactionNonce, + "sender", item.sender, + "previousSelectedNonce", item.latestSelectedTransactionNonce, ) } @@ -114,10 +126,10 @@ func (item *transactionsHeapItem) detectFeeExceededBalance() bool { } hasFeeExceededBalance := item.accumulatedFee.Cmp(item.senderState.Balance) > 0 - if hasFeeExceededBalance && logSelect.GetLevel() <= logger.LogTrace { + if hasFeeExceededBalance { logSelect.Trace("transactionsHeapItem.detectFeeExceededBalance", "tx", item.currentTransaction.TxHash, - "sender", item.currentTransaction.Tx.GetSndAddr(), + "sender", item.sender, "balance", item.senderState.Balance, "accumulatedFee", item.accumulatedFee, ) @@ -132,12 +144,12 @@ func (item *transactionsHeapItem) detectLowerNonce() bool { return false } - isLowerNonce := item.currentTransaction.Tx.GetNonce() < item.senderState.Nonce - if isLowerNonce && logSelect.GetLevel() <= logger.LogTrace { + isLowerNonce := item.currentTransactionNonce < item.senderState.Nonce + if isLowerNonce { logSelect.Trace("transactionsHeapItem.detectLowerNonce", "tx", item.currentTransaction.TxHash, - "nonce", item.currentTransaction.Tx.GetNonce(), - "sender", item.currentTransaction.Tx.GetSndAddr(), + "nonce", item.currentTransactionNonce, + "sender", item.sender, "senderState.Nonce", item.senderState.Nonce, ) } @@ -154,10 +166,10 @@ func (item *transactionsHeapItem) detectBadlyGuarded() bool { transactionGuardian := *item.currentTransaction.Guardian.Load() accountGuardian := item.senderState.Guardian isBadlyGuarded := bytes.Compare(transactionGuardian, accountGuardian) != 0 - if isBadlyGuarded && logSelect.GetLevel() <= logger.LogTrace { + if isBadlyGuarded { logSelect.Trace("transactionsHeapItem.detectBadlyGuarded", "tx", item.currentTransaction.TxHash, - "sender", item.currentTransaction.Tx.GetSndAddr(), + "sender", item.sender, "transactionGuardian", transactionGuardian, "accountGuardian", accountGuardian, ) @@ -175,12 +187,12 @@ func (item *transactionsHeapItem) detectNonceDuplicate() bool { return false } - isDuplicate := item.currentTransaction.Tx.GetNonce() == item.latestSelectedTransaction.Tx.GetNonce() - if isDuplicate && logSelect.GetLevel() <= logger.LogTrace { + isDuplicate := item.currentTransactionNonce == item.latestSelectedTransactionNonce + if isDuplicate { logSelect.Trace("transactionsHeapItem.detectNonceDuplicate", "tx", item.currentTransaction.TxHash, - "sender", item.currentTransaction.Tx.GetSndAddr(), - "nonce", item.currentTransaction.Tx.GetNonce(), + "sender", item.sender, + "nonce", item.currentTransactionNonce, ) } From de2620bbd4f4ec937ed5f8170b5e065e71f601eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Wed, 20 Nov 2024 14:22:52 +0200 Subject: [PATCH 23/34] Fix fee exceeded balance detection. Refactoring. --- common/errors.go | 9 -- txcache/errors.go | 8 + txcache/eviction.go | 6 +- txcache/selection.go | 25 +--- txcache/selection_test.go | 39 ----- txcache/testutils_test.go | 4 +- txcache/transactionsHeapItem.go | 42 ++++-- txcache/transactionsHeapItem_test.go | 210 +++++++++++++++++++++++++++ txcache/txCache.go | 5 +- txcache/txCache_test.go | 2 +- txcache/txListForSender.go | 3 +- 11 files changed, 264 insertions(+), 89 deletions(-) create mode 100644 txcache/errors.go create mode 100644 txcache/transactionsHeapItem_test.go diff --git a/common/errors.go b/common/errors.go index 1cedf57c..4bb40d12 100644 --- a/common/errors.go +++ b/common/errors.go @@ -51,9 +51,6 @@ var ErrFailedCacheEviction = errors.New("failed eviction within cache") // ErrImmuneItemsCapacityReached signals that capacity for immune items is reached var ErrImmuneItemsCapacityReached = errors.New("capacity reached for immune items") -// ErrItemAlreadyInCache signals that an item is already in cache -var ErrItemAlreadyInCache = errors.New("item already in cache") - // ErrCacheSizeInvalid signals that size of cache is less than 1 var ErrCacheSizeInvalid = errors.New("cache size is less than 1") @@ -72,12 +69,6 @@ var ErrNegativeSizeInBytes = errors.New("negative size in bytes") // ErrNilTimeCache signals that a nil time cache has been provided var ErrNilTimeCache = errors.New("nil time cache") -// ErrNilTxGasHandler signals that a nil tx gas handler was provided -var ErrNilTxGasHandler = errors.New("nil tx gas handler") - -// ErrNilAccountStateProvider signals that a nil account state provider was provided -var ErrNilAccountStateProvider = errors.New("nil account state provider") - // ErrNilStoredDataFactory signals that a nil stored data factory has been provided var ErrNilStoredDataFactory = errors.New("nil stored data factory") diff --git a/txcache/errors.go b/txcache/errors.go new file mode 100644 index 00000000..a9bf775f --- /dev/null +++ b/txcache/errors.go @@ -0,0 +1,8 @@ +package txcache + +import "errors" + +var errNilTxGasHandler = errors.New("nil tx gas handler") +var errNilAccountStateProvider = errors.New("nil account state provider") +var errItemAlreadyInCache = errors.New("item already in cache") +var errEmptyBunchOfTransactions = errors.New("empty bunch of transactions") diff --git a/txcache/eviction.go b/txcache/eviction.go index 0c2cbcf1..61d09cfb 100644 --- a/txcache/eviction.go +++ b/txcache/eviction.go @@ -103,13 +103,13 @@ func (cache *TxCache) evictLeastLikelyToSelectTransactions() *evictionJournal { // Initialize the heap with the first transaction of each bunch for _, bunch := range bunches { - if len(bunch) == 0 { - // Some senders may have no transaction anymore (hazardous concurrent removals). + item, err := newTransactionsHeapItem(bunch) + if err != nil { continue } // Items will be reused (see below). Each sender gets one (and only one) item in the heap. - heap.Push(transactionsHeap, newTransactionsHeapItem(bunch)) + heap.Push(transactionsHeap, item) } for pass := 0; cache.isCapacityExceeded(); pass++ { diff --git a/txcache/selection.go b/txcache/selection.go index 95f07888..398ed36d 100644 --- a/txcache/selection.go +++ b/txcache/selection.go @@ -26,13 +26,13 @@ func selectTransactionsFromBunches(accountStateProvider AccountStateProvider, bu // Initialize the heap with the first transaction of each bunch for _, bunch := range bunches { - if len(bunch) == 0 { - // Some senders may have no transactions (hazardous). + item, err := newTransactionsHeapItem(bunch) + if err != nil { continue } // Items will be reused (see below). Each sender gets one (and only one) item in the heap. - heap.Push(transactionsHeap, newTransactionsHeapItem(bunch)) + heap.Push(transactionsHeap, item) } accumulatedGas := uint64(0) @@ -57,7 +57,7 @@ func selectTransactionsFromBunches(accountStateProvider AccountStateProvider, bu } } - requestAccountStateIfNecessary(accountStateProvider, item) + item.requestAccountStateIfNecessary(accountStateProvider) shouldSkipSender := detectSkippableSender(item) if shouldSkipSender { @@ -85,23 +85,6 @@ func selectTransactionsFromBunches(accountStateProvider AccountStateProvider, bu return selectedTransactions, accumulatedGas } -func requestAccountStateIfNecessary(accountStateProvider AccountStateProvider, item *transactionsHeapItem) { - if item.senderStateRequested { - return - } - - item.senderStateRequested = true - senderState, err := accountStateProvider.GetAccountState(item.sender) - if err != nil { - // Hazardous; should never happen. - logSelect.Debug("TxCache.requestAccountStateIfNecessary: nonce not available", "sender", item.sender, "err", err) - return - } - - item.senderStateProvided = true - item.senderState = senderState -} - func detectSkippableSender(item *transactionsHeapItem) bool { if item.detectInitialGap() { return true diff --git a/txcache/selection_test.go b/txcache/selection_test.go index 7fe0b2ca..ba7c8fba 100644 --- a/txcache/selection_test.go +++ b/txcache/selection_test.go @@ -180,45 +180,6 @@ func TestTxCache_SelectTransactions_HandlesGapsAndLowerNonces(t *testing.T) { }) } -func TestTxCache_requestAccountStateIfNecessary(t *testing.T) { - accountStateProvider := txcachemocks.NewAccountStateProviderMock() - - noncesByAddress := accountStateProvider.AccountStateByAddress - noncesByAddress["alice"] = &types.AccountState{ - Nonce: 7, - Balance: big.NewInt(1000000000000000000), - } - noncesByAddress["bob"] = &types.AccountState{ - Nonce: 42, - Balance: big.NewInt(1000000000000000000), - } - - a := &transactionsHeapItem{ - sender: []byte("alice"), - } - - b := &transactionsHeapItem{ - sender: []byte("bob"), - } - - c := &transactionsHeapItem{} - - requestAccountStateIfNecessary(accountStateProvider, a) - requestAccountStateIfNecessary(accountStateProvider, b) - - require.True(t, a.senderStateRequested) - require.True(t, a.senderStateProvided) - require.Equal(t, uint64(7), a.senderState.Nonce) - - require.True(t, b.senderStateRequested) - require.True(t, b.senderStateProvided) - require.Equal(t, uint64(42), b.senderState.Nonce) - - require.False(t, c.senderStateRequested) - require.False(t, c.senderStateProvided) - require.Nil(t, c.senderState) -} - func TestTxCache_SelectTransactions_WhenTransactionsAddedInReversedNonceOrder(t *testing.T) { cache := newUnconstrainedCacheToTest() accountStateProvider := txcachemocks.NewAccountStateProviderMock() diff --git a/txcache/testutils_test.go b/txcache/testutils_test.go index fb6b5db6..08ebe7dd 100644 --- a/txcache/testutils_test.go +++ b/txcache/testutils_test.go @@ -14,8 +14,8 @@ const oneMilion = 1000000 const oneBillion = oneMilion * 1000 const estimatedSizeOfBoundedTxFields = uint64(128) -// The GitHub Actions runners aren't fast. -const selectionLoopMaximumDuration = 3 * time.Second +// The GitHub Actions runners are slow. +const selectionLoopMaximumDuration = 15 * time.Second func (cache *TxCache) areInternalMapsConsistent() bool { internalMapByHash := cache.txByHash diff --git a/txcache/transactionsHeapItem.go b/txcache/transactionsHeapItem.go index 26195d69..299e1a80 100644 --- a/txcache/transactionsHeapItem.go +++ b/txcache/transactionsHeapItem.go @@ -27,12 +27,15 @@ type transactionsHeapItem struct { accumulatedFee *big.Int } -func newTransactionsHeapItem(bunch bunchOfTransactions) *transactionsHeapItem { +func newTransactionsHeapItem(bunch bunchOfTransactions) (*transactionsHeapItem, error) { + if len(bunch) == 0 { + return nil, errEmptyBunchOfTransactions + } + firstTransaction := bunch[0] - sender := firstTransaction.Tx.GetSndAddr() return &transactionsHeapItem{ - sender: sender, + sender: firstTransaction.Tx.GetSndAddr(), bunch: bunch, senderStateRequested: false, @@ -45,7 +48,7 @@ func newTransactionsHeapItem(bunch bunchOfTransactions) *transactionsHeapItem { latestSelectedTransaction: nil, accumulatedFee: big.NewInt(0), - } + }, nil } func (item *transactionsHeapItem) selectTransaction() *WrappedTransaction { @@ -119,15 +122,19 @@ func (item *transactionsHeapItem) detectMiddleGap() bool { return hasMiddleGap } -func (item *transactionsHeapItem) detectFeeExceededBalance() bool { +func (item *transactionsHeapItem) detectWillFeeExceedBalance() bool { if !item.senderStateProvided { // This should never happen during selection. return false } - hasFeeExceededBalance := item.accumulatedFee.Cmp(item.senderState.Balance) > 0 - if hasFeeExceededBalance { - logSelect.Trace("transactionsHeapItem.detectFeeExceededBalance", + senderBalance := item.senderState.Balance + currentTransactionFee := item.currentTransaction.Fee.Load() + futureAccumulatedFee := new(big.Int).Add(item.accumulatedFee, currentTransactionFee) + + willFeeExceedBalance := futureAccumulatedFee.Cmp(senderBalance) > 0 + if willFeeExceedBalance { + logSelect.Trace("transactionsHeapItem.detectWillFeeExceedBalance", "tx", item.currentTransaction.TxHash, "sender", item.sender, "balance", item.senderState.Balance, @@ -135,7 +142,7 @@ func (item *transactionsHeapItem) detectFeeExceededBalance() bool { ) } - return hasFeeExceededBalance + return willFeeExceedBalance } func (item *transactionsHeapItem) detectLowerNonce() bool { @@ -198,3 +205,20 @@ func (item *transactionsHeapItem) detectNonceDuplicate() bool { return isDuplicate } + +func (item *transactionsHeapItem) requestAccountStateIfNecessary(accountStateProvider AccountStateProvider) { + if item.senderStateRequested { + return + } + + item.senderStateRequested = true + senderState, err := accountStateProvider.GetAccountState(item.sender) + if err != nil { + // Hazardous; should never happen. + logSelect.Debug("transactionsHeapItem.requestAccountStateIfNecessary: nonce not available", "sender", item.sender, "err", err) + return + } + + item.senderStateProvided = true + item.senderState = senderState +} diff --git a/txcache/transactionsHeapItem_test.go b/txcache/transactionsHeapItem_test.go new file mode 100644 index 00000000..9bd5d2f1 --- /dev/null +++ b/txcache/transactionsHeapItem_test.go @@ -0,0 +1,210 @@ +package txcache + +import ( + "math/big" + "testing" + + "github.com/multiversx/mx-chain-storage-go/testscommon/txcachemocks" + "github.com/multiversx/mx-chain-storage-go/types" + "github.com/stretchr/testify/require" +) + +func TestNewTransactionsHeapItem(t *testing.T) { + t.Run("empty bunch", func(t *testing.T) { + item, err := newTransactionsHeapItem(nil) + require.Nil(t, item) + require.Equal(t, errEmptyBunchOfTransactions, err) + }) + + t.Run("non-empty bunch", func(t *testing.T) { + bunch := bunchOfTransactions{ + createTx([]byte("tx-1"), "alice", 42), + } + + item, err := newTransactionsHeapItem(bunch) + require.NotNil(t, item) + require.Nil(t, err) + + require.Equal(t, []byte("alice"), item.sender) + require.Equal(t, bunch, item.bunch) + require.False(t, item.senderStateRequested) + require.False(t, item.senderStateProvided) + require.Nil(t, item.senderState) + require.Equal(t, 0, item.currentTransactionIndex) + require.Equal(t, bunch[0], item.currentTransaction) + require.Equal(t, uint64(42), item.currentTransactionNonce) + require.Nil(t, item.latestSelectedTransaction) + require.Equal(t, big.NewInt(0), item.accumulatedFee) + }) +} + +func TestTransactionsHeapItem_selectTransaction(t *testing.T) { + txGasHandler := txcachemocks.NewTxGasHandlerMock() + + a := createTx([]byte("tx-1"), "alice", 42) + b := createTx([]byte("tx-2"), "alice", 43) + a.precomputeFields(txGasHandler) + b.precomputeFields(txGasHandler) + + item, err := newTransactionsHeapItem(bunchOfTransactions{a, b}) + require.NoError(t, err) + + selected := item.selectTransaction() + require.Equal(t, a, selected) + require.Equal(t, a, item.latestSelectedTransaction) + require.Equal(t, 42, int(item.latestSelectedTransactionNonce)) + require.Equal(t, "50000000000000", item.accumulatedFee.String()) + + ok := item.gotoNextTransaction() + require.True(t, ok) + + selected = item.selectTransaction() + require.Equal(t, b, selected) + require.Equal(t, b, item.latestSelectedTransaction) + require.Equal(t, 43, int(item.latestSelectedTransactionNonce)) + require.Equal(t, "100000000000000", item.accumulatedFee.String()) + + ok = item.gotoNextTransaction() + require.False(t, ok) +} + +func TestTransactionsHeapItem_detectInitialGap(t *testing.T) { + a := createTx([]byte("tx-1"), "alice", 42) + b := createTx([]byte("tx-2"), "alice", 43) + + t.Run("unknown", func(t *testing.T) { + item, err := newTransactionsHeapItem(bunchOfTransactions{a, b}) + require.NoError(t, err) + + require.False(t, item.detectInitialGap()) + }) + + t.Run("known, without gap", func(t *testing.T) { + item, err := newTransactionsHeapItem(bunchOfTransactions{a, b}) + require.NoError(t, err) + + item.senderStateProvided = true + item.senderState = &types.AccountState{ + Nonce: 42, + } + + require.False(t, item.detectInitialGap()) + }) + + t.Run("known, without gap", func(t *testing.T) { + item, err := newTransactionsHeapItem(bunchOfTransactions{a, b}) + require.NoError(t, err) + + item.senderStateProvided = true + item.senderState = &types.AccountState{ + Nonce: 41, + } + + require.True(t, item.detectInitialGap()) + }) +} + +func TestTransactionsHeapItem_detectMiddleGap(t *testing.T) { + a := createTx([]byte("tx-1"), "alice", 42) + b := createTx([]byte("tx-2"), "alice", 43) + c := createTx([]byte("tx-3"), "alice", 44) + + t.Run("unknown", func(t *testing.T) { + item := &transactionsHeapItem{} + item.latestSelectedTransaction = nil + require.False(t, item.detectInitialGap()) + }) + + t.Run("known, without gap", func(t *testing.T) { + item := &transactionsHeapItem{} + item.latestSelectedTransaction = a + item.latestSelectedTransactionNonce = 42 + item.currentTransaction = b + item.currentTransactionNonce = 43 + + require.False(t, item.detectMiddleGap()) + }) + + t.Run("known, without gap", func(t *testing.T) { + item := &transactionsHeapItem{} + item.latestSelectedTransaction = a + item.latestSelectedTransactionNonce = 42 + item.currentTransaction = c + item.currentTransactionNonce = 44 + + require.True(t, item.detectMiddleGap()) + }) +} + +func TestTransactionsHeapItem_detectFeeExceededBalance(t *testing.T) { + txGasHandler := txcachemocks.NewTxGasHandlerMock() + + a := createTx([]byte("tx-1"), "alice", 42) + b := createTx([]byte("tx-2"), "alice", 43) + a.precomputeFields(txGasHandler) + b.precomputeFields(txGasHandler) + + t.Run("unknown", func(t *testing.T) { + item, err := newTransactionsHeapItem(bunchOfTransactions{a, b}) + require.NoError(t, err) + + require.False(t, item.detectWillFeeExceedBalance()) + }) + + t.Run("known, not exceeded, then exceeded", func(t *testing.T) { + item, err := newTransactionsHeapItem(bunchOfTransactions{a, b}) + require.NoError(t, err) + + item.senderStateProvided = true + item.senderState = &types.AccountState{ + Balance: big.NewInt(50000000000001), + } + + require.False(t, item.detectWillFeeExceedBalance()) + + _ = item.selectTransaction() + _ = item.gotoNextTransaction() + require.Equal(t, "50000000000000", item.accumulatedFee.String()) + + require.True(t, item.detectWillFeeExceedBalance()) + }) +} + +func TestTransactionsHeapItem_requestAccountStateIfNecessary(t *testing.T) { + accountStateProvider := txcachemocks.NewAccountStateProviderMock() + + noncesByAddress := accountStateProvider.AccountStateByAddress + noncesByAddress["alice"] = &types.AccountState{ + Nonce: 7, + Balance: big.NewInt(1000000000000000000), + } + noncesByAddress["bob"] = &types.AccountState{ + Nonce: 42, + Balance: big.NewInt(1000000000000000000), + } + + a := &transactionsHeapItem{ + sender: []byte("alice"), + } + + b := &transactionsHeapItem{ + sender: []byte("bob"), + } + + c := &transactionsHeapItem{} + + a.requestAccountStateIfNecessary(accountStateProvider) + b.requestAccountStateIfNecessary(accountStateProvider) + + require.True(t, a.senderStateRequested) + require.True(t, a.senderStateProvided) + require.Equal(t, uint64(7), a.senderState.Nonce) + + require.True(t, b.senderStateRequested) + require.True(t, b.senderStateProvided) + require.Equal(t, uint64(42), b.senderState.Nonce) + + require.False(t, c.senderStateRequested) + require.False(t, c.senderStateProvided) + require.Nil(t, c.senderState) +} diff --git a/txcache/txCache.go b/txcache/txCache.go index 92796d60..c78e4d03 100644 --- a/txcache/txCache.go +++ b/txcache/txCache.go @@ -7,7 +7,6 @@ import ( "github.com/multiversx/mx-chain-core-go/core" "github.com/multiversx/mx-chain-core-go/core/atomic" "github.com/multiversx/mx-chain-core-go/core/check" - "github.com/multiversx/mx-chain-storage-go/common" "github.com/multiversx/mx-chain-storage-go/monitoring" "github.com/multiversx/mx-chain-storage-go/types" ) @@ -36,7 +35,7 @@ func NewTxCache(config ConfigSourceMe, txGasHandler TxGasHandler) (*TxCache, err return nil, err } if check.IfNil(txGasHandler) { - return nil, common.ErrNilTxGasHandler + return nil, errNilTxGasHandler } // Note: for simplicity, we use the same "numChunks" for both internal concurrent maps @@ -102,7 +101,7 @@ func (cache *TxCache) GetByTxHash(txHash []byte) (*WrappedTransaction, bool) { // It returns up to "maxNum" transactions, with total gas <= "gasRequested". func (cache *TxCache) SelectTransactions(accountStateProvider AccountStateProvider, gasRequested uint64, maxNum int, selectionLoopMaximumDuration time.Duration) ([]*WrappedTransaction, uint64) { if check.IfNil(accountStateProvider) { - log.Error("TxCache.SelectTransactions", "err", common.ErrNilAccountStateProvider) + log.Error("TxCache.SelectTransactions", "err", errNilAccountStateProvider) return nil, 0 } diff --git a/txcache/txCache_test.go b/txcache/txCache_test.go index ecea6bde..c071fa62 100644 --- a/txcache/txCache_test.go +++ b/txcache/txCache_test.go @@ -53,7 +53,7 @@ func Test_NewTxCache(t *testing.T) { badConfig = config cache, err = NewTxCache(config, nil) require.Nil(t, cache) - require.Equal(t, common.ErrNilTxGasHandler, err) + require.Equal(t, errNilTxGasHandler, err) badConfig = config badConfig.NumBytesThreshold = 0 diff --git a/txcache/txListForSender.go b/txcache/txListForSender.go index 7127971a..fc5c048c 100644 --- a/txcache/txListForSender.go +++ b/txcache/txListForSender.go @@ -6,7 +6,6 @@ import ( "sync" "github.com/multiversx/mx-chain-core-go/core/atomic" - "github.com/multiversx/mx-chain-storage-go/common" ) // txListForSender represents a sorted list of transactions of a particular sender @@ -117,7 +116,7 @@ func (listForSender *txListForSender) findInsertionPlace(incomingTx *WrappedTran comparison := bytes.Compare(currentTx.TxHash, incomingTx.TxHash) if comparison == 0 { // The incoming transaction will be discarded, since it's already in the cache. - return nil, common.ErrItemAlreadyInCache + return nil, errItemAlreadyInCache } if comparison < 0 { // We've found an insertion place: right after "element". From fe1bd09329c4f1f20c920d68b96e56deac8678cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Wed, 20 Nov 2024 14:39:06 +0200 Subject: [PATCH 24/34] Additional unit tests. --- txcache/selection.go | 2 +- txcache/testutils_test.go | 6 ++ txcache/transactionsHeapItem.go | 4 - txcache/transactionsHeapItem_test.go | 145 +++++++++++++++++++++++++++ 4 files changed, 152 insertions(+), 5 deletions(-) diff --git a/txcache/selection.go b/txcache/selection.go index 398ed36d..4a987de2 100644 --- a/txcache/selection.go +++ b/txcache/selection.go @@ -92,7 +92,7 @@ func detectSkippableSender(item *transactionsHeapItem) bool { if item.detectMiddleGap() { return true } - if item.detectFeeExceededBalance() { + if item.detectWillFeeExceedBalance() { return true } diff --git a/txcache/testutils_test.go b/txcache/testutils_test.go index 08ebe7dd..556f5894 100644 --- a/txcache/testutils_test.go +++ b/txcache/testutils_test.go @@ -174,6 +174,12 @@ func (wrappedTx *WrappedTransaction) withGasLimit(gasLimit uint64) *WrappedTrans return wrappedTx } +func (wrappedTx *WrappedTransaction) withGuardian(guardian []byte) *WrappedTransaction { + tx := wrappedTx.Tx.(*transaction.Transaction) + tx.GuardianAddr = guardian + return wrappedTx +} + func createFakeSenderAddress(senderTag int) []byte { bytes := make([]byte, 32) binary.LittleEndian.PutUint64(bytes, uint64(senderTag)) diff --git a/txcache/transactionsHeapItem.go b/txcache/transactionsHeapItem.go index 299e1a80..aa13df87 100644 --- a/txcache/transactionsHeapItem.go +++ b/txcache/transactionsHeapItem.go @@ -189,10 +189,6 @@ func (item *transactionsHeapItem) detectNonceDuplicate() bool { if item.latestSelectedTransaction == nil { return false } - if !item.senderStateProvided { - // This should never happen during selection. - return false - } isDuplicate := item.currentTransactionNonce == item.latestSelectedTransactionNonce if isDuplicate { diff --git a/txcache/transactionsHeapItem_test.go b/txcache/transactionsHeapItem_test.go index 9bd5d2f1..365c6ee8 100644 --- a/txcache/transactionsHeapItem_test.go +++ b/txcache/transactionsHeapItem_test.go @@ -170,6 +170,151 @@ func TestTransactionsHeapItem_detectFeeExceededBalance(t *testing.T) { }) } +func TestTransactionsHeapItem_detectLowerNonce(t *testing.T) { + a := createTx([]byte("tx-1"), "alice", 42) + b := createTx([]byte("tx-2"), "alice", 43) + + t.Run("unknown", func(t *testing.T) { + item, err := newTransactionsHeapItem(bunchOfTransactions{a, b}) + require.NoError(t, err) + + require.False(t, item.detectInitialGap()) + }) + + t.Run("known, good", func(t *testing.T) { + item, err := newTransactionsHeapItem(bunchOfTransactions{a, b}) + require.NoError(t, err) + + item.senderStateProvided = true + item.senderState = &types.AccountState{ + Nonce: 42, + } + + require.False(t, item.detectLowerNonce()) + }) + + t.Run("known, lower", func(t *testing.T) { + item, err := newTransactionsHeapItem(bunchOfTransactions{a, b}) + require.NoError(t, err) + + item.senderStateProvided = true + item.senderState = &types.AccountState{ + Nonce: 44, + } + + require.True(t, item.detectLowerNonce()) + }) +} + +func TestTransactionsHeapItem_detectBadlyGuarded(t *testing.T) { + txGasHandler := txcachemocks.NewTxGasHandlerMock() + + a := createTx([]byte("tx-1"), "alice", 42) + b := createTx([]byte("tx-7"), "bob", 43).withGuardian([]byte("heidi")) + + a.precomputeFields(txGasHandler) + b.precomputeFields(txGasHandler) + + t.Run("unknown", func(t *testing.T) { + item, err := newTransactionsHeapItem(bunchOfTransactions{a}) + require.NoError(t, err) + + require.False(t, item.detectBadlyGuarded()) + }) + + t.Run("transaction has no guardian, account has no guardian", func(t *testing.T) { + item, err := newTransactionsHeapItem(bunchOfTransactions{a}) + require.NoError(t, err) + + item.senderStateProvided = true + item.senderState = &types.AccountState{ + Guardian: nil, + } + + require.False(t, item.detectBadlyGuarded()) + }) + + t.Run("transaction has guardian, account has guardian, they match", func(t *testing.T) { + item, err := newTransactionsHeapItem(bunchOfTransactions{b}) + require.NoError(t, err) + + item.senderStateProvided = true + item.senderState = &types.AccountState{ + Guardian: []byte("heidi"), + } + + require.False(t, item.detectBadlyGuarded()) + }) + + t.Run("transaction has guardian, account has guardian, they don't match", func(t *testing.T) { + item, err := newTransactionsHeapItem(bunchOfTransactions{b}) + require.NoError(t, err) + + item.senderStateProvided = true + item.senderState = &types.AccountState{ + Guardian: []byte("grace"), + } + + require.True(t, item.detectBadlyGuarded()) + }) + + t.Run("transaction has guardian, account does not", func(t *testing.T) { + item, err := newTransactionsHeapItem(bunchOfTransactions{b}) + require.NoError(t, err) + + item.senderStateProvided = true + item.senderState = &types.AccountState{ + Guardian: nil, + } + + require.True(t, item.detectBadlyGuarded()) + }) + + t.Run("transaction has no guardian, account has guardian", func(t *testing.T) { + item, err := newTransactionsHeapItem(bunchOfTransactions{a}) + require.NoError(t, err) + + item.senderStateProvided = true + item.senderState = &types.AccountState{ + Guardian: []byte("heidi"), + } + + require.True(t, item.detectBadlyGuarded()) + }) +} + +func TestTransactionsHeapItem_detectNonceDuplicate(t *testing.T) { + a := createTx([]byte("tx-1"), "alice", 42) + b := createTx([]byte("tx-2"), "alice", 43) + c := createTx([]byte("tx-3"), "alice", 42) + + t.Run("unknown", func(t *testing.T) { + item := &transactionsHeapItem{} + item.latestSelectedTransaction = nil + require.False(t, item.detectNonceDuplicate()) + }) + + t.Run("no duplicates", func(t *testing.T) { + item := &transactionsHeapItem{} + item.latestSelectedTransaction = a + item.latestSelectedTransactionNonce = 42 + item.currentTransaction = b + item.currentTransactionNonce = 43 + + require.False(t, item.detectNonceDuplicate()) + }) + + t.Run("duplicates", func(t *testing.T) { + item := &transactionsHeapItem{} + item.latestSelectedTransaction = a + item.latestSelectedTransactionNonce = 42 + item.currentTransaction = c + item.currentTransactionNonce = 42 + + require.True(t, item.detectNonceDuplicate()) + }) +} + func TestTransactionsHeapItem_requestAccountStateIfNecessary(t *testing.T) { accountStateProvider := txcachemocks.NewAccountStateProviderMock() From 67c1c6ea9d6d59fd84a04d090fc5d106c00a44e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Wed, 20 Nov 2024 15:01:15 +0200 Subject: [PATCH 25/34] Adjust benchmark output. --- txcache/eviction_test.go | 8 ++++---- txcache/selection_test.go | 14 +++++++------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/txcache/eviction_test.go b/txcache/eviction_test.go index df7fcf22..2ec09864 100644 --- a/txcache/eviction_test.go +++ b/txcache/eviction_test.go @@ -220,8 +220,8 @@ func TestBenchmarkTxCache_DoEviction(t *testing.T) { // Thread(s) per core: 2 // Core(s) per socket: 4 // - // 0.093771s (TestBenchmarkTxCache_DoEviction_Benchmark/numSenders_=_35000,_numTransactions_=_10) - // 0.424683s (TestBenchmarkTxCache_DoEviction_Benchmark/numSenders_=_100000,_numTransactions_=_5) - // 0.448017s (TestBenchmarkTxCache_DoEviction_Benchmark/numSenders_=_10000,_numTransactions_=_100) - // 0.476738s (TestBenchmarkTxCache_DoEviction_Benchmark/numSenders_=_400000,_numTransactions_=_1) + // 0.160000s (TestBenchmarkTxCache_DoEviction_Benchmark/numSenders_=_35000,_numTransactions_=_10) + // 0.506890s (TestBenchmarkTxCache_DoEviction_Benchmark/numSenders_=_100000,_numTransactions_=_5) + // 0.602928s (TestBenchmarkTxCache_DoEviction_Benchmark/numSenders_=_10000,_numTransactions_=_100) + // 0.654148s (TestBenchmarkTxCache_DoEviction_Benchmark/numSenders_=_400000,_numTransactions_=_1) } diff --git a/txcache/selection_test.go b/txcache/selection_test.go index ba7c8fba..6a2bddc1 100644 --- a/txcache/selection_test.go +++ b/txcache/selection_test.go @@ -291,10 +291,10 @@ func TestBenchmarkTxCache_selectTransactionsFromBunches(t *testing.T) { // Thread(s) per core: 2 // Core(s) per socket: 4 // - // 0.029651s (TestTxCache_selectTransactionsFromBunches/numSenders_=_1000,_numTransactions_=_1000) - // 0.026440s (TestTxCache_selectTransactionsFromBunches/numSenders_=_10000,_numTransactions_=_100) - // 0.122592s (TestTxCache_selectTransactionsFromBunches/numSenders_=_100000,_numTransactions_=_3) - // 0.219072s (TestTxCache_selectTransactionsFromBunches/numSenders_=_300000,_numTransactions_=_1) + // 0.053758s (TestBenchmarkTxCache_selectTransactionsFromBunches/numSenders_=_1000,_numTransactions_=_1000) + // 0.050731s (TestBenchmarkTxCache_selectTransactionsFromBunches/numSenders_=_10000,_numTransactions_=_100) + // 0.302232s (TestBenchmarkTxCache_selectTransactionsFromBunches/numSenders_=_100000,_numTransactions_=_3) + // 0.496604s (TestBenchmarkTxCache_selectTransactionsFromBunches/numSenders_=_300000,_numTransactions_=_1) } func TestTxCache_selectTransactionsFromBunches_lookBreaks_whenTakesTooLong(t *testing.T) { @@ -385,7 +385,7 @@ func TestBenchmarkTxCache_doSelectTransactions(t *testing.T) { // Thread(s) per core: 2 // Core(s) per socket: 4 // - // 0.060508s (TestBenchmarkTxCache_doSelectTransactions/numSenders_=_50000,_numTransactions_=_2,_maxNum_=_50_000) - // 0.103369s (TestBenchmarkTxCache_doSelectTransactions/numSenders_=_100000,_numTransactions_=_1,_maxNum_=_50_000) - // 0.245621s (TestBenchmarkTxCache_doSelectTransactions/numSenders_=_300000,_numTransactions_=_1,_maxNum_=_50_000) + // 0.112178s (TestBenchmarkTxCache_doSelectTransactions/numSenders_=_50000,_numTransactions_=_2,_maxNum_=_50_000) + // 0.160638s (TestBenchmarkTxCache_doSelectTransactions/numSenders_=_100000,_numTransactions_=_1,_maxNum_=_50_000) + // 0.371011s (TestBenchmarkTxCache_doSelectTransactions/numSenders_=_300000,_numTransactions_=_1,_maxNum_=_50_000) } From e20d2e5ddf9465934e77a0462158901b45b1c1c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Wed, 20 Nov 2024 15:02:23 +0200 Subject: [PATCH 26/34] Fix tests. --- txcache/testutils_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/txcache/testutils_test.go b/txcache/testutils_test.go index 556f5894..90186f31 100644 --- a/txcache/testutils_test.go +++ b/txcache/testutils_test.go @@ -14,8 +14,8 @@ const oneMilion = 1000000 const oneBillion = oneMilion * 1000 const estimatedSizeOfBoundedTxFields = uint64(128) -// The GitHub Actions runners are slow. -const selectionLoopMaximumDuration = 15 * time.Second +// The GitHub Actions runners are (extremely) slow. +const selectionLoopMaximumDuration = 30 * time.Second func (cache *TxCache) areInternalMapsConsistent() bool { internalMapByHash := cache.txByHash From b2fa1cee26cef118759553010a659aab1bae6bc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Wed, 20 Nov 2024 15:27:28 +0200 Subject: [PATCH 27/34] Additional unit tests. --- txcache/wrappedTransaction_test.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/txcache/wrappedTransaction_test.go b/txcache/wrappedTransaction_test.go index b24dbc3f..805010b8 100644 --- a/txcache/wrappedTransaction_test.go +++ b/txcache/wrappedTransaction_test.go @@ -14,14 +14,18 @@ func TestWrappedTransaction_precomputeFields(t *testing.T) { tx := createTx([]byte("a"), "a", 1).withDataLength(1).withGasLimit(51500).withGasPrice(oneBillion) tx.precomputeFields(txGasHandler) + require.Equal(t, "51500000000000", tx.Fee.Load().String()) require.Equal(t, oneBillion, int(tx.PricePerUnit.Load())) + require.Empty(t, tx.Guardian.Load()) }) t.Run("move balance gas limit and execution gas limit (1)", func(t *testing.T) { tx := createTx([]byte("b"), "b", 1).withDataLength(1).withGasLimit(51501).withGasPrice(oneBillion) tx.precomputeFields(txGasHandler) + require.Equal(t, "51500010000000", tx.Fee.Load().String()) require.Equal(t, 999_980_777, int(tx.PricePerUnit.Load())) + require.Empty(t, tx.Guardian.Load()) }) t.Run("move balance gas limit and execution gas limit (2)", func(t *testing.T) { @@ -29,8 +33,19 @@ func TestWrappedTransaction_precomputeFields(t *testing.T) { tx.precomputeFields(txGasHandler) actualFee := 51500*oneBillion + (oneMilion-51500)*oneBillion/100 + require.Equal(t, "60985000000000", tx.Fee.Load().String()) require.Equal(t, 60_985_000_000_000, actualFee) require.Equal(t, actualFee/oneMilion, int(tx.PricePerUnit.Load())) + require.Empty(t, tx.Guardian.Load()) + }) + + t.Run("with guardian", func(t *testing.T) { + tx := createTx([]byte("a"), "a", 1).withGuardian([]byte("heidi")) + tx.precomputeFields(txGasHandler) + + require.Equal(t, "50000000000000", tx.Fee.Load().String()) + require.Equal(t, oneBillion, int(tx.PricePerUnit.Load())) + require.Equal(t, []byte("heidi"), *tx.Guardian.Load()) }) } From 48853fb88622590a7a8d47ca7eb8db43915b66d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Wed, 20 Nov 2024 22:29:58 +0200 Subject: [PATCH 28/34] Fix after self review. --- .../txcachemocks/accountStateProviderMock.go | 61 ++++++++++++++++--- txcache/README.md | 2 +- txcache/diagnosis.go | 2 +- txcache/selection.go | 9 ++- txcache/selection_test.go | 48 +++++++-------- txcache/testutils_test.go | 4 +- txcache/transactionsHeapItem.go | 55 +++++++---------- txcache/transactionsHeapItem_test.go | 26 +------- txcache/wrappedTransaction.go | 30 ++++----- txcache/wrappedTransaction_test.go | 24 ++++---- 10 files changed, 135 insertions(+), 126 deletions(-) diff --git a/testscommon/txcachemocks/accountStateProviderMock.go b/testscommon/txcachemocks/accountStateProviderMock.go index c770d0e8..339276f7 100644 --- a/testscommon/txcachemocks/accountStateProviderMock.go +++ b/testscommon/txcachemocks/accountStateProviderMock.go @@ -1,7 +1,7 @@ package txcachemocks import ( - "errors" + "math/big" "github.com/multiversx/mx-chain-storage-go/types" ) @@ -19,21 +19,62 @@ func NewAccountStateProviderMock() *AccountStateProviderMock { } } +// SetNonce - +func (mock *AccountStateProviderMock) SetNonce(address []byte, nonce uint64) { + key := string(address) + + if mock.AccountStateByAddress[key] == nil { + mock.AccountStateByAddress[key] = newDefaultAccountState() + } + + mock.AccountStateByAddress[key].Nonce = nonce +} + +// SetBalance - +func (mock *AccountStateProviderMock) SetBalance(address []byte, balance *big.Int) { + key := string(address) + + if mock.AccountStateByAddress[key] == nil { + mock.AccountStateByAddress[key] = newDefaultAccountState() + } + + mock.AccountStateByAddress[key].Balance = balance +} + +// SetGuardian - +func (mock *AccountStateProviderMock) SetGuardian(address []byte, guardian []byte) { + key := string(address) + + if mock.AccountStateByAddress[key] == nil { + mock.AccountStateByAddress[key] = newDefaultAccountState() + } + + mock.AccountStateByAddress[key].Guardian = guardian +} + // GetAccountState - -func (stub *AccountStateProviderMock) GetAccountState(address []byte) (*types.AccountState, error) { - if stub.GetAccountStateCalled != nil { - return stub.GetAccountStateCalled(address) +func (mock *AccountStateProviderMock) GetAccountState(address []byte) (*types.AccountState, error) { + if mock.GetAccountStateCalled != nil { + return mock.GetAccountStateCalled(address) } - state, ok := stub.AccountStateByAddress[string(address)] - if !ok { - return nil, errors.New("cannot get state") + state, ok := mock.AccountStateByAddress[string(address)] + if ok { + return state, nil } - return state, nil + return newDefaultAccountState(), nil } // IsInterfaceNil - -func (stub *AccountStateProviderMock) IsInterfaceNil() bool { - return stub == nil +func (mock *AccountStateProviderMock) IsInterfaceNil() bool { + return mock == nil +} + +func newDefaultAccountState() *types.AccountState { + return &types.AccountState{ + Nonce: 0, + Balance: big.NewInt(1000000000000000000), + Guardian: nil, + } } diff --git a/txcache/README.md b/txcache/README.md index 3ddee918..913f2e9f 100644 --- a/txcache/README.md +++ b/txcache/README.md @@ -114,7 +114,7 @@ The mempool selects transactions as follows (pseudo-code): func selectTransactions(gasRequested, maxNum): // Setup phase senders := list of all current senders in the mempool, in an arbitrary order - bunchesOfTransactions := sourced from senders; middle-nonces-gap-free, (almost) nonce-duplicates-free, nicely sorted by nonce + bunchesOfTransactions := sourced from senders, nicely sorted by nonce // Holds selected transactions selectedTransactions := empty diff --git a/txcache/diagnosis.go b/txcache/diagnosis.go index ff05d885..6f693c97 100644 --- a/txcache/diagnosis.go +++ b/txcache/diagnosis.go @@ -102,7 +102,7 @@ func convertWrappedTransactionToPrintedTransaction(wrappedTx *WrappedTransaction GasPrice: transaction.GetGasPrice(), GasLimit: transaction.GetGasLimit(), DataLength: len(transaction.GetData()), - PPU: wrappedTx.PricePerUnit.Load(), + PPU: wrappedTx.PricePerUnit, } } diff --git a/txcache/selection.go b/txcache/selection.go index 4a987de2..a9c427f6 100644 --- a/txcache/selection.go +++ b/txcache/selection.go @@ -57,7 +57,12 @@ func selectTransactionsFromBunches(accountStateProvider AccountStateProvider, bu } } - item.requestAccountStateIfNecessary(accountStateProvider) + err := item.requestAccountStateIfNecessary(accountStateProvider) + if err != nil { + // Skip this sender. + logSelect.Debug("TxCache.selectTransactionsFromBunches, could not retrieve account state", "sender", item.sender, "err", err) + continue + } shouldSkipSender := detectSkippableSender(item) if shouldSkipSender { @@ -71,7 +76,7 @@ func selectTransactionsFromBunches(accountStateProvider AccountStateProvider, bu // Transaction isn't selected, but the sender is still in the game (will contribute with other transactions). } else { accumulatedGas += gasLimit - selectedTransactions = append(selectedTransactions, item.selectTransaction()) + selectedTransactions = append(selectedTransactions, item.selectCurrentTransaction()) } // If there are more transactions in the same bunch (same sender as the popped item), diff --git a/txcache/selection_test.go b/txcache/selection_test.go index 6a2bddc1..3fa40ed1 100644 --- a/txcache/selection_test.go +++ b/txcache/selection_test.go @@ -3,13 +3,11 @@ package txcache import ( "fmt" "math" - "math/big" "testing" "time" "github.com/multiversx/mx-chain-core-go/core" "github.com/multiversx/mx-chain-storage-go/testscommon/txcachemocks" - "github.com/multiversx/mx-chain-storage-go/types" "github.com/stretchr/testify/require" ) @@ -17,6 +15,9 @@ func TestTxCache_SelectTransactions_Dummy(t *testing.T) { t.Run("all having same PPU", func(t *testing.T) { cache := newUnconstrainedCacheToTest() accountStateProvider := txcachemocks.NewAccountStateProviderMock() + accountStateProvider.SetNonce([]byte("alice"), 1) + accountStateProvider.SetNonce([]byte("bob"), 5) + accountStateProvider.SetNonce([]byte("carol"), 1) cache.AddTx(createTx([]byte("hash-alice-4"), "alice", 4)) cache.AddTx(createTx([]byte("hash-alice-3"), "alice", 3)) @@ -45,6 +46,9 @@ func TestTxCache_SelectTransactions_Dummy(t *testing.T) { t.Run("alice > carol > bob", func(t *testing.T) { cache := newUnconstrainedCacheToTest() accountStateProvider := txcachemocks.NewAccountStateProviderMock() + accountStateProvider.SetNonce([]byte("alice"), 1) + accountStateProvider.SetNonce([]byte("bob"), 5) + accountStateProvider.SetNonce([]byte("carol"), 3) cache.AddTx(createTx([]byte("hash-alice-1"), "alice", 1).withGasPrice(100)) cache.AddTx(createTx([]byte("hash-bob-5"), "bob", 5).withGasPrice(50)) @@ -65,6 +69,9 @@ func TestTxCache_SelectTransactionsWithBandwidth_Dummy(t *testing.T) { t.Run("transactions with no data field", func(t *testing.T) { cache := newUnconstrainedCacheToTest() accountStateProvider := txcachemocks.NewAccountStateProviderMock() + accountStateProvider.SetNonce([]byte("alice"), 1) + accountStateProvider.SetNonce([]byte("bob"), 5) + accountStateProvider.SetNonce([]byte("carol"), 1) cache.AddTx(createTx([]byte("hash-alice-4"), "alice", 4).withGasLimit(100000)) cache.AddTx(createTx([]byte("hash-alice-3"), "alice", 3).withGasLimit(100000)) @@ -92,6 +99,9 @@ func TestTxCache_SelectTransactions_HandlesGapsAndLowerNonces(t *testing.T) { t.Run("with middle gaps", func(t *testing.T) { cache := newUnconstrainedCacheToTest() accountStateProvider := txcachemocks.NewAccountStateProviderMock() + accountStateProvider.SetNonce([]byte("alice"), 1) + accountStateProvider.SetNonce([]byte("bob"), 42) + accountStateProvider.SetNonce([]byte("carol"), 7) cache.AddTx(createTx([]byte("hash-alice-1"), "alice", 1)) cache.AddTx(createTx([]byte("hash-alice-2"), "alice", 2)) @@ -114,16 +124,9 @@ func TestTxCache_SelectTransactions_HandlesGapsAndLowerNonces(t *testing.T) { t.Run("with initial gaps", func(t *testing.T) { cache := newUnconstrainedCacheToTest() accountStateProvider := txcachemocks.NewAccountStateProviderMock() - - noncesByAddress := accountStateProvider.AccountStateByAddress - noncesByAddress["alice"] = &types.AccountState{ - Nonce: 1, - Balance: big.NewInt(1000000000000000000), - } - noncesByAddress["bob"] = &types.AccountState{ - Nonce: 42, - Balance: big.NewInt(1000000000000000000), - } + accountStateProvider.SetNonce([]byte("alice"), 1) + accountStateProvider.SetNonce([]byte("bob"), 42) + accountStateProvider.SetNonce([]byte("carol"), 7) // No gap cache.AddTx(createTx([]byte("hash-alice-1"), "alice", 1)) @@ -135,7 +138,7 @@ func TestTxCache_SelectTransactions_HandlesGapsAndLowerNonces(t *testing.T) { cache.AddTx(createTx([]byte("hash-bob-43"), "bob", 45)) cache.AddTx(createTx([]byte("hash-bob-44"), "bob", 46)) - // Unknown + // Fine cache.AddTx(createTx([]byte("hash-carol-7"), "carol", 7)) cache.AddTx(createTx([]byte("hash-carol-8"), "carol", 8)) @@ -148,16 +151,9 @@ func TestTxCache_SelectTransactions_HandlesGapsAndLowerNonces(t *testing.T) { t.Run("with lower nonces", func(t *testing.T) { cache := newUnconstrainedCacheToTest() accountStateProvider := txcachemocks.NewAccountStateProviderMock() - - noncesByAddress := accountStateProvider.AccountStateByAddress - noncesByAddress["alice"] = &types.AccountState{ - Nonce: 1, - Balance: big.NewInt(1000000000000000000), - } - noncesByAddress["bob"] = &types.AccountState{ - Nonce: 42, - Balance: big.NewInt(1000000000000000000), - } + accountStateProvider.SetNonce([]byte("alice"), 1) + accountStateProvider.SetNonce([]byte("bob"), 42) + accountStateProvider.SetNonce([]byte("carol"), 7) // Good sequence cache.AddTx(createTx([]byte("hash-alice-1"), "alice", 1)) @@ -169,7 +165,7 @@ func TestTxCache_SelectTransactions_HandlesGapsAndLowerNonces(t *testing.T) { cache.AddTx(createTx([]byte("hash-bob-43"), "bob", 41)) cache.AddTx(createTx([]byte("hash-bob-44"), "bob", 42)) - // Unknown + // Fine cache.AddTx(createTx([]byte("hash-carol-7"), "carol", 7)) cache.AddTx(createTx([]byte("hash-carol-8"), "carol", 8)) @@ -192,7 +188,7 @@ func TestTxCache_SelectTransactions_WhenTransactionsAddedInReversedNonceOrder(t for senderTag := 0; senderTag < nSenders; senderTag++ { sender := fmt.Sprintf("sender:%d", senderTag) - for txNonce := nTransactionsPerSender; txNonce > 0; txNonce-- { + for txNonce := nTransactionsPerSender - 1; txNonce >= 0; txNonce-- { txHash := fmt.Sprintf("hash:%d:%d", senderTag, txNonce) tx := createTx([]byte(txHash), sender, uint64(txNonce)) cache.AddTx(tx) @@ -297,7 +293,7 @@ func TestBenchmarkTxCache_selectTransactionsFromBunches(t *testing.T) { // 0.496604s (TestBenchmarkTxCache_selectTransactionsFromBunches/numSenders_=_300000,_numTransactions_=_1) } -func TestTxCache_selectTransactionsFromBunches_lookBreaks_whenTakesTooLong(t *testing.T) { +func TestTxCache_selectTransactionsFromBunches_loopBreaks_whenTakesTooLong(t *testing.T) { t.Run("numSenders = 300000, numTransactions = 1", func(t *testing.T) { accountStateProvider := txcachemocks.NewAccountStateProviderMock() bunches := createBunchesOfTransactionsWithUniformDistribution(300000, 1) diff --git a/txcache/testutils_test.go b/txcache/testutils_test.go index 90186f31..0a5f3e3d 100644 --- a/txcache/testutils_test.go +++ b/txcache/testutils_test.go @@ -99,7 +99,7 @@ func addManyTransactionsWithUniformDistribution(cache *TxCache, nSenders int, nT for senderTag := 0; senderTag < nSenders; senderTag++ { sender := createFakeSenderAddress(senderTag) - for nonce := nTransactionsPerSender; nonce > 0; nonce-- { + for nonce := nTransactionsPerSender - 1; nonce >= 0; nonce-- { transactionHash := createFakeTxHash(sender, nonce) gasPrice := oneBillion + rand.Intn(3*oneBillion) transaction := createTx(transactionHash, string(sender), uint64(nonce)).withGasPrice(uint64(gasPrice)) @@ -117,7 +117,7 @@ func createBunchesOfTransactionsWithUniformDistribution(nSenders int, nTransacti bunch := make(bunchOfTransactions, 0, nTransactionsPerSender) sender := createFakeSenderAddress(senderTag) - for nonce := nTransactionsPerSender; nonce > 0; nonce-- { + for nonce := 0; nonce < nTransactionsPerSender; nonce++ { transactionHash := createFakeTxHash(sender, nonce) gasPrice := oneBillion + rand.Intn(3*oneBillion) transaction := createTx(transactionHash, string(sender), uint64(nonce)).withGasPrice(uint64(gasPrice)) diff --git a/txcache/transactionsHeapItem.go b/txcache/transactionsHeapItem.go index aa13df87..cbf49256 100644 --- a/txcache/transactionsHeapItem.go +++ b/txcache/transactionsHeapItem.go @@ -11,11 +11,7 @@ type transactionsHeapItem struct { sender []byte bunch bunchOfTransactions - // Whether the sender's state has been requested within a selection session. - senderStateRequested bool - // Whether the sender's state has been requested and provided (with success) within a selection session. - senderStateProvided bool - // The sender's state (if requested and provided). + // The sender's state, as fetched in "requestAccountStateIfNecessary". senderState *types.AccountState currentTransactionIndex int @@ -38,9 +34,7 @@ func newTransactionsHeapItem(bunch bunchOfTransactions) (*transactionsHeapItem, sender: firstTransaction.Tx.GetSndAddr(), bunch: bunch, - senderStateRequested: false, - senderStateProvided: false, - senderState: nil, + senderState: nil, currentTransactionIndex: 0, currentTransaction: firstTransaction, @@ -51,7 +45,7 @@ func newTransactionsHeapItem(bunch bunchOfTransactions) (*transactionsHeapItem, }, nil } -func (item *transactionsHeapItem) selectTransaction() *WrappedTransaction { +func (item *transactionsHeapItem) selectCurrentTransaction() *WrappedTransaction { item.accumulateFee() item.latestSelectedTransaction = item.currentTransaction @@ -61,9 +55,8 @@ func (item *transactionsHeapItem) selectTransaction() *WrappedTransaction { } func (item *transactionsHeapItem) accumulateFee() { - fee := item.currentTransaction.Fee.Load() + fee := item.currentTransaction.Fee if fee == nil { - // This should never happen during selection. return } @@ -85,14 +78,13 @@ func (item *transactionsHeapItem) detectInitialGap() bool { if item.latestSelectedTransaction != nil { return false } - if !item.senderStateProvided { - // This should never happen during selection. + if item.senderState == nil { return false } hasInitialGap := item.currentTransactionNonce > item.senderState.Nonce if hasInitialGap { - logSelect.Trace("transactionsHeapItem.detectGap, initial gap", + logSelect.Trace("transactionsHeapItem.detectInitialGap, initial gap", "tx", item.currentTransaction.TxHash, "nonce", item.currentTransactionNonce, "sender", item.sender, @@ -111,7 +103,7 @@ func (item *transactionsHeapItem) detectMiddleGap() bool { // Detect middle gap. hasMiddleGap := item.currentTransactionNonce > item.latestSelectedTransactionNonce+1 if hasMiddleGap { - logSelect.Trace("transactionsHeapItem.detectGap, middle gap", + logSelect.Trace("transactionsHeapItem.detectMiddleGap, middle gap", "tx", item.currentTransaction.TxHash, "nonce", item.currentTransactionNonce, "sender", item.sender, @@ -123,14 +115,17 @@ func (item *transactionsHeapItem) detectMiddleGap() bool { } func (item *transactionsHeapItem) detectWillFeeExceedBalance() bool { - if !item.senderStateProvided { - // This should never happen during selection. + if item.senderState == nil { return false } + fee := item.currentTransaction.Fee + if fee == nil { + return false + } + + futureAccumulatedFee := new(big.Int).Add(item.accumulatedFee, fee) senderBalance := item.senderState.Balance - currentTransactionFee := item.currentTransaction.Fee.Load() - futureAccumulatedFee := new(big.Int).Add(item.accumulatedFee, currentTransactionFee) willFeeExceedBalance := futureAccumulatedFee.Cmp(senderBalance) > 0 if willFeeExceedBalance { @@ -146,8 +141,7 @@ func (item *transactionsHeapItem) detectWillFeeExceedBalance() bool { } func (item *transactionsHeapItem) detectLowerNonce() bool { - if !item.senderStateProvided { - // This should never happen during selection. + if item.senderState == nil { return false } @@ -165,13 +159,13 @@ func (item *transactionsHeapItem) detectLowerNonce() bool { } func (item *transactionsHeapItem) detectBadlyGuarded() bool { - if !item.senderStateProvided { - // This should never happen during selection. + if item.senderState == nil { return false } - transactionGuardian := *item.currentTransaction.Guardian.Load() + transactionGuardian := item.currentTransaction.Guardian accountGuardian := item.senderState.Guardian + isBadlyGuarded := bytes.Compare(transactionGuardian, accountGuardian) != 0 if isBadlyGuarded { logSelect.Trace("transactionsHeapItem.detectBadlyGuarded", @@ -202,19 +196,16 @@ func (item *transactionsHeapItem) detectNonceDuplicate() bool { return isDuplicate } -func (item *transactionsHeapItem) requestAccountStateIfNecessary(accountStateProvider AccountStateProvider) { - if item.senderStateRequested { - return +func (item *transactionsHeapItem) requestAccountStateIfNecessary(accountStateProvider AccountStateProvider) error { + if item.senderState != nil { + return nil } - item.senderStateRequested = true senderState, err := accountStateProvider.GetAccountState(item.sender) if err != nil { - // Hazardous; should never happen. - logSelect.Debug("transactionsHeapItem.requestAccountStateIfNecessary: nonce not available", "sender", item.sender, "err", err) - return + return err } - item.senderStateProvided = true item.senderState = senderState + return nil } diff --git a/txcache/transactionsHeapItem_test.go b/txcache/transactionsHeapItem_test.go index 365c6ee8..5fb230a4 100644 --- a/txcache/transactionsHeapItem_test.go +++ b/txcache/transactionsHeapItem_test.go @@ -27,8 +27,6 @@ func TestNewTransactionsHeapItem(t *testing.T) { require.Equal(t, []byte("alice"), item.sender) require.Equal(t, bunch, item.bunch) - require.False(t, item.senderStateRequested) - require.False(t, item.senderStateProvided) require.Nil(t, item.senderState) require.Equal(t, 0, item.currentTransactionIndex) require.Equal(t, bunch[0], item.currentTransaction) @@ -49,7 +47,7 @@ func TestTransactionsHeapItem_selectTransaction(t *testing.T) { item, err := newTransactionsHeapItem(bunchOfTransactions{a, b}) require.NoError(t, err) - selected := item.selectTransaction() + selected := item.selectCurrentTransaction() require.Equal(t, a, selected) require.Equal(t, a, item.latestSelectedTransaction) require.Equal(t, 42, int(item.latestSelectedTransactionNonce)) @@ -58,7 +56,7 @@ func TestTransactionsHeapItem_selectTransaction(t *testing.T) { ok := item.gotoNextTransaction() require.True(t, ok) - selected = item.selectTransaction() + selected = item.selectCurrentTransaction() require.Equal(t, b, selected) require.Equal(t, b, item.latestSelectedTransaction) require.Equal(t, 43, int(item.latestSelectedTransactionNonce)) @@ -83,7 +81,6 @@ func TestTransactionsHeapItem_detectInitialGap(t *testing.T) { item, err := newTransactionsHeapItem(bunchOfTransactions{a, b}) require.NoError(t, err) - item.senderStateProvided = true item.senderState = &types.AccountState{ Nonce: 42, } @@ -95,7 +92,6 @@ func TestTransactionsHeapItem_detectInitialGap(t *testing.T) { item, err := newTransactionsHeapItem(bunchOfTransactions{a, b}) require.NoError(t, err) - item.senderStateProvided = true item.senderState = &types.AccountState{ Nonce: 41, } @@ -155,14 +151,13 @@ func TestTransactionsHeapItem_detectFeeExceededBalance(t *testing.T) { item, err := newTransactionsHeapItem(bunchOfTransactions{a, b}) require.NoError(t, err) - item.senderStateProvided = true item.senderState = &types.AccountState{ Balance: big.NewInt(50000000000001), } require.False(t, item.detectWillFeeExceedBalance()) - _ = item.selectTransaction() + _ = item.selectCurrentTransaction() _ = item.gotoNextTransaction() require.Equal(t, "50000000000000", item.accumulatedFee.String()) @@ -185,7 +180,6 @@ func TestTransactionsHeapItem_detectLowerNonce(t *testing.T) { item, err := newTransactionsHeapItem(bunchOfTransactions{a, b}) require.NoError(t, err) - item.senderStateProvided = true item.senderState = &types.AccountState{ Nonce: 42, } @@ -197,7 +191,6 @@ func TestTransactionsHeapItem_detectLowerNonce(t *testing.T) { item, err := newTransactionsHeapItem(bunchOfTransactions{a, b}) require.NoError(t, err) - item.senderStateProvided = true item.senderState = &types.AccountState{ Nonce: 44, } @@ -226,7 +219,6 @@ func TestTransactionsHeapItem_detectBadlyGuarded(t *testing.T) { item, err := newTransactionsHeapItem(bunchOfTransactions{a}) require.NoError(t, err) - item.senderStateProvided = true item.senderState = &types.AccountState{ Guardian: nil, } @@ -238,7 +230,6 @@ func TestTransactionsHeapItem_detectBadlyGuarded(t *testing.T) { item, err := newTransactionsHeapItem(bunchOfTransactions{b}) require.NoError(t, err) - item.senderStateProvided = true item.senderState = &types.AccountState{ Guardian: []byte("heidi"), } @@ -250,7 +241,6 @@ func TestTransactionsHeapItem_detectBadlyGuarded(t *testing.T) { item, err := newTransactionsHeapItem(bunchOfTransactions{b}) require.NoError(t, err) - item.senderStateProvided = true item.senderState = &types.AccountState{ Guardian: []byte("grace"), } @@ -262,7 +252,6 @@ func TestTransactionsHeapItem_detectBadlyGuarded(t *testing.T) { item, err := newTransactionsHeapItem(bunchOfTransactions{b}) require.NoError(t, err) - item.senderStateProvided = true item.senderState = &types.AccountState{ Guardian: nil, } @@ -274,7 +263,6 @@ func TestTransactionsHeapItem_detectBadlyGuarded(t *testing.T) { item, err := newTransactionsHeapItem(bunchOfTransactions{a}) require.NoError(t, err) - item.senderStateProvided = true item.senderState = &types.AccountState{ Guardian: []byte("heidi"), } @@ -341,15 +329,7 @@ func TestTransactionsHeapItem_requestAccountStateIfNecessary(t *testing.T) { a.requestAccountStateIfNecessary(accountStateProvider) b.requestAccountStateIfNecessary(accountStateProvider) - require.True(t, a.senderStateRequested) - require.True(t, a.senderStateProvided) require.Equal(t, uint64(7), a.senderState.Nonce) - - require.True(t, b.senderStateRequested) - require.True(t, b.senderStateProvided) require.Equal(t, uint64(42), b.senderState.Nonce) - - require.False(t, c.senderStateRequested) - require.False(t, c.senderStateProvided) require.Nil(t, c.senderState) } diff --git a/txcache/wrappedTransaction.go b/txcache/wrappedTransaction.go index 883f54df..30c5cef8 100644 --- a/txcache/wrappedTransaction.go +++ b/txcache/wrappedTransaction.go @@ -3,7 +3,6 @@ package txcache import ( "bytes" "math/big" - "sync/atomic" "github.com/multiversx/mx-chain-core-go/data" ) @@ -19,37 +18,34 @@ type WrappedTransaction struct { ReceiverShardID uint32 Size int64 - Fee atomic.Pointer[big.Int] - PricePerUnit atomic.Uint64 - Guardian atomic.Pointer[[]byte] + // These fields are only set within "precomputeFields". + // We don't need to protect them with a mutex, since "precomputeFields" is called only once for each transaction. + // Additional note: "WrappedTransaction" objects are created by the Node, in dataRetriever/txpool/shardedTxPool.go. + Fee *big.Int + PricePerUnit uint64 + Guardian []byte } // precomputeFields computes (and caches) the (average) price per gas unit. func (wrappedTx *WrappedTransaction) precomputeFields(txGasHandler TxGasHandler) { - fee := txGasHandler.ComputeTxFee(wrappedTx.Tx) + wrappedTx.Fee = txGasHandler.ComputeTxFee(wrappedTx.Tx) gasLimit := wrappedTx.Tx.GetGasLimit() - if gasLimit == 0 { - return + if gasLimit != 0 { + wrappedTx.PricePerUnit = wrappedTx.Fee.Uint64() / gasLimit } - wrappedTx.Fee.Store(fee) - wrappedTx.PricePerUnit.Store(fee.Uint64() / gasLimit) - txAsGuardedTransaction, ok := wrappedTx.Tx.(data.GuardedTransactionHandler) - if !ok { - return + if ok { + wrappedTx.Guardian = txAsGuardedTransaction.GetGuardianAddr() } - - guardian := txAsGuardedTransaction.GetGuardianAddr() - wrappedTx.Guardian.Store(&guardian) } // Equality is out of scope (not possible in our case). func (wrappedTx *WrappedTransaction) isTransactionMoreValuableForNetwork(otherTransaction *WrappedTransaction) bool { // First, compare by price per unit - ppu := wrappedTx.PricePerUnit.Load() - ppuOther := otherTransaction.PricePerUnit.Load() + ppu := wrappedTx.PricePerUnit + ppuOther := otherTransaction.PricePerUnit if ppu != ppuOther { return ppu > ppuOther } diff --git a/txcache/wrappedTransaction_test.go b/txcache/wrappedTransaction_test.go index 805010b8..fe8bc5e7 100644 --- a/txcache/wrappedTransaction_test.go +++ b/txcache/wrappedTransaction_test.go @@ -14,18 +14,18 @@ func TestWrappedTransaction_precomputeFields(t *testing.T) { tx := createTx([]byte("a"), "a", 1).withDataLength(1).withGasLimit(51500).withGasPrice(oneBillion) tx.precomputeFields(txGasHandler) - require.Equal(t, "51500000000000", tx.Fee.Load().String()) - require.Equal(t, oneBillion, int(tx.PricePerUnit.Load())) - require.Empty(t, tx.Guardian.Load()) + require.Equal(t, "51500000000000", tx.Fee.String()) + require.Equal(t, oneBillion, int(tx.PricePerUnit)) + require.Empty(t, tx.Guardian) }) t.Run("move balance gas limit and execution gas limit (1)", func(t *testing.T) { tx := createTx([]byte("b"), "b", 1).withDataLength(1).withGasLimit(51501).withGasPrice(oneBillion) tx.precomputeFields(txGasHandler) - require.Equal(t, "51500010000000", tx.Fee.Load().String()) - require.Equal(t, 999_980_777, int(tx.PricePerUnit.Load())) - require.Empty(t, tx.Guardian.Load()) + require.Equal(t, "51500010000000", tx.Fee.String()) + require.Equal(t, 999_980_777, int(tx.PricePerUnit)) + require.Empty(t, tx.Guardian) }) t.Run("move balance gas limit and execution gas limit (2)", func(t *testing.T) { @@ -33,19 +33,19 @@ func TestWrappedTransaction_precomputeFields(t *testing.T) { tx.precomputeFields(txGasHandler) actualFee := 51500*oneBillion + (oneMilion-51500)*oneBillion/100 - require.Equal(t, "60985000000000", tx.Fee.Load().String()) + require.Equal(t, "60985000000000", tx.Fee.String()) require.Equal(t, 60_985_000_000_000, actualFee) - require.Equal(t, actualFee/oneMilion, int(tx.PricePerUnit.Load())) - require.Empty(t, tx.Guardian.Load()) + require.Equal(t, actualFee/oneMilion, int(tx.PricePerUnit)) + require.Empty(t, tx.Guardian) }) t.Run("with guardian", func(t *testing.T) { tx := createTx([]byte("a"), "a", 1).withGuardian([]byte("heidi")) tx.precomputeFields(txGasHandler) - require.Equal(t, "50000000000000", tx.Fee.Load().String()) - require.Equal(t, oneBillion, int(tx.PricePerUnit.Load())) - require.Equal(t, []byte("heidi"), *tx.Guardian.Load()) + require.Equal(t, "50000000000000", tx.Fee.String()) + require.Equal(t, oneBillion, int(tx.PricePerUnit)) + require.Equal(t, []byte("heidi"), tx.Guardian) }) } From f159f60802b2ef2adacc3bddc14738f56808fba5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Wed, 20 Nov 2024 23:03:54 +0200 Subject: [PATCH 29/34] Fix after self review. --- txcache/eviction_test.go | 8 +-- txcache/selection_test.go | 91 ++++++++++++++++++++++++++++----- txcache/transactionsHeap.go | 4 +- txcache/transactionsHeapItem.go | 4 ++ txcache/txListForSender.go | 5 -- txcache/wrappedTransaction.go | 7 +-- 6 files changed, 91 insertions(+), 28 deletions(-) diff --git a/txcache/eviction_test.go b/txcache/eviction_test.go index 2ec09864..fb51859f 100644 --- a/txcache/eviction_test.go +++ b/txcache/eviction_test.go @@ -220,8 +220,8 @@ func TestBenchmarkTxCache_DoEviction(t *testing.T) { // Thread(s) per core: 2 // Core(s) per socket: 4 // - // 0.160000s (TestBenchmarkTxCache_DoEviction_Benchmark/numSenders_=_35000,_numTransactions_=_10) - // 0.506890s (TestBenchmarkTxCache_DoEviction_Benchmark/numSenders_=_100000,_numTransactions_=_5) - // 0.602928s (TestBenchmarkTxCache_DoEviction_Benchmark/numSenders_=_10000,_numTransactions_=_100) - // 0.654148s (TestBenchmarkTxCache_DoEviction_Benchmark/numSenders_=_400000,_numTransactions_=_1) + // 0.119274s (TestBenchmarkTxCache_DoEviction/numSenders_=_35000,_numTransactions_=_10) + // 0.484147s (TestBenchmarkTxCache_DoEviction/numSenders_=_100000,_numTransactions_=_5) + // 0.504588s (TestBenchmarkTxCache_DoEviction/numSenders_=_10000,_numTransactions_=_100) + // 0.571885s (TestBenchmarkTxCache_DoEviction/numSenders_=_400000,_numTransactions_=_1) } diff --git a/txcache/selection_test.go b/txcache/selection_test.go index 3fa40ed1..e2003a4c 100644 --- a/txcache/selection_test.go +++ b/txcache/selection_test.go @@ -3,6 +3,7 @@ package txcache import ( "fmt" "math" + "math/big" "testing" "time" @@ -95,7 +96,7 @@ func TestTxCache_SelectTransactionsWithBandwidth_Dummy(t *testing.T) { }) } -func TestTxCache_SelectTransactions_HandlesGapsAndLowerNonces(t *testing.T) { +func TestTxCache_SelectTransactions_HandlesNotExecutableTransactions(t *testing.T) { t.Run("with middle gaps", func(t *testing.T) { cache := newUnconstrainedCacheToTest() accountStateProvider := txcachemocks.NewAccountStateProviderMock() @@ -128,7 +129,7 @@ func TestTxCache_SelectTransactions_HandlesGapsAndLowerNonces(t *testing.T) { accountStateProvider.SetNonce([]byte("bob"), 42) accountStateProvider.SetNonce([]byte("carol"), 7) - // No gap + // Good cache.AddTx(createTx([]byte("hash-alice-1"), "alice", 1)) cache.AddTx(createTx([]byte("hash-alice-2"), "alice", 2)) cache.AddTx(createTx([]byte("hash-alice-3"), "alice", 3)) @@ -138,7 +139,7 @@ func TestTxCache_SelectTransactions_HandlesGapsAndLowerNonces(t *testing.T) { cache.AddTx(createTx([]byte("hash-bob-43"), "bob", 45)) cache.AddTx(createTx([]byte("hash-bob-44"), "bob", 46)) - // Fine + // Good cache.AddTx(createTx([]byte("hash-carol-7"), "carol", 7)) cache.AddTx(createTx([]byte("hash-carol-8"), "carol", 8)) @@ -155,7 +156,7 @@ func TestTxCache_SelectTransactions_HandlesGapsAndLowerNonces(t *testing.T) { accountStateProvider.SetNonce([]byte("bob"), 42) accountStateProvider.SetNonce([]byte("carol"), 7) - // Good sequence + // Good cache.AddTx(createTx([]byte("hash-alice-1"), "alice", 1)) cache.AddTx(createTx([]byte("hash-alice-2"), "alice", 2)) cache.AddTx(createTx([]byte("hash-alice-3"), "alice", 3)) @@ -165,7 +166,7 @@ func TestTxCache_SelectTransactions_HandlesGapsAndLowerNonces(t *testing.T) { cache.AddTx(createTx([]byte("hash-bob-43"), "bob", 41)) cache.AddTx(createTx([]byte("hash-bob-44"), "bob", 42)) - // Fine + // Good cache.AddTx(createTx([]byte("hash-carol-7"), "carol", 7)) cache.AddTx(createTx([]byte("hash-carol-8"), "carol", 8)) @@ -174,6 +175,72 @@ func TestTxCache_SelectTransactions_HandlesGapsAndLowerNonces(t *testing.T) { require.Len(t, sorted, expectedNumSelected) require.Equal(t, 300000, int(accumulatedGas)) }) + + t.Run("with duplicated nonces", func(t *testing.T) { + cache := newUnconstrainedCacheToTest() + accountStateProvider := txcachemocks.NewAccountStateProviderMock() + accountStateProvider.SetNonce([]byte("alice"), 1) + + cache.AddTx(createTx([]byte("hash-alice-1"), "alice", 1)) + cache.AddTx(createTx([]byte("hash-alice-2"), "alice", 2)) + cache.AddTx(createTx([]byte("hash-alice-3a"), "alice", 3)) + cache.AddTx(createTx([]byte("hash-alice-3b"), "alice", 3).withGasPrice(oneBillion * 2)) + cache.AddTx(createTx([]byte("hash-alice-3c"), "alice", 3)) + cache.AddTx(createTx([]byte("hash-alice-4"), "alice", 4)) + + sorted, accumulatedGas := cache.SelectTransactions(accountStateProvider, math.MaxUint64, math.MaxInt, selectionLoopMaximumDuration) + require.Len(t, sorted, 4) + require.Equal(t, 200000, int(accumulatedGas)) + + require.Equal(t, "hash-alice-1", string(sorted[0].TxHash)) + require.Equal(t, "hash-alice-2", string(sorted[1].TxHash)) + require.Equal(t, "hash-alice-3b", string(sorted[2].TxHash)) + require.Equal(t, "hash-alice-4", string(sorted[3].TxHash)) + }) + + t.Run("with fee exceeding balance", func(t *testing.T) { + cache := newUnconstrainedCacheToTest() + accountStateProvider := txcachemocks.NewAccountStateProviderMock() + accountStateProvider.SetNonce([]byte("alice"), 1) + accountStateProvider.SetBalance([]byte("alice"), big.NewInt(150000000000000)) + accountStateProvider.SetNonce([]byte("bob"), 42) + accountStateProvider.SetBalance([]byte("bob"), big.NewInt(70000000000000)) + + // Enough balance + cache.AddTx(createTx([]byte("hash-alice-1"), "alice", 1)) + cache.AddTx(createTx([]byte("hash-alice-2"), "alice", 2)) + cache.AddTx(createTx([]byte("hash-alice-3"), "alice", 3)) + + // Not enough balance + cache.AddTx(createTx([]byte("hash-bob-42"), "bob", 40)) + cache.AddTx(createTx([]byte("hash-bob-43"), "bob", 41)) + cache.AddTx(createTx([]byte("hash-bob-44"), "bob", 42)) + + sorted, accumulatedGas := cache.SelectTransactions(accountStateProvider, math.MaxUint64, math.MaxInt, selectionLoopMaximumDuration) + expectedNumSelected := 3 + 1 // 3 alice + 1 bob + require.Len(t, sorted, expectedNumSelected) + require.Equal(t, 200000, int(accumulatedGas)) + }) + + t.Run("with guardians", func(t *testing.T) { + cache := newUnconstrainedCacheToTest() + accountStateProvider := txcachemocks.NewAccountStateProviderMock() + accountStateProvider.SetNonce([]byte("alice"), 1) + accountStateProvider.SetNonce([]byte("bob"), 42) + accountStateProvider.SetGuardian([]byte("bob"), []byte("heidi")) + + cache.AddTx(createTx([]byte("hash-alice-1"), "alice", 1)) + cache.AddTx(createTx([]byte("hash-bob-42a"), "bob", 42)) + cache.AddTx(createTx([]byte("hash-bob-42b"), "bob", 42).withGuardian([]byte("heidi")).withGasLimit(100000)) + cache.AddTx(createTx([]byte("hash-bob-43"), "bob", 43).withGuardian([]byte("grace")).withGasLimit(100000)) + + sorted, accumulatedGas := cache.SelectTransactions(accountStateProvider, math.MaxUint64, math.MaxInt, selectionLoopMaximumDuration) + require.Len(t, sorted, 2) + require.Equal(t, 150000, int(accumulatedGas)) + + require.Equal(t, "hash-alice-1", string(sorted[0].TxHash)) + require.Equal(t, "hash-bob-42b", string(sorted[1].TxHash)) + }) } func TestTxCache_SelectTransactions_WhenTransactionsAddedInReversedNonceOrder(t *testing.T) { @@ -287,10 +354,10 @@ func TestBenchmarkTxCache_selectTransactionsFromBunches(t *testing.T) { // Thread(s) per core: 2 // Core(s) per socket: 4 // - // 0.053758s (TestBenchmarkTxCache_selectTransactionsFromBunches/numSenders_=_1000,_numTransactions_=_1000) - // 0.050731s (TestBenchmarkTxCache_selectTransactionsFromBunches/numSenders_=_10000,_numTransactions_=_100) - // 0.302232s (TestBenchmarkTxCache_selectTransactionsFromBunches/numSenders_=_100000,_numTransactions_=_3) - // 0.496604s (TestBenchmarkTxCache_selectTransactionsFromBunches/numSenders_=_300000,_numTransactions_=_1) + // 0.057519s (TestBenchmarkTxCache_selectTransactionsFromBunches/numSenders_=_1000,_numTransactions_=_1000) + // 0.048023s (TestBenchmarkTxCache_selectTransactionsFromBunches/numSenders_=_10000,_numTransactions_=_100) + // 0.289515s (TestBenchmarkTxCache_selectTransactionsFromBunches/numSenders_=_100000,_numTransactions_=_3) + // 0.460242s (TestBenchmarkTxCache_selectTransactionsFromBunches/numSenders_=_300000,_numTransactions_=_1) } func TestTxCache_selectTransactionsFromBunches_loopBreaks_whenTakesTooLong(t *testing.T) { @@ -381,7 +448,7 @@ func TestBenchmarkTxCache_doSelectTransactions(t *testing.T) { // Thread(s) per core: 2 // Core(s) per socket: 4 // - // 0.112178s (TestBenchmarkTxCache_doSelectTransactions/numSenders_=_50000,_numTransactions_=_2,_maxNum_=_50_000) - // 0.160638s (TestBenchmarkTxCache_doSelectTransactions/numSenders_=_100000,_numTransactions_=_1,_maxNum_=_50_000) - // 0.371011s (TestBenchmarkTxCache_doSelectTransactions/numSenders_=_300000,_numTransactions_=_1,_maxNum_=_50_000) + // 0.107361s (TestBenchmarkTxCache_doSelectTransactions/numSenders_=_50000,_numTransactions_=_2,_maxNum_=_50_000) + // 0.168364s (TestBenchmarkTxCache_doSelectTransactions/numSenders_=_100000,_numTransactions_=_1,_maxNum_=_50_000) + // 0.305363s (TestBenchmarkTxCache_doSelectTransactions/numSenders_=_300000,_numTransactions_=_1,_maxNum_=_50_000) } diff --git a/txcache/transactionsHeap.go b/txcache/transactionsHeap.go index fef11698..e96ebb54 100644 --- a/txcache/transactionsHeap.go +++ b/txcache/transactionsHeap.go @@ -11,7 +11,7 @@ func newMinTransactionsHeap(capacity int) *transactionsHeap { } h.less = func(i, j int) bool { - return h.items[j].currentTransaction.isTransactionMoreValuableForNetwork(h.items[i].currentTransaction) + return h.items[j].holdsTransactionMoreValuableForNetwork(h.items[i]) } return &h @@ -23,7 +23,7 @@ func newMaxTransactionsHeap(capacity int) *transactionsHeap { } h.less = func(i, j int) bool { - return h.items[i].currentTransaction.isTransactionMoreValuableForNetwork(h.items[j].currentTransaction) + return h.items[i].holdsTransactionMoreValuableForNetwork(h.items[j]) } return &h diff --git a/txcache/transactionsHeapItem.go b/txcache/transactionsHeapItem.go index cbf49256..e47c6800 100644 --- a/txcache/transactionsHeapItem.go +++ b/txcache/transactionsHeapItem.go @@ -209,3 +209,7 @@ func (item *transactionsHeapItem) requestAccountStateIfNecessary(accountStatePro item.senderState = senderState return nil } + +func (item *transactionsHeapItem) holdsTransactionMoreValuableForNetwork(other *transactionsHeapItem) bool { + return item.currentTransaction.isTransactionMoreValuableForNetwork(other.currentTransaction) +} diff --git a/txcache/txListForSender.go b/txcache/txListForSender.go index fc5c048c..67e4e8b6 100644 --- a/txcache/txListForSender.go +++ b/txcache/txListForSender.go @@ -240,8 +240,3 @@ func (listForSender *txListForSender) removeTransactionsWithHigherOrEqualNonce(g element = prevElement } } - -// GetKey returns the key -func (listForSender *txListForSender) GetKey() string { - return listForSender.sender -} diff --git a/txcache/wrappedTransaction.go b/txcache/wrappedTransaction.go index 30c5cef8..ad6104ed 100644 --- a/txcache/wrappedTransaction.go +++ b/txcache/wrappedTransaction.go @@ -43,11 +43,8 @@ func (wrappedTx *WrappedTransaction) precomputeFields(txGasHandler TxGasHandler) // Equality is out of scope (not possible in our case). func (wrappedTx *WrappedTransaction) isTransactionMoreValuableForNetwork(otherTransaction *WrappedTransaction) bool { - // First, compare by price per unit - ppu := wrappedTx.PricePerUnit - ppuOther := otherTransaction.PricePerUnit - if ppu != ppuOther { - return ppu > ppuOther + if wrappedTx.PricePerUnit != otherTransaction.PricePerUnit { + return wrappedTx.PricePerUnit > otherTransaction.PricePerUnit } // In the end, compare by transaction hash From d5ef41b70b3f3cf8057b958454668b500b3dc8fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Wed, 20 Nov 2024 23:10:22 +0200 Subject: [PATCH 30/34] Remove out-of-reality test. --- txcache/txCache_test.go | 40 ---------------------------------------- 1 file changed, 40 deletions(-) diff --git a/txcache/txCache_test.go b/txcache/txCache_test.go index c071fa62..88748e3a 100644 --- a/txcache/txCache_test.go +++ b/txcache/txCache_test.go @@ -7,7 +7,6 @@ import ( "sort" "sync" "testing" - "time" "github.com/multiversx/mx-chain-core-go/core/check" "github.com/multiversx/mx-chain-storage-go/common" @@ -446,45 +445,6 @@ func Test_IsInterfaceNil(t *testing.T) { require.True(t, check.IfNil(thisIsNil)) } -func TestTxCache_ConcurrentMutationAndSelection(t *testing.T) { - cache := newUnconstrainedCacheToTest() - accountStateProvider := txcachemocks.NewAccountStateProviderMock() - - // Alice will quickly move between two score buckets (chunks) - cheapTransaction := createTx([]byte("alice-x-o"), "alice", 0).withDataLength(1).withGasLimit(300000000).withGasPrice(oneBillion) - expensiveTransaction := createTx([]byte("alice-x-1"), "alice", 1).withDataLength(42).withGasLimit(50000000).withGasPrice(10 * oneBillion) - cache.AddTx(cheapTransaction) - cache.AddTx(expensiveTransaction) - - wg := sync.WaitGroup{} - - // Simulate selection - wg.Add(1) - go func() { - for i := 0; i < 100; i++ { - fmt.Println("Selection", i) - _, _ = cache.SelectTransactions(accountStateProvider, math.MaxUint64, math.MaxInt, selectionLoopMaximumDuration) - } - - wg.Done() - }() - - // Simulate add / remove transactions - wg.Add(1) - go func() { - for i := 0; i < 100; i++ { - fmt.Println("Add / remove", i) - cache.Remove([]byte("alice-x-1")) - cache.AddTx(expensiveTransaction) - } - - wg.Done() - }() - - timedOut := waitTimeout(&wg, 1*time.Second) - require.False(t, timedOut, "Timed out. Perhaps deadlock?") -} - func TestTxCache_TransactionIsAdded_EvenWhenInternalMapsAreInconsistent(t *testing.T) { cache := newUnconstrainedCacheToTest() From 457c06ca6f2f5378ac8639b27434905a0a590c2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Thu, 21 Nov 2024 09:16:31 +0200 Subject: [PATCH 31/34] Update readme. --- txcache/README.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/txcache/README.md b/txcache/README.md index 913f2e9f..186b4284 100644 --- a/txcache/README.md +++ b/txcache/README.md @@ -154,8 +154,6 @@ Thus, the mempool selects transactions using an efficient and value-driven algor - **Organize transactions into bunches:** - For each sender, collect all their pending transactions and organize them into a "bunch." - Each bunch is: - - **Middle-nonces-gap-free:** There are no missing nonces between transactions. - - **Duplicates-free:** No duplicate transactions are included. - **Sorted by nonce:** Transactions are ordered in ascending order based on their nonce values. - **Prepare the heap:** @@ -182,9 +180,14 @@ Thus, the mempool selects transactions using an efficient and value-driven algor - The number of selected transactions reaches `maxNum`. **Additional notes:** - - Within the selection loop, the current nonce of the sender is queryied from the blockchain, if necessary. - - If an initial nonce gap is detected, the sender is excluded from the selection process. - - Transactions with nonces lower than the current nonce of the sender are skipped (not included in the selection). + - Within the selection loop, the current nonce of the sender is queryied from the blockchain, lazily (when needed). + - If an initial nonce gap is detected, the sender is (completely) skipped in the current selection session. + - If a middle nonce gap is detected, the sender is skipped (from now on) in the current selection session. + - Transactions with nonces lower than the current nonce of the sender are skipped. + - Transactions with duplicate nonces are skipped. See paragraph 5 for more details. + - Badly guarded transactions are skipped. + - Once the accumulated fees of selected transactions of a given sender exceed the sender's balance, the sender is skipped (from now one). + ### Paragraph 5 From 92d0476c7cb3901e193f202cb3f38d2569c19726 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Thu, 21 Nov 2024 09:52:36 +0200 Subject: [PATCH 32/34] Fix linter issues. --- txcache/transactionsHeapItem.go | 2 +- txcache/transactionsHeapItem_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/txcache/transactionsHeapItem.go b/txcache/transactionsHeapItem.go index e47c6800..5b16b07a 100644 --- a/txcache/transactionsHeapItem.go +++ b/txcache/transactionsHeapItem.go @@ -166,7 +166,7 @@ func (item *transactionsHeapItem) detectBadlyGuarded() bool { transactionGuardian := item.currentTransaction.Guardian accountGuardian := item.senderState.Guardian - isBadlyGuarded := bytes.Compare(transactionGuardian, accountGuardian) != 0 + isBadlyGuarded := !bytes.Equal(transactionGuardian, accountGuardian) if isBadlyGuarded { logSelect.Trace("transactionsHeapItem.detectBadlyGuarded", "tx", item.currentTransaction.TxHash, diff --git a/txcache/transactionsHeapItem_test.go b/txcache/transactionsHeapItem_test.go index 5fb230a4..37eb9793 100644 --- a/txcache/transactionsHeapItem_test.go +++ b/txcache/transactionsHeapItem_test.go @@ -326,8 +326,8 @@ func TestTransactionsHeapItem_requestAccountStateIfNecessary(t *testing.T) { c := &transactionsHeapItem{} - a.requestAccountStateIfNecessary(accountStateProvider) - b.requestAccountStateIfNecessary(accountStateProvider) + _ = a.requestAccountStateIfNecessary(accountStateProvider) + _ = b.requestAccountStateIfNecessary(accountStateProvider) require.Equal(t, uint64(7), a.senderState.Nonce) require.Equal(t, uint64(42), b.senderState.Nonce) From 93c958ad66cf9f7d4d855d899417b57a13e43af6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Sun, 24 Nov 2024 22:36:43 +0200 Subject: [PATCH 33/34] Fix after review (part 1). --- .../txcachemocks/accountStateProviderMock.go | 28 ++++---- txcache/selection_test.go | 20 ------ txcache/transactionsHeapItem.go | 21 +----- txcache/transactionsHeapItem_test.go | 72 ------------------- txcache/wrappedTransaction.go | 6 -- txcache/wrappedTransaction_test.go | 8 +-- types/accountState.go | 5 +- 7 files changed, 20 insertions(+), 140 deletions(-) diff --git a/testscommon/txcachemocks/accountStateProviderMock.go b/testscommon/txcachemocks/accountStateProviderMock.go index 339276f7..32de46f8 100644 --- a/testscommon/txcachemocks/accountStateProviderMock.go +++ b/testscommon/txcachemocks/accountStateProviderMock.go @@ -2,12 +2,15 @@ package txcachemocks import ( "math/big" + "sync" "github.com/multiversx/mx-chain-storage-go/types" ) // AccountStateProviderMock - type AccountStateProviderMock struct { + mutex sync.Mutex + AccountStateByAddress map[string]*types.AccountState GetAccountStateCalled func(address []byte) (*types.AccountState, error) } @@ -21,6 +24,9 @@ func NewAccountStateProviderMock() *AccountStateProviderMock { // SetNonce - func (mock *AccountStateProviderMock) SetNonce(address []byte, nonce uint64) { + mock.mutex.Lock() + defer mock.mutex.Unlock() + key := string(address) if mock.AccountStateByAddress[key] == nil { @@ -32,28 +38,23 @@ func (mock *AccountStateProviderMock) SetNonce(address []byte, nonce uint64) { // SetBalance - func (mock *AccountStateProviderMock) SetBalance(address []byte, balance *big.Int) { - key := string(address) - - if mock.AccountStateByAddress[key] == nil { - mock.AccountStateByAddress[key] = newDefaultAccountState() - } - - mock.AccountStateByAddress[key].Balance = balance -} + mock.mutex.Lock() + defer mock.mutex.Unlock() -// SetGuardian - -func (mock *AccountStateProviderMock) SetGuardian(address []byte, guardian []byte) { key := string(address) if mock.AccountStateByAddress[key] == nil { mock.AccountStateByAddress[key] = newDefaultAccountState() } - mock.AccountStateByAddress[key].Guardian = guardian + mock.AccountStateByAddress[key].Balance = balance } // GetAccountState - func (mock *AccountStateProviderMock) GetAccountState(address []byte) (*types.AccountState, error) { + mock.mutex.Lock() + defer mock.mutex.Unlock() + if mock.GetAccountStateCalled != nil { return mock.GetAccountStateCalled(address) } @@ -73,8 +74,7 @@ func (mock *AccountStateProviderMock) IsInterfaceNil() bool { func newDefaultAccountState() *types.AccountState { return &types.AccountState{ - Nonce: 0, - Balance: big.NewInt(1000000000000000000), - Guardian: nil, + Nonce: 0, + Balance: big.NewInt(1000000000000000000), } } diff --git a/txcache/selection_test.go b/txcache/selection_test.go index e2003a4c..7684a566 100644 --- a/txcache/selection_test.go +++ b/txcache/selection_test.go @@ -221,26 +221,6 @@ func TestTxCache_SelectTransactions_HandlesNotExecutableTransactions(t *testing. require.Len(t, sorted, expectedNumSelected) require.Equal(t, 200000, int(accumulatedGas)) }) - - t.Run("with guardians", func(t *testing.T) { - cache := newUnconstrainedCacheToTest() - accountStateProvider := txcachemocks.NewAccountStateProviderMock() - accountStateProvider.SetNonce([]byte("alice"), 1) - accountStateProvider.SetNonce([]byte("bob"), 42) - accountStateProvider.SetGuardian([]byte("bob"), []byte("heidi")) - - cache.AddTx(createTx([]byte("hash-alice-1"), "alice", 1)) - cache.AddTx(createTx([]byte("hash-bob-42a"), "bob", 42)) - cache.AddTx(createTx([]byte("hash-bob-42b"), "bob", 42).withGuardian([]byte("heidi")).withGasLimit(100000)) - cache.AddTx(createTx([]byte("hash-bob-43"), "bob", 43).withGuardian([]byte("grace")).withGasLimit(100000)) - - sorted, accumulatedGas := cache.SelectTransactions(accountStateProvider, math.MaxUint64, math.MaxInt, selectionLoopMaximumDuration) - require.Len(t, sorted, 2) - require.Equal(t, 150000, int(accumulatedGas)) - - require.Equal(t, "hash-alice-1", string(sorted[0].TxHash)) - require.Equal(t, "hash-bob-42b", string(sorted[1].TxHash)) - }) } func TestTxCache_SelectTransactions_WhenTransactionsAddedInReversedNonceOrder(t *testing.T) { diff --git a/txcache/transactionsHeapItem.go b/txcache/transactionsHeapItem.go index 5b16b07a..b54b46ef 100644 --- a/txcache/transactionsHeapItem.go +++ b/txcache/transactionsHeapItem.go @@ -1,7 +1,6 @@ package txcache import ( - "bytes" "math/big" "github.com/multiversx/mx-chain-storage-go/types" @@ -159,24 +158,8 @@ func (item *transactionsHeapItem) detectLowerNonce() bool { } func (item *transactionsHeapItem) detectBadlyGuarded() bool { - if item.senderState == nil { - return false - } - - transactionGuardian := item.currentTransaction.Guardian - accountGuardian := item.senderState.Guardian - - isBadlyGuarded := !bytes.Equal(transactionGuardian, accountGuardian) - if isBadlyGuarded { - logSelect.Trace("transactionsHeapItem.detectBadlyGuarded", - "tx", item.currentTransaction.TxHash, - "sender", item.sender, - "transactionGuardian", transactionGuardian, - "accountGuardian", accountGuardian, - ) - } - - return isBadlyGuarded + // See MX-16179. + return false } func (item *transactionsHeapItem) detectNonceDuplicate() bool { diff --git a/txcache/transactionsHeapItem_test.go b/txcache/transactionsHeapItem_test.go index 37eb9793..02a449d2 100644 --- a/txcache/transactionsHeapItem_test.go +++ b/txcache/transactionsHeapItem_test.go @@ -199,78 +199,6 @@ func TestTransactionsHeapItem_detectLowerNonce(t *testing.T) { }) } -func TestTransactionsHeapItem_detectBadlyGuarded(t *testing.T) { - txGasHandler := txcachemocks.NewTxGasHandlerMock() - - a := createTx([]byte("tx-1"), "alice", 42) - b := createTx([]byte("tx-7"), "bob", 43).withGuardian([]byte("heidi")) - - a.precomputeFields(txGasHandler) - b.precomputeFields(txGasHandler) - - t.Run("unknown", func(t *testing.T) { - item, err := newTransactionsHeapItem(bunchOfTransactions{a}) - require.NoError(t, err) - - require.False(t, item.detectBadlyGuarded()) - }) - - t.Run("transaction has no guardian, account has no guardian", func(t *testing.T) { - item, err := newTransactionsHeapItem(bunchOfTransactions{a}) - require.NoError(t, err) - - item.senderState = &types.AccountState{ - Guardian: nil, - } - - require.False(t, item.detectBadlyGuarded()) - }) - - t.Run("transaction has guardian, account has guardian, they match", func(t *testing.T) { - item, err := newTransactionsHeapItem(bunchOfTransactions{b}) - require.NoError(t, err) - - item.senderState = &types.AccountState{ - Guardian: []byte("heidi"), - } - - require.False(t, item.detectBadlyGuarded()) - }) - - t.Run("transaction has guardian, account has guardian, they don't match", func(t *testing.T) { - item, err := newTransactionsHeapItem(bunchOfTransactions{b}) - require.NoError(t, err) - - item.senderState = &types.AccountState{ - Guardian: []byte("grace"), - } - - require.True(t, item.detectBadlyGuarded()) - }) - - t.Run("transaction has guardian, account does not", func(t *testing.T) { - item, err := newTransactionsHeapItem(bunchOfTransactions{b}) - require.NoError(t, err) - - item.senderState = &types.AccountState{ - Guardian: nil, - } - - require.True(t, item.detectBadlyGuarded()) - }) - - t.Run("transaction has no guardian, account has guardian", func(t *testing.T) { - item, err := newTransactionsHeapItem(bunchOfTransactions{a}) - require.NoError(t, err) - - item.senderState = &types.AccountState{ - Guardian: []byte("heidi"), - } - - require.True(t, item.detectBadlyGuarded()) - }) -} - func TestTransactionsHeapItem_detectNonceDuplicate(t *testing.T) { a := createTx([]byte("tx-1"), "alice", 42) b := createTx([]byte("tx-2"), "alice", 43) diff --git a/txcache/wrappedTransaction.go b/txcache/wrappedTransaction.go index ad6104ed..aace7d97 100644 --- a/txcache/wrappedTransaction.go +++ b/txcache/wrappedTransaction.go @@ -23,7 +23,6 @@ type WrappedTransaction struct { // Additional note: "WrappedTransaction" objects are created by the Node, in dataRetriever/txpool/shardedTxPool.go. Fee *big.Int PricePerUnit uint64 - Guardian []byte } // precomputeFields computes (and caches) the (average) price per gas unit. @@ -34,11 +33,6 @@ func (wrappedTx *WrappedTransaction) precomputeFields(txGasHandler TxGasHandler) if gasLimit != 0 { wrappedTx.PricePerUnit = wrappedTx.Fee.Uint64() / gasLimit } - - txAsGuardedTransaction, ok := wrappedTx.Tx.(data.GuardedTransactionHandler) - if ok { - wrappedTx.Guardian = txAsGuardedTransaction.GetGuardianAddr() - } } // Equality is out of scope (not possible in our case). diff --git a/txcache/wrappedTransaction_test.go b/txcache/wrappedTransaction_test.go index fe8bc5e7..1b486b7e 100644 --- a/txcache/wrappedTransaction_test.go +++ b/txcache/wrappedTransaction_test.go @@ -16,19 +16,17 @@ func TestWrappedTransaction_precomputeFields(t *testing.T) { require.Equal(t, "51500000000000", tx.Fee.String()) require.Equal(t, oneBillion, int(tx.PricePerUnit)) - require.Empty(t, tx.Guardian) }) - t.Run("move balance gas limit and execution gas limit (1)", func(t *testing.T) { + t.Run("move balance gas limit and execution gas limit (a)", func(t *testing.T) { tx := createTx([]byte("b"), "b", 1).withDataLength(1).withGasLimit(51501).withGasPrice(oneBillion) tx.precomputeFields(txGasHandler) require.Equal(t, "51500010000000", tx.Fee.String()) require.Equal(t, 999_980_777, int(tx.PricePerUnit)) - require.Empty(t, tx.Guardian) }) - t.Run("move balance gas limit and execution gas limit (2)", func(t *testing.T) { + t.Run("move balance gas limit and execution gas limit (b)", func(t *testing.T) { tx := createTx([]byte("c"), "c", 1).withDataLength(1).withGasLimit(oneMilion).withGasPrice(oneBillion) tx.precomputeFields(txGasHandler) @@ -36,7 +34,6 @@ func TestWrappedTransaction_precomputeFields(t *testing.T) { require.Equal(t, "60985000000000", tx.Fee.String()) require.Equal(t, 60_985_000_000_000, actualFee) require.Equal(t, actualFee/oneMilion, int(tx.PricePerUnit)) - require.Empty(t, tx.Guardian) }) t.Run("with guardian", func(t *testing.T) { @@ -45,7 +42,6 @@ func TestWrappedTransaction_precomputeFields(t *testing.T) { require.Equal(t, "50000000000000", tx.Fee.String()) require.Equal(t, oneBillion, int(tx.PricePerUnit)) - require.Equal(t, []byte("heidi"), tx.Guardian) }) } diff --git a/types/accountState.go b/types/accountState.go index d424244b..13c3f326 100644 --- a/types/accountState.go +++ b/types/accountState.go @@ -4,7 +4,6 @@ import "math/big" // AccountState represents the state of an account, as seen by the mempool type AccountState struct { - Nonce uint64 - Balance *big.Int - Guardian []byte + Nonce uint64 + Balance *big.Int } From 8a0cb502901ff859795b491bc62d0d21bf814bdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Tue, 26 Nov 2024 22:02:05 +0200 Subject: [PATCH 34/34] Fix after review (part 2). --- txcache/README.md | 4 +- txcache/constants.go | 2 +- txcache/diagnosis.go | 2 +- txcache/selection.go | 12 ++- txcache/selection_test.go | 119 +++++++++++++++++++++++++++++ txcache/transactionsHeap.go | 4 +- txcache/transactionsHeapItem.go | 2 +- txcache/wrappedTransaction.go | 9 +++ txcache/wrappedTransaction_test.go | 14 +++- 9 files changed, 156 insertions(+), 12 deletions(-) diff --git a/txcache/README.md b/txcache/README.md index 186b4284..55d11f88 100644 --- a/txcache/README.md +++ b/txcache/README.md @@ -180,11 +180,11 @@ Thus, the mempool selects transactions using an efficient and value-driven algor - The number of selected transactions reaches `maxNum`. **Additional notes:** - - Within the selection loop, the current nonce of the sender is queryied from the blockchain, lazily (when needed). + - Within the selection loop, the current nonce of the sender is queried from the blockchain, lazily (when needed). - If an initial nonce gap is detected, the sender is (completely) skipped in the current selection session. - If a middle nonce gap is detected, the sender is skipped (from now on) in the current selection session. - Transactions with nonces lower than the current nonce of the sender are skipped. - - Transactions with duplicate nonces are skipped. See paragraph 5 for more details. + - Transactions having the same nonce as a previously selected one (in the scope of a sender) are skipped. Also see paragraph 5. - Badly guarded transactions are skipped. - Once the accumulated fees of selected transactions of a given sender exceed the sender's balance, the sender is skipped (from now one). diff --git a/txcache/constants.go b/txcache/constants.go index 811cd4b5..5bb61a52 100644 --- a/txcache/constants.go +++ b/txcache/constants.go @@ -3,4 +3,4 @@ package txcache const diagnosisMaxTransactionsToDisplay = 10000 const diagnosisSelectionGasRequested = 10_000_000_000 const initialCapacityOfSelectionSlice = 30000 -const selectionLoopDurationCheckInterval = 16 +const selectionLoopDurationCheckInterval = 10 diff --git a/txcache/diagnosis.go b/txcache/diagnosis.go index 6f693c97..df2a99fe 100644 --- a/txcache/diagnosis.go +++ b/txcache/diagnosis.go @@ -73,7 +73,7 @@ func (cache *TxCache) diagnoseTransactions() { } // marshalTransactionsToNewlineDelimitedJSON converts a list of transactions to a newline-delimited JSON string. -// Note: each line is indexed, to improve readability. The index is easily removable for if separate analysis is needed. +// Note: each line is indexed, to improve readability. The index is easily removable if separate analysis is needed. func marshalTransactionsToNewlineDelimitedJSON(transactions []*WrappedTransaction, linePrefix string) string { builder := strings.Builder{} builder.WriteString("\n") diff --git a/txcache/selection.go b/txcache/selection.go index a9c427f6..acdd1d36 100644 --- a/txcache/selection.go +++ b/txcache/selection.go @@ -6,6 +6,12 @@ import ( ) func (cache *TxCache) doSelectTransactions(accountStateProvider AccountStateProvider, gasRequested uint64, maxNum int, selectionLoopMaximumDuration time.Duration) (bunchOfTransactions, uint64) { + bunches := cache.acquireBunchesOfTransactions() + + return selectTransactionsFromBunches(accountStateProvider, bunches, gasRequested, maxNum, selectionLoopMaximumDuration) +} + +func (cache *TxCache) acquireBunchesOfTransactions() []bunchOfTransactions { senders := cache.getSenders() bunches := make([]bunchOfTransactions, 0, len(senders)) @@ -13,7 +19,7 @@ func (cache *TxCache) doSelectTransactions(accountStateProvider AccountStateProv bunches = append(bunches, sender.getTxs()) } - return selectTransactionsFromBunches(accountStateProvider, bunches, gasRequested, maxNum, selectionLoopMaximumDuration) + return bunches } // Selection tolerates concurrent transaction additions / removals. @@ -72,9 +78,7 @@ func selectTransactionsFromBunches(accountStateProvider AccountStateProvider, bu } shouldSkipTransaction := detectSkippableTransaction(item) - if shouldSkipTransaction { - // Transaction isn't selected, but the sender is still in the game (will contribute with other transactions). - } else { + if !shouldSkipTransaction { accumulatedGas += gasLimit selectedTransactions = append(selectedTransactions, item.selectCurrentTransaction()) } diff --git a/txcache/selection_test.go b/txcache/selection_test.go index 7684a566..f96b3a28 100644 --- a/txcache/selection_test.go +++ b/txcache/selection_test.go @@ -271,6 +271,108 @@ func TestTxCache_selectTransactionsFromBunches(t *testing.T) { }) } +func TestBenchmarkTxCache_acquireBunchesOfTransactions(t *testing.T) { + config := ConfigSourceMe{ + Name: "untitled", + NumChunks: 16, + NumBytesThreshold: 1000000000, + NumBytesPerSenderThreshold: maxNumBytesPerSenderUpperBound, + CountThreshold: 300001, + CountPerSenderThreshold: math.MaxUint32, + EvictionEnabled: false, + NumItemsToPreemptivelyEvict: 1, + } + + txGasHandler := txcachemocks.NewTxGasHandlerMock() + + sw := core.NewStopWatch() + + t.Run("numSenders = 10000, numTransactions = 100", func(t *testing.T) { + cache, err := NewTxCache(config, txGasHandler) + require.Nil(t, err) + + addManyTransactionsWithUniformDistribution(cache, 10000, 100) + + require.Equal(t, 1000000, int(cache.CountTx())) + + sw.Start(t.Name()) + bunches := cache.acquireBunchesOfTransactions() + sw.Stop(t.Name()) + + require.Len(t, bunches, 10000) + require.Len(t, bunches[0], 100) + require.Len(t, bunches[len(bunches)-1], 100) + }) + + t.Run("numSenders = 50000, numTransactions = 2", func(t *testing.T) { + cache, err := NewTxCache(config, txGasHandler) + require.Nil(t, err) + + addManyTransactionsWithUniformDistribution(cache, 50000, 2) + + require.Equal(t, 100000, int(cache.CountTx())) + + sw.Start(t.Name()) + bunches := cache.acquireBunchesOfTransactions() + sw.Stop(t.Name()) + + require.Len(t, bunches, 50000) + require.Len(t, bunches[0], 2) + require.Len(t, bunches[len(bunches)-1], 2) + }) + + t.Run("numSenders = 100000, numTransactions = 1", func(t *testing.T) { + cache, err := NewTxCache(config, txGasHandler) + require.Nil(t, err) + + addManyTransactionsWithUniformDistribution(cache, 100000, 1) + + require.Equal(t, 100000, int(cache.CountTx())) + + sw.Start(t.Name()) + bunches := cache.acquireBunchesOfTransactions() + sw.Stop(t.Name()) + + require.Len(t, bunches, 100000) + require.Len(t, bunches[0], 1) + require.Len(t, bunches[len(bunches)-1], 1) + }) + + t.Run("numSenders = 300000, numTransactions = 1", func(t *testing.T) { + cache, err := NewTxCache(config, txGasHandler) + require.Nil(t, err) + + addManyTransactionsWithUniformDistribution(cache, 300000, 1) + + require.Equal(t, 300000, int(cache.CountTx())) + + sw.Start(t.Name()) + bunches := cache.acquireBunchesOfTransactions() + sw.Stop(t.Name()) + + require.Len(t, bunches, 300000) + require.Len(t, bunches[0], 1) + require.Len(t, bunches[len(bunches)-1], 1) + }) + + for name, measurement := range sw.GetMeasurementsMap() { + fmt.Printf("%fs (%s)\n", measurement, name) + } + + // (1) + // Vendor ID: GenuineIntel + // Model name: 11th Gen Intel(R) Core(TM) i7-1165G7 @ 2.80GHz + // CPU family: 6 + // Model: 140 + // Thread(s) per core: 2 + // Core(s) per socket: 4 + // + // 0.014468s (TestBenchmarkTxCache_acquireBunchesOfTransactions/numSenders_=_10000,_numTransactions_=_100) + // 0.019183s (TestBenchmarkTxCache_acquireBunchesOfTransactions/numSenders_=_50000,_numTransactions_=_2) + // 0.013876s (TestBenchmarkTxCache_acquireBunchesOfTransactions/numSenders_=_100000,_numTransactions_=_1) + // 0.056631s (TestBenchmarkTxCache_acquireBunchesOfTransactions/numSenders_=_300000,_numTransactions_=_1) +} + func TestBenchmarkTxCache_selectTransactionsFromBunches(t *testing.T) { sw := core.NewStopWatch() @@ -368,6 +470,22 @@ func TestBenchmarkTxCache_doSelectTransactions(t *testing.T) { sw := core.NewStopWatch() + t.Run("numSenders = 10000, numTransactions = 100, maxNum = 50_000", func(t *testing.T) { + cache, err := NewTxCache(config, txGasHandler) + require.Nil(t, err) + + addManyTransactionsWithUniformDistribution(cache, 10000, 100) + + require.Equal(t, 1000000, int(cache.CountTx())) + + sw.Start(t.Name()) + selected, accumulatedGas := cache.SelectTransactions(accountStateProvider, 10_000_000_000, 50_000, selectionLoopMaximumDuration) + sw.Stop(t.Name()) + + require.Equal(t, 50000, len(selected)) + require.Equal(t, uint64(2_500_000_000), accumulatedGas) + }) + t.Run("numSenders = 50000, numTransactions = 2, maxNum = 50_000", func(t *testing.T) { cache, err := NewTxCache(config, txGasHandler) require.Nil(t, err) @@ -428,6 +546,7 @@ func TestBenchmarkTxCache_doSelectTransactions(t *testing.T) { // Thread(s) per core: 2 // Core(s) per socket: 4 // + // 0.126612s (TestBenchmarkTxCache_doSelectTransactions/numSenders_=_10000,_numTransactions_=_100,_maxNum_=_50_000) // 0.107361s (TestBenchmarkTxCache_doSelectTransactions/numSenders_=_50000,_numTransactions_=_2,_maxNum_=_50_000) // 0.168364s (TestBenchmarkTxCache_doSelectTransactions/numSenders_=_100000,_numTransactions_=_1,_maxNum_=_50_000) // 0.305363s (TestBenchmarkTxCache_doSelectTransactions/numSenders_=_300000,_numTransactions_=_1,_maxNum_=_50_000) diff --git a/txcache/transactionsHeap.go b/txcache/transactionsHeap.go index e96ebb54..28b4e072 100644 --- a/txcache/transactionsHeap.go +++ b/txcache/transactionsHeap.go @@ -11,7 +11,7 @@ func newMinTransactionsHeap(capacity int) *transactionsHeap { } h.less = func(i, j int) bool { - return h.items[j].holdsTransactionMoreValuableForNetwork(h.items[i]) + return h.items[j].isCurrentTransactionMoreValuableForNetwork(h.items[i]) } return &h @@ -23,7 +23,7 @@ func newMaxTransactionsHeap(capacity int) *transactionsHeap { } h.less = func(i, j int) bool { - return h.items[i].holdsTransactionMoreValuableForNetwork(h.items[j]) + return h.items[i].isCurrentTransactionMoreValuableForNetwork(h.items[j]) } return &h diff --git a/txcache/transactionsHeapItem.go b/txcache/transactionsHeapItem.go index b54b46ef..25191da6 100644 --- a/txcache/transactionsHeapItem.go +++ b/txcache/transactionsHeapItem.go @@ -193,6 +193,6 @@ func (item *transactionsHeapItem) requestAccountStateIfNecessary(accountStatePro return nil } -func (item *transactionsHeapItem) holdsTransactionMoreValuableForNetwork(other *transactionsHeapItem) bool { +func (item *transactionsHeapItem) isCurrentTransactionMoreValuableForNetwork(other *transactionsHeapItem) bool { return item.currentTransaction.isTransactionMoreValuableForNetwork(other.currentTransaction) } diff --git a/txcache/wrappedTransaction.go b/txcache/wrappedTransaction.go index aace7d97..6bcaf471 100644 --- a/txcache/wrappedTransaction.go +++ b/txcache/wrappedTransaction.go @@ -37,10 +37,19 @@ func (wrappedTx *WrappedTransaction) precomputeFields(txGasHandler TxGasHandler) // Equality is out of scope (not possible in our case). func (wrappedTx *WrappedTransaction) isTransactionMoreValuableForNetwork(otherTransaction *WrappedTransaction) bool { + // First, compare by PPU (higher PPU is better). if wrappedTx.PricePerUnit != otherTransaction.PricePerUnit { return wrappedTx.PricePerUnit > otherTransaction.PricePerUnit } + // If PPU is the same, compare by gas limit (higher gas limit is better, promoting less "execution fragmentation"). + gasLimit := wrappedTx.Tx.GetGasLimit() + gasLimitOther := otherTransaction.Tx.GetGasLimit() + + if gasLimit != gasLimitOther { + return gasLimit > gasLimitOther + } + // In the end, compare by transaction hash return bytes.Compare(wrappedTx.TxHash, otherTransaction.TxHash) < 0 } diff --git a/txcache/wrappedTransaction_test.go b/txcache/wrappedTransaction_test.go index 1b486b7e..8adb0b00 100644 --- a/txcache/wrappedTransaction_test.go +++ b/txcache/wrappedTransaction_test.go @@ -58,13 +58,25 @@ func TestWrappedTransaction_isTransactionMoreValuableForNetwork(t *testing.T) { require.True(t, a.isTransactionMoreValuableForNetwork(b)) }) - t.Run("decide by transaction hash (set them up to have the same PPU)", func(t *testing.T) { + t.Run("decide by gas limit (set them up to have the same PPU)", func(t *testing.T) { + a := createTx([]byte("a-7"), "a", 7).withDataLength(30).withGasLimit(95_000).withGasPrice(oneBillion) + a.precomputeFields(txGasHandler) + + b := createTx([]byte("b-7"), "b", 7).withDataLength(60).withGasLimit(140_000).withGasPrice(oneBillion) + b.precomputeFields(txGasHandler) + + require.Equal(t, a.PricePerUnit, b.PricePerUnit) + require.True(t, b.isTransactionMoreValuableForNetwork(a)) + }) + + t.Run("decide by transaction hash (set them up to have the same PPU and gas limit)", func(t *testing.T) { a := createTx([]byte("a-7"), "a", 7) a.precomputeFields(txGasHandler) b := createTx([]byte("b-7"), "b", 7) b.precomputeFields(txGasHandler) + require.Equal(t, a.PricePerUnit, b.PricePerUnit) require.True(t, a.isTransactionMoreValuableForNetwork(b)) }) }