@@ -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 = {
@@ -429,63 +423,124 @@ object RouteCalculation {
429423 // We want to ensure that the set of routes we find have enough capacity to allow sending the total amount,
430424 // without excluding routes with small capacity when the total amount is small.
431425 val minPartAmount = routeParams.mpp.minPartAmount.max(amount / numRoutes).min(amount)
432- routeParams.copy(mpp = MultiPartParams (minPartAmount, numRoutes))
426+ 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 ( route : Route , maxAmount : 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+ // We build some candidate route but may adjust the amounts later if the don't cover the full amount we need to send.
452+ while (paths.nonEmpty && amountLeft > 0 .msat) {
452453 val current = paths.dequeue()
453454 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 = if (routeParams.randomize) {
459- // randomly choose the amount to be between 20% and 100% of the available capacity.
460- val randomizedAmount = candidate.amount * ((20d + Random .nextInt(81 )) / 100 )
461- if (randomizedAmount < routeParams.mpp.minPartAmount) {
462- candidate.copy(amount = routeParams.mpp.minPartAmount.min(amount))
463- } else {
464- candidate.copy(amount = randomizedAmount.min(amount))
465- }
455+ if (candidate.amount >= routeParams.mpp.minPartAmount.min(amountLeft)) {
456+ val chosenAmount = routeParams.mpp.splittingStrategy match {
457+ case MultiPartParams .Randomize =>
458+ // randomly choose the amount to be between 20% and 100% of the available capacity.
459+ val randomizedAmount = candidate.amount * ((20d + Random .nextInt(81 )) / 100 )
460+ randomizedAmount.max(routeParams.mpp.minPartAmount).min(amountLeft)
461+ case MultiPartParams .MaxExpectedAmount =>
462+ val bestAmount = optimizeExpectedValue(current.path, candidate.amount, usedCapacity, routeParams.heuristics.usePastRelaysData, balances, now)
463+ bestAmount.max(routeParams.mpp.minPartAmount).min(amountLeft)
464+ case MultiPartParams .FullCapacity =>
465+ candidate.amount.min(amountLeft)
466+ }
467+ // We update the route with our chosen amount, which is always smaller than the maximum amount.
468+ val chosenRoute = CandidateRoute (candidate.copy(amount = chosenAmount), candidate.amount)
469+ // But we use the route with its maximum amount when updating the used capacity, because we may use more funds below when adjusting the amounts.
470+ updateUsedCapacity(candidate, usedCapacity)
471+ candidates = chosenRoute :: candidates
472+ amountLeft = amountLeft - chosenAmount
473+ paths.enqueue(current)
474+ }
475+ }
476+ // We adjust the amounts to send through each route.
477+ val totalMaximum = candidates.map(_.maxAmount).sum
478+ if (amountLeft == 0 .msat) {
479+ Right (candidates.map(_.route))
480+ } else if (totalMaximum < amount) {
481+ Left (RouteNotFound )
482+ } else {
483+ val totalChosen = candidates.map(_.route.amount).sum
484+ val additionalFraction = (amount - totalChosen).toLong.toDouble / (totalMaximum - totalChosen).toLong.toDouble
485+ var routes : List [Route ] = Nil
486+ var amountLeft = amount
487+ candidates.foreach { case CandidateRoute (route, maxAmount) =>
488+ if (amountLeft > 0 .msat) {
489+ val additionalAmount = MilliSatoshi (((maxAmount - route.amount).toLong * additionalFraction).ceil.toLong)
490+ val amountToSend = (route.amount + additionalAmount).min(amountLeft)
491+ routes = route.copy(amount = amountToSend) :: routes
492+ amountLeft = amountLeft - amountToSend
493+ }
494+ }
495+ Right (routes)
496+ }
497+ }
498+
499+ private def optimizeExpectedValue (route : Seq [GraphEdge ], capacity : MilliSatoshi , usedCapacity : mutable.Map [ShortChannelId , MilliSatoshi ], usePastRelaysData : Boolean , balances : BalancesEstimates , now : TimestampSecond ): MilliSatoshi = {
500+ // We search the maximum value of a polynomial between its two smallest roots (0 and the minimum channel capacity on the path).
501+ // We use binary search to find where the derivative changes sign.
502+ var low = 1L
503+ var high = capacity.toLong
504+ while (high - low > 1L ) {
505+ val x = (high + low) / 2
506+ val d = route.drop(1 ).foldLeft(1.0 / x.toDouble) { case (total, edge) =>
507+ // We compute the success probability `p` for this edge, and its derivative `dp`.
508+ val (p, dp) = if (usePastRelaysData) {
509+ balances.get(edge).canSendAndDerivative(MilliSatoshi (x), now)
466510 } else {
467- candidate.copy(amount = candidate.amount.min(amount))
511+ // If not using past relays data, we assume that balances are uniformly distributed between 0 and the full capacity of the channel.
512+ val c = (edge.capacity - usedCapacity.getOrElse(edge.desc.shortChannelId, 0 msat)).toLong.toDouble
513+ (1.0 - x.toDouble / c, - 1.0 / c)
468514 }
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)
515+ total + dp / p
516+ }
517+ if (d > 0.0 ) {
518+ low = x
519+ } else {
520+ high = x
472521 }
473522 }
523+ MilliSatoshi (high)
474524 }
475525
476526 /** Compute the maximum amount that we can send through the given route. */
477527 private def computeRouteMaxAmount (route : Seq [GraphEdge ], usedCapacity : mutable.Map [ShortChannelId , MilliSatoshi ]): Route = {
478- val firstHopMaxAmount = route.head.maxHtlcAmount( usedCapacity.getOrElse(route.head.desc.shortChannelId, 0 msat))
528+ val firstHopMaxAmount = maxEdgeAmount( route.head, usedCapacity.getOrElse(route.head.desc.shortChannelId, 0 msat))
479529 val amount = route.drop(1 ).foldLeft(firstHopMaxAmount) { case (amount, edge) =>
480530 // We compute fees going forward instead of backwards. That means we will slightly overestimate the fees of some
481531 // edges, but we will always stay inside the capacity bounds we computed.
482532 val amountMinusFees = amount - edge.fee(amount)
483- val edgeMaxAmount = edge.maxHtlcAmount( usedCapacity.getOrElse(edge.desc.shortChannelId, 0 msat))
533+ val edgeMaxAmount = maxEdgeAmount(edge, usedCapacity.getOrElse(edge.desc.shortChannelId, 0 msat))
484534 amountMinusFees.min(edgeMaxAmount)
485535 }
486536 Route (amount.max(0 msat), route.map(graphEdgeToHop), None )
487537 }
488538
539+ private def maxEdgeAmount (edge : GraphEdge , usedCapacity : MilliSatoshi ): MilliSatoshi = {
540+ val maxBalance = edge.balance_opt.getOrElse(edge.params.htlcMaximum_opt.getOrElse(edge.capacity.toMilliSatoshi))
541+ Seq (Some (maxBalance - usedCapacity), edge.params.htlcMaximum_opt).flatten.min.max(0 msat)
542+ }
543+
489544 /** Initialize known used capacity based on pending HTLCs. */
490545 private def initializeUsedCapacity (pendingHtlcs : Seq [Route ]): mutable.Map [ShortChannelId , MilliSatoshi ] = {
491546 val usedCapacity = mutable.Map .empty[ShortChannelId , MilliSatoshi ]
@@ -497,7 +552,14 @@ object RouteCalculation {
497552
498553 /** Update used capacity by taking into account an HTLC sent to the given route. */
499554 private def updateUsedCapacity (route : Route , usedCapacity : mutable.Map [ShortChannelId , MilliSatoshi ]): Unit = {
500- route.hops.foldRight(route.amount) { case (hop, amount) =>
555+ val finalHopAmount = route.finalHop_opt.map(hop => {
556+ hop match {
557+ case BlindedHop (dummyId, _) => usedCapacity.updateWith(dummyId)(previous => Some (route.amount + previous.getOrElse(0 msat)))
558+ case _ : NodeHop => ()
559+ }
560+ route.amount + hop.fee(route.amount)
561+ }).getOrElse(route.amount)
562+ route.hops.foldRight(finalHopAmount) { case (hop, amount) =>
501563 usedCapacity.updateWith(hop.shortChannelId)(previous => Some (amount + previous.getOrElse(0 msat)))
502564 amount + hop.fee(amount)
503565 }
0 commit comments