Skip to content

Commit 31e2021

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 2236852 commit 31e2021

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

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

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

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

@@ -10638,43 +10643,84 @@ where
1063810643
if our_funding_contribution > SignedAmount::MAX_MONEY {
1063910644
return Err(APIError::APIMisuseError {
1064010645
err: format!(
10641-
"Channel {} cannot be spliced; contribution exceeds total bitcoin supply: {}",
10646+
"Channel {} cannot be spliced in; contribution exceeds total bitcoin supply: {}",
1064210647
self.context.channel_id(),
1064310648
our_funding_contribution,
1064410649
),
1064510650
});
1064610651
}
1064710652

10648-
if our_funding_contribution < SignedAmount::ZERO {
10653+
if our_funding_contribution < -SignedAmount::MAX_MONEY {
1064910654
return Err(APIError::APIMisuseError {
1065010655
err: format!(
10651-
"TODO(splicing): Splice-out not supported, only splice in; channel ID {}, contribution {}",
10652-
self.context.channel_id(), our_funding_contribution,
10653-
),
10656+
"Channel {} cannot be spliced out; contribution exceeds total bitcoin supply: {}",
10657+
self.context.channel_id(),
10658+
our_funding_contribution,
10659+
),
1065410660
});
1065510661
}
1065610662

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

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

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,
10670-
)
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-
),
10677-
})?;
10708+
// Check that inputs are sufficient to cover our contribution.
10709+
let _fee = check_v2_funding_inputs_sufficient(
10710+
our_funding_contribution.to_sat(),
10711+
contribution.inputs(),
10712+
true,
10713+
true,
10714+
funding_feerate_per_kw,
10715+
)
10716+
.map_err(|err| APIError::APIMisuseError {
10717+
err: format!(
10718+
"Insufficient inputs for splicing; channel ID {}, err {}",
10719+
self.context.channel_id(),
10720+
err,
10721+
),
10722+
})?;
10723+
}
1067810724

1067910725
for FundingTxInput { txin, prevtx, .. } in contribution.inputs().iter() {
1068010726
const MESSAGE_TEMPLATE: msgs::TxAddInput = msgs::TxAddInput {
@@ -10697,14 +10743,15 @@ where
1069710743
}
1069810744

1069910745
let prev_funding_input = self.funding.to_splice_funding_input();
10700-
let (our_funding_inputs, change_script) = contribution.into_tx_parts();
10746+
let (our_funding_inputs, our_funding_outputs, change_script) = contribution.into_tx_parts();
1070110747
let funding_negotiation_context = FundingNegotiationContext {
1070210748
is_initiator: true,
1070310749
our_funding_contribution,
1070410750
funding_tx_locktime: LockTime::from_consensus(locktime),
1070510751
funding_feerate_sat_per_1000_weight: funding_feerate_per_kw,
1070610752
shared_funding_input: Some(prev_funding_input),
1070710753
our_funding_inputs,
10754+
our_funding_outputs,
1070810755
change_script,
1070910756
};
1071010757

@@ -10827,6 +10874,7 @@ where
1082710874
funding_feerate_sat_per_1000_weight: msg.funding_feerate_per_kw,
1082810875
shared_funding_input: Some(prev_funding_input),
1082910876
our_funding_inputs: Vec::new(),
10877+
our_funding_outputs: Vec::new(),
1083010878
change_script: None,
1083110879
};
1083210880

@@ -12525,6 +12573,7 @@ where
1252512573
funding_feerate_sat_per_1000_weight,
1252612574
shared_funding_input: None,
1252712575
our_funding_inputs: funding_inputs,
12576+
our_funding_outputs: Vec::new(),
1252812577
change_script: None,
1252912578
};
1253012579
let chan = Self {
@@ -12679,6 +12728,7 @@ where
1267912728
funding_feerate_sat_per_1000_weight: msg.funding_feerate_sat_per_1000_weight,
1268012729
shared_funding_input: None,
1268112730
our_funding_inputs: our_funding_inputs.clone(),
12731+
our_funding_outputs: Vec::new(),
1268212732
change_script: None,
1268312733
};
1268412734
let shared_funding_output = TxOut {
@@ -12702,7 +12752,7 @@ where
1270212752
inputs_to_contribute,
1270312753
shared_funding_input: None,
1270412754
shared_funding_output: SharedOwnedOutput::new(shared_funding_output, our_funding_contribution_sats),
12705-
outputs_to_contribute: Vec::new(),
12755+
outputs_to_contribute: funding_negotiation_context.our_funding_outputs.clone(),
1270612756
}
1270712757
).map_err(|err| {
1270812758
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
@@ -2069,7 +2069,7 @@ impl InteractiveTxConstructor {
20692069
/// - `change_output_dust_limit` - The dust limit (in sats) to consider.
20702070
pub(super) fn calculate_change_output_value(
20712071
context: &FundingNegotiationContext, is_splice: bool, shared_output_funding_script: &ScriptBuf,
2072-
funding_outputs: &Vec<TxOut>, change_output_dust_limit: u64,
2072+
change_output_dust_limit: u64,
20732073
) -> Result<Option<u64>, AbortReason> {
20742074
assert!(context.our_funding_contribution > SignedAmount::ZERO);
20752075
let our_funding_contribution_satoshis = context.our_funding_contribution.to_sat() as u64;
@@ -2091,6 +2091,7 @@ pub(super) fn calculate_change_output_value(
20912091
our_funding_inputs_weight = our_funding_inputs_weight.saturating_add(weight);
20922092
}
20932093

2094+
let funding_outputs = &context.our_funding_outputs;
20942095
let total_output_satoshis =
20952096
funding_outputs.iter().fold(0u64, |total, out| total.saturating_add(out.value.to_sat()));
20962097
let our_funding_outputs_weight = funding_outputs.iter().fold(0u64, |weight, out| {
@@ -3187,17 +3188,18 @@ mod tests {
31873188
funding_feerate_sat_per_1000_weight,
31883189
shared_funding_input: None,
31893190
our_funding_inputs: inputs,
3191+
our_funding_outputs: outputs,
31903192
change_script: None,
31913193
};
31923194
assert_eq!(
3193-
calculate_change_output_value(&context, false, &ScriptBuf::new(), &outputs, 300),
3195+
calculate_change_output_value(&context, false, &ScriptBuf::new(), 300),
31943196
Ok(Some(gross_change - fees - common_fees)),
31953197
);
31963198

31973199
// There is leftover for change, without common fees
31983200
let context = FundingNegotiationContext { is_initiator: false, ..context };
31993201
assert_eq!(
3200-
calculate_change_output_value(&context, false, &ScriptBuf::new(), &outputs, 300),
3202+
calculate_change_output_value(&context, false, &ScriptBuf::new(), 300),
32013203
Ok(Some(gross_change - fees)),
32023204
);
32033205

@@ -3208,7 +3210,7 @@ mod tests {
32083210
..context
32093211
};
32103212
assert_eq!(
3211-
calculate_change_output_value(&context, false, &ScriptBuf::new(), &outputs, 300),
3213+
calculate_change_output_value(&context, false, &ScriptBuf::new(), 300),
32123214
Err(AbortReason::InsufficientFees),
32133215
);
32143216

@@ -3219,7 +3221,7 @@ mod tests {
32193221
..context
32203222
};
32213223
assert_eq!(
3222-
calculate_change_output_value(&context, false, &ScriptBuf::new(), &outputs, 300),
3224+
calculate_change_output_value(&context, false, &ScriptBuf::new(), 300),
32233225
Ok(None),
32243226
);
32253227

@@ -3230,7 +3232,7 @@ mod tests {
32303232
..context
32313233
};
32323234
assert_eq!(
3233-
calculate_change_output_value(&context, false, &ScriptBuf::new(), &outputs, 100),
3235+
calculate_change_output_value(&context, false, &ScriptBuf::new(), 100),
32343236
Ok(Some(262)),
32353237
);
32363238

@@ -3242,7 +3244,7 @@ mod tests {
32423244
..context
32433245
};
32443246
assert_eq!(
3245-
calculate_change_output_value(&context, false, &ScriptBuf::new(), &outputs, 300),
3247+
calculate_change_output_value(&context, false, &ScriptBuf::new(), 300),
32463248
Ok(Some(4060)),
32473249
);
32483250
}

0 commit comments

Comments
 (0)