diff --git a/process/smartContract/scQueryService.go b/process/smartContract/scQueryService.go index 3aeb879f384..b243a8db2b0 100644 --- a/process/smartContract/scQueryService.go +++ b/process/smartContract/scQueryService.go @@ -22,14 +22,18 @@ import ( "github.com/multiversx/mx-chain-go/process" "github.com/multiversx/mx-chain-go/process/smartContract/scrCommon" "github.com/multiversx/mx-chain-go/sharding" + logger "github.com/multiversx/mx-chain-logger-go" vmcommon "github.com/multiversx/mx-chain-vm-common-go" "github.com/multiversx/mx-chain-vm-common-go/parsers" ) var _ process.SCQueryService = (*SCQueryService)(nil) +var logQueryService = logger.GetOrCreate("process/smartcontract.queryService") + // MaxGasLimitPerQuery - each unit is the equivalent of 1 nanosecond processing time const MaxGasLimitPerQuery = 300_000_000_000 +const epochDifferenceToConsiderHistory = 2 // SCQueryService can execute Get functions over SC to fetch stored values type SCQueryService struct { @@ -39,7 +43,6 @@ type SCQueryService struct { blockChainHook process.BlockChainHookWithAccountsAdapter mainBlockChain data.ChainHandler apiBlockChain data.ChainHandler - numQueries int gasForQuery uint64 wasmVMChangeLocker common.Locker bootstrapper process.Bootstrapper @@ -179,8 +182,7 @@ func (service *SCQueryService) shouldAllowQueriesExecution() bool { } func (service *SCQueryService) executeScCall(query *process.SCQuery, gasPrice uint64) (*vmcommon.VMOutput, common.BlockInfo, error) { - log.Trace("executeScCall", "function", query.FuncName, "numQueries", service.numQueries) - service.numQueries++ + logQueryService.Trace("executeScCall", "address", query.ScAddress, "function", query.FuncName, "blockNonce", query.BlockNonce.Value, "blockHash", query.BlockHash) shouldEarlyExitBecauseOfSyncState := query.ShouldBeSynced && service.bootstrapper.GetNodeState() == common.NsNotSynchronized if shouldEarlyExitBecauseOfSyncState { @@ -198,10 +200,7 @@ func (service *SCQueryService) executeScCall(query *process.SCQuery, gasPrice ui return nil, nil, err } - accountsAdapter := service.blockChainHook.GetAccountsAdapter() - - holder := holders.NewRootHashHolder(blockRootHash, core.OptionalUint32{Value: blockHeader.GetEpoch(), HasValue: true}) - err = accountsAdapter.RecreateTrieFromEpoch(holder) + err = service.recreateTrie(blockRootHash, blockHeader) if err != nil { return nil, nil, err } @@ -230,15 +229,6 @@ func (service *SCQueryService) executeScCall(query *process.SCQuery, gasPrice ui return nil, nil, err } - if service.hasRetriableExecutionError(vmOutput) { - log.Error("Retriable execution error detected. Will retry (once) executeScCall()", "returnCode", vmOutput.ReturnCode, "returnMessage", vmOutput.ReturnMessage) - - vmOutput, err = vm.RunSmartContractCall(vmInput) - if err != nil { - return nil, nil, err - } - } - if query.SameScState { err = service.checkForRootHashChanges(rootHashBeforeExecution) if err != nil { @@ -259,6 +249,31 @@ func (service *SCQueryService) executeScCall(query *process.SCQuery, gasPrice ui return vmOutput, blockInfo, nil } +func (service *SCQueryService) recreateTrie(blockRootHash []byte, blockHeader data.HeaderHandler) error { + if check.IfNil(blockHeader) { + return process.ErrNilBlockHeader + } + + accountsAdapter := service.blockChainHook.GetAccountsAdapter() + if blockHeader.GetEpoch()+epochDifferenceToConsiderHistory >= service.getCurrentEpoch() { + logQueryService.Trace("calling RecreateTrie, for recent history", "block", blockHeader.GetNonce(), "rootHash", blockRootHash) + return accountsAdapter.RecreateTrie(blockRootHash) + } + + logQueryService.Trace("calling RecreateTrieFromEpoch, for older history", "block", blockHeader.GetNonce(), "rootHash", blockRootHash) + holder := holders.NewRootHashHolder(blockRootHash, core.OptionalUint32{Value: blockHeader.GetEpoch(), HasValue: true}) + return accountsAdapter.RecreateTrieFromEpoch(holder) +} + +func (service *SCQueryService) getCurrentEpoch() uint32 { + header := service.mainBlockChain.GetCurrentBlockHeader() + if check.IfNil(header) { + return 0 + } + + return header.GetEpoch() +} + // TODO: extract duplicated code with nodeBlocks.go func (service *SCQueryService) extractBlockHeaderAndRootHash(query *process.SCQuery) (data.HeaderHandler, []byte, error) { if len(query.BlockHash) > 0 { @@ -417,10 +432,6 @@ func (service *SCQueryService) createVMCallInput(query *process.SCQuery, gasPric return vmContractCallInput } -func (service *SCQueryService) hasRetriableExecutionError(vmOutput *vmcommon.VMOutput) bool { - return vmOutput.ReturnMessage == "allocation error" -} - // ComputeScCallGasLimit will estimate how many gas a transaction will consume func (service *SCQueryService) ComputeScCallGasLimit(tx *transaction.Transaction) (uint64, error) { argParser := parsers.NewCallArgsParser() diff --git a/process/smartContract/scQueryServiceDispatcher.go b/process/smartContract/scQueryServiceDispatcher.go index 2c51b47d55d..981f71f3dd9 100644 --- a/process/smartContract/scQueryServiceDispatcher.go +++ b/process/smartContract/scQueryServiceDispatcher.go @@ -78,7 +78,7 @@ func (sqsd *scQueryServiceDispatcher) Close() error { for _, scQueryService := range sqsd.list { err := scQueryService.Close() if err != nil { - log.Error("error while closing inner SC query service in scQueryServiceDispatcher.Close", "error", err) + logQueryService.Error("error while closing inner SC query service in scQueryServiceDispatcher.Close", "error", err) errFound = err } } diff --git a/process/smartContract/scQueryService_test.go b/process/smartContract/scQueryService_test.go index 69672531752..cd31bc165ec 100644 --- a/process/smartContract/scQueryService_test.go +++ b/process/smartContract/scQueryService_test.go @@ -367,7 +367,7 @@ func TestExecuteQuery_ShouldReceiveQueryCorrectly(t *testing.T) { _, _, _ = target.ExecuteQuery(&query) assert.True(t, runWasCalled) }) - t.Run("block hash should work", func(t *testing.T) { + t.Run("block hash should work - old epoch", func(t *testing.T) { t.Parallel() runWasCalled := false @@ -396,6 +396,13 @@ func TestExecuteQuery_ShouldReceiveQueryCorrectly(t *testing.T) { return uint64(math.MaxUint64) }, } + argsNewSCQuery.MainBlockChain = &testscommon.ChainHandlerStub{ + GetCurrentBlockHeaderCalled: func() data.HeaderHandler { + return &block.Header{ + Epoch: 37, + } + }, + } providedHash := []byte("provided hash") providedRootHash := []byte("provided root hash") argsNewSCQuery.Marshaller = &marshallerMock.MarshalizerMock{} @@ -425,10 +432,10 @@ func TestExecuteQuery_ShouldReceiveQueryCorrectly(t *testing.T) { return 12, nil }, } - wasRecreateTrieCalled := false + recreateTrieWasCalled := false providedAccountsAdapter := &stateMocks.AccountsStub{ RecreateTrieFromEpochCalled: func(options common.RootHashHolder) error { - wasRecreateTrieCalled = true + recreateTrieWasCalled = true assert.Equal(t, providedRootHash, options.GetRootHash()) return nil }, @@ -454,10 +461,10 @@ func TestExecuteQuery_ShouldReceiveQueryCorrectly(t *testing.T) { _, _, err := target.ExecuteQuery(&query) assert.True(t, runWasCalled) - assert.True(t, wasRecreateTrieCalled) + assert.True(t, recreateTrieWasCalled) assert.Nil(t, err) }) - t.Run("block nonce should work", func(t *testing.T) { + t.Run("block hash should work - current epoch", func(t *testing.T) { t.Parallel() runWasCalled := false @@ -488,6 +495,98 @@ func TestExecuteQuery_ShouldReceiveQueryCorrectly(t *testing.T) { } providedHash := []byte("provided hash") providedRootHash := []byte("provided root hash") + argsNewSCQuery.Marshaller = &marshallerMock.MarshalizerMock{} + argsNewSCQuery.StorageService = &storageStubs.ChainStorerStub{ + GetStorerCalled: func(unitType dataRetriever.UnitType) (storage.Storer, error) { + return &storageStubs.StorerStub{ + GetFromEpochCalled: func(key []byte, epoch uint32) ([]byte, error) { + hdr := &block.Header{ + RootHash: providedRootHash, + } + buff, _ := argsNewSCQuery.Marshaller.Marshal(hdr) + return buff, nil + }, + }, nil + }, + } + argsNewSCQuery.HistoryRepository = &dblookupext.HistoryRepositoryStub{ + IsEnabledCalled: func() bool { + return true + }, + GetEpochByHashCalled: func(hash []byte) (uint32, error) { + return 12, nil + }, + } + recreateTrieWasCalled := false + providedAccountsAdapter := &stateMocks.AccountsStub{ + RecreateTrieCalled: func(rootHash []byte) error { + recreateTrieWasCalled = true + assert.Equal(t, providedRootHash, rootHash) + return nil + }, + } + argsNewSCQuery.BlockChainHook = &testscommon.BlockChainHookStub{ + GetAccountsAdapterCalled: func() state.AccountsAdapter { + return providedAccountsAdapter + }, + } + + target, _ := NewSCQueryService(argsNewSCQuery) + + dataArgs := make([][]byte, len(args)) + for i, arg := range args { + dataArgs[i] = append(dataArgs[i], arg.Bytes()...) + } + query := process.SCQuery{ + ScAddress: scAddress, + FuncName: funcName, + Arguments: dataArgs, + BlockHash: providedHash, + } + + _, _, err := target.ExecuteQuery(&query) + assert.True(t, runWasCalled) + assert.True(t, recreateTrieWasCalled) + assert.Nil(t, err) + }) + t.Run("block nonce should work - old epoch", func(t *testing.T) { + t.Parallel() + + runWasCalled := false + + mockVM := &mock.VMExecutionHandlerStub{ + RunSmartContractCallCalled: func(input *vmcommon.ContractCallInput) (output *vmcommon.VMOutput, e error) { + runWasCalled = true + assert.Equal(t, int64(42), big.NewInt(0).SetBytes(input.Arguments[0]).Int64()) + assert.Equal(t, int64(43), big.NewInt(0).SetBytes(input.Arguments[1]).Int64()) + assert.Equal(t, scAddress, input.CallerAddr) + assert.Equal(t, funcName, input.Function) + + return &vmcommon.VMOutput{ + ReturnCode: vmcommon.Ok, + }, nil + }, + } + argsNewSCQuery := createMockArgumentsForSCQuery() + argsNewSCQuery.MainBlockChain = &testscommon.ChainHandlerStub{ + GetCurrentBlockHeaderCalled: func() data.HeaderHandler { + return &block.Header{ + Epoch: 37, + } + }, + } + argsNewSCQuery.VmContainer = &mock.VMContainerMock{ + GetCalled: func(key []byte) (handler vmcommon.VMExecutionHandler, e error) { + return mockVM, nil + }, + } + argsNewSCQuery.EconomicsFee = &economicsmocks.EconomicsHandlerStub{ + MaxGasLimitPerBlockCalled: func(_ uint32) uint64 { + return uint64(math.MaxUint64) + }, + } + providedHash := []byte("provided hash") + providedRootHash := []byte("provided root hash") providedNonce := uint64(123) argsNewSCQuery.Marshaller = &marshallerMock.MarshalizerMock{} counter := 0 @@ -520,10 +619,10 @@ func TestExecuteQuery_ShouldReceiveQueryCorrectly(t *testing.T) { return 12, nil }, } - wasRecreateTrieCalled := false + recreateTrieWasCalled := false providedAccountsAdapter := &stateMocks.AccountsStub{ RecreateTrieFromEpochCalled: func(options common.RootHashHolder) error { - wasRecreateTrieCalled = true + recreateTrieWasCalled = true assert.Equal(t, providedRootHash, options.GetRootHash()) return nil }, @@ -552,7 +651,273 @@ func TestExecuteQuery_ShouldReceiveQueryCorrectly(t *testing.T) { _, _, _ = target.ExecuteQuery(&query) assert.True(t, runWasCalled) - assert.True(t, wasRecreateTrieCalled) + assert.True(t, recreateTrieWasCalled) + }) + t.Run("block nonce should work - current epoch", func(t *testing.T) { + t.Parallel() + + runWasCalled := false + + mockVM := &mock.VMExecutionHandlerStub{ + RunSmartContractCallCalled: func(input *vmcommon.ContractCallInput) (output *vmcommon.VMOutput, e error) { + runWasCalled = true + assert.Equal(t, int64(42), big.NewInt(0).SetBytes(input.Arguments[0]).Int64()) + assert.Equal(t, int64(43), big.NewInt(0).SetBytes(input.Arguments[1]).Int64()) + assert.Equal(t, scAddress, input.CallerAddr) + assert.Equal(t, funcName, input.Function) + + return &vmcommon.VMOutput{ + ReturnCode: vmcommon.Ok, + }, nil + }, + } + argsNewSCQuery := createMockArgumentsForSCQuery() + argsNewSCQuery.VmContainer = &mock.VMContainerMock{ + GetCalled: func(key []byte) (handler vmcommon.VMExecutionHandler, e error) { + return mockVM, nil + }, + } + argsNewSCQuery.EconomicsFee = &economicsmocks.EconomicsHandlerStub{ + MaxGasLimitPerBlockCalled: func(_ uint32) uint64 { + return uint64(math.MaxUint64) + }, + } + providedHash := []byte("provided hash") + providedRootHash := []byte("provided root hash") + providedNonce := uint64(123) + argsNewSCQuery.Marshaller = &marshallerMock.MarshalizerMock{} + argsNewSCQuery.StorageService = &storageStubs.ChainStorerStub{ + GetStorerCalled: func(unitType dataRetriever.UnitType) (storage.Storer, error) { + return &storageStubs.StorerStub{ + GetCalled: func(key []byte) ([]byte, error) { + return providedHash, nil + }, + GetFromEpochCalled: func(key []byte, epoch uint32) ([]byte, error) { + hdr := &block.Header{ + RootHash: providedRootHash, + } + buff, _ := argsNewSCQuery.Marshaller.Marshal(hdr) + return buff, nil + }, + }, nil + }, + } + argsNewSCQuery.HistoryRepository = &dblookupext.HistoryRepositoryStub{ + IsEnabledCalled: func() bool { + return true + }, + GetEpochByHashCalled: func(hash []byte) (uint32, error) { + require.Equal(t, providedHash, hash) + return 12, nil + }, + } + recreateTrieWasCalled := false + providedAccountsAdapter := &stateMocks.AccountsStub{ + RecreateTrieCalled: func(rootHash []byte) error { + recreateTrieWasCalled = true + assert.Equal(t, providedRootHash, rootHash) + return nil + }, + } + argsNewSCQuery.BlockChainHook = &testscommon.BlockChainHookStub{ + GetAccountsAdapterCalled: func() state.AccountsAdapter { + return providedAccountsAdapter + }, + } + + target, _ := NewSCQueryService(argsNewSCQuery) + + dataArgs := make([][]byte, len(args)) + for i, arg := range args { + dataArgs[i] = append(dataArgs[i], arg.Bytes()...) + } + query := process.SCQuery{ + ScAddress: scAddress, + FuncName: funcName, + Arguments: dataArgs, + BlockNonce: core.OptionalUint64{ + Value: providedNonce, + HasValue: true, + }, + } + + _, _, _ = target.ExecuteQuery(&query) + assert.True(t, runWasCalled) + assert.True(t, recreateTrieWasCalled) + }) +} + +func TestSCQueryService_RecreateTrie(t *testing.T) { + t.Parallel() + + testRootHash := []byte("test root hash") + t.Run("should not call RecreateTrie if block header is nil", func(t *testing.T) { + t.Parallel() + + argsNewSCQuery := createMockArgumentsForSCQuery() + argsNewSCQuery.BlockChainHook = &testscommon.BlockChainHookStub{ + GetAccountsAdapterCalled: func() state.AccountsAdapter { + return &stateMocks.AccountsStub{ + RecreateTrieCalled: func(rootHash []byte) error { + require.Fail(t, "should not be called") + return nil + }, + } + }, + } + + service, _ := NewSCQueryService(argsNewSCQuery) + err := service.recreateTrie(testRootHash, nil) + assert.ErrorIs(t, err, process.ErrNilBlockHeader) + }) + t.Run("should call RecreateTrie for genesis block", func(t *testing.T) { + t.Parallel() + + recreateTrieWasCalled := false + argsNewSCQuery := createMockArgumentsForSCQuery() + argsNewSCQuery.MainBlockChain = &testscommon.ChainHandlerStub{ + GetCurrentBlockHeaderCalled: func() data.HeaderHandler { + return nil // after the genesis we do not have a header as current block + }, + } + argsNewSCQuery.BlockChainHook = &testscommon.BlockChainHookStub{ + GetAccountsAdapterCalled: func() state.AccountsAdapter { + return &stateMocks.AccountsStub{ + RecreateTrieCalled: func(rootHash []byte) error { + recreateTrieWasCalled = true + assert.Equal(t, testRootHash, rootHash) + return nil + }, + } + }, + } + + service, _ := NewSCQueryService(argsNewSCQuery) + err := service.recreateTrie(testRootHash, &block.Header{}) + assert.Nil(t, err) + assert.True(t, recreateTrieWasCalled) + }) + t.Run("should call RecreateTrie for block on epoch 0", func(t *testing.T) { + t.Parallel() + + recreateTrieWasCalled := false + argsNewSCQuery := createMockArgumentsForSCQuery() + argsNewSCQuery.MainBlockChain = &testscommon.ChainHandlerStub{ + GetCurrentBlockHeaderCalled: func() data.HeaderHandler { + return &block.Header{ + Epoch: 0, + } + }, + } + argsNewSCQuery.BlockChainHook = &testscommon.BlockChainHookStub{ + GetAccountsAdapterCalled: func() state.AccountsAdapter { + return &stateMocks.AccountsStub{ + RecreateTrieCalled: func(rootHash []byte) error { + recreateTrieWasCalled = true + assert.Equal(t, testRootHash, rootHash) + return nil + }, + } + }, + } + + service, _ := NewSCQueryService(argsNewSCQuery) + err := service.recreateTrie(testRootHash, &block.Header{}) + assert.Nil(t, err) + assert.True(t, recreateTrieWasCalled) + }) + t.Run("should call RecreateTrie for block on epoch 1", func(t *testing.T) { + t.Parallel() + + recreateTrieWasCalled := false + argsNewSCQuery := createMockArgumentsForSCQuery() + argsNewSCQuery.MainBlockChain = &testscommon.ChainHandlerStub{ + GetCurrentBlockHeaderCalled: func() data.HeaderHandler { + return &block.Header{ + Epoch: 1, + } + }, + } + argsNewSCQuery.BlockChainHook = &testscommon.BlockChainHookStub{ + GetAccountsAdapterCalled: func() state.AccountsAdapter { + return &stateMocks.AccountsStub{ + RecreateTrieCalled: func(rootHash []byte) error { + recreateTrieWasCalled = true + assert.Equal(t, testRootHash, rootHash) + return nil + }, + } + }, + } + + service, _ := NewSCQueryService(argsNewSCQuery) + err := service.recreateTrie(testRootHash, &block.Header{ + Epoch: 0, + }) + assert.Nil(t, err) + assert.True(t, recreateTrieWasCalled) + }) + t.Run("should call RecreateTrie for block on epoch 2", func(t *testing.T) { + t.Parallel() + + recreateTrieWasCalled := false + argsNewSCQuery := createMockArgumentsForSCQuery() + argsNewSCQuery.MainBlockChain = &testscommon.ChainHandlerStub{ + GetCurrentBlockHeaderCalled: func() data.HeaderHandler { + return &block.Header{ + Epoch: 3, + } + }, + } + argsNewSCQuery.BlockChainHook = &testscommon.BlockChainHookStub{ + GetAccountsAdapterCalled: func() state.AccountsAdapter { + return &stateMocks.AccountsStub{ + RecreateTrieCalled: func(rootHash []byte) error { + recreateTrieWasCalled = true + assert.Equal(t, testRootHash, rootHash) + return nil + }, + } + }, + } + + service, _ := NewSCQueryService(argsNewSCQuery) + err := service.recreateTrie(testRootHash, &block.Header{ + Epoch: 2, + }) + assert.Nil(t, err) + assert.True(t, recreateTrieWasCalled) + }) + t.Run("should call RecreateTrieFromEpoch for block on epoch 3", func(t *testing.T) { + t.Parallel() + + recreateTrieWasCalled := false + argsNewSCQuery := createMockArgumentsForSCQuery() + argsNewSCQuery.MainBlockChain = &testscommon.ChainHandlerStub{ + GetCurrentBlockHeaderCalled: func() data.HeaderHandler { + return &block.Header{ + Epoch: 3, + } + }, + } + argsNewSCQuery.BlockChainHook = &testscommon.BlockChainHookStub{ + GetAccountsAdapterCalled: func() state.AccountsAdapter { + return &stateMocks.AccountsStub{ + RecreateTrieFromEpochCalled: func(options common.RootHashHolder) error { + recreateTrieWasCalled = true + assert.Equal(t, testRootHash, options.GetRootHash()) + return nil + }, + } + }, + } + + service, _ := NewSCQueryService(argsNewSCQuery) + err := service.recreateTrie(testRootHash, &block.Header{ + Epoch: 0, + }) + assert.Nil(t, err) + assert.True(t, recreateTrieWasCalled) }) } diff --git a/state/accountsDBApi.go b/state/accountsDBApi.go index 4dbc7581a74..86daccf660c 100644 --- a/state/accountsDBApi.go +++ b/state/accountsDBApi.go @@ -175,13 +175,22 @@ func (accountsDB *accountsDBApi) RecreateTrieFromEpoch(options common.RootHashHo accountsDB.mutRecreatedTrieBlockInfo.Lock() defer accountsDB.mutRecreatedTrieBlockInfo.Unlock() + if check.IfNil(options) { + return ErrNilRootHashHolder + } + + newBlockInfo := holders.NewBlockInfo([]byte{}, 0, options.GetRootHash()) + if newBlockInfo.Equal(accountsDB.blockInfo) { + return nil + } + err := accountsDB.innerAccountsAdapter.RecreateTrieFromEpoch(options) if err != nil { accountsDB.blockInfo = nil return err } - accountsDB.blockInfo = holders.NewBlockInfo([]byte{}, 0, options.GetRootHash()) + accountsDB.blockInfo = newBlockInfo return nil } diff --git a/state/accountsDBApi_test.go b/state/accountsDBApi_test.go index b9b48dafe92..0d9aea1c098 100644 --- a/state/accountsDBApi_test.go +++ b/state/accountsDBApi_test.go @@ -230,17 +230,16 @@ func TestAccountsDBApi_RecreateTrieFromEpoch(t *testing.T) { t.Parallel() t.Run("should error if the roothash holder is nil", func(t *testing.T) { - wasCalled := false accountsApi, _ := state.NewAccountsDBApi(&mockState.AccountsStub{ RecreateTrieFromEpochCalled: func(options common.RootHashHolder) error { - wasCalled = true - return trie.ErrNilRootHashHolder + assert.Fail(t, "should have not called accountsApi.RecreateTrieFromEpochCalled") + + return nil }, }, createBlockInfoProviderStub(dummyRootHash)) err := accountsApi.RecreateTrieFromEpoch(nil) assert.Equal(t, trie.ErrNilRootHashHolder, err) - assert.True(t, wasCalled) }) t.Run("should work", func(t *testing.T) { wasCalled := false diff --git a/state/errors.go b/state/errors.go index fd8c0057241..efe422d2061 100644 --- a/state/errors.go +++ b/state/errors.go @@ -145,6 +145,9 @@ var ErrNilStateMetrics = errors.New("nil sstate metrics") // ErrNilChannelsProvider signals that a nil channels provider has been given var ErrNilChannelsProvider = errors.New("nil channels provider") +// ErrNilRootHashHolder signals that a nil root hash holder was provided +var ErrNilRootHashHolder = errors.New("nil root hash holder provided") + // ErrNilStatsHandler signals that a nil stats handler provider has been given var ErrNilStatsHandler = errors.New("nil stats handler")