Skip to content

Commit daaa00c

Browse files
committed
[router] Track full-path htlc-minimum-msat while graph-walking
Previously, we'd happily send funds through a path where, while generating the path, in some middle hope we reduce the value being sent to meet an htlc_maximum, making a later hop invalid due to it no longer meeting its htlc_minimum. Instead, we need to track the path's htlc-minimum while we're transiting the graph.
1 parent 633dcfe commit daaa00c

File tree

1 file changed

+87
-15
lines changed

1 file changed

+87
-15
lines changed

lightning/src/routing/router.rs

Lines changed: 87 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,10 @@ struct RouteGraphNode {
143143
// - how much is needed for a path being constructed
144144
// - how much value can channels following this node (up to the destination) can contribute,
145145
// considering their capacity and fees
146-
value_contribution_msat: u64
146+
value_contribution_msat: u64,
147+
/// The maximum htlc_minimum_msat along the path, taking into consideration the fees required
148+
/// to meet the minimum over the hops required to get there.
149+
path_htlc_minimum_msat: u64,
147150
}
148151

149152
impl cmp::Ord for RouteGraphNode {
@@ -487,7 +490,7 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
487490
// $next_hops_fee_msat represents the fees paid for using all the channel *after* this one,
488491
// since that value has to be transferred over this channel.
489492
( $chan_id: expr, $src_node_id: expr, $dest_node_id: expr, $directional_info: expr, $capacity_sats: expr, $chan_features: expr, $next_hops_fee_msat: expr,
490-
$next_hops_value_contribution: expr ) => {
493+
$next_hops_value_contribution: expr, $incl_fee_next_hops_htlc_minimum_msat: expr ) => {
491494
// Channels to self should not be used. This is more of belt-and-suspenders, because in
492495
// practice these cases should be caught earlier:
493496
// - for regular channels at channel announcement (TODO)
@@ -554,19 +557,27 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
554557
// Can't overflow due to how the values were computed right above.
555558
None => unreachable!(),
556559
};
560+
#[allow(unused_comparisons)] // $incl_fee_next_hops_htlc_minimum_msat is 0 in some calls so rustc complains
561+
let over_path_minimum_msat = amount_to_transfer_over_msat >= $directional_info.htlc_minimum_msat &&
562+
amount_to_transfer_over_msat >= $incl_fee_next_hops_htlc_minimum_msat;
557563

558564
// If HTLC minimum is larger than the amount we're going to transfer, we shouldn't
559565
// bother considering this channel.
560566
// Since we're choosing amount_to_transfer_over_msat as maximum possible, it can
561567
// be only reduced later (not increased), so this channel should just be skipped
562568
// as not sufficient.
563-
if amount_to_transfer_over_msat < $directional_info.htlc_minimum_msat {
569+
if !over_path_minimum_msat {
564570
hit_minimum_limit = true;
565571
} else if contributes_sufficient_value {
566572
// Note that low contribution here (limited by available_liquidity_msat)
567573
// might violate htlc_minimum_msat on the hops which are next along the
568574
// payment path (upstream to the payee). To avoid that, we recompute path
569575
// path fees knowing the final path contribution after constructing it.
576+
let path_htlc_minimum_msat = match compute_fees($incl_fee_next_hops_htlc_minimum_msat, $directional_info.fees)
577+
.map(|fee_msat| fee_msat.checked_add($incl_fee_next_hops_htlc_minimum_msat)) {
578+
Some(Some(value_msat)) => cmp::max(value_msat, $directional_info.htlc_minimum_msat),
579+
_ => u64::max_value()
580+
};
570581
let hm_entry = dist.entry(&$src_node_id);
571582
let old_entry = hm_entry.or_insert_with(|| {
572583
// If there was previously no known way to access
@@ -638,6 +649,7 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
638649
lowest_fee_to_peer_through_node: total_fee_msat,
639650
lowest_fee_to_node: $next_hops_fee_msat as u64 + hop_use_fee_msat,
640651
value_contribution_msat: value_contribution_msat,
652+
path_htlc_minimum_msat,
641653
};
642654

643655
// Update the way of reaching $src_node_id with the given $chan_id (from $dest_node_id),
@@ -697,10 +709,10 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
697709
// meaning how much will be paid in fees after this node (to the best of our knowledge).
698710
// This data can later be helpful to optimize routing (pay lower fees).
699711
macro_rules! add_entries_to_cheapest_to_target_node {
700-
( $node: expr, $node_id: expr, $fee_to_target_msat: expr, $next_hops_value_contribution: expr ) => {
712+
( $node: expr, $node_id: expr, $fee_to_target_msat: expr, $next_hops_value_contribution: expr, $incl_fee_next_hops_htlc_minimum_msat: expr ) => {
701713
if first_hops.is_some() {
702714
if let Some(&(ref first_hop, ref features, ref outbound_capacity_msat)) = first_hop_targets.get(&$node_id) {
703-
add_entry!(first_hop, *our_node_id, $node_id, dummy_directional_info, Some(outbound_capacity_msat / 1000), features.to_context(), $fee_to_target_msat, $next_hops_value_contribution);
715+
add_entry!(first_hop, *our_node_id, $node_id, dummy_directional_info, Some(outbound_capacity_msat / 1000), features.to_context(), $fee_to_target_msat, $next_hops_value_contribution, $incl_fee_next_hops_htlc_minimum_msat);
704716
}
705717
}
706718

@@ -720,15 +732,15 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
720732
if first_hops.is_none() || chan.node_two != *our_node_id {
721733
if let Some(two_to_one) = chan.two_to_one.as_ref() {
722734
if two_to_one.enabled {
723-
add_entry!(chan_id, chan.node_two, chan.node_one, two_to_one, chan.capacity_sats, chan.features, $fee_to_target_msat, $next_hops_value_contribution);
735+
add_entry!(chan_id, chan.node_two, chan.node_one, two_to_one, chan.capacity_sats, chan.features, $fee_to_target_msat, $next_hops_value_contribution, $incl_fee_next_hops_htlc_minimum_msat);
724736
}
725737
}
726738
}
727739
} else {
728740
if first_hops.is_none() || chan.node_one != *our_node_id {
729741
if let Some(one_to_two) = chan.one_to_two.as_ref() {
730742
if one_to_two.enabled {
731-
add_entry!(chan_id, chan.node_one, chan.node_two, one_to_two, chan.capacity_sats, chan.features, $fee_to_target_msat, $next_hops_value_contribution);
743+
add_entry!(chan_id, chan.node_one, chan.node_two, one_to_two, chan.capacity_sats, chan.features, $fee_to_target_msat, $next_hops_value_contribution, $incl_fee_next_hops_htlc_minimum_msat);
732744
}
733745
}
734746

@@ -755,7 +767,7 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
755767
// place where it could be added.
756768
if first_hops.is_some() {
757769
if let Some(&(ref first_hop, ref features, ref outbound_capacity_msat)) = first_hop_targets.get(&payee) {
758-
add_entry!(first_hop, *our_node_id, payee, dummy_directional_info, Some(outbound_capacity_msat / 1000), features.to_context(), 0, path_value_msat);
770+
add_entry!(first_hop, *our_node_id, payee, dummy_directional_info, Some(outbound_capacity_msat / 1000), features.to_context(), 0, path_value_msat, 0);
759771
}
760772
}
761773

@@ -768,7 +780,7 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
768780
// If not, targets.pop() will not even let us enter the loop in step 2.
769781
None => {},
770782
Some(node) => {
771-
add_entries_to_cheapest_to_target_node!(node, payee, 0, path_value_msat);
783+
add_entries_to_cheapest_to_target_node!(node, payee, 0, path_value_msat, 0);
772784
},
773785
}
774786

@@ -787,7 +799,7 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
787799
// bit lazy here. In the future, we should pull them out via our
788800
// ChannelManager, but there's no reason to waste the space until we
789801
// need them.
790-
add_entry!(first_hop, *our_node_id , hop.src_node_id, dummy_directional_info, Some(outbound_capacity_msat / 1000), features.to_context(), 0, path_value_msat);
802+
add_entry!(first_hop, *our_node_id , hop.src_node_id, dummy_directional_info, Some(outbound_capacity_msat / 1000), features.to_context(), 0, path_value_msat, 0);
791803
true
792804
} else {
793805
// In any other case, only add the hop if the source is in the regular network
@@ -797,17 +809,17 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
797809
if have_hop_src_in_graph {
798810
// BOLT 11 doesn't allow inclusion of features for the last hop hints, which
799811
// really sucks, cause we're gonna need that eventually.
800-
let last_hop_htlc_minimum_msat: u64 = match hop.htlc_minimum_msat {
812+
let last_path_htlc_minimum_msat: u64 = match hop.htlc_minimum_msat {
801813
Some(htlc_minimum_msat) => htlc_minimum_msat,
802814
None => 0
803815
};
804816
let directional_info = DummyDirectionalChannelInfo {
805817
cltv_expiry_delta: hop.cltv_expiry_delta as u32,
806-
htlc_minimum_msat: last_hop_htlc_minimum_msat,
818+
htlc_minimum_msat: last_path_htlc_minimum_msat,
807819
htlc_maximum_msat: hop.htlc_maximum_msat,
808820
fees: hop.fees,
809821
};
810-
add_entry!(hop.short_channel_id, hop.src_node_id, payee, directional_info, None::<u64>, ChannelFeatures::empty(), 0, path_value_msat);
822+
add_entry!(hop.short_channel_id, hop.src_node_id, payee, directional_info, None::<u64>, ChannelFeatures::empty(), 0, path_value_msat, 0);
811823
}
812824
}
813825

@@ -824,7 +836,7 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
824836
// Both these cases (and other cases except reaching recommended_value_msat) mean that
825837
// paths_collection will be stopped because found_new_path==false.
826838
// This is not necessarily a routing failure.
827-
'path_construction: while let Some(RouteGraphNode { pubkey, lowest_fee_to_node, value_contribution_msat, .. }) = targets.pop() {
839+
'path_construction: while let Some(RouteGraphNode { pubkey, lowest_fee_to_node, value_contribution_msat, path_htlc_minimum_msat, .. }) = targets.pop() {
828840

829841
// Since we're going payee-to-payer, hitting our node as a target means we should stop
830842
// traversing the graph and arrange the path out of what we found.
@@ -921,7 +933,7 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
921933
match network.get_nodes().get(&pubkey) {
922934
None => {},
923935
Some(node) => {
924-
add_entries_to_cheapest_to_target_node!(node, &pubkey, lowest_fee_to_node, value_contribution_msat);
936+
add_entries_to_cheapest_to_target_node!(node, &pubkey, lowest_fee_to_node, value_contribution_msat, path_htlc_minimum_msat);
925937
},
926938
}
927939
}
@@ -3523,6 +3535,66 @@ mod tests {
35233535
assert_eq!(route.paths[0][1].channel_features.le_flags(), &id_to_feature_flags(13));
35243536
}
35253537
}
3538+
3539+
#[test]
3540+
fn htlc_max_reduction_below_min() {
3541+
// Test that if, while walking the graph, we reduce the value being sent to meet an
3542+
// htlc_maximum_msat, we don't end up undershooting a later htlc_minimum_msat. In the
3543+
// initial version of MPP we'd accept such routes but reject them while recalculating fees,
3544+
// resulting in us thinking there is no possible path, even if other paths exist.
3545+
let (secp_ctx, net_graph_msg_handler, _, logger) = build_graph();
3546+
let (our_privkey, our_id, privkeys, nodes) = get_nodes(&secp_ctx);
3547+
3548+
// We modify the graph to set the htlc_minimum of channel 2 and 4 as needed - channel 2
3549+
// gets an htlc_minimum_msat of 80_000 and channel 4 an htlc_maximum_msat of 90_000. We
3550+
// then try to send 90_000.
3551+
update_channel(&net_graph_msg_handler, &secp_ctx, &our_privkey, UnsignedChannelUpdate {
3552+
chain_hash: genesis_block(Network::Testnet).header.block_hash(),
3553+
short_channel_id: 2,
3554+
timestamp: 2,
3555+
flags: 0,
3556+
cltv_expiry_delta: 0,
3557+
htlc_minimum_msat: 0,
3558+
htlc_maximum_msat: OptionalField::Present(80_000),
3559+
fee_base_msat: 0,
3560+
fee_proportional_millionths: 0,
3561+
excess_data: Vec::new()
3562+
});
3563+
update_channel(&net_graph_msg_handler, &secp_ctx, &privkeys[1], UnsignedChannelUpdate {
3564+
chain_hash: genesis_block(Network::Testnet).header.block_hash(),
3565+
short_channel_id: 4,
3566+
timestamp: 2,
3567+
flags: 0,
3568+
cltv_expiry_delta: (4 << 8) | 1,
3569+
htlc_minimum_msat: 90_000,
3570+
htlc_maximum_msat: OptionalField::Absent,
3571+
fee_base_msat: 0,
3572+
fee_proportional_millionths: 0,
3573+
excess_data: Vec::new()
3574+
});
3575+
3576+
{
3577+
// Now, attempt to route 125 sats (just a bit below the capacity of 3 channels).
3578+
// Our algorithm should provide us with these 3 paths.
3579+
let route = get_route(&our_id, &net_graph_msg_handler.network_graph.read().unwrap(), &nodes[2], Some(InvoiceFeatures::known()), None, &Vec::new(), 90_000, 42, Arc::clone(&logger)).unwrap();
3580+
assert_eq!(route.paths.len(), 1);
3581+
assert_eq!(route.paths[0].len(), 2);
3582+
3583+
assert_eq!(route.paths[0][0].pubkey, nodes[7]);
3584+
assert_eq!(route.paths[0][0].short_channel_id, 12);
3585+
assert_eq!(route.paths[0][0].fee_msat, 90_000*2);
3586+
assert_eq!(route.paths[0][0].cltv_expiry_delta, (13 << 8) | 1);
3587+
assert_eq!(route.paths[0][0].node_features.le_flags(), &id_to_feature_flags(8));
3588+
assert_eq!(route.paths[0][0].channel_features.le_flags(), &id_to_feature_flags(12));
3589+
3590+
assert_eq!(route.paths[0][1].pubkey, nodes[2]);
3591+
assert_eq!(route.paths[0][1].short_channel_id, 13);
3592+
assert_eq!(route.paths[0][1].fee_msat, 90_000);
3593+
assert_eq!(route.paths[0][1].cltv_expiry_delta, 42);
3594+
assert_eq!(route.paths[0][1].node_features.le_flags(), InvoiceFeatures::known().le_flags());
3595+
assert_eq!(route.paths[0][1].channel_features.le_flags(), &id_to_feature_flags(13));
3596+
}
3597+
}
35263598
}
35273599

35283600
#[cfg(all(test, feature = "unstable"))]

0 commit comments

Comments
 (0)