From 75a9dc9103de8fc7bd55c2f490d4a8a5858ad852 Mon Sep 17 00:00:00 2001 From: bitromortac Date: Sat, 18 Feb 2023 09:42:09 +0100 Subject: [PATCH] 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. --- routing/probability_apriori.go | 54 ++++++++++++++--------------- routing/probability_apriori_test.go | 41 +++++++++++++++++----- 2 files changed, 59 insertions(+), 36 deletions(-) diff --git a/routing/probability_apriori.go b/routing/probability_apriori.go index b1c884058..d5d9246e3 100644 --- a/routing/probability_apriori.go +++ b/routing/probability_apriori.go @@ -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 diff --git a/routing/probability_apriori_test.go b/routing/probability_apriori_test.go index 8335ca792..de69f9bb3 100644 --- a/routing/probability_apriori_test.go +++ b/routing/probability_apriori_test.go @@ -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",