github.com/decred/dcrlnd@v0.7.6/autopilot/prefattach.go (about) 1 package autopilot 2 3 import ( 4 prand "math/rand" 5 "time" 6 7 "github.com/decred/dcrd/dcrec/secp256k1/v4" 8 "github.com/decred/dcrd/dcrutil/v4" 9 ) 10 11 // minMedianChanSizeFraction determines the minimum size a channel must have to 12 // count positively when calculating the scores using preferential attachment. 13 // The minimum channel size is calculated as median/minMedianChanSizeFraction, 14 // where median is the median channel size of the entire graph. 15 const minMedianChanSizeFraction = 4 16 17 // PrefAttachment is an implementation of the AttachmentHeuristic interface 18 // that implement a non-linear preferential attachment heuristic. This means 19 // that given a threshold to allocate to automatic channel establishment, the 20 // heuristic will attempt to favor connecting to nodes which already have a set 21 // amount of links, selected by sampling from a power law distribution. The 22 // attachment is non-linear in that it favors nodes with a higher in-degree but 23 // less so than regular linear preferential attachment. As a result, this 24 // creates smaller and less clusters than regular linear preferential 25 // attachment. 26 // 27 // TODO(roasbeef): BA, with k=-3 28 type PrefAttachment struct { 29 } 30 31 // NewPrefAttachment creates a new instance of a PrefAttachment heuristic. 32 func NewPrefAttachment() *PrefAttachment { 33 prand.Seed(time.Now().Unix()) 34 return &PrefAttachment{} 35 } 36 37 // A compile time assertion to ensure PrefAttachment meets the 38 // AttachmentHeuristic interface. 39 var _ AttachmentHeuristic = (*PrefAttachment)(nil) 40 41 // NodeID is a simple type that holds an EC public key serialized in compressed 42 // format. 43 type NodeID [33]byte 44 45 // NewNodeID creates a new nodeID from a passed public key. 46 func NewNodeID(pub *secp256k1.PublicKey) NodeID { 47 var n NodeID 48 copy(n[:], pub.SerializeCompressed()) 49 return n 50 } 51 52 // Name returns the name of this heuristic. 53 // 54 // NOTE: This is a part of the AttachmentHeuristic interface. 55 func (p *PrefAttachment) Name() string { 56 return "preferential" 57 } 58 59 // NodeScores is a method that given the current channel graph and current set 60 // of local channels, scores the given nodes according to the preference of 61 // opening a channel of the given size with them. The returned channel 62 // candidates maps the NodeID to a NodeScore for the node. 63 // 64 // The heuristic employed by this method is one that attempts to promote a 65 // scale-free network globally, via local attachment preferences for new nodes 66 // joining the network with an amount of available funds to be allocated to 67 // channels. Specifically, we consider the degree of each node (and the flow 68 // in/out of the node available via its open channels) and utilize the 69 // Barabási–Albert model to drive our recommended attachment heuristics. If 70 // implemented globally for each new participant, this results in a channel 71 // graph that is scale-free and follows a power law distribution with k=-3. 72 // 73 // To avoid assigning a high score to nodes with a large number of small 74 // channels, we only count channels at least as large as a given fraction of 75 // the graph's median channel size. 76 // 77 // The returned scores will be in the range [0.0, 1.0], where higher scores are 78 // given to nodes already having high connectivity in the graph. 79 // 80 // NOTE: This is a part of the AttachmentHeuristic interface. 81 func (p *PrefAttachment) NodeScores(g ChannelGraph, chans []LocalChannel, 82 chanSize dcrutil.Amount, nodes map[NodeID]struct{}) ( 83 map[NodeID]*NodeScore, error) { 84 85 // We first run though the graph once in order to find the median 86 // channel size. 87 var ( 88 allChans []dcrutil.Amount 89 seenChans = make(map[uint64]struct{}) 90 ) 91 if err := g.ForEachNode(func(n Node) error { 92 err := n.ForEachChannel(func(e ChannelEdge) error { 93 if _, ok := seenChans[e.ChanID.ToUint64()]; ok { 94 return nil 95 } 96 seenChans[e.ChanID.ToUint64()] = struct{}{} 97 allChans = append(allChans, e.Capacity) 98 return nil 99 }) 100 if err != nil { 101 return err 102 } 103 104 return nil 105 }); err != nil { 106 return nil, err 107 } 108 109 medianChanSize := Median(allChans) 110 log.Tracef("Found channel median %v for preferential score heuristic", 111 medianChanSize) 112 113 // Count the number of large-ish channels for each particular node in 114 // the graph. 115 var maxChans int 116 nodeChanNum := make(map[NodeID]int) 117 if err := g.ForEachNode(func(n Node) error { 118 var nodeChans int 119 err := n.ForEachChannel(func(e ChannelEdge) error { 120 // Since connecting to nodes with a lot of small 121 // channels actually worsens our connectivity in the 122 // graph (we will potentially waste time trying to use 123 // these useless channels in path finding), we decrease 124 // the counter for such channels. 125 if e.Capacity < medianChanSize/minMedianChanSizeFraction { 126 nodeChans-- 127 return nil 128 } 129 130 // Larger channels we count. 131 nodeChans++ 132 return nil 133 }) 134 if err != nil { 135 return err 136 } 137 138 // We keep track of the highest-degree node we've seen, as this 139 // will be given the max score. 140 if nodeChans > maxChans { 141 maxChans = nodeChans 142 } 143 144 // If this node is not among our nodes to score, we can return 145 // early. 146 nID := NodeID(n.PubKey()) 147 if _, ok := nodes[nID]; !ok { 148 log.Tracef("Node %x not among nodes to score, "+ 149 "ignoring", nID[:]) 150 return nil 151 } 152 153 // Otherwise we'll record the number of channels. 154 nodeChanNum[nID] = nodeChans 155 log.Tracef("Counted %v channels for node %x", nodeChans, nID[:]) 156 157 return nil 158 }); err != nil { 159 return nil, err 160 } 161 162 // If there are no channels in the graph we cannot determine any 163 // preferences, so we return, indicating all candidates get a score of 164 // zero. 165 if maxChans == 0 { 166 log.Tracef("No channels in the graph") 167 return nil, nil 168 } 169 170 existingPeers := make(map[NodeID]struct{}) 171 for _, c := range chans { 172 existingPeers[c.Node] = struct{}{} 173 } 174 175 // For each node in the set of nodes, count their fraction of channels 176 // in the graph, and use that as the score. 177 candidates := make(map[NodeID]*NodeScore) 178 for nID, nodeChans := range nodeChanNum { 179 180 // If the node is among or existing channel peers, we don't 181 // need another channel. 182 if _, ok := existingPeers[nID]; ok { 183 log.Tracef("Node %x among existing peers for pref "+ 184 "attach heuristic, giving zero score", nID[:]) 185 continue 186 } 187 188 // If the node had no large channels, we skip it, since it 189 // would have gotten a zero score anyway. 190 if nodeChans <= 0 { 191 log.Tracef("Skipping node %x with channel count %v", 192 nID[:], nodeChans) 193 continue 194 } 195 196 // Otherwise we score the node according to its fraction of 197 // channels in the graph, scaled such that the highest-degree 198 // node will be given a score of 1.0. 199 score := float64(nodeChans) / float64(maxChans) 200 log.Tracef("Giving node %x a pref attach score of %v", 201 nID[:], score) 202 203 candidates[nID] = &NodeScore{ 204 NodeID: nID, 205 Score: score, 206 } 207 } 208 209 return candidates, nil 210 }