trie/bintrie: trim verbose doc comments to essentials

This commit is contained in:
CPerezz 2026-04-16 00:03:18 +02:00
parent ad64f4ec04
commit b4a7118d06
No known key found for this signature in database
GPG key ID: 62045F34B97177DD
8 changed files with 30 additions and 133 deletions

View file

@ -18,7 +18,7 @@ package bintrie
import "github.com/ethereum/go-ethereum/common" import "github.com/ethereum/go-ethereum/common"
// HashedNode represents an unresolved node that only stores its hash. // HashedNode is an unresolved node (hash only).
type HashedNode struct { type HashedNode struct {
hash common.Hash hash common.Hash
} }

View file

@ -34,7 +34,7 @@ func keyToPath(depth int, key []byte) ([]byte, error) {
return path, nil return path, nil
} }
// makeKeyPath is a simplified version of keyToPath that doesn't return an error. // makeKeyPath is like keyToPath but panics on invalid depth.
func makeKeyPath(depth int, key []byte) []byte { func makeKeyPath(depth int, key []byte) []byte {
path := make([]byte, 0, depth+1) path := make([]byte, 0, depth+1)
for i := range depth + 1 { for i := range depth + 1 {
@ -44,10 +44,9 @@ func makeKeyPath(depth int, key []byte) []byte {
return path return path
} }
// InternalNode is a binary trie internal node.
type InternalNode struct { type InternalNode struct {
left, right NodeRef left, right NodeRef
depth uint8 depth uint8
mustRecompute bool // true if the hash needs to be recomputed mustRecompute bool
hash common.Hash // cached hash when mustRecompute == false hash common.Hash
} }

View file

@ -47,7 +47,6 @@ func newBinaryNodeIterator(t *BinaryTrie, _ []byte) (trie.NodeIterator, error) {
return it, nil return it, nil
} }
// Next moves the iterator to the next node.
func (it *binaryNodeIterator) Next(descend bool) bool { func (it *binaryNodeIterator) Next(descend bool) bool {
if it.lastErr == errIteratorEnd { if it.lastErr == errIteratorEnd {
return false return false
@ -160,7 +159,6 @@ func (it *binaryNodeIterator) Next(descend bool) bool {
} }
} }
// Error returns the error status of the iterator.
func (it *binaryNodeIterator) Error() error { func (it *binaryNodeIterator) Error() error {
if it.lastErr == errIteratorEnd { if it.lastErr == errIteratorEnd {
return nil return nil
@ -168,12 +166,10 @@ func (it *binaryNodeIterator) Error() error {
return it.lastErr return it.lastErr
} }
// Hash returns the hash of the current node.
func (it *binaryNodeIterator) Hash() common.Hash { func (it *binaryNodeIterator) Hash() common.Hash {
return it.store.ComputeHash(it.current) return it.store.ComputeHash(it.current)
} }
// Parent returns the hash of the parent of the current node.
func (it *binaryNodeIterator) Parent() common.Hash { func (it *binaryNodeIterator) Parent() common.Hash {
if len(it.stack) < 2 { if len(it.stack) < 2 {
return common.Hash{} return common.Hash{}
@ -181,7 +177,7 @@ func (it *binaryNodeIterator) Parent() common.Hash {
return it.store.ComputeHash(it.stack[len(it.stack)-2].Node) return it.store.ComputeHash(it.stack[len(it.stack)-2].Node)
} }
// Path returns the hex-encoded path to the current node. // Path returns the bit-path to the current node.
func (it *binaryNodeIterator) Path() []byte { func (it *binaryNodeIterator) Path() []byte {
if it.Leaf() { if it.Leaf() {
return it.LeafKey() return it.LeafKey()
@ -196,12 +192,10 @@ func (it *binaryNodeIterator) Path() []byte {
return path return path
} }
// NodeBlob returns the serialized bytes of the current node.
func (it *binaryNodeIterator) NodeBlob() []byte { func (it *binaryNodeIterator) NodeBlob() []byte {
return it.store.SerializeNode(it.current) return it.store.SerializeNode(it.current)
} }
// Leaf returns true iff the current node is a leaf node.
func (it *binaryNodeIterator) Leaf() bool { func (it *binaryNodeIterator) Leaf() bool {
if it.current.Kind() != KindStem { if it.current.Kind() != KindStem {
return false return false
@ -221,7 +215,6 @@ func (it *binaryNodeIterator) Leaf() bool {
return sn.hasValue(byte(currentValueIndex)) return sn.hasValue(byte(currentValueIndex))
} }
// LeafKey returns the key of the leaf.
func (it *binaryNodeIterator) LeafKey() []byte { func (it *binaryNodeIterator) LeafKey() []byte {
if it.current.Kind() != KindStem { if it.current.Kind() != KindStem {
panic("Leaf() called on an binary node iterator not at a leaf location") panic("Leaf() called on an binary node iterator not at a leaf location")
@ -230,7 +223,6 @@ func (it *binaryNodeIterator) LeafKey() []byte {
return sn.Key(it.stack[len(it.stack)-1].Index - 1) return sn.Key(it.stack[len(it.stack)-1].Index - 1)
} }
// LeafBlob returns the content of the leaf.
func (it *binaryNodeIterator) LeafBlob() []byte { func (it *binaryNodeIterator) LeafBlob() []byte {
if it.current.Kind() != KindStem { if it.current.Kind() != KindStem {
panic("LeafBlob() called on an binary node iterator not at a leaf location") panic("LeafBlob() called on an binary node iterator not at a leaf location")
@ -239,7 +231,6 @@ func (it *binaryNodeIterator) LeafBlob() []byte {
return sn.getValue(byte(it.stack[len(it.stack)-1].Index - 1)) return sn.getValue(byte(it.stack[len(it.stack)-1].Index - 1))
} }
// LeafProof returns the Merkle proof of the leaf.
func (it *binaryNodeIterator) LeafProof() [][]byte { func (it *binaryNodeIterator) LeafProof() [][]byte {
if it.current.Kind() != KindStem { if it.current.Kind() != KindStem {
panic("LeafProof() called on an binary node iterator not at a leaf location") panic("LeafProof() called on an binary node iterator not at a leaf location")
@ -274,8 +265,7 @@ func (it *binaryNodeIterator) LeafProof() [][]byte {
return proof return proof
} }
// AddResolver sets an intermediate database to use for looking up trie nodes // AddResolver is a no-op (satisfies the NodeIterator interface).
// before reaching into the real persistent layer.
func (it *binaryNodeIterator) AddResolver(trie.NodeResolver) { func (it *binaryNodeIterator) AddResolver(trie.NodeResolver) {
// Not implemented, but should not panic // Not implemented, but should not panic
} }

View file

@ -20,10 +20,10 @@ package bintrie
type NodeKind uint8 type NodeKind uint8
const ( const (
KindEmpty NodeKind = iota // no node KindEmpty NodeKind = iota
KindInternal // internal binary branching node KindInternal
KindStem // leaf group containing up to 256 values KindStem // up to 256 values per stem
KindHashed // unresolved node (hash only) KindHashed
) )
// NodeRef is a compact, GC-invisible reference to a node in a NodeStore. // NodeRef is a compact, GC-invisible reference to a node in a NodeStore.
@ -37,20 +37,17 @@ const (
kindShift uint32 = 30 kindShift uint32 = 30
indexMask uint32 = (1 << kindShift) - 1 indexMask uint32 = (1 << kindShift) - 1
// EmptyRef is the zero-value NodeRef, representing an empty node. // EmptyRef represents an empty node.
EmptyRef NodeRef = 0 EmptyRef NodeRef = 0
) )
// MakeRef creates a NodeRef from a kind and pool index.
func MakeRef(kind NodeKind, idx uint32) NodeRef { func MakeRef(kind NodeKind, idx uint32) NodeRef {
return NodeRef(uint32(kind)<<kindShift | (idx & indexMask)) return NodeRef(uint32(kind)<<kindShift | (idx & indexMask))
} }
// Kind returns the node type tag.
func (r NodeRef) Kind() NodeKind { return NodeKind(uint32(r) >> kindShift) } func (r NodeRef) Kind() NodeKind { return NodeKind(uint32(r) >> kindShift) }
// Index returns the pool index within the node's typed pool. // Index within the typed pool.
func (r NodeRef) Index() uint32 { return uint32(r) & indexMask } func (r NodeRef) Index() uint32 { return uint32(r) & indexMask }
// IsEmpty returns true if this ref represents an empty node.
func (r NodeRef) IsEmpty() bool { return r == EmptyRef } func (r NodeRef) IsEmpty() bool { return r == EmptyRef }

View file

@ -24,31 +24,16 @@ import "github.com/ethereum/go-ethereum/common"
// of the backing data, only the outer pointer slice grows). // of the backing data, only the outer pointer slice grows).
const storeChunkSize = 4096 const storeChunkSize = 4096
// NodeStore is a GC-friendly arena for binary trie nodes. // NodeStore is a GC-friendly arena for binary trie nodes. Nodes are packed
// // into typed chunked pools so pointer-free types (InternalNode, HashedNode)
// Instead of allocating each node as a separate heap object with interface // land in noscan spans the GC skips entirely.
// pointers (which the GC must scan), NodeStore packs nodes into typed chunked
// pools. InternalNode and HashedNode contain ZERO Go pointers, so their pool
// backing arrays are allocated in noscan spans — the GC skips them entirely.
// StemNode has one pointer (valueData []byte) per node.
//
// For a trie with 25K InternalNodes, this reduces GC-scanned pointer-words
// from ~125K (with interface-based nodes) to ~25K (just StemNode valueData),
// an ~80% reduction. At mainnet scale (millions of nodes), this prevents
// multi-second GC pauses.
type NodeStore struct { type NodeStore struct {
// InternalNode pool — NOSCAN: InternalNode contains zero Go pointers.
// Children are NodeRef (uint32), hash is [32]byte.
internalChunks []*[storeChunkSize]InternalNode internalChunks []*[storeChunkSize]InternalNode
internalCount uint32 internalCount uint32
// StemNode pool — each StemNode has one pointer (valueData []byte).
// Still much better than the old design where each InternalNode had
// two BinaryNode interface pointers (4 pointer-words each).
stemChunks []*[storeChunkSize]StemNode stemChunks []*[storeChunkSize]StemNode
stemCount uint32 stemCount uint32
// HashedNode pool — NOSCAN: HashedNode is just [32]byte.
hashedChunks []*[storeChunkSize]HashedNode hashedChunks []*[storeChunkSize]HashedNode
hashedCount uint32 hashedCount uint32
@ -60,19 +45,14 @@ type NodeStore struct {
freeHashed []uint32 freeHashed []uint32
} }
// NewNodeStore creates a new empty NodeStore.
func NewNodeStore() *NodeStore { func NewNodeStore() *NodeStore {
return &NodeStore{root: EmptyRef} return &NodeStore{root: EmptyRef}
} }
// Root returns the root NodeRef.
func (s *NodeStore) Root() NodeRef { return s.root } func (s *NodeStore) Root() NodeRef { return s.root }
// SetRoot sets the root NodeRef.
func (s *NodeStore) SetRoot(ref NodeRef) { s.root = ref } func (s *NodeStore) SetRoot(ref NodeRef) { s.root = ref }
// --- InternalNode allocation ---
func (s *NodeStore) allocInternal() uint32 { func (s *NodeStore) allocInternal() uint32 {
if n := len(s.freeInternals); n > 0 { if n := len(s.freeInternals); n > 0 {
idx := s.freeInternals[n-1] idx := s.freeInternals[n-1]
@ -96,7 +76,6 @@ func (s *NodeStore) getInternal(idx uint32) *InternalNode {
return &s.internalChunks[idx/storeChunkSize][idx%storeChunkSize] return &s.internalChunks[idx/storeChunkSize][idx%storeChunkSize]
} }
// newInternalRef allocates an InternalNode and returns its NodeRef.
func (s *NodeStore) newInternalRef(depth int) NodeRef { func (s *NodeStore) newInternalRef(depth int) NodeRef {
if depth > 248 { if depth > 248 {
panic("node depth exceeds maximum binary trie depth") panic("node depth exceeds maximum binary trie depth")
@ -108,8 +87,6 @@ func (s *NodeStore) newInternalRef(depth int) NodeRef {
return MakeRef(KindInternal, idx) return MakeRef(KindInternal, idx)
} }
// --- StemNode allocation ---
func (s *NodeStore) allocStem() uint32 { func (s *NodeStore) allocStem() uint32 {
if n := len(s.freeStems); n > 0 { if n := len(s.freeStems); n > 0 {
idx := s.freeStems[n-1] idx := s.freeStems[n-1]
@ -133,7 +110,6 @@ func (s *NodeStore) getStem(idx uint32) *StemNode {
return &s.stemChunks[idx/storeChunkSize][idx%storeChunkSize] return &s.stemChunks[idx/storeChunkSize][idx%storeChunkSize]
} }
// newStemRef allocates a StemNode with the given stem/depth and returns its NodeRef.
func (s *NodeStore) newStemRef(stem []byte, depth int) NodeRef { func (s *NodeStore) newStemRef(stem []byte, depth int) NodeRef {
if depth > 248 { if depth > 248 {
panic("node depth exceeds maximum binary trie depth") panic("node depth exceeds maximum binary trie depth")
@ -146,8 +122,6 @@ func (s *NodeStore) newStemRef(stem []byte, depth int) NodeRef {
return MakeRef(KindStem, idx) return MakeRef(KindStem, idx)
} }
// --- HashedNode allocation ---
func (s *NodeStore) allocHashed() uint32 { func (s *NodeStore) allocHashed() uint32 {
if n := len(s.freeHashed); n > 0 { if n := len(s.freeHashed); n > 0 {
idx := s.freeHashed[n-1] idx := s.freeHashed[n-1]
@ -175,14 +149,12 @@ func (s *NodeStore) freeHashedNode(idx uint32) {
s.freeHashed = append(s.freeHashed, idx) s.freeHashed = append(s.freeHashed, idx)
} }
// newHashedRef allocates a HashedNode and returns its NodeRef.
func (s *NodeStore) newHashedRef(hash common.Hash) NodeRef { func (s *NodeStore) newHashedRef(hash common.Hash) NodeRef {
idx := s.allocHashed() idx := s.allocHashed()
*s.getHashed(idx) = HashedNode{hash: hash} *s.getHashed(idx) = HashedNode{hash: hash}
return MakeRef(KindHashed, idx) return MakeRef(KindHashed, idx)
} }
// Copy creates a deep copy of the NodeStore and all its nodes.
func (s *NodeStore) Copy() *NodeStore { func (s *NodeStore) Copy() *NodeStore {
ns := &NodeStore{ ns := &NodeStore{
root: s.root, root: s.root,
@ -190,21 +162,16 @@ func (s *NodeStore) Copy() *NodeStore {
stemCount: s.stemCount, stemCount: s.stemCount,
hashedCount: s.hashedCount, hashedCount: s.hashedCount,
} }
// Deep copy internal chunks
ns.internalChunks = make([]*[storeChunkSize]InternalNode, len(s.internalChunks)) ns.internalChunks = make([]*[storeChunkSize]InternalNode, len(s.internalChunks))
for i, chunk := range s.internalChunks { for i, chunk := range s.internalChunks {
cp := *chunk cp := *chunk
ns.internalChunks[i] = &cp ns.internalChunks[i] = &cp
} }
// Deep copy stem chunks (need to deep copy valueData)
ns.stemChunks = make([]*[storeChunkSize]StemNode, len(s.stemChunks)) ns.stemChunks = make([]*[storeChunkSize]StemNode, len(s.stemChunks))
for i, chunk := range s.stemChunks { for i, chunk := range s.stemChunks {
cp := *chunk cp := *chunk
ns.stemChunks[i] = &cp ns.stemChunks[i] = &cp
} }
// Deep copy pointer fields for each active stem
for i := uint32(0); i < s.stemCount; i++ { for i := uint32(0); i < s.stemCount; i++ {
src := s.getStem(i) src := s.getStem(i)
dst := ns.getStem(i) dst := ns.getStem(i)
@ -213,15 +180,11 @@ func (s *NodeStore) Copy() *NodeStore {
copy(dst.valueData, src.valueData) copy(dst.valueData, src.valueData)
} }
} }
// Deep copy hashed chunks
ns.hashedChunks = make([]*[storeChunkSize]HashedNode, len(s.hashedChunks)) ns.hashedChunks = make([]*[storeChunkSize]HashedNode, len(s.hashedChunks))
for i, chunk := range s.hashedChunks { for i, chunk := range s.hashedChunks {
cp := *chunk cp := *chunk
ns.hashedChunks[i] = &cp ns.hashedChunks[i] = &cp
} }
// Copy free lists
if len(s.freeInternals) > 0 { if len(s.freeInternals) > 0 {
ns.freeInternals = make([]uint32, len(s.freeInternals)) ns.freeInternals = make([]uint32, len(s.freeInternals))
copy(ns.freeInternals, s.freeInternals) copy(ns.freeInternals, s.freeInternals)

View file

@ -22,23 +22,20 @@ import (
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
) )
// StemNode represents a group of `StemNodeWidth` values sharing the same stem. // StemNode holds up to 256 values sharing a 31-byte stem, packed via bitmap.
// It uses a packed representation: bitmap indicates which of the 256 positions
// have values, and valueData stores the values contiguously in bitmap order.
type StemNode struct { type StemNode struct {
Stem [StemSize]byte // Stem path to get to StemNodeWidth values Stem [StemSize]byte
bitmap [StemBitmapSize]byte // bitmap indicating which positions have values bitmap [StemBitmapSize]byte
valueData []byte // packed value data (count * HashSize bytes) valueData []byte
count uint16 // number of values present count uint16
depth uint8 // Depth of the node depth uint8
shared bool // true if valueData is shared with serialized input shared bool // true if valueData is shared with serialized input
mustRecompute bool // true if the hash needs to be recomputed mustRecompute bool // true if the hash needs to be recomputed
hash common.Hash // cached hash when mustRecompute == false hash common.Hash // cached hash when mustRecompute == false
} }
// posInData returns the index within valueData for the given suffix. // posInData returns the offset within valueData, or -1 if absent.
// Returns -1 if the suffix is not present.
func (sn *StemNode) posInData(suffix byte) int { func (sn *StemNode) posInData(suffix byte) int {
idx := int(suffix) idx := int(suffix)
if sn.bitmap[idx/8]>>(7-(idx%8))&1 == 0 { if sn.bitmap[idx/8]>>(7-(idx%8))&1 == 0 {
@ -56,7 +53,6 @@ func (sn *StemNode) posInData(suffix byte) int {
return pos return pos
} }
// getValue returns the value at the given suffix, or nil if not present.
func (sn *StemNode) getValue(suffix byte) []byte { func (sn *StemNode) getValue(suffix byte) []byte {
pos := sn.posInData(suffix) pos := sn.posInData(suffix)
if pos < 0 { if pos < 0 {
@ -66,7 +62,6 @@ func (sn *StemNode) getValue(suffix byte) []byte {
return sn.valueData[start : start+HashSize] return sn.valueData[start : start+HashSize]
} }
// hasValue returns true if the given suffix has a value.
func (sn *StemNode) hasValue(suffix byte) bool { func (sn *StemNode) hasValue(suffix byte) bool {
idx := int(suffix) idx := int(suffix)
return sn.bitmap[idx/8]>>(7-(idx%8))&1 == 1 return sn.bitmap[idx/8]>>(7-(idx%8))&1 == 1
@ -85,7 +80,7 @@ func (sn *StemNode) allValues() [][]byte {
return values return values
} }
// ensureWritable makes the valueData writable (copies if shared with serialized input). // ensureWritable copies valueData if shared (copy-on-write).
func (sn *StemNode) ensureWritable() { func (sn *StemNode) ensureWritable() {
if sn.shared || cap(sn.valueData)-len(sn.valueData) < HashSize { if sn.shared || cap(sn.valueData)-len(sn.valueData) < HashSize {
newData := make([]byte, len(sn.valueData), len(sn.valueData)+HashSize*4) newData := make([]byte, len(sn.valueData), len(sn.valueData)+HashSize*4)
@ -95,21 +90,17 @@ func (sn *StemNode) ensureWritable() {
} }
} }
// setValue sets or inserts a value at the given suffix.
func (sn *StemNode) setValue(suffix byte, value []byte) { func (sn *StemNode) setValue(suffix byte, value []byte) {
sn.ensureWritable() sn.ensureWritable()
idx := int(suffix) idx := int(suffix)
pos := sn.posInData(suffix) pos := sn.posInData(suffix)
if pos >= 0 { if pos >= 0 {
// Overwrite existing value
copy(sn.valueData[pos*HashSize:], value[:HashSize]) copy(sn.valueData[pos*HashSize:], value[:HashSize])
return return
} }
// New value: insert into bitmap and valueData at the correct position.
sn.bitmap[idx/8] |= 1 << (7 - (idx % 8)) sn.bitmap[idx/8] |= 1 << (7 - (idx % 8))
sn.count++ sn.count++
// Find the correct position in valueData (count bits before this position).
insertPos := 0 insertPos := 0
byteIdx := idx / 8 byteIdx := idx / 8
for i := 0; i < byteIdx; i++ { for i := 0; i < byteIdx; i++ {
@ -118,17 +109,12 @@ func (sn *StemNode) setValue(suffix byte, value []byte) {
mask := byte(0xFF) << (8 - (idx % 8)) mask := byte(0xFF) << (8 - (idx % 8))
insertPos += bits.OnesCount8(sn.bitmap[byteIdx] & mask) insertPos += bits.OnesCount8(sn.bitmap[byteIdx] & mask)
// Insert value at the correct position in valueData.
insertOffset := insertPos * HashSize insertOffset := insertPos * HashSize
// Grow the slice
sn.valueData = append(sn.valueData, make([]byte, HashSize)...) sn.valueData = append(sn.valueData, make([]byte, HashSize)...)
// Shift data after insertion point
copy(sn.valueData[insertOffset+HashSize:], sn.valueData[insertOffset:len(sn.valueData)-HashSize]) copy(sn.valueData[insertOffset+HashSize:], sn.valueData[insertOffset:len(sn.valueData)-HashSize])
// Copy the new value
copy(sn.valueData[insertOffset:], value[:HashSize]) copy(sn.valueData[insertOffset:], value[:HashSize])
} }
// Hash returns the hash of the node.
func (sn *StemNode) Hash() common.Hash { func (sn *StemNode) Hash() common.Hash {
if !sn.mustRecompute { if !sn.mustRecompute {
return sn.hash return sn.hash
@ -175,7 +161,6 @@ func (sn *StemNode) Hash() common.Hash {
return sn.hash return sn.hash
} }
// Key returns the full key for the given index.
func (sn *StemNode) Key(i int) []byte { func (sn *StemNode) Key(i int) []byte {
var ret [HashSize]byte var ret [HashSize]byte
copy(ret[:], sn.Stem[:]) copy(ret[:], sn.Stem[:])

View file

@ -25,15 +25,12 @@ import (
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
) )
// NodeFlushFn is called during commit to flush serialized nodes.
type NodeFlushFn func(path []byte, hash common.Hash, serialized []byte) type NodeFlushFn func(path []byte, hash common.Hash, serialized []byte)
// Hash computes and returns the root hash.
func (s *NodeStore) Hash() common.Hash { func (s *NodeStore) Hash() common.Hash {
return s.ComputeHash(s.root) return s.ComputeHash(s.root)
} }
// ComputeHash computes the hash of the node referenced by ref.
func (s *NodeStore) ComputeHash(ref NodeRef) common.Hash { func (s *NodeStore) ComputeHash(ref NodeRef) common.Hash {
switch ref.Kind() { switch ref.Kind() {
case KindInternal: case KindInternal:
@ -49,11 +46,7 @@ func (s *NodeStore) ComputeHash(ref NodeRef) common.Hash {
} }
} }
// hashInternal computes the hash of an InternalNode. At shallow depths // hashInternal hashes an InternalNode, parallelising at shallow depths.
// (< parallelHashDepth), the left subtree is hashed in a goroutine while
// the right subtree is hashed inline. This is safe because left and right
// subtrees are disjoint in a well-formed tree — no node appears in both.
// ComputeHash must not be called concurrently with mutations to the NodeStore.
func (s *NodeStore) hashInternal(idx uint32) common.Hash { func (s *NodeStore) hashInternal(idx uint32) common.Hash {
node := s.getInternal(idx) node := s.getInternal(idx)
if !node.mustRecompute { if !node.mustRecompute {
@ -96,11 +89,7 @@ func (s *NodeStore) hashInternal(idx uint32) common.Hash {
return node.hash return node.hash
} }
// --- Serialization --- // SerializeNode serializes a node into the flat on-disk format.
// SerializeNode serializes a node referenced by ref into the flat format:
// - InternalNode: [nodeTypeInternal(1)][leftHash(32)][rightHash(32)] = 65 bytes
// - StemNode: [nodeTypeStem(1)][stem(31)][bitmap(32)][valueData(variable)]
func (s *NodeStore) SerializeNode(ref NodeRef) []byte { func (s *NodeStore) SerializeNode(ref NodeRef) []byte {
switch ref.Kind() { switch ref.Kind() {
case KindInternal: case KindInternal:
@ -128,18 +117,14 @@ func (s *NodeStore) SerializeNode(ref NodeRef) []byte {
} }
} }
// --- Deserialization ---
var errInvalidSerializedLength = errors.New("invalid serialized node length") var errInvalidSerializedLength = errors.New("invalid serialized node length")
// DeserializeNode deserializes a node from bytes, recomputing its hash. // DeserializeNode deserializes a node from bytes, recomputing its hash.
// The serialized buffer must not be modified after this call.
func (s *NodeStore) DeserializeNode(serialized []byte, depth int) (NodeRef, error) { func (s *NodeStore) DeserializeNode(serialized []byte, depth int) (NodeRef, error) {
return s.deserializeNode(serialized, depth, common.Hash{}, true) return s.deserializeNode(serialized, depth, common.Hash{}, true)
} }
// DeserializeNodeWithHash deserializes a node, using the provided hash. // DeserializeNodeWithHash deserializes a node, using the provided hash.
// The serialized buffer must not be modified after this call.
func (s *NodeStore) DeserializeNodeWithHash(serialized []byte, depth int, hn common.Hash) (NodeRef, error) { func (s *NodeStore) DeserializeNodeWithHash(serialized []byte, depth int, hn common.Hash) (NodeRef, error) {
return s.deserializeNode(serialized, depth, hn, false) return s.deserializeNode(serialized, depth, hn, false)
} }
@ -195,9 +180,7 @@ func (s *NodeStore) deserializeNode(serialized []byte, depth int, hn common.Hash
if len(serialized) < dataEnd { if len(serialized) < dataEnd {
return EmptyRef, errInvalidSerializedLength return EmptyRef, errInvalidSerializedLength
} }
// Zero-copy: valueData aliases the serialized buffer. The shared // Zero-copy: aliases the serialized buffer; ensureWritable() COWs before mutation.
// flag triggers copy-on-write via ensureWritable() before mutation.
// Callers must not modify serialized after this call.
sn.valueData = serialized[dataStart:dataEnd] sn.valueData = serialized[dataStart:dataEnd]
sn.shared = true sn.shared = true
sn.depth = uint8(depth) sn.depth = uint8(depth)
@ -210,10 +193,7 @@ func (s *NodeStore) deserializeNode(serialized []byte, depth int, hn common.Hash
} }
} }
// --- CollectNodes (Commit) --- // CollectNodes flushes every node via flushfn in post-order.
// CollectNodes traverses the trie, serializing and flushing each node via flushfn.
// Children are flushed before their parents (post-order traversal).
func (s *NodeStore) CollectNodes(ref NodeRef, path []byte, flushfn NodeFlushFn) error { func (s *NodeStore) CollectNodes(ref NodeRef, path []byte, flushfn NodeFlushFn) error {
switch ref.Kind() { switch ref.Kind() {
case KindEmpty: case KindEmpty:
@ -244,7 +224,6 @@ func (s *NodeStore) CollectNodes(ref NodeRef, path []byte, flushfn NodeFlushFn)
} }
} }
// ToDot generates a DOT representation for debugging.
func (s *NodeStore) ToDot(ref NodeRef, parent, path string) string { func (s *NodeStore) ToDot(ref NodeRef, parent, path string) string {
switch ref.Kind() { switch ref.Kind() {
case KindInternal: case KindInternal:

View file

@ -26,12 +26,10 @@ import (
// NodeResolverFn resolves a hashed node from the database. // NodeResolverFn resolves a hashed node from the database.
type NodeResolverFn func([]byte, common.Hash) ([]byte, error) type NodeResolverFn func([]byte, common.Hash) ([]byte, error)
// GetSingle retrieves a single value at stem[suffix] from the trie root.
func (s *NodeStore) GetSingle(stem []byte, suffix byte, resolver NodeResolverFn) ([]byte, error) { func (s *NodeStore) GetSingle(stem []byte, suffix byte, resolver NodeResolverFn) ([]byte, error) {
return s.getSingle(s.root, stem, suffix, resolver) return s.getSingle(s.root, stem, suffix, resolver)
} }
// getSingle retrieves a single value using iterative traversal.
func (s *NodeStore) getSingle(ref NodeRef, stem []byte, suffix byte, resolver NodeResolverFn) ([]byte, error) { func (s *NodeStore) getSingle(ref NodeRef, stem []byte, suffix byte, resolver NodeResolverFn) ([]byte, error) {
cur := ref cur := ref
// Track parent for HashedNode resolution (update parent's child ref). // Track parent for HashedNode resolution (update parent's child ref).
@ -100,12 +98,10 @@ func (s *NodeStore) getSingle(ref NodeRef, stem []byte, suffix byte, resolver No
} }
} }
// GetValuesAtStem retrieves all 256 values at a stem.
func (s *NodeStore) GetValuesAtStem(stem []byte, resolver NodeResolverFn) ([][]byte, error) { func (s *NodeStore) GetValuesAtStem(stem []byte, resolver NodeResolverFn) ([][]byte, error) {
return s.getValuesAtStem(s.root, stem, resolver) return s.getValuesAtStem(s.root, stem, resolver)
} }
// getValuesAtStem uses iterative traversal to find the StemNode.
func (s *NodeStore) getValuesAtStem(ref NodeRef, stem []byte, resolver NodeResolverFn) ([][]byte, error) { func (s *NodeStore) getValuesAtStem(ref NodeRef, stem []byte, resolver NodeResolverFn) ([][]byte, error) {
cur := ref cur := ref
var parentIdx uint32 var parentIdx uint32
@ -173,13 +169,11 @@ func (s *NodeStore) getValuesAtStem(ref NodeRef, stem []byte, resolver NodeResol
} }
} }
// InsertSingle inserts a single value at stem[suffix] into the trie.
func (s *NodeStore) InsertSingle(stem []byte, suffix byte, value []byte, resolver NodeResolverFn) error { func (s *NodeStore) InsertSingle(stem []byte, suffix byte, value []byte, resolver NodeResolverFn) error {
if len(value) != HashSize { if len(value) != HashSize {
return errors.New("invalid insertion: value length") return errors.New("invalid insertion: value length")
} }
// Handle root-is-empty case
if s.root.IsEmpty() { if s.root.IsEmpty() {
ref := s.newStemRef(stem, 0) ref := s.newStemRef(stem, 0)
sn := s.getStem(ref.Index()) sn := s.getStem(ref.Index())
@ -188,7 +182,6 @@ func (s *NodeStore) InsertSingle(stem []byte, suffix byte, value []byte, resolve
return nil return nil
} }
// Handle root-is-stem case
if s.root.Kind() == KindStem { if s.root.Kind() == KindStem {
sn := s.getStem(s.root.Index()) sn := s.getStem(s.root.Index())
if sn.Stem == [StemSize]byte(stem[:StemSize]) { if sn.Stem == [StemSize]byte(stem[:StemSize]) {
@ -196,17 +189,14 @@ func (s *NodeStore) InsertSingle(stem []byte, suffix byte, value []byte, resolve
sn.mustRecompute = true sn.mustRecompute = true
return nil return nil
} }
// Different stem — promote root to internal node via split
newRoot := s.splitStemInsert(s.root, stem, suffix, value, int(sn.depth)) newRoot := s.splitStemInsert(s.root, stem, suffix, value, int(sn.depth))
s.root = newRoot s.root = newRoot
return nil return nil
} }
// Root is an InternalNode — iterative descent
return s.insertSingleInternal(stem, suffix, value, resolver) return s.insertSingleInternal(stem, suffix, value, resolver)
} }
// insertSingleInternal handles insertion when root is an InternalNode.
func (s *NodeStore) insertSingleInternal(stem []byte, suffix byte, value []byte, resolver NodeResolverFn) error { func (s *NodeStore) insertSingleInternal(stem []byte, suffix byte, value []byte, resolver NodeResolverFn) error {
type pathEntry struct { type pathEntry struct {
internalIdx uint32 internalIdx uint32
@ -297,8 +287,7 @@ func (s *NodeStore) insertSingleInternal(stem []byte, suffix byte, value []byte,
} }
} }
// splitStemInsert handles the case where we need to split a StemNode // splitStemInsert splits a StemNode into InternalNodes for a divergent stem.
// into a chain of InternalNodes because the new key has a different stem.
func (s *NodeStore) splitStemInsert(existingRef NodeRef, newStem []byte, suffix byte, value []byte, depth int) NodeRef { func (s *NodeStore) splitStemInsert(existingRef NodeRef, newStem []byte, suffix byte, value []byte, depth int) NodeRef {
existing := s.getStem(existingRef.Index()) existing := s.getStem(existingRef.Index())
existingDepth := depth existingDepth := depth
@ -360,7 +349,6 @@ func (s *NodeStore) splitStemInsert(existingRef NodeRef, newStem []byte, suffix
} }
} }
// InsertValuesAtStem inserts a full group of values at the given stem.
func (s *NodeStore) InsertValuesAtStem(stem []byte, values [][]byte, resolver NodeResolverFn) error { func (s *NodeStore) InsertValuesAtStem(stem []byte, values [][]byte, resolver NodeResolverFn) error {
newRoot, err := s.insertValuesAtStem(s.root, stem, values, resolver, 0) newRoot, err := s.insertValuesAtStem(s.root, stem, values, resolver, 0)
if err != nil { if err != nil {
@ -370,7 +358,6 @@ func (s *NodeStore) InsertValuesAtStem(stem []byte, values [][]byte, resolver No
return nil return nil
} }
// insertValuesAtStem recursively inserts values at a stem.
func (s *NodeStore) insertValuesAtStem(ref NodeRef, stem []byte, values [][]byte, resolver NodeResolverFn, depth int) (NodeRef, error) { func (s *NodeStore) insertValuesAtStem(ref NodeRef, stem []byte, values [][]byte, resolver NodeResolverFn, depth int) (NodeRef, error) {
switch ref.Kind() { switch ref.Kind() {
case KindInternal: case KindInternal:
@ -482,7 +469,7 @@ func (s *NodeStore) insertValuesAtStem(ref NodeRef, stem []byte, values [][]byte
} }
} }
// splitStemValuesInsert handles splitting a StemNode when inserting values with a different stem. // splitStemValuesInsert splits a StemNode when the new stem diverges.
func (s *NodeStore) splitStemValuesInsert(existingRef NodeRef, newStem []byte, values [][]byte, resolver NodeResolverFn, depth int) (NodeRef, error) { func (s *NodeStore) splitStemValuesInsert(existingRef NodeRef, newStem []byte, values [][]byte, resolver NodeResolverFn, depth int) (NodeRef, error) {
existing := s.getStem(existingRef.Index()) existing := s.getStem(existingRef.Index())
@ -542,17 +529,14 @@ func (s *NodeStore) splitStemValuesInsert(existingRef NodeRef, newStem []byte, v
return nRef, nil return nRef, nil
} }
// Insert inserts a key-value pair using the full 32-byte key.
func (s *NodeStore) Insert(key []byte, value []byte, resolver NodeResolverFn) error { func (s *NodeStore) Insert(key []byte, value []byte, resolver NodeResolverFn) error {
return s.InsertSingle(key[:StemSize], key[StemSize], value, resolver) return s.InsertSingle(key[:StemSize], key[StemSize], value, resolver)
} }
// Get retrieves the value for the given 32-byte key.
func (s *NodeStore) Get(key []byte, resolver NodeResolverFn) ([]byte, error) { func (s *NodeStore) Get(key []byte, resolver NodeResolverFn) ([]byte, error) {
return s.GetSingle(key[:StemSize], key[StemSize], resolver) return s.GetSingle(key[:StemSize], key[StemSize], resolver)
} }
// GetHeight returns the height of the trie rooted at ref.
func (s *NodeStore) GetHeight(ref NodeRef) int { func (s *NodeStore) GetHeight(ref NodeRef) int {
switch ref.Kind() { switch ref.Kind() {
case KindInternal: case KindInternal: