mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-19 19:30:44 +00:00
cmd/geth,trie: add --contract flag to inspect trie
This commit is contained in:
parent
87d79f9514
commit
6e191e7758
3 changed files with 222 additions and 0 deletions
|
|
@ -68,6 +68,10 @@ var (
|
||||||
Name: "summarize",
|
Name: "summarize",
|
||||||
Usage: "Summarize an existing trie dump file (skip trie traversal)",
|
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,
|
||||||
|
|
@ -118,6 +122,7 @@ Remove blockchain and state databases`,
|
||||||
utils.OutputFileFlag,
|
utils.OutputFileFlag,
|
||||||
inspectTrieDumpPathFlag,
|
inspectTrieDumpPathFlag,
|
||||||
inspectTrieSummarizeFlag,
|
inspectTrieSummarizeFlag,
|
||||||
|
inspectTrieContractFlag,
|
||||||
}, utils.NetworkFlags, utils.DatabaseFlags),
|
}, utils.NetworkFlags, utils.DatabaseFlags),
|
||||||
Usage: "Print detailed trie information about the structure of account trie and storage tries.",
|
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,
|
Description: `This commands iterates the entrie trie-backed state. If the 'blocknum' is not specified,
|
||||||
|
|
@ -488,6 +493,12 @@ func inspectTrie(ctx *cli.Context) error {
|
||||||
triedb := utils.MakeTrieDatabase(ctx, stack, db, false, true, false)
|
triedb := utils.MakeTrieDatabase(ctx, stack, db, false, true, false)
|
||||||
defer triedb.Close()
|
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)
|
log.Info("Inspecting trie", "root", trieRoot, "block", number, "dump", config.DumpPath, "top", topN)
|
||||||
return trie.Inspect(triedb, trieRoot, config)
|
return trie.Inspect(triedb, trieRoot, config)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
157
trie/inspect.go
157
trie/inspect.go
|
|
@ -31,13 +31,19 @@ import (
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"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/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/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/triedb/database"
|
"github.com/ethereum/go-ethereum/triedb/database"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
"golang.org/x/sync/semaphore"
|
"golang.org/x/sync/semaphore"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -117,6 +123,157 @@ func Inspect(triedb database.NodeDatabase, root common.Hash, config *InspectConf
|
||||||
return Summarize(config.DumpPath, config)
|
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.
|
||||||
|
var storageStat *LevelStats
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
storageStat = NewLevelStats()
|
||||||
|
|
||||||
|
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(),
|
||||||
|
"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 {
|
func normalizeInspectConfig(config *InspectConfig) *InspectConfig {
|
||||||
if config == nil {
|
if config == nil {
|
||||||
config = &InspectConfig{}
|
config = &InspectConfig{}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import (
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
"github.com/ethereum/go-ethereum/core/rawdb"
|
||||||
"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"
|
||||||
|
|
@ -156,6 +157,59 @@ func assertAccountTotalsMatchLevels(t *testing.T, account storageStats) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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) {
|
func makeAccountsWithStorage(db *testDb, size int, storage bool) (addresses [][20]byte, accounts [][]byte) {
|
||||||
// Make the random benchmark deterministic
|
// Make the random benchmark deterministic
|
||||||
random := rand.New(rand.NewSource(0))
|
random := rand.New(rand.NewSource(0))
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue