diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala index 2dee8b84a..ca9ed46b9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala @@ -302,15 +302,23 @@ object RouteCalculation { routeParams: RouteParams, currentBlockHeight: Long): Either[RouterException, Seq[Route]] = { // We use Yen's k-shortest paths to find many paths for chunks of the total amount. - val numRoutes = { - val directChannelsCount = g.getEdgesBetween(localNodeId, targetNodeId).length - routeParams.mpp.maxParts.max(directChannelsCount) // if we have direct channels to the target, we can use them all + // When the recipient is a direct peer, we have complete visibility on our local channels so we can use more accurate MPP parameters. + val routeParams1 = { + case class DirectChannel(balance: MilliSatoshi, isEmpty: Boolean) + val directChannels = g.getEdgesBetween(localNodeId, targetNodeId).collect { + // We should always have balance information available for local channels. + case GraphEdge(_, update, _, Some(balance)) => DirectChannel(balance, balance < update.htlcMinimumMsat) + } + // If we have direct channels to the target, we can use them all. + val numRoutes = routeParams.mpp.maxParts.max(directChannels.length) + // If we have direct channels to the target, we can use them all, even if they have only a small balance left. + val minPartAmount = (amount +: routeParams.mpp.minPartAmount +: directChannels.filter(!_.isEmpty).map(_.balance)).min + routeParams.copy(mpp = MultiPartParams(minPartAmount, numRoutes)) } - val routeAmount = routeParams.mpp.minPartAmount.min(amount) - findRouteInternal(g, localNodeId, targetNodeId, routeAmount, maxFee, numRoutes, extraEdges, ignoredEdges, ignoredVertices, routeParams, currentBlockHeight) match { + findRouteInternal(g, localNodeId, targetNodeId, routeParams1.mpp.minPartAmount, maxFee, routeParams1.mpp.maxParts, extraEdges, ignoredEdges, ignoredVertices, routeParams1, currentBlockHeight) match { case Right(routes) => // We use these shortest paths to find a set of non-conflicting HTLCs that send the total amount. - split(amount, mutable.Queue(routes: _*), initializeUsedCapacity(pendingHtlcs), routeParams) match { + split(amount, mutable.Queue(routes: _*), initializeUsedCapacity(pendingHtlcs), routeParams1) match { case Right(routes) if validateMultiPartRoute(amount, maxFee, routes) => Right(routes) case _ => Left(RouteNotFound) } 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 9dac33f27..16f98797e 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 @@ -981,6 +981,13 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { assert(routes.forall(_.length == 1), routes) checkRouteAmounts(routes, amount, 0 msat) } + { + // We set min-part-amount to a value that would exclude channels 1 and 4, but it should be ignored when sending to a direct neighbor. + val Success(routes) = findMultiPartRoute(g, a, b, amount, 1 msat, routeParams = routeParams.copy(mpp = MultiPartParams(20000 msat, 3)), currentBlockHeight = 400000) + assert(routes.length === 4, routes) + assert(routes.forall(_.length == 1), routes) + checkRouteAmounts(routes, amount, 0 msat) + } } test("calculate multipart route to neighbor (single channel, known balance)") {