From d333fcf14ddaa0112b25ec7ddc50205d861a637f Mon Sep 17 00:00:00 2001 From: godismercilex Date: Tue, 13 Sep 2022 20:38:38 +0200 Subject: [PATCH 01/12] add: collections --- collections/collections.go | 64 +++++++++++ collections/errors.go | 12 ++ collections/item.go | 73 ++++++++++++ collections/keys/keys.go | 45 ++++++++ collections/keys/numeric.go | 38 +++++++ collections/keys/pair.go | 89 +++++++++++++++ collections/keys/range.go | 102 +++++++++++++++++ collections/keys/string.go | 38 +++++++ collections/keys/string_test.go | 59 ++++++++++ collections/keyset.go | 76 +++++++++++++ collections/map.go | 191 ++++++++++++++++++++++++++++++++ collections/map_test.go | 86 ++++++++++++++ collections/sequence.go | 44 ++++++++ collections/sequence_test.go | 23 ++++ collections/test_utils_test.go | 21 ++++ 15 files changed, 961 insertions(+) create mode 100644 collections/collections.go create mode 100644 collections/errors.go create mode 100644 collections/item.go create mode 100644 collections/keys/keys.go create mode 100644 collections/keys/numeric.go create mode 100644 collections/keys/pair.go create mode 100644 collections/keys/range.go create mode 100644 collections/keys/string.go create mode 100644 collections/keys/string_test.go create mode 100644 collections/keyset.go create mode 100644 collections/map.go create mode 100644 collections/map_test.go create mode 100644 collections/sequence.go create mode 100644 collections/sequence_test.go create mode 100644 collections/test_utils_test.go diff --git a/collections/collections.go b/collections/collections.go new file mode 100644 index 000000000..15fbabd38 --- /dev/null +++ b/collections/collections.go @@ -0,0 +1,64 @@ +package collections + +import ( + "bytes" + + "github.com/cosmos/cosmos-sdk/codec" + "github.com/gogo/protobuf/proto" +) + +type Object interface { + codec.ProtoMarshaler +} + +// setObject is used when no object functionality is needed. +type setObject struct{} + +func (n setObject) Reset() { + panic("must never be called") +} + +func (n setObject) String() string { + panic("must never be called") +} + +func (n setObject) ProtoMessage() { + panic("must never be called") +} + +func (n setObject) Marshal() ([]byte, error) { + return []byte{}, nil +} + +func (n setObject) MarshalTo(_ []byte) (_ int, _ error) { + panic("must never be called") +} + +func (n setObject) MarshalToSizedBuffer(_ []byte) (int, error) { + panic("must never be called") +} + +func (n setObject) Size() int { + panic("must never be called") +} + +func (n setObject) Unmarshal(b []byte) error { + if !bytes.Equal(b, []byte{}) { + panic("bad usage") + } + return nil +} + +var _ Object = (*setObject)(nil) + +func typeName(o Object) string { + switch o.(type) { + case *setObject, setObject: + return "no-op-object" + } + n := proto.MessageName(o) + if n == "" { + panic("invalid Object implementation") + } + return n +} diff --git a/collections/errors.go b/collections/errors.go new file mode 100644 index 000000000..ef248d5b6 --- /dev/null +++ b/collections/errors.go @@ -0,0 +1,12 @@ +package collections + +import ( + "errors" + "fmt" +) + +var ErrNotFound = errors.New("collections: not found") + +func notFoundError(name string, key string) error { + return fmt.Errorf("%w object '%s' with key %s", ErrNotFound, name, key) +} diff --git a/collections/item.go b/collections/item.go new file mode 100644 index 000000000..98b12b730 --- /dev/null +++ b/collections/item.go @@ -0,0 +1,73 @@ +package collections + +import ( + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/store/prefix" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// itemKey is a constant byte key which maps an Item object. +var itemKey = []byte{0x0} + +// NewItem instantiates a new Item instance. +func NewItem[V any, PV interface { + *V + Object +}](cdc codec.BinaryCodec, sk sdk.StoreKey, prefix uint8) Item[V, PV] { + return Item[V, PV]{ + prefix: []byte{prefix}, + sk: sk, + cdc: cdc, + typeName: typeName(PV(new(V))), + } +} + +// Item represents a state object which will always have one instance +// of itself saved in the namespace. +// Examples are: +// - config +// - parameters +// - a sequence +type Item[V any, PV interface { + *V + Object +}] struct { + _ V + prefix []byte + sk sdk.StoreKey + cdc codec.BinaryCodec + typeName string +} + +func (i Item[V, PV]) getStore(ctx sdk.Context) sdk.KVStore { + return prefix.NewStore(ctx.KVStore(i.sk), i.prefix) +} + +// Get gets the item V or returns an error. +func (i Item[V, PV]) Get(ctx sdk.Context) (V, error) { + s := i.getStore(ctx) + bytes := s.Get(itemKey) + if bytes == nil { + var v V + return v, notFoundError(i.typeName, "item") + } + + var v V + i.cdc.MustUnmarshal(bytes, PV(&v)) + return v, nil +} + +// GetOr either returns the provided default +// if it's not present in state, or the value found in state. +func (i Item[V, PV]) GetOr(ctx sdk.Context, def V) V { + got, err := i.Get(ctx) + if err != nil { + return def + } + return got +} + +// Set sets the item value to v. +func (i Item[V, PV]) Set(ctx sdk.Context, v V) { + i.getStore(ctx).Set(itemKey, i.cdc.MustMarshal(PV(&v))) +} diff --git a/collections/keys/keys.go b/collections/keys/keys.go new file mode 100644 index 000000000..6ebd14306 --- /dev/null +++ b/collections/keys/keys.go @@ -0,0 +1,45 @@ +package keys + +import ( + "fmt" +) + +// Order defines the ordering of keys. +type Order uint8 + +const ( + OrderAscending Order = iota + OrderDescending +) + +// Key defines a type which can be converted to and from bytes. +// Constraints: +// - It's ordered, meaning, for example: +// StringKey("a").KeyBytes() < StringKey("b").KeyBytes(). +// Int64Key(100).KeyBytes() > Int64Key(-100).KeyBytes() +// - Going back and forth using KeyBytes and FromKeyBytes produces the same results. +// - It's prefix safe, meaning that bytes.Contains(StringKey("a").KeyBytes(), StringKey("aa").KeyBytes()) = false. +type Key interface { + // KeyBytes returns the key as bytes. + KeyBytes() []byte + // FromKeyBytes parses the Key from bytes. + // returns i which is the index of the end of the key. + // Constraint: Key == Self (aka the interface implementer). + // NOTE(mercilex): we in theory should return Key[T any] and constrain + // in the collections.Map, collections.IndexedMap, collections.Set + // that T is in fact the Key itself. + // We don't do it otherwise all our APIs would get messy + // due to golang's compiler type inference. + FromKeyBytes(b []byte) (i int, k Key) + // Stringer is implemented to allow human-readable formats, especially important in errors. + fmt.Stringer +} + +func validString[T ~string](s T) error { + for i, c := range s { + if c == 0 { + return fmt.Errorf("invalid null character at index %d: %s", i, s) + } + } + return nil +} diff --git a/collections/keys/numeric.go b/collections/keys/numeric.go new file mode 100644 index 000000000..99a87416d --- /dev/null +++ b/collections/keys/numeric.go @@ -0,0 +1,38 @@ +package keys + +import ( + "encoding/binary" + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +type Uint8Key uint8 + +func (u Uint8Key) KeyBytes() []byte { + return []byte{uint8(u)} +} + +func (u Uint8Key) FromKeyBytes(b []byte) (i int, k Key) { + return 0, Uint8Key(b[0]) +} + +func (u Uint8Key) String() string { return fmt.Sprintf("%d", u) } + +func Uint64[T ~uint64](u T) Uint64Key { + return Uint64Key(u) +} + +type Uint64Key uint64 + +func (u Uint64Key) KeyBytes() []byte { + return sdk.Uint64ToBigEndian(uint64(u)) +} + +func (u Uint64Key) FromKeyBytes(b []byte) (i int, k Key) { + return 7, Uint64(binary.BigEndian.Uint64(b)) +} + +func (u Uint64Key) String() string { + return fmt.Sprintf("%d", u) +} diff --git a/collections/keys/pair.go b/collections/keys/pair.go new file mode 100644 index 000000000..82c2137d6 --- /dev/null +++ b/collections/keys/pair.go @@ -0,0 +1,89 @@ +package keys + +import ( + "fmt" +) + +// Join joins the two parts of a Pair key. +func Join[K1 Key, K2 Key](k1 K1, k2 K2) Pair[K1, K2] { + return Pair[K1, K2]{ + p1: &k1, + p2: &k2, + } +} + +// PairPrefix is used to provide only the K1 part of the Pair. +// Usually used in Range.Prefix where Key is Pair. +func PairPrefix[K1 Key, K2 Key](k1 K1) Pair[K1, K2] { + return Pair[K1, K2]{ + p1: &k1, + p2: nil, + } +} + +// PairSuffix is used to provide only the K2 part of the Pair. +// Usually used in Range.Start or Range.End where Key is Pair. +func PairSuffix[K1 Key, K2 Key](k2 K2) Pair[K1, K2] { + return Pair[K1, K2]{ + p1: nil, + p2: &k2, + } +} + +// Pair represents a multipart key composed of +// two Key of different or equal types. +type Pair[K1 Key, K2 Key] struct { + // p1 is the first part of the Pair. + p1 *K1 + // p2 is the second part of the Pair. + p2 *K2 +} + +func (t Pair[K1, K2]) fkb1(b []byte) (int, K1) { + var k1 K1 + i, p1 := k1.FromKeyBytes(b) + return i, p1.(K1) +} + +func (t Pair[K1, K2]) fkb2(b []byte) (int, K2) { + var k2 K2 + i, p2 := k2.FromKeyBytes(b) + return i, p2.(K2) +} + +func (t Pair[K1, K2]) FromKeyBytes(b []byte) (int, Key) { + // NOTE(mercilex): is it always safe to assume that when we get a part + // of the key it's going to always contain the full key and not only a part? + i1, k1 := t.fkb1(b) + i2, k2 := t.fkb2(b[i1+1:]) // add one to not pass last index + // add one back as the indexes reported back will start from the last index + 1 + return i1 + i2 + 1, Pair[K1, K2]{ + p1: &k1, + p2: &k2, + } +} + +func (t Pair[K1, K2]) KeyBytes() []byte { + if t.p1 != nil && t.p2 != nil { + return append((*t.p1).KeyBytes(), (*t.p2).KeyBytes()...) + } else if t.p1 != nil && t.p2 == nil { + return (*t.p1).KeyBytes() + } else if t.p1 == nil && t.p2 != nil { + return (*t.p2).KeyBytes() + } else { + panic("empty Pair key") + } +} + +func (t Pair[K1, K2]) String() string { + p1 := "" + p2 := "" + if t.p1 != nil { + p1 = (*t.p1).String() + } + if t.p2 != nil { + p2 = (*t.p2).String() + } + + return fmt.Sprintf("('%s', '%s')", p1, p2) +} diff --git a/collections/keys/range.go b/collections/keys/range.go new file mode 100644 index 000000000..ae5034c4d --- /dev/null +++ b/collections/keys/range.go @@ -0,0 +1,102 @@ +package keys + +// NewRange returns a Range instance +// which iterates over all keys in +// ascending order. +func NewRange[K Key]() Range[K] { + return Range[K]{ + prefix: nil, + start: nil, + end: nil, + order: OrderAscending, + } +} + +// Range defines a range of keys. +type Range[K Key] struct { + prefix *K + start *Bound[K] + end *Bound[K] + order Order +} + +// Prefix sets a fixed prefix for the key range. +func (r Range[K]) Prefix(key K) Range[K] { + r.prefix = &key + return r +} + +// Start sets the start range of the key. +func (r Range[K]) Start(bound Bound[K]) Range[K] { + r.start = &bound + return r +} + +// End sets the end range of the key. +func (r Range[K]) End(bound Bound[K]) Range[K] { + r.end = &bound + return r +} + +// Descending sets the key range to be inverse. +func (r Range[K]) Descending() Range[K] { + r.order = OrderDescending + return r +} + +func (r Range[K]) Compile() (prefix []byte, start []byte, end []byte, order Order) { + order = r.order + if r.prefix != nil { + prefix = (*r.prefix).KeyBytes() + } + if r.start != nil { + start = r.compileStart() + } + if r.end != nil { + end = r.compileEnd() + } + return +} + +func (r Range[K]) compileStart() []byte { + bytes := r.start.value.KeyBytes() + // iterator start is inclusive by default + if !r.start.exclusive { + return bytes + } else { + // TODO(mercilex): exclusive case needs to be handled, consists of decreasing key by 1 + panic("implement me") + } +} + +func (r Range[K]) compileEnd() []byte { + bytes := r.end.value.KeyBytes() + // iterator end is exclusive by default + if r.end.exclusive { + return bytes + } else { + // TODO(mercilex): inclusive case needs to be handled, consists of increasing key by 1 + panic("implement me") + } +} + +type Bound[K Key] struct { + value K + exclusive bool +} + +// Inclusive creates a key Bound which is inclusive. +func Inclusive[K Key](k K) Bound[K] { + return Bound[K]{ + value: k, + exclusive: true, + } +} + +// Exclusive creates a key Bound which is exclusive. +func Exclusive[K Key](k K) Bound[K] { + return Bound[K]{ + value: k, + exclusive: false, + } +} diff --git a/collections/keys/string.go b/collections/keys/string.go new file mode 100644 index 000000000..1aa330ae8 --- /dev/null +++ b/collections/keys/string.go @@ -0,0 +1,38 @@ +package keys + +import ( + "fmt" +) + +// String converts any member of the string typeset into a StringKey +// NOTE(mercilex): this exists to avoid type errors in which bytes are being +// converted to a StringKey which is not correct behavior. +func String[T ~string](v T) StringKey { + return StringKey(v) +} + +type StringKey string + +func (s StringKey) KeyBytes() []byte { + if err := validString(s); err != nil { + panic(fmt.Errorf("invalid StringKey: %w", err)) + } + return append([]byte(s), 0) // null terminate it for safe prefixing +} + +func (s StringKey) FromKeyBytes(b []byte) (int, Key) { + l := len(b) + if l < 2 { + panic("invalid StringKey bytes") + } + for i, c := range b { + if c == 0 { + return i, StringKey(b[:i]) + } + } + panic(fmt.Errorf("StringKey is not null terminated: %s", s)) +} + +func (s StringKey) String() string { + return string(s) +} diff --git a/collections/keys/string_test.go b/collections/keys/string_test.go new file mode 100644 index 000000000..3a8cec40e --- /dev/null +++ b/collections/keys/string_test.go @@ -0,0 +1,59 @@ +package keys + +import ( + "bytes" + "sort" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestStringKey(t *testing.T) { + t.Run("bijective", func(t *testing.T) { + x := StringKey("test") + i, b := x.FromKeyBytes(x.KeyBytes()) + require.Equal(t, x, b) + require.Equal(t, 4, i) + }) + + t.Run("panics", func(t *testing.T) { + // invalid string key + require.Panics(t, func() { + invalid := []byte{0x1, 0x0, 0x3} + StringKey(invalid).KeyBytes() + }) + // invalid bytes do not end with 0x0 + require.Panics(t, func() { + StringKey("").FromKeyBytes([]byte{0x1, 0x2}) + }) + // invalid size + require.Panics(t, func() { + StringKey("").FromKeyBytes([]byte{1}) + }) + }) + + t.Run("proper ordering", func(t *testing.T) { + stringKeys := []StringKey{ + "a", "aa", "b", "c", "dd", + "1", "2", "3", "55", StringKey([]byte{1}), + } + + strings := make([]string, len(stringKeys)) + bytesStringKeys := make([][]byte, len(stringKeys)) + for i, stringKey := range stringKeys { + strings[i] = string(stringKey) + bytesStringKeys[i] = stringKey.KeyBytes() + } + + sort.Strings(strings) + sort.Slice(bytesStringKeys, func(i, j int) bool { + return bytes.Compare(bytesStringKeys[i], bytesStringKeys[j]) < 0 + }) + + for i, b := range bytesStringKeys { + expected := strings[i] + got := string(b[:len(b)-1]) // removes null termination + require.Equal(t, expected, got) + } + }) +} diff --git a/collections/keyset.go b/collections/keyset.go new file mode 100644 index 000000000..e16777551 --- /dev/null +++ b/collections/keyset.go @@ -0,0 +1,76 @@ +package collections + +import ( + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/NibiruChain/nibiru/collections/keys" +) + +// KeySet wraps the default Map, but is used only for +// keys.Key presence and ranging functionalities. +type KeySet[K keys.Key] Map[K, setObject, *setObject] + +// KeySetIterator wraps the default MapIterator, but is used only +// for keys.Key ranging. +type KeySetIterator[K keys.Key] MapIterator[K, setObject, *setObject] + +// NewKeySet instantiates a new KeySet. +func NewKeySet[K keys.Key](cdc codec.BinaryCodec, sk sdk.StoreKey, prefix uint8) KeySet[K] { + return KeySet[K]{ + cdc: cdc, + sk: sk, + prefix: []byte{prefix}, + typeName: typeName(new(setObject)), + } +} + +// Has reports whether the key K is present or not in the set. +func (s KeySet[K]) Has(ctx sdk.Context, k K) bool { + _, err := (Map[K, setObject, *setObject])(s).Get(ctx, k) + return err == nil +} + +// Insert inserts the key K in the set. +func (s KeySet[K]) Insert(ctx sdk.Context, k K) { + (Map[K, setObject, *setObject])(s).Insert(ctx, k, setObject{}) +} + +// Delete deletes the key from the set. +// Does not check if the key exists or not. +func (s KeySet[K]) Delete(ctx sdk.Context, k K) { + _ = (Map[K, setObject, *setObject])(s).Delete(ctx, k) +} + +// Iterate returns a KeySetIterator over the provided keys.Range of keys. +func (s KeySet[K]) Iterate(ctx sdk.Context, r keys.Range[K]) KeySetIterator[K] { + mi := (Map[K, setObject, *setObject])(s).Iterate(ctx, r) + return (KeySetIterator[K])(mi) +} + +// Close closes the KeySetIterator. +// No other operation is valid. +func (s KeySetIterator[K]) Close() { + (MapIterator[K, setObject, *setObject])(s).Close() +} + +// Next moves the iterator onto the next key. +func (s KeySetIterator[K]) Next() { + (MapIterator[K, setObject, *setObject])(s).Next() +} + +// Valid checks if the iterator is still valid. +func (s KeySetIterator[K]) Valid() bool { + return (MapIterator[K, setObject, *setObject])(s).Valid() +} + +// Key returns the current iterator key. +func (s KeySetIterator[K]) Key() K { + return (MapIterator[K, setObject, *setObject])(s).Key() +} + +// Keys consumes the iterator fully and returns all the available keys. +// The KeySetIterator is closed after this operation. +func (s KeySetIterator[K]) Keys() []K { + return (MapIterator[K, setObject, *setObject])(s).Keys() +} diff --git a/collections/map.go b/collections/map.go new file mode 100644 index 000000000..30bbf076e --- /dev/null +++ b/collections/map.go @@ -0,0 +1,191 @@ +package collections + +import ( + "fmt" + + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/store/prefix" + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/NibiruChain/nibiru/collections/keys" +) + +func NewMap[K keys.Key, V any, PV interface { + *V + Object +}](cdc codec.BinaryCodec, sk sdk.StoreKey, prefix uint8) Map[K, V, PV] { + return Map[K, V, PV]{ + cdc: cdc, + sk: sk, + prefix: []byte{prefix}, + typeName: typeName(PV(new(V))), + } +} + +// Map defines a collection which simply does mappings between primary keys and objects. +type Map[K keys.Key, V any, PV interface { + *V + Object +}] struct { + cdc codec.BinaryCodec + sk sdk.StoreKey + prefix []byte + _ K + _ V + typeName string +} + +func (m Map[K, V, PV]) getStore(ctx sdk.Context) sdk.KVStore { + return prefix.NewStore(ctx.KVStore(m.sk), m.prefix) +} + +func (m Map[K, V, PV]) Insert(ctx sdk.Context, key K, object V) { + store := m.getStore(ctx) + store.Set(key.KeyBytes(), m.cdc.MustMarshal(PV(&object))) +} + +func (m Map[K, V, PV]) Get(ctx sdk.Context, key K) (V, error) { + store := m.getStore(ctx) + pk := key.KeyBytes() + bytes := store.Get(pk) + if bytes == nil { + var x V + return x, notFoundError(m.typeName, key.String()) + } + + x := new(V) + m.cdc.MustUnmarshal(bytes, PV(x)) + return *x, nil +} + +func (m Map[K, V, PV]) GetOr(ctx sdk.Context, key K, def V) V { + got, err := m.Get(ctx, key) + if err != nil { + return def + } + + return got +} + +func (m Map[K, V, PV]) Delete(ctx sdk.Context, key K) error { + store := m.getStore(ctx) + pk := key.KeyBytes() + if !store.Has(pk) { + return notFoundError(m.typeName, key.String()) + } + + store.Delete(pk) + return nil +} + +func (m Map[K, V, PV]) Iterate(ctx sdk.Context, r keys.Range[K]) MapIterator[K, V, PV] { + store := m.getStore(ctx) + return newMapIterator[K, V, PV](m.cdc, store, r) +} + +func newMapIterator[K keys.Key, V any, PV interface { + *V + Object +}](cdc codec.BinaryCodec, store sdk.KVStore, r keys.Range[K]) MapIterator[K, V, PV] { + pfx, start, end, order := r.Compile() + + // if prefix is not nil then we replace the current store with a prefixed one + if pfx != nil { + store = prefix.NewStore(store, pfx) + } + switch order { + case keys.OrderAscending: + return MapIterator[K, V, PV]{ + prefix: pfx, + cdc: cdc, + iter: store.Iterator(start, end), + } + case keys.OrderDescending: + return MapIterator[K, V, PV]{ + prefix: pfx, + cdc: cdc, + iter: store.ReverseIterator(start, end), + } + default: + panic(fmt.Errorf("unrecognized order")) + } +} + +type MapIterator[K keys.Key, V any, PV interface { + *V + Object +}] struct { + prefix []byte + cdc codec.BinaryCodec + iter sdk.Iterator +} + +func (i MapIterator[K, V, PV]) Close() { + _ = i.iter.Close() +} + +func (i MapIterator[K, V, PV]) Next() { + i.iter.Next() +} + +func (i MapIterator[K, V, PV]) Valid() bool { + return i.iter.Valid() +} + +func (i MapIterator[K, V, PV]) Value() V { + x := PV(new(V)) + i.cdc.MustUnmarshal(i.iter.Value(), x) + return *x +} + +func (i MapIterator[K, V, PV]) Key() K { + var k K + rawKey := append(i.prefix, i.iter.Key()...) + _, c := k.FromKeyBytes(rawKey) // todo(mercilex): can we assert safety here? + return c.(K) +} + +// TODO doc +func (i MapIterator[K, V, PV]) Values() []V { + defer i.Close() + + var values []V + for ; i.iter.Valid(); i.iter.Next() { + values = append(values, i.Value()) + } + return values +} + +// TODO doc +func (i MapIterator[K, V, PV]) Keys() []K { + defer i.Close() + + var keys []K + for ; i.iter.Valid(); i.iter.Next() { + keys = append(keys, i.Key()) + } + return keys +} + +// todo doc +func (i MapIterator[K, V, PV]) KeyValues() []KeyValue[K, V, PV] { + defer i.Close() + + var kvs []KeyValue[K, V, PV] + for ; i.iter.Valid(); i.iter.Next() { + kvs = append(kvs, KeyValue[K, V, PV]{ + Key: i.Key(), + Value: i.Value(), + }) + } + + return kvs +} + +type KeyValue[K keys.Key, V any, PV interface { + *V + Object +}] struct { + Key K + Value V +} diff --git a/collections/map_test.go b/collections/map_test.go new file mode 100644 index 000000000..b35f8b1ae --- /dev/null +++ b/collections/map_test.go @@ -0,0 +1,86 @@ +package collections + +import ( + "testing" + + wellknown "github.com/gogo/protobuf/types" + "github.com/stretchr/testify/require" + + "github.com/NibiruChain/nibiru/collections/keys" +) + +func obj(o string) wellknown.BytesValue { + return wellknown.BytesValue{Value: []byte(o)} +} + +func kv(o string) KeyValue[keys.StringKey, wellknown.BytesValue, *wellknown.BytesValue] { + return KeyValue[keys.StringKey, wellknown.BytesValue, *wellknown.BytesValue]{ + Key: keys.StringKey(o), + Value: wellknown.BytesValue{Value: []byte(o)}, + } +} + +func TestUpstreamIterAssertions(t *testing.T) { + // ugly but asserts upstream behavior + sk, ctx, _ := deps() + kv := ctx.KVStore(sk) + kv.Set([]byte("hi"), []byte{}) + i := kv.Iterator(nil, nil) + err := i.Close() + require.NoError(t, err) + require.NoError(t, i.Close()) +} + +func TestMap(t *testing.T) { + sk, ctx, cdc := deps() + m := NewMap[keys.StringKey, wellknown.BytesValue, *wellknown.BytesValue](cdc, sk, 0) + + key := keys.String("id") + expected := obj("test") + + // test insert and get + m.Insert(ctx, key, expected) + got, err := m.Get(ctx, key) + require.NoError(t, err) + require.Equal(t, expected, got) + + // test delete and get error + err = m.Delete(ctx, key) + require.NoError(t, err) + _, err = m.Get(ctx, key) + require.ErrorIs(t, err, ErrNotFound) + + // test delete errors not exist + err = m.Delete(ctx, key) + require.ErrorIs(t, err, ErrNotFound) +} + +func TestMap_Iterate(t *testing.T) { + sk, ctx, cdc := deps() + m := NewMap[keys.StringKey, wellknown.BytesValue, *wellknown.BytesValue](cdc, sk, 0) + + objs := []KeyValue[keys.StringKey, wellknown.BytesValue, *wellknown.BytesValue]{kv("a"), kv("aa"), kv("b"), kv("bb")} + + m.Insert(ctx, "a", obj("a")) + m.Insert(ctx, "aa", obj("aa")) + m.Insert(ctx, "b", obj("b")) + m.Insert(ctx, "bb", obj("bb")) + + // test iteration ascending + iter := m.Iterate(ctx, keys.NewRange[keys.StringKey]()) + defer iter.Close() + for i, o := range iter.KeyValues() { + require.Equal(t, objs[i], o) + } + + // test iteration descending + dIter := m.Iterate(ctx, keys.NewRange[keys.StringKey]()) + defer dIter.Close() + for i, o := range iter.KeyValues() { + require.Equal(t, objs[len(objs)-1-i], o) + } + + // test all keys + + // test all values +} diff --git a/collections/sequence.go b/collections/sequence.go new file mode 100644 index 000000000..8df7fa43f --- /dev/null +++ b/collections/sequence.go @@ -0,0 +1,44 @@ +package collections + +import ( + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + wellknown "github.com/gogo/protobuf/types" +) + +// DefaultSequenceStart is the initial starting number of the Sequence. +const DefaultSequenceStart uint64 = 1 + +// Sequence defines a collection item which contains an always increasing number. +// Useful for those flows which require ever raising unique ids. +type Sequence struct { + sequence Item[wellknown.UInt64Value, *wellknown.UInt64Value] +} + +// NewSequence instantiates a new sequence object. +func NewSequence(cdc codec.BinaryCodec, sk sdk.StoreKey, prefix uint8) Sequence { + return Sequence{ + sequence: NewItem[wellknown.UInt64Value](cdc, sk, prefix), + } +} + +// Next returns the next available sequence number +// and also increases the sequence number count. +func (s Sequence) Next(ctx sdk.Context) uint64 { + // get current + seq := s.Peek(ctx) + // increase + s.sequence.Set(ctx, wellknown.UInt64Value{Value: seq + 1}) + // return current + return seq +} + +// Peek gets the next available sequence number without increasing it. +func (s Sequence) Peek(ctx sdk.Context) uint64 { + return s.sequence.GetOr(ctx, wellknown.UInt64Value{Value: DefaultSequenceStart}).Value +} + +// Set hard resets the sequence to the provided number. +func (s Sequence) Set(ctx sdk.Context, u uint64) { + s.sequence.Set(ctx, wellknown.UInt64Value{Value: u}) +} diff --git a/collections/sequence_test.go b/collections/sequence_test.go new file mode 100644 index 000000000..288e82661 --- /dev/null +++ b/collections/sequence_test.go @@ -0,0 +1,23 @@ +package collections + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestSequence(t *testing.T) { + sk, ctx, cdc := deps() + + s := NewSequence(cdc, sk, 0) + // assert initial start number + require.Equal(t, DefaultSequenceStart, s.Peek(ctx)) + // assert next reports the default sequence start number + i := s.Next(ctx) + require.Equal(t, DefaultSequenceStart, i) + // assert if we peek next number is DefaultSequenceStart + 1 + require.Equal(t, DefaultSequenceStart+1, s.Peek(ctx)) + // assert set correctly does hard reset + s.Set(ctx, 100) + require.Equal(t, uint64(100), s.Peek(ctx)) +} diff --git a/collections/test_utils_test.go b/collections/test_utils_test.go new file mode 100644 index 000000000..06ca31666 --- /dev/null +++ b/collections/test_utils_test.go @@ -0,0 +1,21 @@ +package collections + +import ( + "github.com/cosmos/cosmos-sdk/codec" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/store" + "github.com/cosmos/cosmos-sdk/store/types" + sdk "github.com/cosmos/cosmos-sdk/types" + db "github.com/tendermint/tm-db" +) + +func deps() (sdk.StoreKey, sdk.Context, codec.BinaryCodec) { + sk := sdk.NewKVStoreKey("mock") + dbm := db.NewMemDB() + ms := store.NewCommitMultiStore(dbm) + ms.MountStoreWithDB(sk, types.StoreTypeIAVL, dbm) + if err := ms.LoadLatestVersion(); err != nil { + panic(err) + } + return sk, sdk.Context{}.WithMultiStore(ms).WithGasMeter(sdk.NewGasMeter(1_000_000_000)), codec.NewProtoCodec(codectypes.NewInterfaceRegistry()) +} From 1bf967eec549698de3b84bda5d38961557216e72 Mon Sep 17 00:00:00 2001 From: godismercilex Date: Tue, 13 Sep 2022 20:39:44 +0200 Subject: [PATCH 02/12] add pair_test --- collections/keys/pair_test.go | 39 +++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 collections/keys/pair_test.go diff --git a/collections/keys/pair_test.go b/collections/keys/pair_test.go new file mode 100644 index 000000000..06c7c8242 --- /dev/null +++ b/collections/keys/pair_test.go @@ -0,0 +1,39 @@ +package keys + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestPair(t *testing.T) { + // we only care about bijectivity + // as Pair is strictly K1, K2 implementation reliant. + + t.Run("joined", func(t *testing.T) { + p := Join(StringKey("hi"), Join(StringKey("hi"), StringKey("hi"))) + bytes := p.KeyBytes() + idx, result := p.FromKeyBytes(bytes) + require.Equalf(t, p, result, "%s <-> %s", p.String(), result.String()) + require.Equal(t, len(bytes)-1, idx) + }) + + t.Run("pair prefix", func(t *testing.T) { + k1 := StringKey("hi") + prefix := PairPrefix[StringKey, Uint64Key](k1) + require.Equal(t, k1.KeyBytes(), prefix.KeyBytes()) + }) + + t.Run("pair suffix", func(t *testing.T) { + k1 := Uint64Key(10) + suffix := PairSuffix[StringKey, Uint64Key](k1) + require.Equal(t, k1.KeyBytes(), suffix.KeyBytes()) + }) + + t.Run("empty", func(t *testing.T) { + var p Pair[StringKey, StringKey] + require.Panics(t, func() { + p.KeyBytes() + }) + }) +} From 4496551c675d83c6a17a537a667fae8d9089d535 Mon Sep 17 00:00:00 2001 From: godismercilex Date: Tue, 13 Sep 2022 20:43:08 +0200 Subject: [PATCH 03/12] chore: CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c0c9731f3..de0c81c8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +* [#894](https://github.com/NibiruChain/nibiru/pull/894) - add the collections package! + ### State Machine Breaking * [#872](https://github.com/NibiruChain/nibiru/pull/872) - x/perp remove module balances from genesis From 43940e84a946333f501e86df23b0d28c964aed32 Mon Sep 17 00:00:00 2001 From: godismercilex Date: Wed, 14 Sep 2022 08:40:23 +0200 Subject: [PATCH 04/12] add: finish range API and range testing --- collections/keys/range.go | 37 ++++++++---- .../keys/range_sdk_integration_test.go | 58 +++++++++++++++++++ collections/keys/range_test.go | 7 +++ 3 files changed, 90 insertions(+), 12 deletions(-) create mode 100644 collections/keys/range_sdk_integration_test.go create mode 100644 collections/keys/range_test.go diff --git a/collections/keys/range.go b/collections/keys/range.go index ae5034c4d..7b30ce3df 100644 --- a/collections/keys/range.go +++ b/collections/keys/range.go @@ -61,42 +61,55 @@ func (r Range[K]) Compile() (prefix []byte, start []byte, end []byte, order Orde func (r Range[K]) compileStart() []byte { bytes := r.start.value.KeyBytes() // iterator start is inclusive by default - if !r.start.exclusive { + if r.start.bound == boundInclusive { return bytes + } else if r.start.bound == boundExclusive { + return extendOneByte(bytes) } else { - // TODO(mercilex): exclusive case needs to be handled, consists of decreasing key by 1 - panic("implement me") + panic("unreachable") } } func (r Range[K]) compileEnd() []byte { bytes := r.end.value.KeyBytes() // iterator end is exclusive by default - if r.end.exclusive { + if r.end.bound == boundExclusive { return bytes + } else if r.end.bound == boundInclusive { + return extendOneByte(bytes) } else { - // TODO(mercilex): inclusive case needs to be handled, consists of increasing key by 1 - panic("implement me") + panic("unreachable") } } +func extendOneByte(b []byte) []byte { + return append(b, 0) +} + +type bound = uint8 + +const ( + boundInclusive = iota + boundExclusive +) + type Bound[K Key] struct { - value K - exclusive bool + value K + bound bound } // Inclusive creates a key Bound which is inclusive. func Inclusive[K Key](k K) Bound[K] { return Bound[K]{ - value: k, - exclusive: true, + value: k, + bound: boundInclusive, } } // Exclusive creates a key Bound which is exclusive. func Exclusive[K Key](k K) Bound[K] { return Bound[K]{ - value: k, - exclusive: false, + value: k, + bound: boundExclusive, } } diff --git a/collections/keys/range_sdk_integration_test.go b/collections/keys/range_sdk_integration_test.go new file mode 100644 index 000000000..6f44b21f3 --- /dev/null +++ b/collections/keys/range_sdk_integration_test.go @@ -0,0 +1,58 @@ +package keys_test + +import ( + "github.com/NibiruChain/nibiru/collections" + "github.com/NibiruChain/nibiru/collections/keys" + "github.com/cosmos/cosmos-sdk/codec" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/store" + "github.com/cosmos/cosmos-sdk/store/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + db "github.com/tendermint/tm-db" + "testing" +) + +// deps is repeated but, don't want to create cross pkg dependencies +func deps() (sdk.StoreKey, sdk.Context, codec.BinaryCodec) { + sk := sdk.NewKVStoreKey("mock") + dbm := db.NewMemDB() + ms := store.NewCommitMultiStore(dbm) + ms.MountStoreWithDB(sk, types.StoreTypeIAVL, dbm) + if err := ms.LoadLatestVersion(); err != nil { + panic(err) + } + return sk, sdk.Context{}.WithMultiStore(ms).WithGasMeter(sdk.NewGasMeter(1_000_000_000)), codec.NewProtoCodec(codectypes.NewInterfaceRegistry()) +} + +func TestRangeBounds(t *testing.T) { + sk, ctx, cdc := deps() + + ks := collections.NewKeySet[keys.Uint64Key](cdc, sk, 0) + + ks.Insert(ctx, 1) + ks.Insert(ctx, 2) + ks.Insert(ctx, 3) + ks.Insert(ctx, 4) + ks.Insert(ctx, 5) + ks.Insert(ctx, 6) + + // let's range (1-5]; expected: 2..5 + start := keys.Exclusive[keys.Uint64Key](1) + end := keys.Inclusive[keys.Uint64Key](5) + rng := keys.NewRange[keys.Uint64Key]().Start(start).End(end) + result := ks.Iterate(ctx, rng).Keys() + require.Equal(t, []keys.Uint64Key{2, 3, 4, 5}, result) + + // let's range [1-5); expected 1..4 + start = keys.Inclusive[keys.Uint64Key](1) + end = keys.Exclusive[keys.Uint64Key](5) + rng = keys.NewRange[keys.Uint64Key]().Start(start).End(end) + result = ks.Iterate(ctx, rng).Keys() + require.Equal(t, []keys.Uint64Key{1, 2, 3, 4}, result) + + // let's range [1-5) descending; expected 4..1 + rng = keys.NewRange[keys.Uint64Key]().Start(start).End(end).Descending() + result = ks.Iterate(ctx, rng).Keys() + require.Equal(t, []keys.Uint64Key{4, 3, 2, 1}, result) +} diff --git a/collections/keys/range_test.go b/collections/keys/range_test.go new file mode 100644 index 000000000..ed36360e6 --- /dev/null +++ b/collections/keys/range_test.go @@ -0,0 +1,7 @@ +package keys + +import "testing" + +func TestRange(t *testing.T) { + +} From ff1f0d90dc1e618d907728b3c7b408eaaf70cfae Mon Sep 17 00:00:00 2001 From: godismercilex Date: Wed, 14 Sep 2022 08:51:48 +0200 Subject: [PATCH 05/12] remove: unused file --- collections/keys/range_test.go | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 collections/keys/range_test.go diff --git a/collections/keys/range_test.go b/collections/keys/range_test.go deleted file mode 100644 index ed36360e6..000000000 --- a/collections/keys/range_test.go +++ /dev/null @@ -1,7 +0,0 @@ -package keys - -import "testing" - -func TestRange(t *testing.T) { - -} From 71e0c217075d9e133a80bd96fa525b4474ca866a Mon Sep 17 00:00:00 2001 From: godismercilex Date: Wed, 14 Sep 2022 08:56:49 +0200 Subject: [PATCH 06/12] chore: lint --- collections/keys/range_sdk_integration_test.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/collections/keys/range_sdk_integration_test.go b/collections/keys/range_sdk_integration_test.go index 6f44b21f3..7d3441967 100644 --- a/collections/keys/range_sdk_integration_test.go +++ b/collections/keys/range_sdk_integration_test.go @@ -1,8 +1,8 @@ package keys_test import ( - "github.com/NibiruChain/nibiru/collections" - "github.com/NibiruChain/nibiru/collections/keys" + "testing" + "github.com/cosmos/cosmos-sdk/codec" codectypes "github.com/cosmos/cosmos-sdk/codec/types" "github.com/cosmos/cosmos-sdk/store" @@ -10,7 +10,9 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" db "github.com/tendermint/tm-db" - "testing" + + "github.com/NibiruChain/nibiru/collections" + "github.com/NibiruChain/nibiru/collections/keys" ) // deps is repeated but, don't want to create cross pkg dependencies From f67685d211d508073ce42a48c9ddafc472621877 Mon Sep 17 00:00:00 2001 From: godismercilex Date: Wed, 14 Sep 2022 19:27:54 +0200 Subject: [PATCH 07/12] change: simplify object API allow keys to be values --- collections/collections.go | 62 +++++++++++++++++++++++-------------- collections/item.go | 8 ++--- collections/keys/numeric.go | 24 ++++++++++++++ collections/keys/string.go | 11 +++++++ collections/keyset.go | 2 +- collections/map.go | 14 ++++----- 6 files changed, 86 insertions(+), 35 deletions(-) diff --git a/collections/collections.go b/collections/collections.go index 15fbabd38..215a13365 100644 --- a/collections/collections.go +++ b/collections/collections.go @@ -4,42 +4,56 @@ import ( "bytes" "github.com/cosmos/cosmos-sdk/codec" - "github.com/gogo/protobuf/proto" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" ) +// Object defines an object which can marshal and unmarshal itself to and from bytes. type Object interface { - codec.ProtoMarshaler + // Marshal marshals the object into bytes. + Marshal() (b []byte, err error) + // Unmarshal populates the object from bytes. + Unmarshal(b []byte) error } -// setObject is used when no object functionality is needed. -type setObject struct{} - -func (n setObject) Reset() { - panic("must never be called") +// storeCodec implements only the subset of functionalities +// required for the ser/de at state layer. +// It respects cosmos-sdk guarantees around interface unpacking. +type storeCodec struct { + ir codectypes.InterfaceRegistry } -func (n setObject) String() string { - panic("must never be called") +func newStoreCodec(cdc codec.BinaryCodec) storeCodec { + return storeCodec{ir: cdc.(*codec.ProtoCodec).InterfaceRegistry()} } -func (n setObject) ProtoMessage() { - panic("must never be called") +func (c storeCodec) marshal(o Object) []byte { + bytes, err := o.Marshal() + if err != nil { + panic(err) + } + return bytes } -func (n setObject) Marshal() ([]byte, error) { - return []byte{}, nil +func (c storeCodec) unmarshal(bytes []byte, o Object) { + err := o.Unmarshal(bytes) + if err != nil { + panic(err) + } + err = codectypes.UnpackInterfaces(o, c.ir) + if err != nil { + panic(err) + } } -func (n setObject) MarshalTo(_ []byte) (_ int, _ error) { - panic("must never be called") -} +// setObject is used when no object functionality is needed. +type setObject struct{} -func (n setObject) MarshalToSizedBuffer(_ []byte) (int, error) { +func (n setObject) String() string { panic("must never be called") } -func (n setObject) Size() int { - panic("must never be called") +func (n setObject) Marshal() ([]byte, error) { + return []byte{}, nil } func (n setObject) Unmarshal(b []byte) error { @@ -56,9 +70,11 @@ func typeName(o Object) string { case *setObject, setObject: return "no-op-object" } - n := proto.MessageName(o) - if n == "" { - panic("invalid Object implementation") + type xname interface { + XXX_MessageName() string + } + if m, ok := o.(xname); ok { + return m.XXX_MessageName() } - return n + panic("invalid proto message") } diff --git a/collections/item.go b/collections/item.go index 98b12b730..aeaee48fb 100644 --- a/collections/item.go +++ b/collections/item.go @@ -17,7 +17,7 @@ func NewItem[V any, PV interface { return Item[V, PV]{ prefix: []byte{prefix}, sk: sk, - cdc: cdc, + cdc: newStoreCodec(cdc), typeName: typeName(PV(new(V))), } } @@ -35,7 +35,7 @@ type Item[V any, PV interface { _ V prefix []byte sk sdk.StoreKey - cdc codec.BinaryCodec + cdc storeCodec typeName string } @@ -53,7 +53,7 @@ func (i Item[V, PV]) Get(ctx sdk.Context) (V, error) { } var v V - i.cdc.MustUnmarshal(bytes, PV(&v)) + i.cdc.unmarshal(bytes, PV(&v)) return v, nil } @@ -69,5 +69,5 @@ func (i Item[V, PV]) GetOr(ctx sdk.Context, def V) V { // Set sets the item value to v. func (i Item[V, PV]) Set(ctx sdk.Context, v V) { - i.getStore(ctx).Set(itemKey, i.cdc.MustMarshal(PV(&v))) + i.getStore(ctx).Set(itemKey, i.cdc.marshal(PV(&v))) } diff --git a/collections/keys/numeric.go b/collections/keys/numeric.go index 99a87416d..85f2ed58d 100644 --- a/collections/keys/numeric.go +++ b/collections/keys/numeric.go @@ -19,6 +19,18 @@ func (u Uint8Key) FromKeyBytes(b []byte) (i int, k Key) { func (u Uint8Key) String() string { return fmt.Sprintf("%d", u) } +func (u Uint8Key) Marshal() ([]byte, error) { + return []byte{uint8(u)}, nil +} + +func (u *Uint8Key) Unmarshal(b []byte) error { + if len(b) != 1 { + return fmt.Errorf("invalid bytes type for Uint8Key") + } + *u = Uint8Key(b[0]) + return nil +} + func Uint64[T ~uint64](u T) Uint64Key { return Uint64Key(u) } @@ -36,3 +48,15 @@ func (u Uint64Key) FromKeyBytes(b []byte) (i int, k Key) { func (u Uint64Key) String() string { return fmt.Sprintf("%d", u) } + +func (u Uint64Key) Marshal() ([]byte, error) { + return sdk.Uint64ToBigEndian(uint64(u)), nil +} + +func (u *Uint64Key) Unmarshal(b []byte) error { + if len(b) != 8 { + return fmt.Errorf("invalid bytes type for Uint64Key") + } + *u = Uint64(binary.BigEndian.Uint64(b)) + return nil +} diff --git a/collections/keys/string.go b/collections/keys/string.go index 1aa330ae8..d1fa4a189 100644 --- a/collections/keys/string.go +++ b/collections/keys/string.go @@ -36,3 +36,14 @@ func (s StringKey) FromKeyBytes(b []byte) (int, Key) { func (s StringKey) String() string { return string(s) } + +// permit strings to be used as collections.Object + +func (s StringKey) Marshal() ([]byte, error) { + return []byte(s), nil +} + +func (s *StringKey) Unmarshal(b []byte) error { + *s = StringKey(b) + return validString(*s) +} diff --git a/collections/keyset.go b/collections/keyset.go index e16777551..eec82dbb1 100644 --- a/collections/keyset.go +++ b/collections/keyset.go @@ -18,7 +18,7 @@ type KeySetIterator[K keys.Key] MapIterator[K, setObject, *setObject] // NewKeySet instantiates a new KeySet. func NewKeySet[K keys.Key](cdc codec.BinaryCodec, sk sdk.StoreKey, prefix uint8) KeySet[K] { return KeySet[K]{ - cdc: cdc, + cdc: newStoreCodec(cdc), sk: sk, prefix: []byte{prefix}, typeName: typeName(new(setObject)), diff --git a/collections/map.go b/collections/map.go index 30bbf076e..a99ce0735 100644 --- a/collections/map.go +++ b/collections/map.go @@ -15,7 +15,7 @@ func NewMap[K keys.Key, V any, PV interface { Object }](cdc codec.BinaryCodec, sk sdk.StoreKey, prefix uint8) Map[K, V, PV] { return Map[K, V, PV]{ - cdc: cdc, + cdc: newStoreCodec(cdc), sk: sk, prefix: []byte{prefix}, typeName: typeName(PV(new(V))), @@ -27,7 +27,7 @@ type Map[K keys.Key, V any, PV interface { *V Object }] struct { - cdc codec.BinaryCodec + cdc storeCodec sk sdk.StoreKey prefix []byte _ K @@ -41,7 +41,7 @@ func (m Map[K, V, PV]) getStore(ctx sdk.Context) sdk.KVStore { func (m Map[K, V, PV]) Insert(ctx sdk.Context, key K, object V) { store := m.getStore(ctx) - store.Set(key.KeyBytes(), m.cdc.MustMarshal(PV(&object))) + store.Set(key.KeyBytes(), m.cdc.marshal(PV(&object))) } func (m Map[K, V, PV]) Get(ctx sdk.Context, key K) (V, error) { @@ -54,7 +54,7 @@ func (m Map[K, V, PV]) Get(ctx sdk.Context, key K) (V, error) { } x := new(V) - m.cdc.MustUnmarshal(bytes, PV(x)) + m.cdc.unmarshal(bytes, PV(x)) return *x, nil } @@ -86,7 +86,7 @@ func (m Map[K, V, PV]) Iterate(ctx sdk.Context, r keys.Range[K]) MapIterator[K, func newMapIterator[K keys.Key, V any, PV interface { *V Object -}](cdc codec.BinaryCodec, store sdk.KVStore, r keys.Range[K]) MapIterator[K, V, PV] { +}](cdc storeCodec, store sdk.KVStore, r keys.Range[K]) MapIterator[K, V, PV] { pfx, start, end, order := r.Compile() // if prefix is not nil then we replace the current store with a prefixed one @@ -116,7 +116,7 @@ type MapIterator[K keys.Key, V any, PV interface { Object }] struct { prefix []byte - cdc codec.BinaryCodec + cdc storeCodec iter sdk.Iterator } @@ -134,7 +134,7 @@ func (i MapIterator[K, V, PV]) Valid() bool { func (i MapIterator[K, V, PV]) Value() V { x := PV(new(V)) - i.cdc.MustUnmarshal(i.iter.Value(), x) + i.cdc.unmarshal(i.iter.Value(), x) return *x } From 4d22ffab109843621b5f866feca07a67334a5c40 Mon Sep 17 00:00:00 2001 From: godismercilex Date: Wed, 14 Sep 2022 20:35:53 +0200 Subject: [PATCH 08/12] change: address reviews --- collections/keys/pair_test.go | 6 +++--- collections/keys/range.go | 31 ++++++++++--------------------- 2 files changed, 13 insertions(+), 24 deletions(-) diff --git a/collections/keys/pair_test.go b/collections/keys/pair_test.go index 06c7c8242..5ec146b8f 100644 --- a/collections/keys/pair_test.go +++ b/collections/keys/pair_test.go @@ -25,9 +25,9 @@ func TestPair(t *testing.T) { }) t.Run("pair suffix", func(t *testing.T) { - k1 := Uint64Key(10) - suffix := PairSuffix[StringKey, Uint64Key](k1) - require.Equal(t, k1.KeyBytes(), suffix.KeyBytes()) + k2 := Uint64Key(10) + suffix := PairSuffix[StringKey, Uint64Key](k2) + require.Equal(t, k2.KeyBytes(), suffix.KeyBytes()) }) t.Run("empty", func(t *testing.T) { diff --git a/collections/keys/range.go b/collections/keys/range.go index 7b30ce3df..3d860be7e 100644 --- a/collections/keys/range.go +++ b/collections/keys/range.go @@ -61,24 +61,20 @@ func (r Range[K]) Compile() (prefix []byte, start []byte, end []byte, order Orde func (r Range[K]) compileStart() []byte { bytes := r.start.value.KeyBytes() // iterator start is inclusive by default - if r.start.bound == boundInclusive { + if r.start.inclusive { return bytes - } else if r.start.bound == boundExclusive { - return extendOneByte(bytes) } else { - panic("unreachable") + return extendOneByte(bytes) } } func (r Range[K]) compileEnd() []byte { bytes := r.end.value.KeyBytes() // iterator end is exclusive by default - if r.end.bound == boundExclusive { + if !r.end.inclusive { return bytes - } else if r.end.bound == boundInclusive { - return extendOneByte(bytes) } else { - panic("unreachable") + return extendOneByte(bytes) } } @@ -86,30 +82,23 @@ func extendOneByte(b []byte) []byte { return append(b, 0) } -type bound = uint8 - -const ( - boundInclusive = iota - boundExclusive -) - type Bound[K Key] struct { - value K - bound bound + value K + inclusive bool } // Inclusive creates a key Bound which is inclusive. func Inclusive[K Key](k K) Bound[K] { return Bound[K]{ - value: k, - bound: boundInclusive, + value: k, + inclusive: true, } } // Exclusive creates a key Bound which is exclusive. func Exclusive[K Key](k K) Bound[K] { return Bound[K]{ - value: k, - bound: boundExclusive, + value: k, + inclusive: false, } } From 3603bf28442d5d4fa49e58eb77b28d5cd6bd3d56 Mon Sep 17 00:00:00 2001 From: godismercilex Date: Wed, 14 Sep 2022 20:40:54 +0200 Subject: [PATCH 09/12] change: address reviews 2 --- collections/keys/keys.go | 14 +++++++------- collections/keys/numeric.go | 4 ++-- collections/keys/pair.go | 5 ++--- collections/keys/pair_test.go | 2 +- collections/keys/string.go | 2 +- collections/keys/string_test.go | 2 +- 6 files changed, 14 insertions(+), 15 deletions(-) diff --git a/collections/keys/keys.go b/collections/keys/keys.go index 6ebd14306..e84dc036c 100644 --- a/collections/keys/keys.go +++ b/collections/keys/keys.go @@ -14,23 +14,23 @@ const ( // Key defines a type which can be converted to and from bytes. // Constraints: -// - It's ordered, meaning, for example: -// StringKey("a").KeyBytes() < StringKey("b").KeyBytes(). -// Int64Key(100).KeyBytes() > Int64Key(-100).KeyBytes() -// - Going back and forth using KeyBytes and FromKeyBytes produces the same results. -// - It's prefix safe, meaning that bytes.Contains(StringKey("a").KeyBytes(), StringKey("aa").KeyBytes()) = false. +// - It's ordered, meaning, for example: +// StringKey("a").KeyBytes() < StringKey("b").KeyBytes(). +// Int64Key(100).KeyBytes() > Int64Key(-100).KeyBytes() +// - Going back and forth using KeyBytes and FromKeyBytes produces the same results. +// - It's prefix safe, meaning that bytes.Contains(StringKey("a").KeyBytes(), StringKey("aa").KeyBytes()) = false. type Key interface { // KeyBytes returns the key as bytes. KeyBytes() []byte // FromKeyBytes parses the Key from bytes. - // returns i which is the index of the end of the key. + // returns i which is the numbers of bytes read from the buffer. // Constraint: Key == Self (aka the interface implementer). // NOTE(mercilex): we in theory should return Key[T any] and constrain // in the collections.Map, collections.IndexedMap, collections.Set // that T is in fact the Key itself. // We don't do it otherwise all our APIs would get messy // due to golang's compiler type inference. - FromKeyBytes(b []byte) (i int, k Key) + FromKeyBytes(buf []byte) (i int, k Key) // Stringer is implemented to allow human-readable formats, especially important in errors. fmt.Stringer } diff --git a/collections/keys/numeric.go b/collections/keys/numeric.go index 85f2ed58d..259a9fb53 100644 --- a/collections/keys/numeric.go +++ b/collections/keys/numeric.go @@ -14,7 +14,7 @@ func (u Uint8Key) KeyBytes() []byte { } func (u Uint8Key) FromKeyBytes(b []byte) (i int, k Key) { - return 0, Uint8Key(b[0]) + return 1, Uint8Key(b[0]) } func (u Uint8Key) String() string { return fmt.Sprintf("%d", u) } @@ -42,7 +42,7 @@ func (u Uint64Key) KeyBytes() []byte { } func (u Uint64Key) FromKeyBytes(b []byte) (i int, k Key) { - return 7, Uint64(binary.BigEndian.Uint64(b)) + return 8, Uint64(binary.BigEndian.Uint64(b)) } func (u Uint64Key) String() string { diff --git a/collections/keys/pair.go b/collections/keys/pair.go index 82c2137d6..53a21ebc6 100644 --- a/collections/keys/pair.go +++ b/collections/keys/pair.go @@ -55,9 +55,8 @@ func (t Pair[K1, K2]) FromKeyBytes(b []byte) (int, Key) { // NOTE(mercilex): is it always safe to assume that when we get a part // of the key it's going to always contain the full key and not only a part? i1, k1 := t.fkb1(b) - i2, k2 := t.fkb2(b[i1+1:]) // add one to not pass last index - // add one back as the indexes reported back will start from the last index + 1 - return i1 + i2 + 1, Pair[K1, K2]{ + i2, k2 := t.fkb2(b[i1:]) + return i1 + i2, Pair[K1, K2]{ p1: &k1, p2: &k2, } diff --git a/collections/keys/pair_test.go b/collections/keys/pair_test.go index 5ec146b8f..27201bedf 100644 --- a/collections/keys/pair_test.go +++ b/collections/keys/pair_test.go @@ -15,7 +15,7 @@ func TestPair(t *testing.T) { bytes := p.KeyBytes() idx, result := p.FromKeyBytes(bytes) require.Equalf(t, p, result, "%s <-> %s", p.String(), result.String()) - require.Equal(t, len(bytes)-1, idx) + require.Equal(t, len(bytes), idx) }) t.Run("pair prefix", func(t *testing.T) { diff --git a/collections/keys/string.go b/collections/keys/string.go index d1fa4a189..a60e4288c 100644 --- a/collections/keys/string.go +++ b/collections/keys/string.go @@ -27,7 +27,7 @@ func (s StringKey) FromKeyBytes(b []byte) (int, Key) { } for i, c := range b { if c == 0 { - return i, StringKey(b[:i]) + return i + 1, StringKey(b[:i]) } } panic(fmt.Errorf("StringKey is not null terminated: %s", s)) diff --git a/collections/keys/string_test.go b/collections/keys/string_test.go index 3a8cec40e..e80175fde 100644 --- a/collections/keys/string_test.go +++ b/collections/keys/string_test.go @@ -13,7 +13,7 @@ func TestStringKey(t *testing.T) { x := StringKey("test") i, b := x.FromKeyBytes(x.KeyBytes()) require.Equal(t, x, b) - require.Equal(t, 4, i) + require.Equal(t, 5, i) }) t.Run("panics", func(t *testing.T) { From 49a3219e5f857ccffe2f2c339d8545a00cc6da86 Mon Sep 17 00:00:00 2001 From: godismercilex Date: Wed, 14 Sep 2022 21:30:00 +0200 Subject: [PATCH 10/12] change: typeName logic --- collections/collections.go | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/collections/collections.go b/collections/collections.go index 215a13365..498dc506b 100644 --- a/collections/collections.go +++ b/collections/collections.go @@ -2,6 +2,7 @@ package collections import ( "bytes" + "github.com/gogo/protobuf/proto" "github.com/cosmos/cosmos-sdk/codec" codectypes "github.com/cosmos/cosmos-sdk/codec/types" @@ -70,11 +71,9 @@ func typeName(o Object) string { case *setObject, setObject: return "no-op-object" } - type xname interface { - XXX_MessageName() string + pm, ok := o.(proto.Message) + if !ok { + return "unknown" } - if m, ok := o.(xname); ok { - return m.XXX_MessageName() - } - panic("invalid proto message") + return proto.MessageName(pm) } From 23281cba364186e622d2b52786adb8c7199b77f5 Mon Sep 17 00:00:00 2001 From: godismercilex Date: Wed, 14 Sep 2022 21:31:02 +0200 Subject: [PATCH 11/12] chore: doc --- collections/collections.go | 1 + 1 file changed, 1 insertion(+) diff --git a/collections/collections.go b/collections/collections.go index 498dc506b..aadc243bc 100644 --- a/collections/collections.go +++ b/collections/collections.go @@ -66,6 +66,7 @@ func (n setObject) Unmarshal(b []byte) error { var _ Object = (*setObject)(nil) +// TODO(mercilex): improve typeName api func typeName(o Object) string { switch o.(type) { case *setObject, setObject: From 8f769370c85ff2195fd921a2240a8df5393ded4f Mon Sep 17 00:00:00 2001 From: godismercilex Date: Wed, 14 Sep 2022 21:41:15 +0200 Subject: [PATCH 12/12] chore: lint --- collections/collections.go | 1 + collections/item.go | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/collections/collections.go b/collections/collections.go index aadc243bc..ae1addb9d 100644 --- a/collections/collections.go +++ b/collections/collections.go @@ -2,6 +2,7 @@ package collections import ( "bytes" + "github.com/gogo/protobuf/proto" "github.com/cosmos/cosmos-sdk/codec" diff --git a/collections/item.go b/collections/item.go index aeaee48fb..cb962973f 100644 --- a/collections/item.go +++ b/collections/item.go @@ -25,9 +25,9 @@ func NewItem[V any, PV interface { // Item represents a state object which will always have one instance // of itself saved in the namespace. // Examples are: -// - config -// - parameters -// - a sequence +// - config +// - parameters +// - a sequence type Item[V any, PV interface { *V Object