diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 39f8e40b522..8246e4471d2 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -36,7 +36,7 @@ use crate::events::FundingInfo; use crate::blinded_path::message::{AsyncPaymentsContext, MessageContext, OffersContext}; use crate::blinded_path::NodeIdLookUp; use crate::blinded_path::message::{BlindedMessagePath, MessageForwardNode}; -use crate::blinded_path::payment::{BlindedPaymentPath, Bolt12RefundContext, PaymentConstraints, PaymentContext, ReceiveTlvs}; +use crate::blinded_path::payment::{BlindedPaymentPath, PaymentConstraints, PaymentContext, ReceiveTlvs}; use crate::chain; use crate::chain::{Confirm, ChannelMonitorUpdateStatus, Watch, BestBlock}; use crate::chain::chaininterface::{BroadcasterInterface, ConfirmationTarget, FeeEstimator, LowerBoundedFeeEstimator}; @@ -65,11 +65,10 @@ use crate::ln::msgs::{ChannelMessageHandler, DecodeError, LightningError}; use crate::ln::outbound_payment; use crate::ln::outbound_payment::{OutboundPayments, PendingOutboundPayment, RetryableInvoiceRequest, SendAlongPathArgs, StaleExpiration}; use crate::ln::wire::Encode; -use crate::offers::invoice::{Bolt12Invoice, DEFAULT_RELATIVE_EXPIRY, DerivedSigningPubkey, InvoiceBuilder}; +use crate::offers::invoice::Bolt12Invoice; use crate::offers::invoice_request::{InvoiceRequest, InvoiceRequestBuilder}; use crate::offers::nonce::Nonce; use crate::offers::parse::Bolt12SemanticError; -use crate::offers::refund::Refund; use crate::offers::signer; #[cfg(async_payments)] use crate::offers::static_invoice::StaticInvoice; @@ -1994,56 +1993,6 @@ where /// # } /// ``` /// -/// ## BOLT 12 Offers -/// -/// ## BOLT 12 Refunds -/// -/// Use [`request_refund_payment`] to send a [`Bolt12Invoice`] for receiving the refund. Similar to -/// *creating* an [`Offer`], this is stateless as it represents an inbound payment. -/// -/// ``` -/// # use lightning::events::{Event, EventsProvider, PaymentPurpose}; -/// # use lightning::ln::channelmanager::{AChannelManager, OffersMessageCommons}; -/// # use lightning::offers::refund::Refund; -/// # -/// # fn example(channel_manager: T, refund: &Refund) { -/// # let channel_manager = channel_manager.get_cm(); -/// let known_payment_hash = match channel_manager.request_refund_payment(refund) { -/// Ok(invoice) => { -/// let payment_hash = invoice.payment_hash(); -/// println!("Requesting refund payment {}", payment_hash); -/// payment_hash -/// }, -/// Err(e) => panic!("Unable to request payment for refund: {:?}", e), -/// }; -/// -/// // On the event processing thread -/// channel_manager.process_pending_events(&|event| { -/// match event { -/// Event::PaymentClaimable { payment_hash, purpose, .. } => match purpose { -/// PaymentPurpose::Bolt12RefundPayment { payment_preimage: Some(payment_preimage), .. } => { -/// assert_eq!(payment_hash, known_payment_hash); -/// println!("Claiming payment {}", payment_hash); -/// channel_manager.claim_funds(payment_preimage); -/// }, -/// PaymentPurpose::Bolt12RefundPayment { payment_preimage: None, .. } => { -/// println!("Unknown payment hash: {}", payment_hash); -/// }, -/// // ... -/// # _ => {}, -/// }, -/// Event::PaymentClaimed { payment_hash, amount_msat, .. } => { -/// assert_eq!(payment_hash, known_payment_hash); -/// println!("Claimed {} msats", amount_msat); -/// }, -/// // ... -/// # _ => {}, -/// } -/// Ok(()) -/// }); -/// # } -/// ``` -/// /// # Persistence /// /// Implements [`Writeable`] to write out all channel state to disk. Implies [`peer_disconnected`] for @@ -2636,6 +2585,7 @@ const MAX_NO_CHANNEL_PEERS: usize = 250; /// become invalid over time as channels are closed. Thus, they are only suitable for short-term use. /// /// [`Offer`]: crate::offers::offer +/// [`Refund`]: crate::offers::refund pub const MAX_SHORT_LIVED_RELATIVE_EXPIRY: Duration = Duration::from_secs(60 * 60 * 24); /// Used by [`ChannelManager::list_recent_payments`] to express the status of recent payments. @@ -9569,7 +9519,7 @@ where /// Sending multiple requests increases the chances of successful delivery in case some /// paths are unavailable. However, only one invoice for a given [`PaymentId`] will be paid, /// even if multiple invoices are received. -const OFFERS_MESSAGE_REQUEST_LIMIT: usize = 10; +pub const OFFERS_MESSAGE_REQUEST_LIMIT: usize = 10; impl ChannelManager where @@ -9656,106 +9606,6 @@ where }) } - /// Creates a [`Bolt12Invoice`] for a [`Refund`] and enqueues it to be sent via an onion - /// message. - /// - /// The resulting invoice uses a [`PaymentHash`] recognized by the [`ChannelManager`] and a - /// [`BlindedPaymentPath`] containing the [`PaymentSecret`] needed to reconstruct the - /// corresponding [`PaymentPreimage`]. It is returned purely for informational purposes. - /// - /// # Limitations - /// - /// Requires a direct connection to an introduction node in [`Refund::paths`] or to - /// [`Refund::payer_signing_pubkey`], if empty. This request is best effort; an invoice will be - /// sent to each node meeting the aforementioned criteria, but there's no guarantee that they - /// will be received and no retries will be made. - /// - /// # Errors - /// - /// Errors if: - /// - the refund is for an unsupported chain, or - /// - the parameterized [`Router`] is unable to create a blinded payment path or reply path for - /// the invoice. - /// - /// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice - pub fn request_refund_payment( - &self, refund: &Refund - ) -> Result { - let expanded_key = &self.inbound_payment_key; - let entropy = &*self.entropy_source; - let secp_ctx = &self.secp_ctx; - - let amount_msats = refund.amount_msats(); - let relative_expiry = DEFAULT_RELATIVE_EXPIRY.as_secs() as u32; - - if refund.chain() != self.chain_hash { - return Err(Bolt12SemanticError::UnsupportedChain); - } - - let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self); - - match self.create_inbound_payment(Some(amount_msats), relative_expiry, None) { - Ok((payment_hash, payment_secret)) => { - let payment_context = PaymentContext::Bolt12Refund(Bolt12RefundContext {}); - let payment_paths = self.create_blinded_payment_paths( - amount_msats, payment_secret, payment_context - ) - .map_err(|_| Bolt12SemanticError::MissingPaths)?; - - #[cfg(feature = "std")] - let builder = refund.respond_using_derived_keys( - payment_paths, payment_hash, expanded_key, entropy - )?; - #[cfg(not(feature = "std"))] - let created_at = Duration::from_secs( - self.highest_seen_timestamp.load(Ordering::Acquire) as u64 - ); - #[cfg(not(feature = "std"))] - let builder = refund.respond_using_derived_keys_no_std( - payment_paths, payment_hash, created_at, expanded_key, entropy - )?; - let builder: InvoiceBuilder = builder.into(); - let invoice = builder.allow_mpp().build_and_sign(secp_ctx)?; - - let nonce = Nonce::from_entropy_source(entropy); - let hmac = payment_hash.hmac_for_offer_payment(nonce, expanded_key); - let context = MessageContext::Offers(OffersContext::InboundPayment { - payment_hash: invoice.payment_hash(), nonce, hmac - }); - let reply_paths = self.create_blinded_paths(context) - .map_err(|_| Bolt12SemanticError::MissingPaths)?; - - let mut pending_offers_messages = self.pending_offers_messages.lock().unwrap(); - if refund.paths().is_empty() { - for reply_path in reply_paths { - let instructions = MessageSendInstructions::WithSpecifiedReplyPath { - destination: Destination::Node(refund.payer_signing_pubkey()), - reply_path, - }; - let message = OffersMessage::Invoice(invoice.clone()); - pending_offers_messages.push((message, instructions)); - } - } else { - reply_paths - .iter() - .flat_map(|reply_path| refund.paths().iter().map(move |path| (path, reply_path))) - .take(OFFERS_MESSAGE_REQUEST_LIMIT) - .for_each(|(path, reply_path)| { - let instructions = MessageSendInstructions::WithSpecifiedReplyPath { - destination: Destination::BlindedPath(path.clone()), - reply_path: reply_path.clone(), - }; - let message = OffersMessage::Invoice(invoice.clone()); - pending_offers_messages.push((message, instructions)); - }); - } - - Ok(invoice) - }, - Err(()) => Err(Bolt12SemanticError::InvalidAmount), - } - } - /// Pays for an [`Offer`] looked up using [BIP 353] Human Readable Names resolved by the DNS /// resolver(s) at `dns_resolvers` which resolve names according to bLIP 32. /// @@ -12202,6 +12052,7 @@ where /// [`Refund`]s, and any reply paths. /// /// [`Offer`]: crate::offers::offer + /// [`Refund`]: crate::offers::refund pub message_router: MR, /// The Logger for use in the ChannelManager and which may be used to log information during /// deserialization. diff --git a/lightning/src/ln/offers_tests.rs b/lightning/src/ln/offers_tests.rs index 6d91652ca20..328a66339f8 100644 --- a/lightning/src/ln/offers_tests.rs +++ b/lightning/src/ln/offers_tests.rs @@ -655,7 +655,7 @@ fn creates_and_pays_for_refund_using_two_hop_blinded_path() { expect_recent_payment!(david, RecentPaymentDetails::AwaitingInvoice, payment_id); let payment_context = PaymentContext::Bolt12Refund(Bolt12RefundContext {}); - let expected_invoice = alice.node.request_refund_payment(&refund).unwrap(); + let expected_invoice = alice.offers_handler.request_refund_payment(&refund).unwrap(); connect_peers(alice, charlie); @@ -784,7 +784,7 @@ fn creates_and_pays_for_refund_using_one_hop_blinded_path() { expect_recent_payment!(bob, RecentPaymentDetails::AwaitingInvoice, payment_id); let payment_context = PaymentContext::Bolt12Refund(Bolt12RefundContext {}); - let expected_invoice = alice.node.request_refund_payment(&refund).unwrap(); + let expected_invoice = alice.offers_handler.request_refund_payment(&refund).unwrap(); let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap(); bob.onion_messenger.handle_onion_message(alice_id, &onion_message); @@ -889,7 +889,7 @@ fn pays_for_refund_without_blinded_paths() { expect_recent_payment!(bob, RecentPaymentDetails::AwaitingInvoice, payment_id); let payment_context = PaymentContext::Bolt12Refund(Bolt12RefundContext {}); - let expected_invoice = alice.node.request_refund_payment(&refund).unwrap(); + let expected_invoice = alice.offers_handler.request_refund_payment(&refund).unwrap(); let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap(); bob.onion_messenger.handle_onion_message(alice_id, &onion_message); @@ -1044,7 +1044,7 @@ fn send_invoice_for_refund_with_distinct_reply_path() { } expect_recent_payment!(alice, RecentPaymentDetails::AwaitingInvoice, payment_id); - let _expected_invoice = david.node.request_refund_payment(&refund).unwrap(); + let _expected_invoice = david.offers_handler.request_refund_payment(&refund).unwrap(); connect_peers(david, bob); @@ -1324,7 +1324,7 @@ fn creates_refund_with_blinded_path_using_unannounced_introduction_node() { } expect_recent_payment!(bob, RecentPaymentDetails::AwaitingInvoice, payment_id); - let expected_invoice = alice.node.request_refund_payment(&refund).unwrap(); + let expected_invoice = alice.offers_handler.request_refund_payment(&refund).unwrap(); let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap(); @@ -1608,7 +1608,7 @@ fn fails_authentication_when_handling_invoice_for_refund() { expect_recent_payment!(david, RecentPaymentDetails::AwaitingInvoice, payment_id); // Send the invoice directly to David instead of using a blinded path. - let expected_invoice = alice.node.request_refund_payment(&refund).unwrap(); + let expected_invoice = alice.offers_handler.request_refund_payment(&refund).unwrap(); connect_peers(david, alice); match &mut alice.node.pending_offers_messages.lock().unwrap().first_mut().unwrap().1 { @@ -1640,7 +1640,7 @@ fn fails_authentication_when_handling_invoice_for_refund() { assert_eq!(path.introduction_node(), &IntroductionNode::NodeId(charlie_id)); } - let expected_invoice = alice.node.request_refund_payment(&refund).unwrap(); + let expected_invoice = alice.offers_handler.request_refund_payment(&refund).unwrap(); match &mut alice.node.pending_offers_messages.lock().unwrap().first_mut().unwrap().1 { MessageSendInstructions::WithSpecifiedReplyPath { destination, .. } => @@ -1773,7 +1773,7 @@ fn fails_creating_refund_or_sending_invoice_without_connected_peers() { .unwrap() .build().unwrap(); - match alice.node.request_refund_payment(&refund) { + match alice.offers_handler.request_refund_payment(&refund) { Ok(_) => panic!("Expected error"), Err(e) => assert_eq!(e, Bolt12SemanticError::MissingPaths), } @@ -1782,7 +1782,7 @@ fn fails_creating_refund_or_sending_invoice_without_connected_peers() { args.send_channel_ready = (true, true); reconnect_nodes(args); - assert!(alice.node.request_refund_payment(&refund).is_ok()); + assert!(alice.offers_handler.request_refund_payment(&refund).is_ok()); } /// Fails creating an invoice request when the offer contains an unsupported chain. @@ -1832,7 +1832,7 @@ fn fails_sending_invoice_with_unsupported_chain_for_refund() { .chain(Network::Signet) .build().unwrap(); - match alice.node.request_refund_payment(&refund) { + match alice.offers_handler.request_refund_payment(&refund) { Ok(_) => panic!("Expected error"), Err(e) => assert_eq!(e, Bolt12SemanticError::UnsupportedChain), } @@ -2055,7 +2055,7 @@ fn fails_sending_invoice_without_blinded_payment_paths_for_refund() { .unwrap() .build().unwrap(); - match alice.node.request_refund_payment(&refund) { + match alice.offers_handler.request_refund_payment(&refund) { Ok(_) => panic!("Expected error"), Err(e) => assert_eq!(e, Bolt12SemanticError::MissingPaths), } @@ -2106,7 +2106,7 @@ fn fails_paying_invoice_more_than_once() { expect_recent_payment!(david, RecentPaymentDetails::AwaitingInvoice, payment_id); // Alice sends the first invoice - alice.node.request_refund_payment(&refund).unwrap(); + alice.offers_handler.request_refund_payment(&refund).unwrap(); connect_peers(alice, charlie); @@ -2126,7 +2126,7 @@ fn fails_paying_invoice_more_than_once() { disconnect_peers(alice, &[charlie]); // Alice sends the second invoice - alice.node.request_refund_payment(&refund).unwrap(); + alice.offers_handler.request_refund_payment(&refund).unwrap(); connect_peers(alice, charlie); connect_peers(david, bob); diff --git a/lightning/src/offers/flow.rs b/lightning/src/offers/flow.rs index fb541151275..911d8f0b4d2 100644 --- a/lightning/src/offers/flow.rs +++ b/lightning/src/offers/flow.rs @@ -17,14 +17,17 @@ use bitcoin::secp256k1::{self, PublicKey, Secp256k1}; use types::payment::PaymentPreimage; use crate::blinded_path::message::{MessageContext, OffersContext}; -use crate::blinded_path::payment::{Bolt12OfferContext, PaymentContext}; +use crate::blinded_path::payment::{Bolt12OfferContext, Bolt12RefundContext, PaymentContext}; use crate::events::{Event, EventHandler, PaymentFailureReason}; use crate::ln::channelmanager::{ Bolt12PaymentError, OffersMessageCommons, PaymentId, RecentPaymentDetails, Verification, + OFFERS_MESSAGE_REQUEST_LIMIT, }; use crate::ln::inbound_payment; use crate::ln::outbound_payment::{Retry, RetryableInvoiceRequest, StaleExpiration}; -use crate::onion_message::messenger::{MessageSendInstructions, Responder, ResponseInstruction}; +use crate::onion_message::messenger::{ + Destination, MessageSendInstructions, Responder, ResponseInstruction, +}; use crate::onion_message::offers::{OffersMessage, OffersMessageHandler}; use crate::offers::invoice::{ @@ -41,6 +44,9 @@ use crate::sign::{EntropySource, NodeSigner, Recipient}; use crate::sync::Mutex; use crate::util::logger::{Logger, WithContext}; +use super::invoice::Bolt12Invoice; +use super::refund::Refund; + /// A trivial trait which describes any [`OffersMessageFlow`]. /// /// This is not exported to bindings users as general cover traits aren't useful in other @@ -263,6 +269,55 @@ where /// # } /// ``` /// +/// ## BOLT 12 Refunds +/// +/// Use [`request_refund_payment`] to send a [`Bolt12Invoice`] for receiving the refund. Similar to +/// *creating* an [`Offer`], this is stateless as it represents an inbound payment. +/// +/// ``` +/// # use lightning::events::{Event, EventsProvider, PaymentPurpose}; +/// # use lightning::ln::channelmanager::OffersMessageCommons; +/// # use lightning::offers::flow::AnOffersMessageFlow; +/// # use lightning::offers::refund::Refund; +/// # +/// # fn example(offers_flow: T, refund: &Refund) { +/// # let offers_flow = offers_flow.get_omf(); +/// let known_payment_hash = match offers_flow.request_refund_payment(refund) { +/// Ok(invoice) => { +/// let payment_hash = invoice.payment_hash(); +/// println!("Requesting refund payment {}", payment_hash); +/// payment_hash +/// }, +/// Err(e) => panic!("Unable to request payment for refund: {:?}", e), +/// }; +/// +/// // On the event processing thread +/// offers_flow.process_pending_offers_events(&|event| { +/// match event { +/// Event::PaymentClaimable { payment_hash, purpose, .. } => match purpose { +/// PaymentPurpose::Bolt12RefundPayment { payment_preimage: Some(payment_preimage), .. } => { +/// assert_eq!(payment_hash, known_payment_hash); +/// println!("Claiming payment {}", payment_hash); +/// offers_flow.claim_funds(payment_preimage); +/// }, +/// PaymentPurpose::Bolt12RefundPayment { payment_preimage: None, .. } => { +/// println!("Unknown payment hash: {}", payment_hash); +/// }, +/// // ... +/// # _ => {}, +/// }, +/// Event::PaymentClaimed { payment_hash, amount_msat, .. } => { +/// assert_eq!(payment_hash, known_payment_hash); +/// println!("Claimed {} msats", amount_msat); +/// }, +/// // ... +/// # _ => {}, +/// } +/// Ok(()) +/// }); +/// # } +/// ``` +/// /// [`Bolt12Invoice`]: crate::offers::invoice /// [`create_offer_builder`]: Self::create_offer_builder' /// [`create_refund_builder`]: Self::[`create_refund_builder`] @@ -270,6 +325,7 @@ where /// [`Offer`]: crate::offers::offer /// [`offers`]: crate::offers /// [`pay_for_offer`]: Self::pay_for_offer +/// [`request_refund_payment`]: Self::request_refund_payment pub struct OffersMessageFlow where ES::Target: EntropySource, @@ -898,4 +954,120 @@ where .map_err(|_| Bolt12SemanticError::DuplicatePaymentId) }) } + + /// Creates a [`Bolt12Invoice`] for a [`Refund`] and enqueues it to be sent via an onion + /// message. + /// + /// The resulting invoice uses a [`PaymentHash`] recognized by the [`ChannelManager`] and a + /// [`BlindedPaymentPath`] containing the [`PaymentSecret`] needed to reconstruct the + /// corresponding [`PaymentPreimage`]. It is returned purely for informational purposes. + /// + /// # Limitations + /// + /// Requires a direct connection to an introduction node in [`Refund::paths`] or to + /// [`Refund::payer_signing_pubkey`], if empty. This request is best effort; an invoice will be + /// sent to each node meeting the aforementioned criteria, but there's no guarantee that they + /// will be received and no retries will be made. + /// + /// # Errors + /// + /// Errors if: + /// - the refund is for an unsupported chain, or + /// - the parameterized [`Router`] is unable to create a blinded payment path or reply path for + /// the invoice. + /// + /// [`BlindedPaymentPath`]: crate::blinded_path::payment::BlindedPaymentPath + /// [`ChannelManager`]: crate::ln::channelmanager::ChannelManager + /// [`PaymentHash`]: crate::types::payment::PaymentHash + /// [`PaymentSecret`]: crate::types::payment::PaymentSecret + /// [`Router`]: crate::routing::router::Router + pub fn request_refund_payment( + &self, refund: &Refund, + ) -> Result { + let expanded_key = &self.inbound_payment_key; + let entropy = &*self.entropy_source; + let secp_ctx = &self.secp_ctx; + + let amount_msats = refund.amount_msats(); + let relative_expiry = DEFAULT_RELATIVE_EXPIRY.as_secs() as u32; + + if refund.chain() != self.commons.get_chain_hash() { + return Err(Bolt12SemanticError::UnsupportedChain); + } + + // TODO: Add persistance through `commons` internal function. Shouldn't be exposed here. + // let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(&*self.commons); + + match self.commons.create_inbound_payment(Some(amount_msats), relative_expiry, None) { + Ok((payment_hash, payment_secret)) => { + let payment_context = PaymentContext::Bolt12Refund(Bolt12RefundContext {}); + let payment_paths = self + .commons + .create_blinded_payment_paths(amount_msats, payment_secret, payment_context) + .map_err(|_| Bolt12SemanticError::MissingPaths)?; + + #[cfg(feature = "std")] + let builder = refund.respond_using_derived_keys( + payment_paths, + payment_hash, + expanded_key, + entropy, + )?; + #[cfg(not(feature = "std"))] + let created_at = Duration::from_secs(self.highest_seen_timestamp.load(Ordering::Acquire) as u64); + #[cfg(not(feature = "std"))] + let builder = refund.respond_using_derived_keys_no_std( + payment_paths, + payment_hash, + created_at, + expanded_key, + entropy, + )?; + let builder: InvoiceBuilder = builder.into(); + let invoice = builder.allow_mpp().build_and_sign(secp_ctx)?; + + let nonce = Nonce::from_entropy_source(entropy); + let hmac = payment_hash.hmac_for_offer_payment(nonce, expanded_key); + let context = MessageContext::Offers(OffersContext::InboundPayment { + payment_hash: invoice.payment_hash(), + nonce, + hmac, + }); + let reply_paths = self + .commons + .create_blinded_paths(context) + .map_err(|_| Bolt12SemanticError::MissingPaths)?; + + let mut pending_offers_messages = self.pending_offers_messages.lock().unwrap(); + if refund.paths().is_empty() { + for reply_path in reply_paths { + let instructions = MessageSendInstructions::WithSpecifiedReplyPath { + destination: Destination::Node(refund.payer_signing_pubkey()), + reply_path, + }; + let message = OffersMessage::Invoice(invoice.clone()); + pending_offers_messages.push((message, instructions)); + } + } else { + reply_paths + .iter() + .flat_map(|reply_path| { + refund.paths().iter().map(move |path| (path, reply_path)) + }) + .take(OFFERS_MESSAGE_REQUEST_LIMIT) + .for_each(|(path, reply_path)| { + let instructions = MessageSendInstructions::WithSpecifiedReplyPath { + destination: Destination::BlindedPath(path.clone()), + reply_path: reply_path.clone(), + }; + let message = OffersMessage::Invoice(invoice.clone()); + pending_offers_messages.push((message, instructions)); + }); + } + + Ok(invoice) + }, + Err(()) => Err(Bolt12SemanticError::InvalidAmount), + } + } }