From 7fc0c04dba00ee38c0b85644095e88b446b3eae7 Mon Sep 17 00:00:00 2001 From: Evgeniy Stratonikov Date: Tue, 12 Jan 2021 13:39:31 +0300 Subject: [PATCH] core: add flags to `Storage.Find` It can be iterated over keys, values or both. Prefix can be stripped. --- examples/iterator/iterator.go | 2 +- examples/storage/storage.go | 2 +- pkg/compiler/syscall_test.go | 9 ++++ pkg/core/interop/storage/find.go | 51 +++++++++++++++++++++++ pkg/core/interop_neo.go | 19 +++++++-- pkg/core/interop_neo_test.go | 70 ++++++++++++++++++++++++-------- pkg/interop/iterator/iterator.go | 1 + pkg/interop/storage/storage.go | 18 +++++++- 8 files changed, 148 insertions(+), 24 deletions(-) create mode 100644 pkg/core/interop/storage/find.go diff --git a/examples/iterator/iterator.go b/examples/iterator/iterator.go index 5f91204a0..03ffccee9 100644 --- a/examples/iterator/iterator.go +++ b/examples/iterator/iterator.go @@ -8,7 +8,7 @@ import ( // NotifyKeysAndValues sends notification with `foo` storage keys and values func NotifyKeysAndValues() bool { - iter := storage.Find(storage.GetContext(), []byte("foo")) + iter := storage.Find(storage.GetContext(), []byte("foo"), storage.None) for iterator.Next(iter) { runtime.Notify("found storage key-value pair", iterator.Value(iter)) } diff --git a/examples/storage/storage.go b/examples/storage/storage.go index 963dfa2a4..3831ff119 100644 --- a/examples/storage/storage.go +++ b/examples/storage/storage.go @@ -32,7 +32,7 @@ func Delete(key []byte) bool { // Find returns an array of key-value pairs with key that matched the passed value func Find(value []byte) []string { - iter := storage.Find(ctx, value) + iter := storage.Find(ctx, value, storage.None) result := []string{} for iterator.Next(iter) { val := iterator.Value(iter).([]string) diff --git a/pkg/compiler/syscall_test.go b/pkg/compiler/syscall_test.go index 71328df2a..9d705ca05 100644 --- a/pkg/compiler/syscall_test.go +++ b/pkg/compiler/syscall_test.go @@ -4,7 +4,9 @@ import ( "math/big" "testing" + istorage "github.com/nspcc-dev/neo-go/pkg/core/interop/storage" "github.com/nspcc-dev/neo-go/pkg/interop/contract" + "github.com/nspcc-dev/neo-go/pkg/interop/storage" "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/stretchr/testify/assert" @@ -23,6 +25,13 @@ func TestCallFlags(t *testing.T) { require.EqualValues(t, contract.NoneFlag, callflag.NoneFlag) } +func TestFindFlags(t *testing.T) { + require.EqualValues(t, storage.None, istorage.FindDefault) + require.EqualValues(t, storage.KeysOnly, istorage.FindKeysOnly) + require.EqualValues(t, storage.RemovePrefix, istorage.FindRemovePrefix) + require.EqualValues(t, storage.ValuesOnly, istorage.FindValuesOnly) +} + func TestStoragePutGet(t *testing.T) { src := ` package foo diff --git a/pkg/core/interop/storage/find.go b/pkg/core/interop/storage/find.go new file mode 100644 index 000000000..fa61d89be --- /dev/null +++ b/pkg/core/interop/storage/find.go @@ -0,0 +1,51 @@ +package storage + +import "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" + +// Storage iterator options. +const ( + FindDefault = 0 + FindKeysOnly = 1 << 0 + FindRemovePrefix = 1 << 1 + FindValuesOnly = 1 << 2 + + FindAll = FindDefault | FindKeysOnly | FindRemovePrefix | FindValuesOnly +) + +type Iterator struct { + m []stackitem.MapElement + opts int64 + index int +} + +func NewIterator(m *stackitem.Map, opts int64) *Iterator { + return &Iterator{ + m: m.Value().([]stackitem.MapElement), + opts: opts, + index: -1, + } +} + +func (s *Iterator) Next() bool { + if s.index < len(s.m) { + s.index += 1 + } + return s.index < len(s.m) +} + +func (s *Iterator) Value() stackitem.Item { + key := s.m[s.index].Key.Value().([]byte) + if s.opts&FindRemovePrefix != 0 { + key = key[1:] + } + if s.opts&FindKeysOnly != 0 { + return stackitem.NewByteArray(key) + } + if s.opts&FindValuesOnly != 0 { + return s.m[s.index].Value + } + return stackitem.NewStruct([]stackitem.Item{ + stackitem.NewByteArray(key), + s.m[s.index].Value, + }) +} diff --git a/pkg/core/interop_neo.go b/pkg/core/interop_neo.go index adf7d3f6a..f200296c8 100644 --- a/pkg/core/interop_neo.go +++ b/pkg/core/interop_neo.go @@ -7,11 +7,14 @@ import ( "sort" "github.com/nspcc-dev/neo-go/pkg/core/interop" - "github.com/nspcc-dev/neo-go/pkg/vm" + "github.com/nspcc-dev/neo-go/pkg/core/interop/storage" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" ) -var errGasLimitExceeded = errors.New("gas limit exceeded") +var ( + errGasLimitExceeded = errors.New("gas limit exceeded") + errFindInvalidOptions = errors.New("invalid Find options") +) // storageFind finds stored key-value pair. func storageFind(ic *interop.Context) error { @@ -21,6 +24,14 @@ func storageFind(ic *interop.Context) error { return fmt.Errorf("%T is not a StorageContext", stcInterface) } prefix := ic.VM.Estack().Pop().Bytes() + opts := ic.VM.Estack().Pop().BigInt().Int64() + if opts&^storage.FindAll != 0 { + return fmt.Errorf("%w: unknown flag", errFindInvalidOptions) + } + if opts&storage.FindValuesOnly != 0 && + opts&(storage.FindKeysOnly|storage.FindRemovePrefix) != 0 { + return fmt.Errorf("%w: KeysOnly conflicts with ValuesOnly", errFindInvalidOptions) + } siMap, err := ic.DAO.GetStorageItemsWithPrefix(stc.ID, prefix) if err != nil { return err @@ -35,8 +46,8 @@ func storageFind(ic *interop.Context) error { filteredMap.Value().([]stackitem.MapElement)[j].Key.Value().([]byte)) == -1 }) - item := vm.NewMapIterator(filteredMap) - ic.VM.Estack().PushVal(item) + item := storage.NewIterator(filteredMap, opts) + ic.VM.Estack().PushVal(stackitem.NewInterop(item)) return nil } diff --git a/pkg/core/interop_neo_test.go b/pkg/core/interop_neo_test.go index 8c2bdf7db..82f11502e 100644 --- a/pkg/core/interop_neo_test.go +++ b/pkg/core/interop_neo_test.go @@ -8,6 +8,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/dao" "github.com/nspcc-dev/neo-go/pkg/core/interop" "github.com/nspcc-dev/neo-go/pkg/core/interop/iterator" + istorage "github.com/nspcc-dev/neo-go/pkg/core/interop/storage" "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" @@ -58,8 +59,9 @@ func TestStorageFind(t *testing.T) { require.NoError(t, err) } - t.Run("normal invocation", func(t *testing.T) { - v.Estack().PushVal([]byte{0x01}) + testFind := func(t *testing.T, prefix byte, opts int64, expected []stackitem.Item) { + v.Estack().PushVal(opts) + v.Estack().PushVal([]byte{prefix}) v.Estack().PushVal(stackitem.NewInterop(&StorageContext{ID: id})) err := storageFind(context) @@ -68,38 +70,71 @@ func TestStorageFind(t *testing.T) { var iter *stackitem.Interop require.NotPanics(t, func() { iter = v.Estack().Pop().Interop() }) - for _, id := range []int{2, 0} { // sorted indices with mathing prefix + for i := range expected { // sorted indices with mathing prefix v.Estack().PushVal(iter) require.NoError(t, iterator.Next(context)) require.True(t, v.Estack().Pop().Bool()) v.Estack().PushVal(iter) require.NoError(t, iterator.Value(context)) - - kv, ok := v.Estack().Pop().Value().([]stackitem.Item) - require.True(t, ok) - require.Len(t, kv, 2) - require.Equal(t, skeys[id], kv[0].Value()) - require.Equal(t, items[id].Value, kv[1].Value()) + require.Equal(t, expected[i], v.Estack().Pop().Item()) } v.Estack().PushVal(iter) require.NoError(t, iterator.Next(context)) require.False(t, v.Estack().Pop().Bool()) + } + + t.Run("normal invocation", func(t *testing.T) { + testFind(t, 0x01, istorage.FindDefault, []stackitem.Item{ + stackitem.NewStruct([]stackitem.Item{ + stackitem.NewByteArray(skeys[2]), + stackitem.NewByteArray(items[2].Value), + }), + stackitem.NewStruct([]stackitem.Item{ + stackitem.NewByteArray(skeys[0]), + stackitem.NewByteArray(items[0].Value), + }), + }) + }) + + t.Run("keys only", func(t *testing.T) { + testFind(t, 0x01, istorage.FindKeysOnly, []stackitem.Item{ + stackitem.NewByteArray(skeys[2]), + stackitem.NewByteArray(skeys[0]), + }) + }) + t.Run("remove prefix", func(t *testing.T) { + testFind(t, 0x01, istorage.FindKeysOnly|istorage.FindRemovePrefix, []stackitem.Item{ + stackitem.NewByteArray(skeys[2][1:]), + stackitem.NewByteArray(skeys[0][1:]), + }) + }) + t.Run("values only", func(t *testing.T) { + testFind(t, 0x01, istorage.FindValuesOnly, []stackitem.Item{ + stackitem.NewByteArray(items[2].Value), + stackitem.NewByteArray(items[0].Value), + }) }) t.Run("normal invocation, empty result", func(t *testing.T) { - v.Estack().PushVal([]byte{0x03}) - v.Estack().PushVal(stackitem.NewInterop(&StorageContext{ID: id})) - - err := storageFind(context) - require.NoError(t, err) - - require.NoError(t, iterator.Next(context)) - require.False(t, v.Estack().Pop().Bool()) + testFind(t, 0x03, istorage.FindDefault, nil) }) + t.Run("invalid options", func(t *testing.T) { + invalid := []int64{ + istorage.FindKeysOnly | istorage.FindValuesOnly, + ^istorage.FindAll, + } + for _, opts := range invalid { + v.Estack().PushVal(opts) + v.Estack().PushVal([]byte{0x01}) + v.Estack().PushVal(stackitem.NewInterop(&StorageContext{ID: id})) + require.Error(t, storageFind(context)) + } + }) t.Run("invalid type for StorageContext", func(t *testing.T) { + v.Estack().PushVal(istorage.FindDefault) v.Estack().PushVal([]byte{0x01}) v.Estack().PushVal(stackitem.NewInterop(nil)) @@ -109,6 +144,7 @@ func TestStorageFind(t *testing.T) { t.Run("invalid id", func(t *testing.T) { invalidID := id + 1 + v.Estack().PushVal(istorage.FindDefault) v.Estack().PushVal([]byte{0x01}) v.Estack().PushVal(stackitem.NewInterop(&StorageContext{ID: invalidID})) diff --git a/pkg/interop/iterator/iterator.go b/pkg/interop/iterator/iterator.go index ff124bb84..396a42dba 100644 --- a/pkg/interop/iterator/iterator.go +++ b/pkg/interop/iterator/iterator.go @@ -28,6 +28,7 @@ func Next(it Iterator) bool { // successful Next call. This function uses `System.Iterator.Value` syscall. // For slices the result is just value. // For maps the result can be casted to a slice of 2 elements: key and value. +// For storage iterators refer to `storage.FindFlags` documentation. func Value(it Iterator) interface{} { return nil } diff --git a/pkg/interop/storage/storage.go b/pkg/interop/storage/storage.go index 8ee4ca0e2..d641e352d 100644 --- a/pkg/interop/storage/storage.go +++ b/pkg/interop/storage/storage.go @@ -14,6 +14,20 @@ import "github.com/nspcc-dev/neo-go/pkg/interop/iterator" // to Neo .net framework's StorageContext class. type Context struct{} +// FindFlags represents parameters to `Find` iterator. +type FindFlags byte + +const ( + // None is default option. Iterator values are key-value pairs. + None FindFlags = 0 + // KeysOnly is used for iterating over keys. + KeysOnly FindFlags = 1 << 0 + // RemovePrefix is used for stripping 1-byte prefix from keys. + RemovePrefix FindFlags = 1 << 1 + // ValuesOnly is used for iterating over values. + ValuesOnly FindFlags = 1 << 2 +) + // ConvertContextToReadOnly returns new context from the given one, but with // writing capability turned off, so that you could only invoke Get and Find // using this new Context. If Context is already read-only this function is a @@ -58,4 +72,6 @@ func Delete(ctx Context, key interface{}) {} // that match the given key (contain it as a prefix). See Put documentation on // possible key types and iterator package documentation on how to use the // returned value. This function uses `System.Storage.Find` syscall. -func Find(ctx Context, key interface{}) iterator.Iterator { return iterator.Iterator{} } +func Find(ctx Context, key interface{}, options FindFlags) iterator.Iterator { + return iterator.Iterator{} +}