Skip to content

Commit

Permalink
Perform interactive tx negotiation during splicing
Browse files Browse the repository at this point in the history
  • Loading branch information
optout21 committed Jan 15, 2025
1 parent b0d96ba commit 6e8a1c1
Show file tree
Hide file tree
Showing 4 changed files with 373 additions and 126 deletions.
284 changes: 200 additions & 84 deletions lightning/src/ln/channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ use crate::ln::types::ChannelId;
use crate::types::payment::{PaymentPreimage, PaymentHash};
use crate::types::features::{ChannelTypeFeatures, InitFeatures};
use crate::ln::interactivetxs::{
get_output_weight, calculate_change_output_value, HandleTxCompleteValue, HandleTxCompleteResult, InteractiveTxConstructor,
estimate_input_weight, get_output_weight, calculate_change_output_value, HandleTxCompleteValue, HandleTxCompleteResult, InteractiveTxConstructor,
InteractiveTxConstructorArgs, InteractiveTxMessageSend, InteractiveTxSigningSession, InteractiveTxMessageSendResult,
OutputOwned, SharedOwnedOutput, TX_COMMON_FIELDS_WEIGHT,
};
Expand Down Expand Up @@ -1142,7 +1142,7 @@ pub(super) enum Channel<SP: Deref> where SP::Target: SignerProvider {
Funded(FundedChannel<SP>),
#[cfg(splicing)]
/// Used during splicing, channel is funded but a new funding is being renegotiated.
RefundingV2(FundedChannel<SP>),
RefundingV2(SplicingChannel<SP>),
}

impl<SP: Deref> Channel<SP> where
Expand All @@ -1156,7 +1156,7 @@ impl<SP: Deref> Channel<SP> where
Channel::UnfundedInboundV1(chan) => &chan.context,
Channel::UnfundedV2(chan) => &chan.context,
#[cfg(splicing)]
Channel::RefundingV2(chan) => &chan.context,
Channel::RefundingV2(chan) => &chan.pre_funded.context,
}
}

Expand All @@ -1167,7 +1167,7 @@ impl<SP: Deref> Channel<SP> where
Channel::UnfundedInboundV1(ref mut chan) => &mut chan.context,
Channel::UnfundedV2(ref mut chan) => &mut chan.context,
#[cfg(splicing)]
Channel::RefundingV2(ref mut chan) => &mut chan.context,
Channel::RefundingV2(ref mut chan) => &mut chan.pre_funded.context,
}
}

Expand All @@ -1183,22 +1183,29 @@ impl<SP: Deref> Channel<SP> where
}

pub fn is_funded(&self) -> bool {
matches!(self, Channel::Funded(_))
match self {
Channel::Funded(_) => true,
#[cfg(splicing)]
Channel::RefundingV2(_) => true,
_ => false,
}
}

pub fn as_funded(&self) -> Option<&FundedChannel<SP>> {
if let Channel::Funded(channel) = self {
Some(channel)
} else {
None
match self {
Channel::Funded(channel) => Some(channel),
#[cfg(splicing)]
Channel::RefundingV2(channel) => Some(&channel.pre_funded),
_ => None,
}
}

pub fn as_funded_mut(&mut self) -> Option<&mut FundedChannel<SP>> {
if let Channel::Funded(channel) = self {
Some(channel)
} else {
None
match self {
Channel::Funded(channel) => Some(channel),
#[cfg(splicing)]
Channel::RefundingV2(channel) => Some(&mut channel.pre_funded),
_ => None,
}
}

Expand Down Expand Up @@ -1239,10 +1246,11 @@ impl<SP: Deref> Channel<SP> where
}

pub fn as_unfunded_v2_mut(&mut self) -> Option<&mut PendingV2Channel<SP>> {
if let Channel::UnfundedV2(channel) = self {
Some(channel)
} else {
None
match self {
Channel::UnfundedV2(channel) => Some(channel),
#[cfg(splicing)]
Channel::RefundingV2(channel) => Some(&mut channel.post_pending),
_ => None,
}
}

Expand Down Expand Up @@ -1294,7 +1302,7 @@ impl<SP: Deref> Channel<SP> where
},
Channel::UnfundedV2(_) => None,
#[cfg(splicing)]
Channel::RefundingV2(chan) => Some(chan.signer_maybe_unblocked(logger)),
Channel::RefundingV2(chan) => Some(chan.pre_funded.signer_maybe_unblocked(logger)),
}
}

Expand Down Expand Up @@ -1423,6 +1431,55 @@ where
}
}

/// Struct holding together various state dureing splicing negotiation
#[cfg(splicing)]
pub(super) struct SplicingChannel<SP: Deref> where SP::Target: SignerProvider {
pub pre_funded: FundedChannel<SP>,
pub post_pending: PendingV2Channel<SP>,
pub post_funded: Option<FundedChannel<SP>>,
}

#[cfg(splicing)]
impl<SP: Deref> SplicingChannel<SP> where SP::Target: SignerProvider {
pub(super) fn new(pre_funded: FundedChannel<SP>, post_pending: PendingV2Channel<SP>) -> Self {
Self {
pre_funded,
post_pending,
post_funded: None,
}
}

pub fn splice_init<ES: Deref, L: Deref>(
&mut self, msg: &msgs::SpliceInit, signer_provider: &SP, entropy_source: &ES, holder_node_id: PublicKey, logger: &L,
) -> Result<msgs::SpliceAck,ChannelError> where ES::Target: EntropySource, L::Target: Logger {
self.post_pending.splice_init(msg, signer_provider, entropy_source, holder_node_id, logger)
}

pub fn splice_ack<ES: Deref, L: Deref>(
&mut self, msg: &msgs::SpliceAck, our_funding_contribution: i64, signer_provider: &SP, entropy_source: &ES, holder_node_id: PublicKey, logger: &L,
) -> Result<Option<InteractiveTxMessageSend>, ChannelError> where ES::Target: EntropySource, L::Target: Logger {
self.post_pending.splice_ack(msg, our_funding_contribution, signer_provider, entropy_source, holder_node_id, logger)
}

pub fn tx_add_input(&mut self, msg: &msgs::TxAddInput) -> InteractiveTxMessageSendResult {
self.post_pending.tx_add_input(msg)
}

pub fn tx_add_output(&mut self, msg: &msgs::TxAddOutput)-> InteractiveTxMessageSendResult {
self.post_pending.tx_add_output(msg)
}

pub fn tx_complete(&mut self, msg: &msgs::TxComplete) -> HandleTxCompleteResult {
self.post_pending.tx_complete(msg)
}

pub fn funding_tx_constructed<L: Deref>(
&mut self, signing_session: &mut InteractiveTxSigningSession, logger: &L
) -> Result<(msgs::CommitmentSigned, Option<Event>), ChannelError> where L::Target: Logger {
self.post_pending.funding_tx_constructed(signing_session, logger)
}
}

/// Contains all state common to unfunded inbound/outbound channels.
pub(super) struct UnfundedChannelContext {
/// A counter tracking how many ticks have elapsed since this unfunded channel was
Expand Down Expand Up @@ -1455,7 +1512,7 @@ impl UnfundedChannelContext {
/// Info about a pending splice, used in the pre-splice channel
#[cfg(splicing)]
#[derive(Clone)]
struct PendingSplicePre {
pub(super) struct PendingSplicePre {
pub our_funding_contribution: i64,
pub funding_feerate_perkw: u32,
pub locktime: u32,
Expand Down Expand Up @@ -4869,6 +4926,54 @@ fn get_v2_channel_reserve_satoshis(channel_value_satoshis: u64, dust_limit_satos
cmp::min(channel_value_satoshis, cmp::max(q, dust_limit_satoshis))
}

pub(super) fn maybe_add_funding_change_output<SP: Deref>(signer_provider: &SP, is_initiator: bool,
our_funding_satoshis: u64, funding_inputs_prev_outputs: &Vec<TxOut>,
funding_outputs: &mut Vec<OutputOwned>, funding_feerate_sat_per_1000_weight: u32,
total_input_satoshis: u64, holder_dust_limit_satoshis: u64, channel_keys_id: [u8; 32],
) -> Result<Option<TxOut>, ChannelError> where
SP::Target: SignerProvider,
{
let our_funding_inputs_weight = funding_inputs_prev_outputs.iter().fold(0u64, |weight, prev_output| {
weight.saturating_add(estimate_input_weight(prev_output).to_wu())
});
let our_funding_outputs_weight = funding_outputs.iter().fold(0u64, |weight, out| {
weight.saturating_add(get_output_weight(&out.tx_out().script_pubkey).to_wu())
});
let our_contributed_weight = our_funding_outputs_weight.saturating_add(our_funding_inputs_weight);
let mut fees_sats = fee_for_weight(funding_feerate_sat_per_1000_weight, our_contributed_weight);

// If we are the initiator, we must pay for weight of all common fields in the funding transaction.
if is_initiator {
let common_fees = fee_for_weight(funding_feerate_sat_per_1000_weight, TX_COMMON_FIELDS_WEIGHT);
fees_sats = fees_sats.saturating_add(common_fees);
}

let remaining_value = total_input_satoshis
.saturating_sub(our_funding_satoshis)
.saturating_sub(fees_sats);

if remaining_value < holder_dust_limit_satoshis {
Ok(None)
} else {
let change_script = signer_provider.get_destination_script(channel_keys_id).map_err(
|_| ChannelError::Close((
"Failed to get change script as new destination script".to_owned(),
ClosureReason::ProcessingError { err: "Failed to get change script as new destination script".to_owned() }
))
)?;
let mut change_output = TxOut {
value: Amount::from_sat(remaining_value),
script_pubkey: change_script,
};
let change_output_weight = get_output_weight(&change_output.script_pubkey).to_wu();

let change_output_fee = fee_for_weight(funding_feerate_sat_per_1000_weight, change_output_weight);
change_output.value = Amount::from_sat(remaining_value.saturating_sub(change_output_fee));
funding_outputs.push(OutputOwned::Single(change_output.clone()));
Ok(Some(change_output))
}
}

pub(super) fn calculate_our_funding_satoshis(
is_initiator: bool, funding_inputs: &[(TxIn, TransactionU16LenLimited)],
total_witness_weight: Weight, funding_feerate_sat_per_1000_weight: u32,
Expand Down Expand Up @@ -4941,7 +5046,7 @@ pub(super) struct FundedChannel<SP: Deref> where SP::Target: SignerProvider {
holder_commitment_point: HolderCommitmentPoint,
/// Info about an in-progress, pending splice (if any), on the pre-splice channel
#[cfg(splicing)]
pending_splice_pre: Option<PendingSplicePre>,
pub pending_splice_pre: Option<PendingSplicePre>,
/// Info about an in-progress, pending splice (if any), on the post-splice channel
#[cfg(splicing)]
pending_splice_post: Option<PendingSplicePost>,
Expand Down Expand Up @@ -8632,11 +8737,11 @@ impl<SP: Deref> FundedChannel<SP> where
Ok(msg)
}

/// Handle splice_init
/// Checks during handling splice_init
#[cfg(splicing)]
pub fn splice_init<ES: Deref, L: Deref>(
&mut self, msg: &msgs::SpliceInit, _signer_provider: &SP, _entropy_source: &ES, _holder_node_id: PublicKey, logger: &L,
) -> Result<msgs::SpliceAck, ChannelError> where ES::Target: EntropySource, L::Target: Logger {
pub fn splice_init_checks<ES: Deref>(
&mut self, msg: &msgs::SpliceInit, _signer_provider: &SP, _entropy_source: &ES, _holder_node_id: PublicKey,
) -> Result<(), ChannelError> where ES::Target: EntropySource {
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;
Expand Down Expand Up @@ -8677,52 +8782,7 @@ impl<SP: Deref> FundedChannel<SP> where
// Early check for reserve requirement, assuming maximum balance of full channel value
// This will also be checked later at tx_complete
let _res = self.context.check_balance_meets_reserve_requirements(post_balance, post_channel_value)?;

// TODO(splicing): Store msg.funding_pubkey

// Apply start of splice change in the state
self.context.splice_start(false, logger);

let splice_ack_msg = self.context.get_splice_ack(our_funding_contribution_satoshis);

// TODO(splicing): start interactive funding negotiation
// let _msg = self.begin_interactive_funding_tx_construction(signer_provider, entropy_source, holder_node_id)
// .map_err(|err| ChannelError::Warn(format!("Failed to start interactive transaction construction, {:?}", err)))?;

Ok(splice_ack_msg)
}

/// Handle splice_ack
#[cfg(splicing)]
pub fn splice_ack<ES: Deref, L: Deref>(
&mut self, msg: &msgs::SpliceAck, _signer_provider: &SP, _entropy_source: &ES, _holder_node_id: PublicKey, logger: &L,
) -> Result<Option<InteractiveTxMessageSend>, ChannelError> where ES::Target: EntropySource, L::Target: Logger {
let their_funding_contribution_satoshis = msg.funding_contribution_satoshis;

// check if splice is pending
let pending_splice = if let Some(pending_splice) = &self.pending_splice_pre {
pending_splice
} else {
return Err(ChannelError::Warn(format!("Channel is not in pending splice")));
};

let our_funding_contribution = pending_splice.our_funding_contribution;

let pre_channel_value = self.context.get_value_satoshis();
let post_channel_value = PendingSplicePre::compute_post_value(pre_channel_value, our_funding_contribution, their_funding_contribution_satoshis);
let post_balance = PendingSplicePre::add_checked(self.context.value_to_self_msat, our_funding_contribution);
// Early check for reserve requirement, assuming maximum balance of full channel value
// This will also be checked later at tx_complete
let _res = self.context.check_balance_meets_reserve_requirements(post_balance, post_channel_value)?;

// Apply start of splice change in the state
self.context.splice_start(true, logger);

// TODO(splicing): start interactive funding negotiation
// let tx_msg_opt = self.begin_interactive_funding_tx_construction(signer_provider, entropy_source, holder_node_id)
// .map_err(|err| ChannelError::Warn(format!("V2 channel rejected due to sender error, {:?}", err)))?;
// Ok(tx_msg_opt)
Ok(None)
Ok(())
}

// Send stuff to our remote peers:
Expand Down Expand Up @@ -9965,6 +10025,56 @@ impl<SP: Deref> PendingV2Channel<SP> where SP::Target: SignerProvider {
}
}

pub fn into_channel(self, signing_session: InteractiveTxSigningSession) -> Result<FundedChannel<SP>, ChannelError>{
let holder_commitment_point = self.unfunded_context.holder_commitment_point.ok_or(ChannelError::close(
format!("Expected to have holder commitment points available upon finishing interactive tx construction for channel {}",
self.context.channel_id())))?;
let channel = FundedChannel {
context: self.context,
interactive_tx_signing_session: Some(signing_session),
holder_commitment_point,
#[cfg(splicing)]
pending_splice_pre: None,
#[cfg(splicing)]
pending_splice_post: self.pending_splice_post,
};

Ok(channel)
}

/// Handle splice_ack
#[cfg(splicing)]
pub fn splice_ack<ES: Deref, L: Deref>(
&mut self, msg: &msgs::SpliceAck, our_funding_contribution: i64, signer_provider: &SP, entropy_source: &ES, holder_node_id: PublicKey, logger: &L,
) -> Result<Option<InteractiveTxMessageSend>, ChannelError> where ES::Target: EntropySource, L::Target: Logger {
let their_funding_contribution_satoshis = msg.funding_contribution_satoshis;

// check if splice is pending
let pending_splice = if let Some(pending_splice) = &self.pending_splice_post {
pending_splice
} else {
return Err(ChannelError::Warn(format!("Channel is not in pending splice")));
};

let pre_channel_value = self.context.get_value_satoshis();
let post_channel_value = PendingSplicePre::compute_post_value(pre_channel_value, our_funding_contribution, their_funding_contribution_satoshis);
let post_balance = PendingSplicePre::add_checked(self.context.value_to_self_msat, our_funding_contribution);
// Early check for reserve requirement, assuming maximum balance of full channel value
// This will also be checked later at tx_complete
let _res = self.context.check_balance_meets_reserve_requirements(post_balance, post_channel_value)?;

// We need the current funding tx as an extra input
let prev_funding_input = pending_splice.get_input_of_previous_funding()?;

// Apply start of splice change in the state
self.context.splice_start(true, logger);

// Start interactive funding negotiation, with the previous funding transaction as an extra shared input
let tx_msg_opt = self.begin_interactive_funding_tx_construction(signer_provider, entropy_source, holder_node_id, Some(prev_funding_input))
.map_err(|err| ChannelError::Warn(format!("V2 channel rejected due to sender error, {:?}", err)))?;
Ok(tx_msg_opt)
}

/// Creates a new dual-funded channel from a remote side's request for one.
/// Assumes chain_hash has already been checked and corresponds with what we expect!
#[allow(dead_code)] // TODO(dual_funding): Remove once V2 channels is enabled.
Expand Down Expand Up @@ -10149,21 +10259,27 @@ impl<SP: Deref> PendingV2Channel<SP> where SP::Target: SignerProvider {
self.generate_accept_channel_v2_message()
}

pub fn into_channel(self, signing_session: InteractiveTxSigningSession) -> Result<FundedChannel<SP>, ChannelError>{
let holder_commitment_point = self.unfunded_context.holder_commitment_point.ok_or(ChannelError::close(
format!("Expected to have holder commitment points available upon finishing interactive tx construction for channel {}",
self.context.channel_id())))?;
let channel = FundedChannel {
context: self.context,
interactive_tx_signing_session: Some(signing_session),
holder_commitment_point,
#[cfg(splicing)]
pending_splice_pre: None,
#[cfg(splicing)]
pending_splice_post: self.pending_splice_post,
};
/// Handle splice_init
/// See also [`splice_init_checks`]
#[cfg(splicing)]
pub fn splice_init<ES: Deref, L: Deref>(
&mut self, _msg: &msgs::SpliceInit, signer_provider: &SP, entropy_source: &ES, holder_node_id: PublicKey, logger: &L,
) -> Result<msgs::SpliceAck,ChannelError> where ES::Target: EntropySource, L::Target: Logger {
// TODO(splicing): Currently not possible to contribute on the splicing-acceptor side
let our_funding_contribution_satoshis = 0i64;

Ok(channel)
// TODO(splicing): Store msg.funding_pubkey

// Apply start of splice change in the state
self.context.splice_start(false, logger);

let splice_ack_msg = self.context.get_splice_ack(our_funding_contribution_satoshis);

// Start interactive funding negotiation. No extra input, as we are not the splice initiator
let _msg = self.begin_interactive_funding_tx_construction(signer_provider, entropy_source, holder_node_id, None)
.map_err(|err| ChannelError::Warn(format!("Failed to start interactive transaction construction, {:?}", err)))?;

Ok(splice_ack_msg)
}
}

Expand Down
Loading

0 comments on commit 6e8a1c1

Please sign in to comment.