Skip to content

Commit

Permalink
feat(primitives/state-machine) TrieBackend implementation (#4318)
Browse files Browse the repository at this point in the history
  • Loading branch information
timwu20 committed Dec 12, 2024
1 parent 2e8ca6f commit 4f44860
Show file tree
Hide file tree
Showing 25 changed files with 5,217 additions and 16 deletions.
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ require (
github.com/dgraph-io/badger/v4 v4.5.0
github.com/dgraph-io/ristretto/v2 v2.0.0
github.com/disiqueira/gotree v1.0.0
github.com/dolthub/maphash v0.1.0
github.com/elastic/go-freelru v0.15.0
github.com/ethereum/go-ethereum v1.14.12
github.com/fatih/color v1.18.0
github.com/gammazero/deque v1.0.0
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,13 @@ github.com/disiqueira/gotree v1.0.0/go.mod h1:7CwL+VWsWAU95DovkdRZAtA7YbtHwGk+tL
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ=
github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/elastic/go-freelru v0.15.0 h1:Jo1aY8JAvpyxbTDJEudrsBfjFDaALpfVv8mxuh9sfvI=
github.com/elastic/go-freelru v0.15.0/go.mod h1:bSdWT4M0lW79K8QbX6XY2heQYSCqD7THoYf82pT/H3I=
github.com/elastic/gosigar v0.12.0/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs=
github.com/elastic/gosigar v0.14.3 h1:xwkKwPia+hSfg9GqrCUKYdId102m9qTJIIr7egmK/uo=
github.com/elastic/gosigar v0.14.3/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs=
Expand Down
103 changes: 103 additions & 0 deletions internal/cost-lru/cost_lru.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Copyright 2024 ChainSafe Systems (ON)
// SPDX-License-Identifier: LGPL-3.0-only

package costlru

import (
"math"
"time"

"github.com/elastic/go-freelru"
)

// LRU is a cost based LRU wrapped around [freelru.LRU].
type LRU[K comparable, V any] struct {
currentCost uint
maxCost uint
costFunc func(K, V) uint32
onEvictCallback freelru.OnEvictCallback[K, V]
*freelru.LRU[K, V]
}

// Costructor for [LRU].
func New[K comparable, V any](
maxCost uint, hash freelru.HashKeyCallback[K], costFunc func(K, V) uint32,
) (*LRU[K, V], error) {
var capacity = uint32(math.MaxUint32)
if maxCost < math.MaxUint32 {
capacity = uint32(maxCost)
}
lru, err := freelru.New[K, V](capacity, hash)
if err != nil {
return nil, err
}

l := LRU[K, V]{
maxCost: maxCost,
costFunc: costFunc,
LRU: lru,
}
lru.SetOnEvict(l.evictCallback)

return &l, nil
}

func (l *LRU[K, V]) costRemove(key K, value V) (cost uint32, removed bool, canAdd bool) {
if l.LRU.Contains(key) {
oldVal, ok := l.LRU.Peek(key)
if !ok {
panic("should be in lru")
}
cost := l.costFunc(key, oldVal)
l.currentCost -= uint(cost)
}
cost = l.costFunc(key, value)
if uint(cost) > l.maxCost {
return cost, removed, false
}
for uint(cost)+l.currentCost > l.maxCost {
_, _, removed = l.LRU.RemoveOldest()
if !removed {
panic("huh?")
}
}
return cost, removed, true
}

func (l *LRU[K, V]) Add(key K, value V) (added bool, evicted bool) {
cost, removed, canAdd := l.costRemove(key, value)
l.currentCost += uint(cost)
evicted = l.LRU.Add(key, value)
return canAdd, evicted || removed
}

func (l *LRU[K, V]) AddWithLifetime(key K, value V, lifetime time.Duration) (added bool, evicted bool) {
cost, removed, canAdd := l.costRemove(key, value)
l.currentCost += uint(cost)
evicted = l.LRU.AddWithLifetime(key, value, lifetime)
return canAdd, evicted || removed
}

func (l *LRU[K, V]) evictCallback(key K, value V) {
cost := l.costFunc(key, value)
if uint(cost) <= l.currentCost {
l.currentCost -= uint(cost)
} else {
l.currentCost = 0
}
if l.onEvictCallback != nil {
l.onEvictCallback(key, value)
}
}

func (l *LRU[K, V]) SetOnEvict(onEvict freelru.OnEvictCallback[K, V]) {
l.onEvictCallback = onEvict
}

func (l *LRU[K, V]) Cost() uint {
return l.currentCost
}

func (l *LRU[K, V]) MaxCost() uint {
return l.maxCost
}
188 changes: 188 additions & 0 deletions internal/cost-lru/cost_lru_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
// Copyright 2024 ChainSafe Systems (ON)
// SPDX-License-Identifier: LGPL-3.0-only

package costlru

import (
"testing"

"github.com/ChainSafe/gossamer/internal/primitives/core/hash"
"github.com/ChainSafe/gossamer/internal/primitives/runtime"
"github.com/dolthub/maphash"
"github.com/stretchr/testify/require"
)

type Hasher struct {
maphash.Hasher[ValueCacheKeyHash[hash.H256]]
}

func (h Hasher) Hash(key ValueCacheKeyHash[hash.H256]) uint32 {
return uint32(h.Hasher.Hash(key))
}

type ValueCacheKeyHash[H runtime.Hash] struct {
StorageRoot H
StorageKey string
}

func TestLRU(t *testing.T) {
hasher := Hasher{maphash.NewHasher[ValueCacheKeyHash[hash.H256]]()}

var costFunc = func(key ValueCacheKeyHash[hash.H256], val []byte) uint32 {
keyCost := uint32(len(key.StorageKey))
return keyCost + uint32(len(val))
}

maxNum := uint(1024)
maxSize := uint(costFunc(ValueCacheKeyHash[hash.H256]{
StorageRoot: hash.NewRandomH256(),
StorageKey: string(hash.NewRandomH256()),
}, []byte{1})) * maxNum

l, err := New[ValueCacheKeyHash[hash.H256], []byte](maxSize, hasher.Hash, costFunc)
require.NoError(t, err)

someRoot := hash.NewRandomH256()
allKeys := make([]ValueCacheKeyHash[hash.H256], 0)
for i := 0; i < int(maxNum)*2; i++ {
hash := ValueCacheKeyHash[hash.H256]{
StorageRoot: someRoot,
StorageKey: string(hash.NewRandomH256()),
}
allKeys = append(allKeys, hash)
added, evicted := l.Add(hash, []byte{uint8(i)})
require.True(t, added)
if i >= int(maxNum) {
require.True(t, evicted)
} else {
require.False(t, evicted)
}
}

require.Equal(t, maxSize, l.currentCost)

for i, key := range l.Keys() {
require.Equal(t, allKeys[int(maxNum)+i], key)
}
}

func TestLRU_EvictCallback(t *testing.T) {
hasher := Hasher{maphash.NewHasher[ValueCacheKeyHash[hash.H256]]()}

var costFunc = func(key ValueCacheKeyHash[hash.H256], val []byte) uint32 {
keyCost := uint32(len(key.StorageKey))
return keyCost + uint32(len(val))
}

maxNum := uint(1024)
maxSize := uint(costFunc(ValueCacheKeyHash[hash.H256]{
StorageRoot: hash.NewRandomH256(),
StorageKey: string(hash.NewRandomH256()),
}, []byte{1})) * maxNum

evictCount := 0
l, err := New[ValueCacheKeyHash[hash.H256], []byte](maxSize, hasher.Hash, costFunc)
require.NoError(t, err)
l.SetOnEvict(func(vckh ValueCacheKeyHash[hash.H256], b []byte) {
evictCount++
})

someRoot := hash.NewRandomH256()
allKeys := make([]ValueCacheKeyHash[hash.H256], 0)
for i := 0; i < int(maxNum)*2; i++ {
hash := ValueCacheKeyHash[hash.H256]{
StorageRoot: someRoot,
StorageKey: string(hash.NewRandomH256()),
}
allKeys = append(allKeys, hash)
added, evicted := l.Add(hash, []byte{uint8(i)})
require.True(t, added)
if i >= int(maxNum) {
require.True(t, evicted)
} else {
require.False(t, evicted)
}
}

require.Equal(t, maxSize, l.currentCost)

for i, key := range l.Keys() {
require.Equal(t, allKeys[int(maxNum)+i], key)
}

require.Equal(t, int(maxNum), evictCount)
}

func TestLRU_Purge(t *testing.T) {
hasher := Hasher{maphash.NewHasher[ValueCacheKeyHash[hash.H256]]()}

var costFunc = func(key ValueCacheKeyHash[hash.H256], val []byte) uint32 {
keyCost := uint32(len(key.StorageKey))
return keyCost + uint32(len(val))
}

maxNum := uint(3)
maxSize := uint(costFunc(ValueCacheKeyHash[hash.H256]{
StorageRoot: hash.NewRandomH256(),
StorageKey: string(hash.NewRandomH256()),
}, []byte{1})) * maxNum

l, err := New[ValueCacheKeyHash[hash.H256], []byte](maxSize, hasher.Hash, costFunc)
require.NoError(t, err)

someRoot := hash.NewRandomH256()
allKeys := make([]ValueCacheKeyHash[hash.H256], 0)
for i := 0; i < int(maxNum)*2; i++ {
hash := ValueCacheKeyHash[hash.H256]{
StorageRoot: someRoot,
StorageKey: string(hash.NewRandomH256()),
}
allKeys = append(allKeys, hash)
added, evicted := l.Add(hash, []byte{uint8(i)})
require.True(t, added)
if i >= int(maxNum) {
require.True(t, evicted)
} else {
require.False(t, evicted)
}
}

require.Equal(t, maxSize, l.currentCost)

for i, key := range l.Keys() {
require.Equal(t, allKeys[int(maxNum)+i], key)
}

l.Purge()
require.Equal(t, 0, l.Len())
require.Equal(t, uint(0), l.currentCost)
}

func TestLRU_Same_Entries(t *testing.T) {
hasher := Hasher{maphash.NewHasher[ValueCacheKeyHash[hash.H256]]()}

var costFunc = func(key ValueCacheKeyHash[hash.H256], val []byte) uint32 {
keyCost := uint32(len(key.StorageKey))
return keyCost + uint32(len(val))
}

someKey := ValueCacheKeyHash[hash.H256]{
StorageRoot: hash.NewRandomH256(),
StorageKey: string(hash.NewRandomH256()),
}

maxNum := uint(5)
maxSize := uint(costFunc(someKey, []byte{1})) * maxNum

l, err := New[ValueCacheKeyHash[hash.H256], []byte](maxSize, hasher.Hash, costFunc)
require.NoError(t, err)

for i := 0; i < int(maxNum); i++ {
added, evicted := l.Add(someKey, []byte{1})
require.True(t, added)
require.False(t, evicted)
}
require.Equal(t, 1, l.Len())
require.Equal(t, int(l.Cost()), int(costFunc(someKey, []byte{1})))

}
6 changes: 6 additions & 0 deletions internal/primitives/core/hash/hash.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,9 @@ func NewRandomH256() H256 {
}
return H256(token)
}

// NewH256 is constructor for a zero case H256
func NewH256() H256 {
token := make([]byte, 32)
return H256(token)
}
16 changes: 8 additions & 8 deletions internal/primitives/database/mem.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,35 +52,35 @@ func (mdb *MemDB[H]) Commit(transaction Transaction[H]) error {
if !ok {
mdb.inner[change.ColumnID] = make(map[string]refCountValue)
}
cv, ok := mdb.inner[change.ColumnID][change.Hash.String()]
cv, ok := mdb.inner[change.ColumnID][string(change.Hash.Bytes())]
if ok {
cv.refCount += 1
mdb.inner[change.ColumnID][change.Hash.String()] = cv
mdb.inner[change.ColumnID][string(change.Hash.Bytes())] = cv
} else {
mdb.inner[change.ColumnID][change.Hash.String()] = refCountValue{1, change.Preimage}
mdb.inner[change.ColumnID][string(change.Hash.Bytes())] = refCountValue{1, change.Preimage}
}
case Reference[H]:
_, ok := mdb.inner[change.ColumnID]
if !ok {
mdb.inner[change.ColumnID] = make(map[string]refCountValue)
}
cv, ok := mdb.inner[change.ColumnID][change.Hash.String()]
cv, ok := mdb.inner[change.ColumnID][string(change.Hash.Bytes())]
if ok {
cv.refCount += 1
mdb.inner[change.ColumnID][change.Hash.String()] = cv
mdb.inner[change.ColumnID][string(change.Hash.Bytes())] = cv
}
case Release[H]:
_, ok := mdb.inner[change.ColumnID]
if !ok {
mdb.inner[change.ColumnID] = make(map[string]refCountValue)
}
cv, ok := mdb.inner[change.ColumnID][change.Hash.String()]
cv, ok := mdb.inner[change.ColumnID][string(change.Hash.Bytes())]
if ok {
cv.refCount -= 1
if cv.refCount == 0 {
delete(mdb.inner[change.ColumnID], change.Hash.String())
delete(mdb.inner[change.ColumnID], string(change.Hash.Bytes()))
} else {
mdb.inner[change.ColumnID][change.Hash.String()] = cv
mdb.inner[change.ColumnID][string(change.Hash.Bytes())] = cv
}
}
}
Expand Down
Loading

0 comments on commit 4f44860

Please sign in to comment.