From 0c621646a627a0e43e10fdb7717ba0dbc45893c5 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Mon, 19 Sep 2022 16:57:46 -0500 Subject: [PATCH] Invoice request message interface and data format Define an interface for BOLT 12 `invoice_request` messages. The underlying format consists of the original bytes and the parsed contents. The bytes are later needed when constructing an `invoice` message. This is because it must mirror all the `offer` and `invoice_request` TLV records, including unknown ones, which aren't represented in the contents. The contents will be used in `invoice` messages to avoid duplication. Some fields while required in a typical user-pays-merchant flow may not be necessary in the merchant-pays-user flow (e.g., refund, ATM). --- lightning/src/ln/features.rs | 9 ++- lightning/src/offers/invoice_request.rs | 101 ++++++++++++++++++++++++ lightning/src/offers/mod.rs | 2 + lightning/src/offers/offer.rs | 17 ++-- lightning/src/offers/payer.rs | 19 +++++ 5 files changed, 140 insertions(+), 8 deletions(-) create mode 100644 lightning/src/offers/invoice_request.rs create mode 100644 lightning/src/offers/payer.rs diff --git a/lightning/src/ln/features.rs b/lightning/src/ln/features.rs index 77d0fa45..1f455471 100644 --- a/lightning/src/ln/features.rs +++ b/lightning/src/ln/features.rs @@ -158,6 +158,7 @@ mod sealed { BasicMPP, ]); define_context!(OfferContext, []); + define_context!(InvoiceRequestContext, []); // This isn't a "real" feature context, and is only used in the channel_type field in an // `OpenChannel` message. define_context!(ChannelTypeContext, [ @@ -367,7 +368,8 @@ mod sealed { supports_keysend, requires_keysend); #[cfg(test)] - define_feature!(123456789, UnknownFeature, [NodeContext, ChannelContext, InvoiceContext, OfferContext], + define_feature!(123456789, UnknownFeature, + [NodeContext, ChannelContext, InvoiceContext, OfferContext, InvoiceRequestContext], "Feature flags for an unknown feature used in testing.", set_unknown_feature_optional, set_unknown_feature_required, supports_unknown_test_feature, requires_unknown_test_feature); } @@ -426,8 +428,10 @@ pub type NodeFeatures = Features; pub type ChannelFeatures = Features; /// Features used within an invoice. pub type InvoiceFeatures = Features; -/// Features used within an offer. +/// Features used within an `offer`. pub type OfferFeatures = Features; +/// Features used within an `invoice_request`. +pub type InvoiceRequestFeatures = Features; /// Features used within the channel_type field in an OpenChannel message. /// @@ -735,6 +739,7 @@ macro_rules! impl_feature_tlv_write { impl_feature_tlv_write!(ChannelTypeFeatures); impl_feature_tlv_write!(OfferFeatures); +impl_feature_tlv_write!(InvoiceRequestFeatures); #[cfg(test)] mod tests { diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs new file mode 100644 index 00000000..43ae18d2 --- /dev/null +++ b/lightning/src/offers/invoice_request.rs @@ -0,0 +1,101 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Data structures and encoding for `invoice_request` messages. + +use bitcoin::blockdata::constants::ChainHash; +use bitcoin::secp256k1::PublicKey; +use bitcoin::secp256k1::schnorr::Signature; +use crate::ln::features::InvoiceRequestFeatures; +use crate::offers::offer::OfferContents; +use crate::offers::payer::PayerContents; +use crate::util::string::PrintableString; + +use crate::prelude::*; + +/// 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 +/// specifies these such that its recipient can send an invoice for payment. +/// +/// [`Offer`]: crate::offers::offer::Offer +#[derive(Clone, Debug)] +pub struct InvoiceRequest { + bytes: Vec, + contents: InvoiceRequestContents, + signature: Option, +} + +/// The contents of an [`InvoiceRequest`], which may be shared with an `Invoice`. +#[derive(Clone, Debug)] +pub(crate) struct InvoiceRequestContents { + payer: PayerContents, + offer: OfferContents, + chain: Option, + amount_msats: Option, + features: InvoiceRequestFeatures, + quantity: Option, + payer_id: PublicKey, + payer_note: Option, +} + +impl InvoiceRequest { + /// An unpredictable series of bytes, typically containing information about the derivation of + /// [`payer_id`]. + /// + /// [`payer_id`]: Self::payer_id + pub fn metadata(&self) -> &[u8] { + &self.contents.payer.0[..] + } + + /// 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()) + } + + /// 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 + } + + /// Features for paying the invoice. + pub fn features(&self) -> &InvoiceRequestFeatures { + &self.contents.features + } + + /// The quantity of the offer's item conforming to [`Offer::supported_quantity`]. + /// + /// [`Offer::supported_quantity`]: crate::offers::offer::Offer::supported_quantity + pub fn quantity(&self) -> Option { + self.contents.quantity + } + + /// A possibly transient pubkey used to sign the invoice request. + pub fn payer_id(&self) -> PublicKey { + self.contents.payer_id + } + + /// Payer provided note to include in the invoice. + pub fn payer_note(&self) -> Option { + self.contents.payer_note.as_ref().map(|payer_note| PrintableString(payer_note.as_str())) + } + + /// Signature of the invoice request using [`payer_id`]. + /// + /// [`payer_id`]: Self::payer_id + pub fn signature(&self) -> Option { + self.signature + } +} diff --git a/lightning/src/offers/mod.rs b/lightning/src/offers/mod.rs index 27365028..a58903f7 100644 --- a/lightning/src/offers/mod.rs +++ b/lightning/src/offers/mod.rs @@ -12,5 +12,7 @@ //! //! Offers are a flexible protocol for Lightning payments. +pub mod invoice_request; pub mod offer; pub mod parse; +mod payer; diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index c31ca9bb..1ac8b0bd 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -224,7 +224,7 @@ impl OfferBuilder { /// 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 +/// An offer is a precursor to an [`InvoiceRequest`]. A merchant publishes an offer from which a /// customer may request an `Invoice` for a specific quantity and using an amount sufficient to /// cover that quantity (i.e., at least `quantity * amount`). See [`Offer::amount`]. /// @@ -232,6 +232,8 @@ impl OfferBuilder { /// latter. /// /// Through the use of [`BlindedPath`]s, offers provide recipient privacy. +/// +/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest #[derive(Clone, Debug)] pub struct Offer { // The serialized offer. Needed when creating an `InvoiceRequest` if the offer contains unknown @@ -240,7 +242,9 @@ pub struct Offer { contents: OfferContents, } -/// The contents of an [`Offer`], which may be shared with an `InvoiceRequest` or an `Invoice`. +/// The contents of an [`Offer`], which may be shared with an [`InvoiceRequest`] or an `Invoice`. +/// +/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest #[derive(Clone, Debug)] pub(super) struct OfferContents { chains: Option>, @@ -263,10 +267,7 @@ impl Offer { /// Payments must be denominated in units of the minimal lightning-payable unit (e.g., msats) /// for the selected chain. pub fn chains(&self) -> Vec { - self.contents.chains - .as_ref() - .cloned() - .unwrap_or_else(|| vec![self.contents.implied_chain()]) + self.contents.chains() } // TODO: Link to corresponding method in `InvoiceRequest`. @@ -346,6 +347,10 @@ impl AsRef<[u8]> for Offer { } impl OfferContents { + pub fn chains(&self) -> Vec { + self.chains.as_ref().cloned().unwrap_or_else(|| vec![self.implied_chain()]) + } + pub fn implied_chain(&self) -> ChainHash { ChainHash::using_genesis_block(Network::Bitcoin) } diff --git a/lightning/src/offers/payer.rs b/lightning/src/offers/payer.rs new file mode 100644 index 00000000..1705be85 --- /dev/null +++ b/lightning/src/offers/payer.rs @@ -0,0 +1,19 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Data structures and encoding for `invoice_request_metadata` records. + +use crate::prelude::*; + +/// An unpredictable sequence of bytes typically containing information needed to derive +/// [`InvoiceRequest::payer_id`]. +/// +/// [`InvoiceRequest::payer_id`]: crate::offers::invoice_request::InvoiceRequest::payer_id +#[derive(Clone, Debug)] +pub(crate) struct PayerContents(pub Vec); -- 2.30.2