From 1e0694ca22db58591f281d68a2b6e0565791b89c Mon Sep 17 00:00:00 2001 From: t-bast Date: Wed, 18 Sep 2024 18:33:02 +0200 Subject: [PATCH 1/2] Allow including routing hints when creating Bolt 11 invoice When nodes only have private channels, they must include routing hints in their Bolt 11 invoices to be able to receive payments. We add a parameter to the `createinvoice` RPC for this. Note that this may leak the channel outpoint if `scid_alias` isn't used. Fixes #2802 --- docs/release-notes/eclair-vnext.md | 1 + .../main/scala/fr/acinq/eclair/Eclair.scala | 22 +++++++++++++++---- .../fr/acinq/eclair/EclairImplSpec.scala | 6 ++--- .../acinq/eclair/api/handlers/Invoice.scala | 7 +++--- 4 files changed, 26 insertions(+), 10 deletions(-) diff --git a/docs/release-notes/eclair-vnext.md b/docs/release-notes/eclair-vnext.md index ede7884a1a..c92646390a 100644 --- a/docs/release-notes/eclair-vnext.md +++ b/docs/release-notes/eclair-vnext.md @@ -27,6 +27,7 @@ Eclair will not allow remote peers to open new obsolete channels that do not sup ### API changes - `channelstats` now takes optional parameters `--count` and `--skip` to control pagination. By default, it will return first 10 entries. (#2890) +- `createinvoice` now takes an optional `--privateChannelIds` parameter that can be used to add routing hints through private channels. (#TODO) ### Miscellaneous improvements and bug fixes diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala index 9aeec55496..fcd39dc71b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -114,7 +114,7 @@ trait Eclair { def nodes(nodeIds_opt: Option[Set[PublicKey]] = None)(implicit timeout: Timeout): Future[Iterable[NodeAnnouncement]] - def receive(description: Either[String, ByteVector32], amount_opt: Option[MilliSatoshi], expire_opt: Option[Long], fallbackAddress_opt: Option[String], paymentPreimage_opt: Option[ByteVector32])(implicit timeout: Timeout): Future[Bolt11Invoice] + def receive(description: Either[String, ByteVector32], amount_opt: Option[MilliSatoshi], expire_opt: Option[Long], fallbackAddress_opt: Option[String], paymentPreimage_opt: Option[ByteVector32], privateChannelIds_opt: Option[List[ByteVector32]])(implicit timeout: Timeout): Future[Bolt11Invoice] def newAddress(): Future[String] @@ -330,14 +330,28 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { } } - override def receive(description: Either[String, ByteVector32], amount_opt: Option[MilliSatoshi], expire_opt: Option[Long], fallbackAddress_opt: Option[String], paymentPreimage_opt: Option[ByteVector32])(implicit timeout: Timeout): Future[Bolt11Invoice] = { + override def receive(description: Either[String, ByteVector32], amount_opt: Option[MilliSatoshi], expire_opt: Option[Long], fallbackAddress_opt: Option[String], paymentPreimage_opt: Option[ByteVector32], privateChannelIds_opt: Option[List[ByteVector32]])(implicit timeout: Timeout): Future[Bolt11Invoice] = { fallbackAddress_opt.foreach { fa => + // If it's not a valid bitcoin address we throw an exception. addressToPublicKeyScript(appKit.nodeParams.chainHash, fa) match { case Left(failure) => throw new IllegalArgumentException(failure.toString) case Right(_) => () } - } // if it's not a bitcoin address throws an exception - appKit.paymentHandler.toTyped.ask(ref => ReceiveStandardPayment(ref, amount_opt, description, expire_opt, fallbackAddress_opt = fallbackAddress_opt, paymentPreimage_opt = paymentPreimage_opt)) + } + for { + routingHints <- getInvoiceRoutingHints(privateChannelIds_opt) + invoice <- appKit.paymentHandler.toTyped.ask[Bolt11Invoice](ref => ReceiveStandardPayment(ref, amount_opt, description, expire_opt, routingHints, fallbackAddress_opt, paymentPreimage_opt)) + } yield invoice + } + + private def getInvoiceRoutingHints(privateChannelIds_opt: Option[List[ByteVector32]])(implicit timeout: Timeout): Future[List[List[Bolt11Invoice.ExtraHop]]] = { + privateChannelIds_opt match { + case Some(channelIds) => + (appKit.router ? GetRouterData).mapTo[Router.Data].map { + d => channelIds.flatMap(cid => d.privateChannels.get(cid)).flatMap(_.toIncomingExtraHop).map(hop => hop :: Nil) + } + case None => Future.successful(Nil) + } } override def newAddress(): Future[String] = { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala index cb2a6b1fb3..cdfdc5d46a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala @@ -313,7 +313,7 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I val fallBackAddressRaw = "muhtvdmsnbQEPFuEmxcChX58fGvXaaUoVt" val eclair = new EclairImpl(kit) - eclair.receive(Left("some desc"), Some(123 msat), Some(456), Some(fallBackAddressRaw), None) + eclair.receive(Left("some desc"), Some(123 msat), Some(456), Some(fallBackAddressRaw), None, None) val receive = paymentHandler.expectMsgType[ReceiveStandardPayment] assert(receive.amount_opt.contains(123 msat)) @@ -321,7 +321,7 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I assert(receive.fallbackAddress_opt.contains(fallBackAddressRaw)) // try with wrong address format - assertThrows[IllegalArgumentException](eclair.receive(Left("some desc"), Some(123 msat), Some(456), Some("wassa wassa"), None)) + assertThrows[IllegalArgumentException](eclair.receive(Left("some desc"), Some(123 msat), Some(456), Some("wassa wassa"), None, None)) } test("passing a payment_preimage to /createinvoice should result in an invoice with payment_hash=H(payment_preimage)") { f => @@ -331,7 +331,7 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I val eclair = new EclairImpl(kitWithPaymentHandler) val paymentPreimage = randomBytes32() - eclair.receive(Left("some desc"), None, None, None, Some(paymentPreimage)).pipeTo(sender.ref) + eclair.receive(Left("some desc"), None, None, None, Some(paymentPreimage), None).pipeTo(sender.ref) assert(sender.expectMsgType[Invoice].paymentHash == Crypto.sha256(paymentPreimage)) } diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Invoice.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Invoice.scala index 025efc8f6e..767b182f83 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Invoice.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Invoice.scala @@ -18,6 +18,7 @@ package fr.acinq.eclair.api.handlers import akka.http.scaladsl.server.Route import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.api.Service import fr.acinq.eclair.api.directives.EclairDirectives import fr.acinq.eclair.api.serde.FormParamExtractors._ @@ -28,9 +29,9 @@ trait Invoice { import fr.acinq.eclair.api.serde.JsonSupport.{formats, marshaller, serialization} val createInvoice: Route = postRequest("createinvoice") { implicit t => - formFields("description".as[String].?, "descriptionHash".as[ByteVector32].?, amountMsatFormParam.?, "expireIn".as[Long].?, "fallbackAddress".as[String].?, "paymentPreimage".as[ByteVector32](bytes32Unmarshaller).?) { - case (Some(desc), None, amountMsat, expire, fallBackAddress, paymentPreimage_opt) => complete(eclairApi.receive(Left(desc), amountMsat, expire, fallBackAddress, paymentPreimage_opt)) - case (None, Some(desc), amountMsat, expire, fallBackAddress, paymentPreimage_opt) => complete(eclairApi.receive(Right(desc), amountMsat, expire, fallBackAddress, paymentPreimage_opt)) + formFields("description".as[String].?, "descriptionHash".as[ByteVector32].?, amountMsatFormParam.?, "expireIn".as[Long].?, "fallbackAddress".as[String].?, "paymentPreimage".as[ByteVector32](bytes32Unmarshaller).?, "privateChannelIds".as[List[ByteVector32]](bytes32ListUnmarshaller).?) { + case (Some(desc), None, amountMsat, expire, fallBackAddress, paymentPreimage_opt, privateChannelIds_opt) => complete(eclairApi.receive(Left(desc), amountMsat, expire, fallBackAddress, paymentPreimage_opt, privateChannelIds_opt)) + case (None, Some(desc), amountMsat, expire, fallBackAddress, paymentPreimage_opt, privateChannelIds_opt) => complete(eclairApi.receive(Right(desc), amountMsat, expire, fallBackAddress, paymentPreimage_opt, privateChannelIds_opt)) case _ => failWith(new RuntimeException("Either 'description' (string) or 'descriptionHash' (sha256 hash of description string) must be supplied")) } } From b15c80688159fd565b11efc7b3d10ae183c2bbca Mon Sep 17 00:00:00 2001 From: t-bast Date: Mon, 23 Sep 2024 03:33:03 +0200 Subject: [PATCH 2/2] fixup! Allow including routing hints when creating Bolt 11 invoice --- docs/release-notes/eclair-vnext.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release-notes/eclair-vnext.md b/docs/release-notes/eclair-vnext.md index c92646390a..f30a78b7e0 100644 --- a/docs/release-notes/eclair-vnext.md +++ b/docs/release-notes/eclair-vnext.md @@ -27,7 +27,7 @@ Eclair will not allow remote peers to open new obsolete channels that do not sup ### API changes - `channelstats` now takes optional parameters `--count` and `--skip` to control pagination. By default, it will return first 10 entries. (#2890) -- `createinvoice` now takes an optional `--privateChannelIds` parameter that can be used to add routing hints through private channels. (#TODO) +- `createinvoice` now takes an optional `--privateChannelIds` parameter that can be used to add routing hints through private channels. (#2909) ### Miscellaneous improvements and bug fixes