diff --git a/cmd/devp2p/discv5cmd.go b/cmd/devp2p/discv5cmd.go index dd253dd082..77f8863a22 100644 --- a/cmd/devp2p/discv5cmd.go +++ b/cmd/devp2p/discv5cmd.go @@ -17,18 +17,37 @@ package main import ( + "bytes" + "encoding/binary" "errors" "fmt" + "os" "slices" "time" "github.com/ethereum/go-ethereum/cmd/devp2p/internal/v5test" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/p2p/discover" + "github.com/ethereum/go-ethereum/p2p/enode" + "github.com/ethereum/go-ethereum/p2p/enr" + "github.com/ethereum/go-ethereum/rlp" "github.com/urfave/cli/v2" ) var ( + discv5DumpFlag = &cli.BoolFlag{ + Name: "dump", + Usage: "Dump all peers every 10 seconds", + } + opStackChainIDFlag = &cli.Uint64Flag{ + Name: "opstack-chainid", + Usage: "Filter nodes by OP Stack chain ID (only nodes with matching opstack ENR entry will be accepted)", + } + chainIDFlag = &cli.Uint64Flag{ + Name: "chainid", + Usage: "Filter nodes by chain ID (only nodes with matching chainID ENR entry will be accepted)", + } discv5Command = &cli.Command{ Name: "discv5", Usage: "Node Discovery v5 tools", @@ -75,7 +94,7 @@ var ( Name: "listen", Usage: "Runs a node", Action: discv5Listen, - Flags: discoveryNodeFlags, + Flags: slices.Concat(discoveryNodeFlags, []cli.Flag{discv5DumpFlag, opStackChainIDFlag, chainIDFlag}), } ) @@ -136,12 +155,105 @@ func discv5Listen(ctx *cli.Context) error { defer disc.Close() fmt.Println(disc.Self()) + + if ctx.Bool(discv5DumpFlag.Name) { + ticker := time.NewTicker(10 * time.Second) + defer ticker.Stop() + for range ticker.C { + nodes := disc.AllNodes() + fmt.Printf("\n--- Dumping %d peers ---\n", len(nodes)) + for i, n := range nodes { + fmt.Printf("\n[%d] %s\n", i+1, n.String()) + dumpRecord(os.Stdout, n.Record()) + } + } + } + select {} } +// opStackENRData is the ENR entry for OP Stack chain identification. +type opStackENRData struct { + chainID uint64 + version uint64 +} + +func (o *opStackENRData) ENRKey() string { return "opstack" } + +func (o *opStackENRData) DecodeRLP(s *rlp.Stream) error { + b, err := s.Bytes() + if err != nil { + return fmt.Errorf("failed to decode outer ENR entry: %w", err) + } + r := bytes.NewReader(b) + chainID, err := binary.ReadUvarint(r) + if err != nil { + return fmt.Errorf("failed to read chain ID var int: %w", err) + } + version, err := binary.ReadUvarint(r) + if err != nil { + return fmt.Errorf("failed to read version var int: %w", err) + } + o.chainID = chainID + o.version = version + return nil +} + +var _ enr.Entry = (*opStackENRData)(nil) + +// chainIDENRData is the ENR entry for chain ID identification. +type chainIDENRData uint64 + +func (c chainIDENRData) ENRKey() string { return "chainID" } + +var _ enr.Entry = (*chainIDENRData)(nil) + // startV5 starts an ephemeral discovery v5 node. func startV5(ctx *cli.Context) (*discover.UDPv5, discover.Config) { ln, config := makeDiscoveryConfig(ctx) + + // Set up OP Stack chain ID filter if specified + if ctx.IsSet(opStackChainIDFlag.Name) { + expectedChainID := ctx.Uint64(opStackChainIDFlag.Name) + config.NodeFilter = func(node *enode.Node) bool { + var dat opStackENRData + if err := node.Load(&dat); err != nil { + log.Debug("Node has no opstack ENR entry", "id", node.ID(), "ip", node.IP(), "err", err) + return false + } + if dat.chainID != expectedChainID { + log.Debug("Node has different chain ID", "id", node.ID(), "ip", node.IP(), "got", dat.chainID, "expected", expectedChainID) + return false + } + if dat.version != 0 { + log.Debug("Node has different version", "id", node.ID(), "ip", node.IP(), "got", dat.version, "expected", 0) + return false + } + log.Info("Node passed filter", "id", node.ID(), "ip", node.IP(), "chainID", dat.chainID) + return true + } + log.Info("OP Stack node filter enabled", "chainID", expectedChainID) + } + + // Set up chain ID filter if specified + if ctx.IsSet(chainIDFlag.Name) { + expectedChainID := ctx.Uint64(chainIDFlag.Name) + config.NodeFilter = func(node *enode.Node) bool { + var dat chainIDENRData + if err := node.Load(&dat); err != nil { + log.Debug("Node has no chainID ENR entry", "id", node.ID(), "ip", node.IP(), "err", err) + return false + } + if uint64(dat) != expectedChainID { + log.Debug("Node has different chain ID", "id", node.ID(), "ip", node.IP(), "got", uint64(dat), "expected", expectedChainID) + return false + } + log.Info("Node passed chainID filter", "id", node.ID(), "ip", node.IP(), "chainID", uint64(dat)) + return true + } + log.Info("Chain ID node filter enabled", "chainID", expectedChainID) + } + socket := listen(ctx, ln) disc, err := discover.ListenV5(socket, ln, config) if err != nil { diff --git a/p2p/discover/common.go b/p2p/discover/common.go index 767cc23b92..7a04d27869 100644 --- a/p2p/discover/common.go +++ b/p2p/discover/common.go @@ -54,10 +54,11 @@ type Config struct { V5RespTimeout time.Duration // timeout for v5 queries // Node table configuration: - Bootnodes []*enode.Node // list of bootstrap nodes - PingInterval time.Duration // speed of node liveness check - RefreshInterval time.Duration // used in bucket refresh - NoFindnodeLivenessCheck bool // turns off validation of table nodes in FINDNODE handler + Bootnodes []*enode.Node // list of bootstrap nodes + PingInterval time.Duration // speed of node liveness check + RefreshInterval time.Duration // used in bucket refresh + NoFindnodeLivenessCheck bool // turns off validation of table nodes in FINDNODE handler + NodeFilter func(*enode.Node) bool // filter function for discovered nodes; if set, only nodes passing the filter are added to the table // The options below are useful in very specific cases, like in unit tests. V5ProtocolID *[6]byte diff --git a/p2p/discover/table.go b/p2p/discover/table.go index b6c35aaaa9..4a6da4fcf1 100644 --- a/p2p/discover/table.go +++ b/p2p/discover/table.go @@ -514,6 +514,10 @@ func (tab *Table) handleAddNode(req addNodeOp) bool { if req.isInbound && !tab.isInitDone() { return false } + // Apply node filter if configured. + if tab.cfg.NodeFilter != nil && !tab.cfg.NodeFilter(req.node) { + return false + } b := tab.bucket(req.node.ID()) n, _ := tab.bumpInBucket(b, req.node, req.isInbound) diff --git a/p2p/discover/table_reval.go b/p2p/discover/table_reval.go index 1519313d19..d4a82978c9 100644 --- a/p2p/discover/table_reval.go +++ b/p2p/discover/table_reval.go @@ -145,11 +145,13 @@ func (tr *tableRevalidation) handleResponse(tab *Table, resp revalidationRespons return } - // Store potential seeds in database. + // Store potential seeds in database and update last pong time. // This is done via defer to avoid holding Table lock while writing to DB. defer func() { if n.isValidatedLive && n.livenessChecks > 5 { tab.db.UpdateNode(resp.n.Node) + // Update last pong time so QuerySeeds can find this node + tab.db.UpdateLastPongReceived(resp.n.ID(), resp.n.IPAddr(), time.Now()) } }()