diff --git a/cli/server/dump.go b/cli/server/dump.go index ff662f2a8..bee34cec8 100644 --- a/cli/server/dump.go +++ b/cli/server/dump.go @@ -1,7 +1,6 @@ package server import ( - "encoding/base64" "encoding/json" "fmt" "io/ioutil" @@ -14,58 +13,9 @@ import ( type dump []blockDump type blockDump struct { - Block uint32 `json:"block"` - Size int `json:"size"` - Storage []storageOp `json:"storage"` -} - -type storageOp struct { - State string `json:"state"` - Key string `json:"key"` - Value string `json:"value,omitempty"` -} - -// batchToMap converts batch to a map so that JSON is compatible -// with https://github.com/NeoResearch/neo-storage-audit/ -func batchToMap(index uint32, batch *storage.MemBatch) blockDump { - size := len(batch.Put) + len(batch.Deleted) - ops := make([]storageOp, 0, size) - for i := range batch.Put { - key := batch.Put[i].Key - if len(key) == 0 || key[0] != byte(storage.STStorage) && key[0] != byte(storage.STTempStorage) { - continue - } - - op := "Added" - if batch.Put[i].Exists { - op = "Changed" - } - - ops = append(ops, storageOp{ - State: op, - Key: base64.StdEncoding.EncodeToString(key[1:]), - Value: base64.StdEncoding.EncodeToString(batch.Put[i].Value), - }) - } - - for i := range batch.Deleted { - key := batch.Deleted[i].Key - if len(key) == 0 || !batch.Deleted[i].Exists || - key[0] != byte(storage.STStorage) && key[0] != byte(storage.STTempStorage) { - continue - } - - ops = append(ops, storageOp{ - State: "Deleted", - Key: base64.StdEncoding.EncodeToString(key[1:]), - }) - } - - return blockDump{ - Block: index, - Size: len(ops), - Storage: ops, - } + Block uint32 `json:"block"` + Size int `json:"size"` + Storage []storage.Operation `json:"storage"` } func newDump() *dump { @@ -73,8 +23,12 @@ func newDump() *dump { } func (d *dump) add(index uint32, batch *storage.MemBatch) { - m := batchToMap(index, batch) - *d = append(*d, m) + ops := storage.BatchToOperations(batch) + *d = append(*d, blockDump{ + Block: index, + Size: len(ops), + Storage: ops, + }) } func (d *dump) tryPersist(prefix string, index uint32) error { diff --git a/pkg/core/storage/store.go b/pkg/core/storage/store.go index 65acfadbb..9e2772eca 100644 --- a/pkg/core/storage/store.go +++ b/pkg/core/storage/store.go @@ -45,6 +45,15 @@ const ( MaxStorageValueLen = 65535 ) +// Operation represents a single KV operation (add/del/change) performed +// in the DB. +type Operation struct { + // State can be Added, Changed or Deleted. + State string `json:"state"` + Key []byte `json:"key"` + Value []byte `json:"value,omitempty"` +} + // SeekRange represents options for Store.Seek operation. type SeekRange struct { // Prefix denotes the Seek's lookup key. @@ -139,3 +148,40 @@ func NewStore(cfg DBConfiguration) (Store, error) { } return store, err } + +// BatchToOperations converts a batch of changes into array of Operations. +func BatchToOperations(batch *MemBatch) []Operation { + size := len(batch.Put) + len(batch.Deleted) + ops := make([]Operation, 0, size) + for i := range batch.Put { + key := batch.Put[i].Key + if len(key) == 0 || key[0] != byte(STStorage) && key[0] != byte(STTempStorage) { + continue + } + + op := "Added" + if batch.Put[i].Exists { + op = "Changed" + } + + ops = append(ops, Operation{ + State: op, + Key: key[1:], + Value: batch.Put[i].Value, + }) + } + + for i := range batch.Deleted { + key := batch.Deleted[i].Key + if len(key) == 0 || !batch.Deleted[i].Exists || + key[0] != byte(STStorage) && key[0] != byte(STTempStorage) { + continue + } + + ops = append(ops, Operation{ + State: "Deleted", + Key: key[1:], + }) + } + return ops +} diff --git a/pkg/core/storage/store_test.go b/pkg/core/storage/store_test.go index 97794f6fb..220ad4a55 100644 --- a/pkg/core/storage/store_test.go +++ b/pkg/core/storage/store_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) var ( @@ -43,3 +44,24 @@ func TestAppendPrefixInt(t *testing.T) { assert.Equal(t, KeyPrefix(expected[i]), KeyPrefix(prefix[0])) } } + +func TestBatchToOperations(t *testing.T) { + b := &MemBatch{ + Put: []KeyValueExists{ + {KeyValue: KeyValue{Key: []byte{byte(STStorage), 0x01}, Value: []byte{0x01}}}, + {KeyValue: KeyValue{Key: []byte{byte(STAccount), 0x02}, Value: []byte{0x02}}}, + {KeyValue: KeyValue{Key: []byte{byte(STStorage), 0x03}, Value: []byte{0x03}}, Exists: true}, + }, + Deleted: []KeyValueExists{ + {KeyValue: KeyValue{Key: []byte{byte(STStorage), 0x04}, Value: []byte{0x04}}}, + {KeyValue: KeyValue{Key: []byte{byte(STAccount), 0x05}, Value: []byte{0x05}}}, + {KeyValue: KeyValue{Key: []byte{byte(STStorage), 0x06}, Value: []byte{0x06}}, Exists: true}, + }, + } + o := []Operation{ + {State: "Added", Key: []byte{0x01}, Value: []byte{0x01}}, + {State: "Changed", Key: []byte{0x03}, Value: []byte{0x03}}, + {State: "Deleted", Key: []byte{0x06}}, + } + require.Equal(t, o, BatchToOperations(b)) +} diff --git a/pkg/rpc/response/result/invoke.go b/pkg/rpc/response/result/invoke.go index f8b5b5cc6..1d24914c2 100644 --- a/pkg/rpc/response/result/invoke.go +++ b/pkg/rpc/response/result/invoke.go @@ -4,7 +4,10 @@ import ( "encoding/json" "fmt" + "github.com/nspcc-dev/neo-go/pkg/core/interop" "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/vm" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" @@ -18,6 +21,7 @@ type Invoke struct { Script []byte Stack []stackitem.Item FaultException string + Notifications []state.NotificationEvent Transaction *transaction.Transaction Diagnostics *InvokeDiag maxIteratorResultItems int @@ -26,36 +30,46 @@ type Invoke struct { // InvokeDiag is an additional diagnostic data for invocation. type InvokeDiag struct { + Changes []storage.Operation `json:"storagechanges"` Invocations []*vm.InvocationTree `json:"invokedcontracts"` } // NewInvoke returns new Invoke structure with the given fields set. -func NewInvoke(vm *vm.VM, finalize func(), script []byte, faultException string, maxIteratorResultItems int) *Invoke { +func NewInvoke(ic *interop.Context, script []byte, faultException string, maxIteratorResultItems int) *Invoke { var diag *InvokeDiag - tree := vm.GetInvocationTree() + tree := ic.VM.GetInvocationTree() if tree != nil { - diag = &InvokeDiag{Invocations: tree.Calls} + diag = &InvokeDiag{ + Invocations: tree.Calls, + Changes: storage.BatchToOperations(ic.DAO.GetBatch()), + } + } + notifications := ic.Notifications + if notifications == nil { + notifications = make([]state.NotificationEvent, 0) } return &Invoke{ - State: vm.State().String(), - GasConsumed: vm.GasConsumed(), + State: ic.VM.State().String(), + GasConsumed: ic.VM.GasConsumed(), Script: script, - Stack: vm.Estack().ToArray(), + Stack: ic.VM.Estack().ToArray(), FaultException: faultException, + Notifications: notifications, Diagnostics: diag, maxIteratorResultItems: maxIteratorResultItems, - finalize: finalize, + finalize: ic.Finalize, } } type invokeAux struct { - State string `json:"state"` - GasConsumed int64 `json:"gasconsumed,string"` - Script []byte `json:"script"` - Stack json.RawMessage `json:"stack"` - FaultException string `json:"exception,omitempty"` - Transaction []byte `json:"tx,omitempty"` - Diagnostics *InvokeDiag `json:"diagnostics,omitempty"` + State string `json:"state"` + GasConsumed int64 `json:"gasconsumed,string"` + Script []byte `json:"script"` + Stack json.RawMessage `json:"stack"` + FaultException string `json:"exception,omitempty"` + Notifications []state.NotificationEvent `json:"notifications"` + Transaction []byte `json:"tx,omitempty"` + Diagnostics *InvokeDiag `json:"diagnostics,omitempty"` } type iteratorAux struct { @@ -133,6 +147,7 @@ func (r Invoke) MarshalJSON() ([]byte, error) { State: r.State, Stack: st, FaultException: r.FaultException, + Notifications: r.Notifications, Transaction: txbytes, Diagnostics: r.Diagnostics, }) @@ -189,6 +204,7 @@ func (r *Invoke) UnmarshalJSON(data []byte) error { r.Script = aux.Script r.State = aux.State r.FaultException = aux.FaultException + r.Notifications = aux.Notifications r.Transaction = tx r.Diagnostics = aux.Diagnostics return nil diff --git a/pkg/rpc/response/result/invoke_test.go b/pkg/rpc/response/result/invoke_test.go index 3ecc1358c..ba4387773 100644 --- a/pkg/rpc/response/result/invoke_test.go +++ b/pkg/rpc/response/result/invoke_test.go @@ -6,6 +6,7 @@ import ( "math/big" "testing" + "github.com/nspcc-dev/neo-go/pkg/core/state" "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" @@ -25,6 +26,7 @@ func TestInvoke_MarshalJSON(t *testing.T) { Script: []byte{10}, Stack: []stackitem.Item{stackitem.NewBigInteger(big.NewInt(1))}, FaultException: "", + Notifications: []state.NotificationEvent{}, Transaction: tx, } @@ -37,6 +39,7 @@ func TestInvoke_MarshalJSON(t *testing.T) { "stack":[ {"type":"Integer","value":"1"} ], + "notifications":[], "tx":"` + base64.StdEncoding.EncodeToString(tx.Bytes()) + `" }` require.JSONEq(t, expected, string(data)) diff --git a/pkg/rpc/server/server.go b/pkg/rpc/server/server.go index cd5f25763..f138d346b 100644 --- a/pkg/rpc/server/server.go +++ b/pkg/rpc/server/server.go @@ -1715,7 +1715,7 @@ func (s *Server) runScriptInVM(t trigger.Type, script []byte, contractScriptHash if err != nil { faultException = err.Error() } - return result.NewInvoke(ic.VM, ic.Finalize, script, faultException, s.config.MaxIteratorResultItems), nil + return result.NewInvoke(ic, script, faultException, s.config.MaxIteratorResultItems), nil } // submitBlock broadcasts a raw block over the NEO network. diff --git a/pkg/rpc/server/server_test.go b/pkg/rpc/server/server_test.go index 0b69169a4..cc54c2259 100644 --- a/pkg/rpc/server/server_test.go +++ b/pkg/rpc/server/server_test.go @@ -24,6 +24,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/fee" "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" "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" @@ -880,6 +881,61 @@ var rpcTestCases = map[string][]rpcTestCase{ assert.NotEqual(t, 0, res.GasConsumed) }, }, + { + name: "positive, with notifications", + params: `["` + NNSHash.StringLE() + `", "transfer", [{"type":"Hash160", "value":"0x0bcd2978634d961c24f5aea0802297ff128724d6"},{"type":"String", "value":"neo.com"},{"type":"Any", "value":null}],["0xb248508f4ef7088e10c48f14d04be3272ca29eee"]]`, + result: func(e *executor) interface{} { + script := []byte{0x0b, 0x0c, 0x07, 0x6e, 0x65, 0x6f, 0x2e, 0x63, 0x6f, 0x6d, 0x0c, 0x14, 0xd6, 0x24, 0x87, 0x12, 0xff, 0x97, 0x22, 0x80, 0xa0, 0xae, 0xf5, 0x24, 0x1c, 0x96, 0x4d, 0x63, 0x78, 0x29, 0xcd, 0x0b, 0x13, 0xc0, 0x1f, 0x0c, 0x08, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x0c, 0x14, 0xdc, 0xe2, 0xd3, 0xba, 0x0e, 0xbb, 0xa9, 0xf4, 0x44, 0xac, 0xbf, 0x50, 0x08, 0x76, 0xfd, 0x7c, 0x3e, 0x2b, 0x60, 0x3a, 0x41, 0x62, 0x7d, 0x5b, 0x52} + return &result.Invoke{ + State: "HALT", + GasConsumed: 33767940, + Script: script, + Stack: []stackitem.Item{stackitem.Make(true)}, + Notifications: []state.NotificationEvent{{ + ScriptHash: NNSHash, + Name: "Transfer", + Item: stackitem.NewArray([]stackitem.Item{ + stackitem.Make([]byte{0xee, 0x9e, 0xa2, 0x2c, 0x27, 0xe3, 0x4b, 0xd0, 0x14, 0x8f, 0xc4, 0x10, 0x8e, 0x08, 0xf7, 0x4e, 0x8f, 0x50, 0x48, 0xb2}), + stackitem.Make([]byte{0xd6, 0x24, 0x87, 0x12, 0xff, 0x97, 0x22, 0x80, 0xa0, 0xae, 0xf5, 0x24, 0x1c, 0x96, 0x4d, 0x63, 0x78, 0x29, 0xcd, 0x0b}), + stackitem.Make(1), + stackitem.Make("neo.com"), + }), + }}, + } + }, + }, + { + name: "positive, with storage changes", + params: `["0xef4073a0f2b305a38ec4050e4d3d28bc40ea63f5", "transfer", [{"type":"Hash160", "value":"0xb248508f4ef7088e10c48f14d04be3272ca29eee"},{"type":"Hash160", "value":"0x0bcd2978634d961c24f5aea0802297ff128724d6"},{"type":"Integer", "value":1},{"type":"Any", "value":null}],["0xb248508f4ef7088e10c48f14d04be3272ca29eee"],true]`, + result: func(e *executor) interface{} { return &result.Invoke{} }, + check: func(t *testing.T, e *executor, inv interface{}) { + res, ok := inv.(*result.Invoke) + require.True(t, ok) + assert.NotNil(t, res.Script) + assert.Equal(t, "HALT", res.State) + assert.Equal(t, []stackitem.Item{stackitem.Make(true)}, res.Stack) + assert.NotEqual(t, 0, res.GasConsumed) + chg := []storage.Operation{{ + State: "Changed", + Key: []byte{0xfa, 0xff, 0xff, 0xff, 0xb}, + Value: []byte{0xbc, 0xf8, 0x8b, 0xa, 0x56, 0x79, 0x12}, + }, { + State: "Added", + Key: []byte{0xfb, 0xff, 0xff, 0xff, 0x14, 0xd6, 0x24, 0x87, 0x12, 0xff, 0x97, 0x22, 0x80, 0xa0, 0xae, 0xf5, 0x24, 0x1c, 0x96, 0x4d, 0x63, 0x78, 0x29, 0xcd, 0xb}, + Value: []byte{0x41, 0x3, 0x21, 0x1, 0x1, 0x21, 0x1, 0x11, 0x0}, + }, { + State: "Changed", + Key: []byte{0xfb, 0xff, 0xff, 0xff, 0x14, 0xee, 0x9e, 0xa2, 0x2c, 0x27, 0xe3, 0x4b, 0xd0, 0x14, 0x8f, 0xc4, 0x10, 0x8e, 0x8, 0xf7, 0x4e, 0x8f, 0x50, 0x48, 0xb2}, + Value: []byte{0x41, 0x3, 0x21, 0x4, 0x2f, 0xd9, 0xf5, 0x5, 0x21, 0x1, 0x11, 0x0}, + }, { + State: "Changed", + Key: []byte{0xfa, 0xff, 0xff, 0xff, 0x14, 0xee, 0x9e, 0xa2, 0x2c, 0x27, 0xe3, 0x4b, 0xd0, 0x14, 0x8f, 0xc4, 0x10, 0x8e, 0x8, 0xf7, 0x4e, 0x8f, 0x50, 0x48, 0xb2}, + Value: []byte{0x41, 0x1, 0x21, 0x5, 0x4, 0xfa, 0xb2, 0x9b, 0xd}, + }} + // Can be returned in any order. + assert.ElementsMatch(t, chg, res.Diagnostics.Changes) + }, + }, { name: "positive, verbose", params: `["` + NNSHash.StringLE() + `", "resolve", [{"type":"String", "value":"neo.com"},{"type":"Integer","value":1}], [], true]`, @@ -888,11 +944,13 @@ var rpcTestCases = map[string][]rpcTestCase{ stdHash, _ := e.chain.GetNativeContractScriptHash(nativenames.StdLib) cryptoHash, _ := e.chain.GetNativeContractScriptHash(nativenames.CryptoLib) return &result.Invoke{ - State: "HALT", - GasConsumed: 17958510, - Script: script, - Stack: []stackitem.Item{stackitem.Make("1.2.3.4")}, + State: "HALT", + GasConsumed: 17958510, + Script: script, + Stack: []stackitem.Item{stackitem.Make("1.2.3.4")}, + Notifications: []state.NotificationEvent{}, Diagnostics: &result.InvokeDiag{ + Changes: []storage.Operation{}, Invocations: []*vm.InvocationTree{{ Current: hash.Hash160(script), Calls: []*vm.InvocationTree{ @@ -967,7 +1025,9 @@ var rpcTestCases = map[string][]rpcTestCase{ Script: script, Stack: []stackitem.Item{}, FaultException: "at instruction 0 (ROT): too big index", + Notifications: []state.NotificationEvent{}, Diagnostics: &result.InvokeDiag{ + Changes: []storage.Operation{}, Invocations: []*vm.InvocationTree{{ Current: hash.Hash160(script), }},