compiler: check emitted event names

Check that all `Notify` invocations in source correspond to some event
in manifest.
Helpful for typos such as `transfer` instead of `Transfer`.
This commit is contained in:
Evgenii Stratonikov 2020-11-24 16:36:35 +03:00
parent 25f1db6de0
commit 75a9a42403
15 changed files with 172 additions and 7 deletions

View file

@ -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 {

View file

@ -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 {

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 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

@ -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

@ -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)

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
}