core: move storage-related interop code into the storage package
This commit is contained in:
parent
f0d7a1da2a
commit
2086bca303
7 changed files with 364 additions and 355 deletions
139
pkg/core/interop/storage/basic.go
Normal file
139
pkg/core/interop/storage/basic.go
Normal 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
|
||||
}
|
113
pkg/core/interop/storage/bench_test.go
Normal file
113
pkg/core/interop/storage/bench_test.go
Normal 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()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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)
|
|
@ -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
|
||||
}
|
|
@ -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},
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue