Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improvements to PR #2752 #2776

Merged
merged 2 commits into from
Nov 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ case class Bolt12Invoice(records: TlvStream[InvoiceTlv]) extends Invoice {
// We add invoice features that are implicitly required for Bolt 12 (the spec doesn't allow explicitly setting them).
f.add(Features.VariableLengthOnion, FeatureSupport.Mandatory).add(Features.RouteBlinding, FeatureSupport.Mandatory)
}
val blindedPaths: Seq[PaymentBlindedRoute] = records.get[InvoicePaths].get.paths.zip(records.get[InvoiceBlindedPay].get.paymentInfo).map { case (route, info) => PaymentBlindedRoute(route, info) }
val blindedPaths: Seq[PaymentBlindedContactInfo] = records.get[InvoicePaths].get.paths.zip(records.get[InvoiceBlindedPay].get.paymentInfo).map { case (route, info) => PaymentBlindedContactInfo(route, info) }
val fallbacks: Option[Seq[FallbackAddress]] = records.get[InvoiceFallbacks].map(_.addresses)
val signature: ByteVector64 = records.get[Signature].get.signature

Expand Down Expand Up @@ -87,9 +87,9 @@ case class Bolt12Invoice(records: TlvStream[InvoiceTlv]) extends Invoice {

}

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

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

object Bolt12Invoice {
val hrp = "lni"
Expand All @@ -110,7 +110,7 @@ object Bolt12Invoice {
nodeKey: PrivateKey,
invoiceExpiry: FiniteDuration,
features: Features[Bolt12Feature],
paths: Seq[PaymentBlindedRoute],
paths: Seq[PaymentBlindedContactInfo],
additionalTlvs: Set[InvoiceTlv] = Set.empty,
customTlvs: Set[GenericTlv] = Set.empty): Bolt12Invoice = {
require(request.amount.nonEmpty || request.offer.amount.nonEmpty)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,7 @@ object MultiPartHandler {
case None => OfferTypes.BlindedPath(blindedRoute.route)
}
val paymentInfo = aggregatePaymentInfo(r.amount, dummyHops, nodeParams.channelConf.minFinalExpiryDelta)
Future.successful(PaymentBlindedRoute(contactInfo, paymentInfo))
Future.successful(PaymentBlindedContactInfo(contactInfo, paymentInfo))
} else {
implicit val timeout: Timeout = 10.seconds
r.router.ask(Router.FinalizeRoute(Router.PredefinedNodeRoute(r.amount, route.nodes))).mapTo[Router.RouteResponse].map(routeResponse => {
Expand All @@ -384,7 +384,7 @@ object MultiPartHandler {
case None => OfferTypes.BlindedPath(blindedRoute.route)
}
val paymentInfo = aggregatePaymentInfo(r.amount, clearRoute.hops ++ dummyHops, nodeParams.channelConf.minFinalExpiryDelta)
PaymentBlindedRoute(contactInfo, paymentInfo)
PaymentBlindedContactInfo(contactInfo, paymentInfo)
})
}
})).map(paths => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,16 @@ 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.payment.{Bolt12Invoice, PaymentBlindedContactInfo, PaymentBlindedRoute}
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.{BlindedPath, CompactBlindedPath, InvoiceRequest, Offer, PaymentInfo}
import fr.acinq.eclair.wire.protocol.OfferTypes._
import fr.acinq.eclair.wire.protocol.{OnionMessagePayloadTlv, TlvStream}
import fr.acinq.eclair.{Features, InvoiceFeature, MilliSatoshi, NodeParams, RealShortChannelId, TimestampSecond, randomKey}

import scala.annotation.tailrec

object OfferPayment {
sealed trait Failure

Expand All @@ -53,7 +55,7 @@ object OfferPayment {
}

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

sealed trait Command
Expand Down Expand Up @@ -99,97 +101,88 @@ object OfferPayment {
} else {
val payerKey = randomKey()
val request = InvoiceRequest(offer, amount, quantity, nodeParams.features.bolt12Features(), payerKey, nodeParams.chainHash)
sendInvoiceRequest(nodeParams, postman, router, paymentInitiator, context, request, payerKey, replyTo, 0, sendPaymentConfig)
val offerPayment = new OfferPayment(replyTo, nodeParams, postman, router, paymentInitiator, payerKey, request, sendPaymentConfig, context)
offerPayment.sendInvoiceRequest(attemptNumber = 0)
}
})
}

def sendInvoiceRequest(nodeParams: NodeParams,
postman: typed.ActorRef[Postman.Command],
router: ActorRef,
paymentInitiator: ActorRef,
context: ActorContext[Command],
request: InvoiceRequest,
payerKey: PrivateKey,
replyTo: ActorRef,
attemptNumber: Int,
sendPaymentConfig: SendPaymentConfig): Behavior[Command] = {
val contactInfo = request.offer.contactInfos(attemptNumber % request.offer.contactInfos.length)
val messageContent = TlvStream[OnionMessagePayloadTlv](OnionMessagePayloadTlv.InvoiceRequest(request.records))
}

private class OfferPayment(replyTo: ActorRef,
nodeParams: NodeParams,
postman: typed.ActorRef[Postman.Command],
router: ActorRef,
paymentInitiator: ActorRef,
payerKey: PrivateKey,
invoiceRequest: InvoiceRequest,
sendPaymentConfig: OfferPayment.SendPaymentConfig,
context: ActorContext[OfferPayment.Command]) {

import OfferPayment._

def sendInvoiceRequest(attemptNumber: Int): Behavior[Command] = {
val contactInfo = invoiceRequest.offer.contactInfos(attemptNumber % invoiceRequest.offer.contactInfos.length)
val messageContent = TlvStream[OnionMessagePayloadTlv](OnionMessagePayloadTlv.InvoiceRequest(invoiceRequest.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, router, paymentInitiator, context, request, payerKey, replyTo, attemptNumber + 1, sendPaymentConfig)
waitForInvoice(attemptNumber + 1)
}

def waitForInvoice(nodeParams: NodeParams,
postman: typed.ActorRef[Postman.Command],
router: ActorRef,
paymentInitiator: ActorRef,
context: ActorContext[Command],
request: InvoiceRequest,
payerKey: PrivateKey,
replyTo: ActorRef,
attemptNumber: Int,
sendPaymentConfig: SendPaymentConfig): Behavior[Command] = {
private def waitForInvoice(attemptNumber: Int): Behavior[Command] = {
Behaviors.receiveMessagePartial {
case WrappedMessageResponse(Postman.Response(payload: InvoicePayload)) if payload.invoice.validateFor(request).isRight =>
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: InvoicePayload)) if payload.invoice.validateFor(invoiceRequest).isRight =>
resolveCompactBlindedPaths(payload.invoice, payload.invoice.blindedPaths, Nil)
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)
replyTo ! InvalidInvoiceResponse(invoiceRequest, payload)
Behaviors.stopped
case WrappedMessageResponse(Postman.NoReply) if attemptNumber < nodeParams.onionMessageConfig.maxAttempts =>
// We didn't get a response, let's retry.
sendInvoiceRequest(nodeParams, postman, router, paymentInitiator, context, request, payerKey, replyTo, attemptNumber, sendPaymentConfig)
sendInvoiceRequest(attemptNumber)
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] = {
/**
* Blinded paths in Bolt 12 invoices may encode the introduction node with an scid and a direction: we need to resolve
* that to a nodeId in order to reach that introduction node and use the blinded path.
*/
@tailrec
private def resolveCompactBlindedPaths(invoice: Bolt12Invoice, toResolve: Seq[PaymentBlindedContactInfo], resolved: Seq[PaymentBlindedRoute]): Behavior[Command] = {
if (toResolve.isEmpty) {
if (resolved.isEmpty) {
// No route could be resolved
sendPaymentToNode.replyTo ! UnknownShortChannelIds(scids)
// We don't know how to reach any of the blinded paths' introduction nodes.
val scids = invoice.blindedPaths.collect { case PaymentBlindedContactInfo(CompactBlindedPath(scdidDir, _, _), _) => scdidDir.scid }
replyTo ! UnknownShortChannelIds(scids)
} else {
paymentInitiator ! sendPaymentToNode.copy(resolvedPaths = resolved)
paymentInitiator ! SendPaymentToNode(replyTo, invoice.amount, invoice, resolved, maxAttempts = sendPaymentConfig.maxAttempts, externalId = sendPaymentConfig.externalId_opt, routeParams = sendPaymentConfig.routeParams, payerKey_opt = Some(payerKey), blockUntilComplete = sendPaymentConfig.blocking)
}
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) =>
case PaymentBlindedContactInfo(BlindedPath(route), paymentInfo) =>
resolveCompactBlindedPaths(invoice, toResolve.tail, resolved :+ PaymentBlindedRoute(route, paymentInfo))
case PaymentBlindedContactInfo(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)
waitForNodeId(invoice, route, paymentInfo, toResolve.tail, resolved)
}
}
}

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] =
private def waitForNodeId(invoice: Bolt12Invoice,
compactRoute: CompactBlindedPath,
paymentInfo: PaymentInfo,
toResolve: Seq[PaymentBlindedContactInfo],
resolved: Seq[PaymentBlindedRoute]): Behavior[Command] =
Behaviors.receiveMessagePartial {
case WrappedNodeId(None) =>
resolve(context, paymentInitiator, router, sendPaymentToNode, toResolve, resolved, scids)
resolveCompactBlindedPaths(invoice, toResolve, resolved)
case WrappedNodeId(Some(nodeId)) =>
val resolvedPaymentBlindedRoute = ResolvedPaymentBlindedRoute(BlindedRoute(nodeId, compactRoute.blindingKey, compactRoute.blindedNodes), paymentInfo)
resolve(context, paymentInitiator, router, sendPaymentToNode, toResolve, resolved :+ resolvedPaymentBlindedRoute, scids)
val resolvedPaymentBlindedRoute = PaymentBlindedRoute(BlindedRoute(nodeId, compactRoute.blindingKey, compactRoute.blindedNodes), paymentInfo)
resolveCompactBlindedPaths(invoice, toResolve, resolved :+ resolvedPaymentBlindedRoute)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ object PaymentInitiator {
case class SendPaymentToNode(replyTo: ActorRef,
recipientAmount: MilliSatoshi,
invoice: Invoice,
resolvedPaths: Seq[ResolvedPaymentBlindedRoute],
resolvedPaths: Seq[PaymentBlindedRoute],
maxAttempts: Int,
externalId: Option[String] = None,
routeParams: RouteParams,
Expand Down Expand Up @@ -384,7 +384,7 @@ object PaymentInitiator {
*/
case class SendPaymentToRoute(recipientAmount: MilliSatoshi,
invoice: Invoice,
resolvedPaths: Seq[ResolvedPaymentBlindedRoute],
resolvedPaths: Seq[PaymentBlindedRoute],
route: PredefinedRoute,
externalId: Option[String],
parentId: Option[UUID],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
import fr.acinq.eclair.crypto.Sphinx
import fr.acinq.eclair.payment.Invoice.ExtraEdge
import fr.acinq.eclair.payment.OutgoingPaymentPacket._
import fr.acinq.eclair.payment.{Bolt11Invoice, Bolt12Invoice, OutgoingPaymentPacket, ResolvedPaymentBlindedRoute}
import fr.acinq.eclair.payment.{Bolt11Invoice, Bolt12Invoice, OutgoingPaymentPacket, PaymentBlindedRoute}
import fr.acinq.eclair.router.Router._
import fr.acinq.eclair.wire.protocol.PaymentOnion.{FinalPayload, IntermediatePayload, OutgoingBlindedPerHopPayload}
import fr.acinq.eclair.wire.protocol.{GenericTlv, OnionRoutingPacket, PaymentOnionCodecs}
Expand Down Expand Up @@ -166,7 +166,8 @@ case class BlindedRecipient(nodeId: PublicKey,
}

object BlindedRecipient {
def apply(invoice: Bolt12Invoice, paths: Seq[ResolvedPaymentBlindedRoute], totalAmount: MilliSatoshi, expiry: CltvExpiry, customTlvs: Set[GenericTlv]): BlindedRecipient = {
/** @param paths the caller must resolve the scid of compact blinded paths, otherwise they will be ignored. */
def apply(invoice: Bolt12Invoice, paths: Seq[PaymentBlindedRoute], totalAmount: MilliSatoshi, expiry: CltvExpiry, customTlvs: Set[GenericTlv]): BlindedRecipient = {
val blindedHops = paths.map(
path => {
// We don't know the scids of channels inside the blinded route, but it's useful to have an ID to refer to a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -801,7 +801,7 @@ object PaymentsDbSpec {
def createBolt12Invoice(amount: MilliSatoshi, payerKey: PrivateKey, recipientKey: PrivateKey, preimage: ByteVector32): Bolt12Invoice = {
val offer = Offer(Some(amount), "some offer", recipientKey.publicKey, Features.empty, Block.TestnetGenesisBlock.hash)
val invoiceRequest = InvoiceRequest(offer, 789 msat, 1, Features.empty, payerKey, Block.TestnetGenesisBlock.hash)
val dummyRoute = PaymentBlindedRoute(BlindedPath(RouteBlinding.create(randomKey(), Seq(randomKey().publicKey), Seq(randomBytes(100))).route), PaymentInfo(0 msat, 0, CltvExpiryDelta(0), 0 msat, 0 msat, Features.empty))
val dummyRoute = PaymentBlindedContactInfo(BlindedPath(RouteBlinding.create(randomKey(), Seq(randomKey().publicKey), Seq(randomBytes(100))).route), PaymentInfo(0 msat, 0, CltvExpiryDelta(0), 0 msat, 0 msat, Features.empty))
Bolt12Invoice(invoiceRequest, preimage, recipientKey, 1 hour, Features.empty, Seq(dummyRoute))
}
}
Loading