@@ -484,6 +484,91 @@ class PaymentPacketTestsCommon : LightningTestSuite() {
484484 assertEquals(paymentMetadata.paymentHash, invoice.paymentHash)
485485 }
486486
487+ @Test
488+ fun `send a trampoline payment to blinded recipient` () {
489+ val features = Features (
490+ Feature .BasicMultiPartPayment to FeatureSupport .Optional ,
491+ Feature .TrampolinePayment to FeatureSupport .Optional ,
492+ )
493+ val offer = OfferTypes .Offer .createNonBlindedOffer(finalAmount, " test offer" , d, features, Block .RegtestGenesisBlock .hash)
494+ // D uses a 1-hop blinded path from its trampoline node C.
495+ val (invoice, blindedRoute) = run {
496+ val payerKey = randomKey()
497+ val request = OfferTypes .InvoiceRequest (offer, finalAmount, 1 , features, payerKey, " hello" , Block .RegtestGenesisBlock .hash)
498+ val paymentMetadata = OfferPaymentMetadata .V1 (offer.offerId, finalAmount, paymentPreimage, payerKey.publicKey(), " hello" , 1 , currentTimestampMillis())
499+ val blindedPayloadC = RouteBlindingEncryptedData (
500+ TlvStream (
501+ RouteBlindingEncryptedDataTlv .OutgoingNodeId (EncodedNodeId (d)),
502+ RouteBlindingEncryptedDataTlv .PaymentRelay (channelUpdateCD.cltvExpiryDelta, channelUpdateCD.feeProportionalMillionths, channelUpdateCD.feeBaseMsat),
503+ RouteBlindingEncryptedDataTlv .PaymentConstraints (finalExpiry, 1 .msat),
504+ )
505+ )
506+ val blindedPayloadD = RouteBlindingEncryptedData (
507+ TlvStream (
508+ RouteBlindingEncryptedDataTlv .PathId (paymentMetadata.toPathId(privD))
509+ )
510+ )
511+ val blindedRouteDetails = RouteBlinding .create(randomKey(), listOf (c, d), listOf (blindedPayloadC, blindedPayloadD).map { it.write().byteVector() })
512+ val paymentInfo = createBlindedPaymentInfo(channelUpdateCD)
513+ val path = Bolt12Invoice .Companion .PaymentBlindedContactInfo (OfferTypes .ContactInfo .BlindedPath (blindedRouteDetails.route), paymentInfo)
514+ val invoice = Bolt12Invoice (request, paymentPreimage, blindedRouteDetails.blindedPrivateKey(privD), 600 , features, listOf (path))
515+ assertEquals(invoice.nodeId, blindedRouteDetails.route.blindedNodeIds.last())
516+ assertNotEquals(invoice.nodeId, d)
517+ assertTrue(invoice.features.hasFeature(Feature .TrampolinePayment ))
518+ Pair (invoice, blindedRouteDetails.route)
519+ }
520+
521+ // B pays that invoice using its trampoline node C to relay to D using trampoline.
522+ val (firstAmount, firstExpiry, onion) = OutgoingPaymentPacket .buildPacketToTrampolineRecipient(invoice.paymentHash, finalAmount, finalExpiry, invoice.blindedPaths.first(), nodeHop_cd)
523+ assertEquals(amountBC, firstAmount)
524+ assertEquals(expiryBC, firstExpiry)
525+
526+ // C decrypts the onion, the trampoline onion and the encrypted data before relaying to D.
527+ val addC = UpdateAddHtlc (randomBytes32(), 1 , firstAmount, paymentHash, firstExpiry, onion.packet)
528+ val (outerC, innerC, trampolineOnionD) = decryptRelayToBlindedTrampoline(addC, privC)
529+ assertEquals(amountBC, outerC.amount)
530+ assertEquals(amountBC, outerC.totalAmount)
531+ assertEquals(expiryBC, outerC.expiry)
532+ assertEquals(2 , innerC.records.records.size)
533+ val encryptedData = innerC.records.get<OnionPaymentPayloadTlv .EncryptedRecipientData >()?.data
534+ assertNotNull(encryptedData)
535+ val pathKey = innerC.records.get<OnionPaymentPayloadTlv .PathKey >()?.publicKey
536+ assertNotNull(pathKey)
537+ assertEquals(blindedRoute.firstPathKey, pathKey)
538+ val (encryptedPayload, nextPathKey) = RouteBlinding .decryptPayload(privC, pathKey, encryptedData).right!!
539+ val decryptedPayload = RouteBlindingEncryptedData .read(encryptedPayload.toByteArray()).right!!
540+ assertEquals(EncodedNodeId (d), decryptedPayload.nextNodeId)
541+ val paymentRelay = decryptedPayload.records.get<RouteBlindingEncryptedDataTlv .PaymentRelay >()
542+ assertEquals(channelUpdateCD.cltvExpiryDelta, paymentRelay?.cltvExpiryDelta)
543+ assertEquals(channelUpdateCD.feeBaseMsat, paymentRelay?.feeBase)
544+ assertEquals(channelUpdateCD.feeProportionalMillionths, paymentRelay?.feeProportionalMillionths)
545+
546+ // C relays the trampoline payment to D.
547+ val onionD = run {
548+ val payloadD = PaymentOnion .FinalPayload .Standard (
549+ TlvStream (
550+ OnionPaymentPayloadTlv .AmountToForward (finalAmount),
551+ OnionPaymentPayloadTlv .OutgoingCltv (finalExpiry),
552+ OnionPaymentPayloadTlv .PaymentData (randomBytes32(), finalAmount),
553+ OnionPaymentPayloadTlv .PathKey (nextPathKey),
554+ OnionPaymentPayloadTlv .TrampolineOnion (trampolineOnionD)
555+ )
556+ )
557+ OutgoingPaymentPacket .buildOnion(listOf (d), listOf (payloadD), paymentHash, OnionRoutingPacket .PaymentPacketLength ).packet
558+ }
559+
560+ // D receives the payment.
561+ val addD = UpdateAddHtlc (randomBytes32(), 3 , finalAmount, paymentHash, finalExpiry, onionD)
562+ val payloadD = IncomingPaymentPacket .decrypt(addD, privD).right!!
563+ assertIs<PaymentOnion .FinalPayload .Blinded >(payloadD)
564+ assertEquals(finalAmount, payloadD.amount)
565+ assertEquals(finalExpiry, payloadD.expiry)
566+ val paymentMetadata = OfferPaymentMetadata .fromPathId(d, payloadD.pathId)
567+ assertNotNull(paymentMetadata)
568+ assertEquals(offer.offerId, paymentMetadata.offerId)
569+ assertEquals(paymentMetadata.paymentHash, invoice.paymentHash)
570+ }
571+
487572 // See bolt04/trampoline-to-blinded-path-payment-onion-test.json
488573 @Test
489574 fun `send a trampoline payment to blinded paths -- reference test vector` () {
0 commit comments