Skip to content

Commit 5cd8016

Browse files
committed
Consider opportunity cost of local channel fees in pathfinding
1 parent 9187d46 commit 5cd8016

File tree

5 files changed

+133
-18
lines changed

5 files changed

+133
-18
lines changed

lnrpc/routerrpc/config.go

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,15 @@ type Config struct {
4242
// DefaultConfig defines the config defaults.
4343
func DefaultConfig() *Config {
4444
defaultRoutingConfig := RoutingConfig{
45-
AprioriHopProbability: routing.DefaultAprioriHopProbability,
46-
AprioriWeight: routing.DefaultAprioriWeight,
47-
MinRouteProbability: routing.DefaultMinRouteProbability,
48-
PenaltyHalfLife: routing.DefaultPenaltyHalfLife,
49-
AttemptCost: routing.DefaultAttemptCost.ToSatoshis(),
50-
AttemptCostPPM: routing.DefaultAttemptCostPPM,
51-
MaxMcHistory: routing.DefaultMaxMcHistory,
52-
McFlushInterval: routing.DefaultMcFlushInterval,
45+
AprioriHopProbability: routing.DefaultAprioriHopProbability,
46+
AprioriWeight: routing.DefaultAprioriWeight,
47+
MinRouteProbability: routing.DefaultMinRouteProbability,
48+
LocalOpportunityCost: routing.DefaultLocalOpportunityCost,
49+
PenaltyHalfLife: routing.DefaultPenaltyHalfLife,
50+
AttemptCost: routing.DefaultAttemptCost.ToSatoshis(),
51+
AttemptCostPPM: routing.DefaultAttemptCostPPM,
52+
MaxMcHistory: routing.DefaultMaxMcHistory,
53+
McFlushInterval: routing.DefaultMcFlushInterval,
5354
}
5455

5556
return &Config{
@@ -60,13 +61,14 @@ func DefaultConfig() *Config {
6061
// GetRoutingConfig returns the routing config based on this sub server config.
6162
func GetRoutingConfig(cfg *Config) *RoutingConfig {
6263
return &RoutingConfig{
63-
AprioriHopProbability: cfg.AprioriHopProbability,
64-
AprioriWeight: cfg.AprioriWeight,
65-
MinRouteProbability: cfg.MinRouteProbability,
66-
AttemptCost: cfg.AttemptCost,
67-
AttemptCostPPM: cfg.AttemptCostPPM,
68-
PenaltyHalfLife: cfg.PenaltyHalfLife,
69-
MaxMcHistory: cfg.MaxMcHistory,
70-
McFlushInterval: cfg.McFlushInterval,
64+
AprioriHopProbability: cfg.AprioriHopProbability,
65+
AprioriWeight: cfg.AprioriWeight,
66+
MinRouteProbability: cfg.MinRouteProbability,
67+
LocalOpportunityCost: cfg.LocalOpportunityCost,
68+
AttemptCost: cfg.AttemptCost,
69+
AttemptCostPPM: cfg.AttemptCostPPM,
70+
PenaltyHalfLife: cfg.PenaltyHalfLife,
71+
MaxMcHistory: cfg.MaxMcHistory,
72+
McFlushInterval: cfg.McFlushInterval,
7173
}
7274
}

lnrpc/routerrpc/routing_config.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,8 @@ type RoutingConfig struct {
4747
// McFlushInterval defines the timer interval to use to flush mission
4848
// control state to the DB.
4949
McFlushInterval time.Duration `long:"mcflushinterval" description:"the timer interval to use to flush mission control state to the DB"`
50+
51+
// LocalOpportunityCost defines whether to consider the local
52+
// channel balance when evaluating routes.
53+
LocalOpportunityCost bool `bool:"localopportunitycost" description:"whether to consider the local channel balance when evaluating routes"`
5054
}

routing/pathfind.go

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,11 @@ var (
7070
// DefaultAprioriHopProbability is the default a priori probability for
7171
// a hop.
7272
DefaultAprioriHopProbability = float64(0.6)
73+
74+
// DefaultLocalOpportunityCost determines whether the pathfinder
75+
// should consider the fee rates set on its local channels when selecting
76+
// a path.
77+
DefaultLocalOpportunityCost = bool(false)
7378
)
7479

7580
// edgePolicyWithSource is a helper struct to keep track of the source node
@@ -362,6 +367,10 @@ type PathFindingConfig struct {
362367
// MinProbability defines the minimum success probability of the
363368
// returned route.
364369
MinProbability float64
370+
371+
// Whether the fee rate on local channels is considered when calculating
372+
// the total fee for a route.
373+
LocalOpportunityCost bool
365374
}
366375

367376
// getOutgoingBalance returns the maximum available balance in any of the
@@ -660,7 +669,7 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
660669
//
661670
// Source node has no predecessor to pay a fee. Therefore set
662671
// fee to zero, because it should not be included in the fee
663-
// limit check and edge weight.
672+
// limit check and edge weight.
664673
//
665674
// Also determine the time lock delta that will be added to the
666675
// route if fromVertex is selected. If fromVertex is the source
@@ -705,11 +714,19 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
705714
return
706715
}
707716

717+
// If this is one of our own channels and
718+
// LocalOpportunityCost is true, then we account for the
719+
// fee on this channel.
720+
var opportunityCostFee lnwire.MilliSatoshi
721+
if cfg.LocalOpportunityCost && fromVertex == source {
722+
opportunityCostFee = edge.policy.ComputeFee(amountToSend)
723+
}
724+
708725
// By adding fromVertex in the route, there will be an extra
709726
// weight composed of the fee that this node will charge and
710727
// the amount that will be locked for timeLockDelta blocks in
711728
// the HTLC that is handed out to fromVertex.
712-
weight := edgeWeight(amountToReceive, fee, timeLockDelta)
729+
weight := edgeWeight(amountToReceive, fee + opportunityCostFee, timeLockDelta)
713730

714731
// Compute the tentative weight to this new channel/edge
715732
// which is the weight from our toNode to the target node

routing/pathfind_test.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -835,6 +835,9 @@ func TestPathFinding(t *testing.T) {
835835
}, {
836836
name: "with metadata",
837837
fn: runFindPathWithMetadata,
838+
}, {
839+
name: "with opportunity cost",
840+
fn: runFindPathWithOpportunityCost,
838841
}}
839842

840843
// Run with graph cache enabled.
@@ -970,6 +973,94 @@ func runFindLowestFeePath(t *testing.T, useCache bool) {
970973
}
971974
}
972975

976+
func runFindPathWithOpportunityCost(t *testing.T, useCache bool) {
977+
// Set up a test graph with two paths from deezy to target. A normal
978+
// pathfinder should choose the path through peer-b, but when the
979+
// LocalOpportunityCost flag is set, it should select a path through
980+
// peer-a in order to account for the opportunity cost of using the
981+
// deezy -> peer-b channel liquidity.
982+
testChannels := []*testChannel{
983+
symmetricTestChannel("deezy", "peer-a", 100000, &testChannelPolicy{
984+
Expiry: 144,
985+
FeeRate: 0,
986+
MinHTLC: 1,
987+
MaxHTLC: 100000000,
988+
}),
989+
symmetricTestChannel("peer-a", "target", 100000, &testChannelPolicy{
990+
Expiry: 144,
991+
FeeRate: 50,
992+
MinHTLC: 1,
993+
MaxHTLC: 100000000,
994+
}),
995+
symmetricTestChannel("deezy", "peer-b", 100000, &testChannelPolicy{
996+
Expiry: 144,
997+
FeeRate: 100,
998+
MinHTLC: 1,
999+
MaxHTLC: 100000000,
1000+
}),
1001+
1002+
symmetricTestChannel("peer-b", "target", 100000, &testChannelPolicy{
1003+
Expiry: 144,
1004+
FeeRate: 0,
1005+
MinHTLC: 1,
1006+
MaxHTLC: 100000000,
1007+
}),
1008+
}
1009+
1010+
ctx := newPathFindingTestContext(t, useCache, testChannels, "deezy")
1011+
ctx.pathFindingConfig = PathFindingConfig{
1012+
LocalOpportunityCost: true,
1013+
}
1014+
const (
1015+
startingHeight = 100
1016+
finalHopCLTV = 1
1017+
)
1018+
1019+
paymentAmt := lnwire.NewMSatFromSatoshis(100)
1020+
target := ctx.keyFromAlias("target")
1021+
path, err := ctx.findPath(target, paymentAmt)
1022+
require.NoError(t, err, "unable to find path")
1023+
route, err := newRoute(
1024+
ctx.source, path, startingHeight,
1025+
finalHopParams{
1026+
amt: paymentAmt,
1027+
cltvDelta: finalHopCLTV,
1028+
records: nil,
1029+
},
1030+
)
1031+
require.NoError(t, err, "unable to create path")
1032+
1033+
if route.Hops[0].PubKeyBytes != ctx.keyFromAlias("peer-a") {
1034+
t.Fatalf("expected route to pass through peer-a, "+
1035+
"but got a route through %v",
1036+
ctx.aliasFromKey(route.Hops[0].PubKeyBytes))
1037+
}
1038+
1039+
// We can then set LocalOpportunityCost to false, and we'll see that
1040+
// the peer-b will be chosen for the first hop.
1041+
ctx.pathFindingConfig = PathFindingConfig{
1042+
LocalOpportunityCost: false,
1043+
}
1044+
1045+
path, err = ctx.findPath(target, paymentAmt)
1046+
require.NoError(t, err, "unable to find path")
1047+
route, err = newRoute(
1048+
ctx.source, path, startingHeight,
1049+
finalHopParams{
1050+
amt: paymentAmt,
1051+
cltvDelta: finalHopCLTV,
1052+
records: nil,
1053+
},
1054+
)
1055+
require.NoError(t, err, "unable to create path")
1056+
1057+
if route.Hops[0].PubKeyBytes != ctx.keyFromAlias("peer-b") {
1058+
t.Fatalf("expected route to pass through peer-b, "+
1059+
"but got a route through %v",
1060+
ctx.aliasFromKey(route.Hops[0].PubKeyBytes))
1061+
}
1062+
}
1063+
9731064
func getAliasFromPubKey(pubKey route.Vertex,
9741065
aliases map[string]route.Vertex) string {
9751066

server.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -895,6 +895,7 @@ func newServer(cfg *Config, listenAddrs []net.Addr,
895895
),
896896
AttemptCostPPM: routingConfig.AttemptCostPPM,
897897
MinProbability: routingConfig.MinRouteProbability,
898+
LocalOpportunityCost: routingConfig.LocalOpportunityCost,
898899
}
899900

900901
sourceNode, err := chanGraph.SourceNode()

0 commit comments

Comments
 (0)