From e2161391089e56381124a6ef80bb3568bd9f075a Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Thu, 28 Nov 2019 19:08:31 +0300 Subject: [PATCH 1/3] 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 +} From 127f8418c841485b62509db8bf3ec8d2d36eb116 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Thu, 28 Nov 2019 19:12:23 +0300 Subject: [PATCH 2/3] rpc: refactor out runScriptInVM() from invokers --- pkg/rpc/server.go | 34 ++++++++++------------------------ 1 file changed, 10 insertions(+), 24 deletions(-) diff --git a/pkg/rpc/server.go b/pkg/rpc/server.go index 34f2dba3a..25ba70e1a 100644 --- a/pkg/rpc/server.go +++ b/pkg/rpc/server.go @@ -353,16 +353,7 @@ func (s *Server) invoke(reqParams Params) (interface{}, error) { 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 + return s.runScriptInVM(script), nil } // invokescript implements the `invokescript` RPC call. @@ -379,16 +370,7 @@ func (s *Server) invokeFunction(reqParams Params) (interface{}, error) { 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 + return s.runScriptInVM(script), nil } // invokescript implements the `invokescript` RPC call. @@ -402,18 +384,22 @@ func (s *Server) invokescript(reqParams Params) (interface{}, error) { return nil, errInvalidParams } + return s.runScriptInVM(script), nil +} + +// runScriptInVM runs given script in a new test VM and returns the invocation +// result. +func (s *Server) runScriptInVM(script []byte) *wrappers.InvokeResult { vm, _ := s.chain.GetTestVM() vm.LoadScript(script) _ = vm.Run() - // It's already being GetBytesHex'ed, so it's a correct string. - echo, _ := reqParams[0].GetString() result := &wrappers.InvokeResult{ State: vm.State(), GasConsumed: "0.1", - Script: echo, + Script: hex.EncodeToString(script), Stack: vm.Estack(), } - return result, nil + return result } func (s *Server) sendrawtransaction(reqParams Params) (interface{}, error) { From b0a22cf1e7c9f1ba09a265c4860ef90e8a8c3db0 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Thu, 28 Nov 2019 19:40:13 +0300 Subject: [PATCH 3/3] cli/smartcontract: add testinvoke cli command That uses new 'invoke' RPC method. --- cli/smartcontract/smart_contract.go | 53 +++++++++++++++++++++++++---- 1 file changed, 46 insertions(+), 7 deletions(-) diff --git a/cli/smartcontract/smart_contract.go b/cli/smartcontract/smart_contract.go index accac7614..e3f8cd2b2 100644 --- a/cli/smartcontract/smart_contract.go +++ b/cli/smartcontract/smart_contract.go @@ -97,6 +97,29 @@ func NewCommands() []cli.Command { }, }, }, + { + Name: "testinvoke", + Usage: "invoke deployed contract on the blockchain (test mode)", + UsageText: "neo-go contract testinvoke -e endpoint scripthash [arguments...]", + Description: `Executes given (as a script hash) deployed script with the given arguments. + It's very similar to the tesinvokefunction command, but differs in the way + arguments are being passed. This invoker does not accept method parameter + and it passes all given parameters as plain values to the contract, not + wrapping them them into array like testinvokefunction does. For arguments + syntax please refer to the testinvokefunction command help. + + Most of the time (if your contract follows the standard convention of + method with array of values parameters) you want to use testinvokefunction + command instead of testinvoke. +`, + Action: testInvoke, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "endpoint, e", + Usage: "RPC endpoint address (like 'http://seed4.ngd.network:20332')", + }, + }, + }, { Name: "testinvokefunction", Usage: "invoke deployed contract on the blockchain (test mode)", @@ -287,7 +310,20 @@ func contractCompile(ctx *cli.Context) error { return nil } +func testInvoke(ctx *cli.Context) error { + return testInvokeInternal(ctx, false) +} + func testInvokeFunction(ctx *cli.Context) error { + return testInvokeInternal(ctx, true) +} + +func testInvokeInternal(ctx *cli.Context, withMethod bool) error { + var resp *rpc.InvokeScriptResponse + var operation string + var paramsStart = 1 + var params = make([]smartcontract.Parameter, 0) + endpoint := ctx.String("endpoint") if len(endpoint) == 0 { return cli.NewExitError(errNoEndpoint, 1) @@ -298,16 +334,15 @@ func testInvokeFunction(ctx *cli.Context) error { return cli.NewExitError(errNoScriptHash, 1) } script := args[0] - operation := "" - if len(args) > 1 { + if withMethod && len(args) > 1 { operation = args[1] + paramsStart++ } - params := make([]smartcontract.Parameter, 0) - if len(args) > 2 { - for k, s := range args[2:] { + if len(args) > paramsStart { + for k, s := range args[paramsStart:] { param, err := smartcontract.NewParameterFromString(s) if err != nil { - return cli.NewExitError(fmt.Errorf("failed to parse argument #%d: %v", k+2+1, err), 1) + return cli.NewExitError(fmt.Errorf("failed to parse argument #%d: %v", k+paramsStart+1, err), 1) } params = append(params, *param) } @@ -318,7 +353,11 @@ func testInvokeFunction(ctx *cli.Context) error { return cli.NewExitError(err, 1) } - resp, err := client.InvokeFunction(script, operation, params) + if withMethod { + resp, err = client.InvokeFunction(script, operation, params) + } else { + resp, err = client.Invoke(script, params) + } if err != nil { return cli.NewExitError(err, 1) }