diff --git a/lightning/src/ln/offers_tests.rs b/lightning/src/ln/offers_tests.rs index 6455a60b139..35a4c61713c 100644 --- a/lightning/src/ln/offers_tests.rs +++ b/lightning/src/ln/offers_tests.rs @@ -566,7 +566,7 @@ fn creates_and_pays_for_offer_using_two_hop_blinded_path() { human_readable_name: None, }, }); - assert_eq!(invoice_request.amount_msats(), None); + assert_eq!(invoice_request.amount_msats(), Some(10_000_000)); assert_ne!(invoice_request.payer_signing_pubkey(), david_id); assert_eq!(reply_path.introduction_node(), &IntroductionNode::NodeId(charlie_id)); @@ -727,7 +727,7 @@ fn creates_and_pays_for_offer_using_one_hop_blinded_path() { human_readable_name: None, }, }); - assert_eq!(invoice_request.amount_msats(), None); + assert_eq!(invoice_request.amount_msats(), Some(10_000_000)); assert_ne!(invoice_request.payer_signing_pubkey(), bob_id); assert_eq!(reply_path.introduction_node(), &IntroductionNode::NodeId(bob_id)); @@ -1116,7 +1116,7 @@ fn creates_and_pays_for_offer_with_retry() { human_readable_name: None, }, }); - assert_eq!(invoice_request.amount_msats(), None); + assert_eq!(invoice_request.amount_msats(), Some(10_000_000)); assert_ne!(invoice_request.payer_signing_pubkey(), bob_id); assert_eq!(reply_path.introduction_node(), &IntroductionNode::NodeId(bob_id)); let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap(); @@ -1411,7 +1411,7 @@ fn fails_authentication_when_handling_invoice_request() { alice.onion_messenger.handle_onion_message(david_id, &onion_message); let (invoice_request, reply_path) = extract_invoice_request(alice, &onion_message); - assert_eq!(invoice_request.amount_msats(), None); + assert_eq!(invoice_request.amount_msats(), Some(10_000_000)); assert_ne!(invoice_request.payer_signing_pubkey(), david_id); assert_eq!(reply_path.introduction_node(), &IntroductionNode::NodeId(charlie_id)); @@ -1441,7 +1441,7 @@ fn fails_authentication_when_handling_invoice_request() { alice.onion_messenger.handle_onion_message(bob_id, &onion_message); let (invoice_request, reply_path) = extract_invoice_request(alice, &onion_message); - assert_eq!(invoice_request.amount_msats(), None); + assert_eq!(invoice_request.amount_msats(), Some(10_000_000)); assert_ne!(invoice_request.payer_signing_pubkey(), david_id); assert_eq!(reply_path.introduction_node(), &IntroductionNode::NodeId(charlie_id)); @@ -1543,7 +1543,7 @@ fn fails_authentication_when_handling_invoice_for_offer() { alice.onion_messenger.handle_onion_message(bob_id, &onion_message); let (invoice_request, reply_path) = extract_invoice_request(alice, &onion_message); - assert_eq!(invoice_request.amount_msats(), None); + assert_eq!(invoice_request.amount_msats(), Some(10_000_000)); assert_ne!(invoice_request.payer_signing_pubkey(), david_id); assert_eq!(reply_path.introduction_node(), &IntroductionNode::NodeId(charlie_id)); diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index 136ea2625de..d6a0392dac2 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -342,7 +342,7 @@ macro_rules! invoice_builder_methods { ( pub(crate) fn amount_msats( invoice_request: &InvoiceRequest ) -> Result { - match invoice_request.amount_msats() { + match invoice_request.contents.inner.amount_msats() { Some(amount_msats) => Ok(amount_msats), None => match invoice_request.contents.inner.offer.amount() { Some(Amount::Bitcoin { amount_msats }) => { @@ -1531,6 +1531,11 @@ impl TryFrom for InvoiceContents { experimental_offer_tlv_stream, experimental_invoice_request_tlv_stream, ) )?; + + if amount_msats != refund.amount_msats() { + return Err(Bolt12SemanticError::InvalidAmount); + } + Ok(InvoiceContents::ForRefund { refund, fields }) } else { let invoice_request = InvoiceRequestContents::try_from( @@ -1539,6 +1544,13 @@ impl TryFrom for InvoiceContents { experimental_offer_tlv_stream, experimental_invoice_request_tlv_stream, ) )?; + + if let Some(requested_amount_msats) = invoice_request.amount_msats() { + if amount_msats != requested_amount_msats { + return Err(Bolt12SemanticError::InvalidAmount); + } + } + Ok(InvoiceContents::ForOffer { invoice_request, fields }) } } @@ -2707,6 +2719,69 @@ mod tests { } } + #[test] + fn fails_parsing_invoice_with_wrong_amount() { + let expanded_key = ExpandedKey::new([42; 32]); + let entropy = FixedEntropy {}; + let nonce = Nonce::from_entropy_source(&entropy); + let secp_ctx = Secp256k1::new(); + let payment_id = PaymentId([1; 32]); + + let invoice = OfferBuilder::new(recipient_pubkey()) + .amount_msats(1000) + .build().unwrap() + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id).unwrap() + .build_and_sign().unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() + .amount_msats_unchecked(2000) + .build().unwrap() + .sign(recipient_sign).unwrap(); + + let mut buffer = Vec::new(); + invoice.write(&mut buffer).unwrap(); + + match Bolt12Invoice::try_from(buffer) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::InvalidAmount)), + } + + let invoice = OfferBuilder::new(recipient_pubkey()) + .amount_msats(1000) + .build().unwrap() + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id).unwrap() + .amount_msats(1000).unwrap() + .build_and_sign().unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() + .amount_msats_unchecked(2000) + .build().unwrap() + .sign(recipient_sign).unwrap(); + + let mut buffer = Vec::new(); + invoice.write(&mut buffer).unwrap(); + + match Bolt12Invoice::try_from(buffer) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::InvalidAmount)), + } + + let invoice = RefundBuilder::new(vec![1; 32], payer_pubkey(), 1000).unwrap() + .build().unwrap() + .respond_using_derived_keys_no_std( + payment_paths(), payment_hash(), now(), &expanded_key, &entropy + ) + .unwrap() + .amount_msats_unchecked(2000) + .build_and_sign(&secp_ctx).unwrap(); + + let mut buffer = Vec::new(); + invoice.write(&mut buffer).unwrap(); + + match Bolt12Invoice::try_from(buffer) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::InvalidAmount)), + } + } + #[test] fn fails_parsing_invoice_without_signature() { let expanded_key = ExpandedKey::new([42; 32]); diff --git a/lightning/src/offers/invoice_macros.rs b/lightning/src/offers/invoice_macros.rs index 93e62d7adaf..dd75fe62504 100644 --- a/lightning/src/offers/invoice_macros.rs +++ b/lightning/src/offers/invoice_macros.rs @@ -87,6 +87,14 @@ macro_rules! invoice_builder_methods_test { ( $self: ident, $self_type: ty, $invoice_fields: expr, $return_type: ty, $return_value: expr $(, $self_mut: tt)? ) => { + #[cfg_attr(c_bindings, allow(dead_code))] + pub(crate) fn amount_msats_unchecked( + $($self_mut)* $self: $self_type, amount_msats: u64, + ) -> $return_type { + $invoice_fields.amount_msats = amount_msats; + $return_value + } + #[cfg_attr(c_bindings, allow(dead_code))] pub(crate) fn features_unchecked( $($self_mut)* $self: $self_type, features: Bolt12InvoiceFeatures diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index 74bbdb8a0bf..957884f69d0 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -79,7 +79,7 @@ use crate::ln::inbound_payment::{ExpandedKey, IV_LEN}; use crate::ln::msgs::DecodeError; use crate::offers::merkle::{SignError, SignFn, SignatureTlvStream, SignatureTlvStreamRef, TaggedHash, TlvStream, self, SIGNATURE_TLV_RECORD_SIZE}; use crate::offers::nonce::Nonce; -use crate::offers::offer::{EXPERIMENTAL_OFFER_TYPES, ExperimentalOfferTlvStream, ExperimentalOfferTlvStreamRef, OFFER_TYPES, Offer, OfferContents, OfferId, OfferTlvStream, OfferTlvStreamRef}; +use crate::offers::offer::{Amount, EXPERIMENTAL_OFFER_TYPES, ExperimentalOfferTlvStream, ExperimentalOfferTlvStreamRef, OFFER_TYPES, Offer, OfferContents, OfferId, OfferTlvStream, OfferTlvStreamRef}; use crate::offers::parse::{Bolt12ParseError, ParsedMessage, Bolt12SemanticError}; use crate::offers::payer::{PayerContents, PayerTlvStream, PayerTlvStreamRef}; use crate::offers::signer::{Metadata, MetadataMaterial}; @@ -665,6 +665,15 @@ macro_rules! invoice_request_accessors { ($self: ident, $contents: expr) => { $contents.amount_msats() } + /// Returns whether an amount was set in the request; otherwise, if [`amount_msats`] is `Some` + /// then it was inferred from the [`Offer::amount`] and [`quantity`]. + /// + /// [`amount_msats`]: Self::amount_msats + /// [`quantity`]: Self::quantity + pub fn has_amount_msats(&$self) -> bool { + $contents.has_amount_msats() + } + /// Features pertaining to requesting an invoice. pub fn invoice_request_features(&$self) -> &InvoiceRequestFeatures { &$contents.features() @@ -974,7 +983,19 @@ impl InvoiceRequestContents { } pub(super) fn amount_msats(&self) -> Option { - self.inner.amount_msats + self.inner + .amount_msats() + .or_else(|| match self.inner.offer.amount() { + Some(Amount::Bitcoin { amount_msats }) => { + Some(amount_msats.saturating_mul(self.quantity().unwrap_or(1))) + }, + Some(Amount::Currency { .. }) => None, + None => { debug_assert!(false); None}, + }) + } + + pub(super) fn has_amount_msats(&self) -> bool { + self.inner.amount_msats().is_some() } pub(super) fn features(&self) -> &InvoiceRequestFeatures { @@ -1015,6 +1036,10 @@ impl InvoiceRequestContentsWithoutPayerSigningPubkey { self.chain.unwrap_or_else(|| self.offer.implied_chain()) } + pub(super) fn amount_msats(&self) -> Option { + self.amount_msats + } + pub(super) fn as_tlv_stream(&self) -> PartialInvoiceRequestTlvStreamRef { let payer = PayerTlvStreamRef { metadata: self.payer.0.as_bytes(), @@ -1381,7 +1406,7 @@ mod tests { assert_eq!(invoice_request.supported_quantity(), Quantity::One); assert_eq!(invoice_request.issuer_signing_pubkey(), Some(recipient_pubkey())); assert_eq!(invoice_request.chain(), ChainHash::using_genesis_block(Network::Bitcoin)); - assert_eq!(invoice_request.amount_msats(), None); + assert_eq!(invoice_request.amount_msats(), Some(1000)); assert_eq!(invoice_request.invoice_request_features(), &InvoiceRequestFeatures::empty()); assert_eq!(invoice_request.quantity(), None); assert_eq!(invoice_request.payer_note(), None); @@ -1657,6 +1682,7 @@ mod tests { .amount_msats(1000).unwrap() .build_and_sign().unwrap(); let (_, _, tlv_stream, _, _, _) = invoice_request.as_tlv_stream(); + assert!(invoice_request.has_amount_msats()); assert_eq!(invoice_request.amount_msats(), Some(1000)); assert_eq!(tlv_stream.amount, Some(1000)); @@ -1668,6 +1694,7 @@ mod tests { .amount_msats(1000).unwrap() .build_and_sign().unwrap(); let (_, _, tlv_stream, _, _, _) = invoice_request.as_tlv_stream(); + assert!(invoice_request.has_amount_msats()); assert_eq!(invoice_request.amount_msats(), Some(1000)); assert_eq!(tlv_stream.amount, Some(1000)); @@ -1678,6 +1705,7 @@ mod tests { .amount_msats(1001).unwrap() .build_and_sign().unwrap(); let (_, _, tlv_stream, _, _, _) = invoice_request.as_tlv_stream(); + assert!(invoice_request.has_amount_msats()); assert_eq!(invoice_request.amount_msats(), Some(1001)); assert_eq!(tlv_stream.amount, Some(1001)); @@ -1748,6 +1776,47 @@ mod tests { } } + #[test] + fn builds_invoice_request_without_amount() { + let expanded_key = ExpandedKey::new([42; 32]); + let entropy = FixedEntropy {}; + let nonce = Nonce::from_entropy_source(&entropy); + let secp_ctx = Secp256k1::new(); + let payment_id = PaymentId([1; 32]); + + let invoice_request = OfferBuilder::new(recipient_pubkey()) + .amount_msats(1000) + .build().unwrap() + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id).unwrap() + .build_and_sign().unwrap(); + let (_, _, tlv_stream, _, _, _) = invoice_request.as_tlv_stream(); + assert!(!invoice_request.has_amount_msats()); + assert_eq!(invoice_request.amount_msats(), Some(1000)); + assert_eq!(tlv_stream.amount, None); + + let invoice_request = OfferBuilder::new(recipient_pubkey()) + .amount_msats(1000) + .supported_quantity(Quantity::Unbounded) + .build().unwrap() + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id).unwrap() + .quantity(2).unwrap() + .build_and_sign().unwrap(); + let (_, _, tlv_stream, _, _, _) = invoice_request.as_tlv_stream(); + assert!(!invoice_request.has_amount_msats()); + assert_eq!(invoice_request.amount_msats(), Some(2000)); + assert_eq!(tlv_stream.amount, None); + + let invoice_request = OfferBuilder::new(recipient_pubkey()) + .amount(Amount::Currency { iso4217_code: *b"USD", amount: 10 }) + .build_unchecked() + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id).unwrap() + .build_unchecked_and_sign(); + let (_, _, tlv_stream, _, _, _) = invoice_request.as_tlv_stream(); + assert!(!invoice_request.has_amount_msats()); + assert_eq!(invoice_request.amount_msats(), None); + assert_eq!(tlv_stream.amount, None); + } + #[test] fn builds_invoice_request_with_features() { let expanded_key = ExpandedKey::new([42; 32]); diff --git a/lightning/src/offers/parse.rs b/lightning/src/offers/parse.rs index f3c481a9f95..6b72c6b1682 100644 --- a/lightning/src/offers/parse.rs +++ b/lightning/src/offers/parse.rs @@ -147,7 +147,7 @@ pub enum Bolt12SemanticError { UnexpectedChain, /// An amount was expected but was missing. MissingAmount, - /// The amount exceeded the total bitcoin supply. + /// The amount exceeded the total bitcoin supply or didn't match an expected amount. InvalidAmount, /// An amount was provided but was not sufficient in value. InsufficientAmount,