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/nspcc-dev/neo-go/cli/flags"
"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/encoding/address"
"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{}) {
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.
@ -132,31 +136,14 @@ func NewCommands() []cli.Command {
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",
Usage: "invoke deployed contract on the blockchain",
UsageText: "neo-go contract invokefunction -e endpoint -w wallet [-a address] [-g gas] scripthash [method] [arguments...]",
Description: `Executes given (as a script hash) deployed script with the given method and
and arguments. See testinvokefunction documentation for the details about
parameters. It differs from testinvokefunction in that this command sends an
invocation transaction to the network.
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,
arguments and cosigners. See testinvokefunction documentation for the details
about parameters. It differs from testinvokefunction in that this command
sends an invocation transaction to the network.
`,
Action: invokeFunction,
Flags: []cli.Flag{
@ -166,36 +153,17 @@ func NewCommands() []cli.Command {
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",
Usage: "invoke deployed contract on the blockchain (test mode)",
UsageText: "neo-go contract testinvokefunction -e endpoint scripthash [method] [arguments...]",
Description: `Executes given (as a script hash) deployed script with the given method and
arguments. If no method is given "" is passed to the script, if no arguments
are given, an empty array is passed. All of the given arguments are
encapsulated into array before invoking the script. The script thus should
follow the regular convention of smart contract arguments (method string and
an array of other 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,
arguments and cosigners. If no method is given "" is passed to the script, if
no arguments are given, an empty array is passed, if no cosigners are given,
no array will be passed. All of the given arguments are encapsulated into
array before invoking the script. The script thus should follow the regular
convention of smart contract arguments (method string and an array of other
arguments).
Arguments always do have regular Neo smart contract parameter types, either
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'
* '03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c' is a
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,
Flags: []cli.Flag{
@ -258,8 +252,12 @@ func NewCommands() []cli.Command {
},
},
{
Name: "testinvokescript",
Usage: "Invoke compiled AVM code on the blockchain (test mode, not creating a transaction for it)",
Name: "testinvokescript",
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,
Flags: []cli.Flag{
endpointFlag,
@ -388,31 +386,25 @@ func contractCompile(ctx *cli.Context) error {
return nil
}
func testInvoke(ctx *cli.Context) error {
return invokeInternal(ctx, false, false)
}
func testInvokeFunction(ctx *cli.Context) error {
return invokeInternal(ctx, true, false)
}
func invoke(ctx *cli.Context) error {
return invokeInternal(ctx, false, true)
return invokeInternal(ctx, false)
}
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 (
err error
gas util.Fixed8
operation string
params = make([]smartcontract.Parameter, 0)
paramsStart = 1
resp *result.Invoke
acc *wallet.Account
err error
gas util.Fixed8
operation string
params = make([]smartcontract.Parameter, 0)
paramsStart = 1
cosigners []transaction.Cosigner
cosignersStart = 0
resp *result.Invoke
acc *wallet.Account
)
endpoint := ctx.String("endpoint")
@ -425,15 +417,19 @@ func invokeInternal(ctx *cli.Context, withMethod bool, signAndPush bool) error {
return cli.NewExitError(errNoScriptHash, 1)
}
script := args[0]
if withMethod {
if len(args) <= 1 {
return cli.NewExitError(errNoMethod, 1)
}
operation = args[1]
paramsStart++
if len(args) <= 1 {
return cli.NewExitError(errNoMethod, 1)
}
operation = args[1]
paramsStart++
if len(args) > paramsStart {
for k, s := range args[paramsStart:] {
if s == cosignersSeparator {
cosignersStart = paramsStart + k + 1
break
}
param, err := smartcontract.NewParameterFromString(s)
if err != nil {
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 {
gas = flags.Fixed8FromContext(ctx, "gas")
acc, err = getAccFromContext(ctx)
@ -454,11 +460,7 @@ func invokeInternal(ctx *cli.Context, withMethod bool, signAndPush bool) error {
return cli.NewExitError(err, 1)
}
if withMethod {
resp, err = c.InvokeFunction(script, operation, params)
} else {
resp, err = c.Invoke(script, params)
}
resp, err = c.InvokeFunction(script, operation, params, cosigners)
if err != nil {
return cli.NewExitError(err, 1)
}
@ -502,13 +504,25 @@ func testInvokeScript(ctx *cli.Context) error {
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{})
if err != nil {
return cli.NewExitError(err, 1)
}
scriptHex := hex.EncodeToString(b)
resp, err := c.InvokeScript(scriptHex)
resp, err := c.InvokeScript(scriptHex, cosigners)
if err != nil {
return cli.NewExitError(err, 1)
}
@ -677,3 +691,26 @@ func parseContractConfig(confFile string) (ProjectConfig, error) {
}
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.
func (bc *Blockchain) GetTestVM() *vm.VM {
systemInterop := bc.newInteropContext(trigger.Application, bc.dao, nil, nil)
func (bc *Blockchain) GetTestVM(tx *transaction.Transaction) *vm.VM {
systemInterop := bc.newInteropContext(trigger.Application, bc.dao, nil, tx)
vm := SpawnVM(systemInterop)
vm.SetPriceGetter(getPrice)
return vm

View file

@ -41,7 +41,7 @@ type Blockchainer interface {
GetScriptHashesForVerifying(*transaction.Transaction) ([]util.Uint160, error)
GetStorageItem(scripthash util.Uint160, key []byte) *state.StorageItem
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)
mempool.Feer // fee interface
PoolTx(*transaction.Transaction) error

View file

@ -1,5 +1,10 @@
package transaction
import (
"fmt"
"strings"
)
// WitnessScope represents set of witness flags for Transaction cosigner.
type WitnessScope byte
@ -17,3 +22,34 @@ const (
// CustomGroups define custom pubkey for group members.
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 {
panic("TODO")
}
func (chain testChain) GetTestVM() *vm.VM {
func (chain testChain) GetTestVM(tx *transaction.Transaction) *vm.VM {
panic("TODO")
}
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.
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 {
return 0, err
} 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.
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 {
return "", err
} 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.
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 {
return "", err
} 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.
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 {
return 0, err
} 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.
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 {
return 0, err
} 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 {
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.
// 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 (
params = request.NewRawParams(script)
params request.RawParams
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 {
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
// with the given operation and parameters.
// 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 (
p = request.NewRawParams(script, operation, params)
p request.RawParams
resp = &result.Invoke{}
)
if err := c.performRequest("invokefunction", p, resp); err != nil {
return nil, err
if cosigners != nil {
p = request.NewRawParams(script, operation, params, cosigners)
} else {
p = request.NewRawParams(script, operation, params)
}
return resp, 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 {
if err := c.performRequest("invokefunction", p, resp); err != nil {
return nil, err
}
return resp, nil

View file

@ -563,7 +563,9 @@ var rpcClientTestCases = map[string][]rpcClientTestCase{
Type: smartcontract.Hash160Type,
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"}}`,
result: func(c *Client) interface{} {
@ -589,7 +591,9 @@ var rpcClientTestCases = map[string][]rpcClientTestCase{
{
name: "positive",
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"}}`,
result: func(c *Client) interface{} {
@ -874,13 +878,13 @@ var rpcClientErrorCases = map[string][]rpcClientErrorCase{
{
name: "invokefunction_invalid_params_error",
invoke: func(c *Client) (interface{}, error) {
return c.InvokeFunction("", "", []smartcontract.Parameter{})
return c.InvokeFunction("", "", []smartcontract.Parameter{}, nil)
},
},
{
name: "invokescript_invalid_params_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",
invoke: func(c *Client) (interface{}, error) {
return c.InvokeFunction("", "", []smartcontract.Parameter{})
return c.InvokeFunction("", "", []smartcontract.Parameter{}, nil)
},
},
{
name: "invokescript_unmarshalling_error",
invoke: func(c *Client) (interface{}, error) {
return c.InvokeScript("")
return c.InvokeScript("", nil)
},
},
{

View file

@ -7,6 +7,7 @@ import (
"fmt"
"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/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/util"
@ -65,6 +66,7 @@ const (
TxFilterT
NotificationFilterT
ExecutionFilterT
Cosigner
)
func (p Param) String() string {
@ -154,6 +156,47 @@ func (p Param) GetBytesHex() ([]byte, error) {
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.
func (p *Param) UnmarshalJSON(data []byte) error {
var s string
@ -167,6 +210,7 @@ func (p *Param) UnmarshalJSON(data []byte) error {
{TxFilterT, &TxFilter{}},
{NotificationFilterT, &NotificationFilter{}},
{ExecutionFilterT, &ExecutionFilter{}},
{Cosigner, &transaction.Cosigner{}},
{ArrayT, &[]Param{}},
}
@ -196,6 +240,8 @@ func (p *Param) UnmarshalJSON(data []byte) error {
} else {
continue
}
case *transaction.Cosigner:
p.Value = *val
case *[]Param:
p.Value = *val
}

View file

@ -5,6 +5,7 @@ import (
"encoding/json"
"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/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/util"
@ -19,9 +20,13 @@ func TestParam_UnmarshalJSON(t *testing.T) {
{"cosigner": "f84d6a337fbc3d3a201d41da99e86b479e7a2554"},
{"sender": "f84d6a337fbc3d3a201d41da99e86b479e7a2554", "cosigner": "f84d6a337fbc3d3a201d41da99e86b479e7a2554"},
{"contract": "f84d6a337fbc3d3a201d41da99e86b479e7a2554"},
{"state": "HALT"}]`
{"state": "HALT"},
{"account": "0xcadb3dc2faa3ef14a13b619c9a43124755aa2569", "scopes": 0},
[{"account": "0xcadb3dc2faa3ef14a13b619c9a43124755aa2569", "scopes": 0}]]`
contr, err := util.Uint160DecodeStringLE("f84d6a337fbc3d3a201d41da99e86b479e7a2554")
require.NoError(t, err)
accountHash, err := util.Uint160DecodeStringLE("cadb3dc2faa3ef14a13b619c9a43124755aa2569")
require.NoError(t, err)
expected := Params{
{
Type: StringT,
@ -83,6 +88,25 @@ func TestParam_UnmarshalJSON(t *testing.T) {
Type: ExecutionFilterT,
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
@ -214,3 +238,67 @@ func TestParamGetBytesHex(t *testing.T) {
_, err = p.GetBytesHex()
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,
"getvalidators": (*Server).getValidators,
"getversion": (*Server).getVersion,
"invoke": (*Server).invoke,
"invokefunction": (*Server).invokeFunction,
"invokescript": (*Server).invokescript,
"sendrawtransaction": (*Server).sendrawtransaction,
@ -619,7 +618,7 @@ func (s *Server) getDecimals(h util.Uint160, cache map[util.Uint160]int64) (int6
if err != nil {
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 {
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
}
// invoke implements the `invoke` 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.
// invokeFunction implements the `invokeFunction` RPC call.
func (s *Server) invokeFunction(reqParams request.Params) (interface{}, *response.Error) {
scriptHashHex, ok := reqParams.ValueWithType(0, request.StringT)
if !ok {
@ -889,11 +863,21 @@ func (s *Server) invokeFunction(reqParams request.Params) (interface{}, *respons
if err != nil {
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 {
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.
@ -907,13 +891,21 @@ func (s *Server) invokescript(reqParams request.Params) (interface{}, *response.
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
// result.
func (s *Server) runScriptInVM(script []byte) *result.Invoke {
vm := s.chain.GetTestVM()
func (s *Server) runScriptInVM(script []byte, tx *transaction.Transaction) *result.Invoke {
vm := s.chain.GetTestVM(tx)
vm.SetGasLimit(s.config.MaxGasInvoke)
vm.LoadScriptWithFlags(script, smartcontract.All)
_ = 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": {
{
name: "positive",
@ -658,6 +619,55 @@ var rpcTestCases = map[string][]rpcTestCase{
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",
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
}