Skip to content

Commit 14cd18b

Browse files
Abide by route hint max_htlc in pathfinding
1 parent 88821cb commit 14cd18b

File tree

3 files changed

+163
-20
lines changed

3 files changed

+163
-20
lines changed

lightning/src/routing/gossip.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1033,6 +1033,11 @@ pub enum EffectiveCapacity {
10331033
/// A capacity sufficient to route any payment, typically used for private channels provided by
10341034
/// an invoice.
10351035
Infinite,
1036+
/// The maximum HTLC amount as provided by an invoice route hint.
1037+
HintMaxHTLC {
1038+
/// The maximum HTLC amount denominated in millisatoshi.
1039+
amount_msat: u64,
1040+
},
10361041
/// A capacity that is unknown possibly because either the chain state is unavailable to know
10371042
/// the total capacity or the `htlc_maximum_msat` was not advertised on the gossip network.
10381043
Unknown,
@@ -1049,6 +1054,7 @@ impl EffectiveCapacity {
10491054
EffectiveCapacity::ExactLiquidity { liquidity_msat } => *liquidity_msat,
10501055
EffectiveCapacity::AdvertisedMaxHTLC { amount_msat } => *amount_msat,
10511056
EffectiveCapacity::Total { capacity_msat, .. } => *capacity_msat,
1057+
EffectiveCapacity::HintMaxHTLC { amount_msat } => *amount_msat,
10521058
EffectiveCapacity::Infinite => u64::max_value(),
10531059
EffectiveCapacity::Unknown => UNKNOWN_CHANNEL_CAPACITY_MSAT,
10541060
}

lightning/src/routing/router.rs

Lines changed: 153 additions & 18 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
}
@@ -965,6 +968,9 @@ fn max_htlc_from_capacity(capacity: EffectiveCapacity, max_channel_saturation_po
965968
EffectiveCapacity::Unknown => EffectiveCapacity::Unknown.as_msat(),
966969
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
}
@@ -1470,8 +1476,9 @@ where L::Target: Logger {
14701476
( $candidate: expr, $src_node_id: expr, $dest_node_id: expr, $next_hops_fee_msat: expr,
14711477
$next_hops_value_contribution: expr, $next_hops_path_htlc_minimum_msat: expr,
14721478
$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;
1479+
// We "return" whether we updated the path at the end, and how much we can route via
1480+
// this channel, via this:
1481+
let mut did_add_update_path_to_src_node = None;
14751482
// Channels to self should not be used. This is more of belt-and-suspenders, because in
14761483
// practice these cases should be caught earlier:
14771484
// - for regular channels at channel announcement (TODO)
@@ -1652,7 +1659,7 @@ where L::Target: Logger {
16521659
{
16531660
old_entry.value_contribution_msat = value_contribution_msat;
16541661
}
1655-
did_add_update_path_to_src_node = true;
1662+
did_add_update_path_to_src_node = Some(value_contribution_msat);
16561663
} else if old_entry.was_processed && new_cost < old_cost {
16571664
#[cfg(all(not(feature = "_bench_unstable"), any(test, fuzzing)))]
16581665
{
@@ -1773,7 +1780,7 @@ where L::Target: Logger {
17731780
for details in first_channels {
17741781
let candidate = CandidateRouteHop::FirstHop { details };
17751782
let added = add_entry!(candidate, our_node_id, payee, 0, path_value_msat,
1776-
0, 0u64, 0, 0);
1783+
0, 0u64, 0, 0).is_some();
17771784
log_trace!(logger, "{} direct route to payee via SCID {}",
17781785
if added { "Added" } else { "Skipped" }, candidate.short_channel_id());
17791786
}
@@ -1820,6 +1827,7 @@ where L::Target: Logger {
18201827
let mut aggregate_next_hops_path_penalty_msat: u64 = 0;
18211828
let mut aggregate_next_hops_cltv_delta: u32 = 0;
18221829
let mut aggregate_next_hops_path_length: u8 = 0;
1830+
let mut aggregate_path_contribution_msat = path_value_msat;
18231831

18241832
for (idx, (hop, prev_hop_id)) in hop_iter.zip(prev_hop_iter).enumerate() {
18251833
let source = NodeId::from_pubkey(&hop.src_node_id);
@@ -1833,10 +1841,13 @@ where L::Target: Logger {
18331841
})
18341842
.unwrap_or_else(|| CandidateRouteHop::PrivateHop { hint: hop });
18351843

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) {
1844+
if let Some(hop_used_msat) = add_entry!(candidate, source, target,
1845+
aggregate_next_hops_fee_msat, aggregate_path_contribution_msat,
1846+
aggregate_next_hops_path_htlc_minimum_msat, aggregate_next_hops_path_penalty_msat,
1847+
aggregate_next_hops_cltv_delta, aggregate_next_hops_path_length)
1848+
{
1849+
aggregate_path_contribution_msat = hop_used_msat;
1850+
} else {
18401851
// If this hop was not used then there is no use checking the preceding
18411852
// hops in the RouteHint. We can break by just searching for a direct
18421853
// channel between last checked hop and first_hop_targets.
@@ -1865,12 +1876,11 @@ where L::Target: Logger {
18651876
// Searching for a direct channel between last checked hop and first_hop_targets
18661877
if let Some(first_channels) = first_hop_targets.get(&NodeId::from_pubkey(&prev_hop_id)) {
18671878
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);
1879+
let first_hop_candidate = CandidateRouteHop::FirstHop { details };
1880+
add_entry!(first_hop_candidate, our_node_id, NodeId::from_pubkey(&prev_hop_id),
1881+
aggregate_next_hops_fee_msat, aggregate_path_contribution_msat,
1882+
aggregate_next_hops_path_htlc_minimum_msat, aggregate_next_hops_path_penalty_msat,
1883+
aggregate_next_hops_cltv_delta, aggregate_next_hops_path_length);
18741884
}
18751885
}
18761886

@@ -1905,10 +1915,11 @@ where L::Target: Logger {
19051915
// path.
19061916
if let Some(first_channels) = first_hop_targets.get(&NodeId::from_pubkey(&hop.src_node_id)) {
19071917
for details in first_channels {
1908-
let candidate = CandidateRouteHop::FirstHop { details };
1909-
add_entry!(candidate, our_node_id,
1918+
let first_hop_candidate = CandidateRouteHop::FirstHop { details };
1919+
add_entry!(first_hop_candidate, our_node_id,
19101920
NodeId::from_pubkey(&hop.src_node_id),
1911-
aggregate_next_hops_fee_msat, path_value_msat,
1921+
aggregate_next_hops_fee_msat,
1922+
aggregate_path_contribution_msat,
19121923
aggregate_next_hops_path_htlc_minimum_msat,
19131924
aggregate_next_hops_path_penalty_msat,
19141925
aggregate_next_hops_cltv_delta,
@@ -5906,6 +5917,130 @@ mod tests {
59065917
assert!(route.is_ok());
59075918
}
59085919

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

lightning/src/routing/scoring.rs

Lines changed: 4 additions & 2 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;

0 commit comments

Comments
 (0)