Merge pull request #2405 from halseth/autopilot-weighted-heuristics-follow-up
[autopilot] Weighted combined attachment heuristic
This commit is contained in:
commit
cebc4d8dba
121
autopilot/combinedattach.go
Normal file
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
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)
|
||||
|
16
pilot.go
16
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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user