Builder for creating invoices for offers
authorJeffrey Czyz <jkczyz@gmail.com>
Tue, 20 Dec 2022 04:23:39 +0000 (22:23 -0600)
committerJeffrey Czyz <jkczyz@gmail.com>
Fri, 20 Jan 2023 22:04:37 +0000 (16:04 -0600)
Add a builder for creating invoices for an offer from a given request
and required fields. Other settings are optional and duplicative
settings will override previous settings. Building produces a
semantically valid `invoice` message for the offer, which then can be
signed with the key associated with the offer's signing pubkey.

lightning/src/offers/invoice.rs
lightning/src/offers/invoice_request.rs
lightning/src/offers/offer.rs
lightning/src/offers/refund.rs
lightning/src/util/ser.rs
lightning/src/util/ser_macros.rs

index 8264aa15fb31da48d9a280dc1d2f293423ba74a7..a937a5483c93cd48b55128b590998174807bad36 100644 (file)
 //! Data structures and encoding for `invoice` messages.
 
 use bitcoin::blockdata::constants::ChainHash;
+use bitcoin::hash_types::{WPubkeyHash, WScriptHash};
+use bitcoin::hashes::Hash;
 use bitcoin::network::constants::Network;
-use bitcoin::secp256k1::PublicKey;
+use bitcoin::secp256k1::{Message, PublicKey};
 use bitcoin::secp256k1::schnorr::Signature;
 use bitcoin::util::address::{Address, Payload, WitnessVersion};
+use bitcoin::util::schnorr::TweakedPublicKey;
 use core::convert::TryFrom;
 use core::time::Duration;
 use crate::io;
 use crate::ln::PaymentHash;
 use crate::ln::features::{BlindedHopFeatures, Bolt12InvoiceFeatures};
 use crate::ln::msgs::DecodeError;
-use crate::offers::invoice_request::{InvoiceRequestContents, InvoiceRequestTlvStream};
-use crate::offers::merkle::{SignatureTlvStream, self};
-use crate::offers::offer::OfferTlvStream;
+use crate::offers::invoice_request::{InvoiceRequest, InvoiceRequestContents, InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef};
+use crate::offers::merkle::{SignError, SignatureTlvStream, SignatureTlvStreamRef, WithoutSignatures, self};
+use crate::offers::offer::{Amount, OfferTlvStream, OfferTlvStreamRef};
 use crate::offers::parse::{ParseError, ParsedMessage, SemanticError};
-use crate::offers::payer::PayerTlvStream;
+use crate::offers::payer::{PayerTlvStream, PayerTlvStreamRef};
 use crate::offers::refund::RefundContents;
 use crate::onion_message::BlindedPath;
-use crate::util::ser::{HighZeroBytesDroppedBigSize, SeekReadable, WithoutLength, Writeable, Writer};
+use crate::util::ser::{HighZeroBytesDroppedBigSize, Iterable, SeekReadable, WithoutLength, Writeable, Writer};
 
 use crate::prelude::*;
 
@@ -38,6 +41,161 @@ const DEFAULT_RELATIVE_EXPIRY: Duration = Duration::from_secs(7200);
 
 const SIGNATURE_TAG: &'static str = concat!("lightning", "invoice", "signature");
 
+/// Builds an [`Invoice`] from either:
+/// - an [`InvoiceRequest`] for the "offer to be paid" flow or
+/// - a [`Refund`] for the "offer for money" flow.
+///
+/// See [module-level documentation] for usage.
+///
+/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
+/// [`Refund`]: crate::offers::refund::Refund
+/// [module-level documentation]: self
+pub struct InvoiceBuilder<'a> {
+       invreq_bytes: &'a Vec<u8>,
+       invoice: InvoiceContents,
+}
+
+impl<'a> InvoiceBuilder<'a> {
+       pub(super) fn for_offer(
+               invoice_request: &'a InvoiceRequest, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>,
+               created_at: Duration, payment_hash: PaymentHash
+       ) -> Result<Self, SemanticError> {
+               if payment_paths.is_empty() {
+                       return Err(SemanticError::MissingPaths);
+               }
+
+               let amount_msats = match invoice_request.amount_msats() {
+                       Some(amount_msats) => amount_msats,
+                       None => match invoice_request.contents.offer.amount() {
+                               Some(Amount::Bitcoin { amount_msats }) => {
+                                       amount_msats * invoice_request.quantity().unwrap_or(1)
+                               },
+                               Some(Amount::Currency { .. }) => return Err(SemanticError::UnsupportedCurrency),
+                               None => return Err(SemanticError::MissingAmount),
+                       },
+               };
+
+               Ok(Self {
+                       invreq_bytes: &invoice_request.bytes,
+                       invoice: InvoiceContents::ForOffer {
+                               invoice_request: invoice_request.contents.clone(),
+                               fields: InvoiceFields {
+                                       payment_paths, created_at, relative_expiry: None, payment_hash, amount_msats,
+                                       fallbacks: None, features: Bolt12InvoiceFeatures::empty(),
+                                       signing_pubkey: invoice_request.contents.offer.signing_pubkey(),
+                               },
+                       },
+               })
+       }
+
+       /// Sets the [`Invoice::relative_expiry`] as seconds since [`Invoice::created_at`]. Any expiry
+       /// that has already passed is valid and can be checked for using [`Invoice::is_expired`].
+       ///
+       /// Successive calls to this method will override the previous setting.
+       pub fn relative_expiry(mut self, relative_expiry_secs: u32) -> Self {
+               let relative_expiry = Duration::from_secs(relative_expiry_secs as u64);
+               self.invoice.fields_mut().relative_expiry = Some(relative_expiry);
+               self
+       }
+
+       /// Adds a P2WSH address to [`Invoice::fallbacks`].
+       ///
+       /// Successive calls to this method will add another address. Caller is responsible for not
+       /// adding duplicate addresses and only calling if capable of receiving to P2WSH addresses.
+       pub fn fallback_v0_p2wsh(mut self, script_hash: &WScriptHash) -> Self {
+               let address = FallbackAddress {
+                       version: WitnessVersion::V0.to_num(),
+                       program: Vec::from(&script_hash.into_inner()[..]),
+               };
+               self.invoice.fields_mut().fallbacks.get_or_insert_with(Vec::new).push(address);
+               self
+       }
+
+       /// Adds a P2WPKH address to [`Invoice::fallbacks`].
+       ///
+       /// Successive calls to this method will add another address. Caller is responsible for not
+       /// adding duplicate addresses and only calling if capable of receiving to P2WPKH addresses.
+       pub fn fallback_v0_p2wpkh(mut self, pubkey_hash: &WPubkeyHash) -> Self {
+               let address = FallbackAddress {
+                       version: WitnessVersion::V0.to_num(),
+                       program: Vec::from(&pubkey_hash.into_inner()[..]),
+               };
+               self.invoice.fields_mut().fallbacks.get_or_insert_with(Vec::new).push(address);
+               self
+       }
+
+       /// Adds a P2TR address to [`Invoice::fallbacks`].
+       ///
+       /// Successive calls to this method will add another address. Caller is responsible for not
+       /// adding duplicate addresses and only calling if capable of receiving to P2TR addresses.
+       pub fn fallback_v1_p2tr_tweaked(mut self, output_key: &TweakedPublicKey) -> Self {
+               let address = FallbackAddress {
+                       version: WitnessVersion::V1.to_num(),
+                       program: Vec::from(&output_key.serialize()[..]),
+               };
+               self.invoice.fields_mut().fallbacks.get_or_insert_with(Vec::new).push(address);
+               self
+       }
+
+       /// Sets [`Invoice::features`] to indicate MPP may be used. Otherwise, MPP is disallowed.
+       pub fn allow_mpp(mut self) -> Self {
+               self.invoice.fields_mut().features.set_basic_mpp_optional();
+               self
+       }
+
+       /// Builds an unsigned [`Invoice`] after checking for valid semantics. It can be signed by
+       /// [`UnsignedInvoice::sign`].
+       pub fn build(self) -> Result<UnsignedInvoice<'a>, SemanticError> {
+               #[cfg(feature = "std")] {
+                       if self.invoice.is_offer_or_refund_expired() {
+                               return Err(SemanticError::AlreadyExpired);
+                       }
+               }
+
+               let InvoiceBuilder { invreq_bytes, invoice } = self;
+               Ok(UnsignedInvoice { invreq_bytes, invoice })
+       }
+}
+
+/// A semantically valid [`Invoice`] that hasn't been signed.
+pub struct UnsignedInvoice<'a> {
+       invreq_bytes: &'a Vec<u8>,
+       invoice: InvoiceContents,
+}
+
+impl<'a> UnsignedInvoice<'a> {
+       /// Signs the invoice using the given function.
+       pub fn sign<F, E>(self, sign: F) -> Result<Invoice, SignError<E>>
+       where
+               F: FnOnce(&Message) -> Result<Signature, E>
+       {
+               // Use the invoice_request bytes instead of the invoice_request TLV stream as the latter may
+               // have contained unknown TLV records, which are not stored in `InvoiceRequestContents` or
+               // `RefundContents`.
+               let (_, _, _, invoice_tlv_stream) = self.invoice.as_tlv_stream();
+               let invoice_request_bytes = WithoutSignatures(self.invreq_bytes);
+               let unsigned_tlv_stream = (invoice_request_bytes, invoice_tlv_stream);
+
+               let mut bytes = Vec::new();
+               unsigned_tlv_stream.write(&mut bytes).unwrap();
+
+               let pubkey = self.invoice.fields().signing_pubkey;
+               let signature = merkle::sign_message(sign, SIGNATURE_TAG, &bytes, pubkey)?;
+
+               // Append the signature TLV record to the bytes.
+               let signature_tlv_stream = SignatureTlvStreamRef {
+                       signature: Some(&signature),
+               };
+               signature_tlv_stream.write(&mut bytes).unwrap();
+
+               Ok(Invoice {
+                       bytes,
+                       contents: self.invoice,
+                       signature,
+               })
+       }
+}
+
 /// An `Invoice` is a payment request, typically corresponding to an [`Offer`] or a [`Refund`].
 ///
 /// An invoice may be sent in response to an [`InvoiceRequest`] in the case of an offer or sent
@@ -199,6 +357,15 @@ impl Invoice {
 }
 
 impl InvoiceContents {
+       /// Whether the original offer or refund has expired.
+       #[cfg(feature = "std")]
+       fn is_offer_or_refund_expired(&self) -> bool {
+               match self {
+                       InvoiceContents::ForOffer { invoice_request, .. } => invoice_request.offer.is_expired(),
+                       InvoiceContents::ForRefund { refund, .. } => refund.is_expired(),
+               }
+       }
+
        fn chain(&self) -> ChainHash {
                match self {
                        InvoiceContents::ForOffer { invoice_request, .. } => invoice_request.chain(),
@@ -212,6 +379,44 @@ impl InvoiceContents {
                        InvoiceContents::ForRefund { fields, .. } => fields,
                }
        }
+
+       fn fields_mut(&mut self) -> &mut InvoiceFields {
+               match self {
+                       InvoiceContents::ForOffer { fields, .. } => fields,
+                       InvoiceContents::ForRefund { fields, .. } => fields,
+               }
+       }
+
+       fn as_tlv_stream(&self) -> PartialInvoiceTlvStreamRef {
+               let (payer, offer, invoice_request) = match self {
+                       InvoiceContents::ForOffer { invoice_request, .. } => invoice_request.as_tlv_stream(),
+                       InvoiceContents::ForRefund { refund, .. } => refund.as_tlv_stream(),
+               };
+               let invoice = self.fields().as_tlv_stream();
+
+               (payer, offer, invoice_request, invoice)
+       }
+}
+
+impl InvoiceFields {
+       fn as_tlv_stream(&self) -> InvoiceTlvStreamRef {
+               let features = {
+                       if self.features == Bolt12InvoiceFeatures::empty() { None }
+                       else { Some(&self.features) }
+               };
+
+               InvoiceTlvStreamRef {
+                       paths: Some(Iterable(self.payment_paths.iter().map(|(path, _)| path))),
+                       blindedpay: Some(Iterable(self.payment_paths.iter().map(|(_, payinfo)| payinfo))),
+                       created_at: Some(self.created_at.as_secs()),
+                       relative_expiry: self.relative_expiry.map(|duration| duration.as_secs() as u32),
+                       payment_hash: Some(&self.payment_hash),
+                       amount: Some(self.amount_msats),
+                       fallbacks: self.fallbacks.as_ref(),
+                       features,
+                       node_id: Some(&self.signing_pubkey),
+               }
+       }
 }
 
 impl Writeable for Invoice {
@@ -230,8 +435,8 @@ impl TryFrom<Vec<u8>> for Invoice {
 }
 
 tlv_stream!(InvoiceTlvStream, InvoiceTlvStreamRef, 160..240, {
-       (160, paths: (Vec<BlindedPath>, WithoutLength)),
-       (162, blindedpay: (Vec<BlindedPayInfo>, WithoutLength)),
+       (160, paths: (Vec<BlindedPath>, WithoutLength, Iterable<'a, BlindedPathIter<'a>, BlindedPath>)),
+       (162, blindedpay: (Vec<BlindedPayInfo>, WithoutLength, Iterable<'a, BlindedPayInfoIter<'a>, BlindedPayInfo>)),
        (164, created_at: (u64, HighZeroBytesDroppedBigSize)),
        (166, relative_expiry: (u32, HighZeroBytesDroppedBigSize)),
        (168, payment_hash: PaymentHash),
@@ -241,7 +446,17 @@ tlv_stream!(InvoiceTlvStream, InvoiceTlvStreamRef, 160..240, {
        (176, node_id: PublicKey),
 });
 
-/// Information needed to route a payment across a [`BlindedPath`] hop.
+type BlindedPathIter<'a> = core::iter::Map<
+       core::slice::Iter<'a, (BlindedPath, BlindedPayInfo)>,
+       for<'r> fn(&'r (BlindedPath, BlindedPayInfo)) -> &'r BlindedPath,
+>;
+
+type BlindedPayInfoIter<'a> = core::iter::Map<
+       core::slice::Iter<'a, (BlindedPath, BlindedPayInfo)>,
+       for<'r> fn(&'r (BlindedPath, BlindedPayInfo)) -> &'r BlindedPayInfo,
+>;
+
+/// Information needed to route a payment across a [`BlindedPath`].
 #[derive(Debug, PartialEq)]
 pub struct BlindedPayInfo {
        fee_base_msat: u32,
@@ -288,6 +503,13 @@ impl SeekReadable for FullInvoiceTlvStream {
 type PartialInvoiceTlvStream =
        (PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream, InvoiceTlvStream);
 
+type PartialInvoiceTlvStreamRef<'a> = (
+       PayerTlvStreamRef<'a>,
+       OfferTlvStreamRef<'a>,
+       InvoiceRequestTlvStreamRef<'a>,
+       InvoiceTlvStreamRef<'a>,
+);
+
 impl TryFrom<ParsedMessage<FullInvoiceTlvStream>> for Invoice {
        type Error = ParseError;
 
index 21c11bcbfc6b2429b078807e5b8682fe02667f8a..126d9b552decf689bee53bae7a2c82a9bf681be5 100644 (file)
@@ -57,13 +57,17 @@ use bitcoin::network::constants::Network;
 use bitcoin::secp256k1::{Message, PublicKey};
 use bitcoin::secp256k1::schnorr::Signature;
 use core::convert::TryFrom;
+use core::time::Duration;
 use crate::io;
+use crate::ln::PaymentHash;
 use crate::ln::features::InvoiceRequestFeatures;
 use crate::ln::msgs::DecodeError;
+use crate::offers::invoice::{BlindedPayInfo, InvoiceBuilder};
 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, PayerTlvStreamRef};
+use crate::onion_message::BlindedPath;
 use crate::util::ser::{HighZeroBytesDroppedBigSize, SeekReadable, WithoutLength, Writeable, Writer};
 use crate::util::string::PrintableString;
 
@@ -250,7 +254,7 @@ impl<'a> UnsignedInvoiceRequest<'a> {
 #[derive(Clone, Debug)]
 pub struct InvoiceRequest {
        pub(super) bytes: Vec<u8>,
-       contents: InvoiceRequestContents,
+       pub(super) contents: InvoiceRequestContents,
        signature: Signature,
 }
 
@@ -319,6 +323,29 @@ impl InvoiceRequest {
                self.signature
        }
 
+       /// Creates an [`Invoice`] for the request with the given required fields.
+       ///
+       /// Unless [`InvoiceBuilder::relative_expiry`] is set, the invoice will expire two hours after
+       /// `created_at`. The caller is expected to remember the preimage of `payment_hash` in order to
+       /// claim a payment for the invoice.
+       ///
+       /// The `payment_paths` parameter is useful for maintaining the payment recipient's privacy. It
+       /// must contain one or more elements.
+       ///
+       /// Errors if the request contains unknown required features.
+       ///
+       /// [`Invoice`]: crate::offers::invoice::Invoice
+       pub fn respond_with(
+               &self, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, created_at: Duration,
+               payment_hash: PaymentHash
+       ) -> Result<InvoiceBuilder, SemanticError> {
+               if self.features().requires_unknown_bits() {
+                       return Err(SemanticError::UnknownRequiredFeatures);
+               }
+
+               InvoiceBuilder::for_offer(self, payment_paths, created_at, payment_hash)
+       }
+
        #[cfg(test)]
        fn as_tlv_stream(&self) -> FullInvoiceRequestTlvStreamRef {
                let (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream) =
index d28a0bdd90ef3041613620972d09373a146764c1..d92d0d8bb4ca9ec3812fc6ad3dd37b5b7b948f9e 100644 (file)
@@ -321,13 +321,7 @@ impl Offer {
        /// Whether the offer has expired.
        #[cfg(feature = "std")]
        pub fn is_expired(&self) -> bool {
-               match self.absolute_expiry() {
-                       Some(seconds_from_epoch) => match SystemTime::UNIX_EPOCH.elapsed() {
-                               Ok(elapsed) => elapsed > seconds_from_epoch,
-                               Err(_) => false,
-                       },
-                       None => false,
-               }
+               self.contents.is_expired()
        }
 
        /// The issuer of the offer, possibly beginning with `user@domain` or `domain`. Intended to be
@@ -412,6 +406,17 @@ impl OfferContents {
                self.chains().contains(&chain)
        }
 
+       #[cfg(feature = "std")]
+       pub(super) fn is_expired(&self) -> bool {
+               match self.absolute_expiry {
+                       Some(seconds_from_epoch) => match SystemTime::UNIX_EPOCH.elapsed() {
+                               Ok(elapsed) => elapsed > seconds_from_epoch,
+                               Err(_) => false,
+                       },
+                       None => false,
+               }
+       }
+
        pub fn amount(&self) -> Option<&Amount> {
                self.amount.as_ref()
        }
index f864e73a3d37f9d7052f5a818d7e176ebc2c7464..fdf763ecb952653dfadea965a5978fe5dd8e841e 100644 (file)
@@ -243,13 +243,7 @@ impl Refund {
        /// Whether the refund has expired.
        #[cfg(feature = "std")]
        pub fn is_expired(&self) -> bool {
-               match self.absolute_expiry() {
-                       Some(seconds_from_epoch) => match SystemTime::UNIX_EPOCH.elapsed() {
-                               Ok(elapsed) => elapsed > seconds_from_epoch,
-                               Err(_) => false,
-                       },
-                       None => false,
-               }
+               self.contents.is_expired()
        }
 
        /// The issuer of the refund, possibly beginning with `user@domain` or `domain`. Intended to be
@@ -315,6 +309,17 @@ impl AsRef<[u8]> for Refund {
 }
 
 impl RefundContents {
+       #[cfg(feature = "std")]
+       pub(super) fn is_expired(&self) -> bool {
+               match self.absolute_expiry {
+                       Some(seconds_from_epoch) => match SystemTime::UNIX_EPOCH.elapsed() {
+                               Ok(elapsed) => elapsed > seconds_from_epoch,
+                               Err(_) => false,
+                       },
+                       None => false,
+               }
+       }
+
        pub(super) fn chain(&self) -> ChainHash {
                self.chain.unwrap_or_else(|| self.implied_chain())
        }
index 84d1a2e084feb65e1472969875a586c81ccd08e5..ebe20677cda310bb2fea4934ecb08d5fe4db8c97 100644 (file)
@@ -624,6 +624,26 @@ impl<'a, T> From<&'a Vec<T>> for WithoutLength<&'a Vec<T>> {
        fn from(v: &'a Vec<T>) -> Self { Self(v) }
 }
 
+#[derive(Debug)]
+pub(crate) struct Iterable<'a, I: Iterator<Item = &'a T> + Clone, T: 'a>(pub I);
+
+impl<'a, I: Iterator<Item = &'a T> + Clone, T: 'a + Writeable> Writeable for Iterable<'a, I, T> {
+       #[inline]
+       fn write<W: Writer>(&self, writer: &mut W) -> Result<(), io::Error> {
+               for ref v in self.0.clone() {
+                       v.write(writer)?;
+               }
+               Ok(())
+       }
+}
+
+#[cfg(test)]
+impl<'a, I: Iterator<Item = &'a T> + Clone, T: 'a + PartialEq> PartialEq for Iterable<'a, I, T> {
+       fn eq(&self, other: &Self) -> bool {
+               self.0.clone().collect::<Vec<_>>() == other.0.clone().collect::<Vec<_>>()
+       }
+}
+
 macro_rules! impl_for_map {
        ($ty: ident, $keybound: ident, $constr: expr) => {
                impl<K, V> Writeable for $ty<K, V>
index afd7fcb2e0fc93b3f536a052c15cec841f8b43b0..373a64e3e0e935d8a63b499041ac1f47064fbc76 100644 (file)
@@ -286,6 +286,9 @@ macro_rules! _decode_tlv {
        ($reader: expr, $field: ident, (option: $trait: ident $(, $read_arg: expr)?)) => {{
                $field = Some($trait::read(&mut $reader $(, $read_arg)*)?);
        }};
+       ($reader: expr, $field: ident, (option, encoding: ($fieldty: ty, $encoding: ident, $encoder:ty))) => {{
+               $crate::_decode_tlv!($reader, $field, (option, encoding: ($fieldty, $encoding)));
+       }};
        ($reader: expr, $field: ident, (option, encoding: ($fieldty: ty, $encoding: ident))) => {{
                $field = {
                        let field: $encoding<$fieldty> = ser::Readable::read(&mut $reader)?;
@@ -730,7 +733,8 @@ macro_rules! tlv_stream {
                        )*
                }
 
-               #[derive(Debug, PartialEq)]
+               #[cfg_attr(test, derive(PartialEq))]
+               #[derive(Debug)]
                pub(super) struct $nameref<'a> {
                        $(
                                pub(super) $field: Option<tlv_record_ref_type!($fieldty)>,
@@ -770,6 +774,7 @@ macro_rules! tlv_stream {
 
 macro_rules! tlv_record_type {
        (($type:ty, $wrapper:ident)) => { $type };
+       (($type:ty, $wrapper:ident, $encoder:ty)) => { $type };
        ($type:ty) => { $type };
 }
 
@@ -780,6 +785,7 @@ macro_rules! tlv_record_ref_type {
        ((u32, $wrapper: ident)) => { u32 };
        ((u64, $wrapper: ident)) => { u64 };
        (($type:ty, $wrapper:ident)) => { &'a $type };
+       (($type:ty, $wrapper:ident, $encoder:ty)) => { $encoder };
        ($type:ty) => { &'a $type };
 }