Skip to content

Commit 9147c41

Browse files
committed
blinded route request
1 parent ea7f7ab commit 9147c41

File tree

13 files changed

+321
-214
lines changed

13 files changed

+321
-214
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/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: 8 additions & 7 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

@@ -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/router/Graph.scala

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ object Graph {
5656
amount >= edge.params.htlcMinimum
5757
}
5858

59+
object PaymentPathWeight {
60+
def apply(amount: MilliSatoshi): PaymentPathWeight = PaymentPathWeight(amount, 0, CltvExpiryDelta(0), 1.0, 0 msat, 0 msat, 0.0)
61+
}
62+
5963
/**
6064
* The cumulative weight of a set of edges (path in the graph).
6165
*
@@ -249,7 +253,7 @@ object Graph {
249253
boundaries: PaymentPathWeight => Boolean,
250254
includeLocalChannelCost: Boolean): Seq[WeightedPath] = {
251255
// find the shortest path (k = 0)
252-
val targetWeight = PaymentPathWeight(amount, 0, CltvExpiryDelta(0), 1.0, 0 msat, 0 msat, 0.0)
256+
val targetWeight = PaymentPathWeight(amount)
253257
dijkstraShortestPath(graph, sourceNode, targetNode, ignoredEdges, ignoredVertices, extraEdges, targetWeight, boundaries, Features.empty, currentBlockHeight, wr, includeLocalChannelCost) match {
254258
case None => Seq.empty // if we can't even find a single path, avoid returning a Seq(Seq.empty)
255259
case Some(shortestPath) =>
@@ -438,6 +442,36 @@ object Graph {
438442
wr: MessageWeightRatios): Option[Seq[GraphEdge]] =
439443
dijkstraShortestPath(g, sourceNode, targetNode, ignoredEdges = Set.empty, ignoredVertices, extraEdges = Set.empty, MessagePathWeight.zero, boundaries, Features(Features.OnionMessages -> FeatureSupport.Mandatory), currentBlockHeight, wr, includeLocalChannelCost = true)
440444

445+
/**
446+
* Find non-overlapping (no vertices shared) payment paths that support route blinding
447+
*
448+
* @param pathsToFind Number of paths to find. We may return fewer paths if we couldn't find more non-overlapping ones.
449+
*/
450+
def routeBlindingPaths(graph: DirectedGraph,
451+
sourceNode: PublicKey,
452+
targetNode: PublicKey,
453+
amount: MilliSatoshi,
454+
ignoredEdges: Set[ChannelDesc],
455+
ignoredVertices: Set[PublicKey],
456+
pathsToFind: Int,
457+
wr: WeightRatios[PaymentPathWeight],
458+
currentBlockHeight: BlockHeight,
459+
boundaries: PaymentPathWeight => Boolean): Seq[WeightedPath] = {
460+
val paths = new mutable.ArrayBuffer[WeightedPath](pathsToFind)
461+
val verticesToIgnore = new mutable.HashSet[PublicKey]()
462+
verticesToIgnore.addAll(ignoredVertices)
463+
for (_ <- 1 to pathsToFind) {
464+
dijkstraShortestPath(graph, sourceNode, targetNode, ignoredEdges, verticesToIgnore.toSet, extraEdges = Set.empty, PaymentPathWeight(amount), boundaries, Features(Features.RouteBlinding -> FeatureSupport.Mandatory), currentBlockHeight, wr, includeLocalChannelCost = true) match {
465+
case Some(path) =>
466+
val weight = pathWeight(sourceNode, path, amount, currentBlockHeight, wr, includeLocalChannelCost = true)
467+
paths += WeightedPath(path, weight)
468+
verticesToIgnore.addAll(path.drop(1).map(_.desc.a))
469+
case None => return paths.toSeq
470+
}
471+
}
472+
paths.toSeq
473+
}
474+
441475
/**
442476
* Calculate the minimum amount that the start node needs to receive to be able to forward @amountWithFees to the end
443477
* node.
@@ -476,7 +510,7 @@ object Graph {
476510
* @param includeLocalChannelCost if the path is for relaying and we need to include the cost of the local channel
477511
*/
478512
def pathWeight(sender: PublicKey, path: Seq[GraphEdge], amount: MilliSatoshi, currentBlockHeight: BlockHeight, wr: WeightRatios[PaymentPathWeight], includeLocalChannelCost: Boolean): PaymentPathWeight = {
479-
path.foldRight(PaymentPathWeight(amount, 0, CltvExpiryDelta(0), 1.0, 0 msat, 0 msat, 0.0)) { (edge, prev) =>
513+
path.foldRight(PaymentPathWeight(amount)) { (edge, prev) =>
480514
wr.addEdgeWeight(sender, edge, prev, currentBlockHeight, includeLocalChannelCost)
481515
}
482516
}

0 commit comments

Comments
 (0)