From 4981660eaa87d14c4e4916cae9f68d2526177a52 Mon Sep 17 00:00:00 2001 From: Thomas HUET Date: Tue, 5 Dec 2023 16:03:37 +0100 Subject: [PATCH] Split MPP by maximizing expected delivered amount As suggested by @renepickhardt in https://github.com/ACINQ/eclair/pull/2785 --- docs/release-notes/eclair-vnext.md | 6 ++ eclair-core/src/main/resources/reference.conf | 1 + .../scala/fr/acinq/eclair/NodeParams.scala | 7 +- .../remote/EclairInternalsSerializer.scala | 12 +++- .../eclair/router/RouteCalculation.scala | 43 ++++++++---- .../scala/fr/acinq/eclair/router/Router.scala | 15 +++- .../scala/fr/acinq/eclair/TestConstants.scala | 2 + .../eclair/integration/IntegrationSpec.scala | 2 +- .../MultiPartPaymentLifecycleSpec.scala | 2 +- .../eclair/payment/PaymentLifecycleSpec.scala | 2 +- .../eclair/router/RouteCalculationSpec.scala | 68 ++++++++++++++++--- 11 files changed, 133 insertions(+), 27 deletions(-) diff --git a/docs/release-notes/eclair-vnext.md b/docs/release-notes/eclair-vnext.md index 56ac5d4413..5624aae051 100644 --- a/docs/release-notes/eclair-vnext.md +++ b/docs/release-notes/eclair-vnext.md @@ -52,6 +52,12 @@ eclair.channel.channel-update { This feature leaks a bit of information about the balance when the channel is almost empty, if you do not wish to use it, set `eclair.channel.channel-update.balance-thresholds = []`. +### New MPP splitting strategy + +Eclair can send large payments using multiple low-capacity routes by sending as much as it can through each route (if `randomize-route-selection = false`) or some random fraction (if `randomize-route-selection = true`). +These splitting strategies are now specified using `mpp.splitting-strategy = "full-capacity"` or `mpp.splitting-strategy = "randomize"`. +In addition, a new strategy is available: `mpp.splitting-strategy = "max-expected-amount"` will send through each route the amount that maximizes the expected delivered amount (amount sent times success probability). + ### API changes - `bumpforceclose` can be used to make a force-close confirm faster, by spending the anchor output (#2743) diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 82fe9de1d5..4dbe8a5092 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -394,6 +394,7 @@ eclair { mpp { min-amount-satoshis = 15000 // minimum amount sent via partial HTLCs max-parts = 5 // maximum number of HTLCs sent per payment: increasing this value will impact performance + splitting-strategy = "randomize" // Can be either "full-capacity", "randomize" or "max-expected-amount" } } 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 90e108e96c..af68bfa7d7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -428,7 +428,12 @@ object NodeParams extends Logging { }, mpp = MultiPartParams( Satoshi(config.getLong("mpp.min-amount-satoshis")).toMilliSatoshi, - config.getInt("mpp.max-parts")), + config.getInt("mpp.max-parts"), + config.getString("mpp.splitting-strategy") match { + case "full-capacity" => MultiPartParams.FullCapacity + case "randomize" => MultiPartParams.Randomize + case "max-expected-amount" => MultiPartParams.MaxExpectedAmount + }), experimentName = name, experimentPercentage = config.getInt("percentage")) 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 4fb6e472cb..e960cf928f 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 @@ -72,7 +72,17 @@ object EclairInternalsSerializer { val multiPartParamsCodec: Codec[MultiPartParams] = ( ("minPartAmount" | millisatoshi) :: - ("maxParts" | int32)).as[MultiPartParams] + ("maxParts" | int32) :: + ("splittingStrategy" | int8.narrow[MultiPartParams.SplittingStrategy]({ + case 0 => Attempt.successful(MultiPartParams.FullCapacity) + case 1 => Attempt.successful(MultiPartParams.Randomize) + case 2 => Attempt.successful(MultiPartParams.MaxExpectedAmount) + case n => Attempt.failure(Err(s"Invalid value $n")) + }, { + case MultiPartParams.FullCapacity => 0 + case MultiPartParams.Randomize => 1 + case MultiPartParams.MaxExpectedAmount => 2 + }))).as[MultiPartParams] val pathFindingConfCodec: Codec[PathFindingConf] = ( ("randomize" | bool(8)) :: 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 b0b1e4c4c9..bb472bb9fe 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 @@ -22,7 +22,6 @@ import com.softwaremill.quicklens.ModifyPimp import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.Logs.LogCategory import fr.acinq.eclair._ -import fr.acinq.eclair.message.SendingMessage import fr.acinq.eclair.payment.send._ import fr.acinq.eclair.router.Graph.GraphStructure.DirectedGraph.graphEdgeToHop import fr.acinq.eclair.router.Graph.GraphStructure.{DirectedGraph, GraphEdge} @@ -420,7 +419,7 @@ object RouteCalculation { // We want to ensure that the set of routes we find have enough capacity to allow sending the total amount, // without excluding routes with small capacity when the total amount is small. val minPartAmount = routeParams.mpp.minPartAmount.max(amount / numRoutes).min(amount) - routeParams.copy(mpp = MultiPartParams(minPartAmount, numRoutes)) + routeParams.copy(mpp = MultiPartParams(minPartAmount, numRoutes, routeParams.mpp.splittingStrategy)) } findRouteInternal(g, localNodeId, targetNodeId, routeParams1.mpp.minPartAmount, maxFee, routeParams1.mpp.maxParts, extraEdges, ignoredEdges, ignoredVertices, routeParams1, currentBlockHeight) match { case Right(routes) => @@ -446,16 +445,16 @@ object RouteCalculation { // this route doesn't have enough capacity left: we remove it and continue. split(amount, paths, usedCapacity, routeParams, selectedRoutes) } else { - val route = if (routeParams.randomize) { - // randomly choose the amount to be between 20% and 100% of the available capacity. - val randomizedAmount = candidate.amount * ((20d + Random.nextInt(81)) / 100) - if (randomizedAmount < routeParams.mpp.minPartAmount) { - candidate.copy(amount = routeParams.mpp.minPartAmount.min(amount)) - } else { - candidate.copy(amount = randomizedAmount.min(amount)) - } - } else { - candidate.copy(amount = candidate.amount.min(amount)) + val route = routeParams.mpp.splittingStrategy match { + case MultiPartParams.Randomize => + // randomly choose the amount to be between 20% and 100% of the available capacity. + val randomizedAmount = candidate.amount * ((20d + Random.nextInt(81)) / 100) + candidate.copy(amount = randomizedAmount.max(routeParams.mpp.minPartAmount).min(amount)) + case MultiPartParams.MaxExpectedAmount => + val bestAmount = optimizeExpectedValue(current.path, candidate.amount, usedCapacity) + candidate.copy(amount = bestAmount.max(routeParams.mpp.minPartAmount).min(amount)) + case MultiPartParams.FullCapacity => + candidate.copy(amount = candidate.amount.min(amount)) } updateUsedCapacity(route, usedCapacity) // NB: we re-enqueue the current path, it may still have capacity for a second HTLC. @@ -464,6 +463,26 @@ object RouteCalculation { } } + private def optimizeExpectedValue(route: Seq[GraphEdge], capacity: MilliSatoshi, usedCapacity: mutable.Map[ShortChannelId, MilliSatoshi]): MilliSatoshi = { + // We search the maximum value of a polynomial between its two smallest roots (0 and the minimum channel capacity on the path). + // We use binary search to find where the derivative changes sign. + var low = 1L + var high = capacity.toLong + while (high - low > 1L) { + val mid = (high + low) / 2 + val d = route.drop(1).foldLeft(1.0 / mid) { case (x, edge) => + val availableCapacity = edge.capacity - usedCapacity.getOrElse(edge.desc.shortChannelId, 0 msat) + x - 1.0 / (availableCapacity.toLong - mid) + } + if (d > 0.0) { + low = mid + } else { + high = mid + } + } + MilliSatoshi(high) + } + /** Compute the maximum amount that we can send through the given route. */ private def computeRouteMaxAmount(route: Seq[GraphEdge], usedCapacity: mutable.Map[ShortChannelId, MilliSatoshi]): Route = { val firstHopMaxAmount = route.head.maxHtlcAmount(usedCapacity.getOrElse(route.head.desc.shortChannelId, 0 msat)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala index 9077c3e39e..ed39ee22c9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala @@ -551,7 +551,20 @@ object Router { override def fee(amount: MilliSatoshi): MilliSatoshi = fee } - case class MultiPartParams(minPartAmount: MilliSatoshi, maxParts: Int) + object MultiPartParams { + sealed trait SplittingStrategy + + /** Send the full capacity of the route */ + object FullCapacity extends SplittingStrategy + + /** Send between 20% and 100% of the capacity of the route */ + object Randomize extends SplittingStrategy + + /** Maximize the expected delivered amount */ + object MaxExpectedAmount extends SplittingStrategy + } + + case class MultiPartParams(minPartAmount: MilliSatoshi, maxParts: Int, splittingStrategy: MultiPartParams.SplittingStrategy) case class RouteParams(randomize: Boolean, boundaries: SearchBoundaries, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala index 19ad419cf3..b1edff81a4 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -204,6 +204,7 @@ object TestConstants { mpp = MultiPartParams( minPartAmount = 15000000 msat, maxParts = 10, + splittingStrategy = MultiPartParams.FullCapacity ), experimentName = "alice-test-experiment", experimentPercentage = 100))), @@ -369,6 +370,7 @@ object TestConstants { mpp = MultiPartParams( minPartAmount = 15000000 msat, maxParts = 10, + splittingStrategy = MultiPartParams.FullCapacity ), experimentName = "bob-test-experiment", experimentPercentage = 100))), diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala index 402783bf2d..3d3908bcf9 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala @@ -64,7 +64,7 @@ abstract class IntegrationSpec extends TestKitBaseClass with BitcoindService wit capacityFactor = 0, hopCost = RelayFees(0 msat, 0), )), - mpp = MultiPartParams(15000000 msat, 6), + mpp = MultiPartParams(15000000 msat, 6, MultiPartParams.FullCapacity), experimentName = "my-test-experiment", experimentPercentage = 100 ).getDefaultRouteParams diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala index ee5da5b8ff..443673f4ab 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala @@ -723,7 +723,7 @@ object MultiPartPaymentLifecycleSpec { 6, CltvExpiryDelta(1008)), Left(WeightRatios(1, 0, 0, 0, RelayFees(0 msat, 0))), - MultiPartParams(1000 msat, 5), + MultiPartParams(1000 msat, 5, MultiPartParams.FullCapacity), experimentName = "my-test-experiment", experimentPercentage = 100 ).getDefaultRouteParams diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala index 0022597a83..46ac8ba64f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala @@ -279,7 +279,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { randomize = false, boundaries = SearchBoundaries(100 msat, 0.0, 20, CltvExpiryDelta(2016)), Left(WeightRatios(1, 0, 0, 0, RelayFees(0 msat, 0))), - MultiPartParams(10_000 msat, 5), + MultiPartParams(10_000 msat, 5, MultiPartParams.FullCapacity), "my-test-experiment", experimentPercentage = 100 ).getDefaultRouteParams 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 edec7c97a9..fa93f40c8d 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 @@ -969,7 +969,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(4L, a, b, 100 msat, 20, minHtlc = 1 msat, balance_opt = Some(16000 msat)), )) // We set max-parts to 3, but it should be ignored when sending to a direct neighbor. - val routeParams = DEFAULT_ROUTE_PARAMS.copy(mpp = MultiPartParams(2500 msat, 3)) + val routeParams = DEFAULT_ROUTE_PARAMS.copy(mpp = MultiPartParams(2500 msat, 3, DEFAULT_ROUTE_PARAMS.mpp.splittingStrategy)) { val Success(routes) = findMultiPartRoute(g, a, b, amount, 1 msat, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) @@ -985,7 +985,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { } { // We set min-part-amount to a value that excludes channels 1 and 4. - val failure = findMultiPartRoute(g, a, b, amount, 1 msat, routeParams = routeParams.copy(mpp = MultiPartParams(16500 msat, 3)), currentBlockHeight = BlockHeight(400000)) + val failure = findMultiPartRoute(g, a, b, amount, 1 msat, routeParams = routeParams.copy(mpp = MultiPartParams(16500 msat, 3, routeParams.mpp.splittingStrategy)), currentBlockHeight = BlockHeight(400000)) assert(failure == Failure(RouteNotFound)) } } @@ -1112,7 +1112,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val amount = 30000 msat - val routeParams = DEFAULT_ROUTE_PARAMS.copy(mpp = MultiPartParams(2500 msat, 5)) + val routeParams = DEFAULT_ROUTE_PARAMS.copy(mpp = MultiPartParams(2500 msat, 5, DEFAULT_ROUTE_PARAMS.mpp.splittingStrategy)) val Success(routes) = findMultiPartRoute(g, a, b, amount, 1 msat, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) assert(routes.forall(_.hops.length == 1), routes) assert(routes.length == 3, routes) @@ -1244,7 +1244,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { // | | // +--- B --- D ---+ // Our balance and the amount we want to send are below the minimum part amount. - val routeParams = DEFAULT_ROUTE_PARAMS.copy(mpp = MultiPartParams(5000 msat, 5)) + val routeParams = DEFAULT_ROUTE_PARAMS.copy(mpp = MultiPartParams(5000 msat, 5, DEFAULT_ROUTE_PARAMS.mpp.splittingStrategy)) val g = DirectedGraph(List( makeEdge(1L, a, b, 50 msat, 100, minHtlc = 1 msat, balance_opt = Some(1500 msat)), makeEdge(2L, b, d, 15 msat, 0, minHtlc = 1 msat, capacity = 25 sat), @@ -1364,7 +1364,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { { val amount = 15_000_000 msat val maxFee = 50_000 msat // this fee is enough to go through the preferred route - val routeParams = DEFAULT_ROUTE_PARAMS.copy(randomize = false, mpp = MultiPartParams(50_000 msat, 5)) + val routeParams = DEFAULT_ROUTE_PARAMS.copy(randomize = false, mpp = MultiPartParams(50_000 msat, 5, DEFAULT_ROUTE_PARAMS.mpp.splittingStrategy)) val Success(routes) = findMultiPartRoute(g, a, d, amount, maxFee, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) checkRouteAmounts(routes, amount, maxFee) assert(routes2Ids(routes) == Set(Seq(100L, 101L))) @@ -1372,14 +1372,14 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { { val amount = 15_000_000 msat val maxFee = 10_000 msat // this fee is too low to go through the preferred route - val routeParams = DEFAULT_ROUTE_PARAMS.copy(randomize = false, mpp = MultiPartParams(50_000 msat, 5)) + val routeParams = DEFAULT_ROUTE_PARAMS.copy(randomize = false, mpp = MultiPartParams(50_000 msat, 5, DEFAULT_ROUTE_PARAMS.mpp.splittingStrategy)) val failure = findMultiPartRoute(g, a, d, amount, maxFee, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) assert(failure == Failure(RouteNotFound)) } { val amount = 5_000_000 msat val maxFee = 10_000 msat // this fee is enough to go through the preferred route, but the cheaper ones can handle it - val routeParams = DEFAULT_ROUTE_PARAMS.copy(randomize = false, mpp = MultiPartParams(50_000 msat, 5)) + val routeParams = DEFAULT_ROUTE_PARAMS.copy(randomize = false, mpp = MultiPartParams(50_000 msat, 5, DEFAULT_ROUTE_PARAMS.mpp.splittingStrategy)) val Success(routes) = findMultiPartRoute(g, a, d, amount, maxFee, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) assert(routes.length == 5) routes.foreach(route => { @@ -1469,7 +1469,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(6L, b, c, 5 msat, 50, minHtlc = 1000 msat, capacity = 20 sat), makeEdge(7L, c, f, 5 msat, 10, minHtlc = 1500 msat, capacity = 50 sat) )) - val routeParams = DEFAULT_ROUTE_PARAMS.copy(mpp = MultiPartParams(1500 msat, 10)) + val routeParams = DEFAULT_ROUTE_PARAMS.copy(mpp = MultiPartParams(1500 msat, 10, DEFAULT_ROUTE_PARAMS.mpp.splittingStrategy)) { val (amount, maxFee) = (15000 msat, 50 msat) @@ -1599,6 +1599,56 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { } } + test("calculate multipart route to remote node using max expected amount splitting strategy") { + // A-------------E + // | | + // +----- B -----+ + // | | + // +----- C ---- + + // | | + // +----- D -----+ + val (amount, maxFee) = (60000 msat, 1000 msat) + val g = DirectedGraph(List( + // The A -> B -> E route is the most economic one, but we already have a pending HTLC in it. + makeEdge(0L, a, e, 50 msat, 0, minHtlc = 100 msat, balance_opt = Some(10000 msat)), + makeEdge(1L, a, b, 50 msat, 0, minHtlc = 100 msat, balance_opt = Some(100000 msat)), + makeEdge(2L, b, e, 50 msat, 0, minHtlc = 100 msat, capacity = 50 sat), + makeEdge(3L, a, c, 50 msat, 0, minHtlc = 100 msat, balance_opt = Some(100000 msat)), + makeEdge(4L, c, e, 50 msat, 0, minHtlc = 100 msat, capacity = 25 sat), + makeEdge(5L, a, d, 50 msat, 0, minHtlc = 100 msat, balance_opt = Some(100000 msat)), + makeEdge(6L, d, e, 50 msat, 0, minHtlc = 100 msat, capacity = 25 sat), + )) + + val routeParams = DEFAULT_ROUTE_PARAMS.copy(mpp = DEFAULT_ROUTE_PARAMS.mpp.copy(splittingStrategy = MultiPartParams.MaxExpectedAmount)) + val Success(routes) = findMultiPartRoute(g, a, e, amount, maxFee, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) + checkRouteAmounts(routes, amount, maxFee) + assert(routes.map(route => (route.amount, route.hops.head.shortChannelId.toLong)).toSet == Set((10000 msat, 0L), (25000 msat, 1L), (12500 msat, 3L), (12500 msat, 5L))) + } + + test("calculate multipart route to remote node using max expected amount splitting strategy, respect minPartAmount") { + // +----- B -----+ + // | | + // A----- C ---- E + // | | + // +----- D -----+ + val (amount, maxFee) = (55000 msat, 1000 msat) + val g = DirectedGraph(List( + // The A -> B -> E route is the most economic one, but we already have a pending HTLC in it. + makeEdge(1L, a, b, 50 msat, 0, minHtlc = 100 msat, balance_opt = Some(100000 msat)), + makeEdge(2L, b, e, 50 msat, 0, minHtlc = 100 msat, capacity = 50 sat), + makeEdge(3L, a, c, 50 msat, 0, minHtlc = 100 msat, balance_opt = Some(100000 msat)), + makeEdge(4L, c, e, 50 msat, 0, minHtlc = 100 msat, capacity = 25 sat), + makeEdge(5L, a, d, 50 msat, 0, minHtlc = 100 msat, balance_opt = Some(100000 msat)), + makeEdge(6L, d, e, 50 msat, 0, minHtlc = 100 msat, capacity = 25 sat), + )) + + val routeParams = DEFAULT_ROUTE_PARAMS.copy(mpp = DEFAULT_ROUTE_PARAMS.mpp.copy(minPartAmount = 15000 msat, splittingStrategy = MultiPartParams.MaxExpectedAmount)) + val Success(routes) = findMultiPartRoute(g, a, e, amount, maxFee, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) + assert(routes.forall(_.hops.length == 2), routes) + checkRouteAmounts(routes, amount, maxFee) + assert(routes.map(route => (route.amount, route.hops.head.shortChannelId.toLong)).toSet == Set((25000 msat, 1L), (15000 msat, 3L), (15000 msat, 5L))) + } + test("loop trap") { // +-----------------+ // | | @@ -1927,7 +1977,7 @@ object RouteCalculationSpec { randomize = false, boundaries = SearchBoundaries(21000 msat, 0.03, 6, CltvExpiryDelta(2016)), Left(NO_WEIGHT_RATIOS), - MultiPartParams(1000 msat, 10), + MultiPartParams(1000 msat, 10, MultiPartParams.FullCapacity), experimentName = "my-test-experiment", experimentPercentage = 100).getDefaultRouteParams