diff --git a/cli/smartcontract/permission.go b/cli/smartcontract/permission.go new file mode 100644 index 000000000..2b53be285 --- /dev/null +++ b/cli/smartcontract/permission.go @@ -0,0 +1,129 @@ +package smartcontract + +import ( + "encoding/hex" + "errors" + "fmt" + + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" + "github.com/nspcc-dev/neo-go/pkg/util" + "gopkg.in/yaml.v2" +) + +type permission manifest.Permission + +const ( + permHashKey = "hash" + permGroupKey = "group" + permMethodKey = "methods" +) + +func (p permission) MarshalYAML() (interface{}, error) { + m := make(yaml.MapSlice, 0, 2) + switch p.Contract.Type { + case manifest.PermissionWildcard: + case manifest.PermissionHash: + m = append(m, yaml.MapItem{ + Key: permHashKey, + Value: p.Contract.Value.(util.Uint160).StringLE(), + }) + case manifest.PermissionGroup: + bs := p.Contract.Value.(*keys.PublicKey).Bytes() + m = append(m, yaml.MapItem{ + Key: permGroupKey, + Value: hex.EncodeToString(bs), + }) + default: + return nil, fmt.Errorf("invalid permission type: %d", p.Contract.Type) + } + + var val interface{} = "*" + if !p.Methods.IsWildcard() { + val = p.Methods.Value + } + + m = append(m, yaml.MapItem{ + Key: permMethodKey, + Value: val, + }) + return m, nil +} + +func (p *permission) UnmarshalYAML(unmarshal func(interface{}) error) error { + var m map[string]interface{} + if err := unmarshal(&m); err != nil { + return err + } + + if err := p.fillType(m); err != nil { + return err + } + + return p.fillMethods(m) +} + +func (p *permission) fillType(m map[string]interface{}) error { + vh, ok1 := m[permHashKey] + vg, ok2 := m[permGroupKey] + switch { + case ok1 && ok2: + return errors.New("permission must have either 'hash' or 'group' field") + case ok1: + s, ok := vh.(string) + if !ok { + return errors.New("invalid 'hash' type") + } + + u, err := util.Uint160DecodeStringLE(s) + if err != nil { + return err + } + + p.Contract.Type = manifest.PermissionHash + p.Contract.Value = u + case ok2: + s, ok := vg.(string) + if !ok { + return errors.New("invalid 'hash' type") + } + + pub, err := keys.NewPublicKeyFromString(s) + if err != nil { + return err + } + + p.Contract.Type = manifest.PermissionGroup + p.Contract.Value = pub + default: + p.Contract.Type = manifest.PermissionWildcard + } + return nil +} + +func (p *permission) fillMethods(m map[string]interface{}) error { + methods, ok := m[permMethodKey] + if !ok { + return errors.New("'methods' field is missing from permission") + } + + switch mt := methods.(type) { + case string: + if mt == "*" { + p.Methods.Value = nil + return nil + } + case []interface{}: + ms := make([]string, len(mt)) + for i := range mt { + ms[i], ok = mt[i].(string) + if !ok { + return errors.New("invalid permission method name") + } + } + p.Methods.Value = ms + return nil + default: + } + return errors.New("'methods' field is invalid") +} diff --git a/cli/smartcontract/smart_contract.go b/cli/smartcontract/smart_contract.go index c45194288..2b9d2c557 100644 --- a/cli/smartcontract/smart_contract.go +++ b/cli/smartcontract/smart_contract.go @@ -401,6 +401,7 @@ func initSmartContract(ctx *cli.Context) error { }, }, }, + Permissions: []permission{permission(*manifest.NewPermission(manifest.PermissionWildcard))}, } b, err := yaml.Marshal(m) if err != nil { @@ -450,6 +451,10 @@ func contractCompile(ctx *cli.Context) error { o.Name = conf.Name o.ContractEvents = conf.Events o.ContractSupportedStandards = conf.SupportedStandards + o.Permissions = make([]manifest.Permission, len(conf.Permissions)) + for i := range conf.Permissions { + o.Permissions[i] = manifest.Permission(conf.Permissions[i]) + } o.SafeMethods = conf.SafeMethods } @@ -674,6 +679,7 @@ type ProjectConfig struct { SafeMethods []string SupportedStandards []string Events []manifest.Event + Permissions []permission } func inspect(ctx *cli.Context) error { diff --git a/cli/smartcontract/smart_contract_test.go b/cli/smartcontract/smart_contract_test.go index a85b8eff2..48a111a74 100644 --- a/cli/smartcontract/smart_contract_test.go +++ b/cli/smartcontract/smart_contract_test.go @@ -1,13 +1,18 @@ package smartcontract import ( + "encoding/hex" "flag" "io/ioutil" "os" "testing" + "github.com/nspcc-dev/neo-go/internal/random" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" "github.com/stretchr/testify/require" "github.com/urfave/cli" + "gopkg.in/yaml.v2" ) func TestInitSmartContract(t *testing.T) { @@ -64,5 +69,73 @@ events: parameters: - name: args type: Array +permissions: +- methods: '*' `, string(manifest)) } + +func testPermissionMarshal(t *testing.T, p *manifest.Permission, expected string) { + out, err := yaml.Marshal((*permission)(p)) + require.NoError(t, err) + require.Equal(t, expected, string(out)) + + t.Run("Unmarshal", func(t *testing.T) { + actual := new(permission) + require.NoError(t, yaml.Unmarshal(out, actual)) + require.Equal(t, p, (*manifest.Permission)(actual)) + }) +} + +func TestPermissionMarshal(t *testing.T) { + t.Run("Wildcard", func(t *testing.T) { + p := manifest.NewPermission(manifest.PermissionWildcard) + testPermissionMarshal(t, p, "methods: '*'\n") + }) + t.Run("no allowed methods", func(t *testing.T) { + p := manifest.NewPermission(manifest.PermissionWildcard) + p.Methods.Restrict() + testPermissionMarshal(t, p, "methods: []\n") + }) + t.Run("hash", func(t *testing.T) { + h := random.Uint160() + p := manifest.NewPermission(manifest.PermissionHash, h) + testPermissionMarshal(t, p, + "hash: "+h.StringLE()+"\n"+ + "methods: '*'\n") + }) + t.Run("group with some methods", func(t *testing.T) { + priv, err := keys.NewPrivateKey() + require.NoError(t, err) + + p := manifest.NewPermission(manifest.PermissionGroup, priv.PublicKey()) + p.Methods.Add("abc") + p.Methods.Add("lamao") + testPermissionMarshal(t, p, + "group: "+hex.EncodeToString(priv.PublicKey().Bytes())+"\n"+ + "methods:\n- abc\n- lamao\n") + }) +} + +func TestPermissionUnmarshalInvalid(t *testing.T) { + priv, err := keys.NewPrivateKey() + require.NoError(t, err) + + pub := hex.EncodeToString(priv.PublicKey().Bytes()) + u160 := random.Uint160().StringLE() + testCases := []string{ + "hash: []\nmethods: '*'\n", // invalid hash type + "hash: notahex\nmethods: '*'\n", // invalid hash + "group: []\nmethods: '*'\n", // invalid group type + "group: notahex\nmethods: '*'\n", // invalid group + "hash: " + u160 + "\n", // missing methods + "group: " + pub + "\nhash: " + u160 + "\nmethods: '*'", // hash/group conflict + "hash: " + u160 + "\nmethods:\n a: b\n", // invalid methods type + "hash: " + u160 + "\nmethods:\n- []\n", // methods array, invalid single + } + + for _, tc := range testCases { + t.Run(tc, func(t *testing.T) { + require.Error(t, yaml.Unmarshal([]byte(tc), new(permission))) + }) + } +} diff --git a/cli/testdata/deploy/neo-go.yml b/cli/testdata/deploy/neo-go.yml index c38e0355b..71987e5a3 100644 --- a/cli/testdata/deploy/neo-go.yml +++ b/cli/testdata/deploy/neo-go.yml @@ -1 +1,4 @@ name: Test deploy +permissions: + - hash: fffdc93764dbaddd97c48f252a53ea4643faa3fd + methods: ["update"] diff --git a/examples/nft-nd-nns/nns.yml b/examples/nft-nd-nns/nns.yml index 79e483ce8..621bc32cc 100644 --- a/examples/nft-nd-nns/nns.yml +++ b/examples/nft-nd-nns/nns.yml @@ -13,3 +13,5 @@ events: type: Integer - name: tokenId type: ByteArray +permissions: + - methods: ["onNEP11Transfer"] diff --git a/examples/nft-nd/nft.yml b/examples/nft-nd/nft.yml index 8b7682598..e8ce679f2 100644 --- a/examples/nft-nd/nft.yml +++ b/examples/nft-nd/nft.yml @@ -12,3 +12,5 @@ events: type: Integer - name: tokenId type: ByteArray +permissions: + - methods: ["onNEP11Transfer"] \ No newline at end of file diff --git a/internal/testchain/transaction.go b/internal/testchain/transaction.go index bf8cfc685..7059affc3 100644 --- a/internal/testchain/transaction.go +++ b/internal/testchain/transaction.go @@ -16,6 +16,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/crypto/hash" "github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" "github.com/nspcc-dev/neo-go/pkg/smartcontract/nef" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm/emit" @@ -78,6 +79,10 @@ func NewDeployTx(bc blockchainer.Blockchainer, name string, sender util.Uint160, o.Name = conf.Name o.ContractEvents = conf.Events o.ContractSupportedStandards = conf.SupportedStandards + o.Permissions = make([]manifest.Permission, len(conf.Permissions)) + for i := range conf.Permissions { + o.Permissions[i] = manifest.Permission(conf.Permissions[i]) + } o.SafeMethods = conf.SafeMethods } m, err := compiler.CreateManifest(di, o) diff --git a/pkg/compiler/compiler.go b/pkg/compiler/compiler.go index 867e70021..01e99a14a 100644 --- a/pkg/compiler/compiler.go +++ b/pkg/compiler/compiler.go @@ -54,6 +54,9 @@ type Options struct { // SafeMethods contains list of methods which will be marked as safe in manifest. SafeMethods []string + + // Permissions is a list of permissions for every contract method. + Permissions []manifest.Permission } type buildInfo struct { diff --git a/pkg/compiler/debug.go b/pkg/compiler/debug.go index 644ac725e..b74fb8cb0 100644 --- a/pkg/compiler/debug.go +++ b/pkg/compiler/debug.go @@ -441,13 +441,6 @@ func (di *DebugInfo) ConvertToManifest(o *Options) (*manifest.Manifest, error) { if result.ABI.Events == nil { result.ABI.Events = make([]manifest.Event, 0) } - result.Permissions = []manifest.Permission{ - { - Contract: manifest.PermissionDesc{ - Type: manifest.PermissionWildcard, - }, - Methods: manifest.WildStrings{}, - }, - } + result.Permissions = o.Permissions return result, nil } diff --git a/pkg/compiler/debug_test.go b/pkg/compiler/debug_test.go index fbcef22d3..efc7efb21 100644 --- a/pkg/compiler/debug_test.go +++ b/pkg/compiler/debug_test.go @@ -175,7 +175,14 @@ func _deploy(data interface{}, isUpdate bool) { x := 1; _ = x } } t.Run("convert to Manifest", func(t *testing.T) { - actual, err := d.ConvertToManifest(&Options{Name: "MyCTR", SafeMethods: []string{"methodInt", "methodString"}}) + p := manifest.NewPermission(manifest.PermissionWildcard) + p.Methods.Add("randomMethod") + + actual, err := d.ConvertToManifest(&Options{ + Name: "MyCTR", + SafeMethods: []string{"methodInt", "methodString"}, + Permissions: []manifest.Permission{*p}, + }) require.NoError(t, err) expected := &manifest.Manifest{ Name: "MyCTR", @@ -267,15 +274,8 @@ func _deploy(data interface{}, isUpdate bool) { x := 1; _ = x } }, Events: []manifest.Event{}, }, - Groups: []manifest.Group{}, - Permissions: []manifest.Permission{ - { - Contract: manifest.PermissionDesc{ - Type: manifest.PermissionWildcard, - }, - Methods: manifest.WildStrings{}, - }, - }, + Groups: []manifest.Group{}, + Permissions: []manifest.Permission{*p}, Trusts: manifest.WildPermissionDescs{ Value: []manifest.PermissionDesc{}, }, diff --git a/pkg/compiler/interop_test.go b/pkg/compiler/interop_test.go index d097be567..165247b03 100644 --- a/pkg/compiler/interop_test.go +++ b/pkg/compiler/interop_test.go @@ -18,6 +18,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/encoding/address" cinterop "github.com/nspcc-dev/neo-go/pkg/interop" "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" "github.com/nspcc-dev/neo-go/pkg/smartcontract/nef" "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" "github.com/nspcc-dev/neo-go/pkg/util" @@ -157,7 +158,12 @@ func TestAppCall(t *testing.T) { inner, di, err := compiler.CompileWithDebugInfo("foo.go", strings.NewReader(srcInner)) require.NoError(t, err) - m, err := di.ConvertToManifest(&compiler.Options{Name: "Foo"}) + m, err := di.ConvertToManifest(&compiler.Options{ + Name: "Foo", + Permissions: []manifest.Permission{ + *manifest.NewPermission(manifest.PermissionWildcard), + }, + }) require.NoError(t, err) ih := hash.Hash160(inner)