From 3d7fa9de93d67d553f3c743772ed3f603aec2eee Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Mon, 29 Jun 2020 11:25:32 +0300 Subject: [PATCH] *: make Notify interop accept event name --- examples/engine/engine.go | 8 ++++---- examples/runtime/runtime.go | 2 +- pkg/compiler/syscall_test.go | 12 ++++++------ pkg/compiler/vm_test.go | 10 ++++++++-- pkg/core/blockchain.go | 15 +++++++-------- pkg/core/interop/runtime/util.go | 1 + pkg/core/interop_system.go | 21 +++++++++++++++------ pkg/core/interop_system_test.go | 16 +++++++++++----- pkg/core/native/native_nep5.go | 2 +- pkg/core/state/notification_event.go | 18 ++++++++++++++++-- pkg/core/state/notification_event_test.go | 3 ++- pkg/interop/runtime/runtime.go | 6 +++--- pkg/rpc/response/result/application_log.go | 6 +++++- pkg/rpc/server/server_test.go | 4 ++-- pkg/rpc/server/testdata/test_contract.avm | Bin 756 -> 792 bytes pkg/rpc/server/testdata/testblocks.acc | Bin 6810 -> 6846 bytes pkg/vm/interop.go | 3 ++- 17 files changed, 84 insertions(+), 43 deletions(-) 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 bdb814d371c7b843a5aaa7f0364df89fdeeb98ee..dbb93433899a0a48093d0625d5a48d3720a8c5c8 100755 GIT binary patch literal 792 zcmZ`%F>ljA7*zqNGLWLM(9C3{8%7R_VLW+cl8bK$7E{+XO&-VrUTqn%@0t9$K zf)ydL(VZEIuvYR8`~pVAotrd`D9cHA?|bjwd-@($4!af9@PP4{c)a5gN~g1a`%B+> zy#d6W_RwZO$$+qPbnK$Vfm&v9EmqUu39q_*|EM6M*WXb5zn zhBgl#?AzAk64Hi*66X2$gB)k)2A`Gw;&Jb^yY=-()isoz&)5_&+78(dB>=3JX0{4Ou8w@SeDw)te!w`166AS)*L8 mtO*Dj(ELXcDaCFHtchAOS8Le}O#GPe5Lj8xZEH)QuKgDli}cR` literal 756 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 delta 3181 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 %s\n", item.Value()) + fmt.Printf("NEO-GO-VM (notify) > [%s] %s\n", string(name), item.Value()) return nil }