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  }