mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-02-26 07:37:20 +00:00
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>
256 lines
8.8 KiB
Go
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
|
|
}
|