Skip to content

Commit 8eb9e70

Browse files
committed
Mixed mode splicing
Some splicing use cases require to simultaneously splice in and out in the same splice transaction. Add support for such splices using the funding inputs to pay the appropriate fees just like the splice-in case, opposed to using the channel value like the splice-out case. This requires using the contributed input value when checking if the inputs are sufficient to cover fees, not the net contributed value. The latter may be negative in the net splice-out case.
1 parent e58cfbc commit 8eb9e70

File tree

4 files changed

+321
-56
lines changed

4 files changed

+321
-56
lines changed

lightning/src/ln/channel.rs

Lines changed: 114 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -6501,8 +6501,7 @@ fn get_v2_channel_reserve_satoshis(channel_value_satoshis: u64, dust_limit_satos
65016501
fn check_splice_contribution_sufficient(
65026502
contribution: &SpliceContribution, is_initiator: bool, funding_feerate: FeeRate,
65036503
) -> Result<SignedAmount, String> {
6504-
let contribution_amount = contribution.value();
6505-
if contribution_amount < SignedAmount::ZERO {
6504+
if contribution.inputs().is_empty() {
65066505
let estimated_fee = Amount::from_sat(estimate_v2_funding_transaction_fee(
65076506
contribution.inputs(),
65086507
contribution.outputs(),
@@ -6511,20 +6510,25 @@ fn check_splice_contribution_sufficient(
65116510
funding_feerate.to_sat_per_kwu() as u32,
65126511
));
65136512

6513+
let contribution_amount = contribution.net_value();
65146514
contribution_amount
65156515
.checked_sub(
65166516
estimated_fee.to_signed().expect("fees should never exceed Amount::MAX_MONEY"),
65176517
)
6518-
.ok_or(format!("Our {contribution_amount} contribution plus the fee estimate exceeds the total bitcoin supply"))
6518+
.ok_or(format!(
6519+
"{estimated_fee} splice-out amount plus {} fee estimate exceeds the total bitcoin supply",
6520+
contribution_amount.unsigned_abs(),
6521+
))
65196522
} else {
65206523
check_v2_funding_inputs_sufficient(
6521-
contribution_amount.to_sat(),
6524+
contribution.value_added(),
65226525
contribution.inputs(),
6526+
contribution.outputs(),
65236527
is_initiator,
65246528
true,
65256529
funding_feerate.to_sat_per_kwu() as u32,
65266530
)
6527-
.map(|_| contribution_amount)
6531+
.map(|_| contribution.net_value())
65286532
}
65296533
}
65306534

@@ -6583,16 +6587,16 @@ fn estimate_v2_funding_transaction_fee(
65836587
/// Returns estimated (partial) fees as additional information
65846588
#[rustfmt::skip]
65856589
fn check_v2_funding_inputs_sufficient(
6586-
contribution_amount: i64, funding_inputs: &[FundingTxInput], is_initiator: bool,
6587-
is_splice: bool, funding_feerate_sat_per_1000_weight: u32,
6588-
) -> Result<u64, String> {
6589-
let estimated_fee = estimate_v2_funding_transaction_fee(
6590-
funding_inputs, &[], is_initiator, is_splice, funding_feerate_sat_per_1000_weight,
6591-
);
6592-
6593-
let mut total_input_sats = 0u64;
6590+
contributed_input_value: Amount, funding_inputs: &[FundingTxInput], outputs: &[TxOut],
6591+
is_initiator: bool, is_splice: bool, funding_feerate_sat_per_1000_weight: u32,
6592+
) -> Result<Amount, String> {
6593+
let estimated_fee = Amount::from_sat(estimate_v2_funding_transaction_fee(
6594+
funding_inputs, outputs, is_initiator, is_splice, funding_feerate_sat_per_1000_weight,
6595+
));
6596+
6597+
let mut total_input_value = Amount::ZERO;
65946598
for FundingTxInput { utxo, .. } in funding_inputs.iter() {
6595-
total_input_sats = total_input_sats.checked_add(utxo.output.value.to_sat())
6599+
total_input_value = total_input_value.checked_add(utxo.output.value)
65966600
.ok_or("Sum of input values is greater than the total bitcoin supply")?;
65976601
}
65986602

@@ -6607,13 +6611,11 @@ fn check_v2_funding_inputs_sufficient(
66076611
// TODO(splicing): refine check including the fact wether a change will be added or not.
66086612
// Can be done once dual funding preparation is included.
66096613

6610-
let minimal_input_amount_needed = contribution_amount.checked_add(estimated_fee as i64)
6611-
.ok_or(format!("Our {contribution_amount} contribution plus the fee estimate exceeds the total bitcoin supply"))?;
6612-
if i64::try_from(total_input_sats).map_err(|_| "Sum of input values is greater than the total bitcoin supply")?
6613-
< minimal_input_amount_needed
6614-
{
6614+
let minimal_input_amount_needed = contributed_input_value.checked_add(estimated_fee)
6615+
.ok_or(format!("{contributed_input_value} contribution plus {estimated_fee} fee estimate exceeds the total bitcoin supply"))?;
6616+
if total_input_value < minimal_input_amount_needed {
66156617
Err(format!(
6616-
"Total input amount {total_input_sats} is lower than needed for contribution {contribution_amount}, considering fees of {estimated_fee}. Need more inputs.",
6618+
"Total input amount {total_input_value} is lower than needed for splice-in contribution {contributed_input_value}, considering fees of {estimated_fee}. Need more inputs.",
66176619
))
66186620
} else {
66196621
Ok(estimated_fee)
@@ -6679,7 +6681,7 @@ impl FundingNegotiationContext {
66796681
};
66806682

66816683
// Optionally add change output
6682-
let change_value_opt = if self.our_funding_contribution > SignedAmount::ZERO {
6684+
let change_value_opt = if !self.our_funding_inputs.is_empty() {
66836685
match calculate_change_output_value(
66846686
&self,
66856687
self.shared_funding_input.is_some(),
@@ -12070,7 +12072,7 @@ where
1207012072
});
1207112073
}
1207212074

12073-
let our_funding_contribution = contribution.value();
12075+
let our_funding_contribution = contribution.net_value();
1207412076
if our_funding_contribution == SignedAmount::ZERO {
1207512077
return Err(APIError::APIMisuseError {
1207612078
err: format!(
@@ -18525,6 +18527,13 @@ mod tests {
1852518527
FundingTxInput::new_p2wpkh(prevtx, 0).unwrap()
1852618528
}
1852718529

18530+
fn funding_output_sats(output_value_sats: u64) -> TxOut {
18531+
TxOut {
18532+
value: Amount::from_sat(output_value_sats),
18533+
script_pubkey: ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros()),
18534+
}
18535+
}
18536+
1852818537
#[test]
1852918538
#[rustfmt::skip]
1853018539
fn test_check_v2_funding_inputs_sufficient() {
@@ -18535,16 +18544,83 @@ mod tests {
1853518544
let expected_fee = if cfg!(feature = "grind_signatures") { 2278 } else { 2284 };
1853618545
assert_eq!(
1853718546
check_v2_funding_inputs_sufficient(
18538-
220_000,
18547+
Amount::from_sat(220_000),
18548+
&[
18549+
funding_input_sats(200_000),
18550+
funding_input_sats(100_000),
18551+
],
18552+
&[],
18553+
true,
18554+
true,
18555+
2000,
18556+
).unwrap(),
18557+
Amount::from_sat(expected_fee),
18558+
);
18559+
}
18560+
18561+
// Net splice-in
18562+
{
18563+
let expected_fee = if cfg!(feature = "grind_signatures") { 2526 } else { 2532 };
18564+
assert_eq!(
18565+
check_v2_funding_inputs_sufficient(
18566+
Amount::from_sat(220_000),
18567+
&[
18568+
funding_input_sats(200_000),
18569+
funding_input_sats(100_000),
18570+
],
18571+
&[
18572+
funding_output_sats(200_000),
18573+
],
18574+
true,
18575+
true,
18576+
2000,
18577+
).unwrap(),
18578+
Amount::from_sat(expected_fee),
18579+
);
18580+
}
18581+
18582+
// Net splice-out
18583+
{
18584+
let expected_fee = if cfg!(feature = "grind_signatures") { 2526 } else { 2532 };
18585+
assert_eq!(
18586+
check_v2_funding_inputs_sufficient(
18587+
Amount::from_sat(220_000),
1853918588
&[
1854018589
funding_input_sats(200_000),
1854118590
funding_input_sats(100_000),
1854218591
],
18592+
&[
18593+
funding_output_sats(400_000),
18594+
],
1854318595
true,
1854418596
true,
1854518597
2000,
1854618598
).unwrap(),
18547-
expected_fee,
18599+
Amount::from_sat(expected_fee),
18600+
);
18601+
}
18602+
18603+
// Net splice-out, inputs insufficient to cover fees
18604+
{
18605+
let expected_fee = if cfg!(feature = "grind_signatures") { 113670 } else { 113940 };
18606+
assert_eq!(
18607+
check_v2_funding_inputs_sufficient(
18608+
Amount::from_sat(220_000),
18609+
&[
18610+
funding_input_sats(200_000),
18611+
funding_input_sats(100_000),
18612+
],
18613+
&[
18614+
funding_output_sats(400_000),
18615+
],
18616+
true,
18617+
true,
18618+
90000,
18619+
),
18620+
Err(format!(
18621+
"Total input amount 0.00300000 BTC is lower than needed for splice-in contribution 0.00220000 BTC, considering fees of {}. Need more inputs.",
18622+
Amount::from_sat(expected_fee),
18623+
)),
1854818624
);
1854918625
}
1855018626

@@ -18553,17 +18629,18 @@ mod tests {
1855318629
let expected_fee = if cfg!(feature = "grind_signatures") { 1736 } else { 1740 };
1855418630
assert_eq!(
1855518631
check_v2_funding_inputs_sufficient(
18556-
220_000,
18632+
Amount::from_sat(220_000),
1855718633
&[
1855818634
funding_input_sats(100_000),
1855918635
],
18636+
&[],
1856018637
true,
1856118638
true,
1856218639
2000,
1856318640
),
1856418641
Err(format!(
18565-
"Total input amount 100000 is lower than needed for contribution 220000, considering fees of {}. Need more inputs.",
18566-
expected_fee,
18642+
"Total input amount 0.00100000 BTC is lower than needed for splice-in contribution 0.00220000 BTC, considering fees of {}. Need more inputs.",
18643+
Amount::from_sat(expected_fee),
1856718644
)),
1856818645
);
1856918646
}
@@ -18573,16 +18650,17 @@ mod tests {
1857318650
let expected_fee = if cfg!(feature = "grind_signatures") { 2278 } else { 2284 };
1857418651
assert_eq!(
1857518652
check_v2_funding_inputs_sufficient(
18576-
(300_000 - expected_fee - 20) as i64,
18653+
Amount::from_sat(300_000 - expected_fee - 20),
1857718654
&[
1857818655
funding_input_sats(200_000),
1857918656
funding_input_sats(100_000),
1858018657
],
18658+
&[],
1858118659
true,
1858218660
true,
1858318661
2000,
1858418662
).unwrap(),
18585-
expected_fee,
18663+
Amount::from_sat(expected_fee),
1858618664
);
1858718665
}
1858818666

@@ -18591,18 +18669,19 @@ mod tests {
1859118669
let expected_fee = if cfg!(feature = "grind_signatures") { 2506 } else { 2513 };
1859218670
assert_eq!(
1859318671
check_v2_funding_inputs_sufficient(
18594-
298032,
18672+
Amount::from_sat(298032),
1859518673
&[
1859618674
funding_input_sats(200_000),
1859718675
funding_input_sats(100_000),
1859818676
],
18677+
&[],
1859918678
true,
1860018679
true,
1860118680
2200,
1860218681
),
1860318682
Err(format!(
18604-
"Total input amount 300000 is lower than needed for contribution 298032, considering fees of {}. Need more inputs.",
18605-
expected_fee
18683+
"Total input amount 0.00300000 BTC is lower than needed for splice-in contribution 0.00298032 BTC, considering fees of {}. Need more inputs.",
18684+
Amount::from_sat(expected_fee),
1860618685
)),
1860718686
);
1860818687
}
@@ -18612,16 +18691,17 @@ mod tests {
1861218691
let expected_fee = if cfg!(feature = "grind_signatures") { 1084 } else { 1088 };
1861318692
assert_eq!(
1861418693
check_v2_funding_inputs_sufficient(
18615-
(300_000 - expected_fee - 20) as i64,
18694+
Amount::from_sat(300_000 - expected_fee - 20),
1861618695
&[
1861718696
funding_input_sats(200_000),
1861818697
funding_input_sats(100_000),
1861918698
],
18699+
&[],
1862018700
false,
1862118701
false,
1862218702
2000,
1862318703
).unwrap(),
18624-
expected_fee,
18704+
Amount::from_sat(expected_fee),
1862518705
);
1862618706
}
1862718707
}

lightning/src/ln/funding.rs

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@ use crate::sign::{P2TR_KEY_PATH_WITNESS_WEIGHT, P2WPKH_WITNESS_WEIGHT};
2121
/// The components of a splice's funding transaction that are contributed by one party.
2222
#[derive(Debug, Clone)]
2323
pub struct SpliceContribution {
24-
/// The amount to contribute to the splice.
25-
value: SignedAmount,
24+
/// The amount from [`inputs`] to contribute to the splice.
25+
///
26+
/// [`inputs`]: Self::inputs
27+
value_added: Amount,
2628

2729
/// The inputs included in the splice's funding transaction to meet the contributed amount
2830
/// plus fees. Any excess amount will be sent to a change output.
@@ -42,27 +44,45 @@ pub struct SpliceContribution {
4244
impl SpliceContribution {
4345
/// Creates a contribution for when funds are only added to a channel.
4446
pub fn splice_in(
45-
value: Amount, inputs: Vec<FundingTxInput>, change_script: Option<ScriptBuf>,
47+
value_added: Amount, inputs: Vec<FundingTxInput>, change_script: Option<ScriptBuf>,
4648
) -> Self {
47-
let value_added = value.to_signed().unwrap_or(SignedAmount::MAX);
48-
49-
Self { value: value_added, inputs, outputs: vec![], change_script }
49+
Self { value_added, inputs, outputs: vec![], change_script }
5050
}
5151

5252
/// Creates a contribution for when funds are only removed from a channel.
5353
pub fn splice_out(outputs: Vec<TxOut>) -> Self {
54-
let value_removed = outputs
54+
Self { value_added: Amount::ZERO, inputs: vec![], outputs, change_script: None }
55+
}
56+
57+
/// Creates a contribution for when funds are both added to and removed from a channel.
58+
///
59+
/// Note that `value_added` represents the value added by `inputs` but should not account for
60+
/// value removed by `outputs`. The net value contributed can be obtained by calling
61+
/// [`SpliceContribution::net_value`].
62+
pub fn splice_in_and_out(
63+
value_added: Amount, inputs: Vec<FundingTxInput>, outputs: Vec<TxOut>,
64+
change_script: Option<ScriptBuf>,
65+
) -> Self {
66+
Self { value_added, inputs, outputs, change_script }
67+
}
68+
69+
/// The net value contributed to a channel by the splice. If negative, more value will be
70+
/// spliced out than spliced in.
71+
pub fn net_value(&self) -> SignedAmount {
72+
let value_added = self.value_added.to_signed().unwrap_or(SignedAmount::MAX);
73+
let value_removed = self
74+
.outputs
5575
.iter()
5676
.map(|txout| txout.value)
5777
.sum::<Amount>()
5878
.to_signed()
5979
.unwrap_or(SignedAmount::MAX);
6080

61-
Self { value: -value_removed, inputs: vec![], outputs, change_script: None }
81+
value_added - value_removed
6282
}
6383

64-
pub(super) fn value(&self) -> SignedAmount {
65-
self.value
84+
pub(super) fn value_added(&self) -> Amount {
85+
self.value_added
6686
}
6787

6888
pub(super) fn inputs(&self) -> &[FundingTxInput] {
@@ -74,7 +94,7 @@ impl SpliceContribution {
7494
}
7595

7696
pub(super) fn into_tx_parts(self) -> (Vec<FundingTxInput>, Vec<TxOut>, Option<ScriptBuf>) {
77-
let SpliceContribution { value: _, inputs, outputs, change_script } = self;
97+
let SpliceContribution { value_added: _, inputs, outputs, change_script } = self;
7898
(inputs, outputs, change_script)
7999
}
80100
}

0 commit comments

Comments
 (0)