go-ethereum/trie/inspect_test.go
Fynn 8450e40798
Some checks are pending
/ Linux Build (push) Waiting to run
/ Linux Build (arm) (push) Waiting to run
/ Keeper Build (push) Waiting to run
/ Windows Build (push) Waiting to run
/ Docker Image (push) Waiting to run
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>
2026-02-24 10:56:00 -07:00

256 lines
8.8 KiB
Go

// 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
}