X-Git-Url: http://git.bitcoin.ninja/index.cgi?a=blobdiff_plain;f=lightning%2Fsrc%2Frouting%2Frouter.rs;h=289221601c5c82e92cd2db457dbfff69a5eef460;hb=fda38196993b22e34ba43f0bb9c94ba842174bf1;hp=90e658604ad1058040643e5b6be48fd93cfe29a8;hpb=7adf2c7f5f1e1c4a72108dcc30899a6ed6964c76;p=rust-lightning diff --git a/lightning/src/routing/router.rs b/lightning/src/routing/router.rs index 90e65860..28922160 100644 --- a/lightning/src/routing/router.rs +++ b/lightning/src/routing/router.rs @@ -17,7 +17,7 @@ use bitcoin::secp256k1::PublicKey; use ln::channelmanager::ChannelDetails; use ln::features::{ChannelFeatures, InvoiceFeatures, NodeFeatures}; use ln::msgs::{DecodeError, ErrorAction, LightningError, MAX_VALUE_MSAT}; -use routing::gossip::{DirectedChannelInfoWithUpdate, EffectiveCapacity, ReadOnlyNetworkGraph, NodeId, RoutingFees}; +use routing::gossip::{DirectedChannelInfoWithUpdate, EffectiveCapacity, ReadOnlyNetworkGraph, NetworkGraph, NodeId, RoutingFees}; use routing::scoring::{ChannelUsage, Score}; use util::ser::{Writeable, Readable, Writer}; use util::logger::{Level, Logger}; @@ -176,6 +176,11 @@ impl_writeable_tlv_based!(RouteParameters, { /// Maximum total CTLV difference we allow for a full payment path. pub const DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA: u32 = 1008; +/// Maximum number of paths we allow an (MPP) payment to have. +// The default limit is currently set rather arbitrary - there aren't any real fundamental path-count +// limits, but for now more than 10 paths likely carries too much one-path failure. +pub const DEFAULT_MAX_PATH_COUNT: u8 = 10; + // The median hop CLTV expiry delta currently seen in the network. const MEDIAN_HOP_CLTV_EXPIRY_DELTA: u32 = 40; @@ -214,13 +219,19 @@ pub struct PaymentParameters { pub expiry_time: Option, /// The maximum total CLTV delta we accept for the route. + /// Defaults to [`DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA`]. pub max_total_cltv_expiry_delta: u32, + + /// The maximum number of paths that may be used by (MPP) payments. + /// Defaults to [`DEFAULT_MAX_PATH_COUNT`]. + pub max_path_count: u8, } impl_writeable_tlv_based!(PaymentParameters, { (0, payee_pubkey, required), (1, max_total_cltv_expiry_delta, (default_value, DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA)), (2, features, option), + (3, max_path_count, (default_value, DEFAULT_MAX_PATH_COUNT)), (4, route_hints, vec_type), (6, expiry_time, option), }); @@ -234,6 +245,7 @@ impl PaymentParameters { route_hints: vec![], expiry_time: None, max_total_cltv_expiry_delta: DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA, + max_path_count: DEFAULT_MAX_PATH_COUNT, } } @@ -269,6 +281,13 @@ impl PaymentParameters { pub fn with_max_total_cltv_expiry_delta(self, max_total_cltv_expiry_delta: u32) -> Self { Self { max_total_cltv_expiry_delta, ..self } } + + /// Includes a limit for the maximum number of payment paths that may be used. + /// + /// (C-not exported) since bindings don't support move semantics + pub fn with_max_path_count(self, max_path_count: u8) -> Self { + Self { max_path_count, ..self } + } } /// A list of hops along a payment path terminating with a channel to the recipient. @@ -671,16 +690,17 @@ fn default_node_features() -> NodeFeatures { /// [`ChannelManager::list_usable_channels`]: crate::ln::channelmanager::ChannelManager::list_usable_channels /// [`Event::PaymentPathFailed`]: crate::util::events::Event::PaymentPathFailed /// [`NetworkGraph`]: crate::routing::gossip::NetworkGraph -pub fn find_route( +pub fn find_route( our_node_pubkey: &PublicKey, route_params: &RouteParameters, - network_graph: &ReadOnlyNetworkGraph, first_hops: Option<&[&ChannelDetails]>, logger: L, + network_graph: &NetworkGraph, first_hops: Option<&[&ChannelDetails]>, logger: L, scorer: &S, random_seed_bytes: &[u8; 32] ) -> Result -where L::Target: Logger { - let mut route = get_route(our_node_pubkey, &route_params.payment_params, network_graph, first_hops, +where L::Target: Logger, GL::Target: Logger { + let graph_lock = network_graph.read_only(); + let mut route = get_route(our_node_pubkey, &route_params.payment_params, &graph_lock, first_hops, route_params.final_value_msat, route_params.final_cltv_expiry_delta, logger, scorer, random_seed_bytes)?; - add_random_cltv_offset(&mut route, &route_params.payment_params, network_graph, random_seed_bytes); + add_random_cltv_offset(&mut route, &route_params.payment_params, &graph_lock, random_seed_bytes); Ok(route) } @@ -780,16 +800,23 @@ where L::Target: Logger { let network_channels = network_graph.channels(); let network_nodes = network_graph.nodes(); + if payment_params.max_path_count == 0 { + return Err(LightningError{err: "Can't find a route with no paths allowed.".to_owned(), action: ErrorAction::IgnoreError}); + } + // Allow MPP only if we have a features set from somewhere that indicates the payee supports // it. If the payee supports it they're supposed to include it in the invoice, so that should // work reliably. - let allow_mpp = if let Some(features) = &payment_params.features { + let allow_mpp = if payment_params.max_path_count == 1 { + false + } else if let Some(features) = &payment_params.features { features.supports_basic_mpp() } else if let Some(node) = network_nodes.get(&payee_node_id) { if let Some(node_info) = node.announcement_info.as_ref() { node_info.features.supports_basic_mpp() } else { false } } else { false }; + log_trace!(logger, "Searching for a route from payer {} to payee {} {} MPP and {} first hops {}overriding the network graph", our_node_pubkey, payment_params.payee_pubkey, if allow_mpp { "with" } else { "without" }, first_hops.map(|hops| hops.len()).unwrap_or(0), if first_hops.is_some() { "" } else { "not " }); @@ -840,6 +867,21 @@ where L::Target: Logger { let recommended_value_msat = final_value_msat * ROUTE_CAPACITY_PROVISION_FACTOR as u64; let mut path_value_msat = final_value_msat; + // Routing Fragmentation Mitigation heuristic: + // + // Routing fragmentation across many payment paths increases the overall routing + // fees as you have irreducible routing fees per-link used (`fee_base_msat`). + // Taking too many smaller paths also increases the chance of payment failure. + // Thus to avoid this effect, we require from our collected links to provide + // at least a minimal contribution to the recommended value yet-to-be-fulfilled. + // This requirement is currently set to be 1/max_path_count of the payment + // value to ensure we only ever return routes that do not violate this limit. + let minimal_value_contribution_msat: u64 = if allow_mpp { + (final_value_msat + (payment_params.max_path_count as u64 - 1)) / payment_params.max_path_count as u64 + } else { + final_value_msat + }; + // Keep track of how much liquidity has been used in selected channels. Used to determine // if the channel can be used by additional MPP paths or to inform path finding decisions. It is // aware of direction *only* to ensure that the correct htlc_maximum_msat value is used. Hence, @@ -847,11 +889,8 @@ where L::Target: Logger { let mut used_channel_liquidities: HashMap<(u64, bool), u64> = HashMap::with_capacity(network_nodes.len()); - // Keeping track of how much value we already collected across other paths. Helps to decide: - // - how much a new path should be transferring (upper bound); - // - whether a channel should be disregarded because - // it's available liquidity is too small comparing to how much more we need to collect; - // - when we want to stop looking for new paths. + // Keeping track of how much value we already collected across other paths. Helps to decide + // when we want to stop looking for new paths. let mut already_collected_value_msat = 0; for (_, channels) in first_hop_targets.iter_mut() { @@ -913,26 +952,6 @@ where L::Target: Logger { *used_liquidity_msat }); - // Routing Fragmentation Mitigation heuristic: - // - // Routing fragmentation across many payment paths increases the overall routing - // fees as you have irreducible routing fees per-link used (`fee_base_msat`). - // Taking too many smaller paths also increases the chance of payment failure. - // Thus to avoid this effect, we require from our collected links to provide - // at least a minimal contribution to the recommended value yet-to-be-fulfilled. - // - // This requirement is currently 5% of the remaining-to-be-collected value. - // This means as we successfully advance in our collection, - // the absolute liquidity contribution is lowered, - // thus increasing the number of potential channels to be selected. - - // Derive the minimal liquidity contribution with a ratio of 20 (5%, rounded up) - // or 100% if we're not allowed to do multipath payments. - let minimal_value_contribution_msat: u64 = if allow_mpp { - (recommended_value_msat - already_collected_value_msat + 19) / 20 - } else { - final_value_msat - }; // Verify the liquidity offered by this channel complies to the minimal contribution. let contributes_sufficient_value = available_value_contribution_msat >= minimal_value_contribution_msat; // Do not consider candidate hops that would exceed the maximum path length. @@ -1504,10 +1523,8 @@ where L::Target: Logger { *used_channel_liquidities.entry((victim_scid, true)).or_default() = exhausted; } - // Track the total amount all our collected paths allow to send so that we: - // - know when to stop looking for more paths - // - know which of the hops are useless considering how much more sats we need - // (contributes_sufficient_value) + // Track the total amount all our collected paths allow to send so that we know + // when to stop looking for more paths already_collected_value_msat += value_contribution_msat; payment_paths.push(payment_path); @@ -1678,6 +1695,8 @@ where L::Target: Logger { }); selected_paths.push(path); } + // Make sure we would never create a route with more paths than we allow. + debug_assert!(selected_paths.len() <= payment_params.max_path_count.into()); if let Some(features) = &payment_params.features { for path in selected_paths.iter_mut() { @@ -1787,15 +1806,16 @@ fn add_random_cltv_offset(route: &mut Route, payment_params: &PaymentParameters, /// exclude the payer, but include the payee). This may be useful, e.g., for probing the chosen path. /// /// Re-uses logic from `find_route`, so the restrictions described there also apply here. -pub fn build_route_from_hops( +pub fn build_route_from_hops( our_node_pubkey: &PublicKey, hops: &[PublicKey], route_params: &RouteParameters, - network_graph: &ReadOnlyNetworkGraph, logger: L, random_seed_bytes: &[u8; 32] + network_graph: &NetworkGraph, logger: L, random_seed_bytes: &[u8; 32] ) -> Result -where L::Target: Logger { +where L::Target: Logger, GL::Target: Logger { + let graph_lock = network_graph.read_only(); let mut route = build_route_from_hops_internal( - our_node_pubkey, hops, &route_params.payment_params, &network_graph, + our_node_pubkey, hops, &route_params.payment_params, &graph_lock, route_params.final_value_msat, route_params.final_cltv_expiry_delta, logger, random_seed_bytes)?; - add_random_cltv_offset(&mut route, &route_params.payment_params, &network_graph, random_seed_bytes); + add_random_cltv_offset(&mut route, &route_params.payment_params, &graph_lock, random_seed_bytes); Ok(route) } @@ -1831,6 +1851,10 @@ fn build_route_from_hops_internal( fn payment_path_failed(&mut self, _path: &[&RouteHop], _short_channel_id: u64) {} fn payment_path_successful(&mut self, _path: &[&RouteHop]) {} + + fn probe_failed(&mut self, _path: &[&RouteHop], _short_channel_id: u64) {} + + fn probe_successful(&mut self, _path: &[&RouteHop]) {} } impl<'a> Writeable for HopScorer { @@ -1858,11 +1882,11 @@ fn build_route_from_hops_internal( #[cfg(test)] mod tests { - use routing::gossip::{NetworkGraph, P2PGossipSync, NodeId}; + use routing::gossip::{NetworkGraph, P2PGossipSync, NodeId, EffectiveCapacity}; use routing::router::{get_route, build_route_from_hops_internal, add_random_cltv_offset, default_node_features, PaymentParameters, Route, RouteHint, RouteHintHop, RouteHop, RoutingFees, DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA, MAX_PATH_LENGTH_ESTIMATE}; - use routing::scoring::{ChannelUsage, Score}; + use routing::scoring::{ChannelUsage, Score, ProbabilisticScorer, ProbabilisticScoringParameters}; use chain::transaction::OutPoint; use chain::keysinterface::KeysInterface; use ln::features::{ChannelFeatures, InitFeatures, InvoiceFeatures, NodeFeatures}; @@ -1921,6 +1945,7 @@ mod tests { is_usable: true, is_public: true, inbound_htlc_minimum_msat: None, inbound_htlc_maximum_msat: None, + config: None, } } @@ -3999,7 +4024,8 @@ mod tests { let scorer = test_utils::TestScorer::with_penalty(0); let keys_manager = test_utils::TestKeysInterface::new(&[0u8; 32], Network::Testnet); let random_seed_bytes = keys_manager.get_secure_random_bytes(); - let payment_params = PaymentParameters::from_node_id(nodes[2]).with_features(InvoiceFeatures::known()); + let payment_params = PaymentParameters::from_node_id(nodes[2]) + .with_features(InvoiceFeatures::known()); // We need a route consisting of 3 paths: // From our node to node2 via node0, node7, node1 (three paths one hop each). @@ -4092,15 +4118,39 @@ mod tests { { // Attempt to route more than available results in a failure. if let Err(LightningError{err, action: ErrorAction::IgnoreError}) = get_route( - &our_id, &payment_params, &network_graph.read_only(), None, 300_000, 42, Arc::clone(&logger), &scorer, &random_seed_bytes) { - assert_eq!(err, "Failed to find a sufficient route to the given destination"); + &our_id, &payment_params, &network_graph.read_only(), None, 300_000, 42, + Arc::clone(&logger), &scorer, &random_seed_bytes) { + assert_eq!(err, "Failed to find a sufficient route to the given destination"); + } else { panic!(); } + } + + { + // Attempt to route while setting max_path_count to 0 results in a failure. + let zero_payment_params = payment_params.clone().with_max_path_count(0); + if let Err(LightningError{err, action: ErrorAction::IgnoreError}) = get_route( + &our_id, &zero_payment_params, &network_graph.read_only(), None, 100, 42, + Arc::clone(&logger), &scorer, &random_seed_bytes) { + assert_eq!(err, "Can't find a route with no paths allowed."); + } else { panic!(); } + } + + { + // Attempt to route while setting max_path_count to 3 results in a failure. + // This is the case because the minimal_value_contribution_msat would require each path + // to account for 1/3 of the total value, which is violated by 2 out of 3 paths. + let fail_payment_params = payment_params.clone().with_max_path_count(3); + if let Err(LightningError{err, action: ErrorAction::IgnoreError}) = get_route( + &our_id, &fail_payment_params, &network_graph.read_only(), None, 250_000, 42, + Arc::clone(&logger), &scorer, &random_seed_bytes) { + assert_eq!(err, "Failed to find a sufficient route to the given destination"); } else { panic!(); } } { // Now, attempt to route 250 sats (just a bit below the capacity). // Our algorithm should provide us with these 3 paths. - let route = get_route(&our_id, &payment_params, &network_graph.read_only(), None, 250_000, 42, Arc::clone(&logger), &scorer, &random_seed_bytes).unwrap(); + let route = get_route(&our_id, &payment_params, &network_graph.read_only(), None, + 250_000, 42, Arc::clone(&logger), &scorer, &random_seed_bytes).unwrap(); assert_eq!(route.paths.len(), 3); let mut total_amount_paid_msat = 0; for path in &route.paths { @@ -4113,7 +4163,8 @@ mod tests { { // Attempt to route an exact amount is also fine - let route = get_route(&our_id, &payment_params, &network_graph.read_only(), None, 290_000, 42, Arc::clone(&logger), &scorer, &random_seed_bytes).unwrap(); + let route = get_route(&our_id, &payment_params, &network_graph.read_only(), None, + 290_000, 42, Arc::clone(&logger), &scorer, &random_seed_bytes).unwrap(); assert_eq!(route.paths.len(), 3); let mut total_amount_paid_msat = 0; for path in &route.paths { @@ -5269,6 +5320,8 @@ mod tests { fn payment_path_failed(&mut self, _path: &[&RouteHop], _short_channel_id: u64) {} fn payment_path_successful(&mut self, _path: &[&RouteHop]) {} + fn probe_failed(&mut self, _path: &[&RouteHop], _short_channel_id: u64) {} + fn probe_successful(&mut self, _path: &[&RouteHop]) {} } struct BadNodeScorer { @@ -5287,6 +5340,8 @@ mod tests { fn payment_path_failed(&mut self, _path: &[&RouteHop], _short_channel_id: u64) {} fn payment_path_successful(&mut self, _path: &[&RouteHop]) {} + fn probe_failed(&mut self, _path: &[&RouteHop], _short_channel_id: u64) {} + fn probe_successful(&mut self, _path: &[&RouteHop]) {} } #[test] @@ -5670,6 +5725,43 @@ mod tests { } } } + + #[test] + fn honors_manual_penalties() { + let (secp_ctx, network_graph, _, _, logger) = build_line_graph(); + let (_, our_id, _, nodes) = get_nodes(&secp_ctx); + + let keys_manager = test_utils::TestKeysInterface::new(&[0u8; 32], Network::Testnet); + let random_seed_bytes = keys_manager.get_secure_random_bytes(); + + let scorer_params = ProbabilisticScoringParameters::default(); + let mut scorer = ProbabilisticScorer::new(scorer_params, Arc::clone(&network_graph), Arc::clone(&logger)); + + // First check set manual penalties are returned by the scorer. + let usage = ChannelUsage { + amount_msat: 0, + inflight_htlc_msat: 0, + effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_024_000, htlc_maximum_msat: Some(1_000) }, + }; + scorer.set_manual_penalty(&NodeId::from_pubkey(&nodes[3]), 123); + scorer.set_manual_penalty(&NodeId::from_pubkey(&nodes[4]), 456); + assert_eq!(scorer.channel_penalty_msat(42, &NodeId::from_pubkey(&nodes[3]), &NodeId::from_pubkey(&nodes[4]), usage), 456); + + // Then check we can get a normal route + let payment_params = PaymentParameters::from_node_id(nodes[10]); + let route = get_route(&our_id, &payment_params, &network_graph.read_only(), None, 100, 42, Arc::clone(&logger), &scorer, &random_seed_bytes); + assert!(route.is_ok()); + + // Then check that we can't get a route if we ban an intermediate node. + scorer.add_banned(&NodeId::from_pubkey(&nodes[3])); + let route = get_route(&our_id, &payment_params, &network_graph.read_only(), None, 100, 42, Arc::clone(&logger), &scorer, &random_seed_bytes); + assert!(route.is_err()); + + // Finally make sure we can route again, when we remove the ban. + scorer.remove_banned(&NodeId::from_pubkey(&nodes[3])); + let route = get_route(&our_id, &payment_params, &network_graph.read_only(), None, 100, 42, Arc::clone(&logger), &scorer, &random_seed_bytes); + assert!(route.is_ok()); + } } #[cfg(all(test, not(feature = "no-std")))] @@ -5764,6 +5856,7 @@ mod benches { is_public: true, inbound_htlc_minimum_msat: None, inbound_htlc_maximum_msat: None, + config: None, } }