diff --git a/cli/contract_test.go b/cli/contract_test.go index a9b391e91..7781f603a 100644 --- a/cli/contract_test.go +++ b/cli/contract_test.go @@ -142,6 +142,24 @@ func TestCompileExamples(t *testing.T) { 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 { diff --git a/cli/smartcontract/smart_contract.go b/cli/smartcontract/smart_contract.go index bb6095175..0e35439cb 100644 --- a/cli/smartcontract/smart_contract.go +++ b/cli/smartcontract/smart_contract.go @@ -154,6 +154,10 @@ func NewCommands() []cli.Command { 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", + }, }, }, { @@ -404,6 +408,7 @@ func contractCompile(ctx *cli.Context) error { ManifestFile: manifestFile, NoStandardCheck: ctx.Bool("no-standards"), + NoEventsCheck: ctx.Bool("no-events"), } if len(confFile) != 0 { diff --git a/cli/testdata/invalid1/invalid.go b/cli/testdata/invalid1/invalid.go new file mode 100644 index 000000000..634971c15 --- /dev/null +++ b/cli/testdata/invalid1/invalid.go @@ -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 +} diff --git a/cli/testdata/invalid1/invalid.yml b/cli/testdata/invalid1/invalid.yml new file mode 100644 index 000000000..73839caba --- /dev/null +++ b/cli/testdata/invalid1/invalid.yml @@ -0,0 +1,7 @@ +name: "Invalid example" +supportedstandards: [] +events: + - name: Event + parameters: + - name: address + type: Hash160 diff --git a/cli/testdata/invalid2/invalid.go b/cli/testdata/invalid2/invalid.go new file mode 100644 index 000000000..3af8a53ce --- /dev/null +++ b/cli/testdata/invalid2/invalid.go @@ -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 +} diff --git a/cli/testdata/invalid2/invalid.yml b/cli/testdata/invalid2/invalid.yml new file mode 100644 index 000000000..73839caba --- /dev/null +++ b/cli/testdata/invalid2/invalid.yml @@ -0,0 +1,7 @@ +name: "Invalid example" +supportedstandards: [] +events: + - name: Event + parameters: + - name: address + type: Hash160 diff --git a/cli/testdata/invalid3/invalid.go b/cli/testdata/invalid3/invalid.go new file mode 100644 index 000000000..ea8cd7618 --- /dev/null +++ b/cli/testdata/invalid3/invalid.go @@ -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 +} diff --git a/cli/testdata/invalid3/invalid.yml b/cli/testdata/invalid3/invalid.yml new file mode 100644 index 000000000..73839caba --- /dev/null +++ b/cli/testdata/invalid3/invalid.yml @@ -0,0 +1,7 @@ +name: "Invalid example" +supportedstandards: [] +events: + - name: Event + parameters: + - name: address + type: Hash160 diff --git a/examples/engine/engine.yml b/examples/engine/engine.yml index 4b2d24672..df94d5a01 100644 --- a/examples/engine/engine.yml +++ b/examples/engine/engine.yml @@ -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 \ No newline at end of file diff --git a/examples/iterator/iterator.go b/examples/iterator/iterator.go index fd5a0ad16..534bd0885 100644 --- a/examples/iterator/iterator.go +++ b/examples/iterator/iterator.go @@ -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 } diff --git a/examples/iterator/iterator.yml b/examples/iterator/iterator.yml index ab4788d0a..e714e9d8a 100644 --- a/examples/iterator/iterator.yml +++ b/examples/iterator/iterator.yml @@ -4,7 +4,7 @@ events: - name: found storage values parameters: - name: values - type: Any + type: InteropInterface - name: found storage keys parameters: - name: keys diff --git a/examples/token/nep17/nep17.go b/examples/token/nep17/nep17.go index fd07567f5..a962d19d3 100644 --- a/examples/token/nep17/nep17.go +++ b/examples/token/nep17/nep17.go @@ -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 } diff --git a/pkg/compiler/codegen.go b/pkg/compiler/codegen.go index 764830d7e..daf288664 100644 --- a/pkg/compiler/codegen.go +++ b/pkg/compiler/codegen.go @@ -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), } } diff --git a/pkg/compiler/compiler.go b/pkg/compiler/compiler.go index bae128f81..41fe8d0c8 100644 --- a/pkg/compiler/compiler.go +++ b/pkg/compiler/compiler.go @@ -35,6 +35,10 @@ 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 @@ -224,6 +228,28 @@ func CompileAndSave(src string, o *Options) ([]byte, error) { 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) diff --git a/pkg/compiler/debug.go b/pkg/compiler/debug.go index 02181a665..88e6bcc9a 100644 --- a/pkg/compiler/debug.go +++ b/pkg/compiler/debug.go @@ -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 }