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 {
|
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] = {
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue