Skip to content

Commit

Permalink
Abide by route hint max_htlc in pathfinding
Browse files Browse the repository at this point in the history
  • Loading branch information
valentinewallace committed May 18, 2023
1 parent 88821cb commit 686f4e1
Show file tree
Hide file tree
Showing 3 changed files with 144 additions and 17 deletions.
6 changes: 6 additions & 0 deletions lightning/src/routing/gossip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1033,6 +1033,11 @@ pub enum EffectiveCapacity {
/// A capacity sufficient to route any payment, typically used for private channels provided by
/// an invoice.
Infinite,
/// The maximum HTLC amount as provided by an invoice route hint.
HintMaxHTLC {
/// The maximum HTLC amount denominated in millisatoshi.
amount_msat: u64,
},
/// A capacity that is unknown possibly because either the chain state is unavailable to know
/// the total capacity or the `htlc_maximum_msat` was not advertised on the gossip network.
Unknown,
Expand All @@ -1049,6 +1054,7 @@ impl EffectiveCapacity {
EffectiveCapacity::ExactLiquidity { liquidity_msat } => *liquidity_msat,
EffectiveCapacity::AdvertisedMaxHTLC { amount_msat } => *amount_msat,
EffectiveCapacity::Total { capacity_msat, .. } => *capacity_msat,
EffectiveCapacity::HintMaxHTLC { amount_msat } => *amount_msat,
EffectiveCapacity::Infinite => u64::max_value(),
EffectiveCapacity::Unknown => UNKNOWN_CHANNEL_CAPACITY_MSAT,
}
Expand Down
149 changes: 134 additions & 15 deletions lightning/src/routing/router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -951,7 +951,10 @@ impl<'a> CandidateRouteHop<'a> {
liquidity_msat: details.next_outbound_htlc_limit_msat,
},
CandidateRouteHop::PublicHop { info, .. } => info.effective_capacity(),
CandidateRouteHop::PrivateHop { .. } => EffectiveCapacity::Infinite,
CandidateRouteHop::PrivateHop { hint } => {
hint.htlc_maximum_msat.map_or(EffectiveCapacity::Infinite,
|max| EffectiveCapacity::HintMaxHTLC { amount_msat: max })
},
}
}
}
Expand All @@ -965,6 +968,7 @@ fn max_htlc_from_capacity(capacity: EffectiveCapacity, max_channel_saturation_po
EffectiveCapacity::Unknown => EffectiveCapacity::Unknown.as_msat(),
EffectiveCapacity::AdvertisedMaxHTLC { amount_msat } =>
amount_msat.checked_shr(saturation_shift).unwrap_or(0),
EffectiveCapacity::HintMaxHTLC { amount_msat } => amount_msat,
EffectiveCapacity::Total { capacity_msat, htlc_maximum_msat } =>
cmp::min(capacity_msat.checked_shr(saturation_shift).unwrap_or(0), htlc_maximum_msat),
}
Expand Down Expand Up @@ -1833,18 +1837,20 @@ where L::Target: Logger {
})
.unwrap_or_else(|| CandidateRouteHop::PrivateHop { hint: hop });

let used_liquidity_msat = used_channel_liquidities
.get(&(hop.short_channel_id, source < target)).copied().unwrap_or(0);
let hint_candidate_contribution_msat = cmp::min(path_value_msat,
candidate.effective_capacity().as_msat().saturating_sub(used_liquidity_msat));
if !add_entry!(candidate, source, target, aggregate_next_hops_fee_msat,
path_value_msat, aggregate_next_hops_path_htlc_minimum_msat,
aggregate_next_hops_path_penalty_msat,
aggregate_next_hops_cltv_delta, aggregate_next_hops_path_length) {
hint_candidate_contribution_msat, aggregate_next_hops_path_htlc_minimum_msat,
aggregate_next_hops_path_penalty_msat, aggregate_next_hops_cltv_delta,
aggregate_next_hops_path_length) {
// 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;
}

let used_liquidity_msat = used_channel_liquidities
.get(&(hop.short_channel_id, source < target)).copied().unwrap_or(0);
let channel_usage = ChannelUsage {
amount_msat: final_value_msat + aggregate_next_hops_fee_msat,
inflight_htlc_msat: used_liquidity_msat,
Expand All @@ -1865,12 +1871,11 @@ where L::Target: Logger {
// Searching for a direct channel between last checked hop and first_hop_targets
if let Some(first_channels) = first_hop_targets.get(&NodeId::from_pubkey(&prev_hop_id)) {
for details in first_channels {
let candidate = CandidateRouteHop::FirstHop { details };
add_entry!(candidate, our_node_id, NodeId::from_pubkey(&prev_hop_id),
aggregate_next_hops_fee_msat, path_value_msat,
aggregate_next_hops_path_htlc_minimum_msat,
aggregate_next_hops_path_penalty_msat, aggregate_next_hops_cltv_delta,
aggregate_next_hops_path_length);
let first_hop_candidate = CandidateRouteHop::FirstHop { details };
add_entry!(first_hop_candidate, our_node_id, NodeId::from_pubkey(&prev_hop_id),
aggregate_next_hops_fee_msat, hint_candidate_contribution_msat,
aggregate_next_hops_path_htlc_minimum_msat, aggregate_next_hops_path_penalty_msat,
aggregate_next_hops_cltv_delta, aggregate_next_hops_path_length);
}
}

Expand Down Expand Up @@ -1905,10 +1910,11 @@ where L::Target: Logger {
// path.
if let Some(first_channels) = first_hop_targets.get(&NodeId::from_pubkey(&hop.src_node_id)) {
for details in first_channels {
let candidate = CandidateRouteHop::FirstHop { details };
add_entry!(candidate, our_node_id,
let first_hop_candidate = CandidateRouteHop::FirstHop { details };
add_entry!(first_hop_candidate, our_node_id,
NodeId::from_pubkey(&hop.src_node_id),
aggregate_next_hops_fee_msat, path_value_msat,
aggregate_next_hops_fee_msat,
hint_candidate_contribution_msat,
aggregate_next_hops_path_htlc_minimum_msat,
aggregate_next_hops_path_penalty_msat,
aggregate_next_hops_cltv_delta,
Expand Down Expand Up @@ -5906,6 +5912,119 @@ mod tests {
assert!(route.is_ok());
}

#[test]
fn respect_route_hint_max_htlc() {
// Make sure that any max_htlc provided in the route hints of the payment params is respected in
// the final route.
let (secp_ctx, network_graph, _, _, logger) = build_graph();
let netgraph = network_graph.read_only();
let (_, our_id, _, nodes) = get_nodes(&secp_ctx);
let scorer = ln_test_utils::TestScorer::new();
let keys_manager = ln_test_utils::TestKeysInterface::new(&[0u8; 32], Network::Testnet);
let random_seed_bytes = keys_manager.get_secure_random_bytes();
let config = UserConfig::default();

let max_htlc_msat = 50_000;
let route_hint_1 = RouteHint(vec![RouteHintHop {
src_node_id: nodes[2],
short_channel_id: 42,
fees: RoutingFees {
base_msat: 100,
proportional_millionths: 0,
},
cltv_expiry_delta: 10,
htlc_minimum_msat: None,
htlc_maximum_msat: Some(max_htlc_msat),
}]);
let dest_node_id = ln_test_utils::pubkey(42);
let payment_params = PaymentParameters::from_node_id(dest_node_id, 42)
.with_route_hints(vec![route_hint_1.clone()]).unwrap()
.with_bolt11_features(channelmanager::provided_invoice_features(&config)).unwrap();

// Make sure we'll error if our route hints don't have enough liquidity according to their
// max_htlc.
if let Err(LightningError{err, action: ErrorAction::IgnoreError}) = get_route(&our_id,
&payment_params, &netgraph, None, max_htlc_msat + 1, Arc::clone(&logger), &scorer, &(),
&random_seed_bytes)
{
assert_eq!(err, "Failed to find a sufficient route to the given destination");
} else { panic!(); }

// Make sure we'll split an MPP payment across route hints if their max_htlcs warrant it.
let mut route_hint_2 = route_hint_1.clone();
route_hint_2.0[0].short_channel_id = 43;
let payment_params = PaymentParameters::from_node_id(dest_node_id, 42)
.with_route_hints(vec![route_hint_1, route_hint_2]).unwrap()
.with_bolt11_features(channelmanager::provided_invoice_features(&config)).unwrap();
let route = get_route(&our_id, &payment_params, &netgraph, None, max_htlc_msat + 1,
Arc::clone(&logger), &scorer, &(), &random_seed_bytes).unwrap();
assert_eq!(route.paths.len(), 2);
assert!(route.paths[0].hops.last().unwrap().fee_msat <= max_htlc_msat);
assert!(route.paths[1].hops.last().unwrap().fee_msat <= max_htlc_msat);
}

#[test]
fn direct_channel_to_hints_with_max_htlc() {
// Check that if we have a first hop channel peer that's connected to multiple provided route
// hints, that we properly split the payment between the route hints if needed.
let logger = Arc::new(ln_test_utils::TestLogger::new());
let network_graph = Arc::new(NetworkGraph::new(Network::Testnet, Arc::clone(&logger)));
let scorer = ln_test_utils::TestScorer::new();
let keys_manager = ln_test_utils::TestKeysInterface::new(&[0u8; 32], Network::Testnet);
let random_seed_bytes = keys_manager.get_secure_random_bytes();
let config = UserConfig::default();

let our_node_id = ln_test_utils::pubkey(42);
let intermed_node_id = ln_test_utils::pubkey(43);
let first_hop = vec![get_channel_details(Some(42), intermed_node_id, InitFeatures::from_le_bytes(vec![0b11]), 10_000_000)];

let amt_msat = 900_000;
let max_htlc_msat = 500_000;
let route_hint_1 = RouteHint(vec![RouteHintHop {
src_node_id: intermed_node_id,
short_channel_id: 44,
fees: RoutingFees {
base_msat: 100,
proportional_millionths: 0,
},
cltv_expiry_delta: 10,
htlc_minimum_msat: None,
htlc_maximum_msat: Some(max_htlc_msat),
}]);
let mut route_hint_2 = route_hint_1.clone();
route_hint_2.0[0].short_channel_id = 45;
route_hint_2.0[0].htlc_maximum_msat = Some(max_htlc_msat);
let dest_node_id = ln_test_utils::pubkey(44);
let payment_params = PaymentParameters::from_node_id(dest_node_id, 42)
.with_route_hints(vec![route_hint_1, route_hint_2]).unwrap()
.with_bolt11_features(channelmanager::provided_invoice_features(&config)).unwrap();

let route = get_route(&our_node_id, &payment_params, &network_graph.read_only(),
Some(&first_hop.iter().collect::<Vec<_>>()), amt_msat, Arc::clone(&logger), &scorer, &(),
&random_seed_bytes).unwrap();
assert_eq!(route.paths.len(), 2);
assert!(route.paths[0].hops.last().unwrap().fee_msat <= max_htlc_msat);
assert!(route.paths[1].hops.last().unwrap().fee_msat <= max_htlc_msat);
assert_eq!(route.get_total_amount(), amt_msat);

// Re-run but with two first hop channels connected to the same route hint peers that must be
// split between.
let first_hops = vec![
get_channel_details(Some(42), intermed_node_id, InitFeatures::from_le_bytes(vec![0b11]), amt_msat - 10),
get_channel_details(Some(43), intermed_node_id, InitFeatures::from_le_bytes(vec![0b11]), amt_msat - 10),
];
let route = get_route(&our_node_id, &payment_params, &network_graph.read_only(),
Some(&first_hops.iter().collect::<Vec<_>>()), amt_msat, Arc::clone(&logger), &scorer, &(),
&random_seed_bytes).unwrap();
// TODO: `get_route` returns a suboptimal route here because first hop channels are not sorted
// in order of available liquidity during pathfinding.
assert_eq!(route.paths.len(), 3);
assert!(route.paths[0].hops.last().unwrap().fee_msat <= max_htlc_msat);
assert!(route.paths[1].hops.last().unwrap().fee_msat <= max_htlc_msat);
assert!(route.paths[2].hops.last().unwrap().fee_msat <= max_htlc_msat);
assert_eq!(route.get_total_amount(), amt_msat);
}

#[test]
fn blinded_route_ser() {
let blinded_path_1 = BlindedPath {
Expand Down
6 changes: 4 additions & 2 deletions lightning/src/routing/scoring.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1243,8 +1243,10 @@ impl<G: Deref<Target = NetworkGraph<L>>, L: Deref, T: Time> Score for Probabilis

let mut anti_probing_penalty_msat = 0;
match usage.effective_capacity {
EffectiveCapacity::ExactLiquidity { liquidity_msat } => {
if usage.amount_msat > liquidity_msat {
EffectiveCapacity::ExactLiquidity { liquidity_msat: amount_msat } |
EffectiveCapacity::HintMaxHTLC { amount_msat } =>
{
if usage.amount_msat > amount_msat {
return u64::max_value();
} else {
return base_penalty_msat;
Expand Down

0 comments on commit 686f4e1

Please sign in to comment.