mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-06-19 21:31:37 +00:00
236 lines
6.2 KiB
Go
236 lines
6.2 KiB
Go
// Package db provides the NOMT trie database combining PebbleDB page
|
|
// storage with the PageWalker merkle engine.
|
|
//
|
|
// Trie pages are stored as 4KB blobs in geth's ethdb under key prefix 0x04.
|
|
// Flat key-value storage (accounts, storage slots) stays on geth's PebbleDB
|
|
// under separate prefixes managed by triedb/nomtdb.
|
|
package db
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"runtime"
|
|
"sort"
|
|
"sync"
|
|
|
|
"github.com/ethereum/go-ethereum/ethdb"
|
|
"github.com/ethereum/go-ethereum/nomt/core"
|
|
"github.com/ethereum/go-ethereum/nomt/merkle"
|
|
)
|
|
|
|
const (
|
|
// nomtPagePrefix is the ethdb key prefix for NOMT trie pages.
|
|
// Key format: 0x04 || PageID.Encode()[32] → RawPage[4032]
|
|
nomtPagePrefix byte = 0x04
|
|
|
|
// nomtMetaPrefix is the ethdb key prefix for NOMT metadata.
|
|
nomtMetaPrefix byte = 0x05
|
|
)
|
|
|
|
// nomtMetaRootKey is the ethdb key for the persisted page tree root.
|
|
var nomtMetaRootKey = []byte{nomtMetaPrefix, 'r', 'o', 'o', 't'}
|
|
|
|
// Config holds configuration for the NOMT database.
|
|
type Config struct {
|
|
// NumWorkers is the number of parallel goroutines for trie updates.
|
|
// Defaults to runtime.NumCPU() if zero.
|
|
NumWorkers int
|
|
}
|
|
|
|
// DefaultConfig returns a default configuration.
|
|
func DefaultConfig() Config {
|
|
return Config{}
|
|
}
|
|
|
|
// DB is the NOMT trie database.
|
|
type DB struct {
|
|
diskdb ethdb.Database
|
|
root core.Node
|
|
numWorkers int
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
// New creates or opens a NOMT trie database backed by the given ethdb.
|
|
// The page tree root is loaded from persisted metadata if available.
|
|
func New(diskdb ethdb.Database, config Config) (*DB, error) {
|
|
numWorkers := config.NumWorkers
|
|
if numWorkers <= 0 {
|
|
numWorkers = runtime.NumCPU()
|
|
}
|
|
|
|
db := &DB{
|
|
diskdb: diskdb,
|
|
root: core.Terminator,
|
|
numWorkers: numWorkers,
|
|
}
|
|
|
|
// Load persisted root.
|
|
if data, err := diskdb.Get(nomtMetaRootKey); err == nil && len(data) == 32 {
|
|
copy(db.root[:], data)
|
|
}
|
|
|
|
return db, nil
|
|
}
|
|
|
|
// Root returns the current trie root hash.
|
|
func (db *DB) Root() core.Node {
|
|
db.mu.RLock()
|
|
defer db.mu.RUnlock()
|
|
return db.root
|
|
}
|
|
|
|
// Update applies a batch of stem key-value pairs to the trie.
|
|
// The pairs are sorted internally before processing.
|
|
func (db *DB) Update(ops []core.StemKeyValue) (core.Node, error) {
|
|
sort.Slice(ops, func(i, j int) bool {
|
|
return stemLess(&ops[i].Stem, &ops[j].Stem)
|
|
})
|
|
return db.UpdateSorted(ops)
|
|
}
|
|
|
|
// UpdateSorted applies a pre-sorted batch of stem key-value pairs to the trie.
|
|
// The caller must ensure ops are sorted by stem path.
|
|
func (db *DB) UpdateSorted(ops []core.StemKeyValue) (core.Node, error) {
|
|
if len(ops) == 0 {
|
|
return db.Root(), nil
|
|
}
|
|
|
|
db.mu.Lock()
|
|
defer db.mu.Unlock()
|
|
|
|
pageSetFactory := func() merkle.PageSet {
|
|
return newPebblePageSet(db.diskdb)
|
|
}
|
|
out := merkle.ParallelUpdate(db.root, ops, db.numWorkers, pageSetFactory)
|
|
|
|
// Persist updated pages via atomic batch write.
|
|
batch := db.diskdb.NewBatch()
|
|
for _, up := range out.Pages {
|
|
key := nomtPageKey(up.PageID)
|
|
if up.Diff.IsCleared() {
|
|
if err := batch.Delete(key); err != nil {
|
|
return core.Terminator, fmt.Errorf("nomt/db: delete page: %w", err)
|
|
}
|
|
} else {
|
|
if err := batch.Put(key, up.Page[:]); err != nil {
|
|
return core.Terminator, fmt.Errorf("nomt/db: put page: %w", err)
|
|
}
|
|
}
|
|
}
|
|
// Persist root.
|
|
if err := batch.Put(nomtMetaRootKey, out.Root[:]); err != nil {
|
|
return core.Terminator, fmt.Errorf("nomt/db: put root: %w", err)
|
|
}
|
|
if err := batch.Write(); err != nil {
|
|
return core.Terminator, fmt.Errorf("nomt/db: batch write: %w", err)
|
|
}
|
|
|
|
db.root = out.Root
|
|
return out.Root, nil
|
|
}
|
|
|
|
// LoadPage loads a page from ethdb storage by its PageID.
|
|
func (db *DB) LoadPage(pageID core.PageID) (*core.RawPage, error) {
|
|
data, err := db.diskdb.Get(nomtPageKey(pageID))
|
|
if err != nil {
|
|
return nil, nil // Not found.
|
|
}
|
|
if len(data) != core.PageSize {
|
|
return nil, fmt.Errorf("nomt/db: page size mismatch: got %d, want %d", len(data), core.PageSize)
|
|
}
|
|
page := new(core.RawPage)
|
|
copy(page[:], data)
|
|
return page, nil
|
|
}
|
|
|
|
// Close is a no-op — the ethdb lifecycle is managed by the caller.
|
|
func (db *DB) Close() error {
|
|
return nil
|
|
}
|
|
|
|
// --- PebblePageSet ---
|
|
|
|
// pebblePageSet implements merkle.PageSet backed by ethdb (PebbleDB).
|
|
type pebblePageSet struct {
|
|
diskdb ethdb.Database
|
|
cache map[string]*core.RawPage
|
|
}
|
|
|
|
func newPebblePageSet(diskdb ethdb.Database) *pebblePageSet {
|
|
return &pebblePageSet{
|
|
diskdb: diskdb,
|
|
cache: make(map[string]*core.RawPage, 16),
|
|
}
|
|
}
|
|
|
|
func (ps *pebblePageSet) Get(pageID core.PageID) (
|
|
*core.RawPage, merkle.PageOrigin, bool,
|
|
) {
|
|
key := pageIDCacheKey(pageID)
|
|
if cached, ok := ps.cache[key]; ok {
|
|
// Return a copy so the walker can mutate freely.
|
|
pageCopy := new(core.RawPage)
|
|
*pageCopy = *cached
|
|
return pageCopy, merkle.PageOrigin{
|
|
Kind: merkle.PageOriginPersisted,
|
|
}, true
|
|
}
|
|
|
|
data, err := ps.diskdb.Get(nomtPageKey(pageID))
|
|
if err != nil || len(data) != core.PageSize {
|
|
// Return a fresh page if not found — this handles the case
|
|
// where the trie is being built from scratch or expanded
|
|
// into new regions.
|
|
fresh := new(core.RawPage)
|
|
return fresh, merkle.PageOrigin{Kind: merkle.PageOriginFresh}, true
|
|
}
|
|
|
|
page := new(core.RawPage)
|
|
copy(page[:], data)
|
|
ps.cache[key] = page
|
|
|
|
// Return a copy so the walker can mutate freely.
|
|
pageCopy := new(core.RawPage)
|
|
*pageCopy = *page
|
|
return pageCopy, merkle.PageOrigin{
|
|
Kind: merkle.PageOriginPersisted,
|
|
}, true
|
|
}
|
|
|
|
func (ps *pebblePageSet) Contains(pageID core.PageID) bool {
|
|
key := pageIDCacheKey(pageID)
|
|
if _, ok := ps.cache[key]; ok {
|
|
return true
|
|
}
|
|
has, _ := ps.diskdb.Has(nomtPageKey(pageID))
|
|
return has
|
|
}
|
|
|
|
func (ps *pebblePageSet) Fresh(pageID core.PageID) *core.RawPage {
|
|
return new(core.RawPage)
|
|
}
|
|
|
|
func (ps *pebblePageSet) Insert(
|
|
pageID core.PageID, page *core.RawPage, origin merkle.PageOrigin,
|
|
) {
|
|
ps.cache[pageIDCacheKey(pageID)] = page
|
|
}
|
|
|
|
// nomtPageKey builds the ethdb key for a NOMT trie page.
|
|
func nomtPageKey(id core.PageID) []byte {
|
|
encoded := id.Encode()
|
|
key := make([]byte, 1+len(encoded))
|
|
key[0] = nomtPagePrefix
|
|
copy(key[1:], encoded[:])
|
|
return key
|
|
}
|
|
|
|
// pageIDCacheKey returns a string key for the in-memory cache.
|
|
func pageIDCacheKey(id core.PageID) string {
|
|
encoded := id.Encode()
|
|
return string(encoded[:])
|
|
}
|
|
|
|
func stemLess(a, b *core.StemPath) bool {
|
|
return bytes.Compare(a[:], b[:]) < 0
|
|
}
|