@@ -910,6 +910,10 @@ pub const MAX_CHAN_DUST_LIMIT_SATOSHIS: u64 = MAX_STD_OUTPUT_DUST_LIMIT_SATOSHIS
910910pub const MIN_CHAN_DUST_LIMIT_SATOSHIS: u64 = 354;
911911
912912// Just a reasonable implementation-specific safe lower bound, higher than the dust limit.
913+ // Deprecated: This constant is kept for backward compatibility.
914+ // The minimum channel reserve is now configurable via `ChannelHandshakeConfig::min_their_channel_reserve_satoshis`.
915+ // This constant retains its original value for API compatibility, but the actual behavior uses the config value.
916+ #[allow(dead_code)]
913917pub const MIN_THEIR_CHAN_RESERVE_SATOSHIS: u64 = 1000;
914918
915919/// Used to return a simple Error back to ChannelManager. Will get converted to a
@@ -3456,9 +3460,10 @@ where
34563460 }
34573461 }
34583462
3459- if holder_selected_channel_reserve_satoshis < MIN_CHAN_DUST_LIMIT_SATOSHIS {
3460- // Protocol level safety check in place, although it should never happen because
3461- // of `MIN_THEIR_CHAN_RESERVE_SATOSHIS`
3463+ // Allow bypassing dust limit when min_their_channel_reserve_satoshis is explicitly set to 0 (LSP use case)
3464+ if holder_selected_channel_reserve_satoshis < MIN_CHAN_DUST_LIMIT_SATOSHIS
3465+ && config.channel_handshake_config.min_their_channel_reserve_satoshis > 0 {
3466+ // Protocol level safety check in place
34623467 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)));
34633468 }
34643469 if holder_selected_channel_reserve_satoshis * 1000 >= full_channel_value_msat {
@@ -3468,7 +3473,9 @@ where
34683473 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.",
34693474 msg_channel_reserve_satoshis, MIN_CHAN_DUST_LIMIT_SATOSHIS);
34703475 }
3471- if holder_selected_channel_reserve_satoshis < open_channel_fields.dust_limit_satoshis {
3476+ // Allow bypassing dust limit when min_their_channel_reserve_satoshis is explicitly set to 0 (LSP use case)
3477+ if holder_selected_channel_reserve_satoshis < open_channel_fields.dust_limit_satoshis
3478+ && config.channel_handshake_config.min_their_channel_reserve_satoshis > 0 {
34723479 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)));
34733480 }
34743481
@@ -4181,7 +4188,9 @@ where
41814188 if channel_reserve_satoshis > funding.get_value_satoshis() {
41824189 return Err(ChannelError::close(format!("Bogus channel_reserve_satoshis ({}). Must not be greater than ({})", channel_reserve_satoshis, funding.get_value_satoshis())));
41834190 }
4184- if common_fields.dust_limit_satoshis > funding.holder_selected_channel_reserve_satoshis {
4191+ // Allow bypassing dust limit when holder_selected_channel_reserve_satoshis is 0 (LSP use case)
4192+ if common_fields.dust_limit_satoshis > funding.holder_selected_channel_reserve_satoshis
4193+ && funding.holder_selected_channel_reserve_satoshis > 0 {
41854194 return Err(ChannelError::close(format!("Dust limit ({}) is bigger than our channel reserve ({})", common_fields.dust_limit_satoshis, funding.holder_selected_channel_reserve_satoshis)));
41864195 }
41874196 if channel_reserve_satoshis > funding.get_value_satoshis() - funding.holder_selected_channel_reserve_satoshis {
@@ -6441,15 +6450,20 @@ fn get_holder_max_htlc_value_in_flight_msat(
64416450/// Guaranteed to return a value no larger than channel_value_satoshis
64426451///
64436452/// This is used both for outbound and inbound channels and has lower bound
6444- /// of `MIN_THEIR_CHAN_RESERVE_SATOSHIS `.
6453+ /// of `ChannelHandshakeConfig::min_their_channel_reserve_satoshis `.
64456454pub(crate) fn get_holder_selected_channel_reserve_satoshis(
64466455 channel_value_satoshis: u64, config: &UserConfig,
64476456) -> u64 {
64486457 let counterparty_chan_reserve_prop_mil =
64496458 config.channel_handshake_config.their_channel_reserve_proportional_millionths as u64;
6459+ let min_their_channel_reserve_satoshis =
6460+ config.channel_handshake_config.min_their_channel_reserve_satoshis;
64506461 let calculated_reserve =
64516462 channel_value_satoshis.saturating_mul(counterparty_chan_reserve_prop_mil) / 1_000_000;
6452- cmp::min(channel_value_satoshis, cmp::max(calculated_reserve, MIN_THEIR_CHAN_RESERVE_SATOSHIS))
6463+ cmp::min(
6464+ channel_value_satoshis,
6465+ cmp::max(calculated_reserve, min_their_channel_reserve_satoshis),
6466+ )
64536467}
64546468
64556469/// This is for legacy reasons, present for forward-compatibility.
@@ -13372,9 +13386,10 @@ where
1337213386 L::Target: Logger,
1337313387 {
1337413388 let holder_selected_channel_reserve_satoshis = get_holder_selected_channel_reserve_satoshis(channel_value_satoshis, config);
13375- if holder_selected_channel_reserve_satoshis < MIN_CHAN_DUST_LIMIT_SATOSHIS {
13376- // Protocol level safety check in place, although it should never happen because
13377- // of `MIN_THEIR_CHAN_RESERVE_SATOSHIS`
13389+ // Allow bypassing dust limit when min_their_channel_reserve_satoshis is explicitly set to 0 (LSP use case)
13390+ if holder_selected_channel_reserve_satoshis < MIN_CHAN_DUST_LIMIT_SATOSHIS
13391+ && config.channel_handshake_config.min_their_channel_reserve_satoshis > 0 {
13392+ // Protocol level safety check in place
1337813393 return Err(APIError::APIMisuseError { err: format!("Holder selected channel reserve below \
1337913394 implemention limit dust_limit_satoshis {}", holder_selected_channel_reserve_satoshis) });
1338013395 }
@@ -15681,7 +15696,28 @@ pub(crate) fn hold_time_since(send_timestamp: Option<Duration>) -> Option<u32> {
1568115696
1568215697#[cfg(test)]
1568315698mod tests {
15684- use crate::chain::chaininterface::LowerBoundedFeeEstimator;
15699+ use std::cmp;
15700+ use bitcoin::amount::Amount;
15701+ use bitcoin::constants::ChainHash;
15702+ use bitcoin::script::{ScriptBuf, Builder};
15703+ use bitcoin::transaction::{Transaction, TxOut, Version};
15704+ use bitcoin::opcodes;
15705+ use bitcoin::network::Network;
15706+ use crate::ln::onion_utils::INVALID_ONION_BLINDING;
15707+ use crate::types::payment::{PaymentHash, PaymentPreimage};
15708+ use crate::ln::channel_keys::{RevocationKey, RevocationBasepoint};
15709+ use crate::ln::channelmanager::{self, HTLCSource, PaymentId};
15710+ use crate::ln::channel::InitFeatures;
15711+ use crate::ln::channel::{AwaitingChannelReadyFlags, Channel, ChannelState, InboundHTLCOutput, OutboundV1Channel, InboundV1Channel, OutboundHTLCOutput, InboundHTLCState, OutboundHTLCState, HTLCCandidate, HTLCInitiator, HTLCUpdateAwaitingACK, commit_tx_fee_sat};
15712+ use crate::ln::channel::{MAX_FUNDING_SATOSHIS_NO_WUMBO, TOTAL_BITCOIN_SUPPLY_SATOSHIS};
15713+ use crate::types::features::{ChannelFeatures, ChannelTypeFeatures, NodeFeatures};
15714+ use crate::ln::msgs;
15715+ use crate::ln::msgs::{ChannelUpdate, DecodeError, UnsignedChannelUpdate, MAX_VALUE_MSAT};
15716+ use crate::ln::script::ShutdownScript;
15717+ use crate::ln::chan_utils::{self, htlc_success_tx_weight, htlc_timeout_tx_weight};
15718+ use crate::chain::BestBlock;
15719+ use crate::chain::chaininterface::{FeeEstimator, LowerBoundedFeeEstimator, ConfirmationTarget};
15720+ use crate::sign::{ChannelSigner, InMemorySigner, EntropySource, SignerProvider};
1568515721 use crate::chain::transaction::OutPoint;
1568615722 use crate::chain::BestBlock;
1568715723 use crate::ln::chan_utils::{self, commit_tx_fee_sat, ChannelTransactionParameters};
@@ -16181,7 +16217,7 @@ mod tests {
1618116217 test_self_and_counterparty_channel_reserve(10_000_000, 0.60, 0.30);
1618216218
1618316219 // Test with calculated channel reserve less than lower bound
16184- // i.e `MIN_THEIR_CHAN_RESERVE_SATOSHIS `
16220+ // i.e `ChannelHandshakeConfig::min_their_channel_reserve_satoshis `
1618516221 test_self_and_counterparty_channel_reserve(100_000, 0.00002, 0.30);
1618616222
1618716223 // Test with invalid channel reserves since sum of both is greater than or equal
@@ -16207,8 +16243,8 @@ mod tests {
1620716243 outbound_node_config.channel_handshake_config.their_channel_reserve_proportional_millionths = (outbound_selected_channel_reserve_perc * 1_000_000.0) as u32;
1620816244 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();
1620916245
16210- 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);
16211- assert_eq!(chan.funding .holder_selected_channel_reserve_satoshis, expected_outbound_selected_chan_reserve);
16246+ 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);
16247+ assert_eq!(chan.context .holder_selected_channel_reserve_satoshis, expected_outbound_selected_chan_reserve);
1621216248
1621316249 let chan_open_channel_msg = chan.get_open_channel(ChainHash::using_genesis_block(network), &&logger).unwrap();
1621416250 let mut inbound_node_config = UserConfig::default();
@@ -16217,7 +16253,7 @@ mod tests {
1621716253 if outbound_selected_channel_reserve_perc + inbound_selected_channel_reserve_perc < 1.0 {
1621816254 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();
1621916255
16220- 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);
16256+ 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);
1622116257
1622216258 assert_eq!(chan_inbound_node.funding.holder_selected_channel_reserve_satoshis, expected_inbound_selected_chan_reserve);
1622316259 assert_eq!(chan_inbound_node.funding.counterparty_selected_channel_reserve_satoshis.unwrap(), expected_outbound_selected_chan_reserve);
@@ -16228,6 +16264,61 @@ mod tests {
1622816264 }
1622916265 }
1623016266
16267+ #[test]
16268+ #[rustfmt::skip]
16269+ fn test_configurable_min_channel_reserve() {
16270+ let fee_est = LowerBoundedFeeEstimator::new(&TestFeeEstimator { fee_est: 15_000 });
16271+ let logger = test_utils::TestLogger::new();
16272+ let secp_ctx = Secp256k1::new();
16273+ let keys_provider = test_utils::TestKeysInterface::new(&[42; 32], Network::Testnet);
16274+ let outbound_node_id = PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap());
16275+
16276+ // Test with min_their_channel_reserve_satoshis set to 0 (LSP use case)
16277+ let mut config = UserConfig::default();
16278+ config.channel_handshake_config.min_their_channel_reserve_satoshis = 0;
16279+ config.channel_handshake_config.their_channel_reserve_proportional_millionths = 0;
16280+
16281+ let chan = OutboundV1Channel::<&TestKeysInterface>::new(
16282+ &fee_est, &&keys_provider, &&keys_provider, outbound_node_id,
16283+ &channelmanager::provided_init_features(&config),
16284+ 1_000_000, 100_000, 42, &config, 0, 42, None, &logger
16285+ ).unwrap();
16286+
16287+ // With 0 minimum and 0 proportional, reserve should be 0 (bypasses dust limit)
16288+ assert_eq!(chan.context.holder_selected_channel_reserve_satoshis, 0);
16289+
16290+ // Test with custom minimum enforced when proportional is lower
16291+ config.channel_handshake_config.min_their_channel_reserve_satoshis = 10_000;
16292+ config.channel_handshake_config.their_channel_reserve_proportional_millionths = 10_000; // 1%
16293+
16294+ let chan_small = OutboundV1Channel::<&TestKeysInterface>::new(
16295+ &fee_est, &&keys_provider, &&keys_provider, outbound_node_id,
16296+ &channelmanager::provided_init_features(&config),
16297+ 100_000, 100_000, 42, &config, 0, 42, None, &logger
16298+ ).unwrap();
16299+
16300+ // Proportional would be 1% of 100k = 1000, but minimum is 10000, so 10000 should be used
16301+ assert_eq!(chan_small.context.holder_selected_channel_reserve_satoshis, 10_000);
16302+
16303+ // Test that dust limit is still enforced when min_their_channel_reserve_satoshis is non-zero but below dust limit
16304+ config.channel_handshake_config.min_their_channel_reserve_satoshis = 100; // Below dust limit of 354
16305+ config.channel_handshake_config.their_channel_reserve_proportional_millionths = 0;
16306+
16307+ let result = OutboundV1Channel::<&TestKeysInterface>::new(
16308+ &fee_est, &&keys_provider, &&keys_provider, outbound_node_id,
16309+ &channelmanager::provided_init_features(&config),
16310+ 1_000_000, 100_000, 42, &config, 0, 42, None, &logger
16311+ );
16312+
16313+ // Should fail because 100 < 354 (dust limit) and min_their_channel_reserve_satoshis > 0
16314+ assert!(result.is_err());
16315+ if let Err(APIError::APIMisuseError { err }) = result {
16316+ assert!(err.contains("dust_limit_satoshis"));
16317+ } else {
16318+ panic!("Expected APIMisuseError");
16319+ }
16320+ }
16321+
1623116322 #[test]
1623216323 #[rustfmt::skip]
1623316324 fn channel_update() {
0 commit comments