Merge pull request #1057 from TheBlueMatt/2021-08-invoice-fails
authorMatt Corallo <649246+TheBlueMatt@users.noreply.github.com>
Tue, 31 Aug 2021 22:11:22 +0000 (22:11 +0000)
committerGitHub <noreply@github.com>
Tue, 31 Aug 2021 22:11:22 +0000 (22:11 +0000)
Fix and modernize lightning-invoice API

1  2 
lightning/src/routing/router.rs

index d8f26cdbf64156ec28c47c21cd86b3b15ef2cf10,ea55ce1a9d004940dae4441c85b0a550bcf4aed7..5030f6aaacbf9569a0ff5e2d26a20a31104c0c9b
@@@ -28,7 -28,7 +28,7 @@@ use core::cmp
  use core::ops::Deref;
  
  /// A hop in a route
- #[derive(Clone, PartialEq)]
+ #[derive(Clone, Hash, PartialEq, Eq)]
  pub struct RouteHop {
        /// The node_id of the node at this hop.
        pub pubkey: PublicKey,
@@@ -60,7 -60,7 +60,7 @@@ impl_writeable_tlv_based!(RouteHop, 
  
  /// A route directs a payment from the sender (us) to the recipient. If the recipient supports MPP,
  /// it can take multiple paths. Each path is composed of one or more hops through the network.
- #[derive(Clone, PartialEq)]
+ #[derive(Clone, Hash, PartialEq, Eq)]
  pub struct Route {
        /// The list of routes taken for a single (potentially-)multi-part payment. The pubkey of the
        /// last RouteHop in each path must be the same.
@@@ -108,11 -108,11 +108,11 @@@ impl Readable for Route 
  }
  
  /// A list of hops along a payment path terminating with a channel to the recipient.
- #[derive(Eq, PartialEq, Debug, Clone)]
+ #[derive(Clone, Debug, Hash, Eq, PartialEq)]
  pub struct RouteHint(pub Vec<RouteHintHop>);
  
  /// A channel descriptor for a hop along a payment path.
- #[derive(Eq, PartialEq, Debug, Clone)]
+ #[derive(Clone, Debug, Hash, Eq, PartialEq)]
  pub struct RouteHintHop {
        /// The node_id of the non-target end of the route
        pub src_node_id: PublicKey,
@@@ -375,11 -375,10 +375,11 @@@ pub fn get_route<L: Deref>(our_node_id
                return Err(LightningError{err: "Cannot send a payment of 0 msat".to_owned(), action: ErrorAction::IgnoreError});
        }
  
 -      let last_hops = last_hops.iter().filter_map(|hops| hops.0.last()).collect::<Vec<_>>();
 -      for last_hop in last_hops.iter() {
 -              if last_hop.src_node_id == *payee {
 -                      return Err(LightningError{err: "Last hop cannot have a payee as a source.".to_owned(), action: ErrorAction::IgnoreError});
 +      for route in last_hops.iter() {
 +              for hop in &route.0 {
 +                      if hop.src_node_id == *payee {
 +                              return Err(LightningError{err: "Last hop cannot have a payee as a source.".to_owned(), action: ErrorAction::IgnoreError});
 +                      }
                }
        }
  
                // If a caller provided us with last hops, add them to routing targets. Since this happens
                // earlier than general path finding, they will be somewhat prioritized, although currently
                // it matters only if the fees are exactly the same.
 -              for hop in last_hops.iter() {
 +              for route in last_hops.iter().filter(|route| !route.0.is_empty()) {
 +                      let first_hop_in_route = &(route.0)[0];
                        let have_hop_src_in_graph =
 -                              // Only add the last hop to our candidate set if either we have a direct channel or
 -                              // they are in the regular network graph.
 -                              first_hop_targets.get(&hop.src_node_id).is_some() ||
 -                              network.get_nodes().get(&hop.src_node_id).is_some();
 +                              // 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(&first_hop_in_route.src_node_id).is_some() ||
 +                              network.get_nodes().get(&first_hop_in_route.src_node_id).is_some();
                        if have_hop_src_in_graph {
 -                              // BOLT 11 doesn't allow inclusion of features for the last hop hints, which
 -                              // really sucks, cause we're gonna need that eventually.
 -                              let last_hop_htlc_minimum_msat: u64 = match hop.htlc_minimum_msat {
 -                                      Some(htlc_minimum_msat) => htlc_minimum_msat,
 -                                      None => 0
 -                              };
 -                              let directional_info = DummyDirectionalChannelInfo {
 -                                      cltv_expiry_delta: hop.cltv_expiry_delta as u32,
 -                                      htlc_minimum_msat: last_hop_htlc_minimum_msat,
 -                                      htlc_maximum_msat: hop.htlc_maximum_msat,
 -                                      fees: hop.fees,
 -                              };
 -                              // We assume that the recipient only included route hints for routes which had
 -                              // sufficient value to route `final_value_msat`. Note that in the case of "0-value"
 -                              // invoices where the invoice does not specify value this may not be the case, but
 -                              // better to include the hints than not.
 -                              if add_entry!(hop.short_channel_id, hop.src_node_id, payee, directional_info, Some((final_value_msat + 999) / 1000), &empty_channel_features, 0, path_value_msat, 0) {
 -                                      // If this hop connects to a node with which we have a direct channel,
 -                                      // ignore the network graph and, if the last hop was added, add our
 -                                      // direct channel to the candidate set.
 -                                      //
 -                                      // Note that we *must* check if the last hop was added as `add_entry`
 -                                      // always assumes that the third argument is a node to which we have a
 -                                      // path.
 -                                      if let Some(&(ref first_hop, ref features, ref outbound_capacity_msat, _)) = first_hop_targets.get(&hop.src_node_id) {
 -                                              add_entry!(first_hop, *our_node_id , hop.src_node_id, dummy_directional_info, Some(outbound_capacity_msat / 1000), features, 0, path_value_msat, 0);
 +                              // 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();
 +                              let prev_hop_iter = core::iter::once(payee).chain(
 +                                      route.0.iter().skip(1).rev().map(|hop| &hop.src_node_id));
 +                              let mut hop_used = true;
 +                              let mut aggregate_next_hops_fee_msat: u64 = 0;
 +                              let mut aggregate_next_hops_path_htlc_minimum_msat: u64 = 0;
 +
 +                              for (idx, (hop, prev_hop_id)) in hop_iter.zip(prev_hop_iter).enumerate() {
 +                                      // BOLT 11 doesn't allow inclusion of features for the last hop hints, which
 +                                      // really sucks, cause we're gonna need that eventually.
 +                                      let hop_htlc_minimum_msat: u64 = hop.htlc_minimum_msat.unwrap_or(0);
 +
 +                                      let directional_info = DummyDirectionalChannelInfo {
 +                                              cltv_expiry_delta: hop.cltv_expiry_delta as u32,
 +                                              htlc_minimum_msat: hop_htlc_minimum_msat,
 +                                              htlc_maximum_msat: hop.htlc_maximum_msat,
 +                                              fees: hop.fees,
 +                                      };
 +
 +                                      let reqd_channel_cap = if let Some (val) = final_value_msat.checked_add(match idx {
 +                                              0 => 999,
 +                                              _ => aggregate_next_hops_fee_msat.checked_add(999).unwrap_or(u64::max_value())
 +                                      }) { Some( val / 1000 ) } else { break; }; // converting from msat or breaking if max ~ infinity
 +
 +
 +                                      // We assume that the recipient only included route hints for routes which had
 +                                      // sufficient value to route `final_value_msat`. Note that in the case of "0-value"
 +                                      // invoices where the invoice does not specify value this may not be the case, but
 +                                      // better to include the hints than not.
 +                                      if !add_entry!(hop.short_channel_id, hop.src_node_id, prev_hop_id, directional_info, reqd_channel_cap, &empty_channel_features, aggregate_next_hops_fee_msat, path_value_msat, aggregate_next_hops_path_htlc_minimum_msat) {
 +                                              // If this hop was not used then there is no use checking the preceding hops
 +                                              // in the RouteHint. We can break by just searching for a direct channel between
 +                                              // last checked hop and first_hop_targets
 +                                              hop_used = false;
 +                                      }
 +
 +                                      // Searching for a direct channel between last checked hop and first_hop_targets
 +                                      if let Some(&(ref first_hop, ref features, ref outbound_capacity_msat, _)) = first_hop_targets.get(&prev_hop_id) {
 +                                              add_entry!(first_hop, *our_node_id , prev_hop_id, dummy_directional_info, Some(outbound_capacity_msat / 1000), features, aggregate_next_hops_fee_msat, path_value_msat, aggregate_next_hops_path_htlc_minimum_msat);
 +                                      }
 +
 +                                      if !hop_used {
 +                                              break;
 +                                      }
 +
 +                                      // In the next values of the iterator, the aggregate fees already reflects
 +                                      // the sum of value sent from payer (final_value_msat) and routing fees
 +                                      // for the last node in the RouteHint. We need to just add the fees to
 +                                      // route through the current node so that the preceeding node (next iteration)
 +                                      // can use it.
 +                                      let hops_fee = compute_fees(aggregate_next_hops_fee_msat + final_value_msat, hop.fees)
 +                                              .map_or(None, |inc| inc.checked_add(aggregate_next_hops_fee_msat));
 +                                      aggregate_next_hops_fee_msat = if let Some(val) = hops_fee { val } else { break; };
 +
 +                                      let hop_htlc_minimum_msat_inc = if let Some(val) = compute_fees(aggregate_next_hops_path_htlc_minimum_msat, hop.fees) { val } else { break; };
 +                                      let hops_path_htlc_minimum = aggregate_next_hops_path_htlc_minimum_msat
 +                                              .checked_add(hop_htlc_minimum_msat_inc);
 +                                      aggregate_next_hops_path_htlc_minimum_msat = if let Some(val) = hops_path_htlc_minimum { cmp::max(hop_htlc_minimum_msat, val) } else { break; };
 +
 +                                      if idx == route.0.len() - 1 {
 +                                              // The last hop in this iterator is the first hop in
 +                                              // overall RouteHint.
 +                                              // If this hop connects to a node with which we have a direct channel,
 +                                              // ignore the network graph and, if the last hop was added, add our
 +                                              // direct channel to the candidate set.
 +                                              //
 +                                              // Note that we *must* check if the last hop was added as `add_entry`
 +                                              // always assumes that the third argument is a node to which we have a
 +                                              // path.
 +                                              if let Some(&(ref first_hop, ref features, ref outbound_capacity_msat, _)) = first_hop_targets.get(&hop.src_node_id) {
 +                                                      add_entry!(first_hop, *our_node_id , hop.src_node_id, dummy_directional_info, Some(outbound_capacity_msat / 1000), features, aggregate_next_hops_fee_msat, path_value_msat, aggregate_next_hops_path_htlc_minimum_msat);
 +                                              }
                                        }
                                }
                        }
@@@ -1394,7 -1343,7 +1394,7 @@@ mod tests 
                let logger = Arc::new(test_utils::TestLogger::new());
                let chain_monitor = Arc::new(test_utils::TestChainSource::new(Network::Testnet));
                let net_graph_msg_handler = NetGraphMsgHandler::new(genesis_block(Network::Testnet).header.block_hash(), None, Arc::clone(&logger));
 -              // Build network from our_id to node7:
 +              // Build network from our_id to node6:
                //
                //        -1(1)2-  node0  -1(3)2-
                //       /                       \
                //      \                      /
                //       -1(7)2- node5 -1(10)2-
                //
 +              // Channels 5, 8, 9 and 10 are private channels.
 +              //
                // chan5  1-to-2: enabled, 100 msat fee
                // chan5  2-to-1: enabled, 0 fee
                //
                        cltv_expiry_delta: (8 << 8) | 1,
                        htlc_minimum_msat: None,
                        htlc_maximum_msat: None,
 +              }
 +              ]), RouteHint(vec![RouteHintHop {
 +                      src_node_id: nodes[4].clone(),
 +                      short_channel_id: 9,
 +                      fees: RoutingFees {
 +                              base_msat: 1001,
 +                              proportional_millionths: 0,
 +                      },
 +                      cltv_expiry_delta: (9 << 8) | 1,
 +                      htlc_minimum_msat: None,
 +                      htlc_maximum_msat: None,
                }]), RouteHint(vec![RouteHintHop {
 +                      src_node_id: nodes[5].clone(),
 +                      short_channel_id: 10,
 +                      fees: zero_fees,
 +                      cltv_expiry_delta: (10 << 8) | 1,
 +                      htlc_minimum_msat: None,
 +                      htlc_maximum_msat: None,
 +              }])]
 +      }
 +
 +      fn last_hops_multi_private_channels(nodes: &Vec<PublicKey>) -> Vec<RouteHint> {
 +              let zero_fees = RoutingFees {
 +                      base_msat: 0,
 +                      proportional_millionths: 0,
 +              };
 +              vec![RouteHint(vec![RouteHintHop {
 +                      src_node_id: nodes[2].clone(),
 +                      short_channel_id: 5,
 +                      fees: RoutingFees {
 +                              base_msat: 100,
 +                              proportional_millionths: 0,
 +                      },
 +                      cltv_expiry_delta: (5 << 8) | 1,
 +                      htlc_minimum_msat: None,
 +                      htlc_maximum_msat: None,
 +              }, RouteHintHop {
 +                      src_node_id: nodes[3].clone(),
 +                      short_channel_id: 8,
 +                      fees: zero_fees,
 +                      cltv_expiry_delta: (8 << 8) | 1,
 +                      htlc_minimum_msat: None,
 +                      htlc_maximum_msat: None,
 +              }
 +              ]), RouteHint(vec![RouteHintHop {
                        src_node_id: nodes[4].clone(),
                        short_channel_id: 9,
                        fees: RoutingFees {
        }
  
        #[test]
 -      fn last_hops_test() {
 +      fn partial_route_hint_test() {
                let (secp_ctx, net_graph_msg_handler, _, logger) = build_graph();
                let (_, our_id, _, nodes) = get_nodes(&secp_ctx);
  
                // Simple test across 2, 3, 5, and 4 via a last_hop channel
 +              // Tests the behaviour when the RouteHint contains a suboptimal hop.
 +              // RouteHint may be partially used by the algo to build the best path.
  
                // First check that last hop can't have its source as the payee.
                let invalid_last_hop = RouteHint(vec![RouteHintHop {
                        htlc_maximum_msat: None,
                }]);
  
 -              let mut invalid_last_hops = last_hops(&nodes);
 +              let mut invalid_last_hops = last_hops_multi_private_channels(&nodes);
                invalid_last_hops.push(invalid_last_hop);
                {
                        if let Err(LightningError{err, action: ErrorAction::IgnoreError}) = get_route(&our_id, &net_graph_msg_handler.network_graph.read().unwrap(), &nodes[6], None, None, &invalid_last_hops.iter().collect::<Vec<_>>(), 100, 42, Arc::clone(&logger)) {
                        } else { panic!(); }
                }
  
 -              let route = get_route(&our_id, &net_graph_msg_handler.network_graph.read().unwrap(), &nodes[6], None, None, &last_hops(&nodes).iter().collect::<Vec<_>>(), 100, 42, Arc::clone(&logger)).unwrap();
 +              let route = get_route(&our_id, &net_graph_msg_handler.network_graph.read().unwrap(), &nodes[6], None, None, &last_hops_multi_private_channels(&nodes).iter().collect::<Vec<_>>(), 100, 42, Arc::clone(&logger)).unwrap();
 +              assert_eq!(route.paths[0].len(), 5);
 +
 +              assert_eq!(route.paths[0][0].pubkey, nodes[1]);
 +              assert_eq!(route.paths[0][0].short_channel_id, 2);
 +              assert_eq!(route.paths[0][0].fee_msat, 100);
 +              assert_eq!(route.paths[0][0].cltv_expiry_delta, (4 << 8) | 1);
 +              assert_eq!(route.paths[0][0].node_features.le_flags(), &id_to_feature_flags(2));
 +              assert_eq!(route.paths[0][0].channel_features.le_flags(), &id_to_feature_flags(2));
 +
 +              assert_eq!(route.paths[0][1].pubkey, nodes[2]);
 +              assert_eq!(route.paths[0][1].short_channel_id, 4);
 +              assert_eq!(route.paths[0][1].fee_msat, 0);
 +              assert_eq!(route.paths[0][1].cltv_expiry_delta, (6 << 8) | 1);
 +              assert_eq!(route.paths[0][1].node_features.le_flags(), &id_to_feature_flags(3));
 +              assert_eq!(route.paths[0][1].channel_features.le_flags(), &id_to_feature_flags(4));
 +
 +              assert_eq!(route.paths[0][2].pubkey, nodes[4]);
 +              assert_eq!(route.paths[0][2].short_channel_id, 6);
 +              assert_eq!(route.paths[0][2].fee_msat, 0);
 +              assert_eq!(route.paths[0][2].cltv_expiry_delta, (11 << 8) | 1);
 +              assert_eq!(route.paths[0][2].node_features.le_flags(), &id_to_feature_flags(5));
 +              assert_eq!(route.paths[0][2].channel_features.le_flags(), &id_to_feature_flags(6));
 +
 +              assert_eq!(route.paths[0][3].pubkey, nodes[3]);
 +              assert_eq!(route.paths[0][3].short_channel_id, 11);
 +              assert_eq!(route.paths[0][3].fee_msat, 0);
 +              assert_eq!(route.paths[0][3].cltv_expiry_delta, (8 << 8) | 1);
 +              // If we have a peer in the node map, we'll use their features here since we don't have
 +              // a way of figuring out their features from the invoice:
 +              assert_eq!(route.paths[0][3].node_features.le_flags(), &id_to_feature_flags(4));
 +              assert_eq!(route.paths[0][3].channel_features.le_flags(), &id_to_feature_flags(11));
 +
 +              assert_eq!(route.paths[0][4].pubkey, nodes[6]);
 +              assert_eq!(route.paths[0][4].short_channel_id, 8);
 +              assert_eq!(route.paths[0][4].fee_msat, 100);
 +              assert_eq!(route.paths[0][4].cltv_expiry_delta, 42);
 +              assert_eq!(route.paths[0][4].node_features.le_flags(), &Vec::<u8>::new()); // We dont pass flags in from invoices yet
 +              assert_eq!(route.paths[0][4].channel_features.le_flags(), &Vec::<u8>::new()); // We can't learn any flags from invoices, sadly
 +      }
 +
 +      fn empty_last_hop(nodes: &Vec<PublicKey>) -> Vec<RouteHint> {
 +              let zero_fees = RoutingFees {
 +                      base_msat: 0,
 +                      proportional_millionths: 0,
 +              };
 +              vec![RouteHint(vec![RouteHintHop {
 +                      src_node_id: nodes[3].clone(),
 +                      short_channel_id: 8,
 +                      fees: zero_fees,
 +                      cltv_expiry_delta: (8 << 8) | 1,
 +                      htlc_minimum_msat: None,
 +                      htlc_maximum_msat: None,
 +              }]), RouteHint(vec![
 +
 +              ]), RouteHint(vec![RouteHintHop {
 +                      src_node_id: nodes[5].clone(),
 +                      short_channel_id: 10,
 +                      fees: zero_fees,
 +                      cltv_expiry_delta: (10 << 8) | 1,
 +                      htlc_minimum_msat: None,
 +                      htlc_maximum_msat: None,
 +              }])]
 +      }
 +
 +      #[test]
 +      fn ignores_empty_last_hops_test() {
 +              let (secp_ctx, net_graph_msg_handler, _, logger) = build_graph();
 +              let (_, our_id, _, nodes) = get_nodes(&secp_ctx);
 +
 +              // Test handling of an empty RouteHint passed in Invoice.
 +
 +              let route = get_route(&our_id, &net_graph_msg_handler.network_graph.read().unwrap(), &nodes[6], None, None, &empty_last_hop(&nodes).iter().collect::<Vec<_>>(), 100, 42, Arc::clone(&logger)).unwrap();
                assert_eq!(route.paths[0].len(), 5);
  
                assert_eq!(route.paths[0][0].pubkey, nodes[1]);
                assert_eq!(route.paths[0][4].channel_features.le_flags(), &Vec::<u8>::new()); // We can't learn any flags from invoices, sadly
        }
  
 +      fn multi_hint_last_hops(nodes: &Vec<PublicKey>) -> Vec<RouteHint> {
 +              let zero_fees = RoutingFees {
 +                      base_msat: 0,
 +                      proportional_millionths: 0,
 +              };
 +              vec![RouteHint(vec![RouteHintHop {
 +                      src_node_id: nodes[2].clone(),
 +                      short_channel_id: 5,
 +                      fees: RoutingFees {
 +                              base_msat: 100,
 +                              proportional_millionths: 0,
 +                      },
 +                      cltv_expiry_delta: (5 << 8) | 1,
 +                      htlc_minimum_msat: None,
 +                      htlc_maximum_msat: None,
 +              }, RouteHintHop {
 +                      src_node_id: nodes[3].clone(),
 +                      short_channel_id: 8,
 +                      fees: zero_fees,
 +                      cltv_expiry_delta: (8 << 8) | 1,
 +                      htlc_minimum_msat: None,
 +                      htlc_maximum_msat: None,
 +              }]), RouteHint(vec![RouteHintHop {
 +                      src_node_id: nodes[5].clone(),
 +                      short_channel_id: 10,
 +                      fees: zero_fees,
 +                      cltv_expiry_delta: (10 << 8) | 1,
 +                      htlc_minimum_msat: None,
 +                      htlc_maximum_msat: None,
 +              }])]
 +      }
 +
 +      #[test]
 +      fn multi_hint_last_hops_test() {
 +              let (secp_ctx, net_graph_msg_handler, _, logger) = build_graph();
 +              let (_, our_id, privkeys, nodes) = get_nodes(&secp_ctx);
 +              // Test through channels 2, 3, 5, 8.
 +              // Test shows that multiple hop hints are considered.
 +
 +              // Disabling channels 6 & 7 by flags=2
 +              update_channel(&net_graph_msg_handler, &secp_ctx, &privkeys[2], UnsignedChannelUpdate {
 +                      chain_hash: genesis_block(Network::Testnet).header.block_hash(),
 +                      short_channel_id: 6,
 +                      timestamp: 2,
 +                      flags: 2, // to disable
 +                      cltv_expiry_delta: 0,
 +                      htlc_minimum_msat: 0,
 +                      htlc_maximum_msat: OptionalField::Absent,
 +                      fee_base_msat: 0,
 +                      fee_proportional_millionths: 0,
 +                      excess_data: Vec::new()
 +              });
 +              update_channel(&net_graph_msg_handler, &secp_ctx, &privkeys[2], UnsignedChannelUpdate {
 +                      chain_hash: genesis_block(Network::Testnet).header.block_hash(),
 +                      short_channel_id: 7,
 +                      timestamp: 2,
 +                      flags: 2, // to disable
 +                      cltv_expiry_delta: 0,
 +                      htlc_minimum_msat: 0,
 +                      htlc_maximum_msat: OptionalField::Absent,
 +                      fee_base_msat: 0,
 +                      fee_proportional_millionths: 0,
 +                      excess_data: Vec::new()
 +              });
 +
 +              let route = get_route(&our_id, &net_graph_msg_handler.network_graph.read().unwrap(), &nodes[6], None, None, &multi_hint_last_hops(&nodes).iter().collect::<Vec<_>>(), 100, 42, Arc::clone(&logger)).unwrap();
 +              assert_eq!(route.paths[0].len(), 4);
 +
 +              assert_eq!(route.paths[0][0].pubkey, nodes[1]);
 +              assert_eq!(route.paths[0][0].short_channel_id, 2);
 +              assert_eq!(route.paths[0][0].fee_msat, 200);
 +              assert_eq!(route.paths[0][0].cltv_expiry_delta, 1025);
 +              assert_eq!(route.paths[0][0].node_features.le_flags(), &id_to_feature_flags(2));
 +              assert_eq!(route.paths[0][0].channel_features.le_flags(), &id_to_feature_flags(2));
 +
 +              assert_eq!(route.paths[0][1].pubkey, nodes[2]);
 +              assert_eq!(route.paths[0][1].short_channel_id, 4);
 +              assert_eq!(route.paths[0][1].fee_msat, 100);
 +              assert_eq!(route.paths[0][1].cltv_expiry_delta, 1281);
 +              assert_eq!(route.paths[0][1].node_features.le_flags(), &id_to_feature_flags(3));
 +              assert_eq!(route.paths[0][1].channel_features.le_flags(), &id_to_feature_flags(4));
 +
 +              assert_eq!(route.paths[0][2].pubkey, nodes[3]);
 +              assert_eq!(route.paths[0][2].short_channel_id, 5);
 +              assert_eq!(route.paths[0][2].fee_msat, 0);
 +              assert_eq!(route.paths[0][2].cltv_expiry_delta, 2049);
 +              assert_eq!(route.paths[0][2].node_features.le_flags(), &id_to_feature_flags(4));
 +              assert_eq!(route.paths[0][2].channel_features.le_flags(), &Vec::<u8>::new());
 +
 +              assert_eq!(route.paths[0][3].pubkey, nodes[6]);
 +              assert_eq!(route.paths[0][3].short_channel_id, 8);
 +              assert_eq!(route.paths[0][3].fee_msat, 100);
 +              assert_eq!(route.paths[0][3].cltv_expiry_delta, 42);
 +              assert_eq!(route.paths[0][3].node_features.le_flags(), &Vec::<u8>::new()); // We dont pass flags in from invoices yet
 +              assert_eq!(route.paths[0][3].channel_features.le_flags(), &Vec::<u8>::new()); // We can't learn any flags from invoices, sadly
 +      }
 +
 +      fn last_hops_with_public_channel(nodes: &Vec<PublicKey>) -> Vec<RouteHint> {
 +              let zero_fees = RoutingFees {
 +                      base_msat: 0,
 +                      proportional_millionths: 0,
 +              };
 +              vec![RouteHint(vec![RouteHintHop {
 +                      src_node_id: nodes[4].clone(),
 +                      short_channel_id: 11,
 +                      fees: zero_fees,
 +                      cltv_expiry_delta: (11 << 8) | 1,
 +                      htlc_minimum_msat: None,
 +                      htlc_maximum_msat: None,
 +              }, RouteHintHop {
 +                      src_node_id: nodes[3].clone(),
 +                      short_channel_id: 8,
 +                      fees: zero_fees,
 +                      cltv_expiry_delta: (8 << 8) | 1,
 +                      htlc_minimum_msat: None,
 +                      htlc_maximum_msat: None,
 +              }]), RouteHint(vec![RouteHintHop {
 +                      src_node_id: nodes[4].clone(),
 +                      short_channel_id: 9,
 +                      fees: RoutingFees {
 +                              base_msat: 1001,
 +                              proportional_millionths: 0,
 +                      },
 +                      cltv_expiry_delta: (9 << 8) | 1,
 +                      htlc_minimum_msat: None,
 +                      htlc_maximum_msat: None,
 +              }]), RouteHint(vec![RouteHintHop {
 +                      src_node_id: nodes[5].clone(),
 +                      short_channel_id: 10,
 +                      fees: zero_fees,
 +                      cltv_expiry_delta: (10 << 8) | 1,
 +                      htlc_minimum_msat: None,
 +                      htlc_maximum_msat: None,
 +              }])]
 +      }
 +
 +      #[test]
 +      fn last_hops_with_public_channel_test() {
 +              let (secp_ctx, net_graph_msg_handler, _, logger) = build_graph();
 +              let (_, our_id, _, nodes) = get_nodes(&secp_ctx);
 +              // This test shows that public routes can be present in the invoice
 +              // which would be handled in the same manner.
 +
 +              let route = get_route(&our_id, &net_graph_msg_handler.network_graph.read().unwrap(), &nodes[6], None, None, &last_hops_with_public_channel(&nodes).iter().collect::<Vec<_>>(), 100, 42, Arc::clone(&logger)).unwrap();
 +              assert_eq!(route.paths[0].len(), 5);
 +
 +              assert_eq!(route.paths[0][0].pubkey, nodes[1]);
 +              assert_eq!(route.paths[0][0].short_channel_id, 2);
 +              assert_eq!(route.paths[0][0].fee_msat, 100);
 +              assert_eq!(route.paths[0][0].cltv_expiry_delta, (4 << 8) | 1);
 +              assert_eq!(route.paths[0][0].node_features.le_flags(), &id_to_feature_flags(2));
 +              assert_eq!(route.paths[0][0].channel_features.le_flags(), &id_to_feature_flags(2));
 +
 +              assert_eq!(route.paths[0][1].pubkey, nodes[2]);
 +              assert_eq!(route.paths[0][1].short_channel_id, 4);
 +              assert_eq!(route.paths[0][1].fee_msat, 0);
 +              assert_eq!(route.paths[0][1].cltv_expiry_delta, (6 << 8) | 1);
 +              assert_eq!(route.paths[0][1].node_features.le_flags(), &id_to_feature_flags(3));
 +              assert_eq!(route.paths[0][1].channel_features.le_flags(), &id_to_feature_flags(4));
 +
 +              assert_eq!(route.paths[0][2].pubkey, nodes[4]);
 +              assert_eq!(route.paths[0][2].short_channel_id, 6);
 +              assert_eq!(route.paths[0][2].fee_msat, 0);
 +              assert_eq!(route.paths[0][2].cltv_expiry_delta, (11 << 8) | 1);
 +              assert_eq!(route.paths[0][2].node_features.le_flags(), &id_to_feature_flags(5));
 +              assert_eq!(route.paths[0][2].channel_features.le_flags(), &id_to_feature_flags(6));
 +
 +              assert_eq!(route.paths[0][3].pubkey, nodes[3]);
 +              assert_eq!(route.paths[0][3].short_channel_id, 11);
 +              assert_eq!(route.paths[0][3].fee_msat, 0);
 +              assert_eq!(route.paths[0][3].cltv_expiry_delta, (8 << 8) | 1);
 +              // If we have a peer in the node map, we'll use their features here since we don't have
 +              // a way of figuring out their features from the invoice:
 +              assert_eq!(route.paths[0][3].node_features.le_flags(), &id_to_feature_flags(4));
 +              assert_eq!(route.paths[0][3].channel_features.le_flags(), &Vec::<u8>::new());
 +
 +              assert_eq!(route.paths[0][4].pubkey, nodes[6]);
 +              assert_eq!(route.paths[0][4].short_channel_id, 8);
 +              assert_eq!(route.paths[0][4].fee_msat, 100);
 +              assert_eq!(route.paths[0][4].cltv_expiry_delta, 42);
 +              assert_eq!(route.paths[0][4].node_features.le_flags(), &Vec::<u8>::new()); // We dont pass flags in from invoices yet
 +              assert_eq!(route.paths[0][4].channel_features.le_flags(), &Vec::<u8>::new()); // We can't learn any flags from invoices, sadly
 +      }
 +
        #[test]
        fn our_chans_last_hop_connect_test() {
                let (secp_ctx, net_graph_msg_handler, _, logger) = build_graph();