1
0
Fork 0
mirror of https://github.com/ACINQ/eclair.git synced 2025-02-23 22:46:44 +01:00

MPP scale min part amount based on total amount (#1911)

Scale MPP partial amounts based on the total amount we want to
send and the number of parts we allow.

This avoids polluting the results with cheap routes that don't have the
capacity to route when the amount is big.
This commit is contained in:
Bastien Teinturier 2021-08-17 16:35:43 +02:00 committed by GitHub
parent ebed5ad9ea
commit 49e1996391
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 66 additions and 13 deletions

View file

@ -246,7 +246,7 @@ eclair {
mpp { mpp {
min-amount-satoshis = 15000 // minimum amount sent via partial HTLCs min-amount-satoshis = 15000 // minimum amount sent via partial HTLCs
max-parts = 6 // maximum number of HTLCs sent per payment: increasing this value will impact performance max-parts = 5 // maximum number of HTLCs sent per payment: increasing this value will impact performance
} }
} }
} }

View file

@ -338,8 +338,9 @@ object RouteCalculation {
// If we have direct channels to the target, we can use them all. // If we have direct channels to the target, we can use them all.
// We also count empty channels, which allows replacing them with a non-direct route (multiple hops). // We also count empty channels, which allows replacing them with a non-direct route (multiple hops).
val numRoutes = routeParams.mpp.maxParts.max(directChannels.length) 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. // We want to ensure that the set of routes we find have enough capacity to allow sending the total amount,
val minPartAmount = (amount +: routeParams.mpp.minPartAmount +: directChannels.filter(!_.isEmpty).map(_.balance)).min // 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))
} }
findRouteInternal(g, localNodeId, targetNodeId, routeParams1.mpp.minPartAmount, maxFee, routeParams1.mpp.maxParts, extraEdges, ignoredEdges, ignoredVertices, routeParams1, currentBlockHeight) match { findRouteInternal(g, localNodeId, targetNodeId, routeParams1.mpp.minPartAmount, maxFee, routeParams1.mpp.maxParts, extraEdges, ignoredEdges, ignoredVertices, routeParams1, currentBlockHeight) match {

View file

@ -975,12 +975,12 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution {
} }
test("calculate multipart route to neighbor (many channels, known balance)") { test("calculate multipart route to neighbor (many channels, known balance)") {
val amount = 65000 msat val amount = 60000 msat
val g = DirectedGraph(List( val g = DirectedGraph(List(
makeEdge(1L, a, b, 50 msat, 100, minHtlc = 1 msat, balance_opt = Some(15000 msat)), makeEdge(1L, a, b, 50 msat, 100, minHtlc = 1 msat, balance_opt = Some(15000 msat)),
makeEdge(2L, a, b, 15 msat, 10, minHtlc = 1 msat, balance_opt = Some(25000 msat)), makeEdge(2L, a, b, 15 msat, 10, minHtlc = 1 msat, balance_opt = Some(21000 msat)),
makeEdge(3L, a, b, 1 msat, 50, minHtlc = 1 msat, balance_opt = Some(20000 msat)), makeEdge(3L, a, b, 1 msat, 50, minHtlc = 1 msat, balance_opt = Some(17000 msat)),
makeEdge(4L, a, b, 100 msat, 20, minHtlc = 1 msat, balance_opt = Some(10000 msat)), 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. // 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))
@ -998,11 +998,9 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution {
checkRouteAmounts(routes, amount, 0 msat) 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. // We set min-part-amount to a value that excludes channels 1 and 4.
val Success(routes) = findMultiPartRoute(g, a, b, amount, 1 msat, routeParams = routeParams.copy(mpp = MultiPartParams(20000 msat, 3)), currentBlockHeight = 400000) val failure = findMultiPartRoute(g, a, b, amount, 1 msat, routeParams = routeParams.copy(mpp = MultiPartParams(16500 msat, 3)), currentBlockHeight = 400000)
assert(routes.length === 4, routes) assert(failure === Failure(RouteNotFound))
assert(routes.forall(_.length == 1), routes)
checkRouteAmounts(routes, amount, 0 msat)
} }
} }
@ -1352,6 +1350,60 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution {
} }
} }
test("calculate multipart route to remote node (ignore cheap routes with low capacity)") {
//
// +---> B1 -----+
// | |
// +---> B2 -----+
// | |
// +---> ... ----+
// | |
// +---> B10 ----+
// | |
// | v
// A ---> C ---> D
val cheapEdges = (1 to 10).flatMap(i => {
val bi = randomKey().publicKey
List(
makeEdge(2 * i, a, bi, 1 msat, 1, minHtlc = 1 msat, capacity = 1500 sat, balance_opt = Some(1_200_000 msat)),
makeEdge(2 * i + 1, bi, d, 1 msat, 1, minHtlc = 1 msat, capacity = 1500 sat),
)
})
val preferredEdges = List(
makeEdge(100, a, c, 5 msat, 1000, minHtlc = 1 msat, capacity = 25000 sat, balance_opt = Some(20_000_000 msat)),
makeEdge(101, c, d, 5 msat, 1000, minHtlc = 1 msat, capacity = 25000 sat),
)
val g = DirectedGraph(preferredEdges ++ cheapEdges)
{
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 Success(routes) = findMultiPartRoute(g, a, d, amount, maxFee, routeParams = routeParams, currentBlockHeight = 400000)
checkRouteAmounts(routes, amount, maxFee)
assert(routes2Ids(routes) === Set(Seq(100L, 101L)))
}
{
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 failure = findMultiPartRoute(g, a, d, amount, maxFee, routeParams = routeParams, currentBlockHeight = 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 Success(routes) = findMultiPartRoute(g, a, d, amount, maxFee, routeParams = routeParams, currentBlockHeight = 400000)
assert(routes.length === 5)
routes.foreach(route => {
assert(route.length === 2)
assert(route.amount <= 1_200_000.msat)
assert(!route.hops.flatMap(h => Seq(h.nodeId, h.nextNodeId)).contains(c))
})
}
}
test("calculate multipart route to remote node (ignored channels and nodes)") { test("calculate multipart route to remote node (ignored channels and nodes)") {
// +----- B --xxx-- C -----+ // +----- B --xxx-- C -----+
// | +-------- D --------+ | // | +-------- D --------+ |
@ -1737,7 +1789,7 @@ object RouteCalculationSpec {
val DEFAULT_CAPACITY = 100000 sat val DEFAULT_CAPACITY = 100000 sat
val NO_WEIGHT_RATIOS: WeightRatios = WeightRatios(1, 0, 0, 0, 0 msat, 0) val NO_WEIGHT_RATIOS: WeightRatios = WeightRatios(1, 0, 0, 0, 0 msat, 0)
val DEFAULT_ROUTE_PARAMS = RouteParams(randomize = false, 21000 msat, 0.03, 6, CltvExpiryDelta(2016), NO_WEIGHT_RATIOS, MultiPartParams(1000 msat, 10), false) val DEFAULT_ROUTE_PARAMS = RouteParams(randomize = false, 21000 msat, 0.03, 6, CltvExpiryDelta(2016), NO_WEIGHT_RATIOS, MultiPartParams(1000 msat, 10), includeLocalChannelCost = false)
val DUMMY_SIG = Transactions.PlaceHolderSig val DUMMY_SIG = Transactions.PlaceHolderSig