diff --git a/go.mod b/go.mod index c1d46238d5f..0a330e9cef2 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/multiversx/mx-chain-es-indexer-go v1.7.10 github.com/multiversx/mx-chain-logger-go v1.0.15 github.com/multiversx/mx-chain-scenario-go v1.4.4 - github.com/multiversx/mx-chain-storage-go v1.0.18-0.20241128102604-90581ee84b60 + github.com/multiversx/mx-chain-storage-go v1.0.18-0.20241128110709-156b1244e04f github.com/multiversx/mx-chain-vm-common-go v1.5.16 github.com/multiversx/mx-chain-vm-go v1.5.37 github.com/multiversx/mx-chain-vm-v1_2-go v1.2.68 diff --git a/go.sum b/go.sum index aeb3451d8aa..5b6066d2161 100644 --- a/go.sum +++ b/go.sum @@ -397,8 +397,8 @@ github.com/multiversx/mx-chain-logger-go v1.0.15 h1:HlNdK8etyJyL9NQ+6mIXyKPEBo+w github.com/multiversx/mx-chain-logger-go v1.0.15/go.mod h1:t3PRKaWB1M+i6gUfD27KXgzLJJC+mAQiN+FLlL1yoGQ= github.com/multiversx/mx-chain-scenario-go v1.4.4 h1:DVE2V+FPeyD/yWoC+KEfPK3jsFzHeruelESfpTlf460= github.com/multiversx/mx-chain-scenario-go v1.4.4/go.mod h1:kI+TWR3oIEgUkbwkHCPo2CQ3VjIge+ezGTibiSGwMxo= -github.com/multiversx/mx-chain-storage-go v1.0.18-0.20241128102604-90581ee84b60 h1:XFO7MbjpdSJE/mC8wv3937iIWoAQC9YL2vwzO0bvmMg= -github.com/multiversx/mx-chain-storage-go v1.0.18-0.20241128102604-90581ee84b60/go.mod h1:eFDEOrG7Wiyk5I/ObpwcN2eoBlOnnfeEMTvTer1cymk= +github.com/multiversx/mx-chain-storage-go v1.0.18-0.20241128110709-156b1244e04f h1:sRPekt5fzNr+c7w2IzwufOeqANTT3Du6ciD3FX5mCvI= +github.com/multiversx/mx-chain-storage-go v1.0.18-0.20241128110709-156b1244e04f/go.mod h1:eFDEOrG7Wiyk5I/ObpwcN2eoBlOnnfeEMTvTer1cymk= github.com/multiversx/mx-chain-vm-common-go v1.5.16 h1:g1SqYjxl7K66Y1O/q6tvDJ37fzpzlxCSfRzSm/woQQY= github.com/multiversx/mx-chain-vm-common-go v1.5.16/go.mod h1:1rSkXreUZNXyPTTdhj47M+Fy62yjxbu3aAsXEtKN3UY= github.com/multiversx/mx-chain-vm-go v1.5.37 h1:Iy3KCvM+DOq1f9UPA7uYK/rI3ZbBOXc2CVNO2/vm5zw= diff --git a/integrationTests/singleShard/block/executingMiniblocks/executingMiniblocks_test.go b/integrationTests/singleShard/block/executingMiniblocks/executingMiniblocks_test.go index 8d6c00af8ae..2c7bb0f7a7c 100644 --- a/integrationTests/singleShard/block/executingMiniblocks/executingMiniblocks_test.go +++ b/integrationTests/singleShard/block/executingMiniblocks/executingMiniblocks_test.go @@ -82,12 +82,11 @@ func TestShardShouldNotProposeAndExecuteTwoBlocksInSameRound(t *testing.T) { } // TestShardShouldProposeBlockContainingInvalidTransactions tests the following scenario: -// 1. generate 3 move balance transactions: one that can be executed, one that can not be executed but the account has -// the balance for the fee and one that is completely invalid (no balance left for it) +// 1. generate 3 move balance transactions: one that can be executed, one to be processed as invalid, and one that isn't executable (no balance left for fee). // 2. proposer will have those 3 transactions in its pools and will propose a block // 3. another node will be able to sync the proposed block (and request - receive) the 2 transactions that // will end up in the block (one valid and one invalid) -// 4. the non-executable transaction will be removed from the proposer's pool +// 4. the non-executable transaction will not be immediately removed from the proposer's pool. See MX-16200. func TestShardShouldProposeBlockContainingInvalidTransactions(t *testing.T) { if testing.Short() { t.Skip("this is not a short test") @@ -195,7 +194,18 @@ func testStateOnNodes(t *testing.T, nodes []*integrationTests.TestProcessorNode, testTxIsInMiniblock(t, proposer, hashes[txValidIdx], block.TxBlock) testTxIsInMiniblock(t, proposer, hashes[txInvalidIdx], block.InvalidBlock) testTxIsInNotInBody(t, proposer, hashes[txDeletedIdx]) - testTxHashNotPresentInPool(t, proposer, hashes[txDeletedIdx]) + + // Removed from mempool. + _, ok := proposer.DataPool.Transactions().SearchFirstData(hashes[txValidIdx]) + assert.False(t, ok) + + // Removed from mempool. + _, ok = proposer.DataPool.Transactions().SearchFirstData(hashes[txInvalidIdx]) + assert.False(t, ok) + + // Not removed from mempool (see MX-16200). + _, ok = proposer.DataPool.Transactions().SearchFirstData(hashes[txDeletedIdx]) + assert.True(t, ok) } func testSameBlockHeight(t *testing.T, nodes []*integrationTests.TestProcessorNode, idxProposer int, expectedHeight uint64) { @@ -208,11 +218,6 @@ func testSameBlockHeight(t *testing.T, nodes []*integrationTests.TestProcessorNo } } -func testTxHashNotPresentInPool(t *testing.T, proposer *integrationTests.TestProcessorNode, hash []byte) { - txCache := proposer.DataPool.Transactions() - _, ok := txCache.SearchFirstData(hash) - assert.False(t, ok) -} func testTxIsInMiniblock(t *testing.T, proposer *integrationTests.TestProcessorNode, hash []byte, bt block.Type) { hdrHandler := proposer.BlockChain.GetCurrentBlockHeader() diff --git a/process/block/preprocess/selectionSession.go b/process/block/preprocess/selectionSession.go index 4476e94204d..6d0f8a6d397 100644 --- a/process/block/preprocess/selectionSession.go +++ b/process/block/preprocess/selectionSession.go @@ -1,32 +1,58 @@ package preprocess import ( + "bytes" "errors" + "math/big" + "github.com/multiversx/mx-chain-core-go/core" "github.com/multiversx/mx-chain-core-go/core/check" "github.com/multiversx/mx-chain-core-go/data" "github.com/multiversx/mx-chain-core-go/data/transaction" + "github.com/multiversx/mx-chain-core-go/marshal" "github.com/multiversx/mx-chain-go/process" "github.com/multiversx/mx-chain-go/state" "github.com/multiversx/mx-chain-go/storage/txcache" + vmcommon "github.com/multiversx/mx-chain-vm-common-go" + "github.com/multiversx/mx-chain-vm-common-go/parsers" ) type selectionSession struct { accountsAdapter state.AccountsAdapter transactionsProcessor process.TransactionProcessor + callArgumentsParser process.CallArgumentsParser + esdtTransferParser vmcommon.ESDTTransferParser } -func newSelectionSession(accountsAdapter state.AccountsAdapter, transactionsProcessor process.TransactionProcessor) (*selectionSession, error) { - if check.IfNil(accountsAdapter) { +type argsSelectionSession struct { + accountsAdapter state.AccountsAdapter + transactionsProcessor process.TransactionProcessor + marshalizer marshal.Marshalizer +} + +func newSelectionSession(args argsSelectionSession) (*selectionSession, error) { + if check.IfNil(args.accountsAdapter) { return nil, process.ErrNilAccountsAdapter } - if check.IfNil(transactionsProcessor) { + if check.IfNil(args.transactionsProcessor) { return nil, process.ErrNilTxProcessor } + if check.IfNil(args.marshalizer) { + return nil, process.ErrNilMarshalizer + } + + argsParser := parsers.NewCallArgsParser() + + esdtTransferParser, err := parsers.NewESDTTransferParser(args.marshalizer) + if err != nil { + return nil, err + } return &selectionSession{ - accountsAdapter: accountsAdapter, - transactionsProcessor: transactionsProcessor, + accountsAdapter: args.accountsAdapter, + transactionsProcessor: args.transactionsProcessor, + callArgumentsParser: argsParser, + esdtTransferParser: esdtTransferParser, }, nil } @@ -73,6 +99,60 @@ func (session *selectionSession) IsIncorrectlyGuarded(tx data.TransactionHandler return errors.Is(err, process.ErrTransactionNotExecutable) } +// GetTransferredValue returns the value transferred by a transaction. +func (session *selectionSession) GetTransferredValue(tx data.TransactionHandler) *big.Int { + value := tx.GetValue() + hasValue := value != nil && value.Sign() != 0 + if hasValue { + // Early exit (optimization): a transaction can either bear a regular value or be a "MultiESDTNFTTransfer". + return value + } + + data := tx.GetData() + hasData := len(data) > 0 + if !hasData { + // Early exit (optimization): no "MultiESDTNFTTransfer" to parse. + return tx.GetValue() + } + + maybeMultiTransfer := bytes.HasPrefix(data, []byte(core.BuiltInFunctionMultiESDTNFTTransfer)) + if !maybeMultiTransfer { + // Early exit (optimization). + return nil + } + + function, args, err := session.callArgumentsParser.ParseData(string(data)) + if err != nil { + return nil + } + + if function != core.BuiltInFunctionMultiESDTNFTTransfer { + // Early exit (optimization). + return nil + } + + esdtTransfers, err := session.esdtTransferParser.ParseESDTTransfers(tx.GetSndAddr(), tx.GetRcvAddr(), function, args) + if err != nil { + return nil + } + + accumulatedNativeValue := big.NewInt(0) + + for _, transfer := range esdtTransfers.ESDTTransfers { + if transfer.ESDTTokenNonce != 0 { + continue + } + if string(transfer.ESDTTokenName) != vmcommon.EGLDIdentifier { + // We only care about native transfers. + continue + } + + _ = accumulatedNativeValue.Add(accumulatedNativeValue, transfer.ESDTValue) + } + + return accumulatedNativeValue +} + // IsInterfaceNil returns true if there is no value under the interface func (session *selectionSession) IsInterfaceNil() bool { return session == nil diff --git a/process/block/preprocess/selectionSession_test.go b/process/block/preprocess/selectionSession_test.go index 3affcb922f9..9f69a994f0c 100644 --- a/process/block/preprocess/selectionSession_test.go +++ b/process/block/preprocess/selectionSession_test.go @@ -2,10 +2,14 @@ package preprocess import ( "bytes" + "encoding/hex" "fmt" + "math/big" "testing" + "github.com/multiversx/mx-chain-core-go/core" "github.com/multiversx/mx-chain-core-go/data/transaction" + "github.com/multiversx/mx-chain-core-go/marshal" "github.com/multiversx/mx-chain-go/process" "github.com/multiversx/mx-chain-go/state" "github.com/multiversx/mx-chain-go/testscommon" @@ -17,15 +21,27 @@ import ( func TestNewSelectionSession(t *testing.T) { t.Parallel() - session, err := newSelectionSession(nil, &testscommon.TxProcessorStub{}) + session, err := newSelectionSession(argsSelectionSession{ + accountsAdapter: nil, + transactionsProcessor: &testscommon.TxProcessorStub{}, + marshalizer: &marshal.GogoProtoMarshalizer{}, + }) require.Nil(t, session) require.ErrorIs(t, err, process.ErrNilAccountsAdapter) - session, err = newSelectionSession(&stateMock.AccountsStub{}, nil) + session, err = newSelectionSession(argsSelectionSession{ + accountsAdapter: &stateMock.AccountsStub{}, + transactionsProcessor: nil, + marshalizer: &marshal.GogoProtoMarshalizer{}, + }) require.Nil(t, session) require.ErrorIs(t, err, process.ErrNilTxProcessor) - session, err = newSelectionSession(&stateMock.AccountsStub{}, &testscommon.TxProcessorStub{}) + session, err = newSelectionSession(argsSelectionSession{ + accountsAdapter: &stateMock.AccountsStub{}, + transactionsProcessor: &testscommon.TxProcessorStub{}, + marshalizer: &marshal.GogoProtoMarshalizer{}, + }) require.NoError(t, err) require.NotNil(t, session) } @@ -57,7 +73,11 @@ func TestSelectionSession_GetAccountState(t *testing.T) { return nil, fmt.Errorf("account not found: %s", address) } - session, err := newSelectionSession(accounts, processor) + session, err := newSelectionSession(argsSelectionSession{ + accountsAdapter: accounts, + transactionsProcessor: processor, + marshalizer: &marshal.GogoProtoMarshalizer{}, + }) require.NoError(t, err) require.NotNil(t, session) @@ -99,7 +119,11 @@ func TestSelectionSession_IsIncorrectlyGuarded(t *testing.T) { return nil } - session, err := newSelectionSession(accounts, processor) + session, err := newSelectionSession(argsSelectionSession{ + accountsAdapter: accounts, + transactionsProcessor: processor, + marshalizer: &marshal.GogoProtoMarshalizer{}, + }) require.NoError(t, err) require.NotNil(t, session) @@ -115,3 +139,146 @@ func TestSelectionSession_IsIncorrectlyGuarded(t *testing.T) { isIncorrectlyGuarded = session.IsIncorrectlyGuarded(&transaction.Transaction{Nonce: 45, SndAddr: []byte("bob")}) require.True(t, isIncorrectlyGuarded) } + +func TestSelectionSession_GetTransferredValue(t *testing.T) { + t.Parallel() + + session, err := newSelectionSession(argsSelectionSession{ + accountsAdapter: &stateMock.AccountsStub{}, + transactionsProcessor: &testscommon.TxProcessorStub{}, + marshalizer: &marshal.GogoProtoMarshalizer{}, + }) + require.NoError(t, err) + require.NotNil(t, session) + + t.Run("with value", func(t *testing.T) { + value := session.GetTransferredValue(&transaction.Transaction{ + Value: big.NewInt(1000000000000000000), + }) + require.Equal(t, big.NewInt(1000000000000000000), value) + }) + + t.Run("with value and data", func(t *testing.T) { + value := session.GetTransferredValue(&transaction.Transaction{ + Value: big.NewInt(1000000000000000000), + Data: []byte("data"), + }) + require.Equal(t, big.NewInt(1000000000000000000), value) + }) + + t.Run("native transfer within MultiESDTNFTTransfer", func(t *testing.T) { + value := session.GetTransferredValue(&transaction.Transaction{ + SndAddr: testscommon.TestPubKeyAlice, + RcvAddr: testscommon.TestPubKeyAlice, + Data: []byte("MultiESDTNFTTransfer@8049d639e5a6980d1cd2392abcce41029cda74a1563523a202f09641cc2618f8@03@4e46542d313233343536@0a@01@544553542d393837363534@01@01@45474c442d303030303030@@0de0b6b3a7640000"), + }) + require.Equal(t, big.NewInt(1000000000000000000), value) + }) + + t.Run("native transfer within MultiESDTNFTTransfer; transfer & execute", func(t *testing.T) { + value := session.GetTransferredValue(&transaction.Transaction{ + SndAddr: testscommon.TestPubKeyAlice, + RcvAddr: testscommon.TestPubKeyAlice, + Data: []byte("MultiESDTNFTTransfer@00000000000000000500b9353fe8407f87310c87e12fa1ac807f0485da39d152@03@4e46542d313233343536@01@01@4e46542d313233343536@2a@01@45474c442d303030303030@@0de0b6b3a7640000@64756d6d79@07"), + }) + require.Equal(t, big.NewInt(1000000000000000000), value) + }) +} + +func TestBenchmarkSelectionSession_GetTransferredValue(t *testing.T) { + session, err := newSelectionSession(argsSelectionSession{ + accountsAdapter: &stateMock.AccountsStub{}, + transactionsProcessor: &testscommon.TxProcessorStub{}, + marshalizer: &marshal.GogoProtoMarshalizer{}, + }) + require.NoError(t, err) + require.NotNil(t, session) + + sw := core.NewStopWatch() + + valueMultiplier := int64(1_000_000_000_000) + + t.Run("numTransactions = 5_000", func(t *testing.T) { + numTransactions := 5_000 + transactions := createMultiESDTNFTTransfersWithNativeTransfer(numTransactions, valueMultiplier) + + sw.Start(t.Name()) + + for i := 0; i < numTransactions; i++ { + tx := transactions[i] + value := session.GetTransferredValue(tx) + require.Equal(t, big.NewInt(int64(i)*valueMultiplier), value) + } + + sw.Stop(t.Name()) + }) + + t.Run("numTransactions = 10_000", func(t *testing.T) { + numTransactions := 10_000 + transactions := createMultiESDTNFTTransfersWithNativeTransfer(numTransactions, valueMultiplier) + + sw.Start(t.Name()) + + for i := 0; i < numTransactions; i++ { + tx := transactions[i] + value := session.GetTransferredValue(tx) + require.Equal(t, big.NewInt(int64(i)*valueMultiplier), value) + } + + sw.Stop(t.Name()) + }) + + t.Run("numTransactions = 20_000", func(t *testing.T) { + numTransactions := 20_000 + transactions := createMultiESDTNFTTransfersWithNativeTransfer(numTransactions, valueMultiplier) + + sw.Start(t.Name()) + + for i := 0; i < numTransactions; i++ { + tx := transactions[i] + value := session.GetTransferredValue(tx) + require.Equal(t, big.NewInt(int64(i)*valueMultiplier), value) + } + + sw.Stop(t.Name()) + }) + + 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 + // + // NOTE: 20% is also due to the require() / assert() calls. + // 0.012993s (TestBenchmarkSelectionSession_GetTransferredValue/numTransactions_=_5_000) + // 0.024580s (TestBenchmarkSelectionSession_GetTransferredValue/numTransactions_=_10_000) + // 0.048808s (TestBenchmarkSelectionSession_GetTransferredValue/numTransactions_=_20_000) +} + +func createMultiESDTNFTTransfersWithNativeTransfer(numTransactions int, valueMultiplier int64) []*transaction.Transaction { + transactions := make([]*transaction.Transaction, 0, numTransactions) + + for i := 0; i < numTransactions; i++ { + nativeValue := big.NewInt(int64(i) * valueMultiplier) + data := fmt.Sprintf( + "MultiESDTNFTTransfer@8049d639e5a6980d1cd2392abcce41029cda74a1563523a202f09641cc2618f8@03@4e46542d313233343536@0a@01@544553542d393837363534@01@01@45474c442d303030303030@@%s", + hex.EncodeToString(nativeValue.Bytes()), + ) + + tx := &transaction.Transaction{ + SndAddr: testscommon.TestPubKeyAlice, + RcvAddr: testscommon.TestPubKeyAlice, + Data: []byte(data), + } + + transactions = append(transactions, tx) + } + + return transactions +} diff --git a/process/block/preprocess/transactions.go b/process/block/preprocess/transactions.go index 5379ed046a7..3ebf1528ac9 100644 --- a/process/block/preprocess/transactions.go +++ b/process/block/preprocess/transactions.go @@ -1411,7 +1411,11 @@ func (txs *transactions) computeSortedTxs( sortedTransactionsProvider := createSortedTransactionsProvider(txShardPool) log.Debug("computeSortedTxs.GetSortedTransactions") - session, err := newSelectionSession(txs.basePreProcess.accounts, txs.txProcessor) + session, err := newSelectionSession(argsSelectionSession{ + accountsAdapter: txs.accounts, + transactionsProcessor: txs.txProcessor, + marshalizer: txs.marshalizer, + }) if err != nil { return nil, nil, err }