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

441 lines
9.1 KiB
Go
Raw Normal View History

2021-11-16 14:22:01 +00:00
package discv5
import (
"context"
"crypto/ecdsa"
"math/rand"
"net"
"sync"
"time"
"github.com/ethereum/go-ethereum/p2p/enode"
"github.com/ethereum/go-ethereum/p2p/enr"
"github.com/ethereum/go-ethereum/p2p/nat"
2022-10-19 19:39:32 +00:00
"github.com/libp2p/go-libp2p/core/discovery"
"github.com/libp2p/go-libp2p/core/host"
"github.com/libp2p/go-libp2p/core/peer"
"github.com/waku-org/go-discover/discover"
"github.com/waku-org/go-waku/logging"
"github.com/waku-org/go-waku/waku/v2/utils"
"go.uber.org/zap"
2021-11-16 14:22:01 +00:00
)
type DiscoveryV5 struct {
2022-10-23 13:13:43 +00:00
sync.RWMutex
2021-11-17 16:19:42 +00:00
2021-11-16 14:22:01 +00:00
discovery.Discovery
params *discV5Parameters
ctx context.Context
2021-11-16 14:22:01 +00:00
host host.Host
config discover.Config
udpAddr *net.UDPAddr
listener *discover.UDPv5
localnode *enode.LocalNode
NAT nat.Interface
quit chan struct{}
2022-10-23 13:13:43 +00:00
started bool
2021-11-16 14:22:01 +00:00
log *zap.Logger
wg *sync.WaitGroup
peerCache peerCache
discoverCtx context.Context
discoverCancelFunc context.CancelFunc
2021-11-16 14:22:01 +00:00
}
type peerCache struct {
sync.RWMutex
2022-10-23 13:13:43 +00:00
recs map[peer.ID]PeerRecord
2021-11-16 14:22:01 +00:00
rng *rand.Rand
}
2022-10-23 13:13:43 +00:00
type PeerRecord struct {
2021-11-16 14:22:01 +00:00
expire int64
2022-10-23 13:13:43 +00:00
Peer peer.AddrInfo
Node enode.Node
2021-11-16 14:22:01 +00:00
}
type discV5Parameters struct {
2021-11-17 16:19:42 +00:00
autoUpdate bool
bootnodes []*enode.Node
udpPort int
advertiseAddr *net.IP
2021-11-16 14:22:01 +00:00
}
type DiscoveryV5Option func(*discV5Parameters)
var protocolID = [6]byte{'d', '5', 'w', 'a', 'k', 'u'}
2021-11-17 16:19:42 +00:00
func WithAutoUpdate(autoUpdate bool) DiscoveryV5Option {
return func(params *discV5Parameters) {
params.autoUpdate = autoUpdate
}
}
2021-11-16 14:22:01 +00:00
func WithBootnodes(bootnodes []*enode.Node) DiscoveryV5Option {
return func(params *discV5Parameters) {
params.bootnodes = bootnodes
}
}
2021-11-17 16:19:42 +00:00
func WithAdvertiseAddr(addr net.IP) DiscoveryV5Option {
2021-11-16 14:22:01 +00:00
return func(params *discV5Parameters) {
2021-11-17 16:19:42 +00:00
params.advertiseAddr = &addr
2021-11-16 14:22:01 +00:00
}
}
func WithUDPPort(port int) DiscoveryV5Option {
return func(params *discV5Parameters) {
params.udpPort = port
}
}
func DefaultOptions() []DiscoveryV5Option {
return []DiscoveryV5Option{
WithUDPPort(9000),
}
}
const MaxPeersToDiscover = 600
func NewDiscoveryV5(ctx context.Context, host host.Host, priv *ecdsa.PrivateKey, localnode *enode.LocalNode, 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 = nil
if params.advertiseAddr == nil {
NAT = nat.Any()
}
2021-11-16 14:22:01 +00:00
return &DiscoveryV5{
ctx: ctx,
2021-11-16 14:22:01 +00:00
host: host,
params: params,
NAT: NAT,
wg: &sync.WaitGroup{},
2021-11-16 14:22:01 +00:00
peerCache: peerCache{
rng: rand.New(rand.NewSource(rand.Int63())),
2022-10-23 13:13:43 +00:00
recs: make(map[peer.ID]PeerRecord),
2021-11-16 14:22:01 +00:00
},
localnode: localnode,
config: discover.Config{
PrivateKey: priv,
Bootnodes: params.bootnodes,
ValidNodeFn: func(n enode.Node) bool {
// TODO: track https://github.com/status-im/nim-waku/issues/770 for improvements over validation func
return evaluateNode(&n)
},
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,
2021-11-16 14:22:01 +00:00
Port: params.udpPort,
},
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()
}
2021-11-17 16:19:42 +00:00
func (d *DiscoveryV5) listen() 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.wg.Add(1)
go func() {
defer d.wg.Done()
nat.Map(d.NAT, d.quit, "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
}
func (d *DiscoveryV5) Start() error {
d.Lock()
defer d.Unlock()
2022-10-23 13:13:43 +00:00
if d.started {
return nil
}
d.wg.Wait() // Waiting for other go routines to stop
d.quit = make(chan struct{}, 1)
2022-10-23 13:13:43 +00:00
d.started = true
2021-11-17 16:19:42 +00:00
err := d.listen()
if err != nil {
return err
}
// create cancellable
d.discoverCtx, d.discoverCancelFunc = context.WithCancel(d.ctx)
go d.runDiscoveryV5Loop()
2021-11-16 14:22:01 +00:00
return nil
}
2022-12-09 17:50:20 +00:00
func (d *DiscoveryV5) SetBootnodes(nodes []*enode.Node) error {
return d.listener.SetFallbackNodes(nodes)
}
2021-11-16 14:22:01 +00:00
func (d *DiscoveryV5) Stop() {
2021-11-17 16:19:42 +00:00
d.Lock()
defer d.Unlock()
2022-10-23 13:13:43 +00:00
if !d.started {
return
}
close(d.quit)
d.discoverCancelFunc()
2021-11-16 14:22:01 +00:00
d.listener.Close()
2021-11-17 16:19:42 +00:00
d.listener = nil
2022-10-23 13:13:43 +00:00
d.started = false
d.log.Info("stopped Discovery V5")
d.wg.Wait()
2021-11-17 16:19:42 +00:00
}
2022-03-18 19:50:10 +00:00
/*
2021-11-16 14:22:01 +00:00
func isWakuNode(node *enode.Node) bool {
enrField := new(utils.WakuEnrBitfield)
if err := node.Record().Load(enr.WithEntry(utils.WakuENRField, &enrField)); err != nil {
2021-11-16 14:22:01 +00:00
if !enr.IsNotFound(err) {
utils.Logger().Named("discv5").Error("could not retrieve port for enr ", zap.Any("node", node))
2021-11-16 14:22:01 +00:00
}
return false
}
if enrField != nil {
return *enrField != uint8(0)
}
return false
}
2022-03-18 19:50:10 +00:00
*/
2021-11-16 14:22:01 +00:00
func hasTCPPort(node *enode.Node) bool {
enrTCP := new(enr.TCP)
if err := node.Record().Load(enr.WithEntry(enrTCP.ENRKey(), enrTCP)); err != nil {
if !enr.IsNotFound(err) {
utils.Logger().Named("discv5").Error("retrieving port for enr", logging.ENode("enr", node))
2021-11-16 14:22:01 +00:00
}
return false
}
return true
}
func evaluateNode(node *enode.Node) bool {
2021-11-16 14:22:01 +00:00
if node == nil || node.IP() == nil {
return false
}
2022-03-10 22:14:50 +00:00
// TODO: consider node filtering based on ENR; we do not filter based on ENR in the first waku discv5 beta stage
if /*!isWakuNode(node) ||*/ !hasTCPPort(node) {
2021-11-16 14:22:01 +00:00
return false
}
_, err := utils.EnodeToPeerInfo(node)
2021-11-17 16:19:42 +00:00
2021-11-16 14:22:01 +00:00
if err != nil {
utils.Logger().Named("discv5").Error("obtaining peer info from enode", logging.ENode("enr", node), zap.Error(err))
2021-11-16 14:22:01 +00:00
return false
}
return true
}
func (d *DiscoveryV5) Advertise(ctx context.Context, ns string, opts ...discovery.Option) (time.Duration, error) {
2021-11-16 14:22:01 +00:00
// Get options
var options discovery.Options
err := options.Apply(opts...)
if err != nil {
return 0, err
}
// TODO: once discv5 spec introduces capability and topic discovery, implement this function
return 20 * time.Minute, nil
}
func (d *DiscoveryV5) iterate(iterator enode.Iterator, limit int, doneCh chan struct{}) {
defer d.wg.Done()
2021-11-16 14:22:01 +00:00
for {
if len(d.peerCache.recs) >= limit {
break
}
if d.discoverCtx.Err() != nil {
2021-11-17 16:19:42 +00:00
break
}
2021-11-16 14:22:01 +00:00
exists := iterator.Next()
if !exists {
break
}
addresses, err := utils.Multiaddress(iterator.Node())
2021-11-16 14:22:01 +00:00
if err != nil {
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.log.Error("converting multiaddrs to addrinfos", zap.Error(err))
2021-11-16 14:22:01 +00:00
continue
}
d.peerCache.Lock()
for _, p := range peerAddrs {
2022-10-23 13:13:43 +00:00
d.peerCache.recs[p.ID] = PeerRecord{
expire: time.Now().Unix() + 3600, // Expires in 1hr
2022-10-23 13:13:43 +00:00
Peer: p,
Node: *iterator.Node(),
}
2021-11-16 14:22:01 +00:00
}
d.peerCache.Unlock()
2021-11-16 14:22:01 +00:00
}
close(doneCh)
}
func (d *DiscoveryV5) removeExpiredPeers() int {
// Remove all expired entries from cache
currentTime := time.Now().Unix()
newCacheSize := len(d.peerCache.recs)
for p := range d.peerCache.recs {
rec := d.peerCache.recs[p]
if rec.expire < currentTime {
newCacheSize--
delete(d.peerCache.recs, p)
}
}
return newCacheSize
}
func (d *DiscoveryV5) runDiscoveryV5Loop() {
iterator := d.listener.RandomNodes()
iterator = enode.Filter(iterator, evaluateNode)
defer iterator.Close()
doneCh := make(chan struct{})
d.wg.Add(1)
go d.iterate(iterator, MaxPeersToDiscover, doneCh)
select {
case <-d.discoverCtx.Done():
case <-doneCh:
}
d.log.Warn("Discv5 loop stopped")
}
2022-10-23 13:13:43 +00:00
func (d *DiscoveryV5) FindNodes(ctx context.Context, topic string, opts ...discovery.Option) ([]PeerRecord, error) {
2021-11-16 14:22:01 +00:00
// Get options
var options discovery.Options
err := options.Apply(opts...)
if err != nil {
return nil, err
}
limit := options.Limit
if limit == 0 || limit > MaxPeersToDiscover {
limit = MaxPeersToDiscover
}
2021-11-16 14:22:01 +00:00
// We are ignoring the topic. Future versions might use a map[string]*peerCache instead where the string represents the pubsub topic
d.peerCache.Lock()
defer d.peerCache.Unlock()
d.removeExpiredPeers()
2021-11-16 14:22:01 +00:00
// Randomize and fill channel with available records
count := len(d.peerCache.recs)
if limit < count {
count = limit
}
perm := d.peerCache.rng.Perm(len(d.peerCache.recs))[0:count]
permSet := make(map[int]int)
for i, v := range perm {
permSet[v] = i
}
2022-10-23 13:13:43 +00:00
sendLst := make([]PeerRecord, count)
2021-11-16 14:22:01 +00:00
iter := 0
for k := range d.peerCache.recs {
if sendIndex, ok := permSet[iter]; ok {
2022-10-23 13:13:43 +00:00
sendLst[sendIndex] = d.peerCache.recs[k]
2021-11-16 14:22:01 +00:00
}
iter++
}
2022-10-23 13:13:43 +00:00
return sendLst, err
}
func (d *DiscoveryV5) FindPeers(ctx context.Context, topic string, opts ...discovery.Option) (<-chan peer.AddrInfo, error) {
records, err := d.FindNodes(ctx, topic, opts...)
if err != nil {
return nil, err
}
chPeer := make(chan peer.AddrInfo, len(records))
for _, r := range records {
chPeer <- r.Peer
2021-11-16 14:22:01 +00:00
}
close(chPeer)
return chPeer, err
}
2022-10-23 13:13:43 +00:00
func (d *DiscoveryV5) IsStarted() bool {
d.RLock()
defer d.RUnlock()
return d.started
}