Merge pull request #1373 from nspcc-dev/feature/standard

manifest: support interface checking
This commit is contained in:
Roman Khimov 2020-11-26 14:46:13 +03:00 committed by GitHub
commit badb1d6d49
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 523 additions and 21 deletions

View file

@ -104,3 +104,72 @@ func TestComlileAndInvokeFunction(t *testing.T) {
require.Equal(t, []byte("on update|sub update"), res.Stack[0].Value())
})
}
func TestCompileExamples(t *testing.T) {
const examplePath = "../examples"
infos, err := ioutil.ReadDir(examplePath)
require.NoError(t, err)
// For proper nef generation.
config.Version = "0.90.0-test"
tmpDir := os.TempDir()
e := newExecutor(t, false)
defer e.Close(t)
for _, info := range infos {
t.Run(info.Name(), func(t *testing.T) {
infos, err := ioutil.ReadDir(path.Join(examplePath, info.Name()))
require.NoError(t, err)
require.False(t, len(infos) == 0, "detected smart contract folder with no contract in it")
outPath := path.Join(tmpDir, info.Name()+".nef")
manifestPath := path.Join(tmpDir, info.Name()+".manifest.json")
defer func() {
os.Remove(outPath)
os.Remove(manifestPath)
}()
cfgName := filterFilename(infos, ".yml")
opts := []string{
"neo-go", "contract", "compile",
"--in", path.Join(examplePath, info.Name()),
"--out", outPath,
"--manifest", manifestPath,
"--config", path.Join(examplePath, info.Name(), cfgName),
}
e.Run(t, opts...)
})
}
t.Run("invalid events in manifest", func(t *testing.T) {
const dir = "./testdata/"
for _, name := range []string{"invalid1", "invalid2", "invalid3"} {
outPath := path.Join(tmpDir, name+".nef")
manifestPath := path.Join(tmpDir, name+".manifest.json")
defer func() {
os.Remove(outPath)
os.Remove(manifestPath)
}()
e.RunWithError(t, "neo-go", "contract", "compile",
"--in", path.Join(dir, name),
"--out", outPath,
"--manifest", manifestPath,
"--config", path.Join(dir, name, "invalid.yml"),
)
}
})
}
func filterFilename(infos []os.FileInfo, ext string) string {
for _, info := range infos {
if !info.IsDir() {
name := info.Name()
if strings.HasSuffix(name, ext) {
return name
}
}
}
return ""
}

View file

@ -150,6 +150,14 @@ func NewCommands() []cli.Command {
Name: "config, c",
Usage: "Configuration input file (*.yml)",
},
cli.BoolFlag{
Name: "no-standards",
Usage: "do not check compliance with supported standards",
},
cli.BoolFlag{
Name: "no-events",
Usage: "do not check emitted events with the manifest",
},
},
},
{
@ -398,6 +406,9 @@ func contractCompile(ctx *cli.Context) error {
DebugInfo: debugFile,
ManifestFile: manifestFile,
NoStandardCheck: ctx.Bool("no-standards"),
NoEventsCheck: ctx.Bool("no-events"),
}
if len(confFile) != 0 {

21
cli/testdata/invalid1/invalid.go vendored Normal file
View file

@ -0,0 +1,21 @@
// invalid is an example of contract which doesn't pass event check.
package invalid1
import (
"github.com/nspcc-dev/neo-go/pkg/interop"
"github.com/nspcc-dev/neo-go/pkg/interop/runtime"
)
// Notify1 emits correctly typed event.
func Notify1() bool {
runtime.Notify("Event", interop.Hash160{1, 2, 3})
return true
}
// Notify2 emits invalid event (ByteString instead of Hash160).
func Notify2() bool {
runtime.Notify("Event", []byte{1, 2, 3})
return true
}

7
cli/testdata/invalid1/invalid.yml vendored Normal file
View file

@ -0,0 +1,7 @@
name: "Invalid example"
supportedstandards: []
events:
- name: Event
parameters:
- name: address
type: Hash160

21
cli/testdata/invalid2/invalid.go vendored Normal file
View file

@ -0,0 +1,21 @@
// invalid is an example of contract which doesn't pass event check.
package invalid2
import (
"github.com/nspcc-dev/neo-go/pkg/interop"
"github.com/nspcc-dev/neo-go/pkg/interop/runtime"
)
// Notify1 emits correctly typed event.
func Notify1() bool {
runtime.Notify("Event", interop.Hash160{1, 2, 3})
return true
}
// Notify2 emits invalid event (extra parameter).
func Notify2() bool {
runtime.Notify("Event", interop.Hash160{1, 2, 3}, "extra parameter")
return true
}

7
cli/testdata/invalid2/invalid.yml vendored Normal file
View file

@ -0,0 +1,7 @@
name: "Invalid example"
supportedstandards: []
events:
- name: Event
parameters:
- name: address
type: Hash160

21
cli/testdata/invalid3/invalid.go vendored Normal file
View file

@ -0,0 +1,21 @@
// invalid is an example of contract which doesn't pass event check.
package invalid3
import (
"github.com/nspcc-dev/neo-go/pkg/interop"
"github.com/nspcc-dev/neo-go/pkg/interop/runtime"
)
// Notify1 emits correctly typed event.
func Notify1() bool {
runtime.Notify("Event", interop.Hash160{1, 2, 3})
return true
}
// Notify2 emits invalid event (missing from manifest).
func Notify2() bool {
runtime.Notify("AnotherEvent", interop.Hash160{1, 2, 3})
return true
}

7
cli/testdata/invalid3/invalid.yml vendored Normal file
View file

@ -0,0 +1,7 @@
name: "Invalid example"
supportedstandards: []
events:
- name: Event
parameters:
- name: address
type: Hash160

View file

@ -4,4 +4,16 @@ events:
- name: Tx
parameters:
- name: txHash
type: ByteString
type: Hash256
- name: Calling
parameters:
- name: hash
type: Hash160
- name: Executing
parameters:
- name: hash
type: Hash160
- name: Entry
parameters:
- name: hash
type: Hash160

View file

@ -13,7 +13,9 @@ func NotifyKeysAndValues() bool {
keys := iterator.Keys(iter)
runtime.Notify("found storage values", values)
runtime.Notify("found storage keys", keys)
// For illustration purposes event is emitted with 'Any' type.
var typedKeys interface{} = keys
runtime.Notify("found storage keys", typedKeys)
return true
}

View file

@ -4,7 +4,7 @@ events:
- name: found storage values
parameters:
- name: values
type: Any
type: InteropInterface
- name: found storage keys
parameters:
- name: keys

View file

@ -1,6 +1,7 @@
package tokensale
import (
"github.com/nspcc-dev/neo-go/pkg/interop"
"github.com/nspcc-dev/neo-go/pkg/interop/runtime"
"github.com/nspcc-dev/neo-go/pkg/interop/storage"
"github.com/nspcc-dev/neo-go/pkg/interop/util"
@ -134,39 +135,39 @@ func checkOwnerWitness() bool {
}
// Decimals returns the token decimals
func Decimals() interface{} {
func Decimals() int {
if trigger != runtime.Application {
return false
panic("invalid trigger")
}
return token.Decimals
}
// Symbol returns the token symbol
func Symbol() interface{} {
func Symbol() string {
if trigger != runtime.Application {
return false
panic("invalid trigger")
}
return token.Symbol
}
// TotalSupply returns the token total supply value
func TotalSupply() interface{} {
func TotalSupply() int {
if trigger != runtime.Application {
return false
panic("invalid trigger")
}
return getIntFromDB(ctx, token.CirculationKey)
}
// BalanceOf returns the amount of token on the specified address
func BalanceOf(holder []byte) interface{} {
func BalanceOf(holder interop.Hash160) int {
if trigger != runtime.Application {
return false
panic("invalid trigger")
}
return getIntFromDB(ctx, holder)
}
// Transfer transfers specified amount of token from one user to another
func Transfer(from, to []byte, amount int) bool {
func Transfer(from, to interop.Hash160, amount int, _ interface{}) bool {
if trigger != runtime.Application {
return false
}

View file

@ -1,3 +1,11 @@
name: "My awesome token"
supportedstandards: ["NEP-17"]
events: []
events:
- name: Transfer
parameters:
- name: from
type: Hash160
- name: to
type: Hash160
- name: amount
type: Integer

View file

@ -1,6 +1,7 @@
package nep17
import (
"github.com/nspcc-dev/neo-go/pkg/interop"
"github.com/nspcc-dev/neo-go/pkg/interop/runtime"
"github.com/nspcc-dev/neo-go/pkg/interop/storage"
"github.com/nspcc-dev/neo-go/pkg/interop/util"
@ -44,7 +45,7 @@ func (t Token) BalanceOf(ctx storage.Context, holder []byte) int {
}
// Transfer token from one user to another
func (t Token) Transfer(ctx storage.Context, from []byte, to []byte, amount int, data interface{}) bool {
func (t Token) Transfer(ctx storage.Context, from, to interop.Hash160, amount int, data interface{}) bool {
amountFrom := t.CanTransfer(ctx, from, to, amount)
if amountFrom == -1 {
return false
@ -62,7 +63,7 @@ func (t Token) Transfer(ctx storage.Context, from []byte, to []byte, amount int,
amountTo := getIntFromDB(ctx, to)
totalAmountTo := amountTo + amount
storage.Put(ctx, to, totalAmountTo)
runtime.Notify("transfer", from, to, amount)
runtime.Notify("Transfer", from, to, amount)
return true
}
@ -105,7 +106,7 @@ func IsUsableAddress(addr []byte) bool {
}
// Mint initial supply of tokens
func (t Token) Mint(ctx storage.Context, to []byte) bool {
func (t Token) Mint(ctx storage.Context, to interop.Hash160) bool {
if !IsUsableAddress(t.Owner) {
return false
}
@ -117,6 +118,7 @@ func (t Token) Mint(ctx storage.Context, to []byte) bool {
storage.Put(ctx, to, t.TotalSupply)
storage.Put(ctx, []byte("minted"), true)
storage.Put(ctx, []byte(t.CirculationKey), t.TotalSupply)
runtime.Notify("transfer", "", to, t.TotalSupply)
var from interop.Hash160
runtime.Notify("Transfer", from, to, t.TotalSupply)
return true
}

View file

@ -48,7 +48,7 @@ func TotalSupply() int {
}
// BalanceOf returns the amount of token on the specified address
func BalanceOf(holder interop.Hash160) interface{} {
func BalanceOf(holder interop.Hash160) int {
return token.BalanceOf(ctx, holder)
}
@ -58,6 +58,6 @@ func Transfer(from interop.Hash160, to interop.Hash160, amount int, data interfa
}
// Mint initial supply of tokens
func Mint(to []byte) bool {
func Mint(to interop.Hash160) bool {
return token.Mint(ctx, to)
}

View file

@ -4,8 +4,8 @@ events:
- name: Transfer
parameters:
- name: from
type: ByteString
type: Hash160
- name: to
type: ByteString
type: Hash160
- name: amount
type: Integer

View file

@ -87,6 +87,9 @@ type codegen struct {
// docIndex maps file path to an index in documents array.
docIndex map[string]int
// emittedEvents contains all events emitted by contract.
emittedEvents map[string][][]string
// Label table for recording jump destinations.
l []int
}
@ -870,6 +873,15 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor {
ast.Walk(c, n.Fun)
emit.Opcodes(c.prog.BinWriter, opcode.CALLA)
case isSyscall(f):
if f.pkg.Name() == "runtime" && f.name == "Notify" {
tv := c.typeAndValueOf(n.Args[0])
params := make([]string, 0, len(n.Args[1:]))
for _, p := range n.Args[1:] {
params = append(params, c.scTypeFromExpr(p))
}
name := constant.StringVal(tv.Value)
c.emittedEvents[name] = append(c.emittedEvents[name], params)
}
c.convertSyscall(n, f.pkg.Name(), f.name)
default:
emit.Call(c.prog.BinWriter, opcode.CALLL, f.label)
@ -1883,6 +1895,7 @@ func newCodegen(info *buildInfo, pkg *loader.PackageInfo) *codegen {
initEndOffset: -1,
deployEndOffset: -1,
emittedEvents: make(map[string][][]string),
sequencePoints: make(map[string][]DebugSeqPoint),
}
}

View file

@ -14,6 +14,7 @@ import (
"strings"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest/standard"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/nef"
"golang.org/x/tools/go/loader"
)
@ -34,6 +35,14 @@ type Options struct {
// The name of the output for contract manifest file.
ManifestFile string
// NoEventsCheck specifies if events emitted by contract needs to be present in manifest.
// This setting has effect only if manifest is emitted.
NoEventsCheck bool
// NoStandardCheck specifies if supported standards compliance needs to be checked.
// This setting has effect only if manifest is emitted.
NoStandardCheck bool
// Name is contract's name to be written to manifest.
Name string
@ -214,6 +223,33 @@ func CompileAndSave(src string, o *Options) ([]byte, error) {
if err != nil {
return b, fmt.Errorf("failed to convert debug info to manifest: %w", err)
}
if !o.NoStandardCheck {
if err := standard.Check(m, o.ContractSupportedStandards...); err != nil {
return b, err
}
}
if !o.NoEventsCheck {
for name := range di.EmittedEvents {
ev := m.ABI.GetEvent(name)
if ev == nil {
return nil, fmt.Errorf("event '%s' is emitted but not specified in manifest", name)
}
argsList := di.EmittedEvents[name]
for i := range argsList {
if len(argsList[i]) != len(ev.Parameters) {
return nil, fmt.Errorf("event '%s' should have %d parameters but has %d",
name, len(ev.Parameters), len(argsList[i]))
}
for j := range ev.Parameters {
expected := ev.Parameters[j].Type.String()
if argsList[i][j] != expected {
return nil, fmt.Errorf("event '%s' should have '%s' as type of %d parameter, "+
"got: %s", name, expected, j+1, argsList[i][j])
}
}
}
}
}
mData, err := json.Marshal(m)
if err != nil {
return b, fmt.Errorf("failed to marshal manifest to JSON: %w", err)

View file

@ -24,6 +24,8 @@ type DebugInfo struct {
Documents []string `json:"documents"`
Methods []MethodDebugInfo `json:"methods"`
Events []EventDebugInfo `json:"events"`
// EmittedEvents contains events occuring in code.
EmittedEvents map[string][][]string `json:"-"`
}
// MethodDebugInfo represents smart-contract's method debug information.
@ -162,6 +164,7 @@ func (c *codegen) emitDebugInfo(contract []byte) *DebugInfo {
}
d.Methods = append(d.Methods, *m)
}
d.EmittedEvents = c.emittedEvents
return d
}

View file

@ -89,6 +89,16 @@ func (a *ABI) GetMethod(name string) *Method {
return nil
}
// GetEvent returns event with the specified name.
func (a *ABI) GetEvent(name string) *Event {
for i := range a.Events {
if a.Events[i].Name == name {
return &a.Events[i]
}
}
return nil
}
// CanCall returns true is current contract is allowed to call
// method of another contract.
func (m *Manifest) CanCall(toCall *Manifest, method string) bool {

View file

@ -0,0 +1,76 @@
package standard
import (
"errors"
"fmt"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
)
// Various validation errors.
var (
ErrMethodMissing = errors.New("method missing")
ErrEventMissing = errors.New("event missing")
ErrInvalidReturnType = errors.New("invalid return type")
ErrInvalidParameterCount = errors.New("invalid parameter count")
ErrInvalidParameterType = errors.New("invalid parameter type")
)
var checks = map[string]*manifest.Manifest{
manifest.NEP17StandardName: nep17,
}
// Check checks if manifest complies with all provided standards.
// Currently only NEP-17 is supported.
func Check(m *manifest.Manifest, standards ...string) error {
for i := range standards {
s, ok := checks[standards[i]]
if ok {
if err := Comply(m, s); err != nil {
return fmt.Errorf("manifest is not compliant with '%s': %w", standards[i], err)
}
}
}
return nil
}
// Comply if m has all methods and event from st manifest and they have the same signature.
// Parameter names are ignored.
func Comply(m, st *manifest.Manifest) error {
for _, stm := range st.ABI.Methods {
name := stm.Name
md := m.ABI.GetMethod(name)
if md == nil {
return fmt.Errorf("%w: '%s'", ErrMethodMissing, name)
} else if stm.ReturnType != md.ReturnType {
return fmt.Errorf("%w: '%s' (expected %s, got %s)", ErrInvalidReturnType,
name, stm.ReturnType, md.ReturnType)
} else if len(stm.Parameters) != len(md.Parameters) {
return fmt.Errorf("%w: '%s' (expected %d, got %d)", ErrInvalidParameterCount,
name, len(stm.Parameters), len(md.Parameters))
}
for i := range stm.Parameters {
if stm.Parameters[i].Type != md.Parameters[i].Type {
return fmt.Errorf("%w: '%s'[%d] (expected %s, got %s)", ErrInvalidParameterType,
name, i, stm.Parameters[i].Type, md.Parameters[i].Type)
}
}
}
for _, ste := range st.ABI.Events {
name := ste.Name
ed := m.ABI.GetEvent(name)
if ed == nil {
return fmt.Errorf("%w: event '%s'", ErrEventMissing, name)
} else if len(ste.Parameters) != len(ed.Parameters) {
return fmt.Errorf("%w: event '%s' (expected %d, got %d)", ErrInvalidParameterCount,
name, len(ste.Parameters), len(ed.Parameters))
}
for i := range ste.Parameters {
if ste.Parameters[i].Type != ed.Parameters[i].Type {
return fmt.Errorf("%w: event '%s' (expected %s, got %s)", ErrInvalidParameterType,
name, ste.Parameters[i].Type, ed.Parameters[i].Type)
}
}
}
return nil
}

View file

@ -0,0 +1,113 @@
package standard
import (
"errors"
"testing"
"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/stretchr/testify/require"
)
func fooMethodBarEvent() *manifest.Manifest {
return &manifest.Manifest{
ABI: manifest.ABI{
Methods: []manifest.Method{
{
Name: "foo",
Parameters: []manifest.Parameter{
{Type: smartcontract.ByteArrayType},
{Type: smartcontract.PublicKeyType},
},
ReturnType: smartcontract.IntegerType,
},
},
Events: []manifest.Event{
{
Name: "bar",
Parameters: []manifest.Parameter{
{Type: smartcontract.StringType},
},
},
},
},
}
}
func TestComplyMissingMethod(t *testing.T) {
m := fooMethodBarEvent()
m.ABI.GetMethod("foo").Name = "notafoo"
err := Comply(m, fooMethodBarEvent())
require.True(t, errors.Is(err, ErrMethodMissing))
}
func TestComplyInvalidReturnType(t *testing.T) {
m := fooMethodBarEvent()
m.ABI.GetMethod("foo").ReturnType = smartcontract.VoidType
err := Comply(m, fooMethodBarEvent())
require.True(t, errors.Is(err, ErrInvalidReturnType))
}
func TestComplyMethodParameterCount(t *testing.T) {
t.Run("Method", func(t *testing.T) {
m := fooMethodBarEvent()
f := m.ABI.GetMethod("foo")
f.Parameters = append(f.Parameters, manifest.Parameter{Type: smartcontract.BoolType})
err := Comply(m, fooMethodBarEvent())
require.True(t, errors.Is(err, ErrInvalidParameterCount))
})
t.Run("Event", func(t *testing.T) {
m := fooMethodBarEvent()
ev := m.ABI.GetEvent("bar")
ev.Parameters = append(ev.Parameters[:0])
err := Comply(m, fooMethodBarEvent())
require.True(t, errors.Is(err, ErrInvalidParameterCount))
})
}
func TestComplyParameterType(t *testing.T) {
t.Run("Method", func(t *testing.T) {
m := fooMethodBarEvent()
m.ABI.GetMethod("foo").Parameters[0].Type = smartcontract.InteropInterfaceType
err := Comply(m, fooMethodBarEvent())
require.True(t, errors.Is(err, ErrInvalidParameterType))
})
t.Run("Event", func(t *testing.T) {
m := fooMethodBarEvent()
m.ABI.GetEvent("bar").Parameters[0].Type = smartcontract.InteropInterfaceType
err := Comply(m, fooMethodBarEvent())
require.True(t, errors.Is(err, ErrInvalidParameterType))
})
}
func TestMissingEvent(t *testing.T) {
m := fooMethodBarEvent()
m.ABI.GetEvent("bar").Name = "notabar"
err := Comply(m, fooMethodBarEvent())
require.True(t, errors.Is(err, ErrEventMissing))
}
func TestComplyValid(t *testing.T) {
m := fooMethodBarEvent()
m.ABI.Methods = append(m.ABI.Methods, manifest.Method{
Name: "newmethod",
Offset: 123,
ReturnType: smartcontract.ByteArrayType,
})
m.ABI.Events = append(m.ABI.Events, manifest.Event{
Name: "otherevent",
Parameters: []manifest.Parameter{{
Name: "names do not matter",
Type: smartcontract.IntegerType,
}},
})
require.NoError(t, Comply(m, fooMethodBarEvent()))
}
func TestCheck(t *testing.T) {
m := manifest.NewManifest(util.Uint160{}, "Test")
require.Error(t, Check(m, manifest.NEP17StandardName))
require.NoError(t, Check(nep17, manifest.NEP17StandardName))
}

View file

@ -0,0 +1,5 @@
/*
Package standard contains interfaces for well-defined standards
and function for checking if arbitrary manifest complies with them.
*/
package standard

View file

@ -0,0 +1,57 @@
package standard
import (
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
)
var nep17 = &manifest.Manifest{
ABI: manifest.ABI{
Methods: []manifest.Method{
{
Name: "balanceOf",
Parameters: []manifest.Parameter{
{Type: smartcontract.Hash160Type},
},
ReturnType: smartcontract.IntegerType,
},
{
Name: "decimals",
ReturnType: smartcontract.IntegerType,
},
{
Name: "symbol",
ReturnType: smartcontract.StringType,
},
{
Name: "totalSupply",
ReturnType: smartcontract.IntegerType,
},
{
Name: "transfer",
Parameters: []manifest.Parameter{
{Type: smartcontract.Hash160Type},
{Type: smartcontract.Hash160Type},
{Type: smartcontract.IntegerType},
{Type: smartcontract.AnyType},
},
ReturnType: smartcontract.BoolType,
},
},
Events: []manifest.Event{
{
Name: "Transfer",
Parameters: []manifest.Parameter{
{Type: smartcontract.Hash160Type},
{Type: smartcontract.Hash160Type},
{Type: smartcontract.IntegerType},
},
},
},
},
}
// IsNEP17 checks if m is NEP-17 compliant.
func IsNEP17(m *manifest.Manifest) error {
return Comply(m, nep17)
}