Validate reserved fees on both commitments
What changed, and why it matters
This commit fixes a fee-reservation bug in a Lightning Network node implementation. When a node calculates how much money it can still send through a channel, it must set aside enough funds to pay the on-chain transaction fee if the channel later closes. Previously, the calculation only considered the node's own view of the commitment transaction. Because the two channel partners can agree on different 'dust limits,' the actual fee on the partner's version of the commitment can be higher. If the node did not reserve enough, it could propose a payment that is valid by its own numbers but violates the protocol when the partner checks it, potentially causing the channel update to be rejected or the channel to become inconsistent. The patch now reserves the larger of the two possible fees.
Review the updated fee-reservation logic for correctness under all channel types and dust-limit combinations; run the new unit tests; consider whether any other balance-reporting paths (e.g., `ChannelDetails` fields) need similar dual-commitment treatment.
Security signals we found
Fee-reservation miscalculation in channel balance availability
Asymmetric commitment transaction fees due to differing dust limits
Potential protocol violation when proposing channel state updates
Fix validated by new unit tests covering both funder/fundee dust asymmetry
Evidence from the diff
In rust-lightning’s get_available_balances path, adjust_capacity_for_holder_reserved_fee and adjust_capacity_for_counterparty_reserved_fee previously computed available capacity using only one side’s non-dust HTLC count and dust limit. Since local and remote commitments can have different dust limits, the commit tx fee can differ between the two commitments. The patch rewrites both helpers to compute the available capacity against both the local and remote commitment parameters and returns the minimum (more restrictive) of the two. This ensures next_outbound_htlc_limit_msat and splice-out limits reserve enough fees for either commitment view. New unit tests test_available_balances_both_commitments_dust_on_funder_commitment and test_available_balances_both_commitments_dust_on_fundee_commitment exercise asymmetric dust limits and verify the resulting capacity/splice limits.
Changed components
lightning/src/sign/tx_builder.rslightning/src/ln/htlc_reserve_unit_tests.rsInspect captured patch +355 / −81
diff --git a/lightning/src/ln/htlc_reserve_unit_tests.rs b/lightning/src/ln/htlc_reserve_unit_tests.rs
index 86d98b7..290ae18 100644
--- a/lightning/src/ln/htlc_reserve_unit_tests.rs
+++ b/lightning/src/ln/htlc_reserve_unit_tests.rs
@@ -1053,10 +1053,10 @@ pub fn test_chan_reserve_dust_inbound_htlcs_outbound_chan() {
* 1000;
create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100000, push_amt);
- let (htlc_success_tx_fee_sat, _) =
+ let (_htlc_success_tx_fee_sat, htlc_timeout_tx_fee_sat) =
second_stage_tx_fees_sat(&channel_type_features, feerate_per_kw);
let dust_amt = crate::ln::channel::MIN_CHAN_DUST_LIMIT_SATOSHIS * 1000
- + htlc_success_tx_fee_sat * 1000
+ + htlc_timeout_tx_fee_sat * 1000
- 1;
// In the previous code, routing this dust payment would cause nodes[0] to perceive a channel
// reserve violation even though it's a dust HTLC and therefore shouldn't count towards the
@@ -3528,3 +3528,268 @@ fn test_fail_cannot_afford_dust_htlcs_at_spike_multiple_if_nondust_at_base_feera
true,
);
}
+
+#[xtest(feature = "_externalize_tests")]
+fn test_available_balances_both_commitments_dust_on_funder_commitment() {
+ let mut config = test_default_channel_config();
+
+ let chanmon_cfgs = create_chanmon_cfgs(2);
+ let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
+ config.channel_handshake_config.announced_channel_max_inbound_htlc_value_in_flight_percentage =
+ 100;
+
+ let channel_type = ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies();
+
+ let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]);
+ let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
+
+ const FEERATE: u32 = 253;
+ const TOTAL_ANCHORS_MSAT: u64 = 2 * 330_000;
+ const NODE_0_DUST_LIMIT_MSAT: u64 = 10_000 * 1000;
+ const NODE_1_DUST_LIMIT_MSAT: u64 = 354 * 1000;
+ const CHANNEL_VALUE_MSAT: u64 = 50_000 * 1000;
+ const NODE_0_VALUE_TO_SELF_MSAT: u64 = 25_000 * 1000;
+ const NODE_1_VALUE_TO_SELF_MSAT: u64 = 25_000 * 1000;
+ const NODE_0_SELECTED_CHANNEL_RESERVE_MSAT: u64 = 1_000 * 1_000;
+ const NODE_1_SELECTED_CHANNEL_RESERVE_MSAT: u64 = 10_000 * 1_000;
+
+ let channel_id = create_announced_chan_between_nodes_with_value(
+ &nodes,
+ 0,
+ 1,
+ CHANNEL_VALUE_MSAT / 1000,
+ NODE_1_VALUE_TO_SELF_MSAT,
+ )
+ .2;
+ assert_eq!(nodes[0].node.list_channels()[0].channel_type.as_ref().unwrap(), &channel_type);
+
+ {
+ let per_peer_state_lock;
+ let mut peer_state_lock;
+ let chan =
+ get_channel_ref!(nodes[0], nodes[1], per_peer_state_lock, peer_state_lock, channel_id);
+ chan.context_mut().holder_dust_limit_satoshis = NODE_0_DUST_LIMIT_MSAT / 1000;
+ chan.funding_mut().counterparty_selected_channel_reserve_satoshis =
+ Some(NODE_1_SELECTED_CHANNEL_RESERVE_MSAT / 1000);
+ assert_eq!(chan.context().counterparty_dust_limit_satoshis, NODE_1_DUST_LIMIT_MSAT / 1000);
+ assert_eq!(
+ chan.funding().holder_selected_channel_reserve_satoshis,
+ NODE_0_SELECTED_CHANNEL_RESERVE_MSAT / 1000
+ );
+ }
+
+ {
+ let per_peer_state_lock;
+ let mut peer_state_lock;
+ let chan =
+ get_channel_ref!(nodes[1], nodes[0], per_peer_state_lock, peer_state_lock, channel_id);
+ chan.context_mut().counterparty_dust_limit_satoshis = NODE_0_DUST_LIMIT_MSAT / 1000;
+ chan.funding_mut().holder_selected_channel_reserve_satoshis =
+ NODE_1_SELECTED_CHANNEL_RESERVE_MSAT / 1000;
+ assert_eq!(chan.context().holder_dust_limit_satoshis, NODE_1_DUST_LIMIT_MSAT / 1000);
+ assert_eq!(
+ chan.funding().counterparty_selected_channel_reserve_satoshis,
+ Some(NODE_0_SELECTED_CHANNEL_RESERVE_MSAT / 1000)
+ );
+ }
+
+ // This HTLC is only present on node 1's commitment
+ const SNEAKY_HTLC_MSAT: u64 = 5_000_000;
+
+ route_payment(&nodes[1], &[&nodes[0]], SNEAKY_HTLC_MSAT);
+
+ let node_1_details = &nodes[1].node.list_channels()[0];
+ let expected_outbound_capacity_msat =
+ NODE_1_VALUE_TO_SELF_MSAT - SNEAKY_HTLC_MSAT - NODE_0_SELECTED_CHANNEL_RESERVE_MSAT;
+ assert_eq!(node_1_details.outbound_capacity_msat, expected_outbound_capacity_msat);
+ let expected_available_capacity_msat = expected_outbound_capacity_msat;
+ assert_eq!(node_1_details.next_outbound_htlc_limit_msat, expected_available_capacity_msat);
+ let expected_splice_out_max =
+ NODE_1_VALUE_TO_SELF_MSAT / 1000 - SNEAKY_HTLC_MSAT / 1000 - NODE_1_DUST_LIMIT_MSAT / 1000;
+ assert_eq!(node_1_details.next_splice_out_maximum_sat, expected_splice_out_max);
+
+ let node_0_details = &nodes[0].node.list_channels()[0];
+ let expected_outbound_capacity_msat =
+ NODE_0_VALUE_TO_SELF_MSAT - NODE_1_SELECTED_CHANNEL_RESERVE_MSAT - TOTAL_ANCHORS_MSAT;
+ assert_eq!(node_0_details.outbound_capacity_msat, expected_outbound_capacity_msat);
+ let expected_available_capacity_msat =
+ expected_outbound_capacity_msat - commit_tx_fee_sat(FEERATE, 3, &channel_type) * 1000;
+ assert_eq!(node_0_details.next_outbound_htlc_limit_msat, expected_available_capacity_msat);
+ let expected_splice_out_max = NODE_0_VALUE_TO_SELF_MSAT / 1000
+ - TOTAL_ANCHORS_MSAT / 1000
+ - commit_tx_fee_sat(FEERATE, 2, &channel_type)
+ - NODE_0_DUST_LIMIT_MSAT / 1000;
+ assert_eq!(node_0_details.next_splice_out_maximum_sat, expected_splice_out_max);
+
+ let node_0_payment_msat = expected_available_capacity_msat;
+ send_payment(&nodes[0], &[&nodes[1]], node_0_payment_msat);
+
+ route_payment(&nodes[1], &[&nodes[0]], SNEAKY_HTLC_MSAT);
+ route_payment(&nodes[1], &[&nodes[0]], SNEAKY_HTLC_MSAT);
+
+ let node_0_details = &nodes[0].node.list_channels()[0];
+ let expected_outbound_capacity_msat = NODE_0_VALUE_TO_SELF_MSAT
+ - node_0_payment_msat
+ - NODE_1_SELECTED_CHANNEL_RESERVE_MSAT
+ - TOTAL_ANCHORS_MSAT;
+ assert_eq!(node_0_details.outbound_capacity_msat, expected_outbound_capacity_msat);
+ assert_eq!(
+ node_0_details.outbound_capacity_msat,
+ commit_tx_fee_sat(FEERATE, 3, &channel_type) * 1000
+ );
+ assert_eq!(node_0_details.next_outbound_htlc_limit_msat, 0);
+ assert_eq!(node_0_details.next_splice_out_maximum_sat, 0);
+
+ let node_1_details = &nodes[1].node.list_channels()[0];
+ let expected_outbound_capacity_msat = NODE_1_VALUE_TO_SELF_MSAT + node_0_payment_msat
+ - 3 * SNEAKY_HTLC_MSAT
+ - NODE_0_SELECTED_CHANNEL_RESERVE_MSAT;
+ assert_eq!(node_1_details.outbound_capacity_msat, expected_outbound_capacity_msat);
+ let (_htlc_success_tx_fee_sat, htlc_timeout_tx_fee_sat) =
+ second_stage_tx_fees_sat(&channel_type, FEERATE);
+ let expected_available_capacity_msat =
+ (NODE_1_DUST_LIMIT_MSAT / 1000 + htlc_timeout_tx_fee_sat) * 1000 - 1;
+ assert_eq!(node_1_details.next_outbound_htlc_limit_msat, expected_available_capacity_msat);
+ let expected_splice_out_max = NODE_1_VALUE_TO_SELF_MSAT / 1000 + node_0_payment_msat / 1000
+ - 3 * SNEAKY_HTLC_MSAT / 1000
+ - NODE_1_DUST_LIMIT_MSAT / 1000;
+ assert_eq!(node_1_details.next_splice_out_maximum_sat, expected_splice_out_max);
+}
+
+#[xtest(feature = "_externalize_tests")]
+fn test_available_balances_both_commitments_dust_on_fundee_commitment() {
+ let mut config = test_default_channel_config();
+
+ let chanmon_cfgs = create_chanmon_cfgs(2);
+ let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
+ config.channel_handshake_config.announced_channel_max_inbound_htlc_value_in_flight_percentage =
+ 100;
+
+ let channel_type = ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies();
+
+ let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]);
+ let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
+
+ const FEERATE: u32 = 253;
+ const TOTAL_ANCHORS_MSAT: u64 = 2 * 330_000;
+ const NODE_0_DUST_LIMIT_MSAT: u64 = 354 * 1000;
+ const NODE_1_DUST_LIMIT_MSAT: u64 = 10_000 * 1000;
+ const CHANNEL_VALUE_MSAT: u64 = 50_000 * 1000;
+ const NODE_0_VALUE_TO_SELF_MSAT: u64 = 25_000 * 1000;
+ const NODE_1_VALUE_TO_SELF_MSAT: u64 = 25_000 * 1000;
+ const NODE_0_SELECTED_CHANNEL_RESERVE_MSAT: u64 = 10_000 * 1_000;
+ const NODE_1_SELECTED_CHANNEL_RESERVE_MSAT: u64 = 1_000 * 1_000;
+
+ let channel_id = create_announced_chan_between_nodes_with_value(
+ &nodes,
+ 0,
+ 1,
+ CHANNEL_VALUE_MSAT / 1000,
+ NODE_1_VALUE_TO_SELF_MSAT,
+ )
+ .2;
+ assert_eq!(nodes[0].node.list_channels()[0].channel_type.as_ref().unwrap(), &channel_type);
+
+ {
+ let per_peer_state_lock;
+ let mut peer_state_lock;
+ let chan =
+ get_channel_ref!(nodes[0], nodes[1], per_peer_state_lock, peer_state_lock, channel_id);
+ chan.context_mut().counterparty_dust_limit_satoshis = NODE_1_DUST_LIMIT_MSAT / 1000;
+ chan.funding_mut().holder_selected_channel_reserve_satoshis =
+ NODE_0_SELECTED_CHANNEL_RESERVE_MSAT / 1000;
+ assert_eq!(chan.context().holder_dust_limit_satoshis, NODE_0_DUST_LIMIT_MSAT / 1000);
+ assert_eq!(
+ chan.funding().counterparty_selected_channel_reserve_satoshis,
+ Some(NODE_1_SELECTED_CHANNEL_RESERVE_MSAT / 1000)
+ );
+ }
+
+ {
+ let per_peer_state_lock;
+ let mut peer_state_lock;
+ let chan =
+ get_channel_ref!(nodes[1], nodes[0], per_peer_state_lock, peer_state_lock, channel_id);
+ chan.context_mut().holder_dust_limit_satoshis = NODE_1_DUST_LIMIT_MSAT / 1000;
+ chan.funding_mut().counterparty_selected_channel_reserve_satoshis =
+ Some(NODE_0_SELECTED_CHANNEL_RESERVE_MSAT / 1000);
+ assert_eq!(chan.context().counterparty_dust_limit_satoshis, NODE_0_DUST_LIMIT_MSAT / 1000);
+ assert_eq!(
+ chan.funding().holder_selected_channel_reserve_satoshis,
+ NODE_1_SELECTED_CHANNEL_RESERVE_MSAT / 1000
+ );
+ }
+
+ // This HTLC is only present on node 0's commitment
+ const SNEAKY_HTLC_MSAT: u64 = 5_000_000;
+
+ route_payment(&nodes[1], &[&nodes[0]], SNEAKY_HTLC_MSAT);
+
+ let node_1_details = &nodes[1].node.list_channels()[0];
+ let expected_outbound_capacity_msat =
+ NODE_1_VALUE_TO_SELF_MSAT - SNEAKY_HTLC_MSAT - NODE_0_SELECTED_CHANNEL_RESERVE_MSAT;
+ assert_eq!(node_1_details.outbound_capacity_msat, expected_outbound_capacity_msat);
+ let expected_available_capacity_msat = expected_outbound_capacity_msat;
+ assert_eq!(node_1_details.next_outbound_htlc_limit_msat, expected_available_capacity_msat);
+ let expected_splice_out_max =
+ NODE_1_VALUE_TO_SELF_MSAT / 1000 - SNEAKY_HTLC_MSAT / 1000 - NODE_1_DUST_LIMIT_MSAT / 1000;
+ assert_eq!(node_1_details.next_splice_out_maximum_sat, expected_splice_out_max);
+
+ let node_0_details = &nodes[0].node.list_channels()[0];
+
+ let expected_outbound_capacity_msat =
+ NODE_0_VALUE_TO_SELF_MSAT - NODE_1_SELECTED_CHANNEL_RESERVE_MSAT - TOTAL_ANCHORS_MSAT;
+ assert_eq!(node_0_details.outbound_capacity_msat, expected_outbound_capacity_msat);
+
+ let expected_splice_out_max = NODE_0_VALUE_TO_SELF_MSAT / 1000
+ - TOTAL_ANCHORS_MSAT / 1000
+ - commit_tx_fee_sat(FEERATE, 2, &channel_type)
+ - NODE_0_DUST_LIMIT_MSAT / 1000;
+ assert_eq!(node_0_details.next_splice_out_maximum_sat, expected_splice_out_max);
+
+ let expected_available_capacity_msat =
+ expected_outbound_capacity_msat - commit_tx_fee_sat(FEERATE, 3, &channel_type) * 1000;
+ assert_eq!(node_0_details.next_outbound_htlc_limit_msat, expected_available_capacity_msat);
+
+ let node_0_payment_msat = expected_available_capacity_msat;
+ send_payment(&nodes[0], &[&nodes[1]], node_0_payment_msat);
+
+ route_payment(&nodes[1], &[&nodes[0]], SNEAKY_HTLC_MSAT);
+ route_payment(&nodes[1], &[&nodes[0]], SNEAKY_HTLC_MSAT);
+
+ let node_0_details = &nodes[0].node.list_channels()[0];
+ let expected_outbound_capacity_msat = NODE_0_VALUE_TO_SELF_MSAT
+ - node_0_payment_msat
+ - NODE_1_SELECTED_CHANNEL_RESERVE_MSAT
+ - TOTAL_ANCHORS_MSAT;
+ assert_eq!(node_0_details.outbound_capacity_msat, expected_outbound_capacity_msat);
+ assert_eq!(
+ node_0_details.outbound_capacity_msat,
+ commit_tx_fee_sat(FEERATE, 3, &channel_type) * 1000
+ );
+ assert_eq!(node_0_details.next_outbound_htlc_limit_msat, 0);
+
+ let local_balance_before_fee_sat =
+ NODE_0_VALUE_TO_SELF_MSAT / 1000 - node_0_payment_msat / 1000 - TOTAL_ANCHORS_MSAT / 1000;
+ let post_splice_delta_above_reserve = commit_tx_fee_sat(FEERATE, 4, &channel_type);
+ let divident_sat = local_balance_before_fee_sat * 100 + 100
+ - (post_splice_delta_above_reserve * 100)
+ - CHANNEL_VALUE_MSAT / 1000;
+ let expected_splice_out_max = (divident_sat - 1) / 99;
+ assert_eq!(node_0_details.next_splice_out_maximum_sat, expected_splice_out_max);
+
+ let node_1_details = &nodes[1].node.list_channels()[0];
+ let expected_outbound_capacity_msat = NODE_1_VALUE_TO_SELF_MSAT + node_0_payment_msat
+ - 3 * SNEAKY_HTLC_MSAT
+ - NODE_0_SELECTED_CHANNEL_RESERVE_MSAT;
+ assert_eq!(node_1_details.outbound_capacity_msat, expected_outbound_capacity_msat);
+ let (htlc_success_tx_fee_sat, _htlc_timeout_tx_fee_sat) =
+ second_stage_tx_fees_sat(&channel_type, FEERATE);
+ let expected_available_capacity_msat =
+ (NODE_0_DUST_LIMIT_MSAT / 1000 + htlc_success_tx_fee_sat) * 1000 - 1;
+ assert_eq!(node_1_details.next_outbound_htlc_limit_msat, expected_available_capacity_msat);
+ let expected_splice_out_max = NODE_1_VALUE_TO_SELF_MSAT / 1000 + node_0_payment_msat / 1000
+ - 3 * SNEAKY_HTLC_MSAT / 1000
+ - NODE_1_DUST_LIMIT_MSAT / 1000;
+ assert_eq!(node_1_details.next_splice_out_maximum_sat, expected_splice_out_max);
+}
diff --git a/lightning/src/sign/tx_builder.rs b/lightning/src/sign/tx_builder.rs
index 746f6d3..8f699fc 100644
--- a/lightning/src/sign/tx_builder.rs
+++ b/lightning/src/sign/tx_builder.rs
@@ -482,82 +482,87 @@ fn get_next_splice_out_maximum_sat(
next_splice_out_maximum_sat
}
-fn adjust_capacity_for_holder_reserved_fee(mut available_capacity_msat: u64,
- local_nondust_htlc_count: usize, feerate_per_kw: u32, spiked_feerate: u32,
- channel_constraints: &ChannelConstraints, channel_type: &ChannelTypeFeatures,
+fn adjust_capacity_for_holder_reserved_fee(
+ outbound_capacity_msat: u64, local_nondust_htlc_count: usize, remote_nondust_htlc_count: usize,
+ feerate_per_kw: u32, spiked_feerate: u32, channel_constraints: &ChannelConstraints,
+ channel_type: &ChannelTypeFeatures,
) -> u64 {
- let (_real_htlc_success_tx_fee_sat, real_htlc_timeout_tx_fee_sat) =
+ let read_available_capacity = |nondust_htlc_count, htlc_dust_limit_sat| {
+ // Note here we use the htlc count at the current feerate together with the spiked feerate;
+ // this makes sure that the holder can afford any fee bump between 1x to 2x from the current
+ // feerate.
+ let max_commit_tx_fee_sat =
+ commit_tx_fee_sat(spiked_feerate, nondust_htlc_count + 2, channel_type);
+ let min_commit_tx_fee_sat =
+ commit_tx_fee_sat(spiked_feerate, nondust_htlc_count + 1, channel_type);
+
+ // We should mind channel commit tx fee when computing how much of the available capacity
+ // can be used in the next htlc. Mirrors the logic in send_htlc.
+ //
+ // The fee depends on whether the amount we will be sending is above dust or not,
+ // and the answer will in turn change the amount itself — making it a circular
+ // dependency.
+ // This complicates the computation around dust-values, up to the one-htlc-value.
+
+ // We will first subtract the fee as if we were above-dust. Then, if the resulting
+ // value ends up being below dust, we have this fee available again. In that case,
+ // match the value to right-below-dust.
+ let capacity_minus_max_commitment_fee_msat =
+ outbound_capacity_msat.saturating_sub(max_commit_tx_fee_sat * 1000);
+ if capacity_minus_max_commitment_fee_msat < htlc_dust_limit_sat * 1000 {
+ let capacity_minus_min_commitment_fee_msat =
+ outbound_capacity_msat.saturating_sub(min_commit_tx_fee_sat * 1000);
+ cmp::min(htlc_dust_limit_sat * 1000 - 1, capacity_minus_min_commitment_fee_msat)
+ } else {
+ capacity_minus_max_commitment_fee_msat
+ }
+ };
+
+ let (real_htlc_success_tx_fee_sat, real_htlc_timeout_tx_fee_sat) =
second_stage_tx_fees_sat(channel_type, feerate_per_kw);
- let fee_spike_buffer_htlc = 1;
- // Note here we use the htlc count at the current feerate together with the spiked feerate;
- // this makes sure that the holder can afford any fee bump between 1x to 2x from the current
- // feerate.
- let local_max_commit_tx_fee_sat = commit_tx_fee_sat(
- spiked_feerate,
- local_nondust_htlc_count + fee_spike_buffer_htlc + 1,
- channel_type,
+ let available_capacity_on_local_commitment = read_available_capacity(
+ local_nondust_htlc_count,
+ channel_constraints.holder_dust_limit_satoshis + real_htlc_timeout_tx_fee_sat,
);
- let local_min_commit_tx_fee_sat = commit_tx_fee_sat(
- spiked_feerate,
- local_nondust_htlc_count + fee_spike_buffer_htlc,
- channel_type,
+ let available_capacity_on_remote_commitment = read_available_capacity(
+ remote_nondust_htlc_count,
+ channel_constraints.counterparty_dust_limit_satoshis + real_htlc_success_tx_fee_sat,
);
- // We should mind channel commit tx fee when computing how much of the available capacity
- // can be used in the next htlc. Mirrors the logic in send_htlc.
- //
- // The fee depends on whether the amount we will be sending is above dust or not,
- // and the answer will in turn change the amount itself — making it a circular
- // dependency.
- // This complicates the computation around dust-values, up to the one-htlc-value.
-
- let real_dust_limit_timeout_sat =
- real_htlc_timeout_tx_fee_sat + channel_constraints.holder_dust_limit_satoshis;
- let max_reserved_commit_tx_fee_msat = local_max_commit_tx_fee_sat * 1000;
- let min_reserved_commit_tx_fee_msat = local_min_commit_tx_fee_sat * 1000;
-
- // We will first subtract the fee as if we were above-dust. Then, if the resulting
- // value ends up being below dust, we have this fee available again. In that case,
- // match the value to right-below-dust.
- let capacity_minus_max_commitment_fee_msat =
- available_capacity_msat.saturating_sub(max_reserved_commit_tx_fee_msat);
- if capacity_minus_max_commitment_fee_msat < real_dust_limit_timeout_sat * 1000 {
- let capacity_minus_min_commitment_fee_msat =
- available_capacity_msat.saturating_sub(min_reserved_commit_tx_fee_msat);
- available_capacity_msat = cmp::min(
- real_dust_limit_timeout_sat * 1000 - 1,
- capacity_minus_min_commitment_fee_msat,
- );
- } else {
- available_capacity_msat = capacity_minus_max_commitment_fee_msat;
- }
- available_capacity_msat
+ cmp::min(available_capacity_on_local_commitment, available_capacity_on_remote_commitment)
}
-fn adjust_capacity_for_counterparty_reserved_fee(mut available_capacity_msat: u64,
- remote_balance_before_fee_msat: u64, remote_nondust_htlc_count: usize, feerate_per_kw: u32,
- channel_constraints: &ChannelConstraints, channel_type: &ChannelTypeFeatures
+fn adjust_capacity_for_counterparty_reserved_fee(
+ outbound_capacity_msat: u64, remote_balance_before_fee_msat: u64,
+ local_nondust_htlc_count: usize, remote_nondust_htlc_count: usize, feerate_per_kw: u32,
+ channel_constraints: &ChannelConstraints, channel_type: &ChannelTypeFeatures,
) -> u64 {
- let (real_htlc_success_tx_fee_sat, _real_htlc_timeout_tx_fee_sat) =
+ let read_available_capacity = |nondust_htlc_count, htlc_dust_limit_sat| {
+ let commit_tx_fee_sat =
+ commit_tx_fee_sat(feerate_per_kw, nondust_htlc_count + 1, channel_type);
+ // If the channel is inbound (i.e. counterparty pays the fee), we need to make sure
+ // sending a new HTLC won't reduce their balance below our reserve threshold.
+ if remote_balance_before_fee_msat
+ < commit_tx_fee_sat * 1000
+ + channel_constraints.holder_selected_channel_reserve_satoshis * 1000
+ {
+ // If another HTLC's fee would reduce the remote's balance below the reserve limit
+ // we've selected for them, we can only send dust HTLCs.
+ cmp::min(outbound_capacity_msat, htlc_dust_limit_sat * 1000 - 1)
+ } else {
+ outbound_capacity_msat
+ }
+ };
+ let (real_htlc_success_tx_fee_sat, real_htlc_timeout_tx_fee_sat) =
second_stage_tx_fees_sat(channel_type, feerate_per_kw);
- let remote_commit_tx_fee_sat =
- commit_tx_fee_sat(feerate_per_kw, remote_nondust_htlc_count + 1, channel_type);
- // If the channel is inbound (i.e. counterparty pays the fee), we need to make sure
- // sending a new HTLC won't reduce their balance below our reserve threshold.
- let real_dust_limit_success_sat =
- real_htlc_success_tx_fee_sat + channel_constraints.counterparty_dust_limit_satoshis;
- let max_reserved_commit_tx_fee_msat = remote_commit_tx_fee_sat * 1000;
-
- let holder_selected_chan_reserve_msat =
- channel_constraints.holder_selected_channel_reserve_satoshis * 1000;
- if remote_balance_before_fee_msat
- < max_reserved_commit_tx_fee_msat + holder_selected_chan_reserve_msat
- {
- // If another HTLC's fee would reduce the remote's balance below the reserve limit
- // we've selected for them, we can only send dust HTLCs.
- available_capacity_msat =
- cmp::min(available_capacity_msat, real_dust_limit_success_sat * 1000 - 1);
- }
- available_capacity_msat
+ let available_capacity_on_local_commitment = read_available_capacity(
+ local_nondust_htlc_count,
+ channel_constraints.holder_dust_limit_satoshis + real_htlc_timeout_tx_fee_sat,
+ );
+ let available_capacity_on_remote_commitment = read_available_capacity(
+ remote_nondust_htlc_count,
+ channel_constraints.counterparty_dust_limit_satoshis + real_htlc_success_tx_fee_sat,
+ );
+ cmp::min(available_capacity_on_local_commitment, available_capacity_on_remote_commitment)
}
fn adjust_min_max_htlc_for_dust_exposure(
@@ -726,23 +731,27 @@ fn get_available_balances(
let outbound_capacity_msat = local_balance_before_fee_msat
.saturating_sub(channel_constraints.counterparty_selected_channel_reserve_satoshis * 1000);
- let mut available_capacity_msat = outbound_capacity_msat;
-
- if is_outbound_from_holder {
- available_capacity_msat = adjust_capacity_for_holder_reserved_fee(
- available_capacity_msat, local_nondust_htlc_count, feerate_per_kw,
- spiked_feerate, &channel_constraints, channel_type
- );
+ let available_capacity_msat = if is_outbound_from_holder {
+ adjust_capacity_for_holder_reserved_fee(
+ outbound_capacity_msat,
+ local_nondust_htlc_count,
+ remote_nondust_htlc_count,
+ feerate_per_kw,
+ spiked_feerate,
+ &channel_constraints,
+ channel_type,
+ )
} else {
- available_capacity_msat = adjust_capacity_for_counterparty_reserved_fee(
- available_capacity_msat,
+ adjust_capacity_for_counterparty_reserved_fee(
+ outbound_capacity_msat,
remote_balance_before_fee_msat,
+ local_nondust_htlc_count,
remote_nondust_htlc_count,
feerate_per_kw,
&channel_constraints,
- channel_type
+ channel_type,
)
- }
+ };
let (next_outbound_htlc_minimum_msat, mut available_capacity_msat, dust_exposure_msat) =
adjust_min_max_htlc_for_dust_exposure(
Why this scored 64/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.