Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(oracle): Implement OrderedMap and use it for iterating through maps in x/oracle #1506

Merged
merged 21 commits into from
Jul 24, 2023
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
16f7447
refactor(oracle): Implement OrderedMap and use it for iterating throu…
Unique-Divine Jul 17, 2023
39633e4
doc comments fix
Unique-Divine Jul 17, 2023
36eb6f4
implement indexOf
Unique-Divine Jul 17, 2023
fd0e4fd
chore(deps): Bump github.com/CosmWasm/wasmvm from 1.2.4 to 1.3.0
dependabot[bot] Jul 17, 2023
ab6acc2
Updated changelog - dependabot
Unique-Divine Jul 17, 2023
3de28b4
Merge branch 'master' into realu/omap
Unique-Divine Jul 18, 2023
b16eb3e
Merge branch 'master' into dependabot/go_modules/github.com/CosmWasm/…
Unique-Divine Jul 18, 2023
0fd5b02
Merge branch 'master' into dependabot/go_modules/github.com/CosmWasm/…
Unique-Divine Jul 18, 2023
c4f1d5a
Merge branch 'master' into dependabot/go_modules/github.com/CosmWasm/…
jgimeno Jul 19, 2023
a51704c
chore(build): update wasmvm static lib version to download
helder-moreira Jul 19, 2023
f834301
deps(build.mk): read the WASMVM_VERSION programattically with 'go list'
Unique-Divine Jul 20, 2023
3b69fd7
deps(build.mk): read the WASMVM_VERSION programattically with 'go list'
Unique-Divine Jul 20, 2023
fd0b59c
fix merge conflict
Unique-Divine Jul 20, 2023
ca9b178
Merge branch 'realu/omap' of https://github.com/NibiruChain/nibiru in…
Unique-Divine Jul 20, 2023
0cfde71
Merge branch 'dependabot/go_modules/github.com/CosmWasm/wasmvm-1.3.0'…
Unique-Divine Jul 20, 2023
7335cca
Merge branch 'master' into realu/omap
Unique-Divine Jul 20, 2023
af007b3
Merge branch 'master' into realu/omap
Unique-Divine Jul 24, 2023
7253e07
Merge branch 'master' into realu/omap
Unique-Divine Jul 24, 2023
1738b81
Merge branch 'realu/omap' of https://github.com/NibiruChain/nibiru in…
Unique-Divine Jul 24, 2023
6a0cacd
refactor: simplify data structure
Unique-Divine Jul 24, 2023
d86790b
typo
Unique-Divine Jul 24, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* [#1494](https://github.com/NibiruChain/nibiru/pull/1494) - feat: create cli to add sudo account into genesis
* [#1493](https://github.com/NibiruChain/nibiru/pull/1493) - fix(perp): allow `ClosePosition` when there is bad debt
* [#1500](https://github.com/NibiruChain/nibiru/pull/1500) - refactor(perp): clean up reverse market order mechanics
* [#1506](https://github.com/NibiruChain/nibiru/pull/1506) - refactor(oracle): Implement OrderedMap and use it for iterating through maps in x/oracle
* [#1502](https://github.com/NibiruChain/nibiru/pull/1502) - feat: add ledger build support
* [#1517](https://github.com/NibiruChain/nibiru/pull/1517) - test: add more tests to x/hooks
* [#1518](https://github.com/NibiruChain/nibiru/pull/1518) - test: add more tests to x/perp
Expand Down Expand Up @@ -625,4 +626,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Testing

* [#695](https://github.com/NibiruChain/nibiru/pull/695) Add `OpenPosition` integration tests.
* [#692](https://github.com/NibiruChain/nibiru/pull/692) Add test coverage for Perp MsgServer methods.
* [#692](https://github.com/NibiruChain/nibiru/pull/692) Add test coverage for Perp MsgServer methods.
49 changes: 49 additions & 0 deletions x/common/omap/impl.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package omap

import (
"github.com/NibiruChain/nibiru/x/common/asset"
)

func stringIsLess(a, b string) bool {
return a < b
}

// ---------------------------------------------------------------------------
// OrderedMap[string, V]: OrderedMap_String
// ---------------------------------------------------------------------------

func OrderedMap_String[V any](data map[string]V) OrderedMap[string, V] {
omap := OrderedMap[string, V]{}
return omap.BuildFrom(data, stringSorter{})
}

// stringSorter is a Sorter implementation for keys of type string . It uses
// the built-in string comparison to determine order.
type stringSorter struct{}

var _ Sorter[string] = (*stringSorter)(nil)

func (sorter stringSorter) Less(a, b string) bool {
return stringIsLess(a, b)
}

// ---------------------------------------------------------------------------
// OrderedMap[asset.Pair, V]: OrderedMap_Pair
// ---------------------------------------------------------------------------

func OrderedMap_Pair[V any](
data map[asset.Pair]V,
) OrderedMap[asset.Pair, V] {
omap := OrderedMap[asset.Pair, V]{}
return omap.BuildFrom(data, pairSorter{})
}

// stringSorter is a Sorter implementation for keys of type asset.Pair. It uses
Unique-Divine marked this conversation as resolved.
Show resolved Hide resolved
// the built-in string comparison to determine order.
type pairSorter struct{}

var _ Sorter[asset.Pair] = (*pairSorter)(nil)

func (sorter pairSorter) Less(a, b asset.Pair) bool {
return stringIsLess(a.String(), b.String())
}
151 changes: 151 additions & 0 deletions x/common/omap/omap.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
// Package omap defines a generic-based type for creating ordered maps. It
// exports a "Sorter" interface, allowing the creation of ordered maps with
// custom key and value types.
//
// Specifically, omap supports ordered maps with keys of type string or
// asset.Pair and values of any type. See impl.go for examples.
//
// ## Motivation
//
// Ensuring deterministic behavior is crucial in blockchain systems, as all
// nodes must reach a consensus on the state of the blockchain. Every action,
// given the same input, should consistently yield the same result. A
// divergence in state could impede the ability of nodes to validate a block,
// prohibiting the addition of the block to the chain, which could lead to
// chain halts.
package omap

import (
"sort"
)

// OrderedMap is a wrapper struct around the built-in map that has guarantees
// about order because it sorts its keys with a custom sorter. It has a public
// API that mirrors that functionality of `map`. OrderedMap is built with
// generics, so it can hold various combinations of key-value types.
type OrderedMap[K comparable, V any] struct {
data map[K]V
orderedKeys []K
keyIndexMap map[K]int // useful for delete operation
isOrdered bool
sorter Sorter[K]
}

// Sorter is an interface used for ordering the keys in the OrderedMap.
type Sorter[K any] interface {
// Returns true if 'a' is less than 'b' Less needs to be defined for the
// key type, K, to provide a comparison operation.
Less(a K, b K) bool
}

// ensureOrder is a method on the OrderedMap that sorts the keys in the map
// and rebuilds the index map.
func (om *OrderedMap[K, V]) ensureOrder() {
keys := make([]K, 0, len(om.data))
for key := range om.data {
keys = append(keys, key)
}

// Sort the keys using the Sort function
lessFunc := func(i, j int) bool {
return om.sorter.Less(keys[i], keys[j])
}
sort.Slice(keys, lessFunc)

om.orderedKeys = keys
om.keyIndexMap = make(map[K]int)
for idx, key := range om.orderedKeys {
om.keyIndexMap[key] = idx
}
om.isOrdered = true
}

// BuildFrom is a method that builds an OrderedMap from a given map and a
// sorter for the keys. This function is useful for creating new OrderedMap
// types with typed keys.
func (om OrderedMap[K, V]) BuildFrom(
data map[K]V, sorter Sorter[K],
) OrderedMap[K, V] {
om.data = data
om.sorter = sorter
om.ensureOrder()
return om
}

// Range returns a channel of keys in their sorted order. This allows you
// to iterate over the map in a deterministic order. Using a channel here
// makes it so that the iteration is done lazily rather loading the entire
// map (OrderedMap.data) into memory and then iterating.
func (om OrderedMap[K, V]) Range() <-chan (K) {
iterChan := make(chan K)
go func() {
defer close(iterChan)
// Generate or compute values on-demand
for _, key := range om.orderedKeys {
iterChan <- key
}
}()
return iterChan
}

// Has checks whether a key exists in the map.
func (om OrderedMap[K, V]) Has(key K) bool {
_, exists := om.data[key]
return exists
}

// Len returns the number of items in the map.
func (om OrderedMap[K, V]) Len() int {
return len(om.data)
}

// Keys returns a slice of the keys in their sorted order.
func (om *OrderedMap[K, V]) Keys() []K {
if !om.isOrdered {
om.ensureOrder()
}

Check warning on line 106 in x/common/omap/omap.go

View check run for this annotation

Codecov / codecov/patch

x/common/omap/omap.go#L105-L106

Added lines #L105 - L106 were not covered by tests
return om.orderedKeys
}

// Get returns the value associated with a key in the map. If the key is not
// in the map, it returns nil.
func (om *OrderedMap[K, V]) Get(key K) (out *V) {
v, exists := om.data[key]
if exists {
*out = v
} else {
out = nil
Unique-Divine marked this conversation as resolved.
Show resolved Hide resolved
}
return out

Check warning on line 119 in x/common/omap/omap.go

View check run for this annotation

Codecov / codecov/patch

x/common/omap/omap.go#L112-L119

Added lines #L112 - L119 were not covered by tests
}

func (om *OrderedMap[K, V]) IndexOf(key K) (out *int) {
if om.Get(key) == nil {
return nil
}
for idx, k := range om.Keys() {
if k == key {
return &idx
}

Check warning on line 129 in x/common/omap/omap.go

View check run for this annotation

Codecov / codecov/patch

x/common/omap/omap.go#L122-L129

Added lines #L122 - L129 were not covered by tests
}
return

Check warning on line 131 in x/common/omap/omap.go

View check run for this annotation

Codecov / codecov/patch

x/common/omap/omap.go#L131

Added line #L131 was not covered by tests
}

// Set adds a key-value pair to the map, or updates the value if the key
// already exists. It ensures the keys are ordered after the operation.
func (om *OrderedMap[K, V]) Set(key K, val V) {
om.data[key] = val
om.ensureOrder() // TODO perf: make this more efficient with a clever insert.
}

// Delete removes a key-value pair from the map if the key exists.
func (om *OrderedMap[K, V]) Delete(key K) {
idx, keyExists := om.keyIndexMap[key]
if keyExists {
delete(om.data, key)

orderedKeys := om.orderedKeys
orderedKeys = append(orderedKeys[:idx], orderedKeys[idx+1:]...)
om.orderedKeys = orderedKeys
}
}
114 changes: 114 additions & 0 deletions x/common/omap/omap_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package omap_test

import (
"fmt"
"sort"
"testing"

"github.com/stretchr/testify/require"

"github.com/NibiruChain/nibiru/x/common/asset"
"github.com/NibiruChain/nibiru/x/common/omap"
)

// TestLenHasKeys checks the length of the ordered map and verifies if the map
// contains certain keys.
func TestLenHasKeys(t *testing.T) {
type HasCheck struct {
key string
has bool
}

testCases := []struct {
dataMap map[string]int
len int
hasChecks []HasCheck
}{
{
dataMap: map[string]int{"xyz": 420, "abc": 69},
len: 2,
hasChecks: []HasCheck{
{key: "foo", has: false},
{key: "xyz", has: true},
{key: "bar", has: false},
},
},
{
dataMap: map[string]int{"aaa": 420, "bbb": 69, "ccc": 69, "ddd": 28980},
len: 4,
hasChecks: []HasCheck{
{key: "foo", has: false},
{key: "xyz", has: false},
{key: "bbb", has: true},
},
},
}

for idx, tc := range testCases {
t.Run(fmt.Sprintf("case-%d", idx), func(t *testing.T) {
om := omap.OrderedMap_String[int](tc.dataMap)

require.Equal(t, tc.len, om.Len())

orderedKeys := om.Keys()
definitelyOrderedKeys := []string{}
definitelyOrderedKeys = append(definitelyOrderedKeys, orderedKeys...)
sort.Strings(definitelyOrderedKeys)

require.Equal(t, definitelyOrderedKeys, orderedKeys)

idx := 0
for key := range om.Range() {
require.Equal(t, orderedKeys[idx], key)
idx++
}
})
}
}

// TestGetSetDelete checks the Get, Set, and Delete operations on the OrderedMap.
func TestGetSetDelete(t *testing.T) {
om := omap.OrderedMap_String[string](make(map[string]string))
require.Equal(t, 0, om.Len())

om.Set("foo", "fooval")
require.True(t, om.Has("foo"))
require.Equal(t, 1, om.Len())

om.Delete("bar") // shouldn't cause any problems
om.Delete("foo")
require.False(t, om.Has("foo"))
require.Equal(t, 0, om.Len())
}

// TestOrderedMap_Pair tests an OrderedMap where the key is an asset.Pair, a
// type that isn't built-in.
func TestOrderedMap_Pair(t *testing.T) {
pairStrs := []string{
"abc:xyz", "abc:abc", "aaa:bbb", "xyz:xyz", "bbb:ccc", "xyz:abc",
}
orderedKeyStrs := []string{}
orderedKeyStrs = append(orderedKeyStrs, pairStrs...)
sort.Strings(orderedKeyStrs)

orderedKeys := asset.News(orderedKeyStrs...)
pairs := asset.News(pairStrs...)

type ValueType struct{}
unorderedMap := make(map[asset.Pair]ValueType)
for _, pair := range pairs {
unorderedMap[pair] = ValueType{}
}

om := omap.OrderedMap_Pair[ValueType](unorderedMap)
require.Equal(t, 6, om.Len())
require.EqualValues(t, orderedKeys, om.Keys())
require.NotEqualValues(t, orderedKeys.Strings(), pairStrs)

var pairsFromLoop []asset.Pair
for pair := range om.Range() {
pairsFromLoop = append(pairsFromLoop, pair)
}
require.EqualValues(t, orderedKeys, pairsFromLoop)
require.NotEqualValues(t, pairsFromLoop, pairs)
}
Loading
Loading