Skip to content

Commit 3a48e75

Browse files
authored
Merge pull request #4647 from TheBlueMatt/2026-06-no-bolt12-privacy
Stop using an introduction node in blinded message paths
2 parents 238b974 + 8c08a30 commit 3a48e75

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)