Skip to content

Commit 273fae9

Browse files
Add success probabilities in path finding (#1942)
Add an alternative heuristic for path finding that combines the relay fees with virtual fees for hops, funds locked and failed payments.
1 parent 8b29edb commit 273fae9

File tree

15 files changed

+219
-106
lines changed

15 files changed

+219
-106
lines changed

docs/release-notes/eclair-vnext.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ This release contains many API updates:
127127
- `payinvoice`, `sendtonode`, `findroute`, `findroutetonode` and `findroutebetweennodes` let you specify `--pathFindingExperimentName` when using path-finding A/B testing (#1930)
128128
- the `--maxFeePct` parameter used in `payinvoice` and `sendtonode` must now be an integer between 0 and 100: it was previously a value between 0 and 1, which was misleading for a percentage (#1930)
129129
- `findroute`, `findroutetonode` and `findroutebetweennodes` let you choose the format of the route returned with the `--routeFormat` parameter (supported values are `nodeId` and `shortChannelId`) (#1943)
130+
- `findroute`, `findroutetonode` and `findroutebetweennodes` now accept `--includeLocalChannelCost` to specify if you want to count the fees from your node like trampoline payments do (#1942)
130131

131132
Have a look at our [API documentation](https://acinq.github.io/eclair) for more details.
132133

eclair-core/src/main/resources/reference.conf

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,7 @@ eclair {
215215
channel-age = 0.4 // when computing the weight for a channel, consider its AGE in this proportion
216216
channel-capacity = 0.55 // when computing the weight for a channel, consider its CAPACITY in this proportion
217217
}
218+
use-ratios = true
218219

219220
// Virtual fee for additional hops
220221
// Corresponds to how much you are willing to pay to get one less hop in the payment path
@@ -240,6 +241,21 @@ eclair {
240241
percentage = 100 // 100% of the traffic use the default configuration
241242
}
242243

244+
// alternative routing heuristics (replaces ratios)
245+
test-failure-cost = ${eclair.router.path-finding.default} {
246+
use-ratios = false
247+
248+
locked-funds-risk = 1e-8 // msat per msat locked per block. It should be your expected interest rate per block multiplied by the probability that something goes wrong and your funds stay locked.
249+
// 1e-8 corresponds to an interest rate of ~5% per year (1e-6 per block) and a probability of 1% that the channel will fail and our funds will be locked.
250+
251+
// Virtual fee for failed payments
252+
// Corresponds to how much you are willing to pay to get one less failed payment attempt
253+
failure-cost {
254+
fee-base-msat = 2000
255+
fee-proportional-millionths = 500
256+
}
257+
}
258+
243259
// Examples of other configs as 0% experiments:
244260

245261
// To optimize for fees only:

eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -122,9 +122,9 @@ trait Eclair {
122122

123123
def sendOnChain(address: String, amount: Satoshi, confirmationTarget: Long): Future[ByteVector32]
124124

125-
def findRoute(targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty)(implicit timeout: Timeout): Future[RouteResponse]
125+
def findRoute(targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, includeLocalChannelCost: Boolean = false)(implicit timeout: Timeout): Future[RouteResponse]
126126

127-
def findRouteBetween(sourceNodeId: PublicKey, targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty)(implicit timeout: Timeout): Future[RouteResponse]
127+
def findRouteBetween(sourceNodeId: PublicKey, targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, includeLocalChannelCost: Boolean = false)(implicit timeout: Timeout): Future[RouteResponse]
128128

129129
def sendToRoute(amount: MilliSatoshi, recipientAmount_opt: Option[MilliSatoshi], externalId_opt: Option[String], parentId_opt: Option[UUID], invoice: PaymentRequest, finalCltvExpiryDelta: CltvExpiryDelta, route: PredefinedRoute, trampolineSecret_opt: Option[ByteVector32] = None, trampolineFees_opt: Option[MilliSatoshi] = None, trampolineExpiryDelta_opt: Option[CltvExpiryDelta] = None, trampolineNodes_opt: Seq[PublicKey] = Nil)(implicit timeout: Timeout): Future[SendPaymentToRouteResponse]
130130

@@ -279,8 +279,8 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging {
279279
}
280280
}
281281

282-
override def findRoute(targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty)(implicit timeout: Timeout): Future[RouteResponse] =
283-
findRouteBetween(appKit.nodeParams.nodeId, targetNodeId, amount, pathFindingExperimentName_opt, assistedRoutes)
282+
override def findRoute(targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, includeLocalChannelCost: Boolean = false)(implicit timeout: Timeout): Future[RouteResponse] =
283+
findRouteBetween(appKit.nodeParams.nodeId, targetNodeId, amount, pathFindingExperimentName_opt, assistedRoutes, includeLocalChannelCost)
284284

285285
private def getRouteParams(pathFindingExperimentName_opt: Option[String]): Option[RouteParams] = {
286286
pathFindingExperimentName_opt match {
@@ -289,11 +289,11 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging {
289289
}
290290
}
291291

292-
override def findRouteBetween(sourceNodeId: PublicKey, targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty)(implicit timeout: Timeout): Future[RouteResponse] = {
292+
override def findRouteBetween(sourceNodeId: PublicKey, targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, includeLocalChannelCost: Boolean = false)(implicit timeout: Timeout): Future[RouteResponse] = {
293293
getRouteParams(pathFindingExperimentName_opt) match {
294294
case Some(routeParams) =>
295295
val maxFee = routeParams.getMaxFee(amount)
296-
(appKit.router ? RouteRequest(sourceNodeId, targetNodeId, amount, maxFee, assistedRoutes, routeParams = routeParams)).mapTo[RouteResponse]
296+
(appKit.router ? RouteRequest(sourceNodeId, targetNodeId, amount, maxFee, assistedRoutes, routeParams = routeParams.copy(includeLocalChannelCost = includeLocalChannelCost))).mapTo[RouteResponse]
297297
case None => Future.failed(new IllegalArgumentException(s"Path-finding experiment ${pathFindingExperimentName_opt.get} does not exist."))
298298
}
299299
}

eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import fr.acinq.eclair.crypto.keymanager.{ChannelKeyManager, NodeKeyManager}
2727
import fr.acinq.eclair.db._
2828
import fr.acinq.eclair.io.PeerConnection
2929
import fr.acinq.eclair.payment.relay.Relayer.{RelayFees, RelayParams}
30-
import fr.acinq.eclair.router.Graph.WeightRatios
30+
import fr.acinq.eclair.router.Graph.{HeuristicsConstants, WeightRatios}
3131
import fr.acinq.eclair.router.PathFindingExperimentConf
3232
import fr.acinq.eclair.router.Router.{MultiPartParams, PathFindingConf, RouterConf, SearchBoundaries}
3333
import fr.acinq.eclair.tor.Socks5ProxyParams
@@ -327,13 +327,21 @@ object NodeParams extends Logging {
327327
maxCltv = CltvExpiryDelta(config.getInt("boundaries.max-cltv")),
328328
maxFeeFlat = Satoshi(config.getLong("boundaries.max-fee-flat-sat")).toMilliSatoshi,
329329
maxFeeProportional = config.getDouble("boundaries.max-fee-proportional-percent") / 100.0),
330-
ratios = WeightRatios(
331-
baseFactor = config.getDouble("ratios.base"),
332-
cltvDeltaFactor = config.getDouble("ratios.cltv"),
333-
ageFactor = config.getDouble("ratios.channel-age"),
334-
capacityFactor = config.getDouble("ratios.channel-capacity"),
335-
hopCost = getRelayFees(config.getConfig("hop-cost")),
336-
),
330+
heuristicsParams = if (config.getBoolean("use-ratios")) {
331+
Left(WeightRatios(
332+
baseFactor = config.getDouble("ratios.base"),
333+
cltvDeltaFactor = config.getDouble("ratios.cltv"),
334+
ageFactor = config.getDouble("ratios.channel-age"),
335+
capacityFactor = config.getDouble("ratios.channel-capacity"),
336+
hopCost = getRelayFees(config.getConfig("hop-cost")),
337+
))
338+
} else {
339+
Right(HeuristicsConstants(
340+
lockedFundsRisk = config.getDouble("locked-funds-risk"),
341+
failureCost = getRelayFees(config.getConfig("failure-cost")),
342+
hopCost = getRelayFees(config.getConfig("hop-cost")),
343+
))
344+
},
337345
mpp = MultiPartParams(
338346
Satoshi(config.getLong("mpp.min-amount-satoshis")).toMilliSatoshi,
339347
config.getInt("mpp.max-parts")),

eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ import fr.acinq.eclair.io.Peer.PeerRoutingMessage
2424
import fr.acinq.eclair.io.Switchboard.RouterPeerConf
2525
import fr.acinq.eclair.io.{ClientSpawner, Peer, PeerConnection, Switchboard}
2626
import fr.acinq.eclair.payment.relay.Relayer.RelayFees
27-
import fr.acinq.eclair.router.Graph.WeightRatios
28-
import fr.acinq.eclair.router.Router.{GossipDecision, MultiPartParams, PathFindingConf, RouteParams, RouterConf, SearchBoundaries, SendChannelQuery}
27+
import fr.acinq.eclair.router.Graph.{HeuristicsConstants, WeightRatios}
28+
import fr.acinq.eclair.router.Router.{GossipDecision, MultiPartParams, PathFindingConf, RouterConf, SearchBoundaries, SendChannelQuery}
2929
import fr.acinq.eclair.router._
3030
import fr.acinq.eclair.wire.protocol.CommonCodecs._
3131
import fr.acinq.eclair.wire.protocol.LightningMessageCodecs._
@@ -50,9 +50,9 @@ object EclairInternalsSerializer {
5050

5151
val searchBoundariesCodec: Codec[SearchBoundaries] = (
5252
("maxFee" | millisatoshi) ::
53-
("maxFeeProportional" | double) ::
54-
("maxRouteLength" | int32) ::
55-
("maxCltv" | int32.as[CltvExpiryDelta])).as[SearchBoundaries]
53+
("maxFeeProportional" | double) ::
54+
("maxRouteLength" | int32) ::
55+
("maxCltv" | int32.as[CltvExpiryDelta])).as[SearchBoundaries]
5656

5757
val relayFeesCodec: Codec[RelayFees] = (
5858
("feeBase" | millisatoshi) ::
@@ -65,14 +65,19 @@ object EclairInternalsSerializer {
6565
("capacityFactor" | double) ::
6666
("hopCost" | relayFeesCodec)).as[WeightRatios]
6767

68+
val heuristicsConstantsCodec: Codec[HeuristicsConstants] = (
69+
("lockedFundsRisk" | double) ::
70+
("failureCost" | relayFeesCodec) ::
71+
("hopCost" | relayFeesCodec)).as[HeuristicsConstants]
72+
6873
val multiPartParamsCodec: Codec[MultiPartParams] = (
6974
("minPartAmount" | millisatoshi) ::
7075
("maxParts" | int32)).as[MultiPartParams]
7176

7277
val pathFindingConfCodec: Codec[PathFindingConf] = (
7378
("randomize" | bool(8)) ::
7479
("boundaries" | searchBoundariesCodec) ::
75-
("ratios" | weightRatiosCodec) ::
80+
("heuristicsParams" | either(bool(8), weightRatiosCodec, heuristicsConstantsCodec)) ::
7681
("mpp" | multiPartParamsCodec) ::
7782
("experimentName" | utf8_32) ::
7883
("experimentPercentage" | int32)).as[PathFindingConf]

0 commit comments

Comments
 (0)