Skip to content

Commit 725ee4b

Browse files
committed
Mind htlc_minimum_msat
1 parent 39dae9e commit 725ee4b

File tree

1 file changed

+57
-39
lines changed

1 file changed

+57
-39
lines changed

lightning/src/routing/router.rs

Lines changed: 57 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,10 @@ struct PathBuildingHop {
194194
/// we don't fall below the minimum. Should not be updated manually and
195195
/// generally should not be accessed.
196196
htlc_minimum_msat: u64,
197+
/// Keeps track of sats we overpaid to hit htlc_minimum_msat on this hop. It is useful for
198+
/// routing, so that we know how much a hop has to actually pay in fees: the total fee is
199+
/// next_hop_use_fee_msat + overpaid_fee_msat.
200+
overpaid_fee_msat: u64,
197201
}
198202

199203
impl PathBuildingHop {
@@ -238,8 +242,7 @@ impl PaymentPath {
238242

239243
// If an amount transferred by the path is updated, the fees should be adjusted.
240244
// Any other way to change fees may result in an inconsistency.
241-
// Also, it's only safe to reduce the value, to not violate limits like
242-
// htlc_minimum_msat.
245+
// Also, it's only safe to reduce the value, to not violate channel upper bounds.
243246
fn update_value_and_recompute_fees(&mut self, value_msat: u64) {
244247
if value_msat == self.hops.last().unwrap().route_hop.fee_msat {
245248
// Nothing to change.
@@ -248,9 +251,10 @@ impl PaymentPath {
248251
assert!(value_msat < self.hops.last().unwrap().route_hop.fee_msat);
249252

250253
let mut total_fee_paid_msat = 0 as u64;
251-
// TODO: while reducing the transferred value on hop N, we could cause some N+M hop to fall
252-
// below its htlc_minimum_msat in terms of amount being transferred that. We should handle
253-
// it here similarly to how we handle it in add_entry!.
254+
// While reducing the transferred value on hop N, we could cause some N+M hop to fall
255+
// below its htlc_minimum_msat in terms of amount being transferred that. We handle it here
256+
// similarly to add_entry! by tracking incl_fee_next_hops_htlc_minimum_msat.
257+
let mut incl_fee_next_hops_htlc_minimum_msat = 0;
254258
for i in (0..self.hops.len()).rev() {
255259
let last_hop = i == self.hops.len() - 1;
256260

@@ -269,7 +273,8 @@ impl PaymentPath {
269273
// set it too high just to maliciously take more fees by exploiting this
270274
// match htlc_minimum_msat logic.
271275
let mut cur_hop_transferred_amount_msat = total_fee_paid_msat + value_msat;
272-
if let Some(extra_fees_msat) = cur_hop.htlc_minimum_msat.checked_sub(cur_hop_transferred_amount_msat) {
276+
let effective_htlc_minimum_msat = cmp::max(incl_fee_next_hops_htlc_minimum_msat, cur_hop.htlc_minimum_msat);
277+
if let Some(extra_fees_msat) = effective_htlc_minimum_msat.checked_sub(cur_hop_transferred_amount_msat) {
273278
cur_hop_transferred_amount_msat += extra_fees_msat;
274279
total_fee_paid_msat += extra_fees_msat;
275280
cur_hop_fees_msat += extra_fees_msat;
@@ -299,6 +304,12 @@ impl PaymentPath {
299304
unreachable!();
300305
}
301306
}
307+
308+
// Recompute whole-path htlc_minimum_msat, so that previous hops won't underpay.
309+
incl_fee_next_hops_htlc_minimum_msat = match compute_fees(incl_fee_next_hops_htlc_minimum_msat, cur_hop.channel_fees) {
310+
Some(fee_msat) => cmp::max(fee_msat, cur_hop.htlc_minimum_msat),
311+
None => unreachable!(), // Should have been handled in add_entry.
312+
};
302313
}
303314
}
304315
}
@@ -496,21 +507,21 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
496507

497508
let value_contribution_msat = cmp::min(available_value_contribution_msat, $next_hops_value_contribution);
498509
// Includes paying fees for the use of the following channels.
499-
let amount_to_transfer_over_msat: u64 = match value_contribution_msat.checked_add($next_hops_fee_msat) {
510+
let mut amount_to_transfer_over_msat: u64 = match value_contribution_msat.checked_add($next_hops_fee_msat) {
500511
Some(result) => result,
501512
// Can't overflow due to how the values were computed right above.
502513
None => unreachable!(),
503514
};
504-
let over_path_minimum_msat = amount_to_transfer_over_msat >= $directional_info.htlc_minimum_msat &&
505-
amount_to_transfer_over_msat >= $incl_fee_next_hops_htlc_minimum_msat;
506-
507-
// If HTLC minimum is larger than the amount we're going to transfer, we shouldn't
508-
// bother considering this channel.
509-
// Since we're choosing amount_to_transfer_over_msat as maximum possible, it can
510-
// be only reduced later (not increased), so this channel should just be skipped
511-
// as not sufficient.
512-
// TODO: Explore simply adding fee to hit htlc_minimum_msat
513-
if contributes_sufficient_value && over_path_minimum_msat {
515+
516+
// If HTLC minimum is larger than the amount we're going to transfer, we shouldn
517+
// transfer that value instead, thus overpaying fees.
518+
let effective_htlc_minimum_msat = cmp::max($directional_info.htlc_minimum_msat, $incl_fee_next_hops_htlc_minimum_msat);
519+
let overpaid_fee_msat = match effective_htlc_minimum_msat.checked_sub(amount_to_transfer_over_msat) {
520+
Some(fee_msat) => fee_msat,
521+
None => 0,
522+
};
523+
amount_to_transfer_over_msat += overpaid_fee_msat;
524+
if contributes_sufficient_value {
514525
let hm_entry = dist.entry(&$src_node_id);
515526
let old_entry = hm_entry.or_insert_with(|| {
516527
// If there was previously no known way to access
@@ -542,6 +553,7 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
542553
hop_use_fee_msat: u64::max_value(),
543554
total_fee_msat: u64::max_value(),
544555
htlc_minimum_msat: $directional_info.htlc_minimum_msat,
556+
overpaid_fee_msat
545557
}
546558
});
547559

@@ -588,28 +600,26 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
588600
},
589601
};
590602

591-
// Update the way of reaching $src_node_id with the given $chan_id (from $dest_node_id),
592-
// if this way is cheaper than the already known
603+
// Update the way of reaching $src_node_id with the given $chan_id (from
604+
// $dest_node_id), if this way is cheaper than the already known
593605
// (considering the cost to "reach" this channel from the route destination,
594-
// the cost of using this channel,
606+
// the cost of using this channel (with possible overpayment for htlc min)
595607
// and the cost of routing to the source node of this channel).
596-
// Also, consider that htlc_minimum_msat_difference, because we might end up
597-
// paying it. Consider the following exploit:
598-
// we use 2 paths to transfer 1.5 BTC. One of them is 0-fee normal 1 BTC path,
599-
// and for the other one we picked a 1sat-fee path with htlc_minimum_msat of
600-
// 1 BTC. Now, since the latter is more expensive, we gonna try to cut it
601-
// by 0.5 BTC, but then match htlc_minimum_msat by paying a fee of 0.5 BTC
602-
// to this channel.
608+
// Consider the following exploit: we use 2 paths to transfer 1.5 BTC.
609+
// One of them is 0-fee normal 1 BTC path, and for the other one we picked a
610+
// 1sat-fee path with htlc_minimum_msat of 1 BTC. Now, since the latter is
611+
// more expensive, we gonna try to cut it by 0.5 BTC, but then match
612+
// htlc_minimum_msat by paying a fee of 0.5 BTC to this channel.
603613
// TODO: this scoring could be smarter (e.g. 0.5*htlc_minimum_msat here).
604614
let mut old_cost = old_entry.total_fee_msat;
605-
if let Some(increased_old_cost) = old_cost.checked_add(old_entry.htlc_minimum_msat) {
615+
if let Some(increased_old_cost) = old_cost.checked_add(old_entry.overpaid_fee_msat) {
606616
old_cost = increased_old_cost;
607617
} else {
608618
old_cost = u64::max_value();
609619
}
610620

611621
let mut new_cost = total_fee_msat;
612-
if let Some(increased_new_cost) = new_cost.checked_add($directional_info.htlc_minimum_msat) {
622+
if let Some(increased_new_cost) = new_cost.checked_add(overpaid_fee_msat) {
613623
new_cost = increased_new_cost;
614624
} else {
615625
new_cost = u64::max_value();
@@ -629,9 +639,19 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
629639
cltv_expiry_delta: $directional_info.cltv_expiry_delta as u32,
630640
};
631641
old_entry.channel_fees = $directional_info.fees;
632-
// It's probably fine to replace the old entry, because the new one
633-
// passed the htlc_minimum-related checks above.
634-
old_entry.htlc_minimum_msat = $directional_info.htlc_minimum_msat;
642+
old_entry.htlc_minimum_msat = old_entry.htlc_minimum_msat;
643+
// These info about an entry is only implicitly associated with
644+
// the path which will be taken further: for example, if there are
645+
// two options to get here from the payee side, we will store the
646+
// cheapest, although it might have higher overpayment fee on this
647+
// hop to match htlc_minimum_msat. Then, if the other path is associated
648+
// with this entry for some reason, it might not be able to pay htlc_min
649+
// anymore. To avoid that, here we take the worst-case values.
650+
// It can't really be exploited because it's all about getting to one
651+
// node, which should not have too-high fees in the first place,
652+
// because otherwise it can be "punished" by this selection even if
653+
// only one channel there is too expensive.
654+
old_entry.overpaid_fee_msat = cmp::max(overpaid_fee_msat, old_entry.overpaid_fee_msat);
635655
}
636656
}
637657
}
@@ -817,11 +837,11 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
817837
// We "propagate" the fees one hop backward (topologically) here,
818838
// so that fees paid for a HTLC forwarding on the current channel are
819839
// associated with the previous channel (where they will be subtracted).
820-
ordered_hops.last_mut().unwrap().route_hop.fee_msat = new_entry.hop_use_fee_msat;
840+
ordered_hops.last_mut().unwrap().route_hop.fee_msat = new_entry.hop_use_fee_msat + ordered_hops.last_mut().unwrap().overpaid_fee_msat;
821841
ordered_hops.last_mut().unwrap().route_hop.cltv_expiry_delta = new_entry.route_hop.cltv_expiry_delta;
822842
ordered_hops.push(new_entry.clone());
823843
}
824-
ordered_hops.last_mut().unwrap().route_hop.fee_msat = value_contribution_msat;
844+
ordered_hops.last_mut().unwrap().route_hop.fee_msat = value_contribution_msat + ordered_hops.last_mut().unwrap().overpaid_fee_msat;
825845
ordered_hops.last_mut().unwrap().hop_use_fee_msat = 0;
826846
ordered_hops.last_mut().unwrap().route_hop.cltv_expiry_delta = final_cltv;
827847

@@ -1535,7 +1555,6 @@ mod tests {
15351555
});
15361556

15371557
// Check against amount_to_transfer_over_msat.
1538-
// We will be transferring 300_000_000 msat.
15391558
// Set minimal HTLC of 200_000_000 msat.
15401559
update_channel(&net_graph_msg_handler, &secp_ctx, &our_privkey, UnsignedChannelUpdate {
15411560
chain_hash: genesis_block(Network::Testnet).header.block_hash(),
@@ -1565,10 +1584,9 @@ mod tests {
15651584
excess_data: Vec::new()
15661585
});
15671586

1568-
// Not possible to send 299_999_999: we first allocate 200_000_000 for the channel#12,
1569-
// and then the amount_to_transfer_over_msat over channel#2 is less than our limit.
1570-
if let Err(LightningError{err, action: ErrorAction::IgnoreError}) = get_route(&our_id, &net_graph_msg_handler.network_graph.read().unwrap(), &nodes[2], None, &Vec::new(), 199_999_999, 42, Arc::clone(&logger)) {
1571-
assert_eq!(err, "Failed to find a path to the given destination");
1587+
// Not possible to send 299_999_999.
1588+
if let Err(LightningError{err, action: ErrorAction::IgnoreError}) = get_route(&our_id, &net_graph_msg_handler.network_graph.read().unwrap(), &nodes[2], None, &Vec::new(), 299_999_999, 42, Arc::clone(&logger)) {
1589+
assert_eq!(err, "Failed to find a sufficient route to the given destination");
15721590
} else { panic!(); }
15731591

15741592
// Lift the restriction on the first hop.

0 commit comments

Comments
 (0)