go-ethereum/trie/nomttrie/stem.go
weiihann fbeb697099 nomt: Phase C — stem hash computation and flat state helpers
Add stem-level flat state storage and the groupAndHashStems pipeline
that bridges NomtTrie's per-slot updates to NOMT's page-tree:

- stemValueDBKey/stemValueDBPrefix: ethdb key format (0x03||stem||suffix)
- loadStemValues: prefix-iteration loader for all 256 slots of a stem
- writeStemValues: batch writer with nil=delete semantics
- groupAndHashStems: sort→group→load→merge→hash→write pipeline
  producing sorted []core.StemKeyValue for the page tree

Cross-validated against bintrie's StemNode.Hash algorithm.
11 new tests, all passing with -race.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 22:35:23 +08:00

158 lines
3.9 KiB
Go

package nomttrie
import (
"sort"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/nomt/core"
)
// nomtStemValuePrefix is the ethdb key prefix for stem value flat state.
// Key format: 0x03 || stem[31] || suffix[1] → value[32]
const nomtStemValuePrefix byte = 0x03
// stemValueDBKey returns the ethdb key for a specific (stem, suffix) value slot.
func stemValueDBKey(stem core.StemPath, suffix byte) []byte {
key := make([]byte, 1+core.StemSize+1)
key[0] = nomtStemValuePrefix
copy(key[1:], stem[:])
key[1+core.StemSize] = suffix
return key
}
// stemValueDBPrefix returns the ethdb prefix for all values of a stem.
func stemValueDBPrefix(stem core.StemPath) []byte {
prefix := make([]byte, 1+core.StemSize)
prefix[0] = nomtStemValuePrefix
copy(prefix[1:], stem[:])
return prefix
}
// loadStemValues loads all existing values for a stem from flat state.
// Uses prefix iteration to efficiently find only populated slots.
func loadStemValues(diskdb ethdb.Database, stem core.StemPath) ([core.StemNodeWidth][]byte, error) {
var values [core.StemNodeWidth][]byte
prefix := stemValueDBPrefix(stem)
it := diskdb.NewIterator(prefix, nil)
defer it.Release()
for it.Next() {
key := it.Key()
if len(key) != 1+core.StemSize+1 {
continue
}
suffix := key[1+core.StemSize]
value := make([]byte, len(it.Value()))
copy(value, it.Value())
values[suffix] = value
}
if err := it.Error(); err != nil {
return values, err
}
return values, nil
}
// writeStemValues writes updated stem values to an ethdb batch.
// Nil values delete the key; non-nil values overwrite.
func writeStemValues(batch ethdb.Batch, stem core.StemPath, updates map[byte][]byte) error {
for suffix, value := range updates {
key := stemValueDBKey(stem, suffix)
if value == nil {
if err := batch.Delete(key); err != nil {
return err
}
} else {
if err := batch.Put(key, value); err != nil {
return err
}
}
}
return nil
}
// groupAndHashStems groups stem updates by stem path, loads existing values
// from flat state, merges updates, computes SHA256 stem hashes, writes updated
// values back to flat state, and returns sorted StemKeyValue pairs for the
// NOMT page tree.
//
// Returns only stems that have at least one non-nil value (empty stems are
// excluded, effectively deleting them from the trie).
func groupAndHashStems(
updates []stemUpdate,
diskdb ethdb.Database,
) ([]core.StemKeyValue, error) {
if len(updates) == 0 {
return nil, nil
}
// Sort by stem, then suffix.
sort.Slice(updates, func(i, j int) bool {
if updates[i].Stem != updates[j].Stem {
return stemLess(&updates[i].Stem, &updates[j].Stem)
}
return updates[i].Suffix < updates[j].Suffix
})
batch := diskdb.NewBatch()
result := make([]core.StemKeyValue, 0, len(updates)/2+1)
// Process groups sharing the same stem.
idx := 0
for idx < len(updates) {
stem := updates[idx].Stem
// Load existing values.
values, err := loadStemValues(diskdb, stem)
if err != nil {
return nil, err
}
// Apply updates.
flatUpdates := make(map[byte][]byte, 4)
for idx < len(updates) && updates[idx].Stem == stem {
u := updates[idx]
values[u.Suffix] = u.Value
flatUpdates[u.Suffix] = u.Value
idx++
}
// Write to flat state.
if err := writeStemValues(batch, stem, flatUpdates); err != nil {
return nil, err
}
// Only include stems with at least one non-nil value.
hasValue := false
for _, v := range values {
if v != nil {
hasValue = true
break
}
}
if hasValue {
result = append(result, core.StemKeyValue{
Stem: stem,
Hash: core.HashStem(stem, values),
})
}
}
if err := batch.Write(); err != nil {
return nil, err
}
return result, nil
}
// stemLess compares two stem paths lexicographically.
func stemLess(a, b *core.StemPath) bool {
for i := range a {
if a[i] < b[i] {
return true
}
if a[i] > b[i] {
return false
}
}
return false
}