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

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

@ -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,15 +205,17 @@ 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,
ProbabilityEstimatorCfg: cfg.ProbabilityEstimatorCfg,
prevSuccessProbability: prevSuccessProbability,
}

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

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

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

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

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