From abbe71bb6c8fe3597f9099b69771f4341ea1b5de Mon Sep 17 00:00:00 2001 From: weiihann Date: Wed, 24 Dec 2025 15:08:57 +0800 Subject: [PATCH] core/state, core/tracing: new state update hook add statesize tracer create file debug log deal with genesis debug logs fix storage bytes fix storage fix storage 2 Revert "fix storage 2" This reverts commit cfbefaa84200849024966d1e24055f2b5221801e. Revert "fix storage" This reverts commit 62a4189f70e1f5f9c29e4c769a5fe5d8511d40b1. Revert "fix storage bytes" This reverts commit 1b42707dcd34fc9d9520d04cca3bd85ee3df5fc3. fix storage 3 remove debug logs remove lint lint chore revert lint fix lint fix nil add depth count debug depth created and deleted nodes --- cmd/geth/chaincmd.go | 2 +- core/blockchain.go | 32 +- core/chain_makers.go | 2 +- core/genesis.go | 38 +- core/genesis_test.go | 10 +- core/headerchain_test.go | 2 +- core/state/statedb.go | 10 +- core/state/stateupdate.go | 133 ++++++ core/tracing/hooks.go | 52 +++ eth/filters/filter_system_test.go | 2 +- eth/filters/filter_test.go | 4 +- eth/tracers/live/statesize.go | 715 +++++++++++++++++++++++++++++ eth/tracers/live/statesize_test.go | 244 ++++++++++ tests/block_test_util.go | 2 +- 14 files changed, 1212 insertions(+), 36 deletions(-) create mode 100644 eth/tracers/live/statesize.go create mode 100644 eth/tracers/live/statesize_test.go diff --git a/cmd/geth/chaincmd.go b/cmd/geth/chaincmd.go index e535d7d892..55316c14ab 100644 --- a/cmd/geth/chaincmd.go +++ b/cmd/geth/chaincmd.go @@ -296,7 +296,7 @@ func initGenesis(ctx *cli.Context) error { triedb := utils.MakeTrieDatabase(ctx, stack, chaindb, ctx.Bool(utils.CachePreimagesFlag.Name), false, genesis.IsVerkle()) defer triedb.Close() - _, hash, compatErr, err := core.SetupGenesisBlockWithOverride(chaindb, triedb, genesis, &overrides) + _, hash, compatErr, err := core.SetupGenesisBlockWithOverride(chaindb, triedb, genesis, &overrides, nil) if err != nil { utils.Fatalf("Failed to write genesis block: %v", err) } diff --git a/core/blockchain.go b/core/blockchain.go index 858eceb630..144b59ef89 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -366,7 +366,7 @@ func NewBlockChain(db ethdb.Database, genesis *Genesis, engine consensus.Engine, // yet. The corresponding chain config will be returned, either from the // provided genesis or from the locally stored configuration if the genesis // has already been initialized. - chainConfig, genesisHash, compatErr, err := SetupGenesisBlockWithOverride(db, triedb, genesis, cfg.Overrides) + chainConfig, genesisHash, compatErr, err := SetupGenesisBlockWithOverride(db, triedb, genesis, cfg.Overrides, cfg.VmConfig.Tracer) if err != nil { return nil, err } @@ -1649,15 +1649,31 @@ 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()) + 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 + needStateUpdate = hasStateHook || hasStateSizer ) - if bc.stateSizer == nil { - root, err = statedb.Commit(block.NumberU64(), isEIP158, isCancun) + if needStateUpdate { + r, update, err := statedb.CommitWithUpdate(block.NumberU64(), isEIP158, isCancun) + if err == nil && update != nil { + if hasStateHook { + trUpdate, err := update.ToTracingUpdate() + if err != nil { + return err + } + bc.logger.OnStateUpdate(trUpdate) + } + if hasStateSizer { + bc.stateSizer.Notify(update) + } + } + root = r } else { - root, err = statedb.CommitAndTrack(block.NumberU64(), isEIP158, isCancun, bc.stateSizer) + root, err = statedb.Commit(block.NumberU64(), isEIP158, isCancun) } if err != nil { return err diff --git a/core/chain_makers.go b/core/chain_makers.go index a1e07becba..7ce86b14e9 100644 --- a/core/chain_makers.go +++ b/core/chain_makers.go @@ -481,7 +481,7 @@ func GenerateChainWithGenesis(genesis *Genesis, engine consensus.Engine, n int, } triedb := triedb.NewDatabase(db, triedbConfig) defer triedb.Close() - _, err := genesis.Commit(db, triedb) + _, err := genesis.Commit(db, triedb, nil) if err != nil { panic(err) } diff --git a/core/genesis.go b/core/genesis.go index 7d640c8cae..983ad4c3cb 100644 --- a/core/genesis.go +++ b/core/genesis.go @@ -164,7 +164,7 @@ func hashAlloc(ga *types.GenesisAlloc, isVerkle bool) (common.Hash, error) { // flushAlloc is very similar with hash, but the main difference is all the // generated states will be persisted into the given database. -func flushAlloc(ga *types.GenesisAlloc, triedb *triedb.Database) (common.Hash, error) { +func flushAlloc(ga *types.GenesisAlloc, triedb *triedb.Database, tracer *tracing.Hooks) (common.Hash, error) { emptyRoot := types.EmptyRootHash if triedb.IsVerkle() { emptyRoot = types.EmptyVerkleHash @@ -185,10 +185,26 @@ func flushAlloc(ga *types.GenesisAlloc, triedb *triedb.Database) (common.Hash, e statedb.SetState(addr, key, value) } } - root, err := statedb.Commit(0, false, false) - if err != nil { - return common.Hash{}, err + + var root common.Hash + if tracer != nil && tracer.OnStateUpdate != nil { + r, update, err := statedb.CommitWithUpdate(0, false, false) + if err != nil { + return common.Hash{}, err + } + trUpdate, err := update.ToTracingUpdate() + if err != nil { + return common.Hash{}, err + } + tracer.OnStateUpdate(trUpdate) + root = r + } else { + root, err = statedb.Commit(0, false, false) + if err != nil { + return common.Hash{}, err + } } + // Commit newly generated states into disk if it's not empty. if root != emptyRoot { if err := triedb.Commit(root, true); err != nil { @@ -296,10 +312,10 @@ func (o *ChainOverrides) apply(cfg *params.ChainConfig) error { // specify a fork block below the local head block). In case of a conflict, the // error is a *params.ConfigCompatError and the new, unwritten config is returned. func SetupGenesisBlock(db ethdb.Database, triedb *triedb.Database, genesis *Genesis) (*params.ChainConfig, common.Hash, *params.ConfigCompatError, error) { - return SetupGenesisBlockWithOverride(db, triedb, genesis, nil) + return SetupGenesisBlockWithOverride(db, triedb, genesis, nil, nil) } -func SetupGenesisBlockWithOverride(db ethdb.Database, triedb *triedb.Database, genesis *Genesis, overrides *ChainOverrides) (*params.ChainConfig, common.Hash, *params.ConfigCompatError, error) { +func SetupGenesisBlockWithOverride(db ethdb.Database, triedb *triedb.Database, genesis *Genesis, overrides *ChainOverrides, tracer *tracing.Hooks) (*params.ChainConfig, common.Hash, *params.ConfigCompatError, error) { // Copy the genesis, so we can operate on a copy. genesis = genesis.copy() // Sanitize the supplied genesis, ensuring it has the associated chain @@ -320,7 +336,7 @@ func SetupGenesisBlockWithOverride(db ethdb.Database, triedb *triedb.Database, g return nil, common.Hash{}, nil, err } - block, err := genesis.Commit(db, triedb) + block, err := genesis.Commit(db, triedb, tracer) if err != nil { return nil, common.Hash{}, nil, err } @@ -348,7 +364,7 @@ func SetupGenesisBlockWithOverride(db ethdb.Database, triedb *triedb.Database, g if hash := genesis.ToBlock().Hash(); hash != ghash { return nil, common.Hash{}, nil, &GenesisMismatchError{ghash, hash} } - block, err := genesis.Commit(db, triedb) + block, err := genesis.Commit(db, triedb, tracer) if err != nil { return nil, common.Hash{}, nil, err } @@ -537,7 +553,7 @@ func (g *Genesis) toBlockWithRoot(root common.Hash) *types.Block { // Commit writes the block and state of a genesis specification to the database. // The block is committed as the canonical head block. -func (g *Genesis) Commit(db ethdb.Database, triedb *triedb.Database) (*types.Block, error) { +func (g *Genesis) Commit(db ethdb.Database, triedb *triedb.Database, tracer *tracing.Hooks) (*types.Block, error) { if g.Number != 0 { return nil, errors.New("can't commit genesis block with number > 0") } @@ -552,7 +568,7 @@ func (g *Genesis) Commit(db ethdb.Database, triedb *triedb.Database) (*types.Blo return nil, errors.New("can't start clique chain without signers") } // flush the data to disk and compute the state root - root, err := flushAlloc(&g.Alloc, triedb) + root, err := flushAlloc(&g.Alloc, triedb, tracer) if err != nil { return nil, err } @@ -578,7 +594,7 @@ func (g *Genesis) Commit(db ethdb.Database, triedb *triedb.Database) (*types.Blo // MustCommit writes the genesis block and state to db, panicking on error. // The block is committed as the canonical head block. func (g *Genesis) MustCommit(db ethdb.Database, triedb *triedb.Database) *types.Block { - block, err := g.Commit(db, triedb) + block, err := g.Commit(db, triedb, nil) if err != nil { panic(err) } diff --git a/core/genesis_test.go b/core/genesis_test.go index 1ed475695d..821c71feb9 100644 --- a/core/genesis_test.go +++ b/core/genesis_test.go @@ -88,7 +88,7 @@ func testSetupGenesis(t *testing.T, scheme string) { name: "custom block in DB, genesis == nil", fn: func(db ethdb.Database) (*params.ChainConfig, common.Hash, *params.ConfigCompatError, error) { tdb := triedb.NewDatabase(db, newDbConfig(scheme)) - customg.Commit(db, tdb) + customg.Commit(db, tdb, nil) return SetupGenesisBlock(db, tdb, nil) }, wantHash: customghash, @@ -98,7 +98,7 @@ func testSetupGenesis(t *testing.T, scheme string) { name: "custom block in DB, genesis == sepolia", fn: func(db ethdb.Database) (*params.ChainConfig, common.Hash, *params.ConfigCompatError, error) { tdb := triedb.NewDatabase(db, newDbConfig(scheme)) - customg.Commit(db, tdb) + customg.Commit(db, tdb, nil) return SetupGenesisBlock(db, tdb, DefaultSepoliaGenesisBlock()) }, wantErr: &GenesisMismatchError{Stored: customghash, New: params.SepoliaGenesisHash}, @@ -107,7 +107,7 @@ func testSetupGenesis(t *testing.T, scheme string) { name: "custom block in DB, genesis == hoodi", fn: func(db ethdb.Database) (*params.ChainConfig, common.Hash, *params.ConfigCompatError, error) { tdb := triedb.NewDatabase(db, newDbConfig(scheme)) - customg.Commit(db, tdb) + customg.Commit(db, tdb, nil) return SetupGenesisBlock(db, tdb, DefaultHoodiGenesisBlock()) }, wantErr: &GenesisMismatchError{Stored: customghash, New: params.HoodiGenesisHash}, @@ -116,7 +116,7 @@ func testSetupGenesis(t *testing.T, scheme string) { name: "compatible config in DB", fn: func(db ethdb.Database) (*params.ChainConfig, common.Hash, *params.ConfigCompatError, error) { tdb := triedb.NewDatabase(db, newDbConfig(scheme)) - oldcustomg.Commit(db, tdb) + oldcustomg.Commit(db, tdb, nil) return SetupGenesisBlock(db, tdb, &customg) }, wantHash: customghash, @@ -128,7 +128,7 @@ func testSetupGenesis(t *testing.T, scheme string) { // Commit the 'old' genesis block with Homestead transition at #2. // Advance to block #4, past the homestead transition block of customg. tdb := triedb.NewDatabase(db, newDbConfig(scheme)) - oldcustomg.Commit(db, tdb) + oldcustomg.Commit(db, tdb, nil) bc, _ := NewBlockChain(db, &oldcustomg, ethash.NewFullFaker(), DefaultConfig().WithStateScheme(scheme)) defer bc.Stop() diff --git a/core/headerchain_test.go b/core/headerchain_test.go index b51fb8f226..dba04e2cf2 100644 --- a/core/headerchain_test.go +++ b/core/headerchain_test.go @@ -69,7 +69,7 @@ func TestHeaderInsertion(t *testing.T) { db = rawdb.NewMemoryDatabase() gspec = &Genesis{BaseFee: big.NewInt(params.InitialBaseFee), Config: params.AllEthashProtocolChanges} ) - gspec.Commit(db, triedb.NewDatabase(db, nil)) + gspec.Commit(db, triedb.NewDatabase(db, nil), nil) hc, err := NewHeaderChain(db, gspec.Config, ethash.NewFaker(), func() bool { return false }) if err != nil { t.Fatal(err) diff --git a/core/state/statedb.go b/core/state/statedb.go index 7c6b8bbdfc..bb51ff25ac 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -1390,14 +1390,14 @@ func (s *StateDB) Commit(block uint64, deleteEmptyObjects bool, noStorageWiping return ret.root, nil } -// CommitAndTrack writes the state mutations and notifies the size tracker of the state changes. -func (s *StateDB) CommitAndTrack(block uint64, deleteEmptyObjects bool, noStorageWiping bool, sizer *SizeTracker) (common.Hash, error) { +// CommitWithUpdate writes the state mutations and returns the state update for +// external processing (e.g., live tracing hooks or size tracker). +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 { - return common.Hash{}, err + return common.Hash{}, nil, err } - sizer.Notify(ret) - return ret.root, nil + return ret.root, ret, nil } // Prepare handles the preparatory steps for executing a state transition with. diff --git a/core/state/stateupdate.go b/core/state/stateupdate.go index c043166cf2..fe11f5efdb 100644 --- a/core/state/stateupdate.go +++ b/core/state/stateupdate.go @@ -17,9 +17,14 @@ package state import ( + "fmt" "maps" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/tracing" + "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/ethereum/go-ethereum/triedb" ) @@ -210,3 +215,131 @@ func (sc *stateUpdate) markCodeExistence(reader ContractCodeReader) { code.exists = res } } + +// ToTracingUpdate converts the internal stateUpdate to an exported tracing.StateUpdate. +func (sc *stateUpdate) ToTracingUpdate() (*tracing.StateUpdate, error) { + update := &tracing.StateUpdate{ + OriginRoot: sc.originRoot, + Root: sc.root, + BlockNumber: sc.blockNumber, + AccountChanges: make(map[common.Address]*tracing.AccountChange, len(sc.accountsOrigin)), + StorageChanges: make(map[common.Address]map[common.Hash]*tracing.StorageChange), + CodeChanges: make(map[common.Address]*tracing.CodeChange, len(sc.codes)), + TrieChanges: make(map[common.Hash]map[string]*tracing.TrieNodeChange), + } + + for addr, prev := range sc.accountsOrigin { + addrHash := crypto.Keccak256Hash(addr.Bytes()) + new, exists := sc.accounts[addrHash] + if !exists { + return nil, fmt.Errorf("account %x not found", addr) + } + + change := &tracing.AccountChange{} + + if len(prev) > 0 { + if acct, err := types.FullAccount(prev); err == nil { + change.Prev = &types.StateAccount{ + Nonce: acct.Nonce, + Balance: acct.Balance, + Root: acct.Root, + CodeHash: acct.CodeHash, + } + } + } + + if len(new) > 0 { + if acct, err := types.FullAccount(new); err == nil { + change.New = &types.StateAccount{ + Nonce: acct.Nonce, + Balance: acct.Balance, + Root: acct.Root, + CodeHash: acct.CodeHash, + } + } + } + + update.AccountChanges[addr] = change + } + + for addr, slots := range sc.storagesOrigin { + addrHash := crypto.Keccak256Hash(addr.Bytes()) + subset, exists := sc.storages[addrHash] + if !exists { + return nil, fmt.Errorf("storage %x not found", addr) + } + + storageMap := make(map[common.Hash]*tracing.StorageChange, len(slots)) + for key, encPrev := range slots { + // Get new value - handle both raw and hashed key formats + var ( + exists bool + encNew []byte + decPrev []byte + decNew []byte + err error + ) + if sc.rawStorageKey { + encNew, exists = subset[crypto.Keccak256Hash(key.Bytes())] + } else { + encNew, exists = subset[key] + } + if !exists { + return nil, fmt.Errorf("storage slot %x-%x not found", addr, key) + } + + // Decode the prev and new values + if len(encPrev) > 0 { + _, decPrev, _, err = rlp.Split(encPrev) + if err != nil { + return nil, fmt.Errorf("failed to decode prevValue: %v", err) + } + } + if len(encNew) > 0 { + _, decNew, _, err = rlp.Split(encNew) + if err != nil { + return nil, fmt.Errorf("failed to decode newValue: %v", err) + } + } + + storageMap[key] = &tracing.StorageChange{ + Prev: common.BytesToHash(decPrev), + New: common.BytesToHash(decNew), + } + } + update.StorageChanges[addr] = storageMap + } + + for addr, code := range sc.codes { + update.CodeChanges[addr] = &tracing.CodeChange{ + Hash: code.hash, + Code: code.blob, + Exists: code.exists, + } + } + + if sc.nodes != nil { + for owner, subset := range sc.nodes.Sets { + nodeMap := make(map[string]*tracing.TrieNodeChange, len(subset.Origins)) + for path, oldNode := range subset.Origins { + newNode, exists := subset.Nodes[path] + if !exists { + return nil, fmt.Errorf("node %x-%v not found", owner, path) + } + nodeMap[path] = &tracing.TrieNodeChange{ + Prev: &trienode.Node{ + Hash: crypto.Keccak256Hash(oldNode), + Blob: oldNode, + }, + New: &trienode.Node{ + Hash: newNode.Hash, + Blob: newNode.Blob, + }, + } + } + update.TrieChanges[owner] = nodeMap + } + } + + return update, nil +} diff --git a/core/tracing/hooks.go b/core/tracing/hooks.go index 8e50dc3d8f..4feb9b1164 100644 --- a/core/tracing/hooks.go +++ b/core/tracing/hooks.go @@ -30,6 +30,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/trie/trienode" "github.com/holiman/uint256" ) @@ -75,6 +76,51 @@ type BlockEvent struct { Safe *types.Header } +// StateUpdate represents the state mutations resulting from block execution. +// It provides access to account changes, storage changes, and contract code +// deployments with both previous and new values. +type StateUpdate struct { + OriginRoot common.Hash // State root before the update + Root common.Hash // State root after the update + BlockNumber uint64 + + // AccountChanges contains all account state changes keyed by address. + AccountChanges map[common.Address]*AccountChange + + // StorageChanges contains all storage slot changes keyed by address and storage slot key. + StorageChanges map[common.Address]map[common.Hash]*StorageChange + + // CodeChanges contains all contract code changes keyed by address. + CodeChanges map[common.Address]*CodeChange + + // TrieChanges contains trie node mutations keyed by address hash and trie node path. + TrieChanges map[common.Hash]map[string]*TrieNodeChange +} + +// AccountChange represents a change to an account's state. +type AccountChange struct { + Prev *types.StateAccount // nil if account was created + New *types.StateAccount // nil if account was deleted +} + +// StorageChange represents a change to a storage slot. +type StorageChange struct { + Prev common.Hash // previous value (zero if slot was created) + New common.Hash // new value (zero if slot was deleted) +} + +// CodeChange represents a contract code deployment or change. +type CodeChange struct { + Hash common.Hash + Code []byte + Exists bool // true if the code already deployed before +} + +type TrieNodeChange struct { + Prev *trienode.Node + New *trienode.Node +} + type ( /* - VM events - @@ -161,6 +207,11 @@ type ( // beacon block root. OnSystemCallEndHook = func() + // StateUpdateHook is called after state is committed for a block. + // It provides access to the complete state mutations including account changes, + // storage changes, trie node mutations, and contract code deployments. + StateUpdateHook = func(update *StateUpdate) + /* - State events - */ @@ -209,6 +260,7 @@ type Hooks struct { OnSystemCallStart OnSystemCallStartHook OnSystemCallStartV2 OnSystemCallStartHookV2 OnSystemCallEnd OnSystemCallEndHook + OnStateUpdate StateUpdateHook // State events OnBalanceChange BalanceChangeHook OnNonceChange NonceChangeHook diff --git a/eth/filters/filter_system_test.go b/eth/filters/filter_system_test.go index e5a1a2b25f..6f97d5b664 100644 --- a/eth/filters/filter_system_test.go +++ b/eth/filters/filter_system_test.go @@ -546,7 +546,7 @@ func TestExceedLogQueryLimit(t *testing.T) { } ) - _, err := gspec.Commit(db, triedb.NewDatabase(db, nil)) + _, err := gspec.Commit(db, triedb.NewDatabase(db, nil), nil) if err != nil { t.Fatal(err) } diff --git a/eth/filters/filter_test.go b/eth/filters/filter_test.go index edec3e027f..f44ada20b1 100644 --- a/eth/filters/filter_test.go +++ b/eth/filters/filter_test.go @@ -205,7 +205,7 @@ func testFilters(t *testing.T, history uint64, noHistory bool) { // Hack: GenerateChainWithGenesis creates a new db. // Commit the genesis manually and use GenerateChain. - _, err = gspec.Commit(db, triedb.NewDatabase(db, nil)) + _, err = gspec.Commit(db, triedb.NewDatabase(db, nil), nil) if err != nil { t.Fatal(err) } @@ -426,7 +426,7 @@ func TestRangeLogs(t *testing.T) { BaseFee: big.NewInt(params.InitialBaseFee), } ) - _, err := gspec.Commit(db, triedb.NewDatabase(db, nil)) + _, err := gspec.Commit(db, triedb.NewDatabase(db, nil), nil) if err != nil { t.Fatal(err) } diff --git a/eth/tracers/live/statesize.go b/eth/tracers/live/statesize.go new file mode 100644 index 0000000000..42464900ad --- /dev/null +++ b/eth/tracers/live/statesize.go @@ -0,0 +1,715 @@ +// 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 +} + +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), + } + + // Load existing data if file exists + if err := t.loadExisting(); err != nil { + return nil, fmt.Errorf("failed to load existing statesize data: %v", err) + } + + // 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 +} + +// loadExisting reads the existing CSV file and loads the latest records by block number. +func (s *stateSizeTracer) loadExisting() error { + file, err := os.Open(s.filePath) + if err != nil { + if os.IsNotExist(err) { + return nil // File doesn't exist, nothing to load + } + return err + } + defer file.Close() + + reader := csv.NewReader(bufio.NewReader(file)) + + // Read and skip header + if _, err := reader.Read(); err != nil { + if err == io.EOF { + return nil // Empty file + } + return fmt.Errorf("failed to read CSV header: %v", err) + } + + // Read all records to find the latest block number + var ( + latestBlockNum uint64 + latestRecords = make(map[common.Hash]stateSizeStats) + ) + + for { + record, err := reader.Read() + if err == io.EOF { + break + } + if err != nil { + return fmt.Errorf("failed to read CSV record: %v", err) + } + if len(record) < len(csvHeaders) { + continue // Skip malformed records + } + + blockNum, err := strconv.ParseUint(record[0], 10, 64) + if err != nil { + continue + } + + // If we found a new latest block, clear the map + if blockNum > latestBlockNum { + latestBlockNum = blockNum + latestRecords = make(map[common.Hash]stateSizeStats) + } + + // Only store records from the latest block + if blockNum == latestBlockNum { + root := common.HexToHash(record[1]) + stats, err := parseStats(record) + if err != nil { + log.Warn("Failed to parse statesize record", "error", err) + continue + } + latestRecords[root] = stats + } + } + + s.stats = latestRecords + if len(latestRecords) > 0 { + log.Info("Loaded statesize tracer state", "block", latestBlockNum, "roots", len(latestRecords)) + } + return nil +} + +// 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 + } + + // 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 new file mode 100644 index 0000000000..120887441a --- /dev/null +++ b/eth/tracers/live/statesize_test.go @@ -0,0 +1,244 @@ +// 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]) + } + } +} diff --git a/tests/block_test_util.go b/tests/block_test_util.go index 72fd955c8f..4f6ab65c1a 100644 --- a/tests/block_test_util.go +++ b/tests/block_test_util.go @@ -138,7 +138,7 @@ func (t *BlockTest) Run(snapshotter bool, scheme string, witness bool, tracer *t gspec.Config.TerminalTotalDifficulty = big.NewInt(stdmath.MaxInt64) } triedb := triedb.NewDatabase(db, tconf) - gblock, err := gspec.Commit(db, triedb) + gblock, err := gspec.Commit(db, triedb, nil) if err != nil { return err }