Merge pull request #1047 from nspcc-dev/neo3/rpc/invoke

rpc: update invoke* RPC-calls
This commit is contained in:
Roman Khimov 2020-06-11 21:39:20 +03:00 committed by GitHub
commit fe31c7ed2d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 464 additions and 187 deletions

View file

@ -15,6 +15,7 @@ import (
"github.com/go-yaml/yaml" "github.com/go-yaml/yaml"
"github.com/nspcc-dev/neo-go/cli/flags" "github.com/nspcc-dev/neo-go/cli/flags"
"github.com/nspcc-dev/neo-go/pkg/compiler" "github.com/nspcc-dev/neo-go/pkg/compiler"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/crypto/hash" "github.com/nspcc-dev/neo-go/pkg/crypto/hash"
"github.com/nspcc-dev/neo-go/pkg/encoding/address" "github.com/nspcc-dev/neo-go/pkg/encoding/address"
"github.com/nspcc-dev/neo-go/pkg/rpc/client" "github.com/nspcc-dev/neo-go/pkg/rpc/client"
@ -68,6 +69,9 @@ import "github.com/nspcc-dev/neo-go/pkg/interop/runtime"
func Main(op string, args []interface{}) { func Main(op string, args []interface{}) {
runtime.Notify("Hello world!") runtime.Notify("Hello world!")
}` }`
// cosignersSeparator is a special value which is used to distinguish
// parameters and cosigners for invoke* commands
cosignersSeparator = "--"
) )
// NewCommands returns 'contract' command. // NewCommands returns 'contract' command.
@ -132,31 +136,14 @@ func NewCommands() []cli.Command {
gasFlag, gasFlag,
}, },
}, },
{
Name: "invoke",
Usage: "invoke deployed contract on the blockchain",
UsageText: "neo-go contract invoke -e endpoint -w wallet [-a address] [-g gas] scripthash [arguments...]",
Description: `Executes given (as a script hash) deployed script with the given arguments.
See testinvoke documentation for the details about parameters. It differs
from testinvoke in that this command sends an invocation transaction to
the network.
`,
Action: invoke,
Flags: []cli.Flag{
endpointFlag,
walletFlag,
addressFlag,
gasFlag,
},
},
{ {
Name: "invokefunction", Name: "invokefunction",
Usage: "invoke deployed contract on the blockchain", Usage: "invoke deployed contract on the blockchain",
UsageText: "neo-go contract invokefunction -e endpoint -w wallet [-a address] [-g gas] scripthash [method] [arguments...]", UsageText: "neo-go contract invokefunction -e endpoint -w wallet [-a address] [-g gas] scripthash [method] [arguments...] [--] [cosigners...]",
Description: `Executes given (as a script hash) deployed script with the given method and Description: `Executes given (as a script hash) deployed script with the given method,
and arguments. See testinvokefunction documentation for the details about arguments and cosigners. See testinvokefunction documentation for the details
parameters. It differs from testinvokefunction in that this command sends an about parameters. It differs from testinvokefunction in that this command
invocation transaction to the network. sends an invocation transaction to the network.
`, `,
Action: invokeFunction, Action: invokeFunction,
Flags: []cli.Flag{ Flags: []cli.Flag{
@ -166,36 +153,17 @@ func NewCommands() []cli.Command {
gasFlag, gasFlag,
}, },
}, },
{
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{
endpointFlag,
},
},
{ {
Name: "testinvokefunction", Name: "testinvokefunction",
Usage: "invoke deployed contract on the blockchain (test mode)", Usage: "invoke deployed contract on the blockchain (test mode)",
UsageText: "neo-go contract testinvokefunction -e endpoint scripthash [method] [arguments...]", UsageText: "neo-go contract testinvokefunction -e endpoint scripthash [method] [arguments...] [--] [cosigners...]",
Description: `Executes given (as a script hash) deployed script with the given method and Description: `Executes given (as a script hash) deployed script with the given method,
arguments. If no method is given "" is passed to the script, if no arguments arguments and cosigners. If no method is given "" is passed to the script, if
are given, an empty array is passed. All of the given arguments are no arguments are given, an empty array is passed, if no cosigners are given,
encapsulated into array before invoking the script. The script thus should no array will be passed. All of the given arguments are encapsulated into
follow the regular convention of smart contract arguments (method string and array before invoking the script. The script thus should follow the regular
an array of other arguments). convention of smart contract arguments (method string and an array of other
arguments).
Arguments always do have regular Neo smart contract parameter types, either Arguments always do have regular Neo smart contract parameter types, either
specified explicitly or being inferred from the value. To specify the type specified explicitly or being inferred from the value. To specify the type
@ -251,6 +219,32 @@ func NewCommands() []cli.Command {
* 'string\:string' is a string with a value of 'string:string' * 'string\:string' is a string with a value of 'string:string'
* '03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c' is a * '03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c' is a
key with a value of '03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c' key with a value of '03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c'
Cosigners represent a set of Uint160 hashes with witness scopes and are used
to verify hashes in System.Runtime.CheckWitness syscall. To specify cosigners
use cosigner[:scope] syntax where
* 'cosigner' is hex-encoded 160 bit (20 byte) LE value of cosigner's address,
which could have '0x' prefix.
* 'scope' is a comma-separated set of cosigner's scopes, which could be:
- 'Global' - allows this witness in all contexts. This cannot be combined
with other flags.
- 'CalledByEntry' - means that this condition must hold: EntryScriptHash
== CallingScriptHash. The witness/permission/signature
given on first invocation will automatically expire if
entering deeper internal invokes. This can be default
safe choice for native NEO/GAS.
- 'CustomContracts' - define valid custom contract hashes for witness check.
- 'CustomGroups' - define custom pubkey for group members.
If no scopes were specified, 'Global' used as default. If no cosigners were
specified, no array will be passed. Note that scopes are properly handled by
neo-go RPC server only. C# implementation does not support scopes capability.
Examples:
* '0000000009070e030d0f0e020d0c06050e030c02'
* '0x0000000009070e030d0f0e020d0c06050e030c02'
* '0x0000000009070e030d0f0e020d0c06050e030c02:Global'
* '0000000009070e030d0f0e020d0c06050e030c02:CalledByEntry,CustomGroups'
`, `,
Action: testInvokeFunction, Action: testInvokeFunction,
Flags: []cli.Flag{ Flags: []cli.Flag{
@ -260,6 +254,10 @@ func NewCommands() []cli.Command {
{ {
Name: "testinvokescript", Name: "testinvokescript",
Usage: "Invoke compiled AVM code on the blockchain (test mode, not creating a transaction for it)", Usage: "Invoke compiled AVM code on the blockchain (test mode, not creating a transaction for it)",
UsageText: "neo-go contract testinvokescript -e endpoint -i input.avm [cosigners...]",
Description: `Executes given compiled AVM instructions with the given set of
cosigners. See testinvokefunction documentation for the details about parameters.
`,
Action: testInvokeScript, Action: testInvokeScript,
Flags: []cli.Flag{ Flags: []cli.Flag{
endpointFlag, endpointFlag,
@ -388,29 +386,23 @@ func contractCompile(ctx *cli.Context) error {
return nil return nil
} }
func testInvoke(ctx *cli.Context) error {
return invokeInternal(ctx, false, false)
}
func testInvokeFunction(ctx *cli.Context) error { func testInvokeFunction(ctx *cli.Context) error {
return invokeInternal(ctx, true, false) return invokeInternal(ctx, false)
}
func invoke(ctx *cli.Context) error {
return invokeInternal(ctx, false, true)
} }
func invokeFunction(ctx *cli.Context) error { func invokeFunction(ctx *cli.Context) error {
return invokeInternal(ctx, true, true) return invokeInternal(ctx, true)
} }
func invokeInternal(ctx *cli.Context, withMethod bool, signAndPush bool) error { func invokeInternal(ctx *cli.Context, signAndPush bool) error {
var ( var (
err error err error
gas util.Fixed8 gas util.Fixed8
operation string operation string
params = make([]smartcontract.Parameter, 0) params = make([]smartcontract.Parameter, 0)
paramsStart = 1 paramsStart = 1
cosigners []transaction.Cosigner
cosignersStart = 0
resp *result.Invoke resp *result.Invoke
acc *wallet.Account acc *wallet.Account
) )
@ -425,15 +417,19 @@ func invokeInternal(ctx *cli.Context, withMethod bool, signAndPush bool) error {
return cli.NewExitError(errNoScriptHash, 1) return cli.NewExitError(errNoScriptHash, 1)
} }
script := args[0] script := args[0]
if withMethod {
if len(args) <= 1 { if len(args) <= 1 {
return cli.NewExitError(errNoMethod, 1) return cli.NewExitError(errNoMethod, 1)
} }
operation = args[1] operation = args[1]
paramsStart++ paramsStart++
}
if len(args) > paramsStart { if len(args) > paramsStart {
for k, s := range args[paramsStart:] { for k, s := range args[paramsStart:] {
if s == cosignersSeparator {
cosignersStart = paramsStart + k + 1
break
}
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+paramsStart+1, err), 1) return cli.NewExitError(fmt.Errorf("failed to parse argument #%d: %v", k+paramsStart+1, err), 1)
@ -442,6 +438,16 @@ func invokeInternal(ctx *cli.Context, withMethod bool, signAndPush bool) error {
} }
} }
if len(args) >= cosignersStart && cosignersStart > 0 {
for i, c := range args[cosignersStart:] {
cosigner, err := parseCosigner(c)
if err != nil {
return cli.NewExitError(fmt.Errorf("failed to parse cosigner #%d: %v", i+cosignersStart+1, err), 1)
}
cosigners = append(cosigners, cosigner)
}
}
if signAndPush { if signAndPush {
gas = flags.Fixed8FromContext(ctx, "gas") gas = flags.Fixed8FromContext(ctx, "gas")
acc, err = getAccFromContext(ctx) acc, err = getAccFromContext(ctx)
@ -454,11 +460,7 @@ func invokeInternal(ctx *cli.Context, withMethod bool, signAndPush bool) error {
return cli.NewExitError(err, 1) return cli.NewExitError(err, 1)
} }
if withMethod { resp, err = c.InvokeFunction(script, operation, params, cosigners)
resp, err = c.InvokeFunction(script, operation, params)
} else {
resp, err = c.Invoke(script, params)
}
if err != nil { if err != nil {
return cli.NewExitError(err, 1) return cli.NewExitError(err, 1)
} }
@ -502,13 +504,25 @@ func testInvokeScript(ctx *cli.Context) error {
return cli.NewExitError(err, 1) return cli.NewExitError(err, 1)
} }
args := ctx.Args()
var cosigners []transaction.Cosigner
if args.Present() {
for i, c := range args[:] {
cosigner, err := parseCosigner(c)
if err != nil {
return cli.NewExitError(fmt.Errorf("failed to parse cosigner #%d: %v", i+1, err), 1)
}
cosigners = append(cosigners, cosigner)
}
}
c, err := client.New(context.TODO(), endpoint, client.Options{}) c, err := client.New(context.TODO(), endpoint, client.Options{})
if err != nil { if err != nil {
return cli.NewExitError(err, 1) return cli.NewExitError(err, 1)
} }
scriptHex := hex.EncodeToString(b) scriptHex := hex.EncodeToString(b)
resp, err := c.InvokeScript(scriptHex) resp, err := c.InvokeScript(scriptHex, cosigners)
if err != nil { if err != nil {
return cli.NewExitError(err, 1) return cli.NewExitError(err, 1)
} }
@ -677,3 +691,26 @@ func parseContractConfig(confFile string) (ProjectConfig, error) {
} }
return conf, nil return conf, nil
} }
func parseCosigner(c string) (transaction.Cosigner, error) {
var (
err error
res = transaction.Cosigner{}
)
data := strings.SplitN(strings.ToLower(c), ":", 2)
s := data[0]
if len(s) == 2*util.Uint160Size+2 && s[0:2] == "0x" {
s = s[2:]
}
res.Account, err = util.Uint160DecodeStringLE(s)
if err != nil {
return res, err
}
if len(data) > 1 {
res.Scopes, err = transaction.ScopesFromString(data[1])
if err != nil {
return transaction.Cosigner{}, err
}
}
return res, nil
}

View file

@ -1246,8 +1246,8 @@ func (bc *Blockchain) GetScriptHashesForVerifying(t *transaction.Transaction) ([
} }
// GetTestVM returns a VM and a Store setup for a test run of some sort of code. // GetTestVM returns a VM and a Store setup for a test run of some sort of code.
func (bc *Blockchain) GetTestVM() *vm.VM { func (bc *Blockchain) GetTestVM(tx *transaction.Transaction) *vm.VM {
systemInterop := bc.newInteropContext(trigger.Application, bc.dao, nil, nil) systemInterop := bc.newInteropContext(trigger.Application, bc.dao, nil, tx)
vm := SpawnVM(systemInterop) vm := SpawnVM(systemInterop)
vm.SetPriceGetter(getPrice) vm.SetPriceGetter(getPrice)
return vm return vm

View file

@ -41,7 +41,7 @@ type Blockchainer interface {
GetScriptHashesForVerifying(*transaction.Transaction) ([]util.Uint160, error) GetScriptHashesForVerifying(*transaction.Transaction) ([]util.Uint160, error)
GetStorageItem(scripthash util.Uint160, key []byte) *state.StorageItem GetStorageItem(scripthash util.Uint160, key []byte) *state.StorageItem
GetStorageItems(hash util.Uint160) (map[string]*state.StorageItem, error) GetStorageItems(hash util.Uint160) (map[string]*state.StorageItem, error)
GetTestVM() *vm.VM GetTestVM(tx *transaction.Transaction) *vm.VM
GetTransaction(util.Uint256) (*transaction.Transaction, uint32, error) GetTransaction(util.Uint256) (*transaction.Transaction, uint32, error)
mempool.Feer // fee interface mempool.Feer // fee interface
PoolTx(*transaction.Transaction) error PoolTx(*transaction.Transaction) error

View file

@ -1,5 +1,10 @@
package transaction package transaction
import (
"fmt"
"strings"
)
// WitnessScope represents set of witness flags for Transaction cosigner. // WitnessScope represents set of witness flags for Transaction cosigner.
type WitnessScope byte type WitnessScope byte
@ -17,3 +22,34 @@ const (
// CustomGroups define custom pubkey for group members. // CustomGroups define custom pubkey for group members.
CustomGroups WitnessScope = 0x20 CustomGroups WitnessScope = 0x20
) )
// ScopesFromString converts string of comma-separated scopes to a set of scopes
// (case doesn't matter). String can combine several scopes, e.g. be any of:
// 'Global', 'CalledByEntry,CustomGroups' etc. In case of an empty string an
// error will be returned.
func ScopesFromString(s string) (WitnessScope, error) {
var result WitnessScope
s = strings.ToLower(s)
scopes := strings.Split(s, ",")
dict := map[string]WitnessScope{
"global": Global,
"calledbyentry": CalledByEntry,
"customcontracts": CustomContracts,
"customgroups": CustomGroups,
}
var isGlobal bool
for _, scopeStr := range scopes {
scope, ok := dict[scopeStr]
if !ok {
return result, fmt.Errorf("invalid witness scope: %v", scopeStr)
}
if isGlobal && !(scope == Global) {
return result, fmt.Errorf("Global scope can not be combined with other scopes")
}
result |= scope
if scope == Global {
isGlobal = true
}
}
return result, nil
}

View file

@ -0,0 +1,45 @@
package transaction
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestScopesFromString(t *testing.T) {
s, err := ScopesFromString("")
require.Error(t, err)
_, err = ScopesFromString("123")
require.Error(t, err)
s, err = ScopesFromString("Global")
require.NoError(t, err)
require.Equal(t, Global, s)
s, err = ScopesFromString("CalledByEntry")
require.NoError(t, err)
require.Equal(t, CalledByEntry, s)
s, err = ScopesFromString("CustomContracts")
require.NoError(t, err)
require.Equal(t, CustomContracts, s)
s, err = ScopesFromString("CustomGroups")
require.NoError(t, err)
require.Equal(t, CustomGroups, s)
s, err = ScopesFromString("Calledbyentry,customgroups")
require.NoError(t, err)
require.Equal(t, CalledByEntry|CustomGroups, s)
_, err = ScopesFromString("global,customgroups")
require.Error(t, err)
_, err = ScopesFromString("calledbyentry,global,customgroups")
require.Error(t, err)
s, err = ScopesFromString("Calledbyentry,customgroups,Customgroups")
require.NoError(t, err)
require.Equal(t, CalledByEntry|CustomGroups, s)
}

View file

@ -97,7 +97,7 @@ func (chain testChain) GetScriptHashesForVerifying(*transaction.Transaction) ([]
func (chain testChain) GetStorageItem(scripthash util.Uint160, key []byte) *state.StorageItem { func (chain testChain) GetStorageItem(scripthash util.Uint160, key []byte) *state.StorageItem {
panic("TODO") panic("TODO")
} }
func (chain testChain) GetTestVM() *vm.VM { func (chain testChain) GetTestVM(tx *transaction.Transaction) *vm.VM {
panic("TODO") panic("TODO")
} }
func (chain testChain) GetStorageItems(hash util.Uint160) (map[string]*state.StorageItem, error) { func (chain testChain) GetStorageItems(hash util.Uint160) (map[string]*state.StorageItem, error) {

View file

@ -18,7 +18,7 @@ import (
// NEP5Decimals invokes `decimals` NEP5 method on a specified contract. // NEP5Decimals invokes `decimals` NEP5 method on a specified contract.
func (c *Client) NEP5Decimals(tokenHash util.Uint160) (int64, error) { func (c *Client) NEP5Decimals(tokenHash util.Uint160) (int64, error) {
result, err := c.InvokeFunction(tokenHash.StringLE(), "decimals", []smartcontract.Parameter{}) result, err := c.InvokeFunction(tokenHash.StringLE(), "decimals", []smartcontract.Parameter{}, nil)
if err != nil { if err != nil {
return 0, err return 0, err
} else if result.State != "HALT" || len(result.Stack) == 0 { } else if result.State != "HALT" || len(result.Stack) == 0 {
@ -30,7 +30,7 @@ func (c *Client) NEP5Decimals(tokenHash util.Uint160) (int64, error) {
// NEP5Name invokes `name` NEP5 method on a specified contract. // NEP5Name invokes `name` NEP5 method on a specified contract.
func (c *Client) NEP5Name(tokenHash util.Uint160) (string, error) { func (c *Client) NEP5Name(tokenHash util.Uint160) (string, error) {
result, err := c.InvokeFunction(tokenHash.StringLE(), "name", []smartcontract.Parameter{}) result, err := c.InvokeFunction(tokenHash.StringLE(), "name", []smartcontract.Parameter{}, nil)
if err != nil { if err != nil {
return "", err return "", err
} else if result.State != "HALT" || len(result.Stack) == 0 { } else if result.State != "HALT" || len(result.Stack) == 0 {
@ -42,7 +42,7 @@ func (c *Client) NEP5Name(tokenHash util.Uint160) (string, error) {
// NEP5Symbol invokes `symbol` NEP5 method on a specified contract. // NEP5Symbol invokes `symbol` NEP5 method on a specified contract.
func (c *Client) NEP5Symbol(tokenHash util.Uint160) (string, error) { func (c *Client) NEP5Symbol(tokenHash util.Uint160) (string, error) {
result, err := c.InvokeFunction(tokenHash.StringLE(), "symbol", []smartcontract.Parameter{}) result, err := c.InvokeFunction(tokenHash.StringLE(), "symbol", []smartcontract.Parameter{}, nil)
if err != nil { if err != nil {
return "", err return "", err
} else if result.State != "HALT" || len(result.Stack) == 0 { } else if result.State != "HALT" || len(result.Stack) == 0 {
@ -54,7 +54,7 @@ func (c *Client) NEP5Symbol(tokenHash util.Uint160) (string, error) {
// NEP5TotalSupply invokes `totalSupply` NEP5 method on a specified contract. // NEP5TotalSupply invokes `totalSupply` NEP5 method on a specified contract.
func (c *Client) NEP5TotalSupply(tokenHash util.Uint160) (int64, error) { func (c *Client) NEP5TotalSupply(tokenHash util.Uint160) (int64, error) {
result, err := c.InvokeFunction(tokenHash.StringLE(), "totalSupply", []smartcontract.Parameter{}) result, err := c.InvokeFunction(tokenHash.StringLE(), "totalSupply", []smartcontract.Parameter{}, nil)
if err != nil { if err != nil {
return 0, err return 0, err
} else if result.State != "HALT" || len(result.Stack) == 0 { } else if result.State != "HALT" || len(result.Stack) == 0 {
@ -66,7 +66,7 @@ func (c *Client) NEP5TotalSupply(tokenHash util.Uint160) (int64, error) {
// NEP5BalanceOf invokes `balanceOf` NEP5 method on a specified contract. // NEP5BalanceOf invokes `balanceOf` NEP5 method on a specified contract.
func (c *Client) NEP5BalanceOf(tokenHash util.Uint160) (int64, error) { func (c *Client) NEP5BalanceOf(tokenHash util.Uint160) (int64, error) {
result, err := c.InvokeFunction(tokenHash.StringLE(), "balanceOf", []smartcontract.Parameter{}) result, err := c.InvokeFunction(tokenHash.StringLE(), "balanceOf", []smartcontract.Parameter{}, nil)
if err != nil { if err != nil {
return 0, err return 0, err
} else if result.State != "HALT" || len(result.Stack) == 0 { } else if result.State != "HALT" || len(result.Stack) == 0 {
@ -120,7 +120,12 @@ func (c *Client) CreateNEP5TransferTx(acc *wallet.Account, to util.Uint160, toke
}, },
} }
result, err := c.InvokeScript(hex.EncodeToString(script)) result, err := c.InvokeScript(hex.EncodeToString(script), []transaction.Cosigner{
{
Account: from,
Scopes: transaction.Global,
},
})
if err != nil { if err != nil {
return nil, fmt.Errorf("can't add system fee to transaction: %v", err) return nil, fmt.Errorf("can't add system fee to transaction: %v", err)
} }

View file

@ -339,11 +339,16 @@ func (c *Client) GetVersion() (*result.Version, error) {
// InvokeScript returns the result of the given script after running it true the VM. // InvokeScript returns the result of the given script after running it true the VM.
// NOTE: This is a test invoke and will not affect the blockchain. // NOTE: This is a test invoke and will not affect the blockchain.
func (c *Client) InvokeScript(script string) (*result.Invoke, error) { func (c *Client) InvokeScript(script string, cosigners []transaction.Cosigner) (*result.Invoke, error) {
var ( var (
params = request.NewRawParams(script) params request.RawParams
resp = &result.Invoke{} resp = &result.Invoke{}
) )
if cosigners != nil {
params = request.NewRawParams(script, cosigners)
} else {
params = request.NewRawParams(script)
}
if err := c.performRequest("invokescript", params, resp); err != nil { if err := c.performRequest("invokescript", params, resp); err != nil {
return nil, err return nil, err
} }
@ -353,25 +358,17 @@ func (c *Client) InvokeScript(script string) (*result.Invoke, error) {
// InvokeFunction returns the results after calling the smart contract scripthash // InvokeFunction returns the results after calling the smart contract scripthash
// with the given operation and parameters. // with the given operation and parameters.
// NOTE: this is test invoke and will not affect the blockchain. // NOTE: this is test invoke and will not affect the blockchain.
func (c *Client) InvokeFunction(script, operation string, params []smartcontract.Parameter) (*result.Invoke, error) { func (c *Client) InvokeFunction(script, operation string, params []smartcontract.Parameter, cosigners []transaction.Cosigner) (*result.Invoke, error) {
var ( var (
p request.RawParams
resp = &result.Invoke{}
)
if cosigners != nil {
p = request.NewRawParams(script, operation, params, cosigners)
} else {
p = request.NewRawParams(script, operation, params) p = request.NewRawParams(script, operation, params)
resp = &result.Invoke{}
)
if err := c.performRequest("invokefunction", p, resp); err != nil {
return nil, err
} }
return resp, nil if err := c.performRequest("invokefunction", p, resp); err != nil {
}
// Invoke returns the results after calling the smart contract scripthash
// with the given parameters.
func (c *Client) Invoke(script string, params []smartcontract.Parameter) (*result.Invoke, error) {
var (
p = request.NewRawParams(script, params)
resp = &result.Invoke{}
)
if err := c.performRequest("invoke", p, resp); err != nil {
return nil, err return nil, err
} }
return resp, nil return resp, nil

View file

@ -563,7 +563,9 @@ var rpcClientTestCases = map[string][]rpcClientTestCase{
Type: smartcontract.Hash160Type, Type: smartcontract.Hash160Type,
Value: hash, Value: hash,
}, },
}) }, []transaction.Cosigner{{
Account: util.Uint160{1, 2, 3},
}})
}, },
serverResponse: `{"jsonrpc":"2.0","id":1,"result":{"script":"1426ae7c6c9861ec418468c1f0fdc4a7f2963eb89151c10962616c616e63654f6667be39e7b562f60cbfe2aebca375a2e5ee28737caf","state":"HALT","gas_consumed":"0.311","stack":[{"type":"ByteArray","value":"JivsCEQy"}],"tx":"d101361426ae7c6c9861ec418468c1f0fdc4a7f2963eb89151c10962616c616e63654f6667be39e7b562f60cbfe2aebca375a2e5ee28737caf000000000000000000000000"}}`, serverResponse: `{"jsonrpc":"2.0","id":1,"result":{"script":"1426ae7c6c9861ec418468c1f0fdc4a7f2963eb89151c10962616c616e63654f6667be39e7b562f60cbfe2aebca375a2e5ee28737caf","state":"HALT","gas_consumed":"0.311","stack":[{"type":"ByteArray","value":"JivsCEQy"}],"tx":"d101361426ae7c6c9861ec418468c1f0fdc4a7f2963eb89151c10962616c616e63654f6667be39e7b562f60cbfe2aebca375a2e5ee28737caf000000000000000000000000"}}`,
result: func(c *Client) interface{} { result: func(c *Client) interface{} {
@ -589,7 +591,9 @@ var rpcClientTestCases = map[string][]rpcClientTestCase{
{ {
name: "positive", name: "positive",
invoke: func(c *Client) (interface{}, error) { invoke: func(c *Client) (interface{}, error) {
return c.InvokeScript("00046e616d656724058e5e1b6008847cd662728549088a9ee82191") return c.InvokeScript("00046e616d656724058e5e1b6008847cd662728549088a9ee82191", []transaction.Cosigner{{
Account: util.Uint160{1, 2, 3},
}})
}, },
serverResponse: `{"jsonrpc":"2.0","id":1,"result":{"script":"00046e616d656724058e5e1b6008847cd662728549088a9ee82191","state":"HALT","gas_consumed":"0.161","stack":[{"type":"ByteArray","value":"TkVQNSBHQVM="}],"tx":"d1011b00046e616d656724058e5e1b6008847cd662728549088a9ee82191000000000000000000000000"}}`, serverResponse: `{"jsonrpc":"2.0","id":1,"result":{"script":"00046e616d656724058e5e1b6008847cd662728549088a9ee82191","state":"HALT","gas_consumed":"0.161","stack":[{"type":"ByteArray","value":"TkVQNSBHQVM="}],"tx":"d1011b00046e616d656724058e5e1b6008847cd662728549088a9ee82191000000000000000000000000"}}`,
result: func(c *Client) interface{} { result: func(c *Client) interface{} {
@ -874,13 +878,13 @@ var rpcClientErrorCases = map[string][]rpcClientErrorCase{
{ {
name: "invokefunction_invalid_params_error", name: "invokefunction_invalid_params_error",
invoke: func(c *Client) (interface{}, error) { invoke: func(c *Client) (interface{}, error) {
return c.InvokeFunction("", "", []smartcontract.Parameter{}) return c.InvokeFunction("", "", []smartcontract.Parameter{}, nil)
}, },
}, },
{ {
name: "invokescript_invalid_params_error", name: "invokescript_invalid_params_error",
invoke: func(c *Client) (interface{}, error) { invoke: func(c *Client) (interface{}, error) {
return c.InvokeScript("") return c.InvokeScript("", nil)
}, },
}, },
{ {
@ -1050,13 +1054,13 @@ var rpcClientErrorCases = map[string][]rpcClientErrorCase{
{ {
name: "invokefunction_unmarshalling_error", name: "invokefunction_unmarshalling_error",
invoke: func(c *Client) (interface{}, error) { invoke: func(c *Client) (interface{}, error) {
return c.InvokeFunction("", "", []smartcontract.Parameter{}) return c.InvokeFunction("", "", []smartcontract.Parameter{}, nil)
}, },
}, },
{ {
name: "invokescript_unmarshalling_error", name: "invokescript_unmarshalling_error",
invoke: func(c *Client) (interface{}, error) { invoke: func(c *Client) (interface{}, error) {
return c.InvokeScript("") return c.InvokeScript("", nil)
}, },
}, },
{ {

View file

@ -7,6 +7,7 @@ import (
"fmt" "fmt"
"strconv" "strconv"
"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/encoding/address"
"github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/util"
@ -65,6 +66,7 @@ const (
TxFilterT TxFilterT
NotificationFilterT NotificationFilterT
ExecutionFilterT ExecutionFilterT
Cosigner
) )
func (p Param) String() string { func (p Param) String() string {
@ -154,6 +156,47 @@ func (p Param) GetBytesHex() ([]byte, error) {
return hex.DecodeString(s) return hex.DecodeString(s)
} }
// GetCosigner returns transaction.Cosigner value of the parameter.
func (p Param) GetCosigner() (transaction.Cosigner, error) {
c, ok := p.Value.(transaction.Cosigner)
if !ok {
return transaction.Cosigner{}, errors.New("not a cosigner")
}
return c, nil
}
// GetCosigners returns a slice of transaction.Cosigner with global scope from
// array of Uint160 or array of serialized transaction.Cosigner stored in the
// parameter.
func (p Param) GetCosigners() ([]transaction.Cosigner, error) {
hashes, err := p.GetArray()
if err != nil {
return nil, err
}
cosigners := make([]transaction.Cosigner, len(hashes))
// try to extract hashes first
for i, h := range hashes {
var u util.Uint160
u, err = h.GetUint160FromHex()
if err != nil {
break
}
cosigners[i] = transaction.Cosigner{
Account: u,
Scopes: transaction.Global,
}
}
if err != nil {
for i, h := range hashes {
cosigners[i], err = h.GetCosigner()
if err != nil {
return nil, err
}
}
}
return cosigners, nil
}
// UnmarshalJSON implements json.Unmarshaler interface. // UnmarshalJSON implements json.Unmarshaler interface.
func (p *Param) UnmarshalJSON(data []byte) error { func (p *Param) UnmarshalJSON(data []byte) error {
var s string var s string
@ -167,6 +210,7 @@ func (p *Param) UnmarshalJSON(data []byte) error {
{TxFilterT, &TxFilter{}}, {TxFilterT, &TxFilter{}},
{NotificationFilterT, &NotificationFilter{}}, {NotificationFilterT, &NotificationFilter{}},
{ExecutionFilterT, &ExecutionFilter{}}, {ExecutionFilterT, &ExecutionFilter{}},
{Cosigner, &transaction.Cosigner{}},
{ArrayT, &[]Param{}}, {ArrayT, &[]Param{}},
} }
@ -196,6 +240,8 @@ func (p *Param) UnmarshalJSON(data []byte) error {
} else { } else {
continue continue
} }
case *transaction.Cosigner:
p.Value = *val
case *[]Param: case *[]Param:
p.Value = *val p.Value = *val
} }

View file

@ -5,6 +5,7 @@ import (
"encoding/json" "encoding/json"
"testing" "testing"
"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/encoding/address"
"github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/util"
@ -19,9 +20,13 @@ func TestParam_UnmarshalJSON(t *testing.T) {
{"cosigner": "f84d6a337fbc3d3a201d41da99e86b479e7a2554"}, {"cosigner": "f84d6a337fbc3d3a201d41da99e86b479e7a2554"},
{"sender": "f84d6a337fbc3d3a201d41da99e86b479e7a2554", "cosigner": "f84d6a337fbc3d3a201d41da99e86b479e7a2554"}, {"sender": "f84d6a337fbc3d3a201d41da99e86b479e7a2554", "cosigner": "f84d6a337fbc3d3a201d41da99e86b479e7a2554"},
{"contract": "f84d6a337fbc3d3a201d41da99e86b479e7a2554"}, {"contract": "f84d6a337fbc3d3a201d41da99e86b479e7a2554"},
{"state": "HALT"}]` {"state": "HALT"},
{"account": "0xcadb3dc2faa3ef14a13b619c9a43124755aa2569", "scopes": 0},
[{"account": "0xcadb3dc2faa3ef14a13b619c9a43124755aa2569", "scopes": 0}]]`
contr, err := util.Uint160DecodeStringLE("f84d6a337fbc3d3a201d41da99e86b479e7a2554") contr, err := util.Uint160DecodeStringLE("f84d6a337fbc3d3a201d41da99e86b479e7a2554")
require.NoError(t, err) require.NoError(t, err)
accountHash, err := util.Uint160DecodeStringLE("cadb3dc2faa3ef14a13b619c9a43124755aa2569")
require.NoError(t, err)
expected := Params{ expected := Params{
{ {
Type: StringT, Type: StringT,
@ -83,6 +88,25 @@ func TestParam_UnmarshalJSON(t *testing.T) {
Type: ExecutionFilterT, Type: ExecutionFilterT,
Value: ExecutionFilter{State: "HALT"}, Value: ExecutionFilter{State: "HALT"},
}, },
{
Type: Cosigner,
Value: transaction.Cosigner{
Account: accountHash,
Scopes: transaction.Global,
},
},
{
Type: ArrayT,
Value: []Param{
{
Type: Cosigner,
Value: transaction.Cosigner{
Account: accountHash,
Scopes: transaction.Global,
},
},
},
},
} }
var ps Params var ps Params
@ -214,3 +238,67 @@ func TestParamGetBytesHex(t *testing.T) {
_, err = p.GetBytesHex() _, err = p.GetBytesHex()
require.NotNil(t, err) require.NotNil(t, err)
} }
func TestParamGetCosigner(t *testing.T) {
c := transaction.Cosigner{
Account: util.Uint160{1, 2, 3, 4},
Scopes: transaction.Global,
}
p := Param{Type: Cosigner, Value: c}
actual, err := p.GetCosigner()
require.NoError(t, err)
require.Equal(t, c, actual)
p = Param{Type: Cosigner, Value: `{"account": "0xcadb3dc2faa3ef14a13b619c9a43124755aa2569", "scopes": 0}`}
_, err = p.GetCosigner()
require.Error(t, err)
}
func TestParamGetCosigners(t *testing.T) {
u1 := util.Uint160{1, 2, 3, 4}
u2 := util.Uint160{5, 6, 7, 8}
t.Run("from hashes", func(t *testing.T) {
p := Param{ArrayT, []Param{
{Type: StringT, Value: u1.StringLE()},
{Type: StringT, Value: u2.StringLE()},
}}
actual, err := p.GetCosigners()
require.NoError(t, err)
require.Equal(t, 2, len(actual))
require.True(t, u1.Equals(actual[0].Account))
require.True(t, u2.Equals(actual[1].Account))
})
t.Run("from cosigners", func(t *testing.T) {
c1 := transaction.Cosigner{
Account: u1,
Scopes: transaction.Global,
}
c2 := transaction.Cosigner{
Account: u2,
Scopes: transaction.CustomContracts,
AllowedContracts: []util.Uint160{
{1, 2, 3},
{4, 5, 6},
},
}
p := Param{ArrayT, []Param{
{Type: Cosigner, Value: c1},
{Type: Cosigner, Value: c2},
}}
actual, err := p.GetCosigners()
require.NoError(t, err)
require.Equal(t, 2, len(actual))
require.Equal(t, c1, actual[0])
require.Equal(t, c2, actual[1])
})
t.Run("bad format", func(t *testing.T) {
p := Param{ArrayT, []Param{
{Type: StringT, Value: u1.StringLE()},
{Type: StringT, Value: "bla"},
}}
_, err := p.GetCosigners()
require.Error(t, err)
})
}

View file

@ -98,7 +98,6 @@ var rpcHandlers = map[string]func(*Server, request.Params) (interface{}, *respon
"getunclaimedgas": (*Server).getUnclaimedGas, "getunclaimedgas": (*Server).getUnclaimedGas,
"getvalidators": (*Server).getValidators, "getvalidators": (*Server).getValidators,
"getversion": (*Server).getVersion, "getversion": (*Server).getVersion,
"invoke": (*Server).invoke,
"invokefunction": (*Server).invokeFunction, "invokefunction": (*Server).invokeFunction,
"invokescript": (*Server).invokescript, "invokescript": (*Server).invokescript,
"sendrawtransaction": (*Server).sendrawtransaction, "sendrawtransaction": (*Server).sendrawtransaction,
@ -619,7 +618,7 @@ func (s *Server) getDecimals(h util.Uint160, cache map[util.Uint160]int64) (int6
if err != nil { if err != nil {
return 0, response.NewInternalServerError("Can't create script", err) return 0, response.NewInternalServerError("Can't create script", err)
} }
res := s.runScriptInVM(script) res := s.runScriptInVM(script, nil)
if res == nil || res.State != "HALT" || len(res.Stack) == 0 { if res == nil || res.State != "HALT" || len(res.Stack) == 0 {
return 0, response.NewInternalServerError("execution error", errors.New("no result")) return 0, response.NewInternalServerError("execution error", errors.New("no result"))
} }
@ -854,32 +853,7 @@ func (s *Server) getValidators(_ request.Params) (interface{}, *response.Error)
return res, nil return res, nil
} }
// invoke implements the `invoke` RPC call. // invokeFunction implements the `invokeFunction` RPC call.
func (s *Server) invoke(reqParams request.Params) (interface{}, *response.Error) {
scriptHashHex, ok := reqParams.ValueWithType(0, request.StringT)
if !ok {
return nil, response.ErrInvalidParams
}
scriptHash, err := scriptHashHex.GetUint160FromHex()
if err != nil {
return nil, response.ErrInvalidParams
}
sliceP, ok := reqParams.ValueWithType(1, request.ArrayT)
if !ok {
return nil, response.ErrInvalidParams
}
slice, err := sliceP.GetArray()
if err != nil {
return nil, response.ErrInvalidParams
}
script, err := request.CreateInvocationScript(scriptHash, slice)
if err != nil {
return nil, response.NewInternalServerError("can't create invocation script", err)
}
return s.runScriptInVM(script), nil
}
// invokescript implements the `invokescript` RPC call.
func (s *Server) invokeFunction(reqParams request.Params) (interface{}, *response.Error) { func (s *Server) invokeFunction(reqParams request.Params) (interface{}, *response.Error) {
scriptHashHex, ok := reqParams.ValueWithType(0, request.StringT) scriptHashHex, ok := reqParams.ValueWithType(0, request.StringT)
if !ok { if !ok {
@ -889,11 +863,21 @@ func (s *Server) invokeFunction(reqParams request.Params) (interface{}, *respons
if err != nil { if err != nil {
return nil, response.ErrInvalidParams return nil, response.ErrInvalidParams
} }
script, err := request.CreateFunctionInvocationScript(scriptHash, reqParams[1:]) tx := &transaction.Transaction{}
checkWitnessHashesIndex := len(reqParams)
if checkWitnessHashesIndex > 3 {
cosigners, err := reqParams[3].GetCosigners()
if err != nil {
return nil, response.ErrInvalidParams
}
tx.Cosigners = cosigners
checkWitnessHashesIndex--
}
script, err := request.CreateFunctionInvocationScript(scriptHash, reqParams[1:checkWitnessHashesIndex])
if err != nil { if err != nil {
return nil, response.NewInternalServerError("can't create invocation script", err) return nil, response.NewInternalServerError("can't create invocation script", err)
} }
return s.runScriptInVM(script), nil return s.runScriptInVM(script, tx), nil
} }
// invokescript implements the `invokescript` RPC call. // invokescript implements the `invokescript` RPC call.
@ -907,13 +891,21 @@ func (s *Server) invokescript(reqParams request.Params) (interface{}, *response.
return nil, response.ErrInvalidParams return nil, response.ErrInvalidParams
} }
return s.runScriptInVM(script), nil tx := &transaction.Transaction{}
if len(reqParams) > 1 {
cosigners, err := reqParams[1].GetCosigners()
if err != nil {
return nil, response.ErrInvalidParams
}
tx.Cosigners = cosigners
}
return s.runScriptInVM(script, tx), nil
} }
// runScriptInVM runs given script in a new test VM and returns the invocation // runScriptInVM runs given script in a new test VM and returns the invocation
// result. // result.
func (s *Server) runScriptInVM(script []byte) *result.Invoke { func (s *Server) runScriptInVM(script []byte, tx *transaction.Transaction) *result.Invoke {
vm := s.chain.GetTestVM() vm := s.chain.GetTestVM(tx)
vm.SetGasLimit(s.config.MaxGasInvoke) vm.SetGasLimit(s.config.MaxGasInvoke)
vm.LoadScriptWithFlags(script, smartcontract.All) vm.LoadScriptWithFlags(script, smartcontract.All)
_ = vm.Run() _ = vm.Run()

View file

@ -572,45 +572,6 @@ var rpcTestCases = map[string][]rpcTestCase{
}, },
}, },
}, },
"invoke": {
{
name: "positive",
params: `["50befd26fdf6e4d957c11e078b24ebce6291456f", [{"type": "String", "value": "qwerty"}]]`,
result: func(e *executor) interface{} { return &result.Invoke{} },
check: func(t *testing.T, e *executor, inv interface{}) {
res, ok := inv.(*result.Invoke)
require.True(t, ok)
assert.Equal(t, "0c067177657274790c146f459162ceeb248b071ec157d9e4f6fd26fdbe5041627d5b52", res.Script)
assert.NotEqual(t, "", res.State)
assert.NotEqual(t, 0, res.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",
@ -658,6 +619,55 @@ var rpcTestCases = map[string][]rpcTestCase{
assert.NotEqual(t, 0, res.GasConsumed) assert.NotEqual(t, 0, res.GasConsumed)
}, },
}, },
{
name: "positive, good witness",
// script is hex-encoded `test_verify.avm` representation, hashes are hex-encoded LE bytes of hashes used in the contract with `0x` prefix
params: `["5707000c14010c030e05060c0d020e0f0d030e070900000000db307068115541f827ec8c21aa270700000011400c140d0f03020900020103070304050201000e060c09db307169115541f827ec8c21aa270700000012401340",["0x0000000009070e030d0f0e020d0c06050e030c01","0x090c060e00010205040307030102000902030f0d"]]`,
result: func(e *executor) interface{} { return &result.Invoke{} },
check: func(t *testing.T, e *executor, inv interface{}) {
res, ok := inv.(*result.Invoke)
require.True(t, ok)
assert.Equal(t, "HALT", res.State)
require.Equal(t, 1, len(res.Stack))
require.Equal(t, int64(3), res.Stack[0].Value)
},
},
{
name: "positive, bad witness of second hash",
params: `["5707000c14010c030e05060c0d020e0f0d030e070900000000db307068115541f827ec8c21aa270700000011400c140d0f03020900020103070304050201000e060c09db307169115541f827ec8c21aa270700000012401340",["0x0000000009070e030d0f0e020d0c06050e030c01"]]`,
result: func(e *executor) interface{} { return &result.Invoke{} },
check: func(t *testing.T, e *executor, inv interface{}) {
res, ok := inv.(*result.Invoke)
require.True(t, ok)
assert.Equal(t, "HALT", res.State)
require.Equal(t, 1, len(res.Stack))
require.Equal(t, int64(2), res.Stack[0].Value)
},
},
{
name: "positive, no good hashes",
params: `["5707000c14010c030e05060c0d020e0f0d030e070900000000db307068115541f827ec8c21aa270700000011400c140d0f03020900020103070304050201000e060c09db307169115541f827ec8c21aa270700000012401340"]`,
result: func(e *executor) interface{} { return &result.Invoke{} },
check: func(t *testing.T, e *executor, inv interface{}) {
res, ok := inv.(*result.Invoke)
require.True(t, ok)
assert.Equal(t, "HALT", res.State)
require.Equal(t, 1, len(res.Stack))
require.Equal(t, int64(1), res.Stack[0].Value)
},
},
{
name: "positive, bad hashes witness",
params: `["5707000c14010c030e05060c0d020e0f0d030e070900000000db307068115541f827ec8c21aa270700000011400c140d0f03020900020103070304050201000e060c09db307169115541f827ec8c21aa270700000012401340",["0x0000000009070e030d0f0e020d0c06050e030c02"]]`,
result: func(e *executor) interface{} { return &result.Invoke{} },
check: func(t *testing.T, e *executor, inv interface{}) {
res, ok := inv.(*result.Invoke)
require.True(t, ok)
assert.Equal(t, "HALT", res.State)
assert.Equal(t, 1, len(res.Stack))
assert.Equal(t, int64(1), res.Stack[0].Value)
},
},
{ {
name: "no params", name: "no params",
params: `[]`, params: `[]`,

BIN
pkg/rpc/server/testdata/test_verify.avm vendored Executable file

Binary file not shown.

17
pkg/rpc/server/testdata/test_verify.go vendored Normal file
View file

@ -0,0 +1,17 @@
package testdata
import "github.com/nspcc-dev/neo-go/pkg/interop/runtime"
// This contract is used to test `invokescript` and `invokefunction` RPC-calls
func Main() int {
// h1 and h2 are just random uint160 hashes
h1 := []byte{1, 12, 3, 14, 5, 6, 12, 13, 2, 14, 15, 13, 3, 14, 7, 9, 0, 0, 0, 0}
if !runtime.CheckWitness(h1) {
return 1
}
h2 := []byte{13, 15, 3, 2, 9, 0, 2, 1, 3, 7, 3, 4, 5, 2, 1, 0, 14, 6, 12, 9}
if !runtime.CheckWitness(h2) {
return 2
}
return 3
}