@@ -22,7 +22,7 @@ import fr.acinq.eclair.channel.{CMD_ADD_HTLC, CMD_FAIL_HTLC, CannotExtractShared
2222import fr .acinq .eclair .crypto .Sphinx
2323import fr .acinq .eclair .payment .send .Recipient
2424import fr .acinq .eclair .router .Router .Route
25- import fr .acinq .eclair .wire .protocol .OnionPaymentPayloadTlv .OutgoingBlindedPaths
25+ import fr .acinq .eclair .wire .protocol .OnionPaymentPayloadTlv .{ InvoiceRoutingInfo , OutgoingBlindedPaths }
2626import fr .acinq .eclair .wire .protocol .PaymentOnion .{FinalPayload , IntermediatePayload , PerHopPayload }
2727import fr .acinq .eclair .wire .protocol ._
2828import fr .acinq .eclair .{CltvExpiry , CltvExpiryDelta , Feature , Features , MilliSatoshi , ShortChannelId , UInt64 , randomKey }
@@ -59,6 +59,7 @@ object IncomingPaymentPacket {
5959 def innerPayload : IntermediatePayload .NodeRelay
6060 }
6161 case class RelayToTrampolinePacket (add : UpdateAddHtlc , outerPayload : FinalPayload .Standard , innerPayload : IntermediatePayload .NodeRelay .Standard , nextPacket : OnionRoutingPacket ) extends NodeRelayPacket
62+ case class RelayToNonTrampolinePacket (add : UpdateAddHtlc , outerPayload : FinalPayload .Standard , innerPayload : IntermediatePayload .NodeRelay .ToNonTrampoline ) extends NodeRelayPacket
6263 case class RelayToBlindedPathsPacket (add : UpdateAddHtlc , outerPayload : FinalPayload .Standard , innerPayload : IntermediatePayload .NodeRelay .ToBlindedPaths ) extends NodeRelayPacket
6364 // @formatter:on
6465
@@ -113,16 +114,18 @@ object IncomingPaymentPacket {
113114 * @return whether the payment is to be relayed or if our node is the final recipient (or an error).
114115 */
115116 def decrypt (add : UpdateAddHtlc , privateKey : PrivateKey , features : Features [Feature ]): Either [FailureMessage , IncomingPaymentPacket ] = {
116- // We first derive the decryption key used to peel the onion.
117+ // We first derive the decryption key used to peel the outer onion.
117118 val outerOnionDecryptionKey = add.blinding_opt match {
118119 case Some (blinding) => Sphinx .RouteBlinding .derivePrivateKey(privateKey, blinding)
119120 case None => privateKey
120121 }
121122 decryptOnion(add.paymentHash, outerOnionDecryptionKey, add.onionRoutingPacket).flatMap {
122123 case DecodedOnionPacket (payload, Some (nextPacket)) =>
124+ // We are an intermediate node: we need to relay to one of our peers.
123125 payload.get[OnionPaymentPayloadTlv .EncryptedRecipientData ] match {
124126 case Some (_) if ! features.hasFeature(Features .RouteBlinding ) => Left (InvalidOnionPayload (UInt64 (10 ), 0 ))
125127 case Some (encrypted) =>
128+ // We are inside a blinded path: channel relay information is encrypted.
126129 decryptEncryptedRecipientData(add, privateKey, payload, encrypted.data).flatMap {
127130 case DecodedEncryptedRecipientData (blindedPayload, nextBlinding) =>
128131 validateBlindedChannelRelayPayload(add, payload, blindedPayload, nextBlinding, nextPacket).flatMap {
@@ -132,14 +135,18 @@ object IncomingPaymentPacket {
132135 }
133136 }
134137 case None if add.blinding_opt.isDefined => Left (InvalidOnionBlinding (Sphinx .hash(add.onionRoutingPacket)))
135- case None => IntermediatePayload . ChannelRelay . Standard .validate(payload).left.map(_.failureMessage).map {
136- payload => ChannelRelayPacket (add, payload, nextPacket)
137- }
138+ case None =>
139+ // We are not inside a blinded path: channel relay information is directly available.
140+ IntermediatePayload . ChannelRelay . Standard .validate(payload).left.map(_.failureMessage).map(payload => ChannelRelayPacket (add, payload, nextPacket))
138141 }
139142 case DecodedOnionPacket (payload, None ) =>
143+ // We are the final node for the outer onion, so we are either:
144+ // - the final recipient of the payment.
145+ // - an intermediate trampoline node.
140146 payload.get[OnionPaymentPayloadTlv .EncryptedRecipientData ] match {
141147 case Some (_) if ! features.hasFeature(Features .RouteBlinding ) => Left (InvalidOnionPayload (UInt64 (10 ), 0 ))
142148 case Some (encrypted) =>
149+ // We are the final recipient of a blinded payment.
143150 decryptEncryptedRecipientData(add, privateKey, payload, encrypted.data).flatMap {
144151 case DecodedEncryptedRecipientData (blindedPayload, _) => validateBlindedFinalPayload(add, payload, blindedPayload)
145152 }
@@ -151,15 +158,33 @@ object IncomingPaymentPacket {
151158 // NB: when we enable blinded trampoline routes, we will need to check if the outer onion contains a
152159 // blinding point and use it to derive the decryption key for the blinded trampoline onion.
153160 decryptOnion(add.paymentHash, privateKey, trampolinePacket).flatMap {
154- case DecodedOnionPacket (innerPayload, Some (next)) => validateNodeRelay(add, payload, innerPayload, next)
161+ case DecodedOnionPacket (innerPayload, Some (next)) =>
162+ // We are an intermediate trampoline node.
163+ if (innerPayload.get[InvoiceRoutingInfo ].isDefined) {
164+ // The payment recipient doesn't support trampoline.
165+ // They can be reached with the invoice data provided.
166+ // The payer is a wallet using the legacy trampoline feature.
167+ validateTrampolineToNonTrampoline(add, payload, innerPayload)
168+ } else {
169+ validateNodeRelay(add, payload, innerPayload, next)
170+ }
155171 case DecodedOnionPacket (innerPayload, None ) =>
156172 if (innerPayload.get[OutgoingBlindedPaths ].isDefined) {
173+ // The payment recipient doesn't support trampoline.
174+ // They can be reached using the blinded paths provided.
157175 validateTrampolineToBlindedPaths(add, payload, innerPayload)
176+ } else if (innerPayload.get[InvoiceRoutingInfo ].isDefined) {
177+ // The payment recipient doesn't support trampoline.
178+ // They can be reached with the invoice data provided.
179+ validateTrampolineToNonTrampoline(add, payload, innerPayload)
158180 } else {
181+ // We're the final recipient of this trampoline payment.
159182 validateTrampolineFinalPayload(add, payload, innerPayload)
160183 }
161184 }
162- case None => validateFinalPayload(add, payload)
185+ case None =>
186+ // We are the final recipient of a standard (non-blinded, non-trampoline) payment.
187+ validateFinalPayload(add, payload)
163188 }
164189 }
165190 }
@@ -224,6 +249,16 @@ object IncomingPaymentPacket {
224249 }
225250 }
226251
252+ private def validateTrampolineToNonTrampoline (add : UpdateAddHtlc , outerPayload : TlvStream [OnionPaymentPayloadTlv ], innerPayload : TlvStream [OnionPaymentPayloadTlv ]): Either [FailureMessage , RelayToNonTrampolinePacket ] = {
253+ FinalPayload .Standard .validate(outerPayload).left.map(_.failureMessage).flatMap { outerPayload =>
254+ IntermediatePayload .NodeRelay .ToNonTrampoline .validate(innerPayload).left.map(_.failureMessage).flatMap {
255+ case _ if add.amountMsat < outerPayload.amount => Left (FinalIncorrectHtlcAmount (add.amountMsat))
256+ case _ if add.cltvExpiry != outerPayload.expiry => Left (FinalIncorrectCltvExpiry (add.cltvExpiry))
257+ case innerPayload => Right (RelayToNonTrampolinePacket (add, outerPayload, innerPayload))
258+ }
259+ }
260+ }
261+
227262 private def validateTrampolineToBlindedPaths (add : UpdateAddHtlc , outerPayload : TlvStream [OnionPaymentPayloadTlv ], innerPayload : TlvStream [OnionPaymentPayloadTlv ]): Either [FailureMessage , RelayToBlindedPathsPacket ] = {
228263 FinalPayload .Standard .validate(outerPayload).left.map(_.failureMessage).flatMap { outerPayload =>
229264 IntermediatePayload .NodeRelay .ToBlindedPaths .validate(innerPayload).left.map(_.failureMessage).flatMap {
@@ -255,10 +290,9 @@ object OutgoingPaymentPacket {
255290 case class PaymentPayloads (amount : MilliSatoshi , expiry : CltvExpiry , payloads : Seq [NodePayload ], outerBlinding_opt : Option [PublicKey ])
256291
257292 sealed trait OutgoingPaymentError extends Throwable
258- case class CannotCreateOnion (message : String ) extends OutgoingPaymentError { override def getMessage : String = message }
293+ private case class CannotCreateOnion (message : String ) extends OutgoingPaymentError { override def getMessage : String = message }
259294 case class InvalidRouteRecipient (expected : PublicKey , actual : PublicKey ) extends OutgoingPaymentError { override def getMessage : String = s " expected route to $expected, got route to $actual" }
260295 case class IndirectRelayInBlindedRoute (nextNodeId : PublicKey ) extends OutgoingPaymentError { override def getMessage : String = s " must relay directly to node $nextNodeId inside blinded route " }
261- case class MissingTrampolineHop (trampolineNodeId : PublicKey ) extends OutgoingPaymentError { override def getMessage : String = s " expected route to trampoline node $trampolineNodeId" }
262296 case class MissingBlindedHop (introductionNodeIds : Set [PublicKey ]) extends OutgoingPaymentError { override def getMessage : String = s " expected blinded route using one of the following introduction nodes: ${introductionNodeIds.mkString(" , " )}" }
263297 case object EmptyRoute extends OutgoingPaymentError { override def getMessage : String = " route cannot be empty" }
264298 // @formatter:on
0 commit comments