What changed, and why it matters
This commit adds a missing validation rule for a type of Bitcoin Lightning payment request called a BOLT12 offer. Previously, Eclair would accept offers that named a fiat currency (like USD) but did not include an amount. That combination is invalid according to the Lightning specification, because a currency without an amount is meaningless. The fix rejects such malformed offers and adds a test case. The main risk is that an attacker could trick a node into accepting or acting on an ambiguous offer, potentially leading to incorrect pricing, payment routing confusion, or interoperability failures with other implementations.
Treat as a low-to-moderate security hardening patch. Review whether the invalid offer state could have been propagated to any payment, routing, or accounting logic before this fix, and consider backporting to maintained release branches. No immediate emergency response is indicated.
Security signals we found
Missing input validation on structured protocol message
Spec non-compliance in BOLT12 offer parsing
Potential for ambiguous payment request interpretation
Defensive hardening with added negative test vector
Evidence from the diff
In OfferTypes.scala, the validate() method now returns Left(MissingRequiredTlv(UInt64(8))) when an offer contains an OfferCurrency TLV but no OfferAmount TLV. This enforces the rule from BOLT12 that currency must be accompanied by an amount. A corresponding negative test vector was added to offers-test.json. The change is small and defensive, closing a spec-compliance gap in offer parsing.
Changed components
eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OfferTypes.scalaBOLT12 offer validation logicEclair offer parsing and decodingInspect captured patch +6 / −0
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 57d38ab..c4e3852 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
@@ -309,6 +309,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[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/resources/offers-test.json b/eclair-core/src/test/resources/offers-test.json
index 1096a1a..237ac08 100644
--- a/eclair-core/src/test/resources/offers-test.json
+++ b/eclair-core/src/test/resources/offers-test.json
@@ -578,6 +578,11 @@
"valid": false,
"bolt12": "lno1pqpzwyqkyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd5xvxg"
},
+ {
+ "description": "Missing offer_amount with offer_currency",
+ "valid": false,
+ "bolt12": "lno1qcp4256ypgx9getnwss8vetrw3hhyuckyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd5xvxg"
+ },
{
"description": "Missing offer_issuer_id and no offer_path",
"valid": false,
Why this scored 40/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.