From 88c5197e4445166c4dd8307b9c6903ef4990c70f Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Mon, 19 Dec 2022 22:23:39 -0600 Subject: [PATCH] Builder for creating invoices for offers 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 | 240 +++++++++++++++++++++++- lightning/src/offers/invoice_request.rs | 29 ++- lightning/src/offers/offer.rs | 19 +- lightning/src/offers/refund.rs | 19 +- lightning/src/util/ser.rs | 20 ++ lightning/src/util/ser_macros.rs | 8 +- 6 files changed, 310 insertions(+), 25 deletions(-) diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index 8264aa15f..a937a5483 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -10,24 +10,27 @@ //! 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, + 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 { + 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, 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, + invoice: InvoiceContents, +} + +impl<'a> UnsignedInvoice<'a> { + /// Signs the invoice using the given function. + pub fn sign(self, sign: F) -> Result> + where + F: FnOnce(&Message) -> Result + { + // 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> for Invoice { } tlv_stream!(InvoiceTlvStream, InvoiceTlvStreamRef, 160..240, { - (160, paths: (Vec, WithoutLength)), - (162, blindedpay: (Vec, WithoutLength)), + (160, paths: (Vec, WithoutLength, Iterable<'a, BlindedPathIter<'a>, BlindedPath>)), + (162, blindedpay: (Vec, 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> for Invoice { type Error = ParseError; diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index 21c11bcbf..126d9b552 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -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, - 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 { + 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) = diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index d28a0bdd9..d92d0d8bb 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -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() } diff --git a/lightning/src/offers/refund.rs b/lightning/src/offers/refund.rs index f864e73a3..fdf763ecb 100644 --- a/lightning/src/offers/refund.rs +++ b/lightning/src/offers/refund.rs @@ -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()) } diff --git a/lightning/src/util/ser.rs b/lightning/src/util/ser.rs index 84d1a2e08..ebe20677c 100644 --- a/lightning/src/util/ser.rs +++ b/lightning/src/util/ser.rs @@ -624,6 +624,26 @@ impl<'a, T> From<&'a Vec> for WithoutLength<&'a Vec> { fn from(v: &'a Vec) -> Self { Self(v) } } +#[derive(Debug)] +pub(crate) struct Iterable<'a, I: Iterator + Clone, T: 'a>(pub I); + +impl<'a, I: Iterator + Clone, T: 'a + Writeable> Writeable for Iterable<'a, I, T> { + #[inline] + fn write(&self, writer: &mut W) -> Result<(), io::Error> { + for ref v in self.0.clone() { + v.write(writer)?; + } + Ok(()) + } +} + +#[cfg(test)] +impl<'a, I: Iterator + Clone, T: 'a + PartialEq> PartialEq for Iterable<'a, I, T> { + fn eq(&self, other: &Self) -> bool { + self.0.clone().collect::>() == other.0.clone().collect::>() + } +} + macro_rules! impl_for_map { ($ty: ident, $keybound: ident, $constr: expr) => { impl Writeable for $ty diff --git a/lightning/src/util/ser_macros.rs b/lightning/src/util/ser_macros.rs index afd7fcb2e..373a64e3e 100644 --- a/lightning/src/util/ser_macros.rs +++ b/lightning/src/util/ser_macros.rs @@ -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, @@ -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 }; } -- 2.39.5