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 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

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 package main
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"net" "net"
"net/http"
"slices" "slices"
"strconv" "strconv"
"strings" "strings"
@ -29,11 +29,9 @@ import (
"github.com/ethereum/go-ethereum/cmd/devp2p/internal/v4test" "github.com/ethereum/go-ethereum/cmd/devp2p/internal/v4test"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto" "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/discover"
"github.com/ethereum/go-ethereum/p2p/enode" "github.com/ethereum/go-ethereum/p2p/enode"
"github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rpc"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
@ -176,19 +174,7 @@ func discv4Listen(ctx *cli.Context) error {
fmt.Println(disc.Self()) fmt.Println(disc.Self())
httpAddr := ctx.String(httpAddrFlag.Name) return runRPCServer(ctx.String(httpAddrFlag.Name), map[string]any{"discv4": &discv4API{disc}})
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()
} }
func discv4RequestRecord(ctx *cli.Context) error { func discv4RequestRecord(ctx *cli.Context) error {
@ -428,13 +414,14 @@ type discv4API struct {
host *discover.UDPv4 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() it := api.host.RandomNodes()
defer it.Close() defer it.Close()
for len(ns) < n && it.Next() { go func() {
ns = append(ns, it.Node()) <-ctx.Done()
} it.Close()
return ns }()
return enode.ReadNodes(it, n)
} }
func (api *discv4API) Buckets() [][]discover.BucketNode { func (api *discv4API) Buckets() [][]discover.BucketNode {

View file

@ -17,6 +17,7 @@
package main package main
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"slices" "slices"
@ -25,6 +26,7 @@ import (
"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/p2p/discover" "github.com/ethereum/go-ethereum/p2p/discover"
"github.com/ethereum/go-ethereum/p2p/enode"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
@ -76,7 +78,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 +141,8 @@ func discv5Listen(ctx *cli.Context) error {
defer disc.Close() defer disc.Close()
fmt.Println(disc.Self()) fmt.Println(disc.Self())
select {}
return runRPCServer(ctx.String(httpAddrFlag.Name), map[string]any{"discv5": &discv5API{disc}})
} }
// startV5 starts an ephemeral discovery v5 node. // startV5 starts an ephemeral discovery v5 node.
@ -150,3 +155,21 @@ 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(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 ( import (
"fmt" "fmt"
"net/http"
"os" "os"
"github.com/ethereum/go-ethereum/internal/debug" "github.com/ethereum/go-ethereum/internal/debug"
"github.com/ethereum/go-ethereum/internal/flags" "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/p2p/enode"
"github.com/ethereum/go-ethereum/rpc"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
@ -49,6 +52,7 @@ func init() {
keyCommand, keyCommand,
discv4Command, discv4Command,
discv5Command, discv5Command,
discoveryCommand,
dnsCommand, dnsCommand,
nodesetCommand, nodesetCommand,
rlpxCommand, rlpxCommand,
@ -92,6 +96,22 @@ func getNodeArg(ctx *cli.Context) *enode.Node {
return n 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{}) { func exit(err interface{}) {
if err == nil { if err == nil {
os.Exit(0) os.Exit(0)

View file

@ -21,6 +21,7 @@ import (
"crypto/ecdsa" "crypto/ecdsa"
crand "crypto/rand" crand "crypto/rand"
"encoding/binary" "encoding/binary"
"errors"
"iter" "iter"
"math/rand" "math/rand"
"net" "net"
@ -105,6 +106,30 @@ type ReadPacket struct {
Addr netip.AddrPort 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 { type randomSource interface {
Intn(int) int Intn(int) int
Int63n(int64) int64 Int63n(int64) int64

View file

@ -330,32 +330,6 @@ func (srv *Server) Stop() {
srv.loopWG.Wait() 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. // Start starts running the server.
// Servers can not be re-used after stopping. // Servers can not be re-used after stopping.
func (srv *Server) Start() (err error) { func (srv *Server) Start() (err error) {
@ -463,7 +437,7 @@ func (srv *Server) setupDiscovery() error {
// connection, so v5 can read unhandled messages from v4. // connection, so v5 can read unhandled messages from v4.
if srv.Config.DiscoveryV4 && srv.Config.DiscoveryV5 { if srv.Config.DiscoveryV4 && srv.Config.DiscoveryV5 {
unhandled = make(chan discover.ReadPacket, 100) unhandled = make(chan discover.ReadPacket, 100)
sconn = &sharedUDPConn{conn, unhandled} sconn = &discover.SharedUDPConn{UDPConn: conn, Unhandled: unhandled}
} }
// Start discovery services. // Start discovery services.