Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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._
Expand All @@ -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)

Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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) ::
Expand All @@ -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])

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}