routing: limit capacity factor and tune parameters

* The maximal reduction in the probability is limited to 0.5 (previously
  ~0.05), such that we don't get too low apriori probabilities.
  Otherwise, this may lead to a too strong selection of large (and maybe
  expensive) channels. A two-hop path would get total probability
  penalties of:

  - 1000PPM/(0.6*0.6) = 2778 PPM in the unsaturated case
  - 1000PPM/(0.6*(0.6*0.5)) = 5556 PPM in the saturated case, where the
    second hop is saturated

  The difference in PPM of 2778 PPM should be enough to bias towards the
  first path.

* The smearing factor is reduced. Previously we had to keep a higher
  smearing factor in order to make the capacity factor not go to zero
  for high amounts, to still give a fully saturated channel a chance.
  This is not needed anymore due to the capping to 0.5. A lower value of
  the smearing factor lets us more precisely choose a capacity fraction
  and the capacity factor is more neutral when it comes to intermediate
  amounts.

We set a conservative default value for the capacity fraction, which
still has the effect of discarding exhausted channels, giving a
noticeable effect when about 90% of the capacity is being used.
This commit is contained in:
bitromortac 2023-02-18 09:42:09 +01:00
parent a73581610e
commit 75a9dc9103
No known key found for this signature in database
GPG Key ID: 1965063FC13BEBE2
2 changed files with 59 additions and 36 deletions

View File

@ -16,31 +16,24 @@ const (
// capacity-related probability reweighting works. CapacityFraction
// defines the fraction of the channel capacity at which the effect
// roughly sets in and capacitySmearingFraction defines over which range
// the factor changes from 1 to 0.
//
// We may fall below the minimum required probability
// (DefaultMinRouteProbability) when the amount comes close to the
// available capacity of a single channel of the route in case of no
// prior knowledge about the channels. We want such routes still to be
// available and therefore a probability reduction should not completely
// drop the total probability below DefaultMinRouteProbability.
// For this to hold for a three-hop route we require:
// (DefaultAprioriHopProbability)^3 * minCapacityFactor >
// DefaultMinRouteProbability
//
// For DefaultAprioriHopProbability = 0.6 and
// DefaultMinRouteProbability = 0.01 this results in
// minCapacityFactor ~ 0.05. The following combination of parameters
// fulfill the requirement with capacityFactor(cap, cap) ~ 0.076 (see
// tests).
// the factor changes from 1 to minCapacityFactor.
// DefaultCapacityFraction is the default value for CapacityFraction.
DefaultCapacityFraction = 0.75
// It is chosen such that the capacity factor is active but with a small
// effect. This value together with capacitySmearingFraction leads to a
// noticeable reduction in probability if the amount starts to come
// close to 90% of a channel's capacity.
DefaultCapacityFraction = 0.9999
// We don't want to have a sharp drop of the capacity factor to zero at
// capacityCutoffFraction, but a smooth smearing such that some residual
// probability is left when spending the whole amount, see above.
capacitySmearingFraction = 0.1
// capacitySmearingFraction defines how quickly the capacity factor
// drops from 1 to minCapacityFactor. This value results in about a
// variation over 20% of the capacity.
capacitySmearingFraction = 0.025
// minCapacityFactor is the minimal value the capacityFactor can take.
// Having a too low value can lead to discarding of paths due to the
// enforced minimal proability or to too high pathfinding weights.
minCapacityFactor = 0.5
// minCapacityFraction is the minimum allowed value for
// CapacityFraction. The success probability in the random balance model
@ -265,10 +258,13 @@ func (p *AprioriEstimator) getWeight(age time.Duration) float64 {
}
// capacityFactor is a multiplier that can be used to reduce the probability
// depending on how much of the capacity is sent. The limits are 1 for amt == 0
// and 0 for amt >> cutoffMsat. The function drops significantly when amt
// reaches cutoffMsat. smearingMsat determines over which scale the reduction
// takes place.
// depending on how much of the capacity is sent. In other words, the factor
// sorts out channels that don't provide enough liquidity. Effectively, this
// leads to usage of larger channels in total to increase success probability,
// but it may also increase fees. The limits are 1 for amt == 0 and
// minCapacityFactor for amt >> capacityCutoffFraction. The function drops
// significantly when amt reaches cutoffMsat. smearingMsat determines over which
// scale the reduction takes place.
func capacityFactor(amt lnwire.MilliSatoshi, capacity btcutil.Amount,
capacityCutoffFraction float64) float64 {
@ -299,7 +295,11 @@ func capacityFactor(amt lnwire.MilliSatoshi, capacity btcutil.Amount,
// at cutoffMsat, decaying over the smearingMsat scale.
denominator := 1 + math.Exp(-(amtMsat-cutoffMsat)/smearingMsat)
return 1 - 1/denominator
// The numerator decides what the minimal value of this function will
// be. The minimal value is set by minCapacityFactor.
numerator := 1 - minCapacityFactor
return 1 - numerator/denominator
}
// PairProbability estimates the probability of successfully traversing to

View File

@ -28,11 +28,12 @@ const (
// testCapacity is used to define a capacity for some channels.
testCapacity = btcutil.Amount(100_000)
testAmount = lnwire.MilliSatoshi(50_000_000)
testCapacityFraction = 0.75
testAmount = lnwire.MilliSatoshi(90_000_000)
testCapacityFraction = 0.9999
// Defines the capacityFactor for testAmount and testCapacity.
capFactor = 0.9241
// capFactor is the capacityFactor for testAmount, testCapacity and
// testCapacityFraction.
capFactor = 0.9909715
)
type estimatorTestContext struct {
@ -244,13 +245,13 @@ func TestCapacityCutoff(t *testing.T) {
name: "low amount",
capacityFraction: 0.75,
amountMsat: capacityMSat / 10,
expectedFactor: 0.998,
expectedFactor: 1,
},
{
name: "half amount",
capacityFraction: 0.75,
amountMsat: capacityMSat / 2,
expectedFactor: 0.924,
expectedFactor: 1,
},
{
name: "cutoff amount",
@ -258,13 +259,13 @@ func TestCapacityCutoff(t *testing.T) {
amountMsat: int(
0.75 * float64(capacityMSat),
),
expectedFactor: 0.5,
expectedFactor: 0.75,
},
{
name: "high amount",
capacityFraction: 0.75,
amountMsat: capacityMSat * 80 / 100,
expectedFactor: 0.377,
expectedFactor: 0.560,
},
{
// Even when we spend the full capacity, we still want
@ -274,7 +275,7 @@ func TestCapacityCutoff(t *testing.T) {
name: "full amount",
capacityFraction: 0.75,
amountMsat: capacityMSat,
expectedFactor: 0.076,
expectedFactor: 0.5,
},
{
name: "more than capacity",
@ -282,6 +283,28 @@ func TestCapacityCutoff(t *testing.T) {
amountMsat: capacityMSat + 1,
expectedFactor: 0.0,
},
// Default CapacityFactor of 0.9999.
{
name: "zero amount",
capacityFraction: 0.9999,
amountMsat: 0,
expectedFactor: 1.00,
},
{
name: "90% of the channel capacity",
capacityFraction: 0.9999,
amountMsat: capacityMSat * 90 / 100,
expectedFactor: 0.990,
},
{
// We won't saturate at 0.5 as in the other case but at
// a higher value of 0.75 due to the smearing, this
// translates to a penalty increase of a factor of 1.33.
name: "full amount",
capacityFraction: 0.9999,
amountMsat: capacityMSat,
expectedFactor: 0.75,
},
// Inactive capacity factor.
{
name: "inactive capacity factor",