From 3e57ec483a38c7a84f3578855bba095c0b34a299 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Tue, 2 Dec 2025 15:02:32 -0600 Subject: [PATCH 01/13] Use struct instead of enum for SpliceContribution When adding support for mixed splice-in and splice-out, the contribution amount will need to be computed based on the splice-in and splice-out values. Rather than add a third variant to SpliceContribution, which could have an invalid contribution amount, use a more general struct that can represent splice-in, splice-out, and mixed. Constructors are provided for the typical splice-in and splice-out case whereas support for the mixed case will be added in an independent change. --- fuzz/src/chanmon_consistency.rs | 68 +++---- .../src/upgrade_downgrade_tests.rs | 10 +- lightning/src/ln/funding.rs | 93 +++++---- lightning/src/ln/splicing_tests.rs | 181 ++++++++---------- 4 files changed, 152 insertions(+), 200 deletions(-) diff --git a/fuzz/src/chanmon_consistency.rs b/fuzz/src/chanmon_consistency.rs index aca232471d6..ba3fc9077e8 100644 --- a/fuzz/src/chanmon_consistency.rs +++ b/fuzz/src/chanmon_consistency.rs @@ -1860,11 +1860,8 @@ pub fn do_test(data: &[u8], underlying_out: Out, anchors: bool) { 0xa0 => { let input = FundingTxInput::new_p2wpkh(coinbase_tx.clone(), 0).unwrap(); - let contribution = SpliceContribution::SpliceIn { - value: Amount::from_sat(10_000), - inputs: vec![input], - change_script: None, - }; + let contribution = + SpliceContribution::splice_in(Amount::from_sat(10_000), vec![input], None); let funding_feerate_sat_per_kw = fee_est_a.ret_val.load(atomic::Ordering::Acquire); if let Err(e) = nodes[0].splice_channel( &chan_a_id, @@ -1882,11 +1879,8 @@ pub fn do_test(data: &[u8], underlying_out: Out, anchors: bool) { }, 0xa1 => { let input = FundingTxInput::new_p2wpkh(coinbase_tx.clone(), 1).unwrap(); - let contribution = SpliceContribution::SpliceIn { - value: Amount::from_sat(10_000), - inputs: vec![input], - change_script: None, - }; + let contribution = + SpliceContribution::splice_in(Amount::from_sat(10_000), vec![input], None); let funding_feerate_sat_per_kw = fee_est_b.ret_val.load(atomic::Ordering::Acquire); if let Err(e) = nodes[1].splice_channel( &chan_a_id, @@ -1904,11 +1898,8 @@ pub fn do_test(data: &[u8], underlying_out: Out, anchors: bool) { }, 0xa2 => { let input = FundingTxInput::new_p2wpkh(coinbase_tx.clone(), 0).unwrap(); - let contribution = SpliceContribution::SpliceIn { - value: Amount::from_sat(10_000), - inputs: vec![input], - change_script: None, - }; + let contribution = + SpliceContribution::splice_in(Amount::from_sat(10_000), vec![input], None); let funding_feerate_sat_per_kw = fee_est_b.ret_val.load(atomic::Ordering::Acquire); if let Err(e) = nodes[1].splice_channel( &chan_b_id, @@ -1926,11 +1917,8 @@ pub fn do_test(data: &[u8], underlying_out: Out, anchors: bool) { }, 0xa3 => { let input = FundingTxInput::new_p2wpkh(coinbase_tx.clone(), 1).unwrap(); - let contribution = SpliceContribution::SpliceIn { - value: Amount::from_sat(10_000), - inputs: vec![input], - change_script: None, - }; + let contribution = + SpliceContribution::splice_in(Amount::from_sat(10_000), vec![input], None); let funding_feerate_sat_per_kw = fee_est_c.ret_val.load(atomic::Ordering::Acquire); if let Err(e) = nodes[2].splice_channel( &chan_b_id, @@ -1958,12 +1946,10 @@ pub fn do_test(data: &[u8], underlying_out: Out, anchors: bool) { .map(|chan| chan.outbound_capacity_msat) .unwrap(); if outbound_capacity_msat >= 20_000_000 { - let contribution = SpliceContribution::SpliceOut { - outputs: vec![TxOut { - value: Amount::from_sat(MAX_STD_OUTPUT_DUST_LIMIT_SATOSHIS), - script_pubkey: coinbase_tx.output[0].script_pubkey.clone(), - }], - }; + let contribution = SpliceContribution::splice_out(vec![TxOut { + value: Amount::from_sat(MAX_STD_OUTPUT_DUST_LIMIT_SATOSHIS), + script_pubkey: coinbase_tx.output[0].script_pubkey.clone(), + }]); let funding_feerate_sat_per_kw = fee_est_a.ret_val.load(atomic::Ordering::Acquire); if let Err(e) = nodes[0].splice_channel( @@ -1989,12 +1975,10 @@ pub fn do_test(data: &[u8], underlying_out: Out, anchors: bool) { .map(|chan| chan.outbound_capacity_msat) .unwrap(); if outbound_capacity_msat >= 20_000_000 { - let contribution = SpliceContribution::SpliceOut { - outputs: vec![TxOut { - value: Amount::from_sat(MAX_STD_OUTPUT_DUST_LIMIT_SATOSHIS), - script_pubkey: coinbase_tx.output[1].script_pubkey.clone(), - }], - }; + let contribution = SpliceContribution::splice_out(vec![TxOut { + value: Amount::from_sat(MAX_STD_OUTPUT_DUST_LIMIT_SATOSHIS), + script_pubkey: coinbase_tx.output[1].script_pubkey.clone(), + }]); let funding_feerate_sat_per_kw = fee_est_b.ret_val.load(atomic::Ordering::Acquire); if let Err(e) = nodes[1].splice_channel( @@ -2020,12 +2004,10 @@ pub fn do_test(data: &[u8], underlying_out: Out, anchors: bool) { .map(|chan| chan.outbound_capacity_msat) .unwrap(); if outbound_capacity_msat >= 20_000_000 { - let contribution = SpliceContribution::SpliceOut { - outputs: vec![TxOut { - value: Amount::from_sat(MAX_STD_OUTPUT_DUST_LIMIT_SATOSHIS), - script_pubkey: coinbase_tx.output[1].script_pubkey.clone(), - }], - }; + let contribution = SpliceContribution::splice_out(vec![TxOut { + value: Amount::from_sat(MAX_STD_OUTPUT_DUST_LIMIT_SATOSHIS), + script_pubkey: coinbase_tx.output[1].script_pubkey.clone(), + }]); let funding_feerate_sat_per_kw = fee_est_b.ret_val.load(atomic::Ordering::Acquire); if let Err(e) = nodes[1].splice_channel( @@ -2051,12 +2033,10 @@ pub fn do_test(data: &[u8], underlying_out: Out, anchors: bool) { .map(|chan| chan.outbound_capacity_msat) .unwrap(); if outbound_capacity_msat >= 20_000_000 { - let contribution = SpliceContribution::SpliceOut { - outputs: vec![TxOut { - value: Amount::from_sat(MAX_STD_OUTPUT_DUST_LIMIT_SATOSHIS), - script_pubkey: coinbase_tx.output[2].script_pubkey.clone(), - }], - }; + let contribution = SpliceContribution::splice_out(vec![TxOut { + value: Amount::from_sat(MAX_STD_OUTPUT_DUST_LIMIT_SATOSHIS), + script_pubkey: coinbase_tx.output[2].script_pubkey.clone(), + }]); let funding_feerate_sat_per_kw = fee_est_c.ret_val.load(atomic::Ordering::Acquire); if let Err(e) = nodes[2].splice_channel( diff --git a/lightning-tests/src/upgrade_downgrade_tests.rs b/lightning-tests/src/upgrade_downgrade_tests.rs index cef180fbd4e..924c6c860e1 100644 --- a/lightning-tests/src/upgrade_downgrade_tests.rs +++ b/lightning-tests/src/upgrade_downgrade_tests.rs @@ -441,12 +441,10 @@ fn do_test_0_1_htlc_forward_after_splice(fail_htlc: bool) { reconnect_b_c_args.send_announcement_sigs = (true, true); reconnect_nodes(reconnect_b_c_args); - let contribution = SpliceContribution::SpliceOut { - outputs: vec![TxOut { - value: Amount::from_sat(1_000), - script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), - }], - }; + let contribution = SpliceContribution::splice_out(vec![TxOut { + value: Amount::from_sat(1_000), + script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), + }]); let splice_tx = splice_channel(&nodes[0], &nodes[1], ChannelId(chan_id_bytes_a), contribution); for node in nodes.iter() { mine_transaction(node, &splice_tx); diff --git a/lightning/src/ln/funding.rs b/lightning/src/ln/funding.rs index f80b2b6daea..b7f8740f737 100644 --- a/lightning/src/ln/funding.rs +++ b/lightning/src/ln/funding.rs @@ -20,69 +20,62 @@ use crate::sign::{P2TR_KEY_PATH_WITNESS_WEIGHT, P2WPKH_WITNESS_WEIGHT}; /// The components of a splice's funding transaction that are contributed by one party. #[derive(Debug, Clone)] -pub enum SpliceContribution { - /// When funds are added to a channel. - SpliceIn { - /// The amount to contribute to the splice. - value: Amount, - - /// The inputs included in the splice's funding transaction to meet the contributed amount - /// plus fees. Any excess amount will be sent to a change output. - inputs: Vec, - - /// An optional change output script. This will be used if needed or, when not set, - /// generated using [`SignerProvider::get_destination_script`]. - /// - /// [`SignerProvider::get_destination_script`]: crate::sign::SignerProvider::get_destination_script - change_script: Option, - }, - /// When funds are removed from a channel. - SpliceOut { - /// The outputs to include in the splice's funding transaction. The total value of all - /// outputs plus fees will be the amount that is removed. - outputs: Vec, - }, +pub struct SpliceContribution { + /// The amount to contribute to the splice. + value: SignedAmount, + + /// The inputs included in the splice's funding transaction to meet the contributed amount + /// plus fees. Any excess amount will be sent to a change output. + inputs: Vec, + + /// The outputs to include in the splice's funding transaction. The total value of all + /// outputs plus fees will be the amount that is removed. + outputs: Vec, + + /// An optional change output script. This will be used if needed or, when not set, + /// generated using [`SignerProvider::get_destination_script`]. + /// + /// [`SignerProvider::get_destination_script`]: crate::sign::SignerProvider::get_destination_script + change_script: Option, } impl SpliceContribution { + /// Creates a contribution for when funds are only added to a channel. + pub fn splice_in( + value: Amount, inputs: Vec, change_script: Option, + ) -> Self { + let value_added = value.to_signed().unwrap_or(SignedAmount::MAX); + + Self { value: value_added, inputs, outputs: vec![], change_script } + } + + /// Creates a contribution for when funds are only removed from a channel. + pub fn splice_out(outputs: Vec) -> Self { + let value_removed = outputs + .iter() + .map(|txout| txout.value) + .sum::() + .to_signed() + .unwrap_or(SignedAmount::MAX); + + Self { value: -value_removed, inputs: vec![], outputs, change_script: None } + } + pub(super) fn value(&self) -> SignedAmount { - match self { - SpliceContribution::SpliceIn { value, .. } => { - value.to_signed().unwrap_or(SignedAmount::MAX) - }, - SpliceContribution::SpliceOut { outputs } => { - let value_removed = outputs - .iter() - .map(|txout| txout.value) - .sum::() - .to_signed() - .unwrap_or(SignedAmount::MAX); - -value_removed - }, - } + self.value } pub(super) fn inputs(&self) -> &[FundingTxInput] { - match self { - SpliceContribution::SpliceIn { inputs, .. } => &inputs[..], - SpliceContribution::SpliceOut { .. } => &[], - } + &self.inputs[..] } pub(super) fn outputs(&self) -> &[TxOut] { - match self { - SpliceContribution::SpliceIn { .. } => &[], - SpliceContribution::SpliceOut { outputs } => &outputs[..], - } + &self.outputs[..] } pub(super) fn into_tx_parts(self) -> (Vec, Vec, Option) { - match self { - SpliceContribution::SpliceIn { inputs, change_script, .. } => { - (inputs, vec![], change_script) - }, - SpliceContribution::SpliceOut { outputs } => (vec![], outputs, None), - } + let SpliceContribution { value: _, inputs, outputs, change_script } = self; + (inputs, outputs, change_script) } } diff --git a/lightning/src/ln/splicing_tests.rs b/lightning/src/ln/splicing_tests.rs index a96af7bbc5d..fdd4fa3d4cd 100644 --- a/lightning/src/ln/splicing_tests.rs +++ b/lightning/src/ln/splicing_tests.rs @@ -48,11 +48,8 @@ fn test_v1_splice_in_negative_insufficient_inputs() { let funding_inputs = create_dual_funding_utxos_with_prev_txs(&nodes[0], &[extra_splice_funding_input_sats]); - let contribution = SpliceContribution::SpliceIn { - value: Amount::from_sat(splice_in_sats), - inputs: funding_inputs, - change_script: None, - }; + let contribution = + SpliceContribution::splice_in(Amount::from_sat(splice_in_sats), funding_inputs, None); // Initiate splice-in, with insufficient input contribution let res = nodes[0].node.splice_channel( @@ -425,12 +422,10 @@ fn do_test_splice_state_reset_on_disconnect(reload: bool) { let (_, _, channel_id, _) = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, 50_000_000); - let contribution = SpliceContribution::SpliceOut { - outputs: vec![TxOut { - value: Amount::from_sat(1_000), - script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), - }], - }; + let contribution = SpliceContribution::splice_out(vec![TxOut { + value: Amount::from_sat(1_000), + script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), + }]); nodes[0] .node .splice_channel( @@ -683,12 +678,10 @@ fn test_config_reject_inbound_splices() { let (_, _, channel_id, _) = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, 50_000_000); - let contribution = SpliceContribution::SpliceOut { - outputs: vec![TxOut { - value: Amount::from_sat(1_000), - script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), - }], - }; + let contribution = SpliceContribution::splice_out(vec![TxOut { + value: Amount::from_sat(1_000), + script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), + }]); nodes[0] .node .splice_channel( @@ -746,14 +739,14 @@ fn test_splice_in() { let coinbase_tx1 = provide_anchor_reserves(&nodes); let coinbase_tx2 = provide_anchor_reserves(&nodes); - let initiator_contribution = SpliceContribution::SpliceIn { - value: Amount::from_sat(initial_channel_value_sat * 2), - inputs: vec![ + let initiator_contribution = SpliceContribution::splice_in( + Amount::from_sat(initial_channel_value_sat * 2), + vec![ FundingTxInput::new_p2wpkh(coinbase_tx1, 0).unwrap(), FundingTxInput::new_p2wpkh(coinbase_tx2, 0).unwrap(), ], - change_script: Some(nodes[0].wallet_source.get_change_script().unwrap()), - }; + Some(nodes[0].wallet_source.get_change_script().unwrap()), + ); let splice_tx = splice_channel(&nodes[0], &nodes[1], channel_id, initiator_contribution); mine_transaction(&nodes[0], &splice_tx); @@ -785,18 +778,16 @@ fn test_splice_out() { let _ = send_payment(&nodes[0], &[&nodes[1]], 100_000); - let initiator_contribution = SpliceContribution::SpliceOut { - outputs: vec![ - TxOut { - value: Amount::from_sat(initial_channel_value_sat / 4), - script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), - }, - TxOut { - value: Amount::from_sat(initial_channel_value_sat / 4), - script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(), - }, - ], - }; + let initiator_contribution = SpliceContribution::splice_out(vec![ + TxOut { + value: Amount::from_sat(initial_channel_value_sat / 4), + script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), + }, + TxOut { + value: Amount::from_sat(initial_channel_value_sat / 4), + script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(), + }, + ]); let splice_tx = splice_channel(&nodes[0], &nodes[1], channel_id, initiator_contribution); mine_transaction(&nodes[0], &splice_tx); @@ -854,11 +845,11 @@ fn do_test_splice_commitment_broadcast(splice_status: SpliceStatus, claim_htlcs: let payment_amount = 1_000_000; let (preimage1, payment_hash1, ..) = route_payment(&nodes[0], &[&nodes[1]], payment_amount); let splice_in_amount = initial_channel_capacity / 2; - let initiator_contribution = SpliceContribution::SpliceIn { - value: Amount::from_sat(splice_in_amount), - inputs: vec![FundingTxInput::new_p2wpkh(coinbase_tx.clone(), 0).unwrap()], - change_script: Some(nodes[0].wallet_source.get_change_script().unwrap()), - }; + let initiator_contribution = SpliceContribution::splice_in( + Amount::from_sat(splice_in_amount), + vec![FundingTxInput::new_p2wpkh(coinbase_tx.clone(), 0).unwrap()], + Some(nodes[0].wallet_source.get_change_script().unwrap()), + ); let splice_tx = splice_channel(&nodes[0], &nodes[1], channel_id, initiator_contribution); let (preimage2, payment_hash2, ..) = route_payment(&nodes[0], &[&nodes[1]], payment_amount); let htlc_expiry = nodes[0].best_block_info().1 + TEST_FINAL_CLTV + LATENCY_GRACE_PERIOD_BLOCKS; @@ -1052,18 +1043,16 @@ fn do_test_splice_reestablish(reload: bool, async_monitor_update: bool) { route_payment(&nodes[0], &[&nodes[1]], 1_000_000); // Negotiate the splice up until the nodes exchange `tx_complete`. - let initiator_contribution = SpliceContribution::SpliceOut { - outputs: vec![ - TxOut { - value: Amount::from_sat(initial_channel_value_sat / 4), - script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), - }, - TxOut { - value: Amount::from_sat(initial_channel_value_sat / 4), - script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(), - }, - ], - }; + let initiator_contribution = SpliceContribution::splice_out(vec![ + TxOut { + value: Amount::from_sat(initial_channel_value_sat / 4), + script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), + }, + TxOut { + value: Amount::from_sat(initial_channel_value_sat / 4), + script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(), + }, + ]); let initial_commit_sig_for_acceptor = negotiate_splice_tx(&nodes[0], &nodes[1], channel_id, initiator_contribution); assert_eq!(initial_commit_sig_for_acceptor.htlc_signatures.len(), 1); @@ -1340,12 +1329,10 @@ fn do_test_propose_splice_while_disconnected(reload: bool, use_0conf: bool) { nodes[1].node.peer_disconnected(node_id_0); let splice_out_sat = initial_channel_value_sat / 4; - let node_0_contribution = SpliceContribution::SpliceOut { - outputs: vec![TxOut { - value: Amount::from_sat(splice_out_sat), - script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), - }], - }; + let node_0_contribution = SpliceContribution::splice_out(vec![TxOut { + value: Amount::from_sat(splice_out_sat), + script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), + }]); nodes[0] .node .splice_channel( @@ -1358,12 +1345,10 @@ fn do_test_propose_splice_while_disconnected(reload: bool, use_0conf: bool) { .unwrap(); assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty()); - let node_1_contribution = SpliceContribution::SpliceOut { - outputs: vec![TxOut { - value: Amount::from_sat(splice_out_sat), - script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(), - }], - }; + let node_1_contribution = SpliceContribution::splice_out(vec![TxOut { + value: Amount::from_sat(splice_out_sat), + script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(), + }]); nodes[1] .node .splice_channel( @@ -1616,11 +1601,11 @@ fn disconnect_on_unexpected_interactive_tx_message() { let coinbase_tx = provide_anchor_reserves(&nodes); let splice_in_amount = initial_channel_capacity / 2; - let contribution = SpliceContribution::SpliceIn { - value: Amount::from_sat(splice_in_amount), - inputs: vec![FundingTxInput::new_p2wpkh(coinbase_tx, 0).unwrap()], - change_script: Some(nodes[0].wallet_source.get_change_script().unwrap()), - }; + let contribution = SpliceContribution::splice_in( + Amount::from_sat(splice_in_amount), + vec![FundingTxInput::new_p2wpkh(coinbase_tx, 0).unwrap()], + Some(nodes[0].wallet_source.get_change_script().unwrap()), + ); // Complete interactive-tx construction, but fail by having the acceptor send a duplicate // tx_complete instead of commitment_signed. @@ -1656,11 +1641,11 @@ fn fail_splice_on_interactive_tx_error() { let coinbase_tx = provide_anchor_reserves(&nodes); let splice_in_amount = initial_channel_capacity / 2; - let contribution = SpliceContribution::SpliceIn { - value: Amount::from_sat(splice_in_amount), - inputs: vec![FundingTxInput::new_p2wpkh(coinbase_tx, 0).unwrap()], - change_script: Some(nodes[0].wallet_source.get_change_script().unwrap()), - }; + let contribution = SpliceContribution::splice_in( + Amount::from_sat(splice_in_amount), + vec![FundingTxInput::new_p2wpkh(coinbase_tx, 0).unwrap()], + Some(nodes[0].wallet_source.get_change_script().unwrap()), + ); // Fail during interactive-tx construction by having the acceptor echo back tx_add_input instead // of sending tx_complete. The failure occurs because the serial id will have the wrong parity. @@ -1762,11 +1747,11 @@ fn fail_splice_on_tx_abort() { let coinbase_tx = provide_anchor_reserves(&nodes); let splice_in_amount = initial_channel_capacity / 2; - let contribution = SpliceContribution::SpliceIn { - value: Amount::from_sat(splice_in_amount), - inputs: vec![FundingTxInput::new_p2wpkh(coinbase_tx, 0).unwrap()], - change_script: Some(nodes[0].wallet_source.get_change_script().unwrap()), - }; + let contribution = SpliceContribution::splice_in( + Amount::from_sat(splice_in_amount), + vec![FundingTxInput::new_p2wpkh(coinbase_tx, 0).unwrap()], + Some(nodes[0].wallet_source.get_change_script().unwrap()), + ); // Fail during interactive-tx construction by having the acceptor send tx_abort instead of // tx_complete. @@ -1816,11 +1801,11 @@ fn fail_splice_on_channel_close() { let coinbase_tx = provide_anchor_reserves(&nodes); let splice_in_amount = initial_channel_capacity / 2; - let contribution = SpliceContribution::SpliceIn { - value: Amount::from_sat(splice_in_amount), - inputs: vec![FundingTxInput::new_p2wpkh(coinbase_tx, 0).unwrap()], - change_script: Some(nodes[0].wallet_source.get_change_script().unwrap()), - }; + let contribution = SpliceContribution::splice_in( + Amount::from_sat(splice_in_amount), + vec![FundingTxInput::new_p2wpkh(coinbase_tx, 0).unwrap()], + Some(nodes[0].wallet_source.get_change_script().unwrap()), + ); // Close the channel before completion of interactive-tx construction. let _ = complete_splice_handshake(initiator, acceptor, channel_id, contribution.clone()); @@ -1867,11 +1852,11 @@ fn fail_quiescent_action_on_channel_close() { let coinbase_tx = provide_anchor_reserves(&nodes); let splice_in_amount = initial_channel_capacity / 2; - let contribution = SpliceContribution::SpliceIn { - value: Amount::from_sat(splice_in_amount), - inputs: vec![FundingTxInput::new_p2wpkh(coinbase_tx, 0).unwrap()], - change_script: Some(nodes[0].wallet_source.get_change_script().unwrap()), - }; + let contribution = SpliceContribution::splice_in( + Amount::from_sat(splice_in_amount), + vec![FundingTxInput::new_p2wpkh(coinbase_tx, 0).unwrap()], + Some(nodes[0].wallet_source.get_change_script().unwrap()), + ); // Close the channel before completion of STFU handshake. initiator @@ -1960,23 +1945,19 @@ fn do_test_splice_with_inflight_htlc_forward_and_resolution(expire_scid_pre_forw // Splice both channels, lock them, and connect enough blocks to trigger the legacy SCID pruning // logic while the HTLC is still pending. - let contribution = SpliceContribution::SpliceOut { - outputs: vec![TxOut { - value: Amount::from_sat(1_000), - script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), - }], - }; + let contribution = SpliceContribution::splice_out(vec![TxOut { + value: Amount::from_sat(1_000), + script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), + }]); let splice_tx_0_1 = splice_channel(&nodes[0], &nodes[1], channel_id_0_1, contribution); for node in &nodes { mine_transaction(node, &splice_tx_0_1); } - let contribution = SpliceContribution::SpliceOut { - outputs: vec![TxOut { - value: Amount::from_sat(1_000), - script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(), - }], - }; + let contribution = SpliceContribution::splice_out(vec![TxOut { + value: Amount::from_sat(1_000), + script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(), + }]); let splice_tx_1_2 = splice_channel(&nodes[1], &nodes[2], channel_id_1_2, contribution); for node in &nodes { mine_transaction(node, &splice_tx_1_2); From 2244a31efa01a20e585b5a57ed5c0c5e70ed5f90 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Thu, 4 Dec 2025 14:52:10 -0600 Subject: [PATCH 02/13] Use Amount in calculate_change_output_value --- lightning/src/ln/channel.rs | 6 ++--- lightning/src/ln/interactivetxs.rs | 40 ++++++++++++++++++------------ 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 5b4ac4c0aa5..84c3701a16e 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -6703,12 +6703,12 @@ impl FundingNegotiationContext { }, } }; - let mut change_output = - TxOut { value: Amount::from_sat(change_value), script_pubkey: change_script }; + let mut change_output = TxOut { value: change_value, script_pubkey: change_script }; let change_output_weight = get_output_weight(&change_output.script_pubkey).to_wu(); let change_output_fee = fee_for_weight(self.funding_feerate_sat_per_1000_weight, change_output_weight); - let change_value_decreased_with_fee = change_value.saturating_sub(change_output_fee); + let change_value_decreased_with_fee = + change_value.to_sat().saturating_sub(change_output_fee); // Check dust limit again if change_value_decreased_with_fee > context.holder_dust_limit_satoshis { change_output.value = Amount::from_sat(change_value_decreased_with_fee); diff --git a/lightning/src/ln/interactivetxs.rs b/lightning/src/ln/interactivetxs.rs index 4340aad420a..1ab9c6c68ee 100644 --- a/lightning/src/ln/interactivetxs.rs +++ b/lightning/src/ln/interactivetxs.rs @@ -2337,22 +2337,23 @@ impl InteractiveTxConstructor { pub(super) fn calculate_change_output_value( context: &FundingNegotiationContext, is_splice: bool, shared_output_funding_script: &ScriptBuf, change_output_dust_limit: u64, -) -> Result, AbortReason> { +) -> Result, AbortReason> { assert!(context.our_funding_contribution > SignedAmount::ZERO); - let our_funding_contribution_satoshis = context.our_funding_contribution.to_sat() as u64; + let our_funding_contribution = context.our_funding_contribution.to_unsigned().unwrap(); - let mut total_input_satoshis = 0u64; + let mut total_input_value = Amount::ZERO; let mut our_funding_inputs_weight = 0u64; for FundingTxInput { utxo, .. } in context.our_funding_inputs.iter() { - total_input_satoshis = total_input_satoshis.saturating_add(utxo.output.value.to_sat()); + total_input_value = total_input_value.checked_add(utxo.output.value).unwrap_or(Amount::MAX); let weight = BASE_INPUT_WEIGHT + utxo.satisfaction_weight; our_funding_inputs_weight = our_funding_inputs_weight.saturating_add(weight); } let funding_outputs = &context.our_funding_outputs; - let total_output_satoshis = - funding_outputs.iter().fold(0u64, |total, out| total.saturating_add(out.value.to_sat())); + let total_output_value = funding_outputs + .iter() + .fold(Amount::ZERO, |total, out| total.checked_add(out.value).unwrap_or(Amount::MAX)); let our_funding_outputs_weight = funding_outputs.iter().fold(0u64, |weight, out| { weight.saturating_add(get_output_weight(&out.script_pubkey).to_wu()) }); @@ -2376,15 +2377,22 @@ pub(super) fn calculate_change_output_value( } } - let fees_sats = fee_for_weight(context.funding_feerate_sat_per_1000_weight, weight); - let net_total_less_fees = - total_input_satoshis.saturating_sub(total_output_satoshis).saturating_sub(fees_sats); - if net_total_less_fees < our_funding_contribution_satoshis { + let contributed_fees = + Amount::from_sat(fee_for_weight(context.funding_feerate_sat_per_1000_weight, weight)); + let net_total_less_fees = total_input_value + .checked_sub(total_output_value) + .unwrap_or(Amount::ZERO) + .checked_sub(contributed_fees) + .unwrap_or(Amount::ZERO); + if net_total_less_fees < our_funding_contribution { // Not enough to cover contribution plus fees return Err(AbortReason::InsufficientFees); } - let remaining_value = net_total_less_fees.saturating_sub(our_funding_contribution_satoshis); - if remaining_value < change_output_dust_limit { + + let remaining_value = net_total_less_fees + .checked_sub(our_funding_contribution) + .expect("remaining_value should not be negative"); + if remaining_value.to_sat() < change_output_dust_limit { // Enough to cover contribution plus fees, but leftover is below dust limit; no change Ok(None) } else { @@ -3440,14 +3448,14 @@ mod tests { total_inputs - total_outputs - context.our_funding_contribution.to_unsigned().unwrap(); assert_eq!( calculate_change_output_value(&context, false, &ScriptBuf::new(), 300), - Ok(Some((gross_change - fees - common_fees).to_sat())), + Ok(Some(gross_change - fees - common_fees)), ); // There is leftover for change, without common fees let context = FundingNegotiationContext { is_initiator: false, ..context }; assert_eq!( calculate_change_output_value(&context, false, &ScriptBuf::new(), 300), - Ok(Some((gross_change - fees).to_sat())), + Ok(Some(gross_change - fees)), ); // Insufficient inputs, no leftover @@ -3482,7 +3490,7 @@ mod tests { total_inputs - total_outputs - context.our_funding_contribution.to_unsigned().unwrap(); assert_eq!( calculate_change_output_value(&context, false, &ScriptBuf::new(), 100), - Ok(Some((gross_change - fees).to_sat())), + Ok(Some(gross_change - fees)), ); // Larger fee, smaller change @@ -3496,7 +3504,7 @@ mod tests { total_inputs - total_outputs - context.our_funding_contribution.to_unsigned().unwrap(); assert_eq!( calculate_change_output_value(&context, false, &ScriptBuf::new(), 300), - Ok(Some((gross_change - fees * 3 - common_fees * 3).to_sat())), + Ok(Some(gross_change - fees * 3 - common_fees * 3)), ); } From f34cec3a257435135d4778aaa6b3090453586c20 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Thu, 4 Dec 2025 17:18:33 -0600 Subject: [PATCH 03/13] Check change value in test_splice_in --- lightning/src/ln/splicing_tests.rs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/lightning/src/ln/splicing_tests.rs b/lightning/src/ln/splicing_tests.rs index fdd4fa3d4cd..5ef5666d9a7 100644 --- a/lightning/src/ln/splicing_tests.rs +++ b/lightning/src/ln/splicing_tests.rs @@ -27,8 +27,9 @@ use crate::util::errors::APIError; use crate::util::ser::Writeable; use crate::util::test_channel_signer::SignerOp; +use bitcoin::hashes::Hash; use bitcoin::secp256k1::PublicKey; -use bitcoin::{Amount, OutPoint as BitcoinOutPoint, ScriptBuf, Transaction, TxOut}; +use bitcoin::{Amount, OutPoint as BitcoinOutPoint, ScriptBuf, Transaction, TxOut, WPubkeyHash}; #[test] fn test_v1_splice_in_negative_insufficient_inputs() { @@ -739,16 +740,27 @@ fn test_splice_in() { let coinbase_tx1 = provide_anchor_reserves(&nodes); let coinbase_tx2 = provide_anchor_reserves(&nodes); + + let added_value = Amount::from_sat(initial_channel_value_sat * 2); + let change_script = ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros()); + let fees = Amount::from_sat(321); + let initiator_contribution = SpliceContribution::splice_in( - Amount::from_sat(initial_channel_value_sat * 2), + added_value, vec![ FundingTxInput::new_p2wpkh(coinbase_tx1, 0).unwrap(), FundingTxInput::new_p2wpkh(coinbase_tx2, 0).unwrap(), ], - Some(nodes[0].wallet_source.get_change_script().unwrap()), + Some(change_script.clone()), ); let splice_tx = splice_channel(&nodes[0], &nodes[1], channel_id, initiator_contribution); + let expected_change = Amount::ONE_BTC * 2 - added_value - fees; + assert_eq!( + splice_tx.output.iter().find(|txout| txout.script_pubkey == change_script).unwrap().value, + expected_change, + ); + mine_transaction(&nodes[0], &splice_tx); mine_transaction(&nodes[1], &splice_tx); From 175bf795245340834661b8927dcb9ca964aa0a61 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Thu, 4 Dec 2025 11:25:01 -0600 Subject: [PATCH 04/13] 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. --- lightning/src/ln/channel.rs | 149 ++++++++++++++++++++------ lightning/src/ln/funding.rs | 32 +++++- lightning/src/ln/interactivetxs.rs | 23 ++-- lightning/src/ln/splicing_tests.rs | 164 +++++++++++++++++++++++++++++ 4 files changed, 322 insertions(+), 46 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 84c3701a16e..837279419d4 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -6497,8 +6497,7 @@ fn get_v2_channel_reserve_satoshis(channel_value_satoshis: u64, dust_limit_satos fn check_splice_contribution_sufficient( contribution: &SpliceContribution, is_initiator: bool, funding_feerate: FeeRate, ) -> Result { - let contribution_amount = contribution.value(); - if contribution_amount < SignedAmount::ZERO { + if contribution.inputs().is_empty() { let estimated_fee = Amount::from_sat(estimate_v2_funding_transaction_fee( contribution.inputs(), contribution.outputs(), @@ -6507,20 +6506,26 @@ fn check_splice_contribution_sufficient( funding_feerate.to_sat_per_kwu() as u32, )); + let contribution_amount = contribution.net_value(); contribution_amount .checked_sub( estimated_fee.to_signed().expect("fees should never exceed Amount::MAX_MONEY"), ) - .ok_or(format!("Our {contribution_amount} contribution plus the fee estimate exceeds the total bitcoin supply")) + .ok_or(format!( + "{} splice-out amount plus {} fee estimate exceeds the total bitcoin supply", + contribution_amount.unsigned_abs(), + estimated_fee, + )) } else { check_v2_funding_inputs_sufficient( - contribution_amount.to_sat(), + contribution.input_value(), contribution.inputs(), + contribution.outputs(), is_initiator, true, funding_feerate.to_sat_per_kwu() as u32, ) - .map(|_| contribution_amount) + .map(|_| contribution.net_value()) } } @@ -6579,16 +6584,16 @@ fn estimate_v2_funding_transaction_fee( /// Returns estimated (partial) fees as additional information #[rustfmt::skip] fn check_v2_funding_inputs_sufficient( - contribution_amount: i64, funding_inputs: &[FundingTxInput], is_initiator: bool, - is_splice: bool, funding_feerate_sat_per_1000_weight: u32, -) -> Result { - let estimated_fee = estimate_v2_funding_transaction_fee( - funding_inputs, &[], is_initiator, is_splice, funding_feerate_sat_per_1000_weight, - ); - - let mut total_input_sats = 0u64; + contributed_input_value: Amount, funding_inputs: &[FundingTxInput], outputs: &[TxOut], + is_initiator: bool, is_splice: bool, funding_feerate_sat_per_1000_weight: u32, +) -> Result { + let estimated_fee = Amount::from_sat(estimate_v2_funding_transaction_fee( + funding_inputs, outputs, is_initiator, is_splice, funding_feerate_sat_per_1000_weight, + )); + + let mut total_input_value = Amount::ZERO; for FundingTxInput { utxo, .. } in funding_inputs.iter() { - total_input_sats = total_input_sats.checked_add(utxo.output.value.to_sat()) + total_input_value = total_input_value.checked_add(utxo.output.value) .ok_or("Sum of input values is greater than the total bitcoin supply")?; } @@ -6603,13 +6608,11 @@ fn check_v2_funding_inputs_sufficient( // TODO(splicing): refine check including the fact wether a change will be added or not. // Can be done once dual funding preparation is included. - let minimal_input_amount_needed = contribution_amount.checked_add(estimated_fee as i64) - .ok_or(format!("Our {contribution_amount} contribution plus the fee estimate exceeds the total bitcoin supply"))?; - if i64::try_from(total_input_sats).map_err(|_| "Sum of input values is greater than the total bitcoin supply")? - < minimal_input_amount_needed - { + let minimal_input_amount_needed = contributed_input_value.checked_add(estimated_fee) + .ok_or(format!("{contributed_input_value} contribution plus {estimated_fee} fee estimate exceeds the total bitcoin supply"))?; + if total_input_value < minimal_input_amount_needed { Err(format!( - "Total input amount {total_input_sats} is lower than needed for contribution {contribution_amount}, considering fees of {estimated_fee}. Need more inputs.", + "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.", )) } else { Ok(estimated_fee) @@ -6675,7 +6678,7 @@ impl FundingNegotiationContext { }; // Optionally add change output - let change_value_opt = if self.our_funding_contribution > SignedAmount::ZERO { + let change_value_opt = if !self.our_funding_inputs.is_empty() { match calculate_change_output_value( &self, self.shared_funding_input.is_some(), @@ -11956,7 +11959,7 @@ where }); } - let our_funding_contribution = contribution.value(); + let our_funding_contribution = contribution.net_value(); if our_funding_contribution == SignedAmount::ZERO { return Err(APIError::APIMisuseError { err: format!( @@ -18254,6 +18257,13 @@ mod tests { FundingTxInput::new_p2wpkh(prevtx, 0).unwrap() } + fn funding_output_sats(output_value_sats: u64) -> TxOut { + TxOut { + value: Amount::from_sat(output_value_sats), + script_pubkey: ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros()), + } + } + #[test] #[rustfmt::skip] fn test_check_v2_funding_inputs_sufficient() { @@ -18264,16 +18274,83 @@ mod tests { let expected_fee = if cfg!(feature = "grind_signatures") { 2278 } else { 2284 }; assert_eq!( check_v2_funding_inputs_sufficient( - 220_000, + Amount::from_sat(220_000), + &[ + funding_input_sats(200_000), + funding_input_sats(100_000), + ], + &[], + true, + true, + 2000, + ).unwrap(), + Amount::from_sat(expected_fee), + ); + } + + // Net splice-in + { + let expected_fee = if cfg!(feature = "grind_signatures") { 2526 } else { 2532 }; + assert_eq!( + check_v2_funding_inputs_sufficient( + Amount::from_sat(220_000), + &[ + funding_input_sats(200_000), + funding_input_sats(100_000), + ], + &[ + funding_output_sats(200_000), + ], + true, + true, + 2000, + ).unwrap(), + Amount::from_sat(expected_fee), + ); + } + + // Net splice-out + { + let expected_fee = if cfg!(feature = "grind_signatures") { 2526 } else { 2532 }; + assert_eq!( + check_v2_funding_inputs_sufficient( + Amount::from_sat(220_000), &[ funding_input_sats(200_000), funding_input_sats(100_000), ], + &[ + funding_output_sats(400_000), + ], true, true, 2000, ).unwrap(), - expected_fee, + Amount::from_sat(expected_fee), + ); + } + + // Net splice-out, inputs insufficient to cover fees + { + let expected_fee = if cfg!(feature = "grind_signatures") { 113670 } else { 113940 }; + assert_eq!( + check_v2_funding_inputs_sufficient( + Amount::from_sat(220_000), + &[ + funding_input_sats(200_000), + funding_input_sats(100_000), + ], + &[ + funding_output_sats(400_000), + ], + true, + true, + 90000, + ), + Err(format!( + "Total input amount 0.00300000 BTC is lower than needed for splice-in contribution 0.00220000 BTC, considering fees of {}. Need more inputs.", + Amount::from_sat(expected_fee), + )), ); } @@ -18282,17 +18359,18 @@ mod tests { let expected_fee = if cfg!(feature = "grind_signatures") { 1736 } else { 1740 }; assert_eq!( check_v2_funding_inputs_sufficient( - 220_000, + Amount::from_sat(220_000), &[ funding_input_sats(100_000), ], + &[], true, true, 2000, ), Err(format!( - "Total input amount 100000 is lower than needed for contribution 220000, considering fees of {}. Need more inputs.", - expected_fee, + "Total input amount 0.00100000 BTC is lower than needed for splice-in contribution 0.00220000 BTC, considering fees of {}. Need more inputs.", + Amount::from_sat(expected_fee), )), ); } @@ -18302,16 +18380,17 @@ mod tests { let expected_fee = if cfg!(feature = "grind_signatures") { 2278 } else { 2284 }; assert_eq!( check_v2_funding_inputs_sufficient( - (300_000 - expected_fee - 20) as i64, + Amount::from_sat(300_000 - expected_fee - 20), &[ funding_input_sats(200_000), funding_input_sats(100_000), ], + &[], true, true, 2000, ).unwrap(), - expected_fee, + Amount::from_sat(expected_fee), ); } @@ -18320,18 +18399,19 @@ mod tests { let expected_fee = if cfg!(feature = "grind_signatures") { 2506 } else { 2513 }; assert_eq!( check_v2_funding_inputs_sufficient( - 298032, + Amount::from_sat(298032), &[ funding_input_sats(200_000), funding_input_sats(100_000), ], + &[], true, true, 2200, ), Err(format!( - "Total input amount 300000 is lower than needed for contribution 298032, considering fees of {}. Need more inputs.", - expected_fee + "Total input amount 0.00300000 BTC is lower than needed for splice-in contribution 0.00298032 BTC, considering fees of {}. Need more inputs.", + Amount::from_sat(expected_fee), )), ); } @@ -18341,16 +18421,17 @@ mod tests { let expected_fee = if cfg!(feature = "grind_signatures") { 1084 } else { 1088 }; assert_eq!( check_v2_funding_inputs_sufficient( - (300_000 - expected_fee - 20) as i64, + Amount::from_sat(300_000 - expected_fee - 20), &[ funding_input_sats(200_000), funding_input_sats(100_000), ], + &[], false, false, 2000, ).unwrap(), - expected_fee, + Amount::from_sat(expected_fee), ); } } diff --git a/lightning/src/ln/funding.rs b/lightning/src/ln/funding.rs index b7f8740f737..b6068b739bb 100644 --- a/lightning/src/ln/funding.rs +++ b/lightning/src/ln/funding.rs @@ -61,10 +61,40 @@ impl SpliceContribution { Self { value: -value_removed, inputs: vec![], outputs, change_script: None } } - pub(super) fn value(&self) -> SignedAmount { + /// Creates a contribution for when funds are both added to and removed from a channel. + /// + /// Note that `value_added` represents the value added by `inputs` but should not account for + /// value removed by `outputs`. The net value contributed can be obtained by calling + /// [`SpliceContribution::net_value`]. + pub fn splice_in_and_out( + value_added: Amount, inputs: Vec, outputs: Vec, + change_script: Option, + ) -> Self { + let splice_in = Self::splice_in(value_added, inputs, change_script); + let splice_out = Self::splice_out(outputs); + + Self { + value: splice_in.value + splice_out.value, + inputs: splice_in.inputs, + outputs: splice_out.outputs, + change_script: splice_in.change_script, + } + } + + /// The net value contributed to a channel by the splice. If negative, more value will be + /// spliced out than spliced in. + pub fn net_value(&self) -> SignedAmount { self.value } + pub(super) fn input_value(&self) -> Amount { + (self.net_value() + self.output_value().to_signed().expect("")).to_unsigned().expect("") + } + + pub(super) fn output_value(&self) -> Amount { + self.outputs.iter().map(|txout| txout.value).sum::() + } + pub(super) fn inputs(&self) -> &[FundingTxInput] { &self.inputs[..] } diff --git a/lightning/src/ln/interactivetxs.rs b/lightning/src/ln/interactivetxs.rs index 1ab9c6c68ee..7ed829886c6 100644 --- a/lightning/src/ln/interactivetxs.rs +++ b/lightning/src/ln/interactivetxs.rs @@ -2338,9 +2338,6 @@ pub(super) fn calculate_change_output_value( context: &FundingNegotiationContext, is_splice: bool, shared_output_funding_script: &ScriptBuf, change_output_dust_limit: u64, ) -> Result, AbortReason> { - assert!(context.our_funding_contribution > SignedAmount::ZERO); - let our_funding_contribution = context.our_funding_contribution.to_unsigned().unwrap(); - let mut total_input_value = Amount::ZERO; let mut our_funding_inputs_weight = 0u64; for FundingTxInput { utxo, .. } in context.our_funding_inputs.iter() { @@ -2354,6 +2351,7 @@ pub(super) fn calculate_change_output_value( let total_output_value = funding_outputs .iter() .fold(Amount::ZERO, |total, out| total.checked_add(out.value).unwrap_or(Amount::MAX)); + let our_funding_outputs_weight = funding_outputs.iter().fold(0u64, |weight, out| { weight.saturating_add(get_output_weight(&out.script_pubkey).to_wu()) }); @@ -2379,18 +2377,21 @@ pub(super) fn calculate_change_output_value( let contributed_fees = Amount::from_sat(fee_for_weight(context.funding_feerate_sat_per_1000_weight, weight)); - let net_total_less_fees = total_input_value - .checked_sub(total_output_value) - .unwrap_or(Amount::ZERO) - .checked_sub(contributed_fees) - .unwrap_or(Amount::ZERO); - if net_total_less_fees < our_funding_contribution { + + let contributed_input_value = + context.our_funding_contribution + total_output_value.to_signed().unwrap(); + assert!(contributed_input_value > SignedAmount::ZERO); + let contributed_input_value = contributed_input_value.unsigned_abs(); + + let total_input_value_less_fees = + total_input_value.checked_sub(contributed_fees).unwrap_or(Amount::ZERO); + if total_input_value_less_fees < contributed_input_value { // Not enough to cover contribution plus fees return Err(AbortReason::InsufficientFees); } - let remaining_value = net_total_less_fees - .checked_sub(our_funding_contribution) + let remaining_value = total_input_value_less_fees + .checked_sub(contributed_input_value) .expect("remaining_value should not be negative"); if remaining_value.to_sat() < change_output_dust_limit { // Enough to cover contribution plus fees, but leftover is below dust limit; no change diff --git a/lightning/src/ln/splicing_tests.rs b/lightning/src/ln/splicing_tests.rs index 5ef5666d9a7..1f037b3dae8 100644 --- a/lightning/src/ln/splicing_tests.rs +++ b/lightning/src/ln/splicing_tests.rs @@ -816,6 +816,170 @@ fn test_splice_out() { let _ = send_payment(&nodes[0], &[&nodes[1]], htlc_limit_msat); } +#[test] +fn test_splice_in_and_out() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let mut config = test_default_channel_config(); + config.channel_handshake_config.max_inbound_htlc_value_in_flight_percent_of_channel = 100; + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, Some(config)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); + + let _ = send_payment(&nodes[0], &[&nodes[1]], 100_000); + + let coinbase_tx1 = provide_anchor_reserves(&nodes); + let coinbase_tx2 = provide_anchor_reserves(&nodes); + + // Contribute a net negative value, with fees taken from the contributed inputs and the + // remaining value sent to change + let htlc_limit_msat = nodes[0].node.list_channels()[0].next_outbound_htlc_limit_msat; + let added_value = Amount::from_sat(htlc_limit_msat / 1000); + let removed_value = added_value * 2; + let change_script = ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros()); + let fees = if cfg!(feature = "grind_signatures") { + Amount::from_sat(383) + } else { + Amount::from_sat(384) + }; + + assert!(htlc_limit_msat > initial_channel_value_sat / 2 * 1000); + + let initiator_contribution = SpliceContribution::splice_in_and_out( + added_value, + vec![ + FundingTxInput::new_p2wpkh(coinbase_tx1, 0).unwrap(), + FundingTxInput::new_p2wpkh(coinbase_tx2, 0).unwrap(), + ], + vec![ + TxOut { + value: removed_value / 2, + script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), + }, + TxOut { + value: removed_value / 2, + script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(), + }, + ], + Some(change_script.clone()), + ); + + let splice_tx = splice_channel(&nodes[0], &nodes[1], channel_id, initiator_contribution); + let expected_change = Amount::ONE_BTC * 2 - added_value - fees; + assert_eq!( + splice_tx.output.iter().find(|txout| txout.script_pubkey == change_script).unwrap().value, + expected_change, + ); + + mine_transaction(&nodes[0], &splice_tx); + mine_transaction(&nodes[1], &splice_tx); + + let htlc_limit_msat = nodes[0].node.list_channels()[0].next_outbound_htlc_limit_msat; + assert!(htlc_limit_msat < added_value.to_sat() * 1000); + let _ = send_payment(&nodes[0], &[&nodes[1]], htlc_limit_msat); + + lock_splice_after_blocks(&nodes[0], &nodes[1], ANTI_REORG_DELAY - 1); + + let htlc_limit_msat = nodes[0].node.list_channels()[0].next_outbound_htlc_limit_msat; + assert!(htlc_limit_msat < added_value.to_sat() * 1000); + let _ = send_payment(&nodes[0], &[&nodes[1]], htlc_limit_msat); + + let coinbase_tx1 = provide_anchor_reserves(&nodes); + let coinbase_tx2 = provide_anchor_reserves(&nodes); + + // Contribute a net positive value, with fees taken from the contributed inputs and the + // remaining value sent to change + let added_value = Amount::from_sat(initial_channel_value_sat * 2); + let removed_value = added_value / 2; + let change_script = ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros()); + let fees = if cfg!(feature = "grind_signatures") { + Amount::from_sat(383) + } else { + Amount::from_sat(384) + }; + + let initiator_contribution = SpliceContribution::splice_in_and_out( + added_value, + vec![ + FundingTxInput::new_p2wpkh(coinbase_tx1, 0).unwrap(), + FundingTxInput::new_p2wpkh(coinbase_tx2, 0).unwrap(), + ], + vec![ + TxOut { + value: removed_value / 2, + script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), + }, + TxOut { + value: removed_value / 2, + script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(), + }, + ], + Some(change_script.clone()), + ); + + let splice_tx = splice_channel(&nodes[0], &nodes[1], channel_id, initiator_contribution); + let expected_change = Amount::ONE_BTC * 2 - added_value - fees; + assert_eq!( + splice_tx.output.iter().find(|txout| txout.script_pubkey == change_script).unwrap().value, + expected_change, + ); + + mine_transaction(&nodes[0], &splice_tx); + mine_transaction(&nodes[1], &splice_tx); + + let htlc_limit_msat = nodes[0].node.list_channels()[0].next_outbound_htlc_limit_msat; + assert_eq!(htlc_limit_msat, 0); + + lock_splice_after_blocks(&nodes[0], &nodes[1], ANTI_REORG_DELAY - 1); + + let htlc_limit_msat = nodes[0].node.list_channels()[0].next_outbound_htlc_limit_msat; + assert!(htlc_limit_msat > initial_channel_value_sat / 2 * 1000); + let _ = send_payment(&nodes[0], &[&nodes[1]], htlc_limit_msat); + + let coinbase_tx1 = provide_anchor_reserves(&nodes); + let coinbase_tx2 = provide_anchor_reserves(&nodes); + + // Fail adding a net contribution value of zero + let added_value = Amount::from_sat(initial_channel_value_sat * 2); + let removed_value = added_value; + let change_script = ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros()); + + let initiator_contribution = SpliceContribution::splice_in_and_out( + added_value, + vec![ + FundingTxInput::new_p2wpkh(coinbase_tx1, 0).unwrap(), + FundingTxInput::new_p2wpkh(coinbase_tx2, 0).unwrap(), + ], + vec![ + TxOut { + value: removed_value / 2, + script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), + }, + TxOut { + value: removed_value / 2, + script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(), + }, + ], + Some(change_script), + ); + + assert_eq!( + nodes[0].node.splice_channel( + &channel_id, + &nodes[1].node.get_our_node_id(), + initiator_contribution, + FEERATE_FLOOR_SATS_PER_KW, + None, + ), + Err(APIError::APIMisuseError { + err: format!("Channel {} cannot be spliced; contribution cannot be zero", channel_id), + }), + ); +} + #[cfg(test)] #[derive(PartialEq)] enum SpliceStatus { From ed9b41a8ccabc6c338b2fcde58e152ece6533069 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Tue, 13 Jan 2026 14:43:07 -0600 Subject: [PATCH 05/13] f - inline --- lightning/src/ln/channel.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 837279419d4..9eab6ad2978 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -6512,9 +6512,8 @@ fn check_splice_contribution_sufficient( estimated_fee.to_signed().expect("fees should never exceed Amount::MAX_MONEY"), ) .ok_or(format!( - "{} splice-out amount plus {} fee estimate exceeds the total bitcoin supply", + "{estimated_fee} splice-out amount plus {} fee estimate exceeds the total bitcoin supply", contribution_amount.unsigned_abs(), - estimated_fee, )) } else { check_v2_funding_inputs_sufficient( From afdc3f2e7201534b90bc905c60cef0ccbd3a2a2b Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Wed, 10 Dec 2025 14:52:35 -0600 Subject: [PATCH 06/13] f - calculate net_value --- lightning/src/ln/channel.rs | 2 +- lightning/src/ln/funding.rs | 50 +++++++++++++++---------------------- 2 files changed, 21 insertions(+), 31 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 9eab6ad2978..233ca211bc4 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -6517,7 +6517,7 @@ fn check_splice_contribution_sufficient( )) } else { check_v2_funding_inputs_sufficient( - contribution.input_value(), + contribution.value_added(), contribution.inputs(), contribution.outputs(), is_initiator, diff --git a/lightning/src/ln/funding.rs b/lightning/src/ln/funding.rs index b6068b739bb..8092a0e4451 100644 --- a/lightning/src/ln/funding.rs +++ b/lightning/src/ln/funding.rs @@ -21,8 +21,10 @@ use crate::sign::{P2TR_KEY_PATH_WITNESS_WEIGHT, P2WPKH_WITNESS_WEIGHT}; /// The components of a splice's funding transaction that are contributed by one party. #[derive(Debug, Clone)] pub struct SpliceContribution { - /// The amount to contribute to the splice. - value: SignedAmount, + /// The amount from [`inputs`] to contribute to the splice. + /// + /// [`inputs`]: Self::inputs + value_added: Amount, /// The inputs included in the splice's funding transaction to meet the contributed amount /// plus fees. Any excess amount will be sent to a change output. @@ -42,23 +44,14 @@ pub struct SpliceContribution { impl SpliceContribution { /// Creates a contribution for when funds are only added to a channel. pub fn splice_in( - value: Amount, inputs: Vec, change_script: Option, + value_added: Amount, inputs: Vec, change_script: Option, ) -> Self { - let value_added = value.to_signed().unwrap_or(SignedAmount::MAX); - - Self { value: value_added, inputs, outputs: vec![], change_script } + Self { value_added, inputs, outputs: vec![], change_script } } /// Creates a contribution for when funds are only removed from a channel. pub fn splice_out(outputs: Vec) -> Self { - let value_removed = outputs - .iter() - .map(|txout| txout.value) - .sum::() - .to_signed() - .unwrap_or(SignedAmount::MAX); - - Self { value: -value_removed, inputs: vec![], outputs, change_script: None } + Self { value_added: Amount::ZERO, inputs: vec![], outputs, change_script: None } } /// Creates a contribution for when funds are both added to and removed from a channel. @@ -70,29 +63,26 @@ impl SpliceContribution { value_added: Amount, inputs: Vec, outputs: Vec, change_script: Option, ) -> Self { - let splice_in = Self::splice_in(value_added, inputs, change_script); - let splice_out = Self::splice_out(outputs); - - Self { - value: splice_in.value + splice_out.value, - inputs: splice_in.inputs, - outputs: splice_out.outputs, - change_script: splice_in.change_script, - } + Self { value_added, inputs, outputs, change_script } } /// The net value contributed to a channel by the splice. If negative, more value will be /// spliced out than spliced in. pub fn net_value(&self) -> SignedAmount { - self.value - } + let value_added = self.value_added.to_signed().unwrap_or(SignedAmount::MAX); + let value_removed = self + .outputs + .iter() + .map(|txout| txout.value) + .sum::() + .to_signed() + .unwrap_or(SignedAmount::MAX); - pub(super) fn input_value(&self) -> Amount { - (self.net_value() + self.output_value().to_signed().expect("")).to_unsigned().expect("") + value_added - value_removed } - pub(super) fn output_value(&self) -> Amount { - self.outputs.iter().map(|txout| txout.value).sum::() + pub(super) fn value_added(&self) -> Amount { + self.value_added } pub(super) fn inputs(&self) -> &[FundingTxInput] { @@ -104,7 +94,7 @@ impl SpliceContribution { } pub(super) fn into_tx_parts(self) -> (Vec, Vec, Option) { - let SpliceContribution { value: _, inputs, outputs, change_script } = self; + let SpliceContribution { value_added: _, inputs, outputs, change_script } = self; (inputs, outputs, change_script) } } From fb81fe7cc1e21181159d9e127e1a3237befc6bd9 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Tue, 13 Jan 2026 21:56:54 -0600 Subject: [PATCH 07/13] FIXME: Why does handle_channel_close override the witness? --- lightning/src/events/bump_transaction/mod.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/lightning/src/events/bump_transaction/mod.rs b/lightning/src/events/bump_transaction/mod.rs index 3d9beb82c07..76aa3f8f022 100644 --- a/lightning/src/events/bump_transaction/mod.rs +++ b/lightning/src/events/bump_transaction/mod.rs @@ -1285,14 +1285,8 @@ mod tests { Ok(res) } fn sign_psbt(&self, psbt: Psbt) -> Result { - let mut tx = psbt.unsigned_tx; - for input in tx.input.iter_mut() { - if input.previous_output.txid != Txid::from_byte_array([44; 32]) { - // Channel output, add a realistic size witness to make the assertions happy - input.witness = Witness::from_slice(&[vec![42; 162]]); - } - } - Ok(tx) + // FIXME: Why does handle_channel_close override the witness? + Ok(psbt.unsigned_tx) } } From 6d16896da2a16110892b9039d491b80004dc10a5 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Tue, 13 Jan 2026 16:45:52 -0600 Subject: [PATCH 08/13] Move FundingTxInput::sequence to Utxo A forthcoming commit will change CoinSelection to include FundingTxInput instead of Utxo, though the former will probably be renamed. This is so CoinSelectionSource can be used when funding a splice. Further updating WalletSource to use FundingTxInput is not desirable, however, as it would result in looking up each confirmed UTXOs previous transaction even if it is not selected. See Wallet's implementation of CoinSelectionSource, which delegates to WalletSource for listing all confirmed UTXOs. This commit moves FundingTxInput::sequence to Utxo, and thus the responsibility for setting it to WalletSource implementations. Doing so will allow Wallet's CoinSelectionSource implementation to delegate looking up previous transactions to WalletSource without having to explicitly set the sequence on any FundingTxInput. --- lightning/src/events/bump_transaction/mod.rs | 9 ++++++++- lightning/src/ln/funding.rs | 13 ++++--------- lightning/src/ln/interactivetxs.rs | 8 ++++++-- lightning/src/util/anchor_channel_reserves.rs | 3 ++- 4 files changed, 20 insertions(+), 13 deletions(-) diff --git a/lightning/src/events/bump_transaction/mod.rs b/lightning/src/events/bump_transaction/mod.rs index 76aa3f8f022..4dfc8ef03e6 100644 --- a/lightning/src/events/bump_transaction/mod.rs +++ b/lightning/src/events/bump_transaction/mod.rs @@ -281,12 +281,15 @@ pub struct Utxo { /// with their lengths included, required to satisfy the output's script. The weight consumed by /// the input's `script_sig` must account for [`WITNESS_SCALE_FACTOR`]. pub satisfaction_weight: u64, + /// The sequence number to use in the [`TxIn`] when spending the UTXO. + pub sequence: Sequence, } impl_writeable_tlv_based!(Utxo, { (1, outpoint, required), (3, output, required), (5, satisfaction_weight, required), + (7, sequence, (default_value, Sequence::ENABLE_RBF_NO_LOCKTIME)), }); impl Utxo { @@ -301,6 +304,7 @@ impl Utxo { outpoint, output: TxOut { value, script_pubkey: ScriptBuf::new_p2pkh(pubkey_hash) }, satisfaction_weight: script_sig_size * WITNESS_SCALE_FACTOR as u64 + 1, /* empty witness */ + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, } } @@ -320,6 +324,7 @@ impl Utxo { }, satisfaction_weight: script_sig_size * WITNESS_SCALE_FACTOR as u64 + P2WPKH_WITNESS_WEIGHT, + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, } } @@ -329,6 +334,7 @@ impl Utxo { outpoint, output: TxOut { value, script_pubkey: ScriptBuf::new_p2wpkh(pubkey_hash) }, satisfaction_weight: EMPTY_SCRIPT_SIG_WEIGHT + P2WPKH_WITNESS_WEIGHT, + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, } } } @@ -717,7 +723,7 @@ where tx.input.push(TxIn { previous_output: utxo.outpoint, script_sig: ScriptBuf::new(), - sequence: Sequence::ZERO, + sequence: utxo.sequence, witness: Witness::new(), }); } @@ -1343,6 +1349,7 @@ mod tests { script_pubkey: ScriptBuf::new(), }, satisfaction_weight: 5, // Just the script_sig and witness lengths + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, }], change_output: None, }, diff --git a/lightning/src/ln/funding.rs b/lightning/src/ln/funding.rs index 8092a0e4451..3b7c0bedbef 100644 --- a/lightning/src/ln/funding.rs +++ b/lightning/src/ln/funding.rs @@ -108,11 +108,6 @@ pub struct FundingTxInput { /// [`TxOut`]: bitcoin::TxOut pub(super) utxo: Utxo, - /// The sequence number to use in the [`TxIn`]. - /// - /// [`TxIn`]: bitcoin::TxIn - pub(super) sequence: Sequence, - /// The transaction containing the unspent [`TxOut`] referenced by [`utxo`]. /// /// [`TxOut`]: bitcoin::TxOut @@ -122,7 +117,7 @@ pub struct FundingTxInput { impl_writeable_tlv_based!(FundingTxInput, { (1, utxo, required), - (3, sequence, required), + (3, _sequence, (legacy, Sequence, |input: &FundingTxInput| Some(input.utxo.sequence))), (5, prevtx, required), }); @@ -140,8 +135,8 @@ impl FundingTxInput { .ok_or(())? .clone(), satisfaction_weight: EMPTY_SCRIPT_SIG_WEIGHT + witness_weight.to_wu(), + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, }, - sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, prevtx, }) } @@ -234,14 +229,14 @@ impl FundingTxInput { /// /// [`TxIn`]: bitcoin::TxIn pub fn sequence(&self) -> Sequence { - self.sequence + self.utxo.sequence } /// Sets the sequence number to use in the [`TxIn`]. /// /// [`TxIn`]: bitcoin::TxIn pub fn set_sequence(&mut self, sequence: Sequence) { - self.sequence = sequence; + self.utxo.sequence = sequence; } /// Converts the [`FundingTxInput`] into a [`Utxo`] for coin selection. diff --git a/lightning/src/ln/interactivetxs.rs b/lightning/src/ln/interactivetxs.rs index 7ed829886c6..17f827adf40 100644 --- a/lightning/src/ln/interactivetxs.rs +++ b/lightning/src/ln/interactivetxs.rs @@ -2061,9 +2061,13 @@ impl InteractiveTxConstructor { let mut inputs_to_contribute: Vec<(SerialId, InputOwned)> = inputs_to_contribute .into_iter() - .map(|FundingTxInput { utxo, sequence, prevtx: prev_tx }| { + .map(|FundingTxInput { utxo, prevtx: prev_tx }| { let serial_id = generate_holder_serial_id(entropy_source, is_initiator); - let txin = TxIn { previous_output: utxo.outpoint, sequence, ..Default::default() }; + let txin = TxIn { + previous_output: utxo.outpoint, + sequence: utxo.sequence, + ..Default::default() + }; let prev_output = utxo.output; let input = InputOwned::Single(SingleOwnedInput { input: txin, diff --git a/lightning/src/util/anchor_channel_reserves.rs b/lightning/src/util/anchor_channel_reserves.rs index e50e103211f..6143a30ebd8 100644 --- a/lightning/src/util/anchor_channel_reserves.rs +++ b/lightning/src/util/anchor_channel_reserves.rs @@ -330,7 +330,7 @@ where #[cfg(test)] mod test { use super::*; - use bitcoin::{OutPoint, ScriptBuf, TxOut, Txid}; + use bitcoin::{OutPoint, ScriptBuf, Sequence, TxOut, Txid}; use std::str::FromStr; #[test] @@ -358,6 +358,7 @@ mod test { }, output: TxOut { value: amount, script_pubkey: ScriptBuf::new() }, satisfaction_weight: 1 * 4 + (1 + 1 + 72 + 1 + 33), + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, } } From 02ec3f7a4c7f45805f048525e797a1d798b63b3b Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Tue, 13 Jan 2026 21:30:46 -0600 Subject: [PATCH 09/13] Use FundingTxInput instead of Utxo in CoinSelection In order to reuse CoinSelectionSource for splicing, the previous transaction of each UTXO is needed. Update CoinSelection to use FundingTxInput (renamed to ConfirmedUtxo) so that it is available. This requires adding a method to WalletSource to look up a previous transaction for a UTXO. Otherwise, Wallet's implementation of CoinSelectionSource would need WalletSource to include the previous transactions when listing confirmed UTXOs to select from. But this would be inefficient since only some UTXOs are selected. --- lightning/src/events/bump_transaction/mod.rs | 82 +++++++++++++------ lightning/src/events/bump_transaction/sync.rs | 10 +++ lightning/src/ln/funding.rs | 16 ++-- lightning/src/util/test_utils.rs | 12 ++- 4 files changed, 86 insertions(+), 34 deletions(-) diff --git a/lightning/src/events/bump_transaction/mod.rs b/lightning/src/events/bump_transaction/mod.rs index 4dfc8ef03e6..a5e32e04d7a 100644 --- a/lightning/src/events/bump_transaction/mod.rs +++ b/lightning/src/events/bump_transaction/mod.rs @@ -29,6 +29,7 @@ use crate::ln::chan_utils::{ HTLC_TIMEOUT_INPUT_KEYED_ANCHOR_WITNESS_WEIGHT, HTLC_TIMEOUT_INPUT_P2A_ANCHOR_WITNESS_WEIGHT, P2WSH_TXOUT_WEIGHT, SEGWIT_MARKER_FLAG_WEIGHT, TRUC_CHILD_MAX_WEIGHT, TRUC_MAX_WEIGHT, }; +use crate::ln::funding::ConfirmedUtxo; use crate::ln::types::ChannelId; use crate::prelude::*; use crate::sign::ecdsa::EcdsaChannelSigner; @@ -345,7 +346,7 @@ impl Utxo { pub struct CoinSelection { /// The set of UTXOs (with at least 1 confirmation) to spend and use within a transaction /// requiring additional fees. - pub confirmed_utxos: Vec, + pub confirmed_utxos: Vec, /// An additional output tracking whether any change remained after coin selection. This output /// should always have a value above dust for its given `script_pubkey`. It should not be /// spent until the transaction it belongs to confirms to ensure mempool descendant limits are @@ -353,6 +354,16 @@ pub struct CoinSelection { pub change_output: Option, } +impl CoinSelection { + fn satisfaction_weight(&self) -> u64 { + self.confirmed_utxos.iter().map(|ConfirmedUtxo { utxo, .. }| utxo.satisfaction_weight).sum() + } + + fn amount(&self) -> Amount { + self.confirmed_utxos.iter().map(|ConfirmedUtxo { utxo, .. }| utxo.output.value).sum() + } +} + /// An abstraction over a bitcoin wallet that can perform coin selection over a set of UTXOs and can /// sign for them. The coin selection method aims to mimic Bitcoin Core's `fundrawtransaction` RPC, /// which most wallets should be able to satisfy. Otherwise, consider implementing [`WalletSource`], @@ -419,9 +430,14 @@ pub trait CoinSelectionSource { pub trait WalletSource { /// Returns all UTXOs, with at least 1 confirmation each, that are available to spend. fn list_confirmed_utxos<'a>(&'a self) -> AsyncResult<'a, Vec, ()>; + + /// Returns the previous transaction containing the UTXO. + fn get_prevtx<'a>(&'a self, utxo: &Utxo) -> AsyncResult<'a, Transaction, ()>; + /// Returns a script to use for change above dust resulting from a successful coin selection /// attempt. fn get_change_script<'a>(&'a self) -> AsyncResult<'a, ScriptBuf, ()>; + /// Signs and provides the full [`TxIn::script_sig`] and [`TxIn::witness`] for all inputs within /// the transaction known to the wallet (i.e., any provided via /// [`WalletSource::list_confirmed_utxos`]). @@ -607,10 +623,13 @@ where Some(TxOut { script_pubkey: change_script, value: change_output_amount }) }; - Ok(CoinSelection { - confirmed_utxos: selected_utxos.into_iter().map(|(utxo, _)| utxo).collect(), - change_output, - }) + let mut confirmed_utxos = Vec::with_capacity(selected_utxos.len()); + for (utxo, _) in selected_utxos { + let prevtx = self.source.get_prevtx(&utxo).await?; + confirmed_utxos.push(ConfirmedUtxo { utxo, prevtx }); + } + + Ok(CoinSelection { confirmed_utxos, change_output }) } } @@ -719,7 +738,7 @@ where /// Updates a transaction with the result of a successful coin selection attempt. fn process_coin_selection(&self, tx: &mut Transaction, coin_selection: &CoinSelection) { - for utxo in coin_selection.confirmed_utxos.iter() { + for ConfirmedUtxo { utxo, .. } in coin_selection.confirmed_utxos.iter() { tx.input.push(TxIn { previous_output: utxo.outpoint, script_sig: ScriptBuf::new(), @@ -841,12 +860,10 @@ where output: vec![], }; - let input_satisfaction_weight: u64 = - coin_selection.confirmed_utxos.iter().map(|utxo| utxo.satisfaction_weight).sum(); + let input_satisfaction_weight = coin_selection.satisfaction_weight(); let total_satisfaction_weight = anchor_input_witness_weight + EMPTY_SCRIPT_SIG_WEIGHT + input_satisfaction_weight; - let total_input_amount = must_spend_amount - + coin_selection.confirmed_utxos.iter().map(|utxo| utxo.output.value).sum(); + let total_input_amount = must_spend_amount + coin_selection.amount(); self.process_coin_selection(&mut anchor_tx, &coin_selection); let anchor_txid = anchor_tx.compute_txid(); @@ -856,7 +873,7 @@ where // add witness_utxo to anchor input anchor_psbt.inputs[0].witness_utxo = Some(anchor_descriptor.previous_utxo()); // add witness_utxo to remaining inputs - for (idx, utxo) in coin_selection.confirmed_utxos.into_iter().enumerate() { + for (idx, ConfirmedUtxo { utxo, .. }) in coin_selection.confirmed_utxos.into_iter().enumerate() { // add 1 to skip the anchor input let index = idx + 1; debug_assert_eq!( @@ -1096,13 +1113,11 @@ where utxo_id = claim_id.step_with_bytes(&broadcasted_htlcs.to_be_bytes()); #[cfg(debug_assertions)] - let input_satisfaction_weight: u64 = - coin_selection.confirmed_utxos.iter().map(|utxo| utxo.satisfaction_weight).sum(); + let input_satisfaction_weight = coin_selection.satisfaction_weight(); #[cfg(debug_assertions)] let total_satisfaction_weight = must_spend_satisfaction_weight + input_satisfaction_weight; #[cfg(debug_assertions)] - let input_value: u64 = - coin_selection.confirmed_utxos.iter().map(|utxo| utxo.output.value.to_sat()).sum(); + let input_value = coin_selection.amount().to_sat(); #[cfg(debug_assertions)] let total_input_amount = must_spend_amount + input_value; @@ -1120,7 +1135,7 @@ where } // add witness_utxo to remaining inputs - for (idx, utxo) in coin_selection.confirmed_utxos.into_iter().enumerate() { + for (idx, ConfirmedUtxo { utxo, .. }) in coin_selection.confirmed_utxos.into_iter().enumerate() { // offset to skip the htlc inputs let index = idx + selected_htlcs.len(); debug_assert_eq!(htlc_psbt.unsigned_tx.input[index].previous_output, utxo.outpoint); @@ -1269,9 +1284,8 @@ mod tests { use crate::util::ser::Readable; use crate::util::test_utils::{TestBroadcaster, TestLogger}; - use bitcoin::hashes::Hash; use bitcoin::hex::FromHex; - use bitcoin::{Network, ScriptBuf, Transaction, Txid}; + use bitcoin::{Network, ScriptBuf, Transaction}; struct TestCoinSelectionSource { // (commitment + anchor value, commitment + input weight, target feerate, result) @@ -1291,8 +1305,12 @@ mod tests { Ok(res) } fn sign_psbt(&self, psbt: Psbt) -> Result { - // FIXME: Why does handle_channel_close override the witness? - Ok(psbt.unsigned_tx) + let mut tx = psbt.unsigned_tx; + // Channel output, add a realistic size witness to make the assertions happy + // + // FIXME: This doesn't seem to be needed since handle_channel_close overrides it + tx.input.first_mut().unwrap().witness = Witness::from_slice(&[vec![42; 162]]); + Ok(tx) } } @@ -1328,6 +1346,16 @@ mod tests { .weight() .to_wu(); + let prevtx = Transaction { + version: Version::TWO, + lock_time: LockTime::ZERO, + input: vec![], + output: vec![TxOut { + value: Amount::from_sat(200), + script_pubkey: ScriptBuf::new() + }], + }; + let broadcaster = TestBroadcaster::new(Network::Testnet); let source = TestCoinSelectionSource { expected_selects: Mutex::new(vec![ @@ -1342,14 +1370,14 @@ mod tests { commitment_and_anchor_fee, 868, CoinSelection { - confirmed_utxos: vec![Utxo { - outpoint: OutPoint { txid: Txid::from_byte_array([44; 32]), vout: 0 }, - output: TxOut { - value: Amount::from_sat(200), - script_pubkey: ScriptBuf::new(), + confirmed_utxos: vec![ConfirmedUtxo { + utxo: Utxo { + outpoint: OutPoint { txid: prevtx.compute_txid(), vout: 0 }, + output: prevtx.output[0].clone(), + satisfaction_weight: 5, // Just the script_sig and witness lengths + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, }, - satisfaction_weight: 5, // Just the script_sig and witness lengths - sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, + prevtx, }], change_output: None, }, diff --git a/lightning/src/events/bump_transaction/sync.rs b/lightning/src/events/bump_transaction/sync.rs index 653710a3358..75be588848a 100644 --- a/lightning/src/events/bump_transaction/sync.rs +++ b/lightning/src/events/bump_transaction/sync.rs @@ -36,9 +36,14 @@ use super::{ pub trait WalletSourceSync { /// Returns all UTXOs, with at least 1 confirmation each, that are available to spend. fn list_confirmed_utxos(&self) -> Result, ()>; + + /// Returns the previous transaction containing the UTXO. + fn get_prevtx(&self, utxo: &Utxo) -> Result; + /// Returns a script to use for change above dust resulting from a successful coin selection /// attempt. fn get_change_script(&self) -> Result; + /// Signs and provides the full [`TxIn::script_sig`] and [`TxIn::witness`] for all inputs within /// the transaction known to the wallet (i.e., any provided via /// [`WalletSource::list_confirmed_utxos`]). @@ -76,6 +81,11 @@ where Box::pin(async move { utxos }) } + fn get_prevtx<'a>(&'a self, utxo: &Utxo) -> AsyncResult<'a, Transaction, ()> { + let prevtx = self.0.get_prevtx(utxo); + Box::pin(async move { prevtx }) + } + fn get_change_script<'a>(&'a self) -> AsyncResult<'a, ScriptBuf, ()> { let script = self.0.get_change_script(); Box::pin(async move { script }) diff --git a/lightning/src/ln/funding.rs b/lightning/src/ln/funding.rs index 3b7c0bedbef..2d9cc13ad6e 100644 --- a/lightning/src/ln/funding.rs +++ b/lightning/src/ln/funding.rs @@ -99,22 +99,26 @@ impl SpliceContribution { } } -/// An input to contribute to a channel's funding transaction either when using the v2 channel -/// establishment protocol or when splicing. +/// An unspent transaction output with at least one confirmation. #[derive(Debug, Clone)] -pub struct FundingTxInput { - /// The unspent [`TxOut`] that the input spends. +pub struct ConfirmedUtxo { + /// The unspent [`TxOut`] found in [`prevtx`]. /// /// [`TxOut`]: bitcoin::TxOut - pub(super) utxo: Utxo, + /// [`prevtx`]: Self::prevtx + pub(crate) utxo: Utxo, /// The transaction containing the unspent [`TxOut`] referenced by [`utxo`]. /// /// [`TxOut`]: bitcoin::TxOut /// [`utxo`]: Self::utxo - pub(super) prevtx: Transaction, + pub(crate) prevtx: Transaction, } +/// An input to contribute to a channel's funding transaction either when using the v2 channel +/// establishment protocol or when splicing. +pub type FundingTxInput = ConfirmedUtxo; + impl_writeable_tlv_based!(FundingTxInput, { (1, utxo, required), (3, _sequence, (legacy, Sequence, |input: &FundingTxInput| Some(input.utxo.sequence))), diff --git a/lightning/src/util/test_utils.rs b/lightning/src/util/test_utils.rs index 34c6414b7e0..ae9ebd07ad6 100644 --- a/lightning/src/util/test_utils.rs +++ b/lightning/src/util/test_utils.rs @@ -62,6 +62,7 @@ use crate::util::persist::{KVStore, KVStoreSync, MonitorName}; use crate::util::ser::{Readable, ReadableArgs, Writeable, Writer}; use crate::util::test_channel_signer::{EnforcementState, TestChannelSigner}; +use bitcoin::absolute::LockTime; use bitcoin::amount::Amount; use bitcoin::block::Block; use bitcoin::constants::genesis_block; @@ -71,7 +72,7 @@ use bitcoin::hashes::{hex::FromHex, Hash}; use bitcoin::network::Network; use bitcoin::script::{Builder, Script, ScriptBuf}; use bitcoin::sighash::{EcdsaSighashType, SighashCache}; -use bitcoin::transaction::{Transaction, TxOut}; +use bitcoin::transaction::{Transaction, TxOut, Version}; use bitcoin::{opcodes, Witness}; use bitcoin::secp256k1::ecdh::SharedSecret; @@ -2279,6 +2280,15 @@ impl WalletSourceSync for TestWalletSource { Ok(self.utxos.lock().unwrap().clone()) } + fn get_prevtx(&self, utxo: &Utxo) -> Result { + Ok(Transaction { + version: Version::TWO, + lock_time: LockTime::ZERO, + input: vec![], + output: vec![utxo.output.clone()], + }) + } + fn get_change_script(&self) -> Result { let public_key = bitcoin::PublicKey::new(self.secret_key.public_key(&self.secp)); Ok(ScriptBuf::new_p2wpkh(&public_key.wpubkey_hash().unwrap())) From 77983259a911e387bd60a2083d1fc1ab5d33cf8a Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Wed, 14 Jan 2026 16:06:04 -0600 Subject: [PATCH 10/13] f - use ConfirmedUtxo in TestWalletSource --- lightning/src/events/bump_transaction/mod.rs | 5 ++- lightning/src/ln/functional_test_utils.rs | 3 +- lightning/src/ln/funding.rs | 9 ++--- lightning/src/util/test_utils.rs | 40 ++++++++------------ 4 files changed, 24 insertions(+), 33 deletions(-) diff --git a/lightning/src/events/bump_transaction/mod.rs b/lightning/src/events/bump_transaction/mod.rs index a5e32e04d7a..2c1ca5bb5cc 100644 --- a/lightning/src/events/bump_transaction/mod.rs +++ b/lightning/src/events/bump_transaction/mod.rs @@ -29,7 +29,7 @@ use crate::ln::chan_utils::{ HTLC_TIMEOUT_INPUT_KEYED_ANCHOR_WITNESS_WEIGHT, HTLC_TIMEOUT_INPUT_P2A_ANCHOR_WITNESS_WEIGHT, P2WSH_TXOUT_WEIGHT, SEGWIT_MARKER_FLAG_WEIGHT, TRUC_CHILD_MAX_WEIGHT, TRUC_MAX_WEIGHT, }; -use crate::ln::funding::ConfirmedUtxo; +use crate::ln::funding::FundingTxInput; use crate::ln::types::ChannelId; use crate::prelude::*; use crate::sign::ecdsa::EcdsaChannelSigner; @@ -340,6 +340,9 @@ impl Utxo { } } +/// An unspent transaction output with at least one confirmation. +pub type ConfirmedUtxo = FundingTxInput; + /// The result of a successful coin selection attempt for a transaction requiring additional UTXOs /// to cover its fees. #[derive(Clone, Debug)] diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index ff33d7508b5..6de14478612 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -395,8 +395,7 @@ fn do_connect_block_without_consistency_checks<'a, 'b, 'c, 'd>( let wallet_script = node.wallet_source.get_change_script().unwrap(); for (idx, output) in tx.output.iter().enumerate() { if output.script_pubkey == wallet_script { - let outpoint = bitcoin::OutPoint { txid: tx.compute_txid(), vout: idx as u32 }; - node.wallet_source.add_utxo(outpoint, output.value); + node.wallet_source.add_utxo(tx.clone(), idx as u32); } } } diff --git a/lightning/src/ln/funding.rs b/lightning/src/ln/funding.rs index 2d9cc13ad6e..3bb2cd212cb 100644 --- a/lightning/src/ln/funding.rs +++ b/lightning/src/ln/funding.rs @@ -99,9 +99,10 @@ impl SpliceContribution { } } -/// An unspent transaction output with at least one confirmation. +/// An input to contribute to a channel's funding transaction either when using the v2 channel +/// establishment protocol or when splicing. #[derive(Debug, Clone)] -pub struct ConfirmedUtxo { +pub struct FundingTxInput { /// The unspent [`TxOut`] found in [`prevtx`]. /// /// [`TxOut`]: bitcoin::TxOut @@ -115,10 +116,6 @@ pub struct ConfirmedUtxo { pub(crate) prevtx: Transaction, } -/// An input to contribute to a channel's funding transaction either when using the v2 channel -/// establishment protocol or when splicing. -pub type FundingTxInput = ConfirmedUtxo; - impl_writeable_tlv_based!(FundingTxInput, { (1, utxo, required), (3, _sequence, (legacy, Sequence, |input: &FundingTxInput| Some(input.utxo.sequence))), diff --git a/lightning/src/util/test_utils.rs b/lightning/src/util/test_utils.rs index ae9ebd07ad6..c9781bfc5dd 100644 --- a/lightning/src/util/test_utils.rs +++ b/lightning/src/util/test_utils.rs @@ -22,7 +22,7 @@ use crate::chain::channelmonitor::{ use crate::chain::transaction::OutPoint; use crate::chain::WatchedOutput; use crate::events::bump_transaction::sync::WalletSourceSync; -use crate::events::bump_transaction::Utxo; +use crate::events::bump_transaction::{ConfirmedUtxo, Utxo}; #[cfg(any(test, feature = "_externalize_tests"))] use crate::ln::chan_utils::CommitmentTransaction; use crate::ln::channel_state::ChannelDetails; @@ -62,7 +62,6 @@ use crate::util::persist::{KVStore, KVStoreSync, MonitorName}; use crate::util::ser::{Readable, ReadableArgs, Writeable, Writer}; use crate::util::test_channel_signer::{EnforcementState, TestChannelSigner}; -use bitcoin::absolute::LockTime; use bitcoin::amount::Amount; use bitcoin::block::Block; use bitcoin::constants::genesis_block; @@ -72,7 +71,7 @@ use bitcoin::hashes::{hex::FromHex, Hash}; use bitcoin::network::Network; use bitcoin::script::{Builder, Script, ScriptBuf}; use bitcoin::sighash::{EcdsaSighashType, SighashCache}; -use bitcoin::transaction::{Transaction, TxOut, Version}; +use bitcoin::transaction::{Transaction, TxOut}; use bitcoin::{opcodes, Witness}; use bitcoin::secp256k1::ecdh::SharedSecret; @@ -2211,7 +2210,7 @@ impl Drop for TestScorer { pub struct TestWalletSource { secret_key: SecretKey, - utxos: Mutex>, + utxos: Mutex>, secp: Secp256k1, } @@ -2220,21 +2219,13 @@ impl TestWalletSource { Self { secret_key, utxos: Mutex::new(Vec::new()), secp: Secp256k1::new() } } - pub fn add_utxo(&self, outpoint: bitcoin::OutPoint, value: Amount) -> TxOut { - let public_key = bitcoin::PublicKey::new(self.secret_key.public_key(&self.secp)); - let utxo = Utxo::new_v0_p2wpkh(outpoint, value, &public_key.wpubkey_hash().unwrap()); - self.utxos.lock().unwrap().push(utxo.clone()); - utxo.output - } - - pub fn add_custom_utxo(&self, utxo: Utxo) -> TxOut { - let output = utxo.output.clone(); + pub fn add_utxo(&self, prevtx: Transaction, vout: u32) { + let utxo = ConfirmedUtxo::new_p2wpkh(prevtx, vout).unwrap(); self.utxos.lock().unwrap().push(utxo); - output } pub fn remove_utxo(&self, outpoint: bitcoin::OutPoint) { - self.utxos.lock().unwrap().retain(|utxo| utxo.outpoint != outpoint); + self.utxos.lock().unwrap().retain(|utxo| utxo.outpoint() != outpoint); } pub fn clear_utxos(&self) { @@ -2246,8 +2237,8 @@ impl TestWalletSource { ) -> Result { let utxos = self.utxos.lock().unwrap(); for i in 0..tx.input.len() { - if let Some(utxo) = - utxos.iter().find(|utxo| utxo.outpoint == tx.input[i].previous_output) + if let Some(ConfirmedUtxo { utxo, .. }) = + utxos.iter().find(|utxo| utxo.outpoint() == tx.input[i].previous_output) { let sighash = SighashCache::new(&tx).p2wpkh_signature_hash( i, @@ -2277,16 +2268,17 @@ impl TestWalletSource { impl WalletSourceSync for TestWalletSource { fn list_confirmed_utxos(&self) -> Result, ()> { - Ok(self.utxos.lock().unwrap().clone()) + Ok(self.utxos.lock().unwrap().iter().map(|ConfirmedUtxo { utxo, .. }| utxo.clone()).collect()) } fn get_prevtx(&self, utxo: &Utxo) -> Result { - Ok(Transaction { - version: Version::TWO, - lock_time: LockTime::ZERO, - input: vec![], - output: vec![utxo.output.clone()], - }) + self.utxos + .lock() + .unwrap() + .iter() + .find(|confirmed_utxo| confirmed_utxo.utxo == *utxo) + .map(|ConfirmedUtxo { prevtx, .. }| prevtx.clone()) + .ok_or(()) } fn get_change_script(&self) -> Result { From 531a8d9fb0e12ed59aa9cec2c6f89ba975523287 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Wed, 14 Jan 2026 16:24:32 -0600 Subject: [PATCH 11/13] f - clean-up and rustfmt --- lightning/src/events/bump_transaction/mod.rs | 24 ++++++++++---------- lightning/src/ln/funding.rs | 12 +++++++++- lightning/src/util/test_utils.rs | 14 ++++++++---- 3 files changed, 33 insertions(+), 17 deletions(-) diff --git a/lightning/src/events/bump_transaction/mod.rs b/lightning/src/events/bump_transaction/mod.rs index 2c1ca5bb5cc..6687a625576 100644 --- a/lightning/src/events/bump_transaction/mod.rs +++ b/lightning/src/events/bump_transaction/mod.rs @@ -876,15 +876,15 @@ where // add witness_utxo to anchor input anchor_psbt.inputs[0].witness_utxo = Some(anchor_descriptor.previous_utxo()); // add witness_utxo to remaining inputs - for (idx, ConfirmedUtxo { utxo, .. }) in coin_selection.confirmed_utxos.into_iter().enumerate() { + for (idx, utxo) in coin_selection.confirmed_utxos.into_iter().enumerate() { // add 1 to skip the anchor input let index = idx + 1; debug_assert_eq!( anchor_psbt.unsigned_tx.input[index].previous_output, - utxo.outpoint + utxo.outpoint() ); - if utxo.output.script_pubkey.is_witness_program() { - anchor_psbt.inputs[index].witness_utxo = Some(utxo.output); + if utxo.output().script_pubkey.is_witness_program() { + anchor_psbt.inputs[index].witness_utxo = Some(utxo.into_output()); } } @@ -1138,12 +1138,15 @@ where } // add witness_utxo to remaining inputs - for (idx, ConfirmedUtxo { utxo, .. }) in coin_selection.confirmed_utxos.into_iter().enumerate() { + for (idx, utxo) in coin_selection.confirmed_utxos.into_iter().enumerate() { // offset to skip the htlc inputs let index = idx + selected_htlcs.len(); - debug_assert_eq!(htlc_psbt.unsigned_tx.input[index].previous_output, utxo.outpoint); - if utxo.output.script_pubkey.is_witness_program() { - htlc_psbt.inputs[index].witness_utxo = Some(utxo.output); + debug_assert_eq!( + htlc_psbt.unsigned_tx.input[index].previous_output, + utxo.outpoint() + ); + if utxo.output().script_pubkey.is_witness_program() { + htlc_psbt.inputs[index].witness_utxo = Some(utxo.into_output()); } } @@ -1353,10 +1356,7 @@ mod tests { version: Version::TWO, lock_time: LockTime::ZERO, input: vec![], - output: vec![TxOut { - value: Amount::from_sat(200), - script_pubkey: ScriptBuf::new() - }], + output: vec![TxOut { value: Amount::from_sat(200), script_pubkey: ScriptBuf::new() }], }; let broadcaster = TestBroadcaster::new(Network::Testnet); diff --git a/lightning/src/ln/funding.rs b/lightning/src/ln/funding.rs index 3bb2cd212cb..625b80afab5 100644 --- a/lightning/src/ln/funding.rs +++ b/lightning/src/ln/funding.rs @@ -226,6 +226,11 @@ impl FundingTxInput { self.utxo.outpoint } + /// The unspent output. + pub fn output(&self) -> &TxOut { + &self.utxo.output + } + /// The sequence number to use in the [`TxIn`]. /// /// [`TxIn`]: bitcoin::TxIn @@ -240,8 +245,13 @@ impl FundingTxInput { self.utxo.sequence = sequence; } - /// Converts the [`FundingTxInput`] into a [`Utxo`] for coin selection. + /// Converts the [`FundingTxInput`] into a [`Utxo`]. pub fn into_utxo(self) -> Utxo { self.utxo } + + /// Converts the [`FundingTxInput`] into a [`TxOut`]. + pub fn into_output(self) -> TxOut { + self.utxo.output + } } diff --git a/lightning/src/util/test_utils.rs b/lightning/src/util/test_utils.rs index c9781bfc5dd..81f88c38d98 100644 --- a/lightning/src/util/test_utils.rs +++ b/lightning/src/util/test_utils.rs @@ -2237,13 +2237,13 @@ impl TestWalletSource { ) -> Result { let utxos = self.utxos.lock().unwrap(); for i in 0..tx.input.len() { - if let Some(ConfirmedUtxo { utxo, .. }) = + if let Some(utxo) = utxos.iter().find(|utxo| utxo.outpoint() == tx.input[i].previous_output) { let sighash = SighashCache::new(&tx).p2wpkh_signature_hash( i, - &utxo.output.script_pubkey, - utxo.output.value, + &utxo.output().script_pubkey, + utxo.output().value, EcdsaSighashType::All, )?; #[cfg(not(feature = "grind_signatures"))] @@ -2268,7 +2268,13 @@ impl TestWalletSource { impl WalletSourceSync for TestWalletSource { fn list_confirmed_utxos(&self) -> Result, ()> { - Ok(self.utxos.lock().unwrap().iter().map(|ConfirmedUtxo { utxo, .. }| utxo.clone()).collect()) + Ok(self + .utxos + .lock() + .unwrap() + .iter() + .map(|ConfirmedUtxo { utxo, .. }| utxo.clone()) + .collect()) } fn get_prevtx(&self, utxo: &Utxo) -> Result { From d0c3b80274dc634f89427465d24a0bb947b95b7d Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Thu, 18 Dec 2025 09:54:00 -0600 Subject: [PATCH 12/13] WIP: Add FundingNeeded event for splicing Rather than requiring the user to pass FundingTxInputs when initiating a splice, generate a FundingNeeded event once the channel has become quiescent. This simplifies error handling and UTXO / change address clean-up by consolidating it in SpliceFailed event handling. Later, this event will be used for opportunistic contributions (i.e., when the counterparty wins quiescence or initiates), dual-funding, and RBF. --- lightning/src/events/bump_transaction/mod.rs | 8 +- lightning/src/events/bump_transaction/sync.rs | 3 +- lightning/src/events/mod.rs | 55 ++ lightning/src/ln/channel.rs | 558 ++++------------- lightning/src/ln/channelmanager.rs | 102 +++- lightning/src/ln/funding.rs | 576 +++++++++++++++++- 6 files changed, 831 insertions(+), 471 deletions(-) diff --git a/lightning/src/events/bump_transaction/mod.rs b/lightning/src/events/bump_transaction/mod.rs index 6687a625576..9d921763fab 100644 --- a/lightning/src/events/bump_transaction/mod.rs +++ b/lightning/src/events/bump_transaction/mod.rs @@ -50,7 +50,7 @@ use bitcoin::secp256k1::ecdsa::Signature; use bitcoin::secp256k1::{PublicKey, Secp256k1}; use bitcoin::transaction::Version; use bitcoin::{ - OutPoint, Psbt, PubkeyHash, ScriptBuf, Sequence, Transaction, TxIn, TxOut, WPubkeyHash, Witness, + FeeRate, OutPoint, Psbt, PubkeyHash, ScriptBuf, Sequence, Transaction, TxIn, TxOut, WPubkeyHash, Witness, }; /// A descriptor used to sign for a commitment transaction's anchor output. @@ -270,6 +270,12 @@ pub struct Input { pub satisfaction_weight: u64, } +impl_writeable_tlv_based!(Input, { + (1, outpoint, required), + (3, previous_utxo, required), + (5, satisfaction_weight, required), +}); + /// An unspent transaction output that is available to spend resulting from a successful /// [`CoinSelection`] attempt. #[derive(Clone, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)] diff --git a/lightning/src/events/bump_transaction/sync.rs b/lightning/src/events/bump_transaction/sync.rs index 75be588848a..77066ff952e 100644 --- a/lightning/src/events/bump_transaction/sync.rs +++ b/lightning/src/events/bump_transaction/sync.rs @@ -15,12 +15,13 @@ use core::task; use crate::chain::chaininterface::BroadcasterInterface; use crate::chain::ClaimId; +use crate::ln::funding::FundingTxInput; use crate::prelude::*; use crate::sign::SignerProvider; use crate::util::async_poll::{dummy_waker, AsyncResult, MaybeSend, MaybeSync}; use crate::util::logger::Logger; -use bitcoin::{Psbt, ScriptBuf, Transaction, TxOut}; +use bitcoin::{FeeRate, Psbt, ScriptBuf, Transaction, TxOut}; use super::BumpTransactionEvent; use super::{ diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs index b9c4b1ca1ef..6b55a24a622 100644 --- a/lightning/src/events/mod.rs +++ b/lightning/src/events/mod.rs @@ -25,6 +25,7 @@ use crate::blinded_path::payment::{ use crate::chain::transaction; use crate::ln::channel::FUNDING_CONF_DEADLINE_BLOCKS; use crate::ln::channelmanager::{InterceptId, PaymentId, RecipientOnionFields}; +use crate::ln::funding::FundingTemplate; use crate::ln::types::ChannelId; use crate::ln::{msgs, LocalHTLCFailureReason}; use crate::offers::invoice::Bolt12Invoice; @@ -1816,6 +1817,28 @@ pub enum Event { /// [`ChannelManager::respond_to_static_invoice_request`]: crate::ln::channelmanager::ChannelManager::respond_to_static_invoice_request invoice_request: InvoiceRequest, }, + /// + FundingNeeded { + /// The `channel_id` of the channel which you'll need to pass back into + /// [`ChannelManager::funding_contributed`]. + /// + /// [`ChannelManager::funding_contributed`]: crate::ln::channelmanager::ChannelManager::funding_contributed + channel_id: ChannelId, + /// The counterparty's `node_id`, which you'll need to pass back into + /// [`ChannelManager::funding_contributed`]. + /// + /// [`ChannelManager::funding_contributed`]: crate::ln::channelmanager::ChannelManager::funding_contributed + counterparty_node_id: PublicKey, + /// The `user_channel_id` value passed in for outbound channels, or for inbound channels if + /// [`UserConfig::manually_accept_inbound_channels`] config flag is set to true. Otherwise + /// `user_channel_id` will be randomized for inbound channels. + /// + /// [`UserConfig::manually_accept_inbound_channels`]: crate::util::config::UserConfig::manually_accept_inbound_channels + user_channel_id: u128, + + /// + funding_template: FundingTemplate, + }, /// Indicates that a channel funding transaction constructed interactively is ready to be /// signed. This event will only be triggered if at least one input was contributed. /// @@ -2347,6 +2370,20 @@ impl Writeable for Event { (13, *contributed_outputs, optional_vec), }); }, + &Event::FundingNeeded { + ref channel_id, + ref user_channel_id, + ref counterparty_node_id, + ref funding_template, + } => { + 54u8.write(writer)?; + write_tlv_fields!(writer, { + (1, channel_id, required), + (3, user_channel_id, required), + (5, counterparty_node_id, required), + (7, funding_template, required), + }); + }, // Note that, going forward, all new events must only write data inside of // `write_tlv_fields`. Versions 0.0.101+ will ignore odd-numbered events that write // data via `write_tlv_fields`. @@ -2978,6 +3015,24 @@ impl MaybeReadable for Event { }; f() }, + 54u8 => { + let mut f = || { + _init_and_read_len_prefixed_tlv_fields!(reader, { + (1, channel_id, required), + (3, user_channel_id, required), + (5, counterparty_node_id, required), + (7, funding_template, required), + }); + + Ok(Some(Event::FundingNeeded { + channel_id: channel_id.0.unwrap(), + user_channel_id: user_channel_id.0.unwrap(), + counterparty_node_id: counterparty_node_id.0.unwrap(), + funding_template: funding_template.0.unwrap(), + })) + }; + f() + }, // Versions prior to 0.0.100 did not ignore odd types, instead returning InvalidValue. // Version 0.0.100 failed to properly ignore odd types, possibly resulting in corrupt // reads. diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 233ca211bc4..e57b3bbe81a 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -11,7 +11,7 @@ use bitcoin::absolute::LockTime; use bitcoin::amount::{Amount, SignedAmount}; use bitcoin::consensus::encode; use bitcoin::constants::ChainHash; -use bitcoin::script::{Builder, Script, ScriptBuf, WScriptHash}; +use bitcoin::script::{Builder, Script, ScriptBuf}; use bitcoin::sighash::EcdsaSighashType; use bitcoin::transaction::{Transaction, TxOut}; use bitcoin::Witness; @@ -24,7 +24,7 @@ use bitcoin::hashes::Hash; use bitcoin::secp256k1::constants::PUBLIC_KEY_SIZE; use bitcoin::secp256k1::{ecdsa::Signature, Secp256k1}; use bitcoin::secp256k1::{PublicKey, SecretKey}; -use bitcoin::{secp256k1, sighash, FeeRate, Sequence, TxIn}; +use bitcoin::{secp256k1, sighash, Sequence, TxIn}; use crate::blinded_path::message::BlindedMessagePath; use crate::chain::chaininterface::{ @@ -37,13 +37,14 @@ use crate::chain::channelmonitor::{ use crate::chain::transaction::{OutPoint, TransactionData}; use crate::chain::BestBlock; use crate::events::{ClosureReason, FundingInfo}; +use crate::events::bump_transaction::Input; use crate::ln::chan_utils; use crate::ln::chan_utils::{ get_commitment_transaction_number_obscure_factor, max_htlcs, second_stage_tx_fees_sat, selected_commitment_sat_per_1000_weight, ChannelPublicKeys, ChannelTransactionParameters, ClosingTransaction, CommitmentTransaction, CounterpartyChannelTransactionParameters, CounterpartyCommitmentSecrets, HTLCOutputInCommitment, HolderCommitmentTransaction, - BASE_INPUT_WEIGHT, EMPTY_SCRIPT_SIG_WEIGHT, FUNDING_TRANSACTION_WITNESS_WEIGHT, + EMPTY_SCRIPT_SIG_WEIGHT, FUNDING_TRANSACTION_WITNESS_WEIGHT, }; use crate::ln::channel_state::{ ChannelShutdownState, CounterpartyForwardingInfo, InboundHTLCDetails, InboundHTLCStateDetails, @@ -55,12 +56,11 @@ use crate::ln::channelmanager::{ RAACommitmentOrder, SentHTLCId, BREAKDOWN_TIMEOUT, MAX_LOCAL_BREAKDOWN_TIMEOUT, MIN_CLTV_EXPIRY_DELTA, }; -use crate::ln::funding::{FundingTxInput, SpliceContribution}; +use crate::ln::funding::{FundingTemplate, FundingContribution, FundingTxInput, SpliceContribution}; use crate::ln::interactivetxs::{ calculate_change_output_value, get_output_weight, AbortReason, HandleTxCompleteValue, InteractiveTxConstructor, InteractiveTxConstructorArgs, InteractiveTxMessageSend, InteractiveTxSigningSession, NegotiationError, SharedOwnedInput, SharedOwnedOutput, - TX_COMMON_FIELDS_WEIGHT, }; use crate::ln::msgs; use crate::ln::msgs::{ClosingSigned, ClosingSignedFeeRange, DecodeError, OnionErrorPacket}; @@ -69,7 +69,6 @@ use crate::ln::onion_utils::{ }; use crate::ln::script::{self, ShutdownScript}; use crate::ln::types::ChannelId; -use crate::ln::LN_MAX_MSG_LEN; use crate::offers::static_invoice::StaticInvoice; use crate::routing::gossip::NodeId; use crate::sign::ecdsa::EcdsaChannelSigner; @@ -2484,6 +2483,20 @@ impl FundingScope { self.channel_transaction_parameters.funding_outpoint } + /// Gets the funding output for this channel, if available. + /// + /// When a channel is spliced, this continues to refer to the original funding output (which + /// was spent by the splice transaction) until the splice transaction reaches sufficient + /// confirmations to be locked (and we exchange `splice_locked` messages with our peer). + pub fn get_funding_output(&self) -> Option { + self.channel_transaction_parameters + .make_funding_redeemscript_opt() + .and_then(|redeem_script| Some(TxOut { + value: Amount::from_sat(self.get_value_satoshis()), + script_pubkey: redeem_script.to_p2wsh(), + })) + } + fn get_funding_txid(&self) -> Option { self.channel_transaction_parameters.funding_outpoint.map(|txo| txo.txid) } @@ -2802,7 +2815,8 @@ impl_writeable_tlv_based!(SpliceInstructions, { #[derive(Debug)] pub(crate) enum QuiescentAction { - Splice(SpliceInstructions), + LegacySplice(SpliceInstructions), + Splice(SpliceContribution), #[cfg(any(test, fuzzing))] DoNothing, } @@ -2810,16 +2824,19 @@ pub(crate) enum QuiescentAction { pub(crate) enum StfuResponse { Stfu(msgs::Stfu), SpliceInit(msgs::SpliceInit), + FundingNeeded(FundingTemplate), } #[cfg(any(test, fuzzing))] impl_writeable_tlv_based_enum_upgradable!(QuiescentAction, (0, DoNothing) => {}, - {1, Splice} => (), + {1, LegacySplice} => (), + {2, Splice} => (), ); #[cfg(not(any(test, fuzzing)))] impl_writeable_tlv_based_enum_upgradable!(QuiescentAction,, - {1, Splice} => (), + {1, LegacySplice} => (), + {2, Splice} => (), ); /// Wrapper around a [`Transaction`] useful for caching the result of [`Transaction::compute_txid`]. @@ -6494,130 +6511,6 @@ fn get_v2_channel_reserve_satoshis(channel_value_satoshis: u64, dust_limit_satos cmp::min(channel_value_satoshis, cmp::max(q, dust_limit_satoshis)) } -fn check_splice_contribution_sufficient( - contribution: &SpliceContribution, is_initiator: bool, funding_feerate: FeeRate, -) -> Result { - if contribution.inputs().is_empty() { - let estimated_fee = Amount::from_sat(estimate_v2_funding_transaction_fee( - contribution.inputs(), - contribution.outputs(), - is_initiator, - true, // is_splice - funding_feerate.to_sat_per_kwu() as u32, - )); - - let contribution_amount = contribution.net_value(); - contribution_amount - .checked_sub( - estimated_fee.to_signed().expect("fees should never exceed Amount::MAX_MONEY"), - ) - .ok_or(format!( - "{estimated_fee} splice-out amount plus {} fee estimate exceeds the total bitcoin supply", - contribution_amount.unsigned_abs(), - )) - } else { - check_v2_funding_inputs_sufficient( - contribution.value_added(), - contribution.inputs(), - contribution.outputs(), - is_initiator, - true, - funding_feerate.to_sat_per_kwu() as u32, - ) - .map(|_| contribution.net_value()) - } -} - -/// Estimate our part of the fee of the new funding transaction. -#[allow(dead_code)] // TODO(dual_funding): TODO(splicing): Remove allow once used. -#[rustfmt::skip] -fn estimate_v2_funding_transaction_fee( - funding_inputs: &[FundingTxInput], outputs: &[TxOut], is_initiator: bool, is_splice: bool, - funding_feerate_sat_per_1000_weight: u32, -) -> u64 { - let input_weight: u64 = funding_inputs - .iter() - .map(|input| BASE_INPUT_WEIGHT.saturating_add(input.utxo.satisfaction_weight)) - .fold(0, |total_weight, input_weight| total_weight.saturating_add(input_weight)); - - let output_weight: u64 = outputs - .iter() - .map(|txout| txout.weight().to_wu()) - .fold(0, |total_weight, output_weight| total_weight.saturating_add(output_weight)); - - let mut weight = input_weight.saturating_add(output_weight); - - // The initiator pays for all common fields and the shared output in the funding transaction. - if is_initiator { - weight = weight - .saturating_add(TX_COMMON_FIELDS_WEIGHT) - // The weight of the funding output, a P2WSH output - // NOTE: The witness script hash given here is irrelevant as it's a fixed size and we just want - // to calculate the contributed weight, so we use an all-zero hash. - .saturating_add(get_output_weight(&ScriptBuf::new_p2wsh( - &WScriptHash::from_raw_hash(Hash::all_zeros()) - )).to_wu()); - - // The splice initiator pays for the input spending the previous funding output. - if is_splice { - weight = weight - .saturating_add(BASE_INPUT_WEIGHT) - .saturating_add(EMPTY_SCRIPT_SIG_WEIGHT) - .saturating_add(FUNDING_TRANSACTION_WITNESS_WEIGHT); - #[cfg(feature = "grind_signatures")] - { - // Guarantees a low R signature - weight -= 1; - } - } - } - - fee_for_weight(funding_feerate_sat_per_1000_weight, weight) -} - -/// Verify that the provided inputs to the funding transaction are enough -/// to cover the intended contribution amount *plus* the proportional fees. -/// Fees are computed using `estimate_v2_funding_transaction_fee`, and contain -/// the fees of the inputs, fees of the inputs weight, and for the initiator, -/// the fees of the common fields as well as the output and extra input weights. -/// Returns estimated (partial) fees as additional information -#[rustfmt::skip] -fn check_v2_funding_inputs_sufficient( - contributed_input_value: Amount, funding_inputs: &[FundingTxInput], outputs: &[TxOut], - is_initiator: bool, is_splice: bool, funding_feerate_sat_per_1000_weight: u32, -) -> Result { - let estimated_fee = Amount::from_sat(estimate_v2_funding_transaction_fee( - funding_inputs, outputs, is_initiator, is_splice, funding_feerate_sat_per_1000_weight, - )); - - let mut total_input_value = Amount::ZERO; - for FundingTxInput { utxo, .. } in funding_inputs.iter() { - total_input_value = total_input_value.checked_add(utxo.output.value) - .ok_or("Sum of input values is greater than the total bitcoin supply")?; - } - - // If the inputs are enough to cover intended contribution amount, with fees even when - // there is a change output, we are fine. - // If the inputs are less, but enough to cover intended contribution amount, with - // (lower) fees with no change, we are also fine (change will not be generated). - // So it's enough to check considering the lower, no-change fees. - // - // Note: dust limit is not relevant in this check. - // - // TODO(splicing): refine check including the fact wether a change will be added or not. - // Can be done once dual funding preparation is included. - - let minimal_input_amount_needed = contributed_input_value.checked_add(estimated_fee) - .ok_or(format!("{contributed_input_value} contribution plus {estimated_fee} fee estimate exceeds the total bitcoin supply"))?; - if total_input_value < minimal_input_amount_needed { - Err(format!( - "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.", - )) - } else { - Ok(estimated_fee) - } -} - /// Context for negotiating channels (dual-funded V2 open, splicing) #[derive(Debug)] pub(super) struct FundingNegotiationContext { @@ -6969,7 +6862,7 @@ where self.reset_pending_splice_state() } else { match self.quiescent_action.take() { - Some(QuiescentAction::Splice(instructions)) => { + Some(QuiescentAction::LegacySplice(instructions)) => { self.context.channel_state.clear_awaiting_quiescence(); let (inputs, outputs) = instructions.into_contributed_inputs_and_outputs(); Some(SpliceFundingFailed { @@ -6979,6 +6872,15 @@ where contributed_outputs: outputs, }) }, + Some(QuiescentAction::Splice(contribution)) => { + self.context.channel_state.clear_awaiting_quiescence(); + Some(SpliceFundingFailed { + funding_txo: None, + channel_type: None, + contributed_inputs: vec![], + contributed_outputs: contribution.into_outputs(), + }) + }, #[cfg(any(test, fuzzing))] Some(quiescent_action) => { self.quiescent_action = Some(quiescent_action); @@ -11273,7 +11175,7 @@ where self.get_announcement_sigs(node_signer, chain_hash, user_config, block_height, logger); if let Some(quiescent_action) = self.quiescent_action.as_ref() { - if matches!(quiescent_action, QuiescentAction::Splice(_)) { + if matches!(quiescent_action, QuiescentAction::Splice(_) | QuiescentAction::LegacySplice(_)) { self.context.channel_state.set_awaiting_quiescence(); } } @@ -11923,8 +11825,7 @@ where /// - `change_script`: an option change output script. If `None` and needed, one will be /// generated by `SignerProvider::get_destination_script`. pub fn splice_channel( - &mut self, contribution: SpliceContribution, funding_feerate_per_kw: u32, locktime: u32, - logger: &L, + &mut self, contribution: SpliceContribution, logger: &L, ) -> Result, APIError> where L::Target: Logger, @@ -11938,8 +11839,15 @@ where }); } - // Check if a splice has been initiated already. - // Note: only a single outstanding splice is supported (per spec) + if self.context.channel_state.is_quiescent() { + return Err(APIError::APIMisuseError { + err: format!( + "Channel {} cannot be spliced as it is already quiescent", + self.context.channel_id(), + ), + }); + } + if self.pending_splice.is_some() || self.quiescent_action.is_some() { return Err(APIError::APIMisuseError { err: format!( @@ -11968,71 +11876,67 @@ where }); } - // Fees for splice-out are paid from the channel balance whereas fees for splice-in - // are paid by the funding inputs. Therefore, in the case of splice-out, we add the - // fees on top of the user-specified contribution. We leave the user-specified - // contribution as-is for splice-ins. - let adjusted_funding_contribution = check_splice_contribution_sufficient( - &contribution, - true, - FeeRate::from_sat_per_kwu(u64::from(funding_feerate_per_kw)), - ) - .map_err(|e| APIError::APIMisuseError { - err: format!( - "Channel {} cannot be {}; {}", - self.context.channel_id(), - if our_funding_contribution.is_positive() { "spliced in" } else { "spliced out" }, - e - ), - })?; - // Note: post-splice channel value is not yet known at this point, counterparty contribution is not known // (Cannot test for miminum required post-splice channel value) let their_funding_contribution = SignedAmount::ZERO; self.validate_splice_contributions( - adjusted_funding_contribution, + our_funding_contribution, their_funding_contribution, ) .map_err(|err| APIError::APIMisuseError { err })?; - for FundingTxInput { utxo, prevtx, .. } in contribution.inputs().iter() { - const MESSAGE_TEMPLATE: msgs::TxAddInput = msgs::TxAddInput { - channel_id: ChannelId([0; 32]), - serial_id: 0, - prevtx: None, - prevtx_out: 0, - sequence: 0, - // Mutually exclusive with prevtx, which is accounted for below. - shared_input_txid: None, - }; - let message_len = MESSAGE_TEMPLATE.serialized_length() + prevtx.serialized_length(); - if message_len > LN_MAX_MSG_LEN { - return Err(APIError::APIMisuseError { - err: format!( - "Funding input references a prevtx that is too large for tx_add_input: {}", - utxo.outpoint, - ), + self.propose_quiescence(logger, QuiescentAction::Splice(contribution)) + .map_err(|e| APIError::APIMisuseError { err: e.to_owned() }) + } + + pub fn funding_contributed( + &mut self, contribution: FundingContribution, locktime: u32, logger: &L, + ) -> Result + where + L::Target: Logger, + { + let adjusted_funding_contribution = match contribution.validate() { + Ok(adjusted_contribution) => adjusted_contribution, + Err(e) => { + log_error!( + logger, + "Channel {} cannot be {}; {}", + self.context.channel_id(), + if contribution.net_value().is_positive() { "spliced in" } else { "spliced out" }, + e + ); + + let (contributed_inputs, contributed_outputs) = contribution.into_contributed_inputs_and_outputs(); + + return Err(SpliceFundingFailed { + funding_txo: None, + channel_type: None, + contributed_inputs, + contributed_outputs, }); - } - } + }, + }; + let prev_funding_input = self.funding.to_splice_funding_input(); + let is_initiator = contribution.is_initiator(); + let funding_feerate_per_kw = contribution.feerate().to_sat_per_kwu() as u32; let (our_funding_inputs, our_funding_outputs, change_script) = contribution.into_tx_parts(); - let action = QuiescentAction::Splice(SpliceInstructions { - adjusted_funding_contribution, + let context = FundingNegotiationContext { + is_initiator, + our_funding_contribution: adjusted_funding_contribution, + funding_tx_locktime: LockTime::from_consensus(locktime), + funding_feerate_sat_per_1000_weight: funding_feerate_per_kw, + shared_funding_input: Some(prev_funding_input), our_funding_inputs, our_funding_outputs, change_script, - funding_feerate_per_kw, - locktime, - }); - self.propose_quiescence(logger, action) - .map_err(|e| APIError::APIMisuseError { err: e.to_owned() }) + }; + + Ok(self.send_splice_init_internal(context)) } fn send_splice_init(&mut self, instructions: SpliceInstructions) -> msgs::SpliceInit { - debug_assert!(self.pending_splice.is_none()); - let SpliceInstructions { adjusted_funding_contribution, our_funding_inputs, @@ -12054,6 +11958,11 @@ where change_script, }; + self.send_splice_init_internal(context) + } + + fn send_splice_init_internal(&mut self, context: FundingNegotiationContext) -> msgs::SpliceInit { + debug_assert!(self.pending_splice.is_none()); // Rotate the funding pubkey using the prev_funding_txid as a tweak let prev_funding_txid = self.funding.get_funding_txid(); let funding_pubkey = match (prev_funding_txid, &self.context.holder_signer) { @@ -12068,6 +11977,10 @@ where _ => todo!(), }; + let funding_feerate_per_kw = context.funding_feerate_sat_per_1000_weight; + let funding_contribution_satoshis = context.our_funding_contribution.to_sat(); + let locktime = context.funding_tx_locktime.to_consensus_u32(); + let funding_negotiation = FundingNegotiation::AwaitingAck { context, new_holder_funding_key: funding_pubkey }; self.pending_splice = Some(PendingFunding { @@ -12079,7 +11992,7 @@ where msgs::SpliceInit { channel_id: self.context.channel_id, - funding_contribution_satoshis: adjusted_funding_contribution.to_sat(), + funding_contribution_satoshis, funding_feerate_per_kw, locktime, funding_pubkey, @@ -13301,9 +13214,9 @@ where "Internal Error: Didn't have anything to do after reaching quiescence".to_owned() )); }, - Some(QuiescentAction::Splice(instructions)) => { + Some(QuiescentAction::LegacySplice(instructions)) => { if self.pending_splice.is_some() { - self.quiescent_action = Some(QuiescentAction::Splice(instructions)); + self.quiescent_action = Some(QuiescentAction::LegacySplice(instructions)); return Err(ChannelError::WarnAndDisconnect( format!( @@ -13316,6 +13229,29 @@ where let splice_init = self.send_splice_init(instructions); return Ok(Some(StfuResponse::SpliceInit(splice_init))); }, + Some(QuiescentAction::Splice(contribution)) => { + if self.pending_splice.is_some() { + self.quiescent_action = Some(QuiescentAction::Splice(contribution)); + + return Err(ChannelError::WarnAndDisconnect( + format!( + "Channel {} cannot be spliced as it already has a splice pending", + self.context.channel_id(), + ), + )); + } + + let funding_txo = self.funding.get_funding_txo().expect("funding_txo should be set"); + let previous_utxo = self.funding.get_funding_output().expect("funding_output should be set"); + let shared_input = Input { + outpoint: funding_txo.into_bitcoin_outpoint(), + previous_utxo, + satisfaction_weight: EMPTY_SCRIPT_SIG_WEIGHT + FUNDING_TRANSACTION_WITNESS_WEIGHT, + }; + + let funding_template = FundingTemplate::for_splice(contribution, shared_input); + return Ok(Some(StfuResponse::FundingNeeded(funding_template))); + }, #[cfg(any(test, fuzzing))] Some(QuiescentAction::DoNothing) => { // In quiescence test we want to just hang out here, letting the test manually @@ -18191,250 +18127,6 @@ mod tests { assert!(node_a_chan.check_get_channel_ready(0, &&logger).is_some()); } - #[test] - #[rustfmt::skip] - fn test_estimate_v2_funding_transaction_fee() { - use crate::ln::channel::estimate_v2_funding_transaction_fee; - - let one_input = [funding_input_sats(1_000)]; - let two_inputs = [funding_input_sats(1_000), funding_input_sats(1_000)]; - - // 2 inputs, initiator, 2000 sat/kw feerate - assert_eq!( - estimate_v2_funding_transaction_fee(&two_inputs, &[], true, false, 2000), - if cfg!(feature = "grind_signatures") { 1512 } else { 1516 }, - ); - - // higher feerate - assert_eq!( - estimate_v2_funding_transaction_fee(&two_inputs, &[], true, false, 3000), - if cfg!(feature = "grind_signatures") { 2268 } else { 2274 }, - ); - - // only 1 input - assert_eq!( - estimate_v2_funding_transaction_fee(&one_input, &[], true, false, 2000), - if cfg!(feature = "grind_signatures") { 970 } else { 972 }, - ); - - // 0 inputs - assert_eq!( - estimate_v2_funding_transaction_fee(&[], &[], true, false, 2000), - 428, - ); - - // not initiator - assert_eq!( - estimate_v2_funding_transaction_fee(&[], &[], false, false, 2000), - 0, - ); - - // splice initiator - assert_eq!( - estimate_v2_funding_transaction_fee(&one_input, &[], true, true, 2000), - if cfg!(feature = "grind_signatures") { 1736 } else { 1740 }, - ); - - // splice acceptor - assert_eq!( - estimate_v2_funding_transaction_fee(&one_input, &[], false, true, 2000), - if cfg!(feature = "grind_signatures") { 542 } else { 544 }, - ); - } - - #[rustfmt::skip] - fn funding_input_sats(input_value_sats: u64) -> FundingTxInput { - let prevout = TxOut { - value: Amount::from_sat(input_value_sats), - script_pubkey: ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros()), - }; - let prevtx = Transaction { - input: vec![], output: vec![prevout], - version: Version::TWO, lock_time: bitcoin::absolute::LockTime::ZERO, - }; - - FundingTxInput::new_p2wpkh(prevtx, 0).unwrap() - } - - fn funding_output_sats(output_value_sats: u64) -> TxOut { - TxOut { - value: Amount::from_sat(output_value_sats), - script_pubkey: ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros()), - } - } - - #[test] - #[rustfmt::skip] - fn test_check_v2_funding_inputs_sufficient() { - use crate::ln::channel::check_v2_funding_inputs_sufficient; - - // positive case, inputs well over intended contribution - { - let expected_fee = if cfg!(feature = "grind_signatures") { 2278 } else { 2284 }; - assert_eq!( - check_v2_funding_inputs_sufficient( - Amount::from_sat(220_000), - &[ - funding_input_sats(200_000), - funding_input_sats(100_000), - ], - &[], - true, - true, - 2000, - ).unwrap(), - Amount::from_sat(expected_fee), - ); - } - - // Net splice-in - { - let expected_fee = if cfg!(feature = "grind_signatures") { 2526 } else { 2532 }; - assert_eq!( - check_v2_funding_inputs_sufficient( - Amount::from_sat(220_000), - &[ - funding_input_sats(200_000), - funding_input_sats(100_000), - ], - &[ - funding_output_sats(200_000), - ], - true, - true, - 2000, - ).unwrap(), - Amount::from_sat(expected_fee), - ); - } - - // Net splice-out - { - let expected_fee = if cfg!(feature = "grind_signatures") { 2526 } else { 2532 }; - assert_eq!( - check_v2_funding_inputs_sufficient( - Amount::from_sat(220_000), - &[ - funding_input_sats(200_000), - funding_input_sats(100_000), - ], - &[ - funding_output_sats(400_000), - ], - true, - true, - 2000, - ).unwrap(), - Amount::from_sat(expected_fee), - ); - } - - // Net splice-out, inputs insufficient to cover fees - { - let expected_fee = if cfg!(feature = "grind_signatures") { 113670 } else { 113940 }; - assert_eq!( - check_v2_funding_inputs_sufficient( - Amount::from_sat(220_000), - &[ - funding_input_sats(200_000), - funding_input_sats(100_000), - ], - &[ - funding_output_sats(400_000), - ], - true, - true, - 90000, - ), - Err(format!( - "Total input amount 0.00300000 BTC is lower than needed for splice-in contribution 0.00220000 BTC, considering fees of {}. Need more inputs.", - Amount::from_sat(expected_fee), - )), - ); - } - - // negative case, inputs clearly insufficient - { - let expected_fee = if cfg!(feature = "grind_signatures") { 1736 } else { 1740 }; - assert_eq!( - check_v2_funding_inputs_sufficient( - Amount::from_sat(220_000), - &[ - funding_input_sats(100_000), - ], - &[], - true, - true, - 2000, - ), - Err(format!( - "Total input amount 0.00100000 BTC is lower than needed for splice-in contribution 0.00220000 BTC, considering fees of {}. Need more inputs.", - Amount::from_sat(expected_fee), - )), - ); - } - - // barely covers - { - let expected_fee = if cfg!(feature = "grind_signatures") { 2278 } else { 2284 }; - assert_eq!( - check_v2_funding_inputs_sufficient( - Amount::from_sat(300_000 - expected_fee - 20), - &[ - funding_input_sats(200_000), - funding_input_sats(100_000), - ], - &[], - true, - true, - 2000, - ).unwrap(), - Amount::from_sat(expected_fee), - ); - } - - // higher fee rate, does not cover - { - let expected_fee = if cfg!(feature = "grind_signatures") { 2506 } else { 2513 }; - assert_eq!( - check_v2_funding_inputs_sufficient( - Amount::from_sat(298032), - &[ - funding_input_sats(200_000), - funding_input_sats(100_000), - ], - &[], - true, - true, - 2200, - ), - Err(format!( - "Total input amount 0.00300000 BTC is lower than needed for splice-in contribution 0.00298032 BTC, considering fees of {}. Need more inputs.", - Amount::from_sat(expected_fee), - )), - ); - } - - // barely covers, less fees (no extra weight, not initiator) - { - let expected_fee = if cfg!(feature = "grind_signatures") { 1084 } else { 1088 }; - assert_eq!( - check_v2_funding_inputs_sufficient( - Amount::from_sat(300_000 - expected_fee - 20), - &[ - funding_input_sats(200_000), - funding_input_sats(100_000), - ], - &[], - false, - false, - 2000, - ).unwrap(), - Amount::from_sat(expected_fee), - ); - } - } - fn get_pre_and_post( pre_channel_value: u64, our_funding_contribution: i64, their_funding_contribution: i64, ) -> (u64, u64) { diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 72585d69f80..c85940ff3c6 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -63,7 +63,7 @@ use crate::ln::channel::{ WithChannelContext, }; use crate::ln::channel_state::ChannelDetails; -use crate::ln::funding::SpliceContribution; +use crate::ln::funding::{FundingContribution, SpliceContribution}; use crate::ln::inbound_payment; use crate::ln::interactivetxs::InteractiveTxMessageSend; use crate::ln::msgs; @@ -4743,12 +4743,12 @@ where #[rustfmt::skip] pub fn splice_channel( &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, - contribution: SpliceContribution, funding_feerate_per_kw: u32, locktime: Option, + contribution: SpliceContribution, ) -> Result<(), APIError> { let mut res = Ok(()); PersistenceNotifierGuard::optionally_notify(self, || { let result = self.internal_splice_channel( - channel_id, counterparty_node_id, contribution, funding_feerate_per_kw, locktime + channel_id, counterparty_node_id, contribution, ); res = result; match res { @@ -4761,7 +4761,7 @@ where fn internal_splice_channel( &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, - contribution: SpliceContribution, funding_feerate_per_kw: u32, locktime: Option, + contribution: SpliceContribution, ) -> Result<(), APIError> { let per_peer_state = self.per_peer_state.read().unwrap(); @@ -4780,15 +4780,9 @@ where // Look for the channel match peer_state.channel_by_id.entry(*channel_id) { hash_map::Entry::Occupied(mut chan_phase_entry) => { - let locktime = locktime.unwrap_or_else(|| self.current_best_block().height); if let Some(chan) = chan_phase_entry.get_mut().as_funded_mut() { let logger = WithChannelContext::from(&self.logger, &chan.context, None); - let msg_opt = chan.splice_channel( - contribution, - funding_feerate_per_kw, - locktime, - &&logger, - )?; + let msg_opt = chan.splice_channel(contribution, &&logger)?; if let Some(msg) = msg_opt { peer_state.pending_msg_events.push(MessageSendEvent::SendStfu { node_id: *counterparty_node_id, @@ -6417,6 +6411,79 @@ where result } + /// + pub fn funding_contributed( + &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, + contribution: FundingContribution, locktime: Option, + ) -> Result<(), APIError> { + let mut result = Ok(()); + PersistenceNotifierGuard::optionally_notify(self, || { + let per_peer_state = self.per_peer_state.read().unwrap(); + let peer_state_mutex_opt = per_peer_state.get(counterparty_node_id); + if peer_state_mutex_opt.is_none() { + result = Err(APIError::ChannelUnavailable { + err: format!("Can't find a peer matching the passed counterparty node_id {counterparty_node_id}") + }); + return NotifyOption::SkipPersistNoEvents; + } + + let mut peer_state = peer_state_mutex_opt.unwrap().lock().unwrap(); + + match peer_state.channel_by_id.get_mut(channel_id) { + Some(channel) => match channel.as_funded_mut() { + Some(chan) => { + let locktime = locktime.unwrap_or_else(|| self.current_best_block().height); + let logger = WithChannelContext::from(&self.logger, chan.context(), None); + match chan.funding_contributed(contribution, locktime, &&logger) { + Ok(msg) => { + peer_state.pending_msg_events.push( + MessageSendEvent::SendSpliceInit { + node_id: *counterparty_node_id, + msg, + }, + ); + }, + Err(splice_funding_failed) => { + let pending_events = &mut self.pending_events.lock().unwrap(); + pending_events.push_back((events::Event::SpliceFailed { + channel_id: *channel_id, + counterparty_node_id: *counterparty_node_id, + user_channel_id: channel.context().get_user_id(), + abandoned_funding_txo: splice_funding_failed.funding_txo, + channel_type: splice_funding_failed.channel_type.clone(), + contributed_inputs: splice_funding_failed.contributed_inputs, + contributed_outputs: splice_funding_failed.contributed_outputs, + }, None)); + }, + } + + return NotifyOption::DoPersist; + }, + None => { + result = Err(APIError::APIMisuseError { + err: format!( + "Channel with id {} not expecting funding contribution", + channel_id + ), + }); + return NotifyOption::SkipPersistNoEvents; + }, + }, + None => { + result = Err(APIError::ChannelUnavailable { + err: format!( + "Channel with id {} not found for the passed counterparty node_id {}", + channel_id, counterparty_node_id + ), + }); + return NotifyOption::SkipPersistNoEvents; + }, + } + }); + + result + } + /// Handles a signed funding transaction generated by interactive transaction construction and /// provided by the client. Should only be called in response to a [`FundingTransactionReadyForSigning`] /// event. @@ -11699,6 +11766,19 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ }); Ok(true) }, + Some(StfuResponse::FundingNeeded(funding_template)) => { + let mut pending_events = self.pending_events.lock().unwrap(); + pending_events.push_back(( + events::Event::FundingNeeded { + channel_id: chan.context.channel_id(), + user_channel_id: chan.context.get_user_id(), + counterparty_node_id: chan.context.get_counterparty_node_id(), + funding_template, + }, + None, + )); + Ok(true) + }, } } else { let msg = "Peer sent `stfu` for an unfunded channel"; diff --git a/lightning/src/ln/funding.rs b/lightning/src/ln/funding.rs index 625b80afab5..24027bd29ae 100644 --- a/lightning/src/ln/funding.rs +++ b/lightning/src/ln/funding.rs @@ -11,25 +11,32 @@ use alloc::vec::Vec; -use bitcoin::{Amount, ScriptBuf, SignedAmount, TxOut}; -use bitcoin::{Script, Sequence, Transaction, Weight}; - -use crate::events::bump_transaction::Utxo; -use crate::ln::chan_utils::EMPTY_SCRIPT_SIG_WEIGHT; +use bitcoin::{ + Amount, FeeRate, OutPoint, Script, ScriptBuf, Sequence, SignedAmount, Transaction, TxOut, Weight, + WScriptHash, +}; +use bitcoin::hashes::Hash; +use bitcoin::secp256k1::PublicKey; + +use crate::chain::ClaimId; +use crate::events::bump_transaction::{CoinSelection, CoinSelectionSource, Input, Utxo}; +use crate::events::bump_transaction::sync::CoinSelectionSourceSync; +use crate::ln::chan_utils::{make_funding_redeemscript, BASE_INPUT_WEIGHT, EMPTY_SCRIPT_SIG_WEIGHT, FUNDING_TRANSACTION_WITNESS_WEIGHT}; +use crate::ln::interactivetxs::{ + get_output_weight, TX_COMMON_FIELDS_WEIGHT, +}; +use crate::ln::msgs; +use crate::ln::types::ChannelId; +use crate::ln::LN_MAX_MSG_LEN; use crate::sign::{P2TR_KEY_PATH_WITNESS_WEIGHT, P2WPKH_WITNESS_WEIGHT}; +use crate::util::async_poll::AsyncResult; /// The components of a splice's funding transaction that are contributed by one party. #[derive(Debug, Clone)] pub struct SpliceContribution { - /// The amount from [`inputs`] to contribute to the splice. - /// - /// [`inputs`]: Self::inputs + /// The amount of value to contribute from inputs to the splice's funding transaction. value_added: Amount, - /// The inputs included in the splice's funding transaction to meet the contributed amount - /// plus fees. Any excess amount will be sent to a change output. - inputs: Vec, - /// The outputs to include in the splice's funding transaction. The total value of all /// outputs plus fees will be the amount that is removed. outputs: Vec, @@ -41,17 +48,23 @@ pub struct SpliceContribution { change_script: Option, } +impl_writeable_tlv_based!(SpliceContribution, { + (1, value_added, required), + (3, outputs, optional_vec), + (5, change_script, option), +}); + impl SpliceContribution { /// Creates a contribution for when funds are only added to a channel. pub fn splice_in( - value_added: Amount, inputs: Vec, change_script: Option, + value_added: Amount, change_script: Option, ) -> Self { - Self { value_added, inputs, outputs: vec![], change_script } + Self { value_added, outputs: vec![], change_script } } /// Creates a contribution for when funds are only removed from a channel. pub fn splice_out(outputs: Vec) -> Self { - Self { value_added: Amount::ZERO, inputs: vec![], outputs, change_script: None } + Self { value_added: Amount::ZERO, outputs, change_script: None } } /// Creates a contribution for when funds are both added to and removed from a channel. @@ -60,10 +73,9 @@ impl SpliceContribution { /// value removed by `outputs`. The net value contributed can be obtained by calling /// [`SpliceContribution::net_value`]. pub fn splice_in_and_out( - value_added: Amount, inputs: Vec, outputs: Vec, - change_script: Option, + value_added: Amount, outputs: Vec, change_script: Option, ) -> Self { - Self { value_added, inputs, outputs, change_script } + Self { value_added, outputs, change_script } } /// The net value contributed to a channel by the splice. If negative, more value will be @@ -81,22 +93,287 @@ impl SpliceContribution { value_added - value_removed } - pub(super) fn value_added(&self) -> Amount { - self.value_added + pub(super) fn into_outputs(self) -> Vec { + self.outputs + } +} + +/// +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FundingTemplate { + /// The amount from [`inputs`] to contribute to the splice. + /// + /// [`inputs`]: Self::inputs + value_added: Amount, + + /// The outputs to include in the splice's funding transaction. The total value of all + /// outputs plus fees will be the amount that is removed. + outputs: Vec, + + change_script: Option, + + shared_input: Option, + + is_initiator: bool, +} + +impl_writeable_tlv_based!(FundingTemplate, { + (1, value_added, required), + (3, outputs, optional_vec), + (5, change_script, option), + (7, shared_input, option), + (9, is_initiator, required), +}); + +impl FundingTemplate { + /// + pub(super) fn for_splice( + contribution: SpliceContribution, shared_input: Input, + ) -> Self { + let SpliceContribution { value_added, outputs, change_script } = contribution; + Self { + value_added, + outputs, + change_script, + shared_input: Some(shared_input), + is_initiator: true, + } + } + + /// FIXME: Can these be combined or is a macro needed to DRY them up? + + /// + pub fn build<'a, W: CoinSelectionSource>(self, wallet: W, feerate: FeeRate) -> AsyncResult<'a, FundingContribution, ()> { + todo!() + } + + /// + pub fn build_sync(self, wallet: W, feerate: FeeRate) -> Result { + let FundingTemplate { value_added, outputs, change_script, shared_input, is_initiator } = self; + + let value_removed = outputs.iter().map(|txout| txout.value).sum(); + let is_splice = shared_input.is_some(); + + let inputs = if value_added == Amount::ZERO { + vec![] + } else { + // Used for creating a redeem script for the new funding txo, since the funding pubkeys + // are unknown at this point. Only needed when selecting which UTXOs to include in the + // funding tx that would be sufficient to pay for fees. Hence, the value doesn't matter. + let dummy_pubkey = PublicKey::from_slice(&[2; 33]).unwrap(); + + let shared_output = bitcoin::TxOut { + value: shared_input + .as_ref() + .map(|shared_input| shared_input.previous_utxo.value) + .unwrap_or(Amount::ZERO) + .checked_add(value_added) + .ok_or(())? + .checked_sub(value_removed) + .ok_or(())?, + script_pubkey: make_funding_redeemscript(&dummy_pubkey, &dummy_pubkey).to_p2wsh(), + }; + + let claim_id = ClaimId([0; 32]); + let must_spend = shared_input.map(|input| vec![input]).unwrap_or_default(); + let must_pay_to = &[shared_output]; + let selection = wallet.select_confirmed_utxos(claim_id, must_spend, must_pay_to, feerate.to_sat_per_kwu() as u32, u64::MAX)?; + selection.confirmed_utxos + }; + + // NOTE: Must NOT fail after UTXO selection + + let estimated_fee = estimate_transaction_fee(&inputs, &outputs, is_initiator, is_splice, feerate); + + let contribution = FundingContribution { + value_added, + value_removed, + estimated_fee, + inputs, + outputs, + change_script, + feerate, + is_initiator, + is_splice, + }; + + Ok(contribution) + } +} + +fn estimate_transaction_fee( + inputs: &[FundingTxInput], outputs: &[TxOut], is_initiator: bool, is_splice: bool, + feerate: FeeRate, +) -> Amount { + let input_weight: u64 = inputs + .iter() + .map(|input| BASE_INPUT_WEIGHT.saturating_add(input.utxo.satisfaction_weight)) + .fold(0, |total_weight, input_weight| total_weight.saturating_add(input_weight)); + + let output_weight: u64 = outputs + .iter() + .map(|txout| txout.weight().to_wu()) + .fold(0, |total_weight, output_weight| total_weight.saturating_add(output_weight)); + + let mut weight = input_weight.saturating_add(output_weight); + + // The initiator pays for all common fields and the shared output in the funding transaction. + if is_initiator { + weight = weight + .saturating_add(TX_COMMON_FIELDS_WEIGHT) + // The weight of the funding output, a P2WSH output + // NOTE: The witness script hash given here is irrelevant as it's a fixed size and we just want + // to calculate the contributed weight, so we use an all-zero hash. + .saturating_add(get_output_weight(&ScriptBuf::new_p2wsh( + &WScriptHash::from_raw_hash(Hash::all_zeros()) + )).to_wu()); + + // The splice initiator pays for the input spending the previous funding output. + if is_splice { + weight = weight + .saturating_add(BASE_INPUT_WEIGHT) + .saturating_add(EMPTY_SCRIPT_SIG_WEIGHT) + .saturating_add(FUNDING_TRANSACTION_WITNESS_WEIGHT); + #[cfg(feature = "grind_signatures")] + { + // Guarantees a low R signature + weight -= 1; + } + } + } + + Weight::from_wu(weight) * feerate +} + +/// The components of a splice's funding transaction that are contributed by one party. +#[derive(Debug, Clone)] +pub struct FundingContribution { + value_added: Amount, + + value_removed: Amount, + + estimated_fee: Amount, + + /// The inputs included in the splice's funding transaction to meet the contributed amount + /// plus fees. Any excess amount will be sent to a change output. + inputs: Vec, + + /// The outputs to include in the splice's funding transaction. The total value of all + /// outputs plus fees will be the amount that is removed. + outputs: Vec, + + change_script: Option, + + feerate: FeeRate, + + is_initiator: bool, + + is_splice: bool, +} + +impl FundingContribution { + /// The net value contributed to a channel by the splice. If negative, more value will be + /// spliced out than spliced in. + pub fn net_value(&self) -> SignedAmount { + let value_added = self.value_added.to_signed().unwrap_or(SignedAmount::MAX); + let value_removed = self.value_removed.to_signed().unwrap_or(SignedAmount::MAX); + + value_added - value_removed } - pub(super) fn inputs(&self) -> &[FundingTxInput] { - &self.inputs[..] + pub(super) fn feerate(&self) -> FeeRate { + self.feerate } - pub(super) fn outputs(&self) -> &[TxOut] { - &self.outputs[..] + pub(super) fn is_initiator(&self) -> bool { + self.is_initiator } pub(super) fn into_tx_parts(self) -> (Vec, Vec, Option) { - let SpliceContribution { value_added: _, inputs, outputs, change_script } = self; + let FundingContribution { inputs, outputs, change_script, .. } = self; (inputs, outputs, change_script) } + + pub(super) fn into_contributed_inputs_and_outputs(self) -> (Vec, Vec) { + ( + self.inputs.into_iter().map(|input| input.utxo.outpoint).collect(), + self.outputs, + ) + } + + pub(super) fn validate(&self) -> Result { + debug_assert!(self.is_splice); + + for FundingTxInput { utxo, prevtx, .. } in self.inputs.iter() { + use crate::util::ser::Writeable; + const MESSAGE_TEMPLATE: msgs::TxAddInput = msgs::TxAddInput { + channel_id: ChannelId([0; 32]), + serial_id: 0, + prevtx: None, + prevtx_out: 0, + sequence: 0, + // Mutually exclusive with prevtx, which is accounted for below. + shared_input_txid: None, + }; + let message_len = MESSAGE_TEMPLATE.serialized_length() + prevtx.serialized_length(); + if message_len > LN_MAX_MSG_LEN { + return Err(format!("Funding input references a prevtx that is too large for tx_add_input: {}", utxo.outpoint)); + } + } + + // Fees for splice-out are paid from the channel balance whereas fees for splice-in + // are paid by the funding inputs. Therefore, in the case of splice-out, we add the + // fees on top of the user-specified contribution. We leave the user-specified + // contribution as-is for splice-ins. + if !self.inputs.is_empty() { + let mut total_input_value = Amount::ZERO; + for FundingTxInput { utxo, .. } in self.inputs.iter() { + total_input_value = total_input_value + .checked_add(utxo.output.value) + .ok_or("Sum of input values is greater than the total bitcoin supply")?; + } + + // If the inputs are enough to cover intended contribution amount, with fees even when + // there is a change output, we are fine. + // If the inputs are less, but enough to cover intended contribution amount, with + // (lower) fees with no change, we are also fine (change will not be generated). + // So it's enough to check considering the lower, no-change fees. + // + // Note: dust limit is not relevant in this check. + // + // TODO(splicing): refine check including the fact wether a change will be added or not. + // Can be done once dual funding preparation is included. + + let contributed_input_value = self.value_added; + let estimated_fee = self.estimated_fee; + let minimal_input_amount_needed = contributed_input_value + .checked_add(estimated_fee) + .ok_or(format!("{contributed_input_value} contribution plus {estimated_fee} fee estimate exceeds the total bitcoin supply"))?; + if total_input_value < minimal_input_amount_needed { + return Err(format!( + "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.", + )); + } + } + + let unpaid_fees = self + .inputs + .is_empty() + .then_some(self.estimated_fee) + .unwrap_or(Amount::ZERO) + .to_signed() + .expect("fees should never exceed Amount::MAX_MONEY"); + let contribution_amount = self.net_value(); + let adjusted_contribution = contribution_amount + .checked_sub(unpaid_fees) + .ok_or(format!( + "{} splice-out amount plus {} fee estimate exceeds the total bitcoin supply", + contribution_amount.unsigned_abs(), + self.estimated_fee, + ))?; + + Ok(adjusted_contribution) + } } /// An input to contribute to a channel's funding transaction either when using the v2 channel @@ -255,3 +532,252 @@ impl FundingTxInput { self.utxo.output } } + +#[cfg(test)] +mod tests { + use bitcoin::{Amount, FeeRate, ScriptBuf, WPubkeyHash}; + use bitcoin::transaction::{Transaction, TxOut, Version}; + use super::{FundingContribution, FundingTxInput, estimate_transaction_fee}; + + #[test] + #[rustfmt::skip] + fn test_estimate_transaction_fee() { + let one_input = [funding_input_sats(1_000)]; + let two_inputs = [funding_input_sats(1_000), funding_input_sats(1_000)]; + + // 2 inputs, initiator, 2000 sat/kw feerate + assert_eq!( + estimate_transaction_fee(&two_inputs, &[], true, false, FeeRate::from_sat_per_kwu(2000)), + Amount::from_sat(if cfg!(feature = "grind_signatures") { 1512 } else { 1516 }), + ); + + // higher feerate + assert_eq!( + estimate_transaction_fee(&two_inputs, &[], true, false, FeeRate::from_sat_per_kwu(3000)), + Amount::from_sat(if cfg!(feature = "grind_signatures") { 2268 } else { 2274 }), + ); + + // only 1 input + assert_eq!( + estimate_transaction_fee(&one_input, &[], true, false, FeeRate::from_sat_per_kwu(2000)), + Amount::from_sat(if cfg!(feature = "grind_signatures") { 970 } else { 972 }), + ); + + // 0 inputs + assert_eq!( + estimate_transaction_fee(&[], &[], true, false, FeeRate::from_sat_per_kwu(2000)), + Amount::from_sat(428), + ); + + // not initiator + assert_eq!( + estimate_transaction_fee(&[], &[], false, false, FeeRate::from_sat_per_kwu(2000)), + Amount::from_sat(0), + ); + + // splice initiator + assert_eq!( + estimate_transaction_fee(&one_input, &[], true, true, FeeRate::from_sat_per_kwu(2000)), + Amount::from_sat(if cfg!(feature = "grind_signatures") { 1736 } else { 1740 }), + ); + + // splice acceptor + assert_eq!( + estimate_transaction_fee(&one_input, &[], false, true, FeeRate::from_sat_per_kwu(2000)), + Amount::from_sat(if cfg!(feature = "grind_signatures") { 542 } else { 544 }), + ); + } + + #[rustfmt::skip] + fn funding_input_sats(input_value_sats: u64) -> FundingTxInput { + let prevout = TxOut { + value: Amount::from_sat(input_value_sats), + script_pubkey: ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros()), + }; + let prevtx = Transaction { + input: vec![], output: vec![prevout], + version: Version::TWO, lock_time: bitcoin::absolute::LockTime::ZERO, + }; + + FundingTxInput::new_p2wpkh(prevtx, 0).unwrap() + } + + fn funding_output_sats(output_value_sats: u64) -> TxOut { + TxOut { + value: Amount::from_sat(output_value_sats), + script_pubkey: ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros()), + } + } + + #[test] + #[rustfmt::skip] + fn test_check_v2_funding_inputs_sufficient() { + use crate::ln::channel::check_v2_funding_inputs_sufficient; + + // positive case, inputs well over intended contribution + { + let expected_fee = if cfg!(feature = "grind_signatures") { 2278 } else { 2284 }; + assert_eq!( + check_v2_funding_inputs_sufficient( + Amount::from_sat(220_000), + &[ + funding_input_sats(200_000), + funding_input_sats(100_000), + ], + &[], + true, + true, + 2000, + ).unwrap(), + Amount::from_sat(expected_fee), + ); + } + + // Net splice-in + { + let expected_fee = if cfg!(feature = "grind_signatures") { 2526 } else { 2532 }; + assert_eq!( + check_v2_funding_inputs_sufficient( + Amount::from_sat(220_000), + &[ + funding_input_sats(200_000), + funding_input_sats(100_000), + ], + &[ + funding_output_sats(200_000), + ], + true, + true, + 2000, + ).unwrap(), + Amount::from_sat(expected_fee), + ); + } + + // Net splice-out + { + let expected_fee = if cfg!(feature = "grind_signatures") { 2526 } else { 2532 }; + assert_eq!( + check_v2_funding_inputs_sufficient( + Amount::from_sat(220_000), + &[ + funding_input_sats(200_000), + funding_input_sats(100_000), + ], + &[ + funding_output_sats(400_000), + ], + true, + true, + 2000, + ).unwrap(), + Amount::from_sat(expected_fee), + ); + } + + // Net splice-out, inputs insufficient to cover fees + { + let expected_fee = if cfg!(feature = "grind_signatures") { 113670 } else { 113940 }; + assert_eq!( + check_v2_funding_inputs_sufficient( + Amount::from_sat(220_000), + &[ + funding_input_sats(200_000), + funding_input_sats(100_000), + ], + &[ + funding_output_sats(400_000), + ], + true, + true, + 90000, + ), + Err(format!( + "Total input amount 0.00300000 BTC is lower than needed for splice-in contribution 0.00220000 BTC, considering fees of {}. Need more inputs.", + Amount::from_sat(expected_fee), + )), + ); + } + + // negative case, inputs clearly insufficient + { + let expected_fee = if cfg!(feature = "grind_signatures") { 1736 } else { 1740 }; + assert_eq!( + check_v2_funding_inputs_sufficient( + Amount::from_sat(220_000), + &[ + funding_input_sats(100_000), + ], + &[], + true, + true, + 2000, + ), + Err(format!( + "Total input amount 0.00100000 BTC is lower than needed for splice-in contribution 0.00220000 BTC, considering fees of {}. Need more inputs.", + Amount::from_sat(expected_fee), + )), + ); + } + + // barely covers + { + let expected_fee = if cfg!(feature = "grind_signatures") { 2278 } else { 2284 }; + assert_eq!( + check_v2_funding_inputs_sufficient( + Amount::from_sat(300_000 - expected_fee - 20), + &[ + funding_input_sats(200_000), + funding_input_sats(100_000), + ], + &[], + true, + true, + 2000, + ).unwrap(), + Amount::from_sat(expected_fee), + ); + } + + // higher fee rate, does not cover + { + let expected_fee = if cfg!(feature = "grind_signatures") { 2506 } else { 2513 }; + assert_eq!( + check_v2_funding_inputs_sufficient( + Amount::from_sat(298032), + &[ + funding_input_sats(200_000), + funding_input_sats(100_000), + ], + &[], + true, + true, + 2200, + ), + Err(format!( + "Total input amount 0.00300000 BTC is lower than needed for splice-in contribution 0.00298032 BTC, considering fees of {}. Need more inputs.", + Amount::from_sat(expected_fee), + )), + ); + } + + // barely covers, less fees (no extra weight, not initiator) + { + let expected_fee = if cfg!(feature = "grind_signatures") { 1084 } else { 1088 }; + assert_eq!( + check_v2_funding_inputs_sufficient( + Amount::from_sat(300_000 - expected_fee - 20), + &[ + funding_input_sats(200_000), + funding_input_sats(100_000), + ], + &[], + false, + false, + 2000, + ).unwrap(), + Amount::from_sat(expected_fee), + ); + } + } +} From 94b1aa93066717265e6e0777017e8e3ca52ec1ed Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Wed, 14 Jan 2026 15:28:52 -0600 Subject: [PATCH 13/13] f - fix tests --- lightning/src/events/bump_transaction/mod.rs | 2 +- lightning/src/events/bump_transaction/sync.rs | 3 +- lightning/src/ln/channel.rs | 3 +- lightning/src/ln/functional_test_utils.rs | 4 + lightning/src/ln/funding.rs | 266 ++++++++++-------- lightning/src/ln/splicing_tests.rs | 174 ++++++------ lightning/src/util/test_utils.rs | 4 + 7 files changed, 251 insertions(+), 205 deletions(-) diff --git a/lightning/src/events/bump_transaction/mod.rs b/lightning/src/events/bump_transaction/mod.rs index 9d921763fab..2404ae98611 100644 --- a/lightning/src/events/bump_transaction/mod.rs +++ b/lightning/src/events/bump_transaction/mod.rs @@ -50,7 +50,7 @@ use bitcoin::secp256k1::ecdsa::Signature; use bitcoin::secp256k1::{PublicKey, Secp256k1}; use bitcoin::transaction::Version; use bitcoin::{ - FeeRate, OutPoint, Psbt, PubkeyHash, ScriptBuf, Sequence, Transaction, TxIn, TxOut, WPubkeyHash, Witness, + OutPoint, Psbt, PubkeyHash, ScriptBuf, Sequence, Transaction, TxIn, TxOut, WPubkeyHash, Witness, }; /// A descriptor used to sign for a commitment transaction's anchor output. diff --git a/lightning/src/events/bump_transaction/sync.rs b/lightning/src/events/bump_transaction/sync.rs index 77066ff952e..75be588848a 100644 --- a/lightning/src/events/bump_transaction/sync.rs +++ b/lightning/src/events/bump_transaction/sync.rs @@ -15,13 +15,12 @@ use core::task; use crate::chain::chaininterface::BroadcasterInterface; use crate::chain::ClaimId; -use crate::ln::funding::FundingTxInput; use crate::prelude::*; use crate::sign::SignerProvider; use crate::util::async_poll::{dummy_waker, AsyncResult, MaybeSend, MaybeSync}; use crate::util::logger::Logger; -use bitcoin::{FeeRate, Psbt, ScriptBuf, Transaction, TxOut}; +use bitcoin::{Psbt, ScriptBuf, Transaction, TxOut}; use super::BumpTransactionEvent; use super::{ diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index e57b3bbe81a..07315debb71 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -15758,7 +15758,6 @@ mod tests { }; use crate::ln::channel_keys::{RevocationBasepoint, RevocationKey}; use crate::ln::channelmanager::{self, HTLCSource, PaymentId}; - use crate::ln::funding::FundingTxInput; use crate::ln::msgs; use crate::ln::msgs::{ChannelUpdate, UnsignedChannelUpdate, MAX_VALUE_MSAT}; use crate::ln::onion_utils::{AttributionData, LocalHTLCFailureReason}; @@ -15790,7 +15789,7 @@ mod tests { use bitcoin::secp256k1::{ecdsa::Signature, Secp256k1}; use bitcoin::secp256k1::{PublicKey, SecretKey}; use bitcoin::transaction::{Transaction, TxOut, Version}; - use bitcoin::{ScriptBuf, WPubkeyHash, WitnessProgram, WitnessVersion}; + use bitcoin::{WitnessProgram, WitnessVersion}; use std::cmp; #[test] diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index 6de14478612..c598b83b054 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -612,6 +612,10 @@ impl<'a, 'b, 'c> Node<'a, 'b, 'c> { self.blocks.lock().unwrap()[height as usize].0.header } + pub fn provide_funding_utxos(&self, utxos: usize, amount: Amount) -> Transaction { + provide_anchor_utxo_reserves(core::slice::from_ref(self), utxos, amount) + } + /// Executes `enable_channel_signer_op` for every single signer operation for this channel. #[cfg(test)] pub fn enable_all_channel_signer_ops(&self, peer_id: &PublicKey, chan_id: &ChannelId) { diff --git a/lightning/src/ln/funding.rs b/lightning/src/ln/funding.rs index 24027bd29ae..cd3285b4f66 100644 --- a/lightning/src/ln/funding.rs +++ b/lightning/src/ln/funding.rs @@ -18,8 +18,10 @@ use bitcoin::{ use bitcoin::hashes::Hash; use bitcoin::secp256k1::PublicKey; +use core::ops::Deref; + use crate::chain::ClaimId; -use crate::events::bump_transaction::{CoinSelection, CoinSelectionSource, Input, Utxo}; +use crate::events::bump_transaction::{CoinSelectionSource, Input, Utxo}; use crate::events::bump_transaction::sync::CoinSelectionSourceSync; use crate::ln::chan_utils::{make_funding_redeemscript, BASE_INPUT_WEIGHT, EMPTY_SCRIPT_SIG_WEIGHT, FUNDING_TRANSACTION_WITNESS_WEIGHT}; use crate::ln::interactivetxs::{ @@ -96,6 +98,11 @@ impl SpliceContribution { pub(super) fn into_outputs(self) -> Vec { self.outputs } + + #[cfg(test)] + pub(super) fn into_tx_parts(self) -> (Vec, Option) { + (self.outputs, self.change_script) + } } /// @@ -143,12 +150,18 @@ impl FundingTemplate { /// FIXME: Can these be combined or is a macro needed to DRY them up? /// - pub fn build<'a, W: CoinSelectionSource>(self, wallet: W, feerate: FeeRate) -> AsyncResult<'a, FundingContribution, ()> { + pub fn build<'a, W: Deref>(self, wallet: W, feerate: FeeRate) -> AsyncResult<'a, FundingContribution, ()> + where + W::Target: CoinSelectionSource, + { todo!() } /// - pub fn build_sync(self, wallet: W, feerate: FeeRate) -> Result { + pub fn build_sync(self, wallet: W, feerate: FeeRate) -> Result + where + W::Target: CoinSelectionSourceSync, + { let FundingTemplate { value_added, outputs, change_script, shared_input, is_initiator } = self; let value_removed = outputs.iter().map(|txout| txout.value).sum(); @@ -176,8 +189,13 @@ impl FundingTemplate { let claim_id = ClaimId([0; 32]); let must_spend = shared_input.map(|input| vec![input]).unwrap_or_default(); - let must_pay_to = &[shared_output]; - let selection = wallet.select_confirmed_utxos(claim_id, must_spend, must_pay_to, feerate.to_sat_per_kwu() as u32, u64::MAX)?; + let selection = if outputs.is_empty() { + let must_pay_to = &[shared_output]; + wallet.select_confirmed_utxos(claim_id, must_spend, must_pay_to, feerate.to_sat_per_kwu() as u32, u64::MAX)? + } else { + let must_pay_to: Vec<_> = outputs.iter().cloned().chain(core::iter::once(shared_output)).collect(); + wallet.select_confirmed_utxos(claim_id, must_spend, &must_pay_to, feerate.to_sat_per_kwu() as u32, u64::MAX)? + }; selection.confirmed_utxos }; @@ -302,8 +320,6 @@ impl FundingContribution { } pub(super) fn validate(&self) -> Result { - debug_assert!(self.is_splice); - for FundingTxInput { utxo, prevtx, .. } in self.inputs.iter() { use crate::util::ser::Writeable; const MESSAGE_TEMPLATE: msgs::TxAddInput = msgs::TxAddInput { @@ -536,6 +552,7 @@ impl FundingTxInput { #[cfg(test)] mod tests { use bitcoin::{Amount, FeeRate, ScriptBuf, WPubkeyHash}; + use bitcoin::hashes::Hash; use bitcoin::transaction::{Transaction, TxOut, Version}; use super::{FundingContribution, FundingTxInput, estimate_transaction_fee}; @@ -612,86 +629,91 @@ mod tests { #[test] #[rustfmt::skip] fn test_check_v2_funding_inputs_sufficient() { - use crate::ln::channel::check_v2_funding_inputs_sufficient; - // positive case, inputs well over intended contribution { let expected_fee = if cfg!(feature = "grind_signatures") { 2278 } else { 2284 }; - assert_eq!( - check_v2_funding_inputs_sufficient( - Amount::from_sat(220_000), - &[ - funding_input_sats(200_000), - funding_input_sats(100_000), - ], - &[], - true, - true, - 2000, - ).unwrap(), - Amount::from_sat(expected_fee), - ); + let contribution = FundingContribution { + value_added: Amount::from_sat(220_000), + value_removed: Amount::ZERO, + estimated_fee: Amount::from_sat(expected_fee), + inputs: vec![ + funding_input_sats(200_000), + funding_input_sats(100_000), + ], + outputs: vec![], + change_script: None, + is_initiator: true, + is_splice: true, + feerate: FeeRate::from_sat_per_kwu(2000), + }; + assert_eq!(contribution.validate(), Ok(contribution.value_added.to_signed().unwrap())); } // Net splice-in { let expected_fee = if cfg!(feature = "grind_signatures") { 2526 } else { 2532 }; - assert_eq!( - check_v2_funding_inputs_sufficient( - Amount::from_sat(220_000), - &[ - funding_input_sats(200_000), - funding_input_sats(100_000), - ], - &[ - funding_output_sats(200_000), - ], - true, - true, - 2000, - ).unwrap(), - Amount::from_sat(expected_fee), - ); + let contribution = FundingContribution { + value_added: Amount::from_sat(220_000), + value_removed: Amount::ZERO, + estimated_fee: Amount::from_sat(expected_fee), + inputs: vec![ + funding_input_sats(200_000), + funding_input_sats(100_000), + ], + outputs: vec![ + funding_output_sats(200_000), + ], + change_script: None, + is_initiator: true, + is_splice: true, + feerate: FeeRate::from_sat_per_kwu(2000), + }; + assert_eq!(contribution.validate(), Ok(contribution.value_added.to_signed().unwrap())); } // Net splice-out { let expected_fee = if cfg!(feature = "grind_signatures") { 2526 } else { 2532 }; - assert_eq!( - check_v2_funding_inputs_sufficient( - Amount::from_sat(220_000), - &[ - funding_input_sats(200_000), - funding_input_sats(100_000), - ], - &[ - funding_output_sats(400_000), - ], - true, - true, - 2000, - ).unwrap(), - Amount::from_sat(expected_fee), - ); + let contribution = FundingContribution { + value_added: Amount::from_sat(220_000), + value_removed: Amount::ZERO, + estimated_fee: Amount::from_sat(expected_fee), + inputs: vec![ + funding_input_sats(200_000), + funding_input_sats(100_000), + ], + outputs: vec![ + funding_output_sats(400_000), + ], + change_script: None, + is_initiator: true, + is_splice: true, + feerate: FeeRate::from_sat_per_kwu(2000), + }; + assert_eq!(contribution.validate(), Ok(contribution.value_added.to_signed().unwrap())); } // Net splice-out, inputs insufficient to cover fees { let expected_fee = if cfg!(feature = "grind_signatures") { 113670 } else { 113940 }; + let contribution = FundingContribution { + value_added: Amount::from_sat(220_000), + value_removed: Amount::ZERO, + estimated_fee: Amount::from_sat(expected_fee), + inputs: vec![ + funding_input_sats(200_000), + funding_input_sats(100_000), + ], + outputs: vec![ + funding_output_sats(400_000), + ], + change_script: None, + is_initiator: true, + is_splice: true, + feerate: FeeRate::from_sat_per_kwu(90000), + }; assert_eq!( - check_v2_funding_inputs_sufficient( - Amount::from_sat(220_000), - &[ - funding_input_sats(200_000), - funding_input_sats(100_000), - ], - &[ - funding_output_sats(400_000), - ], - true, - true, - 90000, - ), + contribution.validate(), Err(format!( "Total input amount 0.00300000 BTC is lower than needed for splice-in contribution 0.00220000 BTC, considering fees of {}. Need more inputs.", Amount::from_sat(expected_fee), @@ -702,17 +724,21 @@ mod tests { // negative case, inputs clearly insufficient { let expected_fee = if cfg!(feature = "grind_signatures") { 1736 } else { 1740 }; + let contribution = FundingContribution { + value_added: Amount::from_sat(220_000), + value_removed: Amount::ZERO, + estimated_fee: Amount::from_sat(expected_fee), + inputs: vec![ + funding_input_sats(100_000), + ], + outputs: vec![], + change_script: None, + is_initiator: true, + is_splice: true, + feerate: FeeRate::from_sat_per_kwu(2000), + }; assert_eq!( - check_v2_funding_inputs_sufficient( - Amount::from_sat(220_000), - &[ - funding_input_sats(100_000), - ], - &[], - true, - true, - 2000, - ), + contribution.validate(), Err(format!( "Total input amount 0.00100000 BTC is lower than needed for splice-in contribution 0.00220000 BTC, considering fees of {}. Need more inputs.", Amount::from_sat(expected_fee), @@ -723,37 +749,42 @@ mod tests { // barely covers { let expected_fee = if cfg!(feature = "grind_signatures") { 2278 } else { 2284 }; - assert_eq!( - check_v2_funding_inputs_sufficient( - Amount::from_sat(300_000 - expected_fee - 20), - &[ - funding_input_sats(200_000), - funding_input_sats(100_000), - ], - &[], - true, - true, - 2000, - ).unwrap(), - Amount::from_sat(expected_fee), - ); + let contribution = FundingContribution { + value_added: Amount::from_sat(300_000 - expected_fee - 20), + value_removed: Amount::ZERO, + estimated_fee: Amount::from_sat(expected_fee), + inputs: vec![ + funding_input_sats(200_000), + funding_input_sats(100_000), + ], + outputs: vec![], + change_script: None, + is_initiator: true, + is_splice: true, + feerate: FeeRate::from_sat_per_kwu(2000), + }; + assert_eq!(contribution.validate(), Ok(contribution.value_added.to_signed().unwrap())); } // higher fee rate, does not cover { let expected_fee = if cfg!(feature = "grind_signatures") { 2506 } else { 2513 }; + let contribution = FundingContribution { + value_added: Amount::from_sat(298032), + value_removed: Amount::ZERO, + estimated_fee: Amount::from_sat(expected_fee), + inputs: vec![ + funding_input_sats(200_000), + funding_input_sats(100_000), + ], + outputs: vec![], + change_script: None, + is_initiator: true, + is_splice: true, + feerate: FeeRate::from_sat_per_kwu(2200), + }; assert_eq!( - check_v2_funding_inputs_sufficient( - Amount::from_sat(298032), - &[ - funding_input_sats(200_000), - funding_input_sats(100_000), - ], - &[], - true, - true, - 2200, - ), + contribution.validate(), Err(format!( "Total input amount 0.00300000 BTC is lower than needed for splice-in contribution 0.00298032 BTC, considering fees of {}. Need more inputs.", Amount::from_sat(expected_fee), @@ -764,20 +795,21 @@ mod tests { // barely covers, less fees (no extra weight, not initiator) { let expected_fee = if cfg!(feature = "grind_signatures") { 1084 } else { 1088 }; - assert_eq!( - check_v2_funding_inputs_sufficient( - Amount::from_sat(300_000 - expected_fee - 20), - &[ - funding_input_sats(200_000), - funding_input_sats(100_000), - ], - &[], - false, - false, - 2000, - ).unwrap(), - Amount::from_sat(expected_fee), - ); + let contribution = FundingContribution { + value_added: Amount::from_sat(300_000 - expected_fee - 20), + value_removed: Amount::ZERO, + estimated_fee: Amount::from_sat(expected_fee), + inputs: vec![ + funding_input_sats(200_000), + funding_input_sats(100_000), + ], + outputs: vec![], + change_script: None, + is_initiator: false, + is_splice: false, + feerate: FeeRate::from_sat_per_kwu(2000), + }; + assert_eq!(contribution.validate(), Ok(contribution.value_added.to_signed().unwrap())); } } } diff --git a/lightning/src/ln/splicing_tests.rs b/lightning/src/ln/splicing_tests.rs index 1f037b3dae8..5a755b7ea62 100644 --- a/lightning/src/ln/splicing_tests.rs +++ b/lightning/src/ln/splicing_tests.rs @@ -13,13 +13,13 @@ use crate::chain::chaininterface::FEERATE_FLOOR_SATS_PER_KW; use crate::chain::channelmonitor::{ANTI_REORG_DELAY, LATENCY_GRACE_PERIOD_BLOCKS}; use crate::chain::transaction::OutPoint; use crate::chain::ChannelMonitorUpdateStatus; -use crate::events::bump_transaction::sync::WalletSourceSync; +use crate::events::bump_transaction::sync::{WalletSourceSync, WalletSync}; use crate::events::{ClosureReason, Event, FundingInfo, HTLCHandlingFailureType}; use crate::ln::chan_utils; use crate::ln::channel::CHANNEL_ANNOUNCEMENT_PROPAGATION_DELAY; use crate::ln::channelmanager::{PaymentId, RecipientOnionFields, BREAKDOWN_TIMEOUT}; use crate::ln::functional_test_utils::*; -use crate::ln::funding::{FundingTxInput, SpliceContribution}; +use crate::ln::funding::SpliceContribution; use crate::ln::msgs::{self, BaseMessageHandler, ChannelMessageHandler, MessageSendEvent}; use crate::ln::types::ChannelId; use crate::routing::router::{PaymentParameters, RouteParameters}; @@ -29,7 +29,7 @@ use crate::util::test_channel_signer::SignerOp; use bitcoin::hashes::Hash; use bitcoin::secp256k1::PublicKey; -use bitcoin::{Amount, OutPoint as BitcoinOutPoint, ScriptBuf, Transaction, TxOut, WPubkeyHash}; +use bitcoin::{Amount, FeeRate, OutPoint as BitcoinOutPoint, ScriptBuf, Transaction, TxOut, WPubkeyHash}; #[test] fn test_v1_splice_in_negative_insufficient_inputs() { @@ -38,34 +38,42 @@ fn test_v1_splice_in_negative_insufficient_inputs() { let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + let (_, _, channel_id, _) = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, 0); // Amount being added to the channel through the splice-in - let splice_in_sats = 20_000; + let splice_in_value = Amount::from_sat(20_000); // Create additional inputs, but insufficient - let extra_splice_funding_input_sats = splice_in_sats - 1; - let funding_inputs = - create_dual_funding_utxos_with_prev_txs(&nodes[0], &[extra_splice_funding_input_sats]); + let extra_splice_funding_input = splice_in_value - Amount::ONE_SAT; - let contribution = - SpliceContribution::splice_in(Amount::from_sat(splice_in_sats), funding_inputs, None); + let coinbase_tx = nodes[0].provide_funding_utxos(1, extra_splice_funding_input); + let contribution = SpliceContribution::splice_in(splice_in_value, None); // Initiate splice-in, with insufficient input contribution - let res = nodes[0].node.splice_channel( + nodes[0].node.splice_channel( &channel_id, &nodes[1].node.get_our_node_id(), contribution, - 1024, // funding_feerate_per_kw, - None, // locktime - ); - match res { - Err(APIError::APIMisuseError { err }) => { - assert!(err.contains("Need more inputs")) - }, - _ => panic!("Wrong error {:?}", res.err().unwrap()), - } + ).unwrap(); + + let stfu_init = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1); + nodes[1].node.handle_stfu(node_id_0, &stfu_init); + let stfu_ack = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0); + nodes[0].node.handle_stfu(node_id_1, &stfu_ack); + + let event = get_event!(nodes[0], Event::FundingNeeded); + let funding_template = match event { + Event::FundingNeeded { funding_template, .. } => funding_template, + _ => panic!("Expected Event::FundingNeeded"), + }; + + let wallet = WalletSync::new(nodes[0].wallet_source.clone(), nodes[0].logger); + let feerate = FeeRate::from_sat_per_kwu(1024); + assert!(funding_template.build_sync(&wallet, feerate).is_err()); } pub fn negotiate_splice_tx<'a, 'b, 'c, 'd>( @@ -96,8 +104,6 @@ pub fn complete_splice_handshake<'a, 'b, 'c, 'd>( &channel_id, &node_id_acceptor, initiator_contribution, - FEERATE_FLOOR_SATS_PER_KW, - None, ) .unwrap(); @@ -106,6 +112,8 @@ pub fn complete_splice_handshake<'a, 'b, 'c, 'd>( let stfu_ack = get_event_msg!(acceptor, MessageSendEvent::SendStfu, node_id_initiator); initiator.node.handle_stfu(node_id_acceptor, &stfu_ack); + fund_splice(initiator, acceptor, channel_id); + let splice_init = get_event_msg!(initiator, MessageSendEvent::SendSpliceInit, node_id_acceptor); acceptor.node.handle_splice_init(node_id_initiator, &splice_init); let splice_ack = get_event_msg!(acceptor, MessageSendEvent::SendSpliceAck, node_id_initiator); @@ -120,6 +128,24 @@ pub fn complete_splice_handshake<'a, 'b, 'c, 'd>( new_funding_script } +pub fn fund_splice<'a, 'b, 'c, 'd>( + initiator: &'a Node<'b, 'c, 'd>, acceptor: &'a Node<'b, 'c, 'd>, channel_id: ChannelId, +) { + let node_id_acceptor = acceptor.node.get_our_node_id(); + + let event = get_event!(initiator, Event::FundingNeeded); + let funding_template = match event { + Event::FundingNeeded { funding_template, .. } => funding_template, + _ => panic!("Expected Event::FundingNeeded"), + }; + + let wallet = WalletSync::new(initiator.wallet_source.clone(), initiator.logger); + let feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64); + let funding_contribution = funding_template.build_sync(&wallet, feerate).unwrap(); + let locktime = None; + initiator.node.funding_contributed(&channel_id, &node_id_acceptor, funding_contribution, locktime).unwrap(); +} + pub fn complete_interactive_funding_negotiation<'a, 'b, 'c, 'd>( initiator: &'a Node<'b, 'c, 'd>, acceptor: &'a Node<'b, 'c, 'd>, channel_id: ChannelId, initiator_contribution: SpliceContribution, new_funding_script: ScriptBuf, @@ -136,8 +162,8 @@ pub fn complete_interactive_funding_negotiation<'a, 'b, 'c, 'd>( }) .map(|channel| channel.funding_txo.unwrap()) .unwrap(); - let (initiator_inputs, initiator_outputs, initiator_change_script) = - initiator_contribution.into_tx_parts(); + let initiator_inputs = initiator.wallet_source.utxos(); + let (initiator_outputs, initiator_change_script) = initiator_contribution.into_tx_parts(); let mut expected_initiator_inputs = initiator_inputs .iter() .map(|input| input.utxo.outpoint) @@ -433,8 +459,6 @@ fn do_test_splice_state_reset_on_disconnect(reload: bool) { &channel_id, &node_id_1, contribution.clone(), - FEERATE_FLOOR_SATS_PER_KW, - None, ) .unwrap(); @@ -446,6 +470,8 @@ fn do_test_splice_state_reset_on_disconnect(reload: bool) { let stfu = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0); nodes[0].node.handle_stfu(node_id_1, &stfu); + fund_splice(&nodes[0], &nodes[1], channel_id); + let splice_init = get_event_msg!(nodes[0], MessageSendEvent::SendSpliceInit, node_id_1); nodes[1].node.handle_splice_init(node_id_0, &splice_init); let _ = get_event_msg!(nodes[1], MessageSendEvent::SendSpliceAck, node_id_0); @@ -487,8 +513,6 @@ fn do_test_splice_state_reset_on_disconnect(reload: bool) { &channel_id, &node_id_1, contribution.clone(), - FEERATE_FLOOR_SATS_PER_KW, - None, ) .unwrap(); @@ -500,6 +524,8 @@ fn do_test_splice_state_reset_on_disconnect(reload: bool) { let stfu = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0); nodes[0].node.handle_stfu(node_id_1, &stfu); + fund_splice(&nodes[0], &nodes[1], channel_id); + let splice_init = get_event_msg!(nodes[0], MessageSendEvent::SendSpliceInit, node_id_1); nodes[1].node.handle_splice_init(node_id_0, &splice_init); let splice_ack = get_event_msg!(nodes[1], MessageSendEvent::SendSpliceAck, node_id_0); @@ -546,8 +572,6 @@ fn do_test_splice_state_reset_on_disconnect(reload: bool) { &channel_id, &node_id_1, contribution.clone(), - FEERATE_FLOOR_SATS_PER_KW, - None, ) .unwrap(); @@ -560,6 +584,8 @@ fn do_test_splice_state_reset_on_disconnect(reload: bool) { let stfu = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0); nodes[0].node.handle_stfu(node_id_1, &stfu); + fund_splice(&nodes[0], &nodes[1], channel_id); + let splice_init = get_event_msg!(nodes[0], MessageSendEvent::SendSpliceInit, node_id_1); nodes[1].node.handle_splice_init(node_id_0, &splice_init); let splice_ack = get_event_msg!(nodes[1], MessageSendEvent::SendSpliceAck, node_id_0); @@ -689,8 +715,6 @@ fn test_config_reject_inbound_splices() { &channel_id, &node_id_1, contribution.clone(), - FEERATE_FLOOR_SATS_PER_KW, - None, ) .unwrap(); @@ -699,6 +723,8 @@ fn test_config_reject_inbound_splices() { let stfu = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0); nodes[0].node.handle_stfu(node_id_1, &stfu); + fund_splice(&nodes[0], &nodes[1], channel_id); + let splice_init = get_event_msg!(nodes[0], MessageSendEvent::SendSpliceInit, node_id_1); nodes[1].node.handle_splice_init(node_id_0, &splice_init); @@ -738,24 +764,21 @@ fn test_splice_in() { let _ = send_payment(&nodes[0], &[&nodes[1]], 100_000); - let coinbase_tx1 = provide_anchor_reserves(&nodes); - let coinbase_tx2 = provide_anchor_reserves(&nodes); - let added_value = Amount::from_sat(initial_channel_value_sat * 2); + let utxo_value = added_value * 3 / 4; let change_script = ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros()); let fees = Amount::from_sat(321); + let coinbase_tx = nodes[0].provide_funding_utxos(2, utxo_value); + let _coinbase_tx = nodes[1].provide_funding_utxos(1, Amount::ONE_BTC); + let initiator_contribution = SpliceContribution::splice_in( added_value, - vec![ - FundingTxInput::new_p2wpkh(coinbase_tx1, 0).unwrap(), - FundingTxInput::new_p2wpkh(coinbase_tx2, 0).unwrap(), - ], Some(change_script.clone()), ); let splice_tx = splice_channel(&nodes[0], &nodes[1], channel_id, initiator_contribution); - let expected_change = Amount::ONE_BTC * 2 - added_value - fees; + let expected_change = utxo_value * 2 - added_value - fees; assert_eq!( splice_tx.output.iter().find(|txout| txout.script_pubkey == change_script).unwrap().value, expected_change, @@ -831,14 +854,12 @@ fn test_splice_in_and_out() { let _ = send_payment(&nodes[0], &[&nodes[1]], 100_000); - let coinbase_tx1 = provide_anchor_reserves(&nodes); - let coinbase_tx2 = provide_anchor_reserves(&nodes); - // Contribute a net negative value, with fees taken from the contributed inputs and the // remaining value sent to change let htlc_limit_msat = nodes[0].node.list_channels()[0].next_outbound_htlc_limit_msat; let added_value = Amount::from_sat(htlc_limit_msat / 1000); let removed_value = added_value * 2; + let utxo_value = added_value * 3 / 4; let change_script = ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros()); let fees = if cfg!(feature = "grind_signatures") { Amount::from_sat(383) @@ -848,12 +869,11 @@ fn test_splice_in_and_out() { assert!(htlc_limit_msat > initial_channel_value_sat / 2 * 1000); + let coinbase_tx = nodes[0].provide_funding_utxos(2, utxo_value); + let _coinbase_tx = nodes[1].provide_funding_utxos(1, Amount::ONE_BTC); + let initiator_contribution = SpliceContribution::splice_in_and_out( added_value, - vec![ - FundingTxInput::new_p2wpkh(coinbase_tx1, 0).unwrap(), - FundingTxInput::new_p2wpkh(coinbase_tx2, 0).unwrap(), - ], vec![ TxOut { value: removed_value / 2, @@ -868,7 +888,7 @@ fn test_splice_in_and_out() { ); let splice_tx = splice_channel(&nodes[0], &nodes[1], channel_id, initiator_contribution); - let expected_change = Amount::ONE_BTC * 2 - added_value - fees; + let expected_change = utxo_value * 2 - added_value - fees; assert_eq!( splice_tx.output.iter().find(|txout| txout.script_pubkey == change_script).unwrap().value, expected_change, @@ -887,13 +907,13 @@ fn test_splice_in_and_out() { assert!(htlc_limit_msat < added_value.to_sat() * 1000); let _ = send_payment(&nodes[0], &[&nodes[1]], htlc_limit_msat); - let coinbase_tx1 = provide_anchor_reserves(&nodes); - let coinbase_tx2 = provide_anchor_reserves(&nodes); + nodes[0].wallet_source.clear_utxos(); // Contribute a net positive value, with fees taken from the contributed inputs and the // remaining value sent to change let added_value = Amount::from_sat(initial_channel_value_sat * 2); let removed_value = added_value / 2; + let utxo_value = added_value * 3 / 4; let change_script = ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros()); let fees = if cfg!(feature = "grind_signatures") { Amount::from_sat(383) @@ -901,12 +921,11 @@ fn test_splice_in_and_out() { Amount::from_sat(384) }; + let coinbase_tx = nodes[0].provide_funding_utxos(2, utxo_value); + let _coinbase_tx = nodes[1].provide_funding_utxos(1, Amount::ONE_BTC); + let initiator_contribution = SpliceContribution::splice_in_and_out( added_value, - vec![ - FundingTxInput::new_p2wpkh(coinbase_tx1, 0).unwrap(), - FundingTxInput::new_p2wpkh(coinbase_tx2, 0).unwrap(), - ], vec![ TxOut { value: removed_value / 2, @@ -921,7 +940,7 @@ fn test_splice_in_and_out() { ); let splice_tx = splice_channel(&nodes[0], &nodes[1], channel_id, initiator_contribution); - let expected_change = Amount::ONE_BTC * 2 - added_value - fees; + let expected_change = utxo_value * 2 - added_value - fees; assert_eq!( splice_tx.output.iter().find(|txout| txout.script_pubkey == change_script).unwrap().value, expected_change, @@ -939,20 +958,16 @@ fn test_splice_in_and_out() { assert!(htlc_limit_msat > initial_channel_value_sat / 2 * 1000); let _ = send_payment(&nodes[0], &[&nodes[1]], htlc_limit_msat); - let coinbase_tx1 = provide_anchor_reserves(&nodes); - let coinbase_tx2 = provide_anchor_reserves(&nodes); + nodes[0].wallet_source.clear_utxos(); // Fail adding a net contribution value of zero let added_value = Amount::from_sat(initial_channel_value_sat * 2); let removed_value = added_value; let change_script = ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros()); + let coinbase_tx = nodes[0].provide_funding_utxos(2, Amount::ONE_BTC); let initiator_contribution = SpliceContribution::splice_in_and_out( added_value, - vec![ - FundingTxInput::new_p2wpkh(coinbase_tx1, 0).unwrap(), - FundingTxInput::new_p2wpkh(coinbase_tx2, 0).unwrap(), - ], vec![ TxOut { value: removed_value / 2, @@ -971,8 +986,6 @@ fn test_splice_in_and_out() { &channel_id, &nodes[1].node.get_our_node_id(), initiator_contribution, - FEERATE_FLOOR_SATS_PER_KW, - None, ), Err(APIError::APIMisuseError { err: format!("Channel {} cannot be spliced; contribution cannot be zero", channel_id), @@ -1014,16 +1027,17 @@ fn do_test_splice_commitment_broadcast(splice_status: SpliceStatus, claim_htlcs: let (_, _, channel_id, initial_funding_tx) = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_capacity, 0); - let coinbase_tx = provide_anchor_reserves(&nodes); + let coinbase_tx = nodes[0].provide_funding_utxos(1, Amount::ONE_BTC); + let _coinbase_tx = nodes[1].provide_funding_utxos(1, Amount::ONE_BTC); // We want to have two HTLCs pending to make sure we can claim those sent before and after a // splice negotiation. let payment_amount = 1_000_000; let (preimage1, payment_hash1, ..) = route_payment(&nodes[0], &[&nodes[1]], payment_amount); + let splice_in_amount = initial_channel_capacity / 2; let initiator_contribution = SpliceContribution::splice_in( Amount::from_sat(splice_in_amount), - vec![FundingTxInput::new_p2wpkh(coinbase_tx.clone(), 0).unwrap()], Some(nodes[0].wallet_source.get_change_script().unwrap()), ); let splice_tx = splice_channel(&nodes[0], &nodes[1], channel_id, initiator_contribution); @@ -1515,8 +1529,6 @@ fn do_test_propose_splice_while_disconnected(reload: bool, use_0conf: bool) { &channel_id, &node_id_1, node_0_contribution.clone(), - FEERATE_FLOOR_SATS_PER_KW, - None, ) .unwrap(); assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty()); @@ -1531,8 +1543,6 @@ fn do_test_propose_splice_while_disconnected(reload: bool, use_0conf: bool) { &channel_id, &node_id_0, node_1_contribution.clone(), - FEERATE_FLOOR_SATS_PER_KW, - None, ) .unwrap(); assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); @@ -1567,6 +1577,9 @@ fn do_test_propose_splice_while_disconnected(reload: bool, use_0conf: bool) { } reconnect_args.send_stfu = (true, true); reconnect_nodes(reconnect_args); + + fund_splice(&nodes[0], &nodes[1], channel_id); + let splice_init = get_event_msg!(nodes[0], MessageSendEvent::SendSpliceInit, node_id_1); assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); @@ -1719,6 +1732,8 @@ fn do_test_propose_splice_while_disconnected(reload: bool, use_0conf: bool) { panic!("Unexpected event {:?}", &msg_events[0]); } + fund_splice(&nodes[1], &nodes[0], channel_id); + let splice_init = get_event_msg!(nodes[1], MessageSendEvent::SendSpliceInit, node_id_0); let initial_commit_sig = { nodes[0].node.handle_splice_init(node_id_1, &splice_init); @@ -1775,11 +1790,10 @@ fn disconnect_on_unexpected_interactive_tx_message() { let (_, _, channel_id, _) = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_capacity, 0); - let coinbase_tx = provide_anchor_reserves(&nodes); + let coinbase_tx = initiator.provide_funding_utxos(1, Amount::ONE_BTC); let splice_in_amount = initial_channel_capacity / 2; let contribution = SpliceContribution::splice_in( Amount::from_sat(splice_in_amount), - vec![FundingTxInput::new_p2wpkh(coinbase_tx, 0).unwrap()], Some(nodes[0].wallet_source.get_change_script().unwrap()), ); @@ -1815,11 +1829,10 @@ fn fail_splice_on_interactive_tx_error() { let (_, _, channel_id, _) = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_capacity, 0); - let coinbase_tx = provide_anchor_reserves(&nodes); + let coinbase_tx = initiator.provide_funding_utxos(1, Amount::ONE_BTC); let splice_in_amount = initial_channel_capacity / 2; let contribution = SpliceContribution::splice_in( Amount::from_sat(splice_in_amount), - vec![FundingTxInput::new_p2wpkh(coinbase_tx, 0).unwrap()], Some(nodes[0].wallet_source.get_change_script().unwrap()), ); @@ -1839,7 +1852,7 @@ fn fail_splice_on_interactive_tx_error() { match event { Event::SpliceFailed { contributed_inputs, .. } => { assert_eq!(contributed_inputs.len(), 1); - assert_eq!(contributed_inputs[0], contribution.inputs()[0].outpoint()); + assert_eq!(contributed_inputs[0], nodes[0].wallet_source.utxos()[0].outpoint()); }, _ => panic!("Expected Event::SpliceFailed"), } @@ -1891,7 +1904,7 @@ fn fail_splice_on_interactive_tx_error() { match event { Event::SpliceFailed { contributed_inputs, .. } => { assert_eq!(contributed_inputs.len(), 1); - assert_eq!(contributed_inputs[0], contribution.inputs()[0].outpoint()); + assert_eq!(contributed_inputs[0], initiator.wallet_source.utxos()[0].outpoint()); }, _ => panic!("Expected Event::SpliceFailed"), } @@ -1921,11 +1934,10 @@ fn fail_splice_on_tx_abort() { let (_, _, channel_id, _) = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_capacity, 0); - let coinbase_tx = provide_anchor_reserves(&nodes); + let coinbase_tx = initiator.provide_funding_utxos(1, Amount::ONE_BTC); let splice_in_amount = initial_channel_capacity / 2; let contribution = SpliceContribution::splice_in( Amount::from_sat(splice_in_amount), - vec![FundingTxInput::new_p2wpkh(coinbase_tx, 0).unwrap()], Some(nodes[0].wallet_source.get_change_script().unwrap()), ); @@ -1948,7 +1960,7 @@ fn fail_splice_on_tx_abort() { match event { Event::SpliceFailed { contributed_inputs, .. } => { assert_eq!(contributed_inputs.len(), 1); - assert_eq!(contributed_inputs[0], contribution.inputs()[0].outpoint()); + assert_eq!(contributed_inputs[0], initiator.wallet_source.utxos()[0].outpoint()); }, _ => panic!("Expected Event::SpliceFailed"), } @@ -1975,11 +1987,10 @@ fn fail_splice_on_channel_close() { let (_, _, channel_id, _) = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_capacity, 0); - let coinbase_tx = provide_anchor_reserves(&nodes); + let coinbase_tx = initiator.provide_funding_utxos(1, Amount::ONE_BTC); let splice_in_amount = initial_channel_capacity / 2; let contribution = SpliceContribution::splice_in( Amount::from_sat(splice_in_amount), - vec![FundingTxInput::new_p2wpkh(coinbase_tx, 0).unwrap()], Some(nodes[0].wallet_source.get_change_script().unwrap()), ); @@ -2026,11 +2037,10 @@ fn fail_quiescent_action_on_channel_close() { let (_, _, channel_id, _) = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_capacity, 0); - let coinbase_tx = provide_anchor_reserves(&nodes); let splice_in_amount = initial_channel_capacity / 2; let contribution = SpliceContribution::splice_in( Amount::from_sat(splice_in_amount), - vec![FundingTxInput::new_p2wpkh(coinbase_tx, 0).unwrap()], + //vec![FundingTxInput::new_p2wpkh(coinbase_tx, 0).unwrap()], Some(nodes[0].wallet_source.get_change_script().unwrap()), ); @@ -2041,8 +2051,6 @@ fn fail_quiescent_action_on_channel_close() { &channel_id, &node_id_acceptor, contribution, - FEERATE_FLOOR_SATS_PER_KW, - None, ) .unwrap(); diff --git a/lightning/src/util/test_utils.rs b/lightning/src/util/test_utils.rs index 81f88c38d98..d78162b1ab4 100644 --- a/lightning/src/util/test_utils.rs +++ b/lightning/src/util/test_utils.rs @@ -2232,6 +2232,10 @@ impl TestWalletSource { self.utxos.lock().unwrap().clear(); } + pub fn utxos(&self) -> Vec { + self.utxos.lock().unwrap().clone() + } + pub fn sign_tx( &self, mut tx: Transaction, ) -> Result {