Stateless offer and refund builder utilities
authorJeffrey Czyz <jkczyz@gmail.com>
Tue, 14 Feb 2023 03:54:37 +0000 (21:54 -0600)
committerJeffrey Czyz <jkczyz@gmail.com>
Wed, 18 Oct 2023 23:33:14 +0000 (18:33 -0500)
Add utility functions to ChannelManager for creating OfferBuilder,
and RefundBuilder such that derived keys are used for the signing
pubkey and payer id, respectively. This allows for stateless
verification of any InvoiceRequest and Invoice messages.

Later, blinded paths can be included in the returned builders.

Also tracks future payments using the given PaymentId such that the
corresponding Invoice is paid only once.

lightning/src/ln/channelmanager.rs
lightning/src/ln/outbound_payment.rs
lightning/src/offers/offer.rs
lightning/src/offers/parse.rs
lightning/src/offers/refund.rs

index 3abbd477910bfe79a5fe59a7057432af57f36d7c..3810cfb8171eeca169e29eb5b36f68ca5b23fdfe 100644 (file)
@@ -55,6 +55,9 @@ use crate::ln::msgs::{ChannelMessageHandler, DecodeError, LightningError};
 use crate::ln::outbound_payment;
 use crate::ln::outbound_payment::{OutboundPayments, PaymentAttempts, PendingOutboundPayment, SendAlongPathArgs};
 use crate::ln::wire::Encode;
+use crate::offers::offer::{DerivedMetadata, OfferBuilder};
+use crate::offers::parse::Bolt12SemanticError;
+use crate::offers::refund::RefundBuilder;
 use crate::sign::{EntropySource, KeysManager, NodeSigner, Recipient, SignerProvider, WriteableEcdsaChannelSigner};
 use crate::util::config::{UserConfig, ChannelConfig, ChannelConfigUpdate};
 use crate::util::wakers::{Future, Notifier};
@@ -7123,6 +7126,59 @@ where
                }
        }
 
+       /// Creates an [`OfferBuilder`] such that the [`Offer`] it builds is recognized by the
+       /// [`ChannelManager`] when handling [`InvoiceRequest`] messages for the offer. The offer will
+       /// not have an expiration unless otherwise set on the builder.
+       ///
+       /// [`Offer`]: crate::offers::offer::Offer
+       /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
+       pub fn create_offer_builder(
+               &self, description: String
+       ) -> OfferBuilder<DerivedMetadata, secp256k1::All> {
+               let node_id = self.get_our_node_id();
+               let expanded_key = &self.inbound_payment_key;
+               let entropy = &*self.entropy_source;
+               let secp_ctx = &self.secp_ctx;
+
+               // TODO: Set blinded paths
+               OfferBuilder::deriving_signing_pubkey(description, node_id, expanded_key, entropy, secp_ctx)
+                       .chain_hash(self.chain_hash)
+       }
+
+       /// Creates a [`RefundBuilder`] such that the [`Refund`] it builds is recognized by the
+       /// [`ChannelManager`] when handling [`Bolt12Invoice`] messages for the refund. The builder will
+       /// have the provided expiration set. Any changes to the expiration on the returned builder will
+       /// not be honored by [`ChannelManager`].
+       ///
+       /// The provided `payment_id` is used to ensure that only one invoice is paid for the refund.
+       ///
+       /// [`Refund`]: crate::offers::refund::Refund
+       /// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice
+       pub fn create_refund_builder(
+               &self, description: String, amount_msats: u64, absolute_expiry: Duration,
+               payment_id: PaymentId, retry_strategy: Retry, max_total_routing_fee_msat: Option<u64>
+       ) -> Result<RefundBuilder<secp256k1::All>, Bolt12SemanticError> {
+               let node_id = self.get_our_node_id();
+               let expanded_key = &self.inbound_payment_key;
+               let entropy = &*self.entropy_source;
+               let secp_ctx = &self.secp_ctx;
+
+               // TODO: Set blinded paths
+               let builder = RefundBuilder::deriving_payer_id(
+                       description, node_id, expanded_key, entropy, secp_ctx, amount_msats, payment_id
+               )?
+                       .chain_hash(self.chain_hash)
+                       .absolute_expiry(absolute_expiry);
+
+               self.pending_outbound_payments
+                       .add_new_awaiting_invoice(
+                               payment_id, absolute_expiry, retry_strategy, max_total_routing_fee_msat,
+                       )
+                       .map_err(|_| Bolt12SemanticError::DuplicatePaymentId)?;
+
+               Ok(builder)
+       }
+
        /// Gets a payment secret and payment hash for use in an invoice given to a third party wishing
        /// to pay us.
        ///
index d679036c1d69a25153b500ffea06ccc4b2efcfc9..19faad07bbd518f4de55a442c52067905a5959cd 100644 (file)
@@ -1268,8 +1268,7 @@ impl OutboundPayments {
                (payment, onion_session_privs)
        }
 
-       #[allow(unused)]
-       fn add_new_awaiting_invoice(
+       pub(super) fn add_new_awaiting_invoice(
                &self, payment_id: PaymentId, absolute_expiry: Duration, retry_strategy: Retry,
                max_total_routing_fee_msat: Option<u64>
        ) -> Result<(), ()> {
index 60621b9dc7491686fbf3207a532f0e2bc25a5aa5..ab95d5b192f6936be0701aefb876089a62054906 100644 (file)
@@ -13,6 +13,8 @@
 //! published as a QR code to be scanned by a customer. The customer uses the offer to request an
 //! invoice from the merchant to be paid.
 //!
+//! # Example
+//!
 //! ```
 //! extern crate bitcoin;
 //! extern crate core;
 //! # Ok(())
 //! # }
 //! ```
+//!
+//! # Note
+//!
+//! If constructing an [`Offer`] for use with a [`ChannelManager`], use
+//! [`ChannelManager::create_offer_builder`] instead of [`OfferBuilder::new`].
+//!
+//! [`ChannelManager`]: crate::ln::channelmanager::ChannelManager
+//! [`ChannelManager::create_offer_builder`]: crate::ln::channelmanager::ChannelManager::create_offer_builder
 
 use bitcoin::blockdata::constants::ChainHash;
 use bitcoin::network::constants::Network;
@@ -132,6 +142,14 @@ impl<'a> OfferBuilder<'a, ExplicitMetadata, secp256k1::SignOnly> {
        /// while the offer is valid.
        ///
        /// Use a different pubkey per offer to avoid correlating offers.
+       ///
+       /// # Note
+       ///
+       /// If constructing an [`Offer`] for use with a [`ChannelManager`], use
+       /// [`ChannelManager::create_offer_builder`] instead of [`OfferBuilder::new`].
+       ///
+       /// [`ChannelManager`]: crate::ln::channelmanager::ChannelManager
+       /// [`ChannelManager::create_offer_builder`]: crate::ln::channelmanager::ChannelManager::create_offer_builder
        pub fn new(description: String, signing_pubkey: PublicKey) -> Self {
                OfferBuilder {
                        offer: OfferContents {
@@ -191,9 +209,18 @@ impl<'a, M: MetadataStrategy, T: secp256k1::Signing> OfferBuilder<'a, M, T> {
        /// See [`Offer::chains`] on how this relates to the payment currency.
        ///
        /// Successive calls to this method will add another chain hash.
-       pub fn chain(mut self, network: Network) -> Self {
+       pub fn chain(self, network: Network) -> Self {
+               self.chain_hash(ChainHash::using_genesis_block(network))
+       }
+
+       /// Adds the [`ChainHash`] to [`Offer::chains`]. If not called, the chain hash of
+       /// [`Network::Bitcoin`] is assumed to be the only one supported.
+       ///
+       /// See [`Offer::chains`] on how this relates to the payment currency.
+       ///
+       /// Successive calls to this method will add another chain hash.
+       pub(crate) fn chain_hash(mut self, chain: ChainHash) -> Self {
                let chains = self.offer.chains.get_or_insert_with(Vec::new);
-               let chain = ChainHash::using_genesis_block(network);
                if !chains.contains(&chain) {
                        chains.push(chain);
                }
index e9477086ee981358aea419416e676e4b682c1fc1..c85c2f326b02a816dd85ba285334112ad557c324 100644 (file)
@@ -179,6 +179,8 @@ pub enum Bolt12SemanticError {
        MissingPayerMetadata,
        /// A payer id was expected but was missing.
        MissingPayerId,
+       /// The payment id for a refund or request is already in use.
+       DuplicatePaymentId,
        /// Blinded paths were expected but were missing.
        MissingPaths,
        /// The blinded payinfo given does not match the number of blinded path hops.
index 4b4572b4df9c85fd8970e9758b727d9b6214a067..ecafb2bb5c7b3f3cb4973016ba117cef9ed638ae 100644 (file)
@@ -18,6 +18,8 @@
 //! [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
 //! [`Offer`]: crate::offers::offer::Offer
 //!
+//! # Example
+//!
 //! ```
 //! extern crate bitcoin;
 //! extern crate core;
 //! # Ok(())
 //! # }
 //! ```
+//!
+//! # Note
+//!
+//! If constructing a [`Refund`] for use with a [`ChannelManager`], use
+//! [`ChannelManager::create_refund_builder`] instead of [`RefundBuilder::new`].
+//!
+//! [`ChannelManager`]: crate::ln::channelmanager::ChannelManager
+//! [`ChannelManager::create_refund_builder`]: crate::ln::channelmanager::ChannelManager::create_refund_builder
 
 use bitcoin::blockdata::constants::ChainHash;
 use bitcoin::network::constants::Network;
@@ -120,6 +130,14 @@ impl<'a> RefundBuilder<'a, secp256k1::SignOnly> {
        ///
        /// Additionally, sets the required [`Refund::description`], [`Refund::payer_metadata`], and
        /// [`Refund::amount_msats`].
+       ///
+       /// # Note
+       ///
+       /// If constructing a [`Refund`] for use with a [`ChannelManager`], use
+       /// [`ChannelManager::create_refund_builder`] instead of [`RefundBuilder::new`].
+       ///
+       /// [`ChannelManager`]: crate::ln::channelmanager::ChannelManager
+       /// [`ChannelManager::create_refund_builder`]: crate::ln::channelmanager::ChannelManager::create_refund_builder
        pub fn new(
                description: String, metadata: Vec<u8>, payer_id: PublicKey, amount_msats: u64
        ) -> Result<Self, Bolt12SemanticError> {
@@ -206,8 +224,16 @@ impl<'a, T: secp256k1::Signing> RefundBuilder<'a, T> {
        /// called, [`Network::Bitcoin`] is assumed.
        ///
        /// Successive calls to this method will override the previous setting.
-       pub fn chain(mut self, network: Network) -> Self {
-               self.refund.chain = Some(ChainHash::using_genesis_block(network));
+       pub fn chain(self, network: Network) -> Self {
+               self.chain_hash(ChainHash::using_genesis_block(network))
+       }
+
+       /// Sets the [`Refund::chain`] of the given [`ChainHash`] for paying an invoice. If not called,
+       /// [`Network::Bitcoin`] is assumed.
+       ///
+       /// Successive calls to this method will override the previous setting.
+       pub(crate) fn chain_hash(mut self, chain: ChainHash) -> Self {
+               self.refund.chain = Some(chain);
                self
        }