/** * Bootnode * * A node which implements the DiscoveryV5 protocol for peer * discovery. The purpose of this service is to provide a starting point for * newly connected services to find other peers outside of their network. * * Usage: Run bootnode --help for flag options. */ package main import ( "context" "crypto/ecdsa" "crypto/rand" "encoding/hex" "flag" "fmt" "io" "net" "net/http" "os" "github.com/btcsuite/btcd/btcec" gethlog "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/p2p/discover" "github.com/ethereum/go-ethereum/p2p/enode" "github.com/ethereum/go-ethereum/p2p/enr" ds "github.com/ipfs/go-datastore" dsync "github.com/ipfs/go-datastore/sync" logging "github.com/ipfs/go-log" libp2p "github.com/libp2p/go-libp2p" "github.com/libp2p/go-libp2p-core/crypto" kaddht "github.com/libp2p/go-libp2p-kad-dht" dhtopts "github.com/libp2p/go-libp2p-kad-dht/opts" ma "github.com/multiformats/go-multiaddr" "github.com/pkg/errors" "github.com/prysmaticlabs/go-bitfield" "github.com/prysmaticlabs/go-ssz" pb "github.com/prysmaticlabs/prysm/proto/beacon/p2p/v1" "github.com/prysmaticlabs/prysm/shared/logutil" "github.com/prysmaticlabs/prysm/shared/params" "github.com/prysmaticlabs/prysm/shared/version" "github.com/sirupsen/logrus" _ "go.uber.org/automaxprocs" ) var ( debug = flag.Bool("debug", false, "Enable debug logging") logFileName = flag.String("log-file", "", "Specify log filename, relative or absolute") privateKey = flag.String("private", "", "Private key to use for peer ID") discv5port = flag.Int("discv5-port", 4000, "Port to listen for discv5 connections") kademliaPort = flag.Int("kad-port", 4500, "Port to listen for connections to kad DHT") metricsPort = flag.Int("metrics-port", 5000, "Port to listen for connections") externalIP = flag.String("external-ip", "", "External IP for the bootnode") disableKad = flag.Bool("disable-kad", false, "Disables the bootnode from running kademlia dht") log = logrus.WithField("prefix", "bootnode") ) const dhtProtocol = "/prysm/0.0.0/dht" const defaultIP = "127.0.0.1" type handler struct { listener *discover.UDPv5 } func main() { flag.Parse() if *logFileName != "" { if err := logutil.ConfigurePersistentLogging(*logFileName); err != nil { log.WithError(err).Error("Failed to configuring logging to disk.") } } fmt.Printf("Starting bootnode. Version: %s\n", version.GetVersion()) if *debug { logrus.SetLevel(logrus.DebugLevel) // Geth specific logging. glogger := gethlog.NewGlogHandler(gethlog.StreamHandler(os.Stderr, gethlog.TerminalFormat(false))) glogger.Verbosity(gethlog.LvlTrace) gethlog.Root().SetHandler(glogger) log.Debug("Debug logging enabled.") } privKey, interfacePrivKey := extractPrivateKey() cfg := discover.Config{ PrivateKey: privKey, } listener := createListener(defaultIP, *discv5port, cfg) node := listener.Self() log.Infof("Running bootnode: %s", node.String()) if !*disableKad { startKademliaDHT(interfacePrivKey) } handler := &handler{ listener: listener, } mux := http.NewServeMux() mux.HandleFunc("/p2p", handler.httpHandler) if err := http.ListenAndServe(fmt.Sprintf(":%d", *metricsPort), mux); err != nil { log.Fatalf("Failed to start server %v", err) } select {} } func startKademliaDHT(privKey crypto.PrivKey) { if *debug { logging.SetDebugLogging() } ipAddr := defaultIP if *externalIP != "" { ipAddr = *externalIP } listen, err := ma.NewMultiaddr(fmt.Sprintf("/ip4/%s/tcp/%d", ipAddr, *kademliaPort)) if err != nil { log.Fatalf("Failed to construct new multiaddress. %v", err) } opts := []libp2p.Option{ libp2p.ListenAddrs(listen), } opts = append(opts, libp2p.Identity(privKey)) ctx := context.Background() host, err := libp2p.New(ctx, opts...) if err != nil { log.Fatalf("Failed to create new host. %v", err) } dopts := []dhtopts.Option{ dhtopts.Datastore(dsync.MutexWrap(ds.NewMapDatastore())), dhtopts.Protocols( dhtProtocol, ), } dht, err := kaddht.New(ctx, host, dopts...) if err != nil { log.Fatalf("Failed to create new dht: %v", err) } if err := dht.Bootstrap(context.Background()); err != nil { log.Fatalf("Failed to bootstrap DHT. %v", err) } fmt.Printf("Running Kademlia DHT bootnode: /ip4/%s/tcp/%d/p2p/%s\n", ipAddr, *kademliaPort, host.ID().Pretty()) } func createListener(ipAddr string, port int, cfg discover.Config) *discover.UDPv5 { ip := net.ParseIP(ipAddr) if ip.To4() == nil { log.Fatalf("IPV4 address not provided instead %s was provided", defaultIP) } udpAddr := &net.UDPAddr{ IP: ip, Port: port, } conn, err := net.ListenUDP("udp4", udpAddr) if err != nil { log.Fatal(err) } if *externalIP != "" { ip = net.ParseIP(*externalIP) } localNode, err := createLocalNode(cfg.PrivateKey, ip, port) if err != nil { log.Fatal(err) } network, err := discover.ListenV5(conn, localNode, cfg) if err != nil { log.Fatal(err) } return network } func (h *handler) httpHandler(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) write := func(w io.Writer, b []byte) { if _, err := w.Write(b); err != nil { log.WithError(err).Error("Failed to write to http response") } } allNodes := h.listener.AllNodes() write(w, []byte("Nodes stored in the table:\n")) for i, n := range allNodes { write(w, []byte(fmt.Sprintf("Node %d\n", i))) write(w, []byte(n.String()+"\n")) write(w, []byte("Node ID: "+n.ID().String()+"\n")) write(w, []byte("IP: "+n.IP().String()+"\n")) write(w, []byte(fmt.Sprintf("UDP Port: %d", n.UDP())+"\n")) write(w, []byte(fmt.Sprintf("TCP Port: %d", n.UDP())+"\n\n")) } } func createLocalNode(privKey *ecdsa.PrivateKey, ipAddr net.IP, port int) (*enode.LocalNode, error) { db, err := enode.OpenDB("") if err != nil { return nil, errors.Wrap(err, "Could not open node's peer database") } forkID := &pb.ENRForkID{ CurrentForkDigest: []byte{0, 0, 0, 0}, NextForkVersion: params.BeaconConfig().NextForkVersion, NextForkEpoch: params.BeaconConfig().NextForkEpoch, } forkEntry, err := ssz.Marshal(forkID) if err != nil { return nil, errors.Wrap(err, "Could not marshal fork id") } localNode := enode.NewLocalNode(db, privKey) ipEntry := enr.IP(ipAddr) udpEntry := enr.UDP(port) localNode.SetFallbackIP(ipAddr) localNode.SetFallbackUDP(port) localNode.Set(ipEntry) localNode.Set(udpEntry) localNode.Set(enr.WithEntry("eth2", forkEntry)) localNode.Set(enr.WithEntry("attnets", bitfield.NewBitvector64())) return localNode, nil } func extractPrivateKey() (*ecdsa.PrivateKey, crypto.PrivKey) { var privKey *ecdsa.PrivateKey var interfaceKey crypto.PrivKey if *privateKey != "" { dst, err := hex.DecodeString(*privateKey) if err != nil { panic(err) } unmarshalledKey, err := crypto.UnmarshalSecp256k1PrivateKey(dst) if err != nil { panic(err) } interfaceKey = unmarshalledKey privKey = (*ecdsa.PrivateKey)((*btcec.PrivateKey)(unmarshalledKey.(*crypto.Secp256k1PrivateKey))) } else { privInterfaceKey, _, err := crypto.GenerateSecp256k1Key(rand.Reader) if err != nil { panic(err) } interfaceKey = privInterfaceKey privKey = (*ecdsa.PrivateKey)((*btcec.PrivateKey)(privInterfaceKey.(*crypto.Secp256k1PrivateKey))) log.Warning("No private key was provided. Using default/random private key") b, err := privInterfaceKey.Raw() if err != nil { panic(err) } log.Debugf("Private key %x", b) } return privKey, interfaceKey }