Skip to content

Commit acab158

Browse files
committed
Add splice-out support
Update FundingTxContributions 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 318660f commit acab158

File tree

2 files changed

+137
-43
lines changed

2 files changed

+137
-43
lines changed

lightning/src/ln/channel.rs

Lines changed: 125 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6001,10 +6001,8 @@ impl FundingNegotiationContext {
60016001
debug_assert!(matches!(context.channel_state, ChannelState::NegotiatingFunding(_)));
60026002
}
60036003

6004-
// Add output for funding tx
60056004
// Note: For the error case when the inputs are insufficient, it will be handled after
60066005
// the `calculate_change_output_value` call below
6007-
let mut funding_outputs = Vec::new();
60086006

60096007
let shared_funding_output = TxOut {
60106008
value: Amount::from_sat(funding.get_value_satoshis()),
@@ -6016,18 +6014,27 @@ impl FundingNegotiationContext {
60166014
&self,
60176015
self.shared_funding_input.is_some(),
60186016
&shared_funding_output.script_pubkey,
6019-
&funding_outputs,
60206017
context.holder_dust_limit_satoshis,
60216018
)?
60226019
} else {
60236020
None
60246021
};
60256022

6026-
let (inputs_to_contribute, change_script) = match self.funding_tx_contributions {
6027-
FundingTxContributions::InputsOnly { inputs, change_script } => {
6028-
(inputs.into_iter().map(|(txin, tx, _)| (txin, tx)).collect(), change_script)
6029-
},
6030-
};
6023+
let (funding_inputs, mut funding_outputs, change_script) =
6024+
match self.funding_tx_contributions {
6025+
FundingTxContributions::InputsOnly { inputs, change_script } => (
6026+
inputs.into_iter().map(|(txin, tx, _)| (txin, tx)).collect(),
6027+
vec![],
6028+
change_script,
6029+
),
6030+
FundingTxContributions::OutputsOnly { outputs } => (vec![], outputs, None),
6031+
#[cfg(test)]
6032+
FundingTxContributions::InputsAndOutputs { inputs, outputs, change_script } => (
6033+
inputs.into_iter().map(|(txin, tx, _)| (txin, tx)).collect(),
6034+
outputs,
6035+
change_script,
6036+
),
6037+
};
60316038

60326039
// Add change output if necessary
60336040
if let Some(change_value) = change_value_opt {
@@ -6061,7 +6068,7 @@ impl FundingNegotiationContext {
60616068
feerate_sat_per_kw: self.funding_feerate_sat_per_1000_weight,
60626069
is_initiator: self.is_initiator,
60636070
funding_tx_locktime: self.funding_tx_locktime,
6064-
inputs_to_contribute,
6071+
inputs_to_contribute: funding_inputs,
60656072
shared_funding_input: self.shared_funding_input,
60666073
shared_funding_output: SharedOwnedOutput::new(
60676074
shared_funding_output,
@@ -6082,6 +6089,26 @@ pub enum FundingTxContributions {
60826089
/// change output.
60836090
inputs: Vec<(TxIn, Transaction, Weight)>,
60846091

6092+
/// An optional change output script. This will be used if needed or, if not set, generated
6093+
/// using `SignerProvider::get_destination_script`.
6094+
change_script: Option<ScriptBuf>,
6095+
},
6096+
/// When only outputs are contributed to then funding transaction. This must correspond to a
6097+
/// negative contribution amount.
6098+
OutputsOnly {
6099+
/// The outputs used for removing an amount.
6100+
outputs: Vec<TxOut>,
6101+
},
6102+
/// When both inputs and outputs are contributed to the funding transaction.
6103+
#[cfg(test)]
6104+
InputsAndOutputs {
6105+
/// The inputs used to meet the contributed amount. Any excess amount will be sent to a
6106+
/// change output.
6107+
inputs: Vec<(TxIn, Transaction, Weight)>,
6108+
6109+
/// The outputs used for removing an amount.
6110+
outputs: Vec<TxOut>,
6111+
60856112
/// An optional change output script. This will be used if needed or, if not set, generated
60866113
/// using `SignerProvider::get_destination_script`.
60876114
change_script: Option<ScriptBuf>,
@@ -6093,8 +6120,26 @@ impl FundingTxContributions {
60936120
pub fn inputs(&self) -> &[(TxIn, Transaction, Weight)] {
60946121
match self {
60956122
FundingTxContributions::InputsOnly { inputs, .. } => &inputs[..],
6123+
FundingTxContributions::OutputsOnly { .. } => &[],
6124+
#[cfg(test)]
6125+
FundingTxContributions::InputsAndOutputs { inputs, .. } => &inputs[..],
60966126
}
60976127
}
6128+
6129+
/// Returns an inputs to be contributed to the funding transaction.
6130+
pub fn outputs(&self) -> &[TxOut] {
6131+
match self {
6132+
FundingTxContributions::InputsOnly { .. } => &[],
6133+
FundingTxContributions::OutputsOnly { outputs } => &outputs[..],
6134+
#[cfg(test)]
6135+
FundingTxContributions::InputsAndOutputs { outputs, .. } => &outputs[..],
6136+
}
6137+
}
6138+
6139+
/// Returns the sum of the output amounts.
6140+
pub fn amount_removed(&self) -> Amount {
6141+
self.outputs().iter().map(|txout| txout.value).sum()
6142+
}
60986143
}
60996144

61006145
// Holder designates channel data owned for the benefit of the user client.
@@ -10665,43 +10710,90 @@ where
1066510710
if our_funding_contribution > SignedAmount::MAX_MONEY {
1066610711
return Err(APIError::APIMisuseError {
1066710712
err: format!(
10668-
"Channel {} cannot be spliced; contribution exceeds total bitcoin supply: {}",
10713+
"Channel {} cannot be spliced in; contribution exceeds total bitcoin supply: {}",
1066910714
self.context.channel_id(),
1067010715
our_funding_contribution,
1067110716
),
1067210717
});
1067310718
}
1067410719

10675-
if our_funding_contribution < SignedAmount::ZERO {
10720+
if our_funding_contribution < -SignedAmount::MAX_MONEY {
1067610721
return Err(APIError::APIMisuseError {
1067710722
err: format!(
10678-
"TODO(splicing): Splice-out not supported, only splice in; channel ID {}, contribution {}",
10679-
self.context.channel_id(), our_funding_contribution,
10680-
),
10723+
"Channel {} cannot be spliced out; contribution exceeds total bitcoin supply: {}",
10724+
self.context.channel_id(),
10725+
our_funding_contribution,
10726+
),
10727+
});
10728+
}
10729+
10730+
let funding_inputs = funding_tx_contributions.inputs();
10731+
let funding_outputs = funding_tx_contributions.outputs();
10732+
if !funding_inputs.is_empty() && !funding_outputs.is_empty() {
10733+
return Err(APIError::APIMisuseError {
10734+
err: format!(
10735+
"Channel {} cannot be both spliced in and out; operation not supported",
10736+
self.context.channel_id(),
10737+
),
1068110738
});
1068210739
}
1068310740

10684-
// TODO(splicing): Once splice-out is supported, check that channel balance does not go below 0
10685-
// (or below channel reserve)
10741+
if our_funding_contribution < SignedAmount::ZERO {
10742+
// TODO(splicing): Check that channel balance does not go below the channel reserve
10743+
let post_channel_value = AddSigned::checked_add_signed(
10744+
self.funding.get_value_satoshis(),
10745+
our_funding_contribution_satoshis,
10746+
);
10747+
// FIXME: Should we check value_to_self instead? Do HTLCs need to be accounted for?
10748+
// FIXME: Check that we can pay for the outputs from the channel value?
10749+
if post_channel_value.is_none() {
10750+
return Err(APIError::APIMisuseError {
10751+
err: format!(
10752+
"Channel {} cannot be spliced out; contribution exceeds the channel value: {}",
10753+
self.context.channel_id(),
10754+
our_funding_contribution,
10755+
),
10756+
});
10757+
}
1068610758

10687-
// Note: post-splice channel value is not yet known at this point, counterparty contribution is not known
10688-
// (Cannot test for miminum required post-splice channel value)
10759+
let amount_removed =
10760+
funding_tx_contributions.amount_removed().to_signed().map_err(|_| {
10761+
APIError::APIMisuseError {
10762+
err: format!(
10763+
"Channel {} cannot be spliced out; txout amounts invalid",
10764+
self.context.channel_id(),
10765+
),
10766+
}
10767+
})?;
10768+
if -amount_removed != our_funding_contribution {
10769+
return Err(APIError::APIMisuseError {
10770+
err: format!(
10771+
"Channel {} cannot be spliced out; unexpected txout amounts: {}",
10772+
self.context.channel_id(),
10773+
amount_removed,
10774+
),
10775+
});
10776+
}
10777+
} else {
10778+
// Note: post-splice channel value is not yet known at this point, counterparty contribution is not known
10779+
// (Cannot test for miminum required post-splice channel value)
1068910780

10690-
// Check that inputs are sufficient to cover our contribution.
10691-
let _fee = check_v2_funding_inputs_sufficient(
10692-
our_funding_contribution.to_sat(),
10693-
funding_tx_contributions.inputs(),
10694-
true,
10695-
true,
10696-
funding_feerate_per_kw,
10697-
)
10698-
.map_err(|err| APIError::APIMisuseError {
10699-
err: format!(
10700-
"Insufficient inputs for splicing; channel ID {}, err {}",
10701-
self.context.channel_id(),
10702-
err,
10703-
),
10704-
})?;
10781+
// Check that inputs are sufficient to cover our contribution.
10782+
let _fee = check_v2_funding_inputs_sufficient(
10783+
our_funding_contribution.to_sat(),
10784+
funding_tx_contributions.inputs(),
10785+
true,
10786+
true,
10787+
funding_feerate_per_kw,
10788+
)
10789+
.map_err(|err| APIError::APIMisuseError {
10790+
err: format!(
10791+
"Insufficient inputs for splicing; channel ID {}, err {}",
10792+
self.context.channel_id(),
10793+
err,
10794+
),
10795+
})?;
10796+
}
1070510797

1070610798
for (_, tx, _) in funding_tx_contributions.inputs().iter() {
1070710799
const MESSAGE_TEMPLATE: msgs::TxAddInput = msgs::TxAddInput {

lightning/src/ln/interactivetxs.rs

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

1902+
let funding_outputs = context.funding_tx_contributions.outputs();
19021903
let total_output_satoshis =
19031904
funding_outputs.iter().fold(0u64, |total, out| total.saturating_add(out.value.to_sat()));
19041905
let our_funding_outputs_weight = funding_outputs.iter().fold(0u64, |weight, out| {
@@ -2974,15 +2975,16 @@ mod tests {
29742975
(txin, tx, weight)
29752976
})
29762977
.collect::<Vec<(TxIn, Transaction, Weight)>>();
2977-
let funding_tx_contributions =
2978-
FundingTxContributions::InputsOnly { inputs, change_script: None };
29792978
let our_contributed = 110_000;
29802979
let txout = TxOut { value: Amount::from_sat(10_000), script_pubkey: ScriptBuf::new() };
29812980
let outputs = vec![txout];
2981+
let funding_tx_contributions =
2982+
FundingTxContributions::InputsAndOutputs { inputs, outputs, change_script: None };
29822983
let funding_feerate_sat_per_1000_weight = 3000;
29832984

29842985
let total_inputs: u64 = input_prevouts.iter().map(|o| o.value.to_sat()).sum();
2985-
let total_outputs: u64 = outputs.iter().map(|o| o.value.to_sat()).sum();
2986+
let total_outputs: u64 =
2987+
funding_tx_contributions.outputs().iter().map(|o| o.value.to_sat()).sum();
29862988
let gross_change = total_inputs - total_outputs - our_contributed;
29872989
let fees = 1746;
29882990
let common_fees = 234;
@@ -2997,14 +2999,14 @@ mod tests {
29972999
funding_tx_contributions,
29983000
};
29993001
assert_eq!(
3000-
calculate_change_output_value(&context, false, &ScriptBuf::new(), &outputs, 300),
3002+
calculate_change_output_value(&context, false, &ScriptBuf::new(), 300),
30013003
Ok(Some(gross_change - fees - common_fees)),
30023004
);
30033005

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

@@ -3015,7 +3017,7 @@ mod tests {
30153017
..context
30163018
};
30173019
assert_eq!(
3018-
calculate_change_output_value(&context, false, &ScriptBuf::new(), &outputs, 300),
3020+
calculate_change_output_value(&context, false, &ScriptBuf::new(), 300),
30193021
Err(AbortReason::InsufficientFees),
30203022
);
30213023

@@ -3026,7 +3028,7 @@ mod tests {
30263028
..context
30273029
};
30283030
assert_eq!(
3029-
calculate_change_output_value(&context, false, &ScriptBuf::new(), &outputs, 300),
3031+
calculate_change_output_value(&context, false, &ScriptBuf::new(), 300),
30303032
Ok(None),
30313033
);
30323034

@@ -3037,7 +3039,7 @@ mod tests {
30373039
..context
30383040
};
30393041
assert_eq!(
3040-
calculate_change_output_value(&context, false, &ScriptBuf::new(), &outputs, 100),
3042+
calculate_change_output_value(&context, false, &ScriptBuf::new(), 100),
30413043
Ok(Some(262)),
30423044
);
30433045

@@ -3049,7 +3051,7 @@ mod tests {
30493051
..context
30503052
};
30513053
assert_eq!(
3052-
calculate_change_output_value(&context, false, &ScriptBuf::new(), &outputs, 300),
3054+
calculate_change_output_value(&context, false, &ScriptBuf::new(), 300),
30533055
Ok(Some(4060)),
30543056
);
30553057
}

0 commit comments

Comments
 (0)