diff --git a/fuzz/src/chanmon_consistency.rs b/fuzz/src/chanmon_consistency.rs index 1bcba1fbf37..1cdf617fa07 100644 --- a/fuzz/src/chanmon_consistency.rs +++ b/fuzz/src/chanmon_consistency.rs @@ -34,7 +34,6 @@ use bitcoin::hashes::sha256d::Hash as Sha256dHash; use bitcoin::hash_types::BlockHash; use lightning::blinded_path::BlindedPath; -use lightning::blinded_path::message::ForwardNode; use lightning::blinded_path::payment::ReceiveTlvs; use lightning::chain; use lightning::chain::{BestBlock, ChannelMonitorUpdateStatus, chainmonitor, channelmonitor, Confirm, Watch}; @@ -124,7 +123,7 @@ impl MessageRouter for FuzzRouter { } fn create_blinded_paths( - &self, _recipient: PublicKey, _peers: Vec, _secp_ctx: &Secp256k1, + &self, _recipient: PublicKey, _peers: Vec, _secp_ctx: &Secp256k1, ) -> Result, ()> { unreachable!() } diff --git a/fuzz/src/full_stack.rs b/fuzz/src/full_stack.rs index 2ae5ba2225d..bdd29be9129 100644 --- a/fuzz/src/full_stack.rs +++ b/fuzz/src/full_stack.rs @@ -31,7 +31,6 @@ use bitcoin::hashes::sha256d::Hash as Sha256dHash; use bitcoin::hash_types::{Txid, BlockHash}; use lightning::blinded_path::BlindedPath; -use lightning::blinded_path::message::ForwardNode; use lightning::blinded_path::payment::ReceiveTlvs; use lightning::chain; use lightning::chain::{BestBlock, ChannelMonitorUpdateStatus, Confirm, Listen}; @@ -162,7 +161,7 @@ impl MessageRouter for FuzzRouter { } fn create_blinded_paths( - &self, _recipient: PublicKey, _peers: Vec, _secp_ctx: &Secp256k1, + &self, _recipient: PublicKey, _peers: Vec, _secp_ctx: &Secp256k1, ) -> Result, ()> { unreachable!() } diff --git a/fuzz/src/onion_message.rs b/fuzz/src/onion_message.rs index 4c1c5ac1122..371a9421fc7 100644 --- a/fuzz/src/onion_message.rs +++ b/fuzz/src/onion_message.rs @@ -7,7 +7,6 @@ use bitcoin::secp256k1::ecdsa::RecoverableSignature; use bitcoin::secp256k1::schnorr; use lightning::blinded_path::{BlindedPath, EmptyNodeIdLookUp}; -use lightning::blinded_path::message::ForwardNode; use lightning::ln::features::InitFeatures; use lightning::ln::msgs::{self, DecodeError, OnionMessageHandler}; use lightning::ln::script::ShutdownScript; @@ -89,7 +88,7 @@ impl MessageRouter for TestMessageRouter { } fn create_blinded_paths( - &self, _recipient: PublicKey, _peers: Vec, _secp_ctx: &Secp256k1, + &self, _recipient: PublicKey, _peers: Vec, _secp_ctx: &Secp256k1, ) -> Result, ()> { unreachable!() } diff --git a/lightning/src/blinded_path/payment.rs b/lightning/src/blinded_path/payment.rs index 3b71f77c6d5..1f26f956937 100644 --- a/lightning/src/blinded_path/payment.rs +++ b/lightning/src/blinded_path/payment.rs @@ -23,6 +23,7 @@ use crate::ln::msgs::DecodeError; use crate::offers::invoice::BlindedPayInfo; use crate::offers::invoice_request::InvoiceRequestFields; use crate::offers::offer::OfferId; +use crate::routing::gossip::DirectedChannelInfo; use crate::util::ser::{HighZeroBytesDroppedBigSize, Readable, Writeable, Writer}; #[allow(unused_imports)] @@ -170,6 +171,19 @@ impl PaymentContext { } } +impl PaymentRelay { + fn normalize_cltv_expiry_delta(cltv_expiry_delta: u16) -> Result { + // Avoid exposing esoteric CLTV expiry deltas, which could de-anonymize the path. + match cltv_expiry_delta { + 0..=40 => Ok(40), + 41..=80 => Ok(80), + 81..=144 => Ok(144), + 145..=216 => Ok(216), + _ => Err(()), + } + } +} + impl TryFrom for PaymentRelay { type Error = (); @@ -178,16 +192,25 @@ impl TryFrom for PaymentRelay { fee_base_msat, fee_proportional_millionths, cltv_expiry_delta } = info; - // Avoid exposing esoteric CLTV expiry deltas - let cltv_expiry_delta = match cltv_expiry_delta { - 0..=40 => 40, - 41..=80 => 80, - 81..=144 => 144, - 145..=216 => 216, - _ => return Err(()), - }; + Ok(Self { + cltv_expiry_delta: Self::normalize_cltv_expiry_delta(cltv_expiry_delta)?, + fee_proportional_millionths, + fee_base_msat + }) + } +} + +impl<'a> TryFrom> for PaymentRelay { + type Error = (); + + fn try_from(info: DirectedChannelInfo<'a>) -> Result { + let direction = info.direction(); - Ok(Self { cltv_expiry_delta, fee_proportional_millionths, fee_base_msat }) + Ok(Self { + cltv_expiry_delta: Self::normalize_cltv_expiry_delta(direction.cltv_expiry_delta)?, + fee_proportional_millionths: direction.fees.proportional_millionths, + fee_base_msat: direction.fees.base_msat, + }) } } diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index e3f7243cd3f..992fd3c6f02 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -1554,8 +1554,9 @@ where /// # /// # fn example(channel_manager: T) -> Result<(), Bolt12SemanticError> { /// # let channel_manager = channel_manager.get_cm(); +/// # let absolute_expiry = None; /// let offer = channel_manager -/// .create_offer_builder()? +/// .create_offer_builder(absolute_expiry)? /// # ; /// # // Needed for compiling for c_bindings /// # let builder: lightning::offers::offer::OfferBuilder<_, _> = offer.into(); @@ -2287,6 +2288,19 @@ const MAX_UNFUNDED_CHANNEL_PEERS: usize = 50; /// many peers we reject new (inbound) connections. const MAX_NO_CHANNEL_PEERS: usize = 250; +/// The maximum expiration from the current time where an [`Offer`] or [`Refund`] is considered +/// short-lived, while anything with a greater expiration is considered long-lived. +/// +/// Using [`ChannelManager::create_offer_builder`] or [`ChannelManager::create_refund_builder`], +/// will included a [`BlindedPath`] created using: +/// - [`MessageRouter::create_compact_blinded_paths`] when short-lived, and +/// - [`MessageRouter::create_blinded_paths`] when long-lived. +/// +/// Using compact [`BlindedPath`]s may provide better privacy as the [`MessageRouter`] could select +/// more hops. However, since they use short channel ids instead of pubkeys, they are more likely to +/// become invalid over time as channels are closed. Thus, they are only suitable for short-term use. +pub const MAX_SHORT_LIVED_RELATIVE_EXPIRY: Duration = Duration::from_secs(60 * 60 * 24); + /// Used by [`ChannelManager::list_recent_payments`] to express the status of recent payments. /// These include payments that have yet to find a successful path, or have unresolved HTLCs. #[derive(Debug, PartialEq)] @@ -8240,16 +8254,15 @@ where macro_rules! create_offer_builder { ($self: ident, $builder: ty) => { /// Creates an [`OfferBuilder`] such that the [`Offer`] it builds is recognized by the - /// [`ChannelManager`] when handling [`InvoiceRequest`] messages for the offer. The offer will - /// not have an expiration unless otherwise set on the builder. + /// [`ChannelManager`] when handling [`InvoiceRequest`] messages for the offer. The offer's + /// expiration will be `absolute_expiry` if `Some`, otherwise it will not expire. /// /// # Privacy /// - /// Uses [`MessageRouter::create_blinded_paths`] to construct a [`BlindedPath`] for the offer. - /// However, if one is not found, uses a one-hop [`BlindedPath`] with - /// [`ChannelManager::get_our_node_id`] as the introduction node instead. In the latter case, - /// the node must be announced, otherwise, there is no way to find a path to the introduction in - /// order to send the [`InvoiceRequest`]. + /// Uses [`MessageRouter`] to construct a [`BlindedPath`] for the offer based on the given + /// `absolute_expiry` according to [`MAX_SHORT_LIVED_RELATIVE_EXPIRY`]. See those docs for + /// privacy implications as well as those of the parameterized [`Router`], which implements + /// [`MessageRouter`]. /// /// Also, uses a derived signing pubkey in the offer for recipient privacy. /// @@ -8264,19 +8277,27 @@ macro_rules! create_offer_builder { ($self: ident, $builder: ty) => { /// /// [`Offer`]: crate::offers::offer::Offer /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest - pub fn create_offer_builder(&$self) -> Result<$builder, Bolt12SemanticError> { + pub fn create_offer_builder( + &$self, absolute_expiry: Option + ) -> Result<$builder, Bolt12SemanticError> { let node_id = $self.get_our_node_id(); let expanded_key = &$self.inbound_payment_key; let entropy = &*$self.entropy_source; let secp_ctx = &$self.secp_ctx; - let path = $self.create_blinded_path().map_err(|_| Bolt12SemanticError::MissingPaths)?; + let path = $self.create_blinded_path_using_absolute_expiry(absolute_expiry) + .map_err(|_| Bolt12SemanticError::MissingPaths)?; let builder = OfferBuilder::deriving_signing_pubkey( node_id, expanded_key, entropy, secp_ctx ) .chain_hash($self.chain_hash) .path(path); + let builder = match absolute_expiry { + None => builder, + Some(absolute_expiry) => builder.absolute_expiry(absolute_expiry), + }; + Ok(builder.into()) } } } @@ -8304,11 +8325,10 @@ macro_rules! create_refund_builder { ($self: ident, $builder: ty) => { /// /// # Privacy /// - /// Uses [`MessageRouter::create_blinded_paths`] to construct a [`BlindedPath`] for the refund. - /// However, if one is not found, uses a one-hop [`BlindedPath`] with - /// [`ChannelManager::get_our_node_id`] as the introduction node instead. In the latter case, - /// the node must be announced, otherwise, there is no way to find a path to the introduction in - /// order to send the [`Bolt12Invoice`]. + /// Uses [`MessageRouter`] to construct a [`BlindedPath`] for the refund based on the given + /// `absolute_expiry` according to [`MAX_SHORT_LIVED_RELATIVE_EXPIRY`]. See those docs for + /// privacy implications as well as those of the parameterized [`Router`], which implements + /// [`MessageRouter`]. /// /// Also, uses a derived payer id in the refund for payer privacy. /// @@ -8337,7 +8357,8 @@ macro_rules! create_refund_builder { ($self: ident, $builder: ty) => { let entropy = &*$self.entropy_source; let secp_ctx = &$self.secp_ctx; - let path = $self.create_blinded_path().map_err(|_| Bolt12SemanticError::MissingPaths)?; + let path = $self.create_blinded_path_using_absolute_expiry(Some(absolute_expiry)) + .map_err(|_| Bolt12SemanticError::MissingPaths)?; let builder = RefundBuilder::deriving_payer_id( node_id, expanded_key, entropy, secp_ctx, amount_msats, payment_id )? @@ -8406,10 +8427,9 @@ where /// /// # Privacy /// - /// Uses a one-hop [`BlindedPath`] for the reply path with [`ChannelManager::get_our_node_id`] - /// as the introduction node and a derived payer id for payer privacy. As such, currently, the - /// node must be announced. Otherwise, there is no way to find a path to the introduction node - /// in order to send the [`Bolt12Invoice`]. + /// For payer privacy, uses a derived payer id and uses [`MessageRouter::create_blinded_paths`] + /// to construct a [`BlindedPath`] for the reply path. For further privacy implications, see the + /// docs of the parameterized [`Router`], which implements [`MessageRouter`]. /// /// # Limitations /// @@ -8686,6 +8706,38 @@ where inbound_payment::get_payment_preimage(payment_hash, payment_secret, &self.inbound_payment_key) } + /// Creates a blinded path by delegating to [`MessageRouter`] based on the path's intended + /// lifetime. + /// + /// Whether or not the path is compact depends on whether the path is short-lived or long-lived, + /// respectively, based on the given `absolute_expiry` as seconds since the Unix epoch. See + /// [`MAX_SHORT_LIVED_RELATIVE_EXPIRY`]. + fn create_blinded_path_using_absolute_expiry( + &self, absolute_expiry: Option + ) -> Result { + let now = self.duration_since_epoch(); + let max_short_lived_absolute_expiry = now.saturating_add(MAX_SHORT_LIVED_RELATIVE_EXPIRY); + + if absolute_expiry.unwrap_or(Duration::MAX) <= max_short_lived_absolute_expiry { + self.create_compact_blinded_path() + } else { + self.create_blinded_path() + } + } + + pub(super) fn duration_since_epoch(&self) -> Duration { + #[cfg(not(feature = "std"))] + let now = Duration::from_secs( + self.highest_seen_timestamp.load(Ordering::Acquire) as u64 + ); + #[cfg(feature = "std")] + let now = std::time::SystemTime::now() + .duration_since(std::time::SystemTime::UNIX_EPOCH) + .expect("SystemTime::now() should come after SystemTime::UNIX_EPOCH"); + + now + } + /// Creates a blinded path by delegating to [`MessageRouter::create_blinded_paths`]. /// /// Errors if the `MessageRouter` errors or returns an empty `Vec`. @@ -8696,6 +8748,27 @@ where let peers = self.per_peer_state.read().unwrap() .iter() .map(|(node_id, peer_state)| (node_id, peer_state.lock().unwrap())) + .filter(|(_, peer)| peer.is_connected) + .filter(|(_, peer)| peer.latest_features.supports_onion_messages()) + .map(|(node_id, _)| *node_id) + .collect::>(); + + self.router + .create_blinded_paths(recipient, peers, secp_ctx) + .and_then(|paths| paths.into_iter().next().ok_or(())) + } + + /// Creates a blinded path by delegating to [`MessageRouter::create_compact_blinded_paths`]. + /// + /// Errors if the `MessageRouter` errors or returns an empty `Vec`. + fn create_compact_blinded_path(&self) -> Result { + let recipient = self.get_our_node_id(); + let secp_ctx = &self.secp_ctx; + + let peers = self.per_peer_state.read().unwrap() + .iter() + .map(|(node_id, peer_state)| (node_id, peer_state.lock().unwrap())) + .filter(|(_, peer)| peer.is_connected) .filter(|(_, peer)| peer.latest_features.supports_onion_messages()) .map(|(node_id, peer)| ForwardNode { node_id: *node_id, @@ -8708,7 +8781,7 @@ where .collect::>(); self.router - .create_blinded_paths(recipient, peers, secp_ctx) + .create_compact_blinded_paths(recipient, peers, secp_ctx) .and_then(|paths| paths.into_iter().next().ok_or(())) } diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index c0f3458b3d6..11e0737da1a 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -3259,30 +3259,34 @@ pub fn create_network<'a, 'b: 'a, 'c: 'b>(node_count: usize, cfgs: &'b Vec(node_a: &Node<'a, 'b, 'c>, node_b: &Node<'a, 'b, 'c>) { + let node_id_a = node_a.node.get_our_node_id(); + let node_id_b = node_b.node.get_our_node_id(); + + let init_a = msgs::Init { + features: node_a.init_features(&node_id_b), + networks: None, + remote_network_address: None, + }; + let init_b = msgs::Init { + features: node_b.init_features(&node_id_a), + networks: None, + remote_network_address: None, + }; + + node_a.node.peer_connected(&node_id_b, &init_b, true).unwrap(); + node_b.node.peer_connected(&node_id_a, &init_a, false).unwrap(); + node_a.onion_messenger.peer_connected(&node_id_b, &init_b, true).unwrap(); + node_b.onion_messenger.peer_connected(&node_id_a, &init_a, false).unwrap(); +} + pub fn connect_dummy_node<'a, 'b: 'a, 'c: 'b>(node: &Node<'a, 'b, 'c>) { let node_id_dummy = PublicKey::from_slice(&[2; 33]).unwrap(); @@ -3643,13 +3647,8 @@ pub fn reconnect_nodes<'a, 'b, 'c, 'd>(args: ReconnectArgs<'a, 'b, 'c, 'd>) { pending_cell_htlc_claims, pending_cell_htlc_fails, pending_raa, pending_responding_commitment_signed, pending_responding_commitment_signed_dup_monitor, } = args; - node_a.node.peer_connected(&node_b.node.get_our_node_id(), &msgs::Init { - features: node_b.node.init_features(), networks: None, remote_network_address: None - }, true).unwrap(); + connect_nodes(node_a, node_b); let reestablish_1 = get_chan_reestablish_msgs!(node_a, node_b); - node_b.node.peer_connected(&node_a.node.get_our_node_id(), &msgs::Init { - features: node_a.node.init_features(), networks: None, remote_network_address: None - }, false).unwrap(); let reestablish_2 = get_chan_reestablish_msgs!(node_b, node_a); if send_channel_ready.0 { diff --git a/lightning/src/ln/offers_tests.rs b/lightning/src/ln/offers_tests.rs index eedd82c569b..c7fb5f8fd59 100644 --- a/lightning/src/ln/offers_tests.rs +++ b/lightning/src/ln/offers_tests.rs @@ -46,7 +46,7 @@ use core::time::Duration; use crate::blinded_path::{BlindedPath, IntroductionNode}; use crate::blinded_path::payment::{Bolt12OfferContext, Bolt12RefundContext, PaymentContext}; use crate::events::{Event, MessageSendEventsProvider, PaymentPurpose}; -use crate::ln::channelmanager::{PaymentId, RecentPaymentDetails, Retry, self}; +use crate::ln::channelmanager::{MAX_SHORT_LIVED_RELATIVE_EXPIRY, PaymentId, RecentPaymentDetails, Retry, self}; use crate::ln::functional_test_utils::*; use crate::ln::msgs::{ChannelMessageHandler, Init, NodeAnnouncement, OnionMessage, OnionMessageHandler, RoutingMessageHandler, SocketAddress, UnsignedGossipMessage, UnsignedNodeAnnouncement}; use crate::offers::invoice::Bolt12Invoice; @@ -274,7 +274,7 @@ fn prefers_non_tor_nodes_in_blinded_paths() { announce_node_address(charlie, &[alice, bob, david, &nodes[4], &nodes[5]], tor.clone()); let offer = bob.node - .create_offer_builder().unwrap() + .create_offer_builder(None).unwrap() .amount_msats(10_000_000) .build().unwrap(); assert_ne!(offer.signing_pubkey(), Some(bob_id)); @@ -290,7 +290,7 @@ fn prefers_non_tor_nodes_in_blinded_paths() { announce_node_address(&nodes[5], &[alice, bob, charlie, david, &nodes[4]], tor.clone()); let offer = bob.node - .create_offer_builder().unwrap() + .create_offer_builder(None).unwrap() .amount_msats(10_000_000) .build().unwrap(); assert_ne!(offer.signing_pubkey(), Some(bob_id)); @@ -341,7 +341,7 @@ fn prefers_more_connected_nodes_in_blinded_paths() { disconnect_peers(david, &[bob, &nodes[4], &nodes[5]]); let offer = bob.node - .create_offer_builder().unwrap() + .create_offer_builder(None).unwrap() .amount_msats(10_000_000) .build().unwrap(); assert_ne!(offer.signing_pubkey(), Some(bob_id)); @@ -352,6 +352,124 @@ fn prefers_more_connected_nodes_in_blinded_paths() { } } +/// Checks that blinded paths are compact for short-lived offers. +#[test] +fn creates_short_lived_offer() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 10_000_000, 1_000_000_000); + + let alice = &nodes[0]; + let alice_id = alice.node.get_our_node_id(); + let bob = &nodes[1]; + + let absolute_expiry = alice.node.duration_since_epoch() + MAX_SHORT_LIVED_RELATIVE_EXPIRY; + let offer = alice.node + .create_offer_builder(Some(absolute_expiry)).unwrap() + .build().unwrap(); + assert_eq!(offer.absolute_expiry(), Some(absolute_expiry)); + assert!(!offer.paths().is_empty()); + for path in offer.paths() { + let introduction_node_id = resolve_introduction_node(bob, &path); + assert_eq!(introduction_node_id, alice_id); + assert!(matches!(path.introduction_node, IntroductionNode::DirectedShortChannelId(..))); + } +} + +/// Checks that blinded paths are not compact for long-lived offers. +#[test] +fn creates_long_lived_offer() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 10_000_000, 1_000_000_000); + + let alice = &nodes[0]; + let alice_id = alice.node.get_our_node_id(); + + let absolute_expiry = alice.node.duration_since_epoch() + MAX_SHORT_LIVED_RELATIVE_EXPIRY + + Duration::from_secs(1); + let offer = alice.node + .create_offer_builder(Some(absolute_expiry)) + .unwrap() + .build().unwrap(); + assert_eq!(offer.absolute_expiry(), Some(absolute_expiry)); + assert!(!offer.paths().is_empty()); + for path in offer.paths() { + assert_eq!(path.introduction_node, IntroductionNode::NodeId(alice_id)); + } + + let offer = alice.node + .create_offer_builder(None).unwrap() + .build().unwrap(); + assert_eq!(offer.absolute_expiry(), None); + assert!(!offer.paths().is_empty()); + for path in offer.paths() { + assert_eq!(path.introduction_node, IntroductionNode::NodeId(alice_id)); + } +} + +/// Checks that blinded paths are compact for short-lived refunds. +#[test] +fn creates_short_lived_refund() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 10_000_000, 1_000_000_000); + + let alice = &nodes[0]; + let bob = &nodes[1]; + let bob_id = bob.node.get_our_node_id(); + + let absolute_expiry = bob.node.duration_since_epoch() + MAX_SHORT_LIVED_RELATIVE_EXPIRY; + let payment_id = PaymentId([1; 32]); + let refund = bob.node + .create_refund_builder(10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None) + .unwrap() + .build().unwrap(); + assert_eq!(refund.absolute_expiry(), Some(absolute_expiry)); + assert!(!refund.paths().is_empty()); + for path in refund.paths() { + let introduction_node_id = resolve_introduction_node(alice, &path); + assert_eq!(introduction_node_id, bob_id); + assert!(matches!(path.introduction_node, IntroductionNode::DirectedShortChannelId(..))); + } +} + +/// Checks that blinded paths are not compact for long-lived refunds. +#[test] +fn creates_long_lived_refund() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 10_000_000, 1_000_000_000); + + let bob = &nodes[1]; + let bob_id = bob.node.get_our_node_id(); + + let absolute_expiry = bob.node.duration_since_epoch() + MAX_SHORT_LIVED_RELATIVE_EXPIRY + + Duration::from_secs(1); + let payment_id = PaymentId([1; 32]); + let refund = bob.node + .create_refund_builder(10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None) + .unwrap() + .build().unwrap(); + assert_eq!(refund.absolute_expiry(), Some(absolute_expiry)); + assert!(!refund.paths().is_empty()); + for path in refund.paths() { + assert_eq!(path.introduction_node, IntroductionNode::NodeId(bob_id)); + } +} + /// Checks that an offer can be paid through blinded paths and that ephemeral pubkeys are used /// rather than exposing a node's pubkey. #[test] @@ -391,16 +509,14 @@ fn creates_and_pays_for_offer_using_two_hop_blinded_path() { disconnect_peers(david, &[bob, &nodes[4], &nodes[5]]); let offer = alice.node - .create_offer_builder() + .create_offer_builder(None) .unwrap() .amount_msats(10_000_000) .build().unwrap(); assert_ne!(offer.signing_pubkey(), Some(alice_id)); assert!(!offer.paths().is_empty()); for path in offer.paths() { - let introduction_node_id = resolve_introduction_node(david, &path); - assert_eq!(introduction_node_id, bob_id); - assert!(matches!(path.introduction_node, IntroductionNode::DirectedShortChannelId(..))); + assert_eq!(path.introduction_node, IntroductionNode::NodeId(bob_id)); } let payment_id = PaymentId([1; 32]); @@ -427,11 +543,9 @@ fn creates_and_pays_for_offer_using_two_hop_blinded_path() { payer_note_truncated: None, }, }); - let introduction_node_id = resolve_introduction_node(alice, &reply_path); assert_eq!(invoice_request.amount_msats(), None); assert_ne!(invoice_request.payer_id(), david_id); - assert_eq!(introduction_node_id, charlie_id); - assert!(matches!(reply_path.introduction_node, IntroductionNode::DirectedShortChannelId(..))); + assert_eq!(reply_path.introduction_node, IntroductionNode::NodeId(charlie_id)); let onion_message = alice.onion_messenger.next_onion_message_for_peer(charlie_id).unwrap(); charlie.onion_messenger.handle_onion_message(&alice_id, &onion_message); @@ -503,9 +617,7 @@ fn creates_and_pays_for_refund_using_two_hop_blinded_path() { assert_ne!(refund.payer_id(), david_id); assert!(!refund.paths().is_empty()); for path in refund.paths() { - let introduction_node_id = resolve_introduction_node(alice, &path); - assert_eq!(introduction_node_id, charlie_id); - assert!(matches!(path.introduction_node, IntroductionNode::DirectedShortChannelId(..))); + assert_eq!(path.introduction_node, IntroductionNode::NodeId(charlie_id)); } expect_recent_payment!(david, RecentPaymentDetails::AwaitingInvoice, payment_id); @@ -555,15 +667,13 @@ fn creates_and_pays_for_offer_using_one_hop_blinded_path() { let bob_id = bob.node.get_our_node_id(); let offer = alice.node - .create_offer_builder().unwrap() + .create_offer_builder(None).unwrap() .amount_msats(10_000_000) .build().unwrap(); assert_ne!(offer.signing_pubkey(), Some(alice_id)); assert!(!offer.paths().is_empty()); for path in offer.paths() { - let introduction_node_id = resolve_introduction_node(bob, &path); - assert_eq!(introduction_node_id, alice_id); - assert!(matches!(path.introduction_node, IntroductionNode::DirectedShortChannelId(..))); + assert_eq!(path.introduction_node, IntroductionNode::NodeId(alice_id)); } let payment_id = PaymentId([1; 32]); @@ -582,11 +692,9 @@ fn creates_and_pays_for_offer_using_one_hop_blinded_path() { payer_note_truncated: None, }, }); - let introduction_node_id = resolve_introduction_node(alice, &reply_path); assert_eq!(invoice_request.amount_msats(), None); assert_ne!(invoice_request.payer_id(), bob_id); - assert_eq!(introduction_node_id, bob_id); - assert!(matches!(reply_path.introduction_node, IntroductionNode::DirectedShortChannelId(..))); + assert_eq!(reply_path.introduction_node, IntroductionNode::NodeId(bob_id)); let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap(); bob.onion_messenger.handle_onion_message(&alice_id, &onion_message); @@ -634,9 +742,7 @@ fn creates_and_pays_for_refund_using_one_hop_blinded_path() { assert_ne!(refund.payer_id(), bob_id); assert!(!refund.paths().is_empty()); for path in refund.paths() { - let introduction_node_id = resolve_introduction_node(alice, &path); - assert_eq!(introduction_node_id, bob_id); - assert!(matches!(path.introduction_node, IntroductionNode::DirectedShortChannelId(..))); + assert_eq!(path.introduction_node, IntroductionNode::NodeId(bob_id)); } expect_recent_payment!(bob, RecentPaymentDetails::AwaitingInvoice, payment_id); @@ -681,7 +787,7 @@ fn pays_for_offer_without_blinded_paths() { let bob_id = bob.node.get_our_node_id(); let offer = alice.node - .create_offer_builder().unwrap() + .create_offer_builder(None).unwrap() .clear_paths() .amount_msats(10_000_000) .build().unwrap(); @@ -769,7 +875,7 @@ fn fails_creating_offer_without_blinded_paths() { create_unannounced_chan_between_nodes_with_value(&nodes, 0, 1, 10_000_000, 1_000_000_000); - match nodes[0].node.create_offer_builder() { + match nodes[0].node.create_offer_builder(None) { Ok(_) => panic!("Expected error"), Err(e) => assert_eq!(e, Bolt12SemanticError::MissingPaths), } @@ -798,6 +904,129 @@ fn fails_creating_refund_without_blinded_paths() { assert!(nodes[0].node.list_recent_payments().is_empty()); } +/// Fails creating or paying an offer when a blinded path cannot be created because no peers are +/// connected. +#[test] +fn fails_creating_or_paying_for_offer_without_connected_peers() { + let chanmon_cfgs = create_chanmon_cfgs(6); + let node_cfgs = create_node_cfgs(6, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(6, &node_cfgs, &[None, None, None, None, None, None]); + let nodes = create_network(6, &node_cfgs, &node_chanmgrs); + + create_unannounced_chan_between_nodes_with_value(&nodes, 0, 1, 10_000_000, 1_000_000_000); + create_unannounced_chan_between_nodes_with_value(&nodes, 2, 3, 10_000_000, 1_000_000_000); + create_announced_chan_between_nodes_with_value(&nodes, 1, 2, 10_000_000, 1_000_000_000); + create_announced_chan_between_nodes_with_value(&nodes, 1, 4, 10_000_000, 1_000_000_000); + create_announced_chan_between_nodes_with_value(&nodes, 1, 5, 10_000_000, 1_000_000_000); + create_announced_chan_between_nodes_with_value(&nodes, 2, 4, 10_000_000, 1_000_000_000); + create_announced_chan_between_nodes_with_value(&nodes, 2, 5, 10_000_000, 1_000_000_000); + + let (alice, bob, charlie, david) = (&nodes[0], &nodes[1], &nodes[2], &nodes[3]); + + disconnect_peers(alice, &[bob, charlie, david, &nodes[4], &nodes[5]]); + disconnect_peers(david, &[bob, charlie, &nodes[4], &nodes[5]]); + + let absolute_expiry = alice.node.duration_since_epoch() + MAX_SHORT_LIVED_RELATIVE_EXPIRY; + match alice.node.create_offer_builder(Some(absolute_expiry)) { + Ok(_) => panic!("Expected error"), + Err(e) => assert_eq!(e, Bolt12SemanticError::MissingPaths), + } + + let mut args = ReconnectArgs::new(alice, bob); + args.send_channel_ready = (true, true); + reconnect_nodes(args); + + let offer = alice.node + .create_offer_builder(Some(absolute_expiry)).unwrap() + .amount_msats(10_000_000) + .build().unwrap(); + + let payment_id = PaymentId([1; 32]); + + match david.node.pay_for_offer(&offer, None, None, None, payment_id, Retry::Attempts(0), None) { + Ok(_) => panic!("Expected error"), + Err(e) => assert_eq!(e, Bolt12SemanticError::MissingPaths), + } + + assert!(nodes[0].node.list_recent_payments().is_empty()); + + let mut args = ReconnectArgs::new(charlie, david); + args.send_channel_ready = (true, true); + reconnect_nodes(args); + + assert!( + david.node.pay_for_offer( + &offer, None, None, None, payment_id, Retry::Attempts(0), None + ).is_ok() + ); + + expect_recent_payment!(david, RecentPaymentDetails::AwaitingInvoice, payment_id); +} + +/// Fails creating or sending an invoice for a refund when a blinded path cannot be created because +/// no peers are connected. +#[test] +fn fails_creating_refund_or_sending_invoice_without_connected_peers() { + let mut accept_forward_cfg = test_default_channel_config(); + accept_forward_cfg.accept_forwards_to_priv_channels = true; + + let mut features = channelmanager::provided_init_features(&accept_forward_cfg); + features.set_onion_messages_optional(); + features.set_route_blinding_optional(); + + let chanmon_cfgs = create_chanmon_cfgs(6); + let node_cfgs = create_node_cfgs(6, &chanmon_cfgs); + + *node_cfgs[1].override_init_features.borrow_mut() = Some(features); + + let node_chanmgrs = create_node_chanmgrs( + 6, &node_cfgs, &[None, Some(accept_forward_cfg), None, None, None, None] + ); + let nodes = create_network(6, &node_cfgs, &node_chanmgrs); + + create_unannounced_chan_between_nodes_with_value(&nodes, 0, 1, 10_000_000, 1_000_000_000); + create_unannounced_chan_between_nodes_with_value(&nodes, 2, 3, 10_000_000, 1_000_000_000); + create_announced_chan_between_nodes_with_value(&nodes, 1, 2, 10_000_000, 1_000_000_000); + create_announced_chan_between_nodes_with_value(&nodes, 1, 4, 10_000_000, 1_000_000_000); + create_announced_chan_between_nodes_with_value(&nodes, 1, 5, 10_000_000, 1_000_000_000); + create_announced_chan_between_nodes_with_value(&nodes, 2, 4, 10_000_000, 1_000_000_000); + create_announced_chan_between_nodes_with_value(&nodes, 2, 5, 10_000_000, 1_000_000_000); + + let (alice, bob, charlie, david) = (&nodes[0], &nodes[1], &nodes[2], &nodes[3]); + + disconnect_peers(alice, &[bob, charlie, david, &nodes[4], &nodes[5]]); + disconnect_peers(david, &[bob, charlie, &nodes[4], &nodes[5]]); + + let absolute_expiry = david.node.duration_since_epoch() + MAX_SHORT_LIVED_RELATIVE_EXPIRY; + let payment_id = PaymentId([1; 32]); + match david.node.create_refund_builder( + 10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None + ) { + Ok(_) => panic!("Expected error"), + Err(e) => assert_eq!(e, Bolt12SemanticError::MissingPaths), + } + + let mut args = ReconnectArgs::new(charlie, david); + args.send_channel_ready = (true, true); + reconnect_nodes(args); + + let refund = david.node + .create_refund_builder(10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None) + .unwrap() + .build().unwrap(); + + match alice.node.request_refund_payment(&refund) { + Ok(_) => panic!("Expected error"), + Err(e) => assert_eq!(e, Bolt12SemanticError::MissingPaths), + } + + let mut args = ReconnectArgs::new(alice, bob); + args.send_channel_ready = (true, true); + reconnect_nodes(args); + + assert!(alice.node.request_refund_payment(&refund).is_ok()); +} + /// Fails creating an invoice request when the offer contains an unsupported chain. #[test] fn fails_creating_invoice_request_for_unsupported_chain() { @@ -812,7 +1041,7 @@ fn fails_creating_invoice_request_for_unsupported_chain() { let bob = &nodes[1]; let offer = alice.node - .create_offer_builder().unwrap() + .create_offer_builder(None).unwrap() .clear_chains() .chain(Network::Signet) .build().unwrap(); @@ -872,7 +1101,7 @@ fn fails_creating_invoice_request_without_blinded_reply_path() { disconnect_peers(david, &[bob, &nodes[4], &nodes[5]]); let offer = alice.node - .create_offer_builder().unwrap() + .create_offer_builder(None).unwrap() .amount_msats(10_000_000) .build().unwrap(); @@ -906,7 +1135,7 @@ fn fails_creating_invoice_request_with_duplicate_payment_id() { disconnect_peers(alice, &[charlie, david, &nodes[4], &nodes[5]]); let offer = alice.node - .create_offer_builder().unwrap() + .create_offer_builder(None).unwrap() .amount_msats(10_000_000) .build().unwrap(); @@ -992,7 +1221,7 @@ fn fails_sending_invoice_without_blinded_payment_paths_for_offer() { disconnect_peers(david, &[bob, &nodes[4], &nodes[5]]); let offer = alice.node - .create_offer_builder().unwrap() + .create_offer_builder(None).unwrap() .amount_msats(10_000_000) .build().unwrap(); diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index eb4a837feb6..5ad687a9867 100644 --- a/lightning/src/onion_message/messenger.rs +++ b/lightning/src/onion_message/messenger.rs @@ -162,7 +162,7 @@ for OnionMessenger where /// # }) /// # } /// # fn create_blinded_paths( -/// # &self, _recipient: PublicKey, _peers: Vec, _secp_ctx: &Secp256k1 +/// # &self, _recipient: PublicKey, _peers: Vec, _secp_ctx: &Secp256k1 /// # ) -> Result, ()> { /// # unreachable!() /// # } @@ -186,7 +186,7 @@ for OnionMessenger where /// &keys_manager, &keys_manager, logger, &node_id_lookup, message_router, /// &offers_message_handler, &custom_message_handler /// ); - +/// /// # #[derive(Debug)] /// # struct YourCustomMessage {} /// impl Writeable for YourCustomMessage { @@ -195,6 +195,7 @@ for OnionMessenger where /// // Write your custom onion message to `w` /// } /// } +/// /// impl OnionMessageContents for YourCustomMessage { /// fn tlv_type(&self) -> u64 { /// # let your_custom_message_type = 42; @@ -202,6 +203,7 @@ for OnionMessenger where /// } /// fn msg_type(&self) -> &'static str { "YourCustomMessageType" } /// } +/// /// // Send a custom onion message to a node id. /// let destination = Destination::Node(destination_node_id); /// let reply_path = None; @@ -426,11 +428,46 @@ pub trait MessageRouter { fn create_blinded_paths< T: secp256k1::Signing + secp256k1::Verification >( - &self, recipient: PublicKey, peers: Vec, secp_ctx: &Secp256k1, + &self, recipient: PublicKey, peers: Vec, secp_ctx: &Secp256k1, ) -> Result, ()>; + + /// Creates compact [`BlindedPath`]s to the `recipient` node. The nodes in `peers` are assumed + /// to be direct peers with the `recipient`. + /// + /// Compact blinded paths use short channel ids instead of pubkeys for a smaller serialization, + /// which is beneficial when a QR code is used to transport the data. The SCID is passed using a + /// [`ForwardNode`] but may be `None` for graceful degradation. + /// + /// Implementations using additional intermediate nodes are responsible for using a + /// [`ForwardNode`] with `Some` short channel id, if possible. Similarly, implementations should + /// call [`BlindedPath::use_compact_introduction_node`]. + /// + /// The provided implementation simply delegates to [`MessageRouter::create_blinded_paths`], + /// ignoring the short channel ids. + fn create_compact_blinded_paths< + T: secp256k1::Signing + secp256k1::Verification + >( + &self, recipient: PublicKey, peers: Vec, secp_ctx: &Secp256k1, + ) -> Result, ()> { + let peers = peers + .into_iter() + .map(|ForwardNode { node_id, short_channel_id: _ }| node_id) + .collect(); + self.create_blinded_paths(recipient, peers, secp_ctx) + } } /// A [`MessageRouter`] that can only route to a directly connected [`Destination`]. +/// +/// When creating [`BlindedPath`]s, prefers three-hop paths over two-hops paths for the compact +/// representation. For the non-compact representation, three-hop paths are not considered. +/// +/// # Privacy +/// +/// Creating [`BlindedPath`]s may affect privacy since, if a suitable path cannot be found, it will +/// create a one-hop path using the recipient as the introduction node if it is a announced node. +/// Otherwise, there is no way to find a path to the introduction node in order to send a message, +/// and thus an `Err` is returned. pub struct DefaultMessageRouter>, L: Deref, ES: Deref> where L::Target: Logger, @@ -449,6 +486,100 @@ where pub fn new(network_graph: G, entropy_source: ES) -> Self { Self { network_graph, entropy_source } } + + fn create_blinded_paths_from_iter< + I: Iterator, + T: secp256k1::Signing + secp256k1::Verification + >( + &self, recipient: PublicKey, peers: I, secp_ctx: &Secp256k1, compact_paths: bool + ) -> Result, ()> { + let entropy_source = &*self.entropy_source; + let recipient_node_id = NodeId::from_pubkey(&recipient); + + // Limit the number of blinded paths that are computed. + const MAX_PATHS: usize = 3; + + // Ensure peers have at least three channels so that it is more difficult to infer the + // recipient's node_id. + const MIN_PEER_CHANNELS: usize = 3; + + let network_graph = self.network_graph.deref().read_only(); + let is_recipient_announced = + network_graph.nodes().contains_key(&NodeId::from_pubkey(&recipient)); + + let mut peer_info = peers + .map(|peer| (NodeId::from_pubkey(&peer.node_id), peer)) + // Limit to peers with announced channels + .filter_map(|(node_id, peer)| + network_graph + .node(&node_id) + .filter(|info| info.channels.len() >= MIN_PEER_CHANNELS) + .map(|info| (node_id, peer, info.is_tor_only(), &info.channels)) + ) + // Exclude Tor-only nodes when the recipient is announced. + .filter(|(_, _, is_tor_only, _)| !(*is_tor_only && is_recipient_announced)) + .collect::>(); + + // Prefer using non-Tor nodes with the most channels as the introduction node. + peer_info.sort_unstable_by(|(_, _, a_tor_only, a_channels), (_, _, b_tor_only, b_channels)| { + a_tor_only.cmp(b_tor_only).then(a_channels.len().cmp(&b_channels.len()).reverse()) + }); + + let three_hop_paths = peer_info.iter() + // Pair peers with their other peers + .flat_map(|(node_id, peer, _, channels)| + channels + .iter() + .filter_map(|scid| network_graph.channels().get(scid)) + .filter_map(move |info| info + .as_directed_to(&node_id) + .map(|(_, source)| source) + ) + .filter(|source| **source != recipient_node_id) + .filter(|source| network_graph + .node(source) + .and_then(|info| info.announcement_info.as_ref()) + .map(|info| info.features().supports_onion_messages()) + .unwrap_or(false) + ) + .filter_map(|source| source.as_pubkey().ok()) + .map(move |source_pubkey| (source_pubkey, peer.clone())) + ) + .map(|(source_pubkey, peer)| BlindedPath::new_for_message(&[ForwardNode { node_id: source_pubkey, short_channel_id: None }, peer], recipient, entropy_source, secp_ctx)) + .take(MAX_PATHS); + + let two_hop_paths = peer_info + .iter() + .map(|(_, peer, _, _)| BlindedPath::new_for_message(&[peer.clone()], recipient, entropy_source, secp_ctx)) + .take(MAX_PATHS); + + // Prefer three-hop paths over two-hop paths for compact paths. Fallback to a one-hop path + // if none were found and the recipient node is announced. + let mut paths = (!compact_paths).then(|| vec![]) + .or_else(|| three_hop_paths.collect::, _>>().ok()) + .and_then(|paths| (!paths.is_empty()).then(|| paths)) + .or_else(|| two_hop_paths.collect::, _>>().ok()) + .and_then(|paths| (!paths.is_empty()).then(|| paths)) + .or_else(|| is_recipient_announced + .then(|| BlindedPath::one_hop_for_message(recipient, entropy_source, secp_ctx) + .map(|path| vec![path]) + .unwrap_or(vec![]) + ) + ) + .ok_or(())?; + + if paths.is_empty() { + return Err(()); + } + + if compact_paths { + for path in &mut paths { + path.use_compact_introduction_node(&network_graph); + } + } + + Ok(paths) + } } impl>, L: Deref, ES: Deref> MessageRouter for DefaultMessageRouter @@ -492,59 +623,20 @@ where fn create_blinded_paths< T: secp256k1::Signing + secp256k1::Verification >( - &self, recipient: PublicKey, peers: Vec, secp_ctx: &Secp256k1, + &self, recipient: PublicKey, peers: Vec, secp_ctx: &Secp256k1, ) -> Result, ()> { - // Limit the number of blinded paths that are computed. - const MAX_PATHS: usize = 3; - - // Ensure peers have at least three channels so that it is more difficult to infer the - // recipient's node_id. - const MIN_PEER_CHANNELS: usize = 3; - - let network_graph = self.network_graph.deref().read_only(); - let is_recipient_announced = - network_graph.nodes().contains_key(&NodeId::from_pubkey(&recipient)); - - let mut peer_info = peers.into_iter() - // Limit to peers with announced channels - .filter_map(|peer| - network_graph - .node(&NodeId::from_pubkey(&peer.node_id)) - .filter(|info| info.channels.len() >= MIN_PEER_CHANNELS) - .map(|info| (peer, info.is_tor_only(), info.channels.len())) - ) - // Exclude Tor-only nodes when the recipient is announced. - .filter(|(_, is_tor_only, _)| !(*is_tor_only && is_recipient_announced)) - .collect::>(); - - // Prefer using non-Tor nodes with the most channels as the introduction node. - peer_info.sort_unstable_by(|(_, a_tor_only, a_channels), (_, b_tor_only, b_channels)| { - a_tor_only.cmp(b_tor_only).then(a_channels.cmp(b_channels).reverse()) - }); - - let paths = peer_info.into_iter() - .map(|(peer, _, _)| { - BlindedPath::new_for_message(&[peer], recipient, &*self.entropy_source, secp_ctx) - }) - .take(MAX_PATHS) - .collect::, _>>(); - - let mut paths = match paths { - Ok(paths) if !paths.is_empty() => Ok(paths), - _ => { - if is_recipient_announced { - BlindedPath::one_hop_for_message(recipient, &*self.entropy_source, secp_ctx) - .map(|path| vec![path]) - } else { - Err(()) - } - }, - }?; - for path in &mut paths { - path.use_compact_introduction_node(&network_graph); - } + let peers = peers + .into_iter() + .map(|node_id| ForwardNode { node_id, short_channel_id: None }); + self.create_blinded_paths_from_iter(recipient, peers, secp_ctx, false) + } - Ok(paths) + fn create_compact_blinded_paths< + T: secp256k1::Signing + secp256k1::Verification + >( + &self, recipient: PublicKey, peers: Vec, secp_ctx: &Secp256k1, + ) -> Result, ()> { + self.create_blinded_paths_from_iter(recipient, peers.into_iter(), secp_ctx, true) } } @@ -1081,10 +1173,7 @@ where let peers = self.message_recipients.lock().unwrap() .iter() .filter(|(_, peer)| matches!(peer, OnionMessageRecipient::ConnectedPeer(_))) - .map(|(node_id, _ )| ForwardNode { - node_id: *node_id, - short_channel_id: None, - }) + .map(|(node_id, _ )| *node_id) .collect::>(); self.message_router diff --git a/lightning/src/routing/gossip.rs b/lightning/src/routing/gossip.rs index 25ee1b97ffd..6ed0fa1d0f3 100644 --- a/lightning/src/routing/gossip.rs +++ b/lightning/src/routing/gossip.rs @@ -1040,7 +1040,7 @@ impl<'a> DirectedChannelInfo<'a> { /// Returns information for the direction. #[inline] - pub(super) fn direction(&self) -> &'a ChannelUpdateInfo { self.direction } + pub(crate) fn direction(&self) -> &'a ChannelUpdateInfo { self.direction } /// Returns the `node_id` of the source hop. /// diff --git a/lightning/src/routing/router.rs b/lightning/src/routing/router.rs index e1b5b655719..57828dceece 100644 --- a/lightning/src/routing/router.rs +++ b/lightning/src/routing/router.rs @@ -36,6 +36,11 @@ use core::{cmp, fmt}; use core::ops::Deref; /// A [`Router`] implemented using [`find_route`]. +/// +/// # Privacy +/// +/// Implements [`MessageRouter`] by delegating to [`DefaultMessageRouter`]. See those docs for +/// privacy implications. pub struct DefaultRouter> + Clone, L: Deref, ES: Deref, S: Deref, SP: Sized, Sc: ScoreLookUp> where L::Target: Logger, S::Target: for <'a> LockableScore<'a, ScoreLookUp = Sc>, @@ -88,6 +93,9 @@ impl> + Clone, L: Deref, ES: Deref, S: Deref, &self, recipient: PublicKey, first_hops: Vec, tlvs: ReceiveTlvs, amount_msats: u64, secp_ctx: &Secp256k1 ) -> Result, ()> { + let entropy_source = &*self.entropy_source; + let recipient_node_id = NodeId::from_pubkey(&recipient); + // Limit the number of blinded paths that are computed. const MAX_PAYMENT_PATHS: usize = 3; @@ -95,18 +103,27 @@ impl> + Clone, L: Deref, ES: Deref, S: Deref, // recipient's node_id. const MIN_PEER_CHANNELS: usize = 3; + // The minimum channel balance certainty required for using a channel in a blinded path. + const MIN_CHANNEL_CERTAINTY: f64 = 0.5; + + // The minimum success probability required for using a channel in a blinded path. + const MIN_SUCCESS_PROBABILITY: f64 = 0.25; + let network_graph = self.network_graph.deref().read_only(); - let paths = first_hops.into_iter() + let counterparty_channels = first_hops.into_iter() .filter(|details| details.counterparty.features.supports_route_blinding()) .filter(|details| amount_msats <= details.inbound_capacity_msat) .filter(|details| amount_msats >= details.inbound_htlc_minimum_msat.unwrap_or(0)) .filter(|details| amount_msats <= details.inbound_htlc_maximum_msat.unwrap_or(u64::MAX)) - .filter(|details| network_graph + // Limit to counterparties with announced channels + .filter_map(|details| + network_graph .node(&NodeId::from_pubkey(&details.counterparty.node_id)) - .map(|node_info| node_info.channels.len() >= MIN_PEER_CHANNELS) - .unwrap_or(false) + .map(|info| &info.channels[..]) + .and_then(|channels| (channels.len() >= MIN_PEER_CHANNELS).then(|| channels)) + .map(|channels| (details, channels)) ) - .filter_map(|details| { + .filter_map(|(details, counterparty_channels)| { let short_channel_id = match details.get_inbound_payment_scid() { Some(short_channel_id) => short_channel_id, None => return None, @@ -124,7 +141,7 @@ impl> + Clone, L: Deref, ES: Deref, S: Deref, max_cltv_expiry: tlvs.payment_constraints.max_cltv_expiry + cltv_expiry_delta, htlc_minimum_msat: details.inbound_htlc_minimum_msat.unwrap_or(0), }; - Some(payment::ForwardNode { + let forward_node = payment::ForwardNode { tlvs: ForwardTlvs { short_channel_id, payment_relay, @@ -133,29 +150,112 @@ impl> + Clone, L: Deref, ES: Deref, S: Deref, }, node_id: details.counterparty.node_id, htlc_maximum_msat: details.inbound_htlc_maximum_msat.unwrap_or(u64::MAX), - }) - }) - .map(|forward_node| { + }; + Some((forward_node, counterparty_channels)) + }); + + let scorer = self.scorer.read_lock(); + let three_hop_paths = counterparty_channels.clone() + // Pair counterparties with their other channels + .flat_map(|(forward_node, counterparty_channels)| + counterparty_channels + .iter() + .filter_map(|scid| network_graph.channels().get_key_value(scid)) + .filter_map(move |(scid, info)| info + .as_directed_to(&NodeId::from_pubkey(&forward_node.node_id)) + .map(|(info, source)| (source, *scid, info)) + ) + .filter(|(source, _, _)| **source != recipient_node_id) + .filter(|(source, _, _)| network_graph + .node(source) + .and_then(|info| info.announcement_info.as_ref()) + .map(|info| info.features().supports_route_blinding()) + .unwrap_or(false) + ) + .filter(|(_, _, info)| amount_msats >= info.direction().htlc_minimum_msat) + .filter(|(_, _, info)| amount_msats <= info.direction().htlc_maximum_msat) + .filter(|(_, scid, info)| { + scorer.channel_balance_certainty(*scid, info) >= MIN_CHANNEL_CERTAINTY + }) + .map(move |(source, scid, info)| (source, scid, info, forward_node.clone())) + ) + // Construct blinded paths where the counterparty's counterparty is the introduction + // node: + // + // source --- info ---> counterparty --- counterparty_forward_node ---> recipient + .filter_map(|(introduction_node_id, scid, info, counterparty_forward_node)| { + let amount_msat = amount_msats; + let effective_capacity = info.effective_capacity(); + let usage = ChannelUsage { amount_msat, inflight_htlc_msat: 0, effective_capacity }; + let success_probability = scorer.channel_success_probability( + scid, &info, usage, &self.score_params + ); + + if !success_probability.is_finite() { + return None; + } + + if success_probability < MIN_SUCCESS_PROBABILITY { + return None; + } + + let htlc_minimum_msat = info.direction().htlc_minimum_msat; + let htlc_maximum_msat = info.direction().htlc_maximum_msat; + let payment_relay: PaymentRelay = match info.try_into() { + Ok(payment_relay) => payment_relay, + Err(()) => return None, + }; + let payment_constraints = PaymentConstraints { + max_cltv_expiry: payment_relay.cltv_expiry_delta as u32 + + counterparty_forward_node.tlvs.payment_constraints.max_cltv_expiry, + htlc_minimum_msat, + }; + let introduction_forward_node = payment::ForwardNode { + tlvs: ForwardTlvs { + short_channel_id: scid, + payment_relay, + payment_constraints, + features: BlindedHopFeatures::empty(), + }, + node_id: introduction_node_id.as_pubkey().unwrap(), + htlc_maximum_msat, + }; + let path = BlindedPath::new_for_payment( + &[introduction_forward_node, counterparty_forward_node], recipient, + tlvs.clone(), u64::MAX, MIN_FINAL_CLTV_EXPIRY_DELTA, entropy_source, secp_ctx + ); + + Some(path.map(|path| (path, success_probability))) + }); + + let two_hop_paths = counterparty_channels + .map(|(forward_node, _)| { BlindedPath::new_for_payment( &[forward_node], recipient, tlvs.clone(), u64::MAX, MIN_FINAL_CLTV_EXPIRY_DELTA, - &*self.entropy_source, secp_ctx + entropy_source, secp_ctx ) }) - .take(MAX_PAYMENT_PATHS) - .collect::, _>>(); - - match paths { - Ok(paths) if !paths.is_empty() => Ok(paths), - _ => { - if network_graph.nodes().contains_key(&NodeId::from_pubkey(&recipient)) { - BlindedPath::one_hop_for_payment( - recipient, tlvs, MIN_FINAL_CLTV_EXPIRY_DELTA, &*self.entropy_source, secp_ctx - ).map(|path| vec![path]) - } else { - Err(()) - } - }, - } + .take(MAX_PAYMENT_PATHS); + + three_hop_paths + .collect::, _>>().ok() + .and_then(|paths| (!paths.is_empty()).then(|| paths)) + .map(|mut paths| { + paths.sort_unstable_by(|a, b| b.1.partial_cmp(&a.1).unwrap()); + paths.into_iter().map(|(path, _)| path).take(MAX_PAYMENT_PATHS).collect::>() + }) + .or_else(|| two_hop_paths.collect::, _>>().ok()) + .and_then(|paths| (!paths.is_empty()).then(|| paths)) + .or_else(|| network_graph + .node(&NodeId::from_pubkey(&recipient)).ok_or(()) + .and_then(|_| BlindedPath::one_hop_for_payment( + recipient, tlvs, MIN_FINAL_CLTV_EXPIRY_DELTA, entropy_source, secp_ctx + ) + ) + .map(|path| vec![path]) + .ok() + ) + .ok_or(()) } } @@ -173,10 +273,18 @@ impl< G: Deref> + Clone, L: Deref, ES: Deref, S: Deref, fn create_blinded_paths< T: secp256k1::Signing + secp256k1::Verification > ( - &self, recipient: PublicKey, peers: Vec, secp_ctx: &Secp256k1, + &self, recipient: PublicKey, peers: Vec, secp_ctx: &Secp256k1, ) -> Result, ()> { self.message_router.create_blinded_paths(recipient, peers, secp_ctx) } + + fn create_compact_blinded_paths< + T: secp256k1::Signing + secp256k1::Verification + > ( + &self, recipient: PublicKey, peers: Vec, secp_ctx: &Secp256k1, + ) -> Result, ()> { + self.message_router.create_compact_blinded_paths(recipient, peers, secp_ctx) + } } /// A trait defining behavior for routing a payment. diff --git a/lightning/src/routing/scoring.rs b/lightning/src/routing/scoring.rs index d782958ca91..9acd792b9b0 100644 --- a/lightning/src/routing/scoring.rs +++ b/lightning/src/routing/scoring.rs @@ -57,7 +57,7 @@ //! [`find_route`]: crate::routing::router::find_route use crate::ln::msgs::DecodeError; -use crate::routing::gossip::{EffectiveCapacity, NetworkGraph, NodeId}; +use crate::routing::gossip::{DirectedChannelInfo, EffectiveCapacity, NetworkGraph, NodeId}; use crate::routing::router::{Path, CandidateRouteHop, PublicHopCandidate}; use crate::util::ser::{Readable, ReadableArgs, Writeable, Writer}; use crate::util::logger::Logger; @@ -96,6 +96,7 @@ pub trait ScoreLookUp { /// on a per-routefinding-call basis through to the scorer methods, /// which are used to determine the parameters for the suitability of channels for use. type ScoreParams; + /// Returns the fee in msats willing to be paid to avoid routing `send_amt_msat` through the /// given channel in the direction from `source` to `target`. /// @@ -107,6 +108,28 @@ pub trait ScoreLookUp { fn channel_penalty_msat( &self, candidate: &CandidateRouteHop, usage: ChannelUsage, score_params: &Self::ScoreParams ) -> u64; + + /// Returns the success probability of sending an HTLC through a channel. + /// + /// Expected to return a value between `0.0` and `1.0`, inclusive, where `0.0` indicates + /// highly unlikely and `1.0` highly likely. + /// + /// This is useful to determine whether a channel should be included in a blinded path and the + /// preferred ordering of blinded paths. + fn channel_success_probability( + &self, _short_channel_id: u64, _info: &DirectedChannelInfo, _usage: ChannelUsage, + _score_params: &Self::ScoreParams + ) -> f64 { 0.5 } + + /// Returns how certain any knowledge gained about the channel's liquidity balance is. + /// + /// Expected to return a value between `0.0` and `1.0`, inclusive, where `0.0` indicates + /// complete uncertainty and `1.0` complete certainty. + /// + /// This is useful to determine whether a channel should be included in a blinded path. + fn channel_balance_certainty( + &self, _short_channel_id: u64, _info: &DirectedChannelInfo + ) -> f64 { 0.5 } } /// `ScoreUpdate` is used to update the scorer's internal state after a payment attempt. @@ -1228,6 +1251,27 @@ DirectedChannelLiquidity< L, BRT, T> { liquidity_penalty_msat.saturating_add(amount_penalty_msat) } + fn success_probability( + &self, usage: ChannelUsage, score_params: &ProbabilisticScoringFeeParameters + ) -> f64 { + let amount_msat = usage.amount_msat; + let available_capacity = self.capacity_msat; + let max_liquidity_msat = self.max_liquidity_msat(); + let min_liquidity_msat = core::cmp::min(self.min_liquidity_msat(), max_liquidity_msat); + + if amount_msat <= min_liquidity_msat { + 1.0 + } else if amount_msat >= max_liquidity_msat { + 0.0 + } else { + let (numerator, denominator) = success_probability( + amount_msat, min_liquidity_msat, max_liquidity_msat, available_capacity, + score_params, false + ); + numerator as f64 / denominator as f64 + } + } + /// Returns the lower bound of the channel liquidity balance in this direction. #[inline(always)] fn min_liquidity_msat(&self) -> u64 { @@ -1366,6 +1410,28 @@ impl>, L: Deref> ScoreLookUp for Probabilistic .saturating_add(anti_probing_penalty_msat) .saturating_add(base_penalty_msat) } + + fn channel_success_probability( + &self, short_channel_id: u64, info: &DirectedChannelInfo, usage: ChannelUsage, + score_params: &ProbabilisticScoringFeeParameters + ) -> f64 { + self.channel_liquidities + .get(&short_channel_id) + .unwrap_or(&ChannelLiquidity::new(Duration::ZERO)) + .as_directed(info.source(), info.target(), usage.effective_capacity.as_msat()) + .success_probability(usage, score_params) + } + + fn channel_balance_certainty(&self, short_channel_id: u64, info: &DirectedChannelInfo) -> f64 { + self.channel_liquidities + .get(&short_channel_id) + .map(|channel| + ((channel.min_liquidity_offset_msat + channel.max_liquidity_offset_msat) as f64) + / info.effective_capacity().as_msat() as f64 + ) + .and_then(|certainty| certainty.is_finite().then(|| certainty)) + .unwrap_or(0.0) + } } impl>, L: Deref> ScoreUpdate for ProbabilisticScorer where L::Target: Logger { diff --git a/lightning/src/util/test_utils.rs b/lightning/src/util/test_utils.rs index a5363d32c76..f6616a8e5d2 100644 --- a/lightning/src/util/test_utils.rs +++ b/lightning/src/util/test_utils.rs @@ -250,10 +250,18 @@ impl<'a> MessageRouter for TestRouter<'a> { fn create_blinded_paths< T: secp256k1::Signing + secp256k1::Verification >( - &self, recipient: PublicKey, peers: Vec, secp_ctx: &Secp256k1, + &self, recipient: PublicKey, peers: Vec, secp_ctx: &Secp256k1, ) -> Result, ()> { self.router.create_blinded_paths(recipient, peers, secp_ctx) } + + fn create_compact_blinded_paths< + T: secp256k1::Signing + secp256k1::Verification + >( + &self, recipient: PublicKey, peers: Vec, secp_ctx: &Secp256k1, + ) -> Result, ()> { + self.router.create_compact_blinded_paths(recipient, peers, secp_ctx) + } } impl<'a> Drop for TestRouter<'a> { @@ -285,10 +293,16 @@ impl<'a> MessageRouter for TestMessageRouter<'a> { } fn create_blinded_paths( - &self, recipient: PublicKey, peers: Vec, secp_ctx: &Secp256k1, + &self, recipient: PublicKey, peers: Vec, secp_ctx: &Secp256k1, ) -> Result, ()> { self.inner.create_blinded_paths(recipient, peers, secp_ctx) } + + fn create_compact_blinded_paths( + &self, recipient: PublicKey, peers: Vec, secp_ctx: &Secp256k1, + ) -> Result, ()> { + self.inner.create_compact_blinded_paths(recipient, peers, secp_ctx) + } } pub struct OnlyReadsKeysInterface {}