Skip to content

Commit fc608ab

Browse files
committed
WIP: Mixed mode splicing
1 parent e1aa263 commit fc608ab

File tree

4 files changed

+201
-17
lines changed

4 files changed

+201
-17
lines changed

lightning/src/ln/channel.rs

Lines changed: 11 additions & 5 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(),
@@ -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: 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

lightning/src/ln/splicing_tests.rs

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -816,6 +816,161 @@ fn test_splice_out() {
816816
let _ = send_payment(&nodes[0], &[&nodes[1]], htlc_limit_msat);
817817
}
818818

819+
#[test]
820+
fn test_splice_in_and_out() {
821+
let chanmon_cfgs = create_chanmon_cfgs(2);
822+
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
823+
let mut config = test_default_channel_config();
824+
config.channel_handshake_config.max_inbound_htlc_value_in_flight_percent_of_channel = 100;
825+
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, Some(config)]);
826+
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
827+
828+
let initial_channel_value_sat = 100_000;
829+
let (_, _, channel_id, _) =
830+
create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0);
831+
832+
let _ = send_payment(&nodes[0], &[&nodes[1]], 100_000);
833+
834+
let coinbase_tx1 = provide_anchor_reserves(&nodes);
835+
let coinbase_tx2 = provide_anchor_reserves(&nodes);
836+
837+
// Contribute a net negative value, with fees taken from the contributed inputs and the
838+
// remaining value sent to change
839+
let htlc_limit_msat = nodes[0].node.list_channels()[0].next_outbound_htlc_limit_msat;
840+
let added_value = Amount::from_sat(htlc_limit_msat / 1000);
841+
let removed_value = added_value * 2;
842+
let change_script = ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros());
843+
let fees = Amount::from_sat(383);
844+
845+
assert!(htlc_limit_msat > initial_channel_value_sat / 2 * 1000);
846+
847+
let initiator_contribution = SpliceContribution::splice_in_and_out(
848+
added_value,
849+
vec![
850+
FundingTxInput::new_p2wpkh(coinbase_tx1, 0).unwrap(),
851+
FundingTxInput::new_p2wpkh(coinbase_tx2, 0).unwrap(),
852+
],
853+
vec![
854+
TxOut {
855+
value: removed_value / 2,
856+
script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(),
857+
},
858+
TxOut {
859+
value: removed_value / 2,
860+
script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(),
861+
},
862+
],
863+
Some(change_script.clone()),
864+
);
865+
866+
let splice_tx = splice_channel(&nodes[0], &nodes[1], channel_id, initiator_contribution);
867+
let expected_change = Amount::ONE_BTC * 2 - added_value - fees;
868+
assert_eq!(
869+
splice_tx.output.iter().find(|txout| txout.script_pubkey == change_script).unwrap().value,
870+
expected_change,
871+
);
872+
873+
mine_transaction(&nodes[0], &splice_tx);
874+
mine_transaction(&nodes[1], &splice_tx);
875+
876+
let htlc_limit_msat = nodes[0].node.list_channels()[0].next_outbound_htlc_limit_msat;
877+
assert!(htlc_limit_msat < added_value.to_sat() * 1000);
878+
let _ = send_payment(&nodes[0], &[&nodes[1]], htlc_limit_msat);
879+
880+
lock_splice_after_blocks(&nodes[0], &nodes[1], ANTI_REORG_DELAY - 1);
881+
882+
let htlc_limit_msat = nodes[0].node.list_channels()[0].next_outbound_htlc_limit_msat;
883+
assert!(htlc_limit_msat < added_value.to_sat() * 1000);
884+
let _ = send_payment(&nodes[0], &[&nodes[1]], htlc_limit_msat);
885+
886+
let coinbase_tx1 = provide_anchor_reserves(&nodes);
887+
let coinbase_tx2 = provide_anchor_reserves(&nodes);
888+
889+
// Contribute a net positive value, with fees taken from the contributed inputs and the
890+
// remaining value sent to change
891+
let added_value = Amount::from_sat(initial_channel_value_sat * 2);
892+
let removed_value = added_value / 2;
893+
let change_script = ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros());
894+
let fees = Amount::from_sat(383);
895+
let initiator_contribution = SpliceContribution::splice_in_and_out(
896+
added_value,
897+
vec![
898+
FundingTxInput::new_p2wpkh(coinbase_tx1, 0).unwrap(),
899+
FundingTxInput::new_p2wpkh(coinbase_tx2, 0).unwrap(),
900+
],
901+
vec![
902+
TxOut {
903+
value: removed_value / 2,
904+
script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(),
905+
},
906+
TxOut {
907+
value: removed_value / 2,
908+
script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(),
909+
},
910+
],
911+
Some(change_script.clone()),
912+
);
913+
914+
let splice_tx = splice_channel(&nodes[0], &nodes[1], channel_id, initiator_contribution);
915+
let expected_change = Amount::ONE_BTC * 2 - added_value - fees;
916+
assert_eq!(
917+
splice_tx.output.iter().find(|txout| txout.script_pubkey == change_script).unwrap().value,
918+
expected_change,
919+
);
920+
921+
mine_transaction(&nodes[0], &splice_tx);
922+
mine_transaction(&nodes[1], &splice_tx);
923+
924+
let htlc_limit_msat = nodes[0].node.list_channels()[0].next_outbound_htlc_limit_msat;
925+
assert_eq!(htlc_limit_msat, 0);
926+
927+
lock_splice_after_blocks(&nodes[0], &nodes[1], ANTI_REORG_DELAY - 1);
928+
929+
let htlc_limit_msat = nodes[0].node.list_channels()[0].next_outbound_htlc_limit_msat;
930+
assert!(htlc_limit_msat > initial_channel_value_sat / 2 * 1000);
931+
let _ = send_payment(&nodes[0], &[&nodes[1]], htlc_limit_msat);
932+
933+
let coinbase_tx1 = provide_anchor_reserves(&nodes);
934+
let coinbase_tx2 = provide_anchor_reserves(&nodes);
935+
936+
// Fail adding a net contribution value of zero
937+
let added_value = Amount::from_sat(initial_channel_value_sat * 2);
938+
let removed_value = added_value;
939+
let change_script = ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros());
940+
941+
let initiator_contribution = SpliceContribution::splice_in_and_out(
942+
added_value,
943+
vec![
944+
FundingTxInput::new_p2wpkh(coinbase_tx1, 0).unwrap(),
945+
FundingTxInput::new_p2wpkh(coinbase_tx2, 0).unwrap(),
946+
],
947+
vec![
948+
TxOut {
949+
value: removed_value / 2,
950+
script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(),
951+
},
952+
TxOut {
953+
value: removed_value / 2,
954+
script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(),
955+
},
956+
],
957+
Some(change_script),
958+
);
959+
960+
assert_eq!(
961+
nodes[0].node.splice_channel(
962+
&channel_id,
963+
&nodes[1].node.get_our_node_id(),
964+
initiator_contribution,
965+
FEERATE_FLOOR_SATS_PER_KW,
966+
None,
967+
),
968+
Err(APIError::APIMisuseError {
969+
err: format!("Channel {} cannot be spliced; contribution cannot be zero", channel_id),
970+
}),
971+
);
972+
}
973+
819974
#[cfg(test)]
820975
#[derive(PartialEq)]
821976
enum SpliceStatus {

0 commit comments

Comments
 (0)