diff --git a/autopilot/combinedattach.go b/autopilot/combinedattach.go new file mode 100644 index 00000000..deaa4af8 --- /dev/null +++ b/autopilot/combinedattach.go @@ -0,0 +1,121 @@ +package autopilot + +import ( + "fmt" + + "github.com/btcsuite/btcutil" +) + +// WeightedHeuristic is a tuple that associates a weight to an +// AttachmentHeuristic. This is used to determining a node's final score when +// querying several heuristics for scores. +type WeightedHeuristic struct { + // Weight is this AttachmentHeuristic's relative weight factor. It + // should be between 0.0 and 1.0. + Weight float64 + + AttachmentHeuristic +} + +// WeightedCombAttachment is an implementation of the AttachmentHeuristic +// interface that combines the scores given by several sub-heuristics into one. +type WeightedCombAttachment struct { + heuristics []*WeightedHeuristic +} + +// NewWeightedCombAttachment creates a new instance of a WeightedCombAttachment. +func NewWeightedCombAttachment(h ...*WeightedHeuristic) ( + AttachmentHeuristic, error) { + + // The sum of weights given to the sub-heuristics must sum to exactly + // 1.0. + var sum float64 + for _, w := range h { + sum += w.Weight + } + + if sum != 1.0 { + return nil, fmt.Errorf("weights MUST sum to 1.0 (was %v)", sum) + } + + return &WeightedCombAttachment{ + heuristics: h, + }, nil +} + +// A compile time assertion to ensure WeightedCombAttachment meets the +// AttachmentHeuristic interface. +var _ AttachmentHeuristic = (*WeightedCombAttachment)(nil) + +// NodeScores is a method that given the current channel graph, current set of +// local channels and funds available, scores the given nodes according to the +// preference of opening a channel with them. The returned channel candidates +// maps the NodeID to an attachment directive containing a score and a channel +// size. +// +// The scores is determined by quering the set of sub-heuristics, then +// combining these scores into a final score according to the active +// configuration. +// +// The returned scores will be in the range [0, 1.0], where 0 indicates no +// improvement in connectivity if a channel is opened to this node, while 1.0 +// is the maximum possible improvement in connectivity. +// +// NOTE: This is a part of the AttachmentHeuristic interface. +func (c *WeightedCombAttachment) NodeScores(g ChannelGraph, chans []Channel, + chanSize btcutil.Amount, nodes map[NodeID]struct{}) ( + map[NodeID]*NodeScore, error) { + + // We now query each heuristic to determine the score they give to the + // nodes for the given channel size. + var subScores []map[NodeID]*NodeScore + for _, h := range c.heuristics { + s, err := h.NodeScores( + g, chans, chanSize, nodes, + ) + if err != nil { + return nil, fmt.Errorf("unable to get sub score: %v", + err) + } + + subScores = append(subScores, s) + } + + // We combine the scores given by the sub-heuristics by using the + // heruistics' given weight factor. + scores := make(map[NodeID]*NodeScore) + for nID := range nodes { + score := &NodeScore{ + NodeID: nID, + } + + // Each sub-heuristic should have scored the node, if not it is + // implicitly given a zero score by that heuristic. + for i, h := range c.heuristics { + sub, ok := subScores[i][nID] + if !ok { + continue + } + // Use the heuristic's weight factor to determine of + // how much weight we should give to this particular + // score. + score.Score += h.Weight * sub.Score + } + + switch { + // Instead of adding a node with score 0 to the returned set, + // we just skip it. + case score.Score == 0: + continue + + // Sanity check the new score. + case score.Score < 0 || score.Score > 1.0: + return nil, fmt.Errorf("Invalid node score from "+ + "combination: %v", score.Score) + } + + scores[nID] = score + } + + return scores, nil +} diff --git a/autopilot/interface.go b/autopilot/interface.go index 51405e7f..ec80d6a2 100644 --- a/autopilot/interface.go +++ b/autopilot/interface.go @@ -125,11 +125,12 @@ type AttachmentHeuristic interface { // returned channel candidates maps the NodeID to a NodeScore for the // node. // - // The scores will be in the range [0, M], where 0 indicates no - // improvement in connectivity if a channel is opened to this node, - // while M is the maximum possible improvement in connectivity. The - // size of M is up to the implementation of this interface, so scores - // must be normalized if compared against other implementations. + // The returned scores will be in the range [0, 1.0], where 0 indicates + // no improvement in connectivity if a channel is opened to this node, + // while 1.0 is the maximum possible improvement in connectivity. The + // implementation of this interface must return scores in this range to + // properly allow the autopilot agent to make a reasonable choice based + // on the score from multiple heuristics. // // NOTE: A NodeID not found in the returned map is implicitly given a // score of 0. diff --git a/autopilot/prefattach.go b/autopilot/prefattach.go index af5f4569..bb010239 100644 --- a/autopilot/prefattach.go +++ b/autopilot/prefattach.go @@ -43,9 +43,10 @@ func NewNodeID(pub *btcec.PublicKey) NodeID { return n } -// 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. +// 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 @@ -64,21 +65,25 @@ func (p *PrefAttachment) NodeScores(g ChannelGraph, chans []Channel, chanSize btcutil.Amount, nodes map[NodeID]struct{}) ( map[NodeID]*NodeScore, error) { - // Count the number of channels in the graph. We'll also count the - // number of channels as we go for the nodes we are interested in. - var graphChans int + // Count the number of 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(_ ChannelEdge) error { nodeChans++ - graphChans++ 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()) @@ -97,7 +102,7 @@ func (p *PrefAttachment) NodeScores(g ChannelGraph, chans []Channel, // 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 graphChans == 0 { + if maxChans == 0 { return nil, nil } @@ -127,8 +132,9 @@ func (p *PrefAttachment) NodeScores(g ChannelGraph, chans []Channel, } // Otherwise we score the node according to its fraction of - // channels in the graph. - score := float64(nodeChans) / float64(graphChans) + // channels in the graph, scaled such that the highest-degree + // node will be given a score of 1.0. + score := float64(nodeChans) / float64(maxChans) candidates[nID] = &NodeScore{ NodeID: nID, Score: score, diff --git a/autopilot/prefattach_test.go b/autopilot/prefattach_test.go index b3491041..f987d70e 100644 --- a/autopilot/prefattach_test.go +++ b/autopilot/prefattach_test.go @@ -249,8 +249,8 @@ func TestPrefAttachmentSelectTwoVertexes(t *testing.T) { // Since each of the nodes has 1 channel, out // of only one channel in the graph, we expect - // their score to be 0.5. - expScore := float64(0.5) + // their score to be 1.0. + expScore := float64(1.0) if candidate.Score != expScore { t1.Fatalf("expected candidate score "+ "to be %v, instead was %v", diff --git a/lnd.go b/lnd.go index f6219811..46adb4a4 100644 --- a/lnd.go +++ b/lnd.go @@ -313,10 +313,15 @@ func lndMain() error { return err } - // Set up an auotpilot manager from the current config. This will be + // Set up an autopilot manager from the current config. This will be // used to manage the underlying autopilot agent, starting and stopping // it at will. - atplCfg := initAutoPilot(server, cfg.Autopilot) + atplCfg, err := initAutoPilot(server, cfg.Autopilot) + if err != nil { + ltndLog.Errorf("unable to init autopilot: %v", err) + return err + } + atplManager, err := autopilot.NewManager(atplCfg) if err != nil { ltndLog.Errorf("unable to create autopilot manager: %v", err) diff --git a/pilot.go b/pilot.go index b6d9f63f..3c3e1659 100644 --- a/pilot.go +++ b/pilot.go @@ -83,7 +83,7 @@ var _ autopilot.ChannelController = (*chanController)(nil) // autopilot.Agent instance based on the passed configuration struct. The agent // and all interfaces needed to drive it won't be launched before the Manager's // StartAgent method is called. -func initAutoPilot(svr *server, cfg *autoPilotConfig) *autopilot.ManagerCfg { +func initAutoPilot(svr *server, cfg *autoPilotConfig) (*autopilot.ManagerCfg, error) { atplLog.Infof("Instantiating autopilot with cfg: %v", spew.Sdump(cfg)) // Set up the constraints the autopilot heuristics must adhere to. @@ -98,12 +98,22 @@ func initAutoPilot(svr *server, cfg *autoPilotConfig) *autopilot.ManagerCfg { // First, we'll create the preferential attachment heuristic. prefAttachment := autopilot.NewPrefAttachment() + weightedAttachment, err := autopilot.NewWeightedCombAttachment( + &autopilot.WeightedHeuristic{ + Weight: 1.0, + AttachmentHeuristic: prefAttachment, + }, + ) + if err != nil { + return nil, err + } + // With the heuristic itself created, we can now populate the remainder // of the items that the autopilot agent needs to perform its duties. self := svr.identityPriv.PubKey() pilotCfg := autopilot.Config{ Self: self, - Heuristic: prefAttachment, + Heuristic: weightedAttachment, ChanController: &chanController{ server: svr, private: cfg.Private, @@ -202,5 +212,5 @@ func initAutoPilot(svr *server, cfg *autoPilotConfig) *autopilot.ManagerCfg { }, SubscribeTransactions: svr.cc.wallet.SubscribeTransactions, SubscribeTopology: svr.chanRouter.SubscribeTopology, - } + }, nil }