mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-04 12:11:17 +00:00
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>
158 lines
3.9 KiB
Go
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
|
|
}
|