+ }], route_params: None};
+
+ let payment_params = PaymentParameters::from_node_id(ln_test_utils::pubkey(47), 18);
+ let (_, network_graph, _, _, _) = build_line_graph();
+ add_random_cltv_offset(&mut route, &payment_params, &network_graph.read_only(), &[0; 32]);
+ assert_eq!(route.paths[0].blinded_tail.as_ref().unwrap().excess_final_cltv_expiry_delta, 40);
+ assert_eq!(route.paths[0].hops.last().unwrap().cltv_expiry_delta, 40);
+ }
+
+ #[test]
+ fn simple_blinded_route_hints() {
+ do_simple_blinded_route_hints(1);
+ do_simple_blinded_route_hints(2);
+ do_simple_blinded_route_hints(3);
+ }
+
+ fn do_simple_blinded_route_hints(num_blinded_hops: usize) {
+ // Check that we can generate a route to a blinded path with the expected hops.
+ let (secp_ctx, network, _, _, logger) = build_graph();
+ let (_, our_id, _, nodes) = get_nodes(&secp_ctx);
+ let network_graph = network.read_only();
+
+ 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 mut blinded_path = BlindedPath {
+ introduction_node_id: nodes[2],
+ blinding_point: ln_test_utils::pubkey(42),
+ blinded_hops: Vec::with_capacity(num_blinded_hops),
+ };
+ for i in 0..num_blinded_hops {
+ blinded_path.blinded_hops.push(
+ BlindedHop { blinded_node_id: ln_test_utils::pubkey(42 + i as u8), encrypted_payload: Vec::new() },
+ );
+ }
+ let blinded_payinfo = BlindedPayInfo {
+ fee_base_msat: 100,
+ fee_proportional_millionths: 500,
+ htlc_minimum_msat: 1000,
+ htlc_maximum_msat: 100_000_000,
+ cltv_expiry_delta: 15,
+ features: BlindedHopFeatures::empty(),
+ };
+
+ let payment_params = PaymentParameters::blinded(vec![(blinded_payinfo.clone(), blinded_path.clone())]);
+ let route_params = RouteParameters::from_payment_params_and_value(
+ payment_params, 1001);
+ let route = get_route(&our_id, &route_params, &network_graph, None, Arc::clone(&logger),
+ &scorer, &Default::default(), &random_seed_bytes).unwrap();
+ assert_eq!(route.paths.len(), 1);
+ assert_eq!(route.paths[0].hops.len(), 2);
+
+ let tail = route.paths[0].blinded_tail.as_ref().unwrap();
+ assert_eq!(tail.hops, blinded_path.blinded_hops);
+ assert_eq!(tail.excess_final_cltv_expiry_delta, 0);
+ assert_eq!(tail.final_value_msat, 1001);
+
+ let final_hop = route.paths[0].hops.last().unwrap();
+ assert_eq!(final_hop.pubkey, blinded_path.introduction_node_id);
+ if tail.hops.len() > 1 {
+ assert_eq!(final_hop.fee_msat,
+ blinded_payinfo.fee_base_msat as u64 + blinded_payinfo.fee_proportional_millionths as u64 * tail.final_value_msat / 1000000);
+ assert_eq!(final_hop.cltv_expiry_delta, blinded_payinfo.cltv_expiry_delta as u32);
+ } else {
+ assert_eq!(final_hop.fee_msat, 0);
+ assert_eq!(final_hop.cltv_expiry_delta, 0);
+ }
+ }
+
+ #[test]
+ fn blinded_path_routing_errors() {
+ // Check that we can generate a route to a blinded path with the expected hops.
+ let (secp_ctx, network, _, _, logger) = build_graph();
+ let (_, our_id, _, nodes) = get_nodes(&secp_ctx);
+ let network_graph = network.read_only();
+
+ 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 mut invalid_blinded_path = BlindedPath {
+ introduction_node_id: nodes[2],
+ blinding_point: ln_test_utils::pubkey(42),
+ blinded_hops: vec![
+ BlindedHop { blinded_node_id: ln_test_utils::pubkey(43), encrypted_payload: vec![0; 43] },
+ ],
+ };
+ let blinded_payinfo = BlindedPayInfo {
+ fee_base_msat: 100,
+ fee_proportional_millionths: 500,
+ htlc_minimum_msat: 1000,
+ htlc_maximum_msat: 100_000_000,
+ cltv_expiry_delta: 15,
+ features: BlindedHopFeatures::empty(),
+ };
+
+ let mut invalid_blinded_path_2 = invalid_blinded_path.clone();
+ invalid_blinded_path_2.introduction_node_id = ln_test_utils::pubkey(45);
+ let payment_params = PaymentParameters::blinded(vec![
+ (blinded_payinfo.clone(), invalid_blinded_path.clone()),
+ (blinded_payinfo.clone(), invalid_blinded_path_2)]);
+ let route_params = RouteParameters::from_payment_params_and_value(payment_params, 1001);
+ match get_route(&our_id, &route_params, &network_graph, None, Arc::clone(&logger),
+ &scorer, &Default::default(), &random_seed_bytes)
+ {
+ Err(LightningError { err, .. }) => {
+ assert_eq!(err, "1-hop blinded paths must all have matching introduction node ids");
+ },
+ _ => panic!("Expected error")
+ }
+
+ invalid_blinded_path.introduction_node_id = our_id;
+ let payment_params = PaymentParameters::blinded(vec![(blinded_payinfo.clone(), invalid_blinded_path.clone())]);
+ let route_params = RouteParameters::from_payment_params_and_value(payment_params, 1001);
+ match get_route(&our_id, &route_params, &network_graph, None, Arc::clone(&logger), &scorer,
+ &Default::default(), &random_seed_bytes)
+ {
+ Err(LightningError { err, .. }) => {
+ assert_eq!(err, "Cannot generate a route to blinded paths if we are the introduction node to all of them");
+ },
+ _ => panic!("Expected error")
+ }
+
+ invalid_blinded_path.introduction_node_id = ln_test_utils::pubkey(46);
+ invalid_blinded_path.blinded_hops.clear();
+ let payment_params = PaymentParameters::blinded(vec![(blinded_payinfo, invalid_blinded_path)]);
+ let route_params = RouteParameters::from_payment_params_and_value(payment_params, 1001);
+ match get_route(&our_id, &route_params, &network_graph, None, Arc::clone(&logger), &scorer,
+ &Default::default(), &random_seed_bytes)
+ {
+ Err(LightningError { err, .. }) => {
+ assert_eq!(err, "0-hop blinded path provided");
+ },
+ _ => panic!("Expected error")
+ }
+ }
+
+ #[test]
+ fn matching_intro_node_paths_provided() {
+ // Check that if multiple blinded paths with the same intro node are provided in payment
+ // parameters, we'll return the correct paths in the resulting MPP route.
+ let (secp_ctx, network, _, _, logger) = build_graph();
+ let (_, our_id, _, nodes) = get_nodes(&secp_ctx);
+ let network_graph = network.read_only();
+
+ 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 bolt12_features: Bolt12InvoiceFeatures = channelmanager::provided_invoice_features(&config).to_context();
+ let blinded_path_1 = BlindedPath {
+ introduction_node_id: nodes[2],
+ 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 blinded_payinfo_1 = BlindedPayInfo {
+ fee_base_msat: 0,
+ fee_proportional_millionths: 0,
+ htlc_minimum_msat: 0,
+ htlc_maximum_msat: 30_000,
+ cltv_expiry_delta: 0,
+ features: BlindedHopFeatures::empty(),
+ };
+
+ let mut blinded_path_2 = blinded_path_1.clone();
+ blinded_path_2.blinding_point = ln_test_utils::pubkey(43);
+ let mut blinded_payinfo_2 = blinded_payinfo_1.clone();
+ blinded_payinfo_2.htlc_maximum_msat = 70_000;
+
+ let blinded_hints = vec![
+ (blinded_payinfo_1.clone(), blinded_path_1.clone()),
+ (blinded_payinfo_2.clone(), blinded_path_2.clone()),
+ ];
+ let payment_params = PaymentParameters::blinded(blinded_hints.clone())
+ .with_bolt12_features(bolt12_features.clone()).unwrap();
+
+ let mut route_params = RouteParameters::from_payment_params_and_value(payment_params, 100_000);
+ route_params.max_total_routing_fee_msat = Some(100_000);
+ let route = get_route(&our_id, &route_params, &network_graph, None, Arc::clone(&logger),
+ &scorer, &Default::default(), &random_seed_bytes).unwrap();
+ assert_eq!(route.paths.len(), 2);
+ let mut total_amount_paid_msat = 0;
+ for path in route.paths.into_iter() {
+ assert_eq!(path.hops.last().unwrap().pubkey, nodes[2]);
+ if let Some(bt) = &path.blinded_tail {
+ assert_eq!(bt.blinding_point,
+ blinded_hints.iter().find(|(p, _)| p.htlc_maximum_msat == path.final_value_msat())
+ .map(|(_, bp)| bp.blinding_point).unwrap());
+ } else { panic!(); }
+ total_amount_paid_msat += path.final_value_msat();
+ }
+ assert_eq!(total_amount_paid_msat, 100_000);
+ }
+
+ #[test]
+ fn direct_to_intro_node() {
+ // This previously caused a debug panic in the router when asserting
+ // `used_liquidity_msat <= hop_max_msat`, because when adding first_hop<>blinded_route_hint
+ // direct channels we failed to account for the fee charged for use of the blinded path.
+
+ // Build a graph:
+ // node0 -1(1)2 - node1
+ // such that there isn't enough liquidity to reach node1, but the router thinks there is if it
+ // doesn't account for the blinded path fee.
+
+ 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 amt_msat = 10_000_000;
+ let (_, _, privkeys, nodes) = get_nodes(&secp_ctx);
+ add_channel(&gossip_sync, &secp_ctx, &privkeys[0], &privkeys[1],
+ ChannelFeatures::from_le_bytes(id_to_feature_flags(1)), 1);
+ 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: 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: 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()
+ });
+ let first_hops = vec![
+ get_channel_details(Some(1), nodes[1], InitFeatures::from_le_bytes(vec![0b11]), 10_000_000)];
+
+ let blinded_path = BlindedPath {
+ introduction_node_id: nodes[1],
+ 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 blinded_payinfo = BlindedPayInfo {
+ fee_base_msat: 1000,
+ fee_proportional_millionths: 0,
+ htlc_minimum_msat: 1000,
+ htlc_maximum_msat: MAX_VALUE_MSAT,
+ cltv_expiry_delta: 0,
+ features: BlindedHopFeatures::empty(),
+ };
+ let blinded_hints = vec![(blinded_payinfo.clone(), blinded_path)];
+
+ let payment_params = PaymentParameters::blinded(blinded_hints.clone());
+
+ let netgraph = network_graph.read_only();
+ let route_params = RouteParameters::from_payment_params_and_value(
+ payment_params.clone(), amt_msat);
+ if let Err(LightningError { err, .. }) = get_route(&nodes[0], &route_params, &netgraph,
+ Some(&first_hops.iter().collect::<Vec<_>>()), Arc::clone(&logger), &scorer,
+ &Default::default(), &random_seed_bytes) {
+ assert_eq!(err, "Failed to find a path to the given destination");
+ } else { panic!("Expected error") }
+
+ // Sending an exact amount accounting for the blinded path fee works.
+ let amt_minus_blinded_path_fee = amt_msat - blinded_payinfo.fee_base_msat as u64;
+ let route_params = RouteParameters::from_payment_params_and_value(
+ payment_params, amt_minus_blinded_path_fee);
+ let route = get_route(&nodes[0], &route_params, &netgraph,
+ Some(&first_hops.iter().collect::<Vec<_>>()), Arc::clone(&logger), &scorer,
+ &Default::default(), &random_seed_bytes).unwrap();
+ assert_eq!(route.get_total_fees(), blinded_payinfo.fee_base_msat as u64);
+ assert_eq!(route.get_total_amount(), amt_minus_blinded_path_fee);
+ }
+
+ #[test]
+ fn direct_to_matching_intro_nodes() {
+ // This previously caused us to enter `unreachable` code in the following situation:
+ // 1. We add a route candidate for intro_node contributing a high amount
+ // 2. We add a first_hop<>intro_node route candidate for the same high amount
+ // 3. We see a cheaper blinded route hint for the same intro node but a much lower contribution
+ // amount, and update our route candidate for intro_node for the lower amount
+ // 4. We then attempt to update the aforementioned first_hop<>intro_node route candidate for the
+ // lower contribution amount, but fail (this was previously caused by failure to account for
+ // blinded path fees when adding first_hop<>intro_node candidates)
+ // 5. We go to construct the path from these route candidates and our first_hop<>intro_node
+ // candidate still thinks its path is contributing the original higher amount. This caused us
+ // to hit an `unreachable` overflow when calculating the cheaper intro_node fees over the
+ // larger amount
+ 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();
+
+ // Values are taken from the fuzz input that uncovered this panic.
+ let amt_msat = 21_7020_5185_1403_2640;
+ let (_, _, _, nodes) = get_nodes(&secp_ctx);
+ let first_hops = vec![
+ get_channel_details(Some(1), nodes[1], channelmanager::provided_init_features(&config),
+ 18446744073709551615)];
+
+ let blinded_path = BlindedPath {
+ introduction_node_id: nodes[1],
+ 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 blinded_payinfo = BlindedPayInfo {
+ fee_base_msat: 5046_2720,
+ fee_proportional_millionths: 0,
+ htlc_minimum_msat: 4503_5996_2737_0496,
+ htlc_maximum_msat: 45_0359_9627_3704_9600,
+ cltv_expiry_delta: 0,
+ features: BlindedHopFeatures::empty(),
+ };
+ let mut blinded_hints = vec![
+ (blinded_payinfo.clone(), blinded_path.clone()),
+ (blinded_payinfo.clone(), blinded_path.clone()),
+ ];
+ blinded_hints[1].0.fee_base_msat = 419_4304;
+ blinded_hints[1].0.fee_proportional_millionths = 257;
+ blinded_hints[1].0.htlc_minimum_msat = 280_8908_6115_8400;
+ blinded_hints[1].0.htlc_maximum_msat = 2_8089_0861_1584_0000;
+ blinded_hints[1].0.cltv_expiry_delta = 0;
+
+ let bolt12_features: Bolt12InvoiceFeatures = channelmanager::provided_invoice_features(&config).to_context();
+ let payment_params = 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(&nodes[0], &route_params, &netgraph,
+ Some(&first_hops.iter().collect::<Vec<_>>()), Arc::clone(&logger), &scorer,
+ &Default::default(), &random_seed_bytes).unwrap();
+ assert_eq!(route.get_total_fees(), blinded_payinfo.fee_base_msat as u64);
+ assert_eq!(route.get_total_amount(), amt_msat);
+ }
+
+ #[test]
+ fn we_are_intro_node_candidate_hops() {
+ // This previously led to a panic in the router because we'd generate a Path with only a
+ // BlindedTail and 0 unblinded hops, due to the only candidate hops being blinded route hints
+ // where the origin node is the intro node. We now fully disallow considering candidate hops
+ // where the origin node is the intro node.
+ let (secp_ctx, network_graph, _, _, logger) = build_graph();
+ let (_, our_id, _, nodes) = get_nodes(&secp_ctx);
+ 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();
+
+ // Values are taken from the fuzz input that uncovered this panic.
+ let amt_msat = 21_7020_5185_1423_0019;
+
+ let blinded_path = BlindedPath {
+ introduction_node_id: our_id,
+ 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 blinded_payinfo = BlindedPayInfo {
+ fee_base_msat: 5052_9027,
+ fee_proportional_millionths: 0,
+ htlc_minimum_msat: 21_7020_5185_1423_0019,
+ htlc_maximum_msat: 1844_6744_0737_0955_1615,
+ cltv_expiry_delta: 0,
+ features: BlindedHopFeatures::empty(),
+ };
+ let mut blinded_hints = vec![
+ (blinded_payinfo.clone(), blinded_path.clone()),
+ (blinded_payinfo.clone(), blinded_path.clone()),
+ ];
+ blinded_hints[1].1.introduction_node_id = nodes[6];
+
+ let bolt12_features: Bolt12InvoiceFeatures = channelmanager::provided_invoice_features(&config).to_context();
+ let payment_params = 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);
+ if let Err(LightningError { err, .. }) = get_route(
+ &our_id, &route_params, &netgraph, None, Arc::clone(&logger), &scorer, &(), &random_seed_bytes
+ ) {
+ assert_eq!(err, "Failed to find a path to the given destination");
+ } else { panic!() }
+ }
+
+ #[test]
+ fn we_are_intro_node_bp_in_final_path_fee_calc() {
+ // This previously led to a debug panic in the router because we'd find an invalid Path with
+ // 0 unblinded hops and a blinded tail, leading to the generation of a final
+ // PaymentPathHop::fee_msat that included both the blinded path fees and the final value of
+ // the payment, when it was intended to only include the final value of the payment.
+ let (secp_ctx, network_graph, _, _, logger) = build_graph();
+ let (_, our_id, _, nodes) = get_nodes(&secp_ctx);
+ 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();
+
+ // Values are taken from the fuzz input that uncovered this panic.
+ let amt_msat = 21_7020_5185_1423_0019;
+
+ let blinded_path = BlindedPath {
+ introduction_node_id: our_id,
+ 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 blinded_payinfo = BlindedPayInfo {
+ fee_base_msat: 10_4425_1395,
+ fee_proportional_millionths: 0,
+ htlc_minimum_msat: 21_7301_9934_9094_0931,
+ htlc_maximum_msat: 1844_6744_0737_0955_1615,
+ cltv_expiry_delta: 0,
+ features: BlindedHopFeatures::empty(),
+ };
+ let mut blinded_hints = vec![
+ (blinded_payinfo.clone(), blinded_path.clone()),
+ (blinded_payinfo.clone(), blinded_path.clone()),
+ (blinded_payinfo.clone(), blinded_path.clone()),
+ ];
+ blinded_hints[1].0.fee_base_msat = 5052_9027;
+ blinded_hints[1].0.htlc_minimum_msat = 21_7020_5185_1423_0019;
+ blinded_hints[1].0.htlc_maximum_msat = 1844_6744_0737_0955_1615;
+
+ blinded_hints[2].1.introduction_node_id = nodes[6];
+
+ let bolt12_features: Bolt12InvoiceFeatures = channelmanager::provided_invoice_features(&config).to_context();
+ let payment_params = 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);
+ if let Err(LightningError { err, .. }) = get_route(
+ &our_id, &route_params, &netgraph, None, Arc::clone(&logger), &scorer, &(), &random_seed_bytes
+ ) {
+ assert_eq!(err, "Failed to find a path to the given destination");
+ } else { panic!() }
+ }
+
+ #[test]
+ fn min_htlc_overpay_violates_max_htlc() {
+ do_min_htlc_overpay_violates_max_htlc(true);
+ do_min_htlc_overpay_violates_max_htlc(false);
+ }
+ fn do_min_htlc_overpay_violates_max_htlc(blinded_payee: bool) {
+ // Test that if overpaying to meet a later hop's min_htlc and causes us to violate an earlier
+ // hop's max_htlc, we don't consider that candidate hop valid. Previously we would add this hop
+ // to `targets` and build an invalid path with it, and subsquently hit a debug panic asserting
+ // that the used liquidity for a hop was less than its available liquidity limit.
+ 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();
+
+ // Values are taken from the fuzz input that uncovered this panic.
+ let amt_msat = 7_4009_8048;
+ let (_, our_id, _, nodes) = get_nodes(&secp_ctx);
+ let first_hop_outbound_capacity = 2_7345_2000;
+ let first_hops = vec![get_channel_details(
+ Some(200), nodes[0], channelmanager::provided_init_features(&config),
+ first_hop_outbound_capacity
+ )];
+
+ let base_fee = 1_6778_3453;
+ let htlc_min = 2_5165_8240;
+ let payment_params = if blinded_payee {
+ 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 blinded_payinfo = BlindedPayInfo {
+ fee_base_msat: base_fee,
+ fee_proportional_millionths: 0,
+ htlc_minimum_msat: htlc_min,
+ htlc_maximum_msat: htlc_min * 1000,
+ cltv_expiry_delta: 0,
+ features: BlindedHopFeatures::empty(),
+ };
+ let bolt12_features: Bolt12InvoiceFeatures = channelmanager::provided_invoice_features(&config).to_context();
+ PaymentParameters::blinded(vec![(blinded_payinfo, blinded_path)])
+ .with_bolt12_features(bolt12_features.clone()).unwrap()
+ } else {
+ let route_hint = RouteHint(vec![RouteHintHop {
+ src_node_id: nodes[0],
+ short_channel_id: 42,
+ fees: RoutingFees {
+ base_msat: base_fee,
+ proportional_millionths: 0,
+ },
+ cltv_expiry_delta: 10,
+ htlc_minimum_msat: Some(htlc_min),
+ htlc_maximum_msat: None,
+ }]);
+
+ PaymentParameters::from_node_id(nodes[1], 42)
+ .with_route_hints(vec![route_hint]).unwrap()
+ .with_bolt11_features(channelmanager::provided_invoice_features(&config)).unwrap()
+ };
+
+ let netgraph = network_graph.read_only();
+ let route_params = RouteParameters::from_payment_params_and_value(
+ payment_params, amt_msat);
+ if let Err(LightningError { err, .. }) = get_route(
+ &our_id, &route_params, &netgraph, Some(&first_hops.iter().collect::<Vec<_>>()),
+ Arc::clone(&logger), &scorer, &(), &random_seed_bytes
+ ) {
+ assert_eq!(err, "Failed to find a path to the given destination");
+ } else { panic!() }
+ }
+
+ #[test]
+ fn previously_used_liquidity_violates_max_htlc() {
+ do_previously_used_liquidity_violates_max_htlc(true);
+ do_previously_used_liquidity_violates_max_htlc(false);
+
+ }
+ fn do_previously_used_liquidity_violates_max_htlc(blinded_payee: bool) {
+ // Test that if a candidate first_hop<>route_hint_src_node channel does not have enough
+ // contribution amount to cover the next hop's min_htlc plus fees, we will not consider that
+ // candidate. In this case, the candidate does not have enough due to a previous path taking up
+ // some of its liquidity. Previously we would construct an invalid path and hit a debug panic
+ // asserting that the used liquidity for a hop was less than its available liquidity limit.
+ 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();
+
+ // Values are taken from the fuzz input that uncovered this panic.
+ let amt_msat = 52_4288;
+ let (_, our_id, _, nodes) = get_nodes(&secp_ctx);
+ let first_hops = vec![get_channel_details(
+ Some(161), nodes[0], channelmanager::provided_init_features(&config), 486_4000
+ ), get_channel_details(
+ Some(122), nodes[0], channelmanager::provided_init_features(&config), 179_5000
+ )];
+
+ let base_fees = [0, 425_9840, 0, 0];
+ let htlc_mins = [1_4392, 19_7401, 1027, 6_5535];
+ let payment_params = if blinded_payee {
+ 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 (base_fee, htlc_min) in base_fees.iter().zip(htlc_mins.iter()) {
+ blinded_hints.push((BlindedPayInfo {
+ fee_base_msat: *base_fee,
+ 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()
+ } else {
+ let mut route_hints = Vec::new();
+ for (idx, (base_fee, htlc_min)) in base_fees.iter().zip(htlc_mins.iter()).enumerate() {
+ route_hints.push(RouteHint(vec![RouteHintHop {
+ src_node_id: nodes[0],
+ short_channel_id: 42 + idx as u64,
+ fees: RoutingFees {
+ base_msat: *base_fee,
+ proportional_millionths: 0,
+ },
+ cltv_expiry_delta: 10,
+ htlc_minimum_msat: Some(*htlc_min),
+ htlc_maximum_msat: Some(htlc_min * 100),
+ }]));
+ }
+ PaymentParameters::from_node_id(nodes[1], 42)
+ .with_route_hints(route_hints).unwrap()
+ .with_bolt11_features(channelmanager::provided_invoice_features(&config)).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::<Vec<_>>()),
+ Arc::clone(&logger), &scorer, &(), &random_seed_bytes
+ ).unwrap();
+ assert_eq!(route.paths.len(), 1);
+ assert_eq!(route.get_total_amount(), amt_msat);
+ }
+
+ #[test]
+ fn candidate_path_min() {
+ // Test that if a candidate first_hop<>network_node channel does not have enough contribution
+ // amount to cover the next channel's min htlc plus fees, we will not consider that candidate.
+ // Previously, we were storing RouteGraphNodes with a path_min that did not include fees, and
+ // would add a connecting first_hop node that did not have enough contribution amount, leading
+ // to a debug panic upon invalid path construction.
+ 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(network_graph.clone(), None, logger.clone());
+ 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 = 7_4009_8048;
+ let (_, our_id, privkeys, nodes) = get_nodes(&secp_ctx);
+ let first_hops = vec![get_channel_details(
+ Some(200), nodes[0], channelmanager::provided_init_features(&config), 2_7345_2000
+ )];
+
+ add_channel(&gossip_sync, &secp_ctx, &privkeys[0], &privkeys[6], ChannelFeatures::from_le_bytes(id_to_feature_flags(6)), 6);
+ update_channel(&gossip_sync, &secp_ctx, &privkeys[0], UnsignedChannelUpdate {
+ chain_hash: genesis_block(Network::Testnet).header.block_hash(),
+ short_channel_id: 6,
+ timestamp: 1,
+ flags: 0,
+ cltv_expiry_delta: (6 << 4) | 0,
+ htlc_minimum_msat: 0,
+ htlc_maximum_msat: MAX_VALUE_MSAT,
+ fee_base_msat: 0,
+ fee_proportional_millionths: 0,
+ excess_data: Vec::new()
+ });
+ add_or_update_node(&gossip_sync, &secp_ctx, &privkeys[0], NodeFeatures::from_le_bytes(id_to_feature_flags(1)), 0);
+
+ let htlc_min = 2_5165_8240;
+ let blinded_hints = vec![
+ (BlindedPayInfo {
+ fee_base_msat: 1_6778_3453,
+ fee_proportional_millionths: 0,
+ htlc_minimum_msat: htlc_min,
+ htlc_maximum_msat: htlc_min * 100,
+ cltv_expiry_delta: 10,
+ features: BlindedHopFeatures::empty(),
+ }, 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 bolt12_features: Bolt12InvoiceFeatures = channelmanager::provided_invoice_features(&config).to_context();
+ let payment_params = PaymentParameters::blinded(blinded_hints.clone())
+ .with_bolt12_features(bolt12_features.clone()).unwrap();
+ let route_params = RouteParameters::from_payment_params_and_value(
+ payment_params, amt_msat);
+ let netgraph = network_graph.read_only();
+
+ if let Err(LightningError { err, .. }) = get_route(
+ &our_id, &route_params, &netgraph, Some(&first_hops.iter().collect::<Vec<_>>()),
+ Arc::clone(&logger), &scorer, &ProbabilisticScoringFeeParameters::default(),
+ &random_seed_bytes
+ ) {
+ 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::<Vec<_>>()),
+ Arc::clone(&logger), &scorer, &ProbabilisticScoringFeeParameters::default(),
+ &random_seed_bytes
+ ).unwrap();
+ assert_eq!(route.paths.len(), 1);
+ assert_eq!(route.get_total_amount(), amt_msat);
+ }
+}
+
+#[cfg(all(any(test, ldk_bench), not(feature = "no-std")))]
+pub(crate) mod bench_utils {
+ use super::*;
+ use std::fs::File;
+
+ use bitcoin::hashes::Hash;
+ use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey};
+
+ use crate::chain::transaction::OutPoint;
+ use crate::routing::scoring::ScoreUpdate;
+ use crate::sign::{EntropySource, KeysManager};
+ use crate::ln::ChannelId;
+ use crate::ln::channelmanager::{self, ChannelCounterparty, ChannelDetails};
+ use crate::ln::features::Bolt11InvoiceFeatures;
+ use crate::routing::gossip::NetworkGraph;
+ use crate::util::config::UserConfig;
+ use crate::util::ser::ReadableArgs;
+ use crate::util::test_utils::TestLogger;
+
+ /// Tries to open a network graph file, or panics with a URL to fetch it.
+ pub(crate) fn get_route_file() -> Result<std::fs::File, &'static str> {
+ let res = File::open("net_graph-2023-01-18.bin") // By default we're run in RL/lightning
+ .or_else(|_| File::open("lightning/net_graph-2023-01-18.bin")) // We may be run manually in RL/
+ .or_else(|_| { // Fall back to guessing based on the binary location
+ // path is likely something like .../rust-lightning/target/debug/deps/lightning-...
+ let mut path = std::env::current_exe().unwrap();
+ path.pop(); // lightning-...
+ path.pop(); // deps
+ path.pop(); // debug
+ path.pop(); // target
+ path.push("lightning");
+ path.push("net_graph-2023-01-18.bin");
+ File::open(path)
+ })
+ .or_else(|_| { // Fall back to guessing based on the binary location for a subcrate
+ // path is likely something like .../rust-lightning/bench/target/debug/deps/bench..
+ let mut path = std::env::current_exe().unwrap();
+ path.pop(); // bench...
+ path.pop(); // deps
+ path.pop(); // debug
+ path.pop(); // target
+ path.pop(); // bench
+ path.push("lightning");
+ path.push("net_graph-2023-01-18.bin");
+ File::open(path)
+ })
+ .map_err(|_| "Please fetch https://bitcoin.ninja/ldk-net_graph-v0.0.113-2023-01-18.bin and place it at lightning/net_graph-2023-01-18.bin");
+ #[cfg(require_route_graph_test)]
+ return Ok(res.unwrap());
+ #[cfg(not(require_route_graph_test))]
+ return res;
+ }
+
+ pub(crate) fn read_network_graph(logger: &TestLogger) -> Result<NetworkGraph<&TestLogger>, &'static str> {
+ get_route_file().map(|mut f| NetworkGraph::read(&mut f, logger).unwrap())
+ }
+
+ pub(crate) fn payer_pubkey() -> PublicKey {
+ let secp_ctx = Secp256k1::new();
+ PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap())
+ }
+
+ #[inline]
+ pub(crate) fn first_hop(node_id: PublicKey) -> ChannelDetails {
+ ChannelDetails {
+ channel_id: ChannelId::new_zero(),
+ counterparty: ChannelCounterparty {
+ features: channelmanager::provided_init_features(&UserConfig::default()),
+ node_id,
+ unspendable_punishment_reserve: 0,
+ forwarding_info: None,
+ outbound_htlc_minimum_msat: None,
+ outbound_htlc_maximum_msat: None,
+ },
+ funding_txo: Some(OutPoint {
+ txid: bitcoin::Txid::from_slice(&[0; 32]).unwrap(), index: 0
+ }),
+ channel_type: None,
+ short_channel_id: Some(1),
+ inbound_scid_alias: None,
+ 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,