mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-23 05:06:43 +00:00
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
This commit is contained in:
parent
27b3a6087e
commit
abbe71bb6c
14 changed files with 1212 additions and 36 deletions
|
|
@ -296,7 +296,7 @@ func initGenesis(ctx *cli.Context) error {
|
||||||
triedb := utils.MakeTrieDatabase(ctx, stack, chaindb, ctx.Bool(utils.CachePreimagesFlag.Name), false, genesis.IsVerkle())
|
triedb := utils.MakeTrieDatabase(ctx, stack, chaindb, ctx.Bool(utils.CachePreimagesFlag.Name), false, genesis.IsVerkle())
|
||||||
defer triedb.Close()
|
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 {
|
if err != nil {
|
||||||
utils.Fatalf("Failed to write genesis block: %v", err)
|
utils.Fatalf("Failed to write genesis block: %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
// yet. The corresponding chain config will be returned, either from the
|
||||||
// provided genesis or from the locally stored configuration if the genesis
|
// provided genesis or from the locally stored configuration if the genesis
|
||||||
// has already been initialized.
|
// 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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -1653,11 +1653,27 @@ func (bc *BlockChain) writeBlockWithState(block *types.Block, receipts []*types.
|
||||||
root common.Hash
|
root common.Hash
|
||||||
isEIP158 = bc.chainConfig.IsEIP158(block.Number())
|
isEIP158 = bc.chainConfig.IsEIP158(block.Number())
|
||||||
isCancun = bc.chainConfig.IsCancun(block.Number(), block.Time())
|
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 {
|
if needStateUpdate {
|
||||||
root, err = statedb.Commit(block.NumberU64(), isEIP158, isCancun)
|
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 {
|
} else {
|
||||||
root, err = statedb.CommitAndTrack(block.NumberU64(), isEIP158, isCancun, bc.stateSizer)
|
root, err = statedb.Commit(block.NumberU64(), isEIP158, isCancun)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
||||||
|
|
@ -481,7 +481,7 @@ func GenerateChainWithGenesis(genesis *Genesis, engine consensus.Engine, n int,
|
||||||
}
|
}
|
||||||
triedb := triedb.NewDatabase(db, triedbConfig)
|
triedb := triedb.NewDatabase(db, triedbConfig)
|
||||||
defer triedb.Close()
|
defer triedb.Close()
|
||||||
_, err := genesis.Commit(db, triedb)
|
_, err := genesis.Commit(db, triedb, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
// flushAlloc is very similar with hash, but the main difference is all the
|
||||||
// generated states will be persisted into the given database.
|
// 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
|
emptyRoot := types.EmptyRootHash
|
||||||
if triedb.IsVerkle() {
|
if triedb.IsVerkle() {
|
||||||
emptyRoot = types.EmptyVerkleHash
|
emptyRoot = types.EmptyVerkleHash
|
||||||
|
|
@ -185,10 +185,26 @@ func flushAlloc(ga *types.GenesisAlloc, triedb *triedb.Database) (common.Hash, e
|
||||||
statedb.SetState(addr, key, value)
|
statedb.SetState(addr, key, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
root, err := statedb.Commit(0, false, false)
|
|
||||||
|
var root common.Hash
|
||||||
|
if tracer != nil && tracer.OnStateUpdate != nil {
|
||||||
|
r, update, err := statedb.CommitWithUpdate(0, false, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return common.Hash{}, err
|
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.
|
// Commit newly generated states into disk if it's not empty.
|
||||||
if root != emptyRoot {
|
if root != emptyRoot {
|
||||||
if err := triedb.Commit(root, true); err != nil {
|
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
|
// 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.
|
// 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) {
|
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.
|
// Copy the genesis, so we can operate on a copy.
|
||||||
genesis = genesis.copy()
|
genesis = genesis.copy()
|
||||||
// Sanitize the supplied genesis, ensuring it has the associated chain
|
// 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
|
return nil, common.Hash{}, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
block, err := genesis.Commit(db, triedb)
|
block, err := genesis.Commit(db, triedb, tracer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, common.Hash{}, nil, err
|
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 {
|
if hash := genesis.ToBlock().Hash(); hash != ghash {
|
||||||
return nil, common.Hash{}, nil, &GenesisMismatchError{ghash, hash}
|
return nil, common.Hash{}, nil, &GenesisMismatchError{ghash, hash}
|
||||||
}
|
}
|
||||||
block, err := genesis.Commit(db, triedb)
|
block, err := genesis.Commit(db, triedb, tracer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, common.Hash{}, nil, err
|
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.
|
// Commit writes the block and state of a genesis specification to the database.
|
||||||
// The block is committed as the canonical head block.
|
// 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 {
|
if g.Number != 0 {
|
||||||
return nil, errors.New("can't commit genesis block with 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")
|
return nil, errors.New("can't start clique chain without signers")
|
||||||
}
|
}
|
||||||
// flush the data to disk and compute the state root
|
// 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 {
|
if err != nil {
|
||||||
return nil, err
|
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.
|
// MustCommit writes the genesis block and state to db, panicking on error.
|
||||||
// The block is committed as the canonical head block.
|
// The block is committed as the canonical head block.
|
||||||
func (g *Genesis) MustCommit(db ethdb.Database, triedb *triedb.Database) *types.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 {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,7 @@ func testSetupGenesis(t *testing.T, scheme string) {
|
||||||
name: "custom block in DB, genesis == nil",
|
name: "custom block in DB, genesis == nil",
|
||||||
fn: func(db ethdb.Database) (*params.ChainConfig, common.Hash, *params.ConfigCompatError, error) {
|
fn: func(db ethdb.Database) (*params.ChainConfig, common.Hash, *params.ConfigCompatError, error) {
|
||||||
tdb := triedb.NewDatabase(db, newDbConfig(scheme))
|
tdb := triedb.NewDatabase(db, newDbConfig(scheme))
|
||||||
customg.Commit(db, tdb)
|
customg.Commit(db, tdb, nil)
|
||||||
return SetupGenesisBlock(db, tdb, nil)
|
return SetupGenesisBlock(db, tdb, nil)
|
||||||
},
|
},
|
||||||
wantHash: customghash,
|
wantHash: customghash,
|
||||||
|
|
@ -98,7 +98,7 @@ func testSetupGenesis(t *testing.T, scheme string) {
|
||||||
name: "custom block in DB, genesis == sepolia",
|
name: "custom block in DB, genesis == sepolia",
|
||||||
fn: func(db ethdb.Database) (*params.ChainConfig, common.Hash, *params.ConfigCompatError, error) {
|
fn: func(db ethdb.Database) (*params.ChainConfig, common.Hash, *params.ConfigCompatError, error) {
|
||||||
tdb := triedb.NewDatabase(db, newDbConfig(scheme))
|
tdb := triedb.NewDatabase(db, newDbConfig(scheme))
|
||||||
customg.Commit(db, tdb)
|
customg.Commit(db, tdb, nil)
|
||||||
return SetupGenesisBlock(db, tdb, DefaultSepoliaGenesisBlock())
|
return SetupGenesisBlock(db, tdb, DefaultSepoliaGenesisBlock())
|
||||||
},
|
},
|
||||||
wantErr: &GenesisMismatchError{Stored: customghash, New: params.SepoliaGenesisHash},
|
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",
|
name: "custom block in DB, genesis == hoodi",
|
||||||
fn: func(db ethdb.Database) (*params.ChainConfig, common.Hash, *params.ConfigCompatError, error) {
|
fn: func(db ethdb.Database) (*params.ChainConfig, common.Hash, *params.ConfigCompatError, error) {
|
||||||
tdb := triedb.NewDatabase(db, newDbConfig(scheme))
|
tdb := triedb.NewDatabase(db, newDbConfig(scheme))
|
||||||
customg.Commit(db, tdb)
|
customg.Commit(db, tdb, nil)
|
||||||
return SetupGenesisBlock(db, tdb, DefaultHoodiGenesisBlock())
|
return SetupGenesisBlock(db, tdb, DefaultHoodiGenesisBlock())
|
||||||
},
|
},
|
||||||
wantErr: &GenesisMismatchError{Stored: customghash, New: params.HoodiGenesisHash},
|
wantErr: &GenesisMismatchError{Stored: customghash, New: params.HoodiGenesisHash},
|
||||||
|
|
@ -116,7 +116,7 @@ func testSetupGenesis(t *testing.T, scheme string) {
|
||||||
name: "compatible config in DB",
|
name: "compatible config in DB",
|
||||||
fn: func(db ethdb.Database) (*params.ChainConfig, common.Hash, *params.ConfigCompatError, error) {
|
fn: func(db ethdb.Database) (*params.ChainConfig, common.Hash, *params.ConfigCompatError, error) {
|
||||||
tdb := triedb.NewDatabase(db, newDbConfig(scheme))
|
tdb := triedb.NewDatabase(db, newDbConfig(scheme))
|
||||||
oldcustomg.Commit(db, tdb)
|
oldcustomg.Commit(db, tdb, nil)
|
||||||
return SetupGenesisBlock(db, tdb, &customg)
|
return SetupGenesisBlock(db, tdb, &customg)
|
||||||
},
|
},
|
||||||
wantHash: customghash,
|
wantHash: customghash,
|
||||||
|
|
@ -128,7 +128,7 @@ func testSetupGenesis(t *testing.T, scheme string) {
|
||||||
// Commit the 'old' genesis block with Homestead transition at #2.
|
// Commit the 'old' genesis block with Homestead transition at #2.
|
||||||
// Advance to block #4, past the homestead transition block of customg.
|
// Advance to block #4, past the homestead transition block of customg.
|
||||||
tdb := triedb.NewDatabase(db, newDbConfig(scheme))
|
tdb := triedb.NewDatabase(db, newDbConfig(scheme))
|
||||||
oldcustomg.Commit(db, tdb)
|
oldcustomg.Commit(db, tdb, nil)
|
||||||
|
|
||||||
bc, _ := NewBlockChain(db, &oldcustomg, ethash.NewFullFaker(), DefaultConfig().WithStateScheme(scheme))
|
bc, _ := NewBlockChain(db, &oldcustomg, ethash.NewFullFaker(), DefaultConfig().WithStateScheme(scheme))
|
||||||
defer bc.Stop()
|
defer bc.Stop()
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ func TestHeaderInsertion(t *testing.T) {
|
||||||
db = rawdb.NewMemoryDatabase()
|
db = rawdb.NewMemoryDatabase()
|
||||||
gspec = &Genesis{BaseFee: big.NewInt(params.InitialBaseFee), Config: params.AllEthashProtocolChanges}
|
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 })
|
hc, err := NewHeaderChain(db, gspec.Config, ethash.NewFaker(), func() bool { return false })
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
|
|
||||||
|
|
@ -1390,14 +1390,14 @@ func (s *StateDB) Commit(block uint64, deleteEmptyObjects bool, noStorageWiping
|
||||||
return ret.root, nil
|
return ret.root, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CommitAndTrack writes the state mutations and notifies the size tracker of the state changes.
|
// CommitWithUpdate writes the state mutations and returns the state update for
|
||||||
func (s *StateDB) CommitAndTrack(block uint64, deleteEmptyObjects bool, noStorageWiping bool, sizer *SizeTracker) (common.Hash, error) {
|
// 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)
|
ret, err := s.commitAndFlush(block, deleteEmptyObjects, noStorageWiping, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return common.Hash{}, err
|
return common.Hash{}, nil, err
|
||||||
}
|
}
|
||||||
sizer.Notify(ret)
|
return ret.root, ret, nil
|
||||||
return ret.root, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare handles the preparatory steps for executing a state transition with.
|
// Prepare handles the preparatory steps for executing a state transition with.
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,14 @@
|
||||||
package state
|
package state
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"maps"
|
"maps"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"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/trie/trienode"
|
||||||
"github.com/ethereum/go-ethereum/triedb"
|
"github.com/ethereum/go-ethereum/triedb"
|
||||||
)
|
)
|
||||||
|
|
@ -210,3 +215,131 @@ func (sc *stateUpdate) markCodeExistence(reader ContractCodeReader) {
|
||||||
code.exists = res
|
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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ import (
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
"github.com/ethereum/go-ethereum/params"
|
"github.com/ethereum/go-ethereum/params"
|
||||||
|
"github.com/ethereum/go-ethereum/trie/trienode"
|
||||||
"github.com/holiman/uint256"
|
"github.com/holiman/uint256"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -75,6 +76,51 @@ type BlockEvent struct {
|
||||||
Safe *types.Header
|
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 (
|
type (
|
||||||
/*
|
/*
|
||||||
- VM events -
|
- VM events -
|
||||||
|
|
@ -161,6 +207,11 @@ type (
|
||||||
// beacon block root.
|
// beacon block root.
|
||||||
OnSystemCallEndHook = func()
|
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 -
|
- State events -
|
||||||
*/
|
*/
|
||||||
|
|
@ -209,6 +260,7 @@ type Hooks struct {
|
||||||
OnSystemCallStart OnSystemCallStartHook
|
OnSystemCallStart OnSystemCallStartHook
|
||||||
OnSystemCallStartV2 OnSystemCallStartHookV2
|
OnSystemCallStartV2 OnSystemCallStartHookV2
|
||||||
OnSystemCallEnd OnSystemCallEndHook
|
OnSystemCallEnd OnSystemCallEndHook
|
||||||
|
OnStateUpdate StateUpdateHook
|
||||||
// State events
|
// State events
|
||||||
OnBalanceChange BalanceChangeHook
|
OnBalanceChange BalanceChangeHook
|
||||||
OnNonceChange NonceChangeHook
|
OnNonceChange NonceChangeHook
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -205,7 +205,7 @@ func testFilters(t *testing.T, history uint64, noHistory bool) {
|
||||||
|
|
||||||
// Hack: GenerateChainWithGenesis creates a new db.
|
// Hack: GenerateChainWithGenesis creates a new db.
|
||||||
// Commit the genesis manually and use GenerateChain.
|
// 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 {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
@ -426,7 +426,7 @@ func TestRangeLogs(t *testing.T) {
|
||||||
BaseFee: big.NewInt(params.InitialBaseFee),
|
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 {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
715
eth/tracers/live/statesize.go
Normal file
715
eth/tracers/live/statesize.go
Normal file
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
244
eth/tracers/live/statesize_test.go
Normal file
244
eth/tracers/live/statesize_test.go
Normal file
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -138,7 +138,7 @@ func (t *BlockTest) Run(snapshotter bool, scheme string, witness bool, tracer *t
|
||||||
gspec.Config.TerminalTotalDifficulty = big.NewInt(stdmath.MaxInt64)
|
gspec.Config.TerminalTotalDifficulty = big.NewInt(stdmath.MaxInt64)
|
||||||
}
|
}
|
||||||
triedb := triedb.NewDatabase(db, tconf)
|
triedb := triedb.NewDatabase(db, tconf)
|
||||||
gblock, err := gspec.Commit(db, triedb)
|
gblock, err := gspec.Commit(db, triedb, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue