@@ -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.
67606779pub(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`.
67946823pub(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);
0 commit comments