Skip to content

Commit

Permalink
Use balance estimates from past payments in path-finding
Browse files Browse the repository at this point in the history
  • Loading branch information
thomash-acinq committed Jun 20, 2022
1 parent 6b2e415 commit abb263e
Show file tree
Hide file tree
Showing 9 changed files with 261 additions and 243 deletions.
2 changes: 2 additions & 0 deletions eclair-core/src/main/resources/reference.conf
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,8 @@ eclair {
// probability of success, however is penalizes less the paths with a low probability of success.
use-log-probability = false

use-past-relay-data = false

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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,7 @@ object NodeParams extends Logging {
failureCost = getRelayFees(config.getConfig("failure-cost")),
hopCost = getRelayFees(config.getConfig("hop-cost")),
useLogProbability = config.getBoolean("use-log-probability"),
usePastRelaysData = config.getBoolean("use-past-relay-data"),
))
},
mpp = MultiPartParams(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ object EclairInternalsSerializer {
("lockedFundsRisk" | double) ::
("failureCost" | relayFeesCodec) ::
("hopCost" | relayFeesCodec) ::
("useLogProbability" | bool(8))).as[HeuristicsConstants]
("useLogProbability" | bool(8)) ::
("usePastRelaysData" | bool(8))).as[HeuristicsConstants]

val multiPartParamsCodec: Codec[MultiPartParams] = (
("minPartAmount" | millisatoshi) ::
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,8 @@ object BalanceEstimate {
case class BalancesEstimates(balances: Map[(PublicKey, PublicKey), BalanceEstimate], defaultHalfLife: FiniteDuration) {
private def get(a: PublicKey, b: PublicKey): Option[BalanceEstimate] = balances.get((a, b))

def get(edge: GraphEdge): BalanceEstimate = get(edge.desc.a, edge.desc.b).getOrElse(BalanceEstimate.empty(defaultHalfLife).addEdge(edge))

def addEdge(edge: GraphEdge): BalancesEstimates = BalancesEstimates(
balances.updatedWith((edge.desc.a, edge.desc.b))(balance =>
Some(balance.getOrElse(BalanceEstimate.empty(defaultHalfLife)).addEdge(edge))
Expand Down Expand Up @@ -283,7 +285,7 @@ case class BalancesEstimates(balances: Map[(PublicKey, PublicKey), BalanceEstima

}

case class GraphWithBalanceEstimates(graph: DirectedGraph, private val balances: BalancesEstimates) {
case class GraphWithBalanceEstimates(graph: DirectedGraph, balances: BalancesEstimates) {
def addEdge(edge: GraphEdge): GraphWithBalanceEstimates = GraphWithBalanceEstimates(graph.addEdge(edge), balances.addEdge(edge))

def removeEdge(desc: ChannelDesc): GraphWithBalanceEstimates = GraphWithBalanceEstimates(graph.removeEdge(desc), balances.removeEdge(desc))
Expand Down Expand Up @@ -312,13 +314,6 @@ case class GraphWithBalanceEstimates(graph: DirectedGraph, private val balances:
def channelCouldNotSend(hop: ChannelHop, amount: MilliSatoshi): GraphWithBalanceEstimates = {
GraphWithBalanceEstimates(graph, balances.channelCouldNotSend(hop, amount))
}

def canSend(amount: MilliSatoshi, edge: GraphEdge): Double = {
balances.balances.get((edge.desc.a, edge.desc.b)) match {
case Some(estimate) => estimate.canSend(amount)
case None => BalanceEstimate.empty(1 hour).addEdge(edge).canSend(amount)
}
}
}

object GraphWithBalanceEstimates {
Expand Down
120 changes: 66 additions & 54 deletions eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,9 @@ object RouteCalculation {
val tags = TagSet.Empty.withTag(Tags.MultiPart, r.allowMultiPart).withTag(Tags.Amount, Tags.amountBucket(r.amount))
KamonExt.time(Metrics.FindRouteDuration.withTags(tags.withTag(Tags.NumberOfRoutes, routesToFind.toLong))) {
val result = if (r.allowMultiPart) {
findMultiPartRoute(d.graphWithBalances.graph, r.source, r.target, r.amount, r.maxFee, extraEdges, ignoredEdges, r.ignore.nodes, r.pendingPayments, params, currentBlockHeight)
findMultiPartRoute(d.graphWithBalances, r.source, r.target, r.amount, r.maxFee, extraEdges, ignoredEdges, r.ignore.nodes, r.pendingPayments, params, currentBlockHeight)
} else {
findRoute(d.graphWithBalances.graph, r.source, r.target, r.amount, r.maxFee, routesToFind, extraEdges, ignoredEdges, r.ignore.nodes, params, currentBlockHeight)
findRoute(d.graphWithBalances, r.source, r.target, r.amount, r.maxFee, routesToFind, extraEdges, ignoredEdges, r.ignore.nodes, params, currentBlockHeight)
}
result match {
case Success(routes) =>
Expand Down Expand Up @@ -204,7 +204,7 @@ object RouteCalculation {
* @param routeParams a set of parameters that can restrict the route search
* @return the computed routes to the destination @param targetNodeId
*/
def findRoute(g: DirectedGraph,
def findRoute(g: GraphWithBalanceEstimates,
localNodeId: PublicKey,
targetNodeId: PublicKey,
amount: MilliSatoshi,
Expand All @@ -222,7 +222,7 @@ object RouteCalculation {
}

@tailrec
private def findRouteInternal(g: DirectedGraph,
private def findRouteInternal(g: GraphWithBalanceEstimates,
localNodeId: PublicKey,
targetNodeId: PublicKey,
amount: MilliSatoshi,
Expand Down Expand Up @@ -280,7 +280,7 @@ object RouteCalculation {
* @param routeParams a set of parameters that can restrict the route search
* @return a set of disjoint routes to the destination @param targetNodeId with the payment amount split between them
*/
def findMultiPartRoute(g: DirectedGraph,
def findMultiPartRoute(g: GraphWithBalanceEstimates,
localNodeId: PublicKey,
targetNodeId: PublicKey,
amount: MilliSatoshi,
Expand All @@ -304,7 +304,7 @@ object RouteCalculation {
}
}

private def findMultiPartRouteInternal(g: DirectedGraph,
private def findMultiPartRouteInternal(g: GraphWithBalanceEstimates,
localNodeId: PublicKey,
targetNodeId: PublicKey,
amount: MilliSatoshi,
Expand All @@ -319,7 +319,7 @@ object RouteCalculation {
// 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 {
val directChannels = g.graph.getEdgesBetween(localNodeId, targetNodeId).collect {
// We should always have balance information available for local channels.
// NB: htlcMinimumMsat is set by our peer and may be 0 msat (even though it's not recommended).
case GraphEdge(_, params, _, Some(balance)) => DirectChannel(balance, balance <= 0.msat || balance < params.htlcMinimum)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -277,16 +277,16 @@ class BalanceEstimateSpec extends AnyFunSuite {
val edge_ab = makeEdge(a, b, 1, 10 sat)
val edge_ba = makeEdge(b, a, 1, 10 sat)
val edge_bc = makeEdge(b, c, 6, 10 sat)
assert(graphWithBalances.canSend(27500 msat, edge_ab) === 0.75 +- 0.01)
assert(graphWithBalances.canSend(55000 msat, edge_ab) === 0.5 +- 0.01)
assert(graphWithBalances.canSend(30000 msat, edge_ba) === 0.75 +- 0.01)
assert(graphWithBalances.canSend(60000 msat, edge_ba) === 0.5 +- 0.01)
assert(graphWithBalances.canSend(75000 msat, edge_bc) === 0.5 +- 0.01)
assert(graphWithBalances.canSend(100000 msat, edge_bc) === 0.33 +- 0.01)
assert(graphWithBalances.balances.get(edge_ab).canSend(27500 msat) === 0.75 +- 0.01)
assert(graphWithBalances.balances.get(edge_ab).canSend(55000 msat) === 0.5 +- 0.01)
assert(graphWithBalances.balances.get(edge_ba).canSend(30000 msat) === 0.75 +- 0.01)
assert(graphWithBalances.balances.get(edge_ba).canSend(60000 msat) === 0.5 +- 0.01)
assert(graphWithBalances.balances.get(edge_bc).canSend(75000 msat) === 0.5 +- 0.01)
assert(graphWithBalances.balances.get(edge_bc).canSend(100000 msat) === 0.33 +- 0.01)
val unknownEdge = makeEdge(42, 40 sat)
assert(graphWithBalances.canSend(10000 msat, unknownEdge) === 0.75 +- 0.01)
assert(graphWithBalances.canSend(20000 msat, unknownEdge) === 0.5 +- 0.01)
assert(graphWithBalances.canSend(30000 msat, unknownEdge) === 0.25 +- 0.01)
assert(graphWithBalances.balances.get(unknownEdge).canSend(10000 msat) === 0.75 +- 0.01)
assert(graphWithBalances.balances.get(unknownEdge).canSend(20000 msat) === 0.5 +- 0.01)
assert(graphWithBalances.balances.get(unknownEdge).canSend(30000 msat) === 0.25 +- 0.01)
}

}
24 changes: 13 additions & 11 deletions eclair-core/src/test/scala/fr/acinq/eclair/router/GraphSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong, ShortChannelId}
import org.scalatest.funsuite.AnyFunSuite
import scodec.bits._

import scala.concurrent.duration.DurationInt

class GraphSpec extends AnyFunSuite {

val (a, b, c, d, e, f, g, h) = (
Expand Down Expand Up @@ -168,8 +170,8 @@ class GraphSpec extends AnyFunSuite {

val bIncoming = graph.getIncomingEdgesOf(b)
assert(bIncoming.size == 1)
assert(bIncoming.exists(_.desc.a == a)) // there should be an edge a --> b
assert(bIncoming.exists(_.desc.b == b))
assert(bIncoming(a).exists(_.desc.a == a)) // there should be an edge a --> b
assert(bIncoming(a).exists(_.desc.b == b))

val bOutgoing = graph.edgesOf(b)
assert(bOutgoing.size == 1)
Expand Down Expand Up @@ -221,18 +223,18 @@ class GraphSpec extends AnyFunSuite {
val graph = DirectedGraph(Seq(edgeAB, edgeAD, edgeBC, edgeDC))

assert(graph.edgesOf(a).toSet == Set(edgeAB, edgeAD))
assert(graph.getIncomingEdgesOf(a) == Nil)
assert(graph.getIncomingEdgesOf(a).isEmpty)
assert(graph.edgesOf(c) == Nil)
assert(graph.getIncomingEdgesOf(c).toSet == Set(edgeBC, edgeDC))
assert(graph.getIncomingEdgesOf(c).values.flatten.toSet == Set(edgeBC, edgeDC))

val edgeAB1 = edgeAB.copy(balance_opt = Some(200000 msat))
val edgeBC1 = edgeBC.copy(balance_opt = Some(150000 msat))
val graph1 = graph.addEdge(edgeAB1).addEdge(edgeBC1)

assert(graph1.edgesOf(a).toSet == Set(edgeAB1, edgeAD))
assert(graph1.getIncomingEdgesOf(a) == Nil)
assert(graph1.getIncomingEdgesOf(a).isEmpty)
assert(graph1.edgesOf(c) == Nil)
assert(graph1.getIncomingEdgesOf(c).toSet == Set(edgeBC1, edgeDC))
assert(graph1.getIncomingEdgesOf(c).values.flatten.toSet == Set(edgeBC1, edgeDC))
}

def descFromNodes(shortChannelId: Long, a: PublicKey, b: PublicKey): ChannelDesc = makeEdge(shortChannelId, a, b, 0 msat, 0).desc
Expand All @@ -256,9 +258,9 @@ class GraphSpec extends AnyFunSuite {
val edgeDE = makeEdge(6L, d, e, 9 msat, 0, capacity = 200000 sat)
val graph = DirectedGraph(Seq(edgeAB, edgeBC, edgeCD, edgeDC, edgeCE, edgeDE))

val path :: Nil = yenKshortestPaths(graph, a, e, 100000000 msat,
val path :: Nil = yenKshortestPaths(GraphWithBalanceEstimates(graph, 1 day), a, e, 100000000 msat,
Set.empty, Set.empty, Set.empty, 1,
Right(HeuristicsConstants(1.0E-8, RelayFees(2000 msat, 500), RelayFees(50 msat, 20), useLogProbability = true)),
Right(HeuristicsConstants(1.0E-8, RelayFees(2000 msat, 500), RelayFees(50 msat, 20), useLogProbability = true, usePastRelaysData = false)),
BlockHeight(714930), _ => true, includeLocalChannelCost = true)
assert(path.path == Seq(edgeAB, edgeBC, edgeCE))
}
Expand All @@ -280,7 +282,7 @@ class GraphSpec extends AnyFunSuite {
val edgeDE = makeEdge(6L, d, e, 1 msat, 0, capacity = 200000 sat)
val graph = DirectedGraph(Seq(edgeAB, edgeBC, edgeCD, edgeDC, edgeCE, edgeDE))

val paths = yenKshortestPaths(graph, a, e, 90000000 msat,
val paths = yenKshortestPaths(GraphWithBalanceEstimates(graph, 1 day), a, e, 90000000 msat,
Set.empty, Set.empty, Set.empty, 2,
Left(WeightRatios(1, 0, 0, 0, RelayFees(0 msat, 0))),
BlockHeight(714930), _ => true, includeLocalChannelCost = true)
Expand All @@ -306,7 +308,7 @@ class GraphSpec extends AnyFunSuite {
val edgeDE = makeEdge(6L, d, e, 1 msat, 0, capacity = 200000 sat)
val graph = DirectedGraph(Seq(edgeAB, edgeBC, edgeCD, edgeDC, edgeCE, edgeDE))

val paths = yenKshortestPaths(graph, a, e, 90000000 msat,
val paths = yenKshortestPaths(GraphWithBalanceEstimates(graph, 1 day), a, e, 90000000 msat,
Set.empty, Set.empty, Set.empty, 2,
Left(WeightRatios(1, 0, 0, 0, RelayFees(0 msat, 0))),
BlockHeight(714930), _ => true, includeLocalChannelCost = true)
Expand Down Expand Up @@ -339,7 +341,7 @@ class GraphSpec extends AnyFunSuite {
val edgeGH = makeEdge(9L, g, h, 2 msat, 0, capacity = 100000 sat, minHtlc = 1000 msat)
val graph = DirectedGraph(Seq(edgeCD, edgeDF, edgeCE, edgeED, edgeEF, edgeFG, edgeFH, edgeEG, edgeGH))

val paths = yenKshortestPaths(graph, c, h, 10000000 msat,
val paths = yenKshortestPaths(GraphWithBalanceEstimates(graph, 1 day), c, h, 10000000 msat,
Set.empty, Set.empty, Set.empty, 3,
Left(WeightRatios(1, 0, 0, 0, RelayFees(0 msat, 0))),
BlockHeight(714930), _ => true, includeLocalChannelCost = true)
Expand Down
Loading

0 comments on commit abb263e

Please sign in to comment.