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 Run `devp2p discv5 crawl <nodes.json path>` to create or update a JSON node set containing
discv5 nodes. 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 ### Discovery Test Suites
The devp2p command also contains interactive test suites for Discovery v4 and Discovery 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 ( import (
"errors" "errors"
"fmt" "fmt"
"net/http"
"slices" "slices"
"time" "time"
"github.com/ethereum/go-ethereum/cmd/devp2p/internal/v5test" "github.com/ethereum/go-ethereum/cmd/devp2p/internal/v5test"
"github.com/ethereum/go-ethereum/common" "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/discover"
"github.com/ethereum/go-ethereum/p2p/enode"
"github.com/ethereum/go-ethereum/rpc"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
@ -76,7 +80,9 @@ var (
Name: "listen", Name: "listen",
Usage: "Runs a node", Usage: "Runs a node",
Action: discv5Listen, Action: discv5Listen,
Flags: discoveryNodeFlags, Flags: slices.Concat(discoveryNodeFlags, []cli.Flag{
httpAddrFlag,
}),
} }
) )
@ -137,7 +143,20 @@ func discv5Listen(ctx *cli.Context) error {
defer disc.Close() defer disc.Close()
fmt.Println(disc.Self()) 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. // startV5 starts an ephemeral discovery v5 node.
@ -150,3 +169,19 @@ func startV5(ctx *cli.Context) (*discover.UDPv5, discover.Config) {
} }
return disc, 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, keyCommand,
discv4Command, discv4Command,
discv5Command, discv5Command,
discoveryCommand,
dnsCommand, dnsCommand,
nodesetCommand, nodesetCommand,
rlpxCommand, rlpxCommand,