mirror of
https://github.com/luxfi/node.git
synced 2026-10-30 15:24:05 +00:00
319 lines
8.5 KiB
Go
319 lines
8.5 KiB
Go
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package merkledb
|
|
|
|
import (
|
|
"fmt"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/luxfi/constants"
|
|
"github.com/luxfi/container/maybe"
|
|
"github.com/luxfi/database"
|
|
"github.com/luxfi/database/memdb"
|
|
"github.com/luxfi/util"
|
|
)
|
|
|
|
// Tests:
|
|
// * Putting a key-node pair in the database
|
|
// * Getting a key-node pair from the cache and from the base db
|
|
// * Deleting a key-node pair from the database
|
|
// * Evicting elements from the cache
|
|
// * Flushing the cache
|
|
func Test_IntermediateNodeDB(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
n := newNode(ToKey([]byte{0x00}))
|
|
n.setValue(DefaultHasher, maybe.Some([]byte{byte(0x02)}))
|
|
nodeSize := cacheEntrySize(n.key, n)
|
|
|
|
// use exact multiple of node size so require.Equal(1, db.nodeCache.fifo.Len()) is correct later
|
|
cacheSize := nodeSize * 100
|
|
bufferSize := nodeSize * 20
|
|
|
|
evictionBatchSize := bufferSize
|
|
baseDB := memdb.New()
|
|
db := newIntermediateNodeDB(
|
|
baseDB,
|
|
utils.NewBytesPool(),
|
|
&mockMetrics{},
|
|
cacheSize,
|
|
bufferSize,
|
|
evictionBatchSize,
|
|
4,
|
|
DefaultHasher,
|
|
)
|
|
|
|
// Put a key-node pair
|
|
node1Key := ToKey([]byte{0x01})
|
|
node1 := newNode(node1Key)
|
|
node1.setValue(DefaultHasher, maybe.Some([]byte{byte(0x01)}))
|
|
require.NoError(db.Put(node1Key, node1))
|
|
|
|
// Get the key-node pair from cache
|
|
node1Read, err := db.Get(node1Key)
|
|
require.NoError(err)
|
|
require.Equal(node1, node1Read)
|
|
|
|
// Overwrite the key-node pair
|
|
node1Updated := newNode(node1Key)
|
|
node1Updated.setValue(DefaultHasher, maybe.Some([]byte{byte(0x02)}))
|
|
require.NoError(db.Put(node1Key, node1Updated))
|
|
|
|
// Assert the key-node pair was overwritten
|
|
node1Read, err = db.Get(node1Key)
|
|
require.NoError(err)
|
|
require.Equal(node1Updated, node1Read)
|
|
|
|
// Delete the key-node pair
|
|
require.NoError(db.Delete(node1Key))
|
|
_, err = db.Get(node1Key)
|
|
|
|
// Assert the key-node pair was deleted
|
|
require.Equal(database.ErrNotFound, err)
|
|
|
|
// Put elements in the cache until it is full.
|
|
expectedSize := 0
|
|
added := 0
|
|
for {
|
|
key := ToKey([]byte{byte(added)})
|
|
node := newNode(Key{})
|
|
node.setValue(DefaultHasher, maybe.Some([]byte{byte(added)}))
|
|
newExpectedSize := expectedSize + cacheEntrySize(key, node)
|
|
if newExpectedSize > bufferSize {
|
|
// Don't trigger eviction.
|
|
break
|
|
}
|
|
|
|
require.NoError(db.Put(key, node))
|
|
expectedSize = newExpectedSize
|
|
added++
|
|
}
|
|
|
|
// Assert cache has expected number of elements
|
|
require.Equal(added, db.writeBuffer.fifo.Len())
|
|
|
|
// Put one more element in the cache, which should trigger an eviction
|
|
// of all but 2 elements. 2 elements remain rather than 1 element because of
|
|
// the added key prefix increasing the size tracked by the batch.
|
|
key := ToKey([]byte{byte(added)})
|
|
node := newNode(Key{})
|
|
node.setValue(DefaultHasher, maybe.Some([]byte{byte(added)}))
|
|
require.NoError(db.Put(key, node))
|
|
|
|
// Assert cache has expected number of elements
|
|
require.Equal(1, db.writeBuffer.fifo.Len())
|
|
gotKey, _, ok := db.writeBuffer.fifo.Oldest()
|
|
require.True(ok)
|
|
require.Equal(ToKey([]byte{byte(added)}), gotKey)
|
|
|
|
// Get a node from the base database
|
|
// Use an early key that has been evicted from the cache
|
|
_, inCache := db.writeBuffer.Get(node1Key)
|
|
require.False(inCache)
|
|
nodeRead, err := db.Get(node1Key)
|
|
require.NoError(err)
|
|
require.Equal(maybe.Some([]byte{0x01}), nodeRead.value)
|
|
|
|
// Flush the cache.
|
|
require.NoError(db.Flush())
|
|
|
|
// Assert the cache is empty
|
|
require.Zero(db.writeBuffer.fifo.Len())
|
|
|
|
// Assert the evicted cache elements were written to disk with prefix.
|
|
it := baseDB.NewIteratorWithPrefix(intermediateNodePrefix)
|
|
defer it.Release()
|
|
|
|
count := 0
|
|
for it.Next() {
|
|
count++
|
|
}
|
|
require.NoError(it.Error())
|
|
require.Equal(added+1, count)
|
|
}
|
|
|
|
func FuzzIntermediateNodeDBConstructDBKey(f *testing.F) {
|
|
bufferSize := 200
|
|
cacheSize := 200
|
|
evictionBatchSize := bufferSize
|
|
baseDB := memdb.New()
|
|
|
|
f.Fuzz(func(
|
|
t *testing.T,
|
|
key []byte,
|
|
tokenLength uint,
|
|
) {
|
|
require := require.New(t)
|
|
for _, tokenSize := range validTokenSizes {
|
|
db := newIntermediateNodeDB(
|
|
baseDB,
|
|
utils.NewBytesPool(),
|
|
&mockMetrics{},
|
|
cacheSize,
|
|
bufferSize,
|
|
evictionBatchSize,
|
|
tokenSize,
|
|
DefaultHasher,
|
|
)
|
|
|
|
p := ToKey(key)
|
|
uBitLength := tokenLength * uint(tokenSize)
|
|
if uBitLength >= uint(p.length) {
|
|
// Skip when token length exceeds path length
|
|
return
|
|
}
|
|
p = p.Take(int(uBitLength))
|
|
constructedKey := db.constructDBKey(p)
|
|
baseLength := len(p.value) + len(intermediateNodePrefix)
|
|
require.Equal(intermediateNodePrefix, (*constructedKey)[:len(intermediateNodePrefix)])
|
|
switch {
|
|
case tokenSize == 8:
|
|
// for keys with tokens of size byte, no padding is added
|
|
require.Equal(p.Bytes(), (*constructedKey)[len(intermediateNodePrefix):])
|
|
case p.hasPartialByte():
|
|
require.Len(*constructedKey, baseLength)
|
|
require.Equal(p.Extend(ToToken(1, tokenSize)).Bytes(), (*constructedKey)[len(intermediateNodePrefix):])
|
|
default:
|
|
// when a whole number of bytes, there is an extra padding byte
|
|
require.Len(*constructedKey, baseLength+1)
|
|
require.Equal(p.Extend(ToToken(1, tokenSize)).Bytes(), (*constructedKey)[len(intermediateNodePrefix):])
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func Test_IntermediateNodeDB_ConstructDBKey_DirtyBuffer(t *testing.T) {
|
|
require := require.New(t)
|
|
cacheSize := 200
|
|
bufferSize := 200
|
|
evictionBatchSize := bufferSize
|
|
baseDB := memdb.New()
|
|
db := newIntermediateNodeDB(
|
|
baseDB,
|
|
utils.NewBytesPool(),
|
|
&mockMetrics{},
|
|
cacheSize,
|
|
bufferSize,
|
|
evictionBatchSize,
|
|
4,
|
|
DefaultHasher,
|
|
)
|
|
|
|
db.bufferPool.Put(&[]byte{0xFF, 0xFF, 0xFF})
|
|
constructedKey := db.constructDBKey(ToKey([]byte{}))
|
|
require.Len(*constructedKey, 2)
|
|
require.Equal(intermediateNodePrefix, (*constructedKey)[:len(intermediateNodePrefix)])
|
|
require.Equal(byte(16), (*constructedKey)[len(*constructedKey)-1])
|
|
|
|
db.bufferPool = utils.NewBytesPool()
|
|
db.bufferPool.Put(&[]byte{0xFF, 0xFF, 0xFF})
|
|
p := ToKey([]byte{0xF0}).Take(4)
|
|
constructedKey = db.constructDBKey(p)
|
|
require.Len(*constructedKey, 2)
|
|
require.Equal(intermediateNodePrefix, (*constructedKey)[:len(intermediateNodePrefix)])
|
|
require.Equal(p.Extend(ToToken(1, 4)).Bytes(), (*constructedKey)[len(intermediateNodePrefix):])
|
|
}
|
|
|
|
func TestIntermediateNodeDBClear(t *testing.T) {
|
|
require := require.New(t)
|
|
cacheSize := 200
|
|
bufferSize := 200
|
|
evictionBatchSize := bufferSize
|
|
baseDB := memdb.New()
|
|
db := newIntermediateNodeDB(
|
|
baseDB,
|
|
utils.NewBytesPool(),
|
|
&mockMetrics{},
|
|
cacheSize,
|
|
bufferSize,
|
|
evictionBatchSize,
|
|
4,
|
|
DefaultHasher,
|
|
)
|
|
|
|
for _, b := range [][]byte{{1}, {2}, {3}} {
|
|
require.NoError(db.Put(ToKey(b), newNode(ToKey(b))))
|
|
}
|
|
|
|
require.NoError(db.Clear())
|
|
|
|
iter := baseDB.NewIteratorWithPrefix(intermediateNodePrefix)
|
|
defer iter.Release()
|
|
require.False(iter.Next())
|
|
|
|
require.Zero(db.writeBuffer.currentSize)
|
|
}
|
|
|
|
// Test that deleting the empty key and flushing works correctly.
|
|
// Previously, there was a bug that occurred when deleting the empty key
|
|
// if the cache was empty. The size of the cache entry was reported as 0,
|
|
// which caused the cache's currentSize to be 0, so on resize() we didn't
|
|
// call onEviction. This caused the empty key to not be deleted from the baseDB.
|
|
func TestIntermediateNodeDBDeleteEmptyKey(t *testing.T) {
|
|
require := require.New(t)
|
|
cacheSize := 200
|
|
bufferSize := 200
|
|
evictionBatchSize := bufferSize
|
|
baseDB := memdb.New()
|
|
db := newIntermediateNodeDB(
|
|
baseDB,
|
|
utils.NewBytesPool(),
|
|
&mockMetrics{},
|
|
cacheSize,
|
|
bufferSize,
|
|
evictionBatchSize,
|
|
4,
|
|
DefaultHasher,
|
|
)
|
|
|
|
emptyKey := ToKey([]byte{})
|
|
require.NoError(db.Put(emptyKey, newNode(emptyKey)))
|
|
require.NoError(db.Flush())
|
|
|
|
emptyDBKey := db.constructDBKey(emptyKey)
|
|
has, err := baseDB.Has(*emptyDBKey)
|
|
require.NoError(err)
|
|
require.True(has)
|
|
|
|
require.NoError(db.Delete(ToKey([]byte{})))
|
|
require.NoError(db.Flush())
|
|
|
|
emptyDBKey = db.constructDBKey(emptyKey)
|
|
has, err = baseDB.Has(*emptyDBKey)
|
|
require.NoError(err)
|
|
require.False(has)
|
|
}
|
|
|
|
func Benchmark_IntermediateNodeDB_ConstructDBKey(b *testing.B) {
|
|
keyTokenSizes := []int{0, 1, 4, 16, 64, 256}
|
|
for _, tokenSize := range validTokenSizes {
|
|
db := newIntermediateNodeDB(
|
|
memdb.New(),
|
|
utils.NewBytesPool(),
|
|
&mockMetrics{},
|
|
constants.MiB,
|
|
constants.MiB,
|
|
constants.MiB,
|
|
tokenSize,
|
|
DefaultHasher,
|
|
)
|
|
|
|
for _, keyTokenSize := range keyTokenSizes {
|
|
keyBitSize := keyTokenSize * tokenSize
|
|
keyBytes := make([]byte, bytesNeeded(keyBitSize))
|
|
key := Key{
|
|
length: keyBitSize,
|
|
value: string(keyBytes),
|
|
}
|
|
b.Run(fmt.Sprintf("%d/%d", tokenSize, keyTokenSize), func(b *testing.B) {
|
|
for i := 0; i < b.N; i++ {
|
|
db.bufferPool.Put(db.constructDBKey(key))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|