From 5ce269c035b74b93aafdc9fcaf8b2d96e8334ad0 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 13 Nov 2019 16:55:20 +0300 Subject: [PATCH 1/5] core: change notify implementation to save notifications Fixes #453 and makes it possible to refer to these notifications later. --- pkg/core/blockchain.go | 14 +++++- pkg/core/interop_system.go | 6 ++- pkg/core/interops.go | 14 +++--- pkg/core/notification_event.go | 80 ++++++++++++++++++++++++++++++++++ pkg/core/storage/store.go | 1 + pkg/vm/serialization.go | 23 +++++++--- pkg/vm/stack.go | 5 +++ 7 files changed, 128 insertions(+), 15 deletions(-) create mode 100644 pkg/core/notification_event.go diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 77ae2e8fa..87c6af601 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -473,7 +473,7 @@ func (bc *Blockchain) storeBlock(block *Block) error { vm.LoadScript(t.Script) err := vm.Run() if !vm.HasFailed() { - _, err := systemInterop.mem.Persist() + _, err = systemInterop.mem.Persist() if err != nil { return errors.Wrap(err, "failed to persist invocation results") } @@ -484,6 +484,18 @@ func (bc *Blockchain) storeBlock(block *Block) error { "err": err, }).Warn("contract invocation failed") } + aer := &AppExecResult{ + TxHash: tx.Hash(), + Trigger: 0x10, + VMState: vm.State(), + GasConsumed: util.Fixed8(0), + Stack: vm.Stack("estack"), + Events: systemInterop.notifications, + } + err = putAppExecResultIntoStore(tmpStore, aer) + if err != nil { + return errors.Wrap(err, "failed to store notifications") + } } } diff --git a/pkg/core/interop_system.go b/pkg/core/interop_system.go index fbbf768a2..51aa4ed77 100644 --- a/pkg/core/interop_system.go +++ b/pkg/core/interop_system.go @@ -343,8 +343,10 @@ func (ic *interopContext) runtimeCheckWitness(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 (ic *interopContext) runtimeNotify(v *vm.VM) error { - msg := fmt.Sprintf("%q", v.Estack().Pop().Bytes()) - log.Infof("script %s notifies: %s", getContextScriptHash(v, 0), msg) + // It can be just about anything. + e := v.Estack().Pop() + ne := NotificationEvent{getContextScriptHash(v, 0), e.Item()} + ic.notifications = append(ic.notifications, ne) return nil } diff --git a/pkg/core/interops.go b/pkg/core/interops.go index 1df944eda..a8e1dfa7d 100644 --- a/pkg/core/interops.go +++ b/pkg/core/interops.go @@ -14,16 +14,18 @@ import ( ) type interopContext struct { - bc Blockchainer - trigger byte - block *Block - tx *transaction.Transaction - mem *storage.MemCachedStore + bc Blockchainer + trigger byte + block *Block + tx *transaction.Transaction + mem *storage.MemCachedStore + notifications []NotificationEvent } func newInteropContext(trigger byte, bc Blockchainer, s storage.Store, block *Block, tx *transaction.Transaction) *interopContext { mem := storage.NewMemCachedStore(s) - return &interopContext{bc, trigger, block, tx, mem} + nes := make([]NotificationEvent, 0) + return &interopContext{bc, trigger, block, tx, mem, nes} } // All lists are sorted, keep 'em this way, please. diff --git a/pkg/core/notification_event.go b/pkg/core/notification_event.go new file mode 100644 index 000000000..5b15e39cd --- /dev/null +++ b/pkg/core/notification_event.go @@ -0,0 +1,80 @@ +package core + +import ( + "github.com/CityOfZion/neo-go/pkg/core/storage" + "github.com/CityOfZion/neo-go/pkg/io" + "github.com/CityOfZion/neo-go/pkg/util" + "github.com/CityOfZion/neo-go/pkg/vm" + "github.com/pkg/errors" +) + +// NotificationEvent is a tuple of scripthash that emitted the StackItem as a +// notification and that item itself. +type NotificationEvent struct { + ScriptHash util.Uint160 + Item vm.StackItem +} + +// AppExecResult represent the result of the script execution, gathering together +// all resulting notifications, state, stack and other metadata. +type AppExecResult struct { + TxHash util.Uint256 + Trigger byte + VMState string + GasConsumed util.Fixed8 + Stack string // JSON + Events []NotificationEvent +} + +// putAppExecResultIntoStore puts given application execution result into the +// given store. +func putAppExecResultIntoStore(s storage.Store, aer *AppExecResult) error { + buf := io.NewBufBinWriter() + aer.EncodeBinary(buf.BinWriter) + if buf.Err != nil { + return buf.Err + } + key := storage.AppendPrefix(storage.STNotification, aer.TxHash.Bytes()) + return s.Put(key, buf.Bytes()) +} + +// getAppExecResultFromStore gets application execution result from the +// given store. +func getAppExecResultFromStore(s storage.Store, hash util.Uint256) (*AppExecResult, error) { + aer := &AppExecResult{} + key := storage.AppendPrefix(storage.STNotification, hash.Bytes()) + if b, err := s.Get(key); err == nil { + r := io.NewBinReaderFromBuf(b) + aer.DecodeBinary(r) + if r.Err != nil { + return nil, errors.Wrap(r.Err, "decoding failure:") + } + } else { + return nil, err + } + return aer, nil +} + +// EncodeBinary implements the Serializable interface. +func (ne *NotificationEvent) EncodeBinary(w *io.BinWriter) { + w.WriteLE(ne.ScriptHash) + vm.EncodeBinaryStackItem(ne.Item, w) +} + +// DecodeBinary implements the Serializable interface. +func (ne *NotificationEvent) DecodeBinary(r *io.BinReader) { + r.ReadLE(&ne.ScriptHash) + ne.Item = vm.DecodeBinaryStackItem(r) +} + +// EncodeBinary implements the Serializable interface. +func (aer *AppExecResult) EncodeBinary(w *io.BinWriter) { + w.WriteLE(aer.TxHash) + w.WriteArray(aer.Events) +} + +// DecodeBinary implements the Serializable interface. +func (aer *AppExecResult) DecodeBinary(r *io.BinReader) { + r.ReadLE(&aer.TxHash) + r.ReadArray(&aer.Events) +} diff --git a/pkg/core/storage/store.go b/pkg/core/storage/store.go index b494d1c8d..5ec0f9d67 100644 --- a/pkg/core/storage/store.go +++ b/pkg/core/storage/store.go @@ -14,6 +14,7 @@ const ( STSpentCoin KeyPrefix = 0x45 STValidator KeyPrefix = 0x48 STAsset KeyPrefix = 0x4c + STNotification KeyPrefix = 0x4d STContract KeyPrefix = 0x50 STStorage KeyPrefix = 0x70 IXHeaderHashList KeyPrefix = 0x80 diff --git a/pkg/vm/serialization.go b/pkg/vm/serialization.go index 6d731b1c6..81562df0d 100644 --- a/pkg/vm/serialization.go +++ b/pkg/vm/serialization.go @@ -21,13 +21,20 @@ const ( func serializeItem(item StackItem) ([]byte, error) { w := io.NewBufBinWriter() - serializeItemTo(item, w.BinWriter, make(map[StackItem]bool)) + EncodeBinaryStackItem(item, w.BinWriter) if w.Err != nil { return nil, w.Err } return w.Bytes(), nil } +// EncodeBinaryStackItem encodes given StackItem into the given BinWriter. It's +// similar to io.Serializable's EncodeBinary, but works with StackItem +// interface. +func EncodeBinaryStackItem(item StackItem, w *io.BinWriter) { + serializeItemTo(item, w, make(map[StackItem]bool)) +} + func serializeItemTo(item StackItem, w *io.BinWriter, seen map[StackItem]bool) { if seen[item] { w.Err = errors.New("recursive structures are not supported") @@ -75,14 +82,18 @@ func serializeItemTo(item StackItem, w *io.BinWriter, seen map[StackItem]bool) { func deserializeItem(data []byte) (StackItem, error) { r := io.NewBinReaderFromBuf(data) - item := deserializeItemFrom(r) + item := DecodeBinaryStackItem(r) if r.Err != nil { return nil, r.Err } return item, nil } -func deserializeItemFrom(r *io.BinReader) StackItem { +// DecodeBinaryStackItem decodes previously serialized StackItem from the given +// reader. It's similar to the io.Serializable's DecodeBinary(), but implemented +// as a function because StackItem itself is an interface. Caveat: always check +// reader's error value before using the returned StackItem. +func DecodeBinaryStackItem(r *io.BinReader) StackItem { var t byte r.ReadLE(&t) if r.Err != nil { @@ -107,7 +118,7 @@ func deserializeItemFrom(r *io.BinReader) StackItem { size := int(r.ReadVarUint()) arr := make([]StackItem, size) for i := 0; i < size; i++ { - arr[i] = deserializeItemFrom(r) + arr[i] = DecodeBinaryStackItem(r) } if stackItemType(t) == arrayT { @@ -118,8 +129,8 @@ func deserializeItemFrom(r *io.BinReader) StackItem { size := int(r.ReadVarUint()) m := NewMapItem() for i := 0; i < size; i++ { - value := deserializeItemFrom(r) - key := deserializeItemFrom(r) + value := DecodeBinaryStackItem(r) + key := DecodeBinaryStackItem(r) if r.Err != nil { break } diff --git a/pkg/vm/stack.go b/pkg/vm/stack.go index 183f6eac6..23a4b2247 100644 --- a/pkg/vm/stack.go +++ b/pkg/vm/stack.go @@ -59,6 +59,11 @@ func (e *Element) Prev() *Element { return nil } +// Item returns StackItem contained in the element. +func (e *Element) Item() StackItem { + return e.value +} + // Value returns value of the StackItem contained in the element. func (e *Element) Value() interface{} { return e.value.Value() From 64e20508e0b736a873a01a203779fce198db3828 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Thu, 14 Nov 2019 16:07:15 +0300 Subject: [PATCH 2/5] vm: use hex for ByteArray JSON marshallization It's way more convenient for different purposes. --- pkg/vm/stack_item.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/vm/stack_item.go b/pkg/vm/stack_item.go index c2faabf0f..4b13c12ee 100644 --- a/pkg/vm/stack_item.go +++ b/pkg/vm/stack_item.go @@ -2,6 +2,7 @@ package vm import ( "encoding/binary" + "encoding/hex" "encoding/json" "fmt" "math/big" @@ -197,7 +198,7 @@ func (i *ByteArrayItem) Value() interface{} { // MarshalJSON implements the json.Marshaler interface. func (i *ByteArrayItem) MarshalJSON() ([]byte, error) { - return json.Marshal(string(i.value)) + return json.Marshal(hex.EncodeToString(i.value)) } func (i *ByteArrayItem) String() string { From 5e8122bfac212ea6dc46e68d751e5450e43817f5 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Thu, 14 Nov 2019 16:09:24 +0300 Subject: [PATCH 3/5] core: add contract hash into the error output for checkStorageContext() Makes debugging things easier. --- pkg/core/interop_system.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/core/interop_system.go b/pkg/core/interop_system.go index 51aa4ed77..70bb85bbb 100644 --- a/pkg/core/interop_system.go +++ b/pkg/core/interop_system.go @@ -391,7 +391,7 @@ func (ic *interopContext) checkStorageContext(stc *StorageContext) error { return errors.New("no contract found") } if !contract.HasStorage() { - return errors.New("contract can't have storage") + return fmt.Errorf("contract %s can't use storage", stc.ScriptHash) } return nil } From 01082a8988cf049673e1857415f08534e879b512 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Thu, 14 Nov 2019 16:09:57 +0300 Subject: [PATCH 4/5] core: fix nondeterministic txGetReferences() behavior Ranging over map is nondeterministic and contracts may be unprepared for that. Fixes #454. --- pkg/core/interop_neo.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/core/interop_neo.go b/pkg/core/interop_neo.go index e1211cdfb..39623d6d0 100644 --- a/pkg/core/interop_neo.go +++ b/pkg/core/interop_neo.go @@ -144,8 +144,8 @@ func (ic *interopContext) txGetReferences(v *vm.VM) error { } stackrefs := make([]vm.StackItem, 0, len(refs)) - for k, v := range refs { - tio := txInOut{k, *v} + for _, k := range tx.Inputs { + tio := txInOut{*k, *refs[*k]} stackrefs = append(stackrefs, vm.NewInteropItem(tio)) } v.Estack().PushVal(stackrefs) From a16c2c382590bc0630452bee3959d75120b757ad Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Fri, 15 Nov 2019 17:52:47 +0300 Subject: [PATCH 5/5] core: add NEP5 transfer tracking stub See #498. --- pkg/core/blockchain.go | 39 ++++++++++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 87c6af601..f8126fe87 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -4,6 +4,7 @@ import ( "bytes" "fmt" "math" + "math/big" "sort" "sync/atomic" "time" @@ -468,15 +469,39 @@ func (bc *Blockchain) storeBlock(block *Block) error { case *transaction.InvocationTX: systemInterop := newInteropContext(0x10, bc, tmpStore, block, tx) - vm := bc.spawnVMWithInterops(systemInterop) - vm.SetCheckedHash(tx.VerificationHash().Bytes()) - vm.LoadScript(t.Script) - err := vm.Run() - if !vm.HasFailed() { + v := bc.spawnVMWithInterops(systemInterop) + v.SetCheckedHash(tx.VerificationHash().Bytes()) + v.LoadScript(t.Script) + err := v.Run() + if !v.HasFailed() { _, err = systemInterop.mem.Persist() if err != nil { return errors.Wrap(err, "failed to persist invocation results") } + for _, note := range systemInterop.notifications { + arr, ok := note.Item.Value().([]vm.StackItem) + if !ok || len(arr) != 4 { + continue + } + op, ok := arr[0].Value().([]byte) + if !ok || string(op) != "transfer" { + continue + } + from, ok := arr[1].Value().([]byte) + if !ok { + continue + } + to, ok := arr[2].Value().([]byte) + if !ok { + continue + } + amount, ok := arr[3].Value().(*big.Int) + if !ok { + continue + } + // TODO: #498 + _, _, _, _ = op, from, to, amount + } } else { log.WithFields(log.Fields{ "tx": tx.Hash().ReverseString(), @@ -487,9 +512,9 @@ func (bc *Blockchain) storeBlock(block *Block) error { aer := &AppExecResult{ TxHash: tx.Hash(), Trigger: 0x10, - VMState: vm.State(), + VMState: v.State(), GasConsumed: util.Fixed8(0), - Stack: vm.Stack("estack"), + Stack: v.Stack("estack"), Events: systemInterop.notifications, } err = putAppExecResultIntoStore(tmpStore, aer)