package compiler_test import ( "fmt" "math/big" "os" "path/filepath" "strings" "testing" "github.com/nspcc-dev/neo-go/internal/random" "github.com/nspcc-dev/neo-go/internal/versionutil" "github.com/nspcc-dev/neo-go/pkg/compiler" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/interop/native/neo" "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/stackitem" "github.com/stretchr/testify/require" ) const examplePath = "../../examples" const exampleCompilePath = "testdata/compile" const exampleSavePath = exampleCompilePath + "/save" // Keep contract NEFs consistent between runs. const _ = versionutil.TestVersion type compilerTestCase struct { name string function func(*testing.T) } func TestCompiler(t *testing.T) { testCases := []compilerTestCase{ { name: "TestCompileDirectory", function: func(t *testing.T) { const multiMainDir = "testdata/multi" _, di, err := compiler.CompileWithOptions(multiMainDir, nil, nil) require.NoError(t, err) m := map[string]bool{} for i := range di.Methods { m[di.Methods[i].ID] = true } require.Contains(t, m, "Func1") require.Contains(t, m, "Func2") }, }, { name: "TestCompile", function: func(t *testing.T) { infos, err := os.ReadDir(examplePath) require.NoError(t, err) for _, info := range infos { if !info.IsDir() { // example smart contracts are located in the `examplePath` subdirectories, but // there is also a couple of files inside the `examplePath` which don't need to be compiled continue } if info.Name() == "zkp" { // A set of special ZKP-related examples, they have their own tests. continue } targetPath := filepath.Join(examplePath, info.Name()) require.NoError(t, compileFile(targetPath), info.Name()) } }, }, { name: "TestCompileAndSave", function: func(t *testing.T) { infos, err := os.ReadDir(exampleCompilePath) require.NoError(t, err) err = os.MkdirAll(exampleSavePath, os.ModePerm) require.NoError(t, err) t.Cleanup(func() { err := os.RemoveAll(exampleSavePath) require.NoError(t, err) }) outfile := exampleSavePath + "/test.nef" _, err = compiler.CompileAndSave(exampleCompilePath+"/"+infos[0].Name(), &compiler.Options{Outfile: outfile}) require.NoError(t, err) }, }, { name: "TestCompileAndSave_NEF_constraints", function: func(t *testing.T) { tmp := t.TempDir() src := `package nefconstraints var data = "%s" func Main() string { return data } ` data := make([]byte, stackitem.MaxSize-10) for i := range data { data[i] = byte('a') } in := filepath.Join(tmp, "src.go") require.NoError(t, os.WriteFile(in, []byte(fmt.Sprintf(src, data)), os.ModePerm)) out := filepath.Join(tmp, "test.nef") _, err := compiler.CompileAndSave(in, &compiler.Options{Outfile: out}) require.Error(t, err) require.Contains(t, err.Error(), "serialized NEF size exceeds VM stackitem limits") }, }, } for _, tcase := range testCases { t.Run(tcase.name, tcase.function) } } func compileFile(src string) error { _, err := compiler.Compile(src, nil) return err } func TestOnPayableChecks(t *testing.T) { compileAndCheck := func(t *testing.T, src string) error { _, di, err := compiler.CompileWithOptions("payable.go", strings.NewReader(src), nil) require.NoError(t, err) _, err = compiler.CreateManifest(di, &compiler.Options{Name: "payable"}) return err } t.Run("NEP-11, good", func(t *testing.T) { src := `package payable import "github.com/nspcc-dev/neo-go/pkg/interop" func OnNEP11Payment(from interop.Hash160, amount int, tokenID []byte, data any) {}` require.NoError(t, compileAndCheck(t, src)) }) t.Run("NEP-11, bad", func(t *testing.T) { src := `package payable import "github.com/nspcc-dev/neo-go/pkg/interop" func OnNEP11Payment(from interop.Hash160, amount int, oldParam string, tokenID []byte, data any) {}` require.Error(t, compileAndCheck(t, src)) }) t.Run("NEP-17, good", func(t *testing.T) { src := `package payable import "github.com/nspcc-dev/neo-go/pkg/interop" func OnNEP17Payment(from interop.Hash160, amount int, data any) {}` require.NoError(t, compileAndCheck(t, src)) }) t.Run("NEP-17, bad", func(t *testing.T) { src := `package payable import "github.com/nspcc-dev/neo-go/pkg/interop" func OnNEP17Payment(from interop.Hash160, amount int, data any, extra int) {}` require.Error(t, compileAndCheck(t, src)) }) } func TestSafeMethodWarnings(t *testing.T) { src := `package payable func Main() int { return 1 }` _, di, err := compiler.CompileWithOptions("eventTest.go", strings.NewReader(src), &compiler.Options{Name: "eventTest"}) require.NoError(t, err) _, err = compiler.CreateManifest(di, &compiler.Options{SafeMethods: []string{"main"}, Name: "eventTest"}) require.NoError(t, err) _, err = compiler.CreateManifest(di, &compiler.Options{SafeMethods: []string{"main", "mississippi"}, Name: "eventTest"}) require.Error(t, err) } func TestEventWarnings(t *testing.T) { src := `package payable import "github.com/nspcc-dev/neo-go/pkg/interop/runtime" func Main() { runtime.Notify("Event", 1) }` _, di, err := compiler.CompileWithOptions("eventTest.go", strings.NewReader(src), nil) require.NoError(t, err) t.Run("event it missing from config", func(t *testing.T) { _, err = compiler.CreateManifest(di, &compiler.Options{Name: "payable"}) require.Error(t, err) t.Run("suppress", func(t *testing.T) { _, err = compiler.CreateManifest(di, &compiler.Options{NoEventsCheck: true, Name: "payable"}) require.NoError(t, err) }) }) t.Run("wrong parameter number", func(t *testing.T) { _, err = compiler.CreateManifest(di, &compiler.Options{ ContractEvents: []compiler.HybridEvent{{Name: "Event"}}, Name: "payable", }) require.Error(t, err) }) t.Run("wrong parameter type", func(t *testing.T) { _, err = compiler.CreateManifest(di, &compiler.Options{ ContractEvents: []compiler.HybridEvent{{ Name: "Event", Parameters: []compiler.HybridParameter{{Parameter: manifest.NewParameter("number", smartcontract.StringType)}}, }}, Name: "payable", }) require.Error(t, err) }) t.Run("any parameter type", func(t *testing.T) { _, err = compiler.CreateManifest(di, &compiler.Options{ ContractEvents: []compiler.HybridEvent{{ Name: "Event", Parameters: []compiler.HybridParameter{{Parameter: manifest.NewParameter("number", smartcontract.AnyType)}}, }}, Name: "payable", }) require.NoError(t, err) }) t.Run("good", func(t *testing.T) { _, err = compiler.CreateManifest(di, &compiler.Options{ ContractEvents: []compiler.HybridEvent{{ Name: "Event", Parameters: []compiler.HybridParameter{{Parameter: manifest.NewParameter("number", smartcontract.IntegerType)}}, }}, Name: "payable", }) require.NoError(t, err) }) t.Run("event in imported package", func(t *testing.T) { t.Run("unused", func(t *testing.T) { src := `package foo import "github.com/nspcc-dev/neo-go/pkg/compiler/testdata/notify" func Main() int { return notify.Value }` _, di, err := compiler.CompileWithOptions("eventTest.go", strings.NewReader(src), &compiler.Options{Name: "eventTest"}) require.NoError(t, err) _, err = compiler.CreateManifest(di, &compiler.Options{NoEventsCheck: true, Name: "eventTest"}) require.NoError(t, err) }) t.Run("used", func(t *testing.T) { src := `package foo import "github.com/nspcc-dev/neo-go/pkg/compiler/testdata/notify" func Main() int { notify.EmitEvent() return 42 }` _, di, err := compiler.CompileWithOptions("eventTest.go", strings.NewReader(src), &compiler.Options{Name: "eventTest"}) require.NoError(t, err) _, err = compiler.CreateManifest(di, &compiler.Options{Name: "eventTest"}) require.Error(t, err) _, err = compiler.CreateManifest(di, &compiler.Options{ ContractEvents: []compiler.HybridEvent{{Name: "Event"}}, Name: "eventTest", }) require.NoError(t, err) }) }) t.Run("variadic event args via ellipsis", func(t *testing.T) { src := `package payable import "github.com/nspcc-dev/neo-go/pkg/interop/runtime" func Main() { runtime.Notify("Event", []any{1}...) }` _, di, err := compiler.CompileWithOptions("eventTest.go", strings.NewReader(src), nil) require.NoError(t, err) _, err = compiler.CreateManifest(di, &compiler.Options{ Name: "eventTest", ContractEvents: []compiler.HybridEvent{{ Name: "Event", Parameters: []compiler.HybridParameter{{Parameter: manifest.NewParameter("number", smartcontract.IntegerType)}}, }}, }) require.NoError(t, err) }) } func TestNotifyInVerify(t *testing.T) { srcTmpl := `package payable import "github.com/nspcc-dev/neo-go/pkg/interop/runtime" func Verify() bool { runtime.%s("Event"); return true }` for _, name := range []string{"Notify", "Log"} { t.Run(name, func(t *testing.T) { src := fmt.Sprintf(srcTmpl, name) _, _, err := compiler.CompileWithOptions("eventTest.go", strings.NewReader(src), &compiler.Options{ContractEvents: []compiler.HybridEvent{{Name: "Event"}}}) require.Error(t, err) t.Run("suppress", func(t *testing.T) { _, _, err := compiler.CompileWithOptions("eventTest.go", strings.NewReader(src), &compiler.Options{NoEventsCheck: true}) require.NoError(t, err) }) }) } } func TestInvokedContractsPermissons(t *testing.T) { testCompile := func(t *testing.T, di *compiler.DebugInfo, disable bool, ps ...manifest.Permission) error { o := &compiler.Options{ NoPermissionsCheck: disable, Permissions: ps, Name: "test", } _, err := compiler.CreateManifest(di, o) return err } t.Run("native", func(t *testing.T) { src := `package test import "github.com/nspcc-dev/neo-go/pkg/interop/native/neo" import "github.com/nspcc-dev/neo-go/pkg/interop/native/management" func Main() int { neo.Transfer(nil, nil, 10, nil) management.GetContract(nil) // skip read-only return 0 }` _, di, err := compiler.CompileWithOptions("permissionTest.go", strings.NewReader(src), nil) require.NoError(t, err) var nh util.Uint160 p := manifest.NewPermission(manifest.PermissionHash, nh) require.Error(t, testCompile(t, di, false, *p)) require.NoError(t, testCompile(t, di, true, *p)) copy(nh[:], neo.Hash) p.Contract.Value = nh require.NoError(t, testCompile(t, di, false, *p)) p.Methods.Restrict() require.Error(t, testCompile(t, di, false, *p)) require.NoError(t, testCompile(t, di, true, *p)) }) t.Run("custom", func(t *testing.T) { hashStr := "aaaaaaaaaaaaaaaaaaaa" src := fmt.Sprintf(`package test import "github.com/nspcc-dev/neo-go/pkg/interop/contract" import "github.com/nspcc-dev/neo-go/pkg/interop" import "github.com/nspcc-dev/neo-go/pkg/compiler/testdata/runh" const hash = "%s" var runtimeHash interop.Hash160 var runtimeMethod string func invoke(h string) interop.Hash160 { return nil } func Main() { contract.Call(interop.Hash160(hash), "method1", contract.All) contract.Call(interop.Hash160(hash), "method2", contract.All) contract.Call(interop.Hash160(hash), "method2", contract.All) // skip read-only contract.Call(interop.Hash160(hash), "method3", contract.ReadStates) // skip this contract.Call(interop.Hash160(hash), runtimeMethod, contract.All) contract.Call(runtimeHash, "someMethod", contract.All) contract.Call(interop.Hash160(runtimeHash), "someMethod", contract.All) contract.Call(runh.RuntimeHash(), "method4", contract.All) }`, hashStr) _, di, err := compiler.CompileWithOptions("permissionTest.go", strings.NewReader(src), nil) require.NoError(t, err) var h util.Uint160 copy(h[:], hashStr) p := manifest.NewPermission(manifest.PermissionHash, h) require.NoError(t, testCompile(t, di, false, *p)) p.Methods.Add("method1") require.Error(t, testCompile(t, di, false, *p)) require.NoError(t, testCompile(t, di, true, *p)) pr := manifest.NewPermission(manifest.PermissionHash, random.Uint160()) pr.Methods.Add("someMethod") pr.Methods.Add("method4") t.Run("wildcard", func(t *testing.T) { pw := manifest.NewPermission(manifest.PermissionWildcard) require.NoError(t, testCompile(t, di, false, *p, *pw)) pw.Methods.Add("method2") require.Error(t, testCompile(t, di, false, *p, *pw)) require.NoError(t, testCompile(t, di, false, *p, *pw, *pr)) }) t.Run("group", func(t *testing.T) { priv, _ := keys.NewPrivateKey() pw := manifest.NewPermission(manifest.PermissionGroup, priv.PublicKey()) require.NoError(t, testCompile(t, di, false, *p, *pw)) pw.Methods.Add("invalid") require.Error(t, testCompile(t, di, false, *p, *pw, *pr)) pw.Methods.Add("method2") require.Error(t, testCompile(t, di, false, *p, *pw)) require.NoError(t, testCompile(t, di, false, *p, *pw, *pr)) }) }) } func TestUnnamedParameterCheck(t *testing.T) { t.Run("single argument", func(t *testing.T) { src := ` package testcase func Main(_ int) int { x := 10 return x } ` _, _, err := compiler.CompileWithOptions("test.go", strings.NewReader(src), nil) require.Error(t, err) require.ErrorIs(t, err, compiler.ErrMissingExportedParamName) }) t.Run("several arguments", func(t *testing.T) { src := ` package testcase func Main(a int, b string, _ int) int { x := 10 return x } ` _, _, err := compiler.CompileWithOptions("test.go", strings.NewReader(src), nil) require.Error(t, err) require.ErrorIs(t, err, compiler.ErrMissingExportedParamName) }) t.Run("interface", func(t *testing.T) { src := ` package testcase func OnNEP17Payment(h string, i int, _ any){} ` _, _, err := compiler.CompileWithOptions("test.go", strings.NewReader(src), nil) require.Error(t, err) require.ErrorIs(t, err, compiler.ErrMissingExportedParamName) }) t.Run("a set of unnamed params", func(t *testing.T) { src := ` package testcase func OnNEP17Payment(_ string, _ int, _ any){} ` _, _, err := compiler.CompileWithOptions("test.go", strings.NewReader(src), nil) require.Error(t, err) require.ErrorIs(t, err, compiler.ErrMissingExportedParamName) }) t.Run("mixed named and unnamed params", func(t *testing.T) { src := ` package testcase func OnNEP17Payment(s0, _, s2 string){} ` _, _, err := compiler.CompileWithOptions("test.go", strings.NewReader(src), nil) require.Error(t, err) require.ErrorIs(t, err, compiler.ErrMissingExportedParamName) }) t.Run("empty args", func(t *testing.T) { src := ` package testcase func OnNEP17Payment(){} ` _, _, err := compiler.CompileWithOptions("test.go", strings.NewReader(src), nil) require.NoError(t, err) }) t.Run("good", func(t *testing.T) { src := ` package testcase func OnNEP17Payment(s string, i int, iface interface{}){} ` _, _, err := compiler.CompileWithOptions("test.go", strings.NewReader(src), nil) require.NoError(t, err) }) t.Run("good, use any keyword", func(t *testing.T) { src := ` package testcase func OnNEP17Payment(s string, i int, iface any){} ` _, _, err := compiler.CompileWithOptions("test.go", strings.NewReader(src), nil) require.NoError(t, err) }) t.Run("method with unnamed params", func(t *testing.T) { src := ` package testcase type A int func (rsv A) OnNEP17Payment(_ string, _ int, iface any){} ` _, _, err := compiler.CompileWithOptions("test.go", strings.NewReader(src), nil) require.NoError(t, err) // it's OK for exported method to have unnamed params as it won't be included into manifest }) } func TestReturnValuesCountCheck(t *testing.T) { t.Run("void", func(t *testing.T) { t.Run("exported", func(t *testing.T) { t.Run("func", func(t *testing.T) { src := `package testcase var a int func Main() { a = 5 }` _, _, err := compiler.CompileWithOptions("test.go", strings.NewReader(src), nil) require.NoError(t, err) }) t.Run("method", func(t *testing.T) { src := `package testcase type A int var a int func (rcv A) Main() { a = 5 }` _, _, err := compiler.CompileWithOptions("test.go", strings.NewReader(src), nil) require.NoError(t, err) }) }) t.Run("unexported", func(t *testing.T) { src := `package testcase var a int func main() { a = 5 }` _, _, err := compiler.CompileWithOptions("test.go", strings.NewReader(src), nil) require.NoError(t, err) }) }) t.Run("single return", func(t *testing.T) { t.Run("exported", func(t *testing.T) { t.Run("func", func(t *testing.T) { src := `package testcase var a int func Main() int { a = 5 return a }` eval(t, src, big.NewInt(5)) }) t.Run("method", func(t *testing.T) { src := `package testcase type A int var a int func (rcv A) Main() int { a = 5 return a }` _, _, err := compiler.CompileWithOptions("test.go", strings.NewReader(src), nil) require.NoError(t, err) }) }) t.Run("unexported", func(t *testing.T) { src := `package testcase var a int func main() int { a = 5 return a }` _, _, err := compiler.CompileWithOptions("test.go", strings.NewReader(src), nil) require.NoError(t, err) }) }) t.Run("multiple unnamed return vals", func(t *testing.T) { t.Run("exported", func(t *testing.T) { t.Run("func", func(t *testing.T) { src := `package testcase var a int func Main() (int, int) { a = 5 return a, a }` _, _, err := compiler.CompileWithOptions("test.go", strings.NewReader(src), nil) require.Error(t, err) require.ErrorIs(t, err, compiler.ErrInvalidExportedRetCount) }) t.Run("method", func(t *testing.T) { src := `package testcase type A int var a int func (rcv A) Main() (int, int) { a = 5 return a, a }` _, _, err := compiler.CompileWithOptions("test.go", strings.NewReader(src), nil) require.NoError(t, err) // OK for method to have multiple return values as it won't be included into manifest }) }) t.Run("unexported", func(t *testing.T) { src := `package testcase var a int func main() (int, int) { a = 5 return a, a }` _, _, err := compiler.CompileWithOptions("test.go", strings.NewReader(src), nil) require.NoError(t, err) // OK for unexported function to have multiple return values as it won't be included into manifest }) }) t.Run("multiple named return vals", func(t *testing.T) { t.Run("exported", func(t *testing.T) { t.Run("func", func(t *testing.T) { src := `package testcase var a int func Main() (a int, b int) { a = 5 b = 2 return }` _, _, err := compiler.CompileWithOptions("test.go", strings.NewReader(src), nil) require.Error(t, err) require.ErrorIs(t, err, compiler.ErrInvalidExportedRetCount) }) t.Run("method", func(t *testing.T) { src := `package testcase type A int var a int func (rcv A) Main() (a int, b int) { a = 5 b = 2 return }` _, _, err := compiler.CompileWithOptions("test.go", strings.NewReader(src), nil) require.NoError(t, err) // OK for method to have multiple return values as it won't be included into manifest }) }) t.Run("unexported", func(t *testing.T) { src := `package testcase var a int func main() (a int, b int) { a = 5 b = 2 return }` _, _, err := compiler.CompileWithOptions("test.go", strings.NewReader(src), nil) require.NoError(t, err) // OK for unexported function to have multiple return values as it won't be included into manifest }) }) }