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
|
// returned channel candidates maps the NodeID to a NodeScore for the
|
||||||
// node.
|
// node.
|
||||||
//
|
//
|
||||||
// The scores will be in the range [0, M], where 0 indicates no
|
// The returned scores will be in the range [0, 1.0], where 0 indicates
|
||||||
// improvement in connectivity if a channel is opened to this node,
|
// no improvement in connectivity if a channel is opened to this node,
|
||||||
// while M is the maximum possible improvement in connectivity. The
|
// while 1.0 is the maximum possible improvement in connectivity. The
|
||||||
// size of M is up to the implementation of this interface, so scores
|
// implementation of this interface must return scores in this range to
|
||||||
// must be normalized if compared against other implementations.
|
// 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
|
// NOTE: A NodeID not found in the returned map is implicitly given a
|
||||||
// score of 0.
|
// score of 0.
|
||||||
|
@ -43,9 +43,10 @@ func NewNodeID(pub *btcec.PublicKey) NodeID {
|
|||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
|
|
||||||
// NodeScores is a method that given the current channel graph and
|
// NodeScores is a method that given the current channel graph and current set
|
||||||
// current set of local channels, scores the given nodes according to
|
// of local channels, scores the given nodes according to the preference of
|
||||||
// the preference of opening a channel of the given size with them.
|
// 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
|
// The heuristic employed by this method is one that attempts to promote a
|
||||||
// scale-free network globally, via local attachment preferences for new nodes
|
// 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{}) (
|
chanSize btcutil.Amount, nodes map[NodeID]struct{}) (
|
||||||
map[NodeID]*NodeScore, error) {
|
map[NodeID]*NodeScore, error) {
|
||||||
|
|
||||||
// Count the number of channels in the graph. We'll also count the
|
// Count the number of channels for each particular node in the graph.
|
||||||
// number of channels as we go for the nodes we are interested in.
|
var maxChans int
|
||||||
var graphChans int
|
|
||||||
nodeChanNum := make(map[NodeID]int)
|
nodeChanNum := make(map[NodeID]int)
|
||||||
if err := g.ForEachNode(func(n Node) error {
|
if err := g.ForEachNode(func(n Node) error {
|
||||||
var nodeChans int
|
var nodeChans int
|
||||||
err := n.ForEachChannel(func(_ ChannelEdge) error {
|
err := n.ForEachChannel(func(_ ChannelEdge) error {
|
||||||
nodeChans++
|
nodeChans++
|
||||||
graphChans++
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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
|
// If this node is not among our nodes to score, we can return
|
||||||
// early.
|
// early.
|
||||||
nID := NodeID(n.PubKey())
|
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
|
// If there are no channels in the graph we cannot determine any
|
||||||
// preferences, so we return, indicating all candidates get a score of
|
// preferences, so we return, indicating all candidates get a score of
|
||||||
// zero.
|
// zero.
|
||||||
if graphChans == 0 {
|
if maxChans == 0 {
|
||||||
return nil, nil
|
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
|
// Otherwise we score the node according to its fraction of
|
||||||
// channels in the graph.
|
// channels in the graph, scaled such that the highest-degree
|
||||||
score := float64(nodeChans) / float64(graphChans)
|
// node will be given a score of 1.0.
|
||||||
|
score := float64(nodeChans) / float64(maxChans)
|
||||||
candidates[nID] = &NodeScore{
|
candidates[nID] = &NodeScore{
|
||||||
NodeID: nID,
|
NodeID: nID,
|
||||||
Score: score,
|
Score: score,
|
||||||
|
@ -249,8 +249,8 @@ func TestPrefAttachmentSelectTwoVertexes(t *testing.T) {
|
|||||||
|
|
||||||
// Since each of the nodes has 1 channel, out
|
// Since each of the nodes has 1 channel, out
|
||||||
// of only one channel in the graph, we expect
|
// of only one channel in the graph, we expect
|
||||||
// their score to be 0.5.
|
// their score to be 1.0.
|
||||||
expScore := float64(0.5)
|
expScore := float64(1.0)
|
||||||
if candidate.Score != expScore {
|
if candidate.Score != expScore {
|
||||||
t1.Fatalf("expected candidate score "+
|
t1.Fatalf("expected candidate score "+
|
||||||
"to be %v, instead was %v",
|
"to be %v, instead was %v",
|
||||||
|
9
lnd.go
9
lnd.go
@ -313,10 +313,15 @@ func lndMain() error {
|
|||||||
return err
|
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
|
// used to manage the underlying autopilot agent, starting and stopping
|
||||||
// it at will.
|
// 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)
|
atplManager, err := autopilot.NewManager(atplCfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ltndLog.Errorf("unable to create autopilot manager: %v", err)
|
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
|
// 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
|
// and all interfaces needed to drive it won't be launched before the Manager's
|
||||||
// StartAgent method is called.
|
// 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))
|
atplLog.Infof("Instantiating autopilot with cfg: %v", spew.Sdump(cfg))
|
||||||
|
|
||||||
// Set up the constraints the autopilot heuristics must adhere to.
|
// 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.
|
// First, we'll create the preferential attachment heuristic.
|
||||||
prefAttachment := autopilot.NewPrefAttachment()
|
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
|
// With the heuristic itself created, we can now populate the remainder
|
||||||
// of the items that the autopilot agent needs to perform its duties.
|
// of the items that the autopilot agent needs to perform its duties.
|
||||||
self := svr.identityPriv.PubKey()
|
self := svr.identityPriv.PubKey()
|
||||||
pilotCfg := autopilot.Config{
|
pilotCfg := autopilot.Config{
|
||||||
Self: self,
|
Self: self,
|
||||||
Heuristic: prefAttachment,
|
Heuristic: weightedAttachment,
|
||||||
ChanController: &chanController{
|
ChanController: &chanController{
|
||||||
server: svr,
|
server: svr,
|
||||||
private: cfg.Private,
|
private: cfg.Private,
|
||||||
@ -202,5 +212,5 @@ func initAutoPilot(svr *server, cfg *autoPilotConfig) *autopilot.ManagerCfg {
|
|||||||
},
|
},
|
||||||
SubscribeTransactions: svr.cc.wallet.SubscribeTransactions,
|
SubscribeTransactions: svr.cc.wallet.SubscribeTransactions,
|
||||||
SubscribeTopology: svr.chanRouter.SubscribeTopology,
|
SubscribeTopology: svr.chanRouter.SubscribeTopology,
|
||||||
}
|
}, nil
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user