Skip to content

Commit

Permalink
Allow generating invoice with compact blinded route
Browse files Browse the repository at this point in the history
  • Loading branch information
thomash-acinq committed Nov 3, 2023
1 parent 219a428 commit 359fadf
Show file tree
Hide file tree
Showing 2 changed files with 54 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import fr.acinq.eclair.payment.offer.OfferManager
import fr.acinq.eclair.router.BlindedRouteCreation.{aggregatePaymentInfo, createBlindedRouteFromHops, createBlindedRouteWithoutHops}
import fr.acinq.eclair.router.Router
import fr.acinq.eclair.router.Router.{ChannelHop, HopRelayParams}
import fr.acinq.eclair.wire.protocol.OfferTypes.{InvoiceRequest, InvoiceTlv}
import fr.acinq.eclair.wire.protocol.OfferTypes.{InvoiceRequest, InvoiceTlv, ShortChannelIdDir}
import fr.acinq.eclair.wire.protocol.PaymentOnion.FinalPayload
import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{Bolt11Feature, CltvExpiryDelta, FeatureSupport, Features, Logs, MilliSatoshi, MilliSatoshiLong, NodeParams, ShortChannelId, TimestampMilli, randomBytes32}
Expand Down Expand Up @@ -276,7 +276,7 @@ object MultiPartHandler {
* @param maxFinalExpiryDelta maximum expiry delta that senders can use: the route expiry will be computed based on this value.
* @param dummyHops (optional) dummy hops to add to the blinded route.
*/
case class ReceivingRoute(nodes: Seq[PublicKey], maxFinalExpiryDelta: CltvExpiryDelta, dummyHops: Seq[DummyBlindedHop] = Nil)
case class ReceivingRoute(nodes: Seq[PublicKey], maxFinalExpiryDelta: CltvExpiryDelta, dummyHops: Seq[DummyBlindedHop] = Nil, shortChannelIdDir_opt: Option[ShortChannelIdDir] = None)

/**
* Use this message to create a Bolt 12 invoice to receive a payment for a given offer.
Expand Down Expand Up @@ -368,20 +368,28 @@ object MultiPartHandler {
} else {
createBlindedRouteFromHops(dummyHops, r.pathId, nodeParams.channelConf.htlcMinimum, route.maxFinalExpiryDelta.toCltvExpiry(nodeParams.currentBlockHeight))
}
val contactInfo = route.shortChannelIdDir_opt match {
case Some(shortChannelIdDir) => OfferTypes.CompactBlindedPath(shortChannelIdDir, blindedRoute.route.blindingKey, blindedRoute.route.blindedNodes)
case None => OfferTypes.BlindedPath(blindedRoute.route)
}
val paymentInfo = aggregatePaymentInfo(r.amount, dummyHops, nodeParams.channelConf.minFinalExpiryDelta)
Future.successful((blindedRoute, paymentInfo, r.pathId))
Future.successful(PaymentBlindedRoute(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 => {
val clearRoute = routeResponse.routes.head
val blindedRoute = createBlindedRouteFromHops(clearRoute.hops ++ dummyHops, r.pathId, nodeParams.channelConf.htlcMinimum, route.maxFinalExpiryDelta.toCltvExpiry(nodeParams.currentBlockHeight))
val contactInfo = route.shortChannelIdDir_opt match {
case Some(shortChannelIdDir) => OfferTypes.CompactBlindedPath(shortChannelIdDir, blindedRoute.route.blindingKey, blindedRoute.route.blindedNodes)
case None => OfferTypes.BlindedPath(blindedRoute.route)
}
val paymentInfo = aggregatePaymentInfo(r.amount, clearRoute.hops ++ dummyHops, nodeParams.channelConf.minFinalExpiryDelta)
(blindedRoute, paymentInfo, r.pathId)
PaymentBlindedRoute(contactInfo, paymentInfo)
})
}
})).map(paths => {
val invoiceFeatures = nodeParams.features.bolt12Features()
val invoice = Bolt12Invoice(r.invoiceRequest, r.paymentPreimage, r.nodeKey, nodeParams.invoiceExpiry, invoiceFeatures, paths.map { case (blindedRoute, paymentInfo, _) => PaymentBlindedRoute(OfferTypes.BlindedPath(blindedRoute.route), paymentInfo) }, r.additionalTlvs, r.customTlvs)
val invoice = Bolt12Invoice(r.invoiceRequest, r.paymentPreimage, r.nodeKey, nodeParams.invoiceExpiry, invoiceFeatures, paths, r.additionalTlvs, r.customTlvs)
log.debug("generated invoice={} for offer={}", invoice.toString, r.invoiceRequest.offer.toString)
invoice
}))(WrappedInvoiceResult)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ import fr.acinq.eclair.payment.send.PaymentInitiator.{SendPaymentToNode, SendTra
import fr.acinq.eclair.router.Graph.WeightRatios
import fr.acinq.eclair.router.Router.{GossipDecision, PublicChannel}
import fr.acinq.eclair.router.{Announcements, AnnouncementsBatchValidationSpec, Router}
import fr.acinq.eclair.wire.protocol.OfferTypes.{Offer, OfferPaths}
import fr.acinq.eclair.wire.protocol.OfferTypes.{CompactBlindedPath, Offer, OfferPaths, ShortChannelIdDir}
import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelUpdate, IncorrectOrUnknownPaymentDetails, OfferTypes}
import fr.acinq.eclair.{CltvExpiryDelta, EclairImpl, Features, Kit, MilliSatoshiLong, ShortChannelId, TimestampMilli, randomBytes32, randomKey}
import org.json4s.JsonAST.{JString, JValue}
Expand Down Expand Up @@ -827,6 +827,46 @@ class PaymentIntegrationSpec extends IntegrationSpec {
assert(receivedAmount >= amount)
}

test("send to compact route") {
val recipientKey = randomKey()
val amount = 10_000_000 msat
val chain = nodes("C").nodeParams.chainHash
val pathId = randomBytes32()
val offerPath = OfferTypes.BlindedPath(buildRoute(randomKey(), Seq(IntermediateNode(nodes("B").nodeParams.nodeId), IntermediateNode(nodes("C").nodeParams.nodeId)), Recipient(nodes("C").nodeParams.nodeId, Some(pathId))))
val offer = Offer(Some(amount), "test offer", recipientKey.publicKey, nodes("C").nodeParams.features.bolt12Features(), chain, additionalTlvs = Set(OfferPaths(Seq(offerPath))))
val offerHandler = TypedProbe[HandlerCommand]()(nodes("C").system.toTyped)
nodes("C").offerManager ! RegisterOffer(offer, recipientKey, Some(pathId), offerHandler.ref)

val sender = TestProbe()
val alice = new EclairImpl(nodes("A"))
alice.payOfferBlocking(offer, amount, 1, maxAttempts_opt = Some(3))(30 seconds).pipeTo(sender.ref)

val handleInvoiceRequest = offerHandler.expectMessageType[HandleInvoiceRequest]
val probe = TestProbe()
probe.send(nodes("C").router, Router.GetChannels)
val b = nodes("B").nodeParams.nodeId
val channelWithB = probe.expectMsgType[Iterable[ChannelAnnouncement]].find(ann => ann.nodeId1 == b || ann.nodeId2 == b).get
val receivingRoutes = Seq(
ReceivingRoute(Seq(nodes("B").nodeParams.nodeId, nodes("C").nodeParams.nodeId), CltvExpiryDelta(555), Seq(DummyBlindedHop(55 msat, 55, CltvExpiryDelta(55))), Some(ShortChannelIdDir(channelWithB.nodeId1 == b, channelWithB.shortChannelId)))
)
handleInvoiceRequest.replyTo ! InvoiceRequestActor.ApproveRequest(amount, receivingRoutes, pluginData_opt = Some(hex"eff0"))

val handlePayment = offerHandler.expectMessageType[HandlePayment]
assert(handlePayment.offerId == offer.offerId)
assert(handlePayment.pluginData_opt.contains(hex"eff0"))
handlePayment.replyTo ! PaymentActor.AcceptPayment()

val paymentSent = sender.expectMsgType[PaymentSent]
assert(paymentSent.recipientAmount == amount, paymentSent)
assert(paymentSent.feesPaid >= 0.msat, paymentSent)
val Some(invoice: Bolt12Invoice) = nodes("A").nodeParams.db.payments.listOutgoingPaymentsToOffer(offer.offerId).head.invoice
assert(invoice.blindedPaths.forall(_.route.isInstanceOf[CompactBlindedPath]))

awaitCond(nodes("C").nodeParams.db.payments.getIncomingPayment(paymentSent.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received]))
val Some(IncomingBlindedPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("C").nodeParams.db.payments.getIncomingPayment(paymentSent.paymentHash)
assert(receivedAmount >= amount)
}

test("generate and validate lots of channels") {
val bitcoinClient = new BitcoinCoreClient(bitcoinrpcclient)
// we simulate fake channels by publishing a funding tx and sending announcement messages to a node at random
Expand Down

0 comments on commit 359fadf

Please sign in to comment.