Skip to content

Commit cf3ff25

Browse files
committed
Add splice-out support
Update SpliceContribution with a variant used to support splice-out (i.e., removing funds from a channel). The TxOut values must not exceed the users channel balance after accounting for fees and the reserve requirement.
1 parent 0e882d0 commit cf3ff25

File tree

4 files changed

+175
-72
lines changed

4 files changed

+175
-72
lines changed

lightning/src/ln/channel.rs

Lines changed: 136 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ use bitcoin::secp256k1::{ecdsa::Signature, Secp256k1};
2626
use bitcoin::secp256k1::{PublicKey, SecretKey};
2727
use bitcoin::{secp256k1, sighash};
2828
#[cfg(splicing)]
29-
use bitcoin::{Sequence, TxIn};
29+
use bitcoin::{FeeRate, Sequence, TxIn};
3030

3131
use crate::chain::chaininterface::{
3232
fee_for_weight, ConfirmationTarget, FeeEstimator, LowerBoundedFeeEstimator,
@@ -5878,20 +5878,53 @@ fn get_v2_channel_reserve_satoshis(channel_value_satoshis: u64, dust_limit_satos
58785878
cmp::min(channel_value_satoshis, cmp::max(q, dust_limit_satoshis))
58795879
}
58805880

5881+
#[cfg(splicing)]
5882+
fn check_splice_contribution_sufficient(
5883+
channel_balance: Amount, contribution: &SpliceContribution, is_initiator: bool,
5884+
funding_feerate: FeeRate,
5885+
) -> Result<Amount, ChannelError> {
5886+
let contribution_amount = contribution.value();
5887+
if contribution_amount < SignedAmount::ZERO {
5888+
let estimated_fee = Amount::from_sat(estimate_v2_funding_transaction_fee(
5889+
is_initiator,
5890+
1, // spends the previous funding output
5891+
Weight::from_wu(FUNDING_TRANSACTION_WITNESS_WEIGHT),
5892+
contribution.outputs(),
5893+
funding_feerate.to_sat_per_kwu() as u32,
5894+
));
5895+
5896+
if channel_balance > contribution_amount.unsigned_abs() + estimated_fee {
5897+
Ok(estimated_fee)
5898+
} else {
5899+
Err(ChannelError::Warn(format!(
5900+
"Available channel balance {} is lower than needed for splicing out {}, considering fees of {}",
5901+
channel_balance, contribution_amount.unsigned_abs(), estimated_fee,
5902+
)))
5903+
}
5904+
} else {
5905+
check_v2_funding_inputs_sufficient(
5906+
contribution_amount.to_sat(),
5907+
contribution.inputs(),
5908+
is_initiator,
5909+
true,
5910+
funding_feerate.to_sat_per_kwu() as u32,
5911+
)
5912+
.map(Amount::from_sat)
5913+
}
5914+
}
5915+
58815916
/// Estimate our part of the fee of the new funding transaction.
58825917
/// input_count: Number of contributed inputs.
58835918
/// witness_weight: The witness weight for contributed inputs.
58845919
#[allow(dead_code)] // TODO(dual_funding): TODO(splicing): Remove allow once used.
58855920
#[rustfmt::skip]
58865921
fn estimate_v2_funding_transaction_fee(
5887-
is_initiator: bool, input_count: usize, witness_weight: Weight,
5922+
is_initiator: bool, input_count: usize, witness_weight: Weight, outputs: &[TxOut],
58885923
funding_feerate_sat_per_1000_weight: u32,
58895924
) -> u64 {
5890-
// Inputs
58915925
let mut weight = (input_count as u64) * BASE_INPUT_WEIGHT;
5892-
5893-
// Witnesses
58945926
weight = weight.saturating_add(witness_weight.to_wu());
5927+
weight = weight.saturating_add(outputs.iter().map(|txout| txout.weight().to_wu()).sum());
58955928

58965929
// If we are the initiator, we must pay for weight of all common fields in the funding transaction.
58975930
if is_initiator {
@@ -5928,7 +5961,7 @@ fn check_v2_funding_inputs_sufficient(
59285961
funding_inputs_len += 1;
59295962
total_input_witness_weight += Weight::from_wu(FUNDING_TRANSACTION_WITNESS_WEIGHT);
59305963
}
5931-
let estimated_fee = estimate_v2_funding_transaction_fee(is_initiator, funding_inputs_len, total_input_witness_weight, funding_feerate_sat_per_1000_weight);
5964+
let estimated_fee = estimate_v2_funding_transaction_fee(is_initiator, funding_inputs_len, total_input_witness_weight, &[], funding_feerate_sat_per_1000_weight);
59325965

59335966
let mut total_input_sats = 0u64;
59345967
for (idx, FundingTxInput { txin, prevtx, .. }) in funding_inputs.iter().enumerate() {
@@ -5982,6 +6015,9 @@ pub(super) struct FundingNegotiationContext {
59826015
/// The funding inputs we will be contributing to the channel.
59836016
#[allow(dead_code)] // TODO(dual_funding): Remove once contribution to V2 channels is enabled.
59846017
pub our_funding_inputs: Vec<FundingTxInput>,
6018+
/// The funding outputs we will be contributing to the channel.
6019+
#[allow(dead_code)] // TODO(dual_funding): Remove once contribution to V2 channels is enabled.
6020+
pub our_funding_outputs: Vec<TxOut>,
59856021
/// The change output script. This will be used if needed or -- if not set -- generated using
59866022
/// `SignerProvider::get_destination_script`.
59876023
#[allow(dead_code)] // TODO(splicing): Remove once splicing is enabled.
@@ -6011,45 +6047,47 @@ impl FundingNegotiationContext {
60116047
debug_assert!(matches!(context.channel_state, ChannelState::NegotiatingFunding(_)));
60126048
}
60136049

6014-
// Add output for funding tx
60156050
// Note: For the error case when the inputs are insufficient, it will be handled after
60166051
// the `calculate_change_output_value` call below
6017-
let mut funding_outputs = Vec::new();
60186052

60196053
let shared_funding_output = TxOut {
60206054
value: Amount::from_sat(funding.get_value_satoshis()),
60216055
script_pubkey: funding.get_funding_redeemscript().to_p2wsh(),
60226056
};
60236057

60246058
// Optionally add change output
6025-
if self.our_funding_contribution > SignedAmount::ZERO {
6026-
let change_value_opt = calculate_change_output_value(
6059+
let change_value_opt = if self.our_funding_contribution > SignedAmount::ZERO {
6060+
calculate_change_output_value(
60276061
&self,
60286062
self.shared_funding_input.is_some(),
60296063
&shared_funding_output.script_pubkey,
6030-
&funding_outputs,
60316064
context.holder_dust_limit_satoshis,
6032-
)?;
6033-
if let Some(change_value) = change_value_opt {
6034-
let change_script = if let Some(script) = self.change_script {
6035-
script
6036-
} else {
6037-
signer_provider
6038-
.get_destination_script(context.channel_keys_id)
6039-
.map_err(|_err| AbortReason::InternalError("Error getting change script"))?
6040-
};
6041-
let mut change_output =
6042-
TxOut { value: Amount::from_sat(change_value), script_pubkey: change_script };
6043-
let change_output_weight = get_output_weight(&change_output.script_pubkey).to_wu();
6044-
let change_output_fee =
6045-
fee_for_weight(self.funding_feerate_sat_per_1000_weight, change_output_weight);
6046-
let change_value_decreased_with_fee =
6047-
change_value.saturating_sub(change_output_fee);
6048-
// Check dust limit again
6049-
if change_value_decreased_with_fee > context.holder_dust_limit_satoshis {
6050-
change_output.value = Amount::from_sat(change_value_decreased_with_fee);
6051-
funding_outputs.push(change_output);
6052-
}
6065+
)?
6066+
} else {
6067+
None
6068+
};
6069+
6070+
let mut funding_outputs = self.our_funding_outputs;
6071+
6072+
if let Some(change_value) = change_value_opt {
6073+
let change_script = if let Some(script) = self.change_script {
6074+
script
6075+
} else {
6076+
signer_provider
6077+
.get_destination_script(context.channel_keys_id)
6078+
.map_err(|_err| AbortReason::InternalError("Error getting change script"))?
6079+
};
6080+
let mut change_output =
6081+
TxOut { value: Amount::from_sat(change_value), script_pubkey: change_script };
6082+
let change_output_weight = get_output_weight(&change_output.script_pubkey).to_wu();
6083+
let change_output_fee =
6084+
fee_for_weight(self.funding_feerate_sat_per_1000_weight, change_output_weight);
6085+
let change_value_decreased_with_fee =
6086+
change_value.saturating_sub(change_output_fee);
6087+
// Check dust limit again
6088+
if change_value_decreased_with_fee > context.holder_dust_limit_satoshis {
6089+
change_output.value = Amount::from_sat(change_value_decreased_with_fee);
6090+
funding_outputs.push(change_output);
60536091
}
60546092
}
60556093

@@ -10638,44 +10676,77 @@ where
1063810676
if our_funding_contribution > SignedAmount::MAX_MONEY {
1063910677
return Err(APIError::APIMisuseError {
1064010678
err: format!(
10641-
"Channel {} cannot be spliced; contribution exceeds total bitcoin supply: {}",
10679+
"Channel {} cannot be spliced in; contribution exceeds total bitcoin supply: {}",
1064210680
self.context.channel_id(),
1064310681
our_funding_contribution,
1064410682
),
1064510683
});
1064610684
}
1064710685

10648-
if our_funding_contribution < SignedAmount::ZERO {
10686+
if our_funding_contribution < -SignedAmount::MAX_MONEY {
1064910687
return Err(APIError::APIMisuseError {
1065010688
err: format!(
10651-
"TODO(splicing): Splice-out not supported, only splice in; channel ID {}, contribution {}",
10652-
self.context.channel_id(), our_funding_contribution,
10653-
),
10689+
"Channel {} cannot be spliced out; contribution exceeds total bitcoin supply: {}",
10690+
self.context.channel_id(),
10691+
our_funding_contribution,
10692+
),
1065410693
});
1065510694
}
1065610695

10657-
// TODO(splicing): Once splice-out is supported, check that channel balance does not go below 0
10658-
// (or below channel reserve)
10696+
let funding_inputs = contribution.inputs();
10697+
let funding_outputs = contribution.outputs();
10698+
if !funding_inputs.is_empty() && !funding_outputs.is_empty() {
10699+
return Err(APIError::APIMisuseError {
10700+
err: format!(
10701+
"Channel {} cannot be both spliced in and out; operation not supported",
10702+
self.context.channel_id(),
10703+
),
10704+
});
10705+
}
1065910706

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

10663-
// Check that inputs are sufficient to cover our contribution.
10664-
let _fee = check_v2_funding_inputs_sufficient(
10665-
our_funding_contribution.to_sat(),
10666-
contribution.inputs(),
10667-
true,
10668-
true,
10669-
funding_feerate_per_kw,
10710+
let channel_balance = Amount::from_sat(self.funding.get_value_to_self_msat() / 1000);
10711+
let fees = check_splice_contribution_sufficient(
10712+
channel_balance,
10713+
&contribution,
10714+
true, // is_initiator
10715+
FeeRate::from_sat_per_kwu(funding_feerate_per_kw as u64),
1067010716
)
10671-
.map_err(|err| APIError::APIMisuseError {
10672-
err: format!(
10673-
"Insufficient inputs for splicing; channel ID {}, err {}",
10674-
self.context.channel_id(),
10675-
err,
10676-
),
10717+
.map_err(|e| {
10718+
let splice_type = if our_funding_contribution < SignedAmount::ZERO {
10719+
"spliced out"
10720+
} else {
10721+
"spliced in"
10722+
};
10723+
APIError::APIMisuseError {
10724+
err: format!(
10725+
"Channel {} cannot be {}; {}",
10726+
self.context.channel_id(),
10727+
splice_type,
10728+
e,
10729+
),
10730+
}
1067710731
})?;
1067810732

10733+
// Fees for splice-out are paid from the channel balance whereas fees for splice-in are paid
10734+
// by the funding inputs.
10735+
let adjusted_funding_contribution = if our_funding_contribution < SignedAmount::ZERO {
10736+
let adjusted_funding_contribution = our_funding_contribution
10737+
- fees.to_signed().expect("fees should never exceed splice-out value");
10738+
10739+
// TODO(splicing): Check that channel balance does not go below the channel reserve
10740+
let _post_channel_balance = AddSigned::checked_add_signed(
10741+
channel_balance.to_sat(),
10742+
adjusted_funding_contribution.to_sat(),
10743+
);
10744+
10745+
adjusted_funding_contribution
10746+
} else {
10747+
our_funding_contribution
10748+
};
10749+
1067910750
for FundingTxInput { txin, prevtx, .. } in contribution.inputs().iter() {
1068010751
const MESSAGE_TEMPLATE: msgs::TxAddInput = msgs::TxAddInput {
1068110752
channel_id: ChannelId([0; 32]),
@@ -10697,14 +10768,15 @@ where
1069710768
}
1069810769

1069910770
let prev_funding_input = self.funding.to_splice_funding_input();
10700-
let (our_funding_inputs, change_script) = contribution.into_tx_parts();
10771+
let (our_funding_inputs, our_funding_outputs, change_script) = contribution.into_tx_parts();
1070110772
let funding_negotiation_context = FundingNegotiationContext {
1070210773
is_initiator: true,
10703-
our_funding_contribution,
10774+
our_funding_contribution: adjusted_funding_contribution,
1070410775
funding_tx_locktime: LockTime::from_consensus(locktime),
1070510776
funding_feerate_sat_per_1000_weight: funding_feerate_per_kw,
1070610777
shared_funding_input: Some(prev_funding_input),
1070710778
our_funding_inputs,
10779+
our_funding_outputs,
1070810780
change_script,
1070910781
};
1071010782

@@ -10720,7 +10792,7 @@ where
1072010792

1072110793
Ok(msgs::SpliceInit {
1072210794
channel_id: self.context.channel_id,
10723-
funding_contribution_satoshis: our_funding_contribution.to_sat(),
10795+
funding_contribution_satoshis: adjusted_funding_contribution.to_sat(),
1072410796
funding_feerate_per_kw,
1072510797
locktime,
1072610798
funding_pubkey,
@@ -10827,6 +10899,7 @@ where
1082710899
funding_feerate_sat_per_1000_weight: msg.funding_feerate_per_kw,
1082810900
shared_funding_input: Some(prev_funding_input),
1082910901
our_funding_inputs: Vec::new(),
10902+
our_funding_outputs: Vec::new(),
1083010903
change_script: None,
1083110904
};
1083210905

@@ -12525,6 +12598,7 @@ where
1252512598
funding_feerate_sat_per_1000_weight,
1252612599
shared_funding_input: None,
1252712600
our_funding_inputs: funding_inputs,
12601+
our_funding_outputs: Vec::new(),
1252812602
change_script: None,
1252912603
};
1253012604
let chan = Self {
@@ -12679,6 +12753,7 @@ where
1267912753
funding_feerate_sat_per_1000_weight: msg.funding_feerate_sat_per_1000_weight,
1268012754
shared_funding_input: None,
1268112755
our_funding_inputs: our_funding_inputs.clone(),
12756+
our_funding_outputs: Vec::new(),
1268212757
change_script: None,
1268312758
};
1268412759
let shared_funding_output = TxOut {
@@ -12702,7 +12777,7 @@ where
1270212777
inputs_to_contribute,
1270312778
shared_funding_input: None,
1270412779
shared_funding_output: SharedOwnedOutput::new(shared_funding_output, our_funding_contribution_sats),
12705-
outputs_to_contribute: Vec::new(),
12780+
outputs_to_contribute: funding_negotiation_context.our_funding_outputs.clone(),
1270612781
}
1270712782
).map_err(|err| {
1270812783
let reason = ClosureReason::ProcessingError { err: err.to_string() };
@@ -15873,31 +15948,31 @@ mod tests {
1587315948

1587415949
// 2 inputs with weight 300, initiator, 2000 sat/kw feerate
1587515950
assert_eq!(
15876-
estimate_v2_funding_transaction_fee(true, 2, Weight::from_wu(300), 2000),
15951+
estimate_v2_funding_transaction_fee(true, 2, Weight::from_wu(300), &[], 2000),
1587715952
1668
1587815953
);
1587915954

1588015955
// higher feerate
1588115956
assert_eq!(
15882-
estimate_v2_funding_transaction_fee(true, 2, Weight::from_wu(300), 3000),
15957+
estimate_v2_funding_transaction_fee(true, 2, Weight::from_wu(300), &[], 3000),
1588315958
2502
1588415959
);
1588515960

1588615961
// only 1 input
1588715962
assert_eq!(
15888-
estimate_v2_funding_transaction_fee(true, 1, Weight::from_wu(300), 2000),
15963+
estimate_v2_funding_transaction_fee(true, 1, Weight::from_wu(300), &[], 2000),
1588915964
1348
1589015965
);
1589115966

1589215967
// 0 input weight
1589315968
assert_eq!(
15894-
estimate_v2_funding_transaction_fee(true, 1, Weight::from_wu(0), 2000),
15969+
estimate_v2_funding_transaction_fee(true, 1, Weight::from_wu(0), &[], 2000),
1589515970
748
1589615971
);
1589715972

1589815973
// not initiator
1589915974
assert_eq!(
15900-
estimate_v2_funding_transaction_fee(false, 1, Weight::from_wu(0), 2000),
15975+
estimate_v2_funding_transaction_fee(false, 1, Weight::from_wu(0), &[], 2000),
1590115976
320
1590215977
);
1590315978
}

0 commit comments

Comments
 (0)