From 2086bca303cfb3db1706b716a56919299786947f Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 8 Jun 2022 19:31:49 +0300 Subject: [PATCH] core: move storage-related interop code into the storage package --- pkg/core/interop/storage/basic.go | 139 ++++++++++++ pkg/core/interop/storage/bench_test.go | 113 ++++++++++ pkg/core/interop/storage/find.go | 46 ++++ .../{ => interop/storage}/interops_test.go | 28 +-- .../storage/storage_test.go} | 197 +++++------------- pkg/core/interop_system.go | 181 ---------------- pkg/core/interops.go | 15 +- 7 files changed, 364 insertions(+), 355 deletions(-) create mode 100644 pkg/core/interop/storage/basic.go create mode 100644 pkg/core/interop/storage/bench_test.go rename pkg/core/{ => interop/storage}/interops_test.go (53%) rename pkg/core/{interop_system_core_test.go => interop/storage/storage_test.go} (62%) delete mode 100644 pkg/core/interop_system.go diff --git a/pkg/core/interop/storage/basic.go b/pkg/core/interop/storage/basic.go new file mode 100644 index 000000000..dcb6771c8 --- /dev/null +++ b/pkg/core/interop/storage/basic.go @@ -0,0 +1,139 @@ +package storage + +import ( + "errors" + "fmt" + + "github.com/nspcc-dev/neo-go/pkg/core/interop" + "github.com/nspcc-dev/neo-go/pkg/core/storage" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" +) + +var ( + // ErrGasLimitExceeded is returned from interops when there is not enough + // GAS left in the execution context to complete the action. + ErrGasLimitExceeded = errors.New("gas limit exceeded") + errFindInvalidOptions = errors.New("invalid Find options") +) + +// Context contains contract ID and read/write flag, it's used as +// a context for storage manipulation functions. +type Context struct { + ID int32 + ReadOnly bool +} + +// storageDelete deletes stored key-value pair. +func Delete(ic *interop.Context) error { + stcInterface := ic.VM.Estack().Pop().Value() + stc, ok := stcInterface.(*Context) + if !ok { + return fmt.Errorf("%T is not a storage.Context", stcInterface) + } + if stc.ReadOnly { + return errors.New("storage.Context is read only") + } + key := ic.VM.Estack().Pop().Bytes() + ic.DAO.DeleteStorageItem(stc.ID, key) + return nil +} + +// Get returns stored key-value pair. +func Get(ic *interop.Context) error { + stcInterface := ic.VM.Estack().Pop().Value() + stc, ok := stcInterface.(*Context) + if !ok { + return fmt.Errorf("%T is not a storage.Context", stcInterface) + } + key := ic.VM.Estack().Pop().Bytes() + si := ic.DAO.GetStorageItem(stc.ID, key) + if si != nil { + ic.VM.Estack().PushItem(stackitem.NewByteArray([]byte(si))) + } else { + ic.VM.Estack().PushItem(stackitem.Null{}) + } + return nil +} + +// GetContext returns storage context for the currently executing contract. +func GetContext(ic *interop.Context) error { + return getContextInternal(ic, false) +} + +// GetReadOnlyContext returns read-only storage context for the currently executing contract. +func GetReadOnlyContext(ic *interop.Context) error { + return getContextInternal(ic, true) +} + +// getContextInternal is internal version of storageGetContext and +// storageGetReadOnlyContext which allows to specify ReadOnly context flag. +func getContextInternal(ic *interop.Context, isReadOnly bool) error { + contract, err := ic.GetContract(ic.VM.GetCurrentScriptHash()) + if err != nil { + return err + } + sc := &Context{ + ID: contract.ID, + ReadOnly: isReadOnly, + } + ic.VM.Estack().PushItem(stackitem.NewInterop(sc)) + return nil +} + +func putWithContext(ic *interop.Context, stc *Context, key []byte, value []byte) error { + if len(key) > storage.MaxStorageKeyLen { + return errors.New("key is too big") + } + if len(value) > storage.MaxStorageValueLen { + return errors.New("value is too big") + } + if stc.ReadOnly { + return errors.New("storage.Context is read only") + } + si := ic.DAO.GetStorageItem(stc.ID, key) + sizeInc := len(value) + if si == nil { + sizeInc = len(key) + len(value) + } else if len(value) != 0 { + if len(value) <= len(si) { + sizeInc = (len(value)-1)/4 + 1 + } else if len(si) != 0 { + sizeInc = (len(si)-1)/4 + 1 + len(value) - len(si) + } + } + if !ic.VM.AddGas(int64(sizeInc) * ic.BaseStorageFee()) { + return ErrGasLimitExceeded + } + ic.DAO.PutStorageItem(stc.ID, key, value) + return nil +} + +// Put puts key-value pair into the storage. +func Put(ic *interop.Context) error { + stcInterface := ic.VM.Estack().Pop().Value() + stc, ok := stcInterface.(*Context) + if !ok { + return fmt.Errorf("%T is not a storage.Context", stcInterface) + } + key := ic.VM.Estack().Pop().Bytes() + value := ic.VM.Estack().Pop().Bytes() + return putWithContext(ic, stc, key, value) +} + +// ContextAsReadOnly sets given context to read-only mode. +func ContextAsReadOnly(ic *interop.Context) error { + stcInterface := ic.VM.Estack().Pop().Value() + stc, ok := stcInterface.(*Context) + if !ok { + return fmt.Errorf("%T is not a storage.Context", stcInterface) + } + if !stc.ReadOnly { + stx := &Context{ + ID: stc.ID, + ReadOnly: true, + } + stc = stx + } + ic.VM.Estack().PushItem(stackitem.NewInterop(stc)) + return nil +} diff --git a/pkg/core/interop/storage/bench_test.go b/pkg/core/interop/storage/bench_test.go new file mode 100644 index 000000000..ee64de6b4 --- /dev/null +++ b/pkg/core/interop/storage/bench_test.go @@ -0,0 +1,113 @@ +package storage_test + +import ( + "fmt" + "testing" + + "github.com/nspcc-dev/neo-go/internal/random" + "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/native" + "github.com/nspcc-dev/neo-go/pkg/core/state" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" + "github.com/stretchr/testify/require" +) + +func BenchmarkStorageFind(b *testing.B) { + for count := 10; count <= 10000; count *= 10 { + b.Run(fmt.Sprintf("%dElements", count), func(b *testing.B) { + v, contractState, context, _ := createVMAndContractState(b) + require.NoError(b, native.PutContractState(context.DAO, contractState)) + + items := make(map[string]state.StorageItem) + for i := 0; i < count; i++ { + items["abc"+random.String(10)] = random.Bytes(10) + } + for k, v := range items { + context.DAO.PutStorageItem(contractState.ID, []byte(k), v) + context.DAO.PutStorageItem(contractState.ID+1, []byte(k), v) + } + changes, err := context.DAO.Persist() + require.NoError(b, err) + require.NotEqual(b, 0, changes) + + b.ResetTimer() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + b.StopTimer() + v.Estack().PushVal(istorage.FindDefault) + v.Estack().PushVal("abc") + v.Estack().PushVal(stackitem.NewInterop(&istorage.Context{ID: contractState.ID})) + b.StartTimer() + err := istorage.Find(context) + if err != nil { + b.FailNow() + } + b.StopTimer() + context.Finalize() + } + }) + } +} + +func BenchmarkStorageFindIteratorNext(b *testing.B) { + for count := 10; count <= 10000; count *= 10 { + cases := map[string]int{ + "Pick1": 1, + "PickHalf": count / 2, + "PickAll": count, + } + b.Run(fmt.Sprintf("%dElements", count), func(b *testing.B) { + for name, last := range cases { + b.Run(name, func(b *testing.B) { + v, contractState, context, _ := createVMAndContractState(b) + require.NoError(b, native.PutContractState(context.DAO, contractState)) + + items := make(map[string]state.StorageItem) + for i := 0; i < count; i++ { + items["abc"+random.String(10)] = random.Bytes(10) + } + for k, v := range items { + context.DAO.PutStorageItem(contractState.ID, []byte(k), v) + context.DAO.PutStorageItem(contractState.ID+1, []byte(k), v) + } + changes, err := context.DAO.Persist() + require.NoError(b, err) + require.NotEqual(b, 0, changes) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + b.StopTimer() + v.Estack().PushVal(istorage.FindDefault) + v.Estack().PushVal("abc") + v.Estack().PushVal(stackitem.NewInterop(&istorage.Context{ID: contractState.ID})) + b.StartTimer() + err := istorage.Find(context) + b.StopTimer() + if err != nil { + b.FailNow() + } + res := context.VM.Estack().Pop().Item() + for i := 0; i < last; i++ { + context.VM.Estack().PushVal(res) + b.StartTimer() + require.NoError(b, iterator.Next(context)) + b.StopTimer() + require.True(b, context.VM.Estack().Pop().Bool()) + } + + context.VM.Estack().PushVal(res) + require.NoError(b, iterator.Next(context)) + actual := context.VM.Estack().Pop().Bool() + if last == count { + require.False(b, actual) + } else { + require.True(b, actual) + } + context.Finalize() + } + }) + } + }) + } +} diff --git a/pkg/core/interop/storage/find.go b/pkg/core/interop/storage/find.go index f86ca96f3..62a59b688 100644 --- a/pkg/core/interop/storage/find.go +++ b/pkg/core/interop/storage/find.go @@ -1,6 +1,10 @@ package storage import ( + "context" + "fmt" + + "github.com/nspcc-dev/neo-go/pkg/core/interop" "github.com/nspcc-dev/neo-go/pkg/core/storage" "github.com/nspcc-dev/neo-go/pkg/util/slice" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" @@ -80,3 +84,45 @@ func (s *Iterator) Value() stackitem.Item { value, }) } + +// Find finds stored key-value pair. +func Find(ic *interop.Context) error { + stcInterface := ic.VM.Estack().Pop().Value() + stc, ok := stcInterface.(*Context) + if !ok { + return fmt.Errorf("%T is not a storage,Context", stcInterface) + } + prefix := ic.VM.Estack().Pop().Bytes() + opts := ic.VM.Estack().Pop().BigInt().Int64() + if opts&^FindAll != 0 { + return fmt.Errorf("%w: unknown flag", errFindInvalidOptions) + } + if opts&FindKeysOnly != 0 && + opts&(FindDeserialize|FindPick0|FindPick1) != 0 { + return fmt.Errorf("%w KeysOnly conflicts with other options", errFindInvalidOptions) + } + if opts&FindValuesOnly != 0 && + opts&(FindKeysOnly|FindRemovePrefix) != 0 { + return fmt.Errorf("%w: KeysOnly conflicts with ValuesOnly", errFindInvalidOptions) + } + if opts&FindPick0 != 0 && opts&FindPick1 != 0 { + return fmt.Errorf("%w: Pick0 conflicts with Pick1", errFindInvalidOptions) + } + if opts&FindDeserialize == 0 && (opts&FindPick0 != 0 || opts&FindPick1 != 0) { + return fmt.Errorf("%w: PickN is specified without Deserialize", errFindInvalidOptions) + } + ctx, cancel := context.WithCancel(context.Background()) + seekres := ic.DAO.SeekAsync(ctx, stc.ID, storage.SeekRange{Prefix: prefix}) + item := NewIterator(seekres, prefix, opts) + ic.VM.Estack().PushItem(stackitem.NewInterop(item)) + ic.RegisterCancelFunc(func() { + cancel() + // Underlying persistent store is likely to be a private MemCachedStore. Thus, + // to avoid concurrent map iteration and map write we need to wait until internal + // seek goroutine is finished, because it can access underlying persistent store. + for range seekres { + } + }) + + return nil +} diff --git a/pkg/core/interops_test.go b/pkg/core/interop/storage/interops_test.go similarity index 53% rename from pkg/core/interops_test.go rename to pkg/core/interop/storage/interops_test.go index fb31c0e25..74795fc21 100644 --- a/pkg/core/interops_test.go +++ b/pkg/core/interop/storage/interops_test.go @@ -1,4 +1,4 @@ -package core +package storage_test import ( "reflect" @@ -6,20 +6,10 @@ import ( "testing" "github.com/nspcc-dev/neo-go/pkg/core/interop" - "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" - "github.com/nspcc-dev/neo-go/pkg/vm" + "github.com/nspcc-dev/neo-go/pkg/core/interop/storage" "github.com/stretchr/testify/require" ) -func testNonInterop(t *testing.T, value interface{}, f func(*interop.Context) error) { - v := vm.New() - v.Estack().PushVal(value) - chain := newTestChain(t) - context := chain.newInteropContext(trigger.Application, chain.dao, nil, nil) - context.VM = v - require.Error(t, f(context)) -} - func TestUnexpectedNonInterops(t *testing.T) { vals := map[string]interface{}{ "int": 1, @@ -30,17 +20,19 @@ func TestUnexpectedNonInterops(t *testing.T) { // All of these functions expect an interop item on the stack. funcs := []func(*interop.Context) error{ - storageContextAsReadOnly, - storageDelete, - storageFind, - storageGet, - storagePut, + storage.ContextAsReadOnly, + storage.Delete, + storage.Find, + storage.Get, + storage.Put, } for _, f := range funcs { for k, v := range vals { fname := runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name() t.Run(k+"/"+fname, func(t *testing.T) { - testNonInterop(t, v, f) + vm, ic, _ := createVM(t) + vm.Estack().PushVal(v) + require.Error(t, f(ic)) }) } } diff --git a/pkg/core/interop_system_core_test.go b/pkg/core/interop/storage/storage_test.go similarity index 62% rename from pkg/core/interop_system_core_test.go rename to pkg/core/interop/storage/storage_test.go index 2c7496c10..7cfa53a74 100644 --- a/pkg/core/interop_system_core_test.go +++ b/pkg/core/interop/storage/storage_test.go @@ -1,20 +1,21 @@ -package core +package storage_test import ( "errors" - "fmt" "math/big" - "path/filepath" "testing" - "github.com/nspcc-dev/neo-go/internal/random" + "github.com/nspcc-dev/neo-go/pkg/core" + "github.com/nspcc-dev/neo-go/pkg/core/block" "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/native" "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/neotest/chain" "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" "github.com/nspcc-dev/neo-go/pkg/smartcontract/nef" @@ -24,9 +25,7 @@ import ( "github.com/stretchr/testify/require" ) -var pathToInternalContracts = filepath.Join("..", "..", "internal", "contracts") - -func TestStoragePut(t *testing.T) { +func TestPut(t *testing.T) { _, cs, ic, _ := createVMAndContractState(t) require.NoError(t, native.PutContractState(ic.DAO, cs)) @@ -37,53 +36,53 @@ func TestStoragePut(t *testing.T) { v.GasLimit = gas v.Estack().PushVal(value) v.Estack().PushVal(key) - require.NoError(t, storageGetContext(ic)) + require.NoError(t, istorage.GetContext(ic)) } t.Run("create, not enough gas", func(t *testing.T) { initVM(t, []byte{1}, []byte{2, 3}, 2*native.DefaultStoragePrice) - err := storagePut(ic) - require.True(t, errors.Is(err, errGasLimitExceeded), "got: %v", err) + err := istorage.Put(ic) + require.True(t, errors.Is(err, istorage.ErrGasLimitExceeded), "got: %v", err) }) initVM(t, []byte{4}, []byte{5, 6}, 3*native.DefaultStoragePrice) - require.NoError(t, storagePut(ic)) + require.NoError(t, istorage.Put(ic)) t.Run("update", func(t *testing.T) { t.Run("not enough gas", func(t *testing.T) { initVM(t, []byte{4}, []byte{5, 6, 7, 8}, native.DefaultStoragePrice) - err := storagePut(ic) - require.True(t, errors.Is(err, errGasLimitExceeded), "got: %v", err) + err := istorage.Put(ic) + require.True(t, errors.Is(err, istorage.ErrGasLimitExceeded), "got: %v", err) }) initVM(t, []byte{4}, []byte{5, 6, 7, 8}, 3*native.DefaultStoragePrice) - require.NoError(t, storagePut(ic)) + require.NoError(t, istorage.Put(ic)) initVM(t, []byte{4}, []byte{5, 6}, native.DefaultStoragePrice) - require.NoError(t, storagePut(ic)) + require.NoError(t, istorage.Put(ic)) }) t.Run("check limits", func(t *testing.T) { initVM(t, make([]byte, storage.MaxStorageKeyLen), make([]byte, storage.MaxStorageValueLen), -1) - require.NoError(t, storagePut(ic)) + require.NoError(t, istorage.Put(ic)) }) t.Run("bad", func(t *testing.T) { t.Run("readonly context", func(t *testing.T) { initVM(t, []byte{1}, []byte{1}, -1) - require.NoError(t, storageContextAsReadOnly(ic)) - require.Error(t, storagePut(ic)) + require.NoError(t, istorage.ContextAsReadOnly(ic)) + require.Error(t, istorage.Put(ic)) }) t.Run("big key", func(t *testing.T) { initVM(t, make([]byte, storage.MaxStorageKeyLen+1), []byte{1}, -1) - require.Error(t, storagePut(ic)) + require.Error(t, istorage.Put(ic)) }) t.Run("big value", func(t *testing.T) { initVM(t, []byte{1}, make([]byte, storage.MaxStorageValueLen+1), -1) - require.Error(t, storagePut(ic)) + require.Error(t, istorage.Put(ic)) }) }) } -func TestStorageDelete(t *testing.T) { +func TestDelete(t *testing.T) { v, cs, ic, _ := createVMAndContractState(t) require.NoError(t, native.PutContractState(ic.DAO, cs)) @@ -91,8 +90,8 @@ func TestStorageDelete(t *testing.T) { put := func(key, value string, flag int) { v.Estack().PushVal(value) v.Estack().PushVal(key) - require.NoError(t, storageGetContext(ic)) - require.NoError(t, storagePut(ic)) + require.NoError(t, istorage.GetContext(ic)) + require.NoError(t, istorage.Put(ic)) } put("key1", "value1", 0) put("key2", "value2", 0) @@ -100,123 +99,24 @@ func TestStorageDelete(t *testing.T) { t.Run("good", func(t *testing.T) { v.Estack().PushVal("key1") - require.NoError(t, storageGetContext(ic)) - require.NoError(t, storageDelete(ic)) + require.NoError(t, istorage.GetContext(ic)) + require.NoError(t, istorage.Delete(ic)) }) t.Run("readonly context", func(t *testing.T) { v.Estack().PushVal("key2") - require.NoError(t, storageGetReadOnlyContext(ic)) - require.Error(t, storageDelete(ic)) + require.NoError(t, istorage.GetReadOnlyContext(ic)) + require.Error(t, istorage.Delete(ic)) }) t.Run("readonly context (from normal)", func(t *testing.T) { v.Estack().PushVal("key3") - require.NoError(t, storageGetContext(ic)) - require.NoError(t, storageContextAsReadOnly(ic)) - require.Error(t, storageDelete(ic)) + require.NoError(t, istorage.GetContext(ic)) + require.NoError(t, istorage.ContextAsReadOnly(ic)) + require.Error(t, istorage.Delete(ic)) }) } -func BenchmarkStorageFind(b *testing.B) { - for count := 10; count <= 10000; count *= 10 { - b.Run(fmt.Sprintf("%dElements", count), func(b *testing.B) { - v, contractState, context, chain := createVMAndContractState(b) - require.NoError(b, native.PutContractState(chain.dao, contractState)) - - items := make(map[string]state.StorageItem) - for i := 0; i < count; i++ { - items["abc"+random.String(10)] = random.Bytes(10) - } - for k, v := range items { - context.DAO.PutStorageItem(contractState.ID, []byte(k), v) - context.DAO.PutStorageItem(contractState.ID+1, []byte(k), v) - } - changes, err := context.DAO.Persist() - require.NoError(b, err) - require.NotEqual(b, 0, changes) - - b.ResetTimer() - b.ReportAllocs() - for i := 0; i < b.N; i++ { - b.StopTimer() - v.Estack().PushVal(istorage.FindDefault) - v.Estack().PushVal("abc") - v.Estack().PushVal(stackitem.NewInterop(&StorageContext{ID: contractState.ID})) - b.StartTimer() - err := storageFind(context) - if err != nil { - b.FailNow() - } - b.StopTimer() - context.Finalize() - } - }) - } -} - -func BenchmarkStorageFindIteratorNext(b *testing.B) { - for count := 10; count <= 10000; count *= 10 { - cases := map[string]int{ - "Pick1": 1, - "PickHalf": count / 2, - "PickAll": count, - } - b.Run(fmt.Sprintf("%dElements", count), func(b *testing.B) { - for name, last := range cases { - b.Run(name, func(b *testing.B) { - v, contractState, context, chain := createVMAndContractState(b) - require.NoError(b, native.PutContractState(chain.dao, contractState)) - - items := make(map[string]state.StorageItem) - for i := 0; i < count; i++ { - items["abc"+random.String(10)] = random.Bytes(10) - } - for k, v := range items { - context.DAO.PutStorageItem(contractState.ID, []byte(k), v) - context.DAO.PutStorageItem(contractState.ID+1, []byte(k), v) - } - changes, err := context.DAO.Persist() - require.NoError(b, err) - require.NotEqual(b, 0, changes) - b.ReportAllocs() - b.ResetTimer() - for i := 0; i < b.N; i++ { - b.StopTimer() - v.Estack().PushVal(istorage.FindDefault) - v.Estack().PushVal("abc") - v.Estack().PushVal(stackitem.NewInterop(&StorageContext{ID: contractState.ID})) - b.StartTimer() - err := storageFind(context) - b.StopTimer() - if err != nil { - b.FailNow() - } - res := context.VM.Estack().Pop().Item() - for i := 0; i < last; i++ { - context.VM.Estack().PushVal(res) - b.StartTimer() - require.NoError(b, iterator.Next(context)) - b.StopTimer() - require.True(b, context.VM.Estack().Pop().Bool()) - } - - context.VM.Estack().PushVal(res) - require.NoError(b, iterator.Next(context)) - actual := context.VM.Estack().Pop().Bool() - if last == count { - require.False(b, actual) - } else { - require.True(b, actual) - } - context.Finalize() - } - }) - } - }) - } -} - -func TestStorageFind(t *testing.T) { - v, contractState, context, chain := createVMAndContractState(t) +func TestFind(t *testing.T) { + v, contractState, context, _ := createVMAndContractState(t) arr := []stackitem.Item{ stackitem.NewBigInteger(big.NewInt(42)), @@ -247,7 +147,7 @@ func TestStorageFind(t *testing.T) { []byte{222}, } - require.NoError(t, native.PutContractState(chain.dao, contractState)) + require.NoError(t, native.PutContractState(context.DAO, contractState)) id := contractState.ID @@ -258,9 +158,9 @@ func TestStorageFind(t *testing.T) { testFind := func(t *testing.T, prefix []byte, opts int64, expected []stackitem.Item) { v.Estack().PushVal(opts) v.Estack().PushVal(prefix) - v.Estack().PushVal(stackitem.NewInterop(&StorageContext{ID: id})) + v.Estack().PushVal(stackitem.NewInterop(&istorage.Context{ID: id})) - err := storageFind(context) + err := istorage.Find(context) require.NoError(t, err) var iter *stackitem.Interop @@ -327,8 +227,8 @@ func TestStorageFind(t *testing.T) { t.Run("invalid", func(t *testing.T) { v.Estack().PushVal(istorage.FindDeserialize) v.Estack().PushVal([]byte{0x05}) - v.Estack().PushVal(stackitem.NewInterop(&StorageContext{ID: id})) - err := storageFind(context) + v.Estack().PushVal(stackitem.NewInterop(&istorage.Context{ID: id})) + err := istorage.Find(context) require.NoError(t, err) var iter *stackitem.Interop @@ -371,16 +271,16 @@ func TestStorageFind(t *testing.T) { 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)) + v.Estack().PushVal(stackitem.NewInterop(&istorage.Context{ID: id})) + require.Error(t, istorage.Find(context)) } }) - t.Run("invalid type for StorageContext", func(t *testing.T) { + t.Run("invalid type for storage.Context", func(t *testing.T) { v.Estack().PushVal(istorage.FindDefault) v.Estack().PushVal([]byte{0x01}) v.Estack().PushVal(stackitem.NewInterop(nil)) - require.Error(t, storageFind(context)) + require.Error(t, istorage.Find(context)) }) t.Run("invalid id", func(t *testing.T) { @@ -388,9 +288,9 @@ func TestStorageFind(t *testing.T) { v.Estack().PushVal(istorage.FindDefault) v.Estack().PushVal([]byte{0x01}) - v.Estack().PushVal(stackitem.NewInterop(&StorageContext{ID: invalidID})) + v.Estack().PushVal(stackitem.NewInterop(&istorage.Context{ID: invalidID})) - require.NoError(t, storageFind(context)) + require.NoError(t, istorage.Find(context)) require.NoError(t, iterator.Next(context)) require.False(t, v.Estack().Pop().Bool()) }) @@ -398,15 +298,14 @@ func TestStorageFind(t *testing.T) { // Helper functions to create VM, InteropContext, TX, Account, Contract. -func createVM(t testing.TB) (*vm.VM, *interop.Context, *Blockchain) { - chain := newTestChain(t) - context := chain.newInteropContext(trigger.Application, - chain.dao.GetWrapped(), nil, nil) - v := context.SpawnVM() - return v, context, chain +func createVM(t testing.TB) (*vm.VM, *interop.Context, *core.Blockchain) { + chain, _ := chain.NewSingle(t) + ic := chain.GetTestVM(trigger.Application, &transaction.Transaction{}, &block.Block{}) + v := ic.SpawnVM() + return v, ic, chain } -func createVMAndContractState(t testing.TB) (*vm.VM, *state.Contract, *interop.Context, *Blockchain) { +func createVMAndContractState(t testing.TB) (*vm.VM, *state.Contract, *interop.Context, *core.Blockchain) { script := []byte("testscript") m := manifest.NewManifest("Test") ne, err := nef.NewFile(script) diff --git a/pkg/core/interop_system.go b/pkg/core/interop_system.go deleted file mode 100644 index b483fb16b..000000000 --- a/pkg/core/interop_system.go +++ /dev/null @@ -1,181 +0,0 @@ -package core - -import ( - "context" - "errors" - "fmt" - - "github.com/nspcc-dev/neo-go/pkg/core/interop" - istorage "github.com/nspcc-dev/neo-go/pkg/core/interop/storage" - "github.com/nspcc-dev/neo-go/pkg/core/storage" - "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" -) - -var ( - errGasLimitExceeded = errors.New("gas limit exceeded") - errFindInvalidOptions = errors.New("invalid Find options") -) - -// StorageContext contains storing id and read/write flag, it's used as -// a context for storage manipulation functions. -type StorageContext struct { - ID int32 - ReadOnly bool -} - -// storageDelete deletes stored key-value pair. -func storageDelete(ic *interop.Context) error { - stcInterface := ic.VM.Estack().Pop().Value() - stc, ok := stcInterface.(*StorageContext) - if !ok { - return fmt.Errorf("%T is not a StorageContext", stcInterface) - } - if stc.ReadOnly { - return errors.New("StorageContext is read only") - } - key := ic.VM.Estack().Pop().Bytes() - ic.DAO.DeleteStorageItem(stc.ID, key) - return nil -} - -// storageGet returns stored key-value pair. -func storageGet(ic *interop.Context) error { - stcInterface := ic.VM.Estack().Pop().Value() - stc, ok := stcInterface.(*StorageContext) - if !ok { - return fmt.Errorf("%T is not a StorageContext", stcInterface) - } - key := ic.VM.Estack().Pop().Bytes() - si := ic.DAO.GetStorageItem(stc.ID, key) - if si != nil { - ic.VM.Estack().PushItem(stackitem.NewByteArray([]byte(si))) - } else { - ic.VM.Estack().PushItem(stackitem.Null{}) - } - return nil -} - -// storageGetContext returns storage context (scripthash). -func storageGetContext(ic *interop.Context) error { - return storageGetContextInternal(ic, false) -} - -// storageGetReadOnlyContext returns read-only context (scripthash). -func storageGetReadOnlyContext(ic *interop.Context) error { - return storageGetContextInternal(ic, true) -} - -// storageGetContextInternal is internal version of storageGetContext and -// storageGetReadOnlyContext which allows to specify ReadOnly context flag. -func storageGetContextInternal(ic *interop.Context, isReadOnly bool) error { - contract, err := ic.GetContract(ic.VM.GetCurrentScriptHash()) - if err != nil { - return err - } - sc := &StorageContext{ - ID: contract.ID, - ReadOnly: isReadOnly, - } - ic.VM.Estack().PushItem(stackitem.NewInterop(sc)) - return nil -} - -func putWithContext(ic *interop.Context, stc *StorageContext, key []byte, value []byte) error { - if len(key) > storage.MaxStorageKeyLen { - return errors.New("key is too big") - } - if len(value) > storage.MaxStorageValueLen { - return errors.New("value is too big") - } - if stc.ReadOnly { - return errors.New("StorageContext is read only") - } - si := ic.DAO.GetStorageItem(stc.ID, key) - sizeInc := len(value) - if si == nil { - sizeInc = len(key) + len(value) - } else if len(value) != 0 { - if len(value) <= len(si) { - sizeInc = (len(value)-1)/4 + 1 - } else if len(si) != 0 { - sizeInc = (len(si)-1)/4 + 1 + len(value) - len(si) - } - } - if !ic.VM.AddGas(int64(sizeInc) * ic.BaseStorageFee()) { - return errGasLimitExceeded - } - ic.DAO.PutStorageItem(stc.ID, key, value) - return nil -} - -// storagePut puts key-value pair into the storage. -func storagePut(ic *interop.Context) error { - stcInterface := ic.VM.Estack().Pop().Value() - stc, ok := stcInterface.(*StorageContext) - if !ok { - return fmt.Errorf("%T is not a StorageContext", stcInterface) - } - key := ic.VM.Estack().Pop().Bytes() - value := ic.VM.Estack().Pop().Bytes() - return putWithContext(ic, stc, key, value) -} - -// storageContextAsReadOnly sets given context to read-only mode. -func storageContextAsReadOnly(ic *interop.Context) error { - stcInterface := ic.VM.Estack().Pop().Value() - stc, ok := stcInterface.(*StorageContext) - if !ok { - return fmt.Errorf("%T is not a StorageContext", stcInterface) - } - if !stc.ReadOnly { - stx := &StorageContext{ - ID: stc.ID, - ReadOnly: true, - } - stc = stx - } - ic.VM.Estack().PushItem(stackitem.NewInterop(stc)) - return nil -} - -// storageFind finds stored key-value pair. -func storageFind(ic *interop.Context) error { - stcInterface := ic.VM.Estack().Pop().Value() - stc, ok := stcInterface.(*StorageContext) - if !ok { - return fmt.Errorf("%T is not a StorageContext", stcInterface) - } - prefix := ic.VM.Estack().Pop().Bytes() - opts := ic.VM.Estack().Pop().BigInt().Int64() - if opts&^istorage.FindAll != 0 { - return fmt.Errorf("%w: unknown flag", errFindInvalidOptions) - } - if opts&istorage.FindKeysOnly != 0 && - opts&(istorage.FindDeserialize|istorage.FindPick0|istorage.FindPick1) != 0 { - return fmt.Errorf("%w KeysOnly conflicts with other options", errFindInvalidOptions) - } - if opts&istorage.FindValuesOnly != 0 && - opts&(istorage.FindKeysOnly|istorage.FindRemovePrefix) != 0 { - return fmt.Errorf("%w: KeysOnly conflicts with ValuesOnly", errFindInvalidOptions) - } - if opts&istorage.FindPick0 != 0 && opts&istorage.FindPick1 != 0 { - return fmt.Errorf("%w: Pick0 conflicts with Pick1", errFindInvalidOptions) - } - if opts&istorage.FindDeserialize == 0 && (opts&istorage.FindPick0 != 0 || opts&istorage.FindPick1 != 0) { - return fmt.Errorf("%w: PickN is specified without Deserialize", errFindInvalidOptions) - } - ctx, cancel := context.WithCancel(context.Background()) - seekres := ic.DAO.SeekAsync(ctx, stc.ID, storage.SeekRange{Prefix: prefix}) - item := istorage.NewIterator(seekres, prefix, opts) - ic.VM.Estack().PushItem(stackitem.NewInterop(item)) - ic.RegisterCancelFunc(func() { - cancel() - // Underlying persistent store is likely to be a private MemCachedStore. Thus, - // to avoid concurrent map iteration and map write we need to wait until internal - // seek goroutine is finished, because it can access underlying persistent store. - for range seekres { - } - }) - - return nil -} diff --git a/pkg/core/interops.go b/pkg/core/interops.go index 50c8ee723..5612a4e20 100644 --- a/pkg/core/interops.go +++ b/pkg/core/interops.go @@ -15,6 +15,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames" "github.com/nspcc-dev/neo-go/pkg/core/interop/iterator" "github.com/nspcc-dev/neo-go/pkg/core/interop/runtime" + "github.com/nspcc-dev/neo-go/pkg/core/interop/storage" "github.com/nspcc-dev/neo-go/pkg/core/native" "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" "github.com/nspcc-dev/neo-go/pkg/vm" @@ -62,19 +63,19 @@ var systemInterops = []interop.Function{ {Name: interopnames.SystemRuntimeNotify, Func: runtime.Notify, Price: 1 << 15, RequiredFlags: callflag.AllowNotify, ParamCount: 2}, {Name: interopnames.SystemRuntimePlatform, Func: runtime.Platform, Price: 1 << 3}, - {Name: interopnames.SystemStorageDelete, Func: storageDelete, Price: 1 << 15, + {Name: interopnames.SystemStorageDelete, Func: storage.Delete, Price: 1 << 15, RequiredFlags: callflag.WriteStates, ParamCount: 2}, - {Name: interopnames.SystemStorageFind, Func: storageFind, Price: 1 << 15, RequiredFlags: callflag.ReadStates, + {Name: interopnames.SystemStorageFind, Func: storage.Find, Price: 1 << 15, RequiredFlags: callflag.ReadStates, ParamCount: 3}, - {Name: interopnames.SystemStorageGet, Func: storageGet, Price: 1 << 15, RequiredFlags: callflag.ReadStates, + {Name: interopnames.SystemStorageGet, Func: storage.Get, Price: 1 << 15, RequiredFlags: callflag.ReadStates, ParamCount: 2}, - {Name: interopnames.SystemStorageGetContext, Func: storageGetContext, Price: 1 << 4, + {Name: interopnames.SystemStorageGetContext, Func: storage.GetContext, Price: 1 << 4, RequiredFlags: callflag.ReadStates}, - {Name: interopnames.SystemStorageGetReadOnlyContext, Func: storageGetReadOnlyContext, Price: 1 << 4, + {Name: interopnames.SystemStorageGetReadOnlyContext, Func: storage.GetReadOnlyContext, Price: 1 << 4, RequiredFlags: callflag.ReadStates}, - {Name: interopnames.SystemStoragePut, Func: storagePut, Price: 1 << 15, RequiredFlags: callflag.WriteStates, + {Name: interopnames.SystemStoragePut, Func: storage.Put, Price: 1 << 15, RequiredFlags: callflag.WriteStates, ParamCount: 3}, - {Name: interopnames.SystemStorageAsReadOnly, Func: storageContextAsReadOnly, Price: 1 << 4, + {Name: interopnames.SystemStorageAsReadOnly, Func: storage.ContextAsReadOnly, Price: 1 << 4, RequiredFlags: callflag.ReadStates, ParamCount: 1}, }