Update route hint selection to prefer lowest channel above minimum
[rust-lightning] / lightning-invoice / src / utils.rs
index f837502e3e687db86bda72850ed7c2db4c801599..38f3a597871a8327e9dd33c0c912f5c70688d843 100644 (file)
@@ -38,7 +38,7 @@ use core::time::Duration;
 ///
 /// `invoice_expiry_delta_secs` describes the number of seconds that the invoice is valid for
 /// in excess of the current time.
-/// 
+///
 /// `duration_since_epoch` is the current time since epoch in seconds.
 ///
 /// You can specify a custom `min_final_cltv_expiry_delta`, or let LDK default it to
@@ -56,7 +56,7 @@ use core::time::Duration;
 /// [`ChannelManager::create_inbound_payment_for_hash`]: lightning::ln::channelmanager::ChannelManager::create_inbound_payment_for_hash
 /// [`PhantomRouteHints::channels`]: lightning::ln::channelmanager::PhantomRouteHints::channels
 /// [`MIN_FINAL_CLTV_EXPIRY_DETLA`]: lightning::ln::channelmanager::MIN_FINAL_CLTV_EXPIRY_DELTA
-/// 
+///
 /// This can be used in a `no_std` environment, where [`std::time::SystemTime`] is not
 /// available and the current time is supplied by the caller.
 pub fn create_phantom_invoice<ES: Deref, NS: Deref, L: Deref>(
@@ -98,7 +98,7 @@ where
 ///
 /// `invoice_expiry_delta_secs` describes the number of seconds that the invoice is valid for
 /// in excess of the current time.
-/// 
+///
 /// `duration_since_epoch` is the current time since epoch in seconds.
 ///
 /// Note that the provided `keys_manager`'s `NodeSigner` implementation must support phantom
@@ -110,7 +110,7 @@ where
 /// [`ChannelManager::create_inbound_payment`]: lightning::ln::channelmanager::ChannelManager::create_inbound_payment
 /// [`ChannelManager::create_inbound_payment_for_hash`]: lightning::ln::channelmanager::ChannelManager::create_inbound_payment_for_hash
 /// [`PhantomRouteHints::channels`]: lightning::ln::channelmanager::PhantomRouteHints::channels
-/// 
+///
 /// This can be used in a `no_std` environment, where [`std::time::SystemTime`] is not
 /// available and the current time is supplied by the caller.
 pub fn create_phantom_invoice_with_description_hash<ES: Deref, NS: Deref, L: Deref>(
@@ -509,11 +509,17 @@ fn _create_invoice_from_channelmanager_and_duration_since_epoch_with_payment_has
 ///
 /// The filtering is based on the following criteria:
 /// * Only one channel per counterparty node
-/// * Always select the channel with the highest inbound capacity per counterparty node
+/// * If the counterparty has a channel that is above the `min_inbound_capacity_msat` + 10% scaling
+///   factor (to allow some margin for change in inbound), select the channel with the lowest
+///   inbound capacity that is above this threshold.
+/// * If no `min_inbound_capacity_msat` is specified, or the counterparty has no channels above the
+///   minimum + 10% scaling factor, select the channel with the highest inbound capacity per counterparty.
 /// * Prefer channels with capacity at least `min_inbound_capacity_msat` and where the channel
 ///   `is_usable` (i.e. the peer is connected).
-/// * If any public channel exists, the returned `RouteHint`s will be empty, and the sender will
-///   need to find the path by looking at the public channels instead
+/// * If any public channel exists, only public [`RouteHint`]s will be returned.
+/// * If any public, announced, channel exists (i.e. a channel with 7+ confs, to ensure the
+///   announcement has had a chance to propagate), no [`RouteHint`]s will be returned, as the
+///   sender is expected to find the path by looking at the public channels instead.
 fn filter_channels<L: Deref>(
        channels: Vec<ChannelDetails>, min_inbound_capacity_msat: Option<u64>, logger: &L
 ) -> Vec<RouteHint> where L::Target: Logger {
@@ -522,6 +528,7 @@ fn filter_channels<L: Deref>(
        let mut min_capacity_channel_exists = false;
        let mut online_channel_exists = false;
        let mut online_min_capacity_channel_exists = false;
+       let mut has_pub_unconf_chan = false;
 
        log_trace!(logger, "Considering {} channels for invoice route hints", channels.len());
        for channel in channels.into_iter().filter(|chan| chan.is_channel_ready) {
@@ -531,11 +538,18 @@ fn filter_channels<L: Deref>(
                }
 
                if channel.is_public {
-                       // If any public channel exists, return no hints and let the sender
-                       // look at the public channels instead.
-                       log_trace!(logger, "Not including channels in invoice route hints on account of public channel {}",
-                               log_bytes!(channel.channel_id));
-                       return vec![]
+                       if channel.confirmations.is_some() && channel.confirmations < Some(7) {
+                               // If we have a public channel, but it doesn't have enough confirmations to (yet)
+                               // be in the public network graph (and have gotten a chance to propagate), include
+                               // route hints but only for public channels to protect private channel privacy.
+                               has_pub_unconf_chan = true;
+                       } else {
+                               // If any public channel exists, return no hints and let the sender
+                               // look at the public channels instead.
+                               log_trace!(logger, "Not including channels in invoice route hints on account of public channel {}",
+                                       log_bytes!(channel.channel_id));
+                               return vec![]
+                       }
                }
 
                if channel.inbound_capacity_msat >= min_inbound_capacity {
@@ -557,20 +571,36 @@ fn filter_channels<L: Deref>(
                match filtered_channels.entry(channel.counterparty.node_id) {
                        hash_map::Entry::Occupied(mut entry) => {
                                let current_max_capacity = entry.get().inbound_capacity_msat;
-                               if channel.inbound_capacity_msat < current_max_capacity {
+                               // If this channel is public and the previous channel is not, ensure we replace the
+                               // previous channel to avoid announcing non-public channels.
+                               let new_now_public = channel.is_public && !entry.get().is_public;
+                               // Decide whether we prefer the currently selected channel with the node to the new one,
+                               // based on their inbound capacity. 
+                               let prefer_current = prefer_current_channel(min_inbound_capacity_msat, current_max_capacity,
+                                       channel.inbound_capacity_msat);
+                               // If the public-ness of the channel has not changed (in which case simply defer to
+                               // `new_now_public), and this channel has more desirable inbound than the incumbent,
+                               // prefer to include this channel.
+                               let new_channel_preferable = channel.is_public == entry.get().is_public && !prefer_current;
+
+                               if new_now_public || new_channel_preferable {
                                        log_trace!(logger,
-                                               "Preferring counterparty {} channel {} ({} msats) over {} ({} msats) for invoice route hints",
+                                               "Preferring counterparty {} channel {} (SCID {:?}, {} msats) over {} (SCID {:?}, {} msats) for invoice route hints",
                                                log_pubkey!(channel.counterparty.node_id),
-                                               log_bytes!(entry.get().channel_id), current_max_capacity,
-                                               log_bytes!(channel.channel_id), channel.inbound_capacity_msat);
-                                       continue;
+                                               log_bytes!(channel.channel_id), channel.short_channel_id,
+                                               channel.inbound_capacity_msat,
+                                               log_bytes!(entry.get().channel_id), entry.get().short_channel_id,
+                                               current_max_capacity);
+                                       entry.insert(channel);
+                               } else {
+                                       log_trace!(logger,
+                                               "Preferring counterparty {} channel {} (SCID {:?}, {} msats) over {} (SCID {:?}, {} msats) for invoice route hints",
+                                               log_pubkey!(channel.counterparty.node_id),
+                                               log_bytes!(entry.get().channel_id), entry.get().short_channel_id,
+                                               current_max_capacity,
+                                               log_bytes!(channel.channel_id), channel.short_channel_id,
+                                               channel.inbound_capacity_msat);
                                }
-                               log_trace!(logger,
-                                       "Preferring counterparty {} channel {} ({} msats) over {} ({} msats) for invoice route hints",
-                                       log_pubkey!(channel.counterparty.node_id),
-                                       log_bytes!(channel.channel_id), channel.inbound_capacity_msat,
-                                       log_bytes!(entry.get().channel_id), current_max_capacity);
-                               entry.insert(channel);
                        }
                        hash_map::Entry::Vacant(entry) => {
                                entry.insert(channel);
@@ -600,7 +630,12 @@ fn filter_channels<L: Deref>(
                .map(|(_, channel)| channel)
                .filter(|channel| {
                        let has_enough_capacity = channel.inbound_capacity_msat >= min_inbound_capacity;
-                       let include_channel = if online_min_capacity_channel_exists {
+                       let include_channel = if has_pub_unconf_chan {
+                               // If we have a public channel, but it doesn't have enough confirmations to (yet)
+                               // be in the public network graph (and have gotten a chance to propagate), include
+                               // route hints but only for public channels to protect private channel privacy.
+                               channel.is_public
+                       } else if online_min_capacity_channel_exists {
                                has_enough_capacity && channel.is_usable
                        } else if min_capacity_channel_exists && online_channel_exists {
                                // If there are some online channels and some min_capacity channels, but no
@@ -620,7 +655,7 @@ fn filter_channels<L: Deref>(
                                log_trace!(logger, "Ignoring channel {} without enough capacity for invoice route hints",
                                        log_bytes!(channel.channel_id));
                        } else {
-                               debug_assert!(!channel.is_usable);
+                               debug_assert!(!channel.is_usable || (has_pub_unconf_chan && !channel.is_public));
                                log_trace!(logger, "Ignoring channel {} with disconnected peer",
                                        log_bytes!(channel.channel_id));
                        }
@@ -631,6 +666,41 @@ fn filter_channels<L: Deref>(
                .collect::<Vec<RouteHint>>()
 }
 
+/// prefer_current_channel chooses a channel to use for route hints between a currently selected and candidate
+/// channel based on the inbound capacity of each channel and the minimum inbound capacity requested for the hints,
+/// returning true if the current channel should be preferred over the candidate channel.
+/// * If no minimum amount is requested, the channel with the most inbound is chosen to maximize the chances that a
+///   payment of any size will succeed.
+/// * If we have channels with inbound above our minimum requested inbound (plus a 10% scaling factor, expressed as a
+///   percentage) then we choose the lowest inbound channel with above this amount. If we have sufficient inbound
+///   channels, we don't want to deplete our larger channels with small payments (the off-chain version of "grinding
+///   our change").
+/// * If no channel above our minimum amount exists, then we just prefer the channel with the most inbound to give
+///   payments the best chance of succeeding in multiple parts.
+fn prefer_current_channel(min_inbound_capacity_msat: Option<u64>, current_channel: u64,
+       candidate_channel: u64) -> bool {
+
+       // If no min amount is given for the hints, err of the side of caution and choose the largest channel inbound to
+       // maximize chances of any payment succeeding.
+       if min_inbound_capacity_msat.is_none() {
+               return current_channel > candidate_channel
+       }
+
+       let scaled_min_inbound = min_inbound_capacity_msat.unwrap() * 110;
+       let current_sufficient = current_channel * 100 >= scaled_min_inbound;
+       let candidate_sufficient = candidate_channel * 100 >= scaled_min_inbound;
+
+       if current_sufficient && candidate_sufficient {
+               return current_channel < candidate_channel
+       } else if current_sufficient {
+               return true
+       } else if candidate_sufficient {
+               return false
+       }
+
+       current_channel > candidate_channel
+}
+
 #[cfg(test)]
 mod test {
        use core::time::Duration;
@@ -638,17 +708,45 @@ mod test {
        use bitcoin_hashes::{Hash, sha256};
        use bitcoin_hashes::sha256::Hash as Sha256;
        use lightning::chain::keysinterface::{EntropySource, PhantomKeysManager};
+       use lightning::events::{MessageSendEvent, MessageSendEventsProvider, Event};
        use lightning::ln::{PaymentPreimage, PaymentHash};
        use lightning::ln::channelmanager::{PhantomRouteHints, MIN_FINAL_CLTV_EXPIRY_DELTA, PaymentId};
        use lightning::ln::functional_test_utils::*;
        use lightning::ln::msgs::ChannelMessageHandler;
        use lightning::routing::router::{PaymentParameters, RouteParameters, find_route};
-       use lightning::util::events::{MessageSendEvent, MessageSendEventsProvider, Event};
        use lightning::util::test_utils;
        use lightning::util::config::UserConfig;
        use crate::utils::create_invoice_from_channelmanager_and_duration_since_epoch;
        use std::collections::HashSet;
 
+       #[test]
+       fn test_prefer_current_channel() {
+               // No minimum, prefer larger candidate channel.
+               assert_eq!(crate::utils::prefer_current_channel(None, 100, 200), false);
+
+               // No minimum, prefer larger current channel.
+               assert_eq!(crate::utils::prefer_current_channel(None, 200, 100), true);
+
+               // Minimum set, prefer current channel over minimum + buffer.
+               assert_eq!(crate::utils::prefer_current_channel(Some(100), 115, 100), true);
+
+               // Minimum set, prefer candidate channel over minimum + buffer.
+               assert_eq!(crate::utils::prefer_current_channel(Some(100), 105, 125), false);
+               
+               // Minimum set, both channels sufficient, prefer smaller current channel.
+               assert_eq!(crate::utils::prefer_current_channel(Some(100), 115, 125), true);
+               
+               // Minimum set, both channels sufficient, prefer smaller candidate channel.
+               assert_eq!(crate::utils::prefer_current_channel(Some(100), 200, 160), false);
+
+               // Minimum set, neither sufficient, prefer larger current channel.
+               assert_eq!(crate::utils::prefer_current_channel(Some(200), 100, 50), true);
+
+               // Minimum set, neither sufficient, prefer larger candidate channel.
+               assert_eq!(crate::utils::prefer_current_channel(Some(200), 100, 150), false);
+       }
+
+
        #[test]
        fn test_from_channelmanager() {
                let chanmon_cfgs = create_chanmon_cfgs(2);
@@ -789,6 +887,63 @@ mod test {
                assert_eq!(invoice.payment_hash(), &sha256::Hash::from_slice(&payment_hash.0[..]).unwrap());
        }
 
+       #[test]
+       fn test_hints_has_only_public_confd_channels() {
+               let chanmon_cfgs = create_chanmon_cfgs(2);
+               let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
+               let mut config = test_default_channel_config();
+               config.channel_handshake_config.minimum_depth = 1;
+               let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config), Some(config)]);
+               let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
+
+               // Create a private channel with lots of capacity and a lower value public channel (without
+               // confirming the funding tx yet).
+               let unannounced_scid = create_unannounced_chan_between_nodes_with_value(&nodes, 0, 1, 10_000_000, 0);
+               let conf_tx = create_chan_between_nodes_with_value_init(&nodes[0], &nodes[1], 10_000, 0);
+
+               // Before the channel is available, we should include the unannounced_scid.
+               let mut scid_aliases = HashSet::new();
+               scid_aliases.insert(unannounced_scid.0.short_channel_id_alias.unwrap());
+               match_invoice_routes(Some(5000), &nodes[1], scid_aliases.clone());
+
+               // However after we mine the funding tx and exchange channel_ready messages for the public
+               // channel we'll immediately switch to including it as a route hint, even though it isn't
+               // yet announced.
+               let pub_channel_scid = mine_transaction(&nodes[0], &conf_tx);
+               let node_a_pub_channel_ready = get_event_msg!(nodes[0], MessageSendEvent::SendChannelReady, nodes[1].node.get_our_node_id());
+               nodes[1].node.handle_channel_ready(&nodes[0].node.get_our_node_id(), &node_a_pub_channel_ready);
+
+               assert_eq!(mine_transaction(&nodes[1], &conf_tx), pub_channel_scid);
+               let events = nodes[1].node.get_and_clear_pending_msg_events();
+               assert_eq!(events.len(), 2);
+               if let MessageSendEvent::SendChannelReady { msg, .. } = &events[0] {
+                       nodes[0].node.handle_channel_ready(&nodes[1].node.get_our_node_id(), msg);
+               } else { panic!(); }
+               if let MessageSendEvent::SendChannelUpdate { msg, .. } = &events[1] {
+                       nodes[0].node.handle_channel_update(&nodes[1].node.get_our_node_id(), msg);
+               } else { panic!(); }
+
+               nodes[1].node.handle_channel_update(&nodes[0].node.get_our_node_id(), &get_event_msg!(nodes[0], MessageSendEvent::SendChannelUpdate, nodes[1].node.get_our_node_id()));
+
+               expect_channel_ready_event(&nodes[0], &nodes[1].node.get_our_node_id());
+               expect_channel_ready_event(&nodes[1], &nodes[0].node.get_our_node_id());
+
+               scid_aliases.clear();
+               scid_aliases.insert(node_a_pub_channel_ready.short_channel_id_alias.unwrap());
+               match_invoice_routes(Some(5000), &nodes[1], scid_aliases.clone());
+               // This also applies even if the amount is more than the payment amount, to ensure users
+               // dont screw up their privacy.
+               match_invoice_routes(Some(50_000_000), &nodes[1], scid_aliases.clone());
+
+               // The same remains true until the channel has 7 confirmations, at which point we include
+               // no hints.
+               connect_blocks(&nodes[1], 5);
+               match_invoice_routes(Some(5000), &nodes[1], scid_aliases.clone());
+               connect_blocks(&nodes[1], 1);
+               get_event_msg!(nodes[1], MessageSendEvent::SendAnnouncementSignatures, nodes[0].node.get_our_node_id());
+               match_invoice_routes(Some(5000), &nodes[1], HashSet::new());
+       }
+
        #[test]
        fn test_hints_includes_single_channels_to_nodes() {
                let chanmon_cfgs = create_chanmon_cfgs(3);
@@ -807,17 +962,19 @@ mod test {
        }
 
        #[test]
-       fn test_hints_has_only_highest_inbound_capacity_channel() {
+       fn test_hints_has_only_lowest_inbound_capacity_channel_above_minimum() {
                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);
-               let _chan_1_0_low_inbound_capacity = create_unannounced_chan_between_nodes_with_value(&nodes, 1, 0, 100_000, 0);
-               let chan_1_0_high_inbound_capacity = create_unannounced_chan_between_nodes_with_value(&nodes, 1, 0, 10_000_000, 0);
-               let _chan_1_0_medium_inbound_capacity = create_unannounced_chan_between_nodes_with_value(&nodes, 1, 0, 1_000_000, 0);
+
+               let _chan_1_0_inbound_below_amt = create_unannounced_chan_between_nodes_with_value(&nodes, 1, 0, 10_000, 0);
+               let _chan_1_0_large_inbound_above_amt = create_unannounced_chan_between_nodes_with_value(&nodes, 1, 0, 500_000, 0);
+               let chan_1_0_low_inbound_above_amt = create_unannounced_chan_between_nodes_with_value(&nodes, 1, 0, 200_000, 0);
+
                let mut scid_aliases = HashSet::new();
-               scid_aliases.insert(chan_1_0_high_inbound_capacity.0.short_channel_id_alias.unwrap());
-               match_invoice_routes(Some(5000), &nodes[0], scid_aliases);
+               scid_aliases.insert(chan_1_0_low_inbound_above_amt.0.short_channel_id_alias.unwrap());
+               match_invoice_routes(Some(100_000_000), &nodes[0], scid_aliases);
        }
 
        #[test]
@@ -1390,7 +1547,7 @@ mod test {
 
        #[test]
        #[cfg(feature = "std")]
-       fn test_multi_node_hints_has_only_highest_inbound_capacity_channel() {
+       fn test_multi_node_hints_has_only_lowest_inbound_channel_above_minimum() {
                let mut chanmon_cfgs = create_chanmon_cfgs(3);
                let seed_1 = [42u8; 32];
                let seed_2 = [43u8; 32];
@@ -1401,17 +1558,17 @@ mod test {
                let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, None, None]);
                let nodes = create_network(3, &node_cfgs, &node_chanmgrs);
 
-               let _chan_0_1_low_inbound_capacity = create_unannounced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, 0);
-               let chan_0_1_high_inbound_capacity = create_unannounced_chan_between_nodes_with_value(&nodes, 0, 1, 10_000_000, 0);
-               let _chan_0_1_medium_inbound_capacity = create_unannounced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0);
+               let _chan_0_1_below_amt = create_unannounced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, 0);
+               let _chan_0_1_above_amt_high_inbound = create_unannounced_chan_between_nodes_with_value(&nodes, 0, 1, 500_000, 0);
+               let chan_0_1_above_amt_low_inbound = create_unannounced_chan_between_nodes_with_value(&nodes, 0, 1, 180_000, 0);
                let chan_0_2 = create_unannounced_chan_between_nodes_with_value(&nodes, 0, 2, 100000, 10001);
 
                let mut scid_aliases = HashSet::new();
-               scid_aliases.insert(chan_0_1_high_inbound_capacity.0.short_channel_id_alias.unwrap());
+               scid_aliases.insert(chan_0_1_above_amt_low_inbound.0.short_channel_id_alias.unwrap());
                scid_aliases.insert(chan_0_2.0.short_channel_id_alias.unwrap());
 
                match_multi_node_invoice_routes(
-                       Some(10_000),
+                       Some(100_000_000),
                        &nodes[1],
                        vec![&nodes[1], &nodes[2],],
                        scid_aliases,