From b9d45acf90becafbdf79bc4c127ff5209b7b230c Mon Sep 17 00:00:00 2001 From: weiihann Date: Wed, 31 Dec 2025 12:52:04 +0800 Subject: [PATCH] remove statesize tracer --- eth/tracers/live/statesize.go | 711 ----------------------------- eth/tracers/live/statesize_test.go | 244 ---------- 2 files changed, 955 deletions(-) delete mode 100644 eth/tracers/live/statesize.go delete mode 100644 eth/tracers/live/statesize_test.go diff --git a/eth/tracers/live/statesize.go b/eth/tracers/live/statesize.go deleted file mode 100644 index f502dd7cf6..0000000000 --- a/eth/tracers/live/statesize.go +++ /dev/null @@ -1,711 +0,0 @@ -// 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 live - -import ( - "bufio" - "encoding/csv" - "encoding/json" - "errors" - "fmt" - "io" - "os" - "path/filepath" - "slices" - "strconv" - "sync" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/rawdb" - "github.com/ethereum/go-ethereum/core/tracing" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/eth/tracers" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/rlp" -) - -func init() { - tracers.LiveDirectory.Register("statesize", newStateSizeTracer) -} - -// Database key size constants matching core/state/state_sizer.go -var ( - accountKeySize = int64(len(rawdb.SnapshotAccountPrefix) + common.HashLength) - storageKeySize = int64(len(rawdb.SnapshotStoragePrefix) + common.HashLength*2) - accountTrienodePrefixSize = int64(len(rawdb.TrieNodeAccountPrefix)) - storageTrienodePrefixSize = int64(len(rawdb.TrieNodeStoragePrefix) + common.HashLength) - codeKeySize = int64(len(rawdb.CodePrefix) + common.HashLength) -) - -// CSV column headers (cumulative only) -var csvHeaders = []string{ - "block_number", - "root", - "parent_root", - "accounts", - "account_bytes", - "storages", - "storage_bytes", - "account_trienodes", - "account_trienode_bytes", - "storage_trienodes", - "storage_trienode_bytes", - "codes", - "code_bytes", -} - -// depthCSVHeaders generates headers for the depth CSV files. -// Format: block_number, root, parent_root, total_nodes, account_depth_0..64, storage_depth_0..64 -func depthCSVHeaders() []string { - headers := make([]string, 0, 4+65+65) - headers = append(headers, "block_number", "root", "parent_root", "total_nodes") - for i := 0; i <= 64; i++ { - headers = append(headers, fmt.Sprintf("account_depth_%d", i)) - } - for i := 0; i <= 64; i++ { - headers = append(headers, fmt.Sprintf("storage_depth_%d", i)) - } - return headers -} - -// stateSizeStats represents cumulative state size statistics. -type stateSizeStats struct { - Accounts int64 - AccountBytes int64 - Storages int64 - StorageBytes int64 - AccountTrienodes int64 - AccountTrienodeBytes int64 - StorageTrienodes int64 - StorageTrienodeBytes int64 - Codes int64 - CodeBytes int64 -} - -// stateSizeRecord represents a single CSV record with cumulative stats. -type stateSizeRecord struct { - BlockNumber uint64 - Root common.Hash - ParentRoot common.Hash - Stats stateSizeStats -} - -type stateSizeTracer struct { - mu sync.Mutex - file *os.File - writer *csv.Writer - filePath string - - // Depth tracking - separate files for created and deleted nodes - depthCreatedFile *os.File - depthCreatedWriter *csv.Writer - depthCreatedFilePath string - - depthDeletedFile *os.File - depthDeletedWriter *csv.Writer - depthDeletedFilePath string - - // Map from state root to cumulative stats (for handling forks) - stats map[common.Hash]stateSizeStats - - // initialized is set to true after the first state update processes - // and loads the parent stats from the CSV file - initialized bool -} - -type stateSizeTracerConfig struct { - Path string `json:"path"` // Path to the directory where the tracer logs will be stored -} - -func newStateSizeTracer(cfg json.RawMessage) (*tracing.Hooks, error) { - var config stateSizeTracerConfig - if err := json.Unmarshal(cfg, &config); err != nil { - return nil, fmt.Errorf("failed to parse config: %v", err) - } - if config.Path == "" { - return nil, errors.New("statesize tracer output path is required") - } - - filePath := filepath.Join(config.Path, "statesize.csv") - depthCreatedFilePath := filepath.Join(config.Path, "statesize_depth_created.csv") - depthDeletedFilePath := filepath.Join(config.Path, "statesize_depth_deleted.csv") - - // Ensure the directory exists - if err := os.MkdirAll(config.Path, 0o755); err != nil { - return nil, fmt.Errorf("failed to create statesize directory: %v", err) - } - - t := &stateSizeTracer{ - filePath: filePath, - depthCreatedFilePath: depthCreatedFilePath, - depthDeletedFilePath: depthDeletedFilePath, - stats: make(map[common.Hash]stateSizeStats), - } - - // Open statesize file for appending - file, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) - if err != nil { - return nil, fmt.Errorf("failed to open statesize file: %v", err) - } - t.file = file - t.writer = csv.NewWriter(file) - - // Write header if file is new (empty) - info, err := file.Stat() - if err != nil { - file.Close() - return nil, fmt.Errorf("failed to stat statesize file: %v", err) - } - if info.Size() == 0 { - if err := t.writer.Write(csvHeaders); err != nil { - file.Close() - return nil, fmt.Errorf("failed to write CSV headers: %v", err) - } - t.writer.Flush() - } - - // Open depth created file for appending - depthCreatedFile, err := os.OpenFile(depthCreatedFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) - if err != nil { - file.Close() - return nil, fmt.Errorf("failed to open depth created file: %v", err) - } - t.depthCreatedFile = depthCreatedFile - t.depthCreatedWriter = csv.NewWriter(depthCreatedFile) - - // Write header if depth created file is new (empty) - depthCreatedInfo, err := depthCreatedFile.Stat() - if err != nil { - file.Close() - depthCreatedFile.Close() - return nil, fmt.Errorf("failed to stat depth created file: %v", err) - } - if depthCreatedInfo.Size() == 0 { - if err := t.depthCreatedWriter.Write(depthCSVHeaders()); err != nil { - file.Close() - depthCreatedFile.Close() - return nil, fmt.Errorf("failed to write depth created CSV headers: %v", err) - } - t.depthCreatedWriter.Flush() - } - - // Open depth deleted file for appending - depthDeletedFile, err := os.OpenFile(depthDeletedFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) - if err != nil { - file.Close() - depthCreatedFile.Close() - return nil, fmt.Errorf("failed to open depth deleted file: %v", err) - } - t.depthDeletedFile = depthDeletedFile - t.depthDeletedWriter = csv.NewWriter(depthDeletedFile) - - // Write header if depth deleted file is new (empty) - depthDeletedInfo, err := depthDeletedFile.Stat() - if err != nil { - file.Close() - depthCreatedFile.Close() - depthDeletedFile.Close() - return nil, fmt.Errorf("failed to stat depth deleted file: %v", err) - } - if depthDeletedInfo.Size() == 0 { - if err := t.depthDeletedWriter.Write(depthCSVHeaders()); err != nil { - file.Close() - depthCreatedFile.Close() - depthDeletedFile.Close() - return nil, fmt.Errorf("failed to write depth deleted CSV headers: %v", err) - } - t.depthDeletedWriter.Flush() - } - - return &tracing.Hooks{ - OnStateUpdate: t.onStateUpdate, - OnClose: t.onClose, - }, nil -} - -// loadStatsForRoot searches the CSV file for a record with the given root hash -// and returns its stats. Returns zero stats if not found. -func (s *stateSizeTracer) loadStatsForRoot(root common.Hash) (stateSizeStats, bool) { - file, err := os.Open(s.filePath) - if err != nil { - return stateSizeStats{}, false - } - defer file.Close() - - reader := csv.NewReader(bufio.NewReader(file)) - - // Read and skip header - if _, err := reader.Read(); err != nil { - return stateSizeStats{}, false - } - - // Search for the root in the CSV file (read backwards would be more efficient, - // but CSV doesn't support that easily, so we just remember the last match) - var ( - found bool - lastStats stateSizeStats - ) - - for { - record, err := reader.Read() - if err == io.EOF { - break - } - if err != nil { - return stateSizeStats{}, false - } - if len(record) < len(csvHeaders) { - continue - } - - recordRoot := common.HexToHash(record[1]) - if recordRoot == root { - stats, err := parseStats(record) - if err != nil { - continue - } - lastStats = stats - found = true - // Don't break - keep searching for the latest occurrence - } - } - - if found { - log.Info("Loaded initial stats from CSV", "root", root.Hex()) - } - return lastStats, found -} - -// parseStats extracts cumulative statistics from a CSV record. -func parseStats(record []string) (stateSizeStats, error) { - if len(record) < len(csvHeaders) { - return stateSizeStats{}, errors.New("record too short") - } - - // Cumulative columns start at index 3 - stats := stateSizeStats{} - var err error - - stats.Accounts, err = strconv.ParseInt(record[3], 10, 64) - if err != nil { - return stats, err - } - stats.AccountBytes, err = strconv.ParseInt(record[4], 10, 64) - if err != nil { - return stats, err - } - stats.Storages, err = strconv.ParseInt(record[5], 10, 64) - if err != nil { - return stats, err - } - stats.StorageBytes, err = strconv.ParseInt(record[6], 10, 64) - if err != nil { - return stats, err - } - stats.AccountTrienodes, err = strconv.ParseInt(record[7], 10, 64) - if err != nil { - return stats, err - } - stats.AccountTrienodeBytes, err = strconv.ParseInt(record[8], 10, 64) - if err != nil { - return stats, err - } - stats.StorageTrienodes, err = strconv.ParseInt(record[9], 10, 64) - if err != nil { - return stats, err - } - stats.StorageTrienodeBytes, err = strconv.ParseInt(record[10], 10, 64) - if err != nil { - return stats, err - } - stats.Codes, err = strconv.ParseInt(record[11], 10, 64) - if err != nil { - return stats, err - } - stats.CodeBytes, err = strconv.ParseInt(record[12], 10, 64) - if err != nil { - return stats, err - } - - return stats, nil -} - -func (s *stateSizeTracer) onStateUpdate(update *tracing.StateUpdate) { - if update == nil { - return - } - - // On first update, try to load parent stats from the CSV file - s.mu.Lock() - if !s.initialized && update.OriginRoot != (types.EmptyRootHash) { - s.initialized = true - stats, found := s.loadStatsForRoot(update.OriginRoot) - if !found { - log.Crit("Failed to load parent stats from CSV", "root", update.OriginRoot.Hex()) - return - } - s.stats[update.OriginRoot] = stats - } - s.mu.Unlock() - - // Calculate deltas - var ( - accountsDelta int64 - accountBytesDelta int64 - storagesDelta int64 - storageBytesDelta int64 - accountTrienodesDelta int64 - accountTrienodeBytes int64 - storageTrienodesDelta int64 - storageTrienodeBytes int64 - codesDelta int64 - codeBytesDelta int64 - ) - - // Calculate account size changes - for _, change := range update.AccountChanges { - prevLen := slimAccountSize(change.Prev) - newLen := slimAccountSize(change.New) - - switch { - case prevLen > 0 && newLen == 0: - accountsDelta-- - accountBytesDelta -= accountKeySize + int64(prevLen) - case prevLen == 0 && newLen > 0: - accountsDelta++ - accountBytesDelta += accountKeySize + int64(newLen) - default: - accountBytesDelta += int64(newLen - prevLen) - } - } - - encode := func(val common.Hash) []byte { - if val == (common.Hash{}) { - return nil - } - blob, _ := rlp.EncodeToBytes(common.TrimLeftZeroes(val[:])) - return blob - } - - // Calculate storage size changes - for _, slots := range update.StorageChanges { - for _, change := range slots { - prevLen := len(encode(change.Prev)) - newLen := len(encode(change.New)) - - switch { - case prevLen > 0 && newLen == 0: - storagesDelta-- - storageBytesDelta -= storageKeySize + int64(prevLen) - case prevLen == 0 && newLen > 0: - storagesDelta++ - storageBytesDelta += storageKeySize + int64(newLen) - default: - storageBytesDelta += int64(newLen - prevLen) - } - } - } - - // Calculate trie node size changes and depth counts - var ( - accountDepthCreated [65]int64 - storageDepthCreated [65]int64 - accountDepthDeleted [65]int64 - storageDepthDeleted [65]int64 - ) - - for owner, nodes := range update.TrieChanges { - var ( - keyPrefix int64 - isAccount = owner == (common.Hash{}) - ) - if isAccount { - keyPrefix = accountTrienodePrefixSize - } else { - keyPrefix = storageTrienodePrefixSize - } - - // Calculate depth counts for created/modified and deleted nodes - createdCounts, deletedCounts := calculateDepthCountsByType(nodes) - - for path, change := range nodes { - var prevLen, newLen int - if change.Prev != nil { - prevLen = len(change.Prev.Blob) - } - if change.New != nil { - newLen = len(change.New.Blob) - } - keySize := keyPrefix + int64(len(path)) - - switch { - case prevLen > 0 && newLen == 0: - if isAccount { - accountTrienodesDelta-- - accountTrienodeBytes -= keySize + int64(prevLen) - } else { - storageTrienodesDelta-- - storageTrienodeBytes -= keySize + int64(prevLen) - } - case prevLen == 0 && newLen > 0: - if isAccount { - accountTrienodesDelta++ - accountTrienodeBytes += keySize + int64(newLen) - } else { - storageTrienodesDelta++ - storageTrienodeBytes += keySize + int64(newLen) - } - default: - if isAccount { - accountTrienodeBytes += int64(newLen - prevLen) - } else { - storageTrienodeBytes += int64(newLen - prevLen) - } - } - } - - // Accumulate depth counts - if isAccount { - for i := range 65 { - accountDepthCreated[i] += createdCounts[i] - accountDepthDeleted[i] += deletedCounts[i] - } - } else { - for i := range 65 { - storageDepthCreated[i] += createdCounts[i] - storageDepthDeleted[i] += deletedCounts[i] - } - } - } - - // Calculate contract code size changes - codeExists := make(map[common.Hash]struct{}) - for _, code := range update.CodeChanges { - if _, ok := codeExists[code.Hash]; ok || code.Exists { - continue - } - codesDelta++ - codeBytesDelta += codeKeySize + int64(len(code.Code)) - codeExists[code.Hash] = struct{}{} - } - - // Calculate cumulative statistics - s.mu.Lock() - defer s.mu.Unlock() - - // Look up parent stats - parentStats := s.stats[update.OriginRoot] // zero value if not found - - // Apply deltas to get new cumulative stats - newStats := stateSizeStats{ - Accounts: parentStats.Accounts + accountsDelta, - AccountBytes: parentStats.AccountBytes + accountBytesDelta, - Storages: parentStats.Storages + storagesDelta, - StorageBytes: parentStats.StorageBytes + storageBytesDelta, - AccountTrienodes: parentStats.AccountTrienodes + accountTrienodesDelta, - AccountTrienodeBytes: parentStats.AccountTrienodeBytes + accountTrienodeBytes, - StorageTrienodes: parentStats.StorageTrienodes + storageTrienodesDelta, - StorageTrienodeBytes: parentStats.StorageTrienodeBytes + storageTrienodeBytes, - Codes: parentStats.Codes + codesDelta, - CodeBytes: parentStats.CodeBytes + codeBytesDelta, - } - - // Store the new stats for this root - s.stats[update.Root] = newStats - - // Calculate total nodes for created and deleted - var totalCreated, totalDeleted int64 - for _, c := range accountDepthCreated { - totalCreated += c - } - for _, c := range storageDepthCreated { - totalCreated += c - } - for _, c := range accountDepthDeleted { - totalDeleted += c - } - for _, c := range storageDepthDeleted { - totalDeleted += c - } - - // Write to statesize CSV - s.writeRecord(stateSizeRecord{ - BlockNumber: update.BlockNumber, - Root: update.Root, - ParentRoot: update.OriginRoot, - Stats: newStats, - }) - - // Write to depth CSV files - s.writeDepthRecord(s.depthCreatedWriter, update.BlockNumber, update.Root, update.OriginRoot, totalCreated, accountDepthCreated, storageDepthCreated) - s.writeDepthRecord(s.depthDeletedWriter, update.BlockNumber, update.Root, update.OriginRoot, totalDeleted, accountDepthDeleted, storageDepthDeleted) -} - -func (s *stateSizeTracer) writeRecord(r stateSizeRecord) { - row := []string{ - strconv.FormatUint(r.BlockNumber, 10), - r.Root.Hex(), - r.ParentRoot.Hex(), - strconv.FormatInt(r.Stats.Accounts, 10), - strconv.FormatInt(r.Stats.AccountBytes, 10), - strconv.FormatInt(r.Stats.Storages, 10), - strconv.FormatInt(r.Stats.StorageBytes, 10), - strconv.FormatInt(r.Stats.AccountTrienodes, 10), - strconv.FormatInt(r.Stats.AccountTrienodeBytes, 10), - strconv.FormatInt(r.Stats.StorageTrienodes, 10), - strconv.FormatInt(r.Stats.StorageTrienodeBytes, 10), - strconv.FormatInt(r.Stats.Codes, 10), - strconv.FormatInt(r.Stats.CodeBytes, 10), - } - - if err := s.writer.Write(row); err != nil { - log.Warn("Failed to write statesize record", "error", err) - return - } - s.writer.Flush() - if err := s.writer.Error(); err != nil { - log.Warn("Failed to flush statesize record", "error", err) - } -} - -func (s *stateSizeTracer) writeDepthRecord(writer *csv.Writer, blockNumber uint64, root, parentRoot common.Hash, totalNodes int64, accountDepths, storageDepths [65]int64) { - // Build row: block_number, root, parent_root, total_nodes, account_depth_0..64, storage_depth_0..64 - row := make([]string, 0, 4+65+65) - row = append(row, strconv.FormatUint(blockNumber, 10)) - row = append(row, root.Hex()) - row = append(row, parentRoot.Hex()) - row = append(row, strconv.FormatInt(totalNodes, 10)) - - for i := 0; i < 65; i++ { - row = append(row, strconv.FormatInt(accountDepths[i], 10)) - } - for i := 0; i < 65; i++ { - row = append(row, strconv.FormatInt(storageDepths[i], 10)) - } - - if err := writer.Write(row); err != nil { - log.Warn("Failed to write depth record", "error", err) - return - } - writer.Flush() - if err := writer.Error(); err != nil { - log.Warn("Failed to flush depth record", "error", err) - } -} - -func (s *stateSizeTracer) onClose() { - s.mu.Lock() - defer s.mu.Unlock() - - if s.writer != nil { - s.writer.Flush() - } - if s.file != nil { - if err := s.file.Close(); err != nil { - log.Warn("Failed to close statesize tracer file", "error", err) - } - } - if s.depthCreatedWriter != nil { - s.depthCreatedWriter.Flush() - } - if s.depthCreatedFile != nil { - if err := s.depthCreatedFile.Close(); err != nil { - log.Warn("Failed to close depth created file", "error", err) - } - } - if s.depthDeletedWriter != nil { - s.depthDeletedWriter.Flush() - } - if s.depthDeletedFile != nil { - if err := s.depthDeletedFile.Close(); err != nil { - log.Warn("Failed to close depth deleted file", "error", err) - } - } -} - -// slimAccountSize calculates the RLP-encoded size of an account in slim format. -func slimAccountSize(acct *types.StateAccount) int { - if acct == nil { - return 0 - } - data := types.SlimAccountRLP(*acct) - return len(data) -} - -// calculateDepthCountsByType calculates the depth of each node and separates counts -// into created/modified nodes and deleted nodes. -// - Created/Modified: nodes that exist after the update (New has data) -// - Deleted: nodes that existed before but don't exist after (Prev has data, New is empty) -func calculateDepthCountsByType(pathMap map[string]*tracing.TrieNodeChange) (created, deleted [65]int64) { - n := len(pathMap) - if n == 0 { - return - } - - // First, calculate depth for all nodes using the tree structure - paths := make([]string, 0, n) - for path := range pathMap { - paths = append(paths, path) - } - slices.Sort(paths) - - // Map from path to its depth - depthMap := make(map[string]int, n) - - // Stack stores paths of ancestors - stack := make([]string, 0, 65) - - for _, path := range paths { - // Pop until stack top is a strict prefix of path - for len(stack) > 0 { - top := stack[len(stack)-1] - if len(top) < len(path) && path[:len(top)] == top { - break - } - stack = stack[:len(stack)-1] - } - - depth := len(stack) - depthMap[path] = depth - - stack = append(stack, path) - } - - // Now classify each node based on Prev/New status - for path, change := range pathMap { - depth := depthMap[path] - - var prevLen, newLen int - if change.Prev != nil { - prevLen = len(change.Prev.Blob) - } - if change.New != nil { - newLen = len(change.New.Blob) - } - - // Created/Modified: New has data (node exists after update) - if newLen > 0 { - created[depth]++ - } - // Deleted: Prev has data but New is empty (node removed) - if prevLen > 0 && newLen == 0 { - deleted[depth]++ - } - } - - return -} diff --git a/eth/tracers/live/statesize_test.go b/eth/tracers/live/statesize_test.go deleted file mode 100644 index 120887441a..0000000000 --- a/eth/tracers/live/statesize_test.go +++ /dev/null @@ -1,244 +0,0 @@ -// 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 live - -import ( - "testing" - - "github.com/ethereum/go-ethereum/core/tracing" - "github.com/ethereum/go-ethereum/trie/trienode" -) - -func TestCalculateDepthCountsByType(t *testing.T) { - tests := []struct { - name string - paths []string - expectedCreated [65]int64 - }{ - { - name: "empty map", - paths: []string{}, - expectedCreated: [65]int64{}, - }, - { - name: "root only", - paths: []string{""}, - expectedCreated: func() [65]int64 { - var c [65]int64 - c[0] = 1 - return c - }(), - }, - { - name: "linear chain - all branch nodes", - paths: []string{"", "0", "0a", "0a3"}, - expectedCreated: func() [65]int64 { - var c [65]int64 - c[0] = 1 // "" - c[1] = 1 // "0" - c[2] = 1 // "0a" - c[3] = 1 // "0a3" - return c - }(), - }, - { - name: "extension node - path jump", - paths: []string{"", "0a3"}, // extension from root to "0a3" - expectedCreated: func() [65]int64 { - var c [65]int64 - c[0] = 1 // "" - c[1] = 1 // "0a3" (child of root) - return c - }(), - }, - { - name: "branching at root", - paths: []string{"", "0", "1", "2"}, - expectedCreated: func() [65]int64 { - var c [65]int64 - c[0] = 1 // "" - c[1] = 3 // "0", "1", "2" - return c - }(), - }, - { - name: "two branches from root", - paths: []string{"", "0", "0a", "1", "1b"}, - expectedCreated: func() [65]int64 { - var c [65]int64 - c[0] = 1 // "" - c[1] = 2 // "0", "1" - c[2] = 2 // "0a", "1b" - return c - }(), - }, - { - name: "mixed extension and branch", - paths: []string{"", "0", "0a", "0a3", "0b"}, - expectedCreated: func() [65]int64 { - var c [65]int64 - c[0] = 1 // "" - c[1] = 1 // "0" - c[2] = 2 // "0a", "0b" - c[3] = 1 // "0a3" - return c - }(), - }, - { - name: "deep path with extensions", - paths: []string{"", "abc", "abcdef"}, - expectedCreated: func() [65]int64 { - var c [65]int64 - c[0] = 1 // "" - c[1] = 1 // "abc" - c[2] = 1 // "abcdef" - return c - }(), - }, - { - name: "siblings at various depths", - paths: []string{"", "0", "00", "01", "1", "10", "11"}, - expectedCreated: func() [65]int64 { - var c [65]int64 - c[0] = 1 // "" - c[1] = 2 // "0", "1" - c[2] = 4 // "00", "01", "10", "11" - return c - }(), - }, - { - name: "complex tree", - paths: []string{"", "a", "ab", "abc", "abd", "b", "bc"}, - expectedCreated: func() [65]int64 { - var c [65]int64 - c[0] = 1 // "" - c[1] = 2 // "a", "b" - c[2] = 2 // "ab", "bc" - c[3] = 2 // "abc", "abd" - return c - }(), - }, - { - name: "max depth path", - paths: []string{"", "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}, - expectedCreated: func() [65]int64 { - var c [65]int64 - c[0] = 1 // "" - c[1] = 1 // 64-nibble path (child of root via extension) - return c - }(), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Build pathMap with New blob data (simulates created nodes) - pathMap := make(map[string]*tracing.TrieNodeChange, len(tt.paths)) - for _, p := range tt.paths { - pathMap[p] = &tracing.TrieNodeChange{ - New: &trienode.Node{Blob: []byte{0x01}}, // Non-empty blob marks as created - } - } - - created, deleted := calculateDepthCountsByType(pathMap) - - if created != tt.expectedCreated { - t.Errorf("calculateDepthCountsByType() created mismatch") - for i := 0; i < 65; i++ { - if created[i] != tt.expectedCreated[i] { - t.Errorf(" depth %d: got %d, want %d", i, created[i], tt.expectedCreated[i]) - } - } - } - - // All nodes have New data, so deleted should be all zeros - var expectedDeleted [65]int64 - if deleted != expectedDeleted { - t.Errorf("calculateDepthCountsByType() deleted should be all zeros for created nodes") - } - }) - } -} - -func TestCalculateDepthCountsByType_Deletion(t *testing.T) { - // Test that deleted nodes are counted correctly - pathMap := map[string]*tracing.TrieNodeChange{ - "": {Prev: &trienode.Node{Blob: []byte{0x01}}}, // deleted at depth 0 - "0": {Prev: &trienode.Node{Blob: []byte{0x01}}, New: &trienode.Node{Blob: []byte{}}}, // deleted at depth 1 - "0a": {Prev: &trienode.Node{Blob: []byte{0x01}}}, // deleted at depth 2 - "1": {Prev: &trienode.Node{Blob: []byte{0x01}}, New: &trienode.Node{Blob: []byte{0x02}}}, // modified at depth 1 (not deleted) - } - - created, deleted := calculateDepthCountsByType(pathMap) - - // Expected deleted: depth 0 = 1, depth 1 = 1, depth 2 = 1 - expectedDeleted := [65]int64{1, 1, 1} - for i := 0; i < 65; i++ { - if deleted[i] != expectedDeleted[i] { - t.Errorf("deleted depth %d: got %d, want %d", i, deleted[i], expectedDeleted[i]) - } - } - - // Expected created: only "1" is modified (has New data), depth 1 = 1 - expectedCreated := [65]int64{0, 1} - for i := 0; i < 65; i++ { - if created[i] != expectedCreated[i] { - t.Errorf("created depth %d: got %d, want %d", i, created[i], expectedCreated[i]) - } - } -} - -func TestCalculateDepthCountsByType_Mixed(t *testing.T) { - // Test mixed scenario: some nodes created, some modified, some deleted - pathMap := map[string]*tracing.TrieNodeChange{ - "": { - Prev: &trienode.Node{Blob: []byte{0x01}}, - New: &trienode.Node{Blob: []byte{0x02}}, - }, // modified at depth 0 - "0": { - New: &trienode.Node{Blob: []byte{0x01}}, - }, // created at depth 1 - "1": { - Prev: &trienode.Node{Blob: []byte{0x01}}, - }, // deleted at depth 1 - "0a": { - Prev: &trienode.Node{Blob: []byte{0x01}}, - New: &trienode.Node{Blob: []byte{0x02}}, - }, // modified at depth 2 - "1b": { - Prev: &trienode.Node{Blob: []byte{0x01}}, - }, // deleted at depth 2 - } - - created, deleted := calculateDepthCountsByType(pathMap) - - // Created/Modified: "" (depth 0), "0" (depth 1), "0a" (depth 2) - expectedCreated := [65]int64{1, 1, 1} - for i := 0; i < 65; i++ { - if created[i] != expectedCreated[i] { - t.Errorf("created depth %d: got %d, want %d", i, created[i], expectedCreated[i]) - } - } - - // Deleted: "1" (depth 1), "1b" (depth 2) - expectedDeleted := [65]int64{0, 1, 1} - for i := 0; i < 65; i++ { - if deleted[i] != expectedDeleted[i] { - t.Errorf("deleted depth %d: got %d, want %d", i, deleted[i], expectedDeleted[i]) - } - } -}