// Copyright 2025 The 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 . package stateless import ( "strings" "testing" "github.com/ethereum/go-ethereum/common" ) func expectedLeaves(counts map[int]int64) [16]int64 { var leaves [16]int64 for depth, count := range counts { leaves[depth] = count } return leaves } func TestWitnessStatsAdd(t *testing.T) { tests := []struct { name string nodes map[string][]byte owner common.Hash expectedAccountLeaves map[int]int64 expectedStorageLeaves map[int]int64 }{ { name: "empty nodes", nodes: map[string][]byte{}, owner: common.Hash{}, }, { name: "single account trie leaf at depth 0", nodes: map[string][]byte{ "": []byte("data"), }, owner: common.Hash{}, expectedAccountLeaves: map[int]int64{0: 1}, }, { name: "single account trie leaf", nodes: map[string][]byte{ "abc": []byte("data"), }, owner: common.Hash{}, expectedAccountLeaves: map[int]int64{3: 1}, }, { name: "account trie with internal nodes", nodes: map[string][]byte{ "a": []byte("data1"), "ab": []byte("data2"), "abc": []byte("data3"), }, owner: common.Hash{}, expectedAccountLeaves: map[int]int64{3: 1}, // Only "abc" is a leaf }, { name: "multiple account trie branches", nodes: map[string][]byte{ "a": []byte("data1"), "ab": []byte("data2"), "abc": []byte("data3"), "b": []byte("data4"), "bc": []byte("data5"), "bcd": []byte("data6"), }, owner: common.Hash{}, expectedAccountLeaves: map[int]int64{3: 2}, // "abc" (3) + "bcd" (3) }, { name: "siblings are all leaves", nodes: map[string][]byte{ "aa": []byte("data1"), "ab": []byte("data2"), "ac": []byte("data3"), }, owner: common.Hash{}, expectedAccountLeaves: map[int]int64{2: 3}, }, { name: "storage trie leaves", nodes: map[string][]byte{ "1": []byte("data1"), "12": []byte("data2"), "123": []byte("data3"), "124": []byte("data4"), }, owner: common.HexToHash("0x1234"), expectedStorageLeaves: map[int]int64{3: 2}, // "123" (3) + "124" (3) }, { name: "complex trie structure", nodes: map[string][]byte{ "1": []byte("data1"), "12": []byte("data2"), "123": []byte("data3"), "124": []byte("data4"), "2": []byte("data5"), "23": []byte("data6"), "234": []byte("data7"), "235": []byte("data8"), "3": []byte("data9"), }, owner: common.Hash{}, expectedAccountLeaves: map[int]int64{1: 1, 3: 4}, // "123"(3) + "124"(3) + "234"(3) + "235"(3) + "3"(1) }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { stats := NewWitnessStats() stats.Add(tt.nodes, tt.owner) if got, want := stats.accountTrie.LeafDepths(), expectedLeaves(tt.expectedAccountLeaves); got != want { t.Errorf("account trie leaves = %v, want %v", got, want) } if got, want := stats.storageTrie.LeafDepths(), expectedLeaves(tt.expectedStorageLeaves); got != want { t.Errorf("storage trie leaves = %v, want %v", got, want) } }) } } func TestWitnessStatsStorageTrieAggregation(t *testing.T) { stats := NewWitnessStats() ownerA := common.HexToHash("0xa") ownerB := common.HexToHash("0xb") stats.Add(map[string][]byte{ "a": []byte("data1"), "ab": []byte("data2"), "abc": []byte("data3"), }, ownerA) stats.Add(map[string][]byte{ "xy": []byte("data4"), }, ownerA) stats.Add(map[string][]byte{ "1": []byte("data5"), "12": []byte("data6"), "123": []byte("data7"), "124": []byte("data8"), }, ownerB) if got, want := stats.storageTrie.LeafDepths(), expectedLeaves(map[int]int64{2: 1, 3: 3}); got != want { t.Errorf("storage leaves = %v, want %v", got, want) } if got, want := stats.accountTrie.LeafDepths(), expectedLeaves(nil); got != want { t.Errorf("account leaves = %v, want %v", got, want) } } func TestWitnessStatsPanicsOnDeepLeaf(t *testing.T) { stats := NewWitnessStats() defer func() { if r := recover(); r == nil { t.Fatal("expected panic for depth >= 16") } }() stats.Add(map[string][]byte{strings.Repeat("a", 16): []byte("data")}, common.Hash{}) } func TestWitnessStatsMinMax(t *testing.T) { stats := NewWitnessStats() // Add some account trie nodes with varying depths. stats.Add(map[string][]byte{ "a": []byte("data1"), "ab": []byte("data2"), "abc": []byte("data3"), "abcd": []byte("data4"), "abcde": []byte("data5"), }, common.Hash{}) // Only "abcde" is a leaf (depth 5). for i, v := range stats.accountTrie.LeafDepths() { if v != 0 && i != 5 { t.Errorf("leaf found at invalid depth %d", i) } } // Add more leaves with different depths. stats.Add(map[string][]byte{ "x": []byte("data6"), "yz": []byte("data7"), }, common.Hash{}) // Now we have leaves at depths 1, 2, and 5. for i, v := range stats.accountTrie.LeafDepths() { if v != 0 && (i != 5 && i != 2 && i != 1) { t.Errorf("leaf found at invalid depth %d", i) } } } func TestWitnessStatsAverage(t *testing.T) { stats := NewWitnessStats() // Add nodes that will create leaves at depths 2, 3, and 4. stats.Add(map[string][]byte{ "aa": []byte("data1"), "bb": []byte("data2"), "ccc": []byte("data3"), "dddd": []byte("data4"), }, common.Hash{}) // All are leaves: 2 + 2 + 3 + 4 = 11 total, 4 samples. expectedAvg := int64(11) / int64(4) var actualAvg, totalSamples int64 for i, c := range stats.accountTrie.LeafDepths() { actualAvg += c * int64(i) totalSamples += c } actualAvg = actualAvg / totalSamples if actualAvg != expectedAvg { t.Errorf("account trie average depth = %d, want %d", actualAvg, expectedAvg) } } func BenchmarkWitnessStatsAdd(b *testing.B) { // Create a realistic trie node structure. nodes := make(map[string][]byte) for i := 0; i < 100; i++ { base := string(rune('a' + i%26)) nodes[base] = []byte("data") for j := 0; j < 9; j++ { key := base + string(rune('0'+j)) nodes[key] = []byte("data") } } stats := NewWitnessStats() b.ResetTimer() for i := 0; i < b.N; i++ { stats.Add(nodes, common.Hash{}) } }