Skip to content

Commit 7b7968e

Browse files
Bolt11 invoices: do not re-encode amounts (#771)
* Bolt11 invoices: do not re-encode amounts Bolt11 amounts may not be minimally encoded (the specs says they SHOULD be but it's not a hard requirement). We should preserve the original amount encoding: if it was not minimally encoded and we re-encode it using its minimal representation (for example "5" instead of "5000m") then read() will fail to verify the invoice checksum and return an "invoice isn't canonically encoded" error. --------- Co-authored-by: Pierre-Marie Padiou <[email protected]>
1 parent cf17b62 commit 7b7968e

File tree

2 files changed

+23
-6
lines changed

2 files changed

+23
-6
lines changed

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

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,22 @@ import fr.acinq.lightning.utils.*
1313
import fr.acinq.lightning.wire.LightningCodecs
1414
import kotlin.experimental.and
1515

16+
/**
17+
* @param encodedAmount We counterintuitively store the string-serialized amount instead of the numeric value. The goal is to preserve the
18+
* validity of the signature for invoices generated by 3rd party software when the amount is not canonically encoded.
19+
*/
1620
data class Bolt11Invoice(
1721
val prefix: String,
18-
override val amount: MilliSatoshi?,
22+
val encodedAmount: String,
1923
val timestampSeconds: Long,
2024
override val nodeId: PublicKey,
2125
val tags: List<TaggedField>,
2226
val signature: ByteVector
2327
) : PaymentRequest() {
28+
constructor(prefix: String, amount: MilliSatoshi?, timestampSeconds: Long, nodeId: PublicKey, tags: List<TaggedField>, signature: ByteVector): this(prefix, encodeAmount(amount), timestampSeconds, nodeId, tags, signature)
29+
30+
override val amount: MilliSatoshi? = decodeAmount(encodedAmount)
31+
2432
val chain: Chain? get() = prefixes.entries.firstOrNull { it.value == prefix }?.key
2533

2634
override val paymentHash: ByteVector32 get() = tags.find { it is TaggedField.PaymentHash }!!.run { (this as TaggedField.PaymentHash).hash }
@@ -60,7 +68,7 @@ data class Bolt11Invoice(
6068
else -> timestampSeconds + expirySeconds <= currentTimestampSeconds
6169
}
6270

63-
private fun hrp() = prefix + encodeAmount(amount)
71+
private fun hrp() = prefix + encodedAmount
6472

6573
private fun rawData(): List<Int5> {
6674
val data5 = ArrayList<Int5>()
@@ -147,7 +155,7 @@ data class Bolt11Invoice(
147155

148156
return Bolt11Invoice(
149157
prefix = prefix,
150-
amount = amount,
158+
encodedAmount = encodeAmount(amount),
151159
timestampSeconds = timestampSeconds,
152160
nodeId = privateKey.publicKey(),
153161
tags = tags,
@@ -165,7 +173,7 @@ data class Bolt11Invoice(
165173
fun read(input: String): Try<Bolt11Invoice> = runTrying {
166174
val (hrp, data) = Bech32.decode(input)
167175
val prefix = prefixes.values.find { hrp.startsWith(it) } ?: throw IllegalArgumentException("unknown prefix $hrp")
168-
val amount = decodeAmount(hrp.drop(prefix.length))
176+
val encodedAmount = hrp.drop(prefix.length)
169177
val timestamp = decodeTimestamp(data.toList())
170178
// signature and recovery id, encoded on 65 bytes = 5 * 13 bytes = 5 * 13 * 8 bits = 8 * 13 "5-bits integers"
171179
val sigandrecid = toByteArray(data.copyOfRange(data.size - 8 * 13, data.size).toList())
@@ -203,7 +211,7 @@ data class Bolt11Invoice(
203211
}
204212

205213
loop(data.drop(7).dropLast(104))
206-
val pr = Bolt11Invoice(prefix, amount, timestamp, nodeId, tags, sigandrecid.toByteVector())
214+
val pr = Bolt11Invoice(prefix, encodedAmount, timestamp, nodeId, tags, sigandrecid.toByteVector())
207215
require(pr.signedPreimage().contentEquals(tohash)) { "invoice isn't canonically encoded" }
208216
pr
209217
}

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,16 @@ class Bolt11InvoiceTestsCommon : LightningTestSuite() {
540540
assertNull(pr1.description)
541541
}
542542

543+
@Test
544+
fun `decode and re-encode non-canonically encoded invoice`() {
545+
// invoice encoding is not canonical: 5000m should be 5
546+
val encodedInvoice = createInvoiceUnsafe(5.btc.toMilliSatoshi()).copy(encodedAmount = "5000m").write()
547+
assertTrue { encodedInvoice.startsWith("lnbcrt5000m") }
548+
val invoice = Bolt11Invoice.read(encodedInvoice).get()
549+
val reencodedInvoice = invoice.write()
550+
assertEquals(encodedInvoice, reencodedInvoice)
551+
}
552+
543553
companion object {
544554
fun createInvoiceUnsafe(
545555
amount: MilliSatoshi? = null,
@@ -576,5 +586,4 @@ class Bolt11InvoiceTestsCommon : LightningTestSuite() {
576586
).sign(privateKey)
577587
}
578588
}
579-
580589
}

0 commit comments

Comments
 (0)