go-waku/waku/v2/discv5/discover.go

493 lines
12 KiB
Go
Raw Normal View History

2021-11-16 14:22:01 +00:00
package discv5
import (
"context"
"crypto/ecdsa"
"errors"
"fmt"
2021-11-16 14:22:01 +00:00
"net"
"time"
2021-11-16 14:22:01 +00:00
2022-10-19 19:39:32 +00:00
"github.com/libp2p/go-libp2p/core/host"
"github.com/libp2p/go-libp2p/core/peer"
"github.com/multiformats/go-multiaddr"
"github.com/prometheus/client_golang/prometheus"
"github.com/waku-org/go-discover/discover"
"github.com/waku-org/go-waku/logging"
"github.com/waku-org/go-waku/waku/v2/peermanager"
"github.com/waku-org/go-waku/waku/v2/peerstore"
2023-07-27 18:14:14 +00:00
wenr "github.com/waku-org/go-waku/waku/v2/protocol/enr"
"github.com/waku-org/go-waku/waku/v2/utils"
"go.uber.org/zap"
2022-12-09 18:09:06 +00:00
"github.com/ethereum/go-ethereum/p2p/enode"
"github.com/ethereum/go-ethereum/p2p/enr"
2022-12-09 18:09:06 +00:00
"github.com/ethereum/go-ethereum/p2p/nat"
2021-11-16 14:22:01 +00:00
)
2023-02-07 22:27:22 +00:00
var ErrNoDiscV5Listener = errors.New("no discv5 listener")
2023-07-31 18:58:50 +00:00
// PeerConnector will subscribe to a channel containing the information for all peers found by this discovery protocol
type PeerConnector interface {
Subscribe(context.Context, <-chan peermanager.PeerData)
}
2021-11-16 14:22:01 +00:00
type DiscoveryV5 struct {
params *discV5Parameters
host host.Host
config discover.Config
udpAddr *net.UDPAddr
listener *discover.UDPv5
localnode *enode.LocalNode
metrics Metrics
peerConnector PeerConnector
NAT nat.Interface
2021-11-16 14:22:01 +00:00
log *zap.Logger
*peermanager.CommonDiscoveryService
2021-11-16 14:22:01 +00:00
}
type discV5Parameters struct {
2021-11-17 16:19:42 +00:00
autoUpdate bool
autoFindPeers bool
bootnodes map[enode.ID]*enode.Node
udpPort uint
advertiseAddr []multiaddr.Multiaddr
loopPredicate func(*enode.Node) bool
2021-11-16 14:22:01 +00:00
}
type DiscoveryV5Option func(*discV5Parameters)
var protocolID = [6]byte{'d', '5', 'w', 'a', 'k', 'u'}
2023-07-07 15:51:15 +00:00
const peerDelay = 100 * time.Millisecond
const bucketSize = 16
const delayBetweenDiscoveredPeerCnt = 5 * time.Second
2021-11-17 16:19:42 +00:00
func WithAutoUpdate(autoUpdate bool) DiscoveryV5Option {
return func(params *discV5Parameters) {
params.autoUpdate = autoUpdate
}
}
// WithBootnodes is an option used to specify the bootstrap nodes to use with DiscV5
2021-11-16 14:22:01 +00:00
func WithBootnodes(bootnodes []*enode.Node) DiscoveryV5Option {
return func(params *discV5Parameters) {
params.bootnodes = make(map[enode.ID]*enode.Node)
for _, b := range bootnodes {
params.bootnodes[b.ID()] = b
}
2021-11-16 14:22:01 +00:00
}
}
func WithAdvertiseAddr(addr []multiaddr.Multiaddr) DiscoveryV5Option {
2021-11-16 14:22:01 +00:00
return func(params *discV5Parameters) {
params.advertiseAddr = addr
2021-11-16 14:22:01 +00:00
}
}
func WithUDPPort(port uint) DiscoveryV5Option {
2021-11-16 14:22:01 +00:00
return func(params *discV5Parameters) {
params.udpPort = port
}
}
func WithPredicate(predicate func(*enode.Node) bool) DiscoveryV5Option {
return func(params *discV5Parameters) {
params.loopPredicate = predicate
}
}
func WithAutoFindPeers(find bool) DiscoveryV5Option {
return func(params *discV5Parameters) {
params.autoFindPeers = find
}
}
// DefaultOptions contains the default list of options used when setting up DiscoveryV5
2021-11-16 14:22:01 +00:00
func DefaultOptions() []DiscoveryV5Option {
return []DiscoveryV5Option{
WithUDPPort(9000),
WithAutoFindPeers(true),
2021-11-16 14:22:01 +00:00
}
}
// NewDiscoveryV5 returns a new instance of a DiscoveryV5 struct
func NewDiscoveryV5(priv *ecdsa.PrivateKey, localnode *enode.LocalNode, peerConnector PeerConnector, reg prometheus.Registerer, log *zap.Logger, opts ...DiscoveryV5Option) (*DiscoveryV5, error) {
2021-11-16 14:22:01 +00:00
params := new(discV5Parameters)
optList := DefaultOptions()
optList = append(optList, opts...)
for _, opt := range optList {
opt(params)
}
logger := log.Named("discv5")
var NAT nat.Interface
if params.advertiseAddr == nil {
NAT = nat.Any()
}
var bootnodes []*enode.Node
for _, bootnode := range params.bootnodes {
bootnodes = append(bootnodes, bootnode)
}
2021-11-16 14:22:01 +00:00
return &DiscoveryV5{
params: params,
peerConnector: peerConnector,
NAT: NAT,
CommonDiscoveryService: peermanager.NewCommonDiscoveryService(),
localnode: localnode,
metrics: newMetrics(reg),
2021-11-16 14:22:01 +00:00
config: discover.Config{
PrivateKey: priv,
Bootnodes: bootnodes,
V5Config: discover.V5Config{
ProtocolID: &protocolID,
},
2021-11-16 14:22:01 +00:00
},
udpAddr: &net.UDPAddr{
2021-11-17 16:19:42 +00:00
IP: net.IPv4zero,
Port: int(params.udpPort),
2021-11-16 14:22:01 +00:00
},
2022-06-13 18:30:35 +00:00
log: logger,
2021-11-16 14:22:01 +00:00
}, nil
}
2022-10-23 13:13:43 +00:00
func (d *DiscoveryV5) Node() *enode.Node {
return d.localnode.Node()
}
func (d *DiscoveryV5) listen(ctx context.Context) error {
2021-11-16 14:22:01 +00:00
conn, err := net.ListenUDP("udp", d.udpAddr)
if err != nil {
return err
}
d.udpAddr = conn.LocalAddr().(*net.UDPAddr)
if d.NAT != nil && !d.udpAddr.IP.IsLoopback() {
d.WaitGroup().Add(1)
go func() {
defer d.WaitGroup().Done()
nat.Map(d.NAT, ctx.Done(), "udp", d.udpAddr.Port, d.udpAddr.Port, "go-waku discv5 discovery")
}()
}
d.localnode.SetFallbackUDP(d.udpAddr.Port)
2021-11-16 14:22:01 +00:00
listener, err := discover.ListenV5(conn, d.localnode, d.config)
if err != nil {
return err
}
d.listener = listener
d.log.Info("started Discovery V5",
zap.Stringer("listening", d.udpAddr),
logging.TCPAddr("advertising", d.localnode.Node().IP(), d.localnode.Node().TCP()))
d.log.Info("Discovery V5: discoverable ENR ", logging.ENode("enr", d.localnode.Node()))
2021-11-17 16:19:42 +00:00
return nil
}
2023-04-17 00:04:12 +00:00
// Sets the host to be able to mount or consume a protocol
func (d *DiscoveryV5) SetHost(h host.Host) {
d.host = h
}
// only works if the discovery v5 hasn't been started yet.
func (d *DiscoveryV5) Start(ctx context.Context) error {
return d.CommonDiscoveryService.Start(ctx, d.start)
}
func (d *DiscoveryV5) start() error {
d.peerConnector.Subscribe(d.Context(), d.GetListeningChan())
err := d.listen(d.Context())
2021-11-17 16:19:42 +00:00
if err != nil {
return err
}
if d.params.autoFindPeers {
d.WaitGroup().Add(1)
go func() {
defer d.WaitGroup().Done()
d.runDiscoveryV5Loop(d.Context())
}()
}
2021-11-16 14:22:01 +00:00
return nil
}
// SetBootnodes is used to setup the bootstrap nodes to use for discovering new peers
2022-12-09 17:50:20 +00:00
func (d *DiscoveryV5) SetBootnodes(nodes []*enode.Node) error {
2023-02-07 22:27:22 +00:00
if d.listener == nil {
return ErrNoDiscV5Listener
}
2022-12-09 18:09:06 +00:00
return d.listener.SetFallbackNodes(nodes)
2022-12-09 17:50:20 +00:00
}
// Stop is a function that stops the execution of DiscV5.
// only works if the discovery v5 is in running state
// so we can assume that cancel method is set
2021-11-16 14:22:01 +00:00
func (d *DiscoveryV5) Stop() {
2023-08-28 05:45:26 +00:00
defer func() {
if r := recover(); r != nil {
d.log.Info("recovering from panic and quitting")
}
}()
d.CommonDiscoveryService.Stop(func() {
if d.listener != nil {
d.listener.Close()
d.listener = nil
d.log.Info("stopped Discovery V5")
}
})
2021-11-17 16:19:42 +00:00
}
2021-11-16 14:22:01 +00:00
func isWakuNode(node *enode.Node) bool {
enrField := new(wenr.WakuEnrBitfield)
if err := node.Record().Load(enr.WithEntry(wenr.WakuENRField, &enrField)); err != nil {
2021-11-16 14:22:01 +00:00
if !enr.IsNotFound(err) {
utils.Logger().Named("discv5").Error("could not retrieve waku2 ENR field for enr ", zap.Any("node", node))
2021-11-16 14:22:01 +00:00
}
return false
}
if enrField != nil {
return *enrField != uint8(0) // #RFC 31 requirement
2021-11-16 14:22:01 +00:00
}
return false
}
func (d *DiscoveryV5) evaluateNode() func(node *enode.Node) bool {
return func(node *enode.Node) bool {
if node == nil {
return false
}
2021-11-16 14:22:01 +00:00
// node filtering based on ENR; we do not filter based on ENR in the first waku discv5 beta stage
if !isWakuNode(node) {
return false
}
2021-11-16 14:22:01 +00:00
_, err := wenr.EnodeToPeerInfo(node)
2021-11-17 16:19:42 +00:00
if err != nil {
d.metrics.RecordError(peerInfoFailure)
utils.Logger().Named("discv5").Error("obtaining peer info from enode", logging.ENode("enr", node), zap.Error(err))
return false
}
2021-11-16 14:22:01 +00:00
return true
}
2021-11-16 14:22:01 +00:00
}
2023-07-27 18:14:14 +00:00
// Predicate is a function that is applied to an iterator to filter the nodes to be retrieved according to some logic
type Predicate func(enode.Iterator) enode.Iterator
// PeerIterator gets random nodes from DHT via discv5 listener.
// Used for caching enr address in peerExchange
// Used for connecting to peers in discovery_connector
func (d *DiscoveryV5) PeerIterator(predicate ...Predicate) (enode.Iterator, error) {
if d.listener == nil {
2023-02-07 22:27:22 +00:00
return nil, ErrNoDiscV5Listener
2021-11-16 14:22:01 +00:00
}
iterator := enode.Filter(d.listener.RandomNodes(), d.evaluateNode())
if d.params.loopPredicate != nil {
2023-07-27 18:14:14 +00:00
iterator = enode.Filter(iterator, d.params.loopPredicate)
}
2023-07-27 18:14:14 +00:00
for _, p := range predicate {
iterator = p(iterator)
}
return iterator, nil
}
func (d *DiscoveryV5) Iterate(ctx context.Context, iterator enode.Iterator, onNode func(*enode.Node, peer.AddrInfo) error) {
defer iterator.Close()
2021-11-16 14:22:01 +00:00
peerCnt := 0
for {
2023-07-07 15:51:15 +00:00
if !delayedHasNext(ctx, iterator) {
return
}
peerCnt++
2023-07-07 15:51:15 +00:00
if peerCnt == bucketSize { // Delay every bucketSize peers discovered
peerCnt = 0
t := time.NewTimer(delayBetweenDiscoveredPeerCnt)
select {
case <-ctx.Done():
return
case <-t.C:
t.Stop()
}
}
2023-07-27 18:14:14 +00:00
_, addresses, err := wenr.Multiaddress(iterator.Node())
2021-11-16 14:22:01 +00:00
if err != nil {
d.metrics.RecordError(peerInfoFailure)
d.log.Error("extracting multiaddrs from enr", zap.Error(err))
2021-11-16 14:22:01 +00:00
continue
}
peerAddrs, err := peer.AddrInfosFromP2pAddrs(addresses...)
2021-11-16 14:22:01 +00:00
if err != nil {
d.metrics.RecordError(peerInfoFailure)
d.log.Error("converting multiaddrs to addrinfos", zap.Error(err))
2021-11-16 14:22:01 +00:00
continue
}
if len(peerAddrs) != 0 {
err := onNode(iterator.Node(), peerAddrs[0])
if err != nil {
d.log.Error("processing node", zap.Error(err))
}
}
select {
case <-ctx.Done():
return
default:
2021-11-16 14:22:01 +00:00
}
}
}
2023-07-07 15:51:15 +00:00
func delayedHasNext(ctx context.Context, iterator enode.Iterator) bool {
// Delay if .Next() is too fast
start := time.Now()
hasNext := iterator.Next()
if !hasNext {
return false
}
elapsed := time.Since(start)
if elapsed < peerDelay {
t := time.NewTimer(peerDelay - elapsed)
select {
case <-ctx.Done():
return false
case <-t.C:
t.Stop()
}
}
return true
}
// DefaultPredicate contains the conditions to be applied when filtering peers discovered via discv5
func (d *DiscoveryV5) DefaultPredicate() Predicate {
return FilterPredicate(func(n *enode.Node) bool {
2023-07-27 18:14:14 +00:00
localRS, err := wenr.RelaySharding(d.localnode.Node().Record())
if err != nil {
2023-06-20 20:39:20 +00:00
return false
}
if localRS == nil { // No shard registered, so no need to check for shards
return true
}
if _, ok := d.params.bootnodes[n.ID()]; ok {
return true // The record is a bootnode. Assume it's valid and dont filter it out
}
2023-07-27 18:14:14 +00:00
nodeRS, err := wenr.RelaySharding(n.Record())
if err != nil {
2023-06-20 20:39:20 +00:00
return false
}
if nodeRS == nil {
2023-10-15 19:16:40 +00:00
// Node has no shards registered.
return false
}
if nodeRS.ClusterID != localRS.ClusterID {
2023-06-20 20:39:20 +00:00
return false
}
// Contains any
for _, idx := range localRS.ShardIDs {
if nodeRS.Contains(localRS.ClusterID, idx) {
2023-06-20 20:39:20 +00:00
return true
}
}
return false
})
}
// Iterates over the nodes found via discv5 belonging to the node's current shard, and sends them to peerConnector
func (d *DiscoveryV5) peerLoop(ctx context.Context) error {
iterator, err := d.PeerIterator(d.DefaultPredicate())
2023-07-27 18:14:14 +00:00
if err != nil {
d.metrics.RecordError(iteratorFailure)
2023-07-27 18:14:14 +00:00
return fmt.Errorf("obtaining iterator: %w", err)
}
2023-06-20 20:39:20 +00:00
defer iterator.Close()
d.Iterate(ctx, iterator, func(n *enode.Node, p peer.AddrInfo) error {
peer := peermanager.PeerData{
Origin: peerstore.Discv5,
AddrInfo: p,
ENR: n,
}
if d.PushToChan(peer) {
2023-08-28 05:45:26 +00:00
d.log.Debug("published peer into peer channel", logging.HostID("peerID", peer.AddrInfo.ID))
} else {
d.log.Debug("could not publish peer into peer channel", logging.HostID("peerID", peer.AddrInfo.ID))
}
return nil
})
return nil
2021-11-16 14:22:01 +00:00
}
func (d *DiscoveryV5) runDiscoveryV5Loop(ctx context.Context) {
if len(d.config.Bootnodes) > 0 {
localRS, err := wenr.RelaySharding(d.localnode.Node().Record())
if err == nil && localRS != nil {
iterator := d.DefaultPredicate()(enode.IterNodes(d.config.Bootnodes))
validBootCount := 0
for iterator.Next() {
validBootCount++
}
if validBootCount == 0 {
d.log.Warn("no discv5 bootstrap nodes share this node configured shards")
}
}
}
2021-11-16 14:22:01 +00:00
restartLoop:
for {
err := d.peerLoop(ctx)
if err != nil {
d.log.Debug("iterating discv5", zap.Error(err))
}
2023-05-08 16:29:18 +00:00
t := time.NewTimer(5 * time.Second)
select {
2023-05-08 16:29:18 +00:00
case <-t.C:
t.Stop()
case <-ctx.Done():
2023-05-08 16:29:18 +00:00
t.Stop()
break restartLoop
2021-11-16 14:22:01 +00:00
}
2022-10-23 13:13:43 +00:00
}
d.log.Warn("Discv5 loop stopped")
2021-11-16 14:22:01 +00:00
}