package autopilot import ( prand "math/rand" "time" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcutil" ) // minMedianChanSizeFraction determines the minimum size a channel must have to // count positively when calculating the scores using preferential attachment. // The minimum channel size is calculated as median/minMedianChanSizeFraction, // where median is the median channel size of the entire graph. const minMedianChanSizeFraction = 4 // PrefAttachment is an implementation of the AttachmentHeuristic interface // that implement a non-linear preferential attachment heuristic. This means // that given a threshold to allocate to automatic channel establishment, the // heuristic will attempt to favor connecting to nodes which already have a set // amount of links, selected by sampling from a power law distribution. The // attachment is non-linear in that it favors nodes with a higher in-degree but // less so than regular linear preferential attachment. As a result, this // creates smaller and less clusters than regular linear preferential // attachment. // // TODO(roasbeef): BA, with k=-3 type PrefAttachment struct { } // NewPrefAttachment creates a new instance of a PrefAttachment heuristic. func NewPrefAttachment() *PrefAttachment { prand.Seed(time.Now().Unix()) return &PrefAttachment{} } // A compile time assertion to ensure PrefAttachment meets the // AttachmentHeuristic interface. var _ AttachmentHeuristic = (*PrefAttachment)(nil) // NodeID is a simple type that holds an EC public key serialized in compressed // format. type NodeID [33]byte // NewNodeID creates a new nodeID from a passed public key. func NewNodeID(pub *btcec.PublicKey) NodeID { var n NodeID copy(n[:], pub.SerializeCompressed()) return n } // Name returns the name of this heuristic. // // NOTE: This is a part of the AttachmentHeuristic interface. func (p *PrefAttachment) Name() string { return "preferential" } // NodeScores is a method that given the current channel graph and current set // of local channels, scores the given nodes according to the preference of // opening a channel of the given size with them. The returned channel // candidates maps the NodeID to a NodeScore for the node. // // The heuristic employed by this method is one that attempts to promote a // scale-free network globally, via local attachment preferences for new nodes // joining the network with an amount of available funds to be allocated to // channels. Specifically, we consider the degree of each node (and the flow // in/out of the node available via its open channels) and utilize the // Barabási–Albert model to drive our recommended attachment heuristics. If // implemented globally for each new participant, this results in a channel // graph that is scale-free and follows a power law distribution with k=-3. // // To avoid assigning a high score to nodes with a large number of small // channels, we only count channels at least as large as a given fraction of // the graph's median channel size. // // The returned scores will be in the range [0.0, 1.0], where higher scores are // given to nodes already having high connectivity in the graph. // // NOTE: This is a part of the AttachmentHeuristic interface. func (p *PrefAttachment) NodeScores(g ChannelGraph, chans []LocalChannel, chanSize btcutil.Amount, nodes map[NodeID]struct{}) ( map[NodeID]*NodeScore, error) { // We first run though the graph once in order to find the median // channel size. var ( allChans []btcutil.Amount seenChans = make(map[uint64]struct{}) ) if err := g.ForEachNode(func(n Node) error { err := n.ForEachChannel(func(e ChannelEdge) error { if _, ok := seenChans[e.ChanID.ToUint64()]; ok { return nil } seenChans[e.ChanID.ToUint64()] = struct{}{} allChans = append(allChans, e.Capacity) return nil }) if err != nil { return err } return nil }); err != nil { return nil, err } medianChanSize := Median(allChans) log.Tracef("Found channel median %v for preferential score heuristic", medianChanSize) // Count the number of large-ish channels for each particular node in // the graph. var maxChans int nodeChanNum := make(map[NodeID]int) if err := g.ForEachNode(func(n Node) error { var nodeChans int err := n.ForEachChannel(func(e ChannelEdge) error { // Since connecting to nodes with a lot of small // channels actually worsens our connectivity in the // graph (we will potentially waste time trying to use // these useless channels in path finding), we decrease // the counter for such channels. if e.Capacity < medianChanSize/minMedianChanSizeFraction { nodeChans-- return nil } // Larger channels we count. nodeChans++ return nil }) if err != nil { return err } // We keep track of the highest-degree node we've seen, as this // will be given the max score. if nodeChans > maxChans { maxChans = nodeChans } // If this node is not among our nodes to score, we can return // early. nID := NodeID(n.PubKey()) if _, ok := nodes[nID]; !ok { log.Tracef("Node %x not among nodes to score, "+ "ignoring", nID[:]) return nil } // Otherwise we'll record the number of channels. nodeChanNum[nID] = nodeChans log.Tracef("Counted %v channels for node %x", nodeChans, nID[:]) return nil }); err != nil { return nil, err } // If there are no channels in the graph we cannot determine any // preferences, so we return, indicating all candidates get a score of // zero. if maxChans == 0 { log.Tracef("No channels in the graph") return nil, nil } existingPeers := make(map[NodeID]struct{}) for _, c := range chans { existingPeers[c.Node] = struct{}{} } // For each node in the set of nodes, count their fraction of channels // in the graph, and use that as the score. candidates := make(map[NodeID]*NodeScore) for nID, nodeChans := range nodeChanNum { // If the node is among or existing channel peers, we don't // need another channel. if _, ok := existingPeers[nID]; ok { log.Tracef("Node %x among existing peers for pref "+ "attach heuristic, giving zero score", nID[:]) continue } // If the node had no large channels, we skip it, since it // would have gotten a zero score anyway. if nodeChans <= 0 { log.Tracef("Skipping node %x with channel count %v", nID[:], nodeChans) continue } // Otherwise we score the node according to its fraction of // channels in the graph, scaled such that the highest-degree // node will be given a score of 1.0. score := float64(nodeChans) / float64(maxChans) log.Tracef("Giving node %x a pref attach score of %v", nID[:], score) candidates[nID] = &NodeScore{ NodeID: nID, Score: score, } } return candidates, nil }