+
+ #[test]
+ fn abide_by_route_hint_max_htlc() {
+ // Check that we abide by any htlc_maximum_msat provided in the route hints of the payment
+ // params in the final route.
+ let (secp_ctx, network_graph, _, _, logger) = build_graph();
+ let netgraph = network_graph.read_only();
+ 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();
+
+ let max_htlc_msat = 50_000;
+ let route_hint_1 = RouteHint(vec![RouteHintHop {
+ src_node_id: nodes[2],
+ short_channel_id: 42,
+ fees: RoutingFees {
+ base_msat: 100,
+ proportional_millionths: 0,
+ },
+ cltv_expiry_delta: 10,
+ htlc_minimum_msat: None,
+ htlc_maximum_msat: Some(max_htlc_msat),
+ }]);
+ let dest_node_id = ln_test_utils::pubkey(42);
+ let payment_params = PaymentParameters::from_node_id(dest_node_id, 42)
+ .with_route_hints(vec![route_hint_1.clone()]).unwrap()
+ .with_bolt11_features(channelmanager::provided_invoice_features(&config)).unwrap();
+
+ // Make sure we'll error if our route hints don't have enough liquidity according to their
+ // htlc_maximum_msat.
+ if let Err(LightningError{err, action: ErrorAction::IgnoreError}) = get_route(&our_id,
+ &payment_params, &netgraph, None, max_htlc_msat + 1, Arc::clone(&logger), &scorer, &(),
+ &random_seed_bytes)
+ {
+ assert_eq!(err, "Failed to find a sufficient route to the given destination");
+ } else { panic!(); }
+
+ // Make sure we'll split an MPP payment across route hints if their htlc_maximum_msat warrants.
+ let mut route_hint_2 = route_hint_1.clone();
+ route_hint_2.0[0].short_channel_id = 43;
+ let payment_params = PaymentParameters::from_node_id(dest_node_id, 42)
+ .with_route_hints(vec![route_hint_1, route_hint_2]).unwrap()
+ .with_bolt11_features(channelmanager::provided_invoice_features(&config)).unwrap();
+ let route = get_route(&our_id, &payment_params, &netgraph, None, max_htlc_msat + 1,
+ Arc::clone(&logger), &scorer, &(), &random_seed_bytes).unwrap();
+ assert_eq!(route.paths.len(), 2);
+ assert!(route.paths[0].hops.last().unwrap().fee_msat <= max_htlc_msat);
+ assert!(route.paths[1].hops.last().unwrap().fee_msat <= max_htlc_msat);
+ }
+
+ #[test]
+ fn direct_channel_to_hints_with_max_htlc() {
+ // Check that if we have a first hop channel peer that's connected to multiple provided route
+ // hints, that we properly split the payment between the route hints if needed.
+ 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 = ln_test_utils::pubkey(42);
+ let intermed_node_id = ln_test_utils::pubkey(43);
+ let first_hop = vec![get_channel_details(Some(42), intermed_node_id, InitFeatures::from_le_bytes(vec![0b11]), 10_000_000)];
+
+ let amt_msat = 900_000;
+ let max_htlc_msat = 500_000;
+ let route_hint_1 = RouteHint(vec![RouteHintHop {
+ src_node_id: intermed_node_id,
+ short_channel_id: 44,
+ fees: RoutingFees {
+ base_msat: 100,
+ proportional_millionths: 0,
+ },
+ cltv_expiry_delta: 10,
+ htlc_minimum_msat: None,
+ htlc_maximum_msat: Some(max_htlc_msat),
+ }, RouteHintHop {
+ src_node_id: intermed_node_id,
+ short_channel_id: 45,
+ fees: RoutingFees {
+ base_msat: 100,
+ proportional_millionths: 0,
+ },
+ cltv_expiry_delta: 10,
+ htlc_minimum_msat: None,
+ // Check that later route hint max htlcs don't override earlier ones
+ htlc_maximum_msat: Some(max_htlc_msat - 50),
+ }]);
+ let mut route_hint_2 = route_hint_1.clone();
+ route_hint_2.0[0].short_channel_id = 46;
+ route_hint_2.0[1].short_channel_id = 47;
+ let dest_node_id = ln_test_utils::pubkey(44);
+ let payment_params = PaymentParameters::from_node_id(dest_node_id, 42)
+ .with_route_hints(vec![route_hint_1, route_hint_2]).unwrap()
+ .with_bolt11_features(channelmanager::provided_invoice_features(&config)).unwrap();
+
+ let route = get_route(&our_node_id, &payment_params, &network_graph.read_only(),
+ Some(&first_hop.iter().collect::<Vec<_>>()), amt_msat, Arc::clone(&logger), &scorer, &(),
+ &random_seed_bytes).unwrap();
+ assert_eq!(route.paths.len(), 2);
+ assert!(route.paths[0].hops.last().unwrap().fee_msat <= max_htlc_msat);
+ assert!(route.paths[1].hops.last().unwrap().fee_msat <= max_htlc_msat);
+ assert_eq!(route.get_total_amount(), amt_msat);
+
+ // Re-run but with two first hop channels connected to the same route hint peers that must be
+ // split between.
+ let first_hops = vec![
+ get_channel_details(Some(42), intermed_node_id, InitFeatures::from_le_bytes(vec![0b11]), amt_msat - 10),
+ get_channel_details(Some(43), intermed_node_id, InitFeatures::from_le_bytes(vec![0b11]), amt_msat - 10),
+ ];
+ let route = get_route(&our_node_id, &payment_params, &network_graph.read_only(),
+ Some(&first_hops.iter().collect::<Vec<_>>()), amt_msat, Arc::clone(&logger), &scorer, &(),
+ &random_seed_bytes).unwrap();
+ assert_eq!(route.paths.len(), 2);
+ assert!(route.paths[0].hops.last().unwrap().fee_msat <= max_htlc_msat);
+ assert!(route.paths[1].hops.last().unwrap().fee_msat <= max_htlc_msat);
+ assert_eq!(route.get_total_amount(), amt_msat);
+
+ // Make sure this works for blinded route hints.
+ let blinded_path = BlindedPath {
+ introduction_node_id: intermed_node_id,
+ blinding_point: ln_test_utils::pubkey(42),
+ blinded_hops: vec![
+ BlindedHop { blinded_node_id: ln_test_utils::pubkey(42), encrypted_payload: vec![] },
+ BlindedHop { blinded_node_id: ln_test_utils::pubkey(43), encrypted_payload: vec![] },
+ ],
+ };
+ let blinded_payinfo = BlindedPayInfo {
+ fee_base_msat: 100,
+ fee_proportional_millionths: 0,
+ htlc_minimum_msat: 1,
+ htlc_maximum_msat: max_htlc_msat,
+ cltv_expiry_delta: 10,
+ features: BlindedHopFeatures::empty(),
+ };
+ let bolt12_features: Bolt12InvoiceFeatures = channelmanager::provided_invoice_features(&config).to_context();
+ let payment_params = PaymentParameters::blinded(vec![
+ (blinded_payinfo.clone(), blinded_path.clone()),
+ (blinded_payinfo.clone(), blinded_path.clone())])
+ .with_bolt12_features(bolt12_features).unwrap();
+ let route = get_route(&our_node_id, &payment_params, &network_graph.read_only(),
+ Some(&first_hops.iter().collect::<Vec<_>>()), amt_msat, Arc::clone(&logger), &scorer, &(),
+ &random_seed_bytes).unwrap();
+ assert_eq!(route.paths.len(), 2);
+ assert!(route.paths[0].hops.last().unwrap().fee_msat <= max_htlc_msat);
+ assert!(route.paths[1].hops.last().unwrap().fee_msat <= max_htlc_msat);
+ assert_eq!(route.get_total_amount(), amt_msat);
+ }
+
+ #[test]
+ fn blinded_route_ser() {
+ let blinded_path_1 = BlindedPath {
+ introduction_node_id: ln_test_utils::pubkey(42),
+ blinding_point: ln_test_utils::pubkey(43),
+ blinded_hops: vec![
+ BlindedHop { blinded_node_id: ln_test_utils::pubkey(44), encrypted_payload: Vec::new() },
+ BlindedHop { blinded_node_id: ln_test_utils::pubkey(45), encrypted_payload: Vec::new() }
+ ],
+ };
+ let blinded_path_2 = BlindedPath {
+ introduction_node_id: ln_test_utils::pubkey(46),
+ blinding_point: ln_test_utils::pubkey(47),
+ blinded_hops: vec![
+ BlindedHop { blinded_node_id: ln_test_utils::pubkey(48), encrypted_payload: Vec::new() },
+ BlindedHop { blinded_node_id: ln_test_utils::pubkey(49), encrypted_payload: Vec::new() }
+ ],
+ };
+ // (De)serialize a Route with 1 blinded path out of two total paths.
+ let mut route = Route { paths: vec![Path {
+ hops: vec![RouteHop {
+ pubkey: ln_test_utils::pubkey(50),
+ node_features: NodeFeatures::empty(),
+ short_channel_id: 42,
+ channel_features: ChannelFeatures::empty(),
+ fee_msat: 100,
+ cltv_expiry_delta: 0,
+ }],
+ blinded_tail: Some(BlindedTail {
+ hops: blinded_path_1.blinded_hops,
+ blinding_point: blinded_path_1.blinding_point,
+ excess_final_cltv_expiry_delta: 40,
+ final_value_msat: 100,
+ })}, Path {
+ hops: vec![RouteHop {
+ pubkey: ln_test_utils::pubkey(51),
+ node_features: NodeFeatures::empty(),
+ short_channel_id: 43,
+ channel_features: ChannelFeatures::empty(),
+ fee_msat: 100,
+ cltv_expiry_delta: 0,
+ }], blinded_tail: None }],
+ payment_params: None,
+ };
+ let encoded_route = route.encode();
+ let decoded_route: Route = Readable::read(&mut Cursor::new(&encoded_route[..])).unwrap();
+ assert_eq!(decoded_route.paths[0].blinded_tail, route.paths[0].blinded_tail);
+ assert_eq!(decoded_route.paths[1].blinded_tail, route.paths[1].blinded_tail);
+
+ // (De)serialize a Route with two paths, each containing a blinded tail.
+ route.paths[1].blinded_tail = Some(BlindedTail {
+ hops: blinded_path_2.blinded_hops,
+ blinding_point: blinded_path_2.blinding_point,
+ excess_final_cltv_expiry_delta: 41,
+ final_value_msat: 101,
+ });
+ let encoded_route = route.encode();
+ let decoded_route: Route = Readable::read(&mut Cursor::new(&encoded_route[..])).unwrap();
+ assert_eq!(decoded_route.paths[0].blinded_tail, route.paths[0].blinded_tail);
+ assert_eq!(decoded_route.paths[1].blinded_tail, route.paths[1].blinded_tail);
+ }
+
+ #[test]
+ fn blinded_path_inflight_processing() {
+ // Ensure we'll score the channel that's inbound to a blinded path's introduction node, and
+ // account for the blinded tail's final amount_msat.
+ let mut inflight_htlcs = InFlightHtlcs::new();
+ let blinded_path = BlindedPath {
+ introduction_node_id: ln_test_utils::pubkey(43),
+ blinding_point: ln_test_utils::pubkey(48),
+ blinded_hops: vec![BlindedHop { blinded_node_id: ln_test_utils::pubkey(49), encrypted_payload: Vec::new() }],
+ };
+ let path = Path {
+ hops: vec![RouteHop {
+ pubkey: ln_test_utils::pubkey(42),
+ node_features: NodeFeatures::empty(),
+ short_channel_id: 42,
+ channel_features: ChannelFeatures::empty(),
+ fee_msat: 100,
+ cltv_expiry_delta: 0,
+ },
+ RouteHop {
+ pubkey: blinded_path.introduction_node_id,
+ node_features: NodeFeatures::empty(),
+ short_channel_id: 43,
+ channel_features: ChannelFeatures::empty(),
+ fee_msat: 1,
+ cltv_expiry_delta: 0,
+ }],
+ blinded_tail: Some(BlindedTail {
+ hops: blinded_path.blinded_hops,
+ blinding_point: blinded_path.blinding_point,
+ excess_final_cltv_expiry_delta: 0,
+ final_value_msat: 200,
+ }),
+ };
+ inflight_htlcs.process_path(&path, ln_test_utils::pubkey(44));
+ assert_eq!(*inflight_htlcs.0.get(&(42, true)).unwrap(), 301);
+ assert_eq!(*inflight_htlcs.0.get(&(43, false)).unwrap(), 201);
+ }
+
+ #[test]
+ fn blinded_path_cltv_shadow_offset() {
+ // Make sure we add a shadow offset when sending to blinded paths.
+ let blinded_path = BlindedPath {
+ introduction_node_id: ln_test_utils::pubkey(43),
+ blinding_point: ln_test_utils::pubkey(44),
+ blinded_hops: vec![
+ BlindedHop { blinded_node_id: ln_test_utils::pubkey(45), encrypted_payload: Vec::new() },
+ BlindedHop { blinded_node_id: ln_test_utils::pubkey(46), encrypted_payload: Vec::new() }
+ ],
+ };
+ let mut route = Route { paths: vec![Path {
+ hops: vec![RouteHop {
+ pubkey: ln_test_utils::pubkey(42),
+ node_features: NodeFeatures::empty(),
+ short_channel_id: 42,
+ channel_features: ChannelFeatures::empty(),
+ fee_msat: 100,
+ cltv_expiry_delta: 0,
+ },
+ RouteHop {
+ pubkey: blinded_path.introduction_node_id,
+ node_features: NodeFeatures::empty(),
+ short_channel_id: 43,
+ channel_features: ChannelFeatures::empty(),
+ fee_msat: 1,
+ cltv_expiry_delta: 0,
+ }
+ ],
+ blinded_tail: Some(BlindedTail {
+ hops: blinded_path.blinded_hops,
+ blinding_point: blinded_path.blinding_point,
+ excess_final_cltv_expiry_delta: 0,
+ final_value_msat: 200,
+ }),
+ }], payment_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 final_amt_msat = 1001;
+ let payment_params = PaymentParameters::blinded(vec![(blinded_payinfo.clone(), blinded_path.clone())]);
+ let route = get_route(&our_id, &payment_params, &network_graph, None, final_amt_msat , Arc::clone(&logger),
+ &scorer, &(), &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)]);
+ match get_route(&our_id, &payment_params, &network_graph, None, 1001, Arc::clone(&logger),
+ &scorer, &(), &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())]);
+ match get_route(&our_id, &payment_params, &network_graph, None, 1001, Arc::clone(&logger),
+ &scorer, &(), &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)]);
+ match get_route(&our_id, &payment_params, &network_graph, None, 1001, Arc::clone(&logger),
+ &scorer, &(), &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 route = get_route(&our_id, &payment_params, &network_graph, None,
+ 100_000, Arc::clone(&logger), &scorer, &(), &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);
+ }