prysm-pulse/beacon-chain/p2p/service.go
Nishant Das 9d27449212
Discovery Fixes (#5050)
* connect to dv5 bootnodes

* fix test

* change polling period

* ignore

* Update beacon-chain/p2p/service.go

* Update beacon-chain/p2p/service_test.go

* fix test

Co-authored-by: prylabs-bulldozer[bot] <58059840+prylabs-bulldozer[bot]@users.noreply.github.com>
Co-authored-by: terence tsao <terence@prysmaticlabs.com>
2020-03-09 19:53:37 -07:00

398 lines
11 KiB
Go

package p2p
import (
"context"
"crypto/ecdsa"
"strconv"
"strings"
"time"
"github.com/dgraph-io/ristretto"
"github.com/ethereum/go-ethereum/p2p/enode"
ds "github.com/ipfs/go-datastore"
dsync "github.com/ipfs/go-datastore/sync"
"github.com/libp2p/go-libp2p"
"github.com/libp2p/go-libp2p-core/host"
"github.com/libp2p/go-libp2p-core/network"
"github.com/libp2p/go-libp2p-core/peer"
"github.com/libp2p/go-libp2p-core/protocol"
kaddht "github.com/libp2p/go-libp2p-kad-dht"
dhtopts "github.com/libp2p/go-libp2p-kad-dht/opts"
pubsub "github.com/libp2p/go-libp2p-pubsub"
rhost "github.com/libp2p/go-libp2p/p2p/host/routed"
ma "github.com/multiformats/go-multiaddr"
"github.com/pkg/errors"
"github.com/prysmaticlabs/prysm/beacon-chain/p2p/encoder"
"github.com/prysmaticlabs/prysm/beacon-chain/p2p/peers"
"github.com/prysmaticlabs/prysm/shared"
"github.com/prysmaticlabs/prysm/shared/runutil"
"github.com/sirupsen/logrus"
)
var _ = shared.Service(&Service{})
// Check local table every 5 seconds for newly added peers.
var pollingPeriod = 5 * time.Second
const prysmProtocolPrefix = "/prysm/0.0.0"
// maxBadResponses is the maximum number of bad responses from a peer before we stop talking to it.
const maxBadResponses = 3
// Service for managing peer to peer (p2p) networking.
type Service struct {
ctx context.Context
cancel context.CancelFunc
started bool
cfg *Config
startupErr error
dv5Listener Listener
host host.Host
pubsub *pubsub.PubSub
exclusionList *ristretto.Cache
privKey *ecdsa.PrivateKey
dht *kaddht.IpfsDHT
peers *peers.Status
}
// NewService initializes a new p2p service compatible with shared.Service interface. No
// connections are made until the Start function is called during the service registry startup.
func NewService(cfg *Config) (*Service, error) {
var err error
ctx, cancel := context.WithCancel(context.Background())
cache, _ := ristretto.NewCache(&ristretto.Config{
NumCounters: 1000,
MaxCost: 1000,
BufferItems: 64,
})
s := &Service{
ctx: ctx,
cancel: cancel,
cfg: cfg,
exclusionList: cache,
}
dv5Nodes, kadDHTNodes := parseBootStrapAddrs(s.cfg.BootstrapNodeAddr)
cfg.Discv5BootStrapAddr = dv5Nodes
cfg.KademliaBootStrapAddr = kadDHTNodes
ipAddr := ipAddr()
s.privKey, err = privKey(s.cfg)
if err != nil {
log.WithError(err).Error("Failed to generate p2p private key")
return nil, err
}
opts := buildOptions(s.cfg, ipAddr, s.privKey)
h, err := libp2p.New(s.ctx, opts...)
if err != nil {
log.WithError(err).Error("Failed to create p2p host")
return nil, err
}
if len(cfg.KademliaBootStrapAddr) != 0 && !cfg.NoDiscovery {
dopts := []dhtopts.Option{
dhtopts.Datastore(dsync.MutexWrap(ds.NewMapDatastore())),
dhtopts.Protocols(
prysmProtocolPrefix + "/dht",
),
}
s.dht, err = kaddht.New(ctx, h, dopts...)
if err != nil {
return nil, err
}
// Wrap host with a routed host so that peers can be looked up in the
// distributed hash table by their peer ID.
h = rhost.Wrap(h, s.dht)
}
s.host = h
// TODO(3147): Add gossip sub options
// Gossipsub registration is done before we add in any new peers
// due to libp2p's gossipsub implementation not taking into
// account previously added peers when creating the gossipsub
// object.
psOpts := []pubsub.Option{
pubsub.WithMessageSigning(false),
pubsub.WithStrictSignatureVerification(false),
pubsub.WithMessageIdFn(msgIDFunction),
}
gs, err := pubsub.NewGossipSub(s.ctx, s.host, psOpts...)
if err != nil {
log.WithError(err).Error("Failed to start pubsub")
return nil, err
}
s.pubsub = gs
s.peers = peers.NewStatus(maxBadResponses)
return s, nil
}
// Start the p2p service.
func (s *Service) Start() {
if s.started {
log.Error("Attempted to start p2p service when it was already started")
return
}
var peersToWatch []string
if s.cfg.RelayNodeAddr != "" {
peersToWatch = append(peersToWatch, s.cfg.RelayNodeAddr)
if err := dialRelayNode(s.ctx, s.host, s.cfg.RelayNodeAddr); err != nil {
log.WithError(err).Errorf("Could not dial relay node")
}
peer, err := MakePeer(s.cfg.RelayNodeAddr)
if err != nil {
log.WithError(err).Errorf("Could not create peer")
}
s.host.ConnManager().Protect(peer.ID, "relay")
}
if len(s.cfg.Discv5BootStrapAddr) != 0 && !s.cfg.NoDiscovery {
ipAddr := ipAddr()
listener, err := startDiscoveryV5(ipAddr, s.privKey, s.cfg)
if err != nil {
log.WithError(err).Error("Failed to start discovery")
s.startupErr = err
return
}
err = s.connectToBootnodes()
if err != nil {
log.WithError(err).Error("Could not add bootnode to the exclusion list")
s.startupErr = err
return
}
s.dv5Listener = listener
go s.listenForNewNodes()
}
if len(s.cfg.KademliaBootStrapAddr) != 0 && !s.cfg.NoDiscovery {
for _, addr := range s.cfg.KademliaBootStrapAddr {
peersToWatch = append(peersToWatch, addr)
err := startDHTDiscovery(s.host, addr)
if err != nil {
log.WithError(err).Error("Could not connect to bootnode")
s.startupErr = err
return
}
if err := s.addKadDHTNodesToExclusionList(addr); err != nil {
s.startupErr = err
return
}
peer, err := MakePeer(addr)
if err != nil {
log.WithError(err).Errorf("Could not create peer")
}
s.host.ConnManager().Protect(peer.ID, "bootnode")
}
bcfg := kaddht.DefaultBootstrapConfig
bcfg.Period = 30 * time.Second
if err := s.dht.BootstrapWithConfig(s.ctx, bcfg); err != nil {
log.WithError(err).Error("Failed to bootstrap DHT")
}
}
s.started = true
if len(s.cfg.StaticPeers) > 0 {
addrs, err := peersFromStringAddrs(s.cfg.StaticPeers)
if err != nil {
log.Errorf("Could not connect to static peer: %v", err)
}
s.connectWithAllPeers(addrs)
}
// Periodic functions.
runutil.RunEvery(s.ctx, 5*time.Second, func() {
ensurePeerConnections(s.ctx, s.host, peersToWatch...)
})
runutil.RunEvery(s.ctx, time.Hour, s.Peers().Decay)
runutil.RunEvery(s.ctx, 10*time.Second, s.updateMetrics)
multiAddrs := s.host.Network().ListenAddresses()
logIP4Addr(s.host.ID(), multiAddrs...)
p2pHostAddress := s.cfg.HostAddress
p2pTCPPort := s.cfg.TCPPort
if p2pHostAddress != "" {
logExternalIP4Addr(s.host.ID(), p2pHostAddress, p2pTCPPort)
}
p2pHostDNS := s.cfg.HostDNS
if p2pHostDNS != "" {
logExternalDNSAddr(s.host.ID(), p2pHostDNS, p2pTCPPort)
}
}
// Stop the p2p service and terminate all peer connections.
func (s *Service) Stop() error {
defer s.cancel()
s.started = false
if s.dv5Listener != nil {
s.dv5Listener.Close()
}
return nil
}
// Status of the p2p service. Will return an error if the service is considered unhealthy to
// indicate that this node should not serve traffic until the issue has been resolved.
func (s *Service) Status() error {
if !s.started {
return errors.New("not running")
}
return nil
}
// Started returns true if the p2p service has successfully started.
func (s *Service) Started() bool {
return s.started
}
// Encoding returns the configured networking encoding.
func (s *Service) Encoding() encoder.NetworkEncoding {
encoding := s.cfg.Encoding
switch encoding {
case encoder.SSZ:
return &encoder.SszNetworkEncoder{}
case encoder.SSZSnappy:
return &encoder.SszNetworkEncoder{UseSnappyCompression: true}
default:
panic("Invalid Network Encoding Flag Provided")
}
}
// PubSub returns the p2p pubsub framework.
func (s *Service) PubSub() *pubsub.PubSub {
return s.pubsub
}
// SetStreamHandler sets the protocol handler on the p2p host multiplexer.
// This method is a pass through to libp2pcore.Host.SetStreamHandler.
func (s *Service) SetStreamHandler(topic string, handler network.StreamHandler) {
s.host.SetStreamHandler(protocol.ID(topic), handler)
}
// PeerID returns the Peer ID of the local peer.
func (s *Service) PeerID() peer.ID {
return s.host.ID()
}
// Disconnect from a peer.
func (s *Service) Disconnect(pid peer.ID) error {
return s.host.Network().ClosePeer(pid)
}
// Peers returns the peer status interface.
func (s *Service) Peers() *peers.Status {
return s.peers
}
// listen for new nodes watches for new nodes in the network and adds them to the peerstore.
func (s *Service) listenForNewNodes() {
runutil.RunEvery(s.ctx, pollingPeriod, func() {
nodes := s.dv5Listener.LookupRandom()
multiAddresses := convertToMultiAddr(nodes)
s.connectWithAllPeers(multiAddresses)
})
}
func (s *Service) connectWithAllPeers(multiAddrs []ma.Multiaddr) {
addrInfos, err := peer.AddrInfosFromP2pAddrs(multiAddrs...)
if err != nil {
log.Errorf("Could not convert to peer address info's from multiaddresses: %v", err)
return
}
for _, info := range addrInfos {
// make each dial non-blocking
go func(info peer.AddrInfo) {
if len(s.Peers().Active()) >= int(s.cfg.MaxPeers) {
log.WithFields(logrus.Fields{"peer": info.ID.String(),
"reason": "at peer limit"}).Trace("Not dialing peer")
return
}
if info.ID == s.host.ID() {
return
}
if s.Peers().IsBad(info.ID) {
return
}
if err := s.host.Connect(s.ctx, info); err != nil {
log.Errorf("Could not connect with peer %s: %v", info.String(), err)
s.Peers().IncrementBadResponses(info.ID)
}
}(info)
}
}
func (s *Service) connectToBootnodes() error {
nodes := make([]*enode.Node, 0, len(s.cfg.Discv5BootStrapAddr))
for _, addr := range s.cfg.Discv5BootStrapAddr {
bootNode, err := enode.Parse(enode.ValidSchemes, addr)
if err != nil {
return err
}
nodes = append(nodes, bootNode)
}
multiAddresses := convertToMultiAddr(nodes)
s.connectWithAllPeers(multiAddresses)
return nil
}
func (s *Service) addKadDHTNodesToExclusionList(addr string) error {
multiAddr, err := ma.NewMultiaddr(addr)
if err != nil {
return errors.Wrap(err, "could not get multiaddr")
}
addrInfo, err := peer.AddrInfoFromP2pAddr(multiAddr)
if err != nil {
return err
}
// bootnode is never dialled, so ttl is tentatively 1 year
s.exclusionList.Set(addrInfo.ID.String(), true, 1)
return nil
}
func logIP4Addr(id peer.ID, addrs ...ma.Multiaddr) {
var correctAddr ma.Multiaddr
for _, addr := range addrs {
if strings.Contains(addr.String(), "/ip4/") {
correctAddr = addr
break
}
}
if correctAddr != nil {
log.WithField(
"multiAddr",
correctAddr.String()+"/p2p/"+id.String(),
).Info("Node started p2p server")
}
}
func logExternalIP4Addr(id peer.ID, addr string, port uint) {
if addr != "" {
p := strconv.FormatUint(uint64(port), 10)
log.WithField(
"multiAddr",
"/ip4/"+addr+"/tcp/"+p+"/p2p/"+id.String(),
).Info("Node started external p2p server")
}
}
func logExternalDNSAddr(id peer.ID, addr string, port uint) {
if addr != "" {
p := strconv.FormatUint(uint64(port), 10)
log.WithField(
"multiAddr",
"/dns4/"+addr+"/tcp/"+p+"/p2p/"+id.String(),
).Info("Node started external p2p server")
}
}