Skip to content

Commit

Permalink
feat(store/v2): add the commitment store (#18526)
Browse files Browse the repository at this point in the history
  • Loading branch information
cool-develope authored Nov 22, 2023
1 parent 6ca0b2d commit fc4ce6c
Show file tree
Hide file tree
Showing 21 changed files with 349 additions and 164 deletions.
29 changes: 18 additions & 11 deletions store/changeset.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,43 @@ package store
// track writes. Deletion can be denoted by a nil value or explicitly by the
// Delete field.
type KVPair struct {
Key []byte
Value []byte
StoreKey string // optional
Key []byte
Value []byte
}

// Changeset defines a set of KVPair entries.
type KVPairs []KVPair

// Changeset defines a set of KVPair entries by maintaining a map
// from store key to a slice of KVPair objects.
type Changeset struct {
Pairs []KVPair
Pairs map[string]KVPairs
}

func NewChangeset(pairs ...KVPair) *Changeset {
func NewChangeset(pairs map[string]KVPairs) *Changeset {
return &Changeset{
Pairs: pairs,
}
}

// Size returns the number of key-value pairs in the batch.
func (cs *Changeset) Size() int {
return len(cs.Pairs)
cnt := 0
for _, pairs := range cs.Pairs {
cnt += len(pairs)
}

return cnt
}

// Add adds a key-value pair to the ChangeSet.
func (cs *Changeset) Add(key, value []byte) {
cs.Pairs = append(cs.Pairs, KVPair{
func (cs *Changeset) Add(storeKey string, key, value []byte) {
cs.Pairs[storeKey] = append(cs.Pairs[storeKey], KVPair{
Key: key,
Value: value,
})
}

// AddKVPair adds a KVPair to the ChangeSet.
func (cs *Changeset) AddKVPair(pair KVPair) {
cs.Pairs = append(cs.Pairs, pair)
func (cs *Changeset) AddKVPair(storeKey string, pair KVPair) {
cs.Pairs[storeKey] = append(cs.Pairs[storeKey], pair)
}
34 changes: 14 additions & 20 deletions store/commitment/iavl/tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ import (
ics23 "github.com/cosmos/ics23/go"

log "cosmossdk.io/log"
"cosmossdk.io/store/v2"
"cosmossdk.io/store/v2/commitment"
)

var _ store.Committer = (*IavlTree)(nil)
var _ commitment.Tree = (*IavlTree)(nil)

// IavlTree is a wrapper around iavl.MutableTree.
type IavlTree struct {
Expand All @@ -26,25 +26,19 @@ func NewIavlTree(db dbm.DB, logger log.Logger, cfg *Config) *IavlTree {
}
}

// WriteBatch writes a batch of key-value pairs to the database.
func (t *IavlTree) WriteBatch(cs *store.Changeset) error {
for _, kv := range cs.Pairs {
if kv.Value == nil {
_, res, err := t.tree.Remove(kv.Key)
if err != nil {
return err
}
if !res {
return fmt.Errorf("failed to delete key %X", kv.Key)
}
} else {
_, err := t.tree.Set(kv.Key, kv.Value)
if err != nil {
return err
}
}
// Remove removes the given key from the tree.
func (t *IavlTree) Remove(key []byte) error {
_, res, err := t.tree.Remove(key)
if !res {
return fmt.Errorf("key %x not found", key)
}
return nil
return err
}

// Set sets the given key-value pair in the tree.
func (t *IavlTree) Set(key, value []byte) error {
_, err := t.tree.Set(key, value)
return err
}

// WorkingHash returns the working hash of the database.
Expand Down
28 changes: 9 additions & 19 deletions store/commitment/iavl/tree_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"github.com/stretchr/testify/require"

"cosmossdk.io/log"
"cosmossdk.io/store/v2"
)

func generateTree(treeType string) *IavlTree {
Expand All @@ -25,13 +24,9 @@ func TestIavlTree(t *testing.T) {
require.Equal(t, uint64(0), initVersion)

// write a batch of version 1
cs1 := store.NewChangeset()
cs1.Add([]byte("key1"), []byte("value1"))
cs1.Add([]byte("key2"), []byte("value2"))
cs1.Add([]byte("key3"), []byte("value3"))

err := tree.WriteBatch(cs1)
require.NoError(t, err)
require.NoError(t, tree.Set([]byte("key1"), []byte("value1")))
require.NoError(t, tree.Set([]byte("key2"), []byte("value2")))
require.NoError(t, tree.Set([]byte("key3"), []byte("value3")))

workingHash := tree.WorkingHash()
require.NotNil(t, workingHash)
Expand All @@ -44,13 +39,10 @@ func TestIavlTree(t *testing.T) {
require.Equal(t, uint64(1), tree.GetLatestVersion())

// write a batch of version 2
cs2 := store.NewChangeset()
cs2.Add([]byte("key4"), []byte("value4"))
cs2.Add([]byte("key5"), []byte("value5"))
cs2.Add([]byte("key6"), []byte("value6"))
cs2.Add([]byte("key1"), nil) // delete key1
err = tree.WriteBatch(cs2)
require.NoError(t, err)
require.NoError(t, tree.Set([]byte("key4"), []byte("value4")))
require.NoError(t, tree.Set([]byte("key5"), []byte("value5")))
require.NoError(t, tree.Set([]byte("key6"), []byte("value6")))
require.NoError(t, tree.Remove([]byte("key1"))) // delete key1
version2Hash := tree.WorkingHash()
require.NotNil(t, version2Hash)
commitHash, err = tree.Commit()
Expand All @@ -67,10 +59,8 @@ func TestIavlTree(t *testing.T) {
require.NotNil(t, proof.GetNonexist())

// write a batch of version 3
cs3 := store.NewChangeset()
cs3.Add([]byte("key7"), []byte("value7"))
cs3.Add([]byte("key8"), []byte("value8"))
err = tree.WriteBatch(cs3)
require.NoError(t, tree.Set([]byte("key7"), []byte("value7")))
require.NoError(t, tree.Set([]byte("key8"), []byte("value8")))
require.NoError(t, err)
_, err = tree.Commit()
require.NoError(t, err)
Expand Down
138 changes: 138 additions & 0 deletions store/commitment/store.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package commitment

import (
"errors"
"fmt"

ics23 "github.com/cosmos/ics23/go"

"cosmossdk.io/log"
"cosmossdk.io/store/v2"
)

var _ store.Committer = (*CommitStore)(nil)

// CommitStore is a wrapper around multiple Tree objects mapped by a unique store
// key. Each store key reflects dedicated and unique usage within a module. A caller
// can construct a CommitStore with one or more store keys. It is expected that a
// RootStore use a CommitStore as an abstraction to handle multiple store keys
// and trees.
type CommitStore struct {
logger log.Logger

multiTrees map[string]Tree
}

// NewCommitStore creates a new CommitStore instance.
func NewCommitStore(multiTrees map[string]Tree, logger log.Logger) (*CommitStore, error) {
return &CommitStore{
logger: logger,
multiTrees: multiTrees,
}, nil
}

func (c *CommitStore) WriteBatch(cs *store.Changeset) error {
for storeKey, pairs := range cs.Pairs {
tree, ok := c.multiTrees[storeKey]
if !ok {
return fmt.Errorf("store key %s not found in multiTrees", storeKey)
}
for _, kv := range pairs {
if kv.Value == nil {
if err := tree.Remove(kv.Key); err != nil {
return err
}
} else if err := tree.Set(kv.Key, kv.Value); err != nil {
return err
}
}
}

return nil
}

func (c *CommitStore) WorkingStoreInfos(version uint64) []store.StoreInfo {
storeInfos := make([]store.StoreInfo, 0, len(c.multiTrees))
for storeKey, tree := range c.multiTrees {
storeInfos = append(storeInfos, store.StoreInfo{
Name: storeKey,
CommitID: store.CommitID{
Version: version,
Hash: tree.WorkingHash(),
},
})
}

return storeInfos
}

func (c *CommitStore) GetLatestVersion() (uint64, error) {
latestVersion := uint64(0)
for storeKey, tree := range c.multiTrees {
version := tree.GetLatestVersion()
if latestVersion != 0 && version != latestVersion {
return 0, fmt.Errorf("store %s has version %d, not equal to latest version %d", storeKey, version, latestVersion)
}
latestVersion = version
}

return latestVersion, nil
}

func (c *CommitStore) LoadVersion(targetVersion uint64) error {
for _, tree := range c.multiTrees {
if err := tree.LoadVersion(targetVersion); err != nil {
return err
}
}

return nil
}

func (c *CommitStore) Commit() ([]store.StoreInfo, error) {
storeInfos := make([]store.StoreInfo, 0, len(c.multiTrees))
for storeKey, tree := range c.multiTrees {
hash, err := tree.Commit()
if err != nil {
return nil, err
}
storeInfos = append(storeInfos, store.StoreInfo{
Name: storeKey,
CommitID: store.CommitID{
Version: tree.GetLatestVersion(),
Hash: hash,
},
})
}

return storeInfos, nil
}

func (c *CommitStore) GetProof(storeKey string, version uint64, key []byte) (*ics23.CommitmentProof, error) {
tree, ok := c.multiTrees[storeKey]
if !ok {
return nil, fmt.Errorf("store %s not found", storeKey)
}

return tree.GetProof(version, key)
}

func (c *CommitStore) Prune(version uint64) (ferr error) {
for _, tree := range c.multiTrees {
if err := tree.Prune(version); err != nil {
ferr = errors.Join(ferr, err)
}
}

return ferr
}

func (c *CommitStore) Close() (ferr error) {
for _, tree := range c.multiTrees {
if err := tree.Close(); err != nil {
ferr = errors.Join(ferr, err)
}
}

return ferr
}
21 changes: 21 additions & 0 deletions store/commitment/tree.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package commitment

import (
"io"

ics23 "github.com/cosmos/ics23/go"
)

// Tree is the interface that wraps the basic Tree methods.
type Tree interface {
Set(key, value []byte) error
Remove(key []byte) error
GetLatestVersion() uint64
WorkingHash() []byte
LoadVersion(version uint64) error
Commit() ([]byte, error)
GetProof(version uint64, key []byte) (*ics23.CommitmentProof, error)
Prune(version uint64) error

io.Closer
}
8 changes: 4 additions & 4 deletions store/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,11 @@ type VersionedDatabase interface {
// Committer defines an API for committing state.
type Committer interface {
WriteBatch(cs *Changeset) error
WorkingHash() []byte
GetLatestVersion() uint64
WorkingStoreInfos(version uint64) []StoreInfo
GetLatestVersion() (uint64, error)
LoadVersion(targetVersion uint64) error
Commit() ([]byte, error)
GetProof(version uint64, key []byte) (*ics23.CommitmentProof, error)
Commit() ([]StoreInfo, error)
GetProof(storeKey string, version uint64, key []byte) (*ics23.CommitmentProof, error)

// Prune attempts to prune all versions up to and including the provided
// version argument. The operation should be idempotent. An error should be
Expand Down
2 changes: 1 addition & 1 deletion store/kv/branch/iterator.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ type iterator struct {
key []byte
value []byte
keys []string
values []store.KVPair
values store.KVPairs
reverse bool
exhausted bool // exhausted reflects if the parent iterator is exhausted or not
}
Expand Down
15 changes: 7 additions & 8 deletions store/kv/branch/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,17 +72,16 @@ func (s *Store) GetChangeset() *store.Changeset {
keys := maps.Keys(s.changeset)
slices.Sort(keys)

pairs := make([]store.KVPair, len(keys))
pairs := make(store.KVPairs, len(keys))
for i, key := range keys {
kvPair := s.changeset[key]
pairs[i] = store.KVPair{
Key: []byte(key),
Value: slices.Clone(kvPair.Value),
StoreKey: kvPair.StoreKey,
Key: []byte(key),
Value: slices.Clone(kvPair.Value),
}
}

return store.NewChangeset(pairs...)
return store.NewChangeset(map[string]store.KVPairs{s.storeKey: pairs})
}

func (s *Store) Reset(toVersion uint64) error {
Expand Down Expand Up @@ -170,7 +169,7 @@ func (s *Store) Set(key, value []byte) {
defer s.mu.Unlock()

// omit the key as that can be inferred from the map key
s.changeset[string(key)] = store.KVPair{Value: slices.Clone(value), StoreKey: s.storeKey}
s.changeset[string(key)] = store.KVPair{Value: slices.Clone(value)}
}

func (s *Store) Delete(key []byte) {
Expand All @@ -180,7 +179,7 @@ func (s *Store) Delete(key []byte) {
defer s.mu.Unlock()

// omit the key as that can be inferred from the map key
s.changeset[string(key)] = store.KVPair{Value: nil, StoreKey: s.storeKey}
s.changeset[string(key)] = store.KVPair{Value: nil}
}

func (s *Store) Write() {
Expand Down Expand Up @@ -290,7 +289,7 @@ func (s *Store) newIterator(parentItr store.Iterator, start, end []byte, reverse
slices.Reverse(keys)
}

values := make([]store.KVPair, len(keys))
values := make(store.KVPairs, len(keys))
for i, key := range keys {
values[i] = s.changeset[key]
}
Expand Down
Loading

0 comments on commit fc4ce6c

Please sign in to comment.