discovery: add new interface NetworkPeerBootstrapper w/ 2 impls

This commit adds a new interface the to discovery package:
NetworkPeerBootstrapper. The NetworkPeerBootstrapper interface is meant
to be used to bootstrap a new peer joining the network to the set of
existing active peers within the network. Callers are encouraged to
utilize several boostrappers in series as redundant sources of
information. The MultiSourceBootstrap function will takes a set of
boostrappers, and compose their outputs into a single unified set of
addresses.

Two concrete implementations of the NetworkPeerBootstrapper interface
have been added as a part of this commit: the ChannelGraphBootstrapper
and the DNSSeedBootstrapper. The former will utilize the authenticated
node advertisements within the calling nodes view to boostrap new
connections. The latter will use a set of BOLT-0010 compliant DNS seeds
to query. This DNS seeding more will likely be used by nodes initial
joining the network, as they may not yet have the channel graph as they
haven’t connected to any peers.
This commit is contained in:
Olaoluwa Osuntokun 2017-09-03 16:51:09 -07:00
parent 0769cae565
commit f823a860c8
No known key found for this signature in database
GPG Key ID: 9CC5B105D03521A2

382
discovery/bootstrapper.go Normal file

@ -0,0 +1,382 @@
package discovery
import (
"bytes"
"crypto/rand"
"crypto/sha256"
"fmt"
"net"
"strings"
"github.com/davecgh/go-spew/spew"
"github.com/lightningnetwork/lnd/autopilot"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/roasbeef/btcd/btcec"
"github.com/roasbeef/btcutil/bech32"
)
// NetworkPeerBootstrapper is an interface that represents an initial peer
// boostrap mechanism. This interface is to be used to bootstrap a new peer to
// the connection by providing it with the pubkey+address of a set of existing
// peers on the network. Several bootstrap mechanisms can be implemented such
// as DNS, in channel graph, DHT's, etc.
type NetworkPeerBootstrapper interface {
// SampleNodeAddrs uniformly samples a set of specified address from
// the network peer bootstrapper source. The num addrs field passed in
// denotes how many valid peer addresses to return. The passed set of
// node nodes allows the caller to ignore a set of nodes perhaps
// because they already have connections established.
SampleNodeAddrs(numAddrs uint32,
ignore map[autopilot.NodeID]struct{}) ([]*lnwire.NetAddress, error)
// Name returns a human readable string which names the concrete
// implementation of the NetworkPeerBootstrapper.
Name() string
}
// MultiSourceBootstrap attempts to utilize a set of NetworkPeerBootstrapper
// passed in to return the target (numAddrs) number of peer addresses that can
// be used to bootstrap a peer just joining the Lightning Network. Each
// bootstrapper will be queried successively until the target amount is met. If
// the ignore map is populated, then the bootstrappers will be instructed to
// skip those nodes.
func MultiSourceBootstrap(ignore map[autopilot.NodeID]struct{}, numAddrs uint32,
bootStrappers ...NetworkPeerBootstrapper) ([]*lnwire.NetAddress, error) {
var addrs []*lnwire.NetAddress
for _, bootStrapper := range bootStrappers {
// If we already have enough addresses, then we can exit early
// w/o querying the additional boostrappers.
if uint32(len(addrs)) >= numAddrs {
break
}
log.Infof("Attempting to bootstrap with: %v", bootStrapper.Name())
// If we still need additional addresses, then we'll compute
// the number of address remaining that we need to fetch.
numAddrsLeft := numAddrs - uint32(len(addrs))
log.Tracef("Querying for %v addresses", numAddrsLeft)
netAddrs, err := bootStrapper.SampleNodeAddrs(numAddrsLeft, ignore)
if err != nil {
// If we encounter an error with a bootstrapper, then
// we'll continue on to the next available
// bootstrapper.
log.Errorf("Unable to query bootstrapper %v: %v",
bootStrapper.Name(), err)
continue
}
addrs = append(addrs, netAddrs...)
}
log.Infof("Obtained %v addrs to bootstrap network with", len(addrs))
return addrs, nil
}
// ChannelGraphBootstrapper is an implementation of the NetworkPeerBootstrapper
// which attempts to retrieve advertised peers directly from the active channel
// graph. This instance requires a backing autopilot.ChannelGraph instance in
// order to operate properly.
type ChannelGraphBootstrapper struct {
chanGraph autopilot.ChannelGraph
// hashAccumulator is a set of 32 random bytes that are read upon the
// creation of the channel graph boostrapper. We use this value to
// randomly select nodes within the known graph to connect to. After
// each selection, we rotate the accumulator by hashing it with itself.
hashAccumulator [32]byte
tried map[autopilot.NodeID]struct{}
}
// A compile time assertion to ensure that ChannelGraphBootstrapper meets the
// NetworkPeerBootstrapper interface.
var _ NetworkPeerBootstrapper = (*ChannelGraphBootstrapper)(nil)
// NewGraphBootstrapper returns a new instance of a ChannelGraphBootstrapper
// backed by an active autopilot.ChannelGraph instance. This type of network
// peer bootstrapper will use the authenticated nodes within the known channel
// graph to bootstrap connections.
func NewGraphBootstrapper(cg autopilot.ChannelGraph) (NetworkPeerBootstrapper, error) {
c := &ChannelGraphBootstrapper{
chanGraph: cg,
tried: make(map[autopilot.NodeID]struct{}),
}
if _, err := rand.Read(c.hashAccumulator[:]); err != nil {
return nil, err
}
return c, nil
}
// SampleNodeAddrs uniformly samples a set of specified address from the
// network peer bootstrapper source. The num addrs field passed in denotes how
// many valid peer addresses to return.
//
// NOTE: Part of the NetworkPeerBootstrapper interface.
func (c *ChannelGraphBootstrapper) SampleNodeAddrs(numAddrs uint32,
ignore map[autopilot.NodeID]struct{}) ([]*lnwire.NetAddress, error) {
// We'll merge the ignore map with our currently selected map in order
// to ensure we don't return any duplicate nodes.
for n := range ignore {
c.tried[n] = struct{}{}
}
// In order to bootstrap, we'll iterate all the nodes in the channel
// graph, accumulating nodes until either we go through all active
// nodes, or we reach our limit. We ensure that we meet the randomly
// sample constraint as we maintain an xor accumulator to ensure we
// randomly sample nodes independent of the iteration of the channel
// graph.
sampleAddrs := func() ([]*lnwire.NetAddress, error) {
var (
a []*lnwire.NetAddress
// We'll create a special error so we can return early
// and abort the transaction once we find a match.
errFound = fmt.Errorf("found node")
)
err := c.chanGraph.ForEachNode(func(node autopilot.Node) error {
nID := autopilot.NewNodeID(node.PubKey())
if _, ok := c.tried[nID]; ok {
return nil
}
// We'll select the first node we come across who's
// public key is less than our current accumulator
// value. When comparing, we skip the first byte as
// it's 50/50. If it isn't less, than then we'll
// continue forward.
nodePub := node.PubKey().SerializeCompressed()[1:]
if bytes.Compare(c.hashAccumulator[:], nodePub) > 0 {
return nil
}
for _, nodeAddr := range node.Addrs() {
// If we haven't yet reached our limit, then
// we'll copy over the details of this node
// into the set of addresses to be returned.
tcpAddr, ok := nodeAddr.(*net.TCPAddr)
if !ok {
// If this isn't a valid TCP address,
// then we'll ignore it as currently
// we'll only attempt to connect out to
// TCP peers.
return nil
}
// At this point, we've found an eligible node,
// so we'll return early with our shibboleth
// error.
a = append(a, &lnwire.NetAddress{
IdentityKey: node.PubKey(),
Address: tcpAddr,
})
}
c.tried[nID] = struct{}{}
return errFound
})
if err != nil && err != errFound {
return nil, err
}
return a, nil
}
// We'll loop and sample new addresses from the graph source until
// we've reached our target number of outbound connections or we hit 50
// attempts, which ever comes first.
var (
addrs []*lnwire.NetAddress
tries uint32
)
for tries < 30 && uint32(len(addrs)) < numAddrs {
sampleAddrs, err := sampleAddrs()
if err != nil {
return nil, err
}
tries += 1
// We'll now rotate our hash accumulator one value forwards.
c.hashAccumulator = sha256.Sum256(c.hashAccumulator[:])
// If this attempt didn't yield any addresses, then we'll exit
// early.
if len(sampleAddrs) == 0 {
continue
}
addrs = append(addrs, sampleAddrs...)
}
log.Tracef("Ending hash accumulator state: %x", c.hashAccumulator)
return addrs, nil
}
// Name returns a human readable string which names the concrete implementation
// of the NetworkPeerBootstrapper.
//
// NOTE: Part of the NetworkPeerBootstrapper interface.
func (c *ChannelGraphBootstrapper) Name() string {
return "Authenticated Channel Graph"
}
// DNSSeedBootstrapper as an implementation of the NetworkPeerBootstrapper
// interface which implements peer bootstrapping via a spcial DNS seed as
// defined in BOLT-0010. For further details concerning Lightning's current DNS
// boot strapping protocol, see this link:
// * https://github.com/lightningnetwork/lightning-rfc/blob/master/10-dns-bootstrap.md
type DNSSeedBootstrapper struct {
dnsSeeds []string
}
// A compile time assertion to ensure that DNSSeedBootstrapper meets the
// NetworkPeerjBootstrapper interface.
var _ NetworkPeerBootstrapper = (*ChannelGraphBootstrapper)(nil)
// NewDNSSeedBootstrapper returns a new instance of the DNSSeedBootstrapper.
// The set of passed seeds should point to DNS servers that properly implement
// Lighting's DNS peer bootstrapping protocol as defined in BOLT-0010.
//
//
// TODO(roasbeef): add a lookUpFunc param to pass in, so can divert queries
// over Tor in future
func NewDNSSeedBootstrapper(seeds []string) (NetworkPeerBootstrapper, error) {
return &DNSSeedBootstrapper{
dnsSeeds: seeds,
}, nil
}
// SampleNodeAddrs uniformly samples a set of specified address from the
// network peer bootstrapper source. The num addrs field passed in denotes how
// many valid peer addresses to return. The set of DNS seeds are used
// successively to retrieve eligible target nodes.
func (d *DNSSeedBootstrapper) SampleNodeAddrs(numAddrs uint32,
ignore map[autopilot.NodeID]struct{}) ([]*lnwire.NetAddress, error) {
var netAddrs []*lnwire.NetAddress
// We'll continue this loop until we reach our target address limit.
// Each SRV query to the seed will return 25 random nodes, so we can
// continue to query until we reach our target.
search:
for uint32(len(netAddrs)) < numAddrs {
for _, dnsSeed := range d.dnsSeeds {
// We'll first query the seed with an SRV record so we
// can obtain a random sample of the encoded public
// keys of nodes.
_, addrs, err := net.LookupSRV("nodes", "tcp", dnsSeed)
if err != nil {
return nil, err
}
log.Tracef("Retrieved SRV records from dns seed: %v",
spew.Sdump(addrs))
// Next, we'll need to issue an A record request for
// each of the nodes, skipping it if nothing comes
// back.
for _, nodeSrv := range addrs {
if uint32(len(netAddrs)) >= numAddrs {
break search
}
// With the SRV target obtained, we'll now
// perform another query to obtain the IP
// address for the matching bech32 encoded node
// key.
bechNodeHost := nodeSrv.Target
addrs, err := net.LookupHost(bechNodeHost)
if err != nil {
return nil, err
}
if len(addrs) == 0 {
log.Tracef("No addresses for %v, skipping",
bechNodeHost)
continue
}
log.Tracef("Attempting to convert: %v", bechNodeHost)
// If we have a set of valid addresses, then
// we'll need to parse the public key from the
// original bech32 encoded string.
bechNode := strings.Split(bechNodeHost, ".")
_, nodeBytes5Bits, err := bech32.Decode(bechNode[0])
if err != nil {
return nil, err
}
// Once we have the bech32 decoded pubkey,
// we'll need to convert the 5-bit word
// grouping into our regular 8-bit word
// grouping so we can convert it into a public
// key.
nodeBytes, err := bech32.ConvertBits(
nodeBytes5Bits, 5, 8, false,
)
if err != nil {
return nil, err
}
nodeKey, err := btcec.ParsePubKey(
nodeBytes, btcec.S256(),
)
if err != nil {
return nil, err
}
// If we have an ignore list, and this node is
// in the ignore list, then we'll go to the
// next candidate.
if ignore != nil {
nID := autopilot.NewNodeID(nodeKey)
if _, ok := ignore[nID]; ok {
continue
}
}
// Finally we'll convert the host:port peer to
// a proper TCP address to use within the
// lnwire.NetAddress.
addr := fmt.Sprintf("%v:%v", addrs[0],
nodeSrv.Port)
tcpAddr, err := net.ResolveTCPAddr("tcp", addr)
if err != nil {
return nil, err
}
// Finally, with all the information parsed,
// we'll return this fully valid address as a
// connection attempt.
lnAddr := &lnwire.NetAddress{
IdentityKey: nodeKey,
Address: tcpAddr,
}
log.Tracef("Obtained %v as valid reachable "+
"node", lnAddr)
netAddrs = append(netAddrs, lnAddr)
}
}
}
return netAddrs, nil
}
// Name returns a human readable string which names the concrete
// implementation of the NetworkPeerBootstrapper.
func (d *DNSSeedBootstrapper) Name() string {
return fmt.Sprintf("BOLT-0010 DNS Seed: %v", d.dnsSeeds)
}