diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt12Invoice.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt12Invoice.scala index dfe18d4600..ae65046c51 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt12Invoice.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt12Invoice.scala @@ -127,7 +127,7 @@ object Bolt12Invoice { _ -> () ) if (records.get[InvoiceAmount].isEmpty) return Left(MissingRequiredTlv(UInt64(170))) - if (records.get[InvoicePaths].forall(_.paths.isEmpty)) return Left(MissingRequiredTlv(UInt64(160))) + if (records.get[InvoicePaths].isEmpty) return Left(MissingRequiredTlv(UInt64(160))) if (records.get[InvoiceBlindedPay].map(_.paymentInfo.length) != records.get[InvoicePaths].map(_.paths.length)) return Left(MissingRequiredTlv(UInt64(162))) if (records.get[InvoiceNodeId].isEmpty) return Left(MissingRequiredTlv(UInt64(176))) if (records.get[InvoiceCreatedAt].isEmpty) return Left(MissingRequiredTlv(UInt64(164))) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala index 29894f80f1..8fad3ef339 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala @@ -205,4 +205,12 @@ object CommonCodecs { (bits: BitVector) => Attempt.fromTry(Try(codec.decode(bits))).flatten ) + def nonEmptyList[A](codec: Codec[A], name: String): Codec[Seq[A]] = + list(codec).narrow(l => { + if (l.nonEmpty) { + Attempt.successful(l.toSeq) + } else { + Attempt.failure(Err(s"$name must not be empty")) + } + }, _.toList) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OfferCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OfferCodecs.scala index b8f7cf7c65..73e3d1e37e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OfferCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OfferCodecs.scala @@ -16,7 +16,6 @@ package fr.acinq.eclair.wire.protocol -import fr.acinq.bitcoin.scalacompat.BlockHash import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.crypto.Sphinx.RouteBlinding.{BlindedHop, BlindedRoute} import fr.acinq.eclair.wire.protocol.CommonCodecs._ @@ -30,7 +29,7 @@ import java.util.Currency import scala.util.Try object OfferCodecs { - private val offerChains: Codec[OfferChains] = tlvField(list(blockHash).xmap[Seq[BlockHash]](_.toSeq, _.toList)) + private val offerChains: Codec[OfferChains] = tlvField(nonEmptyList(blockHash, "offer_chains")) private val offerMetadata: Codec[OfferMetadata] = tlvField(bytes) @@ -76,7 +75,7 @@ object OfferCodecs { ("firstPathKey" | publicKey) :: ("path" | blindedNodesCodec)).as[BlindedRoute] - private val offerPaths: Codec[OfferPaths] = tlvField(list(blindedRouteCodec).xmap[Seq[BlindedRoute]](_.toSeq, _.toList)) + private val offerPaths: Codec[OfferPaths] = tlvField(nonEmptyList(blindedRouteCodec, "offer_paths")) private val offerIssuer: Codec[OfferIssuer] = tlvField(utf8) @@ -138,7 +137,7 @@ object OfferCodecs { .typecase(UInt64(240), signature) ).complete) - private val invoicePaths: Codec[InvoicePaths] = tlvField(list(blindedRouteCodec).xmap[Seq[BlindedRoute]](_.toSeq, _.toList)) + private val invoicePaths: Codec[InvoicePaths] = tlvField(nonEmptyList(blindedRouteCodec, "invoice_paths")) val paymentInfo: Codec[PaymentInfo] = (("fee_base_msat" | millisatoshi32) :: @@ -148,7 +147,7 @@ object OfferCodecs { ("htlc_maximum_msat" | millisatoshi) :: ("features" | variableSizeBytes(uint16, bytes))).as[PaymentInfo] - private val invoiceBlindedPay: Codec[InvoiceBlindedPay] = tlvField(list(paymentInfo).xmap[Seq[PaymentInfo]](_.toSeq, _.toList)) + private val invoiceBlindedPay: Codec[InvoiceBlindedPay] = tlvField(nonEmptyList(paymentInfo, "invoice_blindedpay")) private val invoiceCreatedAt: Codec[InvoiceCreatedAt] = tlvField(tu64overflow.as[TimestampSecond]) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OfferTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OfferTypes.scala index c4e38529e5..e15d213dff 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OfferTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OfferTypes.scala @@ -308,7 +308,7 @@ object OfferTypes { def validate(records: TlvStream[OfferTlv]): Either[InvalidTlvPayload, Offer] = { if (records.get[OfferDescription].isEmpty && records.get[OfferAmount].nonEmpty) return Left(MissingRequiredTlv(UInt64(10))) - if (records.get[OfferNodeId].isEmpty && records.get[OfferPaths].forall(_.paths.isEmpty)) return Left(MissingRequiredTlv(UInt64(22))) + if (records.get[OfferNodeId].isEmpty && records.get[OfferPaths].isEmpty) return Left(MissingRequiredTlv(UInt64(22))) if (records.get[OfferCurrency].nonEmpty && records.get[OfferAmount].isEmpty) return Left(MissingRequiredTlv(UInt64(8))) if (records.unknown.exists(!isOfferTlv(_))) return Left(ForbiddenTlv(records.unknown.find(!isOfferTlv(_)).get.tag)) Right(Offer(records)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt12InvoiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt12InvoiceSpec.scala index 2a40ff43ed..a1389b70c5 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt12InvoiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt12InvoiceSpec.scala @@ -383,4 +383,9 @@ class Bolt12InvoiceSpec extends AnyFunSuite { assert(invoice.checkSignature()) assert(invoice.amount == 1000000000.msat) } + + test("invoice paths is set but and empty") { + val invoiceWithEmptyPaths = "lni1qqx2n6mw2fh2ckwdnwylkgqzypp5jl7hlqnf2ugg7j3slkwwcwht57vhyzzwjr4dq84rxzgqqqqqqzqrq83yqzscd9h8vmmfvdjjqamfw35zqmtpdeujqenfv4kxgucvqqfq2ctvd93k293pq0zxw03kpc8tc2vv3kfdne0kntqhq8p70wtdncwq2zngaqp529mmc5pqgdyhl4lcy62hzz855v8annkr46a8n9eqsn5satgpagesjqqqqqq9yqcpufq9vqfetqssyj5djm6dz0zzr8eprw9gu762k75f3lgm96gzwn994peh48k6xalctyr5jfmdyppx7cneqvqsyqaqqz3qpfqyv2sqd04xqg8pp2pq2x236nzneyzqxhct9y7unhcupeukwgf5xzhq0f0nuy6v6vej2dq65qcpufq2cysyqqzpy02klqrqqz8t8twx39z77cq6uq9syypugee7xc8qa0pf3jxe9k0976dvzuqu8eaedk0pcpg2dr5qx3gh008sgrn58w7cg2qhcunaapk9j6patmtda7nhqhzvwv6hflxygyrrglpqka8l6zfhfhprxazkufcn88rl07yxfp5mvjl70etp2pzdkhud3ekul5qnjq46hg" + assert(Bolt12Invoice.fromString(invoiceWithEmptyPaths).isFailure) + } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/OfferTypesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/OfferTypesSpec.scala index b934377f20..7bc7ef185a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/OfferTypesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/OfferTypesSpec.scala @@ -350,4 +350,14 @@ class OfferTypesSpec extends AnyFunSuite { assert(OfferCodecs.offerCurrency.decode(encode("XAU")).isFailure) assert(OfferCodecs.offerCurrency.decode(hex"ffffff".bits).isFailure) } + + test("empty fields") { + val invalidOffers = Seq( + Offer(TlvStream(OfferPaths(Nil))), + Offer(TlvStream(OfferNodeId(randomKey().publicKey), OfferChains(Nil))), + ) + for (invalidOffer <- invalidOffers) { + assert(Offer.decode(invalidOffer.toString).isFailure) + } + } }