Skip to content

Commit 8c08a30

Browse files
committed
Stop using an introduction node in blinded message paths
lnd is preparing to ship a release with opt-in onion messages without support for forwarding onion messages from non-channel peers. This breaks the common BOLT 12 OM flow today where we direct-connect to the blinded path introduction point and send the `invoice_request` without a channel. For CLN it turns out this is fine as they never select a peer for their introduction point at all. However, for LDK this would break existing nodes as nodes might now pick an lnd peer as an introduction node but it won't forward the onion message. For now, we just drop the separate introduction point selection and just always use ourselves as an introduction point (assuming we're an announced node). This should also have the side-effect of making offers marginally more robust, which may be worth it, even if it sucks to drop any pretense of privacy.
1 parent 0c37f08 commit 8c08a30

2 files changed

Lines changed: 35 additions & 191 deletions

File tree

lightning/src/ln/offers_tests.rs

Lines changed: 2 additions & 155 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ use crate::ln::channelmanager::{PaymentId, RecentPaymentDetails, self};
5656
use crate::ln::outbound_payment::{Bolt12PaymentError, RecipientOnionFields, Retry};
5757
use crate::types::features::Bolt12InvoiceFeatures;
5858
use crate::ln::functional_test_utils::*;
59-
use crate::ln::msgs::{BaseMessageHandler, ChannelMessageHandler, Init, NodeAnnouncement, OnionMessage, OnionMessageHandler, RoutingMessageHandler, SocketAddress, UnsignedGossipMessage, UnsignedNodeAnnouncement};
59+
use crate::ln::msgs::{BaseMessageHandler, ChannelMessageHandler, Init, OnionMessage, OnionMessageHandler};
6060
use crate::ln::outbound_payment::IDEMPOTENCY_TIMEOUT_TICKS;
6161
use crate::offers::invoice::Bolt12Invoice;
6262
use crate::offers::invoice_error::InvoiceError;
@@ -66,9 +66,8 @@ use crate::offers::offer::OfferBuilder;
6666
use crate::offers::parse::Bolt12SemanticError;
6767
use crate::onion_message::messenger::{DefaultMessageRouter, Destination, MessageRouter, MessageSendInstructions, NodeIdMessageRouter, NullMessageRouter, PeeledOnion, DUMMY_HOPS_PATH_LENGTH, QR_CODED_DUMMY_HOPS_PATH_LENGTH};
6868
use crate::onion_message::offers::OffersMessage;
69-
use crate::routing::gossip::{NodeAlias, NodeId};
7069
use crate::routing::router::{DEFAULT_PAYMENT_DUMMY_HOPS, PaymentParameters, RouteParameters, RouteParametersConfig};
71-
use crate::sign::{NodeSigner, Recipient};
70+
use crate::sign::NodeSigner;
7271
use crate::util::ser::Writeable;
7372

7473
/// This used to determine whether we built a compact path or not, but now its just a random
@@ -125,38 +124,6 @@ fn disconnect_peers<'a, 'b, 'c>(node_a: &Node<'a, 'b, 'c>, peers: &[&Node<'a, 'b
125124
}
126125
}
127126

128-
fn announce_node_address<'a, 'b, 'c>(
129-
node: &Node<'a, 'b, 'c>, peers: &[&Node<'a, 'b, 'c>], address: SocketAddress,
130-
) {
131-
let features = node.onion_messenger.provided_node_features()
132-
| node.gossip_sync.provided_node_features();
133-
let rgb = [0u8; 3];
134-
let announcement = UnsignedNodeAnnouncement {
135-
features,
136-
timestamp: 1000,
137-
node_id: NodeId::from_pubkey(&node.keys_manager.get_node_id(Recipient::Node).unwrap()),
138-
rgb,
139-
alias: NodeAlias([0u8; 32]),
140-
addresses: vec![address],
141-
excess_address_data: Vec::new(),
142-
excess_data: Vec::new(),
143-
};
144-
let signature = node.keys_manager.sign_gossip_message(
145-
UnsignedGossipMessage::NodeAnnouncement(&announcement)
146-
).unwrap();
147-
148-
let msg = NodeAnnouncement {
149-
signature,
150-
contents: announcement
151-
};
152-
153-
let node_pubkey = node.node.get_our_node_id();
154-
node.gossip_sync.handle_node_announcement(None, &msg).unwrap();
155-
for peer in peers {
156-
peer.gossip_sync.handle_node_announcement(Some(node_pubkey), &msg).unwrap();
157-
}
158-
}
159-
160127
fn resolve_introduction_node<'a, 'b, 'c>(node: &Node<'a, 'b, 'c>, path: &BlindedMessagePath) -> PublicKey {
161128
path.public_introduction_node_id(&node.network_graph.read_only())
162129
.and_then(|node_id| node_id.as_pubkey().ok())
@@ -362,126 +329,6 @@ fn create_refund_with_no_blinded_path() {
362329
assert!(refund.paths().is_empty());
363330
}
364331

365-
/// Checks that blinded paths without Tor-only nodes are preferred when constructing an offer.
366-
#[test]
367-
fn prefers_non_tor_nodes_in_blinded_paths() {
368-
let mut accept_forward_cfg = test_default_channel_config();
369-
accept_forward_cfg.accept_forwards_to_priv_channels = true;
370-
371-
let mut features = channelmanager::provided_init_features(&accept_forward_cfg);
372-
features.set_onion_messages_optional();
373-
features.set_route_blinding_optional();
374-
375-
let chanmon_cfgs = create_chanmon_cfgs(6);
376-
let node_cfgs = create_node_cfgs(6, &chanmon_cfgs);
377-
378-
*node_cfgs[1].override_init_features.borrow_mut() = Some(features);
379-
380-
let node_chanmgrs = create_node_chanmgrs(
381-
6, &node_cfgs, &[None, Some(accept_forward_cfg), None, None, None, None]
382-
);
383-
let nodes = create_network(6, &node_cfgs, &node_chanmgrs);
384-
385-
create_unannounced_chan_between_nodes_with_value(&nodes, 0, 1, 10_000_000, 1_000_000_000);
386-
create_unannounced_chan_between_nodes_with_value(&nodes, 2, 3, 10_000_000, 1_000_000_000);
387-
create_announced_chan_between_nodes_with_value(&nodes, 1, 2, 10_000_000, 1_000_000_000);
388-
create_announced_chan_between_nodes_with_value(&nodes, 1, 4, 10_000_000, 1_000_000_000);
389-
create_announced_chan_between_nodes_with_value(&nodes, 1, 5, 10_000_000, 1_000_000_000);
390-
create_announced_chan_between_nodes_with_value(&nodes, 2, 4, 10_000_000, 1_000_000_000);
391-
create_announced_chan_between_nodes_with_value(&nodes, 2, 5, 10_000_000, 1_000_000_000);
392-
393-
// Add an extra channel so that more than one of Bob's peers have MIN_PEER_CHANNELS.
394-
create_announced_chan_between_nodes_with_value(&nodes, 4, 5, 10_000_000, 1_000_000_000);
395-
396-
let (alice, bob, charlie, david) = (&nodes[0], &nodes[1], &nodes[2], &nodes[3]);
397-
let bob_id = bob.node.get_our_node_id();
398-
let charlie_id = charlie.node.get_our_node_id();
399-
400-
disconnect_peers(alice, &[charlie, david, &nodes[4], &nodes[5]]);
401-
disconnect_peers(david, &[bob, &nodes[4], &nodes[5]]);
402-
403-
let tor = SocketAddress::OnionV2([255, 254, 253, 252, 251, 250, 249, 248, 247, 246, 38, 7]);
404-
announce_node_address(charlie, &[alice, bob, david, &nodes[4], &nodes[5]], tor.clone());
405-
406-
let offer = bob.node
407-
.create_offer_builder().unwrap()
408-
.amount_msats(10_000_000)
409-
.build().unwrap();
410-
assert_ne!(offer.issuer_signing_pubkey(), Some(bob_id));
411-
assert!(!offer.paths().is_empty());
412-
for path in offer.paths() {
413-
let introduction_node_id = resolve_introduction_node(david, &path);
414-
assert_ne!(introduction_node_id, bob_id);
415-
assert_ne!(introduction_node_id, charlie_id);
416-
}
417-
418-
// Use a one-hop blinded path when Bob is announced and all his peers are Tor-only.
419-
announce_node_address(&nodes[4], &[alice, bob, charlie, david, &nodes[5]], tor.clone());
420-
announce_node_address(&nodes[5], &[alice, bob, charlie, david, &nodes[4]], tor.clone());
421-
422-
let offer = bob.node
423-
.create_offer_builder().unwrap()
424-
.amount_msats(10_000_000)
425-
.build().unwrap();
426-
assert_ne!(offer.issuer_signing_pubkey(), Some(bob_id));
427-
assert!(!offer.paths().is_empty());
428-
for path in offer.paths() {
429-
let introduction_node_id = resolve_introduction_node(david, &path);
430-
assert_eq!(introduction_node_id, bob_id);
431-
}
432-
}
433-
434-
/// Checks that blinded paths prefer an introduction node that is the most connected.
435-
#[test]
436-
fn prefers_more_connected_nodes_in_blinded_paths() {
437-
let mut accept_forward_cfg = test_default_channel_config();
438-
accept_forward_cfg.accept_forwards_to_priv_channels = true;
439-
440-
let mut features = channelmanager::provided_init_features(&accept_forward_cfg);
441-
features.set_onion_messages_optional();
442-
features.set_route_blinding_optional();
443-
444-
let chanmon_cfgs = create_chanmon_cfgs(6);
445-
let node_cfgs = create_node_cfgs(6, &chanmon_cfgs);
446-
447-
*node_cfgs[1].override_init_features.borrow_mut() = Some(features);
448-
449-
let node_chanmgrs = create_node_chanmgrs(
450-
6, &node_cfgs, &[None, Some(accept_forward_cfg), None, None, None, None]
451-
);
452-
let nodes = create_network(6, &node_cfgs, &node_chanmgrs);
453-
454-
create_unannounced_chan_between_nodes_with_value(&nodes, 0, 1, 10_000_000, 1_000_000_000);
455-
create_unannounced_chan_between_nodes_with_value(&nodes, 2, 3, 10_000_000, 1_000_000_000);
456-
create_announced_chan_between_nodes_with_value(&nodes, 1, 2, 10_000_000, 1_000_000_000);
457-
create_announced_chan_between_nodes_with_value(&nodes, 1, 4, 10_000_000, 1_000_000_000);
458-
create_announced_chan_between_nodes_with_value(&nodes, 1, 5, 10_000_000, 1_000_000_000);
459-
create_announced_chan_between_nodes_with_value(&nodes, 2, 4, 10_000_000, 1_000_000_000);
460-
create_announced_chan_between_nodes_with_value(&nodes, 2, 5, 10_000_000, 1_000_000_000);
461-
462-
// Add extra channels so that more than one of Bob's peers have MIN_PEER_CHANNELS and one has
463-
// more than the others.
464-
create_announced_chan_between_nodes_with_value(&nodes, 0, 4, 10_000_000, 1_000_000_000);
465-
create_announced_chan_between_nodes_with_value(&nodes, 3, 4, 10_000_000, 1_000_000_000);
466-
467-
let (alice, bob, charlie, david) = (&nodes[0], &nodes[1], &nodes[2], &nodes[3]);
468-
let bob_id = bob.node.get_our_node_id();
469-
470-
disconnect_peers(alice, &[charlie, david, &nodes[4], &nodes[5]]);
471-
disconnect_peers(david, &[bob, &nodes[4], &nodes[5]]);
472-
473-
let offer = bob.node
474-
.create_offer_builder().unwrap()
475-
.amount_msats(10_000_000)
476-
.build().unwrap();
477-
assert_ne!(offer.issuer_signing_pubkey(), Some(bob_id));
478-
assert!(!offer.paths().is_empty());
479-
for path in offer.paths() {
480-
let introduction_node_id = resolve_introduction_node(david, &path);
481-
assert_eq!(introduction_node_id, nodes[4].node.get_our_node_id());
482-
}
483-
}
484-
485332
/// Tests the dummy hop behavior of Offers based on the message router used:
486333
/// - Compact paths (`DefaultMessageRouter`) should not include dummy hops.
487334
/// - Node ID paths (`NodeIdMessageRouter`) may include 0 to [`MAX_DUMMY_HOPS_COUNT`] dummy hops.

lightning/src/onion_message/messenger.rs

Lines changed: 33 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -563,10 +563,6 @@ impl<G: Deref<Target = NetworkGraph<L>>, L: Logger, ES: EntropySource>
563563
// Limit the number of blinded paths that are computed.
564564
const MAX_PATHS: usize = 3;
565565

566-
// Ensure peers have at least three channels so that it is more difficult to infer the
567-
// recipient's node_id.
568-
const MIN_PEER_CHANNELS: usize = 3;
569-
570566
let network_graph = network_graph.deref().read_only();
571567
let is_recipient_announced =
572568
network_graph.nodes().contains_key(&NodeId::from_pubkey(&recipient));
@@ -596,32 +592,6 @@ impl<G: Deref<Target = NetworkGraph<L>>, L: Logger, ES: EntropySource>
596592

597593
let compact_paths = !never_compact_path && size_constrained;
598594

599-
let has_one_peer = peers.len() == 1;
600-
let mut peer_info = peers
601-
.map(|peer| MessageForwardNode {
602-
short_channel_id: if compact_paths { peer.short_channel_id } else { None },
603-
..peer
604-
})
605-
// Limit to peers with announced channels unless the recipient is unannounced.
606-
.filter_map(|peer| {
607-
network_graph
608-
.node(&NodeId::from_pubkey(&peer.node_id))
609-
.filter(|info| {
610-
!is_recipient_announced || info.channels.len() >= MIN_PEER_CHANNELS
611-
})
612-
.map(|info| (peer, info.is_tor_only(), info.channels.len()))
613-
// Allow messages directly with the only peer when unannounced.
614-
.or_else(|| (!is_recipient_announced && has_one_peer).then(|| (peer, false, 0)))
615-
})
616-
// Exclude Tor-only nodes when the recipient is announced.
617-
.filter(|(_, is_tor_only, _)| !(*is_tor_only && is_recipient_announced))
618-
.collect::<Vec<_>>();
619-
620-
// Prefer using non-Tor nodes with the most channels as the introduction node.
621-
peer_info.sort_unstable_by(|(_, a_tor_only, a_channels), (_, b_tor_only, b_channels)| {
622-
a_tor_only.cmp(b_tor_only).then(a_channels.cmp(b_channels).reverse())
623-
});
624-
625595
let build_path = |intermediate_hops: &[MessageForwardNode]| {
626596
// Calculate the dummy hops given the total hop count target (including the recipient).
627597
let dummy_hops_count = path_len_incl_dummys.saturating_sub(intermediate_hops.len() + 1);
@@ -638,12 +608,39 @@ impl<G: Deref<Target = NetworkGraph<L>>, L: Logger, ES: EntropySource>
638608
)
639609
};
640610

641-
// Try to create paths from peer info, fall back to direct path if needed
642-
let mut paths = peer_info
643-
.into_iter()
644-
.map(|(peer, _, _)| build_path(&[peer]))
645-
.take(MAX_PATHS)
646-
.collect::<Vec<_>>();
611+
let has_one_peer = peers.len() == 1;
612+
let mut paths = if !is_recipient_announced {
613+
let mut peer_info = peers
614+
.map(|peer| MessageForwardNode {
615+
short_channel_id: if compact_paths { peer.short_channel_id } else { None },
616+
..peer
617+
})
618+
.filter_map(|peer| {
619+
network_graph
620+
.node(&NodeId::from_pubkey(&peer.node_id))
621+
.map(|info| (peer, info.is_tor_only(), info.channels.len()))
622+
// Allow messages directly with the only peer
623+
.or_else(|| has_one_peer.then(|| (peer, false, 0)))
624+
})
625+
.collect::<Vec<_>>();
626+
627+
// Prefer using non-Tor nodes with the most channels as the introduction node.
628+
peer_info.sort_unstable_by(
629+
|(_, a_tor_only, a_channels), (_, b_tor_only, b_channels)| {
630+
a_tor_only.cmp(b_tor_only).then(a_channels.cmp(b_channels).reverse())
631+
},
632+
);
633+
634+
// Try to create paths from peer info, fall back to direct path if needed
635+
peer_info
636+
.into_iter()
637+
.map(|(peer, _, _)| build_path(&[peer]))
638+
.take(MAX_PATHS)
639+
.collect::<Vec<_>>()
640+
} else {
641+
vec![]
642+
};
643+
647644
if paths.is_empty() {
648645
if is_recipient_announced {
649646
paths = vec![build_path(&[])];

0 commit comments

Comments
 (0)