Skip to content

Commit 7a78998

Browse files
Merge pull request #2305 from valentinewallace/2023-05-respect-hint-maxhtlc
Respect route hint `max_htlc` in pathfinding
2 parents 583a5b2 + f130f95 commit 7a78998

File tree

3 files changed

+209
-48
lines changed

3 files changed

+209
-48
lines changed

lightning/src/routing/gossip.rs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -970,7 +970,7 @@ impl<'a> DirectedChannelInfo<'a> {
970970
htlc_maximum_msat = cmp::min(htlc_maximum_msat, capacity_msat);
971971
EffectiveCapacity::Total { capacity_msat, htlc_maximum_msat: htlc_maximum_msat }
972972
},
973-
None => EffectiveCapacity::MaximumHTLC { amount_msat: htlc_maximum_msat },
973+
None => EffectiveCapacity::AdvertisedMaxHTLC { amount_msat: htlc_maximum_msat },
974974
};
975975

976976
Self {
@@ -1024,7 +1024,7 @@ pub enum EffectiveCapacity {
10241024
liquidity_msat: u64,
10251025
},
10261026
/// The maximum HTLC amount in one direction as advertised on the gossip network.
1027-
MaximumHTLC {
1027+
AdvertisedMaxHTLC {
10281028
/// The maximum HTLC amount denominated in millisatoshi.
10291029
amount_msat: u64,
10301030
},
@@ -1038,6 +1038,11 @@ pub enum EffectiveCapacity {
10381038
/// A capacity sufficient to route any payment, typically used for private channels provided by
10391039
/// an invoice.
10401040
Infinite,
1041+
/// The maximum HTLC amount as provided by an invoice route hint.
1042+
HintMaxHTLC {
1043+
/// The maximum HTLC amount denominated in millisatoshi.
1044+
amount_msat: u64,
1045+
},
10411046
/// A capacity that is unknown possibly because either the chain state is unavailable to know
10421047
/// the total capacity or the `htlc_maximum_msat` was not advertised on the gossip network.
10431048
Unknown,
@@ -1052,8 +1057,9 @@ impl EffectiveCapacity {
10521057
pub fn as_msat(&self) -> u64 {
10531058
match self {
10541059
EffectiveCapacity::ExactLiquidity { liquidity_msat } => *liquidity_msat,
1055-
EffectiveCapacity::MaximumHTLC { amount_msat } => *amount_msat,
1060+
EffectiveCapacity::AdvertisedMaxHTLC { amount_msat } => *amount_msat,
10561061
EffectiveCapacity::Total { capacity_msat, .. } => *capacity_msat,
1062+
EffectiveCapacity::HintMaxHTLC { amount_msat } => *amount_msat,
10571063
EffectiveCapacity::Infinite => u64::max_value(),
10581064
EffectiveCapacity::Unknown => UNKNOWN_CHANNEL_CAPACITY_MSAT,
10591065
}
@@ -1585,7 +1591,7 @@ impl<L: Deref> NetworkGraph<L> where L::Target: Logger {
15851591

15861592
if msg.chain_hash != self.genesis_hash {
15871593
return Err(LightningError {
1588-
err: "Channel announcement chain hash does not match genesis hash".to_owned(),
1594+
err: "Channel announcement chain hash does not match genesis hash".to_owned(),
15891595
action: ErrorAction::IgnoreAndLog(Level::Debug),
15901596
});
15911597
}

lightning/src/routing/router.rs

Lines changed: 194 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -951,7 +951,10 @@ impl<'a> CandidateRouteHop<'a> {
951951
liquidity_msat: details.next_outbound_htlc_limit_msat,
952952
},
953953
CandidateRouteHop::PublicHop { info, .. } => info.effective_capacity(),
954-
CandidateRouteHop::PrivateHop { .. } => EffectiveCapacity::Infinite,
954+
CandidateRouteHop::PrivateHop { hint: RouteHintHop { htlc_maximum_msat: Some(max), .. }} =>
955+
EffectiveCapacity::HintMaxHTLC { amount_msat: *max },
956+
CandidateRouteHop::PrivateHop { hint: RouteHintHop { htlc_maximum_msat: None, .. }} =>
957+
EffectiveCapacity::Infinite,
955958
}
956959
}
957960
}
@@ -963,8 +966,11 @@ fn max_htlc_from_capacity(capacity: EffectiveCapacity, max_channel_saturation_po
963966
EffectiveCapacity::ExactLiquidity { liquidity_msat } => liquidity_msat,
964967
EffectiveCapacity::Infinite => u64::max_value(),
965968
EffectiveCapacity::Unknown => EffectiveCapacity::Unknown.as_msat(),
966-
EffectiveCapacity::MaximumHTLC { amount_msat } =>
969+
EffectiveCapacity::AdvertisedMaxHTLC { amount_msat } =>
967970
amount_msat.checked_shr(saturation_shift).unwrap_or(0),
971+
// Treat htlc_maximum_msat from a route hint as an exact liquidity amount, since the invoice is
972+
// expected to have been generated from up-to-date capacity information.
973+
EffectiveCapacity::HintMaxHTLC { amount_msat } => amount_msat,
968974
EffectiveCapacity::Total { capacity_msat, htlc_maximum_msat } =>
969975
cmp::min(capacity_msat.checked_shr(saturation_shift).unwrap_or(0), htlc_maximum_msat),
970976
}
@@ -1192,6 +1198,41 @@ impl fmt::Display for LoggedPayeePubkey {
11921198
}
11931199
}
11941200

1201+
#[inline]
1202+
fn sort_first_hop_channels(
1203+
channels: &mut Vec<&ChannelDetails>, used_channel_liquidities: &HashMap<(u64, bool), u64>,
1204+
recommended_value_msat: u64, our_node_pubkey: &PublicKey
1205+
) {
1206+
// Sort the first_hops channels to the same node(s) in priority order of which channel we'd
1207+
// most like to use.
1208+
//
1209+
// First, if channels are below `recommended_value_msat`, sort them in descending order,
1210+
// preferring larger channels to avoid splitting the payment into more MPP parts than is
1211+
// required.
1212+
//
1213+
// Second, because simply always sorting in descending order would always use our largest
1214+
// available outbound capacity, needlessly fragmenting our available channel capacities,
1215+
// sort channels above `recommended_value_msat` in ascending order, preferring channels
1216+
// which have enough, but not too much, capacity for the payment.
1217+
//
1218+
// Available outbound balances factor in liquidity already reserved for previously found paths.
1219+
channels.sort_unstable_by(|chan_a, chan_b| {
1220+
let chan_a_outbound_limit_msat = chan_a.next_outbound_htlc_limit_msat
1221+
.saturating_sub(*used_channel_liquidities.get(&(chan_a.get_outbound_payment_scid().unwrap(),
1222+
our_node_pubkey < &chan_a.counterparty.node_id)).unwrap_or(&0));
1223+
let chan_b_outbound_limit_msat = chan_b.next_outbound_htlc_limit_msat
1224+
.saturating_sub(*used_channel_liquidities.get(&(chan_b.get_outbound_payment_scid().unwrap(),
1225+
our_node_pubkey < &chan_b.counterparty.node_id)).unwrap_or(&0));
1226+
if chan_b_outbound_limit_msat < recommended_value_msat || chan_a_outbound_limit_msat < recommended_value_msat {
1227+
// Sort in descending order
1228+
chan_b_outbound_limit_msat.cmp(&chan_a_outbound_limit_msat)
1229+
} else {
1230+
// Sort in ascending order
1231+
chan_a_outbound_limit_msat.cmp(&chan_b_outbound_limit_msat)
1232+
}
1233+
});
1234+
}
1235+
11951236
/// Finds a route from us (payer) to the given target node (payee).
11961237
///
11971238
/// If the payee provided features in their invoice, they should be provided via `params.payee`.
@@ -1437,26 +1478,8 @@ where L::Target: Logger {
14371478
let mut already_collected_value_msat = 0;
14381479

14391480
for (_, channels) in first_hop_targets.iter_mut() {
1440-
// Sort the first_hops channels to the same node(s) in priority order of which channel we'd
1441-
// most like to use.
1442-
//
1443-
// First, if channels are below `recommended_value_msat`, sort them in descending order,
1444-
// preferring larger channels to avoid splitting the payment into more MPP parts than is
1445-
// required.
1446-
//
1447-
// Second, because simply always sorting in descending order would always use our largest
1448-
// available outbound capacity, needlessly fragmenting our available channel capacities,
1449-
// sort channels above `recommended_value_msat` in ascending order, preferring channels
1450-
// which have enough, but not too much, capacity for the payment.
1451-
channels.sort_unstable_by(|chan_a, chan_b| {
1452-
if chan_b.next_outbound_htlc_limit_msat < recommended_value_msat || chan_a.next_outbound_htlc_limit_msat < recommended_value_msat {
1453-
// Sort in descending order
1454-
chan_b.next_outbound_htlc_limit_msat.cmp(&chan_a.next_outbound_htlc_limit_msat)
1455-
} else {
1456-
// Sort in ascending order
1457-
chan_a.next_outbound_htlc_limit_msat.cmp(&chan_b.next_outbound_htlc_limit_msat)
1458-
}
1459-
});
1481+
sort_first_hop_channels(channels, &used_channel_liquidities, recommended_value_msat,
1482+
our_node_pubkey);
14601483
}
14611484

14621485
log_trace!(logger, "Building path from {} to payer {} for value {} msat.",
@@ -1470,8 +1493,9 @@ where L::Target: Logger {
14701493
( $candidate: expr, $src_node_id: expr, $dest_node_id: expr, $next_hops_fee_msat: expr,
14711494
$next_hops_value_contribution: expr, $next_hops_path_htlc_minimum_msat: expr,
14721495
$next_hops_path_penalty_msat: expr, $next_hops_cltv_delta: expr, $next_hops_path_length: expr ) => { {
1473-
// We "return" whether we updated the path at the end, via this:
1474-
let mut did_add_update_path_to_src_node = false;
1496+
// We "return" whether we updated the path at the end, and how much we can route via
1497+
// this channel, via this:
1498+
let mut did_add_update_path_to_src_node = None;
14751499
// Channels to self should not be used. This is more of belt-and-suspenders, because in
14761500
// practice these cases should be caught earlier:
14771501
// - for regular channels at channel announcement (TODO)
@@ -1652,7 +1676,7 @@ where L::Target: Logger {
16521676
{
16531677
old_entry.value_contribution_msat = value_contribution_msat;
16541678
}
1655-
did_add_update_path_to_src_node = true;
1679+
did_add_update_path_to_src_node = Some(value_contribution_msat);
16561680
} else if old_entry.was_processed && new_cost < old_cost {
16571681
#[cfg(all(not(ldk_bench), any(test, fuzzing)))]
16581682
{
@@ -1773,7 +1797,7 @@ where L::Target: Logger {
17731797
for details in first_channels {
17741798
let candidate = CandidateRouteHop::FirstHop { details };
17751799
let added = add_entry!(candidate, our_node_id, payee, 0, path_value_msat,
1776-
0, 0u64, 0, 0);
1800+
0, 0u64, 0, 0).is_some();
17771801
log_trace!(logger, "{} direct route to payee via SCID {}",
17781802
if added { "Added" } else { "Skipped" }, candidate.short_channel_id());
17791803
}
@@ -1820,6 +1844,7 @@ where L::Target: Logger {
18201844
let mut aggregate_next_hops_path_penalty_msat: u64 = 0;
18211845
let mut aggregate_next_hops_cltv_delta: u32 = 0;
18221846
let mut aggregate_next_hops_path_length: u8 = 0;
1847+
let mut aggregate_path_contribution_msat = path_value_msat;
18231848

18241849
for (idx, (hop, prev_hop_id)) in hop_iter.zip(prev_hop_iter).enumerate() {
18251850
let source = NodeId::from_pubkey(&hop.src_node_id);
@@ -1833,10 +1858,13 @@ where L::Target: Logger {
18331858
})
18341859
.unwrap_or_else(|| CandidateRouteHop::PrivateHop { hint: hop });
18351860

1836-
if !add_entry!(candidate, source, target, aggregate_next_hops_fee_msat,
1837-
path_value_msat, aggregate_next_hops_path_htlc_minimum_msat,
1838-
aggregate_next_hops_path_penalty_msat,
1839-
aggregate_next_hops_cltv_delta, aggregate_next_hops_path_length) {
1861+
if let Some(hop_used_msat) = add_entry!(candidate, source, target,
1862+
aggregate_next_hops_fee_msat, aggregate_path_contribution_msat,
1863+
aggregate_next_hops_path_htlc_minimum_msat, aggregate_next_hops_path_penalty_msat,
1864+
aggregate_next_hops_cltv_delta, aggregate_next_hops_path_length)
1865+
{
1866+
aggregate_path_contribution_msat = hop_used_msat;
1867+
} else {
18401868
// If this hop was not used then there is no use checking the preceding
18411869
// hops in the RouteHint. We can break by just searching for a direct
18421870
// channel between last checked hop and first_hop_targets.
@@ -1863,14 +1891,15 @@ where L::Target: Logger {
18631891
.saturating_add(1);
18641892

18651893
// Searching for a direct channel between last checked hop and first_hop_targets
1866-
if let Some(first_channels) = first_hop_targets.get(&NodeId::from_pubkey(&prev_hop_id)) {
1894+
if let Some(first_channels) = first_hop_targets.get_mut(&NodeId::from_pubkey(&prev_hop_id)) {
1895+
sort_first_hop_channels(first_channels, &used_channel_liquidities,
1896+
recommended_value_msat, our_node_pubkey);
18671897
for details in first_channels {
1868-
let candidate = CandidateRouteHop::FirstHop { details };
1869-
add_entry!(candidate, our_node_id, NodeId::from_pubkey(&prev_hop_id),
1870-
aggregate_next_hops_fee_msat, path_value_msat,
1871-
aggregate_next_hops_path_htlc_minimum_msat,
1872-
aggregate_next_hops_path_penalty_msat, aggregate_next_hops_cltv_delta,
1873-
aggregate_next_hops_path_length);
1898+
let first_hop_candidate = CandidateRouteHop::FirstHop { details };
1899+
add_entry!(first_hop_candidate, our_node_id, NodeId::from_pubkey(&prev_hop_id),
1900+
aggregate_next_hops_fee_msat, aggregate_path_contribution_msat,
1901+
aggregate_next_hops_path_htlc_minimum_msat, aggregate_next_hops_path_penalty_msat,
1902+
aggregate_next_hops_cltv_delta, aggregate_next_hops_path_length);
18741903
}
18751904
}
18761905

@@ -1903,12 +1932,15 @@ where L::Target: Logger {
19031932
// Note that we *must* check if the last hop was added as `add_entry`
19041933
// always assumes that the third argument is a node to which we have a
19051934
// path.
1906-
if let Some(first_channels) = first_hop_targets.get(&NodeId::from_pubkey(&hop.src_node_id)) {
1935+
if let Some(first_channels) = first_hop_targets.get_mut(&NodeId::from_pubkey(&hop.src_node_id)) {
1936+
sort_first_hop_channels(first_channels, &used_channel_liquidities,
1937+
recommended_value_msat, our_node_pubkey);
19071938
for details in first_channels {
1908-
let candidate = CandidateRouteHop::FirstHop { details };
1909-
add_entry!(candidate, our_node_id,
1939+
let first_hop_candidate = CandidateRouteHop::FirstHop { details };
1940+
add_entry!(first_hop_candidate, our_node_id,
19101941
NodeId::from_pubkey(&hop.src_node_id),
1911-
aggregate_next_hops_fee_msat, path_value_msat,
1942+
aggregate_next_hops_fee_msat,
1943+
aggregate_path_contribution_msat,
19121944
aggregate_next_hops_path_htlc_minimum_msat,
19131945
aggregate_next_hops_path_penalty_msat,
19141946
aggregate_next_hops_cltv_delta,
@@ -5892,6 +5924,127 @@ mod tests {
58925924
assert!(route.is_ok());
58935925
}
58945926

5927+
#[test]
5928+
fn abide_by_route_hint_max_htlc() {
5929+
// Check that we abide by any htlc_maximum_msat provided in the route hints of the payment
5930+
// params in the final route.
5931+
let (secp_ctx, network_graph, _, _, logger) = build_graph();
5932+
let netgraph = network_graph.read_only();
5933+
let (_, our_id, _, nodes) = get_nodes(&secp_ctx);
5934+
let scorer = ln_test_utils::TestScorer::new();
5935+
let keys_manager = ln_test_utils::TestKeysInterface::new(&[0u8; 32], Network::Testnet);
5936+
let random_seed_bytes = keys_manager.get_secure_random_bytes();
5937+
let config = UserConfig::default();
5938+
5939+
let max_htlc_msat = 50_000;
5940+
let route_hint_1 = RouteHint(vec![RouteHintHop {
5941+
src_node_id: nodes[2],
5942+
short_channel_id: 42,
5943+
fees: RoutingFees {
5944+
base_msat: 100,
5945+
proportional_millionths: 0,
5946+
},
5947+
cltv_expiry_delta: 10,
5948+
htlc_minimum_msat: None,
5949+
htlc_maximum_msat: Some(max_htlc_msat),
5950+
}]);
5951+
let dest_node_id = ln_test_utils::pubkey(42);
5952+
let payment_params = PaymentParameters::from_node_id(dest_node_id, 42)
5953+
.with_route_hints(vec![route_hint_1.clone()]).unwrap()
5954+
.with_bolt11_features(channelmanager::provided_invoice_features(&config)).unwrap();
5955+
5956+
// Make sure we'll error if our route hints don't have enough liquidity according to their
5957+
// htlc_maximum_msat.
5958+
if let Err(LightningError{err, action: ErrorAction::IgnoreError}) = get_route(&our_id,
5959+
&payment_params, &netgraph, None, max_htlc_msat + 1, Arc::clone(&logger), &scorer, &(),
5960+
&random_seed_bytes)
5961+
{
5962+
assert_eq!(err, "Failed to find a sufficient route to the given destination");
5963+
} else { panic!(); }
5964+
5965+
// Make sure we'll split an MPP payment across route hints if their htlc_maximum_msat warrants.
5966+
let mut route_hint_2 = route_hint_1.clone();
5967+
route_hint_2.0[0].short_channel_id = 43;
5968+
let payment_params = PaymentParameters::from_node_id(dest_node_id, 42)
5969+
.with_route_hints(vec![route_hint_1, route_hint_2]).unwrap()
5970+
.with_bolt11_features(channelmanager::provided_invoice_features(&config)).unwrap();
5971+
let route = get_route(&our_id, &payment_params, &netgraph, None, max_htlc_msat + 1,
5972+
Arc::clone(&logger), &scorer, &(), &random_seed_bytes).unwrap();
5973+
assert_eq!(route.paths.len(), 2);
5974+
assert!(route.paths[0].hops.last().unwrap().fee_msat <= max_htlc_msat);
5975+
assert!(route.paths[1].hops.last().unwrap().fee_msat <= max_htlc_msat);
5976+
}
5977+
5978+
#[test]
5979+
fn direct_channel_to_hints_with_max_htlc() {
5980+
// Check that if we have a first hop channel peer that's connected to multiple provided route
5981+
// hints, that we properly split the payment between the route hints if needed.
5982+
let logger = Arc::new(ln_test_utils::TestLogger::new());
5983+
let network_graph = Arc::new(NetworkGraph::new(Network::Testnet, Arc::clone(&logger)));
5984+
let scorer = ln_test_utils::TestScorer::new();
5985+
let keys_manager = ln_test_utils::TestKeysInterface::new(&[0u8; 32], Network::Testnet);
5986+
let random_seed_bytes = keys_manager.get_secure_random_bytes();
5987+
let config = UserConfig::default();
5988+
5989+
let our_node_id = ln_test_utils::pubkey(42);
5990+
let intermed_node_id = ln_test_utils::pubkey(43);
5991+
let first_hop = vec![get_channel_details(Some(42), intermed_node_id, InitFeatures::from_le_bytes(vec![0b11]), 10_000_000)];
5992+
5993+
let amt_msat = 900_000;
5994+
let max_htlc_msat = 500_000;
5995+
let route_hint_1 = RouteHint(vec![RouteHintHop {
5996+
src_node_id: intermed_node_id,
5997+
short_channel_id: 44,
5998+
fees: RoutingFees {
5999+
base_msat: 100,
6000+
proportional_millionths: 0,
6001+
},
6002+
cltv_expiry_delta: 10,
6003+
htlc_minimum_msat: None,
6004+
htlc_maximum_msat: Some(max_htlc_msat),
6005+
}, RouteHintHop {
6006+
src_node_id: intermed_node_id,
6007+
short_channel_id: 45,
6008+
fees: RoutingFees {
6009+
base_msat: 100,
6010+
proportional_millionths: 0,
6011+
},
6012+
cltv_expiry_delta: 10,
6013+
htlc_minimum_msat: None,
6014+
// Check that later route hint max htlcs don't override earlier ones
6015+
htlc_maximum_msat: Some(max_htlc_msat - 50),
6016+
}]);
6017+
let mut route_hint_2 = route_hint_1.clone();
6018+
route_hint_2.0[0].short_channel_id = 46;
6019+
route_hint_2.0[1].short_channel_id = 47;
6020+
let dest_node_id = ln_test_utils::pubkey(44);
6021+
let payment_params = PaymentParameters::from_node_id(dest_node_id, 42)
6022+
.with_route_hints(vec![route_hint_1, route_hint_2]).unwrap()
6023+
.with_bolt11_features(channelmanager::provided_invoice_features(&config)).unwrap();
6024+
6025+
let route = get_route(&our_node_id, &payment_params, &network_graph.read_only(),
6026+
Some(&first_hop.iter().collect::<Vec<_>>()), amt_msat, Arc::clone(&logger), &scorer, &(),
6027+
&random_seed_bytes).unwrap();
6028+
assert_eq!(route.paths.len(), 2);
6029+
assert!(route.paths[0].hops.last().unwrap().fee_msat <= max_htlc_msat);
6030+
assert!(route.paths[1].hops.last().unwrap().fee_msat <= max_htlc_msat);
6031+
assert_eq!(route.get_total_amount(), amt_msat);
6032+
6033+
// Re-run but with two first hop channels connected to the same route hint peers that must be
6034+
// split between.
6035+
let first_hops = vec![
6036+
get_channel_details(Some(42), intermed_node_id, InitFeatures::from_le_bytes(vec![0b11]), amt_msat - 10),
6037+
get_channel_details(Some(43), intermed_node_id, InitFeatures::from_le_bytes(vec![0b11]), amt_msat - 10),
6038+
];
6039+
let route = get_route(&our_node_id, &payment_params, &network_graph.read_only(),
6040+
Some(&first_hops.iter().collect::<Vec<_>>()), amt_msat, Arc::clone(&logger), &scorer, &(),
6041+
&random_seed_bytes).unwrap();
6042+
assert_eq!(route.paths.len(), 2);
6043+
assert!(route.paths[0].hops.last().unwrap().fee_msat <= max_htlc_msat);
6044+
assert!(route.paths[1].hops.last().unwrap().fee_msat <= max_htlc_msat);
6045+
assert_eq!(route.get_total_amount(), amt_msat);
6046+
}
6047+
58956048
#[test]
58966049
fn blinded_route_ser() {
58976050
let blinded_path_1 = BlindedPath {

lightning/src/routing/scoring.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1243,8 +1243,10 @@ impl<G: Deref<Target = NetworkGraph<L>>, L: Deref, T: Time> Score for Probabilis
12431243

12441244
let mut anti_probing_penalty_msat = 0;
12451245
match usage.effective_capacity {
1246-
EffectiveCapacity::ExactLiquidity { liquidity_msat } => {
1247-
if usage.amount_msat > liquidity_msat {
1246+
EffectiveCapacity::ExactLiquidity { liquidity_msat: amount_msat } |
1247+
EffectiveCapacity::HintMaxHTLC { amount_msat } =>
1248+
{
1249+
if usage.amount_msat > amount_msat {
12481250
return u64::max_value();
12491251
} else {
12501252
return base_penalty_msat;
@@ -2869,7 +2871,7 @@ mod tests {
28692871
let usage = ChannelUsage {
28702872
amount_msat: 1,
28712873
inflight_htlc_msat: 0,
2872-
effective_capacity: EffectiveCapacity::MaximumHTLC { amount_msat: 0 },
2874+
effective_capacity: EffectiveCapacity::AdvertisedMaxHTLC { amount_msat: 0 },
28732875
};
28742876
assert_eq!(scorer.channel_penalty_msat(42, &target, &source, usage, &params), 2048);
28752877

0 commit comments

Comments
 (0)