Skip to content

Commit 638e694

Browse files
amackillopFernando Ledesma
authored andcommitted
Make minimum channel reserve configurable (#1)
Add configurable min_their_channel_reserve_satoshis field to ChannelHandshakeConfig, allowing users to set the minimum channel reserve value. Special case: When set to 0, the dust limit check is bypassed. This enables LSP use cases where clients are able to fully withdraw their funds from the channel without closing it. For non-zero values below the dust limit, validation still enforces the dust limit. Replaces hardcoded MIN_THEIR_CHAN_RESERVE_SATOSHIS constant with configurable value while maintaining backward compatibility. Default remains 1000 sats to preserve existing behavior.
1 parent 8c93df7 commit 638e694

2 files changed

Lines changed: 137 additions & 18 deletions

File tree

lightning/src/ln/channel.rs

Lines changed: 106 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -910,6 +910,10 @@ pub const MAX_CHAN_DUST_LIMIT_SATOSHIS: u64 = MAX_STD_OUTPUT_DUST_LIMIT_SATOSHIS
910910
pub 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)]
913917
pub 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`.
64456454
pub(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)]
1568315698
mod 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() {

lightning/src/util/config.rs

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,14 +154,40 @@ pub struct ChannelHandshakeConfig {
154154
///
155155
/// Default value: `10_000` millionths (i.e., 1% of channel value)
156156
///
157-
/// Minimum value: If the calculated proportional value is less than `1000` sats, it will be
158-
/// treated as `1000` sats instead, which is a safe implementation-specific lower
159-
/// bound.
157+
/// Minimum value: If the calculated proportional value is less than `min_their_channel_reserve_satoshis`,
158+
/// it will be treated as `min_their_channel_reserve_satoshis` instead.
160159
///
161160
/// Maximum value: `1_000_000` (i.e., 100% of channel value. Any values larger than one million
162161
/// will be treated as one million instead, although channel negotiations will
163162
/// fail in that case.)
164163
pub their_channel_reserve_proportional_millionths: u32,
164+
/// The minimum absolute channel reserve value in satoshis that will be enforced regardless of
165+
/// the proportional reserve calculation.
166+
///
167+
/// This ensures that even if the proportional reserve calculation results in a very small value
168+
/// (or zero), at least this minimum amount will be required as a channel reserve. This provides
169+
/// a safety mechanism to ensure some minimum reserve is always maintained.
170+
///
171+
/// **Special case: Setting to `0`**
172+
///
173+
/// Setting this value to `0` allows the counterparty to have no channel reserve, enabling them
174+
/// to use their entire channel balance for payments. This is useful for LSP use cases where the
175+
/// LSP wants to allow clients to be able to fully withdraw their funds from the channel without
176+
/// closing it.
177+
///
178+
/// **Security Warning:**
179+
///
180+
/// When set to `0`, the channel reserve no longer provides economic security. If the counterparty
181+
/// broadcasts a revoked state, there is no reserve to claim as punishment. This removes the
182+
/// economic disincentive for the counterparty to attempt cheating. Only use this setting with
183+
/// trusted counterparties (e.g., known LSP clients) or when other trust mechanisms are in place.
184+
///
185+
/// When set to `0`, the dust limit check is bypassed, allowing reserves below the protocol
186+
/// minimum dust limit (354 sats). For any non-zero value below the dust limit, the dust limit
187+
/// check will still be enforced.
188+
///
189+
/// Default value: `1000` sats
190+
pub min_their_channel_reserve_satoshis: u64,
165191
/// If set, we attempt to negotiate the `anchors_zero_fee_htlc_tx`option for all future
166192
/// channels. This feature requires having a reserve of onchain funds readily available to bump
167193
/// transactions in the event of a channel force close to avoid the possibility of losing funds.
@@ -254,6 +280,7 @@ impl Default for ChannelHandshakeConfig {
254280
announce_for_forwarding: false,
255281
commit_upfront_shutdown_pubkey: true,
256282
their_channel_reserve_proportional_millionths: 10_000,
283+
min_their_channel_reserve_satoshis: 1_000,
257284
negotiate_anchors_zero_fee_htlc_tx: false,
258285
negotiate_anchor_zero_fee_commitments: false,
259286
our_max_accepted_htlcs: 50,
@@ -276,6 +303,7 @@ impl Readable for ChannelHandshakeConfig {
276303
announce_for_forwarding: Readable::read(reader)?,
277304
commit_upfront_shutdown_pubkey: Readable::read(reader)?,
278305
their_channel_reserve_proportional_millionths: Readable::read(reader)?,
306+
min_their_channel_reserve_satoshis: Readable::read(reader)?,
279307
negotiate_anchors_zero_fee_htlc_tx: Readable::read(reader)?,
280308
negotiate_anchor_zero_fee_commitments: Readable::read(reader)?,
281309
our_max_accepted_htlcs: Readable::read(reader)?,

0 commit comments

Comments
 (0)