From 6c662a33e1676bd97e9fab3420ab67a61675059f Mon Sep 17 00:00:00 2001 From: Thomas HUET Date: Mon, 11 Sep 2023 11:08:00 +0200 Subject: [PATCH] Add support for sciddir_or_pubkey Offers (https://github.com/lightning/bolts/pull/798) allow nodes to be identified using either the public key or a pair channel id and direction. The goal is to save bytes as channel id and direction only use 9 bytes instead of 33 for a public key. --- .../main/scala/fr/acinq/eclair/Eclair.scala | 8 +-- .../acinq/eclair/json/JsonSerializers.scala | 12 +++- .../acinq/eclair/message/OnionMessages.scala | 8 ++- .../fr/acinq/eclair/message/Postman.scala | 67 ++++++++++++------- .../acinq/eclair/payment/Bolt12Invoice.scala | 2 +- .../eclair/payment/offer/OfferManager.scala | 11 +-- .../payment/receive/MultiPartHandler.scala | 2 +- .../eclair/payment/send/OfferPayment.scala | 10 +-- .../scala/fr/acinq/eclair/router/Router.scala | 12 +++- .../eclair/wire/protocol/OfferCodecs.scala | 32 +++++++-- .../eclair/wire/protocol/OfferTypes.scala | 19 ++++-- 11 files changed, 124 insertions(+), 59 deletions(-) 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 b7ee3bdee2..f6d24c1ca0 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -663,15 +663,15 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { userCustomContent: ByteVector)(implicit timeout: Timeout): Future[SendOnionMessageResponse] = { TlvCodecs.tlvStream(MessageOnionCodecs.onionTlvCodec).decode(userCustomContent.bits) match { case Attempt.Successful(DecodeResult(userTlvs, _)) => - val destination = recipient match { - case Left(key) => OnionMessages.Recipient(key, None) - case Right(route) => OnionMessages.BlindedPath(route) + val contactInfo = recipient match { + case Left(key) => OfferTypes.RecipientNodeId(key) + case Right(route) => OfferTypes.BlindedPath(route) } val routingStrategy = intermediateNodes_opt match { case Some(intermediateNodes) => OnionMessages.RoutingStrategy.UseRoute(intermediateNodes) case None => OnionMessages.RoutingStrategy.FindRoute } - appKit.postman.ask(ref => Postman.SendMessage(destination, routingStrategy, userTlvs, expectsReply, ref)).map { + appKit.postman.ask(ref => Postman.SendMessage(contactInfo, routingStrategy, userTlvs, expectsReply, ref)).map { case Postman.Response(payload) => SendOnionMessageResponse(sent = true, None, Some(SendOnionMessageResponsePayload(payload.records))) case Postman.NoReply => SendOnionMessageResponse(sent = true, Some("No response"), None) case Postman.MessageSent => SendOnionMessageResponse(sent = true, None, None) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala index dccf8fbaf6..c093cdd575 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala @@ -440,9 +440,17 @@ object InvoiceSerializer extends MinimalSerializer({ UnknownFeatureSerializer )), JField("blindedPaths", JArray(p.blindedPaths.map(path => { + val introductionNode = path.route match { + case OfferTypes.BlindedPath(route) => route.introductionNodeId.toString + case OfferTypes.CompactBlindedPath(shortIdDir, _, _) => s"${if (shortIdDir.isNode1) '0' else '1'}x${shortIdDir.scid.toString}" + } + val blindedNodes = path.route match { + case OfferTypes.BlindedPath(route) => route.blindedNodes + case OfferTypes.CompactBlindedPath(_, _, nodes) => nodes + } JObject(List( - JField("introductionNodeId", JString(path.route.introductionNodeId.toString())), - JField("blindedNodeIds", JArray(path.route.blindedNodes.map(n => JString(n.blindedPublicKey.toString())).toList)) + JField("introductionNodeId", JString(introductionNode)), + JField("blindedNodeIds", JArray(blindedNodes.map(n => JString(n.blindedPublicKey.toString)).toList)) )) }).toList)), JField("createdAt", JLong(p.createdAt.toLong)), diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/message/OnionMessages.scala b/eclair-core/src/main/scala/fr/acinq/eclair/message/OnionMessages.scala index e6a83b3d19..23adaf2397 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/message/OnionMessages.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/message/OnionMessages.scala @@ -46,8 +46,12 @@ object OnionMessages { case class IntermediateNode(nodeId: PublicKey, padding: Option[ByteVector] = None, customTlvs: Set[GenericTlv] = Set.empty) // @formatter:off - sealed trait Destination - case class BlindedPath(route: Sphinx.RouteBlinding.BlindedRoute) extends Destination + sealed trait Destination { + def nodeId: PublicKey + } + case class BlindedPath(route: Sphinx.RouteBlinding.BlindedRoute) extends Destination { + override def nodeId: PublicKey = route.introductionNodeId + } case class Recipient(nodeId: PublicKey, pathId: Option[ByteVector], padding: Option[ByteVector] = None, customTlvs: Set[GenericTlv] = Set.empty) extends Destination // @formatter:on diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/message/Postman.scala b/eclair-core/src/main/scala/fr/acinq/eclair/message/Postman.scala index 759f3f325a..ceaee9cff5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/message/Postman.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/message/Postman.scala @@ -22,13 +22,15 @@ import akka.actor.typed.scaladsl.{ActorContext, Behaviors} import akka.actor.typed.{ActorRef, Behavior} import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.eclair.crypto.Sphinx.RouteBlinding.BlindedRoute import fr.acinq.eclair.io.{MessageRelay, Switchboard} import fr.acinq.eclair.message.OnionMessages.{Destination, RoutingStrategy} import fr.acinq.eclair.payment.offer.OfferManager import fr.acinq.eclair.router.Router import fr.acinq.eclair.router.Router.{MessageRoute, MessageRouteNotFound, MessageRouteResponse} import fr.acinq.eclair.wire.protocol.MessageOnion.{FinalPayload, InvoiceRequestPayload} -import fr.acinq.eclair.wire.protocol.{OnionMessagePayloadTlv, TlvStream} +import fr.acinq.eclair.wire.protocol.OfferTypes.{CompactBlindedPath, ContactInfo} +import fr.acinq.eclair.wire.protocol.{OfferTypes, OnionMessagePayloadTlv, TlvStream} import fr.acinq.eclair.{NodeParams, randomBytes32, randomKey} import scala.collection.mutable @@ -39,13 +41,13 @@ object Postman { /** * Builds a message packet and send it to the destination using the provided path. * - * @param destination Recipient of the message + * @param contactInfo Recipient of the message * @param routingStrategy How to reach the destination (recipient or blinded path introduction node). * @param message Content of the message to send * @param expectsReply Whether the message expects a reply * @param replyTo Actor to send the status and reply to */ - case class SendMessage(destination: Destination, + case class SendMessage(contactInfo: ContactInfo, routingStrategy: RoutingStrategy, message: TlvStream[OnionMessagePayloadTlv], expectsReply: Boolean, @@ -62,7 +64,7 @@ object Postman { case class MessageFailed(reason: String) extends MessageStatus // @formatter:on - def apply(nodeParams: NodeParams, switchboard: ActorRef[Switchboard.RelayMessage], router: ActorRef[Router.MessageRouteRequest], offerManager: typed.ActorRef[OfferManager.RequestInvoice]): Behavior[Command] = { + def apply(nodeParams: NodeParams, switchboard: ActorRef[Switchboard.RelayMessage], router: ActorRef[Router.PostmanRequest], offerManager: typed.ActorRef[OfferManager.RequestInvoice]): Behavior[Command] = { Behaviors.setup(context => { context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[OnionMessages.ReceiveMessage](r => WrappedMessage(r.finalPayload))) @@ -109,13 +111,14 @@ object SendingMessage { case object SendMessage extends Command private case class SendingStatus(status: MessageRelay.Status) extends Command private case class WrappedMessageRouteResponse(response: MessageRouteResponse) extends Command + private case class WrappedNodeIdResponse(nodeId_opt: Option[PublicKey]) extends Command // @formatter:on def apply(nodeParams: NodeParams, switchboard: ActorRef[Switchboard.RelayMessage], - router: ActorRef[Router.MessageRouteRequest], + router: ActorRef[Router.PostmanRequest], postman: ActorRef[Postman.Command], - destination: Destination, + destination: ContactInfo, message: TlvStream[OnionMessagePayloadTlv], routingStrategy: RoutingStrategy, expectsReply: Boolean, @@ -129,9 +132,9 @@ object SendingMessage { private class SendingMessage(nodeParams: NodeParams, switchboard: ActorRef[Switchboard.RelayMessage], - router: ActorRef[Router.MessageRouteRequest], + router: ActorRef[Router.PostmanRequest], postman: ActorRef[Postman.Command], - destination: Destination, + contactInfo: ContactInfo, message: TlvStream[OnionMessagePayloadTlv], routingStrategy: RoutingStrategy, expectsReply: Boolean, @@ -143,27 +146,43 @@ private class SendingMessage(nodeParams: NodeParams, def start(): Behavior[Command] = { Behaviors.receiveMessagePartial { case SendMessage => - val targetNodeId = destination match { - case OnionMessages.BlindedPath(route) => route.introductionNodeId - case OnionMessages.Recipient(nodeId, _, _, _) => nodeId - } - routingStrategy match { - case RoutingStrategy.UseRoute(intermediateNodes) => sendToRoute(intermediateNodes, targetNodeId) - case RoutingStrategy.FindRoute if targetNodeId == nodeParams.nodeId => - context.self ! WrappedMessageRouteResponse(MessageRoute(Nil, targetNodeId)) - waitForRouteFromRouter() - case RoutingStrategy.FindRoute => - router ! Router.MessageRouteRequest(context.messageAdapter(WrappedMessageRouteResponse), nodeParams.nodeId, targetNodeId, Set.empty) - waitForRouteFromRouter() + contactInfo match { + case compact: OfferTypes.CompactBlindedPath => + router ! Router.GetNodeId(context.messageAdapter(WrappedNodeIdResponse), compact.introductionNode.scid, compact.introductionNode.isNode1) + waitForNodeId(compact) + case OfferTypes.BlindedPath(route) => sendToDestination(OnionMessages.BlindedPath(route)) + case OfferTypes.RecipientNodeId(nodeId) => sendToDestination(OnionMessages.Recipient(nodeId, None)) } } } - private def waitForRouteFromRouter(): Behavior[Command] = { + private def waitForNodeId(compactBlindedPath: CompactBlindedPath): Behavior[Command] = { + Behaviors.receiveMessagePartial { + case WrappedNodeIdResponse(None) => + replyTo ! Postman.MessageFailed("Unknown target") + Behaviors.stopped + case WrappedNodeIdResponse(Some(nodeId)) => + sendToDestination(OnionMessages.BlindedPath(BlindedRoute(nodeId, compactBlindedPath.blindingKey, compactBlindedPath.blindedNodes))) + } + } + + private def sendToDestination(destination: Destination): Behavior[Command] = { + routingStrategy match { + case RoutingStrategy.UseRoute(intermediateNodes) => sendToRoute(intermediateNodes, destination) + case RoutingStrategy.FindRoute if destination.nodeId == nodeParams.nodeId => + context.self ! WrappedMessageRouteResponse(MessageRoute(Nil, destination.nodeId)) + waitForRouteFromRouter(destination) + case RoutingStrategy.FindRoute => + router ! Router.MessageRouteRequest(context.messageAdapter(WrappedMessageRouteResponse), nodeParams.nodeId, destination.nodeId, Set.empty) + waitForRouteFromRouter(destination) + } + } + + private def waitForRouteFromRouter(destination: Destination): Behavior[Command] = { Behaviors.receiveMessagePartial { case WrappedMessageRouteResponse(MessageRoute(intermediateNodes, targetNodeId)) => context.log.debug("Found route: {}", (intermediateNodes :+ targetNodeId).mkString(" -> ")) - sendToRoute(intermediateNodes, targetNodeId) + sendToRoute(intermediateNodes, destination) case WrappedMessageRouteResponse(MessageRouteNotFound(targetNodeId)) => context.log.debug("No route found to {}", targetNodeId) replyTo ! Postman.MessageFailed("No route found") @@ -171,12 +190,12 @@ private class SendingMessage(nodeParams: NodeParams, } } - private def sendToRoute(intermediateNodes: Seq[PublicKey], targetNodeId: PublicKey): Behavior[Command] = { + private def sendToRoute(intermediateNodes: Seq[PublicKey], destination: Destination): Behavior[Command] = { val messageId = randomBytes32() val replyRoute = if (expectsReply) { val numHopsToAdd = 0.max(nodeParams.onionMessageConfig.minIntermediateHops - intermediateNodes.length - 1) - val intermediateHops = (Seq(targetNodeId) ++ intermediateNodes.reverse ++ Seq.fill(numHopsToAdd)(nodeParams.nodeId)).map(OnionMessages.IntermediateNode(_)) + val intermediateHops = (Seq(destination.nodeId) ++ intermediateNodes.reverse ++ Seq.fill(numHopsToAdd)(nodeParams.nodeId)).map(OnionMessages.IntermediateNode(_)) val lastHop = OnionMessages.Recipient(nodeParams.nodeId, Some(messageId)) Some(OnionMessages.buildRoute(randomKey(), intermediateHops, lastHop)) } else { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt12Invoice.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt12Invoice.scala index 0d96f5b244..0307ab5d11 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt12Invoice.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt12Invoice.scala @@ -86,7 +86,7 @@ case class Bolt12Invoice(records: TlvStream[InvoiceTlv]) extends Invoice { } -case class PaymentBlindedRoute(route: Sphinx.RouteBlinding.BlindedRoute, paymentInfo: PaymentInfo) +case class PaymentBlindedRoute(route: BlindedContactInfo, paymentInfo: PaymentInfo) object Bolt12Invoice { val hrp = "lni" diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/offer/OfferManager.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/offer/OfferManager.scala index 4c2d511c6c..4800b00afe 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/offer/OfferManager.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/offer/OfferManager.scala @@ -20,6 +20,7 @@ import akka.actor.typed.scaladsl.{ActorContext, Behaviors} import akka.actor.typed.{ActorRef, Behavior} import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto} +import fr.acinq.eclair.crypto.Sphinx.RouteBlinding import fr.acinq.eclair.db.{IncomingBlindedPayment, IncomingPaymentStatus, PaymentType} import fr.acinq.eclair.message.{OnionMessages, Postman} import fr.acinq.eclair.payment.MinimalBolt12Invoice @@ -106,7 +107,7 @@ object OfferManager { case RequestInvoice(messagePayload, postman) => registeredOffers.get(messagePayload.invoiceRequest.offer.offerId) match { case Some(registered) if registered.pathId_opt.map(_.bytes) == messagePayload.pathId_opt && messagePayload.invoiceRequest.isValid => - val child = context.spawnAnonymous(InvoiceRequestActor(nodeParams, messagePayload.invoiceRequest, registered.handler, registered.nodeKey, router, OnionMessages.BlindedPath(messagePayload.replyPath), postman)) + val child = context.spawnAnonymous(InvoiceRequestActor(nodeParams, messagePayload.invoiceRequest, registered.handler, registered.nodeKey, router, messagePayload.replyPath, postman)) child ! InvoiceRequestActor.RequestInvoice case _ => context.log.debug("offer {} is not registered or invoice request is invalid", messagePayload.invoiceRequest.offer.offerId) } @@ -167,7 +168,7 @@ object OfferManager { offerHandler: ActorRef[HandleInvoiceRequest], nodeKey: PrivateKey, router: akka.actor.ActorRef, - pathToSender: OnionMessages.Destination, + pathToSender: RouteBlinding.BlindedRoute, postman: ActorRef[Postman.SendMessage]): Behavior[Command] = { Behaviors.setup { context => Behaviors.withMdc(Logs.mdc(category_opt = Some(Logs.LogCategory.PAYMENT))) { @@ -184,13 +185,13 @@ object OfferManager { invoiceRequest: InvoiceRequest, nodeKey: PrivateKey, router: akka.actor.ActorRef, - pathToSender: OnionMessages.Destination, + pathToSender: RouteBlinding.BlindedRoute, postman: ActorRef[Postman.SendMessage], context: ActorContext[Command]) { def waitForHandler(): Behavior[Command] = { Behaviors.receiveMessagePartial { case RejectRequest(error) => - postman ! Postman.SendMessage(pathToSender, OnionMessages.RoutingStrategy.FindRoute, TlvStream(OnionMessagePayloadTlv.InvoiceError(TlvStream(OfferTypes.Error(error)))), expectsReply = false, context.messageAdapter[Postman.OnionMessageResponse](WrappedOnionMessageResponse)) + postman ! Postman.SendMessage(OfferTypes.BlindedPath(pathToSender), OnionMessages.RoutingStrategy.FindRoute, TlvStream(OnionMessagePayloadTlv.InvoiceError(TlvStream(OfferTypes.Error(error)))), expectsReply = false, context.messageAdapter[Postman.OnionMessageResponse](WrappedOnionMessageResponse)) waitForSent() case ApproveRequest(amount, routes, pluginData_opt, additionalTlvs, customTlvs) => val preimage = randomBytes32() @@ -208,7 +209,7 @@ object OfferManager { case WrappedInvoiceResponse(invoiceResponse) => invoiceResponse match { case CreateInvoiceActor.InvoiceCreated(invoice) => - postman ! Postman.SendMessage(pathToSender, OnionMessages.RoutingStrategy.FindRoute, TlvStream(OnionMessagePayloadTlv.Invoice(invoice.records)), expectsReply = false, context.messageAdapter[Postman.OnionMessageResponse](WrappedOnionMessageResponse)) + postman ! Postman.SendMessage(OfferTypes.BlindedPath(pathToSender), OnionMessages.RoutingStrategy.FindRoute, TlvStream(OnionMessagePayloadTlv.Invoice(invoice.records)), expectsReply = false, context.messageAdapter[Postman.OnionMessageResponse](WrappedOnionMessageResponse)) waitForSent() case f: CreateInvoiceActor.InvoiceCreationFailed => context.log.debug("invoice creation failed: {}", f.message) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala index 472156ecad..a7a3930bbf 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala @@ -381,7 +381,7 @@ object MultiPartHandler { } })).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(blindedRoute.route, paymentInfo) }, r.additionalTlvs, r.customTlvs) + 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) log.debug("generated invoice={} for offer={}", invoice.toString, r.invoiceRequest.offer.toString) invoice }))(WrappedInvoiceResult) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/OfferPayment.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/OfferPayment.scala index 0a0ab233c0..fd2afc4afd 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/OfferPayment.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/OfferPayment.scala @@ -103,16 +103,10 @@ object OfferPayment { replyTo: ActorRef, attemptNumber: Int, sendPaymentConfig: SendPaymentConfig): Behavior[Command] = { - val destination = request.offer.contactInfo match { - case Left(blindedRoutes) => - val blindedRoute = blindedRoutes(attemptNumber % blindedRoutes.length) - OnionMessages.BlindedPath(blindedRoute) - case Right(nodeId) => - OnionMessages.Recipient(nodeId, None, None) - } + val contactInfo = request.offer.contactInfos(attemptNumber % request.offer.contactInfos.length) val messageContent = TlvStream[OnionMessagePayloadTlv](OnionMessagePayloadTlv.InvoiceRequest(request.records)) val routingStrategy = if (sendPaymentConfig.connectDirectly) OnionMessages.RoutingStrategy.connectDirectly else OnionMessages.RoutingStrategy.FindRoute - postman ! SendMessage(destination, routingStrategy, messageContent, expectsReply = true, context.messageAdapter(WrappedMessageResponse)) + postman ! SendMessage(contactInfo, routingStrategy, messageContent, expectsReply = true, context.messageAdapter(WrappedMessageResponse)) waitForInvoice(nodeParams, postman, paymentInitiator, context, request, payerKey, replyTo, attemptNumber + 1, sendPaymentConfig) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala index e5ff4d4654..55fa5fe1d8 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala @@ -243,6 +243,10 @@ class Router(val nodeParams: NodeParams, watcher: typed.ActorRef[ZmqWatcher.Comm case Event(r: MessageRouteRequest, d) => stay() using RouteCalculation.handleMessageRouteRequest(d, nodeParams.currentBlockHeight, r, nodeParams.routerConf.messageRouteParams) + case Event(GetNodeId(replyTo, shortChannelId, isNode1), d) => + replyTo ! d.channels.get(shortChannelId).map(channel => if (isNode1) channel.nodeId1 else channel.nodeId2) + stay() + // Warning: order matters here, this must be the first match for HasChainHash messages ! case Event(PeerRoutingMessage(_, _, routingMessage: HasChainHash), _) if routingMessage.chainHash != nodeParams.chainHash => sender() ! TransportHandler.ReadAck(routingMessage) @@ -589,10 +593,16 @@ object Router { extraEdges: Seq[ExtraEdge] = Nil, paymentContext: Option[PaymentContext] = None) + sealed trait PostmanRequest + case class MessageRouteRequest(replyTo: typed.ActorRef[MessageRouteResponse], source: PublicKey, target: PublicKey, - ignoredNodes: Set[PublicKey]) + ignoredNodes: Set[PublicKey]) extends PostmanRequest + + case class GetNodeId(replyTo: typed.ActorRef[Option[PublicKey]], + shortChannelId: RealShortChannelId, + isNode1: Boolean) extends PostmanRequest // @formatter:off sealed trait MessageRouteResponse { def target: PublicKey } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OfferCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OfferCodecs.scala index d3079fec97..420778c72f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OfferCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OfferCodecs.scala @@ -22,7 +22,7 @@ import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.OfferTypes.{InvoiceRequestChain, InvoiceRequestPayerNote, InvoiceRequestQuantity, _} import fr.acinq.eclair.wire.protocol.TlvCodecs.{tlvField, tmillisatoshi, tu32, tu64overflow} import fr.acinq.eclair.{TimestampSecond, UInt64} -import scodec.Codec +import scodec.{Attempt, Codec, Err} import scodec.codecs._ object OfferCodecs { @@ -46,12 +46,34 @@ object OfferCodecs { private val blindedNodesCodec: Codec[Seq[BlindedNode]] = listOfN(uint8, blindedNodeCodec).xmap(_.toSeq, _.toList) - private val pathCodec: Codec[BlindedRoute] = + private val blindedPathCodec: Codec[BlindedPath] = (("firstNodeId" | publicKey) :: ("blinding" | publicKey) :: - ("path" | blindedNodesCodec)).as[BlindedRoute] + ("path" | blindedNodesCodec)).as[BlindedRoute].as[BlindedPath] - private val offerPaths: Codec[OfferPaths] = tlvField(list(pathCodec).xmap[Seq[BlindedRoute]](_.toSeq, _.toList)) + private val isNode1: Codec[Boolean] = uint8.narrow( + n => if (n == 0) Attempt.Successful(true) else if (n == 1) Attempt.Successful(false) else Attempt.Failure(new Err.MatchingDiscriminatorNotFound(n)), + b => if (b) 0 else 1 + ) + + private val shortChannelIdDirCodec: Codec[ShortChannelIdDir] = + (("isNode1" | isNode1) :: + ("scid" | realshortchannelid)).as[ShortChannelIdDir] + + private val compactBlindedPathCodec: Codec[CompactBlindedPath] = + (("introductionNode" | shortChannelIdDirCodec) :: + ("blinding" | publicKey) :: + ("path" | blindedNodesCodec)).as[CompactBlindedPath] + + private val pathCodec: Codec[BlindedContactInfo] = fallback(blindedPathCodec, compactBlindedPathCodec).xmap({ + case Left(path) => path + case Right(compact) => compact + }, { + case path: BlindedPath => Left(path) + case compact: CompactBlindedPath => Right(compact) + }) + + private val offerPaths: Codec[OfferPaths] = tlvField(list(pathCodec).xmap[Seq[BlindedContactInfo]](_.toSeq, _.toList)) private val offerIssuer: Codec[OfferIssuer] = tlvField(utf8) @@ -114,7 +136,7 @@ object OfferCodecs { .typecase(UInt64(240), signature) ).complete - private val invoicePaths: Codec[InvoicePaths] = tlvField(list(pathCodec).xmap[Seq[BlindedRoute]](_.toSeq, _.toList)) + private val invoicePaths: Codec[InvoicePaths] = tlvField(list(pathCodec).xmap[Seq[BlindedContactInfo]](_.toSeq, _.toList)) private val paymentInfo: Codec[PaymentInfo] = (("fee_base_msat" | millisatoshi32) :: diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OfferTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OfferTypes.scala index d359396327..4f6c183bb6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OfferTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OfferTypes.scala @@ -19,11 +19,11 @@ package fr.acinq.eclair.wire.protocol import fr.acinq.bitcoin.Bech32 import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey, XonlyPublicKey} import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, Crypto, LexicographicalOrdering} -import fr.acinq.eclair.crypto.Sphinx.RouteBlinding.BlindedRoute +import fr.acinq.eclair.crypto.Sphinx.RouteBlinding.{BlindedNode, BlindedRoute} import fr.acinq.eclair.wire.protocol.CommonCodecs.varint import fr.acinq.eclair.wire.protocol.OnionRoutingCodecs.{ForbiddenTlv, InvalidTlvPayload, MissingRequiredTlv} import fr.acinq.eclair.wire.protocol.TlvCodecs.genericTlv -import fr.acinq.eclair.{Bolt12Feature, CltvExpiryDelta, Feature, Features, MilliSatoshi, TimestampSecond, UInt64, nodeFee, randomBytes32} +import fr.acinq.eclair.{Bolt12Feature, CltvExpiryDelta, Feature, Features, MilliSatoshi, RealShortChannelId, ShortChannelId, TimestampSecond, UInt64, nodeFee, randomBytes32} import scodec.Codec import scodec.bits.ByteVector import scodec.codecs.vector @@ -35,6 +35,13 @@ import scala.util.{Failure, Try} * see https://github.com/lightning/bolts/blob/master/12-offer-encoding.md */ object OfferTypes { + case class ShortChannelIdDir(isNode1: Boolean, scid: RealShortChannelId) + + sealed trait ContactInfo + sealed trait BlindedContactInfo extends ContactInfo + case class BlindedPath(route: BlindedRoute) extends BlindedContactInfo + case class CompactBlindedPath(introductionNode: ShortChannelIdDir, blindingKey: PublicKey, blindedNodes: Seq[BlindedNode]) extends BlindedContactInfo + case class RecipientNodeId(nodeId: PublicKey) extends ContactInfo sealed trait Bolt12Tlv extends Tlv @@ -84,7 +91,7 @@ object OfferTypes { /** * Paths that can be used to retrieve an invoice. */ - case class OfferPaths(paths: Seq[BlindedRoute]) extends OfferTlv + case class OfferPaths(paths: Seq[BlindedContactInfo]) extends OfferTlv /** * Name of the offer creator. @@ -144,7 +151,7 @@ object OfferTypes { /** * Payment paths to send the payment to. */ - case class InvoicePaths(paths: Seq[BlindedRoute]) extends InvoiceTlv + case class InvoicePaths(paths: Seq[BlindedContactInfo]) extends InvoiceTlv case class PaymentInfo(feeBase: MilliSatoshi, feeProportionalMillionths: Long, @@ -228,12 +235,12 @@ object OfferTypes { val description: String = records.get[OfferDescription].get.description val features: Features[Bolt12Feature] = records.get[OfferFeatures].map(_.features.bolt12Features()).getOrElse(Features.empty) val expiry: Option[TimestampSecond] = records.get[OfferAbsoluteExpiry].map(_.absoluteExpiry) - private val paths: Option[Seq[BlindedRoute]] = records.get[OfferPaths].map(_.paths) + private val paths: Option[Seq[BlindedContactInfo]] = records.get[OfferPaths].map(_.paths) val issuer: Option[String] = records.get[OfferIssuer].map(_.issuer) val quantityMax: Option[Long] = records.get[OfferQuantityMax].map(_.max).map { q => if (q == 0) Long.MaxValue else q } val nodeId: PublicKey = records.get[OfferNodeId].map(_.publicKey).get - val contactInfo: Either[Seq[BlindedRoute], PublicKey] = paths.map(Left(_)).getOrElse(Right(nodeId)) + val contactInfos: Seq[ContactInfo] = paths.getOrElse(Seq(RecipientNodeId(nodeId))) def encode(): String = { val data = OfferCodecs.offerTlvCodec.encode(records).require.bytes