core: move storage-related interop code into the storage package

This commit is contained in:
Roman Khimov 2022-06-08 19:31:49 +03:00
parent f0d7a1da2a
commit 2086bca303
7 changed files with 364 additions and 355 deletions

View file

@ -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
}

View file

@ -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()
}
})
}
})
}
}

View file

@ -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
}

View file

@ -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))
})
}
}

View file

@ -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)

View file

@ -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
}

View file

@ -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},
}