From b9645d8477b63860817f53ac37612485563e5193 Mon Sep 17 00:00:00 2001 From: shaavan Date: Wed, 18 Sep 2024 17:32:05 +0530 Subject: [PATCH 1/6] Introduce `BlindedPathType` 1. Introduce a new enum that allows the user to specify the type of Blinded Path (whether Compact or Full-Length) when creating a Blinded Path. 2. This PR utilizes this new enum in two ways in the upcoming commits: - Allows explicitly providing the type of Blinded Path to be created instead of relying on the absolute expiry time for offers and refunds. - Simplifies the Blinded Path creation process by enabling a single function flow for creating both types of Blinded Path. --- lightning/src/ln/channelmanager.rs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 3914384ca82..fa3bdf652af 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -223,6 +223,34 @@ pub enum PendingHTLCRouting { }, } +/// Represents the types of [`BlindedMessagePath`] that can be created. +pub enum BlindedPathType { + /// A compact version of [`BlindedMessagePath`]. + /// + /// Compact blinded paths use a [`ShortChannelId`] instead of a [`NodeId`] to represent forward nodes. + /// This approach drastically reduces the size of each individual forward packet but requires + /// knowledge of the local channel graph to find the corresponding channel for each node. + /// + /// Compact blinded paths are especially useful when size is a constraint, such as when offers + /// and refund information must be represented in QR code form. + /// + /// [`ShortChannelId`]: crate::blinded_path::message::NextMessageHop::ShortChannelId + /// [`NodeId`]: crate::blinded_path::message::NextMessageHop::NodeId + Compact, + + /// A full-length version of [`BlindedMessagePath`]. + /// + /// Unlike compact blinded paths, full-length paths use each individual forward node's [`NodeId`] for representation. + /// This increases the size of each forward packet as more space is required for representation. + /// However, it does not require knowledge of the local channel graph to create the path. + /// + /// Full-length blinded paths are useful when onion messages are communicated over the wire and size constraints are not an issue, + /// such as when sending a response to an onion message. + /// + /// [`NodeId`]: crate::blinded_path::message::NextMessageHop::NodeId + Full, +} + /// Information used to forward or fail this HTLC that is being forwarded within a blinded path. #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] pub struct BlindedForward { From fab881681ffc84aab54ff79ebe126c0c879cbf88 Mon Sep 17 00:00:00 2001 From: shaavan Date: Wed, 18 Sep 2024 17:45:08 +0530 Subject: [PATCH 2/6] Allow explicit specification of Blinded Path type in `create_offer_builder` 1. This commit introduces flexibility for the user to specify the type of Blinded Path they want, rather than relying on the Offers' absolute expiry to determine the Blinded Path type. 2. It also adds support for creating an Offer without a Blinded Path, using its signing public key for responding to the offer instead. --- lightning/src/ln/channelmanager.rs | 27 ++++++++------- lightning/src/ln/offers_tests.rs | 54 ++++++++++++++++-------------- 2 files changed, 43 insertions(+), 38 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index fa3bdf652af..d6ebcba3892 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -1880,14 +1880,14 @@ where /// /// ``` /// # use lightning::events::{Event, EventsProvider, PaymentPurpose}; -/// # use lightning::ln::channelmanager::AChannelManager; +/// # use lightning::ln::channelmanager::{AChannelManager, BlindedPathType}; /// # use lightning::offers::parse::Bolt12SemanticError; /// # /// # fn example(channel_manager: T) -> Result<(), Bolt12SemanticError> { /// # let channel_manager = channel_manager.get_cm(); -/// # let absolute_expiry = None; +/// # let blinded_path = BlindedPathType::Full; /// let offer = channel_manager -/// .create_offer_builder(absolute_expiry)? +/// .create_offer_builder(Some(blinded_path))? /// # ; /// # // Needed for compiling for c_bindings /// # let builder: lightning::offers::offer::OfferBuilder<_, _> = offer.into(); @@ -9149,7 +9149,7 @@ macro_rules! create_offer_builder { ($self: ident, $builder: ty) => { /// [`Offer`]: crate::offers::offer::Offer /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest pub fn create_offer_builder( - &$self, absolute_expiry: Option + &$self, blinded_path: Option, ) -> Result<$builder, Bolt12SemanticError> { let node_id = $self.get_our_node_id(); let expanded_key = &$self.inbound_payment_key; @@ -9158,17 +9158,20 @@ macro_rules! create_offer_builder { ($self: ident, $builder: ty) => { let nonce = Nonce::from_entropy_source(entropy); let context = OffersContext::InvoiceRequest { nonce }; - let path = $self.create_blinded_paths_using_absolute_expiry(context, absolute_expiry) + + let builder = OfferBuilder::deriving_signing_pubkey(node_id, expanded_key, nonce, secp_ctx) + .chain_hash($self.chain_hash); + + if let Some(path_type) = blinded_path { + let path = match path_type { + BlindedPathType::Compact => $self.create_compact_blinded_paths(context), + BlindedPathType::Full => $self.create_blinded_paths(MessageContext::Offers(context)), + } .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), - }; + return Ok(builder.path(path)); + } Ok(builder.into()) } diff --git a/lightning/src/ln/offers_tests.rs b/lightning/src/ln/offers_tests.rs index b667ce2c05d..49f755360fb 100644 --- a/lightning/src/ln/offers_tests.rs +++ b/lightning/src/ln/offers_tests.rs @@ -48,7 +48,7 @@ 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, MessageSendEventsProvider, PaymentFailureReason, PaymentPurpose}; -use crate::ln::channelmanager::{Bolt12PaymentError, MAX_SHORT_LIVED_RELATIVE_EXPIRY, PaymentId, RecentPaymentDetails, Retry, self}; +use crate::ln::channelmanager::{BlindedPathType, Bolt12PaymentError, MAX_SHORT_LIVED_RELATIVE_EXPIRY, PaymentId, RecentPaymentDetails, Retry, self}; use crate::types::features::Bolt12InvoiceFeatures; use crate::ln::functional_test_utils::*; use crate::ln::inbound_payment::ExpandedKey; @@ -59,7 +59,7 @@ use crate::offers::invoice_error::InvoiceError; use crate::offers::invoice_request::{InvoiceRequest, InvoiceRequestFields}; use crate::offers::nonce::Nonce; use crate::offers::parse::Bolt12SemanticError; -use crate::onion_message::messenger::{Destination, PeeledOnion, MessageSendInstructions}; +use crate::onion_message::messenger::{Destination, MessageSendInstructions, PeeledOnion}; use crate::onion_message::offers::OffersMessage; use crate::onion_message::packet::ParsedOnionMessageContents; use crate::routing::gossip::{NodeAlias, NodeId}; @@ -297,7 +297,7 @@ fn prefers_non_tor_nodes_in_blinded_paths() { announce_node_address(charlie, &[alice, bob, david, &nodes[4], &nodes[5]], tor.clone()); let offer = bob.node - .create_offer_builder(None).unwrap() + .create_offer_builder(Some(BlindedPathType::Full)).unwrap() .amount_msats(10_000_000) .build().unwrap(); assert_ne!(offer.issuer_signing_pubkey(), Some(bob_id)); @@ -313,7 +313,7 @@ fn prefers_non_tor_nodes_in_blinded_paths() { announce_node_address(&nodes[5], &[alice, bob, charlie, david, &nodes[4]], tor.clone()); let offer = bob.node - .create_offer_builder(None).unwrap() + .create_offer_builder(Some(BlindedPathType::Full)).unwrap() .amount_msats(10_000_000) .build().unwrap(); assert_ne!(offer.issuer_signing_pubkey(), Some(bob_id)); @@ -364,7 +364,7 @@ fn prefers_more_connected_nodes_in_blinded_paths() { disconnect_peers(david, &[bob, &nodes[4], &nodes[5]]); let offer = bob.node - .create_offer_builder(None).unwrap() + .create_offer_builder(Some(BlindedPathType::Full)).unwrap() .amount_msats(10_000_000) .build().unwrap(); assert_ne!(offer.issuer_signing_pubkey(), Some(bob_id)); @@ -391,7 +391,8 @@ fn creates_short_lived_offer() { let absolute_expiry = alice.node.duration_since_epoch() + MAX_SHORT_LIVED_RELATIVE_EXPIRY; let offer = alice.node - .create_offer_builder(Some(absolute_expiry)).unwrap() + .create_offer_builder(Some(BlindedPathType::Compact)).unwrap() + .absolute_expiry(absolute_expiry) .build().unwrap(); assert_eq!(offer.absolute_expiry(), Some(absolute_expiry)); assert!(!offer.paths().is_empty()); @@ -418,8 +419,8 @@ fn creates_long_lived_offer() { let absolute_expiry = alice.node.duration_since_epoch() + MAX_SHORT_LIVED_RELATIVE_EXPIRY + Duration::from_secs(1); let offer = alice.node - .create_offer_builder(Some(absolute_expiry)) - .unwrap() + .create_offer_builder(Some(BlindedPathType::Full)).unwrap() + .absolute_expiry(absolute_expiry) .build().unwrap(); assert_eq!(offer.absolute_expiry(), Some(absolute_expiry)); assert!(!offer.paths().is_empty()); @@ -428,7 +429,7 @@ fn creates_long_lived_offer() { } let offer = alice.node - .create_offer_builder(None).unwrap() + .create_offer_builder(Some(BlindedPathType::Full)).unwrap() .build().unwrap(); assert_eq!(offer.absolute_expiry(), None); assert!(!offer.paths().is_empty()); @@ -532,7 +533,7 @@ fn creates_and_pays_for_offer_using_two_hop_blinded_path() { disconnect_peers(david, &[bob, &nodes[4], &nodes[5]]); let offer = alice.node - .create_offer_builder(None) + .create_offer_builder(Some(BlindedPathType::Full)) .unwrap() .amount_msats(10_000_000) .build().unwrap(); @@ -701,7 +702,7 @@ fn creates_and_pays_for_offer_using_one_hop_blinded_path() { let bob_id = bob.node.get_our_node_id(); let offer = alice.node - .create_offer_builder(None).unwrap() + .create_offer_builder(Some(BlindedPathType::Full)).unwrap() .amount_msats(10_000_000) .build().unwrap(); assert_ne!(offer.issuer_signing_pubkey(), Some(alice_id)); @@ -823,7 +824,7 @@ fn pays_for_offer_without_blinded_paths() { let bob_id = bob.node.get_our_node_id(); let offer = alice.node - .create_offer_builder(None).unwrap() + .create_offer_builder(Some(BlindedPathType::Full)).unwrap() .clear_paths() .amount_msats(10_000_000) .build().unwrap(); @@ -946,7 +947,7 @@ fn send_invoice_requests_with_distinct_reply_path() { disconnect_peers(david, &[bob, &nodes[4], &nodes[5]]); let offer = alice.node - .create_offer_builder(None) + .create_offer_builder(Some(BlindedPathType::Full)) .unwrap() .amount_msats(10_000_000) .build().unwrap(); @@ -1082,7 +1083,7 @@ fn creates_and_pays_for_offer_with_retry() { let bob_id = bob.node.get_our_node_id(); let offer = alice.node - .create_offer_builder(None).unwrap() + .create_offer_builder(Some(BlindedPathType::Full)).unwrap() .amount_msats(10_000_000) .build().unwrap(); assert_ne!(offer.issuer_signing_pubkey(), Some(alice_id)); @@ -1157,7 +1158,7 @@ fn pays_bolt12_invoice_asynchronously() { let bob_id = bob.node.get_our_node_id(); let offer = alice.node - .create_offer_builder(None).unwrap() + .create_offer_builder(Some(BlindedPathType::Full)).unwrap() .amount_msats(10_000_000) .build().unwrap(); @@ -1241,7 +1242,7 @@ fn creates_offer_with_blinded_path_using_unannounced_introduction_node() { let bob_id = bob.node.get_our_node_id(); let offer = alice.node - .create_offer_builder(None).unwrap() + .create_offer_builder(Some(BlindedPathType::Full)).unwrap() .amount_msats(10_000_000) .build().unwrap(); assert_ne!(offer.issuer_signing_pubkey(), Some(alice_id)); @@ -1370,7 +1371,7 @@ fn fails_authentication_when_handling_invoice_request() { disconnect_peers(david, &[bob, &nodes[4], &nodes[5]]); let offer = alice.node - .create_offer_builder(None) + .create_offer_builder(Some(BlindedPathType::Full)) .unwrap() .amount_msats(10_000_000) .build().unwrap(); @@ -1382,7 +1383,7 @@ fn fails_authentication_when_handling_invoice_request() { } let invalid_path = alice.node - .create_offer_builder(None) + .create_offer_builder(Some(BlindedPathType::Full)) .unwrap() .build().unwrap() .paths().first().unwrap() @@ -1482,7 +1483,7 @@ fn fails_authentication_when_handling_invoice_for_offer() { disconnect_peers(david, &[bob, &nodes[4], &nodes[5]]); let offer = alice.node - .create_offer_builder(None) + .create_offer_builder(Some(BlindedPathType::Full)) .unwrap() .amount_msats(10_000_000) .build().unwrap(); @@ -1679,7 +1680,7 @@ fn fails_creating_or_paying_for_offer_without_connected_peers() { 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)) { + match alice.node.create_offer_builder(Some(BlindedPathType::Full)) { Ok(_) => panic!("Expected error"), Err(e) => assert_eq!(e, Bolt12SemanticError::MissingPaths), } @@ -1689,7 +1690,8 @@ fn fails_creating_or_paying_for_offer_without_connected_peers() { reconnect_nodes(args); let offer = alice.node - .create_offer_builder(Some(absolute_expiry)).unwrap() + .create_offer_builder(Some(BlindedPathType::Full)).unwrap() + .absolute_expiry(absolute_expiry) .amount_msats(10_000_000) .build().unwrap(); @@ -1793,7 +1795,7 @@ fn fails_creating_invoice_request_for_unsupported_chain() { let bob = &nodes[1]; let offer = alice.node - .create_offer_builder(None).unwrap() + .create_offer_builder(Some(BlindedPathType::Full)).unwrap() .clear_chains() .chain(Network::Signet) .build().unwrap(); @@ -1852,7 +1854,7 @@ fn fails_creating_invoice_request_without_blinded_reply_path() { disconnect_peers(david, &[bob, charlie, &nodes[4], &nodes[5]]); let offer = alice.node - .create_offer_builder(None).unwrap() + .create_offer_builder(Some(BlindedPathType::Full)).unwrap() .amount_msats(10_000_000) .build().unwrap(); @@ -1886,7 +1888,7 @@ fn fails_creating_invoice_request_with_duplicate_payment_id() { disconnect_peers(alice, &[charlie, david, &nodes[4], &nodes[5]]); let offer = alice.node - .create_offer_builder(None).unwrap() + .create_offer_builder(Some(BlindedPathType::Full)).unwrap() .amount_msats(10_000_000) .build().unwrap(); @@ -1972,7 +1974,7 @@ fn fails_sending_invoice_without_blinded_payment_paths_for_offer() { disconnect_peers(david, &[bob, &nodes[4], &nodes[5]]); let offer = alice.node - .create_offer_builder(None).unwrap() + .create_offer_builder(Some(BlindedPathType::Full)).unwrap() .amount_msats(10_000_000) .build().unwrap(); @@ -2181,7 +2183,7 @@ fn fails_paying_invoice_with_unknown_required_features() { disconnect_peers(david, &[bob, &nodes[4], &nodes[5]]); let offer = alice.node - .create_offer_builder(None).unwrap() + .create_offer_builder(Some(BlindedPathType::Full)).unwrap() .amount_msats(10_000_000) .build().unwrap(); From e2974446823729f3304787333aad2e19ac98a210 Mon Sep 17 00:00:00 2001 From: shaavan Date: Wed, 18 Sep 2024 17:52:22 +0530 Subject: [PATCH 3/6] Allow Explicit Specification of Blinded Path Type in Refund Similar to the offer case, this commit introduces the ability to explicitly specify the Blinded Path type. --- lightning/src/ln/channelmanager.rs | 49 ++++++++++++------------------ lightning/src/ln/offers_tests.rs | 32 +++++++++---------- 2 files changed, 36 insertions(+), 45 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index d6ebcba3892..2f974e2a7f5 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -1983,7 +1983,7 @@ where /// ``` /// # use core::time::Duration; /// # use lightning::events::{Event, EventsProvider}; -/// # use lightning::ln::channelmanager::{AChannelManager, PaymentId, RecentPaymentDetails, Retry}; +/// # use lightning::ln::channelmanager::{AChannelManager, BlindedPathType, PaymentId, RecentPaymentDetails, Retry}; /// # use lightning::offers::parse::Bolt12SemanticError; /// # /// # fn example( @@ -1992,9 +1992,10 @@ where /// # ) -> Result<(), Bolt12SemanticError> { /// # let channel_manager = channel_manager.get_cm(); /// let payment_id = PaymentId([42; 32]); +/// let blinded_path = Some(BlindedPathType::Full); /// let refund = channel_manager /// .create_refund_builder( -/// amount_msats, absolute_expiry, payment_id, retry, max_total_routing_fee_msat +/// amount_msats, absolute_expiry, payment_id, retry, max_total_routing_fee_msat, blinded_path /// )? /// # ; /// # // Needed for compiling for c_bindings @@ -9225,7 +9226,7 @@ macro_rules! create_refund_builder { ($self: ident, $builder: ty) => { /// [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 + retry_strategy: Retry, max_total_routing_fee_msat: Option, blinded_path: Option ) -> Result<$builder, Bolt12SemanticError> { let node_id = $self.get_our_node_id(); let expanded_key = &$self.inbound_payment_key; @@ -9234,16 +9235,24 @@ macro_rules! create_refund_builder { ($self: ident, $builder: ty) => { 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)) + + let mut 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); + + if let Some(path_type) = blinded_path { + let path = match path_type { + BlindedPathType::Compact => $self.create_compact_blinded_paths(context), + BlindedPathType::Full => $self.create_blinded_paths(MessageContext::Offers(context)), + } .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); + builder = builder.path(path) + } let _persistence_guard = PersistenceNotifierGuard::notify_on_drop($self); @@ -9627,25 +9636,7 @@ 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)) - } - } - + #[cfg(test)] pub(super) fn duration_since_epoch(&self) -> Duration { #[cfg(not(feature = "std"))] let now = Duration::from_secs( diff --git a/lightning/src/ln/offers_tests.rs b/lightning/src/ln/offers_tests.rs index 49f755360fb..5d1218f3a02 100644 --- a/lightning/src/ln/offers_tests.rs +++ b/lightning/src/ln/offers_tests.rs @@ -455,7 +455,7 @@ fn creates_short_lived_refund() { let absolute_expiry = bob.node.duration_since_epoch() + MAX_SHORT_LIVED_RELATIVE_EXPIRY; let payment_id = PaymentId([1; 32]); let refund = bob.node - .create_refund_builder(10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None) + .create_refund_builder(10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None, Some(BlindedPathType::Compact)) .unwrap() .build().unwrap(); assert_eq!(refund.absolute_expiry(), Some(absolute_expiry)); @@ -484,7 +484,7 @@ fn creates_long_lived_refund() { + Duration::from_secs(1); let payment_id = PaymentId([1; 32]); let refund = bob.node - .create_refund_builder(10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None) + .create_refund_builder(10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None, Some(BlindedPathType::Full)) .unwrap() .build().unwrap(); assert_eq!(refund.absolute_expiry(), Some(absolute_expiry)); @@ -642,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 - .create_refund_builder(10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None) + .create_refund_builder(10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None, Some(BlindedPathType::Full)) .unwrap() .build().unwrap(); assert_eq!(refund.amount_msats(), 10_000_000); @@ -770,7 +770,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 - .create_refund_builder(10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None) + .create_refund_builder(10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None, Some(BlindedPathType::Full)) .unwrap() .build().unwrap(); assert_eq!(refund.amount_msats(), 10_000_000); @@ -878,7 +878,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 - .create_refund_builder(10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None) + .create_refund_builder(10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None, Some(BlindedPathType::Full)) .unwrap() .clear_paths() .build().unwrap(); @@ -1033,7 +1033,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 - .create_refund_builder(10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None) + .create_refund_builder(10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None, Some(BlindedPathType::Full)) .unwrap() .build().unwrap(); assert_ne!(refund.payer_signing_pubkey(), alice_id); @@ -1309,7 +1309,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 - .create_refund_builder(10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None) + .create_refund_builder(10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None, Some(BlindedPathType::Full)) .unwrap() .build().unwrap(); assert_ne!(refund.payer_signing_pubkey(), bob_id); @@ -1592,7 +1592,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 - .create_refund_builder(10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None) + .create_refund_builder(10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None, Some(BlindedPathType::Full)) .unwrap() .build().unwrap(); assert_ne!(refund.payer_signing_pubkey(), david_id); @@ -1626,7 +1626,7 @@ fn fails_authentication_when_handling_invoice_for_refund() { let invalid_path = refund.paths().first().unwrap().clone(); let payment_id = PaymentId([2; 32]); let refund = david.node - .create_refund_builder(10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None) + .create_refund_builder(10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None, Some(BlindedPathType::Full)) .unwrap() .build().unwrap(); assert_ne!(refund.payer_signing_pubkey(), david_id); @@ -1754,7 +1754,7 @@ fn fails_creating_refund_or_sending_invoice_without_connected_peers() { let absolute_expiry = david.node.duration_since_epoch() + MAX_SHORT_LIVED_RELATIVE_EXPIRY; let payment_id = PaymentId([1; 32]); match david.node.create_refund_builder( - 10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None + 10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None, Some(BlindedPathType::Full) ) { Ok(_) => panic!("Expected error"), Err(e) => assert_eq!(e, Bolt12SemanticError::MissingPaths), @@ -1765,7 +1765,7 @@ fn fails_creating_refund_or_sending_invoice_without_connected_peers() { reconnect_nodes(args); let refund = david.node - .create_refund_builder(10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None) + .create_refund_builder(10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None, Some(BlindedPathType::Full)) .unwrap() .build().unwrap(); @@ -1823,7 +1823,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 - .create_refund_builder(10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None) + .create_refund_builder(10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None, Some(BlindedPathType::Full)) .unwrap() .chain(Network::Signet) .build().unwrap(); @@ -1921,13 +1921,13 @@ fn fails_creating_refund_with_duplicate_payment_id() { let payment_id = PaymentId([1; 32]); assert!( nodes[0].node.create_refund_builder( - 10_000, absolute_expiry, payment_id, Retry::Attempts(0), None + 10_000, absolute_expiry, payment_id, Retry::Attempts(0), None, Some(BlindedPathType::Full) ).is_ok() ); expect_recent_payment!(nodes[0], RecentPaymentDetails::AwaitingInvoice, payment_id); match nodes[0].node.create_refund_builder( - 10_000, absolute_expiry, payment_id, Retry::Attempts(0), None + 10_000, absolute_expiry, payment_id, Retry::Attempts(0), None, Some(BlindedPathType::Full) ) { Ok(_) => panic!("Expected error"), Err(e) => assert_eq!(e, Bolt12SemanticError::DuplicatePaymentId), @@ -2047,7 +2047,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 - .create_refund_builder(10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None) + .create_refund_builder(10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None, Some(BlindedPathType::Full)) .unwrap() .build().unwrap(); @@ -2096,7 +2096,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 - .create_refund_builder(10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None) + .create_refund_builder(10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None, Some(BlindedPathType::Full)) .unwrap() .build().unwrap(); expect_recent_payment!(david, RecentPaymentDetails::AwaitingInvoice, payment_id); From ffb13c3f439a3235caf1ebfc1e11506f1bf9e07d Mon Sep 17 00:00:00 2001 From: shaavan Date: Sat, 19 Oct 2024 17:47:49 +0530 Subject: [PATCH 4/6] Update DefaultMessageRouter's implmentation --- lightning/src/onion_message/messenger.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index 8162c55ac08..a9a9ef36c5f 100644 --- a/lightning/src/onion_message/messenger.rs +++ b/lightning/src/onion_message/messenger.rs @@ -628,13 +628,20 @@ where Self::create_blinded_paths_from_iter(network_graph, recipient, context, peers.into_iter(), entropy_source, secp_ctx, false) } + /// This implementation returns only a single `BlindedMessagePath` wrapped in a `Vec`. + /// It is intended for space-constrained scenarios where only one path is needed + /// to minimize overhead. By returning a single path, it ensures reduced memory + /// usage while maintaining the essential functionality of blinded message paths. pub(crate) fn create_compact_blinded_paths< T: secp256k1::Signing + secp256k1::Verification >( network_graph: &G, recipient: PublicKey, context: MessageContext, peers: Vec, entropy_source: &ES, secp_ctx: &Secp256k1, ) -> Result, ()> { - Self::create_blinded_paths_from_iter(network_graph, recipient, context, peers.into_iter(), entropy_source, secp_ctx, true) + let path = Self::create_blinded_paths_from_iter(network_graph, recipient, context, peers.into_iter(), entropy_source, secp_ctx, true) + .and_then(|paths| paths.into_iter().next().ok_or(()))?; + + Ok(vec![path]) } } From 44e9789c1e3acd3def36fd848b4fd575726f2fd7 Mon Sep 17 00:00:00 2001 From: shaavan Date: Sat, 19 Oct 2024 17:52:40 +0530 Subject: [PATCH 5/6] Use all the paths returned by MessageRouter - Instead of appending just a single blinded path to offers and refunds, update the code to append all paths returned by MessageRouter. - This change allows MessageRouter to control the number of blinded paths appended to offers and refunds, providing flexibility for them to contain more than one path. --- lightning/src/ln/channelmanager.rs | 16 +++++++++------- lightning/src/ln/offers_tests.rs | 4 ++-- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 2f974e2a7f5..5abdf52185b 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -9160,18 +9160,19 @@ macro_rules! create_offer_builder { ($self: ident, $builder: ty) => { let nonce = Nonce::from_entropy_source(entropy); let context = OffersContext::InvoiceRequest { nonce }; - let builder = OfferBuilder::deriving_signing_pubkey(node_id, expanded_key, nonce, secp_ctx) + let mut builder = OfferBuilder::deriving_signing_pubkey(node_id, expanded_key, nonce, secp_ctx) .chain_hash($self.chain_hash); if let Some(path_type) = blinded_path { - let path = match path_type { + let paths = match path_type { BlindedPathType::Compact => $self.create_compact_blinded_paths(context), BlindedPathType::Full => $self.create_blinded_paths(MessageContext::Offers(context)), } - .and_then(|paths| paths.into_iter().next().ok_or(())) .map_err(|_| Bolt12SemanticError::MissingPaths)?; - return Ok(builder.path(path)); + builder = paths.into_iter().fold(builder, |builder, path| { + builder.path(path) + }); } Ok(builder.into()) @@ -9244,14 +9245,15 @@ macro_rules! create_refund_builder { ($self: ident, $builder: ty) => { .absolute_expiry(absolute_expiry); if let Some(path_type) = blinded_path { - let path = match path_type { + let paths = match path_type { BlindedPathType::Compact => $self.create_compact_blinded_paths(context), BlindedPathType::Full => $self.create_blinded_paths(MessageContext::Offers(context)), } - .and_then(|paths| paths.into_iter().next().ok_or(())) .map_err(|_| Bolt12SemanticError::MissingPaths)?; - builder = builder.path(path) + builder = paths.into_iter().fold(builder, |builder, path| { + builder.path(path) + }) } let _persistence_guard = PersistenceNotifierGuard::notify_on_drop($self); diff --git a/lightning/src/ln/offers_tests.rs b/lightning/src/ln/offers_tests.rs index 5d1218f3a02..08b58c8360c 100644 --- a/lightning/src/ln/offers_tests.rs +++ b/lightning/src/ln/offers_tests.rs @@ -369,8 +369,8 @@ fn prefers_more_connected_nodes_in_blinded_paths() { .build().unwrap(); assert_ne!(offer.issuer_signing_pubkey(), Some(bob_id)); assert!(!offer.paths().is_empty()); - for path in offer.paths() { - let introduction_node_id = resolve_introduction_node(david, &path); + if let Some(first_path) = offer.paths().iter().next() { + let introduction_node_id = resolve_introduction_node(david, &first_path); assert_eq!(introduction_node_id, nodes[4].node.get_our_node_id()); } } From 863dcd62c78108efa88fc9180412ebc2d50df813 Mon Sep 17 00:00:00 2001 From: shaavan Date: Thu, 3 Oct 2024 17:34:08 +0530 Subject: [PATCH 6/6] Introduce Tests for Offers and Refunds Without Blinded Paths --- lightning/src/ln/offers_tests.rs | 47 ++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/lightning/src/ln/offers_tests.rs b/lightning/src/ln/offers_tests.rs index 08b58c8360c..c58998074d9 100644 --- a/lightning/src/ln/offers_tests.rs +++ b/lightning/src/ln/offers_tests.rs @@ -255,6 +255,53 @@ fn extract_invoice_error<'a, 'b, 'c>( } } +/// Checks that an offer can be created with no blinded paths. +#[test] +fn create_offer_with_no_blinded_path() { + 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, None]); + 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 offer = alice.node + .create_offer_builder(None).unwrap() + .amount_msats(10_000_000) + .build().unwrap(); + assert_eq!(offer.issuer_signing_pubkey(), Some(alice_id)); + assert!(offer.paths().is_empty()); +} + +/// Checks that a refund can be created with no blinded paths. +#[test] +fn create_refund_with_no_blinded_path() { + 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, None]); + 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 absolute_expiry = Duration::from_secs(u64::MAX); + let payment_id = PaymentId([1; 32]); + + let refund = alice.node + .create_refund_builder(10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None, None) + .unwrap() + .build().unwrap(); + assert_eq!(refund.amount_msats(), 10_000_000); + assert_eq!(refund.absolute_expiry(), Some(absolute_expiry)); + assert_eq!(refund.payer_signing_pubkey(), alice_id); + assert!(refund.paths().is_empty()); +} + /// Checks that blinded paths without Tor-only nodes are preferred when constructing an offer. #[test] fn prefers_non_tor_nodes_in_blinded_paths() {