Skip to content

Commit 99b2554

Browse files
committed
Make route path selection optimize strictly for cost / amount
Currently, after we've selected a number of candidate paths, we construct a route from a random set of paths repeatedly, and then select the route with the lowest total cost. In the vast majority of cases this ends up doing a bunch of additional work in order to select the path(s) with the total lowest cost, with some vague attempt at randomization that doesn't actually work. Instead, here, we simply sort available paths by `cost / amount` and select the top paths. This ends up in practice having the same end result with substantially less complexity. In some rare cases it gets a better result, which also would have been achieved through more random trials. This implies there may in such cases be a potential privacy loss, but not a substantial one, given our path selection is ultimately mostly deterministic in many cases (or, if it is not, then privacy is achieved through randomization at the scorer level).
1 parent d6dfa17 commit 99b2554

File tree

1 file changed

+48
-87
lines changed

1 file changed

+48
-87
lines changed

lightning/src/routing/router.rs

Lines changed: 48 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -746,7 +746,7 @@ where L::Target: Logger, GL::Target: Logger {
746746
pub(crate) fn get_route<L: Deref, S: Score>(
747747
our_node_pubkey: &PublicKey, payment_params: &PaymentParameters, network_graph: &ReadOnlyNetworkGraph,
748748
first_hops: Option<&[&ChannelDetails]>, final_value_msat: u64, final_cltv_expiry_delta: u32,
749-
logger: L, scorer: &S, random_seed_bytes: &[u8; 32]
749+
logger: L, scorer: &S, _random_seed_bytes: &[u8; 32]
750750
) -> Result<Route, LightningError>
751751
where L::Target: Logger {
752752
let payee_node_id = NodeId::from_pubkey(&payment_params.payee_pubkey);
@@ -788,11 +788,11 @@ where L::Target: Logger {
788788
// 4. See if we managed to collect paths which aggregately are able to transfer target value
789789
// (not recommended value).
790790
// 5. If yes, proceed. If not, fail routing.
791-
// 6. Randomly combine paths into routes having enough to fulfill the payment. (TODO: knapsack)
792-
// 7. Of all the found paths, select only those with the lowest total fee.
793-
// 8. The last path in every selected route is likely to be more than we need.
794-
// Reduce its value-to-transfer and recompute fees.
795-
// 9. Choose the best route by the lowest total fee.
791+
// 6. Select the paths which have the lowest cost (fee plus scorer penalty) per amount
792+
// transferred up to the transfer target value.
793+
// 7. Reduce the value of the last path until we are sending only the target value.
794+
// 8. If our maximum channel saturation limit caused us to pick two identical paths, combine
795+
// them so that we're not sending two HTLCs along the same path.
796796

797797
// As for the actual search algorithm,
798798
// we do a payee-to-payer pseudo-Dijkstra's sorting by each node's distance from the payee
@@ -1643,96 +1643,57 @@ where L::Target: Logger {
16431643
return Err(LightningError{err: "Failed to find a sufficient route to the given destination".to_owned(), action: ErrorAction::IgnoreError});
16441644
}
16451645

1646-
// Sort by total fees and take the best paths.
1647-
payment_paths.sort_unstable_by_key(|path| path.get_total_fee_paid_msat());
1648-
if payment_paths.len() > 50 {
1649-
payment_paths.truncate(50);
1650-
}
1646+
// Step (6).
1647+
let mut selected_route = payment_paths;
16511648

1652-
// Draw multiple sufficient routes by randomly combining the selected paths.
1653-
let mut drawn_routes = Vec::new();
1654-
let mut prng = ChaCha20::new(random_seed_bytes, &[0u8; 12]);
1655-
let mut random_index_bytes = [0u8; ::core::mem::size_of::<usize>()];
1649+
debug_assert_eq!(selected_route.iter().map(|p| p.get_value_msat()).sum::<u64>(), already_collected_value_msat);
1650+
let mut overpaid_value_msat = already_collected_value_msat - final_value_msat;
16561651

1657-
let num_permutations = payment_paths.len();
1658-
for _ in 0..num_permutations {
1659-
let mut cur_route = Vec::<PaymentPath>::new();
1660-
let mut aggregate_route_value_msat = 0;
1652+
// First, sort by the cost-per-value of the path, selecting only the paths which
1653+
// contribute the most per cost.
1654+
// We sort backwards as we will remove from the front in `retain`, next.
1655+
selected_route.sort_unstable_by(|a, b|
1656+
(((b.get_cost_msat() as u128) << 64) / (b.get_value_msat() as u128))
1657+
.cmp(&(((a.get_cost_msat() as u128) << 64) / (a.get_value_msat() as u128)))
1658+
);
16611659

1662-
// Step (6).
1663-
// Do a Fisher-Yates shuffle to create a random permutation of the payment paths
1664-
for cur_index in (1..payment_paths.len()).rev() {
1665-
prng.process_in_place(&mut random_index_bytes);
1666-
let random_index = usize::from_be_bytes(random_index_bytes).wrapping_rem(cur_index+1);
1667-
payment_paths.swap(cur_index, random_index);
1660+
// We should make sure that at least 1 path left.
1661+
let mut paths_left = selected_route.len();
1662+
selected_route.retain(|path| {
1663+
if paths_left == 1 {
1664+
return true
1665+
}
1666+
let mut keep = true;
1667+
let path_value_msat = path.get_value_msat();
1668+
if path_value_msat <= overpaid_value_msat {
1669+
keep = false;
1670+
overpaid_value_msat -= path_value_msat;
1671+
paths_left -= 1;
16681672
}
1673+
keep
1674+
});
1675+
assert!(selected_route.len() > 0);
16691676

1677+
if overpaid_value_msat != 0 {
16701678
// Step (7).
1671-
for payment_path in &payment_paths {
1672-
cur_route.push(payment_path.clone());
1673-
aggregate_route_value_msat += payment_path.get_value_msat();
1674-
if aggregate_route_value_msat > final_value_msat {
1675-
// Last path likely overpaid. Substract it from the most expensive
1676-
// (in terms of proportional fee) path in this route and recompute fees.
1677-
// This might be not the most economically efficient way, but fewer paths
1678-
// also makes routing more reliable.
1679-
let mut overpaid_value_msat = aggregate_route_value_msat - final_value_msat;
1680-
1681-
// First, we drop some expensive low-value paths entirely if possible, since fewer
1682-
// paths is better: the payment is less likely to fail. In order to do so, we sort
1683-
// by value and fall back to total fees paid, i.e., in case of equal values we
1684-
// prefer lower cost paths.
1685-
cur_route.sort_unstable_by(|a, b| {
1686-
a.get_value_msat().cmp(&b.get_value_msat())
1687-
// Reverse ordering for cost, so we drop higher-cost paths first
1688-
.then_with(|| b.get_cost_msat().cmp(&a.get_cost_msat()))
1689-
});
1690-
1691-
// We should make sure that at least 1 path left.
1692-
let mut paths_left = cur_route.len();
1693-
cur_route.retain(|path| {
1694-
if paths_left == 1 {
1695-
return true
1696-
}
1697-
let mut keep = true;
1698-
let path_value_msat = path.get_value_msat();
1699-
if path_value_msat <= overpaid_value_msat {
1700-
keep = false;
1701-
overpaid_value_msat -= path_value_msat;
1702-
paths_left -= 1;
1703-
}
1704-
keep
1705-
});
1706-
1707-
if overpaid_value_msat == 0 {
1708-
break;
1709-
}
1679+
// Now, subtract the overpaid value from the most-expensive path.
1680+
// TODO: this could also be optimized by also sorting by feerate_per_sat_routed,
1681+
// so that the sender pays less fees overall. And also htlc_minimum_msat.
1682+
selected_route.sort_unstable_by(|a, b| {
1683+
let a_f = a.hops.iter().map(|hop| hop.0.candidate.fees().proportional_millionths as u64).sum::<u64>();
1684+
let b_f = b.hops.iter().map(|hop| hop.0.candidate.fees().proportional_millionths as u64).sum::<u64>();
1685+
a_f.cmp(&b_f).then_with(|| b.get_cost_msat().cmp(&a.get_cost_msat()))
1686+
});
1687+
let expensive_payment_path = selected_route.first_mut().unwrap();
17101688

1711-
assert!(cur_route.len() > 0);
1712-
1713-
// Step (8).
1714-
// Now, subtract the overpaid value from the most-expensive path.
1715-
// TODO: this could also be optimized by also sorting by feerate_per_sat_routed,
1716-
// so that the sender pays less fees overall. And also htlc_minimum_msat.
1717-
cur_route.sort_unstable_by_key(|path| { path.hops.iter().map(|hop| hop.0.candidate.fees().proportional_millionths as u64).sum::<u64>() });
1718-
let expensive_payment_path = cur_route.first_mut().unwrap();
1719-
1720-
// We already dropped all the small value paths above, meaning all the
1721-
// remaining paths are larger than remaining overpaid_value_msat.
1722-
// Thus, this can't be negative.
1723-
let expensive_path_new_value_msat = expensive_payment_path.get_value_msat() - overpaid_value_msat;
1724-
expensive_payment_path.update_value_and_recompute_fees(expensive_path_new_value_msat);
1725-
break;
1726-
}
1727-
}
1728-
drawn_routes.push(cur_route);
1689+
// We already dropped all the small value paths above, meaning all the
1690+
// remaining paths are larger than remaining overpaid_value_msat.
1691+
// Thus, this can't be negative.
1692+
let expensive_path_new_value_msat = expensive_payment_path.get_value_msat() - overpaid_value_msat;
1693+
expensive_payment_path.update_value_and_recompute_fees(expensive_path_new_value_msat);
17291694
}
17301695

1731-
// Step (9).
1732-
// Select the best route by lowest total cost.
1733-
drawn_routes.sort_unstable_by_key(|paths| paths.iter().map(|path| path.get_cost_msat()).sum::<u64>());
1734-
let selected_route = drawn_routes.first_mut().unwrap();
1735-
1696+
// Step (8).
17361697
// Sort by the path itself and combine redundant paths.
17371698
// Note that we sort by SCIDs alone as its simpler but when combining we have to ensure we
17381699
// compare both SCIDs and NodeIds as individual nodes may use random aliases causing collisions

0 commit comments

Comments
 (0)