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"
// HashedNode represents an unresolved node that only stores its hash.
// HashedNode is an unresolved node (hash only).
type HashedNode struct {
hash common.Hash
}

View file

@ -34,7 +34,7 @@ func keyToPath(depth int, key []byte) ([]byte, error) {
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 {
path := make([]byte, 0, depth+1)
for i := range depth + 1 {
@ -44,10 +44,9 @@ func makeKeyPath(depth int, key []byte) []byte {
return path
}
// InternalNode is a binary trie internal node.
type InternalNode struct {
left, right NodeRef
depth uint8
mustRecompute bool // true if the hash needs to be recomputed
hash common.Hash // cached hash when mustRecompute == false
mustRecompute bool
hash common.Hash
}

View file

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

View file

@ -20,10 +20,10 @@ package bintrie
type NodeKind uint8
const (
KindEmpty NodeKind = iota // no node
KindInternal // internal binary branching node
KindStem // leaf group containing up to 256 values
KindHashed // unresolved node (hash only)
KindEmpty NodeKind = iota
KindInternal
KindStem // up to 256 values per stem
KindHashed
)
// NodeRef is a compact, GC-invisible reference to a node in a NodeStore.
@ -37,20 +37,17 @@ const (
kindShift uint32 = 30
indexMask uint32 = (1 << kindShift) - 1
// EmptyRef is the zero-value NodeRef, representing an empty node.
// EmptyRef represents an empty node.
EmptyRef NodeRef = 0
)
// MakeRef creates a NodeRef from a kind and pool index.
func MakeRef(kind NodeKind, idx uint32) NodeRef {
return NodeRef(uint32(kind)<<kindShift | (idx & indexMask))
}
// Kind returns the node type tag.
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 }
// IsEmpty returns true if this ref represents an empty node.
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).
const storeChunkSize = 4096
// NodeStore is a GC-friendly arena for binary trie nodes.
//
// Instead of allocating each node as a separate heap object with interface
// 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.
// NodeStore is a GC-friendly arena for binary trie nodes. Nodes are packed
// into typed chunked pools so pointer-free types (InternalNode, HashedNode)
// land in noscan spans the GC skips entirely.
type NodeStore struct {
// InternalNode pool — NOSCAN: InternalNode contains zero Go pointers.
// Children are NodeRef (uint32), hash is [32]byte.
internalChunks []*[storeChunkSize]InternalNode
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
stemCount uint32
// HashedNode pool — NOSCAN: HashedNode is just [32]byte.
hashedChunks []*[storeChunkSize]HashedNode
hashedCount uint32
@ -60,19 +45,14 @@ type NodeStore struct {
freeHashed []uint32
}
// NewNodeStore creates a new empty NodeStore.
func NewNodeStore() *NodeStore {
return &NodeStore{root: EmptyRef}
}
// Root returns the root NodeRef.
func (s *NodeStore) Root() NodeRef { return s.root }
// SetRoot sets the root NodeRef.
func (s *NodeStore) SetRoot(ref NodeRef) { s.root = ref }
// --- InternalNode allocation ---
func (s *NodeStore) allocInternal() uint32 {
if n := len(s.freeInternals); n > 0 {
idx := s.freeInternals[n-1]
@ -96,7 +76,6 @@ func (s *NodeStore) getInternal(idx uint32) *InternalNode {
return &s.internalChunks[idx/storeChunkSize][idx%storeChunkSize]
}
// newInternalRef allocates an InternalNode and returns its NodeRef.
func (s *NodeStore) newInternalRef(depth int) NodeRef {
if depth > 248 {
panic("node depth exceeds maximum binary trie depth")
@ -108,8 +87,6 @@ func (s *NodeStore) newInternalRef(depth int) NodeRef {
return MakeRef(KindInternal, idx)
}
// --- StemNode allocation ---
func (s *NodeStore) allocStem() uint32 {
if n := len(s.freeStems); n > 0 {
idx := s.freeStems[n-1]
@ -133,7 +110,6 @@ func (s *NodeStore) getStem(idx uint32) *StemNode {
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 {
if depth > 248 {
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)
}
// --- HashedNode allocation ---
func (s *NodeStore) allocHashed() uint32 {
if n := len(s.freeHashed); n > 0 {
idx := s.freeHashed[n-1]
@ -175,14 +149,12 @@ func (s *NodeStore) freeHashedNode(idx uint32) {
s.freeHashed = append(s.freeHashed, idx)
}
// newHashedRef allocates a HashedNode and returns its NodeRef.
func (s *NodeStore) newHashedRef(hash common.Hash) NodeRef {
idx := s.allocHashed()
*s.getHashed(idx) = HashedNode{hash: hash}
return MakeRef(KindHashed, idx)
}
// Copy creates a deep copy of the NodeStore and all its nodes.
func (s *NodeStore) Copy() *NodeStore {
ns := &NodeStore{
root: s.root,
@ -190,21 +162,16 @@ func (s *NodeStore) Copy() *NodeStore {
stemCount: s.stemCount,
hashedCount: s.hashedCount,
}
// Deep copy internal chunks
ns.internalChunks = make([]*[storeChunkSize]InternalNode, len(s.internalChunks))
for i, chunk := range s.internalChunks {
cp := *chunk
ns.internalChunks[i] = &cp
}
// Deep copy stem chunks (need to deep copy valueData)
ns.stemChunks = make([]*[storeChunkSize]StemNode, len(s.stemChunks))
for i, chunk := range s.stemChunks {
cp := *chunk
ns.stemChunks[i] = &cp
}
// Deep copy pointer fields for each active stem
for i := uint32(0); i < s.stemCount; i++ {
src := s.getStem(i)
dst := ns.getStem(i)
@ -213,15 +180,11 @@ func (s *NodeStore) Copy() *NodeStore {
copy(dst.valueData, src.valueData)
}
}
// Deep copy hashed chunks
ns.hashedChunks = make([]*[storeChunkSize]HashedNode, len(s.hashedChunks))
for i, chunk := range s.hashedChunks {
cp := *chunk
ns.hashedChunks[i] = &cp
}
// Copy free lists
if len(s.freeInternals) > 0 {
ns.freeInternals = make([]uint32, len(s.freeInternals))
copy(ns.freeInternals, s.freeInternals)

View file

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

View file

@ -25,15 +25,12 @@ import (
"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)
// Hash computes and returns the root hash.
func (s *NodeStore) Hash() common.Hash {
return s.ComputeHash(s.root)
}
// ComputeHash computes the hash of the node referenced by ref.
func (s *NodeStore) ComputeHash(ref NodeRef) common.Hash {
switch ref.Kind() {
case KindInternal:
@ -49,11 +46,7 @@ func (s *NodeStore) ComputeHash(ref NodeRef) common.Hash {
}
}
// hashInternal computes the hash of an InternalNode. 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.
// hashInternal hashes an InternalNode, parallelising at shallow depths.
func (s *NodeStore) hashInternal(idx uint32) common.Hash {
node := s.getInternal(idx)
if !node.mustRecompute {
@ -96,11 +89,7 @@ func (s *NodeStore) hashInternal(idx uint32) common.Hash {
return node.hash
}
// --- Serialization ---
// 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)]
// SerializeNode serializes a node into the flat on-disk format.
func (s *NodeStore) SerializeNode(ref NodeRef) []byte {
switch ref.Kind() {
case KindInternal:
@ -128,18 +117,14 @@ func (s *NodeStore) SerializeNode(ref NodeRef) []byte {
}
}
// --- Deserialization ---
var errInvalidSerializedLength = errors.New("invalid serialized node length")
// 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) {
return s.deserializeNode(serialized, depth, common.Hash{}, true)
}
// 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) {
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 {
return EmptyRef, errInvalidSerializedLength
}
// Zero-copy: valueData aliases the serialized buffer. The shared
// flag triggers copy-on-write via ensureWritable() before mutation.
// Callers must not modify serialized after this call.
// Zero-copy: aliases the serialized buffer; ensureWritable() COWs before mutation.
sn.valueData = serialized[dataStart:dataEnd]
sn.shared = true
sn.depth = uint8(depth)
@ -210,10 +193,7 @@ func (s *NodeStore) deserializeNode(serialized []byte, depth int, hn common.Hash
}
}
// --- CollectNodes (Commit) ---
// CollectNodes traverses the trie, serializing and flushing each node via flushfn.
// Children are flushed before their parents (post-order traversal).
// CollectNodes flushes every node via flushfn in post-order.
func (s *NodeStore) CollectNodes(ref NodeRef, path []byte, flushfn NodeFlushFn) error {
switch ref.Kind() {
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 {
switch ref.Kind() {
case KindInternal:

View file

@ -26,12 +26,10 @@ import (
// NodeResolverFn resolves a hashed node from the database.
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) {
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) {
cur := 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) {
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) {
cur := ref
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 {
if len(value) != HashSize {
return errors.New("invalid insertion: value length")
}
// Handle root-is-empty case
if s.root.IsEmpty() {
ref := s.newStemRef(stem, 0)
sn := s.getStem(ref.Index())
@ -188,7 +182,6 @@ func (s *NodeStore) InsertSingle(stem []byte, suffix byte, value []byte, resolve
return nil
}
// Handle root-is-stem case
if s.root.Kind() == KindStem {
sn := s.getStem(s.root.Index())
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
return nil
}
// Different stem — promote root to internal node via split
newRoot := s.splitStemInsert(s.root, stem, suffix, value, int(sn.depth))
s.root = newRoot
return nil
}
// Root is an InternalNode — iterative descent
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 {
type pathEntry struct {
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
// into a chain of InternalNodes because the new key has a different stem.
// splitStemInsert splits a StemNode into InternalNodes for a divergent stem.
func (s *NodeStore) splitStemInsert(existingRef NodeRef, newStem []byte, suffix byte, value []byte, depth int) NodeRef {
existing := s.getStem(existingRef.Index())
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 {
newRoot, err := s.insertValuesAtStem(s.root, stem, values, resolver, 0)
if err != nil {
@ -370,7 +358,6 @@ func (s *NodeStore) InsertValuesAtStem(stem []byte, values [][]byte, resolver No
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) {
switch ref.Kind() {
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) {
existing := s.getStem(existingRef.Index())
@ -542,17 +529,14 @@ func (s *NodeStore) splitStemValuesInsert(existingRef NodeRef, newStem []byte, v
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 {
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) {
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 {
switch ref.Kind() {
case KindInternal: