routing: improve equal cost route comparison

When the (virtual) payment attempt cost is set to zero, probabilities
are no longer a factor in determining the best route. In case of routes
with equal costs, we'd just go with the first one found. This commit
refines this behavior by picking the route with the highest probability.
So even though probability doesn't affect the route cost, it is still
used as a tie breaker.
This commit is contained in:
Joost Jager 2019-12-01 14:55:01 +01:00
parent d59aba35a0
commit 3aaf32dc2e
No known key found for this signature in database
GPG Key ID: A61B9D4C393C59C7
3 changed files with 89 additions and 6 deletions

@ -73,6 +73,11 @@ func (d *distanceHeap) Len() int { return len(d.nodes) }
// //
// NOTE: This is part of the heap.Interface implementation. // NOTE: This is part of the heap.Interface implementation.
func (d *distanceHeap) Less(i, j int) bool { func (d *distanceHeap) Less(i, j int) bool {
// If distances are equal, tie break on probability.
if d.nodes[i].dist == d.nodes[j].dist {
return d.nodes[i].probability > d.nodes[j].probability
}
return d.nodes[i].dist < d.nodes[j].dist return d.nodes[i].dist < d.nodes[j].dist
} }

@ -493,13 +493,25 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
int64(cfg.PaymentAttemptPenalty), int64(cfg.PaymentAttemptPenalty),
) )
// If the current best route is better than this candidate // If there is already a best route stored, compare this
// route, return. It is important to also return if the distance // candidate route with the best route so far.
// is equal, because otherwise the algorithm could run into an
// endless loop.
current, ok := distance[fromVertex] current, ok := distance[fromVertex]
if ok && tempDist >= current.dist { if ok {
return // If this route is worse than what we already found,
// skip this route.
if tempDist > current.dist {
return
}
// If the route is equally good and the probability
// isn't better, skip this route. It is important to
// also return if both cost and probability are equal,
// because otherwise the algorithm could run into an
// endless loop.
probNotBetter := probability <= current.probability
if tempDist == current.dist && probNotBetter {
return
}
} }
// Every edge should have a positive time lock delta. If we // Every edge should have a positive time lock delta. If we

@ -2154,6 +2154,72 @@ func testProbabilityRouting(t *testing.T, p10, p11, p20, minProbability float64,
} }
} }
// TestEqualCostRouteSelection asserts that route probability will be used as a
// tie breaker in case the path finding probabilities are equal.
func TestEqualCostRouteSelection(t *testing.T) {
t.Parallel()
// Set up a test graph with two possible paths to the target: via a and
// via b. The routing fees and probabilities are chosen such that the
// algorithm will first explore target->a->source (backwards search).
// This route has fee 6 and a penality of 4 for the 25% success
// probability. The algorithm will then proceed with evaluating
// target->b->source, which has a fee of 8 and a penalty of 2 for the
// 50% success probability. Both routes have the same path finding cost
// of 10. It is expected that in that case, the highest probability
// route (through b) is chosen.
testChannels := []*testChannel{
symmetricTestChannel("source", "a", 100000, &testChannelPolicy{}),
symmetricTestChannel("source", "b", 100000, &testChannelPolicy{}),
symmetricTestChannel("a", "target", 100000, &testChannelPolicy{
Expiry: 144,
FeeBaseMsat: lnwire.NewMSatFromSatoshis(6),
MinHTLC: 1,
}, 1),
symmetricTestChannel("b", "target", 100000, &testChannelPolicy{
Expiry: 100,
FeeBaseMsat: lnwire.NewMSatFromSatoshis(8),
MinHTLC: 1,
}, 2),
}
ctx := newPathFindingTestContext(t, testChannels, "source")
defer ctx.cleanup()
alias := ctx.testGraphInstance.aliasMap
paymentAmt := lnwire.NewMSatFromSatoshis(100)
target := ctx.testGraphInstance.aliasMap["target"]
ctx.restrictParams.ProbabilitySource = func(fromNode, toNode route.Vertex,
amt lnwire.MilliSatoshi) float64 {
switch {
case fromNode == alias["source"] && toNode == alias["a"]:
return 0.25
case fromNode == alias["source"] && toNode == alias["b"]:
return 0.5
default:
return 1
}
}
ctx.pathFindingConfig = PathFindingConfig{
PaymentAttemptPenalty: lnwire.NewMSatFromSatoshis(1),
}
path, err := ctx.findPath(target, paymentAmt)
if err != nil {
t.Fatal(err)
}
if path[1].ChannelID != 2 {
t.Fatalf("expected route to pass through channel %v, "+
"but channel %v was selected instead", 2,
path[1].ChannelID)
}
}
// TestNoCycle tries to guide the path finding algorithm into reconstructing an // TestNoCycle tries to guide the path finding algorithm into reconstructing an
// endless route. It asserts that the algorithm is able to handle this properly. // endless route. It asserts that the algorithm is able to handle this properly.
func TestNoCycle(t *testing.T) { func TestNoCycle(t *testing.T) {