From e2161391089e56381124a6ef80bb3568bd9f075a Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Thu, 28 Nov 2019 19:08:31 +0300 Subject: [PATCH] rpc: implement server-side 'invoke' method, fix #346 --- docs/rpc.md | 16 ++-- pkg/rpc/server.go | 37 +++++++++ pkg/rpc/server_test.go | 39 +++++++++ pkg/rpc/txBuilder.go | 180 +++++++++++++++++++++++------------------ 4 files changed, 188 insertions(+), 84 deletions(-) diff --git a/docs/rpc.md b/docs/rpc.md index 4c65245e8..ffbaa3c4d 100644 --- a/docs/rpc.md +++ b/docs/rpc.md @@ -54,7 +54,7 @@ which would yield the response: | `gettxout` | No (#345) | | `getunspents` | Yes | | `getversion` | Yes | -| `invoke` | No (#346) | +| `invoke` | Yes | | `invokefunction` | Yes | | `invokescript` | Yes | | `sendrawtransaction` | Yes | @@ -63,13 +63,15 @@ which would yield the response: #### Implementation notices -##### `invokefunction` +##### `invokefunction` and `invoke` -neo-go's implementation of `invokefunction` does not return `tx` field in the -answer because that requires signing the transaction with some key in the -server which doesn't fit the model of our node-client interactions. Lacking -this signature the transaction is almost useless, so there is no point in -returning it. +neo-go's implementation of `invokefunction` and `invoke` does not return `tx` +field in the answer because that requires signing the transaction with some +key in the server which doesn't fit the model of our node-client interactions. +Lacking this signature the transaction is almost useless, so there is no point +in returning it. + +Both methods also don't currently support arrays in function parameters. ## Reference diff --git a/pkg/rpc/server.go b/pkg/rpc/server.go index df89546b3..34f2dba3a 100644 --- a/pkg/rpc/server.go +++ b/pkg/rpc/server.go @@ -243,6 +243,9 @@ Methods: getunspentsCalled.Inc() results, resultsErr = s.getAccountState(reqParams, true) + case "invoke": + results, resultsErr = s.invoke(reqParams) + case "invokefunction": results, resultsErr = s.invokeFunction(reqParams) @@ -328,6 +331,40 @@ func (s *Server) getAccountState(reqParams Params, unspents bool) (interface{}, return results, resultsErr } +// invoke implements the `invoke` RPC call. +func (s *Server) invoke(reqParams Params) (interface{}, error) { + scriptHashHex, ok := reqParams.ValueWithType(0, stringT) + if !ok { + return nil, errInvalidParams + } + scriptHash, err := scriptHashHex.GetUint160FromHex() + if err != nil { + return nil, err + } + sliceP, ok := reqParams.ValueWithType(1, arrayT) + if !ok { + return nil, errInvalidParams + } + slice, err := sliceP.GetArray() + if err != nil { + return nil, err + } + script, err := CreateInvocationScript(scriptHash, slice) + if err != nil { + return nil, err + } + vm, _ := s.chain.GetTestVM() + vm.LoadScript(script) + _ = vm.Run() + result := &wrappers.InvokeResult{ + State: vm.State(), + GasConsumed: "0.1", + Script: hex.EncodeToString(script), + Stack: vm.Estack(), + } + return result, nil +} + // invokescript implements the `invokescript` RPC call. func (s *Server) invokeFunction(reqParams Params) (interface{}, error) { scriptHashHex, ok := reqParams.ValueWithType(0, stringT) diff --git a/pkg/rpc/server_test.go b/pkg/rpc/server_test.go index c1860dd72..c64e567d8 100644 --- a/pkg/rpc/server_test.go +++ b/pkg/rpc/server_test.go @@ -251,6 +251,45 @@ var rpcTestCases = map[string][]rpcTestCase{ }, }, }, + "invoke": { + { + name: "positive", + params: `["50befd26fdf6e4d957c11e078b24ebce6291456f", [{"type": "String", "value": "qwerty"}]]`, + result: func(e *executor) interface{} { return &InvokeFunctionResponse{} }, + check: func(t *testing.T, e *executor, result interface{}) { + res, ok := result.(*InvokeFunctionResponse) + require.True(t, ok) + assert.Equal(t, "06717765727479676f459162ceeb248b071ec157d9e4f6fd26fdbe50", res.Result.Script) + assert.NotEqual(t, "", res.Result.State) + assert.NotEqual(t, 0, res.Result.GasConsumed) + }, + }, + { + name: "no params", + params: `[]`, + fail: true, + }, + { + name: "not a string", + params: `[42, []]`, + fail: true, + }, + { + name: "not a scripthash", + params: `["qwerty", []]`, + fail: true, + }, + { + name: "not an array", + params: `["50befd26fdf6e4d957c11e078b24ebce6291456f", 42]`, + fail: true, + }, + { + name: "bad params", + params: `["50befd26fdf6e4d957c11e078b24ebce6291456f", [{"type": "Integer", "value": "qwerty"}]]`, + fail: true, + }, + }, "invokefunction": { { name: "positive", diff --git a/pkg/rpc/txBuilder.go b/pkg/rpc/txBuilder.go index 5ad22d481..d9519d5fc 100644 --- a/pkg/rpc/txBuilder.go +++ b/pkg/rpc/txBuilder.go @@ -166,6 +166,90 @@ func CreateDeploymentScript(avm []byte, contract *ContractDetails) ([]byte, erro return script.Bytes(), nil } +// expandArrayIntoScript pushes all FuncParam parameters from the given array +// into the given buffer in reverse order. +func expandArrayIntoScript(script *bytes.Buffer, slice []Param) error { + for j := len(slice) - 1; j >= 0; j-- { + fp, err := slice[j].GetFuncParam() + if err != nil { + return err + } + switch fp.Type { + case ByteArray, Signature: + str, err := fp.Value.GetBytesHex() + if err != nil { + return err + } + if err := vm.EmitBytes(script, str); err != nil { + return err + } + case String: + str, err := fp.Value.GetString() + if err != nil { + return err + } + if err := vm.EmitString(script, str); err != nil { + return err + } + case Hash160: + hash, err := fp.Value.GetUint160FromHex() + if err != nil { + return err + } + if err := vm.EmitBytes(script, hash.Bytes()); err != nil { + return err + } + case Hash256: + hash, err := fp.Value.GetUint256() + if err != nil { + return err + } + if err := vm.EmitBytes(script, hash.Bytes()); err != nil { + return err + } + case PublicKey: + str, err := fp.Value.GetString() + if err != nil { + return err + } + key, err := keys.NewPublicKeyFromString(string(str)) + if err != nil { + return err + } + if err := vm.EmitBytes(script, key.Bytes()); err != nil { + return err + } + case Integer: + val, err := fp.Value.GetInt() + if err != nil { + return err + } + if err := vm.EmitInt(script, int64(val)); err != nil { + return err + } + case Boolean: + str, err := fp.Value.GetString() + if err != nil { + return err + } + switch str { + case "true": + err = vm.EmitInt(script, 1) + case "false": + err = vm.EmitInt(script, 0) + default: + err = errors.New("wrong boolean value") + } + if err != nil { + return err + } + default: + return fmt.Errorf("parameter type %v is not supported", fp.Type) + } + } + return nil +} + // CreateFunctionInvocationScript creates a script to invoke given contract with // given parameters. func CreateFunctionInvocationScript(contract util.Uint160, params Params) ([]byte, error) { @@ -189,83 +273,9 @@ func CreateFunctionInvocationScript(contract util.Uint160, params Params) ([]byt if err != nil { return nil, err } - for j := len(slice) - 1; j >= 0; j-- { - fp, err := slice[j].GetFuncParam() - if err != nil { - return nil, err - } - switch fp.Type { - case ByteArray, Signature: - str, err := fp.Value.GetBytesHex() - if err != nil { - return nil, err - } - if err := vm.EmitBytes(script, str); err != nil { - return nil, err - } - case String: - str, err := fp.Value.GetString() - if err != nil { - return nil, err - } - if err := vm.EmitString(script, str); err != nil { - return nil, err - } - case Hash160: - hash, err := fp.Value.GetUint160FromHex() - if err != nil { - return nil, err - } - if err := vm.EmitBytes(script, hash.Bytes()); err != nil { - return nil, err - } - case Hash256: - hash, err := fp.Value.GetUint256() - if err != nil { - return nil, err - } - if err := vm.EmitBytes(script, hash.Bytes()); err != nil { - return nil, err - } - case PublicKey: - str, err := fp.Value.GetString() - if err != nil { - return nil, err - } - key, err := keys.NewPublicKeyFromString(string(str)) - if err != nil { - return nil, err - } - if err := vm.EmitBytes(script, key.Bytes()); err != nil { - return nil, err - } - case Integer: - val, err := fp.Value.GetInt() - if err != nil { - return nil, err - } - if err := vm.EmitInt(script, int64(val)); err != nil { - return nil, err - } - case Boolean: - str, err := fp.Value.GetString() - if err != nil { - return nil, err - } - switch str { - case "true": - err = vm.EmitInt(script, 1) - case "false": - err = vm.EmitInt(script, 0) - default: - err = errors.New("wrong boolean value") - } - if err != nil { - return nil, err - } - default: - return nil, fmt.Errorf("parameter type %v is not supported", fp.Type) - } + err = expandArrayIntoScript(script, slice) + if err != nil { + return nil, err } err = vm.EmitInt(script, int64(len(slice))) if err != nil { @@ -283,3 +293,19 @@ func CreateFunctionInvocationScript(contract util.Uint160, params Params) ([]byt } return script.Bytes(), nil } + +// CreateInvocationScript creates a script to invoke given contract with +// given parameters. It differs from CreateFunctionInvocationScript in that it +// expects one array of FuncParams and expands it onto the stack as independent +// elements. +func CreateInvocationScript(contract util.Uint160, funcParams []Param) ([]byte, error) { + script := new(bytes.Buffer) + err := expandArrayIntoScript(script, funcParams) + if err != nil { + return nil, err + } + if err = vm.EmitAppCall(script, contract, false); err != nil { + return nil, err + } + return script.Bytes(), nil +}