Merge pull request #1267 from maticnetwork/psp-p2p-upstream

p2p: cherry-pick commits from geth for peering issues
This commit is contained in:
Pratik Patil 2024-06-13 13:00:54 +05:30 committed by GitHub
commit bd5d54e9fe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 1888 additions and 1075 deletions

View file

@ -20,6 +20,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"net" "net"
"net/http"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@ -30,9 +31,11 @@ import (
"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/internal/flags" "github.com/ethereum/go-ethereum/internal/flags"
"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"
) )
var ( var (
@ -46,6 +49,7 @@ var (
discv4ResolveJSONCommand, discv4ResolveJSONCommand,
discv4CrawlCommand, discv4CrawlCommand,
discv4TestCommand, discv4TestCommand,
discv4ListenCommand,
}, },
} }
discv4PingCommand = &cli.Command{ discv4PingCommand = &cli.Command{
@ -76,6 +80,14 @@ var (
Flags: discoveryNodeFlags, Flags: discoveryNodeFlags,
ArgsUsage: "<nodes.json file>", ArgsUsage: "<nodes.json file>",
} }
discv4ListenCommand = &cli.Command{
Name: "listen",
Usage: "Runs a discovery node",
Action: discv4Listen,
Flags: flags.Merge(discoveryNodeFlags, []cli.Flag{
httpAddrFlag,
}),
}
discv4CrawlCommand = &cli.Command{ discv4CrawlCommand = &cli.Command{
Name: "crawl", Name: "crawl",
Usage: "Updates a nodes.json file with random nodes found in the DHT", Usage: "Updates a nodes.json file with random nodes found in the DHT",
@ -132,6 +144,10 @@ var (
Usage: "Enode of the remote node under test", Usage: "Enode of the remote node under test",
EnvVars: []string{"REMOTE_ENODE"}, EnvVars: []string{"REMOTE_ENODE"},
} }
httpAddrFlag = &cli.StringFlag{
Name: "rpc",
Usage: "HTTP server listening address",
}
) )
var discoveryNodeFlags = []cli.Flag{ var discoveryNodeFlags = []cli.Flag{
@ -158,6 +174,27 @@ func discv4Ping(ctx *cli.Context) error {
return nil return nil
} }
func discv4Listen(ctx *cli.Context) error {
disc, _ := startV4(ctx)
defer disc.Close()
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("", 0, 0)
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 {
n := getNodeArg(ctx) n := getNodeArg(ctx)
disc, _ := startV4(ctx) disc, _ := startV4(ctx)
@ -401,3 +438,23 @@ func parseBootnodes(ctx *cli.Context) ([]*enode.Node, error) {
return nodes, nil return nodes, nil
} }
type discv4API struct {
host *discover.UDPv4
}
func (api *discv4API) 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 *discv4API) Buckets() [][]discover.BucketNode {
return api.host.TableBuckets()
}
func (api *discv4API) Self() *enode.Node {
return api.host.Self()
}

View file

@ -128,7 +128,7 @@ func (te *testenv) localEndpoint(c net.PacketConn) v4wire.Endpoint {
} }
func (te *testenv) remoteEndpoint() v4wire.Endpoint { func (te *testenv) remoteEndpoint() v4wire.Endpoint {
return v4wire.NewEndpoint(te.remoteAddr, 0) return v4wire.NewEndpoint(te.remoteAddr.AddrPort(), 0)
} }
func contains(ns []v4wire.Node, key v4wire.Pubkey) bool { func contains(ns []v4wire.Node, key v4wire.Pubkey) bool {

View file

@ -55,7 +55,7 @@ func (h *bufHandler) Handle(_ context.Context, r slog.Record) error {
} }
func (h *bufHandler) Enabled(_ context.Context, lvl slog.Level) bool { func (h *bufHandler) Enabled(_ context.Context, lvl slog.Level) bool {
return lvl <= h.level return lvl >= h.level
} }
func (h *bufHandler) WithAttrs(attrs []slog.Attr) slog.Handler { func (h *bufHandler) WithAttrs(attrs []slog.Attr) slog.Handler {

View file

@ -27,6 +27,7 @@ import (
"github.com/ethereum/go-ethereum/internal/debug" "github.com/ethereum/go-ethereum/internal/debug"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/p2p"
"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/rpc" "github.com/ethereum/go-ethereum/rpc"
) )
@ -40,6 +41,9 @@ func (n *Node) apis() []rpc.API {
}, { }, {
Namespace: "debug", Namespace: "debug",
Service: debug.Handler, Service: debug.Handler,
}, {
Namespace: "debug",
Service: &p2pDebugAPI{n},
}, { }, {
Namespace: "web3", Namespace: "web3",
Service: &web3API{n}, Service: &web3API{n},
@ -477,3 +481,16 @@ func (api *adminAPI) SetHttpExecutionPoolSize(n int) *ExecutionPoolSize {
return api.GetExecutionPoolSize() return api.GetExecutionPoolSize()
} }
// p2pDebugAPI provides access to p2p internals for debugging.
type p2pDebugAPI struct {
stack *Node
}
func (s *p2pDebugAPI) DiscoveryV4Table() [][]discover.BucketNode {
disc := s.stack.server.DiscoveryV4()
if disc != nil {
return disc.TableBuckets()
}
return nil
}

View file

@ -25,6 +25,7 @@ import (
mrand "math/rand" mrand "math/rand"
"net" "net"
"sync" "sync"
"sync/atomic"
"time" "time"
"github.com/ethereum/go-ethereum/common/mclock" "github.com/ethereum/go-ethereum/common/mclock"
@ -254,7 +255,7 @@ loop:
} }
case task := <-d.doneCh: case task := <-d.doneCh:
id := task.dest.ID() id := task.dest().ID()
delete(d.dialing, id) delete(d.dialing, id)
d.updateStaticPool(id) d.updateStaticPool(id)
d.doneSinceLastLog++ d.doneSinceLastLog++
@ -431,7 +432,7 @@ func (d *dialScheduler) startStaticDials(n int) (started int) {
// updateStaticPool attempts to move the given static dial back into staticPool. // updateStaticPool attempts to move the given static dial back into staticPool.
func (d *dialScheduler) updateStaticPool(id enode.ID) { func (d *dialScheduler) updateStaticPool(id enode.ID) {
task, ok := d.static[id] task, ok := d.static[id]
if ok && task.staticPoolIndex < 0 && d.checkDial(task.dest) == nil { if ok && task.staticPoolIndex < 0 && d.checkDial(task.dest()) == nil {
d.addToStaticPool(task) d.addToStaticPool(task)
} }
} }
@ -459,11 +460,11 @@ func (d *dialScheduler) removeFromStaticPool(idx int) {
// startDial runs the given dial task in a separate goroutine. // startDial runs the given dial task in a separate goroutine.
func (d *dialScheduler) startDial(task *dialTask) { func (d *dialScheduler) startDial(task *dialTask) {
d.log.Trace("Starting p2p dial", "id", task.dest.ID(), "ip", task.dest.IP(), "flag", task.flags) node := task.dest()
hkey := string(task.dest.ID().Bytes()) d.log.Trace("Starting p2p dial", "id", node.ID(), "ip", node.IP(), "flag", task.flags)
hkey := string(node.ID().Bytes())
d.history.add(hkey, d.clock.Now().Add(dialHistoryExpiration)) d.history.add(hkey, d.clock.Now().Add(dialHistoryExpiration))
d.dialing[task.dest.ID()] = task d.dialing[node.ID()] = task
go func() { go func() {
task.run(d) task.run(d)
d.doneCh <- task d.doneCh <- task
@ -474,39 +475,46 @@ func (d *dialScheduler) startDial(task *dialTask) {
type dialTask struct { type dialTask struct {
staticPoolIndex int staticPoolIndex int
flags connFlag flags connFlag
// These fields are private to the task and should not be // These fields are private to the task and should not be
// accessed by dialScheduler while the task is running. // accessed by dialScheduler while the task is running.
dest *enode.Node destPtr atomic.Pointer[enode.Node]
lastResolved mclock.AbsTime lastResolved mclock.AbsTime
resolveDelay time.Duration resolveDelay time.Duration
} }
func newDialTask(dest *enode.Node, flags connFlag) *dialTask { func newDialTask(dest *enode.Node, flags connFlag) *dialTask {
return &dialTask{dest: dest, flags: flags, staticPoolIndex: -1} t := &dialTask{flags: flags, staticPoolIndex: -1}
t.destPtr.Store(dest)
return t
} }
type dialError struct { type dialError struct {
error error
} }
func (t *dialTask) dest() *enode.Node {
return t.destPtr.Load()
}
func (t *dialTask) run(d *dialScheduler) { func (t *dialTask) run(d *dialScheduler) {
if t.needResolve() && !t.resolve(d) { if t.needResolve() && !t.resolve(d) {
return return
} }
err := t.dial(d, t.dest) err := t.dial(d, t.dest())
if err != nil { if err != nil {
// For static nodes, resolve one more time if dialing fails. // For static nodes, resolve one more time if dialing fails.
if _, ok := err.(*dialError); ok && t.flags&staticDialedConn != 0 { if _, ok := err.(*dialError); ok && t.flags&staticDialedConn != 0 {
if t.resolve(d) { if t.resolve(d) {
t.dial(d, t.dest) t.dial(d, t.dest())
} }
} }
} }
} }
func (t *dialTask) needResolve() bool { func (t *dialTask) needResolve() bool {
return t.flags&staticDialedConn != 0 && t.dest.IP() == nil return t.flags&staticDialedConn != 0 && t.dest().IP() == nil
} }
// resolve attempts to find the current endpoint for the destination // resolve attempts to find the current endpoint for the destination
@ -528,7 +536,8 @@ func (t *dialTask) resolve(d *dialScheduler) bool {
return false return false
} }
resolved := d.resolver.Resolve(t.dest) node := t.dest()
resolved := d.resolver.Resolve(node)
t.lastResolved = d.clock.Now() t.lastResolved = d.clock.Now()
if resolved == nil { if resolved == nil {
@ -536,25 +545,22 @@ func (t *dialTask) resolve(d *dialScheduler) bool {
if t.resolveDelay > maxResolveDelay { if t.resolveDelay > maxResolveDelay {
t.resolveDelay = maxResolveDelay t.resolveDelay = maxResolveDelay
} }
d.log.Debug("Resolving node failed", "id", node.ID(), "newdelay", t.resolveDelay)
d.log.Debug("Resolving node failed", "id", t.dest.ID(), "newdelay", t.resolveDelay)
return false return false
} }
// The node was found. // The node was found.
t.resolveDelay = initialResolveDelay t.resolveDelay = initialResolveDelay
t.dest = resolved t.destPtr.Store(resolved)
d.log.Debug("Resolved node", "id", t.dest.ID(), "addr", &net.TCPAddr{IP: t.dest.IP(), Port: t.dest.TCP()}) d.log.Debug("Resolved node", "id", resolved.ID(), "addr", &net.TCPAddr{IP: resolved.IP(), Port: resolved.TCP()})
return true return true
} }
// dial performs the actual connection attempt. // dial performs the actual connection attempt.
func (t *dialTask) dial(d *dialScheduler, dest *enode.Node) error { func (t *dialTask) dial(d *dialScheduler, dest *enode.Node) error {
dialMeter.Mark(1) dialMeter.Mark(1)
fd, err := d.dialer.Dial(d.ctx, t.dest) fd, err := d.dialer.Dial(d.ctx, dest)
if err != nil { if err != nil {
d.log.Trace("Dial error", "id", t.dest.ID(), "addr", nodeAddr(t.dest), "conn", t.flags, "err", cleanupDialErr(err)) d.log.Trace("Dial error", "id", dest.ID(), "addr", nodeAddr(dest), "conn", t.flags, "err", cleanupDialErr(err))
dialConnectionError.Mark(1) dialConnectionError.Mark(1)
return &dialError{err} return &dialError{err}
} }
@ -562,8 +568,9 @@ func (t *dialTask) dial(d *dialScheduler, dest *enode.Node) error {
} }
func (t *dialTask) String() string { func (t *dialTask) String() string {
id := t.dest.ID() node := t.dest()
return fmt.Sprintf("%v %x %v:%d", t.flags, id[:8], t.dest.IP(), t.dest.TCP()) id := node.ID()
return fmt.Sprintf("%v %x %v:%d", t.flags, id[:8], node.IP(), node.TCP())
} }
func cleanupDialErr(err error) error { func cleanupDialErr(err error) error {

View file

@ -18,7 +18,12 @@ package discover
import ( import (
"crypto/ecdsa" "crypto/ecdsa"
crand "crypto/rand"
"encoding/binary"
"math/rand"
"net" "net"
"net/netip"
"sync"
"time" "time"
"github.com/ethereum/go-ethereum/common/mclock" "github.com/ethereum/go-ethereum/common/mclock"
@ -30,8 +35,8 @@ import (
// UDPConn is a network connection on which discovery can operate. // UDPConn is a network connection on which discovery can operate.
type UDPConn interface { type UDPConn interface {
ReadFromUDP(b []byte) (n int, addr *net.UDPAddr, err error) ReadFromUDPAddrPort(b []byte) (n int, addr netip.AddrPort, err error)
WriteToUDP(b []byte, addr *net.UDPAddr) (n int, err error) WriteToUDPAddrPort(b []byte, addr netip.AddrPort) (n int, err error)
Close() error Close() error
LocalAddr() net.Addr LocalAddr() net.Addr
} }
@ -62,7 +67,7 @@ type Config struct {
func (cfg Config) withDefaults() Config { func (cfg Config) withDefaults() Config {
// Node table configuration: // Node table configuration:
if cfg.PingInterval == 0 { if cfg.PingInterval == 0 {
cfg.PingInterval = 10 * time.Second cfg.PingInterval = 3 * time.Second
} }
if cfg.RefreshInterval == 0 { if cfg.RefreshInterval == 0 {
cfg.RefreshInterval = 30 * time.Minute cfg.RefreshInterval = 30 * time.Minute
@ -93,13 +98,46 @@ func ListenUDP(c UDPConn, ln *enode.LocalNode, cfg Config) (*UDPv4, error) {
// channel if configured. // channel if configured.
type ReadPacket struct { type ReadPacket struct {
Data []byte Data []byte
Addr *net.UDPAddr Addr netip.AddrPort
} }
func min(x, y int) int { type randomSource interface {
if x > y { Intn(int) int
return y Int63n(int64) int64
} Shuffle(int, func(int, int))
}
return x
// reseedingRandom is a random number generator that tracks when it was last re-seeded.
type reseedingRandom struct {
mu sync.Mutex
cur *rand.Rand
}
func (r *reseedingRandom) seed() {
var b [8]byte
crand.Read(b[:])
seed := binary.BigEndian.Uint64(b[:])
new := rand.New(rand.NewSource(int64(seed)))
r.mu.Lock()
r.cur = new
r.mu.Unlock()
}
func (r *reseedingRandom) Intn(n int) int {
r.mu.Lock()
defer r.mu.Unlock()
return r.cur.Intn(n)
}
func (r *reseedingRandom) Int63n(n int64) int64 {
r.mu.Lock()
defer r.mu.Unlock()
return r.cur.Int63n(n)
}
func (r *reseedingRandom) Shuffle(n int, swap func(i, j int)) {
r.mu.Lock()
defer r.mu.Unlock()
r.cur.Shuffle(n, swap)
} }

View file

@ -29,16 +29,16 @@ import (
// not need to be an actual node identifier. // not need to be an actual node identifier.
type lookup struct { type lookup struct {
tab *Table tab *Table
queryfunc func(*node) ([]*node, error) queryfunc queryFunc
replyCh chan []*node replyCh chan []*enode.Node
cancelCh <-chan struct{} cancelCh <-chan struct{}
asked, seen map[enode.ID]bool asked, seen map[enode.ID]bool
result nodesByDistance result nodesByDistance
replyBuffer []*node replyBuffer []*enode.Node
queries int queries int
} }
type queryFunc func(*node) ([]*node, error) type queryFunc func(*enode.Node) ([]*enode.Node, error)
func newLookup(ctx context.Context, tab *Table, target enode.ID, q queryFunc) *lookup { func newLookup(ctx context.Context, tab *Table, target enode.ID, q queryFunc) *lookup {
it := &lookup{ it := &lookup{
@ -47,7 +47,7 @@ func newLookup(ctx context.Context, tab *Table, target enode.ID, q queryFunc) *l
asked: make(map[enode.ID]bool), asked: make(map[enode.ID]bool),
seen: make(map[enode.ID]bool), seen: make(map[enode.ID]bool),
result: nodesByDistance{target: target}, result: nodesByDistance{target: target},
replyCh: make(chan []*node, alpha), replyCh: make(chan []*enode.Node, alpha),
cancelCh: ctx.Done(), cancelCh: ctx.Done(),
queries: -1, queries: -1,
} }
@ -62,7 +62,7 @@ func newLookup(ctx context.Context, tab *Table, target enode.ID, q queryFunc) *l
func (it *lookup) run() []*enode.Node { func (it *lookup) run() []*enode.Node {
for it.advance() { for it.advance() {
} }
return unwrapNodes(it.result.entries) return it.result.entries
} }
// advance advances the lookup until any new nodes have been found. // advance advances the lookup until any new nodes have been found.
@ -147,36 +147,14 @@ func (it *lookup) slowdown() {
} }
} }
func (it *lookup) query(n *node, reply chan<- []*node) { func (it *lookup) query(n *enode.Node, reply chan<- []*enode.Node) {
fails := it.tab.db.FindFails(n.ID(), n.IP())
r, err := it.queryfunc(n) r, err := it.queryfunc(n)
if !errors.Is(err, errClosed) { // avoid recording failures on shutdown.
if errors.Is(err, errClosed) { success := len(r) > 0
// Avoid recording failures on shutdown. it.tab.trackRequest(n, success, r)
reply <- nil if err != nil {
return it.tab.log.Trace("FINDNODE failed", "id", n.ID(), "err", err)
} else if len(r) == 0 {
fails++
it.tab.db.UpdateFindFails(n.ID(), n.IP(), fails)
// Remove the node from the local table if it fails to return anything useful too
// many times, but only if there are enough other nodes in the bucket.
dropped := false
if fails >= maxFindnodeFailures && it.tab.bucketLen(n.ID()) >= bucketSize/2 {
dropped = true
it.tab.delete(n)
} }
it.tab.log.Trace("FINDNODE failed", "id", n.ID(), "failcount", fails, "dropped", dropped, "err", err)
} else if fails > 0 {
// Reset failure counter because it counts _consecutive_ failures.
it.tab.db.UpdateFindFails(n.ID(), n.IP(), 0)
}
// Grab as many nodes as possible. Some of them might not be alive anymore, but we'll
// just remove those again during revalidation.
for _, n := range r {
it.tab.addSeenNode(n)
} }
reply <- r reply <- r
} }
@ -184,7 +162,7 @@ func (it *lookup) query(n *node, reply chan<- []*node) {
// lookupIterator performs lookup operations and iterates over all seen nodes. // lookupIterator performs lookup operations and iterates over all seen nodes.
// When a lookup finishes, a new one is created through nextLookup. // When a lookup finishes, a new one is created through nextLookup.
type lookupIterator struct { type lookupIterator struct {
buffer []*node buffer []*enode.Node
nextLookup lookupFunc nextLookup lookupFunc
ctx context.Context ctx context.Context
cancel func() cancel func()
@ -203,8 +181,7 @@ func (it *lookupIterator) Node() *enode.Node {
if len(it.buffer) == 0 { if len(it.buffer) == 0 {
return nil return nil
} }
return it.buffer[0]
return unwrapNode(it.buffer[0])
} }
// Next moves to the next node. // Next moves to the next node.

View file

@ -18,7 +18,7 @@ package discover
import ( import (
"fmt" "fmt"
"net" "net/netip"
"github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/metrics"
) )
@ -58,16 +58,16 @@ func newMeteredConn(conn UDPConn) UDPConn {
return &meteredUdpConn{UDPConn: conn} return &meteredUdpConn{UDPConn: conn}
} }
// Read delegates a network read to the underlying connection, bumping the udp ingress traffic meter along the way. // ReadFromUDPAddrPort delegates a network read to the underlying connection, bumping the udp ingress traffic meter along the way.
func (c *meteredUdpConn) ReadFromUDP(b []byte) (n int, addr *net.UDPAddr, err error) { func (c *meteredUdpConn) ReadFromUDPAddrPort(b []byte) (n int, addr netip.AddrPort, err error) {
n, addr, err = c.UDPConn.ReadFromUDP(b) n, addr, err = c.UDPConn.ReadFromUDPAddrPort(b)
ingressTrafficMeter.Mark(int64(n)) ingressTrafficMeter.Mark(int64(n))
return n, addr, err return n, addr, err
} }
// Write delegates a network write to the underlying connection, bumping the udp egress traffic meter along the way. // WriteToUDP delegates a network write to the underlying connection, bumping the udp egress traffic meter along the way.
func (c *meteredUdpConn) WriteToUDP(b []byte, addr *net.UDPAddr) (n int, err error) { func (c *meteredUdpConn) WriteToUDP(b []byte, addr netip.AddrPort) (n int, err error) {
n, err = c.UDPConn.WriteToUDP(b, addr) n, err = c.UDPConn.WriteToUDPAddrPort(b, addr)
egressTrafficMeter.Mark(int64(n)) egressTrafficMeter.Mark(int64(n))
return n, err return n, err
} }

View file

@ -21,7 +21,8 @@ import (
"crypto/elliptic" "crypto/elliptic"
"errors" "errors"
"math/big" "math/big"
"net" "slices"
"sort"
"time" "time"
"github.com/ethereum/go-ethereum/common/math" "github.com/ethereum/go-ethereum/common/math"
@ -29,12 +30,22 @@ import (
"github.com/ethereum/go-ethereum/p2p/enode" "github.com/ethereum/go-ethereum/p2p/enode"
) )
// node represents a host on the network. type BucketNode struct {
// The fields of Node may not be modified. Node *enode.Node `json:"node"`
type node struct { AddedToTable time.Time `json:"addedToTable"`
enode.Node AddedToBucket time.Time `json:"addedToBucket"`
addedAt time.Time // time when the node was added to the table Checks int `json:"checks"`
Live bool `json:"live"`
}
// tableNode is an entry in Table.
type tableNode struct {
*enode.Node
revalList *revalidationList
addedToTable time.Time // first time node was added to bucket or replacement list
addedToBucket time.Time // time it was added in the actual bucket
livenessChecks uint // how often liveness was checked livenessChecks uint // how often liveness was checked
isValidatedLive bool // true if existence of node is considered validated right now
} }
type encPubkey [64]byte type encPubkey [64]byte
@ -69,36 +80,60 @@ func (e encPubkey) id() enode.ID {
return enode.ID(crypto.Keccak256Hash(e[:])) return enode.ID(crypto.Keccak256Hash(e[:]))
} }
func wrapNode(n *enode.Node) *node { func unwrapNodes(ns []*tableNode) []*enode.Node {
return &node{Node: *n}
}
func wrapNodes(ns []*enode.Node) []*node {
result := make([]*node, len(ns))
for i, n := range ns {
result[i] = wrapNode(n)
}
return result
}
func unwrapNode(n *node) *enode.Node {
return &n.Node
}
func unwrapNodes(ns []*node) []*enode.Node {
result := make([]*enode.Node, len(ns)) result := make([]*enode.Node, len(ns))
for i, n := range ns { for i, n := range ns {
result[i] = unwrapNode(n) result[i] = n.Node
} }
return result return result
} }
func (n *node) addr() *net.UDPAddr { func (n *tableNode) String() string {
return &net.UDPAddr{IP: n.IP(), Port: n.UDP()}
}
func (n *node) String() string {
return n.Node.String() return n.Node.String()
} }
// nodesByDistance is a list of nodes, ordered by distance to target.
type nodesByDistance struct {
entries []*enode.Node
target enode.ID
}
// push adds the given node to the list, keeping the total size below maxElems.
func (h *nodesByDistance) push(n *enode.Node, maxElems int) {
ix := sort.Search(len(h.entries), func(i int) bool {
return enode.DistCmp(h.target, h.entries[i].ID(), n.ID()) > 0
})
end := len(h.entries)
if len(h.entries) < maxElems {
h.entries = append(h.entries, n)
}
if ix < end {
// Slide existing entries down to make room.
// This will overwrite the entry we just appended.
copy(h.entries[ix+1:], h.entries[ix:])
h.entries[ix] = n
}
}
type nodeType interface {
ID() enode.ID
}
// containsID reports whether ns contains a node with the given ID.
func containsID[N nodeType](ns []N, id enode.ID) bool {
for _, n := range ns {
if n.ID() == id {
return true
}
}
return false
}
// deleteNode removes a node from the list.
func deleteNode[N nodeType](list []N, id enode.ID) []N {
return slices.DeleteFunc(list, func(n N) bool {
return n.ID() == id
})
}

View file

@ -24,16 +24,14 @@ package discover
import ( import (
"context" "context"
crand "crypto/rand"
"encoding/binary"
"fmt" "fmt"
mrand "math/rand"
"net" "net"
"sort" "slices"
"sync" "sync"
"time" "time"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/mclock"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/metrics"
"github.com/ethereum/go-ethereum/p2p/enode" "github.com/ethereum/go-ethereum/p2p/enode"
@ -55,7 +53,6 @@ const (
bucketIPLimit, bucketSubnet = 2, 24 // at most 2 addresses from the same /24 bucketIPLimit, bucketSubnet = 2, 24 // at most 2 addresses from the same /24
tableIPLimit, tableSubnet = 10, 24 tableIPLimit, tableSubnet = 10, 24
copyNodesInterval = 30 * time.Second
seedMinTableTime = 5 * time.Minute seedMinTableTime = 5 * time.Minute
seedCount = 30 seedCount = 30
seedMaxAge = 5 * 24 * time.Hour seedMaxAge = 5 * 24 * time.Hour
@ -67,9 +64,10 @@ const (
type Table struct { type Table struct {
mutex sync.Mutex // protects buckets, bucket content, nursery, rand mutex sync.Mutex // protects buckets, bucket content, nursery, rand
buckets [nBuckets]*bucket // index of known nodes by distance buckets [nBuckets]*bucket // index of known nodes by distance
nursery []*node // bootstrap nodes nursery []*enode.Node // bootstrap nodes
rand *mrand.Rand // source of randomness, periodically reseeded rand reseedingRandom // source of randomness, periodically reseeded
ips netutil.DistinctNetSet ips netutil.DistinctNetSet
revalidation tableRevalidation
db *enode.DB // database of known nodes db *enode.DB // database of known nodes
net transport net transport
@ -78,12 +76,16 @@ type Table struct {
// loop channels // loop channels
refreshReq chan chan struct{} refreshReq chan chan struct{}
revalResponseCh chan revalidationResponse
addNodeCh chan addNodeOp
addNodeHandled chan bool
trackRequestCh chan trackRequestOp
initDone chan struct{} initDone chan struct{}
closeReq chan struct{} closeReq chan struct{}
closed chan struct{} closed chan struct{}
nodeAddedHook func(*bucket, *node) nodeAddedHook func(*bucket, *tableNode)
nodeRemovedHook func(*bucket, *node) nodeRemovedHook func(*bucket, *tableNode)
} }
// transport is implemented by the UDP transports. // transport is implemented by the UDP transports.
@ -98,12 +100,24 @@ type transport interface {
// bucket contains nodes, ordered by their last activity. the entry // bucket contains nodes, ordered by their last activity. the entry
// that was most recently active is the first element in entries. // that was most recently active is the first element in entries.
type bucket struct { type bucket struct {
entries []*node // live entries, sorted by time of last contact entries []*tableNode // live entries, sorted by time of last contact
replacements []*node // recently seen nodes to be used if revalidation fails replacements []*tableNode // recently seen nodes to be used if revalidation fails
ips netutil.DistinctNetSet ips netutil.DistinctNetSet
index int index int
} }
type addNodeOp struct {
node *enode.Node
isInbound bool
forceSetLive bool // for tests
}
type trackRequestOp struct {
node *enode.Node
foundNodes []*enode.Node
success bool
}
func newTable(t transport, db *enode.DB, cfg Config) (*Table, error) { func newTable(t transport, db *enode.DB, cfg Config) (*Table, error) {
cfg = cfg.withDefaults() cfg = cfg.withDefaults()
tab := &Table{ tab := &Table{
@ -112,15 +126,15 @@ func newTable(t transport, db *enode.DB, cfg Config) (*Table, error) {
cfg: cfg, cfg: cfg,
log: cfg.Log, log: cfg.Log,
refreshReq: make(chan chan struct{}), refreshReq: make(chan chan struct{}),
revalResponseCh: make(chan revalidationResponse),
addNodeCh: make(chan addNodeOp),
addNodeHandled: make(chan bool),
trackRequestCh: make(chan trackRequestOp),
initDone: make(chan struct{}), initDone: make(chan struct{}),
closeReq: make(chan struct{}), closeReq: make(chan struct{}),
closed: make(chan struct{}), closed: make(chan struct{}),
rand: mrand.New(mrand.NewSource(0)),
ips: netutil.DistinctNetSet{Subnet: tableSubnet, Limit: tableIPLimit}, ips: netutil.DistinctNetSet{Subnet: tableSubnet, Limit: tableIPLimit},
} }
if err := tab.setFallbackNodes(cfg.Bootnodes); err != nil {
return nil, err
}
for i := range tab.buckets { for i := range tab.buckets {
tab.buckets[i] = &bucket{ tab.buckets[i] = &bucket{
@ -128,42 +142,34 @@ func newTable(t transport, db *enode.DB, cfg Config) (*Table, error) {
ips: netutil.DistinctNetSet{Subnet: bucketSubnet, Limit: bucketIPLimit}, ips: netutil.DistinctNetSet{Subnet: bucketSubnet, Limit: bucketIPLimit},
} }
} }
tab.rand.seed()
tab.revalidation.init(&cfg)
tab.seedRand() // initial table content
if err := tab.setFallbackNodes(cfg.Bootnodes); err != nil {
return nil, err
}
tab.loadSeedNodes() tab.loadSeedNodes()
return tab, nil return tab, nil
} }
func newMeteredTable(t transport, db *enode.DB, cfg Config) (*Table, error) {
tab, err := newTable(t, db, cfg)
if err != nil {
return nil, err
}
if metrics.Enabled {
tab.nodeAddedHook = func(b *bucket, n *node) {
bucketsCounter[b.index].Inc(1)
}
tab.nodeRemovedHook = func(b *bucket, n *node) {
bucketsCounter[b.index].Dec(1)
}
}
return tab, nil
}
// Nodes returns all nodes contained in the table. // Nodes returns all nodes contained in the table.
func (tab *Table) Nodes() []*enode.Node { func (tab *Table) Nodes() [][]BucketNode {
if !tab.isInitDone() {
return nil
}
tab.mutex.Lock() tab.mutex.Lock()
defer tab.mutex.Unlock() defer tab.mutex.Unlock()
var nodes []*enode.Node nodes := make([][]BucketNode, len(tab.buckets))
for _, b := range &tab.buckets { for i, b := range &tab.buckets {
for _, n := range b.entries { nodes[i] = make([]BucketNode, len(b.entries))
nodes = append(nodes, unwrapNode(n)) for j, n := range b.entries {
nodes[i][j] = BucketNode{
Node: n.Node,
Checks: int(n.livenessChecks),
Live: n.isValidatedLive,
AddedToTable: n.addedToTable,
AddedToBucket: n.addedToBucket,
}
} }
} }
return nodes return nodes
@ -173,16 +179,6 @@ func (tab *Table) self() *enode.Node {
return tab.net.Self() return tab.net.Self()
} }
func (tab *Table) seedRand() {
var b [8]byte
crand.Read(b[:])
tab.mutex.Lock()
tab.rand.Seed(int64(binary.BigEndian.Uint64(b[:])))
tab.mutex.Unlock()
}
// getNode returns the node with the given ID or nil if it isn't in the table. // getNode returns the node with the given ID or nil if it isn't in the table.
func (tab *Table) getNode(id enode.ID) *enode.Node { func (tab *Table) getNode(id enode.ID) *enode.Node {
tab.mutex.Lock() tab.mutex.Lock()
@ -191,7 +187,7 @@ func (tab *Table) getNode(id enode.ID) *enode.Node {
b := tab.bucket(id) b := tab.bucket(id)
for _, e := range b.entries { for _, e := range b.entries {
if e.ID() == id { if e.ID() == id {
return unwrapNode(e) return e.Node
} }
} }
@ -208,7 +204,7 @@ func (tab *Table) close() {
// are used to connect to the network if the table is empty and there // are used to connect to the network if the table is empty and there
// are no known nodes in the database. // are no known nodes in the database.
func (tab *Table) setFallbackNodes(nodes []*enode.Node) error { func (tab *Table) setFallbackNodes(nodes []*enode.Node) error {
nursery := make([]*node, 0, len(nodes)) nursery := make([]*enode.Node, 0, len(nodes))
for _, n := range nodes { for _, n := range nodes {
if err := n.ValidateComplete(); err != nil { if err := n.ValidateComplete(); err != nil {
return fmt.Errorf("bad bootstrap node %q: %v", n, err) return fmt.Errorf("bad bootstrap node %q: %v", n, err)
@ -217,7 +213,7 @@ func (tab *Table) setFallbackNodes(nodes []*enode.Node) error {
tab.log.Error("Bootstrap node filtered by netrestrict", "id", n.ID(), "ip", n.IP()) tab.log.Error("Bootstrap node filtered by netrestrict", "id", n.ID(), "ip", n.IP())
continue continue
} }
nursery = append(nursery, wrapNode(n)) nursery = append(nursery, n)
} }
tab.nursery = nursery tab.nursery = nursery
return nil return nil
@ -244,53 +240,174 @@ func (tab *Table) refresh() <-chan struct{} {
return done return done
} }
// loop schedules runs of doRefresh, doRevalidate and copyLiveNodes. // findnodeByID returns the n nodes in the table that are closest to the given id.
// This is used by the FINDNODE/v4 handler.
//
// The preferLive parameter says whether the caller wants liveness-checked results. If
// preferLive is true and the table contains any verified nodes, the result will not
// contain unverified nodes. However, if there are no verified nodes at all, the result
// will contain unverified nodes.
func (tab *Table) findnodeByID(target enode.ID, nresults int, preferLive bool) *nodesByDistance {
tab.mutex.Lock()
defer tab.mutex.Unlock()
// Scan all buckets. There might be a better way to do this, but there aren't that many
// buckets, so this solution should be fine. The worst-case complexity of this loop
// is O(tab.len() * nresults).
nodes := &nodesByDistance{target: target}
liveNodes := &nodesByDistance{target: target}
for _, b := range &tab.buckets {
for _, n := range b.entries {
nodes.push(n.Node, nresults)
if preferLive && n.isValidatedLive {
liveNodes.push(n.Node, nresults)
}
}
}
if preferLive && len(liveNodes.entries) > 0 {
return liveNodes
}
return nodes
}
// appendLiveNodes adds nodes at the given distance to the result slice.
// This is used by the FINDNODE/v5 handler.
func (tab *Table) appendLiveNodes(dist uint, result []*enode.Node) []*enode.Node {
if dist > 256 {
return result
}
if dist == 0 {
return append(result, tab.self())
}
tab.mutex.Lock()
for _, n := range tab.bucketAtDistance(int(dist)).entries {
if n.isValidatedLive {
result = append(result, n.Node)
}
}
tab.mutex.Unlock()
// Shuffle result to avoid always returning same nodes in FINDNODE/v5.
tab.rand.Shuffle(len(result), func(i, j int) {
result[i], result[j] = result[j], result[i]
})
return result
}
// len returns the number of nodes in the table.
func (tab *Table) len() (n int) {
tab.mutex.Lock()
defer tab.mutex.Unlock()
for _, b := range &tab.buckets {
n += len(b.entries)
}
return n
}
// addFoundNode adds a node which may not be live. If the bucket has space available,
// adding the node succeeds immediately. Otherwise, the node is added to the replacements
// list.
//
// The caller must not hold tab.mutex.
func (tab *Table) addFoundNode(n *enode.Node, forceSetLive bool) bool {
op := addNodeOp{node: n, isInbound: false, forceSetLive: forceSetLive}
select {
case tab.addNodeCh <- op:
return <-tab.addNodeHandled
case <-tab.closeReq:
return false
}
}
// addInboundNode adds a node from an inbound contact. If the bucket has no space, the
// node is added to the replacements list.
//
// There is an additional safety measure: if the table is still initializing the node is
// not added. This prevents an attack where the table could be filled by just sending ping
// repeatedly.
//
// The caller must not hold tab.mutex.
func (tab *Table) addInboundNode(n *enode.Node) bool {
op := addNodeOp{node: n, isInbound: true}
select {
case tab.addNodeCh <- op:
return <-tab.addNodeHandled
case <-tab.closeReq:
return false
}
}
func (tab *Table) trackRequest(n *enode.Node, success bool, foundNodes []*enode.Node) {
op := trackRequestOp{n, foundNodes, success}
select {
case tab.trackRequestCh <- op:
case <-tab.closeReq:
}
}
// loop is the main loop of Table.
func (tab *Table) loop() { func (tab *Table) loop() {
var ( var (
revalidate = time.NewTimer(tab.nextRevalidateTime())
refresh = time.NewTimer(tab.nextRefreshTime()) refresh = time.NewTimer(tab.nextRefreshTime())
copyNodes = time.NewTicker(copyNodesInterval)
refreshDone = make(chan struct{}) // where doRefresh reports completion refreshDone = make(chan struct{}) // where doRefresh reports completion
revalidateDone chan struct{} // where doRevalidate reports completion
waiting = []chan struct{}{tab.initDone} // holds waiting callers while doRefresh runs waiting = []chan struct{}{tab.initDone} // holds waiting callers while doRefresh runs
revalTimer = mclock.NewAlarm(tab.cfg.Clock)
reseedRandTimer = time.NewTicker(10 * time.Minute)
) )
defer refresh.Stop() defer refresh.Stop()
defer revalidate.Stop() defer revalTimer.Stop()
defer copyNodes.Stop() defer reseedRandTimer.Stop()
// Start initial refresh. // Start initial refresh.
go tab.doRefresh(refreshDone) go tab.doRefresh(refreshDone)
loop: loop:
for { for {
nextTime := tab.revalidation.run(tab, tab.cfg.Clock.Now())
revalTimer.Schedule(nextTime)
select { select {
case <-reseedRandTimer.C:
tab.rand.seed()
case <-revalTimer.C():
case r := <-tab.revalResponseCh:
tab.revalidation.handleResponse(tab, r)
case op := <-tab.addNodeCh:
tab.mutex.Lock()
ok := tab.handleAddNode(op)
tab.mutex.Unlock()
tab.addNodeHandled <- ok
case op := <-tab.trackRequestCh:
tab.handleTrackRequest(op)
case <-refresh.C: case <-refresh.C:
tab.seedRand()
if refreshDone == nil { if refreshDone == nil {
refreshDone = make(chan struct{}) refreshDone = make(chan struct{})
go tab.doRefresh(refreshDone) go tab.doRefresh(refreshDone)
} }
case req := <-tab.refreshReq: case req := <-tab.refreshReq:
waiting = append(waiting, req) waiting = append(waiting, req)
if refreshDone == nil { if refreshDone == nil {
refreshDone = make(chan struct{}) refreshDone = make(chan struct{})
go tab.doRefresh(refreshDone) go tab.doRefresh(refreshDone)
} }
case <-refreshDone: case <-refreshDone:
for _, ch := range waiting { for _, ch := range waiting {
close(ch) close(ch)
} }
waiting, refreshDone = nil, nil waiting, refreshDone = nil, nil
refresh.Reset(tab.nextRefreshTime()) refresh.Reset(tab.nextRefreshTime())
case <-revalidate.C:
revalidateDone = make(chan struct{})
go tab.doRevalidate(revalidateDone)
case <-revalidateDone:
revalidate.Reset(tab.nextRevalidateTime())
revalidateDone = nil
case <-copyNodes.C:
go tab.copyLiveNodes()
case <-tab.closeReq: case <-tab.closeReq:
break loop break loop
} }
@ -303,11 +420,6 @@ loop:
for _, ch := range waiting { for _, ch := range waiting {
close(ch) close(ch)
} }
if revalidateDone != nil {
<-revalidateDone
}
close(tab.closed) close(tab.closed)
} }
@ -336,186 +448,25 @@ func (tab *Table) doRefresh(done chan struct{}) {
} }
func (tab *Table) loadSeedNodes() { func (tab *Table) loadSeedNodes() {
seeds := wrapNodes(tab.db.QuerySeeds(seedCount, seedMaxAge)) seeds := tab.db.QuerySeeds(seedCount, seedMaxAge)
seeds = append(seeds, tab.nursery...) seeds = append(seeds, tab.nursery...)
for i := range seeds { for i := range seeds {
seed := seeds[i] seed := seeds[i]
if tab.log.Enabled(context.Background(), log.LevelTrace) { if tab.log.Enabled(context.Background(), log.LevelTrace) {
age := time.Since(tab.db.LastPongReceived(seed.ID(), seed.IP())) age := time.Since(tab.db.LastPongReceived(seed.ID(), seed.IP()))
tab.log.Trace("Found seed node in database", "id", seed.ID(), "addr", seed.addr(), "age", age) addr, _ := seed.UDPEndpoint()
tab.log.Trace("Found seed node in database", "id", seed.ID(), "addr", addr, "age", age)
} }
tab.addSeenNode(seed) tab.handleAddNode(addNodeOp{node: seed, isInbound: false})
} }
} }
// doRevalidate checks that the last node in a random bucket is still live and replaces or
// deletes the node if it isn't.
func (tab *Table) doRevalidate(done chan<- struct{}) {
defer func() { done <- struct{}{} }()
last, bi := tab.nodeToRevalidate()
if last == nil {
// No non-empty bucket found.
return
}
// Ping the selected node and wait for a pong.
remoteSeq, err := tab.net.ping(unwrapNode(last))
// Also fetch record if the node replied and returned a higher sequence number.
if last.Seq() < remoteSeq {
n, err := tab.net.RequestENR(unwrapNode(last))
if err != nil {
tab.log.Debug("ENR request failed", "id", last.ID(), "addr", last.addr(), "err", err)
} else {
last = &node{Node: *n, addedAt: last.addedAt, livenessChecks: last.livenessChecks}
}
}
tab.mutex.Lock()
defer tab.mutex.Unlock()
b := tab.buckets[bi]
if err == nil {
// The node responded, move it to the front.
last.livenessChecks++
tab.log.Debug("Revalidated node", "b", bi, "id", last.ID(), "checks", last.livenessChecks)
tab.bumpInBucket(b, last)
return
}
// No reply received, pick a replacement or delete the node if there aren't
// any replacements.
if r := tab.replace(b, last); r != nil {
tab.log.Debug("Replaced dead node", "b", bi, "id", last.ID(), "ip", last.IP(), "checks", last.livenessChecks, "r", r.ID(), "rip", r.IP())
} else {
tab.log.Debug("Removed dead node", "b", bi, "id", last.ID(), "ip", last.IP(), "checks", last.livenessChecks)
}
}
// nodeToRevalidate returns the last node in a random, non-empty bucket.
func (tab *Table) nodeToRevalidate() (n *node, bi int) {
tab.mutex.Lock()
defer tab.mutex.Unlock()
for _, bi = range tab.rand.Perm(len(tab.buckets)) {
b := tab.buckets[bi]
if len(b.entries) > 0 {
last := b.entries[len(b.entries)-1]
return last, bi
}
}
return nil, 0
}
func (tab *Table) nextRevalidateTime() time.Duration {
tab.mutex.Lock()
defer tab.mutex.Unlock()
return time.Duration(tab.rand.Int63n(int64(tab.cfg.PingInterval)))
}
func (tab *Table) nextRefreshTime() time.Duration { func (tab *Table) nextRefreshTime() time.Duration {
tab.mutex.Lock()
defer tab.mutex.Unlock()
half := tab.cfg.RefreshInterval / 2 half := tab.cfg.RefreshInterval / 2
return half + time.Duration(tab.rand.Int63n(int64(half))) return half + time.Duration(tab.rand.Int63n(int64(half)))
} }
// copyLiveNodes adds nodes from the table to the database if they have been in the table
// longer than seedMinTableTime.
func (tab *Table) copyLiveNodes() {
tab.mutex.Lock()
defer tab.mutex.Unlock()
now := time.Now()
for _, b := range &tab.buckets {
for _, n := range b.entries {
if n.livenessChecks > 0 && now.Sub(n.addedAt) >= seedMinTableTime {
tab.db.UpdateNode(unwrapNode(n))
}
}
}
}
// findnodeByID returns the n nodes in the table that are closest to the given id.
// This is used by the FINDNODE/v4 handler.
//
// The preferLive parameter says whether the caller wants liveness-checked results. If
// preferLive is true and the table contains any verified nodes, the result will not
// contain unverified nodes. However, if there are no verified nodes at all, the result
// will contain unverified nodes.
func (tab *Table) findnodeByID(target enode.ID, nresults int, preferLive bool) *nodesByDistance {
tab.mutex.Lock()
defer tab.mutex.Unlock()
// Scan all buckets. There might be a better way to do this, but there aren't that many
// buckets, so this solution should be fine. The worst-case complexity of this loop
// is O(tab.len() * nresults).
nodes := &nodesByDistance{target: target}
liveNodes := &nodesByDistance{target: target}
for _, b := range &tab.buckets {
for _, n := range b.entries {
nodes.push(n, nresults)
if preferLive && n.livenessChecks > 0 {
liveNodes.push(n, nresults)
}
}
}
if preferLive && len(liveNodes.entries) > 0 {
return liveNodes
}
return nodes
}
// appendLiveNodes adds nodes at the given distance to the result slice.
func (tab *Table) appendLiveNodes(dist uint, result []*enode.Node) []*enode.Node {
if dist > 256 {
return result
}
if dist == 0 {
return append(result, tab.self())
}
tab.mutex.Lock()
defer tab.mutex.Unlock()
for _, n := range tab.bucketAtDistance(int(dist)).entries {
if n.livenessChecks >= 1 {
node := n.Node // avoid handing out pointer to struct field
result = append(result, &node)
}
}
return result
}
// len returns the number of nodes in the table.
func (tab *Table) len() (n int) {
tab.mutex.Lock()
defer tab.mutex.Unlock()
for _, b := range &tab.buckets {
n += len(b.entries)
}
return n
}
// bucketLen returns the number of nodes in the bucket for the given ID.
func (tab *Table) bucketLen(id enode.ID) int {
tab.mutex.Lock()
defer tab.mutex.Unlock()
return len(tab.bucket(id).entries)
}
// bucket returns the bucket for the given node ID hash. // bucket returns the bucket for the given node ID hash.
func (tab *Table) bucket(id enode.ID) *bucket { func (tab *Table) bucket(id enode.ID) *bucket {
d := enode.LogDist(tab.self().ID(), id) d := enode.LogDist(tab.self().ID(), id)
@ -530,104 +481,6 @@ func (tab *Table) bucketAtDistance(d int) *bucket {
return tab.buckets[d-bucketMinDistance-1] return tab.buckets[d-bucketMinDistance-1]
} }
// addSeenNode adds a node which may or may not be live to the end of a bucket. If the
// bucket has space available, adding the node succeeds immediately. Otherwise, the node is
// added to the replacements list.
//
// The caller must not hold tab.mutex.
func (tab *Table) addSeenNode(n *node) {
if n.ID() == tab.self().ID() {
return
}
tab.mutex.Lock()
defer tab.mutex.Unlock()
b := tab.bucket(n.ID())
if contains(b.entries, n.ID()) {
// Already in bucket, don't add.
return
}
if len(b.entries) >= bucketSize {
// Bucket full, maybe add as replacement.
tab.addReplacement(b, n)
return
}
if !tab.addIP(b, n.IP()) {
// Can't add: IP limit reached.
return
}
// Add to end of bucket:
b.entries = append(b.entries, n)
b.replacements = deleteNode(b.replacements, n)
n.addedAt = time.Now()
if tab.nodeAddedHook != nil {
tab.nodeAddedHook(b, n)
}
}
// addVerifiedNode adds a node whose existence has been verified recently to the front of a
// bucket. If the node is already in the bucket, it is moved to the front. If the bucket
// has no space, the node is added to the replacements list.
//
// There is an additional safety measure: if the table is still initializing the node
// is not added. This prevents an attack where the table could be filled by just sending
// ping repeatedly.
//
// The caller must not hold tab.mutex.
func (tab *Table) addVerifiedNode(n *node) {
if !tab.isInitDone() {
return
}
if n.ID() == tab.self().ID() {
return
}
tab.mutex.Lock()
defer tab.mutex.Unlock()
b := tab.bucket(n.ID())
if tab.bumpInBucket(b, n) {
// Already in bucket, moved to front.
return
}
if len(b.entries) >= bucketSize {
// Bucket full, maybe add as replacement.
tab.addReplacement(b, n)
return
}
if !tab.addIP(b, n.IP()) {
// Can't add: IP limit reached.
return
}
// Add to front of bucket.
b.entries, _ = pushNode(b.entries, n, bucketSize)
b.replacements = deleteNode(b.replacements, n)
n.addedAt = time.Now()
if tab.nodeAddedHook != nil {
tab.nodeAddedHook(b, n)
}
}
// delete removes an entry from the node table. It is used to evacuate dead nodes.
func (tab *Table) delete(node *node) {
tab.mutex.Lock()
defer tab.mutex.Unlock()
tab.deleteInBucket(tab.bucket(node.ID()), node)
}
func (tab *Table) addIP(b *bucket, ip net.IP) bool { func (tab *Table) addIP(b *bucket, ip net.IP) bool {
if len(ip) == 0 { if len(ip) == 0 {
return false // Nodes without IP cannot be added. return false // Nodes without IP cannot be added.
@ -661,98 +514,190 @@ func (tab *Table) removeIP(b *bucket, ip net.IP) {
b.ips.Remove(ip) b.ips.Remove(ip)
} }
func (tab *Table) addReplacement(b *bucket, n *node) { // handleAddNode adds the node in the request to the table, if there is space.
for _, e := range b.replacements { // The caller must hold tab.mutex.
if e.ID() == n.ID() { func (tab *Table) handleAddNode(req addNodeOp) bool {
return // already in list if req.node.ID() == tab.self().ID() {
return false
} }
// For nodes from inbound contact, there is an additional safety measure: if the table
// is still initializing the node is not added.
if req.isInbound && !tab.isInitDone() {
return false
}
b := tab.bucket(req.node.ID())
n, _ := tab.bumpInBucket(b, req.node, req.isInbound)
if n != nil {
// Already in bucket.
return false
}
if len(b.entries) >= bucketSize {
// Bucket full, maybe add as replacement.
tab.addReplacement(b, req.node)
return false
}
if !tab.addIP(b, req.node.IP()) {
// Can't add: IP limit reached.
return false
}
// Add to bucket.
wn := &tableNode{Node: req.node}
if req.forceSetLive {
wn.livenessChecks = 1
wn.isValidatedLive = true
}
b.entries = append(b.entries, wn)
b.replacements = deleteNode(b.replacements, wn.ID())
tab.nodeAdded(b, wn)
return true
}
// addReplacement adds n to the replacement cache of bucket b.
func (tab *Table) addReplacement(b *bucket, n *enode.Node) {
if containsID(b.replacements, n.ID()) {
// TODO: update ENR
return
} }
if !tab.addIP(b, n.IP()) { if !tab.addIP(b, n.IP()) {
return return
} }
var removed *node wn := &tableNode{Node: n, addedToTable: time.Now()}
var removed *tableNode
b.replacements, removed = pushNode(b.replacements, n, maxReplacements) b.replacements, removed = pushNode(b.replacements, wn, maxReplacements)
if removed != nil { if removed != nil {
tab.removeIP(b, removed.IP()) tab.removeIP(b, removed.IP())
} }
} }
// replace removes n from the replacement list and replaces 'last' with it if it is the func (tab *Table) nodeAdded(b *bucket, n *tableNode) {
// last entry in the bucket. If 'last' isn't the last entry, it has either been replaced if n.addedToTable == (time.Time{}) {
// with someone else or became active. n.addedToTable = time.Now()
func (tab *Table) replace(b *bucket, last *node) *node {
if len(b.entries) == 0 || b.entries[len(b.entries)-1].ID() != last.ID() {
// Entry has moved, don't replace it.
return nil
} }
// Still the last entry. n.addedToBucket = time.Now()
if len(b.replacements) == 0 { tab.revalidation.nodeAdded(tab, n)
tab.deleteInBucket(b, last) if tab.nodeAddedHook != nil {
return nil tab.nodeAddedHook(b, n)
}
if metrics.Enabled {
bucketsCounter[b.index].Inc(1)
} }
r := b.replacements[tab.rand.Intn(len(b.replacements))]
b.replacements = deleteNode(b.replacements, r)
b.entries[len(b.entries)-1] = r
tab.removeIP(b, last.IP())
return r
} }
// bumpInBucket moves the given node to the front of the bucket entry list func (tab *Table) nodeRemoved(b *bucket, n *tableNode) {
// if it is contained in that list. tab.revalidation.nodeRemoved(n)
func (tab *Table) bumpInBucket(b *bucket, n *node) bool {
for i := range b.entries {
if b.entries[i].ID() == n.ID() {
if !n.IP().Equal(b.entries[i].IP()) {
// Endpoint has changed, ensure that the new IP fits into table limits.
tab.removeIP(b, b.entries[i].IP())
if !tab.addIP(b, n.IP()) {
// It doesn't, put the previous one back.
tab.addIP(b, b.entries[i].IP())
return false
}
}
// Move it to the front.
copy(b.entries[1:], b.entries[:i])
b.entries[0] = n
return true
}
}
return false
}
func (tab *Table) deleteInBucket(b *bucket, n *node) {
// Check if the node is actually in the bucket so the removed hook
// isn't called multiple times for the same node.
if !contains(b.entries, n.ID()) {
return
}
b.entries = deleteNode(b.entries, n)
tab.removeIP(b, n.IP())
if tab.nodeRemovedHook != nil { if tab.nodeRemovedHook != nil {
tab.nodeRemovedHook(b, n) tab.nodeRemovedHook(b, n)
} }
if metrics.Enabled {
bucketsCounter[b.index].Dec(1)
}
} }
func contains(ns []*node, id enode.ID) bool { // deleteInBucket removes node n from the table.
for _, n := range ns { // If there are replacement nodes in the bucket, the node is replaced.
if n.ID() == id { func (tab *Table) deleteInBucket(b *bucket, id enode.ID) *tableNode {
return true index := slices.IndexFunc(b.entries, func(e *tableNode) bool { return e.ID() == id })
if index == -1 {
// Entry has been removed already.
return nil
}
// Remove the node.
n := b.entries[index]
b.entries = slices.Delete(b.entries, index, index+1)
tab.removeIP(b, n.IP())
tab.nodeRemoved(b, n)
// Add replacement.
if len(b.replacements) == 0 {
tab.log.Debug("Removed dead node", "b", b.index, "id", n.ID(), "ip", n.IP())
return nil
}
rindex := tab.rand.Intn(len(b.replacements))
rep := b.replacements[rindex]
b.replacements = slices.Delete(b.replacements, rindex, rindex+1)
b.entries = append(b.entries, rep)
tab.nodeAdded(b, rep)
tab.log.Debug("Replaced dead node", "b", b.index, "id", n.ID(), "ip", n.IP(), "r", rep.ID(), "rip", rep.IP())
return rep
}
// bumpInBucket updates a node record if it exists in the bucket.
// The second return value reports whether the node's endpoint (IP/port) was updated.
func (tab *Table) bumpInBucket(b *bucket, newRecord *enode.Node, isInbound bool) (n *tableNode, endpointChanged bool) {
i := slices.IndexFunc(b.entries, func(elem *tableNode) bool {
return elem.ID() == newRecord.ID()
})
if i == -1 {
return nil, false // not in bucket
}
n = b.entries[i]
// For inbound updates (from the node itself) we accept any change, even if it sets
// back the sequence number. For found nodes (!isInbound), seq has to advance. Note
// this check also ensures found discv4 nodes (which always have seq=0) can't be
// updated.
if newRecord.Seq() <= n.Seq() && !isInbound {
return n, false
}
// Check endpoint update against IP limits.
ipchanged := newRecord.IPAddr() != n.IPAddr()
portchanged := newRecord.UDP() != n.UDP()
if ipchanged {
tab.removeIP(b, n.IP())
if !tab.addIP(b, newRecord.IP()) {
// It doesn't fit with the limit, put the previous record back.
tab.addIP(b, n.IP())
return n, false
} }
} }
return false // Apply update.
n.Node = newRecord
if ipchanged || portchanged {
// Ensure node is revalidated quickly for endpoint changes.
tab.revalidation.nodeEndpointChanged(tab, n)
return n, true
}
return n, false
}
func (tab *Table) handleTrackRequest(op trackRequestOp) {
var fails int
if op.success {
// Reset failure counter because it counts _consecutive_ failures.
tab.db.UpdateFindFails(op.node.ID(), op.node.IP(), 0)
} else {
fails = tab.db.FindFails(op.node.ID(), op.node.IP())
fails++
tab.db.UpdateFindFails(op.node.ID(), op.node.IP(), fails)
}
tab.mutex.Lock()
defer tab.mutex.Unlock()
b := tab.bucket(op.node.ID())
// Remove the node from the local table if it fails to return anything useful too
// many times, but only if there are enough other nodes in the bucket. This latter
// condition specifically exists to make bootstrapping in smaller test networks more
// reliable.
if fails >= maxFindnodeFailures && len(b.entries) >= bucketSize/4 {
tab.deleteInBucket(b, op.node.ID())
}
// Add found nodes.
for _, n := range op.foundNodes {
tab.handleAddNode(addNodeOp{n, false, false})
}
} }
// pushNode adds n to the front of list, keeping at most max items. // pushNode adds n to the front of list, keeping at most max items.
func pushNode(list []*node, n *node, max int) ([]*node, *node) { func pushNode(list []*tableNode, n *tableNode, max int) ([]*tableNode, *tableNode) {
if len(list) < max { if len(list) < max {
list = append(list, nil) list = append(list, nil)
} }
@ -763,40 +708,3 @@ func pushNode(list []*node, n *node, max int) ([]*node, *node) {
return list, removed return list, removed
} }
// deleteNode removes n from list.
func deleteNode(list []*node, n *node) []*node {
for i := range list {
if list[i].ID() == n.ID() {
return append(list[:i], list[i+1:]...)
}
}
return list
}
// nodesByDistance is a list of nodes, ordered by distance to target.
type nodesByDistance struct {
entries []*node
target enode.ID
}
// push adds the given node to the list, keeping the total size below maxElems.
func (h *nodesByDistance) push(n *node, maxElems int) {
ix := sort.Search(len(h.entries), func(i int) bool {
return enode.DistCmp(h.target, h.entries[i].ID(), n.ID()) > 0
})
end := len(h.entries)
if len(h.entries) < maxElems {
h.entries = append(h.entries, n)
}
if ix < end {
// Slide existing entries down to make room.
// This will overwrite the entry we just appended.
copy(h.entries[ix+1:], h.entries[ix:])
h.entries[ix] = n
}
}

244
p2p/discover/table_reval.go Normal file
View file

@ -0,0 +1,244 @@
// Copyright 2024 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library 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 Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package discover
import (
"fmt"
"math"
"slices"
"time"
"github.com/ethereum/go-ethereum/common/mclock"
"github.com/ethereum/go-ethereum/p2p/enode"
)
const never = mclock.AbsTime(math.MaxInt64)
const slowRevalidationFactor = 3
// tableRevalidation implements the node revalidation process.
// It tracks all nodes contained in Table, and schedules sending PING to them.
type tableRevalidation struct {
fast revalidationList
slow revalidationList
activeReq map[enode.ID]struct{}
}
type revalidationResponse struct {
n *tableNode
newRecord *enode.Node
didRespond bool
}
func (tr *tableRevalidation) init(cfg *Config) {
tr.activeReq = make(map[enode.ID]struct{})
tr.fast.nextTime = never
tr.fast.interval = cfg.PingInterval
tr.fast.name = "fast"
tr.slow.nextTime = never
tr.slow.interval = cfg.PingInterval * slowRevalidationFactor
tr.slow.name = "slow"
}
// nodeAdded is called when the table receives a new node.
func (tr *tableRevalidation) nodeAdded(tab *Table, n *tableNode) {
tr.fast.push(n, tab.cfg.Clock.Now(), &tab.rand)
}
// nodeRemoved is called when a node was removed from the table.
func (tr *tableRevalidation) nodeRemoved(n *tableNode) {
if n.revalList == nil {
panic(fmt.Errorf("removed node %v has nil revalList", n.ID()))
}
n.revalList.remove(n)
}
// nodeEndpointChanged is called when a change in IP or port is detected.
func (tr *tableRevalidation) nodeEndpointChanged(tab *Table, n *tableNode) {
n.isValidatedLive = false
tr.moveToList(&tr.fast, n, tab.cfg.Clock.Now(), &tab.rand)
}
// run performs node revalidation.
// It returns the next time it should be invoked, which is used in the Table main loop
// to schedule a timer. However, run can be called at any time.
func (tr *tableRevalidation) run(tab *Table, now mclock.AbsTime) (nextTime mclock.AbsTime) {
if n := tr.fast.get(now, &tab.rand, tr.activeReq); n != nil {
tr.startRequest(tab, n)
tr.fast.schedule(now, &tab.rand)
}
if n := tr.slow.get(now, &tab.rand, tr.activeReq); n != nil {
tr.startRequest(tab, n)
tr.slow.schedule(now, &tab.rand)
}
return min(tr.fast.nextTime, tr.slow.nextTime)
}
// startRequest spawns a revalidation request for node n.
func (tr *tableRevalidation) startRequest(tab *Table, n *tableNode) {
if _, ok := tr.activeReq[n.ID()]; ok {
panic(fmt.Errorf("duplicate startRequest (node %v)", n.ID()))
}
tr.activeReq[n.ID()] = struct{}{}
resp := revalidationResponse{n: n}
// Fetch the node while holding lock.
tab.mutex.Lock()
node := n.Node
tab.mutex.Unlock()
go tab.doRevalidate(resp, node)
}
func (tab *Table) doRevalidate(resp revalidationResponse, node *enode.Node) {
// Ping the selected node and wait for a pong response.
remoteSeq, err := tab.net.ping(node)
resp.didRespond = err == nil
// Also fetch record if the node replied and returned a higher sequence number.
if remoteSeq > node.Seq() {
newrec, err := tab.net.RequestENR(node)
if err != nil {
tab.log.Debug("ENR request failed", "id", node.ID(), "err", err)
} else {
resp.newRecord = newrec
}
}
select {
case tab.revalResponseCh <- resp:
case <-tab.closed:
}
}
// handleResponse processes the result of a revalidation request.
func (tr *tableRevalidation) handleResponse(tab *Table, resp revalidationResponse) {
var (
now = tab.cfg.Clock.Now()
n = resp.n
b = tab.bucket(n.ID())
)
delete(tr.activeReq, n.ID())
// If the node was removed from the table while getting checked, we need to stop
// processing here to avoid re-adding it.
if n.revalList == nil {
return
}
// Store potential seeds in database.
// This is done via defer to avoid holding Table lock while writing to DB.
defer func() {
if n.isValidatedLive && n.livenessChecks > 5 {
tab.db.UpdateNode(resp.n.Node)
}
}()
// Remaining logic needs access to Table internals.
tab.mutex.Lock()
defer tab.mutex.Unlock()
if !resp.didRespond {
n.livenessChecks /= 3
if n.livenessChecks <= 0 {
tab.deleteInBucket(b, n.ID())
} else {
tab.log.Debug("Node revalidation failed", "b", b.index, "id", n.ID(), "checks", n.livenessChecks, "q", n.revalList.name)
tr.moveToList(&tr.fast, n, now, &tab.rand)
}
return
}
// The node responded.
n.livenessChecks++
n.isValidatedLive = true
tab.log.Debug("Node revalidated", "b", b.index, "id", n.ID(), "checks", n.livenessChecks, "q", n.revalList.name)
var endpointChanged bool
if resp.newRecord != nil {
_, endpointChanged = tab.bumpInBucket(b, resp.newRecord, false)
}
// Node moves to slow list if it passed and hasn't changed.
if !endpointChanged {
tr.moveToList(&tr.slow, n, now, &tab.rand)
}
}
// moveToList ensures n is in the 'dest' list.
func (tr *tableRevalidation) moveToList(dest *revalidationList, n *tableNode, now mclock.AbsTime, rand randomSource) {
if n.revalList == dest {
return
}
if n.revalList != nil {
n.revalList.remove(n)
}
dest.push(n, now, rand)
}
// revalidationList holds a list nodes and the next revalidation time.
type revalidationList struct {
nodes []*tableNode
nextTime mclock.AbsTime
interval time.Duration
name string
}
// get returns a random node from the queue. Nodes in the 'exclude' map are not returned.
func (list *revalidationList) get(now mclock.AbsTime, rand randomSource, exclude map[enode.ID]struct{}) *tableNode {
if now < list.nextTime || len(list.nodes) == 0 {
return nil
}
for i := 0; i < len(list.nodes)*3; i++ {
n := list.nodes[rand.Intn(len(list.nodes))]
_, excluded := exclude[n.ID()]
if !excluded {
return n
}
}
return nil
}
func (list *revalidationList) schedule(now mclock.AbsTime, rand randomSource) {
list.nextTime = now.Add(time.Duration(rand.Int63n(int64(list.interval))))
}
func (list *revalidationList) push(n *tableNode, now mclock.AbsTime, rand randomSource) {
list.nodes = append(list.nodes, n)
if list.nextTime == never {
list.schedule(now, rand)
}
n.revalList = list
}
func (list *revalidationList) remove(n *tableNode) {
i := slices.Index(list.nodes, n)
if i == -1 {
panic(fmt.Errorf("node %v not found in list", n.ID()))
}
list.nodes = slices.Delete(list.nodes, i, i+1)
if len(list.nodes) == 0 {
list.nextTime = never
}
n.revalList = nil
}
func (list *revalidationList) contains(id enode.ID) bool {
return slices.ContainsFunc(list.nodes, func(n *tableNode) bool {
return n.ID() == id
})
}

View file

@ -0,0 +1,119 @@
// Copyright 2024 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library 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 Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package discover
import (
"net"
"testing"
"time"
"github.com/ethereum/go-ethereum/common/mclock"
"github.com/ethereum/go-ethereum/p2p/enode"
"github.com/ethereum/go-ethereum/p2p/enr"
)
// This test checks that revalidation can handle a node disappearing while
// a request is active.
func TestRevalidation_nodeRemoved(t *testing.T) {
var (
clock mclock.Simulated
transport = newPingRecorder()
tab, db = newInactiveTestTable(transport, Config{Clock: &clock})
tr = &tab.revalidation
)
defer db.Close()
// Add a node to the table.
node := nodeAtDistance(tab.self().ID(), 255, net.IP{77, 88, 99, 1})
tab.handleAddNode(addNodeOp{node: node})
// Start a revalidation request. Schedule once to get the next start time,
// then advance the clock to that point and schedule again to start.
next := tr.run(tab, clock.Now())
clock.Run(time.Duration(next + 1))
tr.run(tab, clock.Now())
if len(tr.activeReq) != 1 {
t.Fatal("revalidation request did not start:", tr.activeReq)
}
// Delete the node.
tab.deleteInBucket(tab.bucket(node.ID()), node.ID())
// Now finish the revalidation request.
var resp revalidationResponse
select {
case resp = <-tab.revalResponseCh:
case <-time.After(1 * time.Second):
t.Fatal("timed out waiting for revalidation")
}
tr.handleResponse(tab, resp)
// Ensure the node was not re-added to the table.
if tab.getNode(node.ID()) != nil {
t.Fatal("node was re-added to Table")
}
if tr.fast.contains(node.ID()) || tr.slow.contains(node.ID()) {
t.Fatal("removed node contained in revalidation list")
}
}
// This test checks that nodes with an updated endpoint remain in the fast revalidation list.
func TestRevalidation_endpointUpdate(t *testing.T) {
var (
clock mclock.Simulated
transport = newPingRecorder()
tab, db = newInactiveTestTable(transport, Config{Clock: &clock})
tr = &tab.revalidation
)
defer db.Close()
// Add node to table.
node := nodeAtDistance(tab.self().ID(), 255, net.IP{77, 88, 99, 1})
tab.handleAddNode(addNodeOp{node: node})
// Update the record in transport, including endpoint update.
record := node.Record()
record.Set(enr.IP{100, 100, 100, 100})
record.Set(enr.UDP(9999))
nodev2 := enode.SignNull(record, node.ID())
transport.updateRecord(nodev2)
// Start a revalidation request. Schedule once to get the next start time,
// then advance the clock to that point and schedule again to start.
next := tr.run(tab, clock.Now())
clock.Run(time.Duration(next + 1))
tr.run(tab, clock.Now())
if len(tr.activeReq) != 1 {
t.Fatal("revalidation request did not start:", tr.activeReq)
}
// Now finish the revalidation request.
var resp revalidationResponse
select {
case resp = <-tab.revalResponseCh:
case <-time.After(1 * time.Second):
t.Fatal("timed out waiting for revalidation")
}
tr.handleResponse(tab, resp)
if tr.fast.nodes[0].ID() != node.ID() {
t.Fatal("node not contained in fast revalidation list")
}
if tr.fast.nodes[0].isValidatedLive {
t.Fatal("node is marked live after endpoint change")
}
}

View file

@ -20,14 +20,17 @@ import (
"crypto/ecdsa" "crypto/ecdsa"
"fmt" "fmt"
"math/rand" "math/rand"
"net" "net"
"reflect" "reflect"
"slices"
"testing" "testing"
"testing/quick" "testing/quick"
"time" "time"
"github.com/ethereum/go-ethereum/common/mclock"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/internal/testlog"
"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/p2p/enr" "github.com/ethereum/go-ethereum/p2p/enr"
"github.com/ethereum/go-ethereum/p2p/netutil" "github.com/ethereum/go-ethereum/p2p/netutil"
@ -49,33 +52,60 @@ func TestTable_pingReplace(t *testing.T) {
} }
func testPingReplace(t *testing.T, newNodeIsResponding, lastInBucketIsResponding bool) { func testPingReplace(t *testing.T, newNodeIsResponding, lastInBucketIsResponding bool) {
simclock := new(mclock.Simulated)
transport := newPingRecorder() transport := newPingRecorder()
tab, db := newTestTable(transport, Config{
tab, db := newTestTable(transport) Clock: simclock,
Log: testlog.Logger(t, log.LevelTrace),
})
defer db.Close() defer db.Close()
defer tab.close() defer tab.close()
<-tab.initDone <-tab.initDone
// Fill up the sender's bucket. // Fill up the sender's bucket.
pingKey, _ := crypto.HexToECDSA("45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8") replacementNodeKey, _ := crypto.HexToECDSA("45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8")
pingSender := wrapNode(enode.NewV4(&pingKey.PublicKey, net.IP{127, 0, 0, 1}, 99, 99)) replacementNode := enode.NewV4(&replacementNodeKey.PublicKey, net.IP{127, 0, 0, 1}, 99, 99)
last := fillBucket(tab, pingSender) last := fillBucket(tab, replacementNode.ID())
tab.mutex.Lock()
nodeEvents := newNodeEventRecorder(128)
tab.nodeAddedHook = nodeEvents.nodeAdded
tab.nodeRemovedHook = nodeEvents.nodeRemoved
tab.mutex.Unlock()
// Add the sender as if it just pinged us. Revalidate should replace the last node in // The revalidation process should replace
// its bucket if it is unresponsive. Revalidate again to ensure that // this node in the bucket if it is unresponsive.
transport.dead[last.ID()] = !lastInBucketIsResponding transport.dead[last.ID()] = !lastInBucketIsResponding
transport.dead[pingSender.ID()] = !newNodeIsResponding transport.dead[replacementNode.ID()] = !newNodeIsResponding
tab.addSeenNode(pingSender) // Add replacement node to table.
tab.doRevalidate(make(chan struct{}, 1)) tab.addFoundNode(replacementNode, false)
tab.doRevalidate(make(chan struct{}, 1))
if !transport.pinged[last.ID()] { t.Log("last:", last.ID())
// Oldest node in bucket is pinged to see whether it is still alive. t.Log("replacement:", replacementNode.ID())
t.Error("table did not ping last node in bucket")
// Wait until the last node was pinged.
waitForRevalidationPing(t, transport, tab, last.ID())
if !lastInBucketIsResponding {
if !nodeEvents.waitNodeAbsent(last.ID(), 2*time.Second) {
t.Error("last node was not removed")
}
if !nodeEvents.waitNodePresent(replacementNode.ID(), 2*time.Second) {
t.Error("replacement node was not added")
} }
// If a replacement is expected, we also need to wait until the replacement node
// was pinged and added/removed.
waitForRevalidationPing(t, transport, tab, replacementNode.ID())
if !newNodeIsResponding {
if !nodeEvents.waitNodeAbsent(replacementNode.ID(), 2*time.Second) {
t.Error("replacement node was not removed")
}
}
}
// Check bucket content.
tab.mutex.Lock() tab.mutex.Lock()
defer tab.mutex.Unlock() defer tab.mutex.Unlock()
@ -83,86 +113,50 @@ func testPingReplace(t *testing.T, newNodeIsResponding, lastInBucketIsResponding
if !lastInBucketIsResponding && !newNodeIsResponding { if !lastInBucketIsResponding && !newNodeIsResponding {
wantSize-- wantSize--
} }
bucket := tab.bucket(replacementNode.ID())
if l := len(tab.bucket(pingSender.ID()).entries); l != wantSize { if l := len(bucket.entries); l != wantSize {
t.Errorf("wrong bucket size after bond: got %d, want %d", l, wantSize) t.Errorf("wrong bucket size after revalidation: got %d, want %d", l, wantSize)
} }
if ok := containsID(bucket.entries, last.ID()); ok != lastInBucketIsResponding {
if found := contains(tab.bucket(pingSender.ID()).entries, last.ID()); found != lastInBucketIsResponding { t.Errorf("revalidated node found: %t, want: %t", ok, lastInBucketIsResponding)
t.Errorf("last entry found: %t, want: %t", found, lastInBucketIsResponding)
} }
wantNewEntry := newNodeIsResponding && !lastInBucketIsResponding wantNewEntry := newNodeIsResponding && !lastInBucketIsResponding
if found := contains(tab.bucket(pingSender.ID()).entries, pingSender.ID()); found != wantNewEntry { if ok := containsID(bucket.entries, replacementNode.ID()); ok != wantNewEntry {
t.Errorf("new entry found: %t, want: %t", found, wantNewEntry) t.Errorf("replacement node found: %t, want: %t", ok, wantNewEntry)
} }
} }
func TestBucket_bumpNoDuplicates(t *testing.T) { // waitForRevalidationPing waits until a PING message is sent to a node with the given id.
t.Parallel() func waitForRevalidationPing(t *testing.T, transport *pingRecorder, tab *Table, id enode.ID) *enode.Node {
t.Helper()
cfg := &quick.Config{ simclock := tab.cfg.Clock.(*mclock.Simulated)
MaxCount: 1000, maxAttempts := tab.len() * 8
Rand: rand.New(rand.NewSource(time.Now().Unix())), for i := 0; i < maxAttempts; i++ {
Values: func(args []reflect.Value, rand *rand.Rand) { simclock.Run(tab.cfg.PingInterval * slowRevalidationFactor)
// generate a random list of nodes. this will be the content of the bucket. p := transport.waitPing(2 * time.Second)
n := rand.Intn(bucketSize-1) + 1 if p == nil {
nodes := make([]*node, n) t.Fatal("Table did not send revalidation ping")
for i := range nodes {
nodes[i] = nodeAtDistance(enode.ID{}, 200, intIP(200))
} }
args[0] = reflect.ValueOf(nodes) if id == (enode.ID{}) || p.ID() == id {
// generate random bump positions. return p
bumps := make([]int, rand.Intn(100))
for i := range bumps {
bumps[i] = rand.Intn(len(nodes))
}
args[1] = reflect.ValueOf(bumps)
},
}
prop := func(nodes []*node, bumps []int) (ok bool) {
tab, db := newTestTable(newPingRecorder())
defer db.Close()
defer tab.close()
b := &bucket{entries: make([]*node, len(nodes))}
copy(b.entries, nodes)
for i, pos := range bumps {
tab.bumpInBucket(b, b.entries[pos])
if hasDuplicates(b.entries) {
t.Logf("bucket has duplicates after %d/%d bumps:", i+1, len(bumps))
for _, n := range b.entries {
t.Logf(" %p", n)
}
return false
} }
} }
t.Fatalf("Table did not ping node %v (%d attempts)", id, maxAttempts)
checkIPLimitInvariant(t, tab) return nil
return true
}
if err := quick.Check(prop, cfg); err != nil {
t.Error(err)
}
} }
// This checks that the table-wide IP limit is applied correctly. // This checks that the table-wide IP limit is applied correctly.
func TestTable_IPLimit(t *testing.T) { func TestTable_IPLimit(t *testing.T) {
transport := newPingRecorder() transport := newPingRecorder()
tab, db := newTestTable(transport, Config{})
tab, db := newTestTable(transport)
defer db.Close() defer db.Close()
defer tab.close() defer tab.close()
for i := 0; i < tableIPLimit+1; i++ { for i := 0; i < tableIPLimit+1; i++ {
n := nodeAtDistance(tab.self().ID(), i, net.IP{172, 0, 1, byte(i)}) n := nodeAtDistance(tab.self().ID(), i, net.IP{172, 0, 1, byte(i)})
tab.addSeenNode(n) tab.addFoundNode(n, false)
} }
if tab.len() > tableIPLimit { if tab.len() > tableIPLimit {
@ -175,15 +169,14 @@ func TestTable_IPLimit(t *testing.T) {
// This checks that the per-bucket IP limit is applied correctly. // This checks that the per-bucket IP limit is applied correctly.
func TestTable_BucketIPLimit(t *testing.T) { func TestTable_BucketIPLimit(t *testing.T) {
transport := newPingRecorder() transport := newPingRecorder()
tab, db := newTestTable(transport, Config{})
tab, db := newTestTable(transport)
defer db.Close() defer db.Close()
defer tab.close() defer tab.close()
d := 3 d := 3
for i := 0; i < bucketIPLimit+1; i++ { for i := 0; i < bucketIPLimit+1; i++ {
n := nodeAtDistance(tab.self().ID(), d, net.IP{172, 0, 1, byte(i)}) n := nodeAtDistance(tab.self().ID(), d, net.IP{172, 0, 1, byte(i)})
tab.addSeenNode(n) tab.addFoundNode(n, false)
} }
if tab.len() > bucketIPLimit { if tab.len() > bucketIPLimit {
@ -217,8 +210,7 @@ func TestTable_findnodeByID(t *testing.T) {
test := func(test *closeTest) bool { test := func(test *closeTest) bool {
// for any node table, Target and N // for any node table, Target and N
transport := newPingRecorder() transport := newPingRecorder()
tab, db := newTestTable(transport, Config{})
tab, db := newTestTable(transport)
defer db.Close() defer db.Close()
defer tab.close() defer tab.close()
fillTable(tab, test.All, true) fillTable(tab, test.All, true)
@ -251,7 +243,7 @@ func TestTable_findnodeByID(t *testing.T) {
// check that the result nodes have minimum distance to target. // check that the result nodes have minimum distance to target.
for _, b := range tab.buckets { for _, b := range tab.buckets {
for _, n := range b.entries { for _, n := range b.entries {
if contains(result, n.ID()) { if containsID(result, n.ID()) {
continue // don't run the check below for nodes in result continue // don't run the check below for nodes in result
} }
@ -277,7 +269,7 @@ func TestTable_findnodeByID(t *testing.T) {
type closeTest struct { type closeTest struct {
Self enode.ID Self enode.ID
Target enode.ID Target enode.ID
All []*node All []*enode.Node
N int N int
} }
@ -291,16 +283,15 @@ func (*closeTest) Generate(rand *rand.Rand, size int) reflect.Value {
for _, id := range gen([]enode.ID{}, rand).([]enode.ID) { for _, id := range gen([]enode.ID{}, rand).([]enode.ID) {
r := new(enr.Record) r := new(enr.Record)
r.Set(enr.IP(genIP(rand))) r.Set(enr.IP(genIP(rand)))
n := wrapNode(enode.SignNull(r, id)) n := enode.SignNull(r, id)
n.livenessChecks = 1
t.All = append(t.All, n) t.All = append(t.All, n)
} }
return reflect.ValueOf(t) return reflect.ValueOf(t)
} }
func TestTable_addVerifiedNode(t *testing.T) { func TestTable_addInboundNode(t *testing.T) {
tab, db := newTestTable(newPingRecorder()) tab, db := newTestTable(newPingRecorder(), Config{})
<-tab.initDone <-tab.initDone
defer db.Close() defer db.Close()
@ -309,32 +300,29 @@ func TestTable_addVerifiedNode(t *testing.T) {
// Insert two nodes. // Insert two nodes.
n1 := nodeAtDistance(tab.self().ID(), 256, net.IP{88, 77, 66, 1}) n1 := nodeAtDistance(tab.self().ID(), 256, net.IP{88, 77, 66, 1})
n2 := nodeAtDistance(tab.self().ID(), 256, net.IP{88, 77, 66, 2}) n2 := nodeAtDistance(tab.self().ID(), 256, net.IP{88, 77, 66, 2})
tab.addSeenNode(n1) tab.addFoundNode(n1, false)
tab.addSeenNode(n2) tab.addFoundNode(n2, false)
checkBucketContent(t, tab, []*enode.Node{n1, n2})
// Verify bucket content: // Add a changed version of n2. The bucket should be updated.
bcontent := []*node{n1, n2}
if !reflect.DeepEqual(tab.bucket(n1.ID()).entries, bcontent) {
t.Fatalf("wrong bucket content: %v", tab.bucket(n1.ID()).entries)
}
// Add a changed version of n2.
newrec := n2.Record() newrec := n2.Record()
newrec.Set(enr.IP{99, 99, 99, 99}) newrec.Set(enr.IP{99, 99, 99, 99})
newn2 := wrapNode(enode.SignNull(newrec, n2.ID())) n2v2 := enode.SignNull(newrec, n2.ID())
tab.addVerifiedNode(newn2) tab.addInboundNode(n2v2)
checkBucketContent(t, tab, []*enode.Node{n1, n2v2})
// Check that bucket is updated correctly. // Try updating n2 without sequence number change. The update is accepted
newBcontent := []*node{newn2, n1} // because it's inbound.
if !reflect.DeepEqual(tab.bucket(n1.ID()).entries, newBcontent) { newrec = n2.Record()
t.Fatalf("wrong bucket content after update: %v", tab.bucket(n1.ID()).entries) newrec.Set(enr.IP{100, 100, 100, 100})
} newrec.SetSeq(n2.Seq())
n2v3 := enode.SignNull(newrec, n2.ID())
checkIPLimitInvariant(t, tab) tab.addInboundNode(n2v3)
checkBucketContent(t, tab, []*enode.Node{n1, n2v3})
} }
func TestTable_addSeenNode(t *testing.T) { func TestTable_addFoundNode(t *testing.T) {
tab, db := newTestTable(newPingRecorder()) tab, db := newTestTable(newPingRecorder(), Config{})
<-tab.initDone <-tab.initDone
defer db.Close() defer db.Close()
@ -343,26 +331,86 @@ func TestTable_addSeenNode(t *testing.T) {
// Insert two nodes. // Insert two nodes.
n1 := nodeAtDistance(tab.self().ID(), 256, net.IP{88, 77, 66, 1}) n1 := nodeAtDistance(tab.self().ID(), 256, net.IP{88, 77, 66, 1})
n2 := nodeAtDistance(tab.self().ID(), 256, net.IP{88, 77, 66, 2}) n2 := nodeAtDistance(tab.self().ID(), 256, net.IP{88, 77, 66, 2})
tab.addSeenNode(n1) tab.addFoundNode(n1, false)
tab.addSeenNode(n2) tab.addFoundNode(n2, false)
checkBucketContent(t, tab, []*enode.Node{n1, n2})
// Verify bucket content: // Add a changed version of n2. The bucket should be updated.
bcontent := []*node{n1, n2}
if !reflect.DeepEqual(tab.bucket(n1.ID()).entries, bcontent) {
t.Fatalf("wrong bucket content: %v", tab.bucket(n1.ID()).entries)
}
// Add a changed version of n2.
newrec := n2.Record() newrec := n2.Record()
newrec.Set(enr.IP{99, 99, 99, 99}) newrec.Set(enr.IP{99, 99, 99, 99})
newn2 := wrapNode(enode.SignNull(newrec, n2.ID())) n2v2 := enode.SignNull(newrec, n2.ID())
tab.addSeenNode(newn2) tab.addFoundNode(n2v2, false)
checkBucketContent(t, tab, []*enode.Node{n1, n2v2})
// Check that bucket content is unchanged. // Try updating n2 without a sequence number change.
if !reflect.DeepEqual(tab.bucket(n1.ID()).entries, bcontent) { // The update should not be accepted.
t.Fatalf("wrong bucket content after update: %v", tab.bucket(n1.ID()).entries) newrec = n2.Record()
newrec.Set(enr.IP{100, 100, 100, 100})
newrec.SetSeq(n2.Seq())
n2v3 := enode.SignNull(newrec, n2.ID())
tab.addFoundNode(n2v3, false)
checkBucketContent(t, tab, []*enode.Node{n1, n2v2})
}
// This test checks that discv4 nodes can update their own endpoint via PING.
func TestTable_addInboundNodeUpdateV4Accept(t *testing.T) {
tab, db := newTestTable(newPingRecorder(), Config{})
<-tab.initDone
defer db.Close()
defer tab.close()
// Add a v4 node.
key, _ := crypto.HexToECDSA("dd3757a8075e88d0f2b1431e7d3c5b1562e1c0aab9643707e8cbfcc8dae5cfe3")
n1 := enode.NewV4(&key.PublicKey, net.IP{88, 77, 66, 1}, 9000, 9000)
tab.addInboundNode(n1)
checkBucketContent(t, tab, []*enode.Node{n1})
// Add an updated version with changed IP.
// The update will be accepted because it is inbound.
n1v2 := enode.NewV4(&key.PublicKey, net.IP{99, 99, 99, 99}, 9000, 9000)
tab.addInboundNode(n1v2)
checkBucketContent(t, tab, []*enode.Node{n1v2})
}
// This test checks that discv4 node entries will NOT be updated when a
// changed record is found.
func TestTable_addFoundNodeV4UpdateReject(t *testing.T) {
tab, db := newTestTable(newPingRecorder(), Config{})
<-tab.initDone
defer db.Close()
defer tab.close()
// Add a v4 node.
key, _ := crypto.HexToECDSA("dd3757a8075e88d0f2b1431e7d3c5b1562e1c0aab9643707e8cbfcc8dae5cfe3")
n1 := enode.NewV4(&key.PublicKey, net.IP{88, 77, 66, 1}, 9000, 9000)
tab.addFoundNode(n1, false)
checkBucketContent(t, tab, []*enode.Node{n1})
// Add an updated version with changed IP.
// The update won't be accepted because it isn't inbound.
n1v2 := enode.NewV4(&key.PublicKey, net.IP{99, 99, 99, 99}, 9000, 9000)
tab.addFoundNode(n1v2, false)
checkBucketContent(t, tab, []*enode.Node{n1})
}
func checkBucketContent(t *testing.T, tab *Table, nodes []*enode.Node) {
t.Helper()
b := tab.bucket(nodes[0].ID())
if reflect.DeepEqual(unwrapNodes(b.entries), nodes) {
return
} }
t.Log("wrong bucket content. have nodes:")
for _, n := range b.entries {
t.Logf(" %v (seq=%v, ip=%v)", n.ID(), n.Seq(), n.IP())
}
t.Log("want nodes:")
for _, n := range nodes {
t.Logf(" %v (seq=%v, ip=%v)", n.ID(), n.Seq(), n.IP())
}
t.FailNow()
// Also check IP limits.
checkIPLimitInvariant(t, tab) checkIPLimitInvariant(t, tab)
} }
@ -370,7 +418,10 @@ func TestTable_addSeenNode(t *testing.T) {
// announces a new sequence number, the new record should be pulled. // announces a new sequence number, the new record should be pulled.
func TestTable_revalidateSyncRecord(t *testing.T) { func TestTable_revalidateSyncRecord(t *testing.T) {
transport := newPingRecorder() transport := newPingRecorder()
tab, db := newTestTable(transport) tab, db := newTestTable(transport, Config{
Clock: new(mclock.Simulated),
Log: testlog.Logger(t, log.LevelTrace),
})
<-tab.initDone <-tab.initDone
defer db.Close() defer db.Close()
@ -382,15 +433,18 @@ func TestTable_revalidateSyncRecord(t *testing.T) {
r.Set(enr.IP(net.IP{127, 0, 0, 1})) r.Set(enr.IP(net.IP{127, 0, 0, 1}))
id := enode.ID{1} id := enode.ID{1}
n1 := wrapNode(enode.SignNull(&r, id)) n1 := enode.SignNull(&r, id)
tab.addSeenNode(n1) tab.addFoundNode(n1, false)
// Update the node record. // Update the node record.
r.Set(enr.WithEntry("foo", "bar")) r.Set(enr.WithEntry("foo", "bar"))
n2 := enode.SignNull(&r, id) n2 := enode.SignNull(&r, id)
transport.updateRecord(n2) transport.updateRecord(n2)
tab.doRevalidate(make(chan struct{}, 1)) // Wait for revalidation. We wait for the node to be revalidated two times
// in order to synchronize with the update in the able.
waitForRevalidationPing(t, transport, tab, n2.ID())
waitForRevalidationPing(t, transport, tab, n2.ID())
intable := tab.getNode(id) intable := tab.getNode(id)
if !reflect.DeepEqual(intable, n2) { if !reflect.DeepEqual(intable, n2) {
@ -405,7 +459,7 @@ func TestNodesPush(t *testing.T) {
n1 := nodeAtDistance(target, 255, intIP(1)) n1 := nodeAtDistance(target, 255, intIP(1))
n2 := nodeAtDistance(target, 254, intIP(2)) n2 := nodeAtDistance(target, 254, intIP(2))
n3 := nodeAtDistance(target, 253, intIP(3)) n3 := nodeAtDistance(target, 253, intIP(3))
perm := [][]*node{ perm := [][]*enode.Node{
{n3, n2, n1}, {n3, n2, n1},
{n3, n1, n2}, {n3, n1, n2},
{n2, n3, n1}, {n2, n3, n1},
@ -420,8 +474,7 @@ func TestNodesPush(t *testing.T) {
for _, n := range nodes { for _, n := range nodes {
list.push(n, 3) list.push(n, 3)
} }
if !slices.EqualFunc(list.entries, perm[0], nodeIDEqual) {
if !slicesEqual(list.entries, perm[0], nodeIDEqual) {
t.Fatal("not equal") t.Fatal("not equal")
} }
} }
@ -432,31 +485,16 @@ func TestNodesPush(t *testing.T) {
for _, n := range nodes { for _, n := range nodes {
list.push(n, 2) list.push(n, 2)
} }
if !slices.EqualFunc(list.entries, perm[0][:2], nodeIDEqual) {
if !slicesEqual(list.entries, perm[0][:2], nodeIDEqual) {
t.Fatal("not equal") t.Fatal("not equal")
} }
} }
} }
func nodeIDEqual(n1, n2 *node) bool { func nodeIDEqual[N nodeType](n1, n2 N) bool {
return n1.ID() == n2.ID() return n1.ID() == n2.ID()
} }
func slicesEqual[T any](s1, s2 []T, check func(e1, e2 T) bool) bool {
if len(s1) != len(s2) {
return false
}
for i := range s1 {
if !check(s1[i], s2[i]) {
return false
}
}
return true
}
// gen wraps quick.Value so it's easier to use. // gen wraps quick.Value so it's easier to use.
// it generates a random value of the given value's type. // it generates a random value of the given value's type.
func gen(typ interface{}, rand *rand.Rand) interface{} { func gen(typ interface{}, rand *rand.Rand) interface{} {

View file

@ -25,6 +25,8 @@ import (
"math/rand" "math/rand"
"net" "net"
"sync" "sync"
"sync/atomic"
"time"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/p2p/enode" "github.com/ethereum/go-ethereum/p2p/enode"
@ -41,29 +43,33 @@ func init() {
nullNode = enode.SignNull(&r, enode.ID{}) nullNode = enode.SignNull(&r, enode.ID{})
} }
func newTestTable(t transport) (*Table, *enode.DB) { func newTestTable(t transport, cfg Config) (*Table, *enode.DB) {
cfg := Config{} tab, db := newInactiveTestTable(t, cfg)
go tab.loop()
return tab, db
}
// newInactiveTestTable creates a Table without running the main loop.
func newInactiveTestTable(t transport, cfg Config) (*Table, *enode.DB) {
db, _ := enode.OpenDB("") db, _ := enode.OpenDB("")
tab, _ := newTable(t, db, cfg) tab, _ := newTable(t, db, cfg)
go tab.loop()
return tab, db return tab, db
} }
// nodeAtDistance creates a node for which enode.LogDist(base, n.id) == ld. // nodeAtDistance creates a node for which enode.LogDist(base, n.id) == ld.
func nodeAtDistance(base enode.ID, ld int, ip net.IP) *node { func nodeAtDistance(base enode.ID, ld int, ip net.IP) *enode.Node {
var r enr.Record var r enr.Record
r.Set(enr.IP(ip)) r.Set(enr.IP(ip))
r.Set(enr.UDP(30303)) r.Set(enr.UDP(30303))
return wrapNode(enode.SignNull(&r, idAtDistance(base, ld))) return enode.SignNull(&r, idAtDistance(base, ld))
} }
// nodesAtDistance creates n nodes for which enode.LogDist(base, node.ID()) == ld. // nodesAtDistance creates n nodes for which enode.LogDist(base, node.ID()) == ld.
func nodesAtDistance(base enode.ID, ld int, n int) []*enode.Node { func nodesAtDistance(base enode.ID, ld int, n int) []*enode.Node {
results := make([]*enode.Node, n) results := make([]*enode.Node, n)
for i := range results { for i := range results {
results[i] = unwrapNode(nodeAtDistance(base, ld, intIP(i))) results[i] = nodeAtDistance(base, ld, intIP(i))
} }
return results return results
@ -106,12 +112,14 @@ func intIP(i int) net.IP {
} }
// fillBucket inserts nodes into the given bucket until it is full. // fillBucket inserts nodes into the given bucket until it is full.
func fillBucket(tab *Table, n *node) (last *node) { func fillBucket(tab *Table, id enode.ID) (last *tableNode) {
ld := enode.LogDist(tab.self().ID(), n.ID()) ld := enode.LogDist(tab.self().ID(), id)
b := tab.bucket(n.ID()) b := tab.bucket(id)
for len(b.entries) < bucketSize { for len(b.entries) < bucketSize {
b.entries = append(b.entries, nodeAtDistance(tab.self().ID(), ld, intIP(ld))) node := nodeAtDistance(tab.self().ID(), ld, intIP(ld))
if !tab.addFoundNode(node, false) {
panic("node not added")
}
} }
return b.entries[bucketSize-1] return b.entries[bucketSize-1]
@ -119,19 +127,18 @@ func fillBucket(tab *Table, n *node) (last *node) {
// fillTable adds nodes the table to the end of their corresponding bucket // fillTable adds nodes the table to the end of their corresponding bucket
// if the bucket is not full. The caller must not hold tab.mutex. // if the bucket is not full. The caller must not hold tab.mutex.
func fillTable(tab *Table, nodes []*node, setLive bool) { func fillTable(tab *Table, nodes []*enode.Node, setLive bool) {
for _, n := range nodes { for _, n := range nodes {
if setLive { tab.addFoundNode(n, setLive)
n.livenessChecks = 1
}
tab.addSeenNode(n)
} }
} }
type pingRecorder struct { type pingRecorder struct {
mu sync.Mutex mu sync.Mutex
dead, pinged map[enode.ID]bool cond *sync.Cond
dead map[enode.ID]bool
records map[enode.ID]*enode.Node records map[enode.ID]*enode.Node
pinged []*enode.Node
n *enode.Node n *enode.Node
} }
@ -141,12 +148,13 @@ func newPingRecorder() *pingRecorder {
r.Set(enr.IP{0, 0, 0, 0}) r.Set(enr.IP{0, 0, 0, 0})
n := enode.SignNull(&r, enode.ID{}) n := enode.SignNull(&r, enode.ID{})
return &pingRecorder{ t := &pingRecorder{
dead: make(map[enode.ID]bool), dead: make(map[enode.ID]bool),
pinged: make(map[enode.ID]bool),
records: make(map[enode.ID]*enode.Node), records: make(map[enode.ID]*enode.Node),
n: n, n: n,
} }
t.cond = sync.NewCond(&t.mu)
return t
} }
// updateRecord updates a node record. Future calls to ping and // updateRecord updates a node record. Future calls to ping and
@ -162,12 +170,40 @@ func (t *pingRecorder) Self() *enode.Node { return nullNode }
func (t *pingRecorder) lookupSelf() []*enode.Node { return nil } func (t *pingRecorder) lookupSelf() []*enode.Node { return nil }
func (t *pingRecorder) lookupRandom() []*enode.Node { return nil } func (t *pingRecorder) lookupRandom() []*enode.Node { return nil }
func (t *pingRecorder) waitPing(timeout time.Duration) *enode.Node {
t.mu.Lock()
defer t.mu.Unlock()
// Wake up the loop on timeout.
var timedout atomic.Bool
timer := time.AfterFunc(timeout, func() {
timedout.Store(true)
t.cond.Broadcast()
})
defer timer.Stop()
// Wait for a ping.
for {
if timedout.Load() {
return nil
}
if len(t.pinged) > 0 {
n := t.pinged[0]
t.pinged = append(t.pinged[:0], t.pinged[1:]...)
return n
}
t.cond.Wait()
}
}
// ping simulates a ping request. // ping simulates a ping request.
func (t *pingRecorder) ping(n *enode.Node) (seq uint64, err error) { func (t *pingRecorder) ping(n *enode.Node) (seq uint64, err error) {
t.mu.Lock() t.mu.Lock()
defer t.mu.Unlock() defer t.mu.Unlock()
t.pinged[n.ID()] = true t.pinged = append(t.pinged, n)
t.cond.Broadcast()
if t.dead[n.ID()] { if t.dead[n.ID()] {
return 0, errTimeout return 0, errTimeout
} }
@ -191,7 +227,7 @@ func (t *pingRecorder) RequestENR(n *enode.Node) (*enode.Node, error) {
return t.records[n.ID()], nil return t.records[n.ID()], nil
} }
func hasDuplicates(slice []*node) bool { func hasDuplicates(slice []*enode.Node) bool {
seen := make(map[enode.ID]bool, len(slice)) seen := make(map[enode.ID]bool, len(slice))
for i, e := range slice { for i, e := range slice {
if e == nil { if e == nil {
@ -241,14 +277,14 @@ func nodeEqual(n1 *enode.Node, n2 *enode.Node) bool {
return n1.ID() == n2.ID() && n1.IP().Equal(n2.IP()) return n1.ID() == n2.ID() && n1.IP().Equal(n2.IP())
} }
func sortByID(nodes []*enode.Node) { func sortByID[N nodeType](nodes []N) {
slices.SortFunc(nodes, func(a, b *enode.Node) int { slices.SortFunc(nodes, func(a, b N) int {
return bytes.Compare(a.ID().Bytes(), b.ID().Bytes()) return bytes.Compare(a.ID().Bytes(), b.ID().Bytes())
}) })
} }
func sortedByDistanceTo(distbase enode.ID, slice []*node) bool { func sortedByDistanceTo(distbase enode.ID, slice []*enode.Node) bool {
return slices.IsSortedFunc(slice, func(a, b *node) int { return slices.IsSortedFunc(slice, func(a, b *enode.Node) int {
return enode.DistCmp(distbase, a.ID(), b.ID()) return enode.DistCmp(distbase, a.ID(), b.ID())
}) })
} }
@ -283,3 +319,57 @@ func hexEncPubkey(h string) (ret encPubkey) {
return ret return ret
} }
type nodeEventRecorder struct {
evc chan recordedNodeEvent
}
type recordedNodeEvent struct {
node *tableNode
added bool
}
func newNodeEventRecorder(buffer int) *nodeEventRecorder {
return &nodeEventRecorder{
evc: make(chan recordedNodeEvent, buffer),
}
}
func (set *nodeEventRecorder) nodeAdded(b *bucket, n *tableNode) {
select {
case set.evc <- recordedNodeEvent{n, true}:
default:
panic("no space in event buffer")
}
}
func (set *nodeEventRecorder) nodeRemoved(b *bucket, n *tableNode) {
select {
case set.evc <- recordedNodeEvent{n, false}:
default:
panic("no space in event buffer")
}
}
func (set *nodeEventRecorder) waitNodePresent(id enode.ID, timeout time.Duration) bool {
return set.waitNodeEvent(id, timeout, true)
}
func (set *nodeEventRecorder) waitNodeAbsent(id enode.ID, timeout time.Duration) bool {
return set.waitNodeEvent(id, timeout, false)
}
func (set *nodeEventRecorder) waitNodeEvent(id enode.ID, timeout time.Duration, added bool) bool {
timer := time.NewTimer(timeout)
defer timer.Stop()
for {
select {
case ev := <-set.evc:
if ev.node.ID() == id && ev.added == added {
return true
}
case <-timer.C:
return false
}
}
}

View file

@ -19,14 +19,14 @@ package discover
import ( import (
"crypto/ecdsa" "crypto/ecdsa"
"fmt" "fmt"
"net" "net/netip"
"slices"
"testing" "testing"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/p2p/discover/v4wire" "github.com/ethereum/go-ethereum/p2p/discover/v4wire"
"github.com/ethereum/go-ethereum/p2p/enode" "github.com/ethereum/go-ethereum/p2p/enode"
"github.com/ethereum/go-ethereum/p2p/enr" "github.com/ethereum/go-ethereum/p2p/enr"
"golang.org/x/exp/slices"
) )
func TestUDPv4_Lookup(t *testing.T) { func TestUDPv4_Lookup(t *testing.T) {
@ -40,7 +40,7 @@ func TestUDPv4_Lookup(t *testing.T) {
} }
// Seed table with initial node. // Seed table with initial node.
fillTable(test.table, []*node{wrapNode(lookupTestnet.node(256, 0))}, true) fillTable(test.table, []*enode.Node{lookupTestnet.node(256, 0)}, true)
// Start the lookup. // Start the lookup.
resultC := make(chan []*enode.Node, 1) resultC := make(chan []*enode.Node, 1)
@ -76,9 +76,9 @@ func TestUDPv4_LookupIterator(t *testing.T) {
defer test.close() defer test.close()
// Seed table with initial nodes. // Seed table with initial nodes.
bootnodes := make([]*node, len(lookupTestnet.dists[256])) bootnodes := make([]*enode.Node, len(lookupTestnet.dists[256]))
for i := range lookupTestnet.dists[256] { for i := range lookupTestnet.dists[256] {
bootnodes[i] = wrapNode(lookupTestnet.node(256, i)) bootnodes[i] = lookupTestnet.node(256, i)
} }
fillTable(test.table, bootnodes, true) fillTable(test.table, bootnodes, true)
go serveTestnet(test, lookupTestnet) go serveTestnet(test, lookupTestnet)
@ -115,9 +115,9 @@ func TestUDPv4_LookupIteratorClose(t *testing.T) {
defer test.close() defer test.close()
// Seed table with initial nodes. // Seed table with initial nodes.
bootnodes := make([]*node, len(lookupTestnet.dists[256])) bootnodes := make([]*enode.Node, len(lookupTestnet.dists[256]))
for i := range lookupTestnet.dists[256] { for i := range lookupTestnet.dists[256] {
bootnodes[i] = wrapNode(lookupTestnet.node(256, i)) bootnodes[i] = lookupTestnet.node(256, i)
} }
fillTable(test.table, bootnodes, true) fillTable(test.table, bootnodes, true)
go serveTestnet(test, lookupTestnet) go serveTestnet(test, lookupTestnet)
@ -148,7 +148,7 @@ func TestUDPv4_LookupIteratorClose(t *testing.T) {
func serveTestnet(test *udpTest, testnet *preminedTestnet) { func serveTestnet(test *udpTest, testnet *preminedTestnet) {
for done := false; !done; { for done := false; !done; {
done = test.waitPacketOut(func(p v4wire.Packet, to *net.UDPAddr, hash []byte) { done = test.waitPacketOut(func(p v4wire.Packet, to netip.AddrPort, hash []byte) {
n, key := testnet.nodeByAddr(to) n, key := testnet.nodeByAddr(to)
switch p.(type) { switch p.(type) {
case *v4wire.Ping: case *v4wire.Ping:
@ -171,12 +171,10 @@ func checkLookupResults(t *testing.T, tn *preminedTestnet, results []*enode.Node
for _, e := range results { for _, e := range results {
t.Logf(" ld=%d, %x", enode.LogDist(tn.target.id(), e.ID()), e.ID().Bytes()) t.Logf(" ld=%d, %x", enode.LogDist(tn.target.id(), e.ID()), e.ID().Bytes())
} }
if hasDuplicates(results) {
if hasDuplicates(wrapNodes(results)) {
t.Errorf("result set contains duplicate entries") t.Errorf("result set contains duplicate entries")
} }
if !sortedByDistanceTo(tn.target.id(), results) {
if !sortedByDistanceTo(tn.target.id(), wrapNodes(results)) {
t.Errorf("result set not sorted by distance to target") t.Errorf("result set not sorted by distance to target")
} }
@ -285,9 +283,10 @@ func (tn *preminedTestnet) node(dist, index int) *enode.Node {
return n return n
} }
func (tn *preminedTestnet) nodeByAddr(addr *net.UDPAddr) (*enode.Node, *ecdsa.PrivateKey) { func (tn *preminedTestnet) nodeByAddr(addr netip.AddrPort) (*enode.Node, *ecdsa.PrivateKey) {
dist := int(addr.IP[1])<<8 + int(addr.IP[2]) ip := addr.Addr().As4()
index := int(addr.IP[3]) dist := int(ip[1])<<8 + int(ip[2])
index := int(ip[3])
key := tn.dists[dist][index] key := tn.dists[dist][index]
return tn.node(dist, index), key return tn.node(dist, index), key
@ -296,7 +295,7 @@ func (tn *preminedTestnet) nodeByAddr(addr *net.UDPAddr) (*enode.Node, *ecdsa.Pr
func (tn *preminedTestnet) nodesAtDistance(dist int) []v4wire.Node { func (tn *preminedTestnet) nodesAtDistance(dist int) []v4wire.Node {
result := make([]v4wire.Node, len(tn.dists[dist])) result := make([]v4wire.Node, len(tn.dists[dist]))
for i := range result { for i := range result {
result[i] = nodeToRPC(wrapNode(tn.node(dist, i))) result[i] = nodeToRPC(tn.node(dist, i))
} }
return result return result

View file

@ -26,6 +26,7 @@ import (
"fmt" "fmt"
"io" "io"
"net" "net"
"net/netip"
"sync" "sync"
"time" "time"
@ -45,6 +46,7 @@ var (
errClockWarp = errors.New("reply deadline too far in the future") errClockWarp = errors.New("reply deadline too far in the future")
errClosed = errors.New("socket closed") errClosed = errors.New("socket closed")
errLowPort = errors.New("low port") errLowPort = errors.New("low port")
errNoUDPEndpoint = errors.New("node has no UDP endpoint")
) )
const ( const (
@ -93,7 +95,7 @@ type UDPv4 struct {
type replyMatcher struct { type replyMatcher struct {
// these fields must match in the reply. // these fields must match in the reply.
from enode.ID from enode.ID
ip net.IP ip netip.Addr
ptype byte ptype byte
// time when the request must complete // time when the request must complete
@ -119,7 +121,7 @@ type replyMatchFunc func(v4wire.Packet) (matched bool, requestDone bool)
// reply is a reply packet from a certain node. // reply is a reply packet from a certain node.
type reply struct { type reply struct {
from enode.ID from enode.ID
ip net.IP ip netip.Addr
data v4wire.Packet data v4wire.Packet
// loop indicates whether there was // loop indicates whether there was
// a matching request by sending on this channel. // a matching request by sending on this channel.
@ -142,7 +144,7 @@ func ListenV4(c UDPConn, ln *enode.LocalNode, cfg Config) (*UDPv4, error) {
log: cfg.Log, log: cfg.Log,
} }
tab, err := newMeteredTable(t, ln.Database(), cfg) tab, err := newTable(t, ln.Database(), cfg)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -206,10 +208,12 @@ func (t *UDPv4) Resolve(n *enode.Node) *enode.Node {
} }
func (t *UDPv4) ourEndpoint() v4wire.Endpoint { func (t *UDPv4) ourEndpoint() v4wire.Endpoint {
n := t.Self() node := t.Self()
a := &net.UDPAddr{IP: n.IP(), Port: n.UDP()} addr, ok := node.UDPEndpoint()
if !ok {
return v4wire.NewEndpoint(a, uint16(n.TCP())) return v4wire.Endpoint{}
}
return v4wire.NewEndpoint(addr, uint16(node.TCP()))
} }
// Ping sends a ping message to the given node. // Ping sends a ping message to the given node.
@ -220,7 +224,11 @@ func (t *UDPv4) Ping(n *enode.Node) error {
// ping sends a ping message to the given node and waits for a reply. // ping sends a ping message to the given node and waits for a reply.
func (t *UDPv4) ping(n *enode.Node) (seq uint64, err error) { func (t *UDPv4) ping(n *enode.Node) (seq uint64, err error) {
rm := t.sendPing(n.ID(), &net.UDPAddr{IP: n.IP(), Port: n.UDP()}, nil) addr, ok := n.UDPEndpoint()
if !ok {
return 0, errNoUDPEndpoint
}
rm := t.sendPing(n.ID(), addr, nil)
if err = <-rm.errc; err == nil { if err = <-rm.errc; err == nil {
seq = rm.reply.(*v4wire.Pong).ENRSeq seq = rm.reply.(*v4wire.Pong).ENRSeq
} }
@ -230,7 +238,7 @@ func (t *UDPv4) ping(n *enode.Node) (seq uint64, err error) {
// sendPing sends a ping message to the given node and invokes the callback // sendPing sends a ping message to the given node and invokes the callback
// when the reply arrives. // when the reply arrives.
func (t *UDPv4) sendPing(toid enode.ID, toaddr *net.UDPAddr, callback func()) *replyMatcher { func (t *UDPv4) sendPing(toid enode.ID, toaddr netip.AddrPort, callback func()) *replyMatcher {
req := t.makePing(toaddr) req := t.makePing(toaddr)
packet, hash, err := v4wire.Encode(t.priv, req) packet, hash, err := v4wire.Encode(t.priv, req)
@ -242,7 +250,7 @@ func (t *UDPv4) sendPing(toid enode.ID, toaddr *net.UDPAddr, callback func()) *r
} }
// Add a matcher for the reply to the pending reply queue. Pongs are matched if they // Add a matcher for the reply to the pending reply queue. Pongs are matched if they
// reference the ping we're about to send. // reference the ping we're about to send.
rm := t.pending(toid, toaddr.IP, v4wire.PongPacket, func(p v4wire.Packet) (matched bool, requestDone bool) { rm := t.pending(toid, toaddr.Addr(), v4wire.PongPacket, func(p v4wire.Packet) (matched bool, requestDone bool) {
matched = bytes.Equal(p.(*v4wire.Pong).ReplyTok, hash) matched = bytes.Equal(p.(*v4wire.Pong).ReplyTok, hash)
if matched && callback != nil { if matched && callback != nil {
callback() callback()
@ -251,13 +259,14 @@ func (t *UDPv4) sendPing(toid enode.ID, toaddr *net.UDPAddr, callback func()) *r
return matched, matched return matched, matched
}) })
// Send the packet. // Send the packet.
t.localNode.UDPContact(toaddr) toUDPAddr := &net.UDPAddr{IP: toaddr.Addr().AsSlice()}
t.localNode.UDPContact(toUDPAddr)
t.write(toaddr, toid, req.Name(), packet) t.write(toaddr, toid, req.Name(), packet)
return rm return rm
} }
func (t *UDPv4) makePing(toaddr *net.UDPAddr) *v4wire.Ping { func (t *UDPv4) makePing(toaddr netip.AddrPort) *v4wire.Ping {
return &v4wire.Ping{ return &v4wire.Ping{
Version: 4, Version: 4,
From: t.ourEndpoint(), From: t.ourEndpoint(),
@ -304,8 +313,12 @@ func (t *UDPv4) newRandomLookup(ctx context.Context) *lookup {
func (t *UDPv4) newLookup(ctx context.Context, targetKey encPubkey) *lookup { func (t *UDPv4) newLookup(ctx context.Context, targetKey encPubkey) *lookup {
target := enode.ID(crypto.Keccak256Hash(targetKey[:])) target := enode.ID(crypto.Keccak256Hash(targetKey[:]))
ekey := v4wire.Pubkey(targetKey) ekey := v4wire.Pubkey(targetKey)
it := newLookup(ctx, t.tab, target, func(n *node) ([]*node, error) { it := newLookup(ctx, t.tab, target, func(n *enode.Node) ([]*enode.Node, error) {
return t.findnode(n.ID(), n.addr(), ekey) addr, ok := n.UDPEndpoint()
if !ok {
return nil, errNoUDPEndpoint
}
return t.findnode(n.ID(), addr, ekey)
}) })
return it return it
@ -313,21 +326,20 @@ func (t *UDPv4) newLookup(ctx context.Context, targetKey encPubkey) *lookup {
// findnode sends a findnode request to the given node and waits until // findnode sends a findnode request to the given node and waits until
// the node has sent up to k neighbors. // the node has sent up to k neighbors.
func (t *UDPv4) findnode(toid enode.ID, toaddr *net.UDPAddr, target v4wire.Pubkey) ([]*node, error) { func (t *UDPv4) findnode(toid enode.ID, toAddrPort netip.AddrPort, target v4wire.Pubkey) ([]*enode.Node, error) {
t.ensureBond(toid, toaddr) t.ensureBond(toid, toAddrPort)
// Add a matcher for 'neighbours' replies to the pending reply queue. The matcher is // Add a matcher for 'neighbours' replies to the pending reply queue. The matcher is
// active until enough nodes have been received. // active until enough nodes have been received.
nodes := make([]*node, 0, bucketSize) nodes := make([]*enode.Node, 0, bucketSize)
nreceived := 0 nreceived := 0
rm := t.pending(toid, toaddr.IP, v4wire.NeighborsPacket, func(r v4wire.Packet) (matched bool, requestDone bool) { rm := t.pending(toid, toAddrPort.Addr(), v4wire.NeighborsPacket, func(r v4wire.Packet) (matched bool, requestDone bool) {
reply := r.(*v4wire.Neighbors) reply := r.(*v4wire.Neighbors)
for _, rn := range reply.Nodes { for _, rn := range reply.Nodes {
nreceived++ nreceived++
n, err := t.nodeFromRPC(toAddrPort, rn)
n, err := t.nodeFromRPC(toaddr, rn)
if err != nil { if err != nil {
t.log.Trace("Invalid neighbor node received", "ip", rn.IP, "addr", toaddr, "err", err) t.log.Trace("Invalid neighbor node received", "ip", rn.IP, "addr", toAddrPort, "err", err)
continue continue
} }
@ -336,8 +348,7 @@ func (t *UDPv4) findnode(toid enode.ID, toaddr *net.UDPAddr, target v4wire.Pubke
return true, nreceived >= bucketSize return true, nreceived >= bucketSize
}) })
t.send(toAddrPort, toid, &v4wire.Findnode{
t.send(toaddr, toid, &v4wire.Findnode{
Target: target, Target: target,
Expiration: uint64(time.Now().Add(expiration).Unix()), Expiration: uint64(time.Now().Add(expiration).Unix()),
}) })
@ -356,7 +367,7 @@ func (t *UDPv4) findnode(toid enode.ID, toaddr *net.UDPAddr, target v4wire.Pubke
// RequestENR sends ENRRequest to the given node and waits for a response. // RequestENR sends ENRRequest to the given node and waits for a response.
func (t *UDPv4) RequestENR(n *enode.Node) (*enode.Node, error) { func (t *UDPv4) RequestENR(n *enode.Node) (*enode.Node, error) {
addr := &net.UDPAddr{IP: n.IP(), Port: n.UDP()} addr, _ := n.UDPEndpoint()
t.ensureBond(n.ID(), addr) t.ensureBond(n.ID(), addr)
req := &v4wire.ENRRequest{ req := &v4wire.ENRRequest{
@ -370,7 +381,7 @@ func (t *UDPv4) RequestENR(n *enode.Node) (*enode.Node, error) {
// Add a matcher for the reply to the pending reply queue. Responses are matched if // Add a matcher for the reply to the pending reply queue. Responses are matched if
// they reference the request we're about to send. // they reference the request we're about to send.
rm := t.pending(n.ID(), addr.IP, v4wire.ENRResponsePacket, func(r v4wire.Packet) (matched bool, requestDone bool) { rm := t.pending(n.ID(), addr.Addr(), v4wire.ENRResponsePacket, func(r v4wire.Packet) (matched bool, requestDone bool) {
matched = bytes.Equal(r.(*v4wire.ENRResponse).ReplyTok, hash) matched = bytes.Equal(r.(*v4wire.ENRResponse).ReplyTok, hash)
return matched, matched return matched, matched
}) })
@ -393,17 +404,20 @@ func (t *UDPv4) RequestENR(n *enode.Node) (*enode.Node, error) {
if respN.Seq() < n.Seq() { if respN.Seq() < n.Seq() {
return n, nil // response record is older return n, nil // response record is older
} }
if err := netutil.CheckRelayIP(addr.Addr().AsSlice(), respN.IP()); err != nil {
if err := netutil.CheckRelayIP(addr.IP, respN.IP()); err != nil {
return nil, fmt.Errorf("invalid IP in response record: %v", err) return nil, fmt.Errorf("invalid IP in response record: %v", err)
} }
return respN, nil return respN, nil
} }
func (t *UDPv4) TableBuckets() [][]BucketNode {
return t.tab.Nodes()
}
// pending adds a reply matcher to the pending reply queue. // pending adds a reply matcher to the pending reply queue.
// see the documentation of type replyMatcher for a detailed explanation. // see the documentation of type replyMatcher for a detailed explanation.
func (t *UDPv4) pending(id enode.ID, ip net.IP, ptype byte, callback replyMatchFunc) *replyMatcher { func (t *UDPv4) pending(id enode.ID, ip netip.Addr, ptype byte, callback replyMatchFunc) *replyMatcher {
ch := make(chan error, 1) ch := make(chan error, 1)
p := &replyMatcher{from: id, ip: ip, ptype: ptype, callback: callback, errc: ch} p := &replyMatcher{from: id, ip: ip, ptype: ptype, callback: callback, errc: ch}
select { select {
@ -418,7 +432,7 @@ func (t *UDPv4) pending(id enode.ID, ip net.IP, ptype byte, callback replyMatchF
// handleReply dispatches a reply packet, invoking reply matchers. It returns // handleReply dispatches a reply packet, invoking reply matchers. It returns
// whether any matcher considered the packet acceptable. // whether any matcher considered the packet acceptable.
func (t *UDPv4) handleReply(from enode.ID, fromIP net.IP, req v4wire.Packet) bool { func (t *UDPv4) handleReply(from enode.ID, fromIP netip.Addr, req v4wire.Packet) bool {
matched := make(chan bool, 1) matched := make(chan bool, 1)
select { select {
case t.gotreply <- reply{from, fromIP, req, matched}: case t.gotreply <- reply{from, fromIP, req, matched}:
@ -491,7 +505,7 @@ func (t *UDPv4) loop() {
for el := plist.Front(); el != nil; el = el.Next() { for el := plist.Front(); el != nil; el = el.Next() {
p := el.Value.(*replyMatcher) p := el.Value.(*replyMatcher)
if p.from == r.from && p.ptype == r.data.Kind() && p.ip.Equal(r.ip) { if p.from == r.from && p.ptype == r.data.Kind() && p.ip == r.ip {
ok, requestDone := p.callback(r.data) ok, requestDone := p.callback(r.data)
matched = matched || ok matched = matched || ok
p.reply = r.data p.reply = r.data
@ -535,7 +549,7 @@ func (t *UDPv4) loop() {
} }
} }
func (t *UDPv4) send(toaddr *net.UDPAddr, toid enode.ID, req v4wire.Packet) ([]byte, error) { func (t *UDPv4) send(toaddr netip.AddrPort, toid enode.ID, req v4wire.Packet) ([]byte, error) {
packet, hash, err := v4wire.Encode(t.priv, req) packet, hash, err := v4wire.Encode(t.priv, req)
if err != nil { if err != nil {
return hash, err return hash, err
@ -544,8 +558,8 @@ func (t *UDPv4) send(toaddr *net.UDPAddr, toid enode.ID, req v4wire.Packet) ([]b
return hash, t.write(toaddr, toid, req.Name(), packet) return hash, t.write(toaddr, toid, req.Name(), packet)
} }
func (t *UDPv4) write(toaddr *net.UDPAddr, toid enode.ID, what string, packet []byte) error { func (t *UDPv4) write(toaddr netip.AddrPort, toid enode.ID, what string, packet []byte) error {
_, err := t.conn.WriteToUDP(packet, toaddr) _, err := t.conn.WriteToUDPAddrPort(packet, toaddr)
t.log.Trace(">> "+what, "id", toid, "addr", toaddr, "err", err) t.log.Trace(">> "+what, "id", toid, "addr", toaddr, "err", err)
return err return err
@ -562,7 +576,7 @@ func (t *UDPv4) readLoop(unhandled chan<- ReadPacket) {
buf := make([]byte, maxPacketSize) buf := make([]byte, maxPacketSize)
for { for {
nbytes, from, err := t.conn.ReadFromUDP(buf) nbytes, from, err := t.conn.ReadFromUDPAddrPort(buf)
if netutil.IsTemporaryError(err) { if netutil.IsTemporaryError(err) {
// Ignore temporary read errors. // Ignore temporary read errors.
t.log.Debug("Temporary UDP read error", "err", err) t.log.Debug("Temporary UDP read error", "err", err)
@ -585,7 +599,7 @@ func (t *UDPv4) readLoop(unhandled chan<- ReadPacket) {
} }
} }
func (t *UDPv4) handlePacket(from *net.UDPAddr, buf []byte) error { func (t *UDPv4) handlePacket(from netip.AddrPort, buf []byte) error {
rawpacket, fromKey, hash, err := v4wire.Decode(buf) rawpacket, fromKey, hash, err := v4wire.Decode(buf)
if err != nil { if err != nil {
t.log.Debug("Bad discv4 packet", "addr", from, "err", err) t.log.Debug("Bad discv4 packet", "addr", from, "err", err)
@ -609,15 +623,16 @@ func (t *UDPv4) handlePacket(from *net.UDPAddr, buf []byte) error {
} }
// checkBond checks if the given node has a recent enough endpoint proof. // checkBond checks if the given node has a recent enough endpoint proof.
func (t *UDPv4) checkBond(id enode.ID, ip net.IP) bool { func (t *UDPv4) checkBond(id enode.ID, ip netip.AddrPort) bool {
return time.Since(t.db.LastPongReceived(id, ip)) < bondExpiration return time.Since(t.db.LastPongReceived(id, ip.Addr().AsSlice())) < bondExpiration
} }
// ensureBond solicits a ping from a node if we haven't seen a ping from it for a while. // ensureBond solicits a ping from a node if we haven't seen a ping from it for a while.
// This ensures there is a valid endpoint proof on the remote end. // This ensures there is a valid endpoint proof on the remote end.
func (t *UDPv4) ensureBond(toid enode.ID, toaddr *net.UDPAddr) { func (t *UDPv4) ensureBond(toid enode.ID, toaddr netip.AddrPort) {
tooOld := time.Since(t.db.LastPingReceived(toid, toaddr.IP)) > bondExpiration ip := toaddr.Addr().AsSlice()
if tooOld || t.db.FindFails(toid, toaddr.IP) > maxFindnodeFailures { tooOld := time.Since(t.db.LastPingReceived(toid, ip)) > bondExpiration
if tooOld || t.db.FindFails(toid, ip) > maxFindnodeFailures {
rm := t.sendPing(toid, toaddr, nil) rm := t.sendPing(toid, toaddr, nil)
<-rm.errc <-rm.errc
// Wait for them to ping back and process our pong. // Wait for them to ping back and process our pong.
@ -625,12 +640,11 @@ func (t *UDPv4) ensureBond(toid enode.ID, toaddr *net.UDPAddr) {
} }
} }
func (t *UDPv4) nodeFromRPC(sender *net.UDPAddr, rn v4wire.Node) (*node, error) { func (t *UDPv4) nodeFromRPC(sender netip.AddrPort, rn v4wire.Node) (*enode.Node, error) {
if rn.UDP <= 1024 { if rn.UDP <= 1024 {
return nil, errLowPort return nil, errLowPort
} }
if err := netutil.CheckRelayIP(sender.Addr().AsSlice(), rn.IP); err != nil {
if err := netutil.CheckRelayIP(sender.IP, rn.IP); err != nil {
return nil, err return nil, err
} }
@ -642,14 +656,13 @@ func (t *UDPv4) nodeFromRPC(sender *net.UDPAddr, rn v4wire.Node) (*node, error)
if err != nil { if err != nil {
return nil, err return nil, err
} }
n := enode.NewV4(key, rn.IP, int(rn.TCP), int(rn.UDP))
n := wrapNode(enode.NewV4(key, rn.IP, int(rn.TCP), int(rn.UDP)))
err = n.ValidateComplete() err = n.ValidateComplete()
return n, err return n, err
} }
func nodeToRPC(n *node) v4wire.Node { func nodeToRPC(n *enode.Node) v4wire.Node {
var key ecdsa.PublicKey var key ecdsa.PublicKey
var ekey v4wire.Pubkey var ekey v4wire.Pubkey
@ -692,14 +705,14 @@ type packetHandlerV4 struct {
senderKey *ecdsa.PublicKey // used for ping senderKey *ecdsa.PublicKey // used for ping
// preverify checks whether the packet is valid and should be handled at all. // preverify checks whether the packet is valid and should be handled at all.
preverify func(p *packetHandlerV4, from *net.UDPAddr, fromID enode.ID, fromKey v4wire.Pubkey) error preverify func(p *packetHandlerV4, from netip.AddrPort, fromID enode.ID, fromKey v4wire.Pubkey) error
// handle handles the packet. // handle handles the packet.
handle func(req *packetHandlerV4, from *net.UDPAddr, fromID enode.ID, mac []byte) handle func(req *packetHandlerV4, from netip.AddrPort, fromID enode.ID, mac []byte)
} }
// PING/v4 // PING/v4
func (t *UDPv4) verifyPing(h *packetHandlerV4, from *net.UDPAddr, fromID enode.ID, fromKey v4wire.Pubkey) error { func (t *UDPv4) verifyPing(h *packetHandlerV4, from netip.AddrPort, fromID enode.ID, fromKey v4wire.Pubkey) error {
req := h.Packet.(*v4wire.Ping) req := h.Packet.(*v4wire.Ping)
if v4wire.Expired(req.Expiration) { if v4wire.Expired(req.Expiration) {
@ -714,7 +727,7 @@ func (t *UDPv4) verifyPing(h *packetHandlerV4, from *net.UDPAddr, fromID enode.I
return nil return nil
} }
func (t *UDPv4) handlePing(h *packetHandlerV4, from *net.UDPAddr, fromID enode.ID, mac []byte) { func (t *UDPv4) handlePing(h *packetHandlerV4, from netip.AddrPort, fromID enode.ID, mac []byte) {
req := h.Packet.(*v4wire.Ping) req := h.Packet.(*v4wire.Ping)
// Reply. // Reply.
@ -726,49 +739,51 @@ func (t *UDPv4) handlePing(h *packetHandlerV4, from *net.UDPAddr, fromID enode.I
}) })
// Ping back if our last pong on file is too far in the past. // Ping back if our last pong on file is too far in the past.
n := wrapNode(enode.NewV4(h.senderKey, from.IP, int(req.From.TCP), from.Port)) fromIP := from.Addr().AsSlice()
if time.Since(t.db.LastPongReceived(n.ID(), from.IP)) > bondExpiration { n := enode.NewV4(h.senderKey, fromIP, int(req.From.TCP), int(from.Port()))
if time.Since(t.db.LastPongReceived(n.ID(), fromIP)) > bondExpiration {
t.sendPing(fromID, from, func() { t.sendPing(fromID, from, func() {
t.tab.addVerifiedNode(n) t.tab.addInboundNode(n)
}) })
} else { } else {
t.tab.addVerifiedNode(n) t.tab.addInboundNode(n)
} }
// Update node database and endpoint predictor. // Update node database and endpoint predictor.
t.db.UpdateLastPingReceived(n.ID(), from.IP, time.Now()) t.db.UpdateLastPingReceived(n.ID(), fromIP, time.Now())
t.localNode.UDPEndpointStatement(from, &net.UDPAddr{IP: req.To.IP, Port: int(req.To.UDP)}) fromUDPAddr := &net.UDPAddr{IP: fromIP, Port: int(from.Port())}
toUDPAddr := &net.UDPAddr{IP: req.To.IP, Port: int(req.To.UDP)}
t.localNode.UDPEndpointStatement(fromUDPAddr, toUDPAddr)
} }
// PONG/v4 // PONG/v4
func (t *UDPv4) verifyPong(h *packetHandlerV4, from *net.UDPAddr, fromID enode.ID, fromKey v4wire.Pubkey) error { func (t *UDPv4) verifyPong(h *packetHandlerV4, from netip.AddrPort, fromID enode.ID, fromKey v4wire.Pubkey) error {
req := h.Packet.(*v4wire.Pong) req := h.Packet.(*v4wire.Pong)
if v4wire.Expired(req.Expiration) { if v4wire.Expired(req.Expiration) {
return errExpired return errExpired
} }
if !t.handleReply(fromID, from.Addr(), req) {
if !t.handleReply(fromID, from.IP, req) {
return errUnsolicitedReply return errUnsolicitedReply
} }
fromIP := from.Addr().AsSlice()
t.localNode.UDPEndpointStatement(from, &net.UDPAddr{IP: req.To.IP, Port: int(req.To.UDP)}) fromUDPAddr := &net.UDPAddr{IP: fromIP, Port: int(from.Port())}
t.db.UpdateLastPongReceived(fromID, from.IP, time.Now()) toUDPAddr := &net.UDPAddr{IP: req.To.IP, Port: int(req.To.UDP)}
t.localNode.UDPEndpointStatement(fromUDPAddr, toUDPAddr)
t.db.UpdateLastPongReceived(fromID, fromIP, time.Now())
return nil return nil
} }
// FINDNODE/v4 // FINDNODE/v4
func (t *UDPv4) verifyFindnode(h *packetHandlerV4, from *net.UDPAddr, fromID enode.ID, fromKey v4wire.Pubkey) error { func (t *UDPv4) verifyFindnode(h *packetHandlerV4, from netip.AddrPort, fromID enode.ID, fromKey v4wire.Pubkey) error {
req := h.Packet.(*v4wire.Findnode) req := h.Packet.(*v4wire.Findnode)
if v4wire.Expired(req.Expiration) { if v4wire.Expired(req.Expiration) {
return errExpired return errExpired
} }
if !t.checkBond(fromID, from) {
if !t.checkBond(fromID, from.IP) {
// No endpoint proof pong exists, we don't process the packet. This prevents an // No endpoint proof pong exists, we don't process the packet. This prevents an
// attack vector where the discovery protocol could be used to amplify traffic in a // attack vector where the discovery protocol could be used to amplify traffic in a
// DDOS attack. A malicious actor would send a findnode request with the IP address // DDOS attack. A malicious actor would send a findnode request with the IP address
@ -781,7 +796,7 @@ func (t *UDPv4) verifyFindnode(h *packetHandlerV4, from *net.UDPAddr, fromID eno
return nil return nil
} }
func (t *UDPv4) handleFindnode(h *packetHandlerV4, from *net.UDPAddr, fromID enode.ID, mac []byte) { func (t *UDPv4) handleFindnode(h *packetHandlerV4, from netip.AddrPort, fromID enode.ID, mac []byte) {
req := h.Packet.(*v4wire.Findnode) req := h.Packet.(*v4wire.Findnode)
// Determine closest nodes. // Determine closest nodes.
@ -795,7 +810,8 @@ func (t *UDPv4) handleFindnode(h *packetHandlerV4, from *net.UDPAddr, fromID eno
var sent bool var sent bool
for _, n := range closest { for _, n := range closest {
if netutil.CheckRelayIP(from.IP, n.IP()) == nil { fromIP := from.Addr().AsSlice()
if netutil.CheckRelayIP(fromIP, n.IP()) == nil {
p.Nodes = append(p.Nodes, nodeToRPC(n)) p.Nodes = append(p.Nodes, nodeToRPC(n))
} }
@ -813,14 +829,13 @@ func (t *UDPv4) handleFindnode(h *packetHandlerV4, from *net.UDPAddr, fromID eno
// NEIGHBORS/v4 // NEIGHBORS/v4
func (t *UDPv4) verifyNeighbors(h *packetHandlerV4, from *net.UDPAddr, fromID enode.ID, fromKey v4wire.Pubkey) error { func (t *UDPv4) verifyNeighbors(h *packetHandlerV4, from netip.AddrPort, fromID enode.ID, fromKey v4wire.Pubkey) error {
req := h.Packet.(*v4wire.Neighbors) req := h.Packet.(*v4wire.Neighbors)
if v4wire.Expired(req.Expiration) { if v4wire.Expired(req.Expiration) {
return errExpired return errExpired
} }
if !t.handleReply(fromID, from.Addr(), h.Packet) {
if !t.handleReply(fromID, from.IP, h.Packet) {
return errUnsolicitedReply return errUnsolicitedReply
} }
@ -829,21 +844,20 @@ func (t *UDPv4) verifyNeighbors(h *packetHandlerV4, from *net.UDPAddr, fromID en
// ENRREQUEST/v4 // ENRREQUEST/v4
func (t *UDPv4) verifyENRRequest(h *packetHandlerV4, from *net.UDPAddr, fromID enode.ID, fromKey v4wire.Pubkey) error { func (t *UDPv4) verifyENRRequest(h *packetHandlerV4, from netip.AddrPort, fromID enode.ID, fromKey v4wire.Pubkey) error {
req := h.Packet.(*v4wire.ENRRequest) req := h.Packet.(*v4wire.ENRRequest)
if v4wire.Expired(req.Expiration) { if v4wire.Expired(req.Expiration) {
return errExpired return errExpired
} }
if !t.checkBond(fromID, from) {
if !t.checkBond(fromID, from.IP) {
return errUnknownNode return errUnknownNode
} }
return nil return nil
} }
func (t *UDPv4) handleENRRequest(h *packetHandlerV4, from *net.UDPAddr, fromID enode.ID, mac []byte) { func (t *UDPv4) handleENRRequest(h *packetHandlerV4, from netip.AddrPort, fromID enode.ID, mac []byte) {
t.send(from, fromID, &v4wire.ENRResponse{ t.send(from, fromID, &v4wire.ENRResponse{
ReplyTok: mac, ReplyTok: mac,
Record: *t.localNode.Node().Record(), Record: *t.localNode.Node().Record(),
@ -852,8 +866,8 @@ func (t *UDPv4) handleENRRequest(h *packetHandlerV4, from *net.UDPAddr, fromID e
// ENRRESPONSE/v4 // ENRRESPONSE/v4
func (t *UDPv4) verifyENRResponse(h *packetHandlerV4, from *net.UDPAddr, fromID enode.ID, fromKey v4wire.Pubkey) error { func (t *UDPv4) verifyENRResponse(h *packetHandlerV4, from netip.AddrPort, fromID enode.ID, fromKey v4wire.Pubkey) error {
if !t.handleReply(fromID, from.IP, h.Packet) { if !t.handleReply(fromID, from.Addr(), h.Packet) {
return errUnsolicitedReply return errUnsolicitedReply
} }

View file

@ -26,6 +26,7 @@ import (
"io" "io"
"math/rand" "math/rand"
"net" "net"
"net/netip"
"reflect" "reflect"
"sync" "sync"
"testing" "testing"
@ -55,7 +56,7 @@ type udpTest struct {
udp *UDPv4 udp *UDPv4
sent [][]byte sent [][]byte
localkey, remotekey *ecdsa.PrivateKey localkey, remotekey *ecdsa.PrivateKey
remoteaddr *net.UDPAddr remoteaddr netip.AddrPort
} }
func newUDPTest(t *testing.T) *udpTest { func newUDPTest(t *testing.T) *udpTest {
@ -64,7 +65,7 @@ func newUDPTest(t *testing.T) *udpTest {
pipe: newpipe(), pipe: newpipe(),
localkey: newkey(), localkey: newkey(),
remotekey: newkey(), remotekey: newkey(),
remoteaddr: &net.UDPAddr{IP: net.IP{10, 0, 1, 99}, Port: 30303}, remoteaddr: netip.MustParseAddrPort("10.0.1.99:30303"),
} }
test.db, _ = enode.OpenDB("") test.db, _ = enode.OpenDB("")
@ -93,7 +94,7 @@ func (test *udpTest) packetIn(wantError error, data v4wire.Packet) {
} }
// handles a packet as if it had been sent to the transport by the key/endpoint. // handles a packet as if it had been sent to the transport by the key/endpoint.
func (test *udpTest) packetInFrom(wantError error, key *ecdsa.PrivateKey, addr *net.UDPAddr, data v4wire.Packet) { func (test *udpTest) packetInFrom(wantError error, key *ecdsa.PrivateKey, addr netip.AddrPort, data v4wire.Packet) {
test.t.Helper() test.t.Helper()
enc, _, err := v4wire.Encode(key, data) enc, _, err := v4wire.Encode(key, data)
@ -108,7 +109,7 @@ func (test *udpTest) packetInFrom(wantError error, key *ecdsa.PrivateKey, addr *
} }
// waits for a packet to be sent by the transport. // waits for a packet to be sent by the transport.
// validate should have type func(X, *net.UDPAddr, []byte), where X is a packet type. // validate should have type func(X, netip.AddrPort, []byte), where X is a packet type.
func (test *udpTest) waitPacketOut(validate interface{}) (closed bool) { func (test *udpTest) waitPacketOut(validate interface{}) (closed bool) {
test.t.Helper() test.t.Helper()
@ -133,9 +134,7 @@ func (test *udpTest) waitPacketOut(validate interface{}) (closed bool) {
test.t.Errorf("sent packet type mismatch, got: %v, want: %v", reflect.TypeOf(p), exptype) test.t.Errorf("sent packet type mismatch, got: %v, want: %v", reflect.TypeOf(p), exptype)
return false return false
} }
fn.Call([]reflect.Value{reflect.ValueOf(p), reflect.ValueOf(dgram.to), reflect.ValueOf(hash)})
fn.Call([]reflect.Value{reflect.ValueOf(p), reflect.ValueOf(&dgram.to), reflect.ValueOf(hash)})
return false return false
} }
@ -257,7 +256,7 @@ func TestUDPv4_findnodeTimeout(t *testing.T) {
test := newUDPTest(t) test := newUDPTest(t)
defer test.close() defer test.close()
toaddr := &net.UDPAddr{IP: net.ParseIP("1.2.3.4"), Port: 2222} toaddr := netip.AddrPortFrom(netip.MustParseAddr("1.2.3.4"), 2222)
toid := enode.ID{1, 2, 3, 4} toid := enode.ID{1, 2, 3, 4}
target := v4wire.Pubkey{4, 5, 6, 7} target := v4wire.Pubkey{4, 5, 6, 7}
@ -285,28 +284,25 @@ func TestUDPv4_findnode(t *testing.T) {
for i := 0; i < numCandidates; i++ { for i := 0; i < numCandidates; i++ {
key := newkey() key := newkey()
ip := net.IP{10, 13, 0, byte(i)} ip := net.IP{10, 13, 0, byte(i)}
n := wrapNode(enode.NewV4(&key.PublicKey, ip, 0, 2000)) n := enode.NewV4(&key.PublicKey, ip, 0, 2000)
// Ensure half of table content isn't verified live yet. // Ensure half of table content isn't verified live yet.
if i > numCandidates/2 { if i > numCandidates/2 {
n.livenessChecks = 1
live[n.ID()] = true live[n.ID()] = true
} }
test.table.addFoundNode(n, live[n.ID()])
nodes.push(n, numCandidates) nodes.push(n, numCandidates)
} }
fillTable(test.table, nodes.entries, false)
// ensure there's a bond with the test node, // ensure there's a bond with the test node,
// findnode won't be accepted otherwise. // findnode won't be accepted otherwise.
remoteID := v4wire.EncodePubkey(&test.remotekey.PublicKey).ID() remoteID := v4wire.EncodePubkey(&test.remotekey.PublicKey).ID()
test.table.db.UpdateLastPongReceived(remoteID, test.remoteaddr.IP, time.Now()) test.table.db.UpdateLastPongReceived(remoteID, test.remoteaddr.Addr().AsSlice(), time.Now())
// check that closest neighbors are returned. // check that closest neighbors are returned.
expected := test.table.findnodeByID(testTarget.ID(), bucketSize, true) expected := test.table.findnodeByID(testTarget.ID(), bucketSize, true)
test.packetIn(nil, &v4wire.Findnode{Target: testTarget, Expiration: futureExp}) test.packetIn(nil, &v4wire.Findnode{Target: testTarget, Expiration: futureExp})
waitNeighbors := func(want []*enode.Node) {
waitNeighbors := func(want []*node) { test.waitPacketOut(func(p *v4wire.Neighbors, to netip.AddrPort, hash []byte) {
test.waitPacketOut(func(p *v4wire.Neighbors, to *net.UDPAddr, hash []byte) {
if len(p.Nodes) != len(want) { if len(p.Nodes) != len(want) {
t.Errorf("wrong number of results: got %d, want %d", len(p.Nodes), bucketSize) t.Errorf("wrong number of results: got %d, want %d", len(p.Nodes), bucketSize)
return return
@ -338,11 +334,10 @@ func TestUDPv4_findnodeMultiReply(t *testing.T) {
defer test.close() defer test.close()
rid := enode.PubkeyToIDV4(&test.remotekey.PublicKey) rid := enode.PubkeyToIDV4(&test.remotekey.PublicKey)
test.table.db.UpdateLastPingReceived(rid, test.remoteaddr.IP, time.Now()) test.table.db.UpdateLastPingReceived(rid, test.remoteaddr.Addr().AsSlice(), time.Now())
// queue a pending findnode request // queue a pending findnode request
resultc, errc := make(chan []*node, 1), make(chan error, 1) resultc, errc := make(chan []*enode.Node, 1), make(chan error, 1)
go func() { go func() {
rid := encodePubkey(&test.remotekey.PublicKey).id() rid := encodePubkey(&test.remotekey.PublicKey).id()
@ -356,18 +351,18 @@ func TestUDPv4_findnodeMultiReply(t *testing.T) {
// wait for the findnode to be sent. // wait for the findnode to be sent.
// after it is sent, the transport is waiting for a reply // after it is sent, the transport is waiting for a reply
test.waitPacketOut(func(p *v4wire.Findnode, to *net.UDPAddr, hash []byte) { test.waitPacketOut(func(p *v4wire.Findnode, to netip.AddrPort, hash []byte) {
if p.Target != testTarget { if p.Target != testTarget {
t.Errorf("wrong target: got %v, want %v", p.Target, testTarget) t.Errorf("wrong target: got %v, want %v", p.Target, testTarget)
} }
}) })
// send the reply as two packets. // send the reply as two packets.
list := []*node{ list := []*enode.Node{
wrapNode(enode.MustParse("enode://ba85011c70bcc5c04d8607d3a0ed29aa6179c092cbdda10d5d32684fb33ed01bd94f588ca8f91ac48318087dcb02eaf36773a7a453f0eedd6742af668097b29c@10.0.1.16:30303?discport=30304")), enode.MustParse("enode://ba85011c70bcc5c04d8607d3a0ed29aa6179c092cbdda10d5d32684fb33ed01bd94f588ca8f91ac48318087dcb02eaf36773a7a453f0eedd6742af668097b29c@10.0.1.16:30303?discport=30304"),
wrapNode(enode.MustParse("enode://81fa361d25f157cd421c60dcc28d8dac5ef6a89476633339c5df30287474520caca09627da18543d9079b5b288698b542d56167aa5c09111e55acdbbdf2ef799@10.0.1.16:30303")), enode.MustParse("enode://81fa361d25f157cd421c60dcc28d8dac5ef6a89476633339c5df30287474520caca09627da18543d9079b5b288698b542d56167aa5c09111e55acdbbdf2ef799@10.0.1.16:30303"),
wrapNode(enode.MustParse("enode://9bffefd833d53fac8e652415f4973bee289e8b1a5c6c4cbe70abf817ce8a64cee11b823b66a987f51aaa9fba0d6a91b3e6bf0d5a5d1042de8e9eeea057b217f8@10.0.1.36:30301?discport=17")), enode.MustParse("enode://9bffefd833d53fac8e652415f4973bee289e8b1a5c6c4cbe70abf817ce8a64cee11b823b66a987f51aaa9fba0d6a91b3e6bf0d5a5d1042de8e9eeea057b217f8@10.0.1.36:30301?discport=17"),
wrapNode(enode.MustParse("enode://1b5b4aa662d7cb44a7221bfba67302590b643028197a7d5214790f3bac7aaa4a3241be9e83c09cf1f6c69d007c634faae3dc1b1221793e8446c0b3a09de65960@10.0.1.16:30303")), enode.MustParse("enode://1b5b4aa662d7cb44a7221bfba67302590b643028197a7d5214790f3bac7aaa4a3241be9e83c09cf1f6c69d007c634faae3dc1b1221793e8446c0b3a09de65960@10.0.1.16:30303"),
} }
rpclist := make([]v4wire.Node, len(list)) rpclist := make([]v4wire.Node, len(list))
@ -401,8 +396,8 @@ func TestUDPv4_pingMatch(t *testing.T) {
crand.Read(randToken) crand.Read(randToken)
test.packetIn(nil, &v4wire.Ping{From: testRemote, To: testLocalAnnounced, Version: 4, Expiration: futureExp}) test.packetIn(nil, &v4wire.Ping{From: testRemote, To: testLocalAnnounced, Version: 4, Expiration: futureExp})
test.waitPacketOut(func(*v4wire.Pong, *net.UDPAddr, []byte) {}) test.waitPacketOut(func(*v4wire.Pong, netip.AddrPort, []byte) {})
test.waitPacketOut(func(*v4wire.Ping, *net.UDPAddr, []byte) {}) test.waitPacketOut(func(*v4wire.Ping, netip.AddrPort, []byte) {})
test.packetIn(errUnsolicitedReply, &v4wire.Pong{ReplyTok: randToken, To: testLocalAnnounced, Expiration: futureExp}) test.packetIn(errUnsolicitedReply, &v4wire.Pong{ReplyTok: randToken, To: testLocalAnnounced, Expiration: futureExp})
} }
@ -412,10 +407,10 @@ func TestUDPv4_pingMatchIP(t *testing.T) {
defer test.close() defer test.close()
test.packetIn(nil, &v4wire.Ping{From: testRemote, To: testLocalAnnounced, Version: 4, Expiration: futureExp}) test.packetIn(nil, &v4wire.Ping{From: testRemote, To: testLocalAnnounced, Version: 4, Expiration: futureExp})
test.waitPacketOut(func(*v4wire.Pong, *net.UDPAddr, []byte) {}) test.waitPacketOut(func(*v4wire.Pong, netip.AddrPort, []byte) {})
test.waitPacketOut(func(p *v4wire.Ping, to *net.UDPAddr, hash []byte) { test.waitPacketOut(func(p *v4wire.Ping, to netip.AddrPort, hash []byte) {
wrongAddr := &net.UDPAddr{IP: net.IP{33, 44, 1, 2}, Port: 30000} wrongAddr := netip.MustParseAddrPort("33.44.1.2:30000")
test.packetInFrom(errUnsolicitedReply, test.remotekey, wrongAddr, &v4wire.Pong{ test.packetInFrom(errUnsolicitedReply, test.remotekey, wrongAddr, &v4wire.Pong{
ReplyTok: hash, ReplyTok: hash,
To: testLocalAnnounced, To: testLocalAnnounced,
@ -426,43 +421,36 @@ func TestUDPv4_pingMatchIP(t *testing.T) {
func TestUDPv4_successfulPing(t *testing.T) { func TestUDPv4_successfulPing(t *testing.T) {
test := newUDPTest(t) test := newUDPTest(t)
added := make(chan *node, 1) added := make(chan *tableNode, 1)
test.table.nodeAddedHook = func(b *bucket, n *node) { added <- n } test.table.nodeAddedHook = func(b *bucket, n *tableNode) { added <- n }
defer test.close() defer test.close()
// The remote side sends a ping packet to initiate the exchange. // The remote side sends a ping packet to initiate the exchange.
go test.packetIn(nil, &v4wire.Ping{From: testRemote, To: testLocalAnnounced, Version: 4, Expiration: futureExp}) go test.packetIn(nil, &v4wire.Ping{From: testRemote, To: testLocalAnnounced, Version: 4, Expiration: futureExp})
// The ping is replied to. // The ping is replied to.
test.waitPacketOut(func(p *v4wire.Pong, to *net.UDPAddr, hash []byte) { test.waitPacketOut(func(p *v4wire.Pong, to netip.AddrPort, hash []byte) {
pinghash := test.sent[0][:32] pinghash := test.sent[0][:32]
if !bytes.Equal(p.ReplyTok, pinghash) { if !bytes.Equal(p.ReplyTok, pinghash) {
t.Errorf("got pong.ReplyTok %x, want %x", p.ReplyTok, pinghash) t.Errorf("got pong.ReplyTok %x, want %x", p.ReplyTok, pinghash)
} }
// The mirrored UDP address is the UDP packet sender.
wantTo := v4wire.Endpoint{ // The mirrored TCP port is the one from the ping packet.
// The mirrored UDP address is the UDP packet sender wantTo := v4wire.NewEndpoint(test.remoteaddr, testRemote.TCP)
IP: test.remoteaddr.IP, UDP: uint16(test.remoteaddr.Port),
// The mirrored TCP port is the one from the ping packet
TCP: testRemote.TCP,
}
if !reflect.DeepEqual(p.To, wantTo) { if !reflect.DeepEqual(p.To, wantTo) {
t.Errorf("got pong.To %v, want %v", p.To, wantTo) t.Errorf("got pong.To %v, want %v", p.To, wantTo)
} }
}) })
// Remote is unknown, the table pings back. // Remote is unknown, the table pings back.
test.waitPacketOut(func(p *v4wire.Ping, to *net.UDPAddr, hash []byte) { test.waitPacketOut(func(p *v4wire.Ping, to netip.AddrPort, hash []byte) {
if !reflect.DeepEqual(p.From, test.udp.ourEndpoint()) { wantFrom := test.udp.ourEndpoint()
wantFrom.IP = net.IP{}
if !reflect.DeepEqual(p.From, wantFrom) {
t.Errorf("got ping.From %#v, want %#v", p.From, test.udp.ourEndpoint()) t.Errorf("got ping.From %#v, want %#v", p.From, test.udp.ourEndpoint())
} }
wantTo := v4wire.Endpoint{
// The mirrored UDP address is the UDP packet sender. // The mirrored UDP address is the UDP packet sender.
IP: test.remoteaddr.IP, wantTo := v4wire.NewEndpoint(test.remoteaddr, 0)
UDP: uint16(test.remoteaddr.Port),
TCP: 0,
}
if !reflect.DeepEqual(p.To, wantTo) { if !reflect.DeepEqual(p.To, wantTo) {
t.Errorf("got ping.To %v, want %v", p.To, wantTo) t.Errorf("got ping.To %v, want %v", p.To, wantTo)
} }
@ -478,13 +466,11 @@ func TestUDPv4_successfulPing(t *testing.T) {
if n.ID() != rid { if n.ID() != rid {
t.Errorf("node has wrong ID: got %v, want %v", n.ID(), rid) t.Errorf("node has wrong ID: got %v, want %v", n.ID(), rid)
} }
if !n.IP().Equal(test.remoteaddr.Addr().AsSlice()) {
if !n.IP().Equal(test.remoteaddr.IP) { t.Errorf("node has wrong IP: got %v, want: %v", n.IP(), test.remoteaddr.Addr())
t.Errorf("node has wrong IP: got %v, want: %v", n.IP(), test.remoteaddr.IP)
} }
if n.UDP() != int(test.remoteaddr.Port()) {
if n.UDP() != test.remoteaddr.Port { t.Errorf("node has wrong UDP port: got %v, want: %v", n.UDP(), test.remoteaddr.Port())
t.Errorf("node has wrong UDP port: got %v, want: %v", n.UDP(), test.remoteaddr.Port)
} }
if n.TCP() != int(testRemote.TCP) { if n.TCP() != int(testRemote.TCP) {
@ -508,12 +494,12 @@ func TestUDPv4_EIP868(t *testing.T) {
// Perform endpoint proof and check for sequence number in packet tail. // Perform endpoint proof and check for sequence number in packet tail.
test.packetIn(nil, &v4wire.Ping{Expiration: futureExp}) test.packetIn(nil, &v4wire.Ping{Expiration: futureExp})
test.waitPacketOut(func(p *v4wire.Pong, addr *net.UDPAddr, hash []byte) { test.waitPacketOut(func(p *v4wire.Pong, addr netip.AddrPort, hash []byte) {
if p.ENRSeq != wantNode.Seq() { if p.ENRSeq != wantNode.Seq() {
t.Errorf("wrong sequence number in pong: %d, want %d", p.ENRSeq, wantNode.Seq()) t.Errorf("wrong sequence number in pong: %d, want %d", p.ENRSeq, wantNode.Seq())
} }
}) })
test.waitPacketOut(func(p *v4wire.Ping, addr *net.UDPAddr, hash []byte) { test.waitPacketOut(func(p *v4wire.Ping, addr netip.AddrPort, hash []byte) {
if p.ENRSeq != wantNode.Seq() { if p.ENRSeq != wantNode.Seq() {
t.Errorf("wrong sequence number in ping: %d, want %d", p.ENRSeq, wantNode.Seq()) t.Errorf("wrong sequence number in ping: %d, want %d", p.ENRSeq, wantNode.Seq())
} }
@ -523,7 +509,7 @@ func TestUDPv4_EIP868(t *testing.T) {
// Request should work now. // Request should work now.
test.packetIn(nil, &v4wire.ENRRequest{Expiration: futureExp}) test.packetIn(nil, &v4wire.ENRRequest{Expiration: futureExp})
test.waitPacketOut(func(p *v4wire.ENRResponse, addr *net.UDPAddr, hash []byte) { test.waitPacketOut(func(p *v4wire.ENRResponse, addr netip.AddrPort, hash []byte) {
n, err := enode.New(enode.ValidSchemes, &p.Record) n, err := enode.New(enode.ValidSchemes, &p.Record)
if err != nil { if err != nil {
t.Fatalf("invalid record: %v", err) t.Fatalf("invalid record: %v", err)
@ -634,7 +620,7 @@ type dgramPipe struct {
} }
type dgram struct { type dgram struct {
to net.UDPAddr to netip.AddrPort
data []byte data []byte
} }
@ -648,8 +634,8 @@ func newpipe() *dgramPipe {
} }
} }
// WriteToUDP queues a datagram. // WriteToUDPAddrPort queues a datagram.
func (c *dgramPipe) WriteToUDP(b []byte, to *net.UDPAddr) (n int, err error) { func (c *dgramPipe) WriteToUDPAddrPort(b []byte, to netip.AddrPort) (n int, err error) {
msg := make([]byte, len(b)) msg := make([]byte, len(b))
copy(msg, b) copy(msg, b)
c.mu.Lock() c.mu.Lock()
@ -658,17 +644,16 @@ func (c *dgramPipe) WriteToUDP(b []byte, to *net.UDPAddr) (n int, err error) {
if c.closed { if c.closed {
return 0, errors.New("closed") return 0, errors.New("closed")
} }
c.queue = append(c.queue, dgram{to, b})
c.queue = append(c.queue, dgram{*to, b})
c.cond.Signal() c.cond.Signal()
return len(b), nil return len(b), nil
} }
// ReadFromUDP just hangs until the pipe is closed. // ReadFromUDPAddrPort just hangs until the pipe is closed.
func (c *dgramPipe) ReadFromUDP(b []byte) (n int, addr *net.UDPAddr, err error) { func (c *dgramPipe) ReadFromUDPAddrPort(b []byte) (n int, addr netip.AddrPort, err error) {
<-c.closing <-c.closing
return 0, nil, io.EOF return 0, netip.AddrPort{}, io.EOF
} }
func (c *dgramPipe) Close() error { func (c *dgramPipe) Close() error {

View file

@ -25,6 +25,7 @@ import (
"fmt" "fmt"
"math/big" "math/big"
"net" "net"
"net/netip"
"time" "time"
"github.com/ethereum/go-ethereum/common/math" "github.com/ethereum/go-ethereum/common/math"
@ -150,15 +151,15 @@ type Endpoint struct {
} }
// NewEndpoint creates an endpoint. // NewEndpoint creates an endpoint.
func NewEndpoint(addr *net.UDPAddr, tcpPort uint16) Endpoint { func NewEndpoint(addr netip.AddrPort, tcpPort uint16) Endpoint {
ip := net.IP{} var ip net.IP
if ip4 := addr.IP.To4(); ip4 != nil { if addr.Addr().Is4() || addr.Addr().Is4In6() {
ip = ip4 ip4 := addr.Addr().As4()
} else if ip6 := addr.IP.To16(); ip6 != nil { ip = ip4[:]
ip = ip6 } else {
ip = addr.Addr().AsSlice()
} }
return Endpoint{IP: ip, UDP: addr.Port(), TCP: tcpPort}
return Endpoint{IP: ip, UDP: uint16(addr.Port), TCP: tcpPort}
} }
type Packet interface { type Packet interface {

View file

@ -18,6 +18,7 @@ package discover
import ( import (
"net" "net"
"net/netip"
"sync" "sync"
"time" "time"
@ -70,7 +71,7 @@ func (t *talkSystem) register(protocol string, handler TalkRequestHandler) {
} }
// handleRequest handles a talk request. // handleRequest handles a talk request.
func (t *talkSystem) handleRequest(id enode.ID, addr *net.UDPAddr, req *v5wire.TalkRequest) { func (t *talkSystem) handleRequest(id enode.ID, addr netip.AddrPort, req *v5wire.TalkRequest) {
t.mutex.Lock() t.mutex.Lock()
handler, ok := t.handlers[req.Protocol] handler, ok := t.handlers[req.Protocol]
t.mutex.Unlock() t.mutex.Unlock()
@ -88,7 +89,8 @@ func (t *talkSystem) handleRequest(id enode.ID, addr *net.UDPAddr, req *v5wire.T
case <-t.slots: case <-t.slots:
go func() { go func() {
defer func() { t.slots <- struct{}{} }() defer func() { t.slots <- struct{}{} }()
respMessage := handler(id, addr, req.Message) udpAddr := &net.UDPAddr{IP: addr.Addr().AsSlice(), Port: int(addr.Port())}
respMessage := handler(id, udpAddr, req.Message)
resp := &v5wire.TalkResponse{ReqID: req.ReqID, Message: respMessage} resp := &v5wire.TalkResponse{ReqID: req.ReqID, Message: respMessage}
t.transport.sendFromAnotherThread(id, addr, resp) t.transport.sendFromAnotherThread(id, addr, resp)
}() }()

View file

@ -25,6 +25,7 @@ import (
"fmt" "fmt"
"io" "io"
"net" "net"
"net/netip"
"sync" "sync"
"time" "time"
@ -100,14 +101,14 @@ type UDPv5 struct {
type sendRequest struct { type sendRequest struct {
destID enode.ID destID enode.ID
destAddr *net.UDPAddr destAddr netip.AddrPort
msg v5wire.Packet msg v5wire.Packet
} }
// callV5 represents a remote procedure call against another node. // callV5 represents a remote procedure call against another node.
type callV5 struct { type callV5 struct {
id enode.ID id enode.ID
addr *net.UDPAddr addr netip.AddrPort
node *enode.Node // This is required to perform handshakes. node *enode.Node // This is required to perform handshakes.
packet v5wire.Packet packet v5wire.Packet
@ -177,7 +178,7 @@ func newUDPv5(conn UDPConn, ln *enode.LocalNode, cfg Config) (*UDPv5, error) {
cancelCloseCtx: cancelCloseCtx, cancelCloseCtx: cancelCloseCtx,
} }
t.talk = newTalkSystem(t) t.talk = newTalkSystem(t)
tab, err := newMeteredTable(t, t.db, cfg) tab, err := newTable(t, t.db, cfg)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -239,7 +240,7 @@ func (t *UDPv5) AllNodes() []*enode.Node {
for _, b := range &t.tab.buckets { for _, b := range &t.tab.buckets {
for _, n := range b.entries { for _, n := range b.entries {
nodes = append(nodes, unwrapNode(n)) nodes = append(nodes, n.Node)
} }
} }
@ -273,7 +274,7 @@ func (t *UDPv5) TalkRequest(n *enode.Node, protocol string, request []byte) ([]b
} }
// TalkRequestToID sends a talk request to a node and waits for a response. // TalkRequestToID sends a talk request to a node and waits for a response.
func (t *UDPv5) TalkRequestToID(id enode.ID, addr *net.UDPAddr, protocol string, request []byte) ([]byte, error) { func (t *UDPv5) TalkRequestToID(id enode.ID, addr netip.AddrPort, protocol string, request []byte) ([]byte, error) {
req := &v5wire.TalkRequest{Protocol: protocol, Message: request} req := &v5wire.TalkRequest{Protocol: protocol, Message: request}
resp := t.callToID(id, addr, v5wire.TalkResponseMsg, req) resp := t.callToID(id, addr, v5wire.TalkResponseMsg, req)
defer t.callDone(resp) defer t.callDone(resp)
@ -323,13 +324,13 @@ func (t *UDPv5) newRandomLookup(ctx context.Context) *lookup {
} }
func (t *UDPv5) newLookup(ctx context.Context, target enode.ID) *lookup { func (t *UDPv5) newLookup(ctx context.Context, target enode.ID) *lookup {
return newLookup(ctx, t.tab, target, func(n *node) ([]*node, error) { return newLookup(ctx, t.tab, target, func(n *enode.Node) ([]*enode.Node, error) {
return t.lookupWorker(n, target) return t.lookupWorker(n, target)
}) })
} }
// lookupWorker performs FINDNODE calls against a single node during lookup. // lookupWorker performs FINDNODE calls against a single node during lookup.
func (t *UDPv5) lookupWorker(destNode *node, target enode.ID) ([]*node, error) { func (t *UDPv5) lookupWorker(destNode *enode.Node, target enode.ID) ([]*enode.Node, error) {
var ( var (
dists = lookupDistances(target, destNode.ID()) dists = lookupDistances(target, destNode.ID())
nodes = nodesByDistance{target: target} nodes = nodesByDistance{target: target}
@ -337,15 +338,14 @@ func (t *UDPv5) lookupWorker(destNode *node, target enode.ID) ([]*node, error) {
) )
var r []*enode.Node var r []*enode.Node
r, err = t.findnode(unwrapNode(destNode), dists) r, err = t.findnode(destNode, dists)
if errors.Is(err, errClosed) { if errors.Is(err, errClosed) {
return nil, err return nil, err
} }
for _, n := range r { for _, n := range r {
if n.ID() != t.Self().ID() { if n.ID() != t.Self().ID() {
nodes.push(wrapNode(n), findnodeResultLimit) nodes.push(n, findnodeResultLimit)
} }
} }
@ -449,7 +449,7 @@ func (t *UDPv5) verifyResponseNode(c *callV5, r *enr.Record, distances []uint, s
if err != nil { if err != nil {
return nil, err return nil, err
} }
if err := netutil.CheckRelayIP(c.addr.IP, node.IP()); err != nil { if err := netutil.CheckRelayIP(c.addr.Addr().AsSlice(), node.IP()); err != nil {
return nil, err return nil, err
} }
@ -489,14 +489,14 @@ func containsUint(x uint, xs []uint) bool {
// callToNode sends the given call and sets up a handler for response packets (of message // callToNode sends the given call and sets up a handler for response packets (of message
// type responseType). Responses are dispatched to the call's response channel. // type responseType). Responses are dispatched to the call's response channel.
func (t *UDPv5) callToNode(n *enode.Node, responseType byte, req v5wire.Packet) *callV5 { func (t *UDPv5) callToNode(n *enode.Node, responseType byte, req v5wire.Packet) *callV5 {
addr := &net.UDPAddr{IP: n.IP(), Port: n.UDP()} addr, _ := n.UDPEndpoint()
c := &callV5{id: n.ID(), addr: addr, node: n} c := &callV5{id: n.ID(), addr: addr, node: n}
t.initCall(c, responseType, req) t.initCall(c, responseType, req)
return c return c
} }
// callToID is like callToNode, but for cases where the node record is not available. // callToID is like callToNode, but for cases where the node record is not available.
func (t *UDPv5) callToID(id enode.ID, addr *net.UDPAddr, responseType byte, req v5wire.Packet) *callV5 { func (t *UDPv5) callToID(id enode.ID, addr netip.AddrPort, responseType byte, req v5wire.Packet) *callV5 {
c := &callV5{id: id, addr: addr} c := &callV5{id: id, addr: addr}
t.initCall(c, responseType, req) t.initCall(c, responseType, req)
return c return c
@ -667,12 +667,12 @@ func (t *UDPv5) sendCall(c *callV5) {
// sendResponse sends a response packet to the given node. // sendResponse sends a response packet to the given node.
// This doesn't trigger a handshake even if no keys are available. // This doesn't trigger a handshake even if no keys are available.
func (t *UDPv5) sendResponse(toID enode.ID, toAddr *net.UDPAddr, packet v5wire.Packet) error { func (t *UDPv5) sendResponse(toID enode.ID, toAddr netip.AddrPort, packet v5wire.Packet) error {
_, err := t.send(toID, toAddr, packet, nil) _, err := t.send(toID, toAddr, packet, nil)
return err return err
} }
func (t *UDPv5) sendFromAnotherThread(toID enode.ID, toAddr *net.UDPAddr, packet v5wire.Packet) { func (t *UDPv5) sendFromAnotherThread(toID enode.ID, toAddr netip.AddrPort, packet v5wire.Packet) {
select { select {
case t.sendCh <- sendRequest{toID, toAddr, packet}: case t.sendCh <- sendRequest{toID, toAddr, packet}:
case <-t.closeCtx.Done(): case <-t.closeCtx.Done():
@ -680,7 +680,7 @@ func (t *UDPv5) sendFromAnotherThread(toID enode.ID, toAddr *net.UDPAddr, packet
} }
// send sends a packet to the given node. // send sends a packet to the given node.
func (t *UDPv5) send(toID enode.ID, toAddr *net.UDPAddr, packet v5wire.Packet, c *v5wire.Whoareyou) (v5wire.Nonce, error) { func (t *UDPv5) send(toID enode.ID, toAddr netip.AddrPort, packet v5wire.Packet, c *v5wire.Whoareyou) (v5wire.Nonce, error) {
addr := toAddr.String() addr := toAddr.String()
t.logcontext = append(t.logcontext[:0], "id", toID, "addr", addr) t.logcontext = append(t.logcontext[:0], "id", toID, "addr", addr)
t.logcontext = packet.AppendLogInfo(t.logcontext) t.logcontext = packet.AppendLogInfo(t.logcontext)
@ -693,7 +693,7 @@ func (t *UDPv5) send(toID enode.ID, toAddr *net.UDPAddr, packet v5wire.Packet, c
return nonce, err return nonce, err
} }
_, err = t.conn.WriteToUDP(enc, toAddr) _, err = t.conn.WriteToUDPAddrPort(enc, toAddr)
t.log.Trace(">> "+packet.Name(), t.logcontext...) t.log.Trace(">> "+packet.Name(), t.logcontext...)
return nonce, err return nonce, err
@ -705,7 +705,7 @@ func (t *UDPv5) readLoop() {
buf := make([]byte, maxPacketSize) buf := make([]byte, maxPacketSize)
for range t.readNextCh { for range t.readNextCh {
nbytes, from, err := t.conn.ReadFromUDP(buf) nbytes, from, err := t.conn.ReadFromUDPAddrPort(buf)
if netutil.IsTemporaryError(err) { if netutil.IsTemporaryError(err) {
// Ignore temporary read errors. // Ignore temporary read errors.
t.log.Debug("Temporary UDP read error", "err", err) t.log.Debug("Temporary UDP read error", "err", err)
@ -724,7 +724,7 @@ func (t *UDPv5) readLoop() {
} }
// dispatchReadPacket sends a packet into the dispatch loop. // dispatchReadPacket sends a packet into the dispatch loop.
func (t *UDPv5) dispatchReadPacket(from *net.UDPAddr, content []byte) bool { func (t *UDPv5) dispatchReadPacket(from netip.AddrPort, content []byte) bool {
select { select {
case t.packetInCh <- ReadPacket{content, from}: case t.packetInCh <- ReadPacket{content, from}:
return true return true
@ -734,7 +734,7 @@ func (t *UDPv5) dispatchReadPacket(from *net.UDPAddr, content []byte) bool {
} }
// handlePacket decodes and processes an incoming packet from the network. // handlePacket decodes and processes an incoming packet from the network.
func (t *UDPv5) handlePacket(rawpacket []byte, fromAddr *net.UDPAddr) error { func (t *UDPv5) handlePacket(rawpacket []byte, fromAddr netip.AddrPort) error {
addr := fromAddr.String() addr := fromAddr.String()
fromID, fromNode, packet, err := t.codec.Decode(rawpacket, addr) fromID, fromNode, packet, err := t.codec.Decode(rawpacket, addr)
@ -756,7 +756,7 @@ func (t *UDPv5) handlePacket(rawpacket []byte, fromAddr *net.UDPAddr) error {
if fromNode != nil { if fromNode != nil {
// Handshake succeeded, add to table. // Handshake succeeded, add to table.
t.tab.addSeenNode(wrapNode(fromNode)) t.tab.addInboundNode(fromNode)
} }
if packet.Kind() != v5wire.WhoareyouPacket { if packet.Kind() != v5wire.WhoareyouPacket {
@ -772,13 +772,13 @@ func (t *UDPv5) handlePacket(rawpacket []byte, fromAddr *net.UDPAddr) error {
} }
// handleCallResponse dispatches a response packet to the call waiting for it. // handleCallResponse dispatches a response packet to the call waiting for it.
func (t *UDPv5) handleCallResponse(fromID enode.ID, fromAddr *net.UDPAddr, p v5wire.Packet) bool { func (t *UDPv5) handleCallResponse(fromID enode.ID, fromAddr netip.AddrPort, p v5wire.Packet) bool {
ac := t.activeCallByNode[fromID] ac := t.activeCallByNode[fromID]
if ac == nil || !bytes.Equal(p.RequestID(), ac.reqid) { if ac == nil || !bytes.Equal(p.RequestID(), ac.reqid) {
t.log.Debug(fmt.Sprintf("Unsolicited/late %s response", p.Name()), "id", fromID, "addr", fromAddr) t.log.Debug(fmt.Sprintf("Unsolicited/late %s response", p.Name()), "id", fromID, "addr", fromAddr)
return false return false
} }
if !fromAddr.IP.Equal(ac.addr.IP) || fromAddr.Port != ac.addr.Port { if fromAddr != ac.addr {
t.log.Debug(fmt.Sprintf("%s from wrong endpoint", p.Name()), "id", fromID, "addr", fromAddr) t.log.Debug(fmt.Sprintf("%s from wrong endpoint", p.Name()), "id", fromID, "addr", fromAddr)
return false return false
} }
@ -808,7 +808,7 @@ func (t *UDPv5) getNode(id enode.ID) *enode.Node {
} }
// handle processes incoming packets according to their message type. // handle processes incoming packets according to their message type.
func (t *UDPv5) handle(p v5wire.Packet, fromID enode.ID, fromAddr *net.UDPAddr) { func (t *UDPv5) handle(p v5wire.Packet, fromID enode.ID, fromAddr netip.AddrPort) {
switch p := p.(type) { switch p := p.(type) {
case *v5wire.Unknown: case *v5wire.Unknown:
t.handleUnknown(p, fromID, fromAddr) t.handleUnknown(p, fromID, fromAddr)
@ -818,7 +818,9 @@ func (t *UDPv5) handle(p v5wire.Packet, fromID enode.ID, fromAddr *net.UDPAddr)
t.handlePing(p, fromID, fromAddr) t.handlePing(p, fromID, fromAddr)
case *v5wire.Pong: case *v5wire.Pong:
if t.handleCallResponse(fromID, fromAddr, p) { if t.handleCallResponse(fromID, fromAddr, p) {
t.localNode.UDPEndpointStatement(fromAddr, &net.UDPAddr{IP: p.ToIP, Port: int(p.ToPort)}) fromUDPAddr := &net.UDPAddr{IP: fromAddr.Addr().AsSlice(), Port: int(fromAddr.Port())}
toUDPAddr := &net.UDPAddr{IP: p.ToIP, Port: int(p.ToPort)}
t.localNode.UDPEndpointStatement(fromUDPAddr, toUDPAddr)
} }
case *v5wire.Findnode: case *v5wire.Findnode:
t.handleFindnode(p, fromID, fromAddr) t.handleFindnode(p, fromID, fromAddr)
@ -832,7 +834,7 @@ func (t *UDPv5) handle(p v5wire.Packet, fromID enode.ID, fromAddr *net.UDPAddr)
} }
// handleUnknown initiates a handshake by responding with WHOAREYOU. // handleUnknown initiates a handshake by responding with WHOAREYOU.
func (t *UDPv5) handleUnknown(p *v5wire.Unknown, fromID enode.ID, fromAddr *net.UDPAddr) { func (t *UDPv5) handleUnknown(p *v5wire.Unknown, fromID enode.ID, fromAddr netip.AddrPort) {
challenge := &v5wire.Whoareyou{Nonce: p.Nonce} challenge := &v5wire.Whoareyou{Nonce: p.Nonce}
crand.Read(challenge.IDNonce[:]) crand.Read(challenge.IDNonce[:])
@ -850,7 +852,7 @@ var (
) )
// handleWhoareyou resends the active call as a handshake packet. // handleWhoareyou resends the active call as a handshake packet.
func (t *UDPv5) handleWhoareyou(p *v5wire.Whoareyou, fromID enode.ID, fromAddr *net.UDPAddr) { func (t *UDPv5) handleWhoareyou(p *v5wire.Whoareyou, fromID enode.ID, fromAddr netip.AddrPort) {
c, err := t.matchWithCall(fromID, p.Nonce) c, err := t.matchWithCall(fromID, p.Nonce)
if err != nil { if err != nil {
t.log.Debug("Invalid "+p.Name(), "addr", fromAddr, "err", err) t.log.Debug("Invalid "+p.Name(), "addr", fromAddr, "err", err)
@ -887,33 +889,36 @@ func (t *UDPv5) matchWithCall(fromID enode.ID, nonce v5wire.Nonce) (*callV5, err
} }
// handlePing sends a PONG response. // handlePing sends a PONG response.
func (t *UDPv5) handlePing(p *v5wire.Ping, fromID enode.ID, fromAddr *net.UDPAddr) { func (t *UDPv5) handlePing(p *v5wire.Ping, fromID enode.ID, fromAddr netip.AddrPort) {
remoteIP := fromAddr.IP var remoteIP net.IP
// Handle IPv4 mapped IPv6 addresses in the // Handle IPv4 mapped IPv6 addresses in the event the local node is binded
// event the local node is binded to an // to an ipv6 interface.
// ipv6 interface. if fromAddr.Addr().Is4() || fromAddr.Addr().Is4In6() {
if remoteIP.To4() != nil { ip4 := fromAddr.Addr().As4()
remoteIP = remoteIP.To4() remoteIP = ip4[:]
} else {
remoteIP = fromAddr.Addr().AsSlice()
} }
t.sendResponse(fromID, fromAddr, &v5wire.Pong{ t.sendResponse(fromID, fromAddr, &v5wire.Pong{
ReqID: p.ReqID, ReqID: p.ReqID,
ToIP: remoteIP, ToIP: remoteIP,
ToPort: uint16(fromAddr.Port), ToPort: fromAddr.Port(),
ENRSeq: t.localNode.Node().Seq(), ENRSeq: t.localNode.Node().Seq(),
}) })
} }
// handleFindnode returns nodes to the requester. // handleFindnode returns nodes to the requester.
func (t *UDPv5) handleFindnode(p *v5wire.Findnode, fromID enode.ID, fromAddr *net.UDPAddr) { func (t *UDPv5) handleFindnode(p *v5wire.Findnode, fromID enode.ID, fromAddr netip.AddrPort) {
nodes := t.collectTableNodes(fromAddr.IP, p.Distances, findnodeResultLimit) nodes := t.collectTableNodes(fromAddr.Addr(), p.Distances, findnodeResultLimit)
for _, resp := range packNodes(p.ReqID, nodes) { for _, resp := range packNodes(p.ReqID, nodes) {
t.sendResponse(fromID, fromAddr, resp) t.sendResponse(fromID, fromAddr, resp)
} }
} }
// collectTableNodes creates a FINDNODE result set for the given distances. // collectTableNodes creates a FINDNODE result set for the given distances.
func (t *UDPv5) collectTableNodes(rip net.IP, distances []uint, limit int) []*enode.Node { func (t *UDPv5) collectTableNodes(rip netip.Addr, distances []uint, limit int) []*enode.Node {
ripSlice := rip.AsSlice()
var bn []*enode.Node var bn []*enode.Node
var nodes []*enode.Node var nodes []*enode.Node
@ -929,7 +934,7 @@ func (t *UDPv5) collectTableNodes(rip net.IP, distances []uint, limit int) []*en
for _, n := range t.tab.appendLiveNodes(dist, bn[:0]) { for _, n := range t.tab.appendLiveNodes(dist, bn[:0]) {
// Apply some pre-checks to avoid sending invalid nodes. // Apply some pre-checks to avoid sending invalid nodes.
// Note liveness is checked by appendLiveNodes. // Note liveness is checked by appendLiveNodes.
if netutil.CheckRelayIP(rip, n.IP()) != nil { if netutil.CheckRelayIP(ripSlice, n.IP()) != nil {
continue continue
} }

View file

@ -23,6 +23,7 @@ import (
"fmt" "fmt"
"math/rand" "math/rand"
"net" "net"
"net/netip"
"reflect" "reflect"
"testing" "testing"
"time" "time"
@ -113,7 +114,7 @@ func TestUDPv5_pingHandling(t *testing.T) {
defer test.close() defer test.close()
test.packetIn(&v5wire.Ping{ReqID: []byte("foo")}) test.packetIn(&v5wire.Ping{ReqID: []byte("foo")})
test.waitPacketOut(func(p *v5wire.Pong, addr *net.UDPAddr, _ v5wire.Nonce) { test.waitPacketOut(func(p *v5wire.Pong, addr netip.AddrPort, _ v5wire.Nonce) {
if !bytes.Equal(p.ReqID, []byte("foo")) { if !bytes.Equal(p.ReqID, []byte("foo")) {
t.Error("wrong request ID in response:", p.ReqID) t.Error("wrong request ID in response:", p.ReqID)
} }
@ -150,16 +151,16 @@ func TestUDPv5_unknownPacket(t *testing.T) {
// Unknown packet from unknown node. // Unknown packet from unknown node.
test.packetIn(&v5wire.Unknown{Nonce: nonce}) test.packetIn(&v5wire.Unknown{Nonce: nonce})
test.waitPacketOut(func(p *v5wire.Whoareyou, addr *net.UDPAddr, _ v5wire.Nonce) { test.waitPacketOut(func(p *v5wire.Whoareyou, addr netip.AddrPort, _ v5wire.Nonce) {
check(p, 0) check(p, 0)
}) })
// Make node known. // Make node known.
n := test.getNode(test.remotekey, test.remoteaddr).Node() n := test.getNode(test.remotekey, test.remoteaddr).Node()
test.table.addSeenNode(wrapNode(n)) test.table.addFoundNode(n, false)
test.packetIn(&v5wire.Unknown{Nonce: nonce}) test.packetIn(&v5wire.Unknown{Nonce: nonce})
test.waitPacketOut(func(p *v5wire.Whoareyou, addr *net.UDPAddr, _ v5wire.Nonce) { test.waitPacketOut(func(p *v5wire.Whoareyou, addr netip.AddrPort, _ v5wire.Nonce) {
check(p, n.Seq()) check(p, n.Seq())
}) })
} }
@ -175,9 +176,9 @@ func TestUDPv5_findnodeHandling(t *testing.T) {
nodes253 := nodesAtDistance(test.table.self().ID(), 253, 16) nodes253 := nodesAtDistance(test.table.self().ID(), 253, 16)
nodes249 := nodesAtDistance(test.table.self().ID(), 249, 4) nodes249 := nodesAtDistance(test.table.self().ID(), 249, 4)
nodes248 := nodesAtDistance(test.table.self().ID(), 248, 10) nodes248 := nodesAtDistance(test.table.self().ID(), 248, 10)
fillTable(test.table, wrapNodes(nodes253), true) fillTable(test.table, nodes253, true)
fillTable(test.table, wrapNodes(nodes249), true) fillTable(test.table, nodes249, true)
fillTable(test.table, wrapNodes(nodes248), true) fillTable(test.table, nodes248, true)
// Requesting with distance zero should return the node's own record. // Requesting with distance zero should return the node's own record.
test.packetIn(&v5wire.Findnode{ReqID: []byte{0}, Distances: []uint{0}}) test.packetIn(&v5wire.Findnode{ReqID: []byte{0}, Distances: []uint{0}})
@ -216,7 +217,7 @@ func (test *udpV5Test) expectNodes(wantReqID []byte, wantTotal uint8, wantNodes
} }
for { for {
test.waitPacketOut(func(p *v5wire.Nodes, addr *net.UDPAddr, _ v5wire.Nonce) { test.waitPacketOut(func(p *v5wire.Nodes, addr netip.AddrPort, _ v5wire.Nonce) {
if !bytes.Equal(p.ReqID, wantReqID) { if !bytes.Equal(p.ReqID, wantReqID) {
test.t.Fatalf("wrong request ID %v in response, want %v", p.ReqID, wantReqID) test.t.Fatalf("wrong request ID %v in response, want %v", p.ReqID, wantReqID)
} }
@ -262,8 +263,7 @@ func TestUDPv5_pingCall(t *testing.T) {
_, err := test.udp.ping(remote) _, err := test.udp.ping(remote)
done <- err done <- err
}() }()
test.waitPacketOut(func(p *v5wire.Ping, addr *net.UDPAddr, _ v5wire.Nonce) {}) test.waitPacketOut(func(p *v5wire.Ping, addr netip.AddrPort, _ v5wire.Nonce) {})
if err := <-done; err != errTimeout { if err := <-done; err != errTimeout {
t.Fatalf("want errTimeout, got %q", err) t.Fatalf("want errTimeout, got %q", err)
} }
@ -273,7 +273,7 @@ func TestUDPv5_pingCall(t *testing.T) {
_, err := test.udp.ping(remote) _, err := test.udp.ping(remote)
done <- err done <- err
}() }()
test.waitPacketOut(func(p *v5wire.Ping, addr *net.UDPAddr, _ v5wire.Nonce) { test.waitPacketOut(func(p *v5wire.Ping, addr netip.AddrPort, _ v5wire.Nonce) {
test.packetInFrom(test.remotekey, test.remoteaddr, &v5wire.Pong{ReqID: p.ReqID}) test.packetInFrom(test.remotekey, test.remoteaddr, &v5wire.Pong{ReqID: p.ReqID})
}) })
@ -286,8 +286,8 @@ func TestUDPv5_pingCall(t *testing.T) {
_, err := test.udp.ping(remote) _, err := test.udp.ping(remote)
done <- err done <- err
}() }()
test.waitPacketOut(func(p *v5wire.Ping, addr *net.UDPAddr, _ v5wire.Nonce) { test.waitPacketOut(func(p *v5wire.Ping, addr netip.AddrPort, _ v5wire.Nonce) {
wrongAddr := &net.UDPAddr{IP: net.IP{33, 44, 55, 22}, Port: 10101} wrongAddr := netip.MustParseAddrPort("33.44.55.22:10101")
test.packetInFrom(test.remotekey, wrongAddr, &v5wire.Pong{ReqID: p.ReqID}) test.packetInFrom(test.remotekey, wrongAddr, &v5wire.Pong{ReqID: p.ReqID})
}) })
@ -320,7 +320,7 @@ func TestUDPv5_findnodeCall(t *testing.T) {
}() }()
// Serve the responses: // Serve the responses:
test.waitPacketOut(func(p *v5wire.Findnode, addr *net.UDPAddr, _ v5wire.Nonce) { test.waitPacketOut(func(p *v5wire.Findnode, addr netip.AddrPort, _ v5wire.Nonce) {
if !reflect.DeepEqual(p.Distances, distances) { if !reflect.DeepEqual(p.Distances, distances) {
t.Fatalf("wrong distances in request: %v", p.Distances) t.Fatalf("wrong distances in request: %v", p.Distances)
} }
@ -369,15 +369,15 @@ func TestUDPv5_callResend(t *testing.T) {
}() }()
// Ping answered by WHOAREYOU. // Ping answered by WHOAREYOU.
test.waitPacketOut(func(p *v5wire.Ping, addr *net.UDPAddr, nonce v5wire.Nonce) { test.waitPacketOut(func(p *v5wire.Ping, addr netip.AddrPort, nonce v5wire.Nonce) {
test.packetIn(&v5wire.Whoareyou{Nonce: nonce}) test.packetIn(&v5wire.Whoareyou{Nonce: nonce})
}) })
// Ping should be re-sent. // Ping should be re-sent.
test.waitPacketOut(func(p *v5wire.Ping, addr *net.UDPAddr, _ v5wire.Nonce) { test.waitPacketOut(func(p *v5wire.Ping, addr netip.AddrPort, _ v5wire.Nonce) {
test.packetIn(&v5wire.Pong{ReqID: p.ReqID}) test.packetIn(&v5wire.Pong{ReqID: p.ReqID})
}) })
// Answer the other ping. // Answer the other ping.
test.waitPacketOut(func(p *v5wire.Ping, addr *net.UDPAddr, _ v5wire.Nonce) { test.waitPacketOut(func(p *v5wire.Ping, addr netip.AddrPort, _ v5wire.Nonce) {
test.packetIn(&v5wire.Pong{ReqID: p.ReqID}) test.packetIn(&v5wire.Pong{ReqID: p.ReqID})
}) })
@ -406,11 +406,11 @@ func TestUDPv5_multipleHandshakeRounds(t *testing.T) {
}() }()
// Ping answered by WHOAREYOU. // Ping answered by WHOAREYOU.
test.waitPacketOut(func(p *v5wire.Ping, addr *net.UDPAddr, nonce v5wire.Nonce) { test.waitPacketOut(func(p *v5wire.Ping, addr netip.AddrPort, nonce v5wire.Nonce) {
test.packetIn(&v5wire.Whoareyou{Nonce: nonce}) test.packetIn(&v5wire.Whoareyou{Nonce: nonce})
}) })
// Ping answered by WHOAREYOU again. // Ping answered by WHOAREYOU again.
test.waitPacketOut(func(p *v5wire.Ping, addr *net.UDPAddr, nonce v5wire.Nonce) { test.waitPacketOut(func(p *v5wire.Ping, addr netip.AddrPort, nonce v5wire.Nonce) {
test.packetIn(&v5wire.Whoareyou{Nonce: nonce}) test.packetIn(&v5wire.Whoareyou{Nonce: nonce})
}) })
@ -440,7 +440,7 @@ func TestUDPv5_callTimeoutReset(t *testing.T) {
}() }()
// Serve two responses, slowly. // Serve two responses, slowly.
test.waitPacketOut(func(p *v5wire.Findnode, addr *net.UDPAddr, _ v5wire.Nonce) { test.waitPacketOut(func(p *v5wire.Findnode, addr netip.AddrPort, _ v5wire.Nonce) {
time.Sleep(respTimeout - 50*time.Millisecond) time.Sleep(respTimeout - 50*time.Millisecond)
test.packetIn(&v5wire.Nodes{ test.packetIn(&v5wire.Nodes{
ReqID: p.ReqID, ReqID: p.ReqID,
@ -481,7 +481,7 @@ func TestUDPv5_talkHandling(t *testing.T) {
Protocol: "test", Protocol: "test",
Message: []byte("test request"), Message: []byte("test request"),
}) })
test.waitPacketOut(func(p *v5wire.TalkResponse, addr *net.UDPAddr, _ v5wire.Nonce) { test.waitPacketOut(func(p *v5wire.TalkResponse, addr netip.AddrPort, _ v5wire.Nonce) {
if !bytes.Equal(p.ReqID, []byte("foo")) { if !bytes.Equal(p.ReqID, []byte("foo")) {
t.Error("wrong request ID in response:", p.ReqID) t.Error("wrong request ID in response:", p.ReqID)
} }
@ -503,7 +503,7 @@ func TestUDPv5_talkHandling(t *testing.T) {
Protocol: "wrong", Protocol: "wrong",
Message: []byte("test request"), Message: []byte("test request"),
}) })
test.waitPacketOut(func(p *v5wire.TalkResponse, addr *net.UDPAddr, _ v5wire.Nonce) { test.waitPacketOut(func(p *v5wire.TalkResponse, addr netip.AddrPort, _ v5wire.Nonce) {
if !bytes.Equal(p.ReqID, []byte("2")) { if !bytes.Equal(p.ReqID, []byte("2")) {
t.Error("wrong request ID in response:", p.ReqID) t.Error("wrong request ID in response:", p.ReqID)
} }
@ -533,8 +533,7 @@ func TestUDPv5_talkRequest(t *testing.T) {
_, err := test.udp.TalkRequest(remote, "test", []byte("test request")) _, err := test.udp.TalkRequest(remote, "test", []byte("test request"))
done <- err done <- err
}() }()
test.waitPacketOut(func(p *v5wire.TalkRequest, addr *net.UDPAddr, _ v5wire.Nonce) {}) test.waitPacketOut(func(p *v5wire.TalkRequest, addr netip.AddrPort, _ v5wire.Nonce) {})
if err := <-done; err != errTimeout { if err := <-done; err != errTimeout {
t.Fatalf("want errTimeout, got %q", err) t.Fatalf("want errTimeout, got %q", err)
} }
@ -544,7 +543,7 @@ func TestUDPv5_talkRequest(t *testing.T) {
_, err := test.udp.TalkRequest(remote, "test", []byte("test request")) _, err := test.udp.TalkRequest(remote, "test", []byte("test request"))
done <- err done <- err
}() }()
test.waitPacketOut(func(p *v5wire.TalkRequest, addr *net.UDPAddr, _ v5wire.Nonce) { test.waitPacketOut(func(p *v5wire.TalkRequest, addr netip.AddrPort, _ v5wire.Nonce) {
if p.Protocol != "test" { if p.Protocol != "test" {
t.Errorf("wrong protocol ID in talk request: %q", p.Protocol) t.Errorf("wrong protocol ID in talk request: %q", p.Protocol)
} }
@ -568,7 +567,7 @@ func TestUDPv5_talkRequest(t *testing.T) {
_, err := test.udp.TalkRequestToID(remote.ID(), test.remoteaddr, "test", []byte("test request 2")) _, err := test.udp.TalkRequestToID(remote.ID(), test.remoteaddr, "test", []byte("test request 2"))
done <- err done <- err
}() }()
test.waitPacketOut(func(p *v5wire.TalkRequest, addr *net.UDPAddr, _ v5wire.Nonce) { test.waitPacketOut(func(p *v5wire.TalkRequest, addr netip.AddrPort, _ v5wire.Nonce) {
if p.Protocol != "test" { if p.Protocol != "test" {
t.Errorf("wrong protocol ID in talk request: %q", p.Protocol) t.Errorf("wrong protocol ID in talk request: %q", p.Protocol)
} }
@ -646,13 +645,14 @@ func TestUDPv5_lookup(t *testing.T) {
for d, nn := range lookupTestnet.dists { for d, nn := range lookupTestnet.dists {
for i, key := range nn { for i, key := range nn {
n := lookupTestnet.node(d, i) n := lookupTestnet.node(d, i)
test.getNode(key, &net.UDPAddr{IP: n.IP(), Port: n.UDP()}) addr, _ := n.UDPEndpoint()
test.getNode(key, addr)
} }
} }
// Seed table with initial node. // Seed table with initial node.
initialNode := lookupTestnet.node(256, 0) initialNode := lookupTestnet.node(256, 0)
fillTable(test.table, []*node{wrapNode(initialNode)}, true) fillTable(test.table, []*enode.Node{initialNode}, true)
// Start the lookup. // Start the lookup.
resultC := make(chan []*enode.Node, 1) resultC := make(chan []*enode.Node, 1)
@ -666,7 +666,7 @@ func TestUDPv5_lookup(t *testing.T) {
asked := make(map[enode.ID]bool) asked := make(map[enode.ID]bool)
for done := false; !done; { for done := false; !done; {
done = test.waitPacketOut(func(p v5wire.Packet, to *net.UDPAddr, _ v5wire.Nonce) { done = test.waitPacketOut(func(p v5wire.Packet, to netip.AddrPort, _ v5wire.Nonce) {
recipient, key := lookupTestnet.nodeByAddr(to) recipient, key := lookupTestnet.nodeByAddr(to)
switch p := p.(type) { switch p := p.(type) {
case *v5wire.Ping: case *v5wire.Ping:
@ -723,11 +723,8 @@ func TestUDPv5_PingWithIPV4MappedAddress(t *testing.T) {
test := newUDPV5Test(t) test := newUDPV5Test(t)
defer test.close() defer test.close()
rawIP := net.IPv4(0xFF, 0x12, 0x33, 0xE5) rawIP := netip.AddrFrom4([4]byte{0xFF, 0x12, 0x33, 0xE5})
test.remoteaddr = &net.UDPAddr{ test.remoteaddr = netip.AddrPortFrom(netip.AddrFrom16(rawIP.As16()), 0)
IP: rawIP.To16(),
Port: 0,
}
remote := test.getNode(test.remotekey, test.remoteaddr).Node() remote := test.getNode(test.remotekey, test.remoteaddr).Node()
done := make(chan struct{}, 1) done := make(chan struct{}, 1)
@ -736,7 +733,7 @@ func TestUDPv5_PingWithIPV4MappedAddress(t *testing.T) {
test.udp.handlePing(&v5wire.Ping{ENRSeq: 1}, remote.ID(), test.remoteaddr) test.udp.handlePing(&v5wire.Ping{ENRSeq: 1}, remote.ID(), test.remoteaddr)
done <- struct{}{} done <- struct{}{}
}() }()
test.waitPacketOut(func(p *v5wire.Pong, addr *net.UDPAddr, _ v5wire.Nonce) { test.waitPacketOut(func(p *v5wire.Pong, addr netip.AddrPort, _ v5wire.Nonce) {
if len(p.ToIP) == net.IPv6len { if len(p.ToIP) == net.IPv6len {
t.Error("Received untruncated ip address") t.Error("Received untruncated ip address")
} }
@ -744,8 +741,7 @@ func TestUDPv5_PingWithIPV4MappedAddress(t *testing.T) {
if len(p.ToIP) != net.IPv4len { if len(p.ToIP) != net.IPv4len {
t.Errorf("Received ip address with incorrect length: %d", len(p.ToIP)) t.Errorf("Received ip address with incorrect length: %d", len(p.ToIP))
} }
if !p.ToIP.Equal(rawIP.AsSlice()) {
if !p.ToIP.Equal(rawIP) {
t.Errorf("Received incorrect ip address: wanted %s but received %s", rawIP.String(), p.ToIP.String()) t.Errorf("Received incorrect ip address: wanted %s but received %s", rawIP.String(), p.ToIP.String())
} }
}) })
@ -761,9 +757,9 @@ type udpV5Test struct {
db *enode.DB db *enode.DB
udp *UDPv5 udp *UDPv5
localkey, remotekey *ecdsa.PrivateKey localkey, remotekey *ecdsa.PrivateKey
remoteaddr *net.UDPAddr remoteaddr netip.AddrPort
nodesByID map[enode.ID]*enode.LocalNode nodesByID map[enode.ID]*enode.LocalNode
nodesByIP map[string]*enode.LocalNode nodesByIP map[netip.Addr]*enode.LocalNode
} }
// testCodec is the packet encoding used by protocol tests. This codec does not perform encryption. // testCodec is the packet encoding used by protocol tests. This codec does not perform encryption.
@ -829,9 +825,9 @@ func newUDPV5Test(t *testing.T) *udpV5Test {
pipe: newpipe(), pipe: newpipe(),
localkey: newkey(), localkey: newkey(),
remotekey: newkey(), remotekey: newkey(),
remoteaddr: &net.UDPAddr{IP: net.IP{10, 0, 1, 99}, Port: 30303}, remoteaddr: netip.MustParseAddrPort("10.0.1.99:30303"),
nodesByID: make(map[enode.ID]*enode.LocalNode), nodesByID: make(map[enode.ID]*enode.LocalNode),
nodesByIP: make(map[string]*enode.LocalNode), nodesByIP: make(map[netip.Addr]*enode.LocalNode),
} }
test.db, _ = enode.OpenDB("") test.db, _ = enode.OpenDB("")
ln := enode.NewLocalNode(test.db, test.localkey) ln := enode.NewLocalNode(test.db, test.localkey)
@ -857,8 +853,8 @@ func (test *udpV5Test) packetIn(packet v5wire.Packet) {
test.packetInFrom(test.remotekey, test.remoteaddr, packet) test.packetInFrom(test.remotekey, test.remoteaddr, packet)
} }
// handles a packet as if it had been sent to the transport by the key/endpoint. // packetInFrom handles a packet as if it had been sent to the transport by the key/endpoint.
func (test *udpV5Test) packetInFrom(key *ecdsa.PrivateKey, addr *net.UDPAddr, packet v5wire.Packet) { func (test *udpV5Test) packetInFrom(key *ecdsa.PrivateKey, addr netip.AddrPort, packet v5wire.Packet) {
test.t.Helper() test.t.Helper()
ln := test.getNode(key, addr) ln := test.getNode(key, addr)
@ -875,25 +871,23 @@ func (test *udpV5Test) packetInFrom(key *ecdsa.PrivateKey, addr *net.UDPAddr, pa
} }
// getNode ensures the test knows about a node at the given endpoint. // getNode ensures the test knows about a node at the given endpoint.
func (test *udpV5Test) getNode(key *ecdsa.PrivateKey, addr *net.UDPAddr) *enode.LocalNode { func (test *udpV5Test) getNode(key *ecdsa.PrivateKey, addr netip.AddrPort) *enode.LocalNode {
id := encodePubkey(&key.PublicKey).id() id := encodePubkey(&key.PublicKey).id()
ln := test.nodesByID[id] ln := test.nodesByID[id]
if ln == nil { if ln == nil {
db, _ := enode.OpenDB("") db, _ := enode.OpenDB("")
ln = enode.NewLocalNode(db, key) ln = enode.NewLocalNode(db, key)
ln.SetStaticIP(addr.IP) ln.SetStaticIP(addr.Addr().AsSlice())
ln.Set(enr.UDP(addr.Port)) ln.Set(enr.UDP(addr.Port()))
test.nodesByID[id] = ln test.nodesByID[id] = ln
} }
test.nodesByIP[addr.Addr()] = ln
test.nodesByIP[string(addr.IP)] = ln
return ln return ln
} }
// waitPacketOut waits for the next output packet and handles it using the given 'validate' // waitPacketOut waits for the next output packet and handles it using the given 'validate'
// function. The function must be of type func (X, *net.UDPAddr, v5wire.Nonce) where X is // function. The function must be of type func (X, netip.AddrPort, v5wire.Nonce) where X is
// assignable to packetV5. // assignable to packetV5.
func (test *udpV5Test) waitPacketOut(validate interface{}) (closed bool) { func (test *udpV5Test) waitPacketOut(validate interface{}) (closed bool) {
test.t.Helper() test.t.Helper()
@ -910,8 +904,7 @@ func (test *udpV5Test) waitPacketOut(validate interface{}) (closed bool) {
test.t.Fatalf("timed out waiting for %v", exptype) test.t.Fatalf("timed out waiting for %v", exptype)
return false return false
} }
ln := test.nodesByIP[dgram.to.Addr()]
ln := test.nodesByIP[string(dgram.to.IP)]
if ln == nil { if ln == nil {
test.t.Fatalf("attempt to send to non-existing node %v", &dgram.to) test.t.Fatalf("attempt to send to non-existing node %v", &dgram.to)
return false return false
@ -929,9 +922,7 @@ func (test *udpV5Test) waitPacketOut(validate interface{}) (closed bool) {
test.t.Errorf("sent packet type mismatch, got: %v, want: %v", reflect.TypeOf(p), exptype) test.t.Errorf("sent packet type mismatch, got: %v, want: %v", reflect.TypeOf(p), exptype)
return false return false
} }
fn.Call([]reflect.Value{reflect.ValueOf(p), reflect.ValueOf(dgram.to), reflect.ValueOf(frame.AuthTag)})
fn.Call([]reflect.Value{reflect.ValueOf(p), reflect.ValueOf(&dgram.to), reflect.ValueOf(frame.AuthTag)})
return false return false
} }

View file

@ -172,6 +172,5 @@ func SignNull(r *enr.Record, id ID) *Node {
if err := r.SetSig(NullID{}, []byte{}); err != nil { if err := r.SetSig(NullID{}, []byte{}); err != nil {
panic(err) panic(err)
} }
return newNodeWithID(r, id)
return &Node{r: *r, id: id}
} }

View file

@ -24,6 +24,7 @@ import (
"fmt" "fmt"
"math/bits" "math/bits"
"net" "net"
"net/netip"
"strings" "strings"
"github.com/ethereum/go-ethereum/p2p/enr" "github.com/ethereum/go-ethereum/p2p/enr"
@ -36,6 +37,10 @@ var errMissingPrefix = errors.New("missing 'enr:' prefix for base64-encoded reco
type Node struct { type Node struct {
r enr.Record r enr.Record
id ID id ID
// endpoint information
ip netip.Addr
udp uint16
tcp uint16
} }
// New wraps a node record. The record must be valid according to the given // New wraps a node record. The record must be valid according to the given
@ -44,13 +49,76 @@ func New(validSchemes enr.IdentityScheme, r *enr.Record) (*Node, error) {
if err := r.VerifySignature(validSchemes); err != nil { if err := r.VerifySignature(validSchemes); err != nil {
return nil, err return nil, err
} }
var id ID
node := &Node{r: *r} if n := copy(id[:], validSchemes.NodeAddr(r)); n != len(id) {
if n := copy(node.id[:], validSchemes.NodeAddr(&node.r)); n != len(ID{}) { return nil, fmt.Errorf("invalid node ID length %d, need %d", n, len(id))
return nil, fmt.Errorf("invalid node ID length %d, need %d", n, len(ID{}))
} }
return newNodeWithID(r, id), nil
}
return node, nil func newNodeWithID(r *enr.Record, id ID) *Node {
n := &Node{r: *r, id: id}
// Set the preferred endpoint.
// Here we decide between IPv4 and IPv6, choosing the 'most global' address.
var ip4 netip.Addr
var ip6 netip.Addr
n.Load((*enr.IPv4Addr)(&ip4))
n.Load((*enr.IPv6Addr)(&ip6))
valid4 := validIP(ip4)
valid6 := validIP(ip6)
switch {
case valid4 && valid6:
if localityScore(ip4) >= localityScore(ip6) {
n.setIP4(ip4)
} else {
n.setIP6(ip6)
}
case valid4:
n.setIP4(ip4)
case valid6:
n.setIP6(ip6)
}
return n
}
// validIP reports whether 'ip' is a valid node endpoint IP address.
func validIP(ip netip.Addr) bool {
return ip.IsValid() && !ip.IsMulticast()
}
func localityScore(ip netip.Addr) int {
switch {
case ip.IsUnspecified():
return 0
case ip.IsLoopback():
return 1
case ip.IsLinkLocalUnicast():
return 2
case ip.IsPrivate():
return 3
default:
return 4
}
}
func (n *Node) setIP4(ip netip.Addr) {
n.ip = ip
n.Load((*enr.UDP)(&n.udp))
n.Load((*enr.TCP)(&n.tcp))
}
func (n *Node) setIP6(ip netip.Addr) {
if ip.Is4In6() {
n.setIP4(ip)
return
}
n.ip = ip
if err := n.Load((*enr.UDP6)(&n.udp)); err != nil {
n.Load((*enr.UDP)(&n.udp))
}
if err := n.Load((*enr.TCP6)(&n.tcp)); err != nil {
n.Load((*enr.TCP)(&n.tcp))
}
} }
// MustParse parses a node record or enode:// URL. It panics if the input is invalid. // MustParse parses a node record or enode:// URL. It panics if the input is invalid.
@ -96,50 +164,45 @@ func (n *Node) Seq() uint64 {
return n.r.Seq() return n.r.Seq()
} }
// Incomplete returns true for nodes with no IP address.
func (n *Node) Incomplete() bool {
return n.IP() == nil
}
// Load retrieves an entry from the underlying record. // Load retrieves an entry from the underlying record.
func (n *Node) Load(k enr.Entry) error { func (n *Node) Load(k enr.Entry) error {
return n.r.Load(k) return n.r.Load(k)
} }
// IP returns the IP address of the node. This prefers IPv4 addresses. // IP returns the IP address of the node.
func (n *Node) IP() net.IP { func (n *Node) IP() net.IP {
var ( return net.IP(n.ip.AsSlice())
ip4 enr.IPv4 }
ip6 enr.IPv6
)
if n.Load(&ip4) == nil { // IPAddr returns the IP address of the node.
return net.IP(ip4) func (n *Node) IPAddr() netip.Addr {
} return n.ip
if n.Load(&ip6) == nil {
return net.IP(ip6)
}
return nil
} }
// UDP returns the UDP port of the node. // UDP returns the UDP port of the node.
func (n *Node) UDP() int { func (n *Node) UDP() int {
var port enr.UDP return int(n.udp)
n.Load(&port)
return int(port)
} }
// TCP returns the TCP port of the node. // TCP returns the TCP port of the node.
func (n *Node) TCP() int { func (n *Node) TCP() int {
var port enr.TCP return int(n.tcp)
}
n.Load(&port) // UDPEndpoint returns the announced UDP endpoint.
func (n *Node) UDPEndpoint() (netip.AddrPort, bool) {
if !n.ip.IsValid() || n.ip.IsUnspecified() || n.udp == 0 {
return netip.AddrPort{}, false
}
return netip.AddrPortFrom(n.ip, n.udp), true
}
return int(port) // TCPEndpoint returns the announced TCP endpoint.
func (n *Node) TCPEndpoint() (netip.AddrPort, bool) {
if !n.ip.IsValid() || n.ip.IsUnspecified() || n.tcp == 0 {
return netip.AddrPort{}, false
}
return netip.AddrPortFrom(n.ip, n.tcp), true
} }
// Pubkey returns the secp256k1 public key of the node, if present. // Pubkey returns the secp256k1 public key of the node, if present.
@ -162,18 +225,15 @@ func (n *Node) Record() *enr.Record {
// ValidateComplete checks whether n has a valid IP and UDP port. // ValidateComplete checks whether n has a valid IP and UDP port.
// Deprecated: don't use this method. // Deprecated: don't use this method.
func (n *Node) ValidateComplete() error { func (n *Node) ValidateComplete() error {
if n.Incomplete() { if !n.ip.IsValid() {
return errors.New("missing IP address") return errors.New("missing IP address")
} }
if n.ip.IsMulticast() || n.ip.IsUnspecified() {
if n.UDP() == 0 {
return errors.New("missing UDP port")
}
ip := n.IP()
if ip.IsMulticast() || ip.IsUnspecified() {
return errors.New("invalid IP (multicast/unspecified)") return errors.New("invalid IP (multicast/unspecified)")
} }
if n.udp == 0 {
return errors.New("missing UDP port")
}
// Validate the node key (on curve, etc.). // Validate the node key (on curve, etc.).
var key Secp256k1 var key Secp256k1

View file

@ -21,6 +21,7 @@ import (
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"math/big" "math/big"
"net/netip"
"testing" "testing"
"testing/quick" "testing/quick"
@ -68,6 +69,167 @@ func TestPythonInterop(t *testing.T) {
} }
} }
func TestNodeEndpoints(t *testing.T) {
id := HexID("00000000000000806ad9b61fa5ae014307ebdc964253adcd9f2c0a392aa11abc")
type endpointTest struct {
name string
node *Node
wantIP netip.Addr
wantUDP int
wantTCP int
}
tests := []endpointTest{
{
name: "no-addr",
node: func() *Node {
var r enr.Record
return SignNull(&r, id)
}(),
},
{
name: "udp-only",
node: func() *Node {
var r enr.Record
r.Set(enr.UDP(9000))
return SignNull(&r, id)
}(),
},
{
name: "tcp-only",
node: func() *Node {
var r enr.Record
r.Set(enr.TCP(9000))
return SignNull(&r, id)
}(),
},
{
name: "ipv4-only-loopback",
node: func() *Node {
var r enr.Record
r.Set(enr.IPv4Addr(netip.MustParseAddr("127.0.0.1")))
return SignNull(&r, id)
}(),
wantIP: netip.MustParseAddr("127.0.0.1"),
},
{
name: "ipv4-only-unspecified",
node: func() *Node {
var r enr.Record
r.Set(enr.IPv4Addr(netip.MustParseAddr("0.0.0.0")))
return SignNull(&r, id)
}(),
wantIP: netip.MustParseAddr("0.0.0.0"),
},
{
name: "ipv4-only",
node: func() *Node {
var r enr.Record
r.Set(enr.IPv4Addr(netip.MustParseAddr("99.22.33.1")))
return SignNull(&r, id)
}(),
wantIP: netip.MustParseAddr("99.22.33.1"),
},
{
name: "ipv6-only",
node: func() *Node {
var r enr.Record
r.Set(enr.IPv6Addr(netip.MustParseAddr("2001::ff00:0042:8329")))
return SignNull(&r, id)
}(),
wantIP: netip.MustParseAddr("2001::ff00:0042:8329"),
},
{
name: "ipv4-loopback-and-ipv6-global",
node: func() *Node {
var r enr.Record
r.Set(enr.IPv4Addr(netip.MustParseAddr("127.0.0.1")))
r.Set(enr.UDP(30304))
r.Set(enr.IPv6Addr(netip.MustParseAddr("2001::ff00:0042:8329")))
r.Set(enr.UDP6(30306))
return SignNull(&r, id)
}(),
wantIP: netip.MustParseAddr("2001::ff00:0042:8329"),
wantUDP: 30306,
},
{
name: "ipv4-unspecified-and-ipv6-loopback",
node: func() *Node {
var r enr.Record
r.Set(enr.IPv4Addr(netip.MustParseAddr("0.0.0.0")))
r.Set(enr.IPv6Addr(netip.MustParseAddr("::1")))
return SignNull(&r, id)
}(),
wantIP: netip.MustParseAddr("::1"),
},
{
name: "ipv4-private-and-ipv6-global",
node: func() *Node {
var r enr.Record
r.Set(enr.IPv4Addr(netip.MustParseAddr("192.168.2.2")))
r.Set(enr.UDP(30304))
r.Set(enr.IPv6Addr(netip.MustParseAddr("2001::ff00:0042:8329")))
r.Set(enr.UDP6(30306))
return SignNull(&r, id)
}(),
wantIP: netip.MustParseAddr("2001::ff00:0042:8329"),
wantUDP: 30306,
},
{
name: "ipv4-local-and-ipv6-global",
node: func() *Node {
var r enr.Record
r.Set(enr.IPv4Addr(netip.MustParseAddr("169.254.2.6")))
r.Set(enr.UDP(30304))
r.Set(enr.IPv6Addr(netip.MustParseAddr("2001::ff00:0042:8329")))
r.Set(enr.UDP6(30306))
return SignNull(&r, id)
}(),
wantIP: netip.MustParseAddr("2001::ff00:0042:8329"),
wantUDP: 30306,
},
{
name: "ipv4-private-and-ipv6-private",
node: func() *Node {
var r enr.Record
r.Set(enr.IPv4Addr(netip.MustParseAddr("192.168.2.2")))
r.Set(enr.UDP(30304))
r.Set(enr.IPv6Addr(netip.MustParseAddr("fd00::abcd:1")))
r.Set(enr.UDP6(30306))
return SignNull(&r, id)
}(),
wantIP: netip.MustParseAddr("192.168.2.2"),
wantUDP: 30304,
},
{
name: "ipv4-private-and-ipv6-link-local",
node: func() *Node {
var r enr.Record
r.Set(enr.IPv4Addr(netip.MustParseAddr("192.168.2.2")))
r.Set(enr.UDP(30304))
r.Set(enr.IPv6Addr(netip.MustParseAddr("fe80::1")))
r.Set(enr.UDP6(30306))
return SignNull(&r, id)
}(),
wantIP: netip.MustParseAddr("192.168.2.2"),
wantUDP: 30304,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if test.wantIP != test.node.IPAddr() {
t.Errorf("node has wrong IP %v, want %v", test.node.IPAddr(), test.wantIP)
}
if test.wantUDP != test.node.UDP() {
t.Errorf("node has wrong UDP port %d, want %d", test.node.UDP(), test.wantUDP)
}
if test.wantTCP != test.node.TCP() {
t.Errorf("node has wrong TCP port %d, want %d", test.node.TCP(), test.wantTCP)
}
})
}
}
func TestHexID(t *testing.T) { func TestHexID(t *testing.T) {
ref := ID{0, 0, 0, 0, 0, 0, 0, 128, 106, 217, 182, 31, 165, 174, 1, 67, 7, 235, 220, 150, 66, 83, 173, 205, 159, 44, 10, 57, 42, 161, 26, 188} ref := ID{0, 0, 0, 0, 0, 0, 0, 128, 106, 217, 182, 31, 165, 174, 1, 67, 7, 235, 220, 150, 66, 83, 173, 205, 159, 44, 10, 57, 42, 161, 26, 188}
id1 := HexID("0x00000000000000806ad9b61fa5ae014307ebdc964253adcd9f2c0a392aa11abc") id1 := HexID("0x00000000000000806ad9b61fa5ae014307ebdc964253adcd9f2c0a392aa11abc")

View file

@ -26,6 +26,7 @@ import (
"sync" "sync"
"time" "time"
"github.com/ethereum/go-ethereum/p2p/enr"
"github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rlp"
"github.com/syndtr/goleveldb/leveldb" "github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/errors" "github.com/syndtr/goleveldb/leveldb/errors"
@ -264,14 +265,14 @@ func (db *DB) Node(id ID) *Node {
} }
func mustDecodeNode(id, data []byte) *Node { func mustDecodeNode(id, data []byte) *Node {
node := new(Node) var r enr.Record
if err := rlp.DecodeBytes(data, &node.r); err != nil { if err := rlp.DecodeBytes(data, &r); err != nil {
panic(fmt.Errorf("p2p/enode: can't decode node %x in DB: %v", id, err)) panic(fmt.Errorf("p2p/enode: can't decode node %x in DB: %v", id, err))
} }
// Restore node id cache. if len(id) != len(ID{}) {
copy(node.id[:], id) panic(fmt.Errorf("invalid id length %d", len(id)))
}
return node return newNodeWithID(&r, ID(id))
} }
// UpdateNode inserts - potentially overwriting - a node into the peer database. // UpdateNode inserts - potentially overwriting - a node into the peer database.

View file

@ -201,7 +201,7 @@ func (n *Node) URLv4() string {
} }
u := url.URL{Scheme: "enode"} u := url.URL{Scheme: "enode"}
if n.Incomplete() { if !n.ip.IsValid() {
u.Host = nodeid u.Host = nodeid
} else { } else {
addr := net.TCPAddr{IP: n.IP(), Port: n.TCP()} addr := net.TCPAddr{IP: n.IP(), Port: n.TCP()}

View file

@ -21,6 +21,7 @@ import (
"fmt" "fmt"
"io" "io"
"net" "net"
"net/netip"
"github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rlp"
) )
@ -178,6 +179,60 @@ func (v *IPv6) DecodeRLP(s *rlp.Stream) error {
return nil return nil
} }
// IPv4Addr is the "ip" key, which holds the IP address of the node.
type IPv4Addr netip.Addr
func (v IPv4Addr) ENRKey() string { return "ip" }
// EncodeRLP implements rlp.Encoder.
func (v IPv4Addr) EncodeRLP(w io.Writer) error {
addr := netip.Addr(v)
if !addr.Is4() {
return fmt.Errorf("address is not IPv4")
}
enc := rlp.NewEncoderBuffer(w)
bytes := addr.As4()
enc.WriteBytes(bytes[:])
return enc.Flush()
}
// DecodeRLP implements rlp.Decoder.
func (v *IPv4Addr) DecodeRLP(s *rlp.Stream) error {
var bytes [4]byte
if err := s.ReadBytes(bytes[:]); err != nil {
return err
}
*v = IPv4Addr(netip.AddrFrom4(bytes))
return nil
}
// IPv6Addr is the "ip6" key, which holds the IP address of the node.
type IPv6Addr netip.Addr
func (v IPv6Addr) ENRKey() string { return "ip6" }
// EncodeRLP implements rlp.Encoder.
func (v IPv6Addr) EncodeRLP(w io.Writer) error {
addr := netip.Addr(v)
if !addr.Is6() {
return fmt.Errorf("address is not IPv6")
}
enc := rlp.NewEncoderBuffer(w)
bytes := addr.As16()
enc.WriteBytes(bytes[:])
return enc.Flush()
}
// DecodeRLP implements rlp.Decoder.
func (v *IPv6Addr) DecodeRLP(s *rlp.Stream) error {
var bytes [16]byte
if err := s.ReadBytes(bytes[:]); err != nil {
return err
}
*v = IPv6Addr(netip.AddrFrom16(bytes))
return nil
}
// KeyError is an error related to a key. // KeyError is an error related to a key.
type KeyError struct { type KeyError struct {
Key string Key string

View file

@ -24,6 +24,8 @@ import (
"errors" "errors"
"fmt" "fmt"
"net" "net"
"net/netip"
"slices"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
@ -38,7 +40,6 @@ import (
"github.com/ethereum/go-ethereum/p2p/enr" "github.com/ethereum/go-ethereum/p2p/enr"
"github.com/ethereum/go-ethereum/p2p/nat" "github.com/ethereum/go-ethereum/p2p/nat"
"github.com/ethereum/go-ethereum/p2p/netutil" "github.com/ethereum/go-ethereum/p2p/netutil"
"golang.org/x/exp/slices"
) )
const ( const (
@ -194,8 +195,8 @@ type Server struct {
nodedb *enode.DB nodedb *enode.DB
localnode *enode.LocalNode localnode *enode.LocalNode
ntab *discover.UDPv4 discv4 *discover.UDPv4
DiscV5 *discover.UDPv5 discv5 *discover.UDPv5
discmix *enode.FairMix discmix *enode.FairMix
dialsched *dialScheduler dialsched *dialScheduler
@ -456,6 +457,16 @@ func (srv *Server) Self() *enode.Node {
return ln.Node() return ln.Node()
} }
// DiscoveryV4 returns the discovery v4 instance, if configured.
func (srv *Server) DiscoveryV4() *discover.UDPv4 {
return srv.discv4
}
// DiscoveryV4 returns the discovery v5 instance, if configured.
func (srv *Server) DiscoveryV5() *discover.UDPv5 {
return srv.discv5
}
// Stop terminates the server and all active peer connections. // Stop terminates the server and all active peer connections.
// It blocks until all active connections have been closed. // It blocks until all active connections have been closed.
func (srv *Server) Stop() { func (srv *Server) Stop() {
@ -483,11 +494,11 @@ type sharedUDPConn struct {
unhandled chan discover.ReadPacket unhandled chan discover.ReadPacket
} }
// ReadFromUDP implements discover.UDPConn // ReadFromUDPAddrPort implements discover.UDPConn
func (s *sharedUDPConn) ReadFromUDP(b []byte) (n int, addr *net.UDPAddr, err error) { func (s *sharedUDPConn) ReadFromUDPAddrPort(b []byte) (n int, addr netip.AddrPort, err error) {
packet, ok := <-s.unhandled packet, ok := <-s.unhandled
if !ok { if !ok {
return 0, nil, errors.New("connection was closed") return 0, netip.AddrPort{}, errors.New("connection was closed")
} }
l := len(packet.Data) l := len(packet.Data)
@ -621,13 +632,13 @@ func (srv *Server) setupDiscovery() error {
) )
// If both versions of discovery are running, setup a shared // If both versions of discovery are running, setup a shared
// connection, so v5 can read unhandled messages from v4. // connection, so v5 can read unhandled messages from v4.
if srv.DiscoveryV4 && srv.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 = &sharedUDPConn{conn, unhandled}
} }
// Start discovery services. // Start discovery services.
if srv.DiscoveryV4 { if srv.Config.DiscoveryV4 {
cfg := discover.Config{ cfg := discover.Config{
PrivateKey: srv.PrivateKey, PrivateKey: srv.PrivateKey,
NetRestrict: srv.NetRestrict, NetRestrict: srv.NetRestrict,
@ -640,18 +651,17 @@ func (srv *Server) setupDiscovery() error {
if err != nil { if err != nil {
return err return err
} }
srv.discv4 = ntab
srv.ntab = ntab
srv.discmix.AddSource(ntab.RandomNodes()) srv.discmix.AddSource(ntab.RandomNodes())
} }
if srv.DiscoveryV5 { if srv.Config.DiscoveryV5 {
cfg := discover.Config{ cfg := discover.Config{
PrivateKey: srv.PrivateKey, PrivateKey: srv.PrivateKey,
NetRestrict: srv.NetRestrict, NetRestrict: srv.NetRestrict,
Bootnodes: srv.BootstrapNodesV5, Bootnodes: srv.BootstrapNodesV5,
Log: srv.log, Log: srv.log,
} }
srv.DiscV5, err = discover.ListenV5(sconn, srv.localnode, cfg) srv.discv5, err = discover.ListenV5(sconn, srv.localnode, cfg)
if err != nil { if err != nil {
return err return err
} }
@ -678,8 +688,8 @@ func (srv *Server) setupDialScheduler() {
dialer: srv.Dialer, dialer: srv.Dialer,
clock: srv.clock, clock: srv.clock,
} }
if srv.ntab != nil { if srv.discv4 != nil {
config.resolver = srv.ntab config.resolver = srv.discv4
} }
if config.dialer == nil { if config.dialer == nil {
@ -877,12 +887,11 @@ running:
srv.log.Trace("P2P networking is spinning down") srv.log.Trace("P2P networking is spinning down")
// Terminate discovery. If there is a running lookup it will terminate soon. // Terminate discovery. If there is a running lookup it will terminate soon.
if srv.ntab != nil { if srv.discv4 != nil {
srv.ntab.Close() srv.discv4.Close()
} }
if srv.discv5 != nil {
if srv.DiscV5 != nil { srv.discv5.Close()
srv.DiscV5.Close()
} }
// Disconnect all peers. // Disconnect all peers.
for _, p := range peers { for _, p := range peers {