Files
node/x/merkledb/intermediate_node_db.go
zeekay 7cf87b230b node-unified is the node
The module already called itself github.com/luxfi/node while containing no node
in Go: cmd/luxd was twenty-one lines that printed the size of a few types, and
the Go column of the matrix measured a VM plugin. A repo that fetches the thing
it claims to be has not replaced it, so the Go sources move in — 1515 files,
the daemon and everything under it — and the build reaches outside this module
for nothing.

Rust and C++ sit beside it unchanged and are still measured against it by the
differential. That is the point of one repo: the reference implementation and
the two ports are the same checkout, so parity is a thing you can run rather
than a thing you assert.

Verified: builds, and the closure carries no lux-private — checked at build time
rather than believed. The daemon starts twelve chains on a local network
(P X C Q Z A B G K M F, plus D through its plugin) and reports healthy.
2026-09-07 16:15:25 -07:00

198 lines
5.8 KiB
Go

// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package merkledb
import (
"github.com/luxfi/database"
"github.com/luxfi/node/cache"
"github.com/luxfi/node/cache/lru"
"github.com/luxfi/util"
)
// Holds intermediate nodes. That is, those without values.
// Changes to this database aren't written to [baseDB] until
// they're evicted from the [nodeCache] or Flush is called.
type intermediateNodeDB struct {
bufferPool *utils.BytesPool
// The underlying storage.
// Keys written to [baseDB] are prefixed with [intermediateNodePrefix].
baseDB database.Database
// The write buffer contains nodes that have been changed but have not been written to disk.
// Note that a call to Put may cause a node to be evicted
// from the cache, which will call [OnEviction].
// A non-nil error returned from Put is considered fatal.
// Keys in [nodeCache] aren't prefixed with [intermediateNodePrefix].
writeBuffer onEvictCache[Key, *node]
// If a value is nil, the corresponding key isn't in the trie.
nodeCache cache.Cacher[Key, *node]
// the number of bytes to evict during an eviction batch
evictionBatchSize int
metrics merkleDBMetrics
tokenSize int
hasher Hasher
}
func newIntermediateNodeDB(
db database.Database,
bufferPool *utils.BytesPool,
metrics merkleDBMetrics,
cacheSize int,
writeBufferSize int,
evictionBatchSize int,
tokenSize int,
hasher Hasher,
) *intermediateNodeDB {
result := &intermediateNodeDB{
metrics: metrics,
baseDB: db,
bufferPool: bufferPool,
evictionBatchSize: evictionBatchSize,
tokenSize: tokenSize,
hasher: hasher,
nodeCache: lru.NewSizedCache(cacheSize, cacheEntrySize),
}
result.writeBuffer = newOnEvictCache(
writeBufferSize,
cacheEntrySize,
result.onEviction,
)
return result
}
// A non-nil error is considered fatal and closes [db.baseDB].
func (db *intermediateNodeDB) onEviction(key Key, n *node) error {
writeBatch := db.baseDB.NewBatch()
totalSize := cacheEntrySize(key, n)
if err := db.addToBatch(writeBatch, key, n); err != nil {
_ = db.baseDB.Close()
return err
}
// Evict the oldest [evictionBatchSize] nodes from the cache
// and write them to disk. We write a batch of them, rather than
// just [n], so that we don't immediately evict and write another
// node, because each time this method is called we do a disk write.
// Evicts a total number of bytes, rather than a number of nodes
for totalSize < db.evictionBatchSize {
key, n, exists := db.writeBuffer.removeOldest()
if !exists {
// The cache is empty.
break
}
totalSize += cacheEntrySize(key, n)
if err := db.addToBatch(writeBatch, key, n); err != nil {
_ = db.baseDB.Close()
return err
}
}
if err := writeBatch.Write(); err != nil {
_ = db.baseDB.Close()
return err
}
return nil
}
func (db *intermediateNodeDB) addToBatch(b database.KeyValueWriterDeleter, key Key, n *node) error {
dbKey := db.constructDBKey(key)
defer db.bufferPool.Put(dbKey)
db.metrics.DatabaseNodeWrite()
if n == nil {
return b.Delete(*dbKey)
}
return b.Put(*dbKey, n.bytes())
}
func (db *intermediateNodeDB) Get(key Key) (*node, error) {
if cachedValue, isCached := db.nodeCache.Get(key); isCached {
db.metrics.IntermediateNodeCacheHit()
if cachedValue == nil {
return nil, database.ErrNotFound
}
return cachedValue, nil
}
if cachedValue, isCached := db.writeBuffer.Get(key); isCached {
db.metrics.IntermediateNodeCacheHit()
if cachedValue == nil {
return nil, database.ErrNotFound
}
return cachedValue, nil
}
db.metrics.IntermediateNodeCacheMiss()
dbKey := db.constructDBKey(key)
defer db.bufferPool.Put(dbKey)
db.metrics.DatabaseNodeRead()
nodeBytes, err := db.baseDB.Get(*dbKey)
if err != nil {
return nil, err
}
return parseNode(db.hasher, key, nodeBytes)
}
// constructDBKey returns a key that can be used in [db.baseDB].
// We need to be able to differentiate between two keys of equal
// byte length but different bit length, so we add padding to differentiate.
// Additionally, we add a prefix indicating it is part of the intermediateNodeDB.
func (db *intermediateNodeDB) constructDBKey(key Key) *[]byte {
if db.tokenSize == 8 {
// For tokens of size byte, no padding is needed since byte
// length == token length
return addPrefixToKey(db.bufferPool, intermediateNodePrefix, key.Bytes())
}
var (
prefixLen = len(intermediateNodePrefix)
prefixBitLen = 8 * prefixLen
dualIndex = dualBitIndex(db.tokenSize)
paddingByteValue byte = 1 << dualIndex
paddingSliceValue = []byte{paddingByteValue}
paddingKey = Key{
value: byteSliceToString(paddingSliceValue),
length: db.tokenSize,
}
)
bufferPtr := db.bufferPool.Get(bytesNeeded(prefixBitLen + key.length + db.tokenSize))
copy(*bufferPtr, intermediateNodePrefix) // add prefix
copy((*bufferPtr)[prefixLen:], key.Bytes()) // add key
extendIntoBuffer(*bufferPtr, paddingKey, prefixBitLen+key.length) // add padding
return bufferPtr
}
func (db *intermediateNodeDB) Put(key Key, n *node) error {
db.nodeCache.Put(key, n)
return db.writeBuffer.Put(key, n)
}
func (db *intermediateNodeDB) Flush() error {
db.nodeCache.Flush()
return db.writeBuffer.Flush()
}
func (db *intermediateNodeDB) Delete(key Key) error {
db.nodeCache.Put(key, nil)
return db.writeBuffer.Put(key, nil)
}
func (db *intermediateNodeDB) Clear() error {
db.nodeCache.Flush()
// Reset the buffer. Note we don't flush because that would cause us to
// persist intermediate nodes we're about to delete.
db.writeBuffer = newOnEvictCache(
db.writeBuffer.maxSize,
db.writeBuffer.size,
db.writeBuffer.onEviction,
)
return database.AtomicClearPrefix(db.baseDB, db.baseDB, intermediateNodePrefix)
}