Skip to content

Commit

Permalink
New splice_channel() for initiating splicing, handle splice_init and …
Browse files Browse the repository at this point in the history
…splice_ack messages, but fail afterwards
  • Loading branch information
optout21 committed Jan 5, 2025
1 parent 85d1e5f commit fc39429
Show file tree
Hide file tree
Showing 5 changed files with 795 additions and 12 deletions.
293 changes: 291 additions & 2 deletions lightning/src/ln/channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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};
Expand Down Expand Up @@ -1187,6 +1186,30 @@ impl UnfundedChannelContext {
}
}

/// Info about a pending splice, used in the pre-splice channel
#[cfg(splicing)]
#[derive(Clone)]
struct PendingSplice {
pub our_funding_contribution: i64,
}

#[cfg(splicing)]
impl PendingSplice {
#[inline]
fn add_checked(base: u64, delta: i64) -> u64 {
if delta >= 0 {
base.saturating_add(delta as u64)
} else {
base.saturating_sub(delta.abs() as u64)
}
}

/// Compute the post-splice channel value from the pre-splice values and the peer contributions
pub fn compute_post_value(pre_channel_value: u64, our_funding_contribution: i64, their_funding_contribution: i64) -> u64 {
Self::add_checked(pre_channel_value, our_funding_contribution.saturating_add(their_funding_contribution))
}
}

/// Contains everything about the channel including state, and various flags.
pub(super) struct ChannelContext<SP: Deref> where SP::Target: SignerProvider {
config: LegacyChannelConfig,
Expand Down Expand Up @@ -3635,6 +3658,33 @@ impl<SP: Deref> ChannelContext<SP> where SP::Target: SignerProvider {
(context.holder_selected_channel_reserve_satoshis, context.counterparty_selected_channel_reserve_satoshis)
}

/// Check that a balance value meets the channel reserve requirements or violates them (below reserve).
/// The channel value is an input as opposed to using from self, so that this can be used in case of splicing
/// to checks with new channel value (before being comitted to it).
#[cfg(splicing)]
pub fn check_balance_meets_reserve_requirements(&self, balance: u64, channel_value: u64) -> Result<(), ChannelError> {
if balance == 0 {
return Ok(());
}
let holder_selected_channel_reserve_satoshis = get_v2_channel_reserve_satoshis(
channel_value, self.holder_dust_limit_satoshis);
if balance < holder_selected_channel_reserve_satoshis {
return Err(ChannelError::Warn(format!(
"Balance below reserve mandated by holder, {} vs {}",
balance, holder_selected_channel_reserve_satoshis,
)));
}
let counterparty_selected_channel_reserve_satoshis = get_v2_channel_reserve_satoshis(
channel_value, self.counterparty_dust_limit_satoshis);
if balance < counterparty_selected_channel_reserve_satoshis {
return Err(ChannelError::Warn(format!(
"Balance below reserve mandated by counterparty, {} vs {}",
balance, counterparty_selected_channel_reserve_satoshis,
)));
}
Ok(())
}

/// Get the commitment tx fee for the local's (i.e. our) next commitment transaction based on the
/// number of pending HTLCs that are on track to be in our next commitment tx.
///
Expand Down Expand Up @@ -4097,6 +4147,38 @@ impl<SP: Deref> ChannelContext<SP> where SP::Target: SignerProvider {
self.channel_transaction_parameters = channel_transaction_parameters;
self.get_initial_counterparty_commitment_signature(logger)
}

/// Get the splice message that can be sent during splice initiation.
#[cfg(splicing)]
pub fn get_splice_init(&self, our_funding_contribution_satoshis: i64,
funding_feerate_perkw: u32, locktime: u32,
) -> msgs::SpliceInit {
// Reuse the existing funding pubkey, in spite of the channel value changing
// (though at this point we don't know the new value yet, due tue the optional counterparty contribution)
// Note that channel_keys_id is supposed NOT to change
let funding_pubkey = self.get_holder_pubkeys().funding_pubkey.clone();
msgs::SpliceInit {
channel_id: self.channel_id,
funding_contribution_satoshis: our_funding_contribution_satoshis,
funding_feerate_perkw,
locktime,
funding_pubkey,
require_confirmed_inputs: None,
}
}

/// Get the splice_ack message that can be sent in response to splice initiation.
#[cfg(splicing)]
pub fn get_splice_ack(&self, our_funding_contribution_satoshis: i64) -> msgs::SpliceAck {
// Reuse the existing funding pubkey, in spite of the channel value changing
let funding_pubkey = self.get_holder_pubkeys().funding_pubkey;
msgs::SpliceAck {
channel_id: self.channel_id,
funding_contribution_satoshis: our_funding_contribution_satoshis,
funding_pubkey,
require_confirmed_inputs: None,
}
}
}

// Internal utility functions for channels
Expand Down Expand Up @@ -4221,6 +4303,9 @@ pub(super) struct Channel<SP: Deref> where SP::Target: SignerProvider {
pub context: ChannelContext<SP>,
pub interactive_tx_signing_session: Option<InteractiveTxSigningSession>,
holder_commitment_point: HolderCommitmentPoint,
/// Info about an in-progress, pending splice (if any), on the pre-splice channel
#[cfg(splicing)]
pending_splice_pre: Option<PendingSplice>,
}

#[cfg(any(test, fuzzing))]
Expand Down Expand Up @@ -7832,6 +7917,135 @@ impl<SP: Deref> Channel<SP> where
}
}

/// Initiate splicing
#[cfg(splicing)]
pub fn splice_channel(&mut self, our_funding_contribution_satoshis: i64,
our_funding_inputs: Vec<(TxIn, Transaction)>, funding_feerate_perkw: u32, locktime: u32,
) -> Result<msgs::SpliceInit, ChannelError> {
// Check if a splice has been initiated already.
// Note: this could be handled more nicely, and support multiple outstanding splice's, the incoming splice_ack matters anyways.
if let Some(splice_info) = &self.pending_splice_pre {
return Err(ChannelError::Warn(format!(
"Channel has already a splice pending, contribution {}", splice_info.our_funding_contribution
)));
}

if !matches!(self.context.channel_state, ChannelState::ChannelReady(_)) {
return Err(ChannelError::Warn(format!("Cannot initiate splicing, as channel is not Ready")));
}

let pre_channel_value = self.context.get_value_satoshis();
// Sanity check: capacity cannot decrease below 0
if (pre_channel_value as i64).saturating_add(our_funding_contribution_satoshis) < 0 {
return Err(ChannelError::Warn(format!(
"Post-splicing channel value cannot be negative. It was {} + {}",
pre_channel_value, our_funding_contribution_satoshis
)));
}

if our_funding_contribution_satoshis < 0 {
return Err(ChannelError::Warn(format!(
"TODO(splicing): Splice-out not supported, only splice in, contribution {}",
our_funding_contribution_satoshis,
)));
}

// Note: post-splice channel value is not yet known at this point, counterpary contribution is not known
// (Cannot test for miminum required post-splice channel value)

// Check that inputs are sufficient to cover our contribution
let sum_input: i64 = our_funding_inputs.into_iter().map(
|(txin, tx)| tx.output.get(txin.previous_output.vout as usize).map(|tx| tx.value.to_sat() as i64).unwrap_or(0)
).sum();
if sum_input < our_funding_contribution_satoshis {
return Err(ChannelError::Warn(format!(
"Provided inputs are insufficient for our contribution, {} {}",
sum_input, our_funding_contribution_satoshis,
)));
}

self.pending_splice_pre = Some(PendingSplice {
our_funding_contribution: our_funding_contribution_satoshis,
});

let msg = self.context.get_splice_init(our_funding_contribution_satoshis, funding_feerate_perkw, locktime);
Ok(msg)
}

/// Handle splice_init
#[cfg(splicing)]
pub fn splice_init(&mut self, msg: &msgs::SpliceInit) -> Result<msgs::SpliceAck, ChannelError> {
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.
// Note: this could be handled more nicely, and support multiple outstanding splice's, the incoming splice_ack matters anyways.
if let Some(splice_info) = &self.pending_splice_pre {
return Err(ChannelError::Warn(format!(
"Channel has already a splice pending, contribution {}", splice_info.our_funding_contribution,
)));
}

if !matches!(self.context.channel_state, ChannelState::ChannelReady(_)) {
return Err(ChannelError::Warn(format!("Splicing requested on a channel that is not Ready")));
}

let pre_channel_value = self.context.get_value_satoshis();
// Sanity check: capacity cannot decrease below 0
if (pre_channel_value as i64)
.saturating_add(their_funding_contribution_satoshis)
.saturating_add(our_funding_contribution_satoshis) < 0
{
return Err(ChannelError::Warn(format!(
"Post-splicing channel value cannot be negative. It was {} + {} + {}",
pre_channel_value, their_funding_contribution_satoshis, our_funding_contribution_satoshis,
)));
}

if their_funding_contribution_satoshis.saturating_add(our_funding_contribution_satoshis) < 0 {
return Err(ChannelError::Warn(format!(
"Splice-out not supported, only splice in, relative {} + {}",
their_funding_contribution_satoshis, our_funding_contribution_satoshis,
)));
}

let post_channel_value = PendingSplice::compute_post_value(pre_channel_value, their_funding_contribution_satoshis, our_funding_contribution_satoshis);
let post_balance = PendingSplice::add_checked(self.context.value_to_self_msat, our_funding_contribution_satoshis);
// 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
// TODO(splicing): Apply start of splice (splice_start)

let splice_ack_msg = self.context.get_splice_ack(our_funding_contribution_satoshis);
// 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> {
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 = PendingSplice::compute_post_value(pre_channel_value, our_funding_contribution, their_funding_contribution_satoshis);
let post_balance = PendingSplice::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)?;
Ok(())
}

// Send stuff to our remote peers:

Expand Down Expand Up @@ -8526,6 +8740,8 @@ impl<SP: Deref> OutboundV1Channel<SP> where SP::Target: SignerProvider {
context: self.context,
interactive_tx_signing_session: None,
holder_commitment_point,
#[cfg(splicing)]
pending_splice_pre: None,
};

let need_channel_ready = channel.check_get_channel_ready(0, logger).is_some()
Expand Down Expand Up @@ -8791,6 +9007,8 @@ impl<SP: Deref> InboundV1Channel<SP> where SP::Target: SignerProvider {
context: self.context,
interactive_tx_signing_session: None,
holder_commitment_point,
#[cfg(splicing)]
pending_splice_pre: None,
};
let need_channel_ready = channel.check_get_channel_ready(0, logger).is_some()
|| channel.context.signer_pending_channel_ready;
Expand Down Expand Up @@ -8965,6 +9183,8 @@ impl<SP: Deref> OutboundV2Channel<SP> where SP::Target: SignerProvider {
context: self.context,
interactive_tx_signing_session: Some(signing_session),
holder_commitment_point,
#[cfg(splicing)]
pending_splice_pre: None,
};

Ok(channel)
Expand Down Expand Up @@ -9170,6 +9390,8 @@ impl<SP: Deref> InboundV2Channel<SP> where SP::Target: SignerProvider {
context: self.context,
interactive_tx_signing_session: Some(signing_session),
holder_commitment_point,
#[cfg(splicing)]
pending_splice_pre: None,
};

Ok(channel)
Expand Down Expand Up @@ -10250,6 +10472,8 @@ impl<'a, 'b, 'c, ES: Deref, SP: Deref> ReadableArgs<(&'a ES, &'b SP, u32, &'c Ch
},
interactive_tx_signing_session: None,
holder_commitment_point,
#[cfg(splicing)]
pending_splice_pre: None,
})
}
}
Expand Down Expand Up @@ -12032,4 +12256,69 @@ mod tests {
assert_eq!(node_a_chan.context.channel_state, ChannelState::AwaitingChannelReady(AwaitingChannelReadyFlags::THEIR_CHANNEL_READY));
assert!(node_a_chan.check_get_channel_ready(0, &&logger).is_some());
}

#[cfg(all(test, splicing))]
fn get_pre_and_post(pre_channel_value: u64, our_funding_contribution: i64, their_funding_contribution: i64) -> (u64, u64) {
use crate::ln::channel::PendingSplice;

let post_channel_value = PendingSplice::compute_post_value(pre_channel_value, our_funding_contribution, their_funding_contribution);
(pre_channel_value, post_channel_value)
}

#[cfg(all(test, splicing))]
#[test]
fn test_splice_compute_post_value() {
{
// increase, small amounts
let (pre_channel_value, post_channel_value) = get_pre_and_post(9_000, 6_000, 0);
assert_eq!(pre_channel_value, 9_000);
assert_eq!(post_channel_value, 15_000);
}
{
// increase, small amounts
let (pre_channel_value, post_channel_value) = get_pre_and_post(9_000, 4_000, 2_000);
assert_eq!(pre_channel_value, 9_000);
assert_eq!(post_channel_value, 15_000);
}
{
// increase, small amounts
let (pre_channel_value, post_channel_value) = get_pre_and_post(9_000, 0, 6_000);
assert_eq!(pre_channel_value, 9_000);
assert_eq!(post_channel_value, 15_000);
}
{
// decrease, small amounts
let (pre_channel_value, post_channel_value) = get_pre_and_post(15_000, -6_000, 0);
assert_eq!(pre_channel_value, 15_000);
assert_eq!(post_channel_value, 9_000);
}
{
// decrease, small amounts
let (pre_channel_value, post_channel_value) = get_pre_and_post(15_000, -4_000, -2_000);
assert_eq!(pre_channel_value, 15_000);
assert_eq!(post_channel_value, 9_000);
}
{
// increase and decrease
let (pre_channel_value, post_channel_value) = get_pre_and_post(15_000, 4_000, -2_000);
assert_eq!(pre_channel_value, 15_000);
assert_eq!(post_channel_value, 17_000);
}
let base2: u64 = 2;
let huge63i3 = (base2.pow(63) - 3) as i64;
assert_eq!(huge63i3, 9223372036854775805);
assert_eq!(-huge63i3, -9223372036854775805);
{
// increase, large amount
let (pre_channel_value, post_channel_value) = get_pre_and_post(9_000, huge63i3, 3);
assert_eq!(pre_channel_value, 9_000);
assert_eq!(post_channel_value, 9223372036854784807);
}
{
// increase, large amounts
let (pre_channel_value, post_channel_value) = get_pre_and_post(9_000, huge63i3, huge63i3);
assert_eq!(pre_channel_value, 9_000);
assert_eq!(post_channel_value, 9223372036854784807);
}
}
}
Loading

0 comments on commit fc39429

Please sign in to comment.