diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 3d25934d18d..783e1e183b7 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -2255,7 +2255,7 @@ impl FundingScope { fn for_splice( prev_funding: &Self, context: &ChannelContext, our_funding_contribution: SignedAmount, their_funding_contribution: SignedAmount, counterparty_funding_pubkey: PublicKey, - ) -> Result + ) -> Self where SP::Target: SignerProvider, { @@ -2298,14 +2298,14 @@ impl FundingScope { .funding_pubkey = counterparty_funding_pubkey; // New reserve values are based on the new channel value and are v2-specific - let counterparty_selected_channel_reserve_satoshis = Some(get_v2_channel_reserve_satoshis( + let counterparty_selected_channel_reserve_satoshis = + Some(get_v2_channel_reserve_satoshis(post_channel_value, MIN_CHAN_DUST_LIMIT_SATOSHIS)); + let holder_selected_channel_reserve_satoshis = get_v2_channel_reserve_satoshis( post_channel_value, context.counterparty_dust_limit_satoshis, - )); - let holder_selected_channel_reserve_satoshis = - get_v2_channel_reserve_satoshis(post_channel_value, MIN_CHAN_DUST_LIMIT_SATOSHIS); + ); - Ok(Self { + Self { channel_transaction_parameters: post_channel_transaction_parameters, value_to_self_msat: post_value_to_self_msat, funding_transaction: None, @@ -2329,7 +2329,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. @@ -6187,9 +6187,8 @@ fn get_v2_channel_reserve_satoshis(channel_value_satoshis: u64, dust_limit_satos #[cfg(splicing)] fn check_splice_contribution_sufficient( - channel_balance: Amount, contribution: &SpliceContribution, is_initiator: bool, - funding_feerate: FeeRate, -) -> Result { + contribution: &SpliceContribution, is_initiator: bool, funding_feerate: FeeRate, +) -> Result { let contribution_amount = contribution.value(); if contribution_amount < SignedAmount::ZERO { let estimated_fee = Amount::from_sat(estimate_v2_funding_transaction_fee( @@ -6200,14 +6199,11 @@ fn check_splice_contribution_sufficient( funding_feerate.to_sat_per_kwu() as u32, )); - if channel_balance >= contribution_amount.unsigned_abs() + estimated_fee { - Ok(estimated_fee) - } else { - Err(format!( - "Available channel balance {channel_balance} is lower than needed for splicing out {}, considering fees of {estimated_fee}", - contribution_amount.unsigned_abs(), - )) - } + contribution_amount + .checked_sub( + estimated_fee.to_signed().expect("fees should never exceed Amount::MAX_MONEY"), + ) + .ok_or(format!("Our {contribution_amount} contribution plus the fee estimate exceeds the total bitcoin supply")) } else { check_v2_funding_inputs_sufficient( contribution_amount.to_sat(), @@ -6216,7 +6212,7 @@ fn check_splice_contribution_sufficient( true, funding_feerate.to_sat_per_kwu() as u32, ) - .map(Amount::from_sat) + .map(|_| contribution_amount) } } @@ -6280,7 +6276,8 @@ fn check_v2_funding_inputs_sufficient( let mut total_input_sats = 0u64; for FundingTxInput { utxo, .. } in funding_inputs.iter() { - total_input_sats = total_input_sats.saturating_add(utxo.output.value.to_sat()); + total_input_sats = total_input_sats.checked_add(utxo.output.value.to_sat()) + .ok_or("Sum of input values is greater than the total bitcoin supply")?; } // If the inputs are enough to cover intended contribution amount, with fees even when @@ -6294,8 +6291,11 @@ fn check_v2_funding_inputs_sufficient( // TODO(splicing): refine check including the fact wether a change will be added or not. // Can be done once dual funding preparation is included. - let minimal_input_amount_needed = contribution_amount.saturating_add(estimated_fee as i64); - if (total_input_sats as i64) < minimal_input_amount_needed { + let minimal_input_amount_needed = contribution_amount.checked_add(estimated_fee as i64) + .ok_or(format!("Our {contribution_amount} contribution plus the fee estimate exceeds the total bitcoin supply"))?; + if i64::try_from(total_input_sats).map_err(|_| "Sum of input values is greater than the total bitcoin supply")? + < minimal_input_amount_needed + { Err(format!( "Total input amount {total_input_sats} is lower than needed for contribution {contribution_amount}, considering fees of {estimated_fee}. Need more inputs.", )) @@ -11098,68 +11098,32 @@ where }); } - if our_funding_contribution > SignedAmount::MAX_MONEY { - return Err(APIError::APIMisuseError { - err: format!( - "Channel {} cannot be spliced in; contribution exceeds total bitcoin supply: {}", - self.context.channel_id(), - our_funding_contribution, - ), - }); - } - - if our_funding_contribution < -SignedAmount::MAX_MONEY { - return Err(APIError::APIMisuseError { - err: format!( - "Channel {} cannot be spliced out; contribution exhausts total bitcoin supply: {}", - self.context.channel_id(), - our_funding_contribution, - ), - }); - } - - // Note: post-splice channel value is not yet known at this point, counterparty contribution is not known - // (Cannot test for miminum required post-splice channel value) - - let channel_balance = Amount::from_sat(self.funding.get_value_to_self_msat() / 1000); - let fees = check_splice_contribution_sufficient( - channel_balance, + // Fees for splice-out are paid from the channel balance whereas fees for splice-in + // are paid by the funding inputs. Therefore, in the case of splice-out, we add the + // fees on top of the user-specified contribution. We leave the user-specified + // contribution as-is for splice-ins. + let adjusted_funding_contribution = check_splice_contribution_sufficient( &contribution, - true, // is_initiator - FeeRate::from_sat_per_kwu(funding_feerate_per_kw as u64), + true, + FeeRate::from_sat_per_kwu(u64::from(funding_feerate_per_kw)), ) - .map_err(|e| { - let splice_type = if our_funding_contribution < SignedAmount::ZERO { - "spliced out" - } else { - "spliced in" - }; - APIError::APIMisuseError { - err: format!( - "Channel {} cannot be {}; {}", - self.context.channel_id(), - splice_type, - e, - ), - } + .map_err(|e| APIError::APIMisuseError { + err: format!( + "Channel {} cannot be {}; {}", + self.context.channel_id(), + if our_funding_contribution.is_positive() { "spliced in" } else { "spliced out" }, + e + ), })?; - // Fees for splice-out are paid from the channel balance whereas fees for splice-in are paid - // by the funding inputs. - let adjusted_funding_contribution = if our_funding_contribution < SignedAmount::ZERO { - let adjusted_funding_contribution = our_funding_contribution - - fees.to_signed().expect("fees should never exceed Amount::MAX_MONEY"); - - // TODO(splicing): Check that channel balance does not go below the channel reserve - let _post_channel_balance = AddSigned::checked_add_signed( - channel_balance.to_sat(), - adjusted_funding_contribution.to_sat(), - ); - - adjusted_funding_contribution - } else { - our_funding_contribution - }; + // Note: post-splice channel value is not yet known at this point, counterparty contribution is not known + // (Cannot test for miminum required post-splice channel value) + let their_funding_contribution = SignedAmount::ZERO; + self.validate_splice_contributions( + adjusted_funding_contribution, + their_funding_contribution, + ) + .map_err(|err| APIError::APIMisuseError { err })?; for FundingTxInput { utxo, prevtx, .. } in contribution.inputs().iter() { const MESSAGE_TEMPLATE: msgs::TxAddInput = msgs::TxAddInput { @@ -11281,90 +11245,120 @@ where ))); } + // TODO(splicing): Once splice acceptor can contribute, check that inputs are sufficient, + // similarly to the check in `splice_channel`. debug_assert_eq!(our_funding_contribution, SignedAmount::ZERO); - // TODO(splicing): Move this check once user-provided contributions are supported for - // counterparty-initiated splices. - if our_funding_contribution > SignedAmount::MAX_MONEY { - return Err(ChannelError::WarnAndDisconnect(format!( - "Channel {} cannot be spliced in; our {} contribution exceeds the total bitcoin supply", - self.context.channel_id(), - our_funding_contribution, - ))); - } - - if our_funding_contribution < -SignedAmount::MAX_MONEY { + let their_funding_contribution = SignedAmount::from_sat(msg.funding_contribution_satoshis); + if their_funding_contribution == SignedAmount::ZERO { return Err(ChannelError::WarnAndDisconnect(format!( - "Channel {} cannot be spliced out; our {} contribution exhausts the total bitcoin supply", + "Channel {} cannot be spliced; they are the initiator, and their contribution is zero", self.context.channel_id(), - our_funding_contribution, ))); } - let their_funding_contribution = SignedAmount::from_sat(msg.funding_contribution_satoshis); - self.validate_splice_contribution(their_funding_contribution)?; - - // TODO(splicing): Check that channel balance does not go below the channel reserve + self.validate_splice_contributions(our_funding_contribution, their_funding_contribution) + .map_err(|e| ChannelError::WarnAndDisconnect(e))?; - let splice_funding = FundingScope::for_splice( + Ok(FundingScope::for_splice( &self.funding, &self.context, our_funding_contribution, their_funding_contribution, msg.funding_pubkey, - )?; - - // TODO(splicing): Once splice acceptor can contribute, check that inputs are sufficient, - // similarly to the check in `splice_channel`. - - // Note on channel reserve requirement pre-check: as the splice acceptor does not contribute, - // it can't go below reserve, therefore no pre-check is done here. - - // TODO(splicing): Early check for reserve requirement - - Ok(splice_funding) + )) } #[cfg(splicing)] - fn validate_splice_contribution( - &self, their_funding_contribution: SignedAmount, - ) -> Result<(), ChannelError> { - if their_funding_contribution > SignedAmount::MAX_MONEY { - return Err(ChannelError::WarnAndDisconnect(format!( - "Channel {} cannot be spliced in; their {} contribution exceeds the total bitcoin supply", + fn validate_splice_contributions( + &self, our_funding_contribution: SignedAmount, their_funding_contribution: SignedAmount, + ) -> Result<(), String> { + if our_funding_contribution.abs() > SignedAmount::MAX_MONEY { + return Err(format!( + "Channel {} cannot be spliced; our {} contribution exceeds the total bitcoin supply", self.context.channel_id(), - their_funding_contribution, - ))); + our_funding_contribution, + )); } - if their_funding_contribution < -SignedAmount::MAX_MONEY { - return Err(ChannelError::WarnAndDisconnect(format!( - "Channel {} cannot be spliced out; their {} contribution exhausts the total bitcoin supply", + if their_funding_contribution.abs() > SignedAmount::MAX_MONEY { + return Err(format!( + "Channel {} cannot be spliced; their {} contribution exceeds the total bitcoin supply", self.context.channel_id(), their_funding_contribution, - ))); + )); } - let their_channel_balance = Amount::from_sat(self.funding.get_value_satoshis()) - - Amount::from_sat(self.funding.get_value_to_self_msat() / 1000); - let post_channel_balance = AddSigned::checked_add_signed( - their_channel_balance.to_sat(), + 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_channel_value = self.funding.compute_post_splice_value( + our_funding_contribution.to_sat(), their_funding_contribution.to_sat(), ); + let counterparty_selected_channel_reserve = Amount::from_sat( + get_v2_channel_reserve_satoshis(post_channel_value, MIN_CHAN_DUST_LIMIT_SATOSHIS), + ); + let holder_selected_channel_reserve = Amount::from_sat(get_v2_channel_reserve_satoshis( + post_channel_value, + self.context.counterparty_dust_limit_satoshis, + )); - if post_channel_balance.is_none() { - return Err(ChannelError::WarnAndDisconnect(format!( - "Channel {} cannot be spliced out; their {} contribution exhausts their channel balance: {}", - self.context.channel_id(), - their_funding_contribution, - their_channel_balance, - ))); + // 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( + AddSigned::checked_add_signed( + holder_balance_remaining.to_sat(), + 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 {}", + self.context.channel_id(), + if our_funding_contribution.is_positive() { "spliced in" } else { "spliced out" }, + post_splice_holder_balance, + counterparty_selected_channel_reserve, + ))?; + } + + if their_funding_contribution != SignedAmount::ZERO { + let post_splice_counterparty_balance = Amount::from_sat( + AddSigned::checked_add_signed( + counterparty_balance_remaining.to_sat(), + 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 {}", + self.context.channel_id(), + if their_funding_contribution.is_positive() { "spliced in" } else { "spliced out" }, + post_splice_counterparty_balance, + holder_selected_channel_reserve, + ))?; } Ok(()) } - /// See also [`validate_splice_init`] #[cfg(splicing)] pub(crate) fn splice_init( &mut self, msg: &msgs::SpliceInit, our_funding_contribution_satoshis: i64, @@ -11441,7 +11435,6 @@ where }) } - /// Handle splice_ack #[cfg(splicing)] pub(crate) fn splice_ack( &mut self, msg: &msgs::SpliceAck, signer_provider: &SP, entropy_source: &ES, @@ -11451,53 +11444,7 @@ where ES::Target: EntropySource, L::Target: Logger, { - let pending_splice = if let Some(ref mut pending_splice) = &mut self.pending_splice { - pending_splice - } else { - return Err(ChannelError::Ignore(format!("Channel is not in pending splice"))); - }; - - // TODO(splicing): Add check that we are the splice (quiescence) initiator - - let funding_negotiation_context = match pending_splice.funding_negotiation.take() { - Some(FundingNegotiation::AwaitingAck(context)) => context, - Some(FundingNegotiation::ConstructingTransaction(funding, constructor)) => { - pending_splice.funding_negotiation = - Some(FundingNegotiation::ConstructingTransaction(funding, constructor)); - return Err(ChannelError::WarnAndDisconnect(format!( - "Got unexpected splice_ack; splice negotiation already in progress" - ))); - }, - Some(FundingNegotiation::AwaitingSignatures(funding)) => { - pending_splice.funding_negotiation = - Some(FundingNegotiation::AwaitingSignatures(funding)); - return Err(ChannelError::WarnAndDisconnect(format!( - "Got unexpected splice_ack; splice negotiation already in progress" - ))); - }, - None => { - return Err(ChannelError::Ignore(format!( - "Got unexpected splice_ack; no splice negotiation in progress" - ))); - }, - }; - - let our_funding_contribution = funding_negotiation_context.our_funding_contribution; - debug_assert!(our_funding_contribution.abs() <= SignedAmount::MAX_MONEY); - - let their_funding_contribution = SignedAmount::from_sat(msg.funding_contribution_satoshis); - self.validate_splice_contribution(their_funding_contribution)?; - - let splice_funding = FundingScope::for_splice( - &self.funding, - &self.context, - our_funding_contribution, - their_funding_contribution, - msg.funding_pubkey, - )?; - - // TODO(splicing): Pre-check for reserve requirement - // (Note: It should also be checked later at tx_complete) + let splice_funding = self.validate_splice_ack(msg)?; log_info!( logger, @@ -11507,6 +11454,17 @@ where self.funding.get_value_satoshis(), ); + let pending_splice = + self.pending_splice.as_mut().expect("We should have returned an error earlier!"); + // TODO: Good candidate for a let else statement once MSRV >= 1.65 + let funding_negotiation_context = if let Some(FundingNegotiation::AwaitingAck(context)) = + pending_splice.funding_negotiation.take() + { + context + } else { + panic!("We should have returned an error earlier!"); + }; + let mut interactive_tx_constructor = funding_negotiation_context .into_interactive_tx_constructor( &self.context, @@ -11525,8 +11483,6 @@ where debug_assert!(self.interactive_tx_signing_session.is_none()); - let pending_splice = - self.pending_splice.as_mut().expect("pending_splice should still be set"); pending_splice.funding_negotiation = Some(FundingNegotiation::ConstructingTransaction( splice_funding, interactive_tx_constructor, @@ -11535,6 +11491,107 @@ where Ok(tx_msg_opt) } + /// Checks during handling splice_ack + #[cfg(splicing)] + fn validate_splice_ack(&self, msg: &msgs::SpliceAck) -> Result { + // TODO(splicing): Add check that we are the splice (quiescence) initiator + + let funding_negotiation_context = match &self + .pending_splice + .as_ref() + .ok_or(ChannelError::Ignore(format!("Channel is not in pending splice")))? + .funding_negotiation + { + Some(FundingNegotiation::AwaitingAck(context)) => context, + Some(FundingNegotiation::ConstructingTransaction(_, _)) + | Some(FundingNegotiation::AwaitingSignatures(_)) => { + return Err(ChannelError::WarnAndDisconnect(format!( + "Got unexpected splice_ack; splice negotiation already in progress" + ))); + }, + None => { + return Err(ChannelError::Ignore(format!( + "Got unexpected splice_ack; no splice negotiation in progress" + ))); + }, + }; + + 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))?; + + Ok(FundingScope::for_splice( + &self.funding, + &self.context, + our_funding_contribution, + their_funding_contribution, + msg.funding_pubkey, + )) + } + + #[cfg(splicing)] + fn get_holder_counterparty_balances_floor_incl_fee( + &self, funding: &FundingScope, + ) -> Result<(Amount, Amount), String> { + // We don't care about the exact value of `dust_exposure_limiting_feerate` here as + // we do not validate dust exposure below, but we want to avoid triggering a debug + // assert. + // + // TODO: clean this up here and elsewhere. + let dust_exposure_limiting_feerate = + if funding.get_channel_type().supports_anchor_zero_fee_commitments() { + None + } else { + Some(self.context.feerate_per_kw) + }; + let include_counterparty_unknown_htlcs = true; + // Make sure that that the funder of the channel can pay the transaction fees for an additional + // nondust HTLC on the channel. + let addl_nondust_htlc_count = 1; + + let local_commitment_stats = self.context.get_next_local_commitment_stats( + funding, + None, // htlc_candidate + include_counterparty_unknown_htlcs, + addl_nondust_htlc_count, + self.context.feerate_per_kw, + dust_exposure_limiting_feerate, + ); + let (holder_balance_on_local_msat, counterparty_balance_on_local_msat) = + local_commitment_stats.get_holder_counterparty_balances_incl_fee_msat(); + + let remote_commitment_stats = self.context.get_next_remote_commitment_stats( + funding, + None, // htlc_candidate + include_counterparty_unknown_htlcs, + addl_nondust_htlc_count, + self.context.feerate_per_kw, + dust_exposure_limiting_feerate, + ); + let (holder_balance_on_remote_msat, counterparty_balance_on_remote_msat) = + remote_commitment_stats.get_holder_counterparty_balances_incl_fee_msat(); + + let holder_balance_floor = Amount::from_sat( + cmp::min( + holder_balance_on_local_msat + .ok_or("holder balance exhausted on local commitment")?, + holder_balance_on_remote_msat + .ok_or("holder balance exhausted on remote commitment")?, + ) / 1000, + ); + let counterparty_balance_floor = Amount::from_sat( + cmp::min( + counterparty_balance_on_local_msat + .ok_or("counterparty balance exhausted on local commitment")?, + counterparty_balance_on_remote_msat + .ok_or("counterparty balance exhausted on remote commitment")?, + ) / 1000, + ); + + Ok((holder_balance_floor, counterparty_balance_floor)) + } + #[cfg(splicing)] pub fn splice_locked( &mut self, msg: &msgs::SpliceLocked, node_signer: &NS, chain_hash: ChainHash, diff --git a/lightning/src/sign/tx_builder.rs b/lightning/src/sign/tx_builder.rs index f9c871d59b5..cb5415cb3b3 100644 --- a/lightning/src/sign/tx_builder.rs +++ b/lightning/src/sign/tx_builder.rs @@ -35,6 +35,7 @@ impl HTLCAmountDirection { } pub(crate) struct NextCommitmentStats { + pub is_outbound_from_holder: bool, pub inbound_htlcs_count: usize, pub inbound_htlcs_value_msat: u64, pub holder_balance_before_fee_msat: Option, @@ -48,6 +49,28 @@ pub(crate) struct NextCommitmentStats { pub extra_nondust_htlc_on_counterparty_tx_dust_exposure_msat: Option, } +impl NextCommitmentStats { + pub(crate) fn get_holder_counterparty_balances_incl_fee_msat( + &self, + ) -> (Option, Option) { + if self.is_outbound_from_holder { + ( + self.holder_balance_before_fee_msat.and_then(|balance_msat| { + balance_msat.checked_sub(self.commit_tx_fee_sat * 1000) + }), + self.counterparty_balance_before_fee_msat, + ) + } else { + ( + self.holder_balance_before_fee_msat, + self.counterparty_balance_before_fee_msat.and_then(|balance_msat| { + balance_msat.checked_sub(self.commit_tx_fee_sat * 1000) + }), + ) + } + } +} + fn excess_fees_on_counterparty_tx_dust_exposure_msat( next_commitment_htlcs: &[HTLCAmountDirection], dust_buffer_feerate: u32, excess_feerate: u32, counterparty_dust_limit_satoshis: u64, dust_htlc_exposure_msat: u64, @@ -126,21 +149,19 @@ fn subtract_addl_outputs( // commitment transaction *before* checking whether the remote party's balance is enough to // cover the total anchor sum. - let local_balance_before_fee_msat = if is_outbound_from_holder { - value_to_self_after_htlcs_msat - .and_then(|balance_msat| balance_msat.checked_sub(total_anchors_sat * 1000)) - } else { - value_to_self_after_htlcs_msat - }; - - let remote_balance_before_fee_msat = if !is_outbound_from_holder { - value_to_remote_after_htlcs_msat - .and_then(|balance_msat| balance_msat.checked_sub(total_anchors_sat * 1000)) + if is_outbound_from_holder { + ( + value_to_self_after_htlcs_msat + .and_then(|balance_msat| balance_msat.checked_sub(total_anchors_sat * 1000)), + value_to_remote_after_htlcs_msat, + ) } else { - value_to_remote_after_htlcs_msat - }; - - (local_balance_before_fee_msat, remote_balance_before_fee_msat) + ( + value_to_self_after_htlcs_msat, + value_to_remote_after_htlcs_msat + .and_then(|balance_msat| balance_msat.checked_sub(total_anchors_sat * 1000)), + ) + } } fn get_dust_buffer_feerate(feerate_per_kw: u32) -> u32 { @@ -280,6 +301,7 @@ impl TxBuilder for SpecTxBuilder { }; NextCommitmentStats { + is_outbound_from_holder, inbound_htlcs_count, inbound_htlcs_value_msat, holder_balance_before_fee_msat,