diff --git a/lnrpc/routerrpc/config.go b/lnrpc/routerrpc/config.go index ec036258fe..eb022ad0a4 100644 --- a/lnrpc/routerrpc/config.go +++ b/lnrpc/routerrpc/config.go @@ -42,14 +42,15 @@ type Config struct { // DefaultConfig defines the config defaults. func DefaultConfig() *Config { defaultRoutingConfig := RoutingConfig{ - AprioriHopProbability: routing.DefaultAprioriHopProbability, - AprioriWeight: routing.DefaultAprioriWeight, - MinRouteProbability: routing.DefaultMinRouteProbability, - PenaltyHalfLife: routing.DefaultPenaltyHalfLife, - AttemptCost: routing.DefaultAttemptCost.ToSatoshis(), - AttemptCostPPM: routing.DefaultAttemptCostPPM, - MaxMcHistory: routing.DefaultMaxMcHistory, - McFlushInterval: routing.DefaultMcFlushInterval, + AprioriHopProbability: routing.DefaultAprioriHopProbability, + AprioriWeight: routing.DefaultAprioriWeight, + MinRouteProbability: routing.DefaultMinRouteProbability, + LocalOpportunityCost: routing.DefaultLocalOpportunityCost, + PenaltyHalfLife: routing.DefaultPenaltyHalfLife, + AttemptCost: routing.DefaultAttemptCost.ToSatoshis(), + AttemptCostPPM: routing.DefaultAttemptCostPPM, + MaxMcHistory: routing.DefaultMaxMcHistory, + McFlushInterval: routing.DefaultMcFlushInterval, } return &Config{ @@ -60,13 +61,14 @@ func DefaultConfig() *Config { // GetRoutingConfig returns the routing config based on this sub server config. func GetRoutingConfig(cfg *Config) *RoutingConfig { return &RoutingConfig{ - AprioriHopProbability: cfg.AprioriHopProbability, - AprioriWeight: cfg.AprioriWeight, - MinRouteProbability: cfg.MinRouteProbability, - AttemptCost: cfg.AttemptCost, - AttemptCostPPM: cfg.AttemptCostPPM, - PenaltyHalfLife: cfg.PenaltyHalfLife, - MaxMcHistory: cfg.MaxMcHistory, - McFlushInterval: cfg.McFlushInterval, + AprioriHopProbability: cfg.AprioriHopProbability, + AprioriWeight: cfg.AprioriWeight, + MinRouteProbability: cfg.MinRouteProbability, + LocalOpportunityCost: cfg.LocalOpportunityCost, + AttemptCost: cfg.AttemptCost, + AttemptCostPPM: cfg.AttemptCostPPM, + PenaltyHalfLife: cfg.PenaltyHalfLife, + MaxMcHistory: cfg.MaxMcHistory, + McFlushInterval: cfg.McFlushInterval, } } diff --git a/lnrpc/routerrpc/routing_config.go b/lnrpc/routerrpc/routing_config.go index 2a53ba2e4e..6911fbfc2e 100644 --- a/lnrpc/routerrpc/routing_config.go +++ b/lnrpc/routerrpc/routing_config.go @@ -47,4 +47,10 @@ type RoutingConfig struct { // McFlushInterval defines the timer interval to use to flush mission // control state to the DB. McFlushInterval time.Duration `long:"mcflushinterval" description:"the timer interval to use to flush mission control state to the DB"` + + // LocalOpportunityCost defines whether to consider the local fee rate + // of the first hop channel when evaluating routes. While you do not + // pay this fee since it is your channel, you might want to consider + // it in order to preserve valuale liquidity. + LocalOpportunityCost bool `long:"localopportunitycost" description:"whether to consider the opportunity cost of using local channel liquidity when evaluating routes"` } diff --git a/routing/pathfind.go b/routing/pathfind.go index bffc463368..9f829f6df2 100644 --- a/routing/pathfind.go +++ b/routing/pathfind.go @@ -70,6 +70,11 @@ var ( // DefaultAprioriHopProbability is the default a priori probability for // a hop. DefaultAprioriHopProbability = float64(0.6) + + // DefaultLocalOpportunityCost determines whether the pathfinder + // should consider the fee rates set on its local channels when selecting + // a path. + DefaultLocalOpportunityCost = bool(false) ) // edgePolicyWithSource is a helper struct to keep track of the source node @@ -362,6 +367,10 @@ type PathFindingConfig struct { // MinProbability defines the minimum success probability of the // returned route. MinProbability float64 + + // Whether the fee rate on local channels is considered when calculating + // the total fee for a route. + LocalOpportunityCost bool } // getOutgoingBalance returns the maximum available balance in any of the @@ -660,7 +669,7 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig, // // Source node has no predecessor to pay a fee. Therefore set // fee to zero, because it should not be included in the fee - // limit check and edge weight. + // limit check and edge weight. // // Also determine the time lock delta that will be added to the // route if fromVertex is selected. If fromVertex is the source @@ -705,11 +714,19 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig, return } + // If this is one of our own channels and + // LocalOpportunityCost is true, then we account for the + // fee on this channel. + var opportunityCostFee lnwire.MilliSatoshi + if cfg.LocalOpportunityCost && fromVertex == source { + opportunityCostFee = edge.policy.ComputeFee(amountToSend) + } + // By adding fromVertex in the route, there will be an extra // weight composed of the fee that this node will charge and // the amount that will be locked for timeLockDelta blocks in // the HTLC that is handed out to fromVertex. - weight := edgeWeight(amountToReceive, fee, timeLockDelta) + weight := edgeWeight(amountToReceive, fee + opportunityCostFee, timeLockDelta) // Compute the tentative weight to this new channel/edge // which is the weight from our toNode to the target node diff --git a/routing/pathfind_test.go b/routing/pathfind_test.go index 3fb0dc9ce3..324694acdc 100644 --- a/routing/pathfind_test.go +++ b/routing/pathfind_test.go @@ -835,6 +835,9 @@ func TestPathFinding(t *testing.T) { }, { name: "with metadata", fn: runFindPathWithMetadata, + }, { + name: "with opportunity cost", + fn: runFindPathWithOpportunityCost, }} // Run with graph cache enabled. @@ -970,6 +973,94 @@ func runFindLowestFeePath(t *testing.T, useCache bool) { } } +func runFindPathWithOpportunityCost(t *testing.T, useCache bool) { + // Set up a test graph with two paths from deezy to target. A normal + // pathfinder should choose the path through peer-b, but when the + // LocalOpportunityCost flag is set, it should select a path through + // peer-a in order to account for the opportunity cost of using the + // deezy -> peer-b channel liquidity. + testChannels := []*testChannel{ + symmetricTestChannel("deezy", "peer-a", 100000, &testChannelPolicy{ + Expiry: 144, + FeeRate: 0, + MinHTLC: 1, + MaxHTLC: 100000000, + }), + symmetricTestChannel("peer-a", "target", 100000, &testChannelPolicy{ + Expiry: 144, + FeeRate: 50, + MinHTLC: 1, + MaxHTLC: 100000000, + }), + symmetricTestChannel("deezy", "peer-b", 100000, &testChannelPolicy{ + Expiry: 144, + FeeRate: 100, + MinHTLC: 1, + MaxHTLC: 100000000, + }), + + symmetricTestChannel("peer-b", "target", 100000, &testChannelPolicy{ + Expiry: 144, + FeeRate: 0, + MinHTLC: 1, + MaxHTLC: 100000000, + }), + } + + ctx := newPathFindingTestContext(t, useCache, testChannels, "deezy") + ctx.pathFindingConfig = PathFindingConfig{ + LocalOpportunityCost: true, + } + const ( + startingHeight = 100 + finalHopCLTV = 1 + ) + + paymentAmt := lnwire.NewMSatFromSatoshis(100) + target := ctx.keyFromAlias("target") + path, err := ctx.findPath(target, paymentAmt) + require.NoError(t, err, "unable to find path") + route, err := newRoute( + ctx.source, path, startingHeight, + finalHopParams{ + amt: paymentAmt, + cltvDelta: finalHopCLTV, + records: nil, + }, + ) + require.NoError(t, err, "unable to create path") + + if route.Hops[0].PubKeyBytes != ctx.keyFromAlias("peer-a") { + t.Fatalf("expected route to pass through peer-a, "+ + "but got a route through %v", + ctx.aliasFromKey(route.Hops[0].PubKeyBytes)) + } + + // We can then set LocalOpportunityCost to false, and we'll see that + // the peer-b will be chosen for the first hop. + ctx.pathFindingConfig = PathFindingConfig{ + LocalOpportunityCost: false, + } + + path, err = ctx.findPath(target, paymentAmt) + require.NoError(t, err, "unable to find path") + route, err = newRoute( + ctx.source, path, startingHeight, + finalHopParams{ + amt: paymentAmt, + cltvDelta: finalHopCLTV, + records: nil, + }, + ) + require.NoError(t, err, "unable to create path") + + if route.Hops[0].PubKeyBytes != ctx.keyFromAlias("peer-b") { + t.Fatalf("expected route to pass through peer-b, "+ + "but got a route through %v", + ctx.aliasFromKey(route.Hops[0].PubKeyBytes)) + } +} + func getAliasFromPubKey(pubKey route.Vertex, aliases map[string]route.Vertex) string { diff --git a/server.go b/server.go index 6a6183c1b1..be15adaecd 100644 --- a/server.go +++ b/server.go @@ -893,8 +893,9 @@ func newServer(cfg *Config, listenAddrs []net.Addr, AttemptCost: lnwire.NewMSatFromSatoshis( routingConfig.AttemptCost, ), - AttemptCostPPM: routingConfig.AttemptCostPPM, - MinProbability: routingConfig.MinRouteProbability, + AttemptCostPPM: routingConfig.AttemptCostPPM, + MinProbability: routingConfig.MinRouteProbability, + LocalOpportunityCost: routingConfig.LocalOpportunityCost, } sourceNode, err := chanGraph.SourceNode()