cmd/devp2p: add combined discovery listener

Add `devp2p discovery listen`, which runs all supported discovery
protocol versions (discv4 and discv5) on a single UDP socket, the way a
real node does. For a single-protocol node, `discv4 listen` /
`discv5 listen` remain.

Also expose the --rpc debug API on `discv5 listen`, for parity with
`discv4 listen`.
This commit is contained in:
Chase Wright 2026-06-26 09:08:14 -05:00
parent 3ab52d837d
commit 95a3b35e97
No known key found for this signature in database
GPG key ID: 7EA41EDCB1BE04A7
4 changed files with 157 additions and 2 deletions

View file

@ -80,6 +80,13 @@ Run `devp2p discv5 listen` to run a Discovery v5 node.
Run `devp2p discv5 crawl <nodes.json path>` 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 <addr>` 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

112
cmd/devp2p/discoverycmd.go Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
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
}

View file

@ -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()
}

View file

@ -49,6 +49,7 @@ func init() {
keyCommand,
discv4Command,
discv5Command,
discoveryCommand,
dnsCommand,
nodesetCommand,
rlpxCommand,