diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index 7eb719c104a..5e66b1c9924 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -402,6 +402,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); } @@ -1306,11 +1309,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(currency_bytes), Some(amount)) => { let iso4217_code = CurrencyCode::new(currency_bytes) .map_err(|_| Bolt12SemanticError::InvalidCurrencyCode)?; @@ -1702,6 +1706,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] @@ -1974,6 +1984,59 @@ mod tests { Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::InvalidCurrencyCode) ), } + + // 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]