This commit is contained in:
Chase Wright 2026-07-17 18:22:39 -05:00 committed by GitHub
commit 6cdd6ce537
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 162 additions and 50 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

View file

@ -0,0 +1,76 @@
// 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 (
"fmt"
"slices"
"github.com/ethereum/go-ethereum/p2p/discover"
"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)
v4cfg := config
v4cfg.Unhandled = unhandled
v4, err := discover.ListenV4(socket, ln, v4cfg)
if err != nil {
exit(err)
}
v5, err := discover.ListenV5(&discover.SharedUDPConn{UDPConn: socket, Unhandled: 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())
return runRPCServer(ctx.String(httpAddrFlag.Name), map[string]any{
"discv4": &discv4API{v4},
"discv5": &discv5API{v5},
})
}

View file

@ -17,10 +17,10 @@
package main
import (
"context"
"errors"
"fmt"
"net"
"net/http"
"slices"
"strconv"
"strings"
@ -29,11 +29,9 @@ import (
"github.com/ethereum/go-ethereum/cmd/devp2p/internal/v4test"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"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/params"
"github.com/ethereum/go-ethereum/rpc"
"github.com/urfave/cli/v2"
)
@ -176,19 +174,7 @@ func discv4Listen(ctx *cli.Context) error {
fmt.Println(disc.Self())
httpAddr := ctx.String(httpAddrFlag.Name)
if httpAddr == "" {
// Non-HTTP mode.
select {}
}
api := &discv4API{disc}
log.Info("Starting RPC API server", "addr", httpAddr)
srv := rpc.NewServer()
srv.RegisterName("discv4", api)
http.DefaultServeMux.Handle("/", srv)
httpsrv := http.Server{Addr: httpAddr, Handler: http.DefaultServeMux}
return httpsrv.ListenAndServe()
return runRPCServer(ctx.String(httpAddrFlag.Name), map[string]any{"discv4": &discv4API{disc}})
}
func discv4RequestRecord(ctx *cli.Context) error {
@ -428,13 +414,14 @@ type discv4API struct {
host *discover.UDPv4
}
func (api *discv4API) LookupRandom(n int) (ns []*enode.Node) {
func (api *discv4API) LookupRandom(ctx context.Context, n int) []*enode.Node {
it := api.host.RandomNodes()
defer it.Close()
for len(ns) < n && it.Next() {
ns = append(ns, it.Node())
}
return ns
go func() {
<-ctx.Done()
it.Close()
}()
return enode.ReadNodes(it, n)
}
func (api *discv4API) Buckets() [][]discover.BucketNode {

View file

@ -17,6 +17,7 @@
package main
import (
"context"
"errors"
"fmt"
"slices"
@ -25,6 +26,7 @@ import (
"github.com/ethereum/go-ethereum/cmd/devp2p/internal/v5test"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/p2p/discover"
"github.com/ethereum/go-ethereum/p2p/enode"
"github.com/urfave/cli/v2"
)
@ -76,7 +78,9 @@ var (
Name: "listen",
Usage: "Runs a node",
Action: discv5Listen,
Flags: discoveryNodeFlags,
Flags: slices.Concat(discoveryNodeFlags, []cli.Flag{
httpAddrFlag,
}),
}
)
@ -137,7 +141,8 @@ func discv5Listen(ctx *cli.Context) error {
defer disc.Close()
fmt.Println(disc.Self())
select {}
return runRPCServer(ctx.String(httpAddrFlag.Name), map[string]any{"discv5": &discv5API{disc}})
}
// startV5 starts an ephemeral discovery v5 node.
@ -150,3 +155,21 @@ func startV5(ctx *cli.Context) (*discover.UDPv5, discover.Config) {
}
return disc, config
}
type discv5API struct {
host *discover.UDPv5
}
func (api *discv5API) LookupRandom(ctx context.Context, n int) []*enode.Node {
it := api.host.RandomNodes()
defer it.Close()
go func() {
<-ctx.Done()
it.Close()
}()
return enode.ReadNodes(it, n)
}
func (api *discv5API) Self() *enode.Node {
return api.host.Self()
}

View file

@ -18,11 +18,14 @@ package main
import (
"fmt"
"net/http"
"os"
"github.com/ethereum/go-ethereum/internal/debug"
"github.com/ethereum/go-ethereum/internal/flags"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/p2p/enode"
"github.com/ethereum/go-ethereum/rpc"
"github.com/urfave/cli/v2"
)
@ -49,6 +52,7 @@ func init() {
keyCommand,
discv4Command,
discv5Command,
discoveryCommand,
dnsCommand,
nodesetCommand,
rlpxCommand,
@ -92,6 +96,22 @@ func getNodeArg(ctx *cli.Context) *enode.Node {
return n
}
// runRPCServer serves the given RPC APIs on httpAddr; if httpAddr is empty, it blocks forever without serving.
func runRPCServer(httpAddr string, apis map[string]any) error {
if httpAddr == "" {
select {}
}
srv := rpc.NewServer()
for name, api := range apis {
if err := srv.RegisterName(name, api); err != nil {
return err
}
}
log.Info("Starting RPC API server", "addr", httpAddr)
httpsrv := http.Server{Addr: httpAddr, Handler: srv}
return httpsrv.ListenAndServe()
}
func exit(err interface{}) {
if err == nil {
os.Exit(0)

View file

@ -21,6 +21,7 @@ import (
"crypto/ecdsa"
crand "crypto/rand"
"encoding/binary"
"errors"
"iter"
"math/rand"
"net"
@ -105,6 +106,30 @@ type ReadPacket struct {
Addr netip.AddrPort
}
// SharedUDPConn implements a shared connection. Write sends messages to the underlying
// connection while read returns messages that were found unprocessable and sent to the
// Unhandled channel by the primary listener.
type SharedUDPConn struct {
*net.UDPConn
Unhandled chan ReadPacket
}
// ReadFromUDPAddrPort implements UDPConn.
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
}
// Close implements UDPConn. It does not close the underlying connection.
func (s *SharedUDPConn) Close() error {
return nil
}
type randomSource interface {
Intn(int) int
Int63n(int64) int64

View file

@ -330,32 +330,6 @@ func (srv *Server) Stop() {
srv.loopWG.Wait()
}
// sharedUDPConn implements a shared connection. Write sends messages to the underlying connection while read returns
// messages that were found unprocessable and sent to the unhandled channel by the primary listener.
type sharedUDPConn struct {
*net.UDPConn
unhandled chan discover.ReadPacket
}
// ReadFromUDPAddrPort implements discover.UDPConn
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 := len(packet.Data)
if l > len(b) {
l = len(b)
}
copy(b[:l], packet.Data[:l])
return l, packet.Addr, nil
}
// Close implements discover.UDPConn
func (s *sharedUDPConn) Close() error {
return nil
}
// Start starts running the server.
// Servers can not be re-used after stopping.
func (srv *Server) Start() (err error) {
@ -463,7 +437,7 @@ func (srv *Server) setupDiscovery() error {
// connection, so v5 can read unhandled messages from v4.
if srv.Config.DiscoveryV4 && srv.Config.DiscoveryV5 {
unhandled = make(chan discover.ReadPacket, 100)
sconn = &sharedUDPConn{conn, unhandled}
sconn = &discover.SharedUDPConn{UDPConn: conn, Unhandled: unhandled}
}
// Start discovery services.