diff --git a/cmd/devp2p/README.md b/cmd/devp2p/README.md index b20d921dc4..8ac1b25cc6 100644 --- a/cmd/devp2p/README.md +++ b/cmd/devp2p/README.md @@ -80,6 +80,13 @@ Run `devp2p discv5 listen` to run a Discovery v5 node. Run `devp2p discv5 crawl ` to create or update a JSON node set containing discv5 nodes. +### Combined Discovery + +Run `devp2p discovery listen` to run a node speaking all supported discovery protocol +versions (currently discv4 and discv5) on a single UDP port. For a single-protocol node, use +`devp2p discv4 listen` or `devp2p discv5 listen`. Add `--rpc ` to expose the +per-protocol HTTP API (`discv4_*`, `discv5_*`). + ### Discovery Test Suites The devp2p command also contains interactive test suites for Discovery v4 and Discovery diff --git a/cmd/devp2p/discoverycmd.go b/cmd/devp2p/discoverycmd.go new file mode 100644 index 0000000000..9872fb3697 --- /dev/null +++ b/cmd/devp2p/discoverycmd.go @@ -0,0 +1,112 @@ +// Copyright 2026 The go-ethereum Authors +// This file is part of go-ethereum. +// +// go-ethereum is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// go-ethereum 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with go-ethereum. If not, see . + +package main + +import ( + "errors" + "fmt" + "net" + "net/http" + "net/netip" + "slices" + + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/p2p/discover" + "github.com/ethereum/go-ethereum/rpc" + "github.com/urfave/cli/v2" +) + +var ( + discoveryCommand = &cli.Command{ + Name: "discovery", + Usage: "Node Discovery tools", + Subcommands: []*cli.Command{ + discoveryListenCommand, + }, + } + discoveryListenCommand = &cli.Command{ + Name: "listen", + Usage: "Runs a discovery node speaking all supported protocol versions on one UDP port", + Action: discoveryListen, + Flags: slices.Concat(discoveryNodeFlags, []cli.Flag{ + httpAddrFlag, + }), + } +) + +func discoveryListen(ctx *cli.Context) error { + ln, config := makeDiscoveryConfig(ctx) + socket := listen(ctx, ln) + + // v4 is the primary listener on the real socket and forwards packets it + // can't parse to v5 via the unhandled channel. + unhandled := make(chan discover.ReadPacket, 100) + config.Unhandled = unhandled + v4, err := discover.ListenV4(socket, ln, config) + if err != nil { + exit(err) + } + config.Unhandled = nil + v5, err := discover.ListenV5(&sharedUDPConn{socket, unhandled}, ln, config) + if err != nil { + exit(err) + } + // Close v4 before v5: when sharing the socket, v5's read loop only unblocks + // once v4 closes the underlying socket and the unhandled channel. + defer func() { + v4.Close() + v5.Close() + }() + + // v4 and v5 share the same local node, so this is the single record both announce. + fmt.Println(ln.Node()) + + httpAddr := ctx.String(httpAddrFlag.Name) + if httpAddr == "" { + select {} + } + + srv := rpc.NewServer() + srv.RegisterName("discv4", &discv4API{v4}) + srv.RegisterName("discv5", &discv5API{v5}) + log.Info("Starting RPC API server", "addr", httpAddr) + http.DefaultServeMux.Handle("/", srv) + httpsrv := http.Server{Addr: httpAddr, Handler: http.DefaultServeMux} + return httpsrv.ListenAndServe() +} + +// sharedUDPConn implements a shared connection, identical to the one used by +// p2p.Server: reads return packets the primary v4 listener found unprocessable +// and forwarded on the unhandled channel. +type sharedUDPConn struct { + *net.UDPConn + unhandled chan discover.ReadPacket +} + +func (s *sharedUDPConn) ReadFromUDPAddrPort(b []byte) (n int, addr netip.AddrPort, err error) { + packet, ok := <-s.unhandled + if !ok { + return 0, netip.AddrPort{}, errors.New("connection was closed") + } + l := min(len(packet.Data), len(b)) + copy(b[:l], packet.Data[:l]) + return l, packet.Addr, nil +} + +func (s *sharedUDPConn) Close() error { + return nil +} diff --git a/cmd/devp2p/discv5cmd.go b/cmd/devp2p/discv5cmd.go index 8b39829e60..2bbc7c437b 100644 --- a/cmd/devp2p/discv5cmd.go +++ b/cmd/devp2p/discv5cmd.go @@ -19,12 +19,16 @@ package main import ( "errors" "fmt" + "net/http" "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/rpc" "github.com/urfave/cli/v2" ) @@ -76,7 +80,9 @@ var ( Name: "listen", Usage: "Runs a node", Action: discv5Listen, - Flags: discoveryNodeFlags, + Flags: slices.Concat(discoveryNodeFlags, []cli.Flag{ + httpAddrFlag, + }), } ) @@ -137,7 +143,20 @@ func discv5Listen(ctx *cli.Context) error { defer disc.Close() fmt.Println(disc.Self()) - select {} + + httpAddr := ctx.String(httpAddrFlag.Name) + if httpAddr == "" { + // Non-HTTP mode. + select {} + } + + api := &discv5API{disc} + log.Info("Starting RPC API server", "addr", httpAddr) + srv := rpc.NewServer() + srv.RegisterName("discv5", api) + http.DefaultServeMux.Handle("/", srv) + httpsrv := http.Server{Addr: httpAddr, Handler: http.DefaultServeMux} + return httpsrv.ListenAndServe() } // startV5 starts an ephemeral discovery v5 node. @@ -150,3 +169,19 @@ func startV5(ctx *cli.Context) (*discover.UDPv5, discover.Config) { } return disc, config } + +type discv5API struct { + host *discover.UDPv5 +} + +func (api *discv5API) LookupRandom(n int) (ns []*enode.Node) { + it := api.host.RandomNodes() + for len(ns) < n && it.Next() { + ns = append(ns, it.Node()) + } + return ns +} + +func (api *discv5API) Self() *enode.Node { + return api.host.Self() +} diff --git a/cmd/devp2p/main.go b/cmd/devp2p/main.go index 66974bba58..f33a63962e 100644 --- a/cmd/devp2p/main.go +++ b/cmd/devp2p/main.go @@ -49,6 +49,7 @@ func init() { keyCommand, discv4Command, discv5Command, + discoveryCommand, dnsCommand, nodesetCommand, rlpxCommand,