Clamp our selected reserve to the counterparty's dust limit
What changed, and why it matters
This commit changes how Lightning channels set a minimum 'reserve' amount that each side must keep in the channel. The reserve is now forced to be at least as large as the other side's 'dust limit' (the smallest transaction output considered worth tracking). Without this, a peer could set a very high dust limit while the reserve stayed low, potentially allowing tiny outputs to be treated as non-existent or causing the channel's accounting rules to disagree between the two sides. The change is defensive and prepares for a later feature allowing larger dust limits.
Review the subsequent commit that raises allowed counterparty dust limits to ensure the outbound-channel failure path is exercised and that no edge case allows a dust limit above the selected reserve to be accepted. Consider adding or verifying tests where `their_dust_limit_satoshis` exceeds `MIN_THEIR_CHAN_RESERVE_SATOSHIS`.
Security signals we found
Reserve/dust-limit relationship changed to prevent reserve from falling below counterparty dust limit
Comment explicitly notes protocol-level safety check and failure path for outbound channels with unknown dust limit
Multiple test files updated to pass new parameter, indicating behavioral dependency
Commit message describes the change as required for a subsequent protocol relaxation
Evidence from the diff
The patch modifies get_holder_selected_channel_reserve_satoshis in lightning/src/ln/channel.rs to take a new their_dust_limit_satoshis argument and clamp the selected reserve to max(calculated_reserve, MIN_THEIR_CHAN_RESERVE_SATOSHIS, their_dust_limit_satoshis). For outbound channels the counterparty dust limit is not yet known, so 0 is passed and the channel is failed later if the negotiated dust limit exceeds the selected reserve. For inbound channels the dust limit from the OpenChannel message is used directly. Test files are updated to pass the new parameter. The commit message frames this as groundwork for a later change allowing counterparty dust limits above MIN_THEIR_CHANNEL_RESERVE_SATOSHIS.
Changed components
lightning/src/ln/channel.rslightning/src/ln/channel_open_tests.rslightning/src/ln/functional_tests.rslightning/src/ln/htlc_reserve_unit_tests.rslightning/src/ln/payment_tests.rslightning/src/ln/update_fee_tests.rsInspect captured patch +30 / −14
diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs
index 9361cd3..704039b 100644
--- a/lightning/src/ln/channel.rs
+++ b/lightning/src/ln/channel.rs
@@ -6280,15 +6280,18 @@ fn get_holder_max_htlc_value_in_flight_msat(
/// Guaranteed to return a value no larger than channel_value_satoshis
///
/// This is used both for outbound and inbound channels and has lower bound
-/// of `MIN_THEIR_CHAN_RESERVE_SATOSHIS`.
+/// of `MIN_THEIR_CHAN_RESERVE_SATOSHIS`, and the `dust_limit_satoshis` of
+/// the counterparty.
pub(crate) fn get_holder_selected_channel_reserve_satoshis(
- channel_value_satoshis: u64, config: &UserConfig,
+ channel_value_satoshis: u64, their_dust_limit_satoshis: u64, config: &UserConfig,
) -> u64 {
let counterparty_chan_reserve_prop_mil =
config.channel_handshake_config.their_channel_reserve_proportional_millionths as u64;
let calculated_reserve =
channel_value_satoshis.saturating_mul(counterparty_chan_reserve_prop_mil) / 1_000_000;
- cmp::min(channel_value_satoshis, cmp::max(calculated_reserve, MIN_THEIR_CHAN_RESERVE_SATOSHIS))
+ let channel_reserve_satoshis = cmp::max(calculated_reserve, MIN_THEIR_CHAN_RESERVE_SATOSHIS);
+ let channel_reserve_satoshis = cmp::max(channel_reserve_satoshis, their_dust_limit_satoshis);
+ cmp::min(channel_value_satoshis, channel_reserve_satoshis)
}
/// This is for legacy reasons, present for forward-compatibility.
@@ -13267,7 +13270,15 @@ impl<SP: SignerProvider> OutboundV1Channel<SP> {
channel_value_satoshis: u64, push_msat: u64, user_id: u128, config: &UserConfig, current_chain_height: u32,
outbound_scid_alias: u64, temporary_channel_id: Option<ChannelId>, logger: L
) -> Result<OutboundV1Channel<SP>, APIError> {
- let holder_selected_channel_reserve_satoshis = get_holder_selected_channel_reserve_satoshis(channel_value_satoshis, config);
+ // At this point, we do not know what `dust_limit_satoshis` the counterparty will want for themselves,
+ // so we set the channel reserve with no regard for their dust limit, and fail the channel if they want
+ // a dust limit higher than our selected reserve.
+ let their_dust_limit_satoshis = 0;
+ let holder_selected_channel_reserve_satoshis = get_holder_selected_channel_reserve_satoshis(
+ channel_value_satoshis,
+ their_dust_limit_satoshis,
+ config
+ );
if holder_selected_channel_reserve_satoshis < MIN_CHAN_DUST_LIMIT_SATOSHIS {
// Protocol level safety check in place, although it should never happen because
// of `MIN_THEIR_CHAN_RESERVE_SATOSHIS`
@@ -13649,7 +13660,11 @@ impl<SP: SignerProvider> InboundV1Channel<SP> {
// support this channel type.
let channel_type = channel_type_from_open_channel(&msg.common_fields, our_supported_features)?;
- let holder_selected_channel_reserve_satoshis = get_holder_selected_channel_reserve_satoshis(msg.common_fields.funding_satoshis, config);
+ let holder_selected_channel_reserve_satoshis = get_holder_selected_channel_reserve_satoshis(
+ msg.common_fields.funding_satoshis,
+ msg.common_fields.dust_limit_satoshis,
+ config
+ );
let counterparty_pubkeys = ChannelPublicKeys {
funding_pubkey: msg.common_fields.funding_pubkey,
revocation_basepoint: RevocationBasepoint::from(msg.common_fields.revocation_basepoint),
diff --git a/lightning/src/ln/channel_open_tests.rs b/lightning/src/ln/channel_open_tests.rs
index 08cabc0..9a65e34 100644
--- a/lightning/src/ln/channel_open_tests.rs
+++ b/lightning/src/ln/channel_open_tests.rs
@@ -470,7 +470,7 @@ pub fn test_insane_channel_opens() {
// funding satoshis
let channel_value_sat = 31337; // same as funding satoshis
let channel_reserve_satoshis =
- get_holder_selected_channel_reserve_satoshis(channel_value_sat, &legacy_cfg);
+ get_holder_selected_channel_reserve_satoshis(channel_value_sat, 0, &legacy_cfg);
let push_msat = (channel_value_sat - channel_reserve_satoshis) * 1000;
// Have node0 initiate a channel to node1 with aforementioned parameters
diff --git a/lightning/src/ln/functional_tests.rs b/lightning/src/ln/functional_tests.rs
index 17fbc1f..c98cfa5 100644
--- a/lightning/src/ln/functional_tests.rs
+++ b/lightning/src/ln/functional_tests.rs
@@ -413,7 +413,7 @@ pub fn test_inbound_outbound_capacity_is_not_zero() {
assert_eq!(channels0.len(), 1);
assert_eq!(channels1.len(), 1);
- let reserve = get_holder_selected_channel_reserve_satoshis(100_000, &default_config);
+ let reserve = get_holder_selected_channel_reserve_satoshis(100_000, 0, &default_config);
assert_eq!(channels0[0].inbound_capacity_msat, 95000000 - reserve * 1000);
assert_eq!(channels1[0].outbound_capacity_msat, 95000000 - reserve * 1000);
diff --git a/lightning/src/ln/htlc_reserve_unit_tests.rs b/lightning/src/ln/htlc_reserve_unit_tests.rs
index d88b9a2..2dfd191 100644
--- a/lightning/src/ln/htlc_reserve_unit_tests.rs
+++ b/lightning/src/ln/htlc_reserve_unit_tests.rs
@@ -50,7 +50,7 @@ fn do_test_counterparty_no_reserve(send_from_initiator: bool) {
push_amt -= feerate_per_kw as u64
* (commitment_tx_base_weight(&channel_type_features) + 4 * COMMITMENT_TX_WEIGHT_PER_HTLC)
/ 1000 * 1000;
- push_amt -= get_holder_selected_channel_reserve_satoshis(100_000, &default_config) * 1000;
+ push_amt -= get_holder_selected_channel_reserve_satoshis(100_000, 0, &default_config) * 1000;
let push = if send_from_initiator { 0 } else { push_amt };
let temp_channel_id =
@@ -1008,7 +1008,7 @@ pub fn test_chan_reserve_violation_outbound_htlc_inbound_chan() {
&channel_type_features,
);
- push_amt -= get_holder_selected_channel_reserve_satoshis(100_000, &default_config) * 1000;
+ push_amt -= get_holder_selected_channel_reserve_satoshis(100_000, 0, &default_config) * 1000;
let _ = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, push_amt);
@@ -1052,7 +1052,7 @@ pub fn test_chan_reserve_violation_inbound_htlc_outbound_channel() {
MIN_AFFORDABLE_HTLC_COUNT as u64,
&channel_type_features,
);
- push_amt -= get_holder_selected_channel_reserve_satoshis(100_000, &default_config) * 1000;
+ push_amt -= get_holder_selected_channel_reserve_satoshis(100_000, 0, &default_config) * 1000;
let chan = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, push_amt);
// Send four HTLCs to cover the initial push_msat buffer we're required to include
@@ -1130,7 +1130,7 @@ pub fn test_chan_reserve_dust_inbound_htlcs_outbound_chan() {
MIN_AFFORDABLE_HTLC_COUNT as u64,
&channel_type_features,
);
- push_amt -= get_holder_selected_channel_reserve_satoshis(100_000, &default_config) * 1000;
+ push_amt -= get_holder_selected_channel_reserve_satoshis(100_000, 0, &default_config) * 1000;
create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100000, push_amt);
let (htlc_success_tx_fee_sat, _) =
diff --git a/lightning/src/ln/payment_tests.rs b/lightning/src/ln/payment_tests.rs
index b5cbe0f..7d198d2 100644
--- a/lightning/src/ln/payment_tests.rs
+++ b/lightning/src/ln/payment_tests.rs
@@ -4985,7 +4985,7 @@ fn test_htlc_forward_considers_anchor_outputs_value() {
create_announced_chan_between_nodes_with_value(&nodes, 1, 2, CHAN_AMT, PUSH_MSAT);
let channel_reserve_msat =
- get_holder_selected_channel_reserve_satoshis(CHAN_AMT, &config) * 1000;
+ get_holder_selected_channel_reserve_satoshis(CHAN_AMT, 0, &config) * 1000;
let commitment_fee_msat = chan_utils::commit_tx_fee_sat(
*nodes[1].fee_estimator.sat_per_kw.lock().unwrap(),
2,
diff --git a/lightning/src/ln/update_fee_tests.rs b/lightning/src/ln/update_fee_tests.rs
index ac56639..5a50120 100644
--- a/lightning/src/ln/update_fee_tests.rs
+++ b/lightning/src/ln/update_fee_tests.rs
@@ -408,7 +408,8 @@ pub fn do_test_update_fee_that_funder_cannot_afford(channel_type_features: Chann
);
let channel_id = chan.2;
let secp_ctx = Secp256k1::new();
- let bs_channel_reserve_sats = get_holder_selected_channel_reserve_satoshis(channel_value, &cfg);
+ let bs_channel_reserve_sats =
+ get_holder_selected_channel_reserve_satoshis(channel_value, 0, &cfg);
let (anchor_outputs_value_sats, outputs_num_no_htlcs) =
if channel_type_features.supports_anchors_zero_fee_htlc_tx() {
(ANCHOR_OUTPUT_VALUE_SATOSHI * 2, 4)
@@ -892,7 +893,7 @@ pub fn test_chan_init_feerate_unaffordability() {
// During open, we don't have a "counterparty channel reserve" to check against, so that
// requirement only comes into play on the open_channel handling side.
- push_amt -= get_holder_selected_channel_reserve_satoshis(100_000, &default_config) * 1000;
+ push_amt -= get_holder_selected_channel_reserve_satoshis(100_000, 0, &default_config) * 1000;
nodes[0].node.create_channel(node_b_id, 100_000, push_amt, 42, None, None).unwrap();
let mut open_channel_msg =
get_event_msg!(nodes[0], MessageSendEvent::SendOpenChannel, node_b_id);
Why this scored 58/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.