From dd38e3ec3bd3c47473224790faffb85af069e97f Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Wed, 18 Mar 2020 18:21:12 +0300 Subject: [PATCH 1/8] smartcontract: add smartcontract manifest Manifest contains all of smartcontract's metadata including parameters, return value, permissions etc. --- pkg/smartcontract/manifest/container.go | 106 +++++++++++ pkg/smartcontract/manifest/container_test.go | 112 ++++++++++++ pkg/smartcontract/manifest/manifest.go | 128 +++++++++++++ pkg/smartcontract/manifest/manifest_test.go | 121 ++++++++++++ pkg/smartcontract/manifest/method.go | 89 +++++++++ pkg/smartcontract/manifest/permission.go | 173 ++++++++++++++++++ pkg/smartcontract/manifest/permission_test.go | 94 ++++++++++ 7 files changed, 823 insertions(+) create mode 100644 pkg/smartcontract/manifest/container.go create mode 100644 pkg/smartcontract/manifest/container_test.go create mode 100644 pkg/smartcontract/manifest/manifest.go create mode 100644 pkg/smartcontract/manifest/manifest_test.go create mode 100644 pkg/smartcontract/manifest/method.go create mode 100644 pkg/smartcontract/manifest/permission.go create mode 100644 pkg/smartcontract/manifest/permission_test.go diff --git a/pkg/smartcontract/manifest/container.go b/pkg/smartcontract/manifest/container.go new file mode 100644 index 000000000..77f88de5e --- /dev/null +++ b/pkg/smartcontract/manifest/container.go @@ -0,0 +1,106 @@ +package manifest + +// This file contains types and helper methods for wildcard containers. +// Wildcard container can contain either a finite set of elements or +// every possible element, in which case it is named `wildcard`. + +import ( + "bytes" + "encoding/json" + + "github.com/nspcc-dev/neo-go/pkg/util" +) + +// WildStrings represents string set which can be wildcard. +type WildStrings struct { + Value []string +} + +// WildUint160s represents Uint160 set which can be wildcard. +type WildUint160s struct { + Value []util.Uint160 +} + +// Contains checks if v is in the container. +func (c *WildStrings) Contains(v string) bool { + if c.IsWildcard() { + return true + } + for _, s := range c.Value { + if v == s { + return true + } + } + return false +} + +// Contains checks if v is in the container. +func (c *WildUint160s) Contains(v util.Uint160) bool { + if c.IsWildcard() { + return true + } + for _, u := range c.Value { + if u.Equals(v) { + return true + } + } + return false +} + +// IsWildcard returns true iff container is wildcard. +func (c *WildStrings) IsWildcard() bool { return c.Value == nil } + +// IsWildcard returns true iff container is wildcard. +func (c *WildUint160s) IsWildcard() bool { return c.Value == nil } + +// Restrict transforms container into an empty one. +func (c *WildStrings) Restrict() { c.Value = []string{} } + +// Restrict transforms container into an empty one. +func (c *WildUint160s) Restrict() { c.Value = []util.Uint160{} } + +// Add adds v to the container. +func (c *WildStrings) Add(v string) { c.Value = append(c.Value, v) } + +// Add adds v to the container. +func (c *WildUint160s) Add(v util.Uint160) { c.Value = append(c.Value, v) } + +// MarshalJSON implements json.Marshaler interface. +func (c *WildStrings) MarshalJSON() ([]byte, error) { + if c.IsWildcard() { + return []byte(`"*"`), nil + } + return json.Marshal(c.Value) +} + +// MarshalJSON implements json.Marshaler interface. +func (c *WildUint160s) MarshalJSON() ([]byte, error) { + if c.IsWildcard() { + return []byte(`"*"`), nil + } + return json.Marshal(c.Value) +} + +// UnmarshalJSON implements json.Unmarshaler interface. +func (c *WildStrings) UnmarshalJSON(data []byte) error { + if !bytes.Equal(data, []byte(`"*"`)) { + ss := []string{} + if err := json.Unmarshal(data, &ss); err != nil { + return err + } + c.Value = ss + } + return nil +} + +// UnmarshalJSON implements json.Unmarshaler interface. +func (c *WildUint160s) UnmarshalJSON(data []byte) error { + if !bytes.Equal(data, []byte(`"*"`)) { + us := []util.Uint160{} + if err := json.Unmarshal(data, &us); err != nil { + return err + } + c.Value = us + } + return nil +} diff --git a/pkg/smartcontract/manifest/container_test.go b/pkg/smartcontract/manifest/container_test.go new file mode 100644 index 000000000..fd5325737 --- /dev/null +++ b/pkg/smartcontract/manifest/container_test.go @@ -0,0 +1,112 @@ +package manifest + +import ( + "encoding/json" + "testing" + + "github.com/nspcc-dev/neo-go/pkg/internal/random" + "github.com/nspcc-dev/neo-go/pkg/internal/testserdes" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/stretchr/testify/require" +) + +func TestContainer_Restrict(t *testing.T) { + t.Run("string", func(t *testing.T) { + c := new(WildStrings) + require.True(t, c.IsWildcard()) + require.True(t, c.Contains("abc")) + c.Restrict() + require.False(t, c.IsWildcard()) + require.False(t, c.Contains("abc")) + require.Equal(t, 0, len(c.Value)) + }) + + t.Run("uint160", func(t *testing.T) { + c := new(WildUint160s) + u := random.Uint160() + require.True(t, c.IsWildcard()) + require.True(t, c.Contains(u)) + c.Restrict() + require.False(t, c.IsWildcard()) + require.False(t, c.Contains(u)) + require.Equal(t, 0, len(c.Value)) + }) +} + +func TestContainer_Add(t *testing.T) { + t.Run("string", func(t *testing.T) { + c := new(WildStrings) + require.Equal(t, []string(nil), c.Value) + + c.Add("abc") + require.True(t, c.Contains("abc")) + require.False(t, c.Contains("aaa")) + }) + + t.Run("uint160", func(t *testing.T) { + c := new(WildUint160s) + require.Equal(t, []util.Uint160(nil), c.Value) + + exp := []util.Uint160{random.Uint160(), random.Uint160()} + for i := range exp { + c.Add(exp[i]) + } + for i := range exp { + require.True(t, c.Contains(exp[i])) + } + require.False(t, c.Contains(random.Uint160())) + }) +} + +func TestContainer_MarshalJSON(t *testing.T) { + t.Run("string", func(t *testing.T) { + t.Run("wildcard", func(t *testing.T) { + expected := new(WildStrings) + testserdes.MarshalUnmarshalJSON(t, expected, new(WildStrings)) + }) + + t.Run("empty", func(t *testing.T) { + expected := new(WildStrings) + expected.Restrict() + testserdes.MarshalUnmarshalJSON(t, expected, new(WildStrings)) + }) + + t.Run("non-empty", func(t *testing.T) { + expected := new(WildStrings) + expected.Add("string1") + expected.Add("string2") + testserdes.MarshalUnmarshalJSON(t, expected, new(WildStrings)) + }) + + t.Run("invalid", func(t *testing.T) { + js := []byte(`[123]`) + c := new(WildStrings) + require.Error(t, json.Unmarshal(js, c)) + }) + }) + + t.Run("uint160", func(t *testing.T) { + t.Run("wildcard", func(t *testing.T) { + expected := new(WildUint160s) + testserdes.MarshalUnmarshalJSON(t, expected, new(WildUint160s)) + }) + + t.Run("empty", func(t *testing.T) { + expected := new(WildUint160s) + expected.Restrict() + testserdes.MarshalUnmarshalJSON(t, expected, new(WildUint160s)) + }) + + t.Run("non-empty", func(t *testing.T) { + expected := new(WildUint160s) + expected.Add(random.Uint160()) + testserdes.MarshalUnmarshalJSON(t, expected, new(WildUint160s)) + }) + + t.Run("invalid", func(t *testing.T) { + js := []byte(`["notahex"]`) + c := new(WildUint160s) + require.Error(t, json.Unmarshal(js, c)) + }) + }) +} diff --git a/pkg/smartcontract/manifest/manifest.go b/pkg/smartcontract/manifest/manifest.go new file mode 100644 index 000000000..23bf304bc --- /dev/null +++ b/pkg/smartcontract/manifest/manifest.go @@ -0,0 +1,128 @@ +package manifest + +import ( + "encoding/json" + + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/util" +) + +// MaxManifestSize is a max length for a valid contract manifest. +const MaxManifestSize = 2048 + +// ABI represents a contract application binary interface. +type ABI struct { + Hash util.Uint160 `json:"hash"` + EntryPoint Method `json:"entryPoint"` + Methods []Method `json:"methods"` + Events []Event `json:"events"` +} + +// Manifest represens contract metadata. +type Manifest struct { + // ABI is a contract's ABI. + ABI ABI + // Groups is a set of groups to which a contract belongs. + Groups []Group + // Features is a set of contract's features. + Features smartcontract.PropertyState + Permissions []Permission + // Trusts is a set of hashes to a which contract trusts. + Trusts WildUint160s + // SafeMethods is a set of names of safe methods. + SafeMethods WildStrings + // Extra is an implementation-defined user data. + Extra interface{} +} + +type manifestAux struct { + ABI *ABI `json:"abi"` + Groups []Group `json:"groups"` + Features map[string]bool `json:"features"` + Permissions []Permission `json:"permissions"` + Trusts *WildUint160s `json:"trusts"` + SafeMethods *WildStrings `json:"safeMethods"` + Extra interface{} `json:"extra"` +} + +// NewManifest returns new manifest with necessary fields initialized. +func NewManifest(h util.Uint160) *Manifest { + m := &Manifest{ + ABI: ABI{ + Hash: h, + Methods: []Method{}, + Events: []Event{}, + }, + Groups: []Group{}, + Features: smartcontract.NoProperties, + } + m.Trusts.Restrict() + m.SafeMethods.Restrict() + return m +} + +// DefaultManifest returns default contract manifest. +func DefaultManifest(h util.Uint160) *Manifest { + m := NewManifest(h) + m.ABI.EntryPoint = *DefaultEntryPoint() + m.Permissions = []Permission{*NewPermission(PermissionWildcard)} + return m +} + +// CanCall returns true is current contract is allowed to call +// method of another contract. +func (m *Manifest) CanCall(toCall *Manifest, method string) bool { + // this if is not present in the original code but should probably be here + if toCall.SafeMethods.Contains(method) { + return true + } + for i := range m.Permissions { + if m.Permissions[i].IsAllowed(toCall, method) { + return true + } + } + return false +} + +// MarshalJSON implements json.Marshaler interface. +func (m *Manifest) MarshalJSON() ([]byte, error) { + features := make(map[string]bool) + features["storage"] = m.Features&smartcontract.HasStorage != 0 + features["payable"] = m.Features&smartcontract.IsPayable != 0 + aux := &manifestAux{ + ABI: &m.ABI, + Groups: m.Groups, + Features: features, + Permissions: m.Permissions, + Trusts: &m.Trusts, + SafeMethods: &m.SafeMethods, + Extra: m.Extra, + } + return json.Marshal(aux) +} + +// UnmarshalJSON implements json.Unmarshaler interface. +func (m *Manifest) UnmarshalJSON(data []byte) error { + aux := &manifestAux{ + ABI: &m.ABI, + Trusts: &m.Trusts, + SafeMethods: &m.SafeMethods, + } + + if err := json.Unmarshal(data, aux); err != nil { + return err + } + + if aux.Features["storage"] { + m.Features |= smartcontract.HasStorage + } + if aux.Features["payable"] { + m.Features |= smartcontract.IsPayable + } + + m.Groups = aux.Groups + m.Permissions = aux.Permissions + m.Extra = aux.Extra + + return nil +} diff --git a/pkg/smartcontract/manifest/manifest_test.go b/pkg/smartcontract/manifest/manifest_test.go new file mode 100644 index 000000000..f0869e946 --- /dev/null +++ b/pkg/smartcontract/manifest/manifest_test.go @@ -0,0 +1,121 @@ +package manifest + +import ( + "encoding/json" + "testing" + + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/stretchr/testify/require" +) + +// Test vectors are taken from the main NEO repo +// https://github.com/neo-project/neo/blob/master/tests/neo.UnitTests/SmartContract/Manifest/UT_ContractManifest.cs#L10 +func TestManifest_MarshalJSON(t *testing.T) { + t.Run("default", func(t *testing.T) { + s := `{"groups":[],"features":{"storage":false,"payable":false},"abi":{"hash":"0x0000000000000000000000000000000000000000","entryPoint":{"name":"Main","parameters":[{"name":"operation","type":"String"},{"name":"args","type":"Array"}],"returnType":"Any"},"methods":[],"events":[]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"safeMethods":[],"extra":null}` + m := testUnmarshalMarshalManifest(t, s) + require.Equal(t, DefaultManifest(util.Uint160{}), m) + }) + + // this vector is missing from original repo + t.Run("features", func(t *testing.T) { + s := `{"groups":[],"features":{"storage":true,"payable":true},"abi":{"hash":"0x0000000000000000000000000000000000000000","entryPoint":{"name":"Main","parameters":[{"name":"operation","type":"String"},{"name":"args","type":"Array"}],"returnType":"Any"},"methods":[],"events":[]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"safeMethods":[],"extra":null}` + testUnmarshalMarshalManifest(t, s) + }) + + t.Run("permissions", func(t *testing.T) { + s := `{"groups":[],"features":{"storage":false,"payable":false},"abi":{"hash":"0x0000000000000000000000000000000000000000","entryPoint":{"name":"Main","parameters":[{"name":"operation","type":"String"},{"name":"args","type":"Array"}],"returnType":"Any"},"methods":[],"events":[]},"permissions":[{"contract":"0x0000000000000000000000000000000000000000","methods":["method1","method2"]}],"trusts":[],"safeMethods":[],"extra":null}` + testUnmarshalMarshalManifest(t, s) + }) + + t.Run("safe methods", func(t *testing.T) { + s := `{"groups":[],"features":{"storage":false,"payable":false},"abi":{"hash":"0x0000000000000000000000000000000000000000","entryPoint":{"name":"Main","parameters":[{"name":"operation","type":"String"},{"name":"args","type":"Array"}],"returnType":"Any"},"methods":[],"events":[]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"safeMethods":["balanceOf"],"extra":null}` + testUnmarshalMarshalManifest(t, s) + }) + + t.Run("trust", func(t *testing.T) { + s := `{"groups":[],"features":{"storage":false,"payable":false},"abi":{"hash":"0x0000000000000000000000000000000000000000","entryPoint":{"name":"Main","parameters":[{"name":"operation","type":"String"},{"name":"args","type":"Array"}],"returnType":"Any"},"methods":[],"events":[]},"permissions":[{"contract":"*","methods":"*"}],"trusts":["0x0000000000000000000000000000000000000001"],"safeMethods":[],"extra":null}` + testUnmarshalMarshalManifest(t, s) + }) + + t.Run("groups", func(t *testing.T) { + s := `{"groups":[{"pubKey":"03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c","signature":"QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQQ=="}],"features":{"storage":false,"payable":false},"abi":{"hash":"0x0000000000000000000000000000000000000000","entryPoint":{"name":"Main","parameters":[{"name":"operation","type":"String"},{"name":"args","type":"Array"}],"returnType":"Any"},"methods":[],"events":[]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"safeMethods":[],"extra":null}` + testUnmarshalMarshalManifest(t, s) + }) + + t.Run("extra", func(t *testing.T) { + s := `{"groups":[],"features":{"storage":false,"payable":false},"abi":{"hash":"0x0000000000000000000000000000000000000000","entryPoint":{"name":"Main","parameters":[{"name":"operation","type":"String"},{"name":"args","type":"Array"}],"returnType":"Any"},"methods":[],"events":[]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"safeMethods":[],"extra":{"key":"value"}}` + testUnmarshalMarshalManifest(t, s) + }) +} + +func testUnmarshalMarshalManifest(t *testing.T, s string) *Manifest { + js := []byte(s) + c := NewManifest(util.Uint160{}) + require.NoError(t, json.Unmarshal(js, c)) + + data, err := json.Marshal(c) + require.NoError(t, err) + require.JSONEq(t, s, string(data)) + + return c +} + +func TestManifest_CanCall(t *testing.T) { + t.Run("safe methods", func(t *testing.T) { + man1 := NewManifest(util.Uint160{}) + man2 := DefaultManifest(util.Uint160{}) + require.False(t, man1.CanCall(man2, "method1")) + man2.SafeMethods.Add("method1") + require.True(t, man1.CanCall(man2, "method1")) + }) + + t.Run("wildcard permission", func(t *testing.T) { + man1 := DefaultManifest(util.Uint160{}) + man2 := DefaultManifest(util.Uint160{}) + require.True(t, man1.CanCall(man2, "method1")) + }) +} + +func TestPermission_IsAllowed(t *testing.T) { + manifest := DefaultManifest(util.Uint160{}) + + t.Run("wildcard", func(t *testing.T) { + perm := NewPermission(PermissionWildcard) + require.True(t, perm.IsAllowed(manifest, "AAA")) + }) + + t.Run("hash", func(t *testing.T) { + perm := NewPermission(PermissionHash, util.Uint160{}) + require.True(t, perm.IsAllowed(manifest, "AAA")) + + t.Run("restrict methods", func(t *testing.T) { + perm.Methods.Restrict() + require.False(t, perm.IsAllowed(manifest, "AAA")) + perm.Methods.Add("AAA") + require.True(t, perm.IsAllowed(manifest, "AAA")) + }) + }) + + t.Run("invalid hash", func(t *testing.T) { + perm := NewPermission(PermissionHash, util.Uint160{1}) + require.False(t, perm.IsAllowed(manifest, "AAA")) + }) + + priv, err := keys.NewPrivateKey() + require.NoError(t, err) + manifest.Groups = []Group{{PublicKey: priv.PublicKey()}} + + t.Run("group", func(t *testing.T) { + perm := NewPermission(PermissionGroup, priv.PublicKey()) + require.True(t, perm.IsAllowed(manifest, "AAA")) + }) + + t.Run("invalid group", func(t *testing.T) { + priv2, err := keys.NewPrivateKey() + require.NoError(t, err) + perm := NewPermission(PermissionGroup, priv2.PublicKey()) + require.False(t, perm.IsAllowed(manifest, "AAA")) + }) +} diff --git a/pkg/smartcontract/manifest/method.go b/pkg/smartcontract/manifest/method.go new file mode 100644 index 000000000..4a3ada56a --- /dev/null +++ b/pkg/smartcontract/manifest/method.go @@ -0,0 +1,89 @@ +package manifest + +import ( + "encoding/hex" + "encoding/json" + + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" +) + +// Parameter represents smartcontract's parameter's definition. +type Parameter struct { + Name string `json:"name"` + Type smartcontract.ParamType `json:"type"` +} + +// Event is a description of a single event. +type Event struct { + Name string `json:"name"` + Parameters []Parameter `json:"parameters"` +} + +// Group represents a group of smartcontracts identified by a public key. +// Every SC in a group must provide signature of it's hash to prove +// it belongs to a group. +type Group struct { + PublicKey *keys.PublicKey `json:"pubKey"` + Signature []byte `json:"signature"` +} + +type groupAux struct { + PublicKey string `json:"pubKey"` + Signature []byte `json:"signature"` +} + +// Method represents method's metadata. +type Method struct { + Name string `json:"name"` + Parameters []Parameter `json:"parameters"` + ReturnType smartcontract.ParamType `json:"returnType"` +} + +// NewParameter returns new paramter with the specified name and type. +func NewParameter(name string, typ smartcontract.ParamType) Parameter { + return Parameter{ + Name: name, + Type: typ, + } +} + +// DefaultEntryPoint represents default entrypoint to a contract. +func DefaultEntryPoint() *Method { + return &Method{ + Name: "Main", + Parameters: []Parameter{ + NewParameter("operation", smartcontract.StringType), + NewParameter("args", smartcontract.ArrayType), + }, + ReturnType: smartcontract.AnyType, + } +} + +// MarshalJSON implements json.Marshaler interface. +func (g *Group) MarshalJSON() ([]byte, error) { + aux := &groupAux{ + PublicKey: hex.EncodeToString(g.PublicKey.Bytes()), + Signature: g.Signature, + } + return json.Marshal(aux) +} + +// UnmarshalJSON implements json.Unmarshaler interface. +func (g *Group) UnmarshalJSON(data []byte) error { + aux := new(groupAux) + if err := json.Unmarshal(data, aux); err != nil { + return err + } + b, err := hex.DecodeString(aux.PublicKey) + if err != nil { + return err + } + pub := new(keys.PublicKey) + if err := pub.DecodeBytes(b); err != nil { + return err + } + g.PublicKey = pub + g.Signature = aux.Signature + return nil +} diff --git a/pkg/smartcontract/manifest/permission.go b/pkg/smartcontract/manifest/permission.go new file mode 100644 index 000000000..064d24d99 --- /dev/null +++ b/pkg/smartcontract/manifest/permission.go @@ -0,0 +1,173 @@ +package manifest + +import ( + "encoding/hex" + "encoding/json" + "errors" + "fmt" + + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/util" +) + +// PermissionType represents permission type. +type PermissionType uint8 + +const ( + // PermissionWildcard allows everything. + PermissionWildcard PermissionType = 0 + // PermissionHash restricts called contracts based on hash. + PermissionHash PermissionType = 1 + // PermissionGroup restricts called contracts based on public key. + PermissionGroup PermissionType = 2 +) + +// PermissionDesc is a permission descriptor. +type PermissionDesc struct { + Type PermissionType + Value interface{} +} + +// Permission describes which contracts may be invoked and which methods are called. +type Permission struct { + Contract PermissionDesc `json:"contract"` + Methods WildStrings `json:"methods"` +} + +type permissionAux struct { + Contract PermissionDesc `json:"contract"` + Methods WildStrings `json:"methods"` +} + +// NewPermission returns new permission of a given type. +func NewPermission(typ PermissionType, args ...interface{}) *Permission { + return &Permission{ + Contract: *newPermissionDesc(typ, args...), + } +} + +func newPermissionDesc(typ PermissionType, args ...interface{}) *PermissionDesc { + desc := &PermissionDesc{Type: typ} + switch typ { + case PermissionWildcard: + if len(args) != 0 { + panic("wildcard permission has no arguments") + } + case PermissionHash: + if len(args) == 0 { + panic("hash permission should have an argument") + } else if u, ok := args[0].(util.Uint160); !ok { + panic("hash permission should have util.Uint160 argument") + } else { + desc.Value = u + } + case PermissionGroup: + if len(args) == 0 { + panic("group permission should have an argument") + } else if pub, ok := args[0].(*keys.PublicKey); !ok { + panic("group permission should have a public key argument") + } else { + desc.Value = pub + } + } + return desc +} + +// Hash returns hash for hash-permission. +func (d *PermissionDesc) Hash() util.Uint160 { + return d.Value.(util.Uint160) +} + +// Group returns group's public key for group-permission. +func (d *PermissionDesc) Group() *keys.PublicKey { + return d.Value.(*keys.PublicKey) +} + +// IsAllowed checks if method is allowed to be executed. +func (p *Permission) IsAllowed(m *Manifest, method string) bool { + switch p.Contract.Type { + case PermissionWildcard: + return true + case PermissionHash: + if !p.Contract.Hash().Equals(m.ABI.Hash) { + return false + } + case PermissionGroup: + g := p.Contract.Group() + for i := range m.Groups { + if !g.Equal(m.Groups[i].PublicKey) { + return false + } + } + default: + panic(fmt.Sprintf("unexpected permission: %d", p.Contract.Type)) + } + if p.Methods.IsWildcard() { + return true + } + return p.Methods.Contains(method) +} + +// UnmarshalJSON implements json.Unmarshaler interface. +func (p *Permission) UnmarshalJSON(data []byte) error { + aux := new(permissionAux) + if err := json.Unmarshal(data, aux); err != nil { + return err + } + p.Contract = aux.Contract + p.Methods = aux.Methods + return nil +} + +// MarshalJSON implements json.Marshaler interface. +func (d *PermissionDesc) MarshalJSON() ([]byte, error) { + switch d.Type { + case PermissionHash: + return json.Marshal("0x" + d.Hash().StringLE()) + case PermissionGroup: + return json.Marshal(hex.EncodeToString(d.Group().Bytes())) + default: + return []byte(`"*"`), nil + } +} + +// UnmarshalJSON implements json.Unmarshaler interface. +func (d *PermissionDesc) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + + const uint160HexSize = 2 * util.Uint160Size + switch len(s) { + case 2 + uint160HexSize: + // allow to unmarshal both hex and 0xhex forms + if s[0] != '0' || s[1] != 'x' { + return errors.New("invalid uint160") + } + s = s[2:] + fallthrough + case uint160HexSize: + u, err := util.Uint160DecodeStringLE(s) + if err != nil { + return err + } + d.Type = PermissionHash + d.Value = u + return nil + case 66: + pub, err := keys.NewPublicKeyFromString(s) + if err != nil { + return err + } + d.Type = PermissionGroup + d.Value = pub + return nil + case 1: + if s == "*" { + d.Type = PermissionWildcard + return nil + } + } + return errors.New("unknown permission") +} diff --git a/pkg/smartcontract/manifest/permission_test.go b/pkg/smartcontract/manifest/permission_test.go new file mode 100644 index 000000000..1e3e4f8e3 --- /dev/null +++ b/pkg/smartcontract/manifest/permission_test.go @@ -0,0 +1,94 @@ +package manifest + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/internal/random" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/stretchr/testify/require" +) + +func TestNewPermission(t *testing.T) { + require.Panics(t, func() { NewPermission(PermissionWildcard, util.Uint160{}) }) + require.Panics(t, func() { NewPermission(PermissionHash) }) + require.Panics(t, func() { NewPermission(PermissionHash, 1) }) + require.Panics(t, func() { NewPermission(PermissionGroup) }) + require.Panics(t, func() { NewPermission(PermissionGroup, util.Uint160{}) }) +} + +func TestPermission_MarshalJSON(t *testing.T) { + t.Run("wildcard", func(t *testing.T) { + expected := NewPermission(PermissionWildcard) + expected.Methods.Restrict() + testMarshalUnmarshal(t, expected, NewPermission(PermissionWildcard)) + }) + + t.Run("group", func(t *testing.T) { + expected := NewPermission(PermissionWildcard) + expected.Contract.Type = PermissionGroup + priv, err := keys.NewPrivateKey() + require.NoError(t, err) + expected.Contract.Value = priv.PublicKey() + expected.Methods.Add("method1") + expected.Methods.Add("method2") + testMarshalUnmarshal(t, expected, NewPermission(PermissionWildcard)) + }) + + t.Run("hash", func(t *testing.T) { + expected := NewPermission(PermissionWildcard) + expected.Contract.Type = PermissionHash + expected.Contract.Value = random.Uint160() + testMarshalUnmarshal(t, expected, NewPermission(PermissionWildcard)) + }) +} + +func TestPermissionDesc_MarshalJSON(t *testing.T) { + t.Run("uint160 with 0x", func(t *testing.T) { + u := random.Uint160() + s := u.StringLE() + js := []byte(fmt.Sprintf(`"0x%s"`, s)) + d := new(PermissionDesc) + require.NoError(t, json.Unmarshal(js, d)) + require.Equal(t, u, d.Value.(util.Uint160)) + }) + + t.Run("invalid uint160", func(t *testing.T) { + d := new(PermissionDesc) + s := random.String(util.Uint160Size * 2) + js := []byte(fmt.Sprintf(`"ok%s"`, s)) + require.Error(t, json.Unmarshal(js, d)) + + js = []byte(fmt.Sprintf(`"%s"`, s)) + require.Error(t, json.Unmarshal(js, d)) + }) + + t.Run("invalid public key", func(t *testing.T) { + d := new(PermissionDesc) + s := random.String(65) + s = "k" + s // not a hex + js := []byte(fmt.Sprintf(`"%s"`, s)) + require.Error(t, json.Unmarshal(js, d)) + }) + + t.Run("not a string", func(t *testing.T) { + d := new(PermissionDesc) + js := []byte(`123`) + require.Error(t, json.Unmarshal(js, d)) + }) + + t.Run("invalid string", func(t *testing.T) { + d := new(PermissionDesc) + js := []byte(`"invalid length"`) + require.Error(t, json.Unmarshal(js, d)) + }) +} + +func testMarshalUnmarshal(t *testing.T, expected, actual interface{}) { + data, err := json.Marshal(expected) + require.NoError(t, err) + require.NoError(t, json.Unmarshal(data, actual)) + require.Equal(t, expected, actual) +} From be407332b9cf47977ccdba98674a2e9433176e3d Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Thu, 19 Mar 2020 18:21:56 +0300 Subject: [PATCH 2/8] vm: add TryInteger() to StackItem interface Conversion should be done in a StackItem, not in an Element. --- pkg/vm/context.go | 6 ++++++ pkg/vm/stack.go | 16 ++++------------ pkg/vm/stack_item.go | 45 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 12 deletions(-) diff --git a/pkg/vm/context.go b/pkg/vm/context.go index 041df6b8e..3d3717104 100644 --- a/pkg/vm/context.go +++ b/pkg/vm/context.go @@ -3,6 +3,7 @@ package vm import ( "encoding/binary" "errors" + "math/big" "github.com/nspcc-dev/neo-go/pkg/crypto/hash" "github.com/nspcc-dev/neo-go/pkg/smartcontract" @@ -178,6 +179,11 @@ func (c *Context) TryBytes() ([]byte, error) { return nil, errors.New("can't convert Context to ByteArray") } +// TryInteger implements StackItem interface. +func (c *Context) TryInteger() (*big.Int, error) { + return nil, errors.New("can't convert Context to Integer") +} + // Equals implements StackItem interface. func (c *Context) Equals(s StackItem) bool { return c == s diff --git a/pkg/vm/stack.go b/pkg/vm/stack.go index d0f833db0..6730c4456 100644 --- a/pkg/vm/stack.go +++ b/pkg/vm/stack.go @@ -7,7 +7,6 @@ import ( "math/big" "github.com/nspcc-dev/neo-go/pkg/smartcontract" - "github.com/nspcc-dev/neo-go/pkg/vm/emit" ) // Stack implementation for the neo-go virtual machine. The stack implements @@ -74,18 +73,11 @@ func (e *Element) Value() interface{} { // BigInt attempts to get the underlying value of the element as a big integer. // Will panic if the assertion failed which will be caught by the VM. func (e *Element) BigInt() *big.Int { - switch t := e.value.(type) { - case *BigIntegerItem: - return t.value - case *BoolItem: - if t.value { - return big.NewInt(1) - } - return big.NewInt(0) - default: - b := t.Value().([]uint8) - return emit.BytesToInt(b) + val, err := e.value.TryInteger() + if err != nil { + panic(err) } + return val } // TryBool attempts to get the underlying value of the element as a boolean. diff --git a/pkg/vm/stack_item.go b/pkg/vm/stack_item.go index 3500281bd..93fbf9037 100644 --- a/pkg/vm/stack_item.go +++ b/pkg/vm/stack_item.go @@ -22,6 +22,8 @@ type StackItem interface { Dup() StackItem // TryBytes converts StackItem to a byte slice. TryBytes() ([]byte, error) + // TryInteger converts StackItem to an integer. + TryInteger() (*big.Int, error) // Equals checks if 2 StackItems are equal. Equals(s StackItem) bool // ToContractParameter converts StackItem to smartcontract.Parameter @@ -134,6 +136,11 @@ func (i *StructItem) TryBytes() ([]byte, error) { return nil, errors.New("can't convert Struct to ByteArray") } +// TryInteger implements StackItem interface. +func (i *StructItem) TryInteger() (*big.Int, error) { + return nil, errors.New("can't convert Struct to Integer") +} + // Equals implements StackItem interface. func (i *StructItem) Equals(s StackItem) bool { if i == s { @@ -210,6 +217,11 @@ func (i NullItem) TryBytes() ([]byte, error) { return nil, errors.New("can't convert Null to ByteArray") } +// TryInteger implements StackItem interface. +func (i NullItem) TryInteger() (*big.Int, error) { + return nil, errors.New("can't convert Null to Integer") +} + // Equals implements StackItem interface. func (i NullItem) Equals(s StackItem) bool { _, ok := s.(NullItem) @@ -245,6 +257,11 @@ func (i *BigIntegerItem) TryBytes() ([]byte, error) { return i.Bytes(), nil } +// TryInteger implements StackItem interface. +func (i *BigIntegerItem) TryInteger() (*big.Int, error) { + return i.value, nil +} + // Equals implements StackItem interface. func (i *BigIntegerItem) Equals(s StackItem) bool { if i == s { @@ -334,6 +351,14 @@ func (i *BoolItem) TryBytes() ([]byte, error) { return i.Bytes(), nil } +// TryInteger implements StackItem interface. +func (i *BoolItem) TryInteger() (*big.Int, error) { + if i.value { + return big.NewInt(1), nil + } + return big.NewInt(0), nil +} + // Equals implements StackItem interface. func (i *BoolItem) Equals(s StackItem) bool { if i == s { @@ -388,6 +413,11 @@ func (i *ByteArrayItem) TryBytes() ([]byte, error) { return i.value, nil } +// TryInteger implements StackItem interface. +func (i *ByteArrayItem) TryInteger() (*big.Int, error) { + return emit.BytesToInt(i.value), nil +} + // Equals implements StackItem interface. func (i *ByteArrayItem) Equals(s StackItem) bool { if i == s { @@ -445,6 +475,11 @@ func (i *ArrayItem) TryBytes() ([]byte, error) { return nil, errors.New("can't convert Array to ByteArray") } +// TryInteger implements StackItem interface. +func (i *ArrayItem) TryInteger() (*big.Int, error) { + return nil, errors.New("can't convert Array to Integer") +} + // Equals implements StackItem interface. func (i *ArrayItem) Equals(s StackItem) bool { return i == s @@ -505,6 +540,11 @@ func (i *MapItem) TryBytes() ([]byte, error) { return nil, errors.New("can't convert Map to ByteArray") } +// TryInteger implements StackItem interface. +func (i *MapItem) TryInteger() (*big.Int, error) { + return nil, errors.New("can't convert Map to Integer") +} + // Equals implements StackItem interface. func (i *MapItem) Equals(s StackItem) bool { return i == s @@ -616,6 +656,11 @@ func (i *InteropItem) TryBytes() ([]byte, error) { return nil, errors.New("can't convert Interop to ByteArray") } +// TryInteger implements StackItem interface. +func (i *InteropItem) TryInteger() (*big.Int, error) { + return nil, errors.New("can't convert Interop to Integer") +} + // Equals implements StackItem interface. func (i *InteropItem) Equals(s StackItem) bool { if i == s { From b446753c57a2eb1116e0e7e947a77f0c0c1cb4a6 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Mon, 13 Apr 2020 15:37:44 +0300 Subject: [PATCH 3/8] core,vm: move get/putContextScriptHash to `vm` package --- pkg/core/interop_neo.go | 2 +- pkg/core/interop_system.go | 32 ++++++++------------------------ pkg/vm/context.go | 16 ++++++++++++++++ 3 files changed, 25 insertions(+), 25 deletions(-) diff --git a/pkg/core/interop_neo.go b/pkg/core/interop_neo.go index 5a6990f87..31fb8ac28 100644 --- a/pkg/core/interop_neo.go +++ b/pkg/core/interop_neo.go @@ -574,7 +574,7 @@ func contractMigrate(ic *interop.Context, v *vm.VM) error { return err } if contract.HasStorage() { - hash := getContextScriptHash(v, 0) + hash := v.GetContextScriptHash(0) siMap, err := ic.DAO.GetStorageItems(hash) if err != nil { return err diff --git a/pkg/core/interop_system.go b/pkg/core/interop_system.go index e19373b9f..0809e8b6a 100644 --- a/pkg/core/interop_system.go +++ b/pkg/core/interop_system.go @@ -252,35 +252,19 @@ func engineGetScriptContainer(ic *interop.Context, v *vm.VM) error { return nil } -// pushContextScriptHash returns script hash of the invocation stack element -// number n. -func getContextScriptHash(v *vm.VM, n int) util.Uint160 { - ctxIface := v.Istack().Peek(n).Value() - ctx := ctxIface.(*vm.Context) - return ctx.ScriptHash() -} - -// pushContextScriptHash pushes to evaluation stack the script hash of the -// invocation stack element number n. -func pushContextScriptHash(v *vm.VM, n int) error { - h := getContextScriptHash(v, n) - v.Estack().PushVal(h.BytesBE()) - return nil -} - // engineGetExecutingScriptHash returns executing script hash. func engineGetExecutingScriptHash(ic *interop.Context, v *vm.VM) error { - return pushContextScriptHash(v, 0) + return v.PushContextScriptHash(0) } // engineGetCallingScriptHash returns calling script hash. func engineGetCallingScriptHash(ic *interop.Context, v *vm.VM) error { - return pushContextScriptHash(v, 1) + return v.PushContextScriptHash(1) } // engineGetEntryScriptHash returns entry script hash. func engineGetEntryScriptHash(ic *interop.Context, v *vm.VM) error { - return pushContextScriptHash(v, v.Istack().Len()-1) + return v.PushContextScriptHash(v.Istack().Len() - 1) } // runtimePlatform returns the name of the platform. @@ -354,7 +338,7 @@ func runtimeNotify(ic *interop.Context, v *vm.VM) error { if err != nil { item = vm.NewByteArrayItem([]byte(fmt.Sprintf("bad notification: %v", err))) } - ne := state.NotificationEvent{ScriptHash: getContextScriptHash(v, 0), Item: item} + ne := state.NotificationEvent{ScriptHash: v.GetContextScriptHash(0), Item: item} ic.Notifications = append(ic.Notifications, ne) return nil } @@ -363,7 +347,7 @@ func runtimeNotify(ic *interop.Context, v *vm.VM) error { func runtimeLog(ic *interop.Context, v *vm.VM) error { msg := fmt.Sprintf("%q", v.Estack().Pop().Bytes()) ic.Log.Info("runtime log", - zap.Stringer("script", getContextScriptHash(v, 0)), + zap.Stringer("script", v.GetContextScriptHash(0)), zap.String("logs", msg)) return nil } @@ -445,7 +429,7 @@ func storageGet(ic *interop.Context, v *vm.VM) error { // storageGetContext returns storage context (scripthash). func storageGetContext(ic *interop.Context, v *vm.VM) error { sc := &StorageContext{ - ScriptHash: getContextScriptHash(v, 0), + ScriptHash: v.GetContextScriptHash(0), ReadOnly: false, } v.Estack().PushVal(vm.NewInteropItem(sc)) @@ -455,7 +439,7 @@ func storageGetContext(ic *interop.Context, v *vm.VM) error { // storageGetReadOnlyContext returns read-only context (scripthash). func storageGetReadOnlyContext(ic *interop.Context, v *vm.VM) error { sc := &StorageContext{ - ScriptHash: getContextScriptHash(v, 0), + ScriptHash: v.GetContextScriptHash(0), ReadOnly: true, } v.Estack().PushVal(vm.NewInteropItem(sc)) @@ -537,7 +521,7 @@ func contractDestroy(ic *interop.Context, v *vm.VM) error { if ic.Trigger != trigger.Application { return errors.New("can't destroy contract when not triggered by application") } - hash := getContextScriptHash(v, 0) + hash := v.GetContextScriptHash(0) cs, err := ic.DAO.GetContractState(hash) if err != nil { return nil diff --git a/pkg/vm/context.go b/pkg/vm/context.go index 3d3717104..9e4fa63e6 100644 --- a/pkg/vm/context.go +++ b/pkg/vm/context.go @@ -209,3 +209,19 @@ func (c *Context) atBreakPoint() bool { func (c *Context) String() string { return "execution context" } + +// GetContextScriptHash returns script hash of the invocation stack element +// number n. +func (v *VM) GetContextScriptHash(n int) util.Uint160 { + ctxIface := v.Istack().Peek(n).Value() + ctx := ctxIface.(*Context) + return ctx.ScriptHash() +} + +// PushContextScriptHash pushes to evaluation stack the script hash of the +// invocation stack element number n. +func (v *VM) PushContextScriptHash(n int) error { + h := v.GetContextScriptHash(n) + v.Estack().PushVal(h.BytesBE()) + return nil +} From 8a2130f5b62ae75131c59b1cc26836e21a750dad Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Mon, 30 Mar 2020 11:16:53 +0300 Subject: [PATCH 4/8] core: extend Blockchainer with GetStandByValidators() A list of standby validators can be needed in native contracts. --- pkg/core/blockchainer/blockchainer.go | 1 + pkg/network/helper_test.go | 3 +++ 2 files changed, 4 insertions(+) diff --git a/pkg/core/blockchainer/blockchainer.go b/pkg/core/blockchainer/blockchainer.go index 1e368f90f..0393a4059 100644 --- a/pkg/core/blockchainer/blockchainer.go +++ b/pkg/core/blockchainer/blockchainer.go @@ -37,6 +37,7 @@ type Blockchainer interface { GetNEP5TransferLog(util.Uint160) *state.NEP5TransferLog GetNEP5Balances(util.Uint160) *state.NEP5Balances GetValidators(txes ...*transaction.Transaction) ([]*keys.PublicKey, error) + GetStandByValidators() (keys.PublicKeys, error) GetScriptHashesForVerifying(*transaction.Transaction) ([]util.Uint160, error) GetStorageItem(scripthash util.Uint160, key []byte) *state.StorageItem GetStorageItems(hash util.Uint160) (map[string]*state.StorageItem, error) diff --git a/pkg/network/helper_test.go b/pkg/network/helper_test.go index 25e1fed7d..48db395a7 100644 --- a/pkg/network/helper_test.go +++ b/pkg/network/helper_test.go @@ -99,6 +99,9 @@ func (chain testChain) GetNEP5Balances(util.Uint160) *state.NEP5Balances { func (chain testChain) GetValidators(...*transaction.Transaction) ([]*keys.PublicKey, error) { panic("TODO") } +func (chain testChain) GetStandByValidators() (keys.PublicKeys, error) { + panic("TODO") +} func (chain testChain) GetEnrollments() ([]*state.Validator, error) { panic("TODO") } From 9586af32f2da5cfa3153eac5342035f97f74ea43 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Mon, 13 Apr 2020 15:56:41 +0300 Subject: [PATCH 5/8] core: move runtime.CheckWitness interop to a separate package --- pkg/core/interop/runtime/witness.go | 54 +++++++++++++++++++++++++++++ pkg/core/interop_neo.go | 3 +- pkg/core/interop_system.go | 46 ------------------------ pkg/core/interops.go | 7 ++-- 4 files changed, 60 insertions(+), 50 deletions(-) create mode 100644 pkg/core/interop/runtime/witness.go diff --git a/pkg/core/interop/runtime/witness.go b/pkg/core/interop/runtime/witness.go new file mode 100644 index 000000000..1f1dd58f6 --- /dev/null +++ b/pkg/core/interop/runtime/witness.go @@ -0,0 +1,54 @@ +package runtime + +import ( + "github.com/nspcc-dev/neo-go/pkg/core/interop" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm" + "github.com/pkg/errors" +) + +// CheckHashedWitness checks given hash against current list of script hashes +// for verifying in the interop context. +func CheckHashedWitness(ic *interop.Context, hash util.Uint160) (bool, error) { + hashes, err := ic.Chain.GetScriptHashesForVerifying(ic.Tx) + if err != nil { + return false, errors.Wrap(err, "failed to get script hashes") + } + for _, v := range hashes { + if hash.Equals(v) { + return true, nil + } + } + return false, nil +} + +// CheckKeyedWitness checks hash of signature check contract with a given public +// key against current list of script hashes for verifying in the interop context. +func CheckKeyedWitness(ic *interop.Context, key *keys.PublicKey) (bool, error) { + return CheckHashedWitness(ic, key.GetScriptHash()) +} + +// CheckWitness checks witnesses. +func CheckWitness(ic *interop.Context, v *vm.VM) error { + var res bool + var err error + + hashOrKey := v.Estack().Pop().Bytes() + hash, err := util.Uint160DecodeBytesBE(hashOrKey) + if err != nil { + var key *keys.PublicKey + key, err = keys.NewPublicKeyFromBytes(hashOrKey) + if err != nil { + return errors.New("parameter given is neither a key nor a hash") + } + res, err = CheckKeyedWitness(ic, key) + } else { + res, err = CheckHashedWitness(ic, hash) + } + if err != nil { + return errors.Wrap(err, "failed to check") + } + v.Estack().PushVal(res) + return nil +} diff --git a/pkg/core/interop_neo.go b/pkg/core/interop_neo.go index 31fb8ac28..c43b85ccb 100644 --- a/pkg/core/interop_neo.go +++ b/pkg/core/interop_neo.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/nspcc-dev/neo-go/pkg/core/interop" + "github.com/nspcc-dev/neo-go/pkg/core/interop/runtime" "github.com/nspcc-dev/neo-go/pkg/core/state" "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" @@ -635,7 +636,7 @@ func assetCreate(ic *interop.Context, v *vm.VM) error { if owner.IsInfinity() { return errors.New("can't have infinity as an owner key") } - witnessOk, err := checkKeyedWitness(ic, owner) + witnessOk, err := runtime.CheckKeyedWitness(ic, owner) if err != nil { return err } diff --git a/pkg/core/interop_system.go b/pkg/core/interop_system.go index 0809e8b6a..116a65b54 100644 --- a/pkg/core/interop_system.go +++ b/pkg/core/interop_system.go @@ -11,11 +11,9 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/interop" "github.com/nspcc-dev/neo-go/pkg/core/state" "github.com/nspcc-dev/neo-go/pkg/core/transaction" - "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm" - gherr "github.com/pkg/errors" "go.uber.org/zap" ) @@ -279,50 +277,6 @@ func runtimeGetTrigger(ic *interop.Context, v *vm.VM) error { return nil } -// checkHashedWitness checks given hash against current list of script hashes -// for verifying in the interop context. -func checkHashedWitness(ic *interop.Context, hash util.Uint160) (bool, error) { - hashes, err := ic.Chain.GetScriptHashesForVerifying(ic.Tx) - if err != nil { - return false, gherr.Wrap(err, "failed to get script hashes") - } - for _, v := range hashes { - if hash.Equals(v) { - return true, nil - } - } - return false, nil -} - -// checkKeyedWitness checks hash of signature check contract with a given public -// key against current list of script hashes for verifying in the interop context. -func checkKeyedWitness(ic *interop.Context, key *keys.PublicKey) (bool, error) { - return checkHashedWitness(ic, key.GetScriptHash()) -} - -// runtimeCheckWitness checks witnesses. -func runtimeCheckWitness(ic *interop.Context, v *vm.VM) error { - var res bool - var err error - - hashOrKey := v.Estack().Pop().Bytes() - hash, err := util.Uint160DecodeBytesBE(hashOrKey) - if err != nil { - key, err := keys.NewPublicKeyFromBytes(hashOrKey) - if err != nil { - return errors.New("parameter given is neither a key nor a hash") - } - res, err = checkKeyedWitness(ic, key) - } else { - res, err = checkHashedWitness(ic, hash) - } - if err != nil { - return gherr.Wrap(err, "failed to check") - } - v.Estack().PushVal(res) - return nil -} - // runtimeNotify should pass stack item to the notify plugin to handle it, but // in neo-go the only meaningful thing to do here is to log. func runtimeNotify(ic *interop.Context, v *vm.VM) error { diff --git a/pkg/core/interops.go b/pkg/core/interops.go index 5b337957c..3d4d8355c 100644 --- a/pkg/core/interops.go +++ b/pkg/core/interops.go @@ -14,6 +14,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/interop/crypto" "github.com/nspcc-dev/neo-go/pkg/core/interop/enumerator" "github.com/nspcc-dev/neo-go/pkg/core/interop/iterator" + "github.com/nspcc-dev/neo-go/pkg/core/interop/runtime" "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm" @@ -85,7 +86,7 @@ var systemInterops = []interop.Function{ {Name: "System.Header.GetIndex", Func: headerGetIndex, Price: 1}, {Name: "System.Header.GetPrevHash", Func: headerGetPrevHash, Price: 1}, {Name: "System.Header.GetTimestamp", Func: headerGetTimestamp, Price: 1}, - {Name: "System.Runtime.CheckWitness", Func: runtimeCheckWitness, Price: 200}, + {Name: "System.Runtime.CheckWitness", Func: runtime.CheckWitness, Price: 200}, {Name: "System.Runtime.Deserialize", Func: runtimeDeserialize, Price: 1}, {Name: "System.Runtime.GetTime", Func: runtimeGetTime, Price: 1}, {Name: "System.Runtime.GetTrigger", Func: runtimeGetTrigger, Price: 1}, @@ -163,7 +164,7 @@ var neoInterops = []interop.Function{ {Name: "Neo.Output.GetAssetId", Func: outputGetAssetID, Price: 1}, {Name: "Neo.Output.GetScriptHash", Func: outputGetScriptHash, Price: 1}, {Name: "Neo.Output.GetValue", Func: outputGetValue, Price: 1}, - {Name: "Neo.Runtime.CheckWitness", Func: runtimeCheckWitness, Price: 200}, + {Name: "Neo.Runtime.CheckWitness", Func: runtime.CheckWitness, Price: 200}, {Name: "Neo.Runtime.Deserialize", Func: runtimeDeserialize, Price: 1}, {Name: "Neo.Runtime.GetTime", Func: runtimeGetTime, Price: 1}, {Name: "Neo.Runtime.GetTrigger", Func: runtimeGetTrigger, Price: 1}, @@ -235,7 +236,7 @@ var neoInterops = []interop.Function{ {Name: "AntShares.Output.GetAssetId", Func: outputGetAssetID, Price: 1}, {Name: "AntShares.Output.GetScriptHash", Func: outputGetScriptHash, Price: 1}, {Name: "AntShares.Output.GetValue", Func: outputGetValue, Price: 1}, - {Name: "AntShares.Runtime.CheckWitness", Func: runtimeCheckWitness, Price: 200}, + {Name: "AntShares.Runtime.CheckWitness", Func: runtime.CheckWitness, Price: 200}, {Name: "AntShares.Runtime.Log", Func: runtimeLog, Price: 1}, {Name: "AntShares.Runtime.Notify", Func: runtimeNotify, Price: 1}, {Name: "AntShares.Storage.Delete", Func: storageDelete, Price: 100}, From 3831aec53f5670dd5d38b08f8e3beecf41b8a3aa Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Thu, 16 Apr 2020 15:28:34 +0300 Subject: [PATCH 6/8] vm: make NewBigInteger accept *big.Int It creates big.Int internally anyway, so this is the most flexible way. --- pkg/compiler/for_test.go | 10 ++++---- pkg/compiler/struct_test.go | 4 +-- pkg/compiler/syscall_test.go | 3 ++- pkg/vm/cli/cli.go | 3 ++- pkg/vm/stack_item.go | 4 +-- pkg/vm/stack_item_test.go | 50 ++++++++++++++++++------------------ pkg/vm/vm_test.go | 14 +++++----- 7 files changed, 45 insertions(+), 43 deletions(-) diff --git a/pkg/compiler/for_test.go b/pkg/compiler/for_test.go index 34289613e..11d60c507 100644 --- a/pkg/compiler/for_test.go +++ b/pkg/compiler/for_test.go @@ -34,7 +34,7 @@ func TestEntryPointWithArgs(t *testing.T) { return 2 + args[1].(int) } ` - args := []vm.StackItem{vm.NewBigIntegerItem(0), vm.NewBigIntegerItem(1)} + args := []vm.StackItem{vm.NewBigIntegerItem(big.NewInt(0)), vm.NewBigIntegerItem(big.NewInt(1))} evalWithArgs(t, src, nil, args, big.NewInt(3)) } @@ -49,7 +49,7 @@ func TestEntryPointWithMethodAndArgs(t *testing.T) { return 0 } ` - args := []vm.StackItem{vm.NewBigIntegerItem(0), vm.NewBigIntegerItem(1)} + args := []vm.StackItem{vm.NewBigIntegerItem(big.NewInt(0)), vm.NewBigIntegerItem(big.NewInt(1))} evalWithArgs(t, src, []byte("foobar"), args, big.NewInt(3)) } @@ -154,9 +154,9 @@ func TestIntArray(t *testing.T) { } ` eval(t, src, []vm.StackItem{ - vm.NewBigIntegerItem(1), - vm.NewBigIntegerItem(2), - vm.NewBigIntegerItem(3), + vm.NewBigIntegerItem(big.NewInt(1)), + vm.NewBigIntegerItem(big.NewInt(2)), + vm.NewBigIntegerItem(big.NewInt(3)), }) } diff --git a/pkg/compiler/struct_test.go b/pkg/compiler/struct_test.go index 1dd63e07f..3cca26fd7 100644 --- a/pkg/compiler/struct_test.go +++ b/pkg/compiler/struct_test.go @@ -271,8 +271,8 @@ var structTestCases = []testCase{ } `, []vm.StackItem{ - vm.NewBigIntegerItem(1), - vm.NewBigIntegerItem(2), + vm.NewBigIntegerItem(big.NewInt(1)), + vm.NewBigIntegerItem(big.NewInt(2)), vm.NewByteArrayItem([]byte("hello")), vm.NewByteArrayItem([]byte{}), }, diff --git a/pkg/compiler/syscall_test.go b/pkg/compiler/syscall_test.go index 1eaa53752..d3fc3f14f 100644 --- a/pkg/compiler/syscall_test.go +++ b/pkg/compiler/syscall_test.go @@ -1,6 +1,7 @@ package compiler_test import ( + "math/big" "testing" "github.com/nspcc-dev/neo-go/pkg/vm" @@ -40,7 +41,7 @@ func TestNotify(t *testing.T) { require.NoError(t, v.Run()) require.Equal(t, 3, len(s.events)) - exp0 := []vm.StackItem{vm.NewBigIntegerItem(11), vm.NewByteArrayItem([]byte("sum")), vm.NewBigIntegerItem(12)} + exp0 := []vm.StackItem{vm.NewBigIntegerItem(big.NewInt(11)), vm.NewByteArrayItem([]byte("sum")), vm.NewBigIntegerItem(big.NewInt(12))} assert.Equal(t, exp0, s.events[0].Value()) assert.Equal(t, []vm.StackItem{}, s.events[1].Value()) assert.Equal(t, []vm.StackItem{vm.NewByteArrayItem([]byte("single"))}, s.events[2].Value()) diff --git a/pkg/vm/cli/cli.go b/pkg/vm/cli/cli.go index 2cef728fe..0e3bee234 100644 --- a/pkg/vm/cli/cli.go +++ b/pkg/vm/cli/cli.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "io/ioutil" + "math/big" "os" "strconv" "strings" @@ -438,7 +439,7 @@ func parseArgs(args []string) ([]vm.StackItem, error) { if err != nil { return nil, err } - items[i] = vm.NewBigIntegerItem(val) + items[i] = vm.NewBigIntegerItem(big.NewInt(val)) case stringType: items[i] = vm.NewByteArrayItem([]byte(value)) } diff --git a/pkg/vm/stack_item.go b/pkg/vm/stack_item.go index 93fbf9037..19c5ea6e3 100644 --- a/pkg/vm/stack_item.go +++ b/pkg/vm/stack_item.go @@ -241,9 +241,9 @@ type BigIntegerItem struct { } // NewBigIntegerItem returns an new BigIntegerItem object. -func NewBigIntegerItem(value int64) *BigIntegerItem { +func NewBigIntegerItem(value *big.Int) *BigIntegerItem { return &BigIntegerItem{ - value: big.NewInt(value), + value: value, } } diff --git a/pkg/vm/stack_item_test.go b/pkg/vm/stack_item_test.go index 069682878..9587e5e0a 100644 --- a/pkg/vm/stack_item_test.go +++ b/pkg/vm/stack_item_test.go @@ -104,7 +104,7 @@ var stringerTestCases = []struct { result: "Struct", }, { - input: NewBigIntegerItem(3), + input: NewBigIntegerItem(big.NewInt(3)), result: "BigInteger", }, { @@ -148,48 +148,48 @@ var equalsTestCases = map[string][]struct { }, { item1: NewStructItem(nil), - item2: NewBigIntegerItem(1), + item2: NewBigIntegerItem(big.NewInt(1)), result: false, }, { item1: NewStructItem(nil), - item2: NewStructItem([]StackItem{NewBigIntegerItem(1)}), + item2: NewStructItem([]StackItem{NewBigIntegerItem(big.NewInt(1))}), result: false, }, { - item1: NewStructItem([]StackItem{NewBigIntegerItem(1)}), - item2: NewStructItem([]StackItem{NewBigIntegerItem(2)}), + item1: NewStructItem([]StackItem{NewBigIntegerItem(big.NewInt(1))}), + item2: NewStructItem([]StackItem{NewBigIntegerItem(big.NewInt(2))}), result: false, }, { - item1: NewStructItem([]StackItem{NewBigIntegerItem(1)}), - item2: NewStructItem([]StackItem{NewBigIntegerItem(1)}), + item1: NewStructItem([]StackItem{NewBigIntegerItem(big.NewInt(1))}), + item2: NewStructItem([]StackItem{NewBigIntegerItem(big.NewInt(1))}), result: true, }, }, "bigint": { { - item1: NewBigIntegerItem(2), + item1: NewBigIntegerItem(big.NewInt(2)), item2: nil, result: false, }, { - item1: NewBigIntegerItem(2), - item2: NewBigIntegerItem(2), + item1: NewBigIntegerItem(big.NewInt(2)), + item2: NewBigIntegerItem(big.NewInt(2)), result: true, }, { - item1: NewBigIntegerItem(2), + item1: NewBigIntegerItem(big.NewInt(2)), item2: NewBoolItem(false), result: false, }, { - item1: NewBigIntegerItem(0), + item1: NewBigIntegerItem(big.NewInt(0)), item2: NewBoolItem(false), result: false, }, { - item1: NewBigIntegerItem(2), + item1: NewBigIntegerItem(big.NewInt(2)), item2: makeStackItem(int32(2)), result: true, }, @@ -207,7 +207,7 @@ var equalsTestCases = map[string][]struct { }, { item1: NewBoolItem(true), - item2: NewBigIntegerItem(1), + item2: NewBigIntegerItem(big.NewInt(1)), result: true, }, { @@ -234,7 +234,7 @@ var equalsTestCases = map[string][]struct { }, { item1: NewByteArrayItem([]byte{1}), - item2: NewBigIntegerItem(1), + item2: NewBigIntegerItem(big.NewInt(1)), result: true, }, { @@ -261,7 +261,7 @@ var equalsTestCases = map[string][]struct { }, { item1: NewArrayItem([]StackItem{&BigIntegerItem{big.NewInt(1)}}), - item2: NewBigIntegerItem(1), + item2: NewBigIntegerItem(big.NewInt(1)), result: false, }, { @@ -282,13 +282,13 @@ var equalsTestCases = map[string][]struct { result: false, }, { - item1: &MapItem{value: []MapElement{{NewByteArrayItem([]byte("first")), NewBigIntegerItem(1)}, {NewBoolItem(true), NewByteArrayItem([]byte{2})}}}, - item2: &MapItem{value: []MapElement{{NewByteArrayItem([]byte("first")), NewBigIntegerItem(1)}, {NewBoolItem(true), NewByteArrayItem([]byte{2})}}}, + item1: &MapItem{value: []MapElement{{NewByteArrayItem([]byte("first")), NewBigIntegerItem(big.NewInt(1))}, {NewBoolItem(true), NewByteArrayItem([]byte{2})}}}, + item2: &MapItem{value: []MapElement{{NewByteArrayItem([]byte("first")), NewBigIntegerItem(big.NewInt(1))}, {NewBoolItem(true), NewByteArrayItem([]byte{2})}}}, result: false, }, { - item1: &MapItem{value: []MapElement{{NewByteArrayItem([]byte("first")), NewBigIntegerItem(1)}, {NewBoolItem(true), NewByteArrayItem([]byte{2})}}}, - item2: &MapItem{value: []MapElement{{NewByteArrayItem([]byte("first")), NewBigIntegerItem(1)}, {NewBoolItem(true), NewByteArrayItem([]byte{3})}}}, + item1: &MapItem{value: []MapElement{{NewByteArrayItem([]byte("first")), NewBigIntegerItem(big.NewInt(1))}, {NewBoolItem(true), NewByteArrayItem([]byte{2})}}}, + item2: &MapItem{value: []MapElement{{NewByteArrayItem([]byte("first")), NewBigIntegerItem(big.NewInt(1))}, {NewBoolItem(true), NewByteArrayItem([]byte{3})}}}, result: false, }, }, @@ -333,7 +333,7 @@ var marshalJSONTestCases = []struct { result []byte }{ { - input: NewBigIntegerItem(2), + input: NewBigIntegerItem(big.NewInt(2)), result: []byte(`2`), }, { @@ -386,7 +386,7 @@ var toContractParameterTestCases = []struct { }{ { input: NewStructItem([]StackItem{ - NewBigIntegerItem(1), + NewBigIntegerItem(big.NewInt(1)), NewBoolItem(true), }), result: smartcontract.Parameter{Type: smartcontract.ArrayType, Value: []smartcontract.Parameter{ @@ -403,7 +403,7 @@ var toContractParameterTestCases = []struct { result: smartcontract.Parameter{Type: smartcontract.ByteArrayType, Value: []byte{0x01, 0x02, 0x03}}, }, { - input: NewArrayItem([]StackItem{NewBigIntegerItem(2), NewBoolItem(true)}), + input: NewArrayItem([]StackItem{NewBigIntegerItem(big.NewInt(2)), NewBoolItem(true)}), result: smartcontract.Parameter{Type: smartcontract.ArrayType, Value: []smartcontract.Parameter{ {Type: smartcontract.IntegerType, Value: int64(2)}, {Type: smartcontract.BoolType, Value: true}, @@ -415,8 +415,8 @@ var toContractParameterTestCases = []struct { }, { input: &MapItem{value: []MapElement{ - {NewBigIntegerItem(1), NewBoolItem(true)}, - {NewByteArrayItem([]byte("qwerty")), NewBigIntegerItem(3)}, + {NewBigIntegerItem(big.NewInt(1)), NewBoolItem(true)}, + {NewByteArrayItem([]byte("qwerty")), NewBigIntegerItem(big.NewInt(3))}, {NewBoolItem(true), NewBoolItem(false)}, }}, result: smartcontract.Parameter{ diff --git a/pkg/vm/vm_test.go b/pkg/vm/vm_test.go index e35b7d162..e1572e22e 100644 --- a/pkg/vm/vm_test.go +++ b/pkg/vm/vm_test.go @@ -441,7 +441,7 @@ func testIterableCreate(t *testing.T, typ string) { vm := load(prog) arr := []StackItem{ - NewBigIntegerItem(42), + NewBigIntegerItem(big.NewInt(42)), NewByteArrayItem([]byte{3, 2, 1}), } vm.estack.Push(&Element{value: NewArrayItem(arr)}) @@ -479,7 +479,7 @@ func testIterableConcat(t *testing.T, typ string) { arr := []StackItem{ NewBoolItem(false), - NewBigIntegerItem(123), + NewBigIntegerItem(big.NewInt(123)), NewMapItem(), } vm.estack.Push(&Element{value: NewArrayItem(arr[:1])}) @@ -521,15 +521,15 @@ func TestIteratorKeys(t *testing.T) { v := load(prog) arr := NewArrayItem([]StackItem{ NewBoolItem(false), - NewBigIntegerItem(42), + NewBigIntegerItem(big.NewInt(42)), }) v.estack.PushVal(arr) runVM(t, v) checkEnumeratorStack(t, v, []StackItem{ - NewBigIntegerItem(1), NewBoolItem(true), - NewBigIntegerItem(0), NewBoolItem(true), + NewBigIntegerItem(big.NewInt(1)), NewBoolItem(true), + NewBigIntegerItem(big.NewInt(0)), NewBoolItem(true), }) } @@ -541,7 +541,7 @@ func TestIteratorValues(t *testing.T) { v := load(prog) m := NewMapItem() - m.Add(NewBigIntegerItem(1), NewBoolItem(false)) + m.Add(NewBigIntegerItem(big.NewInt(1)), NewBoolItem(false)) m.Add(NewByteArrayItem([]byte{32}), NewByteArrayItem([]byte{7})) v.estack.PushVal(m) @@ -680,7 +680,7 @@ func TestDeserializeUnknown(t *testing.T) { prog := append(getSyscallProg("Neo.Runtime.Deserialize"), byte(opcode.RET)) vm := load(prog) - data, err := SerializeItem(NewBigIntegerItem(123)) + data, err := SerializeItem(NewBigIntegerItem(big.NewInt(123))) require.NoError(t, err) data[0] = 0xFF From 25354c44f93a7851e7a2d45882d7448eed7f0672 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Thu, 19 Mar 2020 18:52:37 +0300 Subject: [PATCH 7/8] core: implement NativeContract support --- pkg/core/blockchain.go | 17 ++++ pkg/core/interops.go | 8 ++ pkg/core/native/contract.go | 144 +++++++++++++++++++++++++++++++ pkg/core/native/interop.go | 16 ++++ pkg/core/native_contract_test.go | 106 +++++++++++++++++++++++ pkg/smartcontract/call_flags.go | 14 +++ 6 files changed, 305 insertions(+) create mode 100644 pkg/core/native/contract.go create mode 100644 pkg/core/native/interop.go create mode 100644 pkg/core/native_contract_test.go create mode 100644 pkg/smartcontract/call_flags.go diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 20ebc9f65..d8a9509ea 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -14,6 +14,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/dao" "github.com/nspcc-dev/neo-go/pkg/core/interop" "github.com/nspcc-dev/neo-go/pkg/core/mempool" + "github.com/nspcc-dev/neo-go/pkg/core/native" "github.com/nspcc-dev/neo-go/pkg/core/state" "github.com/nspcc-dev/neo-go/pkg/core/storage" "github.com/nspcc-dev/neo-go/pkg/core/transaction" @@ -123,6 +124,8 @@ type Blockchain struct { log *zap.Logger lastBatch *storage.MemBatch + + contracts native.Contracts } type headersOpFunc func(headerList *HeaderHashList) @@ -167,6 +170,8 @@ func NewBlockchain(s storage.Store, cfg config.ProtocolConfiguration, log *zap.L generationAmount: genAmount, decrementInterval: decrementInterval, + + contracts: *native.NewContracts(), } if err := bc.init(); err != nil { @@ -726,6 +731,13 @@ func (bc *Blockchain) storeBlock(block *block.Block) error { bc.lastBatch = cache.DAO.GetBatch() } + for i := range bc.contracts.Contracts { + systemInterop := bc.newInteropContext(trigger.Application, cache, block, nil) + if err := bc.contracts.Contracts[i].OnPersist(systemInterop); err != nil { + return err + } + } + _, err := cache.Persist() if err != nil { return err @@ -832,6 +844,11 @@ func (bc *Blockchain) LastBatch() *storage.MemBatch { return bc.lastBatch } +// RegisterNative registers native contract in the blockchain. +func (bc *Blockchain) RegisterNative(c native.Contract) { + bc.contracts.Add(c) +} + // processOutputs processes transaction outputs. func processOutputs(tx *transaction.Transaction, dao *dao.Cached) error { for index, output := range tx.Outputs { diff --git a/pkg/core/interops.go b/pkg/core/interops.go index 3d4d8355c..05443042e 100644 --- a/pkg/core/interops.go +++ b/pkg/core/interops.go @@ -15,6 +15,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/interop/enumerator" "github.com/nspcc-dev/neo-go/pkg/core/interop/iterator" "github.com/nspcc-dev/neo-go/pkg/core/interop/runtime" + "github.com/nspcc-dev/neo-go/pkg/core/native" "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm" @@ -24,7 +25,12 @@ import ( // up for current blockchain. func SpawnVM(ic *interop.Context) *vm.VM { vm := vm.New() + bc := ic.Chain.(*Blockchain) vm.SetScriptGetter(func(hash util.Uint160) ([]byte, bool) { + if c := bc.contracts.ByHash(hash); c != nil { + meta := c.Metadata() + return meta.Script, (meta.Manifest.Features&smartcontract.HasDynamicInvoke != 0) + } cs, err := ic.DAO.GetContractState(hash) if err != nil { return nil, false @@ -34,6 +40,7 @@ func SpawnVM(ic *interop.Context) *vm.VM { }) vm.RegisterInteropGetter(getSystemInterop(ic)) vm.RegisterInteropGetter(getNeoInterop(ic)) + vm.RegisterInteropGetter(bc.contracts.GetNativeInterop(ic)) return vm } @@ -161,6 +168,7 @@ var neoInterops = []interop.Function{ {Name: "Neo.Iterator.Key", Func: iterator.Key, Price: 1}, {Name: "Neo.Iterator.Keys", Func: iterator.Keys, Price: 1}, {Name: "Neo.Iterator.Values", Func: iterator.Values, Price: 1}, + {Name: "Neo.Native.Deploy", Func: native.Deploy, Price: 1}, {Name: "Neo.Output.GetAssetId", Func: outputGetAssetID, Price: 1}, {Name: "Neo.Output.GetScriptHash", Func: outputGetScriptHash, Price: 1}, {Name: "Neo.Output.GetValue", Func: outputGetValue, Price: 1}, diff --git a/pkg/core/native/contract.go b/pkg/core/native/contract.go new file mode 100644 index 000000000..f68556ded --- /dev/null +++ b/pkg/core/native/contract.go @@ -0,0 +1,144 @@ +package native + +import ( + "fmt" + + "github.com/nspcc-dev/neo-go/pkg/core/interop" + "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" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm" + "github.com/nspcc-dev/neo-go/pkg/vm/emit" + "github.com/pkg/errors" +) + +// Method is a signature for a native method. +type Method = func(ic *interop.Context, args []vm.StackItem) vm.StackItem + +// MethodAndPrice is a native-contract method descriptor. +type MethodAndPrice struct { + Func Method + Price int64 + RequiredFlags smartcontract.CallFlag +} + +// Contract is an interface for all native contracts. +type Contract interface { + Metadata() *ContractMD + OnPersist(*interop.Context) error +} + +// ContractMD represents native contract instance. +type ContractMD struct { + Manifest manifest.Manifest + ServiceName string + ServiceID uint32 + Script []byte + Hash util.Uint160 + Methods map[string]MethodAndPrice +} + +// Contracts is a set of registered native contracts. +type Contracts struct { + Contracts []Contract +} + +// NewContractMD returns Contract with the specified list of methods. +func NewContractMD(name string) *ContractMD { + c := &ContractMD{ + ServiceName: name, + ServiceID: vm.InteropNameToID([]byte(name)), + Methods: make(map[string]MethodAndPrice), + } + + w := io.NewBufBinWriter() + emit.Syscall(w.BinWriter, c.ServiceName) + c.Script = w.Bytes() + c.Hash = hash.Hash160(c.Script) + c.Manifest = *manifest.DefaultManifest(c.Hash) + + return c +} + +// ByHash returns native contract with the specified hash. +func (cs *Contracts) ByHash(h util.Uint160) Contract { + for _, ctr := range cs.Contracts { + if ctr.Metadata().Hash.Equals(h) { + return ctr + } + } + return nil +} + +// ByID returns native contract with the specified id. +func (cs *Contracts) ByID(id uint32) Contract { + for _, ctr := range cs.Contracts { + if ctr.Metadata().ServiceID == id { + return ctr + } + } + return nil +} + +// AddMethod adds new method to a native contract. +func (c *ContractMD) AddMethod(md *MethodAndPrice, desc *manifest.Method, safe bool) { + c.Manifest.ABI.Methods = append(c.Manifest.ABI.Methods, *desc) + c.Methods[desc.Name] = *md + if safe { + c.Manifest.SafeMethods.Add(desc.Name) + } +} + +// AddEvent adds new event to a native contract. +func (c *ContractMD) AddEvent(name string, ps ...manifest.Parameter) { + c.Manifest.ABI.Events = append(c.Manifest.ABI.Events, manifest.Event{ + Name: name, + Parameters: ps, + }) +} + +// NewContracts returns new empty set of native contracts. +func NewContracts() *Contracts { + return &Contracts{ + Contracts: []Contract{}, + } +} + +// Add adds new native contracts to the list. +func (cs *Contracts) Add(c Contract) { + cs.Contracts = append(cs.Contracts, c) +} + +// GetNativeInterop returns an interop getter for a given set of contracts. +func (cs *Contracts) GetNativeInterop(ic *interop.Context) func(uint32) *vm.InteropFuncPrice { + return func(id uint32) *vm.InteropFuncPrice { + if c := cs.ByID(id); c != nil { + return &vm.InteropFuncPrice{ + Func: getNativeInterop(ic, c), + Price: 0, // TODO price func + } + } + return nil + } +} + +// getNativeInterop returns native contract interop. +func getNativeInterop(ic *interop.Context, c Contract) func(v *vm.VM) error { + return func(v *vm.VM) error { + h := v.GetContextScriptHash(0) + if !h.Equals(c.Metadata().Hash) { + return errors.New("invalid hash") + } + name := string(v.Estack().Pop().Bytes()) + args := v.Estack().Pop().Array() + m, ok := c.Metadata().Methods[name] + if !ok { + return fmt.Errorf("method %s not found", name) + } + result := m.Func(ic, args) + v.Estack().PushVal(result) + return nil + } +} diff --git a/pkg/core/native/interop.go b/pkg/core/native/interop.go new file mode 100644 index 000000000..6b6cec16e --- /dev/null +++ b/pkg/core/native/interop.go @@ -0,0 +1,16 @@ +package native + +import ( + "errors" + + "github.com/nspcc-dev/neo-go/pkg/core/interop" + "github.com/nspcc-dev/neo-go/pkg/vm" +) + +// Deploy deploys native contract. +func Deploy(ic *interop.Context, _ *vm.VM) error { + if ic.Block.Index != 0 { + return errors.New("native contracts can be deployed only at 0 block") + } + return nil +} diff --git a/pkg/core/native_contract_test.go b/pkg/core/native_contract_test.go new file mode 100644 index 000000000..414f160e1 --- /dev/null +++ b/pkg/core/native_contract_test.go @@ -0,0 +1,106 @@ +package core + +import ( + "errors" + "math/rand" + "testing" + + "github.com/nspcc-dev/neo-go/pkg/core/interop" + "github.com/nspcc-dev/neo-go/pkg/core/native" + "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/io" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" + "github.com/nspcc-dev/neo-go/pkg/vm" + "github.com/nspcc-dev/neo-go/pkg/vm/emit" + "github.com/stretchr/testify/require" +) + +type testNative struct { + meta native.ContractMD + blocks chan uint32 +} + +func (tn *testNative) Metadata() *native.ContractMD { + return &tn.meta +} + +func (tn *testNative) OnPersist(ic *interop.Context) error { + select { + case tn.blocks <- ic.Block.Index: + return nil + default: + return errors.New("error on persist") + } +} + +var _ native.Contract = (*testNative)(nil) + +func newTestNative() *testNative { + tn := &testNative{ + meta: *native.NewContractMD("Test.Native.Sum"), + blocks: make(chan uint32, 1), + } + desc := &manifest.Method{ + Name: "sum", + Parameters: []manifest.Parameter{ + manifest.NewParameter("addend1", smartcontract.IntegerType), + manifest.NewParameter("addend2", smartcontract.IntegerType), + }, + ReturnType: smartcontract.IntegerType, + } + md := &native.MethodAndPrice{ + Func: tn.sum, + Price: 1, + RequiredFlags: smartcontract.NoneFlag, + } + tn.meta.AddMethod(md, desc, true) + + return tn +} + +func (tn *testNative) sum(_ *interop.Context, args []vm.StackItem) vm.StackItem { + s1, err := args[0].TryInteger() + if err != nil { + panic(err) + } + s2, err := args[1].TryInteger() + if err != nil { + panic(err) + } + return vm.NewBigIntegerItem(s1.Add(s1, s2)) +} + +func TestNativeContract_Invoke(t *testing.T) { + chain := newTestChain(t) + defer chain.Close() + + tn := newTestNative() + chain.RegisterNative(tn) + + w := io.NewBufBinWriter() + emit.AppCallWithOperationAndArgs(w.BinWriter, tn.Metadata().Hash, "sum", int64(14), int64(28)) + script := w.Bytes() + tx := transaction.NewInvocationTX(script, 0) + mn := transaction.NewMinerTXWithNonce(rand.Uint32()) + validUntil := chain.blockHeight + 1 + tx.ValidUntilBlock = validUntil + mn.ValidUntilBlock = validUntil + b := chain.newBlock(mn, tx) + require.NoError(t, chain.AddBlock(b)) + + res, err := chain.GetAppExecResult(tx.Hash()) + require.NoError(t, err) + require.Equal(t, "HALT", res.VMState) + require.Equal(t, 1, len(res.Stack)) + require.Equal(t, smartcontract.IntegerType, res.Stack[0].Type) + require.EqualValues(t, 42, res.Stack[0].Value) + + require.NoError(t, chain.persist()) + select { + case index := <-tn.blocks: + require.Equal(t, chain.blockHeight, index) + default: + require.Fail(t, "onPersist wasn't called") + } +} diff --git a/pkg/smartcontract/call_flags.go b/pkg/smartcontract/call_flags.go new file mode 100644 index 000000000..9bc042fd5 --- /dev/null +++ b/pkg/smartcontract/call_flags.go @@ -0,0 +1,14 @@ +package smartcontract + +// CallFlag represents call flag. +type CallFlag byte + +// Default flags. +const ( + NoneFlag CallFlag = 0 + AllowModifyStates CallFlag = 1 << iota + AllowCall + AllowNotify + ReadOnly = AllowCall | AllowNotify + All = AllowModifyStates | AllowCall | AllowNotify +) From 76700f31cf004c40961870e8bc01418a9d22b49e Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Wed, 25 Mar 2020 13:00:11 +0300 Subject: [PATCH 8/8] core: implement skeletons for NEO/GAS native contracts --- pkg/core/blockchain.go | 140 +++--------- pkg/core/dao/dao.go | 48 +++++ pkg/core/native/contract.go | 14 ++ pkg/core/native/native_gas.go | 134 ++++++++++++ pkg/core/native/native_neo.go | 380 +++++++++++++++++++++++++++++++++ pkg/core/native/native_nep5.go | 229 ++++++++++++++++++++ pkg/core/state/account.go | 6 + pkg/core/state/native_state.go | 45 ++++ pkg/core/storage/store.go | 2 + 9 files changed, 893 insertions(+), 105 deletions(-) create mode 100644 pkg/core/native/native_gas.go create mode 100644 pkg/core/native/native_neo.go create mode 100644 pkg/core/native/native_nep5.go create mode 100644 pkg/core/state/native_state.go diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index d8a9509ea..441c0f909 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -198,6 +198,9 @@ func (bc *Blockchain) init() error { if err != nil { return err } + if err := bc.initNative(); err != nil { + return err + } return bc.storeBlock(genesisBlock) } if ver != version { @@ -270,6 +273,27 @@ func (bc *Blockchain) init() error { return nil } +func (bc *Blockchain) initNative() error { + ic := bc.newInteropContext(trigger.Application, bc.dao, nil, nil) + + gas := native.NewGAS() + neo := native.NewNEO() + neo.GAS = gas + gas.NEO = neo + + if err := gas.Initialize(ic); err != nil { + return fmt.Errorf("can't initialize GAS native contract: %v", err) + } + if err := neo.Initialize(ic); err != nil { + return fmt.Errorf("can't initialize NEO native contract: %v", err) + } + + bc.contracts.SetGAS(gas) + bc.contracts.SetNEO(neo) + + return nil +} + // Run runs chain loop. func (bc *Blockchain) Run() { persistTimer := time.NewTimer(persistInterval) @@ -639,7 +663,7 @@ func (bc *Blockchain) storeBlock(block *block.Block) error { return err } case *transaction.StateTX: - if err := processStateTX(cache, t); err != nil { + if err := bc.processStateTX(cache, tx, t); err != nil { return err } case *transaction.PublishTX: @@ -887,21 +911,8 @@ func processTXWithValidatorsSubtract(output *transaction.Output, account *state. // modAccountVotes adds given value to given account voted validators. func modAccountVotes(account *state.Account, dao *dao.Cached, value util.Fixed8) error { - for _, vote := range account.Votes { - validator, err := dao.GetValidatorStateOrNew(vote) - if err != nil { - return err - } - validator.Votes += value - if validator.UnregisteredAndHasNoVotes() { - if err := dao.DeleteValidatorState(validator); err != nil { - return err - } - } else { - if err := dao.PutValidatorState(validator); err != nil { - return err - } - } + if err := native.ModifyAccountVotes(account, dao, value); err != nil { + return err } if len(account.Votes) > 0 { vc, err := dao.GetValidatorsCount() @@ -937,55 +948,19 @@ func processValidatorStateDescriptor(descriptor *transaction.StateDescriptor, da return nil } -func processAccountStateDescriptor(descriptor *transaction.StateDescriptor, dao *dao.Cached) error { +func (bc *Blockchain) processAccountStateDescriptor(descriptor *transaction.StateDescriptor, t *transaction.Transaction, dao *dao.Cached) error { hash, err := util.Uint160DecodeBytesBE(descriptor.Key) if err != nil { return err } - account, err := dao.GetAccountStateOrNew(hash) - if err != nil { - return err - } if descriptor.Field == "Votes" { - balance := account.GetBalanceValues()[GoverningTokenID()] - if err = modAccountVotes(account, dao, -balance); err != nil { - return err - } - votes := keys.PublicKeys{} - err := votes.DecodeBytes(descriptor.Value) - if err != nil { + if err := votes.DecodeBytes(descriptor.Value); err != nil { return err } - if len(votes) > state.MaxValidatorsVoted { - return errors.New("voting candidate limit exceeded") - } - if len(votes) > 0 { - account.Votes = votes - for _, vote := range account.Votes { - validatorState, err := dao.GetValidatorStateOrNew(vote) - if err != nil { - return err - } - validatorState.Votes += balance - if err = dao.PutValidatorState(validatorState); err != nil { - return err - } - } - vc, err := dao.GetValidatorsCount() - if err != nil { - return err - } - vc[len(account.Votes)-1] += balance - err = dao.PutValidatorsCount(vc) - if err != nil { - return err - } - } else { - account.Votes = nil - } - return dao.PutAccountState(account) + ic := bc.newInteropContext(trigger.Application, dao, nil, t) + return bc.contracts.NEO.VoteInternal(ic, hash, votes) } return nil } @@ -1807,59 +1782,14 @@ func (bc *Blockchain) GetValidators(txes ...*transaction.Transaction) ([]*keys.P return nil, err } case *transaction.StateTX: - if err := processStateTX(cache, t); err != nil { + if err := bc.processStateTX(cache, tx, t); err != nil { return nil, err } } } } - validators := cache.GetValidators() - sort.Slice(validators, func(i, j int) bool { - // Unregistered validators go to the end of the list. - if validators[i].Registered != validators[j].Registered { - return validators[i].Registered - } - // The most-voted validators should end up in the front of the list. - if validators[i].Votes != validators[j].Votes { - return validators[i].Votes > validators[j].Votes - } - // Ties are broken with public keys. - return validators[i].PublicKey.Cmp(validators[j].PublicKey) == -1 - }) - - validatorsCount, err := cache.GetValidatorsCount() - if err != nil { - return nil, err - } - count := validatorsCount.GetWeightedAverage() - standByValidators, err := bc.GetStandByValidators() - if err != nil { - return nil, err - } - if count < len(standByValidators) { - count = len(standByValidators) - } - - uniqueSBValidators := standByValidators.Unique() - result := keys.PublicKeys{} - for _, validator := range validators { - if validator.RegisteredAndHasVotes() || uniqueSBValidators.Contains(validator.PublicKey) { - result = append(result, validator.PublicKey) - } - } - - if result.Len() >= count { - result = result[:count] - } else { - for i := 0; i < uniqueSBValidators.Len() && result.Len() < count; i++ { - if !result.Contains(uniqueSBValidators[i]) { - result = append(result, uniqueSBValidators[i]) - } - } - } - sort.Sort(result) - return result, nil + return bc.contracts.NEO.GetValidatorsInternal(bc, cache) } // GetEnrollments returns all registered validators and non-registered SB validators @@ -1896,11 +1826,11 @@ func (bc *Blockchain) GetEnrollments() ([]*state.Validator, error) { return result, nil } -func processStateTX(dao *dao.Cached, tx *transaction.StateTX) error { +func (bc *Blockchain) processStateTX(dao *dao.Cached, t *transaction.Transaction, tx *transaction.StateTX) error { for _, desc := range tx.Descriptors { switch desc.Type { case transaction.Account: - if err := processAccountStateDescriptor(desc, dao); err != nil { + if err := bc.processAccountStateDescriptor(desc, t, dao); err != nil { return err } case transaction.Validator: diff --git a/pkg/core/dao/dao.go b/pkg/core/dao/dao.go index 88a0b9981..c5a13a338 100644 --- a/pkg/core/dao/dao.go +++ b/pkg/core/dao/dao.go @@ -32,8 +32,10 @@ type DAO interface { GetCurrentBlockHeight() (uint32, error) GetCurrentHeaderHeight() (i uint32, h util.Uint256, err error) GetHeaderHashes() ([]util.Uint256, error) + GetNativeContractState(h util.Uint160) ([]byte, error) GetNEP5Balances(acc util.Uint160) (*state.NEP5Balances, error) GetNEP5TransferLog(acc util.Uint160, index uint32) (*state.NEP5TransferLog, error) + GetNextBlockValidators() (keys.PublicKeys, error) GetStorageItem(scripthash util.Uint160, key []byte) *state.StorageItem GetStorageItems(hash util.Uint160) (map[string]*state.StorageItem, error) GetTransaction(hash util.Uint256) (*transaction.Transaction, uint32, error) @@ -53,8 +55,10 @@ type DAO interface { PutAssetState(as *state.Asset) error PutContractState(cs *state.Contract) error PutCurrentHeader(hashAndIndex []byte) error + PutNativeContractState(h util.Uint160, value []byte) error PutNEP5Balances(acc util.Uint160, bs *state.NEP5Balances) error PutNEP5TransferLog(acc util.Uint160, index uint32, lg *state.NEP5TransferLog) error + PutNextBlockValidators(keys.PublicKeys) error PutStorageItem(scripthash util.Uint160, key []byte, si *state.StorageItem) error PutUnspentCoinState(hash util.Uint256, ucs *state.UnspentCoin) error PutValidatorState(vs *state.Validator) error @@ -207,6 +211,18 @@ func (dao *Simple) DeleteContractState(hash util.Uint160) error { return dao.Store.Delete(key) } +// GetNativeContractState retrieves native contract state from the store. +func (dao *Simple) GetNativeContractState(h util.Uint160) ([]byte, error) { + key := storage.AppendPrefix(storage.STNativeContract, h.BytesBE()) + return dao.Store.Get(key) +} + +// PutNativeContractState puts native contract state into the store. +func (dao *Simple) PutNativeContractState(h util.Uint160, value []byte) error { + key := storage.AppendPrefix(storage.STNativeContract, h.BytesBE()) + return dao.Store.Put(key, value) +} + // -- end contracts. // -- start nep5 balances. @@ -310,6 +326,38 @@ func (dao *Simple) putUnspentCoinState(hash util.Uint256, ucs *state.UnspentCoin // -- start validator. +// GetNextBlockValidators retrieves next block validators from store or nil if they are missing. +func (dao *Simple) GetNextBlockValidators() (keys.PublicKeys, error) { + key := []byte{byte(storage.STNextValidators)} + buf, err := dao.Store.Get(key) + if err != nil { + if err == storage.ErrKeyNotFound { + return nil, nil + } + return nil, err + } + + var pubs keys.PublicKeys + r := io.NewBinReaderFromBuf(buf) + r.ReadArray(&pubs) + if r.Err != nil { + return nil, r.Err + } + return pubs, nil +} + +// PutNextBlockValidators puts next block validators to store. +func (dao *Simple) PutNextBlockValidators(pubs keys.PublicKeys) error { + w := io.NewBufBinWriter() + w.WriteArray(pubs) + if w.Err != nil { + return w.Err + } + + key := []byte{byte(storage.STNextValidators)} + return dao.Store.Put(key, w.Bytes()) +} + // GetValidatorStateOrNew gets validator from store or created new one in case of error. func (dao *Simple) GetValidatorStateOrNew(publicKey *keys.PublicKey) (*state.Validator, error) { validatorState, err := dao.GetValidatorState(publicKey) diff --git a/pkg/core/native/contract.go b/pkg/core/native/contract.go index f68556ded..150fb4ee8 100644 --- a/pkg/core/native/contract.go +++ b/pkg/core/native/contract.go @@ -42,9 +42,23 @@ type ContractMD struct { // Contracts is a set of registered native contracts. type Contracts struct { + NEO *NEO + GAS *GAS Contracts []Contract } +// SetGAS sets GAS native contract. +func (cs *Contracts) SetGAS(g *GAS) { + cs.GAS = g + cs.Contracts = append(cs.Contracts, g) +} + +// SetNEO sets NEO native contract. +func (cs *Contracts) SetNEO(n *NEO) { + cs.NEO = n + cs.Contracts = append(cs.Contracts, n) +} + // NewContractMD returns Contract with the specified list of methods. func NewContractMD(name string) *ContractMD { c := &ContractMD{ diff --git a/pkg/core/native/native_gas.go b/pkg/core/native/native_gas.go new file mode 100644 index 000000000..87736f0de --- /dev/null +++ b/pkg/core/native/native_gas.go @@ -0,0 +1,134 @@ +package native + +import ( + "errors" + "math/big" + + "github.com/nspcc-dev/neo-go/pkg/core/interop" + "github.com/nspcc-dev/neo-go/pkg/core/state" + "github.com/nspcc-dev/neo-go/pkg/core/storage" + "github.com/nspcc-dev/neo-go/pkg/crypto/hash" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm" + "github.com/nspcc-dev/neo-go/pkg/vm/emit" +) + +// GAS represents GAS native contract. +type GAS struct { + nep5TokenNative + NEO *NEO +} + +const gasSyscallName = "Neo.Native.Tokens.GAS" + +// NewGAS returns GAS native contract. +func NewGAS() *GAS { + nep5 := newNEP5Native(gasSyscallName) + nep5.name = "GAS" + nep5.symbol = "gas" + nep5.decimals = 8 + nep5.factor = 100000000 + + g := &GAS{nep5TokenNative: *nep5} + + desc := newDescriptor("getSysFeeAmount", smartcontract.IntegerType, + manifest.NewParameter("index", smartcontract.IntegerType)) + md := newMethodAndPrice(g.getSysFeeAmount, 1, smartcontract.NoneFlag) + g.AddMethod(md, desc, true) + + g.onPersist = chainOnPersist(g.onPersist, g.OnPersist) + g.incBalance = g.increaseBalance + return g +} + +// initFromStore initializes variable contract parameters from the store. +func (g *GAS) initFromStore(data []byte) error { + g.totalSupply = *emit.BytesToInt(data) + return nil +} + +func (g *GAS) serializeState() []byte { + return emit.IntToBytes(&g.totalSupply) +} + +func (g *GAS) increaseBalance(_ *interop.Context, acc *state.Account, amount *big.Int) error { + if sign := amount.Sign(); sign == 0 { + return nil + } else if sign == -1 && acc.GAS.Balance.Cmp(new(big.Int).Neg(amount)) == -1 { + return errors.New("insufficient funds") + } + acc.GAS.Balance.Add(&acc.GAS.Balance, amount) + return nil +} + +// Initialize initializes GAS contract. +func (g *GAS) Initialize(ic *interop.Context) error { + data, err := ic.DAO.GetNativeContractState(g.Hash) + if err == nil { + return g.initFromStore(data) + } else if err != storage.ErrKeyNotFound { + return err + } + + if err := g.nep5TokenNative.Initialize(); err != nil { + return err + } + h, _, err := getStandbyValidatorsHash(ic) + if err != nil { + return err + } + g.mint(ic, h, big.NewInt(30000000*g.factor)) + return ic.DAO.PutNativeContractState(g.Hash, g.serializeState()) +} + +// OnPersist implements Contract interface. +func (g *GAS) OnPersist(ic *interop.Context) error { + //for _ ,tx := range ic.block.Transactions { + // g.burn(ic, tx.Sender, tx.SystemFee + tx.NetworkFee) + //} + //validators := g.NEO.getNextBlockValidators(ic) + //var netFee util.Fixed8 + //for _, tx := range ic.block.Transactions { + // netFee += tx.NetworkFee + //} + //g.mint(ic, , netFee) + return ic.DAO.PutNativeContractState(g.Hash, g.serializeState()) +} + +func (g *GAS) getSysFeeAmount(ic *interop.Context, args []vm.StackItem) vm.StackItem { + index := toBigInt(args[0]) + h := ic.Chain.GetHeaderHash(int(index.Int64())) + _, sf, err := ic.DAO.GetBlock(h) + if err != nil { + panic(err) + } + return vm.NewBigIntegerItem(big.NewInt(int64(sf))) +} + +func getStandbyValidatorsHash(ic *interop.Context) (util.Uint160, []*keys.PublicKey, error) { + vs, err := ic.Chain.GetStandByValidators() + if err != nil { + return util.Uint160{}, nil, err + } + s, err := smartcontract.CreateMultiSigRedeemScript(len(vs)/2+1, vs) + if err != nil { + return util.Uint160{}, nil, err + } + return hash.Hash160(s), vs, nil +} + +func chainOnPersist(fs ...func(*interop.Context) error) func(*interop.Context) error { + return func(ic *interop.Context) error { + for i := range fs { + if fs[i] != nil { + if err := fs[i](ic); err != nil { + return err + } + } + } + return nil + } +} diff --git a/pkg/core/native/native_neo.go b/pkg/core/native/native_neo.go new file mode 100644 index 000000000..71498a187 --- /dev/null +++ b/pkg/core/native/native_neo.go @@ -0,0 +1,380 @@ +package native + +import ( + "math/big" + "sort" + + "github.com/nspcc-dev/neo-go/pkg/core/blockchainer" + "github.com/nspcc-dev/neo-go/pkg/core/dao" + "github.com/nspcc-dev/neo-go/pkg/core/interop" + "github.com/nspcc-dev/neo-go/pkg/core/interop/runtime" + "github.com/nspcc-dev/neo-go/pkg/core/state" + "github.com/nspcc-dev/neo-go/pkg/core/storage" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm" + "github.com/nspcc-dev/neo-go/pkg/vm/emit" + "github.com/pkg/errors" +) + +// NEO represents NEO native contract. +type NEO struct { + nep5TokenNative + GAS *GAS +} + +const neoSyscallName = "Neo.Native.Tokens.NEO" + +// NewNEO returns NEO native contract. +func NewNEO() *NEO { + nep5 := newNEP5Native(neoSyscallName) + nep5.name = "NEO" + nep5.symbol = "neo" + nep5.decimals = 0 + nep5.factor = 1 + + n := &NEO{nep5TokenNative: *nep5} + + desc := newDescriptor("unclaimedGas", smartcontract.IntegerType, + manifest.NewParameter("account", smartcontract.Hash160Type), + manifest.NewParameter("end", smartcontract.IntegerType)) + md := newMethodAndPrice(n.unclaimedGas, 1, smartcontract.NoneFlag) + n.AddMethod(md, desc, true) + + desc = newDescriptor("registerValidator", smartcontract.BoolType, + manifest.NewParameter("pubkey", smartcontract.PublicKeyType)) + md = newMethodAndPrice(n.registerValidator, 1, smartcontract.NoneFlag) + n.AddMethod(md, desc, false) + + desc = newDescriptor("vote", smartcontract.BoolType, + manifest.NewParameter("account", smartcontract.Hash160Type), + manifest.NewParameter("pubkeys", smartcontract.ArrayType)) + md = newMethodAndPrice(n.vote, 1, smartcontract.NoneFlag) + n.AddMethod(md, desc, false) + + desc = newDescriptor("getRegisteredValidators", smartcontract.ArrayType) + md = newMethodAndPrice(n.getRegisteredValidators, 1, smartcontract.NoneFlag) + n.AddMethod(md, desc, true) + + desc = newDescriptor("getValidators", smartcontract.ArrayType) + md = newMethodAndPrice(n.getValidators, 1, smartcontract.NoneFlag) + n.AddMethod(md, desc, true) + + desc = newDescriptor("getNextBlockValidators", smartcontract.ArrayType) + md = newMethodAndPrice(n.getNextBlockValidators, 1, smartcontract.NoneFlag) + n.AddMethod(md, desc, true) + + n.onPersist = chainOnPersist(n.onPersist, n.OnPersist) + n.incBalance = n.increaseBalance + + return n +} + +// Initialize initializes NEO contract. +func (n *NEO) Initialize(ic *interop.Context) error { + data, err := ic.DAO.GetNativeContractState(n.Hash) + if err == nil { + return n.initFromStore(data) + } else if err != storage.ErrKeyNotFound { + return err + } + + if err := n.nep5TokenNative.Initialize(); err != nil { + return err + } + + h, vs, err := getStandbyValidatorsHash(ic) + if err != nil { + return err + } + n.mint(ic, h, big.NewInt(100000000*n.factor)) + + for i := range vs { + if err := n.registerValidatorInternal(ic, vs[i]); err != nil { + return err + } + } + + return ic.DAO.PutNativeContractState(n.Hash, n.serializeState()) +} + +// initFromStore initializes variable contract parameters from the store. +func (n *NEO) initFromStore(data []byte) error { + n.totalSupply = *emit.BytesToInt(data) + return nil +} + +func (n *NEO) serializeState() []byte { + return emit.IntToBytes(&n.totalSupply) +} + +// OnPersist implements Contract interface. +func (n *NEO) OnPersist(ic *interop.Context) error { + pubs, err := n.GetValidatorsInternal(ic.Chain, ic.DAO) + if err != nil { + return err + } + if err := ic.DAO.PutNextBlockValidators(pubs); err != nil { + return err + } + return ic.DAO.PutNativeContractState(n.Hash, n.serializeState()) +} + +func (n *NEO) increaseBalance(ic *interop.Context, acc *state.Account, amount *big.Int) error { + if sign := amount.Sign(); sign == 0 { + return nil + } else if sign == -1 && acc.NEO.Balance.Cmp(new(big.Int).Neg(amount)) == -1 { + return errors.New("insufficient funds") + } + if err := n.distributeGas(ic, acc); err != nil { + return err + } + acc.NEO.Balance.Add(&acc.NEO.Balance, amount) + return nil +} + +func (n *NEO) distributeGas(ic *interop.Context, acc *state.Account) error { + if ic.Block == nil { + return nil + } + sys, net, err := ic.Chain.CalculateClaimable(util.Fixed8(acc.NEO.Balance.Int64()), acc.NEO.BalanceHeight, ic.Block.Index) + if err != nil { + return err + } + acc.NEO.BalanceHeight = ic.Block.Index + n.GAS.mint(ic, acc.ScriptHash, big.NewInt(int64(sys+net))) + return nil +} + +func (n *NEO) unclaimedGas(ic *interop.Context, args []vm.StackItem) vm.StackItem { + u := toUint160(args[0]) + end := uint32(toBigInt(args[1]).Int64()) + bs, err := ic.DAO.GetNEP5Balances(u) + if err != nil { + panic(err) + } + tr := bs.Trackers[n.Hash] + + sys, net, err := ic.Chain.CalculateClaimable(util.Fixed8(tr.Balance), tr.LastUpdatedBlock, end) + if err != nil { + panic(err) + } + return vm.NewBigIntegerItem(big.NewInt(int64(sys.Add(net)))) +} + +func (n *NEO) registerValidator(ic *interop.Context, args []vm.StackItem) vm.StackItem { + err := n.registerValidatorInternal(ic, toPublicKey(args[0])) + return vm.NewBoolItem(err == nil) +} + +func (n *NEO) registerValidatorInternal(ic *interop.Context, pub *keys.PublicKey) error { + _, err := ic.DAO.GetValidatorState(pub) + if err == nil { + return err + } + return ic.DAO.PutValidatorState(&state.Validator{PublicKey: pub}) +} + +func (n *NEO) vote(ic *interop.Context, args []vm.StackItem) vm.StackItem { + acc := toUint160(args[0]) + arr := args[1].Value().([]vm.StackItem) + var pubs keys.PublicKeys + for i := range arr { + pub := new(keys.PublicKey) + bs, err := arr[i].TryBytes() + if err != nil { + panic(err) + } else if err := pub.DecodeBytes(bs); err != nil { + panic(err) + } + pubs = append(pubs, pub) + } + err := n.VoteInternal(ic, acc, pubs) + return vm.NewBoolItem(err == nil) +} + +// VoteInternal votes from account h for validarors specified in pubs. +func (n *NEO) VoteInternal(ic *interop.Context, h util.Uint160, pubs keys.PublicKeys) error { + ok, err := runtime.CheckHashedWitness(ic, h) + if err != nil { + return err + } else if !ok { + return errors.New("invalid signature") + } + acc, err := ic.DAO.GetAccountState(h) + if err != nil { + return err + } + balance := util.Fixed8(acc.NEO.Balance.Int64()) + if err := ModifyAccountVotes(acc, ic.DAO, -balance); err != nil { + return err + } + pubs = pubs.Unique() + var newPubs keys.PublicKeys + for _, pub := range pubs { + _, err := ic.DAO.GetValidatorState(pub) + if err != nil { + if err == storage.ErrKeyNotFound { + continue + } + return err + } + newPubs = append(newPubs, pub) + } + if lp, lv := len(newPubs), len(acc.Votes); lp != lv { + vc, err := ic.DAO.GetValidatorsCount() + if err != nil { + return err + } + if lv > 0 { + vc[lv-1] -= balance + } + if len(newPubs) > 0 { + vc[lp-1] += balance + } + if err := ic.DAO.PutValidatorsCount(vc); err != nil { + return err + } + } + acc.Votes = newPubs + return ModifyAccountVotes(acc, ic.DAO, balance) +} + +// ModifyAccountVotes modifies votes of the specified account by value (can be negative). +func ModifyAccountVotes(acc *state.Account, d dao.DAO, value util.Fixed8) error { + for _, vote := range acc.Votes { + validator, err := d.GetValidatorStateOrNew(vote) + if err != nil { + return err + } + validator.Votes += value + if validator.UnregisteredAndHasNoVotes() { + if err := d.DeleteValidatorState(validator); err != nil { + return err + } + } else { + if err := d.PutValidatorState(validator); err != nil { + return err + } + } + } + return nil +} + +func (n *NEO) getRegisteredValidators(ic *interop.Context, _ []vm.StackItem) vm.StackItem { + vs := ic.DAO.GetValidators() + arr := make([]vm.StackItem, len(vs)) + for i := range vs { + arr[i] = vm.NewStructItem([]vm.StackItem{ + vm.NewByteArrayItem(vs[i].PublicKey.Bytes()), + vm.NewBigIntegerItem(big.NewInt(int64(vs[i].Votes))), + }) + } + return vm.NewArrayItem(arr) +} + +// GetValidatorsInternal returns a list of current validators. +func (n *NEO) GetValidatorsInternal(bc blockchainer.Blockchainer, d dao.DAO) ([]*keys.PublicKey, error) { + validatorsCount, err := d.GetValidatorsCount() + if err != nil { + return nil, err + } else if len(validatorsCount) == 0 { + sb, err := bc.GetStandByValidators() + if err != nil { + return nil, err + } + return sb, nil + } + + validators := d.GetValidators() + sort.Slice(validators, func(i, j int) bool { + // Unregistered validators go to the end of the list. + if validators[i].Registered != validators[j].Registered { + return validators[i].Registered + } + // The most-voted validators should end up in the front of the list. + if validators[i].Votes != validators[j].Votes { + return validators[i].Votes > validators[j].Votes + } + // Ties are broken with public keys. + return validators[i].PublicKey.Cmp(validators[j].PublicKey) == -1 + }) + + count := validatorsCount.GetWeightedAverage() + standByValidators, err := bc.GetStandByValidators() + if err != nil { + return nil, err + } + if count < len(standByValidators) { + count = len(standByValidators) + } + + uniqueSBValidators := standByValidators.Unique() + result := keys.PublicKeys{} + for _, validator := range validators { + if validator.RegisteredAndHasVotes() || uniqueSBValidators.Contains(validator.PublicKey) { + result = append(result, validator.PublicKey) + } + } + + if result.Len() >= count { + result = result[:count] + } else { + for i := 0; i < uniqueSBValidators.Len() && result.Len() < count; i++ { + if !result.Contains(uniqueSBValidators[i]) { + result = append(result, uniqueSBValidators[i]) + } + } + } + sort.Sort(result) + return result, nil +} + +func (n *NEO) getValidators(ic *interop.Context, _ []vm.StackItem) vm.StackItem { + result, err := n.GetValidatorsInternal(ic.Chain, ic.DAO) + if err != nil { + panic(err) + } + return pubsToArray(result) +} + +func (n *NEO) getNextBlockValidators(ic *interop.Context, _ []vm.StackItem) vm.StackItem { + result, err := n.GetNextBlockValidatorsInternal(ic.Chain, ic.DAO) + if err != nil { + panic(err) + } + return pubsToArray(result) +} + +// GetNextBlockValidatorsInternal returns next block validators. +func (n *NEO) GetNextBlockValidatorsInternal(bc blockchainer.Blockchainer, d dao.DAO) ([]*keys.PublicKey, error) { + result, err := d.GetNextBlockValidators() + if err != nil { + return nil, err + } else if result == nil { + return bc.GetStandByValidators() + } + return result, nil +} + +func pubsToArray(pubs keys.PublicKeys) vm.StackItem { + arr := make([]vm.StackItem, len(pubs)) + for i := range pubs { + arr[i] = vm.NewByteArrayItem(pubs[i].Bytes()) + } + return vm.NewArrayItem(arr) +} + +func toPublicKey(s vm.StackItem) *keys.PublicKey { + buf, err := s.TryBytes() + if err != nil { + panic(err) + } + pub := new(keys.PublicKey) + if err := pub.DecodeBytes(buf); err != nil { + panic(err) + } + return pub +} diff --git a/pkg/core/native/native_nep5.go b/pkg/core/native/native_nep5.go new file mode 100644 index 000000000..e4476be14 --- /dev/null +++ b/pkg/core/native/native_nep5.go @@ -0,0 +1,229 @@ +package native + +import ( + "errors" + "math/big" + + "github.com/nspcc-dev/neo-go/pkg/core/interop" + "github.com/nspcc-dev/neo-go/pkg/core/state" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm" +) + +// nep5TokenNative represents NEP-5 token contract. +type nep5TokenNative struct { + ContractMD + name string + symbol string + decimals int64 + factor int64 + totalSupply big.Int + onPersist func(*interop.Context) error + incBalance func(*interop.Context, *state.Account, *big.Int) error +} + +func (c *nep5TokenNative) Metadata() *ContractMD { + return &c.ContractMD +} + +var _ Contract = (*nep5TokenNative)(nil) + +func newNEP5Native(name string) *nep5TokenNative { + n := &nep5TokenNative{ContractMD: *NewContractMD(name)} + + desc := newDescriptor("name", smartcontract.StringType) + md := newMethodAndPrice(n.Name, 1, smartcontract.NoneFlag) + n.AddMethod(md, desc, true) + + desc = newDescriptor("symbol", smartcontract.StringType) + md = newMethodAndPrice(n.Symbol, 1, smartcontract.NoneFlag) + n.AddMethod(md, desc, true) + + desc = newDescriptor("decimals", smartcontract.IntegerType) + md = newMethodAndPrice(n.Decimals, 1, smartcontract.NoneFlag) + n.AddMethod(md, desc, true) + + desc = newDescriptor("balanceOf", smartcontract.IntegerType, + manifest.NewParameter("account", smartcontract.Hash160Type)) + md = newMethodAndPrice(n.balanceOf, 1, smartcontract.NoneFlag) + n.AddMethod(md, desc, true) + + desc = newDescriptor("transfer", smartcontract.BoolType, + manifest.NewParameter("from", smartcontract.Hash160Type), + manifest.NewParameter("to", smartcontract.Hash160Type), + manifest.NewParameter("amount", smartcontract.IntegerType), + ) + md = newMethodAndPrice(n.Transfer, 1, smartcontract.NoneFlag) + n.AddMethod(md, desc, false) + + n.AddEvent("Transfer", desc.Parameters...) + + return n +} + +func (c *nep5TokenNative) Initialize() error { + return nil +} + +func (c *nep5TokenNative) Name(_ *interop.Context, _ []vm.StackItem) vm.StackItem { + return vm.NewByteArrayItem([]byte(c.name)) +} + +func (c *nep5TokenNative) Symbol(_ *interop.Context, _ []vm.StackItem) vm.StackItem { + return vm.NewByteArrayItem([]byte(c.symbol)) +} + +func (c *nep5TokenNative) Decimals(_ *interop.Context, _ []vm.StackItem) vm.StackItem { + return vm.NewBigIntegerItem(big.NewInt(c.decimals)) +} + +func (c *nep5TokenNative) Transfer(ic *interop.Context, args []vm.StackItem) vm.StackItem { + from := toUint160(args[0]) + to := toUint160(args[1]) + amount := toBigInt(args[2]) + err := c.transfer(ic, from, to, amount) + return vm.NewBoolItem(err == nil) +} + +func addrToStackItem(u *util.Uint160) vm.StackItem { + if u == nil { + return nil + } + return vm.NewByteArrayItem(u.BytesBE()) +} + +func (c *nep5TokenNative) emitTransfer(ic *interop.Context, from, to *util.Uint160, amount *big.Int) { + ne := state.NotificationEvent{ + ScriptHash: c.Hash, + Item: vm.NewArrayItem([]vm.StackItem{ + vm.NewByteArrayItem([]byte("Transfer")), + addrToStackItem(from), + addrToStackItem(to), + vm.NewBigIntegerItem(amount), + }), + } + ic.Notifications = append(ic.Notifications, ne) +} + +func (c *nep5TokenNative) transfer(ic *interop.Context, from, to util.Uint160, amount *big.Int) error { + if amount.Sign() == -1 { + return errors.New("negative amount") + } + + accFrom, err := ic.DAO.GetAccountStateOrNew(from) + if err != nil { + return err + } + + isEmpty := from.Equals(to) || amount.Sign() == 0 + inc := amount + if isEmpty { + inc = big.NewInt(0) + } + if err := c.incBalance(ic, accFrom, inc); err != nil { + return err + } + if err := ic.DAO.PutAccountState(accFrom); err != nil { + return err + } + + if !isEmpty { + accTo, err := ic.DAO.GetAccountStateOrNew(to) + if err != nil { + return err + } + if err := c.incBalance(ic, accTo, amount); err != nil { + return err + } + if err := ic.DAO.PutAccountState(accTo); err != nil { + return err + } + } + + c.emitTransfer(ic, &from, &to, amount) + return nil +} + +func (c *nep5TokenNative) balanceOf(ic *interop.Context, args []vm.StackItem) vm.StackItem { + h := toUint160(args[0]) + bs, err := ic.DAO.GetNEP5Balances(h) + if err != nil { + panic(err) + } + balance := bs.Trackers[c.Hash].Balance + return vm.NewBigIntegerItem(big.NewInt(balance)) +} + +func (c *nep5TokenNative) mint(ic *interop.Context, h util.Uint160, amount *big.Int) { + c.addTokens(ic, h, amount) + c.emitTransfer(ic, nil, &h, amount) +} + +func (c *nep5TokenNative) burn(ic *interop.Context, h util.Uint160, amount *big.Int) { + amount = new(big.Int).Neg(amount) + c.addTokens(ic, h, amount) + c.emitTransfer(ic, &h, nil, amount) +} + +func (c *nep5TokenNative) addTokens(ic *interop.Context, h util.Uint160, amount *big.Int) { + if sign := amount.Sign(); sign == -1 { + panic("negative amount") + } else if sign == 0 { + return + } + + acc, err := ic.DAO.GetAccountStateOrNew(h) + if err != nil { + panic(err) + } + if err := c.incBalance(ic, acc, amount); err != nil { + panic(err) + } + if err := ic.DAO.PutAccountState(acc); err != nil { + panic(err) + } + + c.totalSupply.Add(&c.totalSupply, amount) +} + +func (c *nep5TokenNative) OnPersist(ic *interop.Context) error { + return c.onPersist(ic) +} + +func newDescriptor(name string, ret smartcontract.ParamType, ps ...manifest.Parameter) *manifest.Method { + return &manifest.Method{ + Name: name, + Parameters: ps, + ReturnType: ret, + } +} + +func newMethodAndPrice(f Method, price int64, flags smartcontract.CallFlag) *MethodAndPrice { + return &MethodAndPrice{ + Func: f, + Price: price, + RequiredFlags: flags, + } +} + +func toBigInt(s vm.StackItem) *big.Int { + bi, err := s.TryInteger() + if err != nil { + panic(err) + } + return bi +} + +func toUint160(s vm.StackItem) util.Uint160 { + buf, err := s.TryBytes() + if err != nil { + panic(err) + } + u, err := util.Uint160DecodeBytesBE(buf) + if err != nil { + panic(err) + } + return u +} diff --git a/pkg/core/state/account.go b/pkg/core/state/account.go index 4d163b03c..f18a25d15 100644 --- a/pkg/core/state/account.go +++ b/pkg/core/state/account.go @@ -33,6 +33,8 @@ type Account struct { ScriptHash util.Uint160 IsFrozen bool Votes []*keys.PublicKey + GAS NEP5BalanceState + NEO NEOBalanceState Balances map[util.Uint256][]UnspentBalance Unclaimed UnclaimedBalances } @@ -55,6 +57,8 @@ func (s *Account) DecodeBinary(br *io.BinReader) { br.ReadBytes(s.ScriptHash[:]) s.IsFrozen = br.ReadBool() br.ReadArray(&s.Votes) + s.GAS.DecodeBinary(br) + s.NEO.DecodeBinary(br) s.Balances = make(map[util.Uint256][]UnspentBalance) lenBalances := br.ReadVarUint() @@ -80,6 +84,8 @@ func (s *Account) EncodeBinary(bw *io.BinWriter) { bw.WriteBytes(s.ScriptHash[:]) bw.WriteBool(s.IsFrozen) bw.WriteArray(s.Votes) + s.GAS.EncodeBinary(bw) + s.NEO.EncodeBinary(bw) bw.WriteVarUint(uint64(len(s.Balances))) for k, v := range s.Balances { diff --git a/pkg/core/state/native_state.go b/pkg/core/state/native_state.go new file mode 100644 index 000000000..9744cc754 --- /dev/null +++ b/pkg/core/state/native_state.go @@ -0,0 +1,45 @@ +package state + +import ( + "math/big" + + "github.com/nspcc-dev/neo-go/pkg/io" + "github.com/nspcc-dev/neo-go/pkg/vm/emit" +) + +// NEP5BalanceState represents balance state of a NEP5-token. +type NEP5BalanceState struct { + Balance big.Int +} + +// NEOBalanceState represents balance state of a NEO-token. +type NEOBalanceState struct { + NEP5BalanceState + BalanceHeight uint32 +} + +// EncodeBinary implements io.Serializable interface. +func (s *NEP5BalanceState) EncodeBinary(w *io.BinWriter) { + w.WriteVarBytes(emit.IntToBytes(&s.Balance)) +} + +// DecodeBinary implements io.Serializable interface. +func (s *NEP5BalanceState) DecodeBinary(r *io.BinReader) { + buf := r.ReadVarBytes() + if r.Err != nil { + return + } + s.Balance = *emit.BytesToInt(buf) +} + +// EncodeBinary implements io.Serializable interface. +func (s *NEOBalanceState) EncodeBinary(w *io.BinWriter) { + s.NEP5BalanceState.EncodeBinary(w) + w.WriteU32LE(s.BalanceHeight) +} + +// DecodeBinary implements io.Serializable interface. +func (s *NEOBalanceState) DecodeBinary(r *io.BinReader) { + s.NEP5BalanceState.DecodeBinary(r) + s.BalanceHeight = r.ReadU32LE() +} diff --git a/pkg/core/storage/store.go b/pkg/core/storage/store.go index 73b3866b1..b9fb6874d 100644 --- a/pkg/core/storage/store.go +++ b/pkg/core/storage/store.go @@ -12,10 +12,12 @@ const ( STAccount KeyPrefix = 0x40 STCoin KeyPrefix = 0x44 STSpentCoin KeyPrefix = 0x45 + STNextValidators KeyPrefix = 0x47 STValidator KeyPrefix = 0x48 STAsset KeyPrefix = 0x4c STNotification KeyPrefix = 0x4d STContract KeyPrefix = 0x50 + STNativeContract KeyPrefix = 0x51 STStorage KeyPrefix = 0x70 STNEP5Transfers KeyPrefix = 0x72 STNEP5Balances KeyPrefix = 0x73