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:
parent
7b24b586a0
commit
e10e8f11de
@ -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(
|
||||
|
10
server.go
10
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,
|
||||
},
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user