Skip to content

Commit a41ec40

Browse files
authored
Handle feature-less BOLT 11 invoices (#687)
We were not handling correctly feature-less invoices, which are spec-compliant. We do reject invoices that don't have `var_onion_optin`, but the check happens later. Instead, we currently throw a `NullPointerException`. The way we are checking requirements in the `init` block requires lazy-init properties, otherwise the object initialization will fail before any logic can happen. Also, as per the spec, the feature tag must be skipped at serialization if there are no features enabled.
1 parent 3913e6b commit a41ec40

File tree

2 files changed

+33
-21
lines changed

2 files changed

+33
-21
lines changed

src/commonMain/kotlin/fr/acinq/lightning/payment/Bolt11Invoice.kt

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -21,25 +21,25 @@ data class Bolt11Invoice(
2121
val tags: List<TaggedField>,
2222
val signature: ByteVector
2323
) : PaymentRequest() {
24-
val chain: Chain? = prefixes.entries.firstOrNull { it.value == prefix }?.key
24+
val chain: Chain? get() = prefixes.entries.firstOrNull { it.value == prefix }?.key
2525

26-
override val paymentHash: ByteVector32 = tags.find { it is TaggedField.PaymentHash }!!.run { (this as TaggedField.PaymentHash).hash }
26+
override val paymentHash: ByteVector32 get() = tags.find { it is TaggedField.PaymentHash }!!.run { (this as TaggedField.PaymentHash).hash }
2727

28-
val paymentSecret: ByteVector32 = tags.find { it is TaggedField.PaymentSecret }!!.run { (this as TaggedField.PaymentSecret).secret }
28+
val paymentSecret: ByteVector32 get() = tags.find { it is TaggedField.PaymentSecret }!!.run { (this as TaggedField.PaymentSecret).secret }
2929

30-
val paymentMetadata: ByteVector? = tags.find { it is TaggedField.PaymentMetadata }?.run { (this as TaggedField.PaymentMetadata).data }
30+
val paymentMetadata: ByteVector? get() = tags.find { it is TaggedField.PaymentMetadata }?.run { (this as TaggedField.PaymentMetadata).data }
3131

32-
val description: String? = tags.find { it is TaggedField.Description }?.run { (this as TaggedField.Description).description }
32+
val description: String? get() = tags.find { it is TaggedField.Description }?.run { (this as TaggedField.Description).description }
3333

34-
val descriptionHash: ByteVector32? = tags.find { it is TaggedField.DescriptionHash }?.run { (this as TaggedField.DescriptionHash).hash }
34+
val descriptionHash: ByteVector32? get() = tags.find { it is TaggedField.DescriptionHash }?.run { (this as TaggedField.DescriptionHash).hash }
3535

36-
val expirySeconds: Long? = tags.find { it is TaggedField.Expiry }?.run { (this as TaggedField.Expiry).expirySeconds }
36+
val expirySeconds: Long? get() = tags.find { it is TaggedField.Expiry }?.run { (this as TaggedField.Expiry).expirySeconds }
3737

38-
val minFinalExpiryDelta: CltvExpiryDelta? = tags.find { it is TaggedField.MinFinalCltvExpiry }?.run { CltvExpiryDelta((this as TaggedField.MinFinalCltvExpiry).cltvExpiry.toInt()) }
38+
val minFinalExpiryDelta: CltvExpiryDelta? get() = tags.find { it is TaggedField.MinFinalCltvExpiry }?.run { CltvExpiryDelta((this as TaggedField.MinFinalCltvExpiry).cltvExpiry.toInt()) }
3939

4040
val fallbackAddress: String? = tags.find { it is TaggedField.FallbackAddress }?.run { (this as TaggedField.FallbackAddress).toAddress(prefix) }
4141

42-
override val features: Features = tags.find { it is TaggedField.Features }.run { Features((this as TaggedField.Features).bits) }
42+
override val features: Features get() = tags.filterIsInstance<TaggedField.Features>().firstOrNull()?.run { Features(this.bits) } ?: Features.empty
4343

4444
val routingInfo: List<TaggedField.RoutingInfo> = tags.filterIsInstance<TaggedField.RoutingInfo>()
4545

@@ -55,7 +55,7 @@ data class Bolt11Invoice(
5555
require(description != null || descriptionHash != null) { "there must be exactly one description tag or one description hash tag" }
5656
}
5757

58-
override fun isExpired(currentTimestampSeconds: Long): Boolean = when (expirySeconds) {
58+
override fun isExpired(currentTimestampSeconds: Long): Boolean = when (val expirySeconds = expirySeconds) {
5959
null -> timestampSeconds + DEFAULT_EXPIRY_SECONDS <= currentTimestampSeconds
6060
else -> timestampSeconds + expirySeconds <= currentTimestampSeconds
6161
}
@@ -65,14 +65,16 @@ data class Bolt11Invoice(
6565
private fun rawData(): List<Int5> {
6666
val data5 = ArrayList<Int5>()
6767
data5.addAll(encodeTimestamp(timestampSeconds))
68-
tags.forEach {
69-
val encoded = it.encode()
70-
val len = encoded.size
71-
data5.add(it.tag)
72-
data5.add((len / 32).toByte())
73-
data5.add((len.rem(32)).toByte())
74-
data5.addAll(encoded)
75-
}
68+
tags
69+
.filterNot { it is TaggedField.Features && it.bits.isEmpty() }
70+
.forEach {
71+
val encoded = it.encode()
72+
val len = encoded.size
73+
data5.add(it.tag)
74+
data5.add((len / 32).toByte())
75+
data5.add((len.rem(32)).toByte())
76+
data5.addAll(encoded)
77+
}
7678
return data5
7779
}
7880

@@ -266,7 +268,7 @@ data class Bolt11Invoice(
266268
// converts a list of 5 bits values to a byte array
267269
internal fun toByteArray(int5s: List<Int5>): ByteArray {
268270
val allbits = int5s.flatMap { toBits(it) }
269-
return allbits.windowed(8, 8, partialWindows = true){ toByte(it) }.toByteArray()
271+
return allbits.windowed(8, 8, partialWindows = true) { toByte(it) }.toByteArray()
270272
}
271273
}
272274

src/commonTest/kotlin/fr/acinq/lightning/payment/Bolt11InvoiceTestsCommon.kt

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package fr.acinq.lightning.payment
22

33
import fr.acinq.bitcoin.*
44
import fr.acinq.bitcoin.utils.Either
5+
import fr.acinq.bitcoin.utils.Try
56
import fr.acinq.lightning.*
67
import fr.acinq.lightning.Lightning.randomBytes32
78
import fr.acinq.lightning.Lightning.randomKey
@@ -498,10 +499,12 @@ class Bolt11InvoiceTestsCommon : LightningTestSuite() {
498499
assertEquals(pr1.paymentSecret, pr.paymentSecret)
499500

500501
// An invoice without the payment secret feature should be rejected
501-
assertTrue(Bolt11Invoice.read("lnbc40n1pw9qjvwpp5qq3w2ln6krepcslqszkrsfzwy49y0407hvks30ec6pu9s07jur3sdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdencqzysxqrrss7ju0s4dwx6w8a95a9p2xc5vudl09gjl0w2n02sjrvffde632nxwh2l4w35nqepj4j5njhh4z65wyfc724yj6dn9wajvajfn5j7em6wsq2elakl").isFailure)
502+
assertIs<Try.Failure<Throwable>>((Bolt11Invoice.read("lnbc40n1pw9qjvwpp5qq3w2ln6krepcslqszkrsfzwy49y0407hvks30ec6pu9s07jur3sdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdencqzysxqrrss7ju0s4dwx6w8a95a9p2xc5vudl09gjl0w2n02sjrvffde632nxwh2l4w35nqepj4j5njhh4z65wyfc724yj6dn9wajvajfn5j7em6wsq2elakl")))
503+
.let { failure -> assertContains(failure.error.message ?: "", "var_onion_optin must be supported") }
502504

503505
// An invoice that sets the payment secret feature bit must provide a payment secret.
504-
assertTrue(Bolt11Invoice.read("lnbc1230p1pwljzn3pp5qyqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqdq52dhk6efqd9h8vmmfvdjs9qypqsqylvwhf7xlpy6xpecsnpcjjuuslmzzgeyv90mh7k7vs88k2dkxgrkt75qyfjv5ckygw206re7spga5zfd4agtdvtktxh5pkjzhn9dq2cqz9upw7").isFailure)
506+
assertIs<Try.Failure<Throwable>>(Bolt11Invoice.read("lnbc1230p1pwljzn3pp5qyqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqdq52dhk6efqd9h8vmmfvdjs9qypqsqylvwhf7xlpy6xpecsnpcjjuuslmzzgeyv90mh7k7vs88k2dkxgrkt75qyfjv5ckygw206re7spga5zfd4agtdvtktxh5pkjzhn9dq2cqz9upw7"))
507+
.let { failure -> assertContains(failure.error.message ?: "", "there must be exactly one payment secret tag") }
505508

506509
// Invoices must use a payment secret.
507510
assertFails {
@@ -517,6 +520,13 @@ class Bolt11InvoiceTestsCommon : LightningTestSuite() {
517520
}
518521
}
519522

523+
@Test
524+
fun `decode invoice without features`() {
525+
val s = "lnbc10n1pnglwsfpp5fdcjk5vudhe2jzuz0q65dkh4gfmxnn6vexlchc6ta9f25wynp2qshp59warmg27z4nkuvhs5x3vdv998jck5ue8nge2t68dtfvm27n8kvsqxqrrssnp4qf3rsvu5xdrxnv2kgkr4hvpefx257fjw8ugupnug4ls6rf2d5w6yc2rnnz0zuymgjl3p4dvyh8dr4mp969gjrnaggx50nv5ax7wy6yflyr07ek5hdevxtz9angp3jfyfz9ram8d7gw9pr0csr6fpa8rfeu7gpgesr58"
526+
val failure = assertIs<Try.Failure<Throwable>>(Bolt11Invoice.read(s))
527+
assertContains(failure.error.message ?: "", "var_onion_optin must be supported")
528+
}
529+
520530
@Test
521531
fun `invoice with descriptionHash`() {
522532
val descriptionHash = randomBytes32()

0 commit comments

Comments
 (0)