Skip to content

Commit c9cade5

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 4ce8d70 commit c9cade5

File tree

4 files changed

+313
-46
lines changed

4 files changed

+313
-46
lines changed

lightning/src/ln/channel.rs

Lines changed: 115 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -6497,8 +6497,7 @@ fn get_v2_channel_reserve_satoshis(channel_value_satoshis: u64, dust_limit_satos
64976497
fn check_splice_contribution_sufficient(
64986498
contribution: &SpliceContribution, is_initiator: bool, funding_feerate: FeeRate,
64996499
) -> Result<SignedAmount, String> {
6500-
let contribution_amount = contribution.value();
6501-
if contribution_amount < SignedAmount::ZERO {
6500+
if contribution.inputs().is_empty() {
65026501
let estimated_fee = Amount::from_sat(estimate_v2_funding_transaction_fee(
65036502
contribution.inputs(),
65046503
contribution.outputs(),
@@ -6507,20 +6506,26 @@ fn check_splice_contribution_sufficient(
65076506
funding_feerate.to_sat_per_kwu() as u32,
65086507
));
65096508

6509+
let contribution_amount = contribution.net_value();
65106510
contribution_amount
65116511
.checked_sub(
65126512
estimated_fee.to_signed().expect("fees should never exceed Amount::MAX_MONEY"),
65136513
)
6514-
.ok_or(format!("Our {contribution_amount} contribution plus the fee estimate exceeds the total bitcoin supply"))
6514+
.ok_or(format!(
6515+
"{} splice-out amount plus {} fee estimate exceeds the total bitcoin supply",
6516+
contribution_amount.unsigned_abs(),
6517+
estimated_fee,
6518+
))
65156519
} else {
65166520
check_v2_funding_inputs_sufficient(
6517-
contribution_amount.to_sat(),
6521+
contribution.input_value(),
65186522
contribution.inputs(),
6523+
contribution.outputs(),
65196524
is_initiator,
65206525
true,
65216526
funding_feerate.to_sat_per_kwu() as u32,
65226527
)
6523-
.map(|_| contribution_amount)
6528+
.map(|_| contribution.net_value())
65246529
}
65256530
}
65266531

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

@@ -6603,13 +6608,11 @@ fn check_v2_funding_inputs_sufficient(
66036608
// TODO(splicing): refine check including the fact wether a change will be added or not.
66046609
// Can be done once dual funding preparation is included.
66056610

6606-
let minimal_input_amount_needed = contribution_amount.checked_add(estimated_fee as i64)
6607-
.ok_or(format!("Our {contribution_amount} contribution plus the fee estimate exceeds the total bitcoin supply"))?;
6608-
if i64::try_from(total_input_sats).map_err(|_| "Sum of input values is greater than the total bitcoin supply")?
6609-
< minimal_input_amount_needed
6610-
{
6611+
let minimal_input_amount_needed = contributed_input_value.checked_add(estimated_fee)
6612+
.ok_or(format!("{contributed_input_value} contribution plus {estimated_fee} fee estimate exceeds the total bitcoin supply"))?;
6613+
if total_input_value < minimal_input_amount_needed {
66116614
Err(format!(
6612-
"Total input amount {total_input_sats} is lower than needed for contribution {contribution_amount}, considering fees of {estimated_fee}. Need more inputs.",
6615+
"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.",
66136616
))
66146617
} else {
66156618
Ok(estimated_fee)
@@ -6675,7 +6678,7 @@ impl FundingNegotiationContext {
66756678
};
66766679

66776680
// Optionally add change output
6678-
let change_value_opt = if self.our_funding_contribution > SignedAmount::ZERO {
6681+
let change_value_opt = if !self.our_funding_inputs.is_empty() {
66796682
match calculate_change_output_value(
66806683
&self,
66816684
self.shared_funding_input.is_some(),
@@ -11956,7 +11959,7 @@ where
1195611959
});
1195711960
}
1195811961

11959-
let our_funding_contribution = contribution.value();
11962+
let our_funding_contribution = contribution.net_value();
1196011963
if our_funding_contribution == SignedAmount::ZERO {
1196111964
return Err(APIError::APIMisuseError {
1196211965
err: format!(
@@ -18254,6 +18257,13 @@ mod tests {
1825418257
FundingTxInput::new_p2wpkh(prevtx, 0).unwrap()
1825518258
}
1825618259

18260+
fn funding_output_sats(output_value_sats: u64) -> TxOut {
18261+
TxOut {
18262+
value: Amount::from_sat(output_value_sats),
18263+
script_pubkey: ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros()),
18264+
}
18265+
}
18266+
1825718267
#[test]
1825818268
#[rustfmt::skip]
1825918269
fn test_check_v2_funding_inputs_sufficient() {
@@ -18264,16 +18274,83 @@ mod tests {
1826418274
let expected_fee = if cfg!(feature = "grind_signatures") { 2278 } else { 2284 };
1826518275
assert_eq!(
1826618276
check_v2_funding_inputs_sufficient(
18267-
220_000,
18277+
Amount::from_sat(220_000),
18278+
&[
18279+
funding_input_sats(200_000),
18280+
funding_input_sats(100_000),
18281+
],
18282+
&[],
18283+
true,
18284+
true,
18285+
2000,
18286+
).unwrap(),
18287+
Amount::from_sat(expected_fee),
18288+
);
18289+
}
18290+
18291+
// Net splice-in
18292+
{
18293+
let expected_fee = if cfg!(feature = "grind_signatures") { 2526 } else { 2532 };
18294+
assert_eq!(
18295+
check_v2_funding_inputs_sufficient(
18296+
Amount::from_sat(220_000),
18297+
&[
18298+
funding_input_sats(200_000),
18299+
funding_input_sats(100_000),
18300+
],
18301+
&[
18302+
funding_output_sats(200_000),
18303+
],
18304+
true,
18305+
true,
18306+
2000,
18307+
).unwrap(),
18308+
Amount::from_sat(expected_fee),
18309+
);
18310+
}
18311+
18312+
// Net splice-out
18313+
{
18314+
let expected_fee = if cfg!(feature = "grind_signatures") { 2526 } else { 2532 };
18315+
assert_eq!(
18316+
check_v2_funding_inputs_sufficient(
18317+
Amount::from_sat(220_000),
1826818318
&[
1826918319
funding_input_sats(200_000),
1827018320
funding_input_sats(100_000),
1827118321
],
18322+
&[
18323+
funding_output_sats(400_000),
18324+
],
1827218325
true,
1827318326
true,
1827418327
2000,
1827518328
).unwrap(),
18276-
expected_fee,
18329+
Amount::from_sat(expected_fee),
18330+
);
18331+
}
18332+
18333+
// Net splice-out, inputs insufficient to cover fees
18334+
{
18335+
let expected_fee = if cfg!(feature = "grind_signatures") { 113670 } else { 113940 };
18336+
assert_eq!(
18337+
check_v2_funding_inputs_sufficient(
18338+
Amount::from_sat(220_000),
18339+
&[
18340+
funding_input_sats(200_000),
18341+
funding_input_sats(100_000),
18342+
],
18343+
&[
18344+
funding_output_sats(400_000),
18345+
],
18346+
true,
18347+
true,
18348+
90000,
18349+
),
18350+
Err(format!(
18351+
"Total input amount 0.00300000 BTC is lower than needed for splice-in contribution 0.00220000 BTC, considering fees of {}. Need more inputs.",
18352+
Amount::from_sat(expected_fee),
18353+
)),
1827718354
);
1827818355
}
1827918356

@@ -18282,17 +18359,18 @@ mod tests {
1828218359
let expected_fee = if cfg!(feature = "grind_signatures") { 1736 } else { 1740 };
1828318360
assert_eq!(
1828418361
check_v2_funding_inputs_sufficient(
18285-
220_000,
18362+
Amount::from_sat(220_000),
1828618363
&[
1828718364
funding_input_sats(100_000),
1828818365
],
18366+
&[],
1828918367
true,
1829018368
true,
1829118369
2000,
1829218370
),
1829318371
Err(format!(
18294-
"Total input amount 100000 is lower than needed for contribution 220000, considering fees of {}. Need more inputs.",
18295-
expected_fee,
18372+
"Total input amount 0.00100000 BTC is lower than needed for splice-in contribution 0.00220000 BTC, considering fees of {}. Need more inputs.",
18373+
Amount::from_sat(expected_fee),
1829618374
)),
1829718375
);
1829818376
}
@@ -18302,16 +18380,17 @@ mod tests {
1830218380
let expected_fee = if cfg!(feature = "grind_signatures") { 2278 } else { 2284 };
1830318381
assert_eq!(
1830418382
check_v2_funding_inputs_sufficient(
18305-
(300_000 - expected_fee - 20) as i64,
18383+
Amount::from_sat(300_000 - expected_fee - 20),
1830618384
&[
1830718385
funding_input_sats(200_000),
1830818386
funding_input_sats(100_000),
1830918387
],
18388+
&[],
1831018389
true,
1831118390
true,
1831218391
2000,
1831318392
).unwrap(),
18314-
expected_fee,
18393+
Amount::from_sat(expected_fee),
1831518394
);
1831618395
}
1831718396

@@ -18320,18 +18399,19 @@ mod tests {
1832018399
let expected_fee = if cfg!(feature = "grind_signatures") { 2506 } else { 2513 };
1832118400
assert_eq!(
1832218401
check_v2_funding_inputs_sufficient(
18323-
298032,
18402+
Amount::from_sat(298032),
1832418403
&[
1832518404
funding_input_sats(200_000),
1832618405
funding_input_sats(100_000),
1832718406
],
18407+
&[],
1832818408
true,
1832918409
true,
1833018410
2200,
1833118411
),
1833218412
Err(format!(
18333-
"Total input amount 300000 is lower than needed for contribution 298032, considering fees of {}. Need more inputs.",
18334-
expected_fee
18413+
"Total input amount 0.00300000 BTC is lower than needed for splice-in contribution 0.00298032 BTC, considering fees of {}. Need more inputs.",
18414+
Amount::from_sat(expected_fee),
1833518415
)),
1833618416
);
1833718417
}
@@ -18341,16 +18421,17 @@ mod tests {
1834118421
let expected_fee = if cfg!(feature = "grind_signatures") { 1084 } else { 1088 };
1834218422
assert_eq!(
1834318423
check_v2_funding_inputs_sufficient(
18344-
(300_000 - expected_fee - 20) as i64,
18424+
Amount::from_sat(300_000 - expected_fee - 20),
1834518425
&[
1834618426
funding_input_sats(200_000),
1834718427
funding_input_sats(100_000),
1834818428
],
18429+
&[],
1834918430
false,
1835018431
false,
1835118432
2000,
1835218433
).unwrap(),
18353-
expected_fee,
18434+
Amount::from_sat(expected_fee),
1835418435
);
1835518436
}
1835618437
}

lightning/src/ln/funding.rs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,40 @@ impl SpliceContribution {
6161
Self { value: -value_removed, inputs: vec![], outputs, change_script: None }
6262
}
6363

64-
pub(super) fn value(&self) -> SignedAmount {
64+
/// Creates a contribution for when funds are both added to and removed from a channel.
65+
///
66+
/// Note that `value_added` represents the value added by `inputs` but should not account for
67+
/// value removed by `outputs`. The net value contributed can be obtained by calling
68+
/// [`SpliceContribution::net_value`].
69+
pub fn splice_in_and_out(
70+
value_added: Amount, inputs: Vec<FundingTxInput>, outputs: Vec<TxOut>,
71+
change_script: Option<ScriptBuf>,
72+
) -> Self {
73+
let splice_in = Self::splice_in(value_added, inputs, change_script);
74+
let splice_out = Self::splice_out(outputs);
75+
76+
Self {
77+
value: splice_in.value + splice_out.value,
78+
inputs: splice_in.inputs,
79+
outputs: splice_out.outputs,
80+
change_script: splice_in.change_script,
81+
}
82+
}
83+
84+
/// The net value contributed to a channel by the splice. If negative, more value will be
85+
/// spliced out than spliced in.
86+
pub fn net_value(&self) -> SignedAmount {
6587
self.value
6688
}
6789

90+
pub(super) fn input_value(&self) -> Amount {
91+
(self.net_value() + self.output_value().to_signed().expect("")).to_unsigned().expect("")
92+
}
93+
94+
pub(super) fn output_value(&self) -> Amount {
95+
self.outputs.iter().map(|txout| txout.value).sum::<Amount>()
96+
}
97+
6898
pub(super) fn inputs(&self) -> &[FundingTxInput] {
6999
&self.inputs[..]
70100
}

lightning/src/ln/interactivetxs.rs

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2338,9 +2338,6 @@ pub(super) fn calculate_change_output_value(
23382338
context: &FundingNegotiationContext, is_splice: bool, shared_output_funding_script: &ScriptBuf,
23392339
change_output_dust_limit: u64,
23402340
) -> Result<Option<Amount>, AbortReason> {
2341-
assert!(context.our_funding_contribution > SignedAmount::ZERO);
2342-
let our_funding_contribution = context.our_funding_contribution.to_unsigned().unwrap();
2343-
23442341
let mut total_input_value = Amount::ZERO;
23452342
let mut our_funding_inputs_weight = 0u64;
23462343
for FundingTxInput { utxo, .. } in context.our_funding_inputs.iter() {
@@ -2354,6 +2351,7 @@ pub(super) fn calculate_change_output_value(
23542351
let total_output_value = funding_outputs
23552352
.iter()
23562353
.fold(Amount::ZERO, |total, out| total.checked_add(out.value).unwrap_or(Amount::MAX));
2354+
23572355
let our_funding_outputs_weight = funding_outputs.iter().fold(0u64, |weight, out| {
23582356
weight.saturating_add(get_output_weight(&out.script_pubkey).to_wu())
23592357
});
@@ -2379,18 +2377,21 @@ pub(super) fn calculate_change_output_value(
23792377

23802378
let contributed_fees =
23812379
Amount::from_sat(fee_for_weight(context.funding_feerate_sat_per_1000_weight, weight));
2382-
let net_total_less_fees = total_input_value
2383-
.checked_sub(total_output_value)
2384-
.unwrap_or(Amount::ZERO)
2385-
.checked_sub(contributed_fees)
2386-
.unwrap_or(Amount::ZERO);
2387-
if net_total_less_fees < our_funding_contribution {
2380+
2381+
let contributed_input_value =
2382+
context.our_funding_contribution + total_output_value.to_signed().unwrap();
2383+
assert!(contributed_input_value > SignedAmount::ZERO);
2384+
let contributed_input_value = contributed_input_value.unsigned_abs();
2385+
2386+
let total_input_value_less_fees =
2387+
total_input_value.checked_sub(contributed_fees).unwrap_or(Amount::ZERO);
2388+
if total_input_value_less_fees < contributed_input_value {
23882389
// Not enough to cover contribution plus fees
23892390
return Err(AbortReason::InsufficientFees);
23902391
}
23912392

2392-
let remaining_value = net_total_less_fees
2393-
.checked_sub(our_funding_contribution)
2393+
let remaining_value = total_input_value_less_fees
2394+
.checked_sub(contributed_input_value)
23942395
.expect("remaining_value should not be negative");
23952396
if remaining_value.to_sat() < change_output_dust_limit {
23962397
// Enough to cover contribution plus fees, but leftover is below dust limit; no change

0 commit comments

Comments
 (0)