+ #[test]
+ fn adds_and_limits_cltv_offset() {
+ let (secp_ctx, network_graph, _, _, logger) = build_graph();
+ let (_, our_id, _, nodes) = get_nodes(&secp_ctx);
+
+ let scorer = ln_test_utils::TestScorer::new();
+
+ let payment_params = PaymentParameters::from_node_id(nodes[6], 42).with_route_hints(last_hops(&nodes));
+ let keys_manager = ln_test_utils::TestKeysInterface::new(&[0u8; 32], Network::Testnet);
+ let random_seed_bytes = keys_manager.get_secure_random_bytes();
+ let route = get_route(&our_id, &payment_params, &network_graph.read_only(), None, 100, 42, Arc::clone(&logger), &scorer, &random_seed_bytes).unwrap();
+ assert_eq!(route.paths.len(), 1);
+
+ let cltv_expiry_deltas_before = route.paths[0].iter().map(|h| h.cltv_expiry_delta).collect::<Vec<u32>>();
+
+ // Check whether the offset added to the last hop by default is in [1 .. DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA]
+ let mut route_default = route.clone();
+ add_random_cltv_offset(&mut route_default, &payment_params, &network_graph.read_only(), &random_seed_bytes);
+ let cltv_expiry_deltas_default = route_default.paths[0].iter().map(|h| h.cltv_expiry_delta).collect::<Vec<u32>>();
+ assert_eq!(cltv_expiry_deltas_before.split_last().unwrap().1, cltv_expiry_deltas_default.split_last().unwrap().1);
+ assert!(cltv_expiry_deltas_default.last() > cltv_expiry_deltas_before.last());
+ assert!(cltv_expiry_deltas_default.last().unwrap() <= &DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA);
+
+ // Check that no offset is added when we restrict the max_total_cltv_expiry_delta
+ let mut route_limited = route.clone();
+ let limited_max_total_cltv_expiry_delta = cltv_expiry_deltas_before.iter().sum();
+ let limited_payment_params = payment_params.with_max_total_cltv_expiry_delta(limited_max_total_cltv_expiry_delta);
+ add_random_cltv_offset(&mut route_limited, &limited_payment_params, &network_graph.read_only(), &random_seed_bytes);
+ let cltv_expiry_deltas_limited = route_limited.paths[0].iter().map(|h| h.cltv_expiry_delta).collect::<Vec<u32>>();
+ assert_eq!(cltv_expiry_deltas_before, cltv_expiry_deltas_limited);
+ }
+
+ #[test]
+ fn adds_plausible_cltv_offset() {
+ let (secp_ctx, network, _, _, logger) = build_graph();
+ let (_, our_id, _, nodes) = get_nodes(&secp_ctx);
+ let network_graph = network.read_only();
+ let network_nodes = network_graph.nodes();
+ let network_channels = network_graph.channels();
+ let scorer = ln_test_utils::TestScorer::new();
+ let payment_params = PaymentParameters::from_node_id(nodes[3], 0);
+ let keys_manager = ln_test_utils::TestKeysInterface::new(&[4u8; 32], Network::Testnet);
+ let random_seed_bytes = keys_manager.get_secure_random_bytes();
+
+ let mut route = get_route(&our_id, &payment_params, &network_graph, None, 100, 0,
+ Arc::clone(&logger), &scorer, &random_seed_bytes).unwrap();
+ add_random_cltv_offset(&mut route, &payment_params, &network_graph, &random_seed_bytes);
+
+ let mut path_plausibility = vec![];
+
+ for p in route.paths {
+ // 1. Select random observation point
+ let mut prng = ChaCha20::new(&random_seed_bytes, &[0u8; 12]);
+ let mut random_bytes = [0u8; ::core::mem::size_of::<usize>()];
+
+ prng.process_in_place(&mut random_bytes);
+ let random_path_index = usize::from_be_bytes(random_bytes).wrapping_rem(p.len());
+ let observation_point = NodeId::from_pubkey(&p.get(random_path_index).unwrap().pubkey);
+
+ // 2. Calculate what CLTV expiry delta we would observe there
+ let observed_cltv_expiry_delta: u32 = p[random_path_index..].iter().map(|h| h.cltv_expiry_delta).sum();
+
+ // 3. Starting from the observation point, find candidate paths
+ let mut candidates: VecDeque<(NodeId, Vec<u32>)> = VecDeque::new();
+ candidates.push_back((observation_point, vec![]));
+
+ let mut found_plausible_candidate = false;
+
+ 'candidate_loop: while let Some((cur_node_id, cur_path_cltv_deltas)) = candidates.pop_front() {
+ if let Some(remaining) = observed_cltv_expiry_delta.checked_sub(cur_path_cltv_deltas.iter().sum::<u32>()) {
+ if remaining == 0 || remaining.wrapping_rem(40) == 0 || remaining.wrapping_rem(144) == 0 {
+ found_plausible_candidate = true;
+ break 'candidate_loop;
+ }
+ }
+
+ if let Some(cur_node) = network_nodes.get(&cur_node_id) {
+ for channel_id in &cur_node.channels {
+ if let Some(channel_info) = network_channels.get(&channel_id) {
+ if let Some((dir_info, next_id)) = channel_info.as_directed_from(&cur_node_id) {
+ let next_cltv_expiry_delta = dir_info.direction().cltv_expiry_delta as u32;
+ if cur_path_cltv_deltas.iter().sum::<u32>()
+ .saturating_add(next_cltv_expiry_delta) <= observed_cltv_expiry_delta {
+ let mut new_path_cltv_deltas = cur_path_cltv_deltas.clone();
+ new_path_cltv_deltas.push(next_cltv_expiry_delta);
+ candidates.push_back((*next_id, new_path_cltv_deltas));
+ }
+ }
+ }
+ }
+ }
+ }
+
+ path_plausibility.push(found_plausible_candidate);
+ }
+ assert!(path_plausibility.iter().all(|x| *x));
+ }
+
+ #[test]
+ fn builds_correct_path_from_hops() {
+ let (secp_ctx, network, _, _, logger) = build_graph();
+ let (_, our_id, _, nodes) = get_nodes(&secp_ctx);
+ let network_graph = network.read_only();
+
+ let keys_manager = ln_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[3], 0);
+ let hops = [nodes[1], nodes[2], nodes[4], nodes[3]];
+ let route = build_route_from_hops_internal(&our_id, &hops, &payment_params,
+ &network_graph, 100, 0, Arc::clone(&logger), &random_seed_bytes).unwrap();
+ let route_hop_pubkeys = route.paths[0].iter().map(|hop| hop.pubkey).collect::<Vec<_>>();
+ assert_eq!(hops.len(), route.paths[0].len());
+ for (idx, hop_pubkey) in hops.iter().enumerate() {
+ assert!(*hop_pubkey == route_hop_pubkeys[idx]);
+ }
+ }
+
+ #[test]
+ fn avoids_saturating_channels() {
+ let (secp_ctx, network_graph, gossip_sync, _, logger) = build_graph();
+ let (_, our_id, privkeys, nodes) = get_nodes(&secp_ctx);
+
+ let scorer = ProbabilisticScorer::new(Default::default(), &*network_graph, Arc::clone(&logger));
+
+ // Set the fee on channel 13 to 100% to match channel 4 giving us two equivalent paths (us
+ // -> node 7 -> node2 and us -> node 1 -> node 2) which we should balance over.
+ 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: (4 << 4) | 1,
+ htlc_minimum_msat: 0,
+ htlc_maximum_msat: 250_000_000,
+ fee_base_msat: 0,
+ fee_proportional_millionths: 0,
+ excess_data: Vec::new()
+ });
+ update_channel(&gossip_sync, &secp_ctx, &privkeys[7], UnsignedChannelUpdate {
+ chain_hash: genesis_block(Network::Testnet).header.block_hash(),
+ short_channel_id: 13,
+ timestamp: 2,
+ flags: 0,
+ cltv_expiry_delta: (13 << 4) | 1,
+ htlc_minimum_msat: 0,
+ htlc_maximum_msat: 250_000_000,
+ fee_base_msat: 0,
+ fee_proportional_millionths: 0,
+ excess_data: Vec::new()
+ });
+
+ let config = UserConfig::default();
+ let payment_params = PaymentParameters::from_node_id(nodes[2], 42).with_features(channelmanager::provided_invoice_features(&config));
+ let keys_manager = ln_test_utils::TestKeysInterface::new(&[0u8; 32], Network::Testnet);
+ let random_seed_bytes = keys_manager.get_secure_random_bytes();
+ // 100,000 sats is less than the available liquidity on each channel, set above.
+ let route = get_route(&our_id, &payment_params, &network_graph.read_only(), None, 100_000_000, 42, Arc::clone(&logger), &scorer, &random_seed_bytes).unwrap();
+ assert_eq!(route.paths.len(), 2);
+ assert!((route.paths[0][1].short_channel_id == 4 && route.paths[1][1].short_channel_id == 13) ||
+ (route.paths[1][1].short_channel_id == 4 && route.paths[0][1].short_channel_id == 13));
+ }
+