Skip to content

Commit

Permalink
Handle feature-less BOLT 11 invoices (#687)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
pm47 committed Jul 15, 2024
1 parent 3913e6b commit a41ec40
Show file tree
Hide file tree
Showing 2 changed files with 33 additions and 21 deletions.
40 changes: 21 additions & 19 deletions src/commonMain/kotlin/fr/acinq/lightning/payment/Bolt11Invoice.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,25 +21,25 @@ data class Bolt11Invoice(
val tags: List<TaggedField>,
val signature: ByteVector
) : PaymentRequest() {
val chain: Chain? = prefixes.entries.firstOrNull { it.value == prefix }?.key
val chain: Chain? get() = prefixes.entries.firstOrNull { it.value == prefix }?.key

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

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

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

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

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

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

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

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

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

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

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

override fun isExpired(currentTimestampSeconds: Long): Boolean = when (expirySeconds) {
override fun isExpired(currentTimestampSeconds: Long): Boolean = when (val expirySeconds = expirySeconds) {
null -> timestampSeconds + DEFAULT_EXPIRY_SECONDS <= currentTimestampSeconds
else -> timestampSeconds + expirySeconds <= currentTimestampSeconds
}
Expand All @@ -65,14 +65,16 @@ data class Bolt11Invoice(
private fun rawData(): List<Int5> {
val data5 = ArrayList<Int5>()
data5.addAll(encodeTimestamp(timestampSeconds))
tags.forEach {
val encoded = it.encode()
val len = encoded.size
data5.add(it.tag)
data5.add((len / 32).toByte())
data5.add((len.rem(32)).toByte())
data5.addAll(encoded)
}
tags
.filterNot { it is TaggedField.Features && it.bits.isEmpty() }
.forEach {
val encoded = it.encode()
val len = encoded.size
data5.add(it.tag)
data5.add((len / 32).toByte())
data5.add((len.rem(32)).toByte())
data5.addAll(encoded)
}
return data5
}

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package fr.acinq.lightning.payment

import fr.acinq.bitcoin.*
import fr.acinq.bitcoin.utils.Either
import fr.acinq.bitcoin.utils.Try
import fr.acinq.lightning.*
import fr.acinq.lightning.Lightning.randomBytes32
import fr.acinq.lightning.Lightning.randomKey
Expand Down Expand Up @@ -498,10 +499,12 @@ class Bolt11InvoiceTestsCommon : LightningTestSuite() {
assertEquals(pr1.paymentSecret, pr.paymentSecret)

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

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

// Invoices must use a payment secret.
assertFails {
Expand All @@ -517,6 +520,13 @@ class Bolt11InvoiceTestsCommon : LightningTestSuite() {
}
}

@Test
fun `decode invoice without features`() {
val s = "lnbc10n1pnglwsfpp5fdcjk5vudhe2jzuz0q65dkh4gfmxnn6vexlchc6ta9f25wynp2qshp59warmg27z4nkuvhs5x3vdv998jck5ue8nge2t68dtfvm27n8kvsqxqrrssnp4qf3rsvu5xdrxnv2kgkr4hvpefx257fjw8ugupnug4ls6rf2d5w6yc2rnnz0zuymgjl3p4dvyh8dr4mp969gjrnaggx50nv5ax7wy6yflyr07ek5hdevxtz9angp3jfyfz9ram8d7gw9pr0csr6fpa8rfeu7gpgesr58"
val failure = assertIs<Try.Failure<Throwable>>(Bolt11Invoice.read(s))
assertContains(failure.error.message ?: "", "var_onion_optin must be supported")
}

@Test
fun `invoice with descriptionHash`() {
val descriptionHash = randomBytes32()
Expand Down

0 comments on commit a41ec40

Please sign in to comment.