Generate direct-connect event for private offline peers
What changed, and why it matters
This change fixes a functional bug where a Lightning Service Provider (LSP) could fail to pay a mobile client that issued a BOLT12 offer. Previously, if the client was offline and not publicly announced in the network graph, the onion messenger would silently drop the message instead of asking the application to connect to the peer. Now it always emits a 'please connect to this peer' event, even when no network-graph addresses are known, so the app can use a separate wake-up protocol (LSPS5) to reach the client. It is a reliability/availability improvement, not a cryptographic vulnerability.
Review as a normal functional/reliability improvement. No urgent security response is indicated. If deploying, ensure downstream handlers of ConnectionNeeded events tolerate empty address vectors and integrate with the intended wake-up mechanism (e.g., LSPS5).
Security signals we found
Behavioral change in message routing for unannounced/offline peers
ConnectionNeeded event now emitted without requiring network graph presence
No cryptographic, authentication, or memory-safety changes observed
No incident, CVE, or vendor security disclosure referenced in commit
Evidence from the diff
The patch changes OnionMessagePath.first_node_addresses from Option
Changed components
lightning/src/onion_message/messenger.rslightning/src/events/mod.rslightning/src/onion_message/functional_tests.rsfuzz/src/onion_message.rslightning-dns-resolver/src/lib.rsInspect captured patch +57 / −28
diff --git a/fuzz/src/onion_message.rs b/fuzz/src/onion_message.rs
index 7c979fa..934d748 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<PublicKey>, destination: Destination,
) -> Result<OnionMessagePath, ()> {
- Ok(OnionMessagePath { intermediate_nodes: vec![], destination, first_node_addresses: None })
+ Ok(OnionMessagePath { intermediate_nodes: vec![], destination, first_node_addresses: vec![] })
}
fn create_blinded_paths<T: secp256k1::Signing + secp256k1::Verification>(
diff --git a/lightning-dns-resolver/src/lib.rs b/lightning-dns-resolver/src/lib.rs
index 55e3ad7..75fe06f 100644
--- a/lightning-dns-resolver/src/lib.rs
+++ b/lightning-dns-resolver/src/lib.rs
@@ -222,7 +222,7 @@ mod test {
) -> Result<OnionMessagePath, ()> {
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 001b696..d7a13d5 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<msgs::SocketAddress>,
},
/// 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 1cbea9f..605a81a 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 cb66515..890eee8 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<T: secp256k1::Signing + secp256k1::Verification>(
@@ -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<Vec<SocketAddress>>,
+ /// 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<SocketAddress>,
}
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<secp256k1::All>,
mut path: OnionMessagePath, contents: T, reply_path: Option<BlindedMessagePath>,
-) -> Result<(PublicKey, OnionMessage, Option<Vec<SocketAddress>>), SendError>
+) -> Result<(PublicKey, OnionMessage, Vec<SocketAddress>), SendError>
where
ES::Target: EntropySource,
NS::Target: NodeSigner,
@@ -1054,7 +1062,7 @@ pub fn create_onion_message<ES: Deref, NS: Deref, NL: Deref, T: OnionMessageCont
entropy_source: &ES, node_signer: &NS, node_id_lookup: &NL,
secp_ctx: &Secp256k1<secp256k1::All>, path: OnionMessagePath, contents: T,
reply_path: Option<BlindedMessagePath>,
-) -> Result<(PublicKey, OnionMessage, Option<Vec<SocketAddress>>), SendError>
+) -> Result<(PublicKey, OnionMessage, Vec<SocketAddress>), 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<Vec<SocketAddress>>,
+ &self, onion_message: OnionMessage, first_node_id: PublicKey, addresses: Vec<SocketAddress>,
) -> Result<SendSuccess, SendError> {
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);
Why this scored 34/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.