Skip to content

Commit e99fa2e

Browse files
Refactor route finding (#2974)
Refactor the route finding to share the same code for payment routes and onion message routes.
1 parent a35a972 commit e99fa2e

20 files changed

+643
-670
lines changed

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -443,7 +443,10 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging {
443443
for {
444444
ignoredChannels <- getChannelDescs(ignoreShortChannelIds.toSet)
445445
ignore = Ignore(ignoreNodeIds.toSet, ignoredChannels)
446-
response <- (appKit.router ? RouteRequest(sourceNodeId, target, routeParams1, ignore)).mapTo[RouteResponse]
446+
response <- appKit.router.toTyped.ask[PaymentRouteResponse](replyTo => RouteRequest(replyTo, sourceNodeId, target, routeParams1, ignore)).flatMap {
447+
case r: RouteResponse => Future.successful(r)
448+
case PaymentRouteNotFound(error) => Future.failed(error)
449+
}
447450
} yield response
448451
case Left(t) => Future.failed(t)
449452
}

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

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import fr.acinq.eclair.message.OnionMessages.OnionMessageConfig
3333
import fr.acinq.eclair.payment.relay.OnTheFlyFunding
3434
import fr.acinq.eclair.payment.relay.Relayer.{AsyncPaymentsParams, RelayFees, RelayParams}
3535
import fr.acinq.eclair.router.Announcements.AddressException
36-
import fr.acinq.eclair.router.Graph.{HeuristicsConstants, WeightRatios}
36+
import fr.acinq.eclair.router.Graph.{HeuristicsConstants, PaymentWeightRatios}
3737
import fr.acinq.eclair.router.Router._
3838
import fr.acinq.eclair.router.{Graph, PathFindingExperimentConf}
3939
import fr.acinq.eclair.tor.Socks5ProxyParams
@@ -450,20 +450,20 @@ object NodeParams extends Logging {
450450
maxFeeFlat = Satoshi(config.getLong("boundaries.max-fee-flat-sat")).toMilliSatoshi,
451451
maxFeeProportional = config.getDouble("boundaries.max-fee-proportional-percent") / 100.0),
452452
heuristics = if (config.getBoolean("use-ratios")) {
453-
Left(WeightRatios(
453+
PaymentWeightRatios(
454454
baseFactor = config.getDouble("ratios.base"),
455455
cltvDeltaFactor = config.getDouble("ratios.cltv"),
456456
ageFactor = config.getDouble("ratios.channel-age"),
457457
capacityFactor = config.getDouble("ratios.channel-capacity"),
458-
hopCost = getRelayFees(config.getConfig("hop-cost")),
459-
))
458+
hopFees = getRelayFees(config.getConfig("hop-cost")),
459+
)
460460
} else {
461-
Right(HeuristicsConstants(
461+
HeuristicsConstants(
462462
lockedFundsRisk = config.getDouble("locked-funds-risk"),
463-
failureCost = getRelayFees(config.getConfig("failure-cost")),
464-
hopCost = getRelayFees(config.getConfig("hop-cost")),
463+
failureFees = getRelayFees(config.getConfig("failure-cost")),
464+
hopFees = getRelayFees(config.getConfig("hop-cost")),
465465
useLogProbability = config.getBoolean("use-log-probability"),
466-
))
466+
)
467467
},
468468
mpp = MultiPartParams(
469469
Satoshi(config.getLong("mpp.min-amount-satoshis")).toMilliSatoshi,
@@ -482,7 +482,7 @@ object NodeParams extends Logging {
482482
val ratioBase = config.getDouble("ratios.base")
483483
val ratioAge = config.getDouble("ratios.channel-age")
484484
val ratioCapacity = config.getDouble("ratios.channel-capacity")
485-
MessageRouteParams(maxRouteLength, Graph.MessagePath.WeightRatios(ratioBase, ratioAge, ratioCapacity))
485+
MessageRouteParams(maxRouteLength, Graph.MessageWeightRatios(ratioBase, ratioAge, ratioCapacity))
486486
}
487487

488488
val unhandledExceptionStrategy = config.getString("channel.unhandled-exception-strategy") match {

eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ package fr.acinq.eclair.payment.receive
1818

1919
import akka.actor.Actor.Receive
2020
import akka.actor.typed.Behavior
21+
import akka.actor.typed.scaladsl.AskPattern.Askable
2122
import akka.actor.typed.scaladsl.Behaviors
22-
import akka.actor.typed.scaladsl.adapter.ClassicActorContextOps
23+
import akka.actor.typed.scaladsl.adapter.{ClassicActorContextOps, ClassicActorRefOps}
2324
import akka.actor.{ActorContext, ActorRef, PoisonPill, typed}
2425
import akka.event.{DiagnosticLoggingAdapter, LoggingAdapter}
25-
import akka.pattern.ask
2626
import akka.util.Timeout
2727
import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey}
2828
import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto}
@@ -37,7 +37,7 @@ import fr.acinq.eclair.payment._
3737
import fr.acinq.eclair.payment.offer.OfferManager
3838
import fr.acinq.eclair.router.BlindedRouteCreation.{aggregatePaymentInfo, createBlindedRouteFromHops, createBlindedRouteWithoutHops}
3939
import fr.acinq.eclair.router.Router
40-
import fr.acinq.eclair.router.Router.{ChannelHop, HopRelayParams}
40+
import fr.acinq.eclair.router.Router.{ChannelHop, HopRelayParams, PaymentRouteResponse}
4141
import fr.acinq.eclair.wire.protocol.OfferTypes.{InvoiceRequest, InvoiceTlv}
4242
import fr.acinq.eclair.wire.protocol.PaymentOnion.FinalPayload
4343
import fr.acinq.eclair.wire.protocol._
@@ -376,8 +376,7 @@ object MultiPartHandler {
376376
val paymentInfo = aggregatePaymentInfo(r.amount, dummyHops, nodeParams.channelConf.minFinalExpiryDelta)
377377
Future.successful(PaymentBlindedRoute(contactInfo, paymentInfo))
378378
} else {
379-
implicit val timeout: Timeout = 10.seconds
380-
r.router.ask(Router.FinalizeRoute(Router.PredefinedNodeRoute(r.amount, route.nodes))).mapTo[Router.RouteResponse].map(routeResponse => {
379+
r.router.toTyped.ask[PaymentRouteResponse](replyTo => Router.FinalizeRoute(replyTo, Router.PredefinedNodeRoute(r.amount, route.nodes)))(10.seconds, context.system.scheduler).mapTo[Router.RouteResponse].map(routeResponse => {
381380
val clearRoute = routeResponse.routes.head
382381
val blindedRoute = createBlindedRouteFromHops(clearRoute.hops ++ dummyHops, r.pathId, nodeParams.channelConf.htlcMinimum, route.maxFinalExpiryDelta.toCltvExpiry(nodeParams.currentBlockHeight))
383382
val contactInfo = route.shortChannelIdDir_opt match {

eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package fr.acinq.eclair.payment.send
1818

19+
import akka.actor.typed.scaladsl.adapter.ClassicActorRefOps
1920
import akka.actor.{ActorRef, FSM, Props, Status}
2021
import akka.event.Logging.MDC
2122
import fr.acinq.bitcoin.scalacompat.ByteVector32
@@ -58,7 +59,7 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig,
5859
val routeParams = r.routeParams.copy(randomize = false) // we don't randomize the first attempt, regardless of configuration choices
5960
log.debug("sending {} with maximum fee {}", r.recipient.totalAmount, r.routeParams.getMaxFee(r.recipient.totalAmount))
6061
val d = PaymentProgress(r, r.maxAttempts, Map.empty, Ignore.empty, retryRouteRequest = false, failures = Nil)
61-
router ! createRouteRequest(nodeParams, routeParams, d, cfg)
62+
router ! createRouteRequest(self, nodeParams, routeParams, d, cfg)
6263
goto(WAIT_FOR_ROUTES) using d
6364
}
6465

@@ -74,11 +75,11 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig,
7475
// remaining amount. In that case we discard these routes and send a new request to the router.
7576
log.debug("discarding routes, another child payment failed so we need to recompute them ({} payments still pending for {})", d.pending.size, d.pending.values.map(_.amount).sum)
7677
val routeParams = d.request.routeParams.copy(randomize = true) // we randomize route selection when we retry
77-
router ! createRouteRequest(nodeParams, routeParams, d, cfg)
78+
router ! createRouteRequest(self, nodeParams, routeParams, d, cfg)
7879
stay() using d.copy(retryRouteRequest = false)
7980
}
8081

81-
case Event(Status.Failure(t), d: PaymentProgress) =>
82+
case Event(PaymentRouteNotFound(t), d: PaymentProgress) =>
8283
log.warning("router error: {}", t.getMessage)
8384
// If no route can be found, we will retry once with the channels that we previously ignored.
8485
// Channels are mostly ignored for temporary reasons, likely because they didn't have enough balance to forward
@@ -87,7 +88,7 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig,
8788
if (d.ignore.channels.nonEmpty) {
8889
log.debug("retry sending payment without ignoring channels {} ({} payments still pending for {})", d.ignore.channels.map(_.shortChannelId).mkString(","), d.pending.size, d.pending.values.map(_.amount).sum)
8990
val routeParams = d.request.routeParams.copy(randomize = true) // we randomize route selection when we retry
90-
router ! createRouteRequest(nodeParams, routeParams, d, cfg).copy(ignore = d.ignore.emptyChannels())
91+
router ! createRouteRequest(self, nodeParams, routeParams, d, cfg).copy(ignore = d.ignore.emptyChannels())
9192
retriedFailedChannels = true
9293
stay() using d.copy(remainingAttempts = (d.remainingAttempts - 1).max(0), ignore = d.ignore.emptyChannels(), retryRouteRequest = false)
9394
} else {
@@ -135,7 +136,7 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig,
135136
log.debug("child payment failed, retrying payment ({} payments still pending for {})", stillPending.size, stillPending.values.map(_.amount).sum)
136137
val routeParams = d.request.routeParams.copy(randomize = true) // we randomize route selection when we retry
137138
val d1 = d.copy(pending = stillPending, ignore = ignore1, failures = d.failures ++ pf.failures, request = d.request.copy(recipient = recipient1), retryRouteRequest = false)
138-
router ! createRouteRequest(nodeParams, routeParams, d1, cfg)
139+
router ! createRouteRequest(self, nodeParams, routeParams, d1, cfg)
139140
goto(WAIT_FOR_ROUTES) using d1
140141
}
141142

@@ -164,7 +165,7 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig,
164165
gotoSucceededOrStop(PaymentSucceeded(d.request, ps.paymentPreimage, ps.parts, d.pending - ps.parts.head.id))
165166

166167
case Event(_: RouteResponse, _) => stay()
167-
case Event(_: Status.Failure, _) => stay()
168+
case Event(_: PaymentRouteNotFound, _) => stay()
168169
}
169170

170171
when(PAYMENT_SUCCEEDED) {
@@ -190,7 +191,7 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig,
190191
}
191192

192193
case Event(_: RouteResponse, _) => stay()
193-
case Event(_: Status.Failure, _) => stay()
194+
case Event(_: PaymentRouteNotFound, _) => stay()
194195
}
195196

196197
private def spawnChildPaymentFsm(childId: UUID): ActorRef = {
@@ -368,8 +369,8 @@ object MultiPartPaymentLifecycle {
368369
*/
369370
case class PaymentSucceeded(request: SendMultiPartPayment, preimage: ByteVector32, parts: Seq[PartialPayment], pending: Set[UUID]) extends Data
370371

371-
private def createRouteRequest(nodeParams: NodeParams, routeParams: RouteParams, d: PaymentProgress, cfg: SendPaymentConfig): RouteRequest = {
372-
RouteRequest(nodeParams.nodeId, d.request.recipient, routeParams, d.ignore, allowMultiPart = true, d.pending.values.toSeq, Some(cfg.paymentContext))
372+
private def createRouteRequest(replyTo: ActorRef, nodeParams: NodeParams, routeParams: RouteParams, d: PaymentProgress, cfg: SendPaymentConfig): RouteRequest = {
373+
RouteRequest(replyTo.toTyped, nodeParams.nodeId, d.request.recipient, routeParams, d.ignore, allowMultiPart = true, d.pending.values.toSeq, Some(cfg.paymentContext))
373374
}
374375

375376
private def createChildPayment(replyTo: ActorRef, route: Route, request: SendMultiPartPayment): SendPaymentToRoute = {

eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
5454
case Event(request: SendPaymentToRoute, WaitingForRequest) =>
5555
log.debug("sending {} to route {}", request.amount, request.printRoute())
5656
request.route.fold(
57-
hops => router ! FinalizeRoute(hops, request.recipient.extraEdges, paymentContext = Some(cfg.paymentContext)),
57+
hops => router ! FinalizeRoute(self, hops, request.recipient.extraEdges, paymentContext = Some(cfg.paymentContext)),
5858
route => self ! RouteResponse(route :: Nil)
5959
)
6060
if (cfg.storeInDb) {
@@ -64,7 +64,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
6464

6565
case Event(request: SendPaymentToNode, WaitingForRequest) =>
6666
log.debug("sending {} to {}", request.amount, request.recipient.nodeId)
67-
router ! RouteRequest(nodeParams.nodeId, request.recipient, request.routeParams, paymentContext = Some(cfg.paymentContext))
67+
router ! RouteRequest(self, nodeParams.nodeId, request.recipient, request.routeParams, paymentContext = Some(cfg.paymentContext))
6868
if (cfg.storeInDb) {
6969
paymentsDb.addOutgoingPayment(OutgoingPayment(id, cfg.parentId, cfg.externalId, paymentHash, cfg.paymentType, request.amount, request.recipient.totalAmount, request.recipient.nodeId, TimestampMilli.now(), cfg.invoice, cfg.payerKey_opt, OutgoingPaymentStatus.Pending))
7070
}
@@ -84,7 +84,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
8484
myStop(request, Left(PaymentFailed(id, paymentHash, failures :+ LocalFailure(request.amount, route.fullRoute, error))))
8585
}
8686

87-
case Event(Status.Failure(t), WaitingForRoute(request, failures, _)) =>
87+
case Event(PaymentRouteNotFound(t), WaitingForRoute(request, failures, _)) =>
8888
Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(LocalFailure(request.amount, Nil, t))).increment()
8989
myStop(request, Left(PaymentFailed(id, paymentHash, failures :+ LocalFailure(request.amount, Nil, t))))
9090
}
@@ -135,7 +135,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
135135
data.request match {
136136
case request: SendPaymentToNode =>
137137
val ignore1 = PaymentFailure.updateIgnored(failure, data.ignore)
138-
router ! RouteRequest(nodeParams.nodeId, data.recipient, request.routeParams, ignore1, paymentContext = Some(cfg.paymentContext))
138+
router ! RouteRequest(self, nodeParams.nodeId, data.recipient, request.routeParams, ignore1, paymentContext = Some(cfg.paymentContext))
139139
goto(WAITING_FOR_ROUTE) using WaitingForRoute(data.request, data.failures :+ failure, ignore1)
140140
case _: SendPaymentToRoute =>
141141
log.error("unexpected retry during SendPaymentToRoute")
@@ -241,7 +241,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
241241
log.error("unexpected retry during SendPaymentToRoute")
242242
stop(FSM.Normal)
243243
case request: SendPaymentToNode =>
244-
router ! RouteRequest(nodeParams.nodeId, recipient1, request.routeParams, ignore1, paymentContext = Some(cfg.paymentContext))
244+
router ! RouteRequest(self, nodeParams.nodeId, recipient1, request.routeParams, ignore1, paymentContext = Some(cfg.paymentContext))
245245
goto(WAITING_FOR_ROUTE) using WaitingForRoute(request.copy(recipient = recipient1), failures :+ failure, ignore1)
246246
}
247247
} else {
@@ -252,7 +252,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
252252
log.error("unexpected retry during SendPaymentToRoute")
253253
stop(FSM.Normal)
254254
case request: SendPaymentToNode =>
255-
router ! RouteRequest(nodeParams.nodeId, recipient, request.routeParams, ignore + nodeId, paymentContext = Some(cfg.paymentContext))
255+
router ! RouteRequest(self, nodeParams.nodeId, recipient, request.routeParams, ignore + nodeId, paymentContext = Some(cfg.paymentContext))
256256
goto(WAITING_FOR_ROUTE) using WaitingForRoute(request, failures :+ failure, ignore + nodeId)
257257
}
258258
}
@@ -266,7 +266,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
266266
log.error("unexpected retry during SendPaymentToRoute")
267267
stop(FSM.Normal)
268268
case request: SendPaymentToNode =>
269-
router ! RouteRequest(nodeParams.nodeId, recipient, request.routeParams, ignore1, paymentContext = Some(cfg.paymentContext))
269+
router ! RouteRequest(self, nodeParams.nodeId, recipient, request.routeParams, ignore1, paymentContext = Some(cfg.paymentContext))
270270
goto(WAITING_FOR_ROUTE) using WaitingForRoute(request, failures :+ failure, ignore1)
271271
}
272272
case Right(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) =>

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

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ 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.{HeuristicsConstants, WeightRatios}
27+
import fr.acinq.eclair.router.Graph.{HeuristicsConstants, PaymentPathWeight, PaymentWeightRatios, WeightRatios}
2828
import fr.acinq.eclair.router.Router._
2929
import fr.acinq.eclair.router._
3030
import fr.acinq.eclair.wire.protocol.CommonCodecs._
@@ -57,27 +57,32 @@ object EclairInternalsSerializer {
5757
("feeBase" | millisatoshi) ::
5858
("feeProportionalMillionths" | int64)).as[RelayFees]
5959

60-
val weightRatiosCodec: Codec[WeightRatios] = (
60+
val paymentWeightRatiosCodec: Codec[PaymentWeightRatios] = (
6161
("baseFactor" | double) ::
6262
("cltvDeltaFactor" | double) ::
6363
("ageFactor" | double) ::
6464
("capacityFactor" | double) ::
65-
("hopCost" | relayFeesCodec)).as[WeightRatios]
65+
("hopCost" | relayFeesCodec)).as[PaymentWeightRatios]
6666

6767
val heuristicsConstantsCodec: Codec[HeuristicsConstants] = (
6868
("lockedFundsRisk" | double) ::
6969
("failureCost" | relayFeesCodec) ::
7070
("hopCost" | relayFeesCodec) ::
7171
("useLogProbability" | bool(8))).as[HeuristicsConstants]
7272

73+
val weightRatiosCodec: Codec[WeightRatios[PaymentPathWeight]] =
74+
discriminated[WeightRatios[PaymentPathWeight]].by(uint8)
75+
.typecase(0x00, paymentWeightRatiosCodec)
76+
.typecase(0xff, heuristicsConstantsCodec)
77+
7378
val multiPartParamsCodec: Codec[MultiPartParams] = (
7479
("minPartAmount" | millisatoshi) ::
7580
("maxParts" | int32)).as[MultiPartParams]
7681

7782
val pathFindingConfCodec: Codec[PathFindingConf] = (
7883
("randomize" | bool(8)) ::
7984
("boundaries" | searchBoundariesCodec) ::
80-
("heuristicsParams" | either(bool(8), weightRatiosCodec, heuristicsConstantsCodec)) ::
85+
("heuristicsParams" | weightRatiosCodec) ::
8186
("mpp" | multiPartParamsCodec) ::
8287
("experimentName" | utf8_32) ::
8388
("experimentPercentage" | int32)).as[PathFindingConf]
@@ -90,7 +95,7 @@ object EclairInternalsSerializer {
9095
("maxRouteLength" | int32) ::
9196
(("baseFactor" | double) ::
9297
("ageFactor" | double) ::
93-
("capacityFactor" | double)).as[Graph.MessagePath.WeightRatios]).as[MessageRouteParams]
98+
("capacityFactor" | double)).as[Graph.MessageWeightRatios]).as[MessageRouteParams]
9499

95100
val routerConfCodec: Codec[RouterConf] = (
96101
("watchSpentWindow" | finiteDurationCodec) ::

0 commit comments

Comments
 (0)