Merge pull request #2405 from halseth/autopilot-weighted-heuristics-follow-up

[autopilot] Weighted combined attachment heuristic
This commit is contained in:
Olaoluwa Osuntokun 2019-01-22 19:08:46 -08:00 committed by GitHub
commit cebc4d8dba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 165 additions and 22 deletions

121
autopilot/combinedattach.go Normal file

@ -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
}

@ -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.

@ -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,

@ -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",

9
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)

@ -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
}