1
1
package fr.acinq.lightning.payment
2
2
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.*
7
4
import fr.acinq.bitcoin.utils.Either
8
5
import fr.acinq.bitcoin.utils.flatMap
9
6
import fr.acinq.lightning.CltvExpiry
@@ -14,6 +11,7 @@ import fr.acinq.lightning.crypto.sphinx.Sphinx
14
11
import fr.acinq.lightning.crypto.sphinx.Sphinx.hash
15
12
import fr.acinq.lightning.utils.msat
16
13
import fr.acinq.lightning.wire.*
14
+ import io.ktor.utils.io.core.*
17
15
18
16
object IncomingPaymentPacket {
19
17
@@ -44,7 +42,7 @@ object IncomingPaymentPacket {
44
42
val onion = add.fold({ it.finalPacket }, { it.onionRoutingPacket })
45
43
return decryptOnion(paymentHash, onion, privateKey, blinding).flatMap { outer ->
46
44
when (outer) {
47
- is PaymentOnion .FinalPayload .Standard ->
45
+ is PaymentOnion .FinalPayload .Standard -> {
48
46
when (val trampolineOnion = outer.records.get<OnionPaymentPayloadTlv .TrampolineOnion >()) {
49
47
null -> validate(htlcAmount, htlcExpiry, outer)
50
48
else -> {
@@ -54,25 +52,49 @@ object IncomingPaymentPacket {
54
52
is PaymentOnion .FinalPayload .Standard -> validate(htlcAmount, htlcExpiry, outer, innerPayload)
55
53
// Blinded trampoline paths are not supported.
56
54
is PaymentOnion .FinalPayload .Blinded -> Either .Left (InvalidOnionPayload (0 , 0 ))
55
+ is PaymentOnion .FinalPayload .TrampolineBlinded -> Either .Left (InvalidOnionPayload (0 , 0 ))
57
56
}
58
57
}
59
58
}
60
59
}
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 ))
62
84
}
63
85
}
64
86
}
65
87
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 > {
67
89
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 ->
69
91
when {
70
92
! decrypted.isLastPacket -> Either .Left (UnknownNextPeer )
71
93
else -> PaymentOnion .PerHopPayload .read(decrypted.payload.toByteArray()).flatMap { tlvs ->
72
94
when (val encryptedRecipientData = tlvs.get<OnionPaymentPayloadTlv .EncryptedRecipientData >()?.data) {
73
95
null -> when {
74
- blinding != null -> Either .Left (InvalidOnionBlinding (hash(packet)))
75
96
tlvs.get<OnionPaymentPayloadTlv .BlindingPoint >() != null -> Either .Left (InvalidOnionBlinding (hash(packet)))
97
+ blinding != null -> PaymentOnion .FinalPayload .TrampolineBlinded .read(decrypted.payload)
76
98
else -> PaymentOnion .FinalPayload .Standard .read(decrypted.payload)
77
99
}
78
100
else -> when {
@@ -97,6 +119,16 @@ object IncomingPaymentPacket {
97
119
.flatMap { blindedTlvs -> PaymentOnion .FinalPayload .Blinded .validate(tlvs, blindedTlvs) }
98
120
}
99
121
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
+
100
132
private fun validate (htlcAmount : MilliSatoshi , htlcExpiry : CltvExpiry , payload : PaymentOnion .FinalPayload .Standard ): Either <FailureMessage , PaymentOnion .FinalPayload > {
101
133
return when {
102
134
htlcAmount < payload.amount -> Either .Left (FinalIncorrectHtlcAmount (htlcAmount))
@@ -105,15 +137,23 @@ object IncomingPaymentPacket {
105
137
}
106
138
}
107
139
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()
109
149
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)))
112
152
// 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 )
117
157
}
118
158
}
119
159
0 commit comments