diff --git a/lib/runtime/interface.go b/lib/runtime/interface.go index 1448505524..399c17f67a 100644 --- a/lib/runtime/interface.go +++ b/lib/runtime/interface.go @@ -19,6 +19,7 @@ package runtime import ( "github.com/ChainSafe/gossamer/dot/types" "github.com/ChainSafe/gossamer/lib/common" + "github.com/ChainSafe/gossamer/lib/common/optional" "github.com/ChainSafe/gossamer/lib/keystore" "github.com/ChainSafe/gossamer/lib/transaction" "github.com/ChainSafe/gossamer/lib/trie" @@ -66,6 +67,7 @@ type Storage interface { GetChildStorage(keyToChild, key []byte) ([]byte, error) Delete(key []byte) DeleteChild(keyToChild []byte) + DeleteChildLimit(keyToChild []byte, limit *optional.Bytes) (uint32, bool, error) ClearChildStorage(keyToChild, key []byte) error NextKey([]byte) []byte ClearPrefixInChild(keyToChild, prefix []byte) error diff --git a/lib/runtime/storage/trie.go b/lib/runtime/storage/trie.go index 7878fde7a0..aa9a97cb2a 100644 --- a/lib/runtime/storage/trie.go +++ b/lib/runtime/storage/trie.go @@ -18,9 +18,11 @@ package storage import ( "encoding/binary" + "sort" "sync" "github.com/ChainSafe/gossamer/lib/common" + "github.com/ChainSafe/gossamer/lib/common/optional" "github.com/ChainSafe/gossamer/lib/trie" ) @@ -178,6 +180,43 @@ func (s *TrieState) DeleteChild(key []byte) { s.t.DeleteChild(key) } +// DeleteChildLimit deletes up to limit of database entries by lexicographic order, return number +// deleted, true if all delete otherwise false +func (s *TrieState) DeleteChildLimit(key []byte, limit *optional.Bytes) (uint32, bool, error) { + s.lock.Lock() + defer s.lock.Unlock() + tr, err := s.t.GetChild(key) + if err != nil { + return 0, false, err + } + qtyEntries := uint32(len(tr.Entries())) + if limit == nil || !limit.Exists() { + s.t.DeleteChild(key) + return qtyEntries, true, nil + } + limitUint := binary.LittleEndian.Uint32(limit.Value()) + + keys := make([]string, 0, len(tr.Entries())) + for k := range tr.Entries() { + keys = append(keys, k) + } + sort.Strings(keys) + deleted := uint32(0) + for _, k := range keys { + tr.Delete([]byte(k)) + deleted++ + if deleted == limitUint { + break + } + } + + if deleted == qtyEntries { + return deleted, true, nil + } + + return deleted, false, nil +} + // ClearChildStorage removes the child storage entry from the trie func (s *TrieState) ClearChildStorage(keyToChild, key []byte) error { s.lock.Lock() diff --git a/lib/runtime/storage/trie_test.go b/lib/runtime/storage/trie_test.go index c2e22ad62a..dcdb41fe3d 100644 --- a/lib/runtime/storage/trie_test.go +++ b/lib/runtime/storage/trie_test.go @@ -18,10 +18,12 @@ package storage import ( "bytes" + "encoding/binary" "sort" "testing" "github.com/ChainSafe/gossamer/lib/common" + "github.com/ChainSafe/gossamer/lib/common/optional" "github.com/ChainSafe/gossamer/lib/trie" "github.com/stretchr/testify/require" ) @@ -197,3 +199,50 @@ func TestTrieState_RollbackStorageTransaction(t *testing.T) { val := ts.Get([]byte(testCases[0])) require.Equal(t, []byte(testCases[0]), val) } + +func TestTrieState_DeleteChildLimit(t *testing.T) { + ts := newTestTrieState(t) + child := trie.NewEmptyTrie() + + keys := []string{ + "key3", + "key1", + "key2", + } + + for i, key := range keys { + child.Put([]byte(key), []byte{byte(i)}) + } + + keyToChild := []byte("keytochild") + + err := ts.SetChild(keyToChild, child) + require.NoError(t, err) + + testLimitBytes := make([]byte, 4) + binary.LittleEndian.PutUint32(testLimitBytes, uint32(2)) + optLimit2 := optional.NewBytes(true, testLimitBytes) + + testCases := []struct { + key []byte + limit *optional.Bytes + expectedDeleted uint32 + expectedDelAll bool + errMsg string + }{ + {key: []byte("fakekey"), limit: optLimit2, expectedDeleted: 0, expectedDelAll: false, errMsg: "child trie does not exist at key :child_storage:default:fakekey"}, + {key: []byte("keytochild"), limit: optLimit2, expectedDeleted: 2, expectedDelAll: false}, + {key: []byte("keytochild"), limit: nil, expectedDeleted: 1, expectedDelAll: true}, + } + for _, test := range testCases { + deleted, all, err := ts.DeleteChildLimit(test.key, test.limit) + if test.errMsg != "" { + require.Error(t, err) + require.EqualError(t, err, test.errMsg) + continue + } + require.NoError(t, err) + require.Equal(t, test.expectedDeleted, deleted) + require.Equal(t, test.expectedDelAll, all) + } +} diff --git a/lib/runtime/wasmer/imports.go b/lib/runtime/wasmer/imports.go index f0b892c4f4..0c5a928f0d 100644 --- a/lib/runtime/wasmer/imports.go +++ b/lib/runtime/wasmer/imports.go @@ -1118,19 +1118,33 @@ func ext_default_child_storage_storage_kill_version_1(context unsafe.Pointer, ch } //export ext_default_child_storage_storage_kill_version_2 -func ext_default_child_storage_storage_kill_version_2(context unsafe.Pointer, childStorageKeySpan, _ C.int64_t) C.int32_t { +func ext_default_child_storage_storage_kill_version_2(context unsafe.Pointer, childStorageKeySpan, lim C.int64_t) C.int32_t { logger.Debug("[ext_default_child_storage_storage_kill_version_2] executing...") - logger.Warn("[ext_default_child_storage_storage_kill_version_2] somewhat unimplemented") - // TODO: need to use `limit` parameter instanceContext := wasm.IntoInstanceContext(context) ctx := instanceContext.Data().(*runtime.Context) storage := ctx.Storage - childStorageKey := asMemorySlice(instanceContext, childStorageKeySpan) - storage.DeleteChild(childStorageKey) - // note: this function always returns `KillStorageResult::AllRemoved`, which is 0 + limitBytes := asMemorySlice(instanceContext, lim) + buf := &bytes.Buffer{} + buf.Write(limitBytes) + + limit, err := optional.NewBytes(true, nil).Decode(buf) + if err != nil { + logger.Warn("[ext_default_child_storage_storage_kill_version_2] cannot generate limit", "error", err) + return 0 + } + + _, all, err := storage.DeleteChildLimit(childStorageKey, limit) + if err != nil { + logger.Warn("[ext_default_child_storage_storage_kill_version_2] cannot get child storage", "error", err) + } + + if all { + return 1 + } + return 0 } diff --git a/lib/runtime/wasmer/imports_test.go b/lib/runtime/wasmer/imports_test.go index 21b98214e8..a8cf31ff53 100644 --- a/lib/runtime/wasmer/imports_test.go +++ b/lib/runtime/wasmer/imports_test.go @@ -18,6 +18,7 @@ package wasmer import ( "bytes" + "encoding/binary" "os" "sort" "testing" @@ -1071,6 +1072,101 @@ func Test_ext_default_child_storage_storage_kill_version_1(t *testing.T) { require.Nil(t, child) } +func Test_ext_default_child_storage_storage_kill_version_2_limit_all(t *testing.T) { + inst := NewTestInstance(t, runtime.HOST_API_TEST_RUNTIME) + + tr := trie.NewEmptyTrie() + tr.Put([]byte(`key2`), []byte(`value2`)) + tr.Put([]byte(`key1`), []byte(`value1`)) + err := inst.ctx.Storage.SetChild(testChildKey, tr) + require.NoError(t, err) + + // Confirm if value is set + child, err := inst.ctx.Storage.GetChild(testChildKey) + require.NoError(t, err) + require.NotNil(t, child) + + encChildKey, err := scale.Encode(testChildKey) + require.NoError(t, err) + + testLimit := uint32(2) + testLimitBytes := make([]byte, 4) + binary.LittleEndian.PutUint32(testLimitBytes, testLimit) + + optLimit, err := optional.NewBytes(true, testLimitBytes).Encode() + require.NoError(t, err) + + res, err := inst.Exec("rtm_ext_default_child_storage_storage_kill_version_2", append(encChildKey, optLimit...)) + require.NoError(t, err) + require.Equal(t, []byte{1, 0, 0, 0}, res) + + child, err = inst.ctx.Storage.GetChild(testChildKey) + require.NoError(t, err) + require.Equal(t, 0, len(child.Entries())) +} + +func Test_ext_default_child_storage_storage_kill_version_2_limit_1(t *testing.T) { + inst := NewTestInstance(t, runtime.HOST_API_TEST_RUNTIME) + + tr := trie.NewEmptyTrie() + tr.Put([]byte(`key2`), []byte(`value2`)) + tr.Put([]byte(`key1`), []byte(`value1`)) + err := inst.ctx.Storage.SetChild(testChildKey, tr) + require.NoError(t, err) + + // Confirm if value is set + child, err := inst.ctx.Storage.GetChild(testChildKey) + require.NoError(t, err) + require.NotNil(t, child) + + encChildKey, err := scale.Encode(testChildKey) + require.NoError(t, err) + + testLimit := uint32(1) + testLimitBytes := make([]byte, 4) + binary.LittleEndian.PutUint32(testLimitBytes, testLimit) + + optLimit, err := optional.NewBytes(true, testLimitBytes).Encode() + require.NoError(t, err) + + res, err := inst.Exec("rtm_ext_default_child_storage_storage_kill_version_2", append(encChildKey, optLimit...)) + require.NoError(t, err) + require.Equal(t, []byte{0, 0, 0, 0}, res) + + child, err = inst.ctx.Storage.GetChild(testChildKey) + require.NoError(t, err) + require.Equal(t, 1, len(child.Entries())) +} + +func Test_ext_default_child_storage_storage_kill_version_2_limit_none(t *testing.T) { + inst := NewTestInstance(t, runtime.HOST_API_TEST_RUNTIME) + + tr := trie.NewEmptyTrie() + tr.Put([]byte(`key2`), []byte(`value2`)) + tr.Put([]byte(`key1`), []byte(`value1`)) + err := inst.ctx.Storage.SetChild(testChildKey, tr) + require.NoError(t, err) + + // Confirm if value is set + child, err := inst.ctx.Storage.GetChild(testChildKey) + require.NoError(t, err) + require.NotNil(t, child) + + encChildKey, err := scale.Encode(testChildKey) + require.NoError(t, err) + + optLimit, err := optional.NewBytes(false, nil).Encode() + require.NoError(t, err) + + res, err := inst.Exec("rtm_ext_default_child_storage_storage_kill_version_2", append(encChildKey, optLimit...)) + require.NoError(t, err) + require.Equal(t, []byte{1, 0, 0, 0}, res) + + child, err = inst.ctx.Storage.GetChild(testChildKey) + require.Error(t, err) + require.Nil(t, child) +} + func Test_ext_storage_append_version_1(t *testing.T) { inst := NewTestInstance(t, runtime.HOST_API_TEST_RUNTIME)