Skip to content

Commit f857368

Browse files
Make trampoline payments use per-channel fee and cltv (#1853)
Trampoline payments used to ignore the fee and cltv set for the local channel and use a global default value instead. We now use the correct fee and cltv for the specific local channel that we take.
1 parent 85ed433 commit f857368

File tree

9 files changed

+81
-63
lines changed

9 files changed

+81
-63
lines changed

eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala

+4-3
Original file line numberDiff line numberDiff line change
@@ -120,12 +120,13 @@ object NodeRelay {
120120

121121
/** Compute route params that honor our fee and cltv requirements. */
122122
def computeRouteParams(nodeParams: NodeParams, amountIn: MilliSatoshi, expiryIn: CltvExpiry, amountOut: MilliSatoshi, expiryOut: CltvExpiry): RouteParams = {
123-
val routeMaxCltv = expiryIn - expiryOut - nodeParams.expiryDelta
124-
val routeMaxFee = amountIn - amountOut - nodeFee(nodeParams.feeBase, nodeParams.feeProportionalMillionth, amountOut)
123+
val routeMaxCltv = expiryIn - expiryOut
124+
val routeMaxFee = amountIn - amountOut
125125
RouteCalculation.getDefaultRouteParams(nodeParams.routerConf).copy(
126126
maxFeeBase = routeMaxFee,
127127
routeMaxCltv = routeMaxCltv,
128-
maxFeePct = 0 // we disable percent-based max fee calculation, we're only interested in collecting our node fee
128+
maxFeePct = 0, // we disable percent-based max fee calculation, we're only interested in collecting our node fee
129+
includeLocalChannelCost = true,
129130
)
130131
}
131132

eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala

+50-44
Original file line numberDiff line numberDiff line change
@@ -73,17 +73,18 @@ object Graph {
7373
* Yen's algorithm to find the k-shortest (loop-less) paths in a graph, uses dijkstra as search algo. Is guaranteed to
7474
* terminate finding at most @pathsToFind paths sorted by cost (the cheapest is in position 0).
7575
*
76-
* @param graph the graph on which will be performed the search
77-
* @param sourceNode the starting node of the path we're looking for (payer)
78-
* @param targetNode the destination node of the path (recipient)
79-
* @param amount amount to send to the last node
80-
* @param ignoredEdges channels that should be avoided
81-
* @param ignoredVertices nodes that should be avoided
82-
* @param extraEdges additional edges that can be used (e.g. private channels from invoices)
83-
* @param pathsToFind number of distinct paths to be returned
84-
* @param wr ratios used to 'weight' edges when searching for the shortest path
85-
* @param currentBlockHeight the height of the chain tip (latest block)
86-
* @param boundaries a predicate function that can be used to impose limits on the outcome of the search
76+
* @param graph the graph on which will be performed the search
77+
* @param sourceNode the starting node of the path we're looking for (payer)
78+
* @param targetNode the destination node of the path (recipient)
79+
* @param amount amount to send to the last node
80+
* @param ignoredEdges channels that should be avoided
81+
* @param ignoredVertices nodes that should be avoided
82+
* @param extraEdges additional edges that can be used (e.g. private channels from invoices)
83+
* @param pathsToFind number of distinct paths to be returned
84+
* @param wr ratios used to 'weight' edges when searching for the shortest path
85+
* @param currentBlockHeight the height of the chain tip (latest block)
86+
* @param boundaries a predicate function that can be used to impose limits on the outcome of the search
87+
* @param includeLocalChannelCost if the path is for relaying and we need to include the cost of the local channel
8788
*/
8889
def yenKshortestPaths(graph: DirectedGraph,
8990
sourceNode: PublicKey,
@@ -95,10 +96,11 @@ object Graph {
9596
pathsToFind: Int,
9697
wr: Option[WeightRatios],
9798
currentBlockHeight: Long,
98-
boundaries: RichWeight => Boolean): Seq[WeightedPath] = {
99+
boundaries: RichWeight => Boolean,
100+
includeLocalChannelCost: Boolean): Seq[WeightedPath] = {
99101
// find the shortest path (k = 0)
100102
val targetWeight = RichWeight(amount, 0, CltvExpiryDelta(0), 0)
101-
val shortestPath = dijkstraShortestPath(graph, sourceNode, targetNode, ignoredEdges, ignoredVertices, extraEdges, targetWeight, boundaries, currentBlockHeight, wr)
103+
val shortestPath = dijkstraShortestPath(graph, sourceNode, targetNode, ignoredEdges, ignoredVertices, extraEdges, targetWeight, boundaries, currentBlockHeight, wr, includeLocalChannelCost)
102104
if (shortestPath.isEmpty) {
103105
return Seq.empty // if we can't even find a single path, avoid returning a Seq(Seq.empty)
104106
}
@@ -110,7 +112,7 @@ object Graph {
110112

111113
var allSpurPathsFound = false
112114
val shortestPaths = new mutable.Queue[PathWithSpur]
113-
shortestPaths.enqueue(PathWithSpur(WeightedPath(shortestPath, pathWeight(sourceNode, shortestPath, amount, currentBlockHeight, wr)), 0))
115+
shortestPaths.enqueue(PathWithSpur(WeightedPath(shortestPath, pathWeight(sourceNode, shortestPath, amount, currentBlockHeight, wr, includeLocalChannelCost)), 0))
114116
// stores the candidates for the k-th shortest path, sorted by path cost
115117
val candidates = new mutable.PriorityQueue[PathWithSpur]
116118

@@ -135,12 +137,12 @@ object Graph {
135137
val alreadyExploredEdges = shortestPaths.collect { case p if p.p.path.takeRight(i) == rootPathEdges => p.p.path(p.p.path.length - 1 - i).desc }.toSet
136138
// we also want to ignore any vertex on the root path to prevent loops
137139
val alreadyExploredVertices = rootPathEdges.map(_.desc.b).toSet
138-
val rootPathWeight = pathWeight(sourceNode, rootPathEdges, amount, currentBlockHeight, wr)
140+
val rootPathWeight = pathWeight(sourceNode, rootPathEdges, amount, currentBlockHeight, wr, includeLocalChannelCost)
139141
// find the "spur" path, a sub-path going from the spur node to the target avoiding previously found sub-paths
140-
val spurPath = dijkstraShortestPath(graph, sourceNode, spurNode, ignoredEdges ++ alreadyExploredEdges, ignoredVertices ++ alreadyExploredVertices, extraEdges, rootPathWeight, boundaries, currentBlockHeight, wr)
142+
val spurPath = dijkstraShortestPath(graph, sourceNode, spurNode, ignoredEdges ++ alreadyExploredEdges, ignoredVertices ++ alreadyExploredVertices, extraEdges, rootPathWeight, boundaries, currentBlockHeight, wr, includeLocalChannelCost)
141143
if (spurPath.nonEmpty) {
142144
val completePath = spurPath ++ rootPathEdges
143-
val candidatePath = WeightedPath(completePath, pathWeight(sourceNode, completePath, amount, currentBlockHeight, wr))
145+
val candidatePath = WeightedPath(completePath, pathWeight(sourceNode, completePath, amount, currentBlockHeight, wr, includeLocalChannelCost))
144146
candidates.enqueue(PathWithSpur(candidatePath, i))
145147
}
146148
}
@@ -163,16 +165,17 @@ object Graph {
163165
* path from the target to the source (this is because we want to calculate the weight of the edges correctly). The
164166
* graph @param g is optimized for querying the incoming edges given a vertex.
165167
*
166-
* @param g the graph on which will be performed the search
167-
* @param sourceNode the starting node of the path we're looking for (payer)
168-
* @param targetNode the destination node of the path
169-
* @param ignoredEdges channels that should be avoided
170-
* @param ignoredVertices nodes that should be avoided
171-
* @param extraEdges additional edges that can be used (e.g. private channels from invoices)
172-
* @param initialWeight weight that will be applied to the target node
173-
* @param boundaries a predicate function that can be used to impose limits on the outcome of the search
174-
* @param currentBlockHeight the height of the chain tip (latest block)
175-
* @param wr ratios used to 'weight' edges when searching for the shortest path
168+
* @param g the graph on which will be performed the search
169+
* @param sourceNode the starting node of the path we're looking for (payer)
170+
* @param targetNode the destination node of the path
171+
* @param ignoredEdges channels that should be avoided
172+
* @param ignoredVertices nodes that should be avoided
173+
* @param extraEdges additional edges that can be used (e.g. private channels from invoices)
174+
* @param initialWeight weight that will be applied to the target node
175+
* @param boundaries a predicate function that can be used to impose limits on the outcome of the search
176+
* @param currentBlockHeight the height of the chain tip (latest block)
177+
* @param wr ratios used to 'weight' edges when searching for the shortest path
178+
* @param includeLocalChannelCost if the path is for relaying and we need to include the cost of the local channel
176179
*/
177180
private def dijkstraShortestPath(g: DirectedGraph,
178181
sourceNode: PublicKey,
@@ -183,7 +186,8 @@ object Graph {
183186
initialWeight: RichWeight,
184187
boundaries: RichWeight => Boolean,
185188
currentBlockHeight: Long,
186-
wr: Option[WeightRatios]): Seq[GraphEdge] = {
189+
wr: Option[WeightRatios],
190+
includeLocalChannelCost: Boolean): Seq[GraphEdge] = {
187191
// the graph does not contain source/destination nodes
188192
val sourceNotInGraph = !g.containsVertex(sourceNode) && !extraEdges.exists(_.desc.a == sourceNode)
189193
val targetNotInGraph = !g.containsVertex(targetNode) && !extraEdges.exists(_.desc.b == targetNode)
@@ -221,7 +225,7 @@ object Graph {
221225
val neighbor = edge.desc.a
222226
// NB: this contains the amount (including fees) that will need to be sent to `neighbor`, but the amount that
223227
// will be relayed through that edge is the one in `currentWeight`.
224-
val neighborWeight = addEdgeWeight(sourceNode, edge, current.weight, currentBlockHeight, wr)
228+
val neighborWeight = addEdgeWeight(sourceNode, edge, current.weight, currentBlockHeight, wr, includeLocalChannelCost)
225229
val canRelayAmount = current.weight.cost <= edge.capacity &&
226230
edge.balance_opt.forall(current.weight.cost <= _) &&
227231
edge.update.htlcMaximumMsat.forall(current.weight.cost <= _) &&
@@ -258,16 +262,17 @@ object Graph {
258262
/**
259263
* Add the given edge to the path and compute the new weight.
260264
*
261-
* @param sender node sending the payment
262-
* @param edge the edge we want to cross
263-
* @param prev weight of the rest of the path
264-
* @param currentBlockHeight the height of the chain tip (latest block).
265-
* @param weightRatios ratios used to 'weight' edges when searching for the shortest path
265+
* @param sender node sending the payment
266+
* @param edge the edge we want to cross
267+
* @param prev weight of the rest of the path
268+
* @param currentBlockHeight the height of the chain tip (latest block).
269+
* @param weightRatios ratios used to 'weight' edges when searching for the shortest path
270+
* @param includeLocalChannelCost if the path is for relaying and we need to include the cost of the local channel
266271
*/
267-
private def addEdgeWeight(sender: PublicKey, edge: GraphEdge, prev: RichWeight, currentBlockHeight: Long, weightRatios: Option[WeightRatios]): RichWeight = {
268-
val totalCost = if (edge.desc.a == sender) prev.cost else addEdgeFees(edge, prev.cost)
272+
private def addEdgeWeight(sender: PublicKey, edge: GraphEdge, prev: RichWeight, currentBlockHeight: Long, weightRatios: Option[WeightRatios], includeLocalChannelCost: Boolean): RichWeight = {
273+
val totalCost = if (edge.desc.a == sender && !includeLocalChannelCost) prev.cost else addEdgeFees(edge, prev.cost)
269274
val fee = totalCost - prev.cost
270-
val totalCltv = if (edge.desc.a == sender) prev.cltv else prev.cltv + edge.update.cltvExpiryDelta
275+
val totalCltv = if (edge.desc.a == sender && !includeLocalChannelCost) prev.cltv else prev.cltv + edge.update.cltvExpiryDelta
271276
val factor = weightRatios match {
272277
case None =>
273278
1.0
@@ -322,15 +327,16 @@ object Graph {
322327
* Calculates the total weighted cost of a path.
323328
* Note that the first hop from the sender is ignored: we don't pay a routing fee to ourselves.
324329
*
325-
* @param sender node sending the payment
326-
* @param path candidate path.
327-
* @param amount amount to send to the last node.
328-
* @param currentBlockHeight the height of the chain tip (latest block).
329-
* @param wr ratios used to 'weight' edges when searching for the shortest path
330+
* @param sender node sending the payment
331+
* @param path candidate path.
332+
* @param amount amount to send to the last node.
333+
* @param currentBlockHeight the height of the chain tip (latest block).
334+
* @param wr ratios used to 'weight' edges when searching for the shortest path
335+
* @param includeLocalChannelCost if the path is for relaying and we need to include the cost of the local channel
330336
*/
331-
def pathWeight(sender: PublicKey, path: Seq[GraphEdge], amount: MilliSatoshi, currentBlockHeight: Long, wr: Option[WeightRatios]): RichWeight = {
337+
def pathWeight(sender: PublicKey, path: Seq[GraphEdge], amount: MilliSatoshi, currentBlockHeight: Long, wr: Option[WeightRatios], includeLocalChannelCost: Boolean): RichWeight = {
332338
path.foldRight(RichWeight(amount, 0, CltvExpiryDelta(0), 0)) { (edge, prev) =>
333-
addEdgeWeight(sender, edge, prev, currentBlockHeight, wr)
339+
addEdgeWeight(sender, edge, prev, currentBlockHeight, wr, includeLocalChannelCost)
334340
}
335341
}
336342

eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala

+3-2
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,8 @@ object RouteCalculation {
197197
capacityFactor = routerConf.searchRatioChannelCapacity
198198
))
199199
},
200-
mpp = MultiPartParams(routerConf.mppMinPartAmount, routerConf.mppMaxParts)
200+
mpp = MultiPartParams(routerConf.mppMinPartAmount, routerConf.mppMaxParts),
201+
includeLocalChannelCost = false,
201202
)
202203

203204
/**
@@ -257,7 +258,7 @@ object RouteCalculation {
257258

258259
val boundaries: RichWeight => Boolean = { weight => feeOk(weight.cost - amount) && lengthOk(weight.length) && cltvOk(weight.cltv) }
259260

260-
val foundRoutes: Seq[Graph.WeightedPath] = Graph.yenKshortestPaths(g, localNodeId, targetNodeId, amount, ignoredEdges, ignoredVertices, extraEdges, numRoutes, routeParams.ratios, currentBlockHeight, boundaries)
261+
val foundRoutes: Seq[Graph.WeightedPath] = Graph.yenKshortestPaths(g, localNodeId, targetNodeId, amount, ignoredEdges, ignoredVertices, extraEdges, numRoutes, routeParams.ratios, currentBlockHeight, boundaries, routeParams.includeLocalChannelCost)
261262
if (foundRoutes.nonEmpty) {
262263
val (directRoutes, indirectRoutes) = foundRoutes.partition(_.path.length == 1)
263264
val routes = if (routeParams.randomize) {

eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -432,7 +432,7 @@ object Router {
432432

433433
case class MultiPartParams(minPartAmount: MilliSatoshi, maxParts: Int)
434434

435-
case class RouteParams(randomize: Boolean, maxFeeBase: MilliSatoshi, maxFeePct: Double, routeMaxLength: Int, routeMaxCltv: CltvExpiryDelta, ratios: Option[WeightRatios], mpp: MultiPartParams) {
435+
case class RouteParams(randomize: Boolean, maxFeeBase: MilliSatoshi, maxFeePct: Double, routeMaxLength: Int, routeMaxCltv: CltvExpiryDelta, ratios: Option[WeightRatios], mpp: MultiPartParams, includeLocalChannelCost: Boolean) {
436436
def getMaxFee(amount: MilliSatoshi): MilliSatoshi = {
437437
// The payment fee must satisfy either the flat fee or the percentage fee, not necessarily both.
438438
maxFeeBase.max(amount * maxFeePct)

eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala

+2-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ abstract class IntegrationSpec extends TestKitBaseClass with BitcoindService wit
6060
ageFactor = 0,
6161
capacityFactor = 0
6262
)),
63-
mpp = MultiPartParams(15000000 msat, 6)
63+
mpp = MultiPartParams(15000000 msat, 6),
64+
includeLocalChannelCost = false,
6465
))
6566

6667
// we need to provide a value higher than every node's fulfill-safety-before-timeout

eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -562,7 +562,7 @@ object MultiPartPaymentLifecycleSpec {
562562
val expiry = CltvExpiry(1105)
563563
val finalAmount = 1000000 msat
564564
val finalRecipient = randomKey().publicKey
565-
val routeParams = RouteParams(randomize = false, 15000 msat, 0.01, 6, CltvExpiryDelta(1008), None, MultiPartParams(1000 msat, 5))
565+
val routeParams = RouteParams(randomize = false, 15000 msat, 0.01, 6, CltvExpiryDelta(1008), None, MultiPartParams(1000 msat, 5), false)
566566
val maxFee = 15000 msat // max fee for the defaultAmount
567567

568568
/**

eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
211211
import payFixture._
212212
import cfg._
213213

214-
val request = SendPayment(sender.ref, d, Onion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get), 5, routeParams = Some(RouteParams(randomize = false, 100 msat, 0.0, 20, CltvExpiryDelta(2016), None, MultiPartParams(10000 msat, 5))))
214+
val request = SendPayment(sender.ref, d, Onion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get), 5, routeParams = Some(RouteParams(randomize = false, 100 msat, 0.0, 20, CltvExpiryDelta(2016), None, MultiPartParams(10000 msat, 5), false)))
215215
sender.send(paymentFSM, request)
216216
val routeRequest = routerForwarder.expectMsgType[RouteRequest]
217217
val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]])

eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala

+3-3
Original file line numberDiff line numberDiff line change
@@ -487,10 +487,10 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl
487487

488488
val routeRequest = router.expectMessageType[RouteRequest]
489489
val routeParams = routeRequest.routeParams.get
490-
val fee = nodeFee(nodeParams.feeBase, nodeParams.feeProportionalMillionth, outgoingAmount)
491490
assert(routeParams.maxFeePct === 0) // should be disabled
492-
assert(routeParams.maxFeeBase === incomingAmount - outgoingAmount - fee) // we collect our fee and then use what remains for the rest of the route
493-
assert(routeParams.routeMaxCltv === incomingSinglePart.add.cltvExpiry - outgoingExpiry - nodeParams.expiryDelta) // we apply our cltv delta
491+
assert(routeParams.maxFeeBase === incomingAmount - outgoingAmount)
492+
assert(routeParams.routeMaxCltv === incomingSinglePart.add.cltvExpiry - outgoingExpiry)
493+
assert(routeParams.includeLocalChannelCost)
494494
}
495495

496496
test("relay incoming multi-part payment") { f =>

0 commit comments

Comments
 (0)