From cd8a41a5fa1679f036196e75f05be68046f6c58e Mon Sep 17 00:00:00 2001 From: shaavan Date: Wed, 4 Dec 2024 17:41:11 +0530 Subject: [PATCH 01/14] Remove async BOLT12 handling support This commit temporarily removes support for async BOLT12 message handling to enable a smoother transition in the upcoming refactor. The current implementation of async handling is abrupt, as it requires delving into the synchronous case and generating an event mid-flow based on the `manual_handling` flag. This approach adds unnecessary complexity and coupling. A later commit will introduce a new struct, `OffersMessageFlow`, designed to handle and create offer messages. This new struct will support async handling in a more structured way by allowing users to implement a parameterized trait for asynchronous message handling. Removing the existing async support now ensures a cleaner and more seamless migration of offer-related code from `ChannelManager` to `OffersMessageFlow`. --- lightning/src/events/mod.rs | 68 ++------------------- lightning/src/ln/channelmanager.rs | 37 ------------ lightning/src/ln/offers_tests.rs | 88 ++-------------------------- lightning/src/ln/outbound_payment.rs | 5 +- lightning/src/util/config.rs | 16 ----- 5 files changed, 13 insertions(+), 201 deletions(-) diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs index 6cf966c7214..e665f62b4e2 100644 --- a/lightning/src/events/mod.rs +++ b/lightning/src/events/mod.rs @@ -18,7 +18,6 @@ pub mod bump_transaction; pub use bump_transaction::BumpTransactionEvent; -use crate::blinded_path::message::OffersContext; use crate::blinded_path::payment::{Bolt12OfferContext, Bolt12RefundContext, PaymentContext, PaymentContextRef}; use crate::chain::transaction; use crate::ln::channelmanager::{InterceptId, PaymentId, RecipientOnionFields}; @@ -27,8 +26,6 @@ use crate::types::features::ChannelTypeFeatures; use crate::ln::msgs; use crate::ln::types::ChannelId; use crate::types::payment::{PaymentPreimage, PaymentHash, PaymentSecret}; -use crate::offers::invoice::Bolt12Invoice; -use crate::onion_message::messenger::Responder; use crate::routing::gossip::NetworkUpdate; use crate::routing::router::{BlindedTail, Path, RouteHop, RouteParameters}; use crate::sign::SpendableOutputDescriptor; @@ -587,6 +584,8 @@ pub enum PaymentFailureReason { /// An invoice was received that required unknown features. UnknownRequiredFeatures, /// A [`Bolt12Invoice`] was not received in a reasonable amount of time. + /// + /// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice InvoiceRequestExpired, /// An [`InvoiceRequest`] for the payment was rejected by the recipient. /// @@ -874,39 +873,6 @@ pub enum Event { /// Sockets for connecting to the node. addresses: Vec, }, - /// Indicates a [`Bolt12Invoice`] in response to an [`InvoiceRequest`] or a [`Refund`] was - /// received. - /// - /// This event will only be generated if [`UserConfig::manually_handle_bolt12_invoices`] is set. - /// Use [`ChannelManager::send_payment_for_bolt12_invoice`] to pay the invoice or - /// [`ChannelManager::abandon_payment`] to abandon the associated payment. See those docs for - /// further details. - /// - /// # Failure Behavior and Persistence - /// This event will eventually be replayed after failures-to-handle (i.e., the event handler - /// returning `Err(ReplayEvent ())`) and will be persisted across restarts. - /// - /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest - /// [`Refund`]: crate::offers::refund::Refund - /// [`UserConfig::manually_handle_bolt12_invoices`]: crate::util::config::UserConfig::manually_handle_bolt12_invoices - /// [`ChannelManager::send_payment_for_bolt12_invoice`]: crate::ln::channelmanager::ChannelManager::send_payment_for_bolt12_invoice - /// [`ChannelManager::abandon_payment`]: crate::ln::channelmanager::ChannelManager::abandon_payment - InvoiceReceived { - /// The `payment_id` associated with payment for the invoice. - payment_id: PaymentId, - /// The invoice to pay. - invoice: Bolt12Invoice, - /// The context of the [`BlindedMessagePath`] used to send the invoice. - /// - /// [`BlindedMessagePath`]: crate::blinded_path::message::BlindedMessagePath - context: Option, - /// A responder for replying with an [`InvoiceError`] if needed. - /// - /// `None` if the invoice wasn't sent with a reply path. - /// - /// [`InvoiceError`]: crate::offers::invoice_error::InvoiceError - responder: Option, - }, /// Indicates an outbound payment we made succeeded (i.e. it made it all the way to its target /// and we got back the payment preimage for it). /// @@ -1809,15 +1775,8 @@ impl Writeable for Event { (0, peer_node_id, required), }); }, - &Event::InvoiceReceived { ref payment_id, ref invoice, ref context, ref responder } => { - 41u8.write(writer)?; - write_tlv_fields!(writer, { - (0, payment_id, required), - (2, invoice, required), - (4, context, option), - (6, responder, option), - }); - }, + // Note: The type number `41u8` was previously used for `Event::InvoiceReceived`, which has now been removed. + // To prevent serialization issues, please avoid reusing `41u8` for any new events. &Event::FundingTxBroadcastSafe { ref channel_id, ref user_channel_id, ref funding_txo, ref counterparty_node_id, ref former_temporary_channel_id} => { 43u8.write(writer)?; write_tlv_fields!(writer, { @@ -2303,23 +2262,8 @@ impl MaybeReadable for Event { }; f() }, - 41u8 => { - let mut f = || { - _init_and_read_len_prefixed_tlv_fields!(reader, { - (0, payment_id, required), - (2, invoice, required), - (4, context, option), - (6, responder, option), - }); - Ok(Some(Event::InvoiceReceived { - payment_id: payment_id.0.unwrap(), - invoice: invoice.0.unwrap(), - context, - responder, - })) - }; - f() - }, + // Note: The type number `41u8` was previously used for `Event::InvoiceReceived`, which has now been removed. + // To prevent serialization issues, please avoid reusing `41u8` for any new events. 43u8 => { let mut channel_id = RequiredWrapper(None); let mut user_channel_id = RequiredWrapper(None); diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 15c80d6fbec..9266cc84194 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -4757,35 +4757,6 @@ where self.pending_outbound_payments.test_set_payment_metadata(payment_id, new_payment_metadata); } - /// Pays the [`Bolt12Invoice`] associated with the `payment_id` encoded in its `payer_metadata`. - /// - /// The invoice's `payer_metadata` is used to authenticate that the invoice was indeed requested - /// before attempting a payment. [`Bolt12PaymentError::UnexpectedInvoice`] is returned if this - /// fails or if the encoded `payment_id` is not recognized. The latter may happen once the - /// payment is no longer tracked because the payment was attempted after: - /// - an invoice for the `payment_id` was already paid, - /// - one full [timer tick] has elapsed since initially requesting the invoice when paying an - /// offer, or - /// - the refund corresponding to the invoice has already expired. - /// - /// To retry the payment, request another invoice using a new `payment_id`. - /// - /// Attempting to pay the same invoice twice while the first payment is still pending will - /// result in a [`Bolt12PaymentError::DuplicateInvoice`]. - /// - /// Otherwise, either [`Event::PaymentSent`] or [`Event::PaymentFailed`] are used to indicate - /// whether or not the payment was successful. - /// - /// [timer tick]: Self::timer_tick_occurred - pub fn send_payment_for_bolt12_invoice( - &self, invoice: &Bolt12Invoice, context: Option<&OffersContext>, - ) -> Result<(), Bolt12PaymentError> { - match self.verify_bolt12_invoice(invoice, context) { - Ok(payment_id) => self.send_payment_for_verified_bolt12_invoice(invoice, payment_id), - Err(()) => Err(Bolt12PaymentError::UnexpectedInvoice), - } - } - fn verify_bolt12_invoice( &self, invoice: &Bolt12Invoice, context: Option<&OffersContext>, ) -> Result { @@ -12221,14 +12192,6 @@ where &self.logger, None, None, Some(invoice.payment_hash()), ); - if self.default_configuration.manually_handle_bolt12_invoices { - let event = Event::InvoiceReceived { - payment_id, invoice, context, responder, - }; - self.pending_events.lock().unwrap().push_back((event, None)); - return None; - } - let res = self.send_payment_for_verified_bolt12_invoice(&invoice, payment_id); handle_pay_invoice_res!(res, invoice, logger); }, diff --git a/lightning/src/ln/offers_tests.rs b/lightning/src/ln/offers_tests.rs index 803441f09fe..32362f1aff7 100644 --- a/lightning/src/ln/offers_tests.rs +++ b/lightning/src/ln/offers_tests.rs @@ -48,11 +48,11 @@ use crate::blinded_path::message::BlindedMessagePath; use crate::blinded_path::payment::{Bolt12OfferContext, Bolt12RefundContext, PaymentContext}; use crate::blinded_path::message::{MessageContext, OffersContext}; use crate::events::{ClosureReason, Event, HTLCDestination, MessageSendEventsProvider, PaymentFailureReason, PaymentPurpose}; -use crate::ln::channelmanager::{Bolt12PaymentError, MAX_SHORT_LIVED_RELATIVE_EXPIRY, PaymentId, RecentPaymentDetails, RecipientOnionFields, Retry, self}; +use crate::ln::channelmanager::{MAX_SHORT_LIVED_RELATIVE_EXPIRY, PaymentId, RecentPaymentDetails, RecipientOnionFields, Retry, self}; +use crate::ln::outbound_payment::IDEMPOTENCY_TIMEOUT_TICKS; use crate::types::features::Bolt12InvoiceFeatures; use crate::ln::functional_test_utils::*; use crate::ln::msgs::{ChannelMessageHandler, Init, NodeAnnouncement, OnionMessage, OnionMessageHandler, RoutingMessageHandler, SocketAddress, UnsignedGossipMessage, UnsignedNodeAnnouncement}; -use crate::ln::outbound_payment::IDEMPOTENCY_TIMEOUT_TICKS; use crate::offers::invoice::Bolt12Invoice; use crate::offers::invoice_error::InvoiceError; use crate::offers::invoice_request::{InvoiceRequest, InvoiceRequestFields}; @@ -1144,87 +1144,11 @@ fn creates_and_pays_for_offer_with_retry() { /// Checks that a deferred invoice can be paid asynchronously from an Event::InvoiceReceived. #[test] +#[ignore] fn pays_bolt12_invoice_asynchronously() { - let mut manually_pay_cfg = test_default_channel_config(); - manually_pay_cfg.manually_handle_bolt12_invoices = true; - - let chanmon_cfgs = create_chanmon_cfgs(2); - let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); - let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, Some(manually_pay_cfg)]); - let nodes = create_network(2, &node_cfgs, &node_chanmgrs); - - create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 10_000_000, 1_000_000_000); - - let alice = &nodes[0]; - let alice_id = alice.node.get_our_node_id(); - let bob = &nodes[1]; - let bob_id = bob.node.get_our_node_id(); - - let offer = alice.node - .create_offer_builder(None).unwrap() - .amount_msats(10_000_000) - .build().unwrap(); - - let payment_id = PaymentId([1; 32]); - bob.node.pay_for_offer(&offer, None, None, None, payment_id, Retry::Attempts(0), None).unwrap(); - expect_recent_payment!(bob, RecentPaymentDetails::AwaitingInvoice, payment_id); - - let onion_message = bob.onion_messenger.next_onion_message_for_peer(alice_id).unwrap(); - alice.onion_messenger.handle_onion_message(bob_id, &onion_message); - - let (invoice_request, _) = extract_invoice_request(alice, &onion_message); - let payment_context = PaymentContext::Bolt12Offer(Bolt12OfferContext { - offer_id: offer.id(), - invoice_request: InvoiceRequestFields { - payer_signing_pubkey: invoice_request.payer_signing_pubkey(), - quantity: None, - payer_note_truncated: None, - human_readable_name: None, - }, - }); - - let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap(); - bob.onion_messenger.handle_onion_message(alice_id, &onion_message); - - let (invoice, context) = match get_event!(bob, Event::InvoiceReceived) { - Event::InvoiceReceived { payment_id: actual_payment_id, invoice, context, .. } => { - assert_eq!(actual_payment_id, payment_id); - (invoice, context) - }, - _ => panic!("No Event::InvoiceReceived"), - }; - assert_eq!(invoice.amount_msats(), 10_000_000); - assert_ne!(invoice.signing_pubkey(), alice_id); - assert!(!invoice.payment_paths().is_empty()); - for path in invoice.payment_paths() { - assert_eq!(path.introduction_node(), &IntroductionNode::NodeId(alice_id)); - } - - assert!(bob.node.send_payment_for_bolt12_invoice(&invoice, context.as_ref()).is_ok()); - assert_eq!( - bob.node.send_payment_for_bolt12_invoice(&invoice, context.as_ref()), - Err(Bolt12PaymentError::DuplicateInvoice), - ); - - route_bolt12_payment(bob, &[alice], &invoice); - expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id); - - claim_bolt12_payment(bob, &[alice], payment_context); - expect_recent_payment!(bob, RecentPaymentDetails::Fulfilled, payment_id); - - assert_eq!( - bob.node.send_payment_for_bolt12_invoice(&invoice, context.as_ref()), - Err(Bolt12PaymentError::DuplicateInvoice), - ); - - for _ in 0..=IDEMPOTENCY_TIMEOUT_TICKS { - bob.node.timer_tick_occurred(); - } - - assert_eq!( - bob.node.send_payment_for_bolt12_invoice(&invoice, context.as_ref()), - Err(Bolt12PaymentError::UnexpectedInvoice), - ); + // This test is temporarily disabled as manually_handle_bolt12_invoices and InvoiceReceived + // are no longer present. + todo!("Update this test when the relevant functionality is reintroduced."); } /// Checks that an offer can be created using an unannounced node as a blinded path's introduction diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index eb57c8a94c4..bf460d6be1e 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -475,14 +475,11 @@ impl_writeable_tlv_based_enum_legacy!(StaleExpiration, /// [`Event::PaymentFailed`]: crate::events::Event::PaymentFailed #[derive(Clone, Debug, PartialEq, Eq)] pub enum RetryableSendFailure { - /// The provided [`PaymentParameters::expiry_time`] indicated that the payment has expired or - /// the BOLT 12 invoice paid to via [`ChannelManager::send_payment_for_bolt12_invoice`] was - /// expired. + /// The provided [`PaymentParameters::expiry_time`] indicated that the payment has expired. #[cfg_attr(feature = "std", doc = "")] #[cfg_attr(feature = "std", doc = "Note that this error is *not* caused by [`Retry::Timeout`].")] /// /// [`PaymentParameters::expiry_time`]: crate::routing::router::PaymentParameters::expiry_time - /// [`ChannelManager::send_payment_for_bolt12_invoice`]: crate::ln::channelmanager::ChannelManager::send_payment_for_bolt12_invoice PaymentExpired, /// We were unable to find a route to the destination. RouteNotFound, diff --git a/lightning/src/util/config.rs b/lightning/src/util/config.rs index 745e13ae6c3..8fdd213445e 100644 --- a/lightning/src/util/config.rs +++ b/lightning/src/util/config.rs @@ -844,20 +844,6 @@ pub struct UserConfig { /// [`ChannelManager::get_intercept_scid`]: crate::ln::channelmanager::ChannelManager::get_intercept_scid /// [`Event::HTLCIntercepted`]: crate::events::Event::HTLCIntercepted pub accept_intercept_htlcs: bool, - /// If this is set to `true`, the user needs to manually pay [`Bolt12Invoice`]s when received. - /// - /// When set to `true`, [`Event::InvoiceReceived`] will be generated for each received - /// [`Bolt12Invoice`] instead of being automatically paid after verification. Use - /// [`ChannelManager::send_payment_for_bolt12_invoice`] to pay the invoice or - /// [`ChannelManager::abandon_payment`] to abandon the associated payment. - /// - /// Default value: `false` - /// - /// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice - /// [`Event::InvoiceReceived`]: crate::events::Event::InvoiceReceived - /// [`ChannelManager::send_payment_for_bolt12_invoice`]: crate::ln::channelmanager::ChannelManager::send_payment_for_bolt12_invoice - /// [`ChannelManager::abandon_payment`]: crate::ln::channelmanager::ChannelManager::abandon_payment - pub manually_handle_bolt12_invoices: bool, } impl Default for UserConfig { @@ -870,7 +856,6 @@ impl Default for UserConfig { accept_inbound_channels: true, manually_accept_inbound_channels: false, accept_intercept_htlcs: false, - manually_handle_bolt12_invoices: false, } } } @@ -889,7 +874,6 @@ impl Readable for UserConfig { accept_inbound_channels: Readable::read(reader)?, manually_accept_inbound_channels: Readable::read(reader)?, accept_intercept_htlcs: Readable::read(reader)?, - manually_handle_bolt12_invoices: Readable::read(reader)?, }) } } From b18d9b50b56162895b7e52e8491b76fe7a107611 Mon Sep 17 00:00:00 2001 From: shaavan Date: Tue, 12 Nov 2024 19:29:21 +0530 Subject: [PATCH 02/14] Move `message_received` from `ChannelMessageHandler` to `Offer/OnionMessageHandler` To decouple offers and onion message-related logic from `ChannelManager`, this commit introduces the `message_received` function in `Offer/OnionMessageHandler`. Previously, `message_received` existed in `ChannelMessageHandler`, but its sole responsibility was handling invoice request retries. By moving this function to `Offer/OnionMessageHandler`, we ensure a cleaner separation of concerns and align message processing with the appropriate handler. This transition allows invoice request retries to be managed directly within the offers/onion message context, laying the groundwork for further enhancements to message handling. --- lightning-net-tokio/src/lib.rs | 1 - lightning/src/ln/channelmanager.rs | 66 ++++++++++++------------ lightning/src/ln/msgs.rs | 19 ++++--- lightning/src/ln/offers_tests.rs | 4 +- lightning/src/ln/peer_handler.rs | 4 +- lightning/src/onion_message/messenger.rs | 4 ++ lightning/src/onion_message/offers.rs | 8 +++ lightning/src/util/test_utils.rs | 2 - 8 files changed, 59 insertions(+), 49 deletions(-) diff --git a/lightning-net-tokio/src/lib.rs b/lightning-net-tokio/src/lib.rs index 2ff88bc066a..0e43a3bb93f 100644 --- a/lightning-net-tokio/src/lib.rs +++ b/lightning-net-tokio/src/lib.rs @@ -787,7 +787,6 @@ mod tests { fn get_chain_hashes(&self) -> Option> { Some(vec![ChainHash::using_genesis_block(Network::Testnet)]) } - fn message_received(&self) {} } impl MessageSendEventsProvider for MsgHandler { fn get_and_clear_pending_msg_events(&self) -> Vec { diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 9266cc84194..181a57006b1 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -11985,39 +11985,6 @@ where NotifyOption::SkipPersistHandleEvents }); } - - fn message_received(&self) { - for (payment_id, retryable_invoice_request) in self - .pending_outbound_payments - .release_invoice_requests_awaiting_invoice() - { - let RetryableInvoiceRequest { invoice_request, nonce, .. } = retryable_invoice_request; - let hmac = payment_id.hmac_for_offer_payment(nonce, &self.inbound_payment_key); - let context = MessageContext::Offers(OffersContext::OutboundPayment { - payment_id, - nonce, - hmac: Some(hmac) - }); - match self.create_blinded_paths(context) { - Ok(reply_paths) => match self.enqueue_invoice_request(invoice_request, reply_paths) { - Ok(_) => {} - Err(_) => { - log_warn!(self.logger, - "Retry failed for an invoice request with payment_id: {}", - payment_id - ); - } - }, - Err(_) => { - log_warn!(self.logger, - "Retry failed for an invoice request with payment_id: {}. \ - Reason: router could not find a blinded path to include as the reply path", - payment_id - ); - } - } - } - } } impl @@ -12239,6 +12206,39 @@ where } } + fn message_received(&self) { + for (payment_id, retryable_invoice_request) in self + .pending_outbound_payments + .release_invoice_requests_awaiting_invoice() + { + let RetryableInvoiceRequest { invoice_request, nonce, .. } = retryable_invoice_request; + let hmac = payment_id.hmac_for_offer_payment(nonce, &self.inbound_payment_key); + let context = MessageContext::Offers(OffersContext::OutboundPayment { + payment_id, + nonce, + hmac: Some(hmac) + }); + match self.create_blinded_paths(context) { + Ok(reply_paths) => match self.enqueue_invoice_request(invoice_request, reply_paths) { + Ok(_) => {} + Err(_) => { + log_warn!(self.logger, + "Retry failed for an invoice request with payment_id: {}", + payment_id + ); + } + }, + Err(_) => { + log_warn!(self.logger, + "Retry failed for an invoice request with payment_id: {}. \ + Reason: router could not find a blinded path to include as the reply path", + payment_id + ); + } + } + } + } + fn release_pending_messages(&self) -> Vec<(OffersMessage, MessageSendInstructions)> { core::mem::take(&mut self.pending_offers_messages.lock().unwrap()) } diff --git a/lightning/src/ln/msgs.rs b/lightning/src/ln/msgs.rs index 3601445f4a5..84dda520592 100644 --- a/lightning/src/ln/msgs.rs +++ b/lightning/src/ln/msgs.rs @@ -1610,14 +1610,6 @@ pub trait ChannelMessageHandler : MessageSendEventsProvider { /// If it's `None`, then no particular network chain hash compatibility will be enforced when /// connecting to peers. fn get_chain_hashes(&self) -> Option>; - - /// Indicates that a message was received from any peer for any handler. - /// Called before the message is passed to the appropriate handler. - /// Useful for indicating that a network connection is active. - /// - /// Note: Since this function is called frequently, it should be as - /// efficient as possible for its intended purpose. - fn message_received(&self); } /// A trait to describe an object which can receive routing messages. @@ -1738,6 +1730,17 @@ pub trait OnionMessageHandler { /// /// Note that this method is called before [`Self::peer_connected`]. fn provided_init_features(&self, their_node_id: PublicKey) -> InitFeatures; + + /// Indicates that a message was received from any peer for any handler. + /// + /// This function delegates to the underlying [`OffersMessageHandler::message_received`]. + /// Refer to its documentation for more details on the behavior and implementation. + /// + /// **Note:** Since this function is called frequently, it should be implemented + /// with efficiency in mind to minimize performance overhead. + /// + /// [`OffersMessageHandler::message_received`]: crate::onion_message::offers::OffersMessageHandler::message_received + fn message_received(&self) {} } #[derive(Clone, Debug, PartialEq, Eq)] diff --git a/lightning/src/ln/offers_tests.rs b/lightning/src/ln/offers_tests.rs index 32362f1aff7..95fed704a8a 100644 --- a/lightning/src/ln/offers_tests.rs +++ b/lightning/src/ln/offers_tests.rs @@ -1102,7 +1102,7 @@ fn creates_and_pays_for_offer_with_retry() { // Simulate a scenario where the original onion_message is lost before reaching Alice. // Use handle_message_received to regenerate the message. - bob.node.message_received(); + bob.onion_messenger.message_received(); let onion_message = bob.onion_messenger.next_onion_message_for_peer(alice_id).unwrap(); alice.onion_messenger.handle_onion_message(bob_id, &onion_message); @@ -1125,7 +1125,7 @@ fn creates_and_pays_for_offer_with_retry() { // Expect no more OffersMessage to be enqueued by this point, even after calling // handle_message_received. - bob.node.message_received(); + bob.onion_messenger.message_received(); assert!(bob.onion_messenger.next_onion_message_for_peer(alice_id).is_none()); diff --git a/lightning/src/ln/peer_handler.rs b/lightning/src/ln/peer_handler.rs index 8df168fee12..9b332cf8d9d 100644 --- a/lightning/src/ln/peer_handler.rs +++ b/lightning/src/ln/peer_handler.rs @@ -403,8 +403,6 @@ impl ChannelMessageHandler for ErroringMessageHandler { fn handle_tx_abort(&self, their_node_id: PublicKey, msg: &msgs::TxAbort) { ErroringMessageHandler::push_error(self, their_node_id, msg.channel_id); } - - fn message_received(&self) {} } impl Deref for ErroringMessageHandler { @@ -1643,7 +1641,7 @@ impl, responder: Option, ) -> Option<(OffersMessage, ResponseInstruction)>; + /// Indicates that a message was received from any peer for any handler. + /// Called before the message is passed to the appropriate handler. + /// Useful for indicating that a network connection is active. + /// + /// Note: Since this function is called frequently, it should be as + /// efficient as possible for its intended purpose. + fn message_received(&self) {} + /// Releases any [`OffersMessage`]s that need to be sent. /// /// Typically, this is used for messages initiating a payment flow rather than in response to diff --git a/lightning/src/util/test_utils.rs b/lightning/src/util/test_utils.rs index 22853787974..3e382d8f15f 100644 --- a/lightning/src/util/test_utils.rs +++ b/lightning/src/util/test_utils.rs @@ -1117,8 +1117,6 @@ impl msgs::ChannelMessageHandler for TestChannelMessageHandler { fn handle_tx_abort(&self, _their_node_id: PublicKey, msg: &msgs::TxAbort) { self.received_msg(wire::Message::TxAbort(msg.clone())); } - - fn message_received(&self) {} } impl events::MessageSendEventsProvider for TestChannelMessageHandler { From cea31685d8fbf3b8f10bd6de70e2529a65cc0d3a Mon Sep 17 00:00:00 2001 From: shaavan Date: Thu, 13 Feb 2025 19:05:45 +0530 Subject: [PATCH 03/14] Include flow.rs in rustfmt exclusion list Following commits will move a lot of code from ChannelManager to Flow.rs So to make it easier to review the changes, we temporarily introduce flow.rs to the rustfmt exclusion list. --- rustfmt_excluded_files | 1 + 1 file changed, 1 insertion(+) diff --git a/rustfmt_excluded_files b/rustfmt_excluded_files index 7899a65ee67..76b72cc2e33 100644 --- a/rustfmt_excluded_files +++ b/rustfmt_excluded_files @@ -43,3 +43,4 @@ lightning/src/routing/router.rs lightning/src/routing/scoring.rs lightning/src/routing/test_utils.rs lightning/src/routing/utxo.rs +lightning/src/offers/flow.rs From 392c320c8189b1f0c1405c0f01a9f1a7ce92cbe9 Mon Sep 17 00:00:00 2001 From: shaavan Date: Fri, 15 Nov 2024 17:56:06 +0530 Subject: [PATCH 04/14] Introduce `OffersMessageFlow` This commit introduces a new struct, `OffersMessageFlow`, to extract all offers message-related code out of `ChannelManager`. By moving this logic into a dedicated struct, it creates a foundation for separating responsibilities and sets up a base for further code restructuring in subsequent commits. --- lightning/src/ln/functional_test_utils.rs | 8 + lightning/src/offers/flow.rs | 211 ++++++++++++++++++++++ lightning/src/offers/mod.rs | 1 + 3 files changed, 220 insertions(+) create mode 100644 lightning/src/offers/flow.rs diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index 7822bdac9f1..e9afb7404f5 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -16,6 +16,7 @@ use crate::chain::transaction::OutPoint; use crate::events::{ClaimedHTLC, ClosureReason, Event, HTLCDestination, MessageSendEvent, MessageSendEventsProvider, PathFailure, PaymentPurpose, PaymentFailureReason}; use crate::events::bump_transaction::{BumpTransactionEvent, BumpTransactionEventHandler, Wallet, WalletSource}; use crate::ln::types::ChannelId; +use crate::offers::flow::OffersMessageFlow; use crate::types::payment::{PaymentPreimage, PaymentHash, PaymentSecret}; use crate::ln::channelmanager::{AChannelManager, ChainParameters, ChannelManager, ChannelManagerReadArgs, RAACommitmentOrder, RecipientOnionFields, PaymentId, MIN_CLTV_EXPIRY_DELTA}; use crate::types::features::InitFeatures; @@ -411,6 +412,13 @@ type TestChannelManager<'node_cfg, 'chan_mon_cfg> = ChannelManager< &'chan_mon_cfg test_utils::TestLogger, >; +pub type TestOffersMessageFlow<'chan_man, 'node_cfg, 'chan_mon_cfg> = OffersMessageFlow< + &'node_cfg test_utils::TestKeysInterface, + &'chan_man TestChannelManager<'node_cfg, 'chan_mon_cfg>, + &'node_cfg test_utils::TestMessageRouter<'chan_mon_cfg>, + &'chan_mon_cfg test_utils::TestLogger, +>; + #[cfg(not(feature = "dnssec"))] type TestOnionMessenger<'chan_man, 'node_cfg, 'chan_mon_cfg> = OnionMessenger< DedicatedEntropy, diff --git a/lightning/src/offers/flow.rs b/lightning/src/offers/flow.rs new file mode 100644 index 00000000000..46306dbc027 --- /dev/null +++ b/lightning/src/offers/flow.rs @@ -0,0 +1,211 @@ +// 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. + +//! Provides data structures and functions for creating and managing Offers messages, +//! facilitating communication, and handling Bolt12 invoice payments. + +use bitcoin::secp256k1; +use bitcoin::{key::Secp256k1, PublicKey}; + +use crate::ln::inbound_payment; +use crate::prelude::*; +use crate::sign::EntropySource; +use core::ops::Deref; + +use crate::onion_message::messenger::MessageRouter; +use crate::util::logger::Logger; + +#[cfg(not(c_bindings))] +use { + crate::ln::channelmanager::{SimpleArcChannelManager, SimpleRefChannelManager}, + crate::onion_message::messenger::DefaultMessageRouter, + crate::routing::gossip::NetworkGraph, + crate::sign::KeysManager, + crate::sync::Arc, +}; + +/// Functions commonly shared in usage between [`ChannelManager`] & [`OffersMessageFlow`] +/// +/// [`ChannelManager`]: crate::ln::channelmanager::ChannelManager +pub trait OffersMessageCommons {} + +/// [`SimpleArcOffersMessageFlow`] is useful when you need a [`OffersMessageFlow`] with a static lifetime, e.g. +/// when you're using `lightning-net-tokio` (since `tokio::spawn` requires parameters with static +/// lifetimes). Other times you can afford a reference, which is more efficient, in which case +/// [`SimpleRefOffersMessageFlow`] is the more appropriate type. Defining these type aliases prevents +/// issues such as overly long function definitions. Note that the `OffersMessageFlow` can take any type +/// that implements [`EntropySource`], for its keys manager, [`MessageRouter`] for its message router, or +/// [`OffersMessageCommons`] for its shared core functionalities. But this type alias chooses the concrete types +/// of [`KeysManager`] and [`SimpleArcChannelManager`] and [`DefaultMessageRouter`]. +/// +/// This is not exported to bindings users as type aliases aren't supported in most languages. +#[cfg(not(c_bindings))] +pub type SimpleArcOffersMessageFlow = OffersMessageFlow< + Arc, + Arc>, + Arc>>, Arc, Arc>>, + Arc, +>; + +/// [`SimpleRefOffersMessageFlow`] is a type alias for a OffersMessageFlow reference, and is the reference +/// counterpart to the [`SimpleArcOffersMessageFlow`] type alias. Use this type by default when you don't +/// need a OffersMessageFlow with a static lifetime. You'll need a static lifetime in cases such as +/// usage of lightning-net-tokio (since `tokio::spawn` requires parameters with static lifetimes). +/// But if this is not necessary, using a reference is more efficient. Defining these type aliases +/// issues such as overly long function definitions. Note that the `OffersMessageFlow` can take any type +/// that implements [`EntropySource`], for its keys manager, [`MessageRouter`] for its message router, or +/// [`OffersMessageCommons`] for its shared core functionalities. But this type alias chooses the concrete types +/// of [`KeysManager`] and [`SimpleArcChannelManager`] and [`DefaultMessageRouter`]. +/// +/// This is not exported to bindings users as type aliases aren't supported in most languages. +#[cfg(not(c_bindings))] +pub type SimpleRefOffersMessageFlow<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, 'i, 'j, M, T, F, L> = + OffersMessageFlow< + &'a KeysManager, + &'j SimpleRefChannelManager<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, 'i, M, T, F, L>, + &'i DefaultMessageRouter<&'g NetworkGraph<&'b L>, &'b L, &'a KeysManager>, + &'g L, + >; + +/// A trivial trait which describes any [`OffersMessageFlow`]. +/// +/// This is not exported to bindings users as general cover traits aren't useful in other +/// languages. +pub trait AnOffersMessageFlow { + /// A type implementing [`EntropySource`]. + type EntropySource: EntropySource + ?Sized; + /// A type that may be dereferenced to [`Self::EntropySource`]. + type ES: Deref; + + /// A type implementing [`OffersMessageCommons`]. + type OffersMessageCommons: OffersMessageCommons + ?Sized; + /// A type that may be dereferenced to [`Self::OffersMessageCommons`]. + type OMC: Deref; + + /// A type implementing [`MessageRouter`]. + type MessageRouter: MessageRouter + ?Sized; + /// A type that may be dereferenced to [`Self::MessageRouter`]. + type MR: Deref; + + /// A type implementing [`Logger`]. + type Logger: Logger + ?Sized; + /// A type that may be dereferenced to [`Self::Logger`]. + type L: Deref; + + /// Returns a reference to the actual [`OffersMessageFlow`] object. + fn get_omf(&self) -> &OffersMessageFlow; +} + +impl AnOffersMessageFlow + for OffersMessageFlow +where + ES::Target: EntropySource, + OMC::Target: OffersMessageCommons, + MR::Target: MessageRouter, + L::Target: Logger, +{ + type EntropySource = ES::Target; + type ES = ES; + + type OffersMessageCommons = OMC::Target; + type OMC = OMC; + + type MessageRouter = MR::Target; + type MR = MR; + + type Logger = L::Target; + type L = L; + + fn get_omf(&self) -> &OffersMessageFlow { + self + } +} + +/// Facilitates the handling, communication, and management of Offers messages within a Lightning +/// node, enabling the creation, verification, and resolution of BOLT 12 invoices and related +/// payment flows. +/// +/// The `OffersMessageFlow` struct integrates several components to manage the lifecycle of Offers +/// messages, ensuring robust communication and payment handling: +/// - EntropySource to provide cryptographic randomness essential for Offers message handling. +/// - [`Logger`] for detailed operational logging of Offers-related activity. +/// - OffersMessageCommons for core operations shared across Offers messages, such as metadata +/// verification and signature handling. +/// - MessageRouter for routing Offers messages to their appropriate destinations within the +/// Lightning network. +/// - Manages OffersMessage for creating and processing Offers-related messages. +/// - Handles [`DNSResolverMessage`] for resolving human-readable names in Offers messages +/// (when the `dnssec` feature is enabled). +/// +/// Key Features: +/// - Supports creating BOLT 12 Offers, invoice requests, and refunds. +/// - Integrates with the Lightning node's broader message and payment infrastructure. +/// - Handles cryptographic operations and message validation to ensure compliance with BOLT 12. +/// - Supports DNS resolution for human-readable names (when enabled with `dnssec` feature). +/// +/// This struct is essential for enabling BOLT12 payment workflows in the Lightning network, +/// providing the foundational mechanisms for Offers and related message exchanges. +/// +/// [`DNSResolverMessage`]: crate::onion_message::dns_resolution::DNSResolverMessage +pub struct OffersMessageFlow +where + ES::Target: EntropySource, + OMC::Target: OffersMessageCommons, + MR::Target: MessageRouter, + L::Target: Logger, +{ + secp_ctx: Secp256k1, + our_network_pubkey: PublicKey, + inbound_payment_key: inbound_payment::ExpandedKey, + + /// Contains functions shared between OffersMessageHandler and ChannelManager. + commons: OMC, + + message_router: MR, + entropy_source: ES, + + /// The Logger for use in the OffersMessageFlow and which may be used to log + /// information during deserialization. + pub logger: L, +} + +impl OffersMessageFlow +where + ES::Target: EntropySource, + OMC::Target: OffersMessageCommons, + MR::Target: MessageRouter, + L::Target: Logger, +{ + /// Creates a new [`OffersMessageFlow`] + pub fn new( + expanded_inbound_key: inbound_payment::ExpandedKey, our_network_pubkey: PublicKey, + entropy_source: ES, commons: OMC, message_router: MR, logger: L, + ) -> Self { + let mut secp_ctx = Secp256k1::new(); + secp_ctx.seeded_randomize(&entropy_source.get_secure_random_bytes()); + + Self { + secp_ctx, + our_network_pubkey, + inbound_payment_key: expanded_inbound_key, + + commons, + + message_router, + entropy_source, + + logger, + } + } + + /// Gets the node_id held by this OffersMessageFlow + pub fn get_our_node_id(&self) -> PublicKey { + self.our_network_pubkey + } +} diff --git a/lightning/src/offers/mod.rs b/lightning/src/offers/mod.rs index e4fe7d789db..ef34f15e0b7 100644 --- a/lightning/src/offers/mod.rs +++ b/lightning/src/offers/mod.rs @@ -12,6 +12,7 @@ //! //! Offers are a flexible protocol for Lightning payments. +pub mod flow; #[macro_use] pub mod offer; From 9de4add3f26063489f035ee5b9e3cab20a82cfec Mon Sep 17 00:00:00 2001 From: shaavan Date: Wed, 22 Jan 2025 17:55:02 +0530 Subject: [PATCH 05/14] Introduce `OffersMessageCommons` Trait and Implementation A new trait, `OffersMessageCommons`, is introduced to encapsulate functions that are commonly used by both BOLT12-related functionality and other parts of `ChannelManager`. This enables a clean transition of BOLT12 code to `OffersMessageFlow` by moving shared functions into the new trait, ensuring they remain accessible to both `ChannelManager` and the refactored BOLT12 code. --- lightning/src/ln/channelmanager.rs | 87 ++++++++++++++++++++- lightning/src/ln/mod.rs | 2 +- lightning/src/ln/outbound_payment.rs | 2 +- lightning/src/offers/flow.rs | 111 ++++++++++++++++++++++++++- 4 files changed, 196 insertions(+), 6 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 181a57006b1..c8e5256b699 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -28,7 +28,7 @@ use bitcoin::hashes::hmac::Hmac; use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::hash_types::{BlockHash, Txid}; -use bitcoin::secp256k1::{SecretKey,PublicKey}; +use bitcoin::secp256k1::{schnorr, PublicKey, SecretKey}; use bitcoin::secp256k1::Secp256k1; use bitcoin::{secp256k1, Sequence}; @@ -47,6 +47,7 @@ use crate::events::{self, Event, EventHandler, EventsProvider, InboundChannelFun // construct one themselves. use crate::ln::inbound_payment; use crate::ln::types::ChannelId; +use crate::offers::flow::OffersMessageCommons; use crate::types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; use crate::ln::channel::{self, Channel, ChannelError, ChannelUpdateStatus, FundedChannel, ShutdownResult, UpdateFulfillCommitFetch, OutboundV1Channel, ReconnectionMsg, InboundV1Channel, WithChannelContext}; #[cfg(any(dual_funding, splicing))] @@ -122,7 +123,7 @@ use core::{cmp, mem}; use core::borrow::Borrow; use core::cell::RefCell; use crate::io::Read; -use crate::sync::{Arc, Mutex, RwLock, RwLockReadGuard, FairRwLock, LockTestExt, LockHeldState}; +use crate::sync::{Arc, FairRwLock, LockHeldState, LockTestExt, Mutex, MutexGuard, RwLock, RwLockReadGuard}; use core::sync::atomic::{AtomicUsize, AtomicBool, Ordering}; use core::time::Duration; use core::ops::Deref; @@ -9932,6 +9933,88 @@ macro_rules! create_refund_builder { ($self: ident, $builder: ty) => { } } } +impl OffersMessageCommons for ChannelManager +where + M::Target: chain::Watch<::EcdsaSigner>, + T::Target: BroadcasterInterface, + ES::Target: EntropySource, + NS::Target: NodeSigner, + SP::Target: SignerProvider, + F::Target: FeeEstimator, + R::Target: Router, + MR::Target: MessageRouter, + L::Target: Logger, +{ + fn get_current_blocktime(&self) -> Duration { + Duration::from_secs(self.highest_seen_timestamp.load(Ordering::Acquire) as u64) + } + + fn get_peer_for_blinded_path(&self) -> Vec { + self.per_peer_state.read().unwrap() + .iter() + .map(|(node_id, peer_state)| (node_id, peer_state.lock().unwrap())) + .filter(|(_, peer)| peer.is_connected) + .filter(|(_, peer)| peer.latest_features.supports_onion_messages()) + .map(|(node_id, peer)| MessageForwardNode { + node_id: *node_id, + short_channel_id: peer.channel_by_id + .iter() + .filter(|(_, channel)| channel.context().is_usable()) + .min_by_key(|(_, channel)| channel.context().channel_creation_height) + .and_then(|(_, channel)| channel.context().get_short_channel_id()), + }) + .collect::>() + } + + fn create_blinded_payment_paths( + &self, amount_msats: Option, payment_secret: PaymentSecret, payment_context: PaymentContext, relative_expiry_time: u32, + ) -> Result, ()> { + self.create_blinded_payment_paths(amount_msats, payment_secret, payment_context, relative_expiry_time) + } + + fn create_inbound_payment(&self, min_value_msat: Option, invoice_expiry_delta_secs: u32, + min_final_cltv_expiry_delta: Option) -> Result<(PaymentHash, PaymentSecret), ()> { + self.create_inbound_payment(min_value_msat, invoice_expiry_delta_secs, min_final_cltv_expiry_delta) + } + + fn release_invoice_requests_awaiting_invoice(&self) -> Vec<(PaymentId, RetryableInvoiceRequest)> { + self.pending_outbound_payments.release_invoice_requests_awaiting_invoice() + } + + fn sign_bolt12_invoice( + &self, invoice: &UnsignedBolt12Invoice, + ) -> Result { + self.node_signer.sign_bolt12_invoice(invoice) + } + + fn send_payment_for_verified_bolt12_invoice(&self, invoice: &Bolt12Invoice, payment_id: PaymentId) -> Result<(), Bolt12PaymentError> { + self.send_payment_for_verified_bolt12_invoice(invoice, payment_id) + } + + fn abandon_payment_with_reason(&self, payment_id: PaymentId, reason: PaymentFailureReason) { + self.abandon_payment_with_reason(payment_id, reason); + } + + // ----Temporary Functions---- + // Set of functions temporarily moved to OffersMessageCommons for easier + // transition of code from ChannelManager to OffersMessageFlow + + fn get_pending_offers_messages(&self) -> MutexGuard<'_, Vec<(OffersMessage, MessageSendInstructions)>> { + self.pending_offers_messages.lock().expect("Mutex is locked by other thread.") + } + + fn enqueue_invoice_request(&self, invoice_request: InvoiceRequest, reply_paths: Vec) -> Result<(), Bolt12SemanticError> { + self.enqueue_invoice_request(invoice_request, reply_paths) + } + + #[cfg(async_payments)] + fn initiate_async_payment( + &self, invoice: &StaticInvoice, payment_id: PaymentId + ) -> Result<(), Bolt12PaymentError> { + self.initiate_async_payment(invoice, payment_id) + } +} + /// Defines the maximum number of [`OffersMessage`] including different reply paths to be sent /// along different paths. /// Sending multiple requests increases the chances of successful delivery in case some diff --git a/lightning/src/ln/mod.rs b/lightning/src/ln/mod.rs index 201f2ecfeff..70f32dffa1f 100644 --- a/lightning/src/ln/mod.rs +++ b/lightning/src/ln/mod.rs @@ -41,7 +41,7 @@ pub mod channel; pub(crate) mod channel; pub(crate) mod onion_utils; -mod outbound_payment; +pub(crate) mod outbound_payment; pub mod wire; #[allow(dead_code)] // TODO(dual_funding): Remove once contribution to V2 channels is enabled. diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index bf460d6be1e..766a4db5c0d 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -140,7 +140,7 @@ pub(crate) enum PendingOutboundPayment { } #[derive(Clone)] -pub(crate) struct RetryableInvoiceRequest { +pub struct RetryableInvoiceRequest { pub(crate) invoice_request: InvoiceRequest, pub(crate) nonce: Nonce, pub(super) needs_retry: bool, diff --git a/lightning/src/offers/flow.rs b/lightning/src/offers/flow.rs index 46306dbc027..a77c0a25ec1 100644 --- a/lightning/src/offers/flow.rs +++ b/lightning/src/offers/flow.rs @@ -17,10 +17,29 @@ use crate::ln::inbound_payment; use crate::prelude::*; use crate::sign::EntropySource; use core::ops::Deref; +use core::time::Duration; +use bitcoin::secp256k1::schnorr; +use lightning_invoice::PaymentSecret; +use types::payment::PaymentHash; + +use crate::blinded_path::message::{BlindedMessagePath, MessageForwardNode}; +use crate::blinded_path::payment::{BlindedPaymentPath, PaymentContext}; +use crate::events::PaymentFailureReason; +use crate::ln::channelmanager::{Bolt12PaymentError, PaymentId}; +use crate::ln::outbound_payment::RetryableInvoiceRequest; +use crate::offers::invoice::{Bolt12Invoice, UnsignedBolt12Invoice}; +use crate::offers::invoice_request::InvoiceRequest; +use crate::offers::parse::Bolt12SemanticError; +use crate::onion_message::messenger::MessageSendInstructions; +use crate::onion_message::offers::OffersMessage; +use crate::sync::MutexGuard; use crate::onion_message::messenger::MessageRouter; use crate::util::logger::Logger; +#[cfg(async_payments)] +use crate::offers::static_invoice::StaticInvoice; + #[cfg(not(c_bindings))] use { crate::ln::channelmanager::{SimpleArcChannelManager, SimpleRefChannelManager}, @@ -30,10 +49,98 @@ use { crate::sync::Arc, }; -/// Functions commonly shared in usage between [`ChannelManager`] & [`OffersMessageFlow`] +/// Functions commonly shared in usage between [`ChannelManager`] & `OffersMessageFlow` /// /// [`ChannelManager`]: crate::ln::channelmanager::ChannelManager -pub trait OffersMessageCommons {} +pub trait OffersMessageCommons { + /// Get the current time determined by highest seen timestamp + fn get_current_blocktime(&self) -> Duration; + + /// Get the vector of peers that can be used for a blinded path + fn get_peer_for_blinded_path(&self) -> Vec; + + /// Creates multi-hop blinded payment paths for the given `amount_msats` by delegating to + /// [`Router::create_blinded_payment_paths`]. + /// + /// [`Router::create_blinded_payment_paths`]: crate::routing::router::Router::create_blinded_payment_paths + fn create_blinded_payment_paths( + &self, amount_msats: Option, payment_secret: PaymentSecret, payment_context: PaymentContext, relative_expiry_time: u32, + ) -> Result, ()>; + + /// Gets a payment secret and payment hash for use in an invoice given to a third party wishing + /// to pay us. + /// + /// This differs from [`create_inbound_payment_for_hash`] only in that it generates the + /// [`PaymentHash`] and [`PaymentPreimage`] for you. + /// + /// The [`PaymentPreimage`] will ultimately be returned to you in the [`PaymentClaimable`] event, which + /// will have the [`PaymentClaimable::purpose`] return `Some` for [`PaymentPurpose::preimage`]. That + /// should then be passed directly to [`claim_funds`]. + /// + /// See [`create_inbound_payment_for_hash`] for detailed documentation on behavior and requirements. + /// + /// Note that a malicious eavesdropper can intuit whether an inbound payment was created by + /// `create_inbound_payment` or `create_inbound_payment_for_hash` based on runtime. + /// + /// # Note + /// + /// If you register an inbound payment with this method, then serialize the `ChannelManager`, then + /// deserialize it with a node running 0.0.103 and earlier, the payment will fail to be received. + /// + /// Errors if `min_value_msat` is greater than total bitcoin supply. + /// + /// If `min_final_cltv_expiry_delta` is set to some value, then the payment will not be receivable + /// on versions of LDK prior to 0.0.114. + /// + /// [`claim_funds`]: crate::ln::channelmanager::ChannelManager::claim_funds + /// [`PaymentClaimable`]: crate::events::Event::PaymentClaimable + /// [`PaymentClaimable::purpose`]: crate::events::Event::PaymentClaimable::purpose + /// [`PaymentPurpose::preimage`]: crate::events::PaymentPurpose::preimage + /// [`create_inbound_payment_for_hash`]: crate::ln::channelmanager::ChannelManager::create_inbound_payment_for_hash + /// [`PaymentPreimage`]: crate::types::payment::PaymentPreimage + fn create_inbound_payment(&self, min_value_msat: Option, invoice_expiry_delta_secs: u32, + min_final_cltv_expiry_delta: Option) -> Result<(PaymentHash, PaymentSecret), ()>; + + /// Release invoice requests awaiting invoice + fn release_invoice_requests_awaiting_invoice(&self) -> Vec<(PaymentId, RetryableInvoiceRequest)>; + + /// Signs the [`TaggedHash`] of a BOLT 12 invoice. + /// + /// May be called by a function passed to [`UnsignedBolt12Invoice::sign`] where `invoice` is the + /// callee. + /// + /// Implementors may check that the `invoice` is expected rather than blindly signing the tagged + /// hash. An `Ok` result should sign `invoice.tagged_hash().as_digest()` with the node's signing + /// key or an ephemeral key to preserve privacy, whichever is associated with + /// [`UnsignedBolt12Invoice::signing_pubkey`]. + /// + /// [`TaggedHash`]: crate::offers::merkle::TaggedHash + fn sign_bolt12_invoice( + &self, invoice: &UnsignedBolt12Invoice, + ) -> Result; + + /// Send payment for verified bolt12 invoice + fn send_payment_for_verified_bolt12_invoice(&self, invoice: &Bolt12Invoice, payment_id: PaymentId) -> Result<(), Bolt12PaymentError>; + + /// Abandon Payment with Reason + fn abandon_payment_with_reason(&self, payment_id: PaymentId, reason: PaymentFailureReason); + + // ----Temporary Functions---- + // Set of functions temporarily moved to OffersMessageCommons for easier + // transition of code from ChannelManager to OffersMessageFlow + + /// Get pending offers messages + fn get_pending_offers_messages(&self) -> MutexGuard<'_, Vec<(OffersMessage, MessageSendInstructions)>>; + + /// Enqueue invoice request + fn enqueue_invoice_request(&self, invoice_request: InvoiceRequest, reply_paths: Vec) -> Result<(), Bolt12SemanticError>; + + /// Initiate a new async payment + #[cfg(async_payments)] + fn initiate_async_payment( + &self, invoice: &StaticInvoice, payment_id: PaymentId + ) -> Result<(), Bolt12PaymentError>; +} /// [`SimpleArcOffersMessageFlow`] is useful when you need a [`OffersMessageFlow`] with a static lifetime, e.g. /// when you're using `lightning-net-tokio` (since `tokio::spawn` requires parameters with static From d94a368310ced06a864f6ae0ead37c9f0c8257df Mon Sep 17 00:00:00 2001 From: shaavan Date: Thu, 13 Feb 2025 19:42:03 +0530 Subject: [PATCH 06/14] Refactor OffersMessageHandler Implementation to OffersMessageFlow This commit introduces the `OffersMessageHandler` implementation for `OffersMessageFlow`, enabling direct access to offer-specific functionality through `OffersMessageFlow`. With `OffersMessageFlow` now serving as the source of `OffersMessageHandler` implementation, the implementation in `ChannelManager` is no longer needed and has been safely removed. --- lightning/src/ln/channelmanager.rs | 317 ++--------------- lightning/src/ln/functional_test_utils.rs | 23 +- .../src/ln/max_payment_path_len_tests.rs | 2 +- lightning/src/offers/flow.rs | 324 +++++++++++++++++- lightning/src/onion_message/messenger.rs | 5 +- 5 files changed, 360 insertions(+), 311 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index c8e5256b699..80f7106c565 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -67,8 +67,7 @@ use crate::ln::msgs::{ChannelMessageHandler, CommitmentUpdate, DecodeError, Ligh #[cfg(test)] use crate::ln::outbound_payment; use crate::ln::outbound_payment::{OutboundPayments, PendingOutboundPayment, RetryableInvoiceRequest, SendAlongPathArgs, StaleExpiration}; -use crate::offers::invoice::{Bolt12Invoice, DEFAULT_RELATIVE_EXPIRY, DerivedSigningPubkey, ExplicitSigningPubkey, InvoiceBuilder, UnsignedBolt12Invoice}; -use crate::offers::invoice_error::InvoiceError; +use crate::offers::invoice::{Bolt12Invoice, DEFAULT_RELATIVE_EXPIRY, DerivedSigningPubkey, InvoiceBuilder, UnsignedBolt12Invoice}; use crate::offers::invoice_request::{InvoiceRequest, InvoiceRequestBuilder}; use crate::offers::nonce::Nonce; use crate::offers::offer::{Offer, OfferBuilder}; @@ -78,7 +77,7 @@ use crate::offers::signer; use crate::onion_message::async_payments::{AsyncPaymentsMessage, HeldHtlcAvailable, ReleaseHeldHtlc, AsyncPaymentsMessageHandler}; use crate::onion_message::dns_resolution::HumanReadableName; use crate::onion_message::messenger::{Destination, MessageRouter, Responder, ResponseInstruction, MessageSendInstructions}; -use crate::onion_message::offers::{OffersMessage, OffersMessageHandler}; +use crate::onion_message::offers::OffersMessage; use crate::sign::{EntropySource, NodeSigner, Recipient, SignerProvider}; use crate::sign::ecdsa::EcdsaChannelSigner; use crate::util::config::{UserConfig, ChannelConfig, ChannelConfigUpdate}; @@ -1662,7 +1661,6 @@ where /// Additionally, it implements the following traits: /// - [`ChannelMessageHandler`] to handle off-chain channel activity from peers /// - [`MessageSendEventsProvider`] to similarly send such messages to peers -/// - [`OffersMessageHandler`] for BOLT 12 message handling and sending /// - [`EventsProvider`] to generate user-actionable [`Event`]s /// - [`chain::Listen`] and [`chain::Confirm`] for notification of on-chain activity /// @@ -2086,10 +2084,10 @@ where /// /// The [`offers`] module is useful for creating BOLT 12 offers. An [`Offer`] is a precursor to a /// [`Bolt12Invoice`], which must first be requested by the payer. The interchange of these messages -/// as defined in the specification is handled by [`ChannelManager`] and its implementation of -/// [`OffersMessageHandler`]. However, this only works with an [`Offer`] created using a builder -/// returned by [`create_offer_builder`]. With this approach, BOLT 12 offers and invoices are -/// stateless just as BOLT 11 invoices are. +/// as defined in the specification is handled by [`ChannelManager`] and [`OffersMessageFlow`]'s +/// implementation of [`OffersMessageHandler`]. However, this only works with an [`Offer`] created +/// using a builder returned by [`create_offer_builder`]. With this approach, BOLT 12 offers and +/// invoices are stateless just as BOLT 11 invoices are. /// /// ``` /// # use lightning::events::{Event, EventsProvider, PaymentPurpose}; @@ -2385,6 +2383,8 @@ where /// [`update_channel`]: chain::Watch::update_channel /// [`ChannelUpdate`]: msgs::ChannelUpdate /// [`read`]: ReadableArgs::read +/// [`OffersMessageFlow`]: crate::offers::flow::OffersMessageFlow +/// [`OffersMessageHandler`]: crate::onion_message::offers::OffersMessageHandler // // Lock order: // The tree structure below illustrates the lock order requirements for the different locks of the @@ -2555,7 +2555,7 @@ where our_network_pubkey: PublicKey, - inbound_payment_key: inbound_payment::ExpandedKey, + pub(super) inbound_payment_key: inbound_payment::ExpandedKey, /// LDK puts the [fake scids] that it generates into namespaces, to identify the type of an /// incoming payment. To make it harder for a third-party to identify the type of a payment, @@ -4758,23 +4758,6 @@ where self.pending_outbound_payments.test_set_payment_metadata(payment_id, new_payment_metadata); } - fn verify_bolt12_invoice( - &self, invoice: &Bolt12Invoice, context: Option<&OffersContext>, - ) -> Result { - let secp_ctx = &self.secp_ctx; - let expanded_key = &self.inbound_payment_key; - - match context { - None if invoice.is_for_refund_without_paths() => { - invoice.verify_using_metadata(expanded_key, secp_ctx) - }, - Some(&OffersContext::OutboundPayment { payment_id, nonce, .. }) => { - invoice.verify_using_payer_data(payment_id, nonce, expanded_key, secp_ctx) - }, - _ => Err(()), - } - } - fn send_payment_for_verified_bolt12_invoice(&self, invoice: &Bolt12Invoice, payment_id: PaymentId) -> Result<(), Bolt12PaymentError> { let best_block_height = self.best_block.read().unwrap().height; let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self); @@ -9948,7 +9931,12 @@ where fn get_current_blocktime(&self) -> Duration { Duration::from_secs(self.highest_seen_timestamp.load(Ordering::Acquire) as u64) } - + + #[cfg(not(feature = "std"))] + fn get_highest_seen_timestamp(&self) -> Duration { + Duration::from_secs(self.highest_seen_timestamp.load(Ordering::Acquire) as u64) + } + fn get_peer_for_blinded_path(&self) -> Vec { self.per_peer_state.read().unwrap() .iter() @@ -9965,36 +9953,36 @@ where }) .collect::>() } - + fn create_blinded_payment_paths( &self, amount_msats: Option, payment_secret: PaymentSecret, payment_context: PaymentContext, relative_expiry_time: u32, ) -> Result, ()> { self.create_blinded_payment_paths(amount_msats, payment_secret, payment_context, relative_expiry_time) } - + fn create_inbound_payment(&self, min_value_msat: Option, invoice_expiry_delta_secs: u32, min_final_cltv_expiry_delta: Option) -> Result<(PaymentHash, PaymentSecret), ()> { self.create_inbound_payment(min_value_msat, invoice_expiry_delta_secs, min_final_cltv_expiry_delta) } - + fn release_invoice_requests_awaiting_invoice(&self) -> Vec<(PaymentId, RetryableInvoiceRequest)> { self.pending_outbound_payments.release_invoice_requests_awaiting_invoice() } - + fn sign_bolt12_invoice( &self, invoice: &UnsignedBolt12Invoice, ) -> Result { self.node_signer.sign_bolt12_invoice(invoice) } - + fn send_payment_for_verified_bolt12_invoice(&self, invoice: &Bolt12Invoice, payment_id: PaymentId) -> Result<(), Bolt12PaymentError> { self.send_payment_for_verified_bolt12_invoice(invoice, payment_id) } - + fn abandon_payment_with_reason(&self, payment_id: PaymentId, reason: PaymentFailureReason) { self.abandon_payment_with_reason(payment_id, reason); } - + // ----Temporary Functions---- // Set of functions temporarily moved to OffersMessageCommons for easier // transition of code from ChannelManager to OffersMessageFlow @@ -10002,11 +9990,11 @@ where fn get_pending_offers_messages(&self) -> MutexGuard<'_, Vec<(OffersMessage, MessageSendInstructions)>> { self.pending_offers_messages.lock().expect("Mutex is locked by other thread.") } - + fn enqueue_invoice_request(&self, invoice_request: InvoiceRequest, reply_paths: Vec) -> Result<(), Bolt12SemanticError> { self.enqueue_invoice_request(invoice_request, reply_paths) } - + #[cfg(async_payments)] fn initiate_async_payment( &self, invoice: &StaticInvoice, payment_id: PaymentId @@ -12070,263 +12058,6 @@ where } } -impl -OffersMessageHandler for ChannelManager -where - M::Target: chain::Watch<::EcdsaSigner>, - T::Target: BroadcasterInterface, - ES::Target: EntropySource, - NS::Target: NodeSigner, - SP::Target: SignerProvider, - F::Target: FeeEstimator, - R::Target: Router, - MR::Target: MessageRouter, - L::Target: Logger, -{ - fn handle_message( - &self, message: OffersMessage, context: Option, responder: Option, - ) -> Option<(OffersMessage, ResponseInstruction)> { - let secp_ctx = &self.secp_ctx; - let expanded_key = &self.inbound_payment_key; - - macro_rules! handle_pay_invoice_res { - ($res: expr, $invoice: expr, $logger: expr) => {{ - let error = match $res { - Err(Bolt12PaymentError::UnknownRequiredFeatures) => { - log_trace!( - $logger, "Invoice requires unknown features: {:?}", - $invoice.invoice_features() - ); - InvoiceError::from(Bolt12SemanticError::UnknownRequiredFeatures) - }, - Err(Bolt12PaymentError::SendingFailed(e)) => { - log_trace!($logger, "Failed paying invoice: {:?}", e); - InvoiceError::from_string(format!("{:?}", e)) - }, - #[cfg(async_payments)] - Err(Bolt12PaymentError::BlindedPathCreationFailed) => { - let err_msg = "Failed to create a blinded path back to ourselves"; - log_trace!($logger, "{}", err_msg); - InvoiceError::from_string(err_msg.to_string()) - }, - Err(Bolt12PaymentError::UnexpectedInvoice) - | Err(Bolt12PaymentError::DuplicateInvoice) - | Ok(()) => return None, - }; - - match responder { - Some(responder) => return Some((OffersMessage::InvoiceError(error), responder.respond())), - None => { - log_trace!($logger, "No reply path to send error: {:?}", error); - return None - }, - } - }} - } - - match message { - OffersMessage::InvoiceRequest(invoice_request) => { - let responder = match responder { - Some(responder) => responder, - None => return None, - }; - - let nonce = match context { - None if invoice_request.metadata().is_some() => None, - Some(OffersContext::InvoiceRequest { nonce }) => Some(nonce), - _ => return None, - }; - - let invoice_request = match nonce { - Some(nonce) => match invoice_request.verify_using_recipient_data( - nonce, expanded_key, secp_ctx, - ) { - Ok(invoice_request) => invoice_request, - Err(()) => return None, - }, - None => match invoice_request.verify_using_metadata(expanded_key, secp_ctx) { - Ok(invoice_request) => invoice_request, - Err(()) => return None, - }, - }; - - let amount_msats = match InvoiceBuilder::::amount_msats( - &invoice_request.inner - ) { - Ok(amount_msats) => amount_msats, - Err(error) => return Some((OffersMessage::InvoiceError(error.into()), responder.respond())), - }; - - let relative_expiry = DEFAULT_RELATIVE_EXPIRY.as_secs() as u32; - let (payment_hash, payment_secret) = match self.create_inbound_payment( - Some(amount_msats), relative_expiry, None - ) { - Ok((payment_hash, payment_secret)) => (payment_hash, payment_secret), - Err(()) => { - let error = Bolt12SemanticError::InvalidAmount; - return Some((OffersMessage::InvoiceError(error.into()), responder.respond())); - }, - }; - - let payment_context = PaymentContext::Bolt12Offer(Bolt12OfferContext { - offer_id: invoice_request.offer_id, - invoice_request: invoice_request.fields(), - }); - let payment_paths = match self.create_blinded_payment_paths( - Some(amount_msats), payment_secret, payment_context, relative_expiry - ) { - Ok(payment_paths) => payment_paths, - Err(()) => { - let error = Bolt12SemanticError::MissingPaths; - return Some((OffersMessage::InvoiceError(error.into()), responder.respond())); - }, - }; - - #[cfg(not(feature = "std"))] - let created_at = Duration::from_secs( - self.highest_seen_timestamp.load(Ordering::Acquire) as u64 - ); - - let response = if invoice_request.keys.is_some() { - #[cfg(feature = "std")] - let builder = invoice_request.respond_using_derived_keys( - payment_paths, payment_hash - ); - #[cfg(not(feature = "std"))] - let builder = invoice_request.respond_using_derived_keys_no_std( - payment_paths, payment_hash, created_at - ); - builder - .map(InvoiceBuilder::::from) - .and_then(|builder| builder.allow_mpp().build_and_sign(secp_ctx)) - .map_err(InvoiceError::from) - } else { - #[cfg(feature = "std")] - let builder = invoice_request.respond_with(payment_paths, payment_hash); - #[cfg(not(feature = "std"))] - let builder = invoice_request.respond_with_no_std( - payment_paths, payment_hash, created_at - ); - builder - .map(InvoiceBuilder::::from) - .and_then(|builder| builder.allow_mpp().build()) - .map_err(InvoiceError::from) - .and_then(|invoice| { - #[cfg(c_bindings)] - let mut invoice = invoice; - invoice - .sign(|invoice: &UnsignedBolt12Invoice| - self.node_signer.sign_bolt12_invoice(invoice) - ) - .map_err(InvoiceError::from) - }) - }; - - match response { - Ok(invoice) => { - let nonce = Nonce::from_entropy_source(&*self.entropy_source); - let hmac = payment_hash.hmac_for_offer_payment(nonce, expanded_key); - let context = MessageContext::Offers(OffersContext::InboundPayment { payment_hash, nonce, hmac }); - Some((OffersMessage::Invoice(invoice), responder.respond_with_reply_path(context))) - }, - Err(error) => Some((OffersMessage::InvoiceError(error.into()), responder.respond())), - } - }, - OffersMessage::Invoice(invoice) => { - let payment_id = match self.verify_bolt12_invoice(&invoice, context.as_ref()) { - Ok(payment_id) => payment_id, - Err(()) => return None, - }; - - let logger = WithContext::from( - &self.logger, None, None, Some(invoice.payment_hash()), - ); - - let res = self.send_payment_for_verified_bolt12_invoice(&invoice, payment_id); - handle_pay_invoice_res!(res, invoice, logger); - }, - #[cfg(async_payments)] - OffersMessage::StaticInvoice(invoice) => { - let payment_id = match context { - Some(OffersContext::OutboundPayment { payment_id, nonce, hmac: Some(hmac) }) => { - if payment_id.verify_for_offer_payment(hmac, nonce, expanded_key).is_err() { - return None - } - payment_id - }, - _ => return None - }; - let res = self.initiate_async_payment(&invoice, payment_id); - handle_pay_invoice_res!(res, invoice, self.logger); - }, - OffersMessage::InvoiceError(invoice_error) => { - let payment_hash = match context { - Some(OffersContext::InboundPayment { payment_hash, nonce, hmac }) => { - match payment_hash.verify_for_offer_payment(hmac, nonce, expanded_key) { - Ok(_) => Some(payment_hash), - Err(_) => None, - } - }, - _ => None, - }; - - let logger = WithContext::from(&self.logger, None, None, payment_hash); - log_trace!(logger, "Received invoice_error: {}", invoice_error); - - match context { - Some(OffersContext::OutboundPayment { payment_id, nonce, hmac: Some(hmac) }) => { - if let Ok(()) = payment_id.verify_for_offer_payment(hmac, nonce, expanded_key) { - self.abandon_payment_with_reason( - payment_id, PaymentFailureReason::InvoiceRequestRejected, - ); - } - }, - _ => {}, - } - - None - }, - } - } - - fn message_received(&self) { - for (payment_id, retryable_invoice_request) in self - .pending_outbound_payments - .release_invoice_requests_awaiting_invoice() - { - let RetryableInvoiceRequest { invoice_request, nonce, .. } = retryable_invoice_request; - let hmac = payment_id.hmac_for_offer_payment(nonce, &self.inbound_payment_key); - let context = MessageContext::Offers(OffersContext::OutboundPayment { - payment_id, - nonce, - hmac: Some(hmac) - }); - match self.create_blinded_paths(context) { - Ok(reply_paths) => match self.enqueue_invoice_request(invoice_request, reply_paths) { - Ok(_) => {} - Err(_) => { - log_warn!(self.logger, - "Retry failed for an invoice request with payment_id: {}", - payment_id - ); - } - }, - Err(_) => { - log_warn!(self.logger, - "Retry failed for an invoice request with payment_id: {}. \ - Reason: router could not find a blinded path to include as the reply path", - payment_id - ); - } - } - } - } - - fn release_pending_messages(&self) -> Vec<(OffersMessage, MessageSendInstructions)> { - core::mem::take(&mut self.pending_offers_messages.lock().unwrap()) - } -} - impl AsyncPaymentsMessageHandler for ChannelManager where diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index e9afb7404f5..652ff4233c9 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -426,7 +426,7 @@ type TestOnionMessenger<'chan_man, 'node_cfg, 'chan_mon_cfg> = OnionMessenger< &'chan_mon_cfg test_utils::TestLogger, &'chan_man TestChannelManager<'node_cfg, 'chan_mon_cfg>, &'node_cfg test_utils::TestMessageRouter<'chan_mon_cfg>, - &'chan_man TestChannelManager<'node_cfg, 'chan_mon_cfg>, + Arc>, &'chan_man TestChannelManager<'node_cfg, 'chan_mon_cfg>, IgnoringMessageHandler, IgnoringMessageHandler, @@ -439,7 +439,7 @@ type TestOnionMessenger<'chan_man, 'node_cfg, 'chan_mon_cfg> = OnionMessenger< &'chan_mon_cfg test_utils::TestLogger, &'chan_man TestChannelManager<'node_cfg, 'chan_mon_cfg>, &'node_cfg test_utils::TestMessageRouter<'chan_mon_cfg>, - &'chan_man TestChannelManager<'node_cfg, 'chan_mon_cfg>, + Arc>, &'chan_man TestChannelManager<'node_cfg, 'chan_mon_cfg>, &'chan_man TestChannelManager<'node_cfg, 'chan_mon_cfg>, IgnoringMessageHandler, @@ -466,6 +466,7 @@ pub struct Node<'chan_man, 'node_cfg: 'chan_man, 'chan_mon_cfg: 'node_cfg> { pub keys_manager: &'chan_mon_cfg test_utils::TestKeysInterface, pub node: &'chan_man TestChannelManager<'node_cfg, 'chan_mon_cfg>, pub onion_messenger: TestOnionMessenger<'chan_man, 'node_cfg, 'chan_mon_cfg>, + pub offers_handler: Arc>, pub network_graph: &'node_cfg NetworkGraph<&'chan_mon_cfg test_utils::TestLogger>, pub gossip_sync: P2PGossipSync<&'node_cfg NetworkGraph<&'chan_mon_cfg test_utils::TestLogger>, &'chan_mon_cfg test_utils::TestChainSource, &'chan_mon_cfg test_utils::TestLogger>, pub node_seed: [u8; 32], @@ -1184,8 +1185,14 @@ macro_rules! reload_node { $node.chain_monitor = &$new_chain_monitor; $new_channelmanager = _reload_node(&$node, $new_config, &chanman_encoded, $monitors_encoded); + + let offers_handler = $crate::sync::Arc::new($crate::offers::flow::OffersMessageFlow::new( + $new_channelmanager.inbound_payment_key, $new_channelmanager.get_our_node_id(), $node.keys_manager, &$new_channelmanager, $node.message_router, $node.logger + )); + $node.node = &$new_channelmanager; - $node.onion_messenger.set_offers_handler(&$new_channelmanager); + $node.offers_handler = offers_handler.clone(); + $node.onion_messenger.set_offers_handler(offers_handler); }; ($node: expr, $chanman_encoded: expr, $monitors_encoded: expr, $persister: ident, $new_chain_monitor: ident, $new_channelmanager: ident) => { reload_node!($node, $crate::util::config::UserConfig::default(), $chanman_encoded, $monitors_encoded, $persister, $new_chain_monitor, $new_channelmanager); @@ -3359,16 +3366,19 @@ pub fn create_network<'a, 'b: 'a, 'c: 'b>(node_count: usize, cfgs: &'b Vec(node_count: usize, cfgs: &'b Vec Duration; + #[cfg(not(feature = "std"))] + /// Get the approximate current time using the highest seen timestamp + fn get_highest_seen_timestamp(&self) -> Duration; + /// Get the vector of peers that can be used for a blinded path fn get_peer_for_blinded_path(&self) -> Vec; @@ -316,3 +325,300 @@ where self.our_network_pubkey } } + +impl OffersMessageFlow +where + ES::Target: EntropySource, + OMC::Target: OffersMessageCommons, + MR::Target: MessageRouter, + L::Target: Logger, +{ + fn verify_bolt12_invoice( + &self, invoice: &Bolt12Invoice, context: Option<&OffersContext>, + ) -> Result { + let secp_ctx = &self.secp_ctx; + let expanded_key = &self.inbound_payment_key; + + match context { + None if invoice.is_for_refund_without_paths() => { + invoice.verify_using_metadata(expanded_key, secp_ctx) + }, + Some(&OffersContext::OutboundPayment { payment_id, nonce, .. }) => { + invoice.verify_using_payer_data(payment_id, nonce, expanded_key, secp_ctx) + }, + _ => Err(()), + } + } + + /// Creates a collection of blinded paths by delegating to + /// [`MessageRouter::create_blinded_paths`]. + /// + /// Errors if the `MessageRouter` errors. + /// + /// [`MessageRouter::create_blinded_paths`]: crate::onion_message::messenger::MessageRouter::create_blinded_paths + pub fn create_blinded_paths( + &self, context: MessageContext, + ) -> Result, ()> { + let recipient = self.get_our_node_id(); + let secp_ctx = &self.secp_ctx; + + let peers = self.commons.get_peer_for_blinded_path() + .into_iter() + .map(|node| node.node_id) + .collect(); + + self.message_router + .create_blinded_paths(recipient, context, peers, secp_ctx) + .and_then(|paths| (!paths.is_empty()).then(|| paths).ok_or(())) + } +} + +impl OffersMessageHandler for OffersMessageFlow +where + ES::Target: EntropySource, + OMC::Target: OffersMessageCommons, + M::Target: MessageRouter, + L::Target: Logger, +{ + fn handle_message( + &self, message: OffersMessage, context: Option, responder: Option, + ) -> Option<(OffersMessage, ResponseInstruction)> { + let secp_ctx = &self.secp_ctx; + let expanded_key = &self.inbound_payment_key; + + macro_rules! handle_pay_invoice_res { + ($res: expr, $invoice: expr, $logger: expr) => {{ + let error = match $res { + Err(Bolt12PaymentError::UnknownRequiredFeatures) => { + log_trace!( + $logger, "Invoice requires unknown features: {:?}", + $invoice.invoice_features() + ); + InvoiceError::from(Bolt12SemanticError::UnknownRequiredFeatures) + }, + Err(Bolt12PaymentError::SendingFailed(e)) => { + log_trace!($logger, "Failed paying invoice: {:?}", e); + InvoiceError::from_string(format!("{:?}", e)) + }, + #[cfg(async_payments)] + Err(Bolt12PaymentError::BlindedPathCreationFailed) => { + let err_msg = "Failed to create a blinded path back to ourselves"; + log_trace!($logger, "{}", err_msg); + InvoiceError::from_string(err_msg.to_string()) + }, + Err(Bolt12PaymentError::UnexpectedInvoice) + | Err(Bolt12PaymentError::DuplicateInvoice) + | Ok(()) => return None, + }; + + match responder { + Some(responder) => return Some((OffersMessage::InvoiceError(error), responder.respond())), + None => { + log_trace!($logger, "No reply path to send error: {:?}", error); + return None + }, + } + }} + } + + match message { + OffersMessage::InvoiceRequest(invoice_request) => { + let responder = match responder { + Some(responder) => responder, + None => return None, + }; + + let nonce = match context { + None if invoice_request.metadata().is_some() => None, + Some(OffersContext::InvoiceRequest { nonce }) => Some(nonce), + _ => return None, + }; + + let invoice_request = match nonce { + Some(nonce) => match invoice_request.verify_using_recipient_data( + nonce, expanded_key, secp_ctx, + ) { + Ok(invoice_request) => invoice_request, + Err(()) => return None, + }, + None => match invoice_request.verify_using_metadata(expanded_key, secp_ctx) { + Ok(invoice_request) => invoice_request, + Err(()) => return None, + }, + }; + + let amount_msats = match InvoiceBuilder::::amount_msats( + &invoice_request.inner + ) { + Ok(amount_msats) => amount_msats, + Err(error) => return Some((OffersMessage::InvoiceError(error.into()), responder.respond())), + }; + + let relative_expiry = DEFAULT_RELATIVE_EXPIRY.as_secs() as u32; + let (payment_hash, payment_secret) = match self.commons.create_inbound_payment( + Some(amount_msats), relative_expiry, None + ) { + Ok((payment_hash, payment_secret)) => (payment_hash, payment_secret), + Err(()) => { + let error = Bolt12SemanticError::InvalidAmount; + return Some((OffersMessage::InvoiceError(error.into()), responder.respond())); + }, + }; + + let payment_context = PaymentContext::Bolt12Offer(Bolt12OfferContext { + offer_id: invoice_request.offer_id, + invoice_request: invoice_request.fields(), + }); + let payment_paths = match self.commons.create_blinded_payment_paths( + Some(amount_msats), payment_secret, payment_context, relative_expiry + ) { + Ok(payment_paths) => payment_paths, + Err(()) => { + let error = Bolt12SemanticError::MissingPaths; + return Some((OffersMessage::InvoiceError(error.into()), responder.respond())); + }, + }; + + #[cfg(not(feature = "std"))] + let created_at = self.commons.get_highest_seen_timestamp(); + + let response = if invoice_request.keys.is_some() { + #[cfg(feature = "std")] + let builder = invoice_request.respond_using_derived_keys( + payment_paths, payment_hash + ); + #[cfg(not(feature = "std"))] + let builder = invoice_request.respond_using_derived_keys_no_std( + payment_paths, payment_hash, created_at + ); + builder + .map(InvoiceBuilder::::from) + .and_then(|builder| builder.allow_mpp().build_and_sign(secp_ctx)) + .map_err(InvoiceError::from) + } else { + #[cfg(feature = "std")] + let builder = invoice_request.respond_with(payment_paths, payment_hash); + #[cfg(not(feature = "std"))] + let builder = invoice_request.respond_with_no_std( + payment_paths, payment_hash, created_at + ); + builder + .map(InvoiceBuilder::::from) + .and_then(|builder| builder.allow_mpp().build()) + .map_err(InvoiceError::from) + .and_then(|invoice| { + #[cfg(c_bindings)] + let mut invoice = invoice; + invoice + .sign(|invoice: &UnsignedBolt12Invoice| + self.commons.sign_bolt12_invoice(invoice) + ) + .map_err(InvoiceError::from) + }) + }; + + match response { + Ok(invoice) => { + let nonce = Nonce::from_entropy_source(&*self.entropy_source); + let hmac = payment_hash.hmac_for_offer_payment(nonce, expanded_key); + let context = MessageContext::Offers(OffersContext::InboundPayment { payment_hash, nonce, hmac }); + Some((OffersMessage::Invoice(invoice), responder.respond_with_reply_path(context))) + }, + Err(error) => Some((OffersMessage::InvoiceError(error.into()), responder.respond())), + } + }, + OffersMessage::Invoice(invoice) => { + let payment_id = match self.verify_bolt12_invoice(&invoice, context.as_ref()) { + Ok(payment_id) => payment_id, + Err(()) => return None, + }; + + let logger = WithContext::from( + &self.logger, None, None, Some(invoice.payment_hash()), + ); + + let res = self.commons.send_payment_for_verified_bolt12_invoice(&invoice, payment_id); + handle_pay_invoice_res!(res, invoice, logger); + }, + #[cfg(async_payments)] + OffersMessage::StaticInvoice(invoice) => { + let payment_id = match context { + Some(OffersContext::OutboundPayment { payment_id, nonce, hmac: Some(hmac) }) => { + if payment_id.verify_for_offer_payment(hmac, nonce, expanded_key).is_err() { + return None + } + payment_id + }, + _ => return None + }; + let res = self.commons.initiate_async_payment(&invoice, payment_id); + handle_pay_invoice_res!(res, invoice, self.logger); + }, + OffersMessage::InvoiceError(invoice_error) => { + let payment_hash = match context { + Some(OffersContext::InboundPayment { payment_hash, nonce, hmac }) => { + match payment_hash.verify_for_offer_payment(hmac, nonce, expanded_key) { + Ok(_) => Some(payment_hash), + Err(_) => None, + } + }, + _ => None, + }; + + let logger = WithContext::from(&self.logger, None, None, payment_hash); + log_trace!(logger, "Received invoice_error: {}", invoice_error); + + match context { + Some(OffersContext::OutboundPayment { payment_id, nonce, hmac: Some(hmac) }) => { + if let Ok(()) = payment_id.verify_for_offer_payment(hmac, nonce, expanded_key) { + self.commons.abandon_payment_with_reason( + payment_id, PaymentFailureReason::InvoiceRequestRejected, + ); + } + }, + _ => {}, + } + + None + }, + } + } + + fn message_received(&self) { + for (payment_id, retryable_invoice_request) in self + .commons + .release_invoice_requests_awaiting_invoice() + { + let RetryableInvoiceRequest { invoice_request, nonce, .. } = retryable_invoice_request; + let hmac = payment_id.hmac_for_offer_payment(nonce, &self.inbound_payment_key); + let context = MessageContext::Offers(OffersContext::OutboundPayment { + payment_id, + nonce, + hmac: Some(hmac) + }); + match self.create_blinded_paths(context) { + Ok(reply_paths) => match self.commons.enqueue_invoice_request(invoice_request, reply_paths) { + Ok(_) => {} + Err(_) => { + log_warn!(self.logger, + "Retry failed for an invoice request with payment_id: {}", + payment_id + ); + } + }, + Err(_) => { + log_warn!(self.logger, + "Retry failed for an invoice request with payment_id: {}. \ + Reason: router could not find a blinded path to include as the reply path", + payment_id + ); + } + } + } + } + + fn release_pending_messages(&self) -> Vec<(OffersMessage, MessageSendInstructions)> { + core::mem::take(&mut self.commons.get_pending_offers_messages()) + } +} + diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index 1b3d07a63ad..b8b288898b5 100644 --- a/lightning/src/onion_message/messenger.rs +++ b/lightning/src/onion_message/messenger.rs @@ -54,6 +54,7 @@ use core::sync::atomic::{AtomicBool, Ordering}; use { crate::ln::channelmanager::{SimpleArcChannelManager, SimpleRefChannelManager}, crate::ln::peer_handler::IgnoringMessageHandler, + crate::offers::flow::SimpleArcOffersMessageFlow, crate::sign::KeysManager, crate::sync::Arc, }; @@ -2105,7 +2106,7 @@ pub type SimpleArcOnionMessenger = OnionMessenger< Arc, Arc>, Arc>>, Arc, Arc>>, - Arc>, + Arc>, Arc>, Arc>, IgnoringMessageHandler, @@ -2126,7 +2127,7 @@ pub type SimpleArcOnionMessenger = OnionMessenger< Arc, Arc>, Arc>>, Arc, Arc>>, - Arc>, + Arc>, Arc>, IgnoringMessageHandler, IgnoringMessageHandler, From b1b28ca3fa78630ff7fecebbc7dc7412002a2c34 Mon Sep 17 00:00:00 2001 From: shaavan Date: Thu, 13 Feb 2025 20:45:37 +0530 Subject: [PATCH 07/14] Move DNSResolverMessageHandler impl to OffersMessageFlow --- lightning-dns-resolver/src/lib.rs | 4 +- lightning/src/ln/channelmanager.rs | 193 +++++----------------- lightning/src/ln/functional_test_utils.rs | 4 +- lightning/src/ln/outbound_payment.rs | 4 +- lightning/src/offers/flow.rs | 191 ++++++++++++++++++++- lightning/src/onion_message/messenger.rs | 2 +- 6 files changed, 234 insertions(+), 164 deletions(-) diff --git a/lightning-dns-resolver/src/lib.rs b/lightning-dns-resolver/src/lib.rs index 8f855cb5fb7..35b59236ea8 100644 --- a/lightning-dns-resolver/src/lib.rs +++ b/lightning-dns-resolver/src/lib.rs @@ -393,7 +393,7 @@ mod test { // When we get the proof back, override its contents to an offer from nodes[1] let bs_offer = nodes[1].node.create_offer_builder(None).unwrap().build().unwrap(); nodes[0] - .node + .offers_handler .testing_dnssec_proof_offer_resolution_override .lock() .unwrap() @@ -404,7 +404,7 @@ mod test { let retry = Retry::Attempts(0); let amt = 42_000; nodes[0] - .node + .offers_handler .pay_for_offer_from_human_readable_name(name, amt, payment_id, retry, None, resolvers) .unwrap(); diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 80f7106c565..5de9c243c0f 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -93,9 +93,7 @@ use crate::util::errors::APIError; }; #[cfg(feature = "dnssec")] -use crate::blinded_path::message::DNSResolverContext; -#[cfg(feature = "dnssec")] -use crate::onion_message::dns_resolution::{DNSResolverMessage, DNSResolverMessageHandler, DNSSECQuery, DNSSECProof, OMNameResolver}; +use crate::onion_message::dns_resolution::OMNameResolver; #[cfg(not(c_bindings))] use { @@ -2670,16 +2668,6 @@ where #[cfg(feature = "dnssec")] hrn_resolver: OMNameResolver, - #[cfg(feature = "dnssec")] - pending_dns_onion_messages: Mutex>, - - #[cfg(feature = "_test_utils")] - /// In testing, it is useful be able to forge a name -> offer mapping so that we can pay an - /// offer generated in the test. - /// - /// This allows for doing so, validating proofs as normal, but, if they pass, replacing the - /// offer they resolve to to the given one. - pub testing_dnssec_proof_offer_resolution_override: Mutex>, #[cfg(test)] pub(super) entropy_source: ES, @@ -3604,11 +3592,6 @@ where #[cfg(feature = "dnssec")] hrn_resolver: OMNameResolver::new(current_timestamp, params.best_block.height), - #[cfg(feature = "dnssec")] - pending_dns_onion_messages: Mutex::new(Vec::new()), - - #[cfg(feature = "_test_utils")] - testing_dnssec_proof_offer_resolution_override: Mutex::new(new_hash_map()), } } @@ -9937,6 +9920,11 @@ where Duration::from_secs(self.highest_seen_timestamp.load(Ordering::Acquire) as u64) } + #[cfg(feature = "dnssec")] + fn get_hrn_resolver(&self) -> &OMNameResolver { + &self.hrn_resolver + } + fn get_peer_for_blinded_path(&self) -> Vec { self.per_peer_state.read().unwrap() .iter() @@ -9983,6 +9971,26 @@ where self.abandon_payment_with_reason(payment_id, reason); } + #[cfg(feature = "dnssec")] + fn add_new_awaiting_offer( + &self, payment_id: PaymentId, expiration: StaleExpiration, retry_strategy: Retry, + max_total_routing_fee_msat: Option, amount_msats: u64, + ) -> Result<(), ()> { + self.pending_outbound_payments.add_new_awaiting_offer(payment_id, expiration, retry_strategy, max_total_routing_fee_msat, amount_msats) + } + + #[cfg(feature = "dnssec")] + fn amt_msats_for_payment_awaiting_offer(&self, payment_id: PaymentId) -> Result { + self.pending_outbound_payments.amt_msats_for_payment_awaiting_offer(payment_id) + } + + #[cfg(feature = "dnssec")] + fn received_offer( + &self, payment_id: PaymentId, retryable_invoice_request: Option, + ) -> Result<(), ()> { + self.pending_outbound_payments.received_offer(payment_id, retryable_invoice_request) + } + // ----Temporary Functions---- // Set of functions temporarily moved to OffersMessageCommons for easier // transition of code from ChannelManager to OffersMessageFlow @@ -9995,6 +10003,14 @@ where self.enqueue_invoice_request(invoice_request, reply_paths) } + fn pay_for_offer_intern Result<(), Bolt12SemanticError>>( + &self, offer: &Offer, quantity: Option, amount_msats: Option, + payer_note: Option, payment_id: PaymentId, + human_readable_name: Option, create_pending_payment: CPP, + ) -> Result<(), Bolt12SemanticError> { + self.pay_for_offer_intern(offer, quantity, amount_msats, payer_note, payment_id, human_readable_name, create_pending_payment) + } + #[cfg(async_payments)] fn initiate_async_payment( &self, invoice: &StaticInvoice, payment_id: PaymentId @@ -10008,7 +10024,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(crate) const OFFERS_MESSAGE_REQUEST_LIMIT: usize = 10; impl ChannelManager where @@ -10229,7 +10245,8 @@ where let reply_paths = self.create_blinded_paths(context) .map_err(|_| Bolt12SemanticError::MissingPaths)?; - let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self); + // Temporarily removing this to find the best way to integrate the guard + // let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self); create_pending_payment(&invoice_request, nonce)?; @@ -10372,73 +10389,6 @@ where } } - /// 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. - /// - /// If the wallet supports paying on-chain schemes, you should instead use - /// [`OMNameResolver::resolve_name`] and [`OMNameResolver::handle_dnssec_proof_for_uri`] (by - /// implementing [`DNSResolverMessageHandler`]) directly to look up a URI and then delegate to - /// your normal URI handling. - /// - /// If `max_total_routing_fee_msat` is not specified, the default from - /// [`RouteParameters::from_payment_params_and_value`] is applied. - /// - /// # Payment - /// - /// The provided `payment_id` is used to ensure that only one invoice is paid for the request - /// when received. See [Avoiding Duplicate Payments] for other requirements once the payment has - /// been sent. - /// - /// To revoke the request, use [`ChannelManager::abandon_payment`] prior to receiving the - /// invoice. If abandoned, or an invoice isn't received in a reasonable amount of time, the - /// payment will fail with an [`Event::InvoiceRequestFailed`]. - /// - /// # Privacy - /// - /// For payer privacy, uses a derived payer id and uses [`MessageRouter::create_blinded_paths`] - /// to construct a [`BlindedPath`] for the reply path. For further privacy implications, see the - /// docs of the parameterized [`Router`], which implements [`MessageRouter`]. - /// - /// # Limitations - /// - /// Requires a direct connection to the given [`Destination`] as well as an introduction node in - /// [`Offer::paths`] or to [`Offer::signing_pubkey`], if empty. A similar restriction applies to - /// the responding [`Bolt12Invoice::payment_paths`]. - /// - /// # Errors - /// - /// Errors if: - /// - a duplicate `payment_id` is provided given the caveats in the aforementioned link, - /// - /// [`Bolt12Invoice::payment_paths`]: crate::offers::invoice::Bolt12Invoice::payment_paths - /// [Avoiding Duplicate Payments]: #avoiding-duplicate-payments - #[cfg(feature = "dnssec")] - pub fn pay_for_offer_from_human_readable_name( - &self, name: HumanReadableName, amount_msats: u64, payment_id: PaymentId, - retry_strategy: Retry, max_total_routing_fee_msat: Option, - dns_resolvers: Vec, - ) -> Result<(), ()> { - let (onion_message, context) = - self.hrn_resolver.resolve_name(payment_id, name, &*self.entropy_source)?; - let reply_paths = self.create_blinded_paths(MessageContext::DNSResolver(context))?; - let expiration = StaleExpiration::TimerTicks(1); - self.pending_outbound_payments.add_new_awaiting_offer(payment_id, expiration, retry_strategy, max_total_routing_fee_msat, amount_msats)?; - let message_params = dns_resolvers - .iter() - .flat_map(|destination| reply_paths.iter().map(move |path| (path, destination))) - .take(OFFERS_MESSAGE_REQUEST_LIMIT); - for (reply_path, destination) in message_params { - self.pending_dns_onion_messages.lock().unwrap().push(( - DNSResolverMessage::DNSSECQuery(onion_message.clone()), - MessageSendInstructions::WithSpecifiedReplyPath { - destination: destination.clone(), - reply_path: reply_path.clone(), - }, - )); - } - Ok(()) - } - /// Gets a payment secret and payment hash for use in an invoice given to a third party wishing /// to pay us. /// @@ -12113,70 +12063,6 @@ where } } -#[cfg(feature = "dnssec")] -impl -DNSResolverMessageHandler for ChannelManager -where - M::Target: chain::Watch<::EcdsaSigner>, - T::Target: BroadcasterInterface, - ES::Target: EntropySource, - NS::Target: NodeSigner, - SP::Target: SignerProvider, - F::Target: FeeEstimator, - R::Target: Router, - MR::Target: MessageRouter, - L::Target: Logger, -{ - fn handle_dnssec_query( - &self, _message: DNSSECQuery, _responder: Option, - ) -> Option<(DNSResolverMessage, ResponseInstruction)> { - None - } - - fn handle_dnssec_proof(&self, message: DNSSECProof, context: DNSResolverContext) { - let offer_opt = self.hrn_resolver.handle_dnssec_proof_for_offer(message, context); - #[cfg_attr(not(feature = "_test_utils"), allow(unused_mut))] - if let Some((completed_requests, mut offer)) = offer_opt { - for (name, payment_id) in completed_requests { - #[cfg(feature = "_test_utils")] - if let Some(replacement_offer) = self.testing_dnssec_proof_offer_resolution_override.lock().unwrap().remove(&name) { - // If we have multiple pending requests we may end up over-using the override - // offer, but tests can deal with that. - offer = replacement_offer; - } - if let Ok(amt_msats) = self.pending_outbound_payments.amt_msats_for_payment_awaiting_offer(payment_id) { - let offer_pay_res = - self.pay_for_offer_intern(&offer, None, Some(amt_msats), None, payment_id, Some(name), - |invoice_request, nonce| { - let retryable_invoice_request = RetryableInvoiceRequest { - invoice_request: invoice_request.clone(), - nonce, - needs_retry: true, - }; - self.pending_outbound_payments - .received_offer(payment_id, Some(retryable_invoice_request)) - .map_err(|_| Bolt12SemanticError::DuplicatePaymentId) - }); - if offer_pay_res.is_err() { - // The offer we tried to pay is the canonical current offer for the name we - // wanted to pay. If we can't pay it, there's no way to recover so fail the - // payment. - // Note that the PaymentFailureReason should be ignored for an - // AwaitingInvoice payment. - self.pending_outbound_payments.abandon_payment( - payment_id, PaymentFailureReason::RouteNotFound, &self.pending_events, - ); - } - } - } - } - } - - fn release_pending_messages(&self) -> Vec<(DNSResolverMessage, MessageSendInstructions)> { - core::mem::take(&mut self.pending_dns_onion_messages.lock().unwrap()) - } -} - impl NodeIdLookUp for ChannelManager where @@ -14146,11 +14032,6 @@ where #[cfg(feature = "dnssec")] hrn_resolver: OMNameResolver::new(highest_seen_timestamp, best_block_height), - #[cfg(feature = "dnssec")] - pending_dns_onion_messages: Mutex::new(Vec::new()), - - #[cfg(feature = "_test_utils")] - testing_dnssec_proof_offer_resolution_override: Mutex::new(new_hash_map()), }; let mut processed_claims: HashSet> = new_hash_set(); diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index 652ff4233c9..d0f45c49f1c 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -441,7 +441,7 @@ type TestOnionMessenger<'chan_man, 'node_cfg, 'chan_mon_cfg> = OnionMessenger< &'node_cfg test_utils::TestMessageRouter<'chan_mon_cfg>, Arc>, &'chan_man TestChannelManager<'node_cfg, 'chan_mon_cfg>, - &'chan_man TestChannelManager<'node_cfg, 'chan_mon_cfg>, + Arc>, IgnoringMessageHandler, >; @@ -3372,7 +3372,7 @@ pub fn create_network<'a, 'b: 'a, 'c: 'b>(node_count: usize, cfgs: &'b Vec Duration; + #[cfg(feature = "dnssec")] + /// Get hrn resolver + fn get_hrn_resolver(&self) -> &OMNameResolver; + /// Get the vector of peers that can be used for a blinded path fn get_peer_for_blinded_path(&self) -> Vec; @@ -134,6 +149,23 @@ pub trait OffersMessageCommons { /// Abandon Payment with Reason fn abandon_payment_with_reason(&self, payment_id: PaymentId, reason: PaymentFailureReason); + #[cfg(feature = "dnssec")] + /// Add new awaiting offer + fn add_new_awaiting_offer( + &self, payment_id: PaymentId, expiration: StaleExpiration, retry_strategy: Retry, + max_total_routing_fee_msat: Option, amount_msats: u64, + ) -> Result<(), ()>; + + #[cfg(feature = "dnssec")] + /// Amount for payment awaiting offer + fn amt_msats_for_payment_awaiting_offer(&self, payment_id: PaymentId) -> Result; + + #[cfg(feature = "dnssec")] + /// Received Offer + fn received_offer( + &self, payment_id: PaymentId, retryable_invoice_request: Option, + ) -> Result<(), ()>; + // ----Temporary Functions---- // Set of functions temporarily moved to OffersMessageCommons for easier // transition of code from ChannelManager to OffersMessageFlow @@ -144,6 +176,13 @@ pub trait OffersMessageCommons { /// Enqueue invoice request fn enqueue_invoice_request(&self, invoice_request: InvoiceRequest, reply_paths: Vec) -> Result<(), Bolt12SemanticError>; + /// Internal pay for offer function + fn pay_for_offer_intern Result<(), Bolt12SemanticError>>( + &self, offer: &Offer, quantity: Option, amount_msats: Option, + payer_note: Option, payment_id: PaymentId, + human_readable_name: Option, create_pending_payment: CPP, + ) -> Result<(), Bolt12SemanticError>; + /// Initiate a new async payment #[cfg(async_payments)] fn initiate_async_payment( @@ -286,6 +325,17 @@ where message_router: MR, entropy_source: ES, + #[cfg(feature = "_test_utils")] + /// In testing, it is useful be able to forge a name -> offer mapping so that we can pay an + /// offer generated in the test. + /// + /// This allows for doing so, validating proofs as normal, but, if they pass, replacing the + /// offer they resolve to to the given one. + pub testing_dnssec_proof_offer_resolution_override: Mutex>, + + #[cfg(feature = "dnssec")] + pending_dns_onion_messages: Mutex>, + /// The Logger for use in the OffersMessageFlow and which may be used to log /// information during deserialization. pub logger: L, @@ -317,6 +367,11 @@ where entropy_source, logger, + + #[cfg(feature = "_test_utils")] + testing_dnssec_proof_offer_resolution_override: Mutex::new(new_hash_map()), + #[cfg(feature = "dnssec")] + pending_dns_onion_messages: Mutex::new(Vec::new()), } } @@ -373,6 +428,81 @@ where } } +impl OffersMessageFlow +where + ES::Target: EntropySource, + OMC::Target: OffersMessageCommons, + MR::Target: MessageRouter, + L::Target: Logger, +{ + /// 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. + /// + /// If the wallet supports paying on-chain schemes, you should instead use + /// [`OMNameResolver::resolve_name`] and [`OMNameResolver::handle_dnssec_proof_for_uri`] (by + /// implementing [`DNSResolverMessageHandler`]) directly to look up a URI and then delegate to + /// your normal URI handling. + /// + /// If `max_total_routing_fee_msat` is not specified, the default from + /// [`RouteParameters::from_payment_params_and_value`] is applied. + /// + /// # Payment + /// + /// The provided `payment_id` is used to ensure that only one invoice is paid for the request + /// when received. See [Avoiding Duplicate Payments] for other requirements once the payment has + /// been sent. + /// + /// To revoke the request, use [`ChannelManager::abandon_payment`] prior to receiving the + /// invoice. If abandoned, or an invoice isn't received in a reasonable amount of time, the + /// payment will fail with an [`Event::InvoiceRequestFailed`]. + /// + /// # Privacy + /// + /// For payer privacy, uses a derived payer id and uses [`MessageRouter::create_blinded_paths`] + /// to construct a [`BlindedPath`] for the reply path. For further privacy implications, see the + /// docs of the parameterized [`Router`], which implements [`MessageRouter`]. + /// + /// # Limitations + /// + /// Requires a direct connection to the given [`Destination`] as well as an introduction node in + /// [`Offer::paths`] or to [`Offer::signing_pubkey`], if empty. A similar restriction applies to + /// the responding [`Bolt12Invoice::payment_paths`]. + /// + /// # Errors + /// + /// Errors if: + /// - a duplicate `payment_id` is provided given the caveats in the aforementioned link, + /// + /// [`Bolt12Invoice::payment_paths`]: crate::offers::invoice::Bolt12Invoice::payment_paths + /// [Avoiding Duplicate Payments]: #avoiding-duplicate-payments + #[cfg(feature = "dnssec")] + pub fn pay_for_offer_from_human_readable_name( + &self, name: HumanReadableName, amount_msats: u64, payment_id: PaymentId, + retry_strategy: Retry, max_total_routing_fee_msat: Option, + dns_resolvers: Vec, + ) -> Result<(), ()> { + let (onion_message, context) = + self.commons.get_hrn_resolver().resolve_name(payment_id, name, &*self.entropy_source)?; + let reply_paths = self.create_blinded_paths(MessageContext::DNSResolver(context))?; + let expiration = StaleExpiration::TimerTicks(1); + self.commons.add_new_awaiting_offer(payment_id, expiration, retry_strategy, max_total_routing_fee_msat, amount_msats)?; + let message_params = dns_resolvers + .iter() + .flat_map(|destination| reply_paths.iter().map(move |path| (path, destination))) + .take(OFFERS_MESSAGE_REQUEST_LIMIT); + for (reply_path, destination) in message_params { + self.pending_dns_onion_messages.lock().unwrap().push(( + DNSResolverMessage::DNSSECQuery(onion_message.clone()), + MessageSendInstructions::WithSpecifiedReplyPath { + destination: destination.clone(), + reply_path: reply_path.clone(), + }, + )); + } + Ok(()) + } +} + impl OffersMessageHandler for OffersMessageFlow where ES::Target: EntropySource, @@ -622,3 +752,62 @@ where } } +#[cfg(feature = "dnssec")] +impl DNSResolverMessageHandler + for OffersMessageFlow +where + ES::Target: EntropySource, + OMC::Target: OffersMessageCommons, + MR::Target: MessageRouter, + L::Target: Logger, +{ + fn handle_dnssec_query( + &self, _message: DNSSECQuery, _responder: Option, + ) -> Option<(DNSResolverMessage, ResponseInstruction)> { + None + } + + fn handle_dnssec_proof(&self, message: DNSSECProof, context: DNSResolverContext) { + let offer_opt = self.commons.get_hrn_resolver().handle_dnssec_proof_for_offer(message, context); + #[cfg_attr(not(feature = "_test_utils"), allow(unused_mut))] + if let Some((completed_requests, mut offer)) = offer_opt { + for (name, payment_id) in completed_requests { + #[cfg(feature = "_test_utils")] + if let Some(replacement_offer) = self.testing_dnssec_proof_offer_resolution_override.lock().unwrap().remove(&name) { + // If we have multiple pending requests we may end up over-using the override + // offer, but tests can deal with that. + offer = replacement_offer; + } + if let Ok(amt_msats) = self.commons.amt_msats_for_payment_awaiting_offer(payment_id) { + let offer_pay_res = + self.commons.pay_for_offer_intern(&offer, None, Some(amt_msats), None, payment_id, Some(name), + |invoice_request, nonce| { + let retryable_invoice_request = RetryableInvoiceRequest { + invoice_request: invoice_request.clone(), + nonce, + needs_retry: true, + }; + self.commons + .received_offer(payment_id, Some(retryable_invoice_request)) + .map_err(|_| Bolt12SemanticError::DuplicatePaymentId) + }); + if offer_pay_res.is_err() { + // The offer we tried to pay is the canonical current offer for the name we + // wanted to pay. If we can't pay it, there's no way to recover so fail the + // payment. + // Note that the PaymentFailureReason should be ignored for an + // AwaitingInvoice payment. + self.commons.abandon_payment_with_reason( + payment_id, PaymentFailureReason::RouteNotFound + ); + } + } + } + } + } + + fn release_pending_messages(&self) -> Vec<(DNSResolverMessage, MessageSendInstructions)> { + core::mem::take(&mut self.pending_dns_onion_messages.lock().unwrap()) + } +} + diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index b8b288898b5..216a84b190a 100644 --- a/lightning/src/onion_message/messenger.rs +++ b/lightning/src/onion_message/messenger.rs @@ -2108,7 +2108,7 @@ pub type SimpleArcOnionMessenger = OnionMessenger< Arc>>, Arc, Arc>>, Arc>, Arc>, - Arc>, + Arc>, IgnoringMessageHandler, >; From 314c513c124f799dae4986193a717f356a2df684 Mon Sep 17 00:00:00 2001 From: shaavan Date: Thu, 6 Feb 2025 21:15:08 +0530 Subject: [PATCH 08/14] Refactor initiate_async_payment to improve separation of concerns 1. Previously, `initiate_async_payment` handled both the outbound payment state update (marking a static invoice as received) and the message queuing for async payments. This tightly coupled two distinct responsibilities within `ChannelManager`. 2. This refactor **separates concerns** by moving message queuing into a separate function, allowing `initiate_async_payment` to focus solely on updating `pending_outbound_payments`. This paves the way for moving message queuing to `flow.rs` in upcoming commits, ensuring `MessageRouter` logic remains independent of `ChannelManager`. 3. **This introduces a behavior change:** Since `pending_outbound_payments` is now handled separately, we drop PersistenceNotifierGuard before queuing messages. As a result, `NotifyOption` is no longer called for the message queuing process. 4. **Why is this safe?** Because `pending_async_payments_messages` is **not under `total_consistency_lock`**, meaning it does not require persistence notifications. This aligns with the lock order tree and maintains correct locking discipline. By making this change, we improve modularity, enforce a cleaner separation between `ChannelManager` and `MessageRouter`, and remove unnecessary persistence dependencies. --- lightning/src/ln/channelmanager.rs | 72 +++++++++++++++++------------- 1 file changed, 40 insertions(+), 32 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 5de9c243c0f..67ade6e3247 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -4755,7 +4755,7 @@ where } #[cfg(async_payments)] - fn initiate_async_payment( + fn handle_static_invoice_received( &self, invoice: &StaticInvoice, payment_id: PaymentId ) -> Result<(), Bolt12PaymentError> { let mut res = Ok(()); @@ -4777,43 +4777,51 @@ where return NotifyOption::DoPersist } }; - - let nonce = Nonce::from_entropy_source(&*self.entropy_source); - let hmac = payment_id.hmac_for_async_payment(nonce, &self.inbound_payment_key); - let reply_paths = match self.create_blinded_paths( - MessageContext::AsyncPayments( - AsyncPaymentsContext::OutboundPayment { payment_id, nonce, hmac } - ) - ) { - Ok(paths) => paths, - Err(()) => { - self.abandon_payment_with_reason(payment_id, PaymentFailureReason::BlindedPathCreationFailed); - res = Err(Bolt12PaymentError::BlindedPathCreationFailed); - return NotifyOption::DoPersist - } - }; - - let mut pending_async_payments_messages = self.pending_async_payments_messages.lock().unwrap(); - const HTLC_AVAILABLE_LIMIT: usize = 10; - reply_paths - .iter() - .flat_map(|reply_path| invoice.message_paths().iter().map(move |invoice_path| (invoice_path, reply_path))) - .take(HTLC_AVAILABLE_LIMIT) - .for_each(|(invoice_path, reply_path)| { - let instructions = MessageSendInstructions::WithSpecifiedReplyPath { - destination: Destination::BlindedPath(invoice_path.clone()), - reply_path: reply_path.clone(), - }; - let message = AsyncPaymentsMessage::HeldHtlcAvailable(HeldHtlcAvailable {}); - pending_async_payments_messages.push((message, instructions)); - }); - NotifyOption::DoPersist }); res } + #[cfg(async_payments)] + fn initiate_async_payment( + &self, invoice: &StaticInvoice, payment_id: PaymentId + ) -> Result<(), Bolt12PaymentError> { + + self.handle_static_invoice_received(invoice, payment_id)?; + + let nonce = Nonce::from_entropy_source(&*self.entropy_source); + let hmac = payment_id.hmac_for_async_payment(nonce, &self.inbound_payment_key); + let reply_paths = match self.create_blinded_paths( + MessageContext::AsyncPayments( + AsyncPaymentsContext::OutboundPayment { payment_id, nonce, hmac } + ) + ) { + Ok(paths) => paths, + Err(()) => { + self.abandon_payment_with_reason(payment_id, PaymentFailureReason::BlindedPathCreationFailed); + return Err(Bolt12PaymentError::BlindedPathCreationFailed); + } + }; + + let mut pending_async_payments_messages = self.pending_async_payments_messages.lock().unwrap(); + const HTLC_AVAILABLE_LIMIT: usize = 10; + reply_paths + .iter() + .flat_map(|reply_path| invoice.message_paths().iter().map(move |invoice_path| (invoice_path, reply_path))) + .take(HTLC_AVAILABLE_LIMIT) + .for_each(|(invoice_path, reply_path)| { + let instructions = MessageSendInstructions::WithSpecifiedReplyPath { + destination: Destination::BlindedPath(invoice_path.clone()), + reply_path: reply_path.clone(), + }; + let message = AsyncPaymentsMessage::HeldHtlcAvailable(HeldHtlcAvailable {}); + pending_async_payments_messages.push((message, instructions)); + }); + + Ok(()) + } + #[cfg(async_payments)] fn send_payment_for_static_invoice( &self, payment_id: PaymentId From 763fd545156930760c96d2a67628837d73508b25 Mon Sep 17 00:00:00 2001 From: shaavan Date: Thu, 13 Feb 2025 21:19:33 +0530 Subject: [PATCH 09/14] Move AsyncPaymentsMessageHandler to flow.rs --- lightning-background-processor/src/lib.rs | 27 ++++- lightning/src/ln/async_payments_tests.rs | 6 +- lightning/src/ln/channelmanager.rs | 127 +++----------------- lightning/src/ln/functional_test_utils.rs | 8 +- lightning/src/offers/flow.rs | 140 ++++++++++++++++++++-- lightning/src/onion_message/messenger.rs | 4 +- 6 files changed, 183 insertions(+), 129 deletions(-) diff --git a/lightning-background-processor/src/lib.rs b/lightning-background-processor/src/lib.rs index ef889d4e80f..c49d0cc1ad6 100644 --- a/lightning-background-processor/src/lib.rs +++ b/lightning-background-processor/src/lib.rs @@ -1083,6 +1083,7 @@ mod tests { IgnoringMessageHandler, MessageHandler, PeerManager, SocketDescriptor, }; use lightning::ln::types::ChannelId; + use lightning::offers::flow; use lightning::onion_message::messenger::{DefaultMessageRouter, OnionMessenger}; use lightning::routing::gossip::{NetworkGraph, P2PGossipSync}; use lightning::routing::router::{CandidateRouteHop, DefaultRouter, Path, RouteHop}; @@ -1178,6 +1179,19 @@ mod tests { >, >; + type OffersMessageFlow = flow::OffersMessageFlow< + Arc, + Arc, + Arc< + DefaultMessageRouter< + Arc>>, + Arc, + Arc, + >, + >, + Arc, + >; + type OM = OnionMessenger< Arc, Arc, @@ -1191,7 +1205,7 @@ mod tests { >, >, IgnoringMessageHandler, - Arc, + Arc, IgnoringMessageHandler, IgnoringMessageHandler, >; @@ -1587,6 +1601,15 @@ mod tests { params, genesis_block.header.time, )); + let offers_message_flow = Arc::new(OffersMessageFlow::new( + manager.inbound_payment_key, + manager.get_our_node_id(), + keys_manager.clone(), + manager.clone(), + msg_router.clone(), + logger.clone(), + )); + let messenger = Arc::new(OnionMessenger::new( keys_manager.clone(), keys_manager.clone(), @@ -1594,7 +1617,7 @@ mod tests { manager.clone(), msg_router.clone(), IgnoringMessageHandler {}, - manager.clone(), + offers_message_flow, IgnoringMessageHandler {}, IgnoringMessageHandler {}, )); diff --git a/lightning/src/ln/async_payments_tests.rs b/lightning/src/ln/async_payments_tests.rs index 8d18938272c..c28e5700111 100644 --- a/lightning/src/ln/async_payments_tests.rs +++ b/lightning/src/ln/async_payments_tests.rs @@ -332,7 +332,7 @@ fn ignore_unexpected_static_invoice() { nodes[0] .onion_messenger .handle_onion_message(nodes[1].node.get_our_node_id(), &unexpected_static_invoice_om); - let async_pmts_msgs = AsyncPaymentsMessageHandler::release_pending_messages(nodes[0].node); + let async_pmts_msgs = nodes[0].offers_handler.release_pending_messages(); assert!(async_pmts_msgs.is_empty()); assert!(nodes[0].node.get_and_clear_pending_events().is_empty()); @@ -356,7 +356,7 @@ fn ignore_unexpected_static_invoice() { nodes[0] .onion_messenger .handle_onion_message(nodes[1].node.get_our_node_id(), &static_invoice_om); - let async_pmts_msgs = AsyncPaymentsMessageHandler::release_pending_messages(nodes[0].node); + let async_pmts_msgs = nodes[0].offers_handler.release_pending_messages(); assert!(!async_pmts_msgs.is_empty()); assert!(async_pmts_msgs .into_iter() @@ -381,7 +381,7 @@ fn ignore_unexpected_static_invoice() { nodes[0] .onion_messenger .handle_onion_message(nodes[1].node.get_our_node_id(), &dup_static_invoice_om); - let async_pmts_msgs = AsyncPaymentsMessageHandler::release_pending_messages(nodes[0].node); + let async_pmts_msgs = nodes[0].offers_handler.release_pending_messages(); assert!(async_pmts_msgs.is_empty()); } diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 67ade6e3247..d257577657c 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -33,7 +33,7 @@ use bitcoin::secp256k1::Secp256k1; use bitcoin::{secp256k1, Sequence}; use crate::events::FundingInfo; -use crate::blinded_path::message::{AsyncPaymentsContext, MessageContext, OffersContext}; +use crate::blinded_path::message::{MessageContext, OffersContext}; use crate::blinded_path::NodeIdLookUp; use crate::blinded_path::message::{BlindedMessagePath, MessageForwardNode}; use crate::blinded_path::payment::{AsyncBolt12OfferContext, BlindedPaymentPath, Bolt12OfferContext, Bolt12RefundContext, PaymentConstraints, PaymentContext, UnauthenticatedReceiveTlvs}; @@ -74,9 +74,8 @@ use crate::offers::offer::{Offer, OfferBuilder}; use crate::offers::parse::Bolt12SemanticError; use crate::offers::refund::{Refund, RefundBuilder}; use crate::offers::signer; -use crate::onion_message::async_payments::{AsyncPaymentsMessage, HeldHtlcAvailable, ReleaseHeldHtlc, AsyncPaymentsMessageHandler}; use crate::onion_message::dns_resolution::HumanReadableName; -use crate::onion_message::messenger::{Destination, MessageRouter, Responder, ResponseInstruction, MessageSendInstructions}; +use crate::onion_message::messenger::{Destination, MessageRouter, MessageSendInstructions}; use crate::onion_message::offers::OffersMessage; use crate::sign::{EntropySource, NodeSigner, Recipient, SignerProvider}; use crate::sign::ecdsa::EcdsaChannelSigner; @@ -88,6 +87,7 @@ use crate::util::ser::{BigSize, FixedLengthReader, Readable, ReadableArgs, Maybe use crate::util::logger::{Level, Logger, WithContext}; use crate::util::errors::APIError; #[cfg(async_payments)] use { + crate::blinded_path::message::AsyncPaymentsContext, crate::offers::offer::Amount, crate::offers::static_invoice::{DEFAULT_RELATIVE_EXPIRY as STATIC_INVOICE_DEFAULT_RELATIVE_EXPIRY, StaticInvoice, StaticInvoiceBuilder}, }; @@ -2553,7 +2553,10 @@ where our_network_pubkey: PublicKey, - pub(super) inbound_payment_key: inbound_payment::ExpandedKey, + /// The [`ExpandedKey`] for use in encrypting and decrypting inbound payment data. + /// + /// [`ExpandedKey`]: crate::ln::inbound_payment::ExpandedKey + pub inbound_payment_key: inbound_payment::ExpandedKey, /// LDK puts the [fake scids] that it generates into namespaces, to identify the type of an /// incoming payment. To make it harder for a third-party to identify the type of a payment, @@ -2646,7 +2649,6 @@ where pending_offers_messages: Mutex>, #[cfg(any(test, feature = "_test_utils"))] pub(crate) pending_offers_messages: Mutex>, - pending_async_payments_messages: Mutex>, /// Tracks the message events that are to be broadcasted when we are connected to some peer. pending_broadcast_messages: Mutex>, @@ -3579,7 +3581,6 @@ where funding_batch_states: Mutex::new(BTreeMap::new()), pending_offers_messages: Mutex::new(Vec::new()), - pending_async_payments_messages: Mutex::new(Vec::new()), pending_broadcast_messages: Mutex::new(Vec::new()), last_days_feerates: Mutex::new(VecDeque::new()), @@ -4783,45 +4784,6 @@ where res } - #[cfg(async_payments)] - fn initiate_async_payment( - &self, invoice: &StaticInvoice, payment_id: PaymentId - ) -> Result<(), Bolt12PaymentError> { - - self.handle_static_invoice_received(invoice, payment_id)?; - - let nonce = Nonce::from_entropy_source(&*self.entropy_source); - let hmac = payment_id.hmac_for_async_payment(nonce, &self.inbound_payment_key); - let reply_paths = match self.create_blinded_paths( - MessageContext::AsyncPayments( - AsyncPaymentsContext::OutboundPayment { payment_id, nonce, hmac } - ) - ) { - Ok(paths) => paths, - Err(()) => { - self.abandon_payment_with_reason(payment_id, PaymentFailureReason::BlindedPathCreationFailed); - return Err(Bolt12PaymentError::BlindedPathCreationFailed); - } - }; - - let mut pending_async_payments_messages = self.pending_async_payments_messages.lock().unwrap(); - const HTLC_AVAILABLE_LIMIT: usize = 10; - reply_paths - .iter() - .flat_map(|reply_path| invoice.message_paths().iter().map(move |invoice_path| (invoice_path, reply_path))) - .take(HTLC_AVAILABLE_LIMIT) - .for_each(|(invoice_path, reply_path)| { - let instructions = MessageSendInstructions::WithSpecifiedReplyPath { - destination: Destination::BlindedPath(invoice_path.clone()), - reply_path: reply_path.clone(), - }; - let message = AsyncPaymentsMessage::HeldHtlcAvailable(HeldHtlcAvailable {}); - pending_async_payments_messages.push((message, instructions)); - }); - - Ok(()) - } - #[cfg(async_payments)] fn send_payment_for_static_invoice( &self, payment_id: PaymentId @@ -9999,6 +9961,18 @@ where self.pending_outbound_payments.received_offer(payment_id, retryable_invoice_request) } + #[cfg(async_payments)] + fn handle_static_invoice_received( + &self, invoice: &StaticInvoice, payment_id: PaymentId + ) -> Result<(), Bolt12PaymentError> { + self.handle_static_invoice_received(invoice, payment_id) + } + + #[cfg(async_payments)] + fn send_payment_for_static_invoice(&self, payment_id: PaymentId) -> Result<(), Bolt12PaymentError> { + self.send_payment_for_static_invoice(payment_id) + } + // ----Temporary Functions---- // Set of functions temporarily moved to OffersMessageCommons for easier // transition of code from ChannelManager to OffersMessageFlow @@ -10018,13 +9992,6 @@ where ) -> Result<(), Bolt12SemanticError> { self.pay_for_offer_intern(offer, quantity, amount_msats, payer_note, payment_id, human_readable_name, create_pending_payment) } - - #[cfg(async_payments)] - fn initiate_async_payment( - &self, invoice: &StaticInvoice, payment_id: PaymentId - ) -> Result<(), Bolt12PaymentError> { - self.initiate_async_payment(invoice, payment_id) - } } /// Defines the maximum number of [`OffersMessage`] including different reply paths to be sent @@ -12016,61 +11983,6 @@ where } } -impl -AsyncPaymentsMessageHandler for ChannelManager -where - M::Target: chain::Watch<::EcdsaSigner>, - T::Target: BroadcasterInterface, - ES::Target: EntropySource, - NS::Target: NodeSigner, - SP::Target: SignerProvider, - F::Target: FeeEstimator, - R::Target: Router, - MR::Target: MessageRouter, - L::Target: Logger, -{ - fn handle_held_htlc_available( - &self, _message: HeldHtlcAvailable, _context: AsyncPaymentsContext, - _responder: Option - ) -> Option<(ReleaseHeldHtlc, ResponseInstruction)> { - #[cfg(async_payments)] { - match _context { - AsyncPaymentsContext::InboundPayment { nonce, hmac, path_absolute_expiry } => { - if let Err(()) = signer::verify_held_htlc_available_context( - nonce, hmac, &self.inbound_payment_key - ) { return None } - if self.duration_since_epoch() > path_absolute_expiry { return None } - }, - _ => return None - } - return _responder.map(|responder| (ReleaseHeldHtlc {}, responder.respond())) - } - #[cfg(not(async_payments))] - return None - } - - fn handle_release_held_htlc(&self, _message: ReleaseHeldHtlc, _context: AsyncPaymentsContext) { - #[cfg(async_payments)] { - let (payment_id, nonce, hmac) = match _context { - AsyncPaymentsContext::OutboundPayment { payment_id, hmac, nonce } => { - (payment_id, nonce, hmac) - }, - _ => return - }; - if payment_id.verify_for_async_payment(hmac, nonce, &self.inbound_payment_key).is_err() { return } - if let Err(e) = self.send_payment_for_static_invoice(payment_id) { - log_trace!( - self.logger, "Failed to release held HTLC with payment id {}: {:?}", payment_id, e - ); - } - } - } - - fn release_pending_messages(&self) -> Vec<(AsyncPaymentsMessage, MessageSendInstructions)> { - core::mem::take(&mut self.pending_async_payments_messages.lock().unwrap()) - } -} - impl NodeIdLookUp for ChannelManager where @@ -14025,7 +13937,6 @@ where funding_batch_states: Mutex::new(BTreeMap::new()), pending_offers_messages: Mutex::new(Vec::new()), - pending_async_payments_messages: Mutex::new(Vec::new()), pending_broadcast_messages: Mutex::new(Vec::new()), diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index d0f45c49f1c..589dc9c76f9 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -427,7 +427,7 @@ type TestOnionMessenger<'chan_man, 'node_cfg, 'chan_mon_cfg> = OnionMessenger< &'chan_man TestChannelManager<'node_cfg, 'chan_mon_cfg>, &'node_cfg test_utils::TestMessageRouter<'chan_mon_cfg>, Arc>, - &'chan_man TestChannelManager<'node_cfg, 'chan_mon_cfg>, + Arc>, IgnoringMessageHandler, IgnoringMessageHandler, >; @@ -440,7 +440,7 @@ type TestOnionMessenger<'chan_man, 'node_cfg, 'chan_mon_cfg> = OnionMessenger< &'chan_man TestChannelManager<'node_cfg, 'chan_mon_cfg>, &'node_cfg test_utils::TestMessageRouter<'chan_mon_cfg>, Arc>, - &'chan_man TestChannelManager<'node_cfg, 'chan_mon_cfg>, + Arc>, Arc>, IgnoringMessageHandler, >; @@ -3372,13 +3372,13 @@ pub fn create_network<'a, 'b: 'a, 'c: 'b>(node_count: usize, cfgs: &'b Vec, ) -> Result<(), ()>; + #[cfg(async_payments)] + /// Handles the received [`StaticInvoice`], ensuring it is neither unexpected nor a duplicate + /// before proceeding with further operations. + fn handle_static_invoice_received( + &self, invoice: &StaticInvoice, payment_id: PaymentId, + ) -> Result<(), Bolt12PaymentError>; + + #[cfg(async_payments)] + /// Sends payment for the [`StaticInvoice`] associated with the given `payment_id`. + fn send_payment_for_static_invoice(&self, payment_id: PaymentId) -> Result<(), Bolt12PaymentError>; + // ----Temporary Functions---- // Set of functions temporarily moved to OffersMessageCommons for easier // transition of code from ChannelManager to OffersMessageFlow @@ -182,12 +196,6 @@ pub trait OffersMessageCommons { payer_note: Option, payment_id: PaymentId, human_readable_name: Option, create_pending_payment: CPP, ) -> Result<(), Bolt12SemanticError>; - - /// Initiate a new async payment - #[cfg(async_payments)] - fn initiate_async_payment( - &self, invoice: &StaticInvoice, payment_id: PaymentId - ) -> Result<(), Bolt12PaymentError>; } /// [`SimpleArcOffersMessageFlow`] is useful when you need a [`OffersMessageFlow`] with a static lifetime, e.g. @@ -325,6 +333,8 @@ where message_router: MR, entropy_source: ES, + pending_async_payments_messages: Mutex>, + #[cfg(feature = "_test_utils")] /// In testing, it is useful be able to forge a name -> offer mapping so that we can pay an /// offer generated in the test. @@ -366,6 +376,8 @@ where message_router, entropy_source, + pending_async_payments_messages: Mutex::new(Vec::new()), + logger, #[cfg(feature = "_test_utils")] @@ -405,6 +417,17 @@ where } } + pub(crate) fn duration_since_epoch(&self) -> Duration { + #[cfg(not(feature = "std"))] + let now = self.commons.get_highest_seen_timestamp(); + #[cfg(feature = "std")] + let now = std::time::SystemTime::now() + .duration_since(std::time::SystemTime::UNIX_EPOCH) + .expect("SystemTime::now() should come after SystemTime::UNIX_EPOCH"); + + now + } + /// Creates a collection of blinded paths by delegating to /// [`MessageRouter::create_blinded_paths`]. /// @@ -503,6 +526,53 @@ where } } +impl OffersMessageFlow +where + ES::Target: EntropySource, + OMC::Target: OffersMessageCommons, + MR::Target: MessageRouter, + L::Target: Logger, +{ + #[cfg(async_payments)] + fn initiate_async_payment( + &self, invoice: &StaticInvoice, payment_id: PaymentId + ) -> Result<(), Bolt12PaymentError> { + + self.commons.handle_static_invoice_received(invoice, payment_id)?; + + let nonce = Nonce::from_entropy_source(&*self.entropy_source); + let hmac = payment_id.hmac_for_async_payment(nonce, &self.inbound_payment_key); + let reply_paths = match self.create_blinded_paths( + MessageContext::AsyncPayments( + AsyncPaymentsContext::OutboundPayment { payment_id, nonce, hmac } + ) + ) { + Ok(paths) => paths, + Err(()) => { + self.commons.abandon_payment_with_reason(payment_id, PaymentFailureReason::BlindedPathCreationFailed); + return Err(Bolt12PaymentError::BlindedPathCreationFailed); + } + }; + + let mut pending_async_payments_messages = self.pending_async_payments_messages.lock().unwrap(); + const HTLC_AVAILABLE_LIMIT: usize = 10; + reply_paths + .iter() + .flat_map(|reply_path| invoice.message_paths().iter().map(move |invoice_path| (invoice_path, reply_path))) + .take(HTLC_AVAILABLE_LIMIT) + .for_each(|(invoice_path, reply_path)| { + let instructions = MessageSendInstructions::WithSpecifiedReplyPath { + destination: Destination::BlindedPath(invoice_path.clone()), + reply_path: reply_path.clone(), + }; + let message = AsyncPaymentsMessage::HeldHtlcAvailable(HeldHtlcAvailable {}); + pending_async_payments_messages.push((message, instructions)); + }); + + Ok(()) + } +} + impl OffersMessageHandler for OffersMessageFlow where ES::Target: EntropySource, @@ -681,7 +751,7 @@ where }, _ => return None }; - let res = self.commons.initiate_async_payment(&invoice, payment_id); + let res = self.initiate_async_payment(&invoice, payment_id); handle_pay_invoice_res!(res, invoice, self.logger); }, OffersMessage::InvoiceError(invoice_error) => { @@ -811,3 +881,53 @@ where } } +impl AsyncPaymentsMessageHandler + for OffersMessageFlow +where + ES::Target: EntropySource, + OMC::Target: OffersMessageCommons, + MR::Target: MessageRouter, + L::Target: Logger, +{ + fn handle_held_htlc_available( + &self, _message: HeldHtlcAvailable, _context: AsyncPaymentsContext, + _responder: Option + ) -> Option<(ReleaseHeldHtlc, ResponseInstruction)> { + #[cfg(async_payments)] { + match _context { + AsyncPaymentsContext::InboundPayment { nonce, hmac, path_absolute_expiry } => { + if let Err(()) = signer::verify_held_htlc_available_context( + nonce, hmac, &self.inbound_payment_key + ) { return None } + if self.duration_since_epoch() > path_absolute_expiry { return None } + }, + _ => return None + } + return _responder.map(|responder| (ReleaseHeldHtlc {}, responder.respond())) + } + #[cfg(not(async_payments))] + return None + } + + fn handle_release_held_htlc(&self, _message: ReleaseHeldHtlc, _context: AsyncPaymentsContext) { + #[cfg(async_payments)] { + let (payment_id, nonce, hmac) = match _context { + AsyncPaymentsContext::OutboundPayment { payment_id, hmac, nonce } => { + (payment_id, nonce, hmac) + }, + _ => return + }; + if payment_id.verify_for_async_payment(hmac, nonce, &self.inbound_payment_key).is_err() { return } + if let Err(e) = self.commons.send_payment_for_static_invoice(payment_id) { + log_trace!( + self.logger, "Failed to release held HTLC with payment id {}: {:?}", payment_id, e + ); + } + } + } + + fn release_pending_messages(&self) -> Vec<(AsyncPaymentsMessage, MessageSendInstructions)> { + core::mem::take(&mut self.pending_async_payments_messages.lock().unwrap()) + } +} + diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index 216a84b190a..4d68a358547 100644 --- a/lightning/src/onion_message/messenger.rs +++ b/lightning/src/onion_message/messenger.rs @@ -2107,7 +2107,7 @@ pub type SimpleArcOnionMessenger = OnionMessenger< Arc>, Arc>>, Arc, Arc>>, Arc>, - Arc>, + Arc>, Arc>, IgnoringMessageHandler, >; @@ -2128,7 +2128,7 @@ pub type SimpleArcOnionMessenger = OnionMessenger< Arc>, Arc>>, Arc, Arc>>, Arc>, - Arc>, + Arc>, IgnoringMessageHandler, IgnoringMessageHandler, >; From c859cd76a84a99fd7f114f639d04cea19459ade5 Mon Sep 17 00:00:00 2001 From: shaavan Date: Thu, 13 Feb 2025 21:44:20 +0530 Subject: [PATCH 10/14] Move Bolt12 builders to flow.rs --- lightning-dns-resolver/src/lib.rs | 2 +- lightning/src/ln/async_payments_tests.rs | 16 +- lightning/src/ln/channelmanager.rs | 459 ++--------------- lightning/src/ln/inbound_payment.rs | 4 +- .../src/ln/max_payment_path_len_tests.rs | 2 +- lightning/src/ln/offers_tests.rs | 97 ++-- lightning/src/offers/flow.rs | 483 +++++++++++++++++- lightning/src/offers/offer.rs | 16 +- lightning/src/offers/refund.rs | 16 +- 9 files changed, 588 insertions(+), 507 deletions(-) diff --git a/lightning-dns-resolver/src/lib.rs b/lightning-dns-resolver/src/lib.rs index 35b59236ea8..9e67389756a 100644 --- a/lightning-dns-resolver/src/lib.rs +++ b/lightning-dns-resolver/src/lib.rs @@ -391,7 +391,7 @@ mod test { let name = HumanReadableName::from_encoded("matt@mattcorallo.com").unwrap(); // When we get the proof back, override its contents to an offer from nodes[1] - let bs_offer = nodes[1].node.create_offer_builder(None).unwrap().build().unwrap(); + let bs_offer = nodes[1].offers_handler.create_offer_builder(None).unwrap().build().unwrap(); nodes[0] .offers_handler .testing_dnssec_proof_offer_resolution_override diff --git a/lightning/src/ln/async_payments_tests.rs b/lightning/src/ln/async_payments_tests.rs index c28e5700111..cd76eed85ef 100644 --- a/lightning/src/ln/async_payments_tests.rs +++ b/lightning/src/ln/async_payments_tests.rs @@ -116,12 +116,12 @@ fn create_static_invoice( ) .unwrap(); let (offer_builder, offer_nonce) = recipient - .node + .offers_handler .create_async_receive_offer_builder(blinded_paths_to_always_online_node) .unwrap(); let offer = offer_builder.build().unwrap(); let static_invoice = recipient - .node + .offers_handler .create_static_invoice_builder(&offer, offer_nonce, relative_expiry) .unwrap() .build_and_sign(&secp_ctx) @@ -222,12 +222,12 @@ fn static_invoice_unknown_required_features() { ) .unwrap(); let (offer_builder, nonce) = nodes[2] - .node + .offers_handler .create_async_receive_offer_builder(blinded_paths_to_always_online_node) .unwrap(); let offer = offer_builder.build().unwrap(); let static_invoice_unknown_req_features = nodes[2] - .node + .offers_handler .create_static_invoice_builder(&offer, nonce, None) .unwrap() .features_unchecked(Bolt12InvoiceFeatures::unknown()) @@ -492,7 +492,7 @@ fn expired_static_invoice_fail() { // Wait until the static invoice expires before providing it to the sender. let block = create_dummy_block( nodes[0].best_block_hash(), - nodes[0].node.duration_since_epoch().as_secs() as u32 + INVOICE_EXPIRY_SECS + 1, + nodes[0].offers_handler.duration_since_epoch().as_secs() as u32 + INVOICE_EXPIRY_SECS + 1, Vec::new(), ); connect_block(&nodes[0], &block); @@ -823,7 +823,7 @@ fn invalid_async_receive_with_retry( ) .unwrap(); let (offer_builder, offer_nonce) = nodes[2] - .node + .offers_handler .create_async_receive_offer_builder(blinded_paths_to_always_online_node) .unwrap(); let offer = offer_builder.build().unwrap(); @@ -835,7 +835,7 @@ fn invalid_async_receive_with_retry( let mut static_invoice_paths = Vec::new(); for _ in 0..3 { let static_inv_for_path = nodes[2] - .node + .offers_handler .create_static_invoice_builder(&offer, offer_nonce, None) .unwrap() .build_and_sign(&secp_ctx) @@ -845,7 +845,7 @@ fn invalid_async_receive_with_retry( nodes[2].router.expect_blinded_payment_paths(static_invoice_paths); let static_invoice = nodes[2] - .node + .offers_handler .create_static_invoice_builder(&offer, offer_nonce, None) .unwrap() .build_and_sign(&secp_ctx) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index d257577657c..20c9d757e49 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -70,9 +70,9 @@ use crate::ln::outbound_payment::{OutboundPayments, PendingOutboundPayment, Retr use crate::offers::invoice::{Bolt12Invoice, DEFAULT_RELATIVE_EXPIRY, DerivedSigningPubkey, InvoiceBuilder, UnsignedBolt12Invoice}; use crate::offers::invoice_request::{InvoiceRequest, InvoiceRequestBuilder}; use crate::offers::nonce::Nonce; -use crate::offers::offer::{Offer, OfferBuilder}; +use crate::offers::offer::Offer; use crate::offers::parse::Bolt12SemanticError; -use crate::offers::refund::{Refund, RefundBuilder}; +use crate::offers::refund::Refund; use crate::offers::signer; use crate::onion_message::dns_resolution::HumanReadableName; use crate::onion_message::messenger::{Destination, MessageRouter, MessageSendInstructions}; @@ -86,29 +86,20 @@ use crate::util::string::UntrustedString; use crate::util::ser::{BigSize, FixedLengthReader, Readable, ReadableArgs, MaybeReadable, Writeable, Writer, VecWriter}; use crate::util::logger::{Level, Logger, WithContext}; use crate::util::errors::APIError; -#[cfg(async_payments)] use { - crate::blinded_path::message::AsyncPaymentsContext, - crate::offers::offer::Amount, - crate::offers::static_invoice::{DEFAULT_RELATIVE_EXPIRY as STATIC_INVOICE_DEFAULT_RELATIVE_EXPIRY, StaticInvoice, StaticInvoiceBuilder}, -}; +#[cfg(async_payments)] + use crate::offers::static_invoice::StaticInvoice; #[cfg(feature = "dnssec")] use crate::onion_message::dns_resolution::OMNameResolver; #[cfg(not(c_bindings))] use { - crate::offers::offer::DerivedMetadata, crate::onion_message::messenger::DefaultMessageRouter, crate::routing::router::DefaultRouter, crate::routing::gossip::NetworkGraph, crate::routing::scoring::{ProbabilisticScorer, ProbabilisticScoringFeeParameters}, crate::sign::KeysManager, }; -#[cfg(c_bindings)] -use { - crate::offers::offer::OfferWithDerivedMetadataBuilder, - crate::offers::refund::RefundMaybeWithDerivedMetadataBuilder, -}; use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, CreationError, Currency, Description, InvoiceBuilder as Bolt11InvoiceBuilder, SignOrCreationError, DEFAULT_EXPIRY_TIME}; @@ -2079,57 +2070,8 @@ where /// ``` /// /// ## BOLT 12 Offers -/// -/// The [`offers`] module is useful for creating BOLT 12 offers. An [`Offer`] is a precursor to a -/// [`Bolt12Invoice`], which must first be requested by the payer. The interchange of these messages -/// as defined in the specification is handled by [`ChannelManager`] and [`OffersMessageFlow`]'s -/// implementation of [`OffersMessageHandler`]. However, this only works with an [`Offer`] created -/// using a builder returned by [`create_offer_builder`]. With this approach, BOLT 12 offers and -/// invoices are stateless just as BOLT 11 invoices are. -/// -/// ``` -/// # use lightning::events::{Event, EventsProvider, PaymentPurpose}; -/// # use lightning::ln::channelmanager::AChannelManager; -/// # use lightning::offers::parse::Bolt12SemanticError; -/// # -/// # fn example(channel_manager: T) -> Result<(), Bolt12SemanticError> { -/// # let channel_manager = channel_manager.get_cm(); -/// # let absolute_expiry = None; -/// let offer = channel_manager -/// .create_offer_builder(absolute_expiry)? -/// # ; -/// # // Needed for compiling for c_bindings -/// # let builder: lightning::offers::offer::OfferBuilder<_, _> = offer.into(); -/// # let offer = builder -/// .description("coffee".to_string()) -/// .amount_msats(10_000_000) -/// .build()?; -/// let bech32_offer = offer.to_string(); -/// -/// // On the event processing thread -/// channel_manager.process_pending_events(&|event| { -/// match event { -/// Event::PaymentClaimable { payment_hash, purpose, .. } => match purpose { -/// PaymentPurpose::Bolt12OfferPayment { payment_preimage: Some(payment_preimage), .. } => { -/// println!("Claiming payment {}", payment_hash); -/// channel_manager.claim_funds(payment_preimage); -/// }, -/// PaymentPurpose::Bolt12OfferPayment { payment_preimage: None, .. } => { -/// println!("Unknown payment hash: {}", payment_hash); -/// } -/// # _ => {}, -/// }, -/// Event::PaymentClaimed { payment_hash, amount_msat, .. } => { -/// println!("Claimed {} msats", amount_msat); -/// }, -/// // ... -/// # _ => {}, -/// } -/// Ok(()) -/// }); -/// # Ok(()) -/// # } -/// ``` +/// +/// For more information on creating offers, see [`create_offer_builder`]. /// /// Use [`pay_for_offer`] to initiated payment, which sends an [`InvoiceRequest`] for an [`Offer`] /// and pays the [`Bolt12Invoice`] response. @@ -2183,67 +2125,8 @@ where /// ``` /// /// ## BOLT 12 Refunds -/// -/// A [`Refund`] is a request for an invoice to be paid. Like *paying* for an [`Offer`], *creating* -/// a [`Refund`] involves maintaining state since it represents a future outbound payment. -/// Therefore, use [`create_refund_builder`] when creating one, otherwise [`ChannelManager`] will -/// refuse to pay any corresponding [`Bolt12Invoice`] that it receives. -/// -/// ``` -/// # use core::time::Duration; -/// # use lightning::events::{Event, EventsProvider}; -/// # use lightning::ln::channelmanager::{AChannelManager, PaymentId, RecentPaymentDetails, Retry}; -/// # use lightning::offers::parse::Bolt12SemanticError; -/// # -/// # fn example( -/// # channel_manager: T, amount_msats: u64, absolute_expiry: Duration, retry: Retry, -/// # max_total_routing_fee_msat: Option -/// # ) -> Result<(), Bolt12SemanticError> { -/// # let channel_manager = channel_manager.get_cm(); -/// let payment_id = PaymentId([42; 32]); -/// let refund = channel_manager -/// .create_refund_builder( -/// amount_msats, absolute_expiry, payment_id, retry, max_total_routing_fee_msat -/// )? -/// # ; -/// # // Needed for compiling for c_bindings -/// # let builder: lightning::offers::refund::RefundBuilder<_> = refund.into(); -/// # let refund = builder -/// .description("coffee".to_string()) -/// .payer_note("refund for order 1234".to_string()) -/// .build()?; -/// let bech32_refund = refund.to_string(); -/// -/// // First the payment will be waiting on an invoice -/// let expected_payment_id = payment_id; -/// assert!( -/// channel_manager.list_recent_payments().iter().find(|details| matches!( -/// details, -/// RecentPaymentDetails::AwaitingInvoice { payment_id: expected_payment_id } -/// )).is_some() -/// ); -/// -/// // Once the invoice is received, a payment will be sent -/// assert!( -/// channel_manager.list_recent_payments().iter().find(|details| matches!( -/// details, -/// RecentPaymentDetails::Pending { payment_id: expected_payment_id, .. } -/// )).is_some() -/// ); -/// -/// // On the event processing thread -/// channel_manager.process_pending_events(&|event| { -/// match event { -/// Event::PaymentSent { payment_id: Some(payment_id), .. } => println!("Paid {}", payment_id), -/// Event::PaymentFailed { payment_id, .. } => println!("Failed paying {}", payment_id), -/// // ... -/// # _ => {}, -/// } -/// Ok(()) -/// }); -/// # Ok(()) -/// # } -/// ``` +/// +/// For more information on creating refunds, see [`create_refund_builder`]. /// /// 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. @@ -2369,10 +2252,10 @@ where /// [`claim_funds`]: Self::claim_funds /// [`send_payment`]: Self::send_payment /// [`offers`]: crate::offers -/// [`create_offer_builder`]: Self::create_offer_builder +/// [`create_offer_builder`]: crate::offers::flow::OffersMessageFlow::create_offer_builder /// [`pay_for_offer`]: Self::pay_for_offer /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest -/// [`create_refund_builder`]: Self::create_refund_builder +/// [`create_refund_builder`]: crate::offers::flow::OffersMessageFlow::create_refund_builder /// [`request_refund_payment`]: Self::request_refund_payment /// [`peer_disconnected`]: msgs::ChannelMessageHandler::peer_disconnected /// [`funding_created`]: msgs::FundingCreated @@ -2381,8 +2264,6 @@ where /// [`update_channel`]: chain::Watch::update_channel /// [`ChannelUpdate`]: msgs::ChannelUpdate /// [`read`]: ReadableArgs::read -/// [`OffersMessageFlow`]: crate::offers::flow::OffersMessageFlow -/// [`OffersMessageHandler`]: crate::onion_message::offers::OffersMessageHandler // // Lock order: // The tree structure below illustrates the lock order requirements for the different locks of the @@ -2867,19 +2748,6 @@ const MAX_UNFUNDED_CHANNEL_PEERS: usize = 50; /// many peers we reject new (inbound) connections. const MAX_NO_CHANNEL_PEERS: usize = 250; -/// The maximum expiration from the current time where an [`Offer`] or [`Refund`] is considered -/// short-lived, while anything with a greater expiration is considered long-lived. -/// -/// Using [`ChannelManager::create_offer_builder`] or [`ChannelManager::create_refund_builder`], -/// will included a [`BlindedMessagePath`] created using: -/// - [`MessageRouter::create_compact_blinded_paths`] when short-lived, and -/// - [`MessageRouter::create_blinded_paths`] when long-lived. -/// -/// Using compact [`BlindedMessagePath`]s may provide better privacy as the [`MessageRouter`] could select -/// more hops. However, since they use short channel ids instead of pubkeys, they are more likely to -/// become invalid over time as channels are closed. Thus, they are only suitable for short-term use. -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. /// These include payments that have yet to find a successful path, or have unresolved HTLCs. #[derive(Debug, PartialEq)] @@ -9737,138 +9605,6 @@ impl Default for Bolt11InvoiceParameters { } } -macro_rules! create_offer_builder { ($self: ident, $builder: ty) => { - /// Creates an [`OfferBuilder`] such that the [`Offer`] it builds is recognized by the - /// [`ChannelManager`] when handling [`InvoiceRequest`] messages for the offer. The offer's - /// expiration will be `absolute_expiry` if `Some`, otherwise it will not expire. - /// - /// # Privacy - /// - /// Uses [`MessageRouter`] to construct a [`BlindedMessagePath`] for the offer based on the given - /// `absolute_expiry` according to [`MAX_SHORT_LIVED_RELATIVE_EXPIRY`]. See those docs for - /// privacy implications as well as those of the parameterized [`Router`], which implements - /// [`MessageRouter`]. - /// - /// Also, uses a derived signing pubkey in the offer for recipient privacy. - /// - /// # Limitations - /// - /// Requires a direct connection to the introduction node in the responding [`InvoiceRequest`]'s - /// reply path. - /// - /// # Errors - /// - /// Errors if the parameterized [`Router`] is unable to create a blinded path for the offer. - /// - /// [`Offer`]: crate::offers::offer::Offer - /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest - pub fn create_offer_builder( - &$self, absolute_expiry: Option - ) -> Result<$builder, 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; - - let nonce = Nonce::from_entropy_source(entropy); - let context = OffersContext::InvoiceRequest { nonce }; - let path = $self.create_blinded_paths_using_absolute_expiry(context, absolute_expiry) - .and_then(|paths| paths.into_iter().next().ok_or(())) - .map_err(|_| Bolt12SemanticError::MissingPaths)?; - let builder = OfferBuilder::deriving_signing_pubkey(node_id, expanded_key, nonce, secp_ctx) - .chain_hash($self.chain_hash) - .path(path); - - let builder = match absolute_expiry { - None => builder, - Some(absolute_expiry) => builder.absolute_expiry(absolute_expiry), - }; - - Ok(builder.into()) - } -} } - -macro_rules! create_refund_builder { ($self: ident, $builder: ty) => { - /// Creates a [`RefundBuilder`] such that the [`Refund`] it builds is recognized by the - /// [`ChannelManager`] when handling [`Bolt12Invoice`] messages for the refund. - /// - /// # Payment - /// - /// The provided `payment_id` is used to ensure that only one invoice is paid for the refund. - /// See [Avoiding Duplicate Payments] for other requirements once the payment has been sent. - /// - /// The builder will have the provided expiration set. Any changes to the expiration on the - /// returned builder will not be honored by [`ChannelManager`]. For non-`std`, the highest seen - /// block time minus two hours is used for the current time when determining if the refund has - /// expired. - /// - /// To revoke the refund, use [`ChannelManager::abandon_payment`] prior to receiving the - /// invoice. If abandoned, or an invoice isn't received before expiration, the payment will fail - /// with an [`Event::PaymentFailed`]. - /// - /// If `max_total_routing_fee_msat` is not specified, The default from - /// [`RouteParameters::from_payment_params_and_value`] is applied. - /// - /// # Privacy - /// - /// Uses [`MessageRouter`] to construct a [`BlindedMessagePath`] for the refund based on the given - /// `absolute_expiry` according to [`MAX_SHORT_LIVED_RELATIVE_EXPIRY`]. See those docs for - /// privacy implications as well as those of the parameterized [`Router`], which implements - /// [`MessageRouter`]. - /// - /// Also, uses a derived payer id in the refund for payer privacy. - /// - /// # Limitations - /// - /// Requires a direct connection to an introduction node in the responding - /// [`Bolt12Invoice::payment_paths`]. - /// - /// # Errors - /// - /// Errors if: - /// - a duplicate `payment_id` is provided given the caveats in the aforementioned link, - /// - `amount_msats` is invalid, or - /// - the parameterized [`Router`] is unable to create a blinded path for the refund. - /// - /// [`Refund`]: crate::offers::refund::Refund - /// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice - /// [`Bolt12Invoice::payment_paths`]: crate::offers::invoice::Bolt12Invoice::payment_paths - /// [Avoiding Duplicate Payments]: #avoiding-duplicate-payments - pub fn create_refund_builder( - &$self, amount_msats: u64, absolute_expiry: Duration, payment_id: PaymentId, - retry_strategy: Retry, max_total_routing_fee_msat: Option - ) -> Result<$builder, 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; - - let nonce = Nonce::from_entropy_source(entropy); - let context = OffersContext::OutboundPayment { payment_id, nonce, hmac: None }; - let path = $self.create_blinded_paths_using_absolute_expiry(context, Some(absolute_expiry)) - .and_then(|paths| paths.into_iter().next().ok_or(())) - .map_err(|_| Bolt12SemanticError::MissingPaths)?; - - let builder = RefundBuilder::deriving_signing_pubkey( - node_id, expanded_key, nonce, secp_ctx, amount_msats, payment_id - )? - .chain_hash($self.chain_hash) - .absolute_expiry(absolute_expiry) - .path(path); - - let _persistence_guard = PersistenceNotifierGuard::notify_on_drop($self); - - let expiration = StaleExpiration::AbsoluteTimeout(absolute_expiry); - $self.pending_outbound_payments - .add_new_awaiting_invoice( - payment_id, expiration, retry_strategy, max_total_routing_fee_msat, None, - ) - .map_err(|_| Bolt12SemanticError::DuplicatePaymentId)?; - - Ok(builder.into()) - } -} } - impl OffersMessageCommons for ChannelManager where M::Target: chain::Watch<::EcdsaSigner>, @@ -9881,6 +9617,10 @@ where MR::Target: MessageRouter, L::Target: Logger, { + fn get_chain_hash(&self) -> ChainHash { + self.chain_hash + } + fn get_current_blocktime(&self) -> Duration { Duration::from_secs(self.highest_seen_timestamp.load(Ordering::Acquire) as u64) } @@ -9927,6 +9667,15 @@ where self.pending_outbound_payments.release_invoice_requests_awaiting_invoice() } + fn add_new_awaiting_invoice( + &self, payment_id: PaymentId, expiration: StaleExpiration, retry_strategy: Retry, + max_total_routing_fee_msat: Option, retryable_invoice_request: Option + ) -> Result<(), ()> { + self.pending_outbound_payments.add_new_awaiting_invoice ( + payment_id, expiration, retry_strategy, max_total_routing_fee_msat, retryable_invoice_request, + ) + } + fn sign_bolt12_invoice( &self, invoice: &UnsignedBolt12Invoice, ) -> Result { @@ -10013,99 +9762,6 @@ where MR::Target: MessageRouter, L::Target: Logger, { - #[cfg(not(c_bindings))] - create_offer_builder!(self, OfferBuilder); - #[cfg(not(c_bindings))] - create_refund_builder!(self, RefundBuilder); - - #[cfg(c_bindings)] - create_offer_builder!(self, OfferWithDerivedMetadataBuilder); - #[cfg(c_bindings)] - create_refund_builder!(self, RefundMaybeWithDerivedMetadataBuilder); - - /// Create an offer for receiving async payments as an often-offline recipient. - /// - /// Because we may be offline when the payer attempts to request an invoice, you MUST: - /// 1. Provide at least 1 [`BlindedMessagePath`] terminating at an always-online node that will - /// serve the [`StaticInvoice`] created from this offer on our behalf. - /// 2. Use [`Self::create_static_invoice_builder`] to create a [`StaticInvoice`] from this - /// [`Offer`] plus the returned [`Nonce`], and provide the static invoice to the - /// aforementioned always-online node. - #[cfg(async_payments)] - pub fn create_async_receive_offer_builder( - &self, message_paths_to_always_online_node: Vec - ) -> Result<(OfferBuilder, Nonce), Bolt12SemanticError> { - if message_paths_to_always_online_node.is_empty() { - return Err(Bolt12SemanticError::MissingPaths) - } - - 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; - - let nonce = Nonce::from_entropy_source(entropy); - let mut builder = OfferBuilder::deriving_signing_pubkey( - node_id, expanded_key, nonce, secp_ctx - ).chain_hash(self.chain_hash); - - for path in message_paths_to_always_online_node { - builder = builder.path(path); - } - - Ok((builder.into(), nonce)) - } - - /// Creates a [`StaticInvoiceBuilder`] from the corresponding [`Offer`] and [`Nonce`] that were - /// created via [`Self::create_async_receive_offer_builder`]. If `relative_expiry` is unset, the - /// invoice's expiry will default to [`STATIC_INVOICE_DEFAULT_RELATIVE_EXPIRY`]. - #[cfg(async_payments)] - pub fn create_static_invoice_builder<'a>( - &self, offer: &'a Offer, offer_nonce: Nonce, relative_expiry: Option - ) -> Result, Bolt12SemanticError> { - let expanded_key = &self.inbound_payment_key; - let entropy = &*self.entropy_source; - let secp_ctx = &self.secp_ctx; - - let payment_context = PaymentContext::AsyncBolt12Offer( - AsyncBolt12OfferContext { offer_nonce } - ); - let amount_msat = offer.amount().and_then(|amount| { - match amount { - Amount::Bitcoin { amount_msats } => Some(amount_msats), - Amount::Currency { .. } => None - } - }); - - let relative_expiry = relative_expiry.unwrap_or(STATIC_INVOICE_DEFAULT_RELATIVE_EXPIRY); - let relative_expiry_secs: u32 = relative_expiry.as_secs().try_into().unwrap_or(u32::MAX); - - let created_at = self.duration_since_epoch(); - let payment_secret = inbound_payment::create_for_spontaneous_payment( - &self.inbound_payment_key, amount_msat, relative_expiry_secs, created_at.as_secs(), None - ).map_err(|()| Bolt12SemanticError::InvalidAmount)?; - - let payment_paths = self.create_blinded_payment_paths( - amount_msat, payment_secret, payment_context, relative_expiry_secs - ).map_err(|()| Bolt12SemanticError::MissingPaths)?; - - let nonce = Nonce::from_entropy_source(entropy); - let hmac = signer::hmac_for_held_htlc_available_context(nonce, expanded_key); - let path_absolute_expiry = Duration::from_secs( - inbound_payment::calculate_absolute_expiry(created_at.as_secs(), relative_expiry_secs) - ); - let context = MessageContext::AsyncPayments( - AsyncPaymentsContext::InboundPayment { nonce, hmac, path_absolute_expiry } - ); - let async_receive_message_paths = self.create_blinded_paths(context) - .map_err(|()| Bolt12SemanticError::MissingPaths)?; - - StaticInvoiceBuilder::for_offer_using_derived_keys( - offer, payment_paths, async_receive_message_paths, created_at, expanded_key, - offer_nonce, secp_ctx - ).map(|inv| inv.allow_mpp().relative_expiry(relative_expiry_secs)) - } - /// Pays for an [`Offer`] using the given parameters by creating an [`InvoiceRequest`] and /// enqueuing it to be sent via an onion message. [`ChannelManager`] will pay the actual /// [`Bolt12Invoice`] once it is received. @@ -10462,38 +10118,6 @@ where inbound_payment::get_payment_preimage(payment_hash, payment_secret, &self.inbound_payment_key) } - /// Creates a collection of blinded paths by delegating to [`MessageRouter`] based on - /// the path's intended lifetime. - /// - /// Whether or not the path is compact depends on whether the path is short-lived or long-lived, - /// respectively, based on the given `absolute_expiry` as seconds since the Unix epoch. See - /// [`MAX_SHORT_LIVED_RELATIVE_EXPIRY`]. - fn create_blinded_paths_using_absolute_expiry( - &self, context: OffersContext, absolute_expiry: Option, - ) -> Result, ()> { - let now = self.duration_since_epoch(); - let max_short_lived_absolute_expiry = now.saturating_add(MAX_SHORT_LIVED_RELATIVE_EXPIRY); - - if absolute_expiry.unwrap_or(Duration::MAX) <= max_short_lived_absolute_expiry { - self.create_compact_blinded_paths(context) - } else { - self.create_blinded_paths(MessageContext::Offers(context)) - } - } - - pub(super) fn duration_since_epoch(&self) -> Duration { - #[cfg(not(feature = "std"))] - let now = Duration::from_secs( - self.highest_seen_timestamp.load(Ordering::Acquire) as u64 - ); - #[cfg(feature = "std")] - let now = std::time::SystemTime::now() - .duration_since(std::time::SystemTime::UNIX_EPOCH) - .expect("SystemTime::now() should come after SystemTime::UNIX_EPOCH"); - - now - } - /// Creates a collection of blinded paths by delegating to /// [`MessageRouter::create_blinded_paths`]. /// @@ -10515,32 +10139,17 @@ where .and_then(|paths| (!paths.is_empty()).then(|| paths).ok_or(())) } - /// Creates a collection of blinded paths by delegating to - /// [`MessageRouter::create_compact_blinded_paths`]. - /// - /// Errors if the `MessageRouter` errors. - fn create_compact_blinded_paths(&self, context: OffersContext) -> Result, ()> { - let recipient = self.get_our_node_id(); - let secp_ctx = &self.secp_ctx; - - let peers = self.per_peer_state.read().unwrap() - .iter() - .map(|(node_id, peer_state)| (node_id, peer_state.lock().unwrap())) - .filter(|(_, peer)| peer.is_connected) - .filter(|(_, peer)| peer.latest_features.supports_onion_messages()) - .map(|(node_id, peer)| MessageForwardNode { - node_id: *node_id, - short_channel_id: peer.channel_by_id - .iter() - .filter(|(_, channel)| channel.context().is_usable()) - .min_by_key(|(_, channel)| channel.context().channel_creation_height) - .and_then(|(_, channel)| channel.context().get_short_channel_id()), - }) - .collect::>(); - - self.message_router - .create_compact_blinded_paths(recipient, MessageContext::Offers(context), peers, secp_ctx) - .and_then(|paths| (!paths.is_empty()).then(|| paths).ok_or(())) + #[cfg(async_payments)] + pub(super) fn duration_since_epoch(&self) -> Duration { + #[cfg(not(feature = "std"))] + let now = Duration::from_secs( + self.highest_seen_timestamp.load(Ordering::Acquire) as u64 + ); + #[cfg(feature = "std")] + let now = std::time::SystemTime::now() + .duration_since(std::time::SystemTime::UNIX_EPOCH) + .expect("SystemTime::now() should come after SystemTime::UNIX_EPOCH"); + now } /// Creates multi-hop blinded payment paths for the given `amount_msats` by delegating to diff --git a/lightning/src/ln/inbound_payment.rs b/lightning/src/ln/inbound_payment.rs index 87781793d06..66a434d182b 100644 --- a/lightning/src/ln/inbound_payment.rs +++ b/lightning/src/ln/inbound_payment.rs @@ -194,7 +194,7 @@ pub fn create_from_hash(keys: &ExpandedKey, min_value_msat: Option, payment } #[cfg(async_payments)] -pub(super) fn create_for_spontaneous_payment( +pub(crate) fn create_for_spontaneous_payment( keys: &ExpandedKey, min_value_msat: Option, invoice_expiry_delta_secs: u32, current_time: u64, min_final_cltv_expiry_delta: Option ) -> Result { @@ -213,7 +213,7 @@ pub(super) fn create_for_spontaneous_payment( Ok(construct_payment_secret(&iv_bytes, &metadata_bytes, &keys.metadata_key)) } -pub(super) fn calculate_absolute_expiry(highest_seen_timestamp: u64, invoice_expiry_delta_secs: u32) -> u64 { +pub(crate) fn calculate_absolute_expiry(highest_seen_timestamp: u64, invoice_expiry_delta_secs: u32) -> u64 { // We assume that highest_seen_timestamp is pretty close to the current time - it's updated when // we receive a new block with the maximum time we've seen in a header. It should never be more // than two hours in the future. Thus, we add two hours here as a buffer to ensure we diff --git a/lightning/src/ln/max_payment_path_len_tests.rs b/lightning/src/ln/max_payment_path_len_tests.rs index efb07b641bd..d9eaa270d6f 100644 --- a/lightning/src/ln/max_payment_path_len_tests.rs +++ b/lightning/src/ln/max_payment_path_len_tests.rs @@ -391,7 +391,7 @@ fn bolt12_invoice_too_large_blinded_paths() { ) ]); - let offer = nodes[1].node.create_offer_builder(None).unwrap().build().unwrap(); + let offer = nodes[1].offers_handler.create_offer_builder(None).unwrap().build().unwrap(); let payment_id = PaymentId([1; 32]); nodes[0].node.pay_for_offer(&offer, None, Some(5000), None, payment_id, Retry::Attempts(0), None).unwrap(); let invreq_om = nodes[0].onion_messenger.next_onion_message_for_peer(nodes[1].node.get_our_node_id()).unwrap(); diff --git a/lightning/src/ln/offers_tests.rs b/lightning/src/ln/offers_tests.rs index 95fed704a8a..c4a93c9975d 100644 --- a/lightning/src/ln/offers_tests.rs +++ b/lightning/src/ln/offers_tests.rs @@ -48,8 +48,9 @@ use crate::blinded_path::message::BlindedMessagePath; use crate::blinded_path::payment::{Bolt12OfferContext, Bolt12RefundContext, PaymentContext}; use crate::blinded_path::message::{MessageContext, OffersContext}; use crate::events::{ClosureReason, Event, HTLCDestination, MessageSendEventsProvider, PaymentFailureReason, PaymentPurpose}; -use crate::ln::channelmanager::{MAX_SHORT_LIVED_RELATIVE_EXPIRY, PaymentId, RecentPaymentDetails, RecipientOnionFields, Retry, self}; +use crate::ln::channelmanager::{PaymentId, RecentPaymentDetails, RecipientOnionFields, Retry, self}; use crate::ln::outbound_payment::IDEMPOTENCY_TIMEOUT_TICKS; +use crate::offers::flow::MAX_SHORT_LIVED_RELATIVE_EXPIRY; use crate::types::features::Bolt12InvoiceFeatures; use crate::ln::functional_test_utils::*; use crate::ln::msgs::{ChannelMessageHandler, Init, NodeAnnouncement, OnionMessage, OnionMessageHandler, RoutingMessageHandler, SocketAddress, UnsignedGossipMessage, UnsignedNodeAnnouncement}; @@ -296,7 +297,7 @@ fn prefers_non_tor_nodes_in_blinded_paths() { let tor = SocketAddress::OnionV2([255, 254, 253, 252, 251, 250, 249, 248, 247, 246, 38, 7]); announce_node_address(charlie, &[alice, bob, david, &nodes[4], &nodes[5]], tor.clone()); - let offer = bob.node + let offer = bob.offers_handler .create_offer_builder(None).unwrap() .amount_msats(10_000_000) .build().unwrap(); @@ -312,7 +313,7 @@ fn prefers_non_tor_nodes_in_blinded_paths() { announce_node_address(&nodes[4], &[alice, bob, charlie, david, &nodes[5]], tor.clone()); announce_node_address(&nodes[5], &[alice, bob, charlie, david, &nodes[4]], tor.clone()); - let offer = bob.node + let offer = bob.offers_handler .create_offer_builder(None).unwrap() .amount_msats(10_000_000) .build().unwrap(); @@ -363,7 +364,7 @@ fn prefers_more_connected_nodes_in_blinded_paths() { disconnect_peers(alice, &[charlie, david, &nodes[4], &nodes[5]]); disconnect_peers(david, &[bob, &nodes[4], &nodes[5]]); - let offer = bob.node + let offer = bob.offers_handler .create_offer_builder(None).unwrap() .amount_msats(10_000_000) .build().unwrap(); @@ -389,8 +390,8 @@ fn creates_short_lived_offer() { let alice_id = alice.node.get_our_node_id(); let bob = &nodes[1]; - let absolute_expiry = alice.node.duration_since_epoch() + MAX_SHORT_LIVED_RELATIVE_EXPIRY; - let offer = alice.node + let absolute_expiry = alice.offers_handler.duration_since_epoch() + MAX_SHORT_LIVED_RELATIVE_EXPIRY; + let offer = alice.offers_handler .create_offer_builder(Some(absolute_expiry)).unwrap() .build().unwrap(); assert_eq!(offer.absolute_expiry(), Some(absolute_expiry)); @@ -415,9 +416,9 @@ fn creates_long_lived_offer() { let alice = &nodes[0]; let alice_id = alice.node.get_our_node_id(); - let absolute_expiry = alice.node.duration_since_epoch() + MAX_SHORT_LIVED_RELATIVE_EXPIRY + let absolute_expiry = alice.offers_handler.duration_since_epoch() + MAX_SHORT_LIVED_RELATIVE_EXPIRY + Duration::from_secs(1); - let offer = alice.node + let offer = alice.offers_handler .create_offer_builder(Some(absolute_expiry)) .unwrap() .build().unwrap(); @@ -427,7 +428,7 @@ fn creates_long_lived_offer() { assert_eq!(path.introduction_node(), &IntroductionNode::NodeId(alice_id)); } - let offer = alice.node + let offer = alice.offers_handler .create_offer_builder(None).unwrap() .build().unwrap(); assert_eq!(offer.absolute_expiry(), None); @@ -451,9 +452,9 @@ fn creates_short_lived_refund() { let bob = &nodes[1]; let bob_id = bob.node.get_our_node_id(); - let absolute_expiry = bob.node.duration_since_epoch() + MAX_SHORT_LIVED_RELATIVE_EXPIRY; + let absolute_expiry = bob.offers_handler.duration_since_epoch() + MAX_SHORT_LIVED_RELATIVE_EXPIRY; let payment_id = PaymentId([1; 32]); - let refund = bob.node + let refund = bob.offers_handler .create_refund_builder(10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None) .unwrap() .build().unwrap(); @@ -479,10 +480,10 @@ fn creates_long_lived_refund() { let bob = &nodes[1]; let bob_id = bob.node.get_our_node_id(); - let absolute_expiry = bob.node.duration_since_epoch() + MAX_SHORT_LIVED_RELATIVE_EXPIRY + let absolute_expiry = bob.offers_handler.duration_since_epoch() + MAX_SHORT_LIVED_RELATIVE_EXPIRY + Duration::from_secs(1); let payment_id = PaymentId([1; 32]); - let refund = bob.node + let refund = bob.offers_handler .create_refund_builder(10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None) .unwrap() .build().unwrap(); @@ -531,7 +532,7 @@ fn creates_and_pays_for_offer_using_two_hop_blinded_path() { disconnect_peers(alice, &[charlie, david, &nodes[4], &nodes[5]]); disconnect_peers(david, &[bob, &nodes[4], &nodes[5]]); - let offer = alice.node + let offer = alice.offers_handler .create_offer_builder(None) .unwrap() .amount_msats(10_000_000) @@ -641,7 +642,7 @@ fn creates_and_pays_for_refund_using_two_hop_blinded_path() { let absolute_expiry = Duration::from_secs(u64::MAX); let payment_id = PaymentId([1; 32]); - let refund = david.node + let refund = david.offers_handler .create_refund_builder(10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None) .unwrap() .build().unwrap(); @@ -701,7 +702,7 @@ fn creates_and_pays_for_offer_using_one_hop_blinded_path() { let bob = &nodes[1]; let bob_id = bob.node.get_our_node_id(); - let offer = alice.node + let offer = alice.offers_handler .create_offer_builder(None).unwrap() .amount_msats(10_000_000) .build().unwrap(); @@ -770,7 +771,7 @@ fn creates_and_pays_for_refund_using_one_hop_blinded_path() { let absolute_expiry = Duration::from_secs(u64::MAX); let payment_id = PaymentId([1; 32]); - let refund = bob.node + let refund = bob.offers_handler .create_refund_builder(10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None) .unwrap() .build().unwrap(); @@ -824,7 +825,7 @@ fn pays_for_offer_without_blinded_paths() { let bob = &nodes[1]; let bob_id = bob.node.get_our_node_id(); - let offer = alice.node + let offer = alice.offers_handler .create_offer_builder(None).unwrap() .clear_paths() .amount_msats(10_000_000) @@ -879,7 +880,7 @@ fn pays_for_refund_without_blinded_paths() { let absolute_expiry = Duration::from_secs(u64::MAX); let payment_id = PaymentId([1; 32]); - let refund = bob.node + let refund = bob.offers_handler .create_refund_builder(10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None) .unwrap() .clear_paths() @@ -948,7 +949,7 @@ fn send_invoice_requests_with_distinct_reply_path() { disconnect_peers(alice, &[charlie, david, &nodes[4], &nodes[5], &nodes[6]]); disconnect_peers(david, &[bob, &nodes[4], &nodes[5]]); - let offer = alice.node + let offer = alice.offers_handler .create_offer_builder(None) .unwrap() .amount_msats(10_000_000) @@ -1034,7 +1035,7 @@ fn send_invoice_for_refund_with_distinct_reply_path() { let absolute_expiry = Duration::from_secs(u64::MAX); let payment_id = PaymentId([1; 32]); - let refund = alice.node + let refund = alice.offers_handler .create_refund_builder(10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None) .unwrap() .build().unwrap(); @@ -1084,7 +1085,7 @@ fn creates_and_pays_for_offer_with_retry() { let bob = &nodes[1]; let bob_id = bob.node.get_our_node_id(); - let offer = alice.node + let offer = alice.offers_handler .create_offer_builder(None).unwrap() .amount_msats(10_000_000) .build().unwrap(); @@ -1169,7 +1170,7 @@ fn creates_offer_with_blinded_path_using_unannounced_introduction_node() { let bob = &nodes[1]; let bob_id = bob.node.get_our_node_id(); - let offer = alice.node + let offer = alice.offers_handler .create_offer_builder(None).unwrap() .amount_msats(10_000_000) .build().unwrap(); @@ -1237,7 +1238,7 @@ fn creates_refund_with_blinded_path_using_unannounced_introduction_node() { let absolute_expiry = Duration::from_secs(u64::MAX); let payment_id = PaymentId([1; 32]); - let refund = bob.node + let refund = bob.offers_handler .create_refund_builder(10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None) .unwrap() .build().unwrap(); @@ -1299,7 +1300,7 @@ fn fails_authentication_when_handling_invoice_request() { disconnect_peers(alice, &[charlie, david, &nodes[4], &nodes[5]]); disconnect_peers(david, &[bob, &nodes[4], &nodes[5]]); - let offer = alice.node + let offer = alice.offers_handler .create_offer_builder(None) .unwrap() .amount_msats(10_000_000) @@ -1311,7 +1312,7 @@ fn fails_authentication_when_handling_invoice_request() { assert_eq!(path.introduction_node(), &IntroductionNode::NodeId(bob_id)); } - let invalid_path = alice.node + let invalid_path = alice.offers_handler .create_offer_builder(None) .unwrap() .build().unwrap() @@ -1411,7 +1412,7 @@ fn fails_authentication_when_handling_invoice_for_offer() { disconnect_peers(alice, &[charlie, david, &nodes[4], &nodes[5]]); disconnect_peers(david, &[bob, &nodes[4], &nodes[5]]); - let offer = alice.node + let offer = alice.offers_handler .create_offer_builder(None) .unwrap() .amount_msats(10_000_000) @@ -1520,7 +1521,7 @@ fn fails_authentication_when_handling_invoice_for_refund() { let absolute_expiry = Duration::from_secs(u64::MAX); let payment_id = PaymentId([1; 32]); - let refund = david.node + let refund = david.offers_handler .create_refund_builder(10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None) .unwrap() .build().unwrap(); @@ -1554,7 +1555,7 @@ fn fails_authentication_when_handling_invoice_for_refund() { // Send the invoice to David using an invalid blinded path. let invalid_path = refund.paths().first().unwrap().clone(); let payment_id = PaymentId([2; 32]); - let refund = david.node + let refund = david.offers_handler .create_refund_builder(10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None) .unwrap() .build().unwrap(); @@ -1608,8 +1609,8 @@ fn fails_creating_or_paying_for_offer_without_connected_peers() { disconnect_peers(alice, &[bob, charlie, david, &nodes[4], &nodes[5]]); disconnect_peers(david, &[bob, charlie, &nodes[4], &nodes[5]]); - let absolute_expiry = alice.node.duration_since_epoch() + MAX_SHORT_LIVED_RELATIVE_EXPIRY; - match alice.node.create_offer_builder(Some(absolute_expiry)) { + let absolute_expiry = alice.offers_handler.duration_since_epoch() + MAX_SHORT_LIVED_RELATIVE_EXPIRY; + match alice.offers_handler.create_offer_builder(Some(absolute_expiry)) { Ok(_) => panic!("Expected error"), Err(e) => assert_eq!(e, Bolt12SemanticError::MissingPaths), } @@ -1618,7 +1619,7 @@ fn fails_creating_or_paying_for_offer_without_connected_peers() { args.send_channel_ready = (true, true); reconnect_nodes(args); - let offer = alice.node + let offer = alice.offers_handler .create_offer_builder(Some(absolute_expiry)).unwrap() .amount_msats(10_000_000) .build().unwrap(); @@ -1679,9 +1680,9 @@ fn fails_creating_refund_or_sending_invoice_without_connected_peers() { disconnect_peers(alice, &[bob, charlie, david, &nodes[4], &nodes[5]]); disconnect_peers(david, &[bob, charlie, &nodes[4], &nodes[5]]); - let absolute_expiry = david.node.duration_since_epoch() + MAX_SHORT_LIVED_RELATIVE_EXPIRY; + let absolute_expiry = david.offers_handler.duration_since_epoch() + MAX_SHORT_LIVED_RELATIVE_EXPIRY; let payment_id = PaymentId([1; 32]); - match david.node.create_refund_builder( + match david.offers_handler.create_refund_builder( 10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None ) { Ok(_) => panic!("Expected error"), @@ -1692,7 +1693,7 @@ fn fails_creating_refund_or_sending_invoice_without_connected_peers() { args.send_channel_ready = (true, true); reconnect_nodes(args); - let refund = david.node + let refund = david.offers_handler .create_refund_builder(10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None) .unwrap() .build().unwrap(); @@ -1722,7 +1723,7 @@ fn fails_creating_invoice_request_for_unsupported_chain() { let alice = &nodes[0]; let bob = &nodes[1]; - let offer = alice.node + let offer = alice.offers_handler .create_offer_builder(None).unwrap() .clear_chains() .chain(Network::Signet) @@ -1750,7 +1751,7 @@ fn fails_sending_invoice_with_unsupported_chain_for_refund() { let absolute_expiry = Duration::from_secs(u64::MAX); let payment_id = PaymentId([1; 32]); - let refund = bob.node + let refund = bob.offers_handler .create_refund_builder(10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None) .unwrap() .chain(Network::Signet) @@ -1781,7 +1782,7 @@ fn fails_creating_invoice_request_without_blinded_reply_path() { disconnect_peers(alice, &[charlie, david, &nodes[4], &nodes[5]]); disconnect_peers(david, &[bob, charlie, &nodes[4], &nodes[5]]); - let offer = alice.node + let offer = alice.offers_handler .create_offer_builder(None).unwrap() .amount_msats(10_000_000) .build().unwrap(); @@ -1815,7 +1816,7 @@ fn fails_creating_invoice_request_with_duplicate_payment_id() { disconnect_peers(alice, &[charlie, david, &nodes[4], &nodes[5]]); - let offer = alice.node + let offer = alice.offers_handler .create_offer_builder(None).unwrap() .amount_msats(10_000_000) .build().unwrap(); @@ -1848,13 +1849,13 @@ fn fails_creating_refund_with_duplicate_payment_id() { let absolute_expiry = Duration::from_secs(u64::MAX); let payment_id = PaymentId([1; 32]); assert!( - nodes[0].node.create_refund_builder( + nodes[0].offers_handler.create_refund_builder( 10_000, absolute_expiry, payment_id, Retry::Attempts(0), None ).is_ok() ); expect_recent_payment!(nodes[0], RecentPaymentDetails::AwaitingInvoice, payment_id); - match nodes[0].node.create_refund_builder( + match nodes[0].offers_handler.create_refund_builder( 10_000, absolute_expiry, payment_id, Retry::Attempts(0), None ) { Ok(_) => panic!("Expected error"), @@ -1901,7 +1902,7 @@ fn fails_sending_invoice_without_blinded_payment_paths_for_offer() { disconnect_peers(alice, &[charlie, david, &nodes[4], &nodes[5]]); disconnect_peers(david, &[bob, &nodes[4], &nodes[5]]); - let offer = alice.node + let offer = alice.offers_handler .create_offer_builder(None).unwrap() .amount_msats(10_000_000) .build().unwrap(); @@ -1974,7 +1975,7 @@ fn fails_sending_invoice_without_blinded_payment_paths_for_refund() { let absolute_expiry = Duration::from_secs(u64::MAX); let payment_id = PaymentId([1; 32]); - let refund = david.node + let refund = david.offers_handler .create_refund_builder(10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None) .unwrap() .build().unwrap(); @@ -2023,7 +2024,7 @@ fn fails_paying_invoice_more_than_once() { let absolute_expiry = Duration::from_secs(u64::MAX); let payment_id = PaymentId([1; 32]); - let refund = david.node + let refund = david.offers_handler .create_refund_builder(10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None) .unwrap() .build().unwrap(); @@ -2110,7 +2111,7 @@ fn fails_paying_invoice_with_unknown_required_features() { disconnect_peers(alice, &[charlie, david, &nodes[4], &nodes[5]]); disconnect_peers(david, &[bob, &nodes[4], &nodes[5]]); - let offer = alice.node + let offer = alice.offers_handler .create_offer_builder(None).unwrap() .amount_msats(10_000_000) .build().unwrap(); @@ -2145,7 +2146,7 @@ fn fails_paying_invoice_with_unknown_required_features() { let expanded_key = alice.keys_manager.get_inbound_payment_key(); let secp_ctx = Secp256k1::new(); - let created_at = alice.node.duration_since_epoch(); + let created_at = alice.offers_handler.duration_since_epoch(); let invoice = invoice_request .verify_using_recipient_data(nonce, &expanded_key, &secp_ctx).unwrap() .respond_using_derived_keys_no_std(payment_paths, payment_hash, created_at).unwrap() @@ -2190,7 +2191,7 @@ fn rejects_keysend_to_non_static_invoice_path() { create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); // First pay the offer and save the payment preimage and invoice. - let offer = nodes[1].node.create_offer_builder(None).unwrap().build().unwrap(); + let offer = nodes[1].offers_handler.create_offer_builder(None).unwrap().build().unwrap(); let amt_msat = 5000; let payment_id = PaymentId([1; 32]); nodes[0].node.pay_for_offer(&offer, None, Some(amt_msat), None, payment_id, Retry::Attempts(1), None).unwrap(); @@ -2269,7 +2270,7 @@ fn no_double_pay_with_stale_channelmanager() { let bob_id = nodes[1].node.get_our_node_id(); let amt_msat = nodes[0].node.list_usable_channels()[0].next_outbound_htlc_limit_msat + 1; // Force MPP - let offer = nodes[1].node + let offer = nodes[1].offers_handler .create_offer_builder(None).unwrap() .clear_paths() .amount_msats(amt_msat) diff --git a/lightning/src/offers/flow.rs b/lightning/src/offers/flow.rs index dc18f36dff2..bb94e64d491 100644 --- a/lightning/src/offers/flow.rs +++ b/lightning/src/offers/flow.rs @@ -10,6 +10,7 @@ //! Provides data structures and functions for creating and managing Offers messages, //! facilitating communication, and handling Bolt12 invoice payments. +use bitcoin::constants::ChainHash; use bitcoin::key::Secp256k1; use bitcoin::secp256k1::{self, PublicKey}; @@ -32,10 +33,12 @@ use crate::blinded_path::message::{AsyncPaymentsContext, BlindedMessagePath, Mes use crate::blinded_path::payment::{BlindedPaymentPath, Bolt12OfferContext, PaymentContext}; use crate::events::PaymentFailureReason; use crate::ln::channelmanager::{Bolt12PaymentError, PaymentId, Verification}; -use crate::ln::outbound_payment::RetryableInvoiceRequest; +use crate::ln::outbound_payment::{Retry, RetryableInvoiceRequest, StaleExpiration}; use crate::offers::invoice::{Bolt12Invoice, DerivedSigningPubkey, ExplicitSigningPubkey, InvoiceBuilder, UnsignedBolt12Invoice, DEFAULT_RELATIVE_EXPIRY}; use crate::offers::invoice_request::InvoiceRequest; +use crate::offers::offer::OfferBuilder; use crate::offers::parse::Bolt12SemanticError; +use crate::offers::refund::RefundBuilder; use crate::onion_message::messenger::{MessageSendInstructions, Responder, ResponseInstruction}; use crate::onion_message::offers::{OffersMessage, OffersMessageHandler}; use crate::sync::{Mutex, MutexGuard}; @@ -45,13 +48,22 @@ use crate::util::logger::{Logger, WithContext}; #[cfg(not(feature = "std"))] use core::sync::atomic::Ordering; -#[cfg(async_payments)] use { - crate::offers::static_invoice::StaticInvoice, - crate::offers::signer, +#[cfg(async_payments)] +use { + crate::offers::offer::Amount, + crate::blinded_path::payment::AsyncBolt12OfferContext, + crate::offers::signer, + crate::offers::static_invoice::{DEFAULT_RELATIVE_EXPIRY as STATIC_INVOICE_DEFAULT_RELATIVE_EXPIRY, StaticInvoice, StaticInvoiceBuilder}, +}; + +#[cfg(c_bindings)] use { + crate::offers::refund::RefundMaybeWithDerivedMetadataBuilder, + crate::offers::offer::OfferWithDerivedMetadataBuilder, }; #[cfg(not(c_bindings))] use { + crate::offers::offer::DerivedMetadata, crate::ln::channelmanager::{SimpleArcChannelManager, SimpleRefChannelManager}, crate::onion_message::messenger::DefaultMessageRouter, crate::routing::gossip::NetworkGraph, @@ -63,7 +75,6 @@ use { use { crate::blinded_path::message::DNSResolverContext, crate::ln::channelmanager::OFFERS_MESSAGE_REQUEST_LIMIT, - crate::ln::outbound_payment::{Retry, StaleExpiration}, crate::onion_message::dns_resolution::{DNSResolverMessage, DNSResolverMessageHandler, DNSSECProof, DNSSECQuery, OMNameResolver}, crate::onion_message::messenger::Destination, }; @@ -72,6 +83,9 @@ use { /// /// [`ChannelManager`]: crate::ln::channelmanager::ChannelManager pub trait OffersMessageCommons { + /// Get the [`ChainHash`] of the chain + fn get_chain_hash(&self) -> ChainHash; + /// Get the current time determined by highest seen timestamp fn get_current_blocktime(&self) -> Duration; @@ -131,6 +145,13 @@ pub trait OffersMessageCommons { /// Release invoice requests awaiting invoice fn release_invoice_requests_awaiting_invoice(&self) -> Vec<(PaymentId, RetryableInvoiceRequest)>; + /// Add new awaiting invoice + fn add_new_awaiting_invoice( + &self, payment_id: PaymentId, expiration: StaleExpiration, retry_strategy: Retry, + max_total_routing_fee_msat: Option, + retryable_invoice_request: Option, + ) -> Result<(), ()>; + /// Signs the [`TaggedHash`] of a BOLT 12 invoice. /// /// May be called by a function passed to [`UnsignedBolt12Invoice::sign`] where `invoice` is the @@ -314,8 +335,161 @@ where /// /// This struct is essential for enabling BOLT12 payment workflows in the Lightning network, /// providing the foundational mechanisms for Offers and related message exchanges. +/// +/// ## Relationship with `ChannelManager` +/// +/// [`OffersMessageFlow`] and [`ChannelManager`] work in tandem to facilitate BOLT 12 functionality within +/// a Lightning node: +/// - The `OffersMessageFlow` is responsible for creating, managing, and verifying Offers messages, +/// such as BOLT 12 invoices and refunds. +/// - The `ChannelManager` manages the lifecycle of payments tied to these Offers messages, handling +/// tasks like payment execution, tracking payment states, and processing related events. +/// +/// The relationship is further reinforced through the [`OffersMessageCommons`] trait: +/// - `ChannelManager` implements the [`OffersMessageCommons`] trait, providing shared functionality +/// such as metadata verification and signature handling. +/// - `OffersMessageFlow` relies on this trait to perform common operations, enabling it to delegate +/// these tasks to `ChannelManager` by default. +/// +/// Practical Use Case: +/// - A BOLT 12 Offer is created using `OffersMessageFlow`, which provides the necessary interface +/// for Offer creation and invoice generation. +/// - Once a payment is initiated for the Offer, `ChannelManager` takes over, managing the payment's +/// lifecycle, including retries, claims, and event handling. +/// - Events such as `Event::PaymentClaimable` or `Event::PaymentFailed` are processed by +/// `ChannelManager`, which may invoke functionality defined in `OffersMessageCommons` to support +/// Offers-related operations. +/// +/// This modular and decoupled design ensures that `OffersMessageFlow` and `ChannelManager` can +/// operate independently while maintaining a seamless integration for handling Offers and payments +/// in the Lightning network. +/// +/// ## BOLT 12 Offers +/// +/// The [`offers`] module is useful for creating BOLT 12 offers. An [`Offer`] is a precursor to a +/// [`Bolt12Invoice`], which must first be requested by the payer. The interchange of these messages +/// as defined in the specification is handled by [`OffersMessageFlow`] and its implementation of +/// [`OffersMessageHandler`]. However, this only works with an [`Offer`] created using a builder +/// returned by [`create_offer_builder`]. With this approach, BOLT 12 offers and invoices are +/// stateless just as BOLT 11 invoices are. +/// +/// ``` +/// # use lightning::events::{Event, EventsProvider, PaymentPurpose}; +/// # use lightning::ln::channelmanager::AChannelManager; +/// # use lightning::offers::flow::AnOffersMessageFlow; +/// # use lightning::offers::parse::Bolt12SemanticError; +/// +/// # +/// # fn example(offers_flow: T, channel_manager: U) -> Result<(), Bolt12SemanticError> { +/// # let offers_flow = offers_flow.get_omf(); +/// # let channel_manager = channel_manager.get_cm(); +/// # let absolute_expiry = None; +/// # let offer = offers_flow +/// .create_offer_builder(absolute_expiry)? +/// # ; +/// # // Needed for compiling for c_bindings +/// # let builder: lightning::offers::offer::OfferBuilder<_, _> = offer.into(); +/// # let offer = builder +/// .description("coffee".to_string()) +/// .amount_msats(10_000_000) +/// .build()?; +/// let bech32_offer = offer.to_string(); /// +/// // On the event processing thread +/// channel_manager.process_pending_events(&|event| { +/// match event { +/// Event::PaymentClaimable { payment_hash, purpose, .. } => match purpose { +/// PaymentPurpose::Bolt12OfferPayment { payment_preimage: Some(payment_preimage), .. } => { +/// println!("Claiming payment {}", payment_hash); +/// channel_manager.claim_funds(payment_preimage); +/// }, +/// PaymentPurpose::Bolt12OfferPayment { payment_preimage: None, .. } => { +/// println!("Unknown payment hash: {}", payment_hash); +/// } +/// # _ => {}, +/// }, +/// Event::PaymentClaimed { payment_hash, amount_msat, .. } => { +/// println!("Claimed {} msats", amount_msat); +/// }, +/// // ... +/// # _ => {}, +/// } +/// Ok(()) +/// }); +/// # Ok(()) +/// # } +/// ``` +/// +/// ## Bolt 12 Refund +/// A [`Refund`] is a request for an invoice to be paid. Like *paying* for an [`Offer`], *creating* +/// a [`Refund`] involves maintaining state since it represents a future outbound payment. +/// Therefore, use [`create_refund_builder`] when creating one, otherwise [`OffersMessageFlow`] will +/// refuse to pay any corresponding [`Bolt12Invoice`] that it receives. +/// +/// ``` +/// # use core::time::Duration; +/// # use lightning::events::{Event, EventsProvider}; +/// # use lightning::ln::channelmanager::{AChannelManager, PaymentId, RecentPaymentDetails, Retry}; +/// # use lightning::offers::flow::AnOffersMessageFlow; +/// # use lightning::offers::parse::Bolt12SemanticError; +/// # +/// # fn example( +/// # offers_flow: T, channel_manager: U, amount_msats: u64, absolute_expiry: Duration, retry: Retry, +/// # max_total_routing_fee_msat: Option +/// # ) -> Result<(), Bolt12SemanticError> { +/// # let offers_flow = offers_flow.get_omf(); +/// # let channel_manager = channel_manager.get_cm(); +/// # let payment_id = PaymentId([42; 32]); +/// # let refund = offers_flow +/// .create_refund_builder( +/// amount_msats, absolute_expiry, payment_id, retry, max_total_routing_fee_msat +/// )? +/// # ; +/// # // Needed for compiling for c_bindings +/// # let builder: lightning::offers::refund::RefundBuilder<_> = refund.into(); +/// # let refund = builder +/// .description("coffee".to_string()) +/// .payer_note("refund for order 1234".to_string()) +/// .build()?; +/// let bech32_refund = refund.to_string(); +/// +/// // First the payment will be waiting on an invoice +/// let expected_payment_id = payment_id; +/// assert!( +/// channel_manager.list_recent_payments().iter().find(|details| matches!( +/// details, +/// RecentPaymentDetails::AwaitingInvoice { payment_id: expected_payment_id } +/// )).is_some() +/// ); +/// +/// // Once the invoice is received, a payment will be sent +/// assert!( +/// channel_manager.list_recent_payments().iter().find(|details| matches!( +/// details, +/// RecentPaymentDetails::Pending { payment_id: expected_payment_id, .. } +/// )).is_some() +/// ); +/// +/// // On the event processing thread +/// channel_manager.process_pending_events(&|event| { +/// match event { +/// Event::PaymentSent { payment_id: Some(payment_id), .. } => println!("Paid {}", payment_id), +/// Event::PaymentFailed { payment_id, .. } => println!("Failed paying {}", payment_id), +/// // ... +/// # _ => {}, +/// } +/// Ok(()) +/// }); +/// # Ok(()) +/// # } +/// ``` +/// +/// [`offers`]: crate::offers +/// [`Refund`]: crate::offers::refund::Refund +/// [`ChannelManager`]: crate::ln::channelmanager::ChannelManager /// [`DNSResolverMessage`]: crate::onion_message::dns_resolution::DNSResolverMessage +/// [`create_offer_builder`]: Self::create_offer_builder +/// [`create_refund_builder`]: Self::create_refund_builder pub struct OffersMessageFlow where ES::Target: EntropySource, @@ -393,6 +567,26 @@ where } } +/// The maximum expiration from the current time where an [`Offer`] or [`Refund`] is considered +/// short-lived, while anything with a greater expiration is considered long-lived. +/// +/// Using [`OffersMessageFlow::create_offer_builder`] or [`OffersMessageFlow::create_refund_builder`], +/// will included a [`BlindedMessagePath`] created using: +/// - [`MessageRouter::create_compact_blinded_paths`] when short-lived, and +/// - [`MessageRouter::create_blinded_paths`] when long-lived. +/// +/// [`OffersMessageFlow::create_offer_builder`]: crate::offers::flow::OffersMessageFlow::create_offer_builder +/// [`OffersMessageFlow::create_refund_builder`]: crate::offers::flow::OffersMessageFlow::create_refund_builder +/// +/// +/// Using compact [`BlindedMessagePath`]s may provide better privacy as the [`MessageRouter`] could select +/// more hops. However, since they use short channel ids instead of pubkeys, they are more likely to +/// 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); + impl OffersMessageFlow where ES::Target: EntropySource, @@ -449,6 +643,40 @@ where .create_blinded_paths(recipient, context, peers, secp_ctx) .and_then(|paths| (!paths.is_empty()).then(|| paths).ok_or(())) } + + /// Creates a collection of blinded paths by delegating to + /// [`MessageRouter::create_compact_blinded_paths`]. + /// + /// Errors if the `MessageRouter` errors. + fn create_compact_blinded_paths(&self, context: OffersContext) -> Result, ()> { + let recipient = self.get_our_node_id(); + let secp_ctx = &self.secp_ctx; + + let peers = self.commons.get_peer_for_blinded_path(); + + self.message_router + .create_compact_blinded_paths(recipient, MessageContext::Offers(context), peers, secp_ctx) + .and_then(|paths| (!paths.is_empty()).then(|| paths).ok_or(())) + } + + /// Creates a collection of blinded paths by delegating to [`MessageRouter`] based on + /// the path's intended lifetime. + /// + /// Whether or not the path is compact depends on whether the path is short-lived or long-lived, + /// respectively, based on the given `absolute_expiry` as seconds since the Unix epoch. See + /// [`MAX_SHORT_LIVED_RELATIVE_EXPIRY`]. + fn create_blinded_paths_using_absolute_expiry( + &self, context: OffersContext, absolute_expiry: Option, + ) -> Result, ()> { + let now = self.duration_since_epoch(); + let max_short_lived_absolute_expiry = now.saturating_add(MAX_SHORT_LIVED_RELATIVE_EXPIRY); + + if absolute_expiry.unwrap_or(Duration::MAX) <= max_short_lived_absolute_expiry { + self.create_compact_blinded_paths(context) + } else { + self.create_blinded_paths(MessageContext::Offers(context)) + } + } } impl OffersMessageFlow @@ -482,7 +710,7 @@ where /// # Privacy /// /// For payer privacy, uses a derived payer id and uses [`MessageRouter::create_blinded_paths`] - /// to construct a [`BlindedPath`] for the reply path. For further privacy implications, see the + /// to construct a [`BlindedMessagePath`] for the reply path. For further privacy implications, see the /// docs of the parameterized [`Router`], which implements [`MessageRouter`]. /// /// # Limitations @@ -497,7 +725,10 @@ where /// - a duplicate `payment_id` is provided given the caveats in the aforementioned link, /// /// [`Bolt12Invoice::payment_paths`]: crate::offers::invoice::Bolt12Invoice::payment_paths + /// [`ChannelManager::abandon_payment`]: crate::ln::channelmanager::ChannelManager::abandon_payment /// [Avoiding Duplicate Payments]: #avoiding-duplicate-payments + /// [`Router`]: crate::routing::router::Router + /// [`RouteParameters::from_payment_params_and_value`]: crate::routing::router::RouteParameters::from_payment_params_and_value #[cfg(feature = "dnssec")] pub fn pay_for_offer_from_human_readable_name( &self, name: HumanReadableName, amount_msats: u64, payment_id: PaymentId, @@ -573,6 +804,245 @@ where } } +macro_rules! create_offer_builder { ($self: ident, $builder: ty) => { + /// Creates an [`OfferBuilder`] such that the [`Offer`] it builds is recognized by the + /// [`ChannelManager`] when handling [`InvoiceRequest`] messages for the offer. The offer's + /// expiration will be `absolute_expiry` if `Some`, otherwise it will not expire. + /// + /// # Privacy + /// + /// Uses [`MessageRouter`] to construct a [`BlindedMessagePath`] for the offer based on the given + /// `absolute_expiry` according to [`MAX_SHORT_LIVED_RELATIVE_EXPIRY`]. See those docs for + /// privacy implications as well as those of the parameterized [`Router`], which implements + /// [`MessageRouter`]. + /// + /// Also, uses a derived signing pubkey in the offer for recipient privacy. + /// + /// # Limitations + /// + /// Requires a direct connection to the introduction node in the responding [`InvoiceRequest`]'s + /// reply path. + /// + /// # Errors + /// + /// Errors if the parameterized [`Router`] is unable to create a blinded path for the offer. + /// + /// [`Offer`]: crate::offers::offer::Offer + /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest + /// [`ChannelManager`]: crate::ln::channelmanager::ChannelManager + /// [`Router`]: crate::routing::router::Router + pub fn create_offer_builder( + &$self, absolute_expiry: Option + ) -> Result<$builder, 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; + + let nonce = Nonce::from_entropy_source(entropy); + let context = OffersContext::InvoiceRequest { nonce }; + let path = $self.create_blinded_paths_using_absolute_expiry(context, absolute_expiry) + .and_then(|paths| paths.into_iter().next().ok_or(())) + .map_err(|_| Bolt12SemanticError::MissingPaths)?; + let builder = OfferBuilder::deriving_signing_pubkey(node_id, expanded_key, nonce, secp_ctx) + .chain_hash($self.commons.get_chain_hash()) + .path(path); + + let builder = match absolute_expiry { + None => builder, + Some(absolute_expiry) => builder.absolute_expiry(absolute_expiry), + }; + + Ok(builder.into()) + } +} } + +macro_rules! create_refund_builder { ($self: ident, $builder: ty) => { + /// Creates a [`RefundBuilder`] such that the [`Refund`] it builds is recognized by the + /// [`ChannelManager`] when handling [`Bolt12Invoice`] messages for the refund. + /// + /// # Payment + /// + /// The provided `payment_id` is used to ensure that only one invoice is paid for the refund. + /// See [Avoiding Duplicate Payments] for other requirements once the payment has been sent. + /// + /// The builder will have the provided expiration set. Any changes to the expiration on the + /// returned builder will not be honored by [`ChannelManager`]. For non-`std`, the highest seen + /// block time minus two hours is used for the current time when determining if the refund has + /// expired. + /// + /// To revoke the refund, use [`ChannelManager::abandon_payment`] prior to receiving the + /// invoice. If abandoned, or an invoice isn't received before expiration, the payment will fail + /// with an [`Event::PaymentFailed`]. + /// + /// If `max_total_routing_fee_msat` is not specified, The default from + /// [`RouteParameters::from_payment_params_and_value`] is applied. + /// + /// # Privacy + /// + /// Uses [`MessageRouter`] to construct a [`BlindedMessagePath`] for the refund based on the given + /// `absolute_expiry` according to [`MAX_SHORT_LIVED_RELATIVE_EXPIRY`]. See those docs for + /// privacy implications as well as those of the parameterized [`Router`], which implements + /// [`MessageRouter`]. + /// + /// Also, uses a derived payer id in the refund for payer privacy. + /// + /// # Limitations + /// + /// Requires a direct connection to an introduction node in the responding + /// [`Bolt12Invoice::payment_paths`]. + /// + /// # Errors + /// + /// Errors if: + /// - a duplicate `payment_id` is provided given the caveats in the aforementioned link, + /// - `amount_msats` is invalid, or + /// - the parameterized [`Router`] is unable to create a blinded path for the refund. + /// + /// [`Refund`]: crate::offers::refund::Refund + /// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice + /// [`Bolt12Invoice::payment_paths`]: crate::offers::invoice::Bolt12Invoice::payment_paths + /// [`Router`]: crate::routing::router::Router + /// [Avoiding Duplicate Payments]: #avoiding-duplicate-payments + /// [`ChannelManager`]: crate::ln::channelmanager::ChannelManager + /// [`ChannelManager::abandon_payment`]: crate::ln::channelmanager::ChannelManager::abandon_payment + /// [`Event::PaymentFailed`]: crate::events::Event::PaymentFailed + /// [`RouteParameters::from_payment_params_and_value`]: crate::routing::router::RouteParameters::from_payment_params_and_value + pub fn create_refund_builder( + &$self, amount_msats: u64, absolute_expiry: Duration, payment_id: PaymentId, + retry_strategy: Retry, max_total_routing_fee_msat: Option + ) -> Result<$builder, 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; + + let nonce = Nonce::from_entropy_source(entropy); + let context = OffersContext::OutboundPayment { payment_id, nonce, hmac: None }; + let path = $self.create_blinded_paths_using_absolute_expiry(context, Some(absolute_expiry)) + .and_then(|paths| paths.into_iter().next().ok_or(())) + .map_err(|_| Bolt12SemanticError::MissingPaths)?; + + let builder = RefundBuilder::deriving_signing_pubkey( + node_id, expanded_key, nonce, secp_ctx, amount_msats, payment_id + )? + .chain_hash($self.commons.get_chain_hash()) + .absolute_expiry(absolute_expiry) + .path(path); + + let expiration = StaleExpiration::AbsoluteTimeout(absolute_expiry); + $self.commons + .add_new_awaiting_invoice( + payment_id, expiration, retry_strategy, max_total_routing_fee_msat, None, + ) + .map_err(|_| Bolt12SemanticError::DuplicatePaymentId)?; + + Ok(builder.into()) + } +} } + + +impl OffersMessageFlow +where + ES::Target: EntropySource, + OMC::Target: OffersMessageCommons, + MR::Target: MessageRouter, + L::Target: Logger, +{ + #[cfg(not(c_bindings))] + create_offer_builder!(self, OfferBuilder); + #[cfg(not(c_bindings))] + create_refund_builder!(self, RefundBuilder); + + #[cfg(c_bindings)] + create_offer_builder!(self, OfferWithDerivedMetadataBuilder); + #[cfg(c_bindings)] + create_refund_builder!(self, RefundMaybeWithDerivedMetadataBuilder); + + /// Create an offer for receiving async payments as an often-offline recipient. + /// + /// Because we may be offline when the payer attempts to request an invoice, you MUST: + /// 1. Provide at least 1 [`BlindedMessagePath`] terminating at an always-online node that will + /// serve the [`StaticInvoice`] created from this offer on our behalf. + /// 2. Use [`Self::create_static_invoice_builder`] to create a [`StaticInvoice`] from this + /// [`Offer`] plus the returned [`Nonce`], and provide the static invoice to the + /// aforementioned always-online node. + #[cfg(async_payments)] + pub fn create_async_receive_offer_builder( + &self, message_paths_to_always_online_node: Vec + ) -> Result<(OfferBuilder, Nonce), Bolt12SemanticError> { + if message_paths_to_always_online_node.is_empty() { + return Err(Bolt12SemanticError::MissingPaths) + } + + 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; + + let nonce = Nonce::from_entropy_source(entropy); + let mut builder = OfferBuilder::deriving_signing_pubkey( + node_id, expanded_key, nonce, secp_ctx + ).chain_hash(self.commons.get_chain_hash()); + + for path in message_paths_to_always_online_node { + builder = builder.path(path); + } + + Ok((builder.into(), nonce)) + } + + /// Creates a [`StaticInvoiceBuilder`] from the corresponding [`Offer`] and [`Nonce`] that were + /// created via [`Self::create_async_receive_offer_builder`]. If `relative_expiry` is unset, the + /// invoice's expiry will default to [`STATIC_INVOICE_DEFAULT_RELATIVE_EXPIRY`]. + #[cfg(async_payments)] + pub fn create_static_invoice_builder<'a>( + &self, offer: &'a Offer, offer_nonce: Nonce, relative_expiry: Option + ) -> Result, Bolt12SemanticError> { + let expanded_key = &self.inbound_payment_key; + let entropy = &*self.entropy_source; + let secp_ctx = &self.secp_ctx; + + let payment_context = PaymentContext::AsyncBolt12Offer( + AsyncBolt12OfferContext { offer_nonce } + ); + let amount_msat = offer.amount().and_then(|amount| { + match amount { + Amount::Bitcoin { amount_msats } => Some(amount_msats), + Amount::Currency { .. } => None + } + }); + + let relative_expiry = relative_expiry.unwrap_or(STATIC_INVOICE_DEFAULT_RELATIVE_EXPIRY); + let relative_expiry_secs: u32 = relative_expiry.as_secs().try_into().unwrap_or(u32::MAX); + + let created_at = self.duration_since_epoch(); + let payment_secret = inbound_payment::create_for_spontaneous_payment( + &self.inbound_payment_key, amount_msat, relative_expiry_secs, created_at.as_secs(), None + ).map_err(|()| Bolt12SemanticError::InvalidAmount)?; + + let payment_paths = self.commons.create_blinded_payment_paths( + amount_msat, payment_secret, payment_context, relative_expiry_secs + ).map_err(|()| Bolt12SemanticError::MissingPaths)?; + + let nonce = Nonce::from_entropy_source(entropy); + let hmac = signer::hmac_for_held_htlc_available_context(nonce, expanded_key); + let path_absolute_expiry = Duration::from_secs( + inbound_payment::calculate_absolute_expiry(created_at.as_secs(), relative_expiry_secs) + ); + let context = MessageContext::AsyncPayments( + AsyncPaymentsContext::InboundPayment { nonce, hmac, path_absolute_expiry } + ); + let async_receive_message_paths = self.create_blinded_paths(context) + .map_err(|()| Bolt12SemanticError::MissingPaths)?; + + StaticInvoiceBuilder::for_offer_using_derived_keys( + offer, payment_paths, async_receive_message_paths, created_at, expanded_key, + offer_nonce, secp_ctx + ).map(|inv| inv.allow_mpp().relative_expiry(relative_expiry_secs)) + } +} + impl OffersMessageHandler for OffersMessageFlow where ES::Target: EntropySource, @@ -931,3 +1401,4 @@ where } } + diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index e9a2d9428e2..75db005cbb4 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -71,11 +71,11 @@ //! //! # Note //! -//! If constructing an [`Offer`] for use with a [`ChannelManager`], use -//! [`ChannelManager::create_offer_builder`] instead of [`OfferBuilder::new`]. +//! If constructing an [`Offer`] for use with a [`OffersMessageFlow`], use +//! [`OffersMessageFlow::create_offer_builder`] instead of [`OfferBuilder::new`]. //! -//! [`ChannelManager`]: crate::ln::channelmanager::ChannelManager -//! [`ChannelManager::create_offer_builder`]: crate::ln::channelmanager::ChannelManager::create_offer_builder +//! [`OffersMessageFlow`]: crate::offers::flow::OffersMessageFlow +//! [`OffersMessageFlow::create_offer_builder`]: crate::offers::flow::OffersMessageFlow::create_offer_builder use crate::blinded_path::message::BlindedMessagePath; use crate::io; @@ -227,11 +227,11 @@ macro_rules! offer_explicit_metadata_builder_methods { /// /// # Note /// - /// If constructing an [`Offer`] for use with a [`ChannelManager`], use - /// [`ChannelManager::create_offer_builder`] instead of [`OfferBuilder::new`]. + /// If constructing an [`Offer`] for use with a [`OffersMessageFlow`], use + /// [`OffersMessageFlow::create_offer_builder`] instead of [`OfferBuilder::new`]. /// - /// [`ChannelManager`]: crate::ln::channelmanager::ChannelManager - /// [`ChannelManager::create_offer_builder`]: crate::ln::channelmanager::ChannelManager::create_offer_builder + /// [`OffersMessageFlow`]: crate::offers::flow::OffersMessageFlow + /// [`OffersMessageFlow::create_offer_builder`]: crate::offers::flow::OffersMessageFlow::create_offer_builder pub fn new(signing_pubkey: PublicKey) -> Self { Self { offer: OfferContents { diff --git a/lightning/src/offers/refund.rs b/lightning/src/offers/refund.rs index 56f3cfa01cf..c5d8e707b9a 100644 --- a/lightning/src/offers/refund.rs +++ b/lightning/src/offers/refund.rs @@ -76,11 +76,11 @@ //! //! # Note //! -//! If constructing a [`Refund`] for use with a [`ChannelManager`], use -//! [`ChannelManager::create_refund_builder`] instead of [`RefundBuilder::new`]. +//! If constructing a [`Refund`] for use with a [`OffersMessageFlow`], use +//! [`OffersMessageFlow::create_refund_builder`] instead of [`RefundBuilder::new`]. //! -//! [`ChannelManager`]: crate::ln::channelmanager::ChannelManager -//! [`ChannelManager::create_refund_builder`]: crate::ln::channelmanager::ChannelManager::create_refund_builder +//! [`OffersMessageFlow`]: crate::offers::flow::OffersMessageFlow +//! [`OffersMessageFlow::create_refund_builder`]: crate::offers::flow::OffersMessageFlow::create_offer_builder use crate::blinded_path::message::BlindedMessagePath; use crate::blinded_path::payment::BlindedPaymentPath; @@ -162,11 +162,11 @@ macro_rules! refund_explicit_metadata_builder_methods { /// /// # Note /// - /// If constructing a [`Refund`] for use with a [`ChannelManager`], use - /// [`ChannelManager::create_refund_builder`] instead of [`RefundBuilder::new`]. + /// If constructing a [`Refund`] for use with a [`OffersMessageFlow`], use + /// [`OffersMessageFlow::create_refund_builder`] instead of [`RefundBuilder::new`]. /// - /// [`ChannelManager`]: crate::ln::channelmanager::ChannelManager - /// [`ChannelManager::create_refund_builder`]: crate::ln::channelmanager::ChannelManager::create_refund_builder + /// [`OffersMessageFlow`]: crate::offers::flow::OffersMessageFlow + /// [`OffersMessageFlow::create_refund_builder`]: crate::offers::flow::OffersMessageFlow::create_refund_builder pub fn new( metadata: Vec, signing_pubkey: PublicKey, amount_msats: u64, ) -> Result { From 7777c3b8f75d70fcae2a64cf3eb9f92b81abaeb5 Mon Sep 17 00:00:00 2001 From: shaavan Date: Thu, 13 Feb 2025 22:13:31 +0530 Subject: [PATCH 11/14] Move Offer Payers to flow.rs --- lightning/src/ln/async_payments_tests.rs | 18 +- lightning/src/ln/channelmanager.rs | 251 ++---------------- .../src/ln/max_payment_path_len_tests.rs | 2 +- lightning/src/ln/offers_tests.rs | 40 +-- lightning/src/offers/flow.rs | 241 +++++++++++++++-- 5 files changed, 273 insertions(+), 279 deletions(-) diff --git a/lightning/src/ln/async_payments_tests.rs b/lightning/src/ln/async_payments_tests.rs index cd76eed85ef..57b646568e5 100644 --- a/lightning/src/ln/async_payments_tests.rs +++ b/lightning/src/ln/async_payments_tests.rs @@ -237,7 +237,7 @@ fn static_invoice_unknown_required_features() { let amt_msat = 5000; let payment_id = PaymentId([1; 32]); nodes[0] - .node + .offers_handler .pay_for_offer(&offer, None, Some(amt_msat), None, payment_id, Retry::Attempts(0), None) .unwrap(); @@ -297,7 +297,7 @@ fn ignore_unexpected_static_invoice() { let amt_msat = 5000; let payment_id = PaymentId([1; 32]); nodes[0] - .node + .offers_handler .pay_for_offer(&offer, None, Some(amt_msat), None, payment_id, Retry::Attempts(0), None) .unwrap(); @@ -414,7 +414,7 @@ fn async_receive_flow_success() { let amt_msat = 5000; let payment_id = PaymentId([1; 32]); nodes[0] - .node + .offers_handler .pay_for_offer(&offer, None, Some(amt_msat), None, payment_id, Retry::Attempts(0), None) .unwrap(); let release_held_htlc_om = @@ -462,7 +462,7 @@ fn expired_static_invoice_fail() { let amt_msat = 5000; let payment_id = PaymentId([1; 32]); nodes[0] - .node + .offers_handler .pay_for_offer(&offer, None, Some(amt_msat), None, payment_id, Retry::Attempts(0), None) .unwrap(); @@ -545,7 +545,7 @@ fn async_receive_mpp() { let amt_msat = 15_000_000; let payment_id = PaymentId([1; 32]); nodes[0] - .node + .offers_handler .pay_for_offer(&offer, None, Some(amt_msat), None, payment_id, Retry::Attempts(1), None) .unwrap(); let release_held_htlc_om_3_0 = @@ -629,7 +629,7 @@ fn amount_doesnt_match_invreq() { let amt_msat = 5000; let payment_id = PaymentId([1; 32]); nodes[0] - .node + .offers_handler .pay_for_offer(&offer, None, Some(amt_msat), None, payment_id, Retry::Attempts(1), None) .unwrap(); let release_held_htlc_om_3_0 = @@ -858,7 +858,7 @@ fn invalid_async_receive_with_retry( *nodes[0].keys_manager.override_random_bytes.lock().unwrap() = Some(hardcoded_random_bytes); nodes[0] - .node + .offers_handler .pay_for_offer(&offer, None, Some(amt_msat), None, payment_id, Retry::Attempts(2), None) .unwrap(); let release_held_htlc_om_2_0 = @@ -947,7 +947,7 @@ fn expired_static_invoice_message_path() { let amt_msat = 5000; let payment_id = PaymentId([1; 32]); nodes[0] - .node + .offers_handler .pay_for_offer(&offer, None, Some(amt_msat), None, payment_id, Retry::Attempts(1), None) .unwrap(); @@ -1051,7 +1051,7 @@ fn expired_static_invoice_payment_path() { let amt_msat = 5000; let payment_id = PaymentId([1; 32]); nodes[0] - .node + .offers_handler .pay_for_offer(&offer, None, Some(amt_msat), None, payment_id, Retry::Attempts(0), None) .unwrap(); let release_held_htlc_om = diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 20c9d757e49..35c7f92a64b 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -48,6 +48,7 @@ use crate::events::{self, Event, EventHandler, EventsProvider, InboundChannelFun use crate::ln::inbound_payment; use crate::ln::types::ChannelId; use crate::offers::flow::OffersMessageCommons; +use crate::offers::invoice_request::InvoiceRequest; use crate::types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; use crate::ln::channel::{self, Channel, ChannelError, ChannelUpdateStatus, FundedChannel, ShutdownResult, UpdateFulfillCommitFetch, OutboundV1Channel, ReconnectionMsg, InboundV1Channel, WithChannelContext}; #[cfg(any(dual_funding, splicing))] @@ -68,13 +69,10 @@ use crate::ln::msgs::{ChannelMessageHandler, CommitmentUpdate, DecodeError, Ligh use crate::ln::outbound_payment; use crate::ln::outbound_payment::{OutboundPayments, PendingOutboundPayment, RetryableInvoiceRequest, SendAlongPathArgs, StaleExpiration}; use crate::offers::invoice::{Bolt12Invoice, DEFAULT_RELATIVE_EXPIRY, DerivedSigningPubkey, InvoiceBuilder, UnsignedBolt12Invoice}; -use crate::offers::invoice_request::{InvoiceRequest, InvoiceRequestBuilder}; use crate::offers::nonce::Nonce; -use crate::offers::offer::Offer; use crate::offers::parse::Bolt12SemanticError; use crate::offers::refund::Refund; use crate::offers::signer; -use crate::onion_message::dns_resolution::HumanReadableName; use crate::onion_message::messenger::{Destination, MessageRouter, MessageSendInstructions}; use crate::onion_message::offers::OffersMessage; use crate::sign::{EntropySource, NodeSigner, Recipient, SignerProvider}; @@ -248,6 +246,8 @@ pub enum PendingHTLCRouting { /// [`NodeSigner::get_inbound_payment_key`]. has_recipient_created_payment_secret: bool, /// The [`InvoiceRequest`] associated with the [`Offer`] corresponding to this payment. + /// + /// [`Offer`]: crate::offers::offer::Offer invoice_request: Option, /// The context of the payment included by the recipient in a blinded path, or `None` if a /// blinded path was not used. @@ -2070,62 +2070,13 @@ where /// ``` /// /// ## BOLT 12 Offers -/// -/// For more information on creating offers, see [`create_offer_builder`]. -/// -/// Use [`pay_for_offer`] to initiated payment, which sends an [`InvoiceRequest`] for an [`Offer`] -/// and pays the [`Bolt12Invoice`] response. /// -/// ``` -/// # use lightning::events::{Event, EventsProvider}; -/// # use lightning::ln::channelmanager::{AChannelManager, PaymentId, RecentPaymentDetails, Retry}; -/// # use lightning::offers::offer::Offer; -/// # -/// # fn example( -/// # channel_manager: T, offer: &Offer, quantity: Option, amount_msats: Option, -/// # payer_note: Option, retry: Retry, max_total_routing_fee_msat: Option -/// # ) { -/// # let channel_manager = channel_manager.get_cm(); -/// let payment_id = PaymentId([42; 32]); -/// match channel_manager.pay_for_offer( -/// offer, quantity, amount_msats, payer_note, payment_id, retry, max_total_routing_fee_msat -/// ) { -/// Ok(()) => println!("Requesting invoice for offer"), -/// Err(e) => println!("Unable to request invoice for offer: {:?}", e), -/// } -/// -/// // First the payment will be waiting on an invoice -/// let expected_payment_id = payment_id; -/// assert!( -/// channel_manager.list_recent_payments().iter().find(|details| matches!( -/// details, -/// RecentPaymentDetails::AwaitingInvoice { payment_id: expected_payment_id } -/// )).is_some() -/// ); -/// -/// // Once the invoice is received, a payment will be sent -/// assert!( -/// channel_manager.list_recent_payments().iter().find(|details| matches!( -/// details, -/// RecentPaymentDetails::Pending { payment_id: expected_payment_id, .. } -/// )).is_some() -/// ); +/// For more information on creating offers, see [`create_offer_builder`]. /// -/// // On the event processing thread -/// channel_manager.process_pending_events(&|event| { -/// match event { -/// Event::PaymentSent { payment_id: Some(payment_id), .. } => println!("Paid {}", payment_id), -/// Event::PaymentFailed { payment_id, .. } => println!("Failed paying {}", payment_id), -/// // ... -/// # _ => {}, -/// } -/// Ok(()) -/// }); -/// # } -/// ``` +/// For details on initiating payments for offers, see [`pay_for_offer`]. /// /// ## BOLT 12 Refunds -/// +/// /// For more information on creating refunds, see [`create_refund_builder`]. /// /// Use [`request_refund_payment`] to send a [`Bolt12Invoice`] for receiving the refund. Similar to @@ -2252,8 +2203,8 @@ where /// [`claim_funds`]: Self::claim_funds /// [`send_payment`]: Self::send_payment /// [`offers`]: crate::offers +/// [`Offer`]: crate::offers::offer::Offer /// [`create_offer_builder`]: crate::offers::flow::OffersMessageFlow::create_offer_builder -/// [`pay_for_offer`]: Self::pay_for_offer /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest /// [`create_refund_builder`]: crate::offers::flow::OffersMessageFlow::create_refund_builder /// [`request_refund_payment`]: Self::request_refund_payment @@ -2264,6 +2215,7 @@ where /// [`update_channel`]: chain::Watch::update_channel /// [`ChannelUpdate`]: msgs::ChannelUpdate /// [`read`]: ReadableArgs::read +/// [`pay_for_offer`]: crate::offers::flow::OffersMessageFlow::pay_for_offer // // Lock order: // The tree structure below illustrates the lock order requirements for the different locks of the @@ -2754,8 +2706,10 @@ const MAX_NO_CHANNEL_PEERS: usize = 250; pub enum RecentPaymentDetails { /// When an invoice was requested and thus a payment has not yet been sent. AwaitingInvoice { - /// A user-provided identifier in [`ChannelManager::pay_for_offer`] used to uniquely identify a + /// A user-provided identifier in [`OffersMessageFlow::pay_for_offer`] used to uniquely identify a /// payment and ensure idempotency in LDK. + /// + /// [`OffersMessageFlow::pay_for_offer`]: crate::offers::flow::OffersMessageFlow::pay_for_offer payment_id: PaymentId, }, /// When a payment is still being sent and awaiting successful delivery. @@ -2764,7 +2718,7 @@ pub enum RecentPaymentDetails { /// identify a payment and ensure idempotency in LDK. /// /// [`send_payment`]: crate::ln::channelmanager::ChannelManager::send_payment - /// [`pay_for_offer`]: crate::ln::channelmanager::ChannelManager::pay_for_offer + /// [`pay_for_offer`]: crate::offers::flow::OffersMessageFlow::pay_for_offer payment_id: PaymentId, /// Hash of the payment that is currently being sent but has yet to be fulfilled or /// abandoned. @@ -2781,7 +2735,7 @@ pub enum RecentPaymentDetails { /// identify a payment and ensure idempotency in LDK. /// /// [`send_payment`]: crate::ln::channelmanager::ChannelManager::send_payment - /// [`pay_for_offer`]: crate::ln::channelmanager::ChannelManager::pay_for_offer + /// [`pay_for_offer`]: crate::offers::flow::OffersMessageFlow::pay_for_offer payment_id: PaymentId, /// Hash of the payment that was claimed. `None` for serializations of [`ChannelManager`] /// made before LDK version 0.0.104. @@ -2795,7 +2749,7 @@ pub enum RecentPaymentDetails { /// identify a payment and ensure idempotency in LDK. /// /// [`send_payment`]: crate::ln::channelmanager::ChannelManager::send_payment - /// [`pay_for_offer`]: crate::ln::channelmanager::ChannelManager::pay_for_offer + /// [`pay_for_offer`]: crate::offers::flow::OffersMessageFlow::pay_for_offer payment_id: PaymentId, /// Hash of the payment that we have given up trying to send. payment_hash: PaymentHash, @@ -4693,7 +4647,7 @@ where /// /// # Requested Invoices /// - /// In the case of paying a [`Bolt12Invoice`] via [`ChannelManager::pay_for_offer`], abandoning + /// In the case of paying a [`Bolt12Invoice`] via [`OffersMessageFlow::pay_for_offer`], abandoning /// the payment prior to receiving the invoice will result in an [`Event::PaymentFailed`] and /// prevent any attempts at paying it once received. /// @@ -4702,6 +4656,7 @@ where /// If an [`Event::PaymentFailed`] is generated and we restart without first persisting the /// [`ChannelManager`], another [`Event::PaymentFailed`] may be generated. /// + /// [`OffersMessageFlow::pay_for_offer`]: crate::offers::flow::OffersMessageFlow::pay_for_offer /// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice pub fn abandon_payment(&self, payment_id: PaymentId) { self.abandon_payment_with_reason(payment_id, PaymentFailureReason::UserAbandoned) @@ -9668,7 +9623,7 @@ where } fn add_new_awaiting_invoice( - &self, payment_id: PaymentId, expiration: StaleExpiration, retry_strategy: Retry, + &self, payment_id: PaymentId, expiration: StaleExpiration, retry_strategy: Retry, max_total_routing_fee_msat: Option, retryable_invoice_request: Option ) -> Result<(), ()> { self.pending_outbound_payments.add_new_awaiting_invoice ( @@ -9729,18 +9684,6 @@ where fn get_pending_offers_messages(&self) -> MutexGuard<'_, Vec<(OffersMessage, MessageSendInstructions)>> { self.pending_offers_messages.lock().expect("Mutex is locked by other thread.") } - - fn enqueue_invoice_request(&self, invoice_request: InvoiceRequest, reply_paths: Vec) -> Result<(), Bolt12SemanticError> { - self.enqueue_invoice_request(invoice_request, reply_paths) - } - - fn pay_for_offer_intern Result<(), Bolt12SemanticError>>( - &self, offer: &Offer, quantity: Option, amount_msats: Option, - payer_note: Option, payment_id: PaymentId, - human_readable_name: Option, create_pending_payment: CPP, - ) -> Result<(), Bolt12SemanticError> { - self.pay_for_offer_intern(offer, quantity, amount_msats, payer_note, payment_id, human_readable_name, create_pending_payment) - } } /// Defines the maximum number of [`OffersMessage`] including different reply paths to be sent @@ -9762,164 +9705,6 @@ where MR::Target: MessageRouter, L::Target: Logger, { - /// Pays for an [`Offer`] using the given parameters by creating an [`InvoiceRequest`] and - /// enqueuing it to be sent via an onion message. [`ChannelManager`] will pay the actual - /// [`Bolt12Invoice`] once it is received. - /// - /// Uses [`InvoiceRequestBuilder`] such that the [`InvoiceRequest`] it builds is recognized by - /// the [`ChannelManager`] when handling a [`Bolt12Invoice`] message in response to the request. - /// The optional parameters are used in the builder, if `Some`: - /// - `quantity` for [`InvoiceRequest::quantity`] which must be set if - /// [`Offer::expects_quantity`] is `true`. - /// - `amount_msats` if overpaying what is required for the given `quantity` is desired, and - /// - `payer_note` for [`InvoiceRequest::payer_note`]. - /// - /// If `max_total_routing_fee_msat` is not specified, The default from - /// [`RouteParameters::from_payment_params_and_value`] is applied. - /// - /// # Payment - /// - /// The provided `payment_id` is used to ensure that only one invoice is paid for the request - /// when received. See [Avoiding Duplicate Payments] for other requirements once the payment has - /// been sent. - /// - /// To revoke the request, use [`ChannelManager::abandon_payment`] prior to receiving the - /// invoice. If abandoned, or an invoice isn't received in a reasonable amount of time, the - /// payment will fail with an [`Event::PaymentFailed`]. - /// - /// # Privacy - /// - /// For payer privacy, uses a derived payer id and uses [`MessageRouter::create_blinded_paths`] - /// to construct a [`BlindedMessagePath`] for the reply path. For further privacy implications, see the - /// docs of the parameterized [`Router`], which implements [`MessageRouter`]. - /// - /// # Limitations - /// - /// Requires a direct connection to an introduction node in [`Offer::paths`] or to - /// [`Offer::issuer_signing_pubkey`], if empty. A similar restriction applies to the responding - /// [`Bolt12Invoice::payment_paths`]. - /// - /// # Errors - /// - /// Errors if: - /// - a duplicate `payment_id` is provided given the caveats in the aforementioned link, - /// - the provided parameters are invalid for the offer, - /// - the offer is for an unsupported chain, or - /// - the parameterized [`Router`] is unable to create a blinded reply path for the invoice - /// request. - /// - /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest - /// [`InvoiceRequest::quantity`]: crate::offers::invoice_request::InvoiceRequest::quantity - /// [`InvoiceRequest::payer_note`]: crate::offers::invoice_request::InvoiceRequest::payer_note - /// [`InvoiceRequestBuilder`]: crate::offers::invoice_request::InvoiceRequestBuilder - /// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice - /// [`Bolt12Invoice::payment_paths`]: crate::offers::invoice::Bolt12Invoice::payment_paths - /// [Avoiding Duplicate Payments]: #avoiding-duplicate-payments - pub fn pay_for_offer( - &self, offer: &Offer, quantity: Option, amount_msats: Option, - payer_note: Option, payment_id: PaymentId, retry_strategy: Retry, - max_total_routing_fee_msat: Option - ) -> Result<(), Bolt12SemanticError> { - self.pay_for_offer_intern(offer, quantity, amount_msats, payer_note, payment_id, None, |invoice_request, nonce| { - let expiration = StaleExpiration::TimerTicks(1); - let retryable_invoice_request = RetryableInvoiceRequest { - invoice_request: invoice_request.clone(), - nonce, - needs_retry: true, - }; - self.pending_outbound_payments - .add_new_awaiting_invoice( - payment_id, expiration, retry_strategy, max_total_routing_fee_msat, - Some(retryable_invoice_request) - ) - .map_err(|_| Bolt12SemanticError::DuplicatePaymentId) - }) - } - - fn pay_for_offer_intern Result<(), Bolt12SemanticError>>( - &self, offer: &Offer, quantity: Option, amount_msats: Option, - payer_note: Option, payment_id: PaymentId, - human_readable_name: Option, create_pending_payment: CPP, - ) -> Result<(), Bolt12SemanticError> { - let expanded_key = &self.inbound_payment_key; - let entropy = &*self.entropy_source; - let secp_ctx = &self.secp_ctx; - - let nonce = Nonce::from_entropy_source(entropy); - let builder: InvoiceRequestBuilder = offer - .request_invoice(expanded_key, nonce, secp_ctx, payment_id)? - .into(); - let builder = builder.chain_hash(self.chain_hash)?; - - let builder = match quantity { - None => builder, - Some(quantity) => builder.quantity(quantity)?, - }; - let builder = match amount_msats { - None => builder, - Some(amount_msats) => builder.amount_msats(amount_msats)?, - }; - let builder = match payer_note { - None => builder, - Some(payer_note) => builder.payer_note(payer_note), - }; - let builder = match human_readable_name { - None => builder, - Some(hrn) => builder.sourced_from_human_readable_name(hrn), - }; - let invoice_request = builder.build_and_sign()?; - - let hmac = payment_id.hmac_for_offer_payment(nonce, expanded_key); - let context = MessageContext::Offers( - OffersContext::OutboundPayment { payment_id, nonce, hmac: Some(hmac) } - ); - let reply_paths = self.create_blinded_paths(context) - .map_err(|_| Bolt12SemanticError::MissingPaths)?; - - // Temporarily removing this to find the best way to integrate the guard - // let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self); - - create_pending_payment(&invoice_request, nonce)?; - - self.enqueue_invoice_request(invoice_request, reply_paths) - } - - fn enqueue_invoice_request( - &self, - invoice_request: InvoiceRequest, - reply_paths: Vec, - ) -> Result<(), Bolt12SemanticError> { - let mut pending_offers_messages = self.pending_offers_messages.lock().unwrap(); - if !invoice_request.paths().is_empty() { - reply_paths - .iter() - .flat_map(|reply_path| invoice_request.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::InvoiceRequest(invoice_request.clone()); - pending_offers_messages.push((message, instructions)); - }); - } else if let Some(node_id) = invoice_request.issuer_signing_pubkey() { - for reply_path in reply_paths { - let instructions = MessageSendInstructions::WithSpecifiedReplyPath { - destination: Destination::Node(node_id), - reply_path, - }; - let message = OffersMessage::InvoiceRequest(invoice_request.clone()); - pending_offers_messages.push((message, instructions)); - } - } else { - debug_assert!(false); - return Err(Bolt12SemanticError::MissingIssuerSigningPubkey); - } - - Ok(()) - } - /// Creates a [`Bolt12Invoice`] for a [`Refund`] and enqueues it to be sent via an onion /// message. /// @@ -12443,6 +12228,8 @@ where pub router: R, /// The [`MessageRouter`] used for constructing [`BlindedMessagePath`]s for [`Offer`]s, /// [`Refund`]s, and any reply paths. + /// + /// [`Offer`]: crate::offers::offer::Offer 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/max_payment_path_len_tests.rs b/lightning/src/ln/max_payment_path_len_tests.rs index d9eaa270d6f..0f359d1b037 100644 --- a/lightning/src/ln/max_payment_path_len_tests.rs +++ b/lightning/src/ln/max_payment_path_len_tests.rs @@ -393,7 +393,7 @@ fn bolt12_invoice_too_large_blinded_paths() { let offer = nodes[1].offers_handler.create_offer_builder(None).unwrap().build().unwrap(); let payment_id = PaymentId([1; 32]); - nodes[0].node.pay_for_offer(&offer, None, Some(5000), None, payment_id, Retry::Attempts(0), None).unwrap(); + nodes[0].offers_handler.pay_for_offer(&offer, None, Some(5000), None, payment_id, Retry::Attempts(0), None).unwrap(); let invreq_om = nodes[0].onion_messenger.next_onion_message_for_peer(nodes[1].node.get_our_node_id()).unwrap(); nodes[1].onion_messenger.handle_onion_message(nodes[0].node.get_our_node_id(), &invreq_om); diff --git a/lightning/src/ln/offers_tests.rs b/lightning/src/ln/offers_tests.rs index c4a93c9975d..373784c27f1 100644 --- a/lightning/src/ln/offers_tests.rs +++ b/lightning/src/ln/offers_tests.rs @@ -544,7 +544,7 @@ fn creates_and_pays_for_offer_using_two_hop_blinded_path() { } let payment_id = PaymentId([1; 32]); - david.node.pay_for_offer(&offer, None, None, None, payment_id, Retry::Attempts(0), None) + david.offers_handler.pay_for_offer(&offer, None, None, None, payment_id, Retry::Attempts(0), None) .unwrap(); expect_recent_payment!(david, RecentPaymentDetails::AwaitingInvoice, payment_id); @@ -713,7 +713,7 @@ fn creates_and_pays_for_offer_using_one_hop_blinded_path() { } let payment_id = PaymentId([1; 32]); - bob.node.pay_for_offer(&offer, None, None, None, payment_id, Retry::Attempts(0), None).unwrap(); + bob.offers_handler.pay_for_offer(&offer, None, None, None, payment_id, Retry::Attempts(0), None).unwrap(); expect_recent_payment!(bob, RecentPaymentDetails::AwaitingInvoice, payment_id); let onion_message = bob.onion_messenger.next_onion_message_for_peer(alice_id).unwrap(); @@ -834,7 +834,7 @@ fn pays_for_offer_without_blinded_paths() { assert!(offer.paths().is_empty()); let payment_id = PaymentId([1; 32]); - bob.node.pay_for_offer(&offer, None, None, None, payment_id, Retry::Attempts(0), None).unwrap(); + bob.offers_handler.pay_for_offer(&offer, None, None, None, payment_id, Retry::Attempts(0), None).unwrap(); expect_recent_payment!(bob, RecentPaymentDetails::AwaitingInvoice, payment_id); let onion_message = bob.onion_messenger.next_onion_message_for_peer(alice_id).unwrap(); @@ -961,7 +961,7 @@ fn send_invoice_requests_with_distinct_reply_path() { } let payment_id = PaymentId([1; 32]); - david.node.pay_for_offer(&offer, None, None, None, payment_id, Retry::Attempts(0), None) + david.offers_handler.pay_for_offer(&offer, None, None, None, payment_id, Retry::Attempts(0), None) .unwrap(); expect_recent_payment!(david, RecentPaymentDetails::AwaitingInvoice, payment_id); connect_peers(david, bob); @@ -1095,7 +1095,7 @@ fn creates_and_pays_for_offer_with_retry() { assert_eq!(path.introduction_node(), &IntroductionNode::NodeId(alice_id)); } let payment_id = PaymentId([1; 32]); - bob.node.pay_for_offer(&offer, None, None, None, payment_id, Retry::Attempts(0), None).unwrap(); + bob.offers_handler.pay_for_offer(&offer, None, None, None, payment_id, Retry::Attempts(0), None).unwrap(); expect_recent_payment!(bob, RecentPaymentDetails::AwaitingInvoice, payment_id); let _lost_onion_message = bob.onion_messenger.next_onion_message_for_peer(alice_id).unwrap(); @@ -1181,7 +1181,7 @@ fn creates_offer_with_blinded_path_using_unannounced_introduction_node() { } let payment_id = PaymentId([1; 32]); - bob.node.pay_for_offer(&offer, None, None, None, payment_id, Retry::Attempts(0), None).unwrap(); + bob.offers_handler.pay_for_offer(&offer, None, None, None, payment_id, Retry::Attempts(0), None).unwrap(); expect_recent_payment!(bob, RecentPaymentDetails::AwaitingInvoice, payment_id); let onion_message = bob.onion_messenger.next_onion_message_for_peer(alice_id).unwrap(); @@ -1322,7 +1322,7 @@ fn fails_authentication_when_handling_invoice_request() { // Send the invoice request directly to Alice instead of using a blinded path. let payment_id = PaymentId([1; 32]); - david.node.pay_for_offer(&offer, None, None, None, payment_id, Retry::Attempts(0), None) + david.offers_handler.pay_for_offer(&offer, None, None, None, payment_id, Retry::Attempts(0), None) .unwrap(); expect_recent_payment!(david, RecentPaymentDetails::AwaitingInvoice, payment_id); @@ -1348,7 +1348,7 @@ fn fails_authentication_when_handling_invoice_request() { // Send the invoice request to Alice using an invalid blinded path. let payment_id = PaymentId([2; 32]); - david.node.pay_for_offer(&offer, None, None, None, payment_id, Retry::Attempts(0), None) + david.offers_handler.pay_for_offer(&offer, None, None, None, payment_id, Retry::Attempts(0), None) .unwrap(); expect_recent_payment!(david, RecentPaymentDetails::AwaitingInvoice, payment_id); @@ -1425,7 +1425,7 @@ fn fails_authentication_when_handling_invoice_for_offer() { // Initiate an invoice request, but abandon tracking it. let payment_id = PaymentId([1; 32]); - david.node.pay_for_offer(&offer, None, None, None, payment_id, Retry::Attempts(0), None) + david.offers_handler.pay_for_offer(&offer, None, None, None, payment_id, Retry::Attempts(0), None) .unwrap(); david.node.abandon_payment(payment_id); get_event!(david, Event::PaymentFailed); @@ -1442,7 +1442,7 @@ fn fails_authentication_when_handling_invoice_for_offer() { }; let payment_id = PaymentId([2; 32]); - david.node.pay_for_offer(&offer, None, None, None, payment_id, Retry::Attempts(0), None) + david.offers_handler.pay_for_offer(&offer, None, None, None, payment_id, Retry::Attempts(0), None) .unwrap(); expect_recent_payment!(david, RecentPaymentDetails::AwaitingInvoice, payment_id); @@ -1626,7 +1626,7 @@ fn fails_creating_or_paying_for_offer_without_connected_peers() { let payment_id = PaymentId([1; 32]); - match david.node.pay_for_offer(&offer, None, None, None, payment_id, Retry::Attempts(0), None) { + match david.offers_handler.pay_for_offer(&offer, None, None, None, payment_id, Retry::Attempts(0), None) { Ok(_) => panic!("Expected error"), Err(e) => assert_eq!(e, Bolt12SemanticError::MissingPaths), } @@ -1638,7 +1638,7 @@ fn fails_creating_or_paying_for_offer_without_connected_peers() { reconnect_nodes(args); assert!( - david.node.pay_for_offer( + david.offers_handler.pay_for_offer( &offer, None, None, None, payment_id, Retry::Attempts(0), None ).is_ok() ); @@ -1730,7 +1730,7 @@ fn fails_creating_invoice_request_for_unsupported_chain() { .build().unwrap(); let payment_id = PaymentId([1; 32]); - match bob.node.pay_for_offer(&offer, None, None, None, payment_id, Retry::Attempts(0), None) { + match bob.offers_handler.pay_for_offer(&offer, None, None, None, payment_id, Retry::Attempts(0), None) { Ok(_) => panic!("Expected error"), Err(e) => assert_eq!(e, Bolt12SemanticError::UnsupportedChain), } @@ -1789,7 +1789,7 @@ fn fails_creating_invoice_request_without_blinded_reply_path() { let payment_id = PaymentId([1; 32]); - match david.node.pay_for_offer(&offer, None, None, None, payment_id, Retry::Attempts(0), None) { + match david.offers_handler.pay_for_offer(&offer, None, None, None, payment_id, Retry::Attempts(0), None) { Ok(_) => panic!("Expected error"), Err(e) => assert_eq!(e, Bolt12SemanticError::MissingPaths), } @@ -1823,13 +1823,13 @@ fn fails_creating_invoice_request_with_duplicate_payment_id() { let payment_id = PaymentId([1; 32]); assert!( - david.node.pay_for_offer( + david.offers_handler.pay_for_offer( &offer, None, None, None, payment_id, Retry::Attempts(0), None ).is_ok() ); expect_recent_payment!(david, RecentPaymentDetails::AwaitingInvoice, payment_id); - match david.node.pay_for_offer(&offer, None, None, None, payment_id, Retry::Attempts(0), None) { + match david.offers_handler.pay_for_offer(&offer, None, None, None, payment_id, Retry::Attempts(0), None) { Ok(_) => panic!("Expected error"), Err(e) => assert_eq!(e, Bolt12SemanticError::DuplicatePaymentId), } @@ -1908,7 +1908,7 @@ fn fails_sending_invoice_without_blinded_payment_paths_for_offer() { .build().unwrap(); let payment_id = PaymentId([1; 32]); - david.node.pay_for_offer(&offer, None, None, None, payment_id, Retry::Attempts(0), None) + david.offers_handler.pay_for_offer(&offer, None, None, None, payment_id, Retry::Attempts(0), None) .unwrap(); connect_peers(david, bob); @@ -2117,7 +2117,7 @@ fn fails_paying_invoice_with_unknown_required_features() { .build().unwrap(); let payment_id = PaymentId([1; 32]); - david.node.pay_for_offer(&offer, None, None, None, payment_id, Retry::Attempts(0), None) + david.offers_handler.pay_for_offer(&offer, None, None, None, payment_id, Retry::Attempts(0), None) .unwrap(); connect_peers(david, bob); @@ -2194,7 +2194,7 @@ fn rejects_keysend_to_non_static_invoice_path() { let offer = nodes[1].offers_handler.create_offer_builder(None).unwrap().build().unwrap(); let amt_msat = 5000; let payment_id = PaymentId([1; 32]); - nodes[0].node.pay_for_offer(&offer, None, Some(amt_msat), None, payment_id, Retry::Attempts(1), None).unwrap(); + nodes[0].offers_handler.pay_for_offer(&offer, None, Some(amt_msat), None, payment_id, Retry::Attempts(1), None).unwrap(); let invreq_om = nodes[0].onion_messenger.next_onion_message_for_peer(nodes[1].node.get_our_node_id()).unwrap(); nodes[1].onion_messenger.handle_onion_message(nodes[0].node.get_our_node_id(), &invreq_om); let invoice_om = nodes[1].onion_messenger.next_onion_message_for_peer(nodes[0].node.get_our_node_id()).unwrap(); @@ -2279,7 +2279,7 @@ fn no_double_pay_with_stale_channelmanager() { assert!(offer.paths().is_empty()); let payment_id = PaymentId([1; 32]); - nodes[0].node.pay_for_offer(&offer, None, None, None, payment_id, Retry::Attempts(0), None).unwrap(); + nodes[0].offers_handler.pay_for_offer(&offer, None, None, None, payment_id, Retry::Attempts(0), None).unwrap(); expect_recent_payment!(nodes[0], RecentPaymentDetails::AwaitingInvoice, payment_id); let invreq_om = nodes[0].onion_messenger.next_onion_message_for_peer(bob_id).unwrap(); diff --git a/lightning/src/offers/flow.rs b/lightning/src/offers/flow.rs index bb94e64d491..eb7eabc77d7 100644 --- a/lightning/src/offers/flow.rs +++ b/lightning/src/offers/flow.rs @@ -32,14 +32,14 @@ use types::payment::PaymentHash; use crate::blinded_path::message::{AsyncPaymentsContext, BlindedMessagePath, MessageContext, MessageForwardNode, OffersContext}; use crate::blinded_path::payment::{BlindedPaymentPath, Bolt12OfferContext, PaymentContext}; use crate::events::PaymentFailureReason; -use crate::ln::channelmanager::{Bolt12PaymentError, PaymentId, Verification}; +use crate::ln::channelmanager::{Bolt12PaymentError, OFFERS_MESSAGE_REQUEST_LIMIT, PaymentId, Verification}; use crate::ln::outbound_payment::{Retry, RetryableInvoiceRequest, StaleExpiration}; use crate::offers::invoice::{Bolt12Invoice, DerivedSigningPubkey, ExplicitSigningPubkey, InvoiceBuilder, UnsignedBolt12Invoice, DEFAULT_RELATIVE_EXPIRY}; -use crate::offers::invoice_request::InvoiceRequest; +use crate::offers::invoice_request::{InvoiceRequest, InvoiceRequestBuilder}; use crate::offers::offer::OfferBuilder; use crate::offers::parse::Bolt12SemanticError; use crate::offers::refund::RefundBuilder; -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::sync::{Mutex, MutexGuard}; use crate::onion_message::messenger::MessageRouter; @@ -74,9 +74,7 @@ use { #[cfg(feature= "dnssec")] use { crate::blinded_path::message::DNSResolverContext, - crate::ln::channelmanager::OFFERS_MESSAGE_REQUEST_LIMIT, crate::onion_message::dns_resolution::{DNSResolverMessage, DNSResolverMessageHandler, DNSSECProof, DNSSECQuery, OMNameResolver}, - crate::onion_message::messenger::Destination, }; /// Functions commonly shared in usage between [`ChannelManager`] & `OffersMessageFlow` @@ -207,16 +205,6 @@ pub trait OffersMessageCommons { /// Get pending offers messages fn get_pending_offers_messages(&self) -> MutexGuard<'_, Vec<(OffersMessage, MessageSendInstructions)>>; - - /// Enqueue invoice request - fn enqueue_invoice_request(&self, invoice_request: InvoiceRequest, reply_paths: Vec) -> Result<(), Bolt12SemanticError>; - - /// Internal pay for offer function - fn pay_for_offer_intern Result<(), Bolt12SemanticError>>( - &self, offer: &Offer, quantity: Option, amount_msats: Option, - payer_note: Option, payment_id: PaymentId, - human_readable_name: Option, create_pending_payment: CPP, - ) -> Result<(), Bolt12SemanticError>; } /// [`SimpleArcOffersMessageFlow`] is useful when you need a [`OffersMessageFlow`] with a static lifetime, e.g. @@ -420,6 +408,59 @@ where /// # } /// ``` /// +/// Use [`pay_for_offer`] to initiated payment, which sends an [`InvoiceRequest`] for an [`Offer`] +/// and pays the [`Bolt12Invoice`] response. +/// +/// ``` +/// # use lightning::events::{Event, EventsProvider}; +/// # use lightning::ln::channelmanager::{AChannelManager, PaymentId, RecentPaymentDetails, Retry}; +/// # use lightning::offers::flow::{AnOffersMessageFlow, OffersMessageCommons}; +/// # use lightning::offers::offer::Offer; +/// # +/// # fn example( +/// # offers_flow: T, channel_manager: U, offer: &Offer, quantity: Option, amount_msats: Option, +/// # payer_note: Option, retry: Retry, max_total_routing_fee_msat: Option +/// # ) { +/// # let offers_flow = offers_flow.get_omf(); +/// # let channel_manager = channel_manager.get_cm(); +/// let payment_id = PaymentId([42; 32]); +/// match offers_flow.pay_for_offer( +/// offer, quantity, amount_msats, payer_note, payment_id, retry, max_total_routing_fee_msat +/// ) { +/// Ok(()) => println!("Requesting invoice for offer"), +/// Err(e) => println!("Unable to request invoice for offer: {:?}", e), +/// } +/// +/// // First the payment will be waiting on an invoice +/// let expected_payment_id = payment_id; +/// assert!( +/// channel_manager.list_recent_payments().iter().find(|details| matches!( +/// details, +/// RecentPaymentDetails::AwaitingInvoice { payment_id: expected_payment_id } +/// )).is_some() +/// ); +/// +/// // Once the invoice is received, a payment will be sent +/// assert!( +/// channel_manager.list_recent_payments().iter().find(|details| matches!( +/// details, +/// RecentPaymentDetails::Pending { payment_id: expected_payment_id, .. } +/// )).is_some() +/// ); +/// +/// // On the event processing thread +/// channel_manager.process_pending_events(&|event| { +/// match event { +/// Event::PaymentSent { payment_id: Some(payment_id), .. } => println!("Paid {}", payment_id), +/// Event::PaymentFailed { payment_id, .. } => println!("Failed paying {}", payment_id), +/// // ... +/// # _ => {}, +/// } +/// Ok(()) +/// }); +/// # } +/// ``` +/// /// ## Bolt 12 Refund /// A [`Refund`] is a request for an invoice to be paid. Like *paying* for an [`Offer`], *creating* /// a [`Refund`] involves maintaining state since it represents a future outbound payment. @@ -490,6 +531,7 @@ where /// [`DNSResolverMessage`]: crate::onion_message::dns_resolution::DNSResolverMessage /// [`create_offer_builder`]: Self::create_offer_builder /// [`create_refund_builder`]: Self::create_refund_builder +/// [`pay_for_offer`]: crate::offers::flow::OffersMessageFlow::pay_for_offer pub struct OffersMessageFlow where ES::Target: EntropySource, @@ -908,6 +950,8 @@ macro_rules! create_refund_builder { ($self: ident, $builder: ty) => { /// [`ChannelManager::abandon_payment`]: crate::ln::channelmanager::ChannelManager::abandon_payment /// [`Event::PaymentFailed`]: crate::events::Event::PaymentFailed /// [`RouteParameters::from_payment_params_and_value`]: crate::routing::router::RouteParameters::from_payment_params_and_value + /// [`Event::PaymentFailed`]: crate::events::Event::PaymentFailed + /// [`Router`]: crate::routing::router::Router pub fn create_refund_builder( &$self, amount_msats: u64, absolute_expiry: Duration, payment_id: PaymentId, retry_strategy: Retry, max_total_routing_fee_msat: Option @@ -1041,6 +1085,169 @@ where offer_nonce, secp_ctx ).map(|inv| inv.allow_mpp().relative_expiry(relative_expiry_secs)) } + + /// Pays for an [`Offer`] using the given parameters by creating an [`InvoiceRequest`] and + /// enqueuing it to be sent via an onion message. [`ChannelManager`] will pay the actual + /// [`Bolt12Invoice`] once it is received. + /// + /// Uses [`InvoiceRequestBuilder`] such that the [`InvoiceRequest`] it builds is recognized by + /// the [`ChannelManager`] when handling a [`Bolt12Invoice`] message in response to the request. + /// The optional parameters are used in the builder, if `Some`: + /// - `quantity` for [`InvoiceRequest::quantity`] which must be set if + /// [`Offer::expects_quantity`] is `true`. + /// - `amount_msats` if overpaying what is required for the given `quantity` is desired, and + /// - `payer_note` for [`InvoiceRequest::payer_note`]. + /// + /// If `max_total_routing_fee_msat` is not specified, The default from + /// [`RouteParameters::from_payment_params_and_value`] is applied. + /// + /// # Payment + /// + /// The provided `payment_id` is used to ensure that only one invoice is paid for the request + /// when received. See [Avoiding Duplicate Payments] for other requirements once the payment has + /// been sent. + /// + /// To revoke the request, use [`ChannelManager::abandon_payment`] prior to receiving the + /// invoice. If abandoned, or an invoice isn't received in a reasonable amount of time, the + /// payment will fail with an [`Event::PaymentFailed`]. + /// + /// # Privacy + /// + /// For payer privacy, uses a derived payer id and uses [`MessageRouter::create_blinded_paths`] + /// to construct a [`BlindedMessagePath`] for the reply path. For further privacy implications, see the + /// docs of the parameterized [`Router`], which implements [`MessageRouter`]. + /// + /// # Limitations + /// + /// Requires a direct connection to an introduction node in [`Offer::paths`] or to + /// [`Offer::issuer_signing_pubkey`], if empty. A similar restriction applies to the responding + /// [`Bolt12Invoice::payment_paths`]. + /// + /// # Errors + /// + /// Errors if: + /// - a duplicate `payment_id` is provided given the caveats in the aforementioned link, + /// - the provided parameters are invalid for the offer, + /// - the offer is for an unsupported chain, or + /// - the parameterized [`Router`] is unable to create a blinded reply path for the invoice + /// request. + /// + /// [`ChannelManager`]: crate::ln::channelmanager::ChannelManager + /// [`ChannelManager::abandon_payment`]: crate::ln::channelmanager::ChannelManager::abandon_payment + /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest + /// [`InvoiceRequest::quantity`]: crate::offers::invoice_request::InvoiceRequest::quantity + /// [`InvoiceRequest::payer_note`]: crate::offers::invoice_request::InvoiceRequest::payer_note + /// [`InvoiceRequestBuilder`]: crate::offers::invoice_request::InvoiceRequestBuilder + /// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice + /// [`Bolt12Invoice::payment_paths`]: crate::offers::invoice::Bolt12Invoice::payment_paths + /// [Avoiding Duplicate Payments]: #avoiding-duplicate-payments + /// [`RouteParameters::from_payment_params_and_value`]: crate::routing::router::RouteParameters::from_payment_params_and_value + /// [`Router`]: crate::routing::router::Router + /// [`Event::PaymentFailed`]: crate::events::Event::PaymentFailed + pub fn pay_for_offer( + &self, offer: &Offer, quantity: Option, amount_msats: Option, + payer_note: Option, payment_id: PaymentId, retry_strategy: Retry, + max_total_routing_fee_msat: Option + ) -> Result<(), Bolt12SemanticError> { + self.pay_for_offer_intern(offer, quantity, amount_msats, payer_note, payment_id, None, |invoice_request, nonce| { + let expiration = StaleExpiration::TimerTicks(1); + let retryable_invoice_request = RetryableInvoiceRequest { + invoice_request: invoice_request.clone(), + nonce, + needs_retry: true, + }; + self.commons + .add_new_awaiting_invoice( + payment_id, expiration, retry_strategy, max_total_routing_fee_msat, + Some(retryable_invoice_request) + ) + .map_err(|_| Bolt12SemanticError::DuplicatePaymentId) + }) + } + + fn pay_for_offer_intern Result<(), Bolt12SemanticError>>( + &self, offer: &Offer, quantity: Option, amount_msats: Option, + payer_note: Option, payment_id: PaymentId, + human_readable_name: Option, create_pending_payment: CPP, + ) -> Result<(), Bolt12SemanticError> { + let expanded_key = &self.inbound_payment_key; + let entropy = &*self.entropy_source; + let secp_ctx = &self.secp_ctx; + + let nonce = Nonce::from_entropy_source(entropy); + let builder: InvoiceRequestBuilder = offer + .request_invoice(expanded_key, nonce, secp_ctx, payment_id)? + .into(); + let builder = builder.chain_hash(self.commons.get_chain_hash())?; + + let builder = match quantity { + None => builder, + Some(quantity) => builder.quantity(quantity)?, + }; + let builder = match amount_msats { + None => builder, + Some(amount_msats) => builder.amount_msats(amount_msats)?, + }; + let builder = match payer_note { + None => builder, + Some(payer_note) => builder.payer_note(payer_note), + }; + let builder = match human_readable_name { + None => builder, + Some(hrn) => builder.sourced_from_human_readable_name(hrn), + }; + let invoice_request = builder.build_and_sign()?; + + let hmac = payment_id.hmac_for_offer_payment(nonce, expanded_key); + let context = MessageContext::Offers( + OffersContext::OutboundPayment { payment_id, nonce, hmac: Some(hmac) } + ); + let reply_paths = self.create_blinded_paths(context) + .map_err(|_| Bolt12SemanticError::MissingPaths)?; + + // Temporarily removing this to find the best way to integrate the guard + // let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self); + + create_pending_payment(&invoice_request, nonce)?; + + self.enqueue_invoice_request(invoice_request, reply_paths) + } + + fn enqueue_invoice_request( + &self, + invoice_request: InvoiceRequest, + reply_paths: Vec, + ) -> Result<(), Bolt12SemanticError> { + let mut pending_offers_messages = self.commons.get_pending_offers_messages(); + if !invoice_request.paths().is_empty() { + reply_paths + .iter() + .flat_map(|reply_path| invoice_request.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::InvoiceRequest(invoice_request.clone()); + pending_offers_messages.push((message, instructions)); + }); + } else if let Some(node_id) = invoice_request.issuer_signing_pubkey() { + for reply_path in reply_paths { + let instructions = MessageSendInstructions::WithSpecifiedReplyPath { + destination: Destination::Node(node_id), + reply_path, + }; + let message = OffersMessage::InvoiceRequest(invoice_request.clone()); + pending_offers_messages.push((message, instructions)); + } + } else { + debug_assert!(false); + return Err(Bolt12SemanticError::MissingIssuerSigningPubkey); + } + + Ok(()) + } } impl OffersMessageHandler for OffersMessageFlow @@ -1267,7 +1474,7 @@ where hmac: Some(hmac) }); match self.create_blinded_paths(context) { - Ok(reply_paths) => match self.commons.enqueue_invoice_request(invoice_request, reply_paths) { + Ok(reply_paths) => match self.enqueue_invoice_request(invoice_request, reply_paths) { Ok(_) => {} Err(_) => { log_warn!(self.logger, @@ -1320,7 +1527,7 @@ where } if let Ok(amt_msats) = self.commons.amt_msats_for_payment_awaiting_offer(payment_id) { let offer_pay_res = - self.commons.pay_for_offer_intern(&offer, None, Some(amt_msats), None, payment_id, Some(name), + self.pay_for_offer_intern(&offer, None, Some(amt_msats), None, payment_id, Some(name), |invoice_request, nonce| { let retryable_invoice_request = RetryableInvoiceRequest { invoice_request: invoice_request.clone(), From d942501fcea8f1dbda24a8bca9e84f61947611e8 Mon Sep 17 00:00:00 2001 From: shaavan Date: Thu, 13 Feb 2025 22:28:47 +0530 Subject: [PATCH 12/14] Finish Bolt12 move from ChannelManager to Flow --- lightning/src/ln/channelmanager.rs | 222 +++-------------------------- lightning/src/ln/offers_tests.rs | 40 +++--- lightning/src/offers/flow.rs | 194 +++++++++++++++++++++++-- 3 files changed, 222 insertions(+), 234 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 35c7f92a64b..50551ad1302 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -33,10 +33,9 @@ use bitcoin::secp256k1::Secp256k1; use bitcoin::{secp256k1, Sequence}; use crate::events::FundingInfo; -use crate::blinded_path::message::{MessageContext, OffersContext}; use crate::blinded_path::NodeIdLookUp; -use crate::blinded_path::message::{BlindedMessagePath, MessageForwardNode}; -use crate::blinded_path::payment::{AsyncBolt12OfferContext, BlindedPaymentPath, Bolt12OfferContext, Bolt12RefundContext, PaymentConstraints, PaymentContext, UnauthenticatedReceiveTlvs}; +use crate::blinded_path::message::MessageForwardNode; +use crate::blinded_path::payment::{AsyncBolt12OfferContext, BlindedPaymentPath, Bolt12OfferContext, PaymentConstraints, PaymentContext, UnauthenticatedReceiveTlvs}; use crate::chain; use crate::chain::{Confirm, ChannelMonitorUpdateStatus, Watch, BestBlock}; use crate::chain::chaininterface::{BroadcasterInterface, ConfirmationTarget, FeeEstimator, LowerBoundedFeeEstimator}; @@ -49,6 +48,7 @@ use crate::ln::inbound_payment; use crate::ln::types::ChannelId; use crate::offers::flow::OffersMessageCommons; use crate::offers::invoice_request::InvoiceRequest; +use crate::sign::ecdsa::EcdsaChannelSigner; use crate::types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; use crate::ln::channel::{self, Channel, ChannelError, ChannelUpdateStatus, FundedChannel, ShutdownResult, UpdateFulfillCommitFetch, OutboundV1Channel, ReconnectionMsg, InboundV1Channel, WithChannelContext}; #[cfg(any(dual_funding, splicing))] @@ -68,15 +68,11 @@ use crate::ln::msgs::{ChannelMessageHandler, CommitmentUpdate, DecodeError, Ligh #[cfg(test)] use crate::ln::outbound_payment; use crate::ln::outbound_payment::{OutboundPayments, PendingOutboundPayment, RetryableInvoiceRequest, SendAlongPathArgs, StaleExpiration}; -use crate::offers::invoice::{Bolt12Invoice, DEFAULT_RELATIVE_EXPIRY, DerivedSigningPubkey, InvoiceBuilder, UnsignedBolt12Invoice}; +use crate::offers::invoice::{Bolt12Invoice, UnsignedBolt12Invoice}; use crate::offers::nonce::Nonce; -use crate::offers::parse::Bolt12SemanticError; -use crate::offers::refund::Refund; use crate::offers::signer; -use crate::onion_message::messenger::{Destination, MessageRouter, MessageSendInstructions}; -use crate::onion_message::offers::OffersMessage; +use crate::onion_message::messenger::MessageRouter; use crate::sign::{EntropySource, NodeSigner, Recipient, SignerProvider}; -use crate::sign::ecdsa::EcdsaChannelSigner; use crate::util::config::{UserConfig, ChannelConfig, ChannelConfigUpdate}; use crate::util::wakers::{Future, Notifier}; use crate::util::scid_utils::fake_scid; @@ -109,7 +105,7 @@ use core::{cmp, mem}; use core::borrow::Borrow; use core::cell::RefCell; use crate::io::Read; -use crate::sync::{Arc, FairRwLock, LockHeldState, LockTestExt, Mutex, MutexGuard, RwLock, RwLockReadGuard}; +use crate::sync::{Arc, FairRwLock, LockHeldState, LockTestExt, Mutex, RwLock, RwLockReadGuard}; use core::sync::atomic::{AtomicUsize, AtomicBool, Ordering}; use core::time::Duration; use core::ops::Deref; @@ -486,11 +482,15 @@ impl Ord for ClaimableHTLC { pub trait Verification { /// Constructs an HMAC to include in [`OffersContext`] for the data along with the given /// [`Nonce`]. + /// + /// [`OffersContext`]: crate::blinded_path::message::OffersContext fn hmac_for_offer_payment( &self, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey, ) -> Hmac; /// Authenticates the data using an HMAC and a [`Nonce`] taken from an [`OffersContext`]. + /// + /// [`OffersContext`]: crate::blinded_path::message::OffersContext fn verify_for_offer_payment( &self, hmac: Hmac, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey, ) -> Result<(), ()>; @@ -499,6 +499,8 @@ pub trait Verification { impl Verification for PaymentHash { /// Constructs an HMAC to include in [`OffersContext::InboundPayment`] for the payment hash /// along with the given [`Nonce`]. + /// + /// [`OffersContext::InboundPayment`]: crate::blinded_path::message::OffersContext::InboundPayment fn hmac_for_offer_payment( &self, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey, ) -> Hmac { @@ -507,6 +509,8 @@ impl Verification for PaymentHash { /// Authenticates the payment id using an HMAC and a [`Nonce`] taken from an /// [`OffersContext::InboundPayment`]. + /// + /// [`OffersContext::InboundPayment`]: crate::blinded_path::message::OffersContext::InboundPayment fn verify_for_offer_payment( &self, hmac: Hmac, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey, ) -> Result<(), ()> { @@ -561,6 +565,8 @@ impl PaymentId { impl Verification for PaymentId { /// Constructs an HMAC to include in [`OffersContext::OutboundPayment`] for the payment id /// along with the given [`Nonce`]. + /// + /// [`OffersContext::OutboundPayment`]: crate::blinded_path::message::OffersContext::OutboundPayment fn hmac_for_offer_payment( &self, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey, ) -> Hmac { @@ -569,6 +575,8 @@ impl Verification for PaymentId { /// Authenticates the payment id using an HMAC and a [`Nonce`] taken from an /// [`OffersContext::OutboundPayment`]. + /// + /// [`OffersContext::OutboundPayment`]: crate::blinded_path::message::OffersContext::OutboundPayment fn verify_for_offer_payment( &self, hmac: Hmac, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey, ) -> Result<(), ()> { @@ -2079,51 +2087,7 @@ where /// /// For more information on creating refunds, see [`create_refund_builder`]. /// -/// 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; -/// # 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(()) -/// }); -/// # } -/// ``` +/// For requesting refund payments, see [`request_refund_payment`]. /// /// # Persistence /// @@ -2207,7 +2171,7 @@ where /// [`create_offer_builder`]: crate::offers::flow::OffersMessageFlow::create_offer_builder /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest /// [`create_refund_builder`]: crate::offers::flow::OffersMessageFlow::create_refund_builder -/// [`request_refund_payment`]: Self::request_refund_payment +/// [`request_refund_payment`]: crate::offers::flow::OffersMessageFlow::request_refund_payment /// [`peer_disconnected`]: msgs::ChannelMessageHandler::peer_disconnected /// [`funding_created`]: msgs::FundingCreated /// [`funding_transaction_generated`]: Self::funding_transaction_generated @@ -2478,11 +2442,6 @@ where event_persist_notifier: Notifier, needs_persist_flag: AtomicBool, - #[cfg(not(any(test, feature = "_test_utils")))] - pending_offers_messages: Mutex>, - #[cfg(any(test, feature = "_test_utils"))] - pub(crate) pending_offers_messages: Mutex>, - /// Tracks the message events that are to be broadcasted when we are connected to some peer. pending_broadcast_messages: Mutex>, @@ -3402,7 +3361,6 @@ where needs_persist_flag: AtomicBool::new(false), funding_batch_states: Mutex::new(BTreeMap::new()), - pending_offers_messages: Mutex::new(Vec::new()), pending_broadcast_messages: Mutex::new(Vec::new()), last_days_feerates: Mutex::new(VecDeque::new()), @@ -9676,23 +9634,8 @@ where fn send_payment_for_static_invoice(&self, payment_id: PaymentId) -> Result<(), Bolt12PaymentError> { self.send_payment_for_static_invoice(payment_id) } - - // ----Temporary Functions---- - // Set of functions temporarily moved to OffersMessageCommons for easier - // transition of code from ChannelManager to OffersMessageFlow - - fn get_pending_offers_messages(&self) -> MutexGuard<'_, Vec<(OffersMessage, MessageSendInstructions)>> { - self.pending_offers_messages.lock().expect("Mutex is locked by other thread.") - } } -/// Defines the maximum number of [`OffersMessage`] including different reply paths to be sent -/// along different paths. -/// 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. -pub(crate) const OFFERS_MESSAGE_REQUEST_LIMIT: usize = 10; - impl ChannelManager where M::Target: chain::Watch<::EcdsaSigner>, @@ -9705,106 +9648,6 @@ where MR::Target: MessageRouter, L::Target: Logger, { - /// 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( - Some(amount_msats), payment_secret, payment_context, relative_expiry, - ) - .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), - } - } - /// Gets a payment secret and payment hash for use in an invoice given to a third party wishing /// to pay us. /// @@ -9903,27 +9746,6 @@ where inbound_payment::get_payment_preimage(payment_hash, payment_secret, &self.inbound_payment_key) } - /// Creates a collection of blinded paths by delegating to - /// [`MessageRouter::create_blinded_paths`]. - /// - /// Errors if the `MessageRouter` errors. - fn create_blinded_paths(&self, context: MessageContext) -> Result, ()> { - let recipient = self.get_our_node_id(); - let secp_ctx = &self.secp_ctx; - - let peers = self.per_peer_state.read().unwrap() - .iter() - .map(|(node_id, peer_state)| (node_id, peer_state.lock().unwrap())) - .filter(|(_, peer)| peer.is_connected) - .filter(|(_, peer)| peer.latest_features.supports_onion_messages()) - .map(|(node_id, _)| *node_id) - .collect::>(); - - self.message_router - .create_blinded_paths(recipient, context, peers, secp_ctx) - .and_then(|paths| (!paths.is_empty()).then(|| paths).ok_or(())) - } - #[cfg(async_payments)] pub(super) fn duration_since_epoch(&self) -> Duration { #[cfg(not(feature = "std"))] @@ -12229,7 +12051,9 @@ where /// The [`MessageRouter`] used for constructing [`BlindedMessagePath`]s for [`Offer`]s, /// [`Refund`]s, and any reply paths. /// + /// [`BlindedMessagePath`]: crate::blinded_path::message::BlindedMessagePath /// [`Offer`]: crate::offers::offer::Offer + /// [`Refund`]: crate::offers::refund::Refund pub message_router: MR, /// The Logger for use in the ChannelManager and which may be used to log information during /// deserialization. @@ -13332,8 +13156,6 @@ where funding_batch_states: Mutex::new(BTreeMap::new()), - pending_offers_messages: Mutex::new(Vec::new()), - pending_broadcast_messages: Mutex::new(Vec::new()), entropy_source: args.entropy_source, diff --git a/lightning/src/ln/offers_tests.rs b/lightning/src/ln/offers_tests.rs index 373784c27f1..a9085b8512c 100644 --- a/lightning/src/ln/offers_tests.rs +++ b/lightning/src/ln/offers_tests.rs @@ -656,7 +656,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); @@ -785,7 +785,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); @@ -890,7 +890,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); @@ -1045,7 +1045,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); @@ -1249,7 +1249,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(); @@ -1327,7 +1327,7 @@ fn fails_authentication_when_handling_invoice_request() { expect_recent_payment!(david, RecentPaymentDetails::AwaitingInvoice, payment_id); connect_peers(david, alice); - match &mut david.node.pending_offers_messages.lock().unwrap().first_mut().unwrap().1 { + match &mut david.offers_handler.pending_offers_messages.lock().unwrap().first_mut().unwrap().1 { MessageSendInstructions::WithSpecifiedReplyPath { destination, .. } => *destination = Destination::Node(alice_id), _ => panic!(), @@ -1352,7 +1352,7 @@ fn fails_authentication_when_handling_invoice_request() { .unwrap(); expect_recent_payment!(david, RecentPaymentDetails::AwaitingInvoice, payment_id); - match &mut david.node.pending_offers_messages.lock().unwrap().first_mut().unwrap().1 { + match &mut david.offers_handler.pending_offers_messages.lock().unwrap().first_mut().unwrap().1 { MessageSendInstructions::WithSpecifiedReplyPath { destination, .. } => *destination = Destination::BlindedPath(invalid_path), _ => panic!(), @@ -1432,7 +1432,7 @@ fn fails_authentication_when_handling_invoice_for_offer() { // Don't send the invoice request, but grab its reply path to use with a different request. let invalid_reply_path = { - let mut pending_offers_messages = david.node.pending_offers_messages.lock().unwrap(); + let mut pending_offers_messages = david.offers_handler.pending_offers_messages.lock().unwrap(); let pending_invoice_request = pending_offers_messages.pop().unwrap(); pending_offers_messages.clear(); match pending_invoice_request.1 { @@ -1449,7 +1449,7 @@ fn fails_authentication_when_handling_invoice_for_offer() { // Swap out the reply path to force authentication to fail when handling the invoice since it // will be sent over the wrong blinded path. { - let mut pending_offers_messages = david.node.pending_offers_messages.lock().unwrap(); + let mut pending_offers_messages = david.offers_handler.pending_offers_messages.lock().unwrap(); let mut pending_invoice_request = pending_offers_messages.first_mut().unwrap(); match &mut pending_invoice_request.1 { MessageSendInstructions::WithSpecifiedReplyPath { reply_path, .. } => @@ -1533,10 +1533,10 @@ 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 { + match &mut alice.offers_handler.pending_offers_messages.lock().unwrap().first_mut().unwrap().1 { MessageSendInstructions::WithSpecifiedReplyPath { destination, .. } => *destination = Destination::Node(david_id), _ => panic!(), @@ -1565,9 +1565,9 @@ 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 { + match &mut alice.offers_handler.pending_offers_messages.lock().unwrap().first_mut().unwrap().1 { MessageSendInstructions::WithSpecifiedReplyPath { destination, .. } => *destination = Destination::BlindedPath(invalid_path), _ => panic!(), @@ -1698,7 +1698,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), } @@ -1707,7 +1707,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. @@ -1757,7 +1757,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), } @@ -1980,7 +1980,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), } @@ -2031,7 +2031,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); @@ -2051,7 +2051,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); @@ -2158,7 +2158,7 @@ fn fails_paying_invoice_with_unknown_required_features() { destination: Destination::BlindedPath(reply_path), }; let message = OffersMessage::Invoice(invoice); - alice.node.pending_offers_messages.lock().unwrap().push((message, instructions)); + alice.offers_handler.pending_offers_messages.lock().unwrap().push((message, instructions)); let onion_message = alice.onion_messenger.next_onion_message_for_peer(charlie_id).unwrap(); charlie.onion_messenger.handle_onion_message(alice_id, &onion_message); diff --git a/lightning/src/offers/flow.rs b/lightning/src/offers/flow.rs index eb7eabc77d7..b28b086f8fc 100644 --- a/lightning/src/offers/flow.rs +++ b/lightning/src/offers/flow.rs @@ -30,18 +30,18 @@ use lightning_invoice::PaymentSecret; use types::payment::PaymentHash; use crate::blinded_path::message::{AsyncPaymentsContext, BlindedMessagePath, MessageContext, MessageForwardNode, OffersContext}; -use crate::blinded_path::payment::{BlindedPaymentPath, Bolt12OfferContext, PaymentContext}; +use crate::blinded_path::payment::{BlindedPaymentPath, Bolt12OfferContext, Bolt12RefundContext, PaymentContext}; use crate::events::PaymentFailureReason; -use crate::ln::channelmanager::{Bolt12PaymentError, OFFERS_MESSAGE_REQUEST_LIMIT, PaymentId, Verification}; +use crate::ln::channelmanager::{Bolt12PaymentError, PaymentId, Verification}; use crate::ln::outbound_payment::{Retry, RetryableInvoiceRequest, StaleExpiration}; use crate::offers::invoice::{Bolt12Invoice, DerivedSigningPubkey, ExplicitSigningPubkey, InvoiceBuilder, UnsignedBolt12Invoice, DEFAULT_RELATIVE_EXPIRY}; use crate::offers::invoice_request::{InvoiceRequest, InvoiceRequestBuilder}; use crate::offers::offer::OfferBuilder; use crate::offers::parse::Bolt12SemanticError; -use crate::offers::refund::RefundBuilder; +use crate::offers::refund::{Refund, RefundBuilder}; use crate::onion_message::messenger::{Destination, MessageSendInstructions, Responder, ResponseInstruction}; use crate::onion_message::offers::{OffersMessage, OffersMessageHandler}; -use crate::sync::{Mutex, MutexGuard}; +use crate::sync::Mutex; use crate::onion_message::messenger::MessageRouter; use crate::util::logger::{Logger, WithContext}; @@ -198,13 +198,6 @@ pub trait OffersMessageCommons { #[cfg(async_payments)] /// Sends payment for the [`StaticInvoice`] associated with the given `payment_id`. fn send_payment_for_static_invoice(&self, payment_id: PaymentId) -> Result<(), Bolt12PaymentError>; - - // ----Temporary Functions---- - // Set of functions temporarily moved to OffersMessageCommons for easier - // transition of code from ChannelManager to OffersMessageFlow - - /// Get pending offers messages - fn get_pending_offers_messages(&self) -> MutexGuard<'_, Vec<(OffersMessage, MessageSendInstructions)>>; } /// [`SimpleArcOffersMessageFlow`] is useful when you need a [`OffersMessageFlow`] with a static lifetime, e.g. @@ -462,6 +455,7 @@ where /// ``` /// /// ## Bolt 12 Refund +/// /// A [`Refund`] is a request for an invoice to be paid. Like *paying* for an [`Offer`], *creating* /// a [`Refund`] involves maintaining state since it represents a future outbound payment. /// Therefore, use [`create_refund_builder`] when creating one, otherwise [`OffersMessageFlow`] will @@ -524,7 +518,66 @@ where /// # Ok(()) /// # } /// ``` -/// +/// +/// 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; +/// # use lightning::offers::flow::{AnOffersMessageFlow, OffersMessageCommons}; +/// # use lightning::offers::refund::Refund; +/// # +/// # fn example(offers_flow: T, channel_manager: U, refund: &Refund) { +/// # let offers_flow = offers_flow.get_omf(); +/// # let channel_manager = channel_manager.get_cm(); +/// 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 +/// 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(()) +/// }); +/// # } +/// ``` +/// +/// [`DNSResolverMessage`]: crate::onion_message::dns_resolution::DNSResolverMessage +/// [`ChannelManager`]: crate::ln::channelmanager::ChannelManager +/// [`Bolt12Invoice`]: crate::offers::invoice +/// [`create_offer_builder`]: Self::create_offer_builder +/// [`create_refund_builder`]: Self::create_refund_builder +/// [`Refund`]: crate::offers::refund::Refund +/// [`InvoiceRequest`]: crate::offers::invoice_request +/// [`Offer`]: crate::offers::offer +/// [`offers`]: crate::offers +/// [`pay_for_offer`]: Self::pay_for_offer +/// [`request_refund_payment`]: Self::request_refund_payment /// [`offers`]: crate::offers /// [`Refund`]: crate::offers::refund::Refund /// [`ChannelManager`]: crate::ln::channelmanager::ChannelManager @@ -549,6 +602,11 @@ where message_router: MR, entropy_source: ES, + #[cfg(not(any(test, feature = "_test_utils")))] + pending_offers_messages: Mutex>, + #[cfg(any(test, feature = "_test_utils"))] + pub(crate) pending_offers_messages: Mutex>, + pending_async_payments_messages: Mutex>, #[cfg(feature = "_test_utils")] @@ -592,6 +650,7 @@ where message_router, entropy_source, + pending_offers_messages: Mutex::new(Vec::new()), pending_async_payments_messages: Mutex::new(Vec::new()), logger, @@ -721,6 +780,13 @@ where } } +/// Defines the maximum number of [`OffersMessage`] including different reply paths to be sent +/// along different paths. +/// 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; + impl OffersMessageFlow where ES::Target: EntropySource, @@ -1218,7 +1284,7 @@ where invoice_request: InvoiceRequest, reply_paths: Vec, ) -> Result<(), Bolt12SemanticError> { - let mut pending_offers_messages = self.commons.get_pending_offers_messages(); + let mut pending_offers_messages = self.pending_offers_messages.lock().unwrap(); if !invoice_request.paths().is_empty() { reply_paths .iter() @@ -1248,6 +1314,106 @@ where Ok(()) } + + /// 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. + /// + /// [`ChannelManager`]: crate::ln::channelmanager::ChannelManager + /// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice + /// [`PaymentPreimage`]: crate::types::payment::PaymentPreimage + /// [`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); + } + + 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( + Some(amount_msats), payment_secret, payment_context, relative_expiry, + ) + .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 = self.commons.get_highest_seen_timestamp(); + #[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), + } + } + } impl OffersMessageHandler for OffersMessageFlow @@ -1495,7 +1661,7 @@ where } fn release_pending_messages(&self) -> Vec<(OffersMessage, MessageSendInstructions)> { - core::mem::take(&mut self.commons.get_pending_offers_messages()) + core::mem::take(&mut self.pending_offers_messages.lock().unwrap()) } } From 8a29e3fa18294deaed0dd331d2638d4bf035777f Mon Sep 17 00:00:00 2001 From: shaavan Date: Sat, 15 Feb 2025 18:41:55 +0530 Subject: [PATCH 13/14] Apply rust formatting to flow.rs --- lightning/src/offers/flow.rs | 537 ++++++++++++++++++++++------------- 1 file changed, 347 insertions(+), 190 deletions(-) diff --git a/lightning/src/offers/flow.rs b/lightning/src/offers/flow.rs index b28b086f8fc..8dd514e29b7 100644 --- a/lightning/src/offers/flow.rs +++ b/lightning/src/offers/flow.rs @@ -16,9 +16,11 @@ use bitcoin::secp256k1::{self, PublicKey}; use crate::ln::inbound_payment; use crate::offers::invoice_error::InvoiceError; -use crate::offers::offer::Offer; use crate::offers::nonce::Nonce; -use crate::onion_message::async_payments::{AsyncPaymentsMessage, AsyncPaymentsMessageHandler, HeldHtlcAvailable, ReleaseHeldHtlc}; +use crate::offers::offer::Offer; +use crate::onion_message::async_payments::{ + AsyncPaymentsMessage, AsyncPaymentsMessageHandler, HeldHtlcAvailable, ReleaseHeldHtlc, +}; use crate::onion_message::dns_resolution::HumanReadableName; use crate::prelude::*; use crate::sign::EntropySource; @@ -29,20 +31,29 @@ use bitcoin::secp256k1::schnorr; use lightning_invoice::PaymentSecret; use types::payment::PaymentHash; -use crate::blinded_path::message::{AsyncPaymentsContext, BlindedMessagePath, MessageContext, MessageForwardNode, OffersContext}; -use crate::blinded_path::payment::{BlindedPaymentPath, Bolt12OfferContext, Bolt12RefundContext, PaymentContext}; +use crate::blinded_path::message::{ + AsyncPaymentsContext, BlindedMessagePath, MessageContext, MessageForwardNode, OffersContext, +}; +use crate::blinded_path::payment::{ + BlindedPaymentPath, Bolt12OfferContext, Bolt12RefundContext, PaymentContext, +}; use crate::events::PaymentFailureReason; use crate::ln::channelmanager::{Bolt12PaymentError, PaymentId, Verification}; use crate::ln::outbound_payment::{Retry, RetryableInvoiceRequest, StaleExpiration}; -use crate::offers::invoice::{Bolt12Invoice, DerivedSigningPubkey, ExplicitSigningPubkey, InvoiceBuilder, UnsignedBolt12Invoice, DEFAULT_RELATIVE_EXPIRY}; +use crate::offers::invoice::{ + Bolt12Invoice, DerivedSigningPubkey, ExplicitSigningPubkey, InvoiceBuilder, + UnsignedBolt12Invoice, DEFAULT_RELATIVE_EXPIRY, +}; use crate::offers::invoice_request::{InvoiceRequest, InvoiceRequestBuilder}; use crate::offers::offer::OfferBuilder; use crate::offers::parse::Bolt12SemanticError; use crate::offers::refund::{Refund, RefundBuilder}; -use crate::onion_message::messenger::{Destination, MessageSendInstructions, Responder, ResponseInstruction}; +use crate::onion_message::messenger::MessageRouter; +use crate::onion_message::messenger::{ + Destination, MessageSendInstructions, Responder, ResponseInstruction, +}; use crate::onion_message::offers::{OffersMessage, OffersMessageHandler}; use crate::sync::Mutex; -use crate::onion_message::messenger::MessageRouter; use crate::util::logger::{Logger, WithContext}; #[cfg(not(feature = "std"))] @@ -50,31 +61,37 @@ use core::sync::atomic::Ordering; #[cfg(async_payments)] use { - crate::offers::offer::Amount, crate::blinded_path::payment::AsyncBolt12OfferContext, - crate::offers::signer, - crate::offers::static_invoice::{DEFAULT_RELATIVE_EXPIRY as STATIC_INVOICE_DEFAULT_RELATIVE_EXPIRY, StaticInvoice, StaticInvoiceBuilder}, + crate::offers::offer::Amount, + crate::offers::signer, + crate::offers::static_invoice::{ + StaticInvoice, StaticInvoiceBuilder, + DEFAULT_RELATIVE_EXPIRY as STATIC_INVOICE_DEFAULT_RELATIVE_EXPIRY, + }, }; -#[cfg(c_bindings)] use { - crate::offers::refund::RefundMaybeWithDerivedMetadataBuilder, +#[cfg(c_bindings)] +use { crate::offers::offer::OfferWithDerivedMetadataBuilder, + crate::offers::refund::RefundMaybeWithDerivedMetadataBuilder, }; #[cfg(not(c_bindings))] use { - crate::offers::offer::DerivedMetadata, crate::ln::channelmanager::{SimpleArcChannelManager, SimpleRefChannelManager}, + crate::offers::offer::DerivedMetadata, crate::onion_message::messenger::DefaultMessageRouter, crate::routing::gossip::NetworkGraph, crate::sign::KeysManager, crate::sync::Arc, }; -#[cfg(feature= "dnssec")] +#[cfg(feature = "dnssec")] use { crate::blinded_path::message::DNSResolverContext, - crate::onion_message::dns_resolution::{DNSResolverMessage, DNSResolverMessageHandler, DNSSECProof, DNSSECQuery, OMNameResolver}, + crate::onion_message::dns_resolution::{ + DNSResolverMessage, DNSResolverMessageHandler, DNSSECProof, DNSSECQuery, OMNameResolver, + }, }; /// Functions commonly shared in usage between [`ChannelManager`] & `OffersMessageFlow` @@ -103,7 +120,8 @@ pub trait OffersMessageCommons { /// /// [`Router::create_blinded_payment_paths`]: crate::routing::router::Router::create_blinded_payment_paths fn create_blinded_payment_paths( - &self, amount_msats: Option, payment_secret: PaymentSecret, payment_context: PaymentContext, relative_expiry_time: u32, + &self, amount_msats: Option, payment_secret: PaymentSecret, + payment_context: PaymentContext, relative_expiry_time: u32, ) -> Result, ()>; /// Gets a payment secret and payment hash for use in an invoice given to a third party wishing @@ -137,11 +155,15 @@ pub trait OffersMessageCommons { /// [`PaymentPurpose::preimage`]: crate::events::PaymentPurpose::preimage /// [`create_inbound_payment_for_hash`]: crate::ln::channelmanager::ChannelManager::create_inbound_payment_for_hash /// [`PaymentPreimage`]: crate::types::payment::PaymentPreimage - fn create_inbound_payment(&self, min_value_msat: Option, invoice_expiry_delta_secs: u32, - min_final_cltv_expiry_delta: Option) -> Result<(PaymentHash, PaymentSecret), ()>; + fn create_inbound_payment( + &self, min_value_msat: Option, invoice_expiry_delta_secs: u32, + min_final_cltv_expiry_delta: Option, + ) -> Result<(PaymentHash, PaymentSecret), ()>; /// Release invoice requests awaiting invoice - fn release_invoice_requests_awaiting_invoice(&self) -> Vec<(PaymentId, RetryableInvoiceRequest)>; + fn release_invoice_requests_awaiting_invoice( + &self, + ) -> Vec<(PaymentId, RetryableInvoiceRequest)>; /// Add new awaiting invoice fn add_new_awaiting_invoice( @@ -166,7 +188,9 @@ pub trait OffersMessageCommons { ) -> Result; /// Send payment for verified bolt12 invoice - fn send_payment_for_verified_bolt12_invoice(&self, invoice: &Bolt12Invoice, payment_id: PaymentId) -> Result<(), Bolt12PaymentError>; + fn send_payment_for_verified_bolt12_invoice( + &self, invoice: &Bolt12Invoice, payment_id: PaymentId, + ) -> Result<(), Bolt12PaymentError>; /// Abandon Payment with Reason fn abandon_payment_with_reason(&self, payment_id: PaymentId, reason: PaymentFailureReason); @@ -197,7 +221,9 @@ pub trait OffersMessageCommons { #[cfg(async_payments)] /// Sends payment for the [`StaticInvoice`] associated with the given `payment_id`. - fn send_payment_for_static_invoice(&self, payment_id: PaymentId) -> Result<(), Bolt12PaymentError>; + fn send_payment_for_static_invoice( + &self, payment_id: PaymentId, + ) -> Result<(), Bolt12PaymentError>; } /// [`SimpleArcOffersMessageFlow`] is useful when you need a [`OffersMessageFlow`] with a static lifetime, e.g. @@ -316,7 +342,7 @@ where /// /// This struct is essential for enabling BOLT12 payment workflows in the Lightning network, /// providing the foundational mechanisms for Offers and related message exchanges. -/// +/// /// ## Relationship with `ChannelManager` /// /// [`OffersMessageFlow`] and [`ChannelManager`] work in tandem to facilitate BOLT 12 functionality within @@ -400,7 +426,7 @@ where /// # Ok(()) /// # } /// ``` -/// +/// /// Use [`pay_for_offer`] to initiated payment, which sends an [`InvoiceRequest`] for an [`Offer`] /// and pays the [`Bolt12Invoice`] response. /// @@ -453,7 +479,7 @@ where /// }); /// # } /// ``` -/// +/// /// ## Bolt 12 Refund /// /// A [`Refund`] is a request for an invoice to be paid. Like *paying* for an [`Offer`], *creating* @@ -735,10 +761,8 @@ where let recipient = self.get_our_node_id(); let secp_ctx = &self.secp_ctx; - let peers = self.commons.get_peer_for_blinded_path() - .into_iter() - .map(|node| node.node_id) - .collect(); + let peers = + self.commons.get_peer_for_blinded_path().into_iter().map(|node| node.node_id).collect(); self.message_router .create_blinded_paths(recipient, context, peers, secp_ctx) @@ -749,14 +773,21 @@ where /// [`MessageRouter::create_compact_blinded_paths`]. /// /// Errors if the `MessageRouter` errors. - fn create_compact_blinded_paths(&self, context: OffersContext) -> Result, ()> { + fn create_compact_blinded_paths( + &self, context: OffersContext, + ) -> Result, ()> { let recipient = self.get_our_node_id(); let secp_ctx = &self.secp_ctx; let peers = self.commons.get_peer_for_blinded_path(); self.message_router - .create_compact_blinded_paths(recipient, MessageContext::Offers(context), peers, secp_ctx) + .create_compact_blinded_paths( + recipient, + MessageContext::Offers(context), + peers, + secp_ctx, + ) .and_then(|paths| (!paths.is_empty()).then(|| paths).ok_or(())) } @@ -843,11 +874,20 @@ where retry_strategy: Retry, max_total_routing_fee_msat: Option, dns_resolvers: Vec, ) -> Result<(), ()> { - let (onion_message, context) = - self.commons.get_hrn_resolver().resolve_name(payment_id, name, &*self.entropy_source)?; + let (onion_message, context) = self.commons.get_hrn_resolver().resolve_name( + payment_id, + name, + &*self.entropy_source, + )?; let reply_paths = self.create_blinded_paths(MessageContext::DNSResolver(context))?; let expiration = StaleExpiration::TimerTicks(1); - self.commons.add_new_awaiting_offer(payment_id, expiration, retry_strategy, max_total_routing_fee_msat, amount_msats)?; + self.commons.add_new_awaiting_offer( + payment_id, + expiration, + retry_strategy, + max_total_routing_fee_msat, + amount_msats, + )?; let message_params = dns_resolvers .iter() .flat_map(|destination| reply_paths.iter().map(move |path| (path, destination))) @@ -874,30 +914,33 @@ where { #[cfg(async_payments)] fn initiate_async_payment( - &self, invoice: &StaticInvoice, payment_id: PaymentId + &self, invoice: &StaticInvoice, payment_id: PaymentId, ) -> Result<(), Bolt12PaymentError> { - self.commons.handle_static_invoice_received(invoice, payment_id)?; let nonce = Nonce::from_entropy_source(&*self.entropy_source); let hmac = payment_id.hmac_for_async_payment(nonce, &self.inbound_payment_key); - let reply_paths = match self.create_blinded_paths( - MessageContext::AsyncPayments( - AsyncPaymentsContext::OutboundPayment { payment_id, nonce, hmac } - ) - ) { + let reply_paths = match self.create_blinded_paths(MessageContext::AsyncPayments( + AsyncPaymentsContext::OutboundPayment { payment_id, nonce, hmac }, + )) { Ok(paths) => paths, Err(()) => { - self.commons.abandon_payment_with_reason(payment_id, PaymentFailureReason::BlindedPathCreationFailed); + self.commons.abandon_payment_with_reason( + payment_id, + PaymentFailureReason::BlindedPathCreationFailed, + ); return Err(Bolt12PaymentError::BlindedPathCreationFailed); - } + }, }; - let mut pending_async_payments_messages = self.pending_async_payments_messages.lock().unwrap(); + let mut pending_async_payments_messages = + self.pending_async_payments_messages.lock().unwrap(); const HTLC_AVAILABLE_LIMIT: usize = 10; reply_paths .iter() - .flat_map(|reply_path| invoice.message_paths().iter().map(move |invoice_path| (invoice_path, reply_path))) + .flat_map(|reply_path| { + invoice.message_paths().iter().map(move |invoice_path| (invoice_path, reply_path)) + }) .take(HTLC_AVAILABLE_LIMIT) .for_each(|(invoice_path, reply_path)| { let instructions = MessageSendInstructions::WithSpecifiedReplyPath { @@ -1051,7 +1094,6 @@ macro_rules! create_refund_builder { ($self: ident, $builder: ty) => { } } } - impl OffersMessageFlow where ES::Target: EntropySource, @@ -1079,10 +1121,10 @@ where /// aforementioned always-online node. #[cfg(async_payments)] pub fn create_async_receive_offer_builder( - &self, message_paths_to_always_online_node: Vec + &self, message_paths_to_always_online_node: Vec, ) -> Result<(OfferBuilder, Nonce), Bolt12SemanticError> { if message_paths_to_always_online_node.is_empty() { - return Err(Bolt12SemanticError::MissingPaths) + return Err(Bolt12SemanticError::MissingPaths); } let node_id = self.get_our_node_id(); @@ -1091,9 +1133,9 @@ where let secp_ctx = &self.secp_ctx; let nonce = Nonce::from_entropy_source(entropy); - let mut builder = OfferBuilder::deriving_signing_pubkey( - node_id, expanded_key, nonce, secp_ctx - ).chain_hash(self.commons.get_chain_hash()); + let mut builder = + OfferBuilder::deriving_signing_pubkey(node_id, expanded_key, nonce, secp_ctx) + .chain_hash(self.commons.get_chain_hash()); for path in message_paths_to_always_online_node { builder = builder.path(path); @@ -1107,20 +1149,17 @@ where /// invoice's expiry will default to [`STATIC_INVOICE_DEFAULT_RELATIVE_EXPIRY`]. #[cfg(async_payments)] pub fn create_static_invoice_builder<'a>( - &self, offer: &'a Offer, offer_nonce: Nonce, relative_expiry: Option + &self, offer: &'a Offer, offer_nonce: Nonce, relative_expiry: Option, ) -> Result, Bolt12SemanticError> { let expanded_key = &self.inbound_payment_key; let entropy = &*self.entropy_source; let secp_ctx = &self.secp_ctx; - let payment_context = PaymentContext::AsyncBolt12Offer( - AsyncBolt12OfferContext { offer_nonce } - ); - let amount_msat = offer.amount().and_then(|amount| { - match amount { - Amount::Bitcoin { amount_msats } => Some(amount_msats), - Amount::Currency { .. } => None - } + let payment_context = + PaymentContext::AsyncBolt12Offer(AsyncBolt12OfferContext { offer_nonce }); + let amount_msat = offer.amount().and_then(|amount| match amount { + Amount::Bitcoin { amount_msats } => Some(amount_msats), + Amount::Currency { .. } => None, }); let relative_expiry = relative_expiry.unwrap_or(STATIC_INVOICE_DEFAULT_RELATIVE_EXPIRY); @@ -1128,28 +1167,48 @@ where let created_at = self.duration_since_epoch(); let payment_secret = inbound_payment::create_for_spontaneous_payment( - &self.inbound_payment_key, amount_msat, relative_expiry_secs, created_at.as_secs(), None - ).map_err(|()| Bolt12SemanticError::InvalidAmount)?; - - let payment_paths = self.commons.create_blinded_payment_paths( - amount_msat, payment_secret, payment_context, relative_expiry_secs - ).map_err(|()| Bolt12SemanticError::MissingPaths)?; + &self.inbound_payment_key, + amount_msat, + relative_expiry_secs, + created_at.as_secs(), + None, + ) + .map_err(|()| Bolt12SemanticError::InvalidAmount)?; + + let payment_paths = self + .commons + .create_blinded_payment_paths( + amount_msat, + payment_secret, + payment_context, + relative_expiry_secs, + ) + .map_err(|()| Bolt12SemanticError::MissingPaths)?; let nonce = Nonce::from_entropy_source(entropy); let hmac = signer::hmac_for_held_htlc_available_context(nonce, expanded_key); - let path_absolute_expiry = Duration::from_secs( - inbound_payment::calculate_absolute_expiry(created_at.as_secs(), relative_expiry_secs) - ); - let context = MessageContext::AsyncPayments( - AsyncPaymentsContext::InboundPayment { nonce, hmac, path_absolute_expiry } - ); - let async_receive_message_paths = self.create_blinded_paths(context) - .map_err(|()| Bolt12SemanticError::MissingPaths)?; + let path_absolute_expiry = Duration::from_secs(inbound_payment::calculate_absolute_expiry( + created_at.as_secs(), + relative_expiry_secs, + )); + let context = MessageContext::AsyncPayments(AsyncPaymentsContext::InboundPayment { + nonce, + hmac, + path_absolute_expiry, + }); + let async_receive_message_paths = + self.create_blinded_paths(context).map_err(|()| Bolt12SemanticError::MissingPaths)?; StaticInvoiceBuilder::for_offer_using_derived_keys( - offer, payment_paths, async_receive_message_paths, created_at, expanded_key, - offer_nonce, secp_ctx - ).map(|inv| inv.allow_mpp().relative_expiry(relative_expiry_secs)) + offer, + payment_paths, + async_receive_message_paths, + created_at, + expanded_key, + offer_nonce, + secp_ctx, + ) + .map(|inv| inv.allow_mpp().relative_expiry(relative_expiry_secs)) } /// Pays for an [`Offer`] using the given parameters by creating an [`InvoiceRequest`] and @@ -1213,25 +1272,38 @@ where pub fn pay_for_offer( &self, offer: &Offer, quantity: Option, amount_msats: Option, payer_note: Option, payment_id: PaymentId, retry_strategy: Retry, - max_total_routing_fee_msat: Option + max_total_routing_fee_msat: Option, ) -> Result<(), Bolt12SemanticError> { - self.pay_for_offer_intern(offer, quantity, amount_msats, payer_note, payment_id, None, |invoice_request, nonce| { - let expiration = StaleExpiration::TimerTicks(1); - let retryable_invoice_request = RetryableInvoiceRequest { - invoice_request: invoice_request.clone(), - nonce, - needs_retry: true, - }; - self.commons - .add_new_awaiting_invoice( - payment_id, expiration, retry_strategy, max_total_routing_fee_msat, - Some(retryable_invoice_request) - ) - .map_err(|_| Bolt12SemanticError::DuplicatePaymentId) - }) + self.pay_for_offer_intern( + offer, + quantity, + amount_msats, + payer_note, + payment_id, + None, + |invoice_request, nonce| { + let expiration = StaleExpiration::TimerTicks(1); + let retryable_invoice_request = RetryableInvoiceRequest { + invoice_request: invoice_request.clone(), + nonce, + needs_retry: true, + }; + self.commons + .add_new_awaiting_invoice( + payment_id, + expiration, + retry_strategy, + max_total_routing_fee_msat, + Some(retryable_invoice_request), + ) + .map_err(|_| Bolt12SemanticError::DuplicatePaymentId) + }, + ) } - fn pay_for_offer_intern Result<(), Bolt12SemanticError>>( + fn pay_for_offer_intern< + CPP: FnOnce(&InvoiceRequest, Nonce) -> Result<(), Bolt12SemanticError>, + >( &self, offer: &Offer, quantity: Option, amount_msats: Option, payer_note: Option, payment_id: PaymentId, human_readable_name: Option, create_pending_payment: CPP, @@ -1241,9 +1313,8 @@ where let secp_ctx = &self.secp_ctx; let nonce = Nonce::from_entropy_source(entropy); - let builder: InvoiceRequestBuilder = offer - .request_invoice(expanded_key, nonce, secp_ctx, payment_id)? - .into(); + let builder: InvoiceRequestBuilder = + offer.request_invoice(expanded_key, nonce, secp_ctx, payment_id)?.into(); let builder = builder.chain_hash(self.commons.get_chain_hash())?; let builder = match quantity { @@ -1265,11 +1336,13 @@ where let invoice_request = builder.build_and_sign()?; let hmac = payment_id.hmac_for_offer_payment(nonce, expanded_key); - let context = MessageContext::Offers( - OffersContext::OutboundPayment { payment_id, nonce, hmac: Some(hmac) } - ); - let reply_paths = self.create_blinded_paths(context) - .map_err(|_| Bolt12SemanticError::MissingPaths)?; + let context = MessageContext::Offers(OffersContext::OutboundPayment { + payment_id, + nonce, + hmac: Some(hmac), + }); + let reply_paths = + self.create_blinded_paths(context).map_err(|_| Bolt12SemanticError::MissingPaths)?; // Temporarily removing this to find the best way to integrate the guard // let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self); @@ -1280,15 +1353,15 @@ where } fn enqueue_invoice_request( - &self, - invoice_request: InvoiceRequest, - reply_paths: Vec, + &self, invoice_request: InvoiceRequest, reply_paths: Vec, ) -> Result<(), Bolt12SemanticError> { let mut pending_offers_messages = self.pending_offers_messages.lock().unwrap(); if !invoice_request.paths().is_empty() { reply_paths .iter() - .flat_map(|reply_path| invoice_request.paths().iter().map(move |path| (path, reply_path))) + .flat_map(|reply_path| { + invoice_request.paths().iter().map(move |path| (path, reply_path)) + }) .take(OFFERS_MESSAGE_REQUEST_LIMIT) .for_each(|(path, reply_path)| { let instructions = MessageSendInstructions::WithSpecifiedReplyPath { @@ -1341,7 +1414,7 @@ where /// [`PaymentPreimage`]: crate::types::payment::PaymentPreimage /// [`Router`]: crate::routing::router::Router pub fn request_refund_payment( - &self, refund: &Refund + &self, refund: &Refund, ) -> Result { let expanded_key = &self.inbound_payment_key; let entropy = &*self.entropy_source; @@ -1357,20 +1430,32 @@ where 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( - Some(amount_msats), payment_secret, payment_context, relative_expiry, - ) + let payment_paths = self + .commons + .create_blinded_payment_paths( + Some(amount_msats), + payment_secret, + payment_context, + relative_expiry, + ) .map_err(|_| Bolt12SemanticError::MissingPaths)?; #[cfg(feature = "std")] let builder = refund.respond_using_derived_keys( - payment_paths, payment_hash, expanded_key, entropy + payment_paths, + payment_hash, + expanded_key, + entropy, )?; #[cfg(not(feature = "std"))] let created_at = self.commons.get_highest_seen_timestamp(); #[cfg(not(feature = "std"))] let builder = refund.respond_using_derived_keys_no_std( - payment_paths, payment_hash, created_at, expanded_key, entropy + payment_paths, + payment_hash, + created_at, + expanded_key, + entropy, )?; let builder: InvoiceBuilder = builder.into(); let invoice = builder.allow_mpp().build_and_sign(secp_ctx)?; @@ -1378,9 +1463,12 @@ where 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 + payment_hash: invoice.payment_hash(), + nonce, + hmac, }); - let reply_paths = self.create_blinded_paths(context) + let reply_paths = self + .create_blinded_paths(context) .map_err(|_| Bolt12SemanticError::MissingPaths)?; let mut pending_offers_messages = self.pending_offers_messages.lock().unwrap(); @@ -1396,7 +1484,9 @@ where } else { reply_paths .iter() - .flat_map(|reply_path| refund.paths().iter().map(move |path| (path, reply_path))) + .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 { @@ -1413,10 +1503,10 @@ where Err(()) => Err(Bolt12SemanticError::InvalidAmount), } } - } -impl OffersMessageHandler for OffersMessageFlow +impl OffersMessageHandler + for OffersMessageFlow where ES::Target: EntropySource, OMC::Target: OffersMessageCommons, @@ -1434,7 +1524,8 @@ where let error = match $res { Err(Bolt12PaymentError::UnknownRequiredFeatures) => { log_trace!( - $logger, "Invoice requires unknown features: {:?}", + $logger, + "Invoice requires unknown features: {:?}", $invoice.invoice_features() ); InvoiceError::from(Bolt12SemanticError::UnknownRequiredFeatures) @@ -1450,18 +1541,20 @@ where InvoiceError::from_string(err_msg.to_string()) }, Err(Bolt12PaymentError::UnexpectedInvoice) - | Err(Bolt12PaymentError::DuplicateInvoice) - | Ok(()) => return None, + | Err(Bolt12PaymentError::DuplicateInvoice) + | Ok(()) => return None, }; match responder { - Some(responder) => return Some((OffersMessage::InvoiceError(error), responder.respond())), + Some(responder) => { + return Some((OffersMessage::InvoiceError(error), responder.respond())) + }, None => { log_trace!($logger, "No reply path to send error: {:?}", error); - return None + return None; }, } - }} + }}; } match message { @@ -1479,7 +1572,9 @@ where let invoice_request = match nonce { Some(nonce) => match invoice_request.verify_using_recipient_data( - nonce, expanded_key, secp_ctx, + nonce, + expanded_key, + secp_ctx, ) { Ok(invoice_request) => invoice_request, Err(()) => return None, @@ -1491,20 +1586,30 @@ where }; let amount_msats = match InvoiceBuilder::::amount_msats( - &invoice_request.inner + &invoice_request.inner, ) { Ok(amount_msats) => amount_msats, - Err(error) => return Some((OffersMessage::InvoiceError(error.into()), responder.respond())), + Err(error) => { + return Some(( + OffersMessage::InvoiceError(error.into()), + responder.respond(), + )) + }, }; let relative_expiry = DEFAULT_RELATIVE_EXPIRY.as_secs() as u32; let (payment_hash, payment_secret) = match self.commons.create_inbound_payment( - Some(amount_msats), relative_expiry, None + Some(amount_msats), + relative_expiry, + None, ) { Ok((payment_hash, payment_secret)) => (payment_hash, payment_secret), Err(()) => { let error = Bolt12SemanticError::InvalidAmount; - return Some((OffersMessage::InvoiceError(error.into()), responder.respond())); + return Some(( + OffersMessage::InvoiceError(error.into()), + responder.respond(), + )); }, }; @@ -1513,12 +1618,18 @@ where invoice_request: invoice_request.fields(), }); let payment_paths = match self.commons.create_blinded_payment_paths( - Some(amount_msats), payment_secret, payment_context, relative_expiry + Some(amount_msats), + payment_secret, + payment_context, + relative_expiry, ) { Ok(payment_paths) => payment_paths, Err(()) => { let error = Bolt12SemanticError::MissingPaths; - return Some((OffersMessage::InvoiceError(error.into()), responder.respond())); + return Some(( + OffersMessage::InvoiceError(error.into()), + responder.respond(), + )); }, }; @@ -1527,12 +1638,12 @@ where let response = if invoice_request.keys.is_some() { #[cfg(feature = "std")] - let builder = invoice_request.respond_using_derived_keys( - payment_paths, payment_hash - ); + let builder = invoice_request.respond_using_derived_keys(payment_paths, payment_hash); #[cfg(not(feature = "std"))] let builder = invoice_request.respond_using_derived_keys_no_std( - payment_paths, payment_hash, created_at + payment_paths, + payment_hash, + created_at, ); builder .map(InvoiceBuilder::::from) @@ -1542,9 +1653,7 @@ where #[cfg(feature = "std")] let builder = invoice_request.respond_with(payment_paths, payment_hash); #[cfg(not(feature = "std"))] - let builder = invoice_request.respond_with_no_std( - payment_paths, payment_hash, created_at - ); + let builder = invoice_request.respond_with_no_std(payment_paths, payment_hash, created_at); builder .map(InvoiceBuilder::::from) .and_then(|builder| builder.allow_mpp().build()) @@ -1553,9 +1662,9 @@ where #[cfg(c_bindings)] let mut invoice = invoice; invoice - .sign(|invoice: &UnsignedBolt12Invoice| + .sign(|invoice: &UnsignedBolt12Invoice| { self.commons.sign_bolt12_invoice(invoice) - ) + }) .map_err(InvoiceError::from) }) }; @@ -1564,10 +1673,19 @@ where Ok(invoice) => { let nonce = Nonce::from_entropy_source(&*self.entropy_source); let hmac = payment_hash.hmac_for_offer_payment(nonce, expanded_key); - let context = MessageContext::Offers(OffersContext::InboundPayment { payment_hash, nonce, hmac }); - Some((OffersMessage::Invoice(invoice), responder.respond_with_reply_path(context))) + let context = MessageContext::Offers(OffersContext::InboundPayment { + payment_hash, + nonce, + hmac, + }); + Some(( + OffersMessage::Invoice(invoice), + responder.respond_with_reply_path(context), + )) + }, + Err(error) => { + Some((OffersMessage::InvoiceError(error.into()), responder.respond())) }, - Err(error) => Some((OffersMessage::InvoiceError(error.into()), responder.respond())), } }, OffersMessage::Invoice(invoice) => { @@ -1576,23 +1694,27 @@ where Err(()) => return None, }; - let logger = WithContext::from( - &self.logger, None, None, Some(invoice.payment_hash()), - ); + let logger = + WithContext::from(&self.logger, None, None, Some(invoice.payment_hash())); - let res = self.commons.send_payment_for_verified_bolt12_invoice(&invoice, payment_id); + let res = + self.commons.send_payment_for_verified_bolt12_invoice(&invoice, payment_id); handle_pay_invoice_res!(res, invoice, logger); }, #[cfg(async_payments)] OffersMessage::StaticInvoice(invoice) => { let payment_id = match context { - Some(OffersContext::OutboundPayment { payment_id, nonce, hmac: Some(hmac) }) => { + Some(OffersContext::OutboundPayment { + payment_id, + nonce, + hmac: Some(hmac), + }) => { if payment_id.verify_for_offer_payment(hmac, nonce, expanded_key).is_err() { - return None + return None; } payment_id }, - _ => return None + _ => return None, }; let res = self.initiate_async_payment(&invoice, payment_id); handle_pay_invoice_res!(res, invoice, self.logger); @@ -1612,10 +1734,17 @@ where log_trace!(logger, "Received invoice_error: {}", invoice_error); match context { - Some(OffersContext::OutboundPayment { payment_id, nonce, hmac: Some(hmac) }) => { - if let Ok(()) = payment_id.verify_for_offer_payment(hmac, nonce, expanded_key) { + Some(OffersContext::OutboundPayment { + payment_id, + nonce, + hmac: Some(hmac), + }) => { + if let Ok(()) = + payment_id.verify_for_offer_payment(hmac, nonce, expanded_key) + { self.commons.abandon_payment_with_reason( - payment_id, PaymentFailureReason::InvoiceRequestRejected, + payment_id, + PaymentFailureReason::InvoiceRequestRejected, ); } }, @@ -1628,34 +1757,36 @@ where } fn message_received(&self) { - for (payment_id, retryable_invoice_request) in self - .commons - .release_invoice_requests_awaiting_invoice() + for (payment_id, retryable_invoice_request) in + self.commons.release_invoice_requests_awaiting_invoice() { let RetryableInvoiceRequest { invoice_request, nonce, .. } = retryable_invoice_request; let hmac = payment_id.hmac_for_offer_payment(nonce, &self.inbound_payment_key); let context = MessageContext::Offers(OffersContext::OutboundPayment { payment_id, nonce, - hmac: Some(hmac) + hmac: Some(hmac), }); match self.create_blinded_paths(context) { - Ok(reply_paths) => match self.enqueue_invoice_request(invoice_request, reply_paths) { - Ok(_) => {} + Ok(reply_paths) => match self.enqueue_invoice_request(invoice_request, reply_paths) + { + Ok(_) => {}, Err(_) => { - log_warn!(self.logger, + log_warn!( + self.logger, "Retry failed for an invoice request with payment_id: {}", payment_id ); - } + }, }, Err(_) => { - log_warn!(self.logger, + log_warn!( + self.logger, "Retry failed for an invoice request with payment_id: {}. \ Reason: router could not find a blinded path to include as the reply path", payment_id ); - } + }, } } } @@ -1681,29 +1812,42 @@ where } fn handle_dnssec_proof(&self, message: DNSSECProof, context: DNSResolverContext) { - let offer_opt = self.commons.get_hrn_resolver().handle_dnssec_proof_for_offer(message, context); + let offer_opt = + self.commons.get_hrn_resolver().handle_dnssec_proof_for_offer(message, context); #[cfg_attr(not(feature = "_test_utils"), allow(unused_mut))] if let Some((completed_requests, mut offer)) = offer_opt { for (name, payment_id) in completed_requests { #[cfg(feature = "_test_utils")] - if let Some(replacement_offer) = self.testing_dnssec_proof_offer_resolution_override.lock().unwrap().remove(&name) { + if let Some(replacement_offer) = self + .testing_dnssec_proof_offer_resolution_override + .lock() + .unwrap() + .remove(&name) + { // If we have multiple pending requests we may end up over-using the override // offer, but tests can deal with that. offer = replacement_offer; } - if let Ok(amt_msats) = self.commons.amt_msats_for_payment_awaiting_offer(payment_id) { - let offer_pay_res = - self.pay_for_offer_intern(&offer, None, Some(amt_msats), None, payment_id, Some(name), - |invoice_request, nonce| { - let retryable_invoice_request = RetryableInvoiceRequest { - invoice_request: invoice_request.clone(), - nonce, - needs_retry: true, - }; - self.commons - .received_offer(payment_id, Some(retryable_invoice_request)) - .map_err(|_| Bolt12SemanticError::DuplicatePaymentId) - }); + if let Ok(amt_msats) = self.commons.amt_msats_for_payment_awaiting_offer(payment_id) + { + let offer_pay_res = self.pay_for_offer_intern( + &offer, + None, + Some(amt_msats), + None, + payment_id, + Some(name), + |invoice_request, nonce| { + let retryable_invoice_request = RetryableInvoiceRequest { + invoice_request: invoice_request.clone(), + nonce, + needs_retry: true, + }; + self.commons + .received_offer(payment_id, Some(retryable_invoice_request)) + .map_err(|_| Bolt12SemanticError::DuplicatePaymentId) + }, + ); if offer_pay_res.is_err() { // The offer we tried to pay is the canonical current offer for the name we // wanted to pay. If we can't pay it, there's no way to recover so fail the @@ -1711,7 +1855,8 @@ where // Note that the PaymentFailureReason should be ignored for an // AwaitingInvoice payment. self.commons.abandon_payment_with_reason( - payment_id, PaymentFailureReason::RouteNotFound + payment_id, + PaymentFailureReason::RouteNotFound, ); } } @@ -1734,36 +1879,50 @@ where { fn handle_held_htlc_available( &self, _message: HeldHtlcAvailable, _context: AsyncPaymentsContext, - _responder: Option + _responder: Option, ) -> Option<(ReleaseHeldHtlc, ResponseInstruction)> { - #[cfg(async_payments)] { + #[cfg(async_payments)] + { match _context { AsyncPaymentsContext::InboundPayment { nonce, hmac, path_absolute_expiry } => { if let Err(()) = signer::verify_held_htlc_available_context( - nonce, hmac, &self.inbound_payment_key - ) { return None } - if self.duration_since_epoch() > path_absolute_expiry { return None } + nonce, + hmac, + &self.inbound_payment_key, + ) { + return None; + } + if self.duration_since_epoch() > path_absolute_expiry { + return None; + } }, - _ => return None + _ => return None, } - return _responder.map(|responder| (ReleaseHeldHtlc {}, responder.respond())) + return _responder.map(|responder| (ReleaseHeldHtlc {}, responder.respond())); } #[cfg(not(async_payments))] - return None + return None; } fn handle_release_held_htlc(&self, _message: ReleaseHeldHtlc, _context: AsyncPaymentsContext) { - #[cfg(async_payments)] { + #[cfg(async_payments)] + { let (payment_id, nonce, hmac) = match _context { AsyncPaymentsContext::OutboundPayment { payment_id, hmac, nonce } => { (payment_id, nonce, hmac) }, - _ => return + _ => return, }; - if payment_id.verify_for_async_payment(hmac, nonce, &self.inbound_payment_key).is_err() { return } + if payment_id.verify_for_async_payment(hmac, nonce, &self.inbound_payment_key).is_err() + { + return; + } if let Err(e) = self.commons.send_payment_for_static_invoice(payment_id) { log_trace!( - self.logger, "Failed to release held HTLC with payment id {}: {:?}", payment_id, e + self.logger, + "Failed to release held HTLC with payment id {}: {:?}", + payment_id, + e ); } } @@ -1773,5 +1932,3 @@ where core::mem::take(&mut self.pending_async_payments_messages.lock().unwrap()) } } - - From 928e741900cd369a228769898cb801bb0999b9a7 Mon Sep 17 00:00:00 2001 From: shaavan Date: Sat, 15 Feb 2025 19:18:14 +0530 Subject: [PATCH 14/14] Remove flow.rs from rustfmt exclusion list --- rustfmt_excluded_files | 1 - 1 file changed, 1 deletion(-) diff --git a/rustfmt_excluded_files b/rustfmt_excluded_files index 76b72cc2e33..7899a65ee67 100644 --- a/rustfmt_excluded_files +++ b/rustfmt_excluded_files @@ -43,4 +43,3 @@ lightning/src/routing/router.rs lightning/src/routing/scoring.rs lightning/src/routing/test_utils.rs lightning/src/routing/utxo.rs -lightning/src/offers/flow.rs