From 3913e6be2209583b331445ca7ec30a7099aa9659 Mon Sep 17 00:00:00 2001 From: Thomas HUET <81159533+thomash-acinq@users.noreply.github.com> Date: Mon, 15 Jul 2024 14:40:58 +0200 Subject: [PATCH] Prune route hints and blinded paths in trampoline onion (#686) When using trampoline, the route hints or blinded paths may not all fit inside the onion. If that's the case, we prune some of them until it fits. --- .../acinq/lightning/crypto/sphinx/Sphinx.kt | 2 +- .../payment/OutgoingPaymentPacket.kt | 29 ++++++++++++++++--- .../payment/PaymentPacketTestsCommon.kt | 10 +++++++ 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/crypto/sphinx/Sphinx.kt b/src/commonMain/kotlin/fr/acinq/lightning/crypto/sphinx/Sphinx.kt index 4f0d76398..f22ab194e 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/crypto/sphinx/Sphinx.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/crypto/sphinx/Sphinx.kt @@ -214,7 +214,7 @@ object Sphinx { is Either.Right -> packet.value.payload.size() } - require(payload.size <= packetLength - MacLength) { "packet payload cannot exceed ${packetLength - MacLength} bytes" } + require(payload.size <= packetLength - MacLength) { "packet payload cannot exceed ${packetLength - MacLength} bytes, is ${payload.size} bytes" } val (currentMac, currentPayload) = when (packet) { // Packet construction starts with an empty mac and random payload. diff --git a/src/commonMain/kotlin/fr/acinq/lightning/payment/OutgoingPaymentPacket.kt b/src/commonMain/kotlin/fr/acinq/lightning/payment/OutgoingPaymentPacket.kt index 395be443a..7d0a1fc0d 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/payment/OutgoingPaymentPacket.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/payment/OutgoingPaymentPacket.kt @@ -18,6 +18,7 @@ import fr.acinq.lightning.router.Hop import fr.acinq.lightning.router.NodeHop import fr.acinq.lightning.utils.UUID import fr.acinq.lightning.wire.FailureMessage +import fr.acinq.lightning.wire.OnionPaymentPayloadTlv import fr.acinq.lightning.wire.OnionRoutingPacket import fr.acinq.lightning.wire.PaymentOnion @@ -72,7 +73,7 @@ object OutgoingPaymentPacket { // NB: the final payload will never reach the recipient, since the next-to-last trampoline hop will convert that to a legacy payment // We use the smallest final payload possible, otherwise we may overflow the trampoline onion size. val dummyFinalPayload = PaymentOnion.FinalPayload.Standard.createSinglePartPayload(finalPayload.amount, finalPayload.expiry, finalPayload.paymentSecret, null) - val (firstAmount, firstExpiry, payloads) = hops.drop(1).reversed().fold(Triple(finalPayload.amount, finalPayload.expiry, listOf(dummyFinalPayload))) { triple, hop -> + val (firstAmount, firstExpiry, initialPayloads) = hops.drop(1).reversed().fold(Triple(finalPayload.amount, finalPayload.expiry, listOf(dummyFinalPayload))) { triple, hop -> val (amount, expiry, payloads) = triple val payload = when (payloads.size) { // The next-to-last trampoline hop must include invoice data to indicate the conversion to a legacy payment. @@ -81,8 +82,20 @@ object OutgoingPaymentPacket { } Triple(amount + hop.fee(amount), expiry + hop.cltvExpiryDelta, listOf(payload) + payloads) } + var payloads = initialPayloads val nodes = hops.map { it.nextNodeId } - val onion = buildOnion(nodes, payloads, invoice.paymentHash, payloadLength = null) + var onion = buildOnion(nodes, payloads, invoice.paymentHash, payloadLength = null) + // Ensure that this onion can fit inside the outer 1300 bytes onion. The outer onion fields need ~150 bytes and we add some safety margin. + while (onion.packet.payload.size() > 1000) { + payloads = payloads.map { payload -> when (payload) { + is PaymentOnion.RelayToNonTrampolinePayload -> payload.copy(records = payload.records.copy(records = payload.records.records.map { when (it) { + is OnionPaymentPayloadTlv.InvoiceRoutingInfo -> OnionPaymentPayloadTlv.InvoiceRoutingInfo(it.extraHops.dropLast(1)) + else -> it + } }.toSet())) + else -> payload + } } + onion = buildOnion(nodes, payloads, invoice.paymentHash, payloadLength = null) + } return Triple(firstAmount, firstExpiry, onion) } @@ -97,8 +110,16 @@ object OutgoingPaymentPacket { * @param finalExpiry cltv expiry that should be received by the final recipient. */ fun buildTrampolineToNonTrampolinePacket(invoice: Bolt12Invoice, hop: NodeHop, finalAmount: MilliSatoshi, finalExpiry: CltvExpiry): Triple { - val payload = PaymentOnion.RelayToBlindedPayload.create(finalAmount, finalExpiry, invoice) - val onion = buildOnion(listOf(hop.nodeId), listOf(payload), invoice.paymentHash, payloadLength = null) + var payload = PaymentOnion.RelayToBlindedPayload.create(finalAmount, finalExpiry, invoice) + var onion = buildOnion(listOf(hop.nodeId), listOf(payload), invoice.paymentHash, payloadLength = null) + // Ensure that this onion can fit inside the outer 1300 bytes onion. The outer onion fields need ~150 bytes and we add some safety margin. + while (onion.packet.payload.size() > 1000) { + payload = payload.copy(records = payload.records.copy(records = payload.records.records.map { when (it) { + is OnionPaymentPayloadTlv.OutgoingBlindedPaths -> OnionPaymentPayloadTlv.OutgoingBlindedPaths(it.paths.dropLast(1)) + else -> it + } }.toSet())) + onion = buildOnion(listOf(hop.nodeId), listOf(payload), invoice.paymentHash, payloadLength = null) + } return Triple(finalAmount + hop.fee(finalAmount), finalExpiry + hop.cltvExpiryDelta, onion) } diff --git a/src/commonTest/kotlin/fr/acinq/lightning/payment/PaymentPacketTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/payment/PaymentPacketTestsCommon.kt index d00dd7fbc..c780d31f5 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/payment/PaymentPacketTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/payment/PaymentPacketTestsCommon.kt @@ -12,6 +12,7 @@ import fr.acinq.lightning.channel.states.Channel import fr.acinq.lightning.crypto.RouteBlinding import fr.acinq.lightning.crypto.sphinx.Sphinx import fr.acinq.lightning.crypto.sphinx.Sphinx.hash +import fr.acinq.lightning.payment.OutgoingPaymentHandler.PaymentAttempt import fr.acinq.lightning.router.ChannelHop import fr.acinq.lightning.router.NodeHop import fr.acinq.lightning.tests.utils.LightningTestSuite @@ -657,5 +658,14 @@ class PaymentPacketTestsCommon : LightningTestSuite() { val failure = IncomingPaymentPacket.decrypt(UpdateAddHtlc(randomBytes32(), 4, amountE, paymentHash, expiryE, onionE.packet), privE) assertEquals(Either.Left(FinalIncorrectCltvExpiry(invalidExpiry)), failure) } + + @Test + fun `relay to blinded with many large blinded routes`() { + val invoice = Bolt12Invoice.fromString("lni1qqs0sehhttf0swv6sxsxuefk9q23yj9h0cl4wyn324jlt2gll4fzk0syzquta4dy4m9jgp5s970w2lw8928ppnqz662mq8r6dyy7w9kgv0ann0zlk7aacwpykl7uu5adckf7t0sgpees93j6nwr4xle4ck4d2wup6j044fx6f5zkdacvjen3herm493en2lmqgprfesng92cpl68qvlxv96prguvtl7yv49nwq9a92zl5n2pv73u4wgqx08jupl9mcphw7ww5mv50j7xkcjxvjamrysy3wsfmgj4cu9aq4y6hszdk5rys4qwa4muxczk4nuj54gyw3jqygaqqk2edh9c90ergwvaxu93jynty9z0gzrpcwuhkzt05epatv45qqg93d3x5hx8jgwgkwpnz85y3narw93pqvmp27mzgwjaswd76lgxm88qw2ehqszd2mlgntvnwq0vzrk4ehq325pqdl3gcz4k7xeh9sdx5fr2uclhf7f3aqm9u9dq38rg6cvsqqqqqqq9yqjwyp2qxqsqqpvzzqnfhw87mvwxx0qrljmlxppxr7h4hmdgdzpsxnj0rw2jckjtc93g44v3gmrfva58gmnfdenjq6tnyp3k7mmvypq5dg8aqsqsxhj07sv0ez642nzanm4xvwtvyfaag2dry5wge0r3zqpt5g2mls3xqwsadj5tn7gm50l43xmsu2dszx8s672wrkqn48qp7ftw6kvhltlrkqsrwc9agnmlg8dzefxe4vlz9euhhwjzeflzrrkkurluwhzfjqf62trsq2dm4984vptwe3zpea0e3n27w75pyt2ym8g734awf99ep759a4xdf29epp5z3e303e08q2e6qdycnlzdkv4c80u2pr7ql2ndpa9zfafwyqkf9r9cqtaza35txq9rppm56ajy97nema0j8vtuld097u0hjhh5cmwhlp8du95va7wvq2emxkvafy2wg8ghl7vpkw2w0w0pp2tr6gqqmnvxy9u9jg9rkwn2flxn9d3vd9x0g94wdhgap22l780d0t9zywpj7yu949347hsag0s6pgpkuaf2pg8xqzjk336e8azw8672qtvvljye6gtevmnrygav24jvsxmypf25rqs8ersr9wfsqhdxjl6lmy3lqac4q96rjn02vtk4n6zcaqzr5qhlqlywrga7lmwt99gyj23ndzrl2y8380nxydqntsh7gpajau2wrvpshzpryq37n72ujf26djermgcf47wfsfdts3p97u0lnt6szck8zlqzqfz6ex8p9xtugj6jtuyf3flkqk9ahu8e6dxgvp49xl24x6hdmljcjqptk8erxq4uwnkv4g32p4d8un0f9az5ft59mwvgtf3xxm3xepx68x6vk46pf5xh4lyr35m32q5ukd5xj4kj7vka67uggjd4saahn3dswdwsqe8epg2pvele4cz0zvq288t94n69cj87ewal7yqvvpq6tj6mpjw2d36uhlf2khtjlpdfv672gvkszdpqht37shtf0ytyfnl7xkeudm2xscvpack3whk29hsfr36cv95ddzc2klk2yc93s09y2uh2xz5k5jap4550z2vd7gy9faxd2gcr4xjmke8yhn0wdsqxpe78qyqy0elprd0swe9yxc82as23vm5fyumc7nv470xfsjy3zjg4zfvpa6kv3ndl0w3ukc6627dua9g7pmzfupesqvpjfw3rjt39hlmk40gtralyk5a4lq42207acdqekpgmdjqpmw0zy3cz8d3cpvw4a28lqnf3etu8adl43zra0zk5m094h86g4yfm8cxnz7zqyqa5q4dz6ceqp0jeuakvyx4653h0kjllhhpfduquh4ch8rtu9lf20gqznhxe60n3m45k80vcgnvut8cfgpvy37ae93rpah9sa4ddyzyt9gu0uj5zny00vrkkxycrxm8wk3gzyrn3q8caxrqwa48fwqzlagph95y5tw555px0s97y4lgspgedkg4q9gxgv4qyc7x8433j0a5fnntdjhlsmx4wje53dj8y4c3757yt87kenvej8ugwkg05flugshd906tvtqcldrcgxfqqw8ecz926ft3ehdahpjhueag8znklv6qvz7w3htszz5k3we0dd8r2grxpz5edgtuh28u0c5ugrf8autgu3crkjt58ev9gcvnetn5kjwk4wehr8yq7lulsug7dhyg0reyqlhhsxs8ueghtm02g7jd2p9ytwstsacry3y8mv202y4qqqqqqqqqqqq9qq6qqqqqqqqqqqqqsqqqqqf2q5htpqqqqqqqraqqqqqqpqp5qqqqqqqqqqqqpqqqqqqqqh6uzcqqqqqqqqqqqqqqeqqrgqqqqqqqqqqqqzqqqqqqjnems5qqqpfqyv6g47m4gyqw3qa7penn7yv39ufk8j0mpj0ldfyu9qd7j2vcgt27rqe9l5ydpm2szfcs2uqczqqqtqggrxc2hkcjr5hvrn0kh6pkeecrjkdcyqn2kl6y6mymsrmqsa4wdcy2lqsxw9n37kdz8zq0ytckjvcy7jcwqrqj5rgd56q4e3g76k0lja96hzkdda7z5sxqwemrdjje2rqm7jcv6ll39dut0mqjvrl4skedkfz5mu").get() + val (trampolineAmount, trampolineExpiry, trampolineOnion) = OutgoingPaymentPacket.buildTrampolineToNonTrampolinePacket(invoice, NodeHop(randomKey().publicKey(), randomKey().publicKey(), CltvExpiryDelta(444), 0.msat), finalAmount, finalExpiry) + val trampolinePayload = PaymentAttempt.TrampolinePayload(trampolineAmount, trampolineExpiry, randomBytes32(), trampolineOnion.packet) + val channelHops: List = listOf(ChannelHop(randomKey().publicKey(), randomKey().publicKey(), defaultChannelUpdate)) + OutgoingPaymentPacket.buildCommand(UUID.randomUUID(), randomBytes32(), channelHops, trampolinePayload.createFinalPayload(finalAmount)) + } }