Skip to content

Commit f51e0c0

Browse files
committed
Consider dust exposure when assembling a route
When calculating the amount available to send for the next HTLC, if we over-count we may create routes which are not actually usable. Historically this has been an issue, which we resolve over a few commits. Here we consider how much adding one additional (dust) HTLC would impact our total dust exposure, setting the new next-HTLC-minimum field to require HTLCs be non-dust if required or set our next-HTLC maximum if we cannot send a dust HTLC but do have some additional exposure remaining. We also add some testing when sending to ensure that send failures are accounted for in our balance calculations. Fixes #2252.
1 parent 1a9b3f1 commit f51e0c0

File tree

2 files changed

+64
-10
lines changed

2 files changed

+64
-10
lines changed

lightning/src/ln/channel.rs

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2749,6 +2749,45 @@ impl<Signer: WriteableEcdsaChannelSigner> Channel<Signer> {
27492749
}
27502750
}
27512751

2752+
let mut next_outbound_htlc_min_msat = self.counterparty_htlc_minimum_msat;
2753+
2754+
// If we get close to our maximum dust exposure, we end up in a situation where we can send
2755+
// between zero and the remaining dust exposure limit remaining OR above the dust limit.
2756+
// Because we cannot express this as a simple min/max, we prefer to tell the user they can
2757+
// send above the dust limit (as the router can always overpay to meet the dust limit).
2758+
let mut remaining_msat_below_dust_exposure_limit = None;
2759+
let mut dust_exposure_dust_limit_msat = 0;
2760+
2761+
let (htlc_success_dust_limit, htlc_timeout_dust_limit) = if self.opt_anchors() {
2762+
(self.counterparty_dust_limit_satoshis, self.holder_dust_limit_satoshis)
2763+
} else {
2764+
let dust_buffer_feerate = self.get_dust_buffer_feerate(None) as u64;
2765+
(self.counterparty_dust_limit_satoshis + dust_buffer_feerate * htlc_success_tx_weight(false) / 1000,
2766+
self.holder_dust_limit_satoshis + dust_buffer_feerate * htlc_timeout_tx_weight(false) / 1000)
2767+
};
2768+
let on_counterparty_dust_htlc_exposure_msat = inbound_stats.on_counterparty_tx_dust_exposure_msat + outbound_stats.on_counterparty_tx_dust_exposure_msat;
2769+
if on_counterparty_dust_htlc_exposure_msat + htlc_success_dust_limit * 1000 - 1 > self.get_max_dust_htlc_exposure_msat() {
2770+
remaining_msat_below_dust_exposure_limit =
2771+
Some(self.get_max_dust_htlc_exposure_msat().saturating_sub(on_counterparty_dust_htlc_exposure_msat));
2772+
dust_exposure_dust_limit_msat = cmp::max(dust_exposure_dust_limit_msat, htlc_success_dust_limit * 1000);
2773+
}
2774+
2775+
let on_holder_dust_htlc_exposure_msat = inbound_stats.on_holder_tx_dust_exposure_msat + outbound_stats.on_holder_tx_dust_exposure_msat;
2776+
if on_holder_dust_htlc_exposure_msat + htlc_timeout_dust_limit * 1000 - 1 > self.get_max_dust_htlc_exposure_msat() {
2777+
remaining_msat_below_dust_exposure_limit = Some(cmp::max(
2778+
remaining_msat_below_dust_exposure_limit.unwrap_or(0),
2779+
self.get_max_dust_htlc_exposure_msat().saturating_sub(on_holder_dust_htlc_exposure_msat)));
2780+
dust_exposure_dust_limit_msat = cmp::max(dust_exposure_dust_limit_msat, htlc_timeout_dust_limit * 1000);
2781+
}
2782+
2783+
if let Some(remaining_limit_msat) = remaining_msat_below_dust_exposure_limit {
2784+
if available_capacity_msat < dust_exposure_dust_limit_msat {
2785+
available_capacity_msat = cmp::min(available_capacity_msat, remaining_limit_msat);
2786+
} else {
2787+
next_outbound_htlc_min_msat = cmp::max(next_outbound_htlc_min_msat, dust_exposure_dust_limit_msat);
2788+
}
2789+
}
2790+
27522791
available_capacity_msat = cmp::min(available_capacity_msat,
27532792
self.counterparty_max_htlc_value_in_flight_msat - outbound_stats.pending_htlcs_value_msat);
27542793

@@ -2764,7 +2803,7 @@ impl<Signer: WriteableEcdsaChannelSigner> Channel<Signer> {
27642803
0) as u64,
27652804
outbound_capacity_msat,
27662805
next_outbound_htlc_limit_msat: available_capacity_msat,
2767-
next_outbound_htlc_min_msat: 0,
2806+
next_outbound_htlc_min_msat,
27682807
balance_msat,
27692808
}
27702809
}
@@ -5898,6 +5937,7 @@ impl<Signer: WriteableEcdsaChannelSigner> Channel<Signer> {
58985937
}
58995938

59005939
if amount_msat < self.counterparty_htlc_minimum_msat {
5940+
debug_assert!(amount_msat < self.get_available_balances().next_outbound_htlc_min_msat);
59015941
return Err(ChannelError::Ignore(format!("Cannot send less than their minimum HTLC value ({})", self.counterparty_htlc_minimum_msat)));
59025942
}
59035943

@@ -5946,6 +5986,8 @@ impl<Signer: WriteableEcdsaChannelSigner> Channel<Signer> {
59465986
if amount_msat / 1000 < exposure_dust_limit_success_sats {
59475987
let on_counterparty_dust_htlc_exposure_msat = inbound_stats.on_counterparty_tx_dust_exposure_msat + outbound_stats.on_counterparty_tx_dust_exposure_msat + amount_msat;
59485988
if on_counterparty_dust_htlc_exposure_msat > self.get_max_dust_htlc_exposure_msat() {
5989+
debug_assert!(amount_msat > self.get_available_balances().next_outbound_htlc_limit_msat ||
5990+
amount_msat < self.get_available_balances().next_outbound_htlc_min_msat);
59495991
return Err(ChannelError::Ignore(format!("Cannot send value that would put our exposure to dust HTLCs at {} over the limit {} on counterparty commitment tx",
59505992
on_counterparty_dust_htlc_exposure_msat, self.get_max_dust_htlc_exposure_msat())));
59515993
}
@@ -5955,6 +5997,8 @@ impl<Signer: WriteableEcdsaChannelSigner> Channel<Signer> {
59555997
if amount_msat / 1000 < exposure_dust_limit_timeout_sats {
59565998
let on_holder_dust_htlc_exposure_msat = inbound_stats.on_holder_tx_dust_exposure_msat + outbound_stats.on_holder_tx_dust_exposure_msat + amount_msat;
59575999
if on_holder_dust_htlc_exposure_msat > self.get_max_dust_htlc_exposure_msat() {
6000+
debug_assert!(amount_msat > self.get_available_balances().next_outbound_htlc_limit_msat ||
6001+
amount_msat < self.get_available_balances().next_outbound_htlc_min_msat);
59586002
return Err(ChannelError::Ignore(format!("Cannot send value that would put our exposure to dust HTLCs at {} over the limit {} on holder commitment tx",
59596003
on_holder_dust_htlc_exposure_msat, self.get_max_dust_htlc_exposure_msat())));
59606004
}

lightning/src/ln/functional_tests.rs

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9635,6 +9635,10 @@ fn do_test_max_dust_htlc_exposure(dust_outbound_balance: bool, exposure_breach_e
96359635
let (announcement, as_update, bs_update) = create_chan_between_nodes_with_value_b(&nodes[0], &nodes[1], &channel_ready);
96369636
update_nodes_with_chan_announce(&nodes, 0, 1, &announcement, &as_update, &bs_update);
96379637

9638+
// Fetch a route in advance as we will be unable to once we're unable to send.
9639+
let (mut route, payment_hash, _, payment_secret) =
9640+
get_route_and_payment_hash!(nodes[0], nodes[1], 1000);
9641+
96389642
let dust_buffer_feerate = {
96399643
let per_peer_state = nodes[0].node.per_peer_state.read().unwrap();
96409644
let chan_lock = per_peer_state.get(&nodes[1].node.get_our_node_id()).unwrap().lock().unwrap();
@@ -9647,7 +9651,7 @@ fn do_test_max_dust_htlc_exposure(dust_outbound_balance: bool, exposure_breach_e
96479651
let dust_inbound_htlc_on_holder_tx_msat: u64 = (dust_buffer_feerate * htlc_success_tx_weight(opt_anchors) / 1000 + open_channel.dust_limit_satoshis - 1) * 1000;
96489652
let dust_inbound_htlc_on_holder_tx: u64 = config.channel_config.max_dust_htlc_exposure_msat / dust_inbound_htlc_on_holder_tx_msat;
96499653

9650-
let dust_htlc_on_counterparty_tx: u64 = 25;
9654+
let dust_htlc_on_counterparty_tx: u64 = 4;
96519655
let dust_htlc_on_counterparty_tx_msat: u64 = config.channel_config.max_dust_htlc_exposure_msat / dust_htlc_on_counterparty_tx;
96529656

96539657
if on_holder_tx {
@@ -9672,23 +9676,23 @@ fn do_test_max_dust_htlc_exposure(dust_outbound_balance: bool, exposure_breach_e
96729676
if dust_outbound_balance {
96739677
// Outbound dust threshold: 2132 sats (`dust_buffer_feerate` * HTLC_TIMEOUT_TX_WEIGHT / 1000 + counteparty's `dust_limit_satoshis`)
96749678
// Outbound dust balance: 5000 sats
9675-
for _ in 0..dust_htlc_on_counterparty_tx {
9679+
for _ in 0..dust_htlc_on_counterparty_tx - 1 {
96769680
let (route, payment_hash, _, payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[1], dust_htlc_on_counterparty_tx_msat);
96779681
nodes[0].node.send_payment_with_route(&route, payment_hash,
96789682
RecipientOnionFields::secret_only(payment_secret), PaymentId(payment_hash.0)).unwrap();
96799683
}
96809684
} else {
96819685
// Inbound dust threshold: 2031 sats (`dust_buffer_feerate` * HTLC_TIMEOUT_TX_WEIGHT / 1000 + counteparty's `dust_limit_satoshis`)
96829686
// Inbound dust balance: 5000 sats
9683-
for _ in 0..dust_htlc_on_counterparty_tx {
9687+
for _ in 0..dust_htlc_on_counterparty_tx - 1 {
96849688
route_payment(&nodes[1], &[&nodes[0]], dust_htlc_on_counterparty_tx_msat);
96859689
}
96869690
}
96879691
}
96889692

9689-
let dust_overflow = dust_htlc_on_counterparty_tx_msat * (dust_htlc_on_counterparty_tx + 1);
96909693
if exposure_breach_event == ExposureEvent::AtHTLCForward {
9691-
let (route, payment_hash, _, payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[1], if on_holder_tx { dust_outbound_htlc_on_holder_tx_msat } else { dust_htlc_on_counterparty_tx_msat });
9694+
route.paths[0].hops.last_mut().unwrap().fee_msat =
9695+
if on_holder_tx { dust_outbound_htlc_on_holder_tx_msat } else { dust_htlc_on_counterparty_tx_msat + 1 };
96929696
let mut config = UserConfig::default();
96939697
// With default dust exposure: 5000 sats
96949698
if on_holder_tx {
@@ -9702,10 +9706,13 @@ fn do_test_max_dust_htlc_exposure(dust_outbound_balance: bool, exposure_breach_e
97029706
unwrap_send_err!(nodes[0].node.send_payment_with_route(&route, payment_hash,
97039707
RecipientOnionFields::secret_only(payment_secret), PaymentId(payment_hash.0)
97049708
), true, APIError::ChannelUnavailable { ref err },
9705-
assert_eq!(err, &format!("Cannot send value that would put our exposure to dust HTLCs at {} over the limit {} on counterparty commitment tx", dust_overflow, config.channel_config.max_dust_htlc_exposure_msat)));
9709+
assert_eq!(err,
9710+
&format!("Cannot send value that would put our exposure to dust HTLCs at {} over the limit {} on counterparty commitment tx",
9711+
dust_htlc_on_counterparty_tx_msat * (dust_htlc_on_counterparty_tx - 1) + dust_htlc_on_counterparty_tx_msat + 1,
9712+
config.channel_config.max_dust_htlc_exposure_msat)));
97069713
}
97079714
} else if exposure_breach_event == ExposureEvent::AtHTLCReception {
9708-
let (route, payment_hash, _, payment_secret) = get_route_and_payment_hash!(nodes[1], nodes[0], if on_holder_tx { dust_inbound_htlc_on_holder_tx_msat } else { dust_htlc_on_counterparty_tx_msat });
9715+
let (route, payment_hash, _, payment_secret) = get_route_and_payment_hash!(nodes[1], nodes[0], if on_holder_tx { dust_inbound_htlc_on_holder_tx_msat } else { dust_htlc_on_counterparty_tx_msat + 1 });
97099716
nodes[1].node.send_payment_with_route(&route, payment_hash,
97109717
RecipientOnionFields::secret_only(payment_secret), PaymentId(payment_hash.0)).unwrap();
97119718
check_added_monitors!(nodes[1], 1);
@@ -9721,10 +9728,13 @@ fn do_test_max_dust_htlc_exposure(dust_outbound_balance: bool, exposure_breach_e
97219728
nodes[0].logger.assert_log("lightning::ln::channel".to_string(), format!("Cannot accept value that would put our exposure to dust HTLCs at {} over the limit {} on holder commitment tx", if dust_outbound_balance { dust_outbound_overflow } else { dust_inbound_overflow }, config.channel_config.max_dust_htlc_exposure_msat), 1);
97229729
} else {
97239730
// Outbound dust balance: 5200 sats
9724-
nodes[0].logger.assert_log("lightning::ln::channel".to_string(), format!("Cannot accept value that would put our exposure to dust HTLCs at {} over the limit {} on counterparty commitment tx", dust_overflow, config.channel_config.max_dust_htlc_exposure_msat), 1);
9731+
nodes[0].logger.assert_log("lightning::ln::channel".to_string(),
9732+
format!("Cannot accept value that would put our exposure to dust HTLCs at {} over the limit {} on counterparty commitment tx",
9733+
dust_htlc_on_counterparty_tx_msat * (dust_htlc_on_counterparty_tx - 1) + dust_htlc_on_counterparty_tx_msat + 1,
9734+
config.channel_config.max_dust_htlc_exposure_msat), 1);
97259735
}
97269736
} else if exposure_breach_event == ExposureEvent::AtUpdateFeeOutbound {
9727-
let (route, payment_hash, _, payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[1], 2_500_000);
9737+
route.paths[0].hops.last_mut().unwrap().fee_msat = 2_500_000;
97289738
nodes[0].node.send_payment_with_route(&route, payment_hash,
97299739
RecipientOnionFields::secret_only(payment_secret), PaymentId(payment_hash.0)).unwrap();
97309740
{

0 commit comments

Comments
 (0)