Skip to content

Commit

Permalink
feat(collections): Initialise core (Prefix, KeyEncoder, ValueEncoder,…
Browse files Browse the repository at this point in the history
… Map) (#14134)

Co-authored-by: testinginprod <[email protected]>
Co-authored-by: Aaron Craelius <[email protected]>
  • Loading branch information
3 people authored Dec 8, 2022
1 parent bd59987 commit fca24b6
Show file tree
Hide file tree
Showing 14 changed files with 932 additions and 0 deletions.
22 changes: 22 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,28 @@ jobs:
with:
projectBaseDir: tools/rosetta/

test-collections:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version: 1.19.4
cache: true
cache-dependency-path: collections/go.sum
- uses: technote-space/[email protected]
id: git_diff
with:
PATTERNS: |
collections/**/*.go
collections/go.mod
collections/go.sum
- name: tests
if: env.GIT_DIFF
run: |
cd collections
go test -mod=readonly -timeout 30m -coverprofile=coverage.out -covermode=atomic -tags='norace ledger test_ledger_mock rocksdb_build' ./...
test-cosmovisor:
runs-on: ubuntu-latest
steps:
Expand Down
104 changes: 104 additions & 0 deletions collections/collections.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package collections

import (
"errors"
"math"

storetypes "github.com/cosmos/cosmos-sdk/store/types"
)

var (
// ErrNotFound is returned when the provided key is not present in the StorageProvider.
ErrNotFound = errors.New("collections: not found")
// ErrEncoding is returned when something fails during key or value encoding/decoding.
ErrEncoding = errors.New("collections: encoding error")
)

// StorageProvider is implemented by types
// which provide a KVStore given a StoreKey.
// It represents sdk.Context, it exists to
// reduce dependencies.
type StorageProvider interface {
// KVStore returns a KVStore given its StoreKey.
KVStore(key storetypes.StoreKey) storetypes.KVStore
}

// Prefix defines a segregation namespace
// for specific collections objects.
type Prefix struct {
raw []byte // TODO(testinginprod): maybe add a humanized prefix field?
}

// Bytes returns the raw Prefix bytes.
func (n Prefix) Bytes() []byte { return n.raw }

// NewPrefix returns a Prefix given the provided namespace identifier.
// In the same module, no prefixes should share the same starting bytes
// meaning that having two namespaces whose bytes representation is:
// p1 := []byte("prefix")
// p2 := []byte("prefix1")
// yields to iterations of p1 overlapping over p2.
// If a numeric prefix is provided, it must be between 0 and 255 (uint8).
// If out of bounds this function will panic.
// Reason for which this function is constrained to `int` instead of `uint8` is for
// API ergonomics, golang's type inference will infer int properly but not uint8
// meaning that developers would need to write NewPrefix(uint8(number)) for numeric
// prefixes.
func NewPrefix[T interface{ int | string | []byte }](identifier T) Prefix {
i := any(identifier)
var prefix []byte
switch c := i.(type) {
case int:
if c > math.MaxUint8 || c < 0 {
panic("invalid integer prefix value: must be between 0 and 255")
}
prefix = []byte{uint8(c)}
case string:
prefix = []byte(c)
case []byte:
identifierCopy := make([]byte, len(c))
copy(identifierCopy, c)
prefix = identifierCopy
}
return Prefix{raw: prefix}
}

// KeyCodec defines a generic interface which is implemented
// by types that are capable of encoding and decoding collections keys.
type KeyCodec[T any] interface {
// Encode writes the key bytes into the buffer. Returns the number of
// bytes written. The implementer must expect the buffer to be at least
// of length equal to Size(K) for all encodings.
// It must also return the number of written bytes which must be
// equal to Size(K) for all encodings not involving varints.
// In case of encodings involving varints then the returned
// number of written bytes is allowed to be smaller than Size(K).
Encode(buffer []byte, key T) (int, error)
// Decode reads from the provided bytes buffer to decode
// the key T. Returns the number of bytes read, the type T
// or an error in case of decoding failure.
Decode(buffer []byte) (int, T, error)
// Size returns the buffer size need to encode key T in binary format.
// The returned value must match what is computed by Encode for all
// encodings except the ones involving varints. Varints are expected
// to return the maximum varint bytes buffer length, at the risk of
// over-estimating in order to pick the most performant path.
Size(key T) int
// Stringify returns a string representation of T.
Stringify(key T) string
// KeyType returns a string identifier for the type of the key.
KeyType() string
}

// ValueCodec defines a generic interface which is implemented
// by types that are capable of encoding and decoding collection values.
type ValueCodec[T any] interface {
// Encode encodes the value T into binary format.
Encode(value T) ([]byte, error)
// Decode returns the type T given its binary representation.
Decode(b []byte) (T, error)
// Stringify returns a string representation of T.
Stringify(value T) string
// ValueType returns the identifier for the type.
ValueType() string
}
74 changes: 74 additions & 0 deletions collections/collections_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package collections

import (
"context"
"math"
"testing"

"github.com/cosmos/cosmos-sdk/store/mem"
"github.com/cosmos/cosmos-sdk/store/types"
"github.com/stretchr/testify/require"
)

var _ StorageProvider = (*mockStorageProvider)(nil)

type mockStorageProvider struct {
context.Context
store types.KVStore
}

func (m mockStorageProvider) KVStore(key types.StoreKey) types.KVStore {
return m.store
}

func deps() (types.StoreKey, context.Context) {
kv := mem.NewStore()
key := types.NewKVStoreKey("test")
return key, mockStorageProvider{store: kv}
}

// checkKeyCodec asserts the correct behaviour of a KeyCodec over the type T.
func checkKeyCodec[T any](t *testing.T, encoder KeyCodec[T], key T) {
buffer := make([]byte, encoder.Size(key))
written, err := encoder.Encode(buffer, key)
require.NoError(t, err)
require.Equal(t, len(buffer), written)
read, decodedKey, err := encoder.Decode(buffer)
require.NoError(t, err)
require.Equal(t, len(buffer), read, "encoded key and read bytes must have same size")
require.Equal(t, key, decodedKey, "encoding and decoding produces different keys")
}

// checkValueCodec asserts the correct behaviour of a ValueCodec over the type T.
func checkValueCodec[T any](t *testing.T, encoder ValueCodec[T], value T) {
encodedValue, err := encoder.Encode(value)
require.NoError(t, err)
decodedValue, err := encoder.Decode(encodedValue)
require.NoError(t, err)
require.Equal(t, value, decodedValue, "encoding and decoding produces different values")
}

func TestPrefix(t *testing.T) {
t.Run("panics on invalid int", func(t *testing.T) {
require.Panics(t, func() {
NewPrefix(math.MaxUint8 + 1)
})
})

t.Run("string", func(t *testing.T) {
require.Equal(t, []byte("prefix"), NewPrefix("prefix").Bytes())
})

t.Run("int", func(t *testing.T) {
require.Equal(t, []byte{0x1}, NewPrefix(1).Bytes())
})

t.Run("[]byte", func(t *testing.T) {
bytes := []byte("prefix")
prefix := NewPrefix(bytes)
require.Equal(t, bytes, prefix.Bytes())
// assert if modification happen they do not propagate to prefix
bytes[0] = 0x0
require.Equal(t, []byte("prefix"), prefix.Bytes())
})
}
53 changes: 53 additions & 0 deletions collections/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
module cosmossdk.io/collections

go 1.19

require (
github.com/cosmos/cosmos-sdk v0.46.0-beta2.0.20221207205747-f3be41836f4d
github.com/stretchr/testify v1.8.1
)

require (
cosmossdk.io/errors v1.0.0-beta.7 // indirect
github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect
github.com/cespare/xxhash v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/confio/ics23/go v0.9.0 // indirect
github.com/cosmos/gogoproto v1.4.3 // indirect
github.com/cosmos/gorocksdb v1.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect
github.com/dgraph-io/badger/v2 v2.2007.4 // indirect
github.com/dgraph-io/ristretto v0.1.1 // indirect
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/go-kit/log v0.2.1 // indirect
github.com/go-logfmt/logfmt v0.5.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/glog v1.0.0 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/btree v1.1.2 // indirect
github.com/jmhodges/levigo v1.0.0 // indirect
github.com/klauspost/compress v1.15.12 // indirect
github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/sasha-s/go-deadlock v0.3.1 // indirect
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect
github.com/tendermint/go-amino v0.16.0 // indirect
github.com/tendermint/tendermint v0.37.0-rc2 // indirect
github.com/tendermint/tm-db v0.6.7 // indirect
go.etcd.io/bbolt v1.3.6 // indirect
golang.org/x/crypto v0.4.0 // indirect
golang.org/x/exp v0.0.0-20221019170559-20944726eadf // indirect
golang.org/x/net v0.3.0 // indirect
golang.org/x/sys v0.3.0 // indirect
golang.org/x/text v0.5.0 // indirect
google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6 // indirect
google.golang.org/grpc v1.51.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
)
Loading

0 comments on commit fca24b6

Please sign in to comment.