Reject offer_amount of 0 as invalid per BOLT 12
What changed, and why it matters
This commit tightens validation of BOLT 12 Lightning offers so that an offer cannot specify a payment amount of zero. Previously, zero-amount offers could be built or parsed successfully, which the BOLT 12 specification now forbids. The change prevents users or automated systems from accidentally creating or accepting offers that request no payment, which could be used to confuse wallets, trigger unexpected behavior, or probe nodes.
No immediate action required beyond normal review and merge. Users should ensure they are running a version that includes this validation if they process untrusted BOLT 12 offers.
Security signals we found
Spec-compliance validation change
Input sanitization for parsed offer TLVs
Builder hardening against zero-amount offers
Test coverage added for malformed offer vectors
Evidence from the diff
The patch updates rust-lightning’s BOLT 12 offer builder and TLV deserializer to reject offer_amount values of 0. In offer.rs, the build() method now returns Bolt12SemanticError::InvalidAmount when amount_msats == 0, and TryFrom
Changed components
lightning/src/offers/offer.rslightning/src/offers/invoice_request.rs (tests only)Inspect captured patch +68 / −2
diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs
index 4311d19..7805882 100644
--- a/lightning/src/offers/invoice_request.rs
+++ b/lightning/src/offers/invoice_request.rs
@@ -2040,6 +2040,12 @@ mod tests {
Err(e) => assert_eq!(e, Bolt12SemanticError::MissingAmount),
}
+ // An offer with amount_msats(0) must be rejected by the builder per BOLT 12.
+ match OfferBuilder::new(recipient_pubkey()).amount_msats(0).build() {
+ Ok(_) => panic!("expected error"),
+ Err(e) => assert_eq!(e, Bolt12SemanticError::InvalidAmount),
+ }
+
match OfferBuilder::new(recipient_pubkey())
.amount_msats(1000)
.supported_quantity(Quantity::Unbounded)
diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs
index 5592c50..b270345 100644
--- a/lightning/src/offers/offer.rs
+++ b/lightning/src/offers/offer.rs
@@ -402,7 +402,7 @@ macro_rules! offer_builder_methods { (
pub fn build($($self_mut)* $self: $self_type) -> Result<Offer, Bolt12SemanticError> {
match $self.offer.amount {
Some(Amount::Bitcoin { amount_msats }) => {
- if amount_msats > MAX_VALUE_MSAT {
+ if amount_msats == 0 || amount_msats > MAX_VALUE_MSAT {
return Err(Bolt12SemanticError::InvalidAmount);
}
},
@@ -1306,11 +1306,12 @@ impl TryFrom<FullOfferTlvStream> 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 +1703,12 @@ mod tests {
Ok(_) => panic!("expected error"),
Err(e) => assert_eq!(e, Bolt12SemanticError::InvalidAmount),
}
+
+ // An amount of 0 must be rejected per BOLT 12.
+ match OfferBuilder::new(pubkey(42)).amount_msats(0).build() {
+ Ok(_) => panic!("expected error"),
+ Err(e) => assert_eq!(e, Bolt12SemanticError::InvalidAmount),
+ }
}
#[test]
@@ -1974,6 +1981,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::<Offer>()
+ {
+ Ok(_) => panic!("expected error"),
+ Err(e) => assert_eq!(
+ e,
+ Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::InvalidAmount)
+ ),
+ }
+
+ match "lno1qcp4256ypqqq5qqkyyp4he0fg7pqje62jmnq78cr0ashv4q06qql58tyd9rhp3t2wuyugtq"
+ .parse::<Offer>()
+ {
+ Ok(_) => panic!("expected error"),
+ Err(e) => assert_eq!(
+ e,
+ Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::InvalidAmount)
+ ),
+ }
}
#[test]
Why this scored 39/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.