// 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 trie import ( "encoding/json" "math/rand" "os" "path/filepath" "reflect" "testing" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/trie/trienode" "github.com/holiman/uint256" ) // TestInspect inspects a randomly generated account trie. It's useful for // quickly verifying changes to the results display. func TestInspect(t *testing.T) { db := newTestDatabase(rawdb.NewMemoryDatabase(), rawdb.HashScheme) trie, err := NewStateTrie(TrieID(types.EmptyRootHash), db) if err != nil { t.Fatalf("failed to create state trie: %v", err) } // Create a realistic looking account trie with storage. addresses, accounts := makeAccountsWithStorage(db, 11, true) for i := 0; i < len(addresses); i++ { trie.MustUpdate(crypto.Keccak256(addresses[i][:]), accounts[i]) } // Insert the accounts into the trie and hash it root, nodes := trie.Commit(true) db.Update(root, types.EmptyRootHash, trienode.NewWithNodeSet(nodes)) db.Commit(root) tempDir := t.TempDir() dumpPath := filepath.Join(tempDir, "trie-dump.bin") if err := Inspect(db, root, &InspectConfig{ TopN: 1, DumpPath: dumpPath, Path: filepath.Join(tempDir, "trie-summary.json"), }); err != nil { t.Fatalf("inspect failed: %v", err) } reanalysisPath := filepath.Join(tempDir, "trie-summary-reanalysis.json") if err := Summarize(dumpPath, &InspectConfig{ TopN: 1, Path: reanalysisPath, }); err != nil { t.Fatalf("summarize failed: %v", err) } inspectSummaryPath := filepath.Join(tempDir, "trie-summary.json") inspectOut := loadInspectJSON(t, inspectSummaryPath) reanalysisOut := loadInspectJSON(t, reanalysisPath) if len(inspectOut.StorageSummary.Levels) == 0 { t.Fatal("expected StorageSummary.Levels to be populated") } if inspectOut.AccountTrie.Summary.Size == 0 { t.Fatal("expected account trie size summary to be populated") } if inspectOut.StorageSummary.Totals.Size == 0 { t.Fatal("expected storage trie size summary to be populated") } if !reflect.DeepEqual(inspectOut.AccountTrie, reanalysisOut.AccountTrie) { t.Fatal("account trie summary mismatch between inspect and summarize") } if !reflect.DeepEqual(inspectOut.StorageSummary, reanalysisOut.StorageSummary) { t.Fatal("storage summary mismatch between inspect and summarize") } assertStorageTotalsMatchLevels(t, inspectOut) assertStorageTotalsMatchLevels(t, reanalysisOut) assertAccountTotalsMatchLevels(t, inspectOut.AccountTrie) assertAccountTotalsMatchLevels(t, reanalysisOut.AccountTrie) var histogramTotal uint64 for _, count := range inspectOut.StorageSummary.DepthHistogram { histogramTotal += count } if histogramTotal != inspectOut.StorageSummary.TotalStorageTries { t.Fatalf("depth histogram total %d does not match total storage tries %d", histogramTotal, inspectOut.StorageSummary.TotalStorageTries) } } type inspectJSONOutput struct { // Reuse storageStats for AccountTrie JSON to avoid introducing a parallel // account summary test type. AccountTrie JSON includes Levels+Summary, // which map directly; other storageStats fields remain zero-values. AccountTrie storageStats `json:"AccountTrie"` StorageSummary struct { TotalStorageTries uint64 `json:"TotalStorageTries"` Totals jsonLevel `json:"Totals"` Levels []jsonLevel `json:"Levels"` DepthHistogram [trieStatLevels]uint64 `json:"DepthHistogram"` } `json:"StorageSummary"` } func loadInspectJSON(t *testing.T, path string) inspectJSONOutput { t.Helper() raw, err := os.ReadFile(path) if err != nil { t.Fatalf("failed to read %s: %v", path, err) } var out inspectJSONOutput if err := json.Unmarshal(raw, &out); err != nil { t.Fatalf("failed to decode %s: %v", path, err) } return out } func assertStorageTotalsMatchLevels(t *testing.T, out inspectJSONOutput) { t.Helper() var fromLevels jsonLevel for _, level := range out.StorageSummary.Levels { fromLevels.Short += level.Short fromLevels.Full += level.Full fromLevels.Value += level.Value fromLevels.Size += level.Size } if fromLevels.Short != out.StorageSummary.Totals.Short || fromLevels.Full != out.StorageSummary.Totals.Full || fromLevels.Value != out.StorageSummary.Totals.Value || fromLevels.Size != out.StorageSummary.Totals.Size { t.Fatalf("storage totals mismatch: levels=%+v totals=%+v", fromLevels, out.StorageSummary.Totals) } } func assertAccountTotalsMatchLevels(t *testing.T, account storageStats) { t.Helper() var fromLevels jsonLevel for _, level := range account.Levels { fromLevels.Short += level.Short fromLevels.Full += level.Full fromLevels.Value += level.Value fromLevels.Size += level.Size } if fromLevels.Short != account.Summary.Short || fromLevels.Full != account.Summary.Full || fromLevels.Value != account.Summary.Value || fromLevels.Size != account.Summary.Size { t.Fatalf("account totals mismatch: levels=%+v totals=%+v", fromLevels, account.Summary) } } // TestInspectContract tests the InspectContract function on a single account // with storage and snapshot data. func TestInspectContract(t *testing.T) { diskdb := rawdb.NewMemoryDatabase() db := newTestDatabase(diskdb, rawdb.HashScheme) // Create a contract address and its storage trie. address := common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678") accountHash := crypto.Keccak256Hash(address.Bytes()) // Build a storage trie with some entries. storageTrie := NewEmpty(db) storageSlots := make(map[common.Hash][]byte) for i := 0; i < 10; i++ { k := crypto.Keccak256Hash([]byte{byte(i)}) v := []byte{byte(i + 1)} storageTrie.MustUpdate(k.Bytes(), v) storageSlots[k] = v } storageRoot, storageNodes := storageTrie.Commit(true) db.Update(storageRoot, types.EmptyRootHash, trienode.NewWithNodeSet(storageNodes)) db.Commit(storageRoot) // Build the account trie with the contract account. account := types.StateAccount{ Nonce: 1, Balance: uint256.NewInt(1000), Root: storageRoot, CodeHash: crypto.Keccak256(nil), } accountRLP, err := rlp.EncodeToBytes(&account) if err != nil { t.Fatalf("failed to encode account: %v", err) } accountTrie := NewEmpty(db) accountTrie.MustUpdate(crypto.Keccak256(address.Bytes()), accountRLP) stateRoot, accountNodes := accountTrie.Commit(true) db.Update(stateRoot, types.EmptyRootHash, trienode.NewWithNodeSet(accountNodes)) db.Commit(stateRoot) // Write snapshot data for the account and its storage slots. rawdb.WriteAccountSnapshot(diskdb, accountHash, accountRLP) for k, v := range storageSlots { rawdb.WriteStorageSnapshot(diskdb, accountHash, k, v) } // InspectContract should succeed without error. if err := InspectContract(db, diskdb, stateRoot, address); err != nil { t.Fatalf("InspectContract failed: %v", err) } } func makeAccountsWithStorage(db *testDb, size int, storage bool) (addresses [][20]byte, accounts [][]byte) { // Make the random benchmark deterministic random := rand.New(rand.NewSource(0)) addresses = make([][20]byte, size) for i := 0; i < len(addresses); i++ { data := make([]byte, 20) random.Read(data) copy(addresses[i][:], data) } accounts = make([][]byte, len(addresses)) for i := 0; i < len(accounts); i++ { var ( nonce = uint64(random.Int63()) root = types.EmptyRootHash code = crypto.Keccak256(nil) ) if storage { trie := NewEmpty(db) for range random.Uint32()%256 + 1 { // non-zero k, v := make([]byte, 32), make([]byte, 32) random.Read(k) random.Read(v) trie.MustUpdate(k, v) } var nodes *trienode.NodeSet root, nodes = trie.Commit(true) db.Update(root, types.EmptyRootHash, trienode.NewWithNodeSet(nodes)) db.Commit(root) } numBytes := random.Uint32() % 33 // [0, 32] bytes balanceBytes := make([]byte, numBytes) random.Read(balanceBytes) balance := new(uint256.Int).SetBytes(balanceBytes) data, _ := rlp.EncodeToBytes(&types.StateAccount{ Nonce: nonce, Balance: balance, Root: root, CodeHash: code, }) accounts[i] = data } return addresses, accounts }