@@ -498,6 +498,91 @@ class PaymentPacketTestsCommon : LightningTestSuite() {
498
498
assertEquals(paymentMetadata.paymentHash, invoice.paymentHash)
499
499
}
500
500
501
+ @Test
502
+ fun `send a trampoline payment to blinded recipient` () {
503
+ val features = Features (
504
+ Feature .BasicMultiPartPayment to FeatureSupport .Optional ,
505
+ Feature .TrampolinePayment to FeatureSupport .Optional ,
506
+ )
507
+ val offer = OfferTypes .Offer .createNonBlindedOffer(finalAmount, " test offer" , d, features, Block .RegtestGenesisBlock .hash)
508
+ // D uses a 1-hop blinded path from its trampoline node C.
509
+ val (invoice, blindedRoute) = run {
510
+ val payerKey = randomKey()
511
+ val request = OfferTypes .InvoiceRequest (offer, finalAmount, 1 , features, payerKey, " hello" , Block .RegtestGenesisBlock .hash)
512
+ val paymentMetadata = OfferPaymentMetadata .V1 (offer.offerId, finalAmount, paymentPreimage, payerKey.publicKey(), " hello" , 1 , currentTimestampMillis())
513
+ val blindedPayloadC = RouteBlindingEncryptedData (
514
+ TlvStream (
515
+ RouteBlindingEncryptedDataTlv .OutgoingNodeId (EncodedNodeId (d)),
516
+ RouteBlindingEncryptedDataTlv .PaymentRelay (channelUpdateCD.cltvExpiryDelta, channelUpdateCD.feeProportionalMillionths, channelUpdateCD.feeBaseMsat),
517
+ RouteBlindingEncryptedDataTlv .PaymentConstraints (finalExpiry, 1 .msat),
518
+ )
519
+ )
520
+ val blindedPayloadD = RouteBlindingEncryptedData (
521
+ TlvStream (
522
+ RouteBlindingEncryptedDataTlv .PathId (paymentMetadata.toPathId(privD))
523
+ )
524
+ )
525
+ val blindedRouteDetails = RouteBlinding .create(randomKey(), listOf (c, d), listOf (blindedPayloadC, blindedPayloadD).map { it.write().byteVector() })
526
+ val paymentInfo = createBlindedPaymentInfo(channelUpdateCD)
527
+ val path = Bolt12Invoice .Companion .PaymentBlindedContactInfo (OfferTypes .ContactInfo .BlindedPath (blindedRouteDetails.route), paymentInfo)
528
+ val invoice = Bolt12Invoice (request, paymentPreimage, blindedRouteDetails.blindedPrivateKey(privD), 600 , features, listOf (path))
529
+ assertEquals(invoice.nodeId, blindedRouteDetails.route.blindedNodeIds.last())
530
+ assertNotEquals(invoice.nodeId, d)
531
+ assertTrue(invoice.features.hasFeature(Feature .TrampolinePayment ))
532
+ Pair (invoice, blindedRouteDetails.route)
533
+ }
534
+
535
+ // B pays that invoice using its trampoline node C to relay to D using trampoline.
536
+ val (firstAmount, firstExpiry, onion) = OutgoingPaymentPacket .buildPacketToTrampolineRecipient(invoice.paymentHash, finalAmount, finalExpiry, invoice.blindedPaths.first(), nodeHop_cd)
537
+ assertEquals(amountBC, firstAmount)
538
+ assertEquals(expiryBC, firstExpiry)
539
+
540
+ // C decrypts the onion, the trampoline onion and the encrypted data before relaying to D.
541
+ val addC = UpdateAddHtlc (randomBytes32(), 1 , firstAmount, paymentHash, firstExpiry, onion.packet)
542
+ val (outerC, innerC, trampolineOnionD) = decryptRelayToBlindedTrampoline(addC, privC)
543
+ assertEquals(amountBC, outerC.amount)
544
+ assertEquals(amountBC, outerC.totalAmount)
545
+ assertEquals(expiryBC, outerC.expiry)
546
+ assertEquals(2 , innerC.records.records.size)
547
+ val encryptedData = innerC.records.get<OnionPaymentPayloadTlv .EncryptedRecipientData >()?.data
548
+ assertNotNull(encryptedData)
549
+ val pathKey = innerC.records.get<OnionPaymentPayloadTlv .PathKey >()?.publicKey
550
+ assertNotNull(pathKey)
551
+ assertEquals(blindedRoute.firstPathKey, pathKey)
552
+ val (encryptedPayload, nextPathKey) = RouteBlinding .decryptPayload(privC, pathKey, encryptedData).right!!
553
+ val decryptedPayload = RouteBlindingEncryptedData .read(encryptedPayload.toByteArray()).right!!
554
+ assertEquals(EncodedNodeId (d), decryptedPayload.nextNodeId)
555
+ val paymentRelay = decryptedPayload.records.get<RouteBlindingEncryptedDataTlv .PaymentRelay >()
556
+ assertEquals(channelUpdateCD.cltvExpiryDelta, paymentRelay?.cltvExpiryDelta)
557
+ assertEquals(channelUpdateCD.feeBaseMsat, paymentRelay?.feeBase)
558
+ assertEquals(channelUpdateCD.feeProportionalMillionths, paymentRelay?.feeProportionalMillionths)
559
+
560
+ // C relays the trampoline payment to D.
561
+ val onionD = run {
562
+ val payloadD = PaymentOnion .FinalPayload .Standard (
563
+ TlvStream (
564
+ OnionPaymentPayloadTlv .AmountToForward (finalAmount),
565
+ OnionPaymentPayloadTlv .OutgoingCltv (finalExpiry),
566
+ OnionPaymentPayloadTlv .PaymentData (randomBytes32(), finalAmount),
567
+ OnionPaymentPayloadTlv .PathKey (nextPathKey),
568
+ OnionPaymentPayloadTlv .TrampolineOnion (trampolineOnionD)
569
+ )
570
+ )
571
+ OutgoingPaymentPacket .buildOnion(listOf (d), listOf (payloadD), paymentHash, OnionRoutingPacket .PaymentPacketLength ).packet
572
+ }
573
+
574
+ // D receives the payment.
575
+ val addD = UpdateAddHtlc (randomBytes32(), 3 , finalAmount, paymentHash, finalExpiry, onionD)
576
+ val payloadD = IncomingPaymentPacket .decrypt(addD, privD).right!!
577
+ assertIs<PaymentOnion .FinalPayload .Blinded >(payloadD)
578
+ assertEquals(finalAmount, payloadD.amount)
579
+ assertEquals(finalExpiry, payloadD.expiry)
580
+ val paymentMetadata = OfferPaymentMetadata .fromPathId(d, payloadD.pathId)
581
+ assertNotNull(paymentMetadata)
582
+ assertEquals(offer.offerId, paymentMetadata.offerId)
583
+ assertEquals(paymentMetadata.paymentHash, invoice.paymentHash)
584
+ }
585
+
501
586
// See bolt04/trampoline-to-blinded-path-payment-onion-test.json
502
587
@Test
503
588
fun `send a trampoline payment to blinded paths -- reference test vector` () {
@@ -1316,6 +1401,89 @@ class PaymentPacketTestsCommon : LightningTestSuite() {
1316
1401
}
1317
1402
}
1318
1403
1404
+ @Test
1405
+ fun `build htlc failure onion -- trampoline payment to blinded trampoline recipient` () {
1406
+ // D uses a 1-hop blinded path from its trampoline node C.
1407
+ val (invoice, blindedRoute) = run {
1408
+ val offer = OfferTypes .Offer .createNonBlindedOffer(finalAmount, " test offer" , d, Features (Feature .TrampolinePayment to FeatureSupport .Optional ), Block .RegtestGenesisBlock .hash)
1409
+ val payerKey = randomKey()
1410
+ val request = OfferTypes .InvoiceRequest (offer, finalAmount, 1 , offer.features, payerKey, " hello" , Block .RegtestGenesisBlock .hash)
1411
+ val paymentMetadata = OfferPaymentMetadata .V1 (offer.offerId, finalAmount, paymentPreimage, payerKey.publicKey(), " hello" , 1 , currentTimestampMillis())
1412
+ val blindedPayloadC = RouteBlindingEncryptedData (
1413
+ TlvStream (
1414
+ RouteBlindingEncryptedDataTlv .OutgoingNodeId (EncodedNodeId (d)),
1415
+ RouteBlindingEncryptedDataTlv .PaymentRelay (channelUpdateCD.cltvExpiryDelta, channelUpdateCD.feeProportionalMillionths, channelUpdateCD.feeBaseMsat),
1416
+ RouteBlindingEncryptedDataTlv .PaymentConstraints (finalExpiry, 1 .msat),
1417
+ )
1418
+ )
1419
+ val blindedPayloadD = RouteBlindingEncryptedData (TlvStream (RouteBlindingEncryptedDataTlv .PathId (paymentMetadata.toPathId(privD))))
1420
+ val blindedRouteDetails = RouteBlinding .create(randomKey(), listOf (c, d), listOf (blindedPayloadC, blindedPayloadD).map { it.write().byteVector() })
1421
+ val paymentInfo = createBlindedPaymentInfo(channelUpdateCD)
1422
+ val path = Bolt12Invoice .Companion .PaymentBlindedContactInfo (OfferTypes .ContactInfo .BlindedPath (blindedRouteDetails.route), paymentInfo)
1423
+ val invoice = Bolt12Invoice (request, paymentPreimage, blindedRouteDetails.blindedPrivateKey(privD), 600 , offer.features, listOf (path))
1424
+ assertTrue(invoice.features.hasFeature(Feature .TrampolinePayment ))
1425
+ Pair (invoice, blindedRouteDetails.route)
1426
+ }
1427
+
1428
+ // B pays that invoice using its trampoline node C to relay to D using trampoline.
1429
+ val (amountBC, expiryBC, onionC) = OutgoingPaymentPacket .buildPacketToTrampolineRecipient(invoice.paymentHash, finalAmount, finalExpiry, invoice.blindedPaths.first(), nodeHop_cd)
1430
+ // C decrypts the onion, the trampoline onion and the encrypted data and relays to D.
1431
+ val addC = UpdateAddHtlc (randomBytes32(), 1 , amountBC, invoice.paymentHash, expiryBC, onionC.packet)
1432
+ val (_, innerC, trampolineOnionD) = decryptRelayToBlindedTrampoline(addC, privC)
1433
+ val (addD, willAddD, onionD) = run {
1434
+ val encryptedData = innerC.records.get<OnionPaymentPayloadTlv .EncryptedRecipientData >()?.data!!
1435
+ val pathKey = innerC.records.get<OnionPaymentPayloadTlv .PathKey >()?.publicKey!!
1436
+ val (_, nextPathKey) = RouteBlinding .decryptPayload(privC, pathKey, encryptedData).right!!
1437
+ val payloadD = PaymentOnion .FinalPayload .Standard (
1438
+ TlvStream (
1439
+ OnionPaymentPayloadTlv .AmountToForward (finalAmount),
1440
+ OnionPaymentPayloadTlv .OutgoingCltv (finalExpiry),
1441
+ OnionPaymentPayloadTlv .PaymentData (randomBytes32(), finalAmount),
1442
+ OnionPaymentPayloadTlv .PathKey (nextPathKey),
1443
+ OnionPaymentPayloadTlv .TrampolineOnion (trampolineOnionD)
1444
+ )
1445
+ )
1446
+ val onionD = OutgoingPaymentPacket .buildOnion(listOf (d), listOf (payloadD), paymentHash, OnionRoutingPacket .PaymentPacketLength )
1447
+ val addD = UpdateAddHtlc (randomBytes32(), 2 , finalAmount, addC.paymentHash, finalExpiry, onionD.packet)
1448
+ val willAddD = WillAddHtlc (Block .RegtestGenesisBlock .hash, randomBytes32(), finalAmount, paymentHash, finalExpiry, onionD.packet)
1449
+ Triple (addD, willAddD, onionD)
1450
+ }
1451
+ // D can correctly decrypt the blinded payment.
1452
+ run {
1453
+ val payloadD = IncomingPaymentPacket .decrypt(addD, privD).right!!
1454
+ assertIs<PaymentOnion .FinalPayload .Blinded >(payloadD)
1455
+ assertNotNull(OfferPaymentMetadata .fromPathId(d, payloadD.pathId))
1456
+ }
1457
+ run {
1458
+ val payloadD = IncomingPaymentPacket .decrypt(willAddD, privD).right!!
1459
+ assertIs<PaymentOnion .FinalPayload .Blinded >(payloadD)
1460
+ assertNotNull(OfferPaymentMetadata .fromPathId(d, payloadD.pathId))
1461
+ }
1462
+
1463
+ // D returns a failure: note that it is not a blinded failure, since there is no need to protect the blinded path against probing.
1464
+ val failure = IncorrectOrUnknownPaymentDetails (finalAmount, currentBlockCount)
1465
+ val encryptedFailuresD = run {
1466
+ val encryptedFailureD = OutgoingPaymentPacket .buildHtlcFailure(privD, paymentHash, addD.onionRoutingPacket, addD.pathKey, ChannelCommand .Htlc .Settlement .Fail .Reason .Failure (failure)).right
1467
+ assertNotNull(encryptedFailureD)
1468
+ val willFailD = OutgoingPaymentPacket .buildWillAddHtlcFailure(privD, willAddD, failure)
1469
+ assertIs<WillFailHtlc >(willFailD)
1470
+ listOf (encryptedFailureD, willFailD.reason)
1471
+ }
1472
+ encryptedFailuresD.forEach { encryptedFailureD ->
1473
+ // C cannot decrypt the failure.
1474
+ assertTrue(FailurePacket .decrypt(encryptedFailureD.toByteArray(), onionD.sharedSecrets).isLeft)
1475
+ // C peels the error coming from D and re-wraps it for B.
1476
+ val peeled = FailurePacket .wrap(encryptedFailureD.toByteArray(), onionD.sharedSecrets.first().secret).toByteVector()
1477
+ val encryptedFailureC = OutgoingPaymentPacket .buildHtlcFailure(privC, paymentHash, addC.onionRoutingPacket, addC.pathKey, ChannelCommand .Htlc .Settlement .Fail .Reason .Bytes (peeled)).right
1478
+ assertNotNull(encryptedFailureC)
1479
+ // B decrypts the failure.
1480
+ val decrypted = FailurePacket .decrypt(encryptedFailureC.toByteArray(), onionC.outerSharedSecrets + onionC.innerSharedSecrets)
1481
+ assertTrue(decrypted.isRight)
1482
+ assertEquals(blindedRoute.blindedNodeIds.last(), decrypted.right?.originNode)
1483
+ assertEquals(failure, decrypted.right?.failureMessage)
1484
+ }
1485
+ }
1486
+
1319
1487
@Test
1320
1488
fun `build htlc failure onion -- trampoline payment to blinded non-trampoline recipient` () {
1321
1489
// D uses a 1-hop blinded path from its trampoline node C.
0 commit comments