Skip to content

Commit 841ba7f

Browse files
committed
WIP: Mixed mode splicing
1 parent c5a3cea commit 841ba7f

File tree

4 files changed

+186
-26
lines changed

4 files changed

+186
-26
lines changed

lightning/src/ln/channel.rs

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6498,7 +6498,7 @@ fn check_splice_contribution_sufficient(
64986498
contribution: &SpliceContribution, is_initiator: bool, funding_feerate: FeeRate,
64996499
) -> Result<SignedAmount, String> {
65006500
let contribution_amount = contribution.value();
6501-
if contribution_amount < SignedAmount::ZERO {
6501+
if contribution.inputs().is_empty() {
65026502
let estimated_fee = Amount::from_sat(estimate_v2_funding_transaction_fee(
65036503
contribution.inputs(),
65046504
contribution.outputs(),
@@ -6516,6 +6516,7 @@ fn check_splice_contribution_sufficient(
65166516
check_v2_funding_inputs_sufficient(
65176517
contribution_amount.to_sat(),
65186518
contribution.inputs(),
6519+
contribution.outputs(),
65196520
is_initiator,
65206521
true,
65216522
funding_feerate.to_sat_per_kwu() as u32,
@@ -6579,11 +6580,11 @@ fn estimate_v2_funding_transaction_fee(
65796580
/// Returns estimated (partial) fees as additional information
65806581
#[rustfmt::skip]
65816582
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,
6583+
contribution_amount: i64, funding_inputs: &[FundingTxInput], outputs: &[TxOut],
6584+
is_initiator: bool, is_splice: bool, funding_feerate_sat_per_1000_weight: u32,
65846585
) -> Result<u64, String> {
65856586
let estimated_fee = estimate_v2_funding_transaction_fee(
6586-
funding_inputs, &[], is_initiator, is_splice, funding_feerate_sat_per_1000_weight,
6587+
funding_inputs, outputs, is_initiator, is_splice, funding_feerate_sat_per_1000_weight,
65876588
);
65886589

65896590
let mut total_input_sats = 0u64;
@@ -6675,7 +6676,7 @@ impl FundingNegotiationContext {
66756676
};
66766677

66776678
// Optionally add change output
6678-
let change_value_opt = if self.our_funding_contribution > SignedAmount::ZERO {
6679+
let change_value_opt = if !self.our_funding_inputs.is_empty() {
66796680
match calculate_change_output_value(
66806681
&self,
66816682
self.shared_funding_input.is_some(),
@@ -6704,11 +6705,11 @@ impl FundingNegotiationContext {
67046705
}
67056706
};
67066707
let mut change_output =
6707-
TxOut { value: Amount::from_sat(change_value), script_pubkey: change_script };
6708+
TxOut { value: change_value, script_pubkey: change_script };
67086709
let change_output_weight = get_output_weight(&change_output.script_pubkey).to_wu();
67096710
let change_output_fee =
67106711
fee_for_weight(self.funding_feerate_sat_per_1000_weight, change_output_weight);
6711-
let change_value_decreased_with_fee = change_value.saturating_sub(change_output_fee);
6712+
let change_value_decreased_with_fee = change_value.to_sat().saturating_sub(change_output_fee);
67126713
// Check dust limit again
67136714
if change_value_decreased_with_fee > context.holder_dust_limit_satoshis {
67146715
change_output.value = Amount::from_sat(change_value_decreased_with_fee);
@@ -18269,6 +18270,7 @@ mod tests {
1826918270
funding_input_sats(200_000),
1827018271
funding_input_sats(100_000),
1827118272
],
18273+
&[],
1827218274
true,
1827318275
true,
1827418276
2000,
@@ -18286,6 +18288,7 @@ mod tests {
1828618288
&[
1828718289
funding_input_sats(100_000),
1828818290
],
18291+
&[],
1828918292
true,
1829018293
true,
1829118294
2000,
@@ -18307,6 +18310,7 @@ mod tests {
1830718310
funding_input_sats(200_000),
1830818311
funding_input_sats(100_000),
1830918312
],
18313+
&[],
1831018314
true,
1831118315
true,
1831218316
2000,
@@ -18325,6 +18329,7 @@ mod tests {
1832518329
funding_input_sats(200_000),
1832618330
funding_input_sats(100_000),
1832718331
],
18332+
&[],
1832818333
true,
1832918334
true,
1833018335
2200,
@@ -18346,6 +18351,7 @@ mod tests {
1834618351
funding_input_sats(200_000),
1834718352
funding_input_sats(100_000),
1834818353
],
18354+
&[],
1834918355
false,
1835018356
false,
1835118357
2000,

lightning/src/ln/funding.rs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,29 @@ impl SpliceContribution {
6464
Self { value: -value_removed, inputs: vec![], outputs, change_script: None }
6565
}
6666

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

lightning/src/ln/interactivetxs.rs

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2337,22 +2337,20 @@ impl InteractiveTxConstructor {
23372337
pub(super) fn calculate_change_output_value(
23382338
context: &FundingNegotiationContext, is_splice: bool, shared_output_funding_script: &ScriptBuf,
23392339
change_output_dust_limit: u64,
2340-
) -> Result<Option<u64>, AbortReason> {
2341-
assert!(context.our_funding_contribution > SignedAmount::ZERO);
2342-
let our_funding_contribution_satoshis = context.our_funding_contribution.to_sat() as u64;
2343-
2344-
let mut total_input_satoshis = 0u64;
2340+
) -> Result<Option<Amount>, AbortReason> {
2341+
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() {
2347-
total_input_satoshis = total_input_satoshis.saturating_add(utxo.output.value.to_sat());
2344+
total_input_value = total_input_value.checked_add(utxo.output.value).unwrap_or(Amount::MAX);
23482345

23492346
let weight = BASE_INPUT_WEIGHT + utxo.satisfaction_weight;
23502347
our_funding_inputs_weight = our_funding_inputs_weight.saturating_add(weight);
23512348
}
23522349

23532350
let funding_outputs = &context.our_funding_outputs;
2354-
let total_output_satoshis =
2355-
funding_outputs.iter().fold(0u64, |total, out| total.saturating_add(out.value.to_sat()));
2351+
let total_output_value =
2352+
funding_outputs.iter().fold(Amount::ZERO, |total, out| total.checked_add(out.value).unwrap_or(Amount::MAX));
2353+
23562354
let our_funding_outputs_weight = funding_outputs.iter().fold(0u64, |weight, out| {
23572355
weight.saturating_add(get_output_weight(&out.script_pubkey).to_wu())
23582356
});
@@ -2376,15 +2374,20 @@ pub(super) fn calculate_change_output_value(
23762374
}
23772375
}
23782376

2379-
let fees_sats = fee_for_weight(context.funding_feerate_sat_per_1000_weight, weight);
2380-
let net_total_less_fees =
2381-
total_input_satoshis.saturating_sub(total_output_satoshis).saturating_sub(fees_sats);
2382-
if net_total_less_fees < our_funding_contribution_satoshis {
2377+
let contributed_fees = Amount::from_sat(fee_for_weight(context.funding_feerate_sat_per_1000_weight, weight));
2378+
2379+
let contributed_input_value = context.our_funding_contribution + total_output_value.to_signed().unwrap();
2380+
assert!(contributed_input_value > SignedAmount::ZERO);
2381+
let contributed_input_value = contributed_input_value.unsigned_abs();
2382+
2383+
let total_input_value_less_fees = total_input_value.checked_sub(contributed_fees).unwrap_or(Amount::ZERO);
2384+
if total_input_value_less_fees < contributed_input_value {
23832385
// Not enough to cover contribution plus fees
23842386
return Err(AbortReason::InsufficientFees);
23852387
}
2386-
let remaining_value = net_total_less_fees.saturating_sub(our_funding_contribution_satoshis);
2387-
if remaining_value < change_output_dust_limit {
2388+
2389+
let remaining_value = total_input_value_less_fees.checked_sub(contributed_input_value).unwrap_or(Amount::ZERO);
2390+
if remaining_value.to_sat() < change_output_dust_limit {
23882391
// Enough to cover contribution plus fees, but leftover is below dust limit; no change
23892392
Ok(None)
23902393
} else {
@@ -3440,14 +3443,14 @@ mod tests {
34403443
total_inputs - total_outputs - context.our_funding_contribution.to_unsigned().unwrap();
34413444
assert_eq!(
34423445
calculate_change_output_value(&context, false, &ScriptBuf::new(), 300),
3443-
Ok(Some((gross_change - fees - common_fees).to_sat())),
3446+
Ok(Some(gross_change - fees - common_fees)),
34443447
);
34453448

34463449
// There is leftover for change, without common fees
34473450
let context = FundingNegotiationContext { is_initiator: false, ..context };
34483451
assert_eq!(
34493452
calculate_change_output_value(&context, false, &ScriptBuf::new(), 300),
3450-
Ok(Some((gross_change - fees).to_sat())),
3453+
Ok(Some(gross_change - fees)),
34513454
);
34523455

34533456
// Insufficient inputs, no leftover
@@ -3482,7 +3485,7 @@ mod tests {
34823485
total_inputs - total_outputs - context.our_funding_contribution.to_unsigned().unwrap();
34833486
assert_eq!(
34843487
calculate_change_output_value(&context, false, &ScriptBuf::new(), 100),
3485-
Ok(Some((gross_change - fees).to_sat())),
3488+
Ok(Some(gross_change - fees)),
34863489
);
34873490

34883491
// Larger fee, smaller change
@@ -3496,7 +3499,7 @@ mod tests {
34963499
total_inputs - total_outputs - context.our_funding_contribution.to_unsigned().unwrap();
34973500
assert_eq!(
34983501
calculate_change_output_value(&context, false, &ScriptBuf::new(), 300),
3499-
Ok(Some((gross_change - fees * 3 - common_fees * 3).to_sat())),
3502+
Ok(Some(gross_change - fees * 3 - common_fees * 3)),
35003503
);
35013504
}
35023505

lightning/src/ln/splicing_tests.rs

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -804,6 +804,135 @@ fn test_splice_out() {
804804
let _ = send_payment(&nodes[0], &[&nodes[1]], htlc_limit_msat);
805805
}
806806

807+
#[test]
808+
fn test_splice_in_and_out() {
809+
let chanmon_cfgs = create_chanmon_cfgs(2);
810+
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
811+
let mut config = test_default_channel_config();
812+
config.channel_handshake_config.max_inbound_htlc_value_in_flight_percent_of_channel = 100;
813+
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, Some(config)]);
814+
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
815+
816+
let initial_channel_value_sat = 100_000;
817+
let (_, _, channel_id, _) =
818+
create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0);
819+
820+
let _ = send_payment(&nodes[0], &[&nodes[1]], 100_000);
821+
822+
let coinbase_tx1 = provide_anchor_reserves(&nodes);
823+
let coinbase_tx2 = provide_anchor_reserves(&nodes);
824+
825+
let htlc_limit_msat = nodes[0].node.list_channels()[0].next_outbound_htlc_limit_msat;
826+
assert!(dbg!(htlc_limit_msat) > dbg!(initial_channel_value_sat / 2 * 1000));
827+
828+
// Remove a net value of htlc_limit_msat / 1000 sats
829+
let initiator_contribution = SpliceContribution::splice_in_and_out(
830+
Amount::from_sat(htlc_limit_msat / 1000),
831+
vec![
832+
FundingTxInput::new_p2wpkh(coinbase_tx1, 0).unwrap(),
833+
FundingTxInput::new_p2wpkh(coinbase_tx2, 0).unwrap(),
834+
],
835+
vec![
836+
TxOut {
837+
value: Amount::from_sat(htlc_limit_msat / 1000),
838+
script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(),
839+
},
840+
TxOut {
841+
value: Amount::from_sat(htlc_limit_msat / 1000),
842+
script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(),
843+
},
844+
],
845+
Some(nodes[0].wallet_source.get_change_script().unwrap()),
846+
);
847+
848+
let splice_tx = splice_channel(&nodes[0], &nodes[1], channel_id, initiator_contribution);
849+
mine_transaction(&nodes[0], &splice_tx);
850+
mine_transaction(&nodes[1], &splice_tx);
851+
852+
let htlc_limit_msat = nodes[0].node.list_channels()[0].next_outbound_htlc_limit_msat;
853+
assert!(dbg!(htlc_limit_msat) < dbg!(initial_channel_value_sat / 2 * 1000));
854+
let _ = send_payment(&nodes[0], &[&nodes[1]], htlc_limit_msat);
855+
856+
lock_splice_after_blocks(&nodes[0], &nodes[1], ANTI_REORG_DELAY - 1);
857+
858+
let htlc_limit_msat = nodes[0].node.list_channels()[0].next_outbound_htlc_limit_msat;
859+
assert!(dbg!(htlc_limit_msat) < dbg!(initial_channel_value_sat / 2 * 1000));
860+
//let _ = send_payment(&nodes[0], &[&nodes[1]], htlc_limit_msat);
861+
862+
let coinbase_tx1 = provide_anchor_reserves(&nodes);
863+
let coinbase_tx2 = provide_anchor_reserves(&nodes);
864+
865+
// Add a net value of initial_channel_value_sat
866+
let initiator_contribution = SpliceContribution::splice_in_and_out(
867+
Amount::from_sat(initial_channel_value_sat * 2),
868+
vec![
869+
FundingTxInput::new_p2wpkh(coinbase_tx1, 0).unwrap(),
870+
FundingTxInput::new_p2wpkh(coinbase_tx2, 0).unwrap(),
871+
],
872+
vec![
873+
TxOut {
874+
value: Amount::from_sat(initial_channel_value_sat / 2),
875+
script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(),
876+
},
877+
TxOut {
878+
value: Amount::from_sat(initial_channel_value_sat / 2),
879+
script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(),
880+
},
881+
],
882+
Some(nodes[0].wallet_source.get_change_script().unwrap()),
883+
);
884+
885+
let splice_tx = splice_channel(&nodes[0], &nodes[1], channel_id, initiator_contribution);
886+
mine_transaction(&nodes[0], &splice_tx);
887+
mine_transaction(&nodes[1], &splice_tx);
888+
889+
let htlc_limit_msat = nodes[0].node.list_channels()[0].next_outbound_htlc_limit_msat;
890+
assert!(dbg!(htlc_limit_msat) < dbg!(initial_channel_value_sat / 2 * 1000));
891+
//let _ = send_payment(&nodes[0], &[&nodes[1]], htlc_limit_msat);
892+
893+
lock_splice_after_blocks(&nodes[0], &nodes[1], ANTI_REORG_DELAY - 1);
894+
895+
let htlc_limit_msat = nodes[0].node.list_channels()[0].next_outbound_htlc_limit_msat;
896+
assert!(dbg!(htlc_limit_msat) > dbg!(initial_channel_value_sat / 2 * 1000));
897+
let _ = send_payment(&nodes[0], &[&nodes[1]], htlc_limit_msat);
898+
899+
let coinbase_tx1 = provide_anchor_reserves(&nodes);
900+
let coinbase_tx2 = provide_anchor_reserves(&nodes);
901+
902+
// Fail adding a net contribution value of zero
903+
let initiator_contribution = SpliceContribution::splice_in_and_out(
904+
Amount::from_sat(initial_channel_value_sat * 2),
905+
vec![
906+
FundingTxInput::new_p2wpkh(coinbase_tx1, 0).unwrap(),
907+
FundingTxInput::new_p2wpkh(coinbase_tx2, 0).unwrap(),
908+
],
909+
vec![
910+
TxOut {
911+
value: Amount::from_sat(initial_channel_value_sat),
912+
script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(),
913+
},
914+
TxOut {
915+
value: Amount::from_sat(initial_channel_value_sat),
916+
script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(),
917+
},
918+
],
919+
Some(nodes[0].wallet_source.get_change_script().unwrap()),
920+
);
921+
922+
assert_eq!(
923+
nodes[0].node.splice_channel(
924+
&channel_id,
925+
&nodes[1].node.get_our_node_id(),
926+
initiator_contribution,
927+
FEERATE_FLOOR_SATS_PER_KW,
928+
None,
929+
),
930+
Err(APIError::APIMisuseError {
931+
err: format!("Channel {} cannot be spliced; contribution cannot be zero", channel_id),
932+
}),
933+
);
934+
}
935+
807936
#[cfg(test)]
808937
#[derive(PartialEq)]
809938
enum SpliceStatus {

0 commit comments

Comments
 (0)