Merge pull request #3572 from tankyleo/25-01-htlc-dust-exposure

Tweak htlc dust exposure due to excess fees
This commit is contained in:
Matt Corallo 2025-02-21 20:46:54 +00:00 committed by GitHub
commit 81f8b6786c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 322 additions and 74 deletions

View file

@ -196,15 +196,16 @@ pub(crate) fn commit_tx_fee_sat(feerate_per_kw: u32, num_htlcs: usize, channel_t
/ 1000
}
pub(crate) fn per_outbound_htlc_counterparty_commit_tx_fee_msat(feerate_per_kw: u32, channel_type_features: &ChannelTypeFeatures) -> u64 {
// Note that we need to divide before multiplying to round properly,
// since the lowest denomination of bitcoin on-chain is the satoshi.
let commitment_tx_fee = COMMITMENT_TX_WEIGHT_PER_HTLC * feerate_per_kw as u64 / 1000 * 1000;
if channel_type_features.supports_anchors_zero_fee_htlc_tx() {
commitment_tx_fee + htlc_success_tx_weight(channel_type_features) * feerate_per_kw as u64 / 1000
pub(crate) fn commit_and_htlc_tx_fees_sat(feerate_per_kw: u32, num_accepted_htlcs: usize, num_offered_htlcs: usize, channel_type_features: &ChannelTypeFeatures) -> u64 {
let num_htlcs = num_accepted_htlcs + num_offered_htlcs;
let commit_tx_fees_sat = commit_tx_fee_sat(feerate_per_kw, num_htlcs, channel_type_features);
let htlc_tx_fees_sat = if !channel_type_features.supports_anchors_zero_fee_htlc_tx() {
num_accepted_htlcs as u64 * htlc_success_tx_weight(channel_type_features) * feerate_per_kw as u64 / 1000
+ num_offered_htlcs as u64 * htlc_timeout_tx_weight(channel_type_features) * feerate_per_kw as u64 / 1000
} else {
commitment_tx_fee
}
0
};
commit_tx_fees_sat + htlc_tx_fees_sat
}
// Various functions for key derivation and transaction creation for use within channels. Primarily

View file

@ -46,7 +46,7 @@ use crate::ln::chan_utils::{
HolderCommitmentTransaction, ChannelTransactionParameters,
CounterpartyChannelTransactionParameters, MAX_HTLCS,
get_commitment_transaction_number_obscure_factor,
ClosingTransaction, commit_tx_fee_sat, per_outbound_htlc_counterparty_commit_tx_fee_msat,
ClosingTransaction, commit_tx_fee_sat,
};
use crate::ln::chan_utils;
use crate::ln::onion_utils::HTLCFailReason;
@ -866,6 +866,10 @@ struct HTLCStats {
pending_inbound_htlcs_value_msat: u64,
pending_outbound_htlcs_value_msat: u64,
on_counterparty_tx_dust_exposure_msat: u64,
// If the counterparty sets a feerate on the channel in excess of our dust_exposure_limiting_feerate,
// this will be set to the dust exposure that would result from us adding an additional nondust outbound
// htlc on the counterparty's commitment transaction.
extra_nondust_htlc_on_counterparty_tx_dust_exposure_msat: Option<u64>,
on_holder_tx_dust_exposure_msat: u64,
outbound_holding_cell_msat: u64,
on_holder_tx_outbound_holding_cell_htlcs_count: u32, // dust HTLCs *non*-included
@ -3794,20 +3798,13 @@ impl<SP: Deref> ChannelContext<SP> where SP::Target: SignerProvider {
.or(self.pending_update_fee.map(|(fee, _)| fee))
.unwrap_or(self.feerate_per_kw)
.checked_sub(dust_exposure_limiting_feerate);
if let Some(excess_feerate) = excess_feerate_opt {
let on_counterparty_tx_nondust_htlcs =
on_counterparty_tx_accepted_nondust_htlcs + on_counterparty_tx_offered_nondust_htlcs;
on_counterparty_tx_dust_exposure_msat +=
commit_tx_fee_sat(excess_feerate, on_counterparty_tx_nondust_htlcs, &self.channel_type) * 1000;
if !self.channel_type.supports_anchors_zero_fee_htlc_tx() {
on_counterparty_tx_dust_exposure_msat +=
on_counterparty_tx_accepted_nondust_htlcs as u64 * htlc_success_tx_weight(&self.channel_type)
* excess_feerate as u64 / 1000;
on_counterparty_tx_dust_exposure_msat +=
on_counterparty_tx_offered_nondust_htlcs as u64 * htlc_timeout_tx_weight(&self.channel_type)
* excess_feerate as u64 / 1000;
}
}
let extra_nondust_htlc_on_counterparty_tx_dust_exposure_msat = excess_feerate_opt.map(|excess_feerate| {
let extra_htlc_dust_exposure = on_counterparty_tx_dust_exposure_msat
+ chan_utils::commit_and_htlc_tx_fees_sat(excess_feerate, on_counterparty_tx_accepted_nondust_htlcs + 1, on_counterparty_tx_offered_nondust_htlcs, &self.channel_type) * 1000;
on_counterparty_tx_dust_exposure_msat
+= chan_utils::commit_and_htlc_tx_fees_sat(excess_feerate, on_counterparty_tx_accepted_nondust_htlcs, on_counterparty_tx_offered_nondust_htlcs, &self.channel_type) * 1000;
extra_htlc_dust_exposure
});
HTLCStats {
pending_inbound_htlcs: self.pending_inbound_htlcs.len(),
@ -3815,6 +3812,7 @@ impl<SP: Deref> ChannelContext<SP> where SP::Target: SignerProvider {
pending_inbound_htlcs_value_msat,
pending_outbound_htlcs_value_msat,
on_counterparty_tx_dust_exposure_msat,
extra_nondust_htlc_on_counterparty_tx_dust_exposure_msat,
on_holder_tx_dust_exposure_msat,
outbound_holding_cell_msat,
on_holder_tx_outbound_holding_cell_htlcs_count,
@ -4019,13 +4017,8 @@ impl<SP: Deref> ChannelContext<SP> where SP::Target: SignerProvider {
context.holder_dust_limit_satoshis + dust_buffer_feerate * htlc_timeout_tx_weight(context.get_channel_type()) / 1000)
};
let excess_feerate_opt = self.feerate_per_kw.checked_sub(dust_exposure_limiting_feerate);
if let Some(excess_feerate) = excess_feerate_opt {
let htlc_dust_exposure_msat =
per_outbound_htlc_counterparty_commit_tx_fee_msat(excess_feerate, &context.channel_type);
let nondust_htlc_counterparty_tx_dust_exposure =
htlc_stats.on_counterparty_tx_dust_exposure_msat.saturating_add(htlc_dust_exposure_msat);
if nondust_htlc_counterparty_tx_dust_exposure > max_dust_htlc_exposure_msat {
if let Some(extra_htlc_dust_exposure) = htlc_stats.extra_nondust_htlc_on_counterparty_tx_dust_exposure_msat {
if extra_htlc_dust_exposure > max_dust_htlc_exposure_msat {
// If adding an extra HTLC would put us over the dust limit in total fees, we cannot
// send any non-dust HTLCs.
available_capacity_msat = cmp::min(available_capacity_msat, htlc_success_dust_limit * 1000);
@ -7486,6 +7479,8 @@ impl<SP: Deref> FundedChannel<SP> where
})
}
/// When this function is called, the HTLC is already irrevocably committed to the channel;
/// this function determines whether to fail the HTLC, or forward / claim it.
pub fn can_accept_incoming_htlc<F: Deref, L: Deref>(
&self, msg: &msgs::UpdateAddHTLC, fee_estimator: &LowerBoundedFeeEstimator<F>, logger: L
) -> Result<(), (&'static str, u16)>
@ -7500,33 +7495,19 @@ impl<SP: Deref> FundedChannel<SP> where
let dust_exposure_limiting_feerate = self.context.get_dust_exposure_limiting_feerate(&fee_estimator);
let htlc_stats = self.context.get_pending_htlc_stats(None, dust_exposure_limiting_feerate);
let max_dust_htlc_exposure_msat = self.context.get_max_dust_htlc_exposure_msat(dust_exposure_limiting_feerate);
let (htlc_timeout_dust_limit, htlc_success_dust_limit) = if self.context.get_channel_type().supports_anchors_zero_fee_htlc_tx() {
(0, 0)
let on_counterparty_tx_dust_htlc_exposure_msat = htlc_stats.on_counterparty_tx_dust_exposure_msat;
if on_counterparty_tx_dust_htlc_exposure_msat > max_dust_htlc_exposure_msat {
// Note that the total dust exposure includes both the dust HTLCs and the excess mining fees of the counterparty commitment transaction
log_info!(logger, "Cannot accept value that would put our total dust exposure at {} over the limit {} on counterparty commitment tx",
on_counterparty_tx_dust_htlc_exposure_msat, max_dust_htlc_exposure_msat);
return Err(("Exceeded our total dust exposure limit on counterparty commitment tx", 0x1000|7))
}
let htlc_success_dust_limit = if self.context.get_channel_type().supports_anchors_zero_fee_htlc_tx() {
0
} else {
let dust_buffer_feerate = self.context.get_dust_buffer_feerate(None) as u64;
(dust_buffer_feerate * htlc_timeout_tx_weight(self.context.get_channel_type()) / 1000,
dust_buffer_feerate * htlc_success_tx_weight(self.context.get_channel_type()) / 1000)
dust_buffer_feerate * htlc_success_tx_weight(self.context.get_channel_type()) / 1000
};
let exposure_dust_limit_timeout_sats = htlc_timeout_dust_limit + self.context.counterparty_dust_limit_satoshis;
if msg.amount_msat / 1000 < exposure_dust_limit_timeout_sats {
let on_counterparty_tx_dust_htlc_exposure_msat = htlc_stats.on_counterparty_tx_dust_exposure_msat;
if on_counterparty_tx_dust_htlc_exposure_msat > max_dust_htlc_exposure_msat {
log_info!(logger, "Cannot accept value that would put our exposure to dust HTLCs at {} over the limit {} on counterparty commitment tx",
on_counterparty_tx_dust_htlc_exposure_msat, max_dust_htlc_exposure_msat);
return Err(("Exceeded our dust exposure limit on counterparty commitment tx", 0x1000|7))
}
} else {
let htlc_dust_exposure_msat =
per_outbound_htlc_counterparty_commit_tx_fee_msat(self.context.feerate_per_kw, &self.context.channel_type);
let counterparty_tx_dust_exposure =
htlc_stats.on_counterparty_tx_dust_exposure_msat.saturating_add(htlc_dust_exposure_msat);
if counterparty_tx_dust_exposure > max_dust_htlc_exposure_msat {
log_info!(logger, "Cannot accept value that would put our exposure to tx fee dust at {} over the limit {} on counterparty commitment tx",
counterparty_tx_dust_exposure, max_dust_htlc_exposure_msat);
return Err(("Exceeded our tx fee dust exposure limit on counterparty commitment tx", 0x1000|7))
}
}
let exposure_dust_limit_success_sats = htlc_success_dust_limit + self.context.holder_dust_limit_satoshis;
if msg.amount_msat / 1000 < exposure_dust_limit_success_sats {
let on_holder_tx_dust_htlc_exposure_msat = htlc_stats.on_holder_tx_dust_exposure_msat;

View file

@ -24,7 +24,7 @@ use crate::ln::types::ChannelId;
use crate::types::payment::{PaymentPreimage, PaymentSecret, PaymentHash};
use crate::ln::channel::{get_holder_selected_channel_reserve_satoshis, Channel, InboundV1Channel, OutboundV1Channel, COINBASE_MATURITY, CONCURRENT_INBOUND_HTLC_FEE_BUFFER, FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE, MIN_AFFORDABLE_HTLC_COUNT};
use crate::ln::channelmanager::{self, PaymentId, RAACommitmentOrder, RecipientOnionFields, BREAKDOWN_TIMEOUT, ENABLE_GOSSIP_TICKS, DISABLE_GOSSIP_TICKS, MIN_CLTV_EXPIRY_DELTA};
use crate::ln::channel::{DISCONNECT_PEER_AWAITING_RESPONSE_TICKS, ChannelError};
use crate::ln::channel::{DISCONNECT_PEER_AWAITING_RESPONSE_TICKS, ChannelError, MIN_CHAN_DUST_LIMIT_SATOSHIS};
use crate::ln::{chan_utils, onion_utils};
use crate::ln::chan_utils::{commitment_tx_base_weight, COMMITMENT_TX_WEIGHT_PER_HTLC, OFFERED_HTLC_SCRIPT_WEIGHT, htlc_success_tx_weight, htlc_timeout_tx_weight, HTLCOutputInCommitment};
use crate::routing::gossip::{NetworkGraph, NetworkUpdate};
@ -10378,13 +10378,13 @@ fn do_test_max_dust_htlc_exposure(dust_outbound_balance: bool, exposure_breach_e
} else { 0 };
let initial_feerate = if apply_excess_fee { 253 * 2 } else { 253 };
let expected_dust_buffer_feerate = initial_feerate + 2530;
let mut commitment_tx_cost = commit_tx_fee_msat(initial_feerate - 253, nondust_htlc_count_in_limit, &ChannelTypeFeatures::empty());
commitment_tx_cost +=
let mut commitment_tx_cost_msat = commit_tx_fee_msat(initial_feerate - 253, nondust_htlc_count_in_limit, &ChannelTypeFeatures::empty());
commitment_tx_cost_msat +=
if on_holder_tx {
htlc_success_tx_weight(&ChannelTypeFeatures::empty())
} else {
htlc_timeout_tx_weight(&ChannelTypeFeatures::empty())
} * (initial_feerate as u64 - 253) / 1000 * nondust_htlc_count_in_limit;
} * (initial_feerate as u64 - 253) * nondust_htlc_count_in_limit;
{
let mut feerate_lock = chanmon_cfgs[0].fee_estimator.sat_per_kw.lock().unwrap();
*feerate_lock = initial_feerate;
@ -10393,8 +10393,8 @@ fn do_test_max_dust_htlc_exposure(dust_outbound_balance: bool, exposure_breach_e
// Default test fee estimator rate is 253 sat/kw, so we set the multiplier to 5_000_000 / 253
// to get roughly the same initial value as the default setting when this test was
// originally written.
MaxDustHTLCExposure::FeeRateMultiplier((5_000_000 + commitment_tx_cost) / 253)
} else { MaxDustHTLCExposure::FixedLimitMsat(5_000_000 + commitment_tx_cost) };
MaxDustHTLCExposure::FeeRateMultiplier((5_000_000 + commitment_tx_cost_msat) / 253)
} else { MaxDustHTLCExposure::FixedLimitMsat(5_000_000 + commitment_tx_cost_msat) };
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config), None]);
let mut nodes = create_network(2, &node_cfgs, &node_chanmgrs);
@ -10538,8 +10538,8 @@ fn do_test_max_dust_htlc_exposure(dust_outbound_balance: bool, exposure_breach_e
} else {
// Outbound dust balance: 5200 sats
nodes[0].logger.assert_log("lightning::ln::channel",
format!("Cannot accept value that would put our exposure to dust HTLCs at {} over the limit {} on counterparty commitment tx",
dust_htlc_on_counterparty_tx_msat * dust_htlc_on_counterparty_tx + commitment_tx_cost + 4,
format!("Cannot accept value that would put our total dust exposure at {} over the limit {} on counterparty commitment tx",
dust_htlc_on_counterparty_tx_msat * dust_htlc_on_counterparty_tx + commitment_tx_cost_msat + 4,
max_dust_htlc_exposure_msat), 1);
}
} else if exposure_breach_event == ExposureEvent::AtUpdateFeeOutbound {
@ -10596,9 +10596,20 @@ fn test_max_dust_htlc_exposure() {
}
#[test]
fn test_nondust_htlc_fees_are_dust() {
// Test that the transaction fees paid in nondust HTLCs count towards our dust limit
fn test_nondust_htlc_excess_fees_are_dust() {
// Test that the excess transaction fees paid in nondust HTLCs count towards our dust limit
const DEFAULT_FEERATE: u32 = 253;
const HIGH_FEERATE: u32 = 275;
const EXCESS_FEERATE: u32 = HIGH_FEERATE - DEFAULT_FEERATE;
let chanmon_cfgs = create_chanmon_cfgs(3);
{
// Set the feerate of the channel funder above the `dust_exposure_limiting_feerate` of
// the fundee. This delta means that the fundee will add the mining fees of the commitment and
// htlc transactions in excess of its `dust_exposure_limiting_feerate` to its total dust htlc
// exposure.
let mut feerate_lock = chanmon_cfgs[1].fee_estimator.sat_per_kw.lock().unwrap();
*feerate_lock = HIGH_FEERATE;
}
let node_cfgs = create_node_cfgs(3, &chanmon_cfgs);
let mut config = test_default_channel_config();
@ -10606,18 +10617,16 @@ fn test_nondust_htlc_fees_are_dust() {
config.channel_config.max_dust_htlc_exposure =
MaxDustHTLCExposure::FeeRateMultiplier(10_000);
// Make sure the HTLC limits don't get in the way
config.channel_handshake_limits.min_max_accepted_htlcs = 400;
config.channel_handshake_config.our_max_accepted_htlcs = 400;
config.channel_handshake_limits.min_max_accepted_htlcs = chan_utils::MAX_HTLCS;
config.channel_handshake_config.our_max_accepted_htlcs = chan_utils::MAX_HTLCS;
config.channel_handshake_config.our_htlc_minimum_msat = 1;
config.channel_handshake_config.max_inbound_htlc_value_in_flight_percent_of_channel = 100;
let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[Some(config), Some(config), Some(config)]);
let nodes = create_network(3, &node_cfgs, &node_chanmgrs);
// Create a channel from 1 -> 0 but immediately push all of the funds towards 0
let chan_id_1 = create_announced_chan_between_nodes(&nodes, 1, 0).2;
while nodes[1].node.list_channels()[0].next_outbound_htlc_limit_msat > 0 {
send_payment(&nodes[1], &[&nodes[0]], nodes[1].node.list_channels()[0].next_outbound_htlc_limit_msat);
}
// Leave enough on the funder side to let it pay the mining fees for a commit tx with tons of htlcs
let chan_id_1 = create_announced_chan_between_nodes_with_value(&nodes, 1, 0, 1_000_000, 750_000_000).2;
// First get the channel one HTLC_VALUE HTLC away from the dust limit by sending dust HTLCs
// repeatedly until we run out of space.
@ -10637,16 +10646,24 @@ fn test_nondust_htlc_fees_are_dust() {
assert_ne!(nodes[0].node.list_channels()[0].next_outbound_htlc_minimum_msat, 0,
"Make sure we are able to send once we clear one HTLC");
// Skip the router complaint when node 0 will attempt to pay node 1
let (route_0_1, payment_hash_0_1, _, payment_secret_0_1) = get_route_and_payment_hash!(nodes[0], nodes[1], dust_limit * 2);
assert_eq!(nodes[0].node.list_channels().len(), 1);
assert_eq!(nodes[1].node.list_channels().len(), 1);
assert_eq!(nodes[0].node.list_channels()[0].pending_inbound_htlcs.len(), 0);
assert_eq!(nodes[1].node.list_channels()[0].pending_outbound_htlcs.len(), 0);
// At this point we have somewhere between dust_limit and dust_limit * 2 left in our dust
// exposure limit, and we want to max that out using non-dust HTLCs.
let commitment_tx_per_htlc_cost =
htlc_success_tx_weight(&ChannelTypeFeatures::empty()) * 253;
htlc_success_tx_weight(&ChannelTypeFeatures::empty()) * EXCESS_FEERATE as u64;
let max_htlcs_remaining = dust_limit * 2 / commitment_tx_per_htlc_cost;
assert!(max_htlcs_remaining < 30,
assert!(max_htlcs_remaining < chan_utils::MAX_HTLCS.into(),
"We should be able to fill our dust limit without too many HTLCs");
for i in 0..max_htlcs_remaining + 1 {
assert_ne!(i, max_htlcs_remaining);
if nodes[0].node.list_channels()[0].next_outbound_htlc_limit_msat < dust_limit {
if nodes[0].node.list_channels()[0].next_outbound_htlc_limit_msat <= dust_limit {
// We found our limit, and it was less than max_htlcs_remaining!
// At this point we can only send dust HTLCs as any non-dust HTLCs will overuse our
// remaining dust exposure.
@ -10655,6 +10672,57 @@ fn test_nondust_htlc_fees_are_dust() {
route_payment(&nodes[0], &[&nodes[1]], dust_limit * 2);
}
assert_eq!(nodes[0].node.list_channels().len(), 1);
assert_eq!(nodes[1].node.list_channels().len(), 1);
assert_eq!(nodes[0].node.list_channels()[0].pending_inbound_htlcs.len(), 0);
assert_eq!(nodes[1].node.list_channels()[0].pending_outbound_htlcs.len(), 0);
// Send an additional non-dust htlc from 1 to 0, and check the complaint
let (route, payment_hash, _, payment_secret) = get_route_and_payment_hash!(nodes[1], nodes[0], dust_limit * 2);
nodes[1].node.send_payment_with_route(route, payment_hash,
RecipientOnionFields::secret_only(payment_secret), PaymentId(payment_hash.0)).unwrap();
check_added_monitors!(nodes[1], 1);
let mut events = nodes[1].node.get_and_clear_pending_msg_events();
assert_eq!(events.len(), 1);
let payment_event = SendEvent::from_event(events.remove(0));
nodes[0].node.handle_update_add_htlc(nodes[1].node.get_our_node_id(), &payment_event.msgs[0]);
commitment_signed_dance!(nodes[0], nodes[1], payment_event.commitment_msg, false);
expect_pending_htlcs_forwardable!(nodes[0]);
expect_htlc_handling_failed_destinations!(nodes[0].node.get_and_clear_pending_events(), &[HTLCDestination::FailedPayment { payment_hash }]);
nodes[0].logger.assert_log("lightning::ln::channel",
format!("Cannot accept value that would put our total dust exposure at {} over the limit {} on counterparty commitment tx",
2535000, 2530000), 1);
check_added_monitors!(nodes[0], 1);
// Clear the failed htlc
let updates = get_htlc_update_msgs!(nodes[0], nodes[1].node.get_our_node_id());
assert!(updates.update_add_htlcs.is_empty());
assert!(updates.update_fulfill_htlcs.is_empty());
assert_eq!(updates.update_fail_htlcs.len(), 1);
assert!(updates.update_fail_malformed_htlcs.is_empty());
assert!(updates.update_fee.is_none());
nodes[1].node.handle_update_fail_htlc(nodes[0].node.get_our_node_id(), &updates.update_fail_htlcs[0]);
commitment_signed_dance!(nodes[1], nodes[0], updates.commitment_signed, false);
expect_payment_failed!(nodes[1], payment_hash, false);
assert_eq!(nodes[0].node.list_channels().len(), 1);
assert_eq!(nodes[1].node.list_channels().len(), 1);
assert_eq!(nodes[0].node.list_channels()[0].pending_inbound_htlcs.len(), 0);
assert_eq!(nodes[1].node.list_channels()[0].pending_outbound_htlcs.len(), 0);
// Send an additional non-dust htlc from 0 to 1 using the pre-calculated route above, and check the immediate complaint
unwrap_send_err!(nodes[0], nodes[0].node.send_payment_with_route(route_0_1, payment_hash_0_1,
RecipientOnionFields::secret_only(payment_secret_0_1), PaymentId(payment_hash_0_1.0)
), true, APIError::ChannelUnavailable { .. }, {});
nodes[0].logger.assert_log("lightning::ln::outbound_payment",
format!("Failed to send along path due to error: Channel unavailable: Cannot send more than our next-HTLC maximum - {} msat", 2325000), 1);
assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty());
assert_eq!(nodes[0].node.list_channels().len(), 1);
assert_eq!(nodes[1].node.list_channels().len(), 1);
assert_eq!(nodes[0].node.list_channels()[0].pending_inbound_htlcs.len(), 0);
assert_eq!(nodes[1].node.list_channels()[0].pending_outbound_htlcs.len(), 0);
// At this point non-dust HTLCs are no longer accepted from node 0 -> 1, we also check that
// such HTLCs can't be routed over the same channel either.
create_announced_chan_between_nodes(&nodes, 2, 0);
@ -10682,6 +10750,204 @@ fn test_nondust_htlc_fees_are_dust() {
expect_payment_failed_conditions(&nodes[2], payment_hash, false, PaymentFailedConditions::new());
}
fn do_test_nondust_htlc_fees_dust_exposure_delta(features: ChannelTypeFeatures) {
// Tests the increase in htlc dust exposure due to the excess mining fees of a single non-dust
// HTLC on the counterparty commitment transaction, for both incoming and outgoing htlcs.
//
// Brings the dust exposure up to the base dust exposure using dust htlcs.
// Sets the max dust exposure to 1msat below the expected dust exposure given an additional non-dust htlc.
// Checks a failed payment for a non-dust htlc.
// Sets the max dust exposure equal to the expected dust exposure given an additional non-dust htlc.
// Checks a successful payment for a non-dust htlc.
//
// Runs this sequence for both directions.
let chanmon_cfgs = create_chanmon_cfgs(2);
const DEFAULT_FEERATE: u64 = 253;
const HIGH_FEERATE: u64 = 275;
const EXCESS_FEERATE: u64 = HIGH_FEERATE - DEFAULT_FEERATE;
const DUST_HTLC_COUNT: usize = 4;
// Set dust htlcs to a satoshi value plus a non-zero msat amount to assert that
// the dust accounting rounds transaction fees to the lower satoshi, but does not round dust htlc values.
const DUST_HTLC_MSAT: u64 = 125_123;
const BASE_DUST_EXPOSURE_MSAT: u64 = DUST_HTLC_COUNT as u64 * DUST_HTLC_MSAT;
const NON_DUST_HTLC_MSAT: u64 = 4_000_000;
{
// Set the feerate of the channel funder above the `dust_exposure_limiting_feerate` of
// the fundee. This delta means that the fundee will add the mining fees of the commitment and
// htlc transactions in excess of its `dust_exposure_limiting_feerate` to its total dust htlc
// exposure.
let mut feerate_lock = chanmon_cfgs[0].fee_estimator.sat_per_kw.lock().unwrap();
*feerate_lock = HIGH_FEERATE as u32;
}
// Set `expected_dust_exposure_msat` to match the calculation in `FundedChannel::can_accept_incoming_htlc`
// only_static_remote_key: 500_492 + 22 * (724 + 172) / 1000 * 1000 + 22 * 663 / 1000 * 1000 = 533_492
// anchors_zero_htlc_fee: 500_492 + 22 * (1_124 + 172) / 1000 * 1000 = 528_492
let mut expected_dust_exposure_msat = BASE_DUST_EXPOSURE_MSAT + EXCESS_FEERATE * (commitment_tx_base_weight(&features) + COMMITMENT_TX_WEIGHT_PER_HTLC) / 1000 * 1000;
if features == ChannelTypeFeatures::only_static_remote_key() {
expected_dust_exposure_msat += EXCESS_FEERATE * htlc_timeout_tx_weight(&features) / 1000 * 1000;
assert_eq!(expected_dust_exposure_msat, 533_492);
} else {
assert_eq!(expected_dust_exposure_msat, 528_492);
}
let mut default_config = test_default_channel_config();
if features == ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies() {
default_config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = true;
// in addition to the one above, this setting is also needed to create an anchor channel
default_config.manually_accept_inbound_channels = true;
}
// Set node 1's max dust htlc exposure to 1msat below `expected_dust_exposure_msat`
let mut fixed_limit_config = default_config.clone();
fixed_limit_config.channel_config.max_dust_htlc_exposure = MaxDustHTLCExposure::FixedLimitMsat(expected_dust_exposure_msat - 1);
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(default_config), Some(fixed_limit_config)]);
let mut nodes = create_network(2, &node_cfgs, &node_chanmgrs);
let chan_id = create_chan_between_nodes_with_value(&nodes[0], &nodes[1], 100_000, 50_000_000).3;
let node_1_dust_buffer_feerate = {
let per_peer_state = nodes[1].node.per_peer_state.read().unwrap();
let chan_lock = per_peer_state.get(&nodes[0].node.get_our_node_id()).unwrap().lock().unwrap();
let chan = chan_lock.channel_by_id.get(&chan_id).unwrap();
chan.context().get_dust_buffer_feerate(None) as u64
};
// Skip the router complaint when node 1 will attempt to pay node 0
let (route_1_0, payment_hash_1_0, _, payment_secret_1_0) = get_route_and_payment_hash!(nodes[1], nodes[0], NON_DUST_HTLC_MSAT);
// Bring node 1's dust htlc exposure up to `BASE_DUST_EXPOSURE_MSAT`
for _ in 0..DUST_HTLC_COUNT {
route_payment(&nodes[0], &[&nodes[1]], DUST_HTLC_MSAT);
}
assert_eq!(nodes[0].node.list_channels().len(), 1);
assert_eq!(nodes[1].node.list_channels().len(), 1);
assert_eq!(nodes[0].node.list_channels()[0].pending_inbound_htlcs.len(), 0);
assert_eq!(nodes[1].node.list_channels()[0].pending_outbound_htlcs.len(), 0);
assert_eq!(nodes[0].node.list_channels()[0].pending_outbound_htlcs.len(), DUST_HTLC_COUNT);
assert_eq!(nodes[1].node.list_channels()[0].pending_inbound_htlcs.len(), DUST_HTLC_COUNT);
// Send an additional non-dust htlc from 0 to 1, and check the complaint
let (route, payment_hash, _, payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[1], NON_DUST_HTLC_MSAT);
nodes[0].node.send_payment_with_route(route, payment_hash,
RecipientOnionFields::secret_only(payment_secret), PaymentId(payment_hash.0)).unwrap();
check_added_monitors!(nodes[0], 1);
let mut events = nodes[0].node.get_and_clear_pending_msg_events();
assert_eq!(events.len(), 1);
let payment_event = SendEvent::from_event(events.remove(0));
nodes[1].node.handle_update_add_htlc(nodes[0].node.get_our_node_id(), &payment_event.msgs[0]);
commitment_signed_dance!(nodes[1], nodes[0], payment_event.commitment_msg, false);
expect_pending_htlcs_forwardable!(nodes[1]);
expect_htlc_handling_failed_destinations!(nodes[1].node.get_and_clear_pending_events(), &[HTLCDestination::FailedPayment { payment_hash }]);
nodes[1].logger.assert_log("lightning::ln::channel",
format!("Cannot accept value that would put our total dust exposure at {} over the limit {} on counterparty commitment tx",
expected_dust_exposure_msat, expected_dust_exposure_msat - 1), 1);
check_added_monitors!(nodes[1], 1);
// Clear the failed htlc
let updates = get_htlc_update_msgs!(nodes[1], nodes[0].node.get_our_node_id());
assert!(updates.update_add_htlcs.is_empty());
assert!(updates.update_fulfill_htlcs.is_empty());
assert_eq!(updates.update_fail_htlcs.len(), 1);
assert!(updates.update_fail_malformed_htlcs.is_empty());
assert!(updates.update_fee.is_none());
nodes[0].node.handle_update_fail_htlc(nodes[1].node.get_our_node_id(), &updates.update_fail_htlcs[0]);
commitment_signed_dance!(nodes[0], nodes[1], updates.commitment_signed, false);
expect_payment_failed!(nodes[0], payment_hash, false);
assert_eq!(nodes[0].node.list_channels().len(), 1);
assert_eq!(nodes[1].node.list_channels().len(), 1);
assert_eq!(nodes[0].node.list_channels()[0].pending_inbound_htlcs.len(), 0);
assert_eq!(nodes[1].node.list_channels()[0].pending_outbound_htlcs.len(), 0);
assert_eq!(nodes[0].node.list_channels()[0].pending_outbound_htlcs.len(), DUST_HTLC_COUNT);
assert_eq!(nodes[1].node.list_channels()[0].pending_inbound_htlcs.len(), DUST_HTLC_COUNT);
// Set node 1's max dust htlc exposure equal to the `expected_dust_exposure_msat`
nodes[1].node.update_partial_channel_config(&nodes[0].node.get_our_node_id(), &[chan_id], &ChannelConfigUpdate {
max_dust_htlc_exposure_msat: Some(MaxDustHTLCExposure::FixedLimitMsat(expected_dust_exposure_msat)),
..ChannelConfigUpdate::default()
}).unwrap();
// Check a successful payment
send_payment(&nodes[0], &[&nodes[1]], NON_DUST_HTLC_MSAT);
assert_eq!(nodes[0].node.list_channels().len(), 1);
assert_eq!(nodes[1].node.list_channels().len(), 1);
assert_eq!(nodes[0].node.list_channels()[0].pending_inbound_htlcs.len(), 0);
assert_eq!(nodes[1].node.list_channels()[0].pending_outbound_htlcs.len(), 0);
assert_eq!(nodes[0].node.list_channels()[0].pending_outbound_htlcs.len(), DUST_HTLC_COUNT);
assert_eq!(nodes[1].node.list_channels()[0].pending_inbound_htlcs.len(), DUST_HTLC_COUNT);
// The `expected_dust_exposure_msat` for the outbound htlc changes in the non-anchor case, as the htlc success and timeout transactions have different weights
// only_static_remote_key: 500_492 + 22 * (724 + 172) / 1000 * 1000 + 22 * 703 / 1000 * 1000 = 534_492
if features == ChannelTypeFeatures::only_static_remote_key() {
expected_dust_exposure_msat = BASE_DUST_EXPOSURE_MSAT + EXCESS_FEERATE * (commitment_tx_base_weight(&features) + COMMITMENT_TX_WEIGHT_PER_HTLC) / 1000 * 1000 + EXCESS_FEERATE * htlc_success_tx_weight(&features) / 1000 * 1000;
assert_eq!(expected_dust_exposure_msat, 534_492);
} else {
assert_eq!(expected_dust_exposure_msat, 528_492);
}
// Set node 1's max dust htlc exposure to 1msat below `expected_dust_exposure_msat`
nodes[1].node.update_partial_channel_config(&nodes[0].node.get_our_node_id(), &[chan_id], &ChannelConfigUpdate {
max_dust_htlc_exposure_msat: Some(MaxDustHTLCExposure::FixedLimitMsat(expected_dust_exposure_msat - 1)),
..ChannelConfigUpdate::default()
}).unwrap();
// Send an additional non-dust htlc from 1 to 0 using the pre-calculated route above, and check the immediate complaint
unwrap_send_err!(nodes[1], nodes[1].node.send_payment_with_route(route_1_0, payment_hash_1_0,
RecipientOnionFields::secret_only(payment_secret_1_0), PaymentId(payment_hash_1_0.0)
), true, APIError::ChannelUnavailable { .. }, {});
let dust_limit = if features == ChannelTypeFeatures::only_static_remote_key() {
MIN_CHAN_DUST_LIMIT_SATOSHIS * 1000 + htlc_success_tx_weight(&features) * node_1_dust_buffer_feerate / 1000 * 1000
} else {
MIN_CHAN_DUST_LIMIT_SATOSHIS * 1000
};
nodes[1].logger.assert_log("lightning::ln::outbound_payment",
format!("Failed to send along path due to error: Channel unavailable: Cannot send more than our next-HTLC maximum - {} msat", dust_limit), 1);
assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty());
assert_eq!(nodes[0].node.list_channels().len(), 1);
assert_eq!(nodes[1].node.list_channels().len(), 1);
assert_eq!(nodes[0].node.list_channels()[0].pending_inbound_htlcs.len(), 0);
assert_eq!(nodes[1].node.list_channels()[0].pending_outbound_htlcs.len(), 0);
assert_eq!(nodes[0].node.list_channels()[0].pending_outbound_htlcs.len(), DUST_HTLC_COUNT);
assert_eq!(nodes[1].node.list_channels()[0].pending_inbound_htlcs.len(), DUST_HTLC_COUNT);
// Set node 1's max dust htlc exposure equal to `expected_dust_exposure_msat`
nodes[1].node.update_partial_channel_config(&nodes[0].node.get_our_node_id(), &[chan_id], &ChannelConfigUpdate {
max_dust_htlc_exposure_msat: Some(MaxDustHTLCExposure::FixedLimitMsat(expected_dust_exposure_msat)),
..ChannelConfigUpdate::default()
}).unwrap();
// Check a successful payment
send_payment(&nodes[1], &[&nodes[0]], NON_DUST_HTLC_MSAT);
assert_eq!(nodes[0].node.list_channels().len(), 1);
assert_eq!(nodes[1].node.list_channels().len(), 1);
assert_eq!(nodes[0].node.list_channels()[0].pending_inbound_htlcs.len(), 0);
assert_eq!(nodes[1].node.list_channels()[0].pending_outbound_htlcs.len(), 0);
assert_eq!(nodes[0].node.list_channels()[0].pending_outbound_htlcs.len(), DUST_HTLC_COUNT);
assert_eq!(nodes[1].node.list_channels()[0].pending_inbound_htlcs.len(), DUST_HTLC_COUNT);
}
#[test]
fn test_nondust_htlc_fees_dust_exposure_delta() {
do_test_nondust_htlc_fees_dust_exposure_delta(ChannelTypeFeatures::only_static_remote_key());
do_test_nondust_htlc_fees_dust_exposure_delta(ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies());
}
#[test]
fn test_non_final_funding_tx() {