1
0
Fork 0
mirror of https://github.com/ACINQ/eclair.git synced 2025-02-22 14:22:39 +01:00

Compute k-shortest routes (#813)

We use Yen's algorithm to find the k-shortest (loopless) paths in a graph, and dijkstra as search algo.

We then pick a random route up among the cheapest ones.
This commit is contained in:
araspitzu 2019-01-17 21:15:51 +01:00 committed by Pierre-Marie Padiou
parent 74d454d904
commit d5fe47572c
3 changed files with 295 additions and 56 deletions

View file

@ -9,9 +9,8 @@ import Router._
object Graph { object Graph {
import DirectedGraph._
case class WeightedNode(key: PublicKey, weight: Long) case class WeightedNode(key: PublicKey, weight: Long)
case class WeightedPath(path: Seq[GraphEdge], weight: Long)
/** /**
* This comparator must be consistent with the "equals" behavior, thus for two weighted nodes with * This comparator must be consistent with the "equals" behavior, thus for two weighted nodes with
@ -25,6 +24,99 @@ object Graph {
} }
} }
implicit object PathComparator extends Ordering[WeightedPath] {
override def compare(x: WeightedPath, y: WeightedPath): Int = y.weight.compareTo(x.weight)
}
/**
* Yen's algorithm to find the k-shortest (loopless) paths in a graph, uses dijkstra as search algo. Is guaranteed to terminate finding
* at most @pathsToFind paths sorted by cost (the cheapest is in position 0).
* @param graph
* @param sourceNode
* @param targetNode
* @param amountMsat
* @param pathsToFind
* @return
*/
def yenKshortestPaths(graph: DirectedGraph, sourceNode: PublicKey, targetNode: PublicKey, amountMsat: Long, ignoredEdges: Set[ChannelDesc], extraEdges: Set[GraphEdge], pathsToFind: Int): Seq[WeightedPath] = {
var allSpurPathsFound = false
// stores the shortest paths
val shortestPaths = new mutable.MutableList[WeightedPath]
// stores the candidates for k(K +1) shortest paths, sorted by path cost
val candidates = new mutable.PriorityQueue[WeightedPath]
// find the shortest path, k = 0
val shortestPath = dijkstraShortestPath(graph, sourceNode, targetNode, amountMsat, ignoredEdges, extraEdges)
shortestPaths += WeightedPath(shortestPath, pathCost(shortestPath, amountMsat))
// main loop
for(k <- 1 until pathsToFind) {
if ( !allSpurPathsFound ) {
// for every edge in the path
for (i <- shortestPaths(k - 1).path.indices) {
val prevShortestPath = shortestPaths(k - 1).path
// select the spur node as the i-th element of the k-th previous shortest path (k -1)
val spurEdge = prevShortestPath(i)
// select the subpath from the source to the spur node of the k-th previous shortest path
val rootPathEdges = if(i == 0) prevShortestPath.head :: Nil else prevShortestPath.take(i)
// links to be removed that are part of the previous shortest path and which share the same root path
val edgesToIgnore = shortestPaths.flatMap { weightedPath =>
if ( (i == 0 && (weightedPath.path.head :: Nil) == rootPathEdges) || weightedPath.path.take(i) == rootPathEdges ) {
weightedPath.path(i).desc :: Nil
} else {
Nil
}
}
// find the "spur" path, a subpath going from the spur edge to the target avoiding previously found subpaths
val spurPath = dijkstraShortestPath(graph, spurEdge.desc.a, targetNode, amountMsat, ignoredEdges ++ edgesToIgnore.toSet, extraEdges)
// if there wasn't a path the spur will be empty
if(spurPath.nonEmpty) {
// candidate k-shortest path is made of the rootPath and the new spurPath
val totalPath = rootPathEdges.head.desc.a == spurPath.head.desc.a match {
case true => rootPathEdges.tail ++ spurPath // if the heads are the same node, drop it from the rootPath
case false => rootPathEdges ++ spurPath
}
//val totalPath = concat(rootPathEdges, spurPath.toList)
val candidatePath = WeightedPath(totalPath, pathCost(totalPath, amountMsat))
if (!shortestPaths.contains(candidatePath) && !candidates.exists(_ == candidatePath)) {
candidates.enqueue(candidatePath)
}
}
}
}
if(candidates.isEmpty) {
// handles the case of having exhausted all possible spur paths and it's impossible to reach the target from the source
allSpurPathsFound = true
} else {
// move the best candidate to the shortestPaths container
shortestPaths += candidates.dequeue()
}
}
shortestPaths
}
// Calculates the total cost of a path (amount + fees), direct channels with the source will have a cost of 0 (pay no fees)
def pathCost(path: Seq[GraphEdge], amountMsat: Long): Long = {
path.drop(1).foldRight(amountMsat) { (edge, cost) =>
edgeWeight(edge, cost, isNeighborTarget = false)
}
}
/** /**
* Finds the shortest path in the graph, uses a modified version of Dijsktra's algorithm that computes * Finds the shortest path in the graph, uses a modified version of Dijsktra's algorithm that computes
* the shortest path from the target to the source (this is because we want to calculate the weight of the * the shortest path from the target to the source (this is because we want to calculate the weight of the
@ -38,9 +130,6 @@ object Graph {
* @param extraEdges a list of extra edges we want to consider but are not currently in the graph * @param extraEdges a list of extra edges we want to consider but are not currently in the graph
* @return * @return
*/ */
def shortestPath(g: DirectedGraph, sourceNode: PublicKey, targetNode: PublicKey, amountMsat: Long, ignoredEdges: Set[ChannelDesc], extraEdges: Set[GraphEdge]): Seq[Hop] = {
dijkstraShortestPath(g, sourceNode, targetNode, amountMsat, ignoredEdges, extraEdges).map(graphEdgeToHop)
}
def dijkstraShortestPath(g: DirectedGraph, sourceNode: PublicKey, targetNode: PublicKey, amountMsat: Long, ignoredEdges: Set[ChannelDesc], extraEdges: Set[GraphEdge]): Seq[GraphEdge] = { def dijkstraShortestPath(g: DirectedGraph, sourceNode: PublicKey, targetNode: PublicKey, amountMsat: Long, ignoredEdges: Set[ChannelDesc], extraEdges: Set[GraphEdge]): Seq[GraphEdge] = {

View file

@ -28,7 +28,9 @@ import fr.acinq.eclair.channel._
import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.crypto.TransportHandler
import fr.acinq.eclair.io.Peer.{ChannelClosed, InvalidSignature, NonexistingChannel, PeerRoutingMessage} import fr.acinq.eclair.io.Peer.{ChannelClosed, InvalidSignature, NonexistingChannel, PeerRoutingMessage}
import fr.acinq.eclair.payment.PaymentRequest.ExtraHop import fr.acinq.eclair.payment.PaymentRequest.ExtraHop
import fr.acinq.eclair.router.Graph.GraphStructure.DirectedGraph.graphEdgeToHop
import fr.acinq.eclair.router.Graph.GraphStructure.{DirectedGraph, GraphEdge} import fr.acinq.eclair.router.Graph.GraphStructure.{DirectedGraph, GraphEdge}
import fr.acinq.eclair.router.Graph.WeightedPath
import fr.acinq.eclair.transactions.Scripts import fr.acinq.eclair.transactions.Scripts
import fr.acinq.eclair.wire._ import fr.acinq.eclair.wire._
import scala.collection.{SortedSet, mutable} import scala.collection.{SortedSet, mutable}
@ -36,7 +38,7 @@ import scala.collection.immutable.{SortedMap, TreeMap}
import scala.compat.Platform import scala.compat.Platform
import scala.concurrent.duration._ import scala.concurrent.duration._
import scala.concurrent.{ExecutionContext, Promise} import scala.concurrent.{ExecutionContext, Promise}
import scala.util.Try import scala.util.{Random, Try}
// @formatter:off // @formatter:off
@ -377,7 +379,8 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom
val ignoredUpdates = getIgnoredChannelDesc(d.updates ++ d.privateUpdates ++ assistedUpdates, ignoreNodes) ++ ignoreChannels ++ d.excludedChannels val ignoredUpdates = getIgnoredChannelDesc(d.updates ++ d.privateUpdates ++ assistedUpdates, ignoreNodes) ++ ignoreChannels ++ d.excludedChannels
log.info(s"finding a route $start->$end with assistedChannels={} ignoreNodes={} ignoreChannels={} excludedChannels={}", assistedUpdates.keys.mkString(","), ignoreNodes.map(_.toBin).mkString(","), ignoreChannels.mkString(","), d.excludedChannels.mkString(",")) log.info(s"finding a route $start->$end with assistedChannels={} ignoreNodes={} ignoreChannels={} excludedChannels={}", assistedUpdates.keys.mkString(","), ignoreNodes.map(_.toBin).mkString(","), ignoreChannels.mkString(","), d.excludedChannels.mkString(","))
val extraEdges = assistedUpdates.map { case (c, u) => GraphEdge(c, u) }.toSet val extraEdges = assistedUpdates.map { case (c, u) => GraphEdge(c, u) }.toSet
findRoute(d.graph, start, end, amount, extraEdges = extraEdges, ignoredEdges = ignoredUpdates.toSet) // we ask the router to make a random selection among the three best routes, numRoutes = 3
findRoute(d.graph, start, end, amount, numRoutes = DEFAULT_ROUTES_COUNT, extraEdges = extraEdges, ignoredEdges = ignoredUpdates.toSet)
.map(r => sender ! RouteResponse(r, ignoreNodes, ignoreChannels)) .map(r => sender ! RouteResponse(r, ignoreNodes, ignoreChannels))
.recover { case t => sender ! Status.Failure(t) } .recover { case t => sender ! Status.Failure(t) }
stay stay
@ -776,24 +779,42 @@ object Router {
*/ */
val ROUTE_MAX_LENGTH = 20 val ROUTE_MAX_LENGTH = 20
// The default amount of routes we'll search for when findRoute is called
val DEFAULT_ROUTES_COUNT = 3
// The default allowed 'spread' between the cheapest route found an the others
// routes exceeding this difference won't be considered as a valid result
val DEFAULT_ALLOWED_SPREAD = 0.1D
/** /**
* Find a route in the graph between localNodeId and targetNodeId, returns the route and its cost * Find a route in the graph between localNodeId and targetNodeId, returns the route.
* Will perform a k-shortest path selection given the @param numRoutes and randomly select one of the result,
* the 'route-set' from where we select the result is made of the k-shortest path given that none of them
* exceeds a 10% spread with the cheapest route
* *
* @param g * @param g
* @param localNodeId * @param localNodeId
* @param targetNodeId * @param targetNodeId
* @param amountMsat the amount that will be sent along this route * @param amountMsat the amount that will be sent along this route
* @param numRoutes the number of shortest-paths to find
* @param extraEdges a set of extra edges we want to CONSIDER during the search * @param extraEdges a set of extra edges we want to CONSIDER during the search
* @param ignoredEdges a set of extra edges we want to IGNORE during the search * @param ignoredEdges a set of extra edges we want to IGNORE during the search
* @return the computed route to the destination @targetNodeId * @return the computed route to the destination @targetNodeId
*/ */
def findRoute(g: DirectedGraph, localNodeId: PublicKey, targetNodeId: PublicKey, amountMsat: Long, extraEdges: Set[GraphEdge] = Set.empty, ignoredEdges: Set[ChannelDesc] = Set.empty): Try[Seq[Hop]] = Try { def findRoute(g: DirectedGraph, localNodeId: PublicKey, targetNodeId: PublicKey, amountMsat: Long, numRoutes: Int, extraEdges: Set[GraphEdge] = Set.empty, ignoredEdges: Set[ChannelDesc] = Set.empty): Try[Seq[Hop]] = Try {
if (localNodeId == targetNodeId) throw CannotRouteToSelf if (localNodeId == targetNodeId) throw CannotRouteToSelf
Graph.shortestPath(g, localNodeId, targetNodeId, amountMsat, ignoredEdges, extraEdges) match { val foundRoutes = Graph.yenKshortestPaths(g, localNodeId, targetNodeId, amountMsat, ignoredEdges, extraEdges, numRoutes).toList match {
case Nil => throw RouteNotFound case Nil => throw RouteNotFound
case path => path case route :: Nil if route.path.isEmpty => throw RouteNotFound
case foundRoutes => foundRoutes
} }
// minimum cost
val minimumCost = foundRoutes.head.weight
// routes paying at most minimumCost + 10%
val eligibleRoutes = foundRoutes.filter(_.weight <= (minimumCost + minimumCost * DEFAULT_ALLOWED_SPREAD).round)
Random.shuffle(eligibleRoutes).head.path.map(graphEdgeToHop)
} }
} }

View file

@ -19,11 +19,11 @@ package fr.acinq.eclair.router
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
import fr.acinq.bitcoin.{BinaryData, Block, Crypto} import fr.acinq.bitcoin.{BinaryData, Block, Crypto}
import fr.acinq.eclair.payment.PaymentRequest.ExtraHop import fr.acinq.eclair.payment.PaymentRequest.ExtraHop
import fr.acinq.eclair.router.Graph.GraphStructure.DirectedGraph.graphEdgeToHop
import fr.acinq.eclair.router.Graph.GraphStructure.{DirectedGraph, GraphEdge} import fr.acinq.eclair.router.Graph.GraphStructure.{DirectedGraph, GraphEdge}
import fr.acinq.eclair.wire._ import fr.acinq.eclair.wire._
import fr.acinq.eclair.{ShortChannelId, nodeFee, randomKey} import fr.acinq.eclair.{ShortChannelId, randomKey}
import org.scalatest.FunSuite import org.scalatest.FunSuite
import scala.util.{Failure, Success} import scala.util.{Failure, Success}
/** /**
@ -36,14 +36,6 @@ class RouteCalculationSpec extends FunSuite {
val (a, b, c, d, e) = (randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey) val (a, b, c, d, e) = (randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey)
// the total fee cost for this path
def pathCost(path: Seq[Hop], amountMsat: Long): Long = {
path.drop(1).reverse.foldLeft(amountMsat) { (fee, hop) =>
fee + nodeFee(hop.lastUpdate.feeBaseMsat, hop.lastUpdate.feeProportionalMillionths, fee)
}
}
test("calculate simple route") { test("calculate simple route") {
val updates = List( val updates = List(
@ -55,7 +47,7 @@ class RouteCalculationSpec extends FunSuite {
val g = makeGraph(updates) val g = makeGraph(updates)
val route = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT) val route = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1)
assert(route.map(hops2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil)) assert(route.map(hops2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil))
} }
@ -93,16 +85,16 @@ class RouteCalculationSpec extends FunSuite {
val graph = makeGraph(updates) val graph = makeGraph(updates)
val Success(route) = Router.findRoute(graph, a, d, amountMsat) val Success(route) = Router.findRoute(graph, a, d, amountMsat, numRoutes = 1)
assert(hops2Ids(route) === 4 :: 5 :: 6 :: Nil) assert(hops2Ids(route) === 4 :: 5 :: 6 :: Nil)
assert(pathCost(route, amountMsat) === expectedCost) assert(Graph.pathCost(hops2Edges(route), amountMsat) === expectedCost)
// now channel 5 could route the amount (10000) but not the amount + fees (10007) // now channel 5 could route the amount (10000) but not the amount + fees (10007)
val (desc, update) = makeUpdate(5L, e, f, feeBaseMsat = 1, feeProportionalMillionth = 400, minHtlcMsat = 0, maxHtlcMsat = Some(10005)) val (desc, update) = makeUpdate(5L, e, f, feeBaseMsat = 1, feeProportionalMillionth = 400, minHtlcMsat = 0, maxHtlcMsat = Some(10005))
val graph1 = graph.addEdge(desc, update) val graph1 = graph.addEdge(desc, update)
val Success(route1) = Router.findRoute(graph1, a, d, amountMsat) val Success(route1) = Router.findRoute(graph1, a, d, amountMsat, numRoutes = 1)
assert(hops2Ids(route1) === 1 :: 2 :: 3 :: Nil) assert(hops2Ids(route1) === 1 :: 2 :: 3 :: Nil)
} }
@ -117,7 +109,7 @@ class RouteCalculationSpec extends FunSuite {
).toMap ).toMap
val g = makeGraph(updates) val g = makeGraph(updates)
val route = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT) val route = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1)
assert(route.map(hops2Ids) === Success(2 :: 5 :: Nil)) assert(route.map(hops2Ids) === Success(2 :: 5 :: Nil))
} }
@ -133,11 +125,11 @@ class RouteCalculationSpec extends FunSuite {
val g = makeGraph(updates) val g = makeGraph(updates)
val route1 = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT) val route1 = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1)
assert(route1.map(hops2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil)) assert(route1.map(hops2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil))
val graphWithRemovedEdge = g.removeEdge(ChannelDesc(ShortChannelId(3L), c, d)) val graphWithRemovedEdge = g.removeEdge(ChannelDesc(ShortChannelId(3L), c, d))
val route2 = Router.findRoute(graphWithRemovedEdge, a, e, DEFAULT_AMOUNT_MSAT) val route2 = Router.findRoute(graphWithRemovedEdge, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1)
assert(route2.map(hops2Ids) === Failure(RouteNotFound)) assert(route2.map(hops2Ids) === Failure(RouteNotFound))
} }
@ -159,7 +151,7 @@ class RouteCalculationSpec extends FunSuite {
val graph = makeGraph(updates) val graph = makeGraph(updates)
val route = Router.findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT) val route = Router.findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, numRoutes = 1)
assert(route.map(hops2Ids) === Success(4 :: 3 :: Nil)) assert(route.map(hops2Ids) === Success(4 :: 3 :: Nil))
} }
@ -182,7 +174,7 @@ class RouteCalculationSpec extends FunSuite {
val graph = makeGraph(updates) val graph = makeGraph(updates)
val route = Router.findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT) val route = Router.findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, numRoutes = 1)
assert(route.map(hops2Ids) === Success(4 :: 3 :: Nil)) assert(route.map(hops2Ids) === Success(4 :: 3 :: Nil))
} }
@ -204,7 +196,7 @@ class RouteCalculationSpec extends FunSuite {
val graph = makeGraph(updates) val graph = makeGraph(updates)
val route = Router.findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT) val route = Router.findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, numRoutes = 1)
assert(route.map(hops2Ids) === Success(1 :: 6 :: 3 :: Nil)) assert(route.map(hops2Ids) === Success(1 :: 6 :: 3 :: Nil))
} }
@ -220,7 +212,7 @@ class RouteCalculationSpec extends FunSuite {
val g = makeGraph(updates) val g = makeGraph(updates)
val route = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT) val route = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1)
assert(route.map(hops2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil)) assert(route.map(hops2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil))
} }
@ -233,7 +225,7 @@ class RouteCalculationSpec extends FunSuite {
val g = makeGraph(updates) val g = makeGraph(updates)
val route = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT) val route = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1)
assert(route.map(hops2Ids) === Failure(RouteNotFound)) assert(route.map(hops2Ids) === Failure(RouteNotFound))
} }
@ -247,7 +239,7 @@ class RouteCalculationSpec extends FunSuite {
val g = makeGraph(updates) val g = makeGraph(updates)
val route = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT) val route = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1)
assert(route.map(hops2Ids) === Failure(RouteNotFound)) assert(route.map(hops2Ids) === Failure(RouteNotFound))
} }
@ -260,7 +252,7 @@ class RouteCalculationSpec extends FunSuite {
val g = makeGraph(updates).addVertex(a) val g = makeGraph(updates).addVertex(a)
val route = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT) val route = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1)
assert(route.map(hops2Ids) === Failure(RouteNotFound)) assert(route.map(hops2Ids) === Failure(RouteNotFound))
} }
@ -274,7 +266,7 @@ class RouteCalculationSpec extends FunSuite {
val g = makeGraph(updates) val g = makeGraph(updates)
val route = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT) val route = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1)
assert(route.map(hops2Ids) === Failure(RouteNotFound)) assert(route.map(hops2Ids) === Failure(RouteNotFound))
} }
@ -288,7 +280,7 @@ class RouteCalculationSpec extends FunSuite {
val g = makeGraph(updates) val g = makeGraph(updates)
val route = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT) val route = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1)
assert(route.map(hops2Ids) === Failure(RouteNotFound)) assert(route.map(hops2Ids) === Failure(RouteNotFound))
} }
@ -304,7 +296,7 @@ class RouteCalculationSpec extends FunSuite {
val g = makeGraph(updates) val g = makeGraph(updates)
val route = Router.findRoute(g, a, d, highAmount) val route = Router.findRoute(g, a, d, highAmount, numRoutes = 1)
assert(route.map(hops2Ids) === Failure(RouteNotFound)) assert(route.map(hops2Ids) === Failure(RouteNotFound))
} }
@ -321,7 +313,7 @@ class RouteCalculationSpec extends FunSuite {
val g = makeGraph(updates) val g = makeGraph(updates)
val route = Router.findRoute(g, a, d, lowAmount) val route = Router.findRoute(g, a, d, lowAmount, numRoutes = 1)
assert(route.map(hops2Ids) === Failure(RouteNotFound)) assert(route.map(hops2Ids) === Failure(RouteNotFound))
} }
@ -336,7 +328,7 @@ class RouteCalculationSpec extends FunSuite {
val g = makeGraph(updates) val g = makeGraph(updates)
val route = Router.findRoute(g, a, a, DEFAULT_AMOUNT_MSAT) val route = Router.findRoute(g, a, a, DEFAULT_AMOUNT_MSAT, numRoutes = 1)
assert(route.map(hops2Ids) === Failure(CannotRouteToSelf)) assert(route.map(hops2Ids) === Failure(CannotRouteToSelf))
} }
@ -351,7 +343,7 @@ class RouteCalculationSpec extends FunSuite {
val g = makeGraph(updates) val g = makeGraph(updates)
val route = Router.findRoute(g, a, b, DEFAULT_AMOUNT_MSAT) val route = Router.findRoute(g, a, b, DEFAULT_AMOUNT_MSAT, numRoutes = 1)
assert(route.map(hops2Ids) === Success(1 :: Nil)) assert(route.map(hops2Ids) === Success(1 :: Nil))
} }
@ -367,10 +359,10 @@ class RouteCalculationSpec extends FunSuite {
val g = makeGraph(updates) val g = makeGraph(updates)
val route1 = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT) val route1 = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1)
assert(route1.map(hops2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil)) assert(route1.map(hops2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil))
val route2 = Router.findRoute(g, e, a, DEFAULT_AMOUNT_MSAT) val route2 = Router.findRoute(g, e, a, DEFAULT_AMOUNT_MSAT, numRoutes = 1)
assert(route2.map(hops2Ids) === Failure(RouteNotFound)) assert(route2.map(hops2Ids) === Failure(RouteNotFound))
} }
@ -407,7 +399,7 @@ class RouteCalculationSpec extends FunSuite {
val g = makeGraph(updates) val g = makeGraph(updates)
val hops = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT).get val hops = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1).get
assert(hops === Hop(a, b, uab) :: Hop(b, c, ubc) :: Hop(c, d, ucd) :: Hop(d, e, ude) :: Nil) assert(hops === Hop(a, b, uab) :: Hop(b, c, ubc) :: Hop(c, d, ucd) :: Hop(d, e, ude) :: Nil)
} }
@ -447,7 +439,7 @@ class RouteCalculationSpec extends FunSuite {
val g = makeGraph(updates) val g = makeGraph(updates)
val route1 = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, ignoredEdges = Set(ChannelDesc(ShortChannelId(3L), c, d))) val route1 = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1 , ignoredEdges = Set(ChannelDesc(ShortChannelId(3L), c, d)))
assert(route1.map(hops2Ids) === Failure(RouteNotFound)) assert(route1.map(hops2Ids) === Failure(RouteNotFound))
// verify that we left the graph untouched // verify that we left the graph untouched
@ -456,7 +448,7 @@ class RouteCalculationSpec extends FunSuite {
assert(g.containsVertex(d)) assert(g.containsVertex(d))
// make sure we can find a route if without the blacklist // make sure we can find a route if without the blacklist
val route2 = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT) val route2 = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1)
assert(route2.map(hops2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil)) assert(route2.map(hops2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil))
} }
@ -469,14 +461,14 @@ class RouteCalculationSpec extends FunSuite {
val g = makeGraph(updates) val g = makeGraph(updates)
val route = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT) val route = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1)
assert(route.map(hops2Ids) === Failure(RouteNotFound)) assert(route.map(hops2Ids) === Failure(RouteNotFound))
// now we add the missing edge to reach the destination // now we add the missing edge to reach the destination
val (extraDesc, extraUpdate) = makeUpdate(4L, d, e, 5, 5) val (extraDesc, extraUpdate) = makeUpdate(4L, d, e, 5, 5)
val extraGraphEdges = Set(GraphEdge(extraDesc, extraUpdate)) val extraGraphEdges = Set(GraphEdge(extraDesc, extraUpdate))
val route1 = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, extraEdges = extraGraphEdges) val route1 = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, extraEdges = extraGraphEdges)
assert(route1.map(hops2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil)) assert(route1.map(hops2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil))
} }
@ -491,7 +483,7 @@ class RouteCalculationSpec extends FunSuite {
val g = makeGraph(updates) val g = makeGraph(updates)
val route1 = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT) val route1 = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1)
assert(route1.map(hops2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil)) assert(route1.map(hops2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil))
assert(route1.get(1).lastUpdate.feeBaseMsat == 10) assert(route1.get(1).lastUpdate.feeBaseMsat == 10)
@ -499,7 +491,7 @@ class RouteCalculationSpec extends FunSuite {
val extraGraphEdges = Set(GraphEdge(extraDesc, extraUpdate)) val extraGraphEdges = Set(GraphEdge(extraDesc, extraUpdate))
val route2 = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, extraEdges = extraGraphEdges) val route2 = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, extraEdges = extraGraphEdges)
assert(route2.map(hops2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil)) assert(route2.map(hops2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil))
assert(route2.get(1).lastUpdate.feeBaseMsat == 5) assert(route2.get(1).lastUpdate.feeBaseMsat == 5)
} }
@ -557,10 +549,10 @@ class RouteCalculationSpec extends FunSuite {
val g = makeGraph(updates) val g = makeGraph(updates)
assert(Router.findRoute(g, nodes(0), nodes(18), DEFAULT_AMOUNT_MSAT).map(hops2Ids) === Success(0 until 18)) assert(Router.findRoute(g, nodes(0), nodes(18), DEFAULT_AMOUNT_MSAT, numRoutes = 1).map(hops2Ids) === Success(0 until 18))
assert(Router.findRoute(g, nodes(0), nodes(19), DEFAULT_AMOUNT_MSAT).map(hops2Ids) === Success(0 until 19)) assert(Router.findRoute(g, nodes(0), nodes(19), DEFAULT_AMOUNT_MSAT, numRoutes = 1).map(hops2Ids) === Success(0 until 19))
assert(Router.findRoute(g, nodes(0), nodes(20), DEFAULT_AMOUNT_MSAT).map(hops2Ids) === Success(0 until 20)) assert(Router.findRoute(g, nodes(0), nodes(20), DEFAULT_AMOUNT_MSAT, numRoutes = 1).map(hops2Ids) === Success(0 until 20))
assert(Router.findRoute(g, nodes(0), nodes(21), DEFAULT_AMOUNT_MSAT).map(hops2Ids) === Failure(RouteNotFound)) assert(Router.findRoute(g, nodes(0), nodes(21), DEFAULT_AMOUNT_MSAT, numRoutes = 1).map(hops2Ids) === Failure(RouteNotFound))
} }
test("ignore cheaper route when it has more than 20 hops") { test("ignore cheaper route when it has more than 20 hops") {
@ -577,7 +569,7 @@ class RouteCalculationSpec extends FunSuite {
val g = makeGraph(updates2) val g = makeGraph(updates2)
val route = Router.findRoute(g, nodes(0), nodes(49), DEFAULT_AMOUNT_MSAT) val route = Router.findRoute(g, nodes(0), nodes(49), DEFAULT_AMOUNT_MSAT, numRoutes = 1)
assert(route.map(hops2Ids) === Success(0 :: 1 :: 99 :: 48 :: Nil)) assert(route.map(hops2Ids) === Success(0 :: 1 :: 99 :: 48 :: Nil))
} }
@ -593,10 +585,145 @@ class RouteCalculationSpec extends FunSuite {
val g = makeGraph(updates) val g = makeGraph(updates)
val route1 = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT) val route1 = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1)
assert(route1.map(hops2Ids) === Success(1 :: 2 :: 4 :: 5 :: Nil)) assert(route1.map(hops2Ids) === Success(1 :: 2 :: 4 :: 5 :: Nil))
} }
/**
*
* +---+ +---+ +---+
* | A +-----+ | B +----------> | C |
* +-+-+ | +-+-+ +-+-+
* ^ | ^ |
* | | | |
* | v----> + | |
* +-+-+ <-+-+ +-+-+
* | D +----------> | E +----------> | F |
* +---+ +---+ +---+
*
*/
test("find the k-shortest paths in a graph, k=4") {
val (a, b, c, d, e, f) = (
PublicKey("02999fa724ec3c244e4da52b4a91ad421dc96c9a810587849cd4b2469313519c73"), //a
PublicKey("03f1cb1af20fe9ccda3ea128e27d7c39ee27375c8480f11a87c17197e97541ca6a"), //b
PublicKey("0358e32d245ff5f5a3eb14c78c6f69c67cea7846bdf9aeeb7199e8f6fbb0306484"), //c
PublicKey("029e059b6780f155f38e83601969919aae631ddf6faed58fe860c72225eb327d7c"), //d
PublicKey("02f38f4e37142cc05df44683a83e22dea608cf4691492829ff4cf99888c5ec2d3a"), //e
PublicKey("03fc5b91ce2d857f146fd9b986363374ffe04dc143d8bcd6d7664c8873c463cdfc") //f
)
val edges = Seq(
makeUpdate(1L, d, a, 1, 0),
makeUpdate(2L, d, e, 1, 0),
makeUpdate(3L, a, e, 1, 0),
makeUpdate(4L, e, b, 1, 0),
makeUpdate(5L, e, f, 1, 0),
makeUpdate(6L, b, c, 1, 0),
makeUpdate(7L, c, f, 1, 0)
).toMap
val graph = DirectedGraph.makeGraph(edges)
val fourShortestPaths = Graph.yenKshortestPaths(graph, d, f, DEFAULT_AMOUNT_MSAT, Set.empty, Set.empty, pathsToFind = 4)
assert(fourShortestPaths.size === 4)
assert(hops2Ids(fourShortestPaths(0).path.map(graphEdgeToHop)) === 2 :: 5 :: Nil) // D -> E -> F
assert(hops2Ids(fourShortestPaths(1).path.map(graphEdgeToHop)) === 1 :: 3 :: 5 :: Nil) // D -> A -> E -> F
assert(hops2Ids(fourShortestPaths(2).path.map(graphEdgeToHop)) === 2 :: 4 :: 6 :: 7 :: Nil) // D -> E -> B -> C -> F
assert(hops2Ids(fourShortestPaths(3).path.map(graphEdgeToHop)) === 1 :: 3 :: 4 :: 6 :: 7 :: Nil) // D -> A -> E -> B -> C -> F
}
test("find the k shortest path (wikipedia example)") {
val (c, d, e, f, g, h) = (
PublicKey("02999fa724ec3c244e4da52b4a91ad421dc96c9a810587849cd4b2469313519c73"), //c
PublicKey("03f1cb1af20fe9ccda3ea128e27d7c39ee27375c8480f11a87c17197e97541ca6a"), //d
PublicKey("0358e32d245ff5f5a3eb14c78c6f69c67cea7846bdf9aeeb7199e8f6fbb0306484"), //e
PublicKey("029e059b6780f155f38e83601969919aae631ddf6faed58fe860c72225eb327d7c"), //f
PublicKey("02f38f4e37142cc05df44683a83e22dea608cf4691492829ff4cf99888c5ec2d3a"), //g
PublicKey("03fc5b91ce2d857f146fd9b986363374ffe04dc143d8bcd6d7664c8873c463cdfc") //h
)
val edges = Seq(
makeUpdate(10L, c, e, 2, 0),
makeUpdate(20L, c, d, 3, 0),
makeUpdate(30L, d, f, 4, 5), // D- > F has a higher cost to distinguish it from the 2nd cheapest route
makeUpdate(40L, e, d, 1, 0),
makeUpdate(50L, e, f, 2, 0),
makeUpdate(60L, e, g, 3, 0),
makeUpdate(70L, f, g, 2, 0),
makeUpdate(80L, f, h, 1, 0),
makeUpdate(90L, g, h, 2, 0)
)
val graph = DirectedGraph().addEdges(edges)
val twoShortestPaths = Graph.yenKshortestPaths(graph, c, h, DEFAULT_AMOUNT_MSAT, Set.empty, Set.empty, pathsToFind = 2)
assert(twoShortestPaths.size === 2)
val shortest = twoShortestPaths(0)
assert(hops2Ids(shortest.path.map(graphEdgeToHop)) === 10 :: 50 :: 80 :: Nil) // C -> E -> F -> H
val secondShortest = twoShortestPaths(1)
assert(hops2Ids(secondShortest.path.map(graphEdgeToHop)) === 10 :: 60 :: 90 :: Nil) // C -> E -> G -> H
}
test("terminate looking for k-shortest path if there are no more alternative paths than k"){
val f = randomKey.publicKey
// simple graph with only 2 possible paths from A to F
val edges = Seq(
makeUpdate(1L, a, b, 1, 0),
makeUpdate(2L, b, c, 1, 0),
makeUpdate(3L, c, f, 1, 0),
makeUpdate(4L, c, d, 1, 0),
makeUpdate(5L, d, e, 1, 0),
makeUpdate(6L, e, f, 1, 0)
)
val graph = DirectedGraph().addEdges(edges)
//we ask for 3 shortest paths but only 2 can be found
val foundPaths = Graph.yenKshortestPaths(graph, a, f, DEFAULT_AMOUNT_MSAT, Set.empty, Set.empty, pathsToFind = 3)
assert(foundPaths.size === 2)
assert(hops2Ids(foundPaths(0).path.map(graphEdgeToHop)) === 1 :: 2 :: 3 :: Nil) // A -> B -> C -> F
assert(hops2Ids(foundPaths(1).path.map(graphEdgeToHop)) === 1 :: 2 :: 4 :: 5 :: 6 :: Nil) // A -> B -> C -> D -> E -> F
}
test("select a random route below the allowed fee spread") {
val f = randomKey.publicKey
// A -> B -> C -> D has total cost of 10000005
// A -> E -> C -> D has total cost of 11080003 !!
// A -> E -> F -> D has total cost of 10000006
val g = makeGraph(List(
makeUpdate(1L, a, b, feeBaseMsat = 1, 0),
makeUpdate(4L, a, e, feeBaseMsat = 1, 0),
makeUpdate(2L, b, c, feeBaseMsat = 2, 0),
makeUpdate(3L, c, d, feeBaseMsat = 3, 0),
makeUpdate(5L, e, f, feeBaseMsat = 3, 0),
makeUpdate(6L, f, d, feeBaseMsat = 3, 0),
makeUpdate(7L, e, c, feeBaseMsat = 90000, 99000)
).toMap)
(for { _ <- 0 to 10 } yield Router.findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, numRoutes = 3)).map {
case Failure(_) => assert(false)
case Success(someRoute) =>
val routeCost = Graph.pathCost(hops2Edges(someRoute), DEFAULT_AMOUNT_MSAT)
val allowedSpread = DEFAULT_AMOUNT_MSAT + (DEFAULT_AMOUNT_MSAT * Router.DEFAULT_ALLOWED_SPREAD)
// over the three routes we could only get the 2 cheapest because the third is too expensive (over 10% of the cheapest)
assert(routeCost == 10000005 || routeCost == 10000006)
assert(routeCost < allowedSpread)
}
}
} }
object RouteCalculationSpec { object RouteCalculationSpec {
@ -632,4 +759,6 @@ object RouteCalculationSpec {
def hops2Ids(route: Seq[Hop]) = route.map(hop => hop.lastUpdate.shortChannelId.toLong) def hops2Ids(route: Seq[Hop]) = route.map(hop => hop.lastUpdate.shortChannelId.toLong)
def hops2Edges(route: Seq[Hop]) = route.map(hop => GraphEdge(ChannelDesc(hop.lastUpdate.shortChannelId, hop.nodeId, hop.nextNodeId), hop.lastUpdate))
} }