From: Jeffrey Czyz Date: Wed, 31 Aug 2022 15:19:44 +0000 (-0500) Subject: Builder for creating invoice requests X-Git-Tag: v0.0.113~8^2~3 X-Git-Url: http://git.bitcoin.ninja/?a=commitdiff_plain;h=13ba7cc5233bf7ed692adebefee0b352d4800ab3;p=rust-lightning Builder for creating invoice requests 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. --- diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index 6cedf3eda..dc7d590b7 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -8,23 +8,208 @@ // 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::()? +//! .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, 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 { + 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.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.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, 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(self, sign: F) -> Result> + where + F: FnOnce(&Message) -> Result + { + // 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 { 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 { 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 { 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(&self, writer: &mut W) -> Result<(), io::Error> { WithoutLength(&self.bytes).write(writer) } } +impl Writeable for InvoiceRequestContents { + fn write(&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> for InvoiceRequest { type Error = ParseError; @@ -152,8 +375,7 @@ impl TryFrom> 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 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 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)), + } + } +} diff --git a/lightning/src/offers/merkle.rs b/lightning/src/offers/merkle.rs index 95183bea2..d34a2a073 100644 --- a/lightning/src/offers/merkle.rs +++ b/lightning/src/offers/merkle.rs @@ -24,6 +24,35 @@ tlv_stream!(SignatureTlvStream, SignatureTlvStreamRef, SIGNATURE_TYPES, { (240, signature: Signature), }); +/// Error when signing messages. +#[derive(Debug)] +pub enum SignError { + /// 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( + sign: F, tag: &str, bytes: &[u8], pubkey: PublicKey, +) -> Result> +where + F: FnOnce(&Message) -> Result +{ + 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 { diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index 1403fbd22..d890ab3f6 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -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, - contents: OfferContents, + pub(super) bytes: Vec, + 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, payer_id: PublicKey + ) -> Result { + 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, quantity: Option + ) -> 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) -> 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)) diff --git a/lightning/src/offers/parse.rs b/lightning/src/offers/parse.rs index b9815b811..0b3dda792 100644 --- a/lightning/src/offers/parse.rs +++ b/lightning/src/offers/parse.rs @@ -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.