From 2509c2e3a04cf972c8d3be14d903c0632ec48dcb Mon Sep 17 00:00:00 2001
From: sonhv0212 <son.ho@skymavis.com>
Date: Wed, 18 Dec 2024 18:21:46 +0700
Subject: [PATCH] cmd/db: add inspect-enodedb cli to analyze enode database
 (#649)

inspect-enodedb command inspects nodes in the enode db, calculates the peer (outbound) rate with them
---
 cmd/ronin/dbcmd.go  | 92 +++++++++++++++++++++++++++++++++++++++++++++
 p2p/enode/nodedb.go | 23 ++++++++++++
 2 files changed, 115 insertions(+)

diff --git a/cmd/ronin/dbcmd.go b/cmd/ronin/dbcmd.go
index 56fe03a3bd..6b33ced82e 100644
--- a/cmd/ronin/dbcmd.go
+++ b/cmd/ronin/dbcmd.go
@@ -18,6 +18,7 @@ package main
 
 import (
 	"bytes"
+	"encoding/json"
 	"errors"
 	"fmt"
 	"os"
@@ -33,9 +34,14 @@ import (
 	"github.com/ethereum/go-ethereum/common"
 	"github.com/ethereum/go-ethereum/common/hexutil"
 	"github.com/ethereum/go-ethereum/console/prompt"
+	"github.com/ethereum/go-ethereum/core/forkid"
 	"github.com/ethereum/go-ethereum/core/rawdb"
 	"github.com/ethereum/go-ethereum/ethdb"
 	"github.com/ethereum/go-ethereum/log"
+	"github.com/ethereum/go-ethereum/p2p"
+	"github.com/ethereum/go-ethereum/p2p/enode"
+	"github.com/ethereum/go-ethereum/p2p/enr"
+	"github.com/ethereum/go-ethereum/rlp"
 	"github.com/ethereum/go-ethereum/trie"
 	"github.com/urfave/cli/v2"
 )
@@ -69,6 +75,7 @@ Remove blockchain and state databases`,
 			dbDumpFreezerIndex,
 			dbImportCmd,
 			dbExportCmd,
+			dbInspectEnodeDBCmd,
 		},
 	}
 	dbInspectCmd = &cli.Command{
@@ -243,8 +250,93 @@ WARNING: This is a low-level operation which may cause database corruption!`,
 		},
 		Description: "Exports the specified chain data to an RLP encoded stream, optionally gzip-compressed.",
 	}
+	dbInspectEnodeDBCmd = &cli.Command{
+		Action: dbInspectEnodeDB,
+		Name:   "inspect-enodedb",
+		Usage:  "Inspect nodes in enode db",
+		Flags: []cli.Flag{
+			&cli.StringFlag{
+				Name:     "enodedb",
+				Usage:    "Path to the enode database directory",
+				Required: true,
+			},
+			&cli.StringFlag{
+				Name:  "peersfile",
+				Usage: "File containing a list of peers from admin.peers",
+			},
+		},
+		Category: "DATABASE COMMANDS",
+	}
 )
 
+func dbInspectEnodeDB(ctx *cli.Context) error {
+	path := ctx.String("enodedb")
+	db, err := enode.OpenDB(path)
+	if err != nil {
+		return err
+	}
+
+	nodeCount := 0
+	inspectedNodes := make(map[string]*enode.Node)
+	unknownNodes := []*enode.Node{}
+	db.IterateNodes(func(n *enode.Node) error {
+		nodeCount++
+		var eth struct {
+			ForkID forkid.ID
+			Tail   []rlp.RawValue `rlp:"tail"`
+		}
+		if n.Record().Load(enr.WithEntry("eth", &eth)) == nil {
+			log.Info("Node", "ID", n.ID(), "IP", n.IP(), "UDP", n.UDP(), "TCP", n.TCP(), "eth", eth)
+		} else {
+			unknownNodes = append(unknownNodes, n)
+		}
+		inspectedNodes[n.ID().String()] = n
+		return nil
+	})
+
+	for _, n := range unknownNodes {
+		log.Info("Unknown node", "ID", n.ID(), "IP", n.IP(), "UDP", n.UDP(), "TCP", n.TCP())
+	}
+	log.Info("Total nodes", "count", nodeCount, "unknown", len(unknownNodes))
+
+	// Peers file is optional to calculate the rate of peers in Enode DB
+	if ctx.IsSet("peersfile") {
+		f, err := os.Open(ctx.String("peersfile"))
+		if err != nil {
+			return err
+		}
+
+		peersInfo := []*p2p.PeerInfo{}
+		decoder := json.NewDecoder(f)
+		err = decoder.Decode(&peersInfo)
+		if err != nil {
+			return err
+		}
+
+		foundInEnodeDB := 0
+		outbound := 0
+		for _, peerInfo := range peersInfo {
+			if peerInfo.Network.Inbound {
+				continue
+			}
+
+			outbound++
+			if _, ok := inspectedNodes[peerInfo.ID]; ok {
+				foundInEnodeDB++
+				log.Info("Found peer in EnodeDB", "ID", peerInfo.ID, "Network", peerInfo.Network)
+			} else {
+				log.Info("Peer not found in EnodeDB", "ID", peerInfo.ID, "Network", peerInfo.Network)
+			}
+		}
+
+		log.Info("Peers in EnodeDB", "total", len(peersInfo), "found", foundInEnodeDB,
+			"not_found", outbound-foundInEnodeDB, "rate", float64(foundInEnodeDB)/float64(outbound),
+			"inbound", len(peersInfo)-outbound, "outbound", outbound)
+	}
+
+	return nil
+}
+
 func removeDB(ctx *cli.Context) error {
 	stack, config := makeConfigNode(ctx)
 
diff --git a/p2p/enode/nodedb.go b/p2p/enode/nodedb.go
index d1712f7597..0be448776c 100644
--- a/p2p/enode/nodedb.go
+++ b/p2p/enode/nodedb.go
@@ -26,6 +26,7 @@ import (
 	"sync"
 	"time"
 
+	"github.com/ethereum/go-ethereum/log"
 	"github.com/ethereum/go-ethereum/rlp"
 	"github.com/syndtr/goleveldb/leveldb"
 	"github.com/syndtr/goleveldb/leveldb/errors"
@@ -481,6 +482,28 @@ seek:
 	return nodes
 }
 
+// Testing purposes only.
+func (db *DB) IterateNodes(f func(n *Node) error) {
+	it := db.lvl.NewIterator(util.BytesPrefix([]byte(dbNodePrefix)), nil)
+	defer it.Release()
+
+	for it.Next() {
+		id, rest := splitNodeKey(it.Key())
+		if string(rest) != dbDiscoverRoot {
+			continue
+		}
+		node := mustDecodeNode(id[:], it.Value())
+		if node == nil {
+			return
+		}
+
+		if err := f(node); err != nil {
+			log.Error("error during node iteration", "err", err)
+			return
+		}
+	}
+}
+
 // reads the next node record from the iterator, skipping over other
 // database entries.
 func nextNode(it iterator.Iterator) *Node {