Skip to content

Commit 7926fb9

Browse files
authored
Merge pull request #4114 from valentinewallace/2025-09-fwd-invreq-in-mailbox
Static invoice server: treat forwarded invoice requests as onion message forwards
2 parents dea125a + 1bd3f36 commit 7926fb9

File tree

8 files changed

+236
-102
lines changed

8 files changed

+236
-102
lines changed

fuzz/src/onion_message.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ impl MessageRouter for TestMessageRouter {
102102
fn find_path(
103103
&self, _sender: PublicKey, _peers: Vec<PublicKey>, destination: Destination,
104104
) -> Result<OnionMessagePath, ()> {
105-
Ok(OnionMessagePath { intermediate_nodes: vec![], destination, first_node_addresses: None })
105+
Ok(OnionMessagePath { intermediate_nodes: vec![], destination, first_node_addresses: vec![] })
106106
}
107107

108108
fn create_blinded_paths<T: secp256k1::Signing + secp256k1::Verification>(
@@ -430,7 +430,7 @@ mod tests {
430430
super::do_test(&<Vec<u8>>::from_hex(two_unblinded_hops_om).unwrap(), &logger);
431431
{
432432
let log_entries = logger.lines.lock().unwrap();
433-
assert_eq!(log_entries.get(&("lightning::onion_message::messenger".to_string(), "Forwarding an onion message to peer 020202020202020202020202020202020202020202020202020202020202020202".to_string())), Some(&1));
433+
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));
434434
}
435435

436436
let two_unblinded_two_blinded_om = "\
@@ -471,7 +471,7 @@ mod tests {
471471
super::do_test(&<Vec<u8>>::from_hex(two_unblinded_two_blinded_om).unwrap(), &logger);
472472
{
473473
let log_entries = logger.lines.lock().unwrap();
474-
assert_eq!(log_entries.get(&("lightning::onion_message::messenger".to_string(), "Forwarding an onion message to peer 020202020202020202020202020202020202020202020202020202020202020202".to_string())), Some(&1));
474+
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));
475475
}
476476

477477
let three_blinded_om = "\
@@ -512,7 +512,7 @@ mod tests {
512512
super::do_test(&<Vec<u8>>::from_hex(three_blinded_om).unwrap(), &logger);
513513
{
514514
let log_entries = logger.lines.lock().unwrap();
515-
assert_eq!(log_entries.get(&("lightning::onion_message::messenger".to_string(), "Forwarding an onion message to peer 020202020202020202020202020202020202020202020202020202020202020202".to_string())), Some(&1));
515+
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));
516516
}
517517
}
518518
}

lightning-dns-resolver/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ mod test {
222222
) -> Result<OnionMessagePath, ()> {
223223
Ok(OnionMessagePath {
224224
destination,
225-
first_node_addresses: None,
225+
first_node_addresses: Vec::new(),
226226
intermediate_nodes: Vec::new(),
227227
})
228228
}

lightning/src/events/mod.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -972,7 +972,9 @@ pub enum Event {
972972
ConnectionNeeded {
973973
/// The node id for the node needing a connection.
974974
node_id: PublicKey,
975-
/// Sockets for connecting to the node.
975+
/// Sockets for connecting to the node, if available. We don't require these addresses to be
976+
/// present in case the node id corresponds to a known peer that is offline and can be awoken,
977+
/// such as via the LSPS5 protocol.
976978
addresses: Vec<msgs::SocketAddress>,
977979
},
978980
/// Indicates a [`Bolt12Invoice`] in response to an [`InvoiceRequest`] or a [`Refund`] was
@@ -1617,6 +1619,9 @@ pub enum Event {
16171619
/// `OnionMessenger` was initialized with
16181620
/// [`OnionMessenger::new_with_offline_peer_interception`], see its docs.
16191621
///
1622+
/// The offline peer should be awoken if possible on receipt of this event, such as via the LSPS5
1623+
/// protocol.
1624+
///
16201625
/// # Failure Behavior and Persistence
16211626
/// This event will eventually be replayed after failures-to-handle (i.e., the event handler
16221627
/// returning `Err(ReplayEvent ())`), but won't be persisted across restarts.
@@ -1661,6 +1666,14 @@ pub enum Event {
16611666
/// recipient is online to provide a new invoice. This path should be persisted and
16621667
/// later provided to [`ChannelManager::respond_to_static_invoice_request`].
16631668
///
1669+
/// This path's [`BlindedMessagePath::introduction_node`] MUST be set to our node or one of our
1670+
/// peers. This is because, for DoS protection, invoice requests forwarded over this path are
1671+
/// treated by our node like any other onion message forward and will not generate
1672+
/// [`Event::ConnectionNeeded`] if the first hop in the path is not our peer.
1673+
///
1674+
/// If the next-hop peer in the path is offline, if configured to do so we will generate an
1675+
/// [`Event::OnionMessageIntercepted`] for the invoice request.
1676+
///
16641677
/// [`ChannelManager::respond_to_static_invoice_request`]: crate::ln::channelmanager::ChannelManager::respond_to_static_invoice_request
16651678
invoice_request_path: BlindedMessagePath,
16661679
/// Useful for the recipient to replace a specific invoice stored by us as the static invoice

lightning/src/ln/async_payments_tests.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2887,6 +2887,21 @@ fn async_payment_e2e() {
28872887
.into_iter()
28882888
.find_map(|ev| {
28892889
if let Event::OnionMessageIntercepted { message, .. } = ev {
2890+
// At least one of the intercepted onion messages will be an invoice request that the
2891+
// invoice server is attempting to forward to the recipient, ignore that as we're testing
2892+
// the static invoice flow
2893+
let peeled_onion = recipient.onion_messenger.peel_onion_message(&message).unwrap();
2894+
if matches!(
2895+
peeled_onion,
2896+
PeeledOnion::Offers(OffersMessage::InvoiceRequest { .. }, _, _)
2897+
) {
2898+
return None;
2899+
}
2900+
2901+
assert!(matches!(
2902+
peeled_onion,
2903+
PeeledOnion::AsyncPayments(AsyncPaymentsMessage::HeldHtlcAvailable(_), _, _)
2904+
));
28902905
Some(message)
28912906
} else {
28922907
None

lightning/src/offers/flow.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1169,9 +1169,9 @@ where
11691169
) {
11701170
let mut pending_offers_messages = self.pending_offers_messages.lock().unwrap();
11711171
let message = OffersMessage::InvoiceRequest(invoice_request);
1172-
let instructions = MessageSendInstructions::WithSpecifiedReplyPath {
1172+
let instructions = MessageSendInstructions::ForwardedMessage {
11731173
destination: Destination::BlindedPath(destination),
1174-
reply_path: reply_path.into_blinded_path(),
1174+
reply_path: Some(reply_path.into_blinded_path()),
11751175
};
11761176
pending_offers_messages.push((message, instructions));
11771177
}

lightning/src/onion_message/async_payments.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,11 @@ pub struct ServeStaticInvoice {
169169
/// [`Bolt12Invoice`] if the recipient is online at the time. Use this path to forward the
170170
/// [`InvoiceRequest`] to the async recipient.
171171
///
172+
/// This path's [`BlindedMessagePath::introduction_node`] MUST be set to the static invoice server
173+
/// node or one of its peers. This is because, for DoS protection, invoice requests forwarded over
174+
/// this path are treated by the server node like any other onion message forward and the server
175+
/// will not directly connect to the introduction node if they are not already peers.
176+
///
172177
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
173178
/// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice
174179
pub forward_invoice_request_path: BlindedMessagePath,

lightning/src/onion_message/functional_tests.rs

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -419,7 +419,7 @@ fn two_unblinded_hops() {
419419
let path = OnionMessagePath {
420420
intermediate_nodes: vec![nodes[1].node_id],
421421
destination: Destination::Node(nodes[2].node_id),
422-
first_node_addresses: None,
422+
first_node_addresses: Vec::new(),
423423
};
424424

425425
nodes[0].messenger.send_onion_message_using_path(path, test_msg, None).unwrap();
@@ -494,7 +494,7 @@ fn two_unblinded_two_blinded() {
494494
let path = OnionMessagePath {
495495
intermediate_nodes: vec![nodes[1].node_id, nodes[2].node_id],
496496
destination: Destination::BlindedPath(blinded_path),
497-
first_node_addresses: None,
497+
first_node_addresses: Vec::new(),
498498
};
499499

500500
nodes[0].messenger.send_onion_message_using_path(path, test_msg, None).unwrap();
@@ -660,7 +660,7 @@ fn too_big_packet_error() {
660660
let path = OnionMessagePath {
661661
intermediate_nodes: hops,
662662
destination: Destination::Node(hop_node_id),
663-
first_node_addresses: None,
663+
first_node_addresses: Vec::new(),
664664
};
665665
let err = nodes[0].messenger.send_onion_message_using_path(path, test_msg, None).unwrap_err();
666666
assert_eq!(err, SendError::TooBigPacket);
@@ -822,7 +822,7 @@ fn reply_path() {
822822
let path = OnionMessagePath {
823823
intermediate_nodes: vec![nodes[1].node_id, nodes[2].node_id],
824824
destination: Destination::Node(nodes[3].node_id),
825-
first_node_addresses: None,
825+
first_node_addresses: Vec::new(),
826826
};
827827
let intermediate_nodes = [
828828
MessageForwardNode { node_id: nodes[2].node_id, short_channel_id: None },
@@ -959,7 +959,7 @@ fn many_hops() {
959959
let path = OnionMessagePath {
960960
intermediate_nodes,
961961
destination: Destination::Node(nodes[num_nodes - 1].node_id),
962-
first_node_addresses: None,
962+
first_node_addresses: Vec::new(),
963963
};
964964
nodes[0].messenger.send_onion_message_using_path(path, test_msg, None).unwrap();
965965
nodes[num_nodes - 1].custom_message_handler.expect_message(TestCustomMessage::Pong);
@@ -1012,6 +1012,29 @@ fn requests_peer_connection_for_buffered_messages() {
10121012
connect_peers(&nodes[0], &nodes[1]);
10131013
assert!(nodes[0].messenger.next_onion_message_for_peer(nodes[1].node_id).is_some());
10141014
assert!(nodes[0].messenger.next_onion_message_for_peer(nodes[1].node_id).is_none());
1015+
1016+
// Buffer an onion message for a disconnected node who is not in the network graph.
1017+
disconnect_peers(&nodes[0], &nodes[2]);
1018+
1019+
let message = TestCustomMessage::Ping;
1020+
let destination = Destination::Node(nodes[2].node_id);
1021+
let instructions = MessageSendInstructions::WithoutReplyPath { destination };
1022+
nodes[0].messenger.send_onion_message(message.clone(), instructions.clone()).unwrap();
1023+
1024+
// Check that a ConnectionNeeded event for the peer is provided
1025+
let events = release_events(&nodes[0]);
1026+
assert_eq!(events.len(), 1);
1027+
match &events[0] {
1028+
Event::ConnectionNeeded { node_id, addresses } => {
1029+
assert_eq!(*node_id, nodes[2].node_id);
1030+
assert!(addresses.is_empty());
1031+
},
1032+
e => panic!("Unexpected event: {:?}", e),
1033+
}
1034+
1035+
// Release the buffered onion message when reconnected
1036+
connect_peers(&nodes[0], &nodes[2]);
1037+
assert!(nodes[0].messenger.next_onion_message_for_peer(nodes[2].node_id).is_some());
10151038
}
10161039

10171040
#[test]

0 commit comments

Comments
 (0)