Skip to content

Commit

Permalink
resolve compact paths in OfferPayment
Browse files Browse the repository at this point in the history
  • Loading branch information
thomash-acinq committed Nov 13, 2023
1 parent ca14011 commit c53b52c
Show file tree
Hide file tree
Showing 16 changed files with 256 additions and 315 deletions.
7 changes: 4 additions & 3 deletions eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,8 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging {
} else {
val recipientAmount = recipientAmount_opt.getOrElse(invoice.amount_opt.getOrElse(route.amount))
val trampoline_opt = trampolineFees_opt.map(fees => TrampolineAttempt(trampolineSecret_opt.getOrElse(randomBytes32()), fees, trampolineExpiryDelta_opt.get))
appKit.paymentInitiator.toTyped.ask(replyTo => SendPaymentToRoute(replyTo.toClassic, recipientAmount, invoice, route, externalId_opt, parentId_opt, trampoline_opt))
val sendPayment = SendPaymentToRoute(recipientAmount, invoice, Nil, route, externalId_opt, parentId_opt, trampoline_opt)
(appKit.paymentInitiator ? sendPayment).mapTo[SendPaymentToRouteResponse]
}
}

Expand All @@ -441,7 +442,7 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging {
externalId_opt match {
case Some(externalId) if externalId.length > externalIdMaxLength => Left(new IllegalArgumentException(s"externalId is too long: cannot exceed $externalIdMaxLength characters"))
case _ if invoice.isExpired() => Left(new IllegalArgumentException("invoice has expired"))
case _ => Right(SendPaymentToNode(ActorRef.noSender, amount, invoice, maxAttempts, externalId_opt, routeParams = routeParams))
case _ => Right(SendPaymentToNode(ActorRef.noSender, amount, invoice, Nil, maxAttempts, externalId_opt, routeParams = routeParams))
}
case Left(t) => Left(t)
}
Expand Down Expand Up @@ -701,7 +702,7 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging {
case Left(t) => return Future.failed(t)
}
val sendPaymentConfig = OfferPayment.SendPaymentConfig(externalId_opt, connectDirectly, maxAttempts_opt.getOrElse(appKit.nodeParams.maxPaymentAttempts), routeParams, blocking)
val offerPayment = appKit.system.spawnAnonymous(OfferPayment(appKit.nodeParams, appKit.postman, appKit.paymentInitiator))
val offerPayment = appKit.system.spawnAnonymous(OfferPayment(appKit.nodeParams, appKit.postman, appKit.router, appKit.paymentInitiator))
offerPayment.ask((ref: typed.ActorRef[Any]) => OfferPayment.PayOffer(ref.toClassic, offer, amount, quantity, sendPaymentConfig)).flatMap {
case f: OfferPayment.Failure => Future.failed(new Exception(f.toString))
case x => Future.successful(x)
Expand Down
2 changes: 1 addition & 1 deletion eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,7 @@ class Setup(val datadir: File,
_ = switchboard ! Switchboard.Init(channels)
clientSpawner = system.actorOf(SimpleSupervisor.props(ClientSpawner.props(nodeParams.keyPair, nodeParams.socksProxy_opt, nodeParams.peerConnectionConf, switchboard, router), "client-spawner", SupervisorStrategy.Restart))
server = system.actorOf(SimpleSupervisor.props(Server.props(nodeParams.keyPair, nodeParams.peerConnectionConf, switchboard, router, serverBindingAddress, Some(tcpBound)), "server", SupervisorStrategy.Restart))
paymentInitiator = system.actorOf(SimpleSupervisor.props(PaymentInitiator.props(nodeParams, PaymentInitiator.SimplePaymentFactory(nodeParams, router, register), router), "payment-initiator", SupervisorStrategy.Restart))
paymentInitiator = system.actorOf(SimpleSupervisor.props(PaymentInitiator.props(nodeParams, PaymentInitiator.SimplePaymentFactory(nodeParams, router, register)), "payment-initiator", SupervisorStrategy.Restart))
_ = for (i <- 0 until config.getInt("autoprobe-count")) yield system.actorOf(SimpleSupervisor.props(Autoprobe.props(nodeParams, router, paymentInitiator), s"payment-autoprobe-$i", SupervisorStrategy.Restart))

balanceActor = system.spawn(BalanceActor(nodeParams.db, bitcoinClient, channelsListener, nodeParams.balanceCheckInterval), name = "balance-actor")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import fr.acinq.bitcoin.Bech32
import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey}
import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto}
import fr.acinq.eclair.crypto.Sphinx
import fr.acinq.eclair.crypto.Sphinx.RouteBlinding.BlindedRoute
import fr.acinq.eclair.wire.protocol.OfferTypes._
import fr.acinq.eclair.wire.protocol.OnionRoutingCodecs.{InvalidTlvPayload, MissingRequiredTlv}
import fr.acinq.eclair.wire.protocol.{GenericTlv, OfferCodecs, OfferTypes, TlvStream}
Expand Down Expand Up @@ -88,6 +89,8 @@ case class Bolt12Invoice(records: TlvStream[InvoiceTlv]) extends Invoice {

case class PaymentBlindedRoute(route: BlindedContactInfo, paymentInfo: PaymentInfo)

case class ResolvedPaymentBlindedRoute(route: BlindedRoute, paymentInfo: PaymentInfo)

object Bolt12Invoice {
val hrp = "lni"
val signatureTag: ByteVector = ByteVector(("lightning" + "invoice" + "signature").getBytes)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ class Autoprobe(nodeParams: NodeParams, router: ActorRef, paymentInitiator: Acto
ByteVector.empty)
log.info(s"sending payment probe to node=$targetNodeId payment_hash=${fakeInvoice.paymentHash}")
val routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams
paymentInitiator ! PaymentInitiator.SendPaymentToNode(self, PAYMENT_AMOUNT_MSAT, fakeInvoice, maxAttempts = 1, routeParams = routeParams)
paymentInitiator ! PaymentInitiator.SendPaymentToNode(self, PAYMENT_AMOUNT_MSAT, fakeInvoice, Nil, maxAttempts = 1, routeParams = routeParams)
case None =>
log.info(s"could not find a destination, re-scheduling")
scheduleProbe()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,18 @@ import akka.actor.typed.Behavior
import akka.actor.typed.scaladsl.{ActorContext, Behaviors}
import akka.actor.{ActorRef, typed}
import fr.acinq.bitcoin.scalacompat.ByteVector32
import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey
import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey}
import fr.acinq.eclair.crypto.Sphinx.RouteBlinding.BlindedRoute
import fr.acinq.eclair.message.Postman.{OnionMessageResponse, SendMessage}
import fr.acinq.eclair.message.{OnionMessages, Postman}
import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentToNode
import fr.acinq.eclair.payment.{PaymentBlindedRoute, ResolvedPaymentBlindedRoute}
import fr.acinq.eclair.router.Router
import fr.acinq.eclair.router.Router.RouteParams
import fr.acinq.eclair.wire.protocol.MessageOnion.{FinalPayload, InvoicePayload}
import fr.acinq.eclair.wire.protocol.OfferTypes.{InvoiceRequest, Offer}
import fr.acinq.eclair.wire.protocol.OfferTypes.{BlindedPath, CompactBlindedPath, InvoiceRequest, Offer, PaymentInfo}
import fr.acinq.eclair.wire.protocol.{OnionMessagePayloadTlv, TlvStream}
import fr.acinq.eclair.{Features, InvoiceFeature, MilliSatoshi, NodeParams, TimestampSecond, randomKey}
import fr.acinq.eclair.{Features, InvoiceFeature, MilliSatoshi, NodeParams, RealShortChannelId, TimestampSecond, randomKey}

object OfferPayment {
sealed trait Failure
Expand All @@ -49,6 +52,10 @@ object OfferPayment {
override def toString: String = s"Invalid invoice response: $response, invoice request: $request"
}

case class UnknownShortChannelIds(scids: Seq[RealShortChannelId]) extends Failure {
override def toString: String = s"Unknown short channel ids: $scids"
}

sealed trait Command

case class PayOffer(replyTo: ActorRef,
Expand All @@ -59,6 +66,8 @@ object OfferPayment {

case class WrappedMessageResponse(response: OnionMessageResponse) extends Command

private case class WrappedNodeId(nodeId_opt: Option[PublicKey]) extends Command

case class SendPaymentConfig(externalId_opt: Option[String],
connectDirectly: Boolean,
maxAttempts: Int,
Expand All @@ -67,6 +76,7 @@ object OfferPayment {

def apply(nodeParams: NodeParams,
postman: typed.ActorRef[Postman.Command],
router: ActorRef,
paymentInitiator: ActorRef): Behavior[Command] = {
Behaviors.setup(context =>
Behaviors.receiveMessagePartial {
Expand All @@ -89,13 +99,14 @@ object OfferPayment {
} else {
val payerKey = randomKey()
val request = InvoiceRequest(offer, amount, quantity, nodeParams.features.bolt12Features(), payerKey, nodeParams.chainHash)
sendInvoiceRequest(nodeParams, postman, paymentInitiator, context, request, payerKey, replyTo, 0, sendPaymentConfig)
sendInvoiceRequest(nodeParams, postman, router, paymentInitiator, context, request, payerKey, replyTo, 0, sendPaymentConfig)
}
})
}

def sendInvoiceRequest(nodeParams: NodeParams,
postman: typed.ActorRef[Postman.Command],
router: ActorRef,
paymentInitiator: ActorRef,
context: ActorContext[Command],
request: InvoiceRequest,
Expand All @@ -107,11 +118,12 @@ object OfferPayment {
val messageContent = TlvStream[OnionMessagePayloadTlv](OnionMessagePayloadTlv.InvoiceRequest(request.records))
val routingStrategy = if (sendPaymentConfig.connectDirectly) OnionMessages.RoutingStrategy.connectDirectly else OnionMessages.RoutingStrategy.FindRoute
postman ! SendMessage(contactInfo, routingStrategy, messageContent, expectsReply = true, context.messageAdapter(WrappedMessageResponse))
waitForInvoice(nodeParams, postman, paymentInitiator, context, request, payerKey, replyTo, attemptNumber + 1, sendPaymentConfig)
waitForInvoice(nodeParams, postman, router, paymentInitiator, context, request, payerKey, replyTo, attemptNumber + 1, sendPaymentConfig)
}

def waitForInvoice(nodeParams: NodeParams,
postman: typed.ActorRef[Postman.Command],
router: ActorRef,
paymentInitiator: ActorRef,
context: ActorContext[Command],
request: InvoiceRequest,
Expand All @@ -121,20 +133,63 @@ object OfferPayment {
sendPaymentConfig: SendPaymentConfig): Behavior[Command] = {
Behaviors.receiveMessagePartial {
case WrappedMessageResponse(Postman.Response(payload: InvoicePayload)) if payload.invoice.validateFor(request).isRight =>
val recipientAmount = payload.invoice.amount
paymentInitiator ! SendPaymentToNode(replyTo, recipientAmount, payload.invoice, maxAttempts = sendPaymentConfig.maxAttempts, externalId = sendPaymentConfig.externalId_opt, routeParams = sendPaymentConfig.routeParams, payerKey_opt = Some(payerKey), blockUntilComplete = sendPaymentConfig.blocking)
Behaviors.stopped
val sendPaymentToNode = SendPaymentToNode(replyTo, payload.invoice.amount, payload.invoice, Nil, maxAttempts = sendPaymentConfig.maxAttempts, externalId = sendPaymentConfig.externalId_opt, routeParams = sendPaymentConfig.routeParams, payerKey_opt = Some(payerKey), blockUntilComplete = sendPaymentConfig.blocking)
val scids = payload.invoice.blindedPaths.collect { case PaymentBlindedRoute(CompactBlindedPath(scdidDir, _, _), _) => scdidDir.scid }
resolve(context, paymentInitiator, router, sendPaymentToNode, payload.invoice.blindedPaths, Nil, scids)
case WrappedMessageResponse(Postman.Response(payload)) =>
// We've received a response but it is not an invoice as we expected or it is an invalid invoice.
replyTo ! InvalidInvoiceResponse(request, payload)
Behaviors.stopped
case WrappedMessageResponse(Postman.NoReply) if attemptNumber < nodeParams.onionMessageConfig.maxAttempts =>
// We didn't get a response, let's retry.
sendInvoiceRequest(nodeParams, postman, paymentInitiator, context, request, payerKey, replyTo, attemptNumber, sendPaymentConfig)
sendInvoiceRequest(nodeParams, postman, router, paymentInitiator, context, request, payerKey, replyTo, attemptNumber, sendPaymentConfig)
case WrappedMessageResponse(_) =>
// We can't reach the offer node or the offer node can't reach us.
replyTo ! NoInvoiceResponse
Behaviors.stopped
}
}

def resolve(context: ActorContext[Command],
paymentInitiator: ActorRef,
router: ActorRef,
sendPaymentToNode: SendPaymentToNode,
toResolve: Seq[PaymentBlindedRoute],
resolved: Seq[ResolvedPaymentBlindedRoute],
scids: Seq[RealShortChannelId]): Behavior[Command] = {
if (toResolve.isEmpty) {
if (resolved.isEmpty) {
// No route could be resolved
sendPaymentToNode.replyTo ! UnknownShortChannelIds(scids)
} else {
paymentInitiator ! sendPaymentToNode.copy(resolvedPaths = resolved)
}
Behaviors.stopped
} else {
toResolve.head match {
case PaymentBlindedRoute(BlindedPath(route), paymentInfo) =>
resolve(context, paymentInitiator, router, sendPaymentToNode, toResolve.tail, resolved :+ ResolvedPaymentBlindedRoute(route, paymentInfo), scids)
case PaymentBlindedRoute(route: CompactBlindedPath, paymentInfo) =>
router ! Router.GetNodeId(context.messageAdapter(WrappedNodeId), route.introductionNode.scid, route.introductionNode.isNode1)
waitForNodeId(context, paymentInitiator, router, sendPaymentToNode, route, paymentInfo, toResolve.tail, resolved, scids)
}
}
}

def waitForNodeId(context: ActorContext[Command],
paymentInitiator: ActorRef,
router: ActorRef,
sendPaymentToNode: SendPaymentToNode,
compactRoute: CompactBlindedPath,
paymentInfo: PaymentInfo,
toResolve: Seq[PaymentBlindedRoute],
resolved: Seq[ResolvedPaymentBlindedRoute],
scids: Seq[RealShortChannelId]): Behavior[Command] =
Behaviors.receiveMessagePartial {
case WrappedNodeId(None) =>
resolve(context, paymentInitiator, router, sendPaymentToNode, toResolve, resolved, scids)
case WrappedNodeId(Some(nodeId)) =>
val resolvedPaymentBlindedRoute = ResolvedPaymentBlindedRoute(BlindedRoute(nodeId, compactRoute.blindingKey, compactRoute.blindedNodes), paymentInfo)
resolve(context, paymentInitiator, router, sendPaymentToNode, toResolve, resolved :+ resolvedPaymentBlindedRoute, scids)
}
}
Loading

0 comments on commit c53b52c

Please sign in to comment.