1
0
Fork 0
mirror of https://github.com/ACINQ/eclair.git synced 2025-02-23 14:40:34 +01:00

SendToRoute with routing hints (#1325)

SendToRoute previously didn't accept invoice routing hints.
This was a limitation when paying a wallet invoice.
Invoice hints are now correctly taken into account.
This commit is contained in:
Bastien Teinturier 2020-02-18 10:52:43 +01:00 committed by GitHub
parent 7490bf3222
commit 4579462dfc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 40 additions and 12 deletions

View file

@ -112,12 +112,12 @@ class PaymentInitiator(nodeParams: NodeParams, router: ActorRef, relayer: ActorR
val trampolineSecret = r.trampolineSecret.getOrElse(randomBytes32)
sender ! SendPaymentToRouteResponse(paymentId, parentPaymentId, Some(trampolineSecret))
val (trampolineAmount, trampolineExpiry, trampolineOnion) = buildTrampolinePayment(SendTrampolinePaymentRequest(r.recipientAmount, r.paymentRequest, trampoline, Seq((r.trampolineFees, r.trampolineExpiryDelta)), r.finalExpiryDelta), r.trampolineFees, r.trampolineExpiryDelta)
payFsm forward SendPaymentToRoute(r.route, Onion.createMultiPartPayload(r.amount, trampolineAmount, trampolineExpiry, trampolineSecret, Seq(OnionTlv.TrampolineOnion(trampolineOnion))))
payFsm forward SendPaymentToRoute(r.route, Onion.createMultiPartPayload(r.amount, trampolineAmount, trampolineExpiry, trampolineSecret, Seq(OnionTlv.TrampolineOnion(trampolineOnion))), r.paymentRequest.routingInfo)
case Nil =>
sender ! SendPaymentToRouteResponse(paymentId, parentPaymentId, None)
r.paymentRequest.paymentSecret match {
case Some(paymentSecret) => payFsm forward SendPaymentToRoute(r.route, Onion.createMultiPartPayload(r.amount, r.recipientAmount, finalExpiry, paymentSecret))
case None => payFsm forward SendPaymentToRoute(r.route, FinalLegacyPayload(r.recipientAmount, finalExpiry))
case Some(paymentSecret) => payFsm forward SendPaymentToRoute(r.route, Onion.createMultiPartPayload(r.amount, r.recipientAmount, finalExpiry, paymentSecret), r.paymentRequest.routingInfo)
case None => payFsm forward SendPaymentToRoute(r.route, FinalLegacyPayload(r.recipientAmount, finalExpiry), r.paymentRequest.routingInfo)
}
case _ =>
sender ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(TrampolineMultiNodeNotSupported) :: Nil)

View file

@ -72,7 +72,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
span.tag("expiry", c.finalPayload.expiry.toLong)
log.debug("sending {} to route {}", c.finalPayload.amount, c.hops.mkString("->"))
val send = SendPayment(c.hops.last, c.finalPayload, maxAttempts = 1)
router ! FinalizeRoute(c.hops)
router ! FinalizeRoute(c.hops, c.assistedRoutes)
if (cfg.storeInDb) {
paymentsDb.addOutgoingPayment(OutgoingPayment(id, cfg.parentId, cfg.externalId, paymentHash, PaymentType.Standard, c.finalPayload.amount, cfg.recipientAmount, cfg.recipientNodeId, Platform.currentTime, cfg.paymentRequest, OutgoingPaymentStatus.Pending))
}
@ -282,7 +282,7 @@ object PaymentLifecycle {
* @param hops payment route to use.
* @param finalPayload onion payload for the target node.
*/
case class SendPaymentToRoute(hops: Seq[PublicKey], finalPayload: FinalPayload) {
case class SendPaymentToRoute(hops: Seq[PublicKey], finalPayload: FinalPayload, assistedRoutes: Seq[Seq[ExtraHop]] = Nil) {
require(hops.nonEmpty, s"payment route must not be empty")
val targetNodeId = hops.last
}

View file

@ -148,7 +148,7 @@ case class RouteRequest(source: PublicKey,
ignoreChannels: Set[ChannelDesc] = Set.empty,
routeParams: Option[RouteParams] = None)
case class FinalizeRoute(hops: Seq[PublicKey])
case class FinalizeRoute(hops: Seq[PublicKey], assistedRoutes: Seq[Seq[ExtraHop]] = Nil)
case class RouteResponse(hops: Seq[ChannelHop], ignoreNodes: Set[PublicKey], ignoreChannels: Set[ChannelDesc], allowEmpty: Boolean = false) {
require(allowEmpty || hops.nonEmpty, "route cannot be empty")
@ -533,9 +533,13 @@ class Router(val nodeParams: NodeParams, watcher: ActorRef, initialized: Option[
sender ! d
stay
case Event(FinalizeRoute(partialHops), d) =>
case Event(FinalizeRoute(partialHops, assistedRoutes), d) =>
// NB: using a capacity of 0 msat will impact the path-finding algorithm. However here we don't run any path-finding, so it's ok.
val assistedChannels: Map[ShortChannelId, AssistedChannel] = assistedRoutes.flatMap(toAssistedChannels(_, partialHops.last, 0 msat)).toMap
val extraEdges = assistedChannels.values.map(ac => GraphEdge(ChannelDesc(ac.extraHop.shortChannelId, ac.extraHop.nodeId, ac.nextNodeId), toFakeUpdate(ac.extraHop, ac.htlcMaximum))).toSet
val g = extraEdges.foldLeft(d.graph) { case (g: DirectedGraph, e: GraphEdge) => g.addEdge(e) }
// split into sublists [(a,b),(b,c), ...] then get the edges between each of those pairs
partialHops.sliding(2).map { case List(v1, v2) => d.graph.getEdgesBetween(v1, v2) }.toList match {
partialHops.sliding(2).map { case List(v1, v2) => g.getEdgesBetween(v1, v2) }.toList match {
case edges if edges.nonEmpty && edges.forall(_.nonEmpty) =>
val selectedEdges = edges.map(_.maxBy(_.update.htlcMaximumMsat.getOrElse(0 msat))) // select the largest edge
val hops = selectedEdges.map(d => ChannelHop(d.desc.a, d.desc.b, d.update))
@ -1299,11 +1303,12 @@ object Router {
/**
* Build a `reply_channel_range` message
* @param chunk chunk of scids
* @param chainHash chain hash
*
* @param chunk chunk of scids
* @param chainHash chain hash
* @param defaultEncoding default encoding
* @param queryFlags_opt query flag set by the requester
* @param channels channels map
* @param queryFlags_opt query flag set by the requester
* @param channels channels map
* @return a ReplyChannelRange object
*/
def buildReplyChannelRange(chunk: ShortChannelIdsChunk, chainHash: ByteVector32, defaultEncoding: EncodingType, queryFlags_opt: Option[QueryChannelRangeTlv.QueryFlags], channels: SortedMap[ShortChannelId, PublicChannel]): ReplyChannelRange = {

View file

@ -116,6 +116,29 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
assert(failureMessage == "Not all the nodes in the supplied route are connected with public channels")
}
test("send to route (routing hints)") { routerFixture =>
val payFixture = createPaymentLifecycle()
import payFixture._
val recipient = randomKey.publicKey
val routingHint = Seq(Seq(ExtraHop(c, ShortChannelId(561), 1 msat, 100, CltvExpiryDelta(144))))
val request = SendPaymentToRoute(Seq(a, b, c, recipient), FinalLegacyPayload(defaultAmountMsat, defaultExpiry), routingHint)
sender.send(paymentFSM, request)
routerForwarder.expectMsg(FinalizeRoute(Seq(a, b, c, recipient), routingHint))
val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]])
routerForwarder.forward(routerFixture.router)
val Transition(_, WAITING_FOR_ROUTE, WAITING_FOR_PAYMENT_COMPLETE) = monitor.expectMsgClass(classOf[Transition[_]])
// Payment accepted by the recipient.
sender.send(paymentFSM, UpdateFulfillHtlc(ByteVector32.Zeroes, 0, defaultPaymentHash))
val ps = sender.expectMsgType[PaymentSent]
assert(ps.id === parentId)
awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status.isInstanceOf[OutgoingPaymentStatus.Succeeded]))
}
test("send with route prefix") { _ =>
val payFixture = createPaymentLifecycle()
import payFixture._