routing: extract probability estimator cfg and add validation

In preparation for allowing live update of mc config, we extract our
probability estimator cfg for easy update and add validation.
This commit is contained in:
carla 2021-01-19 10:57:13 +02:00
parent 7b24b586a0
commit e10e8f11de
No known key found for this signature in database
GPG Key ID: 4CA7FE54A6213C91
7 changed files with 116 additions and 50 deletions

View File

@ -58,9 +58,11 @@ func newIntegratedRoutingContext(t *testing.T) *integratedRoutingContext {
finalExpiry: 40, finalExpiry: 40,
mcCfg: MissionControlConfig{ mcCfg: MissionControlConfig{
PenaltyHalfLife: 30 * time.Minute, ProbabilityEstimatorCfg: ProbabilityEstimatorCfg{
AprioriHopProbability: 0.6, PenaltyHalfLife: 30 * time.Minute,
AprioriWeight: 0.5, AprioriHopProbability: 0.6,
AprioriWeight: 0.5,
},
}, },
pathFindingCfg: PathFindingConfig{ pathFindingCfg: PathFindingConfig{

View File

@ -1,6 +1,7 @@
package routing package routing
import ( import (
"errors"
"fmt" "fmt"
"sync" "sync"
"time" "time"
@ -58,6 +59,17 @@ const (
DefaultMinFailureRelaxInterval = time.Minute 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. // NodeResults contains previous results from a node to its peers.
type NodeResults map[route.Vertex]TimedPairResult type NodeResults map[route.Vertex]TimedPairResult
@ -99,33 +111,36 @@ type MissionControl struct {
// MissionControlConfig defines parameters that control mission control // MissionControlConfig defines parameters that control mission control
// behaviour. // behaviour.
type MissionControlConfig struct { type MissionControlConfig struct {
// PenaltyHalfLife defines after how much time a penalized node or // ProbabilityEstimatorConfig is the config we will use for probability
// channel is back at 50% probability. // calculations.
PenaltyHalfLife time.Duration ProbabilityEstimatorCfg
// AprioriHopProbability is the assumed success probability of a hop in
// a route when no other information is available.
AprioriHopProbability float64
// MaxMcHistory defines the maximum number of payment results that are // MaxMcHistory defines the maximum number of payment results that are
// held on disk. // held on disk.
MaxMcHistory int 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 // MinFailureRelaxInterval is the minimum time that must have passed
// since the previously recorded failure before the failure amount may // since the previously recorded failure before the failure amount may
// be raised. // be raised.
MinFailureRelaxInterval time.Duration 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. // String returns a string representation of a mission control config.
func (c *MissionControlConfig) String() string { func (c *MissionControlConfig) String() string {
return fmt.Sprintf("Penalty Half Life: %v, Apriori Hop "+ 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) log.Debugf("Instantiating mission control with config: %v", cfg)
if err := cfg.validate(); err != nil {
return nil, err
}
store, err := newMissionControlStore(db, cfg.MaxMcHistory) store, err := newMissionControlStore(db, cfg.MaxMcHistory)
if err != nil { if err != nil {
return nil, err return nil, err
} }
estimator := &probabilityEstimator{ estimator := &probabilityEstimator{
aprioriHopProbability: cfg.AprioriHopProbability, ProbabilityEstimatorCfg: cfg.ProbabilityEstimatorCfg,
aprioriWeight: cfg.AprioriWeight, prevSuccessProbability: prevSuccessProbability,
penaltyHalfLife: cfg.PenaltyHalfLife,
prevSuccessProbability: prevSuccessProbability,
} }
mc := &MissionControl{ mc := &MissionControl{

View File

@ -80,9 +80,11 @@ func (ctx *mcTestContext) restartMc() {
mc, err := NewMissionControl( mc, err := NewMissionControl(
ctx.db, mcTestSelf, ctx.db, mcTestSelf,
&MissionControlConfig{ &MissionControlConfig{
PenaltyHalfLife: testPenaltyHalfLife, ProbabilityEstimatorCfg: ProbabilityEstimatorCfg{
AprioriHopProbability: testAprioriHopProbability, PenaltyHalfLife: testPenaltyHalfLife,
AprioriWeight: testAprioriWeight, AprioriHopProbability: testAprioriHopProbability,
AprioriWeight: testAprioriWeight,
},
}, },
) )
if err != nil { if err != nil {

View File

@ -1,6 +1,7 @@
package routing package routing
import ( import (
"errors"
"math" "math"
"time" "time"
@ -8,25 +9,61 @@ import (
"github.com/lightningnetwork/lnd/routing/route" "github.com/lightningnetwork/lnd/routing/route"
) )
// probabilityEstimator returns node and pair probabilities based on historical var (
// payment results. // ErrInvalidHalflife is returned when we get an invalid half life.
type probabilityEstimator struct { ErrInvalidHalflife = errors.New("penalty half life must be >= 0")
// penaltyHalfLife defines after how much time a penalized node or
// 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. // 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. // 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 // extent historical results should be extrapolated to untried
// connections. Setting it to one will completely ignore historical // connections. Setting it to one will completely ignore historical
// results and always assume the configured a priori probability for // results and always assume the configured a priori probability for
// untried connections. A value of zero will ignore the a priori // untried connections. A value of zero will ignore the a priori
// probability completely and only base the probability on historical // probability completely and only base the probability on historical
// results, unless there are none available. // 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 // prevSuccessProbability is the assumed probability for node pairs that
// successfully relayed the previous attempt. // 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 // If the channel history is not to be taken into account, we can return
// early here with the configured a priori probability. // early here with the configured a priori probability.
if p.aprioriWeight == 1 { if p.AprioriWeight == 1 {
return p.aprioriHopProbability return p.AprioriHopProbability
} }
// If there is no channel history, our best estimate is still the a // If there is no channel history, our best estimate is still the a
// priori probability. // priori probability.
if len(results) == 0 { 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 // 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 // the weighted average calculation below. When the apriori weight
// approaches 1, the apriori factor goes to infinity. It will heavily // approaches 1, the apriori factor goes to infinity. It will heavily
// outweigh any observations that have been collected. // 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 // Calculate a weighted average consisting of the apriori probability
// and historical observations. This is the part that incentivizes nodes // 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 // effectively prunes all channels of the node forever. This is the most
// aggressive way in which we can penalize nodes and unlikely to yield // aggressive way in which we can penalize nodes and unlikely to yield
// good results in a real network. // good results in a real network.
probabilitiesTotal := p.aprioriHopProbability * aprioriFactor probabilitiesTotal := p.AprioriHopProbability * aprioriFactor
totalWeight := aprioriFactor totalWeight := aprioriFactor
for _, result := range results { 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 // the result is fresh and asymptotically approaches zero over time. The rate at
// which this happens is controlled by the penaltyHalfLife parameter. // which this happens is controlled by the penaltyHalfLife parameter.
func (p *probabilityEstimator) getWeight(age time.Duration) float64 { 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) return math.Pow(2, exp)
} }

View File

@ -40,9 +40,11 @@ func newEstimatorTestContext(t *testing.T) *estimatorTestContext {
return &estimatorTestContext{ return &estimatorTestContext{
t: t, t: t,
estimator: &probabilityEstimator{ estimator: &probabilityEstimator{
aprioriHopProbability: aprioriHopProb, ProbabilityEstimatorCfg: ProbabilityEstimatorCfg{
aprioriWeight: aprioriWeight, AprioriHopProbability: aprioriHopProb,
penaltyHalfLife: time.Hour, AprioriWeight: aprioriWeight,
PenaltyHalfLife: time.Hour,
},
prevSuccessProbability: aprioriPrevSucProb, prevSuccessProbability: aprioriPrevSucProb,
}, },
} }

View File

@ -85,9 +85,11 @@ func createTestCtxFromGraphInstance(startingHeight uint32, graphInstance *testGr
} }
mcConfig := &MissionControlConfig{ mcConfig := &MissionControlConfig{
PenaltyHalfLife: time.Hour, ProbabilityEstimatorCfg: ProbabilityEstimatorCfg{
AprioriHopProbability: 0.9, PenaltyHalfLife: time.Hour,
AprioriWeight: 0.5, AprioriHopProbability: 0.9,
AprioriWeight: 0.5,
},
} }
mc, err := NewMissionControl( mc, err := NewMissionControl(

View File

@ -725,13 +725,17 @@ func newServer(cfg *Config, listenAddrs []net.Addr,
// servers, the mission control instance itself can be moved there too. // servers, the mission control instance itself can be moved there too.
routingConfig := routerrpc.GetRoutingConfig(cfg.SubRPCServers.RouterRPC) routingConfig := routerrpc.GetRoutingConfig(cfg.SubRPCServers.RouterRPC)
estimatorCfg := routing.ProbabilityEstimatorCfg{
AprioriHopProbability: routingConfig.AprioriHopProbability,
PenaltyHalfLife: routingConfig.PenaltyHalfLife,
AprioriWeight: routingConfig.AprioriWeight,
}
s.missionControl, err = routing.NewMissionControl( s.missionControl, err = routing.NewMissionControl(
remoteChanDB, selfNode.PubKeyBytes, remoteChanDB, selfNode.PubKeyBytes,
&routing.MissionControlConfig{ &routing.MissionControlConfig{
AprioriHopProbability: routingConfig.AprioriHopProbability, ProbabilityEstimatorCfg: estimatorCfg,
PenaltyHalfLife: routingConfig.PenaltyHalfLife,
MaxMcHistory: routingConfig.MaxMcHistory, MaxMcHistory: routingConfig.MaxMcHistory,
AprioriWeight: routingConfig.AprioriWeight,
MinFailureRelaxInterval: routing.DefaultMinFailureRelaxInterval, MinFailureRelaxInterval: routing.DefaultMinFailureRelaxInterval,
}, },
) )