diff --git a/fuzz/src/router.rs b/fuzz/src/router.rs index 2e5b15fc7f4..5bf9650ebad 100644 --- a/fuzz/src/router.rs +++ b/fuzz/src/router.rs @@ -248,6 +248,7 @@ pub fn do_test(data: &[u8], out: Out) { outbound_capacity_msat: capacity.saturating_mul(1000), next_outbound_htlc_limit_msat: capacity.saturating_mul(1000), next_outbound_htlc_minimum_msat: 0, + next_splice_out_maximum_sat: capacity, inbound_htlc_minimum_msat: None, inbound_htlc_maximum_msat: None, config: None, diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 45ddc7fdc50..8931b05bcfb 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -122,6 +122,8 @@ pub struct AvailableBalances { pub next_outbound_htlc_limit_msat: u64, /// The minimum value we can assign to the next outbound HTLC pub next_outbound_htlc_minimum_msat: u64, + /// The maximum value of the next splice-out + pub next_splice_out_maximum_sat: u64, } #[derive(Debug, Clone, Copy, PartialEq)] @@ -2746,20 +2748,50 @@ impl FundingScope { prev_funding: &Self, context: &ChannelContext, our_funding_contribution: SignedAmount, their_funding_contribution: SignedAmount, counterparty_funding_pubkey: PublicKey, our_new_holder_keys: ChannelPublicKeys, - ) -> Self { - debug_assert!(our_funding_contribution.unsigned_abs() <= Amount::MAX_MONEY); - debug_assert!(their_funding_contribution.unsigned_abs() <= Amount::MAX_MONEY); + ) -> Result { + if our_funding_contribution.unsigned_abs() > Amount::MAX_MONEY { + return Err(format!( + "Channel {} cannot be spliced; our {} contribution exceeds the total bitcoin supply", + context.channel_id(), + our_funding_contribution, + )); + } - let post_channel_value = prev_funding.compute_post_splice_value( - our_funding_contribution.to_sat(), - their_funding_contribution.to_sat(), - ); + if their_funding_contribution.unsigned_abs() > Amount::MAX_MONEY { + return Err(format!( + "Channel {} cannot be spliced; their {} contribution exceeds the total bitcoin supply", + context.channel_id(), + their_funding_contribution, + )); + } + + let channel_value_satoshis = prev_funding.get_value_satoshis(); + let value_to_self_satoshis = prev_funding.get_value_to_self_msat() / 1000; + let value_to_counterparty_satoshis = channel_value_satoshis + .checked_sub(value_to_self_satoshis) + .expect("value_to_self is greater than channel value"); + let our_funding_contribution_sat = our_funding_contribution.to_sat(); + let their_funding_contribution_sat = their_funding_contribution.to_sat(); let post_value_to_self_msat = prev_funding - .value_to_self_msat - .checked_add_signed(our_funding_contribution.to_sat() * 1000); - debug_assert!(post_value_to_self_msat.is_some()); - let post_value_to_self_msat = post_value_to_self_msat.unwrap(); + .get_value_to_self_msat() + .checked_add_signed(our_funding_contribution_sat * 1000) + .ok_or(format!( + "Our contribution candidate {our_funding_contribution_sat}sat is \ + greater than our total balance in the channel {value_to_self_satoshis}sat" + ))?; + + value_to_counterparty_satoshis.checked_add_signed(their_funding_contribution_sat).ok_or( + format!( + "Their contribution candidate {their_funding_contribution_sat}sat is \ + greater than their total balance in the channel {value_to_counterparty_satoshis}sat" + ), + )?; + + let post_channel_value = prev_funding.get_value_satoshis() + .checked_add_signed(our_funding_contribution.to_sat()) + .and_then(|v| v.checked_add_signed(their_funding_contribution.to_sat())) + .ok_or(format!("The sum of contributions {our_funding_contribution} and {their_funding_contribution} is greater than the channel's value"))?; let channel_parameters = &prev_funding.channel_transaction_parameters; let mut post_channel_transaction_parameters = ChannelTransactionParameters { @@ -2795,7 +2827,7 @@ impl FundingScope { prev_funding.holder_selected_channel_reserve_satoshis == 0, ); - Self { + Ok(Self { channel_transaction_parameters: post_channel_transaction_parameters, value_to_self_msat: post_value_to_self_msat, funding_transaction: None, @@ -2810,12 +2842,6 @@ impl FundingScope { prev.0.saturating_add_signed(our_funding_contribution.to_sat() * 1000); let new_counterparty_balance_msat = prev.1.saturating_add_signed(their_funding_contribution.to_sat() * 1000); - if new_holder_balance_msat < counterparty_selected_channel_reserve_satoshis { - assert_eq!(new_holder_balance_msat, prev.0); - } - if new_counterparty_balance_msat < holder_selected_channel_reserve_satoshis { - assert_eq!(new_counterparty_balance_msat, prev.1); - } Mutex::new((new_holder_balance_msat, new_counterparty_balance_msat)) }, #[cfg(debug_assertions)] @@ -2825,12 +2851,6 @@ impl FundingScope { prev.0.saturating_add_signed(our_funding_contribution.to_sat() * 1000); let new_counterparty_balance_msat = prev.1.saturating_add_signed(their_funding_contribution.to_sat() * 1000); - if new_holder_balance_msat < counterparty_selected_channel_reserve_satoshis { - assert_eq!(new_holder_balance_msat, prev.0); - } - if new_counterparty_balance_msat < holder_selected_channel_reserve_satoshis { - assert_eq!(new_counterparty_balance_msat, prev.1); - } Mutex::new((new_holder_balance_msat, new_counterparty_balance_msat)) }, #[cfg(any(test, fuzzing))] @@ -2841,16 +2861,7 @@ impl FundingScope { funding_tx_confirmed_in: None, minimum_depth_override: None, short_channel_id: None, - } - } - - /// Compute the post-splice channel value from each counterparty's contributions. - pub(super) fn compute_post_splice_value( - &self, our_funding_contribution: i64, their_funding_contribution: i64, - ) -> u64 { - self.get_value_satoshis().saturating_add_signed( - our_funding_contribution.saturating_add(their_funding_contribution), - ) + }) } /// Returns a `SharedOwnedInput` for using this `FundingScope` as the input to a new splice. @@ -6758,7 +6769,7 @@ pub(crate) fn get_legacy_default_holder_selected_channel_reserve_satoshis( /// /// This is used both for outbound and inbound channels and has lower bound /// of `dust_limit_satoshis`. -fn get_v2_channel_reserve_satoshis( +pub(crate) fn get_v2_channel_reserve_satoshis( channel_value_satoshis: u64, dust_limit_satoshis: u64, is_0reserve: bool, ) -> u64 { if is_0reserve { @@ -12390,10 +12401,7 @@ where "build_prior_contribution requires pending_splice" ); let prior = self.pending_splice.as_ref()?.contributions.last()?; - let holder_balance = self - .get_holder_counterparty_balances_floor_incl_fee(&self.funding) - .map(|(h, _)| h) - .ok(); + let holder_balance = self.get_next_splice_out_maximum(&self.funding).ok(); Some(PriorContribution::new(prior.clone(), holder_balance)) } @@ -12468,10 +12476,7 @@ where return contribution; } - let holder_balance = match self - .get_holder_counterparty_balances_floor_incl_fee(&self.funding) - .map(|(holder, _)| holder) - { + let holder_balance = match self.get_next_splice_out_maximum(&self.funding) { Ok(balance) => balance, Err(_) => return contribution, }; @@ -12572,7 +12577,10 @@ where // For splice-out, our_funding_contribution is adjusted to cover fees if there // aren't any inputs. let our_funding_contribution = contribution.net_value(); - self.validate_splice_contributions(our_funding_contribution, SignedAmount::ZERO) + let next_splice_out_maximum = self.get_next_splice_out_maximum(&self.funding)?; + let unsigned_contribution = our_funding_contribution.unsigned_abs(); + next_splice_out_maximum.to_sat().checked_add_signed(our_funding_contribution.to_sat()) + .ok_or(format!("Our splice-out value of {unsigned_contribution} is greater than the maximum {next_splice_out_maximum}")) }) { log_error!(logger, "Channel {} cannot be funded: {}", self.context.channel_id(), e); @@ -12761,9 +12769,6 @@ where ))); } - self.validate_splice_contributions(our_funding_contribution, their_funding_contribution) - .map_err(|e| ChannelError::WarnAndDisconnect(e))?; - // Rotate the pubkeys using the prev_funding_txid as a tweak let prev_funding_txid = self.funding.get_funding_txid(); let funding_pubkey = match prev_funding_txid { @@ -12779,73 +12784,44 @@ where let mut new_keys = self.funding.get_holder_pubkeys().clone(); new_keys.funding_pubkey = funding_pubkey; - Ok(FundingScope::for_splice( - &self.funding, - &self.context, - our_funding_contribution, - their_funding_contribution, - msg.funding_pubkey, - new_keys, - )) + let new_funding = self + .validate_splice_contributions( + our_funding_contribution, + their_funding_contribution, + msg.funding_pubkey, + new_keys, + ) + .map_err(|e| ChannelError::WarnAndDisconnect(e))?; + + Ok(new_funding) } fn validate_splice_contributions( &self, our_funding_contribution: SignedAmount, their_funding_contribution: SignedAmount, - ) -> Result<(), String> { - if our_funding_contribution.unsigned_abs() > Amount::MAX_MONEY { - return Err(format!( - "Channel {} cannot be spliced; our {} contribution exceeds the total bitcoin supply", - self.context.channel_id(), - our_funding_contribution, - )); - } - - if their_funding_contribution.unsigned_abs() > Amount::MAX_MONEY { - return Err(format!( - "Channel {} cannot be spliced; their {} contribution exceeds the total bitcoin supply", - self.context.channel_id(), - their_funding_contribution, - )); - } + counterparty_funding_pubkey: PublicKey, our_new_holder_keys: ChannelPublicKeys, + ) -> Result { + let candidate_scope = FundingScope::for_splice( + &self.funding, + self.context(), + our_funding_contribution, + their_funding_contribution, + counterparty_funding_pubkey, + our_new_holder_keys, + )?; - let (holder_balance_remaining, counterparty_balance_remaining) = - self.get_holder_counterparty_balances_floor_incl_fee(&self.funding).map_err(|e| { - format!("Channel {} cannot be spliced; {}", self.context.channel_id(), e) - })?; + let (post_splice_holder_balance, post_splice_counterparty_balance) = + self.get_holder_counterparty_balances_floor_incl_fee(&candidate_scope).map_err( + |e| format!("Channel {} cannot be spliced; {}", self.context.channel_id(), e), + )?; - let post_channel_value = self.funding.compute_post_splice_value( - our_funding_contribution.to_sat(), - their_funding_contribution.to_sat(), + let holder_selected_channel_reserve = + Amount::from_sat(candidate_scope.holder_selected_channel_reserve_satoshis); + let counterparty_selected_channel_reserve = Amount::from_sat( + candidate_scope.counterparty_selected_channel_reserve_satoshis.expect("Reserve is set"), ); - let counterparty_selected_channel_reserve = - Amount::from_sat(get_v2_channel_reserve_satoshis( - post_channel_value, - MIN_CHAN_DUST_LIMIT_SATOSHIS, - self.funding - .counterparty_selected_channel_reserve_satoshis - .expect("counterparty reserve is set") - == 0, - )); - let holder_selected_channel_reserve = Amount::from_sat(get_v2_channel_reserve_satoshis( - post_channel_value, - self.context.counterparty_dust_limit_satoshis, - self.funding.holder_selected_channel_reserve_satoshis == 0, - )); // We allow parties to draw from their previous reserve, as long as they satisfy their v2 reserve - if our_funding_contribution != SignedAmount::ZERO { - let post_splice_holder_balance = Amount::from_sat( - holder_balance_remaining.to_sat() - .checked_add_signed(our_funding_contribution.to_sat()) - .ok_or(format!( - "Channel {} cannot be spliced out; our remaining balance {} does not cover our negative funding contribution {}", - self.context.channel_id(), - holder_balance_remaining, - our_funding_contribution, - ))?, - ); - post_splice_holder_balance.checked_sub(counterparty_selected_channel_reserve) .ok_or(format!( "Channel {} cannot be {}; our post-splice channel balance {} is smaller than their selected v2 reserve {}", @@ -12857,17 +12833,6 @@ where } if their_funding_contribution != SignedAmount::ZERO { - let post_splice_counterparty_balance = Amount::from_sat( - counterparty_balance_remaining.to_sat() - .checked_add_signed(their_funding_contribution.to_sat()) - .ok_or(format!( - "Channel {} cannot be spliced out; their remaining balance {} does not cover their negative funding contribution {}", - self.context.channel_id(), - counterparty_balance_remaining, - their_funding_contribution, - ))?, - ); - post_splice_counterparty_balance.checked_sub(holder_selected_channel_reserve) .ok_or(format!( "Channel {} cannot be {}; their post-splice channel balance {} is smaller than our selected v2 reserve {}", @@ -12878,15 +12843,41 @@ where ))?; } - Ok(()) + #[cfg(debug_assertions)] + { + let (old_holder_balance_msat, old_counterparty_balance_msat) = + *self.funding.holder_prev_commitment_tx_balance.lock().unwrap(); + let (new_holder_balance_msat, new_counterparty_balance_msat) = + *candidate_scope.holder_prev_commitment_tx_balance.lock().unwrap(); + if new_holder_balance_msat < counterparty_selected_channel_reserve.to_sat() * 1000 { + debug_assert_eq!(new_holder_balance_msat, old_holder_balance_msat); + } + if new_counterparty_balance_msat < holder_selected_channel_reserve.to_sat() * 1000 { + debug_assert_eq!(new_counterparty_balance_msat, old_counterparty_balance_msat); + } + } + #[cfg(debug_assertions)] + { + let (old_holder_balance_msat, old_counterparty_balance_msat) = + *self.funding.counterparty_prev_commitment_tx_balance.lock().unwrap(); + let (new_holder_balance_msat, new_counterparty_balance_msat) = + *candidate_scope.counterparty_prev_commitment_tx_balance.lock().unwrap(); + if new_holder_balance_msat < counterparty_selected_channel_reserve.to_sat() * 1000 { + debug_assert_eq!(new_holder_balance_msat, old_holder_balance_msat); + } + if new_counterparty_balance_msat < holder_selected_channel_reserve.to_sat() * 1000 { + debug_assert_eq!(new_counterparty_balance_msat, old_counterparty_balance_msat); + } + } + + Ok(candidate_scope) } fn resolve_queued_contribution( &self, feerate: FeeRate, logger: &L, ) -> Result<(Option, Option), ChannelError> { let holder_balance = self - .get_holder_counterparty_balances_floor_incl_fee(&self.funding) - .map(|(holder, _)| holder) + .get_next_splice_out_maximum(&self.funding) .map_err(|e| { log_info!( logger, @@ -13063,22 +13054,21 @@ where None => SignedAmount::ZERO, }; - self.validate_splice_contributions(our_funding_contribution, their_funding_contribution) - .map_err(|e| ChannelError::WarnAndDisconnect(e))?; - // Reuse funding pubkeys from the last negotiated candidate since all RBF candidates // for the same splice share the same funding output script. let holder_pubkeys = last_candidate.get_holder_pubkeys().clone(); let counterparty_funding_pubkey = *last_candidate.counterparty_funding_pubkey(); - Ok(FundingScope::for_splice( - &self.funding, - &self.context, - our_funding_contribution, - their_funding_contribution, - counterparty_funding_pubkey, - holder_pubkeys, - )) + let new_funding = self + .validate_splice_contributions( + our_funding_contribution, + their_funding_contribution, + counterparty_funding_pubkey, + holder_pubkeys, + ) + .map_err(|e| ChannelError::WarnAndDisconnect(e))?; + + Ok(new_funding) } pub(crate) fn tx_init_rbf( @@ -13201,8 +13191,6 @@ where Some(value) => SignedAmount::from_sat(value), None => SignedAmount::ZERO, }; - self.validate_splice_contributions(our_funding_contribution, their_funding_contribution) - .map_err(|e| ChannelError::WarnAndDisconnect(e))?; let last_candidate = pending_splice.negotiated_candidates.last().ok_or_else(|| { ChannelError::WarnAndDisconnect("No negotiated splice candidates for RBF".to_owned()) @@ -13210,14 +13198,16 @@ where let holder_pubkeys = last_candidate.get_holder_pubkeys().clone(); let counterparty_funding_pubkey = *last_candidate.counterparty_funding_pubkey(); - Ok(FundingScope::for_splice( - &self.funding, - &self.context, - our_funding_contribution, - their_funding_contribution, - counterparty_funding_pubkey, - holder_pubkeys, - )) + let new_funding = self + .validate_splice_contributions( + our_funding_contribution, + their_funding_contribution, + counterparty_funding_pubkey, + holder_pubkeys, + ) + .map_err(|e| ChannelError::WarnAndDisconnect(e))?; + + Ok(new_funding) } pub(crate) fn tx_ack_rbf( @@ -13302,22 +13292,35 @@ where let our_funding_contribution = funding_negotiation_context.our_funding_contribution; let their_funding_contribution = SignedAmount::from_sat(msg.funding_contribution_satoshis); - self.validate_splice_contributions(our_funding_contribution, their_funding_contribution) - .map_err(|e| ChannelError::WarnAndDisconnect(e))?; let mut new_keys = self.funding.get_holder_pubkeys().clone(); new_keys.funding_pubkey = *new_holder_funding_key; - Ok(FundingScope::for_splice( - &self.funding, - &self.context, - our_funding_contribution, - their_funding_contribution, - msg.funding_pubkey, - new_keys, - )) + let new_funding = self + .validate_splice_contributions( + our_funding_contribution, + their_funding_contribution, + msg.funding_pubkey, + new_keys, + ) + .map_err(|e| ChannelError::WarnAndDisconnect(e))?; + + Ok(new_funding) } + /// The balances returned here should only be used to check that both parties still hold + /// their respective reserves *after* a splice. This function also checks that both local + /// and remote commitments still have at least one output after the splice, which is + /// particularly relevant for zero-reserve channels. + /// + /// Do NOT use this to determine how much the holder can splice out of the channel. The balance + /// of the holder after a splice is not necessarily equal to the funds they can splice out + /// of the channel due to the v2 reserve, and the zero-reserve-at-least-one-output + /// requirements. Note you cannot simply subtract out the reserve, as splicing funds out + /// of the channel changes the reserve the holder must keep in the channel. + /// + /// See [`FundedChannel::get_next_splice_out_maximum`] for the maximum value of the next + /// splice out of the holder's balance. fn get_holder_counterparty_balances_floor_incl_fee( &self, funding: &FundingScope, ) -> Result<(Amount, Amount), String> { @@ -13338,6 +13341,16 @@ where self.context.feerate_per_kw }; + // Different dust limits on the local and remote commitments cause the commitment + // transaction fee to be different depending on the commitment, so we grab the floor + // of both balances across both commitments here. + // + // `get_channel_stats` also checks for at least one output on the commitment given + // these parameters. This is particularly relevant for zero-reserve channels. + // + // This "at-least-one-output" check is why we still run both checks on + // zero-fee-commitment channels, even though those channels don't suffer from the + // commitment transaction fee asymmetry. let (local_stats, _local_htlcs) = self .context .get_next_local_commitment_stats( @@ -13378,6 +13391,55 @@ where Ok((holder_balance_floor, counterparty_balance_floor)) } + /// Determines the maximum value that the holder can splice out of the channel, accounting + /// for the updated reserves after said splice. This maximum also makes sure the local + /// commitment retains at least one output after the splice, which is particularly relevant + /// for zero-reserve channels. + fn get_next_splice_out_maximum(&self, funding: &FundingScope) -> Result { + let include_counterparty_unknown_htlcs = true; + // We are not interested in dust exposure + let dust_exposure_limiting_feerate = None; + + // When reading the available balances, we take the remote's view of the pending + // HTLCs, see `tx_builder` for further details + let (remote_stats, _remote_htlcs) = self + .context + .get_next_remote_commitment_stats( + funding, + None, // htlc_candidate + include_counterparty_unknown_htlcs, + 0, + self.context.feerate_per_kw, + dust_exposure_limiting_feerate, + ) + .map_err(|()| "Balance exhausted on remote commitment")?; + + let next_splice_out_maximum_sat = + remote_stats.available_balances.next_splice_out_maximum_sat; + + #[cfg(debug_assertions)] + { + // After this max splice out, validation passes, accounting for the updated reserves + self.validate_splice_contributions( + SignedAmount::from_sat(-(next_splice_out_maximum_sat as i64)), + SignedAmount::ZERO, + funding.counterparty_funding_pubkey().clone(), + funding.get_holder_pubkeys().clone(), + ) + .unwrap(); + // Splice-out an additional satoshi, and validation fails! + self.validate_splice_contributions( + SignedAmount::from_sat(-((next_splice_out_maximum_sat + 1) as i64)), + SignedAmount::ZERO, + funding.counterparty_funding_pubkey().clone(), + funding.get_holder_pubkeys().clone(), + ) + .unwrap_err(); + } + + Ok(Amount::from_sat(next_splice_out_maximum_sat)) + } + pub fn splice_locked( &mut self, msg: &msgs::SpliceLocked, node_signer: &NS, chain_hash: ChainHash, user_config: &UserConfig, block_height: u32, logger: &L, @@ -13603,6 +13665,9 @@ where next_outbound_htlc_minimum_msat: acc .next_outbound_htlc_minimum_msat .max(e.next_outbound_htlc_minimum_msat), + next_splice_out_maximum_sat: acc + .next_splice_out_maximum_sat + .min(e.next_splice_out_maximum_sat), }) }) } @@ -14140,10 +14205,10 @@ where // the user can reclaim their inputs. if let Err(e) = contribution.validate().and_then(|()| { let our_funding_contribution = contribution.net_value(); - self.validate_splice_contributions( - our_funding_contribution, - SignedAmount::ZERO, - ) + let next_splice_out_maximum = self.get_next_splice_out_maximum(&self.funding)?; + let unsigned_contribution = our_funding_contribution.unsigned_abs(); + next_splice_out_maximum.to_sat().checked_add_signed(our_funding_contribution.to_sat()) + .ok_or(format!("Our splice-out value of {unsigned_contribution} is greater than the maximum {next_splice_out_maximum}")) }) { let failed = self.splice_funding_failed_for(contribution); return Err(( @@ -16803,7 +16868,7 @@ mod tests { use crate::chain::chaininterface::LowerBoundedFeeEstimator; use crate::chain::transaction::OutPoint; use crate::chain::BestBlock; - use crate::ln::chan_utils::{self, commit_tx_fee_sat, ChannelTransactionParameters}; + use crate::ln::chan_utils::{self, commit_tx_fee_sat}; use crate::ln::channel::{ AwaitingChannelReadyFlags, ChannelState, FundedChannel, HTLCUpdateAwaitingACK, InboundHTLCOutput, InboundHTLCState, InboundUpdateAdd, InboundV1Channel, @@ -16821,6 +16886,7 @@ mod tests { use crate::sign::tx_builder::HTLCAmountDirection; #[cfg(ldk_test_vectors)] use crate::sign::{ChannelSigner, EntropySource, InMemorySigner, SignerProvider}; + #[cfg(ldk_test_vectors)] use crate::sync::Mutex; #[cfg(ldk_test_vectors)] use crate::types::features::ChannelTypeFeatures; @@ -19241,95 +19307,4 @@ mod tests { assert_eq!(node_a_chan.context.channel_state, ChannelState::AwaitingChannelReady(AwaitingChannelReadyFlags::THEIR_CHANNEL_READY)); assert!(node_a_chan.check_get_channel_ready(0, &&logger).is_some()); } - - fn get_pre_and_post( - pre_channel_value: u64, our_funding_contribution: i64, their_funding_contribution: i64, - ) -> (u64, u64) { - use crate::ln::channel::{FundingScope, PredictedNextFee}; - - let funding = FundingScope { - value_to_self_msat: 0, - counterparty_selected_channel_reserve_satoshis: None, - holder_selected_channel_reserve_satoshis: 0, - - #[cfg(debug_assertions)] - holder_prev_commitment_tx_balance: Mutex::new((0, 0)), - #[cfg(debug_assertions)] - counterparty_prev_commitment_tx_balance: Mutex::new((0, 0)), - - #[cfg(any(test, fuzzing))] - next_local_fee: Mutex::new(PredictedNextFee::default()), - #[cfg(any(test, fuzzing))] - next_remote_fee: Mutex::new(PredictedNextFee::default()), - - channel_transaction_parameters: ChannelTransactionParameters::test_dummy( - pre_channel_value, - ), - funding_transaction: None, - funding_tx_confirmed_in: None, - funding_tx_confirmation_height: 0, - short_channel_id: None, - minimum_depth_override: None, - }; - let post_channel_value = - funding.compute_post_splice_value(our_funding_contribution, their_funding_contribution); - (pre_channel_value, post_channel_value) - } - - #[test] - fn test_compute_post_splice_value() { - { - // increase, small amounts - let (pre_channel_value, post_channel_value) = get_pre_and_post(9_000, 6_000, 0); - assert_eq!(pre_channel_value, 9_000); - assert_eq!(post_channel_value, 15_000); - } - { - // increase, small amounts - let (pre_channel_value, post_channel_value) = get_pre_and_post(9_000, 4_000, 2_000); - assert_eq!(pre_channel_value, 9_000); - assert_eq!(post_channel_value, 15_000); - } - { - // increase, small amounts - let (pre_channel_value, post_channel_value) = get_pre_and_post(9_000, 0, 6_000); - assert_eq!(pre_channel_value, 9_000); - assert_eq!(post_channel_value, 15_000); - } - { - // decrease, small amounts - let (pre_channel_value, post_channel_value) = get_pre_and_post(15_000, -6_000, 0); - assert_eq!(pre_channel_value, 15_000); - assert_eq!(post_channel_value, 9_000); - } - { - // decrease, small amounts - let (pre_channel_value, post_channel_value) = get_pre_and_post(15_000, -4_000, -2_000); - assert_eq!(pre_channel_value, 15_000); - assert_eq!(post_channel_value, 9_000); - } - { - // increase and decrease - let (pre_channel_value, post_channel_value) = get_pre_and_post(15_000, 4_000, -2_000); - assert_eq!(pre_channel_value, 15_000); - assert_eq!(post_channel_value, 17_000); - } - let base2: u64 = 2; - let huge63i3 = (base2.pow(63) - 3) as i64; - assert_eq!(huge63i3, 9223372036854775805); - assert_eq!(-huge63i3, -9223372036854775805); - { - // increase, large amount - let (pre_channel_value, post_channel_value) = get_pre_and_post(9_000, huge63i3, 3); - assert_eq!(pre_channel_value, 9_000); - assert_eq!(post_channel_value, 9223372036854784807); - } - { - // increase, large amounts - let (pre_channel_value, post_channel_value) = - get_pre_and_post(9_000, huge63i3, huge63i3); - assert_eq!(pre_channel_value, 9_000); - assert_eq!(post_channel_value, 9223372036854784807); - } - } } diff --git a/lightning/src/ln/channel_state.rs b/lightning/src/ln/channel_state.rs index 5547bee8f4c..9fd0df4a1bf 100644 --- a/lightning/src/ln/channel_state.rs +++ b/lightning/src/ln/channel_state.rs @@ -399,6 +399,8 @@ pub struct ChannelDetails { /// an upper-bound. This is intended for use when routing, allowing us to ensure we pick a /// route which is valid. pub next_outbound_htlc_minimum_msat: u64, + /// The maximum value of the next splice out from our channel balance. + pub next_splice_out_maximum_sat: u64, /// The available inbound capacity for the remote peer to send HTLCs to us. This does not /// include any pending HTLCs which are not yet fully resolved (and, thus, whose balance is not /// available for inclusion in new inbound HTLCs). @@ -533,6 +535,7 @@ impl ChannelDetails { outbound_capacity_msat: 0, next_outbound_htlc_limit_msat: 0, next_outbound_htlc_minimum_msat: u64::MAX, + next_splice_out_maximum_sat: 0, } }); let (to_remote_reserve_satoshis, to_self_reserve_satoshis) = @@ -582,6 +585,7 @@ impl ChannelDetails { outbound_capacity_msat: balance.outbound_capacity_msat, next_outbound_htlc_limit_msat: balance.next_outbound_htlc_limit_msat, next_outbound_htlc_minimum_msat: balance.next_outbound_htlc_minimum_msat, + next_splice_out_maximum_sat: balance.next_splice_out_maximum_sat, user_channel_id: context.get_user_id(), confirmations_required: channel.minimum_depth(), confirmations: Some(funding.get_funding_tx_confirmations(best_block_height)), @@ -621,6 +625,7 @@ impl_writeable_tlv_based!(ChannelDetails, { (20, inbound_capacity_msat, required), (21, next_outbound_htlc_minimum_msat, (default_value, 0)), (22, confirmations_required, option), + (23, next_splice_out_maximum_sat, (default_value, u64::from(outbound_capacity_msat.0.unwrap()) / 1000)), (24, force_close_spend_delay, option), (26, is_outbound, required), (28, is_channel_ready, required), @@ -725,6 +730,7 @@ mod tests { outbound_capacity_msat: 24_300, next_outbound_htlc_limit_msat: 20_000, next_outbound_htlc_minimum_msat: 132, + next_splice_out_maximum_sat: 20, inbound_capacity_msat: 42, unspendable_punishment_reserve: Some(8273), confirmations_required: Some(5), diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index a3c33b8320f..30712af956f 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -8121,6 +8121,7 @@ impl< outbound_capacity_msat: 0, next_outbound_htlc_limit_msat: 0, next_outbound_htlc_minimum_msat: u64::MAX, + next_splice_out_maximum_sat: 0, } }); let is_in_range = (balances.next_outbound_htlc_minimum_msat diff --git a/lightning/src/ln/htlc_reserve_unit_tests.rs b/lightning/src/ln/htlc_reserve_unit_tests.rs index aaf81b87be7..45d3cf5950f 100644 --- a/lightning/src/ln/htlc_reserve_unit_tests.rs +++ b/lightning/src/ln/htlc_reserve_unit_tests.rs @@ -2581,7 +2581,7 @@ fn test_0reserve_no_outputs() { do_test_0reserve_no_outputs_p2a_anchor(); } -fn setup_0reserve_no_outputs_channels<'a, 'b, 'c, 'd>( +pub(crate) fn setup_0reserve_no_outputs_channels<'a, 'b, 'c, 'd>( nodes: &'a Vec>, channel_value_sat: u64, dust_limit_satoshis: u64, ) -> (ChannelId, Transaction) { let node_a_id = nodes[0].node.get_our_node_id(); diff --git a/lightning/src/ln/splicing_tests.rs b/lightning/src/ln/splicing_tests.rs index aec7fa9d1e1..05308509dbc 100644 --- a/lightning/src/ln/splicing_tests.rs +++ b/lightning/src/ln/splicing_tests.rs @@ -16,7 +16,8 @@ use crate::chain::ChannelMonitorUpdateStatus; use crate::events::{ClosureReason, Event, FundingInfo, HTLCHandlingFailureType}; use crate::ln::chan_utils; use crate::ln::channel::{ - CHANNEL_ANNOUNCEMENT_PROPAGATION_DELAY, FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE, + ANCHOR_OUTPUT_VALUE_SATOSHI, CHANNEL_ANNOUNCEMENT_PROPAGATION_DELAY, + FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE, }; use crate::ln::channelmanager::{provided_init_features, PaymentId, BREAKDOWN_TIMEOUT}; use crate::ln::functional_test_utils::*; @@ -4155,8 +4156,8 @@ fn do_test_splice_pending_htlcs(config: UserConfig) { format!("Channel {} cannot accept funding contribution", channel_id); assert_eq!(error, APIError::APIMisuseError { err: cannot_accept_contribution }); let cannot_be_funded = format!( - "Channel {} cannot be funded: Channel {} cannot be spliced out; our post-splice channel balance {} is smaller than their selected v2 reserve {}", - channel_id, channel_id, post_splice_reserve - Amount::ONE_SAT, post_splice_reserve + "Channel {} cannot be funded: Our splice-out value of {} is greater than the maximum {}", + channel_id, splice_out_incl_fees + Amount::ONE_SAT, splice_out_incl_fees, ); initiator.logger.assert_log("lightning::ln::channel", cannot_be_funded, 1); @@ -6733,3 +6734,202 @@ fn test_splice_rbf_rejects_own_low_feerate_after_several_attempts() { other => panic!("Expected SpliceFailed, got {:?}", other), } } + +#[test] +fn test_0reserve_splice() { + let mut config = test_default_channel_config(); + config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = false; + config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = false; + let a = do_test_0reserve_splice_holder_validation(false, config.clone()); + let b = do_test_0reserve_splice_holder_validation(true, config.clone()); + assert_eq!(a, b); + assert_eq!(a, ChannelTypeFeatures::only_static_remote_key()); + + config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = true; + config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = false; + let a = do_test_0reserve_splice_holder_validation(false, config.clone()); + let b = do_test_0reserve_splice_holder_validation(true, config.clone()); + assert_eq!(a, b); + assert_eq!(a, ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies()); + + let mut config = test_default_channel_config(); + config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = false; + config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = false; + let a = do_test_0reserve_splice_counterparty_validation(false, config.clone()); + let b = do_test_0reserve_splice_counterparty_validation(true, config.clone()); + assert_eq!(a, b); + assert_eq!(a, ChannelTypeFeatures::only_static_remote_key()); + + config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = true; + config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = false; + let a = do_test_0reserve_splice_counterparty_validation(false, config.clone()); + let b = do_test_0reserve_splice_counterparty_validation(true, config.clone()); + assert_eq!(a, b); + assert_eq!(a, ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies()); + + // TODO: Skip 0FC channels for now as these always have an output on the commitment, the P2A + // output. We will be able to withdraw up to the dust limit of the funding script, which + // is checked in interactivetx. Still need to double check whether that's what we actually + // want. +} + +#[cfg(test)] +fn do_test_0reserve_splice_holder_validation( + splice_passes: bool, config: UserConfig, +) -> ChannelTypeFeatures { + use crate::ln::htlc_reserve_unit_tests::setup_0reserve_no_outputs_channels; + + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = + create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config.clone())]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let _node_id_0 = nodes[0].node.get_our_node_id(); + let _node_id_1 = nodes[1].node.get_our_node_id(); + + let channel_value_sat = 100_000; + // Some dust limit, does not matter + let dust_limit_satoshis = 546; + + let (channel_id, _tx) = + setup_0reserve_no_outputs_channels(&nodes, channel_value_sat, dust_limit_satoshis); + let details = &nodes[0].node.list_channels()[0]; + let channel_type = details.channel_type.clone().unwrap(); + + let feerate = if channel_type == ChannelTypeFeatures::only_static_remote_key() { + 253 * FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE as u32 + } else if channel_type == ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies() { + 253 + } else { + panic!("Unexpected channel type"); + }; + let commit_tx_fee_sat = chan_utils::commit_tx_fee_sat(feerate, 0, &channel_type); + let anchors_sat = + if channel_type == ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies() { + ANCHOR_OUTPUT_VALUE_SATOSHI * 2 + } else { + 0 + }; + + let estimated_fees = 183; + let splice_out_max_value = Amount::from_sat( + channel_value_sat - commit_tx_fee_sat - anchors_sat - estimated_fees - dust_limit_satoshis, + ); + let outputs = vec![TxOut { + value: splice_out_max_value + if splice_passes { Amount::ZERO } else { Amount::ONE_SAT }, + script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), + }]; + + if splice_passes { + let contribution = initiate_splice_out(&nodes[0], &nodes[1], channel_id, outputs).unwrap(); + + let (splice_tx, _) = splice_channel(&nodes[0], &nodes[1], channel_id, contribution); + mine_transaction(&nodes[0], &splice_tx); + mine_transaction(&nodes[1], &splice_tx); + lock_splice_after_blocks(&nodes[0], &nodes[1], ANTI_REORG_DELAY - 1); + } else { + assert!(initiate_splice_out(&nodes[0], &nodes[1], channel_id, outputs).is_err()); + let splice_out_value = + splice_out_max_value + Amount::from_sat(estimated_fees) + Amount::ONE_SAT; + let splice_out_max_value = splice_out_max_value + Amount::from_sat(estimated_fees); + let cannot_be_funded = format!( + "Channel {channel_id} cannot be funded: Our \ + splice-out value of {splice_out_value} is greater than the maximum \ + {splice_out_max_value}" + ); + nodes[0].logger.assert_log("lightning::ln::channel", cannot_be_funded, 1); + } + + channel_type +} + +#[cfg(test)] +fn do_test_0reserve_splice_counterparty_validation( + splice_passes: bool, config: UserConfig, +) -> ChannelTypeFeatures { + use crate::ln::htlc_reserve_unit_tests::setup_0reserve_no_outputs_channels; + + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = + create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config.clone())]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let _node_id_0 = nodes[0].node.get_our_node_id(); + let _node_id_1 = nodes[1].node.get_our_node_id(); + + let channel_value_sat = 100_000; + // Some dust limit, does not matter + let dust_limit_satoshis = 546; + + let (channel_id, _tx) = + setup_0reserve_no_outputs_channels(&nodes, channel_value_sat, dust_limit_satoshis); + let details = &nodes[0].node.list_channels()[0]; + let channel_type = details.channel_type.clone().unwrap(); + + let feerate = if channel_type == ChannelTypeFeatures::only_static_remote_key() { + 253 * FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE as u32 + } else if channel_type == ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies() { + 253 + } else { + panic!("Unexpected channel type"); + }; + let commit_tx_fee_sat = chan_utils::commit_tx_fee_sat(feerate, 0, &channel_type); + let anchors_sat = + if channel_type == ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies() { + ANCHOR_OUTPUT_VALUE_SATOSHI * 2 + } else { + 0 + }; + + let splice_out_value_incl_fees = + Amount::from_sat(channel_value_sat - commit_tx_fee_sat - anchors_sat - dust_limit_satoshis); + + let funding_contribution_sat = + -(splice_out_value_incl_fees.to_sat() as i64) - if splice_passes { 0 } else { 1 }; + let outputs = vec![TxOut { + // Splice out some dummy amount to get past the initiator's validation, + // we'll modify the message in-flight. + value: Amount::from_sat(1_000), + script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), + }]; + let _contribution = initiate_splice_out(&nodes[0], &nodes[1], channel_id, outputs).unwrap(); + + let initiator = &nodes[0]; + let acceptor = &nodes[1]; + let node_id_initiator = initiator.node.get_our_node_id(); + let node_id_acceptor = acceptor.node.get_our_node_id(); + + let stfu_init = get_event_msg!(initiator, MessageSendEvent::SendStfu, node_id_acceptor); + acceptor.node.handle_stfu(node_id_initiator, &stfu_init); + let stfu_ack = get_event_msg!(acceptor, MessageSendEvent::SendStfu, node_id_initiator); + initiator.node.handle_stfu(node_id_acceptor, &stfu_ack); + + let mut splice_init = + get_event_msg!(initiator, MessageSendEvent::SendSpliceInit, node_id_acceptor); + // Make the modification here + splice_init.funding_contribution_satoshis = funding_contribution_sat; + + if splice_passes { + acceptor.node.handle_splice_init(node_id_initiator, &splice_init); + let _splice_ack = + get_event_msg!(acceptor, MessageSendEvent::SendSpliceAck, node_id_initiator); + } else { + acceptor.node.handle_splice_init(node_id_initiator, &splice_init); + let msg_events = acceptor.node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 1); + if let MessageSendEvent::HandleError { action, .. } = &msg_events[0] { + assert!(matches!(action, msgs::ErrorAction::DisconnectPeerWithWarning { .. })); + } else { + panic!("Expected MessageSendEvent::HandleError"); + } + let cannot_splice_out = format!( + "Got non-closing error: Channel {channel_id} cannot \ + be spliced; Balance exhausted on local commitment" + ); + acceptor.logger.assert_log("lightning::ln::channelmanager", cannot_splice_out, 1); + } + + channel_type +} diff --git a/lightning/src/routing/router.rs b/lightning/src/routing/router.rs index edb048c8c7d..fce3996efa1 100644 --- a/lightning/src/routing/router.rs +++ b/lightning/src/routing/router.rs @@ -4150,6 +4150,7 @@ mod tests { outbound_capacity_msat, next_outbound_htlc_limit_msat: outbound_capacity_msat, next_outbound_htlc_minimum_msat: 0, + next_splice_out_maximum_sat: outbound_capacity_msat / 1000, inbound_capacity_msat: 42, unspendable_punishment_reserve: None, confirmations_required: None, @@ -9649,6 +9650,7 @@ pub(crate) mod bench_utils { outbound_capacity_msat: 10_000_000_000, next_outbound_htlc_minimum_msat: 0, next_outbound_htlc_limit_msat: 10_000_000_000, + next_splice_out_maximum_sat: 10_000_000, inbound_capacity_msat: 0, unspendable_punishment_reserve: None, confirmations_required: None, diff --git a/lightning/src/sign/tx_builder.rs b/lightning/src/sign/tx_builder.rs index f51759db5e9..d78f65d223a 100644 --- a/lightning/src/sign/tx_builder.rs +++ b/lightning/src/sign/tx_builder.rs @@ -9,7 +9,9 @@ use crate::ln::chan_utils::{ second_stage_tx_fees_sat, ChannelTransactionParameters, CommitmentTransaction, HTLCOutputInCommitment, }; -use crate::ln::channel::{CommitmentStats, ANCHOR_OUTPUT_VALUE_SATOSHI}; +use crate::ln::channel::{ + get_v2_channel_reserve_satoshis, CommitmentStats, ANCHOR_OUTPUT_VALUE_SATOSHI, +}; use crate::prelude::*; use crate::types::features::ChannelTypeFeatures; use crate::util::logger::Logger; @@ -315,6 +317,107 @@ fn get_next_commitment_stats( }) } +/// Determines the maximum value that the holder can splice out of the channel, accounting +/// for the updated reserves after said splice. This maximum also makes sure the local commitment +/// retains at least one output after the splice, which is particularly relevant for +/// zero-reserve channels. +// +// The equation to determine `max_splice_percentage_constraint_sat` is: +// 1) floor((c - s) / 100) == h - s - d +// We want the maximum value of s that will satisfy this equation, therefore, we solve: +// 2) (c - s) / 100 < h - s - d + 1 +// where c: `channel_value_satoshis` +// s: `max_splice_percentage_constraint_sat` +// h: `local_balance_before_fee_sat` +// d: `post_splice_delta_above_reserve_sat` +// This results in: +// 3) s < (100h + 100 - 100d - c) / 99 +fn get_next_splice_out_maximum_sat( + is_outbound_from_holder: bool, channel_value_satoshis: u64, local_balance_before_fee_msat: u64, + remote_balance_before_fee_msat: u64, spiked_feerate: u32, + spiked_feerate_nondust_htlc_count: usize, post_splice_delta_above_reserve_sat: u64, + channel_constraints: &ChannelConstraints, channel_type: &ChannelTypeFeatures, +) -> u64 { + let local_balance_before_fee_sat = local_balance_before_fee_msat / 1000; + let mut next_splice_out_maximum_sat = if channel_constraints + .counterparty_selected_channel_reserve_satoshis + != 0 + { + let dividend = local_balance_before_fee_sat + .saturating_mul(100) + .saturating_add(100) + .saturating_sub(post_splice_delta_above_reserve_sat.saturating_mul(100)) + .saturating_sub(channel_value_satoshis); + let max_splice_percentage_constraint_sat = dividend.saturating_sub(1) / 99; + let max_splice_dust_limit_constraint_sat = local_balance_before_fee_sat + .saturating_sub(channel_constraints.holder_dust_limit_satoshis) + .saturating_sub(post_splice_delta_above_reserve_sat); + // Take whichever constraint you hit first as you increase the value of the splice-out + let max_splice_out_sat = + cmp::min(max_splice_percentage_constraint_sat, max_splice_dust_limit_constraint_sat); + #[cfg(debug_assertions)] + if max_splice_out_sat == 0 { + let current_balance_sat = + local_balance_before_fee_sat.saturating_sub(post_splice_delta_above_reserve_sat); + let v2_reserve_sat = get_v2_channel_reserve_satoshis( + channel_value_satoshis, + channel_constraints.holder_dust_limit_satoshis, + false, + ); + // If the holder cannot splice out anything, they must be at or + // below the v2 reserve + debug_assert!(current_balance_sat <= v2_reserve_sat); + } else { + let post_splice_reserve_sat = get_v2_channel_reserve_satoshis( + channel_value_satoshis.saturating_sub(max_splice_out_sat), + channel_constraints.holder_dust_limit_satoshis, + false, + ); + // If the holder can splice out some maximum, splicing out that + // maximum lands them at exactly the new v2 reserve + the + // `post_splice_delta_above_reserve_sat` + debug_assert_eq!( + local_balance_before_fee_sat.saturating_sub(max_splice_out_sat), + post_splice_reserve_sat.saturating_add(post_splice_delta_above_reserve_sat) + ); + } + max_splice_out_sat + } else { + // In a zero-reserve channel, the holder is free to withdraw up to its `post_splice_delta_above_reserve_sat` + local_balance_before_fee_sat.saturating_sub(post_splice_delta_above_reserve_sat) + }; + + // We only bother to check the local commitment here, the counterparty will check its own commitment. + // + // If the current `next_splice_out_maximum_sat` would produce a local commitment with no + // outputs, bump this maximum such that, after the splice, the holder's balance covers at + // least `dust_limit_satoshis` and, if they are the funder, `current_spiked_tx_fee_sat`. + // We don't include an additional non-dust inbound HTLC in the `current_spiked_tx_fee_sat`, + // because we don't mind if the holder dips below their dust limit to cover the fee for that + // inbound non-dust HTLC. + if !has_output( + is_outbound_from_holder, + local_balance_before_fee_msat.saturating_sub(next_splice_out_maximum_sat * 1000), + remote_balance_before_fee_msat, + spiked_feerate, + spiked_feerate_nondust_htlc_count, + channel_constraints.holder_dust_limit_satoshis, + channel_type, + ) { + let dust_limit_satoshis = channel_constraints.holder_dust_limit_satoshis; + let current_spiked_tx_fee_sat = commit_tx_fee_sat(spiked_feerate, 0, channel_type); + let min_balance_sat = if is_outbound_from_holder { + dust_limit_satoshis.saturating_add(current_spiked_tx_fee_sat) + } else { + dust_limit_satoshis + }; + next_splice_out_maximum_sat = + (local_balance_before_fee_msat / 1000).saturating_sub(min_balance_sat); + } + + next_splice_out_maximum_sat +} + fn get_available_balances( is_outbound_from_holder: bool, channel_value_satoshis: u64, value_to_holder_msat: u64, pending_htlcs: &[HTLCAmountDirection], feerate_per_kw: u32, @@ -411,6 +514,20 @@ fn get_available_balances( total_anchors_sat.saturating_mul(1000), ); + let next_splice_out_maximum_sat = get_next_splice_out_maximum_sat( + is_outbound_from_holder, + channel_value_satoshis, + local_balance_before_fee_msat, + remote_balance_before_fee_msat, + spiked_feerate, + // The number of non-dust HTLCs on the local commitment at the spiked feerate + local_nondust_htlc_count, + // The post-splice minimum balance of the holder + if is_outbound_from_holder { local_min_commit_tx_fee_sat } else { 0 }, + &channel_constraints, + channel_type, + ); + let outbound_capacity_msat = local_balance_before_fee_msat .saturating_sub(channel_constraints.counterparty_selected_channel_reserve_satoshis * 1000); @@ -583,6 +700,7 @@ fn get_available_balances( outbound_capacity_msat, next_outbound_htlc_limit_msat: available_capacity_msat, next_outbound_htlc_minimum_msat, + next_splice_out_maximum_sat, } }