@@ -704,6 +704,10 @@ pub const MAX_CHAN_DUST_LIMIT_SATOSHIS: u64 = MAX_STD_OUTPUT_DUST_LIMIT_SATOSHIS
704704pub const MIN_CHAN_DUST_LIMIT_SATOSHIS: u64 = 354;
705705
706706// Just a reasonable implementation-specific safe lower bound, higher than the dust limit.
707+ // Deprecated: This constant is kept for backward compatibility.
708+ // The minimum channel reserve is now configurable via `ChannelHandshakeConfig::min_their_channel_reserve_satoshis`.
709+ // This constant retains its original value for API compatibility, but the actual behavior uses the config value.
710+ #[allow(dead_code)]
707711pub const MIN_THEIR_CHAN_RESERVE_SATOSHIS: u64 = 1000;
708712
709713/// Used to return a simple Error back to ChannelManager. Will get converted to a
@@ -1931,9 +1935,10 @@ impl<SP: Deref> ChannelContext<SP> where SP::Target: SignerProvider {
19311935 }
19321936 }
19331937
1934- if holder_selected_channel_reserve_satoshis < MIN_CHAN_DUST_LIMIT_SATOSHIS {
1935- // Protocol level safety check in place, although it should never happen because
1936- // of `MIN_THEIR_CHAN_RESERVE_SATOSHIS`
1938+ // Allow bypassing dust limit when min_their_channel_reserve_satoshis is explicitly set to 0 (LSP use case)
1939+ if holder_selected_channel_reserve_satoshis < MIN_CHAN_DUST_LIMIT_SATOSHIS
1940+ && config.channel_handshake_config.min_their_channel_reserve_satoshis > 0 {
1941+ // Protocol level safety check in place
19371942 return Err(ChannelError::close(format!("Suitable channel reserve not found. remote_channel_reserve was ({}). dust_limit_satoshis is ({}).", holder_selected_channel_reserve_satoshis, MIN_CHAN_DUST_LIMIT_SATOSHIS)));
19381943 }
19391944 if holder_selected_channel_reserve_satoshis * 1000 >= full_channel_value_msat {
@@ -1943,7 +1948,9 @@ impl<SP: Deref> ChannelContext<SP> where SP::Target: SignerProvider {
19431948 log_debug!(logger, "channel_reserve_satoshis ({}) is smaller than our dust limit ({}). We can broadcast stale states without any risk, implying this channel is very insecure for our counterparty.",
19441949 msg_channel_reserve_satoshis, MIN_CHAN_DUST_LIMIT_SATOSHIS);
19451950 }
1946- if holder_selected_channel_reserve_satoshis < open_channel_fields.dust_limit_satoshis {
1951+ // Allow bypassing dust limit when min_their_channel_reserve_satoshis is explicitly set to 0 (LSP use case)
1952+ if holder_selected_channel_reserve_satoshis < open_channel_fields.dust_limit_satoshis
1953+ && config.channel_handshake_config.min_their_channel_reserve_satoshis > 0 {
19471954 return Err(ChannelError::close(format!("Dust limit ({}) too high for the channel reserve we require the remote to keep ({})", open_channel_fields.dust_limit_satoshis, holder_selected_channel_reserve_satoshis)));
19481955 }
19491956
@@ -2579,7 +2586,9 @@ impl<SP: Deref> ChannelContext<SP> where SP::Target: SignerProvider {
25792586 if channel_reserve_satoshis > self.channel_value_satoshis {
25802587 return Err(ChannelError::close(format!("Bogus channel_reserve_satoshis ({}). Must not be greater than ({})", channel_reserve_satoshis, self.channel_value_satoshis)));
25812588 }
2582- if common_fields.dust_limit_satoshis > self.holder_selected_channel_reserve_satoshis {
2589+ // Allow bypassing dust limit when holder_selected_channel_reserve_satoshis is 0 (LSP use case)
2590+ if common_fields.dust_limit_satoshis > self.holder_selected_channel_reserve_satoshis
2591+ && self.holder_selected_channel_reserve_satoshis > 0 {
25832592 return Err(ChannelError::close(format!("Dust limit ({}) is bigger than our channel reserve ({})", common_fields.dust_limit_satoshis, self.holder_selected_channel_reserve_satoshis)));
25842593 }
25852594 if channel_reserve_satoshis > self.channel_value_satoshis - self.holder_selected_channel_reserve_satoshis {
@@ -4067,10 +4076,20 @@ fn get_holder_max_htlc_value_in_flight_msat(channel_value_satoshis: u64, config:
40674076/// Guaranteed to return a value no larger than channel_value_satoshis
40684077///
40694078/// This is used both for outbound and inbound channels and has lower bound
4070- /// of `MIN_THEIR_CHAN_RESERVE_SATOSHIS`.
4071- pub(crate) fn get_holder_selected_channel_reserve_satoshis(channel_value_satoshis: u64, config: &UserConfig) -> u64 {
4072- let calculated_reserve = channel_value_satoshis.saturating_mul(config.channel_handshake_config.their_channel_reserve_proportional_millionths as u64) / 1_000_000;
4073- cmp::min(channel_value_satoshis, cmp::max(calculated_reserve, MIN_THEIR_CHAN_RESERVE_SATOSHIS))
4079+ /// of `ChannelHandshakeConfig::min_their_channel_reserve_satoshis`.
4080+ pub(crate) fn get_holder_selected_channel_reserve_satoshis(
4081+ channel_value_satoshis: u64, config: &UserConfig,
4082+ ) -> u64 {
4083+ let counterparty_chan_reserve_prop_mil =
4084+ config.channel_handshake_config.their_channel_reserve_proportional_millionths as u64;
4085+ let min_their_channel_reserve_satoshis =
4086+ config.channel_handshake_config.min_their_channel_reserve_satoshis;
4087+ let calculated_reserve =
4088+ channel_value_satoshis.saturating_mul(counterparty_chan_reserve_prop_mil) / 1_000_000;
4089+ cmp::min(
4090+ channel_value_satoshis,
4091+ cmp::max(calculated_reserve, min_their_channel_reserve_satoshis),
4092+ )
40744093}
40754094
40764095/// This is for legacy reasons, present for forward-compatibility.
@@ -8207,9 +8226,10 @@ impl<SP: Deref> OutboundV1Channel<SP> where SP::Target: SignerProvider {
82078226 L::Target: Logger,
82088227 {
82098228 let holder_selected_channel_reserve_satoshis = get_holder_selected_channel_reserve_satoshis(channel_value_satoshis, config);
8210- if holder_selected_channel_reserve_satoshis < MIN_CHAN_DUST_LIMIT_SATOSHIS {
8211- // Protocol level safety check in place, although it should never happen because
8212- // of `MIN_THEIR_CHAN_RESERVE_SATOSHIS`
8229+ // Allow bypassing dust limit when min_their_channel_reserve_satoshis is explicitly set to 0 (LSP use case)
8230+ if holder_selected_channel_reserve_satoshis < MIN_CHAN_DUST_LIMIT_SATOSHIS
8231+ && config.channel_handshake_config.min_their_channel_reserve_satoshis > 0 {
8232+ // Protocol level safety check in place
82138233 return Err(APIError::APIMisuseError { err: format!("Holder selected channel reserve below \
82148234 implemention limit dust_limit_satoshis {}", holder_selected_channel_reserve_satoshis) });
82158235 }
@@ -10150,7 +10170,7 @@ mod tests {
1015010170 use crate::ln::channelmanager::{self, HTLCSource, PaymentId};
1015110171 use crate::ln::channel::InitFeatures;
1015210172 use crate::ln::channel::{AwaitingChannelReadyFlags, Channel, ChannelState, InboundHTLCOutput, OutboundV1Channel, InboundV1Channel, OutboundHTLCOutput, InboundHTLCState, OutboundHTLCState, HTLCCandidate, HTLCInitiator, HTLCUpdateAwaitingACK, commit_tx_fee_sat};
10153- use crate::ln::channel::{MAX_FUNDING_SATOSHIS_NO_WUMBO, TOTAL_BITCOIN_SUPPLY_SATOSHIS, MIN_THEIR_CHAN_RESERVE_SATOSHIS };
10173+ use crate::ln::channel::{MAX_FUNDING_SATOSHIS_NO_WUMBO, TOTAL_BITCOIN_SUPPLY_SATOSHIS};
1015410174 use crate::types::features::{ChannelFeatures, ChannelTypeFeatures, NodeFeatures};
1015510175 use crate::ln::msgs;
1015610176 use crate::ln::msgs::{ChannelUpdate, DecodeError, UnsignedChannelUpdate, MAX_VALUE_MSAT};
@@ -10576,7 +10596,7 @@ mod tests {
1057610596 test_self_and_counterparty_channel_reserve(10_000_000, 0.60, 0.30);
1057710597
1057810598 // Test with calculated channel reserve less than lower bound
10579- // i.e `MIN_THEIR_CHAN_RESERVE_SATOSHIS `
10599+ // i.e `ChannelHandshakeConfig::min_their_channel_reserve_satoshis `
1058010600 test_self_and_counterparty_channel_reserve(100_000, 0.00002, 0.30);
1058110601
1058210602 // Test with invalid channel reserves since sum of both is greater than or equal
@@ -10600,7 +10620,7 @@ mod tests {
1060010620 outbound_node_config.channel_handshake_config.their_channel_reserve_proportional_millionths = (outbound_selected_channel_reserve_perc * 1_000_000.0) as u32;
1060110621 let mut chan = OutboundV1Channel::<&TestKeysInterface>::new(&&fee_est, &&keys_provider, &&keys_provider, outbound_node_id, &channelmanager::provided_init_features(&outbound_node_config), channel_value_satoshis, 100_000, 42, &outbound_node_config, 0, 42, None, &logger).unwrap();
1060210622
10603- let expected_outbound_selected_chan_reserve = cmp::max(MIN_THEIR_CHAN_RESERVE_SATOSHIS , (chan.context.channel_value_satoshis as f64 * outbound_selected_channel_reserve_perc) as u64);
10623+ let expected_outbound_selected_chan_reserve = cmp::max(outbound_node_config.channel_handshake_config.min_their_channel_reserve_satoshis , (chan.context.channel_value_satoshis as f64 * outbound_selected_channel_reserve_perc) as u64);
1060410624 assert_eq!(chan.context.holder_selected_channel_reserve_satoshis, expected_outbound_selected_chan_reserve);
1060510625
1060610626 let chan_open_channel_msg = chan.get_open_channel(ChainHash::using_genesis_block(network), &&logger).unwrap();
@@ -10610,7 +10630,7 @@ mod tests {
1061010630 if outbound_selected_channel_reserve_perc + inbound_selected_channel_reserve_perc < 1.0 {
1061110631 let chan_inbound_node = InboundV1Channel::<&TestKeysInterface>::new(&&fee_est, &&keys_provider, &&keys_provider, inbound_node_id, &channelmanager::provided_channel_type_features(&inbound_node_config), &channelmanager::provided_init_features(&outbound_node_config), &chan_open_channel_msg, 7, &inbound_node_config, 0, &&logger, /*is_0conf=*/false).unwrap();
1061210632
10613- let expected_inbound_selected_chan_reserve = cmp::max(MIN_THEIR_CHAN_RESERVE_SATOSHIS , (chan.context.channel_value_satoshis as f64 * inbound_selected_channel_reserve_perc) as u64);
10633+ let expected_inbound_selected_chan_reserve = cmp::max(inbound_node_config.channel_handshake_config.min_their_channel_reserve_satoshis , (chan.context.channel_value_satoshis as f64 * inbound_selected_channel_reserve_perc) as u64);
1061410634
1061510635 assert_eq!(chan_inbound_node.context.holder_selected_channel_reserve_satoshis, expected_inbound_selected_chan_reserve);
1061610636 assert_eq!(chan_inbound_node.context.counterparty_selected_channel_reserve_satoshis.unwrap(), expected_outbound_selected_chan_reserve);
@@ -10622,6 +10642,62 @@ mod tests {
1062210642 }
1062310643
1062410644 #[test]
10645+ #[rustfmt::skip]
10646+ fn test_configurable_min_channel_reserve() {
10647+ let fee_est = LowerBoundedFeeEstimator::new(&TestFeeEstimator { fee_est: 15_000 });
10648+ let logger = test_utils::TestLogger::new();
10649+ let secp_ctx = Secp256k1::new();
10650+ let keys_provider = test_utils::TestKeysInterface::new(&[42; 32], Network::Testnet);
10651+ let outbound_node_id = PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap());
10652+
10653+ // Test with min_their_channel_reserve_satoshis set to 0 (LSP use case)
10654+ let mut config = UserConfig::default();
10655+ config.channel_handshake_config.min_their_channel_reserve_satoshis = 0;
10656+ config.channel_handshake_config.their_channel_reserve_proportional_millionths = 0;
10657+
10658+ let chan = OutboundV1Channel::<&TestKeysInterface>::new(
10659+ &fee_est, &&keys_provider, &&keys_provider, outbound_node_id,
10660+ &channelmanager::provided_init_features(&config),
10661+ 1_000_000, 100_000, 42, &config, 0, 42, None, &logger
10662+ ).unwrap();
10663+
10664+ // With 0 minimum and 0 proportional, reserve should be 0 (bypasses dust limit)
10665+ assert_eq!(chan.context.holder_selected_channel_reserve_satoshis, 0);
10666+
10667+ // Test with custom minimum enforced when proportional is lower
10668+ config.channel_handshake_config.min_their_channel_reserve_satoshis = 10_000;
10669+ config.channel_handshake_config.their_channel_reserve_proportional_millionths = 10_000; // 1%
10670+
10671+ let chan_small = OutboundV1Channel::<&TestKeysInterface>::new(
10672+ &fee_est, &&keys_provider, &&keys_provider, outbound_node_id,
10673+ &channelmanager::provided_init_features(&config),
10674+ 100_000, 100_000, 42, &config, 0, 42, None, &logger
10675+ ).unwrap();
10676+
10677+ // Proportional would be 1% of 100k = 1000, but minimum is 10000, so 10000 should be used
10678+ assert_eq!(chan_small.context.holder_selected_channel_reserve_satoshis, 10_000);
10679+
10680+ // Test that dust limit is still enforced when min_their_channel_reserve_satoshis is non-zero but below dust limit
10681+ config.channel_handshake_config.min_their_channel_reserve_satoshis = 100; // Below dust limit of 354
10682+ config.channel_handshake_config.their_channel_reserve_proportional_millionths = 0;
10683+
10684+ let result = OutboundV1Channel::<&TestKeysInterface>::new(
10685+ &fee_est, &&keys_provider, &&keys_provider, outbound_node_id,
10686+ &channelmanager::provided_init_features(&config),
10687+ 1_000_000, 100_000, 42, &config, 0, 42, None, &logger
10688+ );
10689+
10690+ // Should fail because 100 < 354 (dust limit) and min_their_channel_reserve_satoshis > 0
10691+ assert!(result.is_err());
10692+ if let Err(APIError::APIMisuseError { err }) = result {
10693+ assert!(err.contains("dust_limit_satoshis"));
10694+ } else {
10695+ panic!("Expected APIMisuseError");
10696+ }
10697+ }
10698+
10699+ #[test]
10700+ #[rustfmt::skip]
1062510701 fn channel_update() {
1062610702 let feeest = LowerBoundedFeeEstimator::new(&TestFeeEstimator{fee_est: 15000});
1062710703 let logger = test_utils::TestLogger::new();
0 commit comments