Skip to content

Commit 2b57d4c

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 009ffa2 commit 2b57d4c

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
@@ -5983,6 +5983,9 @@ pub(super) struct FundingNegotiationContext {
59835983
/// The funding inputs we will be contributing to the channel.
59845984
#[allow(dead_code)] // TODO(dual_funding): Remove once contribution to V2 channels is enabled.
59855985
pub our_funding_inputs: Vec<FundingTxInput>,
5986+
/// The funding outputs we will be contributing to the channel.
5987+
#[allow(dead_code)] // TODO(dual_funding): Remove once contribution to V2 channels is enabled.
5988+
pub our_funding_outputs: Vec<TxOut>,
59865989
/// The change output script. This will be used if needed or -- if not set -- generated using
59875990
/// `SignerProvider::get_destination_script`.
59885991
#[allow(dead_code)] // TODO(splicing): Remove once splicing is enabled.
@@ -6012,45 +6015,47 @@ impl FundingNegotiationContext {
60126015
debug_assert!(matches!(context.channel_state, ChannelState::NegotiatingFunding(_)));
60136016
}
60146017

6015-
// Add output for funding tx
60166018
// Note: For the error case when the inputs are insufficient, it will be handled after
60176019
// the `calculate_change_output_value` call below
6018-
let mut funding_outputs = Vec::new();
60196020

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

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

@@ -10646,43 +10651,84 @@ where
1064610651
if our_funding_contribution > SignedAmount::MAX_MONEY {
1064710652
return Err(APIError::APIMisuseError {
1064810653
err: format!(
10649-
"Channel {} cannot be spliced; contribution exceeds total bitcoin supply: {}",
10654+
"Channel {} cannot be spliced in; contribution exceeds total bitcoin supply: {}",
1065010655
self.context.channel_id(),
1065110656
our_funding_contribution,
1065210657
),
1065310658
});
1065410659
}
1065510660

10656-
if our_funding_contribution < SignedAmount::ZERO {
10661+
if our_funding_contribution < -SignedAmount::MAX_MONEY {
1065710662
return Err(APIError::APIMisuseError {
1065810663
err: format!(
10659-
"TODO(splicing): Splice-out not supported, only splice in; channel ID {}, contribution {}",
10660-
self.context.channel_id(), our_funding_contribution,
10661-
),
10664+
"Channel {} cannot be spliced out; contribution exceeds total bitcoin supply: {}",
10665+
self.context.channel_id(),
10666+
our_funding_contribution,
10667+
),
1066210668
});
1066310669
}
1066410670

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

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

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

1068710733
for FundingTxInput { txin, prevtx, .. } in contribution.inputs().iter() {
1068810734
const MESSAGE_TEMPLATE: msgs::TxAddInput = msgs::TxAddInput {
@@ -10705,14 +10751,15 @@ where
1070510751
}
1070610752

1070710753
let prev_funding_input = self.funding.to_splice_funding_input();
10708-
let (our_funding_inputs, change_script) = contribution.into_tx_parts();
10754+
let (our_funding_inputs, our_funding_outputs, change_script) = contribution.into_tx_parts();
1070910755
let funding_negotiation_context = FundingNegotiationContext {
1071010756
is_initiator: true,
1071110757
our_funding_contribution,
1071210758
funding_tx_locktime: LockTime::from_consensus(locktime),
1071310759
funding_feerate_sat_per_1000_weight: funding_feerate_per_kw,
1071410760
shared_funding_input: Some(prev_funding_input),
1071510761
our_funding_inputs,
10762+
our_funding_outputs,
1071610763
change_script,
1071710764
};
1071810765

@@ -10835,6 +10882,7 @@ where
1083510882
funding_feerate_sat_per_1000_weight: msg.funding_feerate_per_kw,
1083610883
shared_funding_input: Some(prev_funding_input),
1083710884
our_funding_inputs: Vec::new(),
10885+
our_funding_outputs: Vec::new(),
1083810886
change_script: None,
1083910887
};
1084010888

@@ -12533,6 +12581,7 @@ where
1253312581
funding_feerate_sat_per_1000_weight,
1253412582
shared_funding_input: None,
1253512583
our_funding_inputs: funding_inputs,
12584+
our_funding_outputs: Vec::new(),
1253612585
change_script: None,
1253712586
};
1253812587
let chan = Self {
@@ -12687,6 +12736,7 @@ where
1268712736
funding_feerate_sat_per_1000_weight: msg.funding_feerate_sat_per_1000_weight,
1268812737
shared_funding_input: None,
1268912738
our_funding_inputs: our_funding_inputs.clone(),
12739+
our_funding_outputs: Vec::new(),
1269012740
change_script: None,
1269112741
};
1269212742
let shared_funding_output = TxOut {
@@ -12710,7 +12760,7 @@ where
1271012760
inputs_to_contribute,
1271112761
shared_funding_input: None,
1271212762
shared_funding_output: SharedOwnedOutput::new(shared_funding_output, our_funding_contribution_sats),
12713-
outputs_to_contribute: Vec::new(),
12763+
outputs_to_contribute: funding_negotiation_context.our_funding_outputs.clone(),
1271412764
}
1271512765
).map_err(|err| {
1271612766
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
/// generated 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)