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:
parent
74d454d904
commit
d5fe47572c
3 changed files with 295 additions and 56 deletions
|
@ -9,9 +9,8 @@ import Router._
|
|||
|
||||
object Graph {
|
||||
|
||||
import DirectedGraph._
|
||||
|
||||
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
|
||||
|
@ -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
|
||||
* 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
|
||||
* @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] = {
|
||||
|
||||
|
|
|
@ -28,7 +28,9 @@ import fr.acinq.eclair.channel._
|
|||
import fr.acinq.eclair.crypto.TransportHandler
|
||||
import fr.acinq.eclair.io.Peer.{ChannelClosed, InvalidSignature, NonexistingChannel, PeerRoutingMessage}
|
||||
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.WeightedPath
|
||||
import fr.acinq.eclair.transactions.Scripts
|
||||
import fr.acinq.eclair.wire._
|
||||
import scala.collection.{SortedSet, mutable}
|
||||
|
@ -36,7 +38,7 @@ import scala.collection.immutable.{SortedMap, TreeMap}
|
|||
import scala.compat.Platform
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.{ExecutionContext, Promise}
|
||||
import scala.util.Try
|
||||
import scala.util.{Random, Try}
|
||||
|
||||
// @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
|
||||
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
|
||||
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))
|
||||
.recover { case t => sender ! Status.Failure(t) }
|
||||
stay
|
||||
|
@ -776,24 +779,42 @@ object Router {
|
|||
*/
|
||||
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 localNodeId
|
||||
* @param targetNodeId
|
||||
* @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 ignoredEdges a set of extra edges we want to IGNORE during the search
|
||||
* @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
|
||||
|
||||
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 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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,11 +19,11 @@ package fr.acinq.eclair.router
|
|||
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
|
||||
import fr.acinq.bitcoin.{BinaryData, Block, Crypto}
|
||||
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.wire._
|
||||
import fr.acinq.eclair.{ShortChannelId, nodeFee, randomKey}
|
||||
import fr.acinq.eclair.{ShortChannelId, randomKey}
|
||||
import org.scalatest.FunSuite
|
||||
|
||||
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)
|
||||
|
||||
|
||||
// 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") {
|
||||
|
||||
val updates = List(
|
||||
|
@ -55,7 +47,7 @@ class RouteCalculationSpec extends FunSuite {
|
|||
|
||||
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))
|
||||
}
|
||||
|
@ -93,16 +85,16 @@ class RouteCalculationSpec extends FunSuite {
|
|||
|
||||
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(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)
|
||||
val (desc, update) = makeUpdate(5L, e, f, feeBaseMsat = 1, feeProportionalMillionth = 400, minHtlcMsat = 0, maxHtlcMsat = Some(10005))
|
||||
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)
|
||||
}
|
||||
|
@ -117,7 +109,7 @@ class RouteCalculationSpec extends FunSuite {
|
|||
).toMap
|
||||
|
||||
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))
|
||||
}
|
||||
|
@ -133,11 +125,11 @@ class RouteCalculationSpec extends FunSuite {
|
|||
|
||||
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))
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
|
@ -159,7 +151,7 @@ class RouteCalculationSpec extends FunSuite {
|
|||
|
||||
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))
|
||||
|
||||
}
|
||||
|
@ -182,7 +174,7 @@ class RouteCalculationSpec extends FunSuite {
|
|||
|
||||
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))
|
||||
}
|
||||
|
||||
|
@ -204,7 +196,7 @@ class RouteCalculationSpec extends FunSuite {
|
|||
|
||||
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))
|
||||
}
|
||||
|
||||
|
@ -220,7 +212,7 @@ class RouteCalculationSpec extends FunSuite {
|
|||
|
||||
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))
|
||||
}
|
||||
|
||||
|
@ -233,7 +225,7 @@ class RouteCalculationSpec extends FunSuite {
|
|||
|
||||
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))
|
||||
}
|
||||
|
||||
|
@ -247,7 +239,7 @@ class RouteCalculationSpec extends FunSuite {
|
|||
|
||||
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))
|
||||
}
|
||||
|
||||
|
@ -260,7 +252,7 @@ class RouteCalculationSpec extends FunSuite {
|
|||
|
||||
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))
|
||||
}
|
||||
|
||||
|
@ -274,7 +266,7 @@ class RouteCalculationSpec extends FunSuite {
|
|||
|
||||
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))
|
||||
}
|
||||
|
||||
|
@ -288,7 +280,7 @@ class RouteCalculationSpec extends FunSuite {
|
|||
|
||||
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))
|
||||
}
|
||||
|
||||
|
@ -304,7 +296,7 @@ class RouteCalculationSpec extends FunSuite {
|
|||
|
||||
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))
|
||||
|
||||
}
|
||||
|
@ -321,7 +313,7 @@ class RouteCalculationSpec extends FunSuite {
|
|||
|
||||
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))
|
||||
|
||||
}
|
||||
|
@ -336,7 +328,7 @@ class RouteCalculationSpec extends FunSuite {
|
|||
|
||||
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))
|
||||
}
|
||||
|
||||
|
@ -351,7 +343,7 @@ class RouteCalculationSpec extends FunSuite {
|
|||
|
||||
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))
|
||||
}
|
||||
|
||||
|
@ -367,10 +359,10 @@ class RouteCalculationSpec extends FunSuite {
|
|||
|
||||
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))
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
|
@ -407,7 +399,7 @@ class RouteCalculationSpec extends FunSuite {
|
|||
|
||||
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)
|
||||
}
|
||||
|
@ -447,7 +439,7 @@ class RouteCalculationSpec extends FunSuite {
|
|||
|
||||
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))
|
||||
|
||||
// verify that we left the graph untouched
|
||||
|
@ -456,7 +448,7 @@ class RouteCalculationSpec extends FunSuite {
|
|||
assert(g.containsVertex(d))
|
||||
|
||||
// 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))
|
||||
}
|
||||
|
||||
|
@ -469,14 +461,14 @@ class RouteCalculationSpec extends FunSuite {
|
|||
|
||||
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))
|
||||
|
||||
// now we add the missing edge to reach the destination
|
||||
val (extraDesc, extraUpdate) = makeUpdate(4L, d, e, 5, 5)
|
||||
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))
|
||||
}
|
||||
|
||||
|
@ -491,7 +483,7 @@ class RouteCalculationSpec extends FunSuite {
|
|||
|
||||
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.get(1).lastUpdate.feeBaseMsat == 10)
|
||||
|
||||
|
@ -499,7 +491,7 @@ class RouteCalculationSpec extends FunSuite {
|
|||
|
||||
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.get(1).lastUpdate.feeBaseMsat == 5)
|
||||
}
|
||||
|
@ -557,10 +549,10 @@ class RouteCalculationSpec extends FunSuite {
|
|||
|
||||
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(19), DEFAULT_AMOUNT_MSAT).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(21), DEFAULT_AMOUNT_MSAT).map(hops2Ids) === Failure(RouteNotFound))
|
||||
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, numRoutes = 1).map(hops2Ids) === Success(0 until 19))
|
||||
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, numRoutes = 1).map(hops2Ids) === Failure(RouteNotFound))
|
||||
}
|
||||
|
||||
test("ignore cheaper route when it has more than 20 hops") {
|
||||
|
@ -577,7 +569,7 @@ class RouteCalculationSpec extends FunSuite {
|
|||
|
||||
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))
|
||||
}
|
||||
|
||||
|
@ -593,10 +585,145 @@ class RouteCalculationSpec extends FunSuite {
|
|||
|
||||
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))
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* +---+ +---+ +---+
|
||||
* | 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 {
|
||||
|
@ -632,4 +759,6 @@ object RouteCalculationSpec {
|
|||
|
||||
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))
|
||||
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue