Skip to content

Commit

Permalink
feat(simulation): Implement store decoder implementation from collect…
Browse files Browse the repository at this point in the history
…ions schema (#16074)

Co-authored-by: unknown unknown <unknown@unknown>
  • Loading branch information
testinginprod and unknown unknown authored May 11, 2023
1 parent 908677e commit 69642f6
Show file tree
Hide file tree
Showing 27 changed files with 306 additions and 44 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
* (modulemanager) [#15829](https://github.com/cosmos/cosmos-sdk/pull/15829) add new endblocker interface to handle valset updates
* (core) [#14860](https://github.com/cosmos/cosmos-sdk/pull/14860) Add `Precommit` and `PrepareCheckState` AppModule callbacks.
* (tx) [#15992](https://github.com/cosmos/cosmos-sdk/pull/15992) Add `WithExtensionOptions` in tx Factory to allow `SetExtensionOptions` with given extension options.

* (types/simulation) [#16074](https://github.com/cosmos/cosmos-sdk/pull/16074) Add generic SimulationStoreDecoder for modules using collections.
### Improvements

* (client) [#16075](https://github.com/cosmos/cosmos-sdk/pull/16075) Partly revert [#15953](https://github.com/cosmos/cosmos-sdk/issues/15953) and `factory.Prepare` does nothing in offline mode.
Expand Down
4 changes: 4 additions & 0 deletions collections/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ Ref: https://keepachangelog.com/en/1.0.0/

## [Unreleased]

### Features

* [#16074](https://github.com/cosmos/cosmos-sdk/pull/16074) – makes the generic Collection interface public, still highly unstable.

## [v0.1.0](https://github.com/cosmos/cosmos-sdk/releases/tag/collections%2Fv0.1.0)

Collections `v0.1.0` is released! Check out the [docs](https://docs.cosmos.network/main/packages/collections) to know how to use the APIs.
50 changes: 50 additions & 0 deletions collections/codec/codec.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,56 @@ type ValueCodec[T any] interface {
ValueType() string
}

// NewUntypedValueCodec returns an UntypedValueCodec for the provided ValueCodec.
func NewUntypedValueCodec[V any](v ValueCodec[V]) UntypedValueCodec {
typeName := fmt.Sprintf("%T", *new(V))
checkType := func(value interface{}) (v V, err error) {
concrete, ok := value.(V)
if !ok {
return v, fmt.Errorf("%w: expected value of type %s, got %T", ErrEncoding, typeName, value)
}
return concrete, nil
}
return UntypedValueCodec{
Decode: func(b []byte) (interface{}, error) { return v.Decode(b) },
Encode: func(value interface{}) ([]byte, error) {
concrete, err := checkType(value)
if err != nil {
return nil, err
}
return v.Encode(concrete)
},
DecodeJSON: func(b []byte) (interface{}, error) {
return v.DecodeJSON(b)
},
EncodeJSON: func(value interface{}) ([]byte, error) {
concrete, err := checkType(value)
if err != nil {
return nil, err
}
return v.EncodeJSON(concrete)
},
Stringify: func(value interface{}) (string, error) {
concrete, err := checkType(value)
if err != nil {
return "", err
}
return v.Stringify(concrete), nil
},
ValueType: func() string { return v.ValueType() },
}
}

// UntypedValueCodec wraps a ValueCodec to expose an untyped API for encoding and decoding values.
type UntypedValueCodec struct {
Decode func(b []byte) (interface{}, error)
Encode func(value interface{}) ([]byte, error)
DecodeJSON func(b []byte) (interface{}, error)
EncodeJSON func(value interface{}) ([]byte, error)
Stringify func(value interface{}) (string, error)
ValueType func() string
}

// KeyToValueCodec converts a KeyCodec into a ValueCodec.
func KeyToValueCodec[K any](keyCodec KeyCodec[K]) ValueCodec[K] { return keyToValueCodec[K]{keyCodec} }

Expand Down
39 changes: 39 additions & 0 deletions collections/codec/codec_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package codec

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestUntypedValueCodec(t *testing.T) {
vc := NewUntypedValueCodec(KeyToValueCodec(NewStringKeyCodec[string]()))

t.Run("encode/decode", func(t *testing.T) {
_, err := vc.Encode(0)
require.ErrorIs(t, err, ErrEncoding)
b, err := vc.Encode("hello")
require.NoError(t, err)
value, err := vc.Decode(b)
require.NoError(t, err)
require.Equal(t, "hello", value)
})

t.Run("json encode/decode", func(t *testing.T) {
_, err := vc.EncodeJSON(0)
require.ErrorIs(t, err, ErrEncoding)
b, err := vc.EncodeJSON("hello")
require.NoError(t, err)
value, err := vc.DecodeJSON(b)
require.NoError(t, err)
require.Equal(t, "hello", value)
})

t.Run("stringify", func(t *testing.T) {
_, err := vc.Stringify(0)
require.ErrorIs(t, err, ErrEncoding)
s, err := vc.Stringify("hello")
require.NoError(t, err)
require.Equal(t, "hello", s)
})
}
47 changes: 41 additions & 6 deletions collections/collections.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package collections

import (
"context"
"errors"
io "io"
"math"

"cosmossdk.io/collections/codec"
Expand Down Expand Up @@ -72,16 +74,20 @@ var (
BytesValue = codec.KeyToValueCodec(BytesKey)
)

// collection is the interface that all collections support. It will eventually
// Collection is the interface that all collections implement. It will eventually
// include methods for importing/exporting genesis data and schema
// reflection for clients.
type collection interface {
// getName is the unique name of the collection within a schema. It must
// NOTE: Unstable.
type Collection interface {
// GetName is the unique name of the collection within a schema. It must
// match format specified by NameRegex.
getName() string
GetName() string

// getPrefix is the unique prefix of the collection within a schema.
getPrefix() []byte
// GetPrefix is the unique prefix of the collection within a schema.
GetPrefix() []byte

// ValueCodec returns the codec used to encode/decode values of the collection.
ValueCodec() codec.UntypedValueCodec

genesisHandler
}
Expand Down Expand Up @@ -122,3 +128,32 @@ func NewPrefix[T interface{ int | string | []byte }](identifier T) Prefix {
}
return prefix
}

var _ Collection = (*collectionImpl[string, string])(nil)

// collectionImpl wraps a Map and implements Collection. This properly splits
// the generic and untyped Collection interface from the typed Map, which every
// collection builds on.
type collectionImpl[K, V any] struct {
m Map[K, V]
}

func (c collectionImpl[K, V]) ValueCodec() codec.UntypedValueCodec {
return codec.NewUntypedValueCodec(c.m.vc)
}

func (c collectionImpl[K, V]) GetName() string { return c.m.name }

func (c collectionImpl[K, V]) GetPrefix() []byte { return NewPrefix(c.m.prefix) }

func (c collectionImpl[K, V]) validateGenesis(r io.Reader) error { return c.m.validateGenesis(r) }

func (c collectionImpl[K, V]) importGenesis(ctx context.Context, r io.Reader) error {
return c.m.importGenesis(ctx, r)
}

func (c collectionImpl[K, V]) exportGenesis(ctx context.Context, w io.Writer) error {
return c.m.exportGenesis(ctx, w)
}

func (c collectionImpl[K, V]) defaultGenesis(w io.Writer) error { return c.m.defaultGenesis(w) }
2 changes: 1 addition & 1 deletion collections/iter.go
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ type KeyValue[K, V any] struct {

// encodeRangeBound encodes a range bound, modifying the key bytes to adhere to bound semantics.
func encodeRangeBound[T any](prefix []byte, keyCodec codec.KeyCodec[T], bound *RangeKey[T]) ([]byte, error) {
key, err := encodeKeyWithPrefix(prefix, keyCodec, bound.key)
key, err := EncodeKeyWithPrefix(prefix, keyCodec, bound.key)
if err != nil {
return nil, err
}
Expand Down
18 changes: 10 additions & 8 deletions collections/map.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,22 +39,22 @@ func NewMap[K, V any](
prefix: prefix.Bytes(),
name: name,
}
schemaBuilder.addCollection(m)
schemaBuilder.addCollection(collectionImpl[K, V]{m})
return m
}

func (m Map[K, V]) getName() string {
func (m Map[K, V]) GetName() string {
return m.name
}

func (m Map[K, V]) getPrefix() []byte {
func (m Map[K, V]) GetPrefix() []byte {
return m.prefix
}

// Set maps the provided value to the provided key in the store.
// Errors with ErrEncoding if key or value encoding fails.
func (m Map[K, V]) Set(ctx context.Context, key K, value V) error {
bytesKey, err := encodeKeyWithPrefix(m.prefix, m.kc, key)
bytesKey, err := EncodeKeyWithPrefix(m.prefix, m.kc, key)
if err != nil {
return err
}
Expand All @@ -73,7 +73,7 @@ func (m Map[K, V]) Set(ctx context.Context, key K, value V) error {
// errors with ErrNotFound if the key does not exist, or
// with ErrEncoding if the key or value decoding fails.
func (m Map[K, V]) Get(ctx context.Context, key K) (v V, err error) {
bytesKey, err := encodeKeyWithPrefix(m.prefix, m.kc, key)
bytesKey, err := EncodeKeyWithPrefix(m.prefix, m.kc, key)
if err != nil {
return v, err
}
Expand All @@ -97,7 +97,7 @@ func (m Map[K, V]) Get(ctx context.Context, key K) (v V, err error) {
// Has reports whether the key is present in storage or not.
// Errors with ErrEncoding if key encoding fails.
func (m Map[K, V]) Has(ctx context.Context, key K) (bool, error) {
bytesKey, err := encodeKeyWithPrefix(m.prefix, m.kc, key)
bytesKey, err := EncodeKeyWithPrefix(m.prefix, m.kc, key)
if err != nil {
return false, err
}
Expand All @@ -109,7 +109,7 @@ func (m Map[K, V]) Has(ctx context.Context, key K) (bool, error) {
// Errors with ErrEncoding if key encoding fails.
// If the key does not exist then this is a no-op.
func (m Map[K, V]) Remove(ctx context.Context, key K) error {
bytesKey, err := encodeKeyWithPrefix(m.prefix, m.kc, key)
bytesKey, err := EncodeKeyWithPrefix(m.prefix, m.kc, key)
if err != nil {
return err
}
Expand Down Expand Up @@ -195,7 +195,9 @@ func (m Map[K, V]) KeyCodec() codec.KeyCodec[K] { return m.kc }
// ValueCodec returns the Map's ValueCodec.
func (m Map[K, V]) ValueCodec() codec.ValueCodec[V] { return m.vc }

func encodeKeyWithPrefix[K any](prefix []byte, kc codec.KeyCodec[K], key K) ([]byte, error) {
// EncodeKeyWithPrefix returns how the collection would store the key in storage given
// prefix, key codec and the concrete key.
func EncodeKeyWithPrefix[K any](prefix []byte, kc codec.KeyCodec[K], key K) ([]byte, error) {
prefixLen := len(prefix)
// preallocate buffer
keyBytes := make([]byte, prefixLen+kc.Size(key))
Expand Down
4 changes: 2 additions & 2 deletions collections/map_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func TestMap_IterateRaw(t *testing.T) {
require.NoError(t, m.Set(ctx, 2, 2))

// test non nil end in ascending order
twoBigEndian, err := encodeKeyWithPrefix(nil, Uint64Key, 2)
twoBigEndian, err := EncodeKeyWithPrefix(nil, Uint64Key, 2)
require.NoError(t, err)
iter, err := m.IterateRaw(ctx, nil, twoBigEndian, OrderAscending)
require.NoError(t, err)
Expand All @@ -76,7 +76,7 @@ func Test_encodeKey(t *testing.T) {
number := []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}
expectedKey := append([]byte(prefix), number...)

gotKey, err := encodeKeyWithPrefix(NewPrefix(prefix).Bytes(), Uint64Key, 0)
gotKey, err := EncodeKeyWithPrefix(NewPrefix(prefix).Bytes(), Uint64Key, 0)
require.NoError(t, err)
require.Equal(t, expectedKey, gotKey)
}
28 changes: 18 additions & 10 deletions collections/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ func NewSchemaBuilderFromAccessor(accessorFunc func(ctx context.Context) store.K
return &SchemaBuilder{
schema: &Schema{
storeAccessor: accessorFunc,
collectionsByName: map[string]collection{},
collectionsByPrefix: map[string]collection{},
collectionsByName: map[string]Collection{},
collectionsByPrefix: map[string]Collection{},
},
}
}
Expand Down Expand Up @@ -84,9 +84,9 @@ func (s *SchemaBuilder) Build() (Schema, error) {
return schema, nil
}

func (s *SchemaBuilder) addCollection(collection collection) {
prefix := collection.getPrefix()
name := collection.getName()
func (s *SchemaBuilder) addCollection(collection Collection) {
prefix := collection.GetPrefix()
name := collection.GetName()

if _, ok := s.schema.collectionsByPrefix[string(prefix)]; ok {
s.appendError(fmt.Errorf("prefix %v already taken within schema", prefix))
Expand Down Expand Up @@ -128,8 +128,8 @@ var nameRegex = regexp.MustCompile("^" + NameRegex + "$")
type Schema struct {
storeAccessor func(context.Context) store.KVStore
collectionsOrdered []string
collectionsByPrefix map[string]collection
collectionsByName map[string]collection
collectionsByPrefix map[string]Collection
collectionsByName map[string]Collection
}

// NewSchema creates a new schema for the provided KVStoreService.
Expand Down Expand Up @@ -157,8 +157,8 @@ func NewMemoryStoreSchema(service store.MemoryStoreService) Schema {
func NewSchemaFromAccessor(accessor func(context.Context) store.KVStore) Schema {
return Schema{
storeAccessor: accessor,
collectionsByName: map[string]collection{},
collectionsByPrefix: map[string]collection{},
collectionsByName: map[string]Collection{},
collectionsByPrefix: map[string]Collection{},
}
}

Expand Down Expand Up @@ -279,10 +279,18 @@ func (s Schema) exportGenesis(ctx context.Context, target appmodule.GenesisTarge
return coll.exportGenesis(ctx, wc)
}

func (s Schema) getCollection(name string) (collection, error) {
func (s Schema) getCollection(name string) (Collection, error) {
coll, ok := s.collectionsByName[name]
if !ok {
return nil, fmt.Errorf("unknown collection: %s", name)
}
return coll, nil
}

func (s Schema) ListCollections() []Collection {
colls := make([]Collection, len(s.collectionsOrdered))
for i, name := range s.collectionsOrdered {
colls[i] = s.collectionsByName[name]
}
return colls
}
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,8 @@ require (

// Below are the long-lived replace of the Cosmos SDK
replace (
// TODO: remove me after collections 0.2. is released.
cosmossdk.io/collections => ./collections
cosmossdk.io/core => ./core
cosmossdk.io/store => ./store
// TODO: remove after 0.7.0 release
Expand Down
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,6 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
cosmossdk.io/api v0.4.1 h1:0ikaYM6GyxTYYcfBiyR8YnLCfhNnhKpEFnaSepCTmqg=
cosmossdk.io/api v0.4.1/go.mod h1:jR7k5ok90LxW2lFUXvd8Vpo/dr4PpiyVegxdm7b1ZdE=
cosmossdk.io/collections v0.1.0 h1:nzJGeiq32KnZroSrhB6rPifw4I85Cgmzw/YAmr4luv8=
cosmossdk.io/collections v0.1.0/go.mod h1:xbauc0YsbUF8qKMVeBZl0pFCunxBIhKN/WlxpZ3lBuo=
cosmossdk.io/depinject v1.0.0-alpha.3 h1:6evFIgj//Y3w09bqOUOzEpFj5tsxBqdc5CfkO7z+zfw=
cosmossdk.io/depinject v1.0.0-alpha.3/go.mod h1:eRbcdQ7MRpIPEM5YUJh8k97nxHpYbc3sMUnEtt8HPWU=
cosmossdk.io/errors v1.0.0-beta.7.0.20230429155654-3ee8242364e4 h1:rOy7iw7HlwKc5Af5qIHLXdBx/F98o6du/I/WGwOW6eA=
Expand Down
2 changes: 2 additions & 0 deletions simapp/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,8 @@ replace (

// Below are the long-lived replace of the SimApp
replace (
// TODO: remove me after collections 0.2. is released.
cosmossdk.io/collections => ../collections
cosmossdk.io/core => ../core
// TODO: remove after 0.7.0 release
cosmossdk.io/x/tx => ../x/tx
Expand Down
2 changes: 0 additions & 2 deletions simapp/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -188,8 +188,6 @@ cloud.google.com/go/webrisk v1.4.0/go.mod h1:Hn8X6Zr+ziE2aNd8SliSDWpEnSS1u4R9+xX
cloud.google.com/go/webrisk v1.5.0/go.mod h1:iPG6fr52Tv7sGk0H6qUFzmL3HHZev1htXuWDEEsqMTg=
cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1Vwf+KmJENM0=
cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoISEXH2bcHC3M=
cosmossdk.io/collections v0.1.0 h1:nzJGeiq32KnZroSrhB6rPifw4I85Cgmzw/YAmr4luv8=
cosmossdk.io/collections v0.1.0/go.mod h1:xbauc0YsbUF8qKMVeBZl0pFCunxBIhKN/WlxpZ3lBuo=
cosmossdk.io/depinject v1.0.0-alpha.3 h1:6evFIgj//Y3w09bqOUOzEpFj5tsxBqdc5CfkO7z+zfw=
cosmossdk.io/depinject v1.0.0-alpha.3/go.mod h1:eRbcdQ7MRpIPEM5YUJh8k97nxHpYbc3sMUnEtt8HPWU=
cosmossdk.io/errors v1.0.0-beta.7.0.20230429155654-3ee8242364e4 h1:rOy7iw7HlwKc5Af5qIHLXdBx/F98o6du/I/WGwOW6eA=
Expand Down
2 changes: 2 additions & 0 deletions tests/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,8 @@ replace (

// Below are the long-lived replace for tests.
replace (
// TODO: remove me after collections v0.2.0 is released
cosmossdk.io/collections => ../collections
cosmossdk.io/core => ../core
// We always want to test against the latest version of the simapp.
cosmossdk.io/simapp => ../simapp
Expand Down
2 changes: 0 additions & 2 deletions tests/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -190,8 +190,6 @@ cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1V
cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoISEXH2bcHC3M=
cosmossdk.io/client/v2 v2.0.0-20230309163709-87da587416ba h1:LuPHCncU2KLMNPItFECs709uo46I9wSu2fAWYVCx+/U=
cosmossdk.io/client/v2 v2.0.0-20230309163709-87da587416ba/go.mod h1:SXdwqO7cN5htalh/lhXWP8V4zKtBrhhcSTU+ytuEtmM=
cosmossdk.io/collections v0.1.0 h1:nzJGeiq32KnZroSrhB6rPifw4I85Cgmzw/YAmr4luv8=
cosmossdk.io/collections v0.1.0/go.mod h1:xbauc0YsbUF8qKMVeBZl0pFCunxBIhKN/WlxpZ3lBuo=
cosmossdk.io/depinject v1.0.0-alpha.3 h1:6evFIgj//Y3w09bqOUOzEpFj5tsxBqdc5CfkO7z+zfw=
cosmossdk.io/depinject v1.0.0-alpha.3/go.mod h1:eRbcdQ7MRpIPEM5YUJh8k97nxHpYbc3sMUnEtt8HPWU=
cosmossdk.io/errors v1.0.0-beta.7.0.20230429155654-3ee8242364e4 h1:rOy7iw7HlwKc5Af5qIHLXdBx/F98o6du/I/WGwOW6eA=
Expand Down
Loading

0 comments on commit 69642f6

Please sign in to comment.