From c6d0e468835fbf33f319bdf178b188c60f264f8d Mon Sep 17 00:00:00 2001 From: Vincenzo Palazzo Date: Tue, 20 Jan 2026 14:22:48 +0100 Subject: [PATCH] Reject offer_amount of 0 as invalid per BOLT 12 Per the spec clarification in https://github.com/lightning/bolts/pull/1316: - Writers MUST set offer_amount greater than zero when present - Readers MUST NOT respond to offers where offer_amount is zero Reject amount_msats(0) in the builder with InvalidAmount, and reject parsed offers with amount=0 (with or without currency) during TLV deserialization. Co-Authored-By: Claude Opus 4.6 Backport of a06c44698c6861b9a770711f33f3d441f4c64a3f Conflicts resolved in: * lightning/src/offers/offer.rs Compared to the upstream commit, this instead allows downstream code to pass a 0 amount but converts it to no-amount to ensure upgraded readers of the built offer will accept it. This avoids changing the API in a backport. --- lightning/src/offers/offer.rs | 65 ++++++++++++++++++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index 0cd1cece6b7..a81e6d5517d 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -382,6 +382,9 @@ macro_rules! offer_builder_methods { ( pub fn build($($self_mut)* $self: $self_type) -> Result { match $self.offer.amount { Some(Amount::Bitcoin { amount_msats }) => { + if amount_msats == 0 { + $self.offer.amount = None; + } if amount_msats > MAX_VALUE_MSAT { return Err(Bolt12SemanticError::InvalidAmount); } @@ -1158,11 +1161,12 @@ impl TryFrom for OfferContents { let amount = match (currency, amount) { (None, None) => None, - (None, Some(amount_msats)) if amount_msats > MAX_VALUE_MSAT => { + (None, Some(amount_msats)) if amount_msats == 0 || amount_msats > MAX_VALUE_MSAT => { return Err(Bolt12SemanticError::InvalidAmount); }, (None, Some(amount_msats)) => Some(Amount::Bitcoin { amount_msats }), (Some(_), None) => return Err(Bolt12SemanticError::MissingAmount), + (Some(_), Some(0)) => return Err(Bolt12SemanticError::InvalidAmount), (Some(iso4217_code), Some(amount)) => Some(Amount::Currency { iso4217_code, amount }), }; @@ -1515,6 +1519,12 @@ mod tests { Ok(_) => panic!("expected error"), Err(e) => assert_eq!(e, Bolt12SemanticError::InvalidAmount), } + + // An amount of 0 is rejected per BOLT 12, so we map it to `None` instead. + match OfferBuilder::new(pubkey(42)).amount_msats(0).build() { + Ok(offer) => assert_eq!(offer.amount(), None), + Err(_) => panic!("expected offer"), + } } #[test] @@ -1764,6 +1774,59 @@ mod tests { Ok(_) => panic!("expected error"), Err(e) => assert_eq!(e, Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::InvalidAmount)), } + + // An offer with amount=0 must be rejected per BOLT 12. + let mut tlv_stream = offer.as_tlv_stream(); + tlv_stream.0.amount = Some(0); + tlv_stream.0.currency = None; + + let mut encoded_offer = Vec::new(); + tlv_stream.write(&mut encoded_offer).unwrap(); + + match Offer::try_from(encoded_offer) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!( + e, + Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::InvalidAmount) + ), + } + + // An offer with amount=0 and a currency must also be rejected. + let mut tlv_stream = offer.as_tlv_stream(); + tlv_stream.0.amount = Some(0); + tlv_stream.0.currency = Some(b"USD"); + + let mut encoded_offer = Vec::new(); + tlv_stream.write(&mut encoded_offer).unwrap(); + + match Offer::try_from(encoded_offer) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!( + e, + Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::InvalidAmount) + ), + } + + // BOLT 12 test vectors: verify rejection of offers with amount=0 from their + // bech32 encoding (see bolt12/offers-test.json). + match "lno1pqqq5qqkyyp4he0fg7pqje62jmnq78cr0ashv4q06qql58tyd9rhp3t2wuyugtq".parse::() + { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!( + e, + Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::InvalidAmount) + ), + } + + match "lno1qcp4256ypqqq5qqkyyp4he0fg7pqje62jmnq78cr0ashv4q06qql58tyd9rhp3t2wuyugtq" + .parse::() + { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!( + e, + Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::InvalidAmount) + ), + } } #[test]