diff --git a/examples/engine/engine.go b/examples/engine/engine.go index f71db3399..3fe1108c3 100644 --- a/examples/engine/engine.go +++ b/examples/engine/engine.go @@ -7,16 +7,16 @@ import ( // Main is that famous Main() function, you know. func Main() bool { tx := runtime.GetScriptContainer() - runtime.Notify(tx.Hash) + runtime.Notify("Tx", tx.Hash) callingScriptHash := runtime.GetCallingScriptHash() - runtime.Notify(callingScriptHash) + runtime.Notify("Calling", callingScriptHash) execScriptHash := runtime.GetExecutingScriptHash() - runtime.Notify(execScriptHash) + runtime.Notify("Executing", execScriptHash) entryScriptHash := runtime.GetEntryScriptHash() - runtime.Notify(entryScriptHash) + runtime.Notify("Entry", entryScriptHash) return true } diff --git a/examples/runtime/runtime.go b/examples/runtime/runtime.go index c2f700d3d..4593b79dd 100644 --- a/examples/runtime/runtime.go +++ b/examples/runtime/runtime.go @@ -54,6 +54,6 @@ func Log(args []interface{}) bool { // Notify notifies about given message func Notify(args []interface{}) bool { - runtime.Notify(args[0]) + runtime.Notify("Event", args[0]) return true } diff --git a/pkg/compiler/syscall_test.go b/pkg/compiler/syscall_test.go index 353b6efb7..475c94144 100644 --- a/pkg/compiler/syscall_test.go +++ b/pkg/compiler/syscall_test.go @@ -30,8 +30,7 @@ func TestNotify(t *testing.T) { src := `package foo import "github.com/nspcc-dev/neo-go/pkg/interop/runtime" func Main(arg int) { - runtime.Notify(arg, "sum", arg+1) - runtime.Notify() + runtime.Notify("Event1", arg, "sum", arg+1) runtime.Notify("single") }` @@ -39,10 +38,11 @@ func TestNotify(t *testing.T) { v.Estack().PushVal(11) require.NoError(t, v.Run()) - require.Equal(t, 3, len(s.events)) + require.Equal(t, 2, len(s.events)) exp0 := []stackitem.Item{stackitem.NewBigInteger(big.NewInt(11)), stackitem.NewByteArray([]byte("sum")), stackitem.NewBigInteger(big.NewInt(12))} - assert.Equal(t, exp0, s.events[0].Value()) - assert.Equal(t, []stackitem.Item{}, s.events[1].Value()) - assert.Equal(t, []stackitem.Item{stackitem.NewByteArray([]byte("single"))}, s.events[2].Value()) + assert.Equal(t, "Event1", s.events[0].Name) + assert.Equal(t, exp0, s.events[0].Item.Value()) + assert.Equal(t, "single", s.events[1].Name) + assert.Equal(t, []stackitem.Item{}, s.events[1].Item.Value()) } diff --git a/pkg/compiler/vm_test.go b/pkg/compiler/vm_test.go index fab6b6eac..65f6aaab1 100644 --- a/pkg/compiler/vm_test.go +++ b/pkg/compiler/vm_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/nspcc-dev/neo-go/pkg/compiler" + "github.com/nspcc-dev/neo-go/pkg/core/state" "github.com/nspcc-dev/neo-go/pkg/vm" "github.com/nspcc-dev/neo-go/pkg/vm/emit" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" @@ -74,7 +75,7 @@ func vmAndCompileInterop(t *testing.T, src string) (*vm.VM, *storagePlugin) { type storagePlugin struct { mem map[string][]byte interops map[uint32]vm.InteropFunc - events []stackitem.Item + events []state.NotificationEvent } func newStoragePlugin() *storagePlugin { @@ -99,7 +100,12 @@ func (s *storagePlugin) getInterop(id uint32) *vm.InteropFuncPrice { } func (s *storagePlugin) Notify(v *vm.VM) error { - s.events = append(s.events, v.Estack().Pop().Item()) + name := string(v.Estack().Pop().Bytes()) + item := stackitem.NewArray(v.Estack().Pop().Array()) + s.events = append(s.events, state.NotificationEvent{ + Name: name, + Item: item, + }) return nil } diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 943f31a84..ceeffdc1e 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -661,16 +661,15 @@ func (bc *Blockchain) storeBlock(block *block.Block) error { } func (bc *Blockchain) handleNotification(note *state.NotificationEvent, d *dao.Cached, b *block.Block, h util.Uint256) { - arr, ok := note.Item.Value().([]stackitem.Item) - if !ok || len(arr) != 4 { + if note.Name != "transfer" && note.Name != "Transfer" { return } - op, ok := arr[0].Value().([]byte) - if !ok || (string(op) != "transfer" && string(op) != "Transfer") { + arr, ok := note.Item.Value().([]stackitem.Item) + if !ok || len(arr) != 3 { return } var from []byte - fromValue := arr[1].Value() + fromValue := arr[0].Value() // we don't have `from` set when we are minting tokens if fromValue != nil { from, ok = fromValue.([]byte) @@ -679,7 +678,7 @@ func (bc *Blockchain) handleNotification(note *state.NotificationEvent, d *dao.C } } var to []byte - toValue := arr[2].Value() + toValue := arr[1].Value() // we don't have `to` set when we are burning tokens if toValue != nil { to, ok = toValue.([]byte) @@ -687,9 +686,9 @@ func (bc *Blockchain) handleNotification(note *state.NotificationEvent, d *dao.C return } } - amount, ok := arr[3].Value().(*big.Int) + amount, ok := arr[2].Value().(*big.Int) if !ok { - bs, ok := arr[3].Value().([]byte) + bs, ok := arr[2].Value().([]byte) if !ok { return } diff --git a/pkg/core/interop/runtime/util.go b/pkg/core/interop/runtime/util.go index 9a79375b9..e1200055c 100644 --- a/pkg/core/interop/runtime/util.go +++ b/pkg/core/interop/runtime/util.go @@ -42,6 +42,7 @@ func GetNotifications(ic *interop.Context, v *vm.VM) error { for i := range notifications { ev := stackitem.NewArray([]stackitem.Item{ stackitem.NewByteArray(notifications[i].ScriptHash.BytesBE()), + stackitem.Make(notifications[i].Name), notifications[i].Item, }) arr.Append(ev) diff --git a/pkg/core/interop_system.go b/pkg/core/interop_system.go index 54c77a315..17f6b00bd 100644 --- a/pkg/core/interop_system.go +++ b/pkg/core/interop_system.go @@ -27,6 +27,8 @@ const ( // MaxTraceableBlocks is the maximum number of blocks before current chain // height we're able to give information about. MaxTraceableBlocks = transaction.MaxValidUntilBlockIncrement + // MaxEventNameLen is the maximum length of a name for event. + MaxEventNameLen = 32 ) // StorageContext contains storing id and read/write flag, it's used as @@ -244,19 +246,26 @@ func runtimeGetTrigger(ic *interop.Context, v *vm.VM) error { // 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, v *vm.VM) error { - // It can be just about anything. - e := v.Estack().Pop() - item := e.Item() + name := v.Estack().Pop().Bytes() + if len(name) > MaxEventNameLen { + return fmt.Errorf("event name must be less than %d", MaxEventNameLen) + } + elem := v.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. I'd probably fail transactions // that emit such broken notifications, but that might break compatibility // with testnet/mainnet, so we're replacing these with error messages. - _, err := stackitem.SerializeItem(item) + _, err := stackitem.SerializeItem(elem.Item()) if err != nil { - item = stackitem.NewByteArray([]byte(fmt.Sprintf("bad notification: %v", err))) + args = []stackitem.Item{stackitem.NewByteArray([]byte(fmt.Sprintf("bad notification: %v", err)))} + } + ne := state.NotificationEvent{ + ScriptHash: v.GetCurrentScriptHash(), + Name: string(name), + Item: stackitem.NewArray(args), } - ne := state.NotificationEvent{ScriptHash: v.GetCurrentScriptHash(), Item: item} ic.Notifications = append(ic.Notifications, ne) return nil } diff --git a/pkg/core/interop_system_test.go b/pkg/core/interop_system_test.go index b115b8f68..02f0b62d3 100644 --- a/pkg/core/interop_system_test.go +++ b/pkg/core/interop_system_test.go @@ -247,9 +247,9 @@ func TestRuntimeGetNotifications(t *testing.T) { defer chain.Close() ic.Notifications = []state.NotificationEvent{ - {ScriptHash: util.Uint160{1}, Item: stackitem.NewByteArray([]byte{11})}, - {ScriptHash: util.Uint160{2}, Item: stackitem.NewByteArray([]byte{22})}, - {ScriptHash: util.Uint160{1}, Item: stackitem.NewByteArray([]byte{33})}, + {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) { @@ -261,7 +261,10 @@ func TestRuntimeGetNotifications(t *testing.T) { for i := range arr { elem := arr[i].Value().([]stackitem.Item) require.Equal(t, ic.Notifications[i].ScriptHash.BytesBE(), elem[0].Value()) - require.Equal(t, ic.Notifications[i].Item, elem[1]) + name, err := elem[1].TryBytes() + require.NoError(t, err) + require.Equal(t, ic.Notifications[i].Name, string(name)) + require.Equal(t, ic.Notifications[i].Item, elem[2]) } }) @@ -274,7 +277,10 @@ func TestRuntimeGetNotifications(t *testing.T) { require.Equal(t, 1, len(arr)) elem := arr[0].Value().([]stackitem.Item) require.Equal(t, h, elem[0].Value()) - require.Equal(t, ic.Notifications[1].Item, elem[1]) + name, err := elem[1].TryBytes() + require.NoError(t, err) + require.Equal(t, ic.Notifications[1].Name, string(name)) + require.Equal(t, ic.Notifications[1].Item, elem[2]) }) } diff --git a/pkg/core/native/native_nep5.go b/pkg/core/native/native_nep5.go index 393e6eabc..308d199e3 100644 --- a/pkg/core/native/native_nep5.go +++ b/pkg/core/native/native_nep5.go @@ -138,8 +138,8 @@ func addrToStackItem(u *util.Uint160) stackitem.Item { func (c *nep5TokenNative) emitTransfer(ic *interop.Context, from, to *util.Uint160, amount *big.Int) { ne := state.NotificationEvent{ ScriptHash: c.Hash, + Name: "Transfer", Item: stackitem.NewArray([]stackitem.Item{ - stackitem.NewByteArray([]byte("Transfer")), addrToStackItem(from), addrToStackItem(to), stackitem.NewBigInteger(amount), diff --git a/pkg/core/state/notification_event.go b/pkg/core/state/notification_event.go index 12699b14a..4849de6c4 100644 --- a/pkg/core/state/notification_event.go +++ b/pkg/core/state/notification_event.go @@ -1,6 +1,8 @@ package state import ( + "errors" + "github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" @@ -12,7 +14,8 @@ import ( // notification and that item itself. type NotificationEvent struct { ScriptHash util.Uint160 - Item stackitem.Item + Name string + Item *stackitem.Array } // AppExecResult represent the result of the script execution, gathering together @@ -29,13 +32,24 @@ type AppExecResult struct { // EncodeBinary implements the Serializable interface. func (ne *NotificationEvent) EncodeBinary(w *io.BinWriter) { ne.ScriptHash.EncodeBinary(w) + w.WriteString(ne.Name) stackitem.EncodeBinaryStackItem(ne.Item, w) } // DecodeBinary implements the Serializable interface. func (ne *NotificationEvent) DecodeBinary(r *io.BinReader) { ne.ScriptHash.DecodeBinary(r) - ne.Item = stackitem.DecodeBinaryStackItem(r) + ne.Name = r.ReadString() + item := stackitem.DecodeBinaryStackItem(r) + if r.Err != nil { + return + } + arr, ok := item.Value().([]stackitem.Item) + if !ok { + r.Err = errors.New("Array or Struct expected") + return + } + ne.Item = stackitem.NewArray(arr) } // EncodeBinary implements the Serializable interface. diff --git a/pkg/core/state/notification_event_test.go b/pkg/core/state/notification_event_test.go index c88e2d5ff..2b81c9bc6 100644 --- a/pkg/core/state/notification_event_test.go +++ b/pkg/core/state/notification_event_test.go @@ -12,7 +12,8 @@ import ( func TestEncodeDecodeNotificationEvent(t *testing.T) { event := &NotificationEvent{ ScriptHash: random.Uint160(), - Item: stackitem.NewBool(true), + Name: "Event", + Item: stackitem.NewArray([]stackitem.Item{stackitem.NewBool(true)}), } testserdes.EncodeDecodeBinary(t, event, new(NotificationEvent)) diff --git a/pkg/interop/runtime/runtime.go b/pkg/interop/runtime/runtime.go index 558362c65..a45088e1b 100644 --- a/pkg/interop/runtime/runtime.go +++ b/pkg/interop/runtime/runtime.go @@ -17,12 +17,12 @@ func CheckWitness(hashOrKey []byte) bool { func Log(message string) {} // Notify sends a notification (collecting all arguments in an array) to the -// executing environment. Unlike Log it can accept any data and resulting -// notification is saved in application log. It's intended to be used as a +// executing environment. Unlike Log it can accept any data along with the event name +// and resulting notification is saved in application log. It's intended to be used as a // part of contract's API to external systems, these events can be monitored // from outside and act upon accordingly. This function uses // `System.Runtime.Notify` syscall. -func Notify(arg ...interface{}) {} +func Notify(name string, arg ...interface{}) {} // GetTime returns the timestamp of the most recent block. Note that when running // script in test mode this would be the last accepted (persisted) block in the diff --git a/pkg/rpc/response/result/application_log.go b/pkg/rpc/response/result/application_log.go index f4fb1772a..afefddf8a 100644 --- a/pkg/rpc/response/result/application_log.go +++ b/pkg/rpc/response/result/application_log.go @@ -28,7 +28,11 @@ type NotificationEvent struct { // result.NotificationEvent. func StateEventToResultNotification(event state.NotificationEvent) NotificationEvent { seen := make(map[stackitem.Item]bool) - item := smartcontract.ParameterFromStackItem(event.Item, seen) + args := stackitem.NewArray([]stackitem.Item{ + stackitem.Make(event.Name), + event.Item, + }) + item := smartcontract.ParameterFromStackItem(args, seen) return NotificationEvent{ Contract: event.ScriptHash, Item: item, diff --git a/pkg/rpc/server/server_test.go b/pkg/rpc/server/server_test.go index 7223259f5..48bbfef89 100644 --- a/pkg/rpc/server/server_test.go +++ b/pkg/rpc/server/server_test.go @@ -51,8 +51,8 @@ type rpcTestCase struct { check func(t *testing.T, e *executor, result interface{}) } -const testContractHash = "10e262ef80c76bdecca287a2c047841fc02c3129" -const deploymentTxHash = "49f555734b90eb7d4f87041b5146a9b6bc7cf70060bb665212773719091b3a81" +const testContractHash = "402da558b87b5e54b59dc242c788bb4dd4cd906c" +const deploymentTxHash = "2afd69cc80ebe900a060450e8628b57063f3ec93ca5fc7f94582be4a4f3a041f" var rpcTestCases = map[string][]rpcTestCase{ "getapplicationlog": { diff --git a/pkg/rpc/server/testdata/test_contract.avm b/pkg/rpc/server/testdata/test_contract.avm index bdb814d37..dbb934338 100755 Binary files a/pkg/rpc/server/testdata/test_contract.avm and b/pkg/rpc/server/testdata/test_contract.avm differ diff --git a/pkg/rpc/server/testdata/testblocks.acc b/pkg/rpc/server/testdata/testblocks.acc index 98d85b7a0..f068d8f47 100644 Binary files a/pkg/rpc/server/testdata/testblocks.acc and b/pkg/rpc/server/testdata/testblocks.acc differ diff --git a/pkg/vm/interop.go b/pkg/vm/interop.go index f24a1bcf4..2c6259334 100644 --- a/pkg/vm/interop.go +++ b/pkg/vm/interop.go @@ -82,8 +82,9 @@ func runtimeLog(vm *VM) error { // runtimeNotify handles the syscall "System.Runtime.Notify" for printing and logging stuff. func runtimeNotify(vm *VM) error { + name := vm.Estack().Pop().Bytes() item := vm.Estack().Pop() - fmt.Printf("NEO-GO-VM (notify) > %s\n", item.Value()) + fmt.Printf("NEO-GO-VM (notify) > [%s] %s\n", string(name), item.Value()) return nil }