diff --git a/pkg/rpc/client/nep5.go b/pkg/rpc/client/nep5.go index 1fc5fa89b..dd3d39fca 100644 --- a/pkg/rpc/client/nep5.go +++ b/pkg/rpc/client/nep5.go @@ -7,6 +7,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/encoding/address" "github.com/nspcc-dev/neo-go/pkg/io" + "github.com/nspcc-dev/neo-go/pkg/rpc/response/result" "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm/emit" @@ -34,8 +35,10 @@ func (c *Client) NEP5Decimals(tokenHash util.Uint160) (int64, error) { result, err := c.InvokeFunction(tokenHash, "decimals", []smartcontract.Parameter{}, nil) if err != nil { return 0, err - } else if result.State != "HALT" || len(result.Stack) == 0 { - return 0, errors.New("invalid VM state") + } + err = getInvocationError(result) + if err != nil { + return 0, fmt.Errorf("failed to get NEP5 decimals: %w", err) } return topIntFromStack(result.Stack) @@ -46,8 +49,10 @@ func (c *Client) NEP5Name(tokenHash util.Uint160) (string, error) { result, err := c.InvokeFunction(tokenHash, "name", []smartcontract.Parameter{}, nil) if err != nil { return "", err - } else if result.State != "HALT" || len(result.Stack) == 0 { - return "", errors.New("invalid VM state") + } + err = getInvocationError(result) + if err != nil { + return "", fmt.Errorf("failed to get NEP5 name: %w", err) } return topStringFromStack(result.Stack) @@ -58,8 +63,10 @@ func (c *Client) NEP5Symbol(tokenHash util.Uint160) (string, error) { result, err := c.InvokeFunction(tokenHash, "symbol", []smartcontract.Parameter{}, nil) if err != nil { return "", err - } else if result.State != "HALT" || len(result.Stack) == 0 { - return "", errors.New("invalid VM state") + } + err = getInvocationError(result) + if err != nil { + return "", fmt.Errorf("failed to get NEP5 symbol: %w", err) } return topStringFromStack(result.Stack) @@ -70,8 +77,10 @@ func (c *Client) NEP5TotalSupply(tokenHash util.Uint160) (int64, error) { result, err := c.InvokeFunction(tokenHash, "totalSupply", []smartcontract.Parameter{}, nil) if err != nil { return 0, err - } else if result.State != "HALT" || len(result.Stack) == 0 { - return 0, errors.New("invalid VM state") + } + err = getInvocationError(result) + if err != nil { + return 0, fmt.Errorf("failed to get NEP5 total supply: %w", err) } return topIntFromStack(result.Stack) @@ -85,8 +94,10 @@ func (c *Client) NEP5BalanceOf(tokenHash, acc util.Uint160) (int64, error) { }}, nil) if err != nil { return 0, err - } else if result.State != "HALT" || len(result.Stack) == 0 { - return 0, errors.New("invalid VM state") + } + err = getInvocationError(result) + if err != nil { + return 0, fmt.Errorf("failed to get NEP5 balance: %w", err) } return topIntFromStack(result.Stack) @@ -156,7 +167,7 @@ func (c *Client) CreateTxFromScript(script []byte, acc *wallet.Account, sysFee, return nil, fmt.Errorf("can't add system fee to transaction: %w", err) } if result.State != "HALT" { - return nil, fmt.Errorf("can't add system fee to transaction: bad vm state: %s", result.State) + return nil, fmt.Errorf("can't add system fee to transaction: bad vm state: %s due to an error: %s", result.State, result.FaultException) } sysFee = result.GasConsumed } @@ -225,3 +236,14 @@ func topStringFromStack(st []stackitem.Item) (string, error) { } return string(bs), nil } + +// getInvocationError returns an error in case of bad VM state or empty stack. +func getInvocationError(result *result.Invoke) error { + if result.State != "HALT" { + return fmt.Errorf("invocation failed: %s", result.FaultException) + } + if len(result.Stack) == 0 { + return errors.New("result stack is empty") + } + return nil +} diff --git a/pkg/rpc/client/policy.go b/pkg/rpc/client/policy.go index cb27263a6..7e6aa59bf 100644 --- a/pkg/rpc/client/policy.go +++ b/pkg/rpc/client/policy.go @@ -1,7 +1,6 @@ package client import ( - "errors" "fmt" "github.com/nspcc-dev/neo-go/pkg/core/native" @@ -33,8 +32,10 @@ func (c *Client) invokeNativePolicyMethod(operation string) (int64, error) { result, err := c.InvokeFunction(PolicyContractHash, operation, []smartcontract.Parameter{}, nil) if err != nil { return 0, err - } else if result.State != "HALT" || len(result.Stack) == 0 { - return 0, errors.New("invalid VM state") + } + err = getInvocationError(result) + if err != nil { + return 0, fmt.Errorf("failed to invoke %s Policy method: %w", operation, err) } return topIntFromStack(result.Stack) @@ -45,10 +46,11 @@ func (c *Client) GetBlockedAccounts() (native.BlockedAccounts, error) { result, err := c.InvokeFunction(PolicyContractHash, "getBlockedAccounts", []smartcontract.Parameter{}, nil) if err != nil { return nil, err - } else if result.State != "HALT" || len(result.Stack) == 0 { - return nil, errors.New("invalid VM state") } - + err = getInvocationError(result) + if err != nil { + return nil, fmt.Errorf("failed to get blocked accounts: %w", err) + } return topBlockedAccountsFromStack(result.Stack) } diff --git a/pkg/rpc/client/rpc.go b/pkg/rpc/client/rpc.go index fb6175c54..1f4761a01 100644 --- a/pkg/rpc/client/rpc.go +++ b/pkg/rpc/client/rpc.go @@ -540,12 +540,17 @@ func (c *Client) AddNetworkFee(tx *transaction.Transaction, extraFee int64, accs for i, cosigner := range tx.Signers { if accs[i].Contract.Deployed { res, err := c.InvokeFunction(cosigner.Account, manifest.MethodVerify, []smartcontract.Parameter{}, tx.Signers) - if err == nil && res.State == "HALT" && len(res.Stack) == 1 { - r, err := topIntFromStack(res.Stack) - if err != nil || r == 0 { - return core.ErrVerificationFailed - } - } else { + if err != nil { + return fmt.Errorf("failed to invoke verify: %w", err) + } + if res.State != "HALT" { + return fmt.Errorf("invalid VM state %s due to an error: %s", res.State, res.FaultException) + } + if l := len(res.Stack); l != 1 { + return fmt.Errorf("result stack length should be equal to 1, got %d", l) + } + r, err := topIntFromStack(res.Stack) + if err != nil || r == 0 { return core.ErrVerificationFailed } tx.NetworkFee += res.GasConsumed diff --git a/pkg/rpc/client/rpc_test.go b/pkg/rpc/client/rpc_test.go index 47fbc1673..01b907994 100644 --- a/pkg/rpc/client/rpc_test.go +++ b/pkg/rpc/client/rpc_test.go @@ -677,6 +677,41 @@ var rpcClientTestCases = map[string][]rpcClientTestCase{ } }, }, + { + name: "positive, FAULT state", + invoke: func(c *Client) (interface{}, error) { + hash, err := util.Uint160DecodeStringLE("91b83e96f2a7c4fdf0c1688441ec61986c7cae26") + if err != nil { + panic(err) + } + contr, err := util.Uint160DecodeStringLE("af7c7328eee5a275a3bcaee2bf0cf662b5e739be") + if err != nil { + panic(err) + } + return c.InvokeFunction(contr, "balanceOf", []smartcontract.Parameter{ + { + Type: smartcontract.Hash160Type, + Value: hash, + }, + }, []transaction.Signer{{ + Account: util.Uint160{1, 2, 3}, + }}) + }, + serverResponse: `{"jsonrpc":"2.0","id":1,"result":{"script":"1426ae7c6c9861ec418468c1f0fdc4a7f2963eb89151c10962616c616e63654f6667be39e7b562f60cbfe2aebca375a2e5ee28737caf","state":"FAULT","gasconsumed":"31100000","stack":[{"type":"ByteString","value":"JivsCEQy"}],"tx":"d101361426ae7c6c9861ec418468c1f0fdc4a7f2963eb89151c10962616c616e63654f6667be39e7b562f60cbfe2aebca375a2e5ee28737caf000000000000000000000000","exception":"gas limit exceeded"}}`, + result: func(c *Client) interface{} { + bytes, err := hex.DecodeString("262bec084432") + if err != nil { + panic(err) + } + return &result.Invoke{ + State: "FAULT", + GasConsumed: 31100000, + Script: "1426ae7c6c9861ec418468c1f0fdc4a7f2963eb89151c10962616c616e63654f6667be39e7b562f60cbfe2aebca375a2e5ee28737caf", + Stack: []stackitem.Item{stackitem.NewByteArray(bytes)}, + FaultException: "gas limit exceeded", + } + }, + }, }, "invokescript": { { diff --git a/pkg/rpc/response/result/invoke.go b/pkg/rpc/response/result/invoke.go index 55fbf9ba9..17439ed95 100644 --- a/pkg/rpc/response/result/invoke.go +++ b/pkg/rpc/response/result/invoke.go @@ -9,17 +9,19 @@ import ( // Invoke represents code invocation result and is used by several RPC calls // that invoke functions, scripts and generic bytecode. type Invoke struct { - State string - GasConsumed int64 - Script string - Stack []stackitem.Item + State string + GasConsumed int64 + Script string + Stack []stackitem.Item + FaultException string } type invokeAux struct { - State string `json:"state"` - GasConsumed int64 `json:"gasconsumed,string"` - Script string `json:"script"` - Stack json.RawMessage `json:"stack"` + State string `json:"state"` + GasConsumed int64 `json:"gasconsumed,string"` + Script string `json:"script"` + Stack json.RawMessage `json:"stack"` + FaultException string `json:"exception,omitempty"` } // MarshalJSON implements json.Marshaler. @@ -43,10 +45,11 @@ func (r Invoke) MarshalJSON() ([]byte, error) { } } return json.Marshal(&invokeAux{ - GasConsumed: r.GasConsumed, - Script: r.Script, - State: r.State, - Stack: st, + GasConsumed: r.GasConsumed, + Script: r.Script, + State: r.State, + Stack: st, + FaultException: r.FaultException, }) } @@ -72,5 +75,6 @@ func (r *Invoke) UnmarshalJSON(data []byte) error { r.GasConsumed = aux.GasConsumed r.Script = aux.Script r.State = aux.State + r.FaultException = aux.FaultException return nil } diff --git a/pkg/rpc/server/server.go b/pkg/rpc/server/server.go index 9db856dda..3a56d2ec8 100644 --- a/pkg/rpc/server/server.go +++ b/pkg/rpc/server/server.go @@ -712,8 +712,14 @@ func (s *Server) getDecimals(contractID int32, cache map[int32]decimals) (decima return decimals{}, fmt.Errorf("can't create script: %w", err) } res := s.runScriptInVM(script, nil) - if res == nil || res.State != "HALT" || len(res.Stack) == 0 { - return decimals{}, errors.New("execution error : no result") + if res == nil { + return decimals{}, fmt.Errorf("execution error: no result") + } + if res.State != "HALT" { + return decimals{}, fmt.Errorf("execution error: bad VM state %s due to an error %s", res.State, res.FaultException) + } + if len(res.Stack) == 0 { + return decimals{}, fmt.Errorf("execution error: empty stack") } d := decimals{Hash: h} @@ -997,12 +1003,17 @@ func (s *Server) runScriptInVM(script []byte, tx *transaction.Transaction) *resu vm := s.chain.GetTestVM(tx) vm.GasLimit = int64(s.config.MaxGasInvoke) vm.LoadScriptWithFlags(script, smartcontract.All) - _ = vm.Run() + err := vm.Run() + var faultException string + if err != nil { + faultException = err.Error() + } result := &result.Invoke{ - State: vm.State().String(), - GasConsumed: vm.GasConsumed(), - Script: hex.EncodeToString(script), - Stack: vm.Estack().ToArray(), + State: vm.State().String(), + GasConsumed: vm.GasConsumed(), + Script: hex.EncodeToString(script), + Stack: vm.Estack().ToArray(), + FaultException: faultException, } return result }