diff --git a/cli/smartcontract/smart_contract.go b/cli/smartcontract/smart_contract.go index fc276cb84..8882543db 100644 --- a/cli/smartcontract/smart_contract.go +++ b/cli/smartcontract/smart_contract.go @@ -16,6 +16,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. @@ -135,11 +139,11 @@ func NewCommands() []cli.Command { { 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{ @@ -152,13 +156,14 @@ func NewCommands() []cli.Command { { 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 @@ -214,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{ @@ -221,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, @@ -362,13 +397,15 @@ func invokeFunction(ctx *cli.Context) 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") @@ -390,6 +427,10 @@ func invokeInternal(ctx *cli.Context, signAndPush bool) error { 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) @@ -398,6 +439,16 @@ func invokeInternal(ctx *cli.Context, 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) @@ -410,7 +461,7 @@ func invokeInternal(ctx *cli.Context, signAndPush bool) error { return cli.NewExitError(err, 1) } - resp, err = c.InvokeFunction(script, operation, params) + resp, err = c.InvokeFunction(script, operation, params, cosigners) if err != nil { return cli.NewExitError(err, 1) } @@ -454,13 +505,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) } @@ -625,3 +688,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/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/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 9d4d41eec..9e26e2809 100644 --- a/pkg/rpc/client/rpc.go +++ b/pkg/rpc/client/rpc.go @@ -338,11 +338,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 } @@ -352,11 +357,16 @@ 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 cosigners != nil { + p = request.NewRawParams(script, operation, params, cosigners) + } else { + p = request.NewRawParams(script, operation, params) + } if err := c.performRequest("invokefunction", p, resp); err != nil { return nil, err } diff --git a/pkg/rpc/client/rpc_test.go b/pkg/rpc/client/rpc_test.go index c3c955d12..aefcde608 100644 --- a/pkg/rpc/client/rpc_test.go +++ b/pkg/rpc/client/rpc_test.go @@ -565,7 +565,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{} { @@ -591,7 +593,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{} { @@ -876,13 +880,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) }, }, { @@ -1052,13 +1056,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) }, }, {