mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-02-26 07:37:20 +00:00
cmd/geth: add inspect trie tool to analysis trie storage (#28892)
This pr adds a tool names `inpsect-trie`, aimed to analyze the mpt and its node storage more efficiently. ## Example ./geth db inspect-trie --datadir server/data-seed/ latest 4000 ## Result - MPT shape - Account Trie - Top N Storage Trie ``` +-------+-------+--------------+-------------+--------------+ | - | LEVEL | SHORTNODECNT | FULLNODECNT | VALUENODECNT | +-------+-------+--------------+-------------+--------------+ | - | 0 | 0 | 1 | 0 | | - | 1 | 0 | 16 | 0 | | - | 2 | 76 | 32 | 74 | | - | 3 | 66 | 1 | 66 | | - | 4 | 2 | 0 | 2 | | Total | 144 | 50 | 142 | +-------+-------+--------------+-------------+--------------+ AccountTrie +-------+-------+--------------+-------------+--------------+ | - | LEVEL | SHORTNODECNT | FULLNODECNT | VALUENODECNT | +-------+-------+--------------+-------------+--------------+ | - | 0 | 0 | 1 | 0 | | - | 1 | 0 | 16 | 0 | | - | 2 | 108 | 84 | 104 | | - | 3 | 195 | 5 | 195 | | - | 4 | 10 | 0 | 10 | | Total | 313 | 106 | 309 | +-------+-------+--------------+-------------+--------------+ ContractTrie-0xc874e65ccffb133d9db4ff637e62532ef6ecef3223845d02f522c55786782911 +-------+-------+--------------+-------------+--------------+ | - | LEVEL | SHORTNODECNT | FULLNODECNT | VALUENODECNT | +-------+-------+--------------+-------------+--------------+ | - | 0 | 0 | 1 | 0 | | - | 1 | 0 | 16 | 0 | | - | 2 | 57 | 14 | 56 | | - | 3 | 33 | 0 | 33 | | Total | 90 | 31 | 89 | +-------+-------+--------------+-------------+--------------+ ContractTrie-0x1d7dcb6a0ce5227c5379fc5b0e004561d7833b063355f69bfea3178f08fbaab4 +-------+-------+--------------+-------------+--------------+ | - | LEVEL | SHORTNODECNT | FULLNODECNT | VALUENODECNT | +-------+-------+--------------+-------------+--------------+ | - | 0 | 0 | 1 | 0 | | - | 1 | 5 | 8 | 5 | | - | 2 | 16 | 1 | 16 | | - | 3 | 2 | 0 | 2 | | Total | 23 | 10 | 23 | +-------+-------+--------------+-------------+--------------+ ContractTrie-0xaa8a4783ebbb3bec45d3e804b3c59bfd486edfa39cbeda1d42bf86c08a0ebc0f +-------+-------+--------------+-------------+--------------+ | - | LEVEL | SHORTNODECNT | FULLNODECNT | VALUENODECNT | +-------+-------+--------------+-------------+--------------+ | - | 0 | 0 | 1 | 0 | | - | 1 | 9 | 3 | 9 | | - | 2 | 7 | 1 | 7 | | - | 3 | 2 | 0 | 2 | | Total | 18 | 5 | 18 | +-------+-------+--------------+-------------+--------------+ ContractTrie-0x9d2804d0562391d7cfcfaf0013f0352e176a94403a58577ebf82168a21514441 +-------+-------+--------------+-------------+--------------+ | - | LEVEL | SHORTNODECNT | FULLNODECNT | VALUENODECNT | +-------+-------+--------------+-------------+--------------+ | - | 0 | 0 | 1 | 0 | | - | 1 | 6 | 4 | 6 | | - | 2 | 8 | 0 | 8 | | Total | 14 | 5 | 14 | +-------+-------+--------------+-------------+--------------+ ContractTrie-0x17e3eb95d0e6e92b42c0b3e95c6e75080c9fcd83e706344712e9587375de96e1 +-------+-------+--------------+-------------+--------------+ | - | LEVEL | SHORTNODECNT | FULLNODECNT | VALUENODECNT | +-------+-------+--------------+-------------+--------------+ | - | 0 | 0 | 1 | 0 | | - | 1 | 5 | 3 | 5 | | - | 2 | 7 | 0 | 7 | | Total | 12 | 4 | 12 | +-------+-------+--------------+-------------+--------------+ ContractTrie-0xc017ca90c8aa37693c38f80436bb15bde46d7b30a503aa808cb7814127468a44 Contract Trie, total trie num: 142, ShortNodeCnt: 620, FullNodeCnt: 204, ValueNodeCnt: 615 ``` --------- Co-authored-by: lightclient <lightclient@protonmail.com> Co-authored-by: MariusVanDerWijden <m.vanderwijden@live.de>
This commit is contained in:
parent
9ecb6c4ae6
commit
8450e40798
11 changed files with 1598 additions and 77 deletions
|
|
@ -19,6 +19,7 @@ package main
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
@ -37,6 +38,7 @@ import (
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
"github.com/ethereum/go-ethereum/ethdb"
|
"github.com/ethereum/go-ethereum/ethdb"
|
||||||
|
"github.com/ethereum/go-ethereum/internal/tablewriter"
|
||||||
"github.com/ethereum/go-ethereum/log"
|
"github.com/ethereum/go-ethereum/log"
|
||||||
"github.com/ethereum/go-ethereum/rlp"
|
"github.com/ethereum/go-ethereum/rlp"
|
||||||
"github.com/ethereum/go-ethereum/trie"
|
"github.com/ethereum/go-ethereum/trie"
|
||||||
|
|
@ -53,6 +55,23 @@ var (
|
||||||
Name: "remove.chain",
|
Name: "remove.chain",
|
||||||
Usage: "If set, selects the state data for removal",
|
Usage: "If set, selects the state data for removal",
|
||||||
}
|
}
|
||||||
|
inspectTrieTopFlag = &cli.IntFlag{
|
||||||
|
Name: "top",
|
||||||
|
Usage: "Print the top N results per ranking category",
|
||||||
|
Value: 10,
|
||||||
|
}
|
||||||
|
inspectTrieDumpPathFlag = &cli.StringFlag{
|
||||||
|
Name: "dump-path",
|
||||||
|
Usage: "Path for the trie statistics dump file",
|
||||||
|
}
|
||||||
|
inspectTrieSummarizeFlag = &cli.StringFlag{
|
||||||
|
Name: "summarize",
|
||||||
|
Usage: "Summarize an existing trie dump file (skip trie traversal)",
|
||||||
|
}
|
||||||
|
inspectTrieContractFlag = &cli.StringFlag{
|
||||||
|
Name: "contract",
|
||||||
|
Usage: "Inspect only the storage of the given contract address (skips full account trie walk)",
|
||||||
|
}
|
||||||
|
|
||||||
removedbCommand = &cli.Command{
|
removedbCommand = &cli.Command{
|
||||||
Action: removeDB,
|
Action: removeDB,
|
||||||
|
|
@ -74,6 +93,7 @@ Remove blockchain and state databases`,
|
||||||
dbCompactCmd,
|
dbCompactCmd,
|
||||||
dbGetCmd,
|
dbGetCmd,
|
||||||
dbDeleteCmd,
|
dbDeleteCmd,
|
||||||
|
dbInspectTrieCmd,
|
||||||
dbPutCmd,
|
dbPutCmd,
|
||||||
dbGetSlotsCmd,
|
dbGetSlotsCmd,
|
||||||
dbDumpFreezerIndex,
|
dbDumpFreezerIndex,
|
||||||
|
|
@ -92,6 +112,22 @@ Remove blockchain and state databases`,
|
||||||
Usage: "Inspect the storage size for each type of data in the database",
|
Usage: "Inspect the storage size for each type of data in the database",
|
||||||
Description: `This commands iterates the entire database. If the optional 'prefix' and 'start' arguments are provided, then the iteration is limited to the given subset of data.`,
|
Description: `This commands iterates the entire database. If the optional 'prefix' and 'start' arguments are provided, then the iteration is limited to the given subset of data.`,
|
||||||
}
|
}
|
||||||
|
dbInspectTrieCmd = &cli.Command{
|
||||||
|
Action: inspectTrie,
|
||||||
|
Name: "inspect-trie",
|
||||||
|
ArgsUsage: "<blocknum>",
|
||||||
|
Flags: slices.Concat([]cli.Flag{
|
||||||
|
utils.ExcludeStorageFlag,
|
||||||
|
inspectTrieTopFlag,
|
||||||
|
utils.OutputFileFlag,
|
||||||
|
inspectTrieDumpPathFlag,
|
||||||
|
inspectTrieSummarizeFlag,
|
||||||
|
inspectTrieContractFlag,
|
||||||
|
}, utils.NetworkFlags, utils.DatabaseFlags),
|
||||||
|
Usage: "Print detailed trie information about the structure of account trie and storage tries.",
|
||||||
|
Description: `This commands iterates the entrie trie-backed state. If the 'blocknum' is not specified,
|
||||||
|
the latest block number will be used by default.`,
|
||||||
|
}
|
||||||
dbCheckStateContentCmd = &cli.Command{
|
dbCheckStateContentCmd = &cli.Command{
|
||||||
Action: checkStateContent,
|
Action: checkStateContent,
|
||||||
Name: "check-state-content",
|
Name: "check-state-content",
|
||||||
|
|
@ -385,6 +421,88 @@ func checkStateContent(ctx *cli.Context) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func inspectTrie(ctx *cli.Context) error {
|
||||||
|
topN := ctx.Int(inspectTrieTopFlag.Name)
|
||||||
|
if topN <= 0 {
|
||||||
|
return fmt.Errorf("invalid --%s value %d (must be > 0)", inspectTrieTopFlag.Name, topN)
|
||||||
|
}
|
||||||
|
config := &trie.InspectConfig{
|
||||||
|
NoStorage: ctx.Bool(utils.ExcludeStorageFlag.Name),
|
||||||
|
TopN: topN,
|
||||||
|
Path: ctx.String(utils.OutputFileFlag.Name),
|
||||||
|
}
|
||||||
|
|
||||||
|
if summarizePath := ctx.String(inspectTrieSummarizeFlag.Name); summarizePath != "" {
|
||||||
|
if ctx.NArg() > 0 {
|
||||||
|
return fmt.Errorf("block number argument is not supported with --%s", inspectTrieSummarizeFlag.Name)
|
||||||
|
}
|
||||||
|
config.DumpPath = summarizePath
|
||||||
|
log.Info("Summarizing trie dump", "path", summarizePath, "top", topN)
|
||||||
|
return trie.Summarize(summarizePath, config)
|
||||||
|
}
|
||||||
|
if ctx.NArg() > 1 {
|
||||||
|
return fmt.Errorf("excessive number of arguments: %v", ctx.Command.ArgsUsage)
|
||||||
|
}
|
||||||
|
|
||||||
|
stack, _ := makeConfigNode(ctx)
|
||||||
|
db := utils.MakeChainDatabase(ctx, stack, false)
|
||||||
|
defer stack.Close()
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
var (
|
||||||
|
trieRoot common.Hash
|
||||||
|
hash common.Hash
|
||||||
|
number uint64
|
||||||
|
)
|
||||||
|
switch {
|
||||||
|
case ctx.NArg() == 0 || ctx.Args().Get(0) == "latest":
|
||||||
|
head := rawdb.ReadHeadHeaderHash(db)
|
||||||
|
n, ok := rawdb.ReadHeaderNumber(db, head)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("could not load head block hash")
|
||||||
|
}
|
||||||
|
number = n
|
||||||
|
case ctx.Args().Get(0) == "snapshot":
|
||||||
|
trieRoot = rawdb.ReadSnapshotRoot(db)
|
||||||
|
number = math.MaxUint64
|
||||||
|
default:
|
||||||
|
var err error
|
||||||
|
number, err = strconv.ParseUint(ctx.Args().Get(0), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to parse blocknum, Args[0]: %v, err: %v", ctx.Args().Get(0), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if number != math.MaxUint64 {
|
||||||
|
hash = rawdb.ReadCanonicalHash(db, number)
|
||||||
|
if hash == (common.Hash{}) {
|
||||||
|
return fmt.Errorf("canonical hash for block %d not found", number)
|
||||||
|
}
|
||||||
|
blockHeader := rawdb.ReadHeader(db, hash, number)
|
||||||
|
trieRoot = blockHeader.Root
|
||||||
|
}
|
||||||
|
if trieRoot == (common.Hash{}) {
|
||||||
|
log.Error("Empty root hash")
|
||||||
|
}
|
||||||
|
|
||||||
|
config.DumpPath = ctx.String(inspectTrieDumpPathFlag.Name)
|
||||||
|
if config.DumpPath == "" {
|
||||||
|
config.DumpPath = stack.ResolvePath("trie-dump.bin")
|
||||||
|
}
|
||||||
|
|
||||||
|
triedb := utils.MakeTrieDatabase(ctx, stack, db, false, true, false)
|
||||||
|
defer triedb.Close()
|
||||||
|
|
||||||
|
if contractAddr := ctx.String(inspectTrieContractFlag.Name); contractAddr != "" {
|
||||||
|
address := common.HexToAddress(contractAddr)
|
||||||
|
log.Info("Inspecting contract", "address", address, "root", trieRoot, "block", number)
|
||||||
|
return trie.InspectContract(triedb, db, trieRoot, address)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Inspecting trie", "root", trieRoot, "block", number, "dump", config.DumpPath, "top", topN)
|
||||||
|
return trie.Inspect(triedb, trieRoot, config)
|
||||||
|
}
|
||||||
|
|
||||||
func showDBStats(db ethdb.KeyValueStater) {
|
func showDBStats(db ethdb.KeyValueStater) {
|
||||||
stats, err := db.Stat()
|
stats, err := db.Stat()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -759,7 +877,7 @@ func showMetaData(ctx *cli.Context) error {
|
||||||
data = append(data, []string{"headHeader.Root", fmt.Sprintf("%v", h.Root)})
|
data = append(data, []string{"headHeader.Root", fmt.Sprintf("%v", h.Root)})
|
||||||
data = append(data, []string{"headHeader.Number", fmt.Sprintf("%d (%#x)", h.Number, h.Number)})
|
data = append(data, []string{"headHeader.Number", fmt.Sprintf("%d (%#x)", h.Number, h.Number)})
|
||||||
}
|
}
|
||||||
table := rawdb.NewTableWriter(os.Stdout)
|
table := tablewriter.NewWriter(os.Stdout)
|
||||||
table.SetHeader([]string{"Field", "Value"})
|
table.SetHeader([]string{"Field", "Value"})
|
||||||
table.AppendBulk(data)
|
table.AppendBulk(data)
|
||||||
table.Render()
|
table.Render()
|
||||||
|
|
|
||||||
|
|
@ -218,6 +218,16 @@ var (
|
||||||
Usage: "Max number of elements (0 = no limit)",
|
Usage: "Max number of elements (0 = no limit)",
|
||||||
Value: 0,
|
Value: 0,
|
||||||
}
|
}
|
||||||
|
TopFlag = &cli.IntFlag{
|
||||||
|
Name: "top",
|
||||||
|
Usage: "Print the top N results",
|
||||||
|
Value: 5,
|
||||||
|
}
|
||||||
|
OutputFileFlag = &cli.StringFlag{
|
||||||
|
Name: "output",
|
||||||
|
Usage: "Writes the result in json to the output",
|
||||||
|
Value: "",
|
||||||
|
}
|
||||||
|
|
||||||
SnapshotFlag = &cli.BoolFlag{
|
SnapshotFlag = &cli.BoolFlag{
|
||||||
Name: "snapshot",
|
Name: "snapshot",
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ import (
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
"github.com/ethereum/go-ethereum/ethdb"
|
"github.com/ethereum/go-ethereum/ethdb"
|
||||||
"github.com/ethereum/go-ethereum/ethdb/memorydb"
|
"github.com/ethereum/go-ethereum/ethdb/memorydb"
|
||||||
|
"github.com/ethereum/go-ethereum/internal/tablewriter"
|
||||||
"github.com/ethereum/go-ethereum/log"
|
"github.com/ethereum/go-ethereum/log"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
)
|
)
|
||||||
|
|
@ -663,7 +664,7 @@ func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error {
|
||||||
total.Add(uint64(ancient.size()))
|
total.Add(uint64(ancient.size()))
|
||||||
}
|
}
|
||||||
|
|
||||||
table := NewTableWriter(os.Stdout)
|
table := tablewriter.NewWriter(os.Stdout)
|
||||||
table.SetHeader([]string{"Database", "Category", "Size", "Items"})
|
table.SetHeader([]string{"Database", "Category", "Size", "Items"})
|
||||||
table.SetFooter([]string{"", "Total", common.StorageSize(total.Load()).String(), fmt.Sprintf("%d", count.Load())})
|
table.SetFooter([]string{"", "Total", common.StorageSize(total.Load()).String(), fmt.Sprintf("%d", count.Load())})
|
||||||
table.AppendBulk(stats)
|
table.AppendBulk(stats)
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ import (
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
"github.com/ethereum/go-ethereum/log"
|
"github.com/ethereum/go-ethereum/log"
|
||||||
"github.com/ethereum/go-ethereum/metrics"
|
"github.com/ethereum/go-ethereum/metrics"
|
||||||
|
"github.com/ethereum/go-ethereum/trie"
|
||||||
)
|
)
|
||||||
|
|
||||||
var accountTrieLeavesAtDepth [16]*metrics.Counter
|
var accountTrieLeavesAtDepth [16]*metrics.Counter
|
||||||
|
|
@ -41,59 +42,68 @@ func init() {
|
||||||
|
|
||||||
// WitnessStats aggregates statistics for account and storage trie accesses.
|
// WitnessStats aggregates statistics for account and storage trie accesses.
|
||||||
type WitnessStats struct {
|
type WitnessStats struct {
|
||||||
accountTrieLeaves [16]int64
|
accountTrie *trie.LevelStats
|
||||||
storageTrieLeaves [16]int64
|
storageTrie *trie.LevelStats
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewWitnessStats creates a new WitnessStats collector.
|
// NewWitnessStats creates a new WitnessStats collector.
|
||||||
func NewWitnessStats() *WitnessStats {
|
func NewWitnessStats() *WitnessStats {
|
||||||
return &WitnessStats{}
|
return &WitnessStats{
|
||||||
|
accountTrie: trie.NewLevelStats(),
|
||||||
|
storageTrie: trie.NewLevelStats(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WitnessStats) init() {
|
||||||
|
if s.accountTrie == nil {
|
||||||
|
s.accountTrie = trie.NewLevelStats()
|
||||||
|
}
|
||||||
|
if s.storageTrie == nil {
|
||||||
|
s.storageTrie = trie.NewLevelStats()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add records trie access depths from the given node paths.
|
// Add records trie access depths from the given node paths.
|
||||||
// If `owner` is the zero hash, accesses are attributed to the account trie;
|
// If `owner` is the zero hash, accesses are attributed to the account trie;
|
||||||
// otherwise, they are attributed to the storage trie of that account.
|
// otherwise, they are attributed to the storage trie of that account.
|
||||||
func (s *WitnessStats) Add(nodes map[string][]byte, owner common.Hash) {
|
func (s *WitnessStats) Add(nodes map[string][]byte, owner common.Hash) {
|
||||||
// Extract paths from the nodes map
|
s.init()
|
||||||
|
|
||||||
|
// Extract paths from the nodes map.
|
||||||
paths := slices.Collect(maps.Keys(nodes))
|
paths := slices.Collect(maps.Keys(nodes))
|
||||||
sort.Strings(paths)
|
sort.Strings(paths)
|
||||||
|
|
||||||
|
ownerStat := s.accountTrie
|
||||||
|
if owner != (common.Hash{}) {
|
||||||
|
ownerStat = s.storageTrie
|
||||||
|
}
|
||||||
|
|
||||||
for i, path := range paths {
|
for i, path := range paths {
|
||||||
// If current path is a prefix of the next path, it's not a leaf.
|
// If current path is a prefix of the next path, it's not a leaf.
|
||||||
// The last path is always a leaf.
|
// The last path is always a leaf.
|
||||||
if i == len(paths)-1 || !strings.HasPrefix(paths[i+1], paths[i]) {
|
if i == len(paths)-1 || !strings.HasPrefix(paths[i+1], paths[i]) {
|
||||||
depth := len(path)
|
ownerStat.AddLeaf(len(path))
|
||||||
if owner == (common.Hash{}) {
|
|
||||||
if depth >= len(s.accountTrieLeaves) {
|
|
||||||
depth = len(s.accountTrieLeaves) - 1
|
|
||||||
}
|
|
||||||
s.accountTrieLeaves[depth] += 1
|
|
||||||
} else {
|
|
||||||
if depth >= len(s.storageTrieLeaves) {
|
|
||||||
depth = len(s.storageTrieLeaves) - 1
|
|
||||||
}
|
|
||||||
s.storageTrieLeaves[depth] += 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReportMetrics reports the collected statistics to the global metrics registry.
|
// ReportMetrics reports the collected statistics to the global metrics registry.
|
||||||
func (s *WitnessStats) ReportMetrics(blockNumber uint64) {
|
func (s *WitnessStats) ReportMetrics(blockNumber uint64) {
|
||||||
// Encode the metrics as JSON for easier consumption
|
s.init()
|
||||||
accountLeavesJson, _ := json.Marshal(s.accountTrieLeaves)
|
|
||||||
storageLeavesJson, _ := json.Marshal(s.storageTrieLeaves)
|
|
||||||
|
|
||||||
// Log account trie depth statistics
|
accountTrieLeaves := s.accountTrie.LeafDepths()
|
||||||
log.Info("Account trie depth stats",
|
storageTrieLeaves := s.storageTrie.LeafDepths()
|
||||||
"block", blockNumber,
|
|
||||||
"leavesAtDepth", string(accountLeavesJson))
|
|
||||||
log.Info("Storage trie depth stats",
|
|
||||||
"block", blockNumber,
|
|
||||||
"leavesAtDepth", string(storageLeavesJson))
|
|
||||||
|
|
||||||
for i := 0; i < 16; i++ {
|
// Encode the metrics as JSON for easier consumption.
|
||||||
accountTrieLeavesAtDepth[i].Inc(s.accountTrieLeaves[i])
|
accountLeavesJSON, _ := json.Marshal(accountTrieLeaves)
|
||||||
storageTrieLeavesAtDepth[i].Inc(s.storageTrieLeaves[i])
|
storageLeavesJSON, _ := json.Marshal(storageTrieLeaves)
|
||||||
|
|
||||||
|
// Log account trie depth statistics.
|
||||||
|
log.Info("Account trie depth stats", "block", blockNumber, "leavesAtDepth", string(accountLeavesJSON))
|
||||||
|
log.Info("Storage trie depth stats", "block", blockNumber, "leavesAtDepth", string(storageLeavesJSON))
|
||||||
|
|
||||||
|
for i := 0; i < len(accountTrieLeavesAtDepth); i++ {
|
||||||
|
accountTrieLeavesAtDepth[i].Inc(accountTrieLeaves[i])
|
||||||
|
storageTrieLeavesAtDepth[i].Inc(storageTrieLeaves[i])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,18 +17,27 @@
|
||||||
package stateless
|
package stateless
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func expectedLeaves(counts map[int]int64) [16]int64 {
|
||||||
|
var leaves [16]int64
|
||||||
|
for depth, count := range counts {
|
||||||
|
leaves[depth] = count
|
||||||
|
}
|
||||||
|
return leaves
|
||||||
|
}
|
||||||
|
|
||||||
func TestWitnessStatsAdd(t *testing.T) {
|
func TestWitnessStatsAdd(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
nodes map[string][]byte
|
nodes map[string][]byte
|
||||||
owner common.Hash
|
owner common.Hash
|
||||||
expectedAccountLeaves map[int64]int64
|
expectedAccountLeaves map[int]int64
|
||||||
expectedStorageLeaves map[int64]int64
|
expectedStorageLeaves map[int]int64
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "empty nodes",
|
name: "empty nodes",
|
||||||
|
|
@ -41,7 +50,7 @@ func TestWitnessStatsAdd(t *testing.T) {
|
||||||
"": []byte("data"),
|
"": []byte("data"),
|
||||||
},
|
},
|
||||||
owner: common.Hash{},
|
owner: common.Hash{},
|
||||||
expectedAccountLeaves: map[int64]int64{0: 1},
|
expectedAccountLeaves: map[int]int64{0: 1},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "single account trie leaf",
|
name: "single account trie leaf",
|
||||||
|
|
@ -49,7 +58,7 @@ func TestWitnessStatsAdd(t *testing.T) {
|
||||||
"abc": []byte("data"),
|
"abc": []byte("data"),
|
||||||
},
|
},
|
||||||
owner: common.Hash{},
|
owner: common.Hash{},
|
||||||
expectedAccountLeaves: map[int64]int64{3: 1},
|
expectedAccountLeaves: map[int]int64{3: 1},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "account trie with internal nodes",
|
name: "account trie with internal nodes",
|
||||||
|
|
@ -59,7 +68,7 @@ func TestWitnessStatsAdd(t *testing.T) {
|
||||||
"abc": []byte("data3"),
|
"abc": []byte("data3"),
|
||||||
},
|
},
|
||||||
owner: common.Hash{},
|
owner: common.Hash{},
|
||||||
expectedAccountLeaves: map[int64]int64{3: 1}, // Only "abc" is a leaf
|
expectedAccountLeaves: map[int]int64{3: 1}, // Only "abc" is a leaf
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "multiple account trie branches",
|
name: "multiple account trie branches",
|
||||||
|
|
@ -72,7 +81,7 @@ func TestWitnessStatsAdd(t *testing.T) {
|
||||||
"bcd": []byte("data6"),
|
"bcd": []byte("data6"),
|
||||||
},
|
},
|
||||||
owner: common.Hash{},
|
owner: common.Hash{},
|
||||||
expectedAccountLeaves: map[int64]int64{3: 2}, // "abc" (3) + "bcd" (3)
|
expectedAccountLeaves: map[int]int64{3: 2}, // "abc" (3) + "bcd" (3)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "siblings are all leaves",
|
name: "siblings are all leaves",
|
||||||
|
|
@ -82,7 +91,7 @@ func TestWitnessStatsAdd(t *testing.T) {
|
||||||
"ac": []byte("data3"),
|
"ac": []byte("data3"),
|
||||||
},
|
},
|
||||||
owner: common.Hash{},
|
owner: common.Hash{},
|
||||||
expectedAccountLeaves: map[int64]int64{2: 3},
|
expectedAccountLeaves: map[int]int64{2: 3},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "storage trie leaves",
|
name: "storage trie leaves",
|
||||||
|
|
@ -93,7 +102,7 @@ func TestWitnessStatsAdd(t *testing.T) {
|
||||||
"124": []byte("data4"),
|
"124": []byte("data4"),
|
||||||
},
|
},
|
||||||
owner: common.HexToHash("0x1234"),
|
owner: common.HexToHash("0x1234"),
|
||||||
expectedStorageLeaves: map[int64]int64{3: 2}, // "123" (3) + "124" (3)
|
expectedStorageLeaves: map[int]int64{3: 2}, // "123" (3) + "124" (3)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "complex trie structure",
|
name: "complex trie structure",
|
||||||
|
|
@ -109,7 +118,7 @@ func TestWitnessStatsAdd(t *testing.T) {
|
||||||
"3": []byte("data9"),
|
"3": []byte("data9"),
|
||||||
},
|
},
|
||||||
owner: common.Hash{},
|
owner: common.Hash{},
|
||||||
expectedAccountLeaves: map[int64]int64{1: 1, 3: 4}, // "123"(3) + "124"(3) + "234"(3) + "235"(3) + "3"(1)
|
expectedAccountLeaves: map[int]int64{1: 1, 3: 4}, // "123"(3) + "124"(3) + "234"(3) + "235"(3) + "3"(1)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -118,32 +127,59 @@ func TestWitnessStatsAdd(t *testing.T) {
|
||||||
stats := NewWitnessStats()
|
stats := NewWitnessStats()
|
||||||
stats.Add(tt.nodes, tt.owner)
|
stats.Add(tt.nodes, tt.owner)
|
||||||
|
|
||||||
var expectedAccountTrieLeaves [16]int64
|
if got, want := stats.accountTrie.LeafDepths(), expectedLeaves(tt.expectedAccountLeaves); got != want {
|
||||||
for depth, count := range tt.expectedAccountLeaves {
|
t.Errorf("account trie leaves = %v, want %v", got, want)
|
||||||
expectedAccountTrieLeaves[depth] = count
|
|
||||||
}
|
}
|
||||||
var expectedStorageTrieLeaves [16]int64
|
if got, want := stats.storageTrie.LeafDepths(), expectedLeaves(tt.expectedStorageLeaves); got != want {
|
||||||
for depth, count := range tt.expectedStorageLeaves {
|
t.Errorf("storage trie leaves = %v, want %v", got, want)
|
||||||
expectedStorageTrieLeaves[depth] = count
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check account trie depth
|
|
||||||
if stats.accountTrieLeaves != expectedAccountTrieLeaves {
|
|
||||||
t.Errorf("Account trie total depth = %v, want %v", stats.accountTrieLeaves, expectedAccountTrieLeaves)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check storage trie depth
|
|
||||||
if stats.storageTrieLeaves != expectedStorageTrieLeaves {
|
|
||||||
t.Errorf("Storage trie total depth = %v, want %v", stats.storageTrieLeaves, expectedStorageTrieLeaves)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestWitnessStatsStorageTrieAggregation(t *testing.T) {
|
||||||
|
stats := NewWitnessStats()
|
||||||
|
ownerA := common.HexToHash("0xa")
|
||||||
|
ownerB := common.HexToHash("0xb")
|
||||||
|
|
||||||
|
stats.Add(map[string][]byte{
|
||||||
|
"a": []byte("data1"),
|
||||||
|
"ab": []byte("data2"),
|
||||||
|
"abc": []byte("data3"),
|
||||||
|
}, ownerA)
|
||||||
|
stats.Add(map[string][]byte{
|
||||||
|
"xy": []byte("data4"),
|
||||||
|
}, ownerA)
|
||||||
|
stats.Add(map[string][]byte{
|
||||||
|
"1": []byte("data5"),
|
||||||
|
"12": []byte("data6"),
|
||||||
|
"123": []byte("data7"),
|
||||||
|
"124": []byte("data8"),
|
||||||
|
}, ownerB)
|
||||||
|
|
||||||
|
if got, want := stats.storageTrie.LeafDepths(), expectedLeaves(map[int]int64{2: 1, 3: 3}); got != want {
|
||||||
|
t.Errorf("storage leaves = %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
if got, want := stats.accountTrie.LeafDepths(), expectedLeaves(nil); got != want {
|
||||||
|
t.Errorf("account leaves = %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWitnessStatsPanicsOnDeepLeaf(t *testing.T) {
|
||||||
|
stats := NewWitnessStats()
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r == nil {
|
||||||
|
t.Fatal("expected panic for depth >= 16")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
stats.Add(map[string][]byte{strings.Repeat("a", 16): []byte("data")}, common.Hash{})
|
||||||
|
}
|
||||||
|
|
||||||
func TestWitnessStatsMinMax(t *testing.T) {
|
func TestWitnessStatsMinMax(t *testing.T) {
|
||||||
stats := NewWitnessStats()
|
stats := NewWitnessStats()
|
||||||
|
|
||||||
// Add some account trie nodes with varying depths
|
// Add some account trie nodes with varying depths.
|
||||||
stats.Add(map[string][]byte{
|
stats.Add(map[string][]byte{
|
||||||
"a": []byte("data1"),
|
"a": []byte("data1"),
|
||||||
"ab": []byte("data2"),
|
"ab": []byte("data2"),
|
||||||
|
|
@ -152,21 +188,21 @@ func TestWitnessStatsMinMax(t *testing.T) {
|
||||||
"abcde": []byte("data5"),
|
"abcde": []byte("data5"),
|
||||||
}, common.Hash{})
|
}, common.Hash{})
|
||||||
|
|
||||||
// Only "abcde" is a leaf (depth 5)
|
// Only "abcde" is a leaf (depth 5).
|
||||||
for i, v := range stats.accountTrieLeaves {
|
for i, v := range stats.accountTrie.LeafDepths() {
|
||||||
if v != 0 && i != 5 {
|
if v != 0 && i != 5 {
|
||||||
t.Errorf("leaf found at invalid depth %d", i)
|
t.Errorf("leaf found at invalid depth %d", i)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add more leaves with different depths
|
// Add more leaves with different depths.
|
||||||
stats.Add(map[string][]byte{
|
stats.Add(map[string][]byte{
|
||||||
"x": []byte("data6"),
|
"x": []byte("data6"),
|
||||||
"yz": []byte("data7"),
|
"yz": []byte("data7"),
|
||||||
}, common.Hash{})
|
}, common.Hash{})
|
||||||
|
|
||||||
// Now we have leaves at depths 1, 2, and 5
|
// Now we have leaves at depths 1, 2, and 5.
|
||||||
for i, v := range stats.accountTrieLeaves {
|
for i, v := range stats.accountTrie.LeafDepths() {
|
||||||
if v != 0 && (i != 5 && i != 2 && i != 1) {
|
if v != 0 && (i != 5 && i != 2 && i != 1) {
|
||||||
t.Errorf("leaf found at invalid depth %d", i)
|
t.Errorf("leaf found at invalid depth %d", i)
|
||||||
}
|
}
|
||||||
|
|
@ -176,7 +212,7 @@ func TestWitnessStatsMinMax(t *testing.T) {
|
||||||
func TestWitnessStatsAverage(t *testing.T) {
|
func TestWitnessStatsAverage(t *testing.T) {
|
||||||
stats := NewWitnessStats()
|
stats := NewWitnessStats()
|
||||||
|
|
||||||
// Add nodes that will create leaves at depths 2, 3, and 4
|
// Add nodes that will create leaves at depths 2, 3, and 4.
|
||||||
stats.Add(map[string][]byte{
|
stats.Add(map[string][]byte{
|
||||||
"aa": []byte("data1"),
|
"aa": []byte("data1"),
|
||||||
"bb": []byte("data2"),
|
"bb": []byte("data2"),
|
||||||
|
|
@ -184,22 +220,22 @@ func TestWitnessStatsAverage(t *testing.T) {
|
||||||
"dddd": []byte("data4"),
|
"dddd": []byte("data4"),
|
||||||
}, common.Hash{})
|
}, common.Hash{})
|
||||||
|
|
||||||
// All are leaves: 2 + 2 + 3 + 4 = 11 total, 4 samples
|
// All are leaves: 2 + 2 + 3 + 4 = 11 total, 4 samples.
|
||||||
expectedAvg := int64(11) / int64(4)
|
expectedAvg := int64(11) / int64(4)
|
||||||
var actualAvg, totalSamples int64
|
var actualAvg, totalSamples int64
|
||||||
for i, c := range stats.accountTrieLeaves {
|
for i, c := range stats.accountTrie.LeafDepths() {
|
||||||
actualAvg += c * int64(i)
|
actualAvg += c * int64(i)
|
||||||
totalSamples += c
|
totalSamples += c
|
||||||
}
|
}
|
||||||
actualAvg = actualAvg / totalSamples
|
actualAvg = actualAvg / totalSamples
|
||||||
|
|
||||||
if actualAvg != expectedAvg {
|
if actualAvg != expectedAvg {
|
||||||
t.Errorf("Account trie average depth = %d, want %d", actualAvg, expectedAvg)
|
t.Errorf("account trie average depth = %d, want %d", actualAvg, expectedAvg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkWitnessStatsAdd(b *testing.B) {
|
func BenchmarkWitnessStatsAdd(b *testing.B) {
|
||||||
// Create a realistic trie node structure
|
// Create a realistic trie node structure.
|
||||||
nodes := make(map[string][]byte)
|
nodes := make(map[string][]byte)
|
||||||
for i := 0; i < 100; i++ {
|
for i := 0; i < 100; i++ {
|
||||||
base := string(rune('a' + i%26))
|
base := string(rune('a' + i%26))
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
// Naive stub implementation for tablewriter
|
// Naive stub implementation for tablewriter
|
||||||
|
|
||||||
package rawdb
|
package tablewriter
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
|
@ -37,7 +37,7 @@ type Table struct {
|
||||||
rows [][]string
|
rows [][]string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTableWriter(w io.Writer) *Table {
|
func NewWriter(w io.Writer) *Table {
|
||||||
return &Table{out: w}
|
return &Table{out: w}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -58,12 +58,12 @@ func (t *Table) SetFooter(footer []string) {
|
||||||
t.footer = footer
|
t.footer = footer
|
||||||
}
|
}
|
||||||
|
|
||||||
// AppendBulk sets all data rows for the table at once, replacing any existing rows.
|
// AppendBulk appends one or more data rows to the table.
|
||||||
//
|
//
|
||||||
// Each row must have the same number of columns as the headers, or validation
|
// Each row must have the same number of columns as the headers, or validation
|
||||||
// will fail during Render().
|
// will fail during Render().
|
||||||
func (t *Table) AppendBulk(rows [][]string) {
|
func (t *Table) AppendBulk(rows [][]string) {
|
||||||
t.rows = rows
|
t.rows = append(t.rows, rows...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render outputs the complete table to the configured writer. The table is rendered
|
// Render outputs the complete table to the configured writer. The table is rendered
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
// 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/>.
|
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package rawdb
|
package tablewriter
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
|
@ -24,7 +24,7 @@ import (
|
||||||
|
|
||||||
func TestTableWriterTinyGo(t *testing.T) {
|
func TestTableWriterTinyGo(t *testing.T) {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
table := NewTableWriter(&buf)
|
table := NewWriter(&buf)
|
||||||
|
|
||||||
headers := []string{"Database", "Size", "Items", "Status"}
|
headers := []string{"Database", "Size", "Items", "Status"}
|
||||||
rows := [][]string{
|
rows := [][]string{
|
||||||
|
|
@ -48,7 +48,7 @@ func TestTableWriterValidationErrors(t *testing.T) {
|
||||||
// Test missing headers
|
// Test missing headers
|
||||||
t.Run("MissingHeaders", func(t *testing.T) {
|
t.Run("MissingHeaders", func(t *testing.T) {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
table := NewTableWriter(&buf)
|
table := NewWriter(&buf)
|
||||||
|
|
||||||
rows := [][]string{{"x", "y", "z"}}
|
rows := [][]string{{"x", "y", "z"}}
|
||||||
|
|
||||||
|
|
@ -63,7 +63,7 @@ func TestTableWriterValidationErrors(t *testing.T) {
|
||||||
|
|
||||||
t.Run("NotEnoughRowColumns", func(t *testing.T) {
|
t.Run("NotEnoughRowColumns", func(t *testing.T) {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
table := NewTableWriter(&buf)
|
table := NewWriter(&buf)
|
||||||
|
|
||||||
headers := []string{"A", "B", "C"}
|
headers := []string{"A", "B", "C"}
|
||||||
badRows := [][]string{
|
badRows := [][]string{
|
||||||
|
|
@ -82,7 +82,7 @@ func TestTableWriterValidationErrors(t *testing.T) {
|
||||||
|
|
||||||
t.Run("TooManyRowColumns", func(t *testing.T) {
|
t.Run("TooManyRowColumns", func(t *testing.T) {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
table := NewTableWriter(&buf)
|
table := NewWriter(&buf)
|
||||||
|
|
||||||
headers := []string{"A", "B", "C"}
|
headers := []string{"A", "B", "C"}
|
||||||
badRows := [][]string{
|
badRows := [][]string{
|
||||||
|
|
@ -102,7 +102,7 @@ func TestTableWriterValidationErrors(t *testing.T) {
|
||||||
// Test mismatched footer columns
|
// Test mismatched footer columns
|
||||||
t.Run("MismatchedFooterColumns", func(t *testing.T) {
|
t.Run("MismatchedFooterColumns", func(t *testing.T) {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
table := NewTableWriter(&buf)
|
table := NewWriter(&buf)
|
||||||
|
|
||||||
headers := []string{"A", "B", "C"}
|
headers := []string{"A", "B", "C"}
|
||||||
rows := [][]string{{"x", "y", "z"}}
|
rows := [][]string{{"x", "y", "z"}}
|
||||||
930
trie/inspect.go
Normal file
930
trie/inspect.go
Normal file
|
|
@ -0,0 +1,930 @@
|
||||||
|
// 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 trie
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"cmp"
|
||||||
|
"container/heap"
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"slices"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/core/rawdb"
|
||||||
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
|
"github.com/ethereum/go-ethereum/ethdb"
|
||||||
|
"github.com/ethereum/go-ethereum/internal/tablewriter"
|
||||||
|
"github.com/ethereum/go-ethereum/log"
|
||||||
|
"github.com/ethereum/go-ethereum/rlp"
|
||||||
|
"github.com/ethereum/go-ethereum/triedb/database"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
|
"golang.org/x/sync/semaphore"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
inspectDumpRecordSize = 32 + trieStatLevels*(3*4+8)
|
||||||
|
inspectDefaultTopN = 10
|
||||||
|
inspectParallelism = int64(16)
|
||||||
|
)
|
||||||
|
|
||||||
|
// inspector is used by the inner inspect function to coordinate across threads.
|
||||||
|
type inspector struct {
|
||||||
|
triedb database.NodeDatabase
|
||||||
|
root common.Hash
|
||||||
|
|
||||||
|
config *InspectConfig
|
||||||
|
accountStat *LevelStats
|
||||||
|
|
||||||
|
sem *semaphore.Weighted
|
||||||
|
|
||||||
|
// Pass 1: dump file writer.
|
||||||
|
dumpMu sync.Mutex
|
||||||
|
dumpBuf *bufio.Writer
|
||||||
|
dumpFile *os.File
|
||||||
|
storageRecordsWritten atomic.Uint64
|
||||||
|
|
||||||
|
errMu sync.Mutex
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
// InspectConfig is a set of options to control inspection and format the output.
|
||||||
|
// TopN determines the maximum number of entries retained for each top-list.
|
||||||
|
// Path controls optional JSON output. DumpPath controls the pass-1 dump location.
|
||||||
|
type InspectConfig struct {
|
||||||
|
NoStorage bool
|
||||||
|
TopN int
|
||||||
|
Path string
|
||||||
|
DumpPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inspect walks the trie with the given root and records the number and type of
|
||||||
|
// nodes at each depth. Storage trie stats are streamed to disk in fixed-size
|
||||||
|
// records, then summarized in a second pass.
|
||||||
|
func Inspect(triedb database.NodeDatabase, root common.Hash, config *InspectConfig) error {
|
||||||
|
trie, err := New(TrieID(root), triedb)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("fail to open trie %s: %w", root, err)
|
||||||
|
}
|
||||||
|
config = normalizeInspectConfig(config)
|
||||||
|
|
||||||
|
dumpFile, err := os.OpenFile(config.DumpPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create trie dump %s: %w", config.DumpPath, err)
|
||||||
|
}
|
||||||
|
in := inspector{
|
||||||
|
triedb: triedb,
|
||||||
|
root: root,
|
||||||
|
config: config,
|
||||||
|
accountStat: NewLevelStats(),
|
||||||
|
sem: semaphore.NewWeighted(inspectParallelism),
|
||||||
|
dumpBuf: bufio.NewWriterSize(dumpFile, 1<<20),
|
||||||
|
dumpFile: dumpFile,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start progress reporter
|
||||||
|
start := time.Now()
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(8 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
accountNodes := in.accountStat.TotalNodes()
|
||||||
|
storageRecords := in.storageRecordsWritten.Load()
|
||||||
|
log.Info("Inspecting trie",
|
||||||
|
"accountNodes", accountNodes,
|
||||||
|
"storageRecords", storageRecords,
|
||||||
|
"elapsed", common.PrettyDuration(time.Since(start)))
|
||||||
|
case <-done:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
in.recordRootSize(trie, root, in.accountStat)
|
||||||
|
in.inspect(trie, trie.root, 0, []byte{}, in.accountStat)
|
||||||
|
|
||||||
|
// inspect is synchronous: it waits for all spawned goroutines in its
|
||||||
|
// subtree before returning, so no additional wait is needed here.
|
||||||
|
|
||||||
|
// Persist account trie stats as the sentinel record.
|
||||||
|
in.writeDumpRecord(common.Hash{}, in.accountStat)
|
||||||
|
if err := in.closeDump(); err != nil {
|
||||||
|
in.setError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop progress reporter
|
||||||
|
close(done)
|
||||||
|
|
||||||
|
if err := in.getError(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return Summarize(config.DumpPath, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// InspectContract inspects the on-disk footprint of a single contract.
|
||||||
|
// It reports snapshot storage (slot count + size) and storage trie node
|
||||||
|
// statistics (node type breakdown and per-depth distribution).
|
||||||
|
func InspectContract(triedb database.NodeDatabase, db ethdb.Database, stateRoot common.Hash, address common.Address) error {
|
||||||
|
// Resolve account from the state trie.
|
||||||
|
accountHash := crypto.Keccak256Hash(address.Bytes())
|
||||||
|
accountTrie, err := New(TrieID(stateRoot), triedb)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open account trie: %w", err)
|
||||||
|
}
|
||||||
|
accountRLP, err := accountTrie.Get(crypto.Keccak256(address.Bytes()))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read account: %w", err)
|
||||||
|
}
|
||||||
|
if accountRLP == nil {
|
||||||
|
return fmt.Errorf("account not found: %s", address)
|
||||||
|
}
|
||||||
|
var account types.StateAccount
|
||||||
|
if err := rlp.DecodeBytes(accountRLP, &account); err != nil {
|
||||||
|
return fmt.Errorf("failed to decode account: %w", err)
|
||||||
|
}
|
||||||
|
if account.Root == (common.Hash{}) || account.Root == types.EmptyRootHash {
|
||||||
|
return fmt.Errorf("account %s has no storage", address)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look up account snapshot.
|
||||||
|
accountData := rawdb.ReadAccountSnapshot(db, accountHash)
|
||||||
|
|
||||||
|
// Run trie walk + snap iteration in parallel.
|
||||||
|
var (
|
||||||
|
snapSlots atomic.Uint64
|
||||||
|
snapSize atomic.Uint64
|
||||||
|
g errgroup.Group
|
||||||
|
start = time.Now()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Goroutine 1: Snapshot storage iteration.
|
||||||
|
g.Go(func() error {
|
||||||
|
prefix := append(rawdb.SnapshotStoragePrefix, accountHash.Bytes()...)
|
||||||
|
it := db.NewIterator(prefix, nil)
|
||||||
|
defer it.Release()
|
||||||
|
|
||||||
|
for it.Next() {
|
||||||
|
if !bytes.HasPrefix(it.Key(), prefix) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
snapSlots.Add(1)
|
||||||
|
snapSize.Add(uint64(len(it.Key()) + len(it.Value())))
|
||||||
|
}
|
||||||
|
return it.Error()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Goroutine 2: Storage trie walk using the existing inspector.
|
||||||
|
storageStat := NewLevelStats()
|
||||||
|
g.Go(func() error {
|
||||||
|
owner := accountHash
|
||||||
|
storage, err := New(StorageTrieID(stateRoot, owner, account.Root), triedb)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open storage trie: %w", err)
|
||||||
|
}
|
||||||
|
in := &inspector{
|
||||||
|
triedb: triedb,
|
||||||
|
root: stateRoot,
|
||||||
|
config: &InspectConfig{NoStorage: true},
|
||||||
|
accountStat: NewLevelStats(), // unused, but needed by inspector
|
||||||
|
sem: semaphore.NewWeighted(inspectParallelism),
|
||||||
|
dumpBuf: bufio.NewWriter(io.Discard),
|
||||||
|
}
|
||||||
|
|
||||||
|
in.recordRootSize(storage, account.Root, storageStat)
|
||||||
|
in.inspect(storage, storage.root, 0, []byte{}, storageStat)
|
||||||
|
|
||||||
|
if err := in.getError(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// Progress reporter.
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(8 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
log.Info("Inspecting contract",
|
||||||
|
"snapSlots", snapSlots.Load(),
|
||||||
|
"trieNodes", storageStat.TotalNodes(),
|
||||||
|
"elapsed", common.PrettyDuration(time.Since(start)))
|
||||||
|
case <-done:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := g.Wait(); err != nil {
|
||||||
|
close(done)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
close(done)
|
||||||
|
|
||||||
|
// Display results.
|
||||||
|
fmt.Printf("\n=== Contract Inspection: %s ===\n", address)
|
||||||
|
fmt.Printf("Account hash: %s\n\n", accountHash)
|
||||||
|
|
||||||
|
if len(accountData) == 0 {
|
||||||
|
fmt.Println("Account snapshot: not found")
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Account snapshot: %s\n", common.StorageSize(len(accountData)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Snapshot storage: %d slots (%s)\n",
|
||||||
|
snapSlots.Load(), common.StorageSize(snapSize.Load()))
|
||||||
|
|
||||||
|
// Compute trie totals from LevelStats.
|
||||||
|
var trieTotal, trieSize uint64
|
||||||
|
for i := 0; i < trieStatLevels; i++ {
|
||||||
|
short, full, value, size := storageStat.level[i].load()
|
||||||
|
trieTotal += short + full + value
|
||||||
|
trieSize += size
|
||||||
|
}
|
||||||
|
fmt.Printf("Storage trie: %d nodes (%s)\n", trieTotal, common.StorageSize(trieSize))
|
||||||
|
|
||||||
|
// Depth distribution table with node type columns.
|
||||||
|
fmt.Println("\nStorage Trie Depth Distribution:")
|
||||||
|
b := new(strings.Builder)
|
||||||
|
table := tablewriter.NewWriter(b)
|
||||||
|
table.SetHeader([]string{"Depth", "Short", "Full", "Value", "Nodes", "Size"})
|
||||||
|
for i := 0; i < trieStatLevels; i++ {
|
||||||
|
short, full, value, size := storageStat.level[i].load()
|
||||||
|
total := short + full + value
|
||||||
|
if total == 0 && size == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
table.AppendBulk([][]string{{
|
||||||
|
fmt.Sprint(i),
|
||||||
|
fmt.Sprint(short),
|
||||||
|
fmt.Sprint(full),
|
||||||
|
fmt.Sprint(value),
|
||||||
|
fmt.Sprint(total),
|
||||||
|
common.StorageSize(size).String(),
|
||||||
|
}})
|
||||||
|
}
|
||||||
|
table.Render()
|
||||||
|
fmt.Print(b.String())
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeInspectConfig(config *InspectConfig) *InspectConfig {
|
||||||
|
if config == nil {
|
||||||
|
config = &InspectConfig{}
|
||||||
|
}
|
||||||
|
if config.TopN <= 0 {
|
||||||
|
config.TopN = inspectDefaultTopN
|
||||||
|
}
|
||||||
|
if config.DumpPath == "" {
|
||||||
|
config.DumpPath = "trie-dump.bin"
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (in *inspector) recordRootSize(trie *Trie, root common.Hash, stat *LevelStats) {
|
||||||
|
if root == (common.Hash{}) || root == types.EmptyRootHash {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
blob := trie.prevalueTracer.Get(nil)
|
||||||
|
if len(blob) == 0 {
|
||||||
|
resolved, err := trie.reader.Node(nil, root)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Failed to read trie root for size accounting", "trie", trie.Hash(), "root", root, "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
blob = resolved
|
||||||
|
}
|
||||||
|
stat.addSize(0, uint64(len(blob)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (in *inspector) closeDump() error {
|
||||||
|
var ret error
|
||||||
|
if in.dumpBuf != nil {
|
||||||
|
if err := in.dumpBuf.Flush(); err != nil {
|
||||||
|
ret = errors.Join(ret, fmt.Errorf("failed to flush trie dump %s: %w", in.config.DumpPath, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if in.dumpFile != nil {
|
||||||
|
if err := in.dumpFile.Close(); err != nil {
|
||||||
|
ret = errors.Join(ret, fmt.Errorf("failed to close trie dump %s: %w", in.config.DumpPath, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func (in *inspector) setError(err error) {
|
||||||
|
if err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
in.errMu.Lock()
|
||||||
|
defer in.errMu.Unlock()
|
||||||
|
in.err = errors.Join(in.err, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (in *inspector) getError() error {
|
||||||
|
in.errMu.Lock()
|
||||||
|
defer in.errMu.Unlock()
|
||||||
|
return in.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (in *inspector) hasError() bool {
|
||||||
|
return in.getError() != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// trySpawn attempts to run fn in a new goroutine, bounded by the semaphore.
|
||||||
|
// If a slot is available the goroutine is started and tracked via wg; the
|
||||||
|
// caller must call wg.Wait() before reading any state written by fn.
|
||||||
|
// Returns false (and does not start a goroutine) when no slot is available.
|
||||||
|
func (in *inspector) trySpawn(wg *sync.WaitGroup, fn func()) bool {
|
||||||
|
if !in.sem.TryAcquire(1) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer in.sem.Release(1)
|
||||||
|
defer wg.Done()
|
||||||
|
fn()
|
||||||
|
}()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (in *inspector) writeDumpRecord(owner common.Hash, s *LevelStats) {
|
||||||
|
if in.hasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var buf [inspectDumpRecordSize]byte
|
||||||
|
copy(buf[:32], owner[:])
|
||||||
|
|
||||||
|
off := 32
|
||||||
|
for i := 0; i < trieStatLevels; i++ {
|
||||||
|
binary.LittleEndian.PutUint32(buf[off:], uint32(s.level[i].short.Load()))
|
||||||
|
off += 4
|
||||||
|
binary.LittleEndian.PutUint32(buf[off:], uint32(s.level[i].full.Load()))
|
||||||
|
off += 4
|
||||||
|
binary.LittleEndian.PutUint32(buf[off:], uint32(s.level[i].value.Load()))
|
||||||
|
off += 4
|
||||||
|
binary.LittleEndian.PutUint64(buf[off:], s.level[i].size.Load())
|
||||||
|
off += 8
|
||||||
|
}
|
||||||
|
in.dumpMu.Lock()
|
||||||
|
_, err := in.dumpBuf.Write(buf[:])
|
||||||
|
in.dumpMu.Unlock()
|
||||||
|
if err != nil {
|
||||||
|
in.setError(fmt.Errorf("failed writing trie dump record: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increment counter for storage tries only (not for account trie)
|
||||||
|
if owner != (common.Hash{}) {
|
||||||
|
in.storageRecordsWritten.Add(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// inspect walks the trie rooted at n and records node statistics into stat.
|
||||||
|
// It may spawn goroutines for subtrees, but always waits for them before
|
||||||
|
// returning — the caller sees a fully-populated stat when inspect returns.
|
||||||
|
func (in *inspector) inspect(trie *Trie, n node, height uint32, path []byte, stat *LevelStats) {
|
||||||
|
if n == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// wg tracks goroutines spawned by this call so we can wait for them
|
||||||
|
// before returning. This guarantees stat is complete when we return,
|
||||||
|
// which is critical for storage tries that write their dump record
|
||||||
|
// immediately after inspect returns.
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
// Four types of nodes can be encountered:
|
||||||
|
// - short: extend path with key, inspect single value.
|
||||||
|
// - full: inspect all 17 children, spin up new threads when possible.
|
||||||
|
// - hash: need to resolve node from disk, retry inspect on result.
|
||||||
|
// - value: if account, begin inspecting storage trie.
|
||||||
|
switch n := (n).(type) {
|
||||||
|
case *shortNode:
|
||||||
|
nextPath := slices.Concat(path, n.Key)
|
||||||
|
in.inspect(trie, n.Val, height+1, nextPath, stat)
|
||||||
|
case *fullNode:
|
||||||
|
for idx, child := range n.Children {
|
||||||
|
if child == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
childPath := slices.Concat(path, []byte{byte(idx)})
|
||||||
|
childNode := child
|
||||||
|
if in.trySpawn(&wg, func() {
|
||||||
|
in.inspect(trie, childNode, height+1, childPath, stat)
|
||||||
|
}) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
in.inspect(trie, childNode, height+1, childPath, stat)
|
||||||
|
}
|
||||||
|
case hashNode:
|
||||||
|
blob, err := trie.reader.Node(path, common.BytesToHash(n))
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Failed to resolve HashNode", "err", err, "trie", trie.Hash(), "height", height+1, "path", path)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
stat.addSize(height, uint64(len(blob)))
|
||||||
|
resolved := mustDecodeNode(n, blob)
|
||||||
|
in.inspect(trie, resolved, height, path, stat)
|
||||||
|
|
||||||
|
// Return early here so this level isn't recorded twice.
|
||||||
|
return
|
||||||
|
case valueNode:
|
||||||
|
if !hasTerm(path) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
var account types.StateAccount
|
||||||
|
if err := rlp.Decode(bytes.NewReader(n), &account); err != nil {
|
||||||
|
// Not an account value.
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if account.Root == (common.Hash{}) || account.Root == types.EmptyRootHash {
|
||||||
|
// Account is empty, nothing further to inspect.
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if !in.config.NoStorage {
|
||||||
|
owner := common.BytesToHash(hexToCompact(path))
|
||||||
|
storage, err := New(StorageTrieID(in.root, owner, account.Root), in.triedb)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Failed to open account storage trie", "node", n, "error", err, "height", height, "path", common.Bytes2Hex(path))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
storageStat := NewLevelStats()
|
||||||
|
run := func() {
|
||||||
|
in.recordRootSize(storage, account.Root, storageStat)
|
||||||
|
in.inspect(storage, storage.root, 0, []byte{}, storageStat)
|
||||||
|
in.writeDumpRecord(owner, storageStat)
|
||||||
|
}
|
||||||
|
if in.trySpawn(&wg, run) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
run()
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("%T: invalid node: %v", n, n))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for all goroutines spawned at this level before recording
|
||||||
|
// the current node. This ensures the entire subtree is counted
|
||||||
|
// before this call returns.
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
// Record stats for current height.
|
||||||
|
stat.add(n, height)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Summarize performs pass 2 over a trie dump and reports account stats,
|
||||||
|
// aggregate storage statistics, and top-N rankings.
|
||||||
|
func Summarize(dumpPath string, config *InspectConfig) error {
|
||||||
|
config = normalizeInspectConfig(config)
|
||||||
|
if dumpPath == "" {
|
||||||
|
dumpPath = config.DumpPath
|
||||||
|
}
|
||||||
|
if dumpPath == "" {
|
||||||
|
return errors.New("missing dump path")
|
||||||
|
}
|
||||||
|
file, err := os.Open(dumpPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open trie dump %s: %w", dumpPath, err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
if info, err := file.Stat(); err == nil {
|
||||||
|
if info.Size()%inspectDumpRecordSize != 0 {
|
||||||
|
return fmt.Errorf("invalid trie dump size %d (not a multiple of %d)", info.Size(), inspectDumpRecordSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
depthTop := newStorageStatsTopN(config.TopN, compareStorageStatsByDepth)
|
||||||
|
totalTop := newStorageStatsTopN(config.TopN, compareStorageStatsByTotal)
|
||||||
|
valueTop := newStorageStatsTopN(config.TopN, compareStorageStatsByValue)
|
||||||
|
|
||||||
|
summary := &inspectSummary{}
|
||||||
|
reader := bufio.NewReaderSize(file, 1<<20)
|
||||||
|
var buf [inspectDumpRecordSize]byte
|
||||||
|
|
||||||
|
for {
|
||||||
|
_, err := io.ReadFull(reader, buf[:])
|
||||||
|
if errors.Is(err, io.EOF) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if errors.Is(err, io.ErrUnexpectedEOF) {
|
||||||
|
return fmt.Errorf("truncated trie dump %s", dumpPath)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed reading trie dump %s: %w", dumpPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
record := decodeDumpRecord(buf[:])
|
||||||
|
snapshot := newStorageStats(record.Owner, record.Levels)
|
||||||
|
if record.Owner == (common.Hash{}) {
|
||||||
|
summary.Account = snapshot
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
summary.StorageCount++
|
||||||
|
summary.DepthHistogram[snapshot.MaxDepth]++
|
||||||
|
for i := 0; i < trieStatLevels; i++ {
|
||||||
|
summary.StorageLevels[i].Short += record.Levels[i].Short
|
||||||
|
summary.StorageLevels[i].Full += record.Levels[i].Full
|
||||||
|
summary.StorageLevels[i].Value += record.Levels[i].Value
|
||||||
|
summary.StorageLevels[i].Size += record.Levels[i].Size
|
||||||
|
}
|
||||||
|
|
||||||
|
depthTop.TryInsert(snapshot)
|
||||||
|
totalTop.TryInsert(snapshot)
|
||||||
|
valueTop.TryInsert(snapshot)
|
||||||
|
}
|
||||||
|
if summary.Account == nil {
|
||||||
|
return fmt.Errorf("dump file %s does not contain the account trie sentinel record", dumpPath)
|
||||||
|
}
|
||||||
|
for i := 0; i < trieStatLevels; i++ {
|
||||||
|
summary.StorageTotals.Short += summary.StorageLevels[i].Short
|
||||||
|
summary.StorageTotals.Full += summary.StorageLevels[i].Full
|
||||||
|
summary.StorageTotals.Value += summary.StorageLevels[i].Value
|
||||||
|
summary.StorageTotals.Size += summary.StorageLevels[i].Size
|
||||||
|
}
|
||||||
|
summary.TopByDepth = depthTop.Sorted()
|
||||||
|
summary.TopByTotalNodes = totalTop.Sorted()
|
||||||
|
summary.TopByValueNodes = valueTop.Sorted()
|
||||||
|
|
||||||
|
if config.Path != "" {
|
||||||
|
return summary.writeJSON(config.Path)
|
||||||
|
}
|
||||||
|
summary.display()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type dumpRecord struct {
|
||||||
|
Owner common.Hash
|
||||||
|
Levels [trieStatLevels]jsonLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeDumpRecord(raw []byte) dumpRecord {
|
||||||
|
var (
|
||||||
|
record dumpRecord
|
||||||
|
off = 32
|
||||||
|
)
|
||||||
|
copy(record.Owner[:], raw[:32])
|
||||||
|
for i := 0; i < trieStatLevels; i++ {
|
||||||
|
record.Levels[i] = jsonLevel{
|
||||||
|
Short: uint64(binary.LittleEndian.Uint32(raw[off:])),
|
||||||
|
Full: uint64(binary.LittleEndian.Uint32(raw[off+4:])),
|
||||||
|
Value: uint64(binary.LittleEndian.Uint32(raw[off+8:])),
|
||||||
|
Size: binary.LittleEndian.Uint64(raw[off+12:]),
|
||||||
|
}
|
||||||
|
off += 20
|
||||||
|
}
|
||||||
|
return record
|
||||||
|
}
|
||||||
|
|
||||||
|
type storageStats struct {
|
||||||
|
Owner common.Hash
|
||||||
|
Levels [trieStatLevels]jsonLevel
|
||||||
|
Summary jsonLevel
|
||||||
|
MaxDepth int
|
||||||
|
TotalNodes uint64
|
||||||
|
TotalSize uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func newStorageStats(owner common.Hash, levels [trieStatLevels]jsonLevel) *storageStats {
|
||||||
|
snapshot := &storageStats{Owner: owner, Levels: levels}
|
||||||
|
for i := 0; i < trieStatLevels; i++ {
|
||||||
|
level := levels[i]
|
||||||
|
if level.Short != 0 || level.Full != 0 || level.Value != 0 {
|
||||||
|
snapshot.MaxDepth = i
|
||||||
|
}
|
||||||
|
snapshot.Summary.Short += level.Short
|
||||||
|
snapshot.Summary.Full += level.Full
|
||||||
|
snapshot.Summary.Value += level.Value
|
||||||
|
snapshot.Summary.Size += level.Size
|
||||||
|
}
|
||||||
|
snapshot.TotalNodes = snapshot.Summary.Short + snapshot.Summary.Full + snapshot.Summary.Value
|
||||||
|
snapshot.TotalSize = snapshot.Summary.Size
|
||||||
|
return snapshot
|
||||||
|
}
|
||||||
|
|
||||||
|
func trimLevels(levels [trieStatLevels]jsonLevel) []jsonLevel {
|
||||||
|
n := len(levels)
|
||||||
|
for n > 0 && levels[n-1] == (jsonLevel{}) {
|
||||||
|
n--
|
||||||
|
}
|
||||||
|
return levels[:n]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *storageStats) MarshalJSON() ([]byte, error) {
|
||||||
|
type jsonStorageSnapshot struct {
|
||||||
|
Owner common.Hash `json:"Owner"`
|
||||||
|
MaxDepth int `json:"MaxDepth"`
|
||||||
|
TotalNodes uint64 `json:"TotalNodes"`
|
||||||
|
TotalSize uint64 `json:"TotalSize"`
|
||||||
|
ValueNodes uint64 `json:"ValueNodes"`
|
||||||
|
Levels []jsonLevel `json:"Levels"`
|
||||||
|
Summary jsonLevel `json:"Summary"`
|
||||||
|
}
|
||||||
|
return json.Marshal(jsonStorageSnapshot{
|
||||||
|
Owner: s.Owner,
|
||||||
|
MaxDepth: s.MaxDepth,
|
||||||
|
TotalNodes: s.TotalNodes,
|
||||||
|
TotalSize: s.TotalSize,
|
||||||
|
ValueNodes: s.Summary.Value,
|
||||||
|
Levels: trimLevels(s.Levels),
|
||||||
|
Summary: s.Summary,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *storageStats) toLevelStats() *LevelStats {
|
||||||
|
stats := NewLevelStats()
|
||||||
|
for i := 0; i < trieStatLevels; i++ {
|
||||||
|
stats.level[i].short.Store(s.Levels[i].Short)
|
||||||
|
stats.level[i].full.Store(s.Levels[i].Full)
|
||||||
|
stats.level[i].value.Store(s.Levels[i].Value)
|
||||||
|
stats.level[i].size.Store(s.Levels[i].Size)
|
||||||
|
}
|
||||||
|
return stats
|
||||||
|
}
|
||||||
|
|
||||||
|
type storageStatsCompare func(a, b *storageStats) int
|
||||||
|
|
||||||
|
type storageStatsTopN struct {
|
||||||
|
limit int
|
||||||
|
cmp storageStatsCompare
|
||||||
|
heap storageStatsHeap
|
||||||
|
}
|
||||||
|
|
||||||
|
type storageStatsHeap struct {
|
||||||
|
items []*storageStats
|
||||||
|
cmp storageStatsCompare
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h storageStatsHeap) Len() int { return len(h.items) }
|
||||||
|
|
||||||
|
func (h storageStatsHeap) Less(i, j int) bool {
|
||||||
|
// Keep the weakest entry at the root (min-heap semantics).
|
||||||
|
return h.cmp(h.items[i], h.items[j]) < 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h storageStatsHeap) Swap(i, j int) { h.items[i], h.items[j] = h.items[j], h.items[i] }
|
||||||
|
|
||||||
|
func (h *storageStatsHeap) Push(x any) {
|
||||||
|
h.items = append(h.items, x.(*storageStats))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *storageStatsHeap) Pop() any {
|
||||||
|
item := h.items[len(h.items)-1]
|
||||||
|
h.items = h.items[:len(h.items)-1]
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
func newStorageStatsTopN(limit int, cmp storageStatsCompare) *storageStatsTopN {
|
||||||
|
h := storageStatsHeap{cmp: cmp}
|
||||||
|
heap.Init(&h)
|
||||||
|
return &storageStatsTopN{limit: limit, cmp: cmp, heap: h}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *storageStatsTopN) TryInsert(item *storageStats) {
|
||||||
|
if t.limit <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if t.heap.Len() < t.limit {
|
||||||
|
heap.Push(&t.heap, item)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if t.cmp(item, t.heap.items[0]) <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
heap.Pop(&t.heap)
|
||||||
|
heap.Push(&t.heap, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *storageStatsTopN) Sorted() []*storageStats {
|
||||||
|
out := append([]*storageStats(nil), t.heap.items...)
|
||||||
|
sort.Slice(out, func(i, j int) bool { return t.cmp(out[i], out[j]) > 0 })
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func compareStorageStatsByDepth(a, b *storageStats) int {
|
||||||
|
return cmp.Or(
|
||||||
|
cmp.Compare(a.MaxDepth, b.MaxDepth),
|
||||||
|
cmp.Compare(a.TotalNodes, b.TotalNodes),
|
||||||
|
cmp.Compare(a.Summary.Value, b.Summary.Value),
|
||||||
|
bytes.Compare(a.Owner[:], b.Owner[:]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func compareStorageStatsByTotal(a, b *storageStats) int {
|
||||||
|
return cmp.Or(
|
||||||
|
cmp.Compare(a.TotalNodes, b.TotalNodes),
|
||||||
|
cmp.Compare(a.MaxDepth, b.MaxDepth),
|
||||||
|
cmp.Compare(a.Summary.Value, b.Summary.Value),
|
||||||
|
bytes.Compare(a.Owner[:], b.Owner[:]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func compareStorageStatsByValue(a, b *storageStats) int {
|
||||||
|
return cmp.Or(
|
||||||
|
cmp.Compare(a.Summary.Value, b.Summary.Value),
|
||||||
|
cmp.Compare(a.MaxDepth, b.MaxDepth),
|
||||||
|
cmp.Compare(a.TotalNodes, b.TotalNodes),
|
||||||
|
bytes.Compare(a.Owner[:], b.Owner[:]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type inspectSummary struct {
|
||||||
|
Account *storageStats
|
||||||
|
StorageCount uint64
|
||||||
|
StorageTotals jsonLevel
|
||||||
|
StorageLevels [trieStatLevels]jsonLevel
|
||||||
|
DepthHistogram [trieStatLevels]uint64
|
||||||
|
TopByDepth []*storageStats
|
||||||
|
TopByTotalNodes []*storageStats
|
||||||
|
TopByValueNodes []*storageStats
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *inspectSummary) display() {
|
||||||
|
s.displayCombinedDepthTable()
|
||||||
|
s.Account.toLevelStats().display("Accounts trie")
|
||||||
|
fmt.Println("Storage trie aggregate summary")
|
||||||
|
fmt.Printf("Total storage tries: %d\n", s.StorageCount)
|
||||||
|
totalNodes := s.StorageTotals.Short + s.StorageTotals.Full + s.StorageTotals.Value
|
||||||
|
fmt.Printf("Total nodes: %d\n", totalNodes)
|
||||||
|
fmt.Printf("Total size: %s\n", common.StorageSize(s.StorageTotals.Size))
|
||||||
|
fmt.Printf(" Short nodes: %d\n", s.StorageTotals.Short)
|
||||||
|
fmt.Printf(" Full nodes: %d\n", s.StorageTotals.Full)
|
||||||
|
fmt.Printf(" Value nodes: %d\n", s.StorageTotals.Value)
|
||||||
|
|
||||||
|
b := new(strings.Builder)
|
||||||
|
table := tablewriter.NewWriter(b)
|
||||||
|
table.SetHeader([]string{"Max Depth", "Storage Tries"})
|
||||||
|
for i, count := range s.DepthHistogram {
|
||||||
|
table.AppendBulk([][]string{{fmt.Sprint(i), fmt.Sprint(count)}})
|
||||||
|
}
|
||||||
|
table.Render()
|
||||||
|
fmt.Print(b.String())
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
s.displayTop("Top storage tries by max depth", s.TopByDepth)
|
||||||
|
s.displayTop("Top storage tries by total node count", s.TopByTotalNodes)
|
||||||
|
s.displayTop("Top storage tries by value (slot) count", s.TopByValueNodes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *inspectSummary) displayCombinedDepthTable() {
|
||||||
|
accountTotal := s.Account.Summary.Short + s.Account.Summary.Full + s.Account.Summary.Value
|
||||||
|
storageTotal := s.StorageTotals.Short + s.StorageTotals.Full + s.StorageTotals.Value
|
||||||
|
accountTotalSize := s.Account.Summary.Size
|
||||||
|
storageTotalSize := s.StorageTotals.Size
|
||||||
|
|
||||||
|
fmt.Println("Trie Depth Distribution")
|
||||||
|
fmt.Printf("Account Trie: %d nodes (%s)\n", accountTotal, common.StorageSize(accountTotalSize))
|
||||||
|
fmt.Printf("Storage Tries: %d nodes (%s) across %d tries\n", storageTotal, common.StorageSize(storageTotalSize), s.StorageCount)
|
||||||
|
|
||||||
|
b := new(strings.Builder)
|
||||||
|
table := tablewriter.NewWriter(b)
|
||||||
|
table.SetHeader([]string{"Depth", "Account Nodes", "Account Size", "Storage Nodes", "Storage Size"})
|
||||||
|
for i := 0; i < trieStatLevels; i++ {
|
||||||
|
accountNodes := s.Account.Levels[i].Short + s.Account.Levels[i].Full + s.Account.Levels[i].Value
|
||||||
|
accountSize := s.Account.Levels[i].Size
|
||||||
|
storageNodes := s.StorageLevels[i].Short + s.StorageLevels[i].Full + s.StorageLevels[i].Value
|
||||||
|
storageSize := s.StorageLevels[i].Size
|
||||||
|
if accountNodes == 0 && storageNodes == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
table.AppendBulk([][]string{{
|
||||||
|
fmt.Sprint(i),
|
||||||
|
fmt.Sprint(accountNodes),
|
||||||
|
common.StorageSize(accountSize).String(),
|
||||||
|
fmt.Sprint(storageNodes),
|
||||||
|
common.StorageSize(storageSize).String(),
|
||||||
|
}})
|
||||||
|
}
|
||||||
|
table.Render()
|
||||||
|
fmt.Print(b.String())
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *inspectSummary) displayTop(title string, list []*storageStats) {
|
||||||
|
fmt.Println(title)
|
||||||
|
if len(list) == 0 {
|
||||||
|
fmt.Println("No storage tries found")
|
||||||
|
fmt.Println()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i, item := range list {
|
||||||
|
fmt.Printf("%d: %s\n", i+1, item.Owner)
|
||||||
|
item.toLevelStats().display("storage trie")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *inspectSummary) MarshalJSON() ([]byte, error) {
|
||||||
|
type jsonAccountTrie struct {
|
||||||
|
Name string `json:"Name"`
|
||||||
|
Levels []jsonLevel `json:"Levels"`
|
||||||
|
Summary jsonLevel `json:"Summary"`
|
||||||
|
}
|
||||||
|
type jsonStorageSummary struct {
|
||||||
|
TotalStorageTries uint64 `json:"TotalStorageTries"`
|
||||||
|
Totals jsonLevel `json:"Totals"`
|
||||||
|
Levels []jsonLevel `json:"Levels"`
|
||||||
|
DepthHistogram [trieStatLevels]uint64 `json:"DepthHistogram"`
|
||||||
|
}
|
||||||
|
type jsonInspectSummary struct {
|
||||||
|
AccountTrie jsonAccountTrie `json:"AccountTrie"`
|
||||||
|
StorageSummary jsonStorageSummary `json:"StorageSummary"`
|
||||||
|
TopByDepth []*storageStats `json:"TopByDepth"`
|
||||||
|
TopByTotalNodes []*storageStats `json:"TopByTotalNodes"`
|
||||||
|
TopByValueNodes []*storageStats `json:"TopByValueNodes"`
|
||||||
|
}
|
||||||
|
return json.Marshal(jsonInspectSummary{
|
||||||
|
AccountTrie: jsonAccountTrie{
|
||||||
|
Name: "account trie",
|
||||||
|
Levels: trimLevels(s.Account.Levels),
|
||||||
|
Summary: s.Account.Summary,
|
||||||
|
},
|
||||||
|
StorageSummary: jsonStorageSummary{
|
||||||
|
TotalStorageTries: s.StorageCount,
|
||||||
|
Totals: s.StorageTotals,
|
||||||
|
Levels: trimLevels(s.StorageLevels),
|
||||||
|
DepthHistogram: s.DepthHistogram,
|
||||||
|
},
|
||||||
|
TopByDepth: s.TopByDepth,
|
||||||
|
TopByTotalNodes: s.TopByTotalNodes,
|
||||||
|
TopByValueNodes: s.TopByValueNodes,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *inspectSummary) writeJSON(path string) error {
|
||||||
|
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
enc := json.NewEncoder(file)
|
||||||
|
enc.SetIndent("", " ")
|
||||||
|
return enc.Encode(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// display will print a table displaying the trie's node statistics.
|
||||||
|
func (s *LevelStats) display(title string) {
|
||||||
|
// Shorten title if too long.
|
||||||
|
if len(title) > 32 {
|
||||||
|
title = title[0:8] + "..." + title[len(title)-8:]
|
||||||
|
}
|
||||||
|
|
||||||
|
b := new(strings.Builder)
|
||||||
|
table := tablewriter.NewWriter(b)
|
||||||
|
table.SetHeader([]string{title, "Level", "Short Nodes", "Full Node", "Value Node"})
|
||||||
|
|
||||||
|
stat := &stat{}
|
||||||
|
for i := range s.level {
|
||||||
|
if s.level[i].empty() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
short, full, value, _ := s.level[i].load()
|
||||||
|
table.AppendBulk([][]string{{"-", fmt.Sprint(i), fmt.Sprint(short), fmt.Sprint(full), fmt.Sprint(value)}})
|
||||||
|
stat.add(&s.level[i])
|
||||||
|
}
|
||||||
|
short, full, value, _ := stat.load()
|
||||||
|
table.SetFooter([]string{"Total", "", fmt.Sprint(short), fmt.Sprint(full), fmt.Sprint(value)})
|
||||||
|
table.Render()
|
||||||
|
fmt.Print(b.String())
|
||||||
|
fmt.Println("Max depth", s.MaxDepth())
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
type jsonLevel struct {
|
||||||
|
Short uint64
|
||||||
|
Full uint64
|
||||||
|
Value uint64
|
||||||
|
Size uint64
|
||||||
|
}
|
||||||
256
trie/inspect_test.go
Normal file
256
trie/inspect_test.go
Normal file
|
|
@ -0,0 +1,256 @@
|
||||||
|
// 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 trie
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"math/rand"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/core/rawdb"
|
||||||
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
|
"github.com/ethereum/go-ethereum/rlp"
|
||||||
|
"github.com/ethereum/go-ethereum/trie/trienode"
|
||||||
|
"github.com/holiman/uint256"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestInspect inspects a randomly generated account trie. It's useful for
|
||||||
|
// quickly verifying changes to the results display.
|
||||||
|
func TestInspect(t *testing.T) {
|
||||||
|
db := newTestDatabase(rawdb.NewMemoryDatabase(), rawdb.HashScheme)
|
||||||
|
trie, err := NewStateTrie(TrieID(types.EmptyRootHash), db)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create state trie: %v", err)
|
||||||
|
}
|
||||||
|
// Create a realistic looking account trie with storage.
|
||||||
|
addresses, accounts := makeAccountsWithStorage(db, 11, true)
|
||||||
|
for i := 0; i < len(addresses); i++ {
|
||||||
|
trie.MustUpdate(crypto.Keccak256(addresses[i][:]), accounts[i])
|
||||||
|
}
|
||||||
|
// Insert the accounts into the trie and hash it
|
||||||
|
root, nodes := trie.Commit(true)
|
||||||
|
db.Update(root, types.EmptyRootHash, trienode.NewWithNodeSet(nodes))
|
||||||
|
db.Commit(root)
|
||||||
|
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
dumpPath := filepath.Join(tempDir, "trie-dump.bin")
|
||||||
|
if err := Inspect(db, root, &InspectConfig{
|
||||||
|
TopN: 1,
|
||||||
|
DumpPath: dumpPath,
|
||||||
|
Path: filepath.Join(tempDir, "trie-summary.json"),
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("inspect failed: %v", err)
|
||||||
|
}
|
||||||
|
reanalysisPath := filepath.Join(tempDir, "trie-summary-reanalysis.json")
|
||||||
|
if err := Summarize(dumpPath, &InspectConfig{
|
||||||
|
TopN: 1,
|
||||||
|
Path: reanalysisPath,
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("summarize failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
inspectSummaryPath := filepath.Join(tempDir, "trie-summary.json")
|
||||||
|
inspectOut := loadInspectJSON(t, inspectSummaryPath)
|
||||||
|
reanalysisOut := loadInspectJSON(t, reanalysisPath)
|
||||||
|
|
||||||
|
if len(inspectOut.StorageSummary.Levels) == 0 {
|
||||||
|
t.Fatal("expected StorageSummary.Levels to be populated")
|
||||||
|
}
|
||||||
|
if inspectOut.AccountTrie.Summary.Size == 0 {
|
||||||
|
t.Fatal("expected account trie size summary to be populated")
|
||||||
|
}
|
||||||
|
if inspectOut.StorageSummary.Totals.Size == 0 {
|
||||||
|
t.Fatal("expected storage trie size summary to be populated")
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(inspectOut.AccountTrie, reanalysisOut.AccountTrie) {
|
||||||
|
t.Fatal("account trie summary mismatch between inspect and summarize")
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(inspectOut.StorageSummary, reanalysisOut.StorageSummary) {
|
||||||
|
t.Fatal("storage summary mismatch between inspect and summarize")
|
||||||
|
}
|
||||||
|
|
||||||
|
assertStorageTotalsMatchLevels(t, inspectOut)
|
||||||
|
assertStorageTotalsMatchLevels(t, reanalysisOut)
|
||||||
|
assertAccountTotalsMatchLevels(t, inspectOut.AccountTrie)
|
||||||
|
assertAccountTotalsMatchLevels(t, reanalysisOut.AccountTrie)
|
||||||
|
|
||||||
|
var histogramTotal uint64
|
||||||
|
for _, count := range inspectOut.StorageSummary.DepthHistogram {
|
||||||
|
histogramTotal += count
|
||||||
|
}
|
||||||
|
if histogramTotal != inspectOut.StorageSummary.TotalStorageTries {
|
||||||
|
t.Fatalf("depth histogram total %d does not match total storage tries %d", histogramTotal, inspectOut.StorageSummary.TotalStorageTries)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type inspectJSONOutput struct {
|
||||||
|
// Reuse storageStats for AccountTrie JSON to avoid introducing a parallel
|
||||||
|
// account summary test type. AccountTrie JSON includes Levels+Summary,
|
||||||
|
// which map directly; other storageStats fields remain zero-values.
|
||||||
|
AccountTrie storageStats `json:"AccountTrie"`
|
||||||
|
|
||||||
|
StorageSummary struct {
|
||||||
|
TotalStorageTries uint64 `json:"TotalStorageTries"`
|
||||||
|
Totals jsonLevel `json:"Totals"`
|
||||||
|
Levels []jsonLevel `json:"Levels"`
|
||||||
|
DepthHistogram [trieStatLevels]uint64 `json:"DepthHistogram"`
|
||||||
|
} `json:"StorageSummary"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadInspectJSON(t *testing.T, path string) inspectJSONOutput {
|
||||||
|
t.Helper()
|
||||||
|
raw, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read %s: %v", path, err)
|
||||||
|
}
|
||||||
|
var out inspectJSONOutput
|
||||||
|
if err := json.Unmarshal(raw, &out); err != nil {
|
||||||
|
t.Fatalf("failed to decode %s: %v", path, err)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertStorageTotalsMatchLevels(t *testing.T, out inspectJSONOutput) {
|
||||||
|
t.Helper()
|
||||||
|
var fromLevels jsonLevel
|
||||||
|
for _, level := range out.StorageSummary.Levels {
|
||||||
|
fromLevels.Short += level.Short
|
||||||
|
fromLevels.Full += level.Full
|
||||||
|
fromLevels.Value += level.Value
|
||||||
|
fromLevels.Size += level.Size
|
||||||
|
}
|
||||||
|
if fromLevels.Short != out.StorageSummary.Totals.Short || fromLevels.Full != out.StorageSummary.Totals.Full || fromLevels.Value != out.StorageSummary.Totals.Value || fromLevels.Size != out.StorageSummary.Totals.Size {
|
||||||
|
t.Fatalf("storage totals mismatch: levels=%+v totals=%+v", fromLevels, out.StorageSummary.Totals)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertAccountTotalsMatchLevels(t *testing.T, account storageStats) {
|
||||||
|
t.Helper()
|
||||||
|
var fromLevels jsonLevel
|
||||||
|
for _, level := range account.Levels {
|
||||||
|
fromLevels.Short += level.Short
|
||||||
|
fromLevels.Full += level.Full
|
||||||
|
fromLevels.Value += level.Value
|
||||||
|
fromLevels.Size += level.Size
|
||||||
|
}
|
||||||
|
if fromLevels.Short != account.Summary.Short || fromLevels.Full != account.Summary.Full || fromLevels.Value != account.Summary.Value || fromLevels.Size != account.Summary.Size {
|
||||||
|
t.Fatalf("account totals mismatch: levels=%+v totals=%+v", fromLevels, account.Summary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestInspectContract tests the InspectContract function on a single account
|
||||||
|
// with storage and snapshot data.
|
||||||
|
func TestInspectContract(t *testing.T) {
|
||||||
|
diskdb := rawdb.NewMemoryDatabase()
|
||||||
|
db := newTestDatabase(diskdb, rawdb.HashScheme)
|
||||||
|
|
||||||
|
// Create a contract address and its storage trie.
|
||||||
|
address := common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678")
|
||||||
|
accountHash := crypto.Keccak256Hash(address.Bytes())
|
||||||
|
|
||||||
|
// Build a storage trie with some entries.
|
||||||
|
storageTrie := NewEmpty(db)
|
||||||
|
storageSlots := make(map[common.Hash][]byte)
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
k := crypto.Keccak256Hash([]byte{byte(i)})
|
||||||
|
v := []byte{byte(i + 1)}
|
||||||
|
storageTrie.MustUpdate(k.Bytes(), v)
|
||||||
|
storageSlots[k] = v
|
||||||
|
}
|
||||||
|
storageRoot, storageNodes := storageTrie.Commit(true)
|
||||||
|
db.Update(storageRoot, types.EmptyRootHash, trienode.NewWithNodeSet(storageNodes))
|
||||||
|
db.Commit(storageRoot)
|
||||||
|
|
||||||
|
// Build the account trie with the contract account.
|
||||||
|
account := types.StateAccount{
|
||||||
|
Nonce: 1,
|
||||||
|
Balance: uint256.NewInt(1000),
|
||||||
|
Root: storageRoot,
|
||||||
|
CodeHash: crypto.Keccak256(nil),
|
||||||
|
}
|
||||||
|
accountRLP, err := rlp.EncodeToBytes(&account)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to encode account: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
accountTrie := NewEmpty(db)
|
||||||
|
accountTrie.MustUpdate(crypto.Keccak256(address.Bytes()), accountRLP)
|
||||||
|
stateRoot, accountNodes := accountTrie.Commit(true)
|
||||||
|
db.Update(stateRoot, types.EmptyRootHash, trienode.NewWithNodeSet(accountNodes))
|
||||||
|
db.Commit(stateRoot)
|
||||||
|
|
||||||
|
// Write snapshot data for the account and its storage slots.
|
||||||
|
rawdb.WriteAccountSnapshot(diskdb, accountHash, accountRLP)
|
||||||
|
for k, v := range storageSlots {
|
||||||
|
rawdb.WriteStorageSnapshot(diskdb, accountHash, k, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// InspectContract should succeed without error.
|
||||||
|
if err := InspectContract(db, diskdb, stateRoot, address); err != nil {
|
||||||
|
t.Fatalf("InspectContract failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeAccountsWithStorage(db *testDb, size int, storage bool) (addresses [][20]byte, accounts [][]byte) {
|
||||||
|
// Make the random benchmark deterministic
|
||||||
|
random := rand.New(rand.NewSource(0))
|
||||||
|
|
||||||
|
addresses = make([][20]byte, size)
|
||||||
|
for i := 0; i < len(addresses); i++ {
|
||||||
|
data := make([]byte, 20)
|
||||||
|
random.Read(data)
|
||||||
|
copy(addresses[i][:], data)
|
||||||
|
}
|
||||||
|
accounts = make([][]byte, len(addresses))
|
||||||
|
for i := 0; i < len(accounts); i++ {
|
||||||
|
var (
|
||||||
|
nonce = uint64(random.Int63())
|
||||||
|
root = types.EmptyRootHash
|
||||||
|
code = crypto.Keccak256(nil)
|
||||||
|
)
|
||||||
|
if storage {
|
||||||
|
trie := NewEmpty(db)
|
||||||
|
for range random.Uint32()%256 + 1 { // non-zero
|
||||||
|
k, v := make([]byte, 32), make([]byte, 32)
|
||||||
|
random.Read(k)
|
||||||
|
random.Read(v)
|
||||||
|
trie.MustUpdate(k, v)
|
||||||
|
}
|
||||||
|
var nodes *trienode.NodeSet
|
||||||
|
root, nodes = trie.Commit(true)
|
||||||
|
db.Update(root, types.EmptyRootHash, trienode.NewWithNodeSet(nodes))
|
||||||
|
db.Commit(root)
|
||||||
|
}
|
||||||
|
numBytes := random.Uint32() % 33 // [0, 32] bytes
|
||||||
|
balanceBytes := make([]byte, numBytes)
|
||||||
|
random.Read(balanceBytes)
|
||||||
|
balance := new(uint256.Int).SetBytes(balanceBytes)
|
||||||
|
data, _ := rlp.EncodeToBytes(&types.StateAccount{
|
||||||
|
Nonce: nonce,
|
||||||
|
Balance: balance,
|
||||||
|
Root: root,
|
||||||
|
CodeHash: code,
|
||||||
|
})
|
||||||
|
accounts[i] = data
|
||||||
|
}
|
||||||
|
return addresses, accounts
|
||||||
|
}
|
||||||
123
trie/levelstats.go
Normal file
123
trie/levelstats.go
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
// 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 trie
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync/atomic"
|
||||||
|
)
|
||||||
|
|
||||||
|
const trieStatLevels = 16
|
||||||
|
|
||||||
|
// LevelStats tracks the type and count of trie nodes at each level in a trie.
|
||||||
|
//
|
||||||
|
// Note: theoretically it is possible to have up to 64 trie levels, but
|
||||||
|
// LevelStats supports exactly 16 levels and panics on deeper paths.
|
||||||
|
type LevelStats struct {
|
||||||
|
level [trieStatLevels]stat
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLevelStats creates an empty trie statistics collector.
|
||||||
|
func NewLevelStats() *LevelStats {
|
||||||
|
return &LevelStats{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MaxDepth iterates each level and finds the deepest level with at least one
|
||||||
|
// trie node.
|
||||||
|
func (s *LevelStats) MaxDepth() int {
|
||||||
|
depth := 0
|
||||||
|
for i := range s.level {
|
||||||
|
if s.level[i].short.Load() != 0 || s.level[i].full.Load() != 0 || s.level[i].value.Load() != 0 {
|
||||||
|
depth = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return depth
|
||||||
|
}
|
||||||
|
|
||||||
|
// TotalNodes returns the total number of nodes across all levels and types.
|
||||||
|
func (s *LevelStats) TotalNodes() uint64 {
|
||||||
|
var total uint64
|
||||||
|
for i := range s.level {
|
||||||
|
total += s.level[i].short.Load() + s.level[i].full.Load() + s.level[i].value.Load()
|
||||||
|
}
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
|
||||||
|
// add increases the node count by one for the specified node type and depth.
|
||||||
|
func (s *LevelStats) add(n node, depth uint32) {
|
||||||
|
d := int(depth)
|
||||||
|
switch (n).(type) {
|
||||||
|
case *shortNode:
|
||||||
|
s.level[d].short.Add(1)
|
||||||
|
case *fullNode:
|
||||||
|
s.level[d].full.Add(1)
|
||||||
|
case valueNode:
|
||||||
|
s.level[d].value.Add(1)
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("%T: invalid node: %v", n, n))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// addSize increases the raw byte-size tally at the specified depth.
|
||||||
|
func (s *LevelStats) addSize(depth uint32, size uint64) {
|
||||||
|
s.level[depth].size.Add(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddLeaf records a leaf depth. Witness collection reuses the value-node bucket
|
||||||
|
// for leaf accounting. It panics if the depth is outside [0, 15].
|
||||||
|
func (s *LevelStats) AddLeaf(depth int) {
|
||||||
|
s.level[depth].value.Add(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LeafDepths returns leaf counts grouped by depth.
|
||||||
|
func (s *LevelStats) LeafDepths() [trieStatLevels]int64 {
|
||||||
|
var leaves [trieStatLevels]int64
|
||||||
|
for i := range s.level {
|
||||||
|
leaves[i] = int64(s.level[i].value.Load())
|
||||||
|
}
|
||||||
|
return leaves
|
||||||
|
}
|
||||||
|
|
||||||
|
// stat is a specific level's count of each node type.
|
||||||
|
type stat struct {
|
||||||
|
short atomic.Uint64
|
||||||
|
full atomic.Uint64
|
||||||
|
value atomic.Uint64
|
||||||
|
size atomic.Uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
// empty is a helper that returns whether there are any trie nodes at the level.
|
||||||
|
func (s *stat) empty() bool {
|
||||||
|
if s.full.Load() == 0 && s.short.Load() == 0 && s.value.Load() == 0 && s.size.Load() == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// load is a helper that loads each node type's value.
|
||||||
|
func (s *stat) load() (uint64, uint64, uint64, uint64) {
|
||||||
|
return s.short.Load(), s.full.Load(), s.value.Load(), s.size.Load()
|
||||||
|
}
|
||||||
|
|
||||||
|
// add is a helper that adds two level's stats together.
|
||||||
|
func (s *stat) add(other *stat) *stat {
|
||||||
|
s.short.Add(other.short.Load())
|
||||||
|
s.full.Add(other.full.Load())
|
||||||
|
s.value.Add(other.value.Load())
|
||||||
|
s.size.Add(other.size.Load())
|
||||||
|
return s
|
||||||
|
}
|
||||||
37
trie/levelstats_test.go
Normal file
37
trie/levelstats_test.go
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
// 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 trie
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestLevelStatsAddLeafDepthBounds(t *testing.T) {
|
||||||
|
stats := NewLevelStats()
|
||||||
|
stats.AddLeaf(15)
|
||||||
|
|
||||||
|
if got := stats.LeafDepths()[15]; got != 1 {
|
||||||
|
t.Fatalf("leaf count at depth 15 = %d, want 1", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLevelStatsAddLeafPanicsOnDepth16(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r == nil {
|
||||||
|
t.Fatal("expected panic for depth >= 16")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
NewLevelStats().AddLeaf(16)
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue