Skip to content

Commit 2d39059

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 6f5828d commit 2d39059

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
@@ -6002,10 +6002,8 @@ impl FundingNegotiationContext {
60026002
debug_assert!(matches!(context.channel_state, ChannelState::NegotiatingFunding(_)));
60036003
}
60046004

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

60106008
let shared_funding_output = TxOut {
60116009
value: Amount::from_sat(funding.get_value_satoshis()),
@@ -6017,18 +6015,27 @@ impl FundingNegotiationContext {
60176015
&self,
60186016
self.shared_funding_input.is_some(),
60196017
&shared_funding_output.script_pubkey,
6020-
&funding_outputs,
60216018
context.holder_dust_limit_satoshis,
60226019
)?
60236020
} else {
60246021
None
60256022
};
60266023

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

60336040
// Add change output if necessary
60346041
if let Some(change_value) = change_value_opt {
@@ -6062,7 +6069,7 @@ impl FundingNegotiationContext {
60626069
feerate_sat_per_kw: self.funding_feerate_sat_per_1000_weight,
60636070
is_initiator: self.is_initiator,
60646071
funding_tx_locktime: self.funding_tx_locktime,
6065-
inputs_to_contribute,
6072+
inputs_to_contribute: funding_inputs,
60666073
shared_funding_input: self.shared_funding_input,
60676074
shared_funding_output: SharedOwnedOutput::new(
60686075
shared_funding_output,
@@ -6083,6 +6090,26 @@ pub enum FundingTxContributions {
60836090
/// change output.
60846091
inputs: Vec<(TxIn, Transaction, Weight)>,
60856092

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

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

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

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

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

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

1070710799
for (_, tx, _) in funding_tx_contributions.inputs().iter() {
1070810800
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)