Skip to content

Commit

Permalink
Implement limit for trie.ClearPrefix
Browse files Browse the repository at this point in the history
  • Loading branch information
arijitAD committed Oct 22, 2021
1 parent 2186caf commit 62cfc8e
Show file tree
Hide file tree
Showing 6 changed files with 513 additions and 14 deletions.
1 change: 1 addition & 0 deletions lib/runtime/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ type Storage interface {
GetChildNextKey(keyToChild, key []byte) ([]byte, error)
GetChild(keyToChild []byte) (*trie.Trie, error)
ClearPrefix(prefix []byte) error
ClearPrefixLimit(prefix []byte, limit *optional.Bytes) (uint32, bool, error)
BeginStorageTransaction()
CommitStorageTransaction()
RollbackStorageTransaction()
Expand Down
16 changes: 16 additions & 0 deletions lib/runtime/storage/trie.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package storage

import (
"encoding/binary"
"errors"
"sort"
"sync"

Expand Down Expand Up @@ -138,6 +139,21 @@ func (s *TrieState) ClearPrefix(prefix []byte) error {
return nil
}

// ClearPrefixLimit deletes key-value pairs from the trie where the key starts with the given prefix till limit reached
func (s *TrieState) ClearPrefixLimit(prefix []byte, limit *optional.Bytes) (uint32, bool, error) {
s.lock.Lock()
defer s.lock.Unlock()

if limit == nil || !limit.Exists() {
return 0, false, errors.New("limit not exists")
}

limitUint := binary.LittleEndian.Uint32(limit.Value())

num, del := s.t.ClearPrefixLimit(prefix, limitUint)
return num, del, nil
}

// TrieEntries returns every key-value pair in the trie
func (s *TrieState) TrieEntries() map[string][]byte {
s.lock.RLock()
Expand Down
58 changes: 51 additions & 7 deletions lib/runtime/wasmer/imports.go
Original file line number Diff line number Diff line change
Expand Up @@ -1693,24 +1693,47 @@ func ext_storage_clear_prefix_version_1(context unsafe.Pointer, prefixSpan C.int
}

//export ext_storage_clear_prefix_version_2
func ext_storage_clear_prefix_version_2(context unsafe.Pointer, prefixSpan, _ C.int64_t) C.int64_t {
func ext_storage_clear_prefix_version_2(context unsafe.Pointer, prefixSpan, lim C.int64_t) C.int64_t {
logger.Trace("[ext_storage_clear_prefix_version_2] executing...")
logger.Warn("[ext_storage_clear_prefix_version_2] somewhat unimplemented")
// TODO: need to use unused `limit` parameter (#1792)

instanceContext := wasm.IntoInstanceContext(context)
ctx := instanceContext.Data().(*runtime.Context)
storage := ctx.Storage

prefix := asMemorySlice(instanceContext, prefixSpan)
logger.Debug("[ext_storage_clear_prefix_version_1]", "prefix", fmt.Sprintf("0x%x", prefix))
logger.Debug("[ext_storage_clear_prefix_version_2]", "prefix", fmt.Sprintf("0x%x", prefix))

err := storage.ClearPrefix(prefix)
limitBytes := asMemorySlice(instanceContext, lim)
buf := &bytes.Buffer{}
buf.Write(limitBytes)

limit, err := optional.NewBytes(true, nil).Decode(buf)
if err != nil {
logger.Error("[ext_storage_clear_prefix_version_1]", "error", err)
logger.Warn("[ext_storage_clear_prefix_version_2] cannot generate limit", "error", err)
return 0
}

return 1
numRemoved, all, err := storage.ClearPrefixLimit(prefix, limit)
if err != nil {
logger.Error("[ext_storage_clear_prefix_version_2]", "error", err)
}

encBytes, err := toKillStorageResultEnum(all, numRemoved)

if err != nil {
logger.Error("[ext_crypto_ed25519_public_keys_version_1] failed to allocate memory", err)
ret, _ := toWasmMemory(instanceContext, nil)
return C.int64_t(ret)
}

valueSpan, err := toWasmMemory(instanceContext, encBytes)
if err != nil {
logger.Error("[ext_storage_get_version_2] failed to allocate", "error", err)
ptr, _ := toWasmMemory(instanceContext, nil)
return C.int64_t(ptr)
}

return C.int64_t(valueSpan)
}

//export ext_storage_exists_version_1
Expand Down Expand Up @@ -1966,6 +1989,27 @@ func toWasmMemoryOptionalUint32(context wasm.InstanceContext, data *uint32) (int
return toWasmMemory(context, enc)
}

// toKillStorageResult returns enum encoded value
func toKillStorageResultEnum(allRemoved bool, numRemoved uint32) ([]byte, error) {
var b, sbytes []byte
sbytes, err := scale.Marshal(numRemoved)
if err != nil {
return nil, err
}

if allRemoved {
// No key remains in the child trie.
b = append(b, byte(0))
} else {
// At least one key still resides in the child trie due to the supplied limit.
b = append(b, byte(1))
}

b = append(b, sbytes...)

return b, err
}

// Wraps slice in optional.FixedSizeBytes and copies result to wasm memory. Returns resulting 64bit span descriptor
func toWasmMemoryFixedSizeOptional(context wasm.InstanceContext, data []byte) (int64, error) {
var opt *optional.FixedSizeBytes
Expand Down
72 changes: 72 additions & 0 deletions lib/runtime/wasmer/imports_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,78 @@ func Test_ext_storage_clear_prefix_version_1(t *testing.T) {
require.NotNil(t, val)
}

func Test_ext_storage_clear_prefix_version_2(t *testing.T) {
inst := NewTestInstance(t, runtime.HOST_API_TEST_RUNTIME)

testkey := []byte("noot")
inst.ctx.Storage.Set(testkey, []byte{1})

testkey2 := []byte("noot1")
inst.ctx.Storage.Set(testkey2, []byte{1})

testkey3 := []byte("noot2")
inst.ctx.Storage.Set(testkey3, []byte{1})

testkey4 := []byte("noot3")
inst.ctx.Storage.Set(testkey4, []byte{1})

testkey5 := []byte("spaghet")
testValue5 := []byte{2}
inst.ctx.Storage.Set(testkey5, testValue5)

enc, err := scale.Marshal(testkey[:3])
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)

// clearing prefix for "noo" prefix with limit 2
encValue, err := inst.Exec("rtm_ext_storage_clear_prefix_version_2", append(enc, optLimit...))
require.NoError(t, err)

var decVal []byte
scale.Unmarshal(encValue, &decVal)

var numDeleted uint32
// numDeleted represents no. of actual keys deleted
scale.Unmarshal(decVal[1:], &numDeleted)
require.Equal(t, uint32(2), numDeleted)

var expectedAllDeleted byte
// expectedAllDeleted value 0 represents all keys deleted, 1 represents keys are pending with prefix in trie
expectedAllDeleted = 1
require.Equal(t, expectedAllDeleted, decVal[0])

val := inst.ctx.Storage.Get(testkey)
require.NotNil(t, val)

val = inst.ctx.Storage.Get(testkey5)
require.NotNil(t, val)
require.Equal(t, testValue5, val)

// clearing prefix again for "noo" prefix with limit 2
encValue, err = inst.Exec("rtm_ext_storage_clear_prefix_version_2", append(enc, optLimit...))
require.NoError(t, err)

scale.Unmarshal(encValue, &decVal)
scale.Unmarshal(decVal[1:], &numDeleted)
require.Equal(t, uint32(2), numDeleted)

expectedAllDeleted = 0
require.Equal(t, expectedAllDeleted, decVal[0])

val = inst.ctx.Storage.Get(testkey)
require.Nil(t, val)

val = inst.ctx.Storage.Get(testkey5)
require.NotNil(t, val)
require.Equal(t, testValue5, val)
}

func Test_ext_storage_get_version_1(t *testing.T) {
inst := NewTestInstance(t, runtime.HOST_API_TEST_RUNTIME)

Expand Down
133 changes: 130 additions & 3 deletions lib/trie/trie.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package trie

import (
"bytes"
"fmt"

"github.com/ChainSafe/gossamer/lib/common"
)
Expand Down Expand Up @@ -509,6 +510,132 @@ func (t *Trie) retrieve(parent node, key []byte) *leaf {
return value
}

// ClearPrefixLimit deletes the keys having the prefix till limit reached
func (t *Trie) ClearPrefixLimit(prefix []byte, limit uint32) (uint32, bool) {
if limit == 0 {
return 0, false
}

p := keyToNibbles(prefix)
if len(p) > 0 && p[len(p)-1] == 0 {
p = p[:len(p)-1]
}

l := limit
var allDeleted bool
t.root, _, allDeleted = t.clearPrefixLimit(t.root, p, &limit)
return l - limit, allDeleted
}

func (t *Trie) clearPrefixLimit(cn node, prefix []byte, limit *uint32) (node, bool, bool) {
curr := t.maybeUpdateGeneration(cn)

switch c := curr.(type) {
case *branch:
length := lenCommonPrefix(c.key, prefix)
if length == len(prefix) {
n, _ := t.deleteNodes(c, []byte{}, limit)
if n == nil {
return n, true, true
}
return n, true, false
}

if len(prefix) == len(c.key)+1 && length == len(prefix)-1 {
i := prefix[len(c.key)]
c.children[i], _ = t.deleteNodes(c.children[i], []byte{}, limit)

c.setDirty(true)
curr = handleDeletion(c, prefix)

if c.children[i] == nil {
return curr, true, true
}
return c, true, false
}

if len(prefix) <= len(c.key) || length < len(c.key) {
// this node doesn't have the prefix, return
return c, false, true
}

i := prefix[len(c.key)]

var wasUpdated, allDeleted bool
c.children[i], wasUpdated, allDeleted = t.clearPrefixLimit(c.children[i], prefix[len(c.key)+1:], limit)
if wasUpdated {
c.setDirty(true)
curr = handleDeletion(c, prefix)
}

return curr, curr.isDirty(), allDeleted
case *leaf:
length := lenCommonPrefix(c.key, prefix)
if length == len(prefix) {
*limit--
return nil, true, true
}
// Prefix not found might be all deleted
return curr, false, true

case nil:
return nil, false, true
}

return nil, false, true
}

func (t *Trie) deleteNodes(cn node, prefix []byte, limit *uint32) (node, bool) {
curr := t.maybeUpdateGeneration(cn)

switch c := curr.(type) {
case *leaf:
if *limit == 0 {
return c, false
}
*limit--
return nil, true
case *branch:
if len(c.key) != 0 {
prefix = append(prefix, c.key...)
}

for i, child := range c.children {
if child == nil {
continue
}

var isDel bool
if c.children[i], isDel = t.deleteNodes(child, prefix, limit); !isDel {
continue
}

c.setDirty(true)
curr = handleDeletion(c, prefix)
isAllNil := c.numChildren() == 0
if isAllNil && c.value == nil {
curr = nil
}

if *limit == 0 {
return curr, true
}
}

if *limit == 0 {
return c, true
}

// Delete the current node as well
if c.value != nil {
*limit--
}
return nil, true
}

return curr, true
}

// ClearPrefix deletes all key-value pairs from the trie where the key starts with the given prefix
func (t *Trie) ClearPrefix(prefix []byte) {
if len(prefix) == 0 {
Expand Down Expand Up @@ -611,10 +738,10 @@ func (t *Trie) delete(parent node, key []byte) (node, bool) {
// Key doesn't exist.
return p, false
case nil:
// do nothing
return nil, false
default:
panic(fmt.Sprintf("%T: invalid node: %v (%v)", p, p, key))
}
// This should never happen.
return nil, false
}

// handleDeletion is called when a value is deleted from a branch
Expand Down
Loading

0 comments on commit 62cfc8e

Please sign in to comment.