@@ -390,14 +390,7 @@ object RouteCalculation {
390390 pendingHtlcs : Seq [Route ] = Nil ,
391391 routeParams : RouteParams ,
392392 currentBlockHeight : BlockHeight ): Try [Seq [Route ]] = Try {
393- val result = findMultiPartRouteInternal(g, localNodeId, targetNodeId, amount, maxFee, extraEdges, ignoredEdges, ignoredVertices, pendingHtlcs, routeParams, currentBlockHeight) match {
394- case Right (routes) => Right (routes)
395- case Left (RouteNotFound ) if routeParams.randomize =>
396- // If we couldn't find a randomized solution, fallback to a deterministic one.
397- findMultiPartRouteInternal(g, localNodeId, targetNodeId, amount, maxFee, extraEdges, ignoredEdges, ignoredVertices, pendingHtlcs, routeParams.copy(randomize = false ), currentBlockHeight)
398- case Left (ex) => Left (ex)
399- }
400- result match {
393+ findMultiPartRouteInternal(g, localNodeId, targetNodeId, amount, maxFee, extraEdges, ignoredEdges, ignoredVertices, pendingHtlcs, routeParams, currentBlockHeight) match {
401394 case Right (routes) => routes
402395 case Left (ex) => return Failure (ex)
403396 }
@@ -413,7 +406,8 @@ object RouteCalculation {
413406 ignoredVertices : Set [PublicKey ] = Set .empty,
414407 pendingHtlcs : Seq [Route ] = Nil ,
415408 routeParams : RouteParams ,
416- currentBlockHeight : BlockHeight ): Either [RouterException , Seq [Route ]] = {
409+ currentBlockHeight : BlockHeight ,
410+ now : TimestampSecond = TimestampSecond .now()): Either [RouterException , Seq [Route ]] = {
417411 // We use Yen's k-shortest paths to find many paths for chunks of the total amount.
418412 // When the recipient is a direct peer, we have complete visibility on our local channels so we can use more accurate MPP parameters.
419413 val routeParams1 = {
@@ -432,62 +426,95 @@ object RouteCalculation {
432426 routeParams.copy(mpp = MultiPartParams (minPartAmount, numRoutes, routeParams.mpp.splittingStrategy))
433427 }
434428 findRouteInternal(g, localNodeId, targetNodeId, routeParams1.mpp.minPartAmount, maxFee, routeParams1.mpp.maxParts, extraEdges, ignoredEdges, ignoredVertices, routeParams1, currentBlockHeight) match {
435- case Right (routes ) =>
429+ case Right (paths ) =>
436430 // We use these shortest paths to find a set of non-conflicting HTLCs that send the total amount.
437- split(amount, mutable.Queue (routes : _* ), initializeUsedCapacity(pendingHtlcs), routeParams1) match {
431+ split(amount, mutable.Queue (paths : _* ), initializeUsedCapacity(pendingHtlcs), routeParams1, g.balances, now ) match {
438432 case Right (routes) if validateMultiPartRoute(amount, maxFee, routes, routeParams.includeLocalChannelCost) => Right (routes)
433+ case Right (_) if routeParams.randomize =>
434+ // We've found a multipart route, but it's too expensive. We try again without randomization to prioritize cheaper paths.
435+ val sortedPaths = paths.sortBy(_.weight.weight)
436+ split(amount, mutable.Queue (sortedPaths : _* ), initializeUsedCapacity(pendingHtlcs), routeParams1, g.balances, now) match {
437+ case Right (routes) if validateMultiPartRoute(amount, maxFee, routes, routeParams.includeLocalChannelCost) => Right (routes)
438+ case _ => Left (RouteNotFound )
439+ }
439440 case _ => Left (RouteNotFound )
440441 }
441442 case Left (ex) => Left (ex)
442443 }
443444 }
444445
445- @ tailrec
446- private def split (amount : MilliSatoshi , paths : mutable.Queue [WeightedPath [PaymentPathWeight ]], usedCapacity : mutable.Map [ShortChannelId , MilliSatoshi ], routeParams : RouteParams , selectedRoutes : Seq [Route ] = Nil ): Either [RouterException , Seq [Route ]] = {
447- if (amount == 0 .msat) {
448- Right (selectedRoutes)
449- } else if (paths.isEmpty) {
450- Left (RouteNotFound )
451- } else {
446+ private case class CandidateRoute (routeWithMaximumAmount : Route , chosenAmount : MilliSatoshi )
447+
448+ private def split (amount : MilliSatoshi , paths : mutable.Queue [WeightedPath [PaymentPathWeight ]], usedCapacity : mutable.Map [ShortChannelId , MilliSatoshi ], routeParams : RouteParams , balances : BalancesEstimates , now : TimestampSecond ): Either [RouterException , Seq [Route ]] = {
449+ var amountLeft = amount
450+ var candidates : List [CandidateRoute ] = Nil
451+ while (paths.nonEmpty && amountLeft > 0 .msat) {
452452 val current = paths.dequeue()
453453 val candidate = computeRouteMaxAmount(current.path, usedCapacity)
454- if (candidate.amount < routeParams.mpp.minPartAmount.min(amount)) {
455- // this route doesn't have enough capacity left: we remove it and continue.
456- split(amount, paths, usedCapacity, routeParams, selectedRoutes)
457- } else {
458- val route = routeParams.mpp.splittingStrategy match {
454+ if (candidate.amount >= routeParams.mpp.minPartAmount.min(amountLeft)) {
455+ val routeFullCapacity = candidate.copy(amount = candidate.amount.min(amountLeft))
456+ val chosenAmount = routeParams.mpp.splittingStrategy match {
459457 case MultiPartParams .Randomize =>
460458 // randomly choose the amount to be between 20% and 100% of the available capacity.
461459 val randomizedAmount = candidate.amount * ((20d + Random .nextInt(81 )) / 100 )
462- candidate.copy(amount = randomizedAmount.max(routeParams.mpp.minPartAmount).min(amount))
460+ if (randomizedAmount < routeParams.mpp.minPartAmount) {
461+ routeParams.mpp.minPartAmount.min(amountLeft)
462+ } else {
463+ randomizedAmount.min(amountLeft)
464+ }
463465 case MultiPartParams .MaxExpectedAmount =>
464- val bestAmount = optimizeExpectedValue(current.path, candidate.amount, usedCapacity)
465- candidate.copy(amount = bestAmount.max(routeParams.mpp.minPartAmount).min(amount) )
466+ val bestAmount = optimizeExpectedValue(current.path, candidate.amount, usedCapacity, routeParams.heuristics.usePastRelaysData, balances, now )
467+ bestAmount.max(routeParams.mpp.minPartAmount).min(amountLeft )
466468 case MultiPartParams .FullCapacity =>
467- candidate.copy( amount = candidate.amount.min(amount))
469+ routeFullCapacity. amount
468470 }
469- updateUsedCapacity(route, usedCapacity)
470- // NB: we re-enqueue the current path, it may still have capacity for a second HTLC.
471- split(amount - route.amount, paths.enqueue(current), usedCapacity, routeParams, route +: selectedRoutes)
471+ updateUsedCapacity(routeFullCapacity, usedCapacity)
472+ candidates = CandidateRoute (routeFullCapacity, chosenAmount) :: candidates
473+ amountLeft = amountLeft - chosenAmount
474+ paths.enqueue(current)
472475 }
473476 }
477+ val totalChosen = candidates.map(_.chosenAmount).sum
478+ val totalMaximum = candidates.map(_.routeWithMaximumAmount.amount).sum
479+ if (totalMaximum < amount) {
480+ Left (RouteNotFound )
481+ } else {
482+ val additionalFraction = if (totalMaximum > totalChosen) (amount - totalChosen).toLong.toDouble / (totalMaximum - totalChosen).toLong.toDouble else 0.0
483+ var routes : List [Route ] = Nil
484+ var amountLeft = amount
485+ candidates.foreach { case CandidateRoute (route, chosenAmount) =>
486+ if (amountLeft > 0 .msat) {
487+ val additionalAmount = MilliSatoshi (((route.amount - chosenAmount).toLong * additionalFraction).ceil.toLong)
488+ val amountToSend = (chosenAmount + additionalAmount).min(amountLeft)
489+ routes = route.copy(amount = amountToSend) :: routes
490+ amountLeft = amountLeft - amountToSend
491+ }
492+ }
493+ Right (routes)
494+ }
474495 }
475496
476- private def optimizeExpectedValue (route : Seq [GraphEdge ], capacity : MilliSatoshi , usedCapacity : mutable.Map [ShortChannelId , MilliSatoshi ]): MilliSatoshi = {
497+ private def optimizeExpectedValue (route : Seq [GraphEdge ], capacity : MilliSatoshi , usedCapacity : mutable.Map [ShortChannelId , MilliSatoshi ], usePastRelaysData : Boolean , balances : BalancesEstimates , now : TimestampSecond ): MilliSatoshi = {
477498 // We search the maximum value of a polynomial between its two smallest roots (0 and the minimum channel capacity on the path).
478499 // We use binary search to find where the derivative changes sign.
479500 var low = 1L
480501 var high = capacity.toLong
481502 while (high - low > 1L ) {
482- val mid = (high + low) / 2
483- val d = route.drop(1 ).foldLeft(1.0 / mid) { case (x, edge) =>
484- val availableCapacity = edge.capacity - usedCapacity.getOrElse(edge.desc.shortChannelId, 0 msat)
485- x - 1.0 / (availableCapacity.toLong - mid)
503+ val x = (high + low) / 2
504+ val d = route.drop(1 ).foldLeft(1.0 / x.toDouble) { case (total, edge) =>
505+ // We compute the success probability `p` for this edge, and its derivative `dp`.
506+ val (p, dp) = if (usePastRelaysData) {
507+ balances.get(edge).canSendAndDerivative(MilliSatoshi (x), now)
508+ } else {
509+ val c = (edge.capacity - usedCapacity.getOrElse(edge.desc.shortChannelId, 0 msat)).toLong.toDouble
510+ (1.0 - x.toDouble / c, - 1.0 / c)
511+ }
512+ total + dp / p
486513 }
487514 if (d > 0.0 ) {
488- low = mid
515+ low = x
489516 } else {
490- high = mid
517+ high = x
491518 }
492519 }
493520 MilliSatoshi (high)
0 commit comments