rpc/storage: add storage changes to invoke* diagnostics

This commit is contained in:
Roman Khimov 2022-01-19 00:02:19 +03:00
parent d79d8324e0
commit 2e452df13a
5 changed files with 118 additions and 56 deletions

View file

@ -1,7 +1,6 @@
package server package server
import ( import (
"encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
@ -16,56 +15,7 @@ type dump []blockDump
type blockDump struct { type blockDump struct {
Block uint32 `json:"block"` Block uint32 `json:"block"`
Size int `json:"size"` Size int `json:"size"`
Storage []storageOp `json:"storage"` Storage []storage.Operation `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,
}
} }
func newDump() *dump { func newDump() *dump {
@ -73,8 +23,12 @@ func newDump() *dump {
} }
func (d *dump) add(index uint32, batch *storage.MemBatch) { func (d *dump) add(index uint32, batch *storage.MemBatch) {
m := batchToMap(index, batch) ops := storage.BatchToOperations(batch)
*d = append(*d, m) *d = append(*d, blockDump{
Block: index,
Size: len(ops),
Storage: ops,
})
} }
func (d *dump) tryPersist(prefix string, index uint32) error { func (d *dump) tryPersist(prefix string, index uint32) error {

View file

@ -45,6 +45,15 @@ const (
MaxStorageValueLen = 65535 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. // SeekRange represents options for Store.Seek operation.
type SeekRange struct { type SeekRange struct {
// Prefix denotes the Seek's lookup key. // Prefix denotes the Seek's lookup key.
@ -139,3 +148,40 @@ func NewStore(cfg DBConfiguration) (Store, error) {
} }
return store, err 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
}

View file

@ -4,6 +4,7 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
var ( var (
@ -43,3 +44,24 @@ func TestAppendPrefixInt(t *testing.T) {
assert.Equal(t, KeyPrefix(expected[i]), KeyPrefix(prefix[0])) 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))
}

View file

@ -7,6 +7,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/core/interop" "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/interop/iterator"
"github.com/nspcc-dev/neo-go/pkg/core/state" "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/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/vm" "github.com/nspcc-dev/neo-go/pkg/vm"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
@ -29,6 +30,7 @@ type Invoke struct {
// InvokeDiag is an additional diagnostic data for invocation. // InvokeDiag is an additional diagnostic data for invocation.
type InvokeDiag struct { type InvokeDiag struct {
Changes []storage.Operation `json:"storagechanges"`
Invocations []*vm.InvocationTree `json:"invokedcontracts"` Invocations []*vm.InvocationTree `json:"invokedcontracts"`
} }
@ -37,7 +39,10 @@ func NewInvoke(ic *interop.Context, script []byte, faultException string, maxIte
var diag *InvokeDiag var diag *InvokeDiag
tree := ic.VM.GetInvocationTree() tree := ic.VM.GetInvocationTree()
if tree != nil { if tree != nil {
diag = &InvokeDiag{Invocations: tree.Calls} diag = &InvokeDiag{
Invocations: tree.Calls,
Changes: storage.BatchToOperations(ic.DAO.GetBatch()),
}
} }
notifications := ic.Notifications notifications := ic.Notifications
if notifications == nil { if notifications == nil {

View file

@ -24,6 +24,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/core/fee" "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/native/nativenames"
"github.com/nspcc-dev/neo-go/pkg/core/state" "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/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/crypto/hash" "github.com/nspcc-dev/neo-go/pkg/crypto/hash"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/crypto/keys"
@ -903,6 +904,38 @@ var rpcTestCases = map[string][]rpcTestCase{
} }
}, },
}, },
{
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", name: "positive, verbose",
params: `["` + NNSHash.StringLE() + `", "resolve", [{"type":"String", "value":"neo.com"},{"type":"Integer","value":1}], [], true]`, params: `["` + NNSHash.StringLE() + `", "resolve", [{"type":"String", "value":"neo.com"},{"type":"Integer","value":1}], [], true]`,
@ -917,6 +950,7 @@ var rpcTestCases = map[string][]rpcTestCase{
Stack: []stackitem.Item{stackitem.Make("1.2.3.4")}, Stack: []stackitem.Item{stackitem.Make("1.2.3.4")},
Notifications: []state.NotificationEvent{}, Notifications: []state.NotificationEvent{},
Diagnostics: &result.InvokeDiag{ Diagnostics: &result.InvokeDiag{
Changes: []storage.Operation{},
Invocations: []*vm.InvocationTree{{ Invocations: []*vm.InvocationTree{{
Current: hash.Hash160(script), Current: hash.Hash160(script),
Calls: []*vm.InvocationTree{ Calls: []*vm.InvocationTree{
@ -993,6 +1027,7 @@ var rpcTestCases = map[string][]rpcTestCase{
FaultException: "at instruction 0 (ROT): too big index", FaultException: "at instruction 0 (ROT): too big index",
Notifications: []state.NotificationEvent{}, Notifications: []state.NotificationEvent{},
Diagnostics: &result.InvokeDiag{ Diagnostics: &result.InvokeDiag{
Changes: []storage.Operation{},
Invocations: []*vm.InvocationTree{{ Invocations: []*vm.InvocationTree{{
Current: hash.Hash160(script), Current: hash.Hash160(script),
}}, }},