diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 295ba8a30b..7284a66ae2 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -256,18 +256,27 @@ eclair { channel-age = 0.4 // when computing the weight for a channel, consider its AGE in this proportion channel-capacity = 0.55 // when computing the weight for a channel, consider its CAPACITY in this proportion } + + hop-cost { + // virtual fee for additional hops: how much you are willing to pay to get one less hop in the payment path + fee-base-msat = 500 + fee-proportional-millionths = 200 + } + locked-funds-risk = 1e-8 // msat per msat locked per block. It should be your expected interest rate per block multiplied by the probability that something goes wrong and your funds stay locked. // 1e-8 corresponds to an interest rate of ~5% per year (1e-6 per block) and a probability of 1% that the channel will fail and our funds will be locked. + // virtual fee for failed payments: how much you are willing to pay to get one less failed payment attempt + // ignored if use-ratio = true failure-cost { fee-base-msat = 2000 fee-proportional-millionths = 500 } - hop-cost { - // virtual fee for additional hops: how much you are willing to pay to get one less hop in the payment path - fee-base-msat = 500 - fee-proportional-millionths = 200 - } + // Using a failure cost breaks Dijkstra (the path returned is no longer guaranteed to be shortest one), if + // that's a concern, you can penalize paths with a low success chance by using the logarithm of the probability + // of success. It satisfies Dijkstra's requirements and is a very good approximation for paths with a high + // probability of success, however is penalizes less the paths with a low probability of success. + use-log-probability = false mpp { min-amount-satoshis = 15000 // minimum amount sent via partial HTLCs 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 ae01b21058..a74a9373c9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -365,6 +365,7 @@ object NodeParams extends Logging { lockedFundsRisk = config.getDouble("locked-funds-risk"), failureCost = getRelayFees(config.getConfig("failure-cost")), hopCost = getRelayFees(config.getConfig("hop-cost")), + useLogProbability = config.getBoolean("use-log-probability"), )) }, 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 590c4a1848..d343fb6bc3 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 @@ -67,7 +67,8 @@ object EclairInternalsSerializer { val heuristicsConstantsCodec: Codec[HeuristicsConstants] = ( ("lockedFundsRisk" | double) :: ("failureCost" | relayFeesCodec) :: - ("hopCost" | relayFeesCodec)).as[HeuristicsConstants] + ("hopCost" | relayFeesCodec) :: + ("useLogProbability" | bool(8))).as[HeuristicsConstants] val multiPartParamsCodec: Codec[MultiPartParams] = ( ("minPartAmount" | millisatoshi) :: 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 aff89f0c14..65e52fdedb 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 @@ -65,7 +65,7 @@ object Graph { * @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) */ - case class HeuristicsConstants(lockedFundsRisk: Double, failureCost: RelayFees, hopCost: RelayFees) + case class HeuristicsConstants(lockedFundsRisk: Double, failureCost: RelayFees, hopCost: RelayFees, useLogProbability: Boolean) case class WeightedNode(key: PublicKey, weight: RichWeight) @@ -329,7 +329,6 @@ object Graph { case Right(heuristicsConstants) => val hopCost = nodeFee(heuristicsConstants.hopCost, prev.amount) val totalHopsCost = prev.virtualFees + hopCost - val riskCost = totalAmount.toLong * totalCltv.toInt * heuristicsConstants.lockedFundsRisk // If the edge was added by the invoice, it is assumed that it can route the payment. // If we know the balance of the channel, then we will check separately that it can relay the payment. val successProbability = if (edge.update.chainHash == ByteVector32.Zeroes || edge.balance_opt.nonEmpty) 1.0 else 1.0 - prev.amount.toLong.toDouble / edge.capacity.toMilliSatoshi.toLong.toDouble @@ -338,8 +337,15 @@ object Graph { } val totalSuccessProbability = prev.successProbability * successProbability val failureCost = nodeFee(heuristicsConstants.failureCost, totalAmount) - val weight = totalFees.toLong + totalHopsCost.toLong + riskCost + failureCost.toLong / totalSuccessProbability - RichWeight(totalAmount, prev.length + 1, totalCltv, totalSuccessProbability, totalFees, totalHopsCost, weight) + if (heuristicsConstants.useLogProbability) { + val riskCost = totalAmount.toLong * cltv.toInt * heuristicsConstants.lockedFundsRisk + val weight = prev.weight + fee.toLong + hopCost.toLong + riskCost - failureCost.toLong * math.log(successProbability) + RichWeight(totalAmount, prev.length + 1, totalCltv, totalSuccessProbability, totalFees, totalHopsCost, weight) + } else { + val totalRiskCost = totalAmount.toLong * totalCltv.toInt * heuristicsConstants.lockedFundsRisk + val weight = totalFees.toLong + totalHopsCost.toLong + totalRiskCost + failureCost.toLong / totalSuccessProbability + RichWeight(totalAmount, prev.length + 1, totalCltv, totalSuccessProbability, totalFees, totalHopsCost, weight) + } } } 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 503d07a4f4..375f14423d 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 @@ -257,7 +257,7 @@ class GraphSpec extends AnyFunSuite { val path :: Nil = yenKshortestPaths(graph, a, e, 100000000 msat, Set.empty, Set.empty, Set.empty, 1, - Right(HeuristicsConstants(1.0E-8, RelayFees(2000 msat, 500), RelayFees(50 msat, 20))), + Right(HeuristicsConstants(1.0E-8, RelayFees(2000 msat, 500), RelayFees(50 msat, 20), useLogProbability = true)), BlockHeight(714930), _ => true, includeLocalChannelCost = true) assert(path.path == Seq(edgeAB, edgeBC, edgeCE)) } 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 b551dbec1c..6ecb23ed5e 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 @@ -1792,16 +1792,32 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(4L, d, b, 400 msat, 500, capacity = (DEFAULT_AMOUNT_MSAT * 3).truncateToSatoshi), )) - val hc = HeuristicsConstants( - lockedFundsRisk = 0.0, - failureCost = RelayFees(1000 msat, 500), - hopCost = RelayFees(0 msat, 0), - ) - 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) - val route :: Nil = routes - assert(route2Ids(route) === 0 :: 2 :: 3 :: 4 :: Nil) + { + val hc = HeuristicsConstants( + lockedFundsRisk = 0.0, + failureCost = RelayFees(1000 msat, 500), + hopCost = RelayFees(0 msat, 0), + useLogProbability = false, + ) + 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) + val route :: Nil = routes + assert(route2Ids(route) === 0 :: 2 :: 3 :: 4 :: Nil) + } + + { + val hc = HeuristicsConstants( + lockedFundsRisk = 0.0, + failureCost = RelayFees(10000 msat, 1000), + hopCost = RelayFees(0 msat, 0), + useLogProbability = 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) + val route :: Nil = routes + assert(route2Ids(route) === 0 :: 2 :: 3 :: 4 :: Nil) + } } test("no path that can get our funds stuck for too long") { @@ -1822,6 +1838,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { lockedFundsRisk = 1e-7, failureCost = RelayFees(0 msat, 0), hopCost = RelayFees(0 msat, 0), + useLogProbability = 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) @@ -1841,6 +1858,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { lockedFundsRisk = 1e-7, failureCost = RelayFees(0 msat, 0), hopCost = RelayFees(0 msat, 0), + useLogProbability = 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)