@@ -928,6 +928,10 @@ pub const MAX_CHAN_DUST_LIMIT_SATOSHIS: u64 = MAX_STD_OUTPUT_DUST_LIMIT_SATOSHIS
928928pub const MIN_CHAN_DUST_LIMIT_SATOSHIS: u64 = 354;
929929
930930// Just a reasonable implementation-specific safe lower bound, higher than the dust limit.
931+ // Deprecated: This constant is kept for backward compatibility.
932+ // The minimum channel reserve is now configurable via `ChannelHandshakeConfig::min_their_channel_reserve_satoshis`.
933+ // This constant retains its original value for API compatibility, but the actual behavior uses the config value.
934+ #[allow(dead_code)]
931935pub const MIN_THEIR_CHAN_RESERVE_SATOSHIS: u64 = 1000;
932936
933937/// Used to return a simple Error back to ChannelManager. Will get converted to a
@@ -3476,9 +3480,10 @@ where
34763480 }
34773481 }
34783482
3479- if holder_selected_channel_reserve_satoshis < MIN_CHAN_DUST_LIMIT_SATOSHIS {
3480- // Protocol level safety check in place, although it should never happen because
3481- // of `MIN_THEIR_CHAN_RESERVE_SATOSHIS`
3483+ // Allow bypassing dust limit when min_their_channel_reserve_satoshis is explicitly set to 0 (LSP use case)
3484+ if holder_selected_channel_reserve_satoshis < MIN_CHAN_DUST_LIMIT_SATOSHIS
3485+ && config.channel_handshake_config.min_their_channel_reserve_satoshis > 0 {
3486+ // Protocol level safety check in place
34823487 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)));
34833488 }
34843489 if holder_selected_channel_reserve_satoshis * 1000 >= full_channel_value_msat {
@@ -3488,7 +3493,9 @@ where
34883493 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.",
34893494 msg_channel_reserve_satoshis, MIN_CHAN_DUST_LIMIT_SATOSHIS);
34903495 }
3491- if holder_selected_channel_reserve_satoshis < open_channel_fields.dust_limit_satoshis {
3496+ // Allow bypassing dust limit when min_their_channel_reserve_satoshis is explicitly set to 0 (LSP use case)
3497+ if holder_selected_channel_reserve_satoshis < open_channel_fields.dust_limit_satoshis
3498+ && config.channel_handshake_config.min_their_channel_reserve_satoshis > 0 {
34923499 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)));
34933500 }
34943501
@@ -4203,7 +4210,9 @@ where
42034210 if channel_reserve_satoshis > funding.get_value_satoshis() {
42044211 return Err(ChannelError::close(format!("Bogus channel_reserve_satoshis ({}). Must not be greater than ({})", channel_reserve_satoshis, funding.get_value_satoshis())));
42054212 }
4206- if common_fields.dust_limit_satoshis > funding.holder_selected_channel_reserve_satoshis {
4213+ // Allow bypassing dust limit when holder_selected_channel_reserve_satoshis is 0 (LSP use case)
4214+ if common_fields.dust_limit_satoshis > funding.holder_selected_channel_reserve_satoshis
4215+ && funding.holder_selected_channel_reserve_satoshis > 0 {
42074216 return Err(ChannelError::close(format!("Dust limit ({}) is bigger than our channel reserve ({})", common_fields.dust_limit_satoshis, funding.holder_selected_channel_reserve_satoshis)));
42084217 }
42094218 if channel_reserve_satoshis > funding.get_value_satoshis() - funding.holder_selected_channel_reserve_satoshis {
@@ -6459,15 +6468,20 @@ fn get_holder_max_htlc_value_in_flight_msat(
64596468/// Guaranteed to return a value no larger than channel_value_satoshis
64606469///
64616470/// This is used both for outbound and inbound channels and has lower bound
6462- /// of `MIN_THEIR_CHAN_RESERVE_SATOSHIS `.
6471+ /// of `ChannelHandshakeConfig::min_their_channel_reserve_satoshis `.
64636472pub(crate) fn get_holder_selected_channel_reserve_satoshis(
64646473 channel_value_satoshis: u64, config: &UserConfig,
64656474) -> u64 {
64666475 let counterparty_chan_reserve_prop_mil =
64676476 config.channel_handshake_config.their_channel_reserve_proportional_millionths as u64;
6477+ let min_their_channel_reserve_satoshis =
6478+ config.channel_handshake_config.min_their_channel_reserve_satoshis;
64686479 let calculated_reserve =
64696480 channel_value_satoshis.saturating_mul(counterparty_chan_reserve_prop_mil) / 1_000_000;
6470- cmp::min(channel_value_satoshis, cmp::max(calculated_reserve, MIN_THEIR_CHAN_RESERVE_SATOSHIS))
6481+ cmp::min(
6482+ channel_value_satoshis,
6483+ cmp::max(calculated_reserve, min_their_channel_reserve_satoshis),
6484+ )
64716485}
64726486
64736487/// This is for legacy reasons, present for forward-compatibility.
@@ -13453,9 +13467,10 @@ where
1345313467 L::Target: Logger,
1345413468 {
1345513469 let holder_selected_channel_reserve_satoshis = get_holder_selected_channel_reserve_satoshis(channel_value_satoshis, config);
13456- if holder_selected_channel_reserve_satoshis < MIN_CHAN_DUST_LIMIT_SATOSHIS {
13457- // Protocol level safety check in place, although it should never happen because
13458- // of `MIN_THEIR_CHAN_RESERVE_SATOSHIS`
13470+ // Allow bypassing dust limit when min_their_channel_reserve_satoshis is explicitly set to 0 (LSP use case)
13471+ if holder_selected_channel_reserve_satoshis < MIN_CHAN_DUST_LIMIT_SATOSHIS
13472+ && config.channel_handshake_config.min_their_channel_reserve_satoshis > 0 {
13473+ // Protocol level safety check in place
1345913474 return Err(APIError::APIMisuseError { err: format!("Holder selected channel reserve below \
1346013475 implemention limit dust_limit_satoshis {}", holder_selected_channel_reserve_satoshis) });
1346113476 }
@@ -15833,10 +15848,7 @@ mod tests {
1583315848 HTLCUpdateAwaitingACK, InboundHTLCOutput, InboundHTLCState, InboundV1Channel,
1583415849 OutboundHTLCOutput, OutboundHTLCState, OutboundV1Channel,
1583515850 };
15836- use crate::ln::channel::{
15837- MAX_FUNDING_SATOSHIS_NO_WUMBO, MIN_THEIR_CHAN_RESERVE_SATOSHIS,
15838- TOTAL_BITCOIN_SUPPLY_SATOSHIS,
15839- };
15851+ use crate::ln::channel::{MAX_FUNDING_SATOSHIS_NO_WUMBO, TOTAL_BITCOIN_SUPPLY_SATOSHIS};
1584015852 use crate::ln::channel_keys::{RevocationBasepoint, RevocationKey};
1584115853 use crate::ln::channelmanager::{self, HTLCSource, PaymentId};
1584215854 use crate::ln::funding::FundingTxInput;
@@ -16315,7 +16327,7 @@ mod tests {
1631516327 test_self_and_counterparty_channel_reserve(10_000_000, 0.60, 0.30);
1631616328
1631716329 // Test with calculated channel reserve less than lower bound
16318- // i.e `MIN_THEIR_CHAN_RESERVE_SATOSHIS `
16330+ // i.e `ChannelHandshakeConfig::min_their_channel_reserve_satoshis `
1631916331 test_self_and_counterparty_channel_reserve(100_000, 0.00002, 0.30);
1632016332
1632116333 // Test with invalid channel reserves since sum of both is greater than or equal
@@ -16341,7 +16353,7 @@ mod tests {
1634116353 outbound_node_config.channel_handshake_config.their_channel_reserve_proportional_millionths = (outbound_selected_channel_reserve_perc * 1_000_000.0) as u32;
1634216354 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();
1634316355
16344- let expected_outbound_selected_chan_reserve = cmp::max(MIN_THEIR_CHAN_RESERVE_SATOSHIS , (chan.funding.get_value_satoshis() as f64 * outbound_selected_channel_reserve_perc) as u64);
16356+ let expected_outbound_selected_chan_reserve = cmp::max(outbound_node_config.channel_handshake_config.min_their_channel_reserve_satoshis , (chan.funding.get_value_satoshis() as f64 * outbound_selected_channel_reserve_perc) as u64);
1634516357 assert_eq!(chan.funding.holder_selected_channel_reserve_satoshis, expected_outbound_selected_chan_reserve);
1634616358
1634716359 let chan_open_channel_msg = chan.get_open_channel(ChainHash::using_genesis_block(network), &&logger).unwrap();
@@ -16351,7 +16363,7 @@ mod tests {
1635116363 if outbound_selected_channel_reserve_perc + inbound_selected_channel_reserve_perc < 1.0 {
1635216364 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();
1635316365
16354- let expected_inbound_selected_chan_reserve = cmp::max(MIN_THEIR_CHAN_RESERVE_SATOSHIS , (chan.funding.get_value_satoshis() as f64 * inbound_selected_channel_reserve_perc) as u64);
16366+ let expected_inbound_selected_chan_reserve = cmp::max(inbound_node_config.channel_handshake_config.min_their_channel_reserve_satoshis , (chan.funding.get_value_satoshis() as f64 * inbound_selected_channel_reserve_perc) as u64);
1635516367
1635616368 assert_eq!(chan_inbound_node.funding.holder_selected_channel_reserve_satoshis, expected_inbound_selected_chan_reserve);
1635716369 assert_eq!(chan_inbound_node.funding.counterparty_selected_channel_reserve_satoshis.unwrap(), expected_outbound_selected_chan_reserve);
@@ -16362,6 +16374,62 @@ mod tests {
1636216374 }
1636316375 }
1636416376
16377+ #[test]
16378+ #[rustfmt::skip]
16379+ fn test_configurable_min_channel_reserve() {
16380+ let test_est = TestFeeEstimator::new(15000);
16381+ let fee_est = LowerBoundedFeeEstimator::new(&test_est);
16382+ let logger = TestLogger::new();
16383+ let secp_ctx = Secp256k1::new();
16384+ let keys_provider = TestKeysInterface::new(&[42; 32], Network::Testnet);
16385+ let outbound_node_id = PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap());
16386+
16387+ // Test with min_their_channel_reserve_satoshis set to 0 (LSP use case)
16388+ let mut config = UserConfig::default();
16389+ config.channel_handshake_config.min_their_channel_reserve_satoshis = 0;
16390+ config.channel_handshake_config.their_channel_reserve_proportional_millionths = 0;
16391+
16392+ let chan = OutboundV1Channel::<&TestKeysInterface>::new(
16393+ &fee_est, &&keys_provider, &&keys_provider, outbound_node_id,
16394+ &channelmanager::provided_init_features(&config),
16395+ 1_000_000, 100_000, 42, &config, 0, 42, None, &logger
16396+ ).unwrap();
16397+
16398+ // With 0 minimum and 0 proportional, reserve should be 0 (bypasses dust limit)
16399+ assert_eq!(chan.funding.holder_selected_channel_reserve_satoshis, 0);
16400+
16401+ // Test with custom minimum enforced when proportional is lower
16402+ config.channel_handshake_config.min_their_channel_reserve_satoshis = 10_000;
16403+ config.channel_handshake_config.their_channel_reserve_proportional_millionths = 10_000; // 1%
16404+
16405+ let chan_small = OutboundV1Channel::<&TestKeysInterface>::new(
16406+ &fee_est, &&keys_provider, &&keys_provider, outbound_node_id,
16407+ &channelmanager::provided_init_features(&config),
16408+ 100_000, 100_000, 42, &config, 0, 42, None, &logger
16409+ ).unwrap();
16410+
16411+ // Proportional would be 1% of 100k = 1000, but minimum is 10000, so 10000 should be used
16412+ assert_eq!(chan_small.funding.holder_selected_channel_reserve_satoshis, 10_000);
16413+
16414+ // Test that dust limit is still enforced when min_their_channel_reserve_satoshis is non-zero but below dust limit
16415+ config.channel_handshake_config.min_their_channel_reserve_satoshis = 100; // Below dust limit of 354
16416+ config.channel_handshake_config.their_channel_reserve_proportional_millionths = 0;
16417+
16418+ let result = OutboundV1Channel::<&TestKeysInterface>::new(
16419+ &fee_est, &&keys_provider, &&keys_provider, outbound_node_id,
16420+ &channelmanager::provided_init_features(&config),
16421+ 1_000_000, 100_000, 42, &config, 0, 42, None, &logger
16422+ );
16423+
16424+ // Should fail because 100 < 354 (dust limit) and min_their_channel_reserve_satoshis > 0
16425+ assert!(result.is_err());
16426+ if let Err(APIError::APIMisuseError { err }) = result {
16427+ assert!(err.contains("dust_limit_satoshis"));
16428+ } else {
16429+ panic!("Expected APIMisuseError");
16430+ }
16431+ }
16432+
1636516433 #[test]
1636616434 #[rustfmt::skip]
1636716435 fn channel_update() {
0 commit comments