@@ -484,6 +484,91 @@ class PaymentPacketTestsCommon : LightningTestSuite() {
484
484
assertEquals(paymentMetadata.paymentHash, invoice.paymentHash)
485
485
}
486
486
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
+
487
572
// See bolt04/trampoline-to-blinded-path-payment-onion-test.json
488
573
@Test
489
574
fun `send a trampoline payment to blinded paths -- reference test vector` () {
0 commit comments