Merge pull request #527 from nspcc-dev/invoke-tx

Invoke RPC
This commit is contained in:
Roman Khimov 2019-11-29 15:43:26 +03:00 committed by GitHub
commit efdcacca81
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 234 additions and 105 deletions

View file

@ -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", Name: "testinvokefunction",
Usage: "invoke deployed contract on the blockchain (test mode)", Usage: "invoke deployed contract on the blockchain (test mode)",
@ -287,7 +310,20 @@ func contractCompile(ctx *cli.Context) error {
return nil return nil
} }
func testInvoke(ctx *cli.Context) error {
return testInvokeInternal(ctx, false)
}
func testInvokeFunction(ctx *cli.Context) error { 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") endpoint := ctx.String("endpoint")
if len(endpoint) == 0 { if len(endpoint) == 0 {
return cli.NewExitError(errNoEndpoint, 1) return cli.NewExitError(errNoEndpoint, 1)
@ -298,16 +334,15 @@ func testInvokeFunction(ctx *cli.Context) error {
return cli.NewExitError(errNoScriptHash, 1) return cli.NewExitError(errNoScriptHash, 1)
} }
script := args[0] script := args[0]
operation := "" if withMethod && len(args) > 1 {
if len(args) > 1 {
operation = args[1] operation = args[1]
paramsStart++
} }
params := make([]smartcontract.Parameter, 0) if len(args) > paramsStart {
if len(args) > 2 { for k, s := range args[paramsStart:] {
for k, s := range args[2:] {
param, err := smartcontract.NewParameterFromString(s) param, err := smartcontract.NewParameterFromString(s)
if err != nil { 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) params = append(params, *param)
} }
@ -318,7 +353,11 @@ func testInvokeFunction(ctx *cli.Context) error {
return cli.NewExitError(err, 1) 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 { if err != nil {
return cli.NewExitError(err, 1) return cli.NewExitError(err, 1)
} }

View file

@ -54,7 +54,7 @@ which would yield the response:
| `gettxout` | No (#345) | | `gettxout` | No (#345) |
| `getunspents` | Yes | | `getunspents` | Yes |
| `getversion` | Yes | | `getversion` | Yes |
| `invoke` | No (#346) | | `invoke` | Yes |
| `invokefunction` | Yes | | `invokefunction` | Yes |
| `invokescript` | Yes | | `invokescript` | Yes |
| `sendrawtransaction` | Yes | | `sendrawtransaction` | Yes |
@ -63,13 +63,15 @@ which would yield the response:
#### Implementation notices #### Implementation notices
##### `invokefunction` ##### `invokefunction` and `invoke`
neo-go's implementation of `invokefunction` does not return `tx` field in the neo-go's implementation of `invokefunction` and `invoke` does not return `tx`
answer because that requires signing the transaction with some key in the field in the answer because that requires signing the transaction with some
server which doesn't fit the model of our node-client interactions. Lacking key in the server which doesn't fit the model of our node-client interactions.
this signature the transaction is almost useless, so there is no point in Lacking this signature the transaction is almost useless, so there is no point
returning it. in returning it.
Both methods also don't currently support arrays in function parameters.
## Reference ## Reference

View file

@ -243,6 +243,9 @@ Methods:
getunspentsCalled.Inc() getunspentsCalled.Inc()
results, resultsErr = s.getAccountState(reqParams, true) results, resultsErr = s.getAccountState(reqParams, true)
case "invoke":
results, resultsErr = s.invoke(reqParams)
case "invokefunction": case "invokefunction":
results, resultsErr = s.invokeFunction(reqParams) results, resultsErr = s.invokeFunction(reqParams)
@ -328,6 +331,31 @@ func (s *Server) getAccountState(reqParams Params, unspents bool) (interface{},
return results, resultsErr 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
}
return s.runScriptInVM(script), nil
}
// invokescript implements the `invokescript` RPC call. // invokescript implements the `invokescript` RPC call.
func (s *Server) invokeFunction(reqParams Params) (interface{}, error) { func (s *Server) invokeFunction(reqParams Params) (interface{}, error) {
scriptHashHex, ok := reqParams.ValueWithType(0, stringT) scriptHashHex, ok := reqParams.ValueWithType(0, stringT)
@ -342,16 +370,7 @@ func (s *Server) invokeFunction(reqParams Params) (interface{}, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
vm, _ := s.chain.GetTestVM() return s.runScriptInVM(script), nil
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. // invokescript implements the `invokescript` RPC call.
@ -365,18 +384,22 @@ func (s *Server) invokescript(reqParams Params) (interface{}, error) {
return nil, errInvalidParams 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, _ := s.chain.GetTestVM()
vm.LoadScript(script) vm.LoadScript(script)
_ = vm.Run() _ = vm.Run()
// It's already being GetBytesHex'ed, so it's a correct string.
echo, _ := reqParams[0].GetString()
result := &wrappers.InvokeResult{ result := &wrappers.InvokeResult{
State: vm.State(), State: vm.State(),
GasConsumed: "0.1", GasConsumed: "0.1",
Script: echo, Script: hex.EncodeToString(script),
Stack: vm.Estack(), Stack: vm.Estack(),
} }
return result, nil return result
} }
func (s *Server) sendrawtransaction(reqParams Params) (interface{}, error) { func (s *Server) sendrawtransaction(reqParams Params) (interface{}, error) {

View file

@ -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": { "invokefunction": {
{ {
name: "positive", name: "positive",

View file

@ -166,6 +166,90 @@ func CreateDeploymentScript(avm []byte, contract *ContractDetails) ([]byte, erro
return script.Bytes(), nil 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 // CreateFunctionInvocationScript creates a script to invoke given contract with
// given parameters. // given parameters.
func CreateFunctionInvocationScript(contract util.Uint160, params Params) ([]byte, error) { func CreateFunctionInvocationScript(contract util.Uint160, params Params) ([]byte, error) {
@ -189,84 +273,10 @@ func CreateFunctionInvocationScript(contract util.Uint160, params Params) ([]byt
if err != nil { if err != nil {
return nil, err return nil, err
} }
for j := len(slice) - 1; j >= 0; j-- { err = expandArrayIntoScript(script, slice)
fp, err := slice[j].GetFuncParam()
if err != nil { if err != nil {
return nil, err 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 = vm.EmitInt(script, int64(len(slice))) err = vm.EmitInt(script, int64(len(slice)))
if err != nil { if err != nil {
return nil, err return nil, err
@ -283,3 +293,19 @@ func CreateFunctionInvocationScript(contract util.Uint160, params Params) ([]byt
} }
return script.Bytes(), nil 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
}