trie: rewrite the subtreeIterator

This commit is contained in:
Gary Rong 2025-09-15 10:19:40 +08:00
parent 2db2a18e83
commit 45ee2fbe19
3 changed files with 231 additions and 139 deletions

View file

@ -146,17 +146,6 @@ type nodeIterator struct {
resolver NodeResolver // optional node resolver for avoiding disk hits resolver NodeResolver // optional node resolver for avoiding disk hits
pool []*nodeIteratorState // local pool for iterator states pool []*nodeIteratorState // local pool for iterator states
// Fields for subtree iteration (original byte keys)
startKey []byte // Start key for subtree iteration (nil for full trie)
stopKey []byte // Stop key for subtree iteration (nil for full trie)
// Precomputed nibble paths for efficient comparison
startPath []byte // Precomputed hex path for startKey (without terminator)
stopPath []byte // Precomputed hex path for stopKey (without terminator)
// Iteration mode
prefixMode bool // True if this is prefix iteration (use HasPrefix check)
} }
// errIteratorEnd is stored in nodeIterator.err when iteration is done. // errIteratorEnd is stored in nodeIterator.err when iteration is done.
@ -307,39 +296,6 @@ func (it *nodeIterator) Next(descend bool) bool {
if it.err != nil { if it.err != nil {
return false return false
} }
// Check if we're still within the subtree boundaries using precomputed paths
if it.startPath != nil && len(path) > 0 {
if it.prefixMode {
// For prefix iteration, use HasPrefix to ensure we stay within the prefix
if !bytes.HasPrefix(path, it.startPath) {
it.err = errIteratorEnd
return false
}
} else {
// For range iteration, ensure we don't return nodes before the lower bound.
// Advance the iterator until we reach a node at or after startPath.
for bytes.Compare(path, it.startPath) < 0 {
// Progress the iterator by pushing the current candidate, then peeking again.
it.push(state, parentIndex, path)
state, parentIndex, path, err = it.peek(descend)
it.err = err
if it.err != nil {
return false
}
if len(path) == 0 {
break
}
}
}
}
if it.stopPath != nil && len(path) > 0 {
if bytes.Compare(path, it.stopPath) >= 0 {
it.err = errIteratorEnd
return false
}
}
it.push(state, parentIndex, path) it.push(state, parentIndex, path)
return true return true
} }
@ -881,7 +837,16 @@ func (it *unionIterator) Error() error {
return nil return nil
} }
// NewSubtreeIterator creates an iterator that only traverses nodes within a subtree // subTreeIterator wraps nodeIterator to traverse a trie within a predefined
// start and limit range, with optional prefix mode.
type subtreeIterator struct {
NodeIterator
stopPath []byte // Precomputed hex path for stopKey (without terminator), nil means no limit
exhausted bool // Flag whether the iterator has been exhausted
}
// newSubtreeIterator creates an iterator that only traverses nodes within a subtree
// defined by the given startKey and stopKey. This supports general range iteration // defined by the given startKey and stopKey. This supports general range iteration
// where startKey is inclusive and stopKey is exclusive. // where startKey is inclusive and stopKey is exclusive.
// //
@ -890,17 +855,27 @@ func (it *unionIterator) Error() error {
// implemented via hex-nibble path comparisons for efficiency). // implemented via hex-nibble path comparisons for efficiency).
// //
// If startKey is nil, iteration starts from the beginning. If stopKey is nil, // If startKey is nil, iteration starts from the beginning. If stopKey is nil,
// iteration continues to the end of the trie. For prefix iteration, use the // iteration continues to the end of the trie.
// Trie.NodeIteratorWithPrefix method which handles prefix semantics correctly. func newSubtreeIterator(trie *Trie, startKey, stopKey []byte) (NodeIterator, error) {
func NewSubtreeIterator(trie *Trie, startKey, stopKey []byte) NodeIterator { it, err := trie.NodeIterator(startKey)
return newSubtreeIterator(trie, startKey, stopKey, false) if err != nil {
} return nil, err
}
// newPrefixIterator creates an iterator that only traverses nodes with the given prefix. if startKey == nil && stopKey == nil {
// This ensures that only keys starting with the prefix are visited. return it, nil
func newPrefixIterator(trie *Trie, prefix []byte) NodeIterator { }
stopKey := nextKey(prefix) // Precompute nibble paths for efficient comparison
return newSubtreeIterator(trie, prefix, stopKey, true) var stopPath []byte
if stopKey != nil {
stopPath = keybytesToHex(stopKey)
if hasTerm(stopPath) {
stopPath = stopPath[:len(stopPath)-1]
}
}
return &subtreeIterator{
NodeIterator: it,
stopPath: stopPath,
}, nil
} }
// nextKey returns the next possible key after the given prefix. // nextKey returns the next possible key after the given prefix.
@ -927,51 +902,25 @@ func nextKey(prefix []byte) []byte {
return nil return nil
} }
func newSubtreeIterator(trie *Trie, startKey, stopKey []byte, prefixMode bool) NodeIterator { // newPrefixIterator creates an iterator that only traverses nodes with the given prefix.
// Precompute nibble paths for efficient comparison // This ensures that only keys starting with the prefix are visited.
var startPath, stopPath []byte func newPrefixIterator(trie *Trie, prefix []byte) (NodeIterator, error) {
if startKey != nil { return newSubtreeIterator(trie, prefix, nextKey(prefix))
startPath = keybytesToHex(startKey) }
if hasTerm(startPath) {
startPath = startPath[:len(startPath)-1] // Next moves the iterator to the next node. If the parameter is false, any child
} // nodes will be skipped.
} func (it *subtreeIterator) Next(descend bool) bool {
if stopKey != nil { if it.exhausted {
stopPath = keybytesToHex(stopKey) return false
if hasTerm(stopPath) { }
stopPath = stopPath[:len(stopPath)-1] if !it.NodeIterator.Next(descend) {
} it.exhausted = true
} return false
}
if trie.Hash() == types.EmptyRootHash { if it.stopPath != nil && reachedPath(it.NodeIterator.Path(), it.stopPath) {
return &nodeIterator{ it.exhausted = true
trie: trie, return false
err: errIteratorEnd, }
startKey: startKey, return true
stopKey: stopKey,
startPath: startPath,
stopPath: stopPath,
prefixMode: prefixMode,
}
}
it := &nodeIterator{
trie: trie,
startKey: startKey,
stopKey: stopKey,
startPath: startPath,
stopPath: stopPath,
prefixMode: prefixMode,
}
// Seek to the starting position if startKey is provided
if startKey != nil && len(startKey) > 0 {
it.err = it.seek(startKey)
} else {
state, err := it.init()
if err != nil {
it.err = err
} else {
it.push(state, nil, nil)
}
}
return it
} }

View file

@ -19,7 +19,9 @@ package trie
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"maps"
"math/rand" "math/rand"
"slices"
"testing" "testing"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
@ -625,16 +627,22 @@ func isTrieNode(scheme string, key, val []byte) (bool, []byte, common.Hash) {
} }
func TestSubtreeIterator(t *testing.T) { func TestSubtreeIterator(t *testing.T) {
diskDb := rawdb.NewMemoryDatabase() var (
db := newTestDatabase(diskDb, rawdb.HashScheme) db = newTestDatabase(rawdb.NewMemoryDatabase(), rawdb.HashScheme)
tr := NewEmpty(db) tr = NewEmpty(db)
)
vals := []struct{ k, v string }{ vals := []struct{ k, v string }{
{"do", "verb"}, {"do", "verb"},
{"dog", "puppy"}, {"dog", "puppy"},
{"doge", "coin"}, {"doge", "coin"},
{"dog\xff", "value6"},
{"dog\xff\xff", "value7"},
{"horse", "stallion"}, {"horse", "stallion"},
{"house", "building"}, {"house", "building"},
{"houses", "multiple"}, {"houses", "multiple"},
{"xyz", "value"},
{"xyz\xff", "value"},
{"xyz\xff\xff", "value"},
} }
all := make(map[string]string) all := make(map[string]string)
for _, val := range vals { for _, val := range vals {
@ -644,12 +652,103 @@ func TestSubtreeIterator(t *testing.T) {
root, nodes := tr.Commit(false) root, nodes := tr.Commit(false)
db.Update(root, types.EmptyRootHash, trienode.NewWithNodeSet(nodes)) db.Update(root, types.EmptyRootHash, trienode.NewWithNodeSet(nodes))
// Test prefix iteration using NewPrefixIterator allNodes := make(map[string][]byte)
prefix := []byte("do") tr, _ = New(TrieID(root), db)
it, err := tr.NodeIterator(nil)
if err != nil {
t.Fatal(err)
}
for it.Next(true) {
allNodes[string(it.Path())] = it.NodeBlob()
}
allKeys := slices.Collect(maps.Keys(all))
suites := []struct {
start []byte
end []byte
expected []string
}{
// entire key range
{
start: nil,
end: nil,
expected: allKeys,
},
{
start: nil,
end: bytes.Repeat([]byte{0xff}, 32),
expected: allKeys,
},
{
start: bytes.Repeat([]byte{0x0}, 32),
end: bytes.Repeat([]byte{0xff}, 32),
expected: allKeys,
},
// key range with start
{
start: []byte("do"),
end: nil,
expected: allKeys,
},
{
start: []byte("doe"),
end: nil,
expected: allKeys[1:],
},
{
start: []byte("dog"),
end: nil,
expected: allKeys[1:],
},
{
start: []byte("doge"),
end: nil,
expected: allKeys[2:],
},
{
start: []byte("dog\xff"),
end: nil,
expected: allKeys[3:],
},
{
start: []byte("dog\xff\xff"),
end: nil,
expected: allKeys[4:],
},
{
start: []byte("dog\xff\xff\xff"),
end: nil,
expected: allKeys[5:],
},
// key range with limit
{
start: nil,
end: []byte("xyz"),
expected: allKeys[:len(allKeys)-3],
},
{
start: nil,
end: []byte("xyz\xff"),
expected: allKeys[:len(allKeys)-2],
},
{
start: nil,
end: []byte("xyz\xff\xff"),
expected: allKeys[:len(allKeys)-1],
},
{
start: nil,
end: []byte("xyz\xff\xff\xff"),
expected: allKeys,
},
}
for _, suite := range suites {
// We need to re-open the trie from the committed state // We need to re-open the trie from the committed state
tr, _ = New(TrieID(root), db) tr, _ = New(TrieID(root), db)
it := newPrefixIterator(tr, prefix) it, err := newSubtreeIterator(tr, suite.start, suite.end)
if err != nil {
t.Fatal(err)
}
found := make(map[string]string) found := make(map[string]string)
for it.Next(true) { for it.Next(true) {
@ -657,20 +756,52 @@ func TestSubtreeIterator(t *testing.T) {
found[string(it.LeafKey())] = string(it.LeafBlob()) found[string(it.LeafKey())] = string(it.LeafBlob())
} }
} }
if len(found) != len(suite.expected) {
// Should find "do", "dog", "doge" but not "horse", "house", "houses" t.Errorf("wrong number of values: got %d, want %d", len(found), len(suite.expected))
expected := map[string]string{ }
"do": "verb", for k, v := range found {
"dog": "puppy", if all[k] != v {
"doge": "coin", t.Errorf("wrong value for %s: got %s, want %s", k, found[k], all[k])
}
} }
if len(found) != len(expected) { expectedNodes := make(map[string][]byte)
t.Errorf("wrong number of values: got %d, want %d", len(found), len(expected)) for path, blob := range allNodes {
if suite.start != nil {
hexStart := keybytesToHex(suite.start)
hexStart = hexStart[:len(hexStart)-1]
if !reachedPath([]byte(path), hexStart) {
continue
} }
for k, v := range expected { }
if found[k] != v { if suite.end != nil {
t.Errorf("wrong value for %s: got %s, want %s", k, found[k], v) hexEnd := keybytesToHex(suite.end)
hexEnd = hexEnd[:len(hexEnd)-1]
if reachedPath([]byte(path), hexEnd) {
continue
}
}
expectedNodes[path] = bytes.Clone(blob)
}
// Compare the result yield from the subtree iterator
var (
subCount int
subIt, _ = newSubtreeIterator(tr, suite.start, suite.end)
)
for subIt.Next(true) {
blob, ok := expectedNodes[string(subIt.Path())]
if !ok {
t.Errorf("Unexpected node iterated, path: %v", subIt.Path())
}
subCount++
if !bytes.Equal(blob, subIt.NodeBlob()) {
t.Errorf("Unexpected node blob, path: %v, want: %v, got: %v", subIt.Path(), blob, subIt.NodeBlob())
}
}
if subCount != len(expectedNodes) {
t.Errorf("Unexpected node being iterated, want: %d, got: %d", len(expectedNodes), subCount)
} }
} }
} }
@ -875,7 +1006,7 @@ func TestPrefixIteratorEdgeCases(t *testing.T) {
} }
} }
// Should find at least the allFF key itself // Should find at least the allFF key itself
if count < 1 { if count != 2 {
t.Errorf("Expected at least 1 result for all-0xff prefix, got %d", count) t.Errorf("Expected at least 1 result for all-0xff prefix, got %d", count)
} }
}) })
@ -919,7 +1050,7 @@ func TestGeneralRangeIteration(t *testing.T) {
// Test range iteration from "banana" to "fig" (exclusive) // Test range iteration from "banana" to "fig" (exclusive)
t.Run("RangeIteration", func(t *testing.T) { t.Run("RangeIteration", func(t *testing.T) {
iter := NewSubtreeIterator(trie, []byte("banana"), []byte("fig")) iter, _ := newSubtreeIterator(trie, []byte("banana"), []byte("fig"))
found := make(map[string]bool) found := make(map[string]bool)
for iter.Next(true) { for iter.Next(true) {
if iter.Leaf() { if iter.Leaf() {
@ -937,7 +1068,7 @@ func TestGeneralRangeIteration(t *testing.T) {
// Test with nil stopKey (iterate to end) // Test with nil stopKey (iterate to end)
t.Run("NilStopKey", func(t *testing.T) { t.Run("NilStopKey", func(t *testing.T) {
iter := NewSubtreeIterator(trie, []byte("date"), nil) iter, _ := newSubtreeIterator(trie, []byte("date"), nil)
found := make(map[string]bool) found := make(map[string]bool)
for iter.Next(true) { for iter.Next(true) {
if iter.Leaf() { if iter.Leaf() {
@ -955,7 +1086,7 @@ func TestGeneralRangeIteration(t *testing.T) {
// Test with nil startKey (iterate from beginning) // Test with nil startKey (iterate from beginning)
t.Run("NilStartKey", func(t *testing.T) { t.Run("NilStartKey", func(t *testing.T) {
iter := NewSubtreeIterator(trie, nil, []byte("cherry")) iter, _ := newSubtreeIterator(trie, nil, []byte("cherry"))
found := make(map[string]bool) found := make(map[string]bool)
for iter.Next(true) { for iter.Next(true) {
if iter.Leaf() { if iter.Leaf() {

View file

@ -134,20 +134,32 @@ func (t *Trie) NodeIterator(start []byte) (NodeIterator, error) {
return newNodeIterator(t, start), nil return newNodeIterator(t, start), nil
} }
// NodeIteratorWithPrefix returns an iterator that returns nodes of the trie whose leaf keys // NodeIteratorWithPrefix returns an iterator that returns nodes of the trie
// start with the given prefix. Iteration includes all keys k where prefix <= k < nextKey(prefix), // whose leaf keys start with the given prefix. Iteration includes all keys
// effectively returning only keys that have the prefix. The iteration stops once it would // where prefix <= k < nextKey(prefix), effectively returning only keys that
// encounter a key that doesn't start with the prefix. // have the prefix. The iteration stops once it would encounter a key that
// doesn't start with the prefix.
// //
// For example, with prefix "dog", the iterator will return "dog", "dogcat", "dogfish" but // For example, with prefix "dog", the iterator will return "dog", "dogcat",
// not "dot" or "fog". An empty prefix iterates the entire trie. // "dogfish" but not "dot" or "fog". An empty prefix iterates the entire trie.
func (t *Trie) NodeIteratorWithPrefix(prefix []byte) (NodeIterator, error) { func (t *Trie) NodeIteratorWithPrefix(prefix []byte) (NodeIterator, error) {
// Short circuit if the trie is already committed and not usable. // Short circuit if the trie is already committed and not usable.
if t.committed { if t.committed {
return nil, ErrCommitted return nil, ErrCommitted
} }
// Use the dedicated prefix iterator which handles prefix checking correctly // Use the dedicated prefix iterator which handles prefix checking correctly
return newPrefixIterator(t, prefix), nil return newPrefixIterator(t, prefix)
}
// NodeIteratorWithRange returns an iterator over trie nodes whose leaf keys
// fall within the specified range. It includes all keys where start <= k < end.
// Iteration stops once a key beyond the end boundary is encountered.
func (t *Trie) NodeIteratorWithRange(start, end []byte) (NodeIterator, error) {
// Short circuit if the trie is already committed and not usable.
if t.committed {
return nil, ErrCommitted
}
return newSubtreeIterator(t, start, end)
} }
// MustGet is a wrapper of Get and will omit any encountered error but just // MustGet is a wrapper of Get and will omit any encountered error but just