From a375c43ae19f949471e2244b6e40dcf1f9d44972 Mon Sep 17 00:00:00 2001 From: t-bast Date: Mon, 16 Sep 2024 17:52:08 +0200 Subject: [PATCH] Update trampoline payment to blinded path to match spec proposal We update our trampoline payments to blinded paths to match the official specification from https://github.com/lightning/bolts/pull/836. The blinded paths and recipient features are included in the trampoline onion, which potentially allows using multiple trampoline hops. That was already what we were doing with experimental TLVs, so we simply update the TLV values to match the spec values. --- .../fr/acinq/lightning/wire/PaymentOnion.kt | 114 +++++++---- .../payment/PaymentPacketTestsCommon.kt | 184 +++++++++++++++++- .../lightning/wire/PaymentOnionTestsCommon.kt | 72 ++++++- 3 files changed, 321 insertions(+), 49 deletions(-) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/wire/PaymentOnion.kt b/src/commonMain/kotlin/fr/acinq/lightning/wire/PaymentOnion.kt index 4b5e73cae..58c9fcba8 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/wire/PaymentOnion.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/wire/PaymentOnion.kt @@ -12,6 +12,7 @@ import fr.acinq.bitcoin.utils.flatMap import fr.acinq.lightning.* import fr.acinq.lightning.payment.Bolt11Invoice import fr.acinq.lightning.payment.Bolt12Invoice +import fr.acinq.lightning.payment.Bolt12Invoice.Companion.PaymentBlindedContactInfo import fr.acinq.lightning.utils.msat import fr.acinq.lightning.utils.toByteVector @@ -151,16 +152,43 @@ sealed class OnionPaymentPayloadTlv : Tlv { } /** - * Invoice feature bits. Only included for intermediate trampoline nodes when they should convert to a legacy payment - * because the final recipient doesn't support trampoline. + * Features that may be used to reach the recipient, provided by the payment sender (usually obtained them from an invoice). + * Only included for a trampoline node when relaying to a non-trampoline recipient using [OutgoingBlindedPaths] or [InvoiceRoutingInfo]. */ - data class InvoiceFeatures(val features: ByteVector) : OnionPaymentPayloadTlv() { - override val tag: Long get() = InvoiceFeatures.tag + data class RecipientFeatures(val features: ByteVector) : OnionPaymentPayloadTlv() { + override val tag: Long get() = RecipientFeatures.tag override fun write(out: Output) = LightningCodecs.writeBytes(features, out) - companion object : TlvValueReader { - const val tag: Long = 66097 - override fun read(input: Input): InvoiceFeatures = InvoiceFeatures(ByteVector(LightningCodecs.bytes(input, input.availableBytes))) + companion object : TlvValueReader { + const val tag: Long = 21 + override fun read(input: Input): RecipientFeatures = RecipientFeatures(ByteVector(LightningCodecs.bytes(input, input.availableBytes))) + } + } + + /** + * Blinded paths that can be used to reach the final recipient. + * Only included for a trampoline node when paying a Bolt 12 invoice that doesn't support trampoline. + */ + data class OutgoingBlindedPaths(val paths: List) : OnionPaymentPayloadTlv() { + override val tag: Long get() = OutgoingBlindedPaths.tag + override fun write(out: Output) { + for (path in paths) { + OfferTypes.writePath(path.route, out) + OfferTypes.writePaymentInfo(path.paymentInfo, out) + } + } + + companion object : TlvValueReader { + const val tag: Long = 22 + override fun read(input: Input): OutgoingBlindedPaths { + val paths = ArrayList() + while (input.availableBytes > 0) { + val route = OfferTypes.readPath(input) + val payInfo = OfferTypes.readPaymentInfo(input) + paths.add(Bolt12Invoice.Companion.PaymentBlindedContactInfo(route, payInfo)) + } + return OutgoingBlindedPaths(paths) + } } } @@ -205,30 +233,6 @@ sealed class OnionPaymentPayloadTlv : Tlv { } } - /** Blinded paths to relay the payment to */ - data class OutgoingBlindedPaths(val paths: List) : OnionPaymentPayloadTlv() { - override val tag: Long get() = OutgoingBlindedPaths.tag - override fun write(out: Output) { - for (path in paths) { - OfferTypes.writePath(path.route, out) - OfferTypes.writePaymentInfo(path.paymentInfo, out) - } - } - - companion object : TlvValueReader { - const val tag: Long = 66102 - override fun read(input: Input): OutgoingBlindedPaths { - val paths = ArrayList() - while (input.availableBytes > 0) { - val route = OfferTypes.readPath(input) - val payInfo = OfferTypes.readPaymentInfo(input) - paths.add(Bolt12Invoice.Companion.PaymentBlindedContactInfo(route, payInfo)) - } - return OutgoingBlindedPaths(paths) - } - } - } - } object PaymentOnion { @@ -256,9 +260,10 @@ object PaymentOnion { OnionPaymentPayloadTlv.PaymentMetadata.tag to OnionPaymentPayloadTlv.PaymentMetadata.Companion as TlvValueReader, OnionPaymentPayloadTlv.TotalAmount.tag to OnionPaymentPayloadTlv.TotalAmount.Companion as TlvValueReader, OnionPaymentPayloadTlv.TrampolineOnion.tag to OnionPaymentPayloadTlv.TrampolineOnion.Companion as TlvValueReader, - OnionPaymentPayloadTlv.InvoiceFeatures.tag to OnionPaymentPayloadTlv.InvoiceFeatures.Companion as TlvValueReader, - OnionPaymentPayloadTlv.InvoiceRoutingInfo.tag to OnionPaymentPayloadTlv.InvoiceRoutingInfo.Companion as TlvValueReader, + OnionPaymentPayloadTlv.RecipientFeatures.tag to OnionPaymentPayloadTlv.RecipientFeatures.Companion as TlvValueReader, OnionPaymentPayloadTlv.OutgoingBlindedPaths.tag to OnionPaymentPayloadTlv.OutgoingBlindedPaths.Companion as TlvValueReader, + // The following TLVs aren't official TLVs from the BOLTs. + OnionPaymentPayloadTlv.InvoiceRoutingInfo.tag to OnionPaymentPayloadTlv.InvoiceRoutingInfo.Companion as TlvValueReader, ) ) @@ -423,6 +428,32 @@ object PaymentOnion { } } + data class BlindedChannelRelayPayload(val records: TlvStream) : PerHopPayload() { + override fun write(out: Output) = tlvSerializer.write(records, out) + + companion object : PerHopPayloadReader { + override fun read(input: Input): Either { + return PerHopPayload.read(input).flatMap { tlvs -> + when { + tlvs.get() != null -> Either.Left(InvalidOnionPayload(OnionPaymentPayloadTlv.AmountToForward.tag, 0)) + tlvs.get() != null -> Either.Left(InvalidOnionPayload(OnionPaymentPayloadTlv.OutgoingCltv.tag, 0)) + tlvs.get() != null -> Either.Left(InvalidOnionPayload(OnionPaymentPayloadTlv.OutgoingChannelId.tag, 0)) + tlvs.get() == null -> Either.Left(InvalidOnionPayload(OnionPaymentPayloadTlv.EncryptedRecipientData.tag, 0)) + else -> Either.Right(BlindedChannelRelayPayload(tlvs)) + } + } + } + + fun create(encryptedData: ByteVector, blinding: PublicKey?): BlindedChannelRelayPayload { + val tlvs = buildSet { + add(OnionPaymentPayloadTlv.EncryptedRecipientData(encryptedData)) + blinding?.let { add(OnionPaymentPayloadTlv.BlindingPoint(it)) } + } + return BlindedChannelRelayPayload(TlvStream(tlvs)) + } + } + } + data class NodeRelayPayload(val records: TlvStream) : PerHopPayload() { val amountToForward = records.get()!!.amount val outgoingCltv = records.get()!!.cltv @@ -468,7 +499,7 @@ object PaymentOnion { val outgoingNodeId = records.get()!!.nodeId val paymentSecret = records.get()!!.secret val paymentMetadata = records.get()?.data - val invoiceFeatures = records.get()!!.features + val invoiceFeatures = records.get()!!.features val invoiceRoutingInfo = records.get()!!.extraHops override fun write(out: Output) = tlvSerializer.write(records, out) @@ -481,7 +512,7 @@ object PaymentOnion { tlvs.get() == null -> Either.Left(InvalidOnionPayload(OnionPaymentPayloadTlv.OutgoingCltv.tag, 0)) tlvs.get() == null -> Either.Left(InvalidOnionPayload(OnionPaymentPayloadTlv.OutgoingNodeId.tag, 0)) tlvs.get() == null -> Either.Left(InvalidOnionPayload(OnionPaymentPayloadTlv.PaymentData.tag, 0)) - tlvs.get() == null -> Either.Left(InvalidOnionPayload(OnionPaymentPayloadTlv.InvoiceFeatures.tag, 0)) + tlvs.get() == null -> Either.Left(InvalidOnionPayload(OnionPaymentPayloadTlv.RecipientFeatures.tag, 0)) tlvs.get() == null -> Either.Left(InvalidOnionPayload(OnionPaymentPayloadTlv.InvoiceRoutingInfo.tag, 0)) tlvs.get() != null -> Either.Left(InvalidOnionPayload(OnionPaymentPayloadTlv.EncryptedRecipientData.tag, 0)) tlvs.get() != null -> Either.Left(InvalidOnionPayload(OnionPaymentPayloadTlv.BlindingPoint.tag, 0)) @@ -499,7 +530,7 @@ object PaymentOnion { add(OnionPaymentPayloadTlv.OutgoingNodeId(targetNodeId)) add(OnionPaymentPayloadTlv.PaymentData(invoice.paymentSecret, totalAmount)) invoice.paymentMetadata?.let { add(OnionPaymentPayloadTlv.PaymentMetadata(it)) } - add(OnionPaymentPayloadTlv.InvoiceFeatures(invoice.features.toByteArray().toByteVector())) + add(OnionPaymentPayloadTlv.RecipientFeatures(invoice.features.toByteArray().toByteVector())) add(OnionPaymentPayloadTlv.InvoiceRoutingInfo(routingInfo.map { it.hints })) } ) @@ -507,11 +538,15 @@ object PaymentOnion { } } + /** + * Create a trampoline payload to tell our trampoline node to relay to a blinded path, where the recipient doesn't support trampoline. + * This only reveals the blinded path to our trampoline node, which doesn't reveal the recipient's identity. + */ data class RelayToBlindedPayload(val records: TlvStream) : PerHopPayload() { val amountToForward = records.get()!!.amount val outgoingCltv = records.get()!!.cltv val outgoingBlindedPaths = records.get()!!.paths - val invoiceFeatures = records.get()!!.features + val recipientFeatures = records.get()?.features ?: Features.empty override fun write(out: Output) = tlvSerializer.write(records, out) @@ -521,7 +556,6 @@ object PaymentOnion { when { tlvs.get() == null -> Either.Left(InvalidOnionPayload(OnionPaymentPayloadTlv.AmountToForward.tag, 0)) tlvs.get() == null -> Either.Left(InvalidOnionPayload(OnionPaymentPayloadTlv.OutgoingCltv.tag, 0)) - tlvs.get() == null -> Either.Left(InvalidOnionPayload(OnionPaymentPayloadTlv.InvoiceFeatures.tag, 0)) tlvs.get() == null -> Either.Left(InvalidOnionPayload(OnionPaymentPayloadTlv.OutgoingBlindedPaths.tag, 0)) tlvs.get() != null -> Either.Left(InvalidOnionPayload(OnionPaymentPayloadTlv.EncryptedRecipientData.tag, 0)) tlvs.get() != null -> Either.Left(InvalidOnionPayload(OnionPaymentPayloadTlv.BlindingPoint.tag, 0)) @@ -530,14 +564,14 @@ object PaymentOnion { } } - fun create(amount: MilliSatoshi, expiry: CltvExpiry, features: Features, blindedPaths: List): RelayToBlindedPayload = + fun create(amount: MilliSatoshi, expiry: CltvExpiry, features: Features, blindedPaths: List): RelayToBlindedPayload = RelayToBlindedPayload( TlvStream( setOf( OnionPaymentPayloadTlv.AmountToForward(amount), OnionPaymentPayloadTlv.OutgoingCltv(expiry), OnionPaymentPayloadTlv.OutgoingBlindedPaths(blindedPaths), - OnionPaymentPayloadTlv.InvoiceFeatures(features.toByteArray().toByteVector()) + OnionPaymentPayloadTlv.RecipientFeatures(features.toByteArray().toByteVector()) ) ) ) diff --git a/src/commonTest/kotlin/fr/acinq/lightning/payment/PaymentPacketTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/payment/PaymentPacketTestsCommon.kt index 5612cf897..91a2cf7ed 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/payment/PaymentPacketTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/payment/PaymentPacketTestsCommon.kt @@ -106,7 +106,7 @@ class PaymentPacketTestsCommon : LightningTestSuite() { val innerPayload = PaymentOnion.NodeRelayPayload.read(decryptedInner.payload).right!! assertNull(innerPayload.records.get()) assertNull(innerPayload.records.get()) - assertNull(innerPayload.records.get()) + assertNull(innerPayload.records.get()) assertNull(innerPayload.records.get()) return Triple(outerPayload, innerPayload, decryptedInner.nextPacket) } @@ -125,7 +125,7 @@ class PaymentPacketTestsCommon : LightningTestSuite() { } // Wallets don't need to decrypt onions for intermediate nodes, but it's useful to test that encryption works correctly. - fun decryptRelayToBlinded(add: UpdateAddHtlc, privateKey: PrivateKey): Pair { + fun decryptRelayToBlinded(add: UpdateAddHtlc, privateKey: PrivateKey): Pair { val decrypted = Sphinx.peel(privateKey, add.paymentHash, add.onionRoutingPacket).right!! assertTrue(decrypted.isLastPacket) val outerPayload = PaymentOnion.FinalPayload.Standard.read(decrypted.payload).right!! @@ -437,7 +437,7 @@ class PaymentPacketTestsCommon : LightningTestSuite() { assertEquals(finalAmount, innerC.amountToForward) assertEquals(finalExpiry, innerC.outgoingCltv) assertEquals(listOf(blindedRoute), innerC.outgoingBlindedPaths.map { it.route.route }) - assertEquals(invoice.features.toByteArray().toByteVector(), innerC.invoiceFeatures) + assertEquals(invoice.features.toByteArray().toByteVector(), innerC.recipientFeatures) // C is the introduction node of the blinded path: it can decrypt the first blinded payload and relay to D. val addD = run { @@ -470,6 +470,184 @@ class PaymentPacketTestsCommon : LightningTestSuite() { assertEquals(paymentMetadata.paymentHash, invoice.paymentHash) } + // See bolt04/trampoline-to-blinded-path-payment-onion-test.json + @Test + fun `send a trampoline payment to blinded paths -- reference test vector`() { + val preimage = ByteVector32.fromValidHex("8bb624f63457695115152f4bf9950bbd14972a5f49d882cb1a68aa064742c057") + val paymentHash = Crypto.sha256(preimage).byteVector32() + assertEquals(ByteVector32("e89bc505e84aaca09613833fc58c9069078fb43bfbea0488f34eec9db99b5f82"), paymentHash) + val alicePayerKey = PrivateKey.fromHex("40086168e170767e1c2587d503fea0eaa66ef21069c5858ec6e532503d6a4bd6") + val offerFeatures = Features(Feature.BasicMultiPartPayment to FeatureSupport.Optional) + // Eve creates a blinded path to herself going through Dave and advertises that she supports MPP. + val (blindedPath, pathId) = run { + val evePriv = PrivateKey.fromHex("4545454545454545454545454545454545454545454545454545454545454545") + val eve = evePriv.publicKey() + assertEquals(PublicKey.fromHex("02edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145"), eve) + val dave = PublicKey.fromHex("032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991") + val offer = OfferTypes.Offer.createNonBlindedOffer(null, "bolt12", eve, offerFeatures, Block.RegtestGenesisBlock.hash) + val paymentMetadata = OfferPaymentMetadata.V1(offer.offerId, 150_000_000.msat, preimage, alicePayerKey.publicKey(), "hello", 1, 0) + val blindedPayloadEve = RouteBlindingEncryptedData(TlvStream(RouteBlindingEncryptedDataTlv.PathId(paymentMetadata.toPathId(evePriv)))) + assertContentEquals( + Hex.decode("06 bf 0149792a42a127e421026a0c616e9490fb560d8fa5374a3d38d97aa618056a2ad70000000008f0d1808bb624f63457695115152f4bf9950bbd14972a5f49d882cb1a68aa064742c05702414343fd4a723942a86d5f60d2cfecb6c5e8a65595c9995332ec2dba8fe004a20000000000000001000000000000000068656c6c6f7bcdd1f21161675ee57f03e449abd395867d703a0fa3c1c92fe9111ad9da9fe216f8c170fc25726261af0195732366dad38384c0ab24060c7cd65c49d1de8411"), + blindedPayloadEve.write(), + ) + val blindedPayloadDave = RouteBlindingEncryptedData( + TlvStream( + RouteBlindingEncryptedDataTlv.OutgoingChannelId(ShortChannelId("572330x42x2465")), + RouteBlindingEncryptedDataTlv.PaymentRelay(CltvExpiryDelta(36), 1000, 500.msat), + RouteBlindingEncryptedDataTlv.PaymentConstraints(CltvExpiry(850_000), 1.msat), + ) + ) + assertContentEquals( + Hex.decode("020808bbaa00002a09a1 0a080024000003e801f4 0c05000cf85001"), + blindedPayloadDave.write(), + ) + val sessionKey = PrivateKey.fromHex("090a684b173ac8da6716859095a779208943cf88680c38c249d3e8831e2caf7e") + val blindedRouteDetails = RouteBlinding.create(sessionKey, listOf(dave, eve), listOf(blindedPayloadDave, blindedPayloadEve).map { it.write().byteVector() }) + assertEquals(EncodedNodeId(dave), blindedRouteDetails.route.introductionNodeId) + assertEquals(PublicKey.fromHex("02c952268f1501cf108839f4f5d0fbb41a97de778a6ead8caf161c569bd4df1ad7"), blindedRouteDetails.lastBlinding) + assertEquals(PublicKey.fromHex("02988face71e92c345a068f740191fd8e53be14f0bb957ef730d3c5f76087b960e"), blindedRouteDetails.route.blindingKey) + val blindedNodes = listOf( + PublicKey.fromHex("0295d40514096a8be54859e7dfe947b376eaafea8afe5cb4eb2c13ff857ed0b4be"), + PublicKey.fromHex("020e2dbadcc2005e859819ddebbe88a834ae8a6d2b049233c07335f15cd1dc5f22"), + ) + assertEquals(blindedNodes, blindedRouteDetails.route.blindedNodeIds) + val encryptedPayloads = listOf( + ByteVector("0ae636dc5963bcfe2a4705538b3b6d2c5cd87dce29374d47cb64d16b3a0d95f21b1af81f31f61c01e81a86"), + ByteVector("bcd747ba974bc6ac175df8d5dbd462acb1dc4f3fa1de21da4c5774d233d8ecd9b84b7420175f9ec920f2ef261cdb83dc28cc3a0eeb970107b3306489bf771ef5b1213bca811d345285405861d08a655b6c237fa247a8b4491beee20c878a60e9816492026d8feb9dafa84585b253978db6a0aa2945df5ef445c61e801fb82f43d59347cc1c013a2351f094cdafb5e0d1f5ccb1055d6a5dd086a69cd75d34ea06067659cb7bb02dda9c2d89978dc725168f93ab2fe22dff354bce6017b60d0cc5b29b01540595e6d024f3812adda1960b4d"), + ) + assertEquals(encryptedPayloads, blindedRouteDetails.route.encryptedPayloads) + val paymentInfo = OfferTypes.PaymentInfo(500.msat, 1000, CltvExpiryDelta(36), 1.msat, 500_000_000.msat, Features.empty) + Pair(Bolt12Invoice.Companion.PaymentBlindedContactInfo(OfferTypes.ContactInfo.BlindedPath(blindedRouteDetails.route), paymentInfo), blindedPayloadEve.pathId) + } + // Alice creates a trampoline onion for Carol that includes Eve's blinded path. + val trampolineOnion = run { + val carol = PublicKey.fromHex("027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007") + val trampolinePayload = PaymentOnion.RelayToBlindedPayload.create(150_000_000.msat, CltvExpiry(800_000), offerFeatures, listOf(blindedPath)) + assertContentEquals( + Hex.decode("fd01b5 020408f0d180 04030c3500 1503020000 16fd01a1032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e66868099102988face71e92c345a068f740191fd8e53be14f0bb957ef730d3c5f76087b960e020295d40514096a8be54859e7dfe947b376eaafea8afe5cb4eb2c13ff857ed0b4be002b0ae636dc5963bcfe2a4705538b3b6d2c5cd87dce29374d47cb64d16b3a0d95f21b1af81f31f61c01e81a86020e2dbadcc2005e859819ddebbe88a834ae8a6d2b049233c07335f15cd1dc5f2200d1bcd747ba974bc6ac175df8d5dbd462acb1dc4f3fa1de21da4c5774d233d8ecd9b84b7420175f9ec920f2ef261cdb83dc28cc3a0eeb970107b3306489bf771ef5b1213bca811d345285405861d08a655b6c237fa247a8b4491beee20c878a60e9816492026d8feb9dafa84585b253978db6a0aa2945df5ef445c61e801fb82f43d59347cc1c013a2351f094cdafb5e0d1f5ccb1055d6a5dd086a69cd75d34ea06067659cb7bb02dda9c2d89978dc725168f93ab2fe22dff354bce6017b60d0cc5b29b01540595e6d024f3812adda1960b4d000001f4000003e800240000000000000001000000001dcd65000000"), + trampolinePayload.write(), + ) + val sessionKey = PrivateKey.fromHex("a64feb81abd58e473df290e9e1c07dc3e56114495cadf33191f44ba5448ebe99") + OutgoingPaymentPacket.buildOnion(sessionKey, listOf(carol), listOf(trampolinePayload), paymentHash).packet + } + assertEquals( + "0002bc59a9abc893d75a8d4f56a6572f9a3507323a8de22abe0496ea8d37da166a8b98b9bf5cf80f093ee323cbb0c5b0713b14779893b07e4cc60110ce2d2240f16be3fd3c23062491fb57d229dac4edbad7a3b26242cffc2a2e9d5a0eae187390d4e096699d093f5ac82d86abdf0fdaae01bf16b80261e30f6ffda635ea7662dc0d124e1137367ab0178d6ed0de8e307a5c94a213b0b5705efcc94440308f477a185f5b41ab698e4c2dd7adea3aa47cccb5f47548c9ec2fee9573d32042eee6851a4f17406b6f6d13e2b794b0bd1676d0c3b33e4ee102823bb9e55f0ec29fc7f9df3332be5f9c68d4482ff60c0183c17742844baf01821cc1a2dbed1f764d124a5696f290db7f43608ddad007da504a56d0c714a0d34eeeed848d08c846609d29123df3f82484a7ae994c37487add9c878a737bb9d6e314139329b2eed131906a5717516f7790f0ec78f3e1a6c9b9c0680221dd290e3e219146039cb02f28eec46b88d5eceae7738182d9b1be14130636943dfa95aee4cf0f81bcdb04b8f92e3c9841f9928a7b39c3c8861dd4b73bf736b1e1b0d9a22c3bf3c12cdb1580c343a129b93cbda9e58675a52cde759040718c25504ea28df3b6da73e832b5bd7b51054a5663d407871c4a90e76824eca922ccde0bdd30e81f1ce9bed788416cc9660b016adccab6a45e0ac23d11030f7076b88184c247da4586d4fa3102e44f882ae88a46cf4a4dd874a9466c31eb94c834ac6c9cfb4bb9a6ef6a6a", + Hex.encode(OnionRoutingPacketSerializer(trampolineOnion.payload.size()).write(trampolineOnion)), + ) + // Alice creates a payment onion for Carol (Alice -> Bob -> Carol). + val onionForBob = run { + val sessionKey = PrivateKey.fromHex("4f777e8dac16e6dfe333066d9efb014f7a51d11762ff76eca4d3a95ada99ba3e") + val bob = PublicKey.fromHex("0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c") + val carol = PublicKey.fromHex("027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007") + val paymentSecret = ByteVector32("7494b65bc092b48a75465e43e29be807eb2cc535ce8aaba31012b8ff1ceac5da") + val payloadBob = PaymentOnion.ChannelRelayPayload.create(ShortChannelId("572330x42x2821"), 150_153_000.msat, CltvExpiry(800_060)) + assertContentEquals( + Hex.decode("15 020408f32728 04030c353c 060808bbaa00002a0b05"), + payloadBob.write(), + ) + val payloadCarol = PaymentOnion.FinalPayload.Standard( + TlvStream( + OnionPaymentPayloadTlv.AmountToForward(150_153_000.msat), + OnionPaymentPayloadTlv.OutgoingCltv(CltvExpiry(800_060)), + OnionPaymentPayloadTlv.PaymentData(paymentSecret, 150_153_000.msat), + OnionPaymentPayloadTlv.TrampolineOnion(trampolineOnion), + ) + ) + assertContentEquals( + Hex.decode( + "fd024f 020408f32728 04030c353c 08247494b65bc092b48a75465e43e29be807eb2cc535ce8aaba31012b8ff1ceac5da08f32728 14fd021a0002bc59a9abc893d75a8d4f56a6572f9a3507323a8de22abe0496ea8d37da166a8b98b9bf5cf80f093ee323cbb0c5b0713b14779893b07e4cc60110ce2d2240f16be3fd3c23062491fb57d229dac4edbad7a3b26242cffc2a2e9d5a0eae187390d4e096699d093f5ac82d86abdf0fdaae01bf16b80261e30f6ffda635ea7662dc0d124e1137367ab0178d6ed0de8e307a5c94a213b0b5705efcc94440308f477a185f5b41ab698e4c2dd7adea3aa47cccb5f47548c9ec2fee9573d32042eee6851a4f17406b6f6d13e2b794b0bd1676d0c3b33e4ee102823bb9e55f0ec29fc7f9df3332be5f9c68d4482ff60c0183c17742844baf01821cc1a2dbed1f764d124a5696f290db7f43608ddad007da504a56d0c714a0d34eeeed848d08c846609d29123df3f82484a7ae994c37487add9c878a737bb9d6e314139329b2eed131906a5717516f7790f0ec78f3e1a6c9b9c0680221dd290e3e219146039cb02f28eec46b88d5eceae7738182d9b1be14130636943dfa95aee4cf0f81bcdb04b8f92e3c9841f9928a7b39c3c8861dd4b73bf736b1e1b0d9a22c3bf3c12cdb1580c343a129b93cbda9e58675a52cde759040718c25504ea28df3b6da73e832b5bd7b51054a5663d407871c4a90e76824eca922ccde0bdd30e81f1ce9bed788416cc9660b016adccab6a45e0ac23d11030f7076b88184c247da4586d4fa3102e44f882ae88a46cf4a4dd874a9466c31eb94c834ac6c9cfb4bb9a6ef6a6a" + ), + payloadCarol.write(), + ) + OutgoingPaymentPacket.buildOnion(sessionKey, listOf(bob, carol), listOf(payloadBob, payloadCarol), paymentHash, OnionRoutingPacket.PaymentPacketLength).packet + } + assertEquals( + "00025fd60556c134ae97e4baedba220a644037754ee67c54fd05e93bf40c17cbb73362fb9dee96001ff229945595b6edb59437a6bc14340622675e61ad0fd4e5e9473ea41567f4f7b0938040e2076c378a98409260c7234f87c58657bcf20af7e63d7192b46cf171e4f73cb11f9f603915389105d91ad630224bea95d735e3988add1e24b5bf28f1d7128db64284d930839ba340d088c74b1fb1bd21136b1809428ec5399c8649e9bdf92d2dcfc694deae5095f9ea212871fc9229a545ddd273635939ed304ba8a2c0a80a1a2ff7f95df532cde150bb304cd84abf88abe6e09b405d10e5e422f6d839a245fd2a300b2f6b95eedecf88479a3950727e6eeac46a34b2930aa9b0d7dd02d021d59800c3f7d5bae58eb45d03f31cce59f04715d7f7158fb2413f9ffe83b869c52019a54f6e0e194479e2eb546a6efe27cdb69863b5ff4e218e57b3e7aff727296036ed6b6756b6b98b22607b699190ced7484df2fd487fd679bf7b327322afd8c9ed658564a2d715cd86e0d270f3fad64980ef2926b82c415cdc537ff5d037b0a2986a44857ce430dfabce135748b4bd4daf3afaac064b1571fbce1369b7d7166c2638d426b6a3a418e5f017699373f614815de8275c74cd57bcfb9f3c5a11183cbb8f488bb255f7a0c3299df1306fdeeca785d81a7bcba5036a4891cd20d1b16c5436c51992d4797e124df65f2d71479739923b46d3daa3a0ecc75404c0475e5cd01665bf375e3897b3a57d2fa89ce1fa7d667ecfe0c097cfb7d94634c5a2b7c6ad5a3de7f9980a0779b66dff957389bed1e19d4681299fbe762a6ca0f9fc0726c203dc2021e74375559453ba0d2c2825142ed007cefb1e1466bb99303dbf4ceaba5eb76d18204910df11e3e3747d6d147c599edfbaf898fbd09d867558dec280e8d1c44d6e440a3c8d3b1afcfe7f3b1731a42bee7b6f7227e755bcc936952b696740f387c0ab93fd8b1d6bd4c74aebe535d6110014cd3d525394027dfe8fad19477d69dc1671d1133f5d8d21b55ddc7f3c76dabf718ca6f02da0d6445e4326b781c6d9041e9e330e44950d10d5dbed7f98b708d1681b75f8fe56c99c7a424899c6a06f36e5b29f2c3db0050bebeffee8b729351518644f98246c1db892ff3305b7610cfb09d5465f5a94da4812d35275c42f4b3a9cbfe626cee01e1818cdbe71565104e112d1c2c74450488d06de19c831d3c3e5203e67229bd9619166aab97fb579623c418b23c05fabf39e21ba0d7baf4aa67034b1ba8c4bd4542825471c7dfb37c24cdfd2d8d06d0e7ddaca01f001449195cc04201a7ae2da86e74d515e2feef3707e29508768f18eb5741ef22dc07f078cf751da83ee2fe9927c760474cdce266fce9b66959d391d51b204fa50cd9a8ff7b45fdd043679a20afa0b440938a721fef14badb97b68ad5e5494dfb2aea8edc1cdb69eb6f13b75bbd496c8eb35a48f229a080ae6744dec87f58058296c2969f0916685ac57a0a44efe4691eb06236f414334f5747a11b100e1d6272ff6082510fa79c64bcfaa58e43525f9fbbea025aa741feb7b18925e2dbd0da2a73748a6c30fe625afb497189d7f188869602989a53892ad24624807e1581eeca2db2cef855aa65af66c4573f9c637699bcbe8ae5f6d9f0713ffe52d453faa39b44be3108e940b322db0d1dc008aff99d4909345ffcf996a382359e7e5b4592522d453fffa9744e1e32a21a237fff4c8c55c1f46fdc5b2e8de267419a3052b33c6065119f690e972ac9b19921bab489a572df128494a1158650665bc875bbc02de3cac75963cee5c10075768d921edacb382044c74848af73092641a57c2050ea0e68dbb6c6121b1bf012073c8812d68fac75a06a8a35bec984c71ff951eb3ef18e96e1158f653a837a9fec2df21cdd816d36bf998ee108b562a60a6", + Hex.encode(OnionRoutingPacketSerializer(OnionRoutingPacket.PaymentPacketLength).write(onionForBob)), + ) + // Bob decrypts the onion and relays to Carol. + val onionForCarol = run { + val bobPriv = PrivateKey.fromHex("4242424242424242424242424242424242424242424242424242424242424242") + val add = UpdateAddHtlc(randomBytes32(), 1, 150_155_000.msat, paymentHash, CltvExpiry(800_100), onionForBob) + val (payload, onion) = decryptChannelRelay(add, bobPriv) + assertEquals(ShortChannelId("572330x42x2821"), payload.outgoingChannelId) + assertEquals(150_153_000.msat, payload.amountToForward) + assertEquals(CltvExpiry(800_060), payload.outgoingCltv) + onion + } + assertEquals( + "0003dc6a4c9b34bdcd2191fc4dacfc1aeb20f71991acbd17847b9ab17d69579c1614da126cd18820e55534d7352839caa436aa79a6d5be26c6ecbd1c79f74442b1b7bf8ed8b739b736b9248769c2c422eebc85fb0d580e95618bcda1be3fd4cfa6ed0d839a2feb878acf686b112febaae9c1494a2ad20d97b2d2f7e54e6e9860e75e35671d5530ff9cf131b16b00a89337781aa37e5b867995d56578e69c031b7b272c4697727210e10bc8456e5cd58ae958d07e2811e2fb767b702c792b26fd6c352306b31f808a0e46d28ababda518d0d33c8f3a301adef4dd4f12fd2f78da4d548b7c12b0d890b6ab24e724e569106ae47b1acea4f5de055ba6d910bbe824810a11349ce7ea557abf02c5104740b52c910cd0bfea5d8666a41448703c054ed0612775e8617eda8df2fdccebf65193301738ba4308b61f447016a0b801de0eff2a7db374e6ccdadc9efbcb2fe0fb56c34fbaffd1bf87e5bf46cca75b77cdb7161532402fbc9323af57304e0b6cfa5082af683ef82a2731e89734b9e377184c647486c63ab57e18d4f42e9ddd55189a064cfc3a2800b8abd291043aab068c8c8ce57f17ef169945bba4d434d67bc883b68ba2c2c92dad42c788a209b4d7a2e00b375b811766ff67fc630ae047b8d2781e00291f6d1e31495a797a7e4ed135585da237cdcd067d37641e49f562f22dd619240bb2411fab802f834d96aa6451fd4b3f585dbde15bc78e692f49a491dcd8e44a8ef035bcbb4863462d7cf6066e0df516dcb6209674abe54e7d2faca26d17019bc2b6ec59ec94b51fd62064e7ff2230e73c375fadf7f305c307870a1d3dcb4eecddce6eeca54bff76b945823364ca823f7f3dc273c5eaa6d7aa3b510bf3bd274c8bd73570d15bc1ff0ba90c3bb65a8e1ee89cc6c44114d658fb89985c7ca8e8eeb32bc8be1e3ab951ff1a720bcc0d4c298ae4c1d06c164615c8af5bbda93f431d5d2be8bc40320c9c3a002bd9f2e39828abb6e7bfde83421d7faed6b16f355b9bd86d018fb3ec0f98ffdcaae8d521bd5003e93382459fc7957e2590409e5c8a88d7c1488884da0e148b01ec99aafe96d418d7cd76d7437d3c1d9d79e79386e3286210fac073eac6cd90031ac1c5b70b494d60e74d243ee44bfb8d0fcc57d3f8683aaadc5a2d346fce681a8d4a4931e932a39e2ab443141eb5c29a475679c5ee4e8b94e9d5de731f03963acaddd7301be90c7ccdfab314f70e843037a98656c31b22c822312719434f7a503bac9f18eb2f0cbc2c2790e93fc1664b82726eb1265a4ffa1e8e72d2898df1d8db9da1586675d242ff565aa008a35aad1c65b50c07ae6c0452bcdaa2f5410600acb3326e335971eba42c1dbac36005b5299ab7b852812717048aa51f272e8ec21c11e22a25b48ef60ed98540d879f5ae6820ac94cfa29e5d0aa74d91ca30ee28e97cd94968b4f246b3f93f36ecbf1c84f12844867f0738c3c775981a827cb05ddc5bebd288b6312b0b3f7d46f6eb4ddaf91e7c6a3afdbc291ac5a151675f3c4ae23ab301a9c3f5e1ba62aef64dc50cd977a34ffe58a78feda76c27cc3d5a3a1e05303e9cdd72d60ed17cc90c88b015f3c4891651537b52d837ef0d5f9a90b01e05a9339a623034aea961f7bdc148f129f61f7e12d4ebd1ed37565935cdaec4ea6b7020e62d5db3bad4a3b1141ec3c78d679498bbb348091f56279a3c01662db7694ba54efd8d8f1271f4b06cba94804c3197f92ea97e93bdef8fcb348a405792855e84c1c9625153187495825c5a293e1efc7b672ddb609aa90caca1e7182ba301313a17364b48cd93b6a26cb6888d5c4cc1d3c6a39cb442c2de12bb1ad396205c9a10023d7edc951fc60a28813867f635c3d74d4206f398849e65eb5a8d8fdeb952ae813073c3b617ed68c7bf1a18a6b9f9e3af316029be4dd8", + Hex.encode(OnionRoutingPacketSerializer(OnionRoutingPacket.PaymentPacketLength).write(onionForCarol)), + ) + // Carol decrypts the onion and relays to the blinded path's introduction node Dave. + val onionForDave = run { + val carolPriv = PrivateKey.fromHex("4343434343434343434343434343434343434343434343434343434343434343") + val add = UpdateAddHtlc(randomBytes32(), 1, 150_153_000.msat, paymentHash, CltvExpiry(800_060), onionForCarol) + val (payload, trampolinePayload) = decryptRelayToBlinded(add, carolPriv) + assertEquals(150_153_000.msat, payload.amount) + assertEquals(CltvExpiry(800_060), payload.expiry) + assertEquals(ByteVector32("7494b65bc092b48a75465e43e29be807eb2cc535ce8aaba31012b8ff1ceac5da"), payload.paymentSecret) + assertEquals(Features(Feature.BasicMultiPartPayment to FeatureSupport.Optional).toByteArray().toByteVector(), trampolinePayload.recipientFeatures) + assertEquals(1, trampolinePayload.outgoingBlindedPaths.size) + assertEquals(150_000_000.msat, trampolinePayload.amountToForward) + assertEquals(CltvExpiry(800_000), trampolinePayload.outgoingCltv) + val outgoingPath = trampolinePayload.outgoingBlindedPaths.first() + assertEquals(outgoingPath.paymentInfo, OfferTypes.PaymentInfo(500.msat, 1000, CltvExpiryDelta(36), 1.msat, 500_000_000.msat, Features.empty)) + val sessionKey = PrivateKey.fromHex("e4acea94d5ddce1a557229bc39f8953ec1398171f9c2c6bb97d20152933be4c4") + val payloadDave = PaymentOnion.BlindedChannelRelayPayload.create(outgoingPath.route.route.encryptedPayloads.first(), outgoingPath.route.route.blindingKey) + assertContentEquals( + Hex.decode("50 0a2b0ae636dc5963bcfe2a4705538b3b6d2c5cd87dce29374d47cb64d16b3a0d95f21b1af81f31f61c01e81a86 0c2102988face71e92c345a068f740191fd8e53be14f0bb957ef730d3c5f76087b960e"), + payloadDave.write(), + ) + val payloadEve = PaymentOnion.FinalPayload.Blinded( + TlvStream( + OnionPaymentPayloadTlv.AmountToForward(trampolinePayload.amountToForward), + OnionPaymentPayloadTlv.OutgoingCltv(trampolinePayload.outgoingCltv), + OnionPaymentPayloadTlv.TotalAmount(trampolinePayload.amountToForward), + OnionPaymentPayloadTlv.EncryptedRecipientData(outgoingPath.route.route.encryptedPayloads.last()) + ), + // ignored + RouteBlindingEncryptedData(TlvStream(RouteBlindingEncryptedDataTlv.PathId(ByteVector("deadbeef")))) + ) + assertContentEquals( + Hex.decode("e4 020408f0d180 04030c3500 0ad1bcd747ba974bc6ac175df8d5dbd462acb1dc4f3fa1de21da4c5774d233d8ecd9b84b7420175f9ec920f2ef261cdb83dc28cc3a0eeb970107b3306489bf771ef5b1213bca811d345285405861d08a655b6c237fa247a8b4491beee20c878a60e9816492026d8feb9dafa84585b253978db6a0aa2945df5ef445c61e801fb82f43d59347cc1c013a2351f094cdafb5e0d1f5ccb1055d6a5dd086a69cd75d34ea06067659cb7bb02dda9c2d89978dc725168f93ab2fe22dff354bce6017b60d0cc5b29b01540595e6d024f3812adda1960b4d 120408f0d180"), + payloadEve.write(), + ) + val nodes = listOf((outgoingPath.route.route.introductionNodeId as EncodedNodeId.WithPublicKey).publicKey, outgoingPath.route.route.blindedNodeIds.last()) + OutgoingPaymentPacket.buildOnion(sessionKey, nodes, listOf(payloadDave, payloadEve), paymentHash, payloadLength = OnionRoutingPacket.PaymentPacketLength).packet + } + assertEquals( + "0003626d5fa7ed0b8706f975ca58ae2c645889514153568aaab7835325ecd3739a5760171be581c1df1ce5267cd012e06f45a97fdbc5d8fc0c140c7432d34027b9ed0f2ab09fd388fe47525309f9c4509a7b9d74d88c28e40b1b6598e42fe2975f857e28b224316f266decf170cbfb5019dea2dab2ee7f1db089d44d1f974d6974bd06e515510cc1c178cd46c2442f07b9a083b4e4c7e9dd8d728508353959497fb8d25ebe8db83c60488566952fb1725267088ccb98acace7147d3846388464aa9512fe94f1962a54d8896d94105b185a41201e0dacf51f755da8e666a78261bc478ca77bd0ef5576bd7a4b24bb1fee9a97618bac0dcc4f1a34d64f5623446e2458089299be2f07592d69619bd6048c37a0460062b194f6f05da8f4ac1c5ff19681067398fdde459c60b4f448d5b3c1152988f6e29dc73b3a5407f1a502dcf2d656bcdba5f05eb4a7ecd3a1373c495dbc23109912aa456f0d9c1460f99f8151886ec8a69af2ac3ed76823ce372fb46a3c20ff114c04ee4a16ff673382b1abedcfc5e8d6f6e77c893dc346cf01f323bb043840546f9728b060514ddc4359c3ebc818abe56c8219260e26c833ff6faef7c02a3e669026dcc0a96ca4f0f8240185422355e0a5c9529bf65e7c52b384cfbe2eeb3ba32c118cfb6961068362b7bf41b2b1580cfc85757fd294840154cb8b13df456c6b86957a33391fd78e3aec6ff2fb9dabeefc63dc4faadfc12016d9d9501381c9751c581256cc24f8d8fecec2a1efaa9579935d5f5f7d14edb64ce97ab9deacb5c9d4c111325c70493ad7921966369315a1ec09c320c3cdc7a65ce52ff8ba5e9a71326e57f8b30766e6e5c77747b07e351c3e91efdf736a31410c9e278a683dfe1ca9c35d7868f11e1e429cb7655ff126438c83f69c3db2c5257e03f7d4f95e8d49400ee36e8d7f1629ac79f0b63430b115349df21e8d286a69b41d52e52e36553b16edf4c77acfb9d4596abd5054daf076b06abb3f84ecbea3e6d324965c7667ec7f83388ee05583f53f258291a806c025e300d63c81f5a411447b3ab3ee47b2dae485b8a87129224ad16fcc043a2d1b89e5c4f35f02675efa79730f5ee07d2de9d6ab503aa329f201ad0c9040d8c3437efde15c53b9212e93e0ece4a3ee7ae99a18b3fd75e8d1ee0ce9c73bfd5c2bbe30a91a3f92169a05887069dd31edf575265425d09998e2466bdf86919cdbfbbdb55c718b046197028b4370dd850833853b969a37e31f2cce96020a1fb22959b4529ae501f44d989b3f7473aaa787899ba200468b070079e2b9a3cd6b04b3caf2de5956aed477e4b3a9f0c93ac3f1042d16ee6a36744460e6d86144522215eeed052daebc7861d7189abe78edb67dd7ae47224b9bdb5907fdca6e6573dfe4bcf24ce1c6a4dfbae6991a8ac6976d9ec8a81f08dbdc34bf1cb18d93aa2e9d876335e0fce0d7a7b6c7080a70b1fc9bef912e4550931005210da7c46c76cf63fb02202df35d332e9ad779ef5ee086fd9fa993852be315691cf84c7e588ec61726b9fe5200ad30b2d43b1684f1dcd8df3f1598ae3841125eadfd534b074d560fd8e0eb9660c93a478ccfdd2308f587a45d5b933af280d39a77e19cd72c170931e4c8e44028bb6db77ec1e9b77af225e39db67bfb80afc6a0efe9864a80222fbfd6c4b6ad9afd43c76f2b9fc0cd0a4b07939147b005be7e6418295830a9bb114cc2c40bdb715077ef4219643455f2675ba00c0e6464f612c32cffa39f49d80ff91cf1363e109101e368114537fbd94428b6ed1934f6d8cd3b6cf2ec736ddcacf63007481fcd6dd9fca8ee39d9a4bbdd06349a5e86af75d8723eeeddc6f84575516997f7db931b91007bcfd21f1b5a8dc69ee846492493054b012e5a4ff3707d5aae44a4ca65210eb1c14c8d138441170f2e5e2920c1e4", + Hex.encode(OnionRoutingPacketSerializer(OnionRoutingPacket.PaymentPacketLength).write(onionForDave)), + ) + // Dave decrypts the onion and relays to Eve. + val onionForEve = run { + val davePriv = PrivateKey.fromHex("4444444444444444444444444444444444444444444444444444444444444444") + val decrypted = Sphinx.peel(davePriv, paymentHash, onionForDave).right!! + assertFalse(decrypted.isLastPacket) + val decoded = PaymentOnion.BlindedChannelRelayPayload.read(decrypted.payload).right!! + assertNotNull(decoded.records.get()) + decrypted.nextPacket + } + assertEquals( + "0002580900a2090fc8b09d77f87781aa5a5964372ee968bd8488da62e04e3f1d68bda5d48cb395a094d2d60f43d8af709c5b44bba1c51f3d590c462829104a18ec68d5c36989a3d6af086f2f61e791e619fd62bf6fccdcdb1dc01bb1798bff5550d385a3ea26ce909d6d218eb12cfa089d11d33a1cb1299510a4c5ac1f767ee18230960b2a37994dc05378ca9d6ce8c29c61dc543f11b676f1ffd3c0c0fe7d43168ecaa1760b115d397b4886c17daaf8dabaac2c5ce3e57f7b5441130e828c5368eb605c841045d84d137197512d9a6efa3bb8fb05a70af7b14f5d01518a61932717ebd04e5022e6925767f07f33b63894bbaff907967999001d6b4cb4b3bc42a9057b8b1d269172f638275688915ae9c07d276fa8aaf037a59069c3d2121a79e8eb869ade6b1dc5073922145d7e1246baa202544d5045fca6fb2974dabc145257d32f0ab5afdbb121b9d93dc1b3d345038714d70ed941be5dec56d4c5cb0582ebcb2d4a78356f75bf1696f82deecec4a97a23746b440082e07ee7d5ebac4c098a48fe0d64de53b303b960b52aabe8df029b9cb5b6079ecac2a2841dd662a2099e1c5995176b9dbc90054b789a07cfbd35e93c0da58eefa7150f7c793b37f4934e2541d6002be6953a0dfda018a881b4d7458d04d4ece3d6d570f1ac46e2eb7ad29652adae7f56ca804d88a1a92ecc17bad4ab7879e93aa56782c46f8b0fca6068a5c3593cddcd372db066bbd7ea615a0fc8b01b61849930959d3ec7951d619b93fc9feecd07c91ec6206a8a489023a55349c1d0b6c294104190090c2c82f1e00c1cf4c9349b09544157aedfed527fa5725408d38d8026916f6baac3218ab5469e157bc91475a5117947efab4af7a64373694cc62b08e0b8bfe1a35ba2f80fac95e043a17e850590bbfbb59d4c190a4fa1642d790e3403a34522f33de66e839c3e3706f9bea9d95efca2f9c7b012bfc39cb9f3e7dad4a1c7b52a8d02151a1b3524a64033d2868e9c450d496f66d71c414870c15911dee4365f1aab8a20b3968a67d04dd724955b0396a00dbdbaa0c0037a2bb8202061f6cc653a10e6ed8ce98a5b1315d5efa96603e989ce1cf315cf2e300f12696c96e45efd397776f8a781d12b8d4e3f265e49c8932cf54525af20977ab1c5b5e0a4f929074baf6b0d4fde175d02a78e0fccd4e814c0a2139475ea16517c33389a41160014f537c43c818f70ba1b9503987885f634f93b995c04f7302d1ff85add09232a2d2be27fbbf98d754c6a0e2c32f66b2a2cd6d5feef4ad10b62303ce05049e862e96987defc569cf6406585fedcc4bf4981ad67cb6af242e25f9bf701e5236deb61305bd0c20c2bfa0d17d6519979f3085427dcad1677959fa40565e16f2feee4b4974de401123f4b3e0f0e740305cdecc8f4b65d638cdd5b1af0013d5806c9d6661b96954463adee45cbacf33c16e836d8e544cab9eb47f9f661d415772a9dae0d4c3ffb44015bc6921e05e6bd8c5159893fd7e5291f6e40c84db19266a35c666afb1ec16d8c4bc507b887df09a2c71a599dbcdf75ced11eb8cd9c65f05a14a3a381971e615bdece5946affe0dbdbbb54a777e5d996e9cb9a5163bc503b88b15b31cd0fa3a8206701aa9f4068e6baac2b2f342e02f94ed22f43f285a6790ff1e216c917af77b5af726e403ce8615959b31e6d051c0a17f737ffef28264ec31c3f0f690f0f142c0b16c88507a44714516fdaee00b697288fdfea823a30bf11fa6cf3ae2215eb42b98aae1e80444c6f2688a5f8f80f1236fb3d12584f33bdfc33beb8c5b7bfdfeb94e25ed4c1fdf69f4a28f6cbb7fa0fb9424927e195908d0a8894555d02f285962a53a984fca3f6b3fb843e4d559e5294c2e01dd1dce5692664881c4dec168d52e42981c6d72f0a84caa78ebf409cb62584ec539f89147c1", + Hex.encode(OnionRoutingPacketSerializer(OnionRoutingPacket.PaymentPacketLength).write(onionForEve)), + ) + // Eve receives the payment. + run { + val evePriv = PrivateKey.fromHex("4545454545454545454545454545454545454545454545454545454545454545") + val blinding = PublicKey.fromHex("02c952268f1501cf108839f4f5d0fbb41a97de778a6ead8caf161c569bd4df1ad7") + val add = UpdateAddHtlc(randomBytes32(), 1, 150_000_000.msat, paymentHash, CltvExpiry(800_000), onionForEve, blinding, null) + val payload = IncomingPaymentPacket.decrypt(add, evePriv).right + assertNotNull(payload) + assertIs(payload) + assertEquals(150_000_000.msat, payload.amount) + assertEquals(CltvExpiry(800_000), payload.expiry) + assertEquals(pathId, payload.pathId) + } + } + @Test fun `receive a channel payment`() { // c -> d diff --git a/src/commonTest/kotlin/fr/acinq/lightning/wire/PaymentOnionTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/wire/PaymentOnionTestsCommon.kt index d54731fa5..3f690d02c 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/wire/PaymentOnionTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/wire/PaymentOnionTestsCommon.kt @@ -3,13 +3,14 @@ package fr.acinq.lightning.wire import fr.acinq.bitcoin.ByteVector import fr.acinq.bitcoin.ByteVector32 import fr.acinq.bitcoin.PublicKey -import fr.acinq.lightning.CltvExpiry -import fr.acinq.lightning.CltvExpiryDelta -import fr.acinq.lightning.MilliSatoshi -import fr.acinq.lightning.ShortChannelId +import fr.acinq.bitcoin.byteVector +import fr.acinq.lightning.* +import fr.acinq.lightning.crypto.RouteBlinding import fr.acinq.lightning.payment.Bolt11Invoice +import fr.acinq.lightning.payment.Bolt12Invoice import fr.acinq.lightning.tests.utils.LightningTestSuite import fr.acinq.lightning.utils.msat +import fr.acinq.lightning.utils.toByteVector import fr.acinq.secp256k1.Hex import kotlin.test.Test import kotlin.test.assertContentEquals @@ -56,6 +57,22 @@ class PaymentOnionTestsCommon : LightningTestSuite() { } } + @Test + fun `encode - decode blinded channel relay payload`() { + val testCases = mapOf( + // @formatter:off + TlvStream(OnionPaymentPayloadTlv.EncryptedRecipientData(ByteVector("0ae636dc5963bcfe3a2f055c8b8f6d2c5cd818362032404a6e35995ba57e101eac7e5fa04bb33a8920f1")), OnionPaymentPayloadTlv.BlindingPoint(PublicKey.fromHex("02988face71e92c345a068f740191fd8e53be14f0bb957ef730d3c5f76087b960e"))) to Hex.decode("4f 0a2a0ae636dc5963bcfe3a2f055c8b8f6d2c5cd818362032404a6e35995ba57e101eac7e5fa04bb33a8920f1 0c2102988face71e92c345a068f740191fd8e53be14f0bb957ef730d3c5f76087b960e"), + // @formatter:on + ) + + testCases.forEach { + val encoded = PaymentOnion.PerHopPayload.tlvSerializer.write(it.key) + assertContentEquals(it.value, encoded) + val decoded = PaymentOnion.BlindedChannelRelayPayload.read(it.value).right!! + assertEquals(it.key, decoded.records) + } + } + @Test fun `encode - decode node relay per-hop payload`() { val nodeId = PublicKey(Hex.decode("02edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145")) @@ -73,6 +90,48 @@ class PaymentOnionTestsCommon : LightningTestSuite() { assertContentEquals(bin, encoded) } + @Test + fun `encode - decode node relay to blinded per-hop payload`() { + val blindedPath = RouteBlinding.BlindedRoute( + EncodedNodeId(PublicKey.fromHex("032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991")), + PublicKey.fromHex("02988face71e92c345a068f740191fd8e53be14f0bb957ef730d3c5f76087b960e"), + listOf( + RouteBlinding.BlindedNode( + PublicKey.fromHex("0295d40514096a8be54859e7dfe947b376eaafea8afe5cb4eb2c13ff857ed0b4be"), + ByteVector("0ae636dc5963bcfe3a2f055c8b8f6d2c5cd818362032404a6e35995ba57e101eac7e5fa04bb33a8920f1") + ), + RouteBlinding.BlindedNode( + PublicKey.fromHex("020e2dbadcc2005e859819ddebbe88a834ae8a6d2b049233c07335f15cd1dc5f22"), + ByteVector("bc211f6ccd409888ca8ab027a5f21d229f9e18ff02cc1161a8344c91bdefc742d221db962561d528ec8f910cf9affeca95a6a9101c3ecd53953fe126a2d780acf9d49304e4bc5499d2a9219171786048c5f1b1e19d27d55ed28f8d") + ), + ) + ) + val paymentInfo = OfferTypes.PaymentInfo(100.msat, 1000, CltvExpiryDelta(144), 1.msat, 500_000_000.msat, Features.empty) + val blindedPaths = listOf(Bolt12Invoice.Companion.PaymentBlindedContactInfo(OfferTypes.ContactInfo.BlindedPath(blindedPath), paymentInfo)) + val features = Features(Feature.TrampolinePayment to FeatureSupport.Optional, Feature.BasicMultiPartPayment to FeatureSupport.Optional) + val expected = PaymentOnion.RelayToBlindedPayload( + TlvStream( + OnionPaymentPayloadTlv.AmountToForward(150_000_000.msat), + OnionPaymentPayloadTlv.OutgoingCltv(CltvExpiry(850_000)), + OnionPaymentPayloadTlv.RecipientFeatures(features.toByteArray().toByteVector()), + OnionPaymentPayloadTlv.OutgoingBlindedPaths(blindedPaths) + ) + ) + val bin = Hex.decode( + "fd0143 020408f0d180 04030cf850 15080200000000020000 16fd012a032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e66868099102988face71e92c345a068f740191fd8e53be14f0bb957ef730d3c5f76087b960e020295d40514096a8be54859e7dfe947b376eaafea8afe5cb4eb2c13ff857ed0b4be002a0ae636dc5963bcfe3a2f055c8b8f6d2c5cd818362032404a6e35995ba57e101eac7e5fa04bb33a8920f1020e2dbadcc2005e859819ddebbe88a834ae8a6d2b049233c07335f15cd1dc5f22005bbc211f6ccd409888ca8ab027a5f21d229f9e18ff02cc1161a8344c91bdefc742d221db962561d528ec8f910cf9affeca95a6a9101c3ecd53953fe126a2d780acf9d49304e4bc5499d2a9219171786048c5f1b1e19d27d55ed28f8d00000064000003e800900000000000000001000000001dcd65000000" + ) + + val decoded = PaymentOnion.RelayToBlindedPayload.read(bin).right!! + assertEquals(decoded, expected) + assertEquals(decoded.amountToForward, 150_000_000.msat) + assertEquals(decoded.outgoingCltv, CltvExpiry(850_000)) + assertEquals(decoded.recipientFeatures, features.toByteArray().byteVector()) + assertEquals(decoded.outgoingBlindedPaths, blindedPaths) + + val encoded = expected.write() + assertContentEquals(bin, encoded) + } + @Test fun `encode - decode node relay to legacy per-hop payload`() { val nodeId = PublicKey(Hex.decode("02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619")) @@ -89,13 +148,13 @@ class PaymentOnionTestsCommon : LightningTestSuite() { OnionPaymentPayloadTlv.AmountToForward(561.msat), OnionPaymentPayloadTlv.OutgoingCltv(CltvExpiry(42)), OnionPaymentPayloadTlv.PaymentData(ByteVector32("eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619"), 1105.msat), - OnionPaymentPayloadTlv.InvoiceFeatures(features), + OnionPaymentPayloadTlv.RecipientFeatures(features), OnionPaymentPayloadTlv.OutgoingNodeId(nodeId), OnionPaymentPayloadTlv.InvoiceRoutingInfo(routingHints) ) ) val bin = Hex.decode( - "f6 02020231 04012a 0822eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f2836866190451 0e2102eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619 fe00010231010a fe000102339b01036d6caac248af96f6afa7f904f550253a0f3ef3f5aa2fe6838a95b216691468e200000000000000010000000a00000064009002025f7117a78150fe2ef97db7cfc83bd57b2e2c0d0dd25eaf467a4a1c2a45ce148600000000000000020000001400000096000c02a051267759c3a149e3e72372f4e0c4054ba597ebfd0eda78a2273023667205ee00000000000000030000001e000000c80018" + "f2 02020231 04012a 0822eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f2836866190451 0e2102eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619 15010a fe000102339b01036d6caac248af96f6afa7f904f550253a0f3ef3f5aa2fe6838a95b216691468e200000000000000010000000a00000064009002025f7117a78150fe2ef97db7cfc83bd57b2e2c0d0dd25eaf467a4a1c2a45ce148600000000000000020000001400000096000c02a051267759c3a149e3e72372f4e0c4054ba597ebfd0eda78a2273023667205ee00000000000000030000001e000000c80018" ) val decoded = PaymentOnion.RelayToNonTrampolinePayload.read(bin).right!! @@ -213,6 +272,7 @@ class PaymentOnionTestsCommon : LightningTestSuite() { // @formatter:off TlvStream(OnionPaymentPayloadTlv.AmountToForward(561.msat), OnionPaymentPayloadTlv.OutgoingCltv(CltvExpiry(1234567)), OnionPaymentPayloadTlv.EncryptedRecipientData(ByteVector("deadbeef")), OnionPaymentPayloadTlv.TotalAmount(1105.msat)) to Hex.decode("13 02020231 040312d687 0a04deadbeef 12020451"), TlvStream(OnionPaymentPayloadTlv.AmountToForward(561.msat), OnionPaymentPayloadTlv.OutgoingCltv(CltvExpiry(1234567)), OnionPaymentPayloadTlv.EncryptedRecipientData(ByteVector("deadbeef")), OnionPaymentPayloadTlv.BlindingPoint(PublicKey.fromHex("036d6caac248af96f6afa7f904f550253a0f3ef3f5aa2fe6838a95b216691468e2")), OnionPaymentPayloadTlv.TotalAmount(1105.msat)) to Hex.decode("36 02020231 040312d687 0a04deadbeef 0c21036d6caac248af96f6afa7f904f550253a0f3ef3f5aa2fe6838a95b216691468e2 12020451"), + TlvStream(OnionPaymentPayloadTlv.AmountToForward(150_000_000.msat), OnionPaymentPayloadTlv.OutgoingCltv(CltvExpiry(850_000)), OnionPaymentPayloadTlv.EncryptedRecipientData(ByteVector("bc211f6ccd409888ca8ab027a5f21d229f9e18ff02cc1161a8344c91bdefc742d221db962561d528ec8f910cf9affeca95a6a9101c3ecd53953fe126a2d780acf9d49304e4bc5499d2a9219171786048c5f1b1e19d27d55ed28f8d")), OnionPaymentPayloadTlv.TotalAmount(150_000_000.msat)) to Hex.decode("6e 020408f0d180 04030cf850 0a5bbc211f6ccd409888ca8ab027a5f21d229f9e18ff02cc1161a8344c91bdefc742d221db962561d528ec8f910cf9affeca95a6a9101c3ecd53953fe126a2d780acf9d49304e4bc5499d2a9219171786048c5f1b1e19d27d55ed28f8d 120408f0d180"), // @formatter:on ) testCases.forEach {