]> git.bitcoin.ninja Git - rust-lightning/commitdiff
Builder for creating invoice requests
authorJeffrey Czyz <jkczyz@gmail.com>
Wed, 31 Aug 2022 15:19:44 +0000 (10:19 -0500)
committerJeffrey Czyz <jkczyz@gmail.com>
Fri, 9 Dec 2022 14:53:46 +0000 (08:53 -0600)
Add a builder for creating invoice requests for an offer given a
payer_id. Other settings may be optional depending on the offer and
duplicative settings will override previous settings. Building produces
a semantically valid `invoice_request` message for the offer, which then
can be signed for the payer_id.

lightning/src/offers/invoice_request.rs
lightning/src/offers/merkle.rs
lightning/src/offers/offer.rs
lightning/src/offers/parse.rs

index 6cedf3eda59d68b2d18ac8aaa3bde05aabfb10b6..dc7d590b7ebd91eb3c23f20ab9707465ad616fb7 100644 (file)
 // licenses.
 
 //! Data structures and encoding for `invoice_request` messages.
+//!
+//! An [`InvoiceRequest`] can be either built from a parsed [`Offer`] as an "offer to be paid" or
+//! built directly as an "offer for money" (e.g., refund, ATM withdrawal). In the former case, it is
+//! typically constructed by a customer and sent to the merchant who had published the corresponding
+//! offer. In the latter case, an offer doesn't exist as a precursor to the request. Rather the
+//! merchant would typically construct the invoice request and present it to the customer.
+//!
+//! The recipient of the request responds with an `Invoice`.
+//!
+//! ```ignore
+//! extern crate bitcoin;
+//! extern crate lightning;
+//!
+//! use bitcoin::network::constants::Network;
+//! use bitcoin::secp256k1::{KeyPair, PublicKey, Secp256k1, SecretKey};
+//! use core::convert::Infallible;
+//! use lightning::ln::features::OfferFeatures;
+//! use lightning::offers::offer::Offer;
+//! use lightning::util::ser::Writeable;
+//!
+//! # fn parse() -> Result<(), lightning::offers::parse::ParseError> {
+//! let secp_ctx = Secp256k1::new();
+//! let keys = KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32])?);
+//! let pubkey = PublicKey::from(keys);
+//! let mut buffer = Vec::new();
+//!
+//! // "offer to be paid" flow
+//! "lno1qcp4256ypq"
+//!     .parse::<Offer>()?
+//!     .request_invoice(vec![42; 64], pubkey)?
+//!     .chain(Network::Testnet)?
+//!     .amount_msats(1000)?
+//!     .quantity(5)?
+//!     .payer_note("foo".to_string())
+//!     .build()?
+//!     .sign::<_, Infallible>(|digest| Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &keys)))
+//!     .expect("failed verifying signature")
+//!     .write(&mut buffer)
+//!     .unwrap();
+//! # Ok(())
+//! # }
+//! ```
 
 use bitcoin::blockdata::constants::ChainHash;
-use bitcoin::secp256k1::PublicKey;
+use bitcoin::network::constants::Network;
+use bitcoin::secp256k1::{Message, PublicKey};
 use bitcoin::secp256k1::schnorr::Signature;
 use core::convert::TryFrom;
 use crate::io;
 use crate::ln::features::InvoiceRequestFeatures;
 use crate::ln::msgs::DecodeError;
-use crate::offers::merkle::{SignatureTlvStream, self};
-use crate::offers::offer::{Amount, OfferContents, OfferTlvStream};
+use crate::offers::merkle::{SignError, SignatureTlvStream, SignatureTlvStreamRef, self};
+use crate::offers::offer::{Offer, OfferContents, OfferTlvStream, OfferTlvStreamRef};
 use crate::offers::parse::{ParseError, ParsedMessage, SemanticError};
-use crate::offers::payer::{PayerContents, PayerTlvStream};
+use crate::offers::payer::{PayerContents, PayerTlvStream, PayerTlvStreamRef};
 use crate::util::ser::{HighZeroBytesDroppedBigSize, SeekReadable, WithoutLength, Writeable, Writer};
 use crate::util::string::PrintableString;
 
 use crate::prelude::*;
 
+const SIGNATURE_TAG: &'static str = concat!("lightning", "invoice_request", "signature");
+
+/// Builds an [`InvoiceRequest`] from an [`Offer`] for the "offer to be paid" flow.
+///
+/// See [module-level documentation] for usage.
+///
+/// [module-level documentation]: self
+pub struct InvoiceRequestBuilder<'a> {
+       offer: &'a Offer,
+       invoice_request: InvoiceRequestContents,
+}
+
+impl<'a> InvoiceRequestBuilder<'a> {
+       pub(super) fn new(offer: &'a Offer, metadata: Vec<u8>, payer_id: PublicKey) -> Self {
+               Self {
+                       offer,
+                       invoice_request: InvoiceRequestContents {
+                               payer: PayerContents(metadata), offer: offer.contents.clone(), chain: None,
+                               amount_msats: None, features: InvoiceRequestFeatures::empty(), quantity: None,
+                               payer_id, payer_note: None,
+                       },
+               }
+       }
+
+       /// Sets the [`InvoiceRequest::chain`] of the given [`Network`] for paying an invoice. If not
+       /// called, [`Network::Bitcoin`] is assumed. Errors if the chain for `network` is not supported
+       /// by the offer.
+       ///
+       /// Successive calls to this method will override the previous setting.
+       pub fn chain(mut self, network: Network) -> Result<Self, SemanticError> {
+               let chain = ChainHash::using_genesis_block(network);
+               if !self.offer.supports_chain(chain) {
+                       return Err(SemanticError::UnsupportedChain);
+               }
+
+               self.invoice_request.chain = Some(chain);
+               Ok(self)
+       }
+
+       /// Sets the [`InvoiceRequest::amount_msats`] for paying an invoice. Errors if `amount_msats` is
+       /// not at least the expected invoice amount (i.e., [`Offer::amount`] times [`quantity`]).
+       ///
+       /// Successive calls to this method will override the previous setting.
+       ///
+       /// [`quantity`]: Self::quantity
+       pub fn amount_msats(mut self, amount_msats: u64) -> Result<Self, SemanticError> {
+               self.invoice_request.offer.check_amount_msats_for_quantity(
+                       Some(amount_msats), self.invoice_request.quantity
+               )?;
+               self.invoice_request.amount_msats = Some(amount_msats);
+               Ok(self)
+       }
+
+       /// Sets [`InvoiceRequest::quantity`] of items. If not set, `1` is assumed. Errors if `quantity`
+       /// does not conform to [`Offer::is_valid_quantity`].
+       ///
+       /// Successive calls to this method will override the previous setting.
+       pub fn quantity(mut self, quantity: u64) -> Result<Self, SemanticError> {
+               self.invoice_request.offer.check_quantity(Some(quantity))?;
+               self.invoice_request.quantity = Some(quantity);
+               Ok(self)
+       }
+
+       /// Sets the [`InvoiceRequest::payer_note`].
+       ///
+       /// Successive calls to this method will override the previous setting.
+       pub fn payer_note(mut self, payer_note: String) -> Self {
+               self.invoice_request.payer_note = Some(payer_note);
+               self
+       }
+
+       /// Builds an unsigned [`InvoiceRequest`] after checking for valid semantics. It can be signed
+       /// by [`UnsignedInvoiceRequest::sign`].
+       pub fn build(mut self) -> Result<UnsignedInvoiceRequest<'a>, SemanticError> {
+               #[cfg(feature = "std")] {
+                       if self.offer.is_expired() {
+                               return Err(SemanticError::AlreadyExpired);
+                       }
+               }
+
+               let chain = self.invoice_request.chain();
+               if !self.offer.supports_chain(chain) {
+                       return Err(SemanticError::UnsupportedChain);
+               }
+
+               if chain == self.offer.implied_chain() {
+                       self.invoice_request.chain = None;
+               }
+
+               if self.offer.amount().is_none() && self.invoice_request.amount_msats.is_none() {
+                       return Err(SemanticError::MissingAmount);
+               }
+
+               self.invoice_request.offer.check_quantity(self.invoice_request.quantity)?;
+               self.invoice_request.offer.check_amount_msats_for_quantity(
+                       self.invoice_request.amount_msats, self.invoice_request.quantity
+               )?;
+
+               let InvoiceRequestBuilder { offer, invoice_request } = self;
+               Ok(UnsignedInvoiceRequest { offer, invoice_request })
+       }
+}
+
+/// A semantically valid [`InvoiceRequest`] that hasn't been signed.
+pub struct UnsignedInvoiceRequest<'a> {
+       offer: &'a Offer,
+       invoice_request: InvoiceRequestContents,
+}
+
+impl<'a> UnsignedInvoiceRequest<'a> {
+       /// Signs the invoice request using the given function.
+       pub fn sign<F, E>(self, sign: F) -> Result<InvoiceRequest, SignError<E>>
+       where
+               F: FnOnce(&Message) -> Result<Signature, E>
+       {
+               // Use the offer bytes instead of the offer TLV stream as the offer may have contained
+               // unknown TLV records, which are not stored in `OfferContents`.
+               let (payer_tlv_stream, _offer_tlv_stream, invoice_request_tlv_stream) =
+                       self.invoice_request.as_tlv_stream();
+               let offer_bytes = WithoutLength(&self.offer.bytes);
+               let unsigned_tlv_stream = (payer_tlv_stream, offer_bytes, invoice_request_tlv_stream);
+
+               let mut bytes = Vec::new();
+               unsigned_tlv_stream.write(&mut bytes).unwrap();
+
+               let pubkey = self.invoice_request.payer_id;
+               let signature = Some(merkle::sign_message(sign, SIGNATURE_TAG, &bytes, pubkey)?);
+
+               // Append the signature TLV record to the bytes.
+               let signature_tlv_stream = SignatureTlvStreamRef {
+                       signature: signature.as_ref(),
+               };
+               signature_tlv_stream.write(&mut bytes).unwrap();
+
+               Ok(InvoiceRequest {
+                       bytes,
+                       contents: self.invoice_request,
+                       signature,
+               })
+       }
+}
+
 /// An `InvoiceRequest` is a request for an `Invoice` formulated from an [`Offer`].
 ///
 /// An offer may provide choices such as quantity, amount, chain, features, etc. An invoice request
@@ -61,17 +246,14 @@ impl InvoiceRequest {
        }
 
        /// A chain from [`Offer::chains`] that the offer is valid for.
-       ///
-       /// [`Offer::chains`]: crate::offers::offer::Offer::chains
        pub fn chain(&self) -> ChainHash {
-               self.contents.chain.unwrap_or_else(|| self.contents.offer.implied_chain())
+               self.contents.chain()
        }
 
        /// The amount to pay in msats (i.e., the minimum lightning-payable unit for [`chain`]), which
        /// must be greater than or equal to [`Offer::amount`], converted if necessary.
        ///
        /// [`chain`]: Self::chain
-       /// [`Offer::amount`]: crate::offers::offer::Offer::amount
        pub fn amount_msats(&self) -> Option<u64> {
                self.contents.amount_msats
        }
@@ -82,8 +264,6 @@ impl InvoiceRequest {
        }
 
        /// The quantity of the offer's item conforming to [`Offer::is_valid_quantity`].
-       ///
-       /// [`Offer::is_valid_quantity`]: crate::offers::offer::Offer::is_valid_quantity
        pub fn quantity(&self) -> Option<u64> {
                self.contents.quantity
        }
@@ -93,7 +273,8 @@ impl InvoiceRequest {
                self.contents.payer_id
        }
 
-       /// Payer provided note to include in the invoice.
+       /// A payer-provided note which will be seen by the recipient and reflected back in the invoice
+       /// response.
        pub fn payer_note(&self) -> Option<PrintableString> {
                self.contents.payer_note.as_ref().map(|payer_note| PrintableString(payer_note.as_str()))
        }
@@ -106,12 +287,48 @@ impl InvoiceRequest {
        }
 }
 
+impl InvoiceRequestContents {
+       fn chain(&self) -> ChainHash {
+               self.chain.unwrap_or_else(|| self.offer.implied_chain())
+       }
+
+       pub(super) fn as_tlv_stream(&self) -> PartialInvoiceRequestTlvStreamRef {
+               let payer = PayerTlvStreamRef {
+                       metadata: Some(&self.payer.0),
+               };
+
+               let offer = self.offer.as_tlv_stream();
+
+               let features = {
+                       if self.features == InvoiceRequestFeatures::empty() { None }
+                       else { Some(&self.features) }
+               };
+
+               let invoice_request = InvoiceRequestTlvStreamRef {
+                       chain: self.chain.as_ref(),
+                       amount: self.amount_msats,
+                       features,
+                       quantity: self.quantity,
+                       payer_id: Some(&self.payer_id),
+                       payer_note: self.payer_note.as_ref(),
+               };
+
+               (payer, offer, invoice_request)
+       }
+}
+
 impl Writeable for InvoiceRequest {
        fn write<W: Writer>(&self, writer: &mut W) -> Result<(), io::Error> {
                WithoutLength(&self.bytes).write(writer)
        }
 }
 
+impl Writeable for InvoiceRequestContents {
+       fn write<W: Writer>(&self, writer: &mut W) -> Result<(), io::Error> {
+               self.as_tlv_stream().write(writer)
+       }
+}
+
 tlv_stream!(InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef, 80..160, {
        (80, chain: ChainHash),
        (82, amount: (u64, HighZeroBytesDroppedBigSize)),
@@ -137,6 +354,12 @@ impl SeekReadable for FullInvoiceRequestTlvStream {
 
 type PartialInvoiceRequestTlvStream = (PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream);
 
+type PartialInvoiceRequestTlvStreamRef<'a> = (
+       PayerTlvStreamRef<'a>,
+       OfferTlvStreamRef<'a>,
+       InvoiceRequestTlvStreamRef<'a>,
+);
+
 impl TryFrom<Vec<u8>> for InvoiceRequest {
        type Error = ParseError;
 
@@ -152,8 +375,7 @@ impl TryFrom<Vec<u8>> for InvoiceRequest {
                )?;
 
                if let Some(signature) = &signature {
-                       let tag = concat!("lightning", "invoice_request", "signature");
-                       merkle::verify_signature(signature, tag, &bytes, contents.payer_id)?;
+                       merkle::verify_signature(signature, SIGNATURE_TAG, &bytes, contents.payer_id)?;
                }
 
                Ok(InvoiceRequest { bytes, contents, signature })
@@ -180,31 +402,14 @@ impl TryFrom<PartialInvoiceRequestTlvStream> for InvoiceRequestContents {
                        return Err(SemanticError::UnsupportedChain);
                }
 
-               let amount_msats = match (offer.amount(), amount) {
-                       (None, None) => return Err(SemanticError::MissingAmount),
-                       (Some(Amount::Currency { .. }), _) => return Err(SemanticError::UnsupportedCurrency),
-                       (_, amount_msats) => amount_msats,
-               };
-
-               let features = features.unwrap_or_else(InvoiceRequestFeatures::empty);
+               if offer.amount().is_none() && amount.is_none() {
+                       return Err(SemanticError::MissingAmount);
+               }
 
-               let expects_quantity = offer.expects_quantity();
-               let quantity = match quantity {
-                       None if expects_quantity => return Err(SemanticError::MissingQuantity),
-                       Some(_) if !expects_quantity => return Err(SemanticError::UnexpectedQuantity),
-                       Some(quantity) if !offer.is_valid_quantity(quantity) => {
-                               return Err(SemanticError::InvalidQuantity);
-                       }
-                       quantity => quantity,
-               };
+               offer.check_quantity(quantity)?;
+               offer.check_amount_msats_for_quantity(amount, quantity)?;
 
-               {
-                       let amount_msats = amount_msats.unwrap_or(offer.amount_msats());
-                       let quantity = quantity.unwrap_or(1);
-                       if amount_msats < offer.expected_invoice_amount_msats(quantity) {
-                               return Err(SemanticError::InsufficientAmount);
-                       }
-               }
+               let features = features.unwrap_or_else(InvoiceRequestFeatures::empty);
 
                let payer_id = match payer_id {
                        None => return Err(SemanticError::MissingPayerId),
@@ -212,7 +417,43 @@ impl TryFrom<PartialInvoiceRequestTlvStream> for InvoiceRequestContents {
                };
 
                Ok(InvoiceRequestContents {
-                       payer, offer, chain, amount_msats, features, quantity, payer_id, payer_note,
+                       payer, offer, chain, amount_msats: amount, features, quantity, payer_id, payer_note,
                })
        }
 }
+
+#[cfg(test)]
+mod tests {
+       use super::InvoiceRequest;
+
+       use bitcoin::secp256k1::{KeyPair, Secp256k1, SecretKey};
+       use core::convert::{Infallible, TryFrom};
+       use crate::ln::msgs::DecodeError;
+       use crate::offers::offer::OfferBuilder;
+       use crate::offers::parse::ParseError;
+       use crate::util::ser::{BigSize, Writeable};
+
+       #[test]
+       fn fails_parsing_invoice_request_with_extra_tlv_records() {
+               let secp_ctx = Secp256k1::new();
+               let keys = KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap());
+               let invoice_request = OfferBuilder::new("foo".into(), keys.public_key())
+                       .amount_msats(1000)
+                       .build().unwrap()
+                       .request_invoice(vec![1; 32], keys.public_key()).unwrap()
+                       .build().unwrap()
+                       .sign::<_, Infallible>(|digest| Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &keys)))
+                       .unwrap();
+
+               let mut encoded_invoice_request = Vec::new();
+               invoice_request.write(&mut encoded_invoice_request).unwrap();
+               BigSize(1002).write(&mut encoded_invoice_request).unwrap();
+               BigSize(32).write(&mut encoded_invoice_request).unwrap();
+               [42u8; 32].write(&mut encoded_invoice_request).unwrap();
+
+               match InvoiceRequest::try_from(encoded_invoice_request) {
+                       Ok(_) => panic!("expected error"),
+                       Err(e) => assert_eq!(e, ParseError::Decode(DecodeError::InvalidValue)),
+               }
+       }
+}
index 95183bea20d2a50c29dc62f2890c0e0381cb402d..d34a2a073f345c1b6c375b0775d378b363897eb3 100644 (file)
@@ -24,6 +24,35 @@ tlv_stream!(SignatureTlvStream, SignatureTlvStreamRef, SIGNATURE_TYPES, {
        (240, signature: Signature),
 });
 
+/// Error when signing messages.
+#[derive(Debug)]
+pub enum SignError<E> {
+       /// User-defined error when signing the message.
+       Signing(E),
+       /// Error when verifying the produced signature using the given pubkey.
+       Verification(secp256k1::Error),
+}
+
+/// Signs a message digest consisting of a tagged hash of the given bytes, checking if it can be
+/// verified with the supplied pubkey.
+///
+/// Panics if `bytes` is not a well-formed TLV stream containing at least one TLV record.
+pub(super) fn sign_message<F, E>(
+       sign: F, tag: &str, bytes: &[u8], pubkey: PublicKey,
+) -> Result<Signature, SignError<E>>
+where
+       F: FnOnce(&Message) -> Result<Signature, E>
+{
+       let digest = message_digest(tag, bytes);
+       let signature = sign(&digest).map_err(|e| SignError::Signing(e))?;
+
+       let pubkey = pubkey.into();
+       let secp_ctx = Secp256k1::verification_only();
+       secp_ctx.verify_schnorr(&signature, &digest, &pubkey).map_err(|e| SignError::Verification(e))?;
+
+       Ok(signature)
+}
+
 /// Verifies the signature with a pubkey over the given bytes using a tagged hash as the message
 /// digest.
 ///
@@ -31,14 +60,18 @@ tlv_stream!(SignatureTlvStream, SignatureTlvStreamRef, SIGNATURE_TYPES, {
 pub(super) fn verify_signature(
        signature: &Signature, tag: &str, bytes: &[u8], pubkey: PublicKey,
 ) -> Result<(), secp256k1::Error> {
-       let tag = sha256::Hash::hash(tag.as_bytes());
-       let merkle_root = root_hash(bytes);
-       let digest = Message::from_slice(&tagged_hash(tag, merkle_root)).unwrap();
+       let digest = message_digest(tag, bytes);
        let pubkey = pubkey.into();
        let secp_ctx = Secp256k1::verification_only();
        secp_ctx.verify_schnorr(signature, &digest, &pubkey)
 }
 
+fn message_digest(tag: &str, bytes: &[u8]) -> Message {
+       let tag = sha256::Hash::hash(tag.as_bytes());
+       let merkle_root = root_hash(bytes);
+       Message::from_slice(&tagged_hash(tag, merkle_root)).unwrap()
+}
+
 /// Computes a merkle root hash for the given data, which must be a well-formed TLV stream
 /// containing at least one TLV record.
 fn root_hash(data: &[u8]) -> sha256::Hash {
index 1403fbd223ec19ab8888f0e2fdaae8432dcd0cb8..d890ab3f642cfb14fffe70f843b3086f8688fdcb 100644 (file)
@@ -76,6 +76,7 @@ use core::time::Duration;
 use crate::io;
 use crate::ln::features::OfferFeatures;
 use crate::ln::msgs::MAX_VALUE_MSAT;
+use crate::offers::invoice_request::InvoiceRequestBuilder;
 use crate::offers::parse::{Bech32Encode, ParseError, ParsedMessage, SemanticError};
 use crate::onion_message::BlindedPath;
 use crate::util::ser::{HighZeroBytesDroppedBigSize, WithoutLength, Writeable, Writer};
@@ -149,15 +150,6 @@ impl OfferBuilder {
                self
        }
 
-       /// Sets the [`Offer::features`].
-       ///
-       /// Successive calls to this method will override the previous setting.
-       #[cfg(test)]
-       pub fn features(mut self, features: OfferFeatures) -> Self {
-               self.offer.features = features;
-               self
-       }
-
        /// Sets the [`Offer::absolute_expiry`] as seconds since the Unix epoch. Any expiry that has
        /// already passed is valid and can be checked for using [`Offer::is_expired`].
        ///
@@ -222,6 +214,14 @@ impl OfferBuilder {
        }
 }
 
+#[cfg(test)]
+impl OfferBuilder {
+       fn features_unchecked(mut self, features: OfferFeatures) -> Self {
+               self.offer.features = features;
+               self
+       }
+}
+
 /// An `Offer` is a potentially long-lived proposal for payment of a good or service.
 ///
 /// An offer is a precursor to an [`InvoiceRequest`]. A merchant publishes an offer from which a
@@ -238,8 +238,8 @@ impl OfferBuilder {
 pub struct Offer {
        // The serialized offer. Needed when creating an `InvoiceRequest` if the offer contains unknown
        // fields.
-       bytes: Vec<u8>,
-       contents: OfferContents,
+       pub(super) bytes: Vec<u8>,
+       pub(super) contents: OfferContents,
 }
 
 /// The contents of an [`Offer`], which may be shared with an [`InvoiceRequest`] or an `Invoice`.
@@ -270,6 +270,10 @@ impl Offer {
                self.contents.chains()
        }
 
+       pub(super) fn implied_chain(&self) -> ChainHash {
+               self.contents.implied_chain()
+       }
+
        /// Returns whether the given chain is supported by the offer.
        pub fn supports_chain(&self, chain: ChainHash) -> bool {
                self.contents.supports_chain(chain)
@@ -351,6 +355,29 @@ impl Offer {
                self.contents.signing_pubkey.unwrap()
        }
 
+       /// Creates an [`InvoiceRequest`] for the offer with the given `metadata` and `payer_id`, which
+       /// will be reflected in the `Invoice` response.
+       ///
+       /// The `metadata` is useful for including information about the derivation of `payer_id` such
+       /// that invoice response handling can be stateless. Also serves as payer-provided entropy while
+       /// hashing in the signature calculation.
+       ///
+       /// This should not leak any information such as by using a simple BIP-32 derivation path.
+       /// Otherwise, payments may be correlated.
+       ///
+       /// Errors if the offer contains unknown required features.
+       ///
+       /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
+       pub fn request_invoice(
+               &self, metadata: Vec<u8>, payer_id: PublicKey
+       ) -> Result<InvoiceRequestBuilder, SemanticError> {
+               if self.features().requires_unknown_bits() {
+                       return Err(SemanticError::UnknownRequiredFeatures);
+               }
+
+               Ok(InvoiceRequestBuilder::new(self, metadata, payer_id))
+       }
+
        #[cfg(test)]
        fn as_tlv_stream(&self) -> OfferTlvStreamRef {
                self.contents.as_tlv_stream()
@@ -380,23 +407,48 @@ impl OfferContents {
                self.amount.as_ref()
        }
 
-       pub fn amount_msats(&self) -> u64 {
-               match self.amount() {
+       pub(super) fn check_amount_msats_for_quantity(
+               &self, amount_msats: Option<u64>, quantity: Option<u64>
+       ) -> Result<(), SemanticError> {
+               let offer_amount_msats = match self.amount {
                        None => 0,
-                       Some(&Amount::Bitcoin { amount_msats }) => amount_msats,
-                       Some(&Amount::Currency { .. }) => unreachable!(),
+                       Some(Amount::Bitcoin { amount_msats }) => amount_msats,
+                       Some(Amount::Currency { .. }) => return Err(SemanticError::UnsupportedCurrency),
+               };
+
+               if !self.expects_quantity() || quantity.is_some() {
+                       let expected_amount_msats = offer_amount_msats * quantity.unwrap_or(1);
+                       let amount_msats = amount_msats.unwrap_or(expected_amount_msats);
+
+                       if amount_msats < expected_amount_msats {
+                               return Err(SemanticError::InsufficientAmount);
+                       }
+
+                       if amount_msats > MAX_VALUE_MSAT {
+                               return Err(SemanticError::InvalidAmount);
+                       }
                }
-       }
 
-       pub fn expected_invoice_amount_msats(&self, quantity: u64) -> u64 {
-               self.amount_msats() * quantity
+               Ok(())
        }
 
        pub fn supported_quantity(&self) -> Quantity {
                self.supported_quantity
        }
 
-       pub fn is_valid_quantity(&self, quantity: u64) -> bool {
+       pub(super) fn check_quantity(&self, quantity: Option<u64>) -> Result<(), SemanticError> {
+               let expects_quantity = self.expects_quantity();
+               match quantity {
+                       None if expects_quantity => Err(SemanticError::MissingQuantity),
+                       Some(_) if !expects_quantity => Err(SemanticError::UnexpectedQuantity),
+                       Some(quantity) if !self.is_valid_quantity(quantity) => {
+                               Err(SemanticError::InvalidQuantity)
+                       },
+                       _ => Ok(()),
+               }
+       }
+
+       fn is_valid_quantity(&self, quantity: u64) -> bool {
                match self.supported_quantity {
                        Quantity::Bounded(n) => {
                                let n = n.get();
@@ -407,14 +459,14 @@ impl OfferContents {
                }
        }
 
-       pub fn expects_quantity(&self) -> bool {
+       fn expects_quantity(&self) -> bool {
                match self.supported_quantity {
                        Quantity::Bounded(n) => n.get() != 1,
                        Quantity::Unbounded => true,
                }
        }
 
-       fn as_tlv_stream(&self) -> OfferTlvStreamRef {
+       pub(super) fn as_tlv_stream(&self) -> OfferTlvStreamRef {
                let (currency, amount) = match &self.amount {
                        None => (None, None),
                        Some(Amount::Bitcoin { amount_msats }) => (None, Some(*amount_msats)),
@@ -760,15 +812,15 @@ mod tests {
        #[test]
        fn builds_offer_with_features() {
                let offer = OfferBuilder::new("foo".into(), pubkey(42))
-                       .features(OfferFeatures::unknown())
+                       .features_unchecked(OfferFeatures::unknown())
                        .build()
                        .unwrap();
                assert_eq!(offer.features(), &OfferFeatures::unknown());
                assert_eq!(offer.as_tlv_stream().features, Some(&OfferFeatures::unknown()));
 
                let offer = OfferBuilder::new("foo".into(), pubkey(42))
-                       .features(OfferFeatures::unknown())
-                       .features(OfferFeatures::empty())
+                       .features_unchecked(OfferFeatures::unknown())
+                       .features_unchecked(OfferFeatures::empty())
                        .build()
                        .unwrap();
                assert_eq!(offer.features(), &OfferFeatures::empty());
@@ -890,6 +942,18 @@ mod tests {
                assert_eq!(tlv_stream.quantity_max, None);
        }
 
+       #[test]
+       fn fails_requesting_invoice_with_unknown_required_features() {
+               match OfferBuilder::new("foo".into(), pubkey(42))
+                       .features_unchecked(OfferFeatures::unknown())
+                       .build().unwrap()
+                       .request_invoice(vec![1; 32], pubkey(43))
+               {
+                       Ok(_) => panic!("expected error"),
+                       Err(e) => assert_eq!(e, SemanticError::UnknownRequiredFeatures),
+               }
+       }
+
        #[test]
        fn parses_offer_with_chains() {
                let offer = OfferBuilder::new("foo".into(), pubkey(42))
index b9815b8117767938bee0ba544f1dd9df73db891f..0b3dda7928593871f8007c5ff50d2d081366dac8 100644 (file)
@@ -123,6 +123,8 @@ pub enum ParseError {
 /// Error when interpreting a TLV stream as a specific type.
 #[derive(Debug, PartialEq)]
 pub enum SemanticError {
+       /// The current [`std::time::SystemTime`] is past the offer or invoice's expiration.
+       AlreadyExpired,
        /// The provided chain hash does not correspond to a supported chain.
        UnsupportedChain,
        /// An amount was expected but was missing.
@@ -133,6 +135,8 @@ pub enum SemanticError {
        InsufficientAmount,
        /// A currency was provided that is not supported.
        UnsupportedCurrency,
+       /// A feature was required but is unknown.
+       UnknownRequiredFeatures,
        /// A required description was not provided.
        MissingDescription,
        /// A signing pubkey was not provided.