From abb263ea0b6cd660c58c299325bc3ecf02206100 Mon Sep 17 00:00:00 2001 From: Thomas HUET Date: Wed, 27 Oct 2021 15:47:57 +0200 Subject: [PATCH] Use balance estimates from past payments in path-finding --- eclair-core/src/main/resources/reference.conf | 2 + .../scala/fr/acinq/eclair/NodeParams.scala | 1 + .../remote/EclairInternalsSerializer.scala | 3 +- .../acinq/eclair/router/BalanceEstimate.scala | 11 +- .../scala/fr/acinq/eclair/router/Graph.scala | 120 ++++--- .../eclair/router/RouteCalculation.scala | 14 +- .../eclair/router/BalanceEstimateSpec.scala | 18 +- .../fr/acinq/eclair/router/GraphSpec.scala | 24 +- .../eclair/router/RouteCalculationSpec.scala | 311 +++++++++--------- 9 files changed, 261 insertions(+), 243 deletions(-) diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 1ee6a20b28..0c4a1c0260 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -288,6 +288,8 @@ eclair { // probability of success, however is penalizes less the paths with a low probability of success. use-log-probability = false + use-past-relay-data = false + mpp { min-amount-satoshis = 15000 // minimum amount sent via partial HTLCs max-parts = 5 // maximum number of HTLCs sent per payment: increasing this value will impact performance diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala index 7a7fdbb066..9d8eee57fa 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -367,6 +367,7 @@ object NodeParams extends Logging { failureCost = getRelayFees(config.getConfig("failure-cost")), hopCost = getRelayFees(config.getConfig("hop-cost")), useLogProbability = config.getBoolean("use-log-probability"), + usePastRelaysData = config.getBoolean("use-past-relay-data"), )) }, mpp = MultiPartParams( diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala index d35bb6c37f..c193b6da68 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala @@ -68,7 +68,8 @@ object EclairInternalsSerializer { ("lockedFundsRisk" | double) :: ("failureCost" | relayFeesCodec) :: ("hopCost" | relayFeesCodec) :: - ("useLogProbability" | bool(8))).as[HeuristicsConstants] + ("useLogProbability" | bool(8)) :: + ("usePastRelaysData" | bool(8))).as[HeuristicsConstants] val multiPartParamsCodec: Codec[MultiPartParams] = ( ("minPartAmount" | millisatoshi) :: diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/BalanceEstimate.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/BalanceEstimate.scala index 18ea2a810d..a4d6f079f3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/BalanceEstimate.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/BalanceEstimate.scala @@ -234,6 +234,8 @@ object BalanceEstimate { case class BalancesEstimates(balances: Map[(PublicKey, PublicKey), BalanceEstimate], defaultHalfLife: FiniteDuration) { private def get(a: PublicKey, b: PublicKey): Option[BalanceEstimate] = balances.get((a, b)) + def get(edge: GraphEdge): BalanceEstimate = get(edge.desc.a, edge.desc.b).getOrElse(BalanceEstimate.empty(defaultHalfLife).addEdge(edge)) + def addEdge(edge: GraphEdge): BalancesEstimates = BalancesEstimates( balances.updatedWith((edge.desc.a, edge.desc.b))(balance => Some(balance.getOrElse(BalanceEstimate.empty(defaultHalfLife)).addEdge(edge)) @@ -283,7 +285,7 @@ case class BalancesEstimates(balances: Map[(PublicKey, PublicKey), BalanceEstima } -case class GraphWithBalanceEstimates(graph: DirectedGraph, private val balances: BalancesEstimates) { +case class GraphWithBalanceEstimates(graph: DirectedGraph, balances: BalancesEstimates) { def addEdge(edge: GraphEdge): GraphWithBalanceEstimates = GraphWithBalanceEstimates(graph.addEdge(edge), balances.addEdge(edge)) def removeEdge(desc: ChannelDesc): GraphWithBalanceEstimates = GraphWithBalanceEstimates(graph.removeEdge(desc), balances.removeEdge(desc)) @@ -312,13 +314,6 @@ case class GraphWithBalanceEstimates(graph: DirectedGraph, private val balances: def channelCouldNotSend(hop: ChannelHop, amount: MilliSatoshi): GraphWithBalanceEstimates = { GraphWithBalanceEstimates(graph, balances.channelCouldNotSend(hop, amount)) } - - def canSend(amount: MilliSatoshi, edge: GraphEdge): Double = { - balances.balances.get((edge.desc.a, edge.desc.b)) match { - case Some(estimate) => estimate.canSend(amount) - case None => BalanceEstimate.empty(1 hour).addEdge(edge).canSend(amount) - } - } } object GraphWithBalanceEstimates { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala index 73c4fcca11..0f80b6db59 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala @@ -61,11 +61,12 @@ object Graph { * The fee for a failed attempt and the fee per hop are never actually spent, they are used to incentivize shorter * paths or path with higher success probability. * - * @param lockedFundsRisk cost of having funds locked in htlc in msat per msat per block - * @param failureCost fee for a failed attempt - * @param hopCost virtual fee per hop (how much we're willing to pay to make the route one hop shorter) + * @param lockedFundsRisk cost of having funds locked in htlc in msat per msat per block + * @param failureCost fee for a failed attempt + * @param hopCost virtual fee per hop (how much we're willing to pay to make the route one hop shorter) + * @param usePastRelaysData use data from past relays to estimate the balance of the channels */ - case class HeuristicsConstants(lockedFundsRisk: Double, failureCost: RelayFees, hopCost: RelayFees, useLogProbability: Boolean) + case class HeuristicsConstants(lockedFundsRisk: Double, failureCost: RelayFees, hopCost: RelayFees, useLogProbability: Boolean, usePastRelaysData: Boolean) case class WeightedNode(key: PublicKey, weight: RichWeight) @@ -109,7 +110,7 @@ object Graph { * @param boundaries a predicate function that can be used to impose limits on the outcome of the search * @param includeLocalChannelCost if the path is for relaying and we need to include the cost of the local channel */ - def yenKshortestPaths(graph: DirectedGraph, + def yenKshortestPaths(g: GraphWithBalanceEstimates, sourceNode: PublicKey, targetNode: PublicKey, amount: MilliSatoshi, @@ -123,7 +124,7 @@ object Graph { includeLocalChannelCost: Boolean): Seq[WeightedPath] = { // find the shortest path (k = 0) val targetWeight = RichWeight(amount, 0, CltvExpiryDelta(0), 1.0, 0 msat, 0 msat, 0.0) - val shortestPath = dijkstraShortestPath(graph, sourceNode, targetNode, ignoredEdges, ignoredVertices, extraEdges, targetWeight, boundaries, currentBlockHeight, wr, includeLocalChannelCost) + val shortestPath = dijkstraShortestPath(g, sourceNode, targetNode, ignoredEdges, ignoredVertices, extraEdges, targetWeight, boundaries, currentBlockHeight, wr, includeLocalChannelCost) if (shortestPath.isEmpty) { return Seq.empty // if we can't even find a single path, avoid returning a Seq(Seq.empty) } @@ -135,7 +136,7 @@ object Graph { var allSpurPathsFound = false val shortestPaths = new mutable.Queue[PathWithSpur] - shortestPaths.enqueue(PathWithSpur(WeightedPath(shortestPath, pathWeight(sourceNode, shortestPath, amount, currentBlockHeight, wr, includeLocalChannelCost)), 0)) + shortestPaths.enqueue(PathWithSpur(WeightedPath(shortestPath, pathWeight(g.balances, sourceNode, shortestPath, amount, currentBlockHeight, wr, includeLocalChannelCost)), 0)) // stores the candidates for the k-th shortest path, sorted by path cost val candidates = new mutable.PriorityQueue[PathWithSpur] @@ -160,12 +161,12 @@ object Graph { val alreadyExploredEdges = shortestPaths.collect { case p if p.p.path.takeRight(i) == rootPathEdges => p.p.path(p.p.path.length - 1 - i).desc }.toSet // we also want to ignore any vertex on the root path to prevent loops val alreadyExploredVertices = rootPathEdges.map(_.desc.b).toSet - val rootPathWeight = pathWeight(sourceNode, rootPathEdges, amount, currentBlockHeight, wr, includeLocalChannelCost) + val rootPathWeight = pathWeight(g.balances, sourceNode, rootPathEdges, amount, currentBlockHeight, wr, includeLocalChannelCost) // find the "spur" path, a sub-path going from the spur node to the target avoiding previously found sub-paths - val spurPath = dijkstraShortestPath(graph, sourceNode, spurNode, ignoredEdges ++ alreadyExploredEdges, ignoredVertices ++ alreadyExploredVertices, extraEdges, rootPathWeight, boundaries, currentBlockHeight, wr, includeLocalChannelCost) + val spurPath = dijkstraShortestPath(g, sourceNode, spurNode, ignoredEdges ++ alreadyExploredEdges, ignoredVertices ++ alreadyExploredVertices, extraEdges, rootPathWeight, boundaries, currentBlockHeight, wr, includeLocalChannelCost) if (spurPath.nonEmpty) { val completePath = spurPath ++ rootPathEdges - val candidatePath = WeightedPath(completePath, pathWeight(sourceNode, completePath, amount, currentBlockHeight, wr, includeLocalChannelCost)) + val candidatePath = WeightedPath(completePath, pathWeight(g.balances, sourceNode, completePath, amount, currentBlockHeight, wr, includeLocalChannelCost)) candidates.enqueue(PathWithSpur(candidatePath, i)) } } @@ -200,7 +201,7 @@ object Graph { * @param wr ratios used to 'weight' edges when searching for the shortest path * @param includeLocalChannelCost if the path is for relaying and we need to include the cost of the local channel */ - private def dijkstraShortestPath(g: DirectedGraph, + private def dijkstraShortestPath(g: GraphWithBalanceEstimates, sourceNode: PublicKey, targetNode: PublicKey, ignoredEdges: Set[ChannelDesc], @@ -212,8 +213,8 @@ object Graph { wr: Either[WeightRatios, HeuristicsConstants], includeLocalChannelCost: Boolean): Seq[GraphEdge] = { // the graph does not contain source/destination nodes - val sourceNotInGraph = !g.containsVertex(sourceNode) && !extraEdges.exists(_.desc.a == sourceNode) - val targetNotInGraph = !g.containsVertex(targetNode) && !extraEdges.exists(_.desc.b == targetNode) + val sourceNotInGraph = !g.graph.containsVertex(sourceNode) && !extraEdges.exists(_.desc.a == sourceNode) + val targetNotInGraph = !g.graph.containsVertex(targetNode) && !extraEdges.exists(_.desc.b == targetNode) if (sourceNotInGraph || targetNotInGraph) { return Seq.empty } @@ -242,10 +243,15 @@ object Graph { val neighborEdges = { val extraNeighbors = extraEdges.filter(_.desc.b == current.key) // the resulting set must have only one element per shortChannelId; we prioritize extra edges - g.getIncomingEdgesOf(current.key).filterNot(e => extraNeighbors.exists(_.desc.shortChannelId == e.desc.shortChannelId)) ++ extraNeighbors + extraNeighbors.foldLeft(g.graph.getIncomingEdgesOf(current.key))( + (neighbors, edge) => + neighbors.updatedWith(edge.desc.a) { + case None => Some(edge :: Nil) + case Some(l) => Some(edge +: l.filterNot(_.desc.shortChannelId == edge.desc.shortChannelId)) + }) } - neighborEdges.foreach { edge => - val neighbor = edge.desc.a + for ((neighbor -> edges) <- neighborEdges; + edge <- edges) { if (current.weight.amount <= edge.capacity && edge.balance_opt.forall(current.weight.amount <= _) && edge.params.htlcMaximum_opt.forall(current.weight.amount <= _) && @@ -254,7 +260,7 @@ object Graph { !ignoredVertices.contains(neighbor)) { // NB: this contains the amount (including fees) that will need to be sent to `neighbor`, but the amount that // will be relayed through that edge is the one in `currentWeight`. - val neighborWeight = addEdgeWeight(sourceNode, edge, current.weight, currentBlockHeight, wr, includeLocalChannelCost) + val neighborWeight = addEdgeWeight(sourceNode, edge, g.balances.get(edge), current.weight, currentBlockHeight, wr, includeLocalChannelCost) if (boundaries(neighborWeight)) { val previousNeighborWeight = bestWeights.getOrElse(neighbor, RichWeight(MilliSatoshi(Long.MaxValue), Int.MaxValue, CltvExpiryDelta(Int.MaxValue), 0.0, MilliSatoshi(Long.MaxValue), MilliSatoshi(Long.MaxValue), Double.MaxValue)) // if this path between neighbor and the target has a shorter distance than previously known, we select it @@ -298,7 +304,7 @@ object Graph { * @param weightRatios ratios used to 'weight' edges when searching for the shortest path * @param includeLocalChannelCost if the path is for relaying and we need to include the cost of the local channel */ - private def addEdgeWeight(sender: PublicKey, edge: GraphEdge, prev: RichWeight, currentBlockHeight: BlockHeight, weightRatios: Either[WeightRatios, HeuristicsConstants], includeLocalChannelCost: Boolean): RichWeight = { + private def addEdgeWeight(sender: PublicKey, edge: GraphEdge, balance: BalanceEstimate, prev: RichWeight, currentBlockHeight: BlockHeight, weightRatios: Either[WeightRatios, HeuristicsConstants], includeLocalChannelCost: Boolean): RichWeight = { val totalAmount = if (edge.desc.a == sender && !includeLocalChannelCost) prev.amount else addEdgeFees(edge, prev.amount) val fee = totalAmount - prev.amount val totalFees = prev.fees + fee @@ -335,7 +341,14 @@ object Graph { val hopCost = nodeFee(heuristicsConstants.hopCost, prev.amount) val totalHopsCost = prev.virtualFees + hopCost // If we know the balance of the channel, then we will check separately that it can relay the payment. - val successProbability = if (edge.balance_opt.nonEmpty) 1.0 else 1.0 - prev.amount.toLong.toDouble / edge.capacity.toMilliSatoshi.toLong.toDouble + val successProbability = + if (edge.balance_opt.nonEmpty){ + 1.0 + } else if (heuristicsConstants.usePastRelaysData) { + balance.canSend(prev.amount) + } else { + 1.0 - prev.amount.toLong.toDouble / edge.capacity.toMilliSatoshi.toLong.toDouble + } if (successProbability < 0) { throw NegativeProbability(edge, prev, heuristicsConstants) } @@ -390,9 +403,9 @@ object Graph { * @param wr ratios used to 'weight' edges when searching for the shortest path * @param includeLocalChannelCost if the path is for relaying and we need to include the cost of the local channel */ - def pathWeight(sender: PublicKey, path: Seq[GraphEdge], amount: MilliSatoshi, currentBlockHeight: BlockHeight, wr: Either[WeightRatios, HeuristicsConstants], includeLocalChannelCost: Boolean): RichWeight = { + def pathWeight(balances: BalancesEstimates, sender: PublicKey, path: Seq[GraphEdge], amount: MilliSatoshi, currentBlockHeight: BlockHeight, wr: Either[WeightRatios, HeuristicsConstants], includeLocalChannelCost: Boolean): RichWeight = { path.foldRight(RichWeight(amount, 0, CltvExpiryDelta(0), 1.0, 0 msat, 0 msat, 0.0)) { (edge, prev) => - addEdgeWeight(sender, edge, prev, currentBlockHeight, wr, includeLocalChannelCost) + addEdgeWeight(sender, edge, balances.get(edge), prev, currentBlockHeight, wr, includeLocalChannelCost) } } @@ -467,8 +480,10 @@ object Graph { ) } - /** A graph data structure that uses an adjacency list, stores the incoming edges of the neighbors */ - case class DirectedGraph(private val vertices: Map[PublicKey, List[GraphEdge]]) { + /** A graph data structure that uses a sparse adjacency matrix. + * `edgesBetween(b)(a)` is the list of edges (= channels) from `a` to `b`. + */ + case class DirectedGraph(private val edgesBetween: Map[PublicKey, Map[PublicKey, List[GraphEdge]]]) { def addEdges(edges: Iterable[GraphEdge]): DirectedGraph = edges.foldLeft(this)((acc, edge) => acc.addEdge(edge)) @@ -481,13 +496,8 @@ object Graph { def addEdge(edge: GraphEdge): DirectedGraph = { val vertexIn = edge.desc.a val vertexOut = edge.desc.b - // the graph is allowed to have multiple edges between the same vertices but only one per channel - if (containsEdge(edge.desc)) { - removeEdge(edge.desc).addEdge(edge) // the recursive call will have the original params - } else { - val withVertices = addVertex(vertexIn).addVertex(vertexOut) - DirectedGraph(withVertices.vertices.updated(vertexOut, edge +: withVertices.vertices(vertexOut))) - } + val withVertices = addVertex(vertexIn) + DirectedGraph(withVertices.edgesBetween.updatedWith(vertexOut)(adj => Some(adj.getOrElse(Map.empty).updatedWith(vertexIn)(channels => Some(edge +: channels.map(_.filterNot(_.desc == edge.desc)).getOrElse(Nil)))))) } /** @@ -499,7 +509,7 @@ object Graph { */ def removeEdge(desc: ChannelDesc): DirectedGraph = { if (containsEdge(desc)) { - DirectedGraph(vertices.updated(desc.b, vertices(desc.b).filterNot(_.desc == desc))) + DirectedGraph(edgesBetween.updatedWith(desc.b)(_.map(_.updatedWith(desc.a)(_.map(_.filterNot(_.desc == desc)))))) } else { this } @@ -515,9 +525,7 @@ object Graph { def getEdge(edge: GraphEdge): Option[GraphEdge] = getEdge(edge.desc) def getEdge(desc: ChannelDesc): Option[GraphEdge] = { - vertices.get(desc.b).flatMap { adj => - adj.find(e => e.desc.shortChannelId == desc.shortChannelId && e.desc.a == desc.a) - } + edgesBetween.get(desc.b).flatMap(_.get(desc.a)).flatMap(_.find(e => e.desc.shortChannelId == desc.shortChannelId)) } /** @@ -526,33 +534,34 @@ object Graph { * @return all the edges going from keyA --> keyB (there might be more than one if there are multiple channels) */ def getEdgesBetween(keyA: PublicKey, keyB: PublicKey): Seq[GraphEdge] = { - vertices.get(keyB) match { - case None => Seq.empty - case Some(adj) => adj.filter(e => e.desc.a == keyA) - } + edgesBetween.get(keyB).flatMap(_.get(keyA)).getOrElse(Seq.empty) } /** * @param keyB the key associated with the target vertex * @return all edges incoming to that vertex */ - def getIncomingEdgesOf(keyB: PublicKey): Seq[GraphEdge] = { - vertices.getOrElse(keyB, List.empty) + def getIncomingEdgesOf(keyB: PublicKey): Map[PublicKey, Seq[GraphEdge]] = { + edgesBetween.getOrElse(keyB, Map.empty) } /** * Removes a vertex and all its associated edges (both incoming and outgoing) */ def removeVertex(key: PublicKey): DirectedGraph = { - DirectedGraph(removeEdges(getIncomingEdgesOf(key).map(_.desc)).vertices - key) + val withoutVertex = + for ((b -> adj) <- edgesBetween if b != key) + yield b -> (for ((a -> edges) <- adj if a != key) + yield (a -> edges)) + DirectedGraph(withoutVertex) } /** * Adds a new vertex to the graph, starting with no edges */ def addVertex(key: PublicKey): DirectedGraph = { - vertices.get(key) match { - case None => DirectedGraph(vertices + (key -> List.empty)) + edgesBetween.get(key) match { + case None => DirectedGraph(edgesBetween + (key -> Map.empty[PublicKey, List[GraphEdge]])) case _ => this } } @@ -569,31 +578,34 @@ object Graph { /** * @return the set of all the vertices in this graph */ - def vertexSet(): Set[PublicKey] = vertices.keySet + def vertexSet(): Set[PublicKey] = edgesBetween.keySet /** * @return an iterator of all the edges in this graph */ - def edgeSet(): Iterable[GraphEdge] = vertices.values.flatten + def edgeSet(): Iterable[GraphEdge] = edgesBetween.values.flatMap(_.values).flatten /** * @return true if this graph contain a vertex with this key, false otherwise */ - def containsVertex(key: PublicKey): Boolean = vertices.contains(key) + def containsVertex(key: PublicKey): Boolean = edgesBetween.contains(key) /** * @return true if this edge desc is in the graph. For edges to be considered equal they must have the same in/out vertices AND same shortChannelId */ def containsEdge(desc: ChannelDesc): Boolean = { - vertices.get(desc.b) match { + edgesBetween.get(desc.b) match { case None => false - case Some(adj) => adj.exists(neighbor => neighbor.desc.shortChannelId == desc.shortChannelId && neighbor.desc.a == desc.a) + case Some(adj) => adj.get(desc.a) match { + case None => false + case Some(channels) => channels.exists(neighbor => neighbor.desc.shortChannelId == desc.shortChannelId) + } } } def prettyPrint(): String = { - vertices.foldLeft("") { case (acc, (vertex, adj)) => - acc + s"[${vertex.toString().take(5)}]: ${adj.map("-> " + _.desc.b.toString().take(5))} \n" + edgesBetween.foldLeft("") { case (acc, (vertex -> adj)) => + acc + s"[${vertex.toString().take(5)}]: ${adj.keys.map("-> " + _.toString().take(5))} \n" } } } @@ -602,7 +614,7 @@ object Graph { // @formatter:off def apply(): DirectedGraph = new DirectedGraph(Map()) - def apply(key: PublicKey): DirectedGraph = new DirectedGraph(Map(key -> List.empty)) + def apply(key: PublicKey): DirectedGraph = new DirectedGraph(Map(key -> Map.empty)) def apply(edge: GraphEdge): DirectedGraph = DirectedGraph().addEdge(edge) def apply(edges: Seq[GraphEdge]): DirectedGraph = DirectedGraph().addEdges(edges) // @formatter:on @@ -617,7 +629,7 @@ object Graph { */ def makeGraph(channels: SortedMap[RealShortChannelId, PublicChannel]): DirectedGraph = { // initialize the map with the appropriate size to avoid resizing during the graph initialization - val mutableMap = new mutable.HashMap[PublicKey, List[GraphEdge]](initialCapacity = channels.size + 1, mutable.HashMap.defaultLoadFactor) + val mutableMap = new mutable.HashMap[PublicKey, Map[PublicKey, List[GraphEdge]]](initialCapacity = channels.size + 1, mutable.HashMap.defaultLoadFactor) // add all the vertices and edges in one go channels.values.foreach { channel => @@ -626,9 +638,9 @@ object Graph { } def addToMap(edge: GraphEdge): Unit = { - mutableMap.put(edge.desc.b, edge +: mutableMap.getOrElse(edge.desc.b, List.empty[GraphEdge])) + mutableMap.updateWith(edge.desc.b)(o => Some(o.getOrElse(Map.empty[PublicKey, List[GraphEdge]]).updatedWith(edge.desc.a)(p => Some(edge +: p.getOrElse(List.empty[GraphEdge]))))) if (!mutableMap.contains(edge.desc.a)) { - mutableMap += edge.desc.a -> List.empty[GraphEdge] + mutableMap += edge.desc.a -> Map.empty[PublicKey, List[GraphEdge]] } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala index 200a81ed2e..0a3dbfbe3a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala @@ -120,9 +120,9 @@ object RouteCalculation { val tags = TagSet.Empty.withTag(Tags.MultiPart, r.allowMultiPart).withTag(Tags.Amount, Tags.amountBucket(r.amount)) KamonExt.time(Metrics.FindRouteDuration.withTags(tags.withTag(Tags.NumberOfRoutes, routesToFind.toLong))) { val result = if (r.allowMultiPart) { - findMultiPartRoute(d.graphWithBalances.graph, r.source, r.target, r.amount, r.maxFee, extraEdges, ignoredEdges, r.ignore.nodes, r.pendingPayments, params, currentBlockHeight) + findMultiPartRoute(d.graphWithBalances, r.source, r.target, r.amount, r.maxFee, extraEdges, ignoredEdges, r.ignore.nodes, r.pendingPayments, params, currentBlockHeight) } else { - findRoute(d.graphWithBalances.graph, r.source, r.target, r.amount, r.maxFee, routesToFind, extraEdges, ignoredEdges, r.ignore.nodes, params, currentBlockHeight) + findRoute(d.graphWithBalances, r.source, r.target, r.amount, r.maxFee, routesToFind, extraEdges, ignoredEdges, r.ignore.nodes, params, currentBlockHeight) } result match { case Success(routes) => @@ -204,7 +204,7 @@ object RouteCalculation { * @param routeParams a set of parameters that can restrict the route search * @return the computed routes to the destination @param targetNodeId */ - def findRoute(g: DirectedGraph, + def findRoute(g: GraphWithBalanceEstimates, localNodeId: PublicKey, targetNodeId: PublicKey, amount: MilliSatoshi, @@ -222,7 +222,7 @@ object RouteCalculation { } @tailrec - private def findRouteInternal(g: DirectedGraph, + private def findRouteInternal(g: GraphWithBalanceEstimates, localNodeId: PublicKey, targetNodeId: PublicKey, amount: MilliSatoshi, @@ -280,7 +280,7 @@ object RouteCalculation { * @param routeParams a set of parameters that can restrict the route search * @return a set of disjoint routes to the destination @param targetNodeId with the payment amount split between them */ - def findMultiPartRoute(g: DirectedGraph, + def findMultiPartRoute(g: GraphWithBalanceEstimates, localNodeId: PublicKey, targetNodeId: PublicKey, amount: MilliSatoshi, @@ -304,7 +304,7 @@ object RouteCalculation { } } - private def findMultiPartRouteInternal(g: DirectedGraph, + private def findMultiPartRouteInternal(g: GraphWithBalanceEstimates, localNodeId: PublicKey, targetNodeId: PublicKey, amount: MilliSatoshi, @@ -319,7 +319,7 @@ object RouteCalculation { // When the recipient is a direct peer, we have complete visibility on our local channels so we can use more accurate MPP parameters. val routeParams1 = { case class DirectChannel(balance: MilliSatoshi, isEmpty: Boolean) - val directChannels = g.getEdgesBetween(localNodeId, targetNodeId).collect { + val directChannels = g.graph.getEdgesBetween(localNodeId, targetNodeId).collect { // We should always have balance information available for local channels. // NB: htlcMinimumMsat is set by our peer and may be 0 msat (even though it's not recommended). case GraphEdge(_, params, _, Some(balance)) => DirectChannel(balance, balance <= 0.msat || balance < params.htlcMinimum) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/BalanceEstimateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/BalanceEstimateSpec.scala index 4e14e981b6..d7e09fb902 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/BalanceEstimateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/BalanceEstimateSpec.scala @@ -277,16 +277,16 @@ class BalanceEstimateSpec extends AnyFunSuite { val edge_ab = makeEdge(a, b, 1, 10 sat) val edge_ba = makeEdge(b, a, 1, 10 sat) val edge_bc = makeEdge(b, c, 6, 10 sat) - assert(graphWithBalances.canSend(27500 msat, edge_ab) === 0.75 +- 0.01) - assert(graphWithBalances.canSend(55000 msat, edge_ab) === 0.5 +- 0.01) - assert(graphWithBalances.canSend(30000 msat, edge_ba) === 0.75 +- 0.01) - assert(graphWithBalances.canSend(60000 msat, edge_ba) === 0.5 +- 0.01) - assert(graphWithBalances.canSend(75000 msat, edge_bc) === 0.5 +- 0.01) - assert(graphWithBalances.canSend(100000 msat, edge_bc) === 0.33 +- 0.01) + assert(graphWithBalances.balances.get(edge_ab).canSend(27500 msat) === 0.75 +- 0.01) + assert(graphWithBalances.balances.get(edge_ab).canSend(55000 msat) === 0.5 +- 0.01) + assert(graphWithBalances.balances.get(edge_ba).canSend(30000 msat) === 0.75 +- 0.01) + assert(graphWithBalances.balances.get(edge_ba).canSend(60000 msat) === 0.5 +- 0.01) + assert(graphWithBalances.balances.get(edge_bc).canSend(75000 msat) === 0.5 +- 0.01) + assert(graphWithBalances.balances.get(edge_bc).canSend(100000 msat) === 0.33 +- 0.01) val unknownEdge = makeEdge(42, 40 sat) - assert(graphWithBalances.canSend(10000 msat, unknownEdge) === 0.75 +- 0.01) - assert(graphWithBalances.canSend(20000 msat, unknownEdge) === 0.5 +- 0.01) - assert(graphWithBalances.canSend(30000 msat, unknownEdge) === 0.25 +- 0.01) + assert(graphWithBalances.balances.get(unknownEdge).canSend(10000 msat) === 0.75 +- 0.01) + assert(graphWithBalances.balances.get(unknownEdge).canSend(20000 msat) === 0.5 +- 0.01) + assert(graphWithBalances.balances.get(unknownEdge).canSend(30000 msat) === 0.25 +- 0.01) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/GraphSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/GraphSpec.scala index c2e832905b..8fd53a2155 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/GraphSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/GraphSpec.scala @@ -27,6 +27,8 @@ import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong, ShortChannelId} import org.scalatest.funsuite.AnyFunSuite import scodec.bits._ +import scala.concurrent.duration.DurationInt + class GraphSpec extends AnyFunSuite { val (a, b, c, d, e, f, g, h) = ( @@ -168,8 +170,8 @@ class GraphSpec extends AnyFunSuite { val bIncoming = graph.getIncomingEdgesOf(b) assert(bIncoming.size == 1) - assert(bIncoming.exists(_.desc.a == a)) // there should be an edge a --> b - assert(bIncoming.exists(_.desc.b == b)) + assert(bIncoming(a).exists(_.desc.a == a)) // there should be an edge a --> b + assert(bIncoming(a).exists(_.desc.b == b)) val bOutgoing = graph.edgesOf(b) assert(bOutgoing.size == 1) @@ -221,18 +223,18 @@ class GraphSpec extends AnyFunSuite { val graph = DirectedGraph(Seq(edgeAB, edgeAD, edgeBC, edgeDC)) assert(graph.edgesOf(a).toSet == Set(edgeAB, edgeAD)) - assert(graph.getIncomingEdgesOf(a) == Nil) + assert(graph.getIncomingEdgesOf(a).isEmpty) assert(graph.edgesOf(c) == Nil) - assert(graph.getIncomingEdgesOf(c).toSet == Set(edgeBC, edgeDC)) + assert(graph.getIncomingEdgesOf(c).values.flatten.toSet == Set(edgeBC, edgeDC)) val edgeAB1 = edgeAB.copy(balance_opt = Some(200000 msat)) val edgeBC1 = edgeBC.copy(balance_opt = Some(150000 msat)) val graph1 = graph.addEdge(edgeAB1).addEdge(edgeBC1) assert(graph1.edgesOf(a).toSet == Set(edgeAB1, edgeAD)) - assert(graph1.getIncomingEdgesOf(a) == Nil) + assert(graph1.getIncomingEdgesOf(a).isEmpty) assert(graph1.edgesOf(c) == Nil) - assert(graph1.getIncomingEdgesOf(c).toSet == Set(edgeBC1, edgeDC)) + assert(graph1.getIncomingEdgesOf(c).values.flatten.toSet == Set(edgeBC1, edgeDC)) } def descFromNodes(shortChannelId: Long, a: PublicKey, b: PublicKey): ChannelDesc = makeEdge(shortChannelId, a, b, 0 msat, 0).desc @@ -256,9 +258,9 @@ class GraphSpec extends AnyFunSuite { val edgeDE = makeEdge(6L, d, e, 9 msat, 0, capacity = 200000 sat) val graph = DirectedGraph(Seq(edgeAB, edgeBC, edgeCD, edgeDC, edgeCE, edgeDE)) - val path :: Nil = yenKshortestPaths(graph, a, e, 100000000 msat, + val path :: Nil = yenKshortestPaths(GraphWithBalanceEstimates(graph, 1 day), a, e, 100000000 msat, Set.empty, Set.empty, Set.empty, 1, - Right(HeuristicsConstants(1.0E-8, RelayFees(2000 msat, 500), RelayFees(50 msat, 20), useLogProbability = true)), + Right(HeuristicsConstants(1.0E-8, RelayFees(2000 msat, 500), RelayFees(50 msat, 20), useLogProbability = true, usePastRelaysData = false)), BlockHeight(714930), _ => true, includeLocalChannelCost = true) assert(path.path == Seq(edgeAB, edgeBC, edgeCE)) } @@ -280,7 +282,7 @@ class GraphSpec extends AnyFunSuite { val edgeDE = makeEdge(6L, d, e, 1 msat, 0, capacity = 200000 sat) val graph = DirectedGraph(Seq(edgeAB, edgeBC, edgeCD, edgeDC, edgeCE, edgeDE)) - val paths = yenKshortestPaths(graph, a, e, 90000000 msat, + val paths = yenKshortestPaths(GraphWithBalanceEstimates(graph, 1 day), a, e, 90000000 msat, Set.empty, Set.empty, Set.empty, 2, Left(WeightRatios(1, 0, 0, 0, RelayFees(0 msat, 0))), BlockHeight(714930), _ => true, includeLocalChannelCost = true) @@ -306,7 +308,7 @@ class GraphSpec extends AnyFunSuite { val edgeDE = makeEdge(6L, d, e, 1 msat, 0, capacity = 200000 sat) val graph = DirectedGraph(Seq(edgeAB, edgeBC, edgeCD, edgeDC, edgeCE, edgeDE)) - val paths = yenKshortestPaths(graph, a, e, 90000000 msat, + val paths = yenKshortestPaths(GraphWithBalanceEstimates(graph, 1 day), a, e, 90000000 msat, Set.empty, Set.empty, Set.empty, 2, Left(WeightRatios(1, 0, 0, 0, RelayFees(0 msat, 0))), BlockHeight(714930), _ => true, includeLocalChannelCost = true) @@ -339,7 +341,7 @@ class GraphSpec extends AnyFunSuite { val edgeGH = makeEdge(9L, g, h, 2 msat, 0, capacity = 100000 sat, minHtlc = 1000 msat) val graph = DirectedGraph(Seq(edgeCD, edgeDF, edgeCE, edgeED, edgeEF, edgeFG, edgeFH, edgeEG, edgeGH)) - val paths = yenKshortestPaths(graph, c, h, 10000000 msat, + val paths = yenKshortestPaths(GraphWithBalanceEstimates(graph, 1 day), c, h, 10000000 msat, Set.empty, Set.empty, Set.empty, 3, Left(WeightRatios(1, 0, 0, 0, RelayFees(0 msat, 0))), BlockHeight(714930), _ => true, includeLocalChannelCost = true) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala index b57d1759f5..71aa2fc6bc 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala @@ -37,6 +37,7 @@ import scodec.bits._ import scala.collection.immutable.SortedMap import scala.collection.mutable +import scala.concurrent.duration.DurationInt import scala.util.{Failure, Random, Success} /** @@ -50,12 +51,12 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val (a, b, c, d, e, f) = (randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey) test("calculate simple route") { - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 1 msat, 10, cltvDelta = CltvExpiryDelta(1), balance_opt = Some(DEFAULT_AMOUNT_MSAT * 2)), makeEdge(2L, b, c, 1 msat, 10, cltvDelta = CltvExpiryDelta(1)), makeEdge(3L, c, d, 1 msat, 10, cltvDelta = CltvExpiryDelta(1)), makeEdge(4L, d, e, 1 msat, 10, cltvDelta = CltvExpiryDelta(1)) - )) + )), 1 day) val Success(route :: Nil) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route) == 1 :: 2 :: 3 :: 4 :: Nil) @@ -71,12 +72,12 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val routeParams = DEFAULT_ROUTE_PARAMS.modify(_.boundaries.maxFeeFlat).setTo(1 msat) val maxFee = routeParams.getMaxFee(DEFAULT_AMOUNT_MSAT) - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 10 msat, 10, cltvDelta = CltvExpiryDelta(1)), makeEdge(2L, b, c, 10 msat, 10, cltvDelta = CltvExpiryDelta(1)), makeEdge(3L, c, d, 10 msat, 10, cltvDelta = CltvExpiryDelta(1)), makeEdge(4L, d, e, 10 msat, 10, cltvDelta = CltvExpiryDelta(1)) - )) + )), 1 day) val Success(route :: Nil) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, maxFee, numRoutes = 1, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route) == 1 :: 2 :: 3 :: 4 :: Nil) @@ -112,17 +113,17 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val amount = 10000 msat val expectedCost = 10007 msat - val graph = DirectedGraph(List( + val graph = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, feeBase = 1 msat, feeProportionalMillionth = 200, minHtlc = 0 msat), makeEdge(4L, a, e, feeBase = 1 msat, feeProportionalMillionth = 200, minHtlc = 0 msat), makeEdge(2L, b, c, feeBase = 1 msat, feeProportionalMillionth = 300, minHtlc = 0 msat), makeEdge(3L, c, d, feeBase = 1 msat, feeProportionalMillionth = 400, minHtlc = 0 msat), makeEdge(5L, e, f, feeBase = 1 msat, feeProportionalMillionth = 400, minHtlc = 0 msat), makeEdge(6L, f, d, feeBase = 1 msat, feeProportionalMillionth = 100, minHtlc = 0 msat) - )) + )), 1 day) val Success(route :: Nil) = findRoute(graph, a, d, amount, maxFee = 7 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - val weightedPath = Graph.pathWeight(a, route2Edges(route), amount, BlockHeight(0), Left(NO_WEIGHT_RATIOS), includeLocalChannelCost = false) + val weightedPath = Graph.pathWeight(graph.balances, a, route2Edges(route), amount, BlockHeight(0), Left(NO_WEIGHT_RATIOS), includeLocalChannelCost = false) assert(route2Ids(route) == 4 :: 5 :: 6 :: Nil) assert(weightedPath.length == 3) assert(weightedPath.amount == expectedCost) @@ -138,25 +139,25 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { } test("calculate route considering the direct channel pays no fees") { - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 5 msat, 0), // a -> b makeEdge(2L, a, d, 15 msat, 0), // a -> d this goes a bit closer to the target and asks for higher fees but is a direct channel makeEdge(3L, b, c, 5 msat, 0), // b -> c makeEdge(4L, c, d, 5 msat, 0), // c -> d makeEdge(5L, d, e, 5 msat, 0) // d -> e - )) + )), 1 day) val Success(route :: Nil) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route) == 2 :: 5 :: Nil) } test("calculate simple route (add and remove edges") { - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 0 msat, 0), makeEdge(2L, b, c, 0 msat, 0), makeEdge(3L, c, d, 0 msat, 0), makeEdge(4L, d, e, 0 msat, 0) - )) + )), 1 day) val Success(route1 :: Nil) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route1) == 1 :: 2 :: 3 :: 4 :: Nil) @@ -174,12 +175,12 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { PublicKey(hex"029e059b6780f155f38e83601969919aae631ddf6faed58fe860c72225eb327d7c") // target ) - val graph = DirectedGraph(List( + val graph = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, f, g, 1 msat, 0), makeEdge(2L, g, h, 1 msat, 0), makeEdge(3L, h, i, 1 msat, 0), makeEdge(4L, f, h, 50 msat, 0) // more expensive but fee will be ignored since f is the payer - )) + )), 1 day) val Success(route :: Nil) = findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route) == 4 :: 3 :: Nil) @@ -193,12 +194,12 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { PublicKey(hex"029e059b6780f155f38e83601969919aae631ddf6faed58fe860c72225eb327d7c") // target ) - val graph = DirectedGraph(List( + val graph = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, f, g, 0 msat, 0), makeEdge(4L, f, i, 50 msat, 0), // our starting node F has a direct channel with I makeEdge(2L, g, h, 0 msat, 0), makeEdge(3L, h, i, 0 msat, 0) - )) + )), 1 day) val Success(route1 :: route2 :: Nil) = findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 2, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route1) == 4 :: Nil) @@ -213,12 +214,12 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { PublicKey(hex"029e059b6780f155f38e83601969919aae631ddf6faed58fe860c72225eb327d7c") // I target ) - val graph = DirectedGraph(List( + val graph = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, f, g, 1 msat, 0, balance_opt = Some(DEFAULT_AMOUNT_MSAT + 50.msat)), // the maximum htlc allowed by this channel is only 50 msat greater than what we're sending makeEdge(2L, g, h, 1 msat, 0, maxHtlc = Some(DEFAULT_AMOUNT_MSAT + 50.msat)), makeEdge(3L, h, i, 1 msat, 0) - )) + )), 1 day) val Success(route :: Nil) = findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route) == 1 :: 2 :: 3 :: Nil) @@ -232,12 +233,12 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { PublicKey(hex"029e059b6780f155f38e83601969919aae631ddf6faed58fe860c72225eb327d7c") // I target ) - val graph = DirectedGraph(List( + val graph = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, f, g, 1 msat, 0, balance_opt = Some(DEFAULT_AMOUNT_MSAT + 50.msat)), // this channel requires a minimum amount that is larger than what we are sending makeEdge(2L, g, h, 1 msat, 0, minHtlc = DEFAULT_AMOUNT_MSAT + 50.msat), makeEdge(3L, h, i, 1 msat, 0) - )) + )), 1 day) val route = findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route == Failure(RouteNotFound)) @@ -251,12 +252,12 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { PublicKey(hex"029e059b6780f155f38e83601969919aae631ddf6faed58fe860c72225eb327d7c") // I target ) - val graph = DirectedGraph(List( + val graph = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, f, g, 0 msat, 0), makeEdge(2L, g, h, 5 msat, 5), // expensive g -> h channel makeEdge(6L, g, h, 0 msat, 0), // cheap g -> h channel makeEdge(3L, h, i, 0 msat, 0) - )) + )), 1 day) val Success(route :: Nil) = findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route) == 1 :: 6 :: 3 :: Nil) @@ -270,56 +271,56 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { PublicKey(hex"029e059b6780f155f38e83601969919aae631ddf6faed58fe860c72225eb327d7c") // I target ) - val graph = DirectedGraph(List( + val graph = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, f, g, 0 msat, 0), makeEdge(2L, g, h, 5 msat, 5, balance_opt = Some(DEFAULT_AMOUNT_MSAT + 1.msat)), // expensive g -> h channel with enough balance makeEdge(6L, g, h, 0 msat, 0, balance_opt = Some(DEFAULT_AMOUNT_MSAT - 10.msat)), // cheap g -> h channel without enough balance makeEdge(3L, h, i, 0 msat, 0) - )) + )), 1 day) val Success(route :: Nil) = findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route) == 1 :: 2 :: 3 :: Nil) } test("calculate longer but cheaper route") { - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 0 msat, 0), makeEdge(2L, b, c, 0 msat, 0), makeEdge(3L, c, d, 0 msat, 0), makeEdge(4L, d, e, 0 msat, 0), makeEdge(5L, b, e, 10 msat, 10) - )) + )), 1 day) val Success(route :: Nil) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route) == 1 :: 2 :: 3 :: 4 :: Nil) } test("no local channels") { - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(2L, b, c, 0 msat, 0), makeEdge(4L, d, e, 0 msat, 0) - )) + )), 1 day) val route = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route == Failure(RouteNotFound)) } test("route not found") { - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 0 msat, 0), makeEdge(2L, b, c, 0 msat, 0), makeEdge(4L, d, e, 0 msat, 0) - )) + )), 1 day) val route = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route == Failure(RouteNotFound)) } test("route not found (source OR target node not connected)") { - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(2L, b, c, 0 msat, 0), makeEdge(4L, c, d, 0 msat, 0) - )).addVertex(a).addVertex(e) + )).addVertex(a).addVertex(e), 1 day) assert(findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) == Failure(RouteNotFound)) assert(findRoute(g, b, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) == Failure(RouteNotFound)) @@ -341,60 +342,60 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(3L, c, d, 0 msat, 0) ) - val g = DirectedGraph(edgesHi) - val g1 = DirectedGraph(edgesLo) + val g = GraphWithBalanceEstimates(DirectedGraph(edgesHi), 1 day) + val g1 = GraphWithBalanceEstimates(DirectedGraph(edgesLo), 1 day) assert(findRoute(g, a, d, highAmount, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) == Failure(RouteNotFound)) assert(findRoute(g1, a, d, lowAmount, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) == Failure(RouteNotFound)) } test("route not found (balance too low)") { - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 1 msat, 2, minHtlc = 10000 msat), makeEdge(2L, b, c, 1 msat, 2, minHtlc = 10000 msat), makeEdge(3L, c, d, 1 msat, 2, minHtlc = 10000 msat) - )) + )), 1 day) assert(findRoute(g, a, d, 15000 msat, 100 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)).isSuccess) // not enough balance on the last edge - val g1 = DirectedGraph(List( + val g1 = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 1 msat, 2, minHtlc = 10000 msat), makeEdge(2L, b, c, 1 msat, 2, minHtlc = 10000 msat), makeEdge(3L, c, d, 1 msat, 2, minHtlc = 10000 msat, balance_opt = Some(10000 msat)) - )) + )), 1 day) // not enough balance on intermediate edge (taking fee into account) - val g2 = DirectedGraph(List( + val g2 = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 1 msat, 2, minHtlc = 10000 msat), makeEdge(2L, b, c, 1 msat, 2, minHtlc = 10000 msat, balance_opt = Some(15000 msat)), makeEdge(3L, c, d, 1 msat, 2, minHtlc = 10000 msat) - )) + )), 1 day) // no enough balance on first edge (taking fee into account) - val g3 = DirectedGraph(List( + val g3 = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 1 msat, 2, minHtlc = 10000 msat, balance_opt = Some(15000 msat)), makeEdge(2L, b, c, 1 msat, 2, minHtlc = 10000 msat), makeEdge(3L, c, d, 1 msat, 2, minHtlc = 10000 msat) - )) + )), 1 day) Seq(g1, g2, g3).foreach(g => assert(findRoute(g, a, d, 15000 msat, 100 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) == Failure(RouteNotFound))) } test("route to self") { - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 0 msat, 0), makeEdge(2L, b, c, 0 msat, 0), makeEdge(3L, c, d, 0 msat, 0) - )) + )), 1 day) val route = findRoute(g, a, a, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route == Failure(CannotRouteToSelf)) } test("route to immediate neighbor") { - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 0 msat, 0, balance_opt = Some(DEFAULT_AMOUNT_MSAT)), makeEdge(2L, b, c, 0 msat, 0), makeEdge(3L, c, d, 0 msat, 0), makeEdge(4L, d, e, 0 msat, 0) - )) + )), 1 day) val Success(route :: Nil) = findRoute(g, a, b, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route) == 1 :: Nil) @@ -402,12 +403,12 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { test("directed graph") { // a->e works, e->a fails - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 0 msat, 0), makeEdge(2L, b, c, 0 msat, 0), makeEdge(3L, c, d, 0 msat, 0), makeEdge(4L, d, e, 0 msat, 0) - )) + )), 1 day) val Success(route1 :: Nil) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route1) == 1 :: 2 :: 3 :: 4 :: Nil) @@ -439,7 +440,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { GraphEdge(ChannelDesc(ShortChannelId(4L), e, d), ChannelRelayParams.FromAnnouncement(ued), DEFAULT_CAPACITY, None) ) - val g = DirectedGraph(edges) + val g = GraphWithBalanceEstimates(DirectedGraph(edges), 1 day) val Success(route :: Nil) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route.hops == channelHopFromUpdate(a, b, uab) :: channelHopFromUpdate(b, c, ubc) :: channelHopFromUpdate(c, d, ucd) :: channelHopFromUpdate(d, e, ude) :: Nil) } @@ -467,20 +468,20 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { } test("blacklist routes") { - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 0 msat, 0), makeEdge(2L, b, c, 0 msat, 0), makeEdge(3L, c, d, 0 msat, 0), makeEdge(4L, d, e, 0 msat, 0) - )) + )), 1 day) val route1 = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, ignoredEdges = Set(ChannelDesc(ShortChannelId(3L), c, d)), routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route1 == Failure(RouteNotFound)) // verify that we left the graph untouched - assert(g.containsEdge(ChannelDesc(ShortChannelId(3), c, d))) - assert(g.containsVertex(c)) - assert(g.containsVertex(d)) + assert(g.graph.containsEdge(ChannelDesc(ShortChannelId(3), c, d))) + assert(g.graph.containsVertex(c)) + assert(g.graph.containsVertex(d)) // make sure we can find a route if without the blacklist val Success(route2 :: Nil) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) @@ -488,11 +489,11 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { } test("route to a destination that is not in the graph (with assisted routes)") { - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 10 msat, 10), makeEdge(2L, b, c, 10 msat, 10), makeEdge(3L, c, d, 10 msat, 10) - )) + )), 1 day) val route = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route == Failure(RouteNotFound)) @@ -504,10 +505,10 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { } test("route from a source that is not in the graph (with assisted routes)") { - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(2L, b, c, 10 msat, 10), makeEdge(3L, c, d, 10 msat, 10) - )) + )), 1 day) val route = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route == Failure(RouteNotFound)) @@ -519,12 +520,12 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { } test("verify that extra hops takes precedence over known channels") { - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 10 msat, 10), makeEdge(2L, b, c, 10 msat, 10), makeEdge(3L, c, d, 10 msat, 10), makeEdge(4L, d, e, 10 msat, 10) - )) + )), 1 day) val Success(route1 :: Nil) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route1) == 1 :: 2 :: 3 :: 4 :: Nil) @@ -587,7 +588,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { .zipWithIndex // ((0, 1), 0) :: ((1, 2), 1) :: ... .map { case ((na, nb), index) => makeEdge(index, na, nb, 5 msat, 0) } - val g = DirectedGraph(edges) + val g = GraphWithBalanceEstimates(DirectedGraph(edges), 1 day) assert(findRoute(g, nodes(0), nodes(18), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)).map(r => route2Ids(r.head)) == Success(0 until 18)) assert(findRoute(g, nodes(0), nodes(19), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)).map(r => route2Ids(r.head)) == Success(0 until 19)) @@ -605,7 +606,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val expensiveShortEdge = makeEdge(99, nodes(2), nodes(48), 1000 msat, 0) // expensive shorter route - val g = DirectedGraph(expensiveShortEdge :: edges) + val g = GraphWithBalanceEstimates(DirectedGraph(expensiveShortEdge :: edges), 1 day) val Success(route :: Nil) = findRoute(g, nodes(0), nodes(49), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route) == 0 :: 1 :: 99 :: 48 :: Nil) @@ -613,14 +614,14 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { test("ignore cheaper route when it has more than the requested CLTV") { val f = randomKey().publicKey - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1, a, b, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, CltvExpiryDelta(50)), makeEdge(2, b, c, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, CltvExpiryDelta(50)), makeEdge(3, c, d, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, CltvExpiryDelta(50)), makeEdge(4, a, e, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, CltvExpiryDelta(9)), makeEdge(5, e, f, feeBase = 5 msat, 0, minHtlc = 0 msat, maxHtlc = None, CltvExpiryDelta(9)), makeEdge(6, f, d, feeBase = 5 msat, 0, minHtlc = 0 msat, maxHtlc = None, CltvExpiryDelta(9)) - )) + )), 1 day) val Success(route :: Nil) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.modify(_.boundaries.maxCltv).setTo(CltvExpiryDelta(28)), currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route) == 4 :: 5 :: 6 :: Nil) @@ -628,27 +629,27 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { test("ignore cheaper route when it grows longer than the requested size") { val f = randomKey().publicKey - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1, a, b, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, CltvExpiryDelta(9)), makeEdge(2, b, c, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, CltvExpiryDelta(9)), makeEdge(3, c, d, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, CltvExpiryDelta(9)), makeEdge(4, d, e, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, CltvExpiryDelta(9)), makeEdge(5, e, f, feeBase = 5 msat, 0, minHtlc = 0 msat, maxHtlc = None, CltvExpiryDelta(9)), makeEdge(6, b, f, feeBase = 5 msat, 0, minHtlc = 0 msat, maxHtlc = None, CltvExpiryDelta(9)) - )) + )), 1 day) val Success(route :: Nil) = findRoute(g, a, f, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.modify(_.boundaries.maxRouteLength).setTo(3), currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route) == 1 :: 6 :: Nil) } test("ignore loops") { - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 10 msat, 10), makeEdge(2L, b, c, 10 msat, 10), makeEdge(3L, c, a, 10 msat, 10), makeEdge(4L, c, d, 10 msat, 10), makeEdge(5L, d, e, 10 msat, 10) - )) + )), 1 day) val Success(route :: Nil) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route) == 1 :: 2 :: 4 :: 5 :: Nil) @@ -656,7 +657,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { test("ensure the route calculation terminates correctly when selecting 0-fees edges") { // the graph contains a possible 0-cost path that goes back on its steps ( e -> f, f -> e ) - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 10 msat, 10), // a -> b makeEdge(2L, b, c, 10 msat, 10), makeEdge(4L, c, d, 10 msat, 10), @@ -664,7 +665,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(6L, e, f, 0 msat, 0), // e -> f makeEdge(6L, f, e, 0 msat, 0), // e <- f makeEdge(5L, e, d, 0 msat, 0) // e -> d - )) + )), 1 day) val Success(route :: Nil) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route) == 1 :: 3 :: 5 :: Nil) @@ -689,7 +690,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { PublicKey(hex"03fc5b91ce2d857f146fd9b986363374ffe04dc143d8bcd6d7664c8873c463cdfc") ) - val g1 = DirectedGraph(Seq( + val g1 = GraphWithBalanceEstimates(DirectedGraph(Seq( makeEdge(1L, d, a, 1 msat, 0, balance_opt = Some(DEFAULT_AMOUNT_MSAT + 4.msat)), makeEdge(2L, d, e, 1 msat, 0, balance_opt = Some(DEFAULT_AMOUNT_MSAT + 3.msat)), makeEdge(3L, a, e, 1 msat, 0, balance_opt = Some(DEFAULT_AMOUNT_MSAT + 3.msat)), @@ -697,7 +698,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(5L, e, f, 1 msat, 0, balance_opt = Some(DEFAULT_AMOUNT_MSAT)), makeEdge(6L, b, c, 1 msat, 0, balance_opt = Some(DEFAULT_AMOUNT_MSAT + 1.msat)), makeEdge(7L, c, f, 1 msat, 0, balance_opt = Some(DEFAULT_AMOUNT_MSAT)) - )) + )), 1 day) val fourShortestPaths = Graph.yenKshortestPaths(g1, d, f, DEFAULT_AMOUNT_MSAT, Set.empty, Set.empty, Set.empty, pathsToFind = 4, Left(NO_WEIGHT_RATIOS), BlockHeight(0), noopBoundaries, includeLocalChannelCost = false) assert(fourShortestPaths.size == 4) @@ -725,7 +726,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { PublicKey(hex"03fc5b91ce2d857f146fd9b986363374ffe04dc143d8bcd6d7664c8873c463cdfc") ) - val graph = DirectedGraph(Seq( + val graph = GraphWithBalanceEstimates(DirectedGraph(Seq( makeEdge(10L, c, e, 2 msat, 0), makeEdge(20L, c, d, 3 msat, 0), makeEdge(30L, d, f, 4 msat, 5), // D- > F has a higher cost to distinguish it from the 2nd cheapest route @@ -735,7 +736,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(70L, f, g, 2 msat, 0), makeEdge(80L, f, h, 1 msat, 0), makeEdge(90L, g, h, 2 msat, 0) - )) + )), 1 day) val twoShortestPaths = Graph.yenKshortestPaths(graph, c, h, DEFAULT_AMOUNT_MSAT, Set.empty, Set.empty, Set.empty, pathsToFind = 2, Left(NO_WEIGHT_RATIOS), BlockHeight(0), noopBoundaries, includeLocalChannelCost = false) @@ -751,7 +752,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val f = randomKey().publicKey // simple graph with only 2 possible paths from A to F - val graph = DirectedGraph(Seq( + val graph = GraphWithBalanceEstimates(DirectedGraph(Seq( makeEdge(1L, a, b, 1 msat, 0), makeEdge(1L, b, a, 1 msat, 0), makeEdge(2L, b, c, 1 msat, 0), @@ -765,7 +766,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(5L, e, d, 1 msat, 0), makeEdge(6L, e, f, 1 msat, 0), makeEdge(6L, f, e, 1 msat, 0) - )) + )), 1 day) // 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, Set.empty, pathsToFind = 3, Left(NO_WEIGHT_RATIOS), BlockHeight(0), noopBoundaries, includeLocalChannelCost = false) @@ -785,7 +786,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { // A -> B -> C -> D has total cost of 10000005 // A -> E -> C -> D has total cost of 10000103 !! // A -> E -> F -> D has total cost of 10000006 - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, feeBase = 1 msat, 0), makeEdge(2L, b, c, feeBase = 2 msat, 0), makeEdge(3L, c, d, feeBase = 3 msat, 0), @@ -793,12 +794,12 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(5L, e, f, feeBase = 3 msat, 0), makeEdge(6L, f, d, feeBase = 3 msat, 0), makeEdge(7L, e, c, feeBase = 100 msat, 0) - )) + )), 1 day) for (_ <- 0 to 10) { val Success(routes) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, strictFee, numRoutes = 3, routeParams = strictFeeParams, currentBlockHeight = BlockHeight(400000)) assert(routes.length == 2, routes) - val weightedPath = Graph.pathWeight(a, route2Edges(routes.head), DEFAULT_AMOUNT_MSAT, BlockHeight(400000), Left(NO_WEIGHT_RATIOS), includeLocalChannelCost = false) + val weightedPath = Graph.pathWeight(g.balances, a, route2Edges(routes.head), DEFAULT_AMOUNT_MSAT, BlockHeight(400000), Left(NO_WEIGHT_RATIOS), includeLocalChannelCost = false) val totalFees = weightedPath.amount - DEFAULT_AMOUNT_MSAT // over the three routes we could only get the 2 cheapest because the third is too expensive (over 7 msat of fees) assert(totalFees == 5.msat || totalFees == 6.msat) @@ -813,7 +814,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { // A -> B -> C -> D is 'fee optimized', lower fees route (totFees = 2, totCltv = 4000) // A -> E -> F -> D is 'timeout optimized', lower CLTV route (totFees = 3, totCltv = 18) // A -> E -> C -> D is 'capacity optimized', more recent channel/larger capacity route - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, feeBase = 0 msat, 1000, minHtlc = 0 msat, capacity = defaultCapacity, cltvDelta = CltvExpiryDelta(13)), makeEdge(4L, a, e, feeBase = 0 msat, 1000, minHtlc = 0 msat, capacity = defaultCapacity, cltvDelta = CltvExpiryDelta(12)), makeEdge(2L, b, c, feeBase = 1 msat, 1000, minHtlc = 0 msat, capacity = defaultCapacity, cltvDelta = CltvExpiryDelta(500)), @@ -821,7 +822,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(5L, e, f, feeBase = 2 msat, 1000, minHtlc = 0 msat, capacity = defaultCapacity, cltvDelta = CltvExpiryDelta(9)), makeEdge(6L, f, d, feeBase = 2 msat, 1000, minHtlc = 0 msat, capacity = defaultCapacity, cltvDelta = CltvExpiryDelta(9)), makeEdge(7L, e, c, feeBase = 2 msat, 1000, minHtlc = 0 msat, capacity = largeCapacity, cltvDelta = CltvExpiryDelta(12)) - )) + )), 1 day) val Success(routeFeeOptimized :: Nil) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route2Nodes(routeFeeOptimized) == (a, b) :: (b, c) :: (c, d) :: Nil) @@ -848,14 +849,14 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { test("prefer going through an older channel if fees and CLTV are the same") { val currentBlockHeight = BlockHeight(554000) - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(ShortChannelId.fromCoordinates(s"${currentBlockHeight.toLong}x0x1").success.value.toLong, a, b, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(144)), makeEdge(ShortChannelId.fromCoordinates(s"${currentBlockHeight.toLong}x0x4").success.value.toLong, a, e, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(144)), makeEdge(ShortChannelId.fromCoordinates(s"${currentBlockHeight.toLong - 3000}x0x2").success.value.toLong, b, c, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(144)), // younger channel makeEdge(ShortChannelId.fromCoordinates(s"${currentBlockHeight.toLong - 3000}x0x3").success.value.toLong, c, d, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(144)), makeEdge(ShortChannelId.fromCoordinates(s"${currentBlockHeight.toLong}x0x5").success.value.toLong, e, f, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(144)), makeEdge(ShortChannelId.fromCoordinates(s"${currentBlockHeight.toLong}x0x6").success.value.toLong, f, d, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(144)) - )) + )), 1 day) val Success(routeScoreOptimized :: Nil) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT / 2, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Left(WeightRatios( baseFactor = 0.01, @@ -869,14 +870,14 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { } test("prefer a route with a smaller total CLTV if fees and score are the same") { - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1, a, b, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(12)), makeEdge(4, a, e, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(12)), makeEdge(2, b, c, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(10)), // smaller CLTV makeEdge(3, c, d, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(12)), makeEdge(5, e, f, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(12)), makeEdge(6, f, d, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(12)) - )) + )), 1 day) val Success(routeScoreOptimized :: Nil) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Left(WeightRatios( baseFactor = 0.01, @@ -892,14 +893,14 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { test("avoid a route that breaks off the max CLTV") { // A -> B -> C -> D is cheaper but has a total CLTV > 2016! // A -> E -> F -> D is more expensive but has a total CLTV < 2016 - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1, a, b, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(144)), makeEdge(4, a, e, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(144)), makeEdge(2, b, c, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(1000)), makeEdge(3, c, d, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(900)), makeEdge(5, e, f, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(144)), makeEdge(6, f, d, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(144)) - )) + )), 1 day) val Success(routeScoreOptimized :: Nil) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT / 2, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Left(WeightRatios( baseFactor = 0.01, @@ -942,7 +943,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { ) ) - val g = DirectedGraph.makeGraph(updates) + val g = GraphWithBalanceEstimates(DirectedGraph.makeGraph(updates), 1 day) val params = DEFAULT_ROUTE_PARAMS .modify(_.boundaries.maxCltv).setTo(CltvExpiryDelta(1008)) .modify(_.heuristics).setTo(Left(WeightRatios(baseFactor = 0, cltvDeltaFactor = 0.15, ageFactor = 0.35, capacityFactor = 0.5, hopCost = RelayFees(0 msat, 0)))) @@ -977,12 +978,12 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { test("calculate multipart route to neighbor (many channels, known balance)") { val amount = 60000 msat - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 50 msat, 100, minHtlc = 1 msat, balance_opt = Some(15000 msat)), makeEdge(2L, a, b, 15 msat, 10, minHtlc = 1 msat, balance_opt = Some(21000 msat)), makeEdge(3L, a, b, 1 msat, 50, minHtlc = 1 msat, balance_opt = Some(17000 msat)), makeEdge(4L, a, b, 100 msat, 20, minHtlc = 1 msat, balance_opt = Some(16000 msat)), - )) + )), 1 day) // We set max-parts to 3, but it should be ignored when sending to a direct neighbor. val routeParams = DEFAULT_ROUTE_PARAMS.copy(mpp = MultiPartParams(2500 msat, 3)) @@ -1006,12 +1007,12 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { } test("calculate multipart route to neighbor (single channel, known balance)") { - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 50 msat, 100, minHtlc = 1 msat, balance_opt = Some(25000 msat)), makeEdge(2L, a, c, 1 msat, 0, minHtlc = 1 msat, balance_opt = Some(50000 msat)), makeEdge(3L, c, b, 1 msat, 0, minHtlc = 1 msat), makeEdge(4L, a, d, 1 msat, 0, minHtlc = 1 msat, balance_opt = Some(45000 msat)), - )) + )), 1 day) val amount = 25000 msat val Success(routes) = findMultiPartRoute(g, a, b, amount, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) @@ -1021,13 +1022,13 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { } test("calculate multipart route to neighbor (many channels, some balance unknown)") { - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 50 msat, 100, minHtlc = 1 msat, balance_opt = Some(15000 msat)), makeEdge(2L, a, b, 15 msat, 10, minHtlc = 1 msat, balance_opt = Some(25000 msat)), makeEdge(3L, a, b, 1 msat, 50, minHtlc = 1 msat, balance_opt = None, capacity = 20 sat), makeEdge(4L, a, b, 100 msat, 20, minHtlc = 1 msat, balance_opt = Some(10000 msat)), makeEdge(5L, a, d, 1 msat, 0, minHtlc = 1 msat, balance_opt = Some(45000 msat)), - )) + )), 1 day) val amount = 65000 msat val Success(routes) = findMultiPartRoute(g, a, b, amount, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) @@ -1038,7 +1039,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { test("calculate multipart route to neighbor (many channels, some empty)") { val amount = 35000 msat - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 50 msat, 100, minHtlc = 1 msat, balance_opt = Some(15000 msat)), makeEdge(2L, a, b, 15 msat, 10, minHtlc = 1 msat, balance_opt = Some(0 msat)), makeEdge(3L, a, b, 1 msat, 50, minHtlc = 1 msat, balance_opt = None, capacity = 15 sat), @@ -1046,7 +1047,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(5L, a, b, 100 msat, 20, minHtlc = 1 msat, balance_opt = Some(10000 msat)), makeEdge(6L, a, d, 1 msat, 0, minHtlc = 1 msat, balance_opt = Some(45000 msat)), makeEdge(7L, a, d, 0 msat, 0, minHtlc = 0 msat, balance_opt = Some(0 msat)), - )) + )), 1 day) { val Success(routes) = findMultiPartRoute(g, a, b, amount, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) @@ -1065,14 +1066,14 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { } test("calculate multipart route to neighbor (ignored channels)") { - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 50 msat, 100, minHtlc = 1 msat, balance_opt = Some(15000 msat)), makeEdge(2L, a, b, 15 msat, 10, minHtlc = 1 msat, balance_opt = Some(25000 msat)), makeEdge(3L, a, b, 1 msat, 50, minHtlc = 1 msat, balance_opt = None, capacity = 50 sat), makeEdge(4L, a, b, 100 msat, 20, minHtlc = 1 msat, balance_opt = Some(10000 msat)), makeEdge(5L, a, b, 1 msat, 10, minHtlc = 1 msat, balance_opt = None, capacity = 10 sat), makeEdge(6L, a, d, 1 msat, 0, minHtlc = 1 msat, balance_opt = Some(45000 msat)), - )) + )), 1 day) val amount = 20000 msat val ignoredEdges = Set(ChannelDesc(ShortChannelId(2L), a, b), ChannelDesc(ShortChannelId(3L), a, b)) @@ -1086,12 +1087,12 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val edge_ab_1 = makeEdge(1L, a, b, 50 msat, 100, minHtlc = 1 msat, balance_opt = Some(15000 msat)) val edge_ab_2 = makeEdge(2L, a, b, 15 msat, 10, minHtlc = 1 msat, balance_opt = Some(25000 msat)) val edge_ab_3 = makeEdge(3L, a, b, 1 msat, 50, minHtlc = 1 msat, balance_opt = None, capacity = 15 sat) - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( edge_ab_1, edge_ab_2, edge_ab_3, makeEdge(4L, a, d, 1 msat, 0, minHtlc = 1 msat, balance_opt = Some(45000 msat)), - )) + )), 1 day) val amount = 50000 msat // These pending HTLCs will have already been taken into account in the edge's `balance_opt` field: findMultiPartRoute @@ -1103,12 +1104,12 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { } test("calculate multipart route to neighbor (restricted htlc_maximum_msat)") { - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 25 msat, 15, minHtlc = 1 msat, maxHtlc = Some(5000 msat), balance_opt = Some(18000 msat)), makeEdge(2L, a, b, 15 msat, 10, minHtlc = 1 msat, maxHtlc = Some(5000 msat), balance_opt = Some(23000 msat)), makeEdge(3L, a, b, 1 msat, 50, minHtlc = 1 msat, maxHtlc = Some(5000 msat), balance_opt = Some(21000 msat)), makeEdge(4L, a, d, 1 msat, 0, minHtlc = 1 msat, balance_opt = Some(45000 msat)), - )) + )), 1 day) val amount = 50000 msat val Success(routes) = findMultiPartRoute(g, a, b, amount, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) @@ -1119,12 +1120,12 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { } test("calculate multipart route to neighbor (restricted htlc_minimum_msat)") { - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 25 msat, 15, minHtlc = 2500 msat, balance_opt = Some(18000 msat)), makeEdge(2L, a, b, 15 msat, 10, minHtlc = 2500 msat, balance_opt = Some(7000 msat)), makeEdge(3L, a, b, 1 msat, 50, minHtlc = 2500 msat, balance_opt = Some(10000 msat)), makeEdge(4L, a, d, 1 msat, 0, minHtlc = 1 msat, balance_opt = Some(45000 msat)), - )) + )), 1 day) val amount = 30000 msat val routeParams = DEFAULT_ROUTE_PARAMS.copy(mpp = MultiPartParams(2500 msat, 5)) @@ -1135,13 +1136,13 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { } test("calculate multipart route to neighbor (through remote channels)") { - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 25 msat, 15, minHtlc = 1000 msat, balance_opt = Some(18000 msat)), makeEdge(2L, a, b, 15 msat, 10, minHtlc = 1000 msat, balance_opt = Some(7000 msat)), makeEdge(3L, a, c, 1000 msat, 10000, minHtlc = 1000 msat, balance_opt = Some(10000 msat)), makeEdge(4L, c, b, 10 msat, 1000, minHtlc = 1000 msat), makeEdge(5L, a, d, 1 msat, 0, minHtlc = 1 msat, balance_opt = Some(25000 msat)), - )) + )), 1 day) val amount = 30000 msat val maxFeeTooLow = findMultiPartRoute(g, a, b, amount, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) @@ -1154,12 +1155,12 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { } test("cannot find multipart route to neighbor (not enough balance)") { - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 0 msat, 0, minHtlc = 1 msat, balance_opt = Some(15000 msat)), makeEdge(2L, a, b, 0 msat, 0, minHtlc = 1 msat, balance_opt = Some(5000 msat)), makeEdge(3L, a, b, 0 msat, 0, minHtlc = 1 msat, balance_opt = Some(10000 msat)), makeEdge(4L, a, d, 0 msat, 0, minHtlc = 1 msat, balance_opt = Some(45000 msat)), - )) + )), 1 day) { val result = findMultiPartRoute(g, a, b, 40000 msat, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) @@ -1172,23 +1173,23 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { } test("cannot find multipart route to neighbor (not enough capacity)") { - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 0 msat, 0, minHtlc = 1 msat, capacity = 1500 sat), makeEdge(2L, a, b, 0 msat, 0, minHtlc = 1 msat, capacity = 2000 sat), makeEdge(3L, a, b, 0 msat, 0, minHtlc = 1 msat, capacity = 1200 sat), makeEdge(4L, a, d, 0 msat, 0, minHtlc = 1 msat, capacity = 4500 sat), - )) + )), 1 day) val result = findMultiPartRoute(g, a, b, 5000000 msat, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(result == Failure(RouteNotFound)) } test("cannot find multipart route to neighbor (restricted htlc_minimum_msat)") { - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 25 msat, 15, minHtlc = 5000 msat, balance_opt = Some(6000 msat)), makeEdge(2L, a, b, 15 msat, 10, minHtlc = 5000 msat, balance_opt = Some(7000 msat)), makeEdge(3L, a, d, 0 msat, 0, minHtlc = 5000 msat, balance_opt = Some(9000 msat)), - )) + )), 1 day) { val result = findMultiPartRoute(g, a, b, 10000 msat, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) @@ -1208,14 +1209,14 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { // +--- B --- D ---+ val (amount, maxFee) = (30000 msat, 150 msat) val edge_ab = makeEdge(1L, a, b, 50 msat, 100, minHtlc = 1 msat, balance_opt = Some(15000 msat)) - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( edge_ab, makeEdge(2L, b, d, 15 msat, 0, minHtlc = 1 msat, capacity = 25 sat), makeEdge(3L, d, e, 15 msat, 0, minHtlc = 0 msat, capacity = 20 sat), makeEdge(4L, a, c, 1 msat, 50, minHtlc = 1 msat, balance_opt = Some(10000 msat)), makeEdge(5L, a, c, 1 msat, 50, minHtlc = 1 msat, balance_opt = Some(8000 msat)), makeEdge(6L, c, e, 50 msat, 30, minHtlc = 1 msat, capacity = 20 sat), - )) + )), 1 day) { val Success(routes) = findMultiPartRoute(g, a, e, amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) @@ -1260,13 +1261,13 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { // +--- B --- D ---+ // Our balance and the amount we want to send are below the minimum part amount. val routeParams = DEFAULT_ROUTE_PARAMS.copy(mpp = MultiPartParams(5000 msat, 5)) - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 50 msat, 100, minHtlc = 1 msat, balance_opt = Some(1500 msat)), makeEdge(2L, b, d, 15 msat, 0, minHtlc = 1 msat, capacity = 25 sat), makeEdge(3L, d, e, 15 msat, 0, minHtlc = 1 msat, capacity = 20 sat), makeEdge(4L, a, c, 1 msat, 50, minHtlc = 1 msat, balance_opt = Some(1000 msat)), makeEdge(5L, c, e, 50 msat, 30, minHtlc = 1 msat, capacity = 20 sat), - )) + )), 1 day) { // We can send single-part tiny payments. @@ -1284,11 +1285,11 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { test("calculate multipart route to remote node (single path)") { val (amount, maxFee) = (100000 msat, 500 msat) - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 50 msat, 100, minHtlc = 1 msat, balance_opt = Some(500000 msat)), makeEdge(2L, b, c, 10 msat, 30, minHtlc = 1 msat, capacity = 150 sat), makeEdge(3L, c, d, 15 msat, 50, minHtlc = 1 msat, capacity = 150 sat), - )) + )), 1 day) val Success(routes) = findMultiPartRoute(g, a, d, amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) checkRouteAmounts(routes, amount, maxFee) @@ -1304,7 +1305,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { // +----- E -------+ val (amount, maxFee) = (400000 msat, 250 msat) val edge_ab = makeEdge(1L, a, b, 50 msat, 100, minHtlc = 1 msat, balance_opt = Some(500000 msat)) - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( edge_ab, makeEdge(2L, b, c, 10 msat, 30, minHtlc = 1 msat, capacity = 150 sat), makeEdge(3L, c, d, 15 msat, 50, minHtlc = 1 msat, capacity = 150 sat), @@ -1312,7 +1313,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(5L, d, f, 5 msat, 50, minHtlc = 1 msat, capacity = 300 sat), makeEdge(6L, b, e, 15 msat, 80, minHtlc = 1 msat, capacity = 210 sat), makeEdge(7L, e, f, 15 msat, 100, minHtlc = 1 msat, capacity = 200 sat), - )) + )), 1 day) { val Success(routes) = findMultiPartRoute(g, a, f, amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) @@ -1374,7 +1375,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(100, a, c, 5 msat, 1000, minHtlc = 1 msat, capacity = 25000 sat, balance_opt = Some(20_000_000 msat)), makeEdge(101, c, d, 5 msat, 1000, minHtlc = 1 msat, capacity = 25000 sat), ) - val g = DirectedGraph(preferredEdges ++ cheapEdges) + val g = GraphWithBalanceEstimates(DirectedGraph(preferredEdges ++ cheapEdges), 1 day) { val amount = 15_000_000 msat @@ -1416,7 +1417,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { // | +-------------------+ | // +---------- E ----------+ val (amount, maxFee) = (25000 msat, 5 msat) - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 1 msat, 0, minHtlc = 1 msat, balance_opt = Some(75000 msat)), makeEdge(2L, b, c, 1 msat, 0, minHtlc = 1 msat, capacity = 150 sat), makeEdge(3L, c, f, 1 msat, 0, minHtlc = 1 msat, capacity = 150 sat), @@ -1427,7 +1428,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(8L, a, f, 1 msat, 0, minHtlc = 1 msat, balance_opt = Some(10000 msat)), makeEdge(9L, a, e, 1 msat, 0, minHtlc = 1 msat, balance_opt = Some(18000 msat)), makeEdge(10L, e, f, 1 msat, 0, minHtlc = 1 msat, capacity = 15 sat), - )) + )), 1 day) val ignoredNodes = Set(d) val ignoredChannels = Set(ChannelDesc(ShortChannelId(2L), b, c)) @@ -1443,7 +1444,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { // | | // +----- D -----+ val (amount, maxFee) = (15000 msat, 5 msat) - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( // The A -> B -> E path is impossible because the A -> B balance is lower than the B -> E htlc_minimum_msat. makeEdge(1L, a, b, 1 msat, 0, minHtlc = 500 msat, balance_opt = Some(7000 msat)), makeEdge(2L, b, e, 1 msat, 0, minHtlc = 10000 msat, capacity = 50 sat), @@ -1451,7 +1452,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(4L, c, e, 1 msat, 0, minHtlc = 500 msat, maxHtlc = Some(4000 msat), capacity = 50 sat), makeEdge(5L, a, d, 1 msat, 0, minHtlc = 500 msat, balance_opt = Some(10000 msat)), makeEdge(6L, d, e, 1 msat, 0, minHtlc = 500 msat, maxHtlc = Some(4000 msat), capacity = 50 sat), - )) + )), 1 day) val Success(routes) = findMultiPartRoute(g, a, e, amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) checkRouteAmounts(routes, amount, maxFee) @@ -1475,7 +1476,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { // +---+ | | +---+ | // | D |-----+ +--->| F |<-----+ // +---+ +---+ - val g = DirectedGraph(Seq( + val g = GraphWithBalanceEstimates(DirectedGraph(Seq( makeEdge(1L, d, a, 100 msat, 1000, minHtlc = 1000 msat, balance_opt = Some(80000 msat)), makeEdge(2L, d, e, 100 msat, 1000, minHtlc = 1500 msat, balance_opt = Some(20000 msat)), makeEdge(3L, a, e, 5 msat, 50, minHtlc = 1200 msat, capacity = 100 sat), @@ -1483,7 +1484,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(5L, e, b, 10 msat, 100, minHtlc = 1100 msat, capacity = 75 sat), makeEdge(6L, b, c, 5 msat, 50, minHtlc = 1000 msat, capacity = 20 sat), makeEdge(7L, c, f, 5 msat, 10, minHtlc = 1500 msat, capacity = 50 sat) - )) + )), 1 day) val routeParams = DEFAULT_ROUTE_PARAMS.copy(mpp = MultiPartParams(1500 msat, 10)) { @@ -1523,12 +1524,12 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { // A D (---) E (---) F // +--- C ---+ val (amount, maxFeeE, maxFeeF) = (10000 msat, 50 msat, 100 msat) - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 1 msat, 0, minHtlc = 1 msat, maxHtlc = Some(4000 msat), balance_opt = Some(7000 msat)), makeEdge(2L, b, d, 1 msat, 0, minHtlc = 1 msat, capacity = 50 sat), makeEdge(3L, a, c, 1 msat, 0, minHtlc = 1 msat, maxHtlc = Some(4000 msat), balance_opt = Some(6000 msat)), makeEdge(4L, c, d, 1 msat, 0, minHtlc = 1 msat, capacity = 40 sat), - )) + )), 1 day) val extraEdges = Set( makeEdge(10L, d, e, 10 msat, 100, minHtlc = 500 msat, capacity = 15 sat), makeEdge(11L, e, f, 5 msat, 100, minHtlc = 500 msat, capacity = 10 sat), @@ -1558,7 +1559,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val (amount, maxFee) = (15000 msat, 100 msat) val edge_ab = makeEdge(1L, a, b, 1 msat, 0, minHtlc = 100 msat, balance_opt = Some(5000 msat)) val edge_be = makeEdge(2L, b, e, 1 msat, 0, minHtlc = 100 msat, capacity = 5 sat) - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( // The A -> B -> E route is the most economic one, but we already have a pending HTLC in it. edge_ab, edge_be, @@ -1566,7 +1567,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(4L, c, e, 50 msat, 0, minHtlc = 100 msat, capacity = 25 sat), makeEdge(5L, a, d, 50 msat, 0, minHtlc = 100 msat, balance_opt = Some(10000 msat)), makeEdge(6L, d, e, 50 msat, 0, minHtlc = 100 msat, capacity = 25 sat), - )) + )), 1 day) val pendingHtlcs = Seq(Route(5000 msat, graphEdgeToHop(edge_ab) :: graphEdgeToHop(edge_be) :: Nil)) val Success(routes) = findMultiPartRoute(g, a, e, amount, maxFee, pendingHtlcs = pendingHtlcs, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) @@ -1595,7 +1596,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { for (_ <- 1 to 100) { val amount = (100 + Random.nextLong(200000)).msat val maxFee = 50.msat.max(amount * 0.03) - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, d, f, Random.nextLong(250).msat, Random.nextInt(10000), minHtlc = Random.nextLong(100).msat, maxHtlc = Some((20000 + Random.nextLong(80000)).msat), CltvExpiryDelta(Random.nextInt(288)), capacity = (10 + Random.nextLong(100)).sat, balance_opt = Some(Random.nextLong(2 * amount.toLong).msat)), makeEdge(2L, d, a, Random.nextLong(250).msat, Random.nextInt(10000), minHtlc = Random.nextLong(100).msat, maxHtlc = Some((20000 + Random.nextLong(80000)).msat), CltvExpiryDelta(Random.nextInt(288)), capacity = (10 + Random.nextLong(100)).sat, balance_opt = Some(Random.nextLong(2 * amount.toLong).msat)), makeEdge(3L, d, e, Random.nextLong(250).msat, Random.nextInt(10000), minHtlc = Random.nextLong(100).msat, maxHtlc = Some((20000 + Random.nextLong(80000)).msat), CltvExpiryDelta(Random.nextInt(288)), capacity = (10 + Random.nextLong(100)).sat, balance_opt = Some(Random.nextLong(2 * amount.toLong).msat)), @@ -1605,7 +1606,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(7L, e, b, Random.nextLong(250).msat, Random.nextInt(10000), minHtlc = Random.nextLong(100).msat, maxHtlc = Some((20000 + Random.nextLong(80000)).msat), CltvExpiryDelta(Random.nextInt(288)), capacity = (10 + Random.nextLong(100)).sat), makeEdge(8L, b, c, Random.nextLong(250).msat, Random.nextInt(10000), minHtlc = Random.nextLong(100).msat, maxHtlc = Some((20000 + Random.nextLong(80000)).msat), CltvExpiryDelta(Random.nextInt(288)), capacity = (10 + Random.nextLong(100)).sat), makeEdge(9L, c, f, Random.nextLong(250).msat, Random.nextInt(10000), minHtlc = Random.nextLong(100).msat, maxHtlc = Some((20000 + Random.nextLong(80000)).msat), CltvExpiryDelta(Random.nextInt(288)), capacity = (10 + Random.nextLong(100)).sat) - )) + )), 1 day) findMultiPartRoute(g, d, f, amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS.copy(randomize = true), currentBlockHeight = BlockHeight(400000)) match { case Success(routes) => checkRouteAmounts(routes, amount, maxFee) @@ -1622,7 +1623,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { // ^ | // | | // F <---+ - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 1000 msat, 1000), makeEdge(2L, b, c, 1000 msat, 1000), makeEdge(3L, c, d, 1000 msat, 1000), @@ -1630,7 +1631,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(5L, b, e, 1000 msat, 1000), makeEdge(6L, c, f, 1000 msat, 1000), makeEdge(7L, f, b, 1000 msat, 1000), - )) + )), 1 day) val Success(routes) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 3, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(routes.length == 2) @@ -1647,7 +1648,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { // | ^ // | | // F ----+ - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, b, a, 1000 msat, 1000), makeEdge(2L, c, b, 1000 msat, 1000), makeEdge(3L, d, c, 1000 msat, 1000), @@ -1655,7 +1656,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(5L, e, b, 1000 msat, 1000), makeEdge(6L, f, c, 1000 msat, 1000), makeEdge(7L, b, f, 1000 msat, 1000), - )) + )), 1 day) val Success(routes) = findRoute(g, e, a, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 3, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(routes.length == 2) @@ -1691,7 +1692,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { q.toSeq } - val g = DirectedGraph(makeEdges(10)) + val g = GraphWithBalanceEstimates(DirectedGraph(makeEdges(10)), 1 day) val Success(routes) = findRoute(g, a, b, DEFAULT_AMOUNT_MSAT, 100000000 msat, numRoutes = 10, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(routes.distinct.length == 10) @@ -1723,7 +1724,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { q.toSeq } - val g = DirectedGraph(makeEdges(10)) + val g = GraphWithBalanceEstimates(DirectedGraph(makeEdges(10)), 1 day) val Success(routes) = findRoute(g, a, b, DEFAULT_AMOUNT_MSAT, 100000000 msat, numRoutes = 10, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(routes.distinct.length == 10) @@ -1732,9 +1733,9 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { } test("can't relay if fee is not sufficient") { - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 1000 msat, 7000), - )) + )), 1 day) assert(findRoute(g, a, b, 10000000 msat, 10000 msat, numRoutes = 3, routeParams = DEFAULT_ROUTE_PARAMS.copy(includeLocalChannelCost = true), currentBlockHeight = BlockHeight(400000)) == Failure(RouteNotFound)) assert(findRoute(g, a, b, 10000000 msat, 100000 msat, numRoutes = 3, routeParams = DEFAULT_ROUTE_PARAMS.copy(includeLocalChannelCost = true), currentBlockHeight = BlockHeight(400000)).isSuccess) @@ -1749,7 +1750,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { // v | // E ---> F val start = randomKey().publicKey - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(0L, start, a, 0 msat, 0), makeEdge(1L, a, b, 1000 msat, 1000), makeEdge(2L, a, c, 0 msat, 0), @@ -1758,7 +1759,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(5L, c, e, 0 msat, 0), makeEdge(6L, e, f, 600 msat, 1000), makeEdge(7L, f, d, 0 msat, 0), - )) + )), 1 day) { // No hop cost val Success(routes) = findRoute(g, start, b, DEFAULT_AMOUNT_MSAT, 100000000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) @@ -1786,13 +1787,13 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { // v | // C ---> D val start = randomKey().publicKey - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(0L, start, a, 0 msat, 0), makeEdge(1L, a, b, 1000 msat, 1000, capacity = (DEFAULT_AMOUNT_MSAT * 1.2).truncateToSatoshi), makeEdge(2L, a, c, 400 msat, 500, capacity = (DEFAULT_AMOUNT_MSAT * 3).truncateToSatoshi), makeEdge(3L, c, d, 400 msat, 500, capacity = (DEFAULT_AMOUNT_MSAT * 3).truncateToSatoshi), makeEdge(4L, d, b, 400 msat, 500, capacity = (DEFAULT_AMOUNT_MSAT * 3).truncateToSatoshi), - )) + )), 1 day) { val hc = HeuristicsConstants( @@ -1800,6 +1801,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { failureCost = RelayFees(1000 msat, 500), hopCost = RelayFees(0 msat, 0), useLogProbability = false, + usePastRelaysData = true, ) val Success(routes) = findRoute(g, start, b, DEFAULT_AMOUNT_MSAT, 100000000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Right(hc)), currentBlockHeight = BlockHeight(400000)) assert(routes.distinct.length == 1) @@ -1814,6 +1816,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { failureCost = RelayFees(10000 msat, 1000), hopCost = RelayFees(0 msat, 0), useLogProbability = true, + usePastRelaysData = true, ) val Success(routes) = findRoute(g, start, b, DEFAULT_AMOUNT_MSAT, 100000000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Right(hc)), currentBlockHeight = BlockHeight(400000)) assert(routes.distinct.length == 1) @@ -1828,19 +1831,20 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { // v | // C ---> D val start = randomKey().publicKey - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(0L, start, a, 0 msat, 0), makeEdge(1L, a, b, 1000 msat, 1000, cltvDelta = CltvExpiryDelta(1000)), makeEdge(2L, a, c, 350 msat, 350, cltvDelta = CltvExpiryDelta(10)), makeEdge(3L, c, d, 350 msat, 350, cltvDelta = CltvExpiryDelta(10)), makeEdge(4L, d, b, 350 msat, 350, cltvDelta = CltvExpiryDelta(10)), - )) + )), 1 day) val hc = HeuristicsConstants( lockedFundsRisk = 1e-7, failureCost = RelayFees(0 msat, 0), hopCost = RelayFees(0 msat, 0), useLogProbability = true, + usePastRelaysData = true, ) val Success(routes) = findRoute(g, start, b, DEFAULT_AMOUNT_MSAT, 100000000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Right(hc)), currentBlockHeight = BlockHeight(400000)) assert(routes.distinct.length == 1) @@ -1850,17 +1854,18 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { test("edge too small to relay payment is ignored") { // A ===> B ===> C <--- D - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 100 msat, 100), makeEdge(2L, b, c, 100 msat, 100), makeEdge(3L, d, c, 100 msat, 100, capacity = 1000 sat), - )) + )), 1 day) val hc = HeuristicsConstants( lockedFundsRisk = 1e-7, failureCost = RelayFees(0 msat, 0), hopCost = RelayFees(0 msat, 0), useLogProbability = true, + usePastRelaysData = true, ) val Success(routes) = findRoute(g, a, c, DEFAULT_AMOUNT_MSAT, 100000000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Right(hc)), currentBlockHeight = BlockHeight(400000)) assert(routes.distinct.length == 1) @@ -1872,11 +1877,11 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { // A ===> B ===> C // \___________/ val recentChannelId = ShortChannelId.fromCoordinates("399990x1x2").success.value.toLong - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 1 msat, 1, capacity = 100_000_000 sat), makeEdge(2L, b, c, 1 msat, 1, capacity = 100_000_000 sat), makeEdge(recentChannelId, a, c, 1000 msat, 100), - )) + )), 1 day) val wr = WeightRatios( baseFactor = 0, @@ -1893,7 +1898,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { test("trampoline relay with direct channel to target") { val amount = 100_000_000 msat - val g = DirectedGraph(List(makeEdge(1L, a, b, 1000 msat, 1000, capacity = 100_000_000 sat))) + val g = GraphWithBalanceEstimates(DirectedGraph(List(makeEdge(1L, a, b, 1000 msat, 1000, capacity = 100_000_000 sat))), 1 day) { val routeParams = DEFAULT_ROUTE_PARAMS.copy(includeLocalChannelCost = true, boundaries = SearchBoundaries(100_999 msat, 0.0, 6, CltvExpiryDelta(576))) @@ -1908,12 +1913,12 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { test("small local edge with liquidity is better than big remote edge") { // A == B == C -- D // \_________/ - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 100 msat, 100, minHtlc = 1000 msat, capacity = 100000000 sat, balance_opt = Some(10000000 msat)), makeEdge(2L, b, c, 100 msat, 100, minHtlc = 1000 msat, capacity = 100000000 sat), makeEdge(3L, a, c, 100 msat, 100, minHtlc = 1000 msat, capacity = 100 sat, balance_opt = Some(100000 msat)), makeEdge(4L, c, d, 100 msat, 100, minHtlc = 1000 msat, capacity = 100000000 sat), - )) + )), 1 day) val wr = WeightRatios( baseFactor = 0,