X-Git-Url: http://git.bitcoin.ninja/index.cgi?a=blobdiff_plain;f=lightning%2Fsrc%2Frouting%2Frouter.rs;h=faf414cc10651fe63fb9a00887554136ccea699e;hb=c86cacd29d72c0af62cfd29d78dcf17a6d8f8c71;hp=47b6bcd2bb106eab9cf6ead63496b48213f51f74;hpb=ea38b938bb824e3663137785eaf0b8cfe64c3275;p=rust-lightning diff --git a/lightning/src/routing/router.rs b/lightning/src/routing/router.rs index 47b6bcd2..faf414cc 100644 --- a/lightning/src/routing/router.rs +++ b/lightning/src/routing/router.rs @@ -112,12 +112,12 @@ pub trait Router { /// [`find_route`]. /// /// [`ScoreLookUp`]: crate::routing::scoring::ScoreLookUp -pub struct ScorerAccountingForInFlightHtlcs<'a, SP: Sized, Sc: 'a + ScoreLookUp, S: Deref> { +pub struct ScorerAccountingForInFlightHtlcs<'a, S: Deref> where S::Target: ScoreLookUp { scorer: S, // Maps a channel's short channel id and its direction to the liquidity used up. inflight_htlcs: &'a InFlightHtlcs, } -impl<'a, SP: Sized, Sc: ScoreLookUp, S: Deref> ScorerAccountingForInFlightHtlcs<'a, SP, Sc, S> { +impl<'a, S: Deref> ScorerAccountingForInFlightHtlcs<'a, S> where S::Target: ScoreLookUp { /// Initialize a new `ScorerAccountingForInFlightHtlcs`. pub fn new(scorer: S, inflight_htlcs: &'a InFlightHtlcs) -> Self { ScorerAccountingForInFlightHtlcs { @@ -127,19 +127,14 @@ impl<'a, SP: Sized, Sc: ScoreLookUp, S: Deref> Sc } } -#[cfg(c_bindings)] -impl<'a, SP: Sized, Sc: ScoreLookUp, S: Deref> Writeable for ScorerAccountingForInFlightHtlcs<'a, SP, Sc, S> { - fn write(&self, writer: &mut W) -> Result<(), io::Error> { self.scorer.write(writer) } -} - -impl<'a, SP: Sized, Sc: 'a + ScoreLookUp, S: Deref> ScoreLookUp for ScorerAccountingForInFlightHtlcs<'a, SP, Sc, S> { - type ScoreParams = Sc::ScoreParams; +impl<'a, S: Deref> ScoreLookUp for ScorerAccountingForInFlightHtlcs<'a, S> where S::Target: ScoreLookUp { + type ScoreParams = ::ScoreParams; fn channel_penalty_msat(&self, short_channel_id: u64, source: &NodeId, target: &NodeId, usage: ChannelUsage, score_params: &Self::ScoreParams) -> u64 { if let Some(used_liquidity) = self.inflight_htlcs.used_liquidity_msat( source, target, short_channel_id ) { let usage = ChannelUsage { - inflight_htlc_msat: usage.inflight_htlc_msat + used_liquidity, + inflight_htlc_msat: usage.inflight_htlc_msat.saturating_add(used_liquidity), ..usage }; @@ -1265,7 +1260,11 @@ impl<'a> PaymentPath<'a> { // Note that this function is not aware of the available_liquidity limit, and thus does not // support increasing the value being transferred beyond what was selected during the initial // routing passes. - fn update_value_and_recompute_fees(&mut self, value_msat: u64) { + // + // Returns the amount that this path contributes to the total payment value, which may be greater + // than `value_msat` if we had to overpay to meet the final node's `htlc_minimum_msat`. + fn update_value_and_recompute_fees(&mut self, value_msat: u64) -> u64 { + let mut extra_contribution_msat = 0; let mut total_fee_paid_msat = 0 as u64; for i in (0..self.hops.len()).rev() { let last_hop = i == self.hops.len() - 1; @@ -1280,6 +1279,7 @@ impl<'a> PaymentPath<'a> { let cur_hop = &mut self.hops.get_mut(i).unwrap().0; cur_hop.next_hops_fee_msat = total_fee_paid_msat; + cur_hop.path_penalty_msat += extra_contribution_msat; // Overpay in fees if we can't save these funds due to htlc_minimum_msat. // We try to account for htlc_minimum_msat in scoring (add_entry!), so that nodes don't // set it too high just to maliciously take more fees by exploiting this @@ -1295,8 +1295,15 @@ impl<'a> PaymentPath<'a> { // Also, this can't be exploited more heavily than *announce a free path and fail // all payments*. cur_hop_transferred_amount_msat += extra_fees_msat; - total_fee_paid_msat += extra_fees_msat; - cur_hop_fees_msat += extra_fees_msat; + + // We remember and return the extra fees on the final hop to allow accounting for + // them in the path's value contribution. + if last_hop { + extra_contribution_msat = extra_fees_msat; + } else { + total_fee_paid_msat += extra_fees_msat; + cur_hop_fees_msat += extra_fees_msat; + } } if last_hop { @@ -1324,6 +1331,7 @@ impl<'a> PaymentPath<'a> { } } } + value_msat + extra_contribution_msat } } @@ -1609,9 +1617,13 @@ where L::Target: Logger { |info| info.features.supports_basic_mpp())) } else { false }; - log_trace!(logger, "Searching for a route from payer {} to {} {} MPP and {} first hops {}overriding the network graph", our_node_pubkey, - LoggedPayeePubkey(payment_params.payee.node_id()), if allow_mpp { "with" } else { "without" }, - first_hops.map(|hops| hops.len()).unwrap_or(0), if first_hops.is_some() { "" } else { "not " }); + let max_total_routing_fee_msat = route_params.max_total_routing_fee_msat.unwrap_or(u64::max_value()); + + log_trace!(logger, "Searching for a route from payer {} to {} {} MPP and {} first hops {}overriding the network graph with a fee limit of {} msat", + our_node_pubkey, LoggedPayeePubkey(payment_params.payee.node_id()), + if allow_mpp { "with" } else { "without" }, + first_hops.map(|hops| hops.len()).unwrap_or(0), if first_hops.is_some() { "" } else { "not " }, + max_total_routing_fee_msat); // Step (1). // Prepare the data we'll use for payee-to-payer search by @@ -1700,11 +1712,13 @@ where L::Target: Logger { LoggedPayeePubkey(payment_params.payee.node_id()), our_node_pubkey, final_value_msat); // Remember how many candidates we ignored to allow for some logging afterwards. - let mut num_ignored_value_contribution = 0; - let mut num_ignored_path_length_limit = 0; - let mut num_ignored_cltv_delta_limit = 0; - let mut num_ignored_previously_failed = 0; - let mut num_ignored_total_fee_limit = 0; + let mut num_ignored_value_contribution: u32 = 0; + let mut num_ignored_path_length_limit: u32 = 0; + let mut num_ignored_cltv_delta_limit: u32 = 0; + let mut num_ignored_previously_failed: u32 = 0; + let mut num_ignored_total_fee_limit: u32 = 0; + let mut num_ignored_avoid_overpayment: u32 = 0; + let mut num_ignored_htlc_minimum_msat_limit: u32 = 0; macro_rules! add_entry { // Adds entry which goes from $src_node_id to $dest_node_id over the $candidate hop. @@ -1773,9 +1787,9 @@ where L::Target: Logger { #[allow(unused_comparisons)] // $next_hops_path_htlc_minimum_msat is 0 in some calls so rustc complains let may_overpay_to_meet_path_minimum_msat = ((amount_to_transfer_over_msat < $candidate.htlc_minimum_msat() && - recommended_value_msat > $candidate.htlc_minimum_msat()) || + recommended_value_msat >= $candidate.htlc_minimum_msat()) || (amount_to_transfer_over_msat < $next_hops_path_htlc_minimum_msat && - recommended_value_msat > $next_hops_path_htlc_minimum_msat)); + recommended_value_msat >= $next_hops_path_htlc_minimum_msat)); let payment_failed_on_this_channel = scid_opt.map_or(false, |scid| payment_params.previously_failed_channels.contains(&scid)); @@ -1813,6 +1827,12 @@ where L::Target: Logger { } num_ignored_previously_failed += 1; } else if may_overpay_to_meet_path_minimum_msat { + if should_log_candidate { + log_trace!(logger, + "Ignoring {} to avoid overpaying to meet htlc_minimum_msat limit.", + LoggedCandidateHop(&$candidate)); + } + num_ignored_avoid_overpayment += 1; hit_minimum_limit = true; } else if over_path_minimum_msat { // Note that low contribution here (limited by available_liquidity_msat) @@ -1869,7 +1889,6 @@ where L::Target: Logger { } // Ignore hops if augmenting the current path to them would put us over `max_total_routing_fee_msat` - let max_total_routing_fee_msat = route_params.max_total_routing_fee_msat.unwrap_or(u64::max_value()); if total_fee_msat > max_total_routing_fee_msat { if should_log_candidate { log_trace!(logger, "Ignoring {} due to exceeding max total routing fee limit.", LoggedCandidateHop(&$candidate)); @@ -1963,6 +1982,13 @@ where L::Target: Logger { } } } + } else { + if should_log_candidate { + log_trace!(logger, + "Ignoring {} due to its htlc_minimum_msat limit.", + LoggedCandidateHop(&$candidate)); + } + num_ignored_htlc_minimum_msat_limit += 1; } } } @@ -2114,14 +2140,15 @@ where L::Target: Logger { for route in payment_params.payee.unblinded_route_hints().iter() .filter(|route| !route.0.is_empty()) { - let first_hop_in_route = &(route.0)[0]; - let have_hop_src_in_graph = - // Only add the hops in this route to our candidate set if either - // we have a direct channel to the first hop or the first hop is - // in the regular network graph. - first_hop_targets.get(&NodeId::from_pubkey(&first_hop_in_route.src_node_id)).is_some() || - network_nodes.get(&NodeId::from_pubkey(&first_hop_in_route.src_node_id)).is_some(); - if have_hop_src_in_graph { + let first_hop_src_id = NodeId::from_pubkey(&route.0.first().unwrap().src_node_id); + let first_hop_src_is_reachable = + // Only add the hops in this route to our candidate set if either we are part of + // the first hop, we have a direct channel to the first hop, or the first hop is in + // the regular network graph. + our_node_id == first_hop_src_id || + first_hop_targets.get(&first_hop_src_id).is_some() || + network_nodes.get(&first_hop_src_id).is_some(); + if first_hop_src_is_reachable { // We start building the path from reverse, i.e., from payee // to the first RouteHintHop in the path. let hop_iter = route.0.iter().rev(); @@ -2138,6 +2165,15 @@ where L::Target: Logger { for (idx, (hop, prev_hop_id)) in hop_iter.zip(prev_hop_iter).enumerate() { let source = NodeId::from_pubkey(&hop.src_node_id); let target = NodeId::from_pubkey(&prev_hop_id); + + if let Some(first_channels) = first_hop_targets.get(&target) { + if first_channels.iter().any(|d| d.outbound_scid_alias == Some(hop.short_channel_id)) { + log_trace!(logger, "Ignoring route hint with SCID {} (and any previous) due to it being a direct channel of ours.", + hop.short_channel_id); + break; + } + } + let candidate = network_channels .get(&hop.short_channel_id) .and_then(|channel| channel.as_directed_to(&target)) @@ -2181,12 +2217,12 @@ where L::Target: Logger { .saturating_add(1); // Searching for a direct channel between last checked hop and first_hop_targets - if let Some(first_channels) = first_hop_targets.get_mut(&NodeId::from_pubkey(&prev_hop_id)) { + if let Some(first_channels) = first_hop_targets.get_mut(&target) { sort_first_hop_channels(first_channels, &used_liquidities, recommended_value_msat, our_node_pubkey); for details in first_channels { let first_hop_candidate = CandidateRouteHop::FirstHop { details }; - add_entry!(first_hop_candidate, our_node_id, NodeId::from_pubkey(&prev_hop_id), + add_entry!(first_hop_candidate, our_node_id, target, aggregate_next_hops_fee_msat, aggregate_path_contribution_msat, aggregate_next_hops_path_htlc_minimum_msat, aggregate_next_hops_path_penalty_msat, aggregate_next_hops_cltv_delta, aggregate_next_hops_path_length); @@ -2330,8 +2366,8 @@ where L::Target: Logger { // recompute the fees again, so that if that's the case, we match the currently // underpaid htlc_minimum_msat with fees. debug_assert_eq!(payment_path.get_value_msat(), value_contribution_msat); - value_contribution_msat = cmp::min(value_contribution_msat, final_value_msat); - payment_path.update_value_and_recompute_fees(value_contribution_msat); + let desired_value_contribution = cmp::min(value_contribution_msat, final_value_msat); + value_contribution_msat = payment_path.update_value_and_recompute_fees(desired_value_contribution); // Since a path allows to transfer as much value as // the smallest channel it has ("bottleneck"), we should recompute @@ -2416,6 +2452,9 @@ where L::Target: Logger { // because we deterministically terminated the search due to low liquidity. if !found_new_path && channel_saturation_pow_half != 0 { channel_saturation_pow_half = 0; + } else if !found_new_path && hit_minimum_limit && already_collected_value_msat < final_value_msat && path_value_msat != recommended_value_msat { + log_trace!(logger, "Failed to collect enough value, but running again to collect extra paths with a potentially higher limit."); + path_value_msat = recommended_value_msat; } else if already_collected_value_msat >= recommended_value_msat || !found_new_path { log_trace!(logger, "Have now collected {} msat (seeking {} msat) in paths. Last path loop {} a new path.", already_collected_value_msat, recommended_value_msat, if found_new_path { "found" } else { "did not find" }); @@ -2430,15 +2469,22 @@ where L::Target: Logger { log_trace!(logger, "Collected exactly our payment amount on the first pass, without hitting an htlc_minimum_msat limit, exiting."); break 'paths_collection; } - log_trace!(logger, "Collected our payment amount on the first pass, but running again to collect extra paths with a potentially higher limit."); + log_trace!(logger, "Collected our payment amount on the first pass, but running again to collect extra paths with a potentially higher value to meet htlc_minimum_msat limit."); path_value_msat = recommended_value_msat; } } let num_ignored_total = num_ignored_value_contribution + num_ignored_path_length_limit + - num_ignored_cltv_delta_limit + num_ignored_previously_failed + num_ignored_total_fee_limit; + num_ignored_cltv_delta_limit + num_ignored_previously_failed + + num_ignored_avoid_overpayment + num_ignored_htlc_minimum_msat_limit + + num_ignored_total_fee_limit; if num_ignored_total > 0 { - log_trace!(logger, "Ignored {} candidate hops due to insufficient value contribution, {} due to path length limit, {} due to CLTV delta limit, {} due to previous payment failure, {} due to maximum total fee limit. Total: {} ignored candidates.", num_ignored_value_contribution, num_ignored_path_length_limit, num_ignored_cltv_delta_limit, num_ignored_previously_failed, num_ignored_total_fee_limit, num_ignored_total); + log_trace!(logger, + "Ignored {} candidate hops due to insufficient value contribution, {} due to path length limit, {} due to CLTV delta limit, {} due to previous payment failure, {} due to htlc_minimum_msat limit, {} to avoid overpaying, {} due to maximum total fee limit. Total: {} ignored candidates.", + num_ignored_value_contribution, num_ignored_path_length_limit, + num_ignored_cltv_delta_limit, num_ignored_previously_failed, + num_ignored_htlc_minimum_msat_limit, num_ignored_avoid_overpayment, + num_ignored_total_fee_limit, num_ignored_total); } // Step (5). @@ -2580,14 +2626,6 @@ where L::Target: Logger { // Make sure we would never create a route with more paths than we allow. debug_assert!(paths.len() <= payment_params.max_path_count.into()); - // Make sure we would never create a route whose total fees exceed max_total_routing_fee_msat. - if let Some(max_total_routing_fee_msat) = route_params.max_total_routing_fee_msat { - if paths.iter().map(|p| p.fee_msat()).sum::() > max_total_routing_fee_msat { - return Err(LightningError{err: format!("Failed to find route that adheres to the maximum total fee limit of {}msat", - max_total_routing_fee_msat), action: ErrorAction::IgnoreError}); - } - } - if let Some(node_features) = payment_params.payee.node_features() { for path in paths.iter_mut() { path.hops.last_mut().unwrap().node_features = node_features.clone(); @@ -2595,6 +2633,15 @@ where L::Target: Logger { } let route = Route { paths, route_params: Some(route_params.clone()) }; + + // Make sure we would never create a route whose total fees exceed max_total_routing_fee_msat. + if let Some(max_total_routing_fee_msat) = route_params.max_total_routing_fee_msat { + if route.get_total_fees() > max_total_routing_fee_msat { + return Err(LightningError{err: format!("Failed to find route that adheres to the maximum total fee limit of {}msat", + max_total_routing_fee_msat), action: ErrorAction::IgnoreError}); + } + } + log_info!(logger, "Got route: {}", log_route!(route)); Ok(route) } @@ -2818,6 +2865,7 @@ mod tests { inbound_scid_alias: None, channel_value_satoshis: 0, user_channel_id: 0, + balance_msat: 0, outbound_capacity_msat, next_outbound_htlc_limit_msat: outbound_capacity_msat, next_outbound_htlc_minimum_msat: 0, @@ -3178,6 +3226,67 @@ mod tests { assert_eq!(fees, 5_000); } + #[test] + fn htlc_minimum_recipient_overpay_test() { + let (secp_ctx, network_graph, gossip_sync, _, logger) = build_graph(); + let (_, our_id, privkeys, nodes) = get_nodes(&secp_ctx); + let config = UserConfig::default(); + let payment_params = PaymentParameters::from_node_id(nodes[2], 42).with_bolt11_features(channelmanager::provided_invoice_features(&config)).unwrap(); + let scorer = ln_test_utils::TestScorer::new(); + let keys_manager = ln_test_utils::TestKeysInterface::new(&[0u8; 32], Network::Testnet); + let random_seed_bytes = keys_manager.get_secure_random_bytes(); + + // Route to node2 over a single path which requires overpaying the recipient themselves. + + // First disable all paths except the us -> node1 -> node2 path + update_channel(&gossip_sync, &secp_ctx, &privkeys[2], UnsignedChannelUpdate { + chain_hash: genesis_block(Network::Testnet).header.block_hash(), + short_channel_id: 13, + timestamp: 2, + flags: 3, + cltv_expiry_delta: 0, + htlc_minimum_msat: 0, + htlc_maximum_msat: 0, + fee_base_msat: 0, + fee_proportional_millionths: 0, + excess_data: Vec::new() + }); + + // Set channel 4 to free but with a high htlc_minimum_msat + update_channel(&gossip_sync, &secp_ctx, &privkeys[1], UnsignedChannelUpdate { + chain_hash: genesis_block(Network::Testnet).header.block_hash(), + short_channel_id: 4, + timestamp: 2, + flags: 0, + cltv_expiry_delta: 0, + htlc_minimum_msat: 15_000, + htlc_maximum_msat: MAX_VALUE_MSAT, + fee_base_msat: 0, + fee_proportional_millionths: 0, + excess_data: Vec::new() + }); + + // Now check that we'll fail to find a path if we fail to find a path if the htlc_minimum + // is overrun. Note that the fees are actually calculated on 3*payment amount as that's + // what we try to find a route for, so this test only just happens to work out to exactly + // the fee limit. + let mut route_params = RouteParameters::from_payment_params_and_value( + payment_params.clone(), 5_000); + route_params.max_total_routing_fee_msat = Some(9_999); + if let Err(LightningError{err, action: ErrorAction::IgnoreError}) = get_route(&our_id, + &route_params, &network_graph.read_only(), None, Arc::clone(&logger), &scorer, + &Default::default(), &random_seed_bytes) { + assert_eq!(err, "Failed to find route that adheres to the maximum total fee limit of 9999msat"); + } else { panic!(); } + + let mut route_params = RouteParameters::from_payment_params_and_value( + payment_params.clone(), 5_000); + route_params.max_total_routing_fee_msat = Some(10_000); + let route = get_route(&our_id, &route_params, &network_graph.read_only(), None, + Arc::clone(&logger), &scorer, &Default::default(), &random_seed_bytes).unwrap(); + assert_eq!(route.get_total_fees(), 10_000); + } + #[test] fn disable_channels_test() { let (secp_ctx, network_graph, gossip_sync, _, logger) = build_graph(); @@ -7521,6 +7630,264 @@ mod tests { assert_eq!(err, "Failed to find a path to the given destination"); } else { panic!() } } + + #[test] + fn path_contribution_includes_min_htlc_overpay() { + // Previously, the fuzzer hit a debug panic because we wouldn't include the amount overpaid to + // meet a last hop's min_htlc in the total collected paths value. We now include this value and + // also penalize hops along the overpaying path to ensure that it gets deprioritized in path + // selection, both tested here. + let secp_ctx = Secp256k1::new(); + let logger = Arc::new(ln_test_utils::TestLogger::new()); + let network_graph = Arc::new(NetworkGraph::new(Network::Testnet, Arc::clone(&logger))); + let scorer = ProbabilisticScorer::new(ProbabilisticScoringDecayParameters::default(), network_graph.clone(), logger.clone()); + let keys_manager = ln_test_utils::TestKeysInterface::new(&[0u8; 32], Network::Testnet); + let random_seed_bytes = keys_manager.get_secure_random_bytes(); + let config = UserConfig::default(); + + // Values are taken from the fuzz input that uncovered this panic. + let amt_msat = 562_0000; + let (_, our_id, _, nodes) = get_nodes(&secp_ctx); + let first_hops = vec![ + get_channel_details( + Some(83), nodes[0], channelmanager::provided_init_features(&config), 2199_0000, + ), + ]; + + let htlc_mins = [49_0000, 1125_0000]; + let payment_params = { + let blinded_path = BlindedPath { + introduction_node_id: nodes[0], + blinding_point: ln_test_utils::pubkey(42), + blinded_hops: vec![ + BlindedHop { blinded_node_id: ln_test_utils::pubkey(42 as u8), encrypted_payload: Vec::new() }, + BlindedHop { blinded_node_id: ln_test_utils::pubkey(42 as u8), encrypted_payload: Vec::new() } + ], + }; + let mut blinded_hints = Vec::new(); + for htlc_min in htlc_mins.iter() { + blinded_hints.push((BlindedPayInfo { + fee_base_msat: 0, + fee_proportional_millionths: 0, + htlc_minimum_msat: *htlc_min, + htlc_maximum_msat: *htlc_min * 100, + cltv_expiry_delta: 10, + features: BlindedHopFeatures::empty(), + }, blinded_path.clone())); + } + let bolt12_features: Bolt12InvoiceFeatures = channelmanager::provided_invoice_features(&config).to_context(); + PaymentParameters::blinded(blinded_hints.clone()) + .with_bolt12_features(bolt12_features.clone()).unwrap() + }; + + let netgraph = network_graph.read_only(); + let route_params = RouteParameters::from_payment_params_and_value( + payment_params, amt_msat); + let route = get_route( + &our_id, &route_params, &netgraph, Some(&first_hops.iter().collect::>()), + Arc::clone(&logger), &scorer, &ProbabilisticScoringFeeParameters::default(), + &random_seed_bytes + ).unwrap(); + assert_eq!(route.paths.len(), 1); + assert_eq!(route.get_total_amount(), amt_msat); + } + + #[test] + fn first_hop_preferred_over_hint() { + // Check that if we have a first hop to a peer we'd always prefer that over a route hint + // they gave us, but we'd still consider all subsequent hints if they are more attractive. + let secp_ctx = Secp256k1::new(); + let logger = Arc::new(ln_test_utils::TestLogger::new()); + let network_graph = Arc::new(NetworkGraph::new(Network::Testnet, Arc::clone(&logger))); + let gossip_sync = P2PGossipSync::new(Arc::clone(&network_graph), None, Arc::clone(&logger)); + let scorer = ln_test_utils::TestScorer::new(); + let keys_manager = ln_test_utils::TestKeysInterface::new(&[0u8; 32], Network::Testnet); + let random_seed_bytes = keys_manager.get_secure_random_bytes(); + let config = UserConfig::default(); + + let amt_msat = 1_000_000; + let (our_privkey, our_node_id, privkeys, nodes) = get_nodes(&secp_ctx); + + add_channel(&gossip_sync, &secp_ctx, &our_privkey, &privkeys[0], + ChannelFeatures::from_le_bytes(id_to_feature_flags(1)), 1); + update_channel(&gossip_sync, &secp_ctx, &our_privkey, UnsignedChannelUpdate { + chain_hash: genesis_block(Network::Testnet).header.block_hash(), + short_channel_id: 1, + timestamp: 1, + flags: 0, + cltv_expiry_delta: 42, + htlc_minimum_msat: 1_000, + htlc_maximum_msat: 10_000_000, + fee_base_msat: 800, + fee_proportional_millionths: 0, + excess_data: Vec::new() + }); + update_channel(&gossip_sync, &secp_ctx, &privkeys[0], UnsignedChannelUpdate { + chain_hash: genesis_block(Network::Testnet).header.block_hash(), + short_channel_id: 1, + timestamp: 1, + flags: 1, + cltv_expiry_delta: 42, + htlc_minimum_msat: 1_000, + htlc_maximum_msat: 10_000_000, + fee_base_msat: 800, + fee_proportional_millionths: 0, + excess_data: Vec::new() + }); + + add_channel(&gossip_sync, &secp_ctx, &privkeys[0], &privkeys[1], + ChannelFeatures::from_le_bytes(id_to_feature_flags(1)), 2); + update_channel(&gossip_sync, &secp_ctx, &privkeys[0], UnsignedChannelUpdate { + chain_hash: genesis_block(Network::Testnet).header.block_hash(), + short_channel_id: 2, + timestamp: 2, + flags: 0, + cltv_expiry_delta: 42, + htlc_minimum_msat: 1_000, + htlc_maximum_msat: 10_000_000, + fee_base_msat: 800, + fee_proportional_millionths: 0, + excess_data: Vec::new() + }); + update_channel(&gossip_sync, &secp_ctx, &privkeys[1], UnsignedChannelUpdate { + chain_hash: genesis_block(Network::Testnet).header.block_hash(), + short_channel_id: 2, + timestamp: 2, + flags: 1, + cltv_expiry_delta: 42, + htlc_minimum_msat: 1_000, + htlc_maximum_msat: 10_000_000, + fee_base_msat: 800, + fee_proportional_millionths: 0, + excess_data: Vec::new() + }); + + let dest_node_id = nodes[2]; + + let route_hint = RouteHint(vec![RouteHintHop { + src_node_id: our_node_id, + short_channel_id: 44, + fees: RoutingFees { + base_msat: 234, + proportional_millionths: 0, + }, + cltv_expiry_delta: 10, + htlc_minimum_msat: None, + htlc_maximum_msat: Some(5_000_000), + }, + RouteHintHop { + src_node_id: nodes[0], + short_channel_id: 45, + fees: RoutingFees { + base_msat: 123, + proportional_millionths: 0, + }, + cltv_expiry_delta: 10, + htlc_minimum_msat: None, + htlc_maximum_msat: None, + }]); + + let payment_params = PaymentParameters::from_node_id(dest_node_id, 42) + .with_route_hints(vec![route_hint]).unwrap() + .with_bolt11_features(channelmanager::provided_invoice_features(&config)).unwrap(); + let route_params = RouteParameters::from_payment_params_and_value( + payment_params, amt_msat); + + // First create an insufficient first hop for channel with SCID 1 and check we'd use the + // route hint. + let first_hop = get_channel_details(Some(1), nodes[0], + channelmanager::provided_init_features(&config), 999_999); + let first_hops = vec![first_hop]; + + let route = get_route(&our_node_id, &route_params.clone(), &network_graph.read_only(), + Some(&first_hops.iter().collect::>()), Arc::clone(&logger), &scorer, + &Default::default(), &random_seed_bytes).unwrap(); + assert_eq!(route.paths.len(), 1); + assert_eq!(route.get_total_amount(), amt_msat); + assert_eq!(route.paths[0].hops.len(), 2); + assert_eq!(route.paths[0].hops[0].short_channel_id, 44); + assert_eq!(route.paths[0].hops[1].short_channel_id, 45); + assert_eq!(route.get_total_fees(), 123); + + // Now check we would trust our first hop info, i.e., fail if we detect the route hint is + // for a first hop channel. + let mut first_hop = get_channel_details(Some(1), nodes[0], channelmanager::provided_init_features(&config), 999_999); + first_hop.outbound_scid_alias = Some(44); + let first_hops = vec![first_hop]; + + let route_res = get_route(&our_node_id, &route_params.clone(), &network_graph.read_only(), + Some(&first_hops.iter().collect::>()), Arc::clone(&logger), &scorer, + &Default::default(), &random_seed_bytes); + assert!(route_res.is_err()); + + // Finally check we'd use the first hop if has sufficient outbound capacity. But we'd stil + // use the cheaper second hop of the route hint. + let mut first_hop = get_channel_details(Some(1), nodes[0], + channelmanager::provided_init_features(&config), 10_000_000); + first_hop.outbound_scid_alias = Some(44); + let first_hops = vec![first_hop]; + + let route = get_route(&our_node_id, &route_params.clone(), &network_graph.read_only(), + Some(&first_hops.iter().collect::>()), Arc::clone(&logger), &scorer, + &Default::default(), &random_seed_bytes).unwrap(); + assert_eq!(route.paths.len(), 1); + assert_eq!(route.get_total_amount(), amt_msat); + assert_eq!(route.paths[0].hops.len(), 2); + assert_eq!(route.paths[0].hops[0].short_channel_id, 1); + assert_eq!(route.paths[0].hops[1].short_channel_id, 45); + assert_eq!(route.get_total_fees(), 123); + } + + #[test] + fn allow_us_being_first_hint() { + // Check that we consider a route hint even if we are the src of the first hop. + let secp_ctx = Secp256k1::new(); + let logger = Arc::new(ln_test_utils::TestLogger::new()); + let network_graph = Arc::new(NetworkGraph::new(Network::Testnet, Arc::clone(&logger))); + let scorer = ln_test_utils::TestScorer::new(); + let keys_manager = ln_test_utils::TestKeysInterface::new(&[0u8; 32], Network::Testnet); + let random_seed_bytes = keys_manager.get_secure_random_bytes(); + let config = UserConfig::default(); + + let (_, our_node_id, _, nodes) = get_nodes(&secp_ctx); + + let amt_msat = 1_000_000; + let dest_node_id = nodes[1]; + + let first_hop = get_channel_details(Some(1), nodes[0], channelmanager::provided_init_features(&config), 10_000_000); + let first_hops = vec![first_hop]; + + let route_hint = RouteHint(vec![RouteHintHop { + src_node_id: our_node_id, + short_channel_id: 44, + fees: RoutingFees { + base_msat: 123, + proportional_millionths: 0, + }, + cltv_expiry_delta: 10, + htlc_minimum_msat: None, + htlc_maximum_msat: None, + }]); + + let payment_params = PaymentParameters::from_node_id(dest_node_id, 42) + .with_route_hints(vec![route_hint]).unwrap() + .with_bolt11_features(channelmanager::provided_invoice_features(&config)).unwrap(); + + let route_params = RouteParameters::from_payment_params_and_value( + payment_params, amt_msat); + + + let route = get_route(&our_node_id, &route_params, &network_graph.read_only(), + Some(&first_hops.iter().collect::>()), Arc::clone(&logger), &scorer, + &Default::default(), &random_seed_bytes).unwrap(); + + assert_eq!(route.paths.len(), 1); + assert_eq!(route.get_total_amount(), amt_msat); + assert_eq!(route.get_total_fees(), 0); + assert_eq!(route.paths[0].hops.len(), 1); + + assert_eq!(route.paths[0].hops[0].short_channel_id, 44); + } } #[cfg(all(any(test, ldk_bench), not(feature = "no-std")))] @@ -7606,6 +7973,7 @@ pub(crate) mod bench_utils { outbound_scid_alias: None, channel_value_satoshis: 10_000_000_000, user_channel_id: 0, + balance_msat: 10_000_000_000, outbound_capacity_msat: 10_000_000_000, next_outbound_htlc_minimum_msat: 0, next_outbound_htlc_limit_msat: 10_000_000_000,