diff --git a/cli/smartcontract/smart_contract.go b/cli/smartcontract/smart_contract.go index e22b986ac..cc7be5a0d 100644 --- a/cli/smartcontract/smart_contract.go +++ b/cli/smartcontract/smart_contract.go @@ -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 +} diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index c1fcb4385..51462f5c5 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -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 diff --git a/pkg/core/blockchainer/blockchainer.go b/pkg/core/blockchainer/blockchainer.go index 5b84826b4..2a9664246 100644 --- a/pkg/core/blockchainer/blockchainer.go +++ b/pkg/core/blockchainer/blockchainer.go @@ -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 diff --git a/pkg/core/transaction/witness_scope.go b/pkg/core/transaction/witness_scope.go index 751347148..2695cc9e7 100644 --- a/pkg/core/transaction/witness_scope.go +++ b/pkg/core/transaction/witness_scope.go @@ -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 +} diff --git a/pkg/core/transaction/witness_scope_test.go b/pkg/core/transaction/witness_scope_test.go new file mode 100644 index 000000000..321465a10 --- /dev/null +++ b/pkg/core/transaction/witness_scope_test.go @@ -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) +} diff --git a/pkg/network/helper_test.go b/pkg/network/helper_test.go index fe60dac6d..70ad94bb8 100644 --- a/pkg/network/helper_test.go +++ b/pkg/network/helper_test.go @@ -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) { diff --git a/pkg/rpc/client/nep5.go b/pkg/rpc/client/nep5.go index ee06343e3..b00622468 100644 --- a/pkg/rpc/client/nep5.go +++ b/pkg/rpc/client/nep5.go @@ -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) } diff --git a/pkg/rpc/client/rpc.go b/pkg/rpc/client/rpc.go index 81add1b93..cb80a67ef 100644 --- a/pkg/rpc/client/rpc.go +++ b/pkg/rpc/client/rpc.go @@ -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 diff --git a/pkg/rpc/client/rpc_test.go b/pkg/rpc/client/rpc_test.go index eccbdb99b..e34bc2ce2 100644 --- a/pkg/rpc/client/rpc_test.go +++ b/pkg/rpc/client/rpc_test.go @@ -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) }, }, { diff --git a/pkg/rpc/request/param.go b/pkg/rpc/request/param.go index 3f5f9acfa..87af906fc 100644 --- a/pkg/rpc/request/param.go +++ b/pkg/rpc/request/param.go @@ -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 } diff --git a/pkg/rpc/request/param_test.go b/pkg/rpc/request/param_test.go index 032173446..e6ada2ced 100644 --- a/pkg/rpc/request/param_test.go +++ b/pkg/rpc/request/param_test.go @@ -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) + }) +} diff --git a/pkg/rpc/server/server.go b/pkg/rpc/server/server.go index 009f097af..1e05c32d7 100644 --- a/pkg/rpc/server/server.go +++ b/pkg/rpc/server/server.go @@ -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() diff --git a/pkg/rpc/server/server_test.go b/pkg/rpc/server/server_test.go index 2d502f960..60437d2b1 100644 --- a/pkg/rpc/server/server_test.go +++ b/pkg/rpc/server/server_test.go @@ -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: `[]`, diff --git a/pkg/rpc/server/testdata/test_verify.avm b/pkg/rpc/server/testdata/test_verify.avm new file mode 100755 index 000000000..100b20621 Binary files /dev/null and b/pkg/rpc/server/testdata/test_verify.avm differ diff --git a/pkg/rpc/server/testdata/test_verify.go b/pkg/rpc/server/testdata/test_verify.go new file mode 100644 index 000000000..1587e14da --- /dev/null +++ b/pkg/rpc/server/testdata/test_verify.go @@ -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 +}