@@ -98,49 +98,50 @@ object Graph {
98
98
boundaries : RichWeight => Boolean ): Seq [WeightedPath ] = {
99
99
// find the shortest path (k = 0)
100
100
val targetWeight = RichWeight (amount, 0 , CltvExpiryDelta (0 ), 0 )
101
- val shortestPath = dijkstraShortestPath(graph, sourceNode, sourceNode, targetNode, ignoredEdges, ignoredVertices, extraEdges, targetWeight, boundaries, currentBlockHeight, wr)
101
+ val shortestPath = dijkstraShortestPath(graph, sourceNode, targetNode, ignoredEdges, ignoredVertices, extraEdges, targetWeight, boundaries, currentBlockHeight, wr)
102
102
if (shortestPath.isEmpty) {
103
103
return Seq .empty // if we can't even find a single path, avoid returning a Seq(Seq.empty)
104
104
}
105
105
106
+ case class PathWithSpur (p : WeightedPath , spurIndex : Int )
107
+ implicit object PathWithSpurComparator extends Ordering [PathWithSpur ] {
108
+ override def compare (x : PathWithSpur , y : PathWithSpur ): Int = y.p.weight.compare(x.p.weight)
109
+ }
110
+
106
111
var allSpurPathsFound = false
107
- val shortestPaths = new mutable.Queue [WeightedPath ]
108
- shortestPaths.enqueue(WeightedPath (shortestPath, pathWeight(sourceNode, shortestPath, amount, currentBlockHeight, wr)))
112
+ val shortestPaths = new mutable.Queue [PathWithSpur ]
113
+ shortestPaths.enqueue(PathWithSpur ( WeightedPath (shortestPath, pathWeight(sourceNode, shortestPath, amount, currentBlockHeight, wr)), 0 ))
109
114
// stores the candidates for the k-th shortest path, sorted by path cost
110
- val candidates = new mutable.PriorityQueue [WeightedPath ]
115
+ val candidates = new mutable.PriorityQueue [PathWithSpur ]
111
116
112
117
// main loop
113
118
for (k <- 1 until pathsToFind) {
114
119
if (! allSpurPathsFound) {
115
- val prevShortestPath = shortestPaths(k - 1 ).path
116
- // for every edge in the path, we will try to find a different path after that edge
117
- for (i <- prevShortestPath.indices ) {
118
- // select the spur node as the i-th element of the previous shortest path
119
- val spurNode = prevShortestPath(i).desc.a
120
- // select the sub-path from the source to the spur node
121
- val rootPathEdges = prevShortestPath.take (i)
120
+ val PathWithSpur ( WeightedPath ( prevShortestPath, _), spurIndex) = shortestPaths(k - 1 )
121
+ // for every new edge in the path, we will try to find a different path before that edge
122
+ for (i <- spurIndex until prevShortestPath.length ) {
123
+ // select the spur node as the i-th element from the target of the previous shortest path
124
+ val spurNode = prevShortestPath(prevShortestPath.length - 1 - i).desc.b
125
+ // select the sub-path from the spur node to the target
126
+ val rootPathEdges = prevShortestPath.takeRight (i)
122
127
// we ignore all the paths that we have already fully explored in previous iterations
123
- // if for example the spur node is B, and we already found shortest paths starting with A-B-C and A-B-D,
124
- // we want to ignore the B-C and B-D edges
125
- // +-- C -- [...]
126
- // |
127
- // A -- B --+-- D -- [...]
128
- // |
129
- // +-- E -- [...]
130
- val alreadyExploredEdges = shortestPaths.collect { case p if p.path.take(i) == rootPathEdges => p.path(i).desc }.toSet
131
- // we also want to ignore any link that can lead back to the previous node (we only want to go forward)
132
- val returningEdges = rootPathEdges.lastOption.map(last => graph.getEdgesBetween(last.desc.b, last.desc.a).map(_.desc).toSet).getOrElse(Set .empty)
128
+ // if for example the spur node is D, and we already found shortest paths ending with A->D->E and B->D->E,
129
+ // we want to ignore the A->D and B->D edges
130
+ // [...] --> A --+
131
+ // |
132
+ // [...] --> B --+--> D --> E
133
+ // |
134
+ // [...] --> C --+
135
+ val alreadyExploredEdges = shortestPaths.collect { case p if p.p.path.takeRight(i) == rootPathEdges => p.p.path(p.p.path.length - 1 - i).desc }.toSet
136
+ // we also want to ignore any vertex on the root path to prevent loops
137
+ val alreadyExploredVertices = rootPathEdges.map(_.desc.b).toSet
138
+ val rootPathWeight = pathWeight(sourceNode, rootPathEdges, amount, currentBlockHeight, wr)
133
139
// find the "spur" path, a sub-path going from the spur node to the target avoiding previously found sub-paths
134
- val spurPath = dijkstraShortestPath(graph, sourceNode, spurNode, targetNode, ignoredEdges ++ alreadyExploredEdges ++ returningEdges, ignoredVertices, extraEdges, targetWeight , boundaries, currentBlockHeight, wr)
140
+ val spurPath = dijkstraShortestPath(graph, sourceNode, spurNode, ignoredEdges ++ alreadyExploredEdges, ignoredVertices ++ alreadyExploredVertices, extraEdges, rootPathWeight , boundaries, currentBlockHeight, wr)
135
141
if (spurPath.nonEmpty) {
136
- // candidate k-shortest path is made of the root path and the new spur path, but the cost of the spur
137
- // path is likely higher than previous shortest paths, so we need to validate that the root path can
138
- // relay the increased amount.
139
- val completePath = rootPathEdges ++ spurPath
142
+ val completePath = spurPath ++ rootPathEdges
140
143
val candidatePath = WeightedPath (completePath, pathWeight(sourceNode, completePath, amount, currentBlockHeight, wr))
141
- if (boundaries(candidatePath.weight) && ! shortestPaths.contains(candidatePath) && ! candidates.exists(_ == candidatePath) && validatePath(completePath, amount)) {
142
- candidates.enqueue(candidatePath)
143
- }
144
+ candidates.enqueue(PathWithSpur (candidatePath, i))
144
145
}
145
146
}
146
147
}
@@ -154,7 +155,7 @@ object Graph {
154
155
}
155
156
}
156
157
157
- shortestPaths.toSeq
158
+ shortestPaths.map(_.p). toSeq
158
159
}
159
160
160
161
/**
@@ -163,8 +164,7 @@ object Graph {
163
164
* graph @param g is optimized for querying the incoming edges given a vertex.
164
165
*
165
166
* @param g the graph on which will be performed the search
166
- * @param sender node sending the payment (may be different from sourceNode when calculating partial paths)
167
- * @param sourceNode the starting node of the path we're looking for
167
+ * @param sourceNode the starting node of the path we're looking for (payer)
168
168
* @param targetNode the destination node of the path
169
169
* @param ignoredEdges channels that should be avoided
170
170
* @param ignoredVertices nodes that should be avoided
@@ -175,7 +175,6 @@ object Graph {
175
175
* @param wr ratios used to 'weight' edges when searching for the shortest path
176
176
*/
177
177
private def dijkstraShortestPath (g : DirectedGraph ,
178
- sender : PublicKey ,
179
178
sourceNode : PublicKey ,
180
179
targetNode : PublicKey ,
181
180
ignoredEdges : Set [ChannelDesc ],
@@ -222,7 +221,7 @@ object Graph {
222
221
val neighbor = edge.desc.a
223
222
// NB: this contains the amount (including fees) that will need to be sent to `neighbor`, but the amount that
224
223
// will be relayed through that edge is the one in `currentWeight`.
225
- val neighborWeight = addEdgeWeight(sender , edge, current.weight, currentBlockHeight, wr)
224
+ val neighborWeight = addEdgeWeight(sourceNode , edge, current.weight, currentBlockHeight, wr)
226
225
val canRelayAmount = current.weight.cost <= edge.capacity &&
227
226
edge.balance_opt.forall(current.weight.cost <= _) &&
228
227
edge.update.htlcMaximumMsat.forall(current.weight.cost <= _) &&
@@ -295,19 +294,14 @@ object Graph {
295
294
296
295
/**
297
296
* Calculate the minimum amount that the start node needs to receive to be able to forward @amountWithFees to the end
298
- * node. To avoid infinite loops caused by zero-fee edges, we use a lower bound fee of 1 msat.
297
+ * node.
299
298
*
300
299
* @param edge the edge we want to cross
301
300
* @param amountToForward the value that this edge will have to carry along
302
301
* @return the new amount updated with the necessary fees for this edge
303
302
*/
304
303
private def addEdgeFees (edge : GraphEdge , amountToForward : MilliSatoshi ): MilliSatoshi = {
305
- if (edgeHasZeroFee(edge)) amountToForward + nodeFee(baseFee = 1 msat, proportionalFee = 0 , amountToForward)
306
- else amountToForward + nodeFee(edge.update.feeBaseMsat, edge.update.feeProportionalMillionths, amountToForward)
307
- }
308
-
309
- private def edgeHasZeroFee (edge : GraphEdge ): Boolean = {
310
- edge.update.feeBaseMsat.toLong == 0 && edge.update.feeProportionalMillionths == 0
304
+ amountToForward + nodeFee(edge.update.feeBaseMsat, edge.update.feeProportionalMillionths, amountToForward)
311
305
}
312
306
313
307
/** Validate that all edges along the path can relay the amount with fees. */
0 commit comments