diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 079201e748b..b2e84588b48 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -2248,7 +2248,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, { @@ -2291,14 +2291,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, @@ -2322,7 +2322,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. @@ -6125,9 +6125,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( @@ -6138,14 +6137,8 @@ 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(ChannelError::Warn(format!( - "Available channel balance {} is lower than needed for splicing out {}, considering fees of {}", - channel_balance, contribution_amount.unsigned_abs(), estimated_fee, - ))) - } + Ok(contribution_amount + - estimated_fee.to_signed().expect("fees should never exceed Amount::MAX_MONEY")) } else { check_v2_funding_inputs_sufficient( contribution_amount.to_sat(), @@ -6154,7 +6147,7 @@ fn check_splice_contribution_sufficient( true, funding_feerate.to_sat_per_kwu() as u32, ) - .map(Amount::from_sat) + .map(|_| contribution_amount) } } @@ -10941,68 +10934,40 @@ 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, - &contribution, - true, // is_initiator - FeeRate::from_sat_per_kwu(funding_feerate_per_kw as u64), - ) - .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, - ), - } - })?; - - // 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 - }; + let their_funding_contribution = SignedAmount::ZERO; + let counterparty_public_key = self + .funding + .channel_transaction_parameters + .counterparty_parameters + .as_ref() + .expect("counterparty_parameters should be set") + .pubkeys + .funding_pubkey; + let splice_funding = self + .validate_splice_contributions( + our_funding_contribution, + their_funding_contribution, + counterparty_public_key, + Some(( + &contribution, + true, // is_initiator + FeeRate::from_sat_per_kwu(funding_feerate_per_kw as u64), + )), + ) + .map_err(|err| APIError::APIMisuseError { err })?; + let adjusted_funding_contribution = SignedAmount::from_sat( + (splice_funding.get_value_to_self_msat() as i64 + - self.funding.get_value_to_self_msat() as i64) + / 1000, + ); + // Assert the adjusted contribution remains the same for splice-ins, and is offset for splice-outs, + // see `validate_splice_contributions` + debug_assert_eq!( + adjusted_funding_contribution == our_funding_contribution, + our_funding_contribution.is_positive() + ); for FundingTxInput { utxo, prevtx, .. } in contribution.inputs().iter() { const MESSAGE_TEMPLATE: msgs::TxAddInput = msgs::TxAddInput { @@ -11089,90 +11054,148 @@ where ))); } - 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 { + 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 in; our {} contribution exceeds the total bitcoin supply", + "Channel {} cannot be spliced; they are the initiator, and their contribution is zero", self.context.channel_id(), - our_funding_contribution, ))); } - if our_funding_contribution < -SignedAmount::MAX_MONEY { - return Err(ChannelError::WarnAndDisconnect(format!( - "Channel {} cannot be spliced out; our {} contribution exhausts the total bitcoin supply", - 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 - - let splice_funding = FundingScope::for_splice( - &self.funding, - &self.context, + self.validate_splice_contributions( 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) + None, + ) + .map_err(|e| ChannelError::WarnAndDisconnect(e)) } #[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, + counterparty_funding_pubkey: PublicKey, + contribution_is_initiator_feerate: Option<(&SpliceContribution, bool, FeeRate)>, + ) -> Result { + // TODO(splicing): Remove once we can contribute as splice acceptor + debug_assert!( + contribution_is_initiator_feerate.map_or(true, |(_, is_initiator, _)| is_initiator) + ); + + 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( + let adjusted_funding_contribution = if let Some((contribution, is_initiator, feerate)) = + contribution_is_initiator_feerate + { + // 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. + check_splice_contribution_sufficient(contribution, is_initiator, feerate).map_err( + |e| { + format!( + "Channel {} cannot be {}; {}", + self.context.channel_id(), + if our_funding_contribution.is_positive() { + "spliced in" + } else { + "spliced out" + }, + e + ) + }, + )? + } else { + our_funding_contribution + }; + + // Sanity check all funding contributions here; we need to do this before building a `FundingScope` + + let our_channel_balance = Amount::from_sat(self.funding.get_value_to_self_msat() / 1000); + AddSigned::checked_add_signed( + our_channel_balance.to_sat(), + adjusted_funding_contribution.to_sat(), + ) + .ok_or(format!( + "Channel {} cannot be spliced out; our {} contribution exhausts our channel balance: {}", + self.context.channel_id(), + adjusted_funding_contribution, + our_channel_balance, + ))?; + + let their_channel_balance = Amount::from_sat( + self.funding.get_value_satoshis() - self.funding.get_value_to_self_msat() / 1000, + ); + AddSigned::checked_add_signed( their_channel_balance.to_sat(), their_funding_contribution.to_sat(), - ); - - if post_channel_balance.is_none() { - return Err(ChannelError::WarnAndDisconnect(format!( + ) + .ok_or(format!( "Channel {} cannot be spliced out; their {} contribution exhausts their channel balance: {}", self.context.channel_id(), their_funding_contribution, their_channel_balance, - ))); + ))?; + + let splice_funding = FundingScope::for_splice( + &self.funding, + &self.context, + adjusted_funding_contribution, + their_funding_contribution, + counterparty_funding_pubkey, + ); + + let (holder_balance_remaining, counterparty_balance_remaining) = + self.get_holder_counterparty_balances_floor_incl_fee(&splice_funding).map_err(|e| { + format!("Channel {} cannot be spliced; {}", self.context.channel_id(), e) + })?; + + // We allow parties to draw from their previous reserve, as long as they satisfy their v2 reserve + + if our_funding_contribution != SignedAmount::ZERO { + let counterparty_selected_channel_reserve_satoshis = splice_funding + .counterparty_selected_channel_reserve_satoshis + .expect("counterparty_selected_channel_reserve_satoshis should be set"); + holder_balance_remaining + .checked_sub(Amount::from_sat(counterparty_selected_channel_reserve_satoshis)) + .ok_or(format!( + "Channel {} cannot be {}; We cannot afford the new counterparty mandated reserve {} vs {}", + self.context.channel_id(), + if our_funding_contribution.is_positive() { "spliced in" } else { "spliced out" }, + holder_balance_remaining, counterparty_selected_channel_reserve_satoshis, + ))?; + } + + if their_funding_contribution != SignedAmount::ZERO { + let holder_selected_channel_reserve_satoshis = + splice_funding.holder_selected_channel_reserve_satoshis; + counterparty_balance_remaining + .checked_sub(Amount::from_sat(holder_selected_channel_reserve_satoshis)) + .ok_or(format!( + "Channel {} cannot be {}; They cannot afford the new holder mandated reserve {} vs {}", + self.context.channel_id(), + if their_funding_contribution.is_positive() { "spliced in" } else { "spliced out" }, + counterparty_balance_remaining, holder_selected_channel_reserve_satoshis, + ))?; } - Ok(()) + Ok(splice_funding) } - /// See also [`validate_splice_init`] #[cfg(splicing)] pub(crate) fn splice_init( &mut self, msg: &msgs::SpliceInit, our_funding_contribution_satoshis: i64, @@ -11240,7 +11263,6 @@ where }) } - /// Handle splice_ack #[cfg(splicing)] pub(crate) fn splice_ack( &mut self, msg: &msgs::SpliceAck, signer_provider: &SP, entropy_source: &ES, @@ -11250,53 +11272,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, @@ -11306,6 +11282,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, @@ -11324,8 +11311,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, @@ -11334,6 +11319,105 @@ 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, + msg.funding_pubkey, + None, + ) + .map_err(|e| ChannelError::WarnAndDisconnect(e)) + } + + #[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) + }; + // This should have no effect because no HTLC updates should be pending + let include_remote_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_remote_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_remote_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,