From 749ebb8ef909173790e7627d3596302e29738156 Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Thu, 4 Sep 2025 10:09:24 -0700 Subject: [PATCH 1/3] Switch TestWalletSource to use P2WPKH script We plan to reuse it for dual-funding/splicing, and those require standard SegWit inputs only. --- lightning/src/util/test_utils.rs | 55 +++++++++++++++++--------------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/lightning/src/util/test_utils.rs b/lightning/src/util/test_utils.rs index d28d0abbc32..50ae8f6ddae 100644 --- a/lightning/src/util/test_utils.rs +++ b/lightning/src/util/test_utils.rs @@ -68,10 +68,10 @@ use bitcoin::constants::ChainHash; use bitcoin::hash_types::{BlockHash, Txid}; use bitcoin::hashes::Hash; use bitcoin::network::Network; -use bitcoin::opcodes; use bitcoin::script::{Builder, Script, ScriptBuf}; use bitcoin::sighash::{EcdsaSighashType, SighashCache}; use bitcoin::transaction::{Transaction, TxOut}; +use bitcoin::{opcodes, Witness}; use bitcoin::secp256k1::ecdh::SharedSecret; use bitcoin::secp256k1::ecdsa::{RecoverableSignature, Signature}; @@ -2001,7 +2001,7 @@ impl TestWalletSource { 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_p2pkh(outpoint, value, &public_key.pubkey_hash()); + let utxo = Utxo::new_v0_p2wpkh(outpoint, value, &public_key.wpubkey_hash().unwrap()); self.utxos.lock().unwrap().push(utxo.clone()); utxo.output } @@ -2015,44 +2015,47 @@ impl TestWalletSource { pub fn remove_utxo(&self, outpoint: bitcoin::OutPoint) { self.utxos.lock().unwrap().retain(|utxo| utxo.outpoint != outpoint); } -} - -impl WalletSourceSync for TestWalletSource { - fn list_confirmed_utxos(&self) -> Result, ()> { - Ok(self.utxos.lock().unwrap().clone()) - } - fn get_change_script(&self) -> Result { - let public_key = bitcoin::PublicKey::new(self.secret_key.public_key(&self.secp)); - Ok(ScriptBuf::new_p2pkh(&public_key.pubkey_hash())) - } - - fn sign_psbt(&self, psbt: Psbt) -> Result { - let mut tx = psbt.extract_tx_unchecked_fee_rate(); + pub fn sign_tx( + &self, mut tx: Transaction, + ) -> 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) { - let sighash = SighashCache::new(&tx) - .legacy_signature_hash( - i, - &utxo.output.script_pubkey, - EcdsaSighashType::All as u32, - ) - .map_err(|_| ())?; + let sighash = SighashCache::new(&tx).p2wpkh_signature_hash( + i, + &utxo.output.script_pubkey, + utxo.output.value, + EcdsaSighashType::All, + )?; let signature = self.secp.sign_ecdsa( &secp256k1::Message::from_digest(sighash.to_byte_array()), &self.secret_key, ); let bitcoin_sig = bitcoin::ecdsa::Signature { signature, sighash_type: EcdsaSighashType::All }; - tx.input[i].script_sig = Builder::new() - .push_slice(&bitcoin_sig.serialize()) - .push_slice(&self.secret_key.public_key(&self.secp).serialize()) - .into_script(); + tx.input[i].witness = + Witness::p2wpkh(&bitcoin_sig, &self.secret_key.public_key(&self.secp)); } } Ok(tx) } } + +impl WalletSourceSync for TestWalletSource { + fn list_confirmed_utxos(&self) -> Result, ()> { + Ok(self.utxos.lock().unwrap().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())) + } + + fn sign_psbt(&self, psbt: Psbt) -> Result { + let tx = psbt.extract_tx_unchecked_fee_rate(); + self.sign_tx(tx).map_err(|_| ()) + } +} From 8ab08840d171c912c67e5b37ad03a5aa7b0e8c57 Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Thu, 4 Sep 2025 10:09:25 -0700 Subject: [PATCH 2/3] Set current block locktime for coinbase in provide_anchor_reserves This guarantees we get a unique txid when calling `provide_anchor_reserves` successively as we're immediately mining (and therefore advancing the chain) the transaction after creating it. --- lightning/src/ln/functional_test_utils.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index 2bc8049c0d4..bd5d3539c35 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -409,7 +409,7 @@ pub fn provide_anchor_reserves<'a, 'b, 'c>(nodes: &[Node<'a, 'b, 'c>]) -> Transa } let tx = Transaction { version: TxVersion::TWO, - lock_time: LockTime::ZERO, + lock_time: LockTime::from_height(nodes[0].best_block_info().1).unwrap(), input: vec![TxIn { ..Default::default() }], output, }; From 307fa5617f5587b2a5d89d41f504a598d8897d1c Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Thu, 4 Sep 2025 10:09:25 -0700 Subject: [PATCH 3/3] Add basic end-to-end splice tests This adds a new test for both splice-in and splice-out in favor of maintaining the existing test. Helpers have been added to DRY up a lot of the logic necessary for driving the splice state machine forward. --- lightning/src/ln/channel.rs | 2 - lightning/src/ln/funding.rs | 3 +- lightning/src/ln/splicing_tests.rs | 577 ++++++++++++++++------------- 3 files changed, 331 insertions(+), 251 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index de894de8088..22fce2561db 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -6130,8 +6130,6 @@ where holder_commitment_transaction_number, self.counterparty_next_commitment_transaction_number, ); - // TODO(splicing) Forced error, as the use case is not complete - return Err(AbortReason::InternalError("Splicing not yet supported")); } else { self.assert_no_commitment_advancement(holder_commitment_transaction_number, "initial commitment_signed"); self.channel_state = ChannelState::FundingNegotiated(FundingNegotiatedFlags::new()); diff --git a/lightning/src/ln/funding.rs b/lightning/src/ln/funding.rs index b0b8cd4aa1c..d0657a41c57 100644 --- a/lightning/src/ln/funding.rs +++ b/lightning/src/ln/funding.rs @@ -17,6 +17,7 @@ use crate::prelude::Vec; 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 { @@ -85,7 +86,7 @@ impl SpliceContribution { /// An input to contribute to a channel's funding transaction either when using the v2 channel /// establishment protocol or when splicing. -#[derive(Clone)] +#[derive(Debug, Clone)] pub struct FundingTxInput { /// The unspent [`TxOut`] that the input spends. /// diff --git a/lightning/src/ln/splicing_tests.rs b/lightning/src/ln/splicing_tests.rs index 0a5a5a27422..65461f2264c 100644 --- a/lightning/src/ln/splicing_tests.rs +++ b/lightning/src/ln/splicing_tests.rs @@ -7,70 +7,36 @@ // You may not use this file except in accordance with one or both of these // licenses. +use crate::chain::chaininterface::FEERATE_FLOOR_SATS_PER_KW; +use crate::chain::channelmonitor::ANTI_REORG_DELAY; +use crate::events::bump_transaction::sync::WalletSourceSync; +use crate::events::Event; +use crate::ln::chan_utils; use crate::ln::functional_test_utils::*; -use crate::ln::funding::SpliceContribution; -use crate::ln::msgs::{BaseMessageHandler, ChannelMessageHandler, MessageSendEvent}; +use crate::ln::funding::{FundingTxInput, SpliceContribution}; +use crate::ln::msgs::{self, BaseMessageHandler, ChannelMessageHandler, MessageSendEvent}; +use crate::ln::types::ChannelId; use crate::util::errors::APIError; -use bitcoin::Amount; +use bitcoin::{Amount, OutPoint as BitcoinOutPoint, ScriptBuf, Transaction, TxOut}; -/// Splicing test, simple splice-in flow. Starts with opening a V1 channel first. -/// Builds on test_channel_open_simple() #[test] -fn test_v1_splice_in() { +fn test_v1_splice_in_negative_insufficient_inputs() { let chanmon_cfgs = create_chanmon_cfgs(2); let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); - // Initiator and Acceptor nodes - let initiator_node_index = 0; - let acceptor_node_index = 1; - let initiator_node = &nodes[initiator_node_index]; - let acceptor_node = &nodes[acceptor_node_index]; - let initiator_node_id = initiator_node.node.get_our_node_id(); - let acceptor_node_id = acceptor_node.node.get_our_node_id(); - - let channel_value_sat = 100_000; - let channel_reserve_amnt_sat = 1_000; - let expect_outputs_in_reverse = true; - - let (_, _, channel_id, _) = create_announced_chan_between_nodes_with_value( - &nodes, - initiator_node_index, - acceptor_node_index, - channel_value_sat, - 0, // push_msat, - ); - - let expected_funded_channel_id = - "ae3367da2c13bc1ceb86bf56418f62828f7ce9d6bfb15a46af5ba1f1ed8b124f"; - assert_eq!(channel_id.to_string(), expected_funded_channel_id); - - let expected_initiator_funding_key = - "020abf01c18d5a2543124a12150d698ebf3a8e17df9993521151a49e115678ceea"; - let expected_acceptor_funding_key = - "036b47248c628fca98159f30f6b03a6cf0be0c4808cff17c75dc855fe94a244766"; - - // ==== Channel is now ready for normal operation - - // Expected balances - let exp_balance1 = 1000 * channel_value_sat; - let mut _exp_balance2 = 0; - - // === Start of Splicing + 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 post_splice_channel_value = channel_value_sat + splice_in_sats; - let funding_feerate_per_kw = 1024; - - // Create additional inputs - let extra_splice_funding_input_sats = 35_000; - let funding_inputs = create_dual_funding_utxos_with_prev_txs( - &initiator_node, - &[extra_splice_funding_input_sats], - ); + + // 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 contribution = SpliceContribution::SpliceIn { value: Amount::from_sat(splice_in_sats), @@ -78,239 +44,354 @@ fn test_v1_splice_in() { change_script: None, }; - // Initiate splice-in - initiator_node - .node - .splice_channel( - &channel_id, - &acceptor_node.node.get_our_node_id(), - contribution, - funding_feerate_per_kw, - None, // locktime - ) - .unwrap(); - - let init_stfu = get_event_msg!(initiator_node, MessageSendEvent::SendStfu, acceptor_node_id); - acceptor_node.node.handle_stfu(initiator_node_id, &init_stfu); - - let ack_stfu = get_event_msg!(acceptor_node, MessageSendEvent::SendStfu, initiator_node_id); - initiator_node.node.handle_stfu(acceptor_node_id, &ack_stfu); - - // Extract the splice_init message - let splice_init_msg = - get_event_msg!(initiator_node, MessageSendEvent::SendSpliceInit, acceptor_node_id); - assert_eq!(splice_init_msg.funding_contribution_satoshis, splice_in_sats as i64); - assert_eq!(splice_init_msg.funding_feerate_per_kw, funding_feerate_per_kw); - assert_eq!(splice_init_msg.funding_pubkey.to_string(), expected_initiator_funding_key); - assert!(splice_init_msg.require_confirmed_inputs.is_none()); - - acceptor_node.node.handle_splice_init(initiator_node.node.get_our_node_id(), &splice_init_msg); - // Extract the splice_ack message - let splice_ack_msg = get_event_msg!( - acceptor_node, - MessageSendEvent::SendSpliceAck, - initiator_node.node.get_our_node_id() + // Initiate splice-in, with insufficient input contribution + let res = nodes[0].node.splice_channel( + &channel_id, + &nodes[1].node.get_our_node_id(), + contribution, + 1024, // funding_feerate_per_kw, + None, // locktime ); - assert_eq!(splice_ack_msg.funding_contribution_satoshis, 0); - assert_eq!(splice_ack_msg.funding_pubkey.to_string(), expected_acceptor_funding_key); - assert!(splice_ack_msg.require_confirmed_inputs.is_none()); + match res { + Err(APIError::APIMisuseError { err }) => { + assert!(err.contains("Need more inputs")) + }, + _ => panic!("Wrong error {:?}", res.err().unwrap()), + } +} - // still pre-splice channel: capacity not updated, channel usable, and funding tx set - assert_eq!(acceptor_node.node.list_channels().len(), 1); - { - let channel = &acceptor_node.node.list_channels()[0]; - assert_eq!(channel.channel_id.to_string(), expected_funded_channel_id); - assert!(channel.is_usable); - assert!(channel.is_channel_ready); - assert_eq!(channel.channel_value_satoshis, channel_value_sat); - assert_eq!(channel.outbound_capacity_msat, 0); - assert!(channel.funding_txo.is_some()); - assert!(channel.confirmations.unwrap() > 0); +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, +) -> msgs::CommitmentSigned { + let node_id_initiator = initiator.node.get_our_node_id(); + let node_id_acceptor = acceptor.node.get_our_node_id(); + + let funding_outpoint = initiator + .node + .list_channels() + .iter() + .find(|channel| { + channel.counterparty.node_id == node_id_acceptor && channel.channel_id == channel_id + }) + .map(|channel| channel.funding_txo.unwrap()) + .unwrap(); + let (initiator_inputs, initiator_outputs, initiator_change_script) = + initiator_contribution.into_tx_parts(); + let mut expected_initiator_inputs = initiator_inputs + .iter() + .map(|input| input.utxo.outpoint) + .chain(core::iter::once(funding_outpoint.into_bitcoin_outpoint())) + .collect::>(); + let mut expected_initiator_scripts = initiator_outputs + .into_iter() + .map(|output| output.script_pubkey) + .chain(core::iter::once(new_funding_script)) + .chain(initiator_change_script.into_iter()) + .collect::>(); + + let mut acceptor_sent_tx_complete = false; + loop { + if !expected_initiator_inputs.is_empty() { + let tx_add_input = + get_event_msg!(initiator, MessageSendEvent::SendTxAddInput, node_id_acceptor); + let input_prevout = BitcoinOutPoint { + txid: tx_add_input + .prevtx + .as_ref() + .map(|prevtx| prevtx.compute_txid()) + .or(tx_add_input.shared_input_txid) + .unwrap(), + vout: tx_add_input.prevtx_out, + }; + expected_initiator_inputs.remove( + expected_initiator_inputs.iter().position(|input| *input == input_prevout).unwrap(), + ); + acceptor.node.handle_tx_add_input(node_id_initiator, &tx_add_input); + } else if !expected_initiator_scripts.is_empty() { + let tx_add_output = + get_event_msg!(initiator, MessageSendEvent::SendTxAddOutput, node_id_acceptor); + expected_initiator_scripts.remove( + expected_initiator_scripts + .iter() + .position(|script| *script == tx_add_output.script) + .unwrap(), + ); + acceptor.node.handle_tx_add_output(node_id_initiator, &tx_add_output); + } else { + let mut msg_events = initiator.node.get_and_clear_pending_msg_events(); + assert_eq!( + msg_events.len(), + if acceptor_sent_tx_complete { 2 } else { 1 }, + "{msg_events:?}" + ); + if let MessageSendEvent::SendTxComplete { ref msg, .. } = msg_events.remove(0) { + acceptor.node.handle_tx_complete(node_id_initiator, msg); + } else { + panic!(); + } + if acceptor_sent_tx_complete { + if let MessageSendEvent::UpdateHTLCs { mut updates, .. } = msg_events.remove(0) { + return updates.commitment_signed.remove(0); + } + panic!(); + } + } + + let mut msg_events = acceptor.node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 1, "{msg_events:?}"); + if let MessageSendEvent::SendTxComplete { ref msg, .. } = msg_events.remove(0) { + initiator.node.handle_tx_complete(node_id_acceptor, msg); + } else { + panic!(); + } + acceptor_sent_tx_complete = true; } +} - initiator_node.node.handle_splice_ack(acceptor_node.node.get_our_node_id(), &splice_ack_msg); +fn sign_interactive_funding_transaction<'a, 'b, 'c, 'd>( + initiator: &'a Node<'b, 'c, 'd>, acceptor: &'a Node<'b, 'c, 'd>, + initial_commit_sig_for_acceptor: msgs::CommitmentSigned, +) { + let node_id_initiator = initiator.node.get_our_node_id(); + let node_id_acceptor = acceptor.node.get_our_node_id(); + + assert!(initiator.node.get_and_clear_pending_msg_events().is_empty()); + acceptor.node.handle_commitment_signed(node_id_initiator, &initial_commit_sig_for_acceptor); + + let mut msg_events = acceptor.node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 2, "{msg_events:?}"); + if let MessageSendEvent::UpdateHTLCs { mut updates, .. } = msg_events.remove(0) { + let commitment_signed = updates.commitment_signed.remove(0); + initiator.node.handle_commitment_signed(node_id_acceptor, &commitment_signed); + } else { + panic!(); + } + if let MessageSendEvent::SendTxSignatures { ref msg, .. } = msg_events.remove(0) { + initiator.node.handle_tx_signatures(node_id_acceptor, msg); + } else { + panic!(); + } - // still pre-splice channel: capacity not updated, channel usable, and funding tx set - assert_eq!(initiator_node.node.list_channels().len(), 1); + let event = get_event!(initiator, Event::FundingTransactionReadyForSigning); + if let Event::FundingTransactionReadyForSigning { + channel_id, + counterparty_node_id, + unsigned_transaction, + .. + } = event { - let channel = &initiator_node.node.list_channels()[0]; - assert_eq!(channel.channel_id.to_string(), expected_funded_channel_id); - assert!(channel.is_usable); - assert!(channel.is_channel_ready); - assert_eq!(channel.channel_value_satoshis, channel_value_sat); - assert_eq!(channel.outbound_capacity_msat, exp_balance1 - 1000 * channel_reserve_amnt_sat); - assert!(channel.funding_txo.is_some()); - assert!(channel.confirmations.unwrap() > 0); + let partially_signed_tx = initiator.wallet_source.sign_tx(unsigned_transaction).unwrap(); + initiator + .node + .funding_transaction_signed(&channel_id, &counterparty_node_id, partially_signed_tx) + .unwrap(); } + let tx_signatures = + get_event_msg!(initiator, MessageSendEvent::SendTxSignatures, node_id_acceptor); + acceptor.node.handle_tx_signatures(node_id_initiator, &tx_signatures); - // exp_balance1 += 1000 * splice_in_sats; // increase in balance + check_added_monitors(&initiator, 1); + check_added_monitors(&acceptor, 1); +} - // Negotiate transaction inputs and outputs +fn splice_channel<'a, 'b, 'c, 'd>( + initiator: &'a Node<'b, 'c, 'd>, acceptor: &'a Node<'b, 'c, 'd>, channel_id: ChannelId, + initiator_contribution: SpliceContribution, +) -> Transaction { + let node_id_initiator = initiator.node.get_our_node_id(); + let node_id_acceptor = acceptor.node.get_our_node_id(); - // First input - let tx_add_input_msg = get_event_msg!( - &initiator_node, - MessageSendEvent::SendTxAddInput, - acceptor_node.node.get_our_node_id() - ); - // check which input is this (order is non-deterministic), based on the presense of prevtx - let inputs_seen_in_reverse = tx_add_input_msg.prevtx.is_some(); - if !inputs_seen_in_reverse { - // Input is the revious funding input - assert_eq!(tx_add_input_msg.prevtx, None); - assert_eq!( - tx_add_input_msg.shared_input_txid.unwrap().to_string(), - "4f128bedf1a15baf465ab1bfd6e97c8f82628f4156bf86eb1cbc132cda6733ae" - ); - } else { - // Input is the extra input - let prevtx_value = tx_add_input_msg.prevtx.as_ref().unwrap().output - [tx_add_input_msg.prevtx_out as usize] - .value - .to_sat(); - assert_eq!(prevtx_value, extra_splice_funding_input_sats); - assert_eq!(tx_add_input_msg.shared_input_txid, None); - } - - acceptor_node + initiator .node - .handle_tx_add_input(initiator_node.node.get_our_node_id(), &tx_add_input_msg); - let tx_complete_msg = get_event_msg!( - acceptor_node, - MessageSendEvent::SendTxComplete, - initiator_node.node.get_our_node_id() - ); + .splice_channel( + &channel_id, + &node_id_acceptor, + initiator_contribution.clone(), + FEERATE_FLOOR_SATS_PER_KW, + None, + ) + .unwrap(); - initiator_node.node.handle_tx_complete(acceptor_node.node.get_our_node_id(), &tx_complete_msg); - // Second input - let tx_add_input2_msg = get_event_msg!( - &initiator_node, - MessageSendEvent::SendTxAddInput, - acceptor_node.node.get_our_node_id() + let stfu_init = get_event_msg!(initiator, MessageSendEvent::SendStfu, node_id_acceptor); + acceptor.node.handle_stfu(node_id_initiator, &stfu_init); + let stfu_ack = get_event_msg!(acceptor, MessageSendEvent::SendStfu, node_id_initiator); + initiator.node.handle_stfu(node_id_acceptor, &stfu_ack); + + 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); + initiator.node.handle_splice_ack(node_id_acceptor, &splice_ack); + + let new_funding_script = chan_utils::make_funding_redeemscript( + &splice_init.funding_pubkey, + &splice_ack.funding_pubkey, + ) + .to_p2wsh(); + + let initial_commit_sig_for_acceptor = complete_interactive_funding_negotiation( + initiator, + acceptor, + channel_id, + initiator_contribution, + new_funding_script, ); - if !inputs_seen_in_reverse { - // Input is the extra input - let prevtx_value = tx_add_input2_msg.prevtx.as_ref().unwrap().output - [tx_add_input2_msg.prevtx_out as usize] - .value - .to_sat(); - assert_eq!(prevtx_value, extra_splice_funding_input_sats); - assert_eq!(tx_add_input2_msg.shared_input_txid, None); - } else { - // Input is the revious funding input - assert_eq!(tx_add_input2_msg.prevtx, None); - assert_eq!( - tx_add_input2_msg.shared_input_txid.unwrap().to_string(), - "4f128bedf1a15baf465ab1bfd6e97c8f82628f4156bf86eb1cbc132cda6733ae" - ); - } + sign_interactive_funding_transaction(initiator, acceptor, initial_commit_sig_for_acceptor); + + let splice_tx = { + let mut initiator_txn = initiator.tx_broadcaster.txn_broadcast(); + assert_eq!(initiator_txn.len(), 1); + let acceptor_txn = acceptor.tx_broadcaster.txn_broadcast(); + assert_eq!(initiator_txn, acceptor_txn); + initiator_txn.remove(0) + }; + splice_tx +} - acceptor_node - .node - .handle_tx_add_input(initiator_node.node.get_our_node_id(), &tx_add_input2_msg); - let tx_complete_msg = get_event_msg!( - acceptor_node, - MessageSendEvent::SendTxComplete, - initiator_node.node.get_our_node_id() - ); +fn lock_splice_after_blocks<'a, 'b, 'c, 'd>( + node_a: &'a Node<'b, 'c, 'd>, node_b: &'a Node<'b, 'c, 'd>, channel_id: ChannelId, + num_blocks: u32, +) { + let (prev_funding_outpoint, prev_funding_script) = node_a + .chain_monitor + .chain_monitor + .get_monitor(channel_id) + .map(|monitor| (monitor.get_funding_txo(), monitor.get_funding_script())) + .unwrap(); - initiator_node.node.handle_tx_complete(acceptor_node.node.get_our_node_id(), &tx_complete_msg); + connect_blocks(node_a, num_blocks); + connect_blocks(node_b, num_blocks); - // TxAddOutput for the change output - let tx_add_output_msg = get_event_msg!( - &initiator_node, - MessageSendEvent::SendTxAddOutput, - acceptor_node.node.get_our_node_id() - ); - if !expect_outputs_in_reverse { - assert!(tx_add_output_msg.script.is_p2wsh()); - assert_eq!(tx_add_output_msg.sats, post_splice_channel_value); + let node_id_a = node_a.node.get_our_node_id(); + let node_id_b = node_b.node.get_our_node_id(); + + let splice_locked_a = get_event_msg!(node_a, MessageSendEvent::SendSpliceLocked, node_id_b); + node_b.node.handle_splice_locked(node_id_a, &splice_locked_a); + + let mut msg_events = node_b.node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 2, "{msg_events:?}"); + if let MessageSendEvent::SendSpliceLocked { msg, .. } = msg_events.remove(0) { + node_a.node.handle_splice_locked(node_id_b, &msg); + } else { + panic!(); + } + if let MessageSendEvent::SendAnnouncementSignatures { msg, .. } = msg_events.remove(0) { + node_a.node.handle_announcement_signatures(node_id_b, &msg); } else { - assert!(tx_add_output_msg.script.is_p2wpkh()); - assert_eq!(tx_add_output_msg.sats, 13979); // extra_splice_funding_input_sats - splice_in_sats + panic!(); } - acceptor_node - .node - .handle_tx_add_output(initiator_node.node.get_our_node_id(), &tx_add_output_msg); - let tx_complete_msg = get_event_msg!( - &acceptor_node, - MessageSendEvent::SendTxComplete, - initiator_node.node.get_our_node_id() - ); + expect_channel_ready_event(&node_a, &node_id_b); + check_added_monitors(&node_a, 1); + expect_channel_ready_event(&node_b, &node_id_a); + check_added_monitors(&node_b, 1); - initiator_node.node.handle_tx_complete(acceptor_node.node.get_our_node_id(), &tx_complete_msg); - // TxAddOutput for the splice funding - let tx_add_output2_msg = get_event_msg!( - &initiator_node, - MessageSendEvent::SendTxAddOutput, - acceptor_node.node.get_our_node_id() - ); - if !expect_outputs_in_reverse { - assert!(tx_add_output2_msg.script.is_p2wpkh()); - assert_eq!(tx_add_output2_msg.sats, 14146); // extra_splice_funding_input_sats - splice_in_sats + let mut msg_events = node_a.node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 2, "{msg_events:?}"); + if let MessageSendEvent::SendAnnouncementSignatures { msg, .. } = msg_events.remove(0) { + node_b.node.handle_announcement_signatures(node_id_a, &msg); } else { - assert!(tx_add_output2_msg.script.is_p2wsh()); - assert_eq!(tx_add_output2_msg.sats, post_splice_channel_value); + panic!(); + } + if let MessageSendEvent::BroadcastChannelAnnouncement { .. } = msg_events.remove(0) { + } else { + panic!(); } - acceptor_node - .node - .handle_tx_add_output(initiator_node.node.get_our_node_id(), &tx_add_output2_msg); - let _tx_complete_msg = get_event_msg!( - acceptor_node, - MessageSendEvent::SendTxComplete, - initiator_node.node.get_our_node_id() - ); - - // TODO(splicing) This is the last tx_complete, which triggers the commitment flow, which is not yet fully implemented - initiator_node.node.handle_tx_complete(acceptor_node.node.get_our_node_id(), &tx_complete_msg); - let events = initiator_node.node.get_and_clear_pending_msg_events(); - assert_eq!(events.len(), 1); - match events[0] { - MessageSendEvent::SendTxAbort { .. } => {}, - _ => panic!("Unexpected event {:?}", events[0]), + let mut msg_events = node_b.node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 1, "{msg_events:?}"); + if let MessageSendEvent::BroadcastChannelAnnouncement { .. } = msg_events.remove(0) { + } else { + panic!(); } - // TODO(splicing): Continue with commitment flow, new tx confirmation, and shutdown + // Remove the corresponding outputs and transactions the chain source is watching. + node_a + .chain_source + .remove_watched_txn_and_outputs(prev_funding_outpoint, prev_funding_script.clone()); + node_b.chain_source.remove_watched_txn_and_outputs(prev_funding_outpoint, prev_funding_script); } #[test] -fn test_v1_splice_in_negative_insufficient_inputs() { +fn test_splice_in() { let chanmon_cfgs = create_chanmon_cfgs(2); let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); - let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + 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, 100_000, 0); + 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); + let initiator_contribution = SpliceContribution::SpliceIn { + value: Amount::from_sat(initial_channel_value_sat * 2), + inputs: 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()), + }; - // Amount being added to the channel through the splice-in - let splice_in_sats = 20_000; + let splice_tx = splice_channel(&nodes[0], &nodes[1], channel_id, initiator_contribution); + mine_transaction(&nodes[0], &splice_tx); + mine_transaction(&nodes[1], &splice_tx); - // 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 htlc_limit_msat = nodes[0].node.list_channels()[0].next_outbound_htlc_limit_msat; + assert!(htlc_limit_msat < initial_channel_value_sat * 1000); + let _ = send_payment(&nodes[0], &[&nodes[1]], htlc_limit_msat); - let contribution = SpliceContribution::SpliceIn { - value: Amount::from_sat(splice_in_sats), - inputs: funding_inputs, - change_script: None, + lock_splice_after_blocks(&nodes[0], &nodes[1], channel_id, 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); + let _ = send_payment(&nodes[0], &[&nodes[1]], htlc_limit_msat); +} + +#[test] +fn test_splice_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 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(), + }, + ], }; - // Initiate splice-in, with insufficient input contribution - let res = 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()), - } + let splice_tx = splice_channel(&nodes[0], &nodes[1], channel_id, initiator_contribution); + 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 < initial_channel_value_sat / 2 * 1000); + let _ = send_payment(&nodes[0], &[&nodes[1]], htlc_limit_msat); + + lock_splice_after_blocks(&nodes[0], &nodes[1], channel_id, 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); }