*: make Notify interop accept event name

This commit is contained in:
Evgenii Stratonikov 2020-06-29 11:25:32 +03:00
parent 1154e180fa
commit 3d7fa9de93
17 changed files with 84 additions and 43 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

Binary file not shown.

Binary file not shown.

View file

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