From 26230d037fbc5fd5340c59242002134e61828c9e Mon Sep 17 00:00:00 2001 From: Guillaume Ballet <3272758+gballet@users.noreply.github.com> Date: Wed, 22 Apr 2026 17:02:18 +0200 Subject: [PATCH] fixes Co-authored-by: Copilot --- trie/bintrie/store_commit.go | 117 ++++++++++++++++++++++++++++++----- trie/bintrie/trie.go | 4 +- triedb/database.go | 4 +- 3 files changed, 106 insertions(+), 19 deletions(-) diff --git a/trie/bintrie/store_commit.go b/trie/bintrie/store_commit.go index 7101087b51..0cf52b1d64 100644 --- a/trie/bintrie/store_commit.go +++ b/trie/bintrie/store_commit.go @@ -230,7 +230,11 @@ func (s *nodeStore) decodeNode(serialized []byte, depth int, hn common.Hash, mus // CollectNodes flushes every node that needs flushing via flushfn in post-order. // Invariant: any ancestor of a node that needs flushing is itself marked, so a // clean root means the whole subtree is clean. -func (s *nodeStore) collectNodes(ref nodeRef, path []byte, flushfn nodeFlushFn) error { +func (s *nodeStore) collectNodes(ref nodeRef, path []byte, flushfn nodeFlushFn, groupDepth int) error { + if groupDepth < 1 || groupDepth > MaxGroupDepth { + return errors.New("groupDepth must be between 1 and 8") + } + switch ref.Kind() { case kindEmpty: return nil @@ -239,21 +243,19 @@ func (s *nodeStore) collectNodes(ref nodeRef, path []byte, flushfn nodeFlushFn) if !node.dirty { return nil } - // Reuse path buffer across children: flushfn consumers - // (NodeSet.AddNode, tracer.Get) clone via string(path), so in-place - // mutation is safe. - path = append(path, 0) - if err := s.collectNodes(node.left, path, flushfn); err != nil { - return err + // Only flush at group boundaries (depth % groupDepth == 0) + if int(node.depth)%groupDepth == 0 { + // We're at a group boundary - first collect any nodes in deeper groups, + // then flush this group + if err := s.collectChildGroups(node, path, flushfn, groupDepth, groupDepth-1); err != nil { + return err + } + flushfn(path, s.computeHash(ref), s.serializeNode(ref)) + return nil } - path[len(path)-1] = 1 - if err := s.collectNodes(node.right, path, flushfn); err != nil { - return err - } - path = path[:len(path)-1] - flushfn(path, s.computeHash(ref), s.serializeNode(ref)) - node.dirty = false - return nil + // Not at a group boundary - this shouldn't happen if we're called correctly from root + // but handle it by continuing to traverse + return s.collectChildGroups(path, flushfn, groupDepth, groupDepth-(int(node.depth)%groupDepth)-1) case kindStem: sn := s.getStem(ref.Index()) if !sn.dirty { @@ -269,6 +271,91 @@ func (s *nodeStore) collectNodes(ref nodeRef, path []byte, flushfn nodeFlushFn) } } +// collectChildGroups traverses within a group to find and collect nodes in the next group. +// remainingLevels is how many more levels below the current node until we reach the group boundary. +// When remainingLevels=0, the current node's children are at the next group boundary. +func (s *nodeStore) collectChildGroups(node *InternalNode, path []byte, flushfn nodeFlushFn, groupDepth int, remainingLevels int) error { + if remainingLevels == 0 { + // Current node is at depth (groupBoundary - 1), its children are at the next group boundary + if !node.left.IsEmpty() { + if err := s.collectNodes(node.left, appendBit(path, 0), flushfn, groupDepth); err != nil { + return err + } + } + if !node.right.IsEmpty() { + if err := s.collectNodes(node.right, appendBit(path, 1), flushfn, groupDepth); err != nil { + return err + } + } + return nil + } + + // Continue traversing within the group. + childDepth := node.depth + 1 + + if !node.left.IsEmpty() { + switch node.left.Kind() { + case kindInternal: + n := s.getInternal(node.left.Index()) + if err := s.collectChildGroups(n, appendBit(path, 0), flushfn, groupDepth, remainingLevels-1); err != nil { + return err + } + default: + extPath := s.extendPathToGroupLeaf(appendBit(path, 0), node.left, remainingLevels, int(childDepth)) + if err := s.collectNodes(node.left, extPath, flushfn, groupDepth); err != nil { + return err + } + } + } + if !node.right.IsEmpty() { + switch node.right.Kind() { + case kindInternal: + n := s.getInternal(node.right.Index()) + if err := s.collectChildGroups(n, appendBit(path, 1), flushfn, groupDepth, remainingLevels-1); err != nil { + return err + } + default: + extPath := s.extendPathToGroupLeaf(appendBit(path, 1), node.right, remainingLevels, int(childDepth)) + if err := s.collectNodes(node.right, extPath, flushfn, groupDepth); err != nil { + return err + } + } + } + return nil +} + +// extendPathToGroupLeaf extends a storage path to the group's leaf boundary, +// matching the projection done by serializeSubtree. For StemNodes, the path +// is extended using the stem's key bits (same as serializeSubtree). For other +// node types, the path is extended with all-zero (left) bits. +func (s *nodeStore) extendPathToGroupLeaf(path []byte, node nodeRef, remainingLevels int, absoluteDepth int) []byte { + if remainingLevels <= 0 { + return path + } + if node.Kind() == kindStem { + sn := s.getStem(node.Index()) + for d := 0; d < remainingLevels; d++ { + bit := sn.Stem[(absoluteDepth+d)/8] >> (7 - ((absoluteDepth + d) % 8)) & 1 + path = appendBit(path, bit) + } + } else { + // HashedNode or other: all-left extension (matches serializeSubtree's + // position << remainingDepth behavior). + for d := 0; d < remainingLevels; d++ { + path = appendBit(path, 0) + } + } + return path +} + +// appendBit appends a bit to a path, returning a new slice +func appendBit(path []byte, bit byte) []byte { + var p [256]byte + copy(p[:], path) + result := p[:len(path)] + return append(result, bit) +} + func (s *nodeStore) toDot(ref nodeRef, parent, path string) string { switch ref.Kind() { case kindInternal: diff --git a/trie/bintrie/trie.go b/trie/bintrie/trie.go index 7a120338eb..5e19dd1e11 100644 --- a/trie/bintrie/trie.go +++ b/trie/bintrie/trie.go @@ -107,7 +107,7 @@ func ChunkifyCode(code []byte) ChunkedCode { // BinaryTrie is the implementation of https://eips.ethereum.org/EIPS/eip-7864. type BinaryTrie struct { - root BinaryNode + store *nodeStore reader *trie.Reader tracer *trie.PrevalueTracer groupDepth int // Number of levels per serialized group (1-8, default 8) @@ -130,7 +130,7 @@ func NewBinaryTrie(root common.Hash, db database.NodeDatabase, groupDepth int) ( return nil, err } t := &BinaryTrie{ - root: NewBinaryNode(), + store: newNodeStore(), reader: reader, tracer: trie.NewPrevalueTracer(), groupDepth: groupDepth, diff --git a/triedb/database.go b/triedb/database.go index dac4ba28b5..ea41a48736 100644 --- a/triedb/database.go +++ b/triedb/database.go @@ -32,7 +32,7 @@ import ( // Config defines all necessary options for database. type Config struct { Preimages bool // Flag whether the preimage of node key is recorded - IsVerkle bool // Flag whether the db is holding a verkle tree + IsUBT bool // Flag whether the db is holding a unified binary tree BinTrieGroupDepth int // Number of levels per serialized group in binary trie (1-8, default 8) HashDB *hashdb.Config // Configs for hash-based scheme PathDB *pathdb.Config // Configs for experimental path-based scheme @@ -46,7 +46,7 @@ var HashDefaults = &Config{ HashDB: hashdb.Defaults, } -// UBTDefaults represents a config for holding verkle trie data +// UBTDefaults represents a config for holding unified binary trie data // using path-based scheme with default settings. var UBTDefaults = &Config{ Preimages: false,