Skip to content

Commit c2955a0

Browse files
authored
Merge pull request lightningdevkit#4580 from tankyleo/2026-04-reserve-greater-than-channel-value
Error if the calculated reserve would be greater than the channel value
2 parents 4f6f15d + 53e156a commit c2955a0

8 files changed

Lines changed: 401 additions & 53 deletions

File tree

lightning/src/ln/channel.rs

Lines changed: 111 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2753,16 +2753,14 @@ impl FundingScope {
27532753
) -> Result<Self, String> {
27542754
if our_funding_contribution.unsigned_abs() > Amount::MAX_MONEY {
27552755
return Err(format!(
2756-
"Channel {} cannot be spliced; our {} contribution exceeds the total bitcoin supply",
2757-
context.channel_id(),
2756+
"Our {} contribution exceeds the total bitcoin supply",
27582757
our_funding_contribution,
27592758
));
27602759
}
27612760

27622761
if their_funding_contribution.unsigned_abs() > Amount::MAX_MONEY {
27632762
return Err(format!(
2764-
"Channel {} cannot be spliced; their {} contribution exceeds the total bitcoin supply",
2765-
context.channel_id(),
2763+
"Their {} contribution exceeds the total bitcoin supply",
27662764
their_funding_contribution,
27672765
));
27682766
}
@@ -2822,17 +2820,31 @@ impl FundingScope {
28222820
// New reserve values are based on the new channel value and are v2-specific
28232821
let counterparty_selected_channel_reserve_satoshis = get_v2_channel_reserve_satoshis(
28242822
post_channel_value_sat,
2825-
MIN_CHAN_DUST_LIMIT_SATOSHIS,
2823+
context.holder_dust_limit_satoshis,
28262824
prev_funding
28272825
.counterparty_selected_channel_reserve_satoshis
28282826
.expect("counterparty reserve is set")
28292827
== 0,
2830-
);
2828+
)
2829+
.map_err(|()| {
2830+
format!(
2831+
"The post-splice channel value {post_channel_value_sat} is smaller \
2832+
than our dust limit {}",
2833+
context.holder_dust_limit_satoshis
2834+
)
2835+
})?;
28312836
let holder_selected_channel_reserve_satoshis = get_v2_channel_reserve_satoshis(
28322837
post_channel_value_sat,
28332838
context.counterparty_dust_limit_satoshis,
28342839
prev_funding.holder_selected_channel_reserve_satoshis == 0,
2835-
);
2840+
)
2841+
.map_err(|()| {
2842+
format!(
2843+
"The post-splice channel value {post_channel_value_sat} is smaller \
2844+
than their dust limit {}",
2845+
context.counterparty_dust_limit_satoshis,
2846+
)
2847+
})?;
28362848

28372849
Ok(Self {
28382850
channel_transaction_parameters: post_channel_transaction_parameters,
@@ -3395,6 +3407,9 @@ pub(super) struct ChannelContext<SP: SignerProvider> {
33953407
/// We use this to close if funding is never broadcasted.
33963408
pub(super) channel_creation_height: u32,
33973409

3410+
#[cfg(any(test, feature = "_test_utils"))]
3411+
pub(crate) counterparty_dust_limit_satoshis: u64,
3412+
#[cfg(not(any(test, feature = "_test_utils")))]
33983413
counterparty_dust_limit_satoshis: u64,
33993414

34003415
#[cfg(any(test, feature = "_test_utils"))]
@@ -6757,20 +6772,32 @@ fn get_legacy_default_holder_max_htlc_value_in_flight_msat(channel_value_satoshi
67576772
/// This is used both for outbound and inbound channels and has lower bound
67586773
/// of `MIN_THEIR_CHAN_RESERVE_SATOSHIS`, and the `dust_limit_satoshis` of
67596774
/// the counterparty.
6775+
///
6776+
/// Returns `Err` if `channel_value_satoshis` is smaller than
6777+
/// `MIN_THEIR_CHAN_RESERVE_SATOSHIS` or the `dust_limit_satoshis` of the
6778+
/// counterparty.
67606779
pub(crate) fn get_holder_selected_channel_reserve_satoshis(
67616780
channel_value_satoshis: u64, their_dust_limit_satoshis: u64, config: &UserConfig,
67626781
is_0reserve: bool,
6763-
) -> u64 {
6782+
) -> Result<u64, ()> {
6783+
if channel_value_satoshis < MIN_THEIR_CHAN_RESERVE_SATOSHIS
6784+
|| channel_value_satoshis < their_dust_limit_satoshis
6785+
{
6786+
return Err(());
6787+
}
67646788
if is_0reserve {
6765-
return 0;
6789+
return Ok(0);
67666790
}
6767-
let counterparty_chan_reserve_prop_mil =
6768-
config.channel_handshake_config.their_channel_reserve_proportional_millionths as u64;
6791+
// As described in the `ChannelHandshakeConfig` docs, we cap this value at 1_000_000.
6792+
let counterparty_chan_reserve_prop_mil = cmp::min(
6793+
config.channel_handshake_config.their_channel_reserve_proportional_millionths as u64,
6794+
1_000_000,
6795+
);
67696796
let calculated_reserve =
67706797
channel_value_satoshis.saturating_mul(counterparty_chan_reserve_prop_mil) / 1_000_000;
67716798
let channel_reserve_satoshis = cmp::max(calculated_reserve, MIN_THEIR_CHAN_RESERVE_SATOSHIS);
67726799
let channel_reserve_satoshis = cmp::max(channel_reserve_satoshis, their_dust_limit_satoshis);
6773-
cmp::min(channel_value_satoshis, channel_reserve_satoshis)
6800+
Ok(channel_reserve_satoshis)
67746801
}
67756802

67766803
/// This is for legacy reasons, present for forward-compatibility.
@@ -6787,19 +6814,24 @@ pub(crate) fn get_legacy_default_holder_selected_channel_reserve_satoshis(
67876814
/// Returns a minimum channel reserve value each party needs to maintain, fixed in the spec to a
67886815
/// default of 1% of the total channel value.
67896816
///
6790-
/// Guaranteed to return a value no larger than channel_value_satoshis
6817+
/// Guaranteed to return a value no larger than `channel_value_satoshis`
67916818
///
67926819
/// This is used both for outbound and inbound channels and has lower bound
67936820
/// of `dust_limit_satoshis`.
6821+
///
6822+
/// Returns `Err` if `channel_value_satoshis` is smaller than `dust_limit_satoshis`.
67946823
pub(crate) fn get_v2_channel_reserve_satoshis(
67956824
channel_value_satoshis: u64, dust_limit_satoshis: u64, is_0reserve: bool,
6796-
) -> u64 {
6825+
) -> Result<u64, ()> {
6826+
if channel_value_satoshis < dust_limit_satoshis {
6827+
return Err(());
6828+
}
67976829
if is_0reserve {
6798-
return 0;
6830+
return Ok(0);
67996831
}
68006832
// Fixed at 1% of channel value by spec.
68016833
let (q, _) = channel_value_satoshis.overflowing_div(100);
6802-
cmp::min(channel_value_satoshis, cmp::max(q, dust_limit_satoshis))
6834+
Ok(cmp::max(q, dust_limit_satoshis))
68036835
}
68046836

68056837
/// Returns the minimum feerate for RBF attempts given a previous feerate.
@@ -12922,7 +12954,8 @@ where
1292212954
their_funding_contribution,
1292312955
counterparty_funding_pubkey,
1292412956
our_new_holder_keys,
12925-
)?;
12957+
)
12958+
.map_err(|e| format!("Channel {} cannot be spliced; {}", self.context.channel_id(), e))?;
1292612959

1292712960
let (post_splice_holder_balance, post_splice_counterparty_balance) =
1292812961
self.get_holder_counterparty_balances_floor_incl_fee(&candidate_scope).map_err(
@@ -14578,12 +14611,19 @@ impl<SP: SignerProvider> OutboundV1Channel<SP> {
1457814611
// a dust limit higher than our selected reserve.
1457914612
let their_dust_limit_satoshis = 0;
1458014613
let is_0reserve = trusted_channel_features.is_some_and(|f| f.is_0reserve());
14581-
let holder_selected_channel_reserve_satoshis = get_holder_selected_channel_reserve_satoshis(
14582-
channel_value_satoshis,
14583-
their_dust_limit_satoshis,
14584-
config,
14585-
is_0reserve,
14586-
);
14614+
let holder_selected_channel_reserve_satoshis =
14615+
get_holder_selected_channel_reserve_satoshis(
14616+
channel_value_satoshis,
14617+
their_dust_limit_satoshis,
14618+
config,
14619+
is_0reserve,
14620+
)
14621+
.map_err(|()| APIError::APIMisuseError {
14622+
err: format!(
14623+
"The channel value {channel_value_satoshis} is smaller than \
14624+
{MIN_THEIR_CHAN_RESERVE_SATOSHIS}"
14625+
),
14626+
})?;
1458714627
if holder_selected_channel_reserve_satoshis < MIN_CHAN_DUST_LIMIT_SATOSHIS && !is_0reserve {
1458814628
// Protocol level safety check in place, although it should never happen because
1458914629
// of `MIN_THEIR_CHAN_RESERVE_SATOSHIS` and `MIN_CHANNEL_VALUE_SATOSHIS`
@@ -14975,12 +15015,20 @@ impl<SP: SignerProvider> InboundV1Channel<SP> {
1497515015
let channel_type =
1497615016
channel_type_from_open_channel(&msg.common_fields, our_supported_features)?;
1497715017

14978-
let holder_selected_channel_reserve_satoshis = get_holder_selected_channel_reserve_satoshis(
14979-
msg.common_fields.funding_satoshis,
14980-
msg.common_fields.dust_limit_satoshis,
14981-
config,
14982-
trusted_channel_features.is_some_and(|f| f.is_0reserve()),
14983-
);
15018+
let holder_selected_channel_reserve_satoshis =
15019+
get_holder_selected_channel_reserve_satoshis(
15020+
msg.common_fields.funding_satoshis,
15021+
msg.common_fields.dust_limit_satoshis,
15022+
config,
15023+
trusted_channel_features.is_some_and(|f| f.is_0reserve()),
15024+
)
15025+
.map_err(|()| {
15026+
ChannelError::close(format!(
15027+
"The channel value {} is smaller than either their dust \
15028+
limit {}, or {MIN_THEIR_CHAN_RESERVE_SATOSHIS}",
15029+
msg.common_fields.funding_satoshis, msg.common_fields.dust_limit_satoshis,
15030+
))
15031+
})?;
1498415032
let counterparty_pubkeys = ChannelPublicKeys {
1498515033
funding_pubkey: msg.common_fields.funding_pubkey,
1498615034
revocation_basepoint: RevocationBasepoint::from(msg.common_fields.revocation_basepoint),
@@ -15237,8 +15285,13 @@ impl<SP: SignerProvider> PendingV2Channel<SP> {
1523715285
});
1523815286

1523915287
let holder_selected_channel_reserve_satoshis = get_v2_channel_reserve_satoshis(
15240-
funding_satoshis, MIN_CHAN_DUST_LIMIT_SATOSHIS, trusted_channel_features.is_some_and(|f| f.is_0reserve()));
15241-
15288+
funding_satoshis, MIN_CHAN_DUST_LIMIT_SATOSHIS, trusted_channel_features.is_some_and(|f| f.is_0reserve())
15289+
).map_err(|()| APIError::APIMisuseError {
15290+
err: format!(
15291+
"The channel value {funding_satoshis} is smaller than their dust \
15292+
limit {MIN_CHAN_DUST_LIMIT_SATOSHIS}"
15293+
)
15294+
})?;
1524215295
let funding_feerate_sat_per_1000_weight = fee_estimator.bounded_sat_per_1000_weight(funding_confirmation_target);
1524315296
let funding_tx_locktime = LockTime::from_height(current_chain_height)
1524415297
.map_err(|_| APIError::APIMisuseError {
@@ -15377,9 +15430,16 @@ impl<SP: SignerProvider> PendingV2Channel<SP> {
1537715430
let channel_value_satoshis =
1537815431
our_funding_contribution_sats.saturating_add(msg.common_fields.funding_satoshis);
1537915432
let counterparty_selected_channel_reserve_satoshis = get_v2_channel_reserve_satoshis(
15380-
channel_value_satoshis, MIN_CHAN_DUST_LIMIT_SATOSHIS, msg.disable_channel_reserve.is_some());
15433+
channel_value_satoshis, MIN_CHAN_DUST_LIMIT_SATOSHIS, msg.disable_channel_reserve.is_some()
15434+
).map_err(|()| ChannelError::close(format!(
15435+
"The channel value {channel_value_satoshis} is smaller than our dust limit {MIN_CHAN_DUST_LIMIT_SATOSHIS}"
15436+
)))?;
15437+
let their_dust_limit_satoshis = msg.common_fields.dust_limit_satoshis;
1538115438
let holder_selected_channel_reserve_satoshis = get_v2_channel_reserve_satoshis(
15382-
channel_value_satoshis, msg.common_fields.dust_limit_satoshis, trusted_channel_features.is_some_and(|f| f.is_0reserve()));
15439+
channel_value_satoshis, their_dust_limit_satoshis, trusted_channel_features.is_some_and(|f| f.is_0reserve())
15440+
).map_err(|()| ChannelError::close(format!(
15441+
"The channel value {channel_value_satoshis} is smaller than their dust limit {their_dust_limit_satoshis}"
15442+
)))?;
1538315443

1538415444
let channel_type = channel_type_from_open_channel(&msg.common_fields, our_supported_features)?;
1538515445

@@ -17570,6 +17630,10 @@ mod tests {
1757017630
// to channel value
1757117631
test_self_and_counterparty_channel_reserve(10_000_000, 0.50, 0.50);
1757217632
test_self_and_counterparty_channel_reserve(10_000_000, 0.60, 0.50);
17633+
17634+
// Make sure we correctly handle reserves greater than the channel value
17635+
test_self_and_counterparty_channel_reserve(100_000, 1.1, 0.30);
17636+
test_self_and_counterparty_channel_reserve(100_000, 0.30, 1.1);
1757317637
}
1757417638

1757517639
#[rustfmt::skip]
@@ -17589,7 +17653,19 @@ mod tests {
1758917653
outbound_node_config.channel_handshake_config.their_channel_reserve_proportional_millionths = (outbound_selected_channel_reserve_perc * 1_000_000.0) as u32;
1759017654
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, None).unwrap();
1759117655

17592-
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);
17656+
let outbound_capped_reserve_perc = if outbound_selected_channel_reserve_perc.lt(&1.0) {
17657+
outbound_selected_channel_reserve_perc
17658+
} else {
17659+
1.0
17660+
};
17661+
17662+
let inbound_capped_reserve_perc = if inbound_selected_channel_reserve_perc.lt(&1.0) {
17663+
inbound_selected_channel_reserve_perc
17664+
} else {
17665+
1.0
17666+
};
17667+
17668+
let expected_outbound_selected_chan_reserve = cmp::max(MIN_THEIR_CHAN_RESERVE_SATOSHIS, (chan.funding.get_value_satoshis() as f64 * outbound_capped_reserve_perc) as u64);
1759317669
assert_eq!(chan.funding.holder_selected_channel_reserve_satoshis, expected_outbound_selected_chan_reserve);
1759417670

1759517671
let chan_open_channel_msg = chan.get_open_channel(ChainHash::using_genesis_block(network), &&logger).unwrap();
@@ -17599,7 +17675,7 @@ mod tests {
1759917675
if outbound_selected_channel_reserve_perc + inbound_selected_channel_reserve_perc < 1.0 {
1760017676
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, None).unwrap();
1760117677

17602-
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);
17678+
let expected_inbound_selected_chan_reserve = cmp::max(MIN_THEIR_CHAN_RESERVE_SATOSHIS, (chan.funding.get_value_satoshis() as f64 * inbound_capped_reserve_perc) as u64);
1760317679

1760417680
assert_eq!(chan_inbound_node.funding.holder_selected_channel_reserve_satoshis, expected_inbound_selected_chan_reserve);
1760517681
assert_eq!(chan_inbound_node.funding.counterparty_selected_channel_reserve_satoshis.unwrap(), expected_outbound_selected_chan_reserve);

lightning/src/ln/channel_open_tests.rs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ use crate::chain::{self, ChannelMonitorUpdateStatus};
1616
use crate::events::{ClosureReason, Event, FundingInfo};
1717
use crate::ln::channel::{
1818
get_holder_selected_channel_reserve_satoshis, ChannelError, InboundV1Channel,
19-
OutboundV1Channel, COINBASE_MATURITY, UNFUNDED_CHANNEL_AGE_LIMIT_TICKS,
19+
OutboundV1Channel, COINBASE_MATURITY, MIN_THEIR_CHAN_RESERVE_SATOSHIS,
20+
UNFUNDED_CHANNEL_AGE_LIMIT_TICKS,
2021
};
2122
use crate::ln::channelmanager::{
2223
self, TrustedChannelFeatures, BREAKDOWN_TIMEOUT, MAX_UNFUNDED_CHANNEL_PEERS,
@@ -473,7 +474,8 @@ pub fn test_insane_channel_opens() {
473474
// funding satoshis
474475
let channel_value_sat = 31337; // same as funding satoshis
475476
let channel_reserve_satoshis =
476-
get_holder_selected_channel_reserve_satoshis(channel_value_sat, 0, &legacy_cfg, false);
477+
get_holder_selected_channel_reserve_satoshis(channel_value_sat, 0, &legacy_cfg, false)
478+
.unwrap();
477479
let push_msat = (channel_value_sat - channel_reserve_satoshis) * 1000;
478480

479481
// Have node0 initiate a channel to node1 with aforementioned parameters
@@ -552,7 +554,13 @@ pub fn test_insane_channel_opens() {
552554
},
553555
);
554556

555-
insane_open_helper("Peer never wants payout outputs?", |mut msg| {
557+
let crazy_dust_limit = channel_value_sat + 1;
558+
let expected_error_str = format!(
559+
"Got non-closing error: The channel value \
560+
{channel_value_sat} is smaller than either their dust limit {crazy_dust_limit}, or \
561+
{MIN_THEIR_CHAN_RESERVE_SATOSHIS}"
562+
);
563+
insane_open_helper(&expected_error_str, |mut msg| {
556564
msg.common_fields.dust_limit_satoshis = msg.common_fields.funding_satoshis + 1;
557565
msg
558566
});

lightning/src/ln/functional_tests.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -415,7 +415,8 @@ pub fn test_inbound_outbound_capacity_is_not_zero() {
415415
assert_eq!(channels0.len(), 1);
416416
assert_eq!(channels1.len(), 1);
417417

418-
let reserve = get_holder_selected_channel_reserve_satoshis(100_000, 0, &default_config, false);
418+
let reserve =
419+
get_holder_selected_channel_reserve_satoshis(100_000, 0, &default_config, false).unwrap();
419420
assert_eq!(channels0[0].inbound_capacity_msat, 95000000 - reserve * 1000);
420421
assert_eq!(channels1[0].outbound_capacity_msat, 95000000 - reserve * 1000);
421422

lightning/src/ln/htlc_reserve_unit_tests.rs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,9 @@ fn do_test_counterparty_no_reserve(send_from_initiator: bool) {
5555
push_amt -= feerate_per_kw as u64
5656
* (commitment_tx_base_weight(&channel_type_features) + 4 * COMMITMENT_TX_WEIGHT_PER_HTLC)
5757
/ 1000 * 1000;
58-
push_amt -=
59-
get_holder_selected_channel_reserve_satoshis(100_000, 0, &default_config, false) * 1000;
58+
push_amt -= get_holder_selected_channel_reserve_satoshis(100_000, 0, &default_config, false)
59+
.unwrap()
60+
* 1000;
6061

6162
let push = if send_from_initiator { 0 } else { push_amt };
6263
let temp_channel_id =
@@ -1002,8 +1003,9 @@ pub fn test_chan_reserve_violation_outbound_htlc_inbound_chan() {
10021003
&channel_type_features,
10031004
);
10041005

1005-
push_amt -=
1006-
get_holder_selected_channel_reserve_satoshis(100_000, 0, &default_config, false) * 1000;
1006+
push_amt -= get_holder_selected_channel_reserve_satoshis(100_000, 0, &default_config, false)
1007+
.unwrap()
1008+
* 1000;
10071009

10081010
let _ = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, push_amt);
10091011

@@ -1048,8 +1050,9 @@ pub fn test_chan_reserve_dust_inbound_htlcs_outbound_chan() {
10481050
MIN_AFFORDABLE_HTLC_COUNT as u64,
10491051
&channel_type_features,
10501052
);
1051-
push_amt -=
1052-
get_holder_selected_channel_reserve_satoshis(100_000, 0, &default_config, false) * 1000;
1053+
push_amt -= get_holder_selected_channel_reserve_satoshis(100_000, 0, &default_config, false)
1054+
.unwrap()
1055+
* 1000;
10531056
create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100000, push_amt);
10541057

10551058
let (htlc_success_tx_fee_sat, _) =

lightning/src/ln/payment_tests.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5063,7 +5063,7 @@ fn test_htlc_forward_considers_anchor_outputs_value() {
50635063
create_announced_chan_between_nodes_with_value(&nodes, 1, 2, CHAN_AMT, PUSH_MSAT);
50645064

50655065
let channel_reserve_msat =
5066-
get_holder_selected_channel_reserve_satoshis(CHAN_AMT, 0, &config, false) * 1000;
5066+
get_holder_selected_channel_reserve_satoshis(CHAN_AMT, 0, &config, false).unwrap() * 1000;
50675067
let commitment_fee_msat = chan_utils::commit_tx_fee_sat(
50685068
*nodes[1].fee_estimator.sat_per_kw.lock().unwrap(),
50695069
2,

0 commit comments

Comments
 (0)