Skip to content

Commit b672a2e

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 1f699d5 commit b672a2e

File tree

3 files changed

+136
-62
lines changed

3 files changed

+136
-62
lines changed

lightning/src/ln/channel.rs

Lines changed: 102 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -5981,6 +5981,9 @@ pub(super) struct FundingNegotiationContext {
59815981
/// The funding inputs we will be contributing to the channel.
59825982
#[allow(dead_code)] // TODO(dual_funding): Remove once contribution to V2 channels is enabled.
59835983
pub our_funding_inputs: Vec<FundingTxInput>,
5984+
/// The funding outputs we will be contributing to the channel.
5985+
#[allow(dead_code)] // TODO(dual_funding): Remove once contribution to V2 channels is enabled.
5986+
pub our_funding_outputs: Vec<TxOut>,
59845987
/// The change output script. This will be used if needed or -- if not set -- generated using
59855988
/// `SignerProvider::get_destination_script`.
59865989
#[allow(dead_code)] // TODO(splicing): Remove once splicing is enabled.
@@ -6010,45 +6013,47 @@ impl FundingNegotiationContext {
60106013
debug_assert!(matches!(context.channel_state, ChannelState::NegotiatingFunding(_)));
60116014
}
60126015

6013-
// Add output for funding tx
60146016
// Note: For the error case when the inputs are insufficient, it will be handled after
60156017
// the `calculate_change_output_value` call below
6016-
let mut funding_outputs = Vec::new();
60176018

60186019
let shared_funding_output = TxOut {
60196020
value: Amount::from_sat(funding.get_value_satoshis()),
60206021
script_pubkey: funding.get_funding_redeemscript().to_p2wsh(),
60216022
};
60226023

60236024
// Optionally add change output
6024-
if self.our_funding_contribution > SignedAmount::ZERO {
6025-
let change_value_opt = calculate_change_output_value(
6025+
let change_value_opt = if self.our_funding_contribution > SignedAmount::ZERO {
6026+
calculate_change_output_value(
60266027
&self,
60276028
self.shared_funding_input.is_some(),
60286029
&shared_funding_output.script_pubkey,
6029-
&funding_outputs,
60306030
context.holder_dust_limit_satoshis,
6031-
)?;
6032-
if let Some(change_value) = change_value_opt {
6033-
let change_script = if let Some(script) = self.change_script {
6034-
script
6035-
} else {
6036-
signer_provider
6037-
.get_destination_script(context.channel_keys_id)
6038-
.map_err(|_err| AbortReason::InternalError("Error getting change script"))?
6039-
};
6040-
let mut change_output =
6041-
TxOut { value: Amount::from_sat(change_value), script_pubkey: change_script };
6042-
let change_output_weight = get_output_weight(&change_output.script_pubkey).to_wu();
6043-
let change_output_fee =
6044-
fee_for_weight(self.funding_feerate_sat_per_1000_weight, change_output_weight);
6045-
let change_value_decreased_with_fee =
6046-
change_value.saturating_sub(change_output_fee);
6047-
// Check dust limit again
6048-
if change_value_decreased_with_fee > context.holder_dust_limit_satoshis {
6049-
change_output.value = Amount::from_sat(change_value_decreased_with_fee);
6050-
funding_outputs.push(change_output);
6051-
}
6031+
)?
6032+
} else {
6033+
None
6034+
};
6035+
6036+
let mut funding_outputs = self.our_funding_outputs;
6037+
6038+
if let Some(change_value) = change_value_opt {
6039+
let change_script = if let Some(script) = self.change_script {
6040+
script
6041+
} else {
6042+
signer_provider
6043+
.get_destination_script(context.channel_keys_id)
6044+
.map_err(|_err| AbortReason::InternalError("Error getting change script"))?
6045+
};
6046+
let mut change_output =
6047+
TxOut { value: Amount::from_sat(change_value), script_pubkey: change_script };
6048+
let change_output_weight = get_output_weight(&change_output.script_pubkey).to_wu();
6049+
let change_output_fee =
6050+
fee_for_weight(self.funding_feerate_sat_per_1000_weight, change_output_weight);
6051+
let change_value_decreased_with_fee =
6052+
change_value.saturating_sub(change_output_fee);
6053+
// Check dust limit again
6054+
if change_value_decreased_with_fee > context.holder_dust_limit_satoshis {
6055+
change_output.value = Amount::from_sat(change_value_decreased_with_fee);
6056+
funding_outputs.push(change_output);
60526057
}
60536058
}
60546059

@@ -10644,43 +10649,84 @@ where
1064410649
if our_funding_contribution > SignedAmount::MAX_MONEY {
1064510650
return Err(APIError::APIMisuseError {
1064610651
err: format!(
10647-
"Channel {} cannot be spliced; contribution exceeds total bitcoin supply: {}",
10652+
"Channel {} cannot be spliced in; contribution exceeds total bitcoin supply: {}",
1064810653
self.context.channel_id(),
1064910654
our_funding_contribution,
1065010655
),
1065110656
});
1065210657
}
1065310658

10654-
if our_funding_contribution < SignedAmount::ZERO {
10659+
if our_funding_contribution < -SignedAmount::MAX_MONEY {
1065510660
return Err(APIError::APIMisuseError {
1065610661
err: format!(
10657-
"TODO(splicing): Splice-out not supported, only splice in; channel ID {}, contribution {}",
10658-
self.context.channel_id(), our_funding_contribution,
10659-
),
10662+
"Channel {} cannot be spliced out; contribution exceeds total bitcoin supply: {}",
10663+
self.context.channel_id(),
10664+
our_funding_contribution,
10665+
),
1066010666
});
1066110667
}
1066210668

10663-
// TODO(splicing): Once splice-out is supported, check that channel balance does not go below 0
10664-
// (or below channel reserve)
10669+
let funding_inputs = contribution.inputs();
10670+
let funding_outputs = contribution.outputs();
10671+
if !funding_inputs.is_empty() && !funding_outputs.is_empty() {
10672+
return Err(APIError::APIMisuseError {
10673+
err: format!(
10674+
"Channel {} cannot be both spliced in and out; operation not supported",
10675+
self.context.channel_id(),
10676+
),
10677+
});
10678+
}
10679+
10680+
if our_funding_contribution < SignedAmount::ZERO {
10681+
// TODO(splicing): Check that channel balance does not go below the channel reserve
10682+
let post_channel_value = AddSigned::checked_add_signed(
10683+
self.funding.get_value_satoshis(),
10684+
our_funding_contribution.to_sat(),
10685+
);
10686+
// FIXME: Should we check value_to_self instead? Do HTLCs need to be accounted for?
10687+
// FIXME: Check that we can pay for the outputs from the channel value?
10688+
if post_channel_value.is_none() {
10689+
return Err(APIError::APIMisuseError {
10690+
err: format!(
10691+
"Channel {} cannot be spliced out; contribution exceeds the channel value: {}",
10692+
self.context.channel_id(),
10693+
our_funding_contribution,
10694+
),
10695+
});
10696+
}
1066510697

10666-
// Note: post-splice channel value is not yet known at this point, counterparty contribution is not known
10667-
// (Cannot test for miminum required post-splice channel value)
10698+
let value_removed: Amount =
10699+
contribution.outputs().iter().map(|txout| txout.value).sum();
10700+
let negated_value_removed = -value_removed.to_signed().unwrap_or(SignedAmount::MAX);
10701+
if negated_value_removed != our_funding_contribution {
10702+
return Err(APIError::APIMisuseError {
10703+
err: format!(
10704+
"Channel {} cannot be spliced out; unexpected txout amounts: {}",
10705+
self.context.channel_id(),
10706+
value_removed,
10707+
),
10708+
});
10709+
}
10710+
} else {
10711+
// Note: post-splice channel value is not yet known at this point, counterparty contribution is not known
10712+
// (Cannot test for miminum required post-splice channel value)
1066810713

10669-
// Check that inputs are sufficient to cover our contribution.
10670-
let _fee = check_v2_funding_inputs_sufficient(
10671-
our_funding_contribution.to_sat(),
10672-
contribution.inputs(),
10673-
true,
10674-
true,
10675-
funding_feerate_per_kw,
10676-
)
10677-
.map_err(|err| APIError::APIMisuseError {
10678-
err: format!(
10679-
"Insufficient inputs for splicing; channel ID {}, err {}",
10680-
self.context.channel_id(),
10681-
err,
10682-
),
10683-
})?;
10714+
// Check that inputs are sufficient to cover our contribution.
10715+
let _fee = check_v2_funding_inputs_sufficient(
10716+
our_funding_contribution.to_sat(),
10717+
contribution.inputs(),
10718+
true,
10719+
true,
10720+
funding_feerate_per_kw,
10721+
)
10722+
.map_err(|err| APIError::APIMisuseError {
10723+
err: format!(
10724+
"Insufficient inputs for splicing; channel ID {}, err {}",
10725+
self.context.channel_id(),
10726+
err,
10727+
),
10728+
})?;
10729+
}
1068410730

1068510731
for FundingTxInput { txin, prevtx, .. } in contribution.inputs().iter() {
1068610732
const MESSAGE_TEMPLATE: msgs::TxAddInput = msgs::TxAddInput {
@@ -10703,14 +10749,15 @@ where
1070310749
}
1070410750

1070510751
let prev_funding_input = self.funding.to_splice_funding_input();
10706-
let (our_funding_inputs, change_script) = contribution.into_tx_parts();
10752+
let (our_funding_inputs, our_funding_outputs, change_script) = contribution.into_tx_parts();
1070710753
let funding_negotiation_context = FundingNegotiationContext {
1070810754
is_initiator: true,
1070910755
our_funding_contribution,
1071010756
funding_tx_locktime: LockTime::from_consensus(locktime),
1071110757
funding_feerate_sat_per_1000_weight: funding_feerate_per_kw,
1071210758
shared_funding_input: Some(prev_funding_input),
1071310759
our_funding_inputs,
10760+
our_funding_outputs,
1071410761
change_script,
1071510762
};
1071610763

@@ -10833,6 +10880,7 @@ where
1083310880
funding_feerate_sat_per_1000_weight: msg.funding_feerate_per_kw,
1083410881
shared_funding_input: Some(prev_funding_input),
1083510882
our_funding_inputs: Vec::new(),
10883+
our_funding_outputs: Vec::new(),
1083610884
change_script: None,
1083710885
};
1083810886

@@ -12531,6 +12579,7 @@ where
1253112579
funding_feerate_sat_per_1000_weight,
1253212580
shared_funding_input: None,
1253312581
our_funding_inputs: funding_inputs,
12582+
our_funding_outputs: Vec::new(),
1253412583
change_script: None,
1253512584
};
1253612585
let chan = Self {
@@ -12685,6 +12734,7 @@ where
1268512734
funding_feerate_sat_per_1000_weight: msg.funding_feerate_sat_per_1000_weight,
1268612735
shared_funding_input: None,
1268712736
our_funding_inputs: our_funding_inputs.clone(),
12737+
our_funding_outputs: Vec::new(),
1268812738
change_script: None,
1268912739
};
1269012740
let shared_funding_output = TxOut {
@@ -12708,7 +12758,7 @@ where
1270812758
inputs_to_contribute,
1270912759
shared_funding_input: None,
1271012760
shared_funding_output: SharedOwnedOutput::new(shared_funding_output, our_funding_contribution_sats),
12711-
outputs_to_contribute: Vec::new(),
12761+
outputs_to_contribute: funding_negotiation_context.our_funding_outputs.clone(),
1271212762
}
1271312763
).map_err(|err| {
1271412764
let reason = ClosureReason::ProcessingError { err: err.to_string() };

lightning/src/ln/channelmanager.rs

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ use bitcoin::secp256k1::Secp256k1;
3232
use bitcoin::secp256k1::{PublicKey, SecretKey};
3333
use bitcoin::{secp256k1, Sequence, SignedAmount, TxIn, Weight};
3434
#[cfg(splicing)]
35-
use bitcoin::{Amount, ScriptBuf};
35+
use bitcoin::{Amount, ScriptBuf, TxOut};
3636

3737
use crate::blinded_path::message::MessageForwardNode;
3838
use crate::blinded_path::message::{AsyncPaymentsContext, OffersContext};
@@ -216,6 +216,14 @@ pub enum SpliceContribution {
216216
/// using `SignerProvider::get_destination_script`.
217217
change_script: Option<ScriptBuf>,
218218
},
219+
/// When only outputs are contributed to then funding transaction.
220+
SpliceOut {
221+
/// The amount to remove from the channel.
222+
value: Amount,
223+
/// The outputs used for removing the amount. The total value of all outputs must equal
224+
/// [`SpliceOut::value`].
225+
outputs: Vec<TxOut>,
226+
},
219227
}
220228

221229
#[cfg(splicing)]
@@ -225,18 +233,32 @@ impl SpliceContribution {
225233
SpliceContribution::SpliceIn { value, .. } => {
226234
value.to_signed().unwrap_or(SignedAmount::MAX)
227235
},
236+
SpliceContribution::SpliceOut { value, .. } => {
237+
value.to_signed().map(|value| -value).unwrap_or(SignedAmount::MIN)
238+
},
228239
}
229240
}
230241

231242
pub(super) fn inputs(&self) -> &[FundingTxInput] {
232243
match self {
233244
SpliceContribution::SpliceIn { inputs, .. } => &inputs[..],
245+
SpliceContribution::SpliceOut { .. } => &[],
246+
}
247+
}
248+
249+
pub(super) fn outputs(&self) -> &[TxOut] {
250+
match self {
251+
SpliceContribution::SpliceIn { .. } => &[],
252+
SpliceContribution::SpliceOut { outputs, .. } => &outputs[..],
234253
}
235254
}
236255

237-
pub(super) fn into_tx_parts(self) -> (Vec<FundingTxInput>, Option<ScriptBuf>) {
256+
pub(super) fn into_tx_parts(self) -> (Vec<FundingTxInput>, Vec<TxOut>, Option<ScriptBuf>) {
238257
match self {
239-
SpliceContribution::SpliceIn { inputs, change_script, .. } => (inputs, change_script),
258+
SpliceContribution::SpliceIn { inputs, change_script, .. } => {
259+
(inputs, vec![], change_script)
260+
},
261+
SpliceContribution::SpliceOut { outputs, .. } => (vec![], outputs, None),
240262
}
241263
}
242264
}

lightning/src/ln/interactivetxs.rs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1878,7 +1878,7 @@ impl InteractiveTxConstructor {
18781878
/// - `change_output_dust_limit` - The dust limit (in sats) to consider.
18791879
pub(super) fn calculate_change_output_value(
18801880
context: &FundingNegotiationContext, is_splice: bool, shared_output_funding_script: &ScriptBuf,
1881-
funding_outputs: &Vec<TxOut>, change_output_dust_limit: u64,
1881+
change_output_dust_limit: u64,
18821882
) -> Result<Option<u64>, AbortReason> {
18831883
assert!(context.our_funding_contribution > SignedAmount::ZERO);
18841884
let our_funding_contribution_satoshis = context.our_funding_contribution.to_sat() as u64;
@@ -1900,6 +1900,7 @@ pub(super) fn calculate_change_output_value(
19001900
our_funding_inputs_weight = our_funding_inputs_weight.saturating_add(weight);
19011901
}
19021902

1903+
let funding_outputs = &context.our_funding_outputs;
19031904
let total_output_satoshis =
19041905
funding_outputs.iter().fold(0u64, |total, out| total.saturating_add(out.value.to_sat()));
19051906
let our_funding_outputs_weight = funding_outputs.iter().fold(0u64, |weight, out| {
@@ -2993,17 +2994,18 @@ mod tests {
29932994
funding_feerate_sat_per_1000_weight,
29942995
shared_funding_input: None,
29952996
our_funding_inputs: inputs,
2997+
our_funding_outputs: outputs,
29962998
change_script: None,
29972999
};
29983000
assert_eq!(
2999-
calculate_change_output_value(&context, false, &ScriptBuf::new(), &outputs, 300),
3001+
calculate_change_output_value(&context, false, &ScriptBuf::new(), 300),
30003002
Ok(Some(gross_change - fees - common_fees)),
30013003
);
30023004

30033005
// There is leftover for change, without common fees
30043006
let context = FundingNegotiationContext { is_initiator: false, ..context };
30053007
assert_eq!(
3006-
calculate_change_output_value(&context, false, &ScriptBuf::new(), &outputs, 300),
3008+
calculate_change_output_value(&context, false, &ScriptBuf::new(), 300),
30073009
Ok(Some(gross_change - fees)),
30083010
);
30093011

@@ -3014,7 +3016,7 @@ mod tests {
30143016
..context
30153017
};
30163018
assert_eq!(
3017-
calculate_change_output_value(&context, false, &ScriptBuf::new(), &outputs, 300),
3019+
calculate_change_output_value(&context, false, &ScriptBuf::new(), 300),
30183020
Err(AbortReason::InsufficientFees),
30193021
);
30203022

@@ -3025,7 +3027,7 @@ mod tests {
30253027
..context
30263028
};
30273029
assert_eq!(
3028-
calculate_change_output_value(&context, false, &ScriptBuf::new(), &outputs, 300),
3030+
calculate_change_output_value(&context, false, &ScriptBuf::new(), 300),
30293031
Ok(None),
30303032
);
30313033

@@ -3036,7 +3038,7 @@ mod tests {
30363038
..context
30373039
};
30383040
assert_eq!(
3039-
calculate_change_output_value(&context, false, &ScriptBuf::new(), &outputs, 100),
3041+
calculate_change_output_value(&context, false, &ScriptBuf::new(), 100),
30403042
Ok(Some(262)),
30413043
);
30423044

@@ -3048,7 +3050,7 @@ mod tests {
30483050
..context
30493051
};
30503052
assert_eq!(
3051-
calculate_change_output_value(&context, false, &ScriptBuf::new(), &outputs, 300),
3053+
calculate_change_output_value(&context, false, &ScriptBuf::new(), 300),
30523054
Ok(Some(4060)),
30533055
);
30543056
}

0 commit comments

Comments
 (0)