/** * 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" "time" gcrypto "github.com/ethereum/go-ethereum/crypto" 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" "github.com/libp2p/go-libp2p/core/crypto" "github.com/pkg/errors" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" "github.com/prysmaticlabs/go-bitfield" "github.com/prysmaticlabs/prysm/v3/async" "github.com/prysmaticlabs/prysm/v3/beacon-chain/core/signing" "github.com/prysmaticlabs/prysm/v3/config/params" ecdsaprysm "github.com/prysmaticlabs/prysm/v3/crypto/ecdsa" "github.com/prysmaticlabs/prysm/v3/encoding/bytesutil" "github.com/prysmaticlabs/prysm/v3/io/logs" "github.com/prysmaticlabs/prysm/v3/network" pb "github.com/prysmaticlabs/prysm/v3/proto/prysm/v1alpha1" _ "github.com/prysmaticlabs/prysm/v3/runtime/maxprocs" "github.com/prysmaticlabs/prysm/v3/runtime/version" "github.com/sirupsen/logrus" ) 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") metricsPort = flag.Int("metrics-port", 5000, "Port to listen for connections") externalIP = flag.String("external-ip", "", "External IP for the bootnode") forkVersion = flag.String("fork-version", "", "Fork Version that the bootnode uses") genesisValidatorsRoot = flag.String("genesis-root", "", "Genesis Validators Root the beacon node uses") seedNode = flag.String("seed-node", "", "External node to connect to") log = logrus.WithField("prefix", "bootnode") discv5PeersCount = promauto.NewGauge(prometheus.GaugeOpts{ Name: "bootstrap_node_discv5_peers", Help: "The current number of discv5 peers of the bootstrap node", }) ) type handler struct { listener *discover.UDPv5 } func main() { flag.Parse() if *logFileName != "" { if err := logs.ConfigurePersistentLogging(*logFileName); err != nil { log.WithError(err).Error("Failed to configuring logging to disk.") } } fmt.Printf("Starting bootnode. Version: %s\n", version.Version()) 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 := extractPrivateKey() cfg := discover.Config{ PrivateKey: privKey, } if *seedNode != "" { log.Debugf("Adding seed node %s", *seedNode) node, err := enode.Parse(enode.ValidSchemes, *seedNode) if err != nil { log.Fatal(err) } cfg.Bootnodes = []*enode.Node{node} } ipAddr, err := network.ExternalIP() if err != nil { log.Fatal(err) } listener := createListener(ipAddr, *discv5port, cfg) node := listener.Self() log.Infof("Running bootnode: %s", node.String()) handler := &handler{ listener: listener, } mux := http.NewServeMux() mux.HandleFunc("/p2p", handler.httpHandler) if err := http.ListenAndServe(fmt.Sprintf(":%d", *metricsPort), mux); err != nil { log.WithError(err).Fatal("Failed to start server") } // Update metrics once per slot. slotDuration := time.Duration(params.BeaconConfig().SecondsPerSlot) async.RunEvery(context.Background(), slotDuration*time.Second, func() { updateMetrics(listener) }) select {} } 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", ipAddr) } var bindIP net.IP var networkVersion string switch { case ip.To16() != nil && ip.To4() == nil: bindIP = net.IPv6zero networkVersion = "udp6" case ip.To4() != nil: bindIP = net.IPv4zero networkVersion = "udp4" default: log.Fatalf("Valid ip address not provided instead %s was provided", ipAddr) } udpAddr := &net.UDPAddr{ IP: bindIP, Port: port, } conn, err := net.ListenUDP(networkVersion, udpAddr) if err != nil { log.Fatal(err) } localNode, err := createLocalNode(cfg.PrivateKey, ip, port) if err != nil { log.Fatal(err) } net, err := discover.ListenV5(conn, localNode, cfg) if err != nil { log.Fatal(err) } return net } func (h *handler) httpHandler(w http.ResponseWriter, _ *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") } external := net.ParseIP(*externalIP) if *externalIP == "" { external = ipAddr } fVersion := params.BeaconConfig().GenesisForkVersion if *forkVersion != "" { fVersion, err = hex.DecodeString(*forkVersion) if err != nil { return nil, errors.Wrap(err, "Could not retrieve fork version") } if len(fVersion) != 4 { return nil, errors.Errorf("Invalid fork version size expected %d but got %d", 4, len(fVersion)) } } genRoot := params.BeaconConfig().ZeroHash if *genesisValidatorsRoot != "" { retRoot, err := hex.DecodeString(*genesisValidatorsRoot) if err != nil { return nil, errors.Wrap(err, "Could not retrieve genesis validators root") } if len(retRoot) != 32 { return nil, errors.Errorf("Invalid root size, expected 32 but got %d", len(retRoot)) } genRoot = bytesutil.ToBytes32(retRoot) } digest, err := signing.ComputeForkDigest(fVersion, genRoot[:]) if err != nil { return nil, errors.Wrap(err, "Could not compute fork digest") } forkID := &pb.ENRForkID{ CurrentForkDigest: digest[:], NextForkVersion: fVersion, NextForkEpoch: params.BeaconConfig().FarFutureEpoch, } forkEntry, err := forkID.MarshalSSZ() if err != nil { return nil, errors.Wrap(err, "Could not marshal fork id") } localNode := enode.NewLocalNode(db, privKey) localNode.Set(enr.WithEntry("eth2", forkEntry)) localNode.Set(enr.WithEntry("attnets", bitfield.NewBitvector64())) localNode.SetFallbackIP(external) localNode.SetFallbackUDP(port) return localNode, nil } func extractPrivateKey() *ecdsa.PrivateKey { var privKey *ecdsa.PrivateKey if *privateKey != "" { dst, err := hex.DecodeString(*privateKey) if err != nil { panic(err) } unmarshalledKey, err := crypto.UnmarshalSecp256k1PrivateKey(dst) if err != nil { panic(err) } privKey, err = ecdsaprysm.ConvertFromInterfacePrivKey(unmarshalledKey) if err != nil { panic(err) } } else { privInterfaceKey, _, err := crypto.GenerateSecp256k1Key(rand.Reader) if err != nil { panic(err) } privKey, err = ecdsaprysm.ConvertFromInterfacePrivKey(privInterfaceKey) if err != nil { panic(err) } 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) } privKey.Curve = gcrypto.S256() return privKey } func updateMetrics(listener *discover.UDPv5) { if listener != nil { discv5PeersCount.Set(float64(len(listener.AllNodes()))) } }