trie/bintrie: skip resolver for zero-hash children

This commit is contained in:
rizkyikiw42 2025-11-27 11:10:45 +08:00
parent 6452b7ad05
commit 926ded3385
2 changed files with 203 additions and 42 deletions

View file

@ -51,6 +51,31 @@ func (bt *InternalNode) GetValuesAtStem(stem []byte, resolver NodeResolverFn) ([
bit := stem[bt.depth/8] >> (7 - (bt.depth % 8)) & 1 bit := stem[bt.depth/8] >> (7 - (bt.depth % 8)) & 1
if bit == 0 { if bit == 0 {
if hn, ok := bt.left.(HashedNode); ok { if hn, ok := bt.left.(HashedNode); ok {
if common.Hash(hn) == (common.Hash{}) {
bt.left = Empty{}
} else {
path, err := keyToPath(bt.depth, stem)
if err != nil {
return nil, fmt.Errorf("GetValuesAtStem resolve error: %w", err)
}
data, err := resolver(path, common.Hash(hn))
if err != nil {
return nil, fmt.Errorf("GetValuesAtStem resolve error: %w", err)
}
node, err := DeserializeNode(data, bt.depth+1)
if err != nil {
return nil, fmt.Errorf("GetValuesAtStem node deserialization error: %w", err)
}
bt.left = node
}
}
return bt.left.GetValuesAtStem(stem, resolver)
}
if hn, ok := bt.right.(HashedNode); ok {
if common.Hash(hn) == (common.Hash{}) {
bt.right = Empty{}
} else {
path, err := keyToPath(bt.depth, stem) path, err := keyToPath(bt.depth, stem)
if err != nil { if err != nil {
return nil, fmt.Errorf("GetValuesAtStem resolve error: %w", err) return nil, fmt.Errorf("GetValuesAtStem resolve error: %w", err)
@ -63,25 +88,8 @@ func (bt *InternalNode) GetValuesAtStem(stem []byte, resolver NodeResolverFn) ([
if err != nil { if err != nil {
return nil, fmt.Errorf("GetValuesAtStem node deserialization error: %w", err) return nil, fmt.Errorf("GetValuesAtStem node deserialization error: %w", err)
} }
bt.left = node bt.right = node
} }
return bt.left.GetValuesAtStem(stem, resolver)
}
if hn, ok := bt.right.(HashedNode); ok {
path, err := keyToPath(bt.depth, stem)
if err != nil {
return nil, fmt.Errorf("GetValuesAtStem resolve error: %w", err)
}
data, err := resolver(path, common.Hash(hn))
if err != nil {
return nil, fmt.Errorf("GetValuesAtStem resolve error: %w", err)
}
node, err := DeserializeNode(data, bt.depth+1)
if err != nil {
return nil, fmt.Errorf("GetValuesAtStem node deserialization error: %w", err)
}
bt.right = node
} }
return bt.right.GetValuesAtStem(stem, resolver) return bt.right.GetValuesAtStem(stem, resolver)
} }
@ -141,6 +149,37 @@ func (bt *InternalNode) InsertValuesAtStem(stem []byte, values [][]byte, resolve
} }
if hn, ok := bt.left.(HashedNode); ok { if hn, ok := bt.left.(HashedNode); ok {
if common.Hash(hn) == (common.Hash{}) {
bt.left = Empty{}
} else {
path, err := keyToPath(bt.depth, stem)
if err != nil {
return nil, fmt.Errorf("InsertValuesAtStem resolve error: %w", err)
}
data, err := resolver(path, common.Hash(hn))
if err != nil {
return nil, fmt.Errorf("InsertValuesAtStem resolve error: %w", err)
}
node, err := DeserializeNode(data, bt.depth+1)
if err != nil {
return nil, fmt.Errorf("InsertValuesAtStem node deserialization error: %w", err)
}
bt.left = node
}
}
bt.left, err = bt.left.InsertValuesAtStem(stem, values, resolver, depth+1)
return bt, err
}
if bt.right == nil {
bt.right = Empty{}
}
if hn, ok := bt.right.(HashedNode); ok {
if common.Hash(hn) == (common.Hash{}) {
bt.right = Empty{}
} else {
path, err := keyToPath(bt.depth, stem) path, err := keyToPath(bt.depth, stem)
if err != nil { if err != nil {
return nil, fmt.Errorf("InsertValuesAtStem resolve error: %w", err) return nil, fmt.Errorf("InsertValuesAtStem resolve error: %w", err)
@ -153,31 +192,8 @@ func (bt *InternalNode) InsertValuesAtStem(stem []byte, values [][]byte, resolve
if err != nil { if err != nil {
return nil, fmt.Errorf("InsertValuesAtStem node deserialization error: %w", err) return nil, fmt.Errorf("InsertValuesAtStem node deserialization error: %w", err)
} }
bt.left = node bt.right = node
} }
bt.left, err = bt.left.InsertValuesAtStem(stem, values, resolver, depth+1)
return bt, err
}
if bt.right == nil {
bt.right = Empty{}
}
if hn, ok := bt.right.(HashedNode); ok {
path, err := keyToPath(bt.depth, stem)
if err != nil {
return nil, fmt.Errorf("InsertValuesAtStem resolve error: %w", err)
}
data, err := resolver(path, common.Hash(hn))
if err != nil {
return nil, fmt.Errorf("InsertValuesAtStem resolve error: %w", err)
}
node, err := DeserializeNode(data, bt.depth+1)
if err != nil {
return nil, fmt.Errorf("InsertValuesAtStem node deserialization error: %w", err)
}
bt.right = node
} }
bt.right, err = bt.right.InsertValuesAtStem(stem, values, resolver, depth+1) bt.right, err = bt.right.InsertValuesAtStem(stem, values, resolver, depth+1)

View file

@ -0,0 +1,145 @@
// Copyright 2025 go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package bintrie
import (
"errors"
"testing"
"github.com/ethereum/go-ethereum/common"
)
// TestZeroHashSkipsResolver tests that zero-hash HashedNodes don't trigger resolver calls
func TestZeroHashSkipsResolver(t *testing.T) {
// Create an InternalNode with one real child and one Empty child
realHash := common.HexToHash("0x1234")
node := &InternalNode{
depth: 0,
left: HashedNode(realHash),
right: Empty{},
}
// Serialize and deserialize to create zero-hash HashedNode
serialized := SerializeNode(node)
deserialized, err := DeserializeNode(serialized, 0)
if err != nil {
t.Fatalf("Failed to deserialize: %v", err)
}
deserializedInternal := deserialized.(*InternalNode)
// Verify that right child is a zero-hash HashedNode after deserialization
if hn, ok := deserializedInternal.right.(HashedNode); ok {
if common.Hash(hn) != (common.Hash{}) {
t.Fatal("Expected right child to be zero-hash HashedNode")
}
} else {
t.Fatalf("Expected right child to be HashedNode, got %T", deserializedInternal.right)
}
// Track resolver calls
resolverCalls := 0
resolver := func(path []byte, hash common.Hash) ([]byte, error) {
resolverCalls++
// Zero-hash should never reach resolver
if hash == (common.Hash{}) {
t.Error("BUG: Resolver called for zero hash")
return nil, errors.New("zero hash should not be resolved")
}
// Return valid data for real hash
if hash == realHash {
stem := make([]byte, 31)
var values [256][]byte
values[5] = common.HexToHash("0xabcd").Bytes()
return SerializeNode(&StemNode{Stem: stem, Values: values[:], depth: 1}), nil
}
return nil, errors.New("not found")
}
// Access right child (zero-hash) - should not call resolver
rightStem := make([]byte, 31)
rightStem[0] = 0x80 // First bit is 1, routes to right child
values, err := deserializedInternal.GetValuesAtStem(rightStem, resolver)
if err != nil {
t.Fatalf("GetValuesAtStem failed: %v", err)
}
// All values should be nil for empty node
for i, v := range values {
if v != nil {
t.Errorf("Expected nil value at index %d, got %x", i, v)
}
}
// Verify resolver was not called for zero-hash
if resolverCalls > 0 {
t.Errorf("Resolver should not have been called for zero-hash child, but was called %d times", resolverCalls)
}
// Now test left child (real hash) - should call resolver
leftStem := make([]byte, 31)
_, err = deserializedInternal.GetValuesAtStem(leftStem, resolver)
if err != nil {
t.Fatalf("GetValuesAtStem failed for left child: %v", err)
}
if resolverCalls != 1 {
t.Errorf("Expected resolver to be called once for real hash, called %d times", resolverCalls)
}
}
// TestZeroHashSkipsResolverOnInsert tests that InsertValuesAtStem also skips zero-hash resolver calls
func TestZeroHashSkipsResolverOnInsert(t *testing.T) {
// Create node after deserialization with zero-hash children
node := &InternalNode{
depth: 0,
left: HashedNode(common.Hash{}), // Zero-hash
right: HashedNode(common.Hash{}), // Zero-hash
}
resolverCalls := 0
resolver := func(path []byte, hash common.Hash) ([]byte, error) {
resolverCalls++
if hash == (common.Hash{}) {
t.Error("BUG: Resolver called for zero hash in InsertValuesAtStem")
return nil, errors.New("zero hash should not be resolved")
}
return nil, errors.New("not found")
}
// Insert values into left subtree (zero-hash child)
leftStem := make([]byte, 31)
var values [256][]byte
values[10] = common.HexToHash("0x5678").Bytes()
_, err := node.InsertValuesAtStem(leftStem, values[:], resolver, 0)
if err != nil {
t.Fatalf("InsertValuesAtStem failed: %v", err)
}
// Verify resolver was not called
if resolverCalls > 0 {
t.Errorf("Resolver should not have been called for zero-hash child, but was called %d times", resolverCalls)
}
}