package trie import ( "bytes" "errors" "fmt" "runtime" "strings" "sync" "sync/atomic" "time" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/mclock" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/triedb/database" "github.com/olekukonko/tablewriter" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/types" "golang.org/x/sync/semaphore" ) type Database interface { database.Database Scheme() string Cap(limit common.StorageSize) error DiskDB() ethdb.Database } const TopN = 3 type Inspector struct { trie *Trie // traverse trie db Database stateRootHash common.Hash blockNum uint64 root node // root of triedb sem *semaphore.Weighted eoaAccountNums uint64 wg sync.WaitGroup results stat topN int totalAccountNum atomic.Uint64 totalStorageNum atomic.Uint64 lastTime mclock.AbsTime } type stat struct { lock sync.RWMutex account *trieStat storageTopN []*trieStat storageTopNTotal []uint64 storageTotal nodeStat storageTrieNum uint64 } type trieStat struct { owner common.Hash totalNodeStat nodeStat nodeStatByLevel [16]nodeStat } type nodeStat struct { ShortNodeCnt atomic.Uint64 FullNodeCnt atomic.Uint64 ValueNodeCnt atomic.Uint64 } func (ns *nodeStat) IsEmpty() bool { if ns.FullNodeCnt.Load() == 0 && ns.ShortNodeCnt.Load() == 0 && ns.ValueNodeCnt.Load() == 0 { return true } return false } func (s *stat) add(ts *trieStat, topN int) { s.lock.Lock() defer s.lock.Unlock() if ts.owner == (common.Hash{}) { s.account = ts return } total := ts.totalNodeStat.ValueNodeCnt.Load() + ts.totalNodeStat.FullNodeCnt.Load() + ts.totalNodeStat.ShortNodeCnt.Load() if len(s.storageTopNTotal) == 0 || total > s.storageTopNTotal[len(s.storageTopNTotal)-1] { var ( i int t uint64 ) for i, t = range s.storageTopNTotal { if total < t { continue } break } s.storageTopNTotal = append(s.storageTopNTotal[:i], append([]uint64{total}, s.storageTopNTotal[i:]...)...) s.storageTopN = append(s.storageTopN[:i], append([]*trieStat{ts}, s.storageTopN[i:]...)...) if len(s.storageTopN) > topN { s.storageTopNTotal = s.storageTopNTotal[:topN] s.storageTopN = s.storageTopN[:topN] } } s.storageTotal.ShortNodeCnt.Add(ts.totalNodeStat.ShortNodeCnt.Load()) s.storageTotal.ValueNodeCnt.Add(ts.totalNodeStat.ValueNodeCnt.Load()) s.storageTotal.FullNodeCnt.Add(ts.totalNodeStat.FullNodeCnt.Load()) s.storageTrieNum++ } func (trieStat *trieStat) add(theNode node, height int) { switch (theNode).(type) { case *shortNode: trieStat.totalNodeStat.ShortNodeCnt.Add(1) trieStat.nodeStatByLevel[height].ShortNodeCnt.Add(1) case *fullNode: trieStat.totalNodeStat.FullNodeCnt.Add(1) trieStat.nodeStatByLevel[height].FullNodeCnt.Add(1) case valueNode: trieStat.totalNodeStat.ValueNodeCnt.Add(1) trieStat.nodeStatByLevel[height].ValueNodeCnt.Add(1) } } func (trieStat *trieStat) Display(ownerAddress string, treeType string) string { sw := new(strings.Builder) table := tablewriter.NewWriter(sw) table.SetHeader([]string{"-", "Level", "ShortNodeCnt", "FullNodeCnt", "ValueNodeCnt"}) if ownerAddress == "" { table.SetCaption(true, fmt.Sprintf("%v", treeType)) } else { table.SetCaption(true, fmt.Sprintf("%v-%v", treeType, ownerAddress)) } table.SetAlignment(1) for i := range trieStat.nodeStatByLevel { if trieStat.nodeStatByLevel[i].IsEmpty() { continue } table.AppendBulk([][]string{ {"-", fmt.Sprintf("%d", i), fmt.Sprintf("%d", trieStat.nodeStatByLevel[i].ShortNodeCnt.Load()), fmt.Sprintf("%d", trieStat.nodeStatByLevel[i].FullNodeCnt.Load()), fmt.Sprintf("%d", trieStat.nodeStatByLevel[i].ValueNodeCnt.Load())}, }) } table.AppendBulk([][]string{ {"Total", "-", fmt.Sprintf("%d", trieStat.totalNodeStat.ShortNodeCnt.Load()), fmt.Sprintf("%d", trieStat.totalNodeStat.FullNodeCnt.Load()), fmt.Sprintf("%d", trieStat.totalNodeStat.ValueNodeCnt.Load())}, }) table.Render() return sw.String() } // NewInspector return a inspector obj func NewInspector(tr *Trie, db Database, stateRootHash common.Hash, blockNum uint64, jobNum uint64, topN int) (*Inspector, error) { if tr == nil { return nil, errors.New("trie is nil") } if tr.root == nil { return nil, errors.New("trie root is nil") } ins := &Inspector{ trie: tr, db: db, stateRootHash: stateRootHash, blockNum: blockNum, root: tr.root, results: stat{}, topN: topN, totalAccountNum: atomic.Uint64{}, totalStorageNum: atomic.Uint64{}, lastTime: mclock.Now(), sem: semaphore.NewWeighted(int64(jobNum)), wg: sync.WaitGroup{}, eoaAccountNums: 0, } return ins, nil } // Run statistics, external call func (s *Inspector) Run() { ticker := time.NewTicker(30 * time.Second) go func() { defer ticker.Stop() for range ticker.C { if s.db.Scheme() == rawdb.HashScheme { s.db.Cap(DEFAULT_TRIEDBCACHE_SIZE) } runtime.GC() } }() log.Info("Find Account Trie Tree", "rootHash: ", s.trie.Hash().String(), "BlockNum: ", s.blockNum) ts := &trieStat{ owner: common.Hash{}, } s.traversal(s.trie, ts, s.root, 0, []byte{}) s.results.add(ts, s.topN) s.wg.Wait() } func (s *Inspector) traversal(trie *Trie, ts *trieStat, n node, height int, path []byte) { // nil node if n == nil { return } ts.add(n, height) switch current := (n).(type) { case *shortNode: s.traversal(trie, ts, current.Val, height, append(path, current.Key...)) case *fullNode: for idx, child := range current.Children { if child == nil { continue } p := common.CopyBytes(append(path, byte(idx))) s.traversal(trie, ts, child, height+1, p) } case hashNode: tn, err := trie.resloveWithoutTrack(current, path) if err != nil { fmt.Printf("Resolve HashNode error: %v, TrieRoot: %v, Height: %v, Path: %v\n", err, trie.Hash().String(), height+1, path) return } s.PrintProgress(trie) s.traversal(trie, ts, tn, height, path) case valueNode: if !hasTerm(path) { break } var account types.StateAccount if err := rlp.Decode(bytes.NewReader(current), &account); err != nil { break } if common.BytesToHash(account.CodeHash) == types.EmptyCodeHash { s.eoaAccountNums++ } if account.Root == (common.Hash{}) || account.Root == types.EmptyRootHash { break } ownerAddress := common.BytesToHash(hexToCompact(path)) contractTrie, err := New(StorageTrieID(s.stateRootHash, ownerAddress, account.Root), s.db) if err != nil { panic(err) } contractTrie.tracer.reset() if s.sem.TryAcquire(1) { s.wg.Add(1) go func() { t := &trieStat{ owner: ownerAddress, } s.traversal(contractTrie, t, contractTrie.root, 0, []byte{}) s.results.add(t, s.topN) s.sem.Release(1) s.wg.Done() }() } else { t := &trieStat{ owner: ownerAddress, } s.traversal(contractTrie, t, contractTrie.root, 0, []byte{}) s.results.add(t, s.topN) } default: panic(errors.New("invalid node type to traverse")) } } func (s *Inspector) PrintProgress(t *Trie) { var ( elapsed = mclock.Now().Sub(s.lastTime) ) if t.owner == (common.Hash{}) { s.totalAccountNum.Add(1) } else { s.totalStorageNum.Add(1) } if elapsed > 4*time.Second { log.Info("traversal progress", "TotalAccountNum", s.totalAccountNum.Load(), "TotalStorageNum", s.totalStorageNum.Load(), "Goroutine", runtime.NumGoroutine()) s.lastTime = mclock.Now() } } func (s *Inspector) DisplayResult() { // display root hash fmt.Println(s.results.account.Display("", "AccountTrie")) fmt.Println("EOA accounts num: ", s.eoaAccountNums) // display contract trie for _, st := range s.results.storageTopN { fmt.Println(st.Display(st.owner.String(), "StorageTrie")) } fmt.Printf("Contract Trie, total trie num: %v, ShortNodeCnt: %v, FullNodeCnt: %v, ValueNodeCnt: %v\n", s.results.storageTrieNum, s.results.storageTotal.ShortNodeCnt.Load(), s.results.storageTotal.FullNodeCnt.Load(), s.results.storageTotal.ValueNodeCnt.Load()) }