From defc54096d366f2903bf99e5d23e3e34c580eeee Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Wed, 22 May 2024 14:43:05 -0500 Subject: [PATCH 1/7] Move DefaultMessageRouter::create_blinded_paths An upcoming change to the MessageRouter trait will require reusing DefaultMessageRouter::create_blinded_paths. Move the code to a utility function so facilitate this. --- lightning/src/onion_message/messenger.rs | 93 +++++++++++++----------- 1 file changed, 51 insertions(+), 42 deletions(-) diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index eb4a837feb6..50049866325 100644 --- a/lightning/src/onion_message/messenger.rs +++ b/lightning/src/onion_message/messenger.rs @@ -449,50 +449,12 @@ where pub fn new(network_graph: G, entropy_source: ES) -> Self { Self { network_graph, entropy_source } } -} - -impl>, L: Deref, ES: Deref> MessageRouter for DefaultMessageRouter -where - L::Target: Logger, - ES::Target: EntropySource, -{ - fn find_path( - &self, sender: PublicKey, peers: Vec, mut destination: Destination - ) -> Result { - let network_graph = self.network_graph.deref().read_only(); - destination.resolve(&network_graph); - - let first_node = match destination.first_node() { - Some(first_node) => first_node, - None => return Err(()), - }; - - if peers.contains(&first_node) || sender == first_node { - Ok(OnionMessagePath { - intermediate_nodes: vec![], destination, first_node_addresses: None - }) - } else { - let node_details = network_graph - .node(&NodeId::from_pubkey(&first_node)) - .and_then(|node_info| node_info.announcement_info.as_ref()) - .map(|announcement_info| (announcement_info.features(), announcement_info.addresses())); - - match node_details { - Some((features, addresses)) if features.supports_onion_messages() && addresses.len() > 0 => { - let first_node_addresses = Some(addresses.clone()); - Ok(OnionMessagePath { - intermediate_nodes: vec![], destination, first_node_addresses - }) - }, - _ => Err(()), - } - } - } - fn create_blinded_paths< + fn create_blinded_paths_from_iter< + I: Iterator, T: secp256k1::Signing + secp256k1::Verification >( - &self, recipient: PublicKey, peers: Vec, secp_ctx: &Secp256k1, + &self, recipient: PublicKey, peers: I, secp_ctx: &Secp256k1, ) -> Result, ()> { // Limit the number of blinded paths that are computed. const MAX_PATHS: usize = 3; @@ -505,7 +467,7 @@ where let is_recipient_announced = network_graph.nodes().contains_key(&NodeId::from_pubkey(&recipient)); - let mut peer_info = peers.into_iter() + let mut peer_info = peers // Limit to peers with announced channels .filter_map(|peer| network_graph @@ -548,6 +510,53 @@ where } } +impl>, L: Deref, ES: Deref> MessageRouter for DefaultMessageRouter +where + L::Target: Logger, + ES::Target: EntropySource, +{ + fn find_path( + &self, sender: PublicKey, peers: Vec, mut destination: Destination + ) -> Result { + let network_graph = self.network_graph.deref().read_only(); + destination.resolve(&network_graph); + + let first_node = match destination.first_node() { + Some(first_node) => first_node, + None => return Err(()), + }; + + if peers.contains(&first_node) || sender == first_node { + Ok(OnionMessagePath { + intermediate_nodes: vec![], destination, first_node_addresses: None + }) + } else { + let node_details = network_graph + .node(&NodeId::from_pubkey(&first_node)) + .and_then(|node_info| node_info.announcement_info.as_ref()) + .map(|announcement_info| (announcement_info.features(), announcement_info.addresses())); + + match node_details { + Some((features, addresses)) if features.supports_onion_messages() && addresses.len() > 0 => { + let first_node_addresses = Some(addresses.clone()); + Ok(OnionMessagePath { + intermediate_nodes: vec![], destination, first_node_addresses + }) + }, + _ => Err(()), + } + } + } + + fn create_blinded_paths< + T: secp256k1::Signing + secp256k1::Verification + >( + &self, recipient: PublicKey, peers: Vec, secp_ctx: &Secp256k1, + ) -> Result, ()> { + self.create_blinded_paths_from_iter(recipient, peers.into_iter(), secp_ctx) + } +} + /// A path for sending an [`OnionMessage`]. #[derive(Clone)] pub struct OnionMessagePath { From 532617188577150cbd974401b10d2b4e0c2e0809 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Wed, 22 May 2024 15:32:32 -0500 Subject: [PATCH 2/7] Refactor MessageRouter::create_blinded_paths Using compact blinded paths isn't always necessary or desirable. For instance, reply paths are communicated via onion messages where space isn't a premium unlike in QR codes. Additionally, long-lived paths could become invalid if the channel associated with the SCID is closed. Refactor MessageRouter::create_blinded_paths into two methods: one for compact blinded paths and one for normal blinded paths. --- fuzz/src/chanmon_consistency.rs | 3 +- fuzz/src/full_stack.rs | 3 +- fuzz/src/onion_message.rs | 3 +- lightning/src/ln/channelmanager.rs | 12 ++--- lightning/src/onion_message/messenger.rs | 56 +++++++++++++++++++----- lightning/src/routing/router.rs | 10 ++++- lightning/src/util/test_utils.rs | 18 +++++++- 7 files changed, 80 insertions(+), 25 deletions(-) diff --git a/fuzz/src/chanmon_consistency.rs b/fuzz/src/chanmon_consistency.rs index 1bcba1fbf37..1cdf617fa07 100644 --- a/fuzz/src/chanmon_consistency.rs +++ b/fuzz/src/chanmon_consistency.rs @@ -34,7 +34,6 @@ use bitcoin::hashes::sha256d::Hash as Sha256dHash; use bitcoin::hash_types::BlockHash; use lightning::blinded_path::BlindedPath; -use lightning::blinded_path::message::ForwardNode; use lightning::blinded_path::payment::ReceiveTlvs; use lightning::chain; use lightning::chain::{BestBlock, ChannelMonitorUpdateStatus, chainmonitor, channelmonitor, Confirm, Watch}; @@ -124,7 +123,7 @@ impl MessageRouter for FuzzRouter { } fn create_blinded_paths( - &self, _recipient: PublicKey, _peers: Vec, _secp_ctx: &Secp256k1, + &self, _recipient: PublicKey, _peers: Vec, _secp_ctx: &Secp256k1, ) -> Result, ()> { unreachable!() } diff --git a/fuzz/src/full_stack.rs b/fuzz/src/full_stack.rs index 2ae5ba2225d..bdd29be9129 100644 --- a/fuzz/src/full_stack.rs +++ b/fuzz/src/full_stack.rs @@ -31,7 +31,6 @@ use bitcoin::hashes::sha256d::Hash as Sha256dHash; use bitcoin::hash_types::{Txid, BlockHash}; use lightning::blinded_path::BlindedPath; -use lightning::blinded_path::message::ForwardNode; use lightning::blinded_path::payment::ReceiveTlvs; use lightning::chain; use lightning::chain::{BestBlock, ChannelMonitorUpdateStatus, Confirm, Listen}; @@ -162,7 +161,7 @@ impl MessageRouter for FuzzRouter { } fn create_blinded_paths( - &self, _recipient: PublicKey, _peers: Vec, _secp_ctx: &Secp256k1, + &self, _recipient: PublicKey, _peers: Vec, _secp_ctx: &Secp256k1, ) -> Result, ()> { unreachable!() } diff --git a/fuzz/src/onion_message.rs b/fuzz/src/onion_message.rs index 4c1c5ac1122..371a9421fc7 100644 --- a/fuzz/src/onion_message.rs +++ b/fuzz/src/onion_message.rs @@ -7,7 +7,6 @@ use bitcoin::secp256k1::ecdsa::RecoverableSignature; use bitcoin::secp256k1::schnorr; use lightning::blinded_path::{BlindedPath, EmptyNodeIdLookUp}; -use lightning::blinded_path::message::ForwardNode; use lightning::ln::features::InitFeatures; use lightning::ln::msgs::{self, DecodeError, OnionMessageHandler}; use lightning::ln::script::ShutdownScript; @@ -89,7 +88,7 @@ impl MessageRouter for TestMessageRouter { } fn create_blinded_paths( - &self, _recipient: PublicKey, _peers: Vec, _secp_ctx: &Secp256k1, + &self, _recipient: PublicKey, _peers: Vec, _secp_ctx: &Secp256k1, ) -> Result, ()> { unreachable!() } diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index e3f7243cd3f..69139194194 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -8245,8 +8245,8 @@ macro_rules! create_offer_builder { ($self: ident, $builder: ty) => { /// /// # Privacy /// - /// Uses [`MessageRouter::create_blinded_paths`] to construct a [`BlindedPath`] for the offer. - /// However, if one is not found, uses a one-hop [`BlindedPath`] with + /// Uses [`MessageRouter::create_compact_blinded_paths`] to construct a [`BlindedPath`] for the + /// offer. However, if one is not found, uses a one-hop [`BlindedPath`] with /// [`ChannelManager::get_our_node_id`] as the introduction node instead. In the latter case, /// the node must be announced, otherwise, there is no way to find a path to the introduction in /// order to send the [`InvoiceRequest`]. @@ -8304,8 +8304,8 @@ macro_rules! create_refund_builder { ($self: ident, $builder: ty) => { /// /// # Privacy /// - /// Uses [`MessageRouter::create_blinded_paths`] to construct a [`BlindedPath`] for the refund. - /// However, if one is not found, uses a one-hop [`BlindedPath`] with + /// Uses [`MessageRouter::create_compact_blinded_paths`] to construct a [`BlindedPath`] for the + /// refund. However, if one is not found, uses a one-hop [`BlindedPath`] with /// [`ChannelManager::get_our_node_id`] as the introduction node instead. In the latter case, /// the node must be announced, otherwise, there is no way to find a path to the introduction in /// order to send the [`Bolt12Invoice`]. @@ -8686,7 +8686,7 @@ where inbound_payment::get_payment_preimage(payment_hash, payment_secret, &self.inbound_payment_key) } - /// Creates a blinded path by delegating to [`MessageRouter::create_blinded_paths`]. + /// Creates a blinded path by delegating to [`MessageRouter::create_compact_blinded_paths`]. /// /// Errors if the `MessageRouter` errors or returns an empty `Vec`. fn create_blinded_path(&self) -> Result { @@ -8708,7 +8708,7 @@ where .collect::>(); self.router - .create_blinded_paths(recipient, peers, secp_ctx) + .create_compact_blinded_paths(recipient, peers, secp_ctx) .and_then(|paths| paths.into_iter().next().ok_or(())) } diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index 50049866325..5ceb0683c57 100644 --- a/lightning/src/onion_message/messenger.rs +++ b/lightning/src/onion_message/messenger.rs @@ -162,7 +162,7 @@ for OnionMessenger where /// # }) /// # } /// # fn create_blinded_paths( -/// # &self, _recipient: PublicKey, _peers: Vec, _secp_ctx: &Secp256k1 +/// # &self, _recipient: PublicKey, _peers: Vec, _secp_ctx: &Secp256k1 /// # ) -> Result, ()> { /// # unreachable!() /// # } @@ -426,8 +426,33 @@ pub trait MessageRouter { fn create_blinded_paths< T: secp256k1::Signing + secp256k1::Verification >( - &self, recipient: PublicKey, peers: Vec, secp_ctx: &Secp256k1, + &self, recipient: PublicKey, peers: Vec, secp_ctx: &Secp256k1, ) -> Result, ()>; + + /// Creates compact [`BlindedPath`]s to the `recipient` node. The nodes in `peers` are assumed + /// to be direct peers with the `recipient`. + /// + /// Compact blinded paths use short channel ids instead of pubkeys for a smaller serialization, + /// which is beneficial when a QR code is used to transport the data. The SCID is passed using a + /// [`ForwardNode`] but may be `None` for graceful degradation. + /// + /// Implementations using additional intermediate nodes are responsible for using a + /// [`ForwardNode`] with `Some` short channel id, if possible. Similarly, implementations should + /// call [`BlindedPath::use_compact_introduction_node`]. + /// + /// The provided implementation simply delegates to [`MessageRouter::create_blinded_paths`], + /// ignoring the short channel ids. + fn create_compact_blinded_paths< + T: secp256k1::Signing + secp256k1::Verification + >( + &self, recipient: PublicKey, peers: Vec, secp_ctx: &Secp256k1, + ) -> Result, ()> { + let peers = peers + .into_iter() + .map(|ForwardNode { node_id, short_channel_id: _ }| node_id) + .collect(); + self.create_blinded_paths(recipient, peers, secp_ctx) + } } /// A [`MessageRouter`] that can only route to a directly connected [`Destination`]. @@ -454,7 +479,7 @@ where I: Iterator, T: secp256k1::Signing + secp256k1::Verification >( - &self, recipient: PublicKey, peers: I, secp_ctx: &Secp256k1, + &self, recipient: PublicKey, peers: I, secp_ctx: &Secp256k1, compact_paths: bool ) -> Result, ()> { // Limit the number of blinded paths that are computed. const MAX_PATHS: usize = 3; @@ -502,8 +527,11 @@ where } }, }?; - for path in &mut paths { - path.use_compact_introduction_node(&network_graph); + + if compact_paths { + for path in &mut paths { + path.use_compact_introduction_node(&network_graph); + } } Ok(paths) @@ -550,10 +578,21 @@ where fn create_blinded_paths< T: secp256k1::Signing + secp256k1::Verification + >( + &self, recipient: PublicKey, peers: Vec, secp_ctx: &Secp256k1, + ) -> Result, ()> { + let peers = peers + .into_iter() + .map(|node_id| ForwardNode { node_id, short_channel_id: None }); + self.create_blinded_paths_from_iter(recipient, peers, secp_ctx, false) + } + + fn create_compact_blinded_paths< + T: secp256k1::Signing + secp256k1::Verification >( &self, recipient: PublicKey, peers: Vec, secp_ctx: &Secp256k1, ) -> Result, ()> { - self.create_blinded_paths_from_iter(recipient, peers.into_iter(), secp_ctx) + self.create_blinded_paths_from_iter(recipient, peers.into_iter(), secp_ctx, true) } } @@ -1090,10 +1129,7 @@ where let peers = self.message_recipients.lock().unwrap() .iter() .filter(|(_, peer)| matches!(peer, OnionMessageRecipient::ConnectedPeer(_))) - .map(|(node_id, _ )| ForwardNode { - node_id: *node_id, - short_channel_id: None, - }) + .map(|(node_id, _ )| *node_id) .collect::>(); self.message_router diff --git a/lightning/src/routing/router.rs b/lightning/src/routing/router.rs index e1b5b655719..c105e914679 100644 --- a/lightning/src/routing/router.rs +++ b/lightning/src/routing/router.rs @@ -173,10 +173,18 @@ impl< G: Deref> + Clone, L: Deref, ES: Deref, S: Deref, fn create_blinded_paths< T: secp256k1::Signing + secp256k1::Verification > ( - &self, recipient: PublicKey, peers: Vec, secp_ctx: &Secp256k1, + &self, recipient: PublicKey, peers: Vec, secp_ctx: &Secp256k1, ) -> Result, ()> { self.message_router.create_blinded_paths(recipient, peers, secp_ctx) } + + fn create_compact_blinded_paths< + T: secp256k1::Signing + secp256k1::Verification + > ( + &self, recipient: PublicKey, peers: Vec, secp_ctx: &Secp256k1, + ) -> Result, ()> { + self.message_router.create_compact_blinded_paths(recipient, peers, secp_ctx) + } } /// A trait defining behavior for routing a payment. diff --git a/lightning/src/util/test_utils.rs b/lightning/src/util/test_utils.rs index a5363d32c76..f6616a8e5d2 100644 --- a/lightning/src/util/test_utils.rs +++ b/lightning/src/util/test_utils.rs @@ -250,10 +250,18 @@ impl<'a> MessageRouter for TestRouter<'a> { fn create_blinded_paths< T: secp256k1::Signing + secp256k1::Verification >( - &self, recipient: PublicKey, peers: Vec, secp_ctx: &Secp256k1, + &self, recipient: PublicKey, peers: Vec, secp_ctx: &Secp256k1, ) -> Result, ()> { self.router.create_blinded_paths(recipient, peers, secp_ctx) } + + fn create_compact_blinded_paths< + T: secp256k1::Signing + secp256k1::Verification + >( + &self, recipient: PublicKey, peers: Vec, secp_ctx: &Secp256k1, + ) -> Result, ()> { + self.router.create_compact_blinded_paths(recipient, peers, secp_ctx) + } } impl<'a> Drop for TestRouter<'a> { @@ -285,10 +293,16 @@ impl<'a> MessageRouter for TestMessageRouter<'a> { } fn create_blinded_paths( - &self, recipient: PublicKey, peers: Vec, secp_ctx: &Secp256k1, + &self, recipient: PublicKey, peers: Vec, secp_ctx: &Secp256k1, ) -> Result, ()> { self.inner.create_blinded_paths(recipient, peers, secp_ctx) } + + fn create_compact_blinded_paths( + &self, recipient: PublicKey, peers: Vec, secp_ctx: &Secp256k1, + ) -> Result, ()> { + self.inner.create_compact_blinded_paths(recipient, peers, secp_ctx) + } } pub struct OnlyReadsKeysInterface {} From 411b9b43f6089b1482eb9831742bf5ecb8238e0d Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Wed, 22 May 2024 16:35:57 -0500 Subject: [PATCH 3/7] Don't use compact blinded paths for reply paths There's no need to save space when creating reply paths since they are part of onion messages rather than in QR codes. Use normal blinded paths for these instead as they are less likely to become invalid in case of channel closure. --- lightning/src/ln/channelmanager.rs | 26 +++++++++++++++++++++++--- lightning/src/ln/offers_tests.rs | 8 ++------ 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 69139194194..391f5b330bf 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -8270,7 +8270,8 @@ macro_rules! create_offer_builder { ($self: ident, $builder: ty) => { let entropy = &*$self.entropy_source; let secp_ctx = &$self.secp_ctx; - let path = $self.create_blinded_path().map_err(|_| Bolt12SemanticError::MissingPaths)?; + let path = $self.create_compact_blinded_path() + .map_err(|_| Bolt12SemanticError::MissingPaths)?; let builder = OfferBuilder::deriving_signing_pubkey( node_id, expanded_key, entropy, secp_ctx ) @@ -8337,7 +8338,8 @@ macro_rules! create_refund_builder { ($self: ident, $builder: ty) => { let entropy = &*$self.entropy_source; let secp_ctx = &$self.secp_ctx; - let path = $self.create_blinded_path().map_err(|_| Bolt12SemanticError::MissingPaths)?; + let path = $self.create_compact_blinded_path() + .map_err(|_| Bolt12SemanticError::MissingPaths)?; let builder = RefundBuilder::deriving_payer_id( node_id, expanded_key, entropy, secp_ctx, amount_msats, payment_id )? @@ -8686,13 +8688,31 @@ where inbound_payment::get_payment_preimage(payment_hash, payment_secret, &self.inbound_payment_key) } - /// Creates a blinded path by delegating to [`MessageRouter::create_compact_blinded_paths`]. + /// Creates a blinded path by delegating to [`MessageRouter::create_blinded_paths`]. /// /// Errors if the `MessageRouter` errors or returns an empty `Vec`. fn create_blinded_path(&self) -> Result { let recipient = self.get_our_node_id(); let secp_ctx = &self.secp_ctx; + let peers = self.per_peer_state.read().unwrap() + .iter() + .filter(|(_, peer)| peer.lock().unwrap().latest_features.supports_onion_messages()) + .map(|(node_id, _)| *node_id) + .collect::>(); + + self.router + .create_blinded_paths(recipient, peers, secp_ctx) + .and_then(|paths| paths.into_iter().next().ok_or(())) + } + + /// Creates a blinded path by delegating to [`MessageRouter::create_compact_blinded_paths`]. + /// + /// Errors if the `MessageRouter` errors or returns an empty `Vec`. + fn create_compact_blinded_path(&self) -> 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())) diff --git a/lightning/src/ln/offers_tests.rs b/lightning/src/ln/offers_tests.rs index eedd82c569b..5594c8ba930 100644 --- a/lightning/src/ln/offers_tests.rs +++ b/lightning/src/ln/offers_tests.rs @@ -427,11 +427,9 @@ fn creates_and_pays_for_offer_using_two_hop_blinded_path() { payer_note_truncated: None, }, }); - let introduction_node_id = resolve_introduction_node(alice, &reply_path); assert_eq!(invoice_request.amount_msats(), None); assert_ne!(invoice_request.payer_id(), david_id); - assert_eq!(introduction_node_id, charlie_id); - assert!(matches!(reply_path.introduction_node, IntroductionNode::DirectedShortChannelId(..))); + assert_eq!(reply_path.introduction_node, IntroductionNode::NodeId(charlie_id)); let onion_message = alice.onion_messenger.next_onion_message_for_peer(charlie_id).unwrap(); charlie.onion_messenger.handle_onion_message(&alice_id, &onion_message); @@ -582,11 +580,9 @@ fn creates_and_pays_for_offer_using_one_hop_blinded_path() { payer_note_truncated: None, }, }); - let introduction_node_id = resolve_introduction_node(alice, &reply_path); assert_eq!(invoice_request.amount_msats(), None); assert_ne!(invoice_request.payer_id(), bob_id); - assert_eq!(introduction_node_id, bob_id); - assert!(matches!(reply_path.introduction_node, IntroductionNode::DirectedShortChannelId(..))); + assert_eq!(reply_path.introduction_node, IntroductionNode::NodeId(bob_id)); let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap(); bob.onion_messenger.handle_onion_message(&alice_id, &onion_message); From 8012c2b213127372bdad150cdc354920d971087d Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Tue, 28 May 2024 18:43:45 -0500 Subject: [PATCH 4/7] Use compact blinded paths for short-lived offers When an offer is short-lived, the likelihood of a channel used in a compact blinded path going away is low. Require passing the absolute expiry of an offer to ChannelManager::create_offer_builder so that it can be used to determine whether or not compact blinded path should be used. Use the same criteria for creating blinded paths for refunds as well. --- lightning/src/ln/channelmanager.rs | 83 ++++++++++++--- lightning/src/ln/offers_tests.rs | 158 ++++++++++++++++++++++++----- 2 files changed, 203 insertions(+), 38 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 391f5b330bf..2fd170ebfc9 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -1554,8 +1554,9 @@ where /// # /// # 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()? +/// .create_offer_builder(absolute_expiry)? /// # ; /// # // Needed for compiling for c_bindings /// # let builder: lightning::offers::offer::OfferBuilder<_, _> = offer.into(); @@ -2287,6 +2288,19 @@ 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 [`BlindedPath`] created using: +/// - [`MessageRouter::create_compact_blinded_paths`] when short-lived, and +/// - [`MessageRouter::create_blinded_paths`] when long-lived. +/// +/// Using compact [`BlindedPath`]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)] @@ -8240,16 +8254,17 @@ 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 will - /// not have an expiration unless otherwise set on the builder. + /// [`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::create_compact_blinded_paths`] to construct a [`BlindedPath`] for the - /// offer. However, if one is not found, uses a one-hop [`BlindedPath`] with + /// Uses [`MessageRouter`] to construct a [`BlindedPath`] for the offer based on the given + /// `absolute_expiry` according to [`MAX_SHORT_LIVED_RELATIVE_EXPIRY`]. See those docs for + /// privacy implications. However, if one is not found, uses a one-hop [`BlindedPath`] with /// [`ChannelManager::get_our_node_id`] as the introduction node instead. In the latter case, - /// the node must be announced, otherwise, there is no way to find a path to the introduction in - /// order to send the [`InvoiceRequest`]. + /// the node must be announced, otherwise, there is no way to find a path to the introduction + /// node in order to send the [`InvoiceRequest`]. /// /// Also, uses a derived signing pubkey in the offer for recipient privacy. /// @@ -8264,13 +8279,15 @@ 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) -> Result<$builder, Bolt12SemanticError> { + 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 path = $self.create_compact_blinded_path() + let path = $self.create_blinded_path_using_absolute_expiry(absolute_expiry) .map_err(|_| Bolt12SemanticError::MissingPaths)?; let builder = OfferBuilder::deriving_signing_pubkey( node_id, expanded_key, entropy, secp_ctx @@ -8278,6 +8295,11 @@ macro_rules! create_offer_builder { ($self: ident, $builder: ty) => { .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()) } } } @@ -8305,11 +8327,12 @@ macro_rules! create_refund_builder { ($self: ident, $builder: ty) => { /// /// # Privacy /// - /// Uses [`MessageRouter::create_compact_blinded_paths`] to construct a [`BlindedPath`] for the - /// refund. However, if one is not found, uses a one-hop [`BlindedPath`] with + /// Uses [`MessageRouter`] to construct a [`BlindedPath`] for the refund based on the given + /// `absolute_expiry` according to [`MAX_SHORT_LIVED_RELATIVE_EXPIRY`]. See those docs for + /// privacy implications. However, if one is not found, uses a one-hop [`BlindedPath`] with /// [`ChannelManager::get_our_node_id`] as the introduction node instead. In the latter case, - /// the node must be announced, otherwise, there is no way to find a path to the introduction in - /// order to send the [`Bolt12Invoice`]. + /// the node must be announced, otherwise, there is no way to find a path to the introduction + /// node in order to send the [`Bolt12Invoice`]. /// /// Also, uses a derived payer id in the refund for payer privacy. /// @@ -8338,7 +8361,7 @@ macro_rules! create_refund_builder { ($self: ident, $builder: ty) => { let entropy = &*$self.entropy_source; let secp_ctx = &$self.secp_ctx; - let path = $self.create_compact_blinded_path() + let path = $self.create_blinded_path_using_absolute_expiry(Some(absolute_expiry)) .map_err(|_| Bolt12SemanticError::MissingPaths)?; let builder = RefundBuilder::deriving_payer_id( node_id, expanded_key, entropy, secp_ctx, amount_msats, payment_id @@ -8688,6 +8711,38 @@ where inbound_payment::get_payment_preimage(payment_hash, payment_secret, &self.inbound_payment_key) } + /// Creates a blinded path 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_path_using_absolute_expiry( + &self, 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_path() + } else { + self.create_blinded_path() + } + } + + 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 blinded path by delegating to [`MessageRouter::create_blinded_paths`]. /// /// Errors if the `MessageRouter` errors or returns an empty `Vec`. diff --git a/lightning/src/ln/offers_tests.rs b/lightning/src/ln/offers_tests.rs index 5594c8ba930..725b8dfe5c7 100644 --- a/lightning/src/ln/offers_tests.rs +++ b/lightning/src/ln/offers_tests.rs @@ -46,7 +46,7 @@ use core::time::Duration; use crate::blinded_path::{BlindedPath, IntroductionNode}; use crate::blinded_path::payment::{Bolt12OfferContext, Bolt12RefundContext, PaymentContext}; use crate::events::{Event, MessageSendEventsProvider, PaymentPurpose}; -use crate::ln::channelmanager::{PaymentId, RecentPaymentDetails, Retry, self}; +use crate::ln::channelmanager::{MAX_SHORT_LIVED_RELATIVE_EXPIRY, PaymentId, RecentPaymentDetails, Retry, self}; use crate::ln::functional_test_utils::*; use crate::ln::msgs::{ChannelMessageHandler, Init, NodeAnnouncement, OnionMessage, OnionMessageHandler, RoutingMessageHandler, SocketAddress, UnsignedGossipMessage, UnsignedNodeAnnouncement}; use crate::offers::invoice::Bolt12Invoice; @@ -274,7 +274,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().unwrap() + .create_offer_builder(None).unwrap() .amount_msats(10_000_000) .build().unwrap(); assert_ne!(offer.signing_pubkey(), Some(bob_id)); @@ -290,7 +290,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().unwrap() + .create_offer_builder(None).unwrap() .amount_msats(10_000_000) .build().unwrap(); assert_ne!(offer.signing_pubkey(), Some(bob_id)); @@ -341,7 +341,7 @@ fn prefers_more_connected_nodes_in_blinded_paths() { disconnect_peers(david, &[bob, &nodes[4], &nodes[5]]); let offer = bob.node - .create_offer_builder().unwrap() + .create_offer_builder(None).unwrap() .amount_msats(10_000_000) .build().unwrap(); assert_ne!(offer.signing_pubkey(), Some(bob_id)); @@ -352,6 +352,124 @@ fn prefers_more_connected_nodes_in_blinded_paths() { } } +/// Checks that blinded paths are compact for short-lived offers. +#[test] +fn creates_short_lived_offer() { + 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 bob = &nodes[1]; + + let absolute_expiry = alice.node.duration_since_epoch() + MAX_SHORT_LIVED_RELATIVE_EXPIRY; + let offer = alice.node + .create_offer_builder(Some(absolute_expiry)).unwrap() + .build().unwrap(); + assert_eq!(offer.absolute_expiry(), Some(absolute_expiry)); + assert!(!offer.paths().is_empty()); + for path in offer.paths() { + let introduction_node_id = resolve_introduction_node(bob, &path); + assert_eq!(introduction_node_id, alice_id); + assert!(matches!(path.introduction_node, IntroductionNode::DirectedShortChannelId(..))); + } +} + +/// Checks that blinded paths are not compact for long-lived offers. +#[test] +fn creates_long_lived_offer() { + 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 = 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() + .build().unwrap(); + assert_eq!(offer.absolute_expiry(), Some(absolute_expiry)); + assert!(!offer.paths().is_empty()); + for path in offer.paths() { + assert_eq!(path.introduction_node, IntroductionNode::NodeId(alice_id)); + } + + let offer = alice.node + .create_offer_builder(None).unwrap() + .build().unwrap(); + assert_eq!(offer.absolute_expiry(), None); + assert!(!offer.paths().is_empty()); + for path in offer.paths() { + assert_eq!(path.introduction_node, IntroductionNode::NodeId(alice_id)); + } +} + +/// Checks that blinded paths are compact for short-lived refunds. +#[test] +fn creates_short_lived_refund() { + 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 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 payment_id = PaymentId([1; 32]); + let refund = bob.node + .create_refund_builder(10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None) + .unwrap() + .build().unwrap(); + assert_eq!(refund.absolute_expiry(), Some(absolute_expiry)); + assert!(!refund.paths().is_empty()); + for path in refund.paths() { + let introduction_node_id = resolve_introduction_node(alice, &path); + assert_eq!(introduction_node_id, bob_id); + assert!(matches!(path.introduction_node, IntroductionNode::DirectedShortChannelId(..))); + } +} + +/// Checks that blinded paths are not compact for long-lived refunds. +#[test] +fn creates_long_lived_refund() { + 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 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 + + 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) + .unwrap() + .build().unwrap(); + assert_eq!(refund.absolute_expiry(), Some(absolute_expiry)); + assert!(!refund.paths().is_empty()); + for path in refund.paths() { + assert_eq!(path.introduction_node, IntroductionNode::NodeId(bob_id)); + } +} + /// Checks that an offer can be paid through blinded paths and that ephemeral pubkeys are used /// rather than exposing a node's pubkey. #[test] @@ -391,16 +509,14 @@ 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() + .create_offer_builder(None) .unwrap() .amount_msats(10_000_000) .build().unwrap(); assert_ne!(offer.signing_pubkey(), Some(alice_id)); assert!(!offer.paths().is_empty()); for path in offer.paths() { - let introduction_node_id = resolve_introduction_node(david, &path); - assert_eq!(introduction_node_id, bob_id); - assert!(matches!(path.introduction_node, IntroductionNode::DirectedShortChannelId(..))); + assert_eq!(path.introduction_node, IntroductionNode::NodeId(bob_id)); } let payment_id = PaymentId([1; 32]); @@ -501,9 +617,7 @@ fn creates_and_pays_for_refund_using_two_hop_blinded_path() { assert_ne!(refund.payer_id(), david_id); assert!(!refund.paths().is_empty()); for path in refund.paths() { - let introduction_node_id = resolve_introduction_node(alice, &path); - assert_eq!(introduction_node_id, charlie_id); - assert!(matches!(path.introduction_node, IntroductionNode::DirectedShortChannelId(..))); + assert_eq!(path.introduction_node, IntroductionNode::NodeId(charlie_id)); } expect_recent_payment!(david, RecentPaymentDetails::AwaitingInvoice, payment_id); @@ -553,15 +667,13 @@ 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().unwrap() + .create_offer_builder(None).unwrap() .amount_msats(10_000_000) .build().unwrap(); assert_ne!(offer.signing_pubkey(), Some(alice_id)); assert!(!offer.paths().is_empty()); for path in offer.paths() { - let introduction_node_id = resolve_introduction_node(bob, &path); - assert_eq!(introduction_node_id, alice_id); - assert!(matches!(path.introduction_node, IntroductionNode::DirectedShortChannelId(..))); + assert_eq!(path.introduction_node, IntroductionNode::NodeId(alice_id)); } let payment_id = PaymentId([1; 32]); @@ -630,9 +742,7 @@ fn creates_and_pays_for_refund_using_one_hop_blinded_path() { assert_ne!(refund.payer_id(), bob_id); assert!(!refund.paths().is_empty()); for path in refund.paths() { - let introduction_node_id = resolve_introduction_node(alice, &path); - assert_eq!(introduction_node_id, bob_id); - assert!(matches!(path.introduction_node, IntroductionNode::DirectedShortChannelId(..))); + assert_eq!(path.introduction_node, IntroductionNode::NodeId(bob_id)); } expect_recent_payment!(bob, RecentPaymentDetails::AwaitingInvoice, payment_id); @@ -677,7 +787,7 @@ fn pays_for_offer_without_blinded_paths() { let bob_id = bob.node.get_our_node_id(); let offer = alice.node - .create_offer_builder().unwrap() + .create_offer_builder(None).unwrap() .clear_paths() .amount_msats(10_000_000) .build().unwrap(); @@ -765,7 +875,7 @@ fn fails_creating_offer_without_blinded_paths() { create_unannounced_chan_between_nodes_with_value(&nodes, 0, 1, 10_000_000, 1_000_000_000); - match nodes[0].node.create_offer_builder() { + match nodes[0].node.create_offer_builder(None) { Ok(_) => panic!("Expected error"), Err(e) => assert_eq!(e, Bolt12SemanticError::MissingPaths), } @@ -808,7 +918,7 @@ fn fails_creating_invoice_request_for_unsupported_chain() { let bob = &nodes[1]; let offer = alice.node - .create_offer_builder().unwrap() + .create_offer_builder(None).unwrap() .clear_chains() .chain(Network::Signet) .build().unwrap(); @@ -868,7 +978,7 @@ fn fails_creating_invoice_request_without_blinded_reply_path() { disconnect_peers(david, &[bob, &nodes[4], &nodes[5]]); let offer = alice.node - .create_offer_builder().unwrap() + .create_offer_builder(None).unwrap() .amount_msats(10_000_000) .build().unwrap(); @@ -902,7 +1012,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().unwrap() + .create_offer_builder(None).unwrap() .amount_msats(10_000_000) .build().unwrap(); @@ -988,7 +1098,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().unwrap() + .create_offer_builder(None).unwrap() .amount_msats(10_000_000) .build().unwrap(); From 15fa0d8f2084d0ad9f9223e5b185ee0543e9e365 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Tue, 4 Jun 2024 18:42:51 -0500 Subject: [PATCH 5/7] Fix functional_test_utils::reconnect_nodes When reconnecting nodes, make sure to notify OnionMessenger that the nodes are now connected. --- lightning/src/ln/functional_test_utils.rs | 47 +++++++++++------------ 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index c0f3458b3d6..11e0737da1a 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -3259,30 +3259,34 @@ pub fn create_network<'a, 'b: 'a, 'c: 'b>(node_count: usize, cfgs: &'b Vec(node_a: &Node<'a, 'b, 'c>, node_b: &Node<'a, 'b, 'c>) { + let node_id_a = node_a.node.get_our_node_id(); + let node_id_b = node_b.node.get_our_node_id(); + + let init_a = msgs::Init { + features: node_a.init_features(&node_id_b), + networks: None, + remote_network_address: None, + }; + let init_b = msgs::Init { + features: node_b.init_features(&node_id_a), + networks: None, + remote_network_address: None, + }; + + node_a.node.peer_connected(&node_id_b, &init_b, true).unwrap(); + node_b.node.peer_connected(&node_id_a, &init_a, false).unwrap(); + node_a.onion_messenger.peer_connected(&node_id_b, &init_b, true).unwrap(); + node_b.onion_messenger.peer_connected(&node_id_a, &init_a, false).unwrap(); +} + pub fn connect_dummy_node<'a, 'b: 'a, 'c: 'b>(node: &Node<'a, 'b, 'c>) { let node_id_dummy = PublicKey::from_slice(&[2; 33]).unwrap(); @@ -3643,13 +3647,8 @@ pub fn reconnect_nodes<'a, 'b, 'c, 'd>(args: ReconnectArgs<'a, 'b, 'c, 'd>) { pending_cell_htlc_claims, pending_cell_htlc_fails, pending_raa, pending_responding_commitment_signed, pending_responding_commitment_signed_dup_monitor, } = args; - node_a.node.peer_connected(&node_b.node.get_our_node_id(), &msgs::Init { - features: node_b.node.init_features(), networks: None, remote_network_address: None - }, true).unwrap(); + connect_nodes(node_a, node_b); let reestablish_1 = get_chan_reestablish_msgs!(node_a, node_b); - node_b.node.peer_connected(&node_a.node.get_our_node_id(), &msgs::Init { - features: node_a.node.init_features(), networks: None, remote_network_address: None - }, false).unwrap(); let reestablish_2 = get_chan_reestablish_msgs!(node_b, node_a); if send_channel_ready.0 { From 7db6616e52e3a2910b7237ce95e80a63abf9e0a5 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Tue, 4 Jun 2024 18:48:07 -0500 Subject: [PATCH 6/7] Exclude disconnected peers from BlindedPath When calling MessageRouter::create_blinded_path, ChannelManager was including disconnected peers. Filter peers such that only connected ones are included. Otherwise, the resulting BlindedPath may not work. --- lightning/src/ln/channelmanager.rs | 5 +- lightning/src/ln/offers_tests.rs | 123 +++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+), 1 deletion(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 2fd170ebfc9..7e81730ce7d 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -8752,7 +8752,9 @@ where let peers = self.per_peer_state.read().unwrap() .iter() - .filter(|(_, peer)| peer.lock().unwrap().latest_features.supports_onion_messages()) + .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::>(); @@ -8771,6 +8773,7 @@ where 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)| ForwardNode { node_id: *node_id, diff --git a/lightning/src/ln/offers_tests.rs b/lightning/src/ln/offers_tests.rs index 725b8dfe5c7..c7fb5f8fd59 100644 --- a/lightning/src/ln/offers_tests.rs +++ b/lightning/src/ln/offers_tests.rs @@ -904,6 +904,129 @@ fn fails_creating_refund_without_blinded_paths() { assert!(nodes[0].node.list_recent_payments().is_empty()); } +/// Fails creating or paying an offer when a blinded path cannot be created because no peers are +/// connected. +#[test] +fn fails_creating_or_paying_for_offer_without_connected_peers() { + let chanmon_cfgs = create_chanmon_cfgs(6); + let node_cfgs = create_node_cfgs(6, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(6, &node_cfgs, &[None, None, None, None, None, None]); + let nodes = create_network(6, &node_cfgs, &node_chanmgrs); + + create_unannounced_chan_between_nodes_with_value(&nodes, 0, 1, 10_000_000, 1_000_000_000); + create_unannounced_chan_between_nodes_with_value(&nodes, 2, 3, 10_000_000, 1_000_000_000); + create_announced_chan_between_nodes_with_value(&nodes, 1, 2, 10_000_000, 1_000_000_000); + create_announced_chan_between_nodes_with_value(&nodes, 1, 4, 10_000_000, 1_000_000_000); + create_announced_chan_between_nodes_with_value(&nodes, 1, 5, 10_000_000, 1_000_000_000); + create_announced_chan_between_nodes_with_value(&nodes, 2, 4, 10_000_000, 1_000_000_000); + create_announced_chan_between_nodes_with_value(&nodes, 2, 5, 10_000_000, 1_000_000_000); + + let (alice, bob, charlie, david) = (&nodes[0], &nodes[1], &nodes[2], &nodes[3]); + + 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)) { + Ok(_) => panic!("Expected error"), + Err(e) => assert_eq!(e, Bolt12SemanticError::MissingPaths), + } + + let mut args = ReconnectArgs::new(alice, bob); + args.send_channel_ready = (true, true); + reconnect_nodes(args); + + let offer = alice.node + .create_offer_builder(Some(absolute_expiry)).unwrap() + .amount_msats(10_000_000) + .build().unwrap(); + + let payment_id = PaymentId([1; 32]); + + match david.node.pay_for_offer(&offer, None, None, None, payment_id, Retry::Attempts(0), None) { + Ok(_) => panic!("Expected error"), + Err(e) => assert_eq!(e, Bolt12SemanticError::MissingPaths), + } + + assert!(nodes[0].node.list_recent_payments().is_empty()); + + let mut args = ReconnectArgs::new(charlie, david); + args.send_channel_ready = (true, true); + reconnect_nodes(args); + + assert!( + david.node.pay_for_offer( + &offer, None, None, None, payment_id, Retry::Attempts(0), None + ).is_ok() + ); + + expect_recent_payment!(david, RecentPaymentDetails::AwaitingInvoice, payment_id); +} + +/// Fails creating or sending an invoice for a refund when a blinded path cannot be created because +/// no peers are connected. +#[test] +fn fails_creating_refund_or_sending_invoice_without_connected_peers() { + let mut accept_forward_cfg = test_default_channel_config(); + accept_forward_cfg.accept_forwards_to_priv_channels = true; + + let mut features = channelmanager::provided_init_features(&accept_forward_cfg); + features.set_onion_messages_optional(); + features.set_route_blinding_optional(); + + let chanmon_cfgs = create_chanmon_cfgs(6); + let node_cfgs = create_node_cfgs(6, &chanmon_cfgs); + + *node_cfgs[1].override_init_features.borrow_mut() = Some(features); + + let node_chanmgrs = create_node_chanmgrs( + 6, &node_cfgs, &[None, Some(accept_forward_cfg), None, None, None, None] + ); + let nodes = create_network(6, &node_cfgs, &node_chanmgrs); + + create_unannounced_chan_between_nodes_with_value(&nodes, 0, 1, 10_000_000, 1_000_000_000); + create_unannounced_chan_between_nodes_with_value(&nodes, 2, 3, 10_000_000, 1_000_000_000); + create_announced_chan_between_nodes_with_value(&nodes, 1, 2, 10_000_000, 1_000_000_000); + create_announced_chan_between_nodes_with_value(&nodes, 1, 4, 10_000_000, 1_000_000_000); + create_announced_chan_between_nodes_with_value(&nodes, 1, 5, 10_000_000, 1_000_000_000); + create_announced_chan_between_nodes_with_value(&nodes, 2, 4, 10_000_000, 1_000_000_000); + create_announced_chan_between_nodes_with_value(&nodes, 2, 5, 10_000_000, 1_000_000_000); + + let (alice, bob, charlie, david) = (&nodes[0], &nodes[1], &nodes[2], &nodes[3]); + + 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 payment_id = PaymentId([1; 32]); + match david.node.create_refund_builder( + 10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None + ) { + Ok(_) => panic!("Expected error"), + Err(e) => assert_eq!(e, Bolt12SemanticError::MissingPaths), + } + + let mut args = ReconnectArgs::new(charlie, david); + args.send_channel_ready = (true, true); + reconnect_nodes(args); + + let refund = david.node + .create_refund_builder(10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None) + .unwrap() + .build().unwrap(); + + match alice.node.request_refund_payment(&refund) { + Ok(_) => panic!("Expected error"), + Err(e) => assert_eq!(e, Bolt12SemanticError::MissingPaths), + } + + let mut args = ReconnectArgs::new(alice, bob); + args.send_channel_ready = (true, true); + reconnect_nodes(args); + + assert!(alice.node.request_refund_payment(&refund).is_ok()); +} + /// Fails creating an invoice request when the offer contains an unsupported chain. #[test] fn fails_creating_invoice_request_for_unsupported_chain() { From c17a026b0716a1226f1803f31645b9707eaf36f3 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Thu, 30 May 2024 14:38:15 -0500 Subject: [PATCH 7/7] Clarify docs regarding one-hop blinded paths The docs assumed ChannelManager is parameterized by DefaultRouter, which may not be the case. Clarify the behavior is specific to using DefaultRouter. --- lightning/src/ln/channelmanager.rs | 19 +++++++------------ lightning/src/onion_message/messenger.rs | 7 +++++++ lightning/src/routing/router.rs | 5 +++++ 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 7e81730ce7d..992fd3c6f02 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -8261,10 +8261,8 @@ macro_rules! create_offer_builder { ($self: ident, $builder: ty) => { /// /// Uses [`MessageRouter`] to construct a [`BlindedPath`] for the offer based on the given /// `absolute_expiry` according to [`MAX_SHORT_LIVED_RELATIVE_EXPIRY`]. See those docs for - /// privacy implications. However, if one is not found, uses a one-hop [`BlindedPath`] with - /// [`ChannelManager::get_our_node_id`] as the introduction node instead. In the latter case, - /// the node must be announced, otherwise, there is no way to find a path to the introduction - /// node in order to send the [`InvoiceRequest`]. + /// 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. /// @@ -8329,10 +8327,8 @@ macro_rules! create_refund_builder { ($self: ident, $builder: ty) => { /// /// Uses [`MessageRouter`] to construct a [`BlindedPath`] for the refund based on the given /// `absolute_expiry` according to [`MAX_SHORT_LIVED_RELATIVE_EXPIRY`]. See those docs for - /// privacy implications. However, if one is not found, uses a one-hop [`BlindedPath`] with - /// [`ChannelManager::get_our_node_id`] as the introduction node instead. In the latter case, - /// the node must be announced, otherwise, there is no way to find a path to the introduction - /// node in order to send the [`Bolt12Invoice`]. + /// 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. /// @@ -8431,10 +8427,9 @@ where /// /// # Privacy /// - /// Uses a one-hop [`BlindedPath`] for the reply path with [`ChannelManager::get_our_node_id`] - /// as the introduction node and a derived payer id for payer privacy. As such, currently, the - /// node must be announced. Otherwise, there is no way to find a path to the introduction node - /// in order to send the [`Bolt12Invoice`]. + /// 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 /// diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index 5ceb0683c57..ee49d00e99d 100644 --- a/lightning/src/onion_message/messenger.rs +++ b/lightning/src/onion_message/messenger.rs @@ -456,6 +456,13 @@ pub trait MessageRouter { } /// A [`MessageRouter`] that can only route to a directly connected [`Destination`]. +/// +/// # Privacy +/// +/// Creating [`BlindedPath`]s may affect privacy since, if a suitable path cannot be found, it will +/// create a one-hop path using the recipient as the introduction node if it is a announced node. +/// Otherwise, there is no way to find a path to the introduction node in order to send a message, +/// and thus an `Err` is returned. pub struct DefaultMessageRouter>, L: Deref, ES: Deref> where L::Target: Logger, diff --git a/lightning/src/routing/router.rs b/lightning/src/routing/router.rs index c105e914679..5b202604e37 100644 --- a/lightning/src/routing/router.rs +++ b/lightning/src/routing/router.rs @@ -36,6 +36,11 @@ use core::{cmp, fmt}; use core::ops::Deref; /// A [`Router`] implemented using [`find_route`]. +/// +/// # Privacy +/// +/// Implements [`MessageRouter`] by delegating to [`DefaultMessageRouter`]. See those docs for +/// privacy implications. pub struct DefaultRouter> + Clone, L: Deref, ES: Deref, S: Deref, SP: Sized, Sc: ScoreLookUp> where L::Target: Logger, S::Target: for <'a> LockableScore<'a, ScoreLookUp = Sc>,