From dc3ab7b3823bccf0337aaadd8802a2bb88f6f526 Mon Sep 17 00:00:00 2001 From: optout <13562139+optout21@users.noreply.github.com> Date: Thu, 6 Mar 2025 13:53:12 +0100 Subject: [PATCH] Implement initial splice negotiation Handle the initial splice negotiation, without continuing to transaction negotiation: - `splice_channel()` for initiating splicing - handling of `splice_init` and `splice_ack` messages, with checks - `splice_ack` always fails (use case still imcomplete) - A test to go through the use case (`test_v1_splice_in`) --- lightning/src/ln/chan_utils.rs | 21 ++ lightning/src/ln/channel.rs | 323 +++++++++++++++++++++- lightning/src/ln/channelmanager.rs | 185 ++++++++++++- lightning/src/ln/dual_funding_tests.rs | 2 +- lightning/src/ln/functional_test_utils.rs | 21 +- lightning/src/ln/mod.rs | 3 + lightning/src/ln/msgs.rs | 6 +- lightning/src/ln/splicing_tests.rs | 159 +++++++++++ 8 files changed, 700 insertions(+), 20 deletions(-) create mode 100644 lightning/src/ln/splicing_tests.rs diff --git a/lightning/src/ln/chan_utils.rs b/lightning/src/ln/chan_utils.rs index da25e4e9aba..1b3cb017501 100644 --- a/lightning/src/ln/chan_utils.rs +++ b/lightning/src/ln/chan_utils.rs @@ -81,6 +81,27 @@ pub const HTLC_TIMEOUT_INPUT_ANCHOR_WITNESS_WEIGHT: u64 = 288; /// outputs. pub const HTLC_SUCCESS_INPUT_ANCHOR_WITNESS_WEIGHT: u64 = 327; +/// The size of the 2-of-2 multisig script +const MULTISIG_SCRIPT_SIZE: u64 = + 1 + // OP_2 + 1 + // data len + 33 + // pubkey1 + 1 + // data len + 33 + // pubkey2 + 1 + // OP_2 + 1; // OP_CHECKMULTISIG +/// The weight of a funding transaction input (2-of-2 P2WSH) +/// See https://github.com/lightning/bolts/blob/master/03-transactions.md#expected-weight-of-the-commitment-transaction +pub const FUNDING_TRANSACTION_WITNESS_WEIGHT: u64 = + 1 + // number_of_witness_elements + 1 + // nil_len + 1 + // sig len + 73 + // sig1 + 1 + // sig len + 73 + // sig2 + 1 + // witness_script_length + MULTISIG_SCRIPT_SIZE; + /// Gets the weight for an HTLC-Success transaction. #[inline] pub fn htlc_success_tx_weight(channel_type_features: &ChannelTypeFeatures) -> u64 { diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 6fa88e4ed59..106ad89ad25 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -11,7 +11,6 @@ use bitcoin::amount::Amount; use bitcoin::constants::ChainHash; use bitcoin::script::{Script, ScriptBuf, Builder, WScriptHash}; use bitcoin::transaction::{Transaction, TxIn}; -use bitcoin::sighash; use bitcoin::sighash::EcdsaSighashType; use bitcoin::consensus::encode; use bitcoin::absolute::LockTime; @@ -25,7 +24,7 @@ use bitcoin::hash_types::{Txid, BlockHash}; use bitcoin::secp256k1::constants::PUBLIC_KEY_SIZE; use bitcoin::secp256k1::{PublicKey,SecretKey}; use bitcoin::secp256k1::{Secp256k1,ecdsa::Signature}; -use bitcoin::secp256k1; +use bitcoin::{secp256k1, sighash}; use crate::ln::types::ChannelId; use crate::types::payment::{PaymentPreimage, PaymentHash}; @@ -48,6 +47,8 @@ use crate::ln::chan_utils::{ get_commitment_transaction_number_obscure_factor, ClosingTransaction, commit_tx_fee_sat, }; +#[cfg(splicing)] +use crate::ln::chan_utils::FUNDING_TRANSACTION_WITNESS_WEIGHT; use crate::ln::chan_utils; use crate::ln::onion_utils::HTLCFailReason; use crate::chain::BestBlock; @@ -1517,6 +1518,8 @@ impl Channel where interactive_tx_signing_session: chan.interactive_tx_signing_session, holder_commitment_point, is_v2_established: true, + #[cfg(splicing)] + pending_splice: None, }; let res = funded_channel.commitment_signed_initial_v2(msg, best_block, signer_provider, logger) .map(|monitor| (Some(monitor), None)) @@ -1719,6 +1722,12 @@ impl FundingScope { } } +/// Info about a pending splice, used in the pre-splice channel +#[cfg(splicing)] +struct PendingSplice { + pub our_funding_contribution: i64, +} + /// Contains everything about the channel including state, and various flags. pub(super) struct ChannelContext where SP::Target: SignerProvider { config: LegacyChannelConfig, @@ -4797,6 +4806,60 @@ fn estimate_v2_funding_transaction_fee( fee_for_weight(funding_feerate_sat_per_1000_weight, weight) } +/// Verify that the provided inputs to the funding transaction are enough +/// to cover the intended contribution amount *plus* the proportional fees. +/// Fees are computed using `estimate_v2_funding_transaction_fee`, and contain +/// the fees of the inputs, fees of the inputs weight, and for the initiator, +/// the fees of the common fields as well as the output and extra input weights. +/// Returns estimated (partial) fees as additional information +#[cfg(splicing)] +fn check_v2_funding_inputs_sufficient( + contribution_amount: i64, funding_inputs: &[(TxIn, Transaction, Weight)], is_initiator: bool, + is_splice: bool, funding_feerate_sat_per_1000_weight: u32, +) -> Result { + let mut total_input_witness_weight = Weight::from_wu(funding_inputs.iter().map(|(_, _, w)| w.to_wu()).sum()); + let mut funding_inputs_len = funding_inputs.len(); + if is_initiator && is_splice { + // consider the weight of the input and witness needed for spending the old funding transaction + funding_inputs_len += 1; + total_input_witness_weight += Weight::from_wu(FUNDING_TRANSACTION_WITNESS_WEIGHT); + } + let estimated_fee = estimate_v2_funding_transaction_fee(is_initiator, funding_inputs_len, total_input_witness_weight, funding_feerate_sat_per_1000_weight); + + let mut total_input_sats = 0u64; + for (idx, input) in funding_inputs.iter().enumerate() { + if let Some(output) = input.1.output.get(input.0.previous_output.vout as usize) { + total_input_sats = total_input_sats.saturating_add(output.value.to_sat()); + } else { + return Err(ChannelError::Warn(format!( + "Transaction with txid {} does not have an output with vout of {} corresponding to TxIn at funding_inputs[{}]", + input.1.compute_txid(), input.0.previous_output.vout, idx + ))); + } + } + + // If the inputs are enough to cover intended contribution amount, with fees even when + // there is a change output, we are fine. + // If the inputs are less, but enough to cover intended contribution amount, with + // (lower) fees with no change, we are also fine (change will not be generated). + // So it's enough to check considering the lower, no-change fees. + // + // Note: dust limit is not relevant in this check. + // + // 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 { + Err(ChannelError::Warn(format!( + "Total input amount {} is lower than needed for contribution {}, considering fees of {}. Need more inputs.", + total_input_sats, contribution_amount, estimated_fee, + ))) + } else { + Ok(estimated_fee) + } +} + /// Context for dual-funded channels. pub(super) struct DualFundingChannelContext { /// The amount in satoshis we will be contributing to the channel. @@ -4826,6 +4889,9 @@ pub(super) struct FundedChannel where SP::Target: SignerProvider { /// Indicates whether this funded channel had been established with V2 channel /// establishment. is_v2_established: bool, + /// Info about an in-progress, pending splice (if any), on the pre-splice channel + #[cfg(splicing)] + pending_splice: Option, } #[cfg(any(test, fuzzing))] @@ -8360,6 +8426,141 @@ impl FundedChannel where } } + /// Initiate splicing. + /// - `our_funding_inputs`: the inputs we contribute to the new funding transaction. + /// Includes the witness weight for this input (e.g. P2WPKH_WITNESS_WEIGHT=109 for typical P2WPKH inputs). + #[cfg(splicing)] + pub fn splice_channel(&mut self, our_funding_contribution_satoshis: i64, + our_funding_inputs: &Vec<(TxIn, Transaction, Weight)>, + funding_feerate_per_kw: u32, locktime: u32, + ) -> Result { + // Check if a splice has been initiated already. + // Note: only a single outstanding splice is supported (per spec) + if let Some(splice_info) = &self.pending_splice { + return Err(APIError::APIMisuseError { err: format!( + "Channel {} cannot be spliced, as it has already a splice pending (contribution {})", + self.context.channel_id(), splice_info.our_funding_contribution + )}); + } + + if !self.context.is_live() { + return Err(APIError::APIMisuseError { err: format!( + "Channel {} cannot be spliced, as channel is not live", + self.context.channel_id() + )}); + } + + // TODO(splicing): check for quiescence + + if our_funding_contribution_satoshis < 0 { + return Err(APIError::APIMisuseError { err: format!( + "TODO(splicing): Splice-out not supported, only splice in; channel ID {}, contribution {}", + self.context.channel_id(), our_funding_contribution_satoshis, + )}); + } + + // TODO(splicing): Once splice-out is supported, check that channel balance does not go below 0 + // (or below channel reserve) + + // 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) + + // Check that inputs are sufficient to cover our contribution. + let _fee = check_v2_funding_inputs_sufficient(our_funding_contribution_satoshis, &our_funding_inputs, true, true, funding_feerate_per_kw) + .map_err(|err| APIError::APIMisuseError { err: format!( + "Insufficient inputs for splicing; channel ID {}, err {}", + self.context.channel_id(), err, + )})?; + + self.pending_splice = Some(PendingSplice { + our_funding_contribution: our_funding_contribution_satoshis, + }); + + let msg = self.get_splice_init(our_funding_contribution_satoshis, funding_feerate_per_kw, locktime); + Ok(msg) + } + + /// Get the splice message that can be sent during splice initiation. + #[cfg(splicing)] + fn get_splice_init(&self, our_funding_contribution_satoshis: i64, + funding_feerate_per_kw: u32, locktime: u32, + ) -> msgs::SpliceInit { + // TODO(splicing): The exisiting pubkey is reused, but a new one should be generated. See #3542. + // Note that channel_keys_id is supposed NOT to change + let funding_pubkey = self.funding.get_holder_pubkeys().funding_pubkey.clone(); + msgs::SpliceInit { + channel_id: self.context.channel_id, + funding_contribution_satoshis: our_funding_contribution_satoshis, + funding_feerate_per_kw, + locktime, + funding_pubkey, + require_confirmed_inputs: None, + } + } + + /// Handle splice_init + #[cfg(splicing)] + pub fn splice_init(&mut self, msg: &msgs::SpliceInit) -> Result { + let their_funding_contribution_satoshis = msg.funding_contribution_satoshis; + // TODO(splicing): Currently not possible to contribute on the splicing-acceptor side + let our_funding_contribution_satoshis = 0i64; + + // Check if a splice has been initiated already. + if let Some(splice_info) = &self.pending_splice { + return Err(ChannelError::Warn(format!( + "Channel has already a splice pending, contribution {}", splice_info.our_funding_contribution, + ))); + } + + // - If it has received shutdown: + // MUST send a warning and close the connection or send an error + // and fail the channel. + if !self.context.is_live() { + return Err(ChannelError::Warn(format!("Splicing requested on a channel that is not live"))); + } + + if their_funding_contribution_satoshis.saturating_add(our_funding_contribution_satoshis) < 0 { + return Err(ChannelError::Warn(format!( + "Splice-out not supported, only splice in, contribution is {} ({} + {})", + their_funding_contribution_satoshis + our_funding_contribution_satoshis, + their_funding_contribution_satoshis, our_funding_contribution_satoshis, + ))); + } + + // 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): Once splice acceptor can contribute, add reserve pre-check, similar to the one in `splice_ack`. + + // TODO(splicing): Store msg.funding_pubkey + // TODO(splicing): Apply start of splice (splice_start) + + // TODO(splicing): The exisiting pubkey is reused, but a new one should be generated. See #3542. + // Note that channel_keys_id is supposed NOT to change + let splice_ack_msg = msgs::SpliceAck { + channel_id: self.context.channel_id, + funding_contribution_satoshis: our_funding_contribution_satoshis, + funding_pubkey: self.funding.get_holder_pubkeys().funding_pubkey, + require_confirmed_inputs: None, + }; + // TODO(splicing): start interactive funding negotiation + Ok(splice_ack_msg) + } + + /// Handle splice_ack + #[cfg(splicing)] + pub fn splice_ack(&mut self, _msg: &msgs::SpliceAck) -> Result<(), ChannelError> { + // check if splice is pending + if self.pending_splice.is_none() { + return Err(ChannelError::Warn(format!("Channel is not in pending splice"))); + }; + + // TODO(splicing): Pre-check for reserve requirement + // (Note: It should also be checked later at tx_complete) + Ok(()) + } // Send stuff to our remote peers: @@ -9282,6 +9483,8 @@ impl OutboundV1Channel where SP::Target: SignerProvider { interactive_tx_signing_session: None, is_v2_established: false, holder_commitment_point, + #[cfg(splicing)] + pending_splice: None, }; let need_channel_ready = channel.check_get_channel_ready(0, logger).is_some() @@ -9549,6 +9752,8 @@ impl InboundV1Channel where SP::Target: SignerProvider { interactive_tx_signing_session: None, is_v2_established: false, holder_commitment_point, + #[cfg(splicing)] + pending_splice: None, }; let need_channel_ready = channel.check_get_channel_ready(0, logger).is_some() || channel.context.signer_pending_channel_ready; @@ -10909,6 +11114,8 @@ impl<'a, 'b, 'c, ES: Deref, SP: Deref> ReadableArgs<(&'a ES, &'b SP, &'c Channel interactive_tx_signing_session: None, is_v2_established, holder_commitment_point, + #[cfg(splicing)] + pending_splice: None, }) } } @@ -10920,8 +11127,12 @@ mod tests { use bitcoin::constants::ChainHash; use bitcoin::script::{ScriptBuf, Builder}; use bitcoin::transaction::{Transaction, TxOut, Version}; + #[cfg(splicing)] + use bitcoin::transaction::TxIn; use bitcoin::opcodes; use bitcoin::network::Network; + #[cfg(splicing)] + use bitcoin::Weight; use crate::ln::onion_utils::INVALID_ONION_BLINDING; use crate::types::payment::{PaymentHash, PaymentPreimage}; use crate::ln::channel_keys::{RevocationKey, RevocationBasepoint}; @@ -12726,4 +12937,112 @@ mod tests { 320 ); } + + #[cfg(splicing)] + fn funding_input_sats(input_value_sats: u64) -> (TxIn, Transaction, Weight) { + use crate::sign::P2WPKH_WITNESS_WEIGHT; + + let input_1_prev_out = TxOut { value: Amount::from_sat(input_value_sats), script_pubkey: ScriptBuf::default() }; + let input_1_prev_tx = Transaction { + input: vec![], output: vec![input_1_prev_out], + version: Version::TWO, lock_time: bitcoin::absolute::LockTime::ZERO, + }; + let input_1_txin = TxIn { + previous_output: bitcoin::OutPoint { txid: input_1_prev_tx.compute_txid(), vout: 0 }, + ..Default::default() + }; + (input_1_txin, input_1_prev_tx, Weight::from_wu(P2WPKH_WITNESS_WEIGHT)) + } + + #[cfg(splicing)] + #[test] + fn test_check_v2_funding_inputs_sufficient() { + use crate::ln::channel::check_v2_funding_inputs_sufficient; + + // positive case, inputs well over intended contribution + assert_eq!( + check_v2_funding_inputs_sufficient( + 220_000, + &[ + funding_input_sats(200_000), + funding_input_sats(100_000), + ], + true, + true, + 2000, + ).unwrap(), + 2268, + ); + + // negative case, inputs clearly insufficient + { + let res = check_v2_funding_inputs_sufficient( + 220_000, + &[ + funding_input_sats(100_000), + ], + true, + true, + 2000, + ); + assert_eq!( + format!("{:?}", res.err().unwrap()), + "Warn: Total input amount 100000 is lower than needed for contribution 220000, considering fees of 1730. Need more inputs.", + ); + } + + // barely covers + { + let expected_fee: u64 = 2268; + assert_eq!( + check_v2_funding_inputs_sufficient( + (300_000 - expected_fee - 20) as i64, + &[ + funding_input_sats(200_000), + funding_input_sats(100_000), + ], + true, + true, + 2000, + ).unwrap(), + expected_fee, + ); + } + + // higher fee rate, does not cover + { + let res = check_v2_funding_inputs_sufficient( + 298032, + &[ + funding_input_sats(200_000), + funding_input_sats(100_000), + ], + true, + true, + 2200, + ); + assert_eq!( + format!("{:?}", res.err().unwrap()), + "Warn: Total input amount 300000 is lower than needed for contribution 298032, considering fees of 2495. Need more inputs.", + ); + } + + // barely covers, less fees (no extra weight, no init) + { + let expected_fee: u64 = 1076; + assert_eq!( + check_v2_funding_inputs_sufficient( + (300_000 - expected_fee - 20) as i64, + &[ + funding_input_sats(200_000), + funding_input_sats(100_000), + ], + false, + false, + 2000, + ).unwrap(), + expected_fee, + ); + } + } } diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 99374c7bb54..5fd2a2c30cf 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -31,6 +31,8 @@ use bitcoin::hash_types::{BlockHash, Txid}; use bitcoin::secp256k1::{SecretKey,PublicKey}; use bitcoin::secp256k1::Secp256k1; use bitcoin::{secp256k1, Sequence}; +#[cfg(splicing)] +use bitcoin::{TxIn, Weight}; use crate::events::FundingInfo; use crate::blinded_path::message::{AsyncPaymentsContext, MessageContext, OffersContext}; @@ -4294,6 +4296,86 @@ where } } + /// Initiate a splice, to change the channel capacity of an existing funded channel. + /// After completion of splicing, the funding transaction will be replaced by a new one, spending the old funding transaction, + /// with optional extra inputs (splice-in) and/or extra outputs (splice-out or change). + /// TODO(splicing): Implementation is currently incomplete. + /// + /// Note: Currently only splice-in is supported (increase in channel capacity), splice-out is not. + /// + /// - `our_funding_contribution_satoshis`: the amount contributed by us to the channel. This will increase our channel balance. + /// - `our_funding_inputs`: the funding inputs provided by us. If our contribution is positive, our funding inputs must cover at least that amount. + /// Includes the witness weight for this input (e.g. P2WPKH_WITNESS_WEIGHT=109 for typical P2WPKH inputs). + /// - `locktime`: Optional locktime for the new funding transaction. If None, set to the current block height. + #[cfg(splicing)] + pub fn splice_channel( + &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, our_funding_contribution_satoshis: i64, + our_funding_inputs: Vec<(TxIn, Transaction, Weight)>, + funding_feerate_per_kw: u32, locktime: Option, + ) -> Result<(), APIError> { + let mut res = Ok(()); + PersistenceNotifierGuard::optionally_notify(self, || { + let result = self.internal_splice_channel( + channel_id, counterparty_node_id, our_funding_contribution_satoshis, &our_funding_inputs, funding_feerate_per_kw, locktime + ); + res = result; + match res { + Ok(_) => NotifyOption::SkipPersistHandleEvents, + Err(_) => NotifyOption::SkipPersistNoEvents, + } + }); + res + } + + /// See [`splice_channel`] + #[cfg(splicing)] + fn internal_splice_channel( + &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, our_funding_contribution_satoshis: i64, + our_funding_inputs: &Vec<(TxIn, Transaction, Weight)>, + funding_feerate_per_kw: u32, locktime: Option, + ) -> Result<(), APIError> { + let per_peer_state = self.per_peer_state.read().unwrap(); + + let peer_state_mutex = match per_peer_state.get(counterparty_node_id) + .ok_or_else(|| APIError::ChannelUnavailable { err: format!("Can't find a peer matching the passed counterparty node_id {}", counterparty_node_id) }) { + Ok(p) => p, + Err(e) => return Err(e), + }; + + let mut peer_state_lock = peer_state_mutex.lock().unwrap(); + let peer_state = &mut *peer_state_lock; + + // Look for the channel + match peer_state.channel_by_id.entry(*channel_id) { + hash_map::Entry::Occupied(mut chan_phase_entry) => { + let locktime = locktime.unwrap_or_else(|| self.current_best_block().height); + if let Some(chan) = chan_phase_entry.get_mut().as_funded_mut() { + let msg = chan.splice_channel(our_funding_contribution_satoshis, our_funding_inputs, funding_feerate_per_kw, locktime)?; + peer_state.pending_msg_events.push(MessageSendEvent::SendSpliceInit { + node_id: *counterparty_node_id, + msg, + }); + Ok(()) + } else { + Err(APIError::ChannelUnavailable { + err: format!( + "Channel with id {} is not funded, cannot splice it", + channel_id + ) + }) + } + }, + hash_map::Entry::Vacant(_) => { + Err(APIError::ChannelUnavailable { + err: format!( + "Channel with id {} not found for the passed counterparty node_id {}", + channel_id, counterparty_node_id, + ) + }) + }, + } + } + fn can_forward_htlc_to_outgoing_channel( &self, chan: &mut FundedChannel, msg: &msgs::UpdateAddHTLC, next_packet: &NextPacketDetails ) -> Result<(), (&'static str, u16)> { @@ -9471,6 +9553,81 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ Ok(NotifyOption::SkipPersistHandleEvents) } + /// Handle incoming splice request, transition channel to splice-pending (unless some check fails). + #[cfg(splicing)] + fn internal_splice_init(&self, counterparty_node_id: &PublicKey, msg: &msgs::SpliceInit) -> Result<(), MsgHandleErrInternal> { + let per_peer_state = self.per_peer_state.read().unwrap(); + let peer_state_mutex = per_peer_state.get(counterparty_node_id) + .ok_or_else(|| { + debug_assert!(false); + MsgHandleErrInternal::send_err_msg_no_close(format!("Can't find a peer matching the passed counterparty node_id {}", counterparty_node_id), msg.channel_id) + })?; + let mut peer_state_lock = peer_state_mutex.lock().unwrap(); + let peer_state = &mut *peer_state_lock; + + // Look for the channel + match peer_state.channel_by_id.entry(msg.channel_id) { + hash_map::Entry::Vacant(_) => return Err(MsgHandleErrInternal::send_err_msg_no_close(format!( + "Got a message for a channel from the wrong node! No such channel for the passed counterparty_node_id {}, channel_id {}", + counterparty_node_id, msg.channel_id, + ), msg.channel_id)), + hash_map::Entry::Occupied(mut chan_entry) => { + if let Some(chan) = chan_entry.get_mut().as_funded_mut() { + let splice_ack_msg = try_channel_entry!(self, peer_state, chan.splice_init(msg), chan_entry); + peer_state.pending_msg_events.push(MessageSendEvent::SendSpliceAck { + node_id: *counterparty_node_id, + msg: splice_ack_msg, + }); + } else { + return Err(MsgHandleErrInternal::send_err_msg_no_close("Channel is not funded, cannot be spliced".to_owned(), msg.channel_id)); + } + }, + }; + + // TODO(splicing): + // Change channel, change phase (remove and add) + // Create new post-splice channel + // etc. + + Ok(()) + } + + /// Handle incoming splice request ack, transition channel to splice-pending (unless some check fails). + #[cfg(splicing)] + fn internal_splice_ack(&self, counterparty_node_id: &PublicKey, msg: &msgs::SpliceAck) -> Result<(), MsgHandleErrInternal> { + let per_peer_state = self.per_peer_state.read().unwrap(); + let peer_state_mutex = per_peer_state.get(counterparty_node_id) + .ok_or_else(|| { + debug_assert!(false); + MsgHandleErrInternal::send_err_msg_no_close(format!("Can't find a peer matching the passed counterparty node_id {}", counterparty_node_id), msg.channel_id) + })?; + let mut peer_state_lock = peer_state_mutex.lock().unwrap(); + let peer_state = &mut *peer_state_lock; + + // Look for the channel + match peer_state.channel_by_id.entry(msg.channel_id) { + hash_map::Entry::Vacant(_) => return Err(MsgHandleErrInternal::send_err_msg_no_close(format!( + "Got a message for a channel from the wrong node! No such channel for the passed counterparty_node_id {}", + counterparty_node_id + ), msg.channel_id)), + hash_map::Entry::Occupied(mut chan_entry) => { + if let Some(chan) = chan_entry.get_mut().as_funded_mut() { + try_channel_entry!(self, peer_state, chan.splice_ack(msg), chan_entry); + } else { + return Err(MsgHandleErrInternal::send_err_msg_no_close("Channel is not funded, cannot splice".to_owned(), msg.channel_id)); + } + }, + }; + + // TODO(splicing): + // Change channel, change phase (remove and add) + // Create new post-splice channel + // Start splice funding transaction negotiation + // etc. + + Err(MsgHandleErrInternal::send_err_msg_no_close("TODO(splicing): Splicing is not implemented (splice_ack)".to_owned(), msg.channel_id)) + } + /// Process pending events from the [`chain::Watch`], returning whether any events were processed. fn process_pending_monitor_events(&self) -> bool { debug_assert!(self.total_consistency_lock.try_write().is_err()); // Caller holds read lock @@ -11960,23 +12117,37 @@ where #[cfg(splicing)] fn handle_splice_init(&self, counterparty_node_id: PublicKey, msg: &msgs::SpliceInit) { - let _: Result<(), _> = handle_error!(self, Err(MsgHandleErrInternal::send_err_msg_no_close( - "Splicing not supported".to_owned(), - msg.channel_id.clone())), counterparty_node_id); + let _persistence_guard = PersistenceNotifierGuard::optionally_notify(self, || { + let res = self.internal_splice_init(&counterparty_node_id, msg); + let persist = match &res { + Err(e) if e.closes_channel() => NotifyOption::DoPersist, + Err(_) => NotifyOption::SkipPersistHandleEvents, + Ok(()) => NotifyOption::SkipPersistHandleEvents, + }; + let _ = handle_error!(self, res, counterparty_node_id); + persist + }); } #[cfg(splicing)] fn handle_splice_ack(&self, counterparty_node_id: PublicKey, msg: &msgs::SpliceAck) { - let _: Result<(), _> = handle_error!(self, Err(MsgHandleErrInternal::send_err_msg_no_close( - "Splicing not supported (splice_ack)".to_owned(), - msg.channel_id.clone())), counterparty_node_id); + let _persistence_guard = PersistenceNotifierGuard::optionally_notify(self, || { + let res = self.internal_splice_ack(&counterparty_node_id, msg); + let persist = match &res { + Err(e) if e.closes_channel() => NotifyOption::DoPersist, + Err(_) => NotifyOption::SkipPersistHandleEvents, + Ok(()) => NotifyOption::SkipPersistHandleEvents, + }; + let _ = handle_error!(self, res, counterparty_node_id); + persist + }); } #[cfg(splicing)] fn handle_splice_locked(&self, counterparty_node_id: PublicKey, msg: &msgs::SpliceLocked) { let _: Result<(), _> = handle_error!(self, Err(MsgHandleErrInternal::send_err_msg_no_close( "Splicing not supported (splice_locked)".to_owned(), - msg.channel_id.clone())), counterparty_node_id); + msg.channel_id)), counterparty_node_id); } fn handle_shutdown(&self, counterparty_node_id: PublicKey, msg: &msgs::Shutdown) { diff --git a/lightning/src/ln/dual_funding_tests.rs b/lightning/src/ln/dual_funding_tests.rs index dd15c60beac..9b2247e1745 100644 --- a/lightning/src/ln/dual_funding_tests.rs +++ b/lightning/src/ln/dual_funding_tests.rs @@ -49,7 +49,7 @@ fn do_test_v2_channel_establishment(session: V2ChannelEstablishmentTestSession) &[session.initiator_input_value_satoshis], ) .into_iter() - .map(|(txin, tx)| (txin, TransactionU16LenLimited::new(tx).unwrap())) + .map(|(txin, tx, _)| (txin, TransactionU16LenLimited::new(tx).unwrap())) .collect(); // Alice creates a dual-funded channel as initiator. diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index 5e592c858f8..64775d9e0f7 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -36,7 +36,7 @@ use crate::util::test_utils; use crate::util::test_utils::{TestChainMonitor, TestScorer, TestKeysInterface}; use crate::util::ser::{ReadableArgs, Writeable}; -use bitcoin::WPubkeyHash; +use bitcoin::{Weight, WPubkeyHash}; use bitcoin::amount::Amount; use bitcoin::block::{Block, Header, Version as BlockVersion}; use bitcoin::locktime::absolute::{LockTime, LOCK_TIME_THRESHOLD}; @@ -58,6 +58,7 @@ use core::mem; use core::ops::Deref; use crate::io; use crate::prelude::*; +use crate::sign::P2WPKH_WITNESS_WEIGHT; use crate::sync::{Arc, Mutex, LockTestExt, RwLock}; pub const CHAN_CONFIRM_DEPTH: u32 = 10; @@ -800,7 +801,7 @@ macro_rules! get_event_msg { assert_eq!(*node_id, $node_id); (*msg).clone() }, - _ => panic!("Unexpected event"), + _ => panic!("Unexpected event {:?}", events[0]), } } } @@ -1232,9 +1233,11 @@ fn internal_create_funding_transaction<'a, 'b, 'c>(node: &Node<'a, 'b, 'c>, } } +/// Create test inputs for a funding transaction. +/// Return the inputs (with prev tx), and the total witness weight for these inputs pub fn create_dual_funding_utxos_with_prev_txs( node: &Node<'_, '_, '_>, utxo_values_in_satoshis: &[u64], -) -> Vec<(TxIn, Transaction)> { +) -> Vec<(TxIn, Transaction, Weight)> { // Ensure we have unique transactions per node by using the locktime. let tx = Transaction { version: TxVersion::TWO, @@ -1247,9 +1250,9 @@ pub fn create_dual_funding_utxos_with_prev_txs( }).collect() }; - let mut result = vec![]; + let mut inputs = vec![]; for i in 0..utxo_values_in_satoshis.len() { - result.push( + inputs.push( (TxIn { previous_output: OutPoint { txid: tx.compute_txid(), @@ -1258,9 +1261,13 @@ pub fn create_dual_funding_utxos_with_prev_txs( script_sig: ScriptBuf::new(), sequence: Sequence::ZERO, witness: Witness::new(), - }, tx.clone())); + }, + tx.clone(), + Weight::from_wu(P2WPKH_WITNESS_WEIGHT), + )); } - result + + inputs } pub fn sign_funding_transaction<'a, 'b, 'c>(node_a: &Node<'a, 'b, 'c>, node_b: &Node<'a, 'b, 'c>, channel_value: u64, expected_temporary_channel_id: ChannelId) -> Transaction { diff --git a/lightning/src/ln/mod.rs b/lightning/src/ln/mod.rs index da8efeb177a..ebb074e3e08 100644 --- a/lightning/src/ln/mod.rs +++ b/lightning/src/ln/mod.rs @@ -61,6 +61,9 @@ mod async_payments_tests; #[cfg(any(test, feature = "_externalize_tests"))] #[allow(unused_mut)] pub mod functional_tests; +#[cfg(all(test, splicing))] +#[allow(unused_mut)] +mod splicing_tests; #[cfg(test)] #[allow(unused_mut)] mod max_payment_path_len_tests; diff --git a/lightning/src/ln/msgs.rs b/lightning/src/ln/msgs.rs index 81505a92cc7..3adf8770b05 100644 --- a/lightning/src/ln/msgs.rs +++ b/lightning/src/ln/msgs.rs @@ -469,7 +469,7 @@ pub struct SpliceInit { /// or remove from its channel balance (splice-out). pub funding_contribution_satoshis: i64, /// The feerate for the new funding transaction, set by the splice initiator - pub funding_feerate_perkw: u32, + pub funding_feerate_per_kw: u32, /// The locktime for the new funding transaction pub locktime: u32, /// The key of the sender (splice initiator) controlling the new funding transaction @@ -2501,7 +2501,7 @@ impl_writeable_msg!(Stfu, { impl_writeable_msg!(SpliceInit, { channel_id, funding_contribution_satoshis, - funding_feerate_perkw, + funding_feerate_per_kw, locktime, funding_pubkey, }, { @@ -4361,7 +4361,7 @@ mod tests { let splice_init = msgs::SpliceInit { channel_id: ChannelId::from_bytes([2; 32]), funding_contribution_satoshis: -123456, - funding_feerate_perkw: 2000, + funding_feerate_per_kw: 2000, locktime: 0, funding_pubkey: pubkey_1, require_confirmed_inputs: Some(()), diff --git a/lightning/src/ln/splicing_tests.rs b/lightning/src/ln/splicing_tests.rs new file mode 100644 index 00000000000..84fe7e31db7 --- /dev/null +++ b/lightning/src/ln/splicing_tests.rs @@ -0,0 +1,159 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +use crate::ln::functional_test_utils::*; +use crate::ln::msgs::{BaseMessageHandler, ChannelMessageHandler, MessageSendEvent}; + +/// Splicing test, simple splice-in flow. Starts with opening a V1 channel first. +/// Builds on test_channel_open_simple() +#[test] +fn test_v1_splice_in() { + 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, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + // Initiator and Acceptor nodes + let initiator_node_index = 0; + let acceptor_node_index = 1; + let initiator_node = &nodes[initiator_node_index]; + let acceptor_node = &nodes[acceptor_node_index]; + + // Instantiate channel parameters where we push the maximum msats given our funding satoshis + let channel_value_sat = 100_000; // same as funding satoshis + let push_msat = 0; + let channel_reserve_amnt_sat = 1_000; + + let (_, _, channel_id, _) = create_announced_chan_between_nodes_with_value( + &nodes, + initiator_node_index, + acceptor_node_index, + channel_value_sat, + push_msat, + ); + + let expected_funded_channel_id = + "ae3367da2c13bc1ceb86bf56418f62828f7ce9d6bfb15a46af5ba1f1ed8b124f"; + assert_eq!(channel_id.to_string(), expected_funded_channel_id); + + let expected_initiator_funding_key = + "03c21e841cbc0b48197d060c71e116c185fa0ac281b7d0aa5924f535154437ca3b"; + let expected_acceptor_funding_key = + "039481c28b904cbe12681e79937373fc76245c1b29871028ae60ba3152162c319b"; + + // ==== Channel is now ready for normal operation + + // === Start of Splicing + println!("Start of Splicing ..., channel_id {}", channel_id); + + // Amount being added to the channel through the splice-in + let splice_in_sats: u64 = 20000; + let funding_feerate_per_kw = 1024; // TODO + + // Create additional inputs + let extra_splice_funding_input_sats = 35_000; + let funding_inputs = create_dual_funding_utxos_with_prev_txs( + &initiator_node, + &[extra_splice_funding_input_sats], + ); + // Initiate splice-in (on initiator_node) + let _res = initiator_node + .node + .splice_channel( + &channel_id, + &acceptor_node.node.get_our_node_id(), + splice_in_sats as i64, + funding_inputs, + funding_feerate_per_kw, + None, // locktime + ) + .unwrap(); + // Extract the splice message from node0 to node1 + let splice_init_msg = get_event_msg!( + initiator_node, + MessageSendEvent::SendSpliceInit, + acceptor_node.node.get_our_node_id() + ); + assert_eq!(splice_init_msg.funding_contribution_satoshis, splice_in_sats as i64); + assert_eq!(splice_init_msg.funding_feerate_per_kw, funding_feerate_per_kw); + assert_eq!(splice_init_msg.funding_pubkey.to_string(), expected_initiator_funding_key); + assert!(splice_init_msg.require_confirmed_inputs.is_none()); + + let _res = acceptor_node + .node + .handle_splice_init(initiator_node.node.get_our_node_id(), &splice_init_msg); + // Extract the splice_ack message from node1 to node0 + let splice_ack_msg = get_event_msg!( + acceptor_node, + MessageSendEvent::SendSpliceAck, + initiator_node.node.get_our_node_id() + ); + assert_eq!(splice_ack_msg.funding_contribution_satoshis, 0); + assert_eq!(splice_ack_msg.funding_pubkey.to_string(), expected_acceptor_funding_key); + assert!(splice_ack_msg.require_confirmed_inputs.is_none()); + + // still pre-splice channel: capacity not updated, channel usable, and funding tx set + assert_eq!(acceptor_node.node.list_channels().len(), 1); + { + let channel = &acceptor_node.node.list_channels()[0]; + assert_eq!(channel.channel_id.to_string(), expected_funded_channel_id); + assert!(channel.is_usable); + assert!(channel.is_channel_ready); + assert_eq!(channel.channel_value_satoshis, channel_value_sat); + assert_eq!(channel.outbound_capacity_msat, 0); + assert!(channel.funding_txo.is_some()); + assert!(channel.confirmations.unwrap() > 0); + } + + let _res = initiator_node + .node + .handle_splice_ack(acceptor_node.node.get_our_node_id(), &splice_ack_msg); + + // still pre-splice channel: capacity not updated, channel usable, and funding tx set + assert_eq!(initiator_node.node.list_channels().len(), 1); + { + let channel = &initiator_node.node.list_channels()[0]; + assert_eq!(channel.channel_id.to_string(), expected_funded_channel_id); + assert!(channel.is_usable); + assert!(channel.is_channel_ready); + assert_eq!(channel.channel_value_satoshis, channel_value_sat); + assert_eq!( + channel.outbound_capacity_msat, + 1000 * (channel_value_sat - channel_reserve_amnt_sat) + ); + assert!(channel.funding_txo.is_some()); + assert!(channel.confirmations.unwrap() > 0); + } + + let _error_msg = get_err_msg(initiator_node, &acceptor_node.node.get_our_node_id()); + + // TODO(splicing): continue with splice transaction negotiation + + // === Close channel, cooperatively + initiator_node.node.close_channel(&channel_id, &acceptor_node.node.get_our_node_id()).unwrap(); + let node0_shutdown_message = get_event_msg!( + initiator_node, + MessageSendEvent::SendShutdown, + acceptor_node.node.get_our_node_id() + ); + acceptor_node + .node + .handle_shutdown(initiator_node.node.get_our_node_id(), &node0_shutdown_message); + let nodes_1_shutdown = get_event_msg!( + acceptor_node, + MessageSendEvent::SendShutdown, + initiator_node.node.get_our_node_id() + ); + initiator_node.node.handle_shutdown(acceptor_node.node.get_our_node_id(), &nodes_1_shutdown); + let _ = get_event_msg!( + initiator_node, + MessageSendEvent::SendClosingSigned, + acceptor_node.node.get_our_node_id() + ); +}