diff --git a/cmd/geth/chaincmd.go b/cmd/geth/chaincmd.go index 95b0ec274e..143c0a6e74 100644 --- a/cmd/geth/chaincmd.go +++ b/cmd/geth/chaincmd.go @@ -115,7 +115,7 @@ if one is set. Otherwise it prints the genesis from the datadir.`, utils.MetricsInfluxDBTokenFlag, utils.MetricsInfluxDBBucketFlag, utils.MetricsInfluxDBOrganizationFlag, - utils.StateSizeTrackingFlag, + utils.StateSizeTrackingFlag, // deprecated utils.VMTraceFlag, utils.VMTraceJsonConfigFlag, utils.TransactionHistoryFlag, diff --git a/cmd/geth/config.go b/cmd/geth/config.go index 8ac9e7f6bd..a2c4327c2e 100644 --- a/cmd/geth/config.go +++ b/cmd/geth/config.go @@ -100,6 +100,7 @@ var deprecatedConfigFields = map[string]bool{ "ethconfig.Config.LightPeers": true, "ethconfig.Config.LightNoPrune": true, "ethconfig.Config.LightNoSyncServe": true, + "ethconfig.Config.EnableStateSizeTracking": true, } type ethstatsConfig struct { diff --git a/cmd/geth/main.go b/cmd/geth/main.go index 48a9ae583f..6bf9c3a587 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -209,8 +209,8 @@ var ( utils.MetricsInfluxDBTokenFlag, utils.MetricsInfluxDBBucketFlag, utils.MetricsInfluxDBOrganizationFlag, - utils.StateSizeTrackingFlag, utils.SnapV2Flag, + utils.StateSizeTrackingFlag, // deprecated } ) diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 9b64742808..9ecd487ff9 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -297,12 +297,6 @@ var ( Usage: "Scheme to use for storing ethereum state ('hash' or 'path')", Category: flags.StateCategory, } - StateSizeTrackingFlag = &cli.BoolFlag{ - Name: "state.size-tracking", - Usage: "Enable state size tracking, retrieve state size with debug_stateSize.", - Value: ethconfig.Defaults.EnableStateSizeTracking, - Category: flags.StateCategory, - } SnapV2Flag = &cli.BoolFlag{ Name: "snap.v2", Usage: "Enable the experimental snap/2 (EIP-8189, BAL-based) sync protocol (advertises and syncs via snap/2; not safe on public networks)", @@ -1975,9 +1969,6 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) { cfg.EthDiscoveryURLs = SplitAndTrim(urls) } } - if ctx.IsSet(StateSizeTrackingFlag.Name) { - cfg.EnableStateSizeTracking = ctx.Bool(StateSizeTrackingFlag.Name) - } if ctx.IsSet(SnapV2Flag.Name) { cfg.SnapV2 = ctx.Bool(SnapV2Flag.Name) } @@ -2495,9 +2486,6 @@ func MakeChain(ctx *cli.Context, stack *node.Node, readonly bool) (*core.BlockCh // - DATADIR/triedb/verkle.journal TrieJournalDirectory: stack.ResolvePath("triedb"), - // Enable state size tracking if enabled - StateSizeTracking: ctx.Bool(StateSizeTrackingFlag.Name), - // Configure the slow block statistic logger (disabled by default) SlowBlockThreshold: ethconfig.Defaults.SlowBlockThreshold, } diff --git a/cmd/utils/flags_legacy.go b/cmd/utils/flags_legacy.go index 2a3e417993..7b2afd6df5 100644 --- a/cmd/utils/flags_legacy.go +++ b/cmd/utils/flags_legacy.go @@ -19,6 +19,7 @@ package utils import ( "fmt" + "github.com/ethereum/go-ethereum/internal/flags" "github.com/urfave/cli/v2" ) @@ -30,7 +31,21 @@ var ShowDeprecated = &cli.Command{ Description: "Show flags that have been deprecated and will soon be removed", } -var DeprecatedFlags = []cli.Flag{} +var DeprecatedFlags = []cli.Flag{ + StateSizeTrackingFlag, +} + +var ( + // Deprecated August 2026, state size tracking was removed along with the + // debug_stateSize endpoint it fed. Kept registered so that nodes carrying + // the flag in their startup scripts do not fail to boot. + StateSizeTrackingFlag = &cli.BoolFlag{ + Name: "state.size-tracking", + Hidden: true, + Usage: "Enable state size tracking (deprecated)", + Category: flags.DeprecatedCategory, + } +) // showDeprecated displays deprecated flags that will be soon removed from the codebase. func showDeprecated(*cli.Context) error { diff --git a/core/blockchain.go b/core/blockchain.go index 5cb8349b10..1bcdaa071b 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -215,9 +215,6 @@ type BlockChainConfig struct { // If the value is -1, indexing is disabled. TxLookupLimit int64 - // StateSizeTracking indicates whether the state size tracking is enabled. - StateSizeTracking bool - // SlowBlockThreshold is the block execution time threshold beyond which // detailed statistics will be logged. Negative value means disabled (default), // zero logs all blocks, positive value filters blocks by execution time. @@ -372,7 +369,6 @@ type BlockChain struct { prefetcher Prefetcher processor Processor // Block transaction processor interface logger *tracing.Hooks - stateSizer *state.SizeTracker // State size tracking lastForkReadyAlert time.Time // Last time there was a fork readiness print out slowBlockThreshold time.Duration // Block execution time threshold beyond which detailed statistics will be logged @@ -570,17 +566,6 @@ func NewBlockChain(db ethdb.Database, genesis *Genesis, engine consensus.Engine, if bc.cfg.TxLookupLimit >= 0 { bc.txIndexer = newTxIndexer(uint64(bc.cfg.TxLookupLimit), bc) } - - // Start state size tracker - if bc.cfg.StateSizeTracking { - stateSizer, err := state.NewSizeTracker(bc.db, bc.triedb) - if err == nil { - bc.stateSizer = stateSizer - log.Info("Enabled state size metrics") - } else { - log.Info("Failed to setup size tracker", "err", err) - } - } return bc, nil } @@ -1355,10 +1340,6 @@ func (bc *BlockChain) stopWithoutSaving() { // Signal shutdown to all goroutines. bc.InterruptInsert(true) - // Stop state size tracker - if bc.stateSizer != nil { - bc.stateSizer.Stop() - } // Now wait for all chain modifications to end and persistent goroutines to exit. // // Note: Close waits for the mutex to become available, i.e. any running chain @@ -1682,28 +1663,22 @@ func (bc *BlockChain) writeBlockWithState(block *types.Block, receipts []*types. log.Debug("Committed block data", "size", common.StorageSize(batch.ValueSize()), "elapsed", common.PrettyDuration(time.Since(start))) var ( - err error - root common.Hash - isEIP158 = bc.chainConfig.IsEIP158(block.Number()) - isCancun = bc.chainConfig.IsCancun(block.Number(), block.Time()) - hasStateHook = bc.logger != nil && bc.logger.OnStateUpdate != nil - hasStateSizer = bc.stateSizer != nil + err error + root common.Hash + isEIP158 = bc.chainConfig.IsEIP158(block.Number()) + isCancun = bc.chainConfig.IsCancun(block.Number(), block.Time()) + hasStateHook = bc.logger != nil && bc.logger.OnStateUpdate != nil ) - if hasStateHook || hasStateSizer { + if hasStateHook { r, update, err := statedb.CommitWithUpdate(block.NumberU64(), isEIP158, isCancun) if err != nil { return err } - if hasStateHook { - trUpdate, err := update.ToTracingUpdate() - if err != nil { - return err - } - bc.logger.OnStateUpdate(trUpdate) - } - if hasStateSizer { - bc.stateSizer.Notify(update) + trUpdate, err := update.ToTracingUpdate() + if err != nil { + return err } + bc.logger.OnStateUpdate(trUpdate) root = r } else { root, err = statedb.Commit(block.NumberU64(), isEIP158, isCancun) @@ -3062,8 +3037,3 @@ func (bc *BlockChain) SetTrieFlushInterval(interval time.Duration) { func (bc *BlockChain) GetTrieFlushInterval() time.Duration { return time.Duration(bc.flushInterval.Load()) } - -// StateSizer returns the state size tracker, or nil if it's not initialized -func (bc *BlockChain) StateSizer() *state.SizeTracker { - return bc.stateSizer -} diff --git a/core/state/state_sizer.go b/core/state/state_sizer.go deleted file mode 100644 index 3293d7e950..0000000000 --- a/core/state/state_sizer.go +++ /dev/null @@ -1,675 +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 state - -import ( - "container/heap" - "errors" - "fmt" - "maps" - "runtime" - "slices" - "sync" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/rawdb" - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/ethdb" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/metrics" - "github.com/ethereum/go-ethereum/triedb" - "golang.org/x/sync/errgroup" -) - -const ( - statEvictThreshold = 128 // the depth of statistic to be preserved -) - -// Database key scheme for states. -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) -) - -// State size metrics -var ( - stateSizeChainHeightGauge = metrics.NewRegisteredGauge("state/height", nil) - stateSizeAccountsCountGauge = metrics.NewRegisteredGauge("state/accounts/count", nil) - stateSizeAccountsBytesGauge = metrics.NewRegisteredGauge("state/accounts/bytes", nil) - stateSizeStoragesCountGauge = metrics.NewRegisteredGauge("state/storages/count", nil) - stateSizeStoragesBytesGauge = metrics.NewRegisteredGauge("state/storages/bytes", nil) - stateSizeAccountTrieNodesCountGauge = metrics.NewRegisteredGauge("state/trienodes/account/count", nil) - stateSizeAccountTrieNodesBytesGauge = metrics.NewRegisteredGauge("state/trienodes/account/bytes", nil) - stateSizeStorageTrieNodesCountGauge = metrics.NewRegisteredGauge("state/trienodes/storage/count", nil) - stateSizeStorageTrieNodesBytesGauge = metrics.NewRegisteredGauge("state/trienodes/storage/bytes", nil) - stateSizeContractsCountGauge = metrics.NewRegisteredGauge("state/contracts/count", nil) - stateSizeContractsBytesGauge = metrics.NewRegisteredGauge("state/contracts/bytes", nil) -) - -// SizeStats represents either the current state size statistics or the size -// differences resulting from a state transition. -type SizeStats struct { - StateRoot common.Hash // State root hash at the time of measurement - BlockNumber uint64 // Associated block number at the time of measurement - - Accounts int64 // Total number of accounts in the state - AccountBytes int64 // Total storage size used by all account data (in bytes) - Storages int64 // Total number of storage slots across all accounts - StorageBytes int64 // Total storage size used by all storage slot data (in bytes) - AccountTrienodes int64 // Total number of account trie nodes in the state - AccountTrienodeBytes int64 // Total storage size occupied by account trie nodes (in bytes) - StorageTrienodes int64 // Total number of storage trie nodes in the state - StorageTrienodeBytes int64 // Total storage size occupied by storage trie nodes (in bytes) - ContractCodes int64 // Total number of contract codes in the state - ContractCodeBytes int64 // Total size of all contract code (in bytes) -} - -func (s SizeStats) String() string { - return fmt.Sprintf("Accounts: %d(%s), Storages: %d(%s), AccountTrienodes: %d(%s), StorageTrienodes: %d(%s), Codes: %d(%s)", - s.Accounts, common.StorageSize(s.AccountBytes), - s.Storages, common.StorageSize(s.StorageBytes), - s.AccountTrienodes, common.StorageSize(s.AccountTrienodeBytes), - s.StorageTrienodes, common.StorageSize(s.StorageTrienodeBytes), - s.ContractCodes, common.StorageSize(s.ContractCodeBytes), - ) -} - -func (s SizeStats) publish() { - stateSizeChainHeightGauge.Update(int64(s.BlockNumber)) - stateSizeAccountsCountGauge.Update(s.Accounts) - stateSizeAccountsBytesGauge.Update(s.AccountBytes) - stateSizeStoragesCountGauge.Update(s.Storages) - stateSizeStoragesBytesGauge.Update(s.StorageBytes) - stateSizeAccountTrieNodesCountGauge.Update(s.AccountTrienodes) - stateSizeAccountTrieNodesBytesGauge.Update(s.AccountTrienodeBytes) - stateSizeStorageTrieNodesCountGauge.Update(s.StorageTrienodes) - stateSizeStorageTrieNodesBytesGauge.Update(s.StorageTrienodeBytes) - stateSizeContractsCountGauge.Update(s.ContractCodes) - stateSizeContractsBytesGauge.Update(s.ContractCodeBytes) -} - -// add applies the given state diffs and produces a new version of the statistics. -func (s SizeStats) add(diff SizeStats) SizeStats { - s.StateRoot = diff.StateRoot - s.BlockNumber = diff.BlockNumber - - s.Accounts += diff.Accounts - s.AccountBytes += diff.AccountBytes - s.Storages += diff.Storages - s.StorageBytes += diff.StorageBytes - s.AccountTrienodes += diff.AccountTrienodes - s.AccountTrienodeBytes += diff.AccountTrienodeBytes - s.StorageTrienodes += diff.StorageTrienodes - s.StorageTrienodeBytes += diff.StorageTrienodeBytes - s.ContractCodes += diff.ContractCodes - s.ContractCodeBytes += diff.ContractCodeBytes - return s -} - -// calSizeStats measures the state size changes of the provided state update. -func calSizeStats(update *StateUpdate) (SizeStats, error) { - stats := SizeStats{ - BlockNumber: update.BlockNumber, - StateRoot: update.Root, - } - accounts, accountOrigin, storages, storageOrigin := update.EncodeMPTState() - - // Measure the account changes - for addr, oldValue := range accountOrigin { - addrHash := crypto.Keccak256Hash(addr.Bytes()) - newValue, exists := accounts[addrHash] - if !exists { - return SizeStats{}, fmt.Errorf("account %x not found", addr) - } - oldLen, newLen := len(oldValue), len(newValue) - - switch { - case oldLen > 0 && newLen == 0: - // Account deletion - stats.Accounts -= 1 - stats.AccountBytes -= accountKeySize + int64(oldLen) - case oldLen == 0 && newLen > 0: - // Account creation - stats.Accounts += 1 - stats.AccountBytes += accountKeySize + int64(newLen) - default: - // Account update - stats.AccountBytes += int64(newLen - oldLen) - } - } - - // Measure storage changes - for addr, slots := range storageOrigin { - addrHash := crypto.Keccak256Hash(addr.Bytes()) - subset, exists := storages[addrHash] - if !exists { - return SizeStats{}, fmt.Errorf("storage %x not found", addr) - } - for key, oldValue := range slots { - var ( - exists bool - newValue []byte - ) - if update.StorageKeyType == StorageKeyPlain { - newValue, exists = subset[crypto.Keccak256Hash(key.Bytes())] - } else { - newValue, exists = subset[key] - } - if !exists { - return SizeStats{}, fmt.Errorf("storage slot %x-%x not found", addr, key) - } - oldLen, newLen := len(oldValue), len(newValue) - - switch { - case oldLen > 0 && newLen == 0: - // Storage deletion - stats.Storages -= 1 - stats.StorageBytes -= storageKeySize + int64(oldLen) - case oldLen == 0 && newLen > 0: - // Storage creation - stats.Storages += 1 - stats.StorageBytes += storageKeySize + int64(newLen) - default: - // Storage update - stats.StorageBytes += int64(newLen - oldLen) - } - } - } - - // Measure trienode changes - for owner, subset := range update.Nodes.Sets { - var ( - keyPrefix int64 - isAccount = owner == (common.Hash{}) - ) - if isAccount { - keyPrefix = accountTrienodePrefixSize - } else { - keyPrefix = storageTrienodePrefixSize - } - - // Iterate over Origins since every modified node has an origin entry - for path, oldNode := range subset.Origins { - newNode, exists := subset.Nodes[path] - if !exists { - return SizeStats{}, fmt.Errorf("node %x-%v not found", owner, path) - } - keySize := keyPrefix + int64(len(path)) - - switch { - case len(oldNode) > 0 && len(newNode.Blob) == 0: - // Node deletion - if isAccount { - stats.AccountTrienodes -= 1 - stats.AccountTrienodeBytes -= keySize + int64(len(oldNode)) - } else { - stats.StorageTrienodes -= 1 - stats.StorageTrienodeBytes -= keySize + int64(len(oldNode)) - } - case len(oldNode) == 0 && len(newNode.Blob) > 0: - // Node creation - if isAccount { - stats.AccountTrienodes += 1 - stats.AccountTrienodeBytes += keySize + int64(len(newNode.Blob)) - } else { - stats.StorageTrienodes += 1 - stats.StorageTrienodeBytes += keySize + int64(len(newNode.Blob)) - } - default: - // Node update - if isAccount { - stats.AccountTrienodeBytes += int64(len(newNode.Blob) - len(oldNode)) - } else { - stats.StorageTrienodeBytes += int64(len(newNode.Blob) - len(oldNode)) - } - } - } - } - - codeExists := make(map[common.Hash]struct{}) - for _, code := range update.Codes { - if _, ok := codeExists[code.Hash]; ok || code.Duplicate { - continue - } - stats.ContractCodes += 1 - stats.ContractCodeBytes += codeKeySize + int64(len(code.Blob)) - codeExists[code.Hash] = struct{}{} - } - return stats, nil -} - -type stateSizeQuery struct { - root *common.Hash // nil means latest - err error // non-nil if the state size is not yet initialized - result chan *SizeStats // nil means the state is unknown -} - -// SizeTracker handles the state size initialization and tracks of state size metrics. -type SizeTracker struct { - db ethdb.KeyValueStore - triedb *triedb.Database - abort chan struct{} - aborted chan struct{} - updateCh chan *StateUpdate - queryCh chan *stateSizeQuery -} - -// NewSizeTracker creates a new state size tracker and starts it automatically -func NewSizeTracker(db ethdb.KeyValueStore, triedb *triedb.Database) (*SizeTracker, error) { - if triedb.Scheme() != rawdb.PathScheme { - return nil, errors.New("state size tracker is not compatible with hash mode") - } - t := &SizeTracker{ - db: db, - triedb: triedb, - abort: make(chan struct{}), - aborted: make(chan struct{}), - updateCh: make(chan *StateUpdate), - queryCh: make(chan *stateSizeQuery), - } - go t.run() - return t, nil -} - -func (t *SizeTracker) Stop() { - close(t.abort) - <-t.aborted -} - -// sizeStatsHeap is a heap.Interface implementation over statesize statistics for -// retrieving the oldest statistics for eviction. -type sizeStatsHeap []SizeStats - -func (h sizeStatsHeap) Len() int { return len(h) } -func (h sizeStatsHeap) Less(i, j int) bool { return h[i].BlockNumber < h[j].BlockNumber } -func (h sizeStatsHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] } - -func (h *sizeStatsHeap) Push(x any) { - *h = append(*h, x.(SizeStats)) -} - -func (h *sizeStatsHeap) Pop() any { - old := *h - n := len(old) - x := old[n-1] - *h = old[0 : n-1] - return x -} - -// run performs the state size initialization and handles updates -func (t *SizeTracker) run() { - defer close(t.aborted) - - var last common.Hash - stats, err := t.init() // launch background thread for state size init - if err != nil { - return - } - h := sizeStatsHeap(slices.Collect(maps.Values(stats))) - heap.Init(&h) - - for { - select { - case u := <-t.updateCh: - base, found := stats[u.OriginRoot] - if !found { - log.Debug("Ignored the state size without parent", "parent", u.OriginRoot, "root", u.Root, "number", u.BlockNumber) - continue - } - diff, err := calSizeStats(u) - if err != nil { - continue - } - stat := base.add(diff) - stats[u.Root] = stat - last = u.Root - - // Publish statistics to metric system - stat.publish() - - // Evict the stale statistics - heap.Push(&h, stats[u.Root]) - for len(h) > 0 && u.BlockNumber-h[0].BlockNumber > statEvictThreshold { - delete(stats, h[0].StateRoot) - heap.Pop(&h) - } - log.Debug("Update state size", "number", stat.BlockNumber, "root", stat.StateRoot, "stat", stat) - - case r := <-t.queryCh: - var root common.Hash - if r.root != nil { - root = *r.root - } else { - root = last - } - if s, ok := stats[root]; ok { - r.result <- &s - } else { - r.result <- nil - } - - case <-t.abort: - return - } - } -} - -type buildResult struct { - stat SizeStats - root common.Hash - blockNumber uint64 - elapsed time.Duration - err error -} - -func (t *SizeTracker) init() (map[common.Hash]SizeStats, error) { - // Wait for snapshot completion and then init - ticker := time.NewTicker(10 * time.Second) - defer ticker.Stop() - -wait: - for { - select { - case <-ticker.C: - if t.triedb.SnapshotCompleted() { - break wait - } - case <-t.updateCh: - continue - case r := <-t.queryCh: - r.err = errors.New("state size is not initialized yet") - r.result <- nil - case <-t.abort: - return nil, errors.New("size tracker closed") - } - } - - var ( - updates = make(map[common.Hash]*StateUpdate) - children = make(map[common.Hash][]common.Hash) - done chan buildResult - ) - - for { - select { - case u := <-t.updateCh: - updates[u.Root] = u - children[u.OriginRoot] = append(children[u.OriginRoot], u.Root) - log.Debug("Received state update", "root", u.Root, "blockNumber", u.BlockNumber) - - case r := <-t.queryCh: - r.err = errors.New("state size is not initialized yet") - r.result <- nil - - case <-ticker.C: - // Only check timer if build hasn't started yet - if done != nil { - continue - } - root := rawdb.ReadSnapshotRoot(t.db) - if root == (common.Hash{}) { - continue - } - entry, exists := updates[root] - if !exists { - continue - } - done = make(chan buildResult) - go t.build(entry.Root, entry.BlockNumber, done) - log.Info("Measuring persistent state size", "root", root.Hex(), "number", entry.BlockNumber) - - case result := <-done: - if result.err != nil { - return nil, result.err - } - var ( - stats = make(map[common.Hash]SizeStats) - apply func(root common.Hash, stat SizeStats) error - ) - apply = func(root common.Hash, base SizeStats) error { - for _, child := range children[root] { - entry, ok := updates[child] - if !ok { - return fmt.Errorf("the state update is not found, %x", child) - } - diff, err := calSizeStats(entry) - if err != nil { - return err - } - stats[child] = base.add(diff) - if err := apply(child, stats[child]); err != nil { - return err - } - } - return nil - } - if err := apply(result.root, result.stat); err != nil { - return nil, err - } - - // Set initial latest stats - stats[result.root] = result.stat - log.Info("Measured persistent state size", "root", result.root, "number", result.blockNumber, "stat", result.stat, "elapsed", common.PrettyDuration(result.elapsed)) - return stats, nil - - case <-t.abort: - return nil, errors.New("size tracker closed") - } - } -} - -func (t *SizeTracker) build(root common.Hash, blockNumber uint64, done chan buildResult) { - // Metrics will be directly updated by each goroutine - var ( - accounts, accountBytes int64 - storages, storageBytes int64 - codes, codeBytes int64 - - accountTrienodes, accountTrienodeBytes int64 - storageTrienodes, storageTrienodeBytes int64 - - group errgroup.Group - start = time.Now() - ) - - // Start all table iterations concurrently with direct metric updates - group.Go(func() error { - count, bytes, err := t.iterateTableParallel(t.abort, rawdb.SnapshotAccountPrefix, "account") - if err != nil { - return err - } - accounts, accountBytes = count, bytes - return nil - }) - - group.Go(func() error { - count, bytes, err := t.iterateTableParallel(t.abort, rawdb.SnapshotStoragePrefix, "storage") - if err != nil { - return err - } - storages, storageBytes = count, bytes - return nil - }) - - group.Go(func() error { - count, bytes, err := t.iterateTableParallel(t.abort, rawdb.TrieNodeAccountPrefix, "accountnode") - if err != nil { - return err - } - accountTrienodes, accountTrienodeBytes = count, bytes - return nil - }) - - group.Go(func() error { - count, bytes, err := t.iterateTableParallel(t.abort, rawdb.TrieNodeStoragePrefix, "storagenode") - if err != nil { - return err - } - storageTrienodes, storageTrienodeBytes = count, bytes - return nil - }) - - group.Go(func() error { - count, bytes, err := t.iterateTable(t.abort, rawdb.CodePrefix, "contractcode") - if err != nil { - return err - } - codes, codeBytes = count, bytes - return nil - }) - - // Wait for all goroutines to complete - if err := group.Wait(); err != nil { - done <- buildResult{err: err} - } else { - stat := SizeStats{ - StateRoot: root, - BlockNumber: blockNumber, - Accounts: accounts, - AccountBytes: accountBytes, - Storages: storages, - StorageBytes: storageBytes, - AccountTrienodes: accountTrienodes, - AccountTrienodeBytes: accountTrienodeBytes, - StorageTrienodes: storageTrienodes, - StorageTrienodeBytes: storageTrienodeBytes, - ContractCodes: codes, - ContractCodeBytes: codeBytes, - } - done <- buildResult{ - root: root, - blockNumber: blockNumber, - stat: stat, - elapsed: time.Since(start), - } - } -} - -// iterateTable performs iteration over a specific table and returns the results. -func (t *SizeTracker) iterateTable(closed chan struct{}, prefix []byte, name string) (int64, int64, error) { - var ( - start = time.Now() - logged = time.Now() - count, bytes int64 - ) - - iter := t.db.NewIterator(prefix, nil) - defer iter.Release() - - log.Debug("Iterating state", "category", name) - for iter.Next() { - count++ - bytes += int64(len(iter.Key()) + len(iter.Value())) - - if time.Since(logged) > time.Second*8 { - logged = time.Now() - - select { - case <-closed: - log.Debug("State iteration cancelled", "category", name) - return 0, 0, errors.New("size tracker closed") - default: - log.Debug("Iterating state", "category", name, "count", count, "size", common.StorageSize(bytes)) - } - } - } - // Check for iterator errors - if err := iter.Error(); err != nil { - log.Error("Iterator error", "category", name, "err", err) - return 0, 0, err - } - log.Debug("Finished state iteration", "category", name, "count", count, "size", common.StorageSize(bytes), "elapsed", common.PrettyDuration(time.Since(start))) - return count, bytes, nil -} - -// iterateTableParallel performs parallel iteration over a table by splitting into -// hex ranges. For storage tables, it splits on the first byte of the account hash -// (after the prefix). -func (t *SizeTracker) iterateTableParallel(closed chan struct{}, prefix []byte, name string) (int64, int64, error) { - var ( - totalCount int64 - totalBytes int64 - - start = time.Now() - workers = runtime.NumCPU() - group errgroup.Group - mu sync.Mutex - ) - group.SetLimit(workers) - log.Debug("Starting parallel state iteration", "category", name, "workers", workers) - - if len(prefix) > 0 { - if blob, err := t.db.Get(prefix); err == nil && len(blob) > 0 { - // If there's a direct hit on the prefix, include it in the stats - totalCount = 1 - totalBytes = int64(len(prefix) + len(blob)) - } - } - for i := 0; i < 256; i++ { - h := byte(i) - group.Go(func() error { - count, bytes, err := t.iterateTable(closed, slices.Concat(prefix, []byte{h}), fmt.Sprintf("%s-%02x", name, h)) - if err != nil { - return err - } - mu.Lock() - totalCount += count - totalBytes += bytes - mu.Unlock() - return nil - }) - } - if err := group.Wait(); err != nil { - return 0, 0, err - } - log.Debug("Finished parallel state iteration", "category", name, "count", totalCount, "size", common.StorageSize(totalBytes), "elapsed", common.PrettyDuration(time.Since(start))) - return totalCount, totalBytes, nil -} - -// Notify is an async method used to send the state update to the size tracker. -// It ignores empty updates (where no state changes occurred). -// If the channel is full, it drops the update to avoid blocking. -func (t *SizeTracker) Notify(update *StateUpdate) { - if update == nil || update.Empty() { - return - } - select { - case t.updateCh <- update: - case <-t.abort: - return - } -} - -// Query returns the state size specified by the root, or nil if not available. -// If the root is nil, query the size of latest chain head; -// If the root is non-nil, query the size of the specified state; -func (t *SizeTracker) Query(root *common.Hash) (*SizeStats, error) { - r := &stateSizeQuery{ - root: root, - result: make(chan *SizeStats, 1), - } - select { - case <-t.aborted: - return nil, errors.New("state sizer has been closed") - case t.queryCh <- r: - return <-r.result, r.err - } -} diff --git a/core/state/state_sizer_test.go b/core/state/state_sizer_test.go deleted file mode 100644 index 539f160985..0000000000 --- a/core/state/state_sizer_test.go +++ /dev/null @@ -1,239 +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 state - -import ( - "testing" - "time" - - "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/triedb" - "github.com/ethereum/go-ethereum/triedb/pathdb" - "github.com/holiman/uint256" -) - -func TestSizeTracker(t *testing.T) { - db := rawdb.NewMemoryDatabase() - defer db.Close() - - tdb := triedb.NewDatabase(db, &triedb.Config{PathDB: pathdb.Defaults}) - sdb := NewDatabase(tdb, nil) - - // Generate 50 blocks to establish a baseline - baselineBlockNum := uint64(50) - currentRoot := types.EmptyRootHash - - addr1 := common.BytesToAddress([]byte{1, 0, 0, 1}) - addr2 := common.BytesToAddress([]byte{1, 0, 0, 2}) - addr3 := common.BytesToAddress([]byte{1, 0, 0, 3}) - - // Create initial state with fixed accounts - state, _ := New(currentRoot, sdb) - state.AddBalance(addr1, uint256.NewInt(1000), tracing.BalanceChangeUnspecified) - state.SetNonce(addr1, 1, tracing.NonceChangeUnspecified) - state.SetState(addr1, common.HexToHash("0x1111"), common.HexToHash("0xaaaa")) - state.SetState(addr1, common.HexToHash("0x2222"), common.HexToHash("0xbbbb")) - - state.AddBalance(addr2, uint256.NewInt(2000), tracing.BalanceChangeUnspecified) - state.SetNonce(addr2, 2, tracing.NonceChangeUnspecified) - state.SetCode(addr2, []byte{0x60, 0x80, 0x60, 0x40, 0x52}, tracing.CodeChangeUnspecified) - - state.AddBalance(addr3, uint256.NewInt(3000), tracing.BalanceChangeUnspecified) - state.SetNonce(addr3, 3, tracing.NonceChangeUnspecified) - - currentRoot, err := state.Commit(1, true, false) - if err != nil { - t.Fatalf("Failed to commit initial state: %v", err) - } - if err := tdb.Commit(currentRoot, false); err != nil { - t.Fatalf("Failed to commit initial trie: %v", err) - } - - for i := 1; i < 50; i++ { // blocks 2-50 - blockNum := uint64(i + 1) - - newState, err := New(currentRoot, sdb) - if err != nil { - t.Fatalf("Failed to create new state at block %d: %v", blockNum, err) - } - testAddr := common.BigToAddress(uint256.NewInt(uint64(i + 100)).ToBig()) - newState.AddBalance(testAddr, uint256.NewInt(uint64((i+1)*1000)), tracing.BalanceChangeUnspecified) - newState.SetNonce(testAddr, uint64(i+10), tracing.NonceChangeUnspecified) - - if i%2 == 0 { - newState.SetState(addr1, common.BigToHash(uint256.NewInt(uint64(i+0x1000)).ToBig()), common.BigToHash(uint256.NewInt(uint64(i+0x2000)).ToBig())) - } - if i%3 == 0 { - newState.SetCode(testAddr, []byte{byte(i), 0x60, 0x80, byte(i + 1), 0x52}, tracing.CodeChangeUnspecified) - } - root, err := newState.Commit(blockNum, true, false) - if err != nil { - t.Fatalf("Failed to commit state at block %d: %v", blockNum, err) - } - if err := tdb.Commit(root, false); err != nil { - t.Fatalf("Failed to commit trie at block %d: %v", blockNum, err) - } - currentRoot = root - } - baselineRoot := currentRoot - - // Close and reopen the trie database so all async flushes triggered by the - // baseline commits are written before we measure the baseline snapshot. - if err := tdb.Close(); err != nil { - t.Fatalf("Failed to close triedb before baseline measurement: %v", err) - } - tdb = triedb.NewDatabase(db, &triedb.Config{PathDB: pathdb.Defaults}) - sdb = NewDatabase(tdb, nil) - - // Wait for snapshot completion - for !tdb.SnapshotCompleted() { - time.Sleep(100 * time.Millisecond) - } - - // Calculate baseline from the intermediate persisted state - baselineTracker := &SizeTracker{ - db: db, - triedb: tdb, - abort: make(chan struct{}), - } - done := make(chan buildResult) - - go baselineTracker.build(baselineRoot, baselineBlockNum, done) - var baselineResult buildResult - select { - case baselineResult = <-done: - if baselineResult.err != nil { - t.Fatalf("Failed to get baseline stats: %v", baselineResult.err) - } - case <-time.After(30 * time.Second): - t.Fatal("Timeout waiting for baseline stats") - } - baseline := baselineResult.stat - - // Now start the tracker and notify it of updates that happen AFTER the baseline - tracker, err := NewSizeTracker(db, tdb) - if err != nil { - t.Fatalf("Failed to create size tracker: %v", err) - } - defer tracker.Stop() - - var trackedUpdates []SizeStats - currentRoot = baselineRoot - - // Generate additional blocks beyond the baseline and track them - for i := 49; i < 130; i++ { // blocks 51-132 - blockNum := uint64(i + 2) - newState, err := New(currentRoot, sdb) - if err != nil { - t.Fatalf("Failed to create new state at block %d: %v", blockNum, err) - } - testAddr := common.BigToAddress(uint256.NewInt(uint64(i + 100)).ToBig()) - newState.AddBalance(testAddr, uint256.NewInt(uint64((i+1)*1000)), tracing.BalanceChangeUnspecified) - newState.SetNonce(testAddr, uint64(i+10), tracing.NonceChangeUnspecified) - - if i%2 == 0 { - newState.SetState(addr1, common.BigToHash(uint256.NewInt(uint64(i+0x1000)).ToBig()), common.BigToHash(uint256.NewInt(uint64(i+0x2000)).ToBig())) - } - if i%3 == 0 { - newState.SetCode(testAddr, []byte{byte(i), 0x60, 0x80, byte(i + 1), 0x52}, tracing.CodeChangeUnspecified) - } - ret, err := newState.commitAndFlush(blockNum, true, false, true) - if err != nil { - t.Fatalf("Failed to commit state at block %d: %v", blockNum, err) - } - tracker.Notify(ret) - - if err := tdb.Commit(ret.Root, false); err != nil { - t.Fatalf("Failed to commit trie at block %d: %v", blockNum, err) - } - - diff, err := calSizeStats(ret) - if err != nil { - t.Fatalf("Failed to calculate size stats for block %d: %v", blockNum, err) - } - trackedUpdates = append(trackedUpdates, diff) - currentRoot = ret.Root - } - finalRoot := rawdb.ReadSnapshotRoot(db) - - // Ensure all commits are flushed to disk - if err := tdb.Close(); err != nil { - t.Fatalf("Failed to close triedb: %v", err) - } - // Reopen the database to simulate a restart - tdb = triedb.NewDatabase(db, &triedb.Config{PathDB: pathdb.Defaults}) - defer tdb.Close() - - finalTracker := &SizeTracker{ - db: db, - triedb: tdb, - abort: make(chan struct{}), - } - finalDone := make(chan buildResult) - - go finalTracker.build(finalRoot, uint64(132), finalDone) - var result buildResult - select { - case result = <-finalDone: - if result.err != nil { - t.Fatalf("Failed to build final stats: %v", result.err) - } - case <-time.After(30 * time.Second): - t.Fatal("Timeout waiting for final stats") - } - actualStats := result.stat - - expectedStats := baseline - for _, diff := range trackedUpdates { - expectedStats = expectedStats.add(diff) - } - - // The final measured stats should match our calculated expected stats exactly - if actualStats.Accounts != expectedStats.Accounts { - t.Errorf("Account count mismatch: baseline(%d) + tracked_changes = %d, but final_measurement = %d", baseline.Accounts, expectedStats.Accounts, actualStats.Accounts) - } - if actualStats.AccountBytes != expectedStats.AccountBytes { - t.Errorf("Account bytes mismatch: expected %d, got %d", expectedStats.AccountBytes, actualStats.AccountBytes) - } - if actualStats.Storages != expectedStats.Storages { - t.Errorf("Storage count mismatch: baseline(%d) + tracked_changes = %d, but final_measurement = %d", baseline.Storages, expectedStats.Storages, actualStats.Storages) - } - if actualStats.StorageBytes != expectedStats.StorageBytes { - t.Errorf("Storage bytes mismatch: expected %d, got %d", expectedStats.StorageBytes, actualStats.StorageBytes) - } - if actualStats.ContractCodes != expectedStats.ContractCodes { - t.Errorf("Contract code count mismatch: baseline(%d) + tracked_changes = %d, but final_measurement = %d", baseline.ContractCodes, expectedStats.ContractCodes, actualStats.ContractCodes) - } - if actualStats.ContractCodeBytes != expectedStats.ContractCodeBytes { - t.Errorf("Contract code bytes mismatch: expected %d, got %d", expectedStats.ContractCodeBytes, actualStats.ContractCodeBytes) - } - if actualStats.AccountTrienodes != expectedStats.AccountTrienodes { - t.Errorf("Account trie nodes mismatch: expected %d, got %d", expectedStats.AccountTrienodes, actualStats.AccountTrienodes) - } - if actualStats.AccountTrienodeBytes != expectedStats.AccountTrienodeBytes { - t.Errorf("Account trie node bytes mismatch: expected %d, got %d", expectedStats.AccountTrienodeBytes, actualStats.AccountTrienodeBytes) - } - if actualStats.StorageTrienodes != expectedStats.StorageTrienodes { - t.Errorf("Storage trie nodes mismatch: expected %d, got %d", expectedStats.StorageTrienodes, actualStats.StorageTrienodes) - } - if actualStats.StorageTrienodeBytes != expectedStats.StorageTrienodeBytes { - t.Errorf("Storage trie node bytes mismatch: expected %d, got %d", expectedStats.StorageTrienodeBytes, actualStats.StorageTrienodeBytes) - } -} diff --git a/core/state/statedb.go b/core/state/statedb.go index 3994489833..3cc67cebee 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -1459,7 +1459,7 @@ func (s *StateDB) Commit(block uint64, deleteEmptyObjects bool, noStorageWiping } // CommitWithUpdate writes the state mutations and returns the state update for -// external processing (e.g., live tracing hooks or size tracker). +// external processing (e.g., live tracing hooks). func (s *StateDB) CommitWithUpdate(block uint64, deleteEmptyObjects bool, noStorageWiping bool) (common.Hash, *StateUpdate, error) { ret, err := s.commitAndFlush(block, deleteEmptyObjects, noStorageWiping, true) if err != nil { diff --git a/eth/api_debug.go b/eth/api_debug.go index 7d512d9d57..fa211ec1ff 100644 --- a/eth/api_debug.go +++ b/eth/api_debug.go @@ -494,54 +494,6 @@ func (api *DebugAPI) GetTrieFlushInterval() (string, error) { return api.eth.blockchain.GetTrieFlushInterval().String(), nil } -// StateSize returns the current state size statistics from the state size tracker. -// Returns an error if the state size tracker is not initialized or if stats are not ready. -func (api *DebugAPI) StateSize(blockHashOrNumber *rpc.BlockNumberOrHash) (interface{}, error) { - sizer := api.eth.blockchain.StateSizer() - if sizer == nil { - return nil, errors.New("state size tracker is not enabled") - } - var ( - err error - stats *state.SizeStats - ) - if blockHashOrNumber == nil { - stats, err = sizer.Query(nil) - } else { - header, herr := api.eth.APIBackend.HeaderByNumberOrHash(context.Background(), *blockHashOrNumber) - if herr != nil || header == nil { - return nil, fmt.Errorf("block %s is unknown", blockHashOrNumber) - } - stats, err = sizer.Query(&header.Root) - } - if err != nil { - return nil, err - } - if stats == nil { - var s string - if blockHashOrNumber == nil { - s = "chain head" - } else { - s = blockHashOrNumber.String() - } - return nil, fmt.Errorf("state size of %s is not available", s) - } - return map[string]interface{}{ - "stateRoot": stats.StateRoot, - "blockNumber": hexutil.Uint64(stats.BlockNumber), - "accounts": hexutil.Uint64(stats.Accounts), - "accountBytes": hexutil.Uint64(stats.AccountBytes), - "storages": hexutil.Uint64(stats.Storages), - "storageBytes": hexutil.Uint64(stats.StorageBytes), - "accountTrienodes": hexutil.Uint64(stats.AccountTrienodes), - "accountTrienodeBytes": hexutil.Uint64(stats.AccountTrienodeBytes), - "storageTrienodes": hexutil.Uint64(stats.StorageTrienodes), - "storageTrienodeBytes": hexutil.Uint64(stats.StorageTrienodeBytes), - "contractCodes": hexutil.Uint64(stats.ContractCodes), - "contractCodeBytes": hexutil.Uint64(stats.ContractCodeBytes), - }, nil -} - func (api *DebugAPI) ExecutionWitness(bn rpc.BlockNumberOrHash) (*stateless.ExtWitness, error) { bc := api.eth.blockchain block, err := api.eth.APIBackend.BlockByNumberOrHash(context.Background(), bn) diff --git a/eth/backend.go b/eth/backend.go index ba9b4d4459..99a358af2a 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -258,7 +258,6 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { // - DATADIR/triedb/merkle.journal // - DATADIR/triedb/verkle.journal TrieJournalDirectory: stack.ResolvePath("triedb"), - StateSizeTracking: config.EnableStateSizeTracking, SlowBlockThreshold: config.SlowBlockThreshold, StatelessSelfValidation: config.StatelessSelfValidation, diff --git a/eth/ethconfig/config.go b/eth/ethconfig/config.go index 8e27a66419..e09f14499e 100644 --- a/eth/ethconfig/config.go +++ b/eth/ethconfig/config.go @@ -182,9 +182,6 @@ type Config struct { // Generate execution witnesses and self-check against them (testing purpose) StatelessSelfValidation bool - // Enables tracking of state size - EnableStateSizeTracking bool - // SnapV2 enables the experimental snap/2 (EIP-8189, BAL-based) sync protocol: // the node advertises snap/2 on the wire and uses the snap/2 state syncer. // It is not safe to enable on public networks yet. diff --git a/eth/ethconfig/gen_config.go b/eth/ethconfig/gen_config.go index 4e61b1cccb..2787b7f354 100644 --- a/eth/ethconfig/gen_config.go +++ b/eth/ethconfig/gen_config.go @@ -56,7 +56,6 @@ func (c Config) MarshalTOML() (interface{}, error) { EnablePreimageRecording bool EnableWitnessStats bool StatelessSelfValidation bool - EnableStateSizeTracking bool SnapV2 bool VMTrace string VMTraceJsonConfig string @@ -113,7 +112,6 @@ func (c Config) MarshalTOML() (interface{}, error) { enc.EnablePreimageRecording = c.EnablePreimageRecording enc.EnableWitnessStats = c.EnableWitnessStats enc.StatelessSelfValidation = c.StatelessSelfValidation - enc.EnableStateSizeTracking = c.EnableStateSizeTracking enc.SnapV2 = c.SnapV2 enc.VMTrace = c.VMTrace enc.VMTraceJsonConfig = c.VMTraceJsonConfig @@ -174,7 +172,6 @@ func (c *Config) UnmarshalTOML(unmarshal func(interface{}) error) error { EnablePreimageRecording *bool EnableWitnessStats *bool StatelessSelfValidation *bool - EnableStateSizeTracking *bool SnapV2 *bool VMTrace *string VMTraceJsonConfig *string @@ -312,9 +309,6 @@ func (c *Config) UnmarshalTOML(unmarshal func(interface{}) error) error { if dec.StatelessSelfValidation != nil { c.StatelessSelfValidation = *dec.StatelessSelfValidation } - if dec.EnableStateSizeTracking != nil { - c.EnableStateSizeTracking = *dec.EnableStateSizeTracking - } if dec.SnapV2 != nil { c.SnapV2 = *dec.SnapV2 } diff --git a/internal/web3ext/web3ext.go b/internal/web3ext/web3ext.go index 85138e9a2c..92e2ec3512 100644 --- a/internal/web3ext/web3ext.go +++ b/internal/web3ext/web3ext.go @@ -475,12 +475,6 @@ web3._extend({ call: 'debug_sync', params: 1 }), - new web3._extend.Method({ - name: 'stateSize', - call: 'debug_stateSize', - params: 1, - inputFormatter: [null], - }), new web3._extend.Method({ name: 'executionWitness', call: 'debug_executionWitness',