Skip to content

Commit 985153e

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 0c3a47c commit 985153e

File tree

1 file changed

+46
-87
lines changed

1 file changed

+46
-87
lines changed

lightning/src/routing/router.rs

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

805805
// As for the actual search algorithm,
806806
// we do a payee-to-payer pseudo-Dijkstra's sorting by each node's distance from the payee
@@ -1655,96 +1655,55 @@ where L::Target: Logger {
16551655
return Err(LightningError{err: "Failed to find a sufficient route to the given destination".to_owned(), action: ErrorAction::IgnoreError});
16561656
}
16571657

1658-
// Sort by total fees and take the best paths.
1659-
payment_paths.sort_unstable_by_key(|path| path.get_total_fee_paid_msat());
1660-
if payment_paths.len() > 50 {
1661-
payment_paths.truncate(50);
1662-
}
1658+
// Step (6).
1659+
let mut selected_route = payment_paths;
16631660

1664-
// Draw multiple sufficient routes by randomly combining the selected paths.
1665-
let mut drawn_routes = Vec::new();
1666-
let mut prng = ChaCha20::new(random_seed_bytes, &[0u8; 12]);
1667-
let mut random_index_bytes = [0u8; ::core::mem::size_of::<usize>()];
1661+
debug_assert_eq!(selected_route.iter().map(|p| p.get_value_msat()).sum::<u64>(), already_collected_value_msat);
1662+
let mut overpaid_value_msat = already_collected_value_msat - final_value_msat;
16681663

1669-
let num_permutations = payment_paths.len();
1670-
for _ in 0..num_permutations {
1671-
let mut cur_route = Vec::<PaymentPath>::new();
1672-
let mut aggregate_route_value_msat = 0;
1664+
// First, sort by the cost-per-value of the path, dropping the paths that cost the most for
1665+
// the value they contribute towards the payment amount.
1666+
// We sort in descending order as we will remove from the front in `retain`, next.
1667+
selected_route.sort_unstable_by(|a, b|
1668+
(((b.get_cost_msat() as u128) << 64) / (b.get_value_msat() as u128))
1669+
.cmp(&(((a.get_cost_msat() as u128) << 64) / (a.get_value_msat() as u128)))
1670+
);
16731671

1674-
// Step (6).
1675-
// Do a Fisher-Yates shuffle to create a random permutation of the payment paths
1676-
for cur_index in (1..payment_paths.len()).rev() {
1677-
prng.process_in_place(&mut random_index_bytes);
1678-
let random_index = usize::from_be_bytes(random_index_bytes).wrapping_rem(cur_index+1);
1679-
payment_paths.swap(cur_index, random_index);
1672+
// We should make sure that at least 1 path left.
1673+
let mut paths_left = selected_route.len();
1674+
selected_route.retain(|path| {
1675+
if paths_left == 1 {
1676+
return true
1677+
}
1678+
let path_value_msat = path.get_value_msat();
1679+
if path_value_msat <= overpaid_value_msat {
1680+
overpaid_value_msat -= path_value_msat;
1681+
paths_left -= 1;
1682+
return false;
16801683
}
1684+
true
1685+
});
1686+
debug_assert!(selected_route.len() > 0);
16811687

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

1723-
assert!(cur_route.len() > 0);
1724-
1725-
// Step (8).
1726-
// Now, subtract the overpaid value from the most-expensive path.
1727-
// TODO: this could also be optimized by also sorting by feerate_per_sat_routed,
1728-
// so that the sender pays less fees overall. And also htlc_minimum_msat.
1729-
cur_route.sort_unstable_by_key(|path| { path.hops.iter().map(|hop| hop.0.candidate.fees().proportional_millionths as u64).sum::<u64>() });
1730-
let expensive_payment_path = cur_route.first_mut().unwrap();
1731-
1732-
// We already dropped all the small value paths above, meaning all the
1733-
// remaining paths are larger than remaining overpaid_value_msat.
1734-
// Thus, this can't be negative.
1735-
let expensive_path_new_value_msat = expensive_payment_path.get_value_msat() - overpaid_value_msat;
1736-
expensive_payment_path.update_value_and_recompute_fees(expensive_path_new_value_msat);
1737-
break;
1738-
}
1739-
}
1740-
drawn_routes.push(cur_route);
1700+
// We already dropped all the paths with value below `overpaid_value_msat` above, thus this
1701+
// can't go negative.
1702+
let expensive_path_new_value_msat = expensive_payment_path.get_value_msat() - overpaid_value_msat;
1703+
expensive_payment_path.update_value_and_recompute_fees(expensive_path_new_value_msat);
17411704
}
17421705

1743-
// Step (9).
1744-
// Select the best route by lowest total cost.
1745-
drawn_routes.sort_unstable_by_key(|paths| paths.iter().map(|path| path.get_cost_msat()).sum::<u64>());
1746-
let selected_route = drawn_routes.first_mut().unwrap();
1747-
1706+
// Step (8).
17481707
// Sort by the path itself and combine redundant paths.
17491708
// Note that we sort by SCIDs alone as its simpler but when combining we have to ensure we
17501709
// compare both SCIDs and NodeIds as individual nodes may use random aliases causing collisions

0 commit comments

Comments
 (0)