diff --git a/fuzz/src/chanmon_consistency.rs b/fuzz/src/chanmon_consistency.rs index ba3fc9077e8..5a3d82935e7 100644 --- a/fuzz/src/chanmon_consistency.rs +++ b/fuzz/src/chanmon_consistency.rs @@ -18,6 +18,7 @@ //! send-side handling is correct, other peers. We consider it a failure if any action results in a //! channel being force-closed. +use bitcoin::FeeRate; use bitcoin::amount::Amount; use bitcoin::constants::genesis_block; use bitcoin::locktime::absolute::LockTime; @@ -43,6 +44,7 @@ use lightning::chain::{ chainmonitor, channelmonitor, BestBlock, ChannelMonitorUpdateStatus, Confirm, Watch, }; use lightning::events; +use lightning::events::bump_transaction::sync::{WalletSourceSync, WalletSync}; use lightning::ln::channel::{ FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE, MAX_STD_OUTPUT_DUST_LIMIT_SATOSHIS, }; @@ -52,7 +54,7 @@ use lightning::ln::channelmanager::{ RecipientOnionFields, }; use lightning::ln::functional_test_utils::*; -use lightning::ln::funding::{FundingTxInput, SpliceContribution}; +use lightning::ln::funding::SpliceContribution; use lightning::ln::inbound_payment::ExpandedKey; use lightning::ln::msgs::{ BaseMessageHandler, ChannelMessageHandler, CommitmentUpdate, Init, MessageSendEvent, @@ -70,12 +72,14 @@ use lightning::sign::{ SignerProvider, }; use lightning::types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; +use lightning::util::async_poll::{MaybeSend, MaybeSync}; use lightning::util::config::UserConfig; use lightning::util::errors::APIError; use lightning::util::hash_tables::*; use lightning::util::logger::Logger; use lightning::util::ser::{LengthReadable, ReadableArgs, Writeable, Writer}; use lightning::util::test_channel_signer::{EnforcementState, TestChannelSigner}; +use lightning::util::test_utils::TestWalletSource; use lightning_invoice::RawBolt11Invoice; @@ -168,63 +172,6 @@ impl Writer for VecWriter { } } -pub struct TestWallet { - secret_key: SecretKey, - utxos: Mutex>, - secp: Secp256k1, -} - -impl TestWallet { - pub fn new(secret_key: SecretKey) -> Self { - Self { secret_key, utxos: Mutex::new(Vec::new()), secp: Secp256k1::new() } - } - - 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())) - } - - 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 = lightning::events::bump_transaction::Utxo::new_v0_p2wpkh( - outpoint, - value, - &public_key.wpubkey_hash().unwrap(), - ); - self.utxos.lock().unwrap().push(utxo.clone()); - utxo.output - } - - 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 = bitcoin::sighash::SighashCache::new(&tx).p2wpkh_signature_hash( - i, - &utxo.output.script_pubkey, - utxo.output.value, - bitcoin::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: bitcoin::EcdsaSighashType::All, - }; - tx.input[i].witness = - bitcoin::Witness::p2wpkh(&bitcoin_sig, &self.secret_key.public_key(&self.secp)); - } - } - Ok(tx) - } -} - /// The LDK API requires that any time we tell it we're done persisting a `ChannelMonitor[Update]` /// we never pass it in as the "latest" `ChannelMonitor` on startup. However, we can pass /// out-of-date monitors as long as we never told LDK we finished persisting them, which we do by @@ -532,7 +479,7 @@ type ChanMan<'a> = ChannelManager< Arc, &'a FuzzRouter, &'a FuzzRouter, - Arc, + Arc, >; #[inline] @@ -701,14 +648,14 @@ fn send_hop_payment( } #[inline] -pub fn do_test(data: &[u8], underlying_out: Out, anchors: bool) { +pub fn do_test(data: &[u8], underlying_out: Out, anchors: bool) { let out = SearchingOutput::new(underlying_out); let broadcast = Arc::new(TestBroadcaster {}); let router = FuzzRouter {}; macro_rules! make_node { ($node_id: expr, $fee_estimator: expr) => {{ - let logger: Arc = + let logger: Arc = Arc::new(test_logger::TestLogger::new($node_id.to_string(), out.clone())); let node_secret = SecretKey::from_slice(&[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, @@ -758,6 +705,7 @@ pub fn do_test(data: &[u8], underlying_out: Out, anchors: bool) { ), monitor, keys_manager, + logger, ) }}; } @@ -772,7 +720,7 @@ pub fn do_test(data: &[u8], underlying_out: Out, anchors: bool) { keys, fee_estimator| { let keys_manager = Arc::clone(keys); - let logger: Arc = + let logger: Arc = Arc::new(test_logger::TestLogger::new(node_id.to_string(), out.clone())); let chain_monitor = Arc::new(TestChainMonitor::new( broadcast.clone(), @@ -1048,9 +996,9 @@ pub fn do_test(data: &[u8], underlying_out: Out, anchors: bool) { }}; } - let wallet_a = TestWallet::new(SecretKey::from_slice(&[1; 32]).unwrap()); - let wallet_b = TestWallet::new(SecretKey::from_slice(&[2; 32]).unwrap()); - let wallet_c = TestWallet::new(SecretKey::from_slice(&[3; 32]).unwrap()); + let wallet_a = TestWalletSource::new(SecretKey::from_slice(&[1; 32]).unwrap()); + let wallet_b = TestWalletSource::new(SecretKey::from_slice(&[2; 32]).unwrap()); + let wallet_c = TestWalletSource::new(SecretKey::from_slice(&[3; 32]).unwrap()); let wallets = vec![wallet_a, wallet_b, wallet_c]; let coinbase_tx = bitcoin::Transaction { version: bitcoin::transaction::Version::TWO, @@ -1064,12 +1012,8 @@ pub fn do_test(data: &[u8], underlying_out: Out, anchors: bool) { }) .collect(), }; - let coinbase_txid = coinbase_tx.compute_txid(); wallets.iter().enumerate().for_each(|(i, w)| { - w.add_utxo( - bitcoin::OutPoint { txid: coinbase_txid, vout: i as u32 }, - Amount::from_sat(100_000), - ); + w.add_utxo(coinbase_tx.clone(), i as u32); }); let fee_est_a = Arc::new(FuzzEstimator { ret_val: atomic::AtomicU32::new(253) }); @@ -1081,11 +1025,13 @@ pub fn do_test(data: &[u8], underlying_out: Out, anchors: bool) { // 3 nodes is enough to hit all the possible cases, notably unknown-source-unknown-dest // forwarding. - let (node_a, mut monitor_a, keys_manager_a) = make_node!(0, fee_est_a); - let (node_b, mut monitor_b, keys_manager_b) = make_node!(1, fee_est_b); - let (node_c, mut monitor_c, keys_manager_c) = make_node!(2, fee_est_c); + let (node_a, mut monitor_a, keys_manager_a, logger_a) = make_node!(0, fee_est_a); + let (node_b, mut monitor_b, keys_manager_b, logger_b) = make_node!(1, fee_est_b); + let (node_c, mut monitor_c, keys_manager_c, logger_c) = make_node!(2, fee_est_c); let mut nodes = [node_a, node_b, node_c]; + let loggers = [logger_a, logger_b, logger_c]; + let fee_estimators = [Arc::clone(&fee_est_a), Arc::clone(&fee_est_b), Arc::clone(&fee_est_c)]; let chan_1_id = make_channel!(nodes[0], nodes[1], keys_manager_b, 0); let chan_2_id = make_channel!(nodes[1], nodes[2], keys_manager_c, 1); @@ -1536,6 +1482,29 @@ pub fn do_test(data: &[u8], underlying_out: Out, anchors: bool) { events::Event::ChannelReady { .. } => {}, events::Event::HTLCHandlingFailed { .. } => {}, + events::Event::FundingNeeded { + channel_id, + counterparty_node_id, + funding_template, + .. + } => { + let wallet = + WalletSync::new(&wallets[$node], Arc::clone(&loggers[$node])); + let feerate_sat_per_kw = + fee_estimators[$node].ret_val.load(atomic::Ordering::Acquire); + let feerate = FeeRate::from_sat_per_kwu(feerate_sat_per_kw as u64); + let contribution = + funding_template.build_sync(&wallet, feerate).unwrap(); + let locktime = None; + nodes[$node] + .funding_contributed( + &channel_id, + &counterparty_node_id, + contribution, + locktime, + ) + .unwrap(); + }, events::Event::FundingTransactionReadyForSigning { channel_id, counterparty_node_id, @@ -1859,16 +1828,11 @@ 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::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); + let contribution = SpliceContribution::splice_in(Amount::from_sat(10_000)); if let Err(e) = nodes[0].splice_channel( &chan_a_id, &nodes[1].get_our_node_id(), contribution, - funding_feerate_sat_per_kw, - None, ) { assert!( matches!(e, APIError::APIMisuseError { ref err } if err.contains("splice pending")), @@ -1878,16 +1842,11 @@ 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::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); + let contribution = SpliceContribution::splice_in(Amount::from_sat(10_000)); if let Err(e) = nodes[1].splice_channel( &chan_a_id, &nodes[0].get_our_node_id(), contribution, - funding_feerate_sat_per_kw, - None, ) { assert!( matches!(e, APIError::APIMisuseError { ref err } if err.contains("splice pending")), @@ -1897,16 +1856,11 @@ 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::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); + let contribution = SpliceContribution::splice_in(Amount::from_sat(10_000)); if let Err(e) = nodes[1].splice_channel( &chan_b_id, &nodes[2].get_our_node_id(), contribution, - funding_feerate_sat_per_kw, - None, ) { assert!( matches!(e, APIError::APIMisuseError { ref err } if err.contains("splice pending")), @@ -1916,16 +1870,11 @@ 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::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); + let contribution = SpliceContribution::splice_in(Amount::from_sat(10_000)); if let Err(e) = nodes[2].splice_channel( &chan_b_id, &nodes[1].get_our_node_id(), contribution, - funding_feerate_sat_per_kw, - None, ) { assert!( matches!(e, APIError::APIMisuseError { ref err } if err.contains("splice pending")), @@ -1950,14 +1899,10 @@ pub fn do_test(data: &[u8], underlying_out: Out, anchors: bool) { 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( &chan_a_id, &nodes[1].get_our_node_id(), contribution, - funding_feerate_sat_per_kw, - None, ) { assert!( matches!(e, APIError::APIMisuseError { ref err } if err.contains("splice pending")), @@ -1979,14 +1924,10 @@ pub fn do_test(data: &[u8], underlying_out: Out, anchors: bool) { 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( &chan_a_id, &nodes[0].get_our_node_id(), contribution, - funding_feerate_sat_per_kw, - None, ) { assert!( matches!(e, APIError::APIMisuseError { ref err } if err.contains("splice pending")), @@ -2008,14 +1949,10 @@ pub fn do_test(data: &[u8], underlying_out: Out, anchors: bool) { 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( &chan_b_id, &nodes[2].get_our_node_id(), contribution, - funding_feerate_sat_per_kw, - None, ) { assert!( matches!(e, APIError::APIMisuseError { ref err } if err.contains("splice pending")), @@ -2037,14 +1974,10 @@ pub fn do_test(data: &[u8], underlying_out: Out, anchors: bool) { 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( &chan_b_id, &nodes[1].get_our_node_id(), contribution, - funding_feerate_sat_per_kw, - None, ) { assert!( matches!(e, APIError::APIMisuseError { ref err } if err.contains("splice pending")), @@ -2276,7 +2209,7 @@ impl SearchingOutput { } } -pub fn chanmon_consistency_test(data: &[u8], out: Out) { +pub fn chanmon_consistency_test(data: &[u8], out: Out) { do_test(data, out.clone(), false); do_test(data, out, true); } diff --git a/lightning-tests/src/upgrade_downgrade_tests.rs b/lightning-tests/src/upgrade_downgrade_tests.rs index 8df670321be..8618b1e3718 100644 --- a/lightning-tests/src/upgrade_downgrade_tests.rs +++ b/lightning-tests/src/upgrade_downgrade_tests.rs @@ -455,7 +455,8 @@ fn do_test_0_1_htlc_forward_after_splice(fail_htlc: bool) { 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); + 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); connect_blocks(node, ANTI_REORG_DELAY - 1); diff --git a/lightning/src/events/bump_transaction/mod.rs b/lightning/src/events/bump_transaction/mod.rs index e141d9b8abc..c0513ba51d9 100644 --- a/lightning/src/events/bump_transaction/mod.rs +++ b/lightning/src/events/bump_transaction/mod.rs @@ -30,6 +30,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::FundingTxInput; use crate::ln::types::ChannelId; use crate::prelude::*; use crate::sign::ecdsa::EcdsaChannelSigner; @@ -270,6 +271,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)] @@ -282,12 +289,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 { @@ -302,6 +312,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, } } @@ -321,6 +332,7 @@ impl Utxo { }, satisfaction_weight: script_sig_size * WITNESS_SCALE_FACTOR as u64 + P2WPKH_WITNESS_WEIGHT, + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, } } @@ -330,17 +342,21 @@ 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, } } } +/// 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)] 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 @@ -348,6 +364,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`], @@ -418,11 +444,18 @@ pub trait WalletSource { fn list_confirmed_utxos<'a>( &'a self, ) -> impl Future, ()>> + MaybeSend + 'a; + + /// Returns the previous transaction containing the UTXO. + fn get_prevtx<'a>( + &'a self, utxo: &Utxo, + ) -> impl Future> + MaybeSend + 'a; + /// Returns a script to use for change above dust resulting from a successful coin selection /// attempt. fn get_change_script<'a>( &'a self, ) -> impl Future> + MaybeSend + 'a; + /// 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`]). @@ -610,10 +643,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 }) } } @@ -724,11 +760,11 @@ 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(), - sequence: Sequence::ZERO, + sequence: utxo.sequence, witness: Witness::new(), }); } @@ -846,12 +882,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(); @@ -866,10 +900,10 @@ where 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()); } } @@ -1101,13 +1135,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; @@ -1128,9 +1160,12 @@ where 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()); } } @@ -1274,9 +1309,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) @@ -1297,12 +1331,10 @@ mod tests { } 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]]); - } - } + // 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) } } @@ -1339,6 +1371,13 @@ 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![ @@ -1353,13 +1392,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 + prevtx, }], change_output: None, }, diff --git a/lightning/src/events/bump_transaction/sync.rs b/lightning/src/events/bump_transaction/sync.rs index 1328c2c1b3a..bbf0846283a 100644 --- a/lightning/src/events/bump_transaction/sync.rs +++ b/lightning/src/events/bump_transaction/sync.rs @@ -37,9 +37,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`]). @@ -79,6 +84,14 @@ where async move { utxos } } + /// Returns the previous transaction containing the UTXO. + fn get_prevtx<'a>( + &'a self, utxo: &Utxo, + ) -> impl Future> + MaybeSend + 'a { + let prevtx = self.0.get_prevtx(utxo); + Box::pin(async move { prevtx }) + } + fn get_change_script<'a>( &'a self, ) -> impl Future> + MaybeSend + 'a { diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs index d97ae6097b6..ca95c6f79eb 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::msgs; use crate::ln::onion_utils::LocalHTLCFailureReason; use crate::ln::types::ChannelId; @@ -1817,6 +1818,36 @@ pub enum Event { /// [`ChannelManager::respond_to_static_invoice_request`]: crate::ln::channelmanager::ChannelManager::respond_to_static_invoice_request invoice_request: InvoiceRequest, }, + /// Indicates that funding is needed for a channel splice or a dual-funded channel open. + /// + /// The client should build a [`FundingContribution`] from the provided [`FundingTemplate`] and + /// pass it to [`ChannelManager::funding_contributed`]. + /// + /// [`FundingContribution`]: crate::ln::funding::FundingContribution + /// [`ChannelManager::funding_contributed`]: crate::ln::channelmanager::ChannelManager::funding_contributed + 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, + /// A template for constructing a [`FundingContribution`], which contains information when + /// the funding was initiated. + /// + /// [`FundingContribution`]: crate::ln::funding::FundingContribution + 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. /// @@ -2348,6 +2379,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`. @@ -2979,6 +3024,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 fd780da8d91..e42b8d15c84 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::{ @@ -36,6 +36,7 @@ use crate::chain::channelmonitor::{ }; use crate::chain::transaction::{OutPoint, TransactionData}; use crate::chain::BestBlock; +use crate::events::bump_transaction::Input; use crate::events::{ClosureReason, FundingInfo}; use crate::ln::chan_utils; use crate::ln::chan_utils::{ @@ -43,7 +44,7 @@ use crate::ln::chan_utils::{ 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,13 @@ 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::{ + FundingContribution, FundingTemplate, 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 +71,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; @@ -2488,6 +2489,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().map(|redeem_script| { + 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) } @@ -2700,6 +2715,7 @@ impl_writeable_tlv_based!(PendingFunding, { enum FundingNegotiation { AwaitingAck { context: FundingNegotiationContext, + change_strategy: ChangeStrategy, new_holder_funding_key: PublicKey, }, ConstructingTransaction { @@ -2806,7 +2822,8 @@ impl_writeable_tlv_based!(SpliceInstructions, { #[derive(Debug)] pub(crate) enum QuiescentAction { - Splice(SpliceInstructions), + LegacySplice(SpliceInstructions), + Splice(SpliceContribution), #[cfg(any(test, fuzzing))] DoNothing, } @@ -2814,16 +2831,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`]. @@ -6498,130 +6518,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 { @@ -6644,10 +6540,17 @@ pub(super) struct FundingNegotiationContext { /// The funding outputs we will be contributing to the channel. #[allow(dead_code)] // TODO(dual_funding): Remove once contribution to V2 channels is enabled. pub our_funding_outputs: Vec, +} + +/// How the funding transaction's change is determined. +#[derive(Debug)] +pub(super) enum ChangeStrategy { + /// The change output, if any, is included in the FundingContribution's outputs. + FromCoinSelection, + /// The change output script. This will be used if needed or -- if not set -- generated using /// `SignerProvider::get_destination_script`. - #[allow(dead_code)] // TODO(splicing): Remove once splicing is enabled. - pub change_script: Option, + LegacyUserProvided(Option), } impl FundingNegotiationContext { @@ -6655,7 +6558,7 @@ impl FundingNegotiationContext { /// If error occurs, it is caused by our side, not the counterparty. fn into_interactive_tx_constructor( mut self, context: &ChannelContext, funding: &FundingScope, signer_provider: &SP, - entropy_source: &ES, holder_node_id: PublicKey, + entropy_source: &ES, holder_node_id: PublicKey, change_strategy: ChangeStrategy, ) -> Result where SP::Target: SignerProvider, @@ -6680,46 +6583,15 @@ impl FundingNegotiationContext { script_pubkey: funding.get_funding_redeemscript().to_p2wsh(), }; - // Optionally add change output - let change_value_opt = if !self.our_funding_inputs.is_empty() { - match calculate_change_output_value( - &self, - self.shared_funding_input.is_some(), - &shared_funding_output.script_pubkey, - context.holder_dust_limit_satoshis, - ) { - Ok(change_value_opt) => change_value_opt, - Err(reason) => { - return Err(self.into_negotiation_error(reason)); - }, - } - } else { - None - }; - - if let Some(change_value) = change_value_opt { - let change_script = if let Some(script) = self.change_script { - script - } else { - match signer_provider.get_destination_script(context.channel_keys_id) { - Ok(script) => script, - Err(_) => { - let reason = AbortReason::InternalError("Error getting change script"); - return Err(self.into_negotiation_error(reason)); - }, - } - }; - 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.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); - self.our_funding_outputs.push(change_output); - } + match self.calculate_change_output( + context, + signer_provider, + &shared_funding_output, + change_strategy, + ) { + Ok(Some(change_output)) => self.our_funding_outputs.push(change_output), + Ok(None) => {}, + Err(reason) => return Err(self.into_negotiation_error(reason)), } let constructor_args = InteractiveTxConstructorArgs { @@ -6741,6 +6613,55 @@ impl FundingNegotiationContext { InteractiveTxConstructor::new(constructor_args) } + fn calculate_change_output( + &self, context: &ChannelContext, signer_provider: &SP, shared_funding_output: &TxOut, + change_strategy: ChangeStrategy, + ) -> Result, AbortReason> + where + SP::Target: SignerProvider, + { + if self.our_funding_inputs.is_empty() { + return Ok(None); + } + + let change_script = match change_strategy { + ChangeStrategy::FromCoinSelection => return Ok(None), + ChangeStrategy::LegacyUserProvided(change_script) => change_script, + }; + + let change_value = calculate_change_output_value( + &self, + self.shared_funding_input.is_some(), + &shared_funding_output.script_pubkey, + context.holder_dust_limit_satoshis, + )?; + + if let Some(change_value) = change_value { + let change_script = match change_script { + Some(script) => script, + None => match signer_provider.get_destination_script(context.channel_keys_id) { + Ok(script) => script, + Err(_) => { + return Err(AbortReason::InternalError("Error getting 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.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); + return Ok(Some(change_output)); + } + } + + Ok(None) + } + fn into_negotiation_error(self, reason: AbortReason) -> NegotiationError { let (contributed_inputs, contributed_outputs) = self.into_contributed_inputs_and_outputs(); NegotiationError { reason, contributed_inputs, contributed_outputs } @@ -6973,7 +6894,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 { @@ -6983,6 +6904,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); @@ -11368,7 +11298,10 @@ 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(); } } @@ -12037,8 +11970,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, @@ -12052,8 +11984,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!( @@ -12082,71 +12021,70 @@ 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, - 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.validate_splice_contributions(our_funding_contribution, their_funding_contribution) + .map_err(|err| APIError::APIMisuseError { err })?; + + 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, + { + debug_assert!(contribution.is_splice()); + + 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 (our_funding_inputs, our_funding_outputs, change_script) = contribution.into_tx_parts(); + 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) = 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, ChangeStrategy::FromCoinSelection)) } 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, @@ -12165,9 +12103,15 @@ where shared_funding_input: Some(prev_funding_input), our_funding_inputs, our_funding_outputs, - change_script, }; + self.send_splice_init_internal(context, ChangeStrategy::LegacyUserProvided(change_script)) + } + + fn send_splice_init_internal( + &mut self, context: FundingNegotiationContext, change_strategy: ChangeStrategy, + ) -> 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) { @@ -12182,8 +12126,15 @@ where _ => todo!(), }; - let funding_negotiation = - FundingNegotiation::AwaitingAck { context, new_holder_funding_key: funding_pubkey }; + 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, + change_strategy, + new_holder_funding_key: funding_pubkey, + }; self.pending_splice = Some(PendingFunding { funding_negotiation: Some(funding_negotiation), negotiated_candidates: vec![], @@ -12193,7 +12144,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, @@ -12413,7 +12364,6 @@ where shared_funding_input: Some(prev_funding_input), our_funding_inputs: Vec::new(), our_funding_outputs: Vec::new(), - change_script: None, }; let mut interactive_tx_constructor = funding_negotiation_context @@ -12423,6 +12373,8 @@ where signer_provider, entropy_source, holder_node_id.clone(), + // ChangeStrategy doesn't matter when no inputs are contributed + ChangeStrategy::FromCoinSelection, ) .map_err(|err| { ChannelError::WarnAndDisconnect(format!( @@ -12477,11 +12429,11 @@ where let pending_splice = self.pending_splice.as_mut().expect("We should have returned an error earlier!"); // TODO: Good candidate for a let else statement once MSRV >= 1.65 - let funding_negotiation_context = - if let Some(FundingNegotiation::AwaitingAck { context, .. }) = + let (funding_negotiation_context, change_strategy) = + if let Some(FundingNegotiation::AwaitingAck { context, change_strategy, .. }) = pending_splice.funding_negotiation.take() { - context + (context, change_strategy) } else { panic!("We should have returned an error earlier!"); }; @@ -12493,6 +12445,7 @@ where signer_provider, entropy_source, holder_node_id.clone(), + change_strategy, ) .map_err(|err| { ChannelError::WarnAndDisconnect(format!( @@ -12523,7 +12476,7 @@ where let (funding_negotiation_context, new_holder_funding_key) = match &pending_splice .funding_negotiation { - Some(FundingNegotiation::AwaitingAck { context, new_holder_funding_key }) => { + Some(FundingNegotiation::AwaitingAck { context, new_holder_funding_key, .. }) => { (context, new_holder_funding_key) }, Some(FundingNegotiation::ConstructingTransaction { .. }) @@ -13443,9 +13396,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!( @@ -13458,6 +13411,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 @@ -14292,7 +14268,6 @@ where shared_funding_input: None, our_funding_inputs: funding_inputs, our_funding_outputs: Vec::new(), - change_script: None, }; let chan = Self { funding, @@ -14446,7 +14421,6 @@ where shared_funding_input: None, our_funding_inputs: our_funding_inputs.clone(), our_funding_outputs: Vec::new(), - change_script: None, }; let shared_funding_output = TxOut { value: Amount::from_sat(funding.get_value_satoshis()), @@ -16078,7 +16052,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}; @@ -16110,7 +16083,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] @@ -18462,250 +18435,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 1ab87a70c72..50731850042 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; @@ -4657,12 +4657,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 { @@ -4675,7 +4675,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(); @@ -4703,15 +4703,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, @@ -6351,6 +6345,105 @@ where result } + /// Adds or removes funds from the given channel as specified by a [`FundingContribution`]. + /// + /// Used to handle an [`Event::FundingNeeded`] by constructing a [`FundingContribution`] from a + /// [`FundingTemplate`] and passing it here. See [`FundingTemplate::build`] and + /// [`FundingTemplate::build_sync`]. + /// + /// Calling this method will commence the process of creating a new funding transaction for the + /// channel. An [`Event::FundingTransactionReadyForSigning`] will be generated once the + /// transaction is successfully constructed interactively with the counterparty. + /// If unsuccessful, an [`Event::SpliceFailed`] will be surfaced instead. + /// + /// Returns [`ChannelUnavailable`] when a channel is not found or an incorrect + /// `counterparty_node_id` is provided. + /// + /// Returns [`APIMisuseError`] when a channel is not in a state where it is expecting funding + /// contribution. + /// + /// [`FundingTemplate`]: crate::ln::funding::FundingTemplate + /// [`FundingTemplate::build`]: crate::ln::funding::FundingTemplate::build + /// [`FundingTemplate::build_sync`]: crate::ln::funding::FundingTemplate::build_sync + /// [`ChannelUnavailable`]: APIError::ChannelUnavailable + /// [`APIMisuseError`]: APIError::APIMisuseError + 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. @@ -12134,6 +12227,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/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index 00fbca201cd..07218785b73 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -395,18 +395,17 @@ 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); } } } } pub fn provide_anchor_reserves<'a, 'b, 'c>(nodes: &[Node<'a, 'b, 'c>]) -> Transaction { - provide_anchor_utxo_reserves(nodes, 1, Amount::ONE_BTC) + provide_utxo_reserves(nodes, 1, Amount::ONE_BTC) } -pub fn provide_anchor_utxo_reserves<'a, 'b, 'c>( +pub fn provide_utxo_reserves<'a, 'b, 'c>( nodes: &[Node<'a, 'b, 'c>], utxos: usize, amount: Amount, ) -> Transaction { let mut output = Vec::with_capacity(nodes.len()); @@ -613,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_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 8092a0e4451..2cddd6fb316 100644 --- a/lightning/src/ln/funding.rs +++ b/lightning/src/ln/funding.rs @@ -9,49 +9,58 @@ //! Types pertaining to funding channels. -use alloc::vec::Vec; +use bitcoin::hashes::Hash; +use bitcoin::secp256k1::PublicKey; +use bitcoin::{ + Amount, FeeRate, OutPoint, Script, ScriptBuf, Sequence, SignedAmount, Transaction, TxOut, + WScriptHash, Weight, +}; -use bitcoin::{Amount, ScriptBuf, SignedAmount, TxOut}; -use bitcoin::{Script, Sequence, Transaction, Weight}; +use core::ops::Deref; -use crate::events::bump_transaction::Utxo; -use crate::ln::chan_utils::EMPTY_SCRIPT_SIG_WEIGHT; +use crate::chain::ClaimId; +use crate::events::bump_transaction::sync::CoinSelectionSourceSync; +use crate::events::bump_transaction::{CoinSelectionSource, Input, Utxo}; +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::prelude::*; use crate::sign::{P2TR_KEY_PATH_WITNESS_WEIGHT, P2WPKH_WITNESS_WEIGHT}; +use crate::util::async_poll::MaybeSend; /// 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. + /// The amount of value to contribute from inputs to the splice's funding transaction. /// - /// [`inputs`]: Self::inputs + /// If `value_added` is [`Amount::ZERO`], then any fees will be deducted from the channel + /// balance instead of paid by 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. - 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. + /// The outputs to include in the splice's funding transaction, whose amounts will be deducted + /// from the channel balance. 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_writeable_tlv_based!(SpliceContribution, { + (1, value_added, required), + (3, outputs, optional_vec), +}); + 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, - ) -> Self { - Self { value_added, inputs, outputs: vec![], change_script } + pub fn splice_in(value_added: Amount) -> Self { + Self { value_added, outputs: vec![] } } /// 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 } } /// Creates a contribution for when funds are both added to and removed from a channel. @@ -59,11 +68,8 @@ impl SpliceContribution { /// 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 { - Self { value_added, inputs, outputs, change_script } + pub fn splice_in_and_out(value_added: Amount, outputs: Vec) -> Self { + Self { value_added, outputs } } /// The net value contributed to a channel by the splice. If negative, more value will be @@ -81,21 +87,323 @@ 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 + } +} + +/// A template for contributing to a channel's splice funding transaction. +/// +/// This is included in an [`Event::FundingNeeded`] when a channel is ready to be spliced. It +/// contains information passed as a [`SpliceContribution`] to [`ChannelManager::splice_channel`]. +/// It must be converted to a [`FundingContribution`] and passed to +/// [`ChannelManager::funding_contributed`] in order to resume the splicing process. +/// +/// [`Event::FundingNeeded`]: crate::events::Event::FundingNeeded +/// [`ChannelManager::splice_channel`]: crate::ln::channelmanager::ChannelManager::splice_channel +/// [`ChannelManager::funding_contributed`]: crate::ln::channelmanager::ChannelManager::funding_contributed +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FundingTemplate { + /// The amount to contribute to the channel. + /// + /// If `value_added` is [`Amount::ZERO`], then any fees will be deducted from the channel + /// balance instead of paid by inputs. + value_added: Amount, + + /// The outputs to contribute to the funding transaction, excluding change. + outputs: Vec, + + /// The shared input, which, if present indicates the funding template is for a splice funding + /// transaction. + shared_input: Option, + + /// Whether the contributor initiated the funding, and thus is responsible for fees incurred for + /// common fields and shared inputs and outputs. + is_initiator: bool, +} + +impl_writeable_tlv_based!(FundingTemplate, { + (1, value_added, required), + (3, outputs, optional_vec), + (5, shared_input, option), + (7, is_initiator, required), +}); + +impl FundingTemplate { + /// Constructs a [`FundingTemplate`] for a splice using the provided shared input. + pub(super) fn for_splice(contribution: SpliceContribution, shared_input: Input) -> Self { + let SpliceContribution { value_added, outputs } = contribution; + Self { value_added, outputs, shared_input: Some(shared_input), is_initiator: true } + } +} + +macro_rules! build_funding_contribution { + ($self:ident, $wallet:ident, $feerate:ident, $($await:tt)*) => {{ + let FundingTemplate { value_added, mut outputs, 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(), + }; + + // FIXME: Should claim_id be an Option? + let claim_id = ClaimId([0; 32]); + let must_spend = shared_input.map(|input| vec![input]).unwrap_or_default(); + 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)$(.$await)*? + } 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)$(.$await)*? + }; + + if let Some(change_output) = selection.change_output { + outputs.push(change_output); + } + + 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, + feerate: $feerate, + is_initiator, + is_splice, + }; + + Ok(contribution) + }}; +} + +impl FundingTemplate { + /// Creates a `FundingContribution` from the template by using `wallet` to perform coin + /// selection with the given fee rate. + pub async fn build( + self, wallet: W, feerate: FeeRate, + ) -> Result + where + W::Target: CoinSelectionSource + MaybeSend, + { + build_funding_contribution!(self, wallet, feerate, await) + } + + /// Creates a `FundingContribution` from the template by using `wallet` to perform coin + /// selection with the given fee rate. + pub fn build_sync( + self, wallet: W, feerate: FeeRate, + ) -> Result + where + W::Target: CoinSelectionSourceSync, + { + build_funding_contribution!(self, wallet, feerate,) + } +} + +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 funding transaction contributed by one party. +#[derive(Debug, Clone)] +pub struct FundingContribution { + /// The amount to contribute to the channel. + /// + /// If `value_added` is [`Amount::ZERO`], then any fees will be deducted from the channel + /// balance instead of paid by `inputs`. + value_added: Amount, + + /// The amount to remove from the channel. + value_removed: Amount, + + /// The estimate fees responsible to be paid for the contribution. + estimated_fee: Amount, + + /// The inputs included in the 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 funding transaction. The total value of all outputs plus fees + /// will be the amount that is removed. + outputs: Vec, + + /// The fee rate used to select `inputs`. + feerate: FeeRate, + + /// Whether the contributor initiated the funding, and thus is responsible for fees incurred for + /// common fields and shared inputs and outputs. + is_initiator: bool, + + /// Whether the contribution is for funding a splice. + 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 feerate(&self) -> FeeRate { + self.feerate + } + + pub(super) fn is_initiator(&self) -> bool { + self.is_initiator + } + + pub(super) fn is_splice(&self) -> bool { + self.is_splice } - pub(super) fn inputs(&self) -> &[FundingTxInput] { - &self.inputs[..] + pub(super) fn into_tx_parts(self) -> (Vec, Vec) { + let FundingContribution { inputs, outputs, .. } = self; + (inputs, outputs) } - pub(super) fn outputs(&self) -> &[TxOut] { - &self.outputs[..] + 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 into_tx_parts(self) -> (Vec, Vec, Option) { - let SpliceContribution { value_added: _, inputs, outputs, change_script } = self; - (inputs, outputs, change_script) + pub(super) fn validate(&self) -> Result { + 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 = if self.inputs.is_empty() { self.estimated_fee } else { 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) } } @@ -103,26 +411,22 @@ impl SpliceContribution { /// establishment protocol or when splicing. #[derive(Debug, Clone)] pub struct FundingTxInput { - /// The unspent [`TxOut`] that the input spends. + /// The unspent [`TxOut`] found in [`prevtx`]. /// /// [`TxOut`]: bitcoin::TxOut - pub(super) utxo: Utxo, - - /// The sequence number to use in the [`TxIn`]. - /// - /// [`TxIn`]: bitcoin::TxIn - pub(super) sequence: Sequence, + /// [`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, } 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 +444,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, }) } @@ -230,22 +534,289 @@ 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 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. + /// 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 + } +} + +#[cfg(test)] +mod tests { + use super::{estimate_transaction_fee, FundingContribution, FundingTxInput}; + use bitcoin::hashes::Hash; + use bitcoin::transaction::{Transaction, TxOut, Version}; + use bitcoin::{Amount, FeeRate, ScriptBuf, WPubkeyHash}; + + #[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() { + // positive case, inputs well over intended contribution + { + let expected_fee = if cfg!(feature = "grind_signatures") { 2278 } else { 2284 }; + 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![], + 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 }; + 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), + ], + 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 }; + 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), + ], + 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), + ], + is_initiator: true, + is_splice: true, + feerate: FeeRate::from_sat_per_kwu(90000), + }; + assert_eq!( + 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), + )), + ); + } + + // 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![], + is_initiator: true, + is_splice: true, + feerate: FeeRate::from_sat_per_kwu(2000), + }; + assert_eq!( + 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), + )), + ); + } + + // barely covers + { + let expected_fee = if cfg!(feature = "grind_signatures") { 2278 } else { 2284 }; + 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![], + 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![], + is_initiator: true, + is_splice: true, + feerate: FeeRate::from_sat_per_kwu(2200), + }; + assert_eq!( + 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), + )), + ); + } + + // barely covers, less fees (no extra weight, not initiator) + { + let expected_fee = if cfg!(feature = "grind_signatures") { 1084 } else { 1088 }; + 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![], + 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/interactivetxs.rs b/lightning/src/ln/interactivetxs.rs index 7ed829886c6..c1874bf7809 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, @@ -3443,7 +3447,6 @@ mod tests { shared_funding_input: None, our_funding_inputs: inputs, our_funding_outputs: outputs, - change_script: None, }; let gross_change = total_inputs - total_outputs - context.our_funding_contribution.to_unsigned().unwrap(); diff --git a/lightning/src/ln/splicing_tests.rs b/lightning/src/ln/splicing_tests.rs index db6680d963c..fc5139938dd 100644 --- a/lightning/src/ln/splicing_tests.rs +++ b/lightning/src/ln/splicing_tests.rs @@ -13,7 +13,7 @@ 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; @@ -21,7 +21,7 @@ use crate::ln::channelmanager::{ provided_init_features, PaymentId, RecipientOnionFields, BREAKDOWN_TIMEOUT, }; use crate::ln::functional_test_utils::*; -use crate::ln::funding::{FundingTxInput, SpliceContribution}; +use crate::ln::funding::{FundingContribution, SpliceContribution}; use crate::ln::msgs::{self, BaseMessageHandler, ChannelMessageHandler, MessageSendEvent}; use crate::ln::types::ChannelId; use crate::routing::router::{PaymentParameters, RouteParameters}; @@ -29,9 +29,10 @@ use crate::util::errors::APIError; use crate::util::ser::Writeable; use crate::util::test_channel_signer::SignerOp; -use bitcoin::hashes::Hash; +use crate::sync::Arc; + use bitcoin::secp256k1::PublicKey; -use bitcoin::{Amount, OutPoint as BitcoinOutPoint, ScriptBuf, Transaction, TxOut, WPubkeyHash}; +use bitcoin::{Amount, FeeRate, OutPoint as BitcoinOutPoint, ScriptBuf, Transaction, TxOut}; #[test] fn test_splicing_not_supported_api_error() { @@ -48,15 +49,9 @@ fn test_splicing_not_supported_api_error() { let (_, _, channel_id, _) = create_announced_chan_between_nodes(&nodes, 0, 1); - let bs_contribution = SpliceContribution::splice_in(Amount::ZERO, Vec::new(), None); + let bs_contribution = SpliceContribution::splice_in(Amount::ZERO); - let res = nodes[1].node.splice_channel( - &channel_id, - &node_id_0, - bs_contribution.clone(), - 0, // funding_feerate_per_kw, - None, // locktime - ); + let res = nodes[1].node.splice_channel(&channel_id, &node_id_0, bs_contribution.clone()); match res { Err(APIError::ChannelUnavailable { err }) => { assert!(err.contains("Peer does not support splicing")) @@ -77,13 +72,7 @@ fn test_splicing_not_supported_api_error() { reconnect_args.send_announcement_sigs = (true, true); reconnect_nodes(reconnect_args); - let res = nodes[1].node.splice_channel( - &channel_id, - &node_id_0, - bs_contribution, - 0, // funding_feerate_per_kw, - None, // locktime - ); + let res = nodes[1].node.splice_channel(&channel_id, &node_id_0, bs_contribution); match res { Err(APIError::ChannelUnavailable { err }) => { assert!(err.contains("Peer does not support quiescence, a splicing prerequisite")) @@ -99,47 +88,55 @@ 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; + + provide_utxo_reserves(&nodes, 1, extra_splice_funding_input); - let contribution = - SpliceContribution::splice_in(Amount::from_sat(splice_in_sats), funding_inputs, None); + let contribution = SpliceContribution::splice_in(splice_in_value); // 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()), - } + nodes[0] + .node + .splice_channel(&channel_id, &nodes[1].node.get_our_node_id(), contribution) + .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(Arc::clone(&nodes[0].wallet_source), 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>( initiator: &'a Node<'b, 'c, 'd>, acceptor: &'a Node<'b, 'c, 'd>, channel_id: ChannelId, initiator_contribution: SpliceContribution, ) -> msgs::CommitmentSigned { - let new_funding_script = - complete_splice_handshake(initiator, acceptor, channel_id, initiator_contribution.clone()); + let (funding_contribution, new_funding_script) = + complete_splice_handshake(initiator, acceptor, channel_id, initiator_contribution); complete_interactive_funding_negotiation( initiator, acceptor, channel_id, - initiator_contribution, + funding_contribution, new_funding_script, ) } @@ -147,26 +144,19 @@ pub fn negotiate_splice_tx<'a, 'b, 'c, 'd>( pub fn complete_splice_handshake<'a, 'b, 'c, 'd>( initiator: &'a Node<'b, 'c, 'd>, acceptor: &'a Node<'b, 'c, 'd>, channel_id: ChannelId, initiator_contribution: SpliceContribution, -) -> ScriptBuf { +) -> (FundingContribution, ScriptBuf) { let node_id_initiator = initiator.node.get_our_node_id(); let node_id_acceptor = acceptor.node.get_our_node_id(); - initiator - .node - .splice_channel( - &channel_id, - &node_id_acceptor, - initiator_contribution, - FEERATE_FLOOR_SATS_PER_KW, - None, - ) - .unwrap(); + initiator.node.splice_channel(&channel_id, &node_id_acceptor, initiator_contribution).unwrap(); 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 funding_contribution = 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); @@ -178,12 +168,35 @@ pub fn complete_splice_handshake<'a, 'b, 'c, 'd>( ) .to_p2wsh(); - new_funding_script + (funding_contribution, 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, +) -> FundingContribution { + 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(Arc::clone(&initiator.wallet_source), 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.clone(), locktime) + .unwrap(); + + funding_contribution } 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, + initiator_contribution: FundingContribution, 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(); @@ -197,8 +210,7 @@ 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_outputs) = initiator_contribution.into_tx_parts(); let mut expected_initiator_inputs = initiator_inputs .iter() .map(|input| input.utxo.outpoint) @@ -208,7 +220,6 @@ pub fn complete_interactive_funding_negotiation<'a, 'b, 'c, 'd>( .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; @@ -341,19 +352,19 @@ pub fn sign_interactive_funding_tx<'a, 'b, 'c, 'd>( pub 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 { +) -> (Transaction, ScriptBuf) { let node_id_initiator = initiator.node.get_our_node_id(); let node_id_acceptor = acceptor.node.get_our_node_id(); - let new_funding_script = - complete_splice_handshake(initiator, acceptor, channel_id, initiator_contribution.clone()); + let (funding_contribution, new_funding_script) = + complete_splice_handshake(initiator, acceptor, channel_id, initiator_contribution); let initial_commit_sig_for_acceptor = complete_interactive_funding_negotiation( initiator, acceptor, channel_id, - initiator_contribution, - new_funding_script, + funding_contribution, + new_funding_script.clone(), ); let (splice_tx, splice_locked) = sign_interactive_funding_tx(initiator, acceptor, initial_commit_sig_for_acceptor, false); @@ -362,7 +373,7 @@ pub fn splice_channel<'a, 'b, 'c, 'd>( expect_splice_pending_event(initiator, &node_id_acceptor); expect_splice_pending_event(acceptor, &node_id_initiator); - splice_tx + (splice_tx, new_funding_script) } pub fn lock_splice_after_blocks<'a, 'b, 'c, 'd>( @@ -488,16 +499,7 @@ fn do_test_splice_state_reset_on_disconnect(reload: bool) { value: Amount::from_sat(1_000), script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), }]); - nodes[0] - .node - .splice_channel( - &channel_id, - &node_id_1, - contribution.clone(), - FEERATE_FLOOR_SATS_PER_KW, - None, - ) - .unwrap(); + nodes[0].node.splice_channel(&channel_id, &node_id_1, contribution.clone()).unwrap(); // Attempt a splice negotiation that only goes up to receiving `splice_init`. Reconnecting // should implicitly abort the negotiation and reset the splice state such that we're able to @@ -507,6 +509,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); @@ -542,16 +546,7 @@ fn do_test_splice_state_reset_on_disconnect(reload: bool) { reconnect_args.send_announcement_sigs = (true, true); reconnect_nodes(reconnect_args); - nodes[0] - .node - .splice_channel( - &channel_id, - &node_id_1, - contribution.clone(), - FEERATE_FLOOR_SATS_PER_KW, - None, - ) - .unwrap(); + nodes[0].node.splice_channel(&channel_id, &node_id_1, contribution.clone()).unwrap(); // Attempt a splice negotiation that ends mid-construction of the funding transaction. // Reconnecting should implicitly abort the negotiation and reset the splice state such that @@ -561,6 +556,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); @@ -601,16 +598,7 @@ fn do_test_splice_state_reset_on_disconnect(reload: bool) { reconnect_args.send_announcement_sigs = (true, true); reconnect_nodes(reconnect_args); - nodes[0] - .node - .splice_channel( - &channel_id, - &node_id_1, - contribution.clone(), - FEERATE_FLOOR_SATS_PER_KW, - None, - ) - .unwrap(); + nodes[0].node.splice_channel(&channel_id, &node_id_1, contribution.clone()).unwrap(); // Attempt a splice negotiation that ends before the initial `commitment_signed` messages are // exchanged. The node missing the other's `commitment_signed` upon reconnecting should @@ -621,6 +609,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); @@ -688,7 +678,7 @@ fn do_test_splice_state_reset_on_disconnect(reload: bool) { // Attempt a splice negotiation that completes, (i.e. `tx_signatures` are exchanged). Reconnecting // should not abort the negotiation or reset the splice state. - let splice_tx = splice_channel(&nodes[0], &nodes[1], channel_id, contribution); + let (splice_tx, _) = splice_channel(&nodes[0], &nodes[1], channel_id, contribution); if reload { let encoded_monitor_0 = get_monitor!(nodes[0], channel_id).encode(); @@ -744,22 +734,15 @@ fn test_config_reject_inbound_splices() { value: Amount::from_sat(1_000), script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), }]); - nodes[0] - .node - .splice_channel( - &channel_id, - &node_id_1, - contribution.clone(), - FEERATE_FLOOR_SATS_PER_KW, - None, - ) - .unwrap(); + nodes[0].node.splice_channel(&channel_id, &node_id_1, contribution.clone()).unwrap(); let stfu = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1); nodes[1].node.handle_stfu(node_id_0, &stfu); 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); @@ -799,26 +782,24 @@ 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 change_script = ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros()); - let fees = Amount::from_sat(321); + let utxo_value = added_value * 3 / 4; + let fees = Amount::from_sat(322); - 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()), - ); + provide_utxo_reserves(&nodes, 2, utxo_value); - let splice_tx = splice_channel(&nodes[0], &nodes[1], channel_id, initiator_contribution); - let expected_change = Amount::ONE_BTC * 2 - added_value - fees; + let initiator_contribution = SpliceContribution::splice_in(added_value); + + let (splice_tx, new_funding_script) = + splice_channel(&nodes[0], &nodes[1], channel_id, initiator_contribution); + let expected_change = utxo_value * 2 - added_value - fees; assert_eq!( - splice_tx.output.iter().find(|txout| txout.script_pubkey == change_script).unwrap().value, + splice_tx + .output + .iter() + .find(|txout| txout.script_pubkey != new_funding_script) + .unwrap() + .value, expected_change, ); @@ -862,7 +843,7 @@ fn test_splice_out() { }, ]); - let splice_tx = splice_channel(&nodes[0], &nodes[1], channel_id, initiator_contribution); + 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); @@ -892,29 +873,24 @@ 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 change_script = ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros()); + let utxo_value = added_value * 3 / 4; let fees = if cfg!(feature = "grind_signatures") { - Amount::from_sat(383) + Amount::from_sat(385) } else { - Amount::from_sat(384) + Amount::from_sat(385) }; assert!(htlc_limit_msat > initial_channel_value_sat / 2 * 1000); + provide_utxo_reserves(&nodes, 2, utxo_value); + 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, @@ -925,13 +901,19 @@ fn test_splice_in_and_out() { 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; + let (splice_tx, new_funding_script) = + splice_channel(&nodes[0], &nodes[1], channel_id, initiator_contribution); + let expected_change = utxo_value * 2 - added_value - fees; assert_eq!( - splice_tx.output.iter().find(|txout| txout.script_pubkey == change_script).unwrap().value, + splice_tx + .output + .iter() + .filter(|txout| txout.value != removed_value / 2) + .find(|txout| txout.script_pubkey != new_funding_script) + .unwrap() + .value, expected_change, ); @@ -948,26 +930,24 @@ 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); - // 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 utxo_value = added_value * 3 / 4; let fees = if cfg!(feature = "grind_signatures") { - Amount::from_sat(383) + Amount::from_sat(385) } else { - Amount::from_sat(384) + Amount::from_sat(385) }; + // Clear UTXOs so that the change output from the previous splice isn't considered + nodes[0].wallet_source.clear_utxos(); + + provide_utxo_reserves(&nodes, 2, utxo_value); + 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, @@ -978,13 +958,19 @@ fn test_splice_in_and_out() { 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; + let (splice_tx, new_funding_script) = + splice_channel(&nodes[0], &nodes[1], channel_id, initiator_contribution); + let expected_change = utxo_value * 2 - added_value - fees; assert_eq!( - splice_tx.output.iter().find(|txout| txout.script_pubkey == change_script).unwrap().value, + splice_tx + .output + .iter() + .filter(|txout| txout.value != removed_value / 2) + .find(|txout| txout.script_pubkey != new_funding_script) + .unwrap() + .value, expected_change, ); @@ -1000,20 +986,14 @@ 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); - // 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()); + + provide_utxo_reserves(&nodes, 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, @@ -1024,7 +1004,6 @@ fn test_splice_in_and_out() { script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(), }, ], - Some(change_script), ); assert_eq!( @@ -1032,8 +1011,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), @@ -1075,19 +1052,16 @@ 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 = provide_utxo_reserves(&nodes, 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); + let initiator_contribution = SpliceContribution::splice_in(Amount::from_sat(splice_in_amount)); + 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; @@ -1570,32 +1544,14 @@ fn do_test_propose_splice_while_disconnected(reload: bool, use_0conf: bool) { value: Amount::from_sat(splice_out_sat), script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), }]); - nodes[0] - .node - .splice_channel( - &channel_id, - &node_id_1, - node_0_contribution.clone(), - FEERATE_FLOOR_SATS_PER_KW, - None, - ) - .unwrap(); + nodes[0].node.splice_channel(&channel_id, &node_id_1, node_0_contribution.clone()).unwrap(); assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty()); 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( - &channel_id, - &node_id_0, - node_1_contribution.clone(), - FEERATE_FLOOR_SATS_PER_KW, - None, - ) - .unwrap(); + nodes[1].node.splice_channel(&channel_id, &node_id_0, node_1_contribution.clone()).unwrap(); assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); if reload { @@ -1628,6 +1584,9 @@ fn do_test_propose_splice_while_disconnected(reload: bool, use_0conf: bool) { } reconnect_args.send_stfu = (true, true); reconnect_nodes(reconnect_args); + + let node_0_funding_contribution = 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()); @@ -1652,7 +1611,7 @@ fn do_test_propose_splice_while_disconnected(reload: bool, use_0conf: bool) { &nodes[0], &nodes[1], channel_id, - node_0_contribution, + node_0_funding_contribution, new_funding_script, ) }; @@ -1780,6 +1739,8 @@ fn do_test_propose_splice_while_disconnected(reload: bool, use_0conf: bool) { panic!("Unexpected event {:?}", &msg_events[0]); } + let node_1_funding_contribution = 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); @@ -1794,7 +1755,7 @@ fn do_test_propose_splice_while_disconnected(reload: bool, use_0conf: bool) { &nodes[1], &nodes[0], channel_id, - node_1_contribution, + node_1_funding_contribution, new_funding_script, ) }; @@ -1836,13 +1797,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); + provide_utxo_reserves(&nodes, 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()), - ); + let contribution = SpliceContribution::splice_in(Amount::from_sat(splice_in_amount)); // Complete interactive-tx construction, but fail by having the acceptor send a duplicate // tx_complete instead of commitment_signed. @@ -1876,17 +1834,15 @@ 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); + provide_utxo_reserves(&nodes, 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()), - ); + let contribution = SpliceContribution::splice_in(Amount::from_sat(splice_in_amount)); // 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. - let _ = complete_splice_handshake(initiator, acceptor, channel_id, contribution.clone()); + let (funding_contribution, _) = + complete_splice_handshake(initiator, acceptor, channel_id, contribution.clone()); let tx_add_input = get_event_msg!(initiator, MessageSendEvent::SendTxAddInput, node_id_acceptor); @@ -1900,7 +1856,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], funding_contribution.into_tx_parts().0[0].outpoint()); }, _ => panic!("Expected Event::SpliceFailed"), } @@ -1918,7 +1874,8 @@ fn fail_splice_on_interactive_tx_error() { &channel_id, SignerOp::SignCounterpartyCommitment, ); - let _ = complete_splice_handshake(initiator, acceptor, channel_id, contribution.clone()); + let (funding_contribution, _) = + complete_splice_handshake(initiator, acceptor, channel_id, contribution.clone()); let tx_add_input = get_event_msg!(initiator, MessageSendEvent::SendTxAddInput, node_id_acceptor); @@ -1952,7 +1909,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], funding_contribution.into_tx_parts().0[0].outpoint()); }, _ => panic!("Expected Event::SpliceFailed"), } @@ -1982,17 +1939,15 @@ 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); + provide_utxo_reserves(&nodes, 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()), - ); + let contribution = SpliceContribution::splice_in(Amount::from_sat(splice_in_amount)); // Fail during interactive-tx construction by having the acceptor send tx_abort instead of // tx_complete. - let _ = complete_splice_handshake(initiator, acceptor, channel_id, contribution.clone()); + let (funding_contribution, _) = + complete_splice_handshake(initiator, acceptor, channel_id, contribution.clone()); let tx_add_input = get_event_msg!(initiator, MessageSendEvent::SendTxAddInput, node_id_acceptor); @@ -2009,7 +1964,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], funding_contribution.into_tx_parts().0[0].outpoint()); }, _ => panic!("Expected Event::SpliceFailed"), } @@ -2036,13 +1991,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); + provide_utxo_reserves(&nodes, 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()), - ); + let contribution = SpliceContribution::splice_in(Amount::from_sat(splice_in_amount)); // Close the channel before completion of interactive-tx construction. let _ = complete_splice_handshake(initiator, acceptor, channel_id, contribution.clone()); @@ -2087,25 +2039,11 @@ 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()], - Some(nodes[0].wallet_source.get_change_script().unwrap()), - ); + let contribution = SpliceContribution::splice_in(Amount::from_sat(splice_in_amount)); // Close the channel before completion of STFU handshake. - initiator - .node - .splice_channel( - &channel_id, - &node_id_acceptor, - contribution, - FEERATE_FLOOR_SATS_PER_KW, - None, - ) - .unwrap(); + initiator.node.splice_channel(&channel_id, &node_id_acceptor, contribution).unwrap(); let _stfu_init = get_event_msg!(initiator, MessageSendEvent::SendStfu, node_id_acceptor); @@ -2186,7 +2124,7 @@ fn do_test_splice_with_inflight_htlc_forward_and_resolution(expire_scid_pre_forw 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); + 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); } @@ -2195,7 +2133,7 @@ fn do_test_splice_with_inflight_htlc_forward_and_resolution(expire_scid_pre_forw 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); + 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); } diff --git a/lightning/src/ln/zero_fee_commitment_tests.rs b/lightning/src/ln/zero_fee_commitment_tests.rs index 2503ad81cde..785894c3cb0 100644 --- a/lightning/src/ln/zero_fee_commitment_tests.rs +++ b/lightning/src/ln/zero_fee_commitment_tests.rs @@ -131,7 +131,7 @@ fn test_htlc_claim_chunking() { let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &configs); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); - let coinbase_tx = provide_anchor_utxo_reserves(&nodes, 50, Amount::from_sat(500)); + let coinbase_tx = provide_utxo_reserves(&nodes, 50, Amount::from_sat(500)); const CHAN_CAPACITY: u64 = 10_000_000; let (_, _, chan_id, _funding_tx) = create_announced_chan_between_nodes_with_value( @@ -322,7 +322,7 @@ fn test_anchor_tx_too_big() { let node_a_id = nodes[0].node.get_our_node_id(); - let _coinbase_tx_a = provide_anchor_utxo_reserves(&nodes, 50, Amount::from_sat(500)); + let _coinbase_tx_a = provide_utxo_reserves(&nodes, 50, Amount::from_sat(500)); const CHAN_CAPACITY: u64 = 10_000_000; let (_, _, chan_id, _funding_tx) = create_announced_chan_between_nodes_with_value( 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, } } diff --git a/lightning/src/util/test_utils.rs b/lightning/src/util/test_utils.rs index 34f5d5fe36e..f1681f432e6 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; @@ -2211,7 +2211,7 @@ impl Drop for TestScorer { pub struct TestWalletSource { secret_key: SecretKey, - utxos: Mutex>, + utxos: Mutex>, secp: Secp256k1, } @@ -2220,21 +2220,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) { @@ -2247,12 +2239,12 @@ impl TestWalletSource { 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) + 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"))] @@ -2277,7 +2269,23 @@ 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 { + 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 { diff --git a/test_cases/chanmon_consistency/any_filename_works b/test_cases/chanmon_consistency/any_filename_works new file mode 100644 index 00000000000..e737eafc596 Binary files /dev/null and b/test_cases/chanmon_consistency/any_filename_works differ