11package fr.acinq.lightning.payment
22
3- import fr.acinq.bitcoin.ByteVector
4- import fr.acinq.bitcoin.ByteVector32
5- import fr.acinq.bitcoin.PrivateKey
6- import fr.acinq.bitcoin.PublicKey
3+ import fr.acinq.bitcoin.*
74import fr.acinq.bitcoin.utils.Either
85import fr.acinq.bitcoin.utils.flatMap
96import fr.acinq.lightning.CltvExpiry
@@ -14,6 +11,7 @@ import fr.acinq.lightning.crypto.sphinx.Sphinx
1411import fr.acinq.lightning.crypto.sphinx.Sphinx.hash
1512import fr.acinq.lightning.utils.msat
1613import fr.acinq.lightning.wire.*
14+ import io.ktor.utils.io.core.*
1715
1816object IncomingPaymentPacket {
1917
@@ -44,7 +42,7 @@ object IncomingPaymentPacket {
4442 val onion = add.fold({ it.finalPacket }, { it.onionRoutingPacket })
4543 return decryptOnion(paymentHash, onion, privateKey, blinding).flatMap { outer ->
4644 when (outer) {
47- is PaymentOnion .FinalPayload .Standard ->
45+ is PaymentOnion .FinalPayload .Standard -> {
4846 when (val trampolineOnion = outer.records.get<OnionPaymentPayloadTlv .TrampolineOnion >()) {
4947 null -> validate(htlcAmount, htlcExpiry, outer)
5048 else -> {
@@ -54,25 +52,49 @@ object IncomingPaymentPacket {
5452 is PaymentOnion .FinalPayload .Standard -> validate(htlcAmount, htlcExpiry, outer, innerPayload)
5553 // Blinded trampoline paths are not supported.
5654 is PaymentOnion .FinalPayload .Blinded -> Either .Left (InvalidOnionPayload (0 , 0 ))
55+ is PaymentOnion .FinalPayload .TrampolineBlinded -> Either .Left (InvalidOnionPayload (0 , 0 ))
5756 }
5857 }
5958 }
6059 }
61- is PaymentOnion .FinalPayload .Blinded -> validate(htlcAmount, htlcExpiry, onion, outer)
60+ }
61+ is PaymentOnion .FinalPayload .Blinded -> {
62+ when (val trampolineOnion = outer.records.get<OnionPaymentPayloadTlv .TrampolineOnion >()) {
63+ null -> validate(htlcAmount, htlcExpiry, onion, outer, innerPayload = null )
64+ else -> {
65+ val associatedData = when (val metadata = OfferPaymentMetadata .fromPathId(privateKey.publicKey(), outer.pathId)) {
66+ null -> paymentHash
67+ else -> {
68+ val onionDecryptionKey = blinding?.let { RouteBlinding .derivePrivateKey(privateKey, it) } ? : privateKey
69+ blindedTrampolineAssociatedData(paymentHash, onionDecryptionKey, metadata.payerKey)
70+ }
71+ }
72+ when (val inner = decryptOnion(associatedData, trampolineOnion.packet, privateKey, blinding)) {
73+ is Either .Left -> Either .Left (inner.value)
74+ is Either .Right -> when (val innerPayload = inner.value) {
75+ is PaymentOnion .FinalPayload .TrampolineBlinded -> validate(htlcAmount, htlcExpiry, onion, outer, innerPayload)
76+ is PaymentOnion .FinalPayload .Blinded -> Either .Left (InvalidOnionPayload (0 , 0 ))
77+ is PaymentOnion .FinalPayload .Standard -> Either .Left (InvalidOnionPayload (0 , 0 ))
78+ }
79+ }
80+ }
81+ }
82+ }
83+ is PaymentOnion .FinalPayload .TrampolineBlinded -> Either .Left (InvalidOnionPayload (OnionPaymentPayloadTlv .PaymentData .tag, 0 ))
6284 }
6385 }
6486 }
6587
66- private fun decryptOnion (paymentHash : ByteVector32 , packet : OnionRoutingPacket , privateKey : PrivateKey , blinding : PublicKey ? ): Either <FailureMessage , PaymentOnion .FinalPayload > {
88+ private fun decryptOnion (associatedData : ByteVector32 , packet : OnionRoutingPacket , privateKey : PrivateKey , blinding : PublicKey ? ): Either <FailureMessage , PaymentOnion .FinalPayload > {
6789 val onionDecryptionKey = blinding?.let { RouteBlinding .derivePrivateKey(privateKey, it) } ? : privateKey
68- return Sphinx .peel(onionDecryptionKey, paymentHash , packet).flatMap { decrypted ->
90+ return Sphinx .peel(onionDecryptionKey, associatedData , packet).flatMap { decrypted ->
6991 when {
7092 ! decrypted.isLastPacket -> Either .Left (UnknownNextPeer )
7193 else -> PaymentOnion .PerHopPayload .read(decrypted.payload.toByteArray()).flatMap { tlvs ->
7294 when (val encryptedRecipientData = tlvs.get<OnionPaymentPayloadTlv .EncryptedRecipientData >()?.data) {
7395 null -> when {
74- blinding != null -> Either .Left (InvalidOnionBlinding (hash(packet)))
7596 tlvs.get<OnionPaymentPayloadTlv .BlindingPoint >() != null -> Either .Left (InvalidOnionBlinding (hash(packet)))
97+ blinding != null -> PaymentOnion .FinalPayload .TrampolineBlinded .read(decrypted.payload)
7698 else -> PaymentOnion .FinalPayload .Standard .read(decrypted.payload)
7799 }
78100 else -> when {
@@ -97,6 +119,16 @@ object IncomingPaymentPacket {
97119 .flatMap { blindedTlvs -> PaymentOnion .FinalPayload .Blinded .validate(tlvs, blindedTlvs) }
98120 }
99121
122+ /* *
123+ * When we're using trampoline with Bolt 12, we expect the payer to include a trampoline payload.
124+ * However, the trampoline node could replace it with a trampoline onion they created.
125+ * To avoid that, we use a shared secret based on the [OfferTypes.InvoiceRequest] to authenticate the payload.
126+ */
127+ private fun blindedTrampolineAssociatedData (paymentHash : ByteVector32 , onionDecryptionKey : PrivateKey , payerId : PublicKey ): ByteVector32 {
128+ val invReqSharedSecret = (payerId * onionDecryptionKey).value.toByteArray()
129+ return Crypto .sha256(" blinded_trampoline_payment" .toByteArray() + paymentHash.toByteArray() + invReqSharedSecret).byteVector32()
130+ }
131+
100132 private fun validate (htlcAmount : MilliSatoshi , htlcExpiry : CltvExpiry , payload : PaymentOnion .FinalPayload .Standard ): Either <FailureMessage , PaymentOnion .FinalPayload > {
101133 return when {
102134 htlcAmount < payload.amount -> Either .Left (FinalIncorrectHtlcAmount (htlcAmount))
@@ -105,15 +137,23 @@ object IncomingPaymentPacket {
105137 }
106138 }
107139
108- private fun validate (htlcAmount : MilliSatoshi , htlcExpiry : CltvExpiry , onion : OnionRoutingPacket , payload : PaymentOnion .FinalPayload .Blinded ): Either <FailureMessage , PaymentOnion .FinalPayload > {
140+ private fun validate (
141+ htlcAmount : MilliSatoshi ,
142+ htlcExpiry : CltvExpiry ,
143+ onion : OnionRoutingPacket ,
144+ outerPayload : PaymentOnion .FinalPayload .Blinded ,
145+ innerPayload : PaymentOnion .FinalPayload .TrampolineBlinded ?
146+ ): Either <FailureMessage , PaymentOnion .FinalPayload > {
147+ val minAmount = listOfNotNull(outerPayload.amount, innerPayload?.amount).max()
148+ val minExpiry = listOfNotNull(outerPayload.expiry, innerPayload?.expiry).max()
109149 return when {
110- payload .recipientData.paymentConstraints?.let { htlcAmount < it.minAmount } == true -> Either .Left (InvalidOnionBlinding (hash(onion)))
111- payload .recipientData.paymentConstraints?.let { it.maxCltvExpiry < htlcExpiry } == true -> Either .Left (InvalidOnionBlinding (hash(onion)))
150+ outerPayload .recipientData.paymentConstraints?.let { htlcAmount < it.minAmount } == true -> Either .Left (InvalidOnionBlinding (hash(onion)))
151+ outerPayload .recipientData.paymentConstraints?.let { it.maxCltvExpiry < htlcExpiry } == true -> Either .Left (InvalidOnionBlinding (hash(onion)))
112152 // We currently don't set the allowed_features field in our invoices.
113- ! Features .areCompatible(Features .empty, payload .recipientData.allowedFeatures) -> Either .Left (InvalidOnionBlinding (hash(onion)))
114- htlcAmount < payload.amount -> Either .Left (InvalidOnionBlinding (hash(onion)))
115- htlcExpiry < payload.expiry -> Either .Left (InvalidOnionBlinding (hash(onion)))
116- else -> Either .Right (payload )
153+ ! Features .areCompatible(Features .empty, outerPayload .recipientData.allowedFeatures) -> Either .Left (InvalidOnionBlinding (hash(onion)))
154+ htlcAmount < minAmount -> Either .Left (InvalidOnionBlinding (hash(onion)))
155+ htlcExpiry < minExpiry -> Either .Left (InvalidOnionBlinding (hash(onion)))
156+ else -> Either .Right (outerPayload )
117157 }
118158 }
119159
0 commit comments