Skip to content

Commit abe2cc9

Browse files
Reject offers with some fields present but empty (#3175)
Offers or invoices where the fields `offer_chains`, `offer_paths`, `invoice_paths`, `invoice_blindedpay` are present but empty are considered invalid. While the spec does not necessarily rejects them explicitly, they can't be paid.
1 parent fa1b0ee commit abe2cc9

File tree

6 files changed

+29
-7
lines changed

6 files changed

+29
-7
lines changed

eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt12Invoice.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ object Bolt12Invoice {
127127
_ -> ()
128128
)
129129
if (records.get[InvoiceAmount].isEmpty) return Left(MissingRequiredTlv(UInt64(170)))
130-
if (records.get[InvoicePaths].forall(_.paths.isEmpty)) return Left(MissingRequiredTlv(UInt64(160)))
130+
if (records.get[InvoicePaths].isEmpty) return Left(MissingRequiredTlv(UInt64(160)))
131131
if (records.get[InvoiceBlindedPay].map(_.paymentInfo.length) != records.get[InvoicePaths].map(_.paths.length)) return Left(MissingRequiredTlv(UInt64(162)))
132132
if (records.get[InvoiceNodeId].isEmpty) return Left(MissingRequiredTlv(UInt64(176)))
133133
if (records.get[InvoiceCreatedAt].isEmpty) return Left(MissingRequiredTlv(UInt64(164)))

eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,4 +205,12 @@ object CommonCodecs {
205205
(bits: BitVector) => Attempt.fromTry(Try(codec.decode(bits))).flatten
206206
)
207207

208+
def nonEmptyList[A](codec: Codec[A], name: String): Codec[Seq[A]] =
209+
list(codec).narrow(l => {
210+
if (l.nonEmpty) {
211+
Attempt.successful(l.toSeq)
212+
} else {
213+
Attempt.failure(Err(s"$name must not be empty"))
214+
}
215+
}, _.toList)
208216
}

eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OfferCodecs.scala

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
package fr.acinq.eclair.wire.protocol
1818

19-
import fr.acinq.bitcoin.scalacompat.BlockHash
2019
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
2120
import fr.acinq.eclair.crypto.Sphinx.RouteBlinding.{BlindedHop, BlindedRoute}
2221
import fr.acinq.eclair.wire.protocol.CommonCodecs._
@@ -30,7 +29,7 @@ import java.util.Currency
3029
import scala.util.Try
3130

3231
object OfferCodecs {
33-
private val offerChains: Codec[OfferChains] = tlvField(list(blockHash).xmap[Seq[BlockHash]](_.toSeq, _.toList))
32+
private val offerChains: Codec[OfferChains] = tlvField(nonEmptyList(blockHash, "offer_chains"))
3433

3534
private val offerMetadata: Codec[OfferMetadata] = tlvField(bytes)
3635

@@ -76,7 +75,7 @@ object OfferCodecs {
7675
("firstPathKey" | publicKey) ::
7776
("path" | blindedNodesCodec)).as[BlindedRoute]
7877

79-
private val offerPaths: Codec[OfferPaths] = tlvField(list(blindedRouteCodec).xmap[Seq[BlindedRoute]](_.toSeq, _.toList))
78+
private val offerPaths: Codec[OfferPaths] = tlvField(nonEmptyList(blindedRouteCodec, "offer_paths"))
8079

8180
private val offerIssuer: Codec[OfferIssuer] = tlvField(utf8)
8281

@@ -138,7 +137,7 @@ object OfferCodecs {
138137
.typecase(UInt64(240), signature)
139138
).complete)
140139

141-
private val invoicePaths: Codec[InvoicePaths] = tlvField(list(blindedRouteCodec).xmap[Seq[BlindedRoute]](_.toSeq, _.toList))
140+
private val invoicePaths: Codec[InvoicePaths] = tlvField(nonEmptyList(blindedRouteCodec, "invoice_paths"))
142141

143142
val paymentInfo: Codec[PaymentInfo] =
144143
(("fee_base_msat" | millisatoshi32) ::
@@ -148,7 +147,7 @@ object OfferCodecs {
148147
("htlc_maximum_msat" | millisatoshi) ::
149148
("features" | variableSizeBytes(uint16, bytes))).as[PaymentInfo]
150149

151-
private val invoiceBlindedPay: Codec[InvoiceBlindedPay] = tlvField(list(paymentInfo).xmap[Seq[PaymentInfo]](_.toSeq, _.toList))
150+
private val invoiceBlindedPay: Codec[InvoiceBlindedPay] = tlvField(nonEmptyList(paymentInfo, "invoice_blindedpay"))
152151

153152
private val invoiceCreatedAt: Codec[InvoiceCreatedAt] = tlvField(tu64overflow.as[TimestampSecond])
154153

eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OfferTypes.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,7 @@ object OfferTypes {
308308

309309
def validate(records: TlvStream[OfferTlv]): Either[InvalidTlvPayload, Offer] = {
310310
if (records.get[OfferDescription].isEmpty && records.get[OfferAmount].nonEmpty) return Left(MissingRequiredTlv(UInt64(10)))
311-
if (records.get[OfferNodeId].isEmpty && records.get[OfferPaths].forall(_.paths.isEmpty)) return Left(MissingRequiredTlv(UInt64(22)))
311+
if (records.get[OfferNodeId].isEmpty && records.get[OfferPaths].isEmpty) return Left(MissingRequiredTlv(UInt64(22)))
312312
if (records.get[OfferCurrency].nonEmpty && records.get[OfferAmount].isEmpty) return Left(MissingRequiredTlv(UInt64(8)))
313313
if (records.unknown.exists(!isOfferTlv(_))) return Left(ForbiddenTlv(records.unknown.find(!isOfferTlv(_)).get.tag))
314314
Right(Offer(records))

eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt12InvoiceSpec.scala

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,4 +383,9 @@ class Bolt12InvoiceSpec extends AnyFunSuite {
383383
assert(invoice.checkSignature())
384384
assert(invoice.amount == 1000000000.msat)
385385
}
386+
387+
test("invoice paths is set but and empty") {
388+
val invoiceWithEmptyPaths = "lni1qqx2n6mw2fh2ckwdnwylkgqzypp5jl7hlqnf2ugg7j3slkwwcwht57vhyzzwjr4dq84rxzgqqqqqqzqrq83yqzscd9h8vmmfvdjjqamfw35zqmtpdeujqenfv4kxgucvqqfq2ctvd93k293pq0zxw03kpc8tc2vv3kfdne0kntqhq8p70wtdncwq2zngaqp529mmc5pqgdyhl4lcy62hzz855v8annkr46a8n9eqsn5satgpagesjqqqqqq9yqcpufq9vqfetqssyj5djm6dz0zzr8eprw9gu762k75f3lgm96gzwn994peh48k6xalctyr5jfmdyppx7cneqvqsyqaqqz3qpfqyv2sqd04xqg8pp2pq2x236nzneyzqxhct9y7unhcupeukwgf5xzhq0f0nuy6v6vej2dq65qcpufq2cysyqqzpy02klqrqqz8t8twx39z77cq6uq9syypugee7xc8qa0pf3jxe9k0976dvzuqu8eaedk0pcpg2dr5qx3gh008sgrn58w7cg2qhcunaapk9j6patmtda7nhqhzvwv6hflxygyrrglpqka8l6zfhfhprxazkufcn88rl07yxfp5mvjl70etp2pzdkhud3ekul5qnjq46hg"
389+
assert(Bolt12Invoice.fromString(invoiceWithEmptyPaths).isFailure)
390+
}
386391
}

eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/OfferTypesSpec.scala

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,4 +350,14 @@ class OfferTypesSpec extends AnyFunSuite {
350350
assert(OfferCodecs.offerCurrency.decode(encode("XAU")).isFailure)
351351
assert(OfferCodecs.offerCurrency.decode(hex"ffffff".bits).isFailure)
352352
}
353+
354+
test("empty fields") {
355+
val invalidOffers = Seq(
356+
Offer(TlvStream(OfferPaths(Nil))),
357+
Offer(TlvStream(OfferNodeId(randomKey().publicKey), OfferChains(Nil))),
358+
)
359+
for (invalidOffer <- invalidOffers) {
360+
assert(Offer.decode(invalidOffer.toString).isFailure)
361+
}
362+
}
353363
}

0 commit comments

Comments
 (0)