// 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
}
/// 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
}
}
/// 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
}
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()))
}
}
}
+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)),
type PartialInvoiceRequestTlvStream = (PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream);
+type PartialInvoiceRequestTlvStreamRef<'a> = (
+ PayerTlvStreamRef<'a>,
+ OfferTlvStreamRef<'a>,
+ InvoiceRequestTlvStreamRef<'a>,
+);
+
impl TryFrom<Vec<u8>> for InvoiceRequest {
type Error = ParseError;
)?;
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 })
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),
};
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)),
+ }
+ }
+}
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};
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`].
///
}
}
+#[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
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`.
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)
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()
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();
}
}
- 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)),
#[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());
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))