diff --git a/routing/integrated_routing_context_test.go b/routing/integrated_routing_context_test.go index 951858ad..6399dc47 100644 --- a/routing/integrated_routing_context_test.go +++ b/routing/integrated_routing_context_test.go @@ -58,9 +58,11 @@ func newIntegratedRoutingContext(t *testing.T) *integratedRoutingContext { finalExpiry: 40, mcCfg: MissionControlConfig{ - PenaltyHalfLife: 30 * time.Minute, - AprioriHopProbability: 0.6, - AprioriWeight: 0.5, + ProbabilityEstimatorCfg: ProbabilityEstimatorCfg{ + PenaltyHalfLife: 30 * time.Minute, + AprioriHopProbability: 0.6, + AprioriWeight: 0.5, + }, }, pathFindingCfg: PathFindingConfig{ diff --git a/routing/missioncontrol.go b/routing/missioncontrol.go index 2c1ca116..d4d61164 100644 --- a/routing/missioncontrol.go +++ b/routing/missioncontrol.go @@ -1,6 +1,7 @@ package routing import ( + "errors" "fmt" "sync" "time" @@ -58,6 +59,17 @@ const ( DefaultMinFailureRelaxInterval = time.Minute ) +var ( + // ErrInvalidMcHistory is returned if we get a negative mission control + // history count. + ErrInvalidMcHistory = errors.New("mission control history must be " + + ">= 0") + + // ErrInvalidFailureInterval is returned if we get an invalid failure + // interval. + ErrInvalidFailureInterval = errors.New("failure interval must be >= 0") +) + // NodeResults contains previous results from a node to its peers. type NodeResults map[route.Vertex]TimedPairResult @@ -99,33 +111,36 @@ type MissionControl struct { // MissionControlConfig defines parameters that control mission control // behaviour. type MissionControlConfig struct { - // PenaltyHalfLife defines after how much time a penalized node or - // channel is back at 50% probability. - PenaltyHalfLife time.Duration - - // AprioriHopProbability is the assumed success probability of a hop in - // a route when no other information is available. - AprioriHopProbability float64 + // ProbabilityEstimatorConfig is the config we will use for probability + // calculations. + ProbabilityEstimatorCfg // MaxMcHistory defines the maximum number of payment results that are // held on disk. MaxMcHistory int - // AprioriWeight is a value in the range [0, 1] that defines to what - // extent historical results should be extrapolated to untried - // connections. Setting it to one will completely ignore historical - // results and always assume the configured a priori probability for - // untried connections. A value of zero will ignore the a priori - // probability completely and only base the probability on historical - // results, unless there are none available. - AprioriWeight float64 - // MinFailureRelaxInterval is the minimum time that must have passed // since the previously recorded failure before the failure amount may // be raised. MinFailureRelaxInterval time.Duration } +func (c *MissionControlConfig) validate() error { + if err := c.ProbabilityEstimatorCfg.validate(); err != nil { + return err + } + + if c.MaxMcHistory < 0 { + return ErrInvalidMcHistory + } + + if c.MinFailureRelaxInterval < 0 { + return ErrInvalidFailureInterval + } + + return nil +} + // String returns a string representation of a mission control config. func (c *MissionControlConfig) String() string { return fmt.Sprintf("Penalty Half Life: %v, Apriori Hop "+ @@ -190,16 +205,18 @@ func NewMissionControl(db kvdb.Backend, self route.Vertex, log.Debugf("Instantiating mission control with config: %v", cfg) + if err := cfg.validate(); err != nil { + return nil, err + } + store, err := newMissionControlStore(db, cfg.MaxMcHistory) if err != nil { return nil, err } estimator := &probabilityEstimator{ - aprioriHopProbability: cfg.AprioriHopProbability, - aprioriWeight: cfg.AprioriWeight, - penaltyHalfLife: cfg.PenaltyHalfLife, - prevSuccessProbability: prevSuccessProbability, + ProbabilityEstimatorCfg: cfg.ProbabilityEstimatorCfg, + prevSuccessProbability: prevSuccessProbability, } mc := &MissionControl{ diff --git a/routing/missioncontrol_test.go b/routing/missioncontrol_test.go index a18da7ba..48922a3b 100644 --- a/routing/missioncontrol_test.go +++ b/routing/missioncontrol_test.go @@ -80,9 +80,11 @@ func (ctx *mcTestContext) restartMc() { mc, err := NewMissionControl( ctx.db, mcTestSelf, &MissionControlConfig{ - PenaltyHalfLife: testPenaltyHalfLife, - AprioriHopProbability: testAprioriHopProbability, - AprioriWeight: testAprioriWeight, + ProbabilityEstimatorCfg: ProbabilityEstimatorCfg{ + PenaltyHalfLife: testPenaltyHalfLife, + AprioriHopProbability: testAprioriHopProbability, + AprioriWeight: testAprioriWeight, + }, }, ) if err != nil { diff --git a/routing/probability_estimator.go b/routing/probability_estimator.go index 238a765f..7cd2df16 100644 --- a/routing/probability_estimator.go +++ b/routing/probability_estimator.go @@ -1,6 +1,7 @@ package routing import ( + "errors" "math" "time" @@ -8,25 +9,61 @@ import ( "github.com/lightningnetwork/lnd/routing/route" ) -// probabilityEstimator returns node and pair probabilities based on historical -// payment results. -type probabilityEstimator struct { - // penaltyHalfLife defines after how much time a penalized node or +var ( + // ErrInvalidHalflife is returned when we get an invalid half life. + ErrInvalidHalflife = errors.New("penalty half life must be >= 0") + + // ErrInvalidHopProbability is returned when we get an invalid hop + // probability. + ErrInvalidHopProbability = errors.New("hop probability must be in [0;1]") + + // ErrInvalidAprioriWeight is returned when we get an apriori weight + // that is out of range. + ErrInvalidAprioriWeight = errors.New("apriori weight must be in [0;1]") +) + +// ProbabilityEstimatorCfg contains configuration for our probability estimator. +type ProbabilityEstimatorCfg struct { + // PenaltyHalfLife defines after how much time a penalized node or // channel is back at 50% probability. - penaltyHalfLife time.Duration + PenaltyHalfLife time.Duration - // aprioriHopProbability is the assumed success probability of a hop in + // AprioriHopProbability is the assumed success probability of a hop in // a route when no other information is available. - aprioriHopProbability float64 + AprioriHopProbability float64 - // aprioriWeight is a value in the range [0, 1] that defines to what + // AprioriWeight is a value in the range [0, 1] that defines to what // extent historical results should be extrapolated to untried // connections. Setting it to one will completely ignore historical // results and always assume the configured a priori probability for // untried connections. A value of zero will ignore the a priori // probability completely and only base the probability on historical // results, unless there are none available. - aprioriWeight float64 + AprioriWeight float64 +} + +func (p ProbabilityEstimatorCfg) validate() error { + if p.PenaltyHalfLife < 0 { + return ErrInvalidHalflife + } + + if p.AprioriHopProbability < 0 || p.AprioriHopProbability > 1 { + return ErrInvalidHopProbability + } + + if p.AprioriWeight < 0 || p.AprioriWeight > 1 { + return ErrInvalidAprioriWeight + } + + return nil +} + +// probabilityEstimator returns node and pair probabilities based on historical +// payment results. +type probabilityEstimator struct { + // ProbabilityEstimatorCfg contains configuration options for our + // estimator. + ProbabilityEstimatorCfg // prevSuccessProbability is the assumed probability for node pairs that // successfully relayed the previous attempt. @@ -41,14 +78,14 @@ func (p *probabilityEstimator) getNodeProbability(now time.Time, // If the channel history is not to be taken into account, we can return // early here with the configured a priori probability. - if p.aprioriWeight == 1 { - return p.aprioriHopProbability + if p.AprioriWeight == 1 { + return p.AprioriHopProbability } // If there is no channel history, our best estimate is still the a // priori probability. if len(results) == 0 { - return p.aprioriHopProbability + return p.AprioriHopProbability } // The value of the apriori weight is in the range [0, 1]. Convert it to @@ -58,7 +95,7 @@ func (p *probabilityEstimator) getNodeProbability(now time.Time, // the weighted average calculation below. When the apriori weight // approaches 1, the apriori factor goes to infinity. It will heavily // outweigh any observations that have been collected. - aprioriFactor := 1/(1-p.aprioriWeight) - 1 + aprioriFactor := 1/(1-p.AprioriWeight) - 1 // Calculate a weighted average consisting of the apriori probability // and historical observations. This is the part that incentivizes nodes @@ -76,7 +113,7 @@ func (p *probabilityEstimator) getNodeProbability(now time.Time, // effectively prunes all channels of the node forever. This is the most // aggressive way in which we can penalize nodes and unlikely to yield // good results in a real network. - probabilitiesTotal := p.aprioriHopProbability * aprioriFactor + probabilitiesTotal := p.AprioriHopProbability * aprioriFactor totalWeight := aprioriFactor for _, result := range results { @@ -106,7 +143,7 @@ func (p *probabilityEstimator) getNodeProbability(now time.Time, // the result is fresh and asymptotically approaches zero over time. The rate at // which this happens is controlled by the penaltyHalfLife parameter. func (p *probabilityEstimator) getWeight(age time.Duration) float64 { - exp := -age.Hours() / p.penaltyHalfLife.Hours() + exp := -age.Hours() / p.PenaltyHalfLife.Hours() return math.Pow(2, exp) } diff --git a/routing/probability_estimator_test.go b/routing/probability_estimator_test.go index 384b39b6..094c0550 100644 --- a/routing/probability_estimator_test.go +++ b/routing/probability_estimator_test.go @@ -40,9 +40,11 @@ func newEstimatorTestContext(t *testing.T) *estimatorTestContext { return &estimatorTestContext{ t: t, estimator: &probabilityEstimator{ - aprioriHopProbability: aprioriHopProb, - aprioriWeight: aprioriWeight, - penaltyHalfLife: time.Hour, + ProbabilityEstimatorCfg: ProbabilityEstimatorCfg{ + AprioriHopProbability: aprioriHopProb, + AprioriWeight: aprioriWeight, + PenaltyHalfLife: time.Hour, + }, prevSuccessProbability: aprioriPrevSucProb, }, } diff --git a/routing/router_test.go b/routing/router_test.go index 12f84772..a04aa7b3 100644 --- a/routing/router_test.go +++ b/routing/router_test.go @@ -85,9 +85,11 @@ func createTestCtxFromGraphInstance(startingHeight uint32, graphInstance *testGr } mcConfig := &MissionControlConfig{ - PenaltyHalfLife: time.Hour, - AprioriHopProbability: 0.9, - AprioriWeight: 0.5, + ProbabilityEstimatorCfg: ProbabilityEstimatorCfg{ + PenaltyHalfLife: time.Hour, + AprioriHopProbability: 0.9, + AprioriWeight: 0.5, + }, } mc, err := NewMissionControl( diff --git a/server.go b/server.go index 6326d6bb..f10e8494 100644 --- a/server.go +++ b/server.go @@ -725,13 +725,17 @@ func newServer(cfg *Config, listenAddrs []net.Addr, // servers, the mission control instance itself can be moved there too. routingConfig := routerrpc.GetRoutingConfig(cfg.SubRPCServers.RouterRPC) + estimatorCfg := routing.ProbabilityEstimatorCfg{ + AprioriHopProbability: routingConfig.AprioriHopProbability, + PenaltyHalfLife: routingConfig.PenaltyHalfLife, + AprioriWeight: routingConfig.AprioriWeight, + } + s.missionControl, err = routing.NewMissionControl( remoteChanDB, selfNode.PubKeyBytes, &routing.MissionControlConfig{ - AprioriHopProbability: routingConfig.AprioriHopProbability, - PenaltyHalfLife: routingConfig.PenaltyHalfLife, + ProbabilityEstimatorCfg: estimatorCfg, MaxMcHistory: routingConfig.MaxMcHistory, - AprioriWeight: routingConfig.AprioriWeight, MinFailureRelaxInterval: routing.DefaultMinFailureRelaxInterval, }, )