From a73581610e1c23f0e18aa7be29047b7af74f0630 Mon Sep 17 00:00:00 2001 From: bitromortac Date: Fri, 24 Feb 2023 11:59:44 +0100 Subject: [PATCH] routing+lnrpc: make capacity factor configurable We make the capacity factor configurable via an lnd.conf routerrpc apriori parameter. The capacity factor trades off increased success probability with a reduced set of channel candidates, which may lead to increased fees. To let users choose whether the factor is active or not, we add a config setting where a capacity fraction of 1.0 disables the factor. We limit the capacity fraction to values between 0.75 and 1.0. Lower values may discard too many channels. --- docs/release-notes/release-notes-0.16.0.md | 2 + lnrpc/routerrpc/config.go | 14 +++-- lnrpc/routerrpc/routing_config.go | 3 + routing/integrated_routing_context_test.go | 1 + routing/missioncontrol_test.go | 1 + routing/probability_apriori.go | 65 +++++++++++++++------ routing/probability_apriori_test.go | 67 ++++++++++++++-------- routing/router_test.go | 1 + sample-lnd.conf | 5 ++ server.go | 1 + 10 files changed, 113 insertions(+), 47 deletions(-) diff --git a/docs/release-notes/release-notes-0.16.0.md b/docs/release-notes/release-notes-0.16.0.md index 73035ad6e..9e10b6875 100644 --- a/docs/release-notes/release-notes-0.16.0.md +++ b/docs/release-notes/release-notes-0.16.0.md @@ -433,6 +433,8 @@ in the lnwire package](https://github.com/lightningnetwork/lnd/pull/7303) * [A new probability model ("bimodal") is added which models channel based liquidities within a probability theory framework.]( https://github.com/lightningnetwork/lnd/pull/6815) +* [The a priori capacity factor is made configurable and its effect is + limited.](https://github.com/lightningnetwork/lnd/pull/7444) ## Configuration * Note that [this pathfinding change](https://github.com/lightningnetwork/lnd/pull/6815) diff --git a/lnrpc/routerrpc/config.go b/lnrpc/routerrpc/config.go index 9877556db..adcc84e80 100644 --- a/lnrpc/routerrpc/config.go +++ b/lnrpc/routerrpc/config.go @@ -50,9 +50,10 @@ func DefaultConfig() *Config { MaxMcHistory: routing.DefaultMaxMcHistory, McFlushInterval: routing.DefaultMcFlushInterval, AprioriConfig: &AprioriConfig{ - HopProbability: routing.DefaultAprioriHopProbability, - Weight: routing.DefaultAprioriWeight, - PenaltyHalfLife: routing.DefaultPenaltyHalfLife, + HopProbability: routing.DefaultAprioriHopProbability, + Weight: routing.DefaultAprioriWeight, + PenaltyHalfLife: routing.DefaultPenaltyHalfLife, + CapacityFraction: routing.DefaultCapacityFraction, }, BimodalConfig: &BimodalConfig{ Scale: int64(routing.DefaultBimodalScaleMsat), @@ -76,9 +77,10 @@ func GetRoutingConfig(cfg *Config) *RoutingConfig { MaxMcHistory: cfg.MaxMcHistory, McFlushInterval: cfg.McFlushInterval, AprioriConfig: &AprioriConfig{ - HopProbability: cfg.AprioriConfig.HopProbability, - Weight: cfg.AprioriConfig.Weight, - PenaltyHalfLife: cfg.AprioriConfig.PenaltyHalfLife, + HopProbability: cfg.AprioriConfig.HopProbability, + Weight: cfg.AprioriConfig.Weight, + PenaltyHalfLife: cfg.AprioriConfig.PenaltyHalfLife, + CapacityFraction: cfg.AprioriConfig.CapacityFraction, }, BimodalConfig: &BimodalConfig{ Scale: cfg.BimodalConfig.Scale, diff --git a/lnrpc/routerrpc/routing_config.go b/lnrpc/routerrpc/routing_config.go index a1bbad590..31494d546 100644 --- a/lnrpc/routerrpc/routing_config.go +++ b/lnrpc/routerrpc/routing_config.go @@ -63,6 +63,9 @@ type AprioriConfig struct { // PenaltyHalfLife defines after how much time a penalized node or // channel is back at 50% probability. PenaltyHalfLife time.Duration `long:"penaltyhalflife" description:"Defines the duration after which a penalized node or channel is back at 50% probability"` + + // CapacityFraction defines the fraction of channels' capacities that is considered liquid. + CapacityFraction float64 `long:"capacityfraction" description:"Defines the fraction of channels' capacities that is considered liquid. Valid values are in [0.75, 1]."` } // BimodalConfig defines parameters for the bimodal probability. diff --git a/routing/integrated_routing_context_test.go b/routing/integrated_routing_context_test.go index 0ab0c1b01..d939dc16b 100644 --- a/routing/integrated_routing_context_test.go +++ b/routing/integrated_routing_context_test.go @@ -73,6 +73,7 @@ func newIntegratedRoutingContext(t *testing.T) *integratedRoutingContext { PenaltyHalfLife: 30 * time.Minute, AprioriHopProbability: 0.6, AprioriWeight: 0.5, + CapacityFraction: testCapacityFraction, } estimator, err := NewAprioriEstimator(aCfg) require.NoError(t, err) diff --git a/routing/missioncontrol_test.go b/routing/missioncontrol_test.go index 91cc8eaa0..27391d53e 100644 --- a/routing/missioncontrol_test.go +++ b/routing/missioncontrol_test.go @@ -94,6 +94,7 @@ func (ctx *mcTestContext) restartMc() { PenaltyHalfLife: testPenaltyHalfLife, AprioriHopProbability: testAprioriHopProbability, AprioriWeight: testAprioriWeight, + CapacityFraction: testCapacityFraction, } estimator, err := NewAprioriEstimator(aCfg) require.NoError(ctx.t, err) diff --git a/routing/probability_apriori.go b/routing/probability_apriori.go index dabd47ff9..b1c884058 100644 --- a/routing/probability_apriori.go +++ b/routing/probability_apriori.go @@ -12,11 +12,11 @@ import ( ) const ( - // capacityCutoffFraction and capacitySmearingFraction define how - // capacity-related probability reweighting works. - // capacityCutoffFraction 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. + // CapacityFraction and capacitySmearingFraction define how + // 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 @@ -34,19 +34,23 @@ const ( // fulfill the requirement with capacityFactor(cap, cap) ~ 0.076 (see // tests). - // The capacityCutoffFraction is a trade-off between usage of the - // provided capacity and expected probability reduction when we send the - // full amount. The success probability in the random balance model can - // be approximated with P(a) = 1 - a/c, for amount a and capacity c. If - // we require a probability P(a) > 0.25, this translates into a value of - // 0.75 for a/c. - capacityCutoffFraction = 0.75 + // DefaultCapacityFraction is the default value for CapacityFraction. + DefaultCapacityFraction = 0.75 // 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 + // minCapacityFraction is the minimum allowed value for + // CapacityFraction. The success probability in the random balance model + // (which may not be an accurate description of the liquidity + // distribution in the network) can be approximated with P(a) = 1 - a/c, + // for amount a and capacity c. If we require a probability P(a) = 0.25, + // this translates into a value of 0.75 for a/c. We limit this value in + // order to not discard too many channels. + minCapacityFraction = 0.75 + // AprioriEstimatorName is used to identify the apriori probability // estimator. AprioriEstimatorName = "apriori" @@ -64,6 +68,11 @@ var ( // ErrInvalidAprioriWeight is returned when we get an apriori weight // that is out of range. ErrInvalidAprioriWeight = errors.New("apriori weight must be in [0, 1]") + + // ErrInvalidCapacityFraction is returned when we get a capacity + // fraction that is out of range. + ErrInvalidCapacityFraction = fmt.Errorf("capacity fraction must be in "+ + "[%v, 1]", minCapacityFraction) ) // AprioriConfig contains configuration for our probability estimator. @@ -84,6 +93,12 @@ type AprioriConfig struct { // probability completely and only base the probability on historical // results, unless there are none available. AprioriWeight float64 + + // CapacityFraction is the fraction of a channel's capacity that we + // consider to have liquidity. For amounts that come close to or exceed + // the fraction, an additional penalty is applied. A value of 1.0 + // disables the capacityFactor. + CapacityFraction float64 } // validate checks the configuration of the estimator for allowed values. @@ -100,6 +115,10 @@ func (p AprioriConfig) validate() error { return ErrInvalidAprioriWeight } + if p.CapacityFraction < minCapacityFraction || p.CapacityFraction > 1 { + return ErrInvalidCapacityFraction + } + return nil } @@ -109,6 +128,7 @@ func DefaultAprioriConfig() AprioriConfig { PenaltyHalfLife: DefaultPenaltyHalfLife, AprioriHopProbability: DefaultAprioriHopProbability, AprioriWeight: DefaultAprioriWeight, + CapacityFraction: DefaultCapacityFraction, } } @@ -155,9 +175,10 @@ func (p *AprioriEstimator) Config() estimatorConfig { func (p *AprioriEstimator) String() string { return fmt.Sprintf("estimator type: %v, penalty halflife time: %v, "+ "apriori hop probability: %v, apriori weight: %v, previous "+ - "success probability: %v", AprioriEstimatorName, - p.PenaltyHalfLife, p.AprioriHopProbability, p.AprioriWeight, - p.prevSuccessProbability) + "success probability: %v, capacity fraction: %v", + AprioriEstimatorName, p.PenaltyHalfLife, + p.AprioriHopProbability, p.AprioriWeight, + p.prevSuccessProbability, p.CapacityFraction) } // getNodeProbability calculates the probability for connections from a node @@ -169,7 +190,9 @@ func (p *AprioriEstimator) getNodeProbability(now time.Time, // We reduce the apriori hop probability if the amount comes close to // the capacity. - apriori := p.AprioriHopProbability * capacityFactor(amt, capacity) + apriori := p.AprioriHopProbability * capacityFactor( + amt, capacity, p.CapacityFraction, + ) // If the channel history is not to be taken into account, we can return // early here with the configured a priori probability. @@ -246,7 +269,15 @@ func (p *AprioriEstimator) getWeight(age time.Duration) float64 { // and 0 for amt >> cutoffMsat. 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) float64 { +func capacityFactor(amt lnwire.MilliSatoshi, capacity btcutil.Amount, + capacityCutoffFraction float64) float64 { + + // The special value of 1.0 for capacityFactor disables any effect from + // this factor. + if capacityCutoffFraction == 1 { + return 1.0 + } + // If we don't have information about the capacity, which can be the // case for hop hints or local channels, we return unity to not alter // anything. diff --git a/routing/probability_apriori_test.go b/routing/probability_apriori_test.go index a055d4ae2..8335ca792 100644 --- a/routing/probability_apriori_test.go +++ b/routing/probability_apriori_test.go @@ -27,8 +27,9 @@ const ( aprioriPrevSucProb = 0.95 // testCapacity is used to define a capacity for some channels. - testCapacity = btcutil.Amount(100_000) - testAmount = lnwire.MilliSatoshi(50_000_000) + testCapacity = btcutil.Amount(100_000) + testAmount = lnwire.MilliSatoshi(50_000_000) + testCapacityFraction = 0.75 // Defines the capacityFactor for testAmount and testCapacity. capFactor = 0.9241 @@ -53,6 +54,7 @@ func newEstimatorTestContext(t *testing.T) *estimatorTestContext { AprioriHopProbability: aprioriHopProb, AprioriWeight: aprioriWeight, PenaltyHalfLife: time.Hour, + CapacityFraction: testCapacityFraction, }, prevSuccessProbability: aprioriPrevSucProb, }, @@ -227,49 +229,65 @@ func TestCapacityCutoff(t *testing.T) { capacityMSat := capacitySat * 1000 tests := []struct { - name string - amountMsat int - expectedFactor float64 + name string + capacityFraction float64 + amountMsat int + expectedFactor float64 }{ + // Minimal CapacityFraction of 0.75. { - name: "zero amount", - expectedFactor: 1, + name: "zero amount", + capacityFraction: 0.75, + expectedFactor: 1, }, { - name: "low amount", - amountMsat: capacityMSat / 10, - expectedFactor: 0.998, + name: "low amount", + capacityFraction: 0.75, + amountMsat: capacityMSat / 10, + expectedFactor: 0.998, }, { - name: "half amount", - amountMsat: capacityMSat / 2, - expectedFactor: 0.924, + name: "half amount", + capacityFraction: 0.75, + amountMsat: capacityMSat / 2, + expectedFactor: 0.924, }, { - name: "cutoff amount", + name: "cutoff amount", + capacityFraction: 0.75, amountMsat: int( - capacityCutoffFraction * float64(capacityMSat), + 0.75 * float64(capacityMSat), ), expectedFactor: 0.5, }, { - name: "high amount", - amountMsat: capacityMSat * 80 / 100, - expectedFactor: 0.377, + name: "high amount", + capacityFraction: 0.75, + amountMsat: capacityMSat * 80 / 100, + expectedFactor: 0.377, }, { // Even when we spend the full capacity, we still want // to have some residual probability to not throw away // routes due to a min probability requirement of the // whole path. - name: "full amount", - amountMsat: capacityMSat, - expectedFactor: 0.076, + name: "full amount", + capacityFraction: 0.75, + amountMsat: capacityMSat, + expectedFactor: 0.076, }, { - name: "more than capacity", - amountMsat: capacityMSat + 1, - expectedFactor: 0.0, + name: "more than capacity", + capacityFraction: 0.75, + amountMsat: capacityMSat + 1, + expectedFactor: 0.0, + }, + // Inactive capacity factor. + { + name: "inactive capacity factor", + capacityFraction: 1.0, + amountMsat: capacityMSat, + expectedFactor: 1.00, }, } @@ -282,6 +300,7 @@ func TestCapacityCutoff(t *testing.T) { got := capacityFactor( lnwire.MilliSatoshi(test.amountMsat), btcutil.Amount(capacitySat), + test.capacityFraction, ) require.InDelta(t, test.expectedFactor, got, 0.001) }) diff --git a/routing/router_test.go b/routing/router_test.go index 8701d8b28..767cf7347 100644 --- a/routing/router_test.go +++ b/routing/router_test.go @@ -123,6 +123,7 @@ func createTestCtxFromGraphInstanceAssumeValid(t *testing.T, PenaltyHalfLife: time.Hour, AprioriHopProbability: 0.9, AprioriWeight: 0.5, + CapacityFraction: testCapacityFraction, } estimator, err := NewAprioriEstimator(aCfg) require.NoError(t, err) diff --git a/sample-lnd.conf b/sample-lnd.conf index 1b64e0c1b..3dfc76da8 100644 --- a/sample-lnd.conf +++ b/sample-lnd.conf @@ -1142,6 +1142,11 @@ litecoin.node=ltcd ; probability (default: 1h0m0s) ; routerrpc.apriori.penaltyhalflife=2h +; Defines the fraction of channels' capacities that is considered liquid in +; pathfinding, a value between [0.75-1.0]. A value of 1.0 disables this +; feature. (default: 0.75) +; routerrpc.apriori.capacityfraction=0.9 + ; Describes the scale over which channels still have some liquidity left on ; both channel ends. A very low value (compared to typical channel capacities) ; means that we assume unbalanced channels, a very high value means randomly diff --git a/server.go b/server.go index 03084f562..915a4b51f 100644 --- a/server.go +++ b/server.go @@ -880,6 +880,7 @@ func newServer(cfg *Config, listenAddrs []net.Addr, AprioriHopProbability: aCfg.HopProbability, PenaltyHalfLife: aCfg.PenaltyHalfLife, AprioriWeight: aCfg.Weight, + CapacityFraction: aCfg.CapacityFraction, } estimator, err = routing.NewAprioriEstimator(