From 912f3c21d646795035f5a032d416bc6eaa56e7e0 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Tue, 23 Sep 2025 16:06:16 -0400 Subject: [PATCH 1/4] Extract util to forward onion messages In an upcoming commit, we will need to repurpose this logic when, as a static invoice server node, we update our support for forwarding invoice requests from payers to often-offline recipients. We currently treat these forwarded invreqs as outbound onion messages that are initiated by our node, when they should really be treated as forwarded onion messages. --- fuzz/src/onion_message.rs | 6 +- lightning/src/onion_message/messenger.rs | 123 ++++++++++++++--------- 2 files changed, 76 insertions(+), 53 deletions(-) diff --git a/fuzz/src/onion_message.rs b/fuzz/src/onion_message.rs index d58b44fa7b6..7c979fa1462 100644 --- a/fuzz/src/onion_message.rs +++ b/fuzz/src/onion_message.rs @@ -430,7 +430,7 @@ mod tests { super::do_test(&>::from_hex(two_unblinded_hops_om).unwrap(), &logger); { let log_entries = logger.lines.lock().unwrap(); - assert_eq!(log_entries.get(&("lightning::onion_message::messenger".to_string(), "Forwarding an onion message to peer 020202020202020202020202020202020202020202020202020202020202020202".to_string())), Some(&1)); + assert_eq!(log_entries.get(&("lightning::onion_message::messenger".to_string(), "Forwarding an onion message to peer 020202020202020202020202020202020202020202020202020202020202020202 when forwarding peeled onion message from 020000000000000000000000000000000000000000000000000000000000000002".to_string())), Some(&1)); } let two_unblinded_two_blinded_om = "\ @@ -471,7 +471,7 @@ mod tests { super::do_test(&>::from_hex(two_unblinded_two_blinded_om).unwrap(), &logger); { let log_entries = logger.lines.lock().unwrap(); - assert_eq!(log_entries.get(&("lightning::onion_message::messenger".to_string(), "Forwarding an onion message to peer 020202020202020202020202020202020202020202020202020202020202020202".to_string())), Some(&1)); + assert_eq!(log_entries.get(&("lightning::onion_message::messenger".to_string(), "Forwarding an onion message to peer 020202020202020202020202020202020202020202020202020202020202020202 when forwarding peeled onion message from 020000000000000000000000000000000000000000000000000000000000000002".to_string())), Some(&1)); } let three_blinded_om = "\ @@ -512,7 +512,7 @@ mod tests { super::do_test(&>::from_hex(three_blinded_om).unwrap(), &logger); { let log_entries = logger.lines.lock().unwrap(); - assert_eq!(log_entries.get(&("lightning::onion_message::messenger".to_string(), "Forwarding an onion message to peer 020202020202020202020202020202020202020202020202020202020202020202".to_string())), Some(&1)); + assert_eq!(log_entries.get(&("lightning::onion_message::messenger".to_string(), "Forwarding an onion message to peer 020202020202020202020202020202020202020202020202020202020202020202 when forwarding peeled onion message from 020000000000000000000000000000000000000000000000000000000000000002".to_string())), Some(&1)); } } } diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index 4fe2a63ae16..de889a94776 100644 --- a/lightning/src/onion_message/messenger.rs +++ b/lightning/src/onion_message/messenger.rs @@ -1620,6 +1620,74 @@ where } } + fn enqueue_forwarded_onion_message( + &self, next_hop: NextMessageHop, onion_message: OnionMessage, log_suffix: fmt::Arguments, + ) -> Result<(), SendError> { + let next_node_id = match next_hop { + NextMessageHop::NodeId(pubkey) => pubkey, + NextMessageHop::ShortChannelId(scid) => match self.node_id_lookup.next_node_id(scid) { + Some(pubkey) => pubkey, + None => { + log_trace!(self.logger, "Dropping forwarded onion messager: unable to resolve next hop using SCID {} {}", scid, log_suffix); + return Err(SendError::GetNodeIdFailed); + }, + }, + }; + + let mut message_recipients = self.message_recipients.lock().unwrap(); + if outbound_buffer_full(&next_node_id, &message_recipients) { + log_trace!( + self.logger, + "Dropping forwarded onion message to peer {}: outbound buffer full {}", + next_node_id, + log_suffix + ); + return Err(SendError::BufferFull); + } + + #[cfg(fuzzing)] + message_recipients + .entry(next_node_id) + .or_insert_with(|| OnionMessageRecipient::ConnectedPeer(VecDeque::new())); + + match message_recipients.entry(next_node_id) { + hash_map::Entry::Occupied(mut e) + if matches!(e.get(), OnionMessageRecipient::ConnectedPeer(..)) => + { + e.get_mut().enqueue_message(onion_message); + log_trace!( + self.logger, + "Forwarding an onion message to peer {} {}", + next_node_id, + log_suffix + ); + Ok(()) + }, + _ if self.intercept_messages_for_offline_peers => { + log_trace!( + self.logger, + "Generating OnionMessageIntercepted event for peer {} {}", + next_node_id, + log_suffix + ); + self.enqueue_intercepted_event(Event::OnionMessageIntercepted { + peer_node_id: next_node_id, + message: onion_message, + }); + Ok(()) + }, + _ => { + log_trace!( + self.logger, + "Dropping forwarded onion message to disconnected peer {} {}", + next_node_id, + log_suffix + ); + Err(SendError::InvalidFirstHop(next_node_id)) + }, + } + } + /// Forwards an [`OnionMessage`] to `peer_node_id`. Useful if we initialized /// the [`OnionMessenger`] with [`Self::new_with_offline_peer_interception`] /// and want to forward a previously intercepted onion message to a peer that @@ -2204,56 +2272,11 @@ where } }, Ok(PeeledOnion::Forward(next_hop, onion_message)) => { - let next_node_id = match next_hop { - NextMessageHop::NodeId(pubkey) => pubkey, - NextMessageHop::ShortChannelId(scid) => { - match self.node_id_lookup.next_node_id(scid) { - Some(pubkey) => pubkey, - None => { - log_trace!(self.logger, "Dropping forwarded onion messager: unable to resolve next hop using SCID {}", scid); - return; - }, - } - }, - }; - - let mut message_recipients = self.message_recipients.lock().unwrap(); - if outbound_buffer_full(&next_node_id, &message_recipients) { - log_trace!( - logger, - "Dropping forwarded onion message to peer {}: outbound buffer full", - next_node_id - ); - return; - } - - #[cfg(fuzzing)] - message_recipients - .entry(next_node_id) - .or_insert_with(|| OnionMessageRecipient::ConnectedPeer(VecDeque::new())); - - match message_recipients.entry(next_node_id) { - hash_map::Entry::Occupied(mut e) - if matches!(e.get(), OnionMessageRecipient::ConnectedPeer(..)) => - { - e.get_mut().enqueue_message(onion_message); - log_trace!(logger, "Forwarding an onion message to peer {}", next_node_id); - }, - _ if self.intercept_messages_for_offline_peers => { - self.enqueue_intercepted_event(Event::OnionMessageIntercepted { - peer_node_id: next_node_id, - message: onion_message, - }); - }, - _ => { - log_trace!( - logger, - "Dropping forwarded onion message to disconnected peer {}", - next_node_id - ); - return; - }, - } + let _ = self.enqueue_forwarded_onion_message( + next_hop, + onion_message, + format_args!("when forwarding peeled onion message from {}", peer_node_id), + ); }, Err(e) => { log_error!(logger, "Failed to process onion message {:?}", e); From 332b3e59f325aaef682dd6e7a113df1596d01c83 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Tue, 23 Sep 2025 16:08:55 -0400 Subject: [PATCH 2/4] Construct outbound OMs before enqueueing them Makes the next commit cleaner when we add support for forwarding onion messages within send_onion_message_internal. Also rename enqueue_onion_message to specify that it is used for outbound onion messages. --- lightning/src/onion_message/messenger.rs | 57 +++++++++++++++--------- 1 file changed, 35 insertions(+), 22 deletions(-) diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index de889a94776..afdd97af276 100644 --- a/lightning/src/onion_message/messenger.rs +++ b/lightning/src/onion_message/messenger.rs @@ -1492,13 +1492,29 @@ where } => (destination, None), }; - let mut logger = WithContext::from(&self.logger, None, None, None); - let result = self.find_path(destination).and_then(|path| { - let first_hop = path.intermediate_nodes.get(0).map(|p| *p); - logger = WithContext::from(&self.logger, first_hop, None, None); - self.enqueue_onion_message(path, contents, reply_path, log_suffix) - }); + let path = self.find_path(destination).map_err(|e| { + log_trace!(self.logger, "Failed to find path {}", log_suffix); + e + })?; + let first_hop = path.intermediate_nodes.get(0).map(|p| *p); + let logger = WithContext::from(&self.logger, first_hop, None, None); + + log_trace!(logger, "Constructing onion message {}: {:?}", log_suffix, contents); + let (first_node_id, onion_message, addresses) = create_onion_message( + &self.entropy_source, + &self.node_signer, + &self.node_id_lookup, + &self.secp_ctx, + path, + contents, + reply_path, + ) + .map_err(|e| { + log_warn!(logger, "Failed to create onion message with {:?} {}", e, log_suffix); + e + })?; + let result = self.enqueue_outbound_onion_message(onion_message, first_node_id, addresses); match result.as_ref() { Err(SendError::GetNodeIdFailed) => { log_warn!(logger, "Unable to retrieve node id {}", log_suffix); @@ -1578,22 +1594,10 @@ where .map_err(|_| SendError::PathNotFound) } - fn enqueue_onion_message( - &self, path: OnionMessagePath, contents: T, reply_path: Option, - log_suffix: fmt::Arguments, + fn enqueue_outbound_onion_message( + &self, onion_message: OnionMessage, first_node_id: PublicKey, + addresses: Option>, ) -> Result { - log_trace!(self.logger, "Constructing onion message {}: {:?}", log_suffix, contents); - - let (first_node_id, onion_message, addresses) = create_onion_message( - &self.entropy_source, - &self.node_signer, - &self.node_id_lookup, - &self.secp_ctx, - path, - contents, - reply_path, - )?; - let mut message_recipients = self.message_recipients.lock().unwrap(); if outbound_buffer_full(&first_node_id, &message_recipients) { return Err(SendError::BufferFull); @@ -1713,7 +1717,16 @@ where pub fn send_onion_message_using_path( &self, path: OnionMessagePath, contents: T, reply_path: Option, ) -> Result { - self.enqueue_onion_message(path, contents, reply_path, format_args!("")) + let (first_node_id, onion_message, addresses) = create_onion_message( + &self.entropy_source, + &self.node_signer, + &self.node_id_lookup, + &self.secp_ctx, + path, + contents, + reply_path, + )?; + self.enqueue_outbound_onion_message(onion_message, first_node_id, addresses) } pub(crate) fn peel_onion_message( From 2460c465727b101d4888a06770973622db27ad6a Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Tue, 23 Sep 2025 16:54:45 -0400 Subject: [PATCH 3/4] Invoice server: treat forwarded invreqs as OM forwards Previously, when a static invoice server forwarded an invoice request to an often-offline recipient, they would treat the outbound message like any other outbound onion message initiated by their own node. That means they would buffer the onion message internally in the onion messenger and generate a ConnectionNeeded event if the next-hop node was offline. Buffering the onion message in this case poses a DoS risk for the invoice server node, since they do not control the quantity of invoice requests they receive on behalf of often-offline recipients. Instead, we should treat these forwarded invoice requests like any other onion message that needs to be forwarded -- if the next-hop node is offline, either drop the message or generate an OnionMessageIntercepted event for it (pushing the DoS management onto the handler of the interception event). --- lightning/src/events/mod.rs | 11 +++++ lightning/src/ln/async_payments_tests.rs | 15 ++++++ lightning/src/offers/flow.rs | 4 +- lightning/src/onion_message/async_payments.rs | 5 ++ lightning/src/onion_message/messenger.rs | 48 +++++++++++++++++-- 5 files changed, 76 insertions(+), 7 deletions(-) diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs index df89894f089..001b69675c5 100644 --- a/lightning/src/events/mod.rs +++ b/lightning/src/events/mod.rs @@ -1617,6 +1617,9 @@ pub enum Event { /// `OnionMessenger` was initialized with /// [`OnionMessenger::new_with_offline_peer_interception`], see its docs. /// + /// The offline peer should be awoken if possible on receipt of this event, such as via the LSPS5 + /// protocol. + /// /// # Failure Behavior and Persistence /// This event will eventually be replayed after failures-to-handle (i.e., the event handler /// returning `Err(ReplayEvent ())`), but won't be persisted across restarts. @@ -1661,6 +1664,14 @@ pub enum Event { /// recipient is online to provide a new invoice. This path should be persisted and /// later provided to [`ChannelManager::respond_to_static_invoice_request`]. /// + /// This path's [`BlindedMessagePath::introduction_node`] MUST be set to our node or one of our + /// peers. This is because, for DoS protection, invoice requests forwarded over this path are + /// treated by our node like any other onion message forward and will not generate + /// [`Event::ConnectionNeeded`] if the first hop in the path is not our peer. + /// + /// If the next-hop peer in the path is offline, if configured to do so we will generate an + /// [`Event::OnionMessageIntercepted`] for the invoice request. + /// /// [`ChannelManager::respond_to_static_invoice_request`]: crate::ln::channelmanager::ChannelManager::respond_to_static_invoice_request invoice_request_path: BlindedMessagePath, /// Useful for the recipient to replace a specific invoice stored by us as the static invoice diff --git a/lightning/src/ln/async_payments_tests.rs b/lightning/src/ln/async_payments_tests.rs index ccef4480efc..9d327d0060b 100644 --- a/lightning/src/ln/async_payments_tests.rs +++ b/lightning/src/ln/async_payments_tests.rs @@ -2887,6 +2887,21 @@ fn async_payment_e2e() { .into_iter() .find_map(|ev| { if let Event::OnionMessageIntercepted { message, .. } = ev { + // At least one of the intercepted onion messages will be an invoice request that the + // invoice server is attempting to forward to the recipient, ignore that as we're testing + // the static invoice flow + let peeled_onion = recipient.onion_messenger.peel_onion_message(&message).unwrap(); + if matches!( + peeled_onion, + PeeledOnion::Offers(OffersMessage::InvoiceRequest { .. }, _, _) + ) { + return None; + } + + assert!(matches!( + peeled_onion, + PeeledOnion::AsyncPayments(AsyncPaymentsMessage::HeldHtlcAvailable(_), _, _) + )); Some(message) } else { None diff --git a/lightning/src/offers/flow.rs b/lightning/src/offers/flow.rs index a6484f0076e..6b0132fda7a 100644 --- a/lightning/src/offers/flow.rs +++ b/lightning/src/offers/flow.rs @@ -1169,9 +1169,9 @@ where ) { let mut pending_offers_messages = self.pending_offers_messages.lock().unwrap(); let message = OffersMessage::InvoiceRequest(invoice_request); - let instructions = MessageSendInstructions::WithSpecifiedReplyPath { + let instructions = MessageSendInstructions::ForwardedMessage { destination: Destination::BlindedPath(destination), - reply_path: reply_path.into_blinded_path(), + reply_path: Some(reply_path.into_blinded_path()), }; pending_offers_messages.push((message, instructions)); } diff --git a/lightning/src/onion_message/async_payments.rs b/lightning/src/onion_message/async_payments.rs index 877af435bb4..127126e150f 100644 --- a/lightning/src/onion_message/async_payments.rs +++ b/lightning/src/onion_message/async_payments.rs @@ -169,6 +169,11 @@ pub struct ServeStaticInvoice { /// [`Bolt12Invoice`] if the recipient is online at the time. Use this path to forward the /// [`InvoiceRequest`] to the async recipient. /// + /// This path's [`BlindedMessagePath::introduction_node`] MUST be set to the static invoice server + /// node or one of its peers. This is because, for DoS protection, invoice requests forwarded over + /// this path are treated by the server node like any other onion message forward and the server + /// will not directly connect to the introduction node if they are not already peers. + /// /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest /// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice pub forward_invoice_request_path: BlindedMessagePath, diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index afdd97af276..cb6651588bd 100644 --- a/lightning/src/onion_message/messenger.rs +++ b/lightning/src/onion_message/messenger.rs @@ -490,6 +490,21 @@ pub enum MessageSendInstructions { /// The instructions provided by the [`Responder`]. instructions: ResponseInstruction, }, + /// Indicates that this onion message did not originate from our node and is being forwarded + /// through us from another node on the network to the destination. + /// + /// We separate out this case because forwarded onion messages are treated differently from + /// outbound onion messages initiated by our node. Outbounds are buffered internally, whereas, for + /// DoS protection, forwards should never be buffered internally and instead will either be + /// dropped or generate an [`Event::OnionMessageIntercepted`] if the next-hop node is + /// disconnected. + ForwardedMessage { + /// The destination where we need to send the forwarded onion message. + destination: Destination, + /// The reply path which should be included in the message, that terminates at the original + /// sender of this forwarded message. + reply_path: Option, + }, } /// A trait defining behavior for routing an [`OnionMessage`]. @@ -1467,6 +1482,7 @@ where fn send_onion_message_internal( &self, contents: T, instructions: MessageSendInstructions, log_suffix: fmt::Arguments, ) -> Result { + let is_forward = matches!(instructions, MessageSendInstructions::ForwardedMessage { .. }); let (destination, reply_path) = match instructions { MessageSendInstructions::WithSpecifiedReplyPath { destination, reply_path } => { (destination, Some(reply_path)) @@ -1490,12 +1506,24 @@ where | MessageSendInstructions::ForReply { instructions: ResponseInstruction { destination, context: None }, } => (destination, None), + MessageSendInstructions::ForwardedMessage { destination, reply_path } => { + (destination, reply_path) + }, }; - let path = self.find_path(destination).map_err(|e| { - log_trace!(self.logger, "Failed to find path {}", log_suffix); - e - })?; + let path = if is_forward { + // If this onion message is being treated as a forward, we shouldn't pathfind to the next hop. + OnionMessagePath { + intermediate_nodes: Vec::new(), + first_node_addresses: None, + destination, + } + } else { + self.find_path(destination).map_err(|e| { + log_trace!(self.logger, "Failed to find path {}", log_suffix); + e + })? + }; let first_hop = path.intermediate_nodes.get(0).map(|p| *p); let logger = WithContext::from(&self.logger, first_hop, None, None); @@ -1514,7 +1542,17 @@ where e })?; - let result = self.enqueue_outbound_onion_message(onion_message, first_node_id, addresses); + let result = if is_forward { + self.enqueue_forwarded_onion_message( + NextMessageHop::NodeId(first_node_id), + onion_message, + log_suffix, + ) + .map(|()| SendSuccess::Buffered) + } else { + self.enqueue_outbound_onion_message(onion_message, first_node_id, addresses) + }; + match result.as_ref() { Err(SendError::GetNodeIdFailed) => { log_warn!(logger, "Unable to retrieve node id {}", log_suffix); From 1bd3f367883502bbcc6b7f4efe6ce49756681159 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Wed, 24 Sep 2025 13:39:23 -0400 Subject: [PATCH 4/4] Generate direct-connect event for private offline peers Imagine an LSP with a mobile client. The LSP wants to pay an offer that client issued. It generates an onion messages invreq and sends it. The OnionMessenger notices it's not connected and searches the network graph. It doesn't find the mobile client so doesn't create a direct connect event. The message gets dropped and the payment fails. Here we start generating the DC event irrespective of the network graph so the LSP can use LSPS5 to wake the client. --- fuzz/src/onion_message.rs | 2 +- lightning-dns-resolver/src/lib.rs | 2 +- lightning/src/events/mod.rs | 4 +- .../src/onion_message/functional_tests.rs | 33 +++++++++++--- lightning/src/onion_message/messenger.rs | 44 ++++++++++--------- 5 files changed, 57 insertions(+), 28 deletions(-) diff --git a/fuzz/src/onion_message.rs b/fuzz/src/onion_message.rs index 7c979fa1462..934d748d6a5 100644 --- a/fuzz/src/onion_message.rs +++ b/fuzz/src/onion_message.rs @@ -102,7 +102,7 @@ impl MessageRouter for TestMessageRouter { fn find_path( &self, _sender: PublicKey, _peers: Vec, destination: Destination, ) -> Result { - Ok(OnionMessagePath { intermediate_nodes: vec![], destination, first_node_addresses: None }) + Ok(OnionMessagePath { intermediate_nodes: vec![], destination, first_node_addresses: vec![] }) } fn create_blinded_paths( diff --git a/lightning-dns-resolver/src/lib.rs b/lightning-dns-resolver/src/lib.rs index 55e3ad7dd50..75fe06fcc9a 100644 --- a/lightning-dns-resolver/src/lib.rs +++ b/lightning-dns-resolver/src/lib.rs @@ -222,7 +222,7 @@ mod test { ) -> Result { Ok(OnionMessagePath { destination, - first_node_addresses: None, + first_node_addresses: Vec::new(), intermediate_nodes: Vec::new(), }) } diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs index 001b69675c5..d7a13d59e3d 100644 --- a/lightning/src/events/mod.rs +++ b/lightning/src/events/mod.rs @@ -972,7 +972,9 @@ pub enum Event { ConnectionNeeded { /// The node id for the node needing a connection. node_id: PublicKey, - /// Sockets for connecting to the node. + /// Sockets for connecting to the node, if available. We don't require these addresses to be + /// present in case the node id corresponds to a known peer that is offline and can be awoken, + /// such as via the LSPS5 protocol. addresses: Vec, }, /// Indicates a [`Bolt12Invoice`] in response to an [`InvoiceRequest`] or a [`Refund`] was diff --git a/lightning/src/onion_message/functional_tests.rs b/lightning/src/onion_message/functional_tests.rs index 1cbea9fef8d..605a81a4f95 100644 --- a/lightning/src/onion_message/functional_tests.rs +++ b/lightning/src/onion_message/functional_tests.rs @@ -419,7 +419,7 @@ fn two_unblinded_hops() { let path = OnionMessagePath { intermediate_nodes: vec![nodes[1].node_id], destination: Destination::Node(nodes[2].node_id), - first_node_addresses: None, + first_node_addresses: Vec::new(), }; nodes[0].messenger.send_onion_message_using_path(path, test_msg, None).unwrap(); @@ -494,7 +494,7 @@ fn two_unblinded_two_blinded() { let path = OnionMessagePath { intermediate_nodes: vec![nodes[1].node_id, nodes[2].node_id], destination: Destination::BlindedPath(blinded_path), - first_node_addresses: None, + first_node_addresses: Vec::new(), }; nodes[0].messenger.send_onion_message_using_path(path, test_msg, None).unwrap(); @@ -660,7 +660,7 @@ fn too_big_packet_error() { let path = OnionMessagePath { intermediate_nodes: hops, destination: Destination::Node(hop_node_id), - first_node_addresses: None, + first_node_addresses: Vec::new(), }; let err = nodes[0].messenger.send_onion_message_using_path(path, test_msg, None).unwrap_err(); assert_eq!(err, SendError::TooBigPacket); @@ -822,7 +822,7 @@ fn reply_path() { let path = OnionMessagePath { intermediate_nodes: vec![nodes[1].node_id, nodes[2].node_id], destination: Destination::Node(nodes[3].node_id), - first_node_addresses: None, + first_node_addresses: Vec::new(), }; let intermediate_nodes = [ MessageForwardNode { node_id: nodes[2].node_id, short_channel_id: None }, @@ -959,7 +959,7 @@ fn many_hops() { let path = OnionMessagePath { intermediate_nodes, destination: Destination::Node(nodes[num_nodes - 1].node_id), - first_node_addresses: None, + first_node_addresses: Vec::new(), }; nodes[0].messenger.send_onion_message_using_path(path, test_msg, None).unwrap(); nodes[num_nodes - 1].custom_message_handler.expect_message(TestCustomMessage::Pong); @@ -1012,6 +1012,29 @@ fn requests_peer_connection_for_buffered_messages() { connect_peers(&nodes[0], &nodes[1]); assert!(nodes[0].messenger.next_onion_message_for_peer(nodes[1].node_id).is_some()); assert!(nodes[0].messenger.next_onion_message_for_peer(nodes[1].node_id).is_none()); + + // Buffer an onion message for a disconnected node who is not in the network graph. + disconnect_peers(&nodes[0], &nodes[2]); + + let message = TestCustomMessage::Ping; + let destination = Destination::Node(nodes[2].node_id); + let instructions = MessageSendInstructions::WithoutReplyPath { destination }; + nodes[0].messenger.send_onion_message(message.clone(), instructions.clone()).unwrap(); + + // Check that a ConnectionNeeded event for the peer is provided + let events = release_events(&nodes[0]); + assert_eq!(events.len(), 1); + match &events[0] { + Event::ConnectionNeeded { node_id, addresses } => { + assert_eq!(*node_id, nodes[2].node_id); + assert!(addresses.is_empty()); + }, + e => panic!("Unexpected event: {:?}", e), + } + + // Release the buffered onion message when reconnected + connect_peers(&nodes[0], &nodes[2]); + assert!(nodes[0].messenger.next_onion_message_for_peer(nodes[2].node_id).is_some()); } #[test] diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index cb6651588bd..890eee8859b 100644 --- a/lightning/src/onion_message/messenger.rs +++ b/lightning/src/onion_message/messenger.rs @@ -210,7 +210,7 @@ where /// # Ok(OnionMessagePath { /// # intermediate_nodes: vec![hop_node_id1, hop_node_id2], /// # destination, -/// # first_node_addresses: None, +/// # first_node_addresses: Vec::new(), /// # }) /// # } /// # fn create_blinded_paths( @@ -681,7 +681,7 @@ where Ok(OnionMessagePath { intermediate_nodes: vec![], destination, - first_node_addresses: None, + first_node_addresses: vec![], }) } else { let node_details = network_graph @@ -695,11 +695,19 @@ where Some((features, addresses)) if features.supports_onion_messages() && addresses.len() > 0 => { - let first_node_addresses = Some(addresses.to_vec()); Ok(OnionMessagePath { intermediate_nodes: vec![], destination, - first_node_addresses, + first_node_addresses: addresses.to_vec(), + }) + }, + None => { + // If the destination is an unannounced node, they may be a known peer that is offline and + // can be woken by the sender. + Ok(OnionMessagePath { + intermediate_nodes: vec![], + destination, + first_node_addresses: vec![], }) }, _ => Err(()), @@ -841,9 +849,9 @@ pub struct OnionMessagePath { /// Addresses that may be used to connect to [`OnionMessagePath::first_node`]. /// - /// Only needs to be set if a connection to the node is required. [`OnionMessenger`] may use - /// this to initiate such a connection. - pub first_node_addresses: Option>, + /// Only needs to be filled in if a connection to the node is required and it is not a known peer. + /// [`OnionMessenger`] may use this to initiate such a connection. + pub first_node_addresses: Vec, } impl OnionMessagePath { @@ -1021,7 +1029,7 @@ pub fn create_onion_message_resolving_destination< entropy_source: &ES, node_signer: &NS, node_id_lookup: &NL, network_graph: &ReadOnlyNetworkGraph, secp_ctx: &Secp256k1, mut path: OnionMessagePath, contents: T, reply_path: Option, -) -> Result<(PublicKey, OnionMessage, Option>), SendError> +) -> Result<(PublicKey, OnionMessage, Vec), SendError> where ES::Target: EntropySource, NS::Target: NodeSigner, @@ -1054,7 +1062,7 @@ pub fn create_onion_message, path: OnionMessagePath, contents: T, reply_path: Option, -) -> Result<(PublicKey, OnionMessage, Option>), SendError> +) -> Result<(PublicKey, OnionMessage, Vec), SendError> where ES::Target: EntropySource, NS::Target: NodeSigner, @@ -1515,7 +1523,7 @@ where // If this onion message is being treated as a forward, we shouldn't pathfind to the next hop. OnionMessagePath { intermediate_nodes: Vec::new(), - first_node_addresses: None, + first_node_addresses: Vec::new(), destination, } } else { @@ -1633,8 +1641,7 @@ where } fn enqueue_outbound_onion_message( - &self, onion_message: OnionMessage, first_node_id: PublicKey, - addresses: Option>, + &self, onion_message: OnionMessage, first_node_id: PublicKey, addresses: Vec, ) -> Result { let mut message_recipients = self.message_recipients.lock().unwrap(); if outbound_buffer_full(&first_node_id, &message_recipients) { @@ -1642,14 +1649,11 @@ where } match message_recipients.entry(first_node_id) { - hash_map::Entry::Vacant(e) => match addresses { - None => Err(SendError::InvalidFirstHop(first_node_id)), - Some(addresses) => { - e.insert(OnionMessageRecipient::pending_connection(addresses)) - .enqueue_message(onion_message); - self.event_notifier.notify(); - Ok(SendSuccess::BufferedAwaitingConnection(first_node_id)) - }, + hash_map::Entry::Vacant(e) => { + e.insert(OnionMessageRecipient::pending_connection(addresses)) + .enqueue_message(onion_message); + self.event_notifier.notify(); + Ok(SendSuccess::BufferedAwaitingConnection(first_node_id)) }, hash_map::Entry::Occupied(mut e) => { e.get_mut().enqueue_message(onion_message);