Skip to content

Commit 931befe

Browse files
authored
core/stateless: only report leaf depth in witness stats (#32507)
Filtering for leaf nodes was missing from #32388, which means that even the root done was reported, which made little sense for the bloatnet data processing we want to do.
1 parent 0e69530 commit 931befe

File tree

2 files changed

+223
-7
lines changed

2 files changed

+223
-7
lines changed

core/stateless/stats.go

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ package stateless
1818

1919
import (
2020
"maps"
21+
"slices"
22+
"sort"
23+
"strings"
2124

2225
"github.com/ethereum/go-ethereum/common"
2326
"github.com/ethereum/go-ethereum/metrics"
@@ -90,13 +93,19 @@ func NewWitnessStats() *WitnessStats {
9093
// If `owner` is the zero hash, accesses are attributed to the account trie;
9194
// otherwise, they are attributed to the storage trie of that account.
9295
func (s *WitnessStats) Add(nodes map[string][]byte, owner common.Hash) {
93-
if owner == (common.Hash{}) {
94-
for path := range maps.Keys(nodes) {
95-
s.accountTrie.add(int64(len(path)))
96-
}
97-
} else {
98-
for path := range maps.Keys(nodes) {
99-
s.storageTrie.add(int64(len(path)))
96+
// Extract paths from the nodes map
97+
paths := slices.Collect(maps.Keys(nodes))
98+
sort.Strings(paths)
99+
100+
for i, path := range paths {
101+
// If current path is a prefix of the next path, it's not a leaf.
102+
// The last path is always a leaf.
103+
if i == len(paths)-1 || !strings.HasPrefix(paths[i+1], paths[i]) {
104+
if owner == (common.Hash{}) {
105+
s.accountTrie.add(int64(len(path)))
106+
} else {
107+
s.storageTrie.add(int64(len(path)))
108+
}
100109
}
101110
}
102111
}

core/stateless/stats_test.go

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
// Copyright 2025 The go-ethereum Authors
2+
// This file is part of the go-ethereum library.
3+
//
4+
// The go-ethereum library is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Lesser General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// The go-ethereum library is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Lesser General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Lesser General Public License
15+
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
16+
17+
package stateless
18+
19+
import (
20+
"testing"
21+
22+
"github.com/ethereum/go-ethereum/common"
23+
)
24+
25+
func TestWitnessStatsAdd(t *testing.T) {
26+
tests := []struct {
27+
name string
28+
nodes map[string][]byte
29+
owner common.Hash
30+
expectedAccountDepth int64
31+
expectedStorageDepth int64
32+
}{
33+
{
34+
name: "empty nodes",
35+
nodes: map[string][]byte{},
36+
owner: common.Hash{},
37+
expectedAccountDepth: 0,
38+
expectedStorageDepth: 0,
39+
},
40+
{
41+
name: "single account trie leaf",
42+
nodes: map[string][]byte{
43+
"abc": []byte("data"),
44+
},
45+
owner: common.Hash{},
46+
expectedAccountDepth: 3,
47+
expectedStorageDepth: 0,
48+
},
49+
{
50+
name: "account trie with internal nodes",
51+
nodes: map[string][]byte{
52+
"a": []byte("data1"),
53+
"ab": []byte("data2"),
54+
"abc": []byte("data3"),
55+
},
56+
owner: common.Hash{},
57+
expectedAccountDepth: 3, // Only "abc" is a leaf
58+
expectedStorageDepth: 0,
59+
},
60+
{
61+
name: "multiple account trie branches",
62+
nodes: map[string][]byte{
63+
"a": []byte("data1"),
64+
"ab": []byte("data2"),
65+
"abc": []byte("data3"),
66+
"b": []byte("data4"),
67+
"bc": []byte("data5"),
68+
"bcd": []byte("data6"),
69+
},
70+
owner: common.Hash{},
71+
expectedAccountDepth: 6, // "abc" (3) + "bcd" (3) = 6
72+
expectedStorageDepth: 0,
73+
},
74+
{
75+
name: "siblings are all leaves",
76+
nodes: map[string][]byte{
77+
"aa": []byte("data1"),
78+
"ab": []byte("data2"),
79+
"ac": []byte("data3"),
80+
},
81+
owner: common.Hash{},
82+
expectedAccountDepth: 6, // 2 + 2 + 2 = 6
83+
expectedStorageDepth: 0,
84+
},
85+
{
86+
name: "storage trie leaves",
87+
nodes: map[string][]byte{
88+
"1": []byte("data1"),
89+
"12": []byte("data2"),
90+
"123": []byte("data3"),
91+
"124": []byte("data4"),
92+
},
93+
owner: common.HexToHash("0x1234"),
94+
expectedAccountDepth: 0,
95+
expectedStorageDepth: 6, // "123" (3) + "124" (3) = 6
96+
},
97+
{
98+
name: "complex trie structure",
99+
nodes: map[string][]byte{
100+
"1": []byte("data1"),
101+
"12": []byte("data2"),
102+
"123": []byte("data3"),
103+
"124": []byte("data4"),
104+
"2": []byte("data5"),
105+
"23": []byte("data6"),
106+
"234": []byte("data7"),
107+
"235": []byte("data8"),
108+
"3": []byte("data9"),
109+
},
110+
owner: common.Hash{},
111+
expectedAccountDepth: 13, // "123"(3) + "124"(3) + "234"(3) + "235"(3) + "3"(1) = 13
112+
expectedStorageDepth: 0,
113+
},
114+
}
115+
116+
for _, tt := range tests {
117+
t.Run(tt.name, func(t *testing.T) {
118+
stats := NewWitnessStats()
119+
stats.Add(tt.nodes, tt.owner)
120+
121+
// Check account trie depth
122+
if stats.accountTrie.totalDepth != tt.expectedAccountDepth {
123+
t.Errorf("Account trie total depth = %d, want %d", stats.accountTrie.totalDepth, tt.expectedAccountDepth)
124+
}
125+
126+
// Check storage trie depth
127+
if stats.storageTrie.totalDepth != tt.expectedStorageDepth {
128+
t.Errorf("Storage trie total depth = %d, want %d", stats.storageTrie.totalDepth, tt.expectedStorageDepth)
129+
}
130+
})
131+
}
132+
}
133+
134+
func TestWitnessStatsMinMax(t *testing.T) {
135+
stats := NewWitnessStats()
136+
137+
// Add some account trie nodes with varying depths
138+
stats.Add(map[string][]byte{
139+
"a": []byte("data1"),
140+
"ab": []byte("data2"),
141+
"abc": []byte("data3"),
142+
"abcd": []byte("data4"),
143+
"abcde": []byte("data5"),
144+
}, common.Hash{})
145+
146+
// Only "abcde" is a leaf (depth 5)
147+
if stats.accountTrie.minDepth != 5 {
148+
t.Errorf("Account trie min depth = %d, want %d", stats.accountTrie.minDepth, 5)
149+
}
150+
if stats.accountTrie.maxDepth != 5 {
151+
t.Errorf("Account trie max depth = %d, want %d", stats.accountTrie.maxDepth, 5)
152+
}
153+
154+
// Add more leaves with different depths
155+
stats.Add(map[string][]byte{
156+
"x": []byte("data6"),
157+
"yz": []byte("data7"),
158+
}, common.Hash{})
159+
160+
// Now we have leaves at depths 1, 2, and 5
161+
if stats.accountTrie.minDepth != 1 {
162+
t.Errorf("Account trie min depth after update = %d, want %d", stats.accountTrie.minDepth, 1)
163+
}
164+
if stats.accountTrie.maxDepth != 5 {
165+
t.Errorf("Account trie max depth after update = %d, want %d", stats.accountTrie.maxDepth, 5)
166+
}
167+
}
168+
169+
func TestWitnessStatsAverage(t *testing.T) {
170+
stats := NewWitnessStats()
171+
172+
// Add nodes that will create leaves at depths 2, 3, and 4
173+
stats.Add(map[string][]byte{
174+
"aa": []byte("data1"),
175+
"bb": []byte("data2"),
176+
"ccc": []byte("data3"),
177+
"dddd": []byte("data4"),
178+
}, common.Hash{})
179+
180+
// All are leaves: 2 + 2 + 3 + 4 = 11 total, 4 samples
181+
expectedAvg := int64(11) / int64(4)
182+
actualAvg := stats.accountTrie.totalDepth / stats.accountTrie.samples
183+
184+
if actualAvg != expectedAvg {
185+
t.Errorf("Account trie average depth = %d, want %d", actualAvg, expectedAvg)
186+
}
187+
}
188+
189+
func BenchmarkWitnessStatsAdd(b *testing.B) {
190+
// Create a realistic trie node structure
191+
nodes := make(map[string][]byte)
192+
for i := 0; i < 100; i++ {
193+
base := string(rune('a' + i%26))
194+
nodes[base] = []byte("data")
195+
for j := 0; j < 9; j++ {
196+
key := base + string(rune('0'+j))
197+
nodes[key] = []byte("data")
198+
}
199+
}
200+
201+
stats := NewWitnessStats()
202+
b.ResetTimer()
203+
204+
for i := 0; i < b.N; i++ {
205+
stats.Add(nodes, common.Hash{})
206+
}
207+
}

0 commit comments

Comments
 (0)