Skip to content

Commit b9e4311

Browse files
committed
Make minimum channel reserve configurable
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 fd85279 commit b9e4311

2 files changed

Lines changed: 118 additions & 20 deletions

File tree

lightning/src/ln/channel.rs

Lines changed: 85 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -928,6 +928,10 @@ pub const MAX_CHAN_DUST_LIMIT_SATOSHIS: u64 = MAX_STD_OUTPUT_DUST_LIMIT_SATOSHIS
928928
pub 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)]
931935
pub 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`.
64636472
pub(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() {

lightning/src/util/config.rs

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,14 +154,42 @@ 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+
///
191+
/// [`MIN_CHAN_DUST_LIMIT_SATOSHIS`]: crate::ln::channel::MIN_CHAN_DUST_LIMIT_SATOSHIS
192+
pub min_their_channel_reserve_satoshis: u64,
165193
/// If set, we attempt to negotiate the `anchors_zero_fee_htlc_tx`option for all future
166194
/// channels. This feature requires having a reserve of onchain funds readily available to bump
167195
/// transactions in the event of a channel force close to avoid the possibility of losing funds.
@@ -254,6 +282,7 @@ impl Default for ChannelHandshakeConfig {
254282
announce_for_forwarding: false,
255283
commit_upfront_shutdown_pubkey: true,
256284
their_channel_reserve_proportional_millionths: 10_000,
285+
min_their_channel_reserve_satoshis: 1_000,
257286
negotiate_anchors_zero_fee_htlc_tx: false,
258287
negotiate_anchor_zero_fee_commitments: false,
259288
our_max_accepted_htlcs: 50,
@@ -276,6 +305,7 @@ impl Readable for ChannelHandshakeConfig {
276305
announce_for_forwarding: Readable::read(reader)?,
277306
commit_upfront_shutdown_pubkey: Readable::read(reader)?,
278307
their_channel_reserve_proportional_millionths: Readable::read(reader)?,
308+
min_their_channel_reserve_satoshis: Readable::read(reader)?,
279309
negotiate_anchors_zero_fee_htlc_tx: Readable::read(reader)?,
280310
negotiate_anchor_zero_fee_commitments: Readable::read(reader)?,
281311
our_max_accepted_htlcs: Readable::read(reader)?,

0 commit comments

Comments
 (0)