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

perf(evm-keeper-precompile): implement sorted map for k.precompiles to remove dead code #1996

Merged
merged 7 commits into from
Aug 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [#1985](https://github.com/NibiruChain/nibiru/pull/1985) - feat(evm)!: Use atto denomination for the wei units in the EVM so that NIBI is "ether" to clients. Only micronibi (unibi) amounts can be transferred. All clients follow the constraint equation, 1 ether == 1 NIBI == 10^6 unibi == 10^18 wei.
- [#1986](https://github.com/NibiruChain/nibiru/pull/1986) - feat(evm): Combine both account queries into "/eth.evm.v1.Query/EthAccount", accepting both nibi-prefixed Bech32 addresses and Ethereum-type hexadecimal addresses as input.
- [#1989](https://github.com/NibiruChain/nibiru/pull/1989) - refactor(evm): simplify evm module address
- [#1996](https://github.com/NibiruChain/nibiru/pull/1996) - perf(evm-keeper-precompile): implement sorted map for `k.precompiles` to remove dead code
- [#1997](https://github.com/NibiruChain/nibiru/pull/1997) - refactor(evm): Remove unnecessary params: "enable_call", "enable_create".

#### Dapp modules: perp, spot, oracle, etc
Expand Down
6 changes: 3 additions & 3 deletions proto/eth/evm/v1/evm.proto
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
}

// Params defines the EVM module parameters
message Params {

Check failure on line 30 in proto/eth/evm/v1/evm.proto

View workflow job for this annotation

GitHub Actions / break-check

Previously present field "7" with name "active_precompiles" on message "Params" was deleted.
option (gogoproto.equal) = true;
// evm_denom represents the token denomination used to run the EVM state
// transitions.
Expand All @@ -47,9 +47,9 @@
// allow_unprotected_txs defines if replay-protected (i.e non EIP155
// signed) transactions can be executed on the state machine.
bool allow_unprotected_txs = 6;
// active_precompiles defines the slice of hex addresses of the precompiled
// contracts that are active
repeated string active_precompiles = 7;
// DEPRECATED: active_precompiles
// All precompiles present according to the VM are active.
reserved 7;
// evm_channels is the list of channel identifiers from EVM compatible chains
repeated string evm_channels = 8 [ (gogoproto.customname) = "EVMChannels" ];

Expand Down
45 changes: 36 additions & 9 deletions x/common/omap/impl.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package omap

import (
"math/big"

gethcommon "github.com/ethereum/go-ethereum/common"

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

Expand All @@ -9,12 +13,12 @@ func stringIsLess(a, b string) bool {
}

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

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

// stringSorter is a Sorter implementation for keys of type string . It uses
Expand All @@ -28,14 +32,14 @@ func (sorter stringSorter) Less(a, b string) bool {
}

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

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

// pairSorter is a Sorter implementation for keys of type asset.Pair. It uses
Expand All @@ -47,3 +51,26 @@ var _ Sorter[asset.Pair] = (*pairSorter)(nil)
func (sorter pairSorter) Less(a, b asset.Pair) bool {
return stringIsLess(a.String(), b.String())
}

// ---------------------------------------------------------------------------
// SortedMap[gethcommon.Address, V]
// ---------------------------------------------------------------------------

func SortedMap_EthAddress[V any](
data map[gethcommon.Address]V,
) SortedMap[gethcommon.Address, V] {
return *new(SortedMap[gethcommon.Address, V]).BuildFrom(
data, addrSorter{},
)
}

// addrSorter implements "omap.Sorter" for the "gethcommon.Address" type.
type addrSorter struct{}

var _ Sorter[gethcommon.Address] = (*addrSorter)(nil)

func (s addrSorter) Less(a, b gethcommon.Address) bool {
aInt := new(big.Int).SetBytes(a.Bytes())
bInt := new(big.Int).SetBytes(b.Bytes())
return aInt.Cmp(bInt) < 0
}
130 changes: 96 additions & 34 deletions x/common/omap/omap.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
// Package omap defines a generic-based type for creating ordered maps. It
// exports a "Sorter" interface, allowing the creation of ordered maps with
// exports a "Sorter" interface, allowing the creation of sorted 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.
// See impl.go for examples.
//
// ## Motivation
//
Expand All @@ -19,14 +18,13 @@ import (
"sort"
)

// OrderedMap is a wrapper struct around the built-in map that has guarantees
// SortedMap 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
// API that mirrors that functionality of `map`. SortedMap 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
type SortedMap[K comparable, V any] struct {
data map[K]V
orderedKeys []K
keyIndexMap map[K]int // useful for delete operation
isOrdered bool
sorter Sorter[K]
}
Expand All @@ -38,11 +36,39 @@ type Sorter[K any] interface {
Less(a K, b K) bool
}

// SorterLeq is true if a <= b implements "less than or equal" using "Less"
func SorterLeq[K comparable](sorter Sorter[K], a, b K) bool {
return sorter.Less(a, b) || a == b
}

// Data returns a copy of the underlying map (unordered, unsorted)
func (om *SortedMap[K, V]) Data() map[K]V {
dataCopy := make(map[K]V, len(om.data))
for k, v := range om.InternalData() {
dataCopy[k] = v
}
return dataCopy
}

// InternalData returns the SortedMap's private map.
func (om *SortedMap[K, V]) InternalData() map[K]V {
return om.data
}

func (om *SortedMap[K, V]) Get(key K) (val V, exists bool) {
val, exists = om.data[key]
return val, exists
}

// 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 {
func (om *SortedMap[K, V]) ensureOrder() {
if om.isOrdered {
return
}

keys := make([]K, 0, len(om.data))
for key := range om.data {
keys = append(keys, key)
}

Expand All @@ -53,20 +79,16 @@ func (om *OrderedMap[K, V]) ensureOrder() {
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(
func (om *SortedMap[K, V]) BuildFrom(
data map[K]V, sorter Sorter[K],
) OrderedMap[K, V] {
om.Data = data
) *SortedMap[K, V] {
om.data = data
om.sorter = sorter
om.ensureOrder()
return om
Expand All @@ -76,7 +98,7 @@ func (om OrderedMap[K, V]) BuildFrom(
// 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) {
func (om *SortedMap[K, V]) Range() <-chan (K) {
iterChan := make(chan K)
go func() {
defer close(iterChan)
Expand All @@ -89,18 +111,18 @@ func (om OrderedMap[K, V]) Range() <-chan (K) {
}

// Has checks whether a key exists in the map.
func (om OrderedMap[K, V]) Has(key K) bool {
_, exists := om.Data[key]
func (om *SortedMap[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)
func (om *SortedMap[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 {
func (om *SortedMap[K, V]) Keys() []K {
if !om.isOrdered {
om.ensureOrder()
}
Expand All @@ -109,19 +131,59 @@ func (om *OrderedMap[K, V]) Keys() []K {

// 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
func (om *SortedMap[K, V]) Set(key K, val V) {
_, exists := om.data[key]
om.data[key] = val

if !exists {
lenBefore := len(om.orderedKeys)

if lenBefore == 0 {
// If the map is empty, create it. There's no need to search.
om.orderedKeys = []K{key}
return
}

// If the key is new, insert it to the correctly sorted position
// Binary search works here and is in the standard library.
idx := sort.Search(lenBefore, func(i int) bool {
return om.sorter.Less(key, om.orderedKeys[i])
})

// Update om.orderedKeys
newSortedKeys := make([]K, lenBefore+1)
front, back := om.orderedKeys[:idx], om.orderedKeys[idx:]
copy(newSortedKeys[:idx], front) // front
newSortedKeys[idx] = key // middle
copy(newSortedKeys[idx+1:], back) // back
om.orderedKeys = newSortedKeys
}
}

// Union combines new key-value pairs into the ordered map.
func (om *SortedMap[K, V]) Union(kvMap map[K]V) {
for key, val := range kvMap {
om.data[key] = val
}
om.isOrdered = false
om.ensureOrder() // TODO perf: make this more efficient with a clever insert.
Comment on lines +163 to 169
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct Union method with potential for optimization.

The Union method is correctly implemented, but there is a TODO for performance improvement.

Would you like assistance in optimizing the Union method for better performance?

}

// 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
func (om *SortedMap[K, V]) Delete(key K) {
if _, keyExists := om.data[key]; keyExists {
lenBeforeDelete := om.Len()
delete(om.data, key)

// Remove the key from orderedKeys while preserving the order
idx := sort.Search(lenBeforeDelete, func(i int) bool {
return SorterLeq(om.sorter, key, om.orderedKeys[i])
})

// Update om.orderedKeys, skipping the deleted key (om.orderedKeys[idx])
newSortedKeys := make([]K, lenBeforeDelete-1)
copy(newSortedKeys[:idx], om.orderedKeys[:idx]) // front
copy(newSortedKeys[idx:], om.orderedKeys[idx+1:]) // middle + back
om.orderedKeys = newSortedKeys
}
}
Loading
Loading