Skip to content

Commit

Permalink
Add support for sciddir_or_pubkey
Browse files Browse the repository at this point in the history
Offers (lightning/bolts#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.
  • Loading branch information
thomash-acinq committed Sep 26, 2023
1 parent 3e1cc9d commit bfff706
Show file tree
Hide file tree
Showing 11 changed files with 124 additions and 59 deletions.
8 changes: 4 additions & 4 deletions eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
67 changes: 43 additions & 24 deletions eclair-core/src/main/scala/fr/acinq/eclair/message/Postman.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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)))

Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -143,40 +146,56 @@ 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")
Behaviors.stopped
}
}

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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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))) {
Expand All @@ -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()
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
12 changes: 11 additions & 1 deletion eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 }
Expand Down
Loading

0 comments on commit bfff706

Please sign in to comment.