diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 56d38d5545c..c90cfaf2566 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -10748,7 +10748,9 @@ where // 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 + if their_funding_contribution_satoshis.is_negative() { + self.validate_their_negative_funding_contribution(&splice_funding)?; + } Ok(splice_funding) } @@ -10821,7 +10823,7 @@ where }) } - /// Handle splice_ack + /// See also [`validate_splice_ack`] #[cfg(splicing)] pub(crate) fn splice_ack( &mut self, msg: &msgs::SpliceAck, signer_provider: &SP, entropy_source: &ES, @@ -10831,26 +10833,66 @@ where ES::Target: EntropySource, L::Target: Logger, { - let pending_splice = if let Some(ref mut pending_splice) = &mut self.pending_splice { - pending_splice + let splice_funding = self.validate_splice_ack(msg)?; + + log_info!( + logger, + "Starting splice funding negotiation for channel {} after receiving splice_ack; new channel value: {} sats (old: {} sats)", + self.context.channel_id, + splice_funding.get_value_satoshis(), + 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 { - return Err(ChannelError::Ignore(format!("Channel is not in pending splice"))); + panic!("We should have returned an error earlier!"); }; + let mut interactive_tx_constructor = funding_negotiation_context + .into_interactive_tx_constructor( + &self.context, + &splice_funding, + signer_provider, + entropy_source, + holder_node_id.clone(), + ) + .map_err(|err| { + ChannelError::WarnAndDisconnect(format!( + "Failed to start interactive transaction construction, {:?}", + err + )) + })?; + let tx_msg_opt = interactive_tx_constructor.take_initiator_first_message(); + + debug_assert!(self.interactive_tx_signing_session.is_none()); + pending_splice.funding_negotiation = Some(FundingNegotiation::ConstructingTransaction( + splice_funding, + interactive_tx_constructor, + )); + + 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 pending_splice.funding_negotiation.take() { + 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(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)); + Some(FundingNegotiation::ConstructingTransaction(_, _)) + | Some(FundingNegotiation::AwaitingSignatures(_)) => { return Err(ChannelError::WarnAndDisconnect(format!( "Got unexpected splice_ack; splice negotiation already in progress" ))); @@ -10874,40 +10916,50 @@ where msg.funding_pubkey, )?; - // TODO(splicing): Pre-check for reserve requirement - // (Note: It should also be checked later at tx_complete) + if their_funding_contribution_satoshis.is_negative() { + self.validate_their_negative_funding_contribution(&splice_funding)?; + } - log_info!( - logger, - "Starting splice funding negotiation for channel {} after receiving splice_ack; new channel value: {} sats (old: {} sats)", - self.context.channel_id, - splice_funding.get_value_satoshis(), - self.funding.get_value_satoshis(), - ); + Ok(splice_funding) + } - let mut interactive_tx_constructor = funding_negotiation_context - .into_interactive_tx_constructor( - &self.context, - &splice_funding, - signer_provider, - entropy_source, - holder_node_id.clone(), - ) - .map_err(|err| { - ChannelError::WarnAndDisconnect(format!( - "Failed to start interactive transaction construction, {:?}", - err - )) - })?; - let tx_msg_opt = interactive_tx_constructor.take_initiator_first_message(); + /// Used to validate a negative `funding_contribution_satoshis` in `splice_init` and `splice_ack` messages. + #[cfg(splicing)] + fn validate_their_negative_funding_contribution( + &self, spliced_funding: &FundingScope, + ) -> Result<(), ChannelError> { + // Calculate the remote's new balance + // + // We only validate the remote's balance on the next *remote* commitment transaction. + // + // We don't care for a small / no to_remote output on our next *local* commitment transaction as the purpose of the + // channel reserve is to ensure we can punish *them* if they misbehave. See `validate_update_add_htlc` for another + // place where we apply the same reasoning. + // + // This comment is only relevant if they are the funder of the channel because the remote balance will be the same + // on both local and remote commitments if they are the fundee. + let spliced_commitment_stats = + self.context.build_commitment_stats(&spliced_funding, false, true, None, None); + let spliced_remote_balance_msat = if spliced_funding.is_outbound() { + spliced_commitment_stats.remote_balance_before_fee_msat + } else { + spliced_commitment_stats + .remote_balance_before_fee_msat + .saturating_sub(spliced_commitment_stats.commit_tx_fee_sat * 1000) + }; - debug_assert!(self.interactive_tx_signing_session.is_none()); - pending_splice.funding_negotiation = Some(FundingNegotiation::ConstructingTransaction( - splice_funding, - interactive_tx_constructor, - )); + // Check if the remote's new balance is under the specified reserve + if spliced_remote_balance_msat + < spliced_funding.holder_selected_channel_reserve_satoshis * 1000 + { + return Err(ChannelError::Warn(format!( + "Remote balance below reserve mandated by holder: {} vs {}", + spliced_remote_balance_msat, + spliced_funding.holder_selected_channel_reserve_satoshis * 1000, + ))); + } - Ok(tx_msg_opt) + Ok(()) } #[cfg(splicing)]