@@ -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
@@ -161,9 +162,14 @@ object IncomingPaymentPacket {
161162 val trampolinePacket_opt = payload.get[OnionPaymentPayloadTlv .TrampolineOnion ].map(_.packet).orElse(payload.get[OnionPaymentPayloadTlv .LegacyTrampolineOnion ].map(_.packet))
162163 trampolinePacket_opt match {
163164 case Some (trampolinePacket) =>
164- // NB: when we enable blinded trampoline routes, we will need to check if the outer onion contains a
165- // path key and use it to derive the decryption key for the blinded trampoline onion.
166- decryptOnion(add.paymentHash, privateKey, trampolinePacket).flatMap {
165+ // If we are an intermediate trampoline node inside a blinded path, the payer doesn't know our node_id
166+ // and has encrypted the trampoline onion to our blinded node_id: in that case, the previous trampoline
167+ // node will provide the path key in the outer onion.
168+ val trampolineOnionDecryptionKey = payload.get[OnionPaymentPayloadTlv .PathKey ].map(_.publicKey) match {
169+ case Some (pathKey) => Sphinx .RouteBlinding .derivePrivateKey(privateKey, pathKey)
170+ case None => privateKey
171+ }
172+ decryptOnion(add.paymentHash, trampolineOnionDecryptionKey, trampolinePacket).flatMap {
167173 case DecodedOnionPacket (innerPayload, Some (next)) =>
168174 // We are an intermediate trampoline node.
169175 if (innerPayload.get[InvoiceRoutingInfo ].isDefined) {
@@ -172,7 +178,8 @@ object IncomingPaymentPacket {
172178 // The payer is a wallet using the legacy trampoline feature.
173179 validateTrampolineToNonTrampoline(add, payload, innerPayload)
174180 } else {
175- validateNodeRelay(add, payload, innerPayload, next)
181+ // The recipient supports trampoline (and may support blinded payments).
182+ validateNodeRelay(add, privateKey, payload, innerPayload, next)
176183 }
177184 case DecodedOnionPacket (innerPayload, None ) =>
178185 if (innerPayload.get[OutgoingBlindedPaths ].isDefined) {
@@ -184,8 +191,8 @@ object IncomingPaymentPacket {
184191 // They can be reached with the invoice data provided.
185192 validateTrampolineToNonTrampoline(add, payload, innerPayload)
186193 } else {
187- // We're the final recipient of this trampoline payment.
188- validateTrampolineFinalPayload(add, payload, innerPayload)
194+ // We're the final recipient of this trampoline payment (which may be blinded) .
195+ validateTrampolineFinalPayload(add, privateKey, payload, innerPayload)
189196 }
190197 }
191198 case None =>
@@ -228,31 +235,50 @@ object IncomingPaymentPacket {
228235 }
229236 }
230237
231- private def validateTrampolineFinalPayload (add : UpdateAddHtlc , outerPayload : TlvStream [OnionPaymentPayloadTlv ], innerPayload : TlvStream [OnionPaymentPayloadTlv ]): Either [FailureMessage , FinalPacket ] = {
232- // The outer payload cannot use route blinding, but the inner payload may (but it's not supported yet).
233- FinalPayload .Standard .validate(outerPayload).left.map(_.failureMessage).flatMap { outerPayload =>
234- FinalPayload .Standard .validate(innerPayload).left.map(_.failureMessage).flatMap {
235- case _ if add.amountMsat < outerPayload.amount => Left (FinalIncorrectHtlcAmount (add.amountMsat))
236- case _ if add.cltvExpiry < outerPayload.expiry => Left (FinalIncorrectCltvExpiry (add.cltvExpiry))
237- case innerPayload if outerPayload.expiry < innerPayload.expiry => Left (FinalIncorrectCltvExpiry (add.cltvExpiry)) // previous trampoline didn't forward the right expiry
238- case innerPayload if outerPayload.totalAmount < innerPayload.amount => Left (FinalIncorrectHtlcAmount (outerPayload.totalAmount)) // previous trampoline didn't forward the right amount
239- case innerPayload =>
240- // We merge contents from the outer and inner payloads.
241- // We must use the inner payload's total amount and payment secret because the payment may be split between multiple trampoline payments (#reckless).
242- val trampolinePacket = outerPayload.records.get[OnionPaymentPayloadTlv .TrampolineOnion ].map(_.packet)
243- Right (FinalPacket (add, FinalPayload .Standard .createPayload(outerPayload.amount, innerPayload.totalAmount, innerPayload.expiry, innerPayload.paymentSecret, innerPayload.paymentMetadata, trampolinePacket)))
244- }
238+ private def validateTrampolineFinalPayload (add : UpdateAddHtlc , privateKey : PrivateKey , outerPayload : TlvStream [OnionPaymentPayloadTlv ], innerPayload : TlvStream [OnionPaymentPayloadTlv ]): Either [FailureMessage , FinalPacket ] = {
239+ // The outer payload cannot use route blinding, but the inner payload may.
240+ FinalPayload .Standard .validate(outerPayload).left.map(_.failureMessage).flatMap {
241+ case outerPayload if add.amountMsat < outerPayload.amount => Left (FinalIncorrectHtlcAmount (add.amountMsat))
242+ case outerPayload if add.cltvExpiry < outerPayload.expiry => Left (FinalIncorrectCltvExpiry (add.cltvExpiry))
243+ case outerPayload =>
244+ innerPayload.get[OnionPaymentPayloadTlv .EncryptedRecipientData ] match {
245+ case Some (encrypted) =>
246+ decryptEncryptedRecipientData(add, privateKey, outerPayload.records, encrypted.data).flatMap {
247+ case DecodedEncryptedRecipientData (blindedPayload, _) => validateBlindedFinalPayload(add, innerPayload, blindedPayload)
248+ }
249+ case None =>
250+ FinalPayload .Standard .validate(innerPayload).left.map(_.failureMessage).flatMap {
251+ case innerPayload if outerPayload.expiry < innerPayload.expiry => Left (FinalIncorrectCltvExpiry (add.cltvExpiry)) // previous trampoline didn't forward the right expiry
252+ case innerPayload if outerPayload.totalAmount < innerPayload.amount => Left (FinalIncorrectHtlcAmount (outerPayload.totalAmount)) // previous trampoline didn't forward the right amount
253+ case innerPayload =>
254+ // We merge contents from the outer and inner payloads.
255+ // We must use the inner payload's total amount and payment secret because the payment may be split between multiple trampoline payments (#reckless).
256+ val trampolinePacket = outerPayload.records.get[OnionPaymentPayloadTlv .TrampolineOnion ].map(_.packet)
257+ Right (FinalPacket (add, FinalPayload .Standard .createPayload(outerPayload.amount, innerPayload.totalAmount, innerPayload.expiry, innerPayload.paymentSecret, innerPayload.paymentMetadata, trampolinePacket)))
258+ }
259+ }
245260 }
246261 }
247262
248- private def validateNodeRelay (add : UpdateAddHtlc , outerPayload : TlvStream [OnionPaymentPayloadTlv ], innerPayload : TlvStream [OnionPaymentPayloadTlv ], next : OnionRoutingPacket ): Either [FailureMessage , RelayToTrampolinePacket ] = {
249- // The outer payload cannot use route blinding, but the inner payload may (but it's not supported yet).
250- FinalPayload .Standard .validate(outerPayload).left.map(_.failureMessage).flatMap { outerPayload =>
251- IntermediatePayload .NodeRelay .Standard .validate(innerPayload).left.map(_.failureMessage).flatMap {
252- case _ if add.amountMsat < outerPayload.amount => Left (FinalIncorrectHtlcAmount (add.amountMsat))
253- case _ if add.cltvExpiry != outerPayload.expiry => Left (FinalIncorrectCltvExpiry (add.cltvExpiry))
254- case innerPayload => Right (RelayToTrampolinePacket (add, outerPayload, innerPayload, next))
255- }
263+ private def validateNodeRelay (add : UpdateAddHtlc , privateKey : PrivateKey , outerPayload : TlvStream [OnionPaymentPayloadTlv ], innerPayload : TlvStream [OnionPaymentPayloadTlv ], next : OnionRoutingPacket ): Either [FailureMessage , IncomingPaymentPacket ] = {
264+ // The outer payload cannot use route blinding, but the inner payload may.
265+ FinalPayload .Standard .validate(outerPayload).left.map(_.failureMessage).flatMap {
266+ case outerPayload if add.amountMsat < outerPayload.amount => Left (FinalIncorrectHtlcAmount (add.amountMsat))
267+ case outerPayload if add.cltvExpiry != outerPayload.expiry => Left (FinalIncorrectCltvExpiry (add.cltvExpiry))
268+ case outerPayload =>
269+ innerPayload.get[OnionPaymentPayloadTlv .EncryptedRecipientData ] match {
270+ case Some (encrypted) =>
271+ // The path key can be found:
272+ // - in the inner payload if we are the introduction node of the blinded path (provided by the payer).
273+ // - in the outer payload if we are an intermediate node in the blinded path (provided by the previous trampoline node).
274+ val pathKey_opt = innerPayload.get[OnionPaymentPayloadTlv .PathKey ].orElse(outerPayload.records.get[OnionPaymentPayloadTlv .PathKey ]).map(_.publicKey)
275+ decryptEncryptedRecipientData(add, privateKey, pathKey_opt, encrypted.data).flatMap {
276+ case DecodedEncryptedRecipientData (blindedPayload, nextPathKey) =>
277+ IntermediatePayload .NodeRelay .Blinded .validate(innerPayload, blindedPayload, nextPathKey).left.map(_.failureMessage).map(innerPayload => RelayToBlindedTrampolinePacket (add, outerPayload, innerPayload, next))
278+ }
279+ case None =>
280+ IntermediatePayload .NodeRelay .Standard .validate(innerPayload).left.map(_.failureMessage).map(innerPayload => RelayToTrampolinePacket (add, outerPayload, innerPayload, next))
281+ }
256282 }
257283 }
258284
0 commit comments