From 2eb256014e95d8e46b51a98bb031906cfc8a2ebd Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Wed, 2 Dec 2020 12:10:43 +0300 Subject: [PATCH 1/8] core: move `System.Runtime.*` interops to `runtime/` package Also extend test suite. --- pkg/core/interop/runtime/engine.go | 93 +++++++++++++ pkg/core/interop/runtime/engine_test.go | 175 ++++++++++++++++++++++++ pkg/core/interop/runtime/util_test.go | 116 ++++++++++++++++ pkg/core/interop_neo_test.go | 6 - pkg/core/interop_system.go | 81 ----------- pkg/core/interop_system_test.go | 71 ---------- pkg/core/interops.go | 16 +-- 7 files changed, 392 insertions(+), 166 deletions(-) create mode 100644 pkg/core/interop/runtime/engine.go create mode 100644 pkg/core/interop/runtime/engine_test.go create mode 100644 pkg/core/interop/runtime/util_test.go diff --git a/pkg/core/interop/runtime/engine.go b/pkg/core/interop/runtime/engine.go new file mode 100644 index 000000000..7dac81171 --- /dev/null +++ b/pkg/core/interop/runtime/engine.go @@ -0,0 +1,93 @@ +package runtime + +import ( + "fmt" + + "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/vm/stackitem" + "go.uber.org/zap" +) + +const ( + // MaxEventNameLen is the maximum length of a name for event. + MaxEventNameLen = 32 + // MaxNotificationSize is the maximum length of a runtime log message. + MaxNotificationSize = 1024 +) + +// GetExecutingScriptHash returns executing script hash. +func GetExecutingScriptHash(ic *interop.Context) error { + return ic.VM.PushContextScriptHash(0) +} + +// GetCallingScriptHash returns calling script hash. +func GetCallingScriptHash(ic *interop.Context) error { + return ic.VM.PushContextScriptHash(1) +} + +// GetEntryScriptHash returns entry script hash. +func GetEntryScriptHash(ic *interop.Context) error { + return ic.VM.PushContextScriptHash(ic.VM.Istack().Len() - 1) +} + +// Platform returns the name of the platform. +func Platform(ic *interop.Context) error { + ic.VM.Estack().PushVal([]byte("NEO")) + return nil +} + +// GetTrigger returns the script trigger. +func GetTrigger(ic *interop.Context) error { + ic.VM.Estack().PushVal(byte(ic.Trigger)) + return nil +} + +// Notify 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 Notify(ic *interop.Context) error { + name := ic.VM.Estack().Pop().String() + if len(name) > MaxEventNameLen { + return fmt.Errorf("event name must be less than %d", MaxEventNameLen) + } + elem := ic.VM.Estack().Pop() + args := elem.Array() + // But it has to be serializable, otherwise we either have some broken + // (recursive) structure inside or an interop item that can't be used + // outside of the interop subsystem anyway. + bytes, err := stackitem.SerializeItem(elem.Item()) + if err != nil { + return fmt.Errorf("bad notification: %w", err) + } + if len(bytes) > MaxNotificationSize { + return fmt.Errorf("notification size shouldn't exceed %d", MaxNotificationSize) + } + ne := state.NotificationEvent{ + ScriptHash: ic.VM.GetCurrentScriptHash(), + Name: name, + Item: stackitem.DeepCopy(stackitem.NewArray(args)).(*stackitem.Array), + } + ic.Notifications = append(ic.Notifications, ne) + return nil +} + +// Log logs the message passed. +func Log(ic *interop.Context) error { + state := ic.VM.Estack().Pop().String() + if len(state) > MaxNotificationSize { + return fmt.Errorf("message length shouldn't exceed %v", MaxNotificationSize) + } + msg := fmt.Sprintf("%q", state) + ic.Log.Info("runtime log", + zap.Stringer("script", ic.VM.GetCurrentScriptHash()), + zap.String("logs", msg)) + return nil +} + +// GetTime returns timestamp of the block being verified, or the latest +// one in the blockchain if no block is given to Context. +func GetTime(ic *interop.Context) error { + header := ic.Block.Header() + ic.VM.Estack().PushVal(header.Timestamp) + return nil +} diff --git a/pkg/core/interop/runtime/engine_test.go b/pkg/core/interop/runtime/engine_test.go new file mode 100644 index 000000000..18be7d1ef --- /dev/null +++ b/pkg/core/interop/runtime/engine_test.go @@ -0,0 +1,175 @@ +package runtime + +import ( + "encoding/json" + "math/big" + "math/rand" + "testing" + + "github.com/nspcc-dev/neo-go/internal/random" + "github.com/nspcc-dev/neo-go/pkg/config/netmode" + "github.com/nspcc-dev/neo-go/pkg/core/block" + "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/smartcontract" + "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" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" + "github.com/stretchr/testify/require" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "go.uber.org/zap/zaptest" +) + +func checkStack(t *testing.T, v *vm.VM, args ...interface{}) { + require.Equal(t, len(args), v.Estack().Len()) + for i := range args { + require.Equal(t, stackitem.Make(args[i]), v.Estack().Pop().Item(), "%d", i) + } +} + +func TestGetTrigger(t *testing.T) { + triggers := []trigger.Type{trigger.Application, trigger.Verification} + for _, tr := range triggers { + ic := &interop.Context{Trigger: tr, VM: vm.New()} + require.NoError(t, GetTrigger(ic)) + checkStack(t, ic.VM, int64(tr)) + } +} + +func TestPlatform(t *testing.T) { + ic := &interop.Context{VM: vm.New()} + require.NoError(t, Platform(ic)) + checkStack(t, ic.VM, "NEO") +} + +func TestGetTime(t *testing.T) { + b := block.New(netmode.UnitTestNet, false) + b.Timestamp = rand.Uint64() + ic := &interop.Context{VM: vm.New(), Block: b} + require.NoError(t, GetTime(ic)) + checkStack(t, ic.VM, new(big.Int).SetUint64(b.Timestamp)) +} + +func TestGetScriptHash(t *testing.T) { + scripts := []struct { + s []byte + h util.Uint160 + }{ + {[]byte{1, 2, 3, 4}, hash.Hash160([]byte{1, 2, 3, 4})}, + {[]byte{1, 2, 3}, util.Uint160{4, 8, 15, 16}}, + {[]byte{1, 2}, hash.Hash160([]byte{1, 2})}, + {[]byte{1}, hash.Hash160([]byte{1})}, + } + + ic := &interop.Context{VM: vm.New()} + ic.VM.LoadScriptWithFlags(scripts[0].s, smartcontract.All) + require.NoError(t, GetEntryScriptHash(ic)) + checkStack(t, ic.VM, scripts[0].h.BytesBE()) + require.NoError(t, GetCallingScriptHash(ic)) + checkStack(t, ic.VM, util.Uint160{}.BytesBE()) + require.NoError(t, GetExecutingScriptHash(ic)) + checkStack(t, ic.VM, scripts[0].h.BytesBE()) + + ic.VM.LoadScriptWithHash(scripts[1].s, scripts[1].h, smartcontract.All) + require.NoError(t, GetEntryScriptHash(ic)) + checkStack(t, ic.VM, scripts[0].h.BytesBE()) + require.NoError(t, GetCallingScriptHash(ic)) + checkStack(t, ic.VM, scripts[0].h.BytesBE()) + require.NoError(t, GetExecutingScriptHash(ic)) + checkStack(t, ic.VM, scripts[1].h.BytesBE()) + + ic.VM.LoadScript(scripts[2].s) + require.NoError(t, GetEntryScriptHash(ic)) + checkStack(t, ic.VM, scripts[0].h.BytesBE()) + require.NoError(t, GetCallingScriptHash(ic)) + checkStack(t, ic.VM, scripts[1].h.BytesBE()) + require.NoError(t, GetExecutingScriptHash(ic)) + checkStack(t, ic.VM, scripts[2].h.BytesBE()) + + ic.VM.LoadScript(scripts[3].s) + require.NoError(t, GetEntryScriptHash(ic)) + checkStack(t, ic.VM, scripts[0].h.BytesBE()) + require.NoError(t, GetCallingScriptHash(ic)) + checkStack(t, ic.VM, scripts[2].h.BytesBE()) + require.NoError(t, GetExecutingScriptHash(ic)) + checkStack(t, ic.VM, scripts[3].h.BytesBE()) +} + +func TestLog(t *testing.T) { + newL := func(l zapcore.Level) (*zap.Logger, *zaptest.Buffer) { + enc := zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()) + w := &zaptest.Buffer{} + zc := zapcore.NewCore(enc, w, l) + return zap.New(zc, zap.ErrorOutput(w)), w + } + h := random.Uint160() + + t.Run("big message", func(t *testing.T) { + ic := &interop.Context{Log: zap.NewNop(), VM: vm.New()} + ic.VM.LoadScriptWithHash([]byte{1}, h, smartcontract.All) + ic.VM.Estack().PushVal(string(make([]byte, MaxNotificationSize+1))) + require.Error(t, Log(ic)) + }) + + t.Run("good", func(t *testing.T) { + log, buf := newL(zapcore.InfoLevel) + ic := &interop.Context{Log: log, VM: vm.New()} + ic.VM.LoadScriptWithHash([]byte{1}, h, smartcontract.All) + ic.VM.Estack().PushVal("hello") + require.NoError(t, Log(ic)) + + ls := buf.Lines() + require.Equal(t, 1, len(ls)) + + var logMsg map[string]interface{} + require.NoError(t, json.Unmarshal([]byte(ls[0]), &logMsg)) + require.Equal(t, "info", logMsg["level"]) + require.Equal(t, "runtime log", logMsg["msg"]) + require.Equal(t, h.StringBE(), logMsg["script"]) + require.Equal(t, `"hello"`, logMsg["logs"]) + + }) +} + +func TestNotify(t *testing.T) { + h := random.Uint160() + newIC := func(name string, args interface{}) *interop.Context { + ic := &interop.Context{VM: vm.New()} + ic.VM.LoadScriptWithHash([]byte{1}, h, smartcontract.NoneFlag) + ic.VM.Estack().PushVal(args) + ic.VM.Estack().PushVal(name) + return ic + } + t.Run("big name", func(t *testing.T) { + ic := newIC(string(make([]byte, MaxEventNameLen+1)), []byte{42}) + require.Error(t, Notify(ic)) + }) + t.Run("recursive struct", func(t *testing.T) { + arr := stackitem.NewArray([]stackitem.Item{stackitem.Null{}}) + arr.Append(arr) + ic := newIC("event", arr) + require.Error(t, Notify(ic)) + }) + t.Run("big notification", func(t *testing.T) { + bs := stackitem.NewByteArray(make([]byte, MaxNotificationSize+1)) + arr := stackitem.NewArray([]stackitem.Item{bs}) + ic := newIC("event", arr) + require.Error(t, Notify(ic)) + }) + t.Run("good", func(t *testing.T) { + arr := stackitem.NewArray([]stackitem.Item{stackitem.Make(42)}) + ic := newIC("good event", arr) + require.NoError(t, Notify(ic)) + require.Equal(t, 1, len(ic.Notifications)) + + ev := ic.Notifications[0] + require.Equal(t, "good event", ev.Name) + require.Equal(t, h, ev.ScriptHash) + require.Equal(t, arr, ev.Item) + // Check deep copy. + arr.Value().([]stackitem.Item)[0] = stackitem.Null{} + require.NotEqual(t, arr, ev.Item) + }) +} diff --git a/pkg/core/interop/runtime/util_test.go b/pkg/core/interop/runtime/util_test.go new file mode 100644 index 000000000..e48238d56 --- /dev/null +++ b/pkg/core/interop/runtime/util_test.go @@ -0,0 +1,116 @@ +package runtime + +import ( + "testing" + + "github.com/nspcc-dev/neo-go/internal/random" + "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/util" + "github.com/nspcc-dev/neo-go/pkg/vm" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" + "github.com/stretchr/testify/require" +) + +func TestGasLeft(t *testing.T) { + t.Run("no limit", func(t *testing.T) { + ic := &interop.Context{VM: vm.New()} + ic.VM.GasLimit = -1 + ic.VM.AddGas(58) + require.NoError(t, GasLeft(ic)) + checkStack(t, ic.VM, -1) + }) + t.Run("with limit", func(t *testing.T) { + ic := &interop.Context{VM: vm.New()} + ic.VM.GasLimit = 100 + ic.VM.AddGas(58) + require.NoError(t, GasLeft(ic)) + checkStack(t, ic.VM, 42) + }) +} + +func TestRuntimeGetNotifications(t *testing.T) { + v := vm.New() + ic := &interop.Context{ + VM: v, + Notifications: []state.NotificationEvent{ + {ScriptHash: util.Uint160{1}, Name: "Event1", Item: stackitem.NewArray([]stackitem.Item{stackitem.NewByteArray([]byte{11})})}, + {ScriptHash: util.Uint160{2}, Name: "Event2", Item: stackitem.NewArray([]stackitem.Item{stackitem.NewByteArray([]byte{22})})}, + {ScriptHash: util.Uint160{1}, Name: "Event1", Item: stackitem.NewArray([]stackitem.Item{stackitem.NewByteArray([]byte{33})})}, + }, + } + + t.Run("NoFilter", func(t *testing.T) { + v.Estack().PushVal(stackitem.Null{}) + require.NoError(t, GetNotifications(ic)) + + arr := v.Estack().Pop().Array() + require.Equal(t, len(ic.Notifications), len(arr)) + for i := range arr { + elem := arr[i].Value().([]stackitem.Item) + require.Equal(t, ic.Notifications[i].ScriptHash.BytesBE(), elem[0].Value()) + name, err := stackitem.ToString(elem[1]) + require.NoError(t, err) + require.Equal(t, ic.Notifications[i].Name, name) + require.Equal(t, ic.Notifications[i].Item, elem[2]) + } + }) + + t.Run("WithFilter", func(t *testing.T) { + h := util.Uint160{2}.BytesBE() + v.Estack().PushVal(h) + require.NoError(t, GetNotifications(ic)) + + arr := v.Estack().Pop().Array() + require.Equal(t, 1, len(arr)) + elem := arr[0].Value().([]stackitem.Item) + require.Equal(t, h, elem[0].Value()) + name, err := stackitem.ToString(elem[1]) + require.NoError(t, err) + require.Equal(t, ic.Notifications[1].Name, name) + require.Equal(t, ic.Notifications[1].Item, elem[2]) + }) + + t.Run("Bad", func(t *testing.T) { + t.Run("not bytes", func(t *testing.T) { + v.Estack().PushVal(stackitem.NewInterop(util.Uint160{1})) + require.Error(t, GetNotifications(ic)) + }) + t.Run("not uint160", func(t *testing.T) { + v.Estack().PushVal([]byte{1, 2, 3}) + require.Error(t, GetNotifications(ic)) + }) + t.Run("too many notifications", func(t *testing.T) { + for i := 0; i <= vm.MaxStackSize; i++ { + ic.Notifications = append(ic.Notifications, state.NotificationEvent{ + ScriptHash: util.Uint160{3}, + Name: "Event3", + Item: stackitem.NewArray(nil), + }) + } + v.Estack().PushVal(stackitem.Null{}) + require.Error(t, GetNotifications(ic)) + }) + }) +} + +func TestRuntimeGetInvocationCounter(t *testing.T) { + ic := &interop.Context{VM: vm.New()} + h := random.Uint160() + ic.VM.Invocations[h] = 42 + + t.Run("No invocations", func(t *testing.T) { + h1 := h + h1[0] ^= 0xFF + ic.VM.LoadScriptWithHash([]byte{1}, h1, smartcontract.NoneFlag) + // do not return an error in this case. + require.NoError(t, GetInvocationCounter(ic)) + checkStack(t, ic.VM, 1) + }) + t.Run("NonZero", func(t *testing.T) { + ic.VM.LoadScriptWithHash([]byte{1}, h, smartcontract.NoneFlag) + require.NoError(t, GetInvocationCounter(ic)) + checkStack(t, ic.VM, 42) + }) +} diff --git a/pkg/core/interop_neo_test.go b/pkg/core/interop_neo_test.go index 1eb23d3c9..816c89728 100644 --- a/pkg/core/interop_neo_test.go +++ b/pkg/core/interop_neo_test.go @@ -37,12 +37,6 @@ import ( * TestRuntimeDeserialize */ -func TestGetTrigger(t *testing.T) { - _, _, context, chain := createVMAndPushBlock(t) - defer chain.Close() - require.NoError(t, runtimeGetTrigger(context)) -} - func TestStorageFind(t *testing.T) { v, contractState, context, chain := createVMAndContractState(t) defer chain.Close() diff --git a/pkg/core/interop_system.go b/pkg/core/interop_system.go index da9a36e2f..50e2f377b 100644 --- a/pkg/core/interop_system.go +++ b/pkg/core/interop_system.go @@ -18,7 +18,6 @@ import ( "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" - "go.uber.org/zap" ) const ( @@ -27,10 +26,6 @@ const ( // MaxStorageValueLen is the maximum length of a value for storage items. // It is set to be the maximum value for uint16. MaxStorageValueLen = 65535 - // MaxEventNameLen is the maximum length of a name for event. - MaxEventNameLen = 32 - // MaxNotificationSize is the maximum length of a runtime log message. - MaxNotificationSize = 1024 ) // StorageContext contains storing id and read/write flag, it's used as @@ -231,82 +226,6 @@ func engineGetScriptContainer(ic *interop.Context) error { return nil } -// engineGetExecutingScriptHash returns executing script hash. -func engineGetExecutingScriptHash(ic *interop.Context) error { - return ic.VM.PushContextScriptHash(0) -} - -// engineGetCallingScriptHash returns calling script hash. -func engineGetCallingScriptHash(ic *interop.Context) error { - return ic.VM.PushContextScriptHash(1) -} - -// engineGetEntryScriptHash returns entry script hash. -func engineGetEntryScriptHash(ic *interop.Context) error { - return ic.VM.PushContextScriptHash(ic.VM.Istack().Len() - 1) -} - -// runtimePlatform returns the name of the platform. -func runtimePlatform(ic *interop.Context) error { - ic.VM.Estack().PushVal([]byte("NEO")) - return nil -} - -// runtimeGetTrigger returns the script trigger. -func runtimeGetTrigger(ic *interop.Context) error { - ic.VM.Estack().PushVal(byte(ic.Trigger)) - 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) error { - name := ic.VM.Estack().Pop().String() - if len(name) > MaxEventNameLen { - return fmt.Errorf("event name must be less than %d", MaxEventNameLen) - } - elem := ic.VM.Estack().Pop() - args := elem.Array() - // But it has to be serializable, otherwise we either have some broken - // (recursive) structure inside or an interop item that can't be used - // outside of the interop subsystem anyway. - bytes, err := stackitem.SerializeItem(elem.Item()) - if err != nil { - return fmt.Errorf("bad notification: %w", err) - } - if len(bytes) > MaxNotificationSize { - return fmt.Errorf("notification size shouldn't exceed %d", MaxNotificationSize) - } - ne := state.NotificationEvent{ - ScriptHash: ic.VM.GetCurrentScriptHash(), - Name: name, - Item: stackitem.DeepCopy(stackitem.NewArray(args)).(*stackitem.Array), - } - ic.Notifications = append(ic.Notifications, ne) - return nil -} - -// runtimeLog logs the message passed. -func runtimeLog(ic *interop.Context) error { - state := ic.VM.Estack().Pop().String() - if len(state) > MaxNotificationSize { - return fmt.Errorf("message length shouldn't exceed %v", MaxNotificationSize) - } - msg := fmt.Sprintf("%q", state) - ic.Log.Info("runtime log", - zap.Stringer("script", ic.VM.GetCurrentScriptHash()), - zap.String("logs", msg)) - return nil -} - -// runtimeGetTime returns timestamp of the block being verified, or the latest -// one in the blockchain if no block is given to Context. -func runtimeGetTime(ic *interop.Context) error { - header := ic.Block.Header() - ic.VM.Estack().PushVal(header.Timestamp) - return nil -} - // storageDelete deletes stored key-value pair. func storageDelete(ic *interop.Context) error { stcInterface := ic.VM.Estack().Pop().Value() diff --git a/pkg/core/interop_system_test.go b/pkg/core/interop_system_test.go index b0f0c1e62..42c62e6bf 100644 --- a/pkg/core/interop_system_test.go +++ b/pkg/core/interop_system_test.go @@ -244,77 +244,6 @@ func TestContractCreateAccount(t *testing.T) { }) } -func TestRuntimeGasLeft(t *testing.T) { - v, ic, chain := createVM(t) - defer chain.Close() - - v.GasLimit = 100 - v.AddGas(58) - require.NoError(t, runtime.GasLeft(ic)) - require.EqualValues(t, 42, v.Estack().Pop().BigInt().Int64()) -} - -func TestRuntimeGetNotifications(t *testing.T) { - v, ic, chain := createVM(t) - defer chain.Close() - - ic.Notifications = []state.NotificationEvent{ - {ScriptHash: util.Uint160{1}, Name: "Event1", Item: stackitem.NewArray([]stackitem.Item{stackitem.NewByteArray([]byte{11})})}, - {ScriptHash: util.Uint160{2}, Name: "Event2", Item: stackitem.NewArray([]stackitem.Item{stackitem.NewByteArray([]byte{22})})}, - {ScriptHash: util.Uint160{1}, Name: "Event1", Item: stackitem.NewArray([]stackitem.Item{stackitem.NewByteArray([]byte{33})})}, - } - - t.Run("NoFilter", func(t *testing.T) { - v.Estack().PushVal(stackitem.Null{}) - require.NoError(t, runtime.GetNotifications(ic)) - - arr := v.Estack().Pop().Array() - require.Equal(t, len(ic.Notifications), len(arr)) - for i := range arr { - elem := arr[i].Value().([]stackitem.Item) - require.Equal(t, ic.Notifications[i].ScriptHash.BytesBE(), elem[0].Value()) - name, err := stackitem.ToString(elem[1]) - require.NoError(t, err) - require.Equal(t, ic.Notifications[i].Name, name) - require.Equal(t, ic.Notifications[i].Item, elem[2]) - } - }) - - t.Run("WithFilter", func(t *testing.T) { - h := util.Uint160{2}.BytesBE() - v.Estack().PushVal(h) - require.NoError(t, runtime.GetNotifications(ic)) - - arr := v.Estack().Pop().Array() - require.Equal(t, 1, len(arr)) - elem := arr[0].Value().([]stackitem.Item) - require.Equal(t, h, elem[0].Value()) - name, err := stackitem.ToString(elem[1]) - require.NoError(t, err) - require.Equal(t, ic.Notifications[1].Name, name) - require.Equal(t, ic.Notifications[1].Item, elem[2]) - }) -} - -func TestRuntimeGetInvocationCounter(t *testing.T) { - v, ic, chain := createVM(t) - defer chain.Close() - - ic.VM.Invocations[hash.Hash160([]byte{2})] = 42 - - t.Run("No invocations", func(t *testing.T) { - v.LoadScript([]byte{1}) - // do not return an error in this case. - require.NoError(t, runtime.GetInvocationCounter(ic)) - require.EqualValues(t, 1, v.Estack().Pop().BigInt().Int64()) - }) - t.Run("NonZero", func(t *testing.T) { - v.LoadScript([]byte{2}) - require.NoError(t, runtime.GetInvocationCounter(ic)) - require.EqualValues(t, 42, v.Estack().Pop().BigInt().Int64()) - }) -} - func TestBlockchainGetContractState(t *testing.T) { v, cs, ic, bc := createVMAndContractState(t) defer bc.Close() diff --git a/pkg/core/interops.go b/pkg/core/interops.go index b60dbaa54..a8dbd95d9 100644 --- a/pkg/core/interops.go +++ b/pkg/core/interops.go @@ -83,19 +83,19 @@ var systemInterops = []interop.Function{ {Name: interopnames.SystemRuntimeCheckWitness, Func: runtime.CheckWitness, Price: 30000, RequiredFlags: smartcontract.NoneFlag, ParamCount: 1}, {Name: interopnames.SystemRuntimeGasLeft, Func: runtime.GasLeft, Price: 400}, - {Name: interopnames.SystemRuntimeGetCallingScriptHash, Func: engineGetCallingScriptHash, Price: 400}, - {Name: interopnames.SystemRuntimeGetEntryScriptHash, Func: engineGetEntryScriptHash, Price: 400}, - {Name: interopnames.SystemRuntimeGetExecutingScriptHash, Func: engineGetExecutingScriptHash, Price: 400}, + {Name: interopnames.SystemRuntimeGetCallingScriptHash, Func: runtime.GetCallingScriptHash, Price: 400}, + {Name: interopnames.SystemRuntimeGetEntryScriptHash, Func: runtime.GetEntryScriptHash, Price: 400}, + {Name: interopnames.SystemRuntimeGetExecutingScriptHash, Func: runtime.GetExecutingScriptHash, Price: 400}, {Name: interopnames.SystemRuntimeGetInvocationCounter, Func: runtime.GetInvocationCounter, Price: 400}, {Name: interopnames.SystemRuntimeGetNotifications, Func: runtime.GetNotifications, Price: 10000, ParamCount: 1}, {Name: interopnames.SystemRuntimeGetScriptContainer, Func: engineGetScriptContainer, Price: 250}, - {Name: interopnames.SystemRuntimeGetTime, Func: runtimeGetTime, Price: 250, RequiredFlags: smartcontract.AllowStates}, - {Name: interopnames.SystemRuntimeGetTrigger, Func: runtimeGetTrigger, Price: 250}, - {Name: interopnames.SystemRuntimeLog, Func: runtimeLog, Price: 1000000, RequiredFlags: smartcontract.AllowNotify, + {Name: interopnames.SystemRuntimeGetTime, Func: runtime.GetTime, Price: 250, RequiredFlags: smartcontract.AllowStates}, + {Name: interopnames.SystemRuntimeGetTrigger, Func: runtime.GetTrigger, Price: 250}, + {Name: interopnames.SystemRuntimeLog, Func: runtime.Log, Price: 1000000, RequiredFlags: smartcontract.AllowNotify, ParamCount: 1, DisallowCallback: true}, - {Name: interopnames.SystemRuntimeNotify, Func: runtimeNotify, Price: 1000000, RequiredFlags: smartcontract.AllowNotify, + {Name: interopnames.SystemRuntimeNotify, Func: runtime.Notify, Price: 1000000, RequiredFlags: smartcontract.AllowNotify, ParamCount: 2, DisallowCallback: true}, - {Name: interopnames.SystemRuntimePlatform, Func: runtimePlatform, Price: 250}, + {Name: interopnames.SystemRuntimePlatform, Func: runtime.Platform, Price: 250}, {Name: interopnames.SystemStorageDelete, Func: storageDelete, Price: StoragePrice, RequiredFlags: smartcontract.AllowModifyStates, ParamCount: 2, DisallowCallback: true}, {Name: interopnames.SystemStorageFind, Func: storageFind, Price: 1000000, RequiredFlags: smartcontract.AllowStates, From d136569ac8aac8ac82e095facf0a66bbf75361b8 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Wed, 2 Dec 2020 12:29:14 +0300 Subject: [PATCH 2/8] core: move `System.Binary.*` interops to `binary/` package Also extend test suite. --- pkg/core/interop/binary/encode.go | 57 ++++++++++++++++ pkg/core/interop/binary/encode_test.go | 92 ++++++++++++++++++++++++++ pkg/core/interop_neo.go | 50 -------------- pkg/core/interop_neo_test.go | 44 ------------ pkg/core/interops.go | 12 ++-- 5 files changed, 155 insertions(+), 100 deletions(-) create mode 100644 pkg/core/interop/binary/encode.go create mode 100644 pkg/core/interop/binary/encode_test.go diff --git a/pkg/core/interop/binary/encode.go b/pkg/core/interop/binary/encode.go new file mode 100644 index 000000000..0cc294fef --- /dev/null +++ b/pkg/core/interop/binary/encode.go @@ -0,0 +1,57 @@ +package binary + +import ( + "encoding/base64" + + "github.com/mr-tron/base58" + "github.com/nspcc-dev/neo-go/pkg/core/interop" + "github.com/nspcc-dev/neo-go/pkg/vm" +) + +// Serialize serializes top stack item into a ByteArray. +func Serialize(ic *interop.Context) error { + return vm.RuntimeSerialize(ic.VM) +} + +// Deserialize deserializes ByteArray from a stack into an item. +func Deserialize(ic *interop.Context) error { + return vm.RuntimeDeserialize(ic.VM) +} + +// EncodeBase64 encodes top stack item into a base64 string. +func EncodeBase64(ic *interop.Context) error { + src := ic.VM.Estack().Pop().Bytes() + result := base64.StdEncoding.EncodeToString(src) + ic.VM.Estack().PushVal([]byte(result)) + return nil +} + +// DecodeBase64 decodes top stack item from base64 string to byte array. +func DecodeBase64(ic *interop.Context) error { + src := ic.VM.Estack().Pop().String() + result, err := base64.StdEncoding.DecodeString(src) + if err != nil { + return err + } + ic.VM.Estack().PushVal(result) + return nil +} + +// EncodeBase58 encodes top stack item into a base58 string. +func EncodeBase58(ic *interop.Context) error { + src := ic.VM.Estack().Pop().Bytes() + result := base58.Encode(src) + ic.VM.Estack().PushVal([]byte(result)) + return nil +} + +// DecodeBase58 decodes top stack item from base58 string to byte array. +func DecodeBase58(ic *interop.Context) error { + src := ic.VM.Estack().Pop().String() + result, err := base58.Decode(src) + if err != nil { + return err + } + ic.VM.Estack().PushVal(result) + return nil +} diff --git a/pkg/core/interop/binary/encode_test.go b/pkg/core/interop/binary/encode_test.go new file mode 100644 index 000000000..eeda9f3a2 --- /dev/null +++ b/pkg/core/interop/binary/encode_test.go @@ -0,0 +1,92 @@ +package binary + +import ( + "encoding/base64" + "math/big" + "testing" + + "github.com/mr-tron/base58" + "github.com/nspcc-dev/neo-go/pkg/core/interop" + "github.com/nspcc-dev/neo-go/pkg/io" + "github.com/nspcc-dev/neo-go/pkg/vm" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" + "github.com/stretchr/testify/require" +) + +func TestRuntimeSerialize(t *testing.T) { + t.Run("recursive", func(t *testing.T) { + arr := stackitem.NewArray(nil) + arr.Append(arr) + ic := &interop.Context{VM: vm.New()} + ic.VM.Estack().PushVal(arr) + require.Error(t, Serialize(ic)) + }) + t.Run("big item", func(t *testing.T) { + ic := &interop.Context{VM: vm.New()} + ic.VM.Estack().PushVal(make([]byte, stackitem.MaxSize)) + require.Error(t, Serialize(ic)) + }) + t.Run("good", func(t *testing.T) { + ic := &interop.Context{VM: vm.New()} + ic.VM.Estack().PushVal(42) + require.NoError(t, Serialize(ic)) + + w := io.NewBufBinWriter() + stackitem.EncodeBinaryStackItem(stackitem.Make(42), w.BinWriter) + require.NoError(t, w.Err) + + encoded := w.Bytes() + require.Equal(t, encoded, ic.VM.Estack().Pop().Bytes()) + + ic.VM.Estack().PushVal(encoded) + require.NoError(t, Deserialize(ic)) + require.Equal(t, big.NewInt(42), ic.VM.Estack().Pop().BigInt()) + + t.Run("bad", func(t *testing.T) { + encoded[0] ^= 0xFF + ic.VM.Estack().PushVal(encoded) + require.Error(t, Deserialize(ic)) + }) + }) +} + +func TestRuntimeEncodeDecode(t *testing.T) { + original := []byte("my pretty string") + encoded64 := base64.StdEncoding.EncodeToString(original) + encoded58 := base58.Encode(original) + v := vm.New() + ic := &interop.Context{VM: v} + + t.Run("Encode64", func(t *testing.T) { + v.Estack().PushVal(original) + require.NoError(t, EncodeBase64(ic)) + actual := v.Estack().Pop().Bytes() + require.Equal(t, []byte(encoded64), actual) + }) + t.Run("Encode58", func(t *testing.T) { + v.Estack().PushVal(original) + require.NoError(t, EncodeBase58(ic)) + actual := v.Estack().Pop().Bytes() + require.Equal(t, []byte(encoded58), actual) + }) + t.Run("Decode64/positive", func(t *testing.T) { + v.Estack().PushVal(encoded64) + require.NoError(t, DecodeBase64(ic)) + actual := v.Estack().Pop().Bytes() + require.Equal(t, original, actual) + }) + t.Run("Decode64/error", func(t *testing.T) { + v.Estack().PushVal(encoded64 + "%") + require.Error(t, DecodeBase64(ic)) + }) + t.Run("Decode58/positive", func(t *testing.T) { + v.Estack().PushVal(encoded58) + require.NoError(t, DecodeBase58(ic)) + actual := v.Estack().Pop().Bytes() + require.Equal(t, original, actual) + }) + t.Run("Decode58/error", func(t *testing.T) { + v.Estack().PushVal(encoded58 + "%") + require.Error(t, DecodeBase58(ic)) + }) +} diff --git a/pkg/core/interop_neo.go b/pkg/core/interop_neo.go index 47d643731..67a8c3d6f 100644 --- a/pkg/core/interop_neo.go +++ b/pkg/core/interop_neo.go @@ -2,14 +2,12 @@ package core import ( "bytes" - "encoding/base64" "encoding/json" "errors" "fmt" "math" "sort" - "github.com/mr-tron/base58" "github.com/nspcc-dev/neo-go/pkg/core/interop" "github.com/nspcc-dev/neo-go/pkg/core/interop/contract" "github.com/nspcc-dev/neo-go/pkg/core/state" @@ -193,51 +191,3 @@ func callDeploy(ic *interop.Context, cs *state.Contract, isUpdate bool) error { } return nil } - -// runtimeSerialize serializes top stack item into a ByteArray. -func runtimeSerialize(ic *interop.Context) error { - return vm.RuntimeSerialize(ic.VM) -} - -// runtimeDeserialize deserializes ByteArray from a stack into an item. -func runtimeDeserialize(ic *interop.Context) error { - return vm.RuntimeDeserialize(ic.VM) -} - -// runtimeEncodeBase64 encodes top stack item into a base64 string. -func runtimeEncodeBase64(ic *interop.Context) error { - src := ic.VM.Estack().Pop().Bytes() - result := base64.StdEncoding.EncodeToString(src) - ic.VM.Estack().PushVal([]byte(result)) - return nil -} - -// runtimeDecodeBase64 decodes top stack item from base64 string to byte array. -func runtimeDecodeBase64(ic *interop.Context) error { - src := ic.VM.Estack().Pop().String() - result, err := base64.StdEncoding.DecodeString(src) - if err != nil { - return err - } - ic.VM.Estack().PushVal(result) - return nil -} - -// runtimeEncodeBase58 encodes top stack item into a base58 string. -func runtimeEncodeBase58(ic *interop.Context) error { - src := ic.VM.Estack().Pop().Bytes() - result := base58.Encode(src) - ic.VM.Estack().PushVal([]byte(result)) - return nil -} - -// runtimeDecodeBase58 decodes top stack item from base58 string to byte array. -func runtimeDecodeBase58(ic *interop.Context) error { - src := ic.VM.Estack().Pop().String() - result, err := base58.Decode(src) - if err != nil { - return err - } - ic.VM.Estack().PushVal(result) - return nil -} diff --git a/pkg/core/interop_neo_test.go b/pkg/core/interop_neo_test.go index 816c89728..5f4462db4 100644 --- a/pkg/core/interop_neo_test.go +++ b/pkg/core/interop_neo_test.go @@ -1,11 +1,9 @@ package core import ( - "encoding/base64" "fmt" "testing" - "github.com/mr-tron/base58" "github.com/nspcc-dev/neo-go/pkg/config/netmode" "github.com/nspcc-dev/neo-go/pkg/core/block" "github.com/nspcc-dev/neo-go/pkg/core/dao" @@ -213,48 +211,6 @@ func TestECDSAVerify(t *testing.T) { }) } -func TestRuntimeEncodeDecode(t *testing.T) { - original := []byte("my pretty string") - encoded64 := base64.StdEncoding.EncodeToString(original) - encoded58 := base58.Encode(original) - v, ic, bc := createVM(t) - defer bc.Close() - - t.Run("Encode64", func(t *testing.T) { - v.Estack().PushVal(original) - require.NoError(t, runtimeEncodeBase64(ic)) - actual := v.Estack().Pop().Bytes() - require.Equal(t, []byte(encoded64), actual) - }) - - t.Run("Encode58", func(t *testing.T) { - v.Estack().PushVal(original) - require.NoError(t, runtimeEncodeBase58(ic)) - actual := v.Estack().Pop().Bytes() - require.Equal(t, []byte(encoded58), actual) - }) - t.Run("Decode64/positive", func(t *testing.T) { - v.Estack().PushVal(encoded64) - require.NoError(t, runtimeDecodeBase64(ic)) - actual := v.Estack().Pop().Bytes() - require.Equal(t, original, actual) - }) - t.Run("Decode64/error", func(t *testing.T) { - v.Estack().PushVal(encoded64 + "%") - require.Error(t, runtimeDecodeBase64(ic)) - }) - t.Run("Decode58/positive", func(t *testing.T) { - v.Estack().PushVal(encoded58) - require.NoError(t, runtimeDecodeBase58(ic)) - actual := v.Estack().Pop().Bytes() - require.Equal(t, original, actual) - }) - t.Run("Decode58/error", func(t *testing.T) { - v.Estack().PushVal(encoded58 + "%") - require.Error(t, runtimeDecodeBase58(ic)) - }) -} - // Helper functions to create VM, InteropContext, TX, Account, Contract. func createVM(t *testing.T) (*vm.VM, *interop.Context, *Blockchain) { diff --git a/pkg/core/interops.go b/pkg/core/interops.go index a8dbd95d9..03d8023c8 100644 --- a/pkg/core/interops.go +++ b/pkg/core/interops.go @@ -34,13 +34,13 @@ func SpawnVM(ic *interop.Context) *vm.VM { // All lists are sorted, keep 'em this way, please. var systemInterops = []interop.Function{ {Name: interopnames.SystemBinaryAtoi, Func: binary.Atoi, Price: 100000, ParamCount: 2}, - {Name: interopnames.SystemBinaryBase58Decode, Func: runtimeDecodeBase58, Price: 100000, ParamCount: 1}, - {Name: interopnames.SystemBinaryBase58Encode, Func: runtimeEncodeBase58, Price: 100000, ParamCount: 1}, - {Name: interopnames.SystemBinaryBase64Decode, Func: runtimeDecodeBase64, Price: 100000, ParamCount: 1}, - {Name: interopnames.SystemBinaryBase64Encode, Func: runtimeEncodeBase64, Price: 100000, ParamCount: 1}, - {Name: interopnames.SystemBinaryDeserialize, Func: runtimeDeserialize, Price: 500000, ParamCount: 1}, + {Name: interopnames.SystemBinaryBase58Decode, Func: binary.DecodeBase58, Price: 100000, ParamCount: 1}, + {Name: interopnames.SystemBinaryBase58Encode, Func: binary.EncodeBase58, Price: 100000, ParamCount: 1}, + {Name: interopnames.SystemBinaryBase64Decode, Func: binary.DecodeBase64, Price: 100000, ParamCount: 1}, + {Name: interopnames.SystemBinaryBase64Encode, Func: binary.EncodeBase64, Price: 100000, ParamCount: 1}, + {Name: interopnames.SystemBinaryDeserialize, Func: binary.Deserialize, Price: 500000, ParamCount: 1}, {Name: interopnames.SystemBinaryItoa, Func: binary.Itoa, Price: 100000, ParamCount: 2}, - {Name: interopnames.SystemBinarySerialize, Func: runtimeSerialize, Price: 100000, ParamCount: 1}, + {Name: interopnames.SystemBinarySerialize, Func: binary.Serialize, Price: 100000, ParamCount: 1}, {Name: interopnames.SystemBlockchainGetBlock, Func: bcGetBlock, Price: 2500000, RequiredFlags: smartcontract.AllowStates, ParamCount: 1}, {Name: interopnames.SystemBlockchainGetContract, Func: bcGetContract, Price: 1000000, From e4ee7cd40727bc1ba8488508d498c7f43e6d8f00 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Wed, 2 Dec 2020 12:38:22 +0300 Subject: [PATCH 3/8] vm: improve coverage for default interops --- pkg/vm/vm_test.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pkg/vm/vm_test.go b/pkg/vm/vm_test.go index 56d8598e9..a813304bf 100644 --- a/pkg/vm/vm_test.go +++ b/pkg/vm/vm_test.go @@ -528,11 +528,21 @@ func testIterableCreate(t *testing.T, typ string, isByteArray bool) { func TestEnumeratorCreate(t *testing.T) { t.Run("Array", func(t *testing.T) { testIterableCreate(t, "Enumerator", false) }) t.Run("ByteArray", func(t *testing.T) { testIterableCreate(t, "Enumerator", true) }) + t.Run("Interop", func(t *testing.T) { + v := New() + v.Estack().PushVal(stackitem.NewInterop([]byte{42})) + require.Error(t, EnumeratorCreate(v)) + }) } func TestIteratorCreate(t *testing.T) { t.Run("Array", func(t *testing.T) { testIterableCreate(t, "Iterator", false) }) t.Run("ByteArray", func(t *testing.T) { testIterableCreate(t, "Iterator", true) }) + t.Run("Interop", func(t *testing.T) { + v := New() + v.Estack().PushVal(stackitem.NewInterop([]byte{42})) + require.Error(t, IteratorCreate(v)) + }) } func testIterableConcat(t *testing.T, typ string) { From b203c235152a79e5e5235379256ab3f130884805 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Wed, 2 Dec 2020 12:53:03 +0300 Subject: [PATCH 4/8] core: cover enumerator/iterator interops --- pkg/core/interop/enumerator/interop_test.go | 35 +++++++++++++++ pkg/core/interop/iterator/interop_test.go | 48 +++++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 pkg/core/interop/enumerator/interop_test.go create mode 100644 pkg/core/interop/iterator/interop_test.go diff --git a/pkg/core/interop/enumerator/interop_test.go b/pkg/core/interop/enumerator/interop_test.go new file mode 100644 index 000000000..20149455d --- /dev/null +++ b/pkg/core/interop/enumerator/interop_test.go @@ -0,0 +1,35 @@ +package enumerator + +import ( + "math/big" + "testing" + + "github.com/nspcc-dev/neo-go/pkg/core/interop" + "github.com/nspcc-dev/neo-go/pkg/vm" + "github.com/stretchr/testify/require" +) + +// Enumerator is thoroughly tested in VM package, these are smoke tests. +func TestEnumerator(t *testing.T) { + ic := &interop.Context{VM: vm.New()} + full := []byte{4, 8, 15} + ic.VM.Estack().PushVal(full[2:]) + require.NoError(t, Create(ic)) + ic.VM.Estack().PushVal(full[:2]) + require.NoError(t, Create(ic)) + require.NoError(t, Concat(ic)) + + res := ic.VM.Estack().Pop().Item() + for i := range full { + ic.VM.Estack().PushVal(res) + require.NoError(t, Next(ic)) + require.True(t, ic.VM.Estack().Pop().Bool()) + ic.VM.Estack().PushVal(res) + require.NoError(t, Value(ic)) + require.Equal(t, big.NewInt(int64(full[i])), ic.VM.Estack().Pop().BigInt()) + } + + ic.VM.Estack().PushVal(res) + require.NoError(t, Next(ic)) + require.False(t, ic.VM.Estack().Pop().Bool()) +} diff --git a/pkg/core/interop/iterator/interop_test.go b/pkg/core/interop/iterator/interop_test.go new file mode 100644 index 000000000..b220b5271 --- /dev/null +++ b/pkg/core/interop/iterator/interop_test.go @@ -0,0 +1,48 @@ +package iterator + +import ( + "math/big" + "testing" + + "github.com/nspcc-dev/neo-go/pkg/core/interop" + "github.com/nspcc-dev/neo-go/pkg/vm" + "github.com/stretchr/testify/require" +) + +// Iterator is thoroughly tested in VM package, these are smoke tests. +func TestIterator(t *testing.T) { + ic := &interop.Context{VM: vm.New()} + full := []byte{4, 8, 15} + ic.VM.Estack().PushVal(full[2:]) + require.NoError(t, Create(ic)) + ic.VM.Estack().PushVal(full[:2]) + require.NoError(t, Create(ic)) + require.NoError(t, Concat(ic)) + + res := ic.VM.Estack().Pop().Item() + ic.VM.Estack().PushVal(res) + require.NoError(t, vm.EnumeratorNext(ic.VM)) + require.True(t, ic.VM.Estack().Pop().Bool()) + + ic.VM.Estack().PushVal(res) + require.NoError(t, Key(ic)) + require.Equal(t, big.NewInt(0), ic.VM.Estack().Pop().BigInt()) + + ic.VM.Estack().PushVal(res) + require.NoError(t, vm.EnumeratorValue(ic.VM)) + require.Equal(t, big.NewInt(int64(full[0])), ic.VM.Estack().Pop().BigInt()) + + ic.VM.Estack().PushVal(res) + require.NoError(t, vm.EnumeratorNext(ic.VM)) + require.True(t, ic.VM.Estack().Pop().Bool()) + + ic.VM.Estack().PushVal(res) + require.NoError(t, Keys(ic)) + require.NoError(t, vm.EnumeratorValue(ic.VM)) + require.Equal(t, big.NewInt(1), ic.VM.Estack().Pop().BigInt()) + + ic.VM.Estack().PushVal(res) + require.NoError(t, Values(ic)) + require.NoError(t, vm.EnumeratorValue(ic.VM)) + require.Equal(t, big.NewInt(int64(full[1])), ic.VM.Estack().Pop().BigInt()) +} From dcc58b7c445508885c332731e472b962e641f37e Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Wed, 2 Dec 2020 14:49:22 +0300 Subject: [PATCH 5/8] core: validate manifest before incrementing ID in `Contract.Create` Also add more tests. --- pkg/core/interop_neo.go | 6 ++-- pkg/core/interop_system_test.go | 51 +++++++++++++++++++++++++++------ 2 files changed, 46 insertions(+), 11 deletions(-) diff --git a/pkg/core/interop_neo.go b/pkg/core/interop_neo.go index 67a8c3d6f..a65902903 100644 --- a/pkg/core/interop_neo.go +++ b/pkg/core/interop_neo.go @@ -115,13 +115,13 @@ func contractCreate(ic *interop.Context) error { if contract != nil && err == nil { return errors.New("contract already exists") } + if !manif.IsValid(h) { + return errors.New("failed to check contract script hash against manifest") + } id, err := ic.DAO.GetAndUpdateNextContractID() if err != nil { return err } - if !manif.IsValid(h) { - return errors.New("failed to check contract script hash against manifest") - } newcontract := &state.Contract{ ID: id, Hash: h, diff --git a/pkg/core/interop_system_test.go b/pkg/core/interop_system_test.go index 42c62e6bf..b0e1788f6 100644 --- a/pkg/core/interop_system_test.go +++ b/pkg/core/interop_system_test.go @@ -589,14 +589,24 @@ func TestContractCreate(t *testing.T) { // nef.NewFile() cares about version a lot. config.Version = "0.90.0-test" + + ne, err := nef.NewFile(cs.Script) + require.NoError(t, err) + neb, err := ne.Bytes() + require.NoError(t, err) + priv, err := keys.NewPrivateKey() + require.NoError(t, err) + sender := util.Uint160{1, 2, 3} + h := state.CreateContractHash(sender, ne.Script) + sig := priv.Sign(h.BytesBE()) + cs.Manifest.Groups = []manifest.Group{{ + PublicKey: priv.PublicKey(), + Signature: sig, + }} + m, err := json.Marshal(cs.Manifest) + require.NoError(t, err) putArgsOnStack := func() { - manifest, err := json.Marshal(cs.Manifest) - require.NoError(t, err) - ne, err := nef.NewFile(cs.Script) - require.NoError(t, err) - neb, err := ne.Bytes() - require.NoError(t, err) - v.Estack().PushVal(manifest) + v.Estack().PushVal(m) v.Estack().PushVal(neb) } @@ -607,11 +617,36 @@ func TestContractCreate(t *testing.T) { }) ic.Tx = transaction.New(netmode.UnitTestNet, []byte{1}, 0) - var sender = util.Uint160{1, 2, 3} ic.Tx.Signers = append(ic.Tx.Signers, transaction.Signer{Account: sender}) cs.ID = 0 cs.Hash = state.CreateContractHash(sender, cs.Script) + t.Run("missing NEF", func(t *testing.T) { + v.Estack().PushVal(m) + v.Estack().PushVal(stackitem.Null{}) + require.Error(t, contractCreate(ic)) + }) + t.Run("missing manifest", func(t *testing.T) { + v.Estack().PushVal(stackitem.Null{}) + v.Estack().PushVal(neb) + require.Error(t, contractCreate(ic)) + }) + t.Run("invalid manifest (empty)", func(t *testing.T) { + v.Estack().PushVal([]byte{}) + v.Estack().PushVal(neb) + require.Error(t, contractCreate(ic)) + }) + + t.Run("invalid manifest (group signature)", func(t *testing.T) { + cs.Manifest.Groups[0].Signature = make([]byte, 11) + rawManif, err := json.Marshal(cs.Manifest) + require.NoError(t, err) + v.Estack().PushVal(rawManif) + v.Estack().PushVal(neb) + require.Error(t, contractCreate(ic)) + }) + + cs.Manifest.Groups[0].Signature = sig t.Run("positive", func(t *testing.T) { putArgsOnStack() From 5bd8ca9597d0d929ecd543f76e3954d249412133 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Wed, 2 Dec 2020 13:13:42 +0300 Subject: [PATCH 6/8] core/tests: extend test suite --- pkg/core/interop_system_test.go | 117 ++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/pkg/core/interop_system_test.go b/pkg/core/interop_system_test.go index b0e1788f6..618ab24a5 100644 --- a/pkg/core/interop_system_test.go +++ b/pkg/core/interop_system_test.go @@ -31,6 +31,28 @@ import ( "github.com/stretchr/testify/require" ) +func TestBCGetTransactionHeight(t *testing.T) { + v, tx, ic, chain := createVMAndTX(t) + defer chain.Close() + + for i := 0; i < 13; i++ { + require.NoError(t, chain.AddBlock(chain.newBlock())) + } + require.NoError(t, ic.DAO.StoreAsTransaction(tx, 13, nil)) + t.Run("good", func(t *testing.T) { + v.Estack().PushVal(tx.Hash().BytesBE()) + require.NoError(t, bcGetTransactionHeight(ic)) + require.Equal(t, big.NewInt(13), v.Estack().Pop().BigInt()) + }) + t.Run("bad", func(t *testing.T) { + h := tx.Hash() + h[0] ^= 0xFF + v.Estack().PushVal(h.BytesBE()) + require.NoError(t, bcGetTransactionHeight(ic)) + require.Equal(t, big.NewInt(-1), v.Estack().Pop().BigInt()) + }) +} + func TestBCGetTransaction(t *testing.T) { v, tx, context, chain := createVMAndTX(t) defer chain.Close() @@ -301,6 +323,83 @@ func TestStoragePut(t *testing.T) { initVM(t, []byte{4}, []byte{5, 6}, StoragePrice) require.NoError(t, storagePut(ic)) }) + + t.Run("check limits", func(t *testing.T) { + initVM(t, make([]byte, MaxStorageKeyLen), make([]byte, MaxStorageValueLen), -1) + require.NoError(t, storagePut(ic)) + }) + + t.Run("bad", func(t *testing.T) { + t.Run("readonly context", func(t *testing.T) { + initVM(t, []byte{1}, []byte{1}, -1) + require.NoError(t, storageContextAsReadOnly(ic)) + require.Error(t, storagePut(ic)) + }) + t.Run("big key", func(t *testing.T) { + initVM(t, make([]byte, MaxStorageKeyLen+1), []byte{1}, -1) + require.Error(t, storagePut(ic)) + }) + t.Run("big value", func(t *testing.T) { + initVM(t, []byte{1}, make([]byte, MaxStorageValueLen+1), -1) + require.Error(t, storagePut(ic)) + }) + t.Run("item exists and is const", func(t *testing.T) { + v := ic.SpawnVM() + v.LoadScript(cs.Script) + v.GasLimit = -1 + v.Estack().PushVal(1) + v.Estack().PushVal("value") + v.Estack().PushVal("key") + require.NoError(t, storageGetContext(ic)) + require.NoError(t, storagePutEx(ic)) + + v.Estack().PushVal("new") + v.Estack().PushVal("key") + require.NoError(t, storageGetContext(ic)) + require.Error(t, storagePut(ic)) + }) + }) +} + +func TestStorageDelete(t *testing.T) { + v, cs, ic, bc := createVMAndContractState(t) + defer bc.Close() + + require.NoError(t, ic.DAO.PutContractState(cs)) + v.LoadScriptWithHash(cs.Script, cs.Hash, smartcontract.All) + put := func(key, value string, flag int) { + v.Estack().PushVal(flag) + v.Estack().PushVal(value) + v.Estack().PushVal(key) + require.NoError(t, storageGetContext(ic)) + require.NoError(t, storagePutEx(ic)) + } + put("key1", "value1", 0) + put("key2", "value2", 0) + put("key3", "value3", 0) + put("key4", "value4", 1) + + t.Run("good", func(t *testing.T) { + v.Estack().PushVal("key1") + require.NoError(t, storageGetContext(ic)) + require.NoError(t, storageDelete(ic)) + }) + t.Run("readonly context", func(t *testing.T) { + v.Estack().PushVal("key2") + require.NoError(t, storageGetReadOnlyContext(ic)) + require.Error(t, storageDelete(ic)) + }) + t.Run("readonly context (from normal)", func(t *testing.T) { + v.Estack().PushVal("key3") + require.NoError(t, storageGetContext(ic)) + require.NoError(t, storageContextAsReadOnly(ic)) + require.Error(t, storageDelete(ic)) + }) + t.Run("constant item", func(t *testing.T) { + v.Estack().PushVal("key4") + require.NoError(t, storageGetContext(ic)) + require.Error(t, storageDelete(ic)) + }) } // getTestContractState returns 2 contracts second of which is allowed to call the first. @@ -826,6 +925,24 @@ func TestContractUpdate(t *testing.T) { }) } +func TestContractDestroy(t *testing.T) { + v, cs, ic, bc := createVMAndContractState(t) + defer bc.Close() + + v.LoadScriptWithHash(cs.Script, cs.Hash, smartcontract.All) + require.NoError(t, contractDestroy(ic)) // silent error when contract is missing + require.NoError(t, ic.DAO.PutContractState(cs)) + + v.Estack().PushVal("value") + v.Estack().PushVal("key") + require.NoError(t, storageGetContext(ic)) + require.NoError(t, storagePut(ic)) + require.NotNil(t, ic.DAO.GetStorageItem(cs.ID, []byte("key"))) + require.NoError(t, contractDestroy(ic)) + require.Nil(t, ic.DAO.GetStorageItem(cs.ID, []byte("key"))) + require.Error(t, storageGetContext(ic)) +} + // TestContractCreateDeploy checks that `_deploy` method was called // during contract creation or update. func TestContractCreateDeploy(t *testing.T) { From d5b4553bb36176dfbbeaf69d11a0ba78240a07ce Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Wed, 2 Dec 2020 15:52:31 +0300 Subject: [PATCH 7/8] keys: allow to create keys on arbitrary curve --- pkg/crypto/keys/private_key.go | 17 ++++++++++++++--- pkg/crypto/keys/private_key_test.go | 17 +++++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/pkg/crypto/keys/private_key.go b/pkg/crypto/keys/private_key.go index c790980f2..111f3283e 100644 --- a/pkg/crypto/keys/private_key.go +++ b/pkg/crypto/keys/private_key.go @@ -10,6 +10,7 @@ import ( "fmt" "math/big" + "github.com/btcsuite/btcd/btcec" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/rfc6979" ) @@ -20,16 +21,26 @@ type PrivateKey struct { ecdsa.PrivateKey } -// NewPrivateKey creates a new random Secp256k1 private key. +// NewPrivateKey creates a new random Secp256r1 private key. func NewPrivateKey() (*PrivateKey, error) { - priv, x, y, err := elliptic.GenerateKey(elliptic.P256(), rand.Reader) + return newPrivateKeyOnCurve(elliptic.P256()) +} + +// NewSecp256k1PrivateKey creates a new random Secp256k1 private key. +func NewSecp256k1PrivateKey() (*PrivateKey, error) { + return newPrivateKeyOnCurve(btcec.S256()) +} + +// newPrivateKeyOnCurve creates a new random private key using curve c. +func newPrivateKeyOnCurve(c elliptic.Curve) (*PrivateKey, error) { + priv, x, y, err := elliptic.GenerateKey(c, rand.Reader) if err != nil { return nil, err } return &PrivateKey{ ecdsa.PrivateKey{ PublicKey: ecdsa.PublicKey{ - Curve: elliptic.P256(), + Curve: c, X: x, Y: y, }, diff --git a/pkg/crypto/keys/private_key_test.go b/pkg/crypto/keys/private_key_test.go index 9089aa302..b8b806080 100644 --- a/pkg/crypto/keys/private_key_test.go +++ b/pkg/crypto/keys/private_key_test.go @@ -6,7 +6,9 @@ import ( "testing" "github.com/nspcc-dev/neo-go/internal/keytestcases" + "github.com/nspcc-dev/neo-go/pkg/crypto/hash" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestPrivateKey(t *testing.T) { @@ -28,6 +30,21 @@ func TestPrivateKey(t *testing.T) { } } +func TestNewPrivateKeyOnCurve(t *testing.T) { + msg := []byte{1, 2, 3} + h := hash.Sha256(msg).BytesBE() + t.Run("Secp256r1", func(t *testing.T) { + p, err := NewPrivateKey() + require.NoError(t, err) + p.PublicKey().Verify(p.Sign(msg), h) + }) + t.Run("Secp256k1", func(t *testing.T) { + p, err := NewSecp256k1PrivateKey() + require.NoError(t, err) + p.PublicKey().Verify(p.Sign(msg), h) + }) +} + func TestPrivateKeyFromWIF(t *testing.T) { for _, testCase := range keytestcases.Arr { key, err := NewPrivateKeyFromWIF(testCase.Wif) From 6e749f49776d74c3bed989677f3afe783e8bc627 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Wed, 2 Dec 2020 15:52:57 +0300 Subject: [PATCH 8/8] core: add tests for crypto interops Also move related test from `core/`. --- pkg/core/interop/crypto/ecdsa_test.go | 205 +++++++++++++++++++++++--- pkg/core/interop/crypto/hash_test.go | 77 ++++++---- pkg/core/interop_neo_test.go | 85 ----------- 3 files changed, 232 insertions(+), 135 deletions(-) diff --git a/pkg/core/interop/crypto/ecdsa_test.go b/pkg/core/interop/crypto/ecdsa_test.go index c2546c81c..58fab77d6 100644 --- a/pkg/core/interop/crypto/ecdsa_test.go +++ b/pkg/core/interop/crypto/ecdsa_test.go @@ -2,9 +2,14 @@ package crypto import ( "encoding/binary" + "fmt" "testing" + "github.com/nspcc-dev/neo-go/pkg/config/netmode" + "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/storage" + "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/vm" @@ -14,14 +19,116 @@ import ( "github.com/stretchr/testify/require" ) -func initCHECKMULTISIG(msg []byte, n int) ([]stackitem.Item, []stackitem.Item, map[string]*keys.PublicKey, error) { +func TestECDSASecp256r1Verify(t *testing.T) { + testECDSAVerify(t, true) +} + +func TestECDSASecp256k1Verify(t *testing.T) { + testECDSAVerify(t, false) +} + +func testECDSAVerify(t *testing.T, isR1 bool) { + var priv *keys.PrivateKey + var err error + if isR1 { + priv, err = keys.NewPrivateKey() + } else { + priv, err = keys.NewSecp256k1PrivateKey() + } + require.NoError(t, err) + + verifyFunc := ECDSASecp256r1Verify + if !isR1 { + verifyFunc = ECDSASecp256k1Verify + } + d := dao.NewSimple(storage.NewMemoryStore(), netmode.UnitTestNet, false) + ic := &interop.Context{DAO: dao.NewCached(d)} + runCase := func(t *testing.T, isErr bool, result interface{}, args ...interface{}) { + ic.SpawnVM() + for i := range args { + ic.VM.Estack().PushVal(args[i]) + } + + var err error + func() { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("panic: %v", r) + } + }() + err = verifyFunc(ic) + }() + + if isErr { + require.Error(t, err) + return + } + require.NoError(t, err) + require.Equal(t, 1, ic.VM.Estack().Len()) + require.Equal(t, result, ic.VM.Estack().Pop().Value().(bool)) + } + + msg := []byte("test message") + + t.Run("success", func(t *testing.T) { + sign := priv.Sign(msg) + runCase(t, false, true, sign, priv.PublicKey().Bytes(), msg) + }) + + t.Run("signed interop item", func(t *testing.T) { + tx := transaction.New(netmode.UnitTestNet, []byte{0, 1, 2}, 1) + msg := tx.GetSignedPart() + sign := priv.Sign(msg) + runCase(t, false, true, sign, priv.PublicKey().Bytes(), stackitem.NewInterop(tx)) + }) + + t.Run("signed script container", func(t *testing.T) { + tx := transaction.New(netmode.UnitTestNet, []byte{0, 1, 2}, 1) + msg := tx.GetSignedPart() + sign := priv.Sign(msg) + ic.Container = tx + runCase(t, false, true, sign, priv.PublicKey().Bytes(), stackitem.Null{}) + }) + + t.Run("missing arguments", func(t *testing.T) { + runCase(t, true, false) + sign := priv.Sign(msg) + runCase(t, true, false, sign) + runCase(t, true, false, sign, priv.PublicKey().Bytes()) + }) + + t.Run("invalid signature", func(t *testing.T) { + sign := priv.Sign(msg) + sign[0] = ^sign[0] + runCase(t, false, false, sign, priv.PublicKey().Bytes(), msg) + }) + + t.Run("invalid public key", func(t *testing.T) { + sign := priv.Sign(msg) + pub := priv.PublicKey().Bytes() + pub[0] = 0xFF // invalid prefix + runCase(t, true, false, sign, pub, msg) + }) + + t.Run("invalid message", func(t *testing.T) { + sign := priv.Sign(msg) + runCase(t, true, false, sign, priv.PublicKey().Bytes(), + stackitem.NewArray([]stackitem.Item{stackitem.NewByteArray(msg)})) + }) +} + +func initCHECKMULTISIG(isR1 bool, msg []byte, n int) ([]stackitem.Item, []stackitem.Item, map[string]*keys.PublicKey, error) { var err error keyMap := make(map[string]*keys.PublicKey) pkeys := make([]*keys.PrivateKey, n) pubs := make([]stackitem.Item, n) for i := range pubs { - pkeys[i], err = keys.NewPrivateKey() + if isR1 { + pkeys[i], err = keys.NewPrivateKey() + } else { + pkeys[i], err = keys.NewSecp256k1PrivateKey() + } if err != nil { return nil, nil, nil, err } @@ -54,18 +161,27 @@ func subSlice(arr []stackitem.Item, indices []int) []stackitem.Item { return result } -func initCHECKMULTISIGVM(t *testing.T, n int, ik, is []int) *vm.VM { +func initCheckMultisigVMNoArgs(isR1 bool) *vm.VM { buf := make([]byte, 5) buf[0] = byte(opcode.SYSCALL) - binary.LittleEndian.PutUint32(buf[1:], ecdsaSecp256r1CheckMultisigID) + if isR1 { + binary.LittleEndian.PutUint32(buf[1:], ecdsaSecp256r1CheckMultisigID) + } else { + binary.LittleEndian.PutUint32(buf[1:], ecdsaSecp256k1CheckMultisigID) + } ic := &interop.Context{Trigger: trigger.Verification} Register(ic) v := ic.SpawnVM() v.LoadScript(buf) + return v +} + +func initCHECKMULTISIGVM(t *testing.T, isR1 bool, n int, ik, is []int) *vm.VM { + v := initCheckMultisigVMNoArgs(isR1) msg := []byte("NEO - An Open Network For Smart Economy") - pubs, sigs, _, err := initCHECKMULTISIG(msg, n) + pubs, sigs, _, err := initCHECKMULTISIG(isR1, msg, n) require.NoError(t, err) pubs = subSlice(pubs, ik) @@ -78,26 +194,34 @@ func initCHECKMULTISIGVM(t *testing.T, n int, ik, is []int) *vm.VM { return v } -func testCHECKMULTISIGGood(t *testing.T, n int, is []int) { - v := initCHECKMULTISIGVM(t, n, nil, is) +func testCHECKMULTISIGGood(t *testing.T, isR1 bool, n int, is []int) { + v := initCHECKMULTISIGVM(t, isR1, n, nil, is) require.NoError(t, v.Run()) assert.Equal(t, 1, v.Estack().Len()) assert.True(t, v.Estack().Pop().Bool()) } -func TestCHECKMULTISIGGood(t *testing.T) { - t.Run("3_1", func(t *testing.T) { testCHECKMULTISIGGood(t, 3, []int{1}) }) - t.Run("2_2", func(t *testing.T) { testCHECKMULTISIGGood(t, 2, []int{0, 1}) }) - t.Run("3_3", func(t *testing.T) { testCHECKMULTISIGGood(t, 3, []int{0, 1, 2}) }) - t.Run("3_2", func(t *testing.T) { testCHECKMULTISIGGood(t, 3, []int{0, 2}) }) - t.Run("4_2", func(t *testing.T) { testCHECKMULTISIGGood(t, 4, []int{0, 2}) }) - t.Run("10_7", func(t *testing.T) { testCHECKMULTISIGGood(t, 10, []int{2, 3, 4, 5, 6, 8, 9}) }) - t.Run("12_9", func(t *testing.T) { testCHECKMULTISIGGood(t, 12, []int{0, 1, 4, 5, 6, 7, 8, 9}) }) +func TestECDSASecp256r1CheckMultisigGood(t *testing.T) { + testCurveCHECKMULTISIGGood(t, true) } -func testCHECKMULTISIGBad(t *testing.T, isErr bool, n int, ik, is []int) { - v := initCHECKMULTISIGVM(t, n, ik, is) +func TestECDSASecp256k1CheckMultisigGood(t *testing.T) { + testCurveCHECKMULTISIGGood(t, false) +} + +func testCurveCHECKMULTISIGGood(t *testing.T, isR1 bool) { + t.Run("3_1", func(t *testing.T) { testCHECKMULTISIGGood(t, isR1, 3, []int{1}) }) + t.Run("2_2", func(t *testing.T) { testCHECKMULTISIGGood(t, isR1, 2, []int{0, 1}) }) + t.Run("3_3", func(t *testing.T) { testCHECKMULTISIGGood(t, isR1, 3, []int{0, 1, 2}) }) + t.Run("3_2", func(t *testing.T) { testCHECKMULTISIGGood(t, isR1, 3, []int{0, 2}) }) + t.Run("4_2", func(t *testing.T) { testCHECKMULTISIGGood(t, isR1, 4, []int{0, 2}) }) + t.Run("10_7", func(t *testing.T) { testCHECKMULTISIGGood(t, isR1, 10, []int{2, 3, 4, 5, 6, 8, 9}) }) + t.Run("12_9", func(t *testing.T) { testCHECKMULTISIGGood(t, isR1, 12, []int{0, 1, 4, 5, 6, 7, 8, 9}) }) +} + +func testCHECKMULTISIGBad(t *testing.T, isR1 bool, isErr bool, n int, ik, is []int) { + v := initCHECKMULTISIGVM(t, isR1, n, ik, is) if isErr { require.Error(t, v.Run()) @@ -108,14 +232,49 @@ func testCHECKMULTISIGBad(t *testing.T, isErr bool, n int, ik, is []int) { assert.False(t, v.Estack().Pop().Bool()) } -func TestCHECKMULTISIGBad(t *testing.T) { - t.Run("1_1 wrong signature", func(t *testing.T) { testCHECKMULTISIGBad(t, false, 2, []int{0}, []int{1}) }) - t.Run("3_2 wrong order", func(t *testing.T) { testCHECKMULTISIGBad(t, false, 3, []int{0, 2}, []int{2, 0}) }) - t.Run("3_2 duplicate sig", func(t *testing.T) { testCHECKMULTISIGBad(t, false, 3, nil, []int{0, 0}) }) - t.Run("1_2 too many signatures", func(t *testing.T) { testCHECKMULTISIGBad(t, true, 2, []int{0}, []int{0, 1}) }) +func TestECDSASecp256r1CheckMultisigBad(t *testing.T) { + testCurveCHECKMULTISIGBad(t, true) +} + +func TestECDSASecp256k1CheckMultisigBad(t *testing.T) { + testCurveCHECKMULTISIGBad(t, false) +} + +func testCurveCHECKMULTISIGBad(t *testing.T, isR1 bool) { + t.Run("1_1 wrong signature", func(t *testing.T) { testCHECKMULTISIGBad(t, isR1, false, 2, []int{0}, []int{1}) }) + t.Run("3_2 wrong order", func(t *testing.T) { testCHECKMULTISIGBad(t, isR1, false, 3, []int{0, 2}, []int{2, 0}) }) + t.Run("3_2 duplicate sig", func(t *testing.T) { testCHECKMULTISIGBad(t, isR1, false, 3, nil, []int{0, 0}) }) + t.Run("1_2 too many signatures", func(t *testing.T) { testCHECKMULTISIGBad(t, isR1, true, 2, []int{0}, []int{0, 1}) }) t.Run("gas limit exceeded", func(t *testing.T) { - v := initCHECKMULTISIGVM(t, 1, []int{0}, []int{0}) + v := initCHECKMULTISIGVM(t, isR1, 1, []int{0}, []int{0}) v.GasLimit = ECDSAVerifyPrice - 1 require.Error(t, v.Run()) }) + + msg := []byte("NEO - An Open Network For Smart Economy") + pubs, sigs, _, err := initCHECKMULTISIG(isR1, msg, 1) + require.NoError(t, err) + arr := stackitem.NewArray([]stackitem.Item{stackitem.NewArray(nil)}) + + t.Run("invalid message type", func(t *testing.T) { + v := initCheckMultisigVMNoArgs(isR1) + v.Estack().PushVal(sigs) + v.Estack().PushVal(pubs) + v.Estack().PushVal(stackitem.NewArray(nil)) + require.Error(t, v.Run()) + }) + t.Run("invalid public keys", func(t *testing.T) { + v := initCheckMultisigVMNoArgs(isR1) + v.Estack().PushVal(sigs) + v.Estack().PushVal(arr) + v.Estack().PushVal(msg) + require.Error(t, v.Run()) + }) + t.Run("invalid signatures", func(t *testing.T) { + v := initCheckMultisigVMNoArgs(isR1) + v.Estack().PushVal(arr) + v.Estack().PushVal(pubs) + v.Estack().PushVal(msg) + require.Error(t, v.Run()) + }) } diff --git a/pkg/core/interop/crypto/hash_test.go b/pkg/core/interop/crypto/hash_test.go index 4d87d27d5..313074d4a 100644 --- a/pkg/core/interop/crypto/hash_test.go +++ b/pkg/core/interop/crypto/hash_test.go @@ -5,42 +5,65 @@ import ( "testing" "github.com/nspcc-dev/neo-go/pkg/core/interop" - "github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames" - "github.com/nspcc-dev/neo-go/pkg/io" - "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" - "github.com/nspcc-dev/neo-go/pkg/vm/emit" - "github.com/stretchr/testify/assert" + "github.com/nspcc-dev/neo-go/pkg/crypto" + "github.com/nspcc-dev/neo-go/pkg/crypto/hash" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/stretchr/testify/require" ) +type testVerifiable []byte + +var _ crypto.Verifiable = testVerifiable{} + +func (v testVerifiable) GetSignedPart() []byte { + return v +} +func (v testVerifiable) GetSignedHash() util.Uint256 { + return hash.Sha256(v) +} + +func testHash0100(t *testing.T, result string, interopFunc func(*interop.Context) error) { + t.Run("good", func(t *testing.T) { + bs := []byte{1, 0} + + checkGood := func(t *testing.T, ic *interop.Context) { + require.NoError(t, interopFunc(ic)) + require.Equal(t, 1, ic.VM.Estack().Len()) + require.Equal(t, result, hex.EncodeToString(ic.VM.Estack().Pop().Bytes())) + } + t.Run("raw bytes", func(t *testing.T) { + ic := &interop.Context{VM: vm.New()} + ic.VM.Estack().PushVal(bs) + checkGood(t, ic) + }) + t.Run("interop", func(t *testing.T) { + ic := &interop.Context{VM: vm.New()} + ic.VM.Estack().PushVal(stackitem.NewInterop(testVerifiable(bs))) + checkGood(t, ic) + }) + t.Run("container", func(t *testing.T) { + ic := &interop.Context{VM: vm.New(), Container: testVerifiable(bs)} + ic.VM.Estack().PushVal(stackitem.Null{}) + checkGood(t, ic) + }) + }) + t.Run("bad message", func(t *testing.T) { + ic := &interop.Context{VM: vm.New()} + ic.VM.Estack().PushVal(stackitem.NewArray(nil)) + require.Error(t, interopFunc(ic)) + }) +} + func TestSHA256(t *testing.T) { // 0x0100 hashes to 47dc540c94ceb704a23875c11273e16bb0b8a87aed84de911f2133568115f254 res := "47dc540c94ceb704a23875c11273e16bb0b8a87aed84de911f2133568115f254" - buf := io.NewBufBinWriter() - emit.Bytes(buf.BinWriter, []byte{1, 0}) - emit.Syscall(buf.BinWriter, interopnames.NeoCryptoSHA256) - prog := buf.Bytes() - ic := &interop.Context{Trigger: trigger.Verification} - Register(ic) - v := ic.SpawnVM() - v.Load(prog) - require.NoError(t, v.Run()) - assert.Equal(t, 1, v.Estack().Len()) - assert.Equal(t, res, hex.EncodeToString(v.Estack().Pop().Bytes())) + testHash0100(t, res, Sha256) } func TestRIPEMD160(t *testing.T) { // 0x0100 hashes to 213492c0c6fc5d61497cf17249dd31cd9964b8a3 res := "213492c0c6fc5d61497cf17249dd31cd9964b8a3" - buf := io.NewBufBinWriter() - emit.Bytes(buf.BinWriter, []byte{1, 0}) - emit.Syscall(buf.BinWriter, interopnames.NeoCryptoRIPEMD160) - prog := buf.Bytes() - ic := &interop.Context{Trigger: trigger.Verification} - Register(ic) - v := ic.SpawnVM() - v.Load(prog) - require.NoError(t, v.Run()) - assert.Equal(t, 1, v.Estack().Len()) - assert.Equal(t, res, hex.EncodeToString(v.Estack().Pop().Bytes())) + testHash0100(t, res, RipeMD160) } diff --git a/pkg/core/interop_neo_test.go b/pkg/core/interop_neo_test.go index 5f4462db4..5366e4d91 100644 --- a/pkg/core/interop_neo_test.go +++ b/pkg/core/interop_neo_test.go @@ -1,21 +1,18 @@ package core import ( - "fmt" "testing" "github.com/nspcc-dev/neo-go/pkg/config/netmode" "github.com/nspcc-dev/neo-go/pkg/core/block" "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/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/state" "github.com/nspcc-dev/neo-go/pkg/core/storage" "github.com/nspcc-dev/neo-go/pkg/core/transaction" "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/manifest" "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" "github.com/nspcc-dev/neo-go/pkg/util" @@ -129,88 +126,6 @@ func TestStorageFind(t *testing.T) { }) } -func TestECDSAVerify(t *testing.T) { - priv, err := keys.NewPrivateKey() - require.NoError(t, err) - - chain := newTestChain(t) - defer chain.Close() - - ic := chain.newInteropContext(trigger.Application, dao.NewSimple(storage.NewMemoryStore(), netmode.UnitTestNet, false), nil, nil) - runCase := func(t *testing.T, isErr bool, result interface{}, args ...interface{}) { - ic.SpawnVM() - for i := range args { - ic.VM.Estack().PushVal(args[i]) - } - - var err error - func() { - defer func() { - if r := recover(); r != nil { - err = fmt.Errorf("panic: %v", r) - } - }() - err = crypto.ECDSASecp256r1Verify(ic) - }() - - if isErr { - require.Error(t, err) - return - } - require.NoError(t, err) - require.Equal(t, 1, ic.VM.Estack().Len()) - require.Equal(t, result, ic.VM.Estack().Pop().Value().(bool)) - } - - msg := []byte("test message") - - t.Run("success", func(t *testing.T) { - sign := priv.Sign(msg) - runCase(t, false, true, sign, priv.PublicKey().Bytes(), msg) - }) - - t.Run("signed interop item", func(t *testing.T) { - tx := transaction.New(netmode.UnitTestNet, []byte{0, 1, 2}, 1) - msg := tx.GetSignedPart() - sign := priv.Sign(msg) - runCase(t, false, true, sign, priv.PublicKey().Bytes(), stackitem.NewInterop(tx)) - }) - - t.Run("signed script container", func(t *testing.T) { - tx := transaction.New(netmode.UnitTestNet, []byte{0, 1, 2}, 1) - msg := tx.GetSignedPart() - sign := priv.Sign(msg) - ic.Container = tx - runCase(t, false, true, sign, priv.PublicKey().Bytes(), stackitem.Null{}) - }) - - t.Run("missing arguments", func(t *testing.T) { - runCase(t, true, false) - sign := priv.Sign(msg) - runCase(t, true, false, sign) - runCase(t, true, false, sign, priv.PublicKey().Bytes()) - }) - - t.Run("invalid signature", func(t *testing.T) { - sign := priv.Sign(msg) - sign[0] = ^sign[0] - runCase(t, false, false, sign, priv.PublicKey().Bytes(), msg) - }) - - t.Run("invalid public key", func(t *testing.T) { - sign := priv.Sign(msg) - pub := priv.PublicKey().Bytes() - pub[0] = 0xFF // invalid prefix - runCase(t, true, false, sign, pub, msg) - }) - - t.Run("invalid message", func(t *testing.T) { - sign := priv.Sign(msg) - runCase(t, true, false, sign, priv.PublicKey().Bytes(), - stackitem.NewArray([]stackitem.Item{stackitem.NewByteArray(msg)})) - }) -} - // Helper functions to create VM, InteropContext, TX, Account, Contract. func createVM(t *testing.T) (*vm.VM, *interop.Context, *Blockchain) {