@@ -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 RelayToBlindedTrampolinePacket (add : UpdateAddHtlc , outerPayload : FinalPayload .Standard , innerPayload : IntermediatePayload .NodeRelay .Blinded , nextPacket : OnionRoutingPacket ) extends NodeRelayPacket
6263 case class RelayToNonTrampolinePacket (add : UpdateAddHtlc , outerPayload : FinalPayload .Standard , innerPayload : IntermediatePayload .NodeRelay .ToNonTrampoline ) extends NodeRelayPacket
6364 case class RelayToBlindedPathsPacket (add : UpdateAddHtlc , outerPayload : FinalPayload .Standard , innerPayload : IntermediatePayload .NodeRelay .ToBlindedPaths ) extends NodeRelayPacket
6465 // @formatter:on
@@ -172,9 +173,14 @@ object IncomingPaymentPacket {
172173 val totalAmount = payload.get[OnionPaymentPayloadTlv .AmountToForward ].map(_.amount).getOrElse(add.amountMsat)
173174 payload.copy(records = payload.records + OnionPaymentPayloadTlv .PaymentData (dummyPaymentSecret, totalAmount))
174175 }
175- // NB: when we enable blinded trampoline routes, we will need to check if the outer onion contains a
176- // path key and use it to derive the decryption key for the blinded trampoline onion.
177- decryptOnion(add.paymentHash, privateKey, trampolinePacket).flatMap {
176+ // If we are an intermediate trampoline node inside a blinded path, the payer doesn't know our node_id
177+ // and has encrypted the trampoline onion to our blinded node_id: in that case, the previous trampoline
178+ // node will provide the path key in the outer onion.
179+ val trampolineOnionDecryptionKey = payload.get[OnionPaymentPayloadTlv .PathKey ].map(_.publicKey) match {
180+ case Some (pathKey) => Sphinx .RouteBlinding .derivePrivateKey(privateKey, pathKey)
181+ case None => privateKey
182+ }
183+ decryptOnion(add.paymentHash, trampolineOnionDecryptionKey, trampolinePacket).flatMap {
178184 case DecodedOnionPacket (innerPayload, Some (next)) =>
179185 // We are an intermediate trampoline node.
180186 if (innerPayload.get[InvoiceRoutingInfo ].isDefined) {
@@ -183,7 +189,8 @@ object IncomingPaymentPacket {
183189 // The payer is a wallet using the legacy trampoline feature.
184190 validateTrampolineToNonTrampoline(add, outerPayload, innerPayload)
185191 } else {
186- validateNodeRelay(add, outerPayload, innerPayload, next)
192+ // The recipient supports trampoline (and may support blinded payments).
193+ validateNodeRelay(add, privateKey, outerPayload, innerPayload, next)
187194 }
188195 case DecodedOnionPacket (innerPayload, None ) =>
189196 if (innerPayload.get[OutgoingBlindedPaths ].isDefined) {
@@ -195,8 +202,8 @@ object IncomingPaymentPacket {
195202 // They can be reached with the invoice data provided.
196203 validateTrampolineToNonTrampoline(add, outerPayload, innerPayload)
197204 } else {
198- // We're the final recipient of this trampoline payment.
199- validateTrampolineFinalPayload(add, outerPayload, innerPayload)
205+ // We're the final recipient of this trampoline payment (which may be blinded) .
206+ validateTrampolineFinalPayload(add, privateKey, outerPayload, innerPayload)
200207 }
201208 }
202209 case None =>
@@ -238,31 +245,50 @@ object IncomingPaymentPacket {
238245 }
239246 }
240247
241- private def validateTrampolineFinalPayload (add : UpdateAddHtlc , outerPayload : TlvStream [OnionPaymentPayloadTlv ], innerPayload : TlvStream [OnionPaymentPayloadTlv ]): Either [FailureMessage , FinalPacket ] = {
242- // The outer payload cannot use route blinding, but the inner payload may (but it's not supported yet).
243- FinalPayload .Standard .validate(outerPayload).left.map(_.failureMessage).flatMap { outerPayload =>
244- FinalPayload .Standard .validate(innerPayload).left.map(_.failureMessage).flatMap {
245- case _ if add.amountMsat < outerPayload.amount => Left (FinalIncorrectHtlcAmount (add.amountMsat))
246- case _ if add.cltvExpiry < outerPayload.expiry => Left (FinalIncorrectCltvExpiry (add.cltvExpiry))
247- case innerPayload if outerPayload.expiry < innerPayload.expiry => Left (FinalIncorrectCltvExpiry (add.cltvExpiry)) // previous trampoline didn't forward the right expiry
248- case innerPayload if outerPayload.totalAmount < innerPayload.amount => Left (FinalIncorrectHtlcAmount (outerPayload.totalAmount)) // previous trampoline didn't forward the right amount
249- case innerPayload =>
250- // We merge contents from the outer and inner payloads.
251- // We must use the inner payload's total amount and payment secret because the payment may be split between multiple trampoline payments (#reckless).
252- val trampolinePacket = outerPayload.records.get[OnionPaymentPayloadTlv .TrampolineOnion ].map(_.packet)
253- Right (FinalPacket (add, FinalPayload .Standard .createPayload(outerPayload.amount, innerPayload.totalAmount, innerPayload.expiry, innerPayload.paymentSecret, innerPayload.paymentMetadata, trampolinePacket)))
254- }
248+ private def validateTrampolineFinalPayload (add : UpdateAddHtlc , privateKey : PrivateKey , outerPayload : TlvStream [OnionPaymentPayloadTlv ], innerPayload : TlvStream [OnionPaymentPayloadTlv ]): Either [FailureMessage , FinalPacket ] = {
249+ // The outer payload cannot use route blinding, but the inner payload may.
250+ FinalPayload .Standard .validate(outerPayload).left.map(_.failureMessage).flatMap {
251+ case outerPayload if add.amountMsat < outerPayload.amount => Left (FinalIncorrectHtlcAmount (add.amountMsat))
252+ case outerPayload if add.cltvExpiry < outerPayload.expiry => Left (FinalIncorrectCltvExpiry (add.cltvExpiry))
253+ case outerPayload =>
254+ innerPayload.get[OnionPaymentPayloadTlv .EncryptedRecipientData ] match {
255+ case Some (encrypted) =>
256+ decryptEncryptedRecipientData(add, privateKey, outerPayload.records, encrypted.data).flatMap {
257+ case DecodedEncryptedRecipientData (blindedPayload, _) => validateBlindedFinalPayload(add, innerPayload, blindedPayload)
258+ }
259+ case None =>
260+ FinalPayload .Standard .validate(innerPayload).left.map(_.failureMessage).flatMap {
261+ case innerPayload if outerPayload.expiry < innerPayload.expiry => Left (FinalIncorrectCltvExpiry (add.cltvExpiry)) // previous trampoline didn't forward the right expiry
262+ case innerPayload if outerPayload.totalAmount < innerPayload.amount => Left (FinalIncorrectHtlcAmount (outerPayload.totalAmount)) // previous trampoline didn't forward the right amount
263+ case innerPayload =>
264+ // We merge contents from the outer and inner payloads.
265+ // We must use the inner payload's total amount and payment secret because the payment may be split between multiple trampoline payments (#reckless).
266+ val trampolinePacket = outerPayload.records.get[OnionPaymentPayloadTlv .TrampolineOnion ].map(_.packet)
267+ Right (FinalPacket (add, FinalPayload .Standard .createPayload(outerPayload.amount, innerPayload.totalAmount, innerPayload.expiry, innerPayload.paymentSecret, innerPayload.paymentMetadata, trampolinePacket)))
268+ }
269+ }
255270 }
256271 }
257272
258- private def validateNodeRelay (add : UpdateAddHtlc , outerPayload : TlvStream [OnionPaymentPayloadTlv ], innerPayload : TlvStream [OnionPaymentPayloadTlv ], next : OnionRoutingPacket ): Either [FailureMessage , RelayToTrampolinePacket ] = {
259- // The outer payload cannot use route blinding, but the inner payload may (but it's not supported yet).
260- FinalPayload .Standard .validate(outerPayload).left.map(_.failureMessage).flatMap { outerPayload =>
261- IntermediatePayload .NodeRelay .Standard .validate(innerPayload).left.map(_.failureMessage).flatMap {
262- case _ if add.amountMsat < outerPayload.amount => Left (FinalIncorrectHtlcAmount (add.amountMsat))
263- case _ if add.cltvExpiry != outerPayload.expiry => Left (FinalIncorrectCltvExpiry (add.cltvExpiry))
264- case innerPayload => Right (RelayToTrampolinePacket (add, outerPayload, innerPayload, next))
265- }
273+ private def validateNodeRelay (add : UpdateAddHtlc , privateKey : PrivateKey , outerPayload : TlvStream [OnionPaymentPayloadTlv ], innerPayload : TlvStream [OnionPaymentPayloadTlv ], next : OnionRoutingPacket ): Either [FailureMessage , IncomingPaymentPacket ] = {
274+ // The outer payload cannot use route blinding, but the inner payload may.
275+ FinalPayload .Standard .validate(outerPayload).left.map(_.failureMessage).flatMap {
276+ case outerPayload if add.amountMsat < outerPayload.amount => Left (FinalIncorrectHtlcAmount (add.amountMsat))
277+ case outerPayload if add.cltvExpiry != outerPayload.expiry => Left (FinalIncorrectCltvExpiry (add.cltvExpiry))
278+ case outerPayload =>
279+ innerPayload.get[OnionPaymentPayloadTlv .EncryptedRecipientData ] match {
280+ case Some (encrypted) =>
281+ // The path key can be found:
282+ // - in the inner payload if we are the introduction node of the blinded path (provided by the payer).
283+ // - in the outer payload if we are an intermediate node in the blinded path (provided by the previous trampoline node).
284+ val pathKey_opt = innerPayload.get[OnionPaymentPayloadTlv .PathKey ].orElse(outerPayload.records.get[OnionPaymentPayloadTlv .PathKey ]).map(_.publicKey)
285+ decryptEncryptedRecipientData(add, privateKey, pathKey_opt, encrypted.data).flatMap {
286+ case DecodedEncryptedRecipientData (blindedPayload, nextPathKey) =>
287+ IntermediatePayload .NodeRelay .Blinded .validate(innerPayload, blindedPayload, nextPathKey).left.map(_.failureMessage).map(innerPayload => RelayToBlindedTrampolinePacket (add, outerPayload, innerPayload, next))
288+ }
289+ case None =>
290+ IntermediatePayload .NodeRelay .Standard .validate(innerPayload).left.map(_.failureMessage).map(innerPayload => RelayToTrampolinePacket (add, outerPayload, innerPayload, next))
291+ }
266292 }
267293 }
268294
0 commit comments