156 lines
6.0 KiB
Go
156 lines
6.0 KiB
Go
|
package routing
|
||
|
|
||
|
import (
|
||
|
"math"
|
||
|
"time"
|
||
|
|
||
|
"github.com/lightningnetwork/lnd/lnwire"
|
||
|
"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
|
||
|
// 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
|
||
|
|
||
|
// 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
|
||
|
|
||
|
// prevSuccessProbability is the assumed probability for node pairs that
|
||
|
// successfully relayed the previous attempt.
|
||
|
prevSuccessProbability float64
|
||
|
}
|
||
|
|
||
|
// getNodeProbability calculates the probability for connections from a node
|
||
|
// that have not been tried before. The results parameter is a list of last
|
||
|
// payment results for that node.
|
||
|
func (p *probabilityEstimator) getNodeProbability(now time.Time,
|
||
|
results NodeResults, amt lnwire.MilliSatoshi) float64 {
|
||
|
|
||
|
// 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 there is no channel history, our best estimate is still the a
|
||
|
// priori probability.
|
||
|
if len(results) == 0 {
|
||
|
return p.aprioriHopProbability
|
||
|
}
|
||
|
|
||
|
// The value of the apriori weight is in the range [0, 1]. Convert it to
|
||
|
// a factor that properly expresses the intention of the weight in the
|
||
|
// following weight average calculation. When the apriori weight is 0,
|
||
|
// the apriori factor is also 0. This means it won't have any effect on
|
||
|
// 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
|
||
|
|
||
|
// Calculate a weighted average consisting of the apriori probability
|
||
|
// and historical observations. This is the part that incentivizes nodes
|
||
|
// to make sure that all (not just some) of their channels are in good
|
||
|
// shape. Senders will steer around nodes that have shown a few
|
||
|
// failures, even though there may be many channels still untried.
|
||
|
//
|
||
|
// If there is just a single observation and the apriori weight is 0,
|
||
|
// this single observation will totally determine the node probability.
|
||
|
// The node probability is returned for all other channels of the node.
|
||
|
// This means that one failure will lead to the success probability
|
||
|
// estimates for all other channels being 0 too. The probability for the
|
||
|
// channel that was tried will not even recover, because it is
|
||
|
// recovering to the node probability (which is zero). So one failure
|
||
|
// 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
|
||
|
totalWeight := aprioriFactor
|
||
|
|
||
|
for _, result := range results {
|
||
|
age := now.Sub(result.timestamp)
|
||
|
|
||
|
switch {
|
||
|
// Weigh success with a constant high weight of 1. There is no
|
||
|
// decay.
|
||
|
case result.success:
|
||
|
totalWeight++
|
||
|
probabilitiesTotal += p.prevSuccessProbability
|
||
|
|
||
|
// Weigh failures in accordance with their age. The base
|
||
|
// probability of a failure is considered zero, so nothing needs
|
||
|
// to be added to probabilitiesTotal.
|
||
|
case amt >= result.minPenalizeAmt:
|
||
|
totalWeight += p.getWeight(age)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return probabilitiesTotal / totalWeight
|
||
|
}
|
||
|
|
||
|
// getWeight calculates a weight in the range [0, 1] that should be assigned to
|
||
|
// a payment result. Weight follows an exponential curve that starts at 1 when
|
||
|
// 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()
|
||
|
return math.Pow(2, exp)
|
||
|
}
|
||
|
|
||
|
// getPairProbability estimates the probability of successfully traversing to
|
||
|
// toNode based on historical payment outcomes for the from node. Those outcomes
|
||
|
// are passed in via the results parameter.
|
||
|
func (p *probabilityEstimator) getPairProbability(
|
||
|
now time.Time, results NodeResults,
|
||
|
toNode route.Vertex, amt lnwire.MilliSatoshi) float64 {
|
||
|
|
||
|
// Retrieve the last pair outcome.
|
||
|
lastPairResult, ok := results[toNode]
|
||
|
|
||
|
// If there is no history for this pair, return the node probability
|
||
|
// that is a probability estimate for untried channel.
|
||
|
if !ok {
|
||
|
return p.getNodeProbability(now, results, amt)
|
||
|
}
|
||
|
|
||
|
// For successes, we have a fixed (high) probability. Those pairs
|
||
|
// will be assumed good until proven otherwise.
|
||
|
if lastPairResult.success {
|
||
|
return p.prevSuccessProbability
|
||
|
}
|
||
|
|
||
|
nodeProbability := p.getNodeProbability(now, results, amt)
|
||
|
|
||
|
// Take into account a minimum penalize amount. For balance errors, a
|
||
|
// failure may be reported with such a minimum to prevent too aggressive
|
||
|
// penalization. If the current amount is smaller than the amount that
|
||
|
// previously triggered a failure, we act as if this is an untried
|
||
|
// channel.
|
||
|
if amt < lastPairResult.minPenalizeAmt {
|
||
|
return nodeProbability
|
||
|
}
|
||
|
|
||
|
timeSinceLastFailure := now.Sub(lastPairResult.timestamp)
|
||
|
|
||
|
// Calculate success probability based on the weight of the last
|
||
|
// failure. When the failure is fresh, its weight is 1 and we'll return
|
||
|
// probability 0. Over time the probability recovers to the node
|
||
|
// probability. It would be as if this channel was never tried before.
|
||
|
weight := p.getWeight(timeSinceLastFailure)
|
||
|
probability := nodeProbability * (1 - weight)
|
||
|
|
||
|
return probability
|
||
|
}
|